Skip to content

Commit d175107

Browse files
authored
recording temperature of last freezing for time-dependent immersion freezing (#1566)
1 parent 134b33c commit d175107

File tree

6 files changed

+150
-40
lines changed

6 files changed

+150
-40
lines changed
+32-2
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,42 @@
11
"""
2-
particle freezing temperature (assigned at initialisation, modified through collisions only,
3-
used in singular regime)
2+
particle freezing temperature
43
"""
54

65
from PySDM.attributes.impl import MaximumAttribute, register_attribute
6+
from ..impl import DerivedAttribute
77

88

99
@register_attribute()
1010
class FreezingTemperature(MaximumAttribute):
11+
"""singular variant: assigned at initialisation, modified through collisions only"""
12+
1113
def __init__(self, builder):
1214
super().__init__(builder, name="freezing temperature")
15+
16+
17+
@register_attribute()
18+
class TemperatureOfLastFreezing(DerivedAttribute):
19+
"""time-dependent variant: assigned upon freezing"""
20+
21+
def __init__(self, builder):
22+
assert "Freezing" in builder.particulator.dynamics
23+
assert not builder.particulator.dynamics["Freezing"].singular
24+
self.signed_water_mass = builder.get_attribute("signed water mass")
25+
self.cell_id = builder.get_attribute("cell id")
26+
super().__init__(
27+
builder,
28+
name="temperature of last freezing",
29+
dependencies=(self.signed_water_mass, self.cell_id),
30+
)
31+
builder.particulator.observers.append(self)
32+
33+
def notify(self):
34+
self.update()
35+
36+
def recalculate(self):
37+
self.particulator.backend.record_freezing_temperatures(
38+
data=self.data,
39+
cell_id=self.cell_id.data,
40+
temperature=self.particulator.environment["T"],
41+
signed_water_mass=self.signed_water_mass.data,
42+
)

PySDM/backends/impl_numba/methods/freezing_methods.py

+30-11
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22
CPU implementation of backend methods for freezing (singular and time-dependent immersion freezing)
33
"""
44

5+
from functools import cached_property
6+
57
import numba
8+
import numpy as np
69

710
from PySDM.backends.impl_common.backend_methods import BackendMethods
811

@@ -56,16 +59,14 @@ def freeze_singular_body(
5659
prob_zero_events = self.formulae.trivia.poissonian_avoidance_function
5760

5861
@numba.njit(**self.default_jit_flags)
59-
def freeze_time_dependent_body( # pylint: disable=unused-argument,too-many-arguments
62+
def freeze_time_dependent_body( # pylint: disable=too-many-arguments
6063
rand,
6164
attributes,
6265
timestep,
6366
cell,
6467
a_w_ice,
6568
temperature,
6669
relative_humidity,
67-
record_freezing_temperature,
68-
freezing_temperature,
6970
thaw,
7071
):
7172
n_sd = len(attributes.signed_water_mass)
@@ -88,8 +89,6 @@ def freeze_time_dependent_body( # pylint: disable=unused-argument,too-many-argu
8889
)
8990
if rand[i] < prob:
9091
_freeze(attributes.signed_water_mass, i)
91-
# if record_freezing_temperature:
92-
# freezing_temperature[i] = temperature[cell_id]
9392

9493
self.freeze_time_dependent_body = freeze_time_dependent_body
9594

@@ -117,8 +116,6 @@ def freeze_time_dependent(
117116
a_w_ice,
118117
temperature,
119118
relative_humidity,
120-
record_freezing_temperature,
121-
freezing_temperature,
122119
thaw: bool
123120
):
124121
self.freeze_time_dependent_body(
@@ -132,9 +129,31 @@ def freeze_time_dependent(
132129
a_w_ice.data,
133130
temperature.data,
134131
relative_humidity.data,
135-
record_freezing_temperature=record_freezing_temperature,
136-
freezing_temperature=(
137-
freezing_temperature.data if record_freezing_temperature else None
138-
),
139132
thaw=thaw,
140133
)
134+
135+
@cached_property
136+
def _record_freezing_temperatures_body(self):
137+
ff = self.formulae_flattened
138+
139+
@numba.njit(**{**self.default_jit_flags, "fastmath": False})
140+
def body(data, cell_id, temperature, signed_water_mass):
141+
for drop_id in numba.prange(len(data)): # pylint: disable=not-an-iterable
142+
if ff.trivia__unfrozen(signed_water_mass[drop_id]):
143+
if data[drop_id] > 0:
144+
data[drop_id] = np.nan
145+
else:
146+
if np.isnan(data[drop_id]):
147+
data[drop_id] = temperature[cell_id[drop_id]]
148+
149+
return body
150+
151+
def record_freezing_temperatures(
152+
self, *, data, cell_id, temperature, signed_water_mass
153+
):
154+
self._record_freezing_temperatures_body(
155+
data=data.data,
156+
cell_id=cell_id.data,
157+
temperature=temperature.data,
158+
signed_water_mass=signed_water_mass.data,
159+
)

PySDM/backends/impl_thrust_rtc/methods/freezing_methods.py

-2
Original file line numberDiff line numberDiff line change
@@ -118,8 +118,6 @@ def freeze_time_dependent( # pylint: disable=unused-argument
118118
a_w_ice,
119119
temperature,
120120
relative_humidity,
121-
record_freezing_temperature,
122-
freezing_temperature,
123121
thaw,
124122
):
125123
n_sd = len(attributes.immersed_surface_area)

PySDM/dynamics/freezing.py

+5-12
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,13 @@
22
immersion freezing using either singular or time-dependent formulation
33
"""
44

5-
from PySDM.physics.heterogeneous_ice_nucleation_rate import Null
65
from PySDM.dynamics.impl import register_dynamic
76

87

98
@register_dynamic()
109
class Freezing:
11-
def __init__(self, *, singular=True, record_freezing_temperature=False, thaw=False):
12-
assert not (record_freezing_temperature and singular)
10+
def __init__(self, *, singular=True, thaw=False):
1311
self.singular = singular
14-
self.record_freezing_temperature = record_freezing_temperature
1512
self.thaw = thaw
1613
self.enable = True
1714
self.rand = None
@@ -26,12 +23,13 @@ def register(self, builder):
2623
)
2724

