Skip to content

Commit 531cf38

Browse files
FelipeAlmeidaUSPGui-FernandesBR
authored andcommitted
ENH: implement parachute opening shock force estimation
1 parent eaa9181 commit 531cf38

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
@@ -773,6 +773,22 @@ def __simulate(self, verbose):
773773
self.y_sol,
774774
self.sensors,
775775
):
776+
777+
# Calculate opening shock force
778+
opening_altitude = self.y_sol[2]
779+
opening_density = self.env.density(opening_altitude)
780+
opening_velocity = (
781+
(self.y_sol[3]) ** 2
782+
+ (self.y_sol[4]) ** 2
783+
+ (self.y_sol[5]) ** 2
784+
) ** 0.5
785+
786+
parachute.opening_shock_force = (
787+
parachute.calculate_opening_shock(
788+
opening_density, opening_velocity
789+
)
790+
)
791+
776792
# Remove parachute from flight parachutes
777793
self.parachutes.remove(parachute)
778794
# Create phase for time after detection and before inflation
@@ -800,8 +816,7 @@ def __simulate(self, verbose):
800816
lambda self, parachute_porosity=parachute.porosity: setattr(
801817
self, "parachute_porosity", parachute_porosity
802818
),
803-
lambda self,
804-
added_mass_coefficient=parachute.added_mass_coefficient: setattr(
819+
lambda self, added_mass_coefficient=parachute.added_mass_coefficient: setattr(
805820
self,
806821
"parachute_added_mass_coefficient",
807822
added_mass_coefficient,
@@ -1048,30 +1063,25 @@ def __simulate(self, verbose):
10481063
i += 1
10491064
# Create flight phase for time after inflation
10501065
callbacks = [
1051-
lambda self,
1052-
parachute_cd_s=parachute.cd_s: setattr(
1066+
lambda self, parachute_cd_s=parachute.cd_s: setattr(
10531067
self, "parachute_cd_s", parachute_cd_s
10541068
),
1055-
lambda self,
1056-
parachute_radius=parachute.radius: setattr(
1069+
lambda self, parachute_radius=parachute.radius: setattr(
10571070
self,
10581071
"parachute_radius",
10591072
parachute_radius,
10601073
),
1061-
lambda self,
1062-
parachute_height=parachute.height: setattr(
1074+
lambda self, parachute_height=parachute.height: setattr(
10631075
self,
10641076
"parachute_height",
10651077
parachute_height,
10661078
),
1067-
lambda self,
1068-
parachute_porosity=parachute.porosity: setattr(
1079+
lambda self, parachute_porosity=parachute.porosity: setattr(
10691080
self,
10701081
"parachute_porosity",
10711082
parachute_porosity,
10721083
),
1073-
lambda self,
1074-
added_mass_coefficient=parachute.added_mass_coefficient: setattr(
1084+
lambda self, added_mass_coefficient=parachute.added_mass_coefficient: setattr(
10751085
self,
10761086
"parachute_added_mass_coefficient",
10771087
added_mass_coefficient,
@@ -1601,7 +1611,9 @@ def udot_rail2(self, t, u, post_processing=False): # pragma: no cover
16011611
# Hey! We will finish this function later, now we just can use u_dot
16021612
return self.u_dot_generalized(t, u, post_processing=post_processing)
16031613

1604-
def u_dot(self, t, u, post_processing=False): # pylint: disable=too-many-locals,too-many-statements
1614+
def u_dot(
1615+
self, t, u, post_processing=False
1616+
): # pylint: disable=too-many-locals,too-many-statements
16051617
"""Calculates derivative of u state vector with respect to time
16061618
when rocket is flying in 6 DOF motion during ascent out of rail
16071619
and descent without parachute.
@@ -2147,7 +2159,9 @@ def u_dot_generalized_3dof(self, t, u, post_processing=False):
21472159

21482160
return u_dot
21492161

2150-
def u_dot_generalized(self, t, u, post_processing=False): # pylint: disable=too-many-locals,too-many-statements
2162+
def u_dot_generalized(
2163+
self, t, u, post_processing=False
2164+
): # pylint: disable=too-many-locals,too-many-statements
21512165
"""Calculates derivative of u state vector with respect to time when the
21522166
rocket is flying in 6 DOF motion in space and significant mass variation
21532167
effects exist. Typical flight phases include powered ascent after launch
@@ -3982,9 +3996,7 @@ def add(self, flight_phase, index=None): # TODO: quite complex method
39823996
new_index = (
39833997
index - 1
39843998
if flight_phase.t < previous_phase.t
3985-
else index + 1
3986-
if flight_phase.t > next_phase.t
3987-
else index
3999+
else index + 1 if flight_phase.t > next_phase.t else index
39884000
)
39894001
flight_phase.t += adjust
39904002
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)