Skip to content

feat: support optional rng for deterministic scene placement sampling - #300

Open
akushonkamen wants to merge 1 commit into
stepjam:masterfrom
akushonkamen:fix/232-seeded-scene-rng
Open

akushonkamen wants to merge 1 commit into
stepjam:masterfrom
akushonkamen:fix/232-seeded-scene-rng

Conversation

@akushonkamen

Copy link
Copy Markdown

Summary

Resets in RLBench are not reproducible even when the global numpy seed is fixed: object placement sampling at reset time draws exclusively from the global numpy random state, and the number of draws varies with max_attempts retries. This PR adds an optional, explicit rng that is threaded through the placement-sampling call chain, making resets deterministic when a seeded generator is supplied, while leaving all existing callers byte-identical in behavior.

Fixes #232

Root cause

Reset-time object placement sampled exclusively from the global numpy random state:

  • np.random.uniform(...) in BoundaryObject._get_position_within_boundary and in the rotation sample (rlbench/backend/spawn_boundary.py:72-83,103 pre-PR),
  • np.random.choice(...) for boundary selection in SpawnBoundary.sample (rlbench/backend/spawn_boundary.py:159 pre-PR),

consumed via Scene.init_episode / Scene._place_task (rlbench/backend/scene.py:127-146,529-537). Two consequences:

  1. Even with np.random.seed(...) fixed, any other consumer of the global state (task code, other libraries, unrelated resets) shifts every subsequent placement, so episode resets cannot be replayed.
  2. The number of global draws is not fixed: a placement that fails validation inside max_attempts retries consumes extra draws, so a later sampling call's value depends on earlier retries.

Fix

An optional rng parameter (accepts np.random.Generator, np.random.RandomState, or any object exposing uniform + choice) is threaded through the chain:

Environment(rng=...) -> Scene(rng=...) -> SpawnBoundary(boundaries, rng=...)
                                       -> BoundaryObject(boundary, rng=...)

DomainRandomizationScene passes it through as well. Default rng=None falls back to the np.random module, so all ~30 existing task SpawnBoundary(...) sites, tools/task_builder.py:289 and tools/task_validator.py:142 behave exactly as before, with no changes required.

Changes

  • rlbench/backend/spawn_boundary.py: SpawnBoundary.__init__ / BoundaryObject.__init__ accept rng=None; uniform/choice calls route to it; docstring documents the parameter.
  • rlbench/backend/scene.py: Scene.__init__ accepts rng=None and forwards it to every SpawnBoundary construction.
  • rlbench/sim2real/domain_randomization_scene.py: passthrough.
  • rlbench/environment.py: Environment.__init__ accepts rng=None and forwards to Scene.
  • tests/unit/test_spawn_boundary.py (new): CoppeliaSim-free regression tests.

Example

env = Environment(..., rng=np.random.default_rng(0))
# every reset now yields identical placements for the same seed

Testing

New tests in tests/unit/test_spawn_boundary.py run without CoppeliaSim/PyRep (pyrep is stubbed only when it is unimportable, so CI with PyRep installed is unaffected). 7 tests: same-seed placements identical across repeated SpawnBoundary instances; different seed yields different placements; positions stay within boundary; RandomState supported; global-np.random backward-compat fallback; boundary-selection accounting; BoundaryError still raised for oversized objects.

Red → green, verified locally (macOS arm64, no CoppeliaSim/PyRep):

  • Pre-fix code + new test file:
    python3 -m unittest tests.unit.test_spawn_boundary
    → Ran 7 tests ... FAILED (errors=6) — all TypeError: SpawnBoundary.__init__() got an unexpected keyword argument 'rng'; the global-np.random backward-compat guard passed.
  • Fixed code:
    python3 -m unittest tests.unit.test_spawn_boundary -v
    → Ran 7 tests ... OK (7/7).
  • python3 -m py_compile on the 5 touched files → OK.

Not run: the repo CI unit suite (pytest -v -n auto tests/unit per .github/workflows/unit_tests.yml) requires CoppeliaSim + PyRep, which are not installable on this machine; all other pre-existing tests/unit modules fail at import here with ModuleNotFoundError: No module named 'gymnasium' (verified on untouched test_task_sets, pre-existing/environmental). No lint config exists in the repo (no setup.cfg/.flake8/pyproject.toml/tox.ini).

AI disclosure

This fix was developed with the assistance of an AI coding agent (code changes, tests, and PR description drafted by the agent, reviewed and submitted by me).

…jam#232)

Reset-time object placement sampling relied on the global numpy random
state (np.random.uniform/choice in rlbench/backend/spawn_boundary.py),
so resets were not reproducible even with a fixed global seed, and the
number of draws changes when Scene.init_episode retries (max_attempts).

Add an optional rng to SpawnBoundary/BoundaryObject, Scene,
DomainRandomizationScene and Environment. When rng is provided (e.g.
np.random.default_rng(seed)), all placement sampling draws are taken
from it, making placements reproducible independent of the global
numpy state. When omitted, behavior is unchanged (np.random global).

Includes a CoppeliaSim-free unit test for SpawnBoundary determinism
(same seed => identical placements; different seed => different),
RandomState support, unchanged global-np.random fallback, and
BoundaryError accounting.

This branch has not been deployed

No deployments
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.

Consistent resetting through np.random.Generator

1 participant