diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ab28dd89..fa33a022f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ Attention: The newest changes should be on top --> ### Added +- ENH: Add simplified opening shock force estimation [#1092](https://github.com/RocketPy-Team/RocketPy/pull/1092) - ENH: Add Qodo PR-Agent workflow using Google Gemini [#1089](https://github.com/RocketPy-Team/RocketPy/pull/1089) - ENH: Support for Meteomatics API in the `Environment` class [#1079](https://github.com/RocketPy-Team/RocketPy/pull/1079) - ENH: update master with develop [#1081](https://github.com/RocketPy-Team/RocketPy/pull/1081) diff --git a/rocketpy/utilities.py b/rocketpy/utilities.py index 6dbd25380..97ebe3f45 100644 --- a/rocketpy/utilities.py +++ b/rocketpy/utilities.py @@ -780,3 +780,46 @@ def load_from_rpy(filename: str, resimulate=False): simulation = json.dumps(data["simulation"]) flight = json.loads(simulation, cls=RocketPyDecoder, resimulate=resimulate) return flight + + +def calculate_simplified_opening_shock_force( + cd_s, air_density, velocity, opening_shock_coefficient=1.5 +): + """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 + ---------- + cd_s : float + Drag coefficient times reference area of the parachute. + 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. + opening_shock_coefficient : float, optional + 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. + + Returns + ------- + float + Estimated peak opening shock force, in Newtons. + """ + dynamic_pressure = 0.5 * air_density * velocity**2 + return opening_shock_coefficient * cd_s * dynamic_pressure diff --git a/tests/unit/test_utilities.py b/tests/unit/test_utilities.py index 146ff1be1..a6ed1f3eb 100644 --- a/tests/unit/test_utilities.py +++ b/tests/unit/test_utilities.py @@ -348,6 +348,41 @@ def test_load_from_rpy(mock_show): # pylint: disable=unused-argument assert loaded_flight.all_info() is None +def test_opening_shock_coefficient_default_is_1_5(): + """Default opening_shock_coefficient must be 1.5.""" + force_default = utilities.calculate_simplified_opening_shock_force(10.0, 1.225, 10) + force_1_5 = utilities.calculate_simplified_opening_shock_force(10.0, 1.225, 10, 1.5) + assert force_default == force_1_5 + + +def test_calculate_simplified_opening_shock_force_matches_formula(): + """calculate_simplified_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 + + expected_force = cx * cd_s * 0.5 * air_density * velocity**2 + assert utilities.calculate_simplified_opening_shock_force( + cd_s, air_density, velocity, cx + ) == pytest.approx(expected_force, rel=1e-9) + + +def test_calculate_simplified_opening_shock_force_scales_with_velocity_squared(): + """Doubling velocity must quadruple the opening shock force.""" + force_v = utilities.calculate_simplified_opening_shock_force(10.0, 1.225, 40.0) + force_2v = utilities.calculate_simplified_opening_shock_force(10.0, 1.225, 80.0) + assert force_2v == pytest.approx(4 * force_v, rel=1e-9) + + +def test_calculate_simplified_opening_shock_force_zero_velocity_is_zero(): + """No dynamic pressure means no opening shock force.""" + assert utilities.calculate_simplified_opening_shock_force( + 10.0, 1.225, 0.0 + ) == pytest.approx(0.0) + + # --- Logging (rocketpy.utilities.enable_logging) ------------------------------