Source code for elastica.timestepper.symplectic_steppers

__doc__ = """Symplectic time steppers and concepts for integrating the kinematic and dynamic equations of rod-like objects.  """

from typing import TYPE_CHECKING, Any

from itertools import zip_longest

from elastica.typing import (
    SystemCollectionType,
    StepType,
    SteppersOperatorsType,
)

import numpy as np

from elastica.rod.data_structures import (
    overload_operator_kinematic_numba,
    overload_operator_dynamic_numba,
)
from elastica.systems.protocol import SymplecticSystemProtocol
from .protocol import SymplecticStepperProtocol

"""
Developer Note
--------------

For the reasons why we define Mixin classes here, the developer
is referred to the same section on `explicit_steppers.py`.
"""


class SymplecticStepperMixin:
    def __init__(self: SymplecticStepperProtocol):
        self.steps_and_prefactors: SteppersOperatorsType = self.step_methods()

    def step_methods(self: SymplecticStepperProtocol) -> SteppersOperatorsType:
        # Let the total number of steps for the Symplectic method
        # be (2*n + 1) (for time-symmetry).
        _steps: list[StepType] = self.get_steps()
        # Prefac here is necessary because the linear-exponential integrator
        # needs only the prefactor and not the dt.
        _prefactors: list[StepType] = self.get_prefactors()
        assert int(np.ceil(len(_steps) / 2)) == len(
            _prefactors
        ), f"{len(_steps)=}, {len(_prefactors)=}"

        # Separate the kinematic and dynamic steps
        _kinematic_steps: list[StepType] = _steps[::2]
        _dynamic_steps: list[StepType] = _steps[1::2]

        def no_operation(*args: Any) -> None:
            pass

        return tuple(
            zip_longest(
                _prefactors,
                _kinematic_steps,
                _dynamic_steps,
                fillvalue=no_operation,
            )
        )

    @property
    def n_stages(self: SymplecticStepperProtocol) -> int:
        return len(self.steps_and_prefactors)

    def step(
        self: SymplecticStepperProtocol,
        SystemCollection: SystemCollectionType,
        time: np.float64,
        dt: np.float64,
    ) -> np.float64:
        return SymplecticStepperMixin.do_step(
            self, self.steps_and_prefactors, SystemCollection, time, dt
        )

    # TODO: Merge with .step method in the future.
    # DEPRECATED: Use .step instead.
    @staticmethod
    def do_step(
        TimeStepper: SymplecticStepperProtocol,
        steps_and_prefactors: SteppersOperatorsType,
        SystemCollection: SystemCollectionType,
        time: np.float64,
        dt: np.float64,
    ) -> np.float64:
        """
        Function for doing symplectic stepper over the user defined rods (system).

        Returns
        -------
        time: float
            The time after the integration step.

        """
        for kin_prefactor, kin_step, dyn_step in steps_and_prefactors[:-1]:

            for system in SystemCollection.block_systems():
                kin_step(system, time, dt)

            time += kin_prefactor(dt)

            # Constrain only values
            SystemCollection.constrain_values(time)

            # We need internal forces and torques because they are used by interaction module.
            for system in SystemCollection.block_systems():
                system.compute_internal_forces_and_torques(time)
                # system.update_internal_forces_and_torques()

            # Add external forces, controls etc.
            SystemCollection.synchronize(time)

            for system in SystemCollection.block_systems():
                dyn_step(system, time, dt)

            # Constrain only rates
            SystemCollection.constrain_rates(time)

        # Peel the last kinematic step and prefactor alone
        last_kin_prefactor = steps_and_prefactors[-1][0]
        last_kin_step = steps_and_prefactors[-1][1]

        for system in SystemCollection.block_systems():
            last_kin_step(system, time, dt)
        time += last_kin_prefactor(dt)
        SystemCollection.constrain_values(time)

        # Call back function, will call the user defined call back functions and store data
        SystemCollection.apply_callbacks(time, round(time / dt))

        # Zero out the external forces and torques
        for system in SystemCollection.block_systems():
            system.zeroed_out_external_forces_and_torques(time)

        return time

    def step_single_instance(
        self: SymplecticStepperProtocol,
        System: SymplecticSystemProtocol,
        time: np.float64,
        dt: np.float64,
    ) -> np.float64:

        for kin_prefactor, kin_step, dyn_step in self.steps_and_prefactors[:-1]:
            kin_step(System, time, dt)
            time += kin_prefactor(dt)
            System.compute_internal_forces_and_torques(time)
            dyn_step(System, time, dt)

        # Peel the last kinematic step and prefactor alone
        last_kin_prefactor = self.steps_and_prefactors[-1][0]
        last_kin_step = self.steps_and_prefactors[-1][1]

        last_kin_step(System, time, dt)
        return time + last_kin_prefactor(dt)


