diff --git a/CHANGELOG.md b/CHANGELOG.md index 11deb613a..c69a1833f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,7 @@ Attention: The newest changes should be on top --> - DOC: Add Flight class usage documentation and update index [#841](https://github.com/RocketPy-Team/RocketPy/pull/841) - ENH: Discretized and No-Pickle Encoding Options [#827] (https://github.com/RocketPy-Team/RocketPy/pull/827) - ENH: Add the Coriolis Force to the Flight class [#799](https://github.com/RocketPy-Team/RocketPy/pull/799) +- ENH: Improve parachute geometric parametrization [#835](https://github.com/RocketPy-Team/RocketPy/pull/835) ### Changed diff --git a/README.md b/README.md index 3eb63c9a6..3e5bf7d95 100644 --- a/README.md +++ b/README.md @@ -262,6 +262,9 @@ main = calisto.add_parachute( sampling_rate=105, lag=1.5, noise=(0, 8.3, 0.5), + parachute_radius=1.5, + parachute_height=1.5, + porosity=0.0432, ) drogue = calisto.add_parachute( @@ -271,6 +274,9 @@ drogue = calisto.add_parachute( sampling_rate=105, lag=1.5, noise=(0, 8.3, 0.5), + parachute_radius=1.5, + parachute_height=1.5, + porosity=0.0432, ) ``` diff --git a/docs/user/first_simulation.rst b/docs/user/first_simulation.rst index 5624ed926..e3ff65724 100644 --- a/docs/user/first_simulation.rst +++ b/docs/user/first_simulation.rst @@ -276,6 +276,9 @@ Finally, we can add any number of Parachutes to the ``Rocket`` object. sampling_rate=105, lag=1.5, noise=(0, 8.3, 0.5), + parachute_radius=1.5, + parachute_height=1.5, + porosity=0.0432, ) drogue = calisto.add_parachute( @@ -285,6 +288,9 @@ Finally, we can add any number of Parachutes to the ``Rocket`` object. sampling_rate=105, lag=1.5, noise=(0, 8.3, 0.5), + parachute_radius=1.5, + parachute_height=1.5, + porosity=0.0432, ) We can then see if the rocket is stable by plotting the static margin: diff --git a/docs/user/rocket/rocket_usage.rst b/docs/user/rocket/rocket_usage.rst index 1b4d2bbd5..349471463 100644 --- a/docs/user/rocket/rocket_usage.rst +++ b/docs/user/rocket/rocket_usage.rst @@ -302,6 +302,9 @@ apogee and another that will be deployed at 800 meters above ground level: sampling_rate=105, lag=1.5, noise=(0, 8.3, 0.5), + parachute_radius=1.5, + parachute_height=1.5, + porosity=0.0432, ) drogue = calisto.add_parachute( @@ -311,6 +314,9 @@ apogee and another that will be deployed at 800 meters above ground level: sampling_rate=105, lag=1.5, noise=(0, 8.3, 0.5), + parachute_radius=1.5, + parachute_height=1.5, + porosity=0.0432, ) .. seealso:: diff --git a/rocketpy/rocket/parachute.py b/rocketpy/rocket/parachute.py index 3f00f9972..e27216e26 100644 --- a/rocketpy/rocket/parachute.py +++ b/rocketpy/rocket/parachute.py @@ -9,7 +9,7 @@ class Parachute: - """Keeps parachute information. + """Keeps information of the parachute, which is modeled as a hemispheroid. Attributes ---------- @@ -92,6 +92,20 @@ class Parachute: Function of noisy_pressure_signal. Parachute.clean_pressure_signal_function : Function Function of clean_pressure_signal. + Parachute.radius : float + Length of the non-unique semi-axis (radius) of the inflated hemispheroid + parachute in meters. + Parachute.height : float, None + Length of the unique semi-axis (height) of the inflated hemispheroid + parachute in meters. + Parachute.porosity : float + Geometric porosity of the canopy (ratio of open area to total canopy area), + in [0, 1]. Affects only the added-mass scaling during descent; it does + not change ``cd_s`` (drag). The default, 0.0432, yields an added-mass + of 1.0 (“neutral” behavior). + 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. """ def __init__( @@ -102,6 +116,9 @@ def __init__( sampling_rate, lag=0, noise=(0, 0, 0), + radius=1.5, + height=None, + porosity=0.0432, ): """Initializes Parachute class. @@ -154,6 +171,19 @@ def __init__( The values are used to add noise to the pressure signal which is passed to the trigger function. Default value is ``(0, 0, 0)``. Units are in Pa. + radius : float, optional + Length of the non-unique semi-axis (radius) of the inflated hemispheroid + parachute. Default value is 1.5. + Units are in meters. + height : float, optional + Length of the unique semi-axis (height) of the inflated hemispheroid + parachute. Default value is the radius of the parachute. + Units are in meters. + porosity : float, optional + Geometric porosity of the canopy (ratio of open area to total canopy area), + in [0, 1]. Affects only the added-mass scaling during descent; it does + not change ``cd_s`` (drag). The default, 0.0432, yields an added-mass + of 1.0 (“neutral” behavior). """ self.name = name self.cd_s = cd_s @@ -170,6 +200,15 @@ def __init__( self.clean_pressure_signal_function = Function(0) self.noisy_pressure_signal_function = Function(0) self.noise_signal_function = Function(0) + self.radius = radius + self.height = height or radius + self.porosity = porosity + self.added_mass_coefficient = 1.068 * ( + 1 + - 1.465 * self.porosity + - 0.25975 * self.porosity**2 + + 1.2626 * self.porosity**3 + ) alpha, beta = self.noise_corr self.noise_function = lambda: alpha * self.noise_signal[-1][ @@ -268,6 +307,9 @@ def to_dict(self, **kwargs): "sampling_rate": self.sampling_rate, "lag": self.lag, "noise": self.noise, + "radius": self.radius, + "height": self.height, + "porosity": self.porosity, } if kwargs.get("include_outputs", False): @@ -298,6 +340,9 @@ def from_dict(cls, data): sampling_rate=data["sampling_rate"], lag=data["lag"], noise=data["noise"], + radius=data.get("radius", 1.5), + height=data.get("height", None), + porosity=data.get("porosity", 0.0432), ) return parachute diff --git a/rocketpy/rocket/rocket.py b/rocketpy/rocket/rocket.py index 1cab142ee..bb32ae7b2 100644 --- a/rocketpy/rocket/rocket.py +++ b/rocketpy/rocket/rocket.py @@ -1433,7 +1433,16 @@ def add_free_form_fins( return fin_set def add_parachute( - self, name, cd_s, trigger, sampling_rate=100, lag=0, noise=(0, 0, 0) + self, + name, + cd_s, + trigger, + sampling_rate=100, + lag=0, + noise=(0, 0, 0), + radius=1.5, + height=None, + porosity=0.0432, ): """Creates a new parachute, storing its parameters such as opening delay, drag coefficients and trigger function. @@ -1492,16 +1501,39 @@ def add_parachute( The values are used to add noise to the pressure signal which is passed to the trigger function. Default value is (0, 0, 0). Units are in pascal. + radius : float, optional + Length of the non-unique semi-axis (radius) of the inflated hemispheroid + parachute. Default value is 1.5. + Units are in meters. + height : float, optional + Length of the unique semi-axis (height) of the inflated hemispheroid + parachute. Default value is the radius of the parachute. + Units are in meters. + porosity : float, optional + Geometric porosity of the canopy (ratio of open area to total canopy area), + in [0, 1]. Affects only the added-mass scaling during descent; it does + not change ``cd_s`` (drag). The default, 0.0432, yields an added-mass + of 1.0 (“neutral” behavior). Returns ------- parachute : Parachute - Parachute containing trigger, sampling_rate, lag, cd_s, noise - and name. Furthermore, it stores clean_pressure_signal, + Parachute containing trigger, sampling_rate, lag, cd_s, noise, radius, + height, porosity and name. Furthermore, it stores clean_pressure_signal, noise_signal and noisyPressureSignal which are filled in during Flight simulation. """ - parachute = Parachute(name, cd_s, trigger, sampling_rate, lag, noise) + parachute = Parachute( + name, + cd_s, + trigger, + sampling_rate, + lag, + noise, + radius, + height, + porosity, + ) self.parachutes.append(parachute) return self.parachutes[-1] diff --git a/rocketpy/simulation/flight.py b/rocketpy/simulation/flight.py index 202b6068a..877fa2b4c 100644 --- a/rocketpy/simulation/flight.py +++ b/rocketpy/simulation/flight.py @@ -761,7 +761,24 @@ def __simulate(self, verbose): callbacks = [ lambda self, parachute_cd_s=parachute.cd_s: setattr( self, "parachute_cd_s", parachute_cd_s - ) + ), + lambda self, + parachute_radius=parachute.parachute_radius: setattr( + self, "parachute_radius", parachute_radius + ), + lambda self, + parachute_height=parachute.parachute_height: setattr( + self, "parachute_height", parachute_height + ), + lambda self, parachute_porosity=parachute.porosity: setattr( + self, "parachute_porosity", parachute_porosity + ), + lambda self, + added_mass_coefficient=parachute.added_mass_coefficient: setattr( + self, + "parachute_added_mass_coefficient", + added_mass_coefficient, + ), ] self.flight_phases.add_phase( node.t + parachute.lag, @@ -1007,7 +1024,31 @@ def __simulate(self, verbose): lambda self, parachute_cd_s=parachute.cd_s: setattr( self, "parachute_cd_s", parachute_cd_s - ) + ), + lambda self, + parachute_radius=parachute.radius: setattr( + self, + "parachute_radius", + parachute_radius, + ), + lambda self, + parachute_height=parachute.height: setattr( + self, + "parachute_height", + parachute_height, + ), + lambda self, + parachute_porosity=parachute.porosity: setattr( + self, + "parachute_porosity", + parachute_porosity, + ), + lambda self, + added_mass_coefficient=parachute.added_mass_coefficient: setattr( + self, + "parachute_added_mass_coefficient", + added_mass_coefficient, + ), ] self.flight_phases.add_phase( overshootable_node.t + parachute.lag, @@ -1960,15 +2001,9 @@ def u_dot_parachute(self, t, u, post_processing=False): wind_velocity_x = self.env.wind_velocity_x.get_value_opt(z) wind_velocity_y = self.env.wind_velocity_y.get_value_opt(z) - # Get Parachute data - cd_s = self.parachute_cd_s - # Get the mass of the rocket mp = self.rocket.dry_mass - # Define constants - ka = 1 # Added mass coefficient (depends on parachute's porosity) - R = 1.5 # Parachute radius # to = 1.2 # eta = 1 # Rdot = (6 * R * (1 - eta) / (1.2**6)) * ( @@ -1976,8 +2011,17 @@ def u_dot_parachute(self, t, u, post_processing=False): # ) # Rdot = 0 + # tf = 8 * nominal diameter / velocity at line stretch + # Calculate added mass - ma = ka * rho * (4 / 3) * np.pi * R**3 + ma = ( + self.parachute_added_mass_coefficient + * rho + * (2 / 3) + * np.pi + * self.parachute_radius**2 + * self.parachute_height + ) # Calculate freestream speed freestream_x = vx - wind_velocity_x @@ -1986,14 +2030,14 @@ def u_dot_parachute(self, t, u, post_processing=False): free_stream_speed = (freestream_x**2 + freestream_y**2 + freestream_z**2) ** 0.5 # Determine drag force - pseudo_drag = -0.5 * rho * cd_s * free_stream_speed + pseudo_drag = -0.5 * rho * self.parachute_cd_s * free_stream_speed # pseudo_drag = pseudo_drag - ka * rho * 4 * np.pi * (R**2) * Rdot - Dx = pseudo_drag * freestream_x + Dx = pseudo_drag * freestream_x # add eta efficiency for wake Dy = pseudo_drag * freestream_y Dz = pseudo_drag * freestream_z ax = Dx / (mp + ma) ay = Dy / (mp + ma) - az = (Dz - 9.8 * mp) / (mp + ma) + az = (Dz - mp * self.env.gravity.get_value_opt(z)) / (mp + ma) # Add coriolis acceleration _, w_earth_y, w_earth_z = self.env.earth_rotation_vector diff --git a/rocketpy/stochastic/stochastic_parachute.py b/rocketpy/stochastic/stochastic_parachute.py index 4cf7746d7..dea8a077d 100644 --- a/rocketpy/stochastic/stochastic_parachute.py +++ b/rocketpy/stochastic/stochastic_parachute.py @@ -29,6 +29,12 @@ class StochasticParachute(StochasticModel): time-correlation). name : list[str] List with the name of the parachute object. This cannot be randomized. + radius : tuple, list, int, float + Radius of the parachute in meters. + height : tuple, list, int, float + Height of the parachute in meters. + porosity : tuple, list, int, float + Porosity of the parachute. """ def __init__( @@ -39,6 +45,9 @@ def __init__( sampling_rate=None, lag=None, noise=None, + radius=None, + height=None, + porosity=None, ): """Initializes the Stochastic Parachute class. @@ -63,6 +72,12 @@ def __init__( noise : list List of tuples in the form of (mean, standard deviation, time-correlation). + radius : tuple, list, int, float + Radius of the parachute in meters. + height : tuple, list, int, float + Height of the parachute in meters. + porosity : tuple, list, int, float + Porosity of the parachute. """ self.parachute = parachute self.cd_s = cd_s @@ -70,6 +85,9 @@ def __init__( self.sampling_rate = sampling_rate self.lag = lag self.noise = noise + self.radius = radius + self.height = height + self.porosity = porosity self._validate_trigger(trigger) self._validate_noise(noise) @@ -81,6 +99,9 @@ def __init__( lag=lag, noise=noise, name=None, + radius=radius, + height=height, + porosity=porosity, ) def _validate_trigger(self, trigger): diff --git a/tests/unit/test_flight.py b/tests/unit/test_flight.py index 04bd97c40..1bd9384e2 100644 --- a/tests/unit/test_flight.py +++ b/tests/unit/test_flight.py @@ -252,7 +252,7 @@ def test_aerodynamic_moments(flight_calisto_custom_wind, flight_time, expected_v ("t_initial", (1.654150, 0.659142, -0.067103)), ("out_of_rail_time", (5.052628, 2.013361, -1.75370)), ("apogee_time", (2.321838, -1.613641, -0.962108)), - ("t_final", (-0.025792, 0.012030, 159.202481)), + ("t_final", (-0.019802, 0.012030, 159.051604)), ], ) def test_aerodynamic_forces(flight_calisto_custom_wind, flight_time, expected_values): diff --git a/tests/unit/test_utilities.py b/tests/unit/test_utilities.py index 55d45de95..2bb8608b5 100644 --- a/tests/unit/test_utilities.py +++ b/tests/unit/test_utilities.py @@ -114,7 +114,7 @@ def test_fin_flutter_analysis(flight_calisto_custom_wind): assert np.isclose(flutter_mach(np.inf), 1.0048188594647927, atol=5e-3) assert np.isclose(safety_factor(0), 64.78797, atol=5e-3) assert np.isclose(safety_factor(10), 2.1948620401502072, atol=5e-3) - assert np.isclose(safety_factor(np.inf), 61.64222220697017, atol=5e-3) + assert np.isclose(safety_factor(np.inf), 61.669562809629035, atol=5e-3) def test_flutter_prints(flight_calisto_custom_wind):