Skip to content

Commit bae4ec8

Browse files
committed
Merge remote-tracking branch 'RocketPy/master' into enh/merge-from-rocketpy
2 parents 774575c + 9bd6ad3 commit bae4ec8

17 files changed

Lines changed: 741 additions & 58 deletions

File tree

CHANGELOG.md

Lines changed: 12 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -32,42 +32,33 @@ Attention: The newest changes should be on top -->
3232

3333
### Added
3434

35-
- ENH: ENH: Interactive 3D Flight Trajectory and Attitude Animation. [#1066](https://github.com/RocketPy-Team/RocketPy/pull/1066)
36-
- ENH: ENH: Interactive 3D Flight Trajectory and Attitude Animation [#1066](https://github.com/RocketPy-Team/RocketPy/pull/1066)
37-
- ENH: ENH: `Function` vectorized speed-up and refactor [#1049](https://github.com/RocketPy-Team/RocketPy/pull/1049)
38-
- ENH: REV: Revert PR #958 [#1063](https://github.com/RocketPy-Team/RocketPy/pull/1063)
39-
- ENH: MNT: remove duplicate and merge-sync entries from Unreleased changelog [#1062](https://github.com/RocketPy-Team/RocketPy/pull/1062)
4035
### Changed
4136

42-
- MNT: MNT: Remove Unused pylint Disable Statements. [#1067](https://github.com/RocketPy-Team/RocketPy/pull/1067)
43-
- MNT: MNT: Remove Unused pylint Disable Statements. [#1067](https://github.com/RocketPy-Team/RocketPy/pull/1067)
44-
4537
### Fixed
4638

47-
## [v1.13.0] - 2026-07-04
39+
## [v1.13.0] - 2026-07-21
4840

4941
### Added
5042

51-
- ENH: ENH: Refactor flight.py latitude/longitude to use inverted_haversine [#1055](https://github.com/RocketPy-Team/RocketPy/pull/1055)
52-
- ENH: TST: Add unit tests for evaluate_reduced_mass and remove TODO [#1051](https://github.com/RocketPy-Team/RocketPy/pull/1051)
43+
- ENH: BUG/MNT: pre-release v1.13.0 review fixes [#1074](https://github.com/RocketPy-Team/RocketPy/pull/1074)
44+
- ENH: `Function` vectorized speed-up and refactor [#1049](https://github.com/RocketPy-Team/RocketPy/pull/1049)
45+
- ENH: Interactive 3D Flight Trajectory and Attitude Animation (PyVista) [#1066](https://github.com/RocketPy-Team/RocketPy/pull/1066)
46+
- ENH: Add optional `max_time` to `StochasticFlight` and carry base-flight attributes into created objects [#1070](https://github.com/RocketPy-Team/RocketPy/pull/1070)
5347
- ENH: seed sensor measurement noise per instance [#1052](https://github.com/RocketPy-Team/RocketPy/pull/1052)
54-
- ENH: FIX: pre release hardening [#1047](https://github.com/RocketPy-Team/RocketPy/pull/1047)
55-
- ENH: DEV: Claude Code ruff hooks (auto-format on edit + pre-push lint guard) [#1046](https://github.com/RocketPy-Team/RocketPy/pull/1046)
56-
- ENH: CI: create a CI for testing docs updates + solve different docs issues [#1045](https://github.com/RocketPy-Team/RocketPy/pull/1045)
57-
- ENH: MNT: final fixes before next release [#1044](https://github.com/RocketPy-Team/RocketPy/pull/1044)
48+
- DEV: Claude Code ruff hooks (auto-format on edit + pre-push lint guard) [#1046](https://github.com/RocketPy-Team/RocketPy/pull/1046)
49+
- CI: create a CI for testing docs updates + solve different docs issues [#1045](https://github.com/RocketPy-Team/RocketPy/pull/1045)
50+
- MNT: final fixes before next release [#1044](https://github.com/RocketPy-Team/RocketPy/pull/1044)
5851
- 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)
5952
- ENH: Add AIGFS and HRRR forecast models to the Environment class [#951](https://github.com/RocketPy-Team/RocketPy/pull/951)
6053
- ENH: Add RingClusterMotor for annular clustered motor modeling [#924](https://github.com/RocketPy-Team/RocketPy/pull/924)
6154
- ENH: Discrete and Continuous Controllers [#946](https://github.com/RocketPy-Team/RocketPy/pull/946)
6255
- ENH: Add custom exceptions and unstable rocket warning [#970](https://github.com/RocketPy-Team/RocketPy/pull/970)
6356
- 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)
6457
- ENH: Pass acceleration (`u_dot`) data to parachute trigger functions [#911](https://github.com/RocketPy-Team/RocketPy/pull/911)
65-
- ENH: Add 3D flight trajectory and attitude animations in Flight plots layer [#909](https://github.com/RocketPy-Team/RocketPy/pull/909)
6658
- ENH: Monte Carlo Formatting Options [#947](https://github.com/RocketPy-Team/RocketPy/pull/947)
6759
- ENH: Adaptive Monte Carlo via Convergence Criteria [#922](https://github.com/RocketPy-Team/RocketPy/pull/922)
6860
- ENH: Auto-Detection of Pressure Conversion Factor [#966](https://github.com/RocketPy-Team/RocketPy/pull/966)
6961
- ENH: Introduce pressure unit conversion when using forecast/reanalysis/ensemble data [#955](https://github.com/RocketPy-Team/RocketPy/pull/955)
70-
- MNT: Refactor parachute implementation with abstract base and hemispherical model split [#958](https://github.com/RocketPy-Team/RocketPy/pull/958)
7162
- DOC: Add aerodynamic surfaces user guide [#1043](https://github.com/RocketPy-Team/RocketPy/pull/1043)
7263
- DOC: Add Valkyrie flight example (Bisky Team) [#967](https://github.com/RocketPy-Team/RocketPy/pull/967)
7364
- DOC: Configure AI instructions and update developer docs [#975](https://github.com/RocketPy-Team/RocketPy/pull/975)
@@ -78,11 +69,13 @@ Attention: The newest changes should be on top -->
7869
### Changed
7970

8071
- REL: bumps up rocketpy version to 1.13.0 [#1048](https://github.com/RocketPy-Team/RocketPy/pull/1048)
81-
- MNT: **Breaking**: `Parachute` is now an abstract base class and can no longer be instantiated directly; use `HemisphericalParachute` (or `Rocket.add_parachute`, which builds it for you). Loading old serialized files still works [#958](https://github.com/RocketPy-Team/RocketPy/pull/958)
72+
- MNT: Remove unused pylint disable statements [#1067](https://github.com/RocketPy-Team/RocketPy/pull/1067)
8273
- MNT: Discrete controllers are now called exactly once per time node (previously twice); results may change for stateful controllers and `observed_variables` no longer contains duplicated entries [#949](https://github.com/RocketPy-Team/RocketPy/pull/949)
8374
- MNT: Multi-dimensional linear `Function` objects now apply their extrapolation rule to points outside the data's convex hull (previously returned NaN) [#969](https://github.com/RocketPy-Team/RocketPy/pull/969)
84-
- MNT: Informational messages previously shown with `print()` now use Python logging and are silent by default; call `rocketpy.utils.enable_logging()` to see them [#973](https://github.com/RocketPy-Team/RocketPy/pull/973)
75+
- MNT: Informational messages previously shown with `print()` now use Python logging and are silent by default; call `rocketpy.utilities.enable_logging()` to see them [#973](https://github.com/RocketPy-Team/RocketPy/pull/973)
8576
- ENH: Refactor flight.py latitude/longitude to use inverted_haversine [#1055](https://github.com/RocketPy-Team/RocketPy/pull/1055)
77+
- MNT: `Function` with `interpolation="akima"` now matches the canonical (SciPy) Akima spline; interpolated values differ slightly from previous releases [#1049](https://github.com/RocketPy-Team/RocketPy/pull/1049)
78+
- MNT: `Function.differentiate` on 1-D array Functions now returns the analytical derivative of the interpolation (previously central finite differences); values at knots and domain edges may change [#1049](https://github.com/RocketPy-Team/RocketPy/pull/1049)
8679

8780
### Deprecated
8881

docs/examples/index.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,13 @@ apogee of some rockets.
1212
:hide-code:
1313

1414
import plotly.graph_objects as go
15+
import plotly.io as pio
16+
17+
# Emit an HTML output that pulls plotly.js from the CDN, so the figure
18+
# renders on the static documentation pages. Without an explicit renderer,
19+
# ``fig.show()`` under jupyter-execute produces a plotly-mimetype bundle
20+
# that a static HTML page has no frontend to display (blank chart).
21+
pio.renderers.default = "notebook_connected"
1522

1623
results = {
1724
# "Name (Year)": (simulated, measured) m

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ monte-carlo = [
7272

7373
animation = [
7474
"pyvista>=0.45",
75+
"imageio",
7576
"imageio-ffmpeg>=0.5"
7677
]
7778

requirements-optional.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ imageio
77
multiprocess>=0.70
88
statsmodels
99
prettytable
10-
vedo>=2024.5.1
10+
pyvista>=0.45
11+
imageio-ffmpeg>=0.5

rocketpy/environment/environment.py

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
get_interval_date_from_time_array,
3939
get_pressure_levels_from_file,
4040
mask_and_clean_dataset,
41+
pressure_unit_to_factor,
4142
)
4243
from rocketpy.environment.weather_model_mapping import WeatherModelMapping
4344
from rocketpy.mathutils.function import NUMERICAL_TYPES, Function, funcify_method
@@ -1158,9 +1159,7 @@ def __determine_pressure_conversion_factor(
11581159
if pressure_conversion_factor is not None:
11591160
# User explicitly supplied a value — honour it.
11601161
if isinstance(pressure_conversion_factor, str):
1161-
return (
1162-
100 if pressure_conversion_factor.lower() in ("mbar", "hpa") else 1
1163-
)
1162+
return pressure_unit_to_factor(pressure_conversion_factor)
11641163
return pressure_conversion_factor
11651164

11661165
# Auto-detect. Primary source: known-model lookup table.
@@ -1173,9 +1172,9 @@ def __determine_pressure_conversion_factor(
11731172
_pa_files = {"GFS", "NAM", "RAP", "HRRR", "AIGFS"}
11741173
if input_dict in _hpa_dicts or input_file in _hpa_dicts:
11751174
return 100
1176-
if input_file in _hpa_files:
1175+
if input_dict in _hpa_files or input_file in _hpa_files:
11771176
return 100
1178-
if input_file in _pa_files:
1177+
if input_dict in _pa_files or input_file in _pa_files:
11791178
return 1
11801179
return None
11811180

@@ -1360,11 +1359,7 @@ def set_atmospheric_model( # pylint: disable=too-many-statements
13601359
"Argument 'pressure_conversion_factor' must be strictly positive!"
13611360
)
13621361
if isinstance(pressure_conversion_factor, str):
1363-
if pressure_conversion_factor.lower() not in (
1364-
"mbar",
1365-
"hpa",
1366-
"pa",
1367-
):
1362+
if pressure_unit_to_factor(pressure_conversion_factor) is None:
13681363
raise ValueError(
13691364
"Argument 'pressure_conversion_factor' unit must be a standard pressure unit ('mbar', 'hPa', 'Pa')!"
13701365
)
@@ -2961,6 +2956,7 @@ def to_dict(self, **kwargs):
29612956
"wind_direction_ensemble": getattr(self, "wind_direction_ensemble", None),
29622957
"wind_speed_ensemble": getattr(self, "wind_speed_ensemble", None),
29632958
"num_ensemble_members": getattr(self, "num_ensemble_members", None),
2959+
"ensemble_member": getattr(self, "ensemble_member", None),
29642960
}
29652961

29662962
if kwargs.get("include_outputs", False):
@@ -3027,6 +3023,7 @@ def from_dict(cls, data): # pylint: disable=too-many-statements
30273023
env.wind_direction_ensemble = data["wind_direction_ensemble"]
30283024
env.wind_speed_ensemble = data["wind_speed_ensemble"]
30293025
env.num_ensemble_members = data["num_ensemble_members"]
3026+
env.ensemble_member = data.get("ensemble_member", 0) or 0
30303027

30313028
env.__reset_barometric_height_function()
30323029
env.calculate_density_profile()

rocketpy/environment/tools.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,35 @@ def geodesic_to_lambert_conformal(lat, lon, projection_variable, x_units="m"):
190190
## These functions are meant to be used with netcdf4 datasets
191191

192192

193+
HPA_UNIT_SYNONYMS = frozenset(
194+
{"hpa", "mbar", "mb", "millibar", "millibars", "hectopascal", "hectopascals"}
195+
)
196+
PA_UNIT_SYNONYMS = frozenset({"pa", "pascal"})
197+
198+
199+
def pressure_unit_to_factor(unit):
200+
"""Return the Pa conversion factor for a pressure-unit string.
201+
202+
Parameters
203+
----------
204+
unit : str
205+
Pressure unit label (case-insensitive), e.g. ``"hPa"``, ``"mb"``,
206+
``"millibar"`` or ``"Pa"``.
207+
208+
Returns
209+
-------
210+
int or None
211+
``100`` for hPa/millibar synonyms, ``1`` for Pa synonyms, or ``None``
212+
if the unit string is not recognised.
213+
"""
214+
unit = unit.lower().strip()
215+
if unit in HPA_UNIT_SYNONYMS:
216+
return 100
217+
if unit in PA_UNIT_SYNONYMS:
218+
return 1
219+
return None
220+
221+
193222
def get_pressure_levels_from_file(data, dictionary, conversion_factor):
194223
"""Extracts pressure levels from a netCDF4 dataset and converts them to Pa.
195224
@@ -219,7 +248,7 @@ def get_pressure_levels_from_file(data, dictionary, conversion_factor):
219248
level_var = data.variables[dictionary["level"]]
220249
if conversion_factor is None:
221250
raw_units = getattr(level_var, "units", "").lower().strip()
222-
if raw_units in ("hpa", "mbar", "millibars", "hectopascal", "hectopascals"):
251+
if raw_units in HPA_UNIT_SYNONYMS:
223252
conversion_factor = 100
224253
else:
225254
conversion_factor = 1

rocketpy/mathutils/_calc/_fitting.py

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,12 @@ def fit_akima(x, y):
100100
h = np.diff(x)
101101
m = np.diff(y) / h # slopes of segments, shape (n-1,)
102102

103+
if n == 2:
104+
# A single segment: Akima reduces to linear interpolation. The ghost-
105+
# value reflection below indexes ``m[1]``/``m[-2]``, which do not exist
106+
# for two points, so this case must be handled explicitly.
107+
return np.vstack([y[:-1], m, np.zeros(1), np.zeros(1)])
108+
103109
# Extend m with 2 ghost values on each side (Akima boundary reflection)
104110
# m_ext indices: 0..n+2 (n-1 interior + 2 left + 2 right)
105111
m_ext = np.empty(n + 3)
@@ -176,12 +182,15 @@ def fit_pchip(x, y):
176182
sign_change = (m[:-1] * m[1:]) <= 0
177183
pos_mask = ~sign_change
178184

179-
# Safe harmonic mean
180-
t[1:-1] = np.where(
181-
pos_mask,
182-
(w1 + w2) / (w1 / m[:-1] + w2 / m[1:]),
183-
0.0,
184-
)
185+
# Safe harmonic mean. The reciprocals are evaluated eagerly before
186+
# ``np.where`` masks them, so silence the harmless divide-by-zero that
187+
# occurs on flat segments (where ``pos_mask`` is already False).
188+
with np.errstate(divide="ignore", invalid="ignore"):
189+
t[1:-1] = np.where(
190+
pos_mask,
191+
(w1 + w2) / (w1 / m[:-1] + w2 / m[1:]),
192+
0.0,
193+
)
185194

186195
# Boundary slopes: one-sided, clamped for monotonicity
187196
t[0] = _pchip_edge_slope(

rocketpy/motors/ring_cluster_motor.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,9 @@ def info(self, *args, **kwargs):
289289
print("Cluster Configuration:")
290290
print(f" - Motors: {self.number} x {type(self.motor).__name__}")
291291
print(f" - Radial Distance: {self.radius} m")
292-
return self.motor.info(*args, **kwargs)
292+
# Report the cluster's own (N-times scaled) quantities, not the single
293+
# base motor's, mirroring the correct behaviour of ``all_info``.
294+
return super().info(*args, **kwargs)
293295

294296
def to_dict(self, **kwargs):
295297
data = super().to_dict(**kwargs)

rocketpy/plots/flight_plots.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -888,10 +888,7 @@ def _animation_options( # pylint: disable=too-many-statements
888888
options = {
889889
"background_color": kwargs.pop("background_color", None),
890890
"playback_controls": kwargs.pop("playback_controls", True),
891-
"show_subrocket_point": kwargs.pop(
892-
"show_subrocket_point",
893-
kwargs.pop("show_subrocket_point", True),
894-
),
891+
"show_subrocket_point": kwargs.pop("show_subrocket_point", True),
895892
"ground_image": kwargs.pop("ground_image", None),
896893
"ground_image_bounds": kwargs.pop("ground_image_bounds", None),
897894
"ground_image_coordinates": kwargs.pop("ground_image_coordinates", "enu"),
@@ -1265,9 +1262,10 @@ def animate_trajectory( # pylint: disable=too-many-statements,too-many-locals
12651262
Ground texture. A mapping may define ``image``, ``bounds``,
12661263
``coordinates`` (``"enu"`` or ``"latlon"``), and ``flip_y`` for
12671264
geographic placement. Default is None.
1268-
color_by : {"speed", "mach", "dynamic_pressure", "acceleration",
1269-
"altitude", False, None}, optional
1270-
Trajectory point scalar. Default is "speed".
1265+
color_by : str | bool | None, optional
1266+
Trajectory point scalar, one of ``"speed"``, ``"mach"``,
1267+
``"dynamic_pressure"``, ``"acceleration"``, ``"altitude"``,
1268+
``False`` or ``None``. Default is "speed".
12711269
show_kinematic_plots : bool, optional
12721270
Show altitude, speed and acceleration histories. Default is False.
12731271
camera_mode : {"static", "follow", "ground", "body"}, optional

rocketpy/rocket/parachute.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from inspect import signature
1+
from inspect import Parameter, signature
22

33
import numpy as np
44

@@ -302,6 +302,10 @@ def __evaluate_trigger_function(self, trigger): # pylint: disable=too-many-stat
302302
def _make_wrapper(fn):
303303
sig = signature(fn)
304304
params = list(sig.parameters.keys())
305+
has_var_positional = any(
306+
param.kind is Parameter.VAR_POSITIONAL
307+
for param in sig.parameters.values()
308+
)
305309

306310
# detect if user function expects acceleration-like argument
307311
expects_udot = any(
@@ -324,6 +328,12 @@ def wrapper(p, h, y, sensors, u_dot):
324328
if num_params >= 5:
325329
# Pass both sensors and u_dot
326330
return fn(p, h, y, sensors, u_dot)
331+
if has_var_positional:
332+
# Variadic triggers (e.g. ``lambda *args: ...``) accept any
333+
# number of positional arguments. Preserve the pre-1.13
334+
# behaviour of calling them with (pressure, height, state,
335+
# sensors).
336+
return fn(p, h, y, sensors)
327337
# If function signature is not supported, raise an error
328338
raise TypeError(
329339
f"Trigger function '{fn.__name__}' has unsupported signature: "

0 commit comments

Comments
 (0)