11"""
2- spectral sampling logic incl. linear, logarithmic, uniform-random and constant-multiplicity
3- sampling classes
2+ spectral discretisation logic incl. linear, logarithmic, and constant-multiplicity
3+ layouts with deterministic, pseudorandom and quasirandom sampling
44"""
55
6+ import abc
67from typing import Optional , Tuple
78
89import numpy as np
9- from scipy import optimize
10+ from scipy . interpolate import interp1d
1011
1112default_cdf_range = (0.00001 , 0.99999 )
1213
1314
14- class SpectralSampling : # pylint: disable=too-few-public-methods
15- def __init__ (self , spectrum , size_range : Optional [Tuple [float , float ]] = None ):
15+ class SpectralSampling :
16+ def __init__ (
17+ self ,
18+ spectrum ,
19+ * ,
20+ size_range : Optional [Tuple [float , float ]] = None ,
21+ error_threshold : Optional [float ] = None ,
22+ ):
1623 self .spectrum = spectrum
24+ self .error_threshold = error_threshold or 0.01
1725
1826 if size_range is None :
19- if hasattr (spectrum , "percentiles" ):
20- self .size_range = spectrum .percentiles (default_cdf_range )
21- else :
22- self .size_range = [np .nan , np .nan ]
23- for i in (0 , 1 ):
24- result = optimize .root (
25- lambda x , value = default_cdf_range [i ]: spectrum .cdf (x ) - value ,
26- x0 = spectrum .median (),
27- )
28- assert result .success
29- self .size_range [i ] = result .x
27+ self .cdf_range = default_cdf_range
28+ self .size_range = spectrum .percentiles (self .cdf_range )
3029 else :
3130 assert len (size_range ) == 2
3231 assert size_range [0 ] > 0
3332 assert size_range [1 ] > size_range [0 ]
3433 self .size_range = size_range
34+ self .cdf_range = (
35+ spectrum .cdf (size_range [0 ]),
36+ spectrum .cdf (size_range [1 ]),
37+ )
3538
39+ @abc .abstractmethod
40+ def _sample (self , frac_values ):
41+ pass
3642
37- class DeterministicSpectralSampling (
38- SpectralSampling
39- ): # pylint: disable=too-few-public-methods
40- # TODO #1031 - error_threshold will be also used in non-deterministic sampling
41- def __init__ (
42- self ,
43- spectrum ,
44- size_range : Optional [Tuple [float , float ]] = None ,
45- error_threshold : Optional [float ] = None ,
46- ):
47- super ().__init__ (spectrum , size_range )
48- self .error_threshold = error_threshold or 0.01
49-
50- def _sample (self , grid , spectrum ):
43+ def _sample_with_grid (self , grid ):
5144 x = grid [1 :- 1 :2 ]
52- cdf = spectrum .cumulative (grid [0 ::2 ])
45+ cdf = self . spectrum .cumulative (grid [0 ::2 ])
5346 y_float = cdf [1 :] - cdf [0 :- 1 ]
5447
55- diff = abs (1 - np .sum (y_float ) / spectrum .norm_factor )
48+ diff = abs (1 - np .sum (y_float ) / self . spectrum .norm_factor )
5649 if diff > self .error_threshold :
5750 raise ValueError (
5851 f"{ diff * 100 :.3g} % error in total real-droplet number due to sampling "
@@ -61,61 +54,100 @@ def _sample(self, grid, spectrum):
6154
6255 return x , y_float
6356
57+ def sample_deterministic (
58+ self , n_sd , * , backend = None
59+ ): # pylint: disable=unused-argument
60+ return self ._sample (
61+ frac_values = np .linspace (
62+ self .cdf_range [0 ], self .cdf_range [1 ], num = 2 * n_sd + 1
63+ )
64+ )
6465
65- class Linear (DeterministicSpectralSampling ): # pylint: disable=too-few-public-methods
66- def sample (self , n_sd , * , backend = None ): # pylint: disable=unused-argument
67- grid = np .linspace (* self .size_range , num = 2 * n_sd + 1 )
68- return self ._sample (grid , self .spectrum )
66+ def sample_quasirandom (self , n_sd , * , backend ):
67+ num_elements = n_sd
68+ storage = backend .Storage .empty (num_elements , dtype = float )
69+ backend .Random (seed = backend .formulae .seed , size = num_elements )(storage )
70+ u01 = storage .to_ndarray ()
6971
72+ frac_values = np .linspace (
73+ self .cdf_range [0 ], self .cdf_range [1 ], num = 2 * n_sd + 1
74+ )
7075
71- class Logarithmic (
72- DeterministicSpectralSampling
73- ): # pylint: disable=too-few-public-methods
74- def __init__ (
75- self ,
76- spectrum ,
77- size_range : [None , Tuple [float , float ]] = None ,
78- error_threshold : Optional [float ] = None ,
79- ):
80- super ().__init__ (spectrum , size_range , error_threshold )
81- self .start = np .log10 (self .size_range [0 ])
82- self .stop = np .log10 (self .size_range [1 ])
76+ for i in range (1 , len (frac_values ) - 1 , 2 ):
77+ frac_values [i ] = frac_values [i - 1 ] + u01 [i // 2 ] * (
78+ frac_values [i + 1 ] - frac_values [i - 1 ]
79+ )
8380
84- def sample (self , n_sd , * , backend = None ): # pylint: disable=unused-argument
85- grid = np .logspace (self .start , self .stop , num = 2 * n_sd + 1 )
86- return self ._sample (grid , self .spectrum )
81+ return self ._sample (frac_values = frac_values )
8782
83+ def sample_pseudorandom (self , n_sd , * , backend ):
84+ num_elements = 2 * n_sd + 1
85+ storage = backend .Storage .empty (num_elements , dtype = float )
86+ backend .Random (seed = backend .formulae .seed , size = num_elements )(storage )
87+ u01 = storage .to_ndarray ()
88+
89+ frac_values = np .sort (
90+ self .cdf_range [0 ] + u01 * (self .cdf_range [1 ] - self .cdf_range [0 ])
91+ )
92+ return self ._sample (frac_values = frac_values )
8893
89- class ConstantMultiplicity (
90- DeterministicSpectralSampling
91- ): # pylint: disable=too-few-public-methods
92- def __init__ (self , spectrum , size_range = None ):
93- super ().__init__ (spectrum , size_range )
9494
95- self .cdf_range = (
96- spectrum .cumulative (self .size_range [0 ]),
97- spectrum .cumulative (self .size_range [1 ]),
95+ class Logarithmic (SpectralSampling ):
96+ def _sample (self , frac_values ):
97+ grid = np .exp (
98+ (np .log (self .size_range [1 ]) - np .log (self .size_range [0 ])) * frac_values
99+ + np .log (self .size_range [0 ])
98100 )
99- assert 0 < self .cdf_range [ 0 ] < self . cdf_range [ 1 ]
101+ return self ._sample_with_grid ( grid )
100102
101- def sample (self , n_sd , * , backend = None ): # pylint: disable=unused-argument
102- cdf_arg = np .linspace (self .cdf_range [0 ], self .cdf_range [1 ], num = 2 * n_sd + 1 )
103- cdf_arg /= self .spectrum .norm_factor
104- percentiles = self .spectrum .percentiles (cdf_arg )
105103
106- assert np .isfinite (percentiles ).all ()
104+ class Linear (SpectralSampling ):
105+ def _sample (self , frac_values ):
106+ grid = self .size_range [0 ] + frac_values * (
107+ self .size_range [1 ] - self .size_range [0 ]
108+ )
109+ return self ._sample_with_grid (grid )
107110
108- return self ._sample (percentiles , self .spectrum )
109111
112+ class ConstantMultiplicity (SpectralSampling ):
113+ def _sample (self , frac_values ):
114+ grid = self .spectrum .percentiles (frac_values )
115+ assert np .isfinite (grid ).all ()
116+ return self ._sample_with_grid (grid )
110117
111- class UniformRandom (SpectralSampling ): # pylint: disable=too-few-public-methods
112- def sample (self , n_sd , * , backend ):
113- n_elements = n_sd
114- storage = backend .Storage .empty (n_elements , dtype = float )
115- backend .Random (seed = backend .formulae .seed , size = n_elements )(storage )
116- u01 = storage .to_ndarray ()
117118
118- pdf_arg = self .size_range [0 ] + u01 * (self .size_range [1 ] - self .size_range [0 ])
119- dr = abs (self .size_range [1 ] - self .size_range [0 ]) / n_sd
120- # TODO #1031 - should also handle error_threshold check
121- return pdf_arg , dr * self .spectrum .size_distribution (pdf_arg )
119+ class AlphaSampling (SpectralSampling ):
120+ """as in [Matsushima et al. 2023](https://doi.org/10.5194/gmd-16-6211-2023)"""
121+
122+ def __init__ (
123+ self ,
124+ spectrum ,
125+ * ,
126+ alpha ,
127+ dist_1_inv ,
128+ interp_points ,
129+ size_range = None ,
130+ error_threshold : Optional [float ] = None ,
131+ ):
132+ super ().__init__ (
133+ spectrum , size_range = size_range , error_threshold = error_threshold
134+ )
135+ self .alpha = alpha
136+ self .dist_0_cdf = self .spectrum .cdf
137+ self .dist_1_inv = dist_1_inv
138+ self .x_prime = np .linspace (
139+ self .size_range [0 ], self .size_range [1 ], num = interp_points
140+ )
141+
142+ def _sample (self , frac_values ):
143+ if self .alpha == 0 :
144+ frac_values = self .spectrum .percentiles (frac_values )
145+ elif self .alpha == 1 :
146+ frac_values = self .dist_1_inv (frac_values , self .size_range )
147+ else :
148+ sd_cdf = self .dist_0_cdf (self .x_prime )
149+ x_sd_cdf = (1 - self .alpha ) * self .x_prime + self .alpha * self .dist_1_inv (
150+ sd_cdf , self .size_range
151+ )
152+ frac_values = interp1d (sd_cdf , x_sd_cdf )(frac_values )
153+ return self ._sample_with_grid (frac_values )
0 commit comments