Skip to content

Commit 2cca437

Browse files
authored
DOC: make the CustomSampler examples answer to their seed (#1097)
Both examples build a generator with np.random.default_rng(seed) and drop it, then sample from the process-global np.random. reset_seed is a no-op, so a sampler written by following this page ignores random_seed. The study runs and the numbers look reasonable; only a second run with the same seed shows they were never reproducible. The bivariate generator needs two more things. It caches 1000 samples, so reset_seed has to discard them or the first 1000 draws after a reseed still come from the generator that was replaced. And its refill test compares samples_generated against used_samples, which only becomes true after the cache has run out, so a study longer than the cache silently got short lists: sample(n_samples=1) returns [] at simulation 1001 and dict_generator raises IndexError on [0]. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
1 parent 1691119 commit 2cca437

1 file changed

Lines changed: 33 additions & 17 deletions

File tree

‎docs/user/custom_sampler.rst‎

Lines changed: 33 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ distributions.
5252
2-Tuple that contains the probability of each normal distribution
5353
of the mixture. Its entries should be non-negative and sum up to 1.
5454
"""
55-
np.random.default_rng(seed)
55+
self.reset_seed(seed)
5656
self.means_tuple = means_tuple
5757
self.sd_tuple = sd_tuple
5858
self.prob_tuple = prob_tuple
@@ -71,12 +71,12 @@ distributions.
7171
List containing n_samples samples
7272
"""
7373
samples_list = [0] * n_samples
74-
mixture_id_list = np.random.binomial(1, self.prob_tuple[0], n_samples)
74+
mixture_id_list = self.rng.binomial(1, self.prob_tuple[0], n_samples)
7575
for i, mixture_id in enumerate(mixture_id_list):
7676
if mixture_id:
77-
samples_list[i] = np.random.normal(self.means_tuple[0], self.sd_tuple[0])
77+
samples_list[i] = self.rng.normal(self.means_tuple[0], self.sd_tuple[0])
7878
else:
79-
samples_list[i] = np.random.normal(self.means_tuple[1], self.sd_tuple[1])
79+
samples_list[i] = self.rng.normal(self.means_tuple[1], self.sd_tuple[1])
8080

8181
return samples_list
8282

@@ -88,7 +88,14 @@ distributions.
8888
seed : int, optional
8989
Seed for the random number generator.
9090
"""
91-
np.random.default_rng(seed)
91+
self.rng = np.random.default_rng(seed)
92+
93+
.. warning::
94+
Keep the generator on the instance and draw from it in *sample*. Calling
95+
``np.random.default_rng(seed)`` and discarding the result is a no-op, and
96+
*sample* then draws from the process-global ``np.random`` instead, which
97+
``random_seed`` does not reach. The study still runs; it is simply not
98+
reproducible, and nothing says so.
9299

93100
This is an example of a distribution that is not implemented in numpy. Note that it is
94101
a general distribution, so we can use it for many different variables.
@@ -216,14 +223,9 @@ below implements an example of such a generator
216223
seed : int, optional
217224
Number to seed random generator, by default None
218225
"""
219-
np.random.default_rng(seed)
220-
self.samples_list = []
221-
self.samples_generated = 0
222-
self.used_samples_x = 0
223-
self.used_samples_y = 0
224226
self.mean = mean
225227
self.cov = cov
226-
self.generate_samples(1000)
228+
self.reset_seed(seed)
227229

228230
def generate_samples(self, n_samples = 1):
229231
"""Generate samples from bivariate Gaussian and append to sample list
@@ -233,23 +235,37 @@ below implements an example of such a generator
233235
n_samples : int, optional
234236
Number of samples to be generated, by default 1
235237
"""
236-
samples_generated = np.random.multivariate_normal(self.mean, self.cov, n_samples)
238+
samples_generated = self.rng.multivariate_normal(self.mean, self.cov, n_samples)
237239
self.samples_generated += n_samples
238240
self.samples_list += list(samples_generated)
239241

240242
def reset_seed(self, seed=None):
241-
np.random.default_rng(seed)
243+
"""Reseeds the generator and discards the samples drawn before it
244+
245+
The cached samples came from the previous generator, so keeping them
246+
would let the first 1000 draws after a reseed ignore the new seed.
247+
"""
248+
self.rng = np.random.default_rng(seed)
249+
self.samples_list = []
250+
self.samples_generated = 0
251+
self.used_samples_x = 0
252+
self.used_samples_y = 0
253+
self.generate_samples(1000)
254+
255+
def top_up(self, used_samples, n_samples):
256+
"""Generates enough samples to cover the request, if it is short"""
257+
shortfall = used_samples + n_samples - self.samples_generated
258+
if shortfall > 0:
259+
self.generate_samples(shortfall)
242260

243261
def get_samples(self, n_samples, axis):
244262
if axis == "x":
245-
if self.samples_generated < self.used_samples_x:
246-
self.generate_samples(n_samples)
263+
self.top_up(self.used_samples_x, n_samples)
247264
samples_list = [
248265
sample[0] for sample in self.samples_list[self.used_samples_x:(self.used_samples_x + n_samples)]
249266
]
250267
if axis == "y":
251-
if self.samples_generated < self.used_samples_y:
252-
self.generate_samples(n_samples)
268+
self.top_up(self.used_samples_y, n_samples)
253269
samples_list = [
254270
sample[1] for sample in self.samples_list[self.used_samples_y:(self.used_samples_y + n_samples)]
255271
]

0 commit comments

Comments
 (0)