3030from rocketpy .prints .monte_carlo_prints import _MonteCarloPrints
3131from rocketpy .simulation .flight import Flight
3232from rocketpy .tools import (
33+ _seed_sequence_to_int ,
3334 generate_monte_carlo_ellipses ,
3435 generate_monte_carlo_ellipses_coordinates ,
3536 import_optional_dependency ,
@@ -240,18 +241,22 @@ def simulate(
240241 self ._export_config = kwargs
241242 self .number_of_simulations = number_of_simulations
242243 self ._initial_sim_idx = self .num_of_loaded_sims if append else 0
243- # Small, picklable root seed state captured once per run; every
244- # simulation index derives its child seed from it (see __child_seed).
245- self .__root_state = None
244+
245+ # Capture the small, picklable root seed state once per run (every
246+ # simulation index derives its child seed from it, see __child_seed).
247+ # This validates random_seed *before* __setup_files truncates any
248+ # existing output, so an invalid seed cannot destroy prior results on
249+ # the way to raising.
250+ self .__capture_root_state (random_seed )
246251
247252 print ("Starting Monte Carlo analysis" )
248253
249254 self .__setup_files (append )
250255
251256 if parallel :
252- self .__run_in_parallel (n_workers , random_seed )
257+ self .__run_in_parallel (n_workers )
253258 else :
254- self .__run_in_serial (random_seed )
259+ self .__run_in_serial ()
255260
256261 self .__terminate_simulation ()
257262
@@ -359,14 +364,12 @@ def __seed_simulation(self, child_seed):
359364 self .rocket ._set_stochastic (_seed_sequence_to_int (rocket_seed ))
360365 self .flight ._set_stochastic (_seed_sequence_to_int (flight_seed ))
361366
362- def __run_in_serial (self , random_seed = None ): # pylint: disable=too-many-statements
367+ def __run_in_serial (self ): # pylint: disable=too-many-statements
363368 """
364369 Runs the monte carlo simulation in serial mode.
365370
366- Parameters
367- ----------
368- random_seed : int or SeedSequence, optional
369- Root seed for the run. See ``simulate``.
371+ The root seed state is captured by ``simulate`` before this runs, so each
372+ simulation index derives its child seed from ``self.__root_state``.
370373
371374 Returns
372375 -------
@@ -377,7 +380,6 @@ def __run_in_serial(self, random_seed=None): # pylint: disable=too-many-stateme
377380 n_simulations = self .number_of_simulations ,
378381 start_time = time (),
379382 )
380- self .__capture_root_state (random_seed )
381383 try :
382384 while sim_monitor .keep_simulating ():
383385 sim_idx = sim_monitor .increment () - 1
@@ -408,17 +410,19 @@ def __run_in_serial(self, random_seed=None): # pylint: disable=too-many-stateme
408410 f .write (inputs_json )
409411 raise error
410412
411- def __run_in_parallel (self , n_workers = None , random_seed = None ):
413+ def __run_in_parallel (self , n_workers = None ):
412414 """
413415 Runs the monte carlo simulation in parallel.
414416
417+ The root seed state is captured by ``simulate`` before this runs and
418+ travels with the pickled instance, so every worker derives the same
419+ per-index child seed from ``self.__root_state``.
420+
415421 Parameters
416422 ----------
417423 n_workers: int, optional
418424 Number of workers to be used. If None, the number of workers
419425 will be equal to the number of CPUs available. Default is None.
420- random_seed : int or SeedSequence, optional
421- Root seed for the run. See ``simulate``.
422426
423427 Returns
424428 -------
@@ -446,8 +450,6 @@ def __run_in_parallel(self, n_workers=None, random_seed=None):
446450 # the sampled inputs do not depend on the number of workers. The
447451 # root state is small and travels with the pickled instance, so no
448452 # per-index seed list is materialized or sent to each process.
449- self .__capture_root_state (random_seed )
450-
451453 for _ in range (n_workers ):
452454 sim_producer = multiprocess .Process (
453455 target = self .__sim_producer ,
@@ -1710,26 +1712,6 @@ def export_errors_to_json(self, filename):
17101712 self ._write_log_to_json (self .errors_log , filename )
17111713
17121714
1713- def _seed_sequence_to_int (seed_sequence ):
1714- """Collapse a ``SeedSequence`` into a 128-bit Python ``int``.
1715-
1716- A plain ``int`` is the one seed type accepted alike by
1717- ``numpy.random.default_rng``, ``numpy.random.RandomState`` and the stdlib
1718- ``random.Random`` (which rejects a ``SeedSequence`` with a ``TypeError``
1719- since Python 3.11), so a custom sampler whose ``reset_seed`` documents an
1720- ``int`` keeps working. All four ``uint32`` words are combined to keep the
1721- full 128-bit pool, so the environment/rocket/flight sub-streams stay
1722- decorrelated instead of collapsing to a single 32-bit word.
1723-
1724- The words are combined by value (little-endian word order), not via
1725- ``tobytes()``, so the seed is the same on big- and little-endian machines
1726- -- a byte-order-dependent seed would break the cross-platform
1727- reproducibility this whole scheme exists to provide.
1728- """
1729- words = seed_sequence .generate_state (4 , dtype = np .uint32 )
1730- return sum (int (word ) << (32 * position ) for position , word in enumerate (words ))
1731-
1732-
17331715def _claim_next_index (sim_monitor , mutex ):
17341716 """Atomically claim the next 0-based simulation index, or ``None`` if done.
17351717
0 commit comments