From 96031dc151b7804d2d514ccdbb38095fb801f61d Mon Sep 17 00:00:00 2001 From: Sylwester Arabas Date: Fri, 2 Aug 2024 13:50:07 +0200 Subject: [PATCH] introducing @register_dynamic decorator to solve dynamic-reuse issue --- PySDM/builder.py | 4 +- PySDM/dynamics/ambient_thermodynamics.py | 3 ++ PySDM/dynamics/aqueous_chemistry.py | 2 + PySDM/dynamics/collisions/collision.py | 4 ++ PySDM/dynamics/condensation.py | 4 +- PySDM/dynamics/displacement.py | 3 ++ PySDM/dynamics/eulerian_advection.py | 3 ++ PySDM/dynamics/freezing.py | 2 + PySDM/dynamics/impl/__init__.py | 2 + PySDM/dynamics/impl/register_dynamic.py | 21 ++++++++++ PySDM/dynamics/isotopic_fractionation.py | 2 + PySDM/dynamics/relaxed_velocity.py | 2 + .../dynamics/test_impl_register_dynamic.py | 39 +++++++++++++++++++ 13 files changed, 88 insertions(+), 3 deletions(-) create mode 100644 PySDM/dynamics/impl/register_dynamic.py create mode 100644 tests/unit_tests/dynamics/test_impl_register_dynamic.py diff --git a/PySDM/builder.py b/PySDM/builder.py index 59d58b2ee8..c2c7e7d429 100644 --- a/PySDM/builder.py +++ b/PySDM/builder.py @@ -121,8 +121,8 @@ def build( self._resolve_attribute(attr_name) self.req_attr_names = None - for dynamic in self.particulator.dynamics.values(): - dynamic.register(self) + for key, dynamic in self.particulator.dynamics.items(): + self.particulator.dynamics[key] = dynamic.instantiate(builder=self) single_buffer_for_all_products = np.empty(self.particulator.mesh.grid) for product in products: diff --git a/PySDM/dynamics/ambient_thermodynamics.py b/PySDM/dynamics/ambient_thermodynamics.py index 2e8e26c3c5..e7ac002ea7 100644 --- a/PySDM/dynamics/ambient_thermodynamics.py +++ b/PySDM/dynamics/ambient_thermodynamics.py @@ -2,7 +2,10 @@ environment-sync triggering class """ +from PySDM.dynamics.impl import register_dynamic + +@register_dynamic() class AmbientThermodynamics: def __init__(self): self.particulator = None diff --git a/PySDM/dynamics/aqueous_chemistry.py b/PySDM/dynamics/aqueous_chemistry.py index 307760bdee..5d850951fc 100644 --- a/PySDM/dynamics/aqueous_chemistry.py +++ b/PySDM/dynamics/aqueous_chemistry.py @@ -13,12 +13,14 @@ M, SpecificGravities, ) +from PySDM.dynamics.impl import register_dynamic DEFAULTS = namedtuple("_", ("pH_min", "pH_max", "pH_rtol", "ionic_strength_threshold"))( pH_min=-1.0, pH_max=14.0, pH_rtol=1e-6, ionic_strength_threshold=0.02 * M ) +@register_dynamic() class AqueousChemistry: # pylint: disable=too-many-instance-attributes def __init__( self, diff --git a/PySDM/dynamics/collisions/collision.py b/PySDM/dynamics/collisions/collision.py index 8e1b147da0..18b24d8637 100644 --- a/PySDM/dynamics/collisions/collision.py +++ b/PySDM/dynamics/collisions/collision.py @@ -23,6 +23,7 @@ RandomGeneratorOptimizerNoPair, ) from PySDM.physics import si +from PySDM.dynamics.impl import register_dynamic # pylint: disable=too-many-lines @@ -36,6 +37,7 @@ ) +@register_dynamic() class Collision: # pylint: disable=too-many-instance-attributes def __init__( self, @@ -288,6 +290,7 @@ def compute_gamma(self, prob, rand, is_first_in_pair, out): ) +@register_dynamic() class Coalescence(Collision): def __init__( self, @@ -316,6 +319,7 @@ def __init__( ) +@register_dynamic() class Breakup(Collision): def __init__( self, diff --git a/PySDM/dynamics/condensation.py b/PySDM/dynamics/condensation.py index d4a3eee5a0..eb863b32b8 100644 --- a/PySDM/dynamics/condensation.py +++ b/PySDM/dynamics/condensation.py @@ -7,7 +7,8 @@ import numpy as np -from ..physics import si +from PySDM.physics import si +from PySDM.dynamics.impl import register_dynamic DEFAULTS = namedtuple("_", ("rtol_x", "rtol_thd", "cond_range", "schedule"))( rtol_x=1e-6, @@ -17,6 +18,7 @@ ) +@register_dynamic() class Condensation: # pylint: disable=too-many-instance-attributes def __init__( self, diff --git a/PySDM/dynamics/displacement.py b/PySDM/dynamics/displacement.py index 5a7f96229c..e6810baf9a 100644 --- a/PySDM/dynamics/displacement.py +++ b/PySDM/dynamics/displacement.py @@ -11,9 +11,12 @@ import numpy as np +from PySDM.dynamics.impl import register_dynamic + DEFAULTS = namedtuple("_", ("rtol", "adaptive"))(rtol=1e-2, adaptive=True) +@register_dynamic() class Displacement: # pylint: disable=too-many-instance-attributes def __init__( self, diff --git a/PySDM/dynamics/eulerian_advection.py b/PySDM/dynamics/eulerian_advection.py index 625d865713..55cae0155e 100644 --- a/PySDM/dynamics/eulerian_advection.py +++ b/PySDM/dynamics/eulerian_advection.py @@ -2,7 +2,10 @@ wrapper class for triggering integration in the Eulerian advection solver """ +from PySDM.dynamics.impl import register_dynamic + +@register_dynamic() class EulerianAdvection: def __init__(self, solvers): self.solvers = solvers diff --git a/PySDM/dynamics/freezing.py b/PySDM/dynamics/freezing.py index aab2c5a8ad..a7d134244b 100644 --- a/PySDM/dynamics/freezing.py +++ b/PySDM/dynamics/freezing.py @@ -7,8 +7,10 @@ TimeDependentAttributes, ) from PySDM.physics.heterogeneous_ice_nucleation_rate import Null +from PySDM.dynamics.impl import register_dynamic +@register_dynamic() class Freezing: def __init__(self, *, singular=True, record_freezing_temperature=False, thaw=False): assert not (record_freezing_temperature and singular) diff --git a/PySDM/dynamics/impl/__init__.py b/PySDM/dynamics/impl/__init__.py index 8e45da48c1..f56de9d66c 100644 --- a/PySDM/dynamics/impl/__init__.py +++ b/PySDM/dynamics/impl/__init__.py @@ -1 +1,3 @@ """ stuff not intended to be imported from user code """ + +from .register_dynamic import register_dynamic diff --git a/PySDM/dynamics/impl/register_dynamic.py b/PySDM/dynamics/impl/register_dynamic.py new file mode 100644 index 0000000000..bc2198db7e --- /dev/null +++ b/PySDM/dynamics/impl/register_dynamic.py @@ -0,0 +1,21 @@ +""" decorator for dynamics classes +ensuring that their instances can be re-used with multiple builders """ + +from copy import deepcopy + + +def _instantiate(self, *, builder): + copy = deepcopy(self) + copy.register(builder=builder) + return copy + + +def register_dynamic(): + def decorator(cls): + if hasattr(cls, "instantiate"): + assert cls.instantiate is _instantiate + else: + setattr(cls, "instantiate", _instantiate) + return cls + + return decorator diff --git a/PySDM/dynamics/isotopic_fractionation.py b/PySDM/dynamics/isotopic_fractionation.py index 0d3761f0fd..dffeea5160 100644 --- a/PySDM/dynamics/isotopic_fractionation.py +++ b/PySDM/dynamics/isotopic_fractionation.py @@ -4,11 +4,13 @@ """ from PySDM.dynamics.condensation import Condensation +from PySDM.dynamics.impl import register_dynamic LIGHT_ISOTOPES = ("1H", "16O") HEAVY_ISOTOPES = ("2H", "3H", "17O", "18O") +@register_dynamic() class IsotopicFractionation: def __init__(self, isotopes: tuple = HEAVY_ISOTOPES): self.isotopes = isotopes diff --git a/PySDM/dynamics/relaxed_velocity.py b/PySDM/dynamics/relaxed_velocity.py index 803635a414..0bbf5e699e 100644 --- a/PySDM/dynamics/relaxed_velocity.py +++ b/PySDM/dynamics/relaxed_velocity.py @@ -6,8 +6,10 @@ from PySDM.attributes.impl.attribute import Attribute from PySDM.particulator import Particulator +from PySDM.dynamics.impl import register_dynamic +@register_dynamic() class RelaxedVelocity: # pylint: disable=too-many-instance-attributes """ A dynamic which updates the fall momentum according to a relaxation timescale diff --git a/tests/unit_tests/dynamics/test_impl_register_dynamic.py b/tests/unit_tests/dynamics/test_impl_register_dynamic.py new file mode 100644 index 0000000000..ff566020ef --- /dev/null +++ b/tests/unit_tests/dynamics/test_impl_register_dynamic.py @@ -0,0 +1,39 @@ +""" checks if @register_product makes dynamics instances reusable """ + +import numpy as np + +from PySDM import Builder +from PySDM.backends import CPU +from PySDM.environments import Box +from PySDM.dynamics.impl import register_dynamic + + +def test_impl_register_dynamic(): + # arrange + @register_dynamic() + class Dynamic: + def __init__(self): + self.particulator = None + + def register(self, *, builder: Builder): + self.particulator = builder.particulator + + dynamic = Dynamic() + kwargs = {"n_sd": 0, "backend": CPU(), "environment": Box(dt=0, dv=0)} + builders = [Builder(**kwargs), Builder(**kwargs)] + + # act + for builder in builders: + builder.add_dynamic(dynamic) + builder.build( + attributes={"multiplicity": np.empty(0), "water mass": np.empty(0)} + ) + + # assert + assert dynamic.particulator is None + assert builders[0].particulator is not builders[1].particulator + for builder in builders: + assert ( + builder.particulator.dynamics["Dynamic"].particulator + is builder.particulator + )