Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
c51beb0
ENH: added radius and porosity to parachute
ArthurJWH Jun 19, 2025
c82edc9
ENH: fixing radius and height attribute in flight class
ArthurJWH Jun 26, 2025
e9077b9
STY: ruff formatting
ArthurJWH Jun 26, 2025
c8005c6
ENH: added new parameters into add_parachute method in Rocket class
ArthurJWH Jul 1, 2025
3c40acc
DOC: added new parameters to the documentation
ArthurJWH Jul 3, 2025
a81a9a6
DOC: improved descriptions of the new parameters
ArthurJWH Jul 15, 2025
303491e
ENH: implementing previous comments
ArthurJWH Aug 12, 2025
9db403a
ENH: fixing formatting and adding docs
ArthurJWH Aug 12, 2025
05553d4
ENH: Sketch implementation of parachute inflation
ArthurJWH Nov 1, 2025
fec6bad
ENH: added parachute radius
ArthurJWH Jun 26, 2025
149ea9e
ENH: fixing radius and height attribute in flight class
ArthurJWH Jun 26, 2025
b9d02dd
STY: ruff formatting
ArthurJWH Jun 26, 2025
17bcb39
ENH: added new parameters into add_parachute method in Rocket class
ArthurJWH Jul 1, 2025
8cef755
DOC: added new parameters to the documentation
ArthurJWH Jul 3, 2025
17b502f
MNT: manually fixing remaining errors from rebase
ArthurJWH Nov 10, 2025
3da56ff
MNT: fixing typo for exponentiation operator
ArthurJWH Nov 10, 2025
523c5a6
ENH: added radius and porosity to parachute
ArthurJWH Jun 19, 2025
9f28d6e
ENH: fixing radius and height attribute in flight class
ArthurJWH Jun 26, 2025
7334fc4
STY: ruff formatting
ArthurJWH Jun 26, 2025
bfb3f37
ENH: added new parameters into add_parachute method in Rocket class
ArthurJWH Jul 1, 2025
a16a48c
DOC: added new parameters to the documentation
ArthurJWH Jul 3, 2025
7404daa
DOC: improved descriptions of the new parameters
ArthurJWH Jul 15, 2025
2f15b09
ENH: implementing previous comments
ArthurJWH Aug 12, 2025
81667b5
ENH: fixing formatting and adding docs
ArthurJWH Aug 12, 2025
4dac748
ENH: Sketch implementation of parachute inflation
ArthurJWH Nov 1, 2025
f1fa769
ENH: added parachute radius
ArthurJWH Jun 26, 2025
7aef262
ENH: fixing radius and height attribute in flight class
ArthurJWH Jun 26, 2025
c7dc21d
STY: ruff formatting
ArthurJWH Jun 26, 2025
85fb3b8
ENH: added new parameters into add_parachute method in Rocket class
ArthurJWH Jul 1, 2025
301e08f
DOC: added new parameters to the documentation
ArthurJWH Jul 3, 2025
b18b9bf
MNT: manually fixing remaining errors from rebase
ArthurJWH Nov 10, 2025
b861b2d
MNT: fixing typo for exponentiation operator
ArthurJWH Nov 10, 2025
7067104
Merge branch 'develop' into enh/parachute-inflation-modeling
ArthurJWH Nov 28, 2025
0c75906
Merge branch 'enh/parachute-inflation-modeling' of https://github.com…
ArthurJWH Dec 14, 2025
2d5d255
ENH: first draft of parachute inflation
ArthurJWH Dec 14, 2025
29fcfb4
Merge branch 'develop' into enh/parachute-inflation-modeling
ArthurJWH Dec 14, 2025
5d8bb23
ENH: Adding callback for parachute volume
ArthurJWH Dec 14, 2025
01bccda
STY: fixing snakecase
ArthurJWH Dec 14, 2025
9f938ad
MNT: Fixing typos and small logical problems
ArthurJWH Dec 28, 2025
5244c8c
MNT: Fixing __t0 attribute reset
ArthurJWH Dec 28, 2025
745c2c0
ENH: Moving initial volume calculation to Parachute class
ArthurJWH Dec 28, 2025
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
18 changes: 17 additions & 1 deletion rocketpy/rocket/parachute.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from inspect import signature

import math
import numpy as np

