From 9cc93a1c82bf1d1c12fb0e8d933ef12cecc37540 Mon Sep 17 00:00:00 2001 From: Joshua Terranova Date: Mon, 10 Aug 2026 18:22:12 -0700 Subject: [PATCH] BUG: stop dict_generator from sampling initial_solution (#1109) Generate stochastic samples from declared constructor inputs only, and validate StochasticFlight.initial_solution on construct. --- rocketpy/stochastic/stochastic_flight.py | 1 + rocketpy/stochastic/stochastic_model.py | 10 +++++-- .../unit/stochastic/test_stochastic_flight.py | 26 +++++++++++++++++++ 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/rocketpy/stochastic/stochastic_flight.py b/rocketpy/stochastic/stochastic_flight.py index 525526798..85187e286 100644 --- a/rocketpy/stochastic/stochastic_flight.py +++ b/rocketpy/stochastic/stochastic_flight.py @@ -94,6 +94,7 @@ def __init__( heading=heading, ) + self._validate_initial_solution(initial_solution) self.initial_solution = initial_solution self.terminate_on_apogee = terminate_on_apogee if max_time is None: diff --git a/rocketpy/stochastic/stochastic_model.py b/rocketpy/stochastic/stochastic_model.py index be2438a0c..d73bf07f8 100644 --- a/rocketpy/stochastic/stochastic_model.py +++ b/rocketpy/stochastic/stochastic_model.py @@ -621,13 +621,19 @@ def dict_generator(self): Notes ----- - 1. The dictionary is generated by iterating over the class attributes and: + 1. The dictionary is generated by iterating over the *declared* + stochastic inputs (constructor kwargs), not every attribute on + ``self``. This avoids treating opaque tuples such as + ``initial_solution`` as ``(nominal, spread, sampler)`` triples. a. If the attribute is a tuple, the value is generated using the\ distribution function specified in the tuple. b. If the attribute is a list, the value is randomly chosen from the list. """ generated_dict = {} - for arg, value in self.__dict__.items(): + for arg in self.__stochastic_dict: + if not hasattr(self, arg): + continue + value = getattr(self, arg) if isinstance(value, tuple): dist_sampler = value[-1] generated_dict[arg] = dist_sampler(value[0], value[1]) diff --git a/tests/unit/stochastic/test_stochastic_flight.py b/tests/unit/stochastic/test_stochastic_flight.py index e03917475..233800701 100644 --- a/tests/unit/stochastic/test_stochastic_flight.py +++ b/tests/unit/stochastic/test_stochastic_flight.py @@ -45,3 +45,29 @@ def test_stochastic_flight_optional_attributes(flight_calisto_robust): assert obj.terminate_on_apogee is True assert obj.time_overshoot is True assert obj.max_time == 987.6 + + +def test_dict_generator_skips_initial_solution_tuple(flight_calisto_robust): + """Regression for #1109: tuple initial_solution must not be sampled.""" + initial_solution = tuple(float(i) for i in range(14)) + stochastic_flight = StochasticFlight( + flight=flight_calisto_robust, + initial_solution=initial_solution, + rail_length=(5.2, 0.1), + ) + generated = next(stochastic_flight.dict_generator()) + assert "initial_solution" not in generated + assert stochastic_flight.initial_solution == initial_solution + + +def test_dict_generator_skips_initial_solution_list(flight_calisto_robust): + """List-form initial_solution must not be randomly subset-sampled.""" + initial_solution = [float(i) for i in range(14)] + stochastic_flight = StochasticFlight( + flight=flight_calisto_robust, + initial_solution=initial_solution, + inclination=[85, 86, 87], + ) + generated = next(stochastic_flight.dict_generator()) + assert "initial_solution" not in generated + assert stochastic_flight.initial_solution == initial_solution