Skip to content

Homogeneous freezing #1488

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

Open
wants to merge 51 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
acb5ead
Added homogeneous freezing routines and Koop 2000 nucleation rate
tluettm Dec 30, 2024
0f53d3f
Untracked temporary files
tluettm Dec 30, 2024
2c215f3
Fixed registration of hom ice nucleation rate
tluettm Dec 30, 2024
73bf2ff
Added logicals for hom/het freezing and fixed passing of attributes
tluettm Jan 3, 2025
91c2cff
Renamed freezing logicals
tluettm Jan 3, 2025
0d21c6f
Added corrected homogeneous nucleation rate
tluettm Jan 4, 2025
6613dfa
added koop and murray nucleation rate
tluettm Jan 4, 2025
45b736f
added homogeneous freezing temperature threshold for later use
tluettm Jan 4, 2025
a8acee6
Fixed some typos in the code
tluettm Jan 4, 2025
2074ed5
Added time dependent hom. freezing test to test_freezing_methods unit…
tluettm Jan 4, 2025
7178a0a
Merge branch 'main' into homogeneous_freezing
tluettm Jan 14, 2025
021ef24
Changed homogenous freezing routine ant test to match new moist envir…
tluettm Jan 15, 2025
7f6d360
Added Kaercher Lohmann Example and renamed koop hom. freezing class
tluettm Feb 14, 2025
f34da15
Expanded Kaercher Lohmann Example
tluettm Feb 15, 2025
bf4367b
Added hom. freezing rates in KA02 example
tluettm Feb 15, 2025
215e8cb
Added size distribution plots to KL02 example
tluettm Feb 15, 2025
6c359ce
Reworked settings in KL02 example
tluettm Feb 18, 2025
c10cb82
Merged main
tluettm Feb 18, 2025
16d0f55
Fixes to make hom. freezing compatible with signed water masses changes
tluettm Feb 19, 2025
a4eaed0
Changed name of ice mass variable in freeze and thaw
tluettm Feb 24, 2025
a48b48e
merge changes to freezing into hom. freezing branch
tluettm Mar 13, 2025
58f7111
renamed immersion freezing unit test
tluettm Mar 13, 2025
341bc61
Merged main with deposition dynamic into freezing
tluettm Apr 1, 2025
fc127e1
some modifications to example
tluettm Apr 1, 2025
109f36e
some modifications to example
tluettm Apr 18, 2025
fd13a2e
added unit test for hom. ice nucleation rates and adjusted formatting
tluettm Apr 18, 2025
0129239
some cleanup and changes to example
tluettm Apr 19, 2025
9de8029
Merged main with freezing changes
tluettm May 2, 2025
98a784b
some changes to test names and comments
tluettm May 2, 2025
a709851
Combined freezing test of homogeneous and immersion freezing + cleanup
tluettm May 6, 2025
29b9bef
reworked Kaercher_Lohmann_2002 example to Spichtinger_et_al_2023 example
tluettm May 14, 2025
f8bd37a
added bulk reference solution to spichtinger_et_al_2023 example
tluettm May 14, 2025
e057298
added json to git ignore
tluettm May 14, 2025
e92286f
cleanup for constants_defaults.py
tluettm May 14, 2025
037e4da
added references to constants_defaults.py
tluettm May 14, 2025
e96d4ca
let precommit do its thing
tluettm May 15, 2025
dbf8bf2
turned run into a Juypter notebook
tluettm May 15, 2025
c26626c
added references to bibliography.json
tluettm May 15, 2025
731a68d
fixes for pylint & pdocs complaints
tluettm May 16, 2025
da147d8
pydoc didnt like my sorting of usage urls and adressing pylint
tluettm May 16, 2025
012081b
maybe pdoc and pylint are happy now?
tluettm May 16, 2025
13f3ebb
fix bib entry ("et_al" instead of "etal" in directory name)
slayoo May 18, 2025
f1213c2
call super class ctor
slayoo May 18, 2025
b35509a
address pylint hints
slayoo May 18, 2025
3bbc7dd
import fix
slayoo May 18, 2025
e922170
Removed .json from git ignore
tluettm May 19, 2025
822c5c0
reformatting setting function call
tluettm May 19, 2025
a82cc9b
Merge branch 'main' into homogeneous_freezing
tluettm May 20, 2025
4746fef
Merge branch 'main' into homogeneous_freezing
tluettm May 26, 2025
4593b99
Added Spichtinger_et_al_2023 to example_tests
tluettm May 26, 2025
f25fe02
fix units in KOOP_MURRAY constants; add constants unit test; add plot…
slayoo May 26, 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
11 changes: 11 additions & 0 deletions PySDM/backends/impl_common/freezing_attributes.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,14 @@ class TimeDependentAttributes(
"""groups attributes required in time-dependent regime"""

__slots__ = ()


class TimeDependentHomogeneousAttributes(
namedtuple(
typename="TimeDependentHomogeneousAttributes",
field_names=("volume", "signed_water_mass"),
)
):
"""groups attributes required in time-dependent regime for homogeneous freezing"""

__slots__ = ()
81 changes: 76 additions & 5 deletions PySDM/backends/impl_numba/methods/freezing_methods.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""
CPU implementation of backend methods for freezing (singular and time-dependent immersion freezing)
CPU implementation of backend methods for homogeneous freezing and
heterogeneous freezing (singular and time-dependent immersion freezing)
"""

from functools import cached_property
Expand All @@ -12,25 +13,27 @@
from ...impl_common.freezing_attributes import (
SingularAttributes,
TimeDependentAttributes,
TimeDependentHomogeneousAttributes,
)


class FreezingMethods(BackendMethods):
def __init__(self):
BackendMethods.__init__(self)
unfrozen_and_saturated = self.formulae.trivia.unfrozen_and_saturated
unfrozen_and_ice_saturated = self.formulae.trivia.unfrozen_and_ice_saturated
frozen_and_above_freezing_point = (
self.formulae.trivia.frozen_and_above_freezing_point
)

@numba.njit(**{**self.default_jit_flags, "parallel": False})
def _freeze(water_mass, i):
water_mass[i] = -1 * water_mass[i]
def _freeze(signed_water_mass, i):
signed_water_mass[i] = -1 * signed_water_mass[i]
# TODO #599: change thd (latent heat)!

@numba.njit(**{**self.default_jit_flags, "parallel": False})
def _thaw(water_mass, i):
water_mass[i] = -1 * water_mass[i]
def _thaw(signed_water_mass, i):
signed_water_mass[i] = -1 * signed_water_mass[i]
# TODO #599: change thd (latent heat)!

@numba.njit(**self.default_jit_flags)
Expand Down Expand Up @@ -92,6 +95,48 @@ def freeze_time_dependent_body( # pylint: disable=too-many-arguments

self.freeze_time_dependent_body = freeze_time_dependent_body

j_hom = self.formulae.homogeneous_ice_nucleation_rate.j_hom

@numba.njit(**self.default_jit_flags)
def freeze_time_dependent_homogeneous_body( # pylint: disable=unused-argument,too-many-arguments
rand,
attributes,
timestep,
cell,
a_w_ice,
temperature,
relative_humidity_ice,
thaw,
):

n_sd = len(attributes.signed_water_mass)
for i in numba.prange(n_sd): # pylint: disable=not-an-iterable
cell_id = cell[i]
if thaw and frozen_and_above_freezing_point(
attributes.signed_water_mass[i], temperature[cell_id]
):
_thaw(attributes.signed_water_mass, i)
elif unfrozen_and_ice_saturated(
attributes.signed_water_mass[i], relative_humidity_ice[cell_id]
):
d_a_w_ice = (relative_humidity_ice[cell_id] - 1.0) * a_w_ice[
cell_id
]
if 0.23 < d_a_w_ice < 0.34:
rate_assuming_constant_temperature_within_dt = (
j_hom(temperature[cell_id], d_a_w_ice)
* attributes.volume[i]
)
prob = 1 - prob_zero_events(
r=rate_assuming_constant_temperature_within_dt, dt=timestep
)
if rand[i] < prob:
_freeze(attributes.signed_water_mass, i)

self.freeze_time_dependent_homogeneous_body = (
freeze_time_dependent_homogeneous_body
)

def freeze_singular(
self, *, attributes, temperature, relative_humidity, cell, thaw: bool
):
Expand Down Expand Up @@ -132,6 +177,32 @@ def freeze_time_dependent(
thaw=thaw,
)

def freeze_time_dependent_homogeneous(
self,
*,
rand,
attributes,
timestep,
cell,
a_w_ice,
temperature,
relative_humidity_ice,
thaw: bool,
):
self.freeze_time_dependent_homogeneous_body(
rand.data,
TimeDependentHomogeneousAttributes(
volume=attributes.volume.data,
signed_water_mass=attributes.signed_water_mass.data,
),
timestep,
cell.data,
a_w_ice.data,
temperature.data,
relative_humidity_ice.data,
thaw=thaw,
)

@cached_property
def _record_freezing_temperatures_body(self):
ff = self.formulae_flattened
Expand Down
44 changes: 36 additions & 8 deletions PySDM/dynamics/freezing.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,25 @@
"""
immersion freezing using either singular or time-dependent formulation
droplet freezing using either singular or
time-dependent formulation for immersion freezing
and homogeneous freezing and thaw
"""

from PySDM.dynamics.impl import register_dynamic


@register_dynamic()
class Freezing:
def __init__(self, *, singular=True, thaw=False):
class Freezing: # pylint: disable=too-many-instance-attributes
def __init__(
self,
*,
singular=True,
homogeneous_freezing=False,
immersion_freezing=True,
thaw=False,
):
self.singular = singular
self.homogeneous_freezing = homogeneous_freezing
self.immersion_freezing = immersion_freezing
self.thaw = thaw
self.enable = True
self.rand = None
Expand All @@ -26,12 +37,21 @@ def register(self, builder):
if self.singular:
builder.request_attribute("freezing temperature")

if not self.singular:
if not self.singular and self.immersion_freezing:
assert (
self.particulator.formulae.heterogeneous_ice_nucleation_rate.__name__
!= "Null"
)
builder.request_attribute("immersed surface area")

if self.homogeneous_freezing:
assert (
self.particulator.formulae.homogeneous_ice_nucleation_rate.__name__
!= "Null"
)
builder.request_attribute("volume")

if self.homogeneous_freezing or not self.singular:
self.rand = self.particulator.Storage.empty(
self.particulator.n_sd, dtype=float
)
Expand All @@ -49,11 +69,19 @@ def __call__(self):
if not self.enable:
return

if self.singular:
self.particulator.immersion_freezing_singular(thaw=self.thaw)
else:
if self.immersion_freezing:
if self.singular:
self.particulator.immersion_freezing_singular(thaw=self.thaw)
else:
self.rand.urand(self.rng)
self.particulator.immersion_freezing_time_dependent(
rand=self.rand,
thaw=self.thaw,
)

if self.homogeneous_freezing:
self.rand.urand(self.rng)
self.particulator.immersion_freezing_time_dependent(
self.particulator.homogeneous_freezing_time_dependent(
rand=self.rand,
thaw=self.thaw,
)
4 changes: 3 additions & 1 deletion PySDM/formulae.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
from PySDM.dynamics.terminal_velocity.gunn_and_kinzer import TpDependent


class Formulae: # pylint: disable=too-few-public-methods,too-many-instance-attributes
class Formulae: # pylint: disable=too-few-public-methods,too-many-instance-attributes,too-many-statements
def __init__( # pylint: disable=too-many-locals
self,
*,
Expand All @@ -47,6 +47,7 @@ def __init__( # pylint: disable=too-many-locals
hydrostatics: str = "ConstantGVapourMixingRatioAndThetaStd",
freezing_temperature_spectrum: str = "Null",
heterogeneous_ice_nucleation_rate: str = "Null",
homogeneous_ice_nucleation_rate: str = "Null",
fragmentation_function: str = "AlwaysN",
isotope_equilibrium_fractionation_factors: str = "Null",
isotope_kinetic_fractionation_factors: str = "Null",
Expand Down Expand Up @@ -85,6 +86,7 @@ def __init__( # pylint: disable=too-many-locals
self.hydrostatics = hydrostatics
self.freezing_temperature_spectrum = freezing_temperature_spectrum
self.heterogeneous_ice_nucleation_rate = heterogeneous_ice_nucleation_rate
self.homogeneous_ice_nucleation_rate = homogeneous_ice_nucleation_rate
self.fragmentation_function = fragmentation_function
self.isotope_equilibrium_fractionation_factors = (
isotope_equilibrium_fractionation_factors
Expand Down
16 changes: 16 additions & 0 deletions PySDM/particulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from PySDM.backends.impl_common.freezing_attributes import (
SingularAttributes,
TimeDependentAttributes,
TimeDependentHomogeneousAttributes,
)
from PySDM.backends.impl_common.index import make_Index
from PySDM.backends.impl_common.indexed_storage import make_IndexedStorage
Expand Down Expand Up @@ -537,3 +538,18 @@ def immersion_freezing_singular(self, *, thaw: bool):
thaw=thaw,
)
self.attributes.mark_updated("signed water mass")

def homogeneous_freezing_time_dependent(self, *, thaw: bool, rand: Storage):
self.backend.freeze_time_dependent_homogeneous(
rand=rand,
attributes=TimeDependentHomogeneousAttributes(
volume=self.attributes["volume"],
signed_water_mass=self.attributes["signed water mass"],
),
timestep=self.dt,
cell=self.attributes["cell id"],
a_w_ice=self.environment["a_w_ice"],
temperature=self.environment["T"],
relative_humidity_ice=self.environment["RH_ice"],
thaw=thaw,
)
2 changes: 2 additions & 0 deletions PySDM/physics/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"""

from . import (
dimensional_analysis,
diffusion_coordinate,
constants_defaults,
diffusion_kinetics,
Expand All @@ -27,6 +28,7 @@
fragmentation_function,
freezing_temperature_spectrum,
heterogeneous_ice_nucleation_rate,
homogeneous_ice_nucleation_rate,
hydrostatics,
hygroscopicity,
impl,
Expand Down
42 changes: 34 additions & 8 deletions PySDM/physics/constants_defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,13 +106,6 @@
""" thermal accommodation coefficient for vapour deposition as recommended in
[Pruppacher & Klett](https://doi.org/10.1007/978-0-306-48100-0) """

p1000 = 1000 * si.hectopascals
c_pd = 1005 * si.joule / si.kilogram / si.kelvin
c_pv = 1850 * si.joule / si.kilogram / si.kelvin
g_std = sci.g * si.metre / si.second**2

c_pw = 4218 * si.joule / si.kilogram / si.kelvin

ARM_C1 = 6.1094 * si.hectopascal
""" [August](https://doi.org/10.1002/andp.18280890511) Roche Magnus formula coefficients
(values from [Alduchov & Eskridge 1996](https://doi.org/10.1175%2F1520-0450%281996%29035%3C0601%3AIMFAOS%3E2.0.CO%3B2))
Expand Down Expand Up @@ -322,8 +315,41 @@
ABIFM_C = np.inf
""" 〃 """

KOOP_2000_C1 = -906.7
""" homogeneous ice nucleation rate
([Koop et al. 2000](https://doi.org/10.1038/35020537)) """
KOOP_2000_C2 = 8502
""" 〃 """
KOOP_2000_C3 = -26924
""" 〃 """
KOOP_2000_C4 = 29180
""" 〃 """
KOOP_UNIT = 1 / si.cm**3 / si.s
""" 〃 """

KOOP_CORR = -1.522
""" homogeneous ice nucleation rate correction factor
([Spichtinger et al. 2023](https://doi.org/10.5194/acp-23-2035-2023)) """

KOOP_MURRAY_C0 = -3020.684
""" homogeneous ice nucleation rate for pure water droplets
([Koop & Murray 20016](https://doi.org/10.1063/1.4962355)) """
KOOP_MURRAY_C1 = -425.921 / si.K
""" 〃 """
KOOP_MURRAY_C2 = -25.9779 / si.K**2
""" 〃 """
KOOP_MURRAY_C3 = -0.868451 / si.K**3
""" 〃 """
KOOP_MURRAY_C4 = -1.66203e-2 / si.K**4
""" 〃 """
KOOP_MURRAY_C5 = -1.71736e-4 / si.K**5
""" 〃 """
KOOP_MURRAY_C6 = -7.46953e-7 / si.K**6
""" 〃 """

J_HET = np.nan
""" constant ice nucleation rate """
J_HOM = np.nan
""" constant ice nucleation rates """

STRAUB_E_D1 = 0.04 * si.cm
""" [Straub et al. 2010](https://doi.org/10.1175/2009JAS3175.1) """
Expand Down
9 changes: 9 additions & 0 deletions PySDM/physics/homogeneous_ice_nucleation_rate/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
"""
homogeneous-freezing rate (aka J_hom) formulations
"""

from .constant import Constant
from .null import Null
from .koop import Koop2000
from .koop_corr import Koop_Correction
from .koop_murray import KoopMurray2016
14 changes: 14 additions & 0 deletions PySDM/physics/homogeneous_ice_nucleation_rate/constant.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"""
constant rate formulation (for tests)
"""

import numpy as np


class Constant: # pylint: disable=too-few-public-methods
def __init__(self, const):
assert np.isfinite(const.J_HOM)

@staticmethod
def j_hom(const, T, a_w_ice): # pylint: disable=unused-argument
return const.J_HOM
23 changes: 23 additions & 0 deletions PySDM/physics/homogeneous_ice_nucleation_rate/koop.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
"""
Koop homogeneous nucleation rate parameterization for solution droplets
valid for 0.26 < da_w_ice < 0.34
([Koop et al. 2000](https://doi.org/10.1038/35020537))
"""


class Koop2000: # pylint: disable=too-few-public-methods
def __init__(self, const):
pass

@staticmethod
def j_hom(const, T, da_w_ice): # pylint: disable=unused-argument
return (
10
** (
const.KOOP_2000_C1
+ const.KOOP_2000_C2 * da_w_ice
+ const.KOOP_2000_C3 * da_w_ice**2.0
+ const.KOOP_2000_C4 * da_w_ice**3.0
)
* const.KOOP_UNIT
)
Loading
Loading