Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
09fad83
early draft of deterministic deposition removal
slayoo Sep 30, 2025
6f9a304
renaming deposition -> sedimentation; plot=False not to hang CI
slayoo Sep 30, 2025
f05ae89
extended test cases
slayoo Nov 8, 2025
47b0e8d
Merged main
tluettm May 18, 2026
b253d09
renaming functions and booleans
tluettm May 20, 2026
15530fb
moved sedimentation_removal to backend
tluettm May 20, 2026
073fc48
Added jit commands to new method. Included ice in unit testing
tluettm May 20, 2026
a981537
Added determernistic sedimentation removal
tluettm May 20, 2026
e1947b6
Merged main
tluettm May 20, 2026
edf5766
Added asserts to unit test
tluettm May 20, 2026
8584ef5
pylint and pdoc and unit-tests fix
tluettm May 21, 2026
e88c5f1
merged main
tluettm May 21, 2026
442dc5b
addressing pylint
tluettm May 21, 2026
224e360
numba fix?
tluettm May 21, 2026
889d85e
added pylint comment
tluettm May 21, 2026
6de1ce1
Merge branch 'main' into deposition_removal
tluettm May 27, 2026
31072a2
Removed assertion from loop
tluettm May 27, 2026
0d31594
Merge branch 'main' into deposition_removal
slayoo May 30, 2026
f37742d
remove spurious "\n" from paper title in bib JSON
slayoo May 30, 2026
2d3a243
fix compatibility with newer PySDM API (no add_dynamic); rename the n…
slayoo May 30, 2026
aa15e27
fix file name in bib
slayoo May 30, 2026
9a1dc9d
Merge branch 'main' into deposition_removal
tluettm Jun 1, 2026
978d341
Moved length_scale calculation from register to call
tluettm Jun 1, 2026
a688815
remove self.length_scale as it is not needed anymore
slayoo Jun 1, 2026
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
1 change: 1 addition & 0 deletions PySDM/backends/impl_numba/methods/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@
from .terminal_velocity_methods import TerminalVelocityMethods
from .seeding_methods import SeedingMethods
from .deposition_methods import DepositionMethods
from .sedimentation_removal_0d_methods import SedimentationRemoval0DMethods
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"""
CPU implementation of backend methods for removal due to sedimentation
in a 0D environment
"""

from functools import cached_property

import numba

from PySDM.backends.impl_common.backend_methods import BackendMethods


class SedimentationRemoval0DMethods(BackendMethods):

@cached_property
def _sedimentation_removal_deterministic_body(self):

@numba.njit(**self.default_jit_flags)
def body(relative_fall_velocity, multiplicity, length_scale, timestep):
n_sd = len(relative_fall_velocity)
for i in numba.prange(n_sd): # pylint: disable=not-an-iterable
multiplicity[i] -= (
multiplicity[i]
* relative_fall_velocity[i]
* timestep
/ length_scale
)

return body

@cached_property
def _sedimentation_removal_stochastic_body(self):

prob_zero_events = self.formulae.trivia.poissonian_avoidance_function

@numba.njit(**self.default_jit_flags)
def body(relative_fall_velocity, multiplicity, length_scale, timestep):
n_sd = len(relative_fall_velocity)
for i in numba.prange(n_sd): # pylint: disable=not-an-iterable
removal_rate = relative_fall_velocity[i] / length_scale
survive_prob = prob_zero_events(r=removal_rate, dt=timestep)
multiplicity[i] *= survive_prob

return body

def sedimentation_removal_deterministic(
self, *, relative_fall_velocity, multiplicity, length_scale, timestep
):
self._sedimentation_removal_deterministic_body(
relative_fall_velocity, multiplicity, length_scale, timestep
)

def sedimentation_removal_stochastic(
self, *, relative_fall_velocity, multiplicity, length_scale, timestep
):
self._sedimentation_removal_stochastic_body(
relative_fall_velocity, multiplicity, length_scale, timestep
)
1 change: 1 addition & 0 deletions PySDM/backends/numba.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ class Numba( # pylint: disable=too-many-ancestors,duplicate-code
methods.IsotopeMethods,
methods.SeedingMethods,
methods.DepositionMethods,
methods.SedimentationRemoval0DMethods,
):
Storage = ImportedStorage
Random = ImportedRandom
Expand Down
1 change: 1 addition & 0 deletions PySDM/dynamics/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@
from PySDM.dynamics.relaxed_velocity import RelaxedVelocity
from PySDM.dynamics.seeding import Seeding
from PySDM.dynamics.vapour_deposition_on_ice import VapourDepositionOnIce
from PySDM.dynamics.sedimentation_removal_0d import SedimentationRemoval0D
27 changes: 27 additions & 0 deletions PySDM/dynamics/sedimentation_removal_0d.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"""deposition removal logic for zero-dimensional environments"""

from typing import Optional
import numpy as np
from PySDM.dynamics.impl import register_dynamic


@register_dynamic()
class SedimentationRemoval0D:
def __init__(self, *, stochastic_sedimentation_removal: Optional[bool] = True):
"""stochastic or deterministic removal"""
self.stochastic_sedimentation_removal = stochastic_sedimentation_removal
self.particulator = None

