Skip to content

Commit 0a739f8

Browse files
committed
BUG: sample list-valued stochastic attributes through the seeded generator
dict_generator drew list-valued attributes with the stdlib random.choice, which reads from an unseeded global Random instance, so random_seed did not make those attributes reproducible. Draw the index from this model's seeded numpy generator instead. Indexing (not numpy.random.choice) also avoids coercing a heterogeneous list -- Function objects, paths, arrays -- to a single dtype. Adds a unit test that a list-valued attribute is reproducible under a fixed seed and that heterogeneous entries are returned unchanged. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
1 parent f3cc6f4 commit 0a739f8

2 files changed

Lines changed: 43 additions & 3 deletions

File tree

rocketpy/stochastic/stochastic_model.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@
33
Stochastic classes.
44
"""
55

6-
from random import choice
7-
86
import numpy as np
97

108
from rocketpy.mathutils.function import Function
@@ -532,7 +530,16 @@ def dict_generator(self):
532530
dist_sampler = value[-1]
533531
generated_dict[arg] = dist_sampler(value[0], value[1])
534532
elif isinstance(value, list):
535-
generated_dict[arg] = choice(value) if value else value
533+
# Draw the index from this model's seeded generator so a
534+
# list-valued attribute is reproducible under random_seed. The
535+
# stdlib random.choice draws from an unseeded global instance,
536+
# and numpy's choice coerces a heterogeneous list (Function,
537+
# paths, arrays) to a single dtype; indexing avoids both.
538+
if value:
539+
index = int(self.__random_number_generator.integers(len(value)))
540+
generated_dict[arg] = value[index]
541+
else:
542+
generated_dict[arg] = value
536543
elif isinstance(value, CustomSampler):
537544
try:
538545
generated_dict[arg] = value.sample(n_samples=1)[0]

tests/unit/stochastic/test_stochastic_model.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,38 @@
1+
from types import SimpleNamespace
2+
13
import pytest
24

5+
from rocketpy.stochastic.stochastic_model import StochasticModel
6+
7+
8+
def _sampled_option(model):
9+
"""Return the value ``dict_generator`` picks for the ``options`` attribute."""
10+
return next(model.dict_generator())["options"]
11+
12+
13+
def test_list_attribute_sampling_is_reproducible_under_seed():
14+
"""A list-valued stochastic attribute is drawn through the model's own seeded
15+
numpy generator, so a fixed seed reproduces the choice. It used to be drawn
16+
with the stdlib ``random.choice`` (an unseeded global instance), which
17+
``random_seed`` could not govern. Heterogeneous entries (paths, callables,
18+
lists) are returned unchanged rather than coerced to a numpy dtype the way
19+
``numpy.random.choice`` would.
20+
"""
21+
options = ["/motor/a.eng", "/motor/b.eng", (lambda t: t), [1, 2, 3]]
22+
model = StochasticModel(obj=SimpleNamespace(), options=options)
23+
24+
model._set_stochastic(42)
25+
first = _sampled_option(model)
26+
model._set_stochastic(42)
27+
assert _sampled_option(model) == first, "same seed must reproduce the choice"
28+
assert any(first is option for option in options), "object returned unchanged"
29+
30+
chosen_ids = set()
31+
for seed in range(16):
32+
model._set_stochastic(seed)
33+
chosen_ids.add(id(_sampled_option(model)))
34+
assert len(chosen_ids) > 1, "different seeds must be able to pick differently"
35+
336

437
@pytest.mark.parametrize(
538
"fixture_name",

0 commit comments

Comments
 (0)