Skip to content

Commit b688e1b

Browse files
tluettmslayoo
andauthored
new attribute: freezing supersaturation (recorded for the last freezing event) (#1878)
Co-authored-by: Sylwester Arabas <sylwester.arabas@agh.edu.pl>
1 parent 61c04c7 commit b688e1b

7 files changed

Lines changed: 201 additions & 60 deletions

File tree

PySDM/attributes/ice/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@
44

55
from .cooling_rate import CoolingRate
66
from .freezing_temperature import FreezingTemperature
7+
from .freezing_supersaturation import SupersaturationOfLastFreezing
78
from .immersed_surface_area import ImmersedSurfaceArea
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
"""
2+
particle freezing supersaturation wrt ice
3+
"""
4+
5+
from PySDM.attributes.impl import register_attribute
6+
from ..impl import DerivedAttribute
7+
8+
9+
@register_attribute()
10+
class SupersaturationOfLastFreezing(DerivedAttribute):
11+
"""time-dependent variant: assigned upon freezing"""
12+
13+
def __init__(self, builder):
14+
assert "Freezing" in builder.particulator.dynamics
15+
assert (
16+
builder.particulator.dynamics["Freezing"].immersion_freezing != "singular"
17+
)
18+
self.signed_water_mass = builder.get_attribute("signed water mass")
19+
self.cell_id = builder.get_attribute("cell id")
20+
super().__init__(
21+
builder,
22+
name="supersaturation of last freezing",
23+
dependencies=(self.signed_water_mass, self.cell_id),
24+
)
25+
builder.particulator.observers.append(self)
26+
27+
def notify(self):
28+
self.update()
29+
30+
def recalculate(self):
31+
self.particulator.backend.record_freezing_supersaturations(
32+
data=self.data,
33+
cell_id=self.cell_id.data,
34+
relative_humidity_ice=self.particulator.environment["RH_ice"],
35+
signed_water_mass=self.signed_water_mass.data,
36+
)

PySDM/backends/impl_numba/methods/freezing_methods.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,3 +299,29 @@ def record_freezing_temperatures(
299299
temperature=temperature.data,
300300
signed_water_mass=signed_water_mass.data,
301301
)
302+
303+
@cached_property
304+
def _record_freezing_supersaturations_body(self):
305+
ff = self.formulae_flattened
306+
307+
@numba.njit(**{**self.default_jit_flags, "fastmath": False})
308+
def body(data, cell_id, relative_humidity_ice, signed_water_mass):
309+
for drop_id in numba.prange(len(data)): # pylint: disable=not-an-iterable
310+
if ff.trivia__unfrozen(signed_water_mass[drop_id]):
311+
if data[drop_id] > 0:
312+
data[drop_id] = np.nan
313+
else:
314+
if np.isnan(data[drop_id]):
315+
data[drop_id] = relative_humidity_ice[cell_id[drop_id]]
316+
317+
return body
318+
319+
def record_freezing_supersaturations(
320+
self, *, data, cell_id, relative_humidity_ice, signed_water_mass
321+
):
322+
self._record_freezing_supersaturations_body(
323+
data=data.data,
324+
cell_id=cell_id.data,
325+
relative_humidity_ice=relative_humidity_ice.data,
326+
signed_water_mass=signed_water_mass.data,
327+
)

examples/PySDM_examples/Luettmer_homogeneous_freezing/plot.py

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,15 @@
1616

1717

1818
def cumulative_histogram(data, bins, reverse=False, density=True):
19-
# Compute regular histogram using given bins
2019
hist, bin_edges = np.histogram(data, bins=bins, density=False)
2120

22-
# Cumulative sum
2321
if reverse:
2422
cum_hist = np.cumsum(hist[::-1])[::-1]
2523
cum_hist_0 = cum_hist[0]
2624
else:
2725
cum_hist = np.cumsum(hist)
2826
cum_hist_0 = cum_hist[-1]
2927

30-
# Normalize
3128
if density:
3229
cum_hist = cum_hist / cum_hist_0
3330

@@ -56,8 +53,7 @@ def plot_thermodynamics_and_bulk(
5653
qv = np.asarray(output["qv"])
5754
T_frz = np.asarray(output["T_frz"])
5855
if show_conc:
59-
nc = np.asarray(output["ns"])
60-
ni = np.asarray(output["ni"])
56+
nc, ni = np.asarray(output["ns"]), np.asarray(output["ni"])
6157

6258
if t_lim is None:
6359
t_lim = np.amax(time)
@@ -67,8 +63,7 @@ def plot_thermodynamics_and_bulk(
6763
first_T_frz = T[first_ice_idx]
6864

6965
if not show_tf:
70-
rc = np.asarray(output["rs"])
71-
ri = np.asarray(output["ri"])
66+
rc, ri = np.asarray(output["rs"]), np.asarray(output["ri"])
7267

7368
if show_jhom:
7469
svp = Formulae(
@@ -90,9 +85,7 @@ def plot_thermodynamics_and_bulk(
9085
radius = np.asarray(output["radius"])
9186
multiplicity = np.asarray(output["multiplicity"])
9287

93-
_, axs = pyplot.subplots(
94-
1, 4, figsize=(20, 5), sharex=False, constrained_layout=True
95-
)
88+
_, axs = pyplot.subplots(1, 4, figsize=(20, 5), constrained_layout=True)
9689

9790
# Temperture profile
9891
iax = 0
@@ -247,30 +240,40 @@ def plot_thermodynamics_and_bulk(
247240
ax.grid(visible=True)
248241

249242

250-
def plot_freezing_temperatures_histogram(ax, simulation):
243+
def plot_freezing_temperatures_histogram(ax, simulation, plot_rhi=False):
251244

252245
number_of_ensemble_runs = simulation["settings"]["number_of_ensemble_runs"]
253246

254247
for i in range(number_of_ensemble_runs):
255248
output = simulation["ensemble_member_outputs"][i]
256-
T_frz = np.asarray(output["T_frz"])
249+
if plot_rhi:
250+
var = np.asarray(output["RHi_frz"])
251+
bins = np.linspace(1, 1.6, num=60, endpoint=True)
252+
cumulative = 1
253+
else:
254+
var = np.asarray(output["T_frz"])
255+
bins = T_frz_bins_kelvin
256+
cumulative = -1
257257
title = "Nucleation rate=" + simulation["settings"]["hom_freezing"]
258258

259-
# Freezing temperatures
260259
ax.hist(
261-
T_frz,
262-
bins=T_frz_bins_kelvin,
260+
var,
261+
bins=bins,
263262
density=True,
264-
cumulative=-1,
263+
cumulative=cumulative,
265264
alpha=1.0,
266265
histtype="step",
267266
linewidth=1.5,
268267
)
269268

270-
ax.set_xlim(left=234, right=239)
271-
ax.axvline(x=235, color="k", linestyle="--")
269+
if plot_rhi:
270+
ax.set_xlim(left=1.0, right=1.6)
271+
ax.set_xlabel("freezing supersaturation wrt ice", fontsize=ax_lab_fsize)
272+
else:
273+
ax.set_xlim(left=234, right=239)
274+
ax.axvline(x=235, color="k", linestyle="--")
275+
ax.set_xlabel("freezing temperature [K]", fontsize=ax_lab_fsize)
272276
ax.set_title(title, fontsize=ax_lab_fsize)
273-
ax.set_xlabel("freezing temperature [K]", fontsize=ax_lab_fsize)
274277
ax.set_ylabel("frozen fraction", fontsize=ax_lab_fsize)
275278
ax.tick_params(labelsize=tick_fsize)
276279
return ax

examples/PySDM_examples/Luettmer_homogeneous_freezing/simple_homogenous_freezing_example.ipynb

Lines changed: 67 additions & 17 deletions
Large diffs are not rendered by default.

examples/PySDM_examples/Luettmer_homogeneous_freezing/simulation.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ def __init__(self, settings):
7979
"signed water mass": self.initial_mass,
8080
}
8181
builder.request_attribute("temperature of last freezing")
82+
builder.request_attribute("supersaturation of last freezing")
8283
builder.request_attribute("radius")
8384
builder.request_attribute("wet to critical volume ratio")
8485

@@ -140,6 +141,9 @@ def save(self, output):
140141
output["T_frz"] = self.particulator.attributes[
141142
"temperature of last freezing"
142143
].data.tolist()
144+
output["RHi_frz"] = self.particulator.attributes[
145+
"supersaturation of last freezing"
146+
].data.tolist()
143147
if not output["radius"]:
144148
output["radius"] = self.particulator.attributes["radius"].data.tolist()
145149
output["multiplicity"] = self.particulator.attributes[
@@ -153,6 +157,7 @@ def run(self):
153157

154158
output = {
155159
"T_frz": [],
160+
"RHi_frz": [],
156161
"radius": [],
157162
"multiplicity": [],
158163
}

tests/unit_tests/dynamics/test_freezing.py

Lines changed: 44 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ def test_record_freezing_temperature_on_time_dependent_freeze(
3838
pytest.skip("TODO #1495")
3939

4040
# arrange
41+
attr_names = (
42+
"temperature of last freezing",
43+
"supersaturation of last freezing",
44+
)
4145
formulae = Formulae(
4246
particle_shape_and_density="MixedPhaseSpheres",
4347
heterogeneous_ice_nucleation_rate="Constant",
@@ -52,15 +56,18 @@ def test_record_freezing_temperature_on_time_dependent_freeze(
5256
backend=backend_class(formulae=formulae),
5357
environment=Box(dt=1 * si.s, dv=1 * si.m**3),
5458
dynamics=(
55-
Freezing(
56-
immersion_freezing=freezing_mode["immersion_freezing"],
57-
homogeneous_freezing=freezing_mode["homogeneous_freezing"],
58-
thaw="instantaneous",
59-
),
59+
[
60+
Freezing(
61+
immersion_freezing=freezing_mode["immersion_freezing"],
62+
homogeneous_freezing=freezing_mode["homogeneous_freezing"],
63+
thaw="instantaneous",
64+
)
65+
]
6066
),
6167
)
6268
if record_freezing_temperature:
63-
builder.request_attribute("temperature of last freezing")
69+
for attr_name in attr_names:
70+
builder.request_attribute(attr_name)
6471
particulator = builder.build(
6572
attributes={
6673
"multiplicity": np.asarray([1]),
@@ -69,46 +76,59 @@ def test_record_freezing_temperature_on_time_dependent_freeze(
6976
}
7077
)
7178

72-
temp_1 = 200 * si.K
73-
temp_2 = 250 * si.K
74-
temp_3 = 220 * si.K
79+
temp_list = np.asarray([200 * si.K, 300 * si.K, 220 * si.K])
80+
rh_list = np.asarray([0.9, 1.0, 1.1])
7581
particulator.environment["a_w_ice"] = np.nan
76-
particulator.environment["T"] = temp_2
82+
particulator.environment["T"] = temp_list[1]
83+
particulator.environment["RH_ice"] = rh_list[0]
7784

7885
# act & assert
79-
attr_name = "temperature of last freezing"
8086
if not record_freezing_temperature:
81-
assert attr_name not in particulator.attributes
87+
for attr_name in attr_names:
88+
assert attr_name not in particulator.attributes
8289
else:
8390
# never frozen yet
84-
np.isnan(particulator.attributes[attr_name].to_ndarray()).all()
91+
for attr_name in attr_names:
92+
np.isnan(particulator.attributes[attr_name].to_ndarray()).all()
8593

8694
# still not frozen since RH not greater than 100%
87-
particulator.environment["RH"] = 1.0
88-
particulator.environment["RH_ice"] = 1.0
95+
particulator.environment["RH"] = rh_list[1]
96+
particulator.environment["RH_ice"] = rh_list[1]
8997
particulator.run(steps=1)
90-
np.isnan(particulator.attributes[attr_name].to_ndarray()).all()
98+
for attr_name in attr_names:
99+
np.isnan(particulator.attributes[attr_name].to_ndarray()).all()
91100
assert all(particulator.attributes["signed water mass"].to_ndarray() > 0)
92101

93102
# should freeze and record T1
94-
particulator.environment["RH"] += EPSILON_RH
95-
particulator.environment["RH_ice"] += EPSILON_RH
96-
particulator.environment["T"] = temp_1
103+
particulator.environment["RH"] = rh_list[2]
104+
particulator.environment["RH_ice"] = rh_list[2]
105+
particulator.environment["T"] = temp_list[0]
97106
particulator.run(steps=1)
98107
assert all(particulator.attributes["signed water mass"].to_ndarray() < 0)
99-
assert all(particulator.attributes[attr_name].to_ndarray() == temp_1)
108+
assert all(
109+
particulator.attributes[attr_names[0]].to_ndarray() == temp_list[0]
110+
)
111+
assert all(
112+
particulator.attributes[attr_names[1]].to_ndarray() == rh_list[2]
113+
)
100114

101115
# should thaw
102-
particulator.environment["T"] = 300 * si.K
116+
particulator.environment["T"] = temp_list[1]
103117
particulator.run(steps=1)
104118
assert all(particulator.attributes["signed water mass"].to_ndarray() > 0)
105-
np.isnan(particulator.attributes[attr_name].to_ndarray()).all()
119+
for attr_name in attr_names:
120+
np.isnan(particulator.attributes[attr_name].to_ndarray()).all()
106121

107122
# should re-freeze and record T2
108-
particulator.environment["T"] = temp_3
123+
particulator.environment["T"] = temp_list[2]
109124
particulator.run(steps=1)
110125
assert all(particulator.attributes["signed water mass"].to_ndarray() < 0)
111-
assert all(particulator.attributes[attr_name].to_ndarray() == temp_3)
126+
assert all(
127+
particulator.attributes[attr_names[0]].to_ndarray() == temp_list[2]
128+
)
129+
assert all(
130+
particulator.attributes[attr_names[1]].to_ndarray() == rh_list[2]
131+
)
112132

113133
# TODO #599
114134
def test_no_subsaturated_freezing(self):

0 commit comments

Comments
 (0)