ENH: StochasticFreeFormFins for Monte Carlo simulations - #1117
ENH: StochasticFreeFormFins for Monte Carlo simulations#1117Gui-FernandesBR wants to merge 6 commits into
Conversation
`_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>
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>
|
Failed to generate code suggestions for PR |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## develop #1117 +/- ##
===========================================
+ Coverage 82.18% 82.94% +0.76%
===========================================
Files 122 129 +7
Lines 16355 16901 +546
===========================================
+ Hits 13441 14019 +578
+ Misses 2914 2882 -32 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
LGTM |
There was a problem hiding this comment.
Pull request overview
Adds stochastic free-form fin support for Monte Carlo simulations and fixes deterministic aerodynamic-surface wrapping.
Changes:
- Adds
StochasticFreeFormFins, exports, validation, and rocket integration. - Adds unit and Monte Carlo integration tests.
- Adds API documentation and changelog entries.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
rocketpy/stochastic/stochastic_aero_surfaces.py |
Implements stochastic free-form fins. |
rocketpy/stochastic/stochastic_rocket.py |
Adds free-form fins and fixes surface wrapping. |
rocketpy/stochastic/__init__.py |
Exports the new model. |
rocketpy/__init__.py |
Adds the public top-level export. |
tests/unit/stochastic/test_stochastic_aero_surfaces.py |
Tests formats, sampling, and validation. |
tests/unit/stochastic/test_stochastic_rocket.py |
Tests rocket integration and wrapping. |
tests/integration/simulation/test_monte_carlo.py |
Tests end-to-end Monte Carlo serialization. |
tests/fixtures/monte_carlo/stochastic_fixtures.py |
Adds a stochastic fins fixture. |
docs/user/stochastic.rst |
Documents block outline randomization. |
docs/reference/classes/monte_carlo/stochastic_models/stochastic_free_form_fins.rst |
Adds API reference. |
docs/reference/classes/monte_carlo/stochastic_models/index.rst |
Adds reference navigation. |
CHANGELOG.md |
Records enhancement and bug fix. |
Suppressed comments (2)
rocketpy/stochastic/stochastic_aero_surfaces.py:528
get_distributionreturns raw NumPyGeneratormethods, and the base generator later callsdist_func(nominal_outline, std_dev). This does not implement the promised block perturbation:normalmakes an independent draw for every coordinate,uniformtreats the outline aslowand0.001ashigh(and raises for coordinates above0.001), andwaldrejects the outline's zero-valued means. Sample one scalar deviation under a defined distribution contract and then add/broadcast that value over the complete outline; please cover the advertiseduniformandwaldforms with regression tests.
_, std_dev, dist_func = super()._validate_tuple(
input_name, (0.0,) + tuple(input_value[1:]), getattr
)
return (nominal_outline, std_dev, dist_func)
rocketpy/stochastic/stochastic_aero_surfaces.py:541
np.shaperaisesValueErrorfor ragged nested sequences, but this catches onlyIndexError. As a result, a malformed ragged outline leaks the wrong exception, and a valid candidate list whose outlines use different numbers of points cannot be accepted. Convert safely once and returnFalseon conversion failure so the caller can validate each candidate separately.
try:
return len(np.shape(value)) == 2 and np.shape(value)[1] == 2
except IndexError: # pragma: no cover - ragged input
return False
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| for outline in shape_points: | ||
| self._validate_outline(outline) | ||
| return shape_points |
There was a problem hiding this comment.
Fixed in c685d63. The choice now comes from a generator of the model's own, derived from the same seed through _sampler_seed but on a stream separate from the one the distributions draw from, so declaring a list input does not shift the numbers every other input gets and existing fixed-seed baselines without a list input stay where they are. StochasticRocket._randomize_position had the same bug and is fixed too. Regression test: test_list_choices_are_reproducible.
| - ``list``: list of candidate outlines, one of which is chosen at | ||
| random. A single outline must therefore be wrapped in a list, | ||
| i.e. ``[[(0, 0), (0.1, 0.1), (0.1, 0)]]``. |
There was a problem hiding this comment.
Fixed in 033f28e. The docstring now describes the two forms a list can take separately: one fixed outline, used as given, or a list of candidate outlines to choose between (which no longer have to have the same number of points). The extra nesting level is not required.
| coordinate of every point. A list is still a set of values to choose from, | ||
| which for this argument means a list of candidate outlines, so a single | ||
| outline must be wrapped in a list to be read as one candidate rather than as | ||
| a list of points:: |
There was a problem hiding this comment.
Fixed in 033f28e, along with the matching docstring. The user guide now documents the bare-outline and candidate-list forms separately, states which distributions can be used for shape_points and why, and notes that the fin root is held on the body line.
Both branches added an entry at the top of the unreleased `Added` section, so the conflict is only in the order of two lines. Both are kept, the new one on top, as the changelog asks for. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`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>
`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>
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>
|
Review comments addressed, and The two suppressed commentsThe distribution contract ( Instead the per-coordinate draw is kept, which is what a manufacturing tolerance actually is, and the contract is now stated and enforced: only The ragged input ( Further defects found while addressing the above
Verification
|
Closes #953.
Why
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.
What
StochasticFreeFormFins(ENH)shape_pointsdoes not fit the one-number-per-input assumptionStochasticModelmakes, 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 of every point.Two formats needed handling before reaching the base class, both of them the natural thing for a user to write:
[(0, 0), (0.08, 0.1), (0.12, 0)]is alist, 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, so a list of outlines still means a choice between shapes.(nominal outline, standard deviation)has a list where_validate_tuplerequires a number. Only that first item is special-cased; the deviation and the distribution name still go through the base class, so they are checked the same way as everywhere else and the distribution is drawn from this model's generator.All six formats are accepted:
An outline that cannot mean a fin shape — empty, ragged, fewer than three points, three-dimensional — now fails during validation rather than reaching
FreeFormFins.Also adds
StochasticRocket.add_free_form_fins, therocketpy/rocketpy.stochasticexports, the API reference page, and a note in the stochastic usage docs aboutshape_pointsbeing the one argument randomized as a block.Deterministic surfaces were never accepted (
BUG)Found while wiring the above, kept as its own commit so it can be reviewed or reverted independently.
_add_surfaceswrapped a plain surface withstochastic_type(component=...), but none of the stochastic aero-surface classes take acomponentkeyword — each names its first parameter after its own surface (nosecone,tail,trapezoidal_fins, ...). Passing anything other than an already-stochastic surface toadd_nose,add_trapezoidal_fins,add_elliptical_finsoradd_tailtherefore raised aTypeError, even though all four document the deterministic type as accepted:Passed positionally instead, which reaches every class regardless of what it calls that parameter.
Tests
24 new tests: the six accepted formats, nine invalid inputs, choice between outlines, the nominal outline surviving when nothing is randomized, and
StochasticRocketintegration. Plus an integration test that runs a real 2-simulation Monte Carlo and asserts the perturbedshape_pointssurvives JSON serialization as an outline rather than as a single point.The regression test for the
component=bug fails with theTypeErrorabove without the fix.Locally: 1926 unit + 153 integration passing, plus the 5 slow Monte Carlo tests.
ruff check,ruff formatandpylintclean (10.00/10).🤖 Generated with Claude Code