-
Notifications
You must be signed in to change notification settings - Fork 46
homogeneous nucleation of droplets & expansion chamber examples (Clare's branch) #1491
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
Changes from all commits
ca90206
f716859
f41866b
82aca13
58f5e14
5f4dc31
5d1be16
d0b56aa
c98eaab
099e7ca
b141f02
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
""" | ||
homogeneous liquid droplet nucleation rate formulations | ||
""" | ||
|
||
from .cnt import CNT | ||
from .constant import Constant | ||
from .null import Null |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
""" | ||
classical nucleation theory (CNT) | ||
formulae from Seinfeld and Pandis Eq (11.47) and (11.52) | ||
""" | ||
|
||
import numpy as np | ||
|
||
|
||
class CNT: | ||
def __init__(self, _): | ||
return | ||
|
||
@staticmethod | ||
def j_liq_homo(const, T, S, e_s): | ||
m1 = const.Mv / const.N_A # kg per molecule | ||
v1 = m1 / const.rho_w # m3 per molecule | ||
N1 = (S * e_s) / (m1 * const.Rv * T) # molecules per m3 | ||
return ( | ||
((2 * const.sgm_w) / (np.pi * m1)) ** (1 / 2) | ||
* (v1 * N1**2 / S) | ||
* np.exp( | ||
(-16 * np.pi * v1**2 * const.sgm_w**3) | ||
/ (3 * const.k_B**3 * T**3 * np.log(S) ** 2) | ||
) | ||
) | ||
|
||
@staticmethod | ||
def r_liq_homo(const, T, S): | ||
m1 = const.Mv / const.N_A # kg per molecule | ||
v1 = m1 / const.rho_w # m3 per molecule | ||
return (2 * const.sgm_w * v1) / (const.k_B * T * np.log(S)) |
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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
""" | ||
do-nothing null formulation (needed as other formulations require parameters | ||
to be set before instantiation of Formulae) | ||
""" | ||
|
||
|
||
class Null: # pylint: disable=unused-argument | ||
def __init__(self, _): | ||
pass | ||
|
||
@staticmethod | ||
def j_liq_homo(const, T, S, e_s): | ||
return 0 | ||
|
||
@staticmethod | ||
def r_liq_homo(const, T, S): | ||
return 0 |
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 | ||
""" |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
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.Lognormal( | ||
norm_factor=N, | ||
m_mode=158 * si.nm, | ||
s_geom=2, | ||
), | ||
}, | ||
) | ||
# mean diameter 316nm, standard deviation 257nm | ||
# not sure how to interpret the standard deviation given in the paper | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Which paper? :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
# because it looks like it's a lognormal distribution in Fig 2 |
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,161 @@ | ||
from collections import namedtuple | ||
|
||
import numpy as np | ||
from PySDM import Builder | ||
from PySDM import products as PySDM_products | ||
from PySDM.backends import CPU | ||
from PySDM.dynamics import AmbientThermodynamics, Condensation | ||
from PySDM.environments import Parcel | ||
from PySDM.initialisation import equilibrate_wet_radii | ||
from PySDM.initialisation.sampling.spectral_sampling import ConstantMultiplicity | ||
from PySDM.physics import si | ||
|
||
|
||
def run_expansion( | ||
formulae, | ||
aerosol, | ||
n_sd_per_mode, | ||
RH0=0.7, | ||
T0=296 * si.K, | ||
p0=1000 * si.hPa, | ||
pf=500 * si.hPa, | ||
dz=10 * si.m, | ||
t_lift=2 * si.s, | ||
t_max=4 * si.s, | ||
): | ||
|
||
# calculate w, dt, n_steps | ||
H = 8500 * si.m # atmospheric scale height | ||
z_lift = -H * np.log(pf / p0) | ||
w_lift = z_lift / t_lift | ||
w = lambda t: w_lift if t < t_lift else 1e-9 | ||
dt = dz / w_lift | ||
n_steps = int(np.ceil(t_max / dt)) | ||
# print(f"z_lift={z_lift}") | ||
# print(f"w_lift={w_lift}") | ||
# print(f"dz={dz}") | ||
# print(f"dt={dt}") | ||
# print(f"n_steps={n_steps}") | ||
|
||
dry_radius_bin_edges = np.geomspace(50 * si.nm, 2000 * si.nm, 40, endpoint=False) | ||
wet_radius_bin_edges = np.geomspace(1 * si.um, 40 * si.um, 40, endpoint=False) | ||
products = ( | ||
PySDM_products.WaterMixingRatio(unit="g/kg", name="liquid_water_mixing_ratio"), | ||
PySDM_products.PeakSupersaturation(name="s"), | ||
PySDM_products.AmbientRelativeHumidity(name="RH"), | ||
PySDM_products.AmbientTemperature(name="T"), | ||
PySDM_products.AmbientPressure(name="p"), | ||
PySDM_products.AmbientWaterVapourMixingRatio( | ||
unit="g/kg", name="water_vapour_mixing_ratio" | ||
), | ||
PySDM_products.ParcelDisplacement(name="z"), | ||
PySDM_products.Time(name="t"), | ||
PySDM_products.ParticleSizeSpectrumPerVolume( | ||
name="dry:dN/dR", | ||
unit="m^-3 m^-1", | ||
radius_bins_edges=dry_radius_bin_edges, | ||
dry=True, | ||
), | ||
PySDM_products.ParticleSizeSpectrumPerVolume( | ||
name="wet:dN/dR", | ||
unit="m^-3 m^-1", | ||
radius_bins_edges=wet_radius_bin_edges, | ||
dry=False, | ||
), | ||
PySDM_products.ActivatedEffectiveRadius( | ||
name="reff", unit="um", count_activated=True, count_unactivated=False | ||
), | ||
) | ||
|
||
const = formulae.constants | ||
pv0 = RH0 * formulae.saturation_vapour_pressure.pvs_water(T0) | ||
|
||
env = Parcel( | ||
dt=dt, | ||
mass_of_dry_air=1e3 * si.kg, | ||
p0=p0, | ||
initial_water_vapour_mixing_ratio=const.eps * pv0 / (p0 - pv0), | ||
w=w, | ||
T0=T0, | ||
) | ||
|
||
n_sd = n_sd_per_mode * len(aerosol.modes) | ||
|
||
builder = Builder(backend=CPU(formulae), n_sd=n_sd, environment=env) | ||
builder.add_dynamic(AmbientThermodynamics()) | ||
builder.add_dynamic(Condensation()) | ||
Comment on lines
+85
to
+86
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We could add here the seeding dynamic (after renaming it as injection?). Inside the seeding dynamic, the super-droplet injection rate function would need to receive additional argument: an environment instance which can be queried for T and S with |
||
builder.request_attribute("critical supersaturation") | ||
|
||
attributes = { | ||
k: np.empty(0) for k in ("dry volume", "kappa times dry volume", "multiplicity") | ||
} | ||
for i, mode in enumerate(aerosol.modes): | ||
kappa, spectrum = mode["kappa"]["Constant"], mode["spectrum"] | ||
r_dry, concentration = ConstantMultiplicity(spectrum).sample(n_sd_per_mode) | ||
v_dry = builder.formulae.trivia.volume(radius=r_dry) | ||
specific_concentration = concentration / builder.formulae.constants.rho_STP | ||
attributes["multiplicity"] = np.append( | ||
attributes["multiplicity"], | ||
specific_concentration * builder.particulator.environment.mass_of_dry_air, | ||
) | ||
attributes["dry volume"] = np.append(attributes["dry volume"], v_dry) | ||
attributes["kappa times dry volume"] = np.append( | ||
attributes["kappa times dry volume"], v_dry * kappa | ||
) | ||
|
||
r_wet = equilibrate_wet_radii( | ||
r_dry=builder.formulae.trivia.radius(volume=attributes["dry volume"]), | ||
environment=builder.particulator.environment, | ||
kappa_times_dry_volume=attributes["kappa times dry volume"], | ||
) | ||
attributes["volume"] = builder.formulae.trivia.volume(radius=r_wet) | ||
|
||
particulator = builder.build(attributes, products=products) | ||
|
||
output = {product.name: [] for product in particulator.products.values()} | ||
output_attributes = { | ||
"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)), | ||
} | ||
|
||
for _ in range(n_steps): | ||
particulator.run(steps=1) | ||
for product in particulator.products.values(): | ||
if product.name == "dry:dN/dR" or product.name == "wet:dN/dR": | ||
continue | ||
value = product.get() | ||
if product.name == "t": | ||
output[product.name].append(value) | ||
else: | ||
output[product.name].append(value[0]) | ||
for key, attr in output_attributes.items(): | ||
attr_data = particulator.attributes[key].to_ndarray() | ||
for drop_id in range(particulator.n_sd): | ||
attr[drop_id].append(attr_data[drop_id]) | ||
|
||
dry_spectrum = particulator.products["dry:dN/dR"].get() | ||
wet_spectrum = particulator.products["wet:dN/dR"].get() | ||
|
||
Output = namedtuple( | ||
"Output", | ||
[ | ||
"profile", | ||
"attributes", | ||
"aerosol", | ||
"dry_radius_bin_edges", | ||
"dry_spectrum", | ||
"wet_radius_bin_edges", | ||
"wet_spectrum", | ||
], | ||
) | ||
return Output( | ||
profile=output, | ||
attributes=output_attributes, | ||
aerosol=aerosol, | ||
dry_radius_bin_edges=dry_radius_bin_edges, | ||
dry_spectrum=dry_spectrum, | ||
wet_radius_bin_edges=wet_radius_bin_edges, | ||
wet_spectrum=wet_spectrum, | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the unit tests have not been yet committed