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: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,10 @@ Attention: The newest changes should be on top -->

### Fixed

- BUG: Fix hard-coded radius value for parachute added mass calculation [#889](https://github.com/RocketPy-Team/RocketPy/pull/889)
- DOC: Fix documentation build [#908](https://github.com/RocketPy-Team/RocketPy/pull/908)
- BUG: energy_data plot not working for 3 dof sims [[#906](https://github.com/RocketPy-Team/RocketPy/issues/906)]
- BUG: Fix parallel Monte Carlo simulation showing incorrect iteration count [#806](https://github.com/RocketPy-Team/RocketPy/pull/806)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this showing as an addition in this PR?

- BUG: Fix CSV column header spacing in FlightDataExporter [#864](https://github.com/RocketPy-Team/RocketPy/issues/864)
- BUG: Fix parallel Monte Carlo simulation showing incorrect iteration count [#806](https://github.com/RocketPy-Team/RocketPy/pull/806)
- BUG: Fix missing titles in roll parameter plots for fin sets [#934](https://github.com/RocketPy-Team/RocketPy/pull/934)
Expand Down
96 changes: 68 additions & 28 deletions rocketpy/rocket/parachute.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,17 +92,25 @@ class Parachute:
Function of noisy_pressure_signal.
Parachute.clean_pressure_signal_function : Function
Function of clean_pressure_signal.
Parachute.drag_coefficient : float
Drag coefficient of the inflated canopy shape, used only when
``radius`` is not provided to estimate the parachute radius from
``cd_s``: ``R = sqrt(cd_s / (drag_coefficient * pi))``. Typical
values: 1.4 for hemispherical canopies (default), 0.75 for flat
circular canopies, 1.5 for extended-skirt canopies.
Parachute.radius : float
Length of the non-unique semi-axis (radius) of the inflated hemispheroid
parachute in meters.
Parachute.height : float, None
parachute in meters. If not provided at construction time, it is
estimated from ``cd_s`` and ``drag_coefficient``.
Parachute.height : float
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).
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 value of 0.0432 is
chosen so that the resulting ``added_mass_coefficient`` equals
approximately 1.0 ("neutral" added-mass 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.
Expand All @@ -116,7 +124,8 @@ def __init__(
sampling_rate,
lag=0,
noise=(0, 0, 0),
radius=1.5,
radius=None,
drag_coefficient=1.4,
height=None,
porosity=0.0432,
):
Expand Down Expand Up @@ -172,25 +181,68 @@ def __init__(
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.
Length of the non-unique semi-axis (radius) of the inflated
hemispheroid parachute. If not provided, it is estimated from
``cd_s`` and ``drag_coefficient`` using:
``radius = sqrt(cd_s / (drag_coefficient * pi))``.
Units are in meters.
drag_coefficient : float, optional
Drag coefficient of the inflated canopy shape, used only when
``radius`` is not provided. It relates the aerodynamic ``cd_s``
to the physical canopy area via
``cd_s = drag_coefficient * pi * radius**2``. Typical values:

- **1.4** — hemispherical canopy (default, NASA SP-8066)
- **0.75** — flat circular canopy
- **1.5** — extended-skirt canopy

Has no effect when ``radius`` is explicitly provided.
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).
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
value of 0.0432 is chosen so that the resulting
``added_mass_coefficient`` equals approximately 1.0 ("neutral"
added-mass behavior).
"""
self.name = name
self.cd_s = cd_s
self.trigger = trigger
self.sampling_rate = sampling_rate
self.lag = lag
self.noise = noise
self.drag_coefficient = drag_coefficient
# Estimate radius from cd_s if not provided.
# cd_s = Cd * S = Cd * π * R² => R = sqrt(cd_s / (Cd * π))
if radius is None:
self.radius = np.sqrt(cd_s / (drag_coefficient * np.pi))
else:
self.radius = radius
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe can be rewritten as self.radius = radius or np.sqrt(cd_s / (drag_coefficient * np.pi))
I am not sure if that reduces the number of statements

self.height = height or self.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
)

self.__init_noise(noise)
self.prints = _ParachutePrints(self)
self.__evaluate_trigger_function(trigger)

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

Parameters
----------
noise : tuple, list
List in the format (mean, standard deviation, time-correlation).
"""
self.noise_signal = [[-1e-6, np.random.normal(noise[0], noise[1])]]
self.noisy_pressure_signal = []
self.clean_pressure_signal = []
Expand All @@ -200,26 +252,12 @@ 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][1]
+ beta * np.random.normal(noise[0], noise[1])
)

self.prints = _ParachutePrints(self)

self.__evaluate_trigger_function(trigger)

def __evaluate_trigger_function(self, trigger):
"""This is used to set the triggerfunc attribute that will be used to
interact with the Flight class.
Expand Down Expand Up @@ -309,6 +347,7 @@ def to_dict(self, **kwargs):
"lag": self.lag,
"noise": self.noise,
"radius": self.radius,
"drag_coefficient": self.drag_coefficient,
"height": self.height,
"porosity": self.porosity,
}
Expand Down Expand Up @@ -341,7 +380,8 @@ def from_dict(cls, data):
sampling_rate=data["sampling_rate"],
lag=data["lag"],
noise=data["noise"],
radius=data.get("radius", 1.5),
radius=data.get("radius", None),
drag_coefficient=data.get("drag_coefficient", 1.4),
height=data.get("height", None),
porosity=data.get("porosity", 0.0432),
)
Expand Down
32 changes: 21 additions & 11 deletions rocketpy/rocket/rocket.py
Original file line number Diff line number Diff line change
Expand Up @@ -1502,7 +1502,8 @@ def add_parachute(
sampling_rate=100,
lag=0,
noise=(0, 0, 0),
radius=1.5,
radius=None,
drag_coefficient=1.4,
height=None,
porosity=0.0432,
):
Expand Down Expand Up @@ -1564,26 +1565,34 @@ def add_parachute(
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.
Length of the non-unique semi-axis (radius) of the inflated
hemispheroid parachute. If not provided, it is estimated from
`cd_s` and `drag_coefficient` using:
`radius = sqrt(cd_s / (drag_coefficient * pi))`.
Units are in meters.
drag_coefficient : float, optional
Drag coefficient of the inflated canopy shape, used only when
`radius` is not provided. Typical values: 1.4 for hemispherical
canopies (default), 0.75 for flat circular canopies, 1.5 for
extended-skirt canopies. Has no effect when `radius` is given.
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).
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
value of 0.0432 yields an `added_mass_coefficient` of
approximately 1.0.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we keep the "neutral behavior" description to keep it consistent with the description in Parachute class?


Returns
-------
parachute : Parachute
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 containing trigger, sampling_rate, lag, cd_s, noise,
radius, drag_coefficient, height, porosity and name. Furthermore,
it stores clean_pressure_signal, noise_signal and
noisyPressureSignal which are filled in during Flight simulation.
"""
parachute = Parachute(
name,
Expand All @@ -1593,6 +1602,7 @@ def add_parachute(
lag,
noise,
radius,
drag_coefficient,
height,
porosity,
)
Expand Down
9 changes: 9 additions & 0 deletions rocketpy/stochastic/stochastic_parachute.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ class StochasticParachute(StochasticModel):
List with the name of the parachute object. This cannot be randomized.
radius : tuple, list, int, float
Radius of the parachute in meters.
drag_coefficient : tuple, list, int, float
Drag coefficient of the inflated canopy shape, used only when
``radius`` is not provided.
height : tuple, list, int, float
Height of the parachute in meters.
porosity : tuple, list, int, float
Expand All @@ -46,6 +49,7 @@ def __init__(
lag=None,
noise=None,
radius=None,
drag_coefficient=None,
height=None,
porosity=None,
):
Expand Down Expand Up @@ -74,6 +78,9 @@ def __init__(
time-correlation).
radius : tuple, list, int, float
Radius of the parachute in meters.
drag_coefficient : tuple, list, int, float
Drag coefficient of the inflated canopy shape, used only when
``radius`` is not provided.
height : tuple, list, int, float
Height of the parachute in meters.
porosity : tuple, list, int, float
Expand All @@ -86,6 +93,7 @@ def __init__(
self.lag = lag
self.noise = noise
self.radius = radius
self.drag_coefficient = drag_coefficient
self.height = height
self.porosity = porosity

Expand All @@ -100,6 +108,7 @@ def __init__(
noise=noise,
name=None,
radius=radius,
drag_coefficient=drag_coefficient,
height=height,
porosity=porosity,
)
Expand Down
35 changes: 15 additions & 20 deletions tests/integration/simulation/test_flight.py
Original file line number Diff line number Diff line change
Expand Up @@ -742,6 +742,20 @@ def test_environment_methods_accessible_in_controller(
"temperature": False,
}

def _call_env_methods(environment, altitude_asl, methods_called):
_ = environment.elevation
methods_called["elevation"] = True
_ = environment.wind_velocity_x(altitude_asl)
methods_called["wind_velocity_x"] = True
_ = environment.wind_velocity_y(altitude_asl)
methods_called["wind_velocity_y"] = True
_ = environment.speed_of_sound(altitude_asl)
methods_called["speed_of_sound"] = True
_ = environment.pressure(altitude_asl)
methods_called["pressure"] = True
_ = environment.temperature(altitude_asl)
methods_called["temperature"] = True

def controller_test_environment_access( # pylint: disable=unused-argument
time,
sampling_rate,
Expand All @@ -758,29 +772,10 @@ def controller_test_environment_access( # pylint: disable=unused-argument
if time < 3.9:
return None

# Test accessing various environment methods
try:
_ = environment.elevation
methods_called["elevation"] = True

_ = environment.wind_velocity_x(altitude_asl)
methods_called["wind_velocity_x"] = True

_ = environment.wind_velocity_y(altitude_asl)
methods_called["wind_velocity_y"] = True

_ = environment.speed_of_sound(altitude_asl)
methods_called["speed_of_sound"] = True

_ = environment.pressure(altitude_asl)
methods_called["pressure"] = True

_ = environment.temperature(altitude_asl)
methods_called["temperature"] = True

_call_env_methods(environment, altitude_asl, methods_called)
air_brakes.deployment_level = 0.3
except AttributeError as e:
# If any method is not accessible, the test should fail
raise AssertionError(f"Environment method not accessible: {e}") from e

return (time, air_brakes.deployment_level)
Expand Down
Loading
Loading