[docs] class PositionVerlet(SymplecticStepperMixin): """ Position Verlet symplectic time stepper class, which includes methods for second-order position Verlet. """ def get_steps(self) -> list[StepType]: return [ self._first_kinematic_step, self._first_dynamic_step, self._first_kinematic_step, ] def get_prefactors(self) -> list[StepType]: return [ self._first_prefactor, self._first_prefactor, ] def _first_prefactor(self, dt: np.float64) -> np.float64: return 0.5 * dt def _first_kinematic_step( self, System: SymplecticSystemProtocol, time: np.float64, dt: np.float64 ) -> None: prefac = self._first_prefactor(dt) overload_operator_kinematic_numba( System.n_nodes, prefac, System.kinematic_states.position_collection, System.kinematic_states.director_collection, System.velocity_collection, System.omega_collection, ) def _first_dynamic_step( self, System: SymplecticSystemProtocol, time: np.float64, dt: np.float64 ) -> None: overload_operator_dynamic_numba( System.dynamic_states.rate_collection, System.dynamic_rates(time, dt), )
[docs] class PEFRL(SymplecticStepperMixin): """ Position Extended Forest-Ruth Like Algorithm of I.M. Omelyan, I.M. Mryglod and R. Folk, Computer Physics Communications 146, 188 (2002), http://arxiv.org/abs/cond-mat/0110585 """ # xi and chi are confusing, but be careful! ξ: np.float64 = np.float64(0.1786178958448091e0) # ξ λ: np.float64 = -np.float64(0.2123418310626054e0) # λ χ: np.float64 = -np.float64(0.6626458266981849e-1) # χ # Pre-calculate other coefficients lambda_dash_coeff: np.float64 = 0.5 * (1.0 - 2.0 * λ) xi_chi_dash_coeff: np.float64 = 1.0 - 2.0 * (ξ + χ) def get_steps(self) -> list[StepType]: operators = [ self._first_kinematic_step, self._first_dynamic_step, self._second_kinematic_step, self._second_dynamic_step, self._third_kinematic_step, ] return operators + operators[-2::-1] def get_prefactors(self) -> list[StepType]: return [ self._first_kinematic_prefactor, self._second_kinematic_prefactor, self._third_kinematic_prefactor, self._second_kinematic_prefactor, self._first_kinematic_prefactor, ] def _first_kinematic_prefactor(self, dt: np.float64) -> np.float64: return self.ξ * dt def _first_kinematic_step( self, System: SymplecticSystemProtocol, time: np.float64, dt: np.float64 ) -> None: prefac = self._first_kinematic_prefactor(dt) overload_operator_kinematic_numba( System.n_nodes, prefac, System.kinematic_states.position_collection, System.kinematic_states.director_collection, System.velocity_collection, System.omega_collection, ) # System.kinematic_states += prefac * System.kinematic_rates(time, prefac) def _first_dynamic_step( self, System: SymplecticSystemProtocol, time: np.float64, dt: np.float64 ) -> None: prefac = self.lambda_dash_coeff * dt overload_operator_dynamic_numba( System.dynamic_states.rate_collection, System.dynamic_rates(time, prefac), ) # System.dynamic_states += prefac * System.dynamic_rates(time, prefac) def _second_kinematic_prefactor(self, dt: np.float64) -> np.float64: return self.χ * dt def _second_kinematic_step( self, System: SymplecticSystemProtocol, time: np.float64, dt: np.float64 ) -> None: prefac = self._second_kinematic_prefactor(dt) overload_operator_kinematic_numba( System.n_nodes, prefac, System.kinematic_states.position_collection, System.kinematic_states.director_collection, System.velocity_collection, System.omega_collection, ) # System.kinematic_states += prefac * System.kinematic_rates(time, prefac) def _second_dynamic_step( self, System: SymplecticSystemProtocol, time: np.float64, dt: np.float64 ) -> None: prefac = self.λ * dt overload_operator_dynamic_numba( System.dynamic_states.rate_collection, System.dynamic_rates(time, prefac), ) # System.dynamic_states += prefac * System.dynamic_rates(time, prefac) def _third_kinematic_prefactor(self, dt: np.float64) -> np.float64: return self.xi_chi_dash_coeff * dt def _third_kinematic_step( self, System: SymplecticSystemProtocol, time: np.float64, dt: np.float64 ) -> None: prefac = self._third_kinematic_prefactor(dt) # Need to fill in overload_operator_kinematic_numba( System.n_nodes, prefac, System.kinematic_states.position_collection, System.kinematic_states.director_collection, System.velocity_collection, System.omega_collection, )
# System.kinematic_states += prefac * System.kinematic_rates(time, prefac) if TYPE_CHECKING: from .protocol import StepperProtocol _: StepperProtocol = PositionVerlet() _: StepperProtocol = PEFRL() # type: ignore [no-redef]