diff --git a/docs/guide/workflow.md b/docs/guide/workflow.md index cf1051384..d981c6360 100644 --- a/docs/guide/workflow.md +++ b/docs/guide/workflow.md @@ -89,7 +89,7 @@ This can be repeated to create multiple rods. Supported geometries are listed in The number of element (`n_elements`) and `base_length` determines the spatial discretization `dx`. More detail discussion is included [here](discretization.md). ::: -

3. Define Boundary Conditions, Forcings, Damping and Connections

+

3.a Define Boundary Conditions, Forcings, and Connections

Now that we have added all our rods to `SystemSimulator`, we need to apply relevant boundary conditions. @@ -123,6 +123,34 @@ SystemSimulator.add_forcing_to(rod1).using( ) ``` +One last condition we can define is the connections between rods. See [this page](../api/connections.rst) for in-depth explanations and documentation. + +```python +from elastica.connections import FixedJoint + +# Connect rod 1 and rod 2. '_connect_idx' specifies the node number that +# the connection should be applied to. You are specifying the index of a +# list so you can use -1 to access the last node. +SystemSimulator.connect( + first_rod = rod1, + second_rod = rod2, + first_connect_idx = -1, # Connect to the last node of the first rod. + second_connect_idx = 0 # Connect to first node of the second rod. + ).using( + FixedJoint, # Type of connection between rods + k = 1e5, # Spring constant of force holding rods together (F = k*x) + nu = 0, # Energy dissipation of joint + kt = 5e3 # Rotational stiffness of rod to avoid rods twisting + ) +``` + +:::{note} +Version 0.3.3: The order of the operation is defined by the order of the definition. For example, if you define the connection before the forcing condition, the connection will be applied first. This is less important for the boundary condition, forcing, and connection since they do not depend on each other. However, it is important for friction, contact, or any custom boundary conditions since they depend on other boundary conditions. +For example, friction should be defined after contact, since contact will define the normal force applied to the surface, which friction depends on. Contact should be defined before any other boundary conditions, since aggregated normal force is used to calculate the repelling force. +::: + +

3.b Define Damping

+ Next, if required, in order to numerically stabilize the simulation, we can apply damping to the rods. See [this page](../api/damping.rst) for in-depth explanations and documentation. @@ -146,26 +174,6 @@ SystemSimulator.dampin(rod2).using( ) ``` -One last condition we can define is the connections between rods. See [this page](../api/connections.rst) for in-depth explanations and documentation. - -```python -from elastica.connections import FixedJoint - -# Connect rod 1 and rod 2. '_connect_idx' specifies the node number that -# the connection should be applied to. You are specifying the index of a -# list so you can use -1 to access the last node. -SystemSimulator.connect( - first_rod = rod1, - second_rod = rod2, - first_connect_idx = -1, # Connect to the last node of the first rod. - second_connect_idx = 0 # Connect to first node of the second rod. - ).using( - FixedJoint, # Type of connection between rods - k = 1e5, # Spring constant of force holding rods together (F = k*x) - nu = 0, # Energy dissipation of joint - kt = 5e3 # Rotational stiffness of rod to avoid rods twisting - ) -```

4. Add Callback Functions (optional)

