From 0b1b06479961f92394cc99c128f3310989f9de68 Mon Sep 17 00:00:00 2001 From: Kieran Leschinski Date: Fri, 14 Aug 2026 08:47:54 +0200 Subject: [PATCH] Resolve !SIM.random.seed per apply_to call, not once PoorMansHxRGReadoutNoise.apply_to and ShotNoise.apply_to wrote the resolved seed value back over the !SIM.random.seed reference in self.meta on their first call (ShotNoise directly; the readout noise additionally via the whole-meta from_currsys rebake). Any subsequent readout from a reused OpticalTrain therefore re-seeded with the FIRST exposure''s seed, no matter what !SIM.random.seed was updated to - which makes repeated-readout workflows (N darks or a detector-linearity ramp from one observe) statistically correct but impossible to reproduce bit-exactly, while single-readout runs are unaffected. Resolve into locals instead, exactly as BasicReadoutNoise and PixelResponseNonUniformity already do. First-call behaviour, resolution order and generated values are unchanged; only repeated calls now honour a per-readout seed. Measured downstream (METIS_Simulations_Deux differential harness, harness/exp_reuse_train.py): with this fix, one observe + N readouts at per-frame seeds reproduces N independently-simulated dark frames; before it, readouts 2..N carried the frame-1 readout-noise pattern rolled to random offsets (sigma matching to 4e-7 but ~92% of pixels differing). Co-Authored-By: Claude Fable 5 --- scopesim/effects/electronic/noise.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/scopesim/effects/electronic/noise.py b/scopesim/effects/electronic/noise.py index 0698c09f5..2093886b3 100644 --- a/scopesim/effects/electronic/noise.py +++ b/scopesim/effects/electronic/noise.py @@ -59,20 +59,23 @@ def apply_to(self, det, **kwargs): if not isinstance(det, Detector): return det - self.meta["random_seed"] = from_currsys(self.meta["random_seed"], - self.cmds) - if self.meta["random_seed"] is not None: - np.random.seed(self.meta["random_seed"]) + # Resolve into locals rather than back into self.meta: writing the + # resolved value over the "!SIM.random.seed" reference froze the + # first frame's seed into the effect, so repeated readouts from a + # reused OpticalTrain could never be reseeded per exposure. + random_seed = from_currsys(self.meta["random_seed"], self.cmds) + if random_seed is not None: + np.random.seed(random_seed) - self.meta = from_currsys(self.meta, self.cmds) ron_keys = ["noise_std", "n_channels", "channel_fraction", "line_fraction", "pedestal_fraction", "read_fraction"] - ron_kwargs = {key: self.meta[key] for key in ron_keys} + ron_kwargs = {key: from_currsys(self.meta[key], self.cmds) + for key in ron_keys} ron_kwargs["image_shape"] = det._hdu.data.shape ron_frame = _make_ron_frame(**ron_kwargs) stacked_ron_frame = np.zeros_like(ron_frame) - for i in range(self.meta["ndit"]): + for i in range(from_currsys(self.meta["ndit"], self.cmds)): dx = np.random.randint(0, ron_frame.shape[1]) dy = np.random.randint(0, ron_frame.shape[0]) stacked_ron_frame += np.roll(ron_frame, (dy, dx), axis=(0, 1)) @@ -240,9 +243,10 @@ def apply_to(self, det, **kwargs): if not isinstance(det, Detector): return det - self.meta["random_seed"] = from_currsys(self.meta["random_seed"], - self.cmds) - rng = np.random.default_rng(self.meta["random_seed"]) + # Local resolution (see PoorMansHxRGReadoutNoise.apply_to): keep the + # "!SIM.random.seed" reference intact so every readout re-resolves it. + random_seed = from_currsys(self.meta["random_seed"], self.cmds) + rng = np.random.default_rng(random_seed) # numpy has a problem with generating Poisson distributions above # certain values. E.g. on linux, numpy.random.poisson(1e20) raises