From 5526cd8d51cd9263960a9a932f96d63a112f5b53 Mon Sep 17 00:00:00 2001 From: thc1006 <84045975+thc1006@users.noreply.github.com> Date: Sat, 8 Aug 2026 08:31:27 +0800 Subject: [PATCH] DOC: make the CustomSampler examples answer to their seed 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> --- docs/user/custom_sampler.rst | 50 ++++++++++++++++++++++++------------ 1 file changed, 33 insertions(+), 17 deletions(-) diff --git a/docs/user/custom_sampler.rst b/docs/user/custom_sampler.rst index 8994c9573..640167320 100644 --- a/docs/user/custom_sampler.rst +++ b/docs/user/custom_sampler.rst @@ -52,7 +52,7 @@ distributions. 2-Tuple that contains the probability of each normal distribution of the mixture. Its entries should be non-negative and sum up to 1. """ - np.random.default_rng(seed) + self.reset_seed(seed) self.means_tuple = means_tuple self.sd_tuple = sd_tuple self.prob_tuple = prob_tuple @@ -71,12 +71,12 @@ distributions. List containing n_samples samples """ samples_list = [0] * n_samples - mixture_id_list = np.random.binomial(1, self.prob_tuple[0], n_samples) + mixture_id_list = self.rng.binomial(1, self.prob_tuple[0], n_samples) for i, mixture_id in enumerate(mixture_id_list): if mixture_id: - samples_list[i] = np.random.normal(self.means_tuple[0], self.sd_tuple[0]) + samples_list[i] = self.rng.normal(self.means_tuple[0], self.sd_tuple[0]) else: - samples_list[i] = np.random.normal(self.means_tuple[1], self.sd_tuple[1]) + samples_list[i] = self.rng.normal(self.means_tuple[1], self.sd_tuple[1]) return samples_list @@ -88,7 +88,14 @@ distributions. seed : int, optional Seed for the random number generator. """ - np.random.default_rng(seed) + self.rng = np.random.default_rng(seed) + +.. warning:: + Keep the generator on the instance and draw from it in *sample*. Calling + ``np.random.default_rng(seed)`` and discarding the result is a no-op, and + *sample* then draws from the process-global ``np.random`` instead, which + ``random_seed`` does not reach. The study still runs; it is simply not + reproducible, and nothing says so. This is an example of a distribution that is not implemented in numpy. Note that it is a general distribution, so we can use it for many different variables. @@ -216,14 +223,9 @@ below implements an example of such a generator seed : int, optional Number to seed random generator, by default None """ - np.random.default_rng(seed) - self.samples_list = [] - self.samples_generated = 0 - self.used_samples_x = 0 - self.used_samples_y = 0 self.mean = mean self.cov = cov - self.generate_samples(1000) + self.reset_seed(seed) def generate_samples(self, n_samples = 1): """Generate samples from bivariate Gaussian and append to sample list @@ -233,23 +235,37 @@ below implements an example of such a generator n_samples : int, optional Number of samples to be generated, by default 1 """ - samples_generated = np.random.multivariate_normal(self.mean, self.cov, n_samples) + samples_generated = self.rng.multivariate_normal(self.mean, self.cov, n_samples) self.samples_generated += n_samples self.samples_list += list(samples_generated) def reset_seed(self, seed=None): - np.random.default_rng(seed) + """Reseeds the generator and discards the samples drawn before it + + The cached samples came from the previous generator, so keeping them + would let the first 1000 draws after a reseed ignore the new seed. + """ + self.rng = np.random.default_rng(seed) + self.samples_list = [] + self.samples_generated = 0 + self.used_samples_x = 0 + self.used_samples_y = 0 + self.generate_samples(1000) + + def top_up(self, used_samples, n_samples): + """Generates enough samples to cover the request, if it is short""" + shortfall = used_samples + n_samples - self.samples_generated + if shortfall > 0: + self.generate_samples(shortfall) def get_samples(self, n_samples, axis): if axis == "x": - if self.samples_generated < self.used_samples_x: - self.generate_samples(n_samples) + self.top_up(self.used_samples_x, n_samples) samples_list = [ sample[0] for sample in self.samples_list[self.used_samples_x:(self.used_samples_x + n_samples)] ] if axis == "y": - if self.samples_generated < self.used_samples_y: - self.generate_samples(n_samples) + self.top_up(self.used_samples_y, n_samples) samples_list = [ sample[1] for sample in self.samples_list[self.used_samples_y:(self.used_samples_y + n_samples)] ]