Skip to content

Commit cea82de

Browse files
authored
introducing ice thawing to Freezing dynamic (still disabled by default) (#993)
1 parent 2b7f314 commit cea82de

File tree

7 files changed

+111
-21
lines changed

7 files changed

+111
-21
lines changed

PySDM/backends/impl_numba/methods/freezing_methods.py

+35-10
Original file line numberDiff line numberDiff line change
@@ -18,22 +18,37 @@ def __init__(self):
1818
BackendMethods.__init__(self)
1919
const = self.formulae.constants
2020
unfrozen_and_saturated = self.formulae.trivia.unfrozen_and_saturated
21+
frozen_and_above_freezing_point = (
22+
self.formulae.trivia.frozen_and_above_freezing_point
23+
)
2124

2225
@numba.njit(
2326
**{**conf.JIT_FLAGS, "fastmath": self.formulae.fastmath, "parallel": False}
2427
)
2528
def _freeze(volume, i):
2629
volume[i] = -1 * volume[i] * const.rho_w / const.rho_i
2730
# TODO #599: change thd (latent heat)!
28-
# TODO #599: handle the negative volume in tests, attributes, products, dynamics, ...
31+
32+
@numba.njit(
33+
**{**conf.JIT_FLAGS, "fastmath": self.formulae.fastmath, "parallel": False}
34+
)
35+
def _thaw(volume, i):
36+
volume[i] = -1 * volume[i] * const.rho_i / const.rho_w
37+
# TODO #599: change thd (latent heat)!
2938

3039
@numba.njit(**{**conf.JIT_FLAGS, "fastmath": self.formulae.fastmath})
31-
def freeze_singular_body(attributes, temperature, relative_humidity, cell):
40+
def freeze_singular_body(
41+
attributes, temperature, relative_humidity, cell, thaw
42+
):
3243
n_sd = len(attributes.freezing_temperature)
3344
for i in numba.prange(n_sd): # pylint: disable=not-an-iterable
3445
if attributes.freezing_temperature[i] == 0:
3546
continue
36-
if (
47+
if thaw and frozen_and_above_freezing_point(
48+
attributes.wet_volume[i], temperature[cell[i]]
49+
):
50+
_thaw(attributes.wet_volume, i)
51+
elif (
3752
unfrozen_and_saturated(
3853
attributes.wet_volume[i], relative_humidity[cell[i]]
3954
)
@@ -56,13 +71,18 @@ def freeze_time_dependent_body( # pylint: disable=unused-argument,too-many-argu
5671
relative_humidity,
5772
record_freezing_temperature,
5873
freezing_temperature,
74+
thaw,
5975
):
6076
n_sd = len(attributes.wet_volume)
6177
for i in numba.prange(n_sd): # pylint: disable=not-an-iterable
6278
if attributes.immersed_surface_area[i] == 0:
6379
continue
6480
cell_id = cell[i]
65-
if unfrozen_and_saturated(
81+
if thaw and frozen_and_above_freezing_point(
82+
attributes.wet_volume[i], temperature[cell_id]
83+
):
84+
_thaw(attributes.wet_volume, i)
85+
elif unfrozen_and_saturated(
6686
attributes.wet_volume[i], relative_humidity[cell_id]
6787
):
6888
rate = j_het(a_w_ice[cell_id])
@@ -77,7 +97,9 @@ def freeze_time_dependent_body( # pylint: disable=unused-argument,too-many-argu
7797

7898
self.freeze_time_dependent_body = freeze_time_dependent_body
7999

80-
def freeze_singular(self, *, attributes, temperature, relative_humidity, cell):
100+
def freeze_singular(
101+
self, *, attributes, temperature, relative_humidity, cell, thaw: bool
102+
):
81103
self.freeze_singular_body(
82104
SingularAttributes(
83105
freezing_temperature=attributes.freezing_temperature.data,
@@ -86,6 +108,7 @@ def freeze_singular(self, *, attributes, temperature, relative_humidity, cell):
86108
temperature.data,
87109
relative_humidity.data,
88110
cell.data,
111+
thaw=thaw,
89112
)
90113

91114
def freeze_time_dependent(
@@ -99,7 +122,8 @@ def freeze_time_dependent(
99122
temperature,
100123
relative_humidity,
101124
record_freezing_temperature,
102-
freezing_temperature
125+
freezing_temperature,
126+
thaw: bool
103127
):
104128
self.freeze_time_dependent_body(
105129
rand.data,
@@ -110,10 +134,11 @@ def freeze_time_dependent(
110134
timestep,
111135
cell.data,
112136
a_w_ice.data,
113-
temperature.data if record_freezing_temperature else None,
137+
temperature.data,
114138
relative_humidity.data,
115139
record_freezing_temperature=record_freezing_temperature,
116-
freezing_temperature=freezing_temperature.data
117-
if record_freezing_temperature
118-
else None,
140+
freezing_temperature=(
141+
freezing_temperature.data if record_freezing_temperature else None
142+
),
143+
thaw=thaw,
119144
)

PySDM/backends/impl_thrust_rtc/methods/freezing_methods.py

+22-3
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,20 @@ def __init__(self):
2323
"cell",
2424
"a_w_ice",
2525
"relative_humidity",
26+
"thaw",
27+
"temperature",
2628
),
2729
name_iter="i",
2830
body=f"""
2931
if (immersed_surface_area[i] == 0) {{
3032
return;
3133
}}
32-
if ({self.formulae.trivia.unfrozen_and_saturated.c_inline(
34+
if (thaw && {self.formulae.trivia.frozen_and_above_freezing_point.c_inline(
35+
volume="wet_volume[i]",
36+
temperature="temperature[cell[i]]"
37+
)}) {{
38+
wet_volume[i] = -1 * wet_volume[i] * {const.rho_i} / {const.rho_w};
39+
}} else if ({self.formulae.trivia.unfrozen_and_saturated.c_inline(
3340
volume="wet_volume[i]",
3441
relative_humidity="relative_humidity[cell[i]]"
3542
)}) {{
@@ -55,13 +62,19 @@ def __init__(self):
5562
"temperature",
5663
"relative_humidity",
5764
"cell",
65+
"thaw",
5866
),
5967
name_iter="i",
6068
body=f"""
6169
if (freezing_temperature[i] == 0) {{
6270
return;
6371
}}
64-
if (
72+
if (thaw && {self.formulae.trivia.frozen_and_above_freezing_point.c_inline(
73+
volume="wet_volume[i]",
74+
temperature="temperature[cell[i]]"
75+
)}) {{
76+
wet_volume[i] = -1 * wet_volume[i] * {const.rho_i} / {const.rho_w};
77+
}} else if (
6578
{self.formulae.trivia.unfrozen_and_saturated.c_inline(
6679
volume="wet_volume[i]",
6780
relative_humidity="relative_humidity[cell[i]]"
@@ -75,7 +88,9 @@ def __init__(self):
7588
)
7689

7790
@nice_thrust(**NICE_THRUST_FLAGS)
78-
def freeze_singular(self, *, attributes, temperature, relative_humidity, cell):
91+
def freeze_singular(
92+
self, *, attributes, temperature, relative_humidity, cell, thaw
93+
):
7994
n_sd = len(attributes.freezing_temperature)
8095
self.freeze_singular_body.launch_n(
8196
n=n_sd,
@@ -85,6 +100,7 @@ def freeze_singular(self, *, attributes, temperature, relative_humidity, cell):
85100
temperature.data,
86101
relative_humidity.data,
87102
cell.data,
103+
trtc.DVBool(thaw),
88104
),
89105
)
90106

@@ -101,6 +117,7 @@ def freeze_time_dependent( # pylint: disable=unused-argument
101117
relative_humidity,
102118
record_freezing_temperature,
103119
freezing_temperature,
120+
thaw,
104121
):
105122
n_sd = len(attributes.immersed_surface_area)
106123
self.freeze_time_dependent_body.launch_n(
@@ -113,5 +130,7 @@ def freeze_time_dependent( # pylint: disable=unused-argument
113130
cell.data,
114131
a_w_ice.data,
115132
relative_humidity.data,
133+
trtc.DVBool(thaw),
134+
temperature.data,
116135
),
117136
)

PySDM/dynamics/freezing.py

+5-6
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,11 @@
99

1010

1111
class Freezing:
12-
def __init__(self, *, singular=True, record_freezing_temperature=False):
12+
def __init__(self, *, singular=True, record_freezing_temperature=False, thaw=False):
1313
assert not (record_freezing_temperature and singular)
1414
self.singular = singular
1515
self.record_freezing_temperature = record_freezing_temperature
16+
self.thaw = thaw
1617
self.enable = True
1718
self.rand = None
1819
self.rng = None
@@ -58,6 +59,7 @@ def __call__(self):
5859
temperature=self.particulator.environment["T"],
5960
relative_humidity=self.particulator.environment["RH"],
6061
cell=self.particulator.attributes["cell id"],
62+
thaw=self.thaw,
6163
)
6264
else:
6365
self.rand.urand(self.rng)
@@ -72,18 +74,15 @@ def __call__(self):
7274
timestep=self.particulator.dt,
7375
cell=self.particulator.attributes["cell id"],
7476
a_w_ice=self.particulator.environment["a_w_ice"],
75-
temperature=(
76-
self.particulator.environment["T"]
77-
if self.record_freezing_temperature
78-
else None
79-
),
77+
temperature=self.particulator.environment["T"],
8078
relative_humidity=self.particulator.environment["RH"],
8179
record_freezing_temperature=self.record_freezing_temperature,
8280
freezing_temperature=(
8381
self.particulator.attributes["freezing temperature"]
8482
if self.record_freezing_temperature
8583
else None
8684
),
85+
thaw=self.thaw,
8786
)
8887

8988
self.particulator.attributes.mark_updated("volume")

PySDM/physics/heterogeneous_ice_nucleation_rate/null.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,4 @@ def __init__(self, _):
1010

1111
@staticmethod
1212
def j_het(const, a_w_ice):
13-
return const.NaN
13+
return 0

PySDM/physics/trivia.py

+4
Original file line numberDiff line numberDiff line change
@@ -75,3 +75,7 @@ def th_std(const, p, T):
7575
@staticmethod
7676
def unfrozen_and_saturated(_, volume, relative_humidity):
7777
return volume > 0 and relative_humidity > 1
78+
79+
@staticmethod
80+
def frozen_and_above_freezing_point(const, volume, temperature):
81+
return volume < 0 and temperature > const.T0

test-time-requirements.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ ghapi
66
pytest
77

88
# note: if cloning both PySDM and PySDM examples, consider "pip install -e"
9-
PySDM-examples @ git+https://github.com/open-atmos/PySDM-examples@151a869#egg=PySDM-examples
9+
PySDM-examples @ git+https://github.com/open-atmos/PySDM-examples@885b82a#egg=PySDM-examples
1010
PyMPDATA @ git+https://github.com/open-atmos/PyMPDATA@63f1c11#egg=PyMPDATA
1111

1212
PyPartMC==0.0.24

tests/unit_tests/backends/test_freezing_methods.py

+43
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# pylint: disable=missing-module-docstring,missing-class-docstring,missing-function-docstring
22
import numpy as np
3+
import pytest
34
from matplotlib import pyplot
45

56
from PySDM import Builder, Formulae
@@ -23,6 +24,47 @@ def test_record_freezing_temperature_on_time_dependent_freeze(self):
2324
def test_no_subsaturated_freezing(self):
2425
pass
2526

27+
@staticmethod
28+
@pytest.mark.parametrize("singular", (True, False))
29+
@pytest.mark.parametrize("thaw", (True, False))
30+
@pytest.mark.parametrize("epsilon", (0, 1e-5))
31+
# pylint: disable=redefined-outer-name
32+
def test_thaw(backend_class, singular, thaw, epsilon):
33+
# arrange
34+
formulae = Formulae()
35+
builder = Builder(n_sd=1, backend=backend_class(formulae=formulae))
36+
env = Box(dt=1 * si.s, dv=1 * si.m**3)
37+
builder.set_environment(env)
38+
builder.add_dynamic(Freezing(singular=singular, thaw=thaw))
39+
particulator = builder.build(
40+
products=(IceWaterContent(),),
41+
attributes={
42+
"n": np.ones(builder.particulator.n_sd),
43+
"volume": -1 * np.ones(builder.particulator.n_sd) * si.um**3,
44+
**(
45+
{"freezing temperature": np.full(builder.particulator.n_sd, -1)}
46+
if singular
47+
else {
48+
"immersed surface area": np.full(builder.particulator.n_sd, -1)
49+
}
50+
),
51+
},
52+
)
53+
env["T"] = formulae.constants.T0 + epsilon
54+
env["RH"] = np.nan
55+
if not singular:
56+
env["a_w_ice"] = np.nan
57+
assert particulator.products["ice water content"].get() > 0
58+
59+
# act
60+
particulator.run(steps=1)
61+
62+
# assert
63+
if thaw and epsilon > 0:
64+
assert particulator.products["ice water content"].get() == 0
65+
else:
66+
assert particulator.products["ice water content"].get() > 0
67+
2668
@staticmethod
2769
# pylint: disable=redefined-outer-name
2870
def test_freeze_singular(backend_class):
@@ -121,6 +163,7 @@ def low(t):
121163
particulator = builder.build(attributes=attributes, products=products)
122164
env["RH"] = 1.0001
123165
env["a_w_ice"] = np.nan
166+
env["T"] = np.nan
124167

125168
cell_id = 0
126169
for i in range(int(total_time / case["dt"]) + 1):

0 commit comments

Comments
 (0)