diff --git a/CHANGELOG.md b/CHANGELOG.md index 11912d29a..9a0d664c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,8 @@ Attention: The newest changes should be on top --> - ENH: Parallel mode for monte-carlo simulations 2 [#768](https://github.com/RocketPy-Team/RocketPy/pull/768) - DOC: ASTRA Flight Example [#770](https://github.com/RocketPy-Team/RocketPy/pull/770) +- ENH: Add Eccentricity to Stochastic Simulations [#792](https://github.com/RocketPy-Team/RocketPy/pull/792) +- ENH: Introduce the StochasticAirBrakes class [#785](https://github.com/RocketPy-Team/RocketPy/pull/785) ### Changed @@ -46,6 +48,7 @@ Attention: The newest changes should be on top --> - BUG: update flight simulation logic to include burn start time [#778](https://github.com/RocketPy-Team/RocketPy/pull/778) - BUG: fixes get_instance_attributes for Flight objects containing a Rocket object without rail buttons [#786](https://github.com/RocketPy-Team/RocketPy/pull/786) - BUG: fixed AGL altitude print for parachutes with lag [#788](https://github.com/RocketPy-Team/RocketPy/pull/788) +- BUG: fix the wind velocity factors usage and better visualization of uniform distributions in Stochastic Classes [#783](https://github.com/RocketPy-Team/RocketPy/pull/783) ## [v1.8.0] - 2025-01-20 diff --git a/rocketpy/__init__.py b/rocketpy/__init__.py index 539b8b2cb..e0436bf02 100644 --- a/rocketpy/__init__.py +++ b/rocketpy/__init__.py @@ -44,6 +44,7 @@ from .sensors import Accelerometer, Barometer, GnssReceiver, Gyroscope from .simulation import Flight, MonteCarlo from .stochastic import ( + StochasticAirBrakes, StochasticEllipticalFins, StochasticEnvironment, StochasticFlight, diff --git a/rocketpy/mathutils/function.py b/rocketpy/mathutils/function.py index 3e66d4f73..a20aa9a03 100644 --- a/rocketpy/mathutils/function.py +++ b/rocketpy/mathutils/function.py @@ -2219,10 +2219,10 @@ def __rsub__(self, other): """ return other + (-self) - def __mul__(self, other): + def __mul__(self, other): # pylint: disable=too-many-statements """Multiplies a Function object and returns a new Function object which gives the result of the multiplication. Only implemented for 1D - domains. + and 2D domains. Parameters ---------- @@ -2238,7 +2238,7 @@ def __mul__(self, other): Returns ------- result : Function - A Function object which gives the result of self(x)*other(x). + A Function object which gives the result of self(x)*other(x) or self(x,y)*other(x,y). """ self_source_is_array = isinstance(self.source, np.ndarray) other_source_is_array = ( @@ -2250,37 +2250,76 @@ def __mul__(self, other): interp = self.__interpolation__ extrap = self.__extrapolation__ - if ( - self_source_is_array - and other_source_is_array - and np.array_equal(self.x_array, other.x_array) - ): - source = np.column_stack((self.x_array, self.y_array * other.y_array)) - outputs = f"({self.__outputs__[0]}*{other.__outputs__[0]})" - return Function(source, inputs, outputs, interp, extrap) - elif isinstance(other, NUMERICAL_TYPES) or self.__is_single_element_array( - other - ): - if not self_source_is_array: - return Function(lambda x: (self.get_value_opt(x) * other), inputs) - source = np.column_stack((self.x_array, np.multiply(self.y_array, other))) - outputs = f"({self.__outputs__[0]}*{other})" - return Function( - source, - inputs, - outputs, - interp, - extrap, - ) - elif callable(other): - return Function(lambda x: (self.get_value_opt(x) * other(x)), inputs) - else: - raise TypeError("Unsupported type for multiplication") + if self.__dom_dim__ == 1: + if ( + self_source_is_array + and other_source_is_array + and np.array_equal(self.x_array, other.x_array) + ): + source = np.column_stack((self.x_array, self.y_array * other.y_array)) + outputs = f"({self.__outputs__[0]}*{other.__outputs__[0]})" + return Function(source, inputs, outputs, interp, extrap) + elif isinstance(other, NUMERICAL_TYPES) or self.__is_single_element_array( + other + ): + if not self_source_is_array: + return Function(lambda x: (self.get_value_opt(x) * other), inputs) + source = np.column_stack( + (self.x_array, np.multiply(self.y_array, other)) + ) + outputs = f"({self.__outputs__[0]}*{other})" + return Function( + source, + inputs, + outputs, + interp, + extrap, + ) + elif callable(other): + return Function(lambda x: (self.get_value_opt(x) * other(x)), inputs) + else: + raise TypeError("Unsupported type for multiplication") + elif self.__dom_dim__ == 2: + if ( + self_source_is_array + and other_source_is_array + and np.array_equal(self.x_array, other.x_array) + and np.array_equal(self.y_array, other.y_array) + ): + source = np.column_stack( + (self.x_array, self.y_array, self.z_array * other.z_array) + ) + outputs = f"({self.__outputs__[0]}*{other.__outputs__[0]})" + return Function(source, inputs, outputs, interp, extrap) + elif isinstance(other, NUMERICAL_TYPES) or self.__is_single_element_array( + other + ): + if not self_source_is_array: + return Function( + lambda x, y: (self.get_value_opt(x, y) * other), inputs + ) + source = np.column_stack( + (self.x_array, self.y_array, np.multiply(self.z_array, other)) + ) + outputs = f"({self.__outputs__[0]}*{other})" + return Function( + source, + inputs, + outputs, + interp, + extrap, + ) + elif callable(other): + return Function( + lambda x, y: (self.get_value_opt(x, y) * other(x)), inputs + ) + else: + raise TypeError("Unsupported type for multiplication") def __rmul__(self, other): """Multiplies 'other' by a Function object and returns a new Function object which gives the result of the multiplication. Only implemented for - 1D domains. + 1D and 2D domains. Parameters ---------- @@ -2290,7 +2329,7 @@ def __rmul__(self, other): Returns ------- result : Function - A Function object which gives the result of other(x)*self(x). + A Function object which gives the result of other(x,y)*self(x,y). """ return self * other diff --git a/rocketpy/simulation/monte_carlo.py b/rocketpy/simulation/monte_carlo.py index 3914f1e54..ff86db7b3 100644 --- a/rocketpy/simulation/monte_carlo.py +++ b/rocketpy/simulation/monte_carlo.py @@ -458,6 +458,7 @@ def __run_single_simulation(self): heading=self.flight._randomize_heading(), initial_solution=self.flight.initial_solution, terminate_on_apogee=self.flight.terminate_on_apogee, + time_overshoot=self.flight.time_overshoot, ) def __evaluate_flight_inputs(self, sim_idx): diff --git a/rocketpy/stochastic/__init__.py b/rocketpy/stochastic/__init__.py index 692eca85f..b1e146246 100644 --- a/rocketpy/stochastic/__init__.py +++ b/rocketpy/stochastic/__init__.py @@ -6,6 +6,7 @@ """ from .stochastic_aero_surfaces import ( + StochasticAirBrakes, StochasticEllipticalFins, StochasticNoseCone, StochasticRailButtons, diff --git a/rocketpy/stochastic/stochastic_aero_surfaces.py b/rocketpy/stochastic/stochastic_aero_surfaces.py index 31c1e9efc..5dda716bb 100644 --- a/rocketpy/stochastic/stochastic_aero_surfaces.py +++ b/rocketpy/stochastic/stochastic_aero_surfaces.py @@ -9,6 +9,7 @@ RailButtons, Tail, TrapezoidalFins, + AirBrakes, ) from .stochastic_model import StochasticModel @@ -432,3 +433,114 @@ def create_object(self): """ generated_dict = next(self.dict_generator()) return RailButtons(**generated_dict) + + +class StochasticAirBrakes(StochasticModel): + """A Stochastic Air Brakes class that inherits from StochasticModel. + + See Also + -------- + :ref:`stochastic_model` and + :class:`AirBrakes ` + + Attributes + ---------- + object : AirBrakes + AirBrakes object to be used for validation. + drag_coefficient_curve : list, str + The drag coefficient curve of the air brakes can account for + either the air brakes' drag alone or the combined drag of both + the rocket and the air brakes. + drag_coefficient_curve_factor : tuple, list, int, float + The drag curve factor of the air brakes. This value scales the + drag coefficient curve to introduce stochastic variability. + reference_area : tuple, list, int, float + Reference area used to non-dimensionalize the drag coefficients. + clamp : bool + If True, the simulation will clamp the deployment level to 0 or 1 if + the deployment level is out of bounds. If False, the simulation will + not clamp the deployment level and will instead raise a warning if + the deployment level is out of bounds. + override_rocket_drag : bool + If False, the air brakes drag coefficient will be added to the + rocket's power off drag coefficient curve. If True, during the + simulation, the rocket's power off drag will be ignored and the air + brakes drag coefficient will be used for the entire rocket instead. + deployment_level : tuple, list, int, float + Initial deployment level, ranging from 0 to 1. + name : list[str] + List with the air brakes object name. This attribute can't be randomized. + """ + + def __init__( + self, + air_brakes, + drag_coefficient_curve=None, + drag_coefficient_curve_factor=(1, 0), + reference_area=None, + clamp=None, + override_rocket_drag=None, + deployment_level=(0, 0), + ): + """Initializes the Stochastic AirBrakes class. + + See Also + -------- + :ref:`stochastic_model` + + Parameters + ---------- + air_brakes : AirBrakes + AirBrakes object to be used for validation. + drag_coefficient_curve : list, str, optional + The drag coefficient curve of the air brakes can account for + either the air brakes' drag alone or the combined drag of both + the rocket and the air brakes. + drag_coefficient_curve_factor : tuple, list, int, float, optional + The drag curve factor of the air brakes. This value scales the + drag coefficient curve to introduce stochastic variability. + reference_area : tuple, list, int, float, optional + Reference area used to non-dimensionalize the drag coefficients. + clamp : bool, optional + If True, the simulation will clamp the deployment level to 0 or 1 if + the deployment level is out of bounds. If False, the simulation will + not clamp the deployment level and will instead raise a warning if + the deployment level is out of bounds. + override_rocket_drag : bool, optional + If False, the air brakes drag coefficient will be added to the + rocket's power off drag coefficient curve. If True, during the + simulation, the rocket's power off drag will be ignored and the air + brakes drag coefficient will be used for the entire rocket instead. + deployment_level : tuple, list, int, float, optional + Initial deployment level, ranging from 0 to 1. + """ + super().__init__( + air_brakes, + drag_coefficient_curve=drag_coefficient_curve, + drag_coefficient_curve_factor=drag_coefficient_curve_factor, + reference_area=reference_area, + clamp=clamp, + override_rocket_drag=override_rocket_drag, + deployment_level=deployment_level, + name=None, + ) + + def create_object(self): + """Creates and returns an AirBrakes object from the randomly generated + input arguments. + + Returns + ------- + air_brake : AirBrakes + AirBrakes object with the randomly generated input arguments. + """ + generated_dict = next(self.dict_generator()) + air_brakes = AirBrakes( + drag_coefficient_curve=generated_dict["drag_coefficient_curve"], + reference_area=generated_dict["reference_area"], + clamp=generated_dict["clamp"], + override_rocket_drag=generated_dict["override_rocket_drag"], + deployment_level=generated_dict["deployment_level"], + ) + air_brakes.drag_coefficient *= generated_dict["drag_coefficient_curve_factor"] + return air_brakes diff --git a/rocketpy/stochastic/stochastic_flight.py b/rocketpy/stochastic/stochastic_flight.py index 4ed778eb5..729313736 100644 --- a/rocketpy/stochastic/stochastic_flight.py +++ b/rocketpy/stochastic/stochastic_flight.py @@ -29,6 +29,9 @@ class StochasticFlight(StochasticModel): terminate_on_apogee : bool Whether or not the flight should terminate on apogee. This attribute can not be randomized. + time_overshoot : bool + If False, the simulation will run at the time step defined by the controller + sampling rate. Be aware that this will make the simulation run much slower. """ def __init__( @@ -39,6 +42,7 @@ def __init__( heading=None, initial_solution=None, terminate_on_apogee=None, + time_overshoot=None, ): """Initializes the Stochastic Flight class. @@ -63,11 +67,17 @@ def __init__( terminate_on_apogee : bool, optional Whether or not the flight should terminate on apogee. This attribute can not be randomized. + time_overshoot : bool + If False, the simulation will run at the time step defined by the controller + sampling rate. Be aware that this will make the simulation run much slower. """ if terminate_on_apogee is not None: assert isinstance(terminate_on_apogee, bool), ( "`terminate_on_apogee` must be a boolean" ) + if time_overshoot is not None: + if not isinstance(time_overshoot, bool): + raise TypeError("`time_overshoot` must be a boolean") super().__init__( flight, rail_length=rail_length, @@ -77,6 +87,7 @@ def __init__( self.initial_solution = initial_solution self.terminate_on_apogee = terminate_on_apogee + self.time_overshoot = time_overshoot def _validate_initial_solution(self, initial_solution): if initial_solution is not None: @@ -128,4 +139,5 @@ def create_object(self): heading=generated_dict["heading"], initial_solution=self.initial_solution, terminate_on_apogee=self.terminate_on_apogee, + time_overshoot=self.time_overshoot, ) diff --git a/rocketpy/stochastic/stochastic_rocket.py b/rocketpy/stochastic/stochastic_rocket.py index 56a8a44d5..9aad8872b 100644 --- a/rocketpy/stochastic/stochastic_rocket.py +++ b/rocketpy/stochastic/stochastic_rocket.py @@ -3,11 +3,13 @@ import warnings from random import choice +from rocketpy.control import _Controller from rocketpy.mathutils.vector_matrix import Vector from rocketpy.motors.empty_motor import EmptyMotor from rocketpy.motors.motor import GenericMotor, Motor from rocketpy.motors.solid_motor import SolidMotor from rocketpy.rocket.aero_surface import ( + AirBrakes, EllipticalFins, NoseCone, RailButtons, @@ -21,6 +23,7 @@ from rocketpy.stochastic.stochastic_motor_model import StochasticMotorModel from .stochastic_aero_surfaces import ( + StochasticAirBrakes, StochasticEllipticalFins, StochasticNoseCone, StochasticRailButtons, @@ -148,6 +151,7 @@ def __init__( self.motors = Components() self.aerodynamic_surfaces = Components() self.rail_buttons = Components() + self.air_brakes = [] self.parachutes = [] self.__components_map = {} super().__init__( @@ -389,6 +393,26 @@ def set_rail_buttons( rail_buttons, self._validate_position(rail_buttons, lower_button_position) ) + def add_air_brakes(self, air_brakes, controller): + """Adds an air brake to the stochastic rocket. + + Parameters + ---------- + air_brakes : StochasticAirBrakes or Airbrakes + The air brake to be added to the stochastic rocket. + controller : _Controller + Deterministic air brake controller. + """ + if not isinstance(air_brakes, (AirBrakes, StochasticAirBrakes)): + raise TypeError( + "`air_brake` must be of AirBrakes or StochasticAirBrakes type" + ) + if isinstance(air_brakes, AirBrakes): + air_brakes = StochasticAirBrakes(air_brakes=air_brakes) + + self.air_brakes.append(air_brakes) + self.air_brake_controller = controller + def add_cp_eccentricity(self, x=None, y=None): """Moves line of action of aerodynamic forces to simulate an eccentricity in the position of the center of pressure relative @@ -630,6 +654,7 @@ def dict_generator(self): generated_dict["motors"] = [] generated_dict["aerodynamic_surfaces"] = [] generated_dict["rail_buttons"] = [] + generated_dict["air_brakes"] = [] generated_dict["parachutes"] = [] self.last_rnd_dict = generated_dict yield generated_dict @@ -672,6 +697,11 @@ def _create_rail_buttons(self, component_stochastic_rail_buttons): ) return rail_buttons, lower_button_position_rnd, upper_button_position_rnd + def _create_air_brake(self, stochastic_air_brake): + air_brake = stochastic_air_brake.create_object() + self.last_rnd_dict["air_brakes"].append(stochastic_air_brake.last_rnd_dict) + return air_brake + def _create_parachute(self, stochastic_parachute): parachute = stochastic_parachute.create_object() self.last_rnd_dict["parachutes"].append(stochastic_parachute.last_rnd_dict) @@ -740,6 +770,17 @@ def create_object(self): surface, position_rnd = self._create_surface(component_surface) rocket.add_surfaces(surface, position_rnd) + for air_brake in self.air_brakes: + air_brake = self._create_air_brake(air_brake) + _controller = _Controller( + interactive_objects=air_brake, + controller_function=self.air_brake_controller.base_controller_function, + sampling_rate=self.air_brake_controller.sampling_rate, + initial_observed_variables=self.air_brake_controller.initial_observed_variables, + ) + rocket.air_brakes.append(air_brake) + rocket._add_controllers(_controller) + for component_rail_buttons in self.rail_buttons: ( rail_buttons,