From 20c83400c8954148579efb1659cf8a0e3a96f758 Mon Sep 17 00:00:00 2001 From: Sylwester Arabas Date: Fri, 13 Jun 2025 15:56:15 +0200 Subject: [PATCH 01/19] constants: redefine room temperature to be 25C instead of 25.01C (#1646) --- PySDM/physics/constants_defaults.py | 2 +- tests/unit_tests/backends/test_oxidation.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/PySDM/physics/constants_defaults.py b/PySDM/physics/constants_defaults.py index 39d89938dc..5df49310b9 100644 --- a/PySDM/physics/constants_defaults.py +++ b/PySDM/physics/constants_defaults.py @@ -295,7 +295,7 @@ p_STP = 101325 * si.pascal """ ... and pressure """ -ROOM_TEMP = T_tri + 25 * si.K +ROOM_TEMP = T0 + 25 * si.K """ room temperature """ dT_u = si.K diff --git a/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, ) From ea9144e0d21dea7e17b01dec4fd9ce38d4a5062f Mon Sep 17 00:00:00 2001 From: Sylwester Arabas Date: Mon, 16 Jun 2025 16:47:39 +0200 Subject: [PATCH 02/19] introduce multiple choices for activation criteria in `ActivableFraction` product; sanitize saturation vs. supersaturation naming and units across the codebase (#1595) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: AgnieszkaZaba <56157996+AgnieszkaZaba@users.noreply.github.com> Co-authored-by: Agnieszka Żaba --- PySDM/attributes/impl/__init__.py | 1 + .../temperature_variation_option_attribute.py | 18 + PySDM/attributes/physics/__init__.py | 4 +- .../attributes/physics/critical_saturation.py | 54 + .../physics/critical_supersaturation.py | 37 - PySDM/attributes/physics/critical_volume.py | 61 +- ...aturation.py => equilibrium_saturation.py} | 6 +- PySDM/environments/parcel.py | 4 +- PySDM/products/condensation/__init__.py | 2 +- .../condensation/activable_fraction.py | 23 +- ..._supersaturation.py => peak_saturation.py} | 8 +- docs/markdown/pysdm_landing.md | 8 +- .../fig_4_kinetic_limitations.ipynb | 2 +- .../Abdul_Razzak_Ghan_2000/run_ARG_parcel.py | 8 +- .../Arabas_and_Shima_2017/fig_5.ipynb | 4350 ++++++++++++++++- .../Arabas_and_Shima_2017/simulation.py | 8 +- .../fig_5_SCIPY_VS_ADAPTIVE.py | 4 +- .../Bartman_et_al_2021/demo_fig2.ipynb | 4 +- .../figure_1.ipynb | 6 +- .../figure_2.ipynb | 6 +- .../figure_ripening_rate.ipynb | 3 +- .../simulation.py | 12 +- .../Graf_et_al_2019/figure_4.ipynb | 6 +- .../Jaruga_and_Pawlowska_2018/fig_2.ipynb | 4 +- .../Fig_4_and_7_and_Tab_4_bottom_rows.ipynb | 4 +- .../Jensen_and_Nugent_2017/simulation.py | 6 +- .../Jouzel_and_Merlivat_1984/fig_8_9.ipynb | 2 +- .../Kreidenweis_et_al_2003/simulation.py | 2 +- .../Lowe_et_al_2019/fig_2.ipynb | 3731 +------------- .../Lowe_et_al_2019/simulation.py | 4 +- .../Pyrcel/example_basic_run.ipynb | 2198 +-------- .../PySDM_examples/Pyrcel/profile_plotter.py | 8 +- .../Shipway_and_Hill_2012/simulation.py | 2 +- .../make_default_product_collection.py | 2 +- .../shipway_and_hill_2012/test_few_steps.py | 6 +- .../lowe_et_al_2019/test_dz_sensitivity.py | 7 +- .../parcel_a/lowe_et_al_2019/test_fig_2.py | 2 +- .../parcel_a/pyrcel/test_parcel_example.py | 16 +- .../test_conservation.py | 112 +- .../arabas_and_shima_2017/test_vs_scipy.py | 10 +- .../test_single_supersaturation_peak.py | 6 +- .../test_figure_1_and_2.py | 10 +- .../test_fig_3_and_tab_4_upper_rows.py | 4 +- .../test_fig_4_and_7_and_tab_4_bottom_rows.py | 2 +- .../jensen_and_nugent_2017/test_fig_5.py | 8 +- .../jensen_and_nugent_2017/test_fig_6.py | 2 +- ...uration.py => test_critical_saturation.py} | 4 +- .../condensation/test_parcel_sanity_checks.py | 8 +- .../products/test_activation_criteria.py | 128 + tests/unit_tests/test_builder.py | 4 +- .../condensation_playground.ipynb | 2535 +--------- 51 files changed, 4989 insertions(+), 8473 deletions(-) create mode 100644 PySDM/attributes/impl/temperature_variation_option_attribute.py create mode 100644 PySDM/attributes/physics/critical_saturation.py delete mode 100644 PySDM/attributes/physics/critical_supersaturation.py rename PySDM/attributes/physics/{equilibrium_supersaturation.py => equilibrium_saturation.py} (86%) rename PySDM/products/condensation/{peak_supersaturation.py => peak_saturation.py} (84%) rename tests/unit_tests/attributes/{test_critical_supersaturation.py => test_critical_saturation.py} (94%) create mode 100644 tests/unit_tests/products/test_activation_criteria.py 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/environments/parcel.py b/PySDM/environments/parcel.py index 245afab028..59e65c54bc 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, 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/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/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/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/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/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/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": { From 9717362a63896dd35778522af4ecaab07db9d1ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20L=C3=BCttmer?= <154344756+tluettm@users.noreply.github.com> Date: Wed, 18 Jun 2025 12:38:41 +0200 Subject: [PATCH 03/19] Homogeneous freezing (as a new option in `Freezing` dynamic, disabled by default + new physics formulae for hom. nucl. rate); new example: `Spichtinger_et_al_2023` (#1488) Co-authored-by: Sylwester Arabas --- .../impl_common/freezing_attributes.py | 11 + .../impl_numba/methods/freezing_methods.py | 145 +- PySDM/dynamics/freezing.py | 44 +- PySDM/formulae.py | 2 + PySDM/particulator.py | 16 + PySDM/physics/__init__.py | 1 + PySDM/physics/constants_defaults.py | 45 +- .../__init__.py | 9 + .../constant.py | 22 + .../homogeneous_ice_nucleation_rate/koop.py | 35 + .../koop_corr.py | 37 + .../koop_murray.py | 38 + .../homogeneous_ice_nucleation_rate/null.py | 23 + PySDM/physics/trivia.py | 4 + docs/bibliography.json | 28 + .../Spichtinger_et_al_2023/__init__.py | 7 + .../Spichtinger_et_al_2023/data/__init__.py | 0 .../data/reference_bulk.py | 47 + .../data/simulation_data.py | 22 + .../Spichtinger_et_al_2023/fig_B1.ipynb | 2004 +++++++++++++++++ .../Spichtinger_et_al_2023/settings.py | 66 + .../Spichtinger_et_al_2023/simulation.py | 121 + examples/docs/pysdm_examples_landing.md | 2 + tests/examples_tests/conftest.py | 3 +- ...immersion_freezing.py => test_freezing.py} | 95 +- .../test_homogeneous_nucleation_rates.py | 119 + 26 files changed, 2885 insertions(+), 61 deletions(-) create mode 100644 PySDM/physics/homogeneous_ice_nucleation_rate/__init__.py create mode 100644 PySDM/physics/homogeneous_ice_nucleation_rate/constant.py create mode 100644 PySDM/physics/homogeneous_ice_nucleation_rate/koop.py create mode 100644 PySDM/physics/homogeneous_ice_nucleation_rate/koop_corr.py create mode 100644 PySDM/physics/homogeneous_ice_nucleation_rate/koop_murray.py create mode 100644 PySDM/physics/homogeneous_ice_nucleation_rate/null.py create mode 100644 examples/PySDM_examples/Spichtinger_et_al_2023/__init__.py create mode 100644 examples/PySDM_examples/Spichtinger_et_al_2023/data/__init__.py create mode 100644 examples/PySDM_examples/Spichtinger_et_al_2023/data/reference_bulk.py create mode 100644 examples/PySDM_examples/Spichtinger_et_al_2023/data/simulation_data.py create mode 100644 examples/PySDM_examples/Spichtinger_et_al_2023/fig_B1.ipynb create mode 100644 examples/PySDM_examples/Spichtinger_et_al_2023/settings.py create mode 100644 examples/PySDM_examples/Spichtinger_et_al_2023/simulation.py rename tests/unit_tests/dynamics/{test_immersion_freezing.py => test_freezing.py} (77%) create mode 100644 tests/unit_tests/physics/test_homogeneous_nucleation_rates.py 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/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 5df49310b9..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)) @@ -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/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/docs/bibliography.json b/docs/bibliography.json index 01546a2c61..44b4be2186 100644 --- a/docs/bibliography.json +++ b/docs/bibliography.json @@ -897,5 +897,33 @@ ], "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)" } } 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/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/conftest.py b/tests/examples_tests/conftest.py index 74cafe3e12..2b57a0daa5 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"], 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]") From 07512fe176fd566d0c577d7a0e4324ce2df63991 Mon Sep 17 00:00:00 2001 From: Sylwester Arabas Date: Wed, 18 Jun 2025 15:34:28 +0200 Subject: [PATCH 04/19] CI: do not run tests for docs/README-only commits in PRs (#1652) --- .github/workflows/tests.yml | 1 + 1 file changed, 1 insertion(+) 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' From cde2788008102a343f89cdb466706a506bda8ab3 Mon Sep 17 00:00:00 2001 From: Rafal Lukosz <93160829+lursz@users.noreply.github.com> Date: Tue, 10 Jun 2025 16:42:58 +0200 Subject: [PATCH 05/19] Alien attempt (#3) * simulation setup * uv lock * introduced all equations * oblate spheroid impl * AlienParcel class extending Parcel * PySDM package internal imports fix * Formulae bug fixes * Planet is now dataclass instead of dict * drop falling without pancake shape * cloudbase fix * figure settings * multicore support for simulation (#2) * parallel v1 * thread count fix * parallel * small parallel fixes * working multithread solution * Multi-thread + vector plots * black, black used everywhere --------- Co-authored-by: Piotr Kubala * pip install joblib * Tests (#1) * tests I guess * uv fix * planet impl * ground truth * gen plot function * unit tests, monoticity tests, sanity tests * tests are now in tests * added ground truth files * removed leftovers * npy files * tests tidyup * tests adapted for current structure but still useless * renamed test to comply with pytest convention * fix ground_truth generation script * refactor unittests * added accuracy tests * Added saturation test for cloud base in unit tests * Refactored imports and clean up unused code in simulation and parcel modules * Enhanced accuracy tests by adding assertions for output completeness and consistency in unit tests * Refactored calculation of simulated mass fraction evaporation point to initialize variable and improved error handling --------- Co-authored-by: Hevagog Co-authored-by: Mateusz Mazur * Notebook not running fix - added back necessary imports m8 u removed too much * black formatter * black for ipynb * Update PySDM/physics/particle_shape_and_density/oblate_spheroid.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Updated tests/examples_tests/Loftus_and_Wordsworth_2021/ground_truth/gen_figure.py Using dir shadowed the built-in dir() function. Renamed this variable (e.g., root_dir) to avoid confusion. Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Fixed pressure values in Planet subclasses to use correct scientific notation * Updated method signature for proper binding. Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Refactored Settings class to remove unused 'coord' parameter and updated diffusion_coordinate assignment * Removed unused Lofus et al. 2021 implementations from particle shape and density, terminal velocity, and ventilation modules * plots warnings fixes * removed files rogers and yau * notebook rerun * notebook fix * minor linter fixes --------- Co-authored-by: emmacware Co-authored-by: Piotr Kubala Co-authored-by: Hevagog Co-authored-by: Mateusz Mazur --- PySDM/physics/terminal_velocity/__init__.py | 4 +- .../Loftus_and_Wordsworth_2021/__init__.py | 8 + .../Loftus_and_Wordsworth_2021/figure_2.ipynb | 14723 ++++++++++++++++ .../Loftus_and_Wordsworth_2021/parcel.py | 62 + .../Loftus_and_Wordsworth_2021/planet.py | 118 + .../Loftus_and_Wordsworth_2021/settings.py | 46 + .../Loftus_and_Wordsworth_2021/simulation.py | 82 + .../Loftus_and_Wordsworth_2021/__init__.py | 0 .../accuracy_test.py | 231 + .../ground_truth/RHgrid.npy | Bin 0 -> 72128 bytes .../ground_truth/RHs.npy | Bin 0 -> 608 bytes .../ground_truth/gen_figure.py | 97 + .../ground_truth/m_frac_evap.npy | Bin 0 -> 72128 bytes .../ground_truth/r0grid.npy | Bin 0 -> 72128 bytes .../ground_truth/r_mins.npy | Bin 0 -> 608 bytes .../Loftus_and_Wordsworth_2021/unit_test.py | 238 + tests/examples_tests/conftest.py | 1 + 17 files changed, 15609 insertions(+), 1 deletion(-) create mode 100644 examples/PySDM_examples/Loftus_and_Wordsworth_2021/__init__.py create mode 100644 examples/PySDM_examples/Loftus_and_Wordsworth_2021/figure_2.ipynb create mode 100644 examples/PySDM_examples/Loftus_and_Wordsworth_2021/parcel.py create mode 100644 examples/PySDM_examples/Loftus_and_Wordsworth_2021/planet.py create mode 100644 examples/PySDM_examples/Loftus_and_Wordsworth_2021/settings.py create mode 100644 examples/PySDM_examples/Loftus_and_Wordsworth_2021/simulation.py create mode 100644 tests/examples_tests/Loftus_and_Wordsworth_2021/__init__.py create mode 100644 tests/examples_tests/Loftus_and_Wordsworth_2021/accuracy_test.py create mode 100644 tests/examples_tests/Loftus_and_Wordsworth_2021/ground_truth/RHgrid.npy create mode 100644 tests/examples_tests/Loftus_and_Wordsworth_2021/ground_truth/RHs.npy create mode 100644 tests/examples_tests/Loftus_and_Wordsworth_2021/ground_truth/gen_figure.py create mode 100644 tests/examples_tests/Loftus_and_Wordsworth_2021/ground_truth/m_frac_evap.npy create mode 100644 tests/examples_tests/Loftus_and_Wordsworth_2021/ground_truth/r0grid.npy create mode 100644 tests/examples_tests/Loftus_and_Wordsworth_2021/ground_truth/r_mins.npy create mode 100644 tests/examples_tests/Loftus_and_Wordsworth_2021/unit_test.py 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/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..2d0f59fb30 --- /dev/null +++ b/examples/PySDM_examples/Loftus_and_Wordsworth_2021/__init__.py @@ -0,0 +1,8 @@ +""" +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..6a27e3ffa6 --- /dev/null +++ b/examples/PySDM_examples/Loftus_and_Wordsworth_2021/figure_2.ipynb @@ -0,0 +1,14723 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "1e8d983b", + "metadata": {}, + "outputs": [], + "source": [ + "!pip install joblib" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "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 (\n", + " Planet,\n", + " EarthLike,\n", + " Earth,\n", + " EarlyMars,\n", + " Jupiter,\n", + " Saturn,\n", + " K2_18B,\n", + ")\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": 4, + "id": "41f0ed6d", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "300.0" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "new_Earth = EarthLike()\n", + "new_Earth.T_STP" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "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", + "def compute_one_RH(i, RH):\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\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(\n", + " x\n", + " )\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", + " else:\n", + " row_data[j] = 1 - (output[\"r\"][-1] / (r * 1e6))\n", + " except Exception as _:\n", + " break\n", + "\n", + " return i, row_data, output" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "4352de81", + "metadata": {}, + "outputs": [], + "source": [ + "all_rows = Parallel(n_jobs=os.cpu_count())(\n", + " delayed(compute_one_RH)(i, RH) for i, RH in enumerate(RH_array[::-1])\n", + ")\n", + "\n", + "last_output = None\n", + "for i, row_data, output in all_rows:\n", + " output_matrix[i] = 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..7ac5eb0dac --- /dev/null +++ b/examples/PySDM_examples/Loftus_and_Wordsworth_2021/parcel.py @@ -0,0 +1,62 @@ +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, + ) + + def advance_parcel_vars(self): + """ + Compute new values of displacement, dry-air density and volume, + and write them to self._tmp and self.mesh.dv + """ + dt = self.particulator.dt + formulae = self.particulator.formulae + T = self["T"][0] + p = self["p"][0] + + dz_dt = -self.particulator.attributes["terminal velocity"].to_ndarray()[0] + water_vapour_mixing_ratio = ( + self["water_vapour_mixing_ratio"][0] + - self.delta_liquid_water_mixing_ratio / 2 + ) + + drho_dz = formulae.hydrostatics.drho_dz( + p=p, + T=T, + water_vapour_mixing_ratio=water_vapour_mixing_ratio, + lv=formulae.latent_heat_vapourisation.lv(T), + d_liquid_water_mixing_ratio__dz=( + self.delta_liquid_water_mixing_ratio / dz_dt / dt + ), + ) + drhod_dz = drho_dz + + self.particulator.backend.explicit_euler(self._tmp["z"], dt, dz_dt) + self.particulator.backend.explicit_euler( + self._tmp["rhod"], dt, dz_dt * drhod_dz + ) + + self.mesh.dv = formulae.trivia.volume_of_density_mass( + (self._tmp["rhod"][0] + self["rhod"][0]) / 2, self.mass_of_dry_air + ) 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..0e4211b397 --- /dev/null +++ b/examples/PySDM_examples/Loftus_and_Wordsworth_2021/planet.py @@ -0,0 +1,118 @@ +from PySDM.physics.constants import si +from dataclasses import dataclass +from typing import Optional, Dict, Any + + +@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 * 1e6 * si.pascal + 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 * 1e6 * si.pascal + 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 * 1e6 * si.pascal + 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 * 1e6 * si.pascal + 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 * 1e6 * si.pascal + 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 * 1e6 * si.pascal + 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 * 1e6 * si.pascal + 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..b0a81752a7 --- /dev/null +++ b/examples/PySDM_examples/Loftus_and_Wordsworth_2021/settings.py @@ -0,0 +1,46 @@ +# Planetary Properties, Loftus and Wordsworth 2021 Table 1 + +from pystrict import strict + +from PySDM import Formulae +from PySDM.dynamics import condensation +from PySDM.physics.constants import si +from PySDM_examples.Loftus_and_Wordsworth_2021.planet import Planet + + +@strict +class Settings: + 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..071dde8bf2 --- /dev/null +++ b/examples/PySDM_examples/Loftus_and_Wordsworth_2021/simulation.py @@ -0,0 +1,82 @@ +import numpy as np + +import PySDM.products as PySDM_products +from PySDM.backends import CPU +from PySDM.builder import Builder +from PySDM.dynamics import AmbientThermodynamics, Condensation +from PySDM.physics import constants as const +from PySDM_examples.Loftus_and_Wordsworth_2021.parcel import AlienParcel + + +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] > 1e-6: + self.particulator.run(1) + self.save(output) + + return output 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..75f57bb05d --- /dev/null +++ b/tests/examples_tests/Loftus_and_Wordsworth_2021/accuracy_test.py @@ -0,0 +1,231 @@ +import pytest +import os +import numpy as np +from scipy.optimize import fsolve + +from PySDM import Formulae +from PySDM.physics import si +from PySDM_examples.Loftus_and_Wordsworth_2021 import Settings, Simulation +from PySDM_examples.Loftus_and_Wordsworth_2021.planet import EarthLike + + +class GroundTruthLoader: + def __init__(self, groundtruth_dir_path, n_samples=2, random_seed=2137): + self.dir_path = groundtruth_dir_path + self.RHs = None + self.r0grid = None + self.m_frac_evap = None + self.n_samples = n_samples # Number of random samples to test + np.random.seed(random_seed) # reproducible random samples during debugging + + 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: + print(f"Error loading ground truth files: {e}") + raise + except Exception as e: + print(f"An unexpected error occurred while loading ground truth data: {e}") + raise + + def __exit__(self, exc_type, exc_val, exc_tb): + pass + + +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_solutions = fsolve(solve_Tcloud, [150.0, 300.0]) + Tcloud = np.max(Tcloud_solutions) + + Zcloud = (planet.T_STP - Tcloud) * cp_mix / planet.g_std + + th_std = formulae_instance.trivia.th_std(planet.p_STP, planet.T_STP) + + pcloud = formulae_instance.hydrostatics.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): + current_dir = os.path.dirname(os.path.abspath(__file__)) + 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}") + + formulae = Formulae( + ventilation="PruppacherAndRasmussen1979", + saturation_vapour_pressure="AugustRocheMagnus", + diffusion_coordinate="WaterMassLogarithm", + ) + + with GroundTruthLoader(groundtruth_dir) as gt: + if gt.RHs is None or gt.r0grid is None or gt.m_frac_evap is None: + pytest.fail("Ground truth data (.npy files) not loaded properly.") + + 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] + + for i_rh, j_r in sampled_ij_pairs: + current_planet_state = EarthLike() + + current_rh = gt.RHs[i_rh] + current_r_m = gt.r0grid[0, j_r] + expected_m_frac_evap = gt.m_frac_evap[i_rh, j_r] + + try: + iwvmr, Tcloud, Zcloud, pcloud = self._calculate_cloud_properties( + current_planet_state, current_rh, formulae + ) + simulated_m_frac_evap_point = self.calc_simulated_m_frac_evap_point( + current_planet_state, + formulae, + i_rh, + j_r, + current_rh, + current_r_m, + expected_m_frac_evap, + iwvmr, + Tcloud, + Zcloud, + pcloud, + ) + except Exception as e: + print( + f"Warning: Error in _calculate_cloud_properties for RH={current_rh} (sample idx {i_rh},{j_r}): {e}." + ) + + error_context = ( + f"Sample (RH_idx={i_rh}, R_idx={j_r}), " + f"RH={current_rh:.4f}, R_m={current_r_m:.3e}. " + f"Expected: {expected_m_frac_evap}, Got: {simulated_m_frac_evap_point}" + ) + + if np.isnan(expected_m_frac_evap): + assert np.isnan( + simulated_m_frac_evap_point + ), f"NaN Mismatch. {error_context} (Expected NaN, got non-NaN)" + else: + assert not np.isnan( + simulated_m_frac_evap_point + ), f"NaN Mismatch. {error_context} (Expected non-NaN, got NaN)" + np.testing.assert_allclose( + simulated_m_frac_evap_point, + expected_m_frac_evap, + rtol=1e-1, # Relative tolerance + atol=1e-1, # Absolute tolerance + err_msg=f"Value Mismatch. {error_context}", + ) + + def calc_simulated_m_frac_evap_point( + self, + current_planet_state, + formulae, + i_rh, + j_r, + current_rh, + current_r_m, + expected_m_frac_evap, + iwvmr, + Tcloud, + Zcloud, + pcloud, + ): + + simulated_m_frac_evap_point = np.nan + + if np.isnan(current_r_m) or current_r_m <= 0: + print(f"Warning: Invalid radius current_r_m={current_r_m} for sample idx {i_rh},{j_r}.") + else: + settings = Settings( + planet=current_planet_state, + r_wet=current_r_m, + mass_of_dry_air=1e5 * si.kg, + initial_water_vapour_mixing_ratio=iwvmr, + pcloud=pcloud, + Zcloud=Zcloud, + Tcloud=Tcloud, + formulae=formulae, + ) + 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: # Non-physical radius + simulated_m_frac_evap_point = 1.0 # 1.0 means fully evaporated + else: + simulated_m_frac_evap_point = np.nan + else: + final_radius_m = final_radius_um * 1e-6 + if current_r_m == 0: + frac_evap = 1.0 + else: + frac_evap = 1.0 - (final_radius_m / current_r_m) ** 3 + frac_evap = np.clip(frac_evap, 0.0, 1.0) + simulated_m_frac_evap_point = frac_evap + else: + simulated_m_frac_evap_point = np.nan + except Exception as e: + print( + f"Warning: Simulation run failed for RH={current_rh}, r={current_r_m} (sample idx {i_rh},{j_r}): {e}." + ) + if np.isclose(expected_m_frac_evap, 1.0, atol=1e-6): + simulated_m_frac_evap_point = 1.0 + else: + simulated_m_frac_evap_point = np.nan + + return simulated_m_frac_evap_point 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 0000000000000000000000000000000000000000..e02761b427cb379b6be665a7fbf61a190db2633e GIT binary patch literal 72128 zcmeI5OG}ht7={(3B8m>U2#uOh8k1S1q#e+n2vLzrE2LaxOgWOGS))j3QP6=xjIa_4 zGIUWAAwq~sMPLw-7?qle<{=$Vqa)#>La4rXW`04dXK~HuCYKl2eK*d$@8{h^*$1<7 zLPN?!D(!i$($jAH9-BQWf0x~1v*(w%%iPYhCrjLUF7xwLXHluk`g!RY=Q)@4@0i5x z4qIHp_H7Q^CENeKR)+XrpM*37;D85spt%R_PuJB}Mtl|$4tPKhwC_M^W|ZUSjxR#O z0T1Yb_8qXss>7YxEhHT9fF5Yyfyuhmn`r^g#O#JT^7G)+;0&@PHm@-vQH7 zPu*7`;eZG9K>H4?x4s>8w)Y7M2Rxt$+IOHcxw?6%yI)8+-~m0*z5~`TgVrZl146<9 z59opR9oTE$R;(Bl5)ODk547)q|M*qZkdW3fG&taaIfjP^YCSOTDms(D2?+;0pa#&gKgFHCk0Ul`Xfw?u;;T{nZ4tPKhwC{lbxM=FAkmijh9Pj`SH1{BI-eNR( z{)&W$`N9|v@Ia>rjpngW&M_h3fCuzI`wj%ohm8f#i<0m#=VtK$4|ICq^gmGWyO8EV zt8l;rJkZ>Oz=ogVlfC)4Q>(p>z810LXk z<{kv@w@wD{JCpD*_kHmI4|ICq|MNJWNg>V6IXK_}9%$}C;6CrH?O3m_J+?02?so&2ikWa6;M}4tPKhwC_Mg)s{!C8)k)s10K)=?K_b8u|K{!XI4l!-~m0* zz5|PE@4Rn(I4dL^@PHm@-+|_%X}R@-vqHiF59opR9Vn{(v-n`6S4cSE0X@*Z12F?F zw`xv!g@gkh&;#u|;E62WTvg{45)ODk547(?+UA6W z10K)=?K==TQ8QZRoD&ibct8)d??B(yjPl}!IU(VI2lPPu4%{mUU6nsRCnOy3fF5Yy zfvo3k^|{gWLc#$L=z;bfSiMlP^O$R1NI2jDJV{IWgP>k|?Vct8)d??6I_J2T3$AS4{{fF5YyfgfS9)*~bqgoFbg&;#u| U(3Cb-5#F>QBpmR79%$czf9(F>CIA2c literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..73132c62032c60981b5d5716004d69315df29ade GIT binary patch literal 608 zcmbR27wQ`j$;eQ~P_3SlTAW;@Zl$1ZlV+i=qoAIaUsO_*m=~X4l#&V(cT3DEP6dh= zXCxM+0{I$d20EHL3bhL411<(IxM06?x!|;Fsf+f-{>nPvO)l93i9<#Hm+ha;bBlPF zdBuJ{h-$fN4?S?0#)3!w*zrGZrg*kE8npP@lM;` zwa)@8xMvSC{ZP?;d$2KX51{%V*slOtpZE}J&O`e|u!9~!&3kMQv<&FN$53;h*k1vf z+PmQi)cmLRU~5l5h1&DX9_-ww&!F}_w}<%ig?-Qtxz^W#FYNh$UzK_?=>^pO7xow#|jdroFV^>ygF2|N2Y&s&2)(+a+Gvo1MF1v?219{bzv* zCsxdQWxpZVJ#OLcSN1v6{(YV)^V(kH=GM+hv9Il)O6SUV&wFjZC{AT-^ZnQMKt*0P z3UBPCpGn% z*1oOeUXsW2xAsokr~YuzcxV6rr%#_{<~#d6HY_SetKQjH)bHM@_4=K?DKNH_b>7>5 TWYGdz{oa0^`@<@cb?@x~9yk!q literal 0 HcmV?d00001 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..ff8d780ac4 --- /dev/null +++ b/tests/examples_tests/Loftus_and_Wordsworth_2021/ground_truth/gen_figure.py @@ -0,0 +1,97 @@ +################################################################ +# make LoWo21 Figure 2 +# r_min, fraction raindrop mass evaporated as functions of RH +################################################################ +import numpy as np +import matplotlib.pyplot as plt +import os + +# 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, + 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") +os.mkdir(figs_path) if not os.path.exists(figs_path) else None +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 0000000000000000000000000000000000000000..657765da4b1600296772f69df82fe9e09157c51c GIT binary patch literal 72128 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�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 z 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 ZA8sQ2aWSG1bV`dp#sWKy4sB>){s(E&21)<` literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..5f4e75e31d867f6c5ca2998a9315b8516510993c GIT binary patch literal 72128 zcmeI*`&*287zS`fbRvyHIv5pGrWn&fWtv+ll}bhFAd(J*RZX!DrLfkjq*!(-5^}ys zOCz;*9gA(TwpD63nVdyMN)Bu7zp&S}zuoiOynn#=`o7P5Kc9Qs^br5RaJ4?m`>Ynl z#-+q4L@q*+EZ#|EBNW9aDpD2EOQRAMv2p)CpBlX+CGPX{l!ehrai9NNIZAAV6C5Pg zHo|n_fBR{Nd)k^?S$4~l>$4x0zUr1|{i&Mxb4`zYLyDbW;la1^nX14Cn{#^QQPouo zTUNc3H#9}7sgmEzjba^6wkCg&8=a|+cV7Kbp0uG>?2`Mpe7ybv+sVg1$s--VR0JpV zfn%i6&PD2K=x=`eyg{)Vx->M~zV=kd+!JdC^|Y&F*Mj~!yZ82m&#qfH%R>7h&^2*E zs-XsI(l_V|8#PcZkjT4B`eXkK@gL4B2cXpPqLCz66J=L38-)|K@Fef*`dQ}Ms5v8c zG8?Lcb(4R6S}o9pwDGIdgTn_xU)Qig&uS3v^v~nbmi6Z4|9j0Cvbhxi6I8K=X^cs%?$(8*4t$0;JP(Hx}-^dDrkxLh*D+VmqqO$}z~IsReA&bK2m-Sq6;gnS{MPceA>Mm!3^CDXQ9 z)Q-aG%KA8w!W=n8j<+V7SzwE@5}R*X;Pz`n$^1>0a6XtOf9*XQquujdGxWyb!Nx8h zufN7XyZPw7+%ge58tgg>wpd}iG^4jX)f!f3PnnA6kA!?#RVkW|O zhv~sIS0_}QODx$r(iykQHSWCDa)Eu=8bQMc7wGoPQQz@)5^k2++Gl>0VS9M4Q=5() zwoP8=rIx@;*OT?37L(C*s6ceTVlqrRpB;Y`;)dJ%&9)ZwxZz5{r)K*ickCXnp7qpk z3dCA|S+Dwdz<6cMv#d%FjCIRyYESb-*37zV^E{`*+xc12NJB5|-CEq4*ye>{#{-^x z|BE-IX|L9|miZvKShA;J-!!a%bDds+FH9b0@69jtLtb0yvSo+-vB>24T|w1!xEDTO zf4MOLHNShyy4D7w{oXx;4&xxCxmx*tQyB!o>0J(;^JXA0^`RhIcP6UL%8dt~pNU83 zthYNY4@Th8C#^#rLeSRKRrK^-2m;Sbj&xSf!s6p4GDTr1_TR|UKD8nYYmT}4cSnZf zr$=X6a(p6S{n~%_Mfq$9FAo(xmPI0=URF5WV-8Np=9mP8%*EFHyW`xGq7Y^_BH-e* zdFY%1-%8ayoSBxaa?OlJ#3k#hbc^}unW~H~t(_0;zO?}Z(_+x7U8;FwLM$5MOhw~f z#-c^5HhJ->IBXQ3ZojfR9+evhe$4P+fT}D(bn}>nc(6P-)=D!07dl*@9_mcMk^-%D zM_LzQn9@8at$8ude$?_wQ7yp&uT8zG_AjvK#^GLz0g31;s(a~ekc92p{cAjrCPAhX zakj!U8Ra9sJy-uI89c9eUh%x*dByXJ_bcA7c)#NPiuWtN5AuDG?}L0FCpddT#U z=^@iYrr%D#oqjw0cKYq?n_%Ar`zF{o!G0X}qUvVV~M zgX|w<{~-HP*_X<`RQ9E^FO~hy?006rGy9#{N6$Wb_R+JCo_+KN#cJr%px;ivoqjw0 zcKYq~+v&H{Z>Qf*zny+N{dW58^xNsT({HEWPQRUgJN9^Bwr{7M$oqjw0cKYq~+v&H{Z>Qf*zny+N{dW58^xNsT({HEWPQRUgJN9^Bwr{7M$oqjw0cKYq~+v&H{Z>Qf*zny+N{dW58 z^xNsT({HEWPQRUgJN9^Bwr{7M$oqjw0cKYq~+v&H{ zZ>Qf*zny+N{dW58^xNsT({HEWPQRUgJN9^Bw J|G)kAe*hZO^Fsgt literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..dba178b0df375d983ec6a4987c4693411186ffba GIT binary patch literal 608 zcmbR27wQ`j$;eQ~P_3SlTAW;@Zl$1ZlV+i=qoAIaUsO_*m=~X4l#&V(cT3DEP6dh= zXCxM+0{I$d20EHL3bhL41Fk9o-GG@}TSF)&j{oXUhn?-6 zGC~$as5{#qFgQ11Uzd};N8#T$b3ZxS-}xuJ^;?{yee*i!mnY6U*n5WV>lATyu746TiZXdUijB5(8~S_$Cfp_ z!Yu7&(waiFVl3<%OQf&&WSQH?-g{*7sL{;+&8}D7<}*$0_i!5D>DgdnpKxlo>Z9Yv z_Q_UO-dZn=?A>~;vx+#3?B^WX-?U4^(Eh=p`{iE)4eYaZyUr$j)3eX6zCZ7phpzpa zX*;xx)@s`y|LW7UaFdq(^+W&U553j2clwsobizv0UZnbu)XP#0`+K{)SIw=|x5RFGW`t9pzE9cThX}{B)#(z0uJNhi+)f+uyzU-0A6IS^EonUuvsa z$=Ej>Tg9KgLCXHvXRUY3)g 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} exceeds 1.01 for {planet.__class__.__name__}" + + def test_water_vapour_mixing_ratio_calculation(self): + """Test water vapour mixing ratio calculation.""" + with self._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 + + @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 self._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 + + @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 self._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(output[key]) for key in output.keys()] + assert all( + l == lengths[0] for l 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() + new_Earth.T_STP + + RH_array = np.linspace(0.25, 0.99, 50) + const = formulae.constants + + def mix(dry, vap, ratio): + return (dry + ratio * vap) / (1 + ratio) + + for i, RH in enumerate(RH_array[::-1]): + new_Earth.RH_zref = RH + + pvs = formulae.saturation_vapour_pressure.pvs_water(new_Earth.T_STP) + initial_water_vapour_mixing_ratio = const.eps / ( + new_Earth.p_STP / new_Earth.RH_zref / pvs - 1 + ) + + Rair = mix(const.Rd, const.Rv, initial_water_vapour_mixing_ratio) + c_p = mix(const.c_pd, const.c_pv, initial_water_vapour_mixing_ratio) + + def f(x): + return initial_water_vapour_mixing_ratio / ( + initial_water_vapour_mixing_ratio + const.eps + ) * new_Earth.p_STP * (x / new_Earth.T_STP) ** ( + c_p / Rair + ) - formulae.saturation_vapour_pressure.pvs_water( + x + ) + + tdews = fsolve(f, [150, 300]) + Tcloud = np.max(tdews) + Zcloud = (new_Earth.T_STP - Tcloud) * c_p / new_Earth.g_std + thstd = formulae.trivia.th_std(new_Earth.p_STP, new_Earth.T_STP) + + pcloud = formulae.hydrostatics.p_of_z_assuming_const_th_and_initial_water_vapour_mixing_ratio( + new_Earth.p_STP, thstd, initial_water_vapour_mixing_ratio, Zcloud + ) + + 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 2b57a0daa5..0b18e0c3ea 100644 --- a/tests/examples_tests/conftest.py +++ b/tests/examples_tests/conftest.py @@ -84,6 +84,7 @@ def findfiles(path, regex): "utils", "Zaba_et_al_2025", "Gonfiantini_1986", + "Loftus_and_Wordsworth_2021", ], } From 29168e0e374a3bfb834bd2cb7a8707b2d3245a37 Mon Sep 17 00:00:00 2001 From: Hevagog Date: Tue, 17 Jun 2025 23:40:55 +0200 Subject: [PATCH 06/19] fix exception messages and more fixtures in tests --- .../accuracy_test.py | 295 +++++++++--------- 1 file changed, 152 insertions(+), 143 deletions(-) diff --git a/tests/examples_tests/Loftus_and_Wordsworth_2021/accuracy_test.py b/tests/examples_tests/Loftus_and_Wordsworth_2021/accuracy_test.py index 75f57bb05d..6f107c8374 100644 --- a/tests/examples_tests/Loftus_and_Wordsworth_2021/accuracy_test.py +++ b/tests/examples_tests/Loftus_and_Wordsworth_2021/accuracy_test.py @@ -1,7 +1,11 @@ +from __future__ import annotations + import pytest import os import numpy as np from scipy.optimize import fsolve +import warnings +from typing import Tuple, List, Generator from PySDM import Formulae from PySDM.physics import si @@ -10,13 +14,15 @@ class GroundTruthLoader: - def __init__(self, groundtruth_dir_path, n_samples=2, random_seed=2137): + 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 # Number of random samples to test - np.random.seed(random_seed) # reproducible random samples during debugging + self.n_samples = n_samples + np.random.seed(random_seed) def __enter__(self): try: @@ -25,16 +31,64 @@ def __enter__(self): self.m_frac_evap = np.load(os.path.join(self.dir_path, "m_frac_evap.npy")) return self except FileNotFoundError as e: - print(f"Error loading ground truth files: {e}") - raise + pytest.fail(f"Error loading ground truth files: {e}") except Exception as e: - print(f"An unexpected error occurred while loading ground truth data: {e}") - raise + pytest.fail(f"Unexpected error loading ground truth data: {e}") def __exit__(self, exc_type, exc_val, exc_tb): pass +@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 + + +@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 + ] + + +@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): @@ -44,15 +98,11 @@ 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) @@ -78,154 +128,113 @@ def solve_Tcloud(T_candidate): ) return initial_water_vapour_mixing_ratio, Tcloud, Zcloud, pcloud - def test_figure_2_replication_accuracy(self): - current_dir = os.path.dirname(os.path.abspath(__file__)) - 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}") - + def test_figure_2_replication_accuracy(self, ground_truth_sample, static_arrays): formulae = Formulae( ventilation="PruppacherAndRasmussen1979", saturation_vapour_pressure="AugustRocheMagnus", diffusion_coordinate="WaterMassLogarithm", ) - - with GroundTruthLoader(groundtruth_dir) as gt: - if gt.RHs is None or gt.r0grid is None or gt.m_frac_evap is None: - pytest.fail("Ground truth data (.npy files) not loaded properly.") - - 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)] + for sample in ground_truth_sample: + planet = EarthLike() + rh = sample["rh"] + r_m = sample["r_m"] + expected = sample["expected_m_frac_evap"] + i_rh = sample["i_rh"] + j_r = sample["j_r"] + try: + iwvmr, Tcloud, Zcloud, pcloud = self._calculate_cloud_properties( + planet, rh, formulae + ) + simulated = self.calc_simulated_m_frac_evap_point( + planet, + formulae, + i_rh, + j_r, + rh, + r_m, + expected, + iwvmr, + Tcloud, + Zcloud, + pcloud, + ) + except Exception as e: + pytest.fail( + f"Error in _calculate_cloud_properties for RH={rh} (sample idx {i_rh},{j_r}): {e}." + ) + error_context = ( + f"Sample (RH_idx={i_rh}, R_idx={j_r}), RH={rh:.4f}, R_m={r_m:.3e}. " + f"Expected: {expected}, Got: {simulated}" ) - - sampled_indices_flat = np.random.choice(len(all_indices), n_samples, replace=False) - sampled_ij_pairs = all_indices[sampled_indices_flat] - - for i_rh, j_r in sampled_ij_pairs: - current_planet_state = EarthLike() - - current_rh = gt.RHs[i_rh] - current_r_m = gt.r0grid[0, j_r] - expected_m_frac_evap = gt.m_frac_evap[i_rh, j_r] - - try: - iwvmr, Tcloud, Zcloud, pcloud = self._calculate_cloud_properties( - current_planet_state, current_rh, formulae - ) - simulated_m_frac_evap_point = self.calc_simulated_m_frac_evap_point( - current_planet_state, - formulae, - i_rh, - j_r, - current_rh, - current_r_m, - expected_m_frac_evap, - iwvmr, - Tcloud, - Zcloud, - pcloud, - ) - except Exception as e: - print( - f"Warning: Error in _calculate_cloud_properties for RH={current_rh} (sample idx {i_rh},{j_r}): {e}." - ) - - error_context = ( - f"Sample (RH_idx={i_rh}, R_idx={j_r}), " - f"RH={current_rh:.4f}, R_m={current_r_m:.3e}. " - f"Expected: {expected_m_frac_evap}, Got: {simulated_m_frac_evap_point}" + 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}", ) - if np.isnan(expected_m_frac_evap): - assert np.isnan( - simulated_m_frac_evap_point - ), f"NaN Mismatch. {error_context} (Expected NaN, got non-NaN)" - else: - assert not np.isnan( - simulated_m_frac_evap_point - ), f"NaN Mismatch. {error_context} (Expected non-NaN, got NaN)" - np.testing.assert_allclose( - simulated_m_frac_evap_point, - expected_m_frac_evap, - rtol=1e-1, # Relative tolerance - atol=1e-1, # Absolute tolerance - err_msg=f"Value Mismatch. {error_context}", - ) - def calc_simulated_m_frac_evap_point( self, - current_planet_state, + planet, formulae, i_rh, j_r, - current_rh, - current_r_m, - expected_m_frac_evap, + rh, + r_m, + expected, iwvmr, Tcloud, Zcloud, pcloud, ): - - simulated_m_frac_evap_point = np.nan - - if np.isnan(current_r_m) or current_r_m <= 0: - print(f"Warning: Invalid radius current_r_m={current_r_m} for sample idx {i_rh},{j_r}.") - else: - settings = Settings( - planet=current_planet_state, - r_wet=current_r_m, - mass_of_dry_air=1e5 * si.kg, - initial_water_vapour_mixing_ratio=iwvmr, - pcloud=pcloud, - Zcloud=Zcloud, - Tcloud=Tcloud, - formulae=formulae, - ) - 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: # Non-physical radius - simulated_m_frac_evap_point = 1.0 # 1.0 means fully evaporated - else: - simulated_m_frac_evap_point = np.nan - else: - final_radius_m = final_radius_um * 1e-6 - if current_r_m == 0: - frac_evap = 1.0 - else: - frac_evap = 1.0 - (final_radius_m / current_r_m) ** 3 - frac_evap = np.clip(frac_evap, 0.0, 1.0) - simulated_m_frac_evap_point = frac_evap - else: - simulated_m_frac_evap_point = np.nan - except Exception as e: - print( - f"Warning: Simulation run failed for RH={current_rh}, r={current_r_m} (sample idx {i_rh},{j_r}): {e}." - ) - if np.isclose(expected_m_frac_evap, 1.0, atol=1e-6): - simulated_m_frac_evap_point = 1.0 + if np.isnan(r_m) or r_m <= 0: + pytest.fail(f"Invalid radius r_m={r_m} for sample idx {i_rh},{j_r}.") + settings = Settings( + planet=planet, + r_wet=r_m, + mass_of_dry_air=1e5 * si.kg, + initial_water_vapour_mixing_ratio=iwvmr, + pcloud=pcloud, + Zcloud=Zcloud, + Tcloud=Tcloud, + formulae=formulae, + ) + 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 r_m == 0: + frac_evap = 1.0 else: - simulated_m_frac_evap_point = np.nan - - return simulated_m_frac_evap_point + frac_evap = 1.0 - (final_radius_m / r_m) ** 3 + return np.clip(frac_evap, 0.0, 1.0) + return np.nan + except Exception as e: + warnings.warn( + f"Simulation run failed for RH={rh:.4f}, r={r_m:.3e} (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 From f55d91ae0e97238920ab402061a9f6e9e2c1466c Mon Sep 17 00:00:00 2001 From: Mateusz Mazur Date: Thu, 19 Jun 2025 22:23:36 +0200 Subject: [PATCH 07/19] refactor: streamlined dz_dt computation in Parcel class and updated planetary pressure units --- PySDM/environments/parcel.py | 5 ++- .../Loftus_and_Wordsworth_2021/parcel.py | 37 +------------------ .../Loftus_and_Wordsworth_2021/planet.py | 21 +++++++---- 3 files changed, 20 insertions(+), 43 deletions(-) diff --git a/PySDM/environments/parcel.py b/PySDM/environments/parcel.py index 59e65c54bc..a6adc96e04 100644 --- a/PySDM/environments/parcel.py +++ b/PySDM/environments/parcel.py @@ -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): + self.w((self.particulator.n_steps + 1 / 2) * dt) # "mid-point" + def get_thd(self): return self["thd"] diff --git a/examples/PySDM_examples/Loftus_and_Wordsworth_2021/parcel.py b/examples/PySDM_examples/Loftus_and_Wordsworth_2021/parcel.py index 7ac5eb0dac..3223e23772 100644 --- a/examples/PySDM_examples/Loftus_and_Wordsworth_2021/parcel.py +++ b/examples/PySDM_examples/Loftus_and_Wordsworth_2021/parcel.py @@ -25,38 +25,5 @@ def __init__( variables=None, ) - def advance_parcel_vars(self): - """ - Compute new values of displacement, dry-air density and volume, - and write them to self._tmp and self.mesh.dv - """ - dt = self.particulator.dt - formulae = self.particulator.formulae - T = self["T"][0] - p = self["p"][0] - - dz_dt = -self.particulator.attributes["terminal velocity"].to_ndarray()[0] - water_vapour_mixing_ratio = ( - self["water_vapour_mixing_ratio"][0] - - self.delta_liquid_water_mixing_ratio / 2 - ) - - drho_dz = formulae.hydrostatics.drho_dz( - p=p, - T=T, - water_vapour_mixing_ratio=water_vapour_mixing_ratio, - lv=formulae.latent_heat_vapourisation.lv(T), - d_liquid_water_mixing_ratio__dz=( - self.delta_liquid_water_mixing_ratio / dz_dt / dt - ), - ) - drhod_dz = drho_dz - - self.particulator.backend.explicit_euler(self._tmp["z"], dt, dz_dt) - self.particulator.backend.explicit_euler( - self._tmp["rhod"], dt, dz_dt * drhod_dz - ) - - self.mesh.dv = formulae.trivia.volume_of_density_mass( - (self._tmp["rhod"][0] + self["rhod"][0]) / 2, self.mass_of_dry_air - ) + def _compute_dz_dt(self, dt): + return -self.particulator.attributes["terminal velocity"].to_ndarray()[0] \ No newline at end of file diff --git a/examples/PySDM_examples/Loftus_and_Wordsworth_2021/planet.py b/examples/PySDM_examples/Loftus_and_Wordsworth_2021/planet.py index 0e4211b397..fbffa399e8 100644 --- a/examples/PySDM_examples/Loftus_and_Wordsworth_2021/planet.py +++ b/examples/PySDM_examples/Loftus_and_Wordsworth_2021/planet.py @@ -1,3 +1,10 @@ +""" +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 PySDM.physics.constants import si from dataclasses import dataclass from typing import Optional, Dict, Any @@ -24,7 +31,7 @@ def to_dict(self) -> Dict[str, Any]: class EarthLike(Planet): g_std: float = 9.82 * si.metre / si.second**2 T_STP: float = 300 * si.kelvin - p_STP: float = 1.01325 * 1e6 * si.pascal + 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 @@ -38,7 +45,7 @@ class EarthLike(Planet): class Earth(Planet): g_std: float = 9.82 * si.metre / si.second**2 T_STP: float = 290 * si.kelvin - p_STP: float = 1.01325 * 1e6 * si.pascal + 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 @@ -52,7 +59,7 @@ class Earth(Planet): class EarlyMars(Planet): g_std: float = 3.71 * si.metre / si.second**2 T_STP: float = 290 * si.kelvin - p_STP: float = 2 * 1e6 * si.pascal + p_STP: float = 2 * 1e5 * si.Pa RH_zref: float = 0.75 dry_molar_conc_H2: float = 0 dry_molar_conc_He: float = 0 @@ -66,7 +73,7 @@ class EarlyMars(Planet): class Jupiter(Planet): g_std: float = 24.84 * si.metre / si.second**2 T_STP: float = 274 * si.kelvin - p_STP: float = 4.85 * 1e6 * si.pascal + 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 @@ -80,7 +87,7 @@ class Jupiter(Planet): class Saturn(Planet): g_std: float = 10.47 * si.metre / si.second**2 T_STP: float = 284 * si.kelvin - p_STP: float = 10.4 * 1e6 * si.pascal + 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 @@ -94,7 +101,7 @@ class Saturn(Planet): 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 * 1e6 * si.pascal + 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 @@ -108,7 +115,7 @@ class K2_18B(Planet): class CompositeTest(Planet): g_std: float = 9.82 * si.metre / si.second**2 T_STP: float = 275 * si.kelvin - p_STP: float = 0.75 * 1e6 * si.pascal + 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 From bb15488e5504aa40779d462029ec045417b91113 Mon Sep 17 00:00:00 2001 From: Mateusz Mazur Date: Thu, 19 Jun 2025 22:57:13 +0200 Subject: [PATCH 08/19] style: improved code formatting and readability --- .../Loftus_and_Wordsworth_2021/parcel.py | 2 +- .../ground_truth/gen_figure.py | 12 +++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/examples/PySDM_examples/Loftus_and_Wordsworth_2021/parcel.py b/examples/PySDM_examples/Loftus_and_Wordsworth_2021/parcel.py index 3223e23772..56380c5750 100644 --- a/examples/PySDM_examples/Loftus_and_Wordsworth_2021/parcel.py +++ b/examples/PySDM_examples/Loftus_and_Wordsworth_2021/parcel.py @@ -26,4 +26,4 @@ def __init__( ) def _compute_dz_dt(self, dt): - return -self.particulator.attributes["terminal velocity"].to_ndarray()[0] \ No newline at end of file + return -self.particulator.attributes["terminal velocity"].to_ndarray()[0] 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 index ff8d780ac4..67626bb2e9 100644 --- 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 @@ -67,9 +67,15 @@ 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].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), From 3f4f9a873322eed60957b5a8ccfa409ac2df3e92 Mon Sep 17 00:00:00 2001 From: lursz Date: Fri, 20 Jun 2025 09:46:40 +0200 Subject: [PATCH 09/19] =?UTF-8?q?=F0=9F=94=A7=20PR=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduced const for MIN DROPLET RADIUS --- .../Loftus_and_Wordsworth_2021/simulation.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/examples/PySDM_examples/Loftus_and_Wordsworth_2021/simulation.py b/examples/PySDM_examples/Loftus_and_Wordsworth_2021/simulation.py index 071dde8bf2..11062d54b2 100644 --- a/examples/PySDM_examples/Loftus_and_Wordsworth_2021/simulation.py +++ b/examples/PySDM_examples/Loftus_and_Wordsworth_2021/simulation.py @@ -7,6 +7,8 @@ from PySDM.physics import constants as const from PySDM_examples.Loftus_and_Wordsworth_2021.parcel import AlienParcel +MIN_DROPLET_RADIUS = 1e-6 + class Simulation: def __init__(self, settings, backend=CPU): @@ -75,7 +77,10 @@ def run(self): } self.save(output) - while self.particulator.environment["z"][0] > 0 and output["r"][-1] > 1e-6: + while ( + self.particulator.environment["z"][0] > 0 + and output["r"][-1] > MIN_DROPLET_RADIUS + ): self.particulator.run(1) self.save(output) From 7354f937390f6d38c14a2d675089899634c574b4 Mon Sep 17 00:00:00 2001 From: Piotr Kubala Date: Mon, 23 Jun 2025 22:06:40 +0200 Subject: [PATCH 10/19] some linter fixes --- .../accuracy_test.py | 28 +++++++---- .../ground_truth/gen_figure.py | 4 +- .../Loftus_and_Wordsworth_2021/unit_test.py | 47 ++++++++++++------- 3 files changed, 50 insertions(+), 29 deletions(-) diff --git a/tests/examples_tests/Loftus_and_Wordsworth_2021/accuracy_test.py b/tests/examples_tests/Loftus_and_Wordsworth_2021/accuracy_test.py index 6f107c8374..24e3a3c1b3 100644 --- a/tests/examples_tests/Loftus_and_Wordsworth_2021/accuracy_test.py +++ b/tests/examples_tests/Loftus_and_Wordsworth_2021/accuracy_test.py @@ -1,12 +1,14 @@ +# pylint: disable=missing-module-docstring from __future__ import annotations -import pytest import os -import numpy as np -from scipy.optimize import fsolve import warnings from typing import Tuple, List, Generator +import pytest +import numpy as np +from scipy.optimize import fsolve + from PySDM import Formulae from PySDM.physics import si from PySDM_examples.Loftus_and_Wordsworth_2021 import Settings, Simulation @@ -32,8 +34,9 @@ def __enter__(self): return self except FileNotFoundError as e: pytest.fail(f"Error loading ground truth files: {e}") - except Exception as e: - pytest.fail(f"Unexpected error loading ground truth data: {e}") + pytest.fail("Ground truth data not loaded successfully.") + + return self def __exit__(self, exc_type, exc_val, exc_tb): pass @@ -123,12 +126,14 @@ def solve_Tcloud(T_candidate): th_std = formulae_instance.trivia.th_std(planet.p_STP, planet.T_STP) - pcloud = formulae_instance.hydrostatics.p_of_z_assuming_const_th_and_initial_water_vapour_mixing_ratio( + pcloud = \ + formulae_instance.hydrostatics\ + .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, static_arrays): + def test_figure_2_replication_accuracy(self, ground_truth_sample): formulae = Formulae( ventilation="PruppacherAndRasmussen1979", saturation_vapour_pressure="AugustRocheMagnus", @@ -142,7 +147,8 @@ def test_figure_2_replication_accuracy(self, ground_truth_sample, static_arrays) i_rh = sample["i_rh"] j_r = sample["j_r"] try: - iwvmr, Tcloud, Zcloud, pcloud = self._calculate_cloud_properties( + iwvmr, Tcloud, Zcloud, pcloud = \ + self._calculate_cloud_properties( planet, rh, formulae ) simulated = self.calc_simulated_m_frac_evap_point( @@ -160,7 +166,8 @@ def test_figure_2_replication_accuracy(self, ground_truth_sample, static_arrays) ) except Exception as e: pytest.fail( - f"Error in _calculate_cloud_properties for RH={rh} (sample idx {i_rh},{j_r}): {e}." + f"Error in _calculate_cloud_properties for RH={rh} " + + f"(sample idx {i_rh},{j_r}): {e}." ) error_context = ( f"Sample (RH_idx={i_rh}, R_idx={j_r}), RH={rh:.4f}, R_m={r_m:.3e}. " @@ -233,7 +240,8 @@ def calc_simulated_m_frac_evap_point( return np.nan except Exception as e: warnings.warn( - f"Simulation run failed for RH={rh:.4f}, r={r_m:.3e} (sample idx {i_rh},{j_r}): {type(e).__name__}: {e}" + f"Simulation run failed for RH={rh:.4f}, r={r_m:.3e} " +\ + f"(sample idx {i_rh},{j_r}): {type(e).__name__}: {e}" ) if np.isclose(expected, 1.0, atol=1e-6): return 1.0 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 index 67626bb2e9..cebc9301e4 100644 --- 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 @@ -67,9 +67,7 @@ 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].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="//" ) diff --git a/tests/examples_tests/Loftus_and_Wordsworth_2021/unit_test.py b/tests/examples_tests/Loftus_and_Wordsworth_2021/unit_test.py index d5b6425e39..b19a0b912b 100644 --- a/tests/examples_tests/Loftus_and_Wordsworth_2021/unit_test.py +++ b/tests/examples_tests/Loftus_and_Wordsworth_2021/unit_test.py @@ -1,3 +1,5 @@ +# pylint: disable=missing-module-docstring +from functools import partial from contextlib import contextmanager from PySDM import Formulae from PySDM.physics import si @@ -54,7 +56,8 @@ def test_planet_classes(self): ) assert ( total_conc <= 1.01 - ), f"Total molar concentration {total_conc} exceeds 1.01 for {planet.__class__.__name__}" + ), \ + f"Total molar concentration {total_conc} exceeds 1.01 for {planet.__class__.__name__}" def test_water_vapour_mixing_ratio_calculation(self): """Test water vapour mixing ratio calculation.""" @@ -100,7 +103,9 @@ def test_simulation_class( Zcloud_val, Tcloud_val, ): - """Test Simulation class initialization and basic functionality with parametrized settings.""" + """ + Test Simulation class initialization and basic functionality with parametrized settings. + """ with self._get_test_resources() as (formulae, earth_like): planet = earth_like @@ -178,7 +183,7 @@ def test_simulation_run_basic( lengths = [len(output[key]) for key in output.keys()] assert all( - l == lengths[0] for l in lengths + length == lengths[0] for length in lengths ), "Not all output arrays have the same length" def test_saturation_at_cloud_base(self): @@ -189,7 +194,6 @@ def test_saturation_at_cloud_base(self): ) new_Earth = EarthLike() - new_Earth.T_STP RH_array = np.linspace(0.25, 0.99, 50) const = formulae.constants @@ -197,7 +201,16 @@ def test_saturation_at_cloud_base(self): def mix(dry, vap, ratio): return (dry + ratio * vap) / (1 + ratio) - for i, RH in enumerate(RH_array[::-1]): + def f(x, water_mixing_ratio, p_stp, t_stp, c_p, Rair): + return water_mixing_ratio / ( + water_mixing_ratio + const.eps + ) * p_stp * (x / t_stp) ** ( + c_p / Rair + ) - formulae.saturation_vapour_pressure.pvs_water( + x + ) + + for RH in RH_array[::-1]: new_Earth.RH_zref = RH pvs = formulae.saturation_vapour_pressure.pvs_water(new_Earth.T_STP) @@ -208,21 +221,23 @@ def mix(dry, vap, ratio): Rair = mix(const.Rd, const.Rv, initial_water_vapour_mixing_ratio) c_p = mix(const.c_pd, const.c_pv, initial_water_vapour_mixing_ratio) - def f(x): - return initial_water_vapour_mixing_ratio / ( - initial_water_vapour_mixing_ratio + const.eps - ) * new_Earth.p_STP * (x / new_Earth.T_STP) ** ( - c_p / Rair - ) - formulae.saturation_vapour_pressure.pvs_water( - x - ) - - tdews = fsolve(f, [150, 300]) + tdews = fsolve( + partial( + f, + water_mixing_ratio=initial_water_vapour_mixing_ratio, + p_stp=new_Earth.p_STP, + t_stp=new_Earth.T_STP, + c_p=c_p, + Rair=Rair, + ), + [150, 300] + ) Tcloud = np.max(tdews) Zcloud = (new_Earth.T_STP - Tcloud) * c_p / new_Earth.g_std thstd = formulae.trivia.th_std(new_Earth.p_STP, new_Earth.T_STP) - pcloud = formulae.hydrostatics.p_of_z_assuming_const_th_and_initial_water_vapour_mixing_ratio( + pcloud = formulae.hydrostatics\ + .p_of_z_assuming_const_th_and_initial_water_vapour_mixing_ratio( new_Earth.p_STP, thstd, initial_water_vapour_mixing_ratio, Zcloud ) From 1d29d0c4f7954d94269c9686b53cf610595c7e8e Mon Sep 17 00:00:00 2001 From: Piotr Kubala Date: Mon, 23 Jun 2025 22:19:23 +0200 Subject: [PATCH 11/19] accuracy test linter --- .../Loftus_and_Wordsworth_2021/accuracy_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/examples_tests/Loftus_and_Wordsworth_2021/accuracy_test.py b/tests/examples_tests/Loftus_and_Wordsworth_2021/accuracy_test.py index 24e3a3c1b3..ecfe58535b 100644 --- a/tests/examples_tests/Loftus_and_Wordsworth_2021/accuracy_test.py +++ b/tests/examples_tests/Loftus_and_Wordsworth_2021/accuracy_test.py @@ -164,7 +164,7 @@ def test_figure_2_replication_accuracy(self, ground_truth_sample): Zcloud, pcloud, ) - except Exception as e: + except Exception as e: # pylint: disable=broad-except pytest.fail( f"Error in _calculate_cloud_properties for RH={rh} " + f"(sample idx {i_rh},{j_r}): {e}." @@ -238,7 +238,7 @@ def calc_simulated_m_frac_evap_point( frac_evap = 1.0 - (final_radius_m / r_m) ** 3 return np.clip(frac_evap, 0.0, 1.0) return np.nan - except Exception as e: + except Exception as e: # pylint: disable=broad-except warnings.warn( f"Simulation run failed for RH={rh:.4f}, r={r_m:.3e} " +\ f"(sample idx {i_rh},{j_r}): {type(e).__name__}: {e}" From 70464d613883e2197f18ac65465a01b7ba6af836 Mon Sep 17 00:00:00 2001 From: Piotr Kubala Date: Mon, 23 Jun 2025 22:59:22 +0200 Subject: [PATCH 12/19] accuracy linter fixed --- .../accuracy_test.py | 123 +++++++++--------- 1 file changed, 61 insertions(+), 62 deletions(-) diff --git a/tests/examples_tests/Loftus_and_Wordsworth_2021/accuracy_test.py b/tests/examples_tests/Loftus_and_Wordsworth_2021/accuracy_test.py index ecfe58535b..1824a04002 100644 --- a/tests/examples_tests/Loftus_and_Wordsworth_2021/accuracy_test.py +++ b/tests/examples_tests/Loftus_and_Wordsworth_2021/accuracy_test.py @@ -42,6 +42,7 @@ 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)) @@ -52,6 +53,7 @@ def ground_truth_data(request) -> Generator[GroundTruthLoader, None, None]: yield gt +# pylint: disable=redefined-outer-name @pytest.fixture def ground_truth_sample(ground_truth_data: GroundTruthLoader) -> List[dict]: gt = ground_truth_data @@ -78,6 +80,7 @@ def ground_truth_sample(ground_truth_data: GroundTruthLoader) -> List[dict]: ] +# pylint: disable=redefined-outer-name @pytest.fixture(scope="module") def static_arrays() -> Tuple[np.ndarray, np.ndarray, np.ndarray, object]: formulae = Formulae( @@ -141,80 +144,76 @@ def test_figure_2_replication_accuracy(self, ground_truth_sample): ) for sample in ground_truth_sample: planet = EarthLike() - rh = sample["rh"] - r_m = sample["r_m"] - expected = sample["expected_m_frac_evap"] - i_rh = sample["i_rh"] - j_r = sample["j_r"] try: iwvmr, Tcloud, Zcloud, pcloud = \ self._calculate_cloud_properties( - planet, rh, formulae + planet, sample["rh"], formulae ) - simulated = self.calc_simulated_m_frac_evap_point( - planet, - formulae, - i_rh, - j_r, - rh, - r_m, - expected, - iwvmr, - Tcloud, - Zcloud, - pcloud, + 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 Exception as e: # pylint: disable=broad-except pytest.fail( - f"Error in _calculate_cloud_properties for RH={rh} " + - f"(sample idx {i_rh},{j_r}): {e}." - ) - error_context = ( - f"Sample (RH_idx={i_rh}, R_idx={j_r}), RH={rh:.4f}, R_m={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}", + 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( - self, - planet, - formulae, i_rh, j_r, rh, - r_m, expected, - iwvmr, - Tcloud, - Zcloud, - pcloud, + settings ): - if np.isnan(r_m) or r_m <= 0: - pytest.fail(f"Invalid radius r_m={r_m} for sample idx {i_rh},{j_r}.") - settings = Settings( - planet=planet, - r_wet=r_m, - mass_of_dry_air=1e5 * si.kg, - initial_water_vapour_mixing_ratio=iwvmr, - pcloud=pcloud, - Zcloud=Zcloud, - Tcloud=Tcloud, - formulae=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, + # ) + + 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() @@ -232,15 +231,15 @@ def calc_simulated_m_frac_evap_point( return 1.0 return np.nan final_radius_m = final_radius_um * 1e-6 - if r_m == 0: + if settings.r_wet == 0: frac_evap = 1.0 else: - frac_evap = 1.0 - (final_radius_m / r_m) ** 3 + 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={r_m:.3e} " +\ + 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): From 736038cc37d8f8856faba90e0090e3c4974edc9b Mon Sep 17 00:00:00 2001 From: Piotr Kubala Date: Mon, 23 Jun 2025 23:19:47 +0200 Subject: [PATCH 13/19] linter fixed --- .../accuracy_test.py | 13 +------ .../Loftus_and_Wordsworth_2021/unit_test.py | 37 ++++++++++++------- 2 files changed, 24 insertions(+), 26 deletions(-) diff --git a/tests/examples_tests/Loftus_and_Wordsworth_2021/accuracy_test.py b/tests/examples_tests/Loftus_and_Wordsworth_2021/accuracy_test.py index 1824a04002..d498b264b7 100644 --- a/tests/examples_tests/Loftus_and_Wordsworth_2021/accuracy_test.py +++ b/tests/examples_tests/Loftus_and_Wordsworth_2021/accuracy_test.py @@ -187,7 +187,7 @@ def test_figure_2_replication_accuracy(self, ground_truth_sample): atol=1e-1, err_msg=f"Value Mismatch. {error_context}", ) - except Exception as e: # pylint: disable=broad-except + 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}." @@ -201,17 +201,6 @@ def calc_simulated_m_frac_evap_point( expected, settings ): - # 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, - # ) - 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) diff --git a/tests/examples_tests/Loftus_and_Wordsworth_2021/unit_test.py b/tests/examples_tests/Loftus_and_Wordsworth_2021/unit_test.py index b19a0b912b..8d611f96ea 100644 --- a/tests/examples_tests/Loftus_and_Wordsworth_2021/unit_test.py +++ b/tests/examples_tests/Loftus_and_Wordsworth_2021/unit_test.py @@ -1,4 +1,6 @@ # pylint: disable=missing-module-docstring + +from collections import namedtuple from functools import partial from contextlib import contextmanager from PySDM import Formulae @@ -23,7 +25,8 @@ class TestLoftusWordsworth2021: @contextmanager - def _get_test_resources(self): + @staticmethod + def _get_test_resources(): formulae = Formulae( ventilation="PruppacherAndRasmussen1979", saturation_vapour_pressure="AugustRocheMagnus", @@ -61,7 +64,7 @@ def test_planet_classes(self): def test_water_vapour_mixing_ratio_calculation(self): """Test water vapour mixing ratio calculation.""" - with self._get_test_resources() as (formulae, earth_like): + with TestLoftusWordsworth2021._get_test_resources() as (formulae, earth_like): const = formulae.constants planet = earth_like @@ -86,6 +89,7 @@ def test_alien_parcel_initialization(self): ) assert parcel is not None + # pylint: disable=too-many-arguments,too-many-positional-arguments @pytest.mark.parametrize( "r_wet_val, mass_of_dry_air_val, iwvmr_val, pcloud_val, Zcloud_val, Tcloud_val", [ @@ -106,7 +110,7 @@ def test_simulation_class( """ Test Simulation class initialization and basic functionality with parametrized settings. """ - with self._get_test_resources() as (formulae, earth_like): + with TestLoftusWordsworth2021._get_test_resources() as (formulae, earth_like): planet = earth_like settings = Settings( @@ -131,6 +135,7 @@ def test_simulation_class( for product in required_products: assert product in products + # pylint: disable=too-many-arguments,too-many-positional-arguments @pytest.mark.parametrize( "r_wet_val, mass_of_dry_air_val, iwvmr_val, pcloud_val, Zcloud_val, Tcloud_val", [ @@ -149,7 +154,7 @@ def test_simulation_run_basic( Tcloud_val, ): """Test basic simulation run functionality.""" - with self._get_test_resources() as (formulae, earth_like): + with TestLoftusWordsworth2021._get_test_resources() as (formulae, earth_like): planet = earth_like settings = Settings( @@ -201,11 +206,11 @@ def test_saturation_at_cloud_base(self): def mix(dry, vap, ratio): return (dry + ratio * vap) / (1 + ratio) - def f(x, water_mixing_ratio, p_stp, t_stp, c_p, Rair): + def f(x, water_mixing_ratio, params): return water_mixing_ratio / ( water_mixing_ratio + const.eps - ) * p_stp * (x / t_stp) ** ( - c_p / Rair + ) * params.p_stp * (x / params.t_stp) ** ( + params.c_p / params.Rair ) - formulae.saturation_vapour_pressure.pvs_water( x ) @@ -213,22 +218,26 @@ def f(x, water_mixing_ratio, p_stp, t_stp, c_p, Rair): for RH in RH_array[::-1]: new_Earth.RH_zref = RH - pvs = formulae.saturation_vapour_pressure.pvs_water(new_Earth.T_STP) initial_water_vapour_mixing_ratio = const.eps / ( - new_Earth.p_STP / new_Earth.RH_zref / pvs - 1 + new_Earth.p_STP / new_Earth.RH_zref / \ + formulae.saturation_vapour_pressure.pvs_water(new_Earth.T_STP) - 1 ) - Rair = mix(const.Rd, const.Rv, initial_water_vapour_mixing_ratio) 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, - p_stp=new_Earth.p_STP, - t_stp=new_Earth.T_STP, - c_p=c_p, - Rair=Rair, + 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] ) From 217919ede4e1bbdf0abf523c4ea37d33f8d901c9 Mon Sep 17 00:00:00 2001 From: Piotr Kubala Date: Mon, 23 Jun 2025 23:23:59 +0200 Subject: [PATCH 14/19] pre-commit run --- .../accuracy_test.py | 35 ++++++++----------- .../ground_truth/gen_figure.py | 4 ++- .../Loftus_and_Wordsworth_2021/unit_test.py | 16 ++++----- 3 files changed, 25 insertions(+), 30 deletions(-) diff --git a/tests/examples_tests/Loftus_and_Wordsworth_2021/accuracy_test.py b/tests/examples_tests/Loftus_and_Wordsworth_2021/accuracy_test.py index d498b264b7..fc0cb54817 100644 --- a/tests/examples_tests/Loftus_and_Wordsworth_2021/accuracy_test.py +++ b/tests/examples_tests/Loftus_and_Wordsworth_2021/accuracy_test.py @@ -129,9 +129,7 @@ def solve_Tcloud(T_candidate): th_std = formulae_instance.trivia.th_std(planet.p_STP, planet.T_STP) - pcloud = \ - formulae_instance.hydrostatics\ - .p_of_z_assuming_const_th_and_initial_water_vapour_mixing_ratio( + pcloud = formulae_instance.hydrostatics.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 @@ -145,8 +143,7 @@ def test_figure_2_replication_accuracy(self, ground_truth_sample): for sample in ground_truth_sample: planet = EarthLike() try: - iwvmr, Tcloud, Zcloud, pcloud = \ - self._calculate_cloud_properties( + iwvmr, Tcloud, Zcloud, pcloud = self._calculate_cloud_properties( planet, sample["rh"], formulae ) settings = Settings( @@ -164,12 +161,12 @@ def test_figure_2_replication_accuracy(self, ground_truth_sample): sample["j_r"], sample["rh"], sample["expected_m_frac_evap"], - settings + 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"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): @@ -189,20 +186,16 @@ def test_figure_2_replication_accuracy(self, ground_truth_sample): ) 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}." + 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 - ): + 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}.") + pytest.fail( + f"Invalid radius r_m={settings.r_wet} for sample idx {i_rh},{j_r}." + ) simulation = Simulation(settings) try: output = simulation.run() @@ -226,10 +219,10 @@ def calc_simulated_m_frac_evap_point( 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 + 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}" + 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 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 index cebc9301e4..67626bb2e9 100644 --- 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 @@ -67,7 +67,9 @@ 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].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="//" ) diff --git a/tests/examples_tests/Loftus_and_Wordsworth_2021/unit_test.py b/tests/examples_tests/Loftus_and_Wordsworth_2021/unit_test.py index 8d611f96ea..911122af28 100644 --- a/tests/examples_tests/Loftus_and_Wordsworth_2021/unit_test.py +++ b/tests/examples_tests/Loftus_and_Wordsworth_2021/unit_test.py @@ -59,8 +59,7 @@ def test_planet_classes(self): ) assert ( total_conc <= 1.01 - ), \ - f"Total molar concentration {total_conc} exceeds 1.01 for {planet.__class__.__name__}" + ), f"Total molar concentration {total_conc} exceeds 1.01 for {planet.__class__.__name__}" def test_water_vapour_mixing_ratio_calculation(self): """Test water vapour mixing ratio calculation.""" @@ -219,8 +218,10 @@ def f(x, water_mixing_ratio, params): 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 + 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) @@ -237,16 +238,15 @@ def f(x, water_mixing_ratio, params): t_stp=new_Earth.T_STP, c_p=c_p, Rair=mix(const.Rd, const.Rv, initial_water_vapour_mixing_ratio), - ) + ), ), - [150, 300] + [150, 300], ) Tcloud = np.max(tdews) Zcloud = (new_Earth.T_STP - Tcloud) * c_p / new_Earth.g_std thstd = formulae.trivia.th_std(new_Earth.p_STP, new_Earth.T_STP) - pcloud = formulae.hydrostatics\ - .p_of_z_assuming_const_th_and_initial_water_vapour_mixing_ratio( + pcloud = formulae.hydrostatics.p_of_z_assuming_const_th_and_initial_water_vapour_mixing_ratio( new_Earth.p_STP, thstd, initial_water_vapour_mixing_ratio, Zcloud ) From b8f819d996314304b5faea6c7478f84bbfa43bc1 Mon Sep 17 00:00:00 2001 From: Piotr Kubala Date: Tue, 24 Jun 2025 01:48:07 +0200 Subject: [PATCH 15/19] linter other fixes --- docs/bibliography.json | 7 +++++ .../accuracy_test.py | 11 +++---- .../ground_truth/gen_figure.py | 4 ++- .../Loftus_and_Wordsworth_2021/unit_test.py | 29 ++++++++++++------- 4 files changed, 34 insertions(+), 17 deletions(-) diff --git a/docs/bibliography.json b/docs/bibliography.json index 44b4be2186..baf49dae51 100644 --- a/docs/bibliography.json +++ b/docs/bibliography.json @@ -925,5 +925,12 @@ ], "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": [ + "PySDM/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/tests/examples_tests/Loftus_and_Wordsworth_2021/accuracy_test.py b/tests/examples_tests/Loftus_and_Wordsworth_2021/accuracy_test.py index fc0cb54817..f23f2fc4f5 100644 --- a/tests/examples_tests/Loftus_and_Wordsworth_2021/accuracy_test.py +++ b/tests/examples_tests/Loftus_and_Wordsworth_2021/accuracy_test.py @@ -9,11 +9,12 @@ import numpy as np from scipy.optimize import fsolve -from PySDM import Formulae -from PySDM.physics import si 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__( @@ -122,14 +123,14 @@ def solve_Tcloud(T_candidate): pvs_tc = formulae_instance.saturation_vapour_pressure.pvs_water(T_candidate) return pv_ad - pvs_tc - Tcloud_solutions = fsolve(solve_Tcloud, [150.0, 300.0]) - Tcloud = np.max(Tcloud_solutions) + 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) - pcloud = formulae_instance.hydrostatics.p_of_z_assuming_const_th_and_initial_water_vapour_mixing_ratio( + 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 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 index 67626bb2e9..fd63165b9d 100644 --- 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 @@ -1,10 +1,12 @@ +# 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 -import os # load results root_dir = os.path.dirname(os.path.abspath(__file__)) diff --git a/tests/examples_tests/Loftus_and_Wordsworth_2021/unit_test.py b/tests/examples_tests/Loftus_and_Wordsworth_2021/unit_test.py index 911122af28..a15d1374b5 100644 --- a/tests/examples_tests/Loftus_and_Wordsworth_2021/unit_test.py +++ b/tests/examples_tests/Loftus_and_Wordsworth_2021/unit_test.py @@ -3,8 +3,6 @@ from collections import namedtuple from functools import partial from contextlib import contextmanager -from PySDM import Formulae -from PySDM.physics import si import pytest import numpy as np from scipy.optimize import fsolve @@ -21,6 +19,9 @@ 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: @@ -57,9 +58,10 @@ def test_planet_classes(self): + planet.dry_molar_conc_O2 + planet.dry_molar_conc_CO2 ) - assert ( - total_conc <= 1.01 - ), f"Total molar concentration {total_conc} exceeds 1.01 for {planet.__class__.__name__}" + 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.""" @@ -88,7 +90,7 @@ def test_alien_parcel_initialization(self): ) assert parcel is not None - # pylint: disable=too-many-arguments,too-many-positional-arguments + # pylint: disable=too-many-arguments @pytest.mark.parametrize( "r_wet_val, mass_of_dry_air_val, iwvmr_val, pcloud_val, Zcloud_val, Tcloud_val", [ @@ -134,7 +136,7 @@ def test_simulation_class( for product in required_products: assert product in products - # pylint: disable=too-many-arguments,too-many-positional-arguments + # pylint: disable=too-many-arguments @pytest.mark.parametrize( "r_wet_val, mass_of_dry_air_val, iwvmr_val, pcloud_val, Zcloud_val, Tcloud_val", [ @@ -185,7 +187,7 @@ def test_simulation_run_basic( assert len(output["z"]) > 0, "Output array 'z' is empty" assert len(output["t"]) > 0, "Output array 't' is empty" - lengths = [len(output[key]) for key in output.keys()] + 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" @@ -243,11 +245,16 @@ def f(x, water_mixing_ratio, params): [150, 300], ) Tcloud = np.max(tdews) - Zcloud = (new_Earth.T_STP - Tcloud) * c_p / new_Earth.g_std thstd = formulae.trivia.th_std(new_Earth.p_STP, new_Earth.T_STP) - pcloud = formulae.hydrostatics.p_of_z_assuming_const_th_and_initial_water_vapour_mixing_ratio( - new_Earth.p_STP, thstd, initial_water_vapour_mixing_ratio, Zcloud + 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( From e0874d5680186ba06eb28647f69c7ac82b94b41d Mon Sep 17 00:00:00 2001 From: Piotr Kubala Date: Tue, 24 Jun 2025 01:58:07 +0200 Subject: [PATCH 16/19] bibliography fix --- docs/bibliography.json | 2 +- .../Loftus_and_Wordsworth_2021/ground_truth/gen_figure.py | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/bibliography.json b/docs/bibliography.json index baf49dae51..8d15c97406 100644 --- a/docs/bibliography.json +++ b/docs/bibliography.json @@ -928,7 +928,7 @@ }, "https://doi.org/10.1029/2020JE006653": { "usages": [ - "PySDM/examples/PySDM_examples/Loftus_and_Wordsworth_2021/__init__.py" + "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/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 index fd63165b9d..89fec0af21 100644 --- 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 @@ -43,7 +43,7 @@ r0grid * 1e3, RHgrid, m_frac_evap, - cmap=plt.cm.binary, + cmap=plt.cm.binary, # pylint: disable=no-member vmin=0, vmax=1, levels=levels_smooth, @@ -95,7 +95,10 @@ 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") -os.mkdir(figs_path) if not os.path.exists(figs_path) else None + +if not os.path.exists(figs_path): + os.mkdir(figs_path) + plt.savefig( os.path.join(figs_path, "fig02.pdf"), transparent=True, From 4d0299aa4edbf711cbf28c6d29596f720da0cf60 Mon Sep 17 00:00:00 2001 From: Piotr Kubala Date: Tue, 24 Jun 2025 02:11:27 +0200 Subject: [PATCH 17/19] parcel small fix --- PySDM/environments/parcel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PySDM/environments/parcel.py b/PySDM/environments/parcel.py index a6adc96e04..fdff10de91 100644 --- a/PySDM/environments/parcel.py +++ b/PySDM/environments/parcel.py @@ -134,7 +134,7 @@ def advance_parcel_vars(self): ) def _compute_dz_dt(self, dt): - self.w((self.particulator.n_steps + 1 / 2) * dt) # "mid-point" + return self.w((self.particulator.n_steps + 1 / 2) * dt) # "mid-point" def get_thd(self): return self["thd"] From 7f114a7008e199eed368db4f3c1e2e8901deccad Mon Sep 17 00:00:00 2001 From: Piotr Kubala Date: Tue, 24 Jun 2025 02:29:15 +0200 Subject: [PATCH 18/19] linter fixes --- .../Loftus_and_Wordsworth_2021/__init__.py | 1 + .../Loftus_and_Wordsworth_2021/figure_2.ipynb | 4 +--- .../PySDM_examples/Loftus_and_Wordsworth_2021/parcel.py | 5 +++++ .../PySDM_examples/Loftus_and_Wordsworth_2021/planet.py | 9 ++++++--- .../Loftus_and_Wordsworth_2021/settings.py | 8 ++++++-- .../Loftus_and_Wordsworth_2021/simulation.py | 7 ++++++- 6 files changed, 25 insertions(+), 9 deletions(-) diff --git a/examples/PySDM_examples/Loftus_and_Wordsworth_2021/__init__.py b/examples/PySDM_examples/Loftus_and_Wordsworth_2021/__init__.py index 2d0f59fb30..2b6bbaec44 100644 --- a/examples/PySDM_examples/Loftus_and_Wordsworth_2021/__init__.py +++ b/examples/PySDM_examples/Loftus_and_Wordsworth_2021/__init__.py @@ -1,3 +1,4 @@ +# 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) diff --git a/examples/PySDM_examples/Loftus_and_Wordsworth_2021/figure_2.ipynb b/examples/PySDM_examples/Loftus_and_Wordsworth_2021/figure_2.ipynb index 6a27e3ffa6..3630950b03 100644 --- a/examples/PySDM_examples/Loftus_and_Wordsworth_2021/figure_2.ipynb +++ b/examples/PySDM_examples/Loftus_and_Wordsworth_2021/figure_2.ipynb @@ -114,9 +114,7 @@ " 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(\n", - " x\n", - " )\n", + " ) - formulae.saturation_vapour_pressure.pvs_water(x)\n", "\n", " tdews = fsolve(f, [150, 300])\n", " Tcloud = np.max(tdews)\n", diff --git a/examples/PySDM_examples/Loftus_and_Wordsworth_2021/parcel.py b/examples/PySDM_examples/Loftus_and_Wordsworth_2021/parcel.py index 56380c5750..6b1b2e30fb 100644 --- a/examples/PySDM_examples/Loftus_and_Wordsworth_2021/parcel.py +++ b/examples/PySDM_examples/Loftus_and_Wordsworth_2021/parcel.py @@ -1,3 +1,7 @@ +""" +Modified Parcel class for the Loftus and Wordsworth 2021 example. +""" + from PySDM.environments.parcel import Parcel @@ -25,5 +29,6 @@ def __init__( 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 index fbffa399e8..d360626619 100644 --- a/examples/PySDM_examples/Loftus_and_Wordsworth_2021/planet.py +++ b/examples/PySDM_examples/Loftus_and_Wordsworth_2021/planet.py @@ -1,14 +1,17 @@ """ 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. +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 PySDM.physics.constants import si from dataclasses import dataclass from typing import Optional, Dict, Any +from PySDM.physics.constants import si + @dataclass class Planet: diff --git a/examples/PySDM_examples/Loftus_and_Wordsworth_2021/settings.py b/examples/PySDM_examples/Loftus_and_Wordsworth_2021/settings.py index b0a81752a7..8f5d6f95ad 100644 --- a/examples/PySDM_examples/Loftus_and_Wordsworth_2021/settings.py +++ b/examples/PySDM_examples/Loftus_and_Wordsworth_2021/settings.py @@ -1,15 +1,19 @@ -# Planetary Properties, Loftus and Wordsworth 2021 Table 1 +""" +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 -from PySDM_examples.Loftus_and_Wordsworth_2021.planet import Planet @strict class Settings: + # pylint: disable=too-many-arguments def __init__( self, r_wet: float, diff --git a/examples/PySDM_examples/Loftus_and_Wordsworth_2021/simulation.py b/examples/PySDM_examples/Loftus_and_Wordsworth_2021/simulation.py index 11062d54b2..a182885e99 100644 --- a/examples/PySDM_examples/Loftus_and_Wordsworth_2021/simulation.py +++ b/examples/PySDM_examples/Loftus_and_Wordsworth_2021/simulation.py @@ -1,11 +1,16 @@ +""" +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 -from PySDM_examples.Loftus_and_Wordsworth_2021.parcel import AlienParcel MIN_DROPLET_RADIUS = 1e-6 From dba682dc335fcc133c2bfd90e98e596b9e6e3c9a Mon Sep 17 00:00:00 2001 From: Piotr Kubala Date: Tue, 24 Jun 2025 02:45:55 +0200 Subject: [PATCH 19/19] last linter --- .../Loftus_and_Wordsworth_2021/figure_2.ipynb | 38 ++++++++----------- 1 file changed, 15 insertions(+), 23 deletions(-) diff --git a/examples/PySDM_examples/Loftus_and_Wordsworth_2021/figure_2.ipynb b/examples/PySDM_examples/Loftus_and_Wordsworth_2021/figure_2.ipynb index 3630950b03..7fa0adb654 100644 --- a/examples/PySDM_examples/Loftus_and_Wordsworth_2021/figure_2.ipynb +++ b/examples/PySDM_examples/Loftus_and_Wordsworth_2021/figure_2.ipynb @@ -12,7 +12,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "id": "d8f644e9", "metadata": {}, "outputs": [], @@ -28,15 +28,7 @@ "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 (\n", - " Planet,\n", - " EarthLike,\n", - " Earth,\n", - " EarlyMars,\n", - " Jupiter,\n", - " Saturn,\n", - " K2_18B,\n", - ")\n", + "from PySDM_examples.Loftus_and_Wordsworth_2021.planet import EarthLike\n", "from PySDM_examples.Loftus_and_Wordsworth_2021 import Simulation" ] }, @@ -56,7 +48,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "id": "41f0ed6d", "metadata": {}, "outputs": [ @@ -73,12 +65,12 @@ ], "source": [ "new_Earth = EarthLike()\n", - "new_Earth.T_STP" + "print(new_Earth.T_STP)" ] }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "id": "d3a8f4b9", "metadata": {}, "outputs": [], @@ -94,12 +86,13 @@ " return (dry + ratio * vap) / (1 + ratio)\n", "\n", "\n", - "def compute_one_RH(i, RH):\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\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", @@ -155,28 +148,27 @@ " if output[\"z\"][-1] > 0:\n", " row_data[j] = np.nan\n", " break\n", - " else:\n", - " row_data[j] = 1 - (output[\"r\"][-1] / (r * 1e6))\n", - " except Exception as _:\n", + " row_data[j] = 1 - (output[\"r\"][-1] / (r * 1e6))\n", + " except Exception as _: # pylint: disable=broad-exception-caught\n", " break\n", "\n", - " return i, row_data, output" + " return index, row_data, output" ] }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "id": "4352de81", "metadata": {}, "outputs": [], "source": [ "all_rows = Parallel(n_jobs=os.cpu_count())(\n", - " delayed(compute_one_RH)(i, RH) for i, RH in enumerate(RH_array[::-1])\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 i, row_data, output in all_rows:\n", - " output_matrix[i] = row_data\n", + "for index, row_data, output in all_rows:\n", + " output_matrix[index] = row_data\n", " last_output = output" ] },