Skip to content

Commit 10d5fc0

Browse files
committed
TST: cover Monte Carlo seeding helpers directly
The reproducible-seeding change added __root_seed_sequence and __seed_simulation plus the per-index serial and parallel seeding, but the only tests that reached them ran a full Monte Carlo and were marked slow, so the coverage job (which does not pass --runslow) never executed them. Add fast unit tests that drive the two helpers directly: every supported random_seed type normalizes to the same root stream, None draws fresh entropy, existing SeedSequence/Generator/BitGenerator objects are reused rather than copied, and each child seed splits three ways so environment, rocket and flight get independent streams. Move the end-to-end simulate reproducibility tests into tests/integration, next to the existing Monte Carlo simulate test. The serial reproducibility run now lives in the non-slow suite; only the fork-based worker-invariance test stays slow, and it imports multiprocess lazily like the library does. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
1 parent 0d5aca5 commit 10d5fc0

2 files changed

Lines changed: 304 additions & 180 deletions

File tree

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
"""End-to-end determinism tests for ``MonteCarlo.simulate(random_seed=...)``.
2+
3+
With a fixed ``random_seed`` the generated random *inputs* are reproducible and
4+
identical across serial and parallel execution and across any number of workers.
5+
Each simulation index draws from its own child stream spawned from the run's root
6+
seed, and ``SeedSequence.spawn`` is prefix-stable, so index ``i`` maps to the same
7+
seed regardless of the worker that runs it. (The seed-handling helpers themselves
8+
are unit tested in ``tests/unit/simulation/test_monte_carlo_determinism``.)
9+
10+
The trajectory integration (``Flight``) is stubbed: worker invariance is a
11+
property of the *input sampling*, which happens before ``Flight`` is built, so a
12+
stub keeps the runs fast while still driving the real serial and parallel loops.
13+
Stubbing the module-level ``Flight`` symbol reaches the parallel workers only
14+
under the ``fork`` start method, so the worker-invariance test skips otherwise and
15+
is marked ``slow`` to match the other Monte Carlo multiprocessing tests.
16+
17+
A dedicated numpy-only rocket is used so *all* randomness flows through the seeded
18+
numpy generator. List-valued stochastic attributes are sampled with the standard
19+
library ``random.choice`` (an unseeded global generator) which ``random_seed``
20+
does not govern; the fixture drops the only such attribute (a multi-element
21+
``thrust_source``) so the inputs are byte-for-byte reproducible from the seed.
22+
"""
23+
24+
import json
25+
26+
import pytest
27+
28+
import rocketpy.simulation.monte_carlo as mc_module
29+
from rocketpy.simulation import MonteCarlo
30+
from rocketpy.stochastic import StochasticRocket, StochasticSolidMotor
31+
32+
33+
class _StubFlight:
34+
"""Minimal stand-in for ``Flight`` that skips trajectory integration."""
35+
36+
def __init__(self, **kwargs): # accepts and ignores MonteCarlo's Flight kwargs
37+
pass
38+
39+
def __getattr__(self, name):
40+
return 0.0
41+
42+
43+
@pytest.fixture
44+
def stochastic_calisto_numpy_only(
45+
cesaroni_m1670,
46+
calisto_robust,
47+
stochastic_nose_cone,
48+
stochastic_trapezoidal_fins,
49+
stochastic_tail,
50+
stochastic_rail_buttons,
51+
stochastic_main_parachute,
52+
stochastic_drogue_parachute,
53+
):
54+
"""A ``StochasticRocket`` whose randomness flows entirely through numpy.
55+
56+
Mirrors the shared ``stochastic_calisto`` fixture but gives the solid motor a
57+
single ``thrust_source`` instead of a multi-element list, so no attribute is
58+
sampled through the unseeded standard-library ``random.choice``.
59+
"""
60+
motor = StochasticSolidMotor(
61+
solid_motor=cesaroni_m1670,
62+
burn_out_time=(4, 0.1),
63+
grains_center_of_mass_position=0.001,
64+
grain_density=50,
65+
grain_separation=1 / 1000,
66+
grain_initial_height=1 / 1000,
67+
grain_initial_inner_radius=0.375 / 1000,
68+
grain_outer_radius=0.375 / 1000,
69+
total_impulse=(6500, 1000),
70+
throat_radius=0.5 / 1000,
71+
nozzle_radius=0.5 / 1000,
72+
nozzle_position=0.001,
73+
)
74+
rocket = StochasticRocket(
75+
rocket=calisto_robust,
76+
radius=0.0127 / 2000,
77+
mass=(15.426, 0.5, "normal"),
78+
inertia_11=(6.321, 0),
79+
inertia_22=0.01,
80+
inertia_33=0.01,
81+
center_of_mass_without_motor=0,
82+
)
83+
rocket.add_motor(motor, position=0.001)
84+
rocket.add_nose(stochastic_nose_cone, position=(1.134, 0.001))
85+
rocket.add_trapezoidal_fins(stochastic_trapezoidal_fins, position=(0.001, "normal"))
86+
rocket.add_tail(stochastic_tail)
87+
rocket.set_rail_buttons(
88+
stochastic_rail_buttons, lower_button_position=(-0.618, 0.001, "normal")
89+
)
90+
rocket.add_parachute(parachute=stochastic_main_parachute)
91+
rocket.add_parachute(parachute=stochastic_drogue_parachute)
92+
return rocket
93+
94+
95+
def _read_inputs_by_index(input_file):
96+
"""Read a ``.inputs.txt`` file into ``{index: raw_json_line}``."""
97+
by_index = {}
98+
with open(input_file, mode="r", encoding="utf-8") as rows:
99+
for line in rows:
100+
line = line.strip()
101+
if not line:
102+
continue
103+
by_index[json.loads(line)["index"]] = line
104+
return by_index
105+
106+
107+
def _simulate_inputs(
108+
monkeypatch, tmp_path, environment, rocket, flight, tag, **simulate_kwargs
109+
):
110+
"""Run a Monte Carlo with a stubbed ``Flight`` and return inputs by index."""
111+
monkeypatch.setattr(mc_module, "Flight", _StubFlight)
112+
montecarlo = MonteCarlo(
113+
filename=str(tmp_path / tag),
114+
environment=environment,
115+
rocket=rocket,
116+
flight=flight,
117+
)
118+
montecarlo.simulate(**simulate_kwargs)
119+
return _read_inputs_by_index(montecarlo.input_file)
120+
121+
122+
def test_serial_inputs_are_reproducible(
123+
monkeypatch,
124+
tmp_path,
125+
stochastic_environment,
126+
stochastic_calisto_numpy_only,
127+
stochastic_flight,
128+
):
129+
"""Two serial runs with the same seed yield byte-identical inputs per index.
130+
131+
This drives the serial ``simulate`` path end to end; the flexible seed types
132+
are covered by the unit test of ``__root_seed_sequence``.
133+
"""
134+
models = (stochastic_environment, stochastic_calisto_numpy_only, stochastic_flight)
135+
run_a = _simulate_inputs(
136+
monkeypatch, tmp_path, *models, "a", number_of_simulations=3, random_seed=7
137+
)
138+
run_b = _simulate_inputs(
139+
monkeypatch, tmp_path, *models, "b", number_of_simulations=3, random_seed=7
140+
)
141+
assert sorted(run_a) == list(range(3))
142+
assert run_a == run_b
143+
144+
145+
@pytest.mark.slow
146+
def test_inputs_are_worker_invariant(
147+
monkeypatch,
148+
tmp_path,
149+
stochastic_environment,
150+
stochastic_calisto_numpy_only,
151+
stochastic_flight,
152+
):
153+
"""serial == parallel(2) == parallel(4): inputs are bit-identical per index."""
154+
multiprocess = pytest.importorskip("multiprocess")
155+
if multiprocess.get_start_method() != "fork":
156+
pytest.skip(
157+
"stub-based parallel determinism test requires the 'fork' start method"
158+
)
159+
160+
models = (stochastic_environment, stochastic_calisto_numpy_only, stochastic_flight)
161+
common = {"number_of_simulations": 8, "random_seed": 314159}
162+
163+
serial = _simulate_inputs(monkeypatch, tmp_path, *models, "serial", **common)
164+
par2 = _simulate_inputs(
165+
monkeypatch, tmp_path, *models, "par2", parallel=True, n_workers=2, **common
166+
)
167+
par4 = _simulate_inputs(
168+
monkeypatch, tmp_path, *models, "par4", parallel=True, n_workers=4, **common
169+
)
170+
171+
expected = list(range(8))
172+
assert sorted(serial) == expected
173+
assert sorted(par2) == expected
174+
assert sorted(par4) == expected
175+
for index in expected:
176+
assert serial[index] == par2[index], f"serial vs parallel(2) differ at {index}"
177+
assert serial[index] == par4[index], f"serial vs parallel(4) differ at {index}"

0 commit comments

Comments
 (0)