From 44d836ab8416bcb10c72a44d9efc5fbd02acad44 Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Thu, 9 May 2024 03:00:17 -0500 Subject: [PATCH 1/7] update: expand synchronize operation order to be determined in the order of the specification --- elastica/modules/base_system.py | 46 +++++++++-------------- elastica/modules/connections.py | 62 ++++++++++++++++--------------- elastica/modules/contact.py | 38 ++++++++----------- elastica/modules/feature_group.py | 27 ++++++++++++++ elastica/modules/forcing.py | 41 ++++++++++---------- elastica/typing.py | 6 ++- 6 files changed, 118 insertions(+), 102 deletions(-) create mode 100644 elastica/modules/feature_group.py diff --git a/elastica/modules/base_system.py b/elastica/modules/base_system.py index 6dbf0bcc4..af16f0f15 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 .feature_group import FeatureGroupFIFO + class BaseSystemCollection(MutableSequence): """ @@ -45,13 +48,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] = FeatureGroupFIFO() + 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 +170,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) 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) 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) 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, current_step) diff --git a/elastica/modules/connections.py b/elastica/modules/connections.py index 8c6cd6267..935b9ed50 100644 --- a/elastica/modules/connections.py +++ b/elastica/modules/connections.py @@ -5,6 +5,7 @@ Provides the connections interface to connect entities (rods, rigid bodies) using joints (see `joints.py`). """ +import functools import numpy as np from elastica.joint import FreeJoint @@ -24,7 +25,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( @@ -63,6 +63,7 @@ def connect( _connector = _Connect(*sys_idx, *sys_dofs) _connector.set_index(first_connect_idx, second_connect_idx) self._connections.append(_connector) + self._feature_group_synchronize.append_id(_connector) return _connector @@ -71,38 +72,41 @@ def _finalize_connections(self): # 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): + return functools.partial( + 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): + return functools.partial( + 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] + ) # 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, - ) - class _Connect: """ @@ -265,7 +269,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..6585e6a88 100644 --- a/elastica/modules/contact.py +++ b/elastica/modules/contact.py @@ -5,7 +5,7 @@ Provides the contact interface to apply contact forces between objects (rods, rigid bodies, surfaces). """ - +import functools from elastica.typing import SystemType, AllowedContactType @@ -23,7 +23,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 +50,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 +61,24 @@ 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): + return functools.partial( + 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]) + class _Contact: """ @@ -153,7 +147,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/feature_group.py b/elastica/modules/feature_group.py new file mode 100644 index 000000000..fd4dbcc0f --- /dev/null +++ b/elastica/modules/feature_group.py @@ -0,0 +1,27 @@ +from typing import Callable +from elastica.typing import OperatorType + +from collection.abc import Iterable + +import itertools + + +class FeatureGroupFIFO(Iterable): + def __init__(self): + self._operator_collection: list[list[OperatorType]] = [] + self._operator_ids: list[int] = [] + + def __iter__(self) -> Callable[[...], None]: + if not self._operator_collection: + raise RuntimeError("Feature group is not instantiated.") + operator_chain = itertools.chain(self._operator_collection) + for operator in operator_chain: + yield operator + + def append_id(self, feature): + self._operator_ids.append(id(feature)) + self._operator_collection.append([]) + + def add_operators(self, feature, operators: list[OperatorType]): + idx = self._operator_idx.index(feature) + self._operator_collection[idx].extend(operators) diff --git a/elastica/modules/forcing.py b/elastica/modules/forcing.py index d8a46b64b..0a4864484 100644 --- a/elastica/modules/forcing.py +++ b/elastica/modules/forcing.py @@ -5,6 +5,7 @@ Provides the forcing interface to apply forces and torques to rod-like objects (external point force, muscle torques, etc). """ +import functools from elastica.interaction import AnisotropicFrictionalPlane @@ -23,7 +24,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 +46,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,21 +55,23 @@ 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]) + # to. + for ext_force_torque in self._ext_forces_torques: + sys_id = ext_force_torque.id() + forcing_instance = ext_force_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( + ext_force_torque, [apply_forces, apply_torques] + ) + + # TODO: remove: we decided to let user to fully decide the order of operations # 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 = [] @@ -80,12 +83,6 @@ def _finalize_forcing(self): 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 - class _ExtForceTorque: """ @@ -146,7 +143,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/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 From 15ba162fbf4822895520d2bd4b3a406f1eec0ffe Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Thu, 9 May 2024 03:22:20 -0500 Subject: [PATCH 2/7] remove dev ordering AnisotropicFrictionPlane --- elastica/modules/feature_group.py | 4 ++-- elastica/modules/forcing.py | 13 ------------- 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/elastica/modules/feature_group.py b/elastica/modules/feature_group.py index fd4dbcc0f..9dd879558 100644 --- a/elastica/modules/feature_group.py +++ b/elastica/modules/feature_group.py @@ -1,7 +1,7 @@ from typing import Callable from elastica.typing import OperatorType -from collection.abc import Iterable +from collections.abc import Iterable import itertools @@ -23,5 +23,5 @@ def append_id(self, feature): self._operator_collection.append([]) def add_operators(self, feature, operators: list[OperatorType]): - idx = self._operator_idx.index(feature) + idx = self._operator_ids.index(feature) self._operator_collection[idx].extend(operators) diff --git a/elastica/modules/forcing.py b/elastica/modules/forcing.py index 0a4864484..1553d85be 100644 --- a/elastica/modules/forcing.py +++ b/elastica/modules/forcing.py @@ -6,7 +6,6 @@ (external point force, muscle torques, etc). """ import functools -from elastica.interaction import AnisotropicFrictionalPlane class Forcing: @@ -71,18 +70,6 @@ def _finalize_forcing(self): ext_force_torque, [apply_forces, apply_torques] ) - # TODO: remove: we decided to let user to fully decide the order of operations - # 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)) - class _ExtForceTorque: """ From 239fb0c491c6b962b6656812e43067d664453bbf Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Thu, 9 May 2024 12:23:42 -0500 Subject: [PATCH 3/7] test: fix test for new synchronize ordering policy --- elastica/modules/base_system.py | 12 +++---- elastica/modules/connections.py | 23 +++++++------- elastica/modules/contact.py | 7 +++-- elastica/modules/feature_group.py | 7 ++--- elastica/modules/forcing.py | 3 ++ tests/test_modules/test_base_system.py | 13 +++++++- tests/test_modules/test_connections.py | 43 ++++++++++++-------------- tests/test_modules/test_contact.py | 27 ++++++++-------- tests/test_modules/test_forcing.py | 23 ++++++++------ 9 files changed, 86 insertions(+), 72 deletions(-) diff --git a/elastica/modules/base_system.py b/elastica/modules/base_system.py index af16f0f15..265ebc152 100644 --- a/elastica/modules/base_system.py +++ b/elastica/modules/base_system.py @@ -32,11 +32,9 @@ class BaseSystemCollection(MutableSequence): _systems: list List of rod-like objects. - """ - - """ Developer Note ----- + Note ---- We can directly subclass a list for the @@ -174,19 +172,19 @@ def finalize(self): def synchronize(self, time: float): # Collection call _feature_group_synchronize for func in self._feature_group_synchronize: - func(time) + func(time=time) def constrain_values(self, time: float): # Collection call _feature_group_constrain_values for func in self._feature_group_constrain_values: - func(time) + func(time=time) def constrain_rates(self, time: float): # Collection call _feature_group_constrain_rates for func in self._feature_group_constrain_rates: - func(time) + func(time=time) def apply_callbacks(self, time: float, current_step: int): # Collection call _feature_group_callback for func in self._feature_group_callback: - func(time, current_step) + func(time=time, current_step=current_step) diff --git a/elastica/modules/connections.py b/elastica/modules/connections.py index 935b9ed50..2c93b7aa2 100644 --- a/elastica/modules/connections.py +++ b/elastica/modules/connections.py @@ -5,7 +5,6 @@ Provides the connections interface to connect entities (rods, rigid bodies) using joints (see `joints.py`). """ -import functools import numpy as np from elastica.joint import FreeJoint @@ -60,16 +59,15 @@ 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) - self._feature_group_synchronize.append_id(_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. @@ -82,8 +80,7 @@ def _finalize_connections(self): # FIXME: lambda t is included because OperatorType takes time as an argument def apply_forces(time): - return functools.partial( - connect_instance.apply_forces, + connect_instance.apply_forces( system_one=self._systems[first_sys_idx], index_one=first_connect_idx, system_two=self._systems[second_sys_idx], @@ -91,8 +88,7 @@ def apply_forces(time): ) def apply_torques(time): - return functools.partial( - connect_instance.apply_torques, + connect_instance.apply_torques( system_one=self._systems[first_sys_idx], index_one=first_connect_idx, system_two=self._systems[second_sys_idx], @@ -103,6 +99,9 @@ def apply_torques(time): connection, [apply_forces, apply_torques] ) + 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/ @@ -156,7 +155,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) diff --git a/elastica/modules/contact.py b/elastica/modules/contact.py index 6585e6a88..5e333aac9 100644 --- a/elastica/modules/contact.py +++ b/elastica/modules/contact.py @@ -5,7 +5,6 @@ Provides the contact interface to apply contact forces between objects (rods, rigid bodies, surfaces). """ -import functools from elastica.typing import SystemType, AllowedContactType @@ -71,14 +70,16 @@ def _finalize_contact(self) -> None: ) def apply_contact(time): - return functools.partial( - contact_instance.apply_contact, + 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._contacts = [] + del self._contacts + class _Contact: """ diff --git a/elastica/modules/feature_group.py b/elastica/modules/feature_group.py index 9dd879558..9578f954d 100644 --- a/elastica/modules/feature_group.py +++ b/elastica/modules/feature_group.py @@ -1,4 +1,3 @@ -from typing import Callable from elastica.typing import OperatorType from collections.abc import Iterable @@ -11,10 +10,10 @@ def __init__(self): self._operator_collection: list[list[OperatorType]] = [] self._operator_ids: list[int] = [] - def __iter__(self) -> Callable[[...], None]: + def __iter__(self) -> OperatorType: if not self._operator_collection: raise RuntimeError("Feature group is not instantiated.") - operator_chain = itertools.chain(self._operator_collection) + operator_chain = itertools.chain.from_iterable(self._operator_collection) for operator in operator_chain: yield operator @@ -23,5 +22,5 @@ def append_id(self, feature): self._operator_collection.append([]) def add_operators(self, feature, operators: list[OperatorType]): - idx = self._operator_ids.index(feature) + idx = self._operator_ids.index(id(feature)) self._operator_collection[idx].extend(operators) diff --git a/elastica/modules/forcing.py b/elastica/modules/forcing.py index 1553d85be..e6e43f133 100644 --- a/elastica/modules/forcing.py +++ b/elastica/modules/forcing.py @@ -70,6 +70,9 @@ def _finalize_forcing(self): ext_force_torque, [apply_forces, apply_torques] ) + self._ext_forces_torques = [] + del self._ext_forces_torques + class _ExtForceTorque: """ 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_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): From f0fc544bb8be1458b03b41a5458ff453b04fea33 Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Thu, 9 May 2024 12:58:42 -0500 Subject: [PATCH 4/7] docs: add note on operation order --- docs/guide/workflow.md | 50 ++++++++++++++++++++++++------------------ 1 file changed, 29 insertions(+), 21 deletions(-) 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)

From 5a68fc6c367eae2861f6b54e1e425f782fdc0d24 Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Fri, 10 May 2024 01:00:29 -0500 Subject: [PATCH 5/7] test: add unittest for grouping operator features --- elastica/modules/feature_group.py | 33 +++++++++++- tests/test_modules/test_feature_grouping.py | 56 +++++++++++++++++++++ 2 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 tests/test_modules/test_feature_grouping.py diff --git a/elastica/modules/feature_group.py b/elastica/modules/feature_group.py index 9578f954d..55970a141 100644 --- a/elastica/modules/feature_group.py +++ b/elastica/modules/feature_group.py @@ -6,21 +6,50 @@ class FeatureGroupFIFO(Iterable): + """ + A class to store the features and their corresponding operators in a FIFO manner. + + Examples + -------- + >>> feature_group = FeatureGroupFIFO() + >>> feature_group.append_id(obj_1) + >>> feature_group.append_id(obj_2) + >>> feature_group.add_operators(obj_1, [OperatorType.ADD, OperatorType.SUBTRACT]) + >>> feature_group.add_operators(obj_2, [OperatorType.SUBTRACT, OperatorType.MULTIPLY]) + >>> list(feature_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. + """ + def __init__(self): self._operator_collection: list[list[OperatorType]] = [] self._operator_ids: list[int] = [] def __iter__(self) -> OperatorType: - if not self._operator_collection: - raise RuntimeError("Feature group is not instantiated.") + """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) diff --git a/tests/test_modules/test_feature_grouping.py b/tests/test_modules/test_feature_grouping.py new file mode 100644 index 000000000..c100401a2 --- /dev/null +++ b/tests/test_modules/test_feature_grouping.py @@ -0,0 +1,56 @@ +from elastica.modules.feature_group import FeatureGroupFIFO + + +def test_add_ids(): + feature_group = FeatureGroupFIFO() + feature_group.append_id(1) + feature_group.append_id(2) + feature_group.append_id(3) + + assert feature_group._operator_ids == [id(1), id(2), id(3)] + + +def test_add_operators(): + feature_group = FeatureGroupFIFO() + feature_group.append_id(1) + feature_group.add_operators(1, [1, 2, 3]) + feature_group.append_id(2) + feature_group.add_operators(2, [4, 5, 6]) + feature_group.append_id(3) + feature_group.add_operators(3, [7, 8, 9]) + + assert feature_group._operator_collection == [[1, 2, 3], [4, 5, 6], [7, 8, 9]] + assert feature_group._operator_ids == [id(1), id(2), id(3)] + + feature_group.append_id(4) + feature_group.add_operators(4, [10, 11, 12]) + + assert feature_group._operator_collection == [ + [1, 2, 3], + [4, 5, 6], + [7, 8, 9], + [10, 11, 12], + ] + assert feature_group._operator_ids == [id(1), id(2), id(3), id(4)] + + +def test_grouping(): + feature_group = FeatureGroupFIFO() + feature_group.append_id(1) + feature_group.add_operators(1, [1, 2, 3]) + feature_group.append_id(2) + feature_group.add_operators(2, [4, 5, 6]) + feature_group.append_id(3) + feature_group.add_operators(3, [7, 8, 9]) + + assert list(feature_group) == [1, 2, 3, 4, 5, 6, 7, 8, 9] + + feature_group.append_id(4) + feature_group.add_operators(4, [10, 11, 12]) + + assert list(feature_group) == [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] + + feature_group.append_id(1) + feature_group.add_operators(1, [13, 14, 15]) + + assert list(feature_group) == [1, 2, 3, 13, 14, 15, 4, 5, 6, 7, 8, 9, 10, 11, 12] From 2e5597b4f904b9810c77f1ad192d94f0babfbee2 Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Fri, 10 May 2024 09:55:44 -0500 Subject: [PATCH 6/7] doc: change word to avoid misusage --- elastica/modules/feature_group.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/elastica/modules/feature_group.py b/elastica/modules/feature_group.py index 55970a141..2358eee4d 100644 --- a/elastica/modules/feature_group.py +++ b/elastica/modules/feature_group.py @@ -14,8 +14,8 @@ class FeatureGroupFIFO(Iterable): >>> feature_group = FeatureGroupFIFO() >>> feature_group.append_id(obj_1) >>> feature_group.append_id(obj_2) - >>> feature_group.add_operators(obj_1, [OperatorType.ADD, OperatorType.SUBTRACT]) - >>> feature_group.add_operators(obj_2, [OperatorType.SUBTRACT, OperatorType.MULTIPLY]) + >>> feature_group.add_operators(obj_1, [ADD, SUBTRACT]) + >>> feature_group.add_operators(obj_2, [SUBTRACT, MULTIPLY]) >>> list(feature_group) [OperatorType.ADD, OperatorType.SUBTRACT, OperatorType.SUBTRACT, OperatorType.MULTIPLY] From 2a6ddfa6955b2d0ad6c7fdef6b45d3902bdd6e0c Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Fri, 10 May 2024 20:35:32 -0500 Subject: [PATCH 7/7] update: add warning messages when contact and friction is not the last operation feature --- elastica/modules/base_system.py | 4 +- elastica/modules/connections.py | 5 ++ elastica/modules/contact.py | 13 ++++ elastica/modules/forcing.py | 16 +++- .../{feature_group.py => operator_group.py} | 21 +++-- tests/test_modules/test_feature_grouping.py | 77 +++++++++++-------- 6 files changed, 90 insertions(+), 46 deletions(-) rename elastica/modules/{feature_group.py => operator_group.py} (71%) diff --git a/elastica/modules/base_system.py b/elastica/modules/base_system.py index 265ebc152..3d8664e54 100644 --- a/elastica/modules/base_system.py +++ b/elastica/modules/base_system.py @@ -16,7 +16,7 @@ from elastica._synchronize_periodic_boundary import _ConstrainPeriodicBoundaries from .memory_block import construct_memory_block_structures -from .feature_group import FeatureGroupFIFO +from .operator_group import OperatorGroupFIFO class BaseSystemCollection(MutableSequence): @@ -46,7 +46,7 @@ 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[OperatorType] = FeatureGroupFIFO() + 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] = [] diff --git a/elastica/modules/connections.py b/elastica/modules/connections.py index 2c93b7aa2..405fe9220 100644 --- a/elastica/modules/connections.py +++ b/elastica/modules/connections.py @@ -99,6 +99,8 @@ def apply_torques(time): connection, [apply_forces, apply_torques] ) + self.warnings(connection) + self._connections = [] del self._connections @@ -106,6 +108,9 @@ def apply_torques(time): # This is to optimize the call tree for better memory accesses # https://brooksandrew.github.io/simpleblog/articles/intro-to-graph-optimization-solving-cpp/ + def warnings(self, connection): + pass + class _Connect: """ diff --git a/elastica/modules/contact.py b/elastica/modules/contact.py index 5e333aac9..efa4c1127 100644 --- a/elastica/modules/contact.py +++ b/elastica/modules/contact.py @@ -5,8 +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: """ @@ -77,9 +80,19 @@ def apply_contact(time): 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: """ diff --git a/elastica/modules/forcing.py b/elastica/modules/forcing.py index e6e43f133..4d1c0d2d5 100644 --- a/elastica/modules/forcing.py +++ b/elastica/modules/forcing.py @@ -5,8 +5,11 @@ Provides the forcing interface to apply forces and torques to rod-like objects (external point force, muscle torques, etc). """ +import logging import functools +logger = logging.getLogger(__name__) + class Forcing: """ @@ -55,9 +58,9 @@ def _finalize_forcing(self): # dev : the first index stores the rod index to apply the boundary condition # to. - for ext_force_torque in self._ext_forces_torques: - sys_id = ext_force_torque.id() - forcing_instance = ext_force_torque.instantiate() + 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] @@ -67,12 +70,17 @@ def _finalize_forcing(self): ) self._feature_group_synchronize.add_operators( - ext_force_torque, [apply_forces, apply_torques] + 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: """ diff --git a/elastica/modules/feature_group.py b/elastica/modules/operator_group.py similarity index 71% rename from elastica/modules/feature_group.py rename to elastica/modules/operator_group.py index 2358eee4d..9e5524846 100644 --- a/elastica/modules/feature_group.py +++ b/elastica/modules/operator_group.py @@ -5,18 +5,18 @@ import itertools -class FeatureGroupFIFO(Iterable): +class OperatorGroupFIFO(Iterable): """ A class to store the features and their corresponding operators in a FIFO manner. Examples -------- - >>> feature_group = FeatureGroupFIFO() - >>> feature_group.append_id(obj_1) - >>> feature_group.append_id(obj_2) - >>> feature_group.add_operators(obj_1, [ADD, SUBTRACT]) - >>> feature_group.add_operators(obj_2, [SUBTRACT, MULTIPLY]) - >>> list(feature_group) + >>> 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 @@ -32,6 +32,9 @@ class FeatureGroupFIFO(Iterable): 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): @@ -53,3 +56,7 @@ 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/tests/test_modules/test_feature_grouping.py b/tests/test_modules/test_feature_grouping.py index c100401a2..76a281a83 100644 --- a/tests/test_modules/test_feature_grouping.py +++ b/tests/test_modules/test_feature_grouping.py @@ -1,56 +1,67 @@ -from elastica.modules.feature_group import FeatureGroupFIFO +from elastica.modules.operator_group import OperatorGroupFIFO def test_add_ids(): - feature_group = FeatureGroupFIFO() - feature_group.append_id(1) - feature_group.append_id(2) - feature_group.append_id(3) + group = OperatorGroupFIFO() + group.append_id(1) + group.append_id(2) + group.append_id(3) - assert feature_group._operator_ids == [id(1), id(2), id(3)] + assert group._operator_ids == [id(1), id(2), id(3)] def test_add_operators(): - feature_group = FeatureGroupFIFO() - feature_group.append_id(1) - feature_group.add_operators(1, [1, 2, 3]) - feature_group.append_id(2) - feature_group.add_operators(2, [4, 5, 6]) - feature_group.append_id(3) - feature_group.add_operators(3, [7, 8, 9]) + 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 feature_group._operator_collection == [[1, 2, 3], [4, 5, 6], [7, 8, 9]] - assert feature_group._operator_ids == [id(1), id(2), id(3)] + assert group._operator_collection == [[1, 2, 3], [4, 5, 6], [7, 8, 9]] + assert group._operator_ids == [id(1), id(2), id(3)] - feature_group.append_id(4) - feature_group.add_operators(4, [10, 11, 12]) + group.append_id(4) + group.add_operators(4, [10, 11, 12]) - assert feature_group._operator_collection == [ + assert group._operator_collection == [ [1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12], ] - assert feature_group._operator_ids == [id(1), id(2), id(3), id(4)] + assert group._operator_ids == [id(1), id(2), id(3), id(4)] def test_grouping(): - feature_group = FeatureGroupFIFO() - feature_group.append_id(1) - feature_group.add_operators(1, [1, 2, 3]) - feature_group.append_id(2) - feature_group.add_operators(2, [4, 5, 6]) - feature_group.append_id(3) - feature_group.add_operators(3, [7, 8, 9]) + 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(feature_group) == [1, 2, 3, 4, 5, 6, 7, 8, 9] + assert list(group) == [1, 2, 3, 4, 5, 6, 7, 8, 9] - feature_group.append_id(4) - feature_group.add_operators(4, [10, 11, 12]) + group.append_id(4) + group.add_operators(4, [10, 11, 12]) - assert list(feature_group) == [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] + assert list(group) == [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] - feature_group.append_id(1) - feature_group.add_operators(1, [13, 14, 15]) + group.append_id(1) + group.add_operators(1, [13, 14, 15]) - assert list(feature_group) == [1, 2, 3, 13, 14, 15, 4, 5, 6, 7, 8, 9, 10, 11, 12] + 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