Skip to content

Commit afc2858

Browse files
authored
Merge branch 'rel/v1.12.0' into copilot/sub-pr-935-again
2 parents 7766f88 + 8938aa7 commit afc2858

7 files changed

Lines changed: 292 additions & 91 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,12 @@ Attention: The newest changes should be on top -->
4040

4141
### Fixed
4242

43-
- BUG: Restore `Rocket.power_off_drag` and `Rocket.power_on_drag` as `Function` objects while preserving raw inputs in `power_off_drag_input` and `power_on_drag_input` [#941](https://github.com/RocketPy-Team/RocketPy/pull/941)
43+
-
4444

4545
## [v1.12.0] - 2026-03-08
4646

4747
### Added
4848

49-
- ENH: Air brakes controller functions now support 8-parameter signature [#854](https://github.com/RocketPy-Team/RocketPy/pull/854)
5049
- TST: Add acceptance tests for 3DOF flight simulation based on Bella Lui rocket [#914] (https://github.com/RocketPy-Team/RocketPy/pull/914_
5150
- ENH: Add background map auto download functionality to Monte Carlo plots [#896](https://github.com/RocketPy-Team/RocketPy/pull/896)
5251
- MNT: net thrust addition to 3 dof in flight class [#907] (https://github.com/RocketPy-Team/RocketPy/pull/907)
@@ -72,6 +71,9 @@ Attention: The newest changes should be on top -->
7271

7372
### Fixed
7473

74+
- BUG: Restore `Rocket.power_off_drag` and `Rocket.power_on_drag` as `Function` objects while preserving raw inputs in `power_off_drag_input` and `power_on_drag_input` [#941](https://github.com/RocketPy-Team/RocketPy/pull/941)
75+
- BUG: Add explicit timeouts to ThrustCurve API requests [#935](https://github.com/RocketPy-Team/RocketPy/pull/935)
76+
- BUG: Fix hard-coded radius value for parachute added mass calculation [#889](https://github.com/RocketPy-Team/RocketPy/pull/889)
7577
- DOC: Fix documentation build [#908](https://github.com/RocketPy-Team/RocketPy/pull/908)
7678
- BUG: energy_data plot not working for 3 dof sims [[#906](https://github.com/RocketPy-Team/RocketPy/issues/906)]
7779
- BUG: Fix CSV column header spacing in FlightDataExporter [#864](https://github.com/RocketPy-Team/RocketPy/issues/864)

rocketpy/motors/motor.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1946,8 +1946,11 @@ def _call_thrustcurve_api(name: str, no_cache: bool = False): # pylint: disable
19461946
------
19471947
ValueError
19481948
If no motor is found or if the downloaded .eng data is missing.
1949+
requests.exceptions.Timeout
1950+
If a search or download request to the ThrustCurve API exceeds the
1951+
timeout limit (5 s connect / 30 s read).
19491952
requests.exceptions.RequestException
1950-
If a network or HTTP error occurs during the API call.
1953+
If any other network or HTTP error occurs during the API call.
19511954
19521955
Notes
19531956
-----
@@ -1973,8 +1976,13 @@ def _call_thrustcurve_api(name: str, no_cache: bool = False): # pylint: disable
19731976
)
19741977

19751978
base_url = "https://www.thrustcurve.org/api/v1"
1979+
_timeout = (5, 30) # (connect timeout, read timeout) in seconds
19761980
# Step 1. Search motor
1977-
response = requests.get(f"{base_url}/search.json", params={"commonName": name})
1981+
response = requests.get(
1982+
f"{base_url}/search.json",
1983+
params={"commonName": name},
1984+
timeout=_timeout,
1985+
)
19781986
response.raise_for_status()
19791987
data = response.json()
19801988

@@ -1994,6 +2002,7 @@ def _call_thrustcurve_api(name: str, no_cache: bool = False): # pylint: disable
19942002
dl_response = requests.get(
19952003
f"{base_url}/download.json",
19962004
params={"motorIds": motor_id, "format": "RASP", "data": "file"},
2005+
timeout=_timeout,
19972006
)
19982007
dl_response.raise_for_status()
19992008
dl_data = dl_response.json()

rocketpy/rocket/parachute.py

Lines changed: 91 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -92,17 +92,25 @@ class Parachute:
9292
Function of noisy_pressure_signal.
9393
Parachute.clean_pressure_signal_function : Function
9494
Function of clean_pressure_signal.
95+
Parachute.drag_coefficient : float
96+
Drag coefficient of the inflated canopy shape, used only when
97+
``radius`` is not provided to estimate the parachute radius from
98+
``cd_s``: ``R = sqrt(cd_s / (drag_coefficient * pi))``. Typical
99+
values: 1.4 for hemispherical canopies (default), 0.75 for flat
100+
circular canopies, 1.5 for extended-skirt canopies.
95101
Parachute.radius : float
96102
Length of the non-unique semi-axis (radius) of the inflated hemispheroid
97-
parachute in meters.
98-
Parachute.height : float, None
103+
parachute in meters. If not provided at construction time, it is
104+
estimated from ``cd_s`` and ``drag_coefficient``.
105+
Parachute.height : float
99106
Length of the unique semi-axis (height) of the inflated hemispheroid
100107
parachute in meters.
101108
Parachute.porosity : float
102-
Geometric porosity of the canopy (ratio of open area to total canopy area),
103-
in [0, 1]. Affects only the added-mass scaling during descent; it does
104-
not change ``cd_s`` (drag). The default, 0.0432, yields an added-mass
105-
of 1.0 (“neutral” behavior).
109+
Geometric porosity of the canopy (ratio of open area to total canopy
110+
area), in [0, 1]. Affects only the added-mass scaling during descent;
111+
it does not change ``cd_s`` (drag). The default value of 0.0432 is
112+
chosen so that the resulting ``added_mass_coefficient`` equals
113+
approximately 1.0 ("neutral" added-mass behavior).
106114
Parachute.added_mass_coefficient : float
107115
Coefficient used to calculate the added-mass due to dragged air. It is
108116
calculated from the porosity of the parachute.
@@ -116,9 +124,10 @@ def __init__(
116124
sampling_rate,
117125
lag=0,
118126
noise=(0, 0, 0),
119-
radius=1.5,
127+
radius=None,
120128
height=None,
121129
porosity=0.0432,
130+
drag_coefficient=1.4,
122131
):
123132
"""Initializes Parachute class.
124133
@@ -172,25 +181,83 @@ def __init__(
172181
passed to the trigger function. Default value is ``(0, 0, 0)``.
173182
Units are in Pa.
174183
radius : float, optional
175-
Length of the non-unique semi-axis (radius) of the inflated hemispheroid
176-
parachute. Default value is 1.5.
184+
Length of the non-unique semi-axis (radius) of the inflated
185+
hemispheroid parachute. If not provided, it is estimated from
186+
``cd_s`` and ``drag_coefficient`` using:
187+
``radius = sqrt(cd_s / (drag_coefficient * pi))``.
177188
Units are in meters.
178189
height : float, optional
179190
Length of the unique semi-axis (height) of the inflated hemispheroid
180191
parachute. Default value is the radius of the parachute.
181192
Units are in meters.
182193
porosity : float, optional
183-
Geometric porosity of the canopy (ratio of open area to total canopy area),
184-
in [0, 1]. Affects only the added-mass scaling during descent; it does
185-
not change ``cd_s`` (drag). The default, 0.0432, yields an added-mass
186-
of 1.0 (“neutral” behavior).
194+
Geometric porosity of the canopy (ratio of open area to total
195+
canopy area), in [0, 1]. Affects only the added-mass scaling
196+
during descent; it does not change ``cd_s`` (drag). The default
197+
value of 0.0432 is chosen so that the resulting
198+
``added_mass_coefficient`` equals approximately 1.0 ("neutral"
199+
added-mass behavior).
200+
drag_coefficient : float, optional
201+
Drag coefficient of the inflated canopy shape, used only when
202+
``radius`` is not provided. It relates the aerodynamic ``cd_s``
203+
to the physical canopy area via
204+
``cd_s = drag_coefficient * pi * radius**2``. Typical values:
205+
206+
- **1.4** — hemispherical canopy (default, NASA SP-8066)
207+
- **0.75** — flat circular canopy
208+
- **1.5** — extended-skirt canopy
209+
210+
Has no effect when ``radius`` is explicitly provided.
187211
"""
212+
213+
# Save arguments as attributes
188214
self.name = name
189215
self.cd_s = cd_s
190216
self.trigger = trigger
191217
self.sampling_rate = sampling_rate
192218
self.lag = lag
193219
self.noise = noise
220+
self.drag_coefficient = drag_coefficient
221+
self.porosity = porosity
222+
223+
# Initialize derived attributes
224+
self.radius = self.__resolve_radius(radius, cd_s, drag_coefficient)
225+
self.height = self.__resolve_height(height, self.radius)
226+
self.added_mass_coefficient = self.__compute_added_mass_coefficient(
227+
self.porosity
228+
)
229+
self.__init_noise(noise)
230+
self.__evaluate_trigger_function(trigger)
231+
232+
# Prints and plots
233+
self.prints = _ParachutePrints(self)
234+
235+
def __resolve_radius(self, radius, cd_s, drag_coefficient):
236+
"""Resolves parachute radius from input or aerodynamic relation."""
237+
if radius is not None:
238+
return radius
239+
240+
# cd_s = Cd * S = Cd * pi * R^2 => R = sqrt(cd_s / (Cd * pi))
241+
return np.sqrt(cd_s / (drag_coefficient * np.pi))
242+
243+
def __resolve_height(self, height, radius):
244+
"""Resolves parachute height defaulting to radius when not provided."""
245+
return height or radius
246+
247+
def __compute_added_mass_coefficient(self, porosity):
248+
"""Computes the added-mass coefficient from canopy porosity."""
249+
return 1.068 * (
250+
1 - 1.465 * porosity - 0.25975 * porosity**2 + 1.2626 * porosity**3
251+
)
252+
253+
def __init_noise(self, noise):
254+
"""Initializes all noise-related attributes.
255+
256+
Parameters
257+
----------
258+
noise : tuple, list
259+
List in the format (mean, standard deviation, time-correlation).
260+
"""
194261
self.noise_signal = [[-1e-6, np.random.normal(noise[0], noise[1])]]
195262
self.noisy_pressure_signal = []
196263
self.clean_pressure_signal = []
@@ -200,32 +267,19 @@ def __init__(
200267
self.clean_pressure_signal_function = Function(0)
201268
self.noisy_pressure_signal_function = Function(0)
202269
self.noise_signal_function = Function(0)
203-
self.radius = radius
204-
self.height = height or radius
205-
self.porosity = porosity
206-
self.added_mass_coefficient = 1.068 * (
207-
1
208-
- 1.465 * self.porosity
209-
- 0.25975 * self.porosity**2
210-
+ 1.2626 * self.porosity**3
211-
)
212-
213270
alpha, beta = self.noise_corr
214271
self.noise_function = lambda: (
215272
alpha * self.noise_signal[-1][1]
216273
+ beta * np.random.normal(noise[0], noise[1])
217274
)
218275

219-
self.prints = _ParachutePrints(self)
220-
221-
self.__evaluate_trigger_function(trigger)
222-
223276
def __evaluate_trigger_function(self, trigger):
224277
"""This is used to set the triggerfunc attribute that will be used to
225278
interact with the Flight class.
226279
"""
227280
# pylint: disable=unused-argument, function-redefined
228-
# The parachute is deployed by a custom function
281+
282+
# Case 1: The parachute is deployed by a custom function
229283
if callable(trigger):
230284
# work around for having added sensors to parachute triggers
231285
# to avoid breaking changes
@@ -238,26 +292,29 @@ def triggerfunc(p, h, y, sensors):
238292

239293
self.triggerfunc = triggerfunc
240294

295+
# Case 2: The parachute is deployed at a given height
241296
elif isinstance(trigger, (int, float)):
242297
# The parachute is deployed at a given height
243-
def triggerfunc(p, h, y, sensors): # pylint: disable=unused-argument
298+
def triggerfunc(p, h, y, sensors):
244299
# p = pressure considering parachute noise signal
245300
# h = height above ground level considering parachute noise signal
246301
# y = [x, y, z, vx, vy, vz, e0, e1, e2, e3, w1, w2, w3]
247302
return y[5] < 0 and h < trigger
248303

249304
self.triggerfunc = triggerfunc
250305

306+
# Case 3: The parachute is deployed at apogee
251307
elif trigger.lower() == "apogee":
252308
# The parachute is deployed at apogee
253-
def triggerfunc(p, h, y, sensors): # pylint: disable=unused-argument
309+
def triggerfunc(p, h, y, sensors):
254310
# p = pressure considering parachute noise signal
255311
# h = height above ground level considering parachute noise signal
256312
# y = [x, y, z, vx, vy, vz, e0, e1, e2, e3, w1, w2, w3]
257313
return y[5] < 0
258314

259315
self.triggerfunc = triggerfunc
260316

317+
# Case 4: Invalid trigger input
261318
else:
262319
raise ValueError(
263320
f"Unable to set the trigger function for parachute '{self.name}'. "
@@ -289,7 +346,7 @@ def info(self):
289346
def all_info(self):
290347
"""Prints all information about the Parachute class."""
291348
self.info()
292-
# self.plots.all() # Parachutes still doesn't have plots
349+
# self.plots.all() # TODO: Parachutes still doesn't have plots
293350

294351
def to_dict(self, **kwargs):
295352
allow_pickle = kwargs.get("allow_pickle", True)
@@ -309,6 +366,7 @@ def to_dict(self, **kwargs):
309366
"lag": self.lag,
310367
"noise": self.noise,
311368
"radius": self.radius,
369+
"drag_coefficient": self.drag_coefficient,
312370
"height": self.height,
313371
"porosity": self.porosity,
314372
}
@@ -341,7 +399,8 @@ def from_dict(cls, data):
341399
sampling_rate=data["sampling_rate"],
342400
lag=data["lag"],
343401
noise=data["noise"],
344-
radius=data.get("radius", 1.5),
402+
radius=data.get("radius", None),
403+
drag_coefficient=data.get("drag_coefficient", 1.4),
345404
height=data.get("height", None),
346405
porosity=data.get("porosity", 0.0432),
347406
)

rocketpy/rocket/rocket.py

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1511,9 +1511,10 @@ def add_parachute(
15111511
sampling_rate=100,
15121512
lag=0,
15131513
noise=(0, 0, 0),
1514-
radius=1.5,
1514+
radius=None,
15151515
height=None,
15161516
porosity=0.0432,
1517+
drag_coefficient=1.4,
15171518
):
15181519
"""Creates a new parachute, storing its parameters such as
15191520
opening delay, drag coefficients and trigger function.
@@ -1573,26 +1574,34 @@ def add_parachute(
15731574
passed to the trigger function. Default value is (0, 0, 0). Units
15741575
are in pascal.
15751576
radius : float, optional
1576-
Length of the non-unique semi-axis (radius) of the inflated hemispheroid
1577-
parachute. Default value is 1.5.
1577+
Length of the non-unique semi-axis (radius) of the inflated
1578+
hemispheroid parachute. If not provided, it is estimated from
1579+
`cd_s` and `drag_coefficient` using:
1580+
`radius = sqrt(cd_s / (drag_coefficient * pi))`.
15781581
Units are in meters.
15791582
height : float, optional
15801583
Length of the unique semi-axis (height) of the inflated hemispheroid
15811584
parachute. Default value is the radius of the parachute.
15821585
Units are in meters.
15831586
porosity : float, optional
1584-
Geometric porosity of the canopy (ratio of open area to total canopy area),
1585-
in [0, 1]. Affects only the added-mass scaling during descent; it does
1586-
not change ``cd_s`` (drag). The default, 0.0432, yields an added-mass
1587-
of 1.0 (“neutral” behavior).
1587+
Geometric porosity of the canopy (ratio of open area to total
1588+
canopy area), in [0, 1]. Affects only the added-mass scaling
1589+
during descent; it does not change `cd_s` (drag). The default
1590+
value of 0.0432 yields an `added_mass_coefficient` of
1591+
approximately 1.0 ("neutral" added-mass behavior).
1592+
drag_coefficient : float, optional
1593+
Drag coefficient of the inflated canopy shape, used only when
1594+
`radius` is not provided. Typical values: 1.4 for hemispherical
1595+
canopies (default), 0.75 for flat circular canopies, 1.5 for
1596+
extended-skirt canopies. Has no effect when `radius` is given.
15881597
15891598
Returns
15901599
-------
15911600
parachute : Parachute
1592-
Parachute containing trigger, sampling_rate, lag, cd_s, noise, radius,
1593-
height, porosity and name. Furthermore, it stores clean_pressure_signal,
1594-
noise_signal and noisyPressureSignal which are filled in during
1595-
Flight simulation.
1601+
Parachute containing trigger, sampling_rate, lag, cd_s, noise,
1602+
radius, drag_coefficient, height, porosity and name. Furthermore,
1603+
it stores clean_pressure_signal, noise_signal and
1604+
noisyPressureSignal which are filled in during Flight simulation.
15961605
"""
15971606
parachute = Parachute(
15981607
name,
@@ -1604,6 +1613,7 @@ def add_parachute(
16041613
radius,
16051614
height,
16061615
porosity,
1616+
drag_coefficient,
16071617
)
16081618
self.parachutes.append(parachute)
16091619
return self.parachutes[-1]

rocketpy/stochastic/stochastic_parachute.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ class StochasticParachute(StochasticModel):
3131
List with the name of the parachute object. This cannot be randomized.
3232
radius : tuple, list, int, float
3333
Radius of the parachute in meters.
34+
drag_coefficient : tuple, list, int, float
35+
Drag coefficient of the inflated canopy shape, used only when
36+
``radius`` is not provided.
3437
height : tuple, list, int, float
3538
Height of the parachute in meters.
3639
porosity : tuple, list, int, float
@@ -46,6 +49,7 @@ def __init__(
4649
lag=None,
4750
noise=None,
4851
radius=None,
52+
drag_coefficient=None,
4953
height=None,
5054
porosity=None,
5155
):
@@ -74,6 +78,9 @@ def __init__(
7478
time-correlation).
7579
radius : tuple, list, int, float
7680
Radius of the parachute in meters.
81+
drag_coefficient : tuple, list, int, float
82+
Drag coefficient of the inflated canopy shape, used only when
83+
``radius`` is not provided.
7784
height : tuple, list, int, float
7885
Height of the parachute in meters.
7986
porosity : tuple, list, int, float
@@ -86,6 +93,7 @@ def __init__(
8693
self.lag = lag
8794
self.noise = noise
8895
self.radius = radius
96+
self.drag_coefficient = drag_coefficient
8997
self.height = height
9098
self.porosity = porosity
9199

@@ -100,6 +108,7 @@ def __init__(
100108
noise=noise,
101109
name=None,
102110
radius=radius,
111+
drag_coefficient=drag_coefficient,
103112
height=height,
104113
porosity=porosity,
105114
)

0 commit comments

Comments
 (0)