Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions docs/user/stochastic.rst
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,45 @@ reliability of your simulations over time.
.. which parameters most significantly impact your simulation results.


Seeding a rocket's components
-----------------------------

A ``StochasticRocket`` holds nested stochastic objects: the motors, the
aerodynamic surfaces, the rail buttons, the parachutes and the air brakes. Each
time the rocket is reset, every component attached to it at that moment is given
a stream of its own, spawned from the seed the reset was given. A main and a
drogue parachute built from the same ``cd_s`` and ``lag`` are then no longer
made to consume the same draws as each other. Independent streams can still land
on equal values, and a specification with no spread always will.

The reset is what builds the tree, not ``add_parachute`` or ``add_nose``. A
rocket resets itself once while being constructed, when it has no components
yet, so anything attached afterwards keeps the generator it was built with until
something resets it again. A serial ``MonteCarlo`` run does not reset it at
all. A parallel one tries to, once per worker, but hands the model a
``SeedSequence`` where an integer is wanted, so that path does not get as far
as building the tree either. Resetting per simulation, from an integer seed the
caller chooses, is what the Monte Carlo seeding work adds.

Each collection is spawned separately, so adding an aerodynamic surface does not
move what the parachutes draw. Within a collection the stream follows insertion
order, so adding a component ahead of another does change what the later one
draws under a fixed seed. The rocket's own inputs, such as ``mass`` and
``radius``, use the seed exactly as given.

.. note::
A component's *position* is a property of the rocket rather than of the
component, so it is drawn from the rocket's own stream.

.. note::
A stream belongs to one stochastic wrapper. Storing the same wrapper twice,
or sharing one between two rockets, is not supported: the second reset
replaces the first, and the two entries end up drawing from one generator.

A shared ``CustomSampler.seed_group`` keeps its own rule: a group belongs to
one model. Sharing one between two components leaves each of them seeding it
from their own child, and the last one to be reset decides what both draw.

Conclusion
----------

Expand Down
45 changes: 34 additions & 11 deletions rocketpy/stochastic/stochastic_rocket.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import warnings

import numpy as np

from rocketpy.control import _Controller
from rocketpy.mathutils.vector_matrix import Vector
from rocketpy.motors.empty_motor import EmptyMotor
Expand All @@ -21,6 +23,7 @@
from rocketpy.rocket.rocket import Rocket
from rocketpy.stochastic.stochastic_generic_motor import StochasticGenericMotor
from rocketpy.stochastic.stochastic_motor_model import StochasticMotorModel
from rocketpy.tools import _seed_sequence_to_int

from .stochastic_aero_surfaces import (
StochasticAirBrakes,
Expand Down Expand Up @@ -173,25 +176,45 @@ def __init__(
coordinate_system_orientation=None,
)

# Nested stochastic objects, in spawn order. A collection's stream is
# addressed by where its name falls in the two tuples read end to end, so
# appending to the first one moves every name in the second.
_POSITIONED_COLLECTIONS = ("aerodynamic_surfaces", "motors", "rail_buttons")
_PLAIN_COLLECTIONS = ("parachutes", "air_brakes")

@classmethod
def _stochastic_collections(cls):
"""The names of every attribute holding nested stochastic objects."""
return cls._POSITIONED_COLLECTIONS + cls._PLAIN_COLLECTIONS

def _set_stochastic(self, seed=None):
"""Set the stochastic attributes for Components, positions and
inputs.

Each component takes its own child of its collection's root, so
components stay independent and one seed still reproduces the rocket.
The body keeps the seed as given, and a collection has a root of its
own, so adding a fin does not move every parachute.

Parameters
----------
seed : int, optional
Seed for the random number generator.
"""
super()._set_stochastic(seed)
self.aerodynamic_surfaces = self.__reset_components(
self.aerodynamic_surfaces, seed
)
self.motors = self.__reset_components(self.motors, seed)
self.rail_buttons = self.__reset_components(self.rail_buttons, seed)
for parachute in self.parachutes:
parachute._set_stochastic(seed)
names = self._stochastic_collections()
roots = dict(zip(names, np.random.SeedSequence(seed).spawn(len(names))))
for name in self._POSITIONED_COLLECTIONS:
setattr(
self, name, self.__reset_components(getattr(self, name), roots[name])
)
for name in self._PLAIN_COLLECTIONS:
for component in getattr(self, name):
component._set_stochastic(
_seed_sequence_to_int(roots[name].spawn(1)[0])
)

def __reset_components(self, components, seed):
def __reset_components(self, components, root):
"""Creates a new Components whose stochastic structures
and their positions are reset.

Expand All @@ -200,8 +223,8 @@ def __reset_components(self, components, seed):
components : Components
The components which contains the stochastic structure that
will be used to create the new components.
seed : int, optional
Seed for the random number generator.
root : numpy.random.SeedSequence
The reseed's root. Each component takes its own spawned child.

Returns
-------
Expand All @@ -213,7 +236,7 @@ def __reset_components(self, components, seed):
new_components = Components()
for stochastic_obj, _ in components:
stochastic_obj_position_info = self.__components_map[stochastic_obj]
stochastic_obj._set_stochastic(seed)
stochastic_obj._set_stochastic(_seed_sequence_to_int(root.spawn(1)[0]))
new_components.add(
stochastic_obj,
self._validate_position(stochastic_obj, stochastic_obj_position_info),
Expand Down
8 changes: 8 additions & 0 deletions rocketpy/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -1467,6 +1467,14 @@ def find_obj_from_hash(obj, hash_, depth_limit=None):
return None


def _seed_sequence_to_int(seed_sequence):
"""Returns a ``SeedSequence`` as the 128-bit ``int`` seed ``random.Random``
accepts, combined by value so it does not depend on byte order.
"""
words = seed_sequence.generate_state(4, dtype=np.uint32)
return sum(int(word) << (32 * position) for position, word in enumerate(words))


if __name__ == "__main__": # pragma: no cover
import doctest

Expand Down
Loading
Loading