Skip to content

Commit 4ca1593

Browse files
ENH: implement parachute opening shock force estimation
1 parent afb3e3e commit 4ca1593

File tree

4 files changed

+166
-25
lines changed

4 files changed

+166
-25
lines changed

rocketpy/rocket/parachute.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ def __init__(
119119
radius=1.5,
120120
height=None,
121121
porosity=0.0432,
122+
opening_shock_coefficient=1.6, # TODO: analyse methods to calculate Cx and X1
122123
):
123124
"""Initializes Parachute class.
124125
@@ -184,6 +185,14 @@ def __init__(
184185
in [0, 1]. Affects only the added-mass scaling during descent; it does
185186
not change ``cd_s`` (drag). The default, 0.0432, yields an added-mass
186187
of 1.0 (“neutral” behavior).
188+
opening_shock_coefficient : float, optional
189+
Coefficient used to calculate the parachute's opening shock force.
190+
Combined factor (Cx * X1) accounting for canopy shape and mass ratio.
191+
Default value set for 1.6, can be calculated by geometrical and porosity
192+
relations.
193+
opening_shock_force : float, optional
194+
Parachute's estimated opening shock force.
195+
Calculated based on Knacke, T. W. (1992). Parachute Recovery Systems Design Manual.
187196
"""
188197
self.name = name
189198
self.cd_s = cd_s
@@ -203,6 +212,8 @@ def __init__(
203212
self.radius = radius
204213
self.height = height or radius
205214
self.porosity = porosity
215+
self.opening_shock_coefficient = opening_shock_coefficient
216+
self.opening_shock_force = None
206217
self.added_mass_coefficient = 1.068 * (
207218
1
208219
- 1.465 * self.porosity
@@ -346,3 +357,28 @@ def from_dict(cls, data):
346357
)
347358

348359
return parachute
360+
361+
def calculate_opening_shock(self, density, velocity):
362+
"""
363+
Calculates the opening shock force based on Knacke's formula.
364+
365+
Fo = Cx * X1 * q * S * Cd
366+
Knacke, T. W. (1992). Parachute Recovery Systems Design Manual.(Page 5-50)
367+
368+
Parameters
369+
----------
370+
density: float
371+
Air density during the parachute's opening (kg/m^3).
372+
velocity: float
373+
Rocket velocity relative to the air during the parachute's opening (m/s).
374+
375+
Returns
376+
-------
377+
force: float
378+
The estimated peak opening shock force during the parachute's opening (N).
379+
"""
380+
381+
dynamic_pressure = 0.5 * density * (velocity**2)
382+
force = self.opening_shock_coefficient * dynamic_pressure * self.cd_s
383+
384+
return force

rocketpy/simulation/flight.py

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -763,6 +763,22 @@ def __simulate(self, verbose):
763763
self.y_sol,
764764
self.sensors,
765765
):
766+
767+
# Calculate opening shock force
768+
opening_altitude = self.y_sol[2]
769+
opening_density = self.env.density(opening_altitude)
770+
opening_velocity = (
771+
(self.y_sol[3]) ** 2
772+
+ (self.y_sol[4]) ** 2
773+
+ (self.y_sol[5]) ** 2
774+
) ** 0.5
775+
776+
parachute.opening_shock_force = (
777+
parachute.calculate_opening_shock(
778+
opening_density, opening_velocity
779+
)
780+
)
781+
766782
# Remove parachute from flight parachutes
767783
self.parachutes.remove(parachute)
768784
# Create phase for time after detection and before inflation
@@ -790,8 +806,7 @@ def __simulate(self, verbose):
790806
lambda self, parachute_porosity=parachute.porosity: setattr(
791807
self, "parachute_porosity", parachute_porosity
792808
),
793-
lambda self,
794-
added_mass_coefficient=parachute.added_mass_coefficient: setattr(
809+
lambda self, added_mass_coefficient=parachute.added_mass_coefficient: setattr(
795810
self,
796811
"parachute_added_mass_coefficient",
797812
added_mass_coefficient,
@@ -1038,30 +1053,25 @@ def __simulate(self, verbose):
10381053
i += 1
10391054
# Create flight phase for time after inflation
10401055
callbacks = [
1041-
lambda self,
1042-
parachute_cd_s=parachute.cd_s: setattr(
1056+
lambda self, parachute_cd_s=parachute.cd_s: setattr(
10431057
self, "parachute_cd_s", parachute_cd_s
10441058
),
1045-
lambda self,
1046-
parachute_radius=parachute.radius: setattr(
1059+
lambda self, parachute_radius=parachute.radius: setattr(
10471060
self,
10481061
"parachute_radius",
10491062
parachute_radius,
10501063
),
1051-
lambda self,
1052-
parachute_height=parachute.height: setattr(
1064+
lambda self, parachute_height=parachute.height: setattr(
10531065
self,
10541066
"parachute_height",
10551067
parachute_height,
10561068
),
1057-
lambda self,
1058-
parachute_porosity=parachute.porosity: setattr(
1069+
lambda self, parachute_porosity=parachute.porosity: setattr(
10591070
self,
10601071
"parachute_porosity",
10611072
parachute_porosity,
10621073
),
1063-
lambda self,
1064-
added_mass_coefficient=parachute.added_mass_coefficient: setattr(
1074+
lambda self, added_mass_coefficient=parachute.added_mass_coefficient: setattr(
10651075
self,
10661076
"parachute_added_mass_coefficient",
10671077
added_mass_coefficient,
@@ -1482,7 +1492,9 @@ def udot_rail2(self, t, u, post_processing=False): # pragma: no cover
14821492
# Hey! We will finish this function later, now we just can use u_dot
14831493
return self.u_dot_generalized(t, u, post_processing=post_processing)
14841494

1485-
def u_dot(self, t, u, post_processing=False): # pylint: disable=too-many-locals,too-many-statements
1495+
def u_dot(
1496+
self, t, u, post_processing=False
1497+
): # pylint: disable=too-many-locals,too-many-statements
14861498
"""Calculates derivative of u state vector with respect to time
14871499
when rocket is flying in 6 DOF motion during ascent out of rail
14881500
and descent without parachute.
@@ -1920,7 +1932,9 @@ def u_dot_generalized_3dof(self, t, u, post_processing=False):
19201932

19211933
return u_dot
19221934

1923-
def u_dot_generalized(self, t, u, post_processing=False): # pylint: disable=too-many-locals,too-many-statements
1935+
def u_dot_generalized(
1936+
self, t, u, post_processing=False
1937+
): # pylint: disable=too-many-locals,too-many-statements
19241938
"""Calculates derivative of u state vector with respect to time when the
19251939
rocket is flying in 6 DOF motion in space and significant mass variation
19261940
effects exist. Typical flight phases include powered ascent after launch
@@ -3741,9 +3755,7 @@ def add(self, flight_phase, index=None): # TODO: quite complex method
37413755
new_index = (
37423756
index - 1
37433757
if flight_phase.t < previous_phase.t
3744-
else index + 1
3745-
if flight_phase.t > next_phase.t
3746-
else index
3758+
else index + 1 if flight_phase.t > next_phase.t else index
37473759
)
37483760
flight_phase.t += adjust
37493761
self.add(flight_phase, new_index)

tests/integration/simulation/test_flight.py

Lines changed: 61 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import numpy as np
55
import pytest
66

7-
from rocketpy import Flight
7+
from rocketpy import Flight, Parachute
88

99
plt.rcParams.update({"figure.max_open_warning": 0})
1010

@@ -69,7 +69,9 @@ def test_all_info_different_solvers(
6969

7070

7171
@patch("matplotlib.pyplot.show")
72-
def test_hybrid_motor_flight(mock_show, flight_calisto_hybrid_modded): # pylint: disable=unused-argument
72+
def test_hybrid_motor_flight(
73+
mock_show, flight_calisto_hybrid_modded
74+
): # pylint: disable=unused-argument
7375
"""Test the flight of a rocket with a hybrid motor. This test only validates
7476
that a flight simulation can be performed with a hybrid motor; it does not
7577
validate the results.
@@ -85,7 +87,9 @@ def test_hybrid_motor_flight(mock_show, flight_calisto_hybrid_modded): # pylint
8587

8688

8789
@patch("matplotlib.pyplot.show")
88-
def test_liquid_motor_flight(mock_show, flight_calisto_liquid_modded): # pylint: disable=unused-argument
90+
def test_liquid_motor_flight(
91+
mock_show, flight_calisto_liquid_modded
92+
): # pylint: disable=unused-argument
8993
"""Test the flight of a rocket with a liquid motor. This test only validates
9094
that a flight simulation can be performed with a liquid motor; it does not
9195
validate the results.
@@ -102,7 +106,9 @@ def test_liquid_motor_flight(mock_show, flight_calisto_liquid_modded): # pylint
102106

103107
@pytest.mark.slow
104108
@patch("matplotlib.pyplot.show")
105-
def test_time_overshoot(mock_show, calisto_robust, example_spaceport_env): # pylint: disable=unused-argument
109+
def test_time_overshoot(
110+
mock_show, calisto_robust, example_spaceport_env
111+
): # pylint: disable=unused-argument
106112
"""Test the time_overshoot parameter of the Flight class. This basically
107113
calls the all_info() method for a simulation without time_overshoot and
108114
checks if it returns None. It is not testing if the values are correct,
@@ -131,7 +137,9 @@ def test_time_overshoot(mock_show, calisto_robust, example_spaceport_env): # py
131137

132138

133139
@patch("matplotlib.pyplot.show")
134-
def test_simpler_parachute_triggers(mock_show, example_plain_env, calisto_robust): # pylint: disable=unused-argument
140+
def test_simpler_parachute_triggers(
141+
mock_show, example_plain_env, calisto_robust
142+
): # pylint: disable=unused-argument
135143
"""Tests different types of parachute triggers. This is important to ensure
136144
the code is working as intended, since the parachute triggers can have very
137145
different format definitions. It will add 3 parachutes using different
@@ -273,7 +281,9 @@ def test_eccentricity_on_flight( # pylint: disable=unused-argument
273281

274282

275283
@patch("matplotlib.pyplot.show")
276-
def test_air_brakes_flight(mock_show, flight_calisto_air_brakes): # pylint: disable=unused-argument
284+
def test_air_brakes_flight(
285+
mock_show, flight_calisto_air_brakes
286+
): # pylint: disable=unused-argument
277287
"""Test the flight of a rocket with air brakes. This test only validates
278288
that a flight simulation can be performed with air brakes; it does not
279289
validate the results.
@@ -294,7 +304,9 @@ def test_air_brakes_flight(mock_show, flight_calisto_air_brakes): # pylint: dis
294304

295305

296306
@patch("matplotlib.pyplot.show")
297-
def test_initial_solution(mock_show, example_plain_env, calisto_robust): # pylint: disable=unused-argument
307+
def test_initial_solution(
308+
mock_show, example_plain_env, calisto_robust
309+
): # pylint: disable=unused-argument
298310
"""Tests the initial_solution option of the Flight class. This test simply
299311
simulates the flight using the initial_solution option and checks if the
300312
all_info method returns None.
@@ -339,7 +351,9 @@ def test_initial_solution(mock_show, example_plain_env, calisto_robust): # pyli
339351

340352

341353
@patch("matplotlib.pyplot.show")
342-
def test_empty_motor_flight(mock_show, example_plain_env, calisto_motorless): # pylint: disable=unused-argument
354+
def test_empty_motor_flight(
355+
mock_show, example_plain_env, calisto_motorless
356+
): # pylint: disable=unused-argument
343357
flight = Flight(
344358
rocket=calisto_motorless,
345359
environment=example_plain_env,
@@ -438,3 +452,42 @@ def test_rocket_csys_equivalence(
438452
flight_calisto_robust.initial_solution,
439453
flight_calisto_nose_to_tail_robust.initial_solution,
440454
)
455+
456+
457+
# TODO: fix the issues on this test and debug shock analysis
458+
def test_opening_shock_recorded_during_flight(calisto, example_plain_env):
459+
"""
460+
Testing if the opening shock is being saved correctly during simulations.
461+
"""
462+
# Defining test parachute
463+
calisto.parachutes = []
464+
465+
target_coeff = 1.75
466+
main_chute = Parachute(
467+
name="Main Test",
468+
cd_s=5.0,
469+
trigger="apogee",
470+
sampling_rate=100,
471+
opening_shock_coefficient=target_coeff,
472+
)
473+
474+
calisto.parachutes.append(main_chute)
475+
476+
# Simulating
477+
flight = Flight(
478+
rocket=calisto,
479+
environment=example_plain_env,
480+
rail_length=5,
481+
inclination=85,
482+
heading=0,
483+
terminate_on_apogee=False,
484+
)
485+
486+
# Analysing results
487+
assert len(flight.parachute_events) > 0, "No parachute event registered!"
488+
489+
event_time, flown_chute = flight.parachute_events[0]
490+
491+
assert flown_chute.opening_shock_force is not None
492+
assert flown_chute.opening_shock_force > 0
493+
assert flown_chute.opening_shock_coefficient == target_coeff

tests/unit/test_parachute_shock.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
from rocketpy import Parachute
2+
import pytest
3+
4+
5+
# TODO: Analyse if, instead of a new file, test could be included in a existing one
6+
def test_knacke_opening_shock_example():
7+
"""
8+
Verifies the opening shock force calculation comparing to a textbook example.
9+
10+
Reference: Knacke, T. W. (1992). Parachute Recovery Systems Design Manual.
11+
(Page 5-51, Figure 5-21)
12+
"""
13+
# Setup
14+
knacke_cd = 0.49
15+
knacke_s = 17.76 # m^2
16+
knacke_cx = 1.088
17+
knacke_x1 = 1.0
18+
19+
knacke_density = 0.458 # kg/m^3
20+
knacke_velocity = 123.2 # m/s
21+
22+
# Expected result
23+
knacke_force = 32916.8 # N
24+
25+
# Defining example parachute
26+
parachute = Parachute(
27+
name="B-47 Test Chute",
28+
cd_s=knacke_cd * knacke_s,
29+
trigger="apogee",
30+
sampling_rate=100,
31+
opening_shock_coefficient=knacke_cx * knacke_x1,
32+
)
33+
34+
# Calculating the shock force
35+
calculated_force = parachute.calculate_opening_shock(
36+
knacke_density, knacke_velocity
37+
)
38+
39+
# Analysing results
40+
assert calculated_force == pytest.approx(knacke_force, rel=1e-2)

0 commit comments

Comments
 (0)