Skip to content
Draft
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
ca90206
add example of lifting parcel for time t_lift and then holding it to …
Nov 27, 2024
f716859
do dp sweep to plot Tmin vs dp under a few conditions of S0 and C0 to…
Dec 19, 2024
f41866b
Merge remote-tracking branch 'oa/main' into expansion_chamber
Dec 20, 2024
82aca13
Merge remote-tracking branch 'oa/main' into expansion_chamber
Jan 6, 2025
58f5e14
add some placeholders for S&P classic nucleation theory homogeneous n…
Jan 7, 2025
5f4dc31
add formulae for critical size of nucleated droplets
Jan 7, 2025
5d1be16
doc eq origin
Jan 7, 2025
d0b56aa
pylint
Jan 7, 2025
c98eaab
add unit tests for formulae relative to tables 11.1 and 11.4 in S&P. …
Jan 7, 2025
099e7ca
add bibliography entries. I'm not sure I did this correctly.
Jan 7, 2025
b141f02
Merge remote-tracking branch 'oa/main' into expansion_chamber
Jan 7, 2025
37053f8
swtich url for S&P from 3rd to 2nd ed. (linked full text available on…
slayoo Jan 7, 2025
ca940e8
accommodate S&P url in doc-generating script
slayoo Jan 7, 2025
70e0548
address pylint hints
slayoo Jan 7, 2025
a501b8f
addressing devops_tests hints - header cells in the notebook
slayoo Jan 7, 2025
e09bdc4
whitespace removal
slayoo Jan 7, 2025
5c3df34
remove null
Jan 10, 2025
4135c22
better variable names
Jan 10, 2025
3b607ca
use Gaussian aerosol mode, not LogNormal
Jan 10, 2025
f6f26bb
change import order
slayoo Jan 10, 2025
e59f1af
Merge remote-tracking branch 'oa/main' into expansion_chamber
Jan 13, 2025
33e2e3a
add expansion_chamber environment. todo check the thermodynamics here…
Jan 13, 2025
b584cfd
Merge remote-tracking branch 'oa/main' into expansion_chamber
Jan 15, 2025
03bffe7
refactor ExpansionChamber env and use it in Erinin_et_al_2025 example
slayoo Jan 26, 2025
ce6e454
forgotten __init__ move
slayoo Jan 26, 2025
220b13e
Merge branch 'main' into expansion_chamber
slayoo Jan 26, 2025
b91a94c
replace assumed temperature profile with adiabatic temperature condit…
Jan 29, 2025
a7f4950
add new physics formulae for adiabatic exponent
Jan 29, 2025
4ffd684
introduce homogeneous nucleation dynamic with some common code with s…
slayoo Jan 30, 2025
cd3375b
raname seeding -> spawning in backend tests
slayoo Jan 30, 2025
f9d99ce
raname injecting -> spawning in backend tests
slayoo Jan 30, 2025
8bc50bb
comment on zero multiplicity -> MC sampling?
slayoo Jan 30, 2025
e5abcb3
work in progress towards getting nucleation dynamic enabled in the ex…
slayoo Jan 30, 2025
fedd946
add back boltzmann constant
Jan 30, 2025
fe4bfb9
fix errors in dynamic to get to error with 'No available null SDs to …
Jan 30, 2025
59a0fa4
wip add nan SDs. getting error about index out of bounds.
Jan 30, 2025
3bb377b
make attribute saving logic compatible with variable state-vector size
slayoo Jan 31, 2025
c4f2ec8
introduce SuperParticleSpawningDynamic.check_extensive_attribute_keys…
slayoo Feb 3, 2025
763d853
make multiplicity a Storage as well (currently stops on: "Python int …
slayoo Feb 4, 2025
1dc09c9
make example run with nucleation by shortening timestep to avoid over…
Feb 4, 2025
d53312a
simplified units handling, enabled condensation adaptivity
slayoo Feb 6, 2025
c11118c
resolve merge conflict
slayoo Feb 6, 2025
0ee157d
fix bib issues
slayoo Feb 6, 2025
ee6cca2
fix multiplicity product. print out fractional change in total partic…
Feb 7, 2025
decce8e
Merge remote-tracking branch 'oa/main' into expansion_chamber
Feb 11, 2025
deb27c4
qv to vapour_mixing_ratio spelled out
Feb 11, 2025
60cb166
fixing tests, addressing pylint hints
slayoo Feb 13, 2025
8989e38
reinstantiate changes from #1509
slayoo Feb 13, 2025
84759aa
addressing pylint hints
slayoo Feb 13, 2025
fe370f8
setup chamber smoke tests execution on CI
slayoo Feb 13, 2025
2b811bd
address pylint hints
slayoo Feb 13, 2025
2bd28ba
fix badge URLs
slayoo Feb 13, 2025
2896faa
add TODO labels (pointing to the PR!_
slayoo Feb 13, 2025
dc1217e
pylint hints, notebook header
slayoo Feb 13, 2025
ab4412d
pylint hints
slayoo Feb 13, 2025
43c4828
missing bib entry
slayoo Feb 13, 2025
897fb3e
simplify notebook code; skip nucleation if RH<1
slayoo Feb 13, 2025
1e7631e
address pylint hints
slayoo Feb 13, 2025
7d180ad
address pylint hints again
slayoo Feb 13, 2025
649caba
fix MockParticulator
slayoo Feb 13, 2025
89d1086
extensions in homogeneous liquid nucleation unit tests
slayoo Feb 14, 2025
12b55dd
less crazy test values for T and S
slayoo Feb 14, 2025
7150f84
unit test for homogeneous nucleation dynamic using constant rate
slayoo Feb 14, 2025
ed24039
Merge remote-tracking branch 'oa/main' into expansion_chamber
Feb 25, 2025
6be95af
Merge remote-tracking branch 'oa/main' into expansion_chamber
Jul 17, 2025
8bceee3
new experiment without seed particles to look at homogeneous nucleati…
Jul 17, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ PySDM_examples/utils/temporary_files/*
*.pkl
*.vtu
*.vts
*.png
*.csv
*.xlsx

# Byte-compiled / optimized / DLL files
__pycache__/
Expand Down
138 changes: 138 additions & 0 deletions PySDM/environments/expansion_chamber.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
"""
Zero-dimensional expansion chamber framework
"""

import numpy as np

from PySDM.environments.impl.moist import Moist
from PySDM.impl.mesh import Mesh
from PySDM.initialisation.equilibrate_wet_radii import (
default_rtol,
equilibrate_wet_radii,
)
from PySDM.environments.impl import register_environment


@register_environment()
class ExpansionChamber(Moist):
def __init__(
self,
*,
dt,
volume: float,
p0: float,
T0: float,
initial_water_vapour_mixing_ratio: float,
pf: float,
tf: float,
variables,
mixed_phase=False,
):
super().__init__(dt, Mesh.mesh_0d(), variables, mixed_phase=mixed_phase)

self.volume = volume
self.p0 = p0
self.T0 = T0
self.initial_water_vapour_mixing_ratio = initial_water_vapour_mixing_ratio
self.pf = pf
self.tf = tf

self.formulae = None
self.delta_liquid_water_mixing_ratio = np.nan
self.params = None

def register(self, builder):
self.formulae = builder.particulator.formulae
pd0 = self.formulae.trivia.p_d(self.p0, self.initial_water_vapour_mixing_ratio)
rhod0 = self.formulae.state_variable_triplet.rhod_of_pd_T(pd0, self.T0)
self.mesh.dv = self.volume

Moist.register(self, builder)

params = (self.p0, self.T0, self.initial_water_vapour_mixing_ratio, 0)
self["p"][:] = params[0]
self["T"][:] = params[1]
self["water_vapour_mixing_ratio"][:] = params[2]
self["t"][:] = params[3]

self._tmp["water_vapour_mixing_ratio"][:] = params[0]
self.sync_chamber_vars()
Moist.sync(self)
self.notify()

def init_attributes(
self,
*,
n_in_dv: [float, np.ndarray],
kappa: float,
r_dry: [float, np.ndarray],
rtol=default_rtol,
include_dry_volume_in_attribute: bool = True,
):
if not isinstance(n_in_dv, np.ndarray):
r_dry = np.array([r_dry])
n_in_dv = np.array([n_in_dv])

attributes = {}
dry_volume = self.formulae.trivia.volume(radius=r_dry)
attributes["kappa times dry volume"] = dry_volume * kappa
attributes["multiplicity"] = n_in_dv
r_wet = equilibrate_wet_radii(
r_dry=r_dry,
environment=self,
kappa_times_dry_volume=attributes["kappa times dry volume"],
rtol=rtol,
)
attributes["volume"] = self.formulae.trivia.volume(radius=r_wet)
if include_dry_volume_in_attribute:
attributes["dry volume"] = dry_volume
return attributes

def formulae_drho_dp(const, p, T, water_vapour_mixing_ratio, lv, dql_dp):
R_q = const.Rv / (1 / water_vapour_mixing_ratio + 1) + const.Rd / (
1 + water_vapour_mixing_ratio
)
cp_q = const.c_pv / (1 / water_vapour_mixing_ratio + 1) + const.c_pd / (
1 + water_vapour_mixing_ratio
)
return -1 / R_q / T - p / R_q / T**2 * lv / cp_q * dql_dp

def advance_chamber_vars(self):
dt = self.particulator.dt
T = self["T"][0]
p = self["p"][0]
t = self["t"][0]

dp_dt = (self.pf - self.p0) / (self.tf) * (self.dt / 2) # mid-point
water_vapour_mixing_ratio = (
self["water_vapour_mixing_ratio"][0]
- self.delta_liquid_water_mixing_ratio / 2
)

drho_dp = self.formulae_drho_dp(
p=p,
T=T,
water_vapour_mixing_ratio=water_vapour_mixing_ratio,
lv=self.formulae.latent_heat.lv(T),
dql_dp=self.delta_liquid_water_mixing_ratio / dt,
)
drhod_dp = drho_dp

self.particulator.backend.explicit_euler(self._tmp["t"], dt, 1)
self.particulator.backend.explicit_euler(self._tmp["p"], dt, dp_dt)
self.particulator.backend.explicit_euler(
self._tmp["rhod"], dt, drhod_dp * dp_dt
)

def sync_chamber_vars(self):
self.delta_liquid_water_mixing_ratio = (
self._tmp["water_vapour_mixing_ratio"][0]
- self["water_vapour_mixing_ratio"][0]
)
for var in self.variables:
self._tmp[var][:] = self[var][:]

def sync(self):
self.sync_chamber_vars()
self.advance_chamber_vars()
super().sync()
2 changes: 2 additions & 0 deletions PySDM/formulae.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ def __init__( # pylint: disable=too-many-locals
hydrostatics: str = "Default",
freezing_temperature_spectrum: str = "Null",
heterogeneous_ice_nucleation_rate: str = "Null",
homogeneous_liquid_nucleation_rate: str = "Null",
fragmentation_function: str = "AlwaysN",
isotope_equilibrium_fractionation_factors: str = "Null",
isotope_meteoric_water_line_excess: str = "Null",
Expand Down Expand Up @@ -76,6 +77,7 @@ def __init__( # pylint: disable=too-many-locals
self.hydrostatics = hydrostatics
self.freezing_temperature_spectrum = freezing_temperature_spectrum
self.heterogeneous_ice_nucleation_rate = heterogeneous_ice_nucleation_rate
self.homogeneous_liquid_nucleation_rate = homogeneous_liquid_nucleation_rate
self.fragmentation_function = fragmentation_function
self.isotope_equilibrium_fractionation_factors = (
isotope_equilibrium_fractionation_factors
Expand Down
1 change: 1 addition & 0 deletions PySDM/physics/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
fragmentation_function,
freezing_temperature_spectrum,
heterogeneous_ice_nucleation_rate,
homogeneous_liquid_nucleation_rate,
hydrostatics,
hygroscopicity,
impl,
Expand Down
1 change: 1 addition & 0 deletions PySDM/physics/constants_defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@

R_str = sci.R * si.joule / si.kelvin / si.mole
N_A = sci.N_A / si.mole
k_B = sci.k * si.joule / si.kelvin

D0 = 2.26e-5 * si.metre**2 / si.second
D_exp = 1.81
Expand Down
7 changes: 7 additions & 0 deletions PySDM/physics/homogeneous_liquid_nucleation_rate/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"""
homogeneous liquid droplet nucleation rate formulations
"""

from PySDM.impl.null_physics_class import Null
from .cnt import CNT
from .constant import Constant
32 changes: 32 additions & 0 deletions PySDM/physics/homogeneous_liquid_nucleation_rate/cnt.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"""
classical nucleation theory (CNT)
formulae from [Seinfeld and Pandis](https://archive.org/details/0237-pdf-atmospheric-chemistry-and-physics-2nd-ed-j.-seinfeld-s.-pandis-wiley-2006-ww)
Eq (11.47) and (11.52)
""" # pylint: disable=line-too-long

import numpy as np


class CNT:
def __init__(self, _):
return

@staticmethod
def j_liq_homo(const, T, S, e_s):
mass_per_moleculue = const.Mv / const.N_A
volume_per_molecule = mass_per_moleculue / const.rho_w
N1 = (S * e_s) / (mass_per_moleculue * const.Rv * T) # molecules per m3
return (
((2 * const.sgm_w) / (np.pi * mass_per_moleculue)) ** (1 / 2)
* (volume_per_molecule * N1**2 / S)
* np.exp(
(-16 * np.pi * volume_per_molecule**2 * const.sgm_w**3)
/ (3 * const.k_B**3 * T**3 * np.log(S) ** 2)
)
)

@staticmethod
def r_liq_homo(const, T, S):
mass_per_moleculue = const.Mv / const.N_A
volume_per_molecule = mass_per_moleculue / const.rho_w
return (2 * const.sgm_w * volume_per_molecule) / (const.k_B * T * np.log(S))
19 changes: 19 additions & 0 deletions PySDM/physics/homogeneous_liquid_nucleation_rate/constant.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"""
constant rate formulation (for tests)
"""

import numpy as np


class Constant:
def __init__(self, const):
assert np.isfinite(const.J_LIQ_HOMO)
assert np.isfinite(const.R_LIQ_HOMO)

@staticmethod
def j_liq_homo(const, T, S, e_s): # pylint: disable=unused-argument
return const.J_LIQ_HOMO

@staticmethod
def r_liq_homo(const, T, S): # pylint: disable=unused-argument
return const.R_LIQ_HOMO
10 changes: 10 additions & 0 deletions docs/bibliography.json
Original file line number Diff line number Diff line change
Expand Up @@ -691,5 +691,15 @@
"usages": ["PySDM/physics/saturation_vapour_pressure/wexler_1976.py"],
"title": "Vapor Pressure Formulation for Water in Range 0 to f 00 °C. A Revision",
"label": "Wexler 1976 (J. Res. NBS A Phys. Ch. 80A)"
},
"https://doi.org/10.48550/arXiv.2501.01467": {
"usages": ["examples/PySDM_examples/expansion_chamber/aerosol.py", "examples/PySDM_examples/expansion_chamber/expansion_experiment.ipynb"],
"title": "Droplet Nucleation In a Rapid Expansion Aerosol Chamber",
"label": "Erinin et al. 2024 (arXiv:2501.01467)"
},
"https://archive.org/details/0237-pdf-atmospheric-chemistry-and-physics-2nd-ed-j.-seinfeld-s.-pandis-wiley-2006-ww": {
"usages": ["PySDM/physics/homogeneous_liquid_nucleation_rate/cnt.py"],
"title": "Atmospheric Chemistry and Physics: From Air Pollution to Climate Change, 2nd Edition",
"label": "Seinfeld & Pandis 2006 (Wiley)"
}
}
2 changes: 1 addition & 1 deletion docs/generate_html.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ def check_urls(urls_from_json):
r"\b(https://doi\.org/10[.][0-9]{4,}(?:[.][0-9]+)*/(?:(?![\"&\'<>^\\])\S)+)\b",
r"\b(https://digitallibrary\.un\.org/record/(?:[0-9])+)\b",
r"\b(http://mi\.mathnet\.ru/dan(?:[0-9])+)\b",
r"\b(https://archive.org/details/(?:[0-9a-z])+)\b",
r"\b(https://archive.org/details/(?:[0-9a-z_\.-])+)\b",
):
urls = re.findall(pattern, text)
if urls:
Expand Down
4 changes: 4 additions & 0 deletions examples/PySDM_examples/expansion_chamber/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# pylint: disable=invalid-name
"""
expansion chamber example imagined as an ascending parcel with updraft velocity matched to dp/dt
"""
47 changes: 47 additions & 0 deletions examples/PySDM_examples/expansion_chamber/aerosol.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
""" aerosol parameters from [Erinin et al. 2025](https://doi.org/10.48550/arXiv.2501.01467) """

from chempy import Substance
from pystrict import strict

from PySDM.initialisation import spectra
from PySDM.initialisation.aerosol_composition import DryAerosolMixture
from PySDM.physics import si


@strict
class AerosolChamber(DryAerosolMixture):
def __init__(
self,
water_molar_volume: float,
N: float = 1e5 / si.cm**3,
):
mode1 = {"CaCO3": 1.0}

super().__init__(
compounds=("CaCO3",),
molar_masses={
"CaCO3": Substance.from_formula("CaCO3").mass * si.g / si.mole
},
densities={
"CaCO3": 2.71 * si.g / si.cm**3,
},
is_soluble={
"CaCO3": True,
},
ionic_dissociation_phi={
"CaCO3": 1,
},
)
self.modes = (
{
"kappa": self.kappa(
mass_fractions=mode1,
water_molar_volume=water_molar_volume,
),
"spectrum": spectra.Gaussian(
norm_factor=N,
loc=316 / 2 * si.nm,
scale=257 / 2 * si.nm,
),
},
)
349 changes: 349 additions & 0 deletions examples/PySDM_examples/expansion_chamber/expansion_experiment.ipynb

Large diffs are not rendered by default.

Loading