Skip to content

Commit 6ae5708

Browse files
BUG: Fix hard-coded radius value for parachute added mass calculation (#889)
* Fix hard-coded radius value for parachute added mass calculation Calculate radius from cd_s using a typical hemispherical parachute drag coefficient (1.4) when radius is not explicitly provided. This fixes drift distance calculations for smaller parachutes like drogues. Formula: R = sqrt(cd_s / (Cd * π)) Closes #860 Co-authored-by: Gui-FernandesBR <63590233+Gui-FernandesBR@users.noreply.github.com> Address code review: improve docstrings and add explicit None defaults Co-authored-by: Gui-FernandesBR <63590233+Gui-FernandesBR@users.noreply.github.com> Add CHANGELOG entry for PR #889 Co-authored-by: Gui-FernandesBR <63590233+Gui-FernandesBR@users.noreply.github.com> Update rocket.add_parachute to use radius=None for consistency Changed the default radius from 1.5 to None in the add_parachute method to match the Parachute class behavior. This ensures consistent automatic radius calculation from cd_s across both APIs. Co-authored-by: Gui-FernandesBR <63590233+Gui-FernandesBR@users.noreply.github.com> Refactor Parachute class to remove hard-coded radius value and introduce drag_coefficient parameter for radius estimation Fix hard-coded radius value for parachute added mass calculation Calculate radius from cd_s using a typical hemispherical parachute drag coefficient (1.4) when radius is not explicitly provided. This fixes drift distance calculations for smaller parachutes like drogues. Formula: R = sqrt(cd_s / (Cd * π)) Closes #860 Co-authored-by: Gui-FernandesBR <63590233+Gui-FernandesBR@users.noreply.github.com> Add CHANGELOG entry for PR #889 Co-authored-by: Gui-FernandesBR <63590233+Gui-FernandesBR@users.noreply.github.com> Refactor Parachute class to remove hard-coded radius value and introduce drag_coefficient parameter for radius estimation MNT: Extract noise initialization to fix pylint too-many-statements in Parachute.__init__ Co-authored-by: Gui-FernandesBR <63590233+Gui-FernandesBR@users.noreply.github.com> * Refactor environment method access in controller test for clarity * fix pylint * fix comments * avoid breaking change with drag_coefficient * reafactors Parachute.__init__ method * fix tests --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Gui-FernandesBR <63590233+Gui-FernandesBR@users.noreply.github.com> Co-authored-by: Gui-FernandesBR <guilherme_fernandes@usp.br>
1 parent e5fcc93 commit 6ae5708

6 files changed

Lines changed: 277 additions & 86 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ Attention: The newest changes should be on top -->
5858

5959
### Fixed
6060

61+
- BUG: Fix hard-coded radius value for parachute added mass calculation [#889](https://github.com/RocketPy-Team/RocketPy/pull/889)
6162
- DOC: Fix documentation build [#908](https://github.com/RocketPy-Team/RocketPy/pull/908)
6263
- BUG: energy_data plot not working for 3 dof sims [[#906](https://github.com/RocketPy-Team/RocketPy/issues/906)]
6364
- BUG: Fix CSV column header spacing in FlightDataExporter [#864](https://github.com/RocketPy-Team/RocketPy/issues/864)

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
@@ -1502,9 +1502,10 @@ def add_parachute(
15021502
sampling_rate=100,
15031503
lag=0,
15041504
noise=(0, 0, 0),
1505-
radius=1.5,
1505+
radius=None,
15061506
height=None,
15071507
porosity=0.0432,
1508+
drag_coefficient=1.4,
15081509
):
15091510
"""Creates a new parachute, storing its parameters such as
15101511
opening delay, drag coefficients and trigger function.
@@ -1564,26 +1565,34 @@ def add_parachute(
15641565
passed to the trigger function. Default value is (0, 0, 0). Units
15651566
are in pascal.
15661567
radius : float, optional
1567-
Length of the non-unique semi-axis (radius) of the inflated hemispheroid
1568-
parachute. Default value is 1.5.
1568+
Length of the non-unique semi-axis (radius) of the inflated
1569+
hemispheroid parachute. If not provided, it is estimated from
1570+
`cd_s` and `drag_coefficient` using:
1571+
`radius = sqrt(cd_s / (drag_coefficient * pi))`.
15691572
Units are in meters.
15701573
height : float, optional
15711574
Length of the unique semi-axis (height) of the inflated hemispheroid
15721575
parachute. Default value is the radius of the parachute.
15731576
Units are in meters.
15741577
porosity : float, optional
1575-
Geometric porosity of the canopy (ratio of open area to total canopy area),
1576-
in [0, 1]. Affects only the added-mass scaling during descent; it does
1577-
not change ``cd_s`` (drag). The default, 0.0432, yields an added-mass
1578-
of 1.0 (“neutral” behavior).
1578+
Geometric porosity of the canopy (ratio of open area to total
1579+
canopy area), in [0, 1]. Affects only the added-mass scaling
1580+
during descent; it does not change `cd_s` (drag). The default
1581+
value of 0.0432 yields an `added_mass_coefficient` of
1582+
approximately 1.0 ("neutral" added-mass behavior).
1583+
drag_coefficient : float, optional
1584+
Drag coefficient of the inflated canopy shape, used only when
1585+
`radius` is not provided. Typical values: 1.4 for hemispherical
1586+
canopies (default), 0.75 for flat circular canopies, 1.5 for
1587+
extended-skirt canopies. Has no effect when `radius` is given.
15791588
15801589
Returns
15811590
-------
15821591
parachute : Parachute
1583-
Parachute containing trigger, sampling_rate, lag, cd_s, noise, radius,
1584-
height, porosity and name. Furthermore, it stores clean_pressure_signal,
1585-
noise_signal and noisyPressureSignal which are filled in during
1586-
Flight simulation.
1592+
Parachute containing trigger, sampling_rate, lag, cd_s, noise,
1593+
radius, drag_coefficient, height, porosity and name. Furthermore,
1594+
it stores clean_pressure_signal, noise_signal and
1595+
noisyPressureSignal which are filled in during Flight simulation.
15871596
"""
15881597
parachute = Parachute(
15891598
name,
@@ -1595,6 +1604,7 @@ def add_parachute(
15951604
radius,
15961605
height,
15971606
porosity,
1607+
drag_coefficient,
15981608
)
15991609
self.parachutes.append(parachute)
16001610
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)