diff --git a/elastica/modules/base_system.py b/elastica/modules/base_system.py index 6dbf0bcc4..3d8664e54 100644 --- a/elastica/modules/base_system.py +++ b/elastica/modules/base_system.py @@ -5,16 +5,19 @@ Basic coordinating for multiple, smaller systems that have an independently integrable interface (i.e. works with symplectic or explicit routines `timestepper.py`.) """ -from typing import Iterable, Callable, AnyStr +from typing import AnyStr, Iterable +from elastica.typing import OperatorType, OperatorCallbackType, OperatorFinalizeType from collections.abc import MutableSequence from elastica.rod import RodBase from elastica.rigidbody import RigidBodyBase from elastica.surface import SurfaceBase -from elastica.modules.memory_block import construct_memory_block_structures from elastica._synchronize_periodic_boundary import _ConstrainPeriodicBoundaries +from .memory_block import construct_memory_block_structures +from .operator_group import OperatorGroupFIFO + class BaseSystemCollection(MutableSequence): """ @@ -29,11 +32,9 @@ class BaseSystemCollection(MutableSequence): _systems: list List of rod-like objects. - """ - - """ Developer Note ----- + Note ---- We can directly subclass a list for the @@ -45,13 +46,11 @@ def __init__(self): # Collection of functions. Each group is executed as a collection at the different steps. # Each component (Forcing, Connection, etc.) registers the executable (callable) function # in the group that that needs to be executed. These should be initialized before mixin. - self._feature_group_synchronize: Iterable[Callable[[float], None]] = [] - self._feature_group_constrain_values: Iterable[Callable[[float], None]] = [] - self._feature_group_constrain_rates: Iterable[Callable[[float], None]] = [] - self._feature_group_callback: Iterable[Callable[[float, int, AnyStr], None]] = ( - [] - ) - self._feature_group_finalize: Iterable[Callable] = [] + self._feature_group_synchronize: Iterable[OperatorType] = OperatorGroupFIFO() + self._feature_group_constrain_values: Iterable[OperatorType] = [] + self._feature_group_constrain_rates: Iterable[OperatorType] = [] + self._feature_group_callback: Iterable[OperatorCallbackType] = [] + self._feature_group_finalize: Iterable[OperatorFinalizeType] = [] # We need to initialize our mixin classes super(BaseSystemCollection, self).__init__() # List of system types/bases that are allowed @@ -169,34 +168,23 @@ def finalize(self): # Toggle the finalize_flag self._finalize_flag = True - # sort _feature_group_synchronize so that _call_contacts is at the end - _call_contacts_index = [] - for idx, feature in enumerate(self._feature_group_synchronize): - if feature.__name__ == "_call_contacts": - _call_contacts_index.append(idx) - - # Move to the _call_contacts to the end of the _feature_group_synchronize list. - for index in _call_contacts_index: - self._feature_group_synchronize.append( - self._feature_group_synchronize.pop(index) - ) def synchronize(self, time: float): # Collection call _feature_group_synchronize - for feature in self._feature_group_synchronize: - feature(time) + for func in self._feature_group_synchronize: + func(time=time) def constrain_values(self, time: float): # Collection call _feature_group_constrain_values - for feature in self._feature_group_constrain_values: - feature(time) + for func in self._feature_group_constrain_values: + func(time=time) def constrain_rates(self, time: float): # Collection call _feature_group_constrain_rates - for feature in self._feature_group_constrain_rates: - feature(time) + for func in self._feature_group_constrain_rates: + func(time=time) def apply_callbacks(self, time: float, current_step: int): # Collection call _feature_group_callback - for feature in self._feature_group_callback: - feature(time, current_step) + for func in self._feature_group_callback: + func(time=time, current_step=current_step) diff --git a/elastica/modules/connections.py b/elastica/modules/connections.py index 8c6cd6267..405fe9220 100644 --- a/elastica/modules/connections.py +++ b/elastica/modules/connections.py @@ -24,7 +24,6 @@ class Connections: def __init__(self): self._connections = [] super(Connections, self).__init__() - self._feature_group_synchronize.append(self._call_connections) self._feature_group_finalize.append(self._finalize_connections) def connect( @@ -60,48 +59,57 @@ def connect( sys_dofs = [self._systems[idx].n_elems for idx in sys_idx] # Create _Connect object, cache it and return to user - _connector = _Connect(*sys_idx, *sys_dofs) - _connector.set_index(first_connect_idx, second_connect_idx) - self._connections.append(_connector) + _connect = _Connect(*sys_idx, *sys_dofs) + _connect.set_index(first_connect_idx, second_connect_idx) + self._connections.append(_connect) + self._feature_group_synchronize.append_id(_connect) - return _connector + return _connect def _finalize_connections(self): # From stored _Connect objects, instantiate the joints and store it - # dev : the first indices stores the # (first rod index, second_rod_idx, connection_idx_on_first_rod, connection_idx_on_second_rod) - # to apply the connections to - # Technically we can use another array but it its one more book-keeping - # step. Being lazy, I put them both in the same array - self._connections[:] = [ - (*connection.id(), connection()) for connection in self._connections - ] + # to apply the connections to. + + for connection in self._connections: + first_sys_idx, second_sys_idx, first_connect_idx, second_connect_idx = ( + connection.id() + ) + connect_instance = connection.instantiate() + + # FIXME: lambda t is included because OperatorType takes time as an argument + def apply_forces(time): + connect_instance.apply_forces( + system_one=self._systems[first_sys_idx], + index_one=first_connect_idx, + system_two=self._systems[second_sys_idx], + index_two=second_connect_idx, + ) + + def apply_torques(time): + connect_instance.apply_torques( + system_one=self._systems[first_sys_idx], + index_one=first_connect_idx, + system_two=self._systems[second_sys_idx], + index_two=second_connect_idx, + ) + + self._feature_group_synchronize.add_operators( + connection, [apply_forces, apply_torques] + ) + + self.warnings(connection) + + self._connections = [] + del self._connections # Need to finally solve CPP here, if we are doing things properly # This is to optimize the call tree for better memory accesses # https://brooksandrew.github.io/simpleblog/articles/intro-to-graph-optimization-solving-cpp/ - def _call_connections(self, *args, **kwargs): - for ( - first_sys_idx, - second_sys_idx, - first_connect_idx, - second_connect_idx, - connection, - ) in self._connections: - connection.apply_forces( - self._systems[first_sys_idx], - first_connect_idx, - self._systems[second_sys_idx], - second_connect_idx, - ) - connection.apply_torques( - self._systems[first_sys_idx], - first_connect_idx, - self._systems[second_sys_idx], - second_connect_idx, - ) + def warnings(self, connection): + pass class _Connect: @@ -152,7 +160,7 @@ def __init__( def set_index(self, first_idx, second_idx): # TODO assert range # First check if the types of first rod idx and second rod idx variable are same. - assert type(first_idx) == type( + assert type(first_idx) is type( second_idx ), "Type of first_connect_idx :{}".format( type(first_idx) @@ -265,7 +273,7 @@ def id(self): self.second_sys_connection_idx, ) - def __call__(self, *args, **kwargs): + def instantiate(self): if not self._connect_cls: raise RuntimeError( "No connections provided to link rod id {0}" diff --git a/elastica/modules/contact.py b/elastica/modules/contact.py index 4f0120c50..efa4c1127 100644 --- a/elastica/modules/contact.py +++ b/elastica/modules/contact.py @@ -5,9 +5,11 @@ Provides the contact interface to apply contact forces between objects (rods, rigid bodies, surfaces). """ - +import logging from elastica.typing import SystemType, AllowedContactType +logger = logging.getLogger(__name__) + class Contact: """ @@ -23,7 +25,6 @@ class Contact: def __init__(self): self._contacts = [] super(Contact, self).__init__() - self._feature_group_synchronize.append(self._call_contacts) self._feature_group_finalize.append(self._finalize_contact) def detect_contact_between( @@ -51,6 +52,7 @@ def detect_contact_between( # Create _Contact object, cache it and return to user _contact = _Contact(*sys_idx) self._contacts.append(_contact) + self._feature_group_synchronize.append_id(_contact) return _contact @@ -61,30 +63,36 @@ def _finalize_contact(self) -> None: # to apply the contacts to # Technically we can use another array but it its one more book-keeping # step. Being lazy, I put them both in the same array - self._contacts[:] = [(*contact.id(), contact()) for contact in self._contacts] - - # check contact order - for ( - first_sys_idx, - second_sys_idx, - contact, - ) in self._contacts: - contact._check_systems_validity( - self._systems[first_sys_idx], - self._systems[second_sys_idx], - ) + for contact in self._contacts: + first_sys_idx, second_sys_idx = contact.id() + contact_instance = contact.instantiate() - def _call_contacts(self, time: float): - for ( - first_sys_idx, - second_sys_idx, - contact, - ) in self._contacts: - contact.apply_contact( + contact_instance._check_systems_validity( self._systems[first_sys_idx], self._systems[second_sys_idx], ) + def apply_contact(time): + contact_instance.apply_contact( + system_one=self._systems[first_sys_idx], + system_two=self._systems[second_sys_idx], + ) + + self._feature_group_synchronize.add_operators(contact, [apply_contact]) + + self.warnings(contact) + + self._contacts = [] + del self._contacts + + def warnings(self, contact): + from elastica.contact_forces import NoContact + + # Classes that should be used last + if not self._feature_group_synchronize.is_last(contact): + if isinstance(contact._contact_cls, NoContact): + logger.warning("Contact features should be instantiated lastly.") + class _Contact: """ @@ -153,7 +161,7 @@ def id(self): self.second_sys_idx, ) - def __call__(self, *args, **kwargs): + def instantiate(self, *args, **kwargs): if not self._contact_cls: raise RuntimeError( "No contacts provided to to establish contact between rod-like object id {0}" diff --git a/elastica/modules/forcing.py b/elastica/modules/forcing.py index d8a46b64b..4d1c0d2d5 100644 --- a/elastica/modules/forcing.py +++ b/elastica/modules/forcing.py @@ -5,7 +5,10 @@ Provides the forcing interface to apply forces and torques to rod-like objects (external point force, muscle torques, etc). """ -from elastica.interaction import AnisotropicFrictionalPlane +import logging +import functools + +logger = logging.getLogger(__name__) class Forcing: @@ -23,7 +26,6 @@ class Forcing: def __init__(self): self._ext_forces_torques = [] super(Forcing, self).__init__() - self._feature_group_synchronize.append(self._call_ext_forces_torques) self._feature_group_finalize.append(self._finalize_forcing) def add_forcing_to(self, system): @@ -46,6 +48,7 @@ def add_forcing_to(self, system): # Create _Constraint object, cache it and return to user _ext_force_torque = _ExtForceTorque(sys_idx) self._ext_forces_torques.append(_ext_force_torque) + self._feature_group_synchronize.append_id(_ext_force_torque) return _ext_force_torque @@ -54,37 +57,29 @@ def _finalize_forcing(self): # inplace : https://stackoverflow.com/a/1208792 # dev : the first index stores the rod index to apply the boundary condition - # to. Technically we can use another array but it its one more book-keeping - # step. Being lazy, I put them both in the same array - self._ext_forces_torques[:] = [ - (ext_force_torque.id(), ext_force_torque()) - for ext_force_torque in self._ext_forces_torques - ] - - # Sort from lowest id to highest id for potentially better memory access - # _ext_forces_torques contains list of tuples. First element of tuple is - # rod number and following elements are the type of boundary condition such as - # [(0, NoForces, GravityForces), (1, UniformTorques), ... ] - # Thus using lambda we iterate over the list of tuples and use rod number (x[0]) - # to sort _ext_forces_torques. - self._ext_forces_torques.sort(key=lambda x: x[0]) - - # Find if there are any friction plane forcing, if add them to the end of the list, - # since friction planes uses external forces. - friction_plane_index = [] - for idx, ext_force_torque in enumerate(self._ext_forces_torques): - if isinstance(ext_force_torque[1], AnisotropicFrictionalPlane): - friction_plane_index.append(idx) - - # Move to the friction forces to the end of the external force and torques list. - for index in friction_plane_index: - self._ext_forces_torques.append(self._ext_forces_torques.pop(index)) - - def _call_ext_forces_torques(self, time, *args, **kwargs): - for sys_id, ext_force_torque in self._ext_forces_torques: - ext_force_torque.apply_forces(self._systems[sys_id], time, *args, **kwargs) - ext_force_torque.apply_torques(self._systems[sys_id], time, *args, **kwargs) - # TODO Apply torque, see if necessary + # to. + for external_force_and_torque in self._ext_forces_torques: + sys_id = external_force_and_torque.id() + forcing_instance = external_force_and_torque.instantiate() + + apply_forces = functools.partial( + forcing_instance.apply_forces, system=self._systems[sys_id] + ) + apply_torques = functools.partial( + forcing_instance.apply_torques, system=self._systems[sys_id] + ) + + self._feature_group_synchronize.add_operators( + external_force_and_torque, [apply_forces, apply_torques] + ) + + self.warnings(external_force_and_torque) + + self._ext_forces_torques = [] + del self._ext_forces_torques + + def warnings(self, external_force_and_torque): + pass class _ExtForceTorque: @@ -146,7 +141,7 @@ def using(self, forcing_cls, *args, **kwargs): def id(self): return self._sys_idx - def __call__(self, *args, **kwargs): + def instantiate(self): """Constructs a constraint after checks Parameters diff --git a/elastica/modules/operator_group.py b/elastica/modules/operator_group.py new file mode 100644 index 000000000..9e5524846 --- /dev/null +++ b/elastica/modules/operator_group.py @@ -0,0 +1,62 @@ +from elastica.typing import OperatorType + +from collections.abc import Iterable + +import itertools + + +class OperatorGroupFIFO(Iterable): + """ + A class to store the features and their corresponding operators in a FIFO manner. + + Examples + -------- + >>> operator_group = OperatorGroupFIFO() + >>> operator_group.append_id(obj_1) + >>> operator_group.append_id(obj_2) + >>> operator_group.add_operators(obj_1, [ADD, SUBTRACT]) + >>> operator_group.add_operators(obj_2, [SUBTRACT, MULTIPLY]) + >>> list(operator_group) + [OperatorType.ADD, OperatorType.SUBTRACT, OperatorType.SUBTRACT, OperatorType.MULTIPLY] + + Attributes + ---------- + _operator_collection : list[list[OperatorType]] + A list of lists of operators. Each list of operators corresponds to a feature. + _operator_ids : list[int] + A list of ids of the features. + + Methods + ------- + append_id(feature) + Appends the id of the feature to the list of ids. + add_operators(feature, operators) + Adds the operators to the list of operators corresponding to the feature. + is_last(feature) + Checks if the feature is the last feature in the FIFO. + Used to check if the specific feature is the last feature in the FIFO. + """ + + def __init__(self): + self._operator_collection: list[list[OperatorType]] = [] + self._operator_ids: list[int] = [] + + def __iter__(self) -> OperatorType: + """Returns an operator iterator to satisfy the Iterable protocol.""" + operator_chain = itertools.chain.from_iterable(self._operator_collection) + for operator in operator_chain: + yield operator + + def append_id(self, feature): + """Appends the id of the feature to the list of ids.""" + self._operator_ids.append(id(feature)) + self._operator_collection.append([]) + + def add_operators(self, feature, operators: list[OperatorType]): + """Adds the operators to the list of operators corresponding to the feature.""" + idx = self._operator_ids.index(id(feature)) + self._operator_collection[idx].extend(operators) + + def is_last(self, feature) -> bool: + """Checks if the feature is the last feature in the FIFO.""" + return id(feature) == self._operator_ids[-1] diff --git a/elastica/typing.py b/elastica/typing.py index d70a16433..4307801e6 100644 --- a/elastica/typing.py +++ b/elastica/typing.py @@ -2,8 +2,12 @@ from elastica.rigidbody import RigidBodyBase from elastica.surface import SurfaceBase -from typing import Type, Union +from typing import Type, Union, TypeAlias, Callable RodType = Type[RodBase] SystemType = Union[RodType, Type[RigidBodyBase]] AllowedContactType = Union[SystemType, Type[SurfaceBase]] + +OperatorType: TypeAlias = Callable[[float], None] +OperatorCallbackType: TypeAlias = Callable[[float, int], None] +OperatorFinalizeType: TypeAlias = Callable diff --git a/tests/test_modules/test_base_system.py b/tests/test_modules/test_base_system.py index ebb3b7c3e..76f63cd4e 100644 --- a/tests/test_modules/test_base_system.py +++ b/tests/test_modules/test_base_system.py @@ -197,7 +197,18 @@ def test_forcing(self, load_collection, legal_forces): simulator_class.add_forcing_to(rod).using(legal_forces) simulator_class.finalize() # After finalize check if the created forcing object is instance of the class we have given. - assert isinstance(simulator_class._ext_forces_torques[-1][-1], legal_forces) + assert isinstance( + simulator_class._feature_group_synchronize._operator_collection[-1][ + -1 + ].func.__self__, + legal_forces, + ) + assert isinstance( + simulator_class._feature_group_synchronize._operator_collection[-1][ + -2 + ].func.__self__, + legal_forces, + ) # TODO: this is a dummy test for synchronize find a better way to test them simulator_class.synchronize(time=0) diff --git a/tests/test_modules/test_connections.py b/tests/test_modules/test_connections.py index bdc2715a4..334ece41b 100644 --- a/tests/test_modules/test_connections.py +++ b/tests/test_modules/test_connections.py @@ -150,7 +150,7 @@ def test_call_without_setting_connect_throws_runtime_error(self, load_connect): connect = load_connect with pytest.raises(RuntimeError) as excinfo: - connect() + connect.instantiate() assert "No connections provided" in str(excinfo.value) def test_call_improper_args_throws(self, load_connect): @@ -173,7 +173,7 @@ def mock_init(self, *args, **kwargs): # Actual test is here, this should not throw with pytest.raises(TypeError) as excinfo: - _ = connect() + _ = connect.instantiate() assert ( r"Unable to construct connection class.\nDid you provide all necessary joint properties?" == str(excinfo.value) @@ -327,21 +327,18 @@ def mock_init(self, *args, **kwargs): def test_connect_finalize_correctness(self, load_rod_with_connects): system_collection_with_connections, connect_cls = load_rod_with_connects + connect = system_collection_with_connections._connections[0] + assert connect._connect_cls == connect_cls system_collection_with_connections._finalize_connections() + assert ( + system_collection_with_connections._feature_group_synchronize._operator_ids[ + 0 + ] + == id(connect) + ) - for ( - fidx, - sidx, - fconnect, - sconnect, - connect, - ) in system_collection_with_connections._connections: - assert type(fidx) is int - assert type(sidx) is int - assert fconnect is None - assert sconnect is None - assert type(connect) is connect_cls + assert not hasattr(system_collection_with_connections, "_connections") @pytest.fixture def load_rod_with_connects_and_indices(self, load_system_with_connects): @@ -392,17 +389,17 @@ def test_connect_call_on_systems(self, load_rod_with_connects_and_indices): system_collection_with_connections_and_indices, connect_cls, ) = load_rod_with_connects_and_indices + mock_connections = [ + c for c in system_collection_with_connections_and_indices._connections + ] system_collection_with_connections_and_indices._finalize_connections() - system_collection_with_connections_and_indices._call_connections() - - for ( - fidx, - sidx, - fconnect, - sconnect, - connect, - ) in system_collection_with_connections_and_indices._connections: + system_collection_with_connections_and_indices.synchronize(0) + + for connection in mock_connections: + fidx, sidx, fconnect, sconnect = connection.id() + connect = connection.instantiate() + end_distance_vector = ( system_collection_with_connections_and_indices._systems[ sidx diff --git a/tests/test_modules/test_contact.py b/tests/test_modules/test_contact.py index 3979bebee..82c41f696 100644 --- a/tests/test_modules/test_contact.py +++ b/tests/test_modules/test_contact.py @@ -48,7 +48,7 @@ def test_call_without_setting_contact_throws_runtime_error(self, load_contact): contact = load_contact with pytest.raises(RuntimeError) as excinfo: - contact() + contact.instantiate() assert "No contacts provided to to establish contact between rod-like object id {0} and {1}, but a Contact was intended as per code. Did you forget to call the `using` method?".format( *contact.id() ) == str( @@ -75,7 +75,7 @@ def mock_init(self, *args, **kwargs): # Actual test is here, this should not throw with pytest.raises(TypeError) as excinfo: - _ = contact() + _ = contact.instantiate() assert ( r"Unable to construct contact class.\nDid you provide all necessary contact properties?" == str(excinfo.value) @@ -260,13 +260,15 @@ def mock_init(self, *args, **kwargs): def test_contact_finalize_correctness(self, load_rod_with_contacts): system_collection_with_contacts, contact_cls = load_rod_with_contacts + contact = system_collection_with_contacts._contacts[0].instantiate() + fidx, sidx = system_collection_with_contacts._contacts[0].id() system_collection_with_contacts._finalize_contact() - for fidx, sidx, contact in system_collection_with_contacts._contacts: - assert type(fidx) is int - assert type(sidx) is int - assert type(contact) is contact_cls + assert not hasattr(system_collection_with_contacts, "_contacts") + assert type(fidx) is int + assert type(sidx) is int + assert type(contact) is contact_cls @pytest.fixture def load_contact_objects_with_incorrect_order(self, load_system_with_contacts): @@ -339,19 +341,18 @@ def load_system_with_rods_in_contact(self, load_system_with_contacts): return system_collection_with_rods_in_contact def test_contact_call_on_systems(self, load_system_with_rods_in_contact): + from elastica.contact_forces import _calculate_contact_forces_rod_rod system_collection_with_rods_in_contact = load_system_with_rods_in_contact + mock_contacts = [c for c in system_collection_with_rods_in_contact._contacts] system_collection_with_rods_in_contact._finalize_contact() - system_collection_with_rods_in_contact._call_contacts(time=0) + system_collection_with_rods_in_contact.synchronize(time=0) - from elastica.contact_forces import _calculate_contact_forces_rod_rod + for _contact in mock_contacts: + fidx, sidx = _contact.id() + contact = _contact.instantiate() - for ( - fidx, - sidx, - contact, - ) in system_collection_with_rods_in_contact._contacts: system_one = system_collection_with_rods_in_contact._systems[fidx] system_two = system_collection_with_rods_in_contact._systems[sidx] external_forces_system_one = np.zeros_like(system_one.external_forces) diff --git a/tests/test_modules/test_feature_grouping.py b/tests/test_modules/test_feature_grouping.py new file mode 100644 index 000000000..76a281a83 --- /dev/null +++ b/tests/test_modules/test_feature_grouping.py @@ -0,0 +1,67 @@ +from elastica.modules.operator_group import OperatorGroupFIFO + + +def test_add_ids(): + group = OperatorGroupFIFO() + group.append_id(1) + group.append_id(2) + group.append_id(3) + + assert group._operator_ids == [id(1), id(2), id(3)] + + +def test_add_operators(): + group = OperatorGroupFIFO() + group.append_id(1) + group.add_operators(1, [1, 2, 3]) + group.append_id(2) + group.add_operators(2, [4, 5, 6]) + group.append_id(3) + group.add_operators(3, [7, 8, 9]) + + assert group._operator_collection == [[1, 2, 3], [4, 5, 6], [7, 8, 9]] + assert group._operator_ids == [id(1), id(2), id(3)] + + group.append_id(4) + group.add_operators(4, [10, 11, 12]) + + assert group._operator_collection == [ + [1, 2, 3], + [4, 5, 6], + [7, 8, 9], + [10, 11, 12], + ] + assert group._operator_ids == [id(1), id(2), id(3), id(4)] + + +def test_grouping(): + group = OperatorGroupFIFO() + group.append_id(1) + group.add_operators(1, [1, 2, 3]) + group.append_id(2) + group.add_operators(2, [4, 5, 6]) + group.append_id(3) + group.add_operators(3, [7, 8, 9]) + + assert list(group) == [1, 2, 3, 4, 5, 6, 7, 8, 9] + + group.append_id(4) + group.add_operators(4, [10, 11, 12]) + + assert list(group) == [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] + + group.append_id(1) + group.add_operators(1, [13, 14, 15]) + + assert list(group) == [1, 2, 3, 13, 14, 15, 4, 5, 6, 7, 8, 9, 10, 11, 12] + + +def test_is_last(): + group = OperatorGroupFIFO() + group.append_id(1) + group.add_operators(1, [1, 2, 3]) + group.append_id(2) + group.add_operators(2, [4, 5, 6]) + + assert group.is_last(1) == False + assert group.is_last(2) == True diff --git a/tests/test_modules/test_forcing.py b/tests/test_modules/test_forcing.py index 67732767d..bd384fc6c 100644 --- a/tests/test_modules/test_forcing.py +++ b/tests/test_modules/test_forcing.py @@ -39,7 +39,7 @@ def test_call_without_setting_forcing_throws_runtime_error(self, load_forcing): forcing = load_forcing with pytest.raises(RuntimeError) as excinfo: - forcing(None) # None is the rod/system parameter + forcing.instantiate() # None is the rod/system parameter assert "No forcing" in str(excinfo.value) def test_call_improper_args_throws(self, load_forcing): @@ -62,7 +62,7 @@ def mock_init(self, *args, **kwargs): # Actual test is here, this should not throw with pytest.raises(TypeError) as excinfo: - _ = forcing() + _ = forcing.instantiate() assert "Unable to construct" in str(excinfo.value) @@ -166,7 +166,7 @@ def mock_init(self, *args, **kwargs): return scwf, MockForcing - def test_friction_plane_forcing_class_sorting(self, load_system_with_forcings): + def test_friction_plane_forcing_class(self, load_system_with_forcings): scwf = load_system_with_forcings @@ -196,19 +196,24 @@ def mock_init(self, *args, **kwargs): ) scwf.add_forcing_to(1).using(MockForcing, 2, 42) # index based forcing + # Now check if the Anisotropic friction and the MockForcing are in the list + assert scwf._ext_forces_torques[-1]._forcing_cls == MockForcing + assert scwf._ext_forces_torques[-2]._forcing_cls == AnisotropicFrictionalPlane scwf._finalize_forcing() - - # Now check if the Anisotropic friction is the last forcing class - assert isinstance(scwf._ext_forces_torques[-1][-1], AnisotropicFrictionalPlane) + assert not hasattr(scwf, "_ext_forces_torques") def test_constrain_finalize_correctness(self, load_rod_with_forcings): scwf, forcing_cls = load_rod_with_forcings + forcing_features = [f for f in scwf._ext_forces_torques] scwf._finalize_forcing() + assert not hasattr(scwf, "_ext_forces_torques") - for x, y in scwf._ext_forces_torques: - assert type(x) is int - assert type(y) is forcing_cls + for _forcing in forcing_features: + x = _forcing.id() + y = _forcing.instantiate() + assert isinstance(x, int) + assert isinstance(y, forcing_cls) @pytest.mark.xfail def test_constrain_finalize_sorted(self, load_rod_with_forcings):