Skip to content

Commit d21abde

Browse files
ENH: StochasticFreeFormFins for Monte Carlo simulations (#1117)
* BUG: accept a deterministic aerodynamic surface in StochasticRocket `_add_surfaces` wrapped a plain surface with `stochastic_type(component=...)`, but none of the stochastic aero-surface classes take a `component` keyword: each names its first parameter after its own surface (`nosecone`, `tail`, `trapezoidal_fins`, ...). Passing anything other than an already-stochastic surface to `add_nose`, `add_trapezoidal_fins`, `add_elliptical_fins` or `add_tail` therefore raised a TypeError, even though all four document the deterministic type as accepted. Passed positionally instead, which reaches every class regardless of what it calls that parameter. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * ENH: StochasticFreeFormFins for Monte Carlo simulations Closes #953. Free-form fin sets were the only aerodynamic surface without a stochastic counterpart, so a rocket using them could not be varied in a Monte Carlo run. `shape_points` does not fit the one-number-per-input assumption the base class makes, and a fin shape is only meaningful as a complete set of points, so the outline is randomized as a block: one sampled deviation applied to every coordinate. Two formats needed handling before reaching the base class, both of them the natural thing to write: - a bare outline is a `list`, which the base class reads as a list of candidate values and would have sampled a single (x, y) point from. It is wrapped as the one candidate outline it is, and a list of outlines still means a choice between shapes. - `(nominal outline, standard deviation)` has a list where `_validate_tuple` requires a number. Only that first item is special-cased; the deviation and the distribution name still go through the base class, so the distribution is drawn from this model's generator like every other input. An outline that cannot mean a fin shape - empty, ragged, fewer than three points, three-dimensional - now fails during validation rather than reaching FreeFormFins. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * BUG: seed the choice between the candidate values of a list input `dict_generator` and `StochasticRocket._randomize_position` picked from a list with `random.choice`, which draws from the interpreter-wide stream. `_set_stochastic` only rebuilds the model's own numpy generator, so that stream was never reseeded: a fixed-seed run did not reproduce the values chosen from a list, and Monte Carlo workers forked from one process inherited a single `random` state and walked the same choice sequence instead of sampling independently. The choice now comes from a generator of the model's own, derived from the same seed through `_sampler_seed` but kept apart from the one the distributions draw from, so that declaring a list input does not shift the numbers every other input gets and existing fixed-seed baselines that use no list input stay where they are. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * MNT: let a stochastic input hold an array as its nominal value `StochasticFreeFormFins` needed a whole fin outline where the base class reads a single number, and got there by overriding `_validate_tuple`, comparing the input name against the literal `"shape_points"` and smuggling a `0.0` placeholder through `super()`. The rest of the machinery was never told, so `_validate_scalar`, `dict_generator` and `visualize_attributes` all still believed the value was a number: the public `visualize_attributes` raised a `TypeError` formatting an outline with `:.5f`, and the next array-valued input would have had to rediscover the same workaround. `array_valued_inputs` declares those inputs by name on the class instead, so validation, sampling and the report agree on which ones they are. `_nominal_value` converts them where the nominal value is looked up, and the report prints the array's shape rather than trying to format its coordinates as one number. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * BUG: fix which shape_points StochasticFreeFormFins accepts and rejects The `shape_points` contract did not hold up to the formats the docstring and the user guide advertise: - `_is_outline` caught only `IndexError`, but `np.shape` raises a `ValueError` on a ragged sequence. A list of candidate outlines with different numbers of points -- choosing between a three-point and a four-point fin, the plainest form of choosing between shapes -- died with an opaque numpy error, and a ragged outline did not fail during validation as claimed. Each candidate is now converted on its own. - Every distribution name was accepted, but the deviation is applied as `dist_func(nominal_outline, std_dev)`, so only the ones that read the first argument as the centre of the draw can work. `uniform` read the outline as its lower bound and `wald` rejected the zeros of a root point, both raising in the middle of a Monte Carlo run. The four that can are accepted and the rest are rejected up front. - An outline of non-numbers passed validation and died inside `_FreeFormGeometry.infer_dimensions`; a numpy array was rejected even though `create_object` produces one and `FreeFormFins` takes one; and `shape_points=[]` raised where an empty list means "use the nominal value" for every other input. The fin root is also held on the body line now. `FreeFormFins` measures the span from y = 0 and slices the chords over that interval, so perturbing every y drove most samples off the line -- 227 of 300 outlines had a point inside the airframe -- and the interference factors were computed from the inflated span that followed. Points nominally on the line stay there and none is allowed to cross it, which is what the root edge of a fin does anyway. The docstring and the user guide said a single outline "must be wrapped in a list" while the code auto-detects a bare one, and that one deviation is shared by every coordinate while each is in fact drawn on its own. Both now describe what the code does. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent a4a61c6 commit d21abde

14 files changed

Lines changed: 933 additions & 18 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ Attention: The newest changes should be on top -->
3232

3333
### Added
3434

35+
- ENH: `StochasticFreeFormFins`, so free-form fin sets can be used in Monte Carlo simulations. The outline is randomized as a block, since a shape is only meaningful as a complete set of points: every coordinate is perturbed by its own draw, the fin root is held on the body line, and a list of candidate outlines can have a different number of points in each. [#953](https://github.com/RocketPy-Team/RocketPy/issues/953)
3536
- ENH: Support for Open Meteo API in the `Environment` class, adding the `open_meteo` and `open_meteo_ensemble` atmospheric models. Pressure-level forecasts, past forecasts (from 2021 onwards) and ensembles are read straight from a keyless JSON API, with no external files and no netCDF/OPeNDAP dependency. [#520](https://github.com/RocketPy-Team/RocketPy/issues/520) [#1119](https://github.com/RocketPy-Team/RocketPy/pull/1119)
3637
- ENH: Add simplified opening shock force estimation [#1092](https://github.com/RocketPy-Team/RocketPy/pull/1092)
3738
- ENH: Add Qodo PR-Agent workflow using Google Gemini [#1089](https://github.com/RocketPy-Team/RocketPy/pull/1089)
@@ -48,9 +49,11 @@ Attention: The newest changes should be on top -->
4849

4950
### Fixed
5051

52+
- BUG: Pick between the candidate values of a list input with the stochastic model's own seeded generator. `random.choice` was used, which draws from the interpreter-wide stream that `_set_stochastic` does not reseed, so a fixed seed did not reproduce the values chosen from a list, and Monte Carlo workers forked from one process walked a single shared stream instead of sampling independently. Fixed-seed baselines that vary a list input change. [#953](https://github.com/RocketPy-Team/RocketPy/issues/953)
5153
- BUG: Report the atmospheric model time period and ensemble member count for lower-case model types. `set_atmospheric_model` documents `type` as case-insensitive, but `Environment.info()` and `all_info()` compared against capitalised literals, so `type="ensemble"` printed no time period and no member count, and skipped the ensemble comparison plot. [#520](https://github.com/RocketPy-Team/RocketPy/issues/520)
5254
- BUG: Give each `CustomSampler` input its own deterministic stream, and seed samplers sharing one generator once as a group. Existing fixed-seed `CustomSampler` baselines change, and samplers built on the legacy `RandomState` must move to `default_rng` because seeds now carry the full 128 bits. [#1102](https://github.com/RocketPy-Team/RocketPy/pull/1102)
5355
- BUG: Accept the callable parachute triggers `StochasticParachute` documents, and reject the ones it cannot mean. An invalid string, an empty list or a boolean now fails during validation instead of reaching `Parachute` or becoming a one-metre height trigger. [#1103](https://github.com/RocketPy-Team/RocketPy/pull/1103)
56+
- BUG: Accept a deterministic aerodynamic surface in `StochasticRocket.add_nose`, `add_trapezoidal_fins`, `add_elliptical_fins` and `add_tail`. Each wrapped the surface with a `component=` keyword none of the stochastic classes accept, so passing anything other than an already-stochastic surface raised a `TypeError`. [#953](https://github.com/RocketPy-Team/RocketPy/issues/953)
5457
- BUG: rocket with a late-starting thrust curve never leaves the rail [#1085](https://github.com/RocketPy-Team/RocketPy/pull/1085)
5558

5659
## [v1.13.0] - 2026-07-21

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ input parameters, enabling robust Monte Carlo simulations.
1919
stochastic_nose_cone
2020
stochastic_trapezoidal_fins
2121
stochastic_elliptical_fins
22+
stochastic_free_form_fins
2223
stochastic_tail
2324
stochastic_rail_buttons
2425
stochastic_rocket
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Stochastic Free Form Fins
2+
-------------------------
3+
4+
.. autoclass:: rocketpy.stochastic.StochasticFreeFormFins
5+
:members:

‎docs/user/stochastic.rst‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,43 @@ passed in a few different ways:
9393
gives you the full control of how the samples are generated. See
9494
:ref:`custom_sampler` for more details.
9595

96+
.. note::
97+
The formats above assume each argument holds a single number. The
98+
``shape_points`` of :class:`rocketpy.stochastic.StochasticFreeFormFins` is
99+
the exception: a fin outline is only meaningful as a complete set of points,
100+
so the deviation given applies to the outline as a block, with every
101+
coordinate of every point perturbed by its own draw. The fin root is held on
102+
the body line, so a point nominally at ``y = 0`` keeps that value and no
103+
point ends up inside the airframe.
104+
105+
Because the deviation has to centre on the nominal coordinate, only the
106+
distributions that read their first argument as that centre can be used here:
107+
*"normal"*, *"gumbel"*, *"laplace"* and *"logistic"*. The others take bounds
108+
(*"uniform"*) or shape parameters (*"wald"*, *"gamma"*, ...), which a set of
109+
coordinates cannot be, and are rejected when the object is created.
110+
111+
A list means either one fixed outline, used as given, or a list of candidate
112+
outlines to choose between, which do not have to have the same number of
113+
points::
114+
115+
# One millimetre of deviation on every coordinate
116+
StochasticFreeFormFins(free_form_fins=fins, shape_points=0.001)
117+
118+
# One fixed outline, not randomized
119+
StochasticFreeFormFins(
120+
free_form_fins=fins,
121+
shape_points=[(0, 0), (0.08, 0.1), (0.12, 0)],
122+
)
123+
124+
# Choose between two outlines
125+
StochasticFreeFormFins(
126+
free_form_fins=fins,
127+
shape_points=[[(0, 0), (0.08, 0.1), (0.12, 0)], [(0, 0), (0.06, 0.12), (0.12, 0)]],
128+
)
129+
130+
A ``CustomSampler`` given for this argument has to yield a whole outline per
131+
sample, since what it returns replaces the outline instead of perturbing it.
132+
96133
.. note::
97134
In statistics, the terms "Normal" and "Gaussian" refer to the same type of \
98135
distribution. This distribution is commonly used and is the default for the \

‎rocketpy/__init__.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
StochasticEllipticalFins,
6363
StochasticEnvironment,
6464
StochasticFlight,
65+
StochasticFreeFormFins,
6566
StochasticNoseCone,
6667
StochasticParachute,
6768
StochasticRocket,

‎rocketpy/stochastic/__init__.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from .stochastic_aero_surfaces import (
1010
StochasticAirBrakes,
1111
StochasticEllipticalFins,
12+
StochasticFreeFormFins,
1213
StochasticNoseCone,
1314
StochasticRailButtons,
1415
StochasticTail,

0 commit comments

Comments
 (0)