From dc1af1ccb59258c36bf7d9bd00cc3aa0759b395b Mon Sep 17 00:00:00 2001 From: SexyERIC0723 Date: Tue, 31 Mar 2026 23:52:14 +0100 Subject: [PATCH 1/2] Replace direct np.random.* calls with np.random.RandomState instances Replace global `np.random` function calls with proper `RandomState` instances for reproducibility: - transforms/utils.py: Replace `np.random.random.__self__` (3 sites) with `np.random.RandomState()` in generate_pos_neg_label_crop_centers, weighted_patch_samples, and get_extreme_points - transforms/signal/array.py: Replace `np.random.choice` (2 sites) with `self.R.choice` in SignalRandAddSine and SignalRandAddSquarePulsePartial (classes already inherit Randomizable) - data/synthetic.py: Replace `np.random.random.__self__` (2 sites) with `np.random.RandomState()` in create_test_image_2d/3d - data/utils.py: Replace `np.random.randint` fallback with `np.random.RandomState().randint` in get_random_patch - utils/ordering.py: Replace `np.random.shuffle` with `np.random.RandomState().shuffle` in random ordering Ref #6888 Signed-off-by: SexyERIC0723 --- monai/data/synthetic.py | 4 ++-- monai/data/utils.py | 2 +- monai/transforms/signal/array.py | 4 ++-- monai/transforms/utils.py | 6 +++--- monai/utils/ordering.py | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/monai/data/synthetic.py b/monai/data/synthetic.py index 97ed57ba7c..e0b2c977af 100644 --- a/monai/data/synthetic.py +++ b/monai/data/synthetic.py @@ -62,7 +62,7 @@ def create_test_image_2d( raise ValueError(f"the minimal size {min_size} of the image should be larger than `2 * rad_max` 2x{rad_max}.") image = np.zeros((height, width)) - rs: np.random.RandomState = np.random.random.__self__ if random_state is None else random_state # type: ignore + rs: np.random.RandomState = np.random.RandomState() if random_state is None else random_state for _ in range(num_objs): x = rs.randint(rad_max, height - rad_max) @@ -139,7 +139,7 @@ def create_test_image_3d( raise ValueError(f"the minimal size {min_size} of the image should be larger than `2 * rad_max` 2x{rad_max}.") image = np.zeros((height, width, depth)) - rs: np.random.RandomState = np.random.random.__self__ if random_state is None else random_state # type: ignore + rs: np.random.RandomState = np.random.RandomState() if random_state is None else random_state for _ in range(num_objs): x = rs.randint(rad_max, height - rad_max) diff --git a/monai/data/utils.py b/monai/data/utils.py index 4e5a3bd7f6..b77ca9c0d3 100644 --- a/monai/data/utils.py +++ b/monai/data/utils.py @@ -120,7 +120,7 @@ def get_random_patch( """ # choose the minimal corner of the patch - rand_int = np.random.randint if rand_state is None else rand_state.randint + rand_int = np.random.RandomState().randint if rand_state is None else rand_state.randint min_corner = tuple(rand_int(0, ms - ps + 1) if ms > ps else 0 for ms, ps in zip(dims, patch_size)) # create the slices for each dimension which define the patch in the source array diff --git a/monai/transforms/signal/array.py b/monai/transforms/signal/array.py index 2f5f83e5b6..c824f2a841 100644 --- a/monai/transforms/signal/array.py +++ b/monai/transforms/signal/array.py @@ -273,7 +273,7 @@ def __call__(self, signal: NdarrayOrTensor) -> NdarrayOrTensor: data = convert_to_tensor(self.freqs * time_partial) sine_partial = self.magnitude * torch.sin(data) - loc = np.random.choice(range(length)) + loc = self.R.choice(range(length)) signal = paste(signal, sine_partial, (loc,)) return signal @@ -354,7 +354,7 @@ def __call__(self, signal: NdarrayOrTensor) -> NdarrayOrTensor: time_partial = np.arange(0, round(self.fracs * length), 1) squaredpulse_partial = self.magnitude * squarepulse(self.freqs * time_partial) - loc = np.random.choice(range(length)) + loc = self.R.choice(range(length)) signal = paste(signal, squaredpulse_partial, (loc,)) return signal diff --git a/monai/transforms/utils.py b/monai/transforms/utils.py index 4bfb8e3edd..eaf96661b2 100644 --- a/monai/transforms/utils.py +++ b/monai/transforms/utils.py @@ -666,7 +666,7 @@ def generate_pos_neg_label_crop_centers( """ if rand_state is None: - rand_state = np.random.random.__self__ # type: ignore + rand_state = np.random.RandomState() centers = [] fg_indices = np.asarray(fg_indices) if isinstance(fg_indices, Sequence) else fg_indices @@ -721,7 +721,7 @@ def generate_label_classes_crop_centers( """ if rand_state is None: - rand_state = np.random.random.__self__ # type: ignore + rand_state = np.random.RandomState() if num_samples < 1: raise ValueError(f"num_samples must be an int number and greater than 0, got {num_samples}.") @@ -1585,7 +1585,7 @@ def get_extreme_points( """ check_non_lazy_pending_ops(img, name="get_extreme_points") if rand_state is None: - rand_state = np.random.random.__self__ # type: ignore + rand_state = np.random.RandomState() indices = where(img != background) if np.size(indices[0]) == 0: raise ValueError("get_extreme_points: no foreground object in mask!") diff --git a/monai/utils/ordering.py b/monai/utils/ordering.py index 1be61f98ab..2c90de2d08 100644 --- a/monai/utils/ordering.py +++ b/monai/utils/ordering.py @@ -202,6 +202,6 @@ def random_idx(rows: int, cols: int, depths: int | None = None) -> np.ndarray: idx.append((r, c)) idx_np = np.array(idx) - np.random.shuffle(idx_np) + np.random.RandomState().shuffle(idx_np) return idx_np From 2bd0f0210810daba6167440b5354f63ca3701166 Mon Sep 17 00:00:00 2001 From: SexyERIC0723 Date: Wed, 1 Apr 2026 02:19:24 +0100 Subject: [PATCH 2/2] Revert global RNG changes that broke reproducibility The previous commit replaced global np.random.* calls with unseeded np.random.RandomState() instances. This broke 12 tests because callers relying on np.random.seed() for determinism no longer get reproducible results from these public API functions. Revert transforms/utils.py, data/synthetic.py, data/utils.py, and utils/ordering.py back to the global RNG. These utility functions accept an optional rand_state parameter -- when None, they intentionally fall back to the global RNG to respect np.random.seed(). The signal/array.py fix (self.R.choice) is retained because those classes already inherit from Randomizable and use self.R for all other random operations -- np.random.choice was an inconsistency. Signed-off-by: SexyERIC0723 --- monai/data/synthetic.py | 4 ++-- monai/data/utils.py | 2 +- monai/transforms/utils.py | 6 +++--- monai/utils/ordering.py | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/monai/data/synthetic.py b/monai/data/synthetic.py index e0b2c977af..97ed57ba7c 100644 --- a/monai/data/synthetic.py +++ b/monai/data/synthetic.py @@ -62,7 +62,7 @@ def create_test_image_2d( raise ValueError(f"the minimal size {min_size} of the image should be larger than `2 * rad_max` 2x{rad_max}.") image = np.zeros((height, width)) - rs: np.random.RandomState = np.random.RandomState() if random_state is None else random_state + rs: np.random.RandomState = np.random.random.__self__ if random_state is None else random_state # type: ignore for _ in range(num_objs): x = rs.randint(rad_max, height - rad_max) @@ -139,7 +139,7 @@ def create_test_image_3d( raise ValueError(f"the minimal size {min_size} of the image should be larger than `2 * rad_max` 2x{rad_max}.") image = np.zeros((height, width, depth)) - rs: np.random.RandomState = np.random.RandomState() if random_state is None else random_state + rs: np.random.RandomState = np.random.random.__self__ if random_state is None else random_state # type: ignore for _ in range(num_objs): x = rs.randint(rad_max, height - rad_max) diff --git a/monai/data/utils.py b/monai/data/utils.py index b77ca9c0d3..4e5a3bd7f6 100644 --- a/monai/data/utils.py +++ b/monai/data/utils.py @@ -120,7 +120,7 @@ def get_random_patch( """ # choose the minimal corner of the patch - rand_int = np.random.RandomState().randint if rand_state is None else rand_state.randint + rand_int = np.random.randint if rand_state is None else rand_state.randint min_corner = tuple(rand_int(0, ms - ps + 1) if ms > ps else 0 for ms, ps in zip(dims, patch_size)) # create the slices for each dimension which define the patch in the source array diff --git a/monai/transforms/utils.py b/monai/transforms/utils.py index eaf96661b2..4bfb8e3edd 100644 --- a/monai/transforms/utils.py +++ b/monai/transforms/utils.py @@ -666,7 +666,7 @@ def generate_pos_neg_label_crop_centers( """ if rand_state is None: - rand_state = np.random.RandomState() + rand_state = np.random.random.__self__ # type: ignore centers = [] fg_indices = np.asarray(fg_indices) if isinstance(fg_indices, Sequence) else fg_indices @@ -721,7 +721,7 @@ def generate_label_classes_crop_centers( """ if rand_state is None: - rand_state = np.random.RandomState() + rand_state = np.random.random.__self__ # type: ignore if num_samples < 1: raise ValueError(f"num_samples must be an int number and greater than 0, got {num_samples}.") @@ -1585,7 +1585,7 @@ def get_extreme_points( """ check_non_lazy_pending_ops(img, name="get_extreme_points") if rand_state is None: - rand_state = np.random.RandomState() + rand_state = np.random.random.__self__ # type: ignore indices = where(img != background) if np.size(indices[0]) == 0: raise ValueError("get_extreme_points: no foreground object in mask!") diff --git a/monai/utils/ordering.py b/monai/utils/ordering.py index 2c90de2d08..1be61f98ab 100644 --- a/monai/utils/ordering.py +++ b/monai/utils/ordering.py @@ -202,6 +202,6 @@ def random_idx(rows: int, cols: int, depths: int | None = None) -> np.ndarray: idx.append((r, c)) idx_np = np.array(idx) - np.random.RandomState().shuffle(idx_np) + np.random.shuffle(idx_np) return idx_np