def register(self, builder):
builder.request_attribute("relative fall velocity")
assert builder.particulator.environment.mesh.n_dims == 0
self.particulator = builder.particulator

def __call__(self):
"""for stochastic removal, see, e.g., the naive scheme in Algorithm 1 in
[Curtis et al. 2016](https://doi.org/10.1016/j.jcp.2016.06.029)"""

self.particulator.sedimentation_removal(
stochastic_sedimentation_removal=self.stochastic_sedimentation_removal,
length_scale=np.cbrt(self.particulator.environment.mesh.dv),
)
16 changes: 16 additions & 0 deletions PySDM/particulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -595,3 +595,19 @@ def thaw_instantaneous(self):
temperature=self.environment["T"],
)
self.attributes.mark_updated("signed water mass")

def sedimentation_removal(self, *, stochastic_sedimentation_removal, length_scale):
if stochastic_sedimentation_removal:
self.backend.sedimentation_removal_stochastic(
relative_fall_velocity=self.attributes["relative fall velocity"].data,
multiplicity=self.attributes["multiplicity"].data,
length_scale=length_scale,
timestep=self.dt,
)
else:
self.backend.sedimentation_removal_deterministic(
relative_fall_velocity=self.attributes["relative fall velocity"].data,
multiplicity=self.attributes["multiplicity"].data,
length_scale=length_scale,
timestep=self.dt,
)
7 changes: 7 additions & 0 deletions docs/bibliography.json
Original file line number Diff line number Diff line change
Expand Up @@ -1047,5 +1047,12 @@
],
"label": "Arabas & Pawlowska 2011",
"title": "Adaptive method of lines for multi-component aerosol condensational growth and CCN activation"
},
"https://doi.org/10.1016/j.jcp.2016.06.029": {
"usages": [
"PySDM/dynamics/sedimentation_removal_0d.py"
],
"label": "Curtis et al. 2016 (J. Comp. Phys. 322)",
"title": "Accelerated simulation of stochastic particle removal processes in particle-resolved aerosol models"
}
}
106 changes: 106 additions & 0 deletions tests/unit_tests/dynamics/test_sedimentation_removal_0d.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# pylint: disable=missing-module-docstring
from matplotlib import pyplot
import pytest
import numpy as np
from PySDM.dynamics import SedimentationRemoval0D
from PySDM.physics import si
from PySDM.environments import Box
from PySDM import Builder
from PySDM.backends import ThrustRTC
from PySDM.products import ParticleConcentration, SuperDropletCountPerGridbox, Time


class TestSedimentationRemoval0D: # pylint: disable=too-few-public-methods,too-many-branches,too-many-locals
@staticmethod
@pytest.mark.parametrize("stochastic", (True, False))
def test_convergence_wrt_dt(backend_class, stochastic, plot=False):
# arrange
dts = 0.5 * si.s, 5 * si.s
dvs = 1e2 * si.m**3, 1e3 * si.m**3, 1e4 * si.m**3
t_max = 600 * si.s
multiplicities = 1e5, 1e6, 1e7, 1e8
water_masses = 1 * si.ug, 2 * si.ug, 3 * si.ug, 4 * si.ug

if backend_class is ThrustRTC:
pytest.skip("TODO #1871")

# act
output = {}
for dt in dts:
for dv in dvs:
builder = Builder(
n_sd=len(multiplicities),
environment=Box(dv=dv, dt=dt),
backend=backend_class(),
dynamics=(
SedimentationRemoval0D(
stochastic_sedimentation_removal=stochastic
),
),
)
particulator = builder.build(
attributes={
"multiplicity": np.asarray(multiplicities),
"signed water mass": np.asarray(water_masses),
},
products=(
ParticleConcentration(),
SuperDropletCountPerGridbox(),
Time(),
),
)
key = f"{dt=} {dv=}"
output[key] = {name: [] for name in particulator.products}
while particulator.n_steps * dt <= t_max:
if len(output[key]["time"]) != 0:
particulator.run(steps=1)
for name, product in particulator.products.items():
output[key][name].append(product.get() + 0)

# plot
pyplot.title(f"{stochastic=}")
pyplot.xlabel("time [s]")
pyplot.ylabel("particle concentration [m$^{-3}$]")
for dt in dts:
for dv in dvs:
key = f"{dt=} {dv=}"
pyplot.plot(
output[key]["time"],
output[key]["particle concentration"],
label=key,
linewidth=4 + 3 * np.log10(dt),
)
pyplot.gca().set_yscale("log")
pyplot.gca().set_xlim(left=0, right=t_max)
pyplot.legend()
pyplot.grid()

if plot:
pyplot.show()
else:
pyplot.clf()

# assert
for dt in dts:
for dv in dvs:
key = f"{dt=} {dv=}"
particle_concentration = np.asarray(
output[key]["particle concentration"]
)
assert particle_concentration[-1] == 0.0

for dv in dvs:
time_compare = 0
for dt in dts:
key = f"{dt=} {dv=}"
time_end = np.asarray(output[key]["time"])[-1]
assert time_end >= time_compare
time_compare = time_end

for dt in dts:
time_compare = 0
for dv in dvs:
key = f"{dt=} {dv=}"
time_end = np.asarray(output[key]["time"])[-1]
assert time_end >= time_compare
time_compare = time_end
Loading