From 0f96e94cfe876841b0a72f18caf15f747d818d3c Mon Sep 17 00:00:00 2001 From: wuisabel-gif <231155141+wuisabel-gif@users.noreply.github.com> Date: Sat, 18 Jul 2026 15:32:11 -0700 Subject: [PATCH 1/2] BUG: seed Monte Carlo per simulation index, not per worker (#1053) --- rocketpy/simulation/monte_carlo.py | 52 ++++++++++++---- tests/unit/simulation/test_monte_carlo.py | 72 +++++++++++++++++++++++ 2 files changed, 112 insertions(+), 12 deletions(-) diff --git a/rocketpy/simulation/monte_carlo.py b/rocketpy/simulation/monte_carlo.py index 21c665d01..d43215a92 100644 --- a/rocketpy/simulation/monte_carlo.py +++ b/rocketpy/simulation/monte_carlo.py @@ -96,6 +96,7 @@ def __init__( flight, export_list=None, data_collector=None, + seed=None, ): """ Initialize a MonteCarlo object. @@ -132,6 +133,14 @@ def __init__( "date": lambda flight: flight.env.date, } + seed : int, optional + Master seed for the whole analysis. Per-simulation seeds are + derived from it by simulation index, so a given index draws the + same inputs whether the run is serial or parallel and regardless + of the number of workers. If None (default), a random master seed + is drawn once; results are then random but still consistent across + execution modes within that run. + Returns ------- None @@ -160,6 +169,9 @@ def __init__( self._check_data_collector(data_collector) self.data_collector = data_collector + self._seed = seed + self._base_entropy = np.random.SeedSequence(seed).entropy + self.import_inputs(self.filename.with_suffix(".inputs.txt")) self.import_outputs(self.filename.with_suffix(".outputs.txt")) self.import_errors(self.filename.with_suffix(".errors.txt")) @@ -285,6 +297,7 @@ def __run_in_serial(self): sim_monitor.increment() inputs_json, outputs_json = "", "" + self.__seed_stochastic_models(sim_monitor.count - 1) flight = self.__run_single_simulation() inputs_json = self.__evaluate_flight_inputs(sim_monitor.count) outputs_json = self.__evaluate_flight_outputs(flight, sim_monitor.count) @@ -339,13 +352,11 @@ def __run_in_parallel(self, n_workers=None): ) processes = [] - seeds = np.random.SeedSequence().spawn(n_workers) - for seed in seeds: + for _ in range(n_workers): sim_producer = multiprocess.Process( target=self.__sim_producer, args=( - seed, sim_monitor, mutex, simulation_error_event, @@ -387,13 +398,11 @@ def __validate_number_of_workers(self, n_workers): raise ValueError("Number of workers must be at least 2 for parallel mode.") return n_workers - def __sim_producer(self, seed, sim_monitor, mutex, error_event): # pylint: disable=too-many-statements + def __sim_producer(self, sim_monitor, mutex, error_event): # pylint: disable=too-many-statements """Simulation producer to be used in parallel by multiprocessing. Parameters ---------- - seed : int - The seed to set the random number generator. sim_monitor : _SimMonitor The simulation monitor object to keep track of the simulations. mutex : multiprocess.Lock @@ -402,15 +411,11 @@ def __sim_producer(self, seed, sim_monitor, mutex, error_event): # pylint: disa Event signaling an error occurred during the simulation. """ try: - # Ensure Processes generate different random numbers - self.environment._set_stochastic(seed) - self.rocket._set_stochastic(seed) - self.flight._set_stochastic(seed) - while sim_monitor.keep_simulating(): sim_idx = sim_monitor.increment() - 1 inputs_json, outputs_json = "", "" + self.__seed_stochastic_models(sim_idx) flight = self.__run_single_simulation() inputs_json = self.__evaluate_flight_inputs(sim_idx) outputs_json = self.__evaluate_flight_outputs(flight, sim_idx) @@ -453,6 +458,29 @@ def __sim_producer(self, seed, sim_monitor, mutex, error_event): # pylint: disa error_event.set() mutex.release() + def __seed_stochastic_models(self, sim_idx): + """Reseed the stochastic models deterministically for one simulation. + + The seed for simulation ``sim_idx`` depends only on the master seed and + the index, so the same index samples the same inputs in serial and + parallel runs and for any number of workers (issue #1053). Each model + gets its own derived seed so their draws stay decorrelated. + + Parameters + ---------- + sim_idx : int + Zero-based index of the simulation being run. + """ + seed_sequence = np.random.SeedSequence( + entropy=self._base_entropy, spawn_key=(sim_idx,) + ) + env_seed, rocket_seed, flight_seed = ( + int(s) for s in seed_sequence.generate_state(3) + ) + self.environment._set_stochastic(env_seed) + self.rocket._set_stochastic(rocket_seed) + self.flight._set_stochastic(flight_seed) + def __run_single_simulation(self): """Runs a single simulation and returns the inputs and outputs. @@ -1377,7 +1405,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/tests/unit/simulation/test_monte_carlo.py b/tests/unit/simulation/test_monte_carlo.py index 7e2e68804..0606dba92 100644 --- a/tests/unit/simulation/test_monte_carlo.py +++ b/tests/unit/simulation/test_monte_carlo.py @@ -513,3 +513,75 @@ def test_simulate_convergence_runs_until_max_when_not_converging(): assert mc.num_of_loaded_sims == 200 assert all(width > 0.5 for width in history) assert len(history) == 4 # 200 / 50 batches + + +def test_seed_makes_inputs_reproducible_by_index( + stochastic_environment, stochastic_calisto, stochastic_flight +): + """Inputs for a given simulation index depend only on the master seed and + the index, so serial and parallel runs (any worker count) sample the same + inputs and a fixed seed reproduces a run. See issue #1053. + + Parameters + ---------- + stochastic_environment : StochasticEnvironment + The stochastic environment object, this is a pytest fixture. + stochastic_calisto : StochasticRocket + The stochastic rocket object, this is a pytest fixture. + stochastic_flight : StochasticFlight + The stochastic flight object, this is a pytest fixture. + """ + + def elevation_for(mc, sim_idx): + mc._MonteCarlo__seed_stochastic_models(sim_idx) # pylint: disable=protected-access + return mc.environment.create_object().elevation + + def make(seed): + return MonteCarlo( + filename="monte_carlo_test", + environment=stochastic_environment, + rocket=stochastic_calisto, + flight=stochastic_flight, + seed=seed, + ) + + mc = make(42) + mc_same = make(42) + mc_other = make(7) + + assert elevation_for(mc, 0) == elevation_for(mc_same, 0) + assert elevation_for(mc, 9) == elevation_for(mc_same, 9) + + value = elevation_for(mc, 3) + assert elevation_for(mc, 3) == value + + assert elevation_for(mc, 0) != elevation_for(mc, 1) + assert elevation_for(mc_other, 0) != elevation_for(mc, 0) + + +def test_unseeded_monte_carlo_is_index_consistent( + stochastic_environment, stochastic_calisto, stochastic_flight +): + """seed=None still yields inputs that depend only on the index within a + run, so serial and parallel stay consistent even without a fixed seed. + + Parameters + ---------- + stochastic_environment : StochasticEnvironment + The stochastic environment object, this is a pytest fixture. + stochastic_calisto : StochasticRocket + The stochastic rocket object, this is a pytest fixture. + stochastic_flight : StochasticFlight + The stochastic flight object, this is a pytest fixture. + """ + mc = MonteCarlo( + filename="monte_carlo_test", + environment=stochastic_environment, + rocket=stochastic_calisto, + flight=stochastic_flight, + ) + + mc._MonteCarlo__seed_stochastic_models(2) # pylint: disable=protected-access + first = mc.environment.create_object().elevation + mc._MonteCarlo__seed_stochastic_models(2) # pylint: disable=protected-access + assert mc.environment.create_object().elevation == first From a0b9536af312e91c7d390c82052cf0c5e0aeea76 Mon Sep 17 00:00:00 2001 From: wuisabel-gif <231155141+wuisabel-gif@users.noreply.github.com> Date: Sat, 18 Jul 2026 20:34:12 -0700 Subject: [PATCH 2/2] DOC: add changelog entry for #1071 --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ae48db43..313629b91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,8 @@ Attention: The newest changes should be on top --> ### Fixed +- BUG: seed Monte Carlo per simulation index, not per worker [#1071](https://github.com/RocketPy-Team/RocketPy/pull/1071) + ## [v1.13.0] - 2026-07-04 ### Added