Skip to content

terminal velocity of ice particles #1562

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

Draft
wants to merge 20 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
294375f
modified sedimentation unit test to test for ice particles
tluettm Mar 18, 2025
d7514a5
added empirical parametrisation for columnar ice crystals
tluettm Mar 24, 2025
20b4f7c
added atmospheric correction factor for columnar ice crystals termina…
tluettm Mar 28, 2025
2f43117
name change of columnar ice crystal sedimentation class
tluettm Mar 28, 2025
d8d7200
changed mass attribute to signed mass attribute for relative fall vel…
tluettm Mar 28, 2025
4a2e1fa
separated ice and liquid terminal velocity classes
tluettm Mar 28, 2025
bf4033f
added check for negative radius (indicating ice mass) to RogersYau an…
tluettm Mar 29, 2025
76b6aa3
further work on columnar ice crystal terminal velocity and added a un…
tluettm Mar 29, 2025
5977dcb
added temperature and pressure dependence for columnar ice crystal te…
tluettm Mar 31, 2025
d1994e8
added todo
tluettm Mar 31, 2025
b9706d1
added terminal velocity for smooth ice spheres
tluettm Apr 1, 2025
dd82205
added new unit test for terminal velocity of ice particles
tluettm Apr 1, 2025
cee7fa8
precommit and associating the TODOs with issue labels
tluettm May 16, 2025
de691c3
addressing some pylint
tluettm May 19, 2025
346f44e
refactor names of terminal velocity method functions for droplets
tluettm Jun 24, 2025
e418c9b
add an assert checking that env change triggers velocity change + tri…
slayoo Jun 24, 2025
0a62014
pre-commit
slayoo Jun 24, 2025
79fabd5
add Heymsfield & Iaquinta 2000 to docs + fix DOI
slayoo Jun 24, 2025
ae60860
add Barthazy and Schefold 2006 to docs
slayoo Jun 24, 2025
3ef0090
missing "usages" for ice terminal velocity
slayoo Jun 24, 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
8 changes: 4 additions & 4 deletions PySDM/attributes/physics/relative_fall_velocity.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""
Attributes for tracking droplet velocity
Attributes for tracking particle velocity
"""

from PySDM.attributes.impl import (
Expand Down Expand Up @@ -29,13 +29,13 @@ def __init__(self, builder):
class RelativeFallVelocity(DerivedAttribute):
def __init__(self, builder):
self.momentum = builder.get_attribute("relative fall momentum")
self.water_mass = builder.get_attribute("water mass")
self.signed_water_mass = builder.get_attribute("signed water mass")

super().__init__(
builder,
name="relative fall velocity",
dependencies=(self.momentum, self.water_mass),
dependencies=(self.momentum, self.signed_water_mass),
)

def recalculate(self):
self.data.ratio(self.momentum.get(), self.water_mass.get())
self.data.ratio(self.momentum.get(), self.signed_water_mass.get())
21 changes: 18 additions & 3 deletions PySDM/attributes/physics/terminal_velocity.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,27 @@
class TerminalVelocity(DerivedAttribute):
def __init__(self, builder):
self.radius = builder.get_attribute("radius")
dependencies = [self.radius]
self.signed_water_mass = builder.get_attribute("signed water mass")
self.cell_id = builder.get_attribute("cell id")
dependencies = [self.radius, self.signed_water_mass, self.cell_id]
super().__init__(builder, name="terminal velocity", dependencies=dependencies)

self.approximation = builder.formulae.terminal_velocity_class(
self.approximation_liquid = builder.formulae.terminal_velocity_class(
builder.particulator
)
self.approximation_ice = builder.formulae.terminal_velocity_ice_class(
builder.particulator
)

def recalculate(self):
self.approximation(self.data, self.radius.get())
self.approximation_liquid(self.data, self.radius.get())
# TODO #1605 order of functions calls changes result. approximation_liquid will override
# approximation_ice when mixed-phase spheres shape active
if self.formulae.particle_shape_and_density.supports_mixed_phase():
self.approximation_ice(
self.data,
self.signed_water_mass.get(),
self.cell_id.get(),
self.particulator.environment["T"],
self.particulator.environment["p"],
)
90 changes: 80 additions & 10 deletions PySDM/backends/impl_numba/methods/terminal_velocity_methods.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,38 +10,40 @@


class TerminalVelocityMethods(BackendMethods):

@cached_property
def _interpolation_body(self):
def _gunn_and_kinzer_interpolation_body(self):
@numba.njit(**self.default_jit_flags)
def body(output, radius, factor, b, c):
for i in numba.prange(len(radius)): # pylint: disable=not-an-iterable
if radius[i] < 0:
output[i] = 0
else:
if radius[i] > 0:
r_id = int(factor * radius[i])
r_rest = ((factor * radius[i]) % 1) / factor
output[i] = b[r_id] + r_rest * c[r_id]
elif radius == 0:
output[i] = 0

return body

def interpolation(self, *, output, radius, factor, b, c):
return self._interpolation_body(
def gunn_and_kinzer_interpolation(self, *, output, radius, factor, b, c):
return self._gunn_and_kinzer_interpolation_body(
output.data, radius.data, factor, b.data, c.data
)

@cached_property
def _terminal_velocity_body(self):
def _rogers_and_yau_terminal_velocity_body(self):
v_term = self.formulae.terminal_velocity.v_term

@numba.njit(**self.default_jit_flags)
def body(*, values, radius):
for i in numba.prange(len(values)): # pylint: disable=not-an-iterable
values[i] = v_term(radius[i])
if radius[i] >= 0.0:
values[i] = v_term(radius[i])

return body

def terminal_velocity(self, *, values, radius):
self._terminal_velocity_body(values=values, radius=radius)
def rogers_and_yau_terminal_velocity(self, *, values, radius):
self._rogers_and_yau_terminal_velocity_body(values=values, radius=radius)

@cached_property
def _power_series_body(self):
Expand All @@ -62,3 +64,71 @@ def power_series(self, *, values, radius, num_terms, prefactors, powers):
prefactors=prefactors,
powers=powers,
)

def terminal_velocity_columnar_ice_crystals(
self, *, values, signed_water_mass, cell_id, temperature, pressure
):
self._terminal_velocity_columnar_ice_crystals_body(
values=values,
signed_water_mass=signed_water_mass,
cell_id=cell_id,
temperature=temperature,
pressure=pressure,
)

@cached_property
def _terminal_velocity_columnar_ice_crystals_body(self):
v_base_term = self.formulae.terminal_velocity_ice.v_base_term
atmospheric_correction_factor = (
self.formulae.terminal_velocity_ice.atmospheric_correction_factor
)

@numba.njit(**self.default_jit_flags)
def body(*, values, signed_water_mass, cell_id, temperature, pressure):
for i in numba.prange(len(values)): # pylint: disable=not-an-iterable
if signed_water_mass[i] < 0:
cid = cell_id[i]
correction = atmospheric_correction_factor(
temperature[cid], pressure[cid]
)
values[i] = v_base_term(-signed_water_mass[i]) * correction

return body

def terminal_velocity_ice_spheres(
self, *, values, signed_water_mass, cell_id, temperature, pressure
): # pylint: disable=unused-argument
self._terminal_velocity_ice_spheres_body(
values=values,
signed_water_mass=signed_water_mass,
cell_id=cell_id,
temperature=temperature,
)

@cached_property
def _terminal_velocity_ice_spheres_body(self):
v_base_term = self.formulae.terminal_velocity_ice.v_base_term
stokes_prefactor = self.formulae.terminal_velocity_ice.stokes_regime
formulae = self.formulae_flattened

def body(*, values, signed_water_mass, cell_id, temperature):
for i in numba.prange(len(values)): # pylint: disable=not-an-iterable
if signed_water_mass[i] < 0:
cid = cell_id[i]
radius = formulae.particle_shape_and_density__mass_to_radius(
signed_water_mass[i]
)
dynamic_viscosity = formulae.air_dynamic_viscosity__eta_air(
temperature[cid]
)
prefactor = stokes_prefactor(radius, dynamic_viscosity)
values[i] = v_base_term(prefactor, radius)
print(
dynamic_viscosity,
signed_water_mass[i],
radius,
prefactor,
values[i],
)

return body
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ def __linear_collection_efficiency_body(self):
)

@cached_property
def __interpolation_body(self):
def __gunn_and_kinzer_interpolation_body(self):
# TODO #599 r<0
return trtc.For(
("output", "radius", "factor", "a", "b"),
Expand Down Expand Up @@ -126,9 +126,9 @@ def linear_collection_efficiency(
)

@nice_thrust(**NICE_THRUST_FLAGS)
def interpolation(self, *, output, radius, factor, b, c):
def gunn_and_kinzer_interpolation(self, *, output, radius, factor, b, c):
factor_device = trtc.DVInt64(factor)
self.__interpolation_body.launch_n(
self.__gunn_and_kinzer_interpolation_body.launch_n(
len(radius), (output.data, radius.data, factor_device, b.data, c.data)
)

Expand Down Expand Up @@ -157,7 +157,7 @@ def power_series(self, *, values, radius, num_terms, prefactors, powers):
)

@cached_property
def __terminal_velocity_body(self):
def __rogers_and_yau_terminal_velocity_body(self):
return trtc.For(
param_names=("values", "radius"),
name_iter="i",
Expand All @@ -171,5 +171,7 @@ def __terminal_velocity_body(self):
)

@nice_thrust(**NICE_THRUST_FLAGS)
def terminal_velocity(self, *, values, radius):
self.__terminal_velocity_body.launch_n(n=values.size(), args=[values, radius])
def rogers_and_yau_terminal_velocity(self, *, values, radius):
self.__rogers_and_yau_terminal_velocity_body.launch_n(
n=values.size(), args=[values, radius]
)
2 changes: 1 addition & 1 deletion PySDM/dynamics/relaxed_velocity.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ def register(self, builder):
"relative fall momentum"
)
self.terminal_vel_attr: Attribute = builder.get_attribute("terminal velocity")
self.water_mass_attr: Attribute = builder.get_attribute("water mass")
self.water_mass_attr: Attribute = builder.get_attribute("signed water mass")
self.sqrt_radius_attr: Attribute = builder.get_attribute(
"square root of radius"
)
Expand Down
2 changes: 2 additions & 0 deletions PySDM/dynamics/terminal_velocity/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@
from PySDM.dynamics.terminal_velocity.gunn_and_kinzer import GunnKinzer1949
from PySDM.dynamics.terminal_velocity.power_series import PowerSeries
from PySDM.dynamics.terminal_velocity.rogers_and_yau import RogersYau
from PySDM.dynamics.terminal_velocity.columnar_ice_crystal import ColumnarIceCrystal
from PySDM.dynamics.terminal_velocity.spheres_ice import IceSphere
20 changes: 20 additions & 0 deletions PySDM/dynamics/terminal_velocity/columnar_ice_crystal.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
"""
[Spichtinger & Gierens 2009](https://doi.org/10.5194/acp-9-685-2009)
Eq. (18) .Assumed shape is columnar based on empirical parameterizations of
[Heymsfield & Iaquinta (2000)](https://doi.org/10.1175/1520-0469(2000)057%3C0916:CCTV%3E2.0.CO;2)
[Barthazy & Schefold (2006)](https://doi.org/10.1016/j.atmosres.2005.12.009)
"""


class ColumnarIceCrystal: # pylint: disable=too-few-public-methods,too-many-arguments
def __init__(self, particulator):
self.particulator = particulator

def __call__(self, output, signed_water_mass, cell_id, temperature, pressure):
self.particulator.backend.terminal_velocity_columnar_ice_crystals(
values=output.data,
signed_water_mass=signed_water_mass.data,
cell_id=cell_id.data,
temperature=temperature.data,
pressure=pressure.data,
)
2 changes: 1 addition & 1 deletion PySDM/dynamics/terminal_velocity/gunn_and_kinzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ def __call__(self, output, radius):
f"Radii can be interpolated up to {self.maximum_radius} m"
+ f" (max value of {r_max} m within input data)"
)
self.particulator.backend.interpolation(
self.particulator.backend.gunn_and_kinzer_interpolation(
output=output, radius=radius, factor=self.factor, b=self.a, c=self.b
)

Expand Down
2 changes: 1 addition & 1 deletion PySDM/dynamics/terminal_velocity/rogers_and_yau.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ def __init__(self, particulator):
self.particulator = particulator

def __call__(self, output, radius):
self.particulator.backend.terminal_velocity(
self.particulator.backend.rogers_and_yau_terminal_velocity(
values=output.data,
radius=radius.data,
)
17 changes: 17 additions & 0 deletions PySDM/dynamics/terminal_velocity/spheres_ice.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
"""
terminal velocity of solid ice spheres
"""


class IceSphere: # pylint: disable=too-few-public-methods,too-many-arguments
def __init__(self, particulator):
self.particulator = particulator

def __call__(self, output, signed_water_mass, cell_id, temperature, pressure):
self.particulator.backend.terminal_velocity_ice_spheres(
values=output.data,
signed_water_mass=signed_water_mass.data,
cell_id=cell_id.data,
temperature=temperature.data,
pressure=pressure.data,
)
14 changes: 13 additions & 1 deletion PySDM/formulae.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,13 @@

from PySDM import physics
from PySDM.backends.impl_numba import conf
from PySDM.dynamics.terminal_velocity import GunnKinzer1949, PowerSeries, RogersYau
from PySDM.dynamics.terminal_velocity import (
GunnKinzer1949,
PowerSeries,
RogersYau,
ColumnarIceCrystal,
IceSphere,
)
from PySDM.dynamics.terminal_velocity.gunn_and_kinzer import TpDependent


Expand Down Expand Up @@ -61,6 +67,7 @@ def __init__( # pylint: disable=too-many-locals
optical_depth: str = "Null",
particle_shape_and_density: str = "LiquidSpheres",
terminal_velocity: str = "GunnKinzer1949",
terminal_velocity_ice: str = "ColumnarIceCrystal",
air_dynamic_viscosity: str = "ZografosEtAl1987",
bulk_phase_partitioning: str = "Null",
handle_all_breakups: bool = False,
Expand Down Expand Up @@ -104,6 +111,7 @@ def __init__( # pylint: disable=too-many-locals
self.particle_shape_and_density = particle_shape_and_density
self.air_dynamic_viscosity = air_dynamic_viscosity
self.terminal_velocity = terminal_velocity
self.terminal_velocity_ice = terminal_velocity_ice
self.bulk_phase_partitioning = bulk_phase_partitioning

self._components = tuple(
Expand Down Expand Up @@ -166,6 +174,10 @@ def __init__( # pylint: disable=too-many-locals
"TpDependent": TpDependent,
"PowerSeries": PowerSeries,
}[terminal_velocity]
self.terminal_velocity_ice_class = {
"ColumnarIceCrystal": ColumnarIceCrystal,
"IceSphere": IceSphere,
}[terminal_velocity_ice]

def __str__(self):
description = []
Expand Down
1 change: 1 addition & 0 deletions PySDM/physics/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
ventilation,
air_dynamic_viscosity,
terminal_velocity,
terminal_velocity_ice,
bulk_phase_partitioning,
)
from .constants import convert_to, in_unit, si
39 changes: 39 additions & 0 deletions PySDM/physics/constants_defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -611,6 +611,45 @@
ROGERS_YAU_TERM_VEL_MEDIUM_R_LIMIT = 600 * si.um
""" 〃 """

SPICHTINGER_GIERENS_TERM_VEL_LIMIT_0 = 2.146e-13 * si.kg
""" empirical terminal velocity formulation
for columnar ice crystals from Table 2. in
[Spichtinger & Gierens 2009](https://doi.org/10.5194/acp-9-685-2009) """
SPICHTINGER_GIERENS_TERM_VEL_LIMIT_1 = 2.166e-9 * si.kg
""" 〃 """
SPICHTINGER_GIERENS_TERM_VEL_LIMIT_2 = 4.264e-8 * si.kg
""" 〃 """
SPICHTINGER_TERM_DELTA_COEFF0 = 0.42
""" 〃 """
SPICHTINGER_TERM_DELTA_COEFF1 = 0.57
""" 〃 """
SPICHTINGER_TERM_DELTA_COEFF2 = 0.31
""" 〃 """
SPICHTINGER_TERM_DELTA_COEFF3 = 0.096
""" 〃 """
SPICHTINGER_TERM_GAMMA_COEFF0 = (
735.4 * si.m / si.s / si.kg**SPICHTINGER_TERM_DELTA_COEFF0
)
""" 〃 """
SPICHTINGER_TERM_GAMMA_COEFF1 = (
63292.4 * si.m / si.s / si.kg**SPICHTINGER_TERM_DELTA_COEFF1
)
""" 〃 """
SPICHTINGER_TERM_GAMMA_COEFF2 = (
329.8 * si.m / si.s / si.kg**SPICHTINGER_TERM_DELTA_COEFF2
)
""" 〃 """
SPICHTINGER_TERM_GAMMA_COEFF3 = 8.8 * si.m / si.s / si.kg**SPICHTINGER_TERM_DELTA_COEFF3
""" 〃 """
SPICHTINGER_CORRECTION_P0 = 300 * si.hectopascal
""" 〃 """
SPICHTINGER_CORRECTION_P_EXPO = -0.178
""" 〃 """
SPICHTINGER_CORRECTION_T0 = 233 * si.kelvin
""" 〃 """
SPICHTINGER_CORRECTION_T_EXPO = -0.394
""" 〃 """

W76W_G0 = -2.9912729e3 * si.K**2
""" [Wexler 1976](https://doi.org/10.6028/jres.080A.071) saturation vapour pressure """
W76W_G1 = -6.0170128e3 * si.K
Expand Down
2 changes: 1 addition & 1 deletion PySDM/physics/terminal_velocity/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""terminal velocity formulae"""
"""terminal velocity formulae for liquid droplets"""

from .rogers_yau import RogersYau
from .gunn_kinzer_1949 import GunnKinzer1949
Expand Down
4 changes: 4 additions & 0 deletions PySDM/physics/terminal_velocity_ice/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
"""terminal velocity formulae for ice particles"""

from .columnar_ice_crystal import ColumnarIceCrystal
from .spheres_ice import IceSphere
Loading
Loading