|
| 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