From b77bd9eaad817945c2366278e2f929292554cfd6 Mon Sep 17 00:00:00 2001 From: Gui-FernandesBR Date: Thu, 9 Jul 2026 23:57:47 -0300 Subject: [PATCH 1/5] ENH: restore concrete generic Parachute; make HemisphericalParachute a thin subclass Addresses the v1.13.0 pre-release review concerns raised in PR #1048: a geometry-specific class only makes sense alongside a generic case, and making Parachute abstract broke direct instantiation for no modeling gain (the hemispherical added-mass physics has been Parachute's behavior since at least v1.11). - Parachute is concrete again with its historical v1.12.1 signature and physics; u_dot and serialization move back into it. - HemisphericalParachute becomes a thin subclass that only makes the canopy geometry explicit and does not expose the noise parameter (noisy trigger readings should come from Sensors instead). - The legacy Rocket.add_parachute(name, cd_s, ...) path builds a generic Parachute again, keeping the FutureWarning nudge to the object API. - StochasticParachute.create_object returns a generic Parachute. - Legacy .rpy signatures remap to Parachute at its new module path. Co-Authored-By: Claude Fable 5 --- rocketpy/_encoders.py | 11 +- rocketpy/rocket/parachute.py | 10 +- .../parachutes/hemispherical_parachute.py | 366 +++--------------- rocketpy/rocket/parachutes/parachute.py | 237 ++++++++++-- rocketpy/rocket/rocket.py | 13 +- rocketpy/stochastic/stochastic_parachute.py | 4 +- 6 files changed, 281 insertions(+), 360 deletions(-) diff --git a/rocketpy/_encoders.py b/rocketpy/_encoders.py index 669034636..a4fb9503e 100644 --- a/rocketpy/_encoders.py +++ b/rocketpy/_encoders.py @@ -250,14 +250,11 @@ def get_class_from_signature(signature): name = signature["name"] # Backward compatibility: the parachute module was moved to the - # ``rocketpy.rocket.parachutes`` subpackage and the old concrete - # ``Parachute`` class became an abstract base, with the hemispherical model - # split out. Remap the legacy signature so ``.rpy`` files saved with older - # versions reconstruct as a concrete ``HemisphericalParachute`` instead of - # silently falling back to a raw dictionary. + # ``rocketpy.rocket.parachutes`` subpackage. Remap the legacy signature so + # ``.rpy`` files saved with older versions import ``Parachute`` from its + # new location instead of silently falling back to a raw dictionary. if module_name == "rocketpy.rocket.parachute" and name == "Parachute": - module_name = "rocketpy.rocket.parachutes.hemispherical_parachute" - name = "HemisphericalParachute" + module_name = "rocketpy.rocket.parachutes.parachute" module = import_module(module_name) inner_class = None diff --git a/rocketpy/rocket/parachute.py b/rocketpy/rocket/parachute.py index 137e98e3a..fb909c86d 100644 --- a/rocketpy/rocket/parachute.py +++ b/rocketpy/rocket/parachute.py @@ -1,10 +1,10 @@ """Backward-compatibility shim for the relocated parachute module. The parachute classes moved from ``rocketpy.rocket.parachute`` to the -``rocketpy.rocket.parachutes`` subpackage, and the old concrete ``Parachute`` -class became an abstract base with the hemispherical model split out into -``HemisphericalParachute``. Importing from this module still works for backward -compatibility but is deprecated and will be removed in v1.15.0. +``rocketpy.rocket.parachutes`` subpackage, which holds the generic +``Parachute`` class and geometry-specific models such as +``HemisphericalParachute``. Importing from this module still works for +backward compatibility but is deprecated and will be removed in v1.15.0. """ import warnings @@ -15,7 +15,7 @@ class became an abstract base with the hemispherical model split out into warnings.warn( "Importing from 'rocketpy.rocket.parachute' is deprecated and will be " "removed in v1.15.0. Import from 'rocketpy.rocket.parachutes' instead, " - "e.g. 'from rocketpy.rocket.parachutes import HemisphericalParachute'.", + "e.g. 'from rocketpy.rocket.parachutes import Parachute'.", DeprecationWarning, stacklevel=2, ) diff --git a/rocketpy/rocket/parachutes/hemispherical_parachute.py b/rocketpy/rocket/parachutes/hemispherical_parachute.py index 25fe33dba..495847d40 100644 --- a/rocketpy/rocket/parachutes/hemispherical_parachute.py +++ b/rocketpy/rocket/parachutes/hemispherical_parachute.py @@ -1,118 +1,46 @@ -import numpy as np - from .parachute import Parachute class HemisphericalParachute(Parachute): """Implements a hemispherical parachute. - Attributes - ---------- - HemisphericalParachute.name : string - Parachute name, such as drogue and main. Has no impact in - simulation, as it is only used to display data in a more - organized matter. - HemisphericalParachute.parachute_type : string - Parachute type, such as hemispherical and parafoil. - HemisphericalParachute.cd_s : float - Drag coefficient times reference area for parachute. It has units of - area and must be given in squared meters. - HemisphericalParachute.trigger : callable, float, str - This parameter defines the trigger condition for the parachute ejection - system. It can be one of the following: - - - A callable function that takes four arguments: - 1. Freestream pressure in pascals. - 2. Height in meters above ground level. - 3. The state vector of the simulation, which is defined as: - - `[x, y, z, vx, vy, vz, e0, e1, e2, e3, wx, wy, wz]`. - - 4. A list of sensors that are attached to the rocket. The most recent - measurements of the sensors are provided with the - ``sensor.measurement`` attribute. The sensors are listed in the same - order as they are added to the rocket. - - The function should return ``True`` if the parachute ejection system - should be triggered and False otherwise. The function will be called - according to the specified sampling rate. + Specializes the generic :class:`Parachute + ` model for hemispherical + canopies, making the canopy geometry explicit. The descent dynamics are + the same as the generic model, which already computes the added mass from + an equivalent hemispheroid. - - A float value, representing an absolute height in meters. In this - case, the parachute will be ejected when the rocket reaches this height - above ground level. + Unlike the generic ``Parachute``, this class does not expose the ``noise`` + parameter. To simulate noisy trigger readings, attach a sensor with + measurement noise to the rocket (see :class:`Barometer + `) and read it from the trigger + function instead. - - The string "apogee" which triggers the parachute at apogee, i.e., - when the rocket reaches its highest point and starts descending. + See Also + -------- + :class:`Parachute ` for + the documentation of all inherited attributes and of the descent dynamics. - - HemisphericalParachute.triggerfunc : function - Trigger function created from the trigger used to evaluate the trigger - condition for the parachute ejection system. It is a callable function - that takes three arguments: Freestream pressure in Pa, Height above - ground level in meters, and the state vector of the simulation. The - returns ``True`` if the parachute ejection system should be triggered - and ``False`` otherwise. - - .. note: - - The function will be called according to the sampling rate specified. - - HemisphericalParachute.sampling_rate : float - Sampling rate, in Hz, for the trigger function. - HemisphericalParachute.lag : float - Time, in seconds, between the parachute ejection system is triggered - and the parachute is fully opened. - HemisphericalParachute.noise : tuple, list - List in the format (mean, standard deviation, time-correlation). - The values are used to add noise to the pressure signal which is passed - to the trigger function. Default value is (0, 0, 0). Units are in Pa. - HemisphericalParachute.noise_bias : float - Mean value of the noise added to the pressure signal, which is - passed to the trigger function. Unit is in Pa. - HemisphericalParachute.noise_deviation : float - Standard deviation of the noise added to the pressure signal, - which is passed to the trigger function. Unit is in Pa. - HemisphericalParachute.noise_corr : tuple, list - Tuple with the correlation between noise and time. - HemisphericalParachute.noise_signal : list of tuple - List of (t, noise signal) corresponding to signal passed to - trigger function. Completed after running a simulation. - HemisphericalParachute.noisy_pressure_signal : list of tuple - List of (t, noisy pressure signal) that is passed to the - trigger function. Completed after running a simulation. - HemisphericalParachute.clean_pressure_signal : list of tuple - List of (t, clean pressure signal) corresponding to signal passed to - trigger function. Completed after running a simulation. - HemisphericalParachute.noise_signal_function : Function - Function of noiseSignal. - HemisphericalParachute.noisy_pressure_signal_function : Function - Function of noisy_pressure_signal. - HemisphericalParachute.clean_pressure_signal_function : Function - Function of clean_pressure_signal. + Attributes + ---------- + HemisphericalParachute.parachute_type : string + Set to "hemispherical". HemisphericalParachute.drag_coefficient : float - Drag coefficient of the inflated canopy shape, used only when + Drag coefficient of the inflated hemispherical canopy, 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. + ``cd_s``: ``R = sqrt(cd_s / (drag_coefficient * pi))``. Default value + is 1.4 (NASA SP-8066). HemisphericalParachute.radius : float - Length of the non-unique semi-axis (radius) of the inflated hemispherical - parachute in meters. If not provided at construction time, it is - estimated from ``cd_s`` and ``drag_coefficient``. + Length of the non-unique semi-axis (radius) of the inflated + hemispherical parachute in meters. If not provided at construction + time, it is estimated from ``cd_s`` and ``drag_coefficient``. HemisphericalParachute.height : float Length of the unique semi-axis (height) of the inflated hemispherical - parachute in meters. - HemisphericalParachute.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 value of 0.0432 is - chosen so that the resulting ``added_mass_coefficient`` equals - approximately 1.0 ("neutral" added-mass behavior). - HemisphericalParachute.added_mass_coefficient : float - Coefficient used to calculate the added-mass due to dragged air. It is - calculated from the porosity of the parachute. + parachute in meters. Default value is the radius of the parachute. """ + parachute_type = "hemispherical" + def __init__( self, name, @@ -120,13 +48,12 @@ def __init__( trigger, sampling_rate, lag=0, - noise=(0, 0, 0), radius=None, height=None, porosity=0.0432, drag_coefficient=1.4, ): - """Initializes Parachute class. + """Initializes HemisphericalParachute class. Parameters ---------- @@ -137,32 +64,10 @@ def __init__( cd_s : float Drag coefficient times reference area of the parachute. trigger : callable, float, str - Defines the trigger condition for the parachute ejection system. It - can be one of the following: - - - A callable function that takes three arguments: \ - - 1. Freestream pressure in pascals. - 2. Height in meters above ground level. - 3. The state vector of the simulation, which is defined as: \ - - .. code-block:: python - - u = [x, y, z, vx, vy, vz, e0, e1, e2, e3, wx, wy, wz] - - .. note:: - - The function should return ``True`` if the parachute \ - ejection system should be triggered and ``False`` otherwise. - - A float value, representing an absolute height in meters. In this \ - case, the parachute will be ejected when the rocket reaches this \ - height above ground level. - - The string "apogee" which triggers the parachute at apogee, i.e., \ - when the rocket reaches its highest point and starts descending. - - .. note:: - - The function will be called according to the sampling rate specified. + Defines the trigger condition for the parachute ejection system. + See :class:`Parachute + ` for the + supported forms. sampling_rate : float Sampling rate in which the parachute trigger will be checked at. It is used to simulate the refresh rate of onboard sensors such @@ -172,11 +77,6 @@ def __init__( parachute is fully opened. During this time, the simulation will consider the rocket as flying without a parachute. Default value is 0. Must be given in seconds. - noise : tuple, list, optional - List in the format (mean, standard deviation, time-correlation). - The values are used to add noise to the pressure signal which is - 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 hemispherical parachute. If not provided, it is estimated from @@ -184,9 +84,9 @@ def __init__( ``radius = sqrt(cd_s / (drag_coefficient * pi))``. Units are in meters. height : float, optional - Length of the unique semi-axis (height) of the inflated hemispherical - parachute. Default value is the radius of the parachute. - Units are in meters. + Length of the unique semi-axis (height) of the inflated + hemispherical 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 @@ -195,194 +95,25 @@ def __init__( ``added_mass_coefficient`` equals approximately 1.0 ("neutral" added-mass behavior). 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. + Drag coefficient of the inflated hemispherical canopy, 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``. Default value is + 1.4 (NASA SP-8066). Has no effect when ``radius`` is explicitly + provided. """ - - parachute_type = "hemispherical" super().__init__( name=name, - parachute_type=parachute_type, + cd_s=cd_s, trigger=trigger, sampling_rate=sampling_rate, lag=lag, - noise=noise, - ) - self.cd_s = cd_s - self.trigger = trigger - self.drag_coefficient = drag_coefficient - self.porosity = porosity - - # Initialize derived attributes - self.radius = self.__resolve_radius(radius, cd_s, drag_coefficient) - self.height = self.__resolve_height(height, self.radius) - self.added_mass_coefficient = self.__compute_added_mass_coefficient( - self.porosity - ) - - def __resolve_radius(self, radius, cd_s, drag_coefficient): - """Resolves parachute radius from input or aerodynamic relation.""" - if radius is not None: - return radius - - # cd_s = Cd * S = Cd * pi * R^2 => R = sqrt(cd_s / (Cd * pi)) - return np.sqrt(cd_s / (drag_coefficient * np.pi)) - - def __resolve_height(self, height, radius): - """Resolves parachute height defaulting to radius when not provided.""" - return height or radius - - def __compute_added_mass_coefficient(self, porosity): - """Computes the added-mass coefficient from canopy porosity.""" - return 1.068 * ( - 1 - 1.465 * porosity - 0.25975 * porosity**2 + 1.2626 * porosity**3 + radius=radius, + height=height, + porosity=porosity, + drag_coefficient=drag_coefficient, ) - def add_information_to_flight(self, flight_obj, additional_info): - """Adds parachute information to flight""" - drag = additional_info["drag"] - t = additional_info["t"] - if self.name not in flight_obj.parachutes_info.keys(): - flight_obj.parachutes_info[self.name] = {"drag": [], "t": []} - flight_obj.parachutes_info[self.name]["drag"].append(drag) - flight_obj.parachutes_info[self.name]["t"].append(t) - else: - # LSODA did not accept last solution, we replace it - if t == flight_obj.parachutes_info[self.name]["t"][-1]: - flight_obj.parachutes_info[self.name]["drag"][-1] = drag - flight_obj.parachutes_info[self.name]["t"][-1] = t - else: - flight_obj.parachutes_info[self.name]["drag"].append(drag) - flight_obj.parachutes_info[self.name]["t"].append(t) - - # pylint: disable=too-many-locals, too-many-statements - def u_dot(self, t, u, flight_information, post_processing=False): - """Calculates derivative of u state vector with respect to time - when rocket is flying under parachute. Each parachute type has - - - Parameters - ---------- - t : float - Time in seconds - u : list - State vector defined by u = [x, y, z, vx, vy, vz, e0, e1, - e2, e3, omega1, omega2, omega3]. - flight_information : dictionary - A dictionary containing additional information used in - the parachute equations of motion. Examples are - Environment and Rocket data - post_processing : bool, optional - If True, adds flight data information directly to self - variables such as self.angle_of_attack. Default is False. - - Return - ------ - u_dot : dict - A dictionary containing two or three keys - 1) state: State vector which depends on the parachute model. - 2) additional_information: information as dict that is added - to the 'parachutes_info' attribute in the Flight class. - 3) post_processing_information: State vector containing - post processing information. - - """ - # Get relevant state data - z, vx, vy, vz = u[2:6] - - env = flight_information["env"] - rocket = flight_information["rocket"] - - # Get atmospheric data - rho = env.density.get_value_opt(z) - wind_velocity_x = env.wind_velocity_x.get_value_opt(z) - wind_velocity_y = env.wind_velocity_y.get_value_opt(z) - - # Get the mass of the rocket - mp = rocket.dry_mass - - # Calculate added mass - ma = ( - self.added_mass_coefficient - * rho - * (2 / 3) - * np.pi - * self.radius**2 - * self.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 - - # Determine drag force - pseudo_drag = -0.5 * rho * self.cd_s * free_stream_speed - Dx = pseudo_drag * freestream_x - Dy = pseudo_drag * freestream_y - Dz = pseudo_drag * freestream_z - total_drag = np.sqrt(Dx**2 + Dy**2 + Dz**2) - ax = Dx / (mp + ma) - ay = Dy / (mp + ma) - az = (Dz - mp * env.gravity.get_value_opt(z)) / (mp + ma) - - # Add coriolis acceleration - _, w_earth_y, w_earth_z = env.earth_rotation_vector - ax -= 2 * (vz * w_earth_y - vy * w_earth_z) - ay -= 2 * (vx * w_earth_z) - az -= 2 * (-vx * w_earth_y) - - additional_info = { - "t": t, - "drag": total_drag, - } - output = { - "state": [vx, vy, vz, ax, ay, az, 0, 0, 0, 0, 0, 0, 0], - "additional_info": additional_info, - } - - if post_processing: - output["post_processing_information"] = [ - t, - ax, - ay, - az, - 0, - 0, - 0, - Dx, - Dy, - Dz, - 0, - 0, - 0, - 0, - ] - return output - - # serialization methods - def to_dict(self, **kwargs): - data = super().to_dict(**kwargs) - data.update( - { - "cd_s": self.cd_s, - "radius": self.radius, - "drag_coefficient": self.drag_coefficient, - "height": self.height, - "porosity": self.porosity, - } - ) - return data - @classmethod def from_dict(cls, data): return cls( @@ -390,10 +121,9 @@ def from_dict(cls, data): cd_s=data["cd_s"], trigger=cls._decode_trigger(data["trigger"]), sampling_rate=data["sampling_rate"], - lag=data["lag"], - noise=data["noise"], + lag=data.get("lag", 0), radius=data.get("radius", None), - drag_coefficient=data.get("drag_coefficient", 1.4), height=data.get("height", None), porosity=data.get("porosity", 0.0432), + drag_coefficient=data.get("drag_coefficient", 1.4), ) diff --git a/rocketpy/rocket/parachutes/parachute.py b/rocketpy/rocket/parachutes/parachute.py index a9b2c290d..8a1bb1cbf 100644 --- a/rocketpy/rocket/parachutes/parachute.py +++ b/rocketpy/rocket/parachutes/parachute.py @@ -1,4 +1,3 @@ -from abc import ABC, abstractmethod from inspect import signature import numpy as np @@ -9,9 +8,13 @@ from ...prints.parachute_prints import _ParachutePrints -class Parachute(ABC): - """Abstract class to specify characteristics and useful operations for - parachutes. Cannot be instantiated. +class Parachute: + """Generic parachute model. Specifies the characteristics and useful + operations of a parachute, including its descent dynamics. + + Specific canopy geometries may be modeled by subclasses, such as + :class:`HemisphericalParachute + `. Attributes ---------- @@ -20,7 +23,10 @@ class Parachute(ABC): simulation, as it is only used to display data in a more organized matter. Parachute.parachute_type : string - Parachute type, such as hemispherical and parafoil. + Parachute type, such as generic and hemispherical. + Parachute.cd_s : float + Drag coefficient times reference area for parachute. It has units of + area and must be given in squared meters. Parachute.trigger : callable, float, str This parameter defines the trigger condition for the parachute ejection system. It can be one of the following: @@ -102,16 +108,45 @@ class Parachute(ABC): 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 + parachute in meters, used to compute the added mass of the dragged + air. 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 + parachute in meters. Default value is the radius of the parachute. + 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 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. """ + parachute_type = "generic" + def __init__( self, name, - parachute_type, + cd_s, trigger, sampling_rate, lag=0, noise=(0, 0, 0), + radius=None, + height=None, + porosity=0.0432, + drag_coefficient=1.4, ): """Initializes Parachute class. @@ -121,8 +156,8 @@ def __init__( Parachute name, such as drogue and main. Has no impact in simulation, as it is only used to display data in a more organized matter. - parachute_type : string - Parachute type, such as hemispherical and parafoil. + cd_s : float + Drag coefficient times reference area of the parachute. trigger : callable, float, str Defines the trigger condition for the parachute ejection system. It can be one of the following: @@ -166,21 +201,77 @@ def __init__( The values are used to add noise to the pressure signal which is 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 + parachute, used to compute the added mass of the dragged air. + If not provided, it is estimated from ``cd_s`` and + ``drag_coefficient`` using: + ``radius = sqrt(cd_s / (drag_coefficient * pi))``. + Units are in meters. + height : float, optional + Length of the unique semi-axis (height) of the inflated + 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 + value of 0.0432 is chosen so that the resulting + ``added_mass_coefficient`` equals approximately 1.0 ("neutral" + added-mass behavior). + 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. """ # Save arguments as attributes self.name = name - self.parachute_type = parachute_type + self.cd_s = cd_s self.trigger = trigger self.sampling_rate = sampling_rate self.lag = lag self.noise = noise + self.drag_coefficient = drag_coefficient + self.porosity = porosity self.__init_noise(noise) self.__evaluate_trigger_function(trigger) + # Initialize derived attributes + self.radius = self.__resolve_radius(radius, cd_s, drag_coefficient) + self.height = self.__resolve_height(height, self.radius) + self.added_mass_coefficient = self.__compute_added_mass_coefficient( + self.porosity + ) + # Prints and plots self.prints = _ParachutePrints(self) + def __resolve_radius(self, radius, cd_s, drag_coefficient): + """Resolves parachute radius from input or aerodynamic relation.""" + if radius is not None: + return radius + + # cd_s = Cd * S = Cd * pi * R^2 => R = sqrt(cd_s / (Cd * pi)) + return np.sqrt(cd_s / (drag_coefficient * np.pi)) + + def __resolve_height(self, height, radius): + """Resolves parachute height defaulting to radius when not provided.""" + return height or radius + + def __compute_added_mass_coefficient(self, porosity): + """Computes the added-mass coefficient from canopy porosity.""" + return 1.068 * ( + 1 - 1.465 * porosity - 0.25975 * porosity**2 + 1.2626 * porosity**3 + ) + def __init_noise(self, noise): """Initializes all noise-related attributes. @@ -322,15 +413,29 @@ def all_info(self): """Prints all information about the Parachute class.""" self.info() - @abstractmethod def add_information_to_flight(self, flight_obj, additional_info): """Adds parachute information to flight""" + drag = additional_info["drag"] + t = additional_info["t"] + if self.name not in flight_obj.parachutes_info.keys(): + flight_obj.parachutes_info[self.name] = {"drag": [], "t": []} + flight_obj.parachutes_info[self.name]["drag"].append(drag) + flight_obj.parachutes_info[self.name]["t"].append(t) + else: + # LSODA did not accept last solution, we replace it + if t == flight_obj.parachutes_info[self.name]["t"][-1]: + flight_obj.parachutes_info[self.name]["drag"][-1] = drag + flight_obj.parachutes_info[self.name]["t"][-1] = t + else: + flight_obj.parachutes_info[self.name]["drag"].append(drag) + flight_obj.parachutes_info[self.name]["t"].append(t) - @abstractmethod + # pylint: disable=too-many-locals, too-many-statements def u_dot(self, t, u, flight_information, post_processing=False): """Calculates derivative of u state vector with respect to time - when rocket is flying under parachute. Each parachute type has - + when rocket is flying under parachute. A 3 DOF approximation is + used. Subclasses may override this method to implement + geometry-specific descent dynamics. Parameters ---------- @@ -358,11 +463,85 @@ def u_dot(self, t, u, flight_information, post_processing=False): post processing information. """ + # Get relevant state data + z, vx, vy, vz = u[2:6] + + env = flight_information["env"] + rocket = flight_information["rocket"] + + # Get atmospheric data + rho = env.density.get_value_opt(z) + wind_velocity_x = env.wind_velocity_x.get_value_opt(z) + wind_velocity_y = env.wind_velocity_y.get_value_opt(z) + + # Get the mass of the rocket + mp = rocket.dry_mass + + # Calculate added mass + ma = ( + self.added_mass_coefficient + * rho + * (2 / 3) + * np.pi + * self.radius**2 + * self.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 + + # Determine drag force + pseudo_drag = -0.5 * rho * self.cd_s * free_stream_speed + Dx = pseudo_drag * freestream_x + Dy = pseudo_drag * freestream_y + Dz = pseudo_drag * freestream_z + total_drag = np.sqrt(Dx**2 + Dy**2 + Dz**2) + ax = Dx / (mp + ma) + ay = Dy / (mp + ma) + az = (Dz - mp * env.gravity.get_value_opt(z)) / (mp + ma) + + # Add coriolis acceleration + _, w_earth_y, w_earth_z = env.earth_rotation_vector + ax -= 2 * (vz * w_earth_y - vy * w_earth_z) + ay -= 2 * (vx * w_earth_z) + az -= 2 * (-vx * w_earth_y) + + additional_info = { + "t": t, + "drag": total_drag, + } + output = { + "state": [vx, vy, vz, ax, ay, az, 0, 0, 0, 0, 0, 0, 0], + "additional_info": additional_info, + } + if post_processing: + output["post_processing_information"] = [ + t, + ax, + ay, + az, + 0, + 0, + 0, + Dx, + Dy, + Dz, + 0, + 0, + 0, + 0, + ] + return output + + # serialization methods def to_dict(self, **kwargs): - """Serializes the fields shared by every parachute model. Subclasses - should call ``super().to_dict(**kwargs)`` and add their model-specific - attributes to the returned dictionary.""" + """Serializes the parachute attributes. Subclasses should call + ``super().to_dict(**kwargs)`` and add their model-specific attributes + to the returned dictionary.""" allow_pickle = kwargs.get("allow_pickle", True) trigger = self.trigger @@ -379,6 +558,11 @@ def to_dict(self, **kwargs): "sampling_rate": self.sampling_rate, "lag": self.lag, "noise": self.noise, + "cd_s": self.cd_s, + "radius": self.radius, + "drag_coefficient": self.drag_coefficient, + "height": self.height, + "porosity": self.porosity, } if kwargs.get("include_outputs", False): @@ -403,10 +587,19 @@ def _decode_trigger(trigger): return trigger @classmethod - @abstractmethod def from_dict(cls, data): - """Reconstructs a parachute from a serialized dictionary. - - Each concrete model must implement this using its own constructor, - since the required arguments differ per model. The shared - ``_decode_trigger`` helper handles the ``trigger`` field.""" + """Reconstructs a parachute from a serialized dictionary. Subclasses + with different constructor arguments must override this method. The + shared ``_decode_trigger`` helper handles the ``trigger`` field.""" + return cls( + name=data["name"], + cd_s=data["cd_s"], + trigger=cls._decode_trigger(data["trigger"]), + sampling_rate=data["sampling_rate"], + lag=data.get("lag", 0), + noise=data.get("noise", (0, 0, 0)), + radius=data.get("radius", None), + height=data.get("height", None), + porosity=data.get("porosity", 0.0432), + drag_coefficient=data.get("drag_coefficient", 1.4), + ) diff --git a/rocketpy/rocket/rocket.py b/rocketpy/rocket/rocket.py index ae9ba392b..6e87c927c 100644 --- a/rocketpy/rocket/rocket.py +++ b/rocketpy/rocket/rocket.py @@ -35,7 +35,6 @@ from rocketpy.rocket.aero_surface.fins.trapezoidal_fin import TrapezoidalFin from rocketpy.rocket.aero_surface.generic_surface import GenericSurface from rocketpy.rocket.components import Components -from rocketpy.rocket.parachutes.hemispherical_parachute import HemisphericalParachute from rocketpy.rocket.parachutes.parachute import Parachute from rocketpy.tools import ( deprecated, @@ -1746,8 +1745,9 @@ def add_parachute( if parachute is not None: if not isinstance(parachute, Parachute): raise TypeError( - "The 'parachute' argument must be an instance of a Parachute " - "subclass (e.g. 'HemisphericalParachute')." + "The 'parachute' argument must be an instance of the " + "'Parachute' class or one of its subclasses (e.g. " + "'HemisphericalParachute')." ) self.parachutes.append(parachute) else: @@ -1755,8 +1755,9 @@ def add_parachute( deprecation_message = ( "Passing parachute parameters directly to 'add_parachute' method is " + "deprecated and will be removed in version 1.14.0. Please create " - + "an object of class 'HemisphericalParachute' and pass it to the " - + "'parachute' argument of 'add_parachute' for the same behavior." + + "an object of class 'Parachute' (or one of its subclasses) and " + + "pass it to the 'parachute' argument of 'add_parachute' for the " + + "same behavior." ) warn(message=deprecation_message, category=FutureWarning, stacklevel=2) if name is None: @@ -1767,7 +1768,7 @@ def add_parachute( raise ValueError( "Invalid 'trigger' argument! Please provide a callable, float, or string!" ) - legacy_parachute = HemisphericalParachute( + legacy_parachute = Parachute( name, cd_s, trigger, diff --git a/rocketpy/stochastic/stochastic_parachute.py b/rocketpy/stochastic/stochastic_parachute.py index c0ad1ae1d..038907187 100644 --- a/rocketpy/stochastic/stochastic_parachute.py +++ b/rocketpy/stochastic/stochastic_parachute.py @@ -1,6 +1,6 @@ """Defines the StochasticParachute class.""" -from rocketpy.rocket import HemisphericalParachute +from rocketpy.rocket import Parachute from .stochastic_model import StochasticModel @@ -148,4 +148,4 @@ def create_object(self): Parachute object with the randomly generated input arguments. """ generated_dict = next(self.dict_generator()) - return HemisphericalParachute(**generated_dict) + return Parachute(**generated_dict) From ffb25ef429e772e030e045c5a1c49ea54b86a540 Mon Sep 17 00:00:00 2001 From: Gui-FernandesBR Date: Thu, 9 Jul 2026 23:57:59 -0300 Subject: [PATCH 2/5] TST: update parachute tests for the concrete generic Parachute - Class-hierarchy tests replace the abstract-base tests: generic Parachute is instantiable (with noise), HemisphericalParachute rejects noise, and from_dict round-trips both classes. - Legacy .rpy decode tests now expect the generic Parachute. - Fixtures and acceptance tests that pass noise return to the generic Parachute, their pre-refactor form. Co-Authored-By: Claude Fable 5 --- tests/acceptance/test_bella_lui_rocket.py | 4 +- tests/acceptance/test_ndrt_2020_rocket.py | 6 +- .../fixtures/parachutes/parachute_fixtures.py | 6 +- tests/unit/rocket/test_parachute.py | 70 +++++++++++++++---- .../stochastic/test_stochastic_parachute.py | 4 +- tests/unit/test_encoders.py | 8 +-- 6 files changed, 71 insertions(+), 27 deletions(-) diff --git a/tests/acceptance/test_bella_lui_rocket.py b/tests/acceptance/test_bella_lui_rocket.py index 54db9abac..d91abd1a1 100644 --- a/tests/acceptance/test_bella_lui_rocket.py +++ b/tests/acceptance/test_bella_lui_rocket.py @@ -10,7 +10,7 @@ Environment, Flight, Function, - HemisphericalParachute, + Parachute, Rocket, SolidMotor, ) @@ -139,7 +139,7 @@ def drogue_trigger(p, h, y): # activate drogue when vz < 0 m/s. return True if y[5] < 0 else False - drogue = HemisphericalParachute( + drogue = Parachute( "Drogue", cd_s=parameters.get("CdS_drogue")[0], trigger=drogue_trigger, diff --git a/tests/acceptance/test_ndrt_2020_rocket.py b/tests/acceptance/test_ndrt_2020_rocket.py index cd03b0012..df6a23cb9 100644 --- a/tests/acceptance/test_ndrt_2020_rocket.py +++ b/tests/acceptance/test_ndrt_2020_rocket.py @@ -3,7 +3,7 @@ import pytest from scipy.signal import savgol_filter -from rocketpy import Environment, Flight, HemisphericalParachute, Rocket, SolidMotor +from rocketpy import Environment, Flight, Parachute, Rocket, SolidMotor @pytest.mark.parametrize( @@ -155,7 +155,7 @@ def main_trigger(p, h, y): # pylint: disable=unused-argument # activate main when vz < 0 m/s and z < 167.64 m (AGL) or 550 ft (AGL) return True if y[5] < 0 and h < 167.64 else False - drogue = HemisphericalParachute( + drogue = Parachute( "Drogue", cd_s=parameters.get("cd_s_drogue")[0], trigger=drogue_trigger, @@ -163,7 +163,7 @@ def main_trigger(p, h, y): # pylint: disable=unused-argument lag=parameters.get("lag_rec")[0], noise=(0, 8.3, 0.5), ) - main = HemisphericalParachute( + main = Parachute( "Main", cd_s=parameters.get("cd_s_main")[0], trigger=main_trigger, diff --git a/tests/fixtures/parachutes/parachute_fixtures.py b/tests/fixtures/parachutes/parachute_fixtures.py index c74fab648..9723cda8e 100644 --- a/tests/fixtures/parachutes/parachute_fixtures.py +++ b/tests/fixtures/parachutes/parachute_fixtures.py @@ -1,6 +1,6 @@ import pytest -from rocketpy import HemisphericalParachute +from rocketpy import Parachute @pytest.fixture @@ -52,7 +52,7 @@ def calisto_main_chute(calisto_main_parachute_trigger): rocketpy.Parachute The main parachute of the Calisto rocket. """ - return HemisphericalParachute( + return Parachute( name="calisto_main_chute", cd_s=10.0, trigger=calisto_main_parachute_trigger, @@ -77,7 +77,7 @@ def calisto_drogue_chute(calisto_drogue_parachute_trigger): rocketpy.Parachute The drogue parachute of the Calisto rocket. """ - return HemisphericalParachute( + return Parachute( name="calisto_drogue_chute", cd_s=1.0, trigger=calisto_drogue_parachute_trigger, diff --git a/tests/unit/rocket/test_parachute.py b/tests/unit/rocket/test_parachute.py index 310c85aac..2a7d8a02c 100644 --- a/tests/unit/rocket/test_parachute.py +++ b/tests/unit/rocket/test_parachute.py @@ -111,26 +111,70 @@ def test_from_dict_defaults_drag_coefficient_to_1_4_when_absent(self): assert parachute.drag_coefficient == pytest.approx(1.4) -class TestParachuteAbstractBase: - """After the PR #958 refactor, ``Parachute`` is an abstract base class and - users must instantiate a concrete model such as ``HemisphericalParachute``.""" - - def test_parachute_abstract_base_cannot_be_instantiated(self): - """Instantiating the abstract ``Parachute`` base directly must raise a - ``TypeError`` (unimplemented abstract methods).""" - with pytest.raises(TypeError, match="abstract"): - # pylint: disable=abstract-class-instantiated,unexpected-keyword-arg - # pylint: disable=no-value-for-parameter - Parachute( +class TestParachuteClassHierarchy: + """``Parachute`` is a concrete generic class, keeping backward + compatibility with pre-v1.13.0 user code, and ``HemisphericalParachute`` + is a geometry-specific subclass that does not expose the ``noise`` + parameter.""" + + def test_generic_parachute_is_concrete_and_instantiable(self): + """The generic ``Parachute`` must be directly instantiable with its + historical signature and behave like the hemispherical model.""" + parachute = Parachute( + name="test", + cd_s=10.0, + trigger="apogee", + sampling_rate=100, + ) + assert parachute.parachute_type == "generic" + assert parachute.radius == pytest.approx(np.sqrt(10.0 / (1.4 * np.pi))) + + def test_generic_parachute_accepts_noise(self): + """The generic ``Parachute`` must keep the historical ``noise`` + parameter for backward compatibility.""" + parachute = Parachute( + name="test", + cd_s=10.0, + trigger="apogee", + sampling_rate=100, + noise=(0, 8.3, 0.5), + ) + assert parachute.noise_bias == 0 + assert parachute.noise_deviation == pytest.approx(8.3) + + def test_hemispherical_parachute_rejects_noise(self): + """``HemisphericalParachute`` must not expose the ``noise`` parameter; + sensors should be used to simulate noisy trigger readings instead.""" + with pytest.raises(TypeError): + # pylint: disable=unexpected-keyword-arg + HemisphericalParachute( name="test", cd_s=10.0, trigger="apogee", sampling_rate=100, + noise=(0, 8.3, 0.5), ) def test_hemispherical_parachute_is_a_parachute_subclass(self): - """The concrete ``HemisphericalParachute`` must derive from the abstract - ``Parachute`` base.""" + """``HemisphericalParachute`` must derive from the generic + ``Parachute`` class.""" assert issubclass(HemisphericalParachute, Parachute) parachute = _make_parachute() assert isinstance(parachute, Parachute) + assert parachute.parachute_type == "hemispherical" + + def test_generic_parachute_from_dict_round_trip(self): + """A generic ``Parachute`` serialized with to_dict must be restored + by ``Parachute.from_dict``, including the ``noise`` field.""" + original = Parachute( + name="test", + cd_s=5.0, + trigger="apogee", + sampling_rate=100, + noise=(0, 8.3, 0.5), + ) + restored = Parachute.from_dict(original.to_dict()) + assert restored.parachute_type == "generic" + assert restored.cd_s == pytest.approx(original.cd_s) + assert restored.noise == original.noise + assert restored.radius == pytest.approx(original.radius) diff --git a/tests/unit/stochastic/test_stochastic_parachute.py b/tests/unit/stochastic/test_stochastic_parachute.py index 01b5efb91..686480a43 100644 --- a/tests/unit/stochastic/test_stochastic_parachute.py +++ b/tests/unit/stochastic/test_stochastic_parachute.py @@ -1,4 +1,4 @@ -from rocketpy.rocket.parachutes import HemisphericalParachute +from rocketpy.rocket.parachutes import Parachute def test_stochastic_parachute_create_object(stochastic_main_parachute): @@ -18,4 +18,4 @@ class creates a StochasticParachute object from the randomly generated None """ obj = stochastic_main_parachute.create_object() - assert isinstance(obj, HemisphericalParachute) + assert isinstance(obj, Parachute) diff --git a/tests/unit/test_encoders.py b/tests/unit/test_encoders.py index 7f5c2f005..0760e7a75 100644 --- a/tests/unit/test_encoders.py +++ b/tests/unit/test_encoders.py @@ -12,13 +12,13 @@ def test_get_class_from_signature_remaps_legacy_parachute(): """The legacy ``rocketpy.rocket.parachute.Parachute`` signature must remap to - the concrete ``HemisphericalParachute`` (the old module was moved and the - class became abstract).""" + ``Parachute`` in its new location (the old module was moved to the + ``rocketpy.rocket.parachutes`` subpackage).""" legacy_signature = { "module": "rocketpy.rocket.parachute", "name": "Parachute", } - assert get_class_from_signature(legacy_signature) is HemisphericalParachute + assert get_class_from_signature(legacy_signature) is Parachute def test_decode_legacy_parachute_rpy_reconstructs_object(): @@ -43,10 +43,10 @@ def test_decode_legacy_parachute_rpy_reconstructs_object(): decoded = json.loads(json.dumps(legacy_entry), cls=RocketPyDecoder) assert isinstance(decoded, Parachute) - assert isinstance(decoded, HemisphericalParachute) assert decoded.name == "drogue" assert decoded.cd_s == 1.0 assert decoded.lag == 1.5 + assert decoded.noise_deviation == 8.3 assert callable(decoded.triggerfunc) From 4e8e77a9929c0e2e23039f253a424ed77f07d9bd Mon Sep 17 00:00:00 2001 From: Gui-FernandesBR Date: Thu, 9 Jul 2026 23:58:00 -0300 Subject: [PATCH 3/5] DOC: teach the generic Parachute in guides and notebooks User guides and example notebooks return to the generic Parachute class (several of them pass the noise parameter, which the hemispherical subclass no longer exposes). The v1.13.0 changelog no longer lists the abstract-Parachute breaking change. Co-Authored-By: Claude Fable 5 --- CHANGELOG.md | 4 +-- docs/examples/andromeda_flight_sim.ipynb | 6 ++--- docs/examples/bella_lui_flight_sim.ipynb | 4 +-- docs/examples/camoes_flight_sim.ipynb | 4 +-- docs/examples/defiance_flight_sim.ipynb | 6 ++--- docs/examples/genesis_flight_sim.ipynb | 6 ++--- docs/examples/halcyon_flight_sim.ipynb | 4 +-- docs/examples/juno3_flight_sim.ipynb | 4 +-- docs/examples/lince_flight_sim.ipynb | 8 +++--- docs/examples/ndrt_2020_flight_sim.ipynb | 6 ++--- .../examples/prometheus_2022_flight_sim.ipynb | 6 ++--- docs/examples/valetudo_flight_sim.ipynb | 4 +-- docs/user/compare_flights.rst | 6 ++--- docs/user/deployable.rst | 10 ++++---- docs/user/first_simulation.rst | 6 ++--- docs/user/rocket/rocket_usage.rst | 25 +++++++++++-------- 16 files changed, 56 insertions(+), 53 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a5108f942..6a508d13b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -61,7 +61,7 @@ Attention: The newest changes should be on top --> - ENH: Adaptive Monte Carlo via Convergence Criteria [#922](https://github.com/RocketPy-Team/RocketPy/pull/922) - ENH: Auto-Detection of Pressure Conversion Factor [#966](https://github.com/RocketPy-Team/RocketPy/pull/966) - ENH: Introduce pressure unit conversion when using forecast/reanalysis/ensemble data [#955](https://github.com/RocketPy-Team/RocketPy/pull/955) -- MNT: Refactor parachute implementation with abstract base and hemispherical model split [#958](https://github.com/RocketPy-Team/RocketPy/pull/958) +- MNT: Move parachute classes to the `rocketpy.rocket.parachutes` subpackage and add the `HemisphericalParachute` model [#958](https://github.com/RocketPy-Team/RocketPy/pull/958) - DOC: Add aerodynamic surfaces user guide [#1043](https://github.com/RocketPy-Team/RocketPy/pull/1043) - DOC: Add Valkyrie flight example (Bisky Team) [#967](https://github.com/RocketPy-Team/RocketPy/pull/967) - DOC: Configure AI instructions and update developer docs [#975](https://github.com/RocketPy-Team/RocketPy/pull/975) @@ -72,7 +72,7 @@ Attention: The newest changes should be on top --> ### Changed - REL: bumps up rocketpy version to 1.13.0 [#1048](https://github.com/RocketPy-Team/RocketPy/pull/1048) -- MNT: **Breaking**: `Parachute` is now an abstract base class and can no longer be instantiated directly; use `HemisphericalParachute` (or `Rocket.add_parachute`, which builds it for you). Loading old serialized files still works [#958](https://github.com/RocketPy-Team/RocketPy/pull/958) +- MNT: `Parachute` remains a concrete generic class, fully backward compatible with previous versions; the new `HemisphericalParachute` subclass implements the same model but does not expose the `noise` parameter (attach a noisy `Barometer` sensor instead) [#958](https://github.com/RocketPy-Team/RocketPy/pull/958) - MNT: Discrete controllers are now called exactly once per time node (previously twice); results may change for stateful controllers and `observed_variables` no longer contains duplicated entries [#949](https://github.com/RocketPy-Team/RocketPy/pull/949) - MNT: Multi-dimensional linear `Function` objects now apply their extrapolation rule to points outside the data's convex hull (previously returned NaN) [#969](https://github.com/RocketPy-Team/RocketPy/pull/969) - MNT: Informational messages previously shown with `print()` now use Python logging and are silent by default; call `rocketpy.utils.enable_logging()` to see them [#973](https://github.com/RocketPy-Team/RocketPy/pull/973) diff --git a/docs/examples/andromeda_flight_sim.ipynb b/docs/examples/andromeda_flight_sim.ipynb index 7ea8a9d05..1ff65f1bc 100644 --- a/docs/examples/andromeda_flight_sim.ipynb +++ b/docs/examples/andromeda_flight_sim.ipynb @@ -41,7 +41,7 @@ " Environment,\n", " Flight,\n", " Function,\n", - " HemisphericalParachute,\n", + " Parachute,\n", " Rocket,\n", " SolidMotor,\n", ")\n", @@ -283,11 +283,11 @@ " position=0.3546,\n", ")\n", "\n", - "Drogue = HemisphericalParachute(\n", + "Drogue = Parachute(\n", " \"Drogue\", cd_s=0.84665922014, trigger=\"apogee\", sampling_rate=100, lag=0\n", ")\n", "\n", - "Main = HemisphericalParachute(\n", + "Main = Parachute(\n", " \"Main\", cd_s=8.362919643856031, trigger=500, sampling_rate=100, lag=0\n", ")\n", "Andromeda.add_parachute(parachute=Drogue)\n", diff --git a/docs/examples/bella_lui_flight_sim.ipynb b/docs/examples/bella_lui_flight_sim.ipynb index 11fe86553..15ca3357b 100644 --- a/docs/examples/bella_lui_flight_sim.ipynb +++ b/docs/examples/bella_lui_flight_sim.ipynb @@ -44,7 +44,7 @@ " Environment,\n", " Flight,\n", " Function,\n", - " HemisphericalParachute,\n", + " Parachute,\n", " Rocket,\n", " SolidMotor,\n", ")" @@ -433,7 +433,7 @@ "metadata": {}, "outputs": [], "source": [ - "Drogue = HemisphericalParachute(\n", + "Drogue = Parachute(\n", " \"Drogue\",\n", " cd_s=parameters.get(\"CdS_drogue\")[0],\n", " trigger=\"apogee\",\n", diff --git a/docs/examples/camoes_flight_sim.ipynb b/docs/examples/camoes_flight_sim.ipynb index 44fac3613..12c66806b 100644 --- a/docs/examples/camoes_flight_sim.ipynb +++ b/docs/examples/camoes_flight_sim.ipynb @@ -52,7 +52,7 @@ " Environment,\n", " Flight,\n", " Function,\n", - " HemisphericalParachute,\n", + " Parachute,\n", " Rocket,\n", " SolidMotor,\n", ")\n", @@ -308,7 +308,7 @@ " return True if y[5] < 5 and y[2] > 300 else False\n", "\n", "\n", - "Drogue = HemisphericalParachute(\n", + "Drogue = Parachute(\n", " \"Drogue\", cd_s=0.33, sampling_rate=400, lag=1.5, trigger=drogue_trigger\n", ")\n", "CAMOES.add_parachute(parachute=Drogue)" diff --git a/docs/examples/defiance_flight_sim.ipynb b/docs/examples/defiance_flight_sim.ipynb index c8bbfa432..90cdfd86e 100644 --- a/docs/examples/defiance_flight_sim.ipynb +++ b/docs/examples/defiance_flight_sim.ipynb @@ -27,7 +27,7 @@ "source": [ "import datetime\n", "\n", - "from rocketpy import Environment, Flight, Function, HemisphericalParachute, Rocket\n", + "from rocketpy import Environment, Flight, Function, Parachute, Rocket\n", "from rocketpy.motors import CylindricalTank, Fluid, HybridMotor\n", "from rocketpy.motors.tank import MassFlowRateBasedTank" ] @@ -221,11 +221,11 @@ "\n", "defiance.add_tail(top_radius=0.07, bottom_radius=0.064, length=0.0597, position=0.1)\n", "\n", - "Main = HemisphericalParachute(\n", + "Main = Parachute(\n", " name=\"main\", cd_s=2.2, trigger=305, sampling_rate=100, lag=0\n", ")\n", "\n", - "Drogue = HemisphericalParachute(\n", + "Drogue = Parachute(\n", " name=\"drogue\", cd_s=1.55, trigger=\"apogee\", sampling_rate=100, lag=0\n", ")\n", "defiance.add_parachute(parachute=Main)\n", diff --git a/docs/examples/genesis_flight_sim.ipynb b/docs/examples/genesis_flight_sim.ipynb index 295387195..fe52bb009 100644 --- a/docs/examples/genesis_flight_sim.ipynb +++ b/docs/examples/genesis_flight_sim.ipynb @@ -37,7 +37,7 @@ "source": [ "import matplotlib.pyplot as plt\n", "\n", - "from rocketpy import Environment, Flight, Function, HemisphericalParachute, Rocket\n", + "from rocketpy import Environment, Flight, Function, Parachute, Rocket\n", "from rocketpy.motors import SolidMotor\n", "\n", "plt.style.use(\"seaborn-v0_8-colorblind\")" @@ -271,7 +271,7 @@ "metadata": {}, "outputs": [], "source": [ - "Drogue = HemisphericalParachute(\n", + "Drogue = Parachute(\n", " name=\"Drogue\",\n", " cd_s=0.285005285533666,\n", " trigger=\"apogee\",\n", @@ -280,7 +280,7 @@ " noise=(0, 8.3, 0.5),\n", ")\n", "\n", - "Main = HemisphericalParachute(\n", + "Main = Parachute(\n", " name=\"Main\",\n", " cd_s=1.1,\n", " trigger=870,\n", diff --git a/docs/examples/halcyon_flight_sim.ipynb b/docs/examples/halcyon_flight_sim.ipynb index ad10aa412..2650a269a 100644 --- a/docs/examples/halcyon_flight_sim.ipynb +++ b/docs/examples/halcyon_flight_sim.ipynb @@ -39,7 +39,7 @@ "\n", "import matplotlib.pyplot as plt\n", "\n", - "from rocketpy import Environment, Flight, Function, HemisphericalParachute, Rocket\n", + "from rocketpy import Environment, Flight, Function, Parachute, Rocket\n", "from rocketpy.motors import CylindricalTank, Fluid, HybridMotor\n", "from rocketpy.motors.tank import MassFlowRateBasedTank\n", "\n", @@ -420,7 +420,7 @@ "metadata": {}, "outputs": [], "source": [ - "Main = HemisphericalParachute(\n", + "Main = Parachute(\n", " name=\"Main\",\n", " cd_s=9.621,\n", " trigger=\"apogee\",\n", diff --git a/docs/examples/juno3_flight_sim.ipynb b/docs/examples/juno3_flight_sim.ipynb index 788b7348f..f5e12e21e 100644 --- a/docs/examples/juno3_flight_sim.ipynb +++ b/docs/examples/juno3_flight_sim.ipynb @@ -43,7 +43,7 @@ " Environment,\n", " Flight,\n", " Function,\n", - " HemisphericalParachute,\n", + " Parachute,\n", " Rocket,\n", " SolidMotor,\n", ")\n", @@ -344,7 +344,7 @@ "metadata": {}, "outputs": [], "source": [ - "drogue = HemisphericalParachute(\n", + "drogue = Parachute(\n", " \"Drogue\",\n", " cd_s=0.885,\n", " trigger=\"apogee\",\n", diff --git a/docs/examples/lince_flight_sim.ipynb b/docs/examples/lince_flight_sim.ipynb index e2db12ba1..f67ad1e63 100644 --- a/docs/examples/lince_flight_sim.ipynb +++ b/docs/examples/lince_flight_sim.ipynb @@ -41,7 +41,7 @@ " Environment,\n", " Flight,\n", " Function,\n", - " HemisphericalParachute,\n", + " Parachute,\n", " Rocket,\n", " SolidMotor,\n", ")\n", @@ -269,7 +269,7 @@ "metadata": {}, "outputs": [], "source": [ - "Main = HemisphericalParachute(\n", + "Main = Parachute(\n", " \"Main\", cd_s=3.9, trigger=\"apogee\", sampling_rate=150, lag=0, noise=(0, 0, 0)\n", ")\n", "LincePreDeploy.add_parachute(parachute=Main)" @@ -333,7 +333,7 @@ "metadata": {}, "outputs": [], "source": [ - "Main_stage_1 = HemisphericalParachute(\n", + "Main_stage_1 = Parachute(\n", " \"Main\", cd_s=3.9, trigger=\"apogee\", sampling_rate=150, lag=0, noise=(0, 0, 0)\n", ")\n", "LincePostDeploy.add_parachute(parachute=Main_stage_1)" @@ -402,7 +402,7 @@ "metadata": {}, "outputs": [], "source": [ - "Main_stage_2 = HemisphericalParachute(\n", + "Main_stage_2 = Parachute(\n", " \"Main\", cd_s=0.159248, trigger=\"apogee\", sampling_rate=150, lag=0, noise=(0, 0, 0)\n", ")\n", "Payload.add_parachute(parachute=Main_stage_2)" diff --git a/docs/examples/ndrt_2020_flight_sim.ipynb b/docs/examples/ndrt_2020_flight_sim.ipynb index 84faa0831..2ce5d4e81 100644 --- a/docs/examples/ndrt_2020_flight_sim.ipynb +++ b/docs/examples/ndrt_2020_flight_sim.ipynb @@ -42,7 +42,7 @@ " Environment,\n", " Flight,\n", " Function,\n", - " HemisphericalParachute,\n", + " Parachute,\n", " Rocket,\n", " SolidMotor,\n", ")" @@ -378,7 +378,7 @@ "metadata": {}, "outputs": [], "source": [ - "drogue = HemisphericalParachute(\n", + "drogue = Parachute(\n", " \"Drogue\",\n", " cd_s=parameters.get(\"cd_s_drogue\")[0],\n", " trigger=\"apogee\",\n", @@ -386,7 +386,7 @@ " lag=parameters.get(\"lag_rec\")[0],\n", " noise=(0, 8.3, 0.5),\n", ")\n", - "main = HemisphericalParachute(\n", + "main = Parachute(\n", " \"Main\",\n", " cd_s=parameters.get(\"cd_s_main\")[0],\n", " trigger=167.64,\n", diff --git a/docs/examples/prometheus_2022_flight_sim.ipynb b/docs/examples/prometheus_2022_flight_sim.ipynb index 22c53eba1..36d5dc1c8 100644 --- a/docs/examples/prometheus_2022_flight_sim.ipynb +++ b/docs/examples/prometheus_2022_flight_sim.ipynb @@ -48,7 +48,7 @@ " Flight,\n", " Function,\n", " GenericMotor,\n", - " HemisphericalParachute,\n", + " Parachute,\n", " Rocket,\n", ")\n", "from rocketpy.simulation.flight_data_importer import FlightDataImporter\n", @@ -282,12 +282,12 @@ " position=0.273,\n", " sweep_length=0.066,\n", ")\n", - "drogue = HemisphericalParachute(\n", + "drogue = Parachute(\n", " \"Drogue\",\n", " cd_s=1.6 * np.pi * 0.3048**2, # Cd = 1.6, D_chute = 24 in\n", " trigger=\"apogee\",\n", ")\n", - "main = HemisphericalParachute(\n", + "main = Parachute(\n", " \"Main\",\n", " cd_s=2.2 * np.pi * 0.9144**2, # Cd = 2.2, D_chute = 72 in\n", " trigger=457.2, # 1500 ft\n", diff --git a/docs/examples/valetudo_flight_sim.ipynb b/docs/examples/valetudo_flight_sim.ipynb index 53f53a89e..2a1397714 100644 --- a/docs/examples/valetudo_flight_sim.ipynb +++ b/docs/examples/valetudo_flight_sim.ipynb @@ -37,7 +37,7 @@ "# Importing libraries\n", "import matplotlib.pyplot as plt\n", "\n", - "from rocketpy import Environment, Flight, HemisphericalParachute, Rocket, SolidMotor" + "from rocketpy import Environment, Flight, Parachute, Rocket, SolidMotor" ] }, { @@ -387,7 +387,7 @@ "metadata": {}, "outputs": [], "source": [ - "drogue = HemisphericalParachute(\n", + "drogue = Parachute(\n", " \"Drogue\",\n", " cd_s=parameters.get(\"cd_s_drogue\")[0],\n", " trigger=\"apogee\",\n", diff --git a/docs/user/compare_flights.rst b/docs/user/compare_flights.rst index c352e8b19..6238637b8 100644 --- a/docs/user/compare_flights.rst +++ b/docs/user/compare_flights.rst @@ -14,7 +14,7 @@ We will start by importing the necessary classes and modules: .. jupyter-execute:: from rocketpy.plots.compare import CompareFlights - from rocketpy import Environment, Flight, Rocket, SolidMotor, HemisphericalParachute + from rocketpy import Environment, Flight, Rocket, SolidMotor, Parachute from datetime import datetime, timedelta @@ -83,7 +83,7 @@ This is done following the same steps as in the :ref:`firstsimulation` example. top_radius=0.0635, bottom_radius=0.0435, length=0.060, position=-1.194656 ) - main_chute = HemisphericalParachute( + main_chute = Parachute( "Main", cd_s=10.0, trigger=800, @@ -92,7 +92,7 @@ This is done following the same steps as in the :ref:`firstsimulation` example. noise=(0, 8.3, 0.5), ) - drogue_chute = HemisphericalParachute( + drogue_chute = Parachute( "Drogue", cd_s=1.0, trigger="apogee", diff --git a/docs/user/deployable.rst b/docs/user/deployable.rst index ddc082a6f..6b1fb69bc 100644 --- a/docs/user/deployable.rst +++ b/docs/user/deployable.rst @@ -8,7 +8,7 @@ Let's start by importing the rocketpy classes we will use. .. jupyter-execute:: - from rocketpy import Environment, SolidMotor, Rocket, Flight, HemisphericalParachute + from rocketpy import Environment, SolidMotor, Rocket, Flight, Parachute Creating Environment @@ -182,7 +182,7 @@ Therefore we should be careful with the value of its mass. # Define Parachutes for the rocket - main_chute = HemisphericalParachute( + main_chute = Parachute( "Main", cd_s=7.2, trigger=800, @@ -191,7 +191,7 @@ Therefore we should be careful with the value of its mass. noise=(0, 8.3, 0.5), ) - drogue_chute = HemisphericalParachute( + drogue_chute = Parachute( "Drogue", cd_s=0.72, trigger="apogee", @@ -244,7 +244,7 @@ surfaces to stabilize it, nor a motor that ignites. It does, however, have parac center_of_mass_without_motor=0, ) - payload_drogue = HemisphericalParachute( + payload_drogue = Parachute( "Drogue", cd_s=0.35, trigger="apogee", @@ -253,7 +253,7 @@ surfaces to stabilize it, nor a motor that ignites. It does, however, have parac noise=(0, 8.3, 0.5), ) - payload_main = HemisphericalParachute( + payload_main = Parachute( "Main", cd_s=4.0, trigger=800, diff --git a/docs/user/first_simulation.rst b/docs/user/first_simulation.rst index effa5ce0d..87cf7022e 100644 --- a/docs/user/first_simulation.rst +++ b/docs/user/first_simulation.rst @@ -67,7 +67,7 @@ we will use from RocketPy: .. jupyter-execute:: - from rocketpy import Environment, SolidMotor, Rocket, Flight, HemisphericalParachute + from rocketpy import Environment, SolidMotor, Rocket, Flight, Parachute .. note:: @@ -272,7 +272,7 @@ Finally, we can add any number of Parachutes to the ``Rocket`` object. .. jupyter-execute:: - main = HemisphericalParachute( + main = Parachute( name="main", cd_s=10.0, trigger=800, # ejection altitude in meters @@ -284,7 +284,7 @@ Finally, we can add any number of Parachutes to the ``Rocket`` object. porosity=0.0432, ) - drogue = HemisphericalParachute( + drogue = Parachute( name="drogue", cd_s=1.0, trigger="apogee", # ejection at apogee diff --git a/docs/user/rocket/rocket_usage.rst b/docs/user/rocket/rocket_usage.rst index 5f8667694..638fd2cc2 100644 --- a/docs/user/rocket/rocket_usage.rst +++ b/docs/user/rocket/rocket_usage.rst @@ -292,23 +292,26 @@ Optionally, we can also define: .. note:: - Since v1.13.0, :class:`~rocketpy.Parachute` is an **abstract base class** - and can no longer be instantiated directly. Instead, instantiate a concrete - parachute model such as :class:`~rocketpy.HemisphericalParachute` (used - below), which derives its geometry-dependent quantities (e.g. the added - mass during descent) from the parachute ``radius`` and ``height``. As a - convenience shortcut, ``Rocket.add_parachute(...)`` can still be called with - keyword arguments (``name``, ``cd_s``, ``trigger``, ...) and will build a - hemispherical parachute for you. + Since v1.13.0, the parachute classes live in the + ``rocketpy.rocket.parachutes`` subpackage. The generic + :class:`~rocketpy.Parachute` class (used below) remains the default + choice and behaves exactly as in previous versions, deriving the added + mass during descent from the parachute ``radius`` and ``height``. A + geometry-specific :class:`~rocketpy.HemisphericalParachute` subclass is + also available; it implements the same model but does not expose the + ``noise`` parameter (attach a noisy sensor to the rocket instead). As a + convenience shortcut, ``Rocket.add_parachute(...)`` can still be called + with keyword arguments (``name``, ``cd_s``, ``trigger``, ...) and will + build a generic parachute for you. Lets add two parachutes to the rocket, one that will be deployed at apogee and another that will be deployed at 800 meters above ground level: .. jupyter-execute:: - from rocketpy import HemisphericalParachute + from rocketpy import Parachute - main = HemisphericalParachute( + main = Parachute( name="Main", cd_s=10.0, trigger=800, @@ -320,7 +323,7 @@ apogee and another that will be deployed at 800 meters above ground level: porosity=0.0432, ) - drogue = HemisphericalParachute( + drogue = Parachute( name="Drogue", cd_s=1.0, trigger="apogee", From 734cb4924fede109dfdd0ccdcaf3e6a2660b9ecf Mon Sep 17 00:00:00 2001 From: Gui-FernandesBR Date: Thu, 9 Jul 2026 23:59:02 -0300 Subject: [PATCH 4/5] MNT: apply ruff format to example notebooks Co-Authored-By: Claude Fable 5 --- docs/examples/andromeda_flight_sim.ipynb | 4 +--- docs/examples/defiance_flight_sim.ipynb | 8 ++------ 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/docs/examples/andromeda_flight_sim.ipynb b/docs/examples/andromeda_flight_sim.ipynb index 1ff65f1bc..4f523539b 100644 --- a/docs/examples/andromeda_flight_sim.ipynb +++ b/docs/examples/andromeda_flight_sim.ipynb @@ -287,9 +287,7 @@ " \"Drogue\", cd_s=0.84665922014, trigger=\"apogee\", sampling_rate=100, lag=0\n", ")\n", "\n", - "Main = Parachute(\n", - " \"Main\", cd_s=8.362919643856031, trigger=500, sampling_rate=100, lag=0\n", - ")\n", + "Main = Parachute(\"Main\", cd_s=8.362919643856031, trigger=500, sampling_rate=100, lag=0)\n", "Andromeda.add_parachute(parachute=Drogue)\n", "Andromeda.add_parachute(parachute=Main)" ] diff --git a/docs/examples/defiance_flight_sim.ipynb b/docs/examples/defiance_flight_sim.ipynb index 90cdfd86e..9b996cf62 100644 --- a/docs/examples/defiance_flight_sim.ipynb +++ b/docs/examples/defiance_flight_sim.ipynb @@ -221,13 +221,9 @@ "\n", "defiance.add_tail(top_radius=0.07, bottom_radius=0.064, length=0.0597, position=0.1)\n", "\n", - "Main = Parachute(\n", - " name=\"main\", cd_s=2.2, trigger=305, sampling_rate=100, lag=0\n", - ")\n", + "Main = Parachute(name=\"main\", cd_s=2.2, trigger=305, sampling_rate=100, lag=0)\n", "\n", - "Drogue = Parachute(\n", - " name=\"drogue\", cd_s=1.55, trigger=\"apogee\", sampling_rate=100, lag=0\n", - ")\n", + "Drogue = Parachute(name=\"drogue\", cd_s=1.55, trigger=\"apogee\", sampling_rate=100, lag=0)\n", "defiance.add_parachute(parachute=Main)\n", "defiance.add_parachute(parachute=Drogue)" ] From 3e8499802dc8b08f7a13e592b5d0b882f287f2ed Mon Sep 17 00:00:00 2001 From: Gui-FernandesBR Date: Fri, 10 Jul 2026 00:00:27 -0300 Subject: [PATCH 5/5] DOC: reference PR #1061 in the v1.13.0 changelog entry Co-Authored-By: Claude Fable 5 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a508d13b..e2fee61ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -72,7 +72,7 @@ Attention: The newest changes should be on top --> ### Changed - REL: bumps up rocketpy version to 1.13.0 [#1048](https://github.com/RocketPy-Team/RocketPy/pull/1048) -- MNT: `Parachute` remains a concrete generic class, fully backward compatible with previous versions; the new `HemisphericalParachute` subclass implements the same model but does not expose the `noise` parameter (attach a noisy `Barometer` sensor instead) [#958](https://github.com/RocketPy-Team/RocketPy/pull/958) +- MNT: `Parachute` remains a concrete generic class, fully backward compatible with previous versions; the new `HemisphericalParachute` subclass implements the same model but does not expose the `noise` parameter (attach a noisy `Barometer` sensor instead) [#958](https://github.com/RocketPy-Team/RocketPy/pull/958) [#1061](https://github.com/RocketPy-Team/RocketPy/pull/1061) - MNT: Discrete controllers are now called exactly once per time node (previously twice); results may change for stateful controllers and `observed_variables` no longer contains duplicated entries [#949](https://github.com/RocketPy-Team/RocketPy/pull/949) - MNT: Multi-dimensional linear `Function` objects now apply their extrapolation rule to points outside the data's convex hull (previously returned NaN) [#969](https://github.com/RocketPy-Team/RocketPy/pull/969) - MNT: Informational messages previously shown with `print()` now use Python logging and are silent by default; call `rocketpy.utils.enable_logging()` to see them [#973](https://github.com/RocketPy-Team/RocketPy/pull/973)