Skip to content

Commit c529d0a

Browse files
committed
TST: verify Monte Carlo seed derivation is start-method invariant
The existing worker-invariance test stubs the module-level Flight and so reaches workers only under fork. Add a test that the per-index seed derived in a worker matches the main process under every available start method (fork, spawn, forkserver), using a top-level picklable target and small picklable arguments so it is valid under spawn/forkserver -- Python 3.14's POSIX default -- without relying on inherited parent state. It runs in ordinary CI. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
1 parent 0a739f8 commit c529d0a

1 file changed

Lines changed: 72 additions & 5 deletions

File tree

tests/integration/simulation/test_monte_carlo_determinism.py

Lines changed: 72 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,21 +14,52 @@
1414
under the ``fork`` start method, so the worker-invariance test skips otherwise and
1515
is marked ``slow`` to match the other Monte Carlo multiprocessing tests.
1616
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.
17+
A dedicated numpy-only rocket keeps the fork-based end-to-end test simple: it
18+
gives the motor a single ``thrust_source`` so the run has no list-valued attribute
19+
at all. List sampling is itself seeded now (it draws through the model generator,
20+
not the stdlib ``random.choice``) and is covered directly in
21+
``tests/unit/stochastic/test_stochastic_model``.
22+
23+
Seed derivation being independent of the multiprocessing start method (fork,
24+
spawn or forkserver) is verified separately by
25+
``test_seed_derivation_is_start_method_invariant``, which uses a top-level
26+
picklable target so it is safe under ``spawn``/``forkserver`` -- unlike the
27+
``Flight``-stub test above, which reaches workers only under ``fork``.
2228
"""
2329

2430
import json
31+
import multiprocessing
32+
from types import SimpleNamespace
2533

34+
import numpy as np
2635
import pytest
2736

2837
import rocketpy.simulation.monte_carlo as mc_module
2938
from rocketpy.simulation import MonteCarlo
39+
from rocketpy.simulation.monte_carlo import _seed_sequence_to_int
3040
from rocketpy.stochastic import StochasticRocket, StochasticSolidMotor
3141

42+
_child_seed = MonteCarlo._MonteCarlo__child_seed
43+
44+
45+
def _available_start_methods():
46+
"""The multiprocessing start methods this platform actually supports."""
47+
supported = multiprocessing.get_all_start_methods()
48+
return [method for method in ("fork", "spawn", "forkserver") if method in supported]
49+
50+
51+
def _derive_index_seeds(root_state, indices):
52+
"""Derive the per-index seed fingerprints from ``root_state``.
53+
54+
Top-level and picklable (only a small tuple and a list of ints cross the
55+
process boundary), so it runs unchanged under every start method -- including
56+
``spawn``/``forkserver``, which re-import this module rather than inheriting
57+
the parent's memory. It calls the real production helpers (``__child_seed``
58+
and ``_seed_sequence_to_int``) so the test tracks the shipped derivation.
59+
"""
60+
plan = SimpleNamespace(_MonteCarlo__root_state=root_state)
61+
return {index: _seed_sequence_to_int(_child_seed(plan, index)) for index in indices}
62+
3263

3364
class _StubFlight:
3465
"""Minimal stand-in for ``Flight`` that skips trajectory integration."""
@@ -175,3 +206,39 @@ def test_inputs_are_worker_invariant(
175206
for index in expected:
176207
assert serial[index] == par2[index], f"serial vs parallel(2) differ at {index}"
177208
assert serial[index] == par4[index], f"serial vs parallel(4) differ at {index}"
209+
210+
211+
@pytest.mark.parametrize("start_method", _available_start_methods())
212+
def test_seed_derivation_is_start_method_invariant(start_method):
213+
"""Per-index seeds derived in a worker match the main process under every
214+
available start method (fork, spawn, forkserver).
215+
216+
The full worker-invariance test above stubs the module-level ``Flight`` and so
217+
only reaches workers under ``fork``. This one instead checks the property that
218+
actually has to hold cross-platform -- that a simulation index maps to the same
219+
seed no matter which process derives it -- using a top-level picklable target
220+
and small picklable arguments, so it is valid under ``spawn``/``forkserver``
221+
(Python 3.14's POSIX default) without relying on any inherited parent state.
222+
Two workers split the indices; their combined result must equal the
223+
single-process derivation.
224+
"""
225+
root = np.random.SeedSequence(2718281828)
226+
root_state = (
227+
root.entropy,
228+
root.spawn_key,
229+
root.pool_size,
230+
root.n_children_spawned,
231+
)
232+
indices = list(range(6))
233+
expected = _derive_index_seeds(root_state, indices)
234+
235+
context = multiprocessing.get_context(start_method)
236+
chunks = [(root_state, indices[0::2]), (root_state, indices[1::2])]
237+
with context.Pool(2) as pool:
238+
results = pool.starmap(_derive_index_seeds, chunks)
239+
240+
combined = {}
241+
for result in results:
242+
combined.update(result)
243+
assert combined == expected
244+
assert sorted(combined) == indices

0 commit comments

Comments
 (0)