Skip to content

Commit df536da

Browse files
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>
1 parent e5fcc93 commit df536da

5 files changed

Lines changed: 211 additions & 39 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,10 @@ 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)]
64+
- BUG: Fix parallel Monte Carlo simulation showing incorrect iteration count [#806](https://github.com/RocketPy-Team/RocketPy/pull/806)
6365
- BUG: Fix CSV column header spacing in FlightDataExporter [#864](https://github.com/RocketPy-Team/RocketPy/issues/864)
6466
- BUG: Fix parallel Monte Carlo simulation showing incorrect iteration count [#806](https://github.com/RocketPy-Team/RocketPy/pull/806)
6567
- BUG: Fix missing titles in roll parameter plots for fin sets [#934](https://github.com/RocketPy-Team/RocketPy/pull/934)

rocketpy/rocket/parachute.py

Lines changed: 68 additions & 28 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,7 +124,8 @@ def __init__(
116124
sampling_rate,
117125
lag=0,
118126
noise=(0, 0, 0),
119-
radius=1.5,
127+
radius=None,
128+
drag_coefficient=1.4,
120129
height=None,
121130
porosity=0.0432,
122131
):
@@ -172,25 +181,68 @@ 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.
189+
drag_coefficient : float, optional
190+
Drag coefficient of the inflated canopy shape, used only when
191+
``radius`` is not provided. It relates the aerodynamic ``cd_s``
192+
to the physical canopy area via
193+
``cd_s = drag_coefficient * pi * radius**2``. Typical values:
194+
195+
- **1.4** — hemispherical canopy (default, NASA SP-8066)
196+
- **0.75** — flat circular canopy
197+
- **1.5** — extended-skirt canopy
198+
199+
Has no effect when ``radius`` is explicitly provided.
178200
height : float, optional
179201
Length of the unique semi-axis (height) of the inflated hemispheroid
180202
parachute. Default value is the radius of the parachute.
181203
Units are in meters.
182204
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).
205+
Geometric porosity of the canopy (ratio of open area to total
206+
canopy area), in [0, 1]. Affects only the added-mass scaling
207+
during descent; it does not change ``cd_s`` (drag). The default
208+
value of 0.0432 is chosen so that the resulting
209+
``added_mass_coefficient`` equals approximately 1.0 ("neutral"
210+
added-mass behavior).
187211
"""
188212
self.name = name
189213
self.cd_s = cd_s
190214
self.trigger = trigger
191215
self.sampling_rate = sampling_rate
192216
self.lag = lag
193217
self.noise = noise
218+
self.drag_coefficient = drag_coefficient
219+
# Estimate radius from cd_s if not provided.
220+
# cd_s = Cd * S = Cd * π * R² => R = sqrt(cd_s / (Cd * π))
221+
if radius is None:
222+
self.radius = np.sqrt(cd_s / (drag_coefficient * np.pi))
223+
else:
224+
self.radius = radius
225+
self.height = height or self.radius
226+
self.porosity = porosity
227+
self.added_mass_coefficient = 1.068 * (
228+
1
229+
- 1.465 * self.porosity
230+
- 0.25975 * self.porosity**2
231+
+ 1.2626 * self.porosity**3
232+
)
233+
234+
self.__init_noise(noise)
235+
self.prints = _ParachutePrints(self)
236+
self.__evaluate_trigger_function(trigger)
237+
238+
def __init_noise(self, noise):
239+
"""Initializes all noise-related attributes.
240+
241+
Parameters
242+
----------
243+
noise : tuple, list
244+
List in the format (mean, standard deviation, time-correlation).
245+
"""
194246
self.noise_signal = [[-1e-6, np.random.normal(noise[0], noise[1])]]
195247
self.noisy_pressure_signal = []
196248
self.clean_pressure_signal = []
@@ -200,26 +252,12 @@ def __init__(
200252
self.clean_pressure_signal_function = Function(0)
201253
self.noisy_pressure_signal_function = Function(0)
202254
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-
213255
alpha, beta = self.noise_corr
214256
self.noise_function = lambda: (
215257
alpha * self.noise_signal[-1][1]
216258
+ beta * np.random.normal(noise[0], noise[1])
217259
)
218260

219-
self.prints = _ParachutePrints(self)
220-
221-
self.__evaluate_trigger_function(trigger)
222-
223261
def __evaluate_trigger_function(self, trigger):
224262
"""This is used to set the triggerfunc attribute that will be used to
225263
interact with the Flight class.
@@ -309,6 +347,7 @@ def to_dict(self, **kwargs):
309347
"lag": self.lag,
310348
"noise": self.noise,
311349
"radius": self.radius,
350+
"drag_coefficient": self.drag_coefficient,
312351
"height": self.height,
313352
"porosity": self.porosity,
314353
}
@@ -341,7 +380,8 @@ def from_dict(cls, data):
341380
sampling_rate=data["sampling_rate"],
342381
lag=data["lag"],
343382
noise=data["noise"],
344-
radius=data.get("radius", 1.5),
383+
radius=data.get("radius", None),
384+
drag_coefficient=data.get("drag_coefficient", 1.4),
345385
height=data.get("height", None),
346386
porosity=data.get("porosity", 0.0432),
347387
)

rocketpy/rocket/rocket.py

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1502,7 +1502,8 @@ def add_parachute(
15021502
sampling_rate=100,
15031503
lag=0,
15041504
noise=(0, 0, 0),
1505-
radius=1.5,
1505+
radius=None,
1506+
drag_coefficient=1.4,
15061507
height=None,
15071508
porosity=0.0432,
15081509
):
@@ -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.
1573+
drag_coefficient : float, optional
1574+
Drag coefficient of the inflated canopy shape, used only when
1575+
`radius` is not provided. Typical values: 1.4 for hemispherical
1576+
canopies (default), 0.75 for flat circular canopies, 1.5 for
1577+
extended-skirt canopies. Has no effect when `radius` is given.
15701578
height : float, optional
15711579
Length of the unique semi-axis (height) of the inflated hemispheroid
15721580
parachute. Default value is the radius of the parachute.
15731581
Units are in meters.
15741582
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).
1583+
Geometric porosity of the canopy (ratio of open area to total
1584+
canopy area), in [0, 1]. Affects only the added-mass scaling
1585+
during descent; it does not change `cd_s` (drag). The default
1586+
value of 0.0432 yields an `added_mass_coefficient` of
1587+
approximately 1.0.
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,
@@ -1593,6 +1602,7 @@ def add_parachute(
15931602
lag,
15941603
noise,
15951604
radius,
1605+
drag_coefficient,
15961606
height,
15971607
porosity,
15981608
)

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
)
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
"""Unit tests for the Parachute class, focusing on the radius and
2+
drag_coefficient parameters introduced in PR #889."""
3+
4+
import numpy as np
5+
import pytest
6+
7+
from rocketpy import Parachute
8+
9+
10+
def _make_parachute(**kwargs):
11+
defaults = dict(
12+
name="test",
13+
cd_s=10.0,
14+
trigger="apogee",
15+
sampling_rate=100,
16+
)
17+
defaults.update(kwargs)
18+
return Parachute(**defaults)
19+
20+
21+
class TestParachuteRadiusEstimation:
22+
"""Tests for auto-computed radius from cd_s and drag_coefficient."""
23+
24+
def test_radius_auto_computed_from_cd_s_default_drag_coefficient(self):
25+
"""When radius is not provided the radius is estimated using the
26+
default drag_coefficient of 1.4 and the formula R = sqrt(cd_s / (Cd * pi))."""
27+
cd_s = 10.0
28+
parachute = _make_parachute(cd_s=cd_s)
29+
expected_radius = np.sqrt(cd_s / (1.4 * np.pi))
30+
assert parachute.radius == pytest.approx(expected_radius, rel=1e-9)
31+
32+
def test_radius_auto_computed_uses_custom_drag_coefficient(self):
33+
"""When drag_coefficient is provided and radius is not, the radius
34+
must be estimated using the given drag_coefficient."""
35+
cd_s = 10.0
36+
custom_cd = 0.75
37+
parachute = _make_parachute(cd_s=cd_s, drag_coefficient=custom_cd)
38+
expected_radius = np.sqrt(cd_s / (custom_cd * np.pi))
39+
assert parachute.radius == pytest.approx(expected_radius, rel=1e-9)
40+
41+
def test_explicit_radius_overrides_estimation(self):
42+
"""When radius is explicitly provided, it must be used directly and
43+
drag_coefficient must be ignored for the radius calculation."""
44+
explicit_radius = 2.5
45+
parachute = _make_parachute(radius=explicit_radius, drag_coefficient=0.5)
46+
assert parachute.radius == explicit_radius
47+
48+
def test_drag_coefficient_stored_on_instance(self):
49+
"""drag_coefficient must be stored as an attribute regardless of
50+
whether radius is provided or not."""
51+
parachute = _make_parachute(drag_coefficient=0.75)
52+
assert parachute.drag_coefficient == 0.75
53+
54+
def test_drag_coefficient_default_is_1_4(self):
55+
"""Default drag_coefficient must be 1.4 for backward compatibility."""
56+
parachute = _make_parachute()
57+
assert parachute.drag_coefficient == pytest.approx(1.4)
58+
59+
def test_drogue_radius_smaller_than_main(self):
60+
"""A drogue (cd_s=1.0) must have a smaller radius than a main (cd_s=10.0)
61+
when using the same drag_coefficient."""
62+
main = _make_parachute(cd_s=10.0)
63+
drogue = _make_parachute(cd_s=1.0)
64+
assert drogue.radius < main.radius
65+
66+
def test_drogue_radius_approximately_0_48(self):
67+
"""For cd_s=1.0 and drag_coefficient=1.4, the estimated radius
68+
must be approximately 0.48 m (fixes the previous hard-coded 1.5 m)."""
69+
drogue = _make_parachute(cd_s=1.0)
70+
assert drogue.radius == pytest.approx(0.476, abs=1e-3)
71+
72+
def test_main_radius_approximately_1_51(self):
73+
"""For cd_s=10.0 and drag_coefficient=1.4, the estimated radius
74+
must be approximately 1.51 m, matching the old hard-coded value."""
75+
main = _make_parachute(cd_s=10.0)
76+
assert main.radius == pytest.approx(1.508, abs=1e-3)
77+
78+
79+
class TestParachuteSerialization:
80+
"""Tests for to_dict / from_dict round-trip including drag_coefficient."""
81+
82+
def test_to_dict_includes_drag_coefficient(self):
83+
"""to_dict must include the drag_coefficient key."""
84+
parachute = _make_parachute(drag_coefficient=0.75)
85+
data = parachute.to_dict()
86+
assert "drag_coefficient" in data
87+
assert data["drag_coefficient"] == 0.75
88+
89+
def test_from_dict_round_trip_preserves_drag_coefficient(self):
90+
"""A Parachute serialized to dict and restored must have the same
91+
drag_coefficient."""
92+
original = _make_parachute(cd_s=5.0, drag_coefficient=0.75)
93+
data = original.to_dict()
94+
restored = Parachute.from_dict(data)
95+
assert restored.drag_coefficient == pytest.approx(0.75)
96+
assert restored.radius == pytest.approx(original.radius, rel=1e-9)
97+
98+
def test_from_dict_defaults_drag_coefficient_to_1_4_when_absent(self):
99+
"""Dicts serialized before drag_coefficient was added (no key) must
100+
fall back to 1.4 for backward compatibility."""
101+
data = dict(
102+
name="legacy",
103+
cd_s=10.0,
104+
trigger="apogee",
105+
sampling_rate=100,
106+
lag=0,
107+
noise=(0, 0, 0),
108+
# no drag_coefficient key — simulates old serialized data
109+
)
110+
parachute = Parachute.from_dict(data)
111+
assert parachute.drag_coefficient == pytest.approx(1.4)

0 commit comments

Comments
 (0)