Updates master with develop before releasing v1.14.0 - #1194
Draft
Gui-FernandesBR wants to merge 95 commits into
Draft
Updates master with develop before releasing v1.14.0#1194Gui-FernandesBR wants to merge 95 commits into
Gui-FernandesBR wants to merge 95 commits into
Conversation
update master with develop
CI: make changelog automation LLM-based (Gemini) and race-safe
* ENH: Resolve pressure_ISA discretization bounds TODO (#1056) * TST: Fix test_flight and environment interpolation due to new ISA bounds * MNT: self-document pressure_ISA bounds and strengthen discretization test Derive the pressure_ISA discretization bounds from the standard-atmosphere layer table (geopotential_height[0]/[-1]) instead of hardcoding -2000/80000, and document why the grid is split around sea level. Strengthen the discretization test with physical-sanity assertions (strictly increasing altitude, strictly decreasing pressure, sea level sampled). No change in numeric output. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * TST: update density doctest for new pressure_ISA discretization The finer spline knots shift density at 1000 m by ~1e-4 (density at sea level is unchanged since 0 m is still sampled exactly). Update the calculate_density_profile doctest expected value accordingly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * TST: relax noisy stream_velocity_z apogee tolerance stream_velocity_z at apogee is a residual of the apogee-time estimation: it is physically ~0 but swings by ~1e-4 m/s across platforms/NumPy versions and atmosphere discretizations (e.g. -8.9e-8 on py3.10 with the new ISA grid, -2.0e-4 with the old grid, +2.6e-4 on py3.14). The previous atol=1e-5 was tighter than this numerical noise, making the assertion flaky. Use atol=1e-3, which is physically negligible (vertical speed peaks above 200 m/s) while still catching real regressions. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * STY: apply ruff markdown formatting to README code blocks CI installs ruff unpinned; ruff 0.16 formats fenced Python blocks in Markdown by default, so `ruff format --check .` now flags README.md on every PR. Reformat the affected snippets (indentation, quotes, call wrapping) to unblock the lint check. No semantic changes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * MNT: satisfy pylint on the pressure_ISA discretization changes CI runs pylint unpinned; three messages were attributable to this PR: - C0415 import-outside-toplevel: move the tools import to the test module top level. - R0915 too-many-statements: keep the discretization block compact so pressure_ISA stays within the 25-statement limit. - C0302 too-many-lines: the terser block keeps environment.py under the 3050-line module limit. np.append over two linspaces yields the same grid as the previous np.concatenate, so all numeric results are unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* ENH: Add native Meteomatics API support to the Environment class Adds a new "meteomatics" atmospheric model to Environment.set_atmospheric_model, porting and generalizing the implementation from the EuRoC-Dev repository. - fetchers.py: fetch_meteomatics_token + fetch_atmospheric_data_from_meteomatics authenticate with username/password (short-lived token), query temperature, pressure and wind components by height above ground level, grouping the parameters to respect the account's per-request limit. - environment.py: process_meteomatics_atmosphere converts the height-AGL data to above-sea-level profiles using the Environment elevation; set_atmospheric_model gains username/password kwargs (falling back to METEOMATICS_USERNAME / METEOMATICS_PASSWORD env vars); save/load handles the new model type. - Network requests use timeouts, do not retry deterministic 4xx failures, and surface actionable RuntimeError messages. - Tests fully mock the API (no real requests, no charges); docs and changelog updated. Closes #545 * MNT: simplify Meteomatics fetcher and profile assembly Follow-up cleanups from the code review of the Meteomatics support: - Collapse the duplicated request/error ladder of the login and data endpoints into a single `_meteomatics_request_json` helper. - Have `_build_meteomatics_parameters` return a {parameter: (profile, height)} mapping so the response parser no longer regex-parses back the strings this module just built. Drops the regex constant and the variable-to-profile table. - Generalize `to_profile_array` to several profiles at once, so the wind u/v grid intersection reuses it instead of repeating it inline. - Reuse the existing `__validate_datetime` helper for the launch-date check, and simplify the model default to `model or "mix"`. - Normalize `atmospheric_model_type` once in `from_dict`. The type is stored as the user spelled it, so the previously case-sensitive `match` and `== "ensemble"` branches silently dropped the ensemble arrays for an Environment built with `type="Ensemble"`. - Trim the elevation warning and raise it before the API call, so it is shown even when the request later fails. * MNT: address Meteomatics review comments Two points raised in the PR review: - `fetch_atmospheric_data_from_meteomatics` stamped the instant with a trailing "Z" without normalizing the timezone, so an aware datetime in a non-UTC zone was sent as the wrong instant. Aware datetimes are now converted to UTC; naive ones are documented as assumed UTC. - Degenerate sampling arguments (`query_limit=0`, resolutions below 2) reached `range()`/`linspace()` and failed with an opaque low-level error after the login had already been paid for. They are now validated up front by `_validate_meteomatics_sampling`. - `process_meteomatics_atmosphere` silently coerced any non-string model to "mix", hiding a mistake such as passing a Dataset or a path as `file` and querying the wrong model. It now accepts None (default) or a string, and raises otherwise. * ENH: Refactor Meteomatics integration into MeteomaticsFetcher class and clean up environment method * ENH: Decompose fetchers.py into rocketpy/environment/fetchers package with dedicated submodules
Both examples build a generator with np.random.default_rng(seed) and drop it, then sample from the process-global np.random. reset_seed is a no-op, so a sampler written by following this page ignores random_seed. The study runs and the numbers look reasonable; only a second run with the same seed shows they were never reproducible. The bivariate generator needs two more things. It caches 1000 samples, so reset_seed has to discard them or the first 1000 draws after a reseed still come from the generator that was replaced. And its refill test compares samples_generated against used_samples, which only becomes true after the cache has run out, so a study longer than the cache silently got short lists: sample(n_samples=1) returns [] at simulation 1001 and dict_generator raises IndexError on [0]. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
…1078) (#1084) Isolates the three VTK/PyVista off-screen animation tests into their own workflow step so a native crash in one of them no longer kills the whole integration run, adds a bounded retry for the crash, forces Mesa software rendering on Linux, and sets fail-fast: false so one platform no longer cancels the rest of the matrix. Closes #1078
…411) (#1085) A motor whose thrust curve starts at t > 0 never left the rail: the rail phase's only time nodes were [t=0, max_time], so LSODA (max_step defaults to inf) took one huge step over the entire burn and the rocket never accelerated. __setup_phase_time_nodes now forces solver stops at ignition and burn-out when burn_start_time > 0, leaving ordinary motors byte-for-byte unchanged. Also moves Codecov's coverage config out of .github/workflows/codecov.yml (where Actions was trying to execute it as a workflow, failing on every run) to .codecov.yml. Closes #411
StochasticRocket.create_object built a Parachute from the full sampled draw, then discarded it and built a second one from six of its ten fields. radius, height, porosity and drag_coefficient never reached the rocket: the second Parachute re-derived radius from cd_s and the default drag coefficient, and height fell back to that radius. Attaching the sampled object itself fixes it and stops Parachute.__init__ running twice per parachute per simulation. Breaking: radius and height feed the parachute added-mass term in flight.py, so descent dynamics and landing points change for any study whose parachute carries geometry. Closes #1094
#1100) * MNT: do not let one Python version cancel the other in the slow matrix #1084 added fail-fast: false to the main test matrix while this was open, so the only half left is the slow one. Same reason: 3.10 failing says nothing about 3.14, so cancelling it costs a result and saves nothing worth having. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com> * MNT: retry the VTK tests on SIGSEGV, not only on macOS SIGBUS #1084 retries the animation tests when they die on 138, which is SIGBUS on macOS. Counting the last 40 Tests runs, the crash was 139 eight times and 138 twice, so the common case fell straight through the retry. Linux SIGBUS is 135 rather than 138, so that missed as well. Also sets fail-fast: false on the slow matrix, which #1084 left out. 3.10 failing says nothing about 3.14, so cancelling it costs a result and saves nothing worth having. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com> --------- Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com> Co-authored-by: Gui-FernandesBR <63590233+Gui-FernandesBR@users.noreply.github.com>
* ENH: Add Qodo PR-Agent workflow using Google Gemini * FIX: point PR Agent workflow at the renamed action repo Codium-ai/pr-agent-action no longer exists (the project moved to the-pr-agent/pr-agent), which made the "Run PR Agent" check fail with "Unable to resolve action". Updates the action reference, grants the contents: write permission it now requires, and sets the Gemini key via the GOOGLE_AI_STUDIO.GEMINI_API_KEY env var per the new action's docs.
* CI: build the docs for pull requests into develop as well `branches` filters on a pull request's base, and almost every PR here is opened against develop: 25 of the last 30, with 3 against master. So the build that runs with -W --keep-going saw a docs change for the first time in a release batch, well away from whatever caused it. The path filter already keeps this off PRs that cannot affect the docs, and `rocketpy/**` is in it because docstrings feed the autodoc reference, so a docstring edit merged into develop could break the API pages unnoticed too. `push` is left on master alone. The pull request check is where the feedback is worth having, and running both would double the cost for a second opinion on the same commit. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com> * Apply suggestion from @Gui-FernandesBR --------- Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com> Co-authored-by: Gui-FernandesBR <63590233+Gui-FernandesBR@users.noreply.github.com>
Every sampler on a model was reset with the model's seed, so two backed by default_rng started from identical state and drew identical underlying values. Not nearly identical, the same to every digit: two Gaussians with different means and spreads both produced the deviate 0.466220770577340. A study varying two parameters that way is varying one, and the correlation it reports between them is an artefact of the seeding. Each sampler now gets a child derived from the model's seed and the input's name. Keyed by name rather than position so declaring another parameter does not move the streams of the ones already there, and crc32 rather than hash because hash is not stable across processes. The documented wind X/Y wrappers are unaffected. Their correlation comes from sharing one samples_list, not from sharing a seed, so handing them separate children leaves it intact: measured 0.7010 against the covariance's 0.6981. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
Two problems with the first version of this, both found in review. CRC32 is 32 bits, and a collision puts two samplers back on one stream, which is the bug the keying exists to prevent. `wd4s4xka50` and `p56cjcee10` are both valid identifiers with CRC32 1560575156, and both derived the same seed. The name is length-prefixed into spawn-key words now, which no two names share, and the child is kept at its full 128 bits to match the Monte Carlo seeding rather than being cut to 64. Samplers can also share one generator on purpose, as the documented wind pair does, and each reset overwrites the last. With one seed per name, whichever was reset last decided the stream, so the same seed meant different runs depending on the order the model was declared in. Seeding is its own pass over sorted names now. The pass is separate from the validation loop deliberately. That loop's order sets __dict__, and so the order every other input is drawn in, so sorting it would have moved the samples of every model with a tuple in it. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
Two wrappers can share one generator on purpose, as the documented wind pair do. One seed per name reset that generator once per wrapper, so every seed but the last was discarded and the group's stream was decided by whichever member sorted last. Adding a third wrapper to the same generator therefore moved the first two, which name keying is meant to prevent. CustomSampler gains a `seed_group` property, `self` by default, so a wrapper can say which generator it shares. Members of a group are seeded once between them, with the seed derived from all their names rather than from whichever went last. The documented wind wrappers declare it. Before, resetting six times for three wrappers and moving the pair when a third arrived. After, once, and adding an independent sampler leaves the group where it was. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
Only RuntimeError was caught, and the seed handed over is now 128 bits, which
the legacy numpy.random.RandomState refuses:
ValueError: Seed must be between 0 and 2**32 - 1
Before this branch a sampler received the model's seed, usually a small int,
so RandomState took it. A sampler built on RandomState therefore breaks here,
and used to break with a bare ValueError that named nothing.
The seed stays 128 bits, since that is what keeps the streams apart and what
default_rng, the documented choice, takes. The error now says which input the
sampler belongs to and keeps the original as its cause.
Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
…discards Three things from review, all small. The documented bivariate generator filled a 1000-pair cache inside reset_seed. With per-index seeding that reset happens once per simulation, so a study built on this example generated a thousand pairs and used one, every time. 0.099 ms each, about 10 s over 100k simulations. `top_up` already fills the shortfall on first use, so the eager fill is gone and the cache starts empty. The group reset went through the first member rather than the group, which assumes every member resets identically and keeps nothing of its own. The group holds the shared state, so it is reset directly when it knows how, and the member is the fallback. `_sampler_seed` now sorts the names itself. The caller does today, and a future one that forgets would hand a single group two different seeds. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
Two rules the property invites breaking, both silent. Identity has to be stable. Building the answer on each call, which returning from a property makes easy, gives every member a different identity and puts each back in a group of its own: a two-member group goes from one reset to two. A group belongs to one model. Declaring the same generator on two models has them both seed it, and the later one wins, which is the overwrite the grouping exists to prevent. The documented wind pair already returns a stored attribute, so the example teaches the stable form. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
The automation that normally writes it cannot run on a pull request from a fork, which is #1101, so this one is by hand. It is a breaking change and the entry says so: fixed-seed CustomSampler baselines move, and a sampler built on the legacy RandomState has to move to default_rng because the seed it now receives is 128 bits wide. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
`scipy>=1.0` is not true. `monte_carlo.py` imports `scipy.stats.bootstrap` at module level, and the SciPy 1.7.0 release notes are where that arrives, so anything from 1.0 to 1.6 satisfies the floor and then fails on `import rocketpy`. `numpy>=1.13` is not true either, and the binding constraint turned out to be a sibling rather than NumPy itself: `matplotlib>=3.9.0` requires `numpy>=1.23`. SciPy then has to be new enough to allow that, and 1.7.2 caps NumPy at `<1.23.0`, so 1.8 is the first that composes. Verified rather than reasoned. On Python 3.10 with numpy 1.23.0 and scipy 1.8.0 pinned and everything else current, `import rocketpy` works and `tests/unit/simulation` with `tests/unit/stochastic` is 162 passed, 5 skipped. At numpy 1.21.3, which is where NumPy's own cp310 wheels start, matplotlib refuses to import. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
…1103) * BUG: accept the callable parachute triggers the docstring promises The `or` sat inside the isinstance call rather than beside it: isinstance(member, (str, int, float) or callable(member)) A non-empty type tuple is truthy, so the expression short-circuited to the tuple and callable(member) was never evaluated. The check reduced to isinstance(member, (str, int, float)), and a callable is none of those. Parachute takes a callable trigger and Flight calls it, and the docstring three lines above says "a list of callables, string 'apogee' or ints/floats". Only the stochastic wrapper refused one. The two non-callable forms passed throughout, which is why the tests never caught it. Left as an assert to match the other fourteen in these two modules, and because raising a different type would break anyone catching AssertionError. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com> * BUG: refuse the trigger forms Parachute cannot use Moving the `or` let callables through, but left four things the check should never have accepted. `["banana"]` passed here and then `Parachute` raised ValueError, so the wrapper only moved the failure to create time. `[True]` is worse: bool is an int, so it went through as a height of one metre. `[]` passed because `all([])` is True. And `numbers.Real` replaces `(int, float)`, which took numpy.float64 because it subclasses float and refused numpy.int64 because it subclasses neither. The check is raised rather than asserted. `python -O` strips an assert outright, and this is the only thing between those triggers and a Parachute that either refuses them later or misreads them. Still an AssertionError, so nothing that catches it has to change. The docstring claimed a tuple form that was never implemented; it now describes what the code does. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com> * BUG: do not accept numpy integers the Parachute will refuse Widening the height check to numbers.Real was the same mistake as accepting "banana": Parachute checks isinstance(trigger, (int, float)), so numpy.float64 passes because it subclasses float and numpy.int64 raises ValueError because it subclasses neither. Letting them through here only moved the failure to create_object. Back to (int, float), matching that check rather than improving on it, and the test that asserted numpy.int64 was accepted now asserts both ends refuse it. The asymmetry is Parachute's rather than this wrapper's and is worth fixing there, where widening the check would not strand anything downstream. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com> * TST: use the state length Flight actually passes The trigger tests built a 14-element state. Flight passes 13: x y z vx vy vz e0 e1 e2 e3 wx wy wz. Only y[5] is read, so both worked, but the test is there to document the contract and was documenting it wrongly. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com> * DOC: add the changelog entry for this branch By hand, because the automation cannot run on a pull request from a fork (#1101). No documented API breaks, but the observable behaviour does: an invalid string, an empty list or a boolean trigger now fails during StochasticParachute validation rather than later in Parachute construction, or silently becoming a one-metre height trigger. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com> --------- Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
# Conflicts: # CHANGELOG.md
BUG: give each CustomSampler its own stream instead of the model's seed Every CustomSampler on a model was reset with the model's own seed, so two samplers backed by default_rng started from the same state and drew the same underlying deviate. Each input now gets a stream keyed by its name, and samplers that share one generator declare a seed_group so the group is seeded once between them. Merged manually rather than through the button: the CHANGELOG conflicted with #1103, and the resolution keeps both entries. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* ENH: Add opening shock force estimation to Parachute class (#1050) Adds an opening_shock_coefficient parameter and a calculate_opening_shock_force method to estimate the peak transient force during parachute inflation, following the simplified model in Knacke's Parachute Recovery Systems Design Manual (1992, Section 5.5). Closes #161 Co-authored-by: ArthurJWH <167456467+ArthurJWH@users.noreply.github.com> * ENH: Moving opening shock force function to utilities * DOC: Updated the CHANGELOG * DOC: Removed cross-reference from previous Parachute method --------- Co-authored-by: Matanski <Matangrod@outlook.co.il>
Gui's point on the review. `python -O` strips an assert, and this is what keeps a non-sampler out of the model, so it has to be a raise. Same shape as #1103, which took the identical route for the parachute triggers. AssertionError is kept rather than swapped for TypeError, because the docstring on develop already documents it and a caller catching it should keep working. Two tests. One is the behaviour; the other runs a child interpreter under -O, since that is the mechanism and the plain test passes either way. Note this module carries thirteen more asserts on develop, none of them mine. Happy to send them separately if you want the same treatment there. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
`stochastic_model.py` has carried this as a TODO:
# TODO: Stop using assert in production code. Use exceptions instead.
The reason it matters is that the optimiser removes them, so the checks stop
running and malformed input reaches the model:
StochasticModel(obj, mass=("not a number", 0.5))
python AssertionError
python -O accepted, mass = ('not a number', 0.5, <Generator.normal>)
That model then fails somewhere later with something that does not point back
at the tuple that caused it.
Twenty one of them, all in stochastic/: thirteen in stochastic_model.py, three
each in stochastic_environment.py and stochastic_flight.py, one in
stochastic_aero_surfaces.py, and the one in stochastic_parachute.py that #1103
did not reach. Converted mechanically through the AST, so the condition and the
message are the ones that were there.
AssertionError is kept rather than swapped for TypeError or ValueError. The
docstrings document it, and callers catching it should keep working. Changing
the type is a separate decision from making the check run at all.
Two kinds of test. One reads each module and fails on a reintroduced `assert`,
which is the mechanism. The other runs a child interpreter under -O, because in
process the assert is still compiled in and the check would pass either way.
Also moves two imports in test_custom_sampler.py to the top of the file, which
pylint flags as C0415 on develop today.
Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
* ENH: model unbonded solid-motor grain CM shift (#340) * MNT: satisfy ruff format on the unbonded-grain changes Blank line before evaluate_geometry, wrap the propellant_I_11 assignment that ran past 88 columns, and let the expected_cm expression in the test break the way the formatter wants. No behaviour change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Gui-FernandesBR <63590233+Gui-FernandesBR@users.noreply.github.com> Co-authored-by: Gui-FernandesBR <guilherme_fernandes@usp.br> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…1161) * BUG: refuse to run a Monte Carlo over a results file it cannot write import_outputs() accepts .csv and .json, points output_file at the file, and offers continuing a simulation from it. simulate() only writes JSONL, and __setup_files opens with w+ when append is False, so the imported file was truncated and then filled with records its own extension does not describe. Checked before any file is opened, and named per path so the message says which one has to change. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com> * DOC: stop offering CSV and JSON results as something to resume from The note under import_outputs said any previously saved file could be used to continue a simulation, which is what led a .csv into output_file in the first place. Say which format that holds for. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com> * BUG: refuse working logs that are one file, and options that split a row import_results points input_file, output_file and error_file at one path, and a run then appends input rows and output rows into it. Compared by inode once the files exist, so a symlink, a hard link, a/../run.txt and a case-insensitive filesystem are all the same file rather than three names. json.dumps kwargs reach the writer, so indent=2 wrote records across several lines while every reader here takes one line at a time. The run finished and the completeness check then called the file it had just written damaged. indent of 0 and "" do the same, as does a newline inside separators. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com> * TST: cover the branch a first run actually takes samefile needs both files to exist, and every case here wrote one first, so the resolved-path fallback that a run with no logs yet goes through was never exercised. Replacing it with False leaves the new test red. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com> --------- Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
* ENH: list NOAA atmosphere datasets and fetch latest (#660) * MNT: satisfy ruff format on the NOAA catalog helpers Collapse the RuntimeError message and the test assertion the formatter wants on one line. No behaviour change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Gui-FernandesBR <guilherme_fernandes@usp.br> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* DOC: add SIL parachute ejection integration example (#524) * DOC: make the SIL mechanism table render as a table RST has no pipe-table syntax. A line starting with "|" is parsed as a line block, so the "Dual path: trigger vs controller callback" section rendered as a column of ragged lines with the pipes and the "---------" separator showing through, rather than as a table. Sphinx does not warn about this, which is why the docs job stayed green and it went unnoticed. Converted to a list-table, matching the two-column explanatory tables already used in docs/user/flight.rst (:header-rows: 1, :widths: 30 70). Verified by building the page with Sphinx 8.1.3 under -W --keep-going: the section now emits one <table class="docutils"> with a real header row, where before it emitted zero tables and one line-block. No new warnings. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Gui-FernandesBR <guilherme_fernandes@usp.br> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: accept numpy integer types as Parachute trigger isinstance(trigger, (int, float)) rejects numpy integer types (np.int64, np.int32) because they don't subclass Python's int or float. Replace with isinstance(trigger, numbers.Real) which covers all numeric types (int, float, numpy scalars) while excluding bool. Fixes #1106 * BUG: accept a NumPy integer height in stochastic/ as well This PR widened Parachute's height check from `(int, float)` to `numbers.Real`, so a height read out of a NumPy array is accepted. StochasticParachute validates the same triggers before a Parachute is ever built, and its copy of the check was still spelled `(int, float)`. A `numpy.int64` height was therefore still refused there, even though the Parachute it would have built accepts it. That mismatch is what turned all six Pytest legs red: `test_a_numpy_integer_is_refused_here_because_parachute_refuses_it` pinned the old asymmetry, and its own docstring said the fix belonged in Parachute. Rather than restate the predicate a second time, Parachute now exposes it as `_is_a_height_trigger` and stochastic/ calls that. The two spellings drifted apart once already; sharing one definition is what stops it happening again. Tests: - the NumPy integers move into `test_what_this_accepts_is_what_a_parachute_accepts`, where they now belong, joined by `numpy.float32` - the refusal test is rewritten around what is still refused by both: `numpy.bool_` and the complex types, none of which are `Real` - a new test asserts the agreement itself over the whole boundary, so changing one side alone fails with both verdicts printed - Parachute gains boundary tests of its own, including that `True` is still refused rather than read as a height of one metre Verified: reverting either side alone turns these red (the drift guard reports `np.float32(800.0): stochastic/ says False, Parachute says True`); ruff check and format clean; pylint 10.00/10; tests/unit 2098 passed, 16 skipped. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: abhi-0203 <abhi-0203@users.noreply.github.com> Co-authored-by: Gui-FernandesBR <guilherme_fernandes@usp.br> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
#1167 declared the eccentricities that add_cp_eccentricity and add_thrust_eccentricity install, so dict_generator draws them. _create_eccentricities then drew them a second time and overwrote the first value. The exported inputs matched the applied ones only because the second write wins, and the extra draw moved every component position create_object places after it. Read what has been drawn already, and draw only a half the caller left out, which is not a declared input and so never reaches dict_generator. The docstring correction is the explanation #1167 landed with, which named the wrong symptom: the value did vary between simulations, what a fixed seed failed to do was reproduce it. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
…pletion (#1166) * Fix spurious ValueError from floating-point roundoff at exact tank depletion * MNT: make _compose_clipped a real drop-in for compose, and format Retargeted this PR from master to develop, which is where RocketPy takes contributions. That also clears most of the lint failure by itself: master's own README.md currently fails `ruff format --check`, so the job was red for a reason outside this diff. Merged develop in as well, since the branch was cut from the v1.13.0 tag. Two changes: - `ruff format` on the two touched files. Two `_compose_clipped(...)` call sites were over 88 columns and the new test needed the surrounding blank lines. - `_compose_clipped` now defers to `outer.compose(inner)` unless both sides are array-sourced. Clipping reads `x_array` / `y_array` and the domain bounds, which only exist for array sources, so a callable source raised `AttributeError: 'Function' object has no attribute 'x_array'` where plain `compose` had worked. `Function.compose` already handles that case with a lambda and performs no bounds check there, so there is no spurious error to absorb and nothing to clip. Uses the public `is_array_source()`, as motors/motor.py and environment/ already do. Tests: three parametrized cases pinning the deferral (array/callable, callable/array, callable/callable) and one that the array/array path still pulls in a value a roundoff below the domain. Removing the guard turns them red with the AttributeError above. I also checked the thing that worried me most about clipping, since `UllageBasedTank.liquid_height` has no bounds check of its own and previously depended on `compose` raising: a gross overfill is still refused. Measured on develop and on this branch, an ullage 1.5x the tank volume and an ullage of -0.5x both raise the same ValueError from Tank's own overfill/underfill checks, which fire independently of the composition. So the clipping really does only absorb boundary noise. Verified: tests/unit 2161 passed, 17 skipped; tests/unit/motors plus tests/integration/motors 116 passed; ruff check and format clean; pylint 10.00/10. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: x <x@y> Co-authored-by: Gui-FernandesBR <guilherme_fernandes@usp.br> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…1180) * DOC: backfill the Unreleased changelog entries the automation missed The last commit to CHANGELOG.md is 5eae273 on 12 August, and the highest pull request it records is #1140. Thirty-nine commits have landed on develop since, and none of them is in the file, so [Unreleased] no longer describes the branch. Add the thirty-two that belong there, taken from the merge list and following the file's own rules: Added for new features, Changed for changes to existing behavior, Fixed for bug fixes, and tests left out. The seven TST commits in the gap are therefore not listed. Entries use the pull request title and link, with the issue linked alongside where the commit names one. One entry is a direct commit to develop with no pull request, so it links the commit instead. This does not fix the automation. #1173 covers that, and the two causes it identifies are both outside a pull request into develop: the pull_request_target workflow is read from the default branch, which still carries the older file, and RELEASE_TOKEN is resolving to empty. * DOC: follow changelog exclusion rules
Nine of the ten EnvironmentAnalysis tests carried @pytest.mark.slow, so test_pytest.yaml never ran them and the subsystem measured 7.0%, 39.7% and 19.4% in the run that gates every pull request. They were not slow because of the work they do. The env_analysis fixture reads two committed files under data/weather/, touches no network, and was declared at function scope, so the nine tests each rebuilt it and twenty years of hourly reanalysis data was parsed nine times. Roughly 212 of the 247 seconds the two files took were that repeated setup. Scope the fixture to the session and drop the nine marks. No test rebinds its attributes; test_exports already loads into a copy.deepcopy rather than into the fixture. Non-slow coverage goes from 84.2757% to 89.6674%, +945 statements, for about 74 seconds across the five steps.
* MNT: install contextily with the other optional requirements
contextily is declared in the monte-carlo extra in pyproject.toml but not
in requirements-optional.txt, and the Makefile's install target reads the
requirements files. test_monte_carlo_plots_background.py opens with
pytest.importorskip("contextily"), so anyone who sets up with `make
install` skips that file: 18 tests, and 53 statements that Codecov counts
as covered.
The workflow installs .[all], so CI already has it and is unaffected. What
this fixes is the local suite silently disagreeing with CI, with a skip
reason as the only clue.
Same specifier as pyproject.toml, including the 3.14 marker.
* TST: mock contextily tile fetches
rocket_plots.py sat at 73.9% and monte_carlo_plots.py at 63.1% because the fixtures never reached several drawing branches. For rocket_plots, add drawings for a rocket carrying one individual Fin rather than a fin set, one carrying a GenericSurface, one on a hybrid motor, one on a liquid motor, one on a ring cluster, and one whose nozzle sits behind its last aerodynamic surface in each coordinate system. Add the two validation refusals. For monte_carlo_plots, cover the ellipse branches that run when a results file is missing its apogee or its impact series, when it has neither, when the image path does not exist, and when a landing point and an image are drawn. Background-map fetching stays in test_monte_carlo_plots_background.py; everything added here runs with background=None and contacts no provider. No production code changes.
* BUG: correct the nozzle gyration tensor parallel axis term The lateral components of the nozzle gyration tensor carried a quarter of the squared nozzle offset. The exit disk second moment per unit area gives the full square, and letting the exit radius go to zero has to leave diag(d^2, d^2, 0). Flight.u_dot already uses the full squared distance for the same quantity. The docstring said the tensor is in kg*m^2. Since T05 = mdot * S - I_dot has to be kg*m^2/s and mdot is kg/s, S is in m^2. Three recorded values in tests/unit/simulation/test_flight.py move past their tolerance and are updated. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com> * TST: re-record the Defiance impact drift guard The nozzle gyration tensor correction moves the Defiance example's impact point about 17 m over a 1626 m range, which is outside the band the guard allows for y and inside it by 0.1 m for x. These two constants are recorded from the deterministic example rather than measured, so they track the model. The measured quantity in the same file is the apogee, and the correction moves the simulation closer to it: 0.709 percent error before, 0.693 percent after. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com> * TST: make the nozzle tensor tolerance mean what it says np.allclose defaults to rtol=1e-5, which for a value near 1.575 is about 157 times the 1e-7 the comment describes. Pass rtol=0 and carry one more digit of the recorded value, which leaves 61 times the margin rather than 3. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com> --------- Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
* ENH: plot flight center-of-pressure evolution (#954) * MNT: drop manual CHANGELOG edit (auto-updated after merge) * DOC: add center_of_pressure plot examples for review (#954) * DOC: drop the PR-review example images from the repository These two PNGs were committed only to illustrate the plot in the pull request description. Nothing in the docs or the code references them, so they added ~345 kB of binaries to the tree for no reader. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Gui-FernandesBR <guilherme.fernandes@maggu.ai> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* BUG: accept the seed type a Monte Carlo worker is handed A parallel run spawns a SeedSequence per worker and passes it to environment, rocket and flight. _sampler_seed then fed it to SeedSequence(entropy=...), which takes an int or a sequence of ints, so the first worker raised TypeError before drawing anything. The call was reached only from the custom sampler reset until #1117 added the list-choice generator, which every model goes through. A real two-worker run passes at d21abde^ in 2.32s and does not finish on develop: the worker's own error path raises UnboundLocalError on inputs_json, so the parent never learns it died and the run hangs. The children of one root share their entropy and differ by spawn_key, so the value is folded through generate_state rather than read off entropy, which would put every worker on one sampler stream. Nothing is consumed, and an int or None seed keeps the stream it had. The fold lives in rocketpy.tools, since the component streams and the per-index seeding both need the same one and three copies would drift on width and word order. _sampler_seed does its own final fold through it as well rather than repeating the four lines. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com> * DOC: say the seed helper derives rather than serializes Calling the result the int a SeedSequence can be rebuilt from reads as a round trip of the entropy and spawn key. It is neither: the helper derives a 128-bit seed and the state it came from cannot be read back out. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com> --------- Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
* chore: update official team logos (black and white) Replaces the current logos with the updated 2026 official versions to align with the team's new visual identity. * DOC: replace the logo artwork in place instead of adding new files The 2026 artwork landed under new names, so nothing rendered it: both docs/conf.py and README.md still pointed at RocketPy_Logo_black.png and RocketPy_Logo_white.png. Writing the new art to those same two paths updates every consumer at once and leaves no reference to fix. Renaming was the alternative and it is the worse one. README.md loads the logo over raw.githubusercontent.com URLs pinned to master, and PyPI renders that README for releases already published, whose text can no longer be edited. New filenames would leave those pages with a broken image for good. Two notes for whoever reads this later. RocketPy_Logo_black.png now holds the coloured variant rather than a black one, since that is the art meant for light backgrounds -- the name is kept for the URLs above, not for accuracy. And the new mark drops the rocket-and-globe glyph, so the aspect ratio goes from 3.30 to 4.39; the logo renders wider at the same height. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Gui-FernandesBR <guilherme.fernandes@maggu.ai> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
… case (#1175) * Fixed gravity accelerometer simulated gravity sign * TST: pin the accelerometer gravity sign to what the instrument does The one test that failed with the sign fix, test_noisy_rotated_accelerometer, recomputes the expression it is checking: it built its expected value with `Vector(U_DOT[3:6]) + Vector([0, 0, -GRAVITY])`, the same term being corrected in Accelerometer.measure. So it never evidenced either sign, it mirrored whichever one the implementation held, and it failed here only because the two copies had drifted apart. Its line is updated to match, with a note on what the quantity is. Mirroring it again would leave the convention untested, so this also adds test_accelerometer_at_rest_reads_gravity_upward, which states the physics instead of the formula: an accelerometer senses the support force holding it up, so at rest it reads +g along its up axis, and zero with consider_gravity off. It is exact rather than bounded, since every noise, bias and drift default is zero. Reverting the fix in measure() fails it. The consider_gravity docs said only that gravity was "considered", which is what let the sign go either way; they now say the sensor reports proper acceleration and what it reads at rest. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * MNT: drop the test artifacts that a local run left in the tree Running the unit suite from the repository root writes flight_calisto_robust.rpy and the three monte_carlo_test.* logs there, and none of them are ignored, so they were picked up by the previous commit. They are outputs, not sources. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Gui-FernandesBR <guilherme.fernandes@maggu.ai> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* BUG: reject live RNG objects as Sensor seeds `Sensor.__init__` passes the seed straight to `numpy.random.default_rng`, which also accepts `Generator` and `BitGenerator` objects. The sensor then constructs successfully and stores the object on `self._seed`, where `to_dict()` emits it verbatim, so the failure only surfaces later at `json.dumps()`, far from the call that caused it. #1124 closed the `SeedSequence` case in #1087 by teaching `RocketPyEncoder` to write one out. That works because a `SeedSequence` is defined by its entropy and spawn key, so it still describes the stream after a round trip. A `Generator` has no such description: its state advances on every draw, so whatever `to_dict()` wrote would depend on when it ran, and restoring it would not reproduce the stream the sensor actually used. Reject those two in the constructor instead, so the failure stays at the call that caused it. Ints, numpy ints, `SeedSequence` and `None` are untouched, as are the sequences of ints `default_rng` accepts and the encoder already serializes, so no seed that works today is rejected. Annotate `seed` on every constructor that takes one, with the type the issue itself names, so the contract is stated where the argument is declared rather than only in the docstring. * BUG: reject RandomState and other non-descriptor Sensor seeds default_rng also accepts RandomState from NumPy 2.2 on, and RocketPy pins no upper bound on numpy, so the previous isinstance list let it through to the same late TypeError at json.dumps() that #1087 reported. Check the stable half of the contract instead of enumerating the live types: accept ints, array_like of ints and SeedSequence, and refuse the rest. A seed kind numpy starts accepting later is now refused at construction rather than reaching serialization. Widen the annotation to the array_like integer contract the check actually takes. It goes through a SeedLike union so the seven signatures stay inside the line limit while help() and inspect.signature() still expand the members. * TST: compare the noise stream across a seed round trip The existing round-trip tests assert on the stored seed value, which would still pass for a seed that survives JSON without naming the stream the original sensor used. Draw from the restored sensor instead and compare it against a fresh one built from the same seed, across the four descriptor kinds the constructor accepts. * BUG: refuse the seed shapes SeedSequence cannot take either The seed check accepted a nested sequence of ints, on the stated grounds that "numpy accepts as entropy just the same". It does not: SeedSequence raises TypeError for a nested sequence and ValueError for an array of two or more dimensions. Older NumPy let the nested form through, which is why this passed under Python 3.10 and failed under 3.14, where a newer NumPy is resolved -- the acceptance was never portable, and RocketPy sets no upper bound on the dependency. So _is_int_array_like now takes a flat sequence only, and an ndarray only at ndim <= 1. Both refusals name the seed, where the NumPy messages they replace name neither it nor the argument that carried it. The nested case moves out of test_int_array_like_seeds_are_accepted and into a rejection test alongside a 2-D array, which the check would have admitted for the same reason. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Gui-FernandesBR <guilherme.fernandes@maggu.ai> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* BUG: give each StochasticRocket component its own random stream _set_stochastic handed the same seed to the rocket body and to every surface, motor, rail button and parachute, so two components built from one spec drew identical values: a main and a drogue with the same cd_s and lag spec drew the same cd_s and the same lag, every time, and a study of both was a study of one counted twice. Air brakes were worse. They are built and sampled in create_object and were not in the reseed at all, so their values came from wherever the generator had been left rather than from the seed: 0.683, then 0.586, then 0.488 for one seed asked three times. Each component now takes its own child of a SeedSequence root, spawned in a fixed order so one seed still reproduces the whole rocket. The collections are named in one place and checked against create_object's own source, since the collection no fixture populates is the one that gets missed. Extracted from #1054. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com> * DOC: shorten the comments on the component seeding Measured against the register the repository uses: inline comments in flight.py average 5.6 words and none of its docstrings run longer than the code they describe. The three added here were four to seven lines of prose where a line would do, and the seed helper carried seven lines of docstring over two lines of code. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com> * BUG: leave the rocket body's stream where it was, and isolate each collection Moving the body to child zero broke every fixed-seed baseline for mass, radius and the body inputs, and nothing about the nested-component fix needed that. The body keeps the seed as given now: stochastic_calisto under seed 42 reads mass=14.906007947 on develop and the same here. Components were also addressed by one global traversal index, so adding a fin moved every motor, rail button, parachute and air brake. Each collection has a root of its own now, spawned from the same seed, so an unrelated component in one of them leaves the others where they were. The source scan compares the two sets both ways. A collection left in the reseed after create_object stops using it still spawns a child and moves every stream after it, which the subset check let through. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com> * TST: count the reseeds, and cover two air brakes on one spec The source scan reads create_object for a literal loop over self.collection, so a helper, a local alias or a getattr would hide a collection from it. Counting what each entry actually receives is the check that survives a refactor, and it is the only one that fails when an entry is reseeded twice. The air brakes are a plain list and take a different route through the reseed than the positioned collections, so two of them on one spec are worth their own case. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com> * TST: stop the isolation test from storing one wrapper twice stochastic_calisto already holds the stochastic_nose_cone fixture, so adding it again put the test into the state #1172 describes: one wrapper in two entries, its position overwritten, and two reseeds landing on the same object. The assertion looked at a different collection and passed anyway. It adds the deterministic nose now, so add_nose builds a wrapper of its own. Nothing pinned the body keeping the seed as given either. Reproducibility and seed uniqueness both hold with the body on a spawned child, so neither would have noticed it going back there. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com> * DOC: say how a rocket's components are seeded The change moves every fixed-seed component baseline and nothing in the user documentation said how components are seeded at all, before or after. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com> * DOC: say what separate streams do and do not promise Two independent streams are not made to consume the same draws; they can still land on equal values, and a specification with no spread always will. The text promised unequal results, which is a stronger claim than spawning gives. It also said each kind of component is spawned separately. The unit is the collection: a nose cone, the fins and the tail share one root. And a stream belongs to one wrapper, so storing one twice or sharing it between rockets is outside what this establishes. That is #1172. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com> * DOC: say that the reset builds the tree, not the add A rocket resets itself while being constructed, when it holds no components yet, so a parachute added afterwards keeps the generator it was built with until the next reset. The text read as though attaching a component gave it a stream, which is only true once something resets the rocket, and a Monte Carlo is what does that. Two wrappers sharing a CustomSampler seed_group are also one stream on purpose. Separate component streams are not meant to take that apart, so the note says so rather than leaving it to be discovered. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com> * DOC: correct what Monte Carlo and a shared sampler group actually do A serial MonteCarlo run never resets the rocket, and a parallel one resets each worker once rather than once per simulation, so the text saying a run resets the rocket for you was wrong for both. Per-simulation reset is the Monte Carlo seeding work, not this change. CustomSampler.seed_group already documents that a group belongs to one model and that the last to seed it wins. Saying two components sharing one stay one stream on purpose read as a guarantee this does not make. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com> * DOC: the parallel path does not get as far as building the tree Saying it resets each worker once reads as though it works and only the grain differs. It hands the model a SeedSequence where an integer is wanted, so it stops before the tree exists, which the PR already records as the Monte Carlo seeding work rather than this change. The two air brake test also says what it is not: both are added with one controller because the rocket keeps a single one, which is #1172. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com> * TST: pin every word of the seed, and say what append-only means Dropping the fourth word left all 33 tools tests passing: the checks were that the high bits are not zero, that the low word matches, that two children differ and that reading twice agrees, none of which a 96 bit truncation breaks. It compares against the integer rebuilt from all four words now, and that mutation fails. A collection's stream is addressed by where its name falls in the two tuples read end to end, so appending to the first moves every name in the second. The comment said append rather than reorder, which reads as though appending to either one is safe. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com> * BUG: take the seed type a parallel run hands the rocket A parallel run spawns a SeedSequence per worker and passes it down, and SeedSequence does not take another one as entropy, so rooting the collections from it raised TypeError. It was unreachable until now: the base _set_stochastic refuses the same type one frame earlier, so a worker never got this far. Once that is fixed the call here is the next one to fail, which is why it is fixed in the same series rather than left for whoever hits it. Copied from the full state rather than spawned from directly. spawn() advances the counter of an object the caller still holds, and a second use of the same seed would then build the components a different tree. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com> * MNT: drop the _seed_sequence_to_int this branch no longer needs to add #1181 landed the same helper on develop, with the same body, so merging develop in left tools.py defining it twice a hundred lines apart. Git had no conflict to report: the two copies were added at different points in the file, so the second simply shadowed the first, and pylint would have failed the branch with E0102 rather than anything explaining why. Develop's copy is kept, its docstring being the fuller of the two. What this branch still adds on its own is _seed_sequence_from, which is left where it was, and the test in test_tools.py now covers the surviving definition. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com> Co-authored-by: Gui-FernandesBR <guilherme.fernandes@maggu.ai> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* BUG: sample around the nominal a stochastic model was built with _set_stochastic re-validates every declared input, and validation reads the nominal off the wrapped object. create_object writes the sampled value back onto that same object on purpose, so re-reading it on a reseed took one simulation's output as the next one's nominal: a wind factor compounded 10 -> 8.576 -> 7.355 -> 6.308 under a single fixed seed, and a plain scalar spec drifted the same way. Read the nominal once and keep it. Containers are copied on the way in, so writing through the wrapped object cannot reach it either. A component position arrives through an injected getter, reads an attribute nothing writes back to, and shares one name across every component, so those are read live rather than cached. Extracted from #1054. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com> * BUG: keep the nominal out of reach of what is generated from it Copying on the way in was not enough. _nominal handed back the kept object itself, and on the empty-spec path that one object reached the model attribute, last_rnd_dict and the FreeFormFins create_object returns, so a write through any of them moved what the next reseed sampled around. _snapshot_of stopped at a tuple as well, which left an array inside an airfoil pair shared with the object it came from. Copy on the way out too, and recurse through the built-in containers. The documented contract now names the four cases that stay outside it: an input added after construction, a component position, an ensemble wind factor, and anything that is not an array or a built-in container. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com> * TST: pin when a late input is captured, and the spread-tuple path An add_* input is configured after __init__, so its nominal is read then. The new test writes the rocket's eccentricity before add_cp_eccentricity and again after it, and only the first one may reach the draw. The (std, distribution) form now runs its own seed histories rather than repeating one seed, which is what a cache keyed by the seed instead of by the model actually fails. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com> * TST: pin the Function nominal as a boundary, not a footnote The documented exception said a Function is held as it was given. Nothing enforced it, so closing the hole later would have gone unnoticed and the documentation would have quietly become wrong. Measured: set_source on the rocket's drag curve moves the drawn value from 0.377 to 0.890, and deepcopy of that curve costs 6 microseconds. Cost is not the reason to leave it. _snapshot_of cannot raise today, and deepcopying whatever a user passed, on a path that runs on every reseed, would make it able to. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com> * BUG: read the nominal again when a late input is configured again Keeping the nominal gave the second add_cp_eccentricity nothing to replace, so it went on sampling around the value the rocket held at the first call: 0.5 where 0.8 was asked for. Reproducible, and around the wrong centre, which is harder to notice than a value that moves. Late configuration drops the kept nominal before validation reads one, and puts it back if validation raises, so a refused call leaves the previous configuration standing. Only the reconfiguration path. Passing None still leaves the earlier declaration in place, which is develop's behaviour and not this branch's. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com> * DOC: say what the snapshot does not do Measured on the current implementation: a cycle recurses until Python stops it, two references to one list come back as two lists, and the elements of an object-dtype array stay shared. None of those reach a supported nominal, but the docstring read like a general deep copy and should not. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com> * BUG: take a late input away when it is configured to None None is a configuration too. The nominal was refreshed but the earlier distribution stayed declared, so the next reseed validated it again and drew an uncertainty the caller had asked to remove. Filed as #1171 while the removal lived elsewhere; it belongs in the replacement helper this branch added, so it is here rather than in a second PR that owns the other half of one state transition. None still means an axis that was never given, and removing what was never declared stays a no-op. Both meanings have a test. The snapshot test asserted a dict entry was not None, which held whether or not anything had been copied, and no test reached the set branch at all. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com> * BUG: replace a pair of late inputs together or not at all add_cp_eccentricity takes x and y in one call, so a y that will not validate left x already replaced and declared. Validation happens for the whole group before anything is committed now. The test gives only y first, so x is undeclared going in and a partial commit shows up as an eccentricity the caller never successfully asked for. Asserting the nominal alone did not catch it, since x's nominal was restored either way. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com> * BUG: give every draw its own copy of a mutable value Copying on the way out of the kept nominal was not the last boundary. The list branch handed back the candidate itself, which for an empty spec is the model's own working value, and FreeFormFins keeps shape_points by reference. Writing through the first generated fins reached the second ones: first = stochastic.create_object() first.shape_points[1] = (9.9, 9.9) second = stochastic.create_object() # (9.9, 9.9) as well No reseed in between, which is how create_object is documented to be used and how a serial Monte Carlo runs it. last_rnd_dict was the same dictionary the values were built from, so it moved with them too. It records what was drawn now, which matters because a Monte Carlo writes it out after the flight rather than before. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com> * BUG: record a draw after the subclass that adjusts it, not before Recording in the base generator put the record before StochasticFreeFormFins had pulled the fin root back onto the body line. Under seed 7 the two root points drifted to 0.000299 and 0.001340, the correction returned them to zero, and the record kept the outline the fins were never built from. The rocket copies each component's record into its own, so the Monte Carlo input log carried it too. That is the failure class #1090 was about, arriving from the other side. _record_draw is the one place a model publishes what it drew, and a subclass that changes a value calls it again. A source scan holds the next subclass to the same rule, since the one that gets it wrong is the one nobody wrote a fixture for. _declare_stochastic_input and the _MISSING sentinel had no callers left after the grouped reconfiguration landed, and the first still carried the None handling that #1171 was about, so they are gone rather than left as a second lifecycle for someone to reach for. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com> * DOC: _choose returns a copy, and say so where it is documented The docstring still promised values itself when there are no candidates, which stopped being true when the draw started handing back a copy. Nothing reaches that branch through a validated input, since an empty list validates to the object's own value, so it is a guard against integers(0) rather than a path with a caller. It has a test now, which is also the one line of this change Codecov had no coverage for. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com> * BUG: record the parachute noise seed the parachute was built with StochasticParachute.create_object derives the pressure noise seed after the draw, and #1134 relied on last_rnd_dict being the same dictionary to carry it into the record. Snapshotting the draw broke that link: the parachute is still built with the seed, but the record loses it, so the Monte Carlo inputs stop describing the parachute that flew. develop recorded 37773913418288439290323614982376424810 before recorded <absent> The source scan missed it because it only read dict_generator overrides. It reads create_object too now, and tracks the names a method binds from a draw rather than guessing at a variable name, so a local a method fills in for its own use is not mistaken for a record. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com> * TST: walk the subclass tree, not the package exports StochasticMotorModel is a StochasticModel subclass that rocketpy.stochastic does not export, so the scan could not see it. It overrides neither method today, which is why nothing was wrong, and which is also why the gap would have gone unnoticed until something did. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com> * BUG: stop an omitted axis from taking away what was declared Removing on None looked like one line inside the new replacement helper, and it is not. add_cp_eccentricity(x=..., y=...) defaults both to None, so an omitted axis and an explicit None read identically, and the removal took away an axis the caller never mentioned: add_cp_eccentricity(x=0.001, y=0.002) add_cp_eccentricity(x=0.005) # y quietly gone develop keeps y here, and so does this again. Removing an earlier declaration needs an argument omission cannot supply, which is a signature change and its own decision, so it stays in #1171 rather than arriving inside a change about nominal ownership. The test that asked for removal is replaced by one that holds the omitted axis in place, since that is the behaviour anything already written depends on. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com> * DOC: say what a second add_cp_eccentricity call does Both arguments read as optional and nothing said what happens when the method is called again, which is the whole of the question behind #1171. Each public docstring now states it: a later call replaces what was configured, an omitted axis keeps what it had, and taking one away is not supported. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com> * BUG: leave an axis that was left out entirely alone Keeping its declaration was not enough. The omitted axis still went through the whole replacement: its kept nominal was dropped, None was validated again into a lone nominal, and that was written back over its distribution. The private side then said the axis was random while the attribute dict_generator reads said it was not, so it stopped varying: add_cp_eccentricity(x=0.001, y=0.002) add_cp_eccentricity(x=0.005) eight draws of y -> one distinct value A serial Monte Carlo never resets, so a whole study would have run with that axis switched off and nothing raised. Dropping the nominal also moved the centre. With the rocket's own y changed between the two calls, the next reset centred the old distribution on 9.0 rather than the 0.0 it was configured around. An axis given as None that already has a configuration is now left out of the transaction: not revalidated, its nominal not re-read, its attribute not rewritten. The test covers both eccentricity methods and looks before the reset as well as after, which is where the previous one missed it. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com> * DOC: say what a snapshot does not reach inside an object array ndarray.copy() copies an object array without copying its entries, so a later write through one of them is still visible. The scope was numeric arrays already; this says so. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com> --------- Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
* MNT: store ref_factor on rocket aero surface components (#561) * BUG: read back the component entries written before ref_factor existed Rocket.from_dict unpacked three fields from every serialized entry, so a .rpy file written before ref_factor was stored failed to load with "not enough values to unpack (expected 3, got 2)" -- which is what the committed fixture tests/fixtures/utilities/flight_calisto_robust.rpy is, and what every file a user already has saved is too. Regenerating the fixture would have turned the suite green while leaving those files unreadable, so the three loops take a trailing star instead and read either length. Nothing is lost by ignoring the stored factor: add_surfaces derives it again from the surface's own radius. Two tests that reached develop after this branch opened, in #1169 and #1170, iterate Components expecting pairs, and now get triples. Both take the star as well, so a later field does not break them again. Also fixes what CI would have failed on regardless of the above: pylint C0415 for the two rocketpy imports inside test functions, now at the top of the module, and one ruff formatting difference in components.py. Simulation results are unchanged -- the 18 acceptance tests pass, and the factor is the same number as before, computed once when the surface is added rather than at every lift evaluation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Gui-FernandesBR <guilherme.fernandes@maggu.ai> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Six ways a worker that failed got away without saying so. It died inside its own handler on an unbound name, so the manager lock it was holding was never given back and the run waited for good. A worker that was killed ran no handler, set no event, and left only an exit code nobody read, so simulate() returned normally with rows missing. Reporting a failure could itself block on a lock a dead sibling still held. Both names are bound before the try now, the handler reports through a bounded lock and releases it from a finally, and the parent reads exit codes and the failure event instead of joining unbounded. A run that is only slow is still never cut short: how a worker ended decides that, not how long it took. An exit code cannot show a worker that left between claiming an index and recording it, so a run is checked against its own logs at the end. Both must hold exactly the simulations asked for, none twice, none unreadable. Scope is the parallel producer. __run_in_serial has the same unbound name and belongs to #1177. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
…1177) * BUG: re-raise KeyboardInterrupt after an interrupted Monte Carlo run simulate() returned normally after Ctrl-C. Both execution modes caught the interrupt and neither re-raised it, so simulate() went on to __terminate_simulation() and returned exactly as it does after a complete study. A caller could not tell a partial run from a finished one without opening the output file and counting rows. Five more holes sat next to that one, found in review and in the interrupt-point audit it prompted: - __run_in_serial bound inputs_json inside the loop body, so Ctrl-C during the first keep_simulating() call reached the handler with nothing bound and the run died with UnboundLocalError from inside the cleanup. - It also never cleared inputs_json after a successful append, so Ctrl-C landing in the progress print — or in the next keep_simulating() call — wrote the row that had just committed into the error file as though it never finished. - _append_simulation_record rolled back on Exception, and KeyboardInterrupt does not derive from Exception, so Ctrl-C between the two appends left a one-sided record. - Its rollback also only truncated the inputs file, so an interrupt inside either write left a torn partial row for the reload to die on with JSONDecodeError instead of the interrupt. - The parallel cleanup began only after every worker had started, so Ctrl-C during the startup loop left the already-started workers running with nobody signalling or joining them. Bind inputs_json before the loop and clear it after each committed append, roll both files back on BaseException best-effort, cover the startup loop with the same cleanup path over a started-workers list, and re-raise in both handlers. Catch the interrupt in simulate() so __terminate_simulation() still runs before it leaves: it reloads the logs through the file setters, and set_num_of_loaded_sims is what the documented append=True continuation reads. The shutdown join stays unbounded, and the docstring now says so: the interrupt propagates once every worker finishes the simulation it is in and exits on its own. A worker stuck inside one simulation blocks the interrupt as it already blocked the run; killing it here could tear a half-written row into logs it holds the mutex for, and the bounded fleet shutdown belongs to #1054. The ordinary exception path is unchanged. Add ten regression tests covering both modes, the early interrupt, the preserved rows, the reload, an interrupted run continued with append=True, the between-appends rollback, the torn-write rollback, the committed row staying out of the error file, and the startup-loop cleanup. They run the real __terminate_simulation and assert the state it produces; all fail on develop, and each fix was also reverted individually with only its own test failing. * BUG: preserve interrupted Monte Carlo cleanup --------- Co-authored-by: Gui-FernandesBR <guilherme_fernandes@usp.br>
* ENH: cache downloaded atmosphere netCDF datasets (#654) * MNT: satisfy ruff format on the atmosphere cache changes Collapse the cache-key assignment the formatter wants on one line. No behaviour change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * BUG: make an atmosphere cache hit reproduce a fresh fetch The cache restored only the eight profile Functions, elevation and _max_expected_height, so every attribute Environment derives from the dataset was missing on the second run of an otherwise identical script. atmospheric_model_type was still set to "Forecast", so info() and all_info() walked into the Forecast branch of the prints and raised AttributeError on atmospheric_model_init_date, and to_dict() serialized those fields as None. Persist the full metadata (date range, grid bounds, and the raw interpolation inputs) alongside the profiles and reinstate it on a hit, bumping the cache format to v2. Saving also indexed temperature and both wind profiles as 2-D arrays while guarding only pressure, which raised IndexError for a constant wind and broke three existing tests. Guarding pressure alone is not sufficient either: np.asarray(None, dtype=float) yields nan, so a missing column would be written out as an entry full of NaN winds. Check every column against the pressure grid before writing. Also: - Expire forecast entries after ROCKETPY_CACHE_TTL seconds (default 6h, the GFS cycle). A launch date days out would otherwise pin the first forecast ever downloaded for it and silently reuse it forever. Reanalysis is immutable and never expires; a TTL of 0 disables expiry. - Let ROCKETPY_CACHE=0/off/false/no/none/disabled turn the cache off. It previously only relocated the directory, so there was no way to opt out globally, and an empty value cached into the working directory. - Include the variable dictionary and pressure conversion factor in the cache key, so the same source decoded two ways no longer collides. - Add clear_atmosphere_cache() to remove the entries. - Treat a damaged cache file as a miss rather than letting netCDF4's RuntimeError escape and take down the simulation with it. - Pass usedforsecurity=False to hashlib.md5 so the key still builds under a FIPS-enabled Python. - Isolate ROCKETPY_CACHE per test via an autouse fixture. The suite was writing to the real ~/.rocketpy_cache, which let one test read profiles cached by an earlier one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * MNT: drop four pylint suppressions that no longer suppress anything The lint job installs pylint unpinned and has not run green on develop since 2026-07-19. Pylint 4.x reports these four disables as useless-suppression, which fails the job with exit code 8 on every open PR regardless of what the PR changes -- two of them already appear in this PR's first CI run from 2026-08-15. These files are untouched by the caching work; this commit only removes the stale comments so the lint job can go green. It is separate so it can be dropped and landed on its own if maintainers prefer. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * MNT: replace the atmosphere cache's broad excepts with named error tuples The five `except Exception` handlers swallowed anything, so a NameError or any other real defect in this module would have been reported to the user as an innocuous cache miss and a re-download. Probed netCDF4 1.7.4 for what it actually raises in these paths: a damaged, empty, truncated, missing or unwritable file gives OSError (or one of its subclasses); a missing variable KeyError; a missing or non-numeric attribute AttributeError; a wrong-length array or undeclared dimension ValueError; an invalid dtype or attribute type TypeError; and touching a closed dataset RuntimeError. RuntimeError is also kept on the open path because netCDF4 surfaces some HDF5-level failures that way and the installed version is not pinned. Those become two documented module constants: CACHE_OPEN_ERRORS for _open_valid_cache, whose only risky call is the open itself, and CACHE_FILE_ERRORS for the read and write bodies, which also touch the file's contents. Tests pin both halves of the contract: each of the six error types still degrades to a cache miss, while an unexpected exception now propagates from the open path and from the read path instead of being silenced. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Gui-FernandesBR <guilherme_fernandes@usp.br> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* ENH: add Folium interactive flight trajectory map (#963) * ENH: enrich Folium trajectory map with the EuRoC-Dev map tooling Carries over the conventions from the team's internal rocketfolium module (EuRoC-Dev) into Flight.plots.trajectory_on_map, and fills the gaps that kept PR #1132 in draft. Map rendering: - Add OpenStreetMap and Esri World Imagery background layers behind a LayerControl. Satellite imagery is what actually answers the recovery question (terrain, tree lines, water), which plain OSM cannot show. - Mark the apogee ground position, labelled with apogee AGL, between the launch and landing markers. Skipped when apogee was never detected, since Flight.apogee_time then keeps its initial value of zero. - Add optional range safety circles around the launch pad, in their own feature group so the layer control can toggle them. The initial viewport widens to contain them, otherwise the largest ring would open off screen and the parameter would be useless. - Add an optional overlay title. The text is HTML-escaped before being injected into the map root. API: - Add time_step, for parity with Flight.export_kml: the ground track is resampled by linear interpolation instead of drawing every integration step, which keeps the HTML small for long flights. - Add color, so the track can be recoloured without post-processing. Docs: - Document the maps extra in installation.rst, next to the animation extra it mirrors. - Add an "Interactive Trajectory Map" section to the Flight user guide, with the parameter table and a cross-reference to export_kml for the 3D case that a 2D map cannot cover. Verified: 10/10 unit tests pass (including the real-folium HTML export), ruff clean, pylint 10.00/10, and sphinx-build -W builds with zero warnings. The rendered map was checked in a browser. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * DOC: illustrate trajectory_on_map with rendered map figures Pad the fitted viewport by 30 px so the launch and landing pins, anchored at the very edge of the bounding box, are no longer clipped by the map border. Caught while rendering the screenshots below. Add two figures to the Flight user guide, captured from the real output: the ground track with the launch, apogee and landing markers over satellite imagery, and the safety_radii circles framed around the pad. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Gui-FernandesBR <guilherme_fernandes@usp.br> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…s) (#920) * "ENH: Implement 3-DOF Single Rail Button Flight Phase (Tip-off Analysis)" Refs #28. This commit was made as a submission to the selective process deliverables challenge. The method udot_rail2 functions as an intermediate flight phase before the rocket has fully left the guide rail, allowing for 3 degrees of freedom (linear motion along the rail, pitch and yaw). Flight init includes a feature to run a simulation without udot_rail2. Numerical values enabling udot_rail2 are very close to 1 DOF flight. Flight phase transitions smoothly from 1 DOF rail phase to 3DOF and from 3 DOF to 6 DOF free flight. Current equations of motion inside udot_rail2 rely heavily on udot_generalized, ensuring 3 DOF through vector operations. Still working on the implementation of proper lagrangean expansion /derivation of equations of motion. Articles "Tip-off effect analysis of a vehicle moving along an inclined guideway by considering dynamic interactions" by Chou et al and "ANALYSIS OF MISSILE LAUNCHERS PART Q Tipoff Effects in Helical Rail Launchers" by Hosken et al are proving useful. --Summary-- Add preliminary udot_rail2 (3-DOF tip-off) support and safe, deterministic phase-insertion handling during rail → 6DOF transitions. Add a feature flag to enable/disable udot_rail2 on Flight init. Add a Hermite-root fallback to avoid hard failures when rail-exit root filtering returns no valid root (warn + midpoint fallback). Add comprehensive unit tests (alignment, no-roll, insertion-order, CSV comparisons) and sample CSV output for comparison runs with udot_rail2 enabled vs disabled. * ENH: derive constrained equations of motion for udot_rail2 tip-off phase Complete the 3-DOF single-rail-button (tip-off) phase from issue #28. - Fix the phase transition ordering: rail1 -> udot_rail2 (at effective_1rl, upper button exit) -> u_dot_generalized (at effective_2rl, lower button exit). Previously the thresholds were swapped, so udot_rail2 was inserted after free flight and never exited. - Replace the placeholder udot_rail2 (which reused free-flight dynamics with an ad-hoc velocity projection) with rigorous constrained dynamics: the lower button slides along the fixed rail while roll is suppressed. The reaction wrench (normal force + roll moment) is solved from a 3x3 linear system so the button's perpendicular acceleration and the roll acceleration vanish, derived in the true body frame on top of the validated u_dot_generalized solution. - Make the feature opt-in (use_udot_rail2 defaults to False); disabled runs are bit-for-bit identical to previous behavior. - Factor the rail-exit root finding into a shared helper. - Rewrite the unit tests to check phase ordering, the opt-in default, the on-rail constraint (button stays on the rail to machine precision), zero roll, and the gravity tip-off direction. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * MNT: address the udot_rail2 review comments - Define the rail axis (`attitude_unit`) for every Flight, not only the ones that start on the rail. It depends solely on the launch inclination and heading, so `udot_rail2` no longer raises `AttributeError` when an `initial_solution` skips the rail phase. Verified equal to the previous quaternion-derived vector to 3e-16 across inclinations, headings and rolls. - Compute the squared distance from the launch point once and share it between the two rail button exit checks. - Rename `r_B` -> `r_button` and `I_CM_inv` -> `inv_inertia_cm`, drop the unused unpacking in `udot_rail2` and the now-dead `K_init`, so pylint is clean without relaxing `.pylintrc`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * DOC: document the tip-off equations of motion The udot_rail2 docstring pointed at a derivation that lived in an untracked scratch file, so the reference was dead for anyone reading the code. Move the derivation into the technical documentation, where the other equations of motion are documented, and cite the two tip-off papers it follows. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * BUG: record t_initial when a flight continues from another Flight Fold the two initial-solution branches of __init_flight_state into one, as the review asked: they set the same monitors, and the Flight-object branch differed only by *not* assigning t_initial. That omission raised `AttributeError: 'Flight' object has no attribute 't_initial'` whenever the continued rocket carried sensors or controllers, since post-processing the initial state reads it. The bug predates this branch; merging the branches fixes it. Covered by a regression test. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * MNT: address the outstanding review points on the tip-off phase Picks up the four unresolved review threads on #920, on top of develop. Blocking: * Move ``use_udot_rail2`` to the end of ``Flight.__init__``. It sat between ``equations_of_motion`` and ``ode_solver``, so any caller passing ``ode_solver``, ``simulation_mode`` or ``post_step_callback`` positionally silently got the wrong value. The docstring entry moves with it. Correctness: * Refuse ``use_udot_rail2=True`` together with ``simulation_mode="3 DOF"`` or ``equations_of_motion="solid_propulsion"``. The phase patches the generalized 6-DOF solution with a constraint wrench built from the full inertia tensor, but those options rebind ``u_dot_generalized`` to reduced formulations that do not carry that state -- ``u_dot_generalized_3dof`` models no attitude at all. The combination was neither guarded nor tested; it now raises ValueError. Note this also covers point-mass motors, which force "3 DOF". * Give ``use_udot_rail2`` a class-level default. Flights restored from a ``.rpy`` written before this feature are rebuilt without ``__init__``, so reading the flag raised AttributeError (caught by test_load_from_rpy). Reporting: * ``between_rails_time`` and ``between_rails_state`` were tracked and serialized, but never surfaced: studying tip-off for dispersion meant digging them out of the raw solution. Adds ``between_rails_velocity`` and ``tip_off_duration``, a "Tip-Off State" section in ``Flight.info()``, and shading of the window in the attitude plots. All of it is inert when the phase is off, so existing output is unchanged. Performance: * ``udot_rail2`` re-interpolated the total mass and the inertia tensor that ``u_dot_generalized`` had just computed for the same t, on every solver evaluation inside the window. The generalized equations now expose both. Verified to leave the trajectory bit-for-bit identical. Adds eight tests covering the guard, the reporting, and the restored-object path. Documents the mode restrictions and the reported attributes in the technical docs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Gui-FernandesBR <guilherme_fernandes@usp.br> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #1194 +/- ##
==========================================
+ Coverage 82.36% 91.51% +9.14%
==========================================
Files 122 132 +10
Lines 16377 18305 +1928
==========================================
+ Hits 13489 16751 +3262
+ Misses 2888 1554 -1334 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
It seems like we already have enough content to prepare a new release!
PRs #1187 and #1190 will also be included in this release.
All other PRs must wait for v1.15 or higher versions.