Skip to content

Commit bc64b7a

Browse files
committed
mnt: add missing parachute.py file
1 parent 5bfc866 commit bc64b7a

1 file changed

Lines changed: 361 additions & 0 deletions

File tree

Lines changed: 361 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,361 @@
1+
from abc import ABC, abstractmethod
2+
from inspect import signature
3+
4+
import numpy as np
5+
6+
from rocketpy.tools import from_hex_decode, to_hex_encode
7+
8+
from ...mathutils.function import Function
9+
from ...prints.parachute_prints import _ParachutePrints
10+
11+
12+
class Parachute(ABC):
13+
"""Abstract class to specify characteristics and useful operations for
14+
parachutes. Cannot be instantiated.
15+
16+
Attributes
17+
----------
18+
Parachute.name : string
19+
Parachute name, such as drogue and main. Has no impact in
20+
simulation, as it is only used to display data in a more
21+
organized matter.
22+
Parachute.parachute_type : string
23+
Parachute type, such as hemispherical and parafoil.
24+
Parachute.trigger : callable, float, str
25+
This parameter defines the trigger condition for the parachute ejection
26+
system. It can be one of the following:
27+
28+
- A callable function that takes four arguments:
29+
1. Freestream pressure in pascals.
30+
2. Height in meters above ground level.
31+
3. The state vector of the simulation, which is defined as:
32+
33+
`[x, y, z, vx, vy, vz, e0, e1, e2, e3, wx, wy, wz]`.
34+
35+
4. A list of sensors that are attached to the rocket. The most recent
36+
measurements of the sensors are provided with the
37+
``sensor.measurement`` attribute. The sensors are listed in the same
38+
order as they are added to the rocket.
39+
40+
The function should return ``True`` if the parachute ejection system
41+
should be triggered and False otherwise. The function will be called
42+
according to the specified sampling rate.
43+
44+
- A float value, representing an absolute height in meters. In this
45+
case, the parachute will be ejected when the rocket reaches this height
46+
above ground level.
47+
48+
- The string "apogee" which triggers the parachute at apogee, i.e.,
49+
when the rocket reaches its highest point and starts descending.
50+
51+
52+
Parachute.triggerfunc : function
53+
Trigger function created from the trigger used to evaluate the trigger
54+
condition for the parachute ejection system. It is a callable function
55+
that takes three arguments: Freestream pressure in Pa, Height above
56+
ground level in meters, and the state vector of the simulation. The
57+
returns ``True`` if the parachute ejection system should be triggered
58+
and ``False`` otherwise.
59+
60+
.. note:
61+
62+
The function will be called according to the sampling rate specified.
63+
64+
Parachute.sampling_rate : float
65+
Sampling rate, in Hz, for the trigger function.
66+
Parachute.lag : float
67+
Time, in seconds, between the parachute ejection system is triggered
68+
and the parachute is fully opened.
69+
Parachute.noise : tuple, list
70+
List in the format (mean, standard deviation, time-correlation).
71+
The values are used to add noise to the pressure signal which is passed
72+
to the trigger function. Default value is (0, 0, 0). Units are in Pa.
73+
Parachute.noise_bias : float
74+
Mean value of the noise added to the pressure signal, which is
75+
passed to the trigger function. Unit is in Pa.
76+
Parachute.noise_deviation : float
77+
Standard deviation of the noise added to the pressure signal,
78+
which is passed to the trigger function. Unit is in Pa.
79+
Parachute.noise_corr : tuple, list
80+
Tuple with the correlation between noise and time.
81+
Parachute.noise_signal : list of tuple
82+
List of (t, noise signal) corresponding to signal passed to
83+
trigger function. Completed after running a simulation.
84+
Parachute.noisy_pressure_signal : list of tuple
85+
List of (t, noisy pressure signal) that is passed to the
86+
trigger function. Completed after running a simulation.
87+
Parachute.clean_pressure_signal : list of tuple
88+
List of (t, clean pressure signal) corresponding to signal passed to
89+
trigger function. Completed after running a simulation.
90+
Parachute.noise_signal_function : Function
91+
Function of noiseSignal.
92+
Parachute.noisy_pressure_signal_function : Function
93+
Function of noisy_pressure_signal.
94+
Parachute.clean_pressure_signal_function : Function
95+
Function of clean_pressure_signal.
96+
"""
97+
98+
def __init__(
99+
self,
100+
name,
101+
parachute_type,
102+
trigger,
103+
sampling_rate,
104+
lag=0,
105+
noise=(0, 0, 0),
106+
):
107+
"""Initializes Parachute class.
108+
109+
Parameters
110+
----------
111+
name : string
112+
Parachute name, such as drogue and main. Has no impact in
113+
simulation, as it is only used to display data in a more
114+
organized matter.
115+
parachute_type : string
116+
Parachute type, such as hemispherical and parafoil.
117+
trigger : callable, float, str
118+
Defines the trigger condition for the parachute ejection system. It
119+
can be one of the following:
120+
121+
- A callable function that takes three arguments: \
122+
123+
1. Freestream pressure in pascals.
124+
2. Height in meters above ground level.
125+
3. The state vector of the simulation, which is defined as: \
126+
127+
.. code-block:: python
128+
129+
u = [x, y, z, vx, vy, vz, e0, e1, e2, e3, wx, wy, wz]
130+
131+
.. note::
132+
133+
The function should return ``True`` if the parachute \
134+
ejection system should be triggered and ``False`` otherwise.
135+
- A float value, representing an absolute height in meters. In this \
136+
case, the parachute will be ejected when the rocket reaches this \
137+
height above ground level.
138+
- The string "apogee" which triggers the parachute at apogee, i.e., \
139+
when the rocket reaches its highest point and starts descending.
140+
141+
.. note::
142+
143+
The function will be called according to the sampling rate specified.
144+
sampling_rate : float
145+
Sampling rate in which the parachute trigger will be checked at.
146+
It is used to simulate the refresh rate of onboard sensors such
147+
as barometers. Default value is 100. Value must be given in hertz.
148+
lag : float, optional
149+
Time between the parachute ejection system is triggered and the
150+
parachute is fully opened. During this time, the simulation will
151+
consider the rocket as flying without a parachute. Default value
152+
is 0. Must be given in seconds.
153+
noise : tuple, list, optional
154+
List in the format (mean, standard deviation, time-correlation).
155+
The values are used to add noise to the pressure signal which is
156+
passed to the trigger function. Default value is ``(0, 0, 0)``.
157+
Units are in Pa.
158+
"""
159+
160+
# Save arguments as attributes
161+
self.name = name
162+
self.parachute_type = parachute_type
163+
self.trigger = trigger
164+
self.sampling_rate = sampling_rate
165+
self.lag = lag
166+
self.noise = noise
167+
self.__init_noise(noise)
168+
self.__evaluate_trigger_function(trigger)
169+
170+
# Prints and plots
171+
self.prints = _ParachutePrints(self)
172+
173+
def __init_noise(self, noise):
174+
"""Initializes all noise-related attributes.
175+
176+
Parameters
177+
----------
178+
noise : tuple, list
179+
List in the format (mean, standard deviation, time-correlation).
180+
"""
181+
self.noise_signal = [[-1e-6, np.random.normal(noise[0], noise[1])]]
182+
self.noisy_pressure_signal = []
183+
self.clean_pressure_signal = []
184+
self.noise_bias = noise[0]
185+
self.noise_deviation = noise[1]
186+
self.noise_corr = (noise[2], (1 - noise[2] ** 2) ** 0.5)
187+
self.clean_pressure_signal_function = Function(0)
188+
self.noisy_pressure_signal_function = Function(0)
189+
self.noise_signal_function = Function(0)
190+
alpha, beta = self.noise_corr
191+
self.noise_function = lambda: (
192+
alpha * self.noise_signal[-1][1]
193+
+ beta * np.random.normal(noise[0], noise[1])
194+
)
195+
196+
def __evaluate_trigger_function(self, trigger):
197+
"""This is used to set the triggerfunc attribute that will be used to
198+
interact with the Flight class.
199+
"""
200+
# pylint: disable=unused-argument, function-redefined
201+
202+
# Case 1: The parachute is deployed by a custom function
203+
if callable(trigger):
204+
# work around for having added sensors to parachute triggers
205+
# to avoid breaking changes
206+
triggerfunc = trigger
207+
sig = signature(triggerfunc)
208+
if len(sig.parameters) == 3:
209+
210+
def triggerfunc(p, h, y, sensors):
211+
return trigger(p, h, y)
212+
213+
self.triggerfunc = triggerfunc
214+
215+
# Case 2: The parachute is deployed at a given height
216+
elif isinstance(trigger, (int, float)):
217+
# The parachute is deployed at a given height
218+
def triggerfunc(p, h, y, sensors):
219+
# p = pressure considering parachute noise signal
220+
# h = height above ground level considering parachute noise signal
221+
# y = [x, y, z, vx, vy, vz, e0, e1, e2, e3, w1, w2, w3]
222+
return y[5] < 0 and h < trigger
223+
224+
self.triggerfunc = triggerfunc
225+
226+
# Case 3: The parachute is deployed at apogee
227+
elif isinstance(trigger, str) and trigger.lower() == "apogee":
228+
# The parachute is deployed at apogee
229+
def triggerfunc(p, h, y, sensors):
230+
# p = pressure considering parachute noise signal
231+
# h = height above ground level considering parachute noise signal
232+
# y = [x, y, z, vx, vy, vz, e0, e1, e2, e3, w1, w2, w3]
233+
return y[5] < 0
234+
235+
self.triggerfunc = triggerfunc
236+
237+
# Case 4: Invalid trigger input
238+
else:
239+
raise ValueError(
240+
f"Unable to set the trigger function for parachute '{self.name}'. "
241+
+ "Trigger must be a callable, a float value or the string 'apogee'. "
242+
+ "See the Parachute class documentation for more information."
243+
)
244+
245+
def __str__(self):
246+
"""Returns a string representation of the Parachute class.
247+
248+
Returns
249+
-------
250+
string
251+
String representation of Parachute class. It is human readable.
252+
"""
253+
return f"Parachute {self.name.title()} of type {self.parachute_type}"
254+
255+
def __repr__(self):
256+
"""Representation method for the class, useful when debugging."""
257+
return (
258+
f"<Parachute {self.name} of type {self.parachute_type} "
259+
+ f"(lag = {self.lag:.4f} m2, trigger = {self.trigger})>"
260+
)
261+
262+
def info(self):
263+
"""Prints information about the Parachute class."""
264+
self.prints.all()
265+
266+
def all_info(self):
267+
"""Prints all information about the Parachute class."""
268+
self.info()
269+
270+
@abstractmethod
271+
def add_information_to_flight(self, flight_obj, additional_info):
272+
"""Adds parachute information to flight"""
273+
274+
@abstractmethod
275+
def u_dot(self, t, u, flight_information, post_processing=False):
276+
"""Calculates derivative of u state vector with respect to time
277+
when rocket is flying under parachute. Each parachute type has
278+
279+
280+
Parameters
281+
----------
282+
t : float
283+
Time in seconds
284+
u : list
285+
State vector defined by u = [x, y, z, vx, vy, vz, e0, e1,
286+
e2, e3, omega1, omega2, omega3].
287+
flight_information : dictionary
288+
A dictionary containing additional information used in
289+
the parachute equations of motion. Examples are
290+
Environment and Rocket data
291+
post_processing : bool, optional
292+
If True, adds flight data information directly to self
293+
variables such as self.angle_of_attack. Default is False.
294+
295+
Return
296+
------
297+
u_dot : dict
298+
A dictionary containing two or three keys
299+
1) state: State vector which depends on the parachute model.
300+
2) additional_information: information as dict that is added
301+
to the 'parachutes_info' attribute in the Flight class.
302+
3) post_processing_information: State vector containing
303+
post processing information.
304+
305+
"""
306+
307+
def to_dict(self, **kwargs):
308+
allow_pickle = kwargs.get("allow_pickle", True)
309+
trigger = self.trigger
310+
311+
if callable(self.trigger) and not isinstance(self.trigger, Function):
312+
if allow_pickle:
313+
trigger = to_hex_encode(trigger)
314+
else:
315+
trigger = trigger.__name__
316+
317+
data = {
318+
"name": self.name,
319+
"parachute_type": self.parachute_type,
320+
"cd_s": self.cd_s,
321+
"trigger": trigger,
322+
"sampling_rate": self.sampling_rate,
323+
"lag": self.lag,
324+
"noise": self.noise,
325+
"radius": self.radius,
326+
"drag_coefficient": self.drag_coefficient,
327+
"height": self.height,
328+
"porosity": self.porosity,
329+
}
330+
331+
if kwargs.get("include_outputs", False):
332+
data["noise_signal"] = self.noise_signal
333+
data["noise_function"] = (
334+
to_hex_encode(self.noise_function)
335+
if allow_pickle
336+
else self.noise_function.__name__
337+
)
338+
data["noisy_pressure_signal"] = self.noisy_pressure_signal
339+
data["clean_pressure_signal"] = self.clean_pressure_signal
340+
341+
return data
342+
343+
@classmethod
344+
def from_dict(cls, data):
345+
trigger = data["trigger"]
346+
347+
try:
348+
trigger = from_hex_decode(trigger)
349+
except (TypeError, ValueError):
350+
pass
351+
352+
parachute = cls(
353+
name=data["name"],
354+
parachute_type=data["parachute_type"],
355+
trigger=trigger,
356+
sampling_rate=data["sampling_rate"],
357+
lag=data["lag"],
358+
noise=data["noise"],
359+
)
360+
361+
return parachute

0 commit comments

Comments
 (0)