2825
builder.request_attribute("signed water mass")
29-
if self.singular or self.record_freezing_temperature:
26+
if self.singular:
3027
builder.request_attribute("freezing temperature")
3128

3229
if not self.singular:
33-
assert not isinstance(
34-
self.particulator.formulae.heterogeneous_ice_nucleation_rate, Null
30+
assert (
31+
self.particulator.formulae.heterogeneous_ice_nucleation_rate.__name__
32+
!= "Null"
3533
)
3634
builder.request_attribute("immersed surface area")
3735
self.rand = self.particulator.Storage.empty(
@@ -57,10 +55,5 @@ def __call__(self):
5755
self.rand.urand(self.rng)
5856
self.particulator.immersion_freezing_time_dependent(
5957
rand=self.rand,
60-
record_freezing_temperature=self.record_freezing_temperature,
6158
thaw=self.thaw,
6259
)
63-
64-
self.particulator.attributes.mark_updated("signed water mass")
65-
if self.record_freezing_temperature:
66-
self.particulator.attributes.mark_updated("freezing temperature")

PySDM/particulator.py

+3-9
Original file line numberDiff line numberDiff line change
@@ -509,9 +509,7 @@ def deposition(self):
509509
# TODO #1524 - should we update here?
510510
# self.update_TpRH(only_if_not_last='VapourDepositionOnIce')
511511

512-
def immersion_freezing_time_dependent(
513-
self, *, thaw: bool, record_freezing_temperature: bool, rand: Storage
514-
):
512+
def immersion_freezing_time_dependent(self, *, thaw: bool, rand: Storage):
515513
self.backend.freeze_time_dependent(
516514
rand=rand,
517515
attributes=TimeDependentAttributes(
@@ -523,14 +521,9 @@ def immersion_freezing_time_dependent(
523521
a_w_ice=self.environment["a_w_ice"],
524522
temperature=self.environment["T"],
525523
relative_humidity=self.environment["RH"],
526-
record_freezing_temperature=record_freezing_temperature,
527-
freezing_temperature=(
528-
self.attributes["freezing temperature"]
529-
if record_freezing_temperature
530-
else None
531-
),
532524
thaw=thaw,
533525
)
526+
self.attributes.mark_updated("signed water mass")
534527

535528
def immersion_freezing_singular(self, *, thaw: bool):
536529
self.backend.freeze_singular(
@@ -543,3 +536,4 @@ def immersion_freezing_singular(self, *, thaw: bool):
543536
cell=self.attributes["cell id"],
544537
thaw=thaw,
545538
)
539+
self.attributes.mark_updated("signed water mass")

tests/unit_tests/dynamics/test_immersion_freezing.py

+80-4
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,78 @@
88
from PySDM.environments import Box
99
from PySDM.physics import si
1010
from PySDM.products import IceWaterContent
11+
from PySDM.backends import GPU
12+
13+
VERY_BIG_J_HET = 1e20
14+
EPSILON_RH = 1e-3
1115

1216

1317
class TestImmersionFreezing:
14-
# TODO #599
15-
def test_record_freezing_temperature_on_time_dependent_freeze(self):
16-
pass
18+
@staticmethod
19+
@pytest.mark.parametrize(
20+
"record_freezing_temperature",
21+
(pytest.param(True, id="recording"), pytest.param(False, id="not recording")),
22+
)
23+
def test_record_freezing_temperature_on_time_dependent_freeze(
24+
backend_class, record_freezing_temperature
25+
):
26+
if backend_class is GPU and record_freezing_temperature:
27+
pytest.skip("TODO #1495")
28+
29+
# arrange
30+
formulae = Formulae(
31+
particle_shape_and_density="MixedPhaseSpheres",
32+
heterogeneous_ice_nucleation_rate="Constant",
33+
constants={"J_HET": VERY_BIG_J_HET},
34+
)
35+
builder = Builder(
36+
n_sd=1,
37+
backend=backend_class(formulae=formulae),
38+
environment=Box(dt=1 * si.s, dv=1 * si.m**3),
39+
)
40+
builder.add_dynamic(Freezing(singular=False, thaw=True))
41+
if record_freezing_temperature:
42+
builder.request_attribute("temperature of last freezing")
43+
particulator = builder.build(
44+
attributes={
45+
"multiplicity": np.asarray([1]),
46+
"signed water mass": np.asarray([1 * si.ug]),
47+
"immersed surface area": np.asarray([1 * si.um**2]),
48+
}
49+
)
50+
51+
temp_1 = 200 * si.K
52+
temp_2 = 250 * si.K
53+
particulator.environment["a_w_ice"] = np.nan
54+
particulator.environment["T"] = temp_1
55+
56+
# act & assert
57+
attr_name = "temperature of last freezing"
58+
if not record_freezing_temperature:
59+
assert attr_name not in particulator.attributes
60+
else:
61+
# never frozen yet
62+
np.isnan(particulator.attributes[attr_name].to_ndarray()).all()
63+
64+
# still not frozen since RH not greater than 100%
65+
particulator.environment["RH"] = 1.0
66+
particulator.run(steps=1)
67+
np.isnan(particulator.attributes[attr_name].to_ndarray()).all()
68+
69+
# should freeze and record T1
70+
particulator.environment["RH"] += EPSILON_RH
71+
particulator.run(steps=1)
72+
assert all(particulator.attributes[attr_name].to_ndarray() == temp_1)
73+
74+
# should thaw
75+
particulator.environment["T"] = 300 * si.K
76+
particulator.run(steps=1)
77+
np.isnan(particulator.attributes[attr_name].to_ndarray()).all()
78+
79+
# should re-freeze and record T2
80+
particulator.environment["T"] = temp_2
81+
particulator.run(steps=1)
82+
assert all(particulator.attributes[attr_name].to_ndarray() == temp_2)
1783

1884
# TODO #599
1985
def test_no_subsaturated_freezing(self):
@@ -25,7 +91,17 @@ def test_no_subsaturated_freezing(self):
2591
@pytest.mark.parametrize("epsilon", (0, 1e-5))
2692
def test_thaw(backend_class, singular, thaw, epsilon):
2793
# arrange
28-
formulae = Formulae(particle_shape_and_density="MixedPhaseSpheres")
94+
formulae = Formulae(
95+
particle_shape_and_density="MixedPhaseSpheres",
96+
**(
97+
{}
98+
if singular
99+
else {
100+
"heterogeneous_ice_nucleation_rate": "Constant",
101+
"constants": {"J_HET": 0},
102+
}
103+
),
104+
)
29105
env = Box(dt=1 * si.s, dv=1 * si.m**3)
30106
builder = Builder(
31107
n_sd=1, backend=backend_class(formulae=formulae), environment=env

0 commit comments

Comments
 (0)