PySDM_examples.Luettmer_et_al_2026.simulation

  1import numpy as np
  2import PySDM.products as PySDM_products
  3from PySDM import Particulator
  4from PySDM.dynamics import (
  5    AmbientThermodynamics,
  6    Condensation,
  7    Freezing,
  8    VapourDepositionOnIce,
  9)
 10from PySDM.environments import Parcel
 11from PySDM.initialisation import discretise_multiplicities
 12from PySDM.initialisation.hygroscopic_equilibrium import equilibrate_dry_radii
 13
 14
 15class Simulation:
 16    def __init__(self, settings):
 17
 18        self.dt = settings.dt
 19
 20        formulae = settings.formulae
 21
 22        self.silent = settings.silent
 23
 24        env = Parcel(
 25            mixed_phase=True,
 26            dt=self.dt,
 27            mass_of_dry_air=settings.mass_of_dry_air,
 28            p0=settings.initial_pressure,
 29            initial_water_vapour_mixing_ratio=settings.initial_water_vapour_mixing_ratio,
 30            T0=settings.initial_temperature,
 31            w=settings.w_updraft,
 32            backend=settings.backend,
 33        )
 34
 35        self.n_sd = settings.n_sd
 36        self.multiplicities = discretise_multiplicities(
 37            settings.specific_concentration * env.mass_of_dry_air
 38        )
 39        self.r_wet = settings.r_wet
 40
 41        kappa = np.full_like(settings.r_wet, settings.kappa)
 42
 43        self.r_dry = equilibrate_dry_radii(
 44            r_wet=self.r_wet,
 45            environment=env,
 46            kappa=kappa,
 47        )
 48        v_dry = settings.formulae.trivia.volume(radius=self.r_dry)
 49        self.initial_mass = formulae.particle_shape_and_density.radius_to_mass(
 50            self.r_wet
 51        )
 52
 53        attributes = {
 54            "multiplicity": self.multiplicities,
 55            "dry volume": v_dry,
 56            "kappa times dry volume": kappa * v_dry,
 57            "signed water mass": self.initial_mass,
 58        }
 59
 60        products = (
 61            PySDM_products.ParcelDisplacement(name="z"),
 62            PySDM_products.Time(name="t"),
 63            PySDM_products.AmbientRelativeHumidity(name="RH", unit="%"),
 64            PySDM_products.AmbientRelativeHumidity(name="RH_ice", unit="%"),
 65            PySDM_products.AmbientTemperature(name="T"),
 66            PySDM_products.AmbientPressure(name="p", unit="hPa"),
 67            PySDM_products.WaterMixingRatio(name="LWC", radius_range=(0, np.inf)),
 68            PySDM_products.WaterMixingRatio(name="IWC", radius_range=(-np.inf, 0)),
 69            PySDM_products.AmbientWaterVapourMixingRatio(
 70                name="qv", var="water_vapour_mixing_ratio"
 71            ),
 72            PySDM_products.ParticleSpecificConcentration(
 73                name="ns", radius_range=(0, np.inf), unit="kg^-1"
 74            ),
 75            PySDM_products.ParticleSpecificConcentration(
 76                name="ni", radius_range=(-np.inf, 0), unit="kg^-1"
 77            ),
 78            PySDM_products.MeanRadius(name="rs", radius_range=(0, np.inf)),
 79            PySDM_products.MeanRadius(name="ri", radius_range=(-np.inf, 0)),
 80        )
 81        self.output_product_list = [
 82            "z",
 83            "t",
 84            "T",
 85            "p",
 86            "RH",
 87            "RH_ice",
 88            "LWC",
 89            "IWC",
 90            "qv",
 91            "ns",
 92            "ni",
 93            "rs",
 94            "ri",
 95        ]
 96
 97        self.particulator = Particulator(
 98            n_sd=settings.n_sd,
 99            environment=env,
100            dynamics=(
101                [
102                    AmbientThermodynamics(),
103                    Condensation(adaptive=True),
104                ]
105                + (
106                    [VapourDepositionOnIce(adaptive=True)]
107                    if settings.deposition_enable
108                    else []
109                )
110                + [
111                    Freezing(
112                        homogeneous_freezing=settings.hom_freezing_type,
113                        immersion_freezing=None,
114                    )
115                ]
116            ),
117            attributes=attributes,
118            products=products,
119            requested_attributes=(
120                "temperature of last freezing",
121                "supersaturation of last freezing",
122                "radius",
123                "wet to critical volume ratio",
124            ),
125        )
126
127        self.n_output = settings.n_output
128        if settings.n_output == 1:
129            self.n_substeps = 1
130        else:
131            self.n_substeps = int(self.n_output / self.dt)
132        self.t_max_duration = settings.t_max_duration
133
134    def save(self, output):
135        cell_id = 0
136
137        for key in self.output_product_list:
138            if key == "t":
139                output[key].append(self.particulator.products[key].get())
140            else:
141                output[key].append(self.particulator.products[key].get()[cell_id])
142
143        output["T_frz"] = self.particulator.attributes[
144            "temperature of last freezing"
145        ].data.tolist()
146        output["RHi_frz"] = self.particulator.attributes[
147            "supersaturation of last freezing"
148        ].data.tolist()
149        if not output["radius"]:
150            output["radius"] = self.particulator.attributes["radius"].data.tolist()
151            output["multiplicity"] = self.particulator.attributes[
152                "multiplicity"
153            ].data.tolist()
154
155    def run(self):
156
157        if not self.silent:
158            print("Starting simulation...")
159
160        output = {
161            "T_frz": [],
162            "RHi_frz": [],
163            "radius": [],
164            "multiplicity": [],
165        }
166        for key in self.output_product_list:
167            output[key] = []
168
169        self.save(output)
170
171        while True:
172
173            self.particulator.run(self.n_substeps)
174            self.save(output)
175
176            w_cr_v_ratio = self.particulator.attributes[
177                "wet to critical volume ratio"
178            ].data
179            sig_mass = self.particulator.attributes["signed water mass"].data
180            frozen = sig_mass < 0
181            unactivated = w_cr_v_ratio < 1
182            if any(frozen) and all(np.logical_or(frozen, unactivated)):
183                if not self.silent:
184                    print("all particles frozen or evaporated")
185                # Assert for water saturation
186                test_water_saturation = np.asarray(output["RH"])
187                # Sort out times before CCN activation & after first occurence of ice
188                test_water_saturation = np.where(
189                    np.asarray(output["rs"]) < 1e-6, 100.0, test_water_saturation
190                )
191                test_water_saturation = np.where(
192                    np.asarray(output["IWC"]) > 0.0, 100.0, test_water_saturation
193                )
194                if np.allclose(test_water_saturation, 100.0, rtol=5e-2) is False:
195                    print(
196                        "Warning: water saturation is too high outside "
197                        "of activation and mixed-phase environment"
198                    )
199
200                break
201            if output["t"][-1] >= self.t_max_duration * self.dt:
202                print("time exceeded")
203                break
204
205        return output
class Simulation:
 16class Simulation:
 17    def __init__(self, settings):
 18
 19        self.dt = settings.dt
 20
 21        formulae = settings.formulae
 22
 23        self.silent = settings.silent
 24
 25        env = Parcel(
 26            mixed_phase=True,
 27            dt=self.dt,
 28            mass_of_dry_air=settings.mass_of_dry_air,
 29            p0=settings.initial_pressure,
 30            initial_water_vapour_mixing_ratio=settings.initial_water_vapour_mixing_ratio,
 31            T0=settings.initial_temperature,
 32            w=settings.w_updraft,
 33            backend=settings.backend,
 34        )
 35
 36        self.n_sd = settings.n_sd
 37        self.multiplicities = discretise_multiplicities(
 38            settings.specific_concentration * env.mass_of_dry_air
 39        )
 40        self.r_wet = settings.r_wet
 41
 42        kappa = np.full_like(settings.r_wet, settings.kappa)
 43
 44        self.r_dry = equilibrate_dry_radii(
 45            r_wet=self.r_wet,
 46            environment=env,
 47            kappa=kappa,
 48        )
 49        v_dry = settings.formulae.trivia.volume(radius=self.r_dry)
 50        self.initial_mass = formulae.particle_shape_and_density.radius_to_mass(
 51            self.r_wet
 52        )
 53
 54        attributes = {
 55            "multiplicity": self.multiplicities,
 56            "dry volume": v_dry,
 57            "kappa times dry volume": kappa * v_dry,
 58            "signed water mass": self.initial_mass,
 59        }
 60
 61        products = (
 62            PySDM_products.ParcelDisplacement(name="z"),
 63            PySDM_products.Time(name="t"),
 64            PySDM_products.AmbientRelativeHumidity(name="RH", unit="%"),
 65            PySDM_products.AmbientRelativeHumidity(name="RH_ice", unit="%"),
 66            PySDM_products.AmbientTemperature(name="T"),
 67            PySDM_products.AmbientPressure(name="p", unit="hPa"),
 68            PySDM_products.WaterMixingRatio(name="LWC", radius_range=(0, np.inf)),
 69            PySDM_products.WaterMixingRatio(name="IWC", radius_range=(-np.inf, 0)),
 70            PySDM_products.AmbientWaterVapourMixingRatio(
 71                name="qv", var="water_vapour_mixing_ratio"
 72            ),
 73            PySDM_products.ParticleSpecificConcentration(
 74                name="ns", radius_range=(0, np.inf), unit="kg^-1"
 75            ),
 76            PySDM_products.ParticleSpecificConcentration(
 77                name="ni", radius_range=(-np.inf, 0), unit="kg^-1"
 78            ),
 79            PySDM_products.MeanRadius(name="rs", radius_range=(0, np.inf)),
 80            PySDM_products.MeanRadius(name="ri", radius_range=(-np.inf, 0)),
 81        )
 82        self.output_product_list = [
 83            "z",
 84            "t",
 85            "T",
 86            "p",
 87            "RH",
 88            "RH_ice",
 89            "LWC",
 90            "IWC",
 91            "qv",
 92            "ns",
 93            "ni",
 94            "rs",
 95            "ri",
 96        ]
 97
 98        self.particulator = Particulator(
 99            n_sd=settings.n_sd,
100            environment=env,
101            dynamics=(
102                [
103                    AmbientThermodynamics(),
104                    Condensation(adaptive=True),
105                ]
106                + (
107                    [VapourDepositionOnIce(adaptive=True)]
108                    if settings.deposition_enable
109                    else []
110                )
111                + [
112                    Freezing(
113                        homogeneous_freezing=settings.hom_freezing_type,
114                        immersion_freezing=None,
115                    )
116                ]
117            ),
118            attributes=attributes,
119            products=products,
120            requested_attributes=(
121                "temperature of last freezing",
122                "supersaturation of last freezing",
123                "radius",
124                "wet to critical volume ratio",
125            ),
126        )
127
128        self.n_output = settings.n_output
129        if settings.n_output == 1:
130            self.n_substeps = 1
131        else:
132            self.n_substeps = int(self.n_output / self.dt)
133        self.t_max_duration = settings.t_max_duration
134
135    def save(self, output):
136        cell_id = 0
137
138        for key in self.output_product_list:
139            if key == "t":
140                output[key].append(self.particulator.products[key].get())
141            else:
142                output[key].append(self.particulator.products[key].get()[cell_id])
143
144        output["T_frz"] = self.particulator.attributes[
145            "temperature of last freezing"
146        ].data.tolist()
147        output["RHi_frz"] = self.particulator.attributes[
148            "supersaturation of last freezing"
149        ].data.tolist()
150        if not output["radius"]:
151            output["radius"] = self.particulator.attributes["radius"].data.tolist()
152            output["multiplicity"] = self.particulator.attributes[
153                "multiplicity"
154            ].data.tolist()
155
156    def run(self):
157
158        if not self.silent:
159            print("Starting simulation...")
160
161        output = {
162            "T_frz": [],
163            "RHi_frz": [],
164            "radius": [],
165            "multiplicity": [],
166        }
167        for key in self.output_product_list:
168            output[key] = []
169
170        self.save(output)
171
172        while True:
173
174            self.particulator.run(self.n_substeps)
175            self.save(output)
176
177            w_cr_v_ratio = self.particulator.attributes[
178                "wet to critical volume ratio"
179            ].data
180            sig_mass = self.particulator.attributes["signed water mass"].data
181            frozen = sig_mass < 0
182            unactivated = w_cr_v_ratio < 1
183            if any(frozen) and all(np.logical_or(frozen, unactivated)):
184                if not self.silent:
185                    print("all particles frozen or evaporated")
186                # Assert for water saturation
187                test_water_saturation = np.asarray(output["RH"])
188                # Sort out times before CCN activation & after first occurence of ice
189                test_water_saturation = np.where(
190                    np.asarray(output["rs"]) < 1e-6, 100.0, test_water_saturation
191                )
192                test_water_saturation = np.where(
193                    np.asarray(output["IWC"]) > 0.0, 100.0, test_water_saturation
194                )
195                if np.allclose(test_water_saturation, 100.0, rtol=5e-2) is False:
196                    print(
197                        "Warning: water saturation is too high outside "
198                        "of activation and mixed-phase environment"
199                    )
200
201                break
202            if output["t"][-1] >= self.t_max_duration * self.dt:
203                print("time exceeded")
204                break
205
206        return output
Simulation(settings)
 17    def __init__(self, settings):
 18
 19        self.dt = settings.dt
 20
 21        formulae = settings.formulae
 22
 23        self.silent = settings.silent
 24
 25        env = Parcel(
 26            mixed_phase=True,
 27            dt=self.dt,
 28            mass_of_dry_air=settings.mass_of_dry_air,
 29            p0=settings.initial_pressure,
 30            initial_water_vapour_mixing_ratio=settings.initial_water_vapour_mixing_ratio,
 31            T0=settings.initial_temperature,
 32            w=settings.w_updraft,
 33            backend=settings.backend,
 34        )
 35
 36        self.n_sd = settings.n_sd
 37        self.multiplicities = discretise_multiplicities(
 38            settings.specific_concentration * env.mass_of_dry_air
 39        )
 40        self.r_wet = settings.r_wet
 41
 42        kappa = np.full_like(settings.r_wet, settings.kappa)
 43
 44        self.r_dry = equilibrate_dry_radii(
 45            r_wet=self.r_wet,
 46            environment=env,
 47            kappa=kappa,
 48        )
 49        v_dry = settings.formulae.trivia.volume(radius=self.r_dry)
 50        self.initial_mass = formulae.particle_shape_and_density.radius_to_mass(
 51            self.r_wet
 52        )
 53
 54        attributes = {
 55            "multiplicity": self.multiplicities,
 56            "dry volume": v_dry,
 57            "kappa times dry volume": kappa * v_dry,
 58            "signed water mass": self.initial_mass,
 59        }
 60
 61        products = (
 62            PySDM_products.ParcelDisplacement(name="z"),
 63            PySDM_products.Time(name="t"),
 64            PySDM_products.AmbientRelativeHumidity(name="RH", unit="%"),
 65            PySDM_products.AmbientRelativeHumidity(name="RH_ice", unit="%"),
 66            PySDM_products.AmbientTemperature(name="T"),
 67            PySDM_products.AmbientPressure(name="p", unit="hPa"),
 68            PySDM_products.WaterMixingRatio(name="LWC", radius_range=(0, np.inf)),
 69            PySDM_products.WaterMixingRatio(name="IWC", radius_range=(-np.inf, 0)),
 70            PySDM_products.AmbientWaterVapourMixingRatio(
 71                name="qv", var="water_vapour_mixing_ratio"
 72            ),
 73            PySDM_products.ParticleSpecificConcentration(
 74                name="ns", radius_range=(0, np.inf), unit="kg^-1"
 75            ),
 76            PySDM_products.ParticleSpecificConcentration(
 77                name="ni", radius_range=(-np.inf, 0), unit="kg^-1"
 78            ),
 79            PySDM_products.MeanRadius(name="rs", radius_range=(0, np.inf)),
 80            PySDM_products.MeanRadius(name="ri", radius_range=(-np.inf, 0)),
 81        )
 82        self.output_product_list = [
 83            "z",
 84            "t",
 85            "T",
 86            "p",
 87            "RH",
 88            "RH_ice",
 89            "LWC",
 90            "IWC",
 91            "qv",
 92            "ns",
 93            "ni",
 94            "rs",
 95            "ri",
 96        ]
 97
 98        self.particulator = Particulator(
 99            n_sd=settings.n_sd,
100            environment=env,
101            dynamics=(
102                [
103                    AmbientThermodynamics(),
104                    Condensation(adaptive=True),
105                ]
106                + (
107                    [VapourDepositionOnIce(adaptive=True)]
108                    if settings.deposition_enable
109                    else []
110                )
111                + [
112                    Freezing(
113                        homogeneous_freezing=settings.hom_freezing_type,
114                        immersion_freezing=None,
115                    )
116                ]
117            ),
118            attributes=attributes,
119            products=products,
120            requested_attributes=(
121                "temperature of last freezing",
122                "supersaturation of last freezing",
123                "radius",
124                "wet to critical volume ratio",
125            ),
126        )
127
128        self.n_output = settings.n_output
129        if settings.n_output == 1:
130            self.n_substeps = 1
131        else:
132            self.n_substeps = int(self.n_output / self.dt)
133        self.t_max_duration = settings.t_max_duration
dt
silent
n_sd
multiplicities
r_wet
r_dry
initial_mass
output_product_list
particulator
n_output
t_max_duration
def save(self, output):
135    def save(self, output):
136        cell_id = 0
137
138        for key in self.output_product_list:
139            if key == "t":
140                output[key].append(self.particulator.products[key].get())
141            else:
142                output[key].append(self.particulator.products[key].get()[cell_id])
143
144        output["T_frz"] = self.particulator.attributes[
145            "temperature of last freezing"
146        ].data.tolist()
147        output["RHi_frz"] = self.particulator.attributes[
148            "supersaturation of last freezing"
149        ].data.tolist()
150        if not output["radius"]:
151            output["radius"] = self.particulator.attributes["radius"].data.tolist()
152            output["multiplicity"] = self.particulator.attributes[
153                "multiplicity"
154            ].data.tolist()
def run(self):
156    def run(self):
157
158        if not self.silent:
159            print("Starting simulation...")
160
161        output = {
162            "T_frz": [],
163            "RHi_frz": [],
164            "radius": [],
165            "multiplicity": [],
166        }
167        for key in self.output_product_list:
168            output[key] = []
169
170        self.save(output)
171
172        while True:
173
174            self.particulator.run(self.n_substeps)
175            self.save(output)
176
177            w_cr_v_ratio = self.particulator.attributes[
178                "wet to critical volume ratio"
179            ].data
180            sig_mass = self.particulator.attributes["signed water mass"].data
181            frozen = sig_mass < 0
182            unactivated = w_cr_v_ratio < 1
183            if any(frozen) and all(np.logical_or(frozen, unactivated)):
184                if not self.silent:
185                    print("all particles frozen or evaporated")
186                # Assert for water saturation
187                test_water_saturation = np.asarray(output["RH"])
188                # Sort out times before CCN activation & after first occurence of ice
189                test_water_saturation = np.where(
190                    np.asarray(output["rs"]) < 1e-6, 100.0, test_water_saturation
191                )
192                test_water_saturation = np.where(
193                    np.asarray(output["IWC"]) > 0.0, 100.0, test_water_saturation
194                )
195                if np.allclose(test_water_saturation, 100.0, rtol=5e-2) is False:
196                    print(
197                        "Warning: water saturation is too high outside "
198                        "of activation and mixed-phase environment"
199                    )
200
201                break
202            if output["t"][-1] >= self.t_max_duration * self.dt:
203                print("time exceeded")
204                break
205
206        return output