Skip to content

Commit f9a10fa

Browse files
committed
TST: compare what a simulation drew, not what wrote the row down
The Windows legs of this branch hung and then reported a mismatch on power_off_drag and power_on_drag, and I read that as the guarantee failing under spawn. It was the test. Two fields a serialized Function carries belong to the writer rather than to the draw. hash is the object's identity in that process. A callable source is its pickle, and the same callable pickles to different bytes in a spawned child: measured on one drag curve, the parent and a forked child agree and a spawned child does not. Both of those drag curves are declared None on the fixture, so nothing varies them and neither field ever carried a draw. Measured with the models seeded by hand, no per-index seeding in the way, one seed: the parent, a forked child and a spawned child all build the same rocket, mass to the last digit. So the guarantee does hold on the start method Windows uses, and there is now a test that says so rather than an assumption. Removing the per-index seeding still turns three of these red, so dropping those two fields has not made the comparison vacuous. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
1 parent 7b85499 commit f9a10fa

1 file changed

Lines changed: 41 additions & 11 deletions

File tree

‎tests/unit/simulation/test_monte_carlo_seeding.py‎

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import json
22
import os
33

4+
import multiprocess
45
import numpy as np
56
import pytest
67

@@ -12,30 +13,32 @@
1213
)
1314

1415

15-
def _without_object_identity(value):
16-
"""The same record with every ``hash`` dropped, at any depth.
16+
def _what_was_drawn(value):
17+
"""The same record without the parts that say which process wrote it.
1718
18-
``RocketPyEncoder`` records ``hash(obj)`` beside a serialized ``Function``,
19-
and that is the object's identity in the process that wrote it, not
20-
anything that was drawn. It agrees between two runs that share memory and
21-
differs between two that do not, which is a property of the start method
22-
rather than of the seed.
19+
Two of the fields a serialized ``Function`` carries are properties of the
20+
writer rather than of the draw. ``hash`` is the object's identity in that
21+
process. A callable ``source`` is its pickle, and the same callable pickles
22+
to different bytes under ``spawn``: measured on one drag curve, the parent
23+
and a forked child agree and a spawned child does not, for a value the
24+
fixture does not vary at all. Everything a run actually draws is numeric
25+
and stays.
2326
"""
2427
if isinstance(value, dict):
2528
return {
26-
key: _without_object_identity(item)
29+
key: _what_was_drawn(item)
2730
for key, item in value.items()
28-
if key != "hash"
31+
if key != "hash" and not (key == "source" and isinstance(item, str))
2932
}
3033
if isinstance(value, list):
31-
return [_without_object_identity(item) for item in value]
34+
return [_what_was_drawn(item) for item in value]
3235
return value
3336

3437

3538
def _sampled_inputs(analysis):
3639
with open(analysis.input_file, "r", encoding="utf-8") as written:
3740
rows = [json.loads(line) for line in written if line.strip()]
38-
return {row["index"]: _without_object_identity(row) for row in rows}
41+
return {row["index"]: _what_was_drawn(row) for row in rows}
3942

4043

4144
def _a_run(tmp_path, stem, models, *, parallel=False, workers=None, seed=None, count=4):
@@ -175,3 +178,30 @@ def test_an_appended_run_carries_the_same_stream_on(models, tmp_path):
175178
part.simulate(4, append=True, random_seed=42)
176179

177180
assert _sampled_inputs(part) == _sampled_inputs(whole)
181+
182+
183+
@pytest.fixture(name="spawned_workers")
184+
def _spawned_workers():
185+
"""Start workers the way Windows does, wherever the test happens to run."""
186+
was = multiprocess.get_start_method()
187+
multiprocess.set_start_method("spawn", force=True)
188+
yield
189+
multiprocess.set_start_method(was, force=True)
190+
191+
192+
@pytest.mark.usefixtures("spawned_workers")
193+
@pytest.mark.skipif(os.cpu_count() < 4, reason="needs four workers to be four")
194+
def test_an_index_keeps_its_inputs_when_the_workers_are_spawned(models, tmp_path):
195+
"""The same guarantee on the start method Windows uses.
196+
197+
A spawned child rebuilds the models by unpickling rather than inheriting
198+
them, so this is a different question from the one above and was worth
199+
asking separately: the first version of these tests compared serialized
200+
callables, which differ between processes for a value nothing varies.
201+
"""
202+
serial = _a_run(tmp_path, "serial", models, seed=42)
203+
two = _a_run(tmp_path, "two", models, parallel=True, workers=2, seed=42)
204+
four = _a_run(tmp_path, "four", models, parallel=True, workers=4, seed=42)
205+
206+
assert _sampled_inputs(serial) == _sampled_inputs(two)
207+
assert _sampled_inputs(serial) == _sampled_inputs(four)

0 commit comments

Comments
 (0)