Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ Attention: The newest changes should be on top -->

### Added

-
- ENH: Add opening_shock_coefficient parameter and calculate_opening_shock_force method to Parachute class. [#161](https://github.com/RocketPy-Team/RocketPy/issues/161)

### Changed

Expand Down
48 changes: 48 additions & 0 deletions rocketpy/rocket/parachute.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,11 @@ class Parachute:
Parachute.added_mass_coefficient : float
Coefficient used to calculate the added-mass due to dragged air. It is
calculated from the porosity of the parachute.
Parachute.opening_shock_coefficient : float
Empirical coefficient (commonly noted Cx) used to estimate the peak
transient force experienced during parachute inflation. Typical
values range from 1.2 to 2.0 depending on the deployment method and
canopy type. Default value is 1.5.
"""

def __init__(
Expand All @@ -128,6 +133,7 @@ def __init__(
height=None,
porosity=0.0432,
drag_coefficient=1.4,
opening_shock_coefficient=1.5,
):
"""Initializes Parachute class.

Expand Down Expand Up @@ -208,6 +214,12 @@ def __init__(
- **1.5** — extended-skirt canopy

Has no effect when ``radius`` is explicitly provided.
opening_shock_coefficient : float, optional
Empirical coefficient (commonly noted Cx) used to estimate the
peak transient force experienced during parachute inflation via
:meth:`calculate_opening_shock_force`. Typical values range from
1.2 to 2.0 depending on the deployment method and canopy type.
Default value is 1.5.
"""

# Save arguments as attributes
Expand All @@ -219,6 +231,7 @@ def __init__(
self.noise = noise
self.drag_coefficient = drag_coefficient
self.porosity = porosity
self.opening_shock_coefficient = opening_shock_coefficient

# Initialize derived attributes
self.radius = self.__resolve_radius(radius, cd_s, drag_coefficient)
Expand Down Expand Up @@ -250,6 +263,39 @@ def __compute_added_mass_coefficient(self, porosity):
1 - 1.465 * porosity - 0.25975 * porosity**2 + 1.2626 * porosity**3
)

def calculate_opening_shock_force(self, air_density, velocity):
"""Estimates the peak transient force experienced by the recovery
hardware during parachute inflation (the "opening shock").

The estimate follows the simplified model described in Knacke's
"Parachute Recovery Systems Design Manual" (1992, Section 5.5):

.. math::

F_0 = C_x \\cdot C_{d} S \\cdot q

where :math:`C_x` is the ``opening_shock_coefficient``,
:math:`C_{d} S` is the parachute's ``cd_s``, and :math:`q` is the
dynamic pressure (:math:`q = \\tfrac{1}{2} \\rho V^2`) at the instant
the canopy begins to inflate.

Parameters
----------
air_density : float
Freestream air density, in kg/m^3, at the moment of parachute
deployment.
velocity : float
Freestream velocity relative to the rocket, in m/s, at the moment
of parachute deployment.

Returns
-------
float
Estimated peak opening shock force, in Newtons.
"""
dynamic_pressure = 0.5 * air_density * velocity**2
return self.opening_shock_coefficient * self.cd_s * dynamic_pressure

def __init_noise(self, noise):
"""Initializes all noise-related attributes.

Expand Down Expand Up @@ -369,6 +415,7 @@ def to_dict(self, **kwargs):
"drag_coefficient": self.drag_coefficient,
"height": self.height,
"porosity": self.porosity,
"opening_shock_coefficient": self.opening_shock_coefficient,
}

if kwargs.get("include_outputs", False):
Expand Down Expand Up @@ -403,6 +450,7 @@ def from_dict(cls, data):
drag_coefficient=data.get("drag_coefficient", 1.4),
height=data.get("height", None),
porosity=data.get("porosity", 0.0432),
opening_shock_coefficient=data.get("opening_shock_coefficient", 1.5),
)

return parachute
71 changes: 71 additions & 0 deletions tests/unit/rocket/test_parachute.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,3 +109,74 @@ def test_from_dict_defaults_drag_coefficient_to_1_4_when_absent(self):
}
parachute = Parachute.from_dict(data)
assert parachute.drag_coefficient == pytest.approx(1.4)


class TestParachuteOpeningShockForce:
"""Tests for the opening_shock_coefficient parameter and
calculate_opening_shock_force method, addressing issue #161."""

def test_opening_shock_coefficient_default_is_1_5(self):
"""Default opening_shock_coefficient must be 1.5."""
parachute = _make_parachute()
assert parachute.opening_shock_coefficient == pytest.approx(1.5)

def test_opening_shock_coefficient_stored_on_instance(self):
"""opening_shock_coefficient must be stored as given."""
parachute = _make_parachute(opening_shock_coefficient=1.8)
assert parachute.opening_shock_coefficient == pytest.approx(1.8)

def test_calculate_opening_shock_force_matches_formula(self):
"""calculate_opening_shock_force must return
Cx * cd_s * 0.5 * rho * V^2."""
cd_s = 10.0
cx = 1.6
air_density = 1.225
velocity = 50.0
parachute = _make_parachute(cd_s=cd_s, opening_shock_coefficient=cx)

expected_force = cx * cd_s * 0.5 * air_density * velocity**2
assert parachute.calculate_opening_shock_force(
air_density, velocity
) == pytest.approx(expected_force, rel=1e-9)

def test_calculate_opening_shock_force_scales_with_velocity_squared(self):
"""Doubling velocity must quadruple the opening shock force."""
parachute = _make_parachute()
force_v = parachute.calculate_opening_shock_force(1.225, 40.0)
force_2v = parachute.calculate_opening_shock_force(1.225, 80.0)
assert force_2v == pytest.approx(4 * force_v, rel=1e-9)

def test_calculate_opening_shock_force_zero_velocity_is_zero(self):
"""No dynamic pressure means no opening shock force."""
parachute = _make_parachute()
assert parachute.calculate_opening_shock_force(1.225, 0.0) == pytest.approx(0.0)

def test_to_dict_includes_opening_shock_coefficient(self):
"""to_dict must include the opening_shock_coefficient key."""
parachute = _make_parachute(opening_shock_coefficient=1.8)
data = parachute.to_dict()
assert "opening_shock_coefficient" in data
assert data["opening_shock_coefficient"] == 1.8

def test_from_dict_round_trip_preserves_opening_shock_coefficient(self):
"""A Parachute serialized to dict and restored must have the same
opening_shock_coefficient."""
original = _make_parachute(cd_s=5.0, opening_shock_coefficient=1.8)
data = original.to_dict()
restored = Parachute.from_dict(data)
assert restored.opening_shock_coefficient == pytest.approx(1.8)

def test_from_dict_defaults_opening_shock_coefficient_to_1_5_when_absent(self):
"""Dicts serialized before opening_shock_coefficient was added (no
key) must fall back to 1.5 for backward compatibility."""
data = {
"name": "legacy",
"cd_s": 10.0,
"trigger": "apogee",
"sampling_rate": 100,
"lag": 0,
"noise": (0, 0, 0),
# no opening_shock_coefficient key — simulates old serialized data
}
parachute = Parachute.from_dict(data)
assert parachute.opening_shock_coefficient == pytest.approx(1.5)
Loading