diff --git a/rocketpy/simulation/monte_carlo.py b/rocketpy/simulation/monte_carlo.py index 21c665d01..b2749a112 100644 --- a/rocketpy/simulation/monte_carlo.py +++ b/rocketpy/simulation/monte_carlo.py @@ -461,12 +461,15 @@ def __run_single_simulation(self): Flight The flight object of the simulation. """ + rocket = self.rocket.create_object() + environment = self.environment.create_object() + flight_inputs = self.flight._sample_flight_inputs() return Flight( - rocket=self.rocket.create_object(), - environment=self.environment.create_object(), - rail_length=self.flight._randomize_rail_length(), - inclination=self.flight._randomize_inclination(), - heading=self.flight._randomize_heading(), + rocket=rocket, + environment=environment, + rail_length=flight_inputs["rail_length"], + inclination=flight_inputs["inclination"], + heading=flight_inputs["heading"], initial_solution=self.flight.initial_solution, terminate_on_apogee=self.flight.terminate_on_apogee, time_overshoot=self.flight.time_overshoot, @@ -1377,7 +1380,7 @@ def export_ellipses_to_kml( # pylint: disable=too-many-statements except KeyError as e: raise KeyError("No impact data found. Skipping impact ellipses.") from e - (apogee_ellipses, impact_ellipses) = generate_monte_carlo_ellipses( + apogee_ellipses, impact_ellipses = generate_monte_carlo_ellipses( impact_x, impact_y, apogee_x, diff --git a/rocketpy/stochastic/stochastic_flight.py b/rocketpy/stochastic/stochastic_flight.py index 525526798..9bc18f0c0 100644 --- a/rocketpy/stochastic/stochastic_flight.py +++ b/rocketpy/stochastic/stochastic_flight.py @@ -122,21 +122,28 @@ def _validate_initial_solution(self, initial_solution): else: raise TypeError("`initial_solution` must be a tuple of numbers") - # TODO: these methods call dict_generator a lot of times unnecessarily + def _sample_flight_inputs(self): + """Sample rail_length, inclination, and heading in a single draw. + + Returns + ------- + dict + Mapping with keys ``rail_length``, ``inclination``, and ``heading``. + Also updates ``last_rnd_dict``. + """ + return next(self.dict_generator()) + def _randomize_rail_length(self): """Randomizes the rail length of the flight.""" - generated_dict = next(self.dict_generator()) - return generated_dict["rail_length"] + return self._sample_flight_inputs()["rail_length"] def _randomize_inclination(self): """Randomizes the inclination of the flight.""" - generated_dict = next(self.dict_generator()) - return generated_dict["inclination"] + return self._sample_flight_inputs()["inclination"] def _randomize_heading(self): """Randomizes the heading of the flight.""" - generated_dict = next(self.dict_generator()) - return generated_dict["heading"] + return self._sample_flight_inputs()["heading"] def create_object(self): """Creates and returns a Flight object from the randomly generated input @@ -147,12 +154,11 @@ def create_object(self): flight : Flight Flight object with the randomly generated input arguments. """ - generated_dict = next(self.dict_generator()) - # TODO: maybe we should use generated_dict["rail_length"] instead + generated_dict = self._sample_flight_inputs() return Flight( rocket=self.obj.rocket, environment=self.obj.env, - rail_length=self._randomize_rail_length(), + rail_length=generated_dict["rail_length"], inclination=generated_dict["inclination"], heading=generated_dict["heading"], initial_solution=self.initial_solution, diff --git a/tests/unit/stochastic/test_stochastic_flight.py b/tests/unit/stochastic/test_stochastic_flight.py index e03917475..121d152e9 100644 --- a/tests/unit/stochastic/test_stochastic_flight.py +++ b/tests/unit/stochastic/test_stochastic_flight.py @@ -45,3 +45,39 @@ 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_create_object_matches_last_rnd_dict(flight_calisto_robust): + """Regression for #1090: create_object must use one dict_generator draw. + + Spreads are set on all three flight inputs so a second draw would diverge + from ``last_rnd_dict``. + """ + stochastic_flight = StochasticFlight( + flight=flight_calisto_robust, + rail_length=(5.2, 0.5), + inclination=(84.7, 1), + heading=(53, 2), + ) + stochastic_flight._set_stochastic(4242) + + flight = stochastic_flight.create_object() + sampled = stochastic_flight.last_rnd_dict + + assert flight.rail_length == sampled["rail_length"] + assert flight.inclination == sampled["inclination"] + assert flight.heading == sampled["heading"] + + +def test_monte_carlo_single_simulation_matches_flight_last_rnd_dict( + monte_carlo_calisto, +): + """Regression for #1090: MonteCarlo must fly the same sample it logs.""" + monte_carlo_calisto.flight._set_stochastic(4242) + + flight = monte_carlo_calisto._MonteCarlo__run_single_simulation() + sampled = monte_carlo_calisto.flight.last_rnd_dict + + assert flight.rail_length == sampled["rail_length"] + assert flight.inclination == sampled["inclination"] + assert flight.heading == sampled["heading"]