Skip to content

Commit 3fe2b62

Browse files
ENH: allow users to provide custom samplers (#803)
* ENH: first implementation of CustomSampler * TST: implementing tests for CustomSampler and making small corrections * DOC: creating documentation for CustomSampler * DEV: changelog --------- Co-authored-by: Mateus Stano Junqueira <[email protected]> Co-authored-by: MateusStano <[email protected]>
1 parent 7ee0872 commit 3fe2b62

File tree

16 files changed

+602
-5
lines changed

16 files changed

+602
-5
lines changed

.vscode/settings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
"Bigl",
4949
"Bigr",
5050
"bijective",
51+
"Bivariate",
5152
"bmatrix",
5253
"boldsymbol",
5354
"boxplot",

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
3131
Attention: The newest changes should be on top -->
3232

3333
### Added
34-
34+
- ENH: allow users to provide custom samplers [#803](https://github.com/RocketPy-Team/RocketPy/pull/803)
3535
- ENH: Implement Multivariate Rejection Sampling (MRS) [#738] (https://github.com/RocketPy-Team/RocketPy/pull/738)
3636
- ENH: Create a rocketpy file to store flight simulations [#800](https://github.com/RocketPy-Team/RocketPy/pull/800)
3737
- ENH: Support for the RSE file format has been added to the library [#798](https://github.com/RocketPy-Team/RocketPy/pull/798)
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Custom Sampler
2+
---------------------------
3+
4+
.. autoclass:: rocketpy.stochastic.CustomSampler
5+
:members:

docs/reference/classes/monte_carlo/stochastic_models/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,4 @@ input parameters, enabling robust Monte Carlo simulations.
2424
stochastic_rocket
2525
stochastic_parachute
2626
stochastic_flight
27+
custom_sampler

docs/user/custom_sampler.rst

Lines changed: 354 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,354 @@
1+
.. _custom_sampler:
2+
3+
Implementing custom sampler for Stochastic objects
4+
==================================================
5+
6+
The :ref:`stochastic_usage` documentation teaches how to work with stochastic
7+
objects, discussing the standard initializations, how to create objects and interpret
8+
outputs. Our goal here is to show how to build a custom sampler that gives complete
9+
control of the distributions used.
10+
11+
Custom Sampler
12+
--------------
13+
14+
Rocketpy provides a ``CustomSampler`` abstract class which works as the backbone of
15+
a custom sampler. We begin by first importing it and some other useful modules
16+
17+
.. jupyter-execute::
18+
19+
from rocketpy import CustomSampler
20+
from matplotlib import pyplot as plt
21+
import numpy as np
22+
23+
In order to use it, we must create a new class that inherits from
24+
it and it **must** implement two methods: *sample* and *reset_seed*. The *sample*
25+
method has one argument, *n_samples*, and must return a list with *n_samples*
26+
entries, each of which is a sample of the desired variable. The *reset_seed* method
27+
has one argument, *seed*, which is used to reset the pseudorandom generators in order
28+
to avoid unwanted dependency across samples. This is especially important when the
29+
``MonteCarlo`` class is used in parallel mode.
30+
31+
Below, we give an example of how to implement a mixture of two Gaussian
32+
distributions.
33+
34+
.. jupyter-execute::
35+
36+
class TwoGaussianMixture(CustomSampler):
37+
"""Class to sample from a mixture of two Gaussian distributions
38+
"""
39+
40+
def __init__(self, means_tuple, sd_tuple, prob_tuple, seed = None):
41+
""" Creates a sampler for a mixture of two Gaussian distributions
42+
43+
Parameters
44+
----------
45+
means_tuple : 2-tuple
46+
2-Tuple that contains the mean of each normal distribution of the
47+
mixture
48+
sd_tuple : 2-tuple
49+
2-Tuple that contains the sd of each normal distribution of the
50+
mixture
51+
prob_tuple : 2-tuple
52+
2-Tuple that contains the probability of each normal distribution
53+
of the mixture. Its entries should be non-negative and sum up to 1.
54+
"""
55+
np.random.default_rng(seed)
56+
self.means_tuple = means_tuple
57+
self.sd_tuple = sd_tuple
58+
self.prob_tuple = prob_tuple
59+
60+
def sample(self, n_samples = 1):
61+
"""Samples from a mixture of two Gaussian
62+
63+
Parameters
64+
----------
65+
n_samples : int, optional
66+
Number of samples, by default 1
67+
68+
Returns
69+
-------
70+
samples_list
71+
List containing n_samples samples
72+
"""
73+
samples_list = [0] * n_samples
74+
mixture_id_list = np.random.binomial(1, self.prob_tuple[0], n_samples)
75+
for i, mixture_id in enumerate(mixture_id_list):
76+
if mixture_id:
77+
samples_list[i] = np.random.normal(self.means_tuple[0], self.sd_tuple[0])
78+
else:
79+
samples_list[i] = np.random.normal(self.means_tuple[1], self.sd_tuple[1])
80+
81+
return samples_list
82+
83+
def reset_seed(self, seed=None):
84+
"""Resets all associated random number generators
85+
86+
Parameters
87+
----------
88+
seed : int, optional
89+
Seed for the random number generator.
90+
"""
91+
np.random.default_rng(seed)
92+
93+
This is an example of a distribution that is not implemented in numpy. Note that it is
94+
a general distribution, so we can use it for many different variables.
95+
96+
.. note::
97+
You can check more information about the mixture of Gaussian distributions
98+
`here <https://en.wikipedia.org/wiki/Mixture_model#Gaussian_mixture_model>`.
99+
Intuitively, if you think of a single Gaussian as a bell curve distribution,
100+
the mixture distribution resembles two bell curves superimposed, each with mode at their
101+
respective mean.
102+
103+
Example: Gaussian Mixture for Total Impulse
104+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
105+
106+
In order to use the new created sampler in a stochastic object, we first need
107+
to build an object. In this example, we will consider a case where the distribution of
108+
the total impulse is a mixture of two gaussian with mean parameters
109+
:math:`(6000, 7000)`, standard deviations :math:`(300, 100)` and mixture probabilities
110+
:math:`(0.7, 0.3)`.
111+
112+
.. jupyter-execute::
113+
114+
means_tuple = (6000, 7000)
115+
sd_tuple = (300, 100)
116+
prob_tuple = (0.7, 0.3)
117+
total_impulse_sampler = TwoGaussianMixture(means_tuple, sd_tuple, prob_tuple)
118+
119+
Finally, we can create ``StochasticSolidMotor`` object as we did in the example of
120+
:ref:`stochastic_usage`, but we pass the sampler object instead for the *total_impulse*
121+
argument
122+
123+
.. jupyter-execute::
124+
125+
from rocketpy import SolidMotor, StochasticSolidMotor
126+
127+
motor = SolidMotor(
128+
thrust_source="../data/motors/cesaroni/Cesaroni_M1670.eng",
129+
dry_mass=1.815,
130+
dry_inertia=(0.125, 0.125, 0.002),
131+
nozzle_radius=33 / 1000,
132+
grain_number=5,
133+
grain_density=1815,
134+
grain_outer_radius=33 / 1000,
135+
grain_initial_inner_radius=15 / 1000,
136+
grain_initial_height=120 / 1000,
137+
grain_separation=5 / 1000,
138+
grains_center_of_mass_position=0.397,
139+
center_of_dry_mass_position=0.317,
140+
nozzle_position=0,
141+
burn_time=3.9,
142+
throat_radius=11 / 1000,
143+
coordinate_system_orientation="nozzle_to_combustion_chamber",
144+
)
145+
146+
stochastic_motor = StochasticSolidMotor(
147+
solid_motor=motor,
148+
burn_start_time=(0, 0.1, "binomial"),
149+
grains_center_of_mass_position=0.001,
150+
grain_density=10,
151+
grain_separation=1 / 1000,
152+
grain_initial_height=1 / 1000,
153+
grain_initial_inner_radius=0.375 / 1000,
154+
grain_outer_radius=0.375 / 1000,
155+
throat_radius=0.5 / 1000,
156+
nozzle_radius=0.5 / 1000,
157+
nozzle_position=0.001,
158+
total_impulse=total_impulse_sampler, # total impulse using custom sampler!
159+
)
160+
161+
stochastic_motor.visualize_attributes()
162+
163+
Let's generate some random motors and check the distribution of the total impulse
164+
165+
.. jupyter-execute::
166+
167+
total_impulse_samples = [
168+
stochastic_motor.create_object().total_impulse for _ in range(200)
169+
]
170+
plt.hist(total_impulse_samples, density = True, bins = 30)
171+
172+
Introducing dependency between parameters
173+
-----------------------------------------
174+
175+
Although probabilistic **independency between samples**, i.e. different flight runs,
176+
is desired for Monte Carlo simulations, it is often important to be able to introduce
177+
**dependency between parameters**. A clear example of this is wind speed: if we know
178+
the wind speed in the x-axis, then our forecast model might tells us that the wind
179+
speed y-axis is more likely to be positive than negative, or vice-versa. These
180+
parameters are then correlated, and using samplers that model these correlations make
181+
the Monte Carlo analysis more robust.
182+
183+
When we use the default numpy samplers, the Monte Carlo analysis samples the
184+
parameters independently from each other. However, using custom samplers, we can
185+
introduce dependency and correlation! It might be a bit tricky, but we will show how
186+
it can be done. First, let us import the modules required
187+
188+
.. jupyter-execute::
189+
190+
from rocketpy import Environment, StochasticEnvironment
191+
from datetime import datetime, timedelta
192+
193+
Assume we want to model the x and y axis wind speed as a Bivariate Gaussian with
194+
parameters :math:`\mu = (1, 1)` and variance-covariance matrix
195+
:math:`\Sigma = \begin{bmatrix} 0.2 & 0.17 \\ 0.17 & 0.3 \end{bmatrix}`. This will
196+
make the correlation between the speeds be of :math:`0.7`.
197+
198+
Now, in order to correlate the parameters using different custom samplers,
199+
**the key trick is to create a common generator that will be used by both.** The code
200+
below implements an example of such a generator
201+
202+
.. jupyter-execute::
203+
204+
class BivariateGaussianGenerator:
205+
"""Bivariate Gaussian generator used across custom samplers
206+
"""
207+
def __init__(self, mean, cov, seed = None):
208+
"""Initializes the generator
209+
210+
Parameters
211+
----------
212+
mean : tuple, list
213+
Tuple or list with mean of bivariate Gaussian
214+
cov : np.array
215+
Variance-Covariance matrix
216+
seed : int, optional
217+
Number to seed random generator, by default None
218+
"""
219+
np.random.default_rng(seed)
220+
self.samples_list = []
221+
self.samples_generated = 0
222+
self.used_samples_x = 0
223+
self.used_samples_y = 0
224+
self.mean = mean
225+
self.cov = cov
226+
self.generate_samples(1000)
227+
228+
def generate_samples(self, n_samples = 1):
229+
"""Generate samples from bivariate Gaussian and append to sample list
230+
231+
Parameters
232+
----------
233+
n_samples : int, optional
234+
Number of samples to be generated, by default 1
235+
"""
236+
samples_generated = np.random.multivariate_normal(self.mean, self.cov, n_samples)
237+
self.samples_generated += n_samples
238+
self.samples_list += list(samples_generated)
239+
240+
def reset_seed(self, seed=None):
241+
np.random.default_rng(seed)
242+
243+
def get_samples(self, n_samples, axis):
244+
if axis == "x":
245+
if self.samples_generated < self.used_samples_x:
246+
self.generate_samples(n_samples)
247+
samples_list = [
248+
sample[0] for sample in self.samples_list[self.used_samples_x:(self.used_samples_x + n_samples)]
249+
]
250+
if axis == "y":
251+
if self.samples_generated < self.used_samples_y:
252+
self.generate_samples(n_samples)
253+
samples_list = [
254+
sample[1] for sample in self.samples_list[self.used_samples_y:(self.used_samples_y + n_samples)]
255+
]
256+
self.update_info(n_samples, axis)
257+
return samples_list
258+
259+
def update_info(self, n_samples, axis):
260+
"""Updates the information of the used samples
261+
262+
Parameters
263+
----------
264+
n_samples : int
265+
Number of samples used
266+
axis : str
267+
Which axis was sampled
268+
"""
269+
if axis == "x":
270+
self.used_samples_x += n_samples
271+
if axis == "y":
272+
self.used_samples_y += n_samples
273+
274+
This generator samples from the bivariate Gaussian and stores then in a *samples_list*
275+
attribute. Then, the idea is to create two samplers for the wind speeds that will share
276+
an object of this class and their sampling methods only get samples from the stored
277+
sample list.
278+
279+
.. jupyter-execute::
280+
281+
class WindXSampler(CustomSampler):
282+
"""Samples from X"""
283+
284+
def __init__(self, bivariate_gaussian_generator):
285+
self.generator = bivariate_gaussian_generator
286+
287+
def sample(self, n_samples=1):
288+
samples_list = self.generator.get_samples(n_samples, "x")
289+
return samples_list
290+
291+
def reset_seed(self, seed=None):
292+
self.generator.reset_seed(seed)
293+
294+
class WindYSampler(CustomSampler):
295+
"""Samples from Y"""
296+
297+
def __init__(self, bivariate_gaussian_generator):
298+
self.generator = bivariate_gaussian_generator
299+
300+
def sample(self, n_samples=1):
301+
samples_list = self.generator.get_samples(n_samples, "y")
302+
return samples_list
303+
304+
def reset_seed(self, seed=None):
305+
self.generator.reset_seed(seed)
306+
307+
Then, we create the objects
308+
309+
.. jupyter-execute::
310+
311+
mean = [1, 2]
312+
cov_mat = [[0.2, 0.171], [0.171, 0.3]]
313+
bivariate_gaussian_generator = BivariateGaussianGenerator(mean, cov_mat)
314+
wind_x_sampler = WindXSampler(bivariate_gaussian_generator)
315+
wind_y_sampler = WindYSampler(bivariate_gaussian_generator)
316+
317+
With the sample objects created, we only need to create the stochastic objects and
318+
pass them as argument
319+
320+
.. jupyter-execute::
321+
322+
spaceport_env = Environment(
323+
latitude=32.990254,
324+
longitude=-106.974998,
325+
elevation=1400,
326+
datum="WGS84",
327+
)
328+
spaceport_env.set_atmospheric_model("custom_atmosphere", wind_u = 1, wind_v = 1)
329+
spaceport_env.set_date(datetime.now() + timedelta(days=1))
330+
331+
stochastic_environment = StochasticEnvironment(
332+
environment=spaceport_env,
333+
elevation=(1400, 10, "normal"),
334+
gravity=None,
335+
latitude=None,
336+
longitude=None,
337+
ensemble_member=None,
338+
wind_velocity_x_factor=wind_x_sampler,
339+
wind_velocity_y_factor=wind_y_sampler
340+
)
341+
342+
Finally, let us check that if there is a correlation between the wind speed in the
343+
two axis
344+
345+
.. jupyter-execute::
346+
347+
wind_velocity_x_samples = [0] * 200
348+
wind_velocity_y_samples = [0] * 200
349+
for i in range(200):
350+
stochastic_environment.create_object()
351+
wind_velocity_x_samples[i] = stochastic_environment.obj.wind_velocity_x(0)
352+
wind_velocity_y_samples[i] = stochastic_environment.obj.wind_velocity_y(0)
353+
354+
np.corrcoef(wind_velocity_x_samples, wind_velocity_y_samples)

docs/user/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ RocketPy's User Guide
3232
:caption: Monte Carlo Simulations
3333

3434
Stochastic Classes <stochastic.rst>
35+
Custom Sampler <custom_sampler.rst>
3536
../notebooks/monte_carlo_analysis/monte_carlo_class_usage.ipynb
3637
../notebooks/monte_carlo_analysis/monte_carlo_analysis.ipynb
3738
../notebooks/monte_carlo_analysis/parachute_drop_from_helicopter.ipynb

0 commit comments

Comments
 (0)