|
20 | 20 | not the stdlib ``random.choice``) and is covered directly in |
21 | 21 | ``tests/unit/stochastic/test_stochastic_model``. |
22 | 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``. |
| 23 | +Seed derivation being independent of the multiprocessing start method is a |
| 24 | +property of the pure derivation helpers, unit tested in |
| 25 | +``tests/unit/simulation/test_monte_carlo_determinism`` (``__child_seed`` equals |
| 26 | +``root.spawn(n)[i]`` bit for bit). A full parallel run under ``spawn`` and |
| 27 | +``forkserver``, exercising the real ``multiprocess`` path rather than the |
| 28 | +helpers, is tracked in #1076; it needs to run its workers in an isolated |
| 29 | +subprocess, since starting a multiprocessing pool in this shared test process |
| 30 | +can destabilize the later off-screen rendering tests. |
28 | 31 | """ |
29 | 32 |
|
30 | 33 | import json |
31 | | -import multiprocessing |
32 | | -from types import SimpleNamespace |
33 | 34 |
|
34 | 35 | import numpy as np |
35 | 36 | import pytest |
36 | 37 |
|
37 | 38 | import rocketpy.simulation.monte_carlo as mc_module |
38 | 39 | from rocketpy.simulation import MonteCarlo |
39 | | -from rocketpy.simulation.monte_carlo import _seed_sequence_to_int |
40 | 40 | from rocketpy.stochastic import StochasticRocket, StochasticSolidMotor |
41 | 41 |
|
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 | | - |
63 | 42 |
|
64 | 43 | class _StubFlight: |
65 | 44 | """Minimal stand-in for ``Flight`` that skips trajectory integration.""" |
@@ -236,39 +215,3 @@ def test_inputs_are_worker_invariant( |
236 | 215 | for index in expected: |
237 | 216 | assert serial[index] == par2[index], f"serial vs parallel(2) differ at {index}" |
238 | 217 | assert serial[index] == par4[index], f"serial vs parallel(4) differ at {index}" |
239 | | - |
240 | | - |
241 | | -@pytest.mark.parametrize("start_method", _available_start_methods()) |
242 | | -def test_seed_derivation_is_start_method_invariant(start_method): |
243 | | - """Per-index seeds derived in a worker match the main process under every |
244 | | - available start method (fork, spawn, forkserver). |
245 | | -
|
246 | | - The full worker-invariance test above stubs the module-level ``Flight`` and so |
247 | | - only reaches workers under ``fork``. This one instead checks the property that |
248 | | - actually has to hold cross-platform -- that a simulation index maps to the same |
249 | | - seed no matter which process derives it -- using a top-level picklable target |
250 | | - and small picklable arguments, so it is valid under ``spawn``/``forkserver`` |
251 | | - (Python 3.14's POSIX default) without relying on any inherited parent state. |
252 | | - Two workers split the indices; their combined result must equal the |
253 | | - single-process derivation. |
254 | | - """ |
255 | | - root = np.random.SeedSequence(2718281828) |
256 | | - root_state = ( |
257 | | - root.entropy, |
258 | | - root.spawn_key, |
259 | | - root.pool_size, |
260 | | - root.n_children_spawned, |
261 | | - ) |
262 | | - indices = list(range(6)) |
263 | | - expected = _derive_index_seeds(root_state, indices) |
264 | | - |
265 | | - context = multiprocessing.get_context(start_method) |
266 | | - chunks = [(root_state, indices[0::2]), (root_state, indices[1::2])] |
267 | | - with context.Pool(2) as pool: |
268 | | - results = pool.starmap(_derive_index_seeds, chunks) |
269 | | - |
270 | | - combined = {} |
271 | | - for result in results: |
272 | | - combined.update(result) |
273 | | - assert combined == expected |
274 | | - assert sorted(combined) == indices |
0 commit comments