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 66 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all 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
4 changes: 4 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,11 @@ jobs:
matrix:
platform: [ubuntu-24.04, macos-13, macos-14, windows-latest]
python-version: ["3.9", "3.12"]
<<<<<<< HEAD
test-suite: ["unit_tests/!(dynamics)", "unit_tests/dynamics/!(collisions)", "unit_tests/dynamics/collisions", "smoke_tests/no_env", "smoke_tests/box", "smoke_tests/parcel_a", "smoke_tests/parcel_b", "smoke_tests/parcel_c", "smoke_tests/parcel_d", "smoke_tests/kinematic_1d", "smoke_tests/kinematic_2d", "smoke_tests/chamber", "tutorials_tests"]
=======
test-suite: ["unit_tests/!(dynamics)", "unit_tests/dynamics/!(condensation|collisions)", "unit_tests/dynamics/condensation", "unit_tests/dynamics/collisions","smoke_tests/no_env", "smoke_tests/box", "smoke_tests/parcel_a", "smoke_tests/parcel_b", "smoke_tests/parcel_c", "smoke_tests/parcel_d", "smoke_tests/kinematic_1d", "smoke_tests/kinematic_2d", "tutorials_tests"]
>>>>>>> oa/main
exclude:
- platform: "macos-14"
python-version: "3.9"
Expand Down
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,15 @@ PySDM_examples/utils/temporary_files/*
*.pkl
*.vtu
*.vts
<<<<<<< HEAD
*.png
*.csv
*.xlsx
=======
*.svg
*.gif
*.ipynb.badges.md
>>>>>>> oa/main

# Byte-compiled / optimized / DLL files
__pycache__/
Expand Down
2 changes: 1 addition & 1 deletion PySDM/backends/impl_numba/methods/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,5 @@
from .pair_methods import PairMethods
from .physics_methods import PhysicsMethods
from .terminal_velocity_methods import TerminalVelocityMethods
from .seeding_methods import SeedingMethods
from .spawning_methods import SpawningMethods
from .deposition_methods import DepositionMethods
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""CPU implementation of backend methods for particle injections"""
"""CPU implementation of backend methods for particle spawning"""

from functools import cached_property

Expand All @@ -7,62 +7,62 @@
from PySDM.backends.impl_common.backend_methods import BackendMethods


class SeedingMethods(BackendMethods): # pylint: disable=too-few-public-methods
class SpawningMethods(BackendMethods): # pylint: disable=too-few-public-methods
@cached_property
def _seeding(self):
def _spawning(self):
@numba.njit(**{**self.default_jit_flags, "parallel": False})
def body( # pylint: disable=too-many-arguments
idx,
multiplicity,
extensive_attributes,
seeded_particle_index,
seeded_particle_multiplicity,
seeded_particle_extensive_attributes,
number_of_super_particles_to_inject: int,
spawned_particle_index,
spawned_particle_multiplicity,
spawned_particle_extensive_attributes,
number_of_super_particles_to_spawn: int,
):
number_of_super_particles_already_injected = 0
# TODO #1387 start enumerating from the end of valid particle set
for i, mult in enumerate(multiplicity):
if (
number_of_super_particles_to_inject
number_of_super_particles_to_spawn
== number_of_super_particles_already_injected
):
break
if mult == 0:
idx[i] = -1
s = seeded_particle_index[
s = spawned_particle_index[
number_of_super_particles_already_injected
]
number_of_super_particles_already_injected += 1
multiplicity[i] = seeded_particle_multiplicity[s]
multiplicity[i] = spawned_particle_multiplicity[s]
for a in range(len(extensive_attributes)):
extensive_attributes[a, i] = (
seeded_particle_extensive_attributes[a, s]
spawned_particle_extensive_attributes[a, s]
)
assert (
number_of_super_particles_to_inject
number_of_super_particles_to_spawn
== number_of_super_particles_already_injected
)

return body

def seeding(
def spawning(
self,
*,
idx,
multiplicity,
extensive_attributes,
seeded_particle_index,
seeded_particle_multiplicity,
seeded_particle_extensive_attributes,
number_of_super_particles_to_inject: int,
spawned_particle_index,
spawned_particle_multiplicity,
spawned_particle_extensive_attributes,
number_of_super_particles_to_spawn: int,
):
self._seeding(
self._spawning(
idx=idx.data,
multiplicity=multiplicity.data,
extensive_attributes=extensive_attributes.data,
seeded_particle_index=seeded_particle_index.data,
seeded_particle_multiplicity=seeded_particle_multiplicity.data,
seeded_particle_extensive_attributes=seeded_particle_extensive_attributes.data,
number_of_super_particles_to_inject=number_of_super_particles_to_inject,
spawned_particle_index=spawned_particle_index.data,
spawned_particle_multiplicity=spawned_particle_multiplicity.data,
spawned_particle_extensive_attributes=spawned_particle_extensive_attributes.data,
number_of_super_particles_to_spawn=number_of_super_particles_to_spawn,
)
4 changes: 2 additions & 2 deletions PySDM/backends/numba.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ class Numba( # pylint: disable=too-many-ancestors,duplicate-code
methods.DisplacementMethods,
methods.TerminalVelocityMethods,
methods.IsotopeMethods,
methods.SeedingMethods,
methods.SpawningMethods,
methods.DepositionMethods,
):
Storage = ImportedStorage
Expand Down Expand Up @@ -78,5 +78,5 @@ def __init__(self, formulae=None, double_precision=True, override_jit_flags=None
methods.DisplacementMethods.__init__(self)
methods.TerminalVelocityMethods.__init__(self)
methods.IsotopeMethods.__init__(self)
methods.SeedingMethods.__init__(self)
methods.SpawningMethods.__init__(self)
methods.DepositionMethods.__init__(self)
1 change: 1 addition & 0 deletions PySDM/dynamics/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@
from PySDM.dynamics.freezing import Freezing
from PySDM.dynamics.relaxed_velocity import RelaxedVelocity
from PySDM.dynamics.seeding import Seeding
from PySDM.dynamics.homogeneous_liquid_nucleation import HomogeneousLiquidNucleation
from PySDM.dynamics.vapour_deposition_on_ice import VapourDepositionOnIce
72 changes: 72 additions & 0 deletions PySDM/dynamics/homogeneous_liquid_nucleation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"""nucleation of droplets in CCN-void air that spawns new super-droplets"""

import numpy as np
from PySDM.dynamics.impl import register_dynamic
from PySDM.dynamics.impl import SuperParticleSpawningDynamic


@register_dynamic()
class HomogeneousLiquidNucleation(SuperParticleSpawningDynamic):
def __init__(self):
self.particulator = None
self.formulae = None
self.index = None

def register(self, builder):
self.particulator = builder.particulator
self.formulae = builder.formulae
self.index = self.particulator.Index.identity_index(1)

def __call__(self):
env = {
k: self.particulator.environment[k].to_ndarray()[0] for k in ("T", "RH")
} # TODO #1492: >0D

if env["RH"] < 1:
return

e_s = self.formulae.saturation_vapour_pressure.pvs_water(env["T"])
j = self.formulae.homogeneous_liquid_nucleation_rate.j_liq_homo(
env["T"], env["RH"], e_s
)

# TODO #1492: take care of cases where round yields zero -> MC sampling?
new_sd_multiplicity = round(
j * self.particulator.environment.mesh.dv * self.particulator.dt
)

if new_sd_multiplicity > 0:
r_wet = self.formulae.homogeneous_liquid_nucleation_rate.r_liq_homo(
env["T"], env["RH"]
)
v_wet = self.formulae.trivia.volume(radius=r_wet)
new_sd_extensive_attributes = {
"signed water mass": (v_wet * self.formulae.constants.rho_w,),
"dry volume": (0,),
"kappa times dry volume": (0,),
}

# TODO #1492: to be done once, not every time we spawn
self.check_extensive_attribute_keys(
particulator_attributes=self.particulator.attributes,
spawned_attributes=new_sd_extensive_attributes,
)

# TODO #1492: allocate once, reuse
new_sd_extensive_attributes = self.particulator.IndexedStorage.from_ndarray(
self.index,
np.asarray(list(new_sd_extensive_attributes.values())),
)
# TODO #1492: ditto
new_sd_multiplicity = self.particulator.IndexedStorage.from_ndarray(
self.index,
np.asarray((new_sd_multiplicity,), dtype=np.int64),
)

self.particulator.spawn(
spawned_particle_index=self.index,
number_of_super_particles_to_spawn=1,
spawned_particle_multiplicity=new_sd_multiplicity,
spawned_particle_extensive_attributes=new_sd_extensive_attributes,
)
# TODO #1492: subtract the water mass from ambient vapour
1 change: 1 addition & 0 deletions PySDM/dynamics/impl/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
"""stuff not intended to be imported from user code"""

from .register_dynamic import register_dynamic
from .super_particle_spawning_dynamic import SuperParticleSpawningDynamic
23 changes: 23 additions & 0 deletions PySDM/dynamics/impl/super_particle_spawning_dynamic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
"""common code for dynamics that spawn new super-droplets"""

from PySDM.impl.particle_attributes import ParticleAttributes


class SuperParticleSpawningDynamic: # pylint: disable=too-few-public-methods
"""base class for dynamics that spawn new super-droplets"""

@staticmethod
def check_extensive_attribute_keys(
particulator_attributes: ParticleAttributes,
spawned_attributes: dict,
):
"""checks if keys (and their order) in user-supplied `spawned_attributes` match
attributes used in the `particulator_attributes`"""
if tuple(particulator_attributes.get_extensive_attribute_keys()) != tuple(
spawned_attributes.keys()
):
raise ValueError(
f"extensive attributes ({spawned_attributes.keys()})"
" do not match those used in particulator"
f" ({particulator_attributes.get_extensive_attribute_keys()})"
)
26 changes: 11 additions & 15 deletions PySDM/dynamics/seeding.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@

from PySDM.dynamics.impl import register_dynamic
from PySDM.initialisation import discretise_multiplicities
from PySDM.dynamics.impl import SuperParticleSpawningDynamic


@register_dynamic()
class Seeding:
class Seeding(SuperParticleSpawningDynamic):
def __init__(
self,
*,
Expand All @@ -33,15 +34,10 @@ def register(self, builder):
self.particulator = builder.particulator

def post_register_setup_when_attributes_are_known(self):
if tuple(self.particulator.attributes.get_extensive_attribute_keys()) != tuple(
self.seeded_particle_extensive_attributes.keys()
):
raise ValueError(
f"extensive attributes ({self.seeded_particle_extensive_attributes.keys()})"
" do not match those used in particulator"
f" ({self.particulator.attributes.get_extensive_attribute_keys()})"
)

SuperParticleSpawningDynamic.check_extensive_attribute_keys(
particulator_attributes=self.particulator.attributes,
spawned_attributes=self.seeded_particle_extensive_attributes,
)
self.index = self.particulator.Index.identity_index(
len(self.seeded_particle_multiplicity)
)
Expand Down Expand Up @@ -86,9 +82,9 @@ def __call__(self):
# or if the number of super particles to inject
# is equal to the number of possible seeds
self.index.shuffle(self.u01)
self.particulator.seeding(
seeded_particle_index=self.index,
number_of_super_particles_to_inject=number_of_super_particles_to_inject,
seeded_particle_multiplicity=self.seeded_particle_multiplicity,
seeded_particle_extensive_attributes=self.seeded_particle_extensive_attributes,
self.particulator.spawn(
spawned_particle_index=self.index,
number_of_super_particles_to_spawn=number_of_super_particles_to_inject,
spawned_particle_multiplicity=self.seeded_particle_multiplicity,
spawned_particle_extensive_attributes=self.seeded_particle_extensive_attributes,
)
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
vapour_mixing_ratio = self["water_vapour_mixing_ratio"][0]
gg = (
1 - formulae.adiabatic_exponent.gamma(vapour_mixing_ratio)
) / formulae.adiabatic_exponent.gamma(vapour_mixing_ratio)
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][:]
Loading
Loading