Skip to content

Commit f1e1df9

Browse files
slayooclaresingerjtbuch
authored
Seeding dynamic and associated initialisation logic (super-droplet injection during simulation) (#1367)
Co-authored-by: claresinger <[email protected]> Co-authored-by: jtbuch <[email protected]>
1 parent ec1f515 commit f1e1df9

26 files changed

+1449
-12
lines changed

PySDM/backends/impl_numba/methods/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,4 @@
1212
from .pair_methods import PairMethods
1313
from .physics_methods import PhysicsMethods
1414
from .terminal_velocity_methods import TerminalVelocityMethods
15+
from .seeding_methods import SeedingMethods

PySDM/backends/impl_numba/methods/condensation_methods.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -445,7 +445,7 @@ def calculate_ml_new( # pylint: disable=too-many-branches,too-many-arguments,to
445445
v_drop = formulae.particle_shape_and_density__mass_to_volume(
446446
attributes.water_mass[drop]
447447
)
448-
if v_drop < 0:
448+
if v_drop <= 0:
449449
continue
450450
x_old = formulae.condensation_coordinate__x(v_drop)
451451
r_old = formulae.trivia__radius(v_drop)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
""" CPU implementation of backend methods for particle injections """
2+
3+
from functools import cached_property
4+
5+
import numba
6+
7+
from PySDM.backends.impl_common.backend_methods import BackendMethods
8+
9+
10+
class SeedingMethods(BackendMethods): # pylint: disable=too-few-public-methods
11+
@cached_property
12+
def _seeding(self):
13+
@numba.njit(**{**self.default_jit_flags, "parallel": False})
14+
def body( # pylint: disable=too-many-arguments
15+
idx,
16+
multiplicity,
17+
extensive_attributes,
18+
seeded_particle_index,
19+
seeded_particle_multiplicity,
20+
seeded_particle_extensive_attributes,
21+
number_of_super_particles_to_inject: int,
22+
):
23+
number_of_super_particles_already_injected = 0
24+
# TODO #1387 start enumerating from the end of valid particle set
25+
for i, mult in enumerate(multiplicity):
26+
if (
27+
number_of_super_particles_to_inject
28+
== number_of_super_particles_already_injected
29+
):
30+
break
31+
if mult == 0:
32+
idx[i] = -1
33+
s = seeded_particle_index[
34+
number_of_super_particles_already_injected
35+
]
36+
number_of_super_particles_already_injected += 1
37+
multiplicity[i] = seeded_particle_multiplicity[s]
38+
for a in range(len(extensive_attributes)):
39+
extensive_attributes[a, i] = (
40+
seeded_particle_extensive_attributes[a, s]
41+
)
42+
assert (
43+
number_of_super_particles_to_inject
44+
== number_of_super_particles_already_injected
45+
)
46+
47+
return body
48+
49+
def seeding(
50+
self,
51+
*,
52+
idx,
53+
multiplicity,
54+
extensive_attributes,
55+
seeded_particle_index,
56+
seeded_particle_multiplicity,
57+
seeded_particle_extensive_attributes,
58+
number_of_super_particles_to_inject: int,
59+
):
60+
self._seeding(
61+
idx=idx.data,
62+
multiplicity=multiplicity.data,
63+
extensive_attributes=extensive_attributes.data,
64+
seeded_particle_index=seeded_particle_index.data,
65+
seeded_particle_multiplicity=seeded_particle_multiplicity.data,
66+
seeded_particle_extensive_attributes=seeded_particle_extensive_attributes.data,
67+
number_of_super_particles_to_inject=number_of_super_particles_to_inject,
68+
)

PySDM/backends/numba.py

+2
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ class Numba( # pylint: disable=too-many-ancestors,duplicate-code
2828
methods.DisplacementMethods,
2929
methods.TerminalVelocityMethods,
3030
methods.IsotopeMethods,
31+
methods.SeedingMethods,
3132
):
3233
Storage = ImportedStorage
3334
Random = ImportedRandom
@@ -75,3 +76,4 @@ def __init__(self, formulae=None, double_precision=True, override_jit_flags=None
7576
methods.DisplacementMethods.__init__(self)
7677
methods.TerminalVelocityMethods.__init__(self)
7778
methods.IsotopeMethods.__init__(self)
79+
methods.SeedingMethods.__init__(self)

PySDM/builder.py

+4
Original file line numberDiff line numberDiff line change
@@ -150,4 +150,8 @@ def build(
150150
for key in self.particulator.dynamics:
151151
self.particulator.timers[key] = WallTimer()
152152

153+
if (attributes["multiplicity"] == 0).any():
154+
self.particulator.attributes.healthy = False
155+
self.particulator.attributes.sanitize()
156+
153157
return self.particulator

PySDM/dynamics/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@
1515
from PySDM.dynamics.eulerian_advection import EulerianAdvection
1616
from PySDM.dynamics.freezing import Freezing
1717
from PySDM.dynamics.relaxed_velocity import RelaxedVelocity
18+
from PySDM.dynamics.seeding import Seeding

PySDM/dynamics/seeding.py

+94
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
""" particle injection handling, requires initalising a simulation with
2+
enough particles flagged with NaN multiplicity (translated to zeros
3+
at multiplicity discretisation """
4+
5+
from collections.abc import Sized
6+
7+
import numpy as np
8+
9+
from PySDM.dynamics.impl import register_dynamic
10+
from PySDM.initialisation import discretise_multiplicities
11+
12+
13+
@register_dynamic()
14+
class Seeding:
15+
def __init__(
16+
self,
17+
*,
18+
super_droplet_injection_rate: callable,
19+
seeded_particle_extensive_attributes: dict,
20+
seeded_particle_multiplicity: Sized,
21+
):
22+
for attr in seeded_particle_extensive_attributes.values():
23+
assert len(seeded_particle_multiplicity) == len(attr)
24+
self.particulator = None
25+
self.super_droplet_injection_rate = super_droplet_injection_rate
26+
self.seeded_particle_extensive_attributes = seeded_particle_extensive_attributes
27+
self.seeded_particle_multiplicity = seeded_particle_multiplicity
28+
self.rnd = None
29+
self.u01 = None
30+
self.index = None
31+
32+
def register(self, builder):
33+
self.particulator = builder.particulator
34+
35+
def post_register_setup_when_attributes_are_known(self):
36+
if tuple(self.particulator.attributes.get_extensive_attribute_keys()) != tuple(
37+
self.seeded_particle_extensive_attributes.keys()
38+
):
39+
raise ValueError(
40+
f"extensive attributes ({self.seeded_particle_extensive_attributes.keys()})"
41+
" do not match those used in particulator"
42+
f" ({self.particulator.attributes.get_extensive_attribute_keys()})"
43+
)
44+
45+
self.index = self.particulator.Index.identity_index(
46+
len(self.seeded_particle_multiplicity)
47+
)
48+
if len(self.seeded_particle_multiplicity) > 1:
49+
self.rnd = self.particulator.Random(
50+
len(self.seeded_particle_multiplicity), self.particulator.formulae.seed
51+
)
52+
self.u01 = self.particulator.Storage.empty(
53+
len(self.seeded_particle_multiplicity), dtype=float
54+
)
55+
self.seeded_particle_multiplicity = (
56+
self.particulator.IndexedStorage.from_ndarray(
57+
self.index,
58+
discretise_multiplicities(
59+
np.asarray(self.seeded_particle_multiplicity)
60+
),
61+
)
62+
)
63+
self.seeded_particle_extensive_attributes = (
64+
self.particulator.IndexedStorage.from_ndarray(
65+
self.index,
66+
np.asarray(list(self.seeded_particle_extensive_attributes.values())),
67+
)
68+
)
69+
70+
def __call__(self):
71+
if self.particulator.n_steps == 0:
72+
self.post_register_setup_when_attributes_are_known()
73+
74+
time = self.particulator.n_steps * self.particulator.dt
75+
number_of_super_particles_to_inject = self.super_droplet_injection_rate(time)
76+
77+
if number_of_super_particles_to_inject > 0:
78+
assert number_of_super_particles_to_inject <= len(
79+
self.seeded_particle_multiplicity
80+
)
81+
82+
if self.rnd is not None:
83+
self.u01.urand(self.rnd)
84+
# TODO #1387 make shuffle smarter
85+
# e.g. don't need to shuffle if only one type of seed particle
86+
# or if the number of super particles to inject
87+
# is equal to the number of possible seeds
88+
self.index.shuffle(self.u01)
89+
self.particulator.seeding(
90+
seeded_particle_index=self.index,
91+
number_of_super_particles_to_inject=number_of_super_particles_to_inject,
92+
seeded_particle_multiplicity=self.seeded_particle_multiplicity,
93+
seeded_particle_extensive_attributes=self.seeded_particle_extensive_attributes,
94+
)

PySDM/impl/particle_attributes.py

+5
Original file line numberDiff line numberDiff line change
@@ -118,3 +118,8 @@ def get_extensive_attribute_keys(self):
118118

119119
def has_attribute(self, attr):
120120
return attr in self.__attributes
121+
122+
def reset_idx(self):
123+
self.__valid_n_sd = self.__idx.shape[0]
124+
self.__idx.reset_index()
125+
self.healthy = False

PySDM/initialisation/discretise_multiplicities.py

+13-7
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,27 @@
66

77

88
def discretise_multiplicities(values_arg):
9-
values_int = values_arg.round().astype(np.int64)
9+
"""any NaN values in the input array are ignored and flagged
10+
with zero multiplicities in the output array"""
11+
12+
values_int = np.where(np.isnan(values_arg), 0, values_arg).round().astype(np.int64)
1013

1114
if np.issubdtype(values_arg.dtype, np.floating):
15+
if np.isnan(values_arg).all():
16+
return values_int
17+
18+
if not np.logical_or(values_int > 0, np.isnan(values_arg)).all():
19+
raise ValueError(
20+
f"int-casting resulted in multiplicity of zero (min(y_float)={min(values_arg)})"
21+
)
22+
1223
percent_diff = 100 * abs(
13-
1 - np.sum(values_arg) / np.sum(values_int.astype(float))
24+
1 - np.nansum(values_arg) / np.sum(values_int.astype(float))
1425
)
1526
if percent_diff > 1:
1627
raise ValueError(
1728
f"{percent_diff}% error in total real-droplet number"
1829
f" due to casting multiplicities to ints"
1930
)
2031

21-
if not (values_int > 0).all():
22-
raise ValueError(
23-
f"int-casting resulted in multiplicity of zero (min(y_float)={min(values_arg)})"
24-
)
25-
2632
return values_int

PySDM/initialisation/init_fall_momenta.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55

66
import numpy as np
77

8-
from PySDM.backends import CPU
98
from PySDM.dynamics.terminal_velocity import GunnKinzer1949
109
from PySDM.formulae import Formulae
1110
from PySDM.particulator import Particulator
@@ -31,6 +30,8 @@ def init_fall_momenta(
3130
if zero:
3231
return np.zeros_like(water_mass)
3332

33+
from PySDM.backends import CPU # pylint: disable=import-outside-toplevel
34+
3435
particulator = Particulator(0, CPU(Formulae())) # TODO #1155
3536

3637
approximation = terminal_velocity_approx(particulator=particulator)

PySDM/particulator.py

+42-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414

1515
class Particulator: # pylint: disable=too-many-public-methods,too-many-instance-attributes
16-
def __init__(self, n_sd, backend: BackendMethods):
16+
def __init__(self, n_sd, backend):
1717
assert isinstance(backend, BackendMethods)
1818
self.__n_sd = n_sd
1919

@@ -438,3 +438,44 @@ def isotopic_fractionation(self, heavy_isotopes: tuple):
438438
self.backend.isotopic_fractionation()
439439
for isotope in heavy_isotopes:
440440
self.attributes.mark_updated(f"moles_{isotope}")
441+
442+
def seeding(
443+
self,
444+
*,
445+
seeded_particle_index,
446+
seeded_particle_multiplicity,
447+
seeded_particle_extensive_attributes,
448+
number_of_super_particles_to_inject,
449+
):
450+
n_null = self.n_sd - self.attributes.super_droplet_count
451+
if n_null == 0:
452+
raise ValueError(
453+
"No available seeds to inject. Please provide particles with nan filled attributes."
454+
)
455+
456+
if number_of_super_particles_to_inject > n_null:
457+
raise ValueError(
458+
"Trying to inject more super particles than space available."
459+
)
460+
461+
if number_of_super_particles_to_inject > len(seeded_particle_multiplicity):
462+
raise ValueError(
463+
"Trying to inject multiple super particles with the same attributes. \
464+
Instead increase multiplicity of injected particles."
465+
)
466+
467+
self.backend.seeding(
468+
idx=self.attributes._ParticleAttributes__idx,
469+
multiplicity=self.attributes["multiplicity"],
470+
extensive_attributes=self.attributes.get_extensive_attribute_storage(),
471+
seeded_particle_index=seeded_particle_index,
472+
seeded_particle_multiplicity=seeded_particle_multiplicity,
473+
seeded_particle_extensive_attributes=seeded_particle_extensive_attributes,
474+
number_of_super_particles_to_inject=number_of_super_particles_to_inject,
475+
)
476+
self.attributes.reset_idx()
477+
self.attributes.sanitize()
478+
479+
self.attributes.mark_updated("multiplicity")
480+
for key in self.attributes.get_extensive_attribute_keys():
481+
self.attributes.mark_updated(key)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from .settings import Settings
2+
from .simulation import Simulation

0 commit comments

Comments
 (0)