from rocketpy.tools import from_hex_decode, to_hex_encode
Expand Down Expand Up @@ -98,6 +99,9 @@ class Parachute:
Parachute.height : float, None
Length of the unique semi-axis (height) of the inflated hemispheroid
parachute in meters.
Parachute.initial_radius : float, None
Initial radius of the parachute on deployment in meters. Used to model the
parachute inflation.
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
Expand All @@ -118,6 +122,7 @@ def __init__(
noise=(0, 0, 0),
radius=1.5,
height=None,
initial_radius=None,
porosity=0.0432,
):
"""Initializes Parachute class.
Expand Down Expand Up @@ -179,6 +184,9 @@ def __init__(
Length of the unique semi-axis (height) of the inflated hemispheroid
parachute. Default value is the radius of the parachute.
Units are in meters.
initial_radius : float, optional
Initial radius of the parachute on deployment in meters. Used to model the
parachute inflation. Default value is the parachute radius (no inflation).
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
Expand All @@ -202,14 +210,20 @@ def __init__(
self.noise_signal_function = Function(0)
self.radius = radius
self.height = height or radius
self.initial_radius = initial_radius or radius
self.initial_volume = (
(4 / 3)
* math.pi
* (self.parachute_height / self.parachute_radius)
* (min(self.parachute_radius, self.rocket.radius)) ** 3
)
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
Expand Down Expand Up @@ -309,6 +323,7 @@ def to_dict(self, **kwargs):
"noise": self.noise,
"radius": self.radius,
"height": self.height,
"initial_radius": self.initial_radius,
"porosity": self.porosity,
}

Expand Down Expand Up @@ -342,6 +357,7 @@ def from_dict(cls, data):
noise=data["noise"],
radius=data.get("radius", 1.5),
height=data.get("height", None),
initial_radius=data.get("initial_radius", None),
porosity=data.get("porosity", 0.0432),
)

Expand Down
23 changes: 14 additions & 9 deletions rocketpy/rocket/rocket.py
Original file line number Diff line number Diff line change
Expand Up @@ -1485,6 +1485,7 @@ def add_parachute(
noise=(0, 0, 0),
radius=1.5,
height=None,
initial_radius=None,
porosity=0.0432,
):
"""Creates a new parachute, storing its parameters such as
Expand Down Expand Up @@ -1552,6 +1553,9 @@ def add_parachute(
Length of the unique semi-axis (height) of the inflated hemispheroid
parachute. Default value is the radius of the parachute.
Units are in meters.
initial_radius : float, optional
Initial radius of the parachute on deployment in meters. Used to model the
parachute inflation. Default value is the parachute radius (no inflation).
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
Expand All @@ -1567,15 +1571,16 @@ def add_parachute(
Flight simulation.
"""
parachute = Parachute(
name,
cd_s,
trigger,
sampling_rate,
lag,
noise,
radius,
height,
porosity,
name=name,
cd_s=cd_s,
trigger=trigger,
sampling_rate=sampling_rate,
lag=lag,
noise=noise,
radius=radius,
height=height,
initial_radius=initial_radius,
porosity=porosity,
)
self.parachutes.append(parachute)
return self.parachutes[-1]
Expand Down
80 changes: 61 additions & 19 deletions rocketpy/simulation/flight.py
Original file line number Diff line number Diff line change
Expand Up @@ -765,6 +765,15 @@ def __simulate(self, verbose):
"parachute_added_mass_coefficient",
added_mass_coefficient,
),
lambda self,
initial_volume=parachute.initial_volume: setattr(
self,
"parachute_volume",
Copy link
Member

Choose a reason for hiding this comment

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

@ArthurJWH why do you need to calculate the parachute_volume within the flight simulation? Can you calculate and store it in the Parachute class?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

My original idea was to assume the initial radius being the rocket radius (compartment from where it is ejected), but I cannot access the rocket radius within Parachute class. I can add a new parameter "initial radius" for the Parachute class, if you think that is better

initial_volume,
),
lambda self: delattr(self, "__t0")
if hasattr(self, "__t0")
else None,
]
self.flight_phases.add_phase(
node.t + parachute.lag,
Expand Down Expand Up @@ -987,6 +996,12 @@ def __check_and_handle_parachute_triggers(
"parachute_added_mass_coefficient",
added_mass_coefficient,
),
lambda self, initial_volume=parachute.initial_volume: setattr(
self,
"parachute_volume",
initial_volume,
),
lambda self: delattr(self, "__t0") if hasattr(self, "__t0") else None,
]
self.flight_phases.add_phase(
node.t + parachute.lag,
Expand Down Expand Up @@ -2702,31 +2717,58 @@ def u_dot_parachute(self, t, u, post_processing=False):
# Get the mass of the rocket
mp = self.rocket.dry_mass

# to = 1.2
# eta = 1
# Rdot = (6 * R * (1 - eta) / (1.2**6)) * (
# (1 - eta) * t**5 + eta * (to**3) * (t**2)
# )
# Rdot = 0

# tf = 8 * nominal diameter / velocity at line stretch

# Calculate added mass
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
freestream_y = vy - wind_velocity_y
freestream_z = vz
free_stream_speed = (freestream_x**2 + freestream_y**2 + freestream_z**2) ** 0.5

# Initialize parachute geometrical parameters
inflated_radius = (
Copy link
Member

Choose a reason for hiding this comment

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

calculating inflated_radius in the Flight class? Do you really need it?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

inflated_radius is the parachute radius in the current Flight stage, it is used to calculate the surface area of the parachute

(3 * self.parachute_volume * self.parachute_radius)
/ (4 * math.pi * self.parachute_height)
) ** (1 / 3)
inflated_height = (
inflated_radius * self.parachute_height / self.parachute_radius
)

# Calculate the surface area of the parachute
if self.parachute_radius > self.parachute_height:
e = math.sqrt(1 - (inflated_height**2) / (inflated_radius**2))
surface_area = (
math.pi * inflated_radius**2 * (1 + (1 - e**2) / e * math.atanh(e))
)
else:
e = math.sqrt(1 - (inflated_radius**2) / (inflated_height**2))
surface_area = (
math.pi
* inflated_radius**2
* (1 + inflated_height / (e * inflated_radius) * math.asin(e))
Comment on lines +2737 to +2746
Copy link

Copilot AI Dec 14, 2025

Choose a reason for hiding this comment

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

The surface area calculation can fail with a domain error when the eccentricity formula yields a negative value under sqrt. This can occur if inflated_height > inflated_radius when radius > height (line 2751), or if inflated_radius > inflated_height when radius <= height (line 2756). Add validation to ensure the discriminant is non-negative before taking the square root, or clamp the eccentricity to valid ranges [0, 1).

Suggested change
e = math.sqrt(1 - (inflated_height**2) / (inflated_radius**2))
surface_area = (
math.pi * inflated_radius**2 * (1 + (1 - e**2) / e * math.atanh(e))
)
else:
e = math.sqrt(1 - (inflated_radius**2) / (inflated_height**2))
surface_area = (
math.pi
* inflated_radius**2
* (1 + inflated_height / (e * inflated_radius) * math.asin(e))
discriminant = 1 - (inflated_height**2) / (inflated_radius**2)
if discriminant < 0:
warnings.warn(
f"Parachute eccentricity discriminant negative ({discriminant:.6f}); "
"clamping to zero. Check parachute geometry parameters."
)
discriminant = 0.0
elif discriminant > 1:
warnings.warn(
f"Parachute eccentricity discriminant > 1 ({discriminant:.6f}); "
"clamping to one. Check parachute geometry parameters."
)
discriminant = 1.0
e = math.sqrt(discriminant)
surface_area = (
math.pi * inflated_radius**2 * (1 + (1 - e**2) / e * math.atanh(e)) if e != 0 else
math.pi * inflated_radius**2 * 2 # limit as e->0
)
else:
discriminant = 1 - (inflated_radius**2) / (inflated_height**2)
if discriminant < 0:
warnings.warn(
f"Parachute eccentricity discriminant negative ({discriminant:.6f}); "
"clamping to zero. Check parachute geometry parameters."
)
discriminant = 0.0
elif discriminant > 1:
warnings.warn(
f"Parachute eccentricity discriminant > 1 ({discriminant:.6f}); "
"clamping to one. Check parachute geometry parameters."
)
discriminant = 1.0
e = math.sqrt(discriminant)
surface_area = (
math.pi
* inflated_radius**2
* (1 + inflated_height / (e * inflated_radius) * math.asin(e)) if e != 0 else
math.pi * inflated_radius**2 * 2 # limit as e->0

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Since the inflated radius / inflated height = radius / height, the if else statement should already account for sign of the value under the square root

)

# Calculate volume flow of air into parachute
volume_flow = (
freestream_z # considering parachute as vertical
* (
(math.pi * inflated_radius**2)
- (self.parachute_porosity * surface_area)
)
)

# Getting time step
Copy link
Member

Choose a reason for hiding this comment

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

do you really need to store step.t0? Why?

Should this really be a public attribute?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I used it to have access to the time step between u_dot_parachute calls (not ideal). I can change it for a private attribute, or do you have any other suggestion?

Copy link
Member

Choose a reason for hiding this comment

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

private is better

self.__t0 = getattr(self, "t0", t)
t1 = t
dt = t1 - self.__t0

# Integrating parachute volume
self.parachute_volume += volume_flow * dt

# Dragged air mass
ma = self.parachute_volume * rho

# Moving time step
self.__t0 = t1

# Determine drag force
pseudo_drag = -0.5 * rho * self.parachute_cd_s * free_stream_speed
# pseudo_drag = pseudo_drag - ka * rho * 4 * np.pi * (R**2) * Rdot
Expand Down
Loading