Skip to content

Commit cb3b0bc

Browse files
MatanskiArthurJWH
andauthored
ENH: Add opening shock force estimation to Parachute class (#1050)
Adds an opening_shock_coefficient parameter and a calculate_opening_shock_force method to estimate the peak transient force during parachute inflation, following the simplified model in Knacke's Parachute Recovery Systems Design Manual (1992, Section 5.5). Closes #161 Co-authored-by: ArthurJWH <167456467+ArthurJWH@users.noreply.github.com>
1 parent 1691119 commit cb3b0bc

2 files changed

Lines changed: 117 additions & 0 deletions

File tree

‎rocketpy/rocket/parachute.py‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,11 @@ class Parachute:
123123
Parachute.added_mass_coefficient : float
124124
Coefficient used to calculate the added-mass due to dragged air. It is
125125
calculated from the porosity of the parachute.
126+
Parachute.opening_shock_coefficient : float
127+
Empirical coefficient (commonly noted Cx) used to estimate the peak
128+
transient force experienced during parachute inflation. Typical
129+
values range from 1.2 to 2.0 depending on the deployment method and
130+
canopy type. Default value is 1.5.
126131
"""
127132

128133
def __init__(
@@ -137,6 +142,7 @@ def __init__(
137142
height=None,
138143
porosity=0.0432,
139144
drag_coefficient=1.4,
145+
opening_shock_coefficient=1.5,
140146
):
141147
"""Initializes Parachute class.
142148
@@ -217,6 +223,12 @@ def __init__(
217223
- **1.5** — extended-skirt canopy
218224
219225
Has no effect when ``radius`` is explicitly provided.
226+
opening_shock_coefficient : float, optional
227+
Empirical coefficient (commonly noted Cx) used to estimate the
228+
peak transient force experienced during parachute inflation via
229+
:meth:`calculate_opening_shock_force`. Typical values range from
230+
1.2 to 2.0 depending on the deployment method and canopy type.
231+
Default value is 1.5.
220232
"""
221233

222234
# Save arguments as attributes
@@ -228,6 +240,7 @@ def __init__(
228240
self.noise = noise
229241
self.drag_coefficient = drag_coefficient
230242
self.porosity = porosity
243+
self.opening_shock_coefficient = opening_shock_coefficient
231244

232245
# Initialize derived attributes
233246
self.radius = self.__resolve_radius(radius, cd_s, drag_coefficient)
@@ -259,6 +272,39 @@ def __compute_added_mass_coefficient(self, porosity):
259272
1 - 1.465 * porosity - 0.25975 * porosity**2 + 1.2626 * porosity**3
260273
)
261274

275+
def calculate_opening_shock_force(self, air_density, velocity):
276+
"""Estimates the peak transient force experienced by the recovery
277+
hardware during parachute inflation (the "opening shock").
278+
279+
The estimate follows the simplified model described in Knacke's
280+
"Parachute Recovery Systems Design Manual" (1992, Section 5.5):
281+
282+
.. math::
283+
284+
F_0 = C_x \\cdot C_{d} S \\cdot q
285+
286+
where :math:`C_x` is the ``opening_shock_coefficient``,
287+
:math:`C_{d} S` is the parachute's ``cd_s``, and :math:`q` is the
288+
dynamic pressure (:math:`q = \\tfrac{1}{2} \\rho V^2`) at the instant
289+
the canopy begins to inflate.
290+
291+
Parameters
292+
----------
293+
air_density : float
294+
Freestream air density, in kg/m^3, at the moment of parachute
295+
deployment.
296+
velocity : float
297+
Freestream velocity relative to the rocket, in m/s, at the moment
298+
of parachute deployment.
299+
300+
Returns
301+
-------
302+
float
303+
Estimated peak opening shock force, in Newtons.
304+
"""
305+
dynamic_pressure = 0.5 * air_density * velocity**2
306+
return self.opening_shock_coefficient * self.cd_s * dynamic_pressure
307+
262308
def __init_noise(self, noise):
263309
"""Initializes all noise-related attributes.
264310
@@ -431,6 +477,7 @@ def to_dict(self, **kwargs):
431477
"drag_coefficient": self.drag_coefficient,
432478
"height": self.height,
433479
"porosity": self.porosity,
480+
"opening_shock_coefficient": self.opening_shock_coefficient,
434481
}
435482

436483
if kwargs.get("include_outputs", False):
@@ -465,6 +512,7 @@ def from_dict(cls, data):
465512
drag_coefficient=data.get("drag_coefficient", 1.4),
466513
height=data.get("height", None),
467514
porosity=data.get("porosity", 0.0432),
515+
opening_shock_coefficient=data.get("opening_shock_coefficient", 1.5),
468516
)
469517

470518
return parachute

‎tests/unit/rocket/test_parachute.py‎

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,75 @@ def test_from_dict_defaults_drag_coefficient_to_1_4_when_absent(self):
111111
assert parachute.drag_coefficient == pytest.approx(1.4)
112112

113113

114+
class TestParachuteOpeningShockForce:
115+
"""Tests for the opening_shock_coefficient parameter and
116+
calculate_opening_shock_force method, addressing issue #161."""
117+
118+
def test_opening_shock_coefficient_default_is_1_5(self):
119+
"""Default opening_shock_coefficient must be 1.5."""
120+
parachute = _make_parachute()
121+
assert parachute.opening_shock_coefficient == pytest.approx(1.5)
122+
123+
def test_opening_shock_coefficient_stored_on_instance(self):
124+
"""opening_shock_coefficient must be stored as given."""
125+
parachute = _make_parachute(opening_shock_coefficient=1.8)
126+
assert parachute.opening_shock_coefficient == pytest.approx(1.8)
127+
128+
def test_calculate_opening_shock_force_matches_formula(self):
129+
"""calculate_opening_shock_force must return
130+
Cx * cd_s * 0.5 * rho * V^2."""
131+
cd_s = 10.0
132+
cx = 1.6
133+
air_density = 1.225
134+
velocity = 50.0
135+
parachute = _make_parachute(cd_s=cd_s, opening_shock_coefficient=cx)
136+
137+
expected_force = cx * cd_s * 0.5 * air_density * velocity**2
138+
assert parachute.calculate_opening_shock_force(
139+
air_density, velocity
140+
) == pytest.approx(expected_force, rel=1e-9)
141+
142+
def test_calculate_opening_shock_force_scales_with_velocity_squared(self):
143+
"""Doubling velocity must quadruple the opening shock force."""
144+
parachute = _make_parachute()
145+
force_v = parachute.calculate_opening_shock_force(1.225, 40.0)
146+
force_2v = parachute.calculate_opening_shock_force(1.225, 80.0)
147+
assert force_2v == pytest.approx(4 * force_v, rel=1e-9)
148+
149+
def test_calculate_opening_shock_force_zero_velocity_is_zero(self):
150+
"""No dynamic pressure means no opening shock force."""
151+
parachute = _make_parachute()
152+
assert parachute.calculate_opening_shock_force(1.225, 0.0) == pytest.approx(0.0)
153+
154+
def test_to_dict_includes_opening_shock_coefficient(self):
155+
"""to_dict must include the opening_shock_coefficient key."""
156+
parachute = _make_parachute(opening_shock_coefficient=1.8)
157+
data = parachute.to_dict()
158+
assert "opening_shock_coefficient" in data
159+
assert data["opening_shock_coefficient"] == 1.8
160+
161+
def test_from_dict_round_trip_preserves_opening_shock_coefficient(self):
162+
"""A Parachute serialized to dict and restored must have the same
163+
opening_shock_coefficient."""
164+
original = _make_parachute(cd_s=5.0, opening_shock_coefficient=1.8)
165+
data = original.to_dict()
166+
restored = Parachute.from_dict(data)
167+
assert restored.opening_shock_coefficient == pytest.approx(1.8)
168+
169+
def test_from_dict_defaults_opening_shock_coefficient_to_1_5_when_absent(self):
170+
"""Dicts serialized before opening_shock_coefficient was added (no
171+
key) must fall back to 1.5 for backward compatibility."""
172+
data = {
173+
"name": "legacy",
174+
"cd_s": 10.0,
175+
"trigger": "apogee",
176+
"sampling_rate": 100,
177+
"lag": 0,
178+
"noise": (0, 0, 0),
179+
# no opening_shock_coefficient key — simulates old serialized data
180+
}
181+
parachute = Parachute.from_dict(data)
182+
assert parachute.opening_shock_coefficient == pytest.approx(1.5)
114183
@pytest.mark.parametrize(
115184
"trigger, expects_udot",
116185
[

0 commit comments

Comments
 (0)