@@ -92,17 +92,25 @@ class Parachute:
9292 Function of noisy_pressure_signal.
9393 Parachute.clean_pressure_signal_function : Function
9494 Function of clean_pressure_signal.
95+ Parachute.drag_coefficient : float
96+ Drag coefficient of the inflated canopy shape, used only when
97+ ``radius`` is not provided to estimate the parachute radius from
98+ ``cd_s``: ``R = sqrt(cd_s / (drag_coefficient * pi))``. Typical
99+ values: 1.4 for hemispherical canopies (default), 0.75 for flat
100+ circular canopies, 1.5 for extended-skirt canopies.
95101 Parachute.radius : float
96102 Length of the non-unique semi-axis (radius) of the inflated hemispheroid
97- parachute in meters.
98- Parachute.height : float, None
103+ parachute in meters. If not provided at construction time, it is
104+ estimated from ``cd_s`` and ``drag_coefficient``.
105+ Parachute.height : float
99106 Length of the unique semi-axis (height) of the inflated hemispheroid
100107 parachute in meters.
101108 Parachute.porosity : float
102- Geometric porosity of the canopy (ratio of open area to total canopy area),
103- in [0, 1]. Affects only the added-mass scaling during descent; it does
104- not change ``cd_s`` (drag). The default, 0.0432, yields an added-mass
105- of 1.0 (“neutral” behavior).
109+ Geometric porosity of the canopy (ratio of open area to total canopy
110+ area), in [0, 1]. Affects only the added-mass scaling during descent;
111+ it does not change ``cd_s`` (drag). The default value of 0.0432 is
112+ chosen so that the resulting ``added_mass_coefficient`` equals
113+ approximately 1.0 ("neutral" added-mass behavior).
106114 Parachute.added_mass_coefficient : float
107115 Coefficient used to calculate the added-mass due to dragged air. It is
108116 calculated from the porosity of the parachute.
@@ -116,9 +124,10 @@ def __init__(
116124 sampling_rate ,
117125 lag = 0 ,
118126 noise = (0 , 0 , 0 ),
119- radius = 1.5 ,
127+ radius = None ,
120128 height = None ,
121129 porosity = 0.0432 ,
130+ drag_coefficient = 1.4 ,
122131 ):
123132 """Initializes Parachute class.
124133
@@ -172,25 +181,83 @@ def __init__(
172181 passed to the trigger function. Default value is ``(0, 0, 0)``.
173182 Units are in Pa.
174183 radius : float, optional
175- Length of the non-unique semi-axis (radius) of the inflated hemispheroid
176- parachute. Default value is 1.5.
184+ Length of the non-unique semi-axis (radius) of the inflated
185+ hemispheroid parachute. If not provided, it is estimated from
186+ ``cd_s`` and ``drag_coefficient`` using:
187+ ``radius = sqrt(cd_s / (drag_coefficient * pi))``.
177188 Units are in meters.
178189 height : float, optional
179190 Length of the unique semi-axis (height) of the inflated hemispheroid
180191 parachute. Default value is the radius of the parachute.
181192 Units are in meters.
182193 porosity : float, optional
183- Geometric porosity of the canopy (ratio of open area to total canopy area),
184- in [0, 1]. Affects only the added-mass scaling during descent; it does
185- not change ``cd_s`` (drag). The default, 0.0432, yields an added-mass
186- of 1.0 (“neutral” behavior).
194+ Geometric porosity of the canopy (ratio of open area to total
195+ canopy area), in [0, 1]. Affects only the added-mass scaling
196+ during descent; it does not change ``cd_s`` (drag). The default
197+ value of 0.0432 is chosen so that the resulting
198+ ``added_mass_coefficient`` equals approximately 1.0 ("neutral"
199+ added-mass behavior).
200+ drag_coefficient : float, optional
201+ Drag coefficient of the inflated canopy shape, used only when
202+ ``radius`` is not provided. It relates the aerodynamic ``cd_s``
203+ to the physical canopy area via
204+ ``cd_s = drag_coefficient * pi * radius**2``. Typical values:
205+
206+ - **1.4** — hemispherical canopy (default, NASA SP-8066)
207+ - **0.75** — flat circular canopy
208+ - **1.5** — extended-skirt canopy
209+
210+ Has no effect when ``radius`` is explicitly provided.
187211 """
212+
213+ # Save arguments as attributes
188214 self .name = name
189215 self .cd_s = cd_s
190216 self .trigger = trigger
191217 self .sampling_rate = sampling_rate
192218 self .lag = lag
193219 self .noise = noise
220+ self .drag_coefficient = drag_coefficient
221+ self .porosity = porosity
222+
223+ # Initialize derived attributes
224+ self .radius = self .__resolve_radius (radius , cd_s , drag_coefficient )
225+ self .height = self .__resolve_height (height , self .radius )
226+ self .added_mass_coefficient = self .__compute_added_mass_coefficient (
227+ self .porosity
228+ )
229+ self .__init_noise (noise )
230+ self .__evaluate_trigger_function (trigger )
231+
232+ # Prints and plots
233+ self .prints = _ParachutePrints (self )
234+
235+ def __resolve_radius (self , radius , cd_s , drag_coefficient ):
236+ """Resolves parachute radius from input or aerodynamic relation."""
237+ if radius is not None :
238+ return radius
239+
240+ # cd_s = Cd * S = Cd * pi * R^2 => R = sqrt(cd_s / (Cd * pi))
241+ return np .sqrt (cd_s / (drag_coefficient * np .pi ))
242+
243+ def __resolve_height (self , height , radius ):
244+ """Resolves parachute height defaulting to radius when not provided."""
245+ return height or radius
246+
247+ def __compute_added_mass_coefficient (self , porosity ):
248+ """Computes the added-mass coefficient from canopy porosity."""
249+ return 1.068 * (
250+ 1 - 1.465 * porosity - 0.25975 * porosity ** 2 + 1.2626 * porosity ** 3
251+ )
252+
253+ def __init_noise (self , noise ):
254+ """Initializes all noise-related attributes.
255+
256+ Parameters
257+ ----------
258+ noise : tuple, list
259+ List in the format (mean, standard deviation, time-correlation).
260+ """
194261 self .noise_signal = [[- 1e-6 , np .random .normal (noise [0 ], noise [1 ])]]
195262 self .noisy_pressure_signal = []
196263 self .clean_pressure_signal = []
@@ -200,32 +267,19 @@ def __init__(
200267 self .clean_pressure_signal_function = Function (0 )
201268 self .noisy_pressure_signal_function = Function (0 )
202269 self .noise_signal_function = Function (0 )
203- self .radius = radius
204- self .height = height or radius
205- self .porosity = porosity
206- self .added_mass_coefficient = 1.068 * (
207- 1
208- - 1.465 * self .porosity
209- - 0.25975 * self .porosity ** 2
210- + 1.2626 * self .porosity ** 3
211- )
212-
213270 alpha , beta = self .noise_corr
214271 self .noise_function = lambda : (
215272 alpha * self .noise_signal [- 1 ][1 ]
216273 + beta * np .random .normal (noise [0 ], noise [1 ])
217274 )
218275
219- self .prints = _ParachutePrints (self )
220-
221- self .__evaluate_trigger_function (trigger )
222-
223276 def __evaluate_trigger_function (self , trigger ):
224277 """This is used to set the triggerfunc attribute that will be used to
225278 interact with the Flight class.
226279 """
227280 # pylint: disable=unused-argument, function-redefined
228- # The parachute is deployed by a custom function
281+
282+ # Case 1: The parachute is deployed by a custom function
229283 if callable (trigger ):
230284 # work around for having added sensors to parachute triggers
231285 # to avoid breaking changes
@@ -238,26 +292,29 @@ def triggerfunc(p, h, y, sensors):
238292
239293 self .triggerfunc = triggerfunc
240294
295+ # Case 2: The parachute is deployed at a given height
241296 elif isinstance (trigger , (int , float )):
242297 # The parachute is deployed at a given height
243- def triggerfunc (p , h , y , sensors ): # pylint: disable=unused-argument
298+ def triggerfunc (p , h , y , sensors ):
244299 # p = pressure considering parachute noise signal
245300 # h = height above ground level considering parachute noise signal
246301 # y = [x, y, z, vx, vy, vz, e0, e1, e2, e3, w1, w2, w3]
247302 return y [5 ] < 0 and h < trigger
248303
249304 self .triggerfunc = triggerfunc
250305
306+ # Case 3: The parachute is deployed at apogee
251307 elif trigger .lower () == "apogee" :
252308 # The parachute is deployed at apogee
253- def triggerfunc (p , h , y , sensors ): # pylint: disable=unused-argument
309+ def triggerfunc (p , h , y , sensors ):
254310 # p = pressure considering parachute noise signal
255311 # h = height above ground level considering parachute noise signal
256312 # y = [x, y, z, vx, vy, vz, e0, e1, e2, e3, w1, w2, w3]
257313 return y [5 ] < 0
258314
259315 self .triggerfunc = triggerfunc
260316
317+ # Case 4: Invalid trigger input
261318 else :
262319 raise ValueError (
263320 f"Unable to set the trigger function for parachute '{ self .name } '. "
@@ -289,7 +346,7 @@ def info(self):
289346 def all_info (self ):
290347 """Prints all information about the Parachute class."""
291348 self .info ()
292- # self.plots.all() # Parachutes still doesn't have plots
349+ # self.plots.all() # TODO: Parachutes still doesn't have plots
293350
294351 def to_dict (self , ** kwargs ):
295352 allow_pickle = kwargs .get ("allow_pickle" , True )
@@ -309,6 +366,7 @@ def to_dict(self, **kwargs):
309366 "lag" : self .lag ,
310367 "noise" : self .noise ,
311368 "radius" : self .radius ,
369+ "drag_coefficient" : self .drag_coefficient ,
312370 "height" : self .height ,
313371 "porosity" : self .porosity ,
314372 }
@@ -341,7 +399,8 @@ def from_dict(cls, data):
341399 sampling_rate = data ["sampling_rate" ],
342400 lag = data ["lag" ],
343401 noise = data ["noise" ],
344- radius = data .get ("radius" , 1.5 ),
402+ radius = data .get ("radius" , None ),
403+ drag_coefficient = data .get ("drag_coefficient" , 1.4 ),
345404 height = data .get ("height" , None ),
346405 porosity = data .get ("porosity" , 0.0432 ),
347406 )
0 commit comments