From acb5ead3945a3634e20199136e68b124afc0ac60 Mon Sep 17 00:00:00 2001 From: Tim Luettmer Date: Mon, 30 Dec 2024 15:09:08 +0100 Subject: [PATCH 01/57] Added homogeneous freezing routines and Koop 2000 nucleation rate --- .../impl_numba/methods/freezing_methods.py | 80 ++++++++++++++++++- PySDM/dynamics/freezing.py | 1 + PySDM/physics/constants_defaults.py | 7 ++ .../__init__.py | 7 ++ .../__init__.py~ | 7 ++ .../constant.py | 14 ++++ .../constant.py~ | 14 ++++ .../homogeneous_ice_nucleation_rate/koop.py | 14 ++++ .../homogeneous_ice_nucleation_rate/koop.py~ | 0 .../koop_murray.py | 0 .../homogeneous_ice_nucleation_rate/null.py | 13 +++ .../homogeneous_ice_nucleation_rate/null.py~ | 13 +++ PySDM/physics/trivia.py | 4 + 13 files changed, 173 insertions(+), 1 deletion(-) create mode 100644 PySDM/physics/homogeneous_ice_nucleation_rate/__init__.py create mode 100644 PySDM/physics/homogeneous_ice_nucleation_rate/__init__.py~ create mode 100644 PySDM/physics/homogeneous_ice_nucleation_rate/constant.py create mode 100644 PySDM/physics/homogeneous_ice_nucleation_rate/constant.py~ create mode 100644 PySDM/physics/homogeneous_ice_nucleation_rate/koop.py create mode 100644 PySDM/physics/homogeneous_ice_nucleation_rate/koop.py~ create mode 100644 PySDM/physics/homogeneous_ice_nucleation_rate/koop_murray.py create mode 100644 PySDM/physics/homogeneous_ice_nucleation_rate/null.py create mode 100644 PySDM/physics/homogeneous_ice_nucleation_rate/null.py~ diff --git a/PySDM/backends/impl_numba/methods/freezing_methods.py b/PySDM/backends/impl_numba/methods/freezing_methods.py index 3a973d7bb3..5a89ea7d75 100644 --- a/PySDM/backends/impl_numba/methods/freezing_methods.py +++ b/PySDM/backends/impl_numba/methods/freezing_methods.py @@ -1,5 +1,5 @@ """ -CPU implementation of backend methods for freezing (singular and time-dependent immersion freezing) +CPU implementation of backend methods for homogeneous freezing and heterogeneous freezing (singular and time-dependent immersion freezing) """ import numba @@ -92,6 +92,49 @@ def freeze_time_dependent_body( # pylint: disable=unused-argument,too-many-argu self.freeze_time_dependent_body = freeze_time_dependent_body + + j_hom = self.formulae._ice_nucleation_rate.j_hom + + @numba.njit(**self.default_jit_flags) + def freeze_time_dependent_homogeneous_body( # pylint: disable=unused-argument,too-many-arguments + rand, + attributes, + timestep, + cell, + a_w_ice, + temperature, + relative_humidity, + record_freezing_temperature, + freezing_temperature, + thaw, + ): + + n_sd = len(attributes.water_mass) + for i in numba.prange(n_sd): # pylint: disable=not-an-iterable + + cell_id = cell[i] + relative_humidity_ice = relative_humidity[cell_id] / a_w_ice[cell_id] + if thaw and frozen_and_above_freezing_point( + attributes.water_mass[i], temperature[cell_id] + ): + _thaw(attributes.water_mass, i) + elif unfrozen_and_ice_saturated( + attributes.water_mass[i], relative_humidity_ice + ): + d_a_w_ice = (relative_humidity_ice - 1) * a_w_ice[cell_id] + rate = j_hom(temperature, d_a_w_ice) + # TODO #594: this assumes constant T throughout timestep, can we do better? + prob = 1 - np.exp( # TODO #599: common code for Poissonian prob + -rate * attributes.volume[i] * timestep + ) + if rand[i] < prob: + _freeze(attributes.water_mass, i) + # if record_freezing_temperature: + # freezing_temperature[i] = temperature[cell_id] + + + self.freeze_time_dependent_homogeneous_body = freeze_time_dependent_homogeneous_body + def freeze_singular( self, *, attributes, temperature, relative_humidity, cell, thaw: bool ): @@ -137,3 +180,38 @@ def freeze_time_dependent( ), thaw=thaw, ) + + + + def freeze_time_dependent_homogeneous( + self, + *, + rand, + attributes, + timestep, + cell, + a_w_ice, + temperature, + relative_humidity, + record_freezing_temperature, + freezing_temperature, + thaw: bool + ): + self.freeze_time_dependent_homogeneous_body( + rand.data, + TimeDependentAttributes( + volume=attributes.volume.data, + water_mass=attributes.water_mass.data, + ), + timestep, + cell.data, + a_w_ice.data, + temperature.data, + relative_humidity.data, + record_freezing_temperature=record_freezing_temperature, + freezing_temperature=( + freezing_temperature.data if record_freezing_temperature else None + ), + thaw=thaw, + ) + diff --git a/PySDM/dynamics/freezing.py b/PySDM/dynamics/freezing.py index a7d134244b..5bbed077d0 100644 --- a/PySDM/dynamics/freezing.py +++ b/PySDM/dynamics/freezing.py @@ -36,6 +36,7 @@ def register(self, builder): builder.formulae.heterogeneous_ice_nucleation_rate, Null ) builder.request_attribute("immersed surface area") + builder.request_attribute("volume") self.rand = self.particulator.Storage.empty( self.particulator.n_sd, dtype=float ) diff --git a/PySDM/physics/constants_defaults.py b/PySDM/physics/constants_defaults.py index aee675d08f..30cf65fcde 100644 --- a/PySDM/physics/constants_defaults.py +++ b/PySDM/physics/constants_defaults.py @@ -185,7 +185,14 @@ ABIFM_C = np.inf ABIFM_UNIT = 1 / si.cm**2 / si.s +KOOP_2000_C1 = -906.7 +KOOP_2000_C2 = 8502 +KOOP_2000_C3 = 26924 +KOOP_2000_C4 = 29180 +KOOP_UNIT = 1 / si.cm**3 / si.s + J_HET = np.nan +J_HOM = np.nan STRAUB_E_D1 = 0.04 * si.cm STRAUB_MU2 = 0.095 * si.cm diff --git a/PySDM/physics/homogeneous_ice_nucleation_rate/__init__.py b/PySDM/physics/homogeneous_ice_nucleation_rate/__init__.py new file mode 100644 index 0000000000..3cb1eabeb4 --- /dev/null +++ b/PySDM/physics/homogeneous_ice_nucleation_rate/__init__.py @@ -0,0 +1,7 @@ +""" +homogeneous-freezing rate (aka J_hom) formulations +""" + +from .constant import Constant +from .null import Null +from .koop import KOOP diff --git a/PySDM/physics/homogeneous_ice_nucleation_rate/__init__.py~ b/PySDM/physics/homogeneous_ice_nucleation_rate/__init__.py~ new file mode 100644 index 0000000000..91fe9627d6 --- /dev/null +++ b/PySDM/physics/homogeneous_ice_nucleation_rate/__init__.py~ @@ -0,0 +1,7 @@ +""" +immersion-freezing rate (aka J_het) formulations +""" + +from .abifm import ABIFM +from .constant import Constant +from .null import Null diff --git a/PySDM/physics/homogeneous_ice_nucleation_rate/constant.py b/PySDM/physics/homogeneous_ice_nucleation_rate/constant.py new file mode 100644 index 0000000000..f5f32353d4 --- /dev/null +++ b/PySDM/physics/homogeneous_ice_nucleation_rate/constant.py @@ -0,0 +1,14 @@ +""" +constant rate formulation (for tests) +""" + +import numpy as np + + +class Constant: # pylint: disable=too-few-public-methods + def __init__(self, const): + assert np.isfinite(const.J_HOM) + + @staticmethod + def j_hom(const, T, a_w_ice): # pylint: disable=unused-argument + return const.J_HOM diff --git a/PySDM/physics/homogeneous_ice_nucleation_rate/constant.py~ b/PySDM/physics/homogeneous_ice_nucleation_rate/constant.py~ new file mode 100644 index 0000000000..4fc862be3f --- /dev/null +++ b/PySDM/physics/homogeneous_ice_nucleation_rate/constant.py~ @@ -0,0 +1,14 @@ +""" +constant rate formulation (for tests) +""" + +import numpy as np + + +class Constant: # pylint: disable=too-few-public-methods + def __init__(self, const): + assert np.isfinite(const.J_HET) + + @staticmethod + def j_het(const, a_w_ice): # pylint: disable=unused-argument + return const.J_HET diff --git a/PySDM/physics/homogeneous_ice_nucleation_rate/koop.py b/PySDM/physics/homogeneous_ice_nucleation_rate/koop.py new file mode 100644 index 0000000000..144b42edde --- /dev/null +++ b/PySDM/physics/homogeneous_ice_nucleation_rate/koop.py @@ -0,0 +1,14 @@ +""" +Koop homogeneous nucleation rate parameterization for solution droplets +valid for 0.26 < da_w_ice < 0.34 + ([Koop et al. 2000](https://doi.org/10.1038/35020537)) +""" + + +class KOOP: # pylint: disable=too-few-public-methods + def __init__(self, const): + pass + + @staticmethod + def j_hom(const, T, da_w_ice): + return 10**(const.KOOP_2000_C1 + const.KOOP_2000_C2 * da_w_ice - const.KOOP_2000_C3 * da_w_ice**2. + const.KOOP_2000_C4 * da_w_ice**3.) * const.KOOP_UNIT diff --git a/PySDM/physics/homogeneous_ice_nucleation_rate/koop.py~ b/PySDM/physics/homogeneous_ice_nucleation_rate/koop.py~ new file mode 100644 index 0000000000..e69de29bb2 diff --git a/PySDM/physics/homogeneous_ice_nucleation_rate/koop_murray.py b/PySDM/physics/homogeneous_ice_nucleation_rate/koop_murray.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/PySDM/physics/homogeneous_ice_nucleation_rate/null.py b/PySDM/physics/homogeneous_ice_nucleation_rate/null.py new file mode 100644 index 0000000000..81ceff95b6 --- /dev/null +++ b/PySDM/physics/homogeneous_ice_nucleation_rate/null.py @@ -0,0 +1,13 @@ +""" +do-nothing null formulation (needed as other formulations require parameters + to be set before instantiation of Formulae) +""" + + +class Null: # pylint: disable=too-few-public-methods,unused-argument + def __init__(self, _): + pass + + @staticmethod + def j_hom(const, T, d_a_w_ice): # pylint: disable=unused-argument + return 0 diff --git a/PySDM/physics/homogeneous_ice_nucleation_rate/null.py~ b/PySDM/physics/homogeneous_ice_nucleation_rate/null.py~ new file mode 100644 index 0000000000..b7e8703f2e --- /dev/null +++ b/PySDM/physics/homogeneous_ice_nucleation_rate/null.py~ @@ -0,0 +1,13 @@ +""" +do-nothing null formulation (needed as other formulations require parameters + to be set before instantiation of Formulae) +""" + + +class Null: # pylint: disable=too-few-public-methods,unused-argument + def __init__(self, _): + pass + + @staticmethod + def j_het(const, a_w_ice): + return 0 diff --git a/PySDM/physics/trivia.py b/PySDM/physics/trivia.py index 3bac176aa5..7331a10471 100644 --- a/PySDM/physics/trivia.py +++ b/PySDM/physics/trivia.py @@ -79,6 +79,10 @@ def th_std(const, p, T): def unfrozen_and_saturated(water_mass, relative_humidity): return water_mass > 0 and relative_humidity > 1 + @staticmethod + def unfrozen_and_ice_saturated(water_mass, relative_humidity_ice): + return water_mass > 0 and relative_humidity_ice > 1 + @staticmethod def frozen_and_above_freezing_point(const, water_mass, temperature): return water_mass < 0 and temperature > const.T0 From 0f53d3f920a2b9b140b1ca627ec4864480d25cef Mon Sep 17 00:00:00 2001 From: Tim Luettmer Date: Mon, 30 Dec 2024 15:27:36 +0100 Subject: [PATCH 02/57] Untracked temporary files --- .../homogeneous_ice_nucleation_rate/__init__.py~ | 7 ------- .../homogeneous_ice_nucleation_rate/constant.py~ | 14 -------------- .../homogeneous_ice_nucleation_rate/koop.py~ | 0 .../homogeneous_ice_nucleation_rate/null.py~ | 13 ------------- 4 files changed, 34 deletions(-) delete mode 100644 PySDM/physics/homogeneous_ice_nucleation_rate/__init__.py~ delete mode 100644 PySDM/physics/homogeneous_ice_nucleation_rate/constant.py~ delete mode 100644 PySDM/physics/homogeneous_ice_nucleation_rate/koop.py~ delete mode 100644 PySDM/physics/homogeneous_ice_nucleation_rate/null.py~ diff --git a/PySDM/physics/homogeneous_ice_nucleation_rate/__init__.py~ b/PySDM/physics/homogeneous_ice_nucleation_rate/__init__.py~ deleted file mode 100644 index 91fe9627d6..0000000000 --- a/PySDM/physics/homogeneous_ice_nucleation_rate/__init__.py~ +++ /dev/null @@ -1,7 +0,0 @@ -""" -immersion-freezing rate (aka J_het) formulations -""" - -from .abifm import ABIFM -from .constant import Constant -from .null import Null diff --git a/PySDM/physics/homogeneous_ice_nucleation_rate/constant.py~ b/PySDM/physics/homogeneous_ice_nucleation_rate/constant.py~ deleted file mode 100644 index 4fc862be3f..0000000000 --- a/PySDM/physics/homogeneous_ice_nucleation_rate/constant.py~ +++ /dev/null @@ -1,14 +0,0 @@ -""" -constant rate formulation (for tests) -""" - -import numpy as np - - -class Constant: # pylint: disable=too-few-public-methods - def __init__(self, const): - assert np.isfinite(const.J_HET) - - @staticmethod - def j_het(const, a_w_ice): # pylint: disable=unused-argument - return const.J_HET diff --git a/PySDM/physics/homogeneous_ice_nucleation_rate/koop.py~ b/PySDM/physics/homogeneous_ice_nucleation_rate/koop.py~ deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/PySDM/physics/homogeneous_ice_nucleation_rate/null.py~ b/PySDM/physics/homogeneous_ice_nucleation_rate/null.py~ deleted file mode 100644 index b7e8703f2e..0000000000 --- a/PySDM/physics/homogeneous_ice_nucleation_rate/null.py~ +++ /dev/null @@ -1,13 +0,0 @@ -""" -do-nothing null formulation (needed as other formulations require parameters - to be set before instantiation of Formulae) -""" - - -class Null: # pylint: disable=too-few-public-methods,unused-argument - def __init__(self, _): - pass - - @staticmethod - def j_het(const, a_w_ice): - return 0 From 2c215f3f48fb12b202f8b70c56e8b7b0aeea0a8d Mon Sep 17 00:00:00 2001 From: Tim Luettmer Date: Mon, 30 Dec 2024 18:47:17 +0100 Subject: [PATCH 03/57] Fixed registration of hom ice nucleation rate --- PySDM/backends/impl_numba/methods/freezing_methods.py | 2 +- PySDM/formulae.py | 2 ++ PySDM/physics/__init__.py | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/PySDM/backends/impl_numba/methods/freezing_methods.py b/PySDM/backends/impl_numba/methods/freezing_methods.py index 5a89ea7d75..1ae22a0a13 100644 --- a/PySDM/backends/impl_numba/methods/freezing_methods.py +++ b/PySDM/backends/impl_numba/methods/freezing_methods.py @@ -93,7 +93,7 @@ def freeze_time_dependent_body( # pylint: disable=unused-argument,too-many-argu self.freeze_time_dependent_body = freeze_time_dependent_body - j_hom = self.formulae._ice_nucleation_rate.j_hom + j_hom = self.formulae.homogeneous_ice_nucleation_rate.j_hom @numba.njit(**self.default_jit_flags) def freeze_time_dependent_homogeneous_body( # pylint: disable=unused-argument,too-many-arguments diff --git a/PySDM/formulae.py b/PySDM/formulae.py index af8dc10c62..2e4f695887 100644 --- a/PySDM/formulae.py +++ b/PySDM/formulae.py @@ -44,6 +44,7 @@ def __init__( # pylint: disable=too-many-locals hydrostatics: str = "Default", freezing_temperature_spectrum: str = "Null", heterogeneous_ice_nucleation_rate: str = "Null", + homogeneous_ice_nucleation_rate: str = "Null", fragmentation_function: str = "AlwaysN", isotope_equilibrium_fractionation_factors: str = "Null", isotope_meteoric_water_line_excess: str = "Null", @@ -76,6 +77,7 @@ def __init__( # pylint: disable=too-many-locals self.hydrostatics = hydrostatics self.freezing_temperature_spectrum = freezing_temperature_spectrum self.heterogeneous_ice_nucleation_rate = heterogeneous_ice_nucleation_rate + self.homogeneous_ice_nucleation_rate = homogeneous_ice_nucleation_rate self.fragmentation_function = fragmentation_function self.isotope_equilibrium_fractionation_factors = ( isotope_equilibrium_fractionation_factors diff --git a/PySDM/physics/__init__.py b/PySDM/physics/__init__.py index 93b1c4f3d0..d907e09194 100644 --- a/PySDM/physics/__init__.py +++ b/PySDM/physics/__init__.py @@ -25,6 +25,7 @@ fragmentation_function, freezing_temperature_spectrum, heterogeneous_ice_nucleation_rate, + homogeneous_ice_nucleation_rate, hydrostatics, hygroscopicity, impl, From 73bf2ffa37744bcea116d1798f8539d7c4108744 Mon Sep 17 00:00:00 2001 From: Tim Luettmer Date: Fri, 3 Jan 2025 19:50:28 +0100 Subject: [PATCH 04/57] Added logicals for hom/het freezing and fixed passing of attributes --- .../impl_common/freezing_attributes.py | 8 ++ .../impl_numba/methods/freezing_methods.py | 5 +- PySDM/dynamics/freezing.py | 107 ++++++++++++------ 3 files changed, 83 insertions(+), 37 deletions(-) diff --git a/PySDM/backends/impl_common/freezing_attributes.py b/PySDM/backends/impl_common/freezing_attributes.py index d94f6d48d6..4ff665a9ba 100644 --- a/PySDM/backends/impl_common/freezing_attributes.py +++ b/PySDM/backends/impl_common/freezing_attributes.py @@ -19,3 +19,11 @@ class TimeDependentAttributes( """groups attributes required in time-dependent regime""" __slots__ = () + + +class TimeDependentHomogeneousAttributes( + namedtuple("TimeDependentHomogeneousAttributes", ("volume", "water_mass")) +): + """groups attributes required in time-dependent regime for homogeneous freezing""" + + __slots__ = () diff --git a/PySDM/backends/impl_numba/methods/freezing_methods.py b/PySDM/backends/impl_numba/methods/freezing_methods.py index 1ae22a0a13..5ea15a0699 100644 --- a/PySDM/backends/impl_numba/methods/freezing_methods.py +++ b/PySDM/backends/impl_numba/methods/freezing_methods.py @@ -10,6 +10,7 @@ from ...impl_common.freezing_attributes import ( SingularAttributes, TimeDependentAttributes, + TimeDependentHomogeneousAttributes, ) @@ -17,6 +18,7 @@ class FreezingMethods(BackendMethods): def __init__(self): BackendMethods.__init__(self) unfrozen_and_saturated = self.formulae.trivia.unfrozen_and_saturated + unfrozen_and_ice_saturated = self.formulae.trivia.unfrozen_and_ice_saturated frozen_and_above_freezing_point = ( self.formulae.trivia.frozen_and_above_freezing_point ) @@ -111,7 +113,6 @@ def freeze_time_dependent_homogeneous_body( # pylint: disable=unused-argument,t n_sd = len(attributes.water_mass) for i in numba.prange(n_sd): # pylint: disable=not-an-iterable - cell_id = cell[i] relative_humidity_ice = relative_humidity[cell_id] / a_w_ice[cell_id] if thaw and frozen_and_above_freezing_point( @@ -199,7 +200,7 @@ def freeze_time_dependent_homogeneous( ): self.freeze_time_dependent_homogeneous_body( rand.data, - TimeDependentAttributes( + TimeDependentHomogeneousAttributes( volume=attributes.volume.data, water_mass=attributes.water_mass.data, ), diff --git a/PySDM/dynamics/freezing.py b/PySDM/dynamics/freezing.py index 5bbed077d0..af86ab6400 100644 --- a/PySDM/dynamics/freezing.py +++ b/PySDM/dynamics/freezing.py @@ -5,6 +5,7 @@ from PySDM.backends.impl_common.freezing_attributes import ( SingularAttributes, TimeDependentAttributes, + TimeDependentHomogeneousAttributes, ) from PySDM.physics.heterogeneous_ice_nucleation_rate import Null from PySDM.dynamics.impl import register_dynamic @@ -12,9 +13,11 @@ @register_dynamic() class Freezing: - def __init__(self, *, singular=True, record_freezing_temperature=False, thaw=False): + def __init__(self, *, singular=True, hom_freezing=False, het_freezing=True, record_freezing_temperature=False, thaw=False): assert not (record_freezing_temperature and singular) self.singular = singular + self.hom_freezing = hom_freezing + self.het_freezing = het_freezing self.record_freezing_temperature = record_freezing_temperature self.thaw = thaw self.enable = True @@ -35,8 +38,8 @@ def register(self, builder): assert not isinstance( builder.formulae.heterogeneous_ice_nucleation_rate, Null ) - builder.request_attribute("immersed surface area") - builder.request_attribute("volume") + if self.het_freezing: + builder.request_attribute("immersed surface area") self.rand = self.particulator.Storage.empty( self.particulator.n_sd, dtype=float ) @@ -44,6 +47,8 @@ def register(self, builder): self.particulator.n_sd, self.particulator.backend.formulae.seed ) + + def __call__(self): if "Coalescence" in self.particulator.dynamics: # TODO #594 @@ -54,42 +59,74 @@ def __call__(self): if not self.enable: return - if self.singular: - self.particulator.backend.freeze_singular( - attributes=SingularAttributes( - freezing_temperature=self.particulator.attributes[ - "freezing temperature" - ], - water_mass=self.particulator.attributes["water mass"], - ), - temperature=self.particulator.environment["T"], - relative_humidity=self.particulator.environment["RH"], - cell=self.particulator.attributes["cell id"], - thaw=self.thaw, - ) - else: - self.rand.urand(self.rng) - self.particulator.backend.freeze_time_dependent( - rand=self.rand, - attributes=TimeDependentAttributes( + if self.het_freezing: + + if self.singular: + self.particulator.backend.freeze_singular( + attributes=SingularAttributes( + freezing_temperature=self.particulator.attributes[ + "freezing temperature" + ], + water_mass=self.particulator.attributes["water mass"], + ), + temperature=self.particulator.environment["T"], + relative_humidity=self.particulator.environment["RH"], + cell=self.particulator.attributes["cell id"], + thaw=self.thaw, + ) + else: + self.rand.urand(self.rng) + self.particulator.backend.freeze_time_dependent( + rand=self.rand, + attributes=TimeDependentAttributes( immersed_surface_area=self.particulator.attributes[ "immersed surface area" ], water_mass=self.particulator.attributes["water mass"], - ), - timestep=self.particulator.dt, - cell=self.particulator.attributes["cell id"], - a_w_ice=self.particulator.environment["a_w_ice"], - temperature=self.particulator.environment["T"], - relative_humidity=self.particulator.environment["RH"], - record_freezing_temperature=self.record_freezing_temperature, - freezing_temperature=( - self.particulator.attributes["freezing temperature"] - if self.record_freezing_temperature - else None - ), - thaw=self.thaw, - ) + ), + timestep=self.particulator.dt, + cell=self.particulator.attributes["cell id"], + a_w_ice=self.particulator.environment["a_w_ice"], + temperature=self.particulator.environment["T"], + relative_humidity=self.particulator.environment["RH"], + record_freezing_temperature=self.record_freezing_temperature, + freezing_temperature=( + self.particulator.attributes["freezing temperature"] + if self.record_freezing_temperature + else None + ), + thaw=self.thaw, + ) + + + if self.hom_freezing: + + + if self.singular: + pass + else: + self.rand.urand(self.rng) + self.particulator.backend.freeze_time_dependent_homogeneous( + rand=self.rand, + attributes=TimeDependentHomogeneousAttributes( + volume=self.particulator.attributes["volume"], + water_mass=self.particulator.attributes["water mass"], + ), + timestep=self.particulator.dt, + cell=self.particulator.attributes["cell id"], + a_w_ice=self.particulator.environment["a_w_ice"], + temperature=self.particulator.environment["T"], + relative_humidity=self.particulator.environment["RH"], + record_freezing_temperature=self.record_freezing_temperature, + freezing_temperature=( + self.particulator.attributes["freezing temperature"] + if self.record_freezing_temperature + else None + ), + thaw=self.thaw, + ) + + self.particulator.attributes.mark_updated("water mass") if self.record_freezing_temperature: From 91c2cff3b2be160efef57fb09fac3ec8cb377f6b Mon Sep 17 00:00:00 2001 From: Tim Luettmer Date: Fri, 3 Jan 2025 20:00:27 +0100 Subject: [PATCH 05/57] Renamed freezing logicals --- PySDM/dynamics/freezing.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/PySDM/dynamics/freezing.py b/PySDM/dynamics/freezing.py index af86ab6400..95bf3ee54b 100644 --- a/PySDM/dynamics/freezing.py +++ b/PySDM/dynamics/freezing.py @@ -13,11 +13,11 @@ @register_dynamic() class Freezing: - def __init__(self, *, singular=True, hom_freezing=False, het_freezing=True, record_freezing_temperature=False, thaw=False): + def __init__(self, *, singular=True, homogeneous_freezing=False, immersion_freezing=True, record_freezing_temperature=False, thaw=False): assert not (record_freezing_temperature and singular) self.singular = singular - self.hom_freezing = hom_freezing - self.het_freezing = het_freezing + self.homogeneous_freezing = homogeneous_freezing + self.immersion_freezing = immersion_freezing self.record_freezing_temperature = record_freezing_temperature self.thaw = thaw self.enable = True @@ -38,7 +38,7 @@ def register(self, builder): assert not isinstance( builder.formulae.heterogeneous_ice_nucleation_rate, Null ) - if self.het_freezing: + if self.immersion_freezing: builder.request_attribute("immersed surface area") self.rand = self.particulator.Storage.empty( self.particulator.n_sd, dtype=float @@ -59,7 +59,7 @@ def __call__(self): if not self.enable: return - if self.het_freezing: + if self.immersion_freezing: if self.singular: self.particulator.backend.freeze_singular( @@ -99,7 +99,7 @@ def __call__(self): ) - if self.hom_freezing: + if self.homogeneous_freezing: if self.singular: From 0d21c6fa475720f2c0747e1d1cf988d7ed81ca95 Mon Sep 17 00:00:00 2001 From: Tim Luettmer Date: Sat, 4 Jan 2025 19:37:10 +0100 Subject: [PATCH 06/57] Added corrected homogeneous nucleation rate --- PySDM/physics/constants_defaults.py | 2 ++ .../homogeneous_ice_nucleation_rate/koop_corr.py | 15 +++++++++++++++ 2 files changed, 17 insertions(+) create mode 100644 PySDM/physics/homogeneous_ice_nucleation_rate/koop_corr.py diff --git a/PySDM/physics/constants_defaults.py b/PySDM/physics/constants_defaults.py index 30cf65fcde..98b237c747 100644 --- a/PySDM/physics/constants_defaults.py +++ b/PySDM/physics/constants_defaults.py @@ -191,6 +191,8 @@ KOOP_2000_C4 = 29180 KOOP_UNIT = 1 / si.cm**3 / si.s +KOOP_CORR = −1.522 + J_HET = np.nan J_HOM = np.nan diff --git a/PySDM/physics/homogeneous_ice_nucleation_rate/koop_corr.py b/PySDM/physics/homogeneous_ice_nucleation_rate/koop_corr.py new file mode 100644 index 0000000000..145370a56c --- /dev/null +++ b/PySDM/physics/homogeneous_ice_nucleation_rate/koop_corr.py @@ -0,0 +1,15 @@ +""" +Koop homogeneous nucleation rate parameterization for solution droplets [Koop et al. 2000] corrected +such that it coincides with homogeneous nucleation rate parameterization for pure water droplets +[Koop and Murray 2016] at water saturation between 235K < T < 240K + ([Spichtinger et al. 2023](https://doi.org/10.5194/acp-23-2035-2023)) +""" + + +class KOOP_CORR: # pylint: disable=too-few-public-methods + def __init__(self, const): + pass + + @staticmethod + def j_hom(const, T, da_w_ice): + return 10**(const.KOOP_2000_C1 + const.KOOP_2000_C2 * da_w_ice - const.KOOP_2000_C3 * da_w_ice**2. + const.KOOP_2000_C4 * da_w_ice**3. + const.KOOP_CORR) * const.KOOP_UNIT From 6613dfafa6a0bad60be343604b7897d8ec1e2a52 Mon Sep 17 00:00:00 2001 From: Tim Luettmer Date: Sat, 4 Jan 2025 19:59:21 +0100 Subject: [PATCH 07/57] added koop and murray nucleation rate --- PySDM/physics/constants_defaults.py | 8 ++++++++ .../koop_murray.py | 15 +++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/PySDM/physics/constants_defaults.py b/PySDM/physics/constants_defaults.py index 98b237c747..e4c035c9d8 100644 --- a/PySDM/physics/constants_defaults.py +++ b/PySDM/physics/constants_defaults.py @@ -193,6 +193,14 @@ KOOP_CORR = −1.522 +KOOP_MURRAY_C0 = −3020.684 +KOOP_MURRAY_C1 = −425.921 +KOOP_MURRAY_C2 = −25.9779 +KOOP_MURRAY_C3 = −0.868451 +KOOP_MURRAY_C4 = −1.66203e−2 +KOOP_MURRAY_C5 = −1.71736e-4 +KOOP_MURRAY_C6 = −7.46953e-7 + J_HET = np.nan J_HOM = np.nan diff --git a/PySDM/physics/homogeneous_ice_nucleation_rate/koop_murray.py b/PySDM/physics/homogeneous_ice_nucleation_rate/koop_murray.py index e69de29bb2..0b5b4ce33d 100644 --- a/PySDM/physics/homogeneous_ice_nucleation_rate/koop_murray.py +++ b/PySDM/physics/homogeneous_ice_nucleation_rate/koop_murray.py @@ -0,0 +1,15 @@ +""" +Koop and Murray homogeneous nucleation rate parameterization for pure water droplets +at water saturation + ([Koop and Murray 2016](https://doi.org/10.1063/1.4962355)) +""" + + +class KOOP_MURRAY: # pylint: disable=too-few-public-methods + def __init__(self, const): + pass + + @staticmethod + def j_hom(const, T, da_w_ice): + T_diff = T - const.T_tri + return 10**(const.KOOP_MURRAY_C0 + const.KOOP_MURRAY_C1*T_diff + const.KOOP_MURRAY_C2*T_diff**2. + const.KOOP_MURRAY_C3*T_diff**3. + const.KOOP_MURRAY_C4*T_diff**4. + const.KOOP_MURRAY_C5*T_diff**5. + const.KOOP_MURRAY_C6*T_diff**6.) * const.KOOP_UNIT From 45b736fc8757a1d39ff6eb3565a764a6b62c1b78 Mon Sep 17 00:00:00 2001 From: Tim Luettmer Date: Sat, 4 Jan 2025 20:07:31 +0100 Subject: [PATCH 08/57] added homogeneous freezing temperature threshold for later use --- .../impl_numba/methods/freezing_methods.py | 15 +++++++++++++++ PySDM/physics/constants_defaults.py | 2 ++ 2 files changed, 17 insertions(+) diff --git a/PySDM/backends/impl_numba/methods/freezing_methods.py b/PySDM/backends/impl_numba/methods/freezing_methods.py index 5ea15a0699..c7f58d96a2 100644 --- a/PySDM/backends/impl_numba/methods/freezing_methods.py +++ b/PySDM/backends/impl_numba/methods/freezing_methods.py @@ -150,6 +150,21 @@ def freeze_singular( thaw=thaw, ) + + def freeze_singular_homogeneous( + self, *, attributes, temperature, relative_humidity, cell, thaw: bool + ): + self.freeze_singular_body( + SingularAttributes( + freezing_temperature=attributes.freezing_temperature.data, + water_mass=attributes.water_mass.data, + ), + temperature.data, + relative_humidity.data, + cell.data, + thaw=thaw, + ) + def freeze_time_dependent( self, *, diff --git a/PySDM/physics/constants_defaults.py b/PySDM/physics/constants_defaults.py index e4c035c9d8..06b0fa13a0 100644 --- a/PySDM/physics/constants_defaults.py +++ b/PySDM/physics/constants_defaults.py @@ -201,6 +201,8 @@ KOOP_MURRAY_C5 = −1.71736e-4 KOOP_MURRAY_C6 = −7.46953e-7 +T_hom_freeze = 235. * si.kelvin + J_HET = np.nan J_HOM = np.nan From a8acee64c3339786340da5ce697292e0d17d8176 Mon Sep 17 00:00:00 2001 From: Tim Luettmer Date: Sat, 4 Jan 2025 20:12:51 +0100 Subject: [PATCH 09/57] Fixed some typos in the code --- .../impl_numba/methods/freezing_methods.py | 15 +-------------- PySDM/physics/constants_defaults.py | 18 +++++++++--------- 2 files changed, 10 insertions(+), 23 deletions(-) diff --git a/PySDM/backends/impl_numba/methods/freezing_methods.py b/PySDM/backends/impl_numba/methods/freezing_methods.py index c7f58d96a2..df37955f9b 100644 --- a/PySDM/backends/impl_numba/methods/freezing_methods.py +++ b/PySDM/backends/impl_numba/methods/freezing_methods.py @@ -150,20 +150,7 @@ def freeze_singular( thaw=thaw, ) - - def freeze_singular_homogeneous( - self, *, attributes, temperature, relative_humidity, cell, thaw: bool - ): - self.freeze_singular_body( - SingularAttributes( - freezing_temperature=attributes.freezing_temperature.data, - water_mass=attributes.water_mass.data, - ), - temperature.data, - relative_humidity.data, - cell.data, - thaw=thaw, - ) + def freeze_time_dependent( self, diff --git a/PySDM/physics/constants_defaults.py b/PySDM/physics/constants_defaults.py index 06b0fa13a0..d8e175ec23 100644 --- a/PySDM/physics/constants_defaults.py +++ b/PySDM/physics/constants_defaults.py @@ -191,15 +191,15 @@ KOOP_2000_C4 = 29180 KOOP_UNIT = 1 / si.cm**3 / si.s -KOOP_CORR = −1.522 - -KOOP_MURRAY_C0 = −3020.684 -KOOP_MURRAY_C1 = −425.921 -KOOP_MURRAY_C2 = −25.9779 -KOOP_MURRAY_C3 = −0.868451 -KOOP_MURRAY_C4 = −1.66203e−2 -KOOP_MURRAY_C5 = −1.71736e-4 -KOOP_MURRAY_C6 = −7.46953e-7 +KOOP_CORR = -1.522 + +KOOP_MURRAY_C0 = -3020.684 +KOOP_MURRAY_C1 = -425.921 +KOOP_MURRAY_C2 = -25.9779 +KOOP_MURRAY_C3 = -0.868451 +KOOP_MURRAY_C4 = -1.66203e-2 +KOOP_MURRAY_C5 = -1.71736e-4 +KOOP_MURRAY_C6 = -7.46953e-7 T_hom_freeze = 235. * si.kelvin From 2074ed5dc301f96e97e5a078ea3214a36f28b3c8 Mon Sep 17 00:00:00 2001 From: Tim Luettmer Date: Sat, 4 Jan 2025 20:16:48 +0100 Subject: [PATCH 10/57] Added time dependent hom. freezing test to test_freezing_methods unit test --- .../backends/test_freezing_methods.py | 126 ++++++++++++++++++ 1 file changed, 126 insertions(+) diff --git a/tests/unit_tests/backends/test_freezing_methods.py b/tests/unit_tests/backends/test_freezing_methods.py index 719a738eb9..fc28bab85b 100644 --- a/tests/unit_tests/backends/test_freezing_methods.py +++ b/tests/unit_tests/backends/test_freezing_methods.py @@ -208,6 +208,132 @@ def low(t): np.testing.assert_array_less(low(arg), data) + @staticmethod + @pytest.mark.parametrize("double_precision", (True,False)) + # pylint: disable=too-many-locals + def test_homogeneous_freeze_time_dependent(backend_class, double_precision, plot=False): + if backend_class.__name__ == "Numba" and not double_precision: + pytest.skip() + if backend_class.__name__ == "ThrustRTC": + pytest.skip() + + + + # Arrange + seed = 44 + cases = ( + {"dt": 5e5, "N": 1}, + {"dt": 1e6, "N": 1}, + {"dt": 5e5, "N": 8}, + {"dt": 1e6, "N": 8}, + {"dt": 5e5, "N": 16}, + {"dt": 1e6, "N": 16}, + ) + rate = 1e-9 + + number_of_real_droplets = 1024 + total_time = ( + 0.25e9 # effectively interpreted here as seconds, i.e. cycle = 1 * si.s + ) + + # dummy (but must-be-set) values + initial_water_mass = ( + 1000 # for sign flip (ice water has negative volumes) + ) + d_v = 666 # products use conc., dividing there, multiplying here, value does not matter + + droplet_volume = initial_water_mass / 1000. + + def hgh(t): + return np.exp(-0.75 * rate * (t - total_time / 4)) + + def low(t): + return np.exp(-1.25 * rate * (t + total_time / 4)) + + + RHi = 1.5 + T = 230 + + # Act + output = {} + + formulae = Formulae( + particle_shape_and_density="MixedPhaseSpheres", + homogeneous_ice_nucleation_rate="Constant", + constants={"J_HOM": rate / droplet_volume}, + seed=seed, + ) + products = (IceWaterContent(name="qi"),) + + for case in cases: + n_sd = int(number_of_real_droplets // case["N"]) + assert n_sd == number_of_real_droplets / case["N"] + assert total_time // case["dt"] == total_time / case["dt"] + + key = f"{case['dt']}:{case['N']}" + output[key] = {"unfrozen_fraction": [], "dt": case["dt"], "N": case["N"]} + + env = Box(dt=case["dt"], dv=d_v) + builder = Builder( + n_sd=n_sd, + backend=backend_class( + formulae=formulae, double_precision=double_precision + ), + environment=env, + ) + builder.add_dynamic(Freezing(singular=False,homogeneous_freezing=True,immersion_freezing=False)) + attributes = { + "multiplicity": np.full(n_sd, int(case["N"])), + "water mass": np.full(n_sd, initial_water_mass), + } + particulator = builder.build(attributes=attributes, products=products) + + + pvs_ice = particulator.formulae.saturation_vapour_pressure.pvs_ice(T) + pvs_water = particulator.formulae.saturation_vapour_pressure.pvs_water(T) + vapour_pressure = RHi * pvs_ice + RH = vapour_pressure / pvs_water + particulator.environment["RH"] = RH + particulator.environment["a_w_ice"] = pvs_ice / pvs_water + particulator.environment["T"] = T + + cell_id = 0 + for i in range(int(total_time / case["dt"]) + 1): + particulator.run(0 if i == 0 else 1) + + ice_mass_per_volume = particulator.products["qi"].get()[cell_id] + ice_mass = ice_mass_per_volume * d_v + ice_number = ice_mass / initial_water_mass + unfrozen_fraction = 1 - ice_number / number_of_real_droplets + output[key]["unfrozen_fraction"].append(unfrozen_fraction) + + + # Plot + fit_x = np.linspace(0, total_time, num=100) + fit_y = np.exp(-rate * fit_x) + + for out in output.values(): + pyplot.step( + out["dt"] * np.arange(len(out["unfrozen_fraction"])), + out["unfrozen_fraction"], + label=f"dt={out['dt']:.2g} / N={out['N']}", + marker=".", + linewidth=1 + out["N"] // 8, + ) + + _plot_fit(fit_x, fit_y, low, hgh, total_time) + if plot: + pyplot.show() + + # Assert + for out in output.values(): + data = np.asarray(out["unfrozen_fraction"]) + arg = out["dt"] * np.arange(len(data)) + np.testing.assert_array_less(data, hgh(arg)) + np.testing.assert_array_less(low(arg), data) + + + def _plot_fit(fit_x, fit_y, low, hgh, total_time): pyplot.plot( fit_x, fit_y, color="black", linestyle="--", label="theory", linewidth=5 From 021ef2439ef505a02331a9640b22c7f0924d0390 Mon Sep 17 00:00:00 2001 From: Tim Luettmer Date: Wed, 15 Jan 2025 18:04:43 +0100 Subject: [PATCH 11/57] Changed homogenous freezing routine ant test to match new moist enviroment with RH_ice. Deleted singular case for hom. freezing --- .../impl_numba/methods/freezing_methods.py | 14 +++--- PySDM/dynamics/freezing.py | 48 +++++++++---------- .../backends/test_freezing_methods.py | 8 +--- 3 files changed, 31 insertions(+), 39 deletions(-) diff --git a/PySDM/backends/impl_numba/methods/freezing_methods.py b/PySDM/backends/impl_numba/methods/freezing_methods.py index df37955f9b..d2cc71ec01 100644 --- a/PySDM/backends/impl_numba/methods/freezing_methods.py +++ b/PySDM/backends/impl_numba/methods/freezing_methods.py @@ -105,24 +105,24 @@ def freeze_time_dependent_homogeneous_body( # pylint: disable=unused-argument,t cell, a_w_ice, temperature, - relative_humidity, + relative_humidity_ice, record_freezing_temperature, freezing_temperature, thaw, ): - + n_sd = len(attributes.water_mass) for i in numba.prange(n_sd): # pylint: disable=not-an-iterable cell_id = cell[i] - relative_humidity_ice = relative_humidity[cell_id] / a_w_ice[cell_id] + #relative_humidity_ice = relative_humidity[cell_id] / a_w_ice[cell_id] if thaw and frozen_and_above_freezing_point( attributes.water_mass[i], temperature[cell_id] ): _thaw(attributes.water_mass, i) elif unfrozen_and_ice_saturated( - attributes.water_mass[i], relative_humidity_ice + attributes.water_mass[i], relative_humidity_ice[cell_id] ): - d_a_w_ice = (relative_humidity_ice - 1) * a_w_ice[cell_id] + d_a_w_ice = (relative_humidity_ice[cell_id] - 1) * a_w_ice[cell_id] rate = j_hom(temperature, d_a_w_ice) # TODO #594: this assumes constant T throughout timestep, can we do better? prob = 1 - np.exp( # TODO #599: common code for Poissonian prob @@ -195,7 +195,7 @@ def freeze_time_dependent_homogeneous( cell, a_w_ice, temperature, - relative_humidity, + relative_humidity_ice, record_freezing_temperature, freezing_temperature, thaw: bool @@ -210,7 +210,7 @@ def freeze_time_dependent_homogeneous( cell.data, a_w_ice.data, temperature.data, - relative_humidity.data, + relative_humidity_ice.data, record_freezing_temperature=record_freezing_temperature, freezing_temperature=( freezing_temperature.data if record_freezing_temperature else None diff --git a/PySDM/dynamics/freezing.py b/PySDM/dynamics/freezing.py index 95bf3ee54b..2956e641d2 100644 --- a/PySDM/dynamics/freezing.py +++ b/PySDM/dynamics/freezing.py @@ -1,5 +1,5 @@ """ -immersion freezing using either singular or time-dependent formulation +homogeneous freezing and heterogeneous freezing (singular and time-dependent immersion freezing) """ from PySDM.backends.impl_common.freezing_attributes import ( @@ -100,31 +100,27 @@ def __call__(self): if self.homogeneous_freezing: - - - if self.singular: - pass - else: - self.rand.urand(self.rng) - self.particulator.backend.freeze_time_dependent_homogeneous( - rand=self.rand, - attributes=TimeDependentHomogeneousAttributes( - volume=self.particulator.attributes["volume"], - water_mass=self.particulator.attributes["water mass"], - ), - timestep=self.particulator.dt, - cell=self.particulator.attributes["cell id"], - a_w_ice=self.particulator.environment["a_w_ice"], - temperature=self.particulator.environment["T"], - relative_humidity=self.particulator.environment["RH"], - record_freezing_temperature=self.record_freezing_temperature, - freezing_temperature=( - self.particulator.attributes["freezing temperature"] - if self.record_freezing_temperature - else None - ), - thaw=self.thaw, - ) + + self.rand.urand(self.rng) + self.particulator.backend.freeze_time_dependent_homogeneous( + rand=self.rand, + attributes=TimeDependentHomogeneousAttributes( + volume=self.particulator.attributes["volume"], + water_mass=self.particulator.attributes["water mass"], + ), + timestep=self.particulator.dt, + cell=self.particulator.attributes["cell id"], + a_w_ice=self.particulator.environment["a_w_ice"], + temperature=self.particulator.environment["T"], + relative_humidity_ice=self.particulator.environment["RH_ice"], + record_freezing_temperature=self.record_freezing_temperature, + freezing_temperature=( + self.particulator.attributes["freezing temperature"] + if self.record_freezing_temperature + else None + ), + thaw=self.thaw, + ) diff --git a/tests/unit_tests/backends/test_freezing_methods.py b/tests/unit_tests/backends/test_freezing_methods.py index fc28bab85b..5b8484d3d5 100644 --- a/tests/unit_tests/backends/test_freezing_methods.py +++ b/tests/unit_tests/backends/test_freezing_methods.py @@ -286,14 +286,10 @@ def low(t): "multiplicity": np.full(n_sd, int(case["N"])), "water mass": np.full(n_sd, initial_water_mass), } - particulator = builder.build(attributes=attributes, products=products) - - + particulator = builder.build(attributes=attributes, products=products) pvs_ice = particulator.formulae.saturation_vapour_pressure.pvs_ice(T) pvs_water = particulator.formulae.saturation_vapour_pressure.pvs_water(T) - vapour_pressure = RHi * pvs_ice - RH = vapour_pressure / pvs_water - particulator.environment["RH"] = RH + particulator.environment["RH_ice"] = RHi particulator.environment["a_w_ice"] = pvs_ice / pvs_water particulator.environment["T"] = T From 7f6d360ea883a97fb51014712d25fa1fa631ae05 Mon Sep 17 00:00:00 2001 From: Tim Luettmer Date: Fri, 14 Feb 2025 15:12:22 +0100 Subject: [PATCH 12/57] Added Kaercher Lohmann Example and renamed koop hom. freezing class --- .../__init__.py | 2 +- .../homogeneous_ice_nucleation_rate/koop.py | 2 +- .../Kaercher_Lohmann_2002/run.py | 161 ++++++++++++++++++ .../Kaercher_Lohmann_2002/settings.py | 67 ++++++++ .../Kaercher_Lohmann_2002/simulation.py | 160 +++++++++++++++++ 5 files changed, 390 insertions(+), 2 deletions(-) create mode 100644 examples/PySDM_examples/Kaercher_Lohmann_2002/run.py create mode 100644 examples/PySDM_examples/Kaercher_Lohmann_2002/settings.py create mode 100644 examples/PySDM_examples/Kaercher_Lohmann_2002/simulation.py diff --git a/PySDM/physics/homogeneous_ice_nucleation_rate/__init__.py b/PySDM/physics/homogeneous_ice_nucleation_rate/__init__.py index 3cb1eabeb4..07d2e99492 100644 --- a/PySDM/physics/homogeneous_ice_nucleation_rate/__init__.py +++ b/PySDM/physics/homogeneous_ice_nucleation_rate/__init__.py @@ -4,4 +4,4 @@ from .constant import Constant from .null import Null -from .koop import KOOP +from .koop import Koop2000 diff --git a/PySDM/physics/homogeneous_ice_nucleation_rate/koop.py b/PySDM/physics/homogeneous_ice_nucleation_rate/koop.py index 144b42edde..4358f59c21 100644 --- a/PySDM/physics/homogeneous_ice_nucleation_rate/koop.py +++ b/PySDM/physics/homogeneous_ice_nucleation_rate/koop.py @@ -5,7 +5,7 @@ """ -class KOOP: # pylint: disable=too-few-public-methods +class Koop2000: # pylint: disable=too-few-public-methods def __init__(self, const): pass diff --git a/examples/PySDM_examples/Kaercher_Lohmann_2002/run.py b/examples/PySDM_examples/Kaercher_Lohmann_2002/run.py new file mode 100644 index 0000000000..f1757e85c7 --- /dev/null +++ b/examples/PySDM_examples/Kaercher_Lohmann_2002/run.py @@ -0,0 +1,161 @@ +from matplotlib import pyplot +from matplotlib.backends.backend_pdf import PdfPages +import numpy as np + +from PySDM_examples.Kaercher_Lohmann_2002.settings import setups +from PySDM_examples.Kaercher_Lohmann_2002.simulation import Simulation +from PySDM_examples.Kaercher_Lohmann_2002.reference import critical_supersaturation + + +kgtoug = 1.e9 + + +def plot( output, setting, pp ): + + + time = output["t"] + temperature = np.asarray(output["T"]) + z = output["z"] + + rh = output["RH"] + rhi = output["RHi"] + rhi_crit = critical_supersaturation(temperature) + + + print(f"{rh=},{rhi=}") + + lwc = np.asarray(output["LWC"]) * kgtoug + iwc = abs(np.asarray(output["IWC"])) * kgtoug + #twc = np.asarray(output["TWC"]) * kgtoug + twc = lwc + iwc + qv = output["qv"] + + print(f"{lwc=},{iwc=},{twc=},{qv=}") + + ns = output["ns"] + ni = output["ni"] + rs = output["rs"] + ri = output["ri"] + print(f"{ns=},{ni=},{rs=},{ri=},") + + print(f"{z=},") + + fig, axs = pyplot.subplots(3, 2, figsize=(10, 10), sharex=True) + + + title = f"w: {setting.w_updraft:.2f} m s-1 T0: {setting.T0:.2f} K Nsd: {setting.n_sd:d} $\kappa$: {setting.kappa:.2f}" + # $\mathrm{m \, s^{-1}}$ + fig.suptitle(title) + + axTz = axs[0,0] + + axTz.plot( + time, z, color="black", linestyle="-", label="dz", linewidth=5 + ) + axTz.set_ylabel("vertical displacemet [m]") + axTz.set_ylim(-5, 1000) + twin = axTz.twinx() + twin.plot( + time, temperature, color="red", linestyle="-", label="T", linewidth=5 + ) + twin.set_ylim(190, 250) + twin.legend(loc='upper right') + twin.set_ylabel("temperature [K]") + axTz.legend(loc='upper left') + axTz.set_xlabel("time [s]") + + + axRH = axs[0,1] + + axRH.plot( + time, rh, color="blue", linestyle="-", label="water", linewidth=5 + ) + axRH.plot( + time, rhi, color="red", linestyle="-", label="ice", linewidth=5 + ) + axRH.plot( + time, rhi_crit, color="black", linestyle="-", label="crit", linewidth=5 + ) + axRH.legend() + axRH.set_xlabel("time [s]") + axRH.set_ylabel("relative humidity [%]") + axRH.set_ylim(50, 200) + + + + axWC = axs[1,0] + + + axWC.plot( + time, twc, color="black", linestyle="--", label="total", linewidth=5 + ) + axWC.plot( + time, lwc, color="blue", linestyle="-", label="water", linewidth=5 + ) + axWC.plot( + time, iwc, color="red", linestyle="-", label="ice", linewidth=5 + ) + #axWC.set_yscale('log') + axWC.legend() + axWC.set_ylim(-0.5, 10) + axWC.set_xlabel("time [s]") + axWC.set_ylabel(r"mass content [$\mathrm{\mu g \, kg^{-1}}$]") + + + + axN = axs[1,1] + + + axN.plot( + time, ns, color="blue", linestyle="-", label="droplet", linewidth=5 ) + axN.plot( + time, ni, color="red", linestyle="-", label="ice", linewidth=5 ) + + axN.legend() + axN.set_ylim(-0.5, 3000) + axN.set_xlabel("time [s]") + axN.set_ylabel(r"number concentration [$\mathrm{cm^{-3}}$]") + + + axR = axs[2,0] + + + axR.plot( + time, rs, color="blue", linestyle="-", label="droplet", linewidth=5 ) + axR.plot( + time, ri, color="red", linestyle="-", label="ice", linewidth=5 ) + + + axR.legend() + #axR.set_ylim(-0.5, 3000) + axR.set_xlabel("time [s]") + axR.set_ylabel(r"mean radius [µm]") + + + fig.tight_layout() + pp.savefig() + +pp = PdfPages( "hom_freezing.pdf" ) + + +for setting in setups: + + + model = Simulation(setting) + + + + + + + output = model.run() + + + #print( output ) + + + plot( output, setting, pp ) + + + +pp.close() diff --git a/examples/PySDM_examples/Kaercher_Lohmann_2002/settings.py b/examples/PySDM_examples/Kaercher_Lohmann_2002/settings.py new file mode 100644 index 0000000000..dda5d78c9f --- /dev/null +++ b/examples/PySDM_examples/Kaercher_Lohmann_2002/settings.py @@ -0,0 +1,67 @@ +import numpy as np +from pystrict import strict + +from PySDM import Formulae +from PySDM.physics.constants import si + + + +@strict +class Settings: + def __init__( + self, + w_updraft: float, + T0: float, + N_solution_droplet: float, + r_solution_droplet: float, + ): + print( w_updraft, T0, N_solution_droplet, r_solution_droplet ) + mass_of_dry_air = 1000 * si.kilogram + self.formulae = Formulae( + # saturation_vapour_pressure="AugustRocheMagnus", + ) + const = self.formulae.constants + self.mass_of_dry_air = mass_of_dry_air + self.p0 = 220 * si.hectopascals + self.RHi0 = 1. + self.kappa = 0.64 + self.T0 = T0 + + pvs_i = self.formulae.saturation_vapour_pressure.pvs_ice(self.T0) + self.initial_water_vapour_mixing_ratio = const.eps / ( + self.p0 / self.RHi0 / pvs_i - 1 + ) + self.w_updraft = w_updraft + self.r_solution_droplet = r_solution_droplet + self.N_solution_drople = N_solution_droplet + self.n_in_dv = N_solution_droplet / const.rho_STP * mass_of_dry_air + + + self.t_duration = 5400 # total duration of simulation + self.dt = 1. + self.n_output = 10 # number of output steps + + +w_updrafts = ( + 10 * si.centimetre / si.second, +) + +T_starts = ( 220 * si.kelvin, ) + +N_solution_droplets = ( 2500 / si.centimetre**3, ) + +r_solution_droplets = ( 0.0555 * si.micrometre, ) + +setups = [] +for w_updraft in w_updrafts: + for T0 in T_starts: + for N_solution_droplet in N_solution_droplets: + for r_solution_droplet in r_solution_droplets: + setups.append( + Settings( + w_updraft=w_updraft, + T0=T0, + N_solution_droplet=N_solution_droplet, + r_solution_droplet=r_solution_droplet, + ) + ) diff --git a/examples/PySDM_examples/Kaercher_Lohmann_2002/simulation.py b/examples/PySDM_examples/Kaercher_Lohmann_2002/simulation.py new file mode 100644 index 0000000000..5aef7bed88 --- /dev/null +++ b/examples/PySDM_examples/Kaercher_Lohmann_2002/simulation.py @@ -0,0 +1,160 @@ +import numpy as np + +import PySDM.products as PySDM_products +from PySDM.backends import CPU +from PySDM.builder import Builder +from PySDM.dynamics import AmbientThermodynamics, Condensation, Freezing +from PySDM.environments import Parcel +from PySDM.physics import constants as const +from PySDM.initialisation import discretise_multiplicities, equilibrate_wet_radii + + +class Simulation: + def __init__(self, settings, backend=CPU): + # t_half = settings.z_half / settings.w_avg + + # dt_output = (2 * t_half) / settings.n_output + # self.n_substeps = 1 + # while dt_output / self.n_substeps >= settings.dt_max: # TODO #334 dt_max + # self.n_substeps += 1 + + dt = settings.dt + T0 = settings.T0 + + formulae = settings.formulae + + env = Parcel( + mixed_phase = True, + dt=dt, + mass_of_dry_air=settings.mass_of_dry_air, + p0=settings.p0, + initial_water_vapour_mixing_ratio=settings.initial_water_vapour_mixing_ratio, + T0=settings.T0, + w=settings.w_updraft, + ) + + builder = Builder( + backend=backend( + formulae=settings.formulae, + **( + {"override_jit_flags": {"parallel": False}} + if backend == CPU + else {} + ) + ), + n_sd=settings.n_sd, + environment=env, + ) + + + builder.add_dynamic(AmbientThermodynamics()) + builder.add_dynamic(Condensation()) + builder.add_dynamic(Freezing(singular=False,homogeneous_freezing=True,immersion_freezing=False)) + + + multiplicities = discretise_multiplicities(settings.specific_concentration * env.mass_of_dry_air) + r_dry = settings.r_dry + v_dry = settings.formulae.trivia.volume(radius=r_dry) + kappa = settings.kappa + + # print( multiplicities ) + # print( settings.specific_concentration * env.mass_of_dry_air) + + + r_wet = equilibrate_wet_radii(r_dry=r_dry, environment=builder.particulator.environment, kappa_times_dry_volume=kappa * v_dry) + # print( f"{kappa=},{r_wet=},{r_dry=}," ) + + + attributes = { + "multiplicity": multiplicities, + 'dry volume': v_dry, + 'kappa times dry volume': kappa * v_dry, + 'volume': formulae.trivia.volume(radius=r_wet), + } + + + + products = [ + PySDM_products.ParcelDisplacement(name="z"), + PySDM_products.Time(name="t"), + PySDM_products.AmbientRelativeHumidity(name="RH", unit="%"), + #PySDM_products.AmbientRelativeHumidity(name="RH_ice", unit="%"), + PySDM_products.AmbientRelativeHumidity(var="RH",name="RH_ice", ice=True, unit="%"), + PySDM_products.AmbientTemperature(name="T"), + PySDM_products.WaterMixingRatio(name="water", radius_range=(0, np.inf)), + PySDM_products.WaterMixingRatio(name="ice", radius_range=(-np.inf, 0)), + PySDM_products.WaterMixingRatio(name="total", radius_range=(-np.inf, np.inf)), + PySDM_products.AmbientWaterVapourMixingRatio( + name="vapour", var="water_vapour_mixing_ratio" + ), + PySDM_products.ParticleConcentration( + name='n_s', unit='1/cm**3', + radius_range=(0, np.inf)), + PySDM_products.ParticleConcentration( + name='n_i', unit='1/cm**3', + radius_range=(-np.inf,0)), + PySDM_products.MeanRadius( + name='r_s', unit='µm', + radius_range=(0,np.inf)), + PySDM_products.MeanRadius( + name='r_i', unit='µm', + radius_range=(-np.inf,0)), + # PySDM_products.ParticleConcentration( + # name='n_c', unit='1/kg', + # radius_range=(0, np.inf)), + ] + + self.particulator = builder.build(attributes, products) + builder.request_attribute("critical supersaturation") + + + self.n_output = settings.n_output + self.n_substeps = int(settings.t_duration / dt / self.n_output) + + print( settings.t_duration, dt, self.n_output, self.n_substeps ) + + print( self.particulator.n_sd ) + + + + def save(self, output): + cell_id = 0 + + output["z"].append(self.particulator.products["z"].get()[cell_id]) + output["t"].append(self.particulator.products["t"].get()) + output["RH"].append(self.particulator.products["RH"].get()[cell_id]) + output["RHi"].append(self.particulator.products["RH_ice"].get()[cell_id]) + output["T"].append(self.particulator.products["T"].get()[cell_id]) + output["LWC"].append(self.particulator.products["water"].get()[cell_id]) + output["IWC"].append(self.particulator.products["ice"].get()[cell_id]) + # output["TWC"].append(self.particulator.products["total"].get()[cell_id]) + output["qv"].append(self.particulator.products["vapour"].get()[cell_id]) + output["ns"].append(self.particulator.products["n_s"].get()[cell_id]) + output["ni"].append(self.particulator.products["n_i"].get()[cell_id]) + output["rs"].append(self.particulator.products["r_s"].get()[cell_id]) + output["ri"].append(self.particulator.products["r_i"].get()[cell_id]) + + def run(self): + output = { + "t": [], + "z": [], + "RH": [], + "RHi": [], + "T": [], + "LWC": [], + "IWC": [], + # "TWC": [], + "qv": [], + "ns": [], + "ni": [], + "rs": [], + "ri": [], + } + + self.save(output) + for _ in range(self.n_output): + self.particulator.run(self.n_substeps) + #print( self.particulator.products["t"].get() ) + self.save(output) + + return output From f34da15d5248f3fe36a76996a71609ec46f629fa Mon Sep 17 00:00:00 2001 From: Tim Luettmer Date: Sat, 15 Feb 2025 17:49:27 +0100 Subject: [PATCH 13/57] Expanded Kaercher Lohmann Example --- .../Kaercher_Lohmann_2002/reference.py | 5 ++ .../Kaercher_Lohmann_2002/run.py | 25 ++---- .../Kaercher_Lohmann_2002/settings.py | 77 ++++++++++++------- .../Kaercher_Lohmann_2002/simulation.py | 15 +--- 4 files changed, 66 insertions(+), 56 deletions(-) create mode 100644 examples/PySDM_examples/Kaercher_Lohmann_2002/reference.py diff --git a/examples/PySDM_examples/Kaercher_Lohmann_2002/reference.py b/examples/PySDM_examples/Kaercher_Lohmann_2002/reference.py new file mode 100644 index 0000000000..8dc82a2eb0 --- /dev/null +++ b/examples/PySDM_examples/Kaercher_Lohmann_2002/reference.py @@ -0,0 +1,5 @@ + + + +def critical_supersaturation(T): + return 2.349 - T / 259. \ No newline at end of file diff --git a/examples/PySDM_examples/Kaercher_Lohmann_2002/run.py b/examples/PySDM_examples/Kaercher_Lohmann_2002/run.py index f1757e85c7..fb141a2494 100644 --- a/examples/PySDM_examples/Kaercher_Lohmann_2002/run.py +++ b/examples/PySDM_examples/Kaercher_Lohmann_2002/run.py @@ -2,9 +2,9 @@ from matplotlib.backends.backend_pdf import PdfPages import numpy as np -from PySDM_examples.Kaercher_Lohmann_2002.settings import setups -from PySDM_examples.Kaercher_Lohmann_2002.simulation import Simulation -from PySDM_examples.Kaercher_Lohmann_2002.reference import critical_supersaturation +from settings import setups +from simulation import Simulation +from reference import critical_supersaturation kgtoug = 1.e9 @@ -19,16 +19,15 @@ def plot( output, setting, pp ): rh = output["RH"] rhi = output["RHi"] - rhi_crit = critical_supersaturation(temperature) + rhi_crit = critical_supersaturation(temperature) * 100. - print(f"{rh=},{rhi=}") + print(f"{rh=},{rhi=},{rhi_crit=}") lwc = np.asarray(output["LWC"]) * kgtoug iwc = abs(np.asarray(output["IWC"])) * kgtoug - #twc = np.asarray(output["TWC"]) * kgtoug twc = lwc + iwc - qv = output["qv"] + qv = np.asarray(output["qv"]) * kgtoug print(f"{lwc=},{iwc=},{twc=},{qv=}") @@ -38,12 +37,11 @@ def plot( output, setting, pp ): ri = output["ri"] print(f"{ns=},{ni=},{rs=},{ri=},") - print(f"{z=},") fig, axs = pyplot.subplots(3, 2, figsize=(10, 10), sharex=True) - title = f"w: {setting.w_updraft:.2f} m s-1 T0: {setting.T0:.2f} K Nsd: {setting.n_sd:d} $\kappa$: {setting.kappa:.2f}" + title = f"w: {setting.w_updraft:.2f} m s-1 T0: {setting.initial_temperature:.2f} K Nsd: {setting.n_sd:d} $\kappa$: {setting.kappa:.2f}" # $\mathrm{m \, s^{-1}}$ fig.suptitle(title) @@ -143,17 +141,8 @@ def plot( output, setting, pp ): model = Simulation(setting) - - - - - output = model.run() - - #print( output ) - - plot( output, setting, pp ) diff --git a/examples/PySDM_examples/Kaercher_Lohmann_2002/settings.py b/examples/PySDM_examples/Kaercher_Lohmann_2002/settings.py index dda5d78c9f..8c6e53c7e1 100644 --- a/examples/PySDM_examples/Kaercher_Lohmann_2002/settings.py +++ b/examples/PySDM_examples/Kaercher_Lohmann_2002/settings.py @@ -3,6 +3,8 @@ from PySDM import Formulae from PySDM.physics.constants import si +from PySDM.initialisation.spectra import Lognormal +from PySDM.initialisation.sampling import spectral_sampling @@ -10,31 +12,44 @@ class Settings: def __init__( self, + n_sd: int, w_updraft: float, T0: float, N_solution_droplet: float, r_solution_droplet: float, + kappa: float, ): - print( w_updraft, T0, N_solution_droplet, r_solution_droplet ) - mass_of_dry_air = 1000 * si.kilogram + + self.n_sd = n_sd + self.w_updraft = w_updraft + self.r_solution_droplet = r_solution_droplet + self.N_solution_drople = N_solution_droplet + + self.mass_of_dry_air = 1000 * si.kilogram + self.initial_pressure = 220 * si.hectopascals + self.initial_ice_supersaturation = 1. + self.kappa = kappa + self.initial_temperature = T0 + self.formulae = Formulae( - # saturation_vapour_pressure="AugustRocheMagnus", + particle_shape_and_density="MixedPhaseSpheres", + homogeneous_ice_nucleation_rate="Constant", + constants={"J_HOM": 1.e15}, ) const = self.formulae.constants - self.mass_of_dry_air = mass_of_dry_air - self.p0 = 220 * si.hectopascals - self.RHi0 = 1. - self.kappa = 0.64 - self.T0 = T0 - - pvs_i = self.formulae.saturation_vapour_pressure.pvs_ice(self.T0) + pvs_i = self.formulae.saturation_vapour_pressure.pvs_ice(self.initial_temperature) self.initial_water_vapour_mixing_ratio = const.eps / ( - self.p0 / self.RHi0 / pvs_i - 1 + self.initial_pressure / self.initial_ice_supersaturation / pvs_i - 1 ) - self.w_updraft = w_updraft - self.r_solution_droplet = r_solution_droplet - self.N_solution_drople = N_solution_droplet - self.n_in_dv = N_solution_droplet / const.rho_STP * mass_of_dry_air + dry_air_density = (self.formulae.trivia.p_d(self.initial_pressure, self.initial_water_vapour_mixing_ratio ) + / self.initial_temperature + / const.Rd ) + + spectrum = Lognormal(norm_factor=N_solution_droplet / dry_air_density, m_mode=r_solution_droplet, s_geom=1.5) + self.r_dry, self.specific_concentration = spectral_sampling.Logarithmic(spectrum).sample(n_sd) + + + self.t_duration = 5400 # total duration of simulation @@ -42,6 +57,8 @@ def __init__( self.n_output = 10 # number of output steps +n_sds = ( 1000, ) + w_updrafts = ( 10 * si.centimetre / si.second, ) @@ -52,16 +69,22 @@ def __init__( r_solution_droplets = ( 0.0555 * si.micrometre, ) +kappas = ( 0.64, ) + setups = [] -for w_updraft in w_updrafts: - for T0 in T_starts: - for N_solution_droplet in N_solution_droplets: - for r_solution_droplet in r_solution_droplets: - setups.append( - Settings( - w_updraft=w_updraft, - T0=T0, - N_solution_droplet=N_solution_droplet, - r_solution_droplet=r_solution_droplet, - ) - ) +for n_sd in n_sds: + for w_updraft in w_updrafts: + for T0 in T_starts: + for N_solution_droplet in N_solution_droplets: + for r_solution_droplet in r_solution_droplets: + for kappa in kappas: + setups.append( + Settings( + n_sd=n_sd, + w_updraft=w_updraft, + T0=T0, + N_solution_droplet=N_solution_droplet, + r_solution_droplet=r_solution_droplet, + kappa=kappa, + ) + ) diff --git a/examples/PySDM_examples/Kaercher_Lohmann_2002/simulation.py b/examples/PySDM_examples/Kaercher_Lohmann_2002/simulation.py index 5aef7bed88..c8937270ff 100644 --- a/examples/PySDM_examples/Kaercher_Lohmann_2002/simulation.py +++ b/examples/PySDM_examples/Kaercher_Lohmann_2002/simulation.py @@ -19,7 +19,6 @@ def __init__(self, settings, backend=CPU): # self.n_substeps += 1 dt = settings.dt - T0 = settings.T0 formulae = settings.formulae @@ -27,9 +26,9 @@ def __init__(self, settings, backend=CPU): mixed_phase = True, dt=dt, mass_of_dry_air=settings.mass_of_dry_air, - p0=settings.p0, + p0=settings.initial_pressure, initial_water_vapour_mixing_ratio=settings.initial_water_vapour_mixing_ratio, - T0=settings.T0, + T0=settings.initial_temperature, w=settings.w_updraft, ) @@ -56,13 +55,8 @@ def __init__(self, settings, backend=CPU): r_dry = settings.r_dry v_dry = settings.formulae.trivia.volume(radius=r_dry) kappa = settings.kappa - - # print( multiplicities ) - # print( settings.specific_concentration * env.mass_of_dry_air) - r_wet = equilibrate_wet_radii(r_dry=r_dry, environment=builder.particulator.environment, kappa_times_dry_volume=kappa * v_dry) - # print( f"{kappa=},{r_wet=},{r_dry=}," ) attributes = { @@ -78,8 +72,8 @@ def __init__(self, settings, backend=CPU): PySDM_products.ParcelDisplacement(name="z"), PySDM_products.Time(name="t"), PySDM_products.AmbientRelativeHumidity(name="RH", unit="%"), - #PySDM_products.AmbientRelativeHumidity(name="RH_ice", unit="%"), - PySDM_products.AmbientRelativeHumidity(var="RH",name="RH_ice", ice=True, unit="%"), + PySDM_products.AmbientRelativeHumidity(name="RH_ice", unit="%"), + #PySDM_products.AmbientRelativeHumidity(var="RH",name="RH_ice", ice=True, unit="%"), PySDM_products.AmbientTemperature(name="T"), PySDM_products.WaterMixingRatio(name="water", radius_range=(0, np.inf)), PySDM_products.WaterMixingRatio(name="ice", radius_range=(-np.inf, 0)), @@ -105,7 +99,6 @@ def __init__(self, settings, backend=CPU): ] self.particulator = builder.build(attributes, products) - builder.request_attribute("critical supersaturation") self.n_output = settings.n_output From bf4367b225c89f7a573716ed08fdcdb8f3f519b5 Mon Sep 17 00:00:00 2001 From: Tim Luettmer Date: Sat, 15 Feb 2025 18:28:09 +0100 Subject: [PATCH 14/57] Added hom. freezing rates in KA02 example --- .../__init__.py | 2 ++ .../koop_corr.py | 2 +- .../koop_murray.py | 2 +- .../Kaercher_Lohmann_2002/run.py | 4 +-- .../Kaercher_Lohmann_2002/settings.py | 27 ++++++++++++------- 5 files changed, 23 insertions(+), 14 deletions(-) diff --git a/PySDM/physics/homogeneous_ice_nucleation_rate/__init__.py b/PySDM/physics/homogeneous_ice_nucleation_rate/__init__.py index 07d2e99492..95fe7ed622 100644 --- a/PySDM/physics/homogeneous_ice_nucleation_rate/__init__.py +++ b/PySDM/physics/homogeneous_ice_nucleation_rate/__init__.py @@ -5,3 +5,5 @@ from .constant import Constant from .null import Null from .koop import Koop2000 +from .koop_corr import Koop_Correction +from .koop_murray import KoopMurray2016 diff --git a/PySDM/physics/homogeneous_ice_nucleation_rate/koop_corr.py b/PySDM/physics/homogeneous_ice_nucleation_rate/koop_corr.py index 145370a56c..af32372657 100644 --- a/PySDM/physics/homogeneous_ice_nucleation_rate/koop_corr.py +++ b/PySDM/physics/homogeneous_ice_nucleation_rate/koop_corr.py @@ -6,7 +6,7 @@ """ -class KOOP_CORR: # pylint: disable=too-few-public-methods +class Koop_Correction: # pylint: disable=too-few-public-methods def __init__(self, const): pass diff --git a/PySDM/physics/homogeneous_ice_nucleation_rate/koop_murray.py b/PySDM/physics/homogeneous_ice_nucleation_rate/koop_murray.py index 0b5b4ce33d..0d022058c4 100644 --- a/PySDM/physics/homogeneous_ice_nucleation_rate/koop_murray.py +++ b/PySDM/physics/homogeneous_ice_nucleation_rate/koop_murray.py @@ -5,7 +5,7 @@ """ -class KOOP_MURRAY: # pylint: disable=too-few-public-methods +class KoopMurray2016: # pylint: disable=too-few-public-methods def __init__(self, const): pass diff --git a/examples/PySDM_examples/Kaercher_Lohmann_2002/run.py b/examples/PySDM_examples/Kaercher_Lohmann_2002/run.py index fb141a2494..6fa740eaaa 100644 --- a/examples/PySDM_examples/Kaercher_Lohmann_2002/run.py +++ b/examples/PySDM_examples/Kaercher_Lohmann_2002/run.py @@ -41,7 +41,7 @@ def plot( output, setting, pp ): fig, axs = pyplot.subplots(3, 2, figsize=(10, 10), sharex=True) - title = f"w: {setting.w_updraft:.2f} m s-1 T0: {setting.initial_temperature:.2f} K Nsd: {setting.n_sd:d} $\kappa$: {setting.kappa:.2f}" + title = f"w: {setting.w_updraft:.2f} m s-1 T0: {setting.initial_temperature:.2f} K Nsd: {setting.n_sd:d} $\kappa$: {setting.kappa:.2f} rate: " + setting.rate # $\mathrm{m \, s^{-1}}$ fig.suptitle(title) @@ -95,7 +95,7 @@ def plot( output, setting, pp ): ) #axWC.set_yscale('log') axWC.legend() - axWC.set_ylim(-0.5, 10) + axWC.set_ylim(-0.5, 100) axWC.set_xlabel("time [s]") axWC.set_ylabel(r"mass content [$\mathrm{\mu g \, kg^{-1}}$]") diff --git a/examples/PySDM_examples/Kaercher_Lohmann_2002/settings.py b/examples/PySDM_examples/Kaercher_Lohmann_2002/settings.py index 8c6e53c7e1..58b5854907 100644 --- a/examples/PySDM_examples/Kaercher_Lohmann_2002/settings.py +++ b/examples/PySDM_examples/Kaercher_Lohmann_2002/settings.py @@ -18,12 +18,14 @@ def __init__( N_solution_droplet: float, r_solution_droplet: float, kappa: float, + rate: str, ): self.n_sd = n_sd self.w_updraft = w_updraft self.r_solution_droplet = r_solution_droplet self.N_solution_drople = N_solution_droplet + self.rate = rate self.mass_of_dry_air = 1000 * si.kilogram self.initial_pressure = 220 * si.hectopascals @@ -33,7 +35,8 @@ def __init__( self.formulae = Formulae( particle_shape_and_density="MixedPhaseSpheres", - homogeneous_ice_nucleation_rate="Constant", + homogeneous_ice_nucleation_rate=rate, + # homogeneous_ice_nucleation_rate="Constant", constants={"J_HOM": 1.e15}, ) const = self.formulae.constants @@ -71,6 +74,8 @@ def __init__( kappas = ( 0.64, ) +hom_rates = ( "Constant","Koop2000", "Koop_Correction") + setups = [] for n_sd in n_sds: for w_updraft in w_updrafts: @@ -78,13 +83,15 @@ def __init__( for N_solution_droplet in N_solution_droplets: for r_solution_droplet in r_solution_droplets: for kappa in kappas: - setups.append( - Settings( - n_sd=n_sd, - w_updraft=w_updraft, - T0=T0, - N_solution_droplet=N_solution_droplet, - r_solution_droplet=r_solution_droplet, - kappa=kappa, + for rate in hom_rates: + setups.append( + Settings( + n_sd=n_sd, + w_updraft=w_updraft, + T0=T0, + N_solution_droplet=N_solution_droplet, + r_solution_droplet=r_solution_droplet, + kappa=kappa, + rate=rate, + ) ) - ) From 215e8cbc321751ce88fe20102e697bd232236fab Mon Sep 17 00:00:00 2001 From: Tim Luettmer Date: Sat, 15 Feb 2025 19:49:10 +0100 Subject: [PATCH 15/57] Added size distribution plots to KL02 example --- .../Kaercher_Lohmann_2002/run.py | 36 ++++++++++++++++--- .../Kaercher_Lohmann_2002/settings.py | 3 +- .../Kaercher_Lohmann_2002/simulation.py | 17 ++++----- 3 files changed, 38 insertions(+), 18 deletions(-) diff --git a/examples/PySDM_examples/Kaercher_Lohmann_2002/run.py b/examples/PySDM_examples/Kaercher_Lohmann_2002/run.py index 6fa740eaaa..ffabf3209f 100644 --- a/examples/PySDM_examples/Kaercher_Lohmann_2002/run.py +++ b/examples/PySDM_examples/Kaercher_Lohmann_2002/run.py @@ -1,3 +1,4 @@ +import matplotlib.pyplot as plt from matplotlib import pyplot from matplotlib.backends.backend_pdf import PdfPages import numpy as np @@ -7,7 +8,28 @@ from reference import critical_supersaturation -kgtoug = 1.e9 +kg_to_µg = 1.e9 +m_to_µm = 1.e6 + +def plot_size_distribution(r_wet, r_dry, N, setting, pp ): + + r_wet, r_dry = r_wet * m_to_µm, r_dry * m_to_µm + + title = f"N0: {setting.N_solution_droplet*1e-6:.2E} cm-3 R0: {setting.r_solution_droplet*m_to_µm:.2E} µm Nsd: {setting.n_sd:d} $\kappa$: {setting.kappa:.2f}" + + fig = plt.figure() + ax = fig.add_subplot(1,1,1) + + ax.scatter( r_dry, N, color="red", label = "dry" ) + ax.scatter( r_wet, N, color="blue", label = "wet" ) + ax.set_title(title) + ax.legend() + ax.set_xscale('log') + ax.set_yscale('log') + ax.set_xlabel(r"radius [µm]") + ax.set_ylabel("multiplicty") + + pp.savefig() def plot( output, setting, pp ): @@ -24,10 +46,10 @@ def plot( output, setting, pp ): print(f"{rh=},{rhi=},{rhi_crit=}") - lwc = np.asarray(output["LWC"]) * kgtoug - iwc = abs(np.asarray(output["IWC"])) * kgtoug + lwc = np.asarray(output["LWC"]) * kg_to_µg + iwc = abs(np.asarray(output["IWC"])) * kg_to_µg twc = lwc + iwc - qv = np.asarray(output["qv"]) * kgtoug + qv = np.asarray(output["qv"]) * kg_to_µg print(f"{lwc=},{iwc=},{twc=},{qv=}") @@ -42,7 +64,7 @@ def plot( output, setting, pp ): title = f"w: {setting.w_updraft:.2f} m s-1 T0: {setting.initial_temperature:.2f} K Nsd: {setting.n_sd:d} $\kappa$: {setting.kappa:.2f} rate: " + setting.rate - # $\mathrm{m \, s^{-1}}$ + fig.suptitle(title) axTz = axs[0,0] @@ -134,6 +156,7 @@ def plot( output, setting, pp ): pp.savefig() pp = PdfPages( "hom_freezing.pdf" ) +pp_size = PdfPages( "hom_freezing_initial_size_distribution.pdf" ) for setting in setups: @@ -143,8 +166,11 @@ def plot( output, setting, pp ): output = model.run() + plot_size_distribution( model.r_wet, model.r_dry, model.multiplicities, setting, pp_size) + plot( output, setting, pp ) pp.close() +pp_size.close() \ No newline at end of file diff --git a/examples/PySDM_examples/Kaercher_Lohmann_2002/settings.py b/examples/PySDM_examples/Kaercher_Lohmann_2002/settings.py index 58b5854907..eca25d5da8 100644 --- a/examples/PySDM_examples/Kaercher_Lohmann_2002/settings.py +++ b/examples/PySDM_examples/Kaercher_Lohmann_2002/settings.py @@ -24,7 +24,7 @@ def __init__( self.n_sd = n_sd self.w_updraft = w_updraft self.r_solution_droplet = r_solution_droplet - self.N_solution_drople = N_solution_droplet + self.N_solution_droplet = N_solution_droplet self.rate = rate self.mass_of_dry_air = 1000 * si.kilogram @@ -36,7 +36,6 @@ def __init__( self.formulae = Formulae( particle_shape_and_density="MixedPhaseSpheres", homogeneous_ice_nucleation_rate=rate, - # homogeneous_ice_nucleation_rate="Constant", constants={"J_HOM": 1.e15}, ) const = self.formulae.constants diff --git a/examples/PySDM_examples/Kaercher_Lohmann_2002/simulation.py b/examples/PySDM_examples/Kaercher_Lohmann_2002/simulation.py index c8937270ff..82346683ce 100644 --- a/examples/PySDM_examples/Kaercher_Lohmann_2002/simulation.py +++ b/examples/PySDM_examples/Kaercher_Lohmann_2002/simulation.py @@ -51,19 +51,18 @@ def __init__(self, settings, backend=CPU): builder.add_dynamic(Freezing(singular=False,homogeneous_freezing=True,immersion_freezing=False)) - multiplicities = discretise_multiplicities(settings.specific_concentration * env.mass_of_dry_air) - r_dry = settings.r_dry - v_dry = settings.formulae.trivia.volume(radius=r_dry) + self.multiplicities = discretise_multiplicities(settings.specific_concentration * env.mass_of_dry_air) + self.r_dry = settings.r_dry + v_dry = settings.formulae.trivia.volume(radius=self.r_dry) kappa = settings.kappa - r_wet = equilibrate_wet_radii(r_dry=r_dry, environment=builder.particulator.environment, kappa_times_dry_volume=kappa * v_dry) - + self.r_wet = equilibrate_wet_radii(r_dry=self.r_dry, environment=builder.particulator.environment, kappa_times_dry_volume=kappa * v_dry) attributes = { - "multiplicity": multiplicities, + "multiplicity": self.multiplicities, 'dry volume': v_dry, 'kappa times dry volume': kappa * v_dry, - 'volume': formulae.trivia.volume(radius=r_wet), + 'volume': formulae.trivia.volume(radius=self.r_wet), } @@ -73,7 +72,6 @@ def __init__(self, settings, backend=CPU): PySDM_products.Time(name="t"), PySDM_products.AmbientRelativeHumidity(name="RH", unit="%"), PySDM_products.AmbientRelativeHumidity(name="RH_ice", unit="%"), - #PySDM_products.AmbientRelativeHumidity(var="RH",name="RH_ice", ice=True, unit="%"), PySDM_products.AmbientTemperature(name="T"), PySDM_products.WaterMixingRatio(name="water", radius_range=(0, np.inf)), PySDM_products.WaterMixingRatio(name="ice", radius_range=(-np.inf, 0)), @@ -93,9 +91,6 @@ def __init__(self, settings, backend=CPU): PySDM_products.MeanRadius( name='r_i', unit='µm', radius_range=(-np.inf,0)), - # PySDM_products.ParticleConcentration( - # name='n_c', unit='1/kg', - # radius_range=(0, np.inf)), ] self.particulator = builder.build(attributes, products) From 6c359ced339927921cd1e3f5b26778b8783e129a Mon Sep 17 00:00:00 2001 From: Tim Luettmer Date: Tue, 18 Feb 2025 17:55:25 +0100 Subject: [PATCH 16/57] Reworked settings in KL02 example --- .../Kaercher_Lohmann_2002/run.py | 39 +++++++---- .../Kaercher_Lohmann_2002/settings.py | 67 +++++-------------- 2 files changed, 41 insertions(+), 65 deletions(-) diff --git a/examples/PySDM_examples/Kaercher_Lohmann_2002/run.py b/examples/PySDM_examples/Kaercher_Lohmann_2002/run.py index ffabf3209f..b7e2e5b3e5 100644 --- a/examples/PySDM_examples/Kaercher_Lohmann_2002/run.py +++ b/examples/PySDM_examples/Kaercher_Lohmann_2002/run.py @@ -3,9 +3,11 @@ from matplotlib.backends.backend_pdf import PdfPages import numpy as np -from settings import setups + +from settings import settings from simulation import Simulation from reference import critical_supersaturation +from PySDM.physics.constants import si kg_to_µg = 1.e9 @@ -15,7 +17,10 @@ def plot_size_distribution(r_wet, r_dry, N, setting, pp ): r_wet, r_dry = r_wet * m_to_µm, r_dry * m_to_µm - title = f"N0: {setting.N_solution_droplet*1e-6:.2E} cm-3 R0: {setting.r_solution_droplet*m_to_µm:.2E} µm Nsd: {setting.n_sd:d} $\kappa$: {setting.kappa:.2f}" + title = f"N0: {setting.N_dv_solution_droplet*1e-6:.2E} cm-3 \ + R0: {setting.r_mean_solution_droplet*m_to_µm:.2E} µm \ + $\sigma$: {setting.sigma_solution_droplet:.2f} \ + Nsd: {setting.n_sd:d} $\kappa$: {setting.kappa:.2f}" fig = plt.figure() ax = fig.add_subplot(1,1,1) @@ -25,6 +30,7 @@ def plot_size_distribution(r_wet, r_dry, N, setting, pp ): ax.set_title(title) ax.legend() ax.set_xscale('log') + ax.set_xlim(5.e-3,5.e0) ax.set_yscale('log') ax.set_xlabel(r"radius [µm]") ax.set_ylabel("multiplicty") @@ -63,7 +69,8 @@ def plot( output, setting, pp ): fig, axs = pyplot.subplots(3, 2, figsize=(10, 10), sharex=True) - title = f"w: {setting.w_updraft:.2f} m s-1 T0: {setting.initial_temperature:.2f} K Nsd: {setting.n_sd:d} $\kappa$: {setting.kappa:.2f} rate: " + setting.rate + title = f"w: {setting.w_updraft:.2f} m s-1 T0: {setting.initial_temperature:.2f} K Nsd: {setting.n_sd:d} \ + rate: " + setting.rate fig.suptitle(title) @@ -155,22 +162,28 @@ def plot( output, setting, pp ): fig.tight_layout() pp.savefig() -pp = PdfPages( "hom_freezing.pdf" ) -pp_size = PdfPages( "hom_freezing_initial_size_distribution.pdf" ) - +general_settings = {"n_sd": 1000, "T0": 220 * si.kelvin, "w_updraft": 10 * si.centimetre / si.second} +distributions = ({"N_dv_solution_droplet": 2500 / si.centimetre**3, \ + "r_mean_solution_droplet": 0.055 * si.micrometre, \ + "sigma_solution_droplet": 1.6}, + {"N_dv_solution_droplet": 8600 / si.centimetre**3, \ + "r_mean_solution_droplet": 0.0275 * si.micrometre, \ + "sigma_solution_droplet": 1.3}, + {"N_dv_solution_droplet": 2000 / si.centimetre**3, \ + "r_mean_solution_droplet": 0.11 * si.micrometre, \ + "sigma_solution_droplet": 2.}, + ) -for setting in setups: +pp = PdfPages( "hom_freezing_for_size_distributions.pdf" ) +for distribution in distributions: + setting = settings( **{**general_settings, **distribution} ) model = Simulation(setting) - output = model.run() - plot_size_distribution( model.r_wet, model.r_dry, model.multiplicities, setting, pp_size) - + plot_size_distribution( model.r_wet, model.r_dry, model.multiplicities, setting, pp) plot( output, setting, pp ) - - pp.close() -pp_size.close() \ No newline at end of file + diff --git a/examples/PySDM_examples/Kaercher_Lohmann_2002/settings.py b/examples/PySDM_examples/Kaercher_Lohmann_2002/settings.py index eca25d5da8..5ffcd4a116 100644 --- a/examples/PySDM_examples/Kaercher_Lohmann_2002/settings.py +++ b/examples/PySDM_examples/Kaercher_Lohmann_2002/settings.py @@ -9,22 +9,26 @@ @strict -class Settings: +class settings: def __init__( self, + *, n_sd: int, w_updraft: float, T0: float, - N_solution_droplet: float, - r_solution_droplet: float, - kappa: float, - rate: str, + N_dv_solution_droplet: float, + r_mean_solution_droplet: float, + sigma_solution_droplet: float, + kappa: float=0.64, + rate: str="Koop2000", + dt: float=1., ): self.n_sd = n_sd self.w_updraft = w_updraft - self.r_solution_droplet = r_solution_droplet - self.N_solution_droplet = N_solution_droplet + self.r_mean_solution_droplet = r_mean_solution_droplet + self.N_dv_solution_droplet = N_dv_solution_droplet + self.sigma_solution_droplet = sigma_solution_droplet self.rate = rate self.mass_of_dry_air = 1000 * si.kilogram @@ -47,50 +51,9 @@ def __init__( / self.initial_temperature / const.Rd ) - spectrum = Lognormal(norm_factor=N_solution_droplet / dry_air_density, m_mode=r_solution_droplet, s_geom=1.5) + spectrum = Lognormal(norm_factor=N_dv_solution_droplet / dry_air_density, m_mode=r_mean_solution_droplet, s_geom=sigma_solution_droplet) self.r_dry, self.specific_concentration = spectral_sampling.Logarithmic(spectrum).sample(n_sd) - - - - - self.t_duration = 5400 # total duration of simulation - self.dt = 1. - self.n_output = 10 # number of output steps - - -n_sds = ( 1000, ) - -w_updrafts = ( - 10 * si.centimetre / si.second, -) - -T_starts = ( 220 * si.kelvin, ) - -N_solution_droplets = ( 2500 / si.centimetre**3, ) - -r_solution_droplets = ( 0.0555 * si.micrometre, ) - -kappas = ( 0.64, ) - -hom_rates = ( "Constant","Koop2000", "Koop_Correction") - -setups = [] -for n_sd in n_sds: - for w_updraft in w_updrafts: - for T0 in T_starts: - for N_solution_droplet in N_solution_droplets: - for r_solution_droplet in r_solution_droplets: - for kappa in kappas: - for rate in hom_rates: - setups.append( - Settings( - n_sd=n_sd, - w_updraft=w_updraft, - T0=T0, - N_solution_droplet=N_solution_droplet, - r_solution_droplet=r_solution_droplet, - kappa=kappa, - rate=rate, - ) - ) + self.t_duration = 10 #5400 # total duration of simulation + self.dt = dt + self.n_output = 10 # number of output steps \ No newline at end of file From 16d0f55337aaec4f25b81282ba76ad36d97a6de5 Mon Sep 17 00:00:00 2001 From: Tim Luettmer Date: Wed, 19 Feb 2025 16:54:54 +0100 Subject: [PATCH 17/57] Fixes to make hom. freezing compatible with signed water masses changes --- .../impl_common/freezing_attributes.py | 5 +- .../impl_numba/methods/freezing_methods.py | 24 +++---- PySDM/dynamics/freezing.py | 67 ++++++++++--------- .../backends/test_freezing_methods.py | 2 +- 4 files changed, 51 insertions(+), 47 deletions(-) diff --git a/PySDM/backends/impl_common/freezing_attributes.py b/PySDM/backends/impl_common/freezing_attributes.py index 51e9a34246..a9f788ff47 100644 --- a/PySDM/backends/impl_common/freezing_attributes.py +++ b/PySDM/backends/impl_common/freezing_attributes.py @@ -28,7 +28,10 @@ class TimeDependentAttributes( class TimeDependentHomogeneousAttributes( - namedtuple("TimeDependentHomogeneousAttributes", ("volume", "signed_wwater_mass")) + namedtuple( + typename="TimeDependentHomogeneousAttributes", + field_names=("volume", "signed_water_mass"), + ) ): """groups attributes required in time-dependent regime for homogeneous freezing""" diff --git a/PySDM/backends/impl_numba/methods/freezing_methods.py b/PySDM/backends/impl_numba/methods/freezing_methods.py index f7faaf5400..6e82d35402 100644 --- a/PySDM/backends/impl_numba/methods/freezing_methods.py +++ b/PySDM/backends/impl_numba/methods/freezing_methods.py @@ -112,25 +112,25 @@ def freeze_time_dependent_homogeneous_body( # pylint: disable=unused-argument,t thaw, ): - n_sd = len(attributes.water_mass) + n_sd = len(attributes.signed_water_mass) for i in numba.prange(n_sd): # pylint: disable=not-an-iterable cell_id = cell[i] - #relative_humidity_ice = relative_humidity[cell_id] / a_w_ice[cell_id] if thaw and frozen_and_above_freezing_point( - attributes.water_mass[i], temperature[cell_id] + attributes.signed_water_mass[i], temperature[cell_id] ): - _thaw(attributes.water_mass, i) + _thaw(attributes.signed_water_mass, i) elif unfrozen_and_ice_saturated( - attributes.water_mass[i], relative_humidity_ice[cell_id] + attributes.signed_water_mass[i], relative_humidity_ice[cell_id] ): - d_a_w_ice = (relative_humidity_ice[cell_id] - 1) * a_w_ice[cell_id] - rate = j_hom(temperature, d_a_w_ice) - # TODO #594: this assumes constant T throughout timestep, can we do better? - prob = 1 - np.exp( # TODO #599: common code for Poissonian prob - -rate * attributes.volume[i] * timestep + d_a_w_ice = (relative_humidity_ice[cell_id] - 1.) * a_w_ice[cell_id] + rate_assuming_constant_temperature_within_dt = ( + j_hom(temperature[cell_id], d_a_w_ice) * attributes.volume[i] + ) + prob = 1 - prob_zero_events( + r=rate_assuming_constant_temperature_within_dt, dt=timestep ) if rand[i] < prob: - _freeze(attributes.water_mass, i) + _freeze(attributes.signed_water_mass, i) # if record_freezing_temperature: # freezing_temperature[i] = temperature[cell_id] @@ -205,7 +205,7 @@ def freeze_time_dependent_homogeneous( rand.data, TimeDependentHomogeneousAttributes( volume=attributes.volume.data, - water_mass=attributes.signed_water_mass.data, + signed_water_mass=attributes.signed_water_mass.data, ), timestep, cell.data, diff --git a/PySDM/dynamics/freezing.py b/PySDM/dynamics/freezing.py index d2c99ae74d..656cd1026f 100644 --- a/PySDM/dynamics/freezing.py +++ b/PySDM/dynamics/freezing.py @@ -64,42 +64,43 @@ def __call__(self): if not self.enable: return - if self.singular: - self.particulator.backend.freeze_singular( - attributes=SingularAttributes( - freezing_temperature=self.particulator.attributes[ - "freezing temperature" - ], - signed_water_mass=self.particulator.attributes["signed_water mass"], - ), - temperature=self.particulator.environment["T"], - relative_humidity=self.particulator.environment["RH"], - cell=self.particulator.attributes["cell id"], - thaw=self.thaw, - ) - else: - self.rand.urand(self.rng) - self.particulator.backend.freeze_time_dependent( - rand=self.rand, - attributes=TimeDependentAttributes( - immersed_surface_area=self.particulator.attributes[ - "immersed surface area" - ], - signed_water_mass=self.particulator.attributes["signed_water mass"], + if self.immersion_freezing: + if self.singular: + self.particulator.backend.freeze_singular( + attributes=SingularAttributes( + freezing_temperature=self.particulator.attributes[ + "freezing temperature" + ], + signed_water_mass=self.particulator.attributes["signed water mass"], ), - timestep=self.particulator.dt, - cell=self.particulator.attributes["cell id"], - a_w_ice=self.particulator.environment["a_w_ice"], temperature=self.particulator.environment["T"], relative_humidity=self.particulator.environment["RH"], - record_freezing_temperature=self.record_freezing_temperature, - freezing_temperature=( - self.particulator.attributes["freezing temperature"] - if self.record_freezing_temperature - else None - ), + cell=self.particulator.attributes["cell id"], thaw=self.thaw, ) + else: + self.rand.urand(self.rng) + self.particulator.backend.freeze_time_dependent( + rand=self.rand, + attributes=TimeDependentAttributes( + immersed_surface_area=self.particulator.attributes[ + "immersed surface area" + ], + signed_water_mass=self.particulator.attributes["signed water mass"], + ), + timestep=self.particulator.dt, + cell=self.particulator.attributes["cell id"], + a_w_ice=self.particulator.environment["a_w_ice"], + temperature=self.particulator.environment["T"], + relative_humidity=self.particulator.environment["RH"], + record_freezing_temperature=self.record_freezing_temperature, + freezing_temperature=( + self.particulator.attributes["freezing temperature"] + if self.record_freezing_temperature + else None + ), + thaw=self.thaw, + ) if self.homogeneous_freezing: @@ -108,8 +109,8 @@ def __call__(self): self.particulator.backend.freeze_time_dependent_homogeneous( rand=self.rand, attributes=TimeDependentHomogeneousAttributes( - volume=self.particulator.attributes["volume"], - signed_water_mass=self.particulator.attributes["signed_water mass"], + volume=self.particulator.attributes["volume"], + signed_water_mass=self.particulator.attributes["signed water mass"], ), timestep=self.particulator.dt, cell=self.particulator.attributes["cell id"], diff --git a/tests/unit_tests/backends/test_freezing_methods.py b/tests/unit_tests/backends/test_freezing_methods.py index 4eb0e1f3ae..3c5aaecb3b 100644 --- a/tests/unit_tests/backends/test_freezing_methods.py +++ b/tests/unit_tests/backends/test_freezing_methods.py @@ -284,7 +284,7 @@ def low(t): builder.add_dynamic(Freezing(singular=False,homogeneous_freezing=True,immersion_freezing=False)) attributes = { "multiplicity": np.full(n_sd, int(case["N"])), - "water mass": np.full(n_sd, initial_water_mass), + "signed water mass": np.full(n_sd, initial_water_mass), } particulator = builder.build(attributes=attributes, products=products) pvs_ice = particulator.formulae.saturation_vapour_pressure.pvs_ice(T) From a4eaed0161861da7ad1418be1fb1cd872e69eb17 Mon Sep 17 00:00:00 2001 From: Tim Luettmer Date: Mon, 24 Feb 2025 10:58:41 +0100 Subject: [PATCH 18/57] Changed name of ice mass variable in freeze and thaw --- .../backends/impl_numba/methods/freezing_methods.py | 13 ++++++++----- .../PySDM_examples/Kaercher_Lohmann_2002/run.py | 2 +- .../Kaercher_Lohmann_2002/settings.py | 2 +- .../Kaercher_Lohmann_2002/simulation.py | 7 ++++++- 4 files changed, 16 insertions(+), 8 deletions(-) diff --git a/PySDM/backends/impl_numba/methods/freezing_methods.py b/PySDM/backends/impl_numba/methods/freezing_methods.py index 6e82d35402..ed798db67a 100644 --- a/PySDM/backends/impl_numba/methods/freezing_methods.py +++ b/PySDM/backends/impl_numba/methods/freezing_methods.py @@ -23,13 +23,13 @@ def __init__(self): ) @numba.njit(**{**self.default_jit_flags, "parallel": False}) - def _freeze(water_mass, i): - water_mass[i] = -1 * water_mass[i] + def _freeze(signed_water_mass, i): + signed_water_mass[i] = -1 * signed_water_mass[i] # TODO #599: change thd (latent heat)! @numba.njit(**{**self.default_jit_flags, "parallel": False}) - def _thaw(water_mass, i): - water_mass[i] = -1 * water_mass[i] + def _thaw(signed_water_mass, i): + signed_water_mass[i] = -1 * signed_water_mass[i] # TODO #599: change thd (latent heat)! @numba.njit(**self.default_jit_flags) @@ -126,14 +126,17 @@ def freeze_time_dependent_homogeneous_body( # pylint: disable=unused-argument,t rate_assuming_constant_temperature_within_dt = ( j_hom(temperature[cell_id], d_a_w_ice) * attributes.volume[i] ) + rate = j_hom(temperature[cell_id], d_a_w_ice) prob = 1 - prob_zero_events( r=rate_assuming_constant_temperature_within_dt, dt=timestep ) + randi = rand[i] if rand[i] < prob: + # print(f"{d_a_w_ice=},{rate=},{prob=},{randi=}") _freeze(attributes.signed_water_mass, i) # if record_freezing_temperature: # freezing_temperature[i] = temperature[cell_id] - + # print( attributes.signed_water_mass ) self.freeze_time_dependent_homogeneous_body = freeze_time_dependent_homogeneous_body diff --git a/examples/PySDM_examples/Kaercher_Lohmann_2002/run.py b/examples/PySDM_examples/Kaercher_Lohmann_2002/run.py index b7e2e5b3e5..0f0bac983f 100644 --- a/examples/PySDM_examples/Kaercher_Lohmann_2002/run.py +++ b/examples/PySDM_examples/Kaercher_Lohmann_2002/run.py @@ -162,7 +162,7 @@ def plot( output, setting, pp ): fig.tight_layout() pp.savefig() -general_settings = {"n_sd": 1000, "T0": 220 * si.kelvin, "w_updraft": 10 * si.centimetre / si.second} +general_settings = {"n_sd": 100, "T0": 220 * si.kelvin, "w_updraft": 10 * si.centimetre / si.second} distributions = ({"N_dv_solution_droplet": 2500 / si.centimetre**3, \ "r_mean_solution_droplet": 0.055 * si.micrometre, \ "sigma_solution_droplet": 1.6}, diff --git a/examples/PySDM_examples/Kaercher_Lohmann_2002/settings.py b/examples/PySDM_examples/Kaercher_Lohmann_2002/settings.py index 5ffcd4a116..f9fd4c64f4 100644 --- a/examples/PySDM_examples/Kaercher_Lohmann_2002/settings.py +++ b/examples/PySDM_examples/Kaercher_Lohmann_2002/settings.py @@ -54,6 +54,6 @@ def __init__( spectrum = Lognormal(norm_factor=N_dv_solution_droplet / dry_air_density, m_mode=r_mean_solution_droplet, s_geom=sigma_solution_droplet) self.r_dry, self.specific_concentration = spectral_sampling.Logarithmic(spectrum).sample(n_sd) - self.t_duration = 10 #5400 # total duration of simulation + self.t_duration = 5400 # total duration of simulation self.dt = dt self.n_output = 10 # number of output steps \ No newline at end of file diff --git a/examples/PySDM_examples/Kaercher_Lohmann_2002/simulation.py b/examples/PySDM_examples/Kaercher_Lohmann_2002/simulation.py index 82346683ce..842b781a36 100644 --- a/examples/PySDM_examples/Kaercher_Lohmann_2002/simulation.py +++ b/examples/PySDM_examples/Kaercher_Lohmann_2002/simulation.py @@ -62,7 +62,8 @@ def __init__(self, settings, backend=CPU): "multiplicity": self.multiplicities, 'dry volume': v_dry, 'kappa times dry volume': kappa * v_dry, - 'volume': formulae.trivia.volume(radius=self.r_wet), + #'volume': formulae.trivia.volume(radius=self.r_wet), + "signed water mass": formulae.particle_shape_and_density.radius_to_mass(self.r_wet), } @@ -141,8 +142,12 @@ def run(self): self.save(output) for _ in range(self.n_output): + # print(self.particulator.__dict__) + # print(self.particulator.attributes.__dict__) + # print(self.particulator.attributes._ParticleAttributes__attributes['signed water mass']) self.particulator.run(self.n_substeps) #print( self.particulator.products["t"].get() ) + self.save(output) return output From 58f71113e1d3a2e57eb423fe13dfdfd70ebd8ec8 Mon Sep 17 00:00:00 2001 From: Tim Luettmer Date: Thu, 13 Mar 2025 13:31:32 +0100 Subject: [PATCH 19/57] renamed immersion freezing unit test --- .../dynamics/{test_immersion_freezing.py => test_freezing.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/unit_tests/dynamics/{test_immersion_freezing.py => test_freezing.py} (100%) diff --git a/tests/unit_tests/dynamics/test_immersion_freezing.py b/tests/unit_tests/dynamics/test_freezing.py similarity index 100% rename from tests/unit_tests/dynamics/test_immersion_freezing.py rename to tests/unit_tests/dynamics/test_freezing.py From fc127e1e3e117470bf8ec706d7ca037cd1773529 Mon Sep 17 00:00:00 2001 From: Tim Luettmer Date: Tue, 1 Apr 2025 18:00:55 +0200 Subject: [PATCH 20/57] some modifications to example --- .../Kaercher_Lohmann_2002/run.py | 89 ++++++++----------- .../Kaercher_Lohmann_2002/settings.py | 6 +- .../Kaercher_Lohmann_2002/simulation.py | 75 ++++++++-------- 3 files changed, 75 insertions(+), 95 deletions(-) diff --git a/examples/PySDM_examples/Kaercher_Lohmann_2002/run.py b/examples/PySDM_examples/Kaercher_Lohmann_2002/run.py index 0f0bac983f..258d9d5bce 100644 --- a/examples/PySDM_examples/Kaercher_Lohmann_2002/run.py +++ b/examples/PySDM_examples/Kaercher_Lohmann_2002/run.py @@ -3,34 +3,32 @@ from matplotlib.backends.backend_pdf import PdfPages import numpy as np - from settings import settings from simulation import Simulation from reference import critical_supersaturation from PySDM.physics.constants import si - kg_to_µg = 1.e9 -m_to_µm = 1.e6 +m_to_µm = 1.e6 -def plot_size_distribution(r_wet, r_dry, N, setting, pp ): +def plot_size_distribution(r_wet, r_dry, N, setting, pp): r_wet, r_dry = r_wet * m_to_µm, r_dry * m_to_µm - title = f"N0: {setting.N_dv_solution_droplet*1e-6:.2E} cm-3 \ - R0: {setting.r_mean_solution_droplet*m_to_µm:.2E} µm \ + title = f"N0: {setting.N_dv_solution_droplet * 1e-6:.2E} cm-3 \ + R0: {setting.r_mean_solution_droplet * m_to_µm:.2E} µm \ $\sigma$: {setting.sigma_solution_droplet:.2f} \ Nsd: {setting.n_sd:d} $\kappa$: {setting.kappa:.2f}" fig = plt.figure() - ax = fig.add_subplot(1,1,1) + ax = fig.add_subplot(1, 1, 1) - ax.scatter( r_dry, N, color="red", label = "dry" ) - ax.scatter( r_wet, N, color="blue", label = "wet" ) + ax.scatter(r_dry, N, color="red", label="dry") + ax.scatter(r_wet, N, color="blue", label="wet") ax.set_title(title) ax.legend() ax.set_xscale('log') - ax.set_xlim(5.e-3,5.e0) + ax.set_xlim(5.e-3, 5.e0) ax.set_yscale('log') ax.set_xlabel(r"radius [µm]") ax.set_ylabel("multiplicty") @@ -38,18 +36,15 @@ def plot_size_distribution(r_wet, r_dry, N, setting, pp ): pp.savefig() -def plot( output, setting, pp ): - - +def plot(output, setting, pp): time = output["t"] temperature = np.asarray(output["T"]) z = output["z"] - + rh = output["RH"] - rhi = output["RHi"] + rhi = output["RHi"] rhi_crit = critical_supersaturation(temperature) * 100. - print(f"{rh=},{rhi=},{rhi_crit=}") lwc = np.asarray(output["LWC"]) * kg_to_µg @@ -65,16 +60,14 @@ def plot( output, setting, pp ): ri = output["ri"] print(f"{ns=},{ni=},{rs=},{ri=},") - fig, axs = pyplot.subplots(3, 2, figsize=(10, 10), sharex=True) - title = f"w: {setting.w_updraft:.2f} m s-1 T0: {setting.initial_temperature:.2f} K Nsd: {setting.n_sd:d} \ rate: " + setting.rate fig.suptitle(title) - axTz = axs[0,0] + axTz = axs[0, 0] axTz.plot( time, z, color="black", linestyle="-", label="dz", linewidth=5 @@ -90,10 +83,9 @@ def plot( output, setting, pp ): twin.set_ylabel("temperature [K]") axTz.legend(loc='upper left') axTz.set_xlabel("time [s]") - - axRH = axs[0,1] - + axRH = axs[0, 1] + axRH.plot( time, rh, color="blue", linestyle="-", label="water", linewidth=5 ) @@ -102,17 +94,14 @@ def plot( output, setting, pp ): ) axRH.plot( time, rhi_crit, color="black", linestyle="-", label="crit", linewidth=5 - ) + ) axRH.legend() axRH.set_xlabel("time [s]") axRH.set_ylabel("relative humidity [%]") axRH.set_ylim(50, 200) + axWC = axs[1, 0] - - axWC = axs[1,0] - - axWC.plot( time, twc, color="black", linestyle="--", label="total", linewidth=5 ) @@ -122,48 +111,44 @@ def plot( output, setting, pp ): axWC.plot( time, iwc, color="red", linestyle="-", label="ice", linewidth=5 ) - #axWC.set_yscale('log') + axWC.set_yscale('log') axWC.legend() - axWC.set_ylim(-0.5, 100) + axWC.set_ylim(1.e1, 1.e5) axWC.set_xlabel("time [s]") axWC.set_ylabel(r"mass content [$\mathrm{\mu g \, kg^{-1}}$]") - - - axN = axs[1,1] - + axN = axs[1, 1] axN.plot( - time, ns, color="blue", linestyle="-", label="droplet", linewidth=5 ) + time, ns, color="blue", linestyle="-", label="droplet", linewidth=5) axN.plot( - time, ni, color="red", linestyle="-", label="ice", linewidth=5 ) + time, ni, color="red", linestyle="-", label="ice", linewidth=5) + axN.set_yscale('log') axN.legend() - axN.set_ylim(-0.5, 3000) + axN.set_ylim(1.e-3, 1.e4) axN.set_xlabel("time [s]") axN.set_ylabel(r"number concentration [$\mathrm{cm^{-3}}$]") - - axR = axs[2,0] - + axR = axs[2, 0] axR.plot( - time, rs, color="blue", linestyle="-", label="droplet", linewidth=5 ) + time, rs, color="blue", linestyle="-", label="droplet", linewidth=5) axR.plot( - time, ri, color="red", linestyle="-", label="ice", linewidth=5 ) - + time, ri, color="red", linestyle="-", label="ice", linewidth=5) axR.legend() - #axR.set_ylim(-0.5, 3000) + axR.set_yscale('log') + axR.set_ylim(5.e-2, 1.e2) axR.set_xlabel("time [s]") axR.set_ylabel(r"mean radius [µm]") - - fig.tight_layout() + fig.tight_layout() pp.savefig() -general_settings = {"n_sd": 100, "T0": 220 * si.kelvin, "w_updraft": 10 * si.centimetre / si.second} -distributions = ({"N_dv_solution_droplet": 2500 / si.centimetre**3, \ + +general_settings = {"n_sd": 1000, "T0": 220 * si.kelvin, "w_updraft": 10 * si.centimetre / si.second} +distributions = ({"N_dv_solution_droplet": 2500 / si.centimetre ** 3, \ "r_mean_solution_droplet": 0.055 * si.micrometre, \ "sigma_solution_droplet": 1.6}, {"N_dv_solution_droplet": 8600 / si.centimetre**3, \ @@ -174,16 +159,14 @@ def plot( output, setting, pp ): "sigma_solution_droplet": 2.}, ) -pp = PdfPages( "hom_freezing_for_size_distributions.pdf" ) +pp = PdfPages("hom_freezing_for_size_distributions.pdf") for distribution in distributions: - - setting = settings( **{**general_settings, **distribution} ) + setting = settings(**{**general_settings, **distribution}) model = Simulation(setting) output = model.run() - plot_size_distribution( model.r_wet, model.r_dry, model.multiplicities, setting, pp) - plot( output, setting, pp ) + plot_size_distribution(model.r_wet, model.r_dry, model.multiplicities, setting, pp) + plot(output, setting, pp) pp.close() - diff --git a/examples/PySDM_examples/Kaercher_Lohmann_2002/settings.py b/examples/PySDM_examples/Kaercher_Lohmann_2002/settings.py index f9fd4c64f4..cc1e369ce9 100644 --- a/examples/PySDM_examples/Kaercher_Lohmann_2002/settings.py +++ b/examples/PySDM_examples/Kaercher_Lohmann_2002/settings.py @@ -6,8 +6,6 @@ from PySDM.initialisation.spectra import Lognormal from PySDM.initialisation.sampling import spectral_sampling - - @strict class settings: def __init__( @@ -21,7 +19,7 @@ def __init__( sigma_solution_droplet: float, kappa: float=0.64, rate: str="Koop2000", - dt: float=1., + dt: float=0.1, ): self.n_sd = n_sd @@ -54,6 +52,6 @@ def __init__( spectrum = Lognormal(norm_factor=N_dv_solution_droplet / dry_air_density, m_mode=r_mean_solution_droplet, s_geom=sigma_solution_droplet) self.r_dry, self.specific_concentration = spectral_sampling.Logarithmic(spectrum).sample(n_sd) - self.t_duration = 5400 # total duration of simulation + self.t_duration = 3600 * 1.5 # total duration of simulation self.dt = dt self.n_output = 10 # number of output steps \ No newline at end of file diff --git a/examples/PySDM_examples/Kaercher_Lohmann_2002/simulation.py b/examples/PySDM_examples/Kaercher_Lohmann_2002/simulation.py index 842b781a36..eab04275ca 100644 --- a/examples/PySDM_examples/Kaercher_Lohmann_2002/simulation.py +++ b/examples/PySDM_examples/Kaercher_Lohmann_2002/simulation.py @@ -3,7 +3,7 @@ import PySDM.products as PySDM_products from PySDM.backends import CPU from PySDM.builder import Builder -from PySDM.dynamics import AmbientThermodynamics, Condensation, Freezing +from PySDM.dynamics import AmbientThermodynamics, Condensation, Freezing, VapourDepositionOnIce from PySDM.environments import Parcel from PySDM.physics import constants as const from PySDM.initialisation import discretise_multiplicities, equilibrate_wet_radii @@ -17,21 +17,21 @@ def __init__(self, settings, backend=CPU): # self.n_substeps = 1 # while dt_output / self.n_substeps >= settings.dt_max: # TODO #334 dt_max # self.n_substeps += 1 - + dt = settings.dt formulae = settings.formulae env = Parcel( - mixed_phase = True, - dt=dt, - mass_of_dry_air=settings.mass_of_dry_air, - p0=settings.initial_pressure, - initial_water_vapour_mixing_ratio=settings.initial_water_vapour_mixing_ratio, - T0=settings.initial_temperature, - w=settings.w_updraft, - ) - + mixed_phase=True, + dt=dt, + mass_of_dry_air=settings.mass_of_dry_air, + p0=settings.initial_pressure, + initial_water_vapour_mixing_ratio=settings.initial_water_vapour_mixing_ratio, + T0=settings.initial_temperature, + w=settings.w_updraft, + ) + builder = Builder( backend=backend( formulae=settings.formulae, @@ -45,35 +45,34 @@ def __init__(self, settings, backend=CPU): environment=env, ) - builder.add_dynamic(AmbientThermodynamics()) builder.add_dynamic(Condensation()) - builder.add_dynamic(Freezing(singular=False,homogeneous_freezing=True,immersion_freezing=False)) - + builder.add_dynamic(VapourDepositionOnIce()) + builder.add_dynamic(Freezing(singular=False, homogeneous_freezing=True, immersion_freezing=False)) self.multiplicities = discretise_multiplicities(settings.specific_concentration * env.mass_of_dry_air) self.r_dry = settings.r_dry v_dry = settings.formulae.trivia.volume(radius=self.r_dry) kappa = settings.kappa - self.r_wet = equilibrate_wet_radii(r_dry=self.r_dry, environment=builder.particulator.environment, kappa_times_dry_volume=kappa * v_dry) - + self.r_wet = equilibrate_wet_radii(r_dry=self.r_dry, environment=builder.particulator.environment, + kappa_times_dry_volume=kappa * v_dry) + attributes = { "multiplicity": self.multiplicities, 'dry volume': v_dry, 'kappa times dry volume': kappa * v_dry, - #'volume': formulae.trivia.volume(radius=self.r_wet), + # 'volume': formulae.trivia.volume(radius=self.r_wet), "signed water mass": formulae.particle_shape_and_density.radius_to_mass(self.r_wet), } - - - + products = [ PySDM_products.ParcelDisplacement(name="z"), PySDM_products.Time(name="t"), PySDM_products.AmbientRelativeHumidity(name="RH", unit="%"), PySDM_products.AmbientRelativeHumidity(name="RH_ice", unit="%"), PySDM_products.AmbientTemperature(name="T"), + PySDM_products.AmbientPressure(name="p", unit='hPa'), PySDM_products.WaterMixingRatio(name="water", radius_range=(0, np.inf)), PySDM_products.WaterMixingRatio(name="ice", radius_range=(-np.inf, 0)), PySDM_products.WaterMixingRatio(name="total", radius_range=(-np.inf, np.inf)), @@ -81,30 +80,27 @@ def __init__(self, settings, backend=CPU): name="vapour", var="water_vapour_mixing_ratio" ), PySDM_products.ParticleConcentration( - name='n_s', unit='1/cm**3', - radius_range=(0, np.inf)), + name='n_s', unit='1/cm**3', + radius_range=(0, np.inf)), PySDM_products.ParticleConcentration( - name='n_i', unit='1/cm**3', - radius_range=(-np.inf,0)), + name='n_i', unit='1/cm**3', + radius_range=(-np.inf, 0)), PySDM_products.MeanRadius( - name='r_s', unit='µm', - radius_range=(0,np.inf)), + name='r_s', unit='µm', + radius_range=(0, np.inf)), PySDM_products.MeanRadius( - name='r_i', unit='µm', - radius_range=(-np.inf,0)), - ] + name='r_i', unit='µm', + radius_range=(-np.inf, 0)), + ] self.particulator = builder.build(attributes, products) - self.n_output = settings.n_output self.n_substeps = int(settings.t_duration / dt / self.n_output) - print( settings.t_duration, dt, self.n_output, self.n_substeps ) - - print( self.particulator.n_sd ) - + print(settings.t_duration, dt, self.n_output, self.n_substeps) + print(self.particulator.n_sd) def save(self, output): cell_id = 0 @@ -114,9 +110,10 @@ def save(self, output): output["RH"].append(self.particulator.products["RH"].get()[cell_id]) output["RHi"].append(self.particulator.products["RH_ice"].get()[cell_id]) output["T"].append(self.particulator.products["T"].get()[cell_id]) + output["P"].append(self.particulator.products["p"].get()[cell_id]) output["LWC"].append(self.particulator.products["water"].get()[cell_id]) output["IWC"].append(self.particulator.products["ice"].get()[cell_id]) - # output["TWC"].append(self.particulator.products["total"].get()[cell_id]) + # output["TWC"].append(self.particulator.products["total"].get()[cell_id]) output["qv"].append(self.particulator.products["vapour"].get()[cell_id]) output["ns"].append(self.particulator.products["n_s"].get()[cell_id]) output["ni"].append(self.particulator.products["n_i"].get()[cell_id]) @@ -130,9 +127,10 @@ def run(self): "RH": [], "RHi": [], "T": [], + "P": [], "LWC": [], "IWC": [], - # "TWC": [], + # "TWC": [], "qv": [], "ns": [], "ni": [], @@ -146,8 +144,9 @@ def run(self): # print(self.particulator.attributes.__dict__) # print(self.particulator.attributes._ParticleAttributes__attributes['signed water mass']) self.particulator.run(self.n_substeps) - #print( self.particulator.products["t"].get() ) - + # print(self.particulator.products["t"].get()) + # print(self.particulator.products["p"].get()) + # print(self.particulator.attributes["signed water mass"].data) self.save(output) return output From 109f36e61fc5f3fd381d1146c6b7f8e91a2ecb21 Mon Sep 17 00:00:00 2001 From: Tim Luettmer Date: Fri, 18 Apr 2025 14:45:26 +0200 Subject: [PATCH 21/57] some modifications to example --- .../Kaercher_Lohmann_2002/plot.py | 440 ++++++++++++++++++ .../Kaercher_Lohmann_2002/reference.py | 26 +- .../Kaercher_Lohmann_2002/run.py | 296 ++++++------ .../Kaercher_Lohmann_2002/settings.py | 50 +- .../Kaercher_Lohmann_2002/simulation.py | 34 +- 5 files changed, 671 insertions(+), 175 deletions(-) create mode 100644 examples/PySDM_examples/Kaercher_Lohmann_2002/plot.py diff --git a/examples/PySDM_examples/Kaercher_Lohmann_2002/plot.py b/examples/PySDM_examples/Kaercher_Lohmann_2002/plot.py new file mode 100644 index 0000000000..f72457e70e --- /dev/null +++ b/examples/PySDM_examples/Kaercher_Lohmann_2002/plot.py @@ -0,0 +1,440 @@ +import matplotlib.pyplot as plt +from matplotlib import pyplot +from matplotlib.backends.backend_pdf import PdfPages +from matplotlib.pyplot import boxplot + +from reference import bulk_model_reference, critical_supersaturation_spich2023 + +import json +import numpy as np + +# general plot settings +ax_lab_fsize = 15 +tick_fsize = 15 +title_fsize = 15 + +kg_to_µg = 1.e9 +m_to_µm = 1.e6 + +def plot_size_distribution(r_wet, r_dry, N, setting, pp): + r_wet, r_dry = r_wet * m_to_µm, r_dry * m_to_µm + + title = f"N0: {setting.N_dv_solution_droplet * 1e-6:.2E} cm-3 \ + R0: {setting.r_mean_solution_droplet * m_to_µm:.2E} µm \ + $\sigma$: {setting.sigma_solution_droplet:.2f} \ + Nsd: {setting.n_sd:d} $\kappa$: {setting.kappa:.2f}" + + fig = plt.figure() + ax = fig.add_subplot(1, 1, 1) + + ax.scatter(r_dry, N, color="red", label="dry") + ax.scatter(r_wet, N, color="blue", label="wet") + ax.set_title(title) + ax.legend() + ax.set_xscale('log') + ax.set_xlim(5.e-3, 5.e0) + ax.set_yscale('log') + ax.set_xlabel(r"radius [µm]") + ax.set_ylabel("multiplicty") + + pp.savefig() + + +def plot_evolution( pp, output ): + ln = 2.5 + + extra_panel = False + + time = output["t"] + temperature = np.asarray(output["T"]) + z = output["z"] + + # rh = output["RH"] + rhi = np.asarray(output["RHi"]) / 100. + + # rhi_crit_old = critical_supersaturation(temperature) + rhi_crit = critical_supersaturation_spich2023(temperature) + + lwc = np.asarray(output["LWC"]) * kg_to_µg + iwc = abs(np.asarray(output["IWC"])) * kg_to_µg + + ns = output["ns"] + ni = output["ni"] + rs = output["rs"] + ri = output["ri"] + + frozen = output["frozen"] + r_wet_init = output["r_wet"] + multiplicities = output["multiplicity"] + multiplicities_unfrozen = np.ma.masked_array(multiplicities, mask=frozen) + multiplicities_frozen = np.ma.masked_array(multiplicities, mask=np.logical_not(frozen)) + + if extra_panel: + fig, axs = pyplot.subplots(3, 2, figsize=(10, 10), sharex=False) + else: + fig, axs = pyplot.subplots(2, 2, figsize=(10, 10), sharex=False) + + + x_limit = [0, np.amax(time)] + + # title = (f"w: {setting.w_updraft:.2f} m s-1 T0: {setting.initial_temperature:.2f} K " + # f"Nsd: {setting.n_sd:d} rate: ") + setting.rate + # fig.suptitle(title) + + axTz = axs[0, 0] + axTz.plot( + time, temperature, color="red", linestyle="-", label="T", linewidth=ln + ) + + twin = axTz.twinx() + twin.plot( + time, z, color="black", linestyle="-", label="dz", linewidth=ln + ) + twin.set_ylabel("vertical displacement [m]", fontsize=ax_lab_fsize) + twin.set_ylim(-5, np.amax(z) + 100) + axTz.grid(True) + axTz.set_ylim(190, 225) + axTz.legend(loc='upper right',fontsize=ax_lab_fsize) + axTz.set_ylabel("temperature [K]", fontsize=ax_lab_fsize) + twin.legend(loc='upper left',fontsize=ax_lab_fsize) + axTz.set_xlabel("time [s]", fontsize=ax_lab_fsize) + axTz.tick_params(labelsize=tick_fsize) + twin.tick_params(labelsize=tick_fsize) + axTz.set_title("(a) air parcel ascent", fontsize=title_fsize) + axTz.set_xlim(x_limit) + + axRH = axs[0, 1] + # axRH.plot( + # time, rh, color="blue", linestyle="-", label="water", linewidth=ln + # ) + axRH.plot( + time, rhi, color="red", linestyle="-", label="ice", linewidth=ln + ) + axRH.plot( + time, rhi_crit, color="black", linestyle="-", label="crit", linewidth=ln + ) + # axRH.plot( + # time, rhi_crit_old, color="black", linestyle="--", label="crit_old", linewidth=ln + # ) + axRH.grid(True) + axRH.legend(loc='lower right',fontsize=ax_lab_fsize) + axRH.set_xlabel("time [s]", fontsize=ax_lab_fsize) + axRH.set_xlim(x_limit) + axRH.set_ylabel("supersaturation ratio", fontsize=ax_lab_fsize) + axRH.set_ylim(1, 1.6) + axRH.tick_params(labelsize=tick_fsize) + axRH.set_title("(b) supersaturation", fontsize=title_fsize) + + axN = axs[1, 0] + + axN.plot( + time, ns, color="blue", linestyle="-", label="solution droplet", linewidth=ln) + axN.plot( + time, ni, color="red", linestyle="-", label="ice", linewidth=ln) + + axN.set_yscale('log') + axN.grid(True) + axN.legend(fontsize=ax_lab_fsize) + axN.set_ylim(1.e-3, 5.e4) + axN.set_xlabel("time [s]", fontsize=ax_lab_fsize) + axN.set_xlim(x_limit) + axN.set_ylabel(r"number concentration [$\mathrm{cm^{-3}}$]" + , fontsize=ax_lab_fsize) + axN.tick_params(labelsize=tick_fsize) + axN.set_title("(c) bulk number concentration", fontsize=title_fsize) + + axR = axs[1, 1] + + axR.plot( + time, rs, color="blue", linestyle="-", label="solution droplet", linewidth=ln) + axR.plot( + time, ri, color="red", linestyle="-", label="ice", linewidth=ln) + + axR.grid(True) + axR.legend(fontsize=ax_lab_fsize) + axR.set_yscale('log') + axR.set_ylim(1.e-2, 1.e2) + axR.set_xlabel("time [s]", fontsize=ax_lab_fsize) + axR.set_xlim(x_limit) + axR.set_ylabel(r"mean radius [µm]", fontsize=ax_lab_fsize) + axR.tick_params(labelsize=tick_fsize) + axR.set_title("(d) mean radius", fontsize=title_fsize) + + if extra_panel: + + axWC = axs[2, 0] + + axWC.plot( + time, lwc, color="blue", linestyle="-", label="water", linewidth=ln + ) + axWC.plot( + time, iwc, color="red", linestyle="-", label="ice", linewidth=ln + ) + axWC.set_yscale('log') + axWC.legend() + axWC.set_ylim(1.e1, 1.e5) + axWC.set_xlabel("time [s]", fontsize=ax_lab_fsize) + axWC.set_ylabel(r"mass content [$\mathrm{\mu g \, kg^{-1}}$]" + , fontsize=ax_lab_fsize) + + axFrz = axs[2, 1] + + axFrz.scatter(r_wet_init, multiplicities_unfrozen, color="black", alpha=0.5, label="unfrozen") + axFrz.scatter(r_wet_init, multiplicities_frozen, color="blue", label="frozen") + axFrz.legend() + axFrz.set_xscale('log') + axFrz.set_xlim(5.e-3, 5.e0) + axFrz.set_yscale('log') + axFrz.set_xlabel(r"initial wet radius [µm]", fontsize=ax_lab_fsize) + axFrz.set_ylabel("multiplicty", fontsize=ax_lab_fsize) + + fig.tight_layout() + pp.savefig() + + +def plot_ensemble(pp, outputs, T0, ens_member): + + title = ("Ensemble simulation with " + str(ens_member) + + f" members for T0: {T0:.2f}K ") + + x_label = "number of super particles" + + ni_bulk_ref = bulk_model_reference(T0) * 1e-6 + + nx = len(outputs) + + x_array = np.array([50, 100, 500, 1000, 5000, 10000, 50000, 100000]) + x_string_array = [] + for x in x_array: + x_string_array.append(f"{x:.0e}") + + ni_arr = np.empty( ( ens_member,np.shape(x_array)[0]) ) + ri_arr = np.empty((ens_member, np.shape(x_array)[0])) + frozen_fraction_arr = np.empty((ens_member, np.shape(x_array)[0])) + min_frozen_r_arr = np.empty((ens_member, np.shape(x_array)[0])) + + jdx = 0 + idx_ref = 0 + for i in range(nx): + output = outputs[i] + n_sd = output["n_sd"] + idx = np.nonzero(x_array==n_sd)[0][0] + + if idx > idx_ref: + idx_ref = idx + jdx = 0 + + ni_arr[jdx,idx] = output["ni"][-1] + ri_arr[jdx,idx] = output["ri"][-1] + frozen = output["frozen"] + frozen_fraction_arr[jdx,idx] = np.sum(frozen) / np.size(frozen) + min_frozen_r_arr[jdx,idx] = m_to_µm * np.amin( + np.ma.masked_array(output["r_dry"], + mask=np.logical_not(frozen)) + ) + jdx += 1 + + fig, axs = pyplot.subplots(2, 2, figsize=(10, 10), sharex=False) + # fig.suptitle(title) + + axN = axs[0, 0] + axN.set_title("(a) nucleated number concentration", fontsize=title_fsize) + axN.boxplot(ni_arr,tick_labels=x_string_array) + axN.set_yscale('log') + axN.set_ylim(1.e-2, 1.e2) + axN.axhline(ni_bulk_ref, c="r") + axN.set_ylabel(r"ice number concentration [$\mathrm{cm^{-3}}$]" + , fontsize=ax_lab_fsize) + axN.set_xlabel(x_label, fontsize=ax_lab_fsize) + axN.tick_params(axis='y', labelsize=tick_fsize) + + axR = axs[0, 1] + axR.set_title("(b) mean radius", fontsize=title_fsize) + axR.boxplot(ri_arr,tick_labels=x_string_array) + axR.set_xlabel(x_label) + axR.set_yscale('log') + axR.set_ylim(5.e-2, 1.e2) + axR.set_ylabel(r"ice mean radius[µm]" + , fontsize=ax_lab_fsize) + axR.tick_params(axis='y', labelsize=tick_fsize) + + axF = axs[1, 0] + axF.set_title("(c) frozen fraction", fontsize=title_fsize) + axF.boxplot(frozen_fraction_arr,tick_labels=x_string_array) + axF.set_xlabel(x_label, fontsize=ax_lab_fsize) + axF.set_yscale('log') + axF.set_ylim(1.e-5, 1) + axF.set_ylabel(r"fraction of frozen super particles" + , fontsize=ax_lab_fsize) + axF.tick_params(axis='y', labelsize=tick_fsize) + + axM = axs[1, 1] + axM.set_title("(d) radius of smallest frozen droplet", fontsize=title_fsize) + axM.boxplot(min_frozen_r_arr,tick_labels=x_string_array) + axM.set_xlabel(x_label, fontsize=ax_lab_fsize) + axM.set_yscale('log') + axM.set_ylim(5.e-3, 5.e0) + axM.set_ylabel(r"minimum radius of frozen droplets [µm]" + , fontsize=ax_lab_fsize) + axM.tick_params(axis='y', labelsize=tick_fsize) + + fig.tight_layout() + pp.savefig() + + + +# plot super particle ensemble +def plot_ensemble_simulation(file_name): + + plot_name = file_name.replace("json","pdf") + + with open(file_name, 'r') as f: + data = json.load(f) + outputs = data["outputs"] + number_of_ensemble_runs = data["number_of_ensemble_runs"] + T0 = data["initial_temperature"] + + pp = PdfPages(plot_name) + plot_ensemble(pp, outputs, T0, number_of_ensemble_runs) + pp.close() + +filename= "ensemble_nsd_25_dsd_0_T0_220.json" +plot_ensemble_simulation(filename) +filename= "ensemble_nsd_25_dsd_0_T0_220_lin.json" +plot_ensemble_simulation(filename) +filename= "ensemble_nsd_2_dsd_0_T0_220_limit.json" +plot_ensemble_simulation(filename) + + + + +# plot super particle ensemble +def plot_simulation_evolution(file_name): + + plot_name = file_name.replace("json","pdf") + + with open(file_name, 'r') as f: + data = json.load(f) + outputs = data["outputs"] + + pp = PdfPages(plot_name) + for output in outputs: + plot_evolution(pp, output) + pp.close() + +# filename="ensemble_1_dsd_1_T0_220_nsd_50000highoutput.json" +# plot_simulation_evolution(filename) + +def plot_size_distribution_discretisation(): + + target_n_sd = 50 + def get_r_and_m(file_name): + with open(file_name, 'r') as f: + data = json.load(f) + for output in data["outputs"]: + if output["n_sd"][0] == target_n_sd: + return( np.array(output["r_wet"][0]), + np.array(output["multiplicity"][0]) ) + + file_name = "ensemble_nsd_1_dsd_0_T0_220_nsd_50.json" + dsd1 = get_r_and_m(file_name) + + file_name = "ensemble_nsd_1_dsd_1_T0_220_nsd_50.json" + dsd2 = get_r_and_m(file_name) + + file_name = "ensemble_nsd_1_dsd_2_T0_220_nsd_50.json" + dsd3 = get_r_and_m(file_name) + + file_name = "ensemble_nsd_1_dsd_0_T0_220_nsd_50_lin.json" + dsd1_lin = get_r_and_m(file_name) + + # file_name = "ensemble_1_dsd_0_T0_220_nsd_50_RH16.json" + # dsd1_16 = get_r_and_m(file_name) + # file_name = "ensemble_1_dsd_0_T0_220_nsd_50_RH10.json" + # dsd1_10 = get_r_and_m(file_name) + + file_name = "ensemble_1_dsd_0_T0_190_nsd_50.json" + dsd_1_190 = get_r_and_m(file_name) + + file_name = "ensemble_1_dsd_0_T0_200_nsd_50.json" + dsd_1_200 = get_r_and_m(file_name) + + file_name = "ensemble_1_dsd_0_T0_220_nsd_50.json" + dsd_1_220 = get_r_and_m(file_name) + + file_name = "ensemble_nsd_1_dsd_0_T0_220_nsd_50_limit.json" + dsd1_lim = get_r_and_m(file_name) + + + + + fig, axs = pyplot.subplots(2, 2, figsize=(10, 10), sharex=False) + # fig.suptitle("Solution droplet size distributions") + + ax = axs[0, 0] + + xaxis_label = r"droplet radius r [µm]" + + ax.scatter(dsd1[0] * m_to_µm, dsd1[1], color="black", label="DSD1") + ax.scatter(dsd2[0] * m_to_µm, dsd2[1], color="red", label="DSD2") + ax.scatter(dsd3[0] * m_to_µm, dsd3[1], color="blue", label="DSD3") + ax.tick_params(labelsize=tick_fsize) + ax.set_xscale('log') + ax.set_xlim(5.e-3, 5.e0) + ax.set_ylim(5e7, 2e12) + ax.set_yscale('log') + ax.set_xlabel(xaxis_label,fontsize=ax_lab_fsize) + ax.set_ylabel("multiplicty",fontsize=ax_lab_fsize) + ax.set_title("(a) logarithmic sampling",fontsize=title_fsize) + ax.legend(fontsize=ax_lab_fsize) + + ax = axs[0, 1] + ax.scatter(dsd1_lim[0] * m_to_µm, dsd1_lim[1], color="black", label="DSD1") + ax.tick_params(labelsize=tick_fsize) + ax.set_xscale('log') + ax.set_xlim(5.e-3, 5.e0) + ax.set_ylim(1e7, 2e12) + ax.set_yscale('log') + ax.set_xlabel(xaxis_label, fontsize=ax_lab_fsize) + ax.set_ylabel("multiplicty", fontsize=ax_lab_fsize) + ax.set_title("(b) limited logarithmic sampling", fontsize=title_fsize) + ax.legend(fontsize=ax_lab_fsize) + + + ax = axs[1, 0] + ax.scatter(dsd1_lin[0] * m_to_µm, dsd1_lin[1], color="black", label="DSD1") + ax.tick_params(labelsize=tick_fsize) + ax.set_xscale('log') + ax.set_xlim(5.e-3, 5.e0) + ax.set_ylim(1e7, 2e12) + ax.set_yscale('log') + ax.set_xlabel(xaxis_label,fontsize=ax_lab_fsize) + ax.set_ylabel("multiplicty",fontsize=ax_lab_fsize) + ax.set_title("(c) linear sampling",fontsize=title_fsize) + ax.legend(fontsize=ax_lab_fsize) + + ax = axs[1, 1] + + # ax.scatter(dsd1_10[0] * m_to_µm, dsd1_10[1], color="black", label="T=220K") + # ax.scatter(dsd1_16[0] * m_to_µm, dsd1_16[1], color="red", label="T=200K") + ax.scatter(dsd_1_220[0] * m_to_µm, dsd_1_220[1], color="black", label="T=220K") + ax.scatter(dsd_1_200[0] * m_to_µm, dsd_1_200[1], color="red", label="T=200K") + ax.scatter(dsd_1_190[0] * m_to_µm, dsd_1_190[1], color="blue", label="T=190K") + ax.tick_params(labelsize=tick_fsize) + ax.set_xscale('log') + ax.set_xlim(5.e-3, 5.e0) + ax.set_ylim(5e7, 2e12) + ax.set_yscale('log') + ax.set_xlabel(xaxis_label, fontsize=ax_lab_fsize) + ax.set_ylabel("multiplicty", fontsize=ax_lab_fsize) + ax.set_title("(d) temperature dependence", fontsize=title_fsize) + ax.legend(fontsize=ax_lab_fsize) + + fig.tight_layout() + plt.savefig("size_distributions.pdf") + + + +# plot_size_distribution_discretisation() + +# print( critical_supersaturation(220.) ) \ No newline at end of file diff --git a/examples/PySDM_examples/Kaercher_Lohmann_2002/reference.py b/examples/PySDM_examples/Kaercher_Lohmann_2002/reference.py index 8dc82a2eb0..817153660e 100644 --- a/examples/PySDM_examples/Kaercher_Lohmann_2002/reference.py +++ b/examples/PySDM_examples/Kaercher_Lohmann_2002/reference.py @@ -1,5 +1,27 @@ +import numpy as np +from PySDM.physics.constants import si +def critical_supersaturation(temperature): + return 2.349 - temperature / 259. +# crit thres +def critical_supersaturation_spich2023 (t): + s20=1.67469 + s21=0.00228125 + s22=-1.36989e-05 + return s20+s21*t+s22*t*t + + +def bulk_model_reference(initial_temperature, updraft=0.1): + n_hom_ice = None + + if initial_temperature == 220.: + if updraft == 0.1: + n_hom_ice = 148121.413358197 + if updraft == 1.: + n_hom_ice = 7268664.77542974 + + + + return( n_hom_ice / si.metre ** 3 ) -def critical_supersaturation(T): - return 2.349 - T / 259. \ No newline at end of file diff --git a/examples/PySDM_examples/Kaercher_Lohmann_2002/run.py b/examples/PySDM_examples/Kaercher_Lohmann_2002/run.py index 258d9d5bce..20cefcabae 100644 --- a/examples/PySDM_examples/Kaercher_Lohmann_2002/run.py +++ b/examples/PySDM_examples/Kaercher_Lohmann_2002/run.py @@ -1,172 +1,144 @@ -import matplotlib.pyplot as plt -from matplotlib import pyplot + from matplotlib.backends.backend_pdf import PdfPages import numpy as np +import pickle +import json -from settings import settings -from simulation import Simulation -from reference import critical_supersaturation from PySDM.physics.constants import si -kg_to_µg = 1.e9 -m_to_µm = 1.e6 - - -def plot_size_distribution(r_wet, r_dry, N, setting, pp): - r_wet, r_dry = r_wet * m_to_µm, r_dry * m_to_µm - - title = f"N0: {setting.N_dv_solution_droplet * 1e-6:.2E} cm-3 \ - R0: {setting.r_mean_solution_droplet * m_to_µm:.2E} µm \ - $\sigma$: {setting.sigma_solution_droplet:.2f} \ - Nsd: {setting.n_sd:d} $\kappa$: {setting.kappa:.2f}" - - fig = plt.figure() - ax = fig.add_subplot(1, 1, 1) - - ax.scatter(r_dry, N, color="red", label="dry") - ax.scatter(r_wet, N, color="blue", label="wet") - ax.set_title(title) - ax.legend() - ax.set_xscale('log') - ax.set_xlim(5.e-3, 5.e0) - ax.set_yscale('log') - ax.set_xlabel(r"radius [µm]") - ax.set_ylabel("multiplicty") - - pp.savefig() - - -def plot(output, setting, pp): - time = output["t"] - temperature = np.asarray(output["T"]) - z = output["z"] - - rh = output["RH"] - rhi = output["RHi"] - rhi_crit = critical_supersaturation(temperature) * 100. - - print(f"{rh=},{rhi=},{rhi_crit=}") - - lwc = np.asarray(output["LWC"]) * kg_to_µg - iwc = abs(np.asarray(output["IWC"])) * kg_to_µg - twc = lwc + iwc - qv = np.asarray(output["qv"]) * kg_to_µg - - print(f"{lwc=},{iwc=},{twc=},{qv=}") - - ns = output["ns"] - ni = output["ni"] - rs = output["rs"] - ri = output["ri"] - print(f"{ns=},{ni=},{rs=},{ri=},") - - fig, axs = pyplot.subplots(3, 2, figsize=(10, 10), sharex=True) - - title = f"w: {setting.w_updraft:.2f} m s-1 T0: {setting.initial_temperature:.2f} K Nsd: {setting.n_sd:d} \ - rate: " + setting.rate - - fig.suptitle(title) - - axTz = axs[0, 0] - - axTz.plot( - time, z, color="black", linestyle="-", label="dz", linewidth=5 - ) - axTz.set_ylabel("vertical displacemet [m]") - axTz.set_ylim(-5, 1000) - twin = axTz.twinx() - twin.plot( - time, temperature, color="red", linestyle="-", label="T", linewidth=5 - ) - twin.set_ylim(190, 250) - twin.legend(loc='upper right') - twin.set_ylabel("temperature [K]") - axTz.legend(loc='upper left') - axTz.set_xlabel("time [s]") - - axRH = axs[0, 1] - - axRH.plot( - time, rh, color="blue", linestyle="-", label="water", linewidth=5 - ) - axRH.plot( - time, rhi, color="red", linestyle="-", label="ice", linewidth=5 - ) - axRH.plot( - time, rhi_crit, color="black", linestyle="-", label="crit", linewidth=5 - ) - axRH.legend() - axRH.set_xlabel("time [s]") - axRH.set_ylabel("relative humidity [%]") - axRH.set_ylim(50, 200) - - axWC = axs[1, 0] - - axWC.plot( - time, twc, color="black", linestyle="--", label="total", linewidth=5 - ) - axWC.plot( - time, lwc, color="blue", linestyle="-", label="water", linewidth=5 - ) - axWC.plot( - time, iwc, color="red", linestyle="-", label="ice", linewidth=5 - ) - axWC.set_yscale('log') - axWC.legend() - axWC.set_ylim(1.e1, 1.e5) - axWC.set_xlabel("time [s]") - axWC.set_ylabel(r"mass content [$\mathrm{\mu g \, kg^{-1}}$]") - - axN = axs[1, 1] - - axN.plot( - time, ns, color="blue", linestyle="-", label="droplet", linewidth=5) - axN.plot( - time, ni, color="red", linestyle="-", label="ice", linewidth=5) - - axN.set_yscale('log') - axN.legend() - axN.set_ylim(1.e-3, 1.e4) - axN.set_xlabel("time [s]") - axN.set_ylabel(r"number concentration [$\mathrm{cm^{-3}}$]") - - axR = axs[2, 0] - - axR.plot( - time, rs, color="blue", linestyle="-", label="droplet", linewidth=5) - axR.plot( - time, ri, color="red", linestyle="-", label="ice", linewidth=5) - - axR.legend() - axR.set_yscale('log') - axR.set_ylim(5.e-2, 1.e2) - axR.set_xlabel("time [s]") - axR.set_ylabel(r"mean radius [µm]") - - fig.tight_layout() - pp.savefig() - - -general_settings = {"n_sd": 1000, "T0": 220 * si.kelvin, "w_updraft": 10 * si.centimetre / si.second} -distributions = ({"N_dv_solution_droplet": 2500 / si.centimetre ** 3, \ - "r_mean_solution_droplet": 0.055 * si.micrometre, \ +from settings import settings as simulation_settings +from simulation import Simulation +# from plot import plot_size_distribution, plot_evolution, plot_ensemble + +general_settings = {"n_sd": 10, "T0": 220 * si.kelvin, + "w_updraft": 10 * si.centimetre / si.second} +distributions = ({"N_dv_solution_droplet": 2500 / si.centimetre ** 3, + "r_mean_solution_droplet": 0.055 * si.micrometre, "sigma_solution_droplet": 1.6}, - {"N_dv_solution_droplet": 8600 / si.centimetre**3, \ - "r_mean_solution_droplet": 0.0275 * si.micrometre, \ + {"N_dv_solution_droplet": 8600 / si.centimetre**3, + "r_mean_solution_droplet": 0.0275 * si.micrometre, "sigma_solution_droplet": 1.3}, - {"N_dv_solution_droplet": 2000 / si.centimetre**3, \ - "r_mean_solution_droplet": 0.11 * si.micrometre, \ + {"N_dv_solution_droplet": 2000 / si.centimetre**3, + "r_mean_solution_droplet": 0.11 * si.micrometre, "sigma_solution_droplet": 2.}, ) -pp = PdfPages("hom_freezing_for_size_distributions.pdf") - -for distribution in distributions: - setting = settings(**{**general_settings, **distribution}) - model = Simulation(setting) - output = model.run() - - plot_size_distribution(model.r_wet, model.r_dry, model.multiplicities, setting, pp) - plot(output, setting, pp) - -pp.close() +# pp = PdfPages("hom_freezing_for_size_distributions.pdf") +# +# for distribution in distributions: +# setting = settings(**{**general_settings, **distribution}) +# model = Simulation(setting) +# output = model.run() +# +# plot_size_distribution(model.r_wet, model.r_dry, model.multiplicities, setting, pp) +# plot(output, setting, model, pp) + + + +# calculate super particle ensemble +def ensemble_simulation(number_of_ensemble_runs=1, + dsd=0, + T0=220.* si.kelvin, + w_updraft=None, + linear_sampling=False, + nsd_single = None, + lower_limit = None, + add_label = "", + RHi_0 = None, + ): + + file_name = ("ensemble_"+str(number_of_ensemble_runs)+"_dsd_"+str(dsd) + + f"_T0_{T0:.0f}") + + + if nsd_single is None: + number_of_super_particles = (50, 100, 500, 1000, 5000, 10000, 50000, 100000) + else: + number_of_super_particles = (nsd_single,) + file_name += "_nsd_"+str(nsd_single) + + if linear_sampling: + file_name += "_lin" + + if lower_limit is not None: + file_name += "_limit" + file_name += add_label + + + outputs = [] + + aerosol_distribution = distributions[dsd] + setting = {**general_settings, + **aerosol_distribution, + "linear_sampling": linear_sampling, + "lower_limit": lower_limit} + + if T0 is not None: + setting["T0"] = T0 + if RHi_0 is not None: + setting["RHi_0"] = RHi_0 + if w_updraft is not None: + setting["w_updraft"] = w_updraft + + for nsd in number_of_super_particles: + setting["n_sd"] = nsd + + print(setting) + + for _ in range(number_of_ensemble_runs): + simulation_setting = simulation_settings(**setting) + model = Simulation(simulation_setting) + output = model.run() + outputs.append(output) + del model, simulation_setting + + data_file = { "outputs":outputs, + "number_of_ensemble_runs": number_of_ensemble_runs, + "initial_temperature": setting["T0"], + "aerosol_distribution": aerosol_distribution, + "w_updraft": setting["w_updraft"], + } + print("Writing "+file_name+".json") + with open(file_name+".json", 'w') as file: + json.dump(data_file, file) + + +# ensemble_simulation(1) +# setting = {**general_settings, **distributions[0] } +# simulation_setting = simulation_settings(**setting) +# +# setting = {**general_settings, **distributions[0], "linear_sampling": True } +# simulation_setting = simulation_settings(**setting) + + +# for DSD plots +# ensemble_simulation(nsd_single = 50, dsd=0) +# ensemble_simulation(nsd_single = 50, dsd=0, T0=220* si.kelvin, +# RHi_0=1.6, +# add_label="_RH16") +# ensemble_simulation(nsd_single = 50, dsd=0, T0=220* si.kelvin, +# RHi_0=1.0, +# add_label="_RH10") +# ensemble_simulation(nsd_single = 50, dsd=0, T0=220* si.kelvin, +# RHi_0=1.4, +# add_label="_RH14") +# ensemble_simulation(nsd_single = 50, dsd=0, T0=220* si.kelvin, +# RHi_0=1.4,) +# ensemble_simulation(nsd_single = 50, dsd=0, T0=200* si.kelvin, +# RHi_0=1.4,) +# ensemble_simulation(nsd_single = 50, dsd=0, T0=190* si.kelvin, +# RHi_0=1.4,) +# ensemble_simulation(nsd_single = 50, dsd=1) +# ensemble_simulation(nsd_single = 50, dsd=2) +# ensemble_simulation(nsd_single = 50, dsd=0, lower_limit=5.5e-8) +# ensemble_simulation(nsd_single = 50, dsd=0, linear_sampling=True) + +# ensemble_simulation(nsd_single = 50000, dsd=1, +# RHi_0=1.0, w_updraft=1.*si.meter / si.second, +# add_label="highoutput") + +# ensemble_simulation( linear_sampling=True, number_of_ensemble_runs=25) +# +# lower_limit_bound = ( 5.5e-8, ) +# for lower_limit in lower_limit_bound: +# ensemble_simulation( lower_limit=lower_limit, number_of_ensemble_runs=25) \ No newline at end of file diff --git a/examples/PySDM_examples/Kaercher_Lohmann_2002/settings.py b/examples/PySDM_examples/Kaercher_Lohmann_2002/settings.py index cc1e369ce9..731069ad07 100644 --- a/examples/PySDM_examples/Kaercher_Lohmann_2002/settings.py +++ b/examples/PySDM_examples/Kaercher_Lohmann_2002/settings.py @@ -1,3 +1,5 @@ +import time + import numpy as np from pystrict import strict @@ -14,12 +16,15 @@ def __init__( n_sd: int, w_updraft: float, T0: float, + RHi_0: float=1.4, N_dv_solution_droplet: float, r_mean_solution_droplet: float, sigma_solution_droplet: float, kappa: float=0.64, rate: str="Koop2000", dt: float=0.1, + linear_sampling: bool=False, + lower_limit: float=None, ): self.n_sd = n_sd @@ -31,14 +36,17 @@ def __init__( self.mass_of_dry_air = 1000 * si.kilogram self.initial_pressure = 220 * si.hectopascals - self.initial_ice_supersaturation = 1. + self.initial_ice_supersaturation = RHi_0 self.kappa = kappa self.initial_temperature = T0 - + print( "RHi0", self.initial_ice_supersaturation ) + print("T0", self.initial_temperature) + print( "w_updraft", self.w_updraft ) self.formulae = Formulae( particle_shape_and_density="MixedPhaseSpheres", homogeneous_ice_nucleation_rate=rate, constants={"J_HOM": 1.e15}, + seed=time.time_ns() ) const = self.formulae.constants pvs_i = self.formulae.saturation_vapour_pressure.pvs_ice(self.initial_temperature) @@ -49,9 +57,39 @@ def __init__( / self.initial_temperature / const.Rd ) - spectrum = Lognormal(norm_factor=N_dv_solution_droplet / dry_air_density, m_mode=r_mean_solution_droplet, s_geom=sigma_solution_droplet) - self.r_dry, self.specific_concentration = spectral_sampling.Logarithmic(spectrum).sample(n_sd) + spectrum = Lognormal(norm_factor=N_dv_solution_droplet / dry_air_density, + m_mode=r_mean_solution_droplet, + s_geom=sigma_solution_droplet) + + - self.t_duration = 3600 * 1.5 # total duration of simulation + upper_limit = 4e-6 + + if linear_sampling: + self.r_dry, self.specific_concentration = ( + spectral_sampling.Linear(spectrum).sample(n_sd)) + else: + if lower_limit is None: + self.r_dry, self.specific_concentration = ( + spectral_sampling.Logarithmic(spectrum, + ).sample(n_sd)) + else: + self.r_dry, self.specific_concentration = ( + spectral_sampling.Logarithmic(spectrum, + size_range=(lower_limit,upper_limit), + error_threshold=0.95, + ).sample(n_sd)) + test_for_zero_n = self.specific_concentration == 0. + if( any(test_for_zero_n) ): + last_non_zero = np.nonzero(self.specific_concentration)[0][-1] + upper_limit = self.specific_concentration[last_non_zero] + self.r_dry, self.specific_concentration = ( + spectral_sampling.Logarithmic(spectrum, + size_range=(lower_limit, upper_limit), + error_threshold=0.95, + ).sample(n_sd)) + + self.t_duration = 3600 #3600 * 1.5 # total duration of simulation self.dt = dt - self.n_output = 10 # number of output steps \ No newline at end of file + self.n_output = 3600 #100 # number of output steps + diff --git a/examples/PySDM_examples/Kaercher_Lohmann_2002/simulation.py b/examples/PySDM_examples/Kaercher_Lohmann_2002/simulation.py index eab04275ca..180e3e69ac 100644 --- a/examples/PySDM_examples/Kaercher_Lohmann_2002/simulation.py +++ b/examples/PySDM_examples/Kaercher_Lohmann_2002/simulation.py @@ -50,6 +50,7 @@ def __init__(self, settings, backend=CPU): builder.add_dynamic(VapourDepositionOnIce()) builder.add_dynamic(Freezing(singular=False, homogeneous_freezing=True, immersion_freezing=False)) + self.n_sd = settings.n_sd self.multiplicities = discretise_multiplicities(settings.specific_concentration * env.mass_of_dry_air) self.r_dry = settings.r_dry v_dry = settings.formulae.trivia.volume(radius=self.r_dry) @@ -98,9 +99,6 @@ def __init__(self, settings, backend=CPU): self.n_output = settings.n_output self.n_substeps = int(settings.t_duration / dt / self.n_output) - print(settings.t_duration, dt, self.n_output, self.n_substeps) - - print(self.particulator.n_sd) def save(self, output): cell_id = 0 @@ -119,7 +117,7 @@ def save(self, output): output["ni"].append(self.particulator.products["n_i"].get()[cell_id]) output["rs"].append(self.particulator.products["r_s"].get()[cell_id]) output["ri"].append(self.particulator.products["r_i"].get()[cell_id]) - + output["water_mass"].append(self.particulator.attributes["signed water mass"].data.tolist()) def run(self): output = { "t": [], @@ -136,9 +134,19 @@ def run(self): "ni": [], "rs": [], "ri": [], + "frozen":[], + "multiplicity": [], + "r_dry":[], + "r_wet": [], + "water_mass":[] } self.save(output) + output["n_sd"] = [ self.n_sd ] + output["r_dry"].append( self.r_dry.tolist() ) + output["r_wet"].append(self.r_wet.tolist()) + + RHi_old = self.particulator.products["RH_ice"].get()[0].copy() for _ in range(self.n_output): # print(self.particulator.__dict__) # print(self.particulator.attributes.__dict__) @@ -146,7 +154,23 @@ def run(self): self.particulator.run(self.n_substeps) # print(self.particulator.products["t"].get()) # print(self.particulator.products["p"].get()) - # print(self.particulator.attributes["signed water mass"].data) + # self.save(output) + + RHi = self.particulator.products["RH_ice"].get()[0].copy() + dRHi = (RHi_old - RHi) / RHi_old + if (dRHi > 0. and RHi < 130.): + print("break") + break + else: + RHi_old = RHi + + frozen = np.where(self.particulator.attributes["signed water mass"].data < 0., 1, 0) + output["frozen"].append(frozen.tolist()) + output["multiplicity"].append( + self.particulator.attributes["multiplicity"].data.tolist() + ) + + return output From fd13a2edde1c229ec74bcca313035979f59c8e8e Mon Sep 17 00:00:00 2001 From: Tim Luettmer Date: Fri, 18 Apr 2025 16:31:38 +0200 Subject: [PATCH 22/57] added unit test for hom. ice nucleation rates and adjusted formatting --- PySDM/physics/constants_defaults.py | 2 +- .../homogeneous_ice_nucleation_rate/koop.py | 5 ++- .../koop_corr.py | 6 +++- .../test_homogeneous_nucleation_rates.py | 36 +++++++++++++++++++ 4 files changed, 46 insertions(+), 3 deletions(-) create mode 100644 tests/unit_tests/physics/test_homogeneous_nucleation_rates.py diff --git a/PySDM/physics/constants_defaults.py b/PySDM/physics/constants_defaults.py index 1ec73fa7b7..b9d1c76b6f 100644 --- a/PySDM/physics/constants_defaults.py +++ b/PySDM/physics/constants_defaults.py @@ -324,7 +324,7 @@ KOOP_2000_C1 = -906.7 KOOP_2000_C2 = 8502 -KOOP_2000_C3 = 26924 +KOOP_2000_C3 = -26924 KOOP_2000_C4 = 29180 KOOP_UNIT = 1 / si.cm**3 / si.s diff --git a/PySDM/physics/homogeneous_ice_nucleation_rate/koop.py b/PySDM/physics/homogeneous_ice_nucleation_rate/koop.py index 4358f59c21..04c4a9e4ee 100644 --- a/PySDM/physics/homogeneous_ice_nucleation_rate/koop.py +++ b/PySDM/physics/homogeneous_ice_nucleation_rate/koop.py @@ -11,4 +11,7 @@ def __init__(self, const): @staticmethod def j_hom(const, T, da_w_ice): - return 10**(const.KOOP_2000_C1 + const.KOOP_2000_C2 * da_w_ice - const.KOOP_2000_C3 * da_w_ice**2. + const.KOOP_2000_C4 * da_w_ice**3.) * const.KOOP_UNIT + return 10**(const.KOOP_2000_C1 + + const.KOOP_2000_C2 * da_w_ice + + const.KOOP_2000_C3 * da_w_ice**2. + + const.KOOP_2000_C4 * da_w_ice**3.) * const.KOOP_UNIT diff --git a/PySDM/physics/homogeneous_ice_nucleation_rate/koop_corr.py b/PySDM/physics/homogeneous_ice_nucleation_rate/koop_corr.py index af32372657..2646df528d 100644 --- a/PySDM/physics/homogeneous_ice_nucleation_rate/koop_corr.py +++ b/PySDM/physics/homogeneous_ice_nucleation_rate/koop_corr.py @@ -12,4 +12,8 @@ def __init__(self, const): @staticmethod def j_hom(const, T, da_w_ice): - return 10**(const.KOOP_2000_C1 + const.KOOP_2000_C2 * da_w_ice - const.KOOP_2000_C3 * da_w_ice**2. + const.KOOP_2000_C4 * da_w_ice**3. + const.KOOP_CORR) * const.KOOP_UNIT + return 10**(const.KOOP_2000_C1 + + const.KOOP_2000_C2 * da_w_ice + + const.KOOP_2000_C3 * da_w_ice**2. + + const.KOOP_2000_C4 * da_w_ice**3. + + const.KOOP_CORR) * const.KOOP_UNIT diff --git a/tests/unit_tests/physics/test_homogeneous_nucleation_rates.py b/tests/unit_tests/physics/test_homogeneous_nucleation_rates.py new file mode 100644 index 0000000000..b352c37706 --- /dev/null +++ b/tests/unit_tests/physics/test_homogeneous_nucleation_rates.py @@ -0,0 +1,36 @@ +""" +test for homogeneous nucleation rate parametrisations +""" +import pytest +import numpy as np +from PySDM.formulae import _choices, Formulae + +class TestHomogeneousIceNucleationRate: + @staticmethod + @pytest.mark.parametrize( + "da_w_ice, expected_value", + ( + (0.27, (5)), + (0.29, (11)), + (0.31, (15)), + (0.33, (20)), + ), + ) + @pytest.mark.parametrize("parametrisation", ("Koop_Correction",)) + def test_homogeneous_ice_nucleation_rate(da_w_ice,expected_value,parametrisation): + """Fig. 2 in [Spichtinger et al. 2023](https://doi.org/10.5194/acp-23-2035-2023)""" + # arrange + formulae = Formulae( + homogeneous_ice_nucleation_rate=parametrisation, + ) + + # act + jhom_log10 = np.log10(formulae.homogeneous_ice_nucleation_rate.j_hom(np.nan, da_w_ice)) + + # assert + np.testing.assert_approx_equal( + actual=jhom_log10, desired=expected_value, significant=2 + ) + + + From 0129239ca3158186efce7401dbd2cfe987f954ae Mon Sep 17 00:00:00 2001 From: Tim Luettmer Date: Sat, 19 Apr 2025 21:18:01 +0200 Subject: [PATCH 23/57] some cleanup and changes to example --- .../impl_numba/methods/freezing_methods.py | 25 +-- .../Kaercher_Lohmann_2002/plot.py | 201 ++++++++++++------ .../Kaercher_Lohmann_2002/reference.py | 4 + .../Kaercher_Lohmann_2002/run.py | 29 ++- .../Kaercher_Lohmann_2002/settings.py | 8 +- .../Kaercher_Lohmann_2002/simulation.py | 2 +- 6 files changed, 184 insertions(+), 85 deletions(-) diff --git a/PySDM/backends/impl_numba/methods/freezing_methods.py b/PySDM/backends/impl_numba/methods/freezing_methods.py index ed798db67a..750680c1ad 100644 --- a/PySDM/backends/impl_numba/methods/freezing_methods.py +++ b/PySDM/backends/impl_numba/methods/freezing_methods.py @@ -123,20 +123,17 @@ def freeze_time_dependent_homogeneous_body( # pylint: disable=unused-argument,t attributes.signed_water_mass[i], relative_humidity_ice[cell_id] ): d_a_w_ice = (relative_humidity_ice[cell_id] - 1.) * a_w_ice[cell_id] - rate_assuming_constant_temperature_within_dt = ( - j_hom(temperature[cell_id], d_a_w_ice) * attributes.volume[i] - ) - rate = j_hom(temperature[cell_id], d_a_w_ice) - prob = 1 - prob_zero_events( - r=rate_assuming_constant_temperature_within_dt, dt=timestep - ) - randi = rand[i] - if rand[i] < prob: - # print(f"{d_a_w_ice=},{rate=},{prob=},{randi=}") - _freeze(attributes.signed_water_mass, i) - # if record_freezing_temperature: - # freezing_temperature[i] = temperature[cell_id] - # print( attributes.signed_water_mass ) + if d_a_w_ice > 0.23 and d_a_w_ice < 0.34: + rate_assuming_constant_temperature_within_dt = ( + j_hom(temperature[cell_id], d_a_w_ice) * attributes.volume[i] + ) + prob = 1 - prob_zero_events( + r=rate_assuming_constant_temperature_within_dt, dt=timestep + ) + if rand[i] < prob: + _freeze(attributes.signed_water_mass, i) + # if record_freezing_temperature: + # freezing_temperature[i] = temperature[cell_id] self.freeze_time_dependent_homogeneous_body = freeze_time_dependent_homogeneous_body diff --git a/examples/PySDM_examples/Kaercher_Lohmann_2002/plot.py b/examples/PySDM_examples/Kaercher_Lohmann_2002/plot.py index f72457e70e..f410180e65 100644 --- a/examples/PySDM_examples/Kaercher_Lohmann_2002/plot.py +++ b/examples/PySDM_examples/Kaercher_Lohmann_2002/plot.py @@ -43,7 +43,7 @@ def plot_size_distribution(r_wet, r_dry, N, setting, pp): def plot_evolution( pp, output ): ln = 2.5 - extra_panel = False + extra_panel = True time = output["t"] temperature = np.asarray(output["T"]) @@ -64,13 +64,13 @@ def plot_evolution( pp, output ): ri = output["ri"] frozen = output["frozen"] - r_wet_init = output["r_wet"] + r_wet_init = np.asarray(output["r_wet"]) * m_to_µm multiplicities = output["multiplicity"] multiplicities_unfrozen = np.ma.masked_array(multiplicities, mask=frozen) multiplicities_frozen = np.ma.masked_array(multiplicities, mask=np.logical_not(frozen)) if extra_panel: - fig, axs = pyplot.subplots(3, 2, figsize=(10, 10), sharex=False) + fig, axs = pyplot.subplots(3, 2, figsize=(10, 15), sharex=False) else: fig, axs = pyplot.subplots(2, 2, figsize=(10, 10), sharex=False) @@ -170,32 +170,38 @@ def plot_evolution( pp, output ): axWC.plot( time, iwc, color="red", linestyle="-", label="ice", linewidth=ln ) + axWC.set_title("(e) mass content", fontsize=title_fsize) axWC.set_yscale('log') - axWC.legend() - axWC.set_ylim(1.e1, 1.e5) + axWC.legend(fontsize=ax_lab_fsize) + axWC.tick_params(labelsize=tick_fsize) + axWC.set_xlim(x_limit) + axWC.set_ylim(1.e0, 5.e4) axWC.set_xlabel("time [s]", fontsize=ax_lab_fsize) axWC.set_ylabel(r"mass content [$\mathrm{\mu g \, kg^{-1}}$]" , fontsize=ax_lab_fsize) + axWC.grid(True) axFrz = axs[2, 1] axFrz.scatter(r_wet_init, multiplicities_unfrozen, color="black", alpha=0.5, label="unfrozen") axFrz.scatter(r_wet_init, multiplicities_frozen, color="blue", label="frozen") - axFrz.legend() + axFrz.set_title("(f) frozen super particles", fontsize=title_fsize) + axFrz.legend(fontsize=ax_lab_fsize) + axFrz.tick_params(labelsize=tick_fsize) axFrz.set_xscale('log') - axFrz.set_xlim(5.e-3, 5.e0) + axFrz.set_xlim(5.e-3, 5.e-1) axFrz.set_yscale('log') - axFrz.set_xlabel(r"initial wet radius [µm]", fontsize=ax_lab_fsize) + axFrz.set_xlabel(r"initial radius [µm]", fontsize=ax_lab_fsize) axFrz.set_ylabel("multiplicty", fontsize=ax_lab_fsize) + # axFrz.grid(True) fig.tight_layout() pp.savefig() -def plot_ensemble(pp, outputs, T0, ens_member): +def plot_ensemble(pp, outputs, T0, ens_member, only_ni = False, title = None): + - title = ("Ensemble simulation with " + str(ens_member) - + f" members for T0: {T0:.2f}K ") x_label = "number of super particles" @@ -234,49 +240,64 @@ def plot_ensemble(pp, outputs, T0, ens_member): ) jdx += 1 - fig, axs = pyplot.subplots(2, 2, figsize=(10, 10), sharex=False) + if only_ni: + fig, ax = pyplot.subplots(1, 1, figsize=(5, 5)) + else: + fig, axs = pyplot.subplots(2, 2, figsize=(10, 10), sharex=False) # fig.suptitle(title) - axN = axs[0, 0] - axN.set_title("(a) nucleated number concentration", fontsize=title_fsize) - axN.boxplot(ni_arr,tick_labels=x_string_array) - axN.set_yscale('log') - axN.set_ylim(1.e-2, 1.e2) - axN.axhline(ni_bulk_ref, c="r") - axN.set_ylabel(r"ice number concentration [$\mathrm{cm^{-3}}$]" - , fontsize=ax_lab_fsize) - axN.set_xlabel(x_label, fontsize=ax_lab_fsize) - axN.tick_params(axis='y', labelsize=tick_fsize) - - axR = axs[0, 1] - axR.set_title("(b) mean radius", fontsize=title_fsize) - axR.boxplot(ri_arr,tick_labels=x_string_array) - axR.set_xlabel(x_label) - axR.set_yscale('log') - axR.set_ylim(5.e-2, 1.e2) - axR.set_ylabel(r"ice mean radius[µm]" - , fontsize=ax_lab_fsize) - axR.tick_params(axis='y', labelsize=tick_fsize) - - axF = axs[1, 0] - axF.set_title("(c) frozen fraction", fontsize=title_fsize) - axF.boxplot(frozen_fraction_arr,tick_labels=x_string_array) - axF.set_xlabel(x_label, fontsize=ax_lab_fsize) - axF.set_yscale('log') - axF.set_ylim(1.e-5, 1) - axF.set_ylabel(r"fraction of frozen super particles" - , fontsize=ax_lab_fsize) - axF.tick_params(axis='y', labelsize=tick_fsize) - - axM = axs[1, 1] - axM.set_title("(d) radius of smallest frozen droplet", fontsize=title_fsize) - axM.boxplot(min_frozen_r_arr,tick_labels=x_string_array) - axM.set_xlabel(x_label, fontsize=ax_lab_fsize) - axM.set_yscale('log') - axM.set_ylim(5.e-3, 5.e0) - axM.set_ylabel(r"minimum radius of frozen droplets [µm]" - , fontsize=ax_lab_fsize) - axM.tick_params(axis='y', labelsize=tick_fsize) + if only_ni: + if title is not None: + ax.set_title(title, fontsize=title_fsize) + ax.boxplot(ni_arr, tick_labels=x_string_array) + ax.set_yscale('log') + ax.set_ylim(1.e-2, 1.e2) + ax.axhline(ni_bulk_ref, c="r") + ax.set_ylabel(r"ice number concentration [$\mathrm{cm^{-3}}$]" + , fontsize=ax_lab_fsize) + ax.set_xlabel(x_label, fontsize=ax_lab_fsize) + ax.tick_params(axis='y', labelsize=tick_fsize) + else: + axN = axs[0, 0] + axN.set_title("(a) nucleated number concentration", fontsize=title_fsize) + axN.boxplot(ni_arr,tick_labels=x_string_array) + axN.set_yscale('log') + axN.set_ylim(1.e-2, 1.e2) + axN.axhline(ni_bulk_ref, c="r") + axN.set_ylabel(r"ice number concentration [$\mathrm{cm^{-3}}$]" + , fontsize=ax_lab_fsize) + axN.set_xlabel(x_label, fontsize=ax_lab_fsize) + axN.tick_params(axis='y', labelsize=tick_fsize) + + axR = axs[0, 1] + axR.set_title("(b) mean radius", fontsize=title_fsize) + axR.boxplot(ri_arr,tick_labels=x_string_array) + axR.set_xlabel(x_label) + axR.set_yscale('log') + axR.set_ylim(5.e-2, 1.e2) + axR.set_ylabel(r"ice mean radius[µm]" + , fontsize=ax_lab_fsize) + axR.tick_params(axis='y', labelsize=tick_fsize) + + axF = axs[1, 0] + axF.set_title("(c) frozen fraction", fontsize=title_fsize) + axF.boxplot(frozen_fraction_arr,tick_labels=x_string_array) + axF.set_xlabel(x_label, fontsize=ax_lab_fsize) + axF.set_yscale('log') + axF.set_ylim(1.e-5, 1) + axF.set_ylabel(r"fraction of frozen super particles" + , fontsize=ax_lab_fsize) + axF.tick_params(axis='y', labelsize=tick_fsize) + + axM = axs[1, 1] + axM.set_title("(d) radius of smallest frozen droplet", fontsize=title_fsize) + axM.boxplot(min_frozen_r_arr,tick_labels=x_string_array) + axM.set_xlabel(x_label, fontsize=ax_lab_fsize) + axM.set_yscale('log') + axM.set_ylim(5.e-3, 5.e0) + axM.set_ylabel(r"minimum radius of frozen droplets [µm]" + , fontsize=ax_lab_fsize) + axM.tick_params(axis='y', labelsize=tick_fsize) fig.tight_layout() pp.savefig() @@ -284,7 +305,7 @@ def plot_ensemble(pp, outputs, T0, ens_member): # plot super particle ensemble -def plot_ensemble_simulation(file_name): +def plot_ensemble_simulation(file_name, only_ni=False, title=None): plot_name = file_name.replace("json","pdf") @@ -295,15 +316,15 @@ def plot_ensemble_simulation(file_name): T0 = data["initial_temperature"] pp = PdfPages(plot_name) - plot_ensemble(pp, outputs, T0, number_of_ensemble_runs) + plot_ensemble(pp, outputs, T0, number_of_ensemble_runs, only_ni=only_ni, title = title) pp.close() -filename= "ensemble_nsd_25_dsd_0_T0_220.json" -plot_ensemble_simulation(filename) +# filename= "ensemble_nsd_25_dsd_0_T0_220.json" +# plot_ensemble_simulation(filename) filename= "ensemble_nsd_25_dsd_0_T0_220_lin.json" -plot_ensemble_simulation(filename) -filename= "ensemble_nsd_2_dsd_0_T0_220_limit.json" -plot_ensemble_simulation(filename) +plot_ensemble_simulation(filename, only_ni=True, title="linear sampling") +filename= "ensemble_nsd_25_dsd_0_T0_220_limit.json" +plot_ensemble_simulation(filename, only_ni=True, title="limited sampling") @@ -435,6 +456,66 @@ def get_r_and_m(file_name): +def plot_ni_as_function_of_w(): + + dsd_list = np.array([0]) + initial_temperatures = np.array([196., 216., 236.]) + updrafts = np.array([0.05, 0.1, 0.5, 1., 5., 10.]) + + dim_size = ( np.shape(dsd_list)[0], np.shape(initial_temperatures)[0], np.shape(updrafts)[0] ) + ni_sdm = np.zeros(dim_size) + + + + for i in range(dim_size[0]): + for j in range(dim_size[1]): + T0 = initial_temperatures[j] + for k in range(dim_size[2]): + w = updrafts[k] + filename = "ensemble_1_dsd_"+str(i)+f"_T0_{T0:.0f}"+f"_W_{w:.2f}"+"_nsd_10.json" + # print(filename) + + with open(filename, 'r') as f: + data = json.load(f) + + T0_sim = data['initial_temperature'] + w_sim = data['w_updraft'] + if T0 == T0_sim and w == w_sim: + output = data['outputs'][0] + ni_last = output["ni"][-1] + print( w, T0, ni_last ) + ni_sdm[i,j,k] = ni_last + + print(ni_sdm) + + fig, axs = pyplot.subplots(1, 3, figsize=(15, 5), sharex=False) + + for i in range(1): + ax = axs[i] + + if i == 0: + ax.set_title("(a) hom. nucleation for DSD 1",fontsize=title_fsize) + if i == 1: + ax.set_title("(b) hom. nucleation for DSD 2", fontsize=title_fsize) + if i == 2: + ax.set_title("(c) hom. nucleation for DSD 3", fontsize=title_fsize) + + for j in range(dim_size[1]): + ax.scatter(updrafts, ni_sdm[i,j,:],label=f"T0={initial_temperatures[j]:.0f}K") + + ax.tick_params(labelsize=tick_fsize) + ax.set_xscale('log') + ax.set_xlabel(r"vertical updraft [$\mathrm{m \, s^{-1}}$]" + , fontsize=ax_lab_fsize) + ax.set_yscale('log') + ax.set_ylim(1.e-1, 8.e3) + ax.set_ylabel(r"ice number concentration [$\mathrm{cm^{-3}}$]" + , fontsize=ax_lab_fsize) + ax.legend(fontsize=ax_lab_fsize,loc="lower right") + + plt.tight_layout + plt.savefig("w_ni_plot.pdf") # plot_size_distribution_discretisation() -# print( critical_supersaturation(220.) ) \ No newline at end of file +# print( critical_supersaturation(220.) ) +# plot_ni_as_function_of_w() \ No newline at end of file diff --git a/examples/PySDM_examples/Kaercher_Lohmann_2002/reference.py b/examples/PySDM_examples/Kaercher_Lohmann_2002/reference.py index 817153660e..968d01590b 100644 --- a/examples/PySDM_examples/Kaercher_Lohmann_2002/reference.py +++ b/examples/PySDM_examples/Kaercher_Lohmann_2002/reference.py @@ -21,6 +21,10 @@ def bulk_model_reference(initial_temperature, updraft=0.1): if updraft == 1.: n_hom_ice = 7268664.77542974 + if initial_temperature == 216.: + if updraft == 1.: + n_hom_ice = 10475282.894692907 + return( n_hom_ice / si.metre ** 3 ) diff --git a/examples/PySDM_examples/Kaercher_Lohmann_2002/run.py b/examples/PySDM_examples/Kaercher_Lohmann_2002/run.py index 20cefcabae..87623abb11 100644 --- a/examples/PySDM_examples/Kaercher_Lohmann_2002/run.py +++ b/examples/PySDM_examples/Kaercher_Lohmann_2002/run.py @@ -10,7 +10,7 @@ from simulation import Simulation # from plot import plot_size_distribution, plot_evolution, plot_ensemble -general_settings = {"n_sd": 10, "T0": 220 * si.kelvin, +general_settings = {"n_sd": 10, "T0": 216 * si.kelvin, "w_updraft": 10 * si.centimetre / si.second} distributions = ({"N_dv_solution_droplet": 2500 / si.centimetre ** 3, "r_mean_solution_droplet": 0.055 * si.micrometre, @@ -38,7 +38,7 @@ # calculate super particle ensemble def ensemble_simulation(number_of_ensemble_runs=1, dsd=0, - T0=220.* si.kelvin, + T0=216.* si.kelvin, w_updraft=None, linear_sampling=False, nsd_single = None, @@ -48,7 +48,7 @@ def ensemble_simulation(number_of_ensemble_runs=1, ): file_name = ("ensemble_"+str(number_of_ensemble_runs)+"_dsd_"+str(dsd) - + f"_T0_{T0:.0f}") + + f"_T0_{T0:.0f}" + f"_W_{w_updraft:.2f}") if nsd_single is None: @@ -112,7 +112,7 @@ def ensemble_simulation(number_of_ensemble_runs=1, # for DSD plots -# ensemble_simulation(nsd_single = 50, dsd=0) +# ensemble_simulation(nsd_single = 50000, dsd=0, add_label="koop_corr", w_updraft=1., number_of_ensemble_runs=25) # ensemble_simulation(nsd_single = 50, dsd=0, T0=220* si.kelvin, # RHi_0=1.6, # add_label="_RH16") @@ -137,8 +137,25 @@ def ensemble_simulation(number_of_ensemble_runs=1, # RHi_0=1.0, w_updraft=1.*si.meter / si.second, # add_label="highoutput") -# ensemble_simulation( linear_sampling=True, number_of_ensemble_runs=25) + +# ensemble_simulation(number_of_ensemble_runs=25, w_updraft=1.) +# ensemble_simulation( linear_sampling=True, number_of_ensemble_runs=25, w_updraft=1.) # # lower_limit_bound = ( 5.5e-8, ) # for lower_limit in lower_limit_bound: -# ensemble_simulation( lower_limit=lower_limit, number_of_ensemble_runs=25) \ No newline at end of file +# ensemble_simulation( lower_limit=lower_limit, number_of_ensemble_runs=25, w_updraft=1.) + +initial_temperatures = [196.,216.,236.] +updrafts = [0.05, 0.1, 0.5, 1., 5., 10.] +number_of_ensemble_runs=1 +dsd=0 + +for T in reversed(initial_temperatures): + for w in reversed(updrafts): + ensemble_simulation(number_of_ensemble_runs=number_of_ensemble_runs, + w_updraft=w, + T0=T, + dsd=dsd, + linear_sampling=False, + nsd_single = 10, + ) \ No newline at end of file diff --git a/examples/PySDM_examples/Kaercher_Lohmann_2002/settings.py b/examples/PySDM_examples/Kaercher_Lohmann_2002/settings.py index 731069ad07..25e5818143 100644 --- a/examples/PySDM_examples/Kaercher_Lohmann_2002/settings.py +++ b/examples/PySDM_examples/Kaercher_Lohmann_2002/settings.py @@ -21,7 +21,7 @@ def __init__( r_mean_solution_droplet: float, sigma_solution_droplet: float, kappa: float=0.64, - rate: str="Koop2000", + rate: str="Koop_Correction", dt: float=0.1, linear_sampling: bool=False, lower_limit: float=None, @@ -35,7 +35,7 @@ def __init__( self.rate = rate self.mass_of_dry_air = 1000 * si.kilogram - self.initial_pressure = 220 * si.hectopascals + self.initial_pressure = 200 * si.hectopascals self.initial_ice_supersaturation = RHi_0 self.kappa = kappa self.initial_temperature = T0 @@ -89,7 +89,7 @@ def __init__( error_threshold=0.95, ).sample(n_sd)) - self.t_duration = 3600 #3600 * 1.5 # total duration of simulation + self.t_duration = 7200 #3600 * 1.5 # total duration of simulation self.dt = dt - self.n_output = 3600 #100 # number of output steps + self.n_output = int(self.t_duration / 100) #100 # number of output steps diff --git a/examples/PySDM_examples/Kaercher_Lohmann_2002/simulation.py b/examples/PySDM_examples/Kaercher_Lohmann_2002/simulation.py index 180e3e69ac..512dbe4c8a 100644 --- a/examples/PySDM_examples/Kaercher_Lohmann_2002/simulation.py +++ b/examples/PySDM_examples/Kaercher_Lohmann_2002/simulation.py @@ -171,6 +171,6 @@ def run(self): output["multiplicity"].append( self.particulator.attributes["multiplicity"].data.tolist() ) - + print("Done. Last ni:", output["ni"][-1] ) return output From 98a784b88a5e647958ca18d7cf874d6e67ae165d Mon Sep 17 00:00:00 2001 From: Tim Luettmer Date: Fri, 2 May 2025 20:20:22 +0200 Subject: [PATCH 24/57] some changes to test names and comments --- PySDM/dynamics/freezing.py | 3 ++- tests/unit_tests/dynamics/test_freezing.py | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/PySDM/dynamics/freezing.py b/PySDM/dynamics/freezing.py index e7e753c700..54e464b457 100644 --- a/PySDM/dynamics/freezing.py +++ b/PySDM/dynamics/freezing.py @@ -1,5 +1,6 @@ """ -immersion freezing using either singular or time-dependent formulation +droplet freezing using either singular or time-dependent formulation for immersion freezing +and homogeneous freezing and thaw """ from PySDM.dynamics.impl import register_dynamic diff --git a/tests/unit_tests/dynamics/test_freezing.py b/tests/unit_tests/dynamics/test_freezing.py index a2e5f07a49..297186d0a0 100644 --- a/tests/unit_tests/dynamics/test_freezing.py +++ b/tests/unit_tests/dynamics/test_freezing.py @@ -14,7 +14,7 @@ EPSILON_RH = 1e-3 -class TestImmersionFreezing: +class TestDropletFreezing: @staticmethod @pytest.mark.parametrize( "record_freezing_temperature", @@ -155,7 +155,7 @@ def test_thaw(backend_class, freezing_type, thaw, epsilon): assert particulator.products["ice water content"].get() > 0 @staticmethod - def test_freeze_singular(backend_class): + def test_immersion_freezing_singular(backend_class): # arrange n_sd = 44 dt = 1 * si.s @@ -195,7 +195,7 @@ def test_freeze_singular(backend_class): @staticmethod @pytest.mark.parametrize("double_precision", (True, False)) # pylint: disable=too-many-locals - def test_freeze_time_dependent(backend_class, double_precision, plot=False): + def test_immersion_freezing_time_dependent(backend_class, double_precision, plot=False): if backend_class.__name__ == "Numba" and not double_precision: pytest.skip() @@ -305,7 +305,7 @@ def low(t): @staticmethod @pytest.mark.parametrize("double_precision", (True,False)) # pylint: disable=too-many-locals - def test_homogeneous_freeze_time_dependent(backend_class, double_precision, plot=False): + def test_homogeneous_freezing_time_dependent(backend_class, double_precision, plot=False): if backend_class.__name__ == "Numba" and not double_precision: pytest.skip() if backend_class.__name__ == "ThrustRTC": From a709851c1bd090de63a7d146754a0bd7aae140fd Mon Sep 17 00:00:00 2001 From: Tim Luettmer Date: Tue, 6 May 2025 15:51:45 +0200 Subject: [PATCH 25/57] Combined freezing test of homogeneous and immersion freezing + cleanup --- .../impl_numba/methods/freezing_methods.py | 2 - tests/unit_tests/dynamics/test_freezing.py | 158 ++++-------------- 2 files changed, 30 insertions(+), 130 deletions(-) diff --git a/PySDM/backends/impl_numba/methods/freezing_methods.py b/PySDM/backends/impl_numba/methods/freezing_methods.py index 1aa1b859f6..6de4cbeb9c 100644 --- a/PySDM/backends/impl_numba/methods/freezing_methods.py +++ b/PySDM/backends/impl_numba/methods/freezing_methods.py @@ -129,8 +129,6 @@ def freeze_time_dependent_homogeneous_body( # pylint: disable=unused-argument,t ) if rand[i] < prob: _freeze(attributes.signed_water_mass, i) - # if record_freezing_temperature: - # freezing_temperature[i] = temperature[cell_id] self.freeze_time_dependent_homogeneous_body = freeze_time_dependent_homogeneous_body diff --git a/tests/unit_tests/dynamics/test_freezing.py b/tests/unit_tests/dynamics/test_freezing.py index 297186d0a0..fe45963f5b 100644 --- a/tests/unit_tests/dynamics/test_freezing.py +++ b/tests/unit_tests/dynamics/test_freezing.py @@ -194,8 +194,9 @@ def test_immersion_freezing_singular(backend_class): @staticmethod @pytest.mark.parametrize("double_precision", (True, False)) + @pytest.mark.parametrize("freezing_type", ("het_time_dependent", "hom_time_dependent")) # pylint: disable=too-many-locals - def test_immersion_freezing_time_dependent(backend_class, double_precision, plot=False): + def test_freezing_time_dependent(backend_class, freezing_type, double_precision, plot=False): if backend_class.__name__ == "Numba" and not double_precision: pytest.skip() @@ -211,6 +212,7 @@ def test_immersion_freezing_time_dependent(backend_class, double_precision, plot ) rate = 1e-9 immersed_surface_area = 1 + droplet_volume = 1 number_of_real_droplets = 1024 total_time = ( @@ -219,7 +221,7 @@ def test_immersion_freezing_time_dependent(backend_class, double_precision, plot # dummy (but must-be-set) values initial_water_mass = ( - 44 # for sign flip (ice water has negative volumes), value does not matter + 1000 # for sign flip (ice water has negative volumes) ) d_v = 666 # products use conc., dividing there, multiplying here, value does not matter @@ -229,15 +231,32 @@ def hgh(t): def low(t): return np.exp(-1.25 * rate * (t + total_time / 4)) + immersion_freezing = True + homogeneous_freezing = False + if freezing_type is "het_time_dependent": + freezing_parameter = { + "heterogeneous_ice_nucleation_rate": "Constant", + "constants": {"J_HET": rate / immersed_surface_area}, + } + elif freezing_type is "hom_time_dependent": + freezing_parameter = { + "homogeneous_ice_nucleation_rate": "Constant", + "constants": {"J_HOM": rate / droplet_volume}, + } + immersion_freezing = False + homogeneous_freezing = True + if backend_class.__name__ == "ThrustRTC": + pytest.skip() + # Act output = {} formulae = Formulae( particle_shape_and_density="MixedPhaseSpheres", - heterogeneous_ice_nucleation_rate="Constant", - constants={"J_HET": rate / immersed_surface_area}, + **(freezing_parameter), seed=seed, ) + products = (IceWaterContent(name="qi"),) for case in cases: @@ -256,7 +275,11 @@ def low(t): ), environment=env, ) - builder.add_dynamic(Freezing(singular=False)) + builder.add_dynamic(Freezing(singular=False, + immersion_freezing=immersion_freezing, + homogeneous_freezing=homogeneous_freezing, + ) + ) attributes = { "multiplicity": np.full(n_sd, int(case["N"])), "immersed surface area": np.full(n_sd, immersed_surface_area), @@ -264,7 +287,8 @@ def low(t): } particulator = builder.build(attributes=attributes, products=products) particulator.environment["RH"] = 1.0001 - particulator.environment["a_w_ice"] = np.nan + particulator.environment["RH_ice"] = 1.5 + particulator.environment["a_w_ice"] = 0.6 particulator.environment["T"] = np.nan cell_id = 0 @@ -302,128 +326,6 @@ def low(t): np.testing.assert_array_less(low(arg), data) - @staticmethod - @pytest.mark.parametrize("double_precision", (True,False)) - # pylint: disable=too-many-locals - def test_homogeneous_freezing_time_dependent(backend_class, double_precision, plot=False): - if backend_class.__name__ == "Numba" and not double_precision: - pytest.skip() - if backend_class.__name__ == "ThrustRTC": - pytest.skip() - - - - # Arrange - seed = 44 - cases = ( - {"dt": 5e5, "N": 1}, - {"dt": 1e6, "N": 1}, - {"dt": 5e5, "N": 8}, - {"dt": 1e6, "N": 8}, - {"dt": 5e5, "N": 16}, - {"dt": 1e6, "N": 16}, - ) - rate = 1e-9 - - number_of_real_droplets = 1024 - total_time = ( - 0.25e9 # effectively interpreted here as seconds, i.e. cycle = 1 * si.s - ) - - # dummy (but must-be-set) values - initial_water_mass = ( - 1000 # for sign flip (ice water has negative volumes) - ) - d_v = 666 # products use conc., dividing there, multiplying here, value does not matter - - droplet_volume = initial_water_mass / 1000. - - def hgh(t): - return np.exp(-0.75 * rate * (t - total_time / 4)) - - def low(t): - return np.exp(-1.25 * rate * (t + total_time / 4)) - - - RHi = 1.5 - T = 230 - - # Act - output = {} - - formulae = Formulae( - particle_shape_and_density="MixedPhaseSpheres", - homogeneous_ice_nucleation_rate="Constant", - constants={"J_HOM": rate / droplet_volume}, - seed=seed, - ) - products = (IceWaterContent(name="qi"),) - - for case in cases: - n_sd = int(number_of_real_droplets // case["N"]) - assert n_sd == number_of_real_droplets / case["N"] - assert total_time // case["dt"] == total_time / case["dt"] - - key = f"{case['dt']}:{case['N']}" - output[key] = {"unfrozen_fraction": [], "dt": case["dt"], "N": case["N"]} - - env = Box(dt=case["dt"], dv=d_v) - builder = Builder( - n_sd=n_sd, - backend=backend_class( - formulae=formulae, double_precision=double_precision - ), - environment=env, - ) - builder.add_dynamic(Freezing(singular=False,homogeneous_freezing=True,immersion_freezing=False)) - attributes = { - "multiplicity": np.full(n_sd, int(case["N"])), - "signed water mass": np.full(n_sd, initial_water_mass), - } - particulator = builder.build(attributes=attributes, products=products) - pvs_ice = particulator.formulae.saturation_vapour_pressure.pvs_ice(T) - pvs_water = particulator.formulae.saturation_vapour_pressure.pvs_water(T) - particulator.environment["RH_ice"] = RHi - particulator.environment["a_w_ice"] = pvs_ice / pvs_water - particulator.environment["T"] = T - - cell_id = 0 - for i in range(int(total_time / case["dt"]) + 1): - particulator.run(0 if i == 0 else 1) - - ice_mass_per_volume = particulator.products["qi"].get()[cell_id] - ice_mass = ice_mass_per_volume * d_v - ice_number = ice_mass / initial_water_mass - unfrozen_fraction = 1 - ice_number / number_of_real_droplets - output[key]["unfrozen_fraction"].append(unfrozen_fraction) - - - # Plot - fit_x = np.linspace(0, total_time, num=100) - fit_y = np.exp(-rate * fit_x) - - for out in output.values(): - pyplot.step( - out["dt"] * np.arange(len(out["unfrozen_fraction"])), - out["unfrozen_fraction"], - label=f"dt={out['dt']:.2g} / N={out['N']}", - marker=".", - linewidth=1 + out["N"] // 8, - ) - - _plot_fit(fit_x, fit_y, low, hgh, total_time) - if plot: - pyplot.show() - - # Assert - for out in output.values(): - data = np.asarray(out["unfrozen_fraction"]) - arg = out["dt"] * np.arange(len(data)) - np.testing.assert_array_less(data, hgh(arg)) - np.testing.assert_array_less(low(arg), data) - - - def _plot_fit(fit_x, fit_y, low, hgh, total_time): pyplot.plot( fit_x, fit_y, color="black", linestyle="--", label="theory", linewidth=5 From 29b9bef0a2a3049a8fcd10a5b30d9457a21f99ab Mon Sep 17 00:00:00 2001 From: Tim Luettmer Date: Wed, 14 May 2025 16:16:38 +0200 Subject: [PATCH 26/57] reworked Kaercher_Lohmann_2002 example to Spichtinger_et_al_2023 example --- .../Kaercher_Lohmann_2002/plot.py | 521 ------------------ .../Kaercher_Lohmann_2002/reference.py | 31 -- .../Kaercher_Lohmann_2002/run.py | 161 ------ .../Kaercher_Lohmann_2002/settings.py | 95 ---- .../Kaercher_Lohmann_2002/simulation.py | 176 ------ .../Spichtinger_et_al_2023/__init__.py | 7 + .../data/reference_bulk.py | 6 + .../data/simulation_data.py | 19 + .../Spichtinger_et_al_2023/run.py | 82 +++ .../Spichtinger_et_al_2023/settings.py | 61 ++ .../Spichtinger_et_al_2023/simulation.py | 109 ++++ 11 files changed, 284 insertions(+), 984 deletions(-) delete mode 100644 examples/PySDM_examples/Kaercher_Lohmann_2002/plot.py delete mode 100644 examples/PySDM_examples/Kaercher_Lohmann_2002/reference.py delete mode 100644 examples/PySDM_examples/Kaercher_Lohmann_2002/run.py delete mode 100644 examples/PySDM_examples/Kaercher_Lohmann_2002/settings.py delete mode 100644 examples/PySDM_examples/Kaercher_Lohmann_2002/simulation.py create mode 100644 examples/PySDM_examples/Spichtinger_et_al_2023/__init__.py create mode 100644 examples/PySDM_examples/Spichtinger_et_al_2023/data/reference_bulk.py create mode 100644 examples/PySDM_examples/Spichtinger_et_al_2023/data/simulation_data.py create mode 100644 examples/PySDM_examples/Spichtinger_et_al_2023/run.py create mode 100644 examples/PySDM_examples/Spichtinger_et_al_2023/settings.py create mode 100644 examples/PySDM_examples/Spichtinger_et_al_2023/simulation.py diff --git a/examples/PySDM_examples/Kaercher_Lohmann_2002/plot.py b/examples/PySDM_examples/Kaercher_Lohmann_2002/plot.py deleted file mode 100644 index f410180e65..0000000000 --- a/examples/PySDM_examples/Kaercher_Lohmann_2002/plot.py +++ /dev/null @@ -1,521 +0,0 @@ -import matplotlib.pyplot as plt -from matplotlib import pyplot -from matplotlib.backends.backend_pdf import PdfPages -from matplotlib.pyplot import boxplot - -from reference import bulk_model_reference, critical_supersaturation_spich2023 - -import json -import numpy as np - -# general plot settings -ax_lab_fsize = 15 -tick_fsize = 15 -title_fsize = 15 - -kg_to_µg = 1.e9 -m_to_µm = 1.e6 - -def plot_size_distribution(r_wet, r_dry, N, setting, pp): - r_wet, r_dry = r_wet * m_to_µm, r_dry * m_to_µm - - title = f"N0: {setting.N_dv_solution_droplet * 1e-6:.2E} cm-3 \ - R0: {setting.r_mean_solution_droplet * m_to_µm:.2E} µm \ - $\sigma$: {setting.sigma_solution_droplet:.2f} \ - Nsd: {setting.n_sd:d} $\kappa$: {setting.kappa:.2f}" - - fig = plt.figure() - ax = fig.add_subplot(1, 1, 1) - - ax.scatter(r_dry, N, color="red", label="dry") - ax.scatter(r_wet, N, color="blue", label="wet") - ax.set_title(title) - ax.legend() - ax.set_xscale('log') - ax.set_xlim(5.e-3, 5.e0) - ax.set_yscale('log') - ax.set_xlabel(r"radius [µm]") - ax.set_ylabel("multiplicty") - - pp.savefig() - - -def plot_evolution( pp, output ): - ln = 2.5 - - extra_panel = True - - time = output["t"] - temperature = np.asarray(output["T"]) - z = output["z"] - - # rh = output["RH"] - rhi = np.asarray(output["RHi"]) / 100. - - # rhi_crit_old = critical_supersaturation(temperature) - rhi_crit = critical_supersaturation_spich2023(temperature) - - lwc = np.asarray(output["LWC"]) * kg_to_µg - iwc = abs(np.asarray(output["IWC"])) * kg_to_µg - - ns = output["ns"] - ni = output["ni"] - rs = output["rs"] - ri = output["ri"] - - frozen = output["frozen"] - r_wet_init = np.asarray(output["r_wet"]) * m_to_µm - multiplicities = output["multiplicity"] - multiplicities_unfrozen = np.ma.masked_array(multiplicities, mask=frozen) - multiplicities_frozen = np.ma.masked_array(multiplicities, mask=np.logical_not(frozen)) - - if extra_panel: - fig, axs = pyplot.subplots(3, 2, figsize=(10, 15), sharex=False) - else: - fig, axs = pyplot.subplots(2, 2, figsize=(10, 10), sharex=False) - - - x_limit = [0, np.amax(time)] - - # title = (f"w: {setting.w_updraft:.2f} m s-1 T0: {setting.initial_temperature:.2f} K " - # f"Nsd: {setting.n_sd:d} rate: ") + setting.rate - # fig.suptitle(title) - - axTz = axs[0, 0] - axTz.plot( - time, temperature, color="red", linestyle="-", label="T", linewidth=ln - ) - - twin = axTz.twinx() - twin.plot( - time, z, color="black", linestyle="-", label="dz", linewidth=ln - ) - twin.set_ylabel("vertical displacement [m]", fontsize=ax_lab_fsize) - twin.set_ylim(-5, np.amax(z) + 100) - axTz.grid(True) - axTz.set_ylim(190, 225) - axTz.legend(loc='upper right',fontsize=ax_lab_fsize) - axTz.set_ylabel("temperature [K]", fontsize=ax_lab_fsize) - twin.legend(loc='upper left',fontsize=ax_lab_fsize) - axTz.set_xlabel("time [s]", fontsize=ax_lab_fsize) - axTz.tick_params(labelsize=tick_fsize) - twin.tick_params(labelsize=tick_fsize) - axTz.set_title("(a) air parcel ascent", fontsize=title_fsize) - axTz.set_xlim(x_limit) - - axRH = axs[0, 1] - # axRH.plot( - # time, rh, color="blue", linestyle="-", label="water", linewidth=ln - # ) - axRH.plot( - time, rhi, color="red", linestyle="-", label="ice", linewidth=ln - ) - axRH.plot( - time, rhi_crit, color="black", linestyle="-", label="crit", linewidth=ln - ) - # axRH.plot( - # time, rhi_crit_old, color="black", linestyle="--", label="crit_old", linewidth=ln - # ) - axRH.grid(True) - axRH.legend(loc='lower right',fontsize=ax_lab_fsize) - axRH.set_xlabel("time [s]", fontsize=ax_lab_fsize) - axRH.set_xlim(x_limit) - axRH.set_ylabel("supersaturation ratio", fontsize=ax_lab_fsize) - axRH.set_ylim(1, 1.6) - axRH.tick_params(labelsize=tick_fsize) - axRH.set_title("(b) supersaturation", fontsize=title_fsize) - - axN = axs[1, 0] - - axN.plot( - time, ns, color="blue", linestyle="-", label="solution droplet", linewidth=ln) - axN.plot( - time, ni, color="red", linestyle="-", label="ice", linewidth=ln) - - axN.set_yscale('log') - axN.grid(True) - axN.legend(fontsize=ax_lab_fsize) - axN.set_ylim(1.e-3, 5.e4) - axN.set_xlabel("time [s]", fontsize=ax_lab_fsize) - axN.set_xlim(x_limit) - axN.set_ylabel(r"number concentration [$\mathrm{cm^{-3}}$]" - , fontsize=ax_lab_fsize) - axN.tick_params(labelsize=tick_fsize) - axN.set_title("(c) bulk number concentration", fontsize=title_fsize) - - axR = axs[1, 1] - - axR.plot( - time, rs, color="blue", linestyle="-", label="solution droplet", linewidth=ln) - axR.plot( - time, ri, color="red", linestyle="-", label="ice", linewidth=ln) - - axR.grid(True) - axR.legend(fontsize=ax_lab_fsize) - axR.set_yscale('log') - axR.set_ylim(1.e-2, 1.e2) - axR.set_xlabel("time [s]", fontsize=ax_lab_fsize) - axR.set_xlim(x_limit) - axR.set_ylabel(r"mean radius [µm]", fontsize=ax_lab_fsize) - axR.tick_params(labelsize=tick_fsize) - axR.set_title("(d) mean radius", fontsize=title_fsize) - - if extra_panel: - - axWC = axs[2, 0] - - axWC.plot( - time, lwc, color="blue", linestyle="-", label="water", linewidth=ln - ) - axWC.plot( - time, iwc, color="red", linestyle="-", label="ice", linewidth=ln - ) - axWC.set_title("(e) mass content", fontsize=title_fsize) - axWC.set_yscale('log') - axWC.legend(fontsize=ax_lab_fsize) - axWC.tick_params(labelsize=tick_fsize) - axWC.set_xlim(x_limit) - axWC.set_ylim(1.e0, 5.e4) - axWC.set_xlabel("time [s]", fontsize=ax_lab_fsize) - axWC.set_ylabel(r"mass content [$\mathrm{\mu g \, kg^{-1}}$]" - , fontsize=ax_lab_fsize) - axWC.grid(True) - - axFrz = axs[2, 1] - - axFrz.scatter(r_wet_init, multiplicities_unfrozen, color="black", alpha=0.5, label="unfrozen") - axFrz.scatter(r_wet_init, multiplicities_frozen, color="blue", label="frozen") - axFrz.set_title("(f) frozen super particles", fontsize=title_fsize) - axFrz.legend(fontsize=ax_lab_fsize) - axFrz.tick_params(labelsize=tick_fsize) - axFrz.set_xscale('log') - axFrz.set_xlim(5.e-3, 5.e-1) - axFrz.set_yscale('log') - axFrz.set_xlabel(r"initial radius [µm]", fontsize=ax_lab_fsize) - axFrz.set_ylabel("multiplicty", fontsize=ax_lab_fsize) - # axFrz.grid(True) - - fig.tight_layout() - pp.savefig() - - -def plot_ensemble(pp, outputs, T0, ens_member, only_ni = False, title = None): - - - - x_label = "number of super particles" - - ni_bulk_ref = bulk_model_reference(T0) * 1e-6 - - nx = len(outputs) - - x_array = np.array([50, 100, 500, 1000, 5000, 10000, 50000, 100000]) - x_string_array = [] - for x in x_array: - x_string_array.append(f"{x:.0e}") - - ni_arr = np.empty( ( ens_member,np.shape(x_array)[0]) ) - ri_arr = np.empty((ens_member, np.shape(x_array)[0])) - frozen_fraction_arr = np.empty((ens_member, np.shape(x_array)[0])) - min_frozen_r_arr = np.empty((ens_member, np.shape(x_array)[0])) - - jdx = 0 - idx_ref = 0 - for i in range(nx): - output = outputs[i] - n_sd = output["n_sd"] - idx = np.nonzero(x_array==n_sd)[0][0] - - if idx > idx_ref: - idx_ref = idx - jdx = 0 - - ni_arr[jdx,idx] = output["ni"][-1] - ri_arr[jdx,idx] = output["ri"][-1] - frozen = output["frozen"] - frozen_fraction_arr[jdx,idx] = np.sum(frozen) / np.size(frozen) - min_frozen_r_arr[jdx,idx] = m_to_µm * np.amin( - np.ma.masked_array(output["r_dry"], - mask=np.logical_not(frozen)) - ) - jdx += 1 - - if only_ni: - fig, ax = pyplot.subplots(1, 1, figsize=(5, 5)) - else: - fig, axs = pyplot.subplots(2, 2, figsize=(10, 10), sharex=False) - # fig.suptitle(title) - - if only_ni: - if title is not None: - ax.set_title(title, fontsize=title_fsize) - ax.boxplot(ni_arr, tick_labels=x_string_array) - ax.set_yscale('log') - ax.set_ylim(1.e-2, 1.e2) - ax.axhline(ni_bulk_ref, c="r") - ax.set_ylabel(r"ice number concentration [$\mathrm{cm^{-3}}$]" - , fontsize=ax_lab_fsize) - ax.set_xlabel(x_label, fontsize=ax_lab_fsize) - ax.tick_params(axis='y', labelsize=tick_fsize) - else: - axN = axs[0, 0] - axN.set_title("(a) nucleated number concentration", fontsize=title_fsize) - axN.boxplot(ni_arr,tick_labels=x_string_array) - axN.set_yscale('log') - axN.set_ylim(1.e-2, 1.e2) - axN.axhline(ni_bulk_ref, c="r") - axN.set_ylabel(r"ice number concentration [$\mathrm{cm^{-3}}$]" - , fontsize=ax_lab_fsize) - axN.set_xlabel(x_label, fontsize=ax_lab_fsize) - axN.tick_params(axis='y', labelsize=tick_fsize) - - axR = axs[0, 1] - axR.set_title("(b) mean radius", fontsize=title_fsize) - axR.boxplot(ri_arr,tick_labels=x_string_array) - axR.set_xlabel(x_label) - axR.set_yscale('log') - axR.set_ylim(5.e-2, 1.e2) - axR.set_ylabel(r"ice mean radius[µm]" - , fontsize=ax_lab_fsize) - axR.tick_params(axis='y', labelsize=tick_fsize) - - axF = axs[1, 0] - axF.set_title("(c) frozen fraction", fontsize=title_fsize) - axF.boxplot(frozen_fraction_arr,tick_labels=x_string_array) - axF.set_xlabel(x_label, fontsize=ax_lab_fsize) - axF.set_yscale('log') - axF.set_ylim(1.e-5, 1) - axF.set_ylabel(r"fraction of frozen super particles" - , fontsize=ax_lab_fsize) - axF.tick_params(axis='y', labelsize=tick_fsize) - - axM = axs[1, 1] - axM.set_title("(d) radius of smallest frozen droplet", fontsize=title_fsize) - axM.boxplot(min_frozen_r_arr,tick_labels=x_string_array) - axM.set_xlabel(x_label, fontsize=ax_lab_fsize) - axM.set_yscale('log') - axM.set_ylim(5.e-3, 5.e0) - axM.set_ylabel(r"minimum radius of frozen droplets [µm]" - , fontsize=ax_lab_fsize) - axM.tick_params(axis='y', labelsize=tick_fsize) - - fig.tight_layout() - pp.savefig() - - - -# plot super particle ensemble -def plot_ensemble_simulation(file_name, only_ni=False, title=None): - - plot_name = file_name.replace("json","pdf") - - with open(file_name, 'r') as f: - data = json.load(f) - outputs = data["outputs"] - number_of_ensemble_runs = data["number_of_ensemble_runs"] - T0 = data["initial_temperature"] - - pp = PdfPages(plot_name) - plot_ensemble(pp, outputs, T0, number_of_ensemble_runs, only_ni=only_ni, title = title) - pp.close() - -# filename= "ensemble_nsd_25_dsd_0_T0_220.json" -# plot_ensemble_simulation(filename) -filename= "ensemble_nsd_25_dsd_0_T0_220_lin.json" -plot_ensemble_simulation(filename, only_ni=True, title="linear sampling") -filename= "ensemble_nsd_25_dsd_0_T0_220_limit.json" -plot_ensemble_simulation(filename, only_ni=True, title="limited sampling") - - - - -# plot super particle ensemble -def plot_simulation_evolution(file_name): - - plot_name = file_name.replace("json","pdf") - - with open(file_name, 'r') as f: - data = json.load(f) - outputs = data["outputs"] - - pp = PdfPages(plot_name) - for output in outputs: - plot_evolution(pp, output) - pp.close() - -# filename="ensemble_1_dsd_1_T0_220_nsd_50000highoutput.json" -# plot_simulation_evolution(filename) - -def plot_size_distribution_discretisation(): - - target_n_sd = 50 - def get_r_and_m(file_name): - with open(file_name, 'r') as f: - data = json.load(f) - for output in data["outputs"]: - if output["n_sd"][0] == target_n_sd: - return( np.array(output["r_wet"][0]), - np.array(output["multiplicity"][0]) ) - - file_name = "ensemble_nsd_1_dsd_0_T0_220_nsd_50.json" - dsd1 = get_r_and_m(file_name) - - file_name = "ensemble_nsd_1_dsd_1_T0_220_nsd_50.json" - dsd2 = get_r_and_m(file_name) - - file_name = "ensemble_nsd_1_dsd_2_T0_220_nsd_50.json" - dsd3 = get_r_and_m(file_name) - - file_name = "ensemble_nsd_1_dsd_0_T0_220_nsd_50_lin.json" - dsd1_lin = get_r_and_m(file_name) - - # file_name = "ensemble_1_dsd_0_T0_220_nsd_50_RH16.json" - # dsd1_16 = get_r_and_m(file_name) - # file_name = "ensemble_1_dsd_0_T0_220_nsd_50_RH10.json" - # dsd1_10 = get_r_and_m(file_name) - - file_name = "ensemble_1_dsd_0_T0_190_nsd_50.json" - dsd_1_190 = get_r_and_m(file_name) - - file_name = "ensemble_1_dsd_0_T0_200_nsd_50.json" - dsd_1_200 = get_r_and_m(file_name) - - file_name = "ensemble_1_dsd_0_T0_220_nsd_50.json" - dsd_1_220 = get_r_and_m(file_name) - - file_name = "ensemble_nsd_1_dsd_0_T0_220_nsd_50_limit.json" - dsd1_lim = get_r_and_m(file_name) - - - - - fig, axs = pyplot.subplots(2, 2, figsize=(10, 10), sharex=False) - # fig.suptitle("Solution droplet size distributions") - - ax = axs[0, 0] - - xaxis_label = r"droplet radius r [µm]" - - ax.scatter(dsd1[0] * m_to_µm, dsd1[1], color="black", label="DSD1") - ax.scatter(dsd2[0] * m_to_µm, dsd2[1], color="red", label="DSD2") - ax.scatter(dsd3[0] * m_to_µm, dsd3[1], color="blue", label="DSD3") - ax.tick_params(labelsize=tick_fsize) - ax.set_xscale('log') - ax.set_xlim(5.e-3, 5.e0) - ax.set_ylim(5e7, 2e12) - ax.set_yscale('log') - ax.set_xlabel(xaxis_label,fontsize=ax_lab_fsize) - ax.set_ylabel("multiplicty",fontsize=ax_lab_fsize) - ax.set_title("(a) logarithmic sampling",fontsize=title_fsize) - ax.legend(fontsize=ax_lab_fsize) - - ax = axs[0, 1] - ax.scatter(dsd1_lim[0] * m_to_µm, dsd1_lim[1], color="black", label="DSD1") - ax.tick_params(labelsize=tick_fsize) - ax.set_xscale('log') - ax.set_xlim(5.e-3, 5.e0) - ax.set_ylim(1e7, 2e12) - ax.set_yscale('log') - ax.set_xlabel(xaxis_label, fontsize=ax_lab_fsize) - ax.set_ylabel("multiplicty", fontsize=ax_lab_fsize) - ax.set_title("(b) limited logarithmic sampling", fontsize=title_fsize) - ax.legend(fontsize=ax_lab_fsize) - - - ax = axs[1, 0] - ax.scatter(dsd1_lin[0] * m_to_µm, dsd1_lin[1], color="black", label="DSD1") - ax.tick_params(labelsize=tick_fsize) - ax.set_xscale('log') - ax.set_xlim(5.e-3, 5.e0) - ax.set_ylim(1e7, 2e12) - ax.set_yscale('log') - ax.set_xlabel(xaxis_label,fontsize=ax_lab_fsize) - ax.set_ylabel("multiplicty",fontsize=ax_lab_fsize) - ax.set_title("(c) linear sampling",fontsize=title_fsize) - ax.legend(fontsize=ax_lab_fsize) - - ax = axs[1, 1] - - # ax.scatter(dsd1_10[0] * m_to_µm, dsd1_10[1], color="black", label="T=220K") - # ax.scatter(dsd1_16[0] * m_to_µm, dsd1_16[1], color="red", label="T=200K") - ax.scatter(dsd_1_220[0] * m_to_µm, dsd_1_220[1], color="black", label="T=220K") - ax.scatter(dsd_1_200[0] * m_to_µm, dsd_1_200[1], color="red", label="T=200K") - ax.scatter(dsd_1_190[0] * m_to_µm, dsd_1_190[1], color="blue", label="T=190K") - ax.tick_params(labelsize=tick_fsize) - ax.set_xscale('log') - ax.set_xlim(5.e-3, 5.e0) - ax.set_ylim(5e7, 2e12) - ax.set_yscale('log') - ax.set_xlabel(xaxis_label, fontsize=ax_lab_fsize) - ax.set_ylabel("multiplicty", fontsize=ax_lab_fsize) - ax.set_title("(d) temperature dependence", fontsize=title_fsize) - ax.legend(fontsize=ax_lab_fsize) - - fig.tight_layout() - plt.savefig("size_distributions.pdf") - - - -def plot_ni_as_function_of_w(): - - dsd_list = np.array([0]) - initial_temperatures = np.array([196., 216., 236.]) - updrafts = np.array([0.05, 0.1, 0.5, 1., 5., 10.]) - - dim_size = ( np.shape(dsd_list)[0], np.shape(initial_temperatures)[0], np.shape(updrafts)[0] ) - ni_sdm = np.zeros(dim_size) - - - - for i in range(dim_size[0]): - for j in range(dim_size[1]): - T0 = initial_temperatures[j] - for k in range(dim_size[2]): - w = updrafts[k] - filename = "ensemble_1_dsd_"+str(i)+f"_T0_{T0:.0f}"+f"_W_{w:.2f}"+"_nsd_10.json" - # print(filename) - - with open(filename, 'r') as f: - data = json.load(f) - - T0_sim = data['initial_temperature'] - w_sim = data['w_updraft'] - if T0 == T0_sim and w == w_sim: - output = data['outputs'][0] - ni_last = output["ni"][-1] - print( w, T0, ni_last ) - ni_sdm[i,j,k] = ni_last - - print(ni_sdm) - - fig, axs = pyplot.subplots(1, 3, figsize=(15, 5), sharex=False) - - for i in range(1): - ax = axs[i] - - if i == 0: - ax.set_title("(a) hom. nucleation for DSD 1",fontsize=title_fsize) - if i == 1: - ax.set_title("(b) hom. nucleation for DSD 2", fontsize=title_fsize) - if i == 2: - ax.set_title("(c) hom. nucleation for DSD 3", fontsize=title_fsize) - - for j in range(dim_size[1]): - ax.scatter(updrafts, ni_sdm[i,j,:],label=f"T0={initial_temperatures[j]:.0f}K") - - ax.tick_params(labelsize=tick_fsize) - ax.set_xscale('log') - ax.set_xlabel(r"vertical updraft [$\mathrm{m \, s^{-1}}$]" - , fontsize=ax_lab_fsize) - ax.set_yscale('log') - ax.set_ylim(1.e-1, 8.e3) - ax.set_ylabel(r"ice number concentration [$\mathrm{cm^{-3}}$]" - , fontsize=ax_lab_fsize) - ax.legend(fontsize=ax_lab_fsize,loc="lower right") - - plt.tight_layout - plt.savefig("w_ni_plot.pdf") -# plot_size_distribution_discretisation() - -# print( critical_supersaturation(220.) ) -# plot_ni_as_function_of_w() \ No newline at end of file diff --git a/examples/PySDM_examples/Kaercher_Lohmann_2002/reference.py b/examples/PySDM_examples/Kaercher_Lohmann_2002/reference.py deleted file mode 100644 index 968d01590b..0000000000 --- a/examples/PySDM_examples/Kaercher_Lohmann_2002/reference.py +++ /dev/null @@ -1,31 +0,0 @@ -import numpy as np -from PySDM.physics.constants import si - -def critical_supersaturation(temperature): - return 2.349 - temperature / 259. - -# crit thres -def critical_supersaturation_spich2023 (t): - s20=1.67469 - s21=0.00228125 - s22=-1.36989e-05 - return s20+s21*t+s22*t*t - - -def bulk_model_reference(initial_temperature, updraft=0.1): - n_hom_ice = None - - if initial_temperature == 220.: - if updraft == 0.1: - n_hom_ice = 148121.413358197 - if updraft == 1.: - n_hom_ice = 7268664.77542974 - - if initial_temperature == 216.: - if updraft == 1.: - n_hom_ice = 10475282.894692907 - - - - return( n_hom_ice / si.metre ** 3 ) - diff --git a/examples/PySDM_examples/Kaercher_Lohmann_2002/run.py b/examples/PySDM_examples/Kaercher_Lohmann_2002/run.py deleted file mode 100644 index 87623abb11..0000000000 --- a/examples/PySDM_examples/Kaercher_Lohmann_2002/run.py +++ /dev/null @@ -1,161 +0,0 @@ - -from matplotlib.backends.backend_pdf import PdfPages -import numpy as np -import pickle -import json - -from PySDM.physics.constants import si - -from settings import settings as simulation_settings -from simulation import Simulation -# from plot import plot_size_distribution, plot_evolution, plot_ensemble - -general_settings = {"n_sd": 10, "T0": 216 * si.kelvin, - "w_updraft": 10 * si.centimetre / si.second} -distributions = ({"N_dv_solution_droplet": 2500 / si.centimetre ** 3, - "r_mean_solution_droplet": 0.055 * si.micrometre, - "sigma_solution_droplet": 1.6}, - {"N_dv_solution_droplet": 8600 / si.centimetre**3, - "r_mean_solution_droplet": 0.0275 * si.micrometre, - "sigma_solution_droplet": 1.3}, - {"N_dv_solution_droplet": 2000 / si.centimetre**3, - "r_mean_solution_droplet": 0.11 * si.micrometre, - "sigma_solution_droplet": 2.}, - ) - -# pp = PdfPages("hom_freezing_for_size_distributions.pdf") -# -# for distribution in distributions: -# setting = settings(**{**general_settings, **distribution}) -# model = Simulation(setting) -# output = model.run() -# -# plot_size_distribution(model.r_wet, model.r_dry, model.multiplicities, setting, pp) -# plot(output, setting, model, pp) - - - -# calculate super particle ensemble -def ensemble_simulation(number_of_ensemble_runs=1, - dsd=0, - T0=216.* si.kelvin, - w_updraft=None, - linear_sampling=False, - nsd_single = None, - lower_limit = None, - add_label = "", - RHi_0 = None, - ): - - file_name = ("ensemble_"+str(number_of_ensemble_runs)+"_dsd_"+str(dsd) - + f"_T0_{T0:.0f}" + f"_W_{w_updraft:.2f}") - - - if nsd_single is None: - number_of_super_particles = (50, 100, 500, 1000, 5000, 10000, 50000, 100000) - else: - number_of_super_particles = (nsd_single,) - file_name += "_nsd_"+str(nsd_single) - - if linear_sampling: - file_name += "_lin" - - if lower_limit is not None: - file_name += "_limit" - file_name += add_label - - - outputs = [] - - aerosol_distribution = distributions[dsd] - setting = {**general_settings, - **aerosol_distribution, - "linear_sampling": linear_sampling, - "lower_limit": lower_limit} - - if T0 is not None: - setting["T0"] = T0 - if RHi_0 is not None: - setting["RHi_0"] = RHi_0 - if w_updraft is not None: - setting["w_updraft"] = w_updraft - - for nsd in number_of_super_particles: - setting["n_sd"] = nsd - - print(setting) - - for _ in range(number_of_ensemble_runs): - simulation_setting = simulation_settings(**setting) - model = Simulation(simulation_setting) - output = model.run() - outputs.append(output) - del model, simulation_setting - - data_file = { "outputs":outputs, - "number_of_ensemble_runs": number_of_ensemble_runs, - "initial_temperature": setting["T0"], - "aerosol_distribution": aerosol_distribution, - "w_updraft": setting["w_updraft"], - } - print("Writing "+file_name+".json") - with open(file_name+".json", 'w') as file: - json.dump(data_file, file) - - -# ensemble_simulation(1) -# setting = {**general_settings, **distributions[0] } -# simulation_setting = simulation_settings(**setting) -# -# setting = {**general_settings, **distributions[0], "linear_sampling": True } -# simulation_setting = simulation_settings(**setting) - - -# for DSD plots -# ensemble_simulation(nsd_single = 50000, dsd=0, add_label="koop_corr", w_updraft=1., number_of_ensemble_runs=25) -# ensemble_simulation(nsd_single = 50, dsd=0, T0=220* si.kelvin, -# RHi_0=1.6, -# add_label="_RH16") -# ensemble_simulation(nsd_single = 50, dsd=0, T0=220* si.kelvin, -# RHi_0=1.0, -# add_label="_RH10") -# ensemble_simulation(nsd_single = 50, dsd=0, T0=220* si.kelvin, -# RHi_0=1.4, -# add_label="_RH14") -# ensemble_simulation(nsd_single = 50, dsd=0, T0=220* si.kelvin, -# RHi_0=1.4,) -# ensemble_simulation(nsd_single = 50, dsd=0, T0=200* si.kelvin, -# RHi_0=1.4,) -# ensemble_simulation(nsd_single = 50, dsd=0, T0=190* si.kelvin, -# RHi_0=1.4,) -# ensemble_simulation(nsd_single = 50, dsd=1) -# ensemble_simulation(nsd_single = 50, dsd=2) -# ensemble_simulation(nsd_single = 50, dsd=0, lower_limit=5.5e-8) -# ensemble_simulation(nsd_single = 50, dsd=0, linear_sampling=True) - -# ensemble_simulation(nsd_single = 50000, dsd=1, -# RHi_0=1.0, w_updraft=1.*si.meter / si.second, -# add_label="highoutput") - - -# ensemble_simulation(number_of_ensemble_runs=25, w_updraft=1.) -# ensemble_simulation( linear_sampling=True, number_of_ensemble_runs=25, w_updraft=1.) -# -# lower_limit_bound = ( 5.5e-8, ) -# for lower_limit in lower_limit_bound: -# ensemble_simulation( lower_limit=lower_limit, number_of_ensemble_runs=25, w_updraft=1.) - -initial_temperatures = [196.,216.,236.] -updrafts = [0.05, 0.1, 0.5, 1., 5., 10.] -number_of_ensemble_runs=1 -dsd=0 - -for T in reversed(initial_temperatures): - for w in reversed(updrafts): - ensemble_simulation(number_of_ensemble_runs=number_of_ensemble_runs, - w_updraft=w, - T0=T, - dsd=dsd, - linear_sampling=False, - nsd_single = 10, - ) \ No newline at end of file diff --git a/examples/PySDM_examples/Kaercher_Lohmann_2002/settings.py b/examples/PySDM_examples/Kaercher_Lohmann_2002/settings.py deleted file mode 100644 index 25e5818143..0000000000 --- a/examples/PySDM_examples/Kaercher_Lohmann_2002/settings.py +++ /dev/null @@ -1,95 +0,0 @@ -import time - -import numpy as np -from pystrict import strict - -from PySDM import Formulae -from PySDM.physics.constants import si -from PySDM.initialisation.spectra import Lognormal -from PySDM.initialisation.sampling import spectral_sampling - -@strict -class settings: - def __init__( - self, - *, - n_sd: int, - w_updraft: float, - T0: float, - RHi_0: float=1.4, - N_dv_solution_droplet: float, - r_mean_solution_droplet: float, - sigma_solution_droplet: float, - kappa: float=0.64, - rate: str="Koop_Correction", - dt: float=0.1, - linear_sampling: bool=False, - lower_limit: float=None, - ): - - self.n_sd = n_sd - self.w_updraft = w_updraft - self.r_mean_solution_droplet = r_mean_solution_droplet - self.N_dv_solution_droplet = N_dv_solution_droplet - self.sigma_solution_droplet = sigma_solution_droplet - self.rate = rate - - self.mass_of_dry_air = 1000 * si.kilogram - self.initial_pressure = 200 * si.hectopascals - self.initial_ice_supersaturation = RHi_0 - self.kappa = kappa - self.initial_temperature = T0 - print( "RHi0", self.initial_ice_supersaturation ) - print("T0", self.initial_temperature) - print( "w_updraft", self.w_updraft ) - self.formulae = Formulae( - particle_shape_and_density="MixedPhaseSpheres", - homogeneous_ice_nucleation_rate=rate, - constants={"J_HOM": 1.e15}, - seed=time.time_ns() - ) - const = self.formulae.constants - pvs_i = self.formulae.saturation_vapour_pressure.pvs_ice(self.initial_temperature) - self.initial_water_vapour_mixing_ratio = const.eps / ( - self.initial_pressure / self.initial_ice_supersaturation / pvs_i - 1 - ) - dry_air_density = (self.formulae.trivia.p_d(self.initial_pressure, self.initial_water_vapour_mixing_ratio ) - / self.initial_temperature - / const.Rd ) - - spectrum = Lognormal(norm_factor=N_dv_solution_droplet / dry_air_density, - m_mode=r_mean_solution_droplet, - s_geom=sigma_solution_droplet) - - - - upper_limit = 4e-6 - - if linear_sampling: - self.r_dry, self.specific_concentration = ( - spectral_sampling.Linear(spectrum).sample(n_sd)) - else: - if lower_limit is None: - self.r_dry, self.specific_concentration = ( - spectral_sampling.Logarithmic(spectrum, - ).sample(n_sd)) - else: - self.r_dry, self.specific_concentration = ( - spectral_sampling.Logarithmic(spectrum, - size_range=(lower_limit,upper_limit), - error_threshold=0.95, - ).sample(n_sd)) - test_for_zero_n = self.specific_concentration == 0. - if( any(test_for_zero_n) ): - last_non_zero = np.nonzero(self.specific_concentration)[0][-1] - upper_limit = self.specific_concentration[last_non_zero] - self.r_dry, self.specific_concentration = ( - spectral_sampling.Logarithmic(spectrum, - size_range=(lower_limit, upper_limit), - error_threshold=0.95, - ).sample(n_sd)) - - self.t_duration = 7200 #3600 * 1.5 # total duration of simulation - self.dt = dt - self.n_output = int(self.t_duration / 100) #100 # number of output steps - diff --git a/examples/PySDM_examples/Kaercher_Lohmann_2002/simulation.py b/examples/PySDM_examples/Kaercher_Lohmann_2002/simulation.py deleted file mode 100644 index 512dbe4c8a..0000000000 --- a/examples/PySDM_examples/Kaercher_Lohmann_2002/simulation.py +++ /dev/null @@ -1,176 +0,0 @@ -import numpy as np - -import PySDM.products as PySDM_products -from PySDM.backends import CPU -from PySDM.builder import Builder -from PySDM.dynamics import AmbientThermodynamics, Condensation, Freezing, VapourDepositionOnIce -from PySDM.environments import Parcel -from PySDM.physics import constants as const -from PySDM.initialisation import discretise_multiplicities, equilibrate_wet_radii - - -class Simulation: - def __init__(self, settings, backend=CPU): - # t_half = settings.z_half / settings.w_avg - - # dt_output = (2 * t_half) / settings.n_output - # self.n_substeps = 1 - # while dt_output / self.n_substeps >= settings.dt_max: # TODO #334 dt_max - # self.n_substeps += 1 - - dt = settings.dt - - formulae = settings.formulae - - env = Parcel( - mixed_phase=True, - dt=dt, - mass_of_dry_air=settings.mass_of_dry_air, - p0=settings.initial_pressure, - initial_water_vapour_mixing_ratio=settings.initial_water_vapour_mixing_ratio, - T0=settings.initial_temperature, - w=settings.w_updraft, - ) - - builder = Builder( - backend=backend( - formulae=settings.formulae, - **( - {"override_jit_flags": {"parallel": False}} - if backend == CPU - else {} - ) - ), - n_sd=settings.n_sd, - environment=env, - ) - - builder.add_dynamic(AmbientThermodynamics()) - builder.add_dynamic(Condensation()) - builder.add_dynamic(VapourDepositionOnIce()) - builder.add_dynamic(Freezing(singular=False, homogeneous_freezing=True, immersion_freezing=False)) - - self.n_sd = settings.n_sd - self.multiplicities = discretise_multiplicities(settings.specific_concentration * env.mass_of_dry_air) - self.r_dry = settings.r_dry - v_dry = settings.formulae.trivia.volume(radius=self.r_dry) - kappa = settings.kappa - - self.r_wet = equilibrate_wet_radii(r_dry=self.r_dry, environment=builder.particulator.environment, - kappa_times_dry_volume=kappa * v_dry) - - attributes = { - "multiplicity": self.multiplicities, - 'dry volume': v_dry, - 'kappa times dry volume': kappa * v_dry, - # 'volume': formulae.trivia.volume(radius=self.r_wet), - "signed water mass": formulae.particle_shape_and_density.radius_to_mass(self.r_wet), - } - - products = [ - PySDM_products.ParcelDisplacement(name="z"), - PySDM_products.Time(name="t"), - PySDM_products.AmbientRelativeHumidity(name="RH", unit="%"), - PySDM_products.AmbientRelativeHumidity(name="RH_ice", unit="%"), - PySDM_products.AmbientTemperature(name="T"), - PySDM_products.AmbientPressure(name="p", unit='hPa'), - PySDM_products.WaterMixingRatio(name="water", radius_range=(0, np.inf)), - PySDM_products.WaterMixingRatio(name="ice", radius_range=(-np.inf, 0)), - PySDM_products.WaterMixingRatio(name="total", radius_range=(-np.inf, np.inf)), - PySDM_products.AmbientWaterVapourMixingRatio( - name="vapour", var="water_vapour_mixing_ratio" - ), - PySDM_products.ParticleConcentration( - name='n_s', unit='1/cm**3', - radius_range=(0, np.inf)), - PySDM_products.ParticleConcentration( - name='n_i', unit='1/cm**3', - radius_range=(-np.inf, 0)), - PySDM_products.MeanRadius( - name='r_s', unit='µm', - radius_range=(0, np.inf)), - PySDM_products.MeanRadius( - name='r_i', unit='µm', - radius_range=(-np.inf, 0)), - ] - - self.particulator = builder.build(attributes, products) - - self.n_output = settings.n_output - self.n_substeps = int(settings.t_duration / dt / self.n_output) - - - def save(self, output): - cell_id = 0 - - output["z"].append(self.particulator.products["z"].get()[cell_id]) - output["t"].append(self.particulator.products["t"].get()) - output["RH"].append(self.particulator.products["RH"].get()[cell_id]) - output["RHi"].append(self.particulator.products["RH_ice"].get()[cell_id]) - output["T"].append(self.particulator.products["T"].get()[cell_id]) - output["P"].append(self.particulator.products["p"].get()[cell_id]) - output["LWC"].append(self.particulator.products["water"].get()[cell_id]) - output["IWC"].append(self.particulator.products["ice"].get()[cell_id]) - # output["TWC"].append(self.particulator.products["total"].get()[cell_id]) - output["qv"].append(self.particulator.products["vapour"].get()[cell_id]) - output["ns"].append(self.particulator.products["n_s"].get()[cell_id]) - output["ni"].append(self.particulator.products["n_i"].get()[cell_id]) - output["rs"].append(self.particulator.products["r_s"].get()[cell_id]) - output["ri"].append(self.particulator.products["r_i"].get()[cell_id]) - output["water_mass"].append(self.particulator.attributes["signed water mass"].data.tolist()) - def run(self): - output = { - "t": [], - "z": [], - "RH": [], - "RHi": [], - "T": [], - "P": [], - "LWC": [], - "IWC": [], - # "TWC": [], - "qv": [], - "ns": [], - "ni": [], - "rs": [], - "ri": [], - "frozen":[], - "multiplicity": [], - "r_dry":[], - "r_wet": [], - "water_mass":[] - } - - self.save(output) - output["n_sd"] = [ self.n_sd ] - output["r_dry"].append( self.r_dry.tolist() ) - output["r_wet"].append(self.r_wet.tolist()) - - RHi_old = self.particulator.products["RH_ice"].get()[0].copy() - for _ in range(self.n_output): - # print(self.particulator.__dict__) - # print(self.particulator.attributes.__dict__) - # print(self.particulator.attributes._ParticleAttributes__attributes['signed water mass']) - self.particulator.run(self.n_substeps) - # print(self.particulator.products["t"].get()) - # print(self.particulator.products["p"].get()) - # - self.save(output) - - - RHi = self.particulator.products["RH_ice"].get()[0].copy() - dRHi = (RHi_old - RHi) / RHi_old - if (dRHi > 0. and RHi < 130.): - print("break") - break - else: - RHi_old = RHi - - frozen = np.where(self.particulator.attributes["signed water mass"].data < 0., 1, 0) - output["frozen"].append(frozen.tolist()) - output["multiplicity"].append( - self.particulator.attributes["multiplicity"].data.tolist() - ) - print("Done. Last ni:", output["ni"][-1] ) - - return output diff --git a/examples/PySDM_examples/Spichtinger_et_al_2023/__init__.py b/examples/PySDM_examples/Spichtinger_et_al_2023/__init__.py new file mode 100644 index 0000000000..6ac40f57df --- /dev/null +++ b/examples/PySDM_examples/Spichtinger_et_al_2023/__init__.py @@ -0,0 +1,7 @@ +# pylint: disable=invalid-name +""" +homogeneous nucleation event example based on Fig. B1. in +[Spichtinger et al. 2023](https://doi.org/10.5194/acp-23-2035-2023) +""" +from .simulation import Simulation +from .settings import Settings \ No newline at end of file diff --git a/examples/PySDM_examples/Spichtinger_et_al_2023/data/reference_bulk.py b/examples/PySDM_examples/Spichtinger_et_al_2023/data/reference_bulk.py new file mode 100644 index 0000000000..6bc855f34a --- /dev/null +++ b/examples/PySDM_examples/Spichtinger_et_al_2023/data/reference_bulk.py @@ -0,0 +1,6 @@ +""" +reference results for bulk scheme in Fig B1. in +[Spichtinger et al. 2023](https://doi.org/10.5194/acp-23-2035-2023) +""" + + diff --git a/examples/PySDM_examples/Spichtinger_et_al_2023/data/simulation_data.py b/examples/PySDM_examples/Spichtinger_et_al_2023/data/simulation_data.py new file mode 100644 index 0000000000..e140c6ead1 --- /dev/null +++ b/examples/PySDM_examples/Spichtinger_et_al_2023/data/simulation_data.py @@ -0,0 +1,19 @@ +import numpy as np + +def saved_simulation_ensemble_mean(): + + ni_ens_mean = np.array([ + [0.00000000e+00, 0.00000000e+00, 0.00000000e+00], + [0.00000000e+00, 0.00000000e+00, 0.00000000e+00], + [1.62069821e+03, 0.00000000e+00, 0.00000000e+00], + [5.25377025e+06, 9.75512904e+05, 4.51431097e+05], + [3.67137290e+07, 5.45240337e+06, 1.53884530e+06], + [7.96514420e+07, 1.13878118e+07, 3.26386880e+06], + [2.19385480e+08, 3.62240242e+07, 9.00657591e+06], + [1.00631095e+09, 2.34443408e+08, 4.90577208e+07], + [1.73457062e+09, 5.20774276e+08, 1.16040316e+08] + ]) + T = np.array([196.0, 216.0, 236.0]) + w = np.array([0.01, 0.03, 0.05, 0.1, 0.3, 0.5, 1.0, 3.0, 5.0]) + + return T, w, ni_ens_mean \ No newline at end of file diff --git a/examples/PySDM_examples/Spichtinger_et_al_2023/run.py b/examples/PySDM_examples/Spichtinger_et_al_2023/run.py new file mode 100644 index 0000000000..48c89a2681 --- /dev/null +++ b/examples/PySDM_examples/Spichtinger_et_al_2023/run.py @@ -0,0 +1,82 @@ +import json +from examples.PySDM_examples.Spichtinger_et_al_2023 import Simulation, Settings +from examples.PySDM_examples.Spichtinger_et_al_2023.data import simulation_data +import numpy as np +from matplotlib import pyplot + + +calculate_data = False +save_to_file = False +plot = True +read_from_json = False + + +if calculate_data: + + general_settings = {"n_sd": 50000, "dt": 0.1} + + initial_temperatures = np.array([196., 216., 236.]) + updrafts = np.array([0.01, 0.03, 0.05, 0.1, 0.3, 0.5, 1., 3., 5.]) + number_of_ensemble_runs = 5 + seeds = [124670285330, 439785398735, 9782539783258, 12874192127481, 12741731272] + + dim_updrafts = len(updrafts) + dim_initial_temperatures = len(initial_temperatures) + + number_concentration_ice = np.zeros([dim_updrafts, dim_initial_temperatures, number_of_ensemble_runs]) + + for i in range(dim_updrafts): + for j in range(dim_initial_temperatures): + for k in range(number_of_ensemble_runs): + setting_dictionary = {**general_settings, } + setting_dictionary["w_updraft"] = updrafts[i] + setting_dictionary["T0"] = initial_temperatures[j] + setting_dictionary["seed"] = seeds[k] + setting = Settings(**setting_dictionary) + model = Simulation(setting) + number_concentration_ice[i,j,k] = model.run() + + if save_to_file: + file_name = "data/ni_w_T_ens_"+str(number_of_ensemble_runs)+".json" + data_file = {"ni": number_concentration_ice.tolist(), + "T": initial_temperatures.tolist(), + "w": updrafts.tolist()} + with open(file_name, 'w') as file: + json.dump(data_file, file) + +if plot: + if calculate_data: + T = initial_temperatures + w = updrafts + ni_ens_mean = np.mean(number_concentration_ice, axis=2) + else: + if read_from_json: + file_name = "data/ni_w_T_ens_5.json" + with open(file_name, 'r') as f: + data = json.load(f) + + ni = data["ni"] + T = data["T"] + w = data["w"] + ni_ens_mean = np.mean(ni, axis=2) + else: + T, w, ni_ens_mean = simulation_data.saved_simulation_ensemble_mean() + + + # plot + fig, ax = pyplot.subplots(1, 1, figsize=(5, 5)) + + for j in range(len(T)): + ax.scatter(w, ni_ens_mean[:, j], label=f"T0={T[j]:.0f}K") + + ax.set_xscale('log') + ax.set_xlim(0.08, 10.) + ax.set_xlabel(r"vertical updraft [$\mathrm{m \, s^{-1}}$]") + + ax.set_yscale('log') + ax.set_ylim(1.e2, 1.e10) + ax.set_ylabel(r"ice number concentration [$\mathrm{cm^{-3}}$]") + + ax.legend(loc="lower right") + + pyplot.show() diff --git a/examples/PySDM_examples/Spichtinger_et_al_2023/settings.py b/examples/PySDM_examples/Spichtinger_et_al_2023/settings.py new file mode 100644 index 0000000000..7db5f096cd --- /dev/null +++ b/examples/PySDM_examples/Spichtinger_et_al_2023/settings.py @@ -0,0 +1,61 @@ +from pystrict import strict + +from PySDM import Formulae +from PySDM.physics.constants import si +from PySDM.initialisation.spectra import Lognormal +from PySDM.initialisation.sampling import spectral_sampling + +@strict +class Settings: + def __init__( + self, + *, + n_sd: int, + w_updraft: float, + T0: float, + seed: int, + dt: float, + ): + + print( f"{w_updraft:},{T0:},{seed:},{dt:}" ) + + self.n_sd = n_sd + self.w_updraft = w_updraft + + self.N_dv_solution_droplet = 2500 / si.centimetre ** 3 + self.r_mean_solution_droplet = 0.055 * si.micrometre + self.sigma_solution_droplet = 1.6 + + self.mass_of_dry_air = 1000 * si.kilogram + self.initial_pressure = 200 * si.hectopascals + self.initial_ice_supersaturation = 1.0 + self.kappa = 0.64 + self.initial_temperature = T0 + + self.formulae = Formulae( + particle_shape_and_density="MixedPhaseSpheres", + homogeneous_ice_nucleation_rate="Koop_Correction", + seed=seed + ) + const = self.formulae.constants + pvs_i = self.formulae.saturation_vapour_pressure.pvs_ice(self.initial_temperature) + self.initial_water_vapour_mixing_ratio = const.eps / ( + self.initial_pressure / self.initial_ice_supersaturation / pvs_i - 1 + ) + dry_air_density = (self.formulae.trivia.p_d(self.initial_pressure, self.initial_water_vapour_mixing_ratio ) + / self.initial_temperature + / const.Rd ) + + spectrum = Lognormal(norm_factor=self.N_dv_solution_droplet / dry_air_density, + m_mode=self.r_mean_solution_droplet, + s_geom=self.sigma_solution_droplet) + + self.r_dry, self.specific_concentration = ( + spectral_sampling.Linear(spectrum).sample(n_sd) + ) + + + self.t_duration = 7200 + self.dt = dt + self.n_output = int(self.t_duration / 100) + diff --git a/examples/PySDM_examples/Spichtinger_et_al_2023/simulation.py b/examples/PySDM_examples/Spichtinger_et_al_2023/simulation.py new file mode 100644 index 0000000000..a6ac960e9d --- /dev/null +++ b/examples/PySDM_examples/Spichtinger_et_al_2023/simulation.py @@ -0,0 +1,109 @@ +import numpy as np + +from PySDM_examples.utils import BasicSimulation + +import PySDM.products as PySDM_products +from PySDM.backends import CPU +from PySDM.builder import Builder +from PySDM.dynamics import AmbientThermodynamics, Condensation, Freezing, VapourDepositionOnIce +from PySDM.environments import Parcel +from PySDM.initialisation import discretise_multiplicities, equilibrate_wet_radii + + +class Simulation(BasicSimulation): + def __init__(self, settings, backend=CPU): + + dt = settings.dt + + formulae = settings.formulae + + env = Parcel( + mixed_phase=True, + dt=dt, + mass_of_dry_air=settings.mass_of_dry_air, + p0=settings.initial_pressure, + initial_water_vapour_mixing_ratio=settings.initial_water_vapour_mixing_ratio, + T0=settings.initial_temperature, + w=settings.w_updraft, + ) + + builder = Builder( + backend=backend( + formulae=settings.formulae, + **( + {"override_jit_flags": {"parallel": False}} + if backend == CPU + else {} + ) + ), + n_sd=settings.n_sd, + environment=env, + ) + + builder.add_dynamic(AmbientThermodynamics()) + builder.add_dynamic(Condensation()) + builder.add_dynamic(VapourDepositionOnIce()) + builder.add_dynamic(Freezing(singular=False, homogeneous_freezing=True, immersion_freezing=False)) + + self.n_sd = settings.n_sd + self.multiplicities = discretise_multiplicities(settings.specific_concentration * env.mass_of_dry_air) + self.r_dry = settings.r_dry + v_dry = settings.formulae.trivia.volume(radius=self.r_dry) + kappa = settings.kappa + + self.r_wet = equilibrate_wet_radii(r_dry=self.r_dry, environment=builder.particulator.environment, + kappa_times_dry_volume=kappa * v_dry) + + attributes = { + "multiplicity": self.multiplicities, + 'dry volume': v_dry, + 'kappa times dry volume': kappa * v_dry, + "signed water mass": formulae.particle_shape_and_density.radius_to_mass(self.r_wet), + } + + products = [ + PySDM_products.Time(name="t"), + PySDM_products.AmbientRelativeHumidity(name="RH_ice", unit="%"), + PySDM_products.ParticleConcentration( + name='n_i', unit='1/m**3', + radius_range=(-np.inf, 0)), + ] + + self.particulator = builder.build(attributes, products) + + self.n_output = settings.n_output + self.n_substeps = int(settings.t_duration / dt / self.n_output) + + + def save(self, output): + cell_id = 0 + output["t"].append(self.particulator.products["t"].get()) + output["ni"].append(self.particulator.products["n_i"].get()[cell_id]) + output["RHi"].append(self.particulator.products["RH_ice"].get()[cell_id]) + + def run(self): + output = { + "t": [], + "ni": [], + "RHi": [], + } + + self.save(output) + + RHi_old = self.particulator.products["RH_ice"].get()[0].copy() + for _ in range(self.n_output): + + self.particulator.run(self.n_substeps) + + self.save(output) + + + RHi = self.particulator.products["RH_ice"].get()[0].copy() + dRHi = (RHi_old - RHi) / RHi_old + if (dRHi > 0. and RHi < 130.): + print("break") + break + else: + RHi_old = RHi + + return output["ni"][-1] From f8bd37a011b80a981b229386c76ccfff5f2905f6 Mon Sep 17 00:00:00 2001 From: Tim Luettmer Date: Wed, 14 May 2025 17:21:19 +0200 Subject: [PATCH 27/57] added bulk reference solution to spichtinger_et_al_2023 example --- .../data/reference_bulk.py | 40 +++++++++++++++++++ .../Spichtinger_et_al_2023/run.py | 6 ++- 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/examples/PySDM_examples/Spichtinger_et_al_2023/data/reference_bulk.py b/examples/PySDM_examples/Spichtinger_et_al_2023/data/reference_bulk.py index 6bc855f34a..6ae2d3371d 100644 --- a/examples/PySDM_examples/Spichtinger_et_al_2023/data/reference_bulk.py +++ b/examples/PySDM_examples/Spichtinger_et_al_2023/data/reference_bulk.py @@ -2,5 +2,45 @@ reference results for bulk scheme in Fig B1. in [Spichtinger et al. 2023](https://doi.org/10.5194/acp-23-2035-2023) """ +import numpy as np +def bulk_model_reference_array(): + + initial_temperatures = np.array([196., 216., 236.]) + updrafts = np.array([0.05, 0.1, 0.3, 0.5, 1., 3., 5., 10.]) + + dim_size = ( np.shape(initial_temperatures)[0], np.shape(updrafts)[0] ) + ni_bulk_ref = np.zeros(dim_size) + + # T = 196 + ni_bulk_ref[0,0] = 643686.1316903427 + ni_bulk_ref[0,1] = 2368481.0609527444 + ni_bulk_ref[0,2] = 20160966.984670535 + ni_bulk_ref[0,3] = 49475281.81718969 + ni_bulk_ref[0,4] = 131080662.23620115 + ni_bulk_ref[0,5] = 401046528.70428866 + ni_bulk_ref[0,6] = 627442148.3402529 + ni_bulk_ref[0,7] = 1151707310.2210448 + + # T = 216 + ni_bulk_ref[1,0] = 60955.84292640147 + ni_bulk_ref[1,1] = 189002.0792186534 + ni_bulk_ref[1,2] = 1200751.6897658105 + ni_bulk_ref[1,3] = 2942110.815055958 + ni_bulk_ref[1,4] = 10475282.894692907 + ni_bulk_ref[1,5] = 90871045.40856971 + ni_bulk_ref[1,6] = 252175505.460412 + ni_bulk_ref[1,7] = 860335156.4717773 + + # T = 236 + ni_bulk_ref[2,0] = 13049.108886452004 + ni_bulk_ref[2,1] = 40422.244759544985 + ni_bulk_ref[2,2] = 237862.49854786208 + ni_bulk_ref[2,3] = 545315.7805748513 + ni_bulk_ref[2,4] = 1707801.469906006 + ni_bulk_ref[2,5] = 11128055.66932415 + ni_bulk_ref[2,6] = 27739585.111447476 + ni_bulk_ref[2,7] = 101799566.47225031 + + return initial_temperatures, updrafts, ni_bulk_ref \ No newline at end of file diff --git a/examples/PySDM_examples/Spichtinger_et_al_2023/run.py b/examples/PySDM_examples/Spichtinger_et_al_2023/run.py index 48c89a2681..35bdd98f35 100644 --- a/examples/PySDM_examples/Spichtinger_et_al_2023/run.py +++ b/examples/PySDM_examples/Spichtinger_et_al_2023/run.py @@ -1,6 +1,6 @@ import json from examples.PySDM_examples.Spichtinger_et_al_2023 import Simulation, Settings -from examples.PySDM_examples.Spichtinger_et_al_2023.data import simulation_data +from examples.PySDM_examples.Spichtinger_et_al_2023.data import simulation_data, reference_bulk import numpy as np from matplotlib import pyplot @@ -63,11 +63,15 @@ T, w, ni_ens_mean = simulation_data.saved_simulation_ensemble_mean() + # bulk reference + T_bulk_ref, w_bulk_ref, ni_bulk_ref = reference_bulk.bulk_model_reference_array() + # plot fig, ax = pyplot.subplots(1, 1, figsize=(5, 5)) for j in range(len(T)): ax.scatter(w, ni_ens_mean[:, j], label=f"T0={T[j]:.0f}K") + ax.plot(w_bulk_ref, ni_bulk_ref[j, :], linestyle='dashed') ax.set_xscale('log') ax.set_xlim(0.08, 10.) From e05729864601c538bb6a85873717a0c50b1f49c9 Mon Sep 17 00:00:00 2001 From: Tim Luettmer Date: Wed, 14 May 2025 17:24:36 +0200 Subject: [PATCH 28/57] added json to git ignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 4234ec2217..d339037ef0 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ PySDM_examples/utils/temporary_files/* *.svg *.gif *.ipynb.badges.md +*.json # Byte-compiled / optimized / DLL files __pycache__/ From e92286f18f6500812c3b1c77448af62d9f941107 Mon Sep 17 00:00:00 2001 From: Tim Luettmer Date: Wed, 14 May 2025 17:30:06 +0200 Subject: [PATCH 29/57] cleanup for constants_defaults.py --- PySDM/physics/constants_defaults.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/PySDM/physics/constants_defaults.py b/PySDM/physics/constants_defaults.py index 0d415b3ba0..7f629e131f 100644 --- a/PySDM/physics/constants_defaults.py +++ b/PySDM/physics/constants_defaults.py @@ -106,13 +106,6 @@ """ thermal accommodation coefficient for vapour deposition as recommended in [Pruppacher & Klett](https://doi.org/10.1007/978-0-306-48100-0) """ -p1000 = 1000 * si.hectopascals -c_pd = 1005 * si.joule / si.kilogram / si.kelvin -c_pv = 1850 * si.joule / si.kilogram / si.kelvin -g_std = sci.g * si.metre / si.second**2 - -c_pw = 4218 * si.joule / si.kilogram / si.kelvin - ARM_C1 = 6.1094 * si.hectopascal """ [August](https://doi.org/10.1002/andp.18280890511) Roche Magnus formula coefficients (values from [Alduchov & Eskridge 1996](https://doi.org/10.1175%2F1520-0450%281996%29035%3C0601%3AIMFAOS%3E2.0.CO%3B2)) From 037e4dae2804aa5968ce26d7547f9fa10ce8f7f0 Mon Sep 17 00:00:00 2001 From: Tim Luettmer Date: Wed, 14 May 2025 17:46:24 +0200 Subject: [PATCH 30/57] added references to constants_defaults.py --- PySDM/physics/constants_defaults.py | 18 ++++++++++++++++-- .../koop_murray.py | 8 +++++++- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/PySDM/physics/constants_defaults.py b/PySDM/physics/constants_defaults.py index 7f629e131f..44f00c15d0 100644 --- a/PySDM/physics/constants_defaults.py +++ b/PySDM/physics/constants_defaults.py @@ -316,22 +316,36 @@ """ 〃 """ KOOP_2000_C1 = -906.7 +""" homogeneous ice nucleation rate +([Koop et al. 2000](https://doi.org/10.1038/35020537)) """ KOOP_2000_C2 = 8502 +""" 〃 """ KOOP_2000_C3 = -26924 +""" 〃 """ KOOP_2000_C4 = 29180 +""" 〃 """ KOOP_UNIT = 1 / si.cm**3 / si.s +""" 〃 """ KOOP_CORR = -1.522 +""" homogeneous ice nucleation rate correction factor +([Spichtinger et al. 2023](https://doi.org/10.5194/acp-23-2035-2023)) """ KOOP_MURRAY_C0 = -3020.684 +""" homogeneous ice nucleation rate for pure water droplets +([Koop & Murray 20016](https://doi.org/10.1063/1.4962355)) """ KOOP_MURRAY_C1 = -425.921 +""" 〃 """ KOOP_MURRAY_C2 = -25.9779 +""" 〃 """ KOOP_MURRAY_C3 = -0.868451 +""" 〃 """ KOOP_MURRAY_C4 = -1.66203e-2 +""" 〃 """ KOOP_MURRAY_C5 = -1.71736e-4 +""" 〃 """ KOOP_MURRAY_C6 = -7.46953e-7 - -T_hom_freeze = 235. * si.kelvin +""" 〃 """ J_HET = np.nan J_HOM = np.nan diff --git a/PySDM/physics/homogeneous_ice_nucleation_rate/koop_murray.py b/PySDM/physics/homogeneous_ice_nucleation_rate/koop_murray.py index 0d022058c4..6c824539c2 100644 --- a/PySDM/physics/homogeneous_ice_nucleation_rate/koop_murray.py +++ b/PySDM/physics/homogeneous_ice_nucleation_rate/koop_murray.py @@ -12,4 +12,10 @@ def __init__(self, const): @staticmethod def j_hom(const, T, da_w_ice): T_diff = T - const.T_tri - return 10**(const.KOOP_MURRAY_C0 + const.KOOP_MURRAY_C1*T_diff + const.KOOP_MURRAY_C2*T_diff**2. + const.KOOP_MURRAY_C3*T_diff**3. + const.KOOP_MURRAY_C4*T_diff**4. + const.KOOP_MURRAY_C5*T_diff**5. + const.KOOP_MURRAY_C6*T_diff**6.) * const.KOOP_UNIT + return 10**(const.KOOP_MURRAY_C0 + + const.KOOP_MURRAY_C1*T_diff + + const.KOOP_MURRAY_C2*T_diff**2. + + const.KOOP_MURRAY_C3*T_diff**3. + + const.KOOP_MURRAY_C4*T_diff**4. + + const.KOOP_MURRAY_C5*T_diff**5. + + const.KOOP_MURRAY_C6*T_diff**6.) * const.KOOP_UNIT From e96d4cae60284043f646e64bd14160dcd133fb12 Mon Sep 17 00:00:00 2001 From: Tim Luettmer Date: Thu, 15 May 2025 15:43:50 +0200 Subject: [PATCH 31/57] let precommit do its thing --- .../impl_numba/methods/freezing_methods.py | 32 +++++----- PySDM/dynamics/freezing.py | 16 +++-- PySDM/particulator.py | 4 +- PySDM/physics/constants_defaults.py | 2 +- .../homogeneous_ice_nucleation_rate/koop.py | 16 +++-- .../koop_corr.py | 20 ++++--- .../koop_murray.py | 22 ++++--- .../homogeneous_ice_nucleation_rate/null.py | 2 +- PySDM/physics/trivia.py | 2 +- .../Spichtinger_et_al_2023/__init__.py | 2 +- .../data/reference_bulk.py | 59 ++++++++++--------- .../data/simulation_data.py | 27 +++++---- .../Spichtinger_et_al_2023/run.py | 48 ++++++++------- .../Spichtinger_et_al_2023/settings.py | 41 +++++++------ .../Spichtinger_et_al_2023/simulation.py | 42 ++++++++----- tests/unit_tests/dynamics/test_freezing.py | 41 ++++++++----- .../test_homogeneous_nucleation_rates.py | 19 +++--- 17 files changed, 231 insertions(+), 164 deletions(-) diff --git a/PySDM/backends/impl_numba/methods/freezing_methods.py b/PySDM/backends/impl_numba/methods/freezing_methods.py index 6de4cbeb9c..9b8f5b567f 100644 --- a/PySDM/backends/impl_numba/methods/freezing_methods.py +++ b/PySDM/backends/impl_numba/methods/freezing_methods.py @@ -94,7 +94,6 @@ def freeze_time_dependent_body( # pylint: disable=too-many-arguments self.freeze_time_dependent_body = freeze_time_dependent_body - j_hom = self.formulae.homogeneous_ice_nucleation_rate.j_hom @numba.njit(**self.default_jit_flags) @@ -119,10 +118,13 @@ def freeze_time_dependent_homogeneous_body( # pylint: disable=unused-argument,t elif unfrozen_and_ice_saturated( attributes.signed_water_mass[i], relative_humidity_ice[cell_id] ): - d_a_w_ice = (relative_humidity_ice[cell_id] - 1.) * a_w_ice[cell_id] + d_a_w_ice = (relative_humidity_ice[cell_id] - 1.0) * a_w_ice[ + cell_id + ] if d_a_w_ice > 0.23 and d_a_w_ice < 0.34: rate_assuming_constant_temperature_within_dt = ( - j_hom(temperature[cell_id], d_a_w_ice) * attributes.volume[i] + j_hom(temperature[cell_id], d_a_w_ice) + * attributes.volume[i] ) prob = 1 - prob_zero_events( r=rate_assuming_constant_temperature_within_dt, dt=timestep @@ -130,7 +132,9 @@ def freeze_time_dependent_homogeneous_body( # pylint: disable=unused-argument,t if rand[i] < prob: _freeze(attributes.signed_water_mass, i) - self.freeze_time_dependent_homogeneous_body = freeze_time_dependent_homogeneous_body + self.freeze_time_dependent_homogeneous_body = ( + freeze_time_dependent_homogeneous_body + ) def freeze_singular( self, *, attributes, temperature, relative_humidity, cell, thaw: bool @@ -173,16 +177,16 @@ def freeze_time_dependent( ) def freeze_time_dependent_homogeneous( - self, - *, - rand, - attributes, - timestep, - cell, - a_w_ice, - temperature, - relative_humidity_ice, - thaw: bool + self, + *, + rand, + attributes, + timestep, + cell, + a_w_ice, + temperature, + relative_humidity_ice, + thaw: bool, ): self.freeze_time_dependent_homogeneous_body( rand.data, diff --git a/PySDM/dynamics/freezing.py b/PySDM/dynamics/freezing.py index 54e464b457..72f41b5e78 100644 --- a/PySDM/dynamics/freezing.py +++ b/PySDM/dynamics/freezing.py @@ -8,7 +8,14 @@ @register_dynamic() class Freezing: - def __init__(self, *, singular=True, homogeneous_freezing=False, immersion_freezing=True, thaw=False): + def __init__( + self, + *, + singular=True, + homogeneous_freezing=False, + immersion_freezing=True, + thaw=False, + ): self.singular = singular self.homogeneous_freezing = homogeneous_freezing self.immersion_freezing = immersion_freezing @@ -39,13 +46,11 @@ def register(self, builder): if self.homogeneous_freezing: assert ( - self.particulator.formulae.homogeneous_ice_nucleation_rate.__name__ - != "Null" + self.particulator.formulae.homogeneous_ice_nucleation_rate.__name__ + != "Null" ) builder.request_attribute("volume") - - if self.homogeneous_freezing or not self.singular: self.rand = self.particulator.Storage.empty( self.particulator.n_sd, dtype=float @@ -74,7 +79,6 @@ def __call__(self): thaw=self.thaw, ) - if self.homogeneous_freezing: self.rand.urand(self.rng) self.particulator.homogeneous_freezing_time_dependent( diff --git a/PySDM/particulator.py b/PySDM/particulator.py index bbaee02310..32ae0697d5 100644 --- a/PySDM/particulator.py +++ b/PySDM/particulator.py @@ -539,9 +539,7 @@ def immersion_freezing_singular(self, *, thaw: bool): ) self.attributes.mark_updated("signed water mass") - def homogeneous_freezing_time_dependent( - self, *, thaw: bool, rand: Storage - ): + def homogeneous_freezing_time_dependent(self, *, thaw: bool, rand: Storage): self.backend.freeze_time_dependent_homogeneous( rand=rand, attributes=TimeDependentHomogeneousAttributes( diff --git a/PySDM/physics/constants_defaults.py b/PySDM/physics/constants_defaults.py index 44f00c15d0..ebdee48627 100644 --- a/PySDM/physics/constants_defaults.py +++ b/PySDM/physics/constants_defaults.py @@ -332,7 +332,7 @@ ([Spichtinger et al. 2023](https://doi.org/10.5194/acp-23-2035-2023)) """ KOOP_MURRAY_C0 = -3020.684 -""" homogeneous ice nucleation rate for pure water droplets +""" homogeneous ice nucleation rate for pure water droplets ([Koop & Murray 20016](https://doi.org/10.1063/1.4962355)) """ KOOP_MURRAY_C1 = -425.921 """ 〃 """ diff --git a/PySDM/physics/homogeneous_ice_nucleation_rate/koop.py b/PySDM/physics/homogeneous_ice_nucleation_rate/koop.py index 04c4a9e4ee..ddca06e8f0 100644 --- a/PySDM/physics/homogeneous_ice_nucleation_rate/koop.py +++ b/PySDM/physics/homogeneous_ice_nucleation_rate/koop.py @@ -1,5 +1,5 @@ """ -Koop homogeneous nucleation rate parameterization for solution droplets +Koop homogeneous nucleation rate parameterization for solution droplets valid for 0.26 < da_w_ice < 0.34 ([Koop et al. 2000](https://doi.org/10.1038/35020537)) """ @@ -11,7 +11,13 @@ def __init__(self, const): @staticmethod def j_hom(const, T, da_w_ice): - return 10**(const.KOOP_2000_C1 - + const.KOOP_2000_C2 * da_w_ice - + const.KOOP_2000_C3 * da_w_ice**2. - + const.KOOP_2000_C4 * da_w_ice**3.) * const.KOOP_UNIT + return ( + 10 + ** ( + const.KOOP_2000_C1 + + const.KOOP_2000_C2 * da_w_ice + + const.KOOP_2000_C3 * da_w_ice**2.0 + + const.KOOP_2000_C4 * da_w_ice**3.0 + ) + * const.KOOP_UNIT + ) diff --git a/PySDM/physics/homogeneous_ice_nucleation_rate/koop_corr.py b/PySDM/physics/homogeneous_ice_nucleation_rate/koop_corr.py index 2646df528d..b3675df0c1 100644 --- a/PySDM/physics/homogeneous_ice_nucleation_rate/koop_corr.py +++ b/PySDM/physics/homogeneous_ice_nucleation_rate/koop_corr.py @@ -1,6 +1,6 @@ """ -Koop homogeneous nucleation rate parameterization for solution droplets [Koop et al. 2000] corrected -such that it coincides with homogeneous nucleation rate parameterization for pure water droplets +Koop homogeneous nucleation rate parameterization for solution droplets [Koop et al. 2000] corrected +such that it coincides with homogeneous nucleation rate parameterization for pure water droplets [Koop and Murray 2016] at water saturation between 235K < T < 240K ([Spichtinger et al. 2023](https://doi.org/10.5194/acp-23-2035-2023)) """ @@ -12,8 +12,14 @@ def __init__(self, const): @staticmethod def j_hom(const, T, da_w_ice): - return 10**(const.KOOP_2000_C1 - + const.KOOP_2000_C2 * da_w_ice - + const.KOOP_2000_C3 * da_w_ice**2. - + const.KOOP_2000_C4 * da_w_ice**3. - + const.KOOP_CORR) * const.KOOP_UNIT + return ( + 10 + ** ( + const.KOOP_2000_C1 + + const.KOOP_2000_C2 * da_w_ice + + const.KOOP_2000_C3 * da_w_ice**2.0 + + const.KOOP_2000_C4 * da_w_ice**3.0 + + const.KOOP_CORR + ) + * const.KOOP_UNIT + ) diff --git a/PySDM/physics/homogeneous_ice_nucleation_rate/koop_murray.py b/PySDM/physics/homogeneous_ice_nucleation_rate/koop_murray.py index 6c824539c2..4c53ad313c 100644 --- a/PySDM/physics/homogeneous_ice_nucleation_rate/koop_murray.py +++ b/PySDM/physics/homogeneous_ice_nucleation_rate/koop_murray.py @@ -1,5 +1,5 @@ """ -Koop and Murray homogeneous nucleation rate parameterization for pure water droplets +Koop and Murray homogeneous nucleation rate parameterization for pure water droplets at water saturation ([Koop and Murray 2016](https://doi.org/10.1063/1.4962355)) """ @@ -12,10 +12,16 @@ def __init__(self, const): @staticmethod def j_hom(const, T, da_w_ice): T_diff = T - const.T_tri - return 10**(const.KOOP_MURRAY_C0 - + const.KOOP_MURRAY_C1*T_diff - + const.KOOP_MURRAY_C2*T_diff**2. - + const.KOOP_MURRAY_C3*T_diff**3. - + const.KOOP_MURRAY_C4*T_diff**4. - + const.KOOP_MURRAY_C5*T_diff**5. - + const.KOOP_MURRAY_C6*T_diff**6.) * const.KOOP_UNIT + return ( + 10 + ** ( + const.KOOP_MURRAY_C0 + + const.KOOP_MURRAY_C1 * T_diff + + const.KOOP_MURRAY_C2 * T_diff**2.0 + + const.KOOP_MURRAY_C3 * T_diff**3.0 + + const.KOOP_MURRAY_C4 * T_diff**4.0 + + const.KOOP_MURRAY_C5 * T_diff**5.0 + + const.KOOP_MURRAY_C6 * T_diff**6.0 + ) + * const.KOOP_UNIT + ) diff --git a/PySDM/physics/homogeneous_ice_nucleation_rate/null.py b/PySDM/physics/homogeneous_ice_nucleation_rate/null.py index 81ceff95b6..6fbf21e9ed 100644 --- a/PySDM/physics/homogeneous_ice_nucleation_rate/null.py +++ b/PySDM/physics/homogeneous_ice_nucleation_rate/null.py @@ -9,5 +9,5 @@ def __init__(self, _): pass @staticmethod - def j_hom(const, T, d_a_w_ice): # pylint: disable=unused-argument + def j_hom(const, T, d_a_w_ice): # pylint: disable=unused-argument return 0 diff --git a/PySDM/physics/trivia.py b/PySDM/physics/trivia.py index a8ddcd8bfe..93f53ce6d8 100644 --- a/PySDM/physics/trivia.py +++ b/PySDM/physics/trivia.py @@ -160,4 +160,4 @@ def poissonian_avoidance_function(r, dt): (or void probability, or avoidance function) in a Poisson counting process with a constant rate `r` """ - return np.exp(-r * dt) \ No newline at end of file + return np.exp(-r * dt) diff --git a/examples/PySDM_examples/Spichtinger_et_al_2023/__init__.py b/examples/PySDM_examples/Spichtinger_et_al_2023/__init__.py index 6ac40f57df..baa2774fa3 100644 --- a/examples/PySDM_examples/Spichtinger_et_al_2023/__init__.py +++ b/examples/PySDM_examples/Spichtinger_et_al_2023/__init__.py @@ -4,4 +4,4 @@ [Spichtinger et al. 2023](https://doi.org/10.5194/acp-23-2035-2023) """ from .simulation import Simulation -from .settings import Settings \ No newline at end of file +from .settings import Settings diff --git a/examples/PySDM_examples/Spichtinger_et_al_2023/data/reference_bulk.py b/examples/PySDM_examples/Spichtinger_et_al_2023/data/reference_bulk.py index 6ae2d3371d..6aec8eb507 100644 --- a/examples/PySDM_examples/Spichtinger_et_al_2023/data/reference_bulk.py +++ b/examples/PySDM_examples/Spichtinger_et_al_2023/data/reference_bulk.py @@ -2,45 +2,46 @@ reference results for bulk scheme in Fig B1. in [Spichtinger et al. 2023](https://doi.org/10.5194/acp-23-2035-2023) """ + import numpy as np def bulk_model_reference_array(): - initial_temperatures = np.array([196., 216., 236.]) - updrafts = np.array([0.05, 0.1, 0.3, 0.5, 1., 3., 5., 10.]) + initial_temperatures = np.array([196.0, 216.0, 236.0]) + updrafts = np.array([0.05, 0.1, 0.3, 0.5, 1.0, 3.0, 5.0, 10.0]) - dim_size = ( np.shape(initial_temperatures)[0], np.shape(updrafts)[0] ) + dim_size = (np.shape(initial_temperatures)[0], np.shape(updrafts)[0]) ni_bulk_ref = np.zeros(dim_size) # T = 196 - ni_bulk_ref[0,0] = 643686.1316903427 - ni_bulk_ref[0,1] = 2368481.0609527444 - ni_bulk_ref[0,2] = 20160966.984670535 - ni_bulk_ref[0,3] = 49475281.81718969 - ni_bulk_ref[0,4] = 131080662.23620115 - ni_bulk_ref[0,5] = 401046528.70428866 - ni_bulk_ref[0,6] = 627442148.3402529 - ni_bulk_ref[0,7] = 1151707310.2210448 + ni_bulk_ref[0, 0] = 643686.1316903427 + ni_bulk_ref[0, 1] = 2368481.0609527444 + ni_bulk_ref[0, 2] = 20160966.984670535 + ni_bulk_ref[0, 3] = 49475281.81718969 + ni_bulk_ref[0, 4] = 131080662.23620115 + ni_bulk_ref[0, 5] = 401046528.70428866 + ni_bulk_ref[0, 6] = 627442148.3402529 + ni_bulk_ref[0, 7] = 1151707310.2210448 # T = 216 - ni_bulk_ref[1,0] = 60955.84292640147 - ni_bulk_ref[1,1] = 189002.0792186534 - ni_bulk_ref[1,2] = 1200751.6897658105 - ni_bulk_ref[1,3] = 2942110.815055958 - ni_bulk_ref[1,4] = 10475282.894692907 - ni_bulk_ref[1,5] = 90871045.40856971 - ni_bulk_ref[1,6] = 252175505.460412 - ni_bulk_ref[1,7] = 860335156.4717773 + ni_bulk_ref[1, 0] = 60955.84292640147 + ni_bulk_ref[1, 1] = 189002.0792186534 + ni_bulk_ref[1, 2] = 1200751.6897658105 + ni_bulk_ref[1, 3] = 2942110.815055958 + ni_bulk_ref[1, 4] = 10475282.894692907 + ni_bulk_ref[1, 5] = 90871045.40856971 + ni_bulk_ref[1, 6] = 252175505.460412 + ni_bulk_ref[1, 7] = 860335156.4717773 # T = 236 - ni_bulk_ref[2,0] = 13049.108886452004 - ni_bulk_ref[2,1] = 40422.244759544985 - ni_bulk_ref[2,2] = 237862.49854786208 - ni_bulk_ref[2,3] = 545315.7805748513 - ni_bulk_ref[2,4] = 1707801.469906006 - ni_bulk_ref[2,5] = 11128055.66932415 - ni_bulk_ref[2,6] = 27739585.111447476 - ni_bulk_ref[2,7] = 101799566.47225031 - - return initial_temperatures, updrafts, ni_bulk_ref \ No newline at end of file + ni_bulk_ref[2, 0] = 13049.108886452004 + ni_bulk_ref[2, 1] = 40422.244759544985 + ni_bulk_ref[2, 2] = 237862.49854786208 + ni_bulk_ref[2, 3] = 545315.7805748513 + ni_bulk_ref[2, 4] = 1707801.469906006 + ni_bulk_ref[2, 5] = 11128055.66932415 + ni_bulk_ref[2, 6] = 27739585.111447476 + ni_bulk_ref[2, 7] = 101799566.47225031 + + return initial_temperatures, updrafts, ni_bulk_ref diff --git a/examples/PySDM_examples/Spichtinger_et_al_2023/data/simulation_data.py b/examples/PySDM_examples/Spichtinger_et_al_2023/data/simulation_data.py index e140c6ead1..3b7f6cb597 100644 --- a/examples/PySDM_examples/Spichtinger_et_al_2023/data/simulation_data.py +++ b/examples/PySDM_examples/Spichtinger_et_al_2023/data/simulation_data.py @@ -1,19 +1,22 @@ import numpy as np + def saved_simulation_ensemble_mean(): - ni_ens_mean = np.array([ - [0.00000000e+00, 0.00000000e+00, 0.00000000e+00], - [0.00000000e+00, 0.00000000e+00, 0.00000000e+00], - [1.62069821e+03, 0.00000000e+00, 0.00000000e+00], - [5.25377025e+06, 9.75512904e+05, 4.51431097e+05], - [3.67137290e+07, 5.45240337e+06, 1.53884530e+06], - [7.96514420e+07, 1.13878118e+07, 3.26386880e+06], - [2.19385480e+08, 3.62240242e+07, 9.00657591e+06], - [1.00631095e+09, 2.34443408e+08, 4.90577208e+07], - [1.73457062e+09, 5.20774276e+08, 1.16040316e+08] - ]) + ni_ens_mean = np.array( + [ + [0.00000000e00, 0.00000000e00, 0.00000000e00], + [0.00000000e00, 0.00000000e00, 0.00000000e00], + [1.62069821e03, 0.00000000e00, 0.00000000e00], + [5.25377025e06, 9.75512904e05, 4.51431097e05], + [3.67137290e07, 5.45240337e06, 1.53884530e06], + [7.96514420e07, 1.13878118e07, 3.26386880e06], + [2.19385480e08, 3.62240242e07, 9.00657591e06], + [1.00631095e09, 2.34443408e08, 4.90577208e07], + [1.73457062e09, 5.20774276e08, 1.16040316e08], + ] + ) T = np.array([196.0, 216.0, 236.0]) w = np.array([0.01, 0.03, 0.05, 0.1, 0.3, 0.5, 1.0, 3.0, 5.0]) - return T, w, ni_ens_mean \ No newline at end of file + return T, w, ni_ens_mean diff --git a/examples/PySDM_examples/Spichtinger_et_al_2023/run.py b/examples/PySDM_examples/Spichtinger_et_al_2023/run.py index 35bdd98f35..6313a40560 100644 --- a/examples/PySDM_examples/Spichtinger_et_al_2023/run.py +++ b/examples/PySDM_examples/Spichtinger_et_al_2023/run.py @@ -1,13 +1,16 @@ import json from examples.PySDM_examples.Spichtinger_et_al_2023 import Simulation, Settings -from examples.PySDM_examples.Spichtinger_et_al_2023.data import simulation_data, reference_bulk +from examples.PySDM_examples.Spichtinger_et_al_2023.data import ( + simulation_data, + reference_bulk, +) import numpy as np from matplotlib import pyplot calculate_data = False -save_to_file = False -plot = True +save_to_file = False +plot = True read_from_json = False @@ -15,33 +18,39 @@ general_settings = {"n_sd": 50000, "dt": 0.1} - initial_temperatures = np.array([196., 216., 236.]) - updrafts = np.array([0.01, 0.03, 0.05, 0.1, 0.3, 0.5, 1., 3., 5.]) + initial_temperatures = np.array([196.0, 216.0, 236.0]) + updrafts = np.array([0.01, 0.03, 0.05, 0.1, 0.3, 0.5, 1.0, 3.0, 5.0]) number_of_ensemble_runs = 5 seeds = [124670285330, 439785398735, 9782539783258, 12874192127481, 12741731272] dim_updrafts = len(updrafts) dim_initial_temperatures = len(initial_temperatures) - number_concentration_ice = np.zeros([dim_updrafts, dim_initial_temperatures, number_of_ensemble_runs]) + number_concentration_ice = np.zeros( + [dim_updrafts, dim_initial_temperatures, number_of_ensemble_runs] + ) for i in range(dim_updrafts): for j in range(dim_initial_temperatures): for k in range(number_of_ensemble_runs): - setting_dictionary = {**general_settings, } + setting_dictionary = { + **general_settings, + } setting_dictionary["w_updraft"] = updrafts[i] setting_dictionary["T0"] = initial_temperatures[j] setting_dictionary["seed"] = seeds[k] setting = Settings(**setting_dictionary) model = Simulation(setting) - number_concentration_ice[i,j,k] = model.run() + number_concentration_ice[i, j, k] = model.run() if save_to_file: - file_name = "data/ni_w_T_ens_"+str(number_of_ensemble_runs)+".json" - data_file = {"ni": number_concentration_ice.tolist(), - "T": initial_temperatures.tolist(), - "w": updrafts.tolist()} - with open(file_name, 'w') as file: + file_name = "data/ni_w_T_ens_" + str(number_of_ensemble_runs) + ".json" + data_file = { + "ni": number_concentration_ice.tolist(), + "T": initial_temperatures.tolist(), + "w": updrafts.tolist(), + } + with open(file_name, "w") as file: json.dump(data_file, file) if plot: @@ -52,7 +61,7 @@ else: if read_from_json: file_name = "data/ni_w_T_ens_5.json" - with open(file_name, 'r') as f: + with open(file_name, "r") as f: data = json.load(f) ni = data["ni"] @@ -62,7 +71,6 @@ else: T, w, ni_ens_mean = simulation_data.saved_simulation_ensemble_mean() - # bulk reference T_bulk_ref, w_bulk_ref, ni_bulk_ref = reference_bulk.bulk_model_reference_array() @@ -71,14 +79,14 @@ for j in range(len(T)): ax.scatter(w, ni_ens_mean[:, j], label=f"T0={T[j]:.0f}K") - ax.plot(w_bulk_ref, ni_bulk_ref[j, :], linestyle='dashed') + ax.plot(w_bulk_ref, ni_bulk_ref[j, :], linestyle="dashed") - ax.set_xscale('log') - ax.set_xlim(0.08, 10.) + ax.set_xscale("log") + ax.set_xlim(0.08, 10.0) ax.set_xlabel(r"vertical updraft [$\mathrm{m \, s^{-1}}$]") - ax.set_yscale('log') - ax.set_ylim(1.e2, 1.e10) + ax.set_yscale("log") + ax.set_ylim(1.0e2, 1.0e10) ax.set_ylabel(r"ice number concentration [$\mathrm{cm^{-3}}$]") ax.legend(loc="lower right") diff --git a/examples/PySDM_examples/Spichtinger_et_al_2023/settings.py b/examples/PySDM_examples/Spichtinger_et_al_2023/settings.py index 7db5f096cd..f4ae698f1f 100644 --- a/examples/PySDM_examples/Spichtinger_et_al_2023/settings.py +++ b/examples/PySDM_examples/Spichtinger_et_al_2023/settings.py @@ -5,6 +5,7 @@ from PySDM.initialisation.spectra import Lognormal from PySDM.initialisation.sampling import spectral_sampling + @strict class Settings: def __init__( @@ -17,12 +18,12 @@ def __init__( dt: float, ): - print( f"{w_updraft:},{T0:},{seed:},{dt:}" ) + print(f"{w_updraft:},{T0:},{seed:},{dt:}") self.n_sd = n_sd self.w_updraft = w_updraft - self.N_dv_solution_droplet = 2500 / si.centimetre ** 3 + self.N_dv_solution_droplet = 2500 / si.centimetre**3 self.r_mean_solution_droplet = 0.055 * si.micrometre self.sigma_solution_droplet = 1.6 @@ -30,32 +31,38 @@ def __init__( self.initial_pressure = 200 * si.hectopascals self.initial_ice_supersaturation = 1.0 self.kappa = 0.64 - self.initial_temperature = T0 + self.initial_temperature = T0 self.formulae = Formulae( particle_shape_and_density="MixedPhaseSpheres", homogeneous_ice_nucleation_rate="Koop_Correction", - seed=seed + seed=seed, ) const = self.formulae.constants - pvs_i = self.formulae.saturation_vapour_pressure.pvs_ice(self.initial_temperature) + pvs_i = self.formulae.saturation_vapour_pressure.pvs_ice( + self.initial_temperature + ) self.initial_water_vapour_mixing_ratio = const.eps / ( self.initial_pressure / self.initial_ice_supersaturation / pvs_i - 1 ) - dry_air_density = (self.formulae.trivia.p_d(self.initial_pressure, self.initial_water_vapour_mixing_ratio ) - / self.initial_temperature - / const.Rd ) - - spectrum = Lognormal(norm_factor=self.N_dv_solution_droplet / dry_air_density, - m_mode=self.r_mean_solution_droplet, - s_geom=self.sigma_solution_droplet) + dry_air_density = ( + self.formulae.trivia.p_d( + self.initial_pressure, self.initial_water_vapour_mixing_ratio + ) + / self.initial_temperature + / const.Rd + ) - self.r_dry, self.specific_concentration = ( - spectral_sampling.Linear(spectrum).sample(n_sd) + spectrum = Lognormal( + norm_factor=self.N_dv_solution_droplet / dry_air_density, + m_mode=self.r_mean_solution_droplet, + s_geom=self.sigma_solution_droplet, ) + self.r_dry, self.specific_concentration = spectral_sampling.Linear( + spectrum + ).sample(n_sd) self.t_duration = 7200 - self.dt = dt - self.n_output = int(self.t_duration / 100) - + self.dt = dt + self.n_output = int(self.t_duration / 100) diff --git a/examples/PySDM_examples/Spichtinger_et_al_2023/simulation.py b/examples/PySDM_examples/Spichtinger_et_al_2023/simulation.py index a6ac960e9d..3623cde4c3 100644 --- a/examples/PySDM_examples/Spichtinger_et_al_2023/simulation.py +++ b/examples/PySDM_examples/Spichtinger_et_al_2023/simulation.py @@ -5,7 +5,12 @@ import PySDM.products as PySDM_products from PySDM.backends import CPU from PySDM.builder import Builder -from PySDM.dynamics import AmbientThermodynamics, Condensation, Freezing, VapourDepositionOnIce +from PySDM.dynamics import ( + AmbientThermodynamics, + Condensation, + Freezing, + VapourDepositionOnIce, +) from PySDM.environments import Parcel from PySDM.initialisation import discretise_multiplicities, equilibrate_wet_radii @@ -34,7 +39,7 @@ def __init__(self, settings, backend=CPU): {"override_jit_flags": {"parallel": False}} if backend == CPU else {} - ) + ), ), n_sd=settings.n_sd, environment=env, @@ -43,30 +48,41 @@ def __init__(self, settings, backend=CPU): builder.add_dynamic(AmbientThermodynamics()) builder.add_dynamic(Condensation()) builder.add_dynamic(VapourDepositionOnIce()) - builder.add_dynamic(Freezing(singular=False, homogeneous_freezing=True, immersion_freezing=False)) + builder.add_dynamic( + Freezing( + singular=False, homogeneous_freezing=True, immersion_freezing=False + ) + ) self.n_sd = settings.n_sd - self.multiplicities = discretise_multiplicities(settings.specific_concentration * env.mass_of_dry_air) + self.multiplicities = discretise_multiplicities( + settings.specific_concentration * env.mass_of_dry_air + ) self.r_dry = settings.r_dry v_dry = settings.formulae.trivia.volume(radius=self.r_dry) kappa = settings.kappa - self.r_wet = equilibrate_wet_radii(r_dry=self.r_dry, environment=builder.particulator.environment, - kappa_times_dry_volume=kappa * v_dry) + self.r_wet = equilibrate_wet_radii( + r_dry=self.r_dry, + environment=builder.particulator.environment, + kappa_times_dry_volume=kappa * v_dry, + ) attributes = { "multiplicity": self.multiplicities, - 'dry volume': v_dry, - 'kappa times dry volume': kappa * v_dry, - "signed water mass": formulae.particle_shape_and_density.radius_to_mass(self.r_wet), + "dry volume": v_dry, + "kappa times dry volume": kappa * v_dry, + "signed water mass": formulae.particle_shape_and_density.radius_to_mass( + self.r_wet + ), } products = [ PySDM_products.Time(name="t"), PySDM_products.AmbientRelativeHumidity(name="RH_ice", unit="%"), PySDM_products.ParticleConcentration( - name='n_i', unit='1/m**3', - radius_range=(-np.inf, 0)), + name="n_i", unit="1/m**3", radius_range=(-np.inf, 0) + ), ] self.particulator = builder.build(attributes, products) @@ -74,7 +90,6 @@ def __init__(self, settings, backend=CPU): self.n_output = settings.n_output self.n_substeps = int(settings.t_duration / dt / self.n_output) - def save(self, output): cell_id = 0 output["t"].append(self.particulator.products["t"].get()) @@ -97,10 +112,9 @@ def run(self): self.save(output) - RHi = self.particulator.products["RH_ice"].get()[0].copy() dRHi = (RHi_old - RHi) / RHi_old - if (dRHi > 0. and RHi < 130.): + if dRHi > 0.0 and RHi < 130.0: print("break") break else: diff --git a/tests/unit_tests/dynamics/test_freezing.py b/tests/unit_tests/dynamics/test_freezing.py index fe45963f5b..237de697b9 100644 --- a/tests/unit_tests/dynamics/test_freezing.py +++ b/tests/unit_tests/dynamics/test_freezing.py @@ -86,7 +86,9 @@ def test_no_subsaturated_freezing(self): pass @staticmethod - @pytest.mark.parametrize("freezing_type", ("het_singular", "het_time_dependent", "hom_time_dependent")) + @pytest.mark.parametrize( + "freezing_type", ("het_singular", "het_time_dependent", "hom_time_dependent") + ) @pytest.mark.parametrize("thaw", (True, False)) @pytest.mark.parametrize("epsilon", (0, 1e-5)) def test_thaw(backend_class, freezing_type, thaw, epsilon): @@ -119,11 +121,14 @@ def test_thaw(backend_class, freezing_type, thaw, epsilon): builder = Builder( n_sd=1, backend=backend_class(formulae=formulae), environment=env ) - builder.add_dynamic(Freezing(singular=singular, - homogeneous_freezing=homogeneous_freezing, - immersion_freezing=immersion_freezing, - thaw=thaw) - ) + builder.add_dynamic( + Freezing( + singular=singular, + homogeneous_freezing=homogeneous_freezing, + immersion_freezing=immersion_freezing, + thaw=thaw, + ) + ) particulator = builder.build( products=(IceWaterContent(),), attributes={ @@ -194,9 +199,13 @@ def test_immersion_freezing_singular(backend_class): @staticmethod @pytest.mark.parametrize("double_precision", (True, False)) - @pytest.mark.parametrize("freezing_type", ("het_time_dependent", "hom_time_dependent")) + @pytest.mark.parametrize( + "freezing_type", ("het_time_dependent", "hom_time_dependent") + ) # pylint: disable=too-many-locals - def test_freezing_time_dependent(backend_class, freezing_type, double_precision, plot=False): + def test_freezing_time_dependent( + backend_class, freezing_type, double_precision, plot=False + ): if backend_class.__name__ == "Numba" and not double_precision: pytest.skip() @@ -220,9 +229,7 @@ def test_freezing_time_dependent(backend_class, freezing_type, double_precision, ) # dummy (but must-be-set) values - initial_water_mass = ( - 1000 # for sign flip (ice water has negative volumes) - ) + initial_water_mass = 1000 # for sign flip (ice water has negative volumes) d_v = 666 # products use conc., dividing there, multiplying here, value does not matter def hgh(t): @@ -275,11 +282,13 @@ def low(t): ), environment=env, ) - builder.add_dynamic(Freezing(singular=False, - immersion_freezing=immersion_freezing, - homogeneous_freezing=homogeneous_freezing, - ) - ) + builder.add_dynamic( + Freezing( + singular=False, + immersion_freezing=immersion_freezing, + homogeneous_freezing=homogeneous_freezing, + ) + ) attributes = { "multiplicity": np.full(n_sd, int(case["N"])), "immersed surface area": np.full(n_sd, immersed_surface_area), diff --git a/tests/unit_tests/physics/test_homogeneous_nucleation_rates.py b/tests/unit_tests/physics/test_homogeneous_nucleation_rates.py index b352c37706..b95c284c38 100644 --- a/tests/unit_tests/physics/test_homogeneous_nucleation_rates.py +++ b/tests/unit_tests/physics/test_homogeneous_nucleation_rates.py @@ -1,23 +1,25 @@ """ test for homogeneous nucleation rate parametrisations """ + import pytest import numpy as np from PySDM.formulae import _choices, Formulae + class TestHomogeneousIceNucleationRate: @staticmethod @pytest.mark.parametrize( "da_w_ice, expected_value", ( - (0.27, (5)), - (0.29, (11)), - (0.31, (15)), - (0.33, (20)), + (0.27, (5)), + (0.29, (11)), + (0.31, (15)), + (0.33, (20)), ), ) @pytest.mark.parametrize("parametrisation", ("Koop_Correction",)) - def test_homogeneous_ice_nucleation_rate(da_w_ice,expected_value,parametrisation): + def test_homogeneous_ice_nucleation_rate(da_w_ice, expected_value, parametrisation): """Fig. 2 in [Spichtinger et al. 2023](https://doi.org/10.5194/acp-23-2035-2023)""" # arrange formulae = Formulae( @@ -25,12 +27,11 @@ def test_homogeneous_ice_nucleation_rate(da_w_ice,expected_value,parametrisation ) # act - jhom_log10 = np.log10(formulae.homogeneous_ice_nucleation_rate.j_hom(np.nan, da_w_ice)) + jhom_log10 = np.log10( + formulae.homogeneous_ice_nucleation_rate.j_hom(np.nan, da_w_ice) + ) # assert np.testing.assert_approx_equal( actual=jhom_log10, desired=expected_value, significant=2 ) - - - From dbf8bf2730b6d8f569931d4fccf1652a8ce2d17c Mon Sep 17 00:00:00 2001 From: Tim Luettmer Date: Thu, 15 May 2025 16:35:31 +0200 Subject: [PATCH 32/57] turned run into a Juypter notebook --- .../Spichtinger_et_al_2023/fig_B1.ipynb | 227 ++++++++++++++++++ .../Spichtinger_et_al_2023/run.py | 94 -------- 2 files changed, 227 insertions(+), 94 deletions(-) create mode 100644 examples/PySDM_examples/Spichtinger_et_al_2023/fig_B1.ipynb delete mode 100644 examples/PySDM_examples/Spichtinger_et_al_2023/run.py diff --git a/examples/PySDM_examples/Spichtinger_et_al_2023/fig_B1.ipynb b/examples/PySDM_examples/Spichtinger_et_al_2023/fig_B1.ipynb new file mode 100644 index 0000000000..1f6c2abcbf --- /dev/null +++ b/examples/PySDM_examples/Spichtinger_et_al_2023/fig_B1.ipynb @@ -0,0 +1,227 @@ +{ + "cells": [ + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "\n", + "#### based on Fig. B1 from Spichtinger et al. 2023 (ACP) \"_Impact of formulations of the homogeneous nucleation rate on ice nucleation events in cirrus_\"\n", + "\n", + "(work in progress)\n", + "\n", + "https://doi.org/10.5194/acp-23-2035-2023" + ], + "id": "a6b09eaef75333df" + }, + { + "cell_type": "code", + "id": "initial_id", + "metadata": { + "collapsed": true, + "ExecuteTime": { + "end_time": "2025-05-15T14:20:31.231964Z", + "start_time": "2025-05-15T14:20:31.221457Z" + } + }, + "source": [ + "import sys\n", + "if 'google.colab' in sys.modules:\n", + " !pip --quiet install open-atmos-jupyter-utils\n", + " from open_atmos_jupyter_utils import pip_install_on_colab\n", + " pip_install_on_colab('PySDM-examples')" + ], + "outputs": [], + "execution_count": 5 + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-05-15T14:32:48.496132Z", + "start_time": "2025-05-15T14:32:47.161494Z" + } + }, + "cell_type": "code", + "source": [ + "import json\n", + "from examples.PySDM_examples.Spichtinger_et_al_2023 import Simulation, Settings\n", + "from examples.PySDM_examples.Spichtinger_et_al_2023.data import (\n", + " simulation_data,\n", + " reference_bulk,\n", + ")\n", + "import numpy as np\n", + "from matplotlib import pyplot\n", + "from open_atmos_jupyter_utils import show_plot" + ], + "id": "69ce798ec8b87121", + "outputs": [], + "execution_count": 11 + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-05-15T14:20:46.119340Z", + "start_time": "2025-05-15T14:20:46.116341Z" + } + }, + "cell_type": "code", + "source": [ + "calculate_data = False\n", + "save_to_file = False\n", + "read_from_json = False" + ], + "id": "fabd7ea8e8a11996", + "outputs": [], + "execution_count": 7 + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-05-15T14:20:46.432048Z", + "start_time": "2025-05-15T14:20:46.320842Z" + } + }, + "cell_type": "code", + "source": [ + "if calculate_data:\n", + "\n", + " general_settings = {\"n_sd\": 50000, \"dt\": 0.1}\n", + "\n", + " initial_temperatures = np.array([196.0, 216.0, 236.0])\n", + " updrafts = np.array([0.01, 0.03, 0.05, 0.1, 0.3, 0.5, 1.0, 3.0, 5.0])\n", + " number_of_ensemble_runs = 5\n", + " seeds = [124670285330, 439785398735, 9782539783258, 12874192127481, 12741731272]\n", + "\n", + " dim_updrafts = len(updrafts)\n", + " dim_initial_temperatures = len(initial_temperatures)\n", + "\n", + " number_concentration_ice = np.zeros(\n", + " [dim_updrafts, dim_initial_temperatures, number_of_ensemble_runs]\n", + " )\n", + "\n", + " for i in range(dim_updrafts):\n", + " for j in range(dim_initial_temperatures):\n", + " for k in range(number_of_ensemble_runs):\n", + " setting_dictionary = {\n", + " **general_settings,\n", + " }\n", + " setting_dictionary[\"w_updraft\"] = updrafts[i]\n", + " setting_dictionary[\"T0\"] = initial_temperatures[j]\n", + " setting_dictionary[\"seed\"] = seeds[k]\n", + " setting = Settings(**setting_dictionary)\n", + " model = Simulation(setting)\n", + " number_concentration_ice[i, j, k] = model.run()\n", + "\n", + " if save_to_file:\n", + " file_name = \"data/ni_w_T_ens_\" + str(number_of_ensemble_runs) + \".json\"\n", + " data_file = {\n", + " \"ni\": number_concentration_ice.tolist(),\n", + " \"T\": initial_temperatures.tolist(),\n", + " \"w\": updrafts.tolist(),\n", + " }\n", + " with open(file_name, \"w\") as file:\n", + " json.dump(data_file, file)" + ], + "id": "1acd9d93e2af385c", + "outputs": [], + "execution_count": 8 + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-05-15T14:32:54.180974Z", + "start_time": "2025-05-15T14:32:51.733640Z" + } + }, + "cell_type": "code", + "source": [ + "if calculate_data:\n", + " T = initial_temperatures\n", + " w = updrafts\n", + " ni_ens_mean = np.mean(number_concentration_ice, axis=2)\n", + "else:\n", + " if read_from_json:\n", + " file_name = \"data/ni_w_T_ens_5.json\"\n", + " with open(file_name, \"r\") as f:\n", + " data = json.load(f)\n", + "\n", + " ni = data[\"ni\"]\n", + " T = data[\"T\"]\n", + " w = data[\"w\"]\n", + " ni_ens_mean = np.mean(ni, axis=2)\n", + " else:\n", + " T, w, ni_ens_mean = simulation_data.saved_simulation_ensemble_mean()\n", + "\n", + "# bulk reference\n", + "T_bulk_ref, w_bulk_ref, ni_bulk_ref = reference_bulk.bulk_model_reference_array()\n", + "\n", + "# plot\n", + "fig, ax = pyplot.subplots(1, 1, figsize=(5, 5))\n", + "\n", + "for j in range(len(T)):\n", + " ax.scatter(w, ni_ens_mean[:, j], label=f\"T0={T[j]:.0f}K\")\n", + " ax.plot(w_bulk_ref, ni_bulk_ref[j, :], linestyle=\"dashed\")\n", + "\n", + "ax.set_xscale(\"log\")\n", + "ax.set_xlim(0.08, 10.0)\n", + "ax.set_xlabel(r\"vertical updraft [$\\mathrm{m \\, s^{-1}}$]\")\n", + "\n", + "ax.set_yscale(\"log\")\n", + "ax.set_ylim(1.0e2, 1.0e10)\n", + "ax.set_ylabel(r\"ice number concentration [$\\mathrm{m^{-3}}$]\")\n", + "\n", + "ax.legend(loc=\"lower right\")\n", + "\n", + "show_plot('fig_B1.pdf')" + ], + "id": "3821d0f892f4af29", + "outputs": [ + { + "data": { + "text/plain": [ + "
" + ], + "image/svg+xml": "\n\n\n \n \n \n \n 2025-05-15T16:32:54.045692\n image/svg+xml\n \n \n Matplotlib v3.10.1, https://matplotlib.org/\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n" + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "HBox(children=(HTML(value=\"./fig_B1.pdf
\"), HTML(value=\" Date: Thu, 15 May 2025 17:14:58 +0200 Subject: [PATCH 33/57] added references to bibliography.json --- docs/bibliography.json | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/docs/bibliography.json b/docs/bibliography.json index 505e225906..61ad94a582 100644 --- a/docs/bibliography.json +++ b/docs/bibliography.json @@ -862,5 +862,33 @@ ], "title": "IAEA Reference Sheet for International Measurement Standards: VSMOW & SLAP", "label": "IAEA 2006" - } + }, + "https://doi.org/10.5194/acp-23-2035-2023": { + "usages": [ + "PySDM/physics/constants_defaults.py", + "examples/PySDM_examples/Spichtinger_etal_2023/fig_B1.ipynb", + "examples/PySDM_examples/Spichtinger_etal_2023/__init__.py", + "examples/PySDM_examples/Spichtinger_etal_2023/data/reference_bulk.py", + "PySDM/physics/homogeneous_ice_nucleation_rate/koop_corr.py" + ], + "title": "Impact of formulations of the homogeneous nucleation\nrate on ice nucleation events in cirrus", + "label": "Spichtinger et al. 2023 /Atmos. Chem. Phys., 23)" + }, + "https://doi.org/10.1038/35020537": { + "usages": [ + "PySDM/physics/constants_defaults.py", + "PySDM/physics/homogeneous_ice_nucleation_rate/koop.py" + ], + "title": "Water activity as the determinant for homogeneous ice nucleation in aqueous solutions", + "label": "Koop et al. 2000 (Nature, 406)" + }, + "https://doi.org/10.1063/1.4962355": { + "usages": [ + "PySDM/physics/constants_defaults.py", + "PySDM/physics/homogeneous_ice_nucleation_rate/koop_murray.py" + ], + "title": "A physically constrained classical description of the homogeneous nucleation of ice in water", + "label": "Koop and Murray 2016 (J. Chem. Phys. 145)" + }, } + From 731a68d5f9782837e62b1076a1fc422f7dcc7e1e Mon Sep 17 00:00:00 2001 From: Tim Luettmer Date: Fri, 16 May 2025 13:44:02 +0200 Subject: [PATCH 34/57] fixes for pylint & pdocs complaints --- docs/bibliography.json | 9 +++++---- .../physics/test_homogeneous_nucleation_rates.py | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/bibliography.json b/docs/bibliography.json index 61ad94a582..4a7fb6084f 100644 --- a/docs/bibliography.json +++ b/docs/bibliography.json @@ -869,10 +869,11 @@ "examples/PySDM_examples/Spichtinger_etal_2023/fig_B1.ipynb", "examples/PySDM_examples/Spichtinger_etal_2023/__init__.py", "examples/PySDM_examples/Spichtinger_etal_2023/data/reference_bulk.py", - "PySDM/physics/homogeneous_ice_nucleation_rate/koop_corr.py" + "PySDM/physics/homogeneous_ice_nucleation_rate/koop_corr.py", + "tests/unit_tests/physics/test_homogeneous_nucleation_rates.py" ], "title": "Impact of formulations of the homogeneous nucleation\nrate on ice nucleation events in cirrus", - "label": "Spichtinger et al. 2023 /Atmos. Chem. Phys., 23)" + "label": "Spichtinger et al. 2023 (Atmos. Chem. Phys. 23)" }, "https://doi.org/10.1038/35020537": { "usages": [ @@ -880,7 +881,7 @@ "PySDM/physics/homogeneous_ice_nucleation_rate/koop.py" ], "title": "Water activity as the determinant for homogeneous ice nucleation in aqueous solutions", - "label": "Koop et al. 2000 (Nature, 406)" + "label": "Koop et al. 2000 (Nature 406)" }, "https://doi.org/10.1063/1.4962355": { "usages": [ @@ -889,6 +890,6 @@ ], "title": "A physically constrained classical description of the homogeneous nucleation of ice in water", "label": "Koop and Murray 2016 (J. Chem. Phys. 145)" - }, + } } diff --git a/tests/unit_tests/physics/test_homogeneous_nucleation_rates.py b/tests/unit_tests/physics/test_homogeneous_nucleation_rates.py index b95c284c38..d3dc536855 100644 --- a/tests/unit_tests/physics/test_homogeneous_nucleation_rates.py +++ b/tests/unit_tests/physics/test_homogeneous_nucleation_rates.py @@ -7,7 +7,7 @@ from PySDM.formulae import _choices, Formulae -class TestHomogeneousIceNucleationRate: +class TestHomogeneousIceNucleationRate: # pylint: disable=too-few-public-methods @staticmethod @pytest.mark.parametrize( "da_w_ice, expected_value", From da147d82913a115b2950aee94c10ce3d156fd1b4 Mon Sep 17 00:00:00 2001 From: Tim Luettmer Date: Fri, 16 May 2025 14:28:18 +0200 Subject: [PATCH 35/57] pydoc didnt like my sorting of usage urls and adressing pylint --- .../backends/impl_numba/methods/freezing_methods.py | 5 +++-- PySDM/dynamics/freezing.py | 6 +++--- PySDM/formulae.py | 2 +- .../physics/homogeneous_ice_nucleation_rate/koop.py | 2 +- .../homogeneous_ice_nucleation_rate/koop_corr.py | 2 +- .../homogeneous_ice_nucleation_rate/koop_murray.py | 2 +- docs/bibliography.json | 4 ++-- tests/unit_tests/dynamics/test_freezing.py | 12 ++++++------ .../physics/test_homogeneous_nucleation_rates.py | 2 +- 9 files changed, 19 insertions(+), 18 deletions(-) diff --git a/PySDM/backends/impl_numba/methods/freezing_methods.py b/PySDM/backends/impl_numba/methods/freezing_methods.py index 9b8f5b567f..99f41017a3 100644 --- a/PySDM/backends/impl_numba/methods/freezing_methods.py +++ b/PySDM/backends/impl_numba/methods/freezing_methods.py @@ -1,5 +1,6 @@ """ -CPU implementation of backend methods for homogeneous freezing and heterogeneous freezing (singular and time-dependent immersion freezing) +CPU implementation of backend methods for homogeneous freezing and +heterogeneous freezing (singular and time-dependent immersion freezing) """ from functools import cached_property @@ -121,7 +122,7 @@ def freeze_time_dependent_homogeneous_body( # pylint: disable=unused-argument,t d_a_w_ice = (relative_humidity_ice[cell_id] - 1.0) * a_w_ice[ cell_id ] - if d_a_w_ice > 0.23 and d_a_w_ice < 0.34: + if 0.23 < d_a_w_ice < 0.34: rate_assuming_constant_temperature_within_dt = ( j_hom(temperature[cell_id], d_a_w_ice) * attributes.volume[i] diff --git a/PySDM/dynamics/freezing.py b/PySDM/dynamics/freezing.py index 72f41b5e78..156478c94a 100644 --- a/PySDM/dynamics/freezing.py +++ b/PySDM/dynamics/freezing.py @@ -1,5 +1,6 @@ """ -droplet freezing using either singular or time-dependent formulation for immersion freezing +droplet freezing using either singular or +time-dependent formulation for immersion freezing and homogeneous freezing and thaw """ @@ -7,7 +8,7 @@ @register_dynamic() -class Freezing: +class Freezing: # pylint: disable=too-many-instance-attributes def __init__( self, *, @@ -19,7 +20,6 @@ def __init__( self.singular = singular self.homogeneous_freezing = homogeneous_freezing self.immersion_freezing = immersion_freezing - # self.record_freezing_temperature = record_freezing_temperature self.thaw = thaw self.enable = True self.rand = None diff --git a/PySDM/formulae.py b/PySDM/formulae.py index 706c3c3977..bb8c7fe1e8 100644 --- a/PySDM/formulae.py +++ b/PySDM/formulae.py @@ -23,7 +23,7 @@ from PySDM.dynamics.terminal_velocity.gunn_and_kinzer import TpDependent -class Formulae: # pylint: disable=too-few-public-methods,too-many-instance-attributes +class Formulae: # pylint: disable=too-few-public-methods,too-many-instance-attributes,too-many-statements def __init__( # pylint: disable=too-many-locals self, *, diff --git a/PySDM/physics/homogeneous_ice_nucleation_rate/koop.py b/PySDM/physics/homogeneous_ice_nucleation_rate/koop.py index ddca06e8f0..683af46a09 100644 --- a/PySDM/physics/homogeneous_ice_nucleation_rate/koop.py +++ b/PySDM/physics/homogeneous_ice_nucleation_rate/koop.py @@ -10,7 +10,7 @@ def __init__(self, const): pass @staticmethod - def j_hom(const, T, da_w_ice): + def j_hom(const, T, da_w_ice): # pylint: disable=unused-argument return ( 10 ** ( diff --git a/PySDM/physics/homogeneous_ice_nucleation_rate/koop_corr.py b/PySDM/physics/homogeneous_ice_nucleation_rate/koop_corr.py index b3675df0c1..1af52074ae 100644 --- a/PySDM/physics/homogeneous_ice_nucleation_rate/koop_corr.py +++ b/PySDM/physics/homogeneous_ice_nucleation_rate/koop_corr.py @@ -11,7 +11,7 @@ def __init__(self, const): pass @staticmethod - def j_hom(const, T, da_w_ice): + def j_hom(const, T, da_w_ice): # pylint: disable=unused-argument return ( 10 ** ( diff --git a/PySDM/physics/homogeneous_ice_nucleation_rate/koop_murray.py b/PySDM/physics/homogeneous_ice_nucleation_rate/koop_murray.py index 4c53ad313c..d6f0822ede 100644 --- a/PySDM/physics/homogeneous_ice_nucleation_rate/koop_murray.py +++ b/PySDM/physics/homogeneous_ice_nucleation_rate/koop_murray.py @@ -10,7 +10,7 @@ def __init__(self, const): pass @staticmethod - def j_hom(const, T, da_w_ice): + def j_hom(const, T, da_w_ice): # pylint: disable=unused-argument T_diff = T - const.T_tri return ( 10 diff --git a/docs/bibliography.json b/docs/bibliography.json index 4a7fb6084f..285c3b04c0 100644 --- a/docs/bibliography.json +++ b/docs/bibliography.json @@ -866,13 +866,13 @@ "https://doi.org/10.5194/acp-23-2035-2023": { "usages": [ "PySDM/physics/constants_defaults.py", + "PySDM/physics/homogeneous_ice_nucleation_rate/koop_corr.py", "examples/PySDM_examples/Spichtinger_etal_2023/fig_B1.ipynb", "examples/PySDM_examples/Spichtinger_etal_2023/__init__.py", "examples/PySDM_examples/Spichtinger_etal_2023/data/reference_bulk.py", - "PySDM/physics/homogeneous_ice_nucleation_rate/koop_corr.py", "tests/unit_tests/physics/test_homogeneous_nucleation_rates.py" ], - "title": "Impact of formulations of the homogeneous nucleation\nrate on ice nucleation events in cirrus", + "title": "Impact of formulations of the homogeneous nucleation rate on ice nucleation events in cirrus", "label": "Spichtinger et al. 2023 (Atmos. Chem. Phys. 23)" }, "https://doi.org/10.1038/35020537": { diff --git a/tests/unit_tests/dynamics/test_freezing.py b/tests/unit_tests/dynamics/test_freezing.py index 237de697b9..e91f7883fb 100644 --- a/tests/unit_tests/dynamics/test_freezing.py +++ b/tests/unit_tests/dynamics/test_freezing.py @@ -96,15 +96,15 @@ def test_thaw(backend_class, freezing_type, thaw, epsilon): singular = False immersion_freezing = True homogeneous_freezing = False - if freezing_type is "het_singular": + if freezing_type == "het_singular": freezing_parameter = {} singular = True - elif freezing_type is "het_time_dependent": + elif freezing_type =="het_time_dependent": freezing_parameter = { "heterogeneous_ice_nucleation_rate": "Constant", "constants": {"J_HET": 0}, } - elif freezing_type is "hom_time_dependent": + elif freezing_type =="hom_time_dependent": freezing_parameter = { "homogeneous_ice_nucleation_rate": "Constant", "constants": {"J_HOM": 0}, @@ -202,7 +202,7 @@ def test_immersion_freezing_singular(backend_class): @pytest.mark.parametrize( "freezing_type", ("het_time_dependent", "hom_time_dependent") ) - # pylint: disable=too-many-locals + # pylint: disable=too-many-locals,too-many-statements def test_freezing_time_dependent( backend_class, freezing_type, double_precision, plot=False ): @@ -240,12 +240,12 @@ def low(t): immersion_freezing = True homogeneous_freezing = False - if freezing_type is "het_time_dependent": + if freezing_type == "het_time_dependent": freezing_parameter = { "heterogeneous_ice_nucleation_rate": "Constant", "constants": {"J_HET": rate / immersed_surface_area}, } - elif freezing_type is "hom_time_dependent": + elif freezing_type == "hom_time_dependent": freezing_parameter = { "homogeneous_ice_nucleation_rate": "Constant", "constants": {"J_HOM": rate / droplet_volume}, diff --git a/tests/unit_tests/physics/test_homogeneous_nucleation_rates.py b/tests/unit_tests/physics/test_homogeneous_nucleation_rates.py index d3dc536855..f6c5e68303 100644 --- a/tests/unit_tests/physics/test_homogeneous_nucleation_rates.py +++ b/tests/unit_tests/physics/test_homogeneous_nucleation_rates.py @@ -4,7 +4,7 @@ import pytest import numpy as np -from PySDM.formulae import _choices, Formulae +from PySDM.formulae import Formulae class TestHomogeneousIceNucleationRate: # pylint: disable=too-few-public-methods From 012081b5b6fda7862f098df77b2338248fce5bea Mon Sep 17 00:00:00 2001 From: Tim Luettmer Date: Fri, 16 May 2025 14:47:28 +0200 Subject: [PATCH 36/57] maybe pdoc and pylint are happy now? --- PySDM/dynamics/freezing.py | 2 +- PySDM/physics/homogeneous_ice_nucleation_rate/koop.py | 2 +- PySDM/physics/homogeneous_ice_nucleation_rate/koop_corr.py | 2 +- PySDM/physics/homogeneous_ice_nucleation_rate/koop_murray.py | 2 +- docs/bibliography.json | 2 +- examples/PySDM_examples/Spichtinger_et_al_2023/simulation.py | 3 +-- tests/unit_tests/dynamics/test_freezing.py | 4 ++-- 7 files changed, 8 insertions(+), 9 deletions(-) diff --git a/PySDM/dynamics/freezing.py b/PySDM/dynamics/freezing.py index 156478c94a..94084e0c5f 100644 --- a/PySDM/dynamics/freezing.py +++ b/PySDM/dynamics/freezing.py @@ -8,7 +8,7 @@ @register_dynamic() -class Freezing: # pylint: disable=too-many-instance-attributes +class Freezing: # pylint: disable=too-many-instance-attributes def __init__( self, *, diff --git a/PySDM/physics/homogeneous_ice_nucleation_rate/koop.py b/PySDM/physics/homogeneous_ice_nucleation_rate/koop.py index 683af46a09..d25742dddb 100644 --- a/PySDM/physics/homogeneous_ice_nucleation_rate/koop.py +++ b/PySDM/physics/homogeneous_ice_nucleation_rate/koop.py @@ -10,7 +10,7 @@ def __init__(self, const): pass @staticmethod - def j_hom(const, T, da_w_ice): # pylint: disable=unused-argument + def j_hom(const, T, da_w_ice): # pylint: disable=unused-argument return ( 10 ** ( diff --git a/PySDM/physics/homogeneous_ice_nucleation_rate/koop_corr.py b/PySDM/physics/homogeneous_ice_nucleation_rate/koop_corr.py index 1af52074ae..4de3f3d880 100644 --- a/PySDM/physics/homogeneous_ice_nucleation_rate/koop_corr.py +++ b/PySDM/physics/homogeneous_ice_nucleation_rate/koop_corr.py @@ -11,7 +11,7 @@ def __init__(self, const): pass @staticmethod - def j_hom(const, T, da_w_ice): # pylint: disable=unused-argument + def j_hom(const, T, da_w_ice): # pylint: disable=unused-argument return ( 10 ** ( diff --git a/PySDM/physics/homogeneous_ice_nucleation_rate/koop_murray.py b/PySDM/physics/homogeneous_ice_nucleation_rate/koop_murray.py index d6f0822ede..8eb341f013 100644 --- a/PySDM/physics/homogeneous_ice_nucleation_rate/koop_murray.py +++ b/PySDM/physics/homogeneous_ice_nucleation_rate/koop_murray.py @@ -10,7 +10,7 @@ def __init__(self, const): pass @staticmethod - def j_hom(const, T, da_w_ice): # pylint: disable=unused-argument + def j_hom(const, T, da_w_ice): # pylint: disable=unused-argument T_diff = T - const.T_tri return ( 10 diff --git a/docs/bibliography.json b/docs/bibliography.json index 285c3b04c0..abef537fae 100644 --- a/docs/bibliography.json +++ b/docs/bibliography.json @@ -867,9 +867,9 @@ "usages": [ "PySDM/physics/constants_defaults.py", "PySDM/physics/homogeneous_ice_nucleation_rate/koop_corr.py", - "examples/PySDM_examples/Spichtinger_etal_2023/fig_B1.ipynb", "examples/PySDM_examples/Spichtinger_etal_2023/__init__.py", "examples/PySDM_examples/Spichtinger_etal_2023/data/reference_bulk.py", + "examples/PySDM_examples/Spichtinger_etal_2023/fig_B1.ipynb", "tests/unit_tests/physics/test_homogeneous_nucleation_rates.py" ], "title": "Impact of formulations of the homogeneous nucleation rate on ice nucleation events in cirrus", diff --git a/examples/PySDM_examples/Spichtinger_et_al_2023/simulation.py b/examples/PySDM_examples/Spichtinger_et_al_2023/simulation.py index 3623cde4c3..c67db1ebbf 100644 --- a/examples/PySDM_examples/Spichtinger_et_al_2023/simulation.py +++ b/examples/PySDM_examples/Spichtinger_et_al_2023/simulation.py @@ -117,7 +117,6 @@ def run(self): if dRHi > 0.0 and RHi < 130.0: print("break") break - else: - RHi_old = RHi + RHi_old = RHi return output["ni"][-1] diff --git a/tests/unit_tests/dynamics/test_freezing.py b/tests/unit_tests/dynamics/test_freezing.py index e91f7883fb..ba46790dd7 100644 --- a/tests/unit_tests/dynamics/test_freezing.py +++ b/tests/unit_tests/dynamics/test_freezing.py @@ -99,12 +99,12 @@ def test_thaw(backend_class, freezing_type, thaw, epsilon): if freezing_type == "het_singular": freezing_parameter = {} singular = True - elif freezing_type =="het_time_dependent": + elif freezing_type == "het_time_dependent": freezing_parameter = { "heterogeneous_ice_nucleation_rate": "Constant", "constants": {"J_HET": 0}, } - elif freezing_type =="hom_time_dependent": + elif freezing_type == "hom_time_dependent": freezing_parameter = { "homogeneous_ice_nucleation_rate": "Constant", "constants": {"J_HOM": 0}, From 13f3ebb882fc597e1280b03537d670b552c43047 Mon Sep 17 00:00:00 2001 From: Sylwester Arabas Date: Sun, 18 May 2025 23:42:08 +0200 Subject: [PATCH 37/57] fix bib entry ("et_al" instead of "etal" in directory name) --- docs/bibliography.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/bibliography.json b/docs/bibliography.json index abef537fae..9e0e10a43d 100644 --- a/docs/bibliography.json +++ b/docs/bibliography.json @@ -867,9 +867,9 @@ "usages": [ "PySDM/physics/constants_defaults.py", "PySDM/physics/homogeneous_ice_nucleation_rate/koop_corr.py", - "examples/PySDM_examples/Spichtinger_etal_2023/__init__.py", - "examples/PySDM_examples/Spichtinger_etal_2023/data/reference_bulk.py", - "examples/PySDM_examples/Spichtinger_etal_2023/fig_B1.ipynb", + "examples/PySDM_examples/Spichtinger_et_al_2023/__init__.py", + "examples/PySDM_examples/Spichtinger_et_al_2023/data/reference_bulk.py", + "examples/PySDM_examples/Spichtinger_et_al_2023/fig_B1.ipynb", "tests/unit_tests/physics/test_homogeneous_nucleation_rates.py" ], "title": "Impact of formulations of the homogeneous nucleation rate on ice nucleation events in cirrus", From f1213c29ec30a3037a38849c89229fc9a61adf2f Mon Sep 17 00:00:00 2001 From: Sylwester Arabas Date: Sun, 18 May 2025 23:42:21 +0200 Subject: [PATCH 38/57] call super class ctor --- examples/PySDM_examples/Spichtinger_et_al_2023/simulation.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/PySDM_examples/Spichtinger_et_al_2023/simulation.py b/examples/PySDM_examples/Spichtinger_et_al_2023/simulation.py index c67db1ebbf..f83eb979eb 100644 --- a/examples/PySDM_examples/Spichtinger_et_al_2023/simulation.py +++ b/examples/PySDM_examples/Spichtinger_et_al_2023/simulation.py @@ -85,10 +85,9 @@ def __init__(self, settings, backend=CPU): ), ] - self.particulator = builder.build(attributes, products) - self.n_output = settings.n_output self.n_substeps = int(settings.t_duration / dt / self.n_output) + super().__init__(builder.build(attributes, products)) def save(self, output): cell_id = 0 From b35509a56448c8589ce9b9094b8520674bfa8ddb Mon Sep 17 00:00:00 2001 From: Sylwester Arabas Date: Mon, 19 May 2025 00:10:44 +0200 Subject: [PATCH 39/57] address pylint hints --- .../Spichtinger_et_al_2023/data/__init__.py | 0 .../PySDM_examples/Spichtinger_et_al_2023/fig_B1.ipynb | 8 ++++---- 2 files changed, 4 insertions(+), 4 deletions(-) create mode 100644 examples/PySDM_examples/Spichtinger_et_al_2023/data/__init__.py diff --git a/examples/PySDM_examples/Spichtinger_et_al_2023/data/__init__.py b/examples/PySDM_examples/Spichtinger_et_al_2023/data/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/PySDM_examples/Spichtinger_et_al_2023/fig_B1.ipynb b/examples/PySDM_examples/Spichtinger_et_al_2023/fig_B1.ipynb index 1f6c2abcbf..67b2e82fa3 100644 --- a/examples/PySDM_examples/Spichtinger_et_al_2023/fig_B1.ipynb +++ b/examples/PySDM_examples/Spichtinger_et_al_2023/fig_B1.ipynb @@ -118,7 +118,7 @@ " \"T\": initial_temperatures.tolist(),\n", " \"w\": updrafts.tolist(),\n", " }\n", - " with open(file_name, \"w\") as file:\n", + " with open(file_name, \"w\", encoding=\"utf-8\") as file:\n", " json.dump(data_file, file)" ], "id": "1acd9d93e2af385c", @@ -141,7 +141,7 @@ "else:\n", " if read_from_json:\n", " file_name = \"data/ni_w_T_ens_5.json\"\n", - " with open(file_name, \"r\") as f:\n", + " with open(file_name, \"r\", encoding=\"utf-8\") as f:\n", " data = json.load(f)\n", "\n", " ni = data[\"ni\"]\n", @@ -157,8 +157,8 @@ "# plot\n", "fig, ax = pyplot.subplots(1, 1, figsize=(5, 5))\n", "\n", - "for j in range(len(T)):\n", - " ax.scatter(w, ni_ens_mean[:, j], label=f\"T0={T[j]:.0f}K\")\n", + "for j, Tj in enumerate(T):\n", + " ax.scatter(w, ni_ens_mean[:, j], label=f\"T0={Tj:.0f}K\")\n", " ax.plot(w_bulk_ref, ni_bulk_ref[j, :], linestyle=\"dashed\")\n", "\n", "ax.set_xscale(\"log\")\n", From 3bbc7ddfd7cdb5ca6fff0867cc809b023eb9af0a Mon Sep 17 00:00:00 2001 From: Sylwester Arabas Date: Mon, 19 May 2025 00:17:40 +0200 Subject: [PATCH 40/57] import fix --- .../PySDM_examples/Spichtinger_et_al_2023/fig_B1.ipynb | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/examples/PySDM_examples/Spichtinger_et_al_2023/fig_B1.ipynb b/examples/PySDM_examples/Spichtinger_et_al_2023/fig_B1.ipynb index 67b2e82fa3..1788e75e43 100644 --- a/examples/PySDM_examples/Spichtinger_et_al_2023/fig_B1.ipynb +++ b/examples/PySDM_examples/Spichtinger_et_al_2023/fig_B1.ipynb @@ -43,11 +43,8 @@ "cell_type": "code", "source": [ "import json\n", - "from examples.PySDM_examples.Spichtinger_et_al_2023 import Simulation, Settings\n", - "from examples.PySDM_examples.Spichtinger_et_al_2023.data import (\n", - " simulation_data,\n", - " reference_bulk,\n", - ")\n", + "from PySDM_examples.Spichtinger_et_al_2023 import Simulation, Settings\n", + "from PySDM_examples.Spichtinger_et_al_2023.data import simulation_data, reference_bulk\n", "import numpy as np\n", "from matplotlib import pyplot\n", "from open_atmos_jupyter_utils import show_plot" From e922170fbab60fe111d2dac4b62d8d9a2c9e5668 Mon Sep 17 00:00:00 2001 From: Tim Luettmer Date: Mon, 19 May 2025 11:13:59 +0200 Subject: [PATCH 41/57] Removed .json from git ignore --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index d339037ef0..4234ec2217 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,6 @@ PySDM_examples/utils/temporary_files/* *.svg *.gif *.ipynb.badges.md -*.json # Byte-compiled / optimized / DLL files __pycache__/ From 822c5c0fa89ab9c309b297ccc7f0d3d1c7fb8f81 Mon Sep 17 00:00:00 2001 From: Tim Luettmer Date: Mon, 19 May 2025 12:10:55 +0200 Subject: [PATCH 42/57] reformatting setting function call --- .../Spichtinger_et_al_2023/fig_B1.ipynb | 34 ++++++++++++------- .../Spichtinger_et_al_2023/settings.py | 2 -- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/examples/PySDM_examples/Spichtinger_et_al_2023/fig_B1.ipynb b/examples/PySDM_examples/Spichtinger_et_al_2023/fig_B1.ipynb index 1788e75e43..e0ffc971e6 100644 --- a/examples/PySDM_examples/Spichtinger_et_al_2023/fig_B1.ipynb +++ b/examples/PySDM_examples/Spichtinger_et_al_2023/fig_B1.ipynb @@ -73,16 +73,14 @@ { "metadata": { "ExecuteTime": { - "end_time": "2025-05-15T14:20:46.432048Z", - "start_time": "2025-05-15T14:20:46.320842Z" + "end_time": "2025-05-19T10:08:33.165481Z", + "start_time": "2025-05-19T10:08:32.903294Z" } }, "cell_type": "code", "source": [ "if calculate_data:\n", "\n", - " general_settings = {\"n_sd\": 50000, \"dt\": 0.1}\n", - "\n", " initial_temperatures = np.array([196.0, 216.0, 236.0])\n", " updrafts = np.array([0.01, 0.03, 0.05, 0.1, 0.3, 0.5, 1.0, 3.0, 5.0])\n", " number_of_ensemble_runs = 5\n", @@ -98,13 +96,11 @@ " for i in range(dim_updrafts):\n", " for j in range(dim_initial_temperatures):\n", " for k in range(number_of_ensemble_runs):\n", - " setting_dictionary = {\n", - " **general_settings,\n", - " }\n", - " setting_dictionary[\"w_updraft\"] = updrafts[i]\n", - " setting_dictionary[\"T0\"] = initial_temperatures[j]\n", - " setting_dictionary[\"seed\"] = seeds[k]\n", - " setting = Settings(**setting_dictionary)\n", + " setting = Settings(n_sd=50000,\n", + " w_updraft=updrafts[i],\n", + " T0=initial_temperatures[j],\n", + " seed=seeds[k],\n", + " dt=0.1)\n", " model = Simulation(setting)\n", " number_concentration_ice[i, j, k] = model.run()\n", "\n", @@ -119,8 +115,20 @@ " json.dump(data_file, file)" ], "id": "1acd9d93e2af385c", - "outputs": [], - "execution_count": 8 + "outputs": [ + { + "ename": "NameError", + "evalue": "name 'calculate_data' is not defined", + "output_type": "error", + "traceback": [ + "\u001B[31m---------------------------------------------------------------------------\u001B[39m", + "\u001B[31mNameError\u001B[39m Traceback (most recent call last)", + "\u001B[36mCell\u001B[39m\u001B[36m \u001B[39m\u001B[32mIn[1]\u001B[39m\u001B[32m, line 1\u001B[39m\n\u001B[32m----> \u001B[39m\u001B[32m1\u001B[39m \u001B[38;5;28;01mif\u001B[39;00m \u001B[43mcalculate_data\u001B[49m:\n\u001B[32m 2\u001B[39m \n\u001B[32m 3\u001B[39m \u001B[38;5;66;03m# general_settings = {\"n_sd\": 50000, \"dt\": 0.1}\u001B[39;00m\n\u001B[32m 5\u001B[39m initial_temperatures = np.array([\u001B[32m196.0\u001B[39m, \u001B[32m216.0\u001B[39m, \u001B[32m236.0\u001B[39m])\n\u001B[32m 6\u001B[39m updrafts = np.array([\u001B[32m0.01\u001B[39m, \u001B[32m0.03\u001B[39m, \u001B[32m0.05\u001B[39m, \u001B[32m0.1\u001B[39m, \u001B[32m0.3\u001B[39m, \u001B[32m0.5\u001B[39m, \u001B[32m1.0\u001B[39m, \u001B[32m3.0\u001B[39m, \u001B[32m5.0\u001B[39m])\n", + "\u001B[31mNameError\u001B[39m: name 'calculate_data' is not defined" + ] + } + ], + "execution_count": 1 }, { "metadata": { diff --git a/examples/PySDM_examples/Spichtinger_et_al_2023/settings.py b/examples/PySDM_examples/Spichtinger_et_al_2023/settings.py index f4ae698f1f..149ccb4fdc 100644 --- a/examples/PySDM_examples/Spichtinger_et_al_2023/settings.py +++ b/examples/PySDM_examples/Spichtinger_et_al_2023/settings.py @@ -18,8 +18,6 @@ def __init__( dt: float, ): - print(f"{w_updraft:},{T0:},{seed:},{dt:}") - self.n_sd = n_sd self.w_updraft = w_updraft From 4593b99746f4d2c605bb96d970938aef889330bb Mon Sep 17 00:00:00 2001 From: Tim Luettmer Date: Mon, 26 May 2025 12:12:24 +0200 Subject: [PATCH 43/57] Added Spichtinger_et_al_2023 to example_tests --- tests/examples_tests/conftest.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/examples_tests/conftest.py b/tests/examples_tests/conftest.py index 55e918ce53..70094d91c4 100644 --- a/tests/examples_tests/conftest.py +++ b/tests/examples_tests/conftest.py @@ -25,6 +25,7 @@ def findfiles(path, regex): "Alpert_and_Knopf_2016", "Ervens_and_Feingold_2012", "Niedermeier_et_al_2014", + "Spichtinger_et_al_2023", ], "isotopes": [ "Bolot_et_al_2013", From f25fe026997c4f3c7ed55b31e562aa578ea736cd Mon Sep 17 00:00:00 2001 From: Sylwester Arabas Date: Mon, 26 May 2025 15:54:04 +0200 Subject: [PATCH 44/57] fix units in KOOP_MURRAY constants; add constants unit test; add plotting to existing S. et al. Fig 2 test, return NaN in Null class --- PySDM/physics/__init__.py | 1 + PySDM/physics/constants_defaults.py | 12 +- .../homogeneous_ice_nucleation_rate/null.py | 4 +- .../test_homogeneous_nucleation_rates.py | 113 +++++++++++++++--- 4 files changed, 107 insertions(+), 23 deletions(-) diff --git a/PySDM/physics/__init__.py b/PySDM/physics/__init__.py index af4f997025..63c042d15c 100644 --- a/PySDM/physics/__init__.py +++ b/PySDM/physics/__init__.py @@ -17,6 +17,7 @@ """ from . import ( + dimensional_analysis, diffusion_coordinate, constants_defaults, diffusion_kinetics, diff --git a/PySDM/physics/constants_defaults.py b/PySDM/physics/constants_defaults.py index e1e4dec707..b907d496cf 100644 --- a/PySDM/physics/constants_defaults.py +++ b/PySDM/physics/constants_defaults.py @@ -334,17 +334,17 @@ KOOP_MURRAY_C0 = -3020.684 """ homogeneous ice nucleation rate for pure water droplets ([Koop & Murray 20016](https://doi.org/10.1063/1.4962355)) """ -KOOP_MURRAY_C1 = -425.921 +KOOP_MURRAY_C1 = -425.921 / si.K """ 〃 """ -KOOP_MURRAY_C2 = -25.9779 +KOOP_MURRAY_C2 = -25.9779 / si.K**2 """ 〃 """ -KOOP_MURRAY_C3 = -0.868451 +KOOP_MURRAY_C3 = -0.868451 / si.K**3 """ 〃 """ -KOOP_MURRAY_C4 = -1.66203e-2 +KOOP_MURRAY_C4 = -1.66203e-2 / si.K**4 """ 〃 """ -KOOP_MURRAY_C5 = -1.71736e-4 +KOOP_MURRAY_C5 = -1.71736e-4 / si.K**5 """ 〃 """ -KOOP_MURRAY_C6 = -7.46953e-7 +KOOP_MURRAY_C6 = -7.46953e-7 / si.K**6 """ 〃 """ J_HET = np.nan diff --git a/PySDM/physics/homogeneous_ice_nucleation_rate/null.py b/PySDM/physics/homogeneous_ice_nucleation_rate/null.py index 6fbf21e9ed..8fd96b4d35 100644 --- a/PySDM/physics/homogeneous_ice_nucleation_rate/null.py +++ b/PySDM/physics/homogeneous_ice_nucleation_rate/null.py @@ -3,6 +3,8 @@ to be set before instantiation of Formulae) """ +import numpy as np + class Null: # pylint: disable=too-few-public-methods,unused-argument def __init__(self, _): @@ -10,4 +12,4 @@ def __init__(self, _): @staticmethod def j_hom(const, T, d_a_w_ice): # pylint: disable=unused-argument - return 0 + return np.nan diff --git a/tests/unit_tests/physics/test_homogeneous_nucleation_rates.py b/tests/unit_tests/physics/test_homogeneous_nucleation_rates.py index f6c5e68303..7d2ad9ebaa 100644 --- a/tests/unit_tests/physics/test_homogeneous_nucleation_rates.py +++ b/tests/unit_tests/physics/test_homogeneous_nucleation_rates.py @@ -2,24 +2,50 @@ test for homogeneous nucleation rate parametrisations """ +from contextlib import nullcontext +import re import pytest +from matplotlib import pyplot import numpy as np -from PySDM.formulae import Formulae +from PySDM.formulae import Formulae, _choices +from PySDM.physics import homogeneous_ice_nucleation_rate +from PySDM import physics +SPICHTINGER_ET_AL_2023_FIG2_DATA = { + "da_w_ice": [0.27, 0.29, 0.31, 0.33], + "jhom_log10": [5, 11, 15, 20], +} -class TestHomogeneousIceNucleationRate: # pylint: disable=too-few-public-methods + +class TestHomogeneousIceNucleationRate: @staticmethod @pytest.mark.parametrize( - "da_w_ice, expected_value", + "index", range(len(SPICHTINGER_ET_AL_2023_FIG2_DATA["da_w_ice"])) + ) + @pytest.mark.parametrize( + "parametrisation, context", ( - (0.27, (5)), - (0.29, (11)), - (0.31, (15)), - (0.33, (20)), + ("Koop_Correction", nullcontext()), + ( + "Koop2000", + pytest.raises( + AssertionError, match="Items are not equal to 2 significant digits" + ), + ), + ( + "KoopMurray2016", + pytest.raises( + ValueError, + match=re.escape( + "x and y must have same first dimension, but have shapes (4,) and (1,)" + ), + ), + ), ), ) - @pytest.mark.parametrize("parametrisation", ("Koop_Correction",)) - def test_homogeneous_ice_nucleation_rate(da_w_ice, expected_value, parametrisation): + def test_fig_2_in_spichtinger_et_al_2023( + index, parametrisation, context, plot=False + ): """Fig. 2 in [Spichtinger et al. 2023](https://doi.org/10.5194/acp-23-2035-2023)""" # arrange formulae = Formulae( @@ -27,11 +53,66 @@ def test_homogeneous_ice_nucleation_rate(da_w_ice, expected_value, parametrisati ) # act - jhom_log10 = np.log10( - formulae.homogeneous_ice_nucleation_rate.j_hom(np.nan, da_w_ice) - ) + with context: + jhom_log10 = np.log10( + formulae.homogeneous_ice_nucleation_rate.j_hom( + np.nan, np.asarray(SPICHTINGER_ET_AL_2023_FIG2_DATA["da_w_ice"]) + ) + ) - # assert - np.testing.assert_approx_equal( - actual=jhom_log10, desired=expected_value, significant=2 - ) + # plot + pyplot.scatter( + x=[SPICHTINGER_ET_AL_2023_FIG2_DATA["da_w_ice"][index]], + y=[SPICHTINGER_ET_AL_2023_FIG2_DATA["jhom_log10"][index]], + color="red", + marker="x", + ) + pyplot.plot( + SPICHTINGER_ET_AL_2023_FIG2_DATA["da_w_ice"], + jhom_log10, + marker=".", + ) + pyplot.gca().set( + xlabel=r"water activity difference $\Delta a_w$", + ylabel="log$_{10}(J)$", + title=parametrisation, + xlim=(0.26, 0.34), + ylim=(0, 25), + ) + pyplot.grid() + if plot: + pyplot.show() + else: + pyplot.clf() + + # assert + np.testing.assert_approx_equal( + actual=jhom_log10[index], + desired=SPICHTINGER_ET_AL_2023_FIG2_DATA["jhom_log10"][index], + significant=2, + ) + + @staticmethod + @pytest.mark.parametrize("variant", _choices(homogeneous_ice_nucleation_rate)) + def test_units(variant): + if variant == "Null": + pytest.skip() + + with physics.dimensional_analysis.DimensionalAnalysis(): + # arrange + si = physics.si + formulae = Formulae( + homogeneous_ice_nucleation_rate=variant, + constants=( + {} if variant != "Constant" else {"J_HOM": 1 / si.m**3 / si.s} + ), + ) + sut = formulae.homogeneous_ice_nucleation_rate + temperature = 250 * si.K + da_w_ice = 0.3 * si.dimensionless + + # act + value = sut.j_hom(temperature, da_w_ice) + + # assert + assert value.check("1/[volume]/[time]") From ad5c3181256d5ee0bce7f46c5535fac104405bcc Mon Sep 17 00:00:00 2001 From: Sylwester Arabas Date: Mon, 9 Jun 2025 13:09:25 +0200 Subject: [PATCH 45/57] resolve cyclic import --- PySDM/physics/__init__.py | 1 - tests/unit_tests/physics/test_homogeneous_nucleation_rates.py | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/PySDM/physics/__init__.py b/PySDM/physics/__init__.py index 482e034920..f2e2bfb43f 100644 --- a/PySDM/physics/__init__.py +++ b/PySDM/physics/__init__.py @@ -17,7 +17,6 @@ """ from . import ( - dimensional_analysis, diffusion_coordinate, constants_defaults, diffusion_kinetics, diff --git a/tests/unit_tests/physics/test_homogeneous_nucleation_rates.py b/tests/unit_tests/physics/test_homogeneous_nucleation_rates.py index 7d2ad9ebaa..df148d500f 100644 --- a/tests/unit_tests/physics/test_homogeneous_nucleation_rates.py +++ b/tests/unit_tests/physics/test_homogeneous_nucleation_rates.py @@ -10,6 +10,7 @@ from PySDM.formulae import Formulae, _choices from PySDM.physics import homogeneous_ice_nucleation_rate from PySDM import physics +from PySDM.physics.dimensional_analysis import DimensionalAnalysis SPICHTINGER_ET_AL_2023_FIG2_DATA = { "da_w_ice": [0.27, 0.29, 0.31, 0.33], @@ -98,7 +99,7 @@ def test_units(variant): if variant == "Null": pytest.skip() - with physics.dimensional_analysis.DimensionalAnalysis(): + with DimensionalAnalysis(): # arrange si = physics.si formulae = Formulae( From be339ad5fd50c53fc25747eb6db8643129e26844 Mon Sep 17 00:00:00 2001 From: Sylwester Arabas Date: Mon, 9 Jun 2025 13:39:38 +0200 Subject: [PATCH 46/57] make Koop Murray formula a one-liner (for GPU), fix T_tri->T0 (same in room temp definition...) --- PySDM/physics/constants_defaults.py | 2 +- .../koop_murray.py | 15 +++++++-------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/PySDM/physics/constants_defaults.py b/PySDM/physics/constants_defaults.py index 240df3bdda..0e4da7f45d 100644 --- a/PySDM/physics/constants_defaults.py +++ b/PySDM/physics/constants_defaults.py @@ -288,7 +288,7 @@ p_STP = 101325 * si.pascal """ ... and pressure """ -ROOM_TEMP = T_tri + 25 * si.K +ROOM_TEMP = T0 + 25 * si.K """ room temperature """ dT_u = si.K diff --git a/PySDM/physics/homogeneous_ice_nucleation_rate/koop_murray.py b/PySDM/physics/homogeneous_ice_nucleation_rate/koop_murray.py index 8eb341f013..b41748d623 100644 --- a/PySDM/physics/homogeneous_ice_nucleation_rate/koop_murray.py +++ b/PySDM/physics/homogeneous_ice_nucleation_rate/koop_murray.py @@ -1,7 +1,7 @@ """ Koop and Murray homogeneous nucleation rate parameterization for pure water droplets at water saturation - ([Koop and Murray 2016](https://doi.org/10.1063/1.4962355)) +([eq. A9, Tab VII in Koop and Murray 2016](https://doi.org/10.1063/1.4962355)) """ @@ -11,17 +11,16 @@ def __init__(self, const): @staticmethod def j_hom(const, T, da_w_ice): # pylint: disable=unused-argument - T_diff = T - const.T_tri return ( 10 ** ( const.KOOP_MURRAY_C0 - + const.KOOP_MURRAY_C1 * T_diff - + const.KOOP_MURRAY_C2 * T_diff**2.0 - + const.KOOP_MURRAY_C3 * T_diff**3.0 - + const.KOOP_MURRAY_C4 * T_diff**4.0 - + const.KOOP_MURRAY_C5 * T_diff**5.0 - + const.KOOP_MURRAY_C6 * T_diff**6.0 + + const.KOOP_MURRAY_C1 * (T - const.T0) + + const.KOOP_MURRAY_C2 * (T - const.T0) ** 2.0 + + const.KOOP_MURRAY_C3 * (T - const.T0) ** 3.0 + + const.KOOP_MURRAY_C4 * (T - const.T0) ** 4.0 + + const.KOOP_MURRAY_C5 * (T - const.T0) ** 5.0 + + const.KOOP_MURRAY_C6 * (T - const.T0) ** 6.0 ) * const.KOOP_UNIT ) From 05620886f491ab69c440452f18a0b65287b6009b Mon Sep 17 00:00:00 2001 From: Sylwester Arabas Date: Mon, 9 Jun 2025 13:58:32 +0200 Subject: [PATCH 47/57] regenerate notebook (contained error messages) --- .../Spichtinger_et_al_2023/fig_B1.ipynb | 1908 ++++++++++++++++- 1 file changed, 1840 insertions(+), 68 deletions(-) diff --git a/examples/PySDM_examples/Spichtinger_et_al_2023/fig_B1.ipynb b/examples/PySDM_examples/Spichtinger_et_al_2023/fig_B1.ipynb index e0ffc971e6..3dad57dcce 100644 --- a/examples/PySDM_examples/Spichtinger_et_al_2023/fig_B1.ipynb +++ b/examples/PySDM_examples/Spichtinger_et_al_2023/fig_B1.ipynb @@ -1,8 +1,9 @@ { "cells": [ { - "metadata": {}, "cell_type": "markdown", + "id": "a6b09eaef75333df", + "metadata": {}, "source": [ "\n", "#### based on Fig. B1 from Spichtinger et al. 2023 (ACP) \"_Impact of formulations of the homogeneous nucleation rate on ice nucleation events in cirrus_\"\n", @@ -10,37 +11,38 @@ "(work in progress)\n", "\n", "https://doi.org/10.5194/acp-23-2035-2023" - ], - "id": "a6b09eaef75333df" + ] }, { "cell_type": "code", + "execution_count": 1, "id": "initial_id", "metadata": { - "collapsed": true, "ExecuteTime": { "end_time": "2025-05-15T14:20:31.231964Z", "start_time": "2025-05-15T14:20:31.221457Z" } }, + "outputs": [], "source": [ "import sys\n", "if 'google.colab' in sys.modules:\n", " !pip --quiet install open-atmos-jupyter-utils\n", " from open_atmos_jupyter_utils import pip_install_on_colab\n", " pip_install_on_colab('PySDM-examples')" - ], - "outputs": [], - "execution_count": 5 + ] }, { + "cell_type": "code", + "execution_count": 2, + "id": "69ce798ec8b87121", "metadata": { "ExecuteTime": { "end_time": "2025-05-15T14:32:48.496132Z", "start_time": "2025-05-15T14:32:47.161494Z" } }, - "cell_type": "code", + "outputs": [], "source": [ "import json\n", "from PySDM_examples.Spichtinger_et_al_2023 import Simulation, Settings\n", @@ -48,36 +50,36 @@ "import numpy as np\n", "from matplotlib import pyplot\n", "from open_atmos_jupyter_utils import show_plot" - ], - "id": "69ce798ec8b87121", - "outputs": [], - "execution_count": 11 + ] }, { + "cell_type": "code", + "execution_count": 3, + "id": "fabd7ea8e8a11996", "metadata": { "ExecuteTime": { "end_time": "2025-05-15T14:20:46.119340Z", "start_time": "2025-05-15T14:20:46.116341Z" } }, - "cell_type": "code", + "outputs": [], "source": [ "calculate_data = False\n", "save_to_file = False\n", "read_from_json = False" - ], - "id": "fabd7ea8e8a11996", - "outputs": [], - "execution_count": 7 + ] }, { + "cell_type": "code", + "execution_count": 4, + "id": "1acd9d93e2af385c", "metadata": { "ExecuteTime": { "end_time": "2025-05-19T10:08:33.165481Z", "start_time": "2025-05-19T10:08:32.903294Z" } }, - "cell_type": "code", + "outputs": [], "source": [ "if calculate_data:\n", "\n", @@ -113,31 +115,1829 @@ " }\n", " with open(file_name, \"w\", encoding=\"utf-8\") as file:\n", " json.dump(data_file, file)" - ], - "id": "1acd9d93e2af385c", - "outputs": [ - { - "ename": "NameError", - "evalue": "name 'calculate_data' is not defined", - "output_type": "error", - "traceback": [ - "\u001B[31m---------------------------------------------------------------------------\u001B[39m", - "\u001B[31mNameError\u001B[39m Traceback (most recent call last)", - "\u001B[36mCell\u001B[39m\u001B[36m \u001B[39m\u001B[32mIn[1]\u001B[39m\u001B[32m, line 1\u001B[39m\n\u001B[32m----> \u001B[39m\u001B[32m1\u001B[39m \u001B[38;5;28;01mif\u001B[39;00m \u001B[43mcalculate_data\u001B[49m:\n\u001B[32m 2\u001B[39m \n\u001B[32m 3\u001B[39m \u001B[38;5;66;03m# general_settings = {\"n_sd\": 50000, \"dt\": 0.1}\u001B[39;00m\n\u001B[32m 5\u001B[39m initial_temperatures = np.array([\u001B[32m196.0\u001B[39m, \u001B[32m216.0\u001B[39m, \u001B[32m236.0\u001B[39m])\n\u001B[32m 6\u001B[39m updrafts = np.array([\u001B[32m0.01\u001B[39m, \u001B[32m0.03\u001B[39m, \u001B[32m0.05\u001B[39m, \u001B[32m0.1\u001B[39m, \u001B[32m0.3\u001B[39m, \u001B[32m0.5\u001B[39m, \u001B[32m1.0\u001B[39m, \u001B[32m3.0\u001B[39m, \u001B[32m5.0\u001B[39m])\n", - "\u001B[31mNameError\u001B[39m: name 'calculate_data' is not defined" - ] - } - ], - "execution_count": 1 + ] }, { + "cell_type": "code", + "execution_count": 5, + "id": "3821d0f892f4af29", "metadata": { "ExecuteTime": { "end_time": "2025-05-15T14:32:54.180974Z", "start_time": "2025-05-15T14:32:51.733640Z" } }, - "cell_type": "code", + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " 2025-06-09T13:51:58.962757\n", + " image/svg+xml\n", + " \n", + " \n", + " Matplotlib v3.8.1, https://matplotlib.org/\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "\n" + ], + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "c73995000eb34665bb23298a60ad0134", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "HBox(children=(HTML(value=\"./fig_B1.pdf
\"), HTML(value=\"" - ], - "image/svg+xml": "\n\n\n \n \n \n \n 2025-05-15T16:32:54.045692\n image/svg+xml\n \n \n Matplotlib v3.10.1, https://matplotlib.org/\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n" - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/plain": [ - "HBox(children=(HTML(value=\"./fig_B1.pdf
\"), HTML(value=\" Date: Mon, 9 Jun 2025 14:38:08 +0200 Subject: [PATCH 48/57] undo chenge to room temperature --- PySDM/physics/constants_defaults.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PySDM/physics/constants_defaults.py b/PySDM/physics/constants_defaults.py index 0e4da7f45d..240df3bdda 100644 --- a/PySDM/physics/constants_defaults.py +++ b/PySDM/physics/constants_defaults.py @@ -288,7 +288,7 @@ p_STP = 101325 * si.pascal """ ... and pressure """ -ROOM_TEMP = T0 + 25 * si.K +ROOM_TEMP = T_tri + 25 * si.K """ room temperature """ dT_u = si.K From 021f945f5963e2aa5330ddc6ae8fb17a21ece051 Mon Sep 17 00:00:00 2001 From: Sylwester Arabas Date: Mon, 9 Jun 2025 15:33:09 +0200 Subject: [PATCH 49/57] add mention do examples docs landing site --- examples/docs/pysdm_examples_landing.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/docs/pysdm_examples_landing.md b/examples/docs/pysdm_examples_landing.md index cb7633ad91..8479131c91 100644 --- a/examples/docs/pysdm_examples_landing.md +++ b/examples/docs/pysdm_examples_landing.md @@ -92,6 +92,8 @@ Example notebooks include: - condensation and aqueous-chemistry - `PySDM_examples.Kreidenweis_et_al_2003`: Hoppel gap simulation setup (i.e. depiction of evolution of aerosol mass spectrum from a monomodal to bimodal due to aqueous‐phase SO2 oxidation) - `PySDM_examples.Jaruga_and_Pawlowska_2018`: exploration of numerical convergence using the above Hoppel-gap simulation setup +- freezing + - `PySDM_examples.Spichtinger_et_al_2023`: homogeneous freezing and ice growth (Wegener-Bergeron-Findeisen process) The parcel environment is also featured in the PySDM tutorials. From 611fc6216a7b4ce81682402760b6301197906b42 Mon Sep 17 00:00:00 2001 From: Sylwester Arabas Date: Sat, 14 Jun 2025 15:33:33 +0200 Subject: [PATCH 50/57] try caching on-demand compiled freezing Numba methods to avoid slowing down unrelated tests --- .../impl_numba/methods/freezing_methods.py | 72 ++++++++++++------- .../test_homogeneous_nucleation_rates.py | 2 +- 2 files changed, 48 insertions(+), 26 deletions(-) diff --git a/PySDM/backends/impl_numba/methods/freezing_methods.py b/PySDM/backends/impl_numba/methods/freezing_methods.py index 99f41017a3..f932aeb4d1 100644 --- a/PySDM/backends/impl_numba/methods/freezing_methods.py +++ b/PySDM/backends/impl_numba/methods/freezing_methods.py @@ -18,28 +18,35 @@ class FreezingMethods(BackendMethods): - def __init__(self): - BackendMethods.__init__(self) - unfrozen_and_saturated = self.formulae.trivia.unfrozen_and_saturated - unfrozen_and_ice_saturated = self.formulae.trivia.unfrozen_and_ice_saturated - frozen_and_above_freezing_point = ( - self.formulae.trivia.frozen_and_above_freezing_point - ) - - @numba.njit(**{**self.default_jit_flags, "parallel": False}) - def _freeze(signed_water_mass, i): + @cached_property + def _freeze(self): + @numba.njit(**self.default_jit_flags) + def body(signed_water_mass, i): signed_water_mass[i] = -1 * signed_water_mass[i] # TODO #599: change thd (latent heat)! - @numba.njit(**{**self.default_jit_flags, "parallel": False}) - def _thaw(signed_water_mass, i): + return body + + @cached_property + def _thaw(self): + @numba.njit(**self.default_jit_flags) + def body(signed_water_mass, i): signed_water_mass[i] = -1 * signed_water_mass[i] # TODO #599: change thd (latent heat)! + return body + + @cached_property + def _freeze_singular_body(self): + _thaw = self._thaw + _freeze = self._freeze + frozen_and_above_freezing_point = ( + self.formulae.trivia.frozen_and_above_freezing_point + ) + unfrozen_and_saturated = self.formulae.trivia.unfrozen_and_saturated + @numba.njit(**self.default_jit_flags) - def freeze_singular_body( - attributes, temperature, relative_humidity, cell, thaw - ): + def body(attributes, temperature, relative_humidity, cell, thaw): n_sd = len(attributes.freezing_temperature) for i in numba.prange(n_sd): # pylint: disable=not-an-iterable if attributes.freezing_temperature[i] == 0: @@ -56,13 +63,21 @@ def freeze_singular_body( ): _freeze(attributes.signed_water_mass, i) - self.freeze_singular_body = freeze_singular_body + return body + @cached_property + def _freeze_time_dependent_body(self): + _thaw = self._thaw + _freeze = self._freeze + frozen_and_above_freezing_point = ( + self.formulae.trivia.frozen_and_above_freezing_point + ) + unfrozen_and_saturated = self.formulae.trivia.unfrozen_and_saturated j_het = self.formulae.heterogeneous_ice_nucleation_rate.j_het prob_zero_events = self.formulae.trivia.poissonian_avoidance_function @numba.njit(**self.default_jit_flags) - def freeze_time_dependent_body( # pylint: disable=too-many-arguments + def body( # pylint: disable=too-many-arguments rand, attributes, timestep, @@ -93,12 +108,21 @@ def freeze_time_dependent_body( # pylint: disable=too-many-arguments if rand[i] < prob: _freeze(attributes.signed_water_mass, i) - self.freeze_time_dependent_body = freeze_time_dependent_body + return body + @cached_property + def _freeze_time_dependent_homogeneous_body(self): + _thaw = self._thaw + _freeze = self._freeze + frozen_and_above_freezing_point = ( + self.formulae.trivia.frozen_and_above_freezing_point + ) + unfrozen_and_ice_saturated = self.formulae.trivia.unfrozen_and_ice_saturated j_hom = self.formulae.homogeneous_ice_nucleation_rate.j_hom + prob_zero_events = self.formulae.trivia.poissonian_avoidance_function @numba.njit(**self.default_jit_flags) - def freeze_time_dependent_homogeneous_body( # pylint: disable=unused-argument,too-many-arguments + def body( # pylint: disable=unused-argument,too-many-arguments rand, attributes, timestep, @@ -133,14 +157,12 @@ def freeze_time_dependent_homogeneous_body( # pylint: disable=unused-argument,t if rand[i] < prob: _freeze(attributes.signed_water_mass, i) - self.freeze_time_dependent_homogeneous_body = ( - freeze_time_dependent_homogeneous_body - ) + return body def freeze_singular( self, *, attributes, temperature, relative_humidity, cell, thaw: bool ): - self.freeze_singular_body( + self._freeze_singular_body( SingularAttributes( freezing_temperature=attributes.freezing_temperature.data, signed_water_mass=attributes.signed_water_mass.data, @@ -163,7 +185,7 @@ def freeze_time_dependent( relative_humidity, thaw: bool, ): - self.freeze_time_dependent_body( + self._freeze_time_dependent_body( rand.data, TimeDependentAttributes( immersed_surface_area=attributes.immersed_surface_area.data, @@ -189,7 +211,7 @@ def freeze_time_dependent_homogeneous( relative_humidity_ice, thaw: bool, ): - self.freeze_time_dependent_homogeneous_body( + self._freeze_time_dependent_homogeneous_body( rand.data, TimeDependentHomogeneousAttributes( volume=attributes.volume.data, diff --git a/tests/unit_tests/physics/test_homogeneous_nucleation_rates.py b/tests/unit_tests/physics/test_homogeneous_nucleation_rates.py index df148d500f..f3b51a01ec 100644 --- a/tests/unit_tests/physics/test_homogeneous_nucleation_rates.py +++ b/tests/unit_tests/physics/test_homogeneous_nucleation_rates.py @@ -1,5 +1,5 @@ """ -test for homogeneous nucleation rate parametrisations +test for homogeneous nucleation rate parameterisations """ from contextlib import nullcontext From b3441b1c0e9b8adec95a5c404bdfc83b275bfbb8 Mon Sep 17 00:00:00 2001 From: Sylwester Arabas Date: Sat, 14 Jun 2025 23:59:27 +0200 Subject: [PATCH 51/57] add parallel=False to thaw and freeze --- PySDM/backends/impl_numba/methods/freezing_methods.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PySDM/backends/impl_numba/methods/freezing_methods.py b/PySDM/backends/impl_numba/methods/freezing_methods.py index f932aeb4d1..0c0bf05e7e 100644 --- a/PySDM/backends/impl_numba/methods/freezing_methods.py +++ b/PySDM/backends/impl_numba/methods/freezing_methods.py @@ -20,7 +20,7 @@ class FreezingMethods(BackendMethods): @cached_property def _freeze(self): - @numba.njit(**self.default_jit_flags) + @numba.njit(**{**JIT_FLAGS, **{"parallel": False}}) def body(signed_water_mass, i): signed_water_mass[i] = -1 * signed_water_mass[i] # TODO #599: change thd (latent heat)! @@ -29,7 +29,7 @@ def body(signed_water_mass, i): @cached_property def _thaw(self): - @numba.njit(**self.default_jit_flags) + @numba.njit(**{**JIT_FLAGS, **{"parallel": False}}) def body(signed_water_mass, i): signed_water_mass[i] = -1 * signed_water_mass[i] # TODO #599: change thd (latent heat)! From baee01ce20f06b2fef4ce82aa19a6a9cdc862b6c Mon Sep 17 00:00:00 2001 From: Sylwester Arabas Date: Sun, 15 Jun 2025 02:49:01 +0200 Subject: [PATCH 52/57] fix flags var --- PySDM/backends/impl_numba/methods/freezing_methods.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PySDM/backends/impl_numba/methods/freezing_methods.py b/PySDM/backends/impl_numba/methods/freezing_methods.py index 0c0bf05e7e..147f3d3591 100644 --- a/PySDM/backends/impl_numba/methods/freezing_methods.py +++ b/PySDM/backends/impl_numba/methods/freezing_methods.py @@ -20,7 +20,7 @@ class FreezingMethods(BackendMethods): @cached_property def _freeze(self): - @numba.njit(**{**JIT_FLAGS, **{"parallel": False}}) + @numba.njit(**{**self.default_jit_flags, **{"parallel": False}}) def body(signed_water_mass, i): signed_water_mass[i] = -1 * signed_water_mass[i] # TODO #599: change thd (latent heat)! @@ -29,7 +29,7 @@ def body(signed_water_mass, i): @cached_property def _thaw(self): - @numba.njit(**{**JIT_FLAGS, **{"parallel": False}}) + @numba.njit(**{**self.default_jit_flags, **{"parallel": False}}) def body(signed_water_mass, i): signed_water_mass[i] = -1 * signed_water_mass[i] # TODO #599: change thd (latent heat)! From 33e3c9d3a73edd0039ce2b160eac1bec509815a4 Mon Sep 17 00:00:00 2001 From: Tim Luettmer Date: Mon, 16 Jun 2025 11:42:52 +0200 Subject: [PATCH 53/57] Changed check for d_aw_ice range for hom. freezing --- .../backends/impl_numba/methods/freezing_methods.py | 10 +++++++++- PySDM/physics/constants_defaults.py | 3 +++ .../homogeneous_ice_nucleation_rate/constant.py | 10 ++++++++++ .../physics/homogeneous_ice_nucleation_rate/koop.py | 12 ++++++++++++ .../homogeneous_ice_nucleation_rate/koop_corr.py | 11 +++++++++++ .../homogeneous_ice_nucleation_rate/koop_murray.py | 11 +++++++++++ .../physics/homogeneous_ice_nucleation_rate/null.py | 10 ++++++++++ 7 files changed, 66 insertions(+), 1 deletion(-) diff --git a/PySDM/backends/impl_numba/methods/freezing_methods.py b/PySDM/backends/impl_numba/methods/freezing_methods.py index 147f3d3591..ef4bc210cc 100644 --- a/PySDM/backends/impl_numba/methods/freezing_methods.py +++ b/PySDM/backends/impl_numba/methods/freezing_methods.py @@ -120,6 +120,12 @@ def _freeze_time_dependent_homogeneous_body(self): unfrozen_and_ice_saturated = self.formulae.trivia.unfrozen_and_ice_saturated j_hom = self.formulae.homogeneous_ice_nucleation_rate.j_hom prob_zero_events = self.formulae.trivia.poissonian_avoidance_function + d_a_w_ice_within_range = ( + self.formulae.homogeneous_ice_nucleation_rate.d_a_w_ice_within_range + ) + d_a_w_ice_maximum = ( + self.formulae.homogeneous_ice_nucleation_rate.d_a_w_ice_maximum + ) @numba.njit(**self.default_jit_flags) def body( # pylint: disable=unused-argument,too-many-arguments @@ -146,7 +152,9 @@ def body( # pylint: disable=unused-argument,too-many-arguments d_a_w_ice = (relative_humidity_ice[cell_id] - 1.0) * a_w_ice[ cell_id ] - if 0.23 < d_a_w_ice < 0.34: + + if d_a_w_ice_within_range(d_a_w_ice): + d_a_w_ice = d_a_w_ice_maximum(d_a_w_ice) rate_assuming_constant_temperature_within_dt = ( j_hom(temperature[cell_id], d_a_w_ice) * attributes.volume[i] diff --git a/PySDM/physics/constants_defaults.py b/PySDM/physics/constants_defaults.py index 240df3bdda..097ff04f8d 100644 --- a/PySDM/physics/constants_defaults.py +++ b/PySDM/physics/constants_defaults.py @@ -326,6 +326,9 @@ """ 〃 """ KOOP_UNIT = 1 / si.cm**3 / si.s """ 〃 """ +KOOP_MIN_DA_W_ICE = 0.26 +""" 〃 """ +KOOP_MAX_DA_W_ICE = 0.34 KOOP_CORR = -1.522 """ homogeneous ice nucleation rate correction factor diff --git a/PySDM/physics/homogeneous_ice_nucleation_rate/constant.py b/PySDM/physics/homogeneous_ice_nucleation_rate/constant.py index f5f32353d4..7112b2b7bf 100644 --- a/PySDM/physics/homogeneous_ice_nucleation_rate/constant.py +++ b/PySDM/physics/homogeneous_ice_nucleation_rate/constant.py @@ -9,6 +9,16 @@ class Constant: # pylint: disable=too-few-public-methods def __init__(self, const): assert np.isfinite(const.J_HOM) + @staticmethod + def d_a_w_ice_within_range(const, da_w_ice): + return da_w_ice >= const.KOOP_MIN_DA_W_ICE + + @staticmethod + def d_a_w_ice_maximum(const, da_w_ice): + return np.where( + da_w_ice > const.KOOP_MAX_DA_W_ICE, const.KOOP_MAX_DA_W_ICE, da_w_ice + ) + @staticmethod def j_hom(const, T, a_w_ice): # pylint: disable=unused-argument return const.J_HOM diff --git a/PySDM/physics/homogeneous_ice_nucleation_rate/koop.py b/PySDM/physics/homogeneous_ice_nucleation_rate/koop.py index d25742dddb..8a1526f4c2 100644 --- a/PySDM/physics/homogeneous_ice_nucleation_rate/koop.py +++ b/PySDM/physics/homogeneous_ice_nucleation_rate/koop.py @@ -4,11 +4,23 @@ ([Koop et al. 2000](https://doi.org/10.1038/35020537)) """ +import numpy as np + class Koop2000: # pylint: disable=too-few-public-methods def __init__(self, const): pass + @staticmethod + def d_a_w_ice_within_range(const, da_w_ice): + return da_w_ice >= const.KOOP_MIN_DA_W_ICE + + @staticmethod + def d_a_w_ice_maximum(const, da_w_ice): + return np.where( + da_w_ice > const.KOOP_MAX_DA_W_ICE, const.KOOP_MAX_DA_W_ICE, da_w_ice + ) + @staticmethod def j_hom(const, T, da_w_ice): # pylint: disable=unused-argument return ( diff --git a/PySDM/physics/homogeneous_ice_nucleation_rate/koop_corr.py b/PySDM/physics/homogeneous_ice_nucleation_rate/koop_corr.py index 4de3f3d880..5ae5cf4714 100644 --- a/PySDM/physics/homogeneous_ice_nucleation_rate/koop_corr.py +++ b/PySDM/physics/homogeneous_ice_nucleation_rate/koop_corr.py @@ -5,11 +5,22 @@ ([Spichtinger et al. 2023](https://doi.org/10.5194/acp-23-2035-2023)) """ +import numpy as np class Koop_Correction: # pylint: disable=too-few-public-methods def __init__(self, const): pass + @staticmethod + def d_a_w_ice_within_range(const, da_w_ice): + return da_w_ice >= const.KOOP_MIN_DA_W_ICE + + @staticmethod + def d_a_w_ice_maximum(const, da_w_ice): + return np.where( + da_w_ice > const.KOOP_MAX_DA_W_ICE, const.KOOP_MAX_DA_W_ICE, da_w_ice + ) + @staticmethod def j_hom(const, T, da_w_ice): # pylint: disable=unused-argument return ( diff --git a/PySDM/physics/homogeneous_ice_nucleation_rate/koop_murray.py b/PySDM/physics/homogeneous_ice_nucleation_rate/koop_murray.py index b41748d623..340f4a8008 100644 --- a/PySDM/physics/homogeneous_ice_nucleation_rate/koop_murray.py +++ b/PySDM/physics/homogeneous_ice_nucleation_rate/koop_murray.py @@ -4,11 +4,22 @@ ([eq. A9, Tab VII in Koop and Murray 2016](https://doi.org/10.1063/1.4962355)) """ +import numpy as np class KoopMurray2016: # pylint: disable=too-few-public-methods def __init__(self, const): pass + @staticmethod + def d_a_w_ice_within_range(const, da_w_ice): + return da_w_ice >= const.KOOP_MIN_DA_W_ICE + + @staticmethod + def d_a_w_ice_maximum(const, da_w_ice): + return np.where( + da_w_ice > const.KOOP_MAX_DA_W_ICE, const.KOOP_MAX_DA_W_ICE, da_w_ice + ) + @staticmethod def j_hom(const, T, da_w_ice): # pylint: disable=unused-argument return ( diff --git a/PySDM/physics/homogeneous_ice_nucleation_rate/null.py b/PySDM/physics/homogeneous_ice_nucleation_rate/null.py index 8fd96b4d35..ea31b620ec 100644 --- a/PySDM/physics/homogeneous_ice_nucleation_rate/null.py +++ b/PySDM/physics/homogeneous_ice_nucleation_rate/null.py @@ -10,6 +10,16 @@ class Null: # pylint: disable=too-few-public-methods,unused-argument def __init__(self, _): pass + @staticmethod + def d_a_w_ice_within_range(const, da_w_ice): + return da_w_ice >= const.KOOP_MIN_DA_W_ICE + + @staticmethod + def d_a_w_ice_maximum(const, da_w_ice): + return np.where( + da_w_ice > const.KOOP_MAX_DA_W_ICE, const.KOOP_MAX_DA_W_ICE, da_w_ice + ) + @staticmethod def j_hom(const, T, d_a_w_ice): # pylint: disable=unused-argument return np.nan From 811681015c5794baf419e0228c86a0c1081ffa18 Mon Sep 17 00:00:00 2001 From: Tim Luettmer Date: Mon, 16 Jun 2025 12:29:21 +0200 Subject: [PATCH 54/57] Changed d_aw_ice range check for constant and null variation of hom. freezing nucleation rate --- PySDM/physics/homogeneous_ice_nucleation_rate/constant.py | 6 ++---- PySDM/physics/homogeneous_ice_nucleation_rate/null.py | 6 ++---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/PySDM/physics/homogeneous_ice_nucleation_rate/constant.py b/PySDM/physics/homogeneous_ice_nucleation_rate/constant.py index 7112b2b7bf..d2da145caa 100644 --- a/PySDM/physics/homogeneous_ice_nucleation_rate/constant.py +++ b/PySDM/physics/homogeneous_ice_nucleation_rate/constant.py @@ -11,13 +11,11 @@ def __init__(self, const): @staticmethod def d_a_w_ice_within_range(const, da_w_ice): - return da_w_ice >= const.KOOP_MIN_DA_W_ICE + return True @staticmethod def d_a_w_ice_maximum(const, da_w_ice): - return np.where( - da_w_ice > const.KOOP_MAX_DA_W_ICE, const.KOOP_MAX_DA_W_ICE, da_w_ice - ) + return da_w_ice @staticmethod def j_hom(const, T, a_w_ice): # pylint: disable=unused-argument diff --git a/PySDM/physics/homogeneous_ice_nucleation_rate/null.py b/PySDM/physics/homogeneous_ice_nucleation_rate/null.py index ea31b620ec..13a798892d 100644 --- a/PySDM/physics/homogeneous_ice_nucleation_rate/null.py +++ b/PySDM/physics/homogeneous_ice_nucleation_rate/null.py @@ -12,13 +12,11 @@ def __init__(self, _): @staticmethod def d_a_w_ice_within_range(const, da_w_ice): - return da_w_ice >= const.KOOP_MIN_DA_W_ICE + return True @staticmethod def d_a_w_ice_maximum(const, da_w_ice): - return np.where( - da_w_ice > const.KOOP_MAX_DA_W_ICE, const.KOOP_MAX_DA_W_ICE, da_w_ice - ) + return da_w_ice @staticmethod def j_hom(const, T, d_a_w_ice): # pylint: disable=unused-argument From 64af2c6e81de17bccf45d34e769f3595a283c974 Mon Sep 17 00:00:00 2001 From: Tim Luettmer Date: Mon, 16 Jun 2025 12:30:19 +0200 Subject: [PATCH 55/57] pylint & pre-commit --- PySDM/physics/homogeneous_ice_nucleation_rate/koop_corr.py | 1 + PySDM/physics/homogeneous_ice_nucleation_rate/koop_murray.py | 1 + 2 files changed, 2 insertions(+) diff --git a/PySDM/physics/homogeneous_ice_nucleation_rate/koop_corr.py b/PySDM/physics/homogeneous_ice_nucleation_rate/koop_corr.py index 5ae5cf4714..a8a3f04380 100644 --- a/PySDM/physics/homogeneous_ice_nucleation_rate/koop_corr.py +++ b/PySDM/physics/homogeneous_ice_nucleation_rate/koop_corr.py @@ -7,6 +7,7 @@ import numpy as np + class Koop_Correction: # pylint: disable=too-few-public-methods def __init__(self, const): pass diff --git a/PySDM/physics/homogeneous_ice_nucleation_rate/koop_murray.py b/PySDM/physics/homogeneous_ice_nucleation_rate/koop_murray.py index 340f4a8008..d3e41816f5 100644 --- a/PySDM/physics/homogeneous_ice_nucleation_rate/koop_murray.py +++ b/PySDM/physics/homogeneous_ice_nucleation_rate/koop_murray.py @@ -6,6 +6,7 @@ import numpy as np + class KoopMurray2016: # pylint: disable=too-few-public-methods def __init__(self, const): pass From 616b188e701a45b67c9303bb5c13b8b7bcc0f66e Mon Sep 17 00:00:00 2001 From: Tim Luettmer Date: Mon, 16 Jun 2025 14:35:51 +0200 Subject: [PATCH 56/57] addressing pylint --- PySDM/physics/homogeneous_ice_nucleation_rate/constant.py | 4 ++-- PySDM/physics/homogeneous_ice_nucleation_rate/null.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/PySDM/physics/homogeneous_ice_nucleation_rate/constant.py b/PySDM/physics/homogeneous_ice_nucleation_rate/constant.py index d2da145caa..9d4a79ef51 100644 --- a/PySDM/physics/homogeneous_ice_nucleation_rate/constant.py +++ b/PySDM/physics/homogeneous_ice_nucleation_rate/constant.py @@ -10,11 +10,11 @@ def __init__(self, const): assert np.isfinite(const.J_HOM) @staticmethod - def d_a_w_ice_within_range(const, da_w_ice): + def d_a_w_ice_within_range(const, da_w_ice): # pylint: disable=unused-argument return True @staticmethod - def d_a_w_ice_maximum(const, da_w_ice): + def d_a_w_ice_maximum(const, da_w_ice): # pylint: disable=unused-argument return da_w_ice @staticmethod diff --git a/PySDM/physics/homogeneous_ice_nucleation_rate/null.py b/PySDM/physics/homogeneous_ice_nucleation_rate/null.py index 13a798892d..739bdf72cf 100644 --- a/PySDM/physics/homogeneous_ice_nucleation_rate/null.py +++ b/PySDM/physics/homogeneous_ice_nucleation_rate/null.py @@ -11,11 +11,11 @@ def __init__(self, _): pass @staticmethod - def d_a_w_ice_within_range(const, da_w_ice): + def d_a_w_ice_within_range(const, da_w_ice): # pylint: disable=unused-argument return True @staticmethod - def d_a_w_ice_maximum(const, da_w_ice): + def d_a_w_ice_maximum(const, da_w_ice): # pylint: disable=unused-argument return da_w_ice @staticmethod From c8afbe24b43d4ace1f3c62aa22c98a2e633de3ef Mon Sep 17 00:00:00 2001 From: Sylwester Arabas Date: Tue, 17 Jun 2025 16:04:15 +0200 Subject: [PATCH 57/57] try rebalancing tesst load for condensation_* suite --- tests/examples_tests/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/examples_tests/conftest.py b/tests/examples_tests/conftest.py index 03863e02dc..2b57a0daa5 100644 --- a/tests/examples_tests/conftest.py +++ b/tests/examples_tests/conftest.py @@ -47,6 +47,7 @@ def findfiles(path, regex): "condensation_a": [ "Lowe_et_al_2019", "Singer_Ward", + "Rogers_1975", ], "condensation_b": [ "Abdul_Razzak_Ghan_2000", @@ -58,7 +59,6 @@ def findfiles(path, regex): "Grabowski_and_Pawlowska_2023", "Jensen_and_Nugent_2017", "Abade_and_Albuquerque_2024", - "Rogers_1975", ], "coagulation": ["Berry_1967", "Shima_et_al_2009"], "breakup": ["Bieli_et_al_2022", "deJong_Mackay_et_al_2023", "Srivastava_1982"],