1414import numpy as np
1515from 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
2123class 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
9798DampenType : 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 "\t simulator.dampen(rod).using(\n "
213214 "\t \t AnalyticalLinearDamper,\n "
@@ -228,7 +229,6 @@ def __init__(self, time_step: np.float64, **kwargs: Any) -> None:
228229 "\t \t time_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+
301397class LaplaceDissipationFilter (DamperBase ):
302398 """
303399 Laplace Dissipation Filter class. This class corresponds qualitatively to a
0 commit comments