Skip to content

Commit eb4c52b

Browse files
slayooabulenok
andauthored
make spatial, spectral and spectro-glacial sampling reproducible if seed set in Formulae (#1026)
Co-authored-by: Oleksii Bulenok <[email protected]>
1 parent 5475562 commit eb4c52b

File tree

13 files changed

+183
-76
lines changed

13 files changed

+183
-76
lines changed

PySDM/backends/impl_numba/storage_impl.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,3 @@ def power(output, exponent):
7070
@numba.njit(**{**conf.JIT_FLAGS, **{"parallel": False}})
7171
def subtract(output, subtrahend):
7272
output[:] -= subtrahend[:]
73-
74-
75-
# @numba.njit(void(f8[:]), **conf.JIT_FLAGS)
76-
def urand(output):
77-
# TODO #986: seed
78-
output.data[:] = np.random.uniform(0, 1, output.shape)

PySDM/environments/box.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,6 @@ def register(self, builder):
3030
def init_attributes(self, *, spectral_discretisation):
3131
attributes = {}
3232
attributes["volume"], attributes["n"] = spectral_discretisation.sample(
33-
self.particulator.n_sd
33+
backend=self.particulator.backend, n_sd=self.particulator.n_sd
3434
)
3535
return attributes

PySDM/environments/kinematic_1d.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,15 +40,19 @@ def init_attributes(
4040
attributes = {}
4141
with np.errstate(all="raise"):
4242
positions = spatial_discretisation.sample(
43-
self.mesh.grid, self.particulator.n_sd
43+
backend=self.particulator.backend,
44+
grid=self.mesh.grid,
45+
n_sd=self.particulator.n_sd,
4446
)
4547
(
4648
attributes["cell id"],
4749
attributes["cell origin"],
4850
attributes["position in cell"],
4951
) = self.mesh.cellular_attributes(positions)
5052

51-
r_dry, n_per_kg = spectral_discretisation.sample(self.particulator.n_sd)
53+
r_dry, n_per_kg = spectral_discretisation.sample(
54+
backend=self.particulator.backend, n_sd=self.particulator.n_sd
55+
)
5256
attributes["dry volume"] = self.formulae.trivia.volume(radius=r_dry)
5357
attributes["kappa times dry volume"] = attributes["dry volume"] * kappa
5458
r_wet = equilibrate_wet_radii(

PySDM/environments/kinematic_2d.py

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
default_rtol,
1212
equilibrate_wet_radii,
1313
)
14-
from PySDM.initialisation.sampling import spectral_sampling
14+
from PySDM.initialisation.sampling.spectral_sampling import ConstantMultiplicity
1515

1616
from ..impl import arakawa_c
1717

@@ -42,30 +42,26 @@ def init_attributes(
4242
kappa,
4343
dry_radius_spectrum,
4444
rtol=default_rtol,
45-
n_sd=None
45+
n_sd=None,
46+
spectral_sampling=ConstantMultiplicity
4647
):
4748
super().sync()
4849
self.notify()
4950
n_sd = n_sd or self.particulator.n_sd
5051
attributes = {}
5152
with np.errstate(all="raise"):
52-
positions = spatial_discretisation.sample(self.mesh.grid, n_sd)
53-
(
54-
attributes["cell id"],
55-
attributes["cell origin"],
56-
attributes["position in cell"],
57-
) = self.mesh.cellular_attributes(positions)
58-
with np.errstate(all="raise"):
59-
positions = spatial_discretisation.sample(self.mesh.grid, n_sd)
53+
positions = spatial_discretisation.sample(
54+
backend=self.particulator.backend, grid=self.mesh.grid, n_sd=n_sd
55+
)
6056
(
6157
attributes["cell id"],
6258
attributes["cell origin"],
6359
attributes["position in cell"],
6460
) = self.mesh.cellular_attributes(positions)
6561

66-
r_dry, n_per_kg = spectral_sampling.ConstantMultiplicity(
67-
spectrum=dry_radius_spectrum
68-
).sample(n_sd)
62+
r_dry, n_per_kg = spectral_sampling(spectrum=dry_radius_spectrum).sample(
63+
n_sd=n_sd, backend=self.particulator.backend
64+
)
6965

7066
attributes["dry volume"] = self.formulae.trivia.volume(radius=r_dry)
7167
attributes["kappa times dry volume"] = kappa * attributes["dry volume"]
Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
11
"""
22
spatial sampling logic (i.e., physical x-y-z coordinates)
33
"""
4-
import numpy as np
5-
64
# TODO #305 QUASIRANDOM & GRID
75
# http://www.ii.uj.edu.pl/~arabas/workshop_2019/files/talk_Shima.pdf
86

97

108
class Pseudorandom: # pylint: disable=too-few-public-methods
119
@staticmethod
12-
def sample(grid, n_sd):
10+
def sample(*, backend, grid, n_sd):
1311
dimension = len(grid)
14-
positions = np.random.rand(dimension, n_sd)
12+
n_elements = dimension * n_sd
13+
14+
storage = backend.Storage.empty(n_elements, dtype=float)
15+
backend.Random(seed=backend.formulae.seed, size=n_elements)(storage)
16+
positions = storage.to_ndarray().reshape(dimension, n_sd)
17+
1518
for dim in range(dimension):
1619
positions[dim, :] *= grid[dim]
1720
return positions

PySDM/initialisation/sampling/spectral_sampling.py

Lines changed: 32 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,12 @@
77
import numpy as np
88
from scipy import optimize
99

10-
from PySDM.physics import constants as const
11-
1210
default_cdf_range = (0.00001, 0.99999)
1311

1412

1513
class SpectralSampling: # pylint: disable=too-few-public-methods
16-
def __init__(
17-
self,
18-
spectrum,
19-
size_range: [None, Tuple[float, float]] = None,
20-
error_threshold: Optional[float] = None,
21-
):
14+
def __init__(self, spectrum, size_range: Optional[Tuple[float, float]] = None):
2215
self.spectrum = spectrum
23-
self.error_threshold = error_threshold or 0.01
2416

2517
if size_range is None:
2618
if hasattr(spectrum, "percentiles"):
@@ -40,6 +32,20 @@ def __init__(
4032
assert size_range[1] > size_range[0]
4133
self.size_range = size_range
4234

35+
36+
class DeterministicSpectralSampling(
37+
SpectralSampling
38+
): # pylint: disable=too-few-public-methods
39+
# TODO #1031 - error_threshold will be also used in non-deterministic sampling
40+
def __init__(
41+
self,
42+
spectrum,
43+
size_range: Optional[Tuple[float, float]] = None,
44+
error_threshold: Optional[float] = None,
45+
):
46+
super().__init__(spectrum, size_range)
47+
self.error_threshold = error_threshold or 0.01
48+
4349
def _sample(self, grid, spectrum):
4450
x = grid[1:-1:2]
4551
cdf = spectrum.cumulative(grid[0::2])
@@ -54,13 +60,15 @@ def _sample(self, grid, spectrum):
5460
return x, y_float
5561

5662

57-
class Linear(SpectralSampling): # pylint: disable=too-few-public-methods
58-
def sample(self, n_sd):
63+
class Linear(DeterministicSpectralSampling): # pylint: disable=too-few-public-methods
64+
def sample(self, n_sd, *, backend=None): # pylint: disable=unused-argument
5965
grid = np.linspace(*self.size_range, num=2 * n_sd + 1)
6066
return self._sample(grid, self.spectrum)
6167

6268

63-
class Logarithmic(SpectralSampling): # pylint: disable=too-few-public-methods
69+
class Logarithmic(
70+
DeterministicSpectralSampling
71+
): # pylint: disable=too-few-public-methods
6472
def __init__(
6573
self,
6674
spectrum,
@@ -71,12 +79,14 @@ def __init__(
7179
self.start = np.log10(self.size_range[0])
7280
self.stop = np.log10(self.size_range[1])
7381

74-
def sample(self, n_sd):
82+
def sample(self, n_sd, *, backend=None): # pylint: disable=unused-argument
7583
grid = np.logspace(self.start, self.stop, num=2 * n_sd + 1)
7684
return self._sample(grid, self.spectrum)
7785

7886

79-
class ConstantMultiplicity(SpectralSampling): # pylint: disable=too-few-public-methods
87+
class ConstantMultiplicity(
88+
DeterministicSpectralSampling
89+
): # pylint: disable=too-few-public-methods
8090
def __init__(self, spectrum, size_range=None):
8191
super().__init__(spectrum, size_range)
8292

@@ -86,7 +96,7 @@ def __init__(self, spectrum, size_range=None):
8696
)
8797
assert 0 < self.cdf_range[0] < self.cdf_range[1]
8898

89-
def sample(self, n_sd):
99+
def sample(self, n_sd, *, backend=None): # pylint: disable=unused-argument
90100
cdf_arg = np.linspace(self.cdf_range[0], self.cdf_range[1], num=2 * n_sd + 1)
91101
cdf_arg /= self.spectrum.norm_factor
92102
percentiles = self.spectrum.percentiles(cdf_arg)
@@ -97,11 +107,13 @@ def sample(self, n_sd):
97107

98108

99109
class UniformRandom(SpectralSampling): # pylint: disable=too-few-public-methods
100-
def __init__(self, spectrum, size_range=None, seed=const.default_random_seed):
101-
super().__init__(spectrum, size_range)
102-
self.rng = np.random.default_rng(seed)
110+
def sample(self, n_sd, *, backend):
111+
n_elements = n_sd
112+
storage = backend.Storage.empty(n_elements, dtype=float)
113+
backend.Random(seed=backend.formulae.seed, size=n_elements)(storage)
114+
u01 = storage.to_ndarray()
103115

104-
def sample(self, n_sd):
105-
pdf_arg = self.rng.uniform(*self.size_range, n_sd)
116+
pdf_arg = self.size_range[0] + u01 * (self.size_range[1] - self.size_range[0])
106117
dr = abs(self.size_range[1] - self.size_range[0]) / n_sd
118+
# TODO #1031 - should also handle error_threshold check
107119
return pdf_arg, dr * self.spectrum.size_distribution(pdf_arg)

PySDM/initialisation/sampling/spectro_glacial_sampling.py

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,14 @@
55
import numpy as np
66

77
from PySDM.initialisation.sampling.spectral_sampling import default_cdf_range
8-
from PySDM.physics import constants as const
98

109
DIM_TEMP = 0
1110
DIM_SURF = 1
1211
N_DIMS = 2
1312

1413

1514
class SpectroGlacialSampling: # pylint: disable=too-few-public-methods
16-
def __init__(
17-
self,
18-
*,
19-
freezing_temperature_spectrum,
20-
insoluble_surface_spectrum,
21-
seed=const.default_random_seed
22-
):
15+
def __init__(self, *, freezing_temperature_spectrum, insoluble_surface_spectrum):
2316
self.insoluble_surface_spectrum = insoluble_surface_spectrum
2417
self.freezing_temperature_spectrum = freezing_temperature_spectrum
2518

@@ -29,15 +22,20 @@ def __init__(
2922
self.temperature_range = freezing_temperature_spectrum.invcdf(
3023
np.asarray(default_cdf_range), insoluble_surface_spectrum.median
3124
)
32-
self.seed = seed
3325

34-
def sample(self, n_sd):
26+
def sample(self, *, backend, n_sd):
3527
simulated = np.empty((n_sd, N_DIMS))
28+
29+
n_elements = n_sd * N_DIMS
30+
storage = backend.Storage.empty(n_elements, dtype=float)
31+
backend.Random(seed=backend.formulae.seed, size=n_elements)(storage)
32+
random_numbers = storage.to_ndarray().reshape(n_sd, N_DIMS)
33+
3634
simulated[:, DIM_SURF] = self.insoluble_surface_spectrum.percentiles(
37-
np.random.random(n_sd)
35+
random_numbers[:, 0]
3836
)
3937
simulated[:, DIM_TEMP] = self.freezing_temperature_spectrum.invcdf(
40-
np.random.random(n_sd), simulated[:, DIM_SURF]
38+
random_numbers[:, 1], simulated[:, DIM_SURF]
4139
)
4240

4341
return (

test-time-requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ ghapi
66
pytest
77

88
# note: if cloning both PySDM and PySDM examples, consider "pip install -e"
9-
PySDM-examples @ git+https://github.com/open-atmos/PySDM-examples@9142101#egg=PySDM-examples
9+
PySDM-examples @ git+https://github.com/open-atmos/PySDM-examples@4900000#egg=PySDM-examples
1010
PyMPDATA @ git+https://github.com/open-atmos/PyMPDATA@63f1c11#egg=PyMPDATA
1111

1212
PyPartMC==0.0.26

tests/unit_tests/dynamics/collisions/test_sdm_multi_cell.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,9 @@ def test_coalescence_call(n_sd, backend_class, adaptive):
3333
particulator, sut = get_dummy_particulator_and_coalescence(
3434
backend_class, len(n), environment=env
3535
)
36-
cell_id, _, _ = env.mesh.cellular_attributes(Pseudorandom.sample(grid, len(n)))
36+
cell_id, _, _ = env.mesh.cellular_attributes(
37+
Pseudorandom.sample(backend=particulator.backend, grid=grid, n_sd=len(n))
38+
)
3739
attributes = {"n": n, "volume": v, "cell id": cell_id}
3840
particulator.build(attributes)
3941
sut.actual_length = particulator.attributes._ParticleAttributes__idx.length

tests/unit_tests/initialisation/test_aerosol_init.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@
77
from PySDM.initialisation.aerosol_composition import DryAerosolMixture
88
from PySDM.physics import si
99

10-
# spectrum = spectra.Lognormal(norm_factor=100.0 / si.cm**3, m_mode=50.0 * si.nm, s_geom=2.0)
11-
1210

1311
@pytest.mark.parametrize(
1412
"mass_fractions",

0 commit comments

Comments
 (0)