Skip to content

Commit b22964e

Browse files
MNT: final fixes before next release
Documentation, CHANGELOG and test polish ahead of the next release (v1.13.0), covering PRs merged since v1.12.0. CHANGELOG - Add missing entries: Individual Fins (#818), AIGFS/HRRR forecast models (#951), duplicate-controller fix (#949), Valkyrie flight example (#967). - Hygiene: de-duplicate #958/#966, move #974 to Fixed and #1041 to Removed only, drop the already-released #914 duplicate, and point the logging (#973) and ND-interp (#969) entries at their PRs. - Backfill #940/#941/#944 (shipped in v1.12.0 code but never logged) under the [v1.12.0] section. Docs - New exceptions reference page (rocketpy.exceptions) wired into the reference index; note UnstableRocketWarning in the rocket stability docs (#970). - tanks.rst: switch examples to radius_function= and add a deprecation note (#957). - forecast.rst: fix the HRRR example (missing code directive + stray sentence) (#951). - airbrakes.rst: document discrete vs continuous controllers (sampling_rate=None) (#946). - rocket_usage.rst: note that Parachute is now an abstract base; instantiate HemisphericalParachute (#958). Tests - New regression tests: 3D ND-interp NaN outside convex hull (#969), abstract Parachute cannot be instantiated (#958), Monte Carlo convergence stopping (#922), EnvironmentAnalysis surviving wind API (#1041), radial-burn grain geometry over time (#944), RingClusterMotor full flight (#924), discrete controller invoked once per node (#949), acceleration-based parachute trigger deploys (#911). - Backfill regression tests for ThrustCurve API timeouts (#940) and power_off/on_drag Function objects + _input attributes (#941). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent f5028d0 commit b22964e

16 files changed

Lines changed: 509 additions & 30 deletions

File tree

CHANGELOG.md

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -32,27 +32,25 @@ Attention: The newest changes should be on top -->
3232

3333
### Added
3434

35-
- ENH: MNT: refactor parachute implementation [#958](https://github.com/RocketPy-Team/RocketPy/pull/958)
35+
- ENH: Individual Fins: add `Fin`, `TrapezoidalFin`, `EllipticalFin`, and `FreeFormFin` classes for modeling asymmetric or individually-positioned fins [#818](https://github.com/RocketPy-Team/RocketPy/pull/818)
36+
- ENH: Add AIGFS and HRRR forecast models to the Environment class [#951](https://github.com/RocketPy-Team/RocketPy/pull/951)
37+
- ENH: Add RingClusterMotor for annular clustered motor modeling [#924](https://github.com/RocketPy-Team/RocketPy/pull/924)
38+
- ENH: Discrete and Continuous Controllers [#946](https://github.com/RocketPy-Team/RocketPy/pull/946)
3639
- ENH: Add custom exceptions and unstable rocket warning [#970](https://github.com/RocketPy-Team/RocketPy/pull/970)
37-
- ENH: MNT: Remove redundant wind_heading/wind_direction functions from EnvironmentAnalysis [#1041](https://github.com/RocketPy-Team/RocketPy/pull/1041)
40+
- ENH: Adopt built-in Python logging instead of print() calls (closes [#450](https://github.com/RocketPy-Team/RocketPy/issues/450)) [#973](https://github.com/RocketPy-Team/RocketPy/pull/973)
3841
- ENH: Pass acceleration (`u_dot`) data to parachute trigger functions [#911](https://github.com/RocketPy-Team/RocketPy/pull/911)
3942
- ENH: Add 3D flight trajectory and attitude animations in Flight plots layer [#909](https://github.com/RocketPy-Team/RocketPy/pull/909)
40-
- ENH: DOC: Configure AI instructions and update developer docs [#975](https://github.com/RocketPy-Team/RocketPy/pull/975)
41-
- ENH: BUG: fix wind heading and direction wraparound interpolation [#974](https://github.com/RocketPy-Team/RocketPy/pull/974)
42-
- ENH: MNT: Sync develop's Renovate config with master (#1039) [#1040](https://github.com/RocketPy-Team/RocketPy/pull/1040)
43-
- ENH: MNT: Configure Renovate Bot targeting develop branch [#972](https://github.com/RocketPy-Team/RocketPy/pull/972)
44-
- MNT: Refactor parachute implementation with abstract base and hemispherical model split [#958](https://github.com/RocketPy-Team/RocketPy/pull/958)
4543
- ENH: Monte Carlo Formatting Options [#947](https://github.com/RocketPy-Team/RocketPy/pull/947)
46-
- ENH: ENH: Auto-Detection of Pressure Conversion Factor [#966](https://github.com/RocketPy-Team/RocketPy/pull/966)
44+
- ENH: Adaptive Monte Carlo via Convergence Criteria [#922](https://github.com/RocketPy-Team/RocketPy/pull/922)
4745
- ENH: Auto-Detection of Pressure Conversion Factor [#966](https://github.com/RocketPy-Team/RocketPy/pull/966)
48-
- ENH: Discrete and Continuous Controllers [#946](https://github.com/RocketPy-Team/RocketPy/pull/946)
46+
- ENH: Introduce pressure unit conversion when using forecast/reanalysis/ensemble data [#955](https://github.com/RocketPy-Team/RocketPy/pull/955)
47+
- MNT: Refactor parachute implementation with abstract base and hemispherical model split [#958](https://github.com/RocketPy-Team/RocketPy/pull/958)
4948
- DOC: Add aerodynamic surfaces user guide [#1043](https://github.com/RocketPy-Team/RocketPy/pull/1043)
50-
- ENH: Add RingClusterMotor for annular clustered motor modeling [#924](https://github.com/RocketPy-Team/RocketPy/pull/924)
51-
- ENH: MNT: introduce pressure unit conversion when using forecast/reanalysis/ensemble data [#955](https://github.com/RocketPy-Team/RocketPy/pull/955)
49+
- DOC: Add Valkyrie flight example (Bisky Team) [#967](https://github.com/RocketPy-Team/RocketPy/pull/967)
50+
- DOC: Configure AI instructions and update developer docs [#975](https://github.com/RocketPy-Team/RocketPy/pull/975)
5251
- ENH: Auto Populate Changelog [#919](https://github.com/RocketPy-Team/RocketPy/pull/919)
53-
- ENH: Adaptive Monte Carlo via Convergence Criteria [#922](https://github.com/RocketPy-Team/RocketPy/pull/922)
54-
- TST: Add acceptance tests for 3DOF flight simulation based on Bella Lui rocket [#914](https://github.com/RocketPy-Team/RocketPy/pull/914)
55-
- ENH: adopt built-in Python logging instead of print() calls [#450](https://github.com/RocketPy-Team/RocketPy/issues/450)
52+
- MNT: Sync develop's Renovate config with master (#1039) [#1040](https://github.com/RocketPy-Team/RocketPy/pull/1040)
53+
- MNT: Configure Renovate Bot targeting develop branch [#972](https://github.com/RocketPy-Team/RocketPy/pull/972)
5654

5755
### Changed
5856

@@ -68,8 +66,9 @@ Attention: The newest changes should be on top -->
6866

6967
### Fixed
7068

69+
- BUG: Remove duplicate controller process; controllers were being invoked twice per time node [#949](https://github.com/RocketPy-Team/RocketPy/pull/949)
7170
- BUG: fix wind heading and direction wraparound interpolation [#974](https://github.com/RocketPy-Team/RocketPy/pull/974)
72-
- BUG: fix NaN in ND linear interpolation outside convex hull [#926](https://github.com/RocketPy-Team/RocketPy/issues/926)
71+
- BUG: fix NaN in ND linear interpolation outside convex hull (closes [#926](https://github.com/RocketPy-Team/RocketPy/issues/926)) [#969](https://github.com/RocketPy-Team/RocketPy/pull/969)
7372
- BUG: Add wraparound logic for wind direction in environment plots [#939](https://github.com/RocketPy-Team/RocketPy/pull/939)
7473

7574
## [v1.12.1] - 2026-04-03
@@ -109,6 +108,9 @@ Attention: The newest changes should be on top -->
109108
### Fixed
110109

111110
- BUG: Fix hard-coded radius value for parachute added mass calculation [#889](https://github.com/RocketPy-Team/RocketPy/pull/889)
111+
- BUG: Fix incorrect Jacobian in `only_radial_burn` branch of `SolidMotor.evaluate_geometry` [#944](https://github.com/RocketPy-Team/RocketPy/pull/944)
112+
- ENH: Restore `power_off_drag`/`power_on_drag` as `Function` objects and preserve raw user input in `_input` attributes [#941](https://github.com/RocketPy-Team/RocketPy/pull/941)
113+
- ENH: Add explicit timeouts to ThrustCurve API requests [#940](https://github.com/RocketPy-Team/RocketPy/pull/940)
112114
- DOC: Fix documentation build [#908](https://github.com/RocketPy-Team/RocketPy/pull/908)
113115
- BUG: energy_data plot not working for 3 dof sims [[#906](https://github.com/RocketPy-Team/RocketPy/issues/906)]
114116
- BUG: Fix CSV column header spacing in FlightDataExporter [#864](https://github.com/RocketPy-Team/RocketPy/issues/864)
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
Exceptions and Warnings
2+
=======================
3+
4+
RocketPy raises a small hierarchy of custom exceptions and warnings so that
5+
error handling can be more specific than catching plain ``ValueError`` or
6+
``UserWarning``. All exceptions inherit from :class:`~rocketpy.exceptions.RocketPyError`,
7+
which makes it possible to catch any RocketPy-specific error with a single
8+
``except`` clause while still deriving from the relevant built-in type
9+
(e.g. :class:`~rocketpy.exceptions.InvalidParameterError` is also a
10+
``ValueError``).
11+
12+
.. automodule:: rocketpy.exceptions
13+
:members:
14+
:show-inheritance:

docs/reference/index.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,10 @@ This reference manual details functions, modules, methods and attributes include
1818
classes/Flight
1919
Utilities <classes/utils/index>
2020
classes/EnvironmentAnalysis
21-
Monte Carlo Analysis <classes/monte_carlo/index>
21+
Monte Carlo Analysis <classes/monte_carlo/index>
2222
Sensitivity Analysis <classes/Sensitivity>
2323
Multivariate Rejection Sampler <classes/MultivariateRejectionSampler>
24+
Exceptions and Warnings <classes/exceptions>
2425

2526
.. toctree::
2627
:maxdepth: 2

docs/user/airbrakes.rst

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -95,9 +95,11 @@ To create an air brakes model, we essentially need to define the following:
9595
``deployment_level`` attribute. Inside this function, any controller logic,
9696
filters, and apogee prediction can be implemented.
9797

98-
- The **sampling rate** of the controller function, in seconds. This is the time
99-
between each call of the controller function, in simulation time. Must be
100-
given in Hertz.
98+
- The **sampling rate** of the controller function, in Hertz. This is how often
99+
the controller function is called in simulation time (a **discrete**
100+
controller). It can also be set to ``None`` to create a **continuous**
101+
controller that is called at every solver step (see
102+
:ref:`discrete-vs-continuous-controllers` below).
101103

102104
Defining the Controller Function
103105
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -108,8 +110,9 @@ The ``controller_function`` must take in the following arguments, in this
108110
order:
109111

110112
1. ``time`` (float): The current simulation time in seconds.
111-
2. ``sampling_rate`` (float): The rate at which the controller
112-
function is called, measured in Hertz (Hz).
113+
2. ``sampling_rate`` (float or ``None``): The rate at which the controller
114+
function is called, measured in Hertz (Hz). It is ``None`` for continuous
115+
controllers, so guard any ``1 / sampling_rate`` computation against ``None``.
113116
3. ``state`` (list): The state vector of the simulation. The state
114117
is a list containing the following values, in this order:
115118

@@ -372,6 +375,41 @@ controller function. If you want to disable this feature, set ``clamp`` to
372375
For more information on the :class:`rocketpy.AirBrakes` class
373376
initialization, see :class:`rocketpy.AirBrakes.__init__` section.
374377

378+
.. _discrete-vs-continuous-controllers:
379+
380+
Discrete vs. Continuous Controllers
381+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
382+
383+
The ``sampling_rate`` argument determines *when* the controller function is
384+
called during the flight simulation:
385+
386+
- **Discrete controller** (``sampling_rate`` set to a number, e.g. ``10``):
387+
the controller function is called at fixed intervals of ``1 / sampling_rate``
388+
seconds. This mirrors a real flight computer that reads its sensors and
389+
updates its actuators at a fixed frequency, and is the recommended choice
390+
when you want the simulation to reflect the actual control-loop rate of your
391+
hardware.
392+
- **Continuous controller** (``sampling_rate=None``): the controller function
393+
is called at *every* solver step of the numerical integrator. Use this when
394+
you want the control law to act as a continuous function of the state rather
395+
than a sampled one (for example, when validating a control model
396+
analytically).
397+
398+
.. warning::
399+
400+
For continuous controllers, ``sampling_rate`` is passed to your controller
401+
function as ``None``. Any computation such as ``1 / sampling_rate`` (a
402+
common pattern for rate-limiting deployment, as shown above) must guard
403+
against ``None`` to avoid a ``TypeError``.
404+
405+
.. note::
406+
407+
Discrete controllers add their sampling instants as time nodes to the
408+
simulation, so remember to set ``time_overshoot=False`` in the ``Flight``
409+
(see below) to make the integrator stop exactly at those instants.
410+
Continuous controllers do not add time nodes; they are evaluated on the
411+
integrator's own steps.
412+
375413
Simulating a Flight
376414
-------------------
377415

docs/user/environment/1-atm-models/forecast.rst

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -173,14 +173,14 @@ hourly updates. It is generally best for day-of-launch weather assessment and
173173
rapidly changing local conditions.
174174

175175
RocketPy supports HRRR through a dedicated THREDDS shortcut.
176-
Like NAM and RAP, HRRR is a regional model over North America.
176+
Like NAM and RAP, HRRR is a regional model over North America, so you need to
177+
specify latitude and longitude points within its coverage area. The same
178+
SpacePort America coordinates used above will be reused here.
177179

178-
If you have a HIRESW-compatible dataset from another provider (or a local copy),
179-
you can still load it explicitly by passing the path/URL in ``file`` and an
180-
appropriate mapping in ``dictionary``.
180+
.. code-block:: python
181181
182182
env_hrrr = Environment(
183-
date=now_plus_twelve,
183+
date=tomorrow,
184184
latitude=32.988528,
185185
longitude=-106.975056,
186186
)

docs/user/motors/tanks.rst

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,13 @@ The predefined ``CylindricalTank`` class is easy to use and is defined as such:
104104

105105
.. jupyter-execute::
106106

107-
cylindrical_geometry = CylindricalTank(radius=0.1, height=2.0, spherical_caps=False)
107+
cylindrical_geometry = CylindricalTank(radius_function=0.1, height=2.0, spherical_caps=False)
108+
109+
.. deprecated:: 1.13.0
110+
The ``radius`` keyword argument of ``CylindricalTank`` and ``SphericalTank``
111+
has been renamed to ``radius_function``. Passing ``radius=`` still works but
112+
now raises a ``DeprecationWarning`` and will be removed in v2.0.0. Use
113+
``radius_function=`` instead (it accepts the same constant radius value).
108114

109115
.. note::
110116
The ``spherical_caps`` parameter is optional and defaults to ``False``. If set
@@ -116,7 +122,7 @@ The predefined ``SphericalTank`` is defined with:
116122

117123
.. jupyter-execute::
118124

119-
spherical_geometry = SphericalTank(radius=0.1)
125+
spherical_geometry = SphericalTank(radius_function=0.1)
120126

121127
.. seealso::
122128
:class:`rocketpy.CylindricalTank` and :class:`rocketpy.SphericalTank`

docs/user/rocket/rocket_usage.rst

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,17 @@ Optionally, we can also define:
290290
- The parachute trigger system lag ``lag``.
291291
- The parachute trigger system noise ``noise``.
292292

293+
.. note::
294+
295+
Since v1.13.0, :class:`~rocketpy.Parachute` is an **abstract base class**
296+
and can no longer be instantiated directly. Instead, instantiate a concrete
297+
parachute model such as :class:`~rocketpy.HemisphericalParachute` (used
298+
below), which derives its geometry-dependent quantities (e.g. the added
299+
mass during descent) from the parachute ``radius`` and ``height``. As a
300+
convenience shortcut, ``Rocket.add_parachute(...)`` can still be called with
301+
keyword arguments (``name``, ``cd_s``, ``trigger``, ...) and will build a
302+
hemispherical parachute for you.
303+
293304
Lets add two parachutes to the rocket, one that will be deployed at
294305
apogee and another that will be deployed at 800 meters above ground level:
295306

@@ -435,6 +446,18 @@ First, lets guarantee that the rocket is stable, by plotting the static margin:
435446
If it is unreasonably **high**, your rocket is **super stable** and the
436447
simulation will most likely **fail**.
437448

449+
.. note::
450+
451+
RocketPy helps you catch this automatically: if the static margin is
452+
**negative at motor ignition**, an
453+
:class:`~rocketpy.exceptions.UnstableRocketWarning` is issued when the
454+
rocket is used in a simulation. The check is skipped when the rocket has
455+
any ``GenericSurface`` aerodynamic surfaces, since their lift coefficient
456+
derivative is not accounted for in the center of pressure calculation,
457+
which makes the static margin unreliable in that case. See
458+
:doc:`/reference/classes/exceptions` for the full list of RocketPy
459+
exceptions and warnings.
460+
438461
The lets check all the information available about the rocket:
439462

440463
.. jupyter-execute::

tests/integration/motors/test_ring_cluster_motor.py

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# pylint: disable=invalid-name
22
import pytest
33
import numpy as np
4-
from rocketpy import SolidMotor, Function
4+
from rocketpy import Flight, Function, Rocket, SolidMotor
55
from rocketpy.motors.ring_cluster_motor import RingClusterMotor
66

77

@@ -137,3 +137,73 @@ def test_cluster_propellant_inertia_dynamic(base_motor):
137137

138138
assert np.isclose(cluster.propellant_I_11(t), expected_ixx) # pylint: disable=not-callable
139139
assert np.isclose(cluster.propellant_I_33(t), expected_izz)
140+
141+
142+
def test_ring_cluster_motor_full_flight(
143+
calisto_motorless,
144+
calisto_nose_cone,
145+
calisto_tail,
146+
calisto_trapezoidal_fins,
147+
example_plain_env,
148+
):
149+
"""Integration test for PR #924: a ``RingClusterMotor`` must work
150+
end-to-end when mounted on a ``Rocket`` and flown to apogee. The clustered
151+
motor scales the base motor's thrust and total impulse by the number of
152+
motors, and the rocket must reach a positive apogee.
153+
154+
A lightweight base motor is used on purpose so the Calisto airframe stays
155+
aerodynamically stable (positive static margin) with the extra aft mass.
156+
"""
157+
# Lightweight base motor so the clustered aft mass keeps the rocket stable.
158+
base = SolidMotor(
159+
thrust_source=lambda t: 800 if t < 3 else 0,
160+
burn_time=3,
161+
dry_mass=0.2,
162+
dry_inertia=(0.01, 0.01, 0.001),
163+
grain_number=1,
164+
grain_density=1700,
165+
grain_outer_radius=0.02,
166+
grain_initial_inner_radius=0.01,
167+
grain_initial_height=0.1,
168+
nozzle_radius=0.01,
169+
grain_separation=0.001,
170+
grains_center_of_mass_position=0.1,
171+
center_of_dry_mass_position=0.1,
172+
coordinate_system_orientation="nozzle_to_combustion_chamber",
173+
)
174+
number = 2
175+
cluster = RingClusterMotor(motor=base, number=number, radius=0.03)
176+
177+
# Clustered thrust / total impulse scale with the number of motors.
178+
assert np.isclose(cluster.thrust(1), base.thrust(1) * number)
179+
assert np.isclose(cluster.total_impulse, base.total_impulse * number)
180+
181+
rocket = calisto_motorless
182+
rocket.add_motor(cluster, position=-1.373)
183+
# Add all aerodynamic surfaces at once so the intermediate (fin-less) state
184+
# is never evaluated as unstable during construction.
185+
rocket.add_surfaces(
186+
[calisto_nose_cone, calisto_tail, calisto_trapezoidal_fins],
187+
[1.160, -1.313, -1.168],
188+
)
189+
rocket.set_rail_buttons(
190+
upper_button_position=0.082,
191+
lower_button_position=-0.618,
192+
angular_position=0,
193+
)
194+
195+
# The clustered motor keeps the rocket aerodynamically stable.
196+
assert rocket.static_margin(0) > 0
197+
198+
flight = Flight(
199+
rocket=rocket,
200+
environment=example_plain_env,
201+
rail_length=5.2,
202+
inclination=85,
203+
heading=0,
204+
terminate_on_apogee=True,
205+
)
206+
207+
assert flight.rocket.motor is cluster
208+
assert flight.t_final > 0
209+
assert flight.apogee > flight.env.elevation

tests/integration/motors/test_solid_motor.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,32 @@ def test_evaluate_geometry_updates_properties(cesaroni_m1670):
5656

5757
# evaluate at intermediate time
5858
assert isinstance(motor.grain_inner_radius(0.5), float)
59+
60+
61+
def test_only_radial_burn_grain_geometry_time_evolution(cesaroni_m1670):
62+
"""Regression for PR #944 (corrected Jacobian of the ``only_radial_burn``
63+
branch of ``evaluate_geometry``).
64+
65+
With radial-only burning the integrated grain geometry over the full burn
66+
must have a constant grain height (no axial regression) and a monotonically
67+
growing inner radius, bounded by the initial inner radius and the grain
68+
outer radius. A wrong Jacobian corrupts this integration.
69+
"""
70+
motor = cesaroni_m1670
71+
motor.only_radial_burn = True
72+
motor.evaluate_geometry()
73+
74+
times = np.linspace(0, motor.grain_burn_out, 30)
75+
heights = np.array([motor.grain_height(t) for t in times])
76+
radii = np.array([motor.grain_inner_radius(t) for t in times])
77+
78+
# Radial-only burn: grain height stays at its initial value the whole burn.
79+
assert np.allclose(heights, motor.grain_initial_height, atol=1e-9)
80+
81+
# Inner radius grows monotonically (non-decreasing) and actually increases.
82+
assert np.all(np.diff(radii) >= -1e-12)
83+
assert radii[-1] > radii[0]
84+
85+
# Physical bounds on the inner radius.
86+
assert np.isclose(radii[0], motor.grain_initial_inner_radius)
87+
assert radii[-1] <= motor.grain_outer_radius + 1e-12

0 commit comments

Comments
 (0)