Skip to content

homogeneous nucleation of liquid droplets & expansion chamber example #1492

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 64 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
64 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
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
1 change: 1 addition & 0 deletions PySDM/environments/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@
from .kinematic_1d import Kinematic1D
from .kinematic_2d import Kinematic2D
from .parcel import Parcel
from .expansion_chamber import ExpansionChamber
93 changes: 93 additions & 0 deletions PySDM/environments/expansion_chamber.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
"""
Zero-dimensional expansion chamber framework
"""

from typing import Optional, List

from PySDM.environments.impl.moist_lagrangian import MoistLagrangian
from PySDM.impl.mesh import Mesh
from PySDM.environments.impl import register_environment


@register_environment()
class ExpansionChamber(MoistLagrangian):
def __init__(
self,
*,
dt,
volume: float,
initial_pressure: float,
initial_temperature: float,
initial_relative_humidity: float,
delta_pressure: float,
delta_time: float,
variables: Optional[List[str]] = None,
mixed_phase=False,
):
variables = (variables or []) + ["rhod"]

super().__init__(dt, Mesh.mesh_0d(), variables, mixed_phase=mixed_phase)

self.dv = volume
self.initial_pressure = initial_pressure
self.initial_temperature = initial_temperature
self.initial_relative_humidity = initial_relative_humidity
self.delta_time = delta_time
self.dp_dt = delta_pressure / delta_time

def register(self, builder):
self.mesh.dv = self.dv

super().register(builder)

formulae = self.particulator.formulae
pv0 = (
self.initial_relative_humidity
* formulae.saturation_vapour_pressure.pvs_water(self.initial_temperature)
)
th_std = formulae.trivia.th_std(
p=self.initial_pressure, T=self.initial_temperature
)
initial_water_vapour_mixing_ratio = (
formulae.constants.eps * pv0 / (self.initial_pressure - pv0)
)

self["rhod"][:] = formulae.state_variable_triplet.rho_d(
p=self.initial_pressure,
water_vapour_mixing_ratio=initial_water_vapour_mixing_ratio,
theta_std=th_std,
)
self["thd"][:] = formulae.state_variable_triplet.th_dry(
th_std, initial_water_vapour_mixing_ratio
)
self["water_vapour_mixing_ratio"][:] = initial_water_vapour_mixing_ratio

self.post_register()

def advance_moist_vars(self):
"""compute new values of dry density and thd, and write them to self._tmp and self["thd"]
assuming water-vapour mixing ratio is not altered by the expansion"""
dt = self.particulator.dt
t_new = (self.particulator.n_steps + 1) * dt
if t_new > self.delta_time:
return

formulae = self.particulator.formulae
p_new = self["p"][0] + self.dp_dt * dt
qv = self["water_vapour_mixing_ratio"][0]
gg = (
1 - formulae.adiabatic_exponent.gamma(qv)
) / formulae.adiabatic_exponent.gamma(qv)
T_new = self.initial_temperature * (self.initial_pressure / p_new) ** gg
wvmr_new = self._tmp["water_vapour_mixing_ratio"][
0
] # TODO #1492 - should _tmp or self[] be used?
pv_new = wvmr_new * p_new / (wvmr_new + formulae.constants.eps)
pd_new = p_new - pv_new

self._tmp["rhod"][:] = pd_new / T_new / formulae.constants.Rd
self["thd"][:] = formulae.trivia.th_std(p=pd_new, T=T_new)

def sync_moist_vars(self):
for var in self.variables:
self._tmp[var][:] = self[var][:]
4 changes: 4 additions & 0 deletions PySDM/environments/impl/moist.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ def _recalculate_temperature_pressure_relative_humidity(self, target):
)

def sync(self):
"""fills the "predicted" set of thermodynamic variables with derived values freshly
computed from `self.get_thd()` and `self.get_water_vapour_mixing_ratio()`"""
target = self._tmp
target["water_vapour_mixing_ratio"].ravel(self.get_water_vapour_mixing_ratio())
target["thd"].ravel(self.get_thd())
Expand Down Expand Up @@ -108,6 +110,8 @@ def get_thd(self) -> np.ndarray:
raise NotImplementedError()

def notify(self):
"""invalidates the "predicted" set of thermodynamic variables storing its
contents into the "current" set of vars"""
if self._values["predicted"] is None:
return

Expand Down
31 changes: 31 additions & 0 deletions PySDM/environments/impl/moist_lagrangian.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""
Zero-dimensional (Lagrangian) moist environment commons
"""

from PySDM.environments.impl.moist import Moist


class MoistLagrangian(Moist):
"""base class for moist Lagrangian environments (parcel, chamber, ...)"""

def get_thd(self):
return self["thd"]

def get_water_vapour_mixing_ratio(self):
return self["water_vapour_mixing_ratio"]

def sync(self):
self.sync_moist_vars()
self.advance_moist_vars()
super().sync()

def post_register(self):
self.sync_moist_vars()
super().sync()
self.notify()

def sync_moist_vars(self):
raise NotImplementedError()

def advance_moist_vars(self):
raise NotImplementedError()
27 changes: 7 additions & 20 deletions PySDM/environments/parcel.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

import numpy as np

from PySDM.environments.impl.moist import Moist
from PySDM.environments.impl.moist_lagrangian import MoistLagrangian
from PySDM.impl.mesh import Mesh
from PySDM.initialisation.equilibrate_wet_radii import (
default_rtol,
Expand All @@ -16,7 +16,7 @@


@register_environment()
class Parcel(Moist): # pylint: disable=too-many-instance-attributes
class Parcel(MoistLagrangian): # pylint: disable=too-many-instance-attributes
def __init__(
self,
*,
Expand Down Expand Up @@ -60,7 +60,7 @@ def register(self, builder):
rhod0, self.mass_of_dry_air
)

Moist.register(self, builder)
super().register(builder)

params = (
self.initial_water_vapour_mixing_ratio,
Expand All @@ -74,11 +74,9 @@ def register(self, builder):
self["rhod"][:] = params[2]
self["z"][:] = params[3]
self["t"][:] = params[4]

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

self.post_register()

def init_attributes(
self,
Expand Down Expand Up @@ -108,7 +106,7 @@ def init_attributes(
attributes["dry volume"] = dry_volume
return attributes

def advance_parcel_vars(self):
def advance_moist_vars(self):
dt = self.particulator.dt
T = self["T"][0]
p = self["p"][0]
Expand Down Expand Up @@ -143,21 +141,10 @@ def advance_parcel_vars(self):
(self._tmp["rhod"][0] + self["rhod"][0]) / 2, self.mass_of_dry_air
)

def get_thd(self):
return self["thd"]

def get_water_vapour_mixing_ratio(self):
return self["water_vapour_mixing_ratio"]

def sync_parcel_vars(self):
def sync_moist_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_parcel_vars()
self.advance_parcel_vars()
super().sync()
4 changes: 4 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: str = "Null",
Expand All @@ -57,6 +58,7 @@ def __init__( # pylint: disable=too-many-locals
terminal_velocity: str = "GunnKinzer1949",
air_dynamic_viscosity: str = "ZografosEtAl1987",
bulk_phase_partitioning: str = "Null",
adiabatic_exponent: str = "Dry",
handle_all_breakups: bool = False,
):
# initialisation of the fields below is just to silence pylint and to enable code hints
Expand All @@ -77,6 +79,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 All @@ -90,6 +93,7 @@ def __init__( # pylint: disable=too-many-locals
self.air_dynamic_viscosity = air_dynamic_viscosity
self.terminal_velocity = terminal_velocity
self.bulk_phase_partitioning = bulk_phase_partitioning
self.adiabatic_exponent = adiabatic_exponent

self._components = tuple(
i
Expand Down
2 changes: 2 additions & 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 All @@ -47,5 +48,6 @@
air_dynamic_viscosity,
terminal_velocity,
bulk_phase_partitioning,
adiabatic_exponent,
)
from .constants import convert_to, in_unit, si
6 changes: 6 additions & 0 deletions PySDM/physics/adiabatic_exponent/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""
Alternative formulations of adiabatic exponent
"""

from .dry import Dry
from .moist_leading_terms import MoistLeadingTerms
13 changes: 13 additions & 0 deletions PySDM/physics/adiabatic_exponent/dry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
"""
adiabatic exponent, dry air
"""


class Dry:
def __init__(self, _):
pass

# pylint: disable=too-many-arguments
@staticmethod
def gamma(const, qv):
return 1 + const.Rd / const.c_vd
18 changes: 18 additions & 0 deletions PySDM/physics/adiabatic_exponent/moist_leading_terms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"""
adiabatic exponent, moist air, expanding to first order in qv, assuming qt=qv
"""


class MoistLeadingTerms:
def __init__(self, _):
pass

# pylint: disable=too-many-arguments
@staticmethod
def gamma(const, qv):
return (
1
+ const.Rd / const.c_vd
+ (const.Rv * qv) / (const.c_vd * (1 - qv))
- (const.Rd * qv * const.c_vv) / (const.c_vd**2 * (1 - qv))
)
3 changes: 3 additions & 0 deletions PySDM/physics/constants_defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -681,6 +681,9 @@ def compute_derived_values(c: dict):

c["Rd_over_c_pd"] = c["Rd"] / c["c_pd"]

c["c_vd"] = c["c_pd"] - c["Rd"]
c["c_vv"] = c["c_pv"] - c["Rv"]

c["water_molar_volume"] = c["Mv"] / c["rho_w"]
c["rho_STP"] = c["p_STP"] / c["Rd"] / c["T_STP"]
c["H_u"] = c["M"] / c["p_STP"]
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
Loading
Loading