Skip to content

Replace direct np.random.* calls with np.random.RandomState instances#8798

Merged
ericspod merged 3 commits intoProject-MONAI:devfrom
haoyu-haoyu:fix/consistent-random-state
Apr 13, 2026
Merged

Replace direct np.random.* calls with np.random.RandomState instances#8798
ericspod merged 3 commits intoProject-MONAI:devfrom
haoyu-haoyu:fix/consistent-random-state

Conversation

@haoyu-haoyu
Copy link
Copy Markdown
Contributor

Description

Replace direct np.random.* global function calls with proper np.random.RandomState instances for reproducibility, as requested in #6888.

Changes

File Call sites Fix
monai/transforms/utils.py 3 × np.random.random.__self__ np.random.RandomState()
monai/transforms/signal/array.py 2 × np.random.choice self.R.choice (classes already Randomizable)
monai/data/synthetic.py 2 × np.random.random.__self__ np.random.RandomState()
monai/data/utils.py 1 × np.random.randint fallback np.random.RandomState().randint
monai/utils/ordering.py 1 × np.random.shuffle np.random.RandomState().shuffle

Scope

This PR covers 9 functional call sites across 5 files. The remaining np.random.* calls in the codebase are either:

  • Docstring examples (auto3dseg, visualize) — educational, not functional
  • np.random.seed in set_determinism() — intentionally sets global state
  • apps/deepedit/ and apps/nuclick/ — would require adding Randomizable inheritance, left for a follow-up

Checks

  • Signed-off-by included (DCO)
  • No new global random state usage introduced
  • Signal transforms use existing self.R from Randomizable base class

Ref #6888

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 Project-MONAI#6888

Signed-off-by: SexyERIC0723 <haoyuwang144@gmail.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 31, 2026

📝 Walkthrough

Walkthrough

This pull request refactors random number generation initialization across five modules in MONAI's data and transform layers. When no explicit random state is provided to functions, the code now creates fresh np.random.RandomState() instances instead of using global RNG sources or deriving from np.random.random.__self__. Affected functions include synthetic image creators, patch extractors, signal transforms, and ordering utilities. The changes standardize RNG initialization without altering function signatures or control flow logic.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed Title clearly summarizes the main change: replacing direct np.random calls with np.random.RandomState instances for better reproducibility.
Description check ✅ Passed PR description fully covers scope, changes across 5 files with specifics, rationale, and limitations. Template requirements met with detailed explanation.
Docstring Coverage ✅ Passed Docstring coverage is 88.89% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@monai/data/synthetic.py`:
- Around line 65-66: The default for random_state in monai/data/synthetic.py
must remain tied to the global numpy RNG so callers using np.random.seed(...)
stay deterministic: when random_state is None, set rs to the global RandomState
(e.g., use np.random.random.__self__ or an equivalent access to the global RNG)
instead of creating a new np.random.RandomState(), and update the
create_test_image_2d docstring to correctly state that the default uses the
global np.random state; ensure the change is applied where rs is set and in the
docstring lines referencing the default behavior so tests like
create_test_image_2d remain deterministic.

In `@monai/data/utils.py`:
- Around line 123-124: The code creates an unseeded RandomState() when
rand_state is None which breaks reproducibility; change the default path to use
the global RNG (np.random.randint) instead of np.random.RandomState(), i.e., use
rand_int = np.random.randint if rand_state is None else rand_state.randint and
keep the existing min_corner generation using rand_int; update callers/tests to
add coverage for get_random_patch(..., rand_state=None) (or the function in this
module that defines rand_state) to assert reproducible behavior after
np.random.seed(...) so the public API remains deterministic when a caller relies
on the global seed.

In `@monai/transforms/utils.py`:
- Around line 668-669: Add unit tests that verify reproducibility when the
functions in monai.transforms.utils that default rand_state to
np.random.RandomState() are called without an explicit rand_state: set a global
seed via np.random.seed(some_int), call the target function twice (back-to-back)
and assert the outputs are identical, and also assert that supplying an explicit
RandomState yields equivalent deterministic behavior; target the three functions
that use "if rand_state is None: rand_state = np.random.RandomState()" so the
tests exercise the default-path (no rand_state argument) and confirm whether
global seeding produces reproducible results.

In `@monai/utils/ordering.py`:
- Around line 204-205: The random ordering branch (ordering_type="random")
creates an unseeded RNG via np.random.RandomState().shuffle(idx_np) which breaks
reproducibility; change this to use the global RNG (np.random.shuffle(idx_np))
or, better, accept an injected seed/RNG on the class/constructor and call
np.random.RandomState(self.rng).shuffle(idx_np) (or use self.rng.shuffle when
passing a RandomState/Generator) so ordering respects seeding; update the code
references in ordering.py where idx_np is shuffled and add unit tests that
exercise the random ordering path to assert reproducible output given the same
seed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 41d8f354-47da-46e8-bca8-6a3dc28a893d

📥 Commits

Reviewing files that changed from the base of the PR and between a8176f1 and dc1af1c.

📒 Files selected for processing (5)
  • monai/data/synthetic.py
  • monai/data/utils.py
  • monai/transforms/signal/array.py
  • monai/transforms/utils.py
  • monai/utils/ordering.py

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 <haoyuwang144@gmail.com>
@haoyu-haoyu
Copy link
Copy Markdown
Contributor Author

Thank you for the review, @coderabbitai! You're absolutely right — replacing the global RNG with unseeded np.random.RandomState() breaks reproducibility for callers relying on np.random.seed().

I've pushed a follow-up commit that reverts the changes to:

  • transforms/utils.py (3 sites)
  • data/synthetic.py (2 sites)
  • data/utils.py (1 site)
  • utils/ordering.py (1 site)

These utility functions accept an optional rand_state parameter and intentionally fall back to the global RNG when None — this is the correct design for public APIs that need to respect np.random.seed().

The only change retained is in signal/array.py: replacing np.random.choice with self.R.choice in SignalRandAddSine and SignalRandAddSquarePulsePartial. These classes already inherit from Randomizable and use self.R for all other random operations — the np.random.choice call was an inconsistency.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 1, 2026

Tip

For best results, initiate chat on the files or code changes.

@haoyu-haoyu Good call on the reverts — that's the right design. Utility functions with an optional rand_state should delegate to the global RNG when None so callers using np.random.seed() get deterministic behavior. The signal/array.py fix is a clean, correct improvement: self.R is exactly the right RNG for Randomizable subclasses.


📝 Learnings were identified and not saved (knowledge base disabled). Enable

Copy link
Copy Markdown

@atharvajoshi01 atharvajoshi01 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clean fix. Using self.R (the RandomState instance) instead of np.random.choice ensures reproducibility when a seed is set. Both call sites in SignalFillEmpty and the square pulse transform had the same issue.

Copy link
Copy Markdown
Member

@ericspod ericspod left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @haoyu-haoyu I see there was more work here that has been reverted, this seems like the right call as pointed out and the remaining changes are fine.

@ericspod ericspod enabled auto-merge (squash) April 13, 2026 21:43
@ericspod ericspod merged commit c13f520 into Project-MONAI:dev Apr 13, 2026
26 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants