Skip to content

Commit dca80c0

Browse files
committed
Merge remote-tracking branch 'origin/update-v1' into elasticapp-integration
2 parents 5840da6 + abcd104 commit dca80c0

File tree

4 files changed

+104
-11
lines changed

4 files changed

+104
-11
lines changed

elastica/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
DamperBase,
5757
AnalyticalLinearDamper,
5858
LaplaceDissipationFilter,
59+
RayleighDissipation,
5960
)
6061
from elastica.modules.base_system import BaseSystemCollection
6162
from elastica.modules.callbacks import CallBacks

elastica/dissipation.py

Lines changed: 101 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@
1414
import numpy as np
1515
from numpy.typing import NDArray
1616

17+
from elastica.typing import SystemType
1718

18-
T = TypeVar("T")
19+
20+
T = TypeVar("T", bound=SystemType)
1921

2022

2123
class DamperBase(Generic[T], ABC):
@@ -91,7 +93,6 @@ def dampen_rates(self, system: T, time: np.float64) -> None:
9193
The time of simulation.
9294
9395
"""
94-
pass
9596

9697

9798
DampenType: TypeAlias = Callable[[RodType], None]
@@ -142,7 +143,7 @@ class AnalyticalLinearDamper(DamperBase):
142143
>>> simulator.dampen(rod).using(
143144
... AnalyticalLinearDamper,
144145
... damping_constant=0.1,
145-
... time_step = 1E-4, # Simulation time-step
146+
... time_step=1E-4,
146147
... )
147148
148149
Notes
@@ -207,7 +208,7 @@ def __init__(self, time_step: np.float64, **kwargs: Any) -> None:
207208
)
208209
else:
209210
# Invalid parameter combination
210-
message = (
211+
raise ValueError(
211212
"AnalyticalLinearDamper usage:\n"
212213
"\tsimulator.dampen(rod).using(\n"
213214
"\t\tAnalyticalLinearDamper,\n"
@@ -228,7 +229,6 @@ def __init__(self, time_step: np.float64, **kwargs: Any) -> None:
228229
"\t\ttime_step=...,\n"
229230
"\t)\n"
230231
)
231-
raise ValueError(message)
232232

233233
def _deprecated_damping_protocol(
234234
self, damping_constant: np.float64, time_step: np.float64
@@ -298,6 +298,102 @@ def dampen_rates(self, system: RodType, time: np.float64) -> None:
298298
self._dampen_rates_protocol(system)
299299

300300

301+
class RayleighDissipation(DamperBase):
302+
"""
303+
Rayleigh dissipation model matching the C++ implementation.
304+
305+
This class implements the C++ force-based damping model for compatibility.
306+
It is deprecated in favor of :class:`AnalyticalLinearDamper` which provides
307+
better numerical stability and unconditional stability. This implementation
308+
is kept for validation for old cases.
309+
310+
This class implements force-based damping that matches the C++ nest-simulator
311+
implementation. It adds damping forces and torques proportional to velocities:
312+
313+
.. math::
314+
315+
\\mathbf{F}_{damp} = -\\nu \\mathbf{v}
316+
317+
\\boldsymbol{\\tau}_{damp} = -\\nu \\boldsymbol{\\omega}
318+
319+
where the damping coefficient :math:`\\nu` can decay exponentially over time.
320+
321+
The damping forces are added to external forces and integrated through the
322+
time stepper, which may require smaller time steps for large damping values.
323+
324+
Parameters
325+
----------
326+
damping_constant : float
327+
Damping coefficient :math:`\\nu` (per unit length). Units: [1/s] or [kg/(m·s)]
328+
329+
Examples
330+
--------
331+
.. code-block:: python
332+
333+
simulator.dampen(rod).using(
334+
RayleighDissipation,
335+
damping_constant=0.1,
336+
)
337+
338+
See Also
339+
--------
340+
AnalyticalLinearDamper : Recommended alternative with better stability
341+
LaplaceDissipationFilter : Alternative filtering-based dissipation
342+
"""
343+
344+
def __init__(
345+
self,
346+
damping_constant: np.float64,
347+
**kwargs: Any,
348+
) -> None:
349+
super().__init__(**kwargs)
350+
351+
if damping_constant < 0.0:
352+
raise ValueError("damping_constant must be non-negative")
353+
354+
_relaxation_time = 0.0 # relaxation: scale damping by exp(-time/relaxation)
355+
356+
# Pre-compute average element length for rescaling
357+
rest_lengths = self._system.rest_lengths
358+
n_elems = self._system.n_elems
359+
self._average_element_length = np.sum(rest_lengths) / n_elems
360+
361+
if _relaxation_time > 0.0:
362+
self.get_nu = lambda time: damping_constant * np.exp(
363+
-time / _relaxation_time
364+
)
365+
else:
366+
self.get_nu = lambda time: damping_constant
367+
368+
def dampen_rates(self, system: RodType, time: np.float64) -> None:
369+
"""
370+
Apply Rayleigh dissipation forces and torques.
371+
372+
Parameters
373+
----------
374+
system : RodType
375+
Rod system to apply damping to
376+
time : float
377+
Current simulation time
378+
"""
379+
# Rescale since nu is per unit length
380+
nu_now = self.get_nu(time) * self._average_element_length
381+
382+
# Apply damping forces: F = -nu * v
383+
# Boundary factor: 0.5 at endpoints, 1.0 otherwise (matches C++)
384+
# dampingForces[i] -= (nuNow * factor) * v[i]
385+
for i in range(system.n_nodes):
386+
factor = 0.5 if (i == 0 or i == system.n_nodes - 1) else 1.0
387+
damping_force = -(nu_now * factor) * system.velocity_collection[:, i]
388+
system.external_forces[:, i] += damping_force
389+
390+
# Apply damping torques: T = -nu * w
391+
# dampingTorques[i] -= nuNow * w[i]
392+
for i in range(system.n_elems):
393+
damping_torque = -nu_now * system.omega_collection[:, i]
394+
system.external_torques[:, i] += damping_torque
395+
396+
301397
class LaplaceDissipationFilter(DamperBase):
302398
"""
303399
Laplace Dissipation Filter class. This class corresponds qualitatively to a

elastica/modules/contact.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,8 @@
1515
)
1616
from .protocol import SystemCollectionProtocol, ModuleProtocol
1717

18-
import logging
19-
2018
from elastica.contact_forces import NoContact
2119

22-
logger = logging.getLogger(__name__)
23-
2420

2521
class Contact(SystemCollectionProtocol):
2622
"""

elastica/timestepper/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@
1111
from .protocol import StepperProtocol
1212

1313

14-
# Deprecated: Remove in the future version
15-
# Many script still uses this method to control timestep. Keep it for backward compatibility
14+
# Deprecated: Kept for backward compatibility.
15+
# Many script still uses this method to control timestep.
1616
def extend_stepper_interface(
1717
stepper: StepperProtocol, system_collection: SystemCollectionType
1818
) -> tuple[

0 commit comments

Comments
 (0)