diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index fe258c5be4..2a1e18a78f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -17,6 +17,7 @@ on: paths-ignore: ['docs/**', '**.md'] pull_request: branches: [ main ] + paths-ignore: ['docs/**', '**.md'] schedule: - cron: '0 13 * * 4' diff --git a/PySDM/attributes/impl/__init__.py b/PySDM/attributes/impl/__init__.py index 39fba8255c..fc322b74b0 100644 --- a/PySDM/attributes/impl/__init__.py +++ b/PySDM/attributes/impl/__init__.py @@ -11,3 +11,4 @@ from .maximum_attribute import MaximumAttribute from .attribute_registry import register_attribute, get_attribute_class from .intensive_attribute import IntensiveAttribute +from .temperature_variation_option_attribute import TemperatureVariationOptionAttribute diff --git a/PySDM/attributes/impl/temperature_variation_option_attribute.py b/PySDM/attributes/impl/temperature_variation_option_attribute.py new file mode 100644 index 0000000000..6a3add92c4 --- /dev/null +++ b/PySDM/attributes/impl/temperature_variation_option_attribute.py @@ -0,0 +1,18 @@ +"""common code for attributes offering an option to neglect temperature variation, +intended for use with Parcel environment only""" + + +class TemperatureVariationOptionAttribute: # pylint: disable=too-few-public-methods + """base class""" + + def __init__(self, builder, neglect_temperature_variations: bool): + if neglect_temperature_variations: + assert builder.particulator.environment.mesh.dimension == 0 + self.neglect_temperature_variations = neglect_temperature_variations + self.initial_temperature = ( + builder.particulator.Storage.from_ndarray( + builder.particulator.environment["T"].to_ndarray() + ) + if neglect_temperature_variations + else None + ) diff --git a/PySDM/attributes/physics/__init__.py b/PySDM/attributes/physics/__init__.py index d1186a6930..44416bd9d9 100644 --- a/PySDM/attributes/physics/__init__.py +++ b/PySDM/attributes/physics/__init__.py @@ -3,11 +3,11 @@ """ from .area import Area -from .critical_supersaturation import CriticalSupersaturation +from .critical_saturation import CriticalSaturation from .critical_volume import CriticalVolume, WetToCriticalVolumeRatio from .dry_radius import DryRadius from .dry_volume import DryVolume -from .equilibrium_supersaturation import EquilibriumSupersaturation +from .equilibrium_saturation import EquilibriumSaturation from .heat import Heat from .hygroscopicity import Kappa, KappaTimesDryVolume from .water_mass import SignedWaterMass diff --git a/PySDM/attributes/physics/critical_saturation.py b/PySDM/attributes/physics/critical_saturation.py new file mode 100644 index 0000000000..b7fa81bf3c --- /dev/null +++ b/PySDM/attributes/physics/critical_saturation.py @@ -0,0 +1,54 @@ +""" +kappa-Koehler critical saturation calculated for either initial or actual environment temperature +""" + +from PySDM.attributes.impl import ( + DerivedAttribute, + register_attribute, + TemperatureVariationOptionAttribute, +) + + +@register_attribute() +class CriticalSaturation(DerivedAttribute, TemperatureVariationOptionAttribute): + def __init__(self, builder, neglect_temperature_variations=False): + assert builder.particulator.mesh.dimension == 0 + + self.v_crit = builder.get_attribute("critical volume") + self.v_dry = builder.get_attribute("dry volume") + self.kappa = builder.get_attribute("kappa") + self.f_org = builder.get_attribute("dry volume organic fraction") + TemperatureVariationOptionAttribute.__init__( + self, builder, neglect_temperature_variations + ) + DerivedAttribute.__init__( + self, + builder=builder, + name="critical saturation", + dependencies=(self.v_crit, self.kappa, self.v_dry, self.f_org), + ) + + def recalculate(self): + temperature = ( + self.initial_temperature + if self.neglect_temperature_variations + else self.particulator.environment["T"] + ) + r_cr = self.formulae.trivia.radius(self.v_crit.data.data) + rd3 = self.v_dry.data.data / self.formulae.constants.PI_4_3 + sgm = self.formulae.surface_tension.sigma( + temperature.data, + self.v_crit.data.data, + self.v_dry.data.data, + self.f_org.data.data, + ) + + self.data.data[:] = self.formulae.hygroscopicity.RH_eq( + r_cr, T=temperature.data, kp=self.kappa.data.data, rd3=rd3, sgm=sgm + ) + + +@register_attribute() +class CriticalSaturationNeglectingTemperatureVariations(CriticalSaturation): + def __init__(self, builder): + super().__init__(builder, neglect_temperature_variations=True) diff --git a/PySDM/attributes/physics/critical_supersaturation.py b/PySDM/attributes/physics/critical_supersaturation.py deleted file mode 100644 index 7b465abcfb..0000000000 --- a/PySDM/attributes/physics/critical_supersaturation.py +++ /dev/null @@ -1,37 +0,0 @@ -""" -kappa-Koehler critical supersaturation calculated for actual environment temperature -""" - -from PySDM.attributes.impl import DerivedAttribute, register_attribute - - -@register_attribute() -class CriticalSupersaturation(DerivedAttribute): - def __init__(self, builder): - self.v_crit = builder.get_attribute("critical volume") - self.v_dry = builder.get_attribute("dry volume") - self.kappa = builder.get_attribute("kappa") - self.f_org = builder.get_attribute("dry volume organic fraction") - - super().__init__( - builder=builder, - name="critical supersaturation", - dependencies=(self.v_crit, self.kappa, self.v_dry, self.f_org), - ) - - def recalculate(self): - if len(self.particulator.environment["T"]) != 1: - raise NotImplementedError() - temperature = self.particulator.environment["T"][0] - r_cr = self.formulae.trivia.radius(self.v_crit.data.data) - rd3 = self.v_dry.data.data / self.formulae.constants.PI_4_3 - sgm = self.formulae.surface_tension.sigma( - temperature, - self.v_crit.data.data, - self.v_dry.data.data, - self.f_org.data.data, - ) - - self.data.data[:] = self.formulae.hygroscopicity.RH_eq( - r_cr, T=temperature, kp=self.kappa.data.data, rd3=rd3, sgm=sgm - ) diff --git a/PySDM/attributes/physics/critical_volume.py b/PySDM/attributes/physics/critical_volume.py index 0634a0f1d1..70ea4cb818 100644 --- a/PySDM/attributes/physics/critical_volume.py +++ b/PySDM/attributes/physics/critical_volume.py @@ -1,13 +1,17 @@ """ -critical wet volume (kappa-Koehler, computed using actual temperature) +critical wet volume (kappa-Koehler, computed using actual or initial temperature) """ -from PySDM.attributes.impl import DerivedAttribute, register_attribute +from PySDM.attributes.impl import ( + DerivedAttribute, + register_attribute, + TemperatureVariationOptionAttribute, +) @register_attribute() -class CriticalVolume(DerivedAttribute): - def __init__(self, builder): +class CriticalVolume(DerivedAttribute, TemperatureVariationOptionAttribute): + def __init__(self, builder, neglect_temperature_variations=False): self.cell_id = builder.get_attribute("cell id") self.v_dry = builder.get_attribute("dry volume") self.v_wet = builder.get_attribute("volume") @@ -15,31 +19,70 @@ def __init__(self, builder): self.f_org = builder.get_attribute("dry volume organic fraction") self.environment = builder.particulator.environment self.particles = builder.particulator + dependencies = [self.v_dry, self.v_wet, self.cell_id] - super().__init__(builder, name="critical volume", dependencies=dependencies) + TemperatureVariationOptionAttribute.__init__( + self, builder, neglect_temperature_variations + ) + DerivedAttribute.__init__( + self, builder, name="critical volume", dependencies=dependencies + ) def recalculate(self): + temperature = ( + self.initial_temperature + if self.neglect_temperature_variations + else self.environment["T"] + ) self.particulator.backend.critical_volume( v_cr=self.data, kappa=self.kappa.get(), f_org=self.f_org.get(), v_dry=self.v_dry.get(), v_wet=self.v_wet.get(), - T=self.environment["T"], + T=temperature, cell=self.cell_id.get(), ) @register_attribute() -class WetToCriticalVolumeRatio(DerivedAttribute): +class CriticalVolumeNeglectingTemperatureVariations(CriticalVolume): def __init__(self, builder): - self.critical_volume = builder.get_attribute("critical volume") + super().__init__(builder, neglect_temperature_variations=True) + + +@register_attribute() +class WetToCriticalVolumeRatio(DerivedAttribute): + def __init__( + self, + builder, + neglect_temperature_variations=False, + name="wet to critical volume ratio", + ): + self.critical_volume = builder.get_attribute( + "critical volume" + + ( + " neglecting temperature variations" + if neglect_temperature_variations + else "" + ) + ) self.volume = builder.get_attribute("volume") super().__init__( builder, - name="wet to critical volume ratio", + name=name, dependencies=(self.critical_volume, self.volume), ) def recalculate(self): self.data.ratio(self.volume.get(), self.critical_volume.get()) + + +@register_attribute() +class WetToCriticalVolumeRatioNeglectingTemperatureVariations(WetToCriticalVolumeRatio): + def __init__(self, builder): + super().__init__( + builder, + neglect_temperature_variations=True, + name="wet to critical volume ratio neglecting temperature variations", + ) diff --git a/PySDM/attributes/physics/equilibrium_supersaturation.py b/PySDM/attributes/physics/equilibrium_saturation.py similarity index 86% rename from PySDM/attributes/physics/equilibrium_supersaturation.py rename to PySDM/attributes/physics/equilibrium_saturation.py index 199b2b731e..c374aca010 100644 --- a/PySDM/attributes/physics/equilibrium_supersaturation.py +++ b/PySDM/attributes/physics/equilibrium_saturation.py @@ -1,12 +1,12 @@ """ -kappa-Koehler equilibrium supersaturation calculated for actual environment temperature +kappa-Koehler equilibrium saturation calculated for actual environment temperature """ from PySDM.attributes.impl import DerivedAttribute, register_attribute @register_attribute() -class EquilibriumSupersaturation(DerivedAttribute): +class EquilibriumSaturation(DerivedAttribute): def __init__(self, builder): self.r_wet = builder.get_attribute("radius") self.v_wet = builder.get_attribute("volume") @@ -16,7 +16,7 @@ def __init__(self, builder): super().__init__( builder=builder, - name="equilibrium supersaturation", + name="equilibrium saturation", dependencies=(self.kappa, self.v_dry, self.f_org, self.r_wet), ) diff --git a/PySDM/backends/impl_common/freezing_attributes.py b/PySDM/backends/impl_common/freezing_attributes.py index a58718c9e7..a9f788ff47 100644 --- a/PySDM/backends/impl_common/freezing_attributes.py +++ b/PySDM/backends/impl_common/freezing_attributes.py @@ -25,3 +25,14 @@ class TimeDependentAttributes( """groups attributes required in time-dependent regime""" __slots__ = () + + +class TimeDependentHomogeneousAttributes( + namedtuple( + typename="TimeDependentHomogeneousAttributes", + field_names=("volume", "signed_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 6799e3b982..ef4bc210cc 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 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 @@ -12,31 +13,40 @@ from ...impl_common.freezing_attributes import ( SingularAttributes, TimeDependentAttributes, + TimeDependentHomogeneousAttributes, ) class FreezingMethods(BackendMethods): - def __init__(self): - BackendMethods.__init__(self) - unfrozen_and_saturated = self.formulae.trivia.unfrozen_and_saturated - frozen_and_above_freezing_point = ( - self.formulae.trivia.frozen_and_above_freezing_point - ) - - @numba.njit(**{**self.default_jit_flags, "parallel": False}) - def _freeze(water_mass, i): - water_mass[i] = -1 * water_mass[i] + @cached_property + def _freeze(self): + @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)! - @numba.njit(**{**self.default_jit_flags, "parallel": False}) - def _thaw(water_mass, i): - water_mass[i] = -1 * water_mass[i] + return body + + @cached_property + def _thaw(self): + @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)! + 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: @@ -53,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, @@ -90,12 +108,69 @@ 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 + 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 + rand, + attributes, + timestep, + cell, + a_w_ice, + temperature, + relative_humidity_ice, + thaw, + ): + + n_sd = len(attributes.signed_water_mass) + for i in numba.prange(n_sd): # pylint: disable=not-an-iterable + cell_id = cell[i] + if thaw and frozen_and_above_freezing_point( + attributes.signed_water_mass[i], temperature[cell_id] + ): + _thaw(attributes.signed_water_mass, i) + 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.0) * a_w_ice[ + cell_id + ] + + 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] + ) + prob = 1 - prob_zero_events( + r=rate_assuming_constant_temperature_within_dt, dt=timestep + ) + if rand[i] < prob: + _freeze(attributes.signed_water_mass, i) + + 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, @@ -118,7 +193,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, @@ -132,6 +207,32 @@ def freeze_time_dependent( thaw=thaw, ) + def freeze_time_dependent_homogeneous( + self, + *, + rand, + attributes, + timestep, + cell, + a_w_ice, + temperature, + relative_humidity_ice, + thaw: bool, + ): + self._freeze_time_dependent_homogeneous_body( + rand.data, + TimeDependentHomogeneousAttributes( + volume=attributes.volume.data, + signed_water_mass=attributes.signed_water_mass.data, + ), + timestep, + cell.data, + a_w_ice.data, + temperature.data, + relative_humidity_ice.data, + thaw=thaw, + ) + @cached_property def _record_freezing_temperatures_body(self): ff = self.formulae_flattened diff --git a/PySDM/dynamics/freezing.py b/PySDM/dynamics/freezing.py index 50eb8dbcbe..94084e0c5f 100644 --- a/PySDM/dynamics/freezing.py +++ b/PySDM/dynamics/freezing.py @@ -1,14 +1,25 @@ """ -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 @register_dynamic() -class Freezing: - def __init__(self, *, singular=True, thaw=False): +class Freezing: # pylint: disable=too-many-instance-attributes + 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 self.thaw = thaw self.enable = True self.rand = None @@ -26,12 +37,21 @@ def register(self, builder): if self.singular: builder.request_attribute("freezing temperature") - if not self.singular: + if not self.singular and self.immersion_freezing: assert ( self.particulator.formulae.heterogeneous_ice_nucleation_rate.__name__ != "Null" ) builder.request_attribute("immersed surface area") + + if self.homogeneous_freezing: + assert ( + 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 ) @@ -49,11 +69,19 @@ def __call__(self): if not self.enable: return - if self.singular: - self.particulator.immersion_freezing_singular(thaw=self.thaw) - else: + if self.immersion_freezing: + if self.singular: + self.particulator.immersion_freezing_singular(thaw=self.thaw) + else: + self.rand.urand(self.rng) + self.particulator.immersion_freezing_time_dependent( + rand=self.rand, + thaw=self.thaw, + ) + + if self.homogeneous_freezing: self.rand.urand(self.rng) - self.particulator.immersion_freezing_time_dependent( + self.particulator.homogeneous_freezing_time_dependent( rand=self.rand, thaw=self.thaw, ) diff --git a/PySDM/environments/parcel.py b/PySDM/environments/parcel.py index 245afab028..fdff10de91 100644 --- a/PySDM/environments/parcel.py +++ b/PySDM/environments/parcel.py @@ -2,7 +2,7 @@ Zero-dimensional adiabatic parcel framework """ -from typing import List, Optional +from typing import List, Optional, Union import numpy as np @@ -25,7 +25,7 @@ def __init__( p0: float, initial_water_vapour_mixing_ratio: float, T0: float, - w: [float, callable], + w: Union[float, callable], z0: float = 0, mixed_phase=False, variables: Optional[List[str]] = None, @@ -106,7 +106,7 @@ def advance_parcel_vars(self): T = self["T"][0] p = self["p"][0] - dz_dt = self.w((self.particulator.n_steps + 1 / 2) * dt) # "mid-point" + dz_dt = self._compute_dz_dt(dt) water_vapour_mixing_ratio = ( self["water_vapour_mixing_ratio"][0] - self.delta_liquid_water_mixing_ratio / 2 @@ -133,6 +133,9 @@ def advance_parcel_vars(self): (self._tmp["rhod"][0] + self["rhod"][0]) / 2, self.mass_of_dry_air ) + def _compute_dz_dt(self, dt): + return self.w((self.particulator.n_steps + 1 / 2) * dt) # "mid-point" + def get_thd(self): return self["thd"] diff --git a/PySDM/formulae.py b/PySDM/formulae.py index 30d9fb43a9..4b796f5784 100644 --- a/PySDM/formulae.py +++ b/PySDM/formulae.py @@ -47,6 +47,7 @@ def __init__( # pylint: disable=too-many-locals hydrostatics: str = "ConstantGVapourMixingRatioAndThetaStd", 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_kinetic_fractionation_factors: str = "Null", @@ -86,6 +87,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/particulator.py b/PySDM/particulator.py index 4f7606ff72..32ae0697d5 100644 --- a/PySDM/particulator.py +++ b/PySDM/particulator.py @@ -8,6 +8,7 @@ from PySDM.backends.impl_common.freezing_attributes import ( SingularAttributes, TimeDependentAttributes, + TimeDependentHomogeneousAttributes, ) from PySDM.backends.impl_common.index import make_Index from PySDM.backends.impl_common.indexed_storage import make_IndexedStorage @@ -537,3 +538,18 @@ def immersion_freezing_singular(self, *, thaw: bool): thaw=thaw, ) self.attributes.mark_updated("signed water mass") + + def homogeneous_freezing_time_dependent(self, *, thaw: bool, rand: Storage): + self.backend.freeze_time_dependent_homogeneous( + rand=rand, + attributes=TimeDependentHomogeneousAttributes( + volume=self.attributes["volume"], + signed_water_mass=self.attributes["signed water mass"], + ), + timestep=self.dt, + cell=self.attributes["cell id"], + a_w_ice=self.environment["a_w_ice"], + temperature=self.environment["T"], + relative_humidity_ice=self.environment["RH_ice"], + thaw=thaw, + ) diff --git a/PySDM/physics/__init__.py b/PySDM/physics/__init__.py index 7437944de6..f2e2bfb43f 100644 --- a/PySDM/physics/__init__.py +++ b/PySDM/physics/__init__.py @@ -27,6 +27,7 @@ fragmentation_function, freezing_temperature_spectrum, heterogeneous_ice_nucleation_rate, + homogeneous_ice_nucleation_rate, hydrostatics, hygroscopicity, impl, diff --git a/PySDM/physics/constants_defaults.py b/PySDM/physics/constants_defaults.py index 39d89938dc..2f08f13f79 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)) @@ -295,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 @@ -322,8 +315,44 @@ ABIFM_C = np.inf """ 〃 """ +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_MIN_DA_W_ICE = 0.26 +""" 〃 """ +KOOP_MAX_DA_W_ICE = 0.34 + +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 / si.K +""" 〃 """ +KOOP_MURRAY_C2 = -25.9779 / si.K**2 +""" 〃 """ +KOOP_MURRAY_C3 = -0.868451 / si.K**3 +""" 〃 """ +KOOP_MURRAY_C4 = -1.66203e-2 / si.K**4 +""" 〃 """ +KOOP_MURRAY_C5 = -1.71736e-4 / si.K**5 +""" 〃 """ +KOOP_MURRAY_C6 = -7.46953e-7 / si.K**6 +""" 〃 """ + J_HET = np.nan -""" constant ice nucleation rate """ +J_HOM = np.nan +""" constant ice nucleation rates """ STRAUB_E_D1 = 0.04 * si.cm """ [Straub et al. 2010](https://doi.org/10.1175/2009JAS3175.1) """ 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..95fe7ed622 --- /dev/null +++ b/PySDM/physics/homogeneous_ice_nucleation_rate/__init__.py @@ -0,0 +1,9 @@ +""" +homogeneous-freezing rate (aka J_hom) formulations +""" + +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/constant.py b/PySDM/physics/homogeneous_ice_nucleation_rate/constant.py new file mode 100644 index 0000000000..9d4a79ef51 --- /dev/null +++ b/PySDM/physics/homogeneous_ice_nucleation_rate/constant.py @@ -0,0 +1,22 @@ +""" +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 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): # pylint: disable=unused-argument + return 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 new file mode 100644 index 0000000000..8a1526f4c2 --- /dev/null +++ b/PySDM/physics/homogeneous_ice_nucleation_rate/koop.py @@ -0,0 +1,35 @@ +""" +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)) +""" + +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 ( + 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 new file mode 100644 index 0000000000..a8a3f04380 --- /dev/null +++ b/PySDM/physics/homogeneous_ice_nucleation_rate/koop_corr.py @@ -0,0 +1,37 @@ +""" +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)) +""" + +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 ( + 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 new file mode 100644 index 0000000000..d3e41816f5 --- /dev/null +++ b/PySDM/physics/homogeneous_ice_nucleation_rate/koop_murray.py @@ -0,0 +1,38 @@ +""" +Koop and Murray homogeneous nucleation rate parameterization for pure water droplets +at water saturation +([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 ( + 10 + ** ( + const.KOOP_MURRAY_C0 + + 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 + ) 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..739bdf72cf --- /dev/null +++ b/PySDM/physics/homogeneous_ice_nucleation_rate/null.py @@ -0,0 +1,23 @@ +""" +do-nothing null formulation (needed as other formulations require parameters + to be set before instantiation of Formulae) +""" + +import numpy as np + + +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): # pylint: disable=unused-argument + return True + + @staticmethod + def d_a_w_ice_maximum(const, da_w_ice): # pylint: disable=unused-argument + return da_w_ice + + @staticmethod + def j_hom(const, T, d_a_w_ice): # pylint: disable=unused-argument + return np.nan diff --git a/PySDM/physics/terminal_velocity/__init__.py b/PySDM/physics/terminal_velocity/__init__.py index 2e012dc474..3434f7ad3a 100644 --- a/PySDM/physics/terminal_velocity/__init__.py +++ b/PySDM/physics/terminal_velocity/__init__.py @@ -1,4 +1,6 @@ -"""terminal velocity formulae""" +""" +terminal velocity formulae +""" from .rogers_yau import RogersYau from .gunn_kinzer_1949 import GunnKinzer1949 diff --git a/PySDM/physics/trivia.py b/PySDM/physics/trivia.py index 59be2d2a49..93f53ce6d8 100644 --- a/PySDM/physics/trivia.py +++ b/PySDM/physics/trivia.py @@ -83,6 +83,10 @@ def unfrozen(signed_water_mass): def unfrozen_and_saturated(signed_water_mass, relative_humidity): return signed_water_mass > 0 and relative_humidity > 1 + @staticmethod + def unfrozen_and_ice_saturated(signed_water_mass, relative_humidity_ice): + return signed_water_mass > 0 and relative_humidity_ice > 1 + @staticmethod def frozen_and_above_freezing_point(const, signed_water_mass, temperature): return signed_water_mass < 0 and temperature > const.T0 diff --git a/PySDM/products/condensation/__init__.py b/PySDM/products/condensation/__init__.py index 39613831a5..5574b07284 100644 --- a/PySDM/products/condensation/__init__.py +++ b/PySDM/products/condensation/__init__.py @@ -5,4 +5,4 @@ from .activable_fraction import ActivableFraction from .condensation_timestep import CondensationTimestepMax, CondensationTimestepMin from .event_rates import ActivatingRate, DeactivatingRate, RipeningRate -from .peak_supersaturation import PeakSupersaturation +from .peak_saturation import PeakSaturation diff --git a/PySDM/products/condensation/activable_fraction.py b/PySDM/products/condensation/activable_fraction.py index 14ec80fc04..b8bc8ef721 100644 --- a/PySDM/products/condensation/activable_fraction.py +++ b/PySDM/products/condensation/activable_fraction.py @@ -1,27 +1,38 @@ """ -fraction of particles with critical supersaturation lower than a given supersaturation +fraction of particles with critical saturation lower than a given saturation (passed as keyword argument while calling `get()`) """ +import numpy as np from PySDM.products.impl import MomentProduct, register_product @register_product() class ActivableFraction(MomentProduct): - def __init__(self, unit="dimensionless", name=None): + def __init__( + self, unit="dimensionless", name=None, filter_attr="critical saturation" + ): super().__init__(name=name, unit=unit) + self.filter_attr = filter_attr def register(self, builder): super().register(builder) - builder.request_attribute("critical supersaturation") + builder.request_attribute(self.filter_attr) def _impl(self, **kwargs): - s_max = kwargs["S_max"] + if self.filter_attr.startswith("critical saturation"): + s_max = kwargs["S_max"] + assert not np.isfinite(s_max) or 0 < s_max < 1.1 + filter_range = (0, s_max) + elif self.filter_attr.startswith("wet to critical volume ratio"): + filter_range = (1, np.inf) + else: + assert False self._download_moment_to_buffer( attr="volume", rank=0, - filter_range=(0, 1 + s_max / 100), - filter_attr="critical supersaturation", + filter_range=filter_range, + filter_attr=self.filter_attr, ) frac = self.buffer.copy() self._download_moment_to_buffer(attr="volume", rank=0) diff --git a/PySDM/products/condensation/peak_supersaturation.py b/PySDM/products/condensation/peak_saturation.py similarity index 84% rename from PySDM/products/condensation/peak_supersaturation.py rename to PySDM/products/condensation/peak_saturation.py index b3dc644bb5..ae36b22a0b 100644 --- a/PySDM/products/condensation/peak_supersaturation.py +++ b/PySDM/products/condensation/peak_saturation.py @@ -1,5 +1,5 @@ """ -highest supersaturation encountered while solving for condensation/evaporation (takes into account +highest saturation encountered while solving for condensation/evaporation (takes into account substeps thus values might differ from ambient saturation reported via `PySDM.products.ambient_thermodynamics.ambient_relative_humidity.AmbientRelativeHumidity`; fetching a value resets the maximum value) @@ -11,7 +11,7 @@ @register_product() -class PeakSupersaturation(Product): +class PeakSaturation(Product): def __init__(self, unit="dimensionless", name=None): super().__init__(unit=unit, name=name) self.condensation = None @@ -28,8 +28,8 @@ def register(self, builder): self.RH_max = np.full_like(self.buffer, np.nan) def _impl(self, **kwargs): - self.buffer[:] = self.RH_max[:] - 1 - self.RH_max[:] = -1 + self.buffer[:] = self.RH_max[:] + self.RH_max[:] = 0 return self.buffer def notify(self): diff --git a/docs/bibliography.json b/docs/bibliography.json index 01546a2c61..8d15c97406 100644 --- a/docs/bibliography.json +++ b/docs/bibliography.json @@ -897,5 +897,40 @@ ], "title": "Remarks on the deuterium excess in precipitation in cold regions", "label": "Fisher 1991 (Tellus B)" + }, + "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_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", + "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)" + }, + "https://doi.org/10.1029/2020JE006653": { + "usages": [ + "examples/PySDM_examples/Loftus_and_Wordsworth_2021/__init__.py" + ], + "title": "The Physics of Falling Raindrops in Diverse Planetary Atmospheres", + "label": "Kaitlyn Loftus, Robin D. Wordsworth (JGR Planets 2021)" } } diff --git a/docs/markdown/pysdm_landing.md b/docs/markdown/pysdm_landing.md index c24dbb1394..37c2eed75c 100644 --- a/docs/markdown/pysdm_landing.md +++ b/docs/markdown/pysdm_landing.md @@ -311,7 +311,7 @@ causes a subset of particles to activate into cloud droplets. Results of the simulation are plotted against vertical [`ParcelDisplacement`](https://open-atmos.github.io/PySDM/PySDM/products/housekeeping/parcel_displacement.html) and depict the evolution of -[`PeakSupersaturation`](https://open-atmos.github.io/PySDM/PySDM/products/condensation/peak_supersaturation.html), +[`PeakSaturation`](https://open-atmos.github.io/PySDM/PySDM/products/condensation/peak_saturation.html), [`EffectiveRadius`](https://open-atmos.github.io/PySDM/PySDM/products/size_spectral/effective_radius.html), [`ParticleConcentration`](https://open-atmos.github.io/PySDM/PySDM/products/size_spectral/particle_concentration.html#ParticleConcentration) and the @@ -367,7 +367,7 @@ attributes["kappa times dry volume"] = kappa * v_dry attributes["volume"] = formulae.trivia.volume(radius=r_wet) particulator = builder.build(attributes, products=[ - products.PeakSupersaturation(name="S_max", unit="%"), + products.PeakSaturation(name="S_max_percent", unit="%"), products.EffectiveRadius(name="r_eff", unit="um", radius_range=cloud_range), products.ParticleConcentration(name="n_c_cm3", unit="cm^-3", radius_range=cloud_range), products.WaterMixingRatio(name="liquid water mixing ratio", unit="g/kg", radius_range=cloud_range), @@ -455,7 +455,7 @@ attributes = py.dict(pyargs( ... )); particulator = builder.build(attributes, py.list({ ... - products.PeakSupersaturation(pyargs('name', 'S_max', 'unit', '%')), ... + products.PeakSaturation(pyargs('name', 'S_max_percent', 'unit', '%')), ... products.EffectiveRadius(pyargs('name', 'r_eff', 'unit', 'um', 'radius_range', cloud_range)), ... products.ParticleConcentration(pyargs('name', 'n_c_cm3', 'unit', 'cm^-3', 'radius_range', cloud_range)), ... products.WaterMixingRatio(pyargs('name', 'liquid water mixing ratio', 'unit', 'g/kg', 'radius_range', cloud_range)) ... @@ -549,7 +549,7 @@ attributes = { } particulator = builder.build(attributes, products=[ - products.PeakSupersaturation(name='S_max', unit='%'), + products.PeakSaturation(name='S_max_percent', unit='%'), products.EffectiveRadius(name='r_eff', unit='um', radius_range=cloud_range), products.ParticleConcentration(name='n_c_cm3', unit='cm^-3', radius_range=cloud_range), products.WaterMixingRatio(name='liquid water mixing ratio', unit='g/kg', radius_range=cloud_range), diff --git a/examples/PySDM_examples/Abdul_Razzak_Ghan_2000/fig_4_kinetic_limitations.ipynb b/examples/PySDM_examples/Abdul_Razzak_Ghan_2000/fig_4_kinetic_limitations.ipynb index 8bf0198e20..081fb11c98 100644 --- a/examples/PySDM_examples/Abdul_Razzak_Ghan_2000/fig_4_kinetic_limitations.ipynb +++ b/examples/PySDM_examples/Abdul_Razzak_Ghan_2000/fig_4_kinetic_limitations.ipynb @@ -196,7 +196,7 @@ } ], "source": [ - "for drop_id, Scrit in enumerate(output.attributes['critical supersaturation']):\n", + "for drop_id, Scrit in enumerate(output.attributes['critical saturation']):\n", " if drop_id < n_sd_per_mode:\n", " pyplot.plot(\n", " np.asarray(Scrit) - 1,\n", diff --git a/examples/PySDM_examples/Abdul_Razzak_Ghan_2000/run_ARG_parcel.py b/examples/PySDM_examples/Abdul_Razzak_Ghan_2000/run_ARG_parcel.py index e0cee0f4da..d2ffac39ef 100644 --- a/examples/PySDM_examples/Abdul_Razzak_Ghan_2000/run_ARG_parcel.py +++ b/examples/PySDM_examples/Abdul_Razzak_Ghan_2000/run_ARG_parcel.py @@ -28,7 +28,7 @@ def run_parcel( ): products = ( PySDM_products.WaterMixingRatio(unit="g/kg", name="liquid water mixing ratio"), - PySDM_products.PeakSupersaturation(name="S max"), + PySDM_products.PeakSaturation(name="S max"), PySDM_products.AmbientRelativeHumidity(name="RH"), PySDM_products.ParcelDisplacement(name="z"), ) @@ -54,7 +54,7 @@ def run_parcel( builder = Builder(backend=CPU(formulae), n_sd=n_sd, environment=env) builder.add_dynamic(AmbientThermodynamics()) builder.add_dynamic(Condensation()) - builder.request_attribute("critical supersaturation") + builder.request_attribute("critical saturation") attributes = { k: np.empty(0) for k in ("dry volume", "kappa times dry volume", "multiplicity") @@ -87,7 +87,7 @@ def run_parcel( "multiplicity": tuple([] for _ in range(particulator.n_sd)), "volume": tuple([] for _ in range(particulator.n_sd)), "critical volume": tuple([] for _ in range(particulator.n_sd)), - "critical supersaturation": tuple([] for _ in range(particulator.n_sd)), + "critical saturation": tuple([] for _ in range(particulator.n_sd)), } for _ in range(n_steps): @@ -109,7 +109,7 @@ def run_parcel( RHmax = np.nanmax(np.asarray(output["RH"])) for i, volume in enumerate(output_attributes["volume"]): if j * n_sd_per_mode <= i < (j + 1) * n_sd_per_mode: - if output_attributes["critical supersaturation"][i][-1] < RHmax: + if output_attributes["critical saturation"][i][-1] < RHmax: activated_drops_j_S += output_attributes["multiplicity"][i][-1] if output_attributes["critical volume"][i][-1] < volume[-1]: activated_drops_j_V += output_attributes["multiplicity"][i][-1] diff --git a/examples/PySDM_examples/Arabas_and_Shima_2017/fig_5.ipynb b/examples/PySDM_examples/Arabas_and_Shima_2017/fig_5.ipynb index d4798900f0..3f62f0c200 100644 --- a/examples/PySDM_examples/Arabas_and_Shima_2017/fig_5.ipynb +++ b/examples/PySDM_examples/Arabas_and_Shima_2017/fig_5.ipynb @@ -37,7 +37,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 7, "metadata": { "ExecuteTime": { "end_time": "2024-02-01T07:21:08.342040Z", @@ -48,7 +48,7 @@ "source": [ "from PySDM_examples.Arabas_and_Shima_2017.example import Simulation, setups\n", "from open_atmos_jupyter_utils import show_plot\n", - "from PySDM.physics import si\n", + "from PySDM.physics import si, in_unit\n", "import numpy as np\n", "import matplotlib.pyplot as plt" ] @@ -72,7 +72,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 8, "metadata": { "ExecuteTime": { "end_time": "2024-02-01T07:19:49.936545Z", @@ -82,20 +82,4336 @@ "outputs": [ { "data": { - "text/plain": "
", - "image/svg+xml": "\n\n\n \n \n \n \n 2024-02-01T08:19:49.818753\n image/svg+xml\n \n \n Matplotlib v3.8.2, 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 \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n" + "image/svg+xml": [ + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " 2025-05-19T13:24:15.209953\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", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \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": { - "text/plain": "HTML(value=\"./fig_5.pdf
\")", "application/vnd.jupyter.widget-view+json": { + "model_id": "771d17a840d348efb10346fea6f5ed50", "version_major": 2, - "version_minor": 0, - "model_id": "ade35c86c33944db96ed11da6a58a8fb" - } + "version_minor": 0 + }, + "text/plain": [ + "HBox(children=(HTML(value=\"./fig_5.pdf
\"), HTML(value=\" 0:\n", + " if levels['CB'] is None and output[\"S_max\"][-1] > 1:\n", " levels['CB'] = .5 * (output[\"z\"][-1] + output[\"z\"][-2])\n", " \n", " if levels['0C'] is None and output[\"T\"][-1] < const.T0:\n", @@ -1421,7 +1421,7 @@ "xy2 = xy1.twiny()\n", "\n", "xy1.plot(\n", - " (np.asarray(output['S_max']) + 1) * 100,\n", + " np.asarray(output['S_max']) * 100,\n", " np.asarray(output['z']) + alt_initial\n", ")\n", "xy2.plot(\n", diff --git a/examples/PySDM_examples/Jaruga_and_Pawlowska_2018/fig_2.ipynb b/examples/PySDM_examples/Jaruga_and_Pawlowska_2018/fig_2.ipynb index 280a595dd7..1c8d8b0a97 100644 --- a/examples/PySDM_examples/Jaruga_and_Pawlowska_2018/fig_2.ipynb +++ b/examples/PySDM_examples/Jaruga_and_Pawlowska_2018/fig_2.ipynb @@ -88,7 +88,7 @@ "source": [ "default_settings = Settings(1,1,1)\n", "products = (\n", - " PySDM_products.PeakSupersaturation(unit='%', name='S_max'),\n", + " PySDM_products.PeakSaturation(name='S_max'),\n", " PySDM_products.ParticleConcentration(name='n_c_cm3', unit='cm^-3', radius_range=default_settings.cloud_radius_range),\n", " PySDM_products.Acidity(name='pH_conc_H_volume_weighted', radius_range=default_settings.cloud_radius_range),\n", " PySDM_products.AqueousMoleFraction('S_VI', name='aq_S_VI_ppb', unit='ppb')\n", @@ -152,7 +152,7 @@ "pH = []\n", "sulfate_ppt = []\n", "for simulation in simulations:\n", - " smax.append(np.nanmax(simulation['output'][\"S_max\"]))\n", + " smax.append((np.nanmax(simulation['output'][\"S_max\"]) - 1) * 100)\n", " droplet_number.append(np.nanmax(simulation['output'][\"n_c_cm3\"]))\n", " pH.append(simulation['output'][\"pH_conc_H_volume_weighted\"][-1])\n", " S_VI = simulation['output'][\"aq_S_VI_ppb\"]\n", diff --git a/examples/PySDM_examples/Jensen_and_Nugent_2017/Fig_4_and_7_and_Tab_4_bottom_rows.ipynb b/examples/PySDM_examples/Jensen_and_Nugent_2017/Fig_4_and_7_and_Tab_4_bottom_rows.ipynb index 57a611d589..ed08eabfff 100644 --- a/examples/PySDM_examples/Jensen_and_Nugent_2017/Fig_4_and_7_and_Tab_4_bottom_rows.ipynb +++ b/examples/PySDM_examples/Jensen_and_Nugent_2017/Fig_4_and_7_and_Tab_4_bottom_rows.ipynb @@ -167,7 +167,7 @@ " \n", ")\n", "height_above_cloud_base = np.asarray(output[\"products\"][\"z\"]) - settings.z0 - CLOUD_BASE\n", - "SS = np.asarray(output[\"products\"][\"S_max\"])\n", + "SS = np.asarray(output[\"products\"][\"S_max\"]) - 1\n", "SS_eq = {}\n", "SS_ef = {}\n", "for mask_label, mask in masks.items():\n", @@ -183,7 +183,7 @@ " plot_drops_with_dry_radii_um=dry_radii_um,\n", " simulation_r_dry=simulation.r_dry\n", " )):\n", - " SS_eq[dry_radii_um[i]] = np.asarray(output[\"attributes\"][\"equilibrium supersaturation\"][drop_id]) - 1\n", + " SS_eq[dry_radii_um[i]] = np.asarray(output[\"attributes\"][\"equilibrium saturation\"][drop_id]) - 1\n", " label_suffix = f' for r_d={in_unit(simulation.r_dry[drop_id], si.um):.2} µm'\n", " axs[mask_label].plot(\n", " in_unit(SS_eq[dry_radii_um[i]], PER_CENT)[mask],\n", diff --git a/examples/PySDM_examples/Jensen_and_Nugent_2017/simulation.py b/examples/PySDM_examples/Jensen_and_Nugent_2017/simulation.py index 465d061395..289e8f27cc 100644 --- a/examples/PySDM_examples/Jensen_and_Nugent_2017/simulation.py +++ b/examples/PySDM_examples/Jensen_and_Nugent_2017/simulation.py @@ -6,7 +6,7 @@ from PySDM.physics import si from PySDM.backends import CPU from PySDM.products import ( - PeakSupersaturation, + PeakSaturation, ParcelDisplacement, Time, ActivatedMeanRadius, @@ -52,7 +52,7 @@ def __init__( ), ) - additional_derived_attributes = ("radius", "equilibrium supersaturation") + additional_derived_attributes = ("radius", "equilibrium saturation") for additional_derived_attribute in additional_derived_attributes: builder.request_attribute(additional_derived_attribute) @@ -93,7 +93,7 @@ def __init__( builder.build( attributes=attributes, products=( - PeakSupersaturation(name="S_max"), + PeakSaturation(name="S_max"), ParcelDisplacement(name="z"), Time(name="t"), ActivatedMeanRadius( diff --git a/examples/PySDM_examples/Jouzel_and_Merlivat_1984/fig_8_9.ipynb b/examples/PySDM_examples/Jouzel_and_Merlivat_1984/fig_8_9.ipynb index 6b30aeef25..aca5e51f28 100644 --- a/examples/PySDM_examples/Jouzel_and_Merlivat_1984/fig_8_9.ipynb +++ b/examples/PySDM_examples/Jouzel_and_Merlivat_1984/fig_8_9.ipynb @@ -187,7 +187,7 @@ "pyplot.gca().set(\n", " ylabel='$\\\\alpha_s\\\\alpha_k$',\n", " xlabel='Si',\n", - " title = \"$\\\\alpha_s\\\\alpha_k$ for $^{18}$O of supersaturation over ice\"\n", + " title = \"$\\\\alpha_s\\\\alpha_k$ for $^{18}$O of saturation over ice\"\n", ")\n", "pyplot.grid()\n", "pyplot.legend()\n", diff --git a/examples/PySDM_examples/Kreidenweis_et_al_2003/simulation.py b/examples/PySDM_examples/Kreidenweis_et_al_2003/simulation.py index c498e1f3de..425893e217 100644 --- a/examples/PySDM_examples/Kreidenweis_et_al_2003/simulation.py +++ b/examples/PySDM_examples/Kreidenweis_et_al_2003/simulation.py @@ -106,7 +106,7 @@ def __init__(self, settings, products=None): PySDM_products.TotalDryMassMixingRatio( settings.DRY_RHO, name="q_dry", unit="ug/kg" ), - PySDM_products.PeakSupersaturation(unit="%", name="S_max"), + PySDM_products.PeakSaturation(unit="%", name="S_max_percent"), PySDM_products.ParticleSpecificConcentration( radius_range=settings.cloud_radius_range, name="n_c_mg", unit="mg^-1" ), diff --git a/examples/PySDM_examples/Loftus_and_Wordsworth_2021/__init__.py b/examples/PySDM_examples/Loftus_and_Wordsworth_2021/__init__.py new file mode 100644 index 0000000000..2b6bbaec44 --- /dev/null +++ b/examples/PySDM_examples/Loftus_and_Wordsworth_2021/__init__.py @@ -0,0 +1,9 @@ +# pylint: disable=line-too-long +""" +The Physics of Falling Raindrops in Diverse Planetary Atmospheres +[Loftus, K., & Wordsworth, R. D. 2021 (Journal of Geophysical Research: Planets, 126)](https://doi.org/10.1029/2020JE006653) + +""" + +from .settings import Settings +from .simulation import Simulation diff --git a/examples/PySDM_examples/Loftus_and_Wordsworth_2021/figure_2.ipynb b/examples/PySDM_examples/Loftus_and_Wordsworth_2021/figure_2.ipynb new file mode 100644 index 0000000000..7fa0adb654 --- /dev/null +++ b/examples/PySDM_examples/Loftus_and_Wordsworth_2021/figure_2.ipynb @@ -0,0 +1,14713 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "1e8d983b", + "metadata": {}, + "outputs": [], + "source": [ + "!pip install joblib" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d8f644e9", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "from numba import njit\n", + "from joblib import Parallel, delayed\n", + "from open_atmos_jupyter_utils import show_plot\n", + "\n", + "from PySDM import Formulae\n", + "from PySDM.physics import si\n", + "from scipy.optimize import fsolve\n", + "from PySDM_examples.Loftus_and_Wordsworth_2021 import Settings\n", + "from PySDM_examples.Loftus_and_Wordsworth_2021.planet import EarthLike\n", + "from PySDM_examples.Loftus_and_Wordsworth_2021 import Simulation" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "92a3a574", + "metadata": {}, + "outputs": [], + "source": [ + "formulae = Formulae(\n", + " ventilation=\"PruppacherAndRasmussen1979\",\n", + " saturation_vapour_pressure=\"AugustRocheMagnus\",\n", + " diffusion_coordinate=\"WaterMassLogarithm\",\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "41f0ed6d", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "300.0" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "new_Earth = EarthLike()\n", + "print(new_Earth.T_STP)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d3a8f4b9", + "metadata": {}, + "outputs": [], + "source": [ + "radius_array = np.logspace(-4.5, -2.5, 50) * si.m\n", + "RH_array = np.linspace(0.25, 0.99, 50)\n", + "output_matrix = np.full((len(RH_array), len(radius_array)), np.nan)\n", + "const = formulae.constants\n", + "\n", + "\n", + "@njit()\n", + "def mix(dry, vap, ratio):\n", + " return (dry + ratio * vap) / (1 + ratio)\n", + "\n", + "\n", + "# pylint: disable=redefined-outer-name\n", + "def compute_one_RH(index, RH_value):\n", + " \"\"\"\n", + " Compute one row of the output_matrix for a given RH.\n", + " Returns a 1D numpy array of length len(radius_array).\n", + " \"\"\"\n", + " new_Earth.RH_zref = RH_value\n", + "\n", + " pvs = formulae.saturation_vapour_pressure.pvs_water(new_Earth.T_STP)\n", + " initial_water_vapour_mixing_ratio = const.eps / (\n", + " new_Earth.p_STP / new_Earth.RH_zref / pvs - 1\n", + " )\n", + "\n", + " Rair = mix(const.Rd, const.Rv, initial_water_vapour_mixing_ratio)\n", + " c_p = mix(const.c_pd, const.c_pv, initial_water_vapour_mixing_ratio)\n", + "\n", + " def f(x):\n", + " return initial_water_vapour_mixing_ratio / (\n", + " initial_water_vapour_mixing_ratio + const.eps\n", + " ) * new_Earth.p_STP * (x / new_Earth.T_STP) ** (\n", + " c_p / Rair\n", + " ) - formulae.saturation_vapour_pressure.pvs_water(x)\n", + "\n", + " tdews = fsolve(f, [150, 300])\n", + " Tcloud = np.max(tdews)\n", + " Zcloud = (new_Earth.T_STP - Tcloud) * c_p / new_Earth.g_std\n", + " thstd = formulae.trivia.th_std(new_Earth.p_STP, new_Earth.T_STP)\n", + "\n", + " pcloud = formulae.hydrostatics.p_of_z_assuming_const_th_and_initial_water_vapour_mixing_ratio(\n", + " new_Earth.p_STP, thstd, initial_water_vapour_mixing_ratio, Zcloud\n", + " )\n", + "\n", + " np.testing.assert_approx_equal(\n", + " actual=pcloud\n", + " * (\n", + " initial_water_vapour_mixing_ratio\n", + " / (initial_water_vapour_mixing_ratio + const.eps)\n", + " )\n", + " / formulae.saturation_vapour_pressure.pvs_water(Tcloud),\n", + " desired=1,\n", + " significant=4,\n", + " )\n", + "\n", + " output = None\n", + " row_data = np.full(len(radius_array), np.nan)\n", + " for j, r in enumerate(radius_array[::-1]):\n", + " settings = Settings(\n", + " planet=new_Earth,\n", + " r_wet=r,\n", + " mass_of_dry_air=1e5 * si.kg,\n", + " initial_water_vapour_mixing_ratio=initial_water_vapour_mixing_ratio,\n", + " pcloud=pcloud,\n", + " Zcloud=Zcloud,\n", + " Tcloud=Tcloud,\n", + " formulae=formulae,\n", + " )\n", + " simulation = Simulation(settings)\n", + " try:\n", + " output = simulation.run()\n", + " if output[\"z\"][-1] > 0:\n", + " row_data[j] = np.nan\n", + " break\n", + " row_data[j] = 1 - (output[\"r\"][-1] / (r * 1e6))\n", + " except Exception as _: # pylint: disable=broad-exception-caught\n", + " break\n", + "\n", + " return index, row_data, output" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4352de81", + "metadata": {}, + "outputs": [], + "source": [ + "all_rows = Parallel(n_jobs=os.cpu_count())(\n", + " delayed(compute_one_RH)(index, RH_value) for index, RH_value in enumerate(RH_array[::-1])\n", + ")\n", + "\n", + "last_output = None\n", + "for index, row_data, output in all_rows:\n", + " output_matrix[index] = row_data\n", + " last_output = output" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "8e6027d8", + "metadata": {}, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " 2025-06-10T15:10:38.519047\n", + " image/svg+xml\n", + " \n", + " \n", + " Matplotlib v3.10.3, 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" + ], + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "53496ecd340048b79ff90faa9da71b6e", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "HBox(children=(HTML(value=\".\\\\tmp_nham5pj.pdf
\"), HTML(val…" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "row_data = output_matrix[18, ::-1] # Reverse the row for plotting\n", + "fig, ax = plt.subplots(\n", + " 2,\n", + " 1,\n", + " figsize=(6, 6),\n", + " sharex=True,\n", + " gridspec_kw={\"height_ratios\": [1, 3]},\n", + " constrained_layout=True,\n", + ")\n", + "ax[0].plot(radius_array, row_data, label=f\"Surface RH = {RH_array[-18]:.2f} %\")\n", + "ax[0].set_ylabel(\"Mass evaporated (%)\")\n", + "ax[0].legend()\n", + "\n", + "h = ax[1].contourf(\n", + " radius_array,\n", + " RH_array[::-1],\n", + " output_matrix[:, ::-1],\n", + " cmap=\"gray_r\",\n", + " levels=np.linspace(0, 1, 100),\n", + ")\n", + "ax[1].set_xscale(\"log\")\n", + "\n", + "# Add labels\n", + "ax[1].set_xlabel(\"Radius (µm)\")\n", + "ax[1].set_ylabel(\"Surface RH (%)\")\n", + "\n", + "cbar = fig.colorbar(h, ax=ax, shrink=0.4)\n", + "cbar.set_label(\"fraction Mass evaporated\")\n", + "contour_levels = [0.1] # Define the level for the contour\n", + "ax[1].contour(\n", + " radius_array,\n", + " RH_array[::-1],\n", + " output_matrix[:, ::-1],\n", + " levels=contour_levels,\n", + " colors=\"red\",\n", + " linewidths=1.5,\n", + ")\n", + "show_plot()" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "369fa24a", + "metadata": {}, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " 2025-06-10T15:10:39.361235\n", + " image/svg+xml\n", + " \n", + " \n", + " Matplotlib v3.10.3, 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" + ], + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "0baa5c73262744e4890c68dcfaa37b13", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "HBox(children=(HTML(value=\".\\\\tmps5jrn8sn.pdf
\"), HTML(val…" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "fig, axs = plt.subplots(1, 3, figsize=(10, 8), sharey=True)\n", + "axs[0].plot(last_output[\"r\"], last_output[\"z\"], label=\"Radius\")\n", + "axs[0].set_ylabel(\"Height (m)\")\n", + "axs[0].set_xlabel(\"Radius (um)\")\n", + "axs[0].legend()\n", + "axs[1].plot(last_output[\"S\"], last_output[\"z\"], label=\"Supersaturation\")\n", + "axs[1].set_xlabel(\"Supersaturation\")\n", + "axs[1].set_ylabel(\"Height (m)\")\n", + "axs[1].legend()\n", + "axs[2].plot(last_output[\"t\"], last_output[\"z\"], label=\"Height\")\n", + "axs[2].set_ylabel(\"Height (m)\")\n", + "axs[2].set_xlabel(\"Time (s)\")\n", + "axs[2].legend()\n", + "show_plot()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.2" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/PySDM_examples/Loftus_and_Wordsworth_2021/parcel.py b/examples/PySDM_examples/Loftus_and_Wordsworth_2021/parcel.py new file mode 100644 index 0000000000..6b1b2e30fb --- /dev/null +++ b/examples/PySDM_examples/Loftus_and_Wordsworth_2021/parcel.py @@ -0,0 +1,34 @@ +""" +Modified Parcel class for the Loftus and Wordsworth 2021 example. +""" + +from PySDM.environments.parcel import Parcel + + +class AlienParcel(Parcel): + def __init__( + self, + dt, + mass_of_dry_air: float, + pcloud: float, + initial_water_vapour_mixing_ratio: float, + Tcloud: float, + w: float = 0, + zcloud: float = 0, + mixed_phase=False, + ): + super().__init__( + dt=dt, + mass_of_dry_air=mass_of_dry_air, + p0=pcloud, + initial_water_vapour_mixing_ratio=initial_water_vapour_mixing_ratio, + T0=Tcloud, + w=w, + z0=zcloud, + mixed_phase=mixed_phase, + variables=None, + ) + + # pylint: disable=unused-argument + def _compute_dz_dt(self, dt): + return -self.particulator.attributes["terminal velocity"].to_ndarray()[0] diff --git a/examples/PySDM_examples/Loftus_and_Wordsworth_2021/planet.py b/examples/PySDM_examples/Loftus_and_Wordsworth_2021/planet.py new file mode 100644 index 0000000000..d360626619 --- /dev/null +++ b/examples/PySDM_examples/Loftus_and_Wordsworth_2021/planet.py @@ -0,0 +1,128 @@ +""" +Planetary properties used in calculations. + +Values are primarily taken from Table 1 of Loftus & Wordsworth (2021), +unless otherwise noted. +Each variable represents a physical property or atmospheric +composition relevant for cloud microphysics modeling. +""" + +from dataclasses import dataclass +from typing import Optional, Dict, Any + +from PySDM.physics.constants import si + + +@dataclass +class Planet: + g_std: float + T_STP: float + p_STP: float + RH_zref: float + dry_molar_conc_H2: float + dry_molar_conc_He: float + dry_molar_conc_N2: float + dry_molar_conc_O2: float + dry_molar_conc_CO2: float + H_LCL: Optional[float] = None + + def to_dict(self) -> Dict[str, Any]: + return vars(self) + + +@dataclass +class EarthLike(Planet): + g_std: float = 9.82 * si.metre / si.second**2 + T_STP: float = 300 * si.kelvin + p_STP: float = 1.01325 * 1e5 * si.Pa + RH_zref: float = 0.75 + dry_molar_conc_H2: float = 0 + dry_molar_conc_He: float = 0 + dry_molar_conc_N2: float = 1 + dry_molar_conc_O2: float = 0 + dry_molar_conc_CO2: float = 0 + H_LCL: float = 8.97 * si.kilometre + + +@dataclass +class Earth(Planet): + g_std: float = 9.82 * si.metre / si.second**2 + T_STP: float = 290 * si.kelvin + p_STP: float = 1.01325 * 1e5 * si.Pa + RH_zref: float = 0.75 + dry_molar_conc_H2: float = 0 + dry_molar_conc_He: float = 0 + dry_molar_conc_N2: float = 0.8 + dry_molar_conc_O2: float = 0.2 + dry_molar_conc_CO2: float = 0 + H_LCL: float = 8.41 * si.kilometre + + +@dataclass +class EarlyMars(Planet): + g_std: float = 3.71 * si.metre / si.second**2 + T_STP: float = 290 * si.kelvin + p_STP: float = 2 * 1e5 * si.Pa + RH_zref: float = 0.75 + dry_molar_conc_H2: float = 0 + dry_molar_conc_He: float = 0 + dry_molar_conc_N2: float = 0 + dry_molar_conc_O2: float = 0 + dry_molar_conc_CO2: float = 1 + H_LCL: float = 14.5 * si.kilometre + + +@dataclass +class Jupiter(Planet): + g_std: float = 24.84 * si.metre / si.second**2 + T_STP: float = 274 * si.kelvin + p_STP: float = 4.85 * 1e5 * si.Pa + RH_zref: float = 1 + dry_molar_conc_H2: float = 0.864 + dry_molar_conc_He: float = 0.136 + dry_molar_conc_N2: float = 0 + dry_molar_conc_O2: float = 0 + dry_molar_conc_CO2: float = 0 + H_LCL: float = 39.8 * si.kilometre + + +@dataclass +class Saturn(Planet): + g_std: float = 10.47 * si.metre / si.second**2 + T_STP: float = 284 * si.kelvin + p_STP: float = 10.4 * 1e5 * si.Pa + RH_zref: float = 1 + dry_molar_conc_H2: float = 0.88 + dry_molar_conc_He: float = 0.12 + dry_molar_conc_N2: float = 0 + dry_molar_conc_O2: float = 0 + dry_molar_conc_CO2: float = 0 + H_LCL: float = 99.2 * si.kilometre + + +@dataclass +class K2_18B(Planet): + g_std: float = 12.44 * si.metre / si.second**2 + T_STP: float = 275 * si.kelvin + p_STP: float = 0.1 * 1e5 * si.Pa + RH_zref: float = 1 + dry_molar_conc_H2: float = 0.9 + dry_molar_conc_He: float = 0.1 + dry_molar_conc_N2: float = 0 + dry_molar_conc_O2: float = 0 + dry_molar_conc_CO2: float = 0 + H_LCL: float = 56.6 * si.kilometre + + +@dataclass +class CompositeTest(Planet): + g_std: float = 9.82 * si.metre / si.second**2 + T_STP: float = 275 * si.kelvin + p_STP: float = 0.75 * 1e5 * si.Pa + RH_zref: float = 1 + dry_molar_conc_H2: float = 0.1 + dry_molar_conc_He: float = 0.1 + dry_molar_conc_N2: float = 0.1 + dry_molar_conc_O2: float = 0.1 + dry_molar_conc_CO2: float = 0.1 + H_LCL: Optional[float] = None diff --git a/examples/PySDM_examples/Loftus_and_Wordsworth_2021/settings.py b/examples/PySDM_examples/Loftus_and_Wordsworth_2021/settings.py new file mode 100644 index 0000000000..8f5d6f95ad --- /dev/null +++ b/examples/PySDM_examples/Loftus_and_Wordsworth_2021/settings.py @@ -0,0 +1,50 @@ +""" +Planetary Properties, Loftus and Wordsworth 2021 Table 1 +""" + +from pystrict import strict + +from PySDM_examples.Loftus_and_Wordsworth_2021.planet import Planet + +from PySDM import Formulae +from PySDM.dynamics import condensation +from PySDM.physics.constants import si + + +@strict +class Settings: + # pylint: disable=too-many-arguments + def __init__( + self, + r_wet: float, + mass_of_dry_air: float, + planet: Planet, + initial_water_vapour_mixing_ratio: float, + pcloud: float, + Zcloud: float, + Tcloud: float, + formulae: Formulae = None, + ): + self.formulae = formulae or Formulae( + saturation_vapour_pressure="AugustRocheMagnus", + diffusion_coordinate="WaterMassLogarithm", + ) + + self.initial_water_vapour_mixing_ratio = initial_water_vapour_mixing_ratio + self.p0 = planet.p_STP + self.RH0 = planet.RH_zref + self.kappa = 0.2 + self.T0 = planet.T_STP + self.z_half = 150 * si.metres + self.dt = 1 * si.second + self.pcloud = pcloud + self.Zcloud = Zcloud + self.Tcloud = Tcloud + + self.r_wet = r_wet + self.mass_of_dry_air = mass_of_dry_air + self.n_output = 500 + + self.rtol_x = 0.5 * (condensation.DEFAULTS.rtol_x) + self.rtol_thd = condensation.DEFAULTS.rtol_thd + self.dt_cond_range = condensation.DEFAULTS.cond_range diff --git a/examples/PySDM_examples/Loftus_and_Wordsworth_2021/simulation.py b/examples/PySDM_examples/Loftus_and_Wordsworth_2021/simulation.py new file mode 100644 index 0000000000..a182885e99 --- /dev/null +++ b/examples/PySDM_examples/Loftus_and_Wordsworth_2021/simulation.py @@ -0,0 +1,92 @@ +""" +Simulation for the Loftus and Wordsworth (2021) example. +""" + +import numpy as np + +from PySDM_examples.Loftus_and_Wordsworth_2021.parcel import AlienParcel + +import PySDM.products as PySDM_products +from PySDM.backends import CPU +from PySDM.builder import Builder +from PySDM.dynamics import AmbientThermodynamics, Condensation +from PySDM.physics import constants as const + +MIN_DROPLET_RADIUS = 1e-6 + + +class Simulation: + def __init__(self, settings, backend=CPU): + builder = Builder( + backend=backend( + formulae=settings.formulae, + **( + {"override_jit_flags": {"parallel": False}} + if backend == CPU + else {} + ), + ), + n_sd=1, + environment=AlienParcel( + dt=settings.dt, + mass_of_dry_air=settings.mass_of_dry_air, + pcloud=settings.pcloud, + zcloud=settings.Zcloud, + initial_water_vapour_mixing_ratio=settings.initial_water_vapour_mixing_ratio, + Tcloud=settings.Tcloud, + ), + ) + + builder.add_dynamic(AmbientThermodynamics()) + builder.add_dynamic( + Condensation( + rtol_x=settings.rtol_x, + rtol_thd=settings.rtol_thd, + dt_cond_range=settings.dt_cond_range, + ) + ) + builder.request_attribute("terminal velocity") + + attributes = {} + r_dry = 1e-10 + attributes["dry volume"] = settings.formulae.trivia.volume(radius=r_dry) + attributes["kappa times dry volume"] = attributes["dry volume"] * settings.kappa + attributes["multiplicity"] = np.array([1], dtype=np.int64) + r_wet = settings.r_wet + attributes["volume"] = settings.formulae.trivia.volume(radius=r_wet) + products = [ + PySDM_products.MeanRadius(name="radius_m1", unit="um"), + PySDM_products.ParcelDisplacement(name="z"), + PySDM_products.AmbientRelativeHumidity(name="RH", unit="%"), + PySDM_products.Time(name="t"), + ] + + self.particulator = builder.build(attributes, products) + + def save(self, output): + cell_id = 0 + output["r"].append( + self.particulator.products["radius_m1"].get(unit=const.si.m)[cell_id] + ) + + output["z"].append(self.particulator.products["z"].get()[cell_id]) + output["S"].append(self.particulator.products["RH"].get()[cell_id] / 100 - 1) + output["t"].append(self.particulator.products["t"].get()) + + def run(self): + output = { + "r": [], + "S": [], + "z": [], + "t": [], + } + + self.save(output) + while ( + self.particulator.environment["z"][0] > 0 + and output["r"][-1] > MIN_DROPLET_RADIUS + ): + self.particulator.run(1) + self.save(output) + + return output diff --git a/examples/PySDM_examples/Lowe_et_al_2019/fig_2.ipynb b/examples/PySDM_examples/Lowe_et_al_2019/fig_2.ipynb index 6cb47b68d4..0bce96f9b5 100644 --- a/examples/PySDM_examples/Lowe_et_al_2019/fig_2.ipynb +++ b/examples/PySDM_examples/Lowe_et_al_2019/fig_2.ipynb @@ -19,32 +19,30 @@ }, { "cell_type": "code", - "execution_count": 1, "metadata": { "ExecuteTime": { - "end_time": "2023-12-29T11:52:36.478184Z", - "start_time": "2023-12-29T11:52:36.471988Z" + "end_time": "2025-06-15T07:32:09.342496Z", + "start_time": "2025-06-15T07:32:09.336429Z" } }, - "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": 1 }, { "cell_type": "code", - "execution_count": 2, "metadata": { "ExecuteTime": { - "end_time": "2023-12-29T14:06:33.850244Z", - "start_time": "2023-12-29T14:06:33.840602Z" + "end_time": "2025-06-15T07:32:12.834828Z", + "start_time": "2025-06-15T07:32:09.355937Z" } }, - "outputs": [], "source": [ "from PySDM_examples.Lowe_et_al_2019 import Settings, Simulation\n", "from PySDM_examples.Lowe_et_al_2019.aerosol_code import AerosolBoreal, AerosolMarine, AerosolNascent\n", @@ -59,18 +57,18 @@ "import numpy as np\n", "import matplotlib\n", "from matplotlib import pyplot" - ] + ], + "outputs": [], + "execution_count": 2 }, { "cell_type": "code", - "execution_count": 3, "metadata": { "ExecuteTime": { - "end_time": "2023-12-29T11:54:54.155673Z", - "start_time": "2023-12-29T11:52:41.483940Z" + "end_time": "2025-06-15T07:32:51.723624Z", + "start_time": "2025-06-15T07:32:12.926684Z" } }, - "outputs": [], "source": [ "output = {}\n", "\n", @@ -95,2147 +93,18 @@ " output[key]['Na_tot'] = Sum(\n", " tuple(settings.aerosol.modes[i]['spectrum']\n", " for i in range(len(settings.aerosol.modes)))).norm_factor" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": 4, "metadata": { "ExecuteTime": { - "end_time": "2023-12-29T11:54:55.991461Z", - "start_time": "2023-12-29T11:54:54.153896Z" + "end_time": "2025-06-15T07:32:52.168894Z", + "start_time": "2025-06-15T07:32:51.735042Z" } }, - "outputs": [ - { - "data": { - "image/svg+xml": [ - "\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " 2024-06-15T12:58:51.625873\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" - ], - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "1308691ee03242aabea1b23f5074e625", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "HTML(value=\"./fig_2ab.pdf
\")" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], "source": [ "figsize = (11, 4)\n", "pyplot.rc('font', size=14)\n", @@ -2245,8 +114,8 @@ "for idx, var in enumerate(vlist):\n", " for key, out_item in output.items():\n", " Y = np.asarray(out_item['z'])\n", - " if var == 'RH':\n", - " X = np.asarray(out_item[var]) - 100\n", + " if var in ('S_max'):\n", + " X = (np.asarray(out_item[var]) - 1) * 100\n", " else:\n", " X = out_item[var]\n", " axs[idx].plot(X, Y, \n", @@ -2275,1525 +144,43 @@ " ax.grid()\n", "axs[0].legend(fontsize=8)\n", "show_plot(\"fig_2ab.pdf\")" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": { - "ExecuteTime": { - "end_time": "2023-12-29T14:07:59.522989Z", - "start_time": "2023-12-29T14:07:59.296920Z" - } - }, + ], "outputs": [ { "data": { - "image/svg+xml": [ - "\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " 2024-06-15T12:58:52.224385\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" - ], "text/plain": [ - "
" - ] + "
" + ], + "image/svg+xml": "\n\n\n \n \n \n \n 2025-06-15T09:32:52.136502\n image/svg+xml\n \n \n Matplotlib v3.9.2, 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" }, "metadata": {}, "output_type": "display_data" }, { "data": { + "text/plain": [ + "HBox(children=(HTML(value=\"./fig_2ab.pdf
\"), HTML(value=\"./fig_2c.pdf
\")" - ] + "version_minor": 0, + "model_id": "b2652dcdf01049caad07ac627062377a" + } }, "metadata": {}, "output_type": "display_data" } ], + "execution_count": 4 + }, + { + "cell_type": "code", + "metadata": { + "ExecuteTime": { + "end_time": "2025-06-15T07:32:52.370265Z", + "start_time": "2025-06-15T07:32:52.177541Z" + } + }, "source": [ "from scipy.ndimage import uniform_filter1d\n", "\n", @@ -3823,14 +210,46 @@ "axs.set_xlim(xticks[0], xticks[-1])\n", "axs.get_xaxis().set_major_formatter(matplotlib.ticker.ScalarFormatter())\n", "show_plot(\"fig_2c.pdf\")" - ] + ], + "outputs": [ + { + "data": { + "text/plain": [ + "
" + ], + "image/svg+xml": "\n\n\n \n \n \n \n 2025-06-15T09:32:52.350925\n image/svg+xml\n \n \n Matplotlib v3.9.2, 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" + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "HBox(children=(HTML(value=\"./fig_2c.pdf
\"), HTML(value=\"\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " 2024-06-18T16:26:41.403284\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" - ], "text/plain": [ "
" - ] + ], + "image/svg+xml": "\n\n\n \n \n \n \n 2025-06-15T10:10:33.900169\n image/svg+xml\n \n \n Matplotlib v3.9.2, 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" }, "metadata": {}, "output_type": "display_data" }, { "data": { + "text/plain": [ + "HBox(children=(HTML(value=\"./supersaturation.pdf
\"), HT…" + ], "application/vnd.jupyter.widget-view+json": { - "model_id": "d7edbfd94195449a83f1c5d767e5610c", "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "HTML(value=\"./supersaturation.pdf
\")" - ] + "version_minor": 0, + "model_id": "2c65384c54db4e3e9bdb53680f672ded" + } }, "metadata": {}, "output_type": "display_data" } ], - "source": [ - "fig, axs = pyplot.subplots(1, 2, sharey=True, figsize=(10, 5))\n", - "\n", - "axS = axs[0]\n", - "axS.plot(np.asarray(output['products']['S_max'])-100, output['products']['z'], color='black')\n", - "axS.set_ylabel('Displacement [m]')\n", - "axS.set_xlabel('Supersaturation [%]')\n", - "axS.set_xlim(0, 0.7)\n", - "axS.set_ylim(0, 250)\n", - "axS.text(0.3, 52, f\"max S = {np.nanmax(output['products']['S_max'])-100:.2f}%\")\n", - "axS.grid()\n", - "\n", - "axT = axS.twiny()\n", - "axT.xaxis.label.set_color('red')\n", - "axT.tick_params(axis='x', colors='red')\n", - "axT.plot(output['products']['T'], output['products']['z'], color='red')\n", - "rng = (270, 274)\n", - "axT.set_xlim(*rng)\n", - "axT.set_xticks(np.linspace(*rng, num=5))\n", - "axT.set_xlabel('Temperature [K]')\n", - "\n", - "axR = axs[1]\n", - "axR.set_xscale('log')\n", - "axR.set_xlim(1e-2, 1e1)\n", - "for drop_id, volume in enumerate(output['attributes']['volume']):\n", - " axR.plot(\n", - " settings.formulae.trivia.radius(volume=np.asarray(volume)) / si.um,\n", - " output['products']['z'],\n", - " color='magenta' if drop_id < settings.n_sd_per_mode[0] else 'blue',\n", - " label='sulfate' if drop_id == 0 else 'sea salt' if drop_id == settings.n_sd_per_mode[0] else ''\n", - " )\n", - "axR.legend(loc='upper right')\n", - "axR.set_xlabel('Droplet radius [μm]')\n", - "\n", - "show_plot(\"supersaturation.pdf\")" - ] + "execution_count": 7 }, { "cell_type": "code", "execution_count": 6, "metadata": { "ExecuteTime": { - "end_time": "2024-02-01T18:50:48.282682Z", + "end_time": "2025-06-15T08:09:50.808752Z", "start_time": "2024-02-01T18:50:47.502563Z" } }, diff --git a/examples/PySDM_examples/Pyrcel/profile_plotter.py b/examples/PySDM_examples/Pyrcel/profile_plotter.py index 5f945a31a8..b8c508aa4a 100644 --- a/examples/PySDM_examples/Pyrcel/profile_plotter.py +++ b/examples/PySDM_examples/Pyrcel/profile_plotter.py @@ -28,8 +28,12 @@ def plot(self, output): def plot_data(self, settings, output): _, axs = pyplot.subplots(1, 2, sharey=True, figsize=(10, 5)) axS = axs[0] + if output["products"].get("S_max"): + SS_percent = (np.asarray(output["products"]["S_max"]) - 1) * 100 + else: + SS_percent = np.asarray(output["products"]["S_max_percent"]) - 100 axS.plot( - np.asarray(output["products"]["S_max"]) - 100, + SS_percent, output["products"]["z"], color="black", ) @@ -37,7 +41,7 @@ def plot_data(self, settings, output): axS.set_xlabel("Supersaturation [%]") axS.set_xlim(0, 0.7) axS.set_ylim(0, 250) - axS.text(0.3, 52, f"max S = {np.nanmax(output['products']['S_max'])-100:.2f}%") + axS.text(0.3, 52, f"max SS = {np.nanmax(SS_percent):.2f}%") axS.grid() axT = axS.twiny() diff --git a/examples/PySDM_examples/Shipway_and_Hill_2012/simulation.py b/examples/PySDM_examples/Shipway_and_Hill_2012/simulation.py index 7b4008aa93..d20c7527a3 100644 --- a/examples/PySDM_examples/Shipway_and_Hill_2012/simulation.py +++ b/examples/PySDM_examples/Shipway_and_Hill_2012/simulation.py @@ -148,7 +148,7 @@ def zZ_to_z_above_reservoir(zZ): PySDM_products.RipeningRate(name="ripening"), PySDM_products.ActivatingRate(name="activating"), PySDM_products.DeactivatingRate(name="deactivating"), - PySDM_products.PeakSupersaturation(unit="%"), + PySDM_products.PeakSaturation(), PySDM_products.ParticleSizeSpectrumPerVolume( name="dry spectrum", radius_bins_edges=settings.r_bins_edges_dry, 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..baa2774fa3 --- /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 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/data/reference_bulk.py b/examples/PySDM_examples/Spichtinger_et_al_2023/data/reference_bulk.py new file mode 100644 index 0000000000..6aec8eb507 --- /dev/null +++ b/examples/PySDM_examples/Spichtinger_et_al_2023/data/reference_bulk.py @@ -0,0 +1,47 @@ +""" +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.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]) + 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 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..3b7f6cb597 --- /dev/null +++ b/examples/PySDM_examples/Spichtinger_et_al_2023/data/simulation_data.py @@ -0,0 +1,22 @@ +import numpy as np + + +def saved_simulation_ensemble_mean(): + + 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 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..3dad57dcce --- /dev/null +++ b/examples/PySDM_examples/Spichtinger_et_al_2023/fig_B1.ipynb @@ -0,0 +1,2004 @@ +{ + "cells": [ + { + "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", + "\n", + "(work in progress)\n", + "\n", + "https://doi.org/10.5194/acp-23-2035-2023" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "initial_id", + "metadata": { + "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')" + ] + }, + { + "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" + } + }, + "outputs": [], + "source": [ + "import json\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" + ] + }, + { + "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" + } + }, + "outputs": [], + "source": [ + "calculate_data = False\n", + "save_to_file = False\n", + "read_from_json = False" + ] + }, + { + "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" + } + }, + "outputs": [], + "source": [ + "if calculate_data:\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 = 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", + " 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\", encoding=\"utf-8\") as file:\n", + " json.dump(data_file, file)" + ] + }, + { + "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" + } + }, + "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=\" 0.0 and RHi < 130.0: + print("break") + break + RHi_old = RHi + + return output["ni"][-1] diff --git a/examples/PySDM_examples/utils/kinematic_2d/make_default_product_collection.py b/examples/PySDM_examples/utils/kinematic_2d/make_default_product_collection.py index 458f2b4d6b..9408219875 100644 --- a/examples/PySDM_examples/utils/kinematic_2d/make_default_product_collection.py +++ b/examples/PySDM_examples/utils/kinematic_2d/make_default_product_collection.py @@ -67,7 +67,7 @@ def make_default_product_collection(settings): if settings.processes["condensation"]: products.append(PySDM_products.CondensationTimestepMin(name="dt_cond_min")) products.append(PySDM_products.CondensationTimestepMax(name="dt_cond_max")) - products.append(PySDM_products.PeakSupersaturation(unit="%", name="S_max")) + products.append(PySDM_products.PeakSaturation(unit="%", name="S_max_percent")) products.append(PySDM_products.ActivatingRate()) products.append(PySDM_products.DeactivatingRate()) products.append(PySDM_products.RipeningRate()) 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. diff --git a/tests/examples_tests/Loftus_and_Wordsworth_2021/__init__.py b/tests/examples_tests/Loftus_and_Wordsworth_2021/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/examples_tests/Loftus_and_Wordsworth_2021/accuracy_test.py b/tests/examples_tests/Loftus_and_Wordsworth_2021/accuracy_test.py new file mode 100644 index 0000000000..f23f2fc4f5 --- /dev/null +++ b/tests/examples_tests/Loftus_and_Wordsworth_2021/accuracy_test.py @@ -0,0 +1,230 @@ +# pylint: disable=missing-module-docstring +from __future__ import annotations + +import os +import warnings +from typing import Tuple, List, Generator + +import pytest +import numpy as np +from scipy.optimize import fsolve + +from PySDM_examples.Loftus_and_Wordsworth_2021 import Settings, Simulation +from PySDM_examples.Loftus_and_Wordsworth_2021.planet import EarthLike + +from PySDM import Formulae +from PySDM.physics import si + + +class GroundTruthLoader: + def __init__( + self, groundtruth_dir_path: str, n_samples: int = 2, random_seed: int = 2137 + ): + self.dir_path = groundtruth_dir_path + self.RHs = None + self.r0grid = None + self.m_frac_evap = None + self.n_samples = n_samples + np.random.seed(random_seed) + + def __enter__(self): + try: + self.RHs = np.load(os.path.join(self.dir_path, "RHs.npy")) + self.r0grid = np.load(os.path.join(self.dir_path, "r0grid.npy")) + self.m_frac_evap = np.load(os.path.join(self.dir_path, "m_frac_evap.npy")) + return self + except FileNotFoundError as e: + pytest.fail(f"Error loading ground truth files: {e}") + pytest.fail("Ground truth data not loaded successfully.") + + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + pass + + +# pylint: disable=redefined-outer-name +@pytest.fixture(scope="module") +def ground_truth_data(request) -> Generator[GroundTruthLoader, None, None]: + current_dir = os.path.dirname(os.path.abspath(request.fspath)) + groundtruth_dir = os.path.abspath(os.path.join(current_dir, "ground_truth")) + if not os.path.isdir(groundtruth_dir): + pytest.fail(f"Groundtruth directory not found at {groundtruth_dir}") + with GroundTruthLoader(groundtruth_dir) as gt: + yield gt + + +# pylint: disable=redefined-outer-name +@pytest.fixture +def ground_truth_sample(ground_truth_data: GroundTruthLoader) -> List[dict]: + gt = ground_truth_data + n_rh_values = len(gt.RHs) + n_radius_values = gt.r0grid.shape[1] + total_points = n_rh_values * n_radius_values + n_samples = min(gt.n_samples, total_points) + if n_samples == 0: + pytest.skip("No data points available to sample.") + all_indices = np.array( + [(i, j) for i in range(n_rh_values) for j in range(n_radius_values)] + ) + sampled_indices_flat = np.random.choice(len(all_indices), n_samples, replace=False) + sampled_ij_pairs = all_indices[sampled_indices_flat] + return [ + { + "rh": gt.RHs[i_rh], + "r_m": gt.r0grid[0, j_r], + "expected_m_frac_evap": gt.m_frac_evap[i_rh, j_r], + "i_rh": i_rh, + "j_r": j_r, + } + for i_rh, j_r in sampled_ij_pairs + ] + + +# pylint: disable=redefined-outer-name +@pytest.fixture(scope="module") +def static_arrays() -> Tuple[np.ndarray, np.ndarray, np.ndarray, object]: + formulae = Formulae( + ventilation="PruppacherAndRasmussen1979", + saturation_vapour_pressure="AugustRocheMagnus", + diffusion_coordinate="WaterMassLogarithm", + ) + radius_array = np.logspace(-4.5, -2.5, 50) * si.m + RH_array = np.linspace(0.25, 0.99, 50) + output_matrix = np.full((len(RH_array), len(radius_array)), np.nan) + const = formulae.constants + return radius_array, RH_array, output_matrix, const + + +class TestNPYComparison: + @staticmethod + def _mix(dry_prop, vap_prop, ratio): + return (dry_prop + ratio * vap_prop) / (1 + ratio) + + def _calculate_cloud_properties( + self, planet: EarthLike, surface_RH: float, formulae_instance: Formulae + ): + const = formulae_instance.constants + planet.RH_zref = surface_RH + pvs_stp = formulae_instance.saturation_vapour_pressure.pvs_water(planet.T_STP) + initial_water_vapour_mixing_ratio = const.eps / ( + planet.p_STP / planet.RH_zref / pvs_stp - 1 + ) + R_air_mix = self._mix(const.Rd, const.Rv, initial_water_vapour_mixing_ratio) + cp_mix = self._mix(const.c_pd, const.c_pv, initial_water_vapour_mixing_ratio) + + def solve_Tcloud(T_candidate): + pv_ad = ( + initial_water_vapour_mixing_ratio + / (initial_water_vapour_mixing_ratio + const.eps) + * planet.p_STP + * (T_candidate / planet.T_STP) ** (cp_mix / R_air_mix) + ) + pvs_tc = formulae_instance.saturation_vapour_pressure.pvs_water(T_candidate) + return pv_ad - pvs_tc + + Tcloud = np.max(fsolve(solve_Tcloud, [150.0, 300.0])) + + Zcloud = (planet.T_STP - Tcloud) * cp_mix / planet.g_std + + th_std = formulae_instance.trivia.th_std(planet.p_STP, planet.T_STP) + + hydro = formulae_instance.hydrostatics + pcloud = hydro.p_of_z_assuming_const_th_and_initial_water_vapour_mixing_ratio( + planet.p_STP, th_std, initial_water_vapour_mixing_ratio, Zcloud + ) + return initial_water_vapour_mixing_ratio, Tcloud, Zcloud, pcloud + + def test_figure_2_replication_accuracy(self, ground_truth_sample): + formulae = Formulae( + ventilation="PruppacherAndRasmussen1979", + saturation_vapour_pressure="AugustRocheMagnus", + diffusion_coordinate="WaterMassLogarithm", + ) + for sample in ground_truth_sample: + planet = EarthLike() + try: + iwvmr, Tcloud, Zcloud, pcloud = self._calculate_cloud_properties( + planet, sample["rh"], formulae + ) + settings = Settings( + planet=planet, + r_wet=sample["r_m"], + mass_of_dry_air=1e5 * si.kg, + initial_water_vapour_mixing_ratio=iwvmr, + pcloud=pcloud, + Zcloud=Zcloud, + Tcloud=Tcloud, + formulae=formulae, + ) + simulated = TestNPYComparison.calc_simulated_m_frac_evap_point( + sample["i_rh"], + sample["j_r"], + sample["rh"], + sample["expected_m_frac_evap"], + settings, + ) + expected = sample["expected_m_frac_evap"] + error_context = ( + f"Sample (RH_idx={sample['i_rh']}, R_idx={sample['j_r']}), " + + f"RH={sample['rh']:.4f}, R_m={sample['r_m']:.3e}. " + f"Expected: {expected}, Got: {simulated}" + ) + if np.isnan(expected): + assert np.isnan( + simulated + ), f"NaN Mismatch. {error_context} (Expected NaN, got non-NaN)" + else: + assert not np.isnan( + simulated + ), f"NaN Mismatch. {error_context} (Expected non-NaN, got NaN)" + np.testing.assert_allclose( + simulated, + expected, + rtol=1e-1, + atol=1e-1, + err_msg=f"Value Mismatch. {error_context}", + ) + except ValueError as e: + pytest.fail( + f"Error in _calculate_cloud_properties for RH={sample['rh']} " + + f"(sample idx {sample['i_rh']},{sample['j_r']}): {e}." + ) + + @staticmethod + def calc_simulated_m_frac_evap_point(i_rh, j_r, rh, expected, settings): + if np.isnan(settings.r_wet) or settings.r_wet <= 0: + pytest.fail( + f"Invalid radius r_m={settings.r_wet} for sample idx {i_rh},{j_r}." + ) + simulation = Simulation(settings) + try: + output = simulation.run() + if ( + output + and "r" in output + and len(output["r"]) > 0 + and "z" in output + and len(output["z"]) > 0 + ): + final_radius_um = output["r"][-1] + if np.isnan(final_radius_um) or final_radius_um < 0: + final_radius_m = final_radius_um * 1e-6 + if final_radius_m < 0: + return 1.0 + return np.nan + final_radius_m = final_radius_um * 1e-6 + if settings.r_wet == 0: + frac_evap = 1.0 + else: + frac_evap = 1.0 - (final_radius_m / settings.r_wet) ** 3 + return np.clip(frac_evap, 0.0, 1.0) + return np.nan + except Exception as e: # pylint: disable=broad-except + warnings.warn( + f"Simulation run failed for RH={rh:.4f}, r={settings.r_wet:.3e} " + + f"(sample idx {i_rh},{j_r}): {type(e).__name__}: {e}" + ) + if np.isclose(expected, 1.0, atol=1e-6): + return 1.0 + return np.nan diff --git a/tests/examples_tests/Loftus_and_Wordsworth_2021/ground_truth/RHgrid.npy b/tests/examples_tests/Loftus_and_Wordsworth_2021/ground_truth/RHgrid.npy new file mode 100644 index 0000000000..e02761b427 Binary files /dev/null and b/tests/examples_tests/Loftus_and_Wordsworth_2021/ground_truth/RHgrid.npy differ diff --git a/tests/examples_tests/Loftus_and_Wordsworth_2021/ground_truth/RHs.npy b/tests/examples_tests/Loftus_and_Wordsworth_2021/ground_truth/RHs.npy new file mode 100644 index 0000000000..73132c6203 Binary files /dev/null and b/tests/examples_tests/Loftus_and_Wordsworth_2021/ground_truth/RHs.npy differ diff --git a/tests/examples_tests/Loftus_and_Wordsworth_2021/ground_truth/gen_figure.py b/tests/examples_tests/Loftus_and_Wordsworth_2021/ground_truth/gen_figure.py new file mode 100644 index 0000000000..89fec0af21 --- /dev/null +++ b/tests/examples_tests/Loftus_and_Wordsworth_2021/ground_truth/gen_figure.py @@ -0,0 +1,108 @@ +# pylint: disable=missing-module-docstring + +################################################################ +# make LoWo21 Figure 2 +# r_min, fraction raindrop mass evaporated as functions of RH +################################################################ +import os +import numpy as np +import matplotlib.pyplot as plt + +# load results +root_dir = os.path.dirname(os.path.abspath(__file__)) +RHs = np.load(os.path.join(root_dir, "RHs.npy")) +r0grid = np.load(os.path.join(root_dir, "r0grid.npy")) +RHgrid = np.load(os.path.join(root_dir, "RHgrid.npy")) +m_frac_evap = np.load(os.path.join(root_dir, "m_frac_evap.npy")) +r_mins = np.load(os.path.join(root_dir, "r_mins.npy")) + +i_RH75 = 29 # index for RH=0.75 + +# make figure 2 +# only put colorbar on lower panel, still line up x-axes +f, axs = plt.subplots( + 2, + 2, + sharex="col", + figsize=(6, 7), + gridspec_kw={"height_ratios": [1, 3], "width_ratios": [20, 1]}, +) +plt.subplots_adjust(hspace=0.05) +axs[0, 0].set_xscale("log") +axs[1, 0].set_xlabel(r"$r_0$ [mm]") +axs[1, 0].set_ylabel("surface RH [ ]") +axs[0, 0].tick_params(right=True, which="both") +axs[1, 0].tick_params(right=True, which="both") +axs[1, 0].tick_params(top=True, which="both") +axs[0, 0].tick_params(top=True, which="both") +axs[0, 0].set_ylabel("fraction mass \n evaporated [ ]") +axs[0, 0].set_xlim(r0grid[0, 0] * 1e3, r0grid[0, -1] * 1e3) +axs[0, 0].set_ylim(-0.04, 1.04) +levels_smooth = np.linspace(0, 1, 250) +cmesh = axs[1, 0].contourf( + r0grid * 1e3, + RHgrid, + m_frac_evap, + cmap=plt.cm.binary, # pylint: disable=no-member + vmin=0, + vmax=1, + levels=levels_smooth, +) + + +cb = f.colorbar(cmesh, cax=axs[1, 1]) +cb.solids.set_edgecolor("face") +axs[0, 1].axis("off") +cb.solids.set_edgecolor("face") +cb.solids.set_linewidth(1e-5) +cb.set_label("fraction mass evaporated [ ]") +cb.set_ticks([0, 0.1, 0.25, 0.5, 0.75, 1]) +axs[1, 0].axhline(0.75, lw=0.5, ls="--", c="plum") +axs[1, 0].plot(r_mins * 1e3, RHs, lw=3, c="darkviolet", zorder=10) +c_10 = axs[1, 0].contour( + r0grid * 1e3, + RHgrid, + m_frac_evap, + colors="indigo", + linewidths=1, + linestyles="--", + levels=[0.1], +) +cb.add_lines(c_10) +axs[1, 0].clabel( + c_10, c_10.levels, fmt={0.1: "10% mass evaporated"}, fontsize="smaller" +) +axs[1, 0].fill_betweenx( + RHs, 1e-2, (r_mins - 1e-6) * 1e3, edgecolor="k", facecolor="w", hatch="//" +) +axs[1, 0].annotate( + text="TOTAL \\n EVAPORATION", xy=(0.04, 0.55), c="k", backgroundcolor="w" +) +axs[1, 0].annotate( + text=r"$r_\mathrm{min}$", + xy=(0.35, 0.27), + c="darkviolet", + backgroundcolor="w", + size=8, +) + +axs[0, 0].scatter(r_mins[i_RH75] * 1e3, 0.99, color="darkviolet", zorder=10) +axs[0, 0].axvline(r_mins[i_RH75] * 1e3, lw=0.5, c="darkviolet", ls="--") +axs[0, 0].plot(r0grid[0, :] * 1e3, m_frac_evap[i_RH75, :], lw=2.05, c="k") +axs[0, 0].axhline(1, c="w", lw=3) +axs[0, 0].plot([1e-2, r_mins[i_RH75] * 1e3], [1, 1], c="k", lw=2.05, ls="--") +axs[0, 0].annotate(text="surface RH = 0.75", xy=(0.8, 0.85), c="plum", size=8) +axs[0, 0].annotate(text=r"$r_\mathrm{min}$", xy=(0.13, 0.05), c="darkviolet", size=8) + +figs_path = os.path.join(dir, "figs") + +if not os.path.exists(figs_path): + os.mkdir(figs_path) + +plt.savefig( + os.path.join(figs_path, "fig02.pdf"), + transparent=True, + bbox_inches="tight", + pad_inches=0.5, +) +plt.close() diff --git a/tests/examples_tests/Loftus_and_Wordsworth_2021/ground_truth/m_frac_evap.npy b/tests/examples_tests/Loftus_and_Wordsworth_2021/ground_truth/m_frac_evap.npy new file mode 100644 index 0000000000..657765da4b Binary files /dev/null and b/tests/examples_tests/Loftus_and_Wordsworth_2021/ground_truth/m_frac_evap.npy differ diff --git a/tests/examples_tests/Loftus_and_Wordsworth_2021/ground_truth/r0grid.npy b/tests/examples_tests/Loftus_and_Wordsworth_2021/ground_truth/r0grid.npy new file mode 100644 index 0000000000..5f4e75e31d Binary files /dev/null and b/tests/examples_tests/Loftus_and_Wordsworth_2021/ground_truth/r0grid.npy differ diff --git a/tests/examples_tests/Loftus_and_Wordsworth_2021/ground_truth/r_mins.npy b/tests/examples_tests/Loftus_and_Wordsworth_2021/ground_truth/r_mins.npy new file mode 100644 index 0000000000..dba178b0df Binary files /dev/null and b/tests/examples_tests/Loftus_and_Wordsworth_2021/ground_truth/r_mins.npy differ diff --git a/tests/examples_tests/Loftus_and_Wordsworth_2021/unit_test.py b/tests/examples_tests/Loftus_and_Wordsworth_2021/unit_test.py new file mode 100644 index 0000000000..a15d1374b5 --- /dev/null +++ b/tests/examples_tests/Loftus_and_Wordsworth_2021/unit_test.py @@ -0,0 +1,269 @@ +# pylint: disable=missing-module-docstring + +from collections import namedtuple +from functools import partial +from contextlib import contextmanager +import pytest +import numpy as np +from scipy.optimize import fsolve + +from PySDM_examples.Loftus_and_Wordsworth_2021.planet import ( + EarthLike, + Earth, + EarlyMars, + Jupiter, + Saturn, + K2_18B, +) +from PySDM_examples.Loftus_and_Wordsworth_2021.simulation import Simulation +from PySDM_examples.Loftus_and_Wordsworth_2021.parcel import AlienParcel +from PySDM_examples.Loftus_and_Wordsworth_2021 import Settings + +from PySDM import Formulae +from PySDM.physics import si + + +class TestLoftusWordsworth2021: + + @contextmanager + @staticmethod + def _get_test_resources(): + formulae = Formulae( + ventilation="PruppacherAndRasmussen1979", + saturation_vapour_pressure="AugustRocheMagnus", + diffusion_coordinate="WaterMassLogarithm", + ) + earth_like = EarthLike() + try: + yield formulae, earth_like + finally: + pass + + def test_planet_classes(self): + """Test planet class instantiation and basic properties.""" + planets = [EarthLike(), Earth(), EarlyMars(), Jupiter(), Saturn(), K2_18B()] + + for planet in planets: + assert planet.g_std > 0 + assert planet.T_STP > 0 + assert planet.p_STP > 0 + assert planet.RH_zref >= 0 + assert planet.RH_zref <= 1 + + # atmospheric composition sums to 1 or less + total_conc = ( + planet.dry_molar_conc_H2 + + planet.dry_molar_conc_He + + planet.dry_molar_conc_N2 + + planet.dry_molar_conc_O2 + + planet.dry_molar_conc_CO2 + ) + assert total_conc <= 1.01, ( + f"Total molar concentration {total_conc} " + + f"exceeds 1.01 for {planet.__class__.__name__}" + ) + + def test_water_vapour_mixing_ratio_calculation(self): + """Test water vapour mixing ratio calculation.""" + with TestLoftusWordsworth2021._get_test_resources() as (formulae, earth_like): + const = formulae.constants + planet = earth_like + + pvs = formulae.saturation_vapour_pressure.pvs_water(planet.T_STP) + initial_water_vapour_mixing_ratio = const.eps / ( + planet.p_STP / planet.RH_zref / pvs - 1 + ) + + assert initial_water_vapour_mixing_ratio > 0 + assert initial_water_vapour_mixing_ratio < 0.1 # Should be less than 10% + + def test_alien_parcel_initialization(self): + """Test AlienParcel class initialization.""" + parcel = AlienParcel( + dt=1.0 * si.second, + mass_of_dry_air=1e5 * si.kg, + pcloud=90000 * si.pascal, + initial_water_vapour_mixing_ratio=0.01, + Tcloud=280 * si.kelvin, + w=0, + zcloud=1000 * si.m, + ) + assert parcel is not None + + # pylint: disable=too-many-arguments + @pytest.mark.parametrize( + "r_wet_val, mass_of_dry_air_val, iwvmr_val, pcloud_val, Zcloud_val, Tcloud_val", + [ + (1e-4, 1e5, 0.01, 90000, 1000, 280), + (1e-5, 1e4, 0.005, 80000, 500, 270), + (2e-4, 2e5, 0.02, 95000, 1500, 290), + ], + ) + def test_simulation_class( + self, + r_wet_val, + mass_of_dry_air_val, + iwvmr_val, + pcloud_val, + Zcloud_val, + Tcloud_val, + ): + """ + Test Simulation class initialization and basic functionality with parametrized settings. + """ + with TestLoftusWordsworth2021._get_test_resources() as (formulae, earth_like): + planet = earth_like + + settings = Settings( + planet=planet, + r_wet=r_wet_val * si.m, + mass_of_dry_air=mass_of_dry_air_val * si.kg, + initial_water_vapour_mixing_ratio=iwvmr_val, + pcloud=pcloud_val * si.pascal, + Zcloud=Zcloud_val * si.m, + Tcloud=Tcloud_val * si.kelvin, + formulae=formulae, + ) + + simulation = Simulation(settings) + + assert hasattr(simulation, "particulator") + assert hasattr(simulation, "run") + assert hasattr(simulation, "save") + + products = simulation.particulator.products + required_products = ["radius_m1", "z", "RH", "t"] + for product in required_products: + assert product in products + + # pylint: disable=too-many-arguments + @pytest.mark.parametrize( + "r_wet_val, mass_of_dry_air_val, iwvmr_val, pcloud_val, Zcloud_val, Tcloud_val", + [ + (1e-5, 1e5, 0.01, 90000, 100, 280), + (1e-5, 1e4, 0.005, 80000, 500, 270), + (2e-4, 2e5, 0.02, 95000, 1500, 290), + ], + ) + def test_simulation_run_basic( + self, + r_wet_val, + mass_of_dry_air_val, + iwvmr_val, + pcloud_val, + Zcloud_val, + Tcloud_val, + ): + """Test basic simulation run functionality.""" + with TestLoftusWordsworth2021._get_test_resources() as (formulae, earth_like): + planet = earth_like + + settings = Settings( + planet=planet, + r_wet=r_wet_val * si.m, + mass_of_dry_air=mass_of_dry_air_val * si.kg, + initial_water_vapour_mixing_ratio=iwvmr_val, + pcloud=pcloud_val * si.pascal, + Zcloud=Zcloud_val * si.m, + Tcloud=Tcloud_val * si.kelvin, + formulae=formulae, + ) + + simulation = Simulation(settings) + output = simulation.run() + + assert "r" in output + assert "S" in output + assert "z" in output + assert "t" in output + + assert output["r"] is not None + assert output["S"] is not None + assert output["z"] is not None + assert output["t"] is not None + + assert len(output["r"]) > 0, "Output array 'r' is empty" + assert len(output["S"]) > 0, "Output array 'S' is empty" + assert len(output["z"]) > 0, "Output array 'z' is empty" + assert len(output["t"]) > 0, "Output array 't' is empty" + + lengths = [len(one_output) for one_output in output.values()] + assert all( + length == lengths[0] for length in lengths + ), "Not all output arrays have the same length" + + def test_saturation_at_cloud_base(self): + formulae = Formulae( + ventilation="PruppacherAndRasmussen1979", + saturation_vapour_pressure="AugustRocheMagnus", + diffusion_coordinate="WaterMassLogarithm", + ) + + new_Earth = EarthLike() + + RH_array = np.linspace(0.25, 0.99, 50) + const = formulae.constants + + def mix(dry, vap, ratio): + return (dry + ratio * vap) / (1 + ratio) + + def f(x, water_mixing_ratio, params): + return water_mixing_ratio / ( + water_mixing_ratio + const.eps + ) * params.p_stp * (x / params.t_stp) ** ( + params.c_p / params.Rair + ) - formulae.saturation_vapour_pressure.pvs_water( + x + ) + + for RH in RH_array[::-1]: + new_Earth.RH_zref = RH + + initial_water_vapour_mixing_ratio = const.eps / ( + new_Earth.p_STP + / new_Earth.RH_zref + / formulae.saturation_vapour_pressure.pvs_water(new_Earth.T_STP) + - 1 + ) + + c_p = mix(const.c_pd, const.c_pv, initial_water_vapour_mixing_ratio) + + tdews = fsolve( + partial( + f, + water_mixing_ratio=initial_water_vapour_mixing_ratio, + params=namedtuple( + "params", + ["p_stp", "t_stp", "c_p", "Rair"], + )( + p_stp=new_Earth.p_STP, + t_stp=new_Earth.T_STP, + c_p=c_p, + Rair=mix(const.Rd, const.Rv, initial_water_vapour_mixing_ratio), + ), + ), + [150, 300], + ) + Tcloud = np.max(tdews) + thstd = formulae.trivia.th_std(new_Earth.p_STP, new_Earth.T_STP) + + hydro = formulae.hydrostatics + pcloud = ( + hydro.p_of_z_assuming_const_th_and_initial_water_vapour_mixing_ratio( + new_Earth.p_STP, + thstd, + initial_water_vapour_mixing_ratio, + (new_Earth.T_STP - Tcloud) * c_p / new_Earth.g_std, + ) + ) + + np.testing.assert_approx_equal( + actual=pcloud + * ( + initial_water_vapour_mixing_ratio + / (initial_water_vapour_mixing_ratio + const.eps) + ) + / formulae.saturation_vapour_pressure.pvs_water(Tcloud), + desired=1, + significant=4, + ) diff --git a/tests/examples_tests/conftest.py b/tests/examples_tests/conftest.py index 74cafe3e12..0b18e0c3ea 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", @@ -46,6 +47,7 @@ def findfiles(path, regex): "condensation_a": [ "Lowe_et_al_2019", "Singer_Ward", + "Rogers_1975", ], "condensation_b": [ "Abdul_Razzak_Ghan_2000", @@ -57,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"], @@ -83,6 +84,7 @@ def findfiles(path, regex): "utils", "Zaba_et_al_2025", "Gonfiantini_1986", + "Loftus_and_Wordsworth_2021", ], } diff --git a/tests/smoke_tests/kinematic_1d/shipway_and_hill_2012/test_few_steps.py b/tests/smoke_tests/kinematic_1d/shipway_and_hill_2012/test_few_steps.py index ee48b7de9f..c5434f9b76 100644 --- a/tests/smoke_tests/kinematic_1d/shipway_and_hill_2012/test_few_steps.py +++ b/tests/smoke_tests/kinematic_1d/shipway_and_hill_2012/test_few_steps.py @@ -43,7 +43,7 @@ def mean_profile_over_last_steps(var, smooth=True): for var in ( "RH", - "peak supersaturation", + "peak saturation", "T", "water_vapour_mixing_ratio", "p", @@ -74,7 +74,7 @@ def mean_profile_over_last_steps(var, smooth=True): assert 0.5 * n_sd_per_gridbox < min(sd_prof) < 1.5 * n_sd_per_gridbox assert 0.5 * n_sd_per_gridbox < max(sd_prof) < 1.5 * n_sd_per_gridbox - assert 0.01 < max(mean_profile_over_last_steps("peak supersaturation")) < 0.1 + assert 1.0001 < max(mean_profile_over_last_steps("peak saturation")) < 1.001 assert min(mean_profile_over_last_steps("cloud water mixing ratio")) < 1e-10 assert 0.1 < max(mean_profile_over_last_steps("cloud water mixing ratio")) < 0.15 assert max(mean_profile_over_last_steps("activating")) == 0 @@ -108,7 +108,7 @@ def mean_profile_over_last_steps(var): assert 0.5 * n_sd_per_gridbox < min(sd_prof) < 1.5 * n_sd_per_gridbox assert 0.5 * n_sd_per_gridbox < max(sd_prof) < 1.5 * n_sd_per_gridbox - assert 0.01 < max(mean_profile_over_last_steps("peak supersaturation")) < 0.1 + assert 1.0001 < max(mean_profile_over_last_steps("peak saturation")) < 1.001 assert min(mean_profile_over_last_steps("cloud water mixing ratio")) < 1e-10 assert 0.5 < max(mean_profile_over_last_steps("cloud water mixing ratio")) < 0.75 assert max(mean_profile_over_last_steps("activating")) == 0 diff --git a/tests/smoke_tests/parcel_a/lowe_et_al_2019/test_dz_sensitivity.py b/tests/smoke_tests/parcel_a/lowe_et_al_2019/test_dz_sensitivity.py index d65f1c7f12..53683f6698 100644 --- a/tests/smoke_tests/parcel_a/lowe_et_al_2019/test_dz_sensitivity.py +++ b/tests/smoke_tests/parcel_a/lowe_et_al_2019/test_dz_sensitivity.py @@ -1,4 +1,4 @@ -"""checks how parcel equilibrium supersaturation depends on dz""" +"""checks how parcel equilibrium saturation depends on dz""" import numpy as np from matplotlib import pyplot @@ -44,7 +44,10 @@ def test_dz_sensitivity( for idx, var in enumerate(vlist): for key, out_item in output.items(): Y = np.asarray(out_item["z"]) - X = out_item[var] + if var == "S_max": + X = (np.asarray(out_item[var]) - 1) * 100 + else: + X = out_item[var] axs[idx].plot( X, Y, label=f"dz={key} m", color=out_item["color"], linestyle="-" ) diff --git a/tests/smoke_tests/parcel_a/lowe_et_al_2019/test_fig_2.py b/tests/smoke_tests/parcel_a/lowe_et_al_2019/test_fig_2.py index 26d1f9c706..e15a1a136a 100644 --- a/tests/smoke_tests/parcel_a/lowe_et_al_2019/test_fig_2.py +++ b/tests/smoke_tests/parcel_a/lowe_et_al_2019/test_fig_2.py @@ -65,7 +65,7 @@ class TestFig2: # pylint: disable=too-few-public-methods # TODO #1247 AerosolMarine passes, but others fail # TODO #1246 general mismatches in parcel profiles @pytest.mark.xfail() - def test_peak_supersaturation_and_final_concentration( + def test_peak_saturation_and_final_concentration( *, aerosol, surface_tension, s_max, s_100m, n_100m ): # arrange diff --git a/tests/smoke_tests/parcel_a/pyrcel/test_parcel_example.py b/tests/smoke_tests/parcel_a/pyrcel/test_parcel_example.py index c81bad5a55..0f2f9398c9 100644 --- a/tests/smoke_tests/parcel_a/pyrcel/test_parcel_example.py +++ b/tests/smoke_tests/parcel_a/pyrcel/test_parcel_example.py @@ -20,9 +20,7 @@ class TestParcelExample: # pylint: disable=too-few-public-methods @pytest.mark.xfail( strict=True ) # TODO #1246 s_250m (only) fails for both solver options - def test_supersaturation_and_temperature_profile( - s_max, s_250m, T_250m, scipy_solver - ): + def test_humidity_and_temperature_profile(s_max, s_250m, T_250m, scipy_solver): # arrange settings = Settings( dz=1 * si.m, @@ -46,7 +44,7 @@ def test_supersaturation_and_temperature_profile( settings, products=( ParcelDisplacement(name="z"), - AmbientRelativeHumidity(name="RH", unit="%"), + AmbientRelativeHumidity(name="RH_percent", unit="%"), AmbientTemperature(name="T"), ), scipy_solver=scipy_solver, @@ -56,15 +54,17 @@ def test_supersaturation_and_temperature_profile( output = simulation.run() # assert - print(np.nanmax(np.asarray(output["products"]["RH"])) - 100, s_max) + print(np.nanmax((np.asarray(output["products"]["RH"])) - 1) * 100, s_max) print(output["products"]["T"][-1], T_250m) - print(output["products"]["RH"][-1] - 100, s_250m) + print(output["products"]["RH_percent"][-1] - 100, s_250m) np.testing.assert_approx_equal( - np.nanmax(np.asarray(output["products"]["RH"])) - 100, s_max, significant=2 + np.nanmax(np.asarray(output["products"]["RH_percent"])) - 100, + s_max, + significant=2, ) np.testing.assert_approx_equal( output["products"]["T"][-1], T_250m, significant=2 ) np.testing.assert_approx_equal( - output["products"]["RH"][-1] - 100, s_250m, significant=2 + output["products"]["RH_percent"][-1] - 100, s_250m, significant=2 ) diff --git a/tests/smoke_tests/parcel_b/arabas_and_shima_2017/test_conservation.py b/tests/smoke_tests/parcel_b/arabas_and_shima_2017/test_conservation.py index 27c588cd66..f8094af257 100644 --- a/tests/smoke_tests/parcel_b/arabas_and_shima_2017/test_conservation.py +++ b/tests/smoke_tests/parcel_b/arabas_and_shima_2017/test_conservation.py @@ -16,65 +16,67 @@ def liquid_water_mixing_ratio(simulation: Simulation): return mass_of_all_droplets / env.mass_of_dry_air -@pytest.mark.parametrize("settings_idx", range(len(w_avgs))) -@pytest.mark.parametrize("mass_of_dry_air", (1, 10000)) -@pytest.mark.parametrize("scheme", ("SciPy", "CPU", "GPU")) -@pytest.mark.parametrize("coord", ("WaterMassLogarithm", "WaterMass")) -def test_water_mass_conservation(settings_idx, mass_of_dry_air, scheme, coord): - # Arrange - assert scheme in ("SciPy", "CPU", "GPU") +class TestConservation: + @staticmethod + @pytest.mark.parametrize("settings_idx", range(len(w_avgs))) + @pytest.mark.parametrize("mass_of_dry_air", (1, 10000)) + @pytest.mark.parametrize("scheme", ("SciPy", "CPU", "GPU")) + @pytest.mark.parametrize("coord", ("WaterMassLogarithm", "WaterMass")) + def test_water_mass_conservation(settings_idx, mass_of_dry_air, scheme, coord): + # Arrange + assert scheme in ("SciPy", "CPU", "GPU") - settings = Settings( - w_avg=setups[settings_idx].w_avg, - N_STP=setups[settings_idx].N_STP, - r_dry=setups[settings_idx].r_dry, - mass_of_dry_air=mass_of_dry_air, - coord=coord, - ) - settings.n_output = 50 - settings.coord = coord - simulation = Simulation(settings, GPU if scheme == "GPU" else CPU) - initial_total_water_mixing_ratio = ( - settings.initial_water_vapour_mixing_ratio - + liquid_water_mixing_ratio(simulation) - ) + settings = Settings( + w_avg=setups[settings_idx].w_avg, + N_STP=setups[settings_idx].N_STP, + r_dry=setups[settings_idx].r_dry, + mass_of_dry_air=mass_of_dry_air, + coord=coord, + ) + settings.n_output = 50 + settings.coord = coord + simulation = Simulation(settings, GPU if scheme == "GPU" else CPU) + initial_total_water_mixing_ratio = ( + settings.initial_water_vapour_mixing_ratio + + liquid_water_mixing_ratio(simulation) + ) - if scheme == "SciPy": - scipy_ode_condensation_solver.patch_particulator(simulation.particulator) + if scheme == "SciPy": + scipy_ode_condensation_solver.patch_particulator(simulation.particulator) - # Act - simulation.particulator.products["S_max"].get() - output = simulation.run() + # Act + simulation.particulator.products["S_max"].get() + output = simulation.run() - # Assert - (total_water_mixing_ratio,) = simulation.particulator.environment[ - "water_vapour_mixing_ratio" - ].to_ndarray() + liquid_water_mixing_ratio(simulation) - np.testing.assert_approx_equal( - total_water_mixing_ratio, initial_total_water_mixing_ratio, significant=6 - ) - if scheme != "SciPy": - assert simulation.particulator.products["S_max"].get() >= output["S"][-1] + # Assert + (total_water_mixing_ratio,) = simulation.particulator.environment[ + "water_vapour_mixing_ratio" + ].to_ndarray() + liquid_water_mixing_ratio(simulation) + np.testing.assert_approx_equal( + total_water_mixing_ratio, initial_total_water_mixing_ratio, significant=6 + ) + if scheme != "SciPy": # TODO #1608 + assert simulation.particulator.products["S_max"].get() >= output["RH"][-1] + @staticmethod + @pytest.mark.parametrize("settings_idx", range(len(w_avgs))) + @pytest.mark.parametrize("mass_of_dry_air", [1, 10000]) + @pytest.mark.parametrize("coord", ("WaterMassLogarithm", "WaterMass")) + def test_energy_conservation(settings_idx, mass_of_dry_air, coord): + # Arrange + settings = Settings( + w_avg=setups[settings_idx].w_avg, + N_STP=setups[settings_idx].N_STP, + r_dry=setups[settings_idx].r_dry, + mass_of_dry_air=mass_of_dry_air, + coord=coord, + ) + simulation = Simulation(settings) + env = simulation.particulator.environment + thd0 = env["thd"] -@pytest.mark.parametrize("settings_idx", range(len(w_avgs))) -@pytest.mark.parametrize("mass_of_dry_air", [1, 10000]) -@pytest.mark.parametrize("coord", ("WaterMassLogarithm", "WaterMass")) -def test_energy_conservation(settings_idx, mass_of_dry_air, coord): - # Arrange - settings = Settings( - w_avg=setups[settings_idx].w_avg, - N_STP=setups[settings_idx].N_STP, - r_dry=setups[settings_idx].r_dry, - mass_of_dry_air=mass_of_dry_air, - coord=coord, - ) - simulation = Simulation(settings) - env = simulation.particulator.environment - thd0 = env["thd"] - - # Act - simulation.run() + # Act + simulation.run() - # Assert - np.testing.assert_array_almost_equal(thd0.to_ndarray(), env["thd"].to_ndarray()) + # Assert + np.testing.assert_array_almost_equal(thd0.to_ndarray(), env["thd"].to_ndarray()) diff --git a/tests/smoke_tests/parcel_b/arabas_and_shima_2017/test_vs_scipy.py b/tests/smoke_tests/parcel_b/arabas_and_shima_2017/test_vs_scipy.py index 7fcc071b5d..88f062c981 100644 --- a/tests/smoke_tests/parcel_b/arabas_and_shima_2017/test_vs_scipy.py +++ b/tests/smoke_tests/parcel_b/arabas_and_shima_2017/test_vs_scipy.py @@ -26,13 +26,13 @@ def split(arg1, arg2): @pytest.mark.parametrize("scheme", ("CPU",)) # 'GPU')) # TODO #588 def test_vs_scipy(settings_idx, data, rtol, leg, scheme): # Arrange - supersaturation = {} + saturation = {} for sch in schemes: sut = data[sch][rtol][settings_idx] - ascent, descent = split(sut["S"], sut["z"]) - supersaturation[sch] = ascent if leg == "ascent" else descent + ascent, descent = split(sut["RH"], sut["z"]) + saturation[sch] = ascent if leg == "ascent" else descent # Assert - desired = np.array(supersaturation["SciPy"]) - actual = np.array(supersaturation[scheme]) + desired = np.array(saturation["SciPy"]) + actual = np.array(saturation[scheme]) assert np.mean((desired - actual) ** 2) < rtol diff --git a/tests/smoke_tests/parcel_c/abdul_razzak_ghan_2000/test_single_supersaturation_peak.py b/tests/smoke_tests/parcel_c/abdul_razzak_ghan_2000/test_single_supersaturation_peak.py index 0af4d35b6a..ffd8a582cf 100644 --- a/tests/smoke_tests/parcel_c/abdul_razzak_ghan_2000/test_single_supersaturation_peak.py +++ b/tests/smoke_tests/parcel_c/abdul_razzak_ghan_2000/test_single_supersaturation_peak.py @@ -29,13 +29,13 @@ @pytest.mark.parametrize("rtol_x", (1e-7,)) @pytest.mark.parametrize("adaptive", (True,)) @pytest.mark.parametrize("scheme", ("PySDM",)) -def test_single_supersaturation_peak( +def test_single_saturation_peak( adaptive, scheme, rtol_x, rtol_thd, plot=False ): # pylint: disable=too-many-locals # arrange products = ( PySDM_products.WaterMixingRatio(unit="g/kg", name="liquid water mixing ratio"), - PySDM_products.PeakSupersaturation(name="S max"), + PySDM_products.PeakSaturation(name="S max"), PySDM_products.AmbientRelativeHumidity(name="RH"), PySDM_products.ParcelDisplacement(name="z"), ) @@ -111,7 +111,7 @@ def test_single_supersaturation_peak( pyplot.xlabel("radius [um]") pyplot.ylabel("z [m]") twin = pyplot.twiny() - twin.plot(output["S max"], output["z"], label="S max (top axis)") + twin.plot(np.asarray(output["S max"]) - 1, output["z"], label="S max (top axis)") twin.plot(np.asarray(output["RH"]) - 1, output["z"], label="ambient RH (top axis)") twin.legend(loc="upper center") twin.set_xlim(-0.001, 0.0015) diff --git a/tests/smoke_tests/parcel_c/grabowski_and_pawlowska_2023/test_figure_1_and_2.py b/tests/smoke_tests/parcel_c/grabowski_and_pawlowska_2023/test_figure_1_and_2.py index 8e0e7edbd8..bda07b259d 100644 --- a/tests/smoke_tests/parcel_c/grabowski_and_pawlowska_2023/test_figure_1_and_2.py +++ b/tests/smoke_tests/parcel_c/grabowski_and_pawlowska_2023/test_figure_1_and_2.py @@ -51,7 +51,7 @@ class TestFigure1And2: @staticmethod @pytest.mark.parametrize("aerosol", AEROSOLS) @pytest.mark.parametrize("w_cm_per_s", VELOCITIES_CM_PER_S) - @pytest.mark.parametrize("attribute", ("volume", "equilibrium supersaturation")) + @pytest.mark.parametrize("attribute", ("volume", "equilibrium saturation")) @pytest.mark.parametrize("drop_id", (0, -1)) def test_values_at_final_step( outputs: dict, @@ -85,7 +85,7 @@ def test_values_at_final_step( }, }, }, - "equilibrium supersaturation": { + "equilibrium saturation": { "pristine": { 25: {0: 0.05 / 100 + 1, -1: 0.005 / 100 + 1}, 100: {0: 0.15 / 100 + 1, -1: 0.005 / 100 + 1}, @@ -118,12 +118,12 @@ def test_ambient_humidity( ): output = outputs[aerosol][w_cm_per_s] attributes = output["attributes"] - for vol, crit_vol, eq_ss in zip( + for vol, crit_vol, eq_s in zip( attributes["volume"], attributes["critical volume"], - attributes["equilibrium supersaturation"], + attributes["equilibrium saturation"], ): if np.all(vol < crit_vol): assert np.isclose( - output["products"]["S_max"], eq_ss, rtol=rtol, atol=0 + output["products"]["S_max"], eq_s, rtol=rtol, atol=0 ).all() diff --git a/tests/smoke_tests/parcel_d/jensen_and_nugent_2017/test_fig_3_and_tab_4_upper_rows.py b/tests/smoke_tests/parcel_d/jensen_and_nugent_2017/test_fig_3_and_tab_4_upper_rows.py index 5638019921..8aca4f5b86 100644 --- a/tests/smoke_tests/parcel_d/jensen_and_nugent_2017/test_fig_3_and_tab_4_upper_rows.py +++ b/tests/smoke_tests/parcel_d/jensen_and_nugent_2017/test_fig_3_and_tab_4_upper_rows.py @@ -17,7 +17,7 @@ def find_cloud_base_index(products): for index, value in enumerate(products["S_max"]): - if value > 0: + if value > 1: cloud_base_index = index break return cloud_base_index @@ -67,7 +67,7 @@ def test_cloud_base_height(variables): @staticmethod def test_supersaturation_maximum(variables): - supersaturation = np.asarray(variables["output"]["products"]["S_max"]) + supersaturation = np.asarray(variables["output"]["products"]["S_max"]) - 1 assert signal.argrelextrema(supersaturation, np.greater)[0].shape[0] == 1 assert 0.35 * PER_CENT < np.nanmax(supersaturation) < 0.5 * PER_CENT diff --git a/tests/smoke_tests/parcel_d/jensen_and_nugent_2017/test_fig_4_and_7_and_tab_4_bottom_rows.py b/tests/smoke_tests/parcel_d/jensen_and_nugent_2017/test_fig_4_and_7_and_tab_4_bottom_rows.py index 42f23eba4e..3c71135ad2 100644 --- a/tests/smoke_tests/parcel_d/jensen_and_nugent_2017/test_fig_4_and_7_and_tab_4_bottom_rows.py +++ b/tests/smoke_tests/parcel_d/jensen_and_nugent_2017/test_fig_4_and_7_and_tab_4_bottom_rows.py @@ -46,7 +46,7 @@ def test_cloud_base_height(variables): @staticmethod def test_supersaturation_maximum(variables): - supersaturation = np.asarray(variables["output"]["products"]["S_max"]) + supersaturation = np.asarray(variables["output"]["products"]["S_max"]) - 1 assert signal.argrelextrema(supersaturation, np.greater)[0].shape[0] == 1 assert 0.4 * PER_CENT < np.nanmax(supersaturation) < 0.5 * PER_CENT diff --git a/tests/smoke_tests/parcel_d/jensen_and_nugent_2017/test_fig_5.py b/tests/smoke_tests/parcel_d/jensen_and_nugent_2017/test_fig_5.py index eeba97aa47..1402d4bda1 100644 --- a/tests/smoke_tests/parcel_d/jensen_and_nugent_2017/test_fig_5.py +++ b/tests/smoke_tests/parcel_d/jensen_and_nugent_2017/test_fig_5.py @@ -51,10 +51,10 @@ def test_cloud_base_height(variables): @staticmethod @pytest.mark.xfail(strict=True, reason="TODO #1266") - def test_supersaturation_maximum(variables): - supersaturation = np.asarray(variables["output"]["products"]["S_max"]) - assert signal.argrelextrema(supersaturation, np.greater)[0].shape[0] == 1 - assert 1.2 * PER_CENT < np.nanmax(supersaturation) < 1.4 * PER_CENT + def test_saturation_maximum(variables): + saturation = np.asarray(variables["output"]["products"]["S_max"]) + assert signal.argrelextrema(saturation, np.greater)[0].shape[0] == 1 + assert 1.2 * PER_CENT < np.nanmax(saturation - 1) < 1.4 * PER_CENT @staticmethod @pytest.mark.parametrize("drop_id", range(int(0.8 * N_SD), N_SD)) diff --git a/tests/smoke_tests/parcel_d/jensen_and_nugent_2017/test_fig_6.py b/tests/smoke_tests/parcel_d/jensen_and_nugent_2017/test_fig_6.py index 5aec61a1cf..a67c4e9912 100644 --- a/tests/smoke_tests/parcel_d/jensen_and_nugent_2017/test_fig_6.py +++ b/tests/smoke_tests/parcel_d/jensen_and_nugent_2017/test_fig_6.py @@ -51,7 +51,7 @@ def test_cloud_base_height(variables): @staticmethod def test_supersaturation_maximum(variables): - supersaturation = np.asarray(variables["output"]["products"]["S_max"]) + supersaturation = np.asarray(variables["output"]["products"]["S_max"]) - 1 extrema = signal.argrelextrema(supersaturation, np.greater, order=12) assert extrema[0].shape[0] == 1 assert 1.2 * PER_CENT < np.nanmax(supersaturation) < 1.3 * PER_CENT diff --git a/tests/unit_tests/attributes/test_critical_supersaturation.py b/tests/unit_tests/attributes/test_critical_saturation.py similarity index 94% rename from tests/unit_tests/attributes/test_critical_supersaturation.py rename to tests/unit_tests/attributes/test_critical_saturation.py index 4953312650..e83dbdb70a 100644 --- a/tests/unit_tests/attributes/test_critical_supersaturation.py +++ b/tests/unit_tests/attributes/test_critical_saturation.py @@ -8,11 +8,11 @@ from PySDM.products.condensation import ActivableFraction -def test_critical_supersaturation(): +def test_critical_saturation(): # arrange T = 300 * si.K n_sd = 100 - S_max = 0.01 + S_max = 1.000101 vdry = np.linspace(0.001, 1, n_sd) * si.um**3 env = Box(dt=np.nan, dv=np.nan) diff --git a/tests/unit_tests/backends/test_oxidation.py b/tests/unit_tests/backends/test_oxidation.py index 747040eba7..5ed2534b6c 100644 --- a/tests/unit_tests/backends/test_oxidation.py +++ b/tests/unit_tests/backends/test_oxidation.py @@ -181,5 +181,5 @@ def test_oxidation(conc, dt): np.testing.assert_allclose( actual=moles[k].data / volume - conc["input"][k], desired=conc["output"][k] * dt, - rtol=1e-11, + rtol=1e-10, ) diff --git a/tests/unit_tests/dynamics/condensation/test_parcel_sanity_checks.py b/tests/unit_tests/dynamics/condensation/test_parcel_sanity_checks.py index 8804cd2e17..9920f37906 100644 --- a/tests/unit_tests/dynamics/condensation/test_parcel_sanity_checks.py +++ b/tests/unit_tests/dynamics/condensation/test_parcel_sanity_checks.py @@ -40,7 +40,7 @@ class TestParcelSanityChecks: ), ), ) - def test_noisy_supersaturation_profiles(backend_class, plot=False): + def test_noisy_saturation_profiles(backend_class, plot=False): """cases found using the README parcel snippet""" # arrange env = Parcel( @@ -75,7 +75,7 @@ def test_noisy_supersaturation_profiles(backend_class, plot=False): "volume": FORMULAE.trivia.volume(radius=r_wet), }, products=( - products.PeakSupersaturation(name="S_max", unit="%"), + products.PeakSaturation(name="S_max_percent", unit="%"), products.EffectiveRadius( name="r_eff", unit="um", radius_range=CLOUD_RANGE ), @@ -117,8 +117,8 @@ def test_noisy_supersaturation_profiles(backend_class, plot=False): pyplot.clf() # assert - supersaturation_peaks, _ = signal.find_peaks(output["S_max"]) - assert len(supersaturation_peaks) == 1 + saturation_peaks, _ = signal.find_peaks(output["S_max_percent"]) + assert len(saturation_peaks) == 1 @staticmethod @pytest.mark.parametrize("update_thd", (True, False)) diff --git a/tests/unit_tests/dynamics/test_immersion_freezing.py b/tests/unit_tests/dynamics/test_freezing.py similarity index 77% rename from tests/unit_tests/dynamics/test_immersion_freezing.py rename to tests/unit_tests/dynamics/test_freezing.py index d9f8486cec..ba46790dd7 100644 --- a/tests/unit_tests/dynamics/test_immersion_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", @@ -86,27 +86,49 @@ def test_no_subsaturated_freezing(self): pass @staticmethod - @pytest.mark.parametrize("singular", (True, False)) + @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, singular, thaw, epsilon): + def test_thaw(backend_class, freezing_type, thaw, epsilon): # arrange + singular = False + immersion_freezing = True + homogeneous_freezing = False + if freezing_type == "het_singular": + freezing_parameter = {} + singular = True + elif freezing_type == "het_time_dependent": + freezing_parameter = { + "heterogeneous_ice_nucleation_rate": "Constant", + "constants": {"J_HET": 0}, + } + elif freezing_type == "hom_time_dependent": + freezing_parameter = { + "homogeneous_ice_nucleation_rate": "Constant", + "constants": {"J_HOM": 0}, + } + immersion_freezing = False + homogeneous_freezing = True + if backend_class.__name__ == "ThrustRTC": + pytest.skip() formulae = Formulae( particle_shape_and_density="MixedPhaseSpheres", - **( - {} - if singular - else { - "heterogeneous_ice_nucleation_rate": "Constant", - "constants": {"J_HET": 0}, - } - ), + **(freezing_parameter), ) env = Box(dt=1 * si.s, dv=1 * si.m**3) builder = Builder( n_sd=1, backend=backend_class(formulae=formulae), environment=env ) - builder.add_dynamic(Freezing(singular=singular, thaw=thaw)) + builder.add_dynamic( + Freezing( + singular=singular, + homogeneous_freezing=homogeneous_freezing, + immersion_freezing=immersion_freezing, + thaw=thaw, + ) + ) particulator = builder.build( products=(IceWaterContent(),), attributes={ @@ -123,6 +145,7 @@ def test_thaw(backend_class, singular, thaw, epsilon): ) particulator.environment["T"] = formulae.constants.T0 + epsilon particulator.environment["RH"] = np.nan + particulator.environment["RH_ice"] = np.nan if not singular: particulator.environment["a_w_ice"] = np.nan assert particulator.products["ice water content"].get() > 0 @@ -137,7 +160,7 @@ def test_thaw(backend_class, singular, 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 @@ -176,8 +199,13 @@ 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): + @pytest.mark.parametrize( + "freezing_type", ("het_time_dependent", "hom_time_dependent") + ) + # pylint: disable=too-many-locals,too-many-statements + def test_freezing_time_dependent( + backend_class, freezing_type, double_precision, plot=False + ): if backend_class.__name__ == "Numba" and not double_precision: pytest.skip() @@ -193,6 +221,7 @@ def test_freeze_time_dependent(backend_class, double_precision, plot=False): ) rate = 1e-9 immersed_surface_area = 1 + droplet_volume = 1 number_of_real_droplets = 1024 total_time = ( @@ -200,9 +229,7 @@ def test_freeze_time_dependent(backend_class, double_precision, plot=False): ) # dummy (but must-be-set) values - initial_water_mass = ( - 44 # for sign flip (ice water has negative volumes), value does not matter - ) + 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): @@ -211,15 +238,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 == "het_time_dependent": + freezing_parameter = { + "heterogeneous_ice_nucleation_rate": "Constant", + "constants": {"J_HET": rate / immersed_surface_area}, + } + elif freezing_type == "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: @@ -238,7 +282,13 @@ 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), @@ -246,7 +296,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 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..f3b51a01ec --- /dev/null +++ b/tests/unit_tests/physics/test_homogeneous_nucleation_rates.py @@ -0,0 +1,119 @@ +""" +test for homogeneous nucleation rate parameterisations +""" + +from contextlib import nullcontext +import re +import pytest +from matplotlib import pyplot +import numpy as np +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], + "jhom_log10": [5, 11, 15, 20], +} + + +class TestHomogeneousIceNucleationRate: + @staticmethod + @pytest.mark.parametrize( + "index", range(len(SPICHTINGER_ET_AL_2023_FIG2_DATA["da_w_ice"])) + ) + @pytest.mark.parametrize( + "parametrisation, context", + ( + ("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,)" + ), + ), + ), + ), + ) + 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( + homogeneous_ice_nucleation_rate=parametrisation, + ) + + # act + 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"]) + ) + ) + + # 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 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]") diff --git a/tests/unit_tests/products/test_activation_criteria.py b/tests/unit_tests/products/test_activation_criteria.py new file mode 100644 index 0000000000..ef37d6931e --- /dev/null +++ b/tests/unit_tests/products/test_activation_criteria.py @@ -0,0 +1,128 @@ +"""tests four different ways of diagnosing activable fraction in a Parcel environment""" + +import pytest +import numpy as np +from matplotlib import pyplot + +from PySDM.products import ( + ActivableFraction, + AmbientRelativeHumidity, + PeakSaturation, + Time, +) +from PySDM.environments import Parcel +from PySDM.dynamics import Condensation, AmbientThermodynamics +from PySDM import Builder +from PySDM.physics import si +from PySDM.backends import CPU, GPU +from PySDM.initialisation.spectra import Lognormal +from PySDM.initialisation.sampling import spectral_sampling + + +@pytest.mark.parametrize( + "backend", + ( + pytest.param(GPU(), marks=pytest.mark.xfail(strict=True)), + CPU(override_jit_flags={"parallel": False}), + ), +) +def test_activation_criteria(backend, plot=False): + # arrange + builder = Builder( + n_sd=1000, + backend=backend, + environment=Parcel( + dt=2 * si.s, + mass_of_dry_air=100 * si.kg, + p0=1000 * si.hPa, + initial_water_vapour_mixing_ratio=22 * si.g / si.kg, + T0=300 * si.K, + w=2.5 * si.m / si.s, + ), + ) + builder.add_dynamic(AmbientThermodynamics()) + builder.add_dynamic(Condensation()) + + r_dry, specific_concentration = spectral_sampling.ConstantMultiplicity( + Lognormal(norm_factor=1e4 / si.mg, m_mode=50 * si.nm, s_geom=1.5) + ).sample(builder.particulator.n_sd) + + particulator = builder.build( + attributes=builder.particulator.environment.init_attributes( + n_in_dv=specific_concentration + * builder.particulator.environment.mass_of_dry_air, + kappa=0.666, + r_dry=r_dry, + ), + products=( + Time(), + PeakSaturation(name="S_max"), + AmbientRelativeHumidity(name="RH"), + ActivableFraction( + name="AF1 (r_wet > r_cri(T))", + filter_attr="wet to critical volume ratio", + ), + ActivableFraction( + name="AF2 (r_wet > r_cri(T0))", + filter_attr="wet to critical volume ratio neglecting temperature variations", + ), + ActivableFraction( + name="AF3 (S_crit(T) < S_max)", filter_attr="critical saturation" + ), + ActivableFraction( + name="AF4 (S_crit(T0) < S_max)", + filter_attr="critical saturation neglecting temperature variations", + ), + ), + ) + + # act + data = {product: [] for product in particulator.products} + s_max = np.nan + for i in range(50): + particulator.run(steps=1) + for key, product in particulator.products.items(): + if ( + key == "S_max" + and i > 0 + and (np.isnan(s_max) or data["S_max"][-1] > s_max) + ): + s_max = data["S_max"][-1] + value = product.get(**({} if key.startswith("wet") else {"S_max": s_max})) + if isinstance(value, np.ndarray): + value = value[0] + data[key].append(value) + + # plot + pyplot.title(backend.__class__.__name__) + for k, datum in data.items(): + if k.startswith("AF"): + pyplot.plot(data["time"], datum, label=k, marker=k[2], markersize=10) + pyplot.xlabel("time [s] (values at the end of each timestep)") + pyplot.ylabel("activated fraction [1]") + pyplot.ylim(0.4, 0.65) + pyplot.legend() + pyplot.grid() + + twin = pyplot.gca().twinx() + twin.plot(data["time"], np.asarray(data["RH"]) * 100 - 100, color="red", marker="o") + twin.set_ylabel("supersaturation [%]", color="red") + twin.set_ylim(-1.5, 0.5) + twin.set_yticks(np.linspace(-1.5, 0.5, 5, endpoint=True)) + + if plot: + pyplot.show() + else: + pyplot.clf() + + # assert + for k, datum in data.items(): + if k.startswith("AF"): + assert datum[0] == 0 + assert 0.4 < datum[-1] <= 0.65 + assert np.all(np.diff(datum[40:]) <= 0) + assert ( + data["AF1 (r_wet > r_cri(T))"][-1] + < data["AF3 (S_crit(T) < S_max)"][-1] + < data["AF4 (S_crit(T0) < S_max)"][-1] + ) diff --git a/tests/unit_tests/test_builder.py b/tests/unit_tests/test_builder.py index 7bb17578f2..51f7cca18b 100644 --- a/tests/unit_tests/test_builder.py +++ b/tests/unit_tests/test_builder.py @@ -32,7 +32,7 @@ def test_request_attribute(): builder.add_dynamic(Condensation()) # act - builder.request_attribute("critical supersaturation") + builder.request_attribute("critical saturation") # assert particulator = builder.build( @@ -48,7 +48,7 @@ def test_request_attribute(): }, ) particulator.environment["T"] = np.nan - _ = particulator.attributes["critical supersaturation"].to_ndarray() + _ = particulator.attributes["critical saturation"].to_ndarray() @staticmethod @pytest.mark.parametrize( diff --git a/tutorials/condensation/condensation_playground.ipynb b/tutorials/condensation/condensation_playground.ipynb index a1ea9e2126..d34bf7584d 100644 --- a/tutorials/condensation/condensation_playground.ipynb +++ b/tutorials/condensation/condensation_playground.ipynb @@ -22,21 +22,21 @@ }, { "cell_type": "code", - "execution_count": 1, "metadata": { "ExecuteTime": { - "end_time": "2025-05-15T14:37:24.852784Z", - "start_time": "2025-05-15T14:37:24.848905Z" + "end_time": "2025-06-15T21:05:57.860497Z", + "start_time": "2025-06-15T21:05:57.852968Z" } }, - "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": 1 }, { "cell_type": "markdown", @@ -104,14 +104,12 @@ }, { "cell_type": "code", - "execution_count": 2, "metadata": { "ExecuteTime": { - "end_time": "2025-05-15T14:37:28.579961Z", - "start_time": "2025-05-15T14:37:24.855244Z" + "end_time": "2025-06-15T21:06:01.231290Z", + "start_time": "2025-06-15T21:05:57.864439Z" } }, - "outputs": [], "source": [ "# import functions for creating interactive widget\n", "from PySDM_examples.utils import widgets\n", @@ -132,33 +130,33 @@ "from PySDM_examples.Pyrcel.settings import Settings\n", "from PySDM_examples.Pyrcel.simulation import Simulation\n", "from PySDM_examples.Pyrcel.profile_plotter import ProfilePlotter" - ] + ], + "outputs": [], + "execution_count": 2 }, { "cell_type": "code", - "execution_count": 3, "metadata": { "ExecuteTime": { - "end_time": "2025-05-15T14:37:28.660764Z", - "start_time": "2025-05-15T14:37:28.658338Z" + "end_time": "2025-06-15T21:06:01.321715Z", + "start_time": "2025-06-15T21:06:01.318787Z" } }, - "outputs": [], "source": [ "# create progress bar for widget\n", "progbar = widgets.IntProgress(min=0, max=100, description='%')" - ] + ], + "outputs": [], + "execution_count": 3 }, { "cell_type": "code", - "execution_count": 4, "metadata": { "ExecuteTime": { - "end_time": "2025-05-15T14:37:28.681841Z", - "start_time": "2025-05-15T14:37:28.678698Z" + "end_time": "2025-06-15T21:06:01.341605Z", + "start_time": "2025-06-15T21:06:01.334766Z" } }, - "outputs": [], "source": [ "# create initial aerosol distribution\n", "# run cloud parcel model\n", @@ -202,7 +200,7 @@ " ParcelDisplacement(\n", " name='z'),\n", " AmbientRelativeHumidity(\n", - " name='S_max', unit='%', var='RH'),\n", + " name='S_max_percent', unit='%', var='RH'),\n", " AmbientTemperature(\n", " name='T'),\n", " ParticleSizeSpectrumPerVolume(\n", @@ -217,7 +215,9 @@ " plotter = ProfilePlotter(settings)\n", " plotter.plot(output)\n", " plotter.show()\n" - ] + ], + "outputs": [], + "execution_count": 4 }, { "cell_type": "markdown", @@ -235,2446 +235,12 @@ }, { "cell_type": "code", - "execution_count": 5, "metadata": { "ExecuteTime": { - "end_time": "2025-05-15T14:37:31.104742Z", - "start_time": "2025-05-15T14:37:28.685494Z" + "end_time": "2025-06-15T21:06:18.983727Z", + "start_time": "2025-06-15T21:06:01.357467Z" } }, - "outputs": [ - { - "data": { - "image/svg+xml": [ - "\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " 2025-05-15T17:05:43.492516\n", - " image/svg+xml\n", - " \n", - " \n", - " Matplotlib v3.10.0, 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", - " \n", - " \n", - "\n" - ], - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "428770de014142efa5e9c2dd2fd69613", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "HBox(children=(HTML(value=\"./tmpx0bm3k69.pdf
\"), HTML(value…" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], "source": [ "# create widget\n", "# use to explore how the hygroscopicity, number concentration, and mean radius\n", @@ -2696,7 +262,52 @@ " widgets.display(sliders, progbar, widgets.interactive_output(demo, inputs))\n", "else:\n", " demo(**{k:v.value for k,v in inputs.items()})" - ] + ], + "outputs": [ + { + "data": { + "text/plain": [ + "HBox(children=(FloatSlider(value=1.2, continuous_update=False, description='κ2', max=1.4, min=0.2, readout_for…" + ], + "application/vnd.jupyter.widget-view+json": { + "version_major": 2, + "version_minor": 0, + "model_id": "3d358b03c7e7445a9a756b6e982b062d" + } + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "IntProgress(value=100, description='%')" + ], + "application/vnd.jupyter.widget-view+json": { + "version_major": 2, + "version_minor": 0, + "model_id": "bc960df8a8204bf8ae95576e44da6cec" + } + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "Output()" + ], + "application/vnd.jupyter.widget-view+json": { + "version_major": 2, + "version_minor": 0, + "model_id": "aacc0a809a7d40dcad8f54381b5667c8" + } + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "execution_count": 5 }, { "cell_type": "markdown", @@ -2725,15 +336,15 @@ }, { "cell_type": "code", - "execution_count": null, "metadata": { "ExecuteTime": { - "end_time": "2025-05-15T14:37:31.123249Z", - "start_time": "2025-05-15T14:37:31.120727Z" + "end_time": "2025-06-15T21:06:19.025154Z", + "start_time": "2025-06-15T21:06:19.022634Z" } }, + "source": [], "outputs": [], - "source": [] + "execution_count": null } ], "metadata": {