From 8f7721cbf5a3eb5ce11907fe496b45a37e861ece Mon Sep 17 00:00:00 2001 From: KE7 Date: Tue, 14 Apr 2026 18:58:45 -0700 Subject: [PATCH] Add scenic.setSeed() public API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Expose the seeding logic that the -s/--seed CLI option already uses as a module-level function, so Scenic scenarios can be made deterministic from Python code without touching `random` / `numpy.random` directly. Motivation: docs/api.rst currently teaches users to call `random.seed(n)` directly to get deterministic generation, which is incomplete (it does not seed numpy.random, so any distribution backed by numpy's RNG still drifts) and exposes an implementation detail. Meanwhile, `scenic/__main__.py` already does the right thing for the CLI: it seeds both `random` and `numpy.random`. This PR turns that into a named API following the same pattern as the existing `scenic.setDebuggingOptions` (added in 9b9c8509). Changes: - `scenic/__init__.py`: add `setSeed(seed)`, documented, that seeds both `random` and `numpy.random`. - `scenic/__main__.py`: replace the inline `random.seed(args.seed); numpy.random.seed(args.seed)` block with `scenic.setSeed(args.seed)`, and drop the now-unused `import random` / `import numpy` at the top. Single source of truth for how `--seed` works. - `tests/syntax/test_basic.py`: add `test_setSeed` next to `test_verbose`, mirroring the existing `tests/test_main.py::test_seed` CLI-level structure (same seed → same sample; different seed → different sample). - `docs/api.rst`: document the new API with `.. autofunction::` and update the testcode example to use `scenic.setSeed(12345)` instead of `random.seed(12345)`. Expected output is unchanged — the example's `foo Range(0, 5)` is the same whether you seed via `random.seed` alone or via `scenic.setSeed` (verified locally). - `docs/options.rst`: add the standard "can be configured from the Python API using `scenic.setSeed`" cross-reference to the `-s`/`--seed` entry, matching the pattern already used for `scenic.setDebuggingOptions`. Local CI sign-off: - black 25.1.0 + isort 5.12.0 clean on all touched .py files - pytest --fast --no-graphics: 1444 passed, 538 skipped, 3 xfailed - pytest --no-graphics (slow, incl. tests/test_docs.py::test_build_docs): clean - tests/test_main.py::test_seed (verifies the __main__.py refactor): passes --- docs/api.rst | 10 ++++++++-- docs/options.rst | 2 ++ src/scenic/__init__.py | 23 +++++++++++++++++++++++ src/scenic/__main__.py | 6 +----- tests/syntax/test_basic.py | 11 +++++++++++ 5 files changed, 45 insertions(+), 7 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index 440f450f2..9e8fc299f 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -28,10 +28,16 @@ the sampled values for all the global parameters and objects in the scene from t import os os.chdir('..') +To make sampling deterministic, seed Scenic's random number generators +before calling `Scenario.generate` using `scenic.setSeed` (the programmatic +equivalent of the :option:`--seed` command-line option): + +.. autofunction:: scenic.setSeed + .. testcode:: - import random, scenic - random.seed(12345) + import scenic + scenic.setSeed(12345) scenario = scenic.scenarioFromString('ego = new Object with foo Range(0, 5)') scene, numIterations = scenario.generate() print(f'ego has foo = {scene.egoObject.foo}') diff --git a/docs/options.rst b/docs/options.rst index 1df985596..3efdc6c75 100644 --- a/docs/options.rst +++ b/docs/options.rst @@ -48,6 +48,8 @@ General Scenario Control (although :mod:`random` and :mod:`numpy.random` should not be used in place of Scenic's own sampling constructs in Scenic code). + This option can be configured from the Python API using `scenic.setSeed`. + .. option:: --scenario If the given Scenic file defines multiple scenarios, select which one to run. diff --git a/src/scenic/__init__.py b/src/scenic/__init__.py index ac15ea073..589730ef2 100644 --- a/src/scenic/__init__.py +++ b/src/scenic/__init__.py @@ -1,8 +1,31 @@ """A compiler and scene generator for the Scenic scenario description language.""" +import random as _random + +import numpy as _numpy + import scenic.core.errors as _errors from scenic.core.errors import setDebuggingOptions from scenic.syntax.translator import scenarioFromFile, scenarioFromString _errors.showInternalBacktrace = False # see comment in errors module del _errors + + +def setSeed(seed): + """Seed the random number generators Scenic uses for sampling. + + Seeds both the Python :mod:`random` module and :mod:`numpy.random`, which + are what Scenic's rejection sampler draws from. After calling this, + subsequent calls into Scenic (e.g. :func:`scenarioFromFile` / + :meth:`Scenario.generate`) will produce deterministic results for the + given seed. + + This is the programmatic equivalent of the ``-s``/``--seed`` + command-line option; see :option:`--seed`. + + Args: + seed (int): Seed value to pass to both RNGs. + """ + _random.seed(seed) + _numpy.random.seed(seed) diff --git a/src/scenic/__main__.py b/src/scenic/__main__.py index 05f527fba..2053b0ab4 100644 --- a/src/scenic/__main__.py +++ b/src/scenic/__main__.py @@ -3,13 +3,10 @@ import argparse from importlib import metadata -import random import sys import time import warnings -import numpy - import scenic from scenic.core.distributions import RejectionException import scenic.core.errors as errors @@ -185,8 +182,7 @@ if args.verbosity >= 1: print(f"Using random seed = {args.seed}") - random.seed(args.seed) - numpy.random.seed(args.seed) + scenic.setSeed(args.seed) # Load scenario from file if args.verbosity >= 1: diff --git a/tests/syntax/test_basic.py b/tests/syntax/test_basic.py index 8c3d01483..0b70ae6ce 100644 --- a/tests/syntax/test_basic.py +++ b/tests/syntax/test_basic.py @@ -249,6 +249,17 @@ def test_verbose(): setDebuggingOptions(verbosity=1) +def test_setSeed(): + scenic.setSeed(12345) + p1 = sampleParamPFrom("param p = Range(0, 1)") + scenic.setSeed(12345) + p2 = sampleParamPFrom("param p = Range(0, 1)") + assert p1 == p2 + scenic.setSeed(54321) + p3 = sampleParamPFrom("param p = Range(0, 1)") + assert p1 != p3 + + def test_dump_ast(): scenic.syntax.translator.dumpScenicAST = True try: