Source code for rocketpy.simulation.flight

# pylint: disable=too-many-lines
import math
import warnings
from copy import deepcopy
from functools import cached_property

import numpy as np
from scipy.integrate import BDF, DOP853, LSODA, RK23, RK45, OdeSolver, Radau

from rocketpy.simulation.flight_data_exporter import FlightDataExporter

from ..mathutils.function import Function, funcify_method
from ..mathutils.vector_matrix import Matrix, Vector
from ..motors.point_mass_motor import PointMassMotor
from ..plots.flight_plots import _FlightPlots
from ..prints.flight_prints import _FlightPrints
from ..rocket import PointMassRocket
from ..tools import (
    calculate_cubic_hermite_coefficients,
    deprecated,
    euler313_to_quaternions,
    find_closest,
    find_root_linear_interpolation,
    find_roots_cubic_function,
    quaternions_to_nutation,
    quaternions_to_precession,
    quaternions_to_spin,
)

ODE_SOLVER_MAP = {
    "RK23": RK23,
    "RK45": RK45,
    "DOP853": DOP853,
    "Radau": Radau,
    "BDF": BDF,
    "LSODA": LSODA,
}


# pylint: disable=too-many-public-methods
# pylint: disable=too-many-instance-attributes
[docs] class Flight: """Keeps all flight information and has a method to simulate flight. Attributes ---------- Flight.env : Environment Environment object describing rail length, elevation, gravity and weather condition. See Environment class for more details. Flight.rocket : Rocket Rocket class describing rocket. See Rocket class for more details. Flight.parachutes : Parachute Direct link to parachutes of the Rocket. See Rocket class for more details. Flight.frontal_surface_wind : float Surface wind speed in m/s aligned with the launch rail. Flight.lateral_surface_wind : float Surface wind speed in m/s perpendicular to launch rail. Flight.FlightPhases : class Helper class to organize and manage different flight phases. Flight.TimeNodes : class Helper class to manage time discretization during simulation. Flight.time_iterator : function Helper iterator function to generate time discretization points. Flight.rail_length : float, int Launch rail length in meters. Flight.effective_1rl : float Original rail length minus the distance measured from nozzle exit to the upper rail button. It assumes the nozzle to be aligned with the beginning of the rail. Flight.effective_2rl : float Original rail length minus the distance measured from nozzle exit to the lower rail button. It assumes the nozzle to be aligned with the beginning of the rail. Flight.name: str Name of the flight. Flight._controllers : list List of controllers to be used during simulation. Flight.max_time : int, float Maximum simulation time allowed. Refers to physical time being simulated, not time taken to run simulation. Flight.max_time_step : int, float Maximum time step to use during numerical integration in seconds. Flight.min_time_step : int, float Minimum time step to use during numerical integration in seconds. Flight.rtol : int, float Maximum relative error tolerance to be tolerated in the numerical integration scheme. Flight.atol : int, float Maximum absolute error tolerance to be tolerated in the integration scheme. Flight.time_overshoot : bool, optional If True, decouples ODE time step from parachute and controller trigger functions sampling rate. The time steps can overshoot the necessary trigger function evaluation points and then interpolation is used to calculate them and feed the triggers. Can greatly improve run time in some cases. Flight.terminate_on_apogee : bool Whether to terminate simulation when rocket reaches apogee. Flight.solver : scipy.integrate.LSODA Scipy LSODA integration scheme. Flight.x : Function Rocket's X coordinate (positive east) as a function of time. Flight.y : Function Rocket's Y coordinate (positive north) as a function of time. Flight.z : Function Rocket's z coordinate (positive up) as a function of time. Flight.vx : Function Velocity of the rocket's center of dry mass in the X (East) direction of the inertial frame as a function of time. Flight.vy : Function Velocity of the rocket's center of dry mass in the Y (North) direction of the inertial frame as a function of time. Flight.vz : Function Velocity of the rocket's center of dry mass in the Z (Up) direction of the inertial frame as a function of time. Flight.e0 : Function Rocket's Euler parameter 0 as a function of time. Flight.e1 : Function Rocket's Euler parameter 1 as a function of time. Flight.e2 : Function Rocket's Euler parameter 2 as a function of time. Flight.e3 : Function Rocket's Euler parameter 3 as a function of time. Flight.w1 : Function Angular velocity of the rocket in the x direction of the rocket's body frame as a function of time, in rad/s. Sometimes referred to as pitch rate (q). Flight.w2 : Function Angular velocity of the rocket in the y direction of the rocket's body frame as a function of time, in rad/s. Sometimes referred to as yaw rate (r). Flight.w3 : Function Angular velocity of the rocket in the z direction of the rocket's body frame as a function of time, in rad/s. Sometimes referred to as roll rate (p). Flight.latitude: Function Rocket's latitude coordinates (positive North) as a function of time. The Equator has a latitude equal to 0, by convention. Flight.longitude: Function Rocket's longitude coordinates (positive East) as a function of time. Greenwich meridian has a longitude equal to 0, by convention. Flight.inclination : int, float Launch rail inclination angle relative to ground, given in degrees. Flight.heading : int, float Launch heading angle relative to north given in degrees. Flight.initial_solution : list List defines initial condition - [t_initial, x_init, y_init, z_init, vx_init, vy_init, vz_init, e0_init, e1_init, e2_init, e3_init, w1_init, w2_init, w3_init] Flight.t_initial : int, float Initial simulation time in seconds. Usually 0. Flight.solution : list Solution array which keeps results from each numerical integration. Flight.t : float Current integration time. Flight.y : list Current integration state vector u. Flight.out_of_rail_time : int, float Time, in seconds, in which the rocket completely leaves the rail. Flight.out_of_rail_state : list State vector u corresponding to state when the rocket completely leaves the rail. Flight.out_of_rail_velocity : int, float Velocity, in m/s, with which the rocket completely leaves the rail. Flight.apogee_state : array State vector u corresponding to state when the rocket's vertical velocity is zero in the apogee. Flight.apogee_time : int, float Time, in seconds, in which the rocket's vertical velocity reaches zero in the apogee. Flight.apogee_x : int, float X coordinate (positive east) of the center of mass of the rocket when it reaches apogee. Flight.apogee_y : int, float Y coordinate (positive north) of the center of mass of the rocket when it reaches apogee. Flight.apogee : int, float Z coordinate, or altitude, of the center of mass of the rocket when it reaches apogee. Flight.x_impact : int, float X coordinate (positive east) of the center of mass of the rocket when it impacts ground. Flight.y_impact : int, float Y coordinate (positive east) of the center of mass of the rocket when it impacts ground. Flight.impact_velocity : int, float Velocity magnitude of the center of mass of the rocket when it impacts ground. Flight.impact_state : array State vector u corresponding to state when the rocket impacts the ground. Flight.parachute_events : array List that stores parachute events triggered during flight. Flight.function_evaluations : array List that stores number of derivative function evaluations during numerical integration in cumulative manner. Flight.function_evaluations_per_time_step : list List that stores number of derivative function evaluations per time step during numerical integration. Flight.time_steps : array List of time steps taking during numerical integration in seconds. Flight.flight_phases : Flight.FlightPhases Stores and manages flight phases. Flight.wind_velocity_x : Function Wind velocity X (East) experienced by the rocket as a function of time. Flight.wind_velocity_y : Function Wind velocity Y (North) experienced by the rocket as a function of time. Flight.density : Function Air density experienced by the rocket as a function of time. Flight.pressure : Function Air pressure experienced by the rocket as a function of time. Flight.dynamic_viscosity : Function Air dynamic viscosity experienced by the rocket as a function of time. Flight.speed_of_sound : Function Speed of Sound in air experienced by the rocket as a function of time. Flight.ax : Function Acceleration of the rocket's center of dry mass along the X (East) axis in the inertial frame as a function of time. Flight.ay : Function Acceleration of the rocket's center of dry mass along the Y (North) axis in the inertial frame as a function of time. Flight.az : Function Acceleration of the rocket's center of dry mass along the Z (Up) axis in the inertial frame as a function of time. Flight.alpha1 : Function Angular acceleration of the rocket in the x direction of the rocket's body frame as a function of time, in rad/s. Sometimes referred to as yaw acceleration. Flight.alpha2 : Function Angular acceleration of the rocket in the y direction of the rocket's body frame as a function of time, in rad/s. Sometimes referred to as yaw acceleration. Flight.alpha3 : Function Angular acceleration of the rocket in the z direction of the rocket's body frame as a function of time, in rad/s. Sometimes referred to as roll acceleration. Flight.speed : Function Rocket velocity magnitude in m/s relative to ground as a function of time. Flight.max_speed : float Maximum velocity magnitude in m/s reached by the rocket relative to ground during flight. Flight.max_speed_time : float Time in seconds at which rocket reaches maximum velocity magnitude relative to ground. Flight.horizontal_speed : Function Rocket's velocity magnitude in the horizontal (North-East) plane in m/s as a function of time. Flight.acceleration : Function Rocket acceleration magnitude in m/s² relative to ground as a function of time. Flight.max_acceleration : float Maximum acceleration magnitude in m/s² reached by the rocket relative to ground during flight. Flight.max_acceleration_time : float Time in seconds at which rocket reaches maximum acceleration magnitude relative to ground. Flight.path_angle : Function Rocket's flight path angle, or the angle that the rocket's velocity makes with the horizontal (North-East) plane. Measured in degrees and expressed as a function of time. Flight.attitude_vector_x : Function Rocket's attitude vector, or the vector that points in the rocket's axis of symmetry, component in the X direction (East) as a function of time. Flight.attitude_vector_y : Function Rocket's attitude vector, or the vector that points in the rocket's axis of symmetry, component in the Y direction (East) as a function of time. Flight.attitude_vector_z : Function Rocket's attitude vector, or the vector that points in the rocket's axis of symmetry, component in the Z direction (East) as a function of time. Flight.attitude_angle : Function Rocket's attitude angle, or the angle that the rocket's axis of symmetry makes with the horizontal (North-East) plane. Measured in degrees and expressed as a function of time. Flight.lateral_attitude_angle : Function Rocket's lateral attitude angle, or the angle that the rocket's axis of symmetry makes with plane defined by the launch rail direction and the Z (up) axis. Measured in degrees and expressed as a function of time. Flight.phi : Function Rocket's Spin Euler Angle, φ, according to the 3-2-3 rotation system nomenclature (NASA Standard Aerospace). Measured in degrees and expressed as a function of time. Flight.theta : Function Rocket's Nutation Euler Angle, θ, according to the 3-2-3 rotation system nomenclature (NASA Standard Aerospace). Measured in degrees and expressed as a function of time. Flight.psi : Function Rocket's Precession Euler Angle, ψ, according to the 3-2-3 rotation system nomenclature (NASA Standard Aerospace). Measured in degrees and expressed as a function of time. Flight.R1 : Function Aerodynamic force acting along the x-axis of the rocket's body frame as a function of time. Expressed in Newtons (N). Flight.R2 : Function Aerodynamic force acting along the y-axis of the rocket's body frame as a function of time. Expressed in Newtons (N). Flight.R3 : Function Aerodynamic force acting along the z-axis of the rocket's body frame as a function of time. Expressed in Newtons (N). Flight.M1 : Function Aerodynamic moment acting along the x-axis of the rocket's body frame as a function of time. Expressed in Newtons (N). Flight.M2 : Function Aerodynamic moment acting along the y-axis of the rocket's body frame as a function of time. Expressed in Newtons (N). Flight.M3 : Function Aerodynamic moment acting along the z-axis of the rocket's body frame as a function of time. Expressed in Newtons (N). Flight.net_thrust : Function Rocket's engine net thrust as a function of time in Newton. This is the actual thrust force experienced by the rocket. It may be corrected with the atmospheric pressure if a reference pressure is defined. Flight.aerodynamic_lift : Function Resultant force perpendicular to rockets axis due to aerodynamic effects as a function of time. Units in N. Expressed as a function of time. Can be called or accessed as array. Flight.aerodynamic_drag : Function Resultant force aligned with the rockets axis due to aerodynamic effects as a function of time. Units in N. Expressed as a function of time. Can be called or accessed as array. Flight.aerodynamic_bending_moment : Function Resultant moment perpendicular to rocket's axis due to aerodynamic effects as a function of time. Units in N m. Expressed as a function of time. Can be called or accessed as array. Flight.aerodynamic_spin_moment : Function Resultant moment aligned with the rockets axis due to aerodynamic effects as a function of time. Units in N m. Expressed as a function of time. Can be called or accessed as array. Flight.rail_button1_normal_force : Function Upper rail button normal force in N as a function of time. Flight.max_rail_button1_normal_force : float Maximum upper rail button normal force experienced during rail flight phase in N. Flight.rail_button1_shear_force : Function Upper rail button shear force in N as a function of time. Flight.max_rail_button1_shear_force : float Maximum upper rail button shear force experienced during rail flight phase in N. Flight.rail_button2_normal_force : Function Lower rail button normal force in N as a function of time. Flight.max_rail_button2_normal_force : float Maximum lower rail button normal force experienced during rail flight phase in N. Flight.rail_button2_shear_force : Function Lower rail button shear force in N as a function of time. Flight.max_rail_button2_shear_force : float Maximum lower rail button shear force experienced during rail flight phase in N. Flight.rotational_energy : Function Rocket's rotational kinetic energy as a function of time. Units in J. Flight.translational_energy : Function Rocket's translational kinetic energy as a function of time. Units in J. Flight.kinetic_energy : Function Rocket's total kinetic energy as a function of time. Units in J. Flight.potential_energy : Function Rocket's gravitational potential energy as a function of time. Units in J. Flight.total_energy : Function Rocket's total mechanical energy as a function of time. Units in J. Flight.thrust_power : Function Rocket's engine thrust power output as a function of time in Watts. Flight.drag_power : Function Aerodynamic drag power output as a function of time in Watts. Flight.attitude_frequency_response : Function Fourier Frequency Analysis of the rocket's attitude angle. Expressed as the absolute value of the magnitude as a function of frequency in Hz. Flight.omega1_frequency_response : Function Fourier Frequency Analysis of the rocket's angular velocity omega 1. Expressed as the absolute value of the magnitude as a function of frequency in Hz. Flight.omega2_frequency_response : Function Fourier Frequency Analysis of the rocket's angular velocity omega 2. Expressed as the absolute value of the magnitude as a function of frequency in Hz. Flight.omega3_frequency_response : Function Fourier Frequency Analysis of the rocket's angular velocity omega 3. Expressed as the absolute value of the magnitude as a function of frequency in Hz. Flight.static_margin : Function Rocket's static margin during flight in calibers. Flight.stability_margin : Function Rocket's stability margin during flight, in calibers. Flight.initial_stability_margin : float Rocket's initial stability margin in calibers. Flight.out_of_rail_stability_margin : float Rocket's stability margin in calibers when it leaves the rail. Flight.stream_velocity_x : Function Freestream velocity x (East) component, in m/s, as a function of time. Flight.stream_velocity_y : Function Freestream velocity y (North) component, in m/s, as a function of time. Flight.stream_velocity_z : Function Freestream velocity z (up) component, in m/s, as a function of time. Flight.free_stream_speed : Function Freestream velocity magnitude, in m/s, as a function of time. Flight.apogee_freestream_speed : float Freestream speed of the rocket at apogee in m/s. Flight.mach_number : Function Rocket's Mach number defined as its freestream speed divided by the speed of sound at its altitude. Expressed as a function of time. Flight.max_mach_number : float Rocket's maximum Mach number experienced during flight. Flight.max_mach_number_time : float Time at which the rocket experiences the maximum Mach number. Flight.reynolds_number : Function Rocket's Reynolds number, using its diameter as reference length and free_stream_speed as reference velocity. Expressed as a function of time. Flight.max_reynolds_number : float Rocket's maximum Reynolds number experienced during flight. Flight.max_reynolds_number_time : float Time at which the rocket experiences the maximum Reynolds number. Flight.dynamic_pressure : Function Dynamic pressure experienced by the rocket in Pa as a function of time, defined by 0.5*rho*V^2, where rho is air density and V is the freestream speed. Flight.max_dynamic_pressure : float Maximum dynamic pressure, in Pa, experienced by the rocket. Flight.max_dynamic_pressure_time : float Time at which the rocket experiences maximum dynamic pressure. Flight.total_pressure : Function Total pressure experienced by the rocket in Pa as a function of time. Flight.max_total_pressure : float Maximum total pressure, in Pa, experienced by the rocket. Flight.max_total_pressure_time : float Time at which the rocket experiences maximum total pressure. Flight.angle_of_attack : Function Rocket's angle of attack in degrees as a function of time. Defined as the minimum angle between the attitude vector and the freestream velocity vector. Can be called or accessed as array. Flight.simulation_mode : str Simulation mode for the flight. Can be "6 DOF" or "3 DOF". Flight.rail_button1_bending_moment : Function Internal bending moment at upper rail button attachment point in N·m as a function of time. Calculated using beam theory during rail phase. Flight.max_rail_button1_bending_moment : float Maximum internal bending moment experienced at upper rail button attachment point during rail flight phase in N·m. Flight.rail_button2_bending_moment : Function Internal bending moment at lower rail button attachment point in N·m as a function of time. Calculated using beam theory during rail phase. Flight.max_rail_button2_bending_moment : float Maximum internal bending moment experienced at lower rail button attachment point during rail flight phase in N·m. """
[docs] def __init__( # pylint: disable=too-many-arguments,too-many-statements self, rocket, environment, rail_length, inclination=80.0, heading=90.0, initial_solution=None, terminate_on_apogee=False, max_time=600, max_time_step=np.inf, min_time_step=0, rtol=1e-6, atol=None, time_overshoot=True, verbose=False, name="Flight", equations_of_motion="standard", ode_solver="LSODA", simulation_mode="6 DOF", ): """Run a trajectory simulation. Parameters ---------- rocket : Rocket Rocket to simulate. environment : Environment Environment to run simulation on. rail_length : int, float Length in which the rocket will be attached to the rail, only moving along a fixed direction, that is, the line parallel to the rail. Currently, if the an initial_solution is passed, the rail length is not used. inclination : int, float, optional Rail inclination angle relative to ground, given in degrees. Default is 80. heading : int, float, optional Heading angle relative to north given in degrees. Default is 90, which points in the x (east) direction. initial_solution : array, Flight, optional Initial solution array to be used. Format is: .. code-block:: python initial_solution = [ self.t_initial, x_init, y_init, z_init, vx_init, vy_init, vz_init, e0_init, e1_init, e2_init, e3_init, w1_init, w2_init, w3_init ] If a Flight object is used, the last state vector will be used as initial solution. If None, the initial solution will start with all null values, except for the euler parameters which will be calculated based on given values of inclination and heading. Default is None. terminate_on_apogee : boolean, optional Whether to terminate simulation when rocket reaches apogee. Default is False. max_time : int, float, optional Maximum time in which to simulate trajectory in seconds. Using this without setting a max_time_step may cause unexpected errors. Default is 600. max_time_step : int, float, optional Maximum time step to use during integration in seconds. Default is 0.01. min_time_step : int, float, optional Minimum time step to use during integration in seconds. Default is 0.01. rtol : float, array, optional Maximum relative error tolerance to be tolerated in the integration scheme. Can be given as array for each state space variable. Default is 1e-6. atol : float, optional Maximum absolute error tolerance to be tolerated in the integration scheme. Can be given as array for each state space variable. Default is 6*[1e-3] + 4*[1e-6] + 3*[1e-3]. time_overshoot : bool, optional If True, decouples ODE time step from parachute and controller trigger functions sampling rate. The time steps can overshoot the necessary trigger function evaluation points and then interpolation is used to calculate them and feed the triggers. Can greatly improve run time in some cases. Default is True. verbose : bool, optional If true, verbose mode is activated. Default is False. name : str, optional Name of the flight. Default is "Flight". equations_of_motion : str, optional Type of equations of motion to use. Can be "standard" or "solid_propulsion". Default is "standard". Solid propulsion is a more restricted set of equations of motion that only works for solid propulsion rockets. Such equations were used in RocketPy v0 and are kept here for backwards compatibility. ode_solver : str, ``scipy.integrate.OdeSolver``, optional Integration method to use to solve the equations of motion ODE. Available options are: 'RK23', 'RK45', 'DOP853', 'Radau', 'BDF', 'LSODA' from ``scipy.integrate.solve_ivp``. Default is 'LSODA', which is recommended for most flights. A custom ``scipy.integrate.OdeSolver`` can be passed as well. For more information on the integration methods, see the scipy documentation [1]_. Returns ------- None References ---------- .. [1] https://docs.scipy.org/doc/scipy/reference/generated/scipy.integrate.solve_ivp.html """ # Save arguments self.env = environment self.rocket = rocket self.rail_length = rail_length if self.rail_length <= 0: # pragma: no cover raise ValueError("Rail length must be a positive value.") self.parachutes = self.rocket.parachutes[:] self.inclination = inclination self.heading = heading self.max_time = max_time self.max_time_step = max_time_step self.min_time_step = min_time_step self.rtol = rtol self.atol = atol or 6 * [1e-3] + 4 * [1e-6] + 3 * [1e-3] self.initial_solution = initial_solution self.time_overshoot = time_overshoot self.terminate_on_apogee = terminate_on_apogee self.name = name self.equations_of_motion = equations_of_motion self.simulation_mode = simulation_mode self.ode_solver = ode_solver # Controller initialization self.__init_controllers() # Flight initialization self.__init_solution_monitors() self.__init_equations_of_motion() self.__init_solver_monitors() # Create known flight phases self.flight_phases = self.FlightPhases() self.flight_phases.add_phase( self.t_initial, self.initial_derivative, clear=False ) self.flight_phases.add_phase(self.max_time) # Simulate flight self.__simulate(verbose) # Initialize prints and plots objects self.prints = _FlightPrints(self) self.plots = _FlightPlots(self)
def __repr__(self): return ( f"<Flight(rocket= {self.rocket}, " f"environment= {self.env}, " f"rail_length= {self.rail_length}, " f"inclination= {self.inclination}, " f"heading = {self.heading}," f"name= {self.name})>" ) # pylint: disable=too-many-nested-blocks, too-many-branches, too-many-locals,too-many-statements def __simulate(self, verbose): """Simulate the flight trajectory.""" for phase_index, phase in self.time_iterator(self.flight_phases): # Determine maximum time for this flight phase phase.time_bound = self.flight_phases[phase_index + 1].t # Evaluate callbacks for callback in phase.callbacks: callback(self) # Create solver for this flight phase self.function_evaluations.append(0) phase.solver = self._solver( phase.derivative, t0=phase.t, y0=self.y_sol, t_bound=phase.time_bound, rtol=self.rtol, atol=self.atol, max_step=self.max_time_step, min_step=self.min_time_step, ) # Initialize phase time nodes self.__setup_phase_time_nodes(phase) # Iterate through time nodes for node_index, node in self.time_iterator(phase.time_nodes): # Determine time bound for this time node node.time_bound = phase.time_nodes[node_index + 1].t phase.solver.t_bound = node.time_bound if self.__is_lsoda: phase.solver._lsoda_solver._integrator.rwork[0] = ( phase.solver.t_bound ) phase.solver._lsoda_solver._integrator.call_args[4] = ( phase.solver._lsoda_solver._integrator.rwork ) phase.solver.status = "running" # Feed required parachute and discrete controller triggers # TODO: parachutes should be moved to controllers for callback in node.callbacks: callback(self) self.__process_sensors_and_controllers_at_current_node(node, phase) for controller in node._controllers: controller( self.t, self.y_sol, self.solution, self.sensors, self.env, ) for parachute in node.parachutes: # Calculate and save pressure signal ( noisy_pressure, height_above_ground_level, ) = self.__calculate_and_save_pressure_signals( parachute, node.t, self.y_sol[2] ) if parachute.triggerfunc( noisy_pressure, height_above_ground_level, self.y_sol, self.sensors, ): # Remove parachute from flight parachutes self.parachutes.remove(parachute) # Create phase for time after detection and before inflation # Must only be created if parachute has any lag i = 1 if parachute.lag != 0: self.flight_phases.add_phase( node.t, phase.derivative, clear=True, index=phase_index + i, ) i += 1 # Create flight phase for time after inflation callbacks = [ lambda self, parachute_cd_s=parachute.cd_s: setattr( self, "parachute_cd_s", parachute_cd_s ), lambda self, parachute_radius=parachute.radius: setattr( self, "parachute_radius", parachute_radius ), lambda self, parachute_height=parachute.height: setattr( self, "parachute_height", parachute_height ), lambda self, parachute_porosity=parachute.porosity: setattr( self, "parachute_porosity", parachute_porosity ), lambda self, added_mass_coefficient=parachute.added_mass_coefficient: ( setattr( self, "parachute_added_mass_coefficient", added_mass_coefficient, ) ), ] self.flight_phases.add_phase( node.t + parachute.lag, self.u_dot_parachute, callbacks, clear=False, index=phase_index + i, ) # Prepare to leave loops and start new flight phase phase.time_nodes.flush_after(node_index) phase.time_nodes.add_node(self.t, [], [], []) phase.solver.status = "finished" # Save parachute event self.parachute_events.append([self.t, parachute]) if self.__check_and_handle_parachute_triggers( node, phase, phase_index, node_index ): break # Stop simulation if parachute is deployed # Step through simulation while phase.solver.status == "running": # Execute solver step, log solution and function evaluations phase.solver.step() self.solution += [[phase.solver.t, *phase.solver.y]] self.function_evaluations.append(phase.solver.nfev) # Update time and state self.t = phase.solver.t self.y_sol = phase.solver.y if verbose: print(f"Current Simulation Time: {self.t:3.4f} s", end="\r") if self.__check_simulation_events(phase, phase_index, node_index): break # Stop if simulation termination event occurred # Process overshootable time nodes if enabled if self.time_overshoot and self.__process_overshootable_nodes( phase, phase_index, node_index ): break # If controlled flight, post process must be done on sim time # Post-process controllers if needed if self._controllers: phase.derivative(self.t, self.y_sol, post_processing=True) self.t_final = self.t self.__transform_pressure_signals_lists_to_functions() if self._controllers: # cache post process variables self.__evaluate_post_process = np.array(self.__post_processed_variables) if self.sensors: self.__cache_sensor_data() if verbose: print(f"\n>>> Simulation Completed at Time: {self.t:3.4f} s") def __setup_phase_time_nodes(self, phase): """Set up time nodes for the current phase. Parameters ---------- phase : FlightPhase The current flight phase. """ phase.time_nodes = self.TimeNodes() # Add first time node phase.time_nodes.add_node(phase.t, [], [], []) if self.time_overshoot is False: phase.time_nodes.add_parachutes(self.parachutes, phase.t, phase.time_bound) phase.time_nodes.add_sensors(self.rocket.sensors, phase.t, phase.time_bound) phase.time_nodes.add_controllers( self._controllers, phase.t, phase.time_bound ) # Add last time node phase.time_nodes.add_node(phase.time_bound, [], [], []) # Organize time nodes phase.time_nodes.sort() phase.time_nodes.merge() # Clear triggers from first time node if necessary if phase.clear: phase.time_nodes[0].parachutes = [] phase.time_nodes[0].callbacks = [] def __process_sensors_and_controllers_at_current_node(self, node, phase): """Process sensors and controllers at the current node. Parameters ---------- node : TimeNode The current time node. phase : FlightPhase The current flight phase. """ if self.sensors: u_dot = phase.derivative(self.t, self.y_sol) self.__measure_sensors(node._component_sensors, u_dot) for controller in node._controllers: controller( self.t, self.y_sol, self.solution, self.sensors, self.env, ) def __measure_sensors(self, component_sensors, u_dot, t=None, y_sol=None): """Measure sensors with the given state and derivative. Parameters ---------- component_sensors : list List of (sensor, position) tuples. u_dot : array_like State derivative vector. t : float, optional Time for measurement. If None, uses self.t. y_sol : array_like, optional State vector. If None, uses self.y_sol. """ if t is None: t = self.t if y_sol is None: y_sol = self.y_sol for sensor, position in component_sensors: relative_position = position - self.rocket._csys * Vector( [0, 0, self.rocket.center_of_dry_mass_position] ) sensor.measure( t, u=y_sol, u_dot=u_dot, relative_position=relative_position, environment=self.env, gravity=self.env.gravity.get_value_opt( y_sol[2] if len(y_sol) > 2 else self.solution[-1][3] ), pressure=self.env.pressure, earth_radius=self.env.earth_radius, initial_coordinates=(self.env.latitude, self.env.longitude), ) def __check_and_handle_parachute_triggers( self, node, phase, phase_index, node_index ): """Check for parachute triggers and handle deployment. Parameters ---------- node : TimeNode The current time node. phase : FlightPhase The current flight phase. phase_index : int The index of the current phase. node_index : int The index of the current node. Returns ------- bool True if a parachute was triggered and the phase should break. """ for parachute in node.parachutes: # Calculate and save pressure signal ( noisy_pressure, height_above_ground_level, ) = self.__calculate_and_save_pressure_signals( parachute, node.t, self.y_sol[2] ) if not parachute.triggerfunc( noisy_pressure, height_above_ground_level, self.y_sol, self.sensors, ): continue # Check next parachute # Remove parachute from flight parachutes (if not already removed) if parachute in self.parachutes: self.parachutes.remove(parachute) else: continue # Parachute already triggered, skip to next # Create phase for time after detection and before inflation # Must only be created if parachute has any lag i = 1 if parachute.lag != 0: self.flight_phases.add_phase( node.t, phase.derivative, clear=True, index=phase_index + i, ) i += 1 # Create flight phase for time after inflation callbacks = [ lambda self, parachute_cd_s=parachute.cd_s: setattr( self, "parachute_cd_s", parachute_cd_s ), lambda self, parachute_radius=parachute.radius: setattr( self, "parachute_radius", parachute_radius ), lambda self, parachute_height=parachute.height: setattr( self, "parachute_height", parachute_height ), lambda self, parachute_porosity=parachute.porosity: setattr( self, "parachute_porosity", parachute_porosity ), lambda self, added_mass_coefficient=parachute.added_mass_coefficient: ( setattr( self, "parachute_added_mass_coefficient", added_mass_coefficient, ) ), ] self.flight_phases.add_phase( node.t + parachute.lag, self.u_dot_parachute, callbacks, clear=False, index=phase_index + i, ) # Prepare to leave loops and start new flight phase phase.time_nodes.flush_after(node_index) phase.time_nodes.add_node(self.t, [], [], []) phase.solver.status = "finished" self.parachute_events.append([self.t, parachute]) return True return False def __check_simulation_events(self, phase, phase_index, node_index): """Check for simulation events like out of rail, apogee, and impact. Parameters ---------- phase : FlightPhase The current flight phase. phase_index : int The index of the current phase. node_index : int The index of the current node. Returns ------- bool True if an event occurred and the simulation should break. """ # Check for first out of rail event if len(self.out_of_rail_state) == 1 and ( self.y_sol[0] ** 2 + self.y_sol[1] ** 2 + (self.y_sol[2] - self.env.elevation) ** 2 >= self.effective_1rl**2 ): return self.__handle_out_of_rail_event(phase, phase_index, node_index) # Check for apogee event # TODO: negative vz doesn't really mean apogee. Improve this. if len(self.apogee_state) == 1 and self.y_sol[5] < 0: return self.__handle_apogee_event(phase, phase_index, node_index) # Check for impact event if self.y_sol[2] < self.env.elevation: return self.__handle_impact_event(phase, phase_index, node_index) return False def __handle_out_of_rail_event(self, phase, phase_index, node_index): """Handle the out of rail event. Parameters ---------- phase : FlightPhase The current flight phase. phase_index : int The index of the current phase. node_index : int The index of the current node. Returns ------- bool True to indicate the simulation should break. """ # Check exactly when it went out using root finding # Disconsider elevation self.solution[-2][3] -= self.env.elevation self.solution[-1][3] -= self.env.elevation # Get points y0 = sum(self.solution[-2][i] ** 2 for i in [1, 2, 3]) - self.effective_1rl**2 yp0 = 2 * sum( self.solution[-2][i] * self.solution[-2][i + 3] for i in [1, 2, 3] ) t1 = self.solution[-1][0] - self.solution[-2][0] y1 = sum(self.solution[-1][i] ** 2 for i in [1, 2, 3]) - self.effective_1rl**2 yp1 = 2 * sum( self.solution[-1][i] * self.solution[-1][i + 3] for i in [1, 2, 3] ) # Put elevation back self.solution[-2][3] += self.env.elevation self.solution[-1][3] += self.env.elevation # Cubic Hermite interpolation (ax**3 + bx**2 + cx + d) a, b, c, d = calculate_cubic_hermite_coefficients( 0, float(phase.solver.step_size), y0, yp0, y1, yp1, ) a += 1e-5 # TODO: why?? # Find roots t_roots = find_roots_cubic_function(a, b, c, d) # Find correct root valid_t_root = [ t_root.real for t_root in t_roots if 0 < t_root.real < t1 and abs(t_root.imag) < 0.001 ] if len(valid_t_root) > 1: # pragma: no cover raise ValueError("Multiple roots found when solving for rail exit time.") if len(valid_t_root) == 0: # pragma: no cover raise ValueError("No valid roots found when solving for rail exit time.") # Determine final state when upper button is going out of rail self.t = valid_t_root[0] + self.solution[-2][0] interpolator = phase.solver.dense_output() self.y_sol = interpolator(self.t) self.solution[-1] = [self.t, *self.y_sol] self.out_of_rail_time = self.t self.out_of_rail_time_index = len(self.solution) - 1 self.out_of_rail_state = self.y_sol # Create new flight phase self.flight_phases.add_phase( self.t, self.u_dot_generalized, index=phase_index + 1, ) # Prepare to leave loops and start new flight phase phase.time_nodes.flush_after(node_index) phase.time_nodes.add_node(self.t, [], [], []) phase.solver.status = "finished" return True def __handle_apogee_event(self, phase, phase_index, node_index): """Handle the apogee event. Parameters ---------- phase : FlightPhase The current flight phase. phase_index : int The index of the current phase. node_index : int The index of the current node. Returns ------- bool True if simulation should break, False otherwise. """ # Assume linear vz(t) to detect when vz = 0 t0, vz0 = self.solution[-2][0], self.solution[-2][6] t1, vz1 = self.solution[-1][0], self.solution[-1][6] t_root = find_root_linear_interpolation(t0, t1, vz0, vz1, 0) # Fetch state at t_root interpolator = phase.solver.dense_output() self.apogee_state = interpolator(t_root) # Store apogee data self.apogee_time = t_root self.apogee_x = self.apogee_state[0] self.apogee_y = self.apogee_state[1] self.apogee = self.apogee_state[2] if self.terminate_on_apogee: self.t = self.t_final = t_root # Roll back solution self.solution[-1] = [self.t, *self.apogee_state] # Set last flight phase self.flight_phases.flush_after(phase_index) self.flight_phases.add_phase(self.t) # Prepare to leave loops and start new flight phase phase.time_nodes.flush_after(node_index) phase.time_nodes.add_node(self.t, [], [], []) phase.solver.status = "finished" return True elif len(self.solution) > 2: # adding the apogee state to solution increases accuracy # we can only do this if the apogee is not the first state self.solution.insert(-1, [t_root, *self.apogee_state]) return False def __handle_impact_event(self, phase, phase_index, node_index): """Handle the impact event. Parameters ---------- phase : FlightPhase The current flight phase. phase_index : int The index of the current phase. node_index : int The index of the current node. Returns ------- bool True to indicate the simulation should break. """ # Check exactly when it happened using root finding # Cubic Hermite interpolation (ax**3 + bx**2 + cx + d) a, b, c, d = calculate_cubic_hermite_coefficients( x0=0, # t0 x1=float(phase.solver.step_size), # t1 - t0 y0=float(self.solution[-2][3] - self.env.elevation), # z0 yp0=float(self.solution[-2][6]), # vz0 y1=float(self.solution[-1][3] - self.env.elevation), # z1 yp1=float(self.solution[-1][6]), # vz1 ) # Find roots t_roots = find_roots_cubic_function(a, b, c, d) # Find correct root t1 = self.solution[-1][0] - self.solution[-2][0] valid_t_root = [ t_root.real for t_root in t_roots if abs(t_root.imag) < 0.001 and 0 < t_root.real < t1 ] if len(valid_t_root) > 1: # pragma: no cover raise ValueError("Multiple roots found when solving for impact time.") # Determine impact state at t_root self.t = self.t_final = valid_t_root[0] + self.solution[-2][0] interpolator = phase.solver.dense_output() self.y_sol = self.impact_state = interpolator(self.t) # Roll back solution self.solution[-1] = [self.t, *self.y_sol] # Save impact state self.x_impact = self.impact_state[0] self.y_impact = self.impact_state[1] self.z_impact = self.impact_state[2] self.impact_velocity = self.impact_state[5] # Set last flight phase self.flight_phases.flush_after(phase_index) self.flight_phases.add_phase(self.t) # Prepare to leave loops and start new flight phase phase.time_nodes.flush_after(node_index) phase.time_nodes.add_node(self.t, [], [], []) phase.solver.status = "finished" return True def __process_overshootable_nodes(self, phase, phase_index, node_index): """Process overshootable time nodes for parachutes, controllers, and sensors. Parameters ---------- phase : FlightPhase The current flight phase. phase_index : int The index of the current phase. node_index : int The index of the current node. Returns ------- bool True if a parachute was triggered and the simulation should break. """ overshootable_nodes = self.TimeNodes() overshootable_nodes.add_parachutes( self.parachutes, self.solution[-2][0], self.t ) overshootable_nodes.add_controllers( self._controllers, self.solution[-2][0], self.t ) overshootable_nodes.add_sensors( self.rocket.sensors, self.solution[-2][0], self.t ) # Add last time node (always skipped) overshootable_nodes.add_node(self.t, [], [], []) if len(overshootable_nodes) < 1: return False # Early exit overshootable_nodes.sort() overshootable_nodes.merge() # Clear if necessary if overshootable_nodes[0].t == phase.t and phase.clear: overshootable_nodes[0].parachutes = [] overshootable_nodes[0].callbacks = [] # Feed overshootable time nodes trigger interpolator = phase.solver.dense_output() for overshootable_index, overshootable_node in self.time_iterator( overshootable_nodes ): # Calculate state at node time overshootable_node.y_sol = interpolator(overshootable_node.t) # Check for parachute triggers if self.__check_overshootable_parachute_triggers( overshootable_node, overshootable_nodes, overshootable_index, phase, phase_index, node_index, ): return True # Process controllers at overshootable node for controller in overshootable_node._controllers: controller( overshootable_node.t, overshootable_node.y_sol, self.solution, self.sensors, self.env, ) # Process sensors at overshootable node if overshootable_node._component_sensors: # Calculate u_dot for sensors at interpolated state u_dot = phase.derivative(overshootable_node.t, overshootable_node.y_sol) self.__measure_sensors( overshootable_node._component_sensors, u_dot, overshootable_node.t, overshootable_node.y_sol, ) return False def __check_overshootable_parachute_triggers( self, overshootable_node, overshootable_nodes, overshootable_index, phase, phase_index, node_index, ): """Check for parachute triggers in overshootable nodes. Parameters ---------- overshootable_node : TimeNode The current overshootable node. overshootable_nodes : TimeNodes The overshootable nodes collection. overshootable_index : int Index of the current overshootable node. phase : FlightPhase The current flight phase. phase_index : int The index of the current phase. node_index : int The index of the current node. Returns ------- bool True if a parachute was triggered and the simulation should break. """ for parachute in overshootable_node.parachutes: # Calculate and save pressure signal ( noisy_pressure, height_above_ground_level, ) = self.__calculate_and_save_pressure_signals( parachute, overshootable_node.t, overshootable_node.y_sol[2], ) # Check for parachute trigger if not parachute.triggerfunc( noisy_pressure, height_above_ground_level, overshootable_node.y_sol, self.sensors, ): continue # Check next parachute # Remove parachute from flight parachutes self.parachutes.remove(parachute) # Create phase for time after detection and before inflation # Must only be created if parachute has any lag i = 1 if parachute.lag != 0: self.flight_phases.add_phase( overshootable_node.t, phase.derivative, clear=True, index=phase_index + i, ) i += 1 # Create flight phase for time after inflation callbacks = [ lambda self, parachute_cd_s=parachute.cd_s: setattr( self, "parachute_cd_s", parachute_cd_s ), lambda self, parachute_radius=parachute.radius: setattr( self, "parachute_radius", parachute_radius ), lambda self, parachute_height=parachute.height: setattr( self, "parachute_height", parachute_height ), lambda self, parachute_porosity=parachute.porosity: setattr( self, "parachute_porosity", parachute_porosity ), lambda self, added_mass_coefficient=parachute.added_mass_coefficient: ( setattr( self, "parachute_added_mass_coefficient", added_mass_coefficient, ) ), ] self.flight_phases.add_phase( overshootable_node.t + parachute.lag, self.u_dot_parachute, callbacks, clear=False, index=phase_index + i, ) # Rollback history self.t = overshootable_node.t self.y_sol = overshootable_node.y_sol self.solution[-1] = [overshootable_node.t, *overshootable_node.y_sol] # Prepare to leave loops and start new flight phase overshootable_nodes.flush_after(overshootable_index) phase.time_nodes.flush_after(node_index) phase.time_nodes.add_node(self.t, [], [], []) phase.solver.status = "finished" # Save parachute event self.parachute_events.append([self.t, parachute]) return True return False def __calculate_and_save_pressure_signals(self, parachute, t, z): """Gets noise and pressure signals and saves them in the parachute object given the current time and altitude. Parameters ---------- parachute : Parachute The parachute object to calculate signals for. t : float The current time in seconds. z : float The altitude above sea level in meters. Returns ------- tuple[float, float] The noisy pressure and height above ground level. """ # Calculate pressure and noise pressure = self.env.pressure.get_value_opt(z) noise = parachute.noise_function() noisy_pressure = pressure + noise # Stores in the parachute object parachute.clean_pressure_signal.append([t, pressure]) parachute.noise_signal.append([t, noise]) # Gets height above ground level considering noise height_above_ground_level = ( self.env.barometric_height.get_value_opt(noisy_pressure) - self.env.elevation ) return noisy_pressure, height_above_ground_level def __init_solution_monitors(self): # Initialize solution monitors self.out_of_rail_time = 0 self.out_of_rail_time_index = 0 self.out_of_rail_state = np.array([0]) self.apogee_state = np.array([0]) self.apogee = 0 self.apogee_time = 0 self.x_impact = 0 self.y_impact = 0 self.impact_velocity = 0 self.impact_state = np.array([0]) self.parachute_events = [] self.__post_processed_variables = [] def __init_flight_state(self): """Initialize flight state variables.""" if self.initial_solution is None: # Initialize time and state variables self.t_initial = 0 x_init, y_init, z_init = 0, 0, self.env.elevation vx_init, vy_init, vz_init = 0, 0, 0 w1_init, w2_init, w3_init = 0, 0, 0 # Initialize attitude # Precession / Heading Angle self.psi_init = np.radians(-self.heading) # Nutation / Attitude Angle self.theta_init = np.radians(self.inclination - 90) # Spin / Bank Angle self.phi_init = 0 # Consider Rail Buttons position, if there is rail buttons try: self.phi_init += ( self.rocket.rail_buttons[0].component.angular_position_rad if self.rocket._csys == 1 else 2 * np.pi - self.rocket.rail_buttons[0].component.angular_position_rad ) except IndexError: pass # 3-1-3 Euler Angles to Euler Parameters e0_init, e1_init, e2_init, e3_init = euler313_to_quaternions( self.phi_init, self.theta_init, self.psi_init ) # Store initial conditions self.initial_solution = [ self.t_initial, x_init, y_init, z_init, vx_init, vy_init, vz_init, e0_init, e1_init, e2_init, e3_init, w1_init, w2_init, w3_init, ] # Set initial derivative for rail phase self.initial_derivative = self.udot_rail1 elif isinstance(self.initial_solution, Flight): # Initialize time and state variables based on last solution of # previous flight self.initial_solution = self.initial_solution.solution[-1] # Set unused monitors self.out_of_rail_state = self.initial_solution[1:] self.out_of_rail_time = self.initial_solution[0] self.out_of_rail_time_index = 0 # Set initial derivative for 6-DOF flight phase self.initial_derivative = self.u_dot_generalized else: # Initial solution given, ignore rail phase # TODO: Check if rocket is actually out of rail. Otherwise, start at rail self.out_of_rail_state = self.initial_solution[1:] self.out_of_rail_time = self.initial_solution[0] self.out_of_rail_time_index = 0 self.t_initial = self.initial_solution[0] self.initial_derivative = self.u_dot_generalized if self._controllers or self.sensors: # Handle post process during simulation, get initial accel/forces self.initial_derivative( self.t_initial, self.initial_solution[1:], post_processing=True ) def __init_solver_monitors(self): # Initialize solver monitors self.function_evaluations = [] # Initialize solution state self.solution = [] self.__init_flight_state() self.t_initial = self.initial_solution[0] self.solution.append(self.initial_solution) self.t = self.solution[-1][0] self.y_sol = self.solution[-1][1:] self.__set_ode_solver(self.ode_solver) def __init_equations_of_motion(self): """Initialize equations of motion.""" # Determine if a point-mass model is used. is_point_mass = isinstance(self.rocket, PointMassRocket) or ( hasattr(self.rocket, "motor") and isinstance(self.rocket.motor, PointMassMotor) ) # Set simulation mode based on model type. if is_point_mass: if self.simulation_mode != "3 DOF": warnings.warn( "A point-mass model was detected. Simulation mode should be '3 DOF'.", UserWarning, ) self.simulation_mode = "3 DOF" # Set the equations of motion based on the final simulation mode. if self.simulation_mode == "3 DOF": self.u_dot_generalized = self.u_dot_generalized_3dof elif self.simulation_mode == "6 DOF": self.u_dot_generalized = ( self.u_dot if self.equations_of_motion == "solid_propulsion" else self.u_dot_generalized ) else: raise ValueError( f"Invalid simulation_mode: {self.simulation_mode}. " "Must be '3 DOF' or '6 DOF'." ) def __init_controllers(self): """Initialize controllers and sensors""" self._controllers = self.rocket._controllers[:] self.sensors = self.rocket.sensors.get_components() # reset controllable object to initial state (only airbrakes for now) for air_brakes in self.rocket.air_brakes: air_brakes._reset() self.sensor_data = {} for sensor in self.sensors: sensor._reset(self.rocket) # resets noise and measurement list self.sensor_data[sensor] = [] def __cache_sensor_data(self): """Cache sensor data for simulations with sensors.""" sensor_data = {} sensors = [] for sensor in self.sensors: # skip sensors that are used more then once in the rocket if sensor not in sensors: sensors.append(sensor) sensor_data[sensor] = sensor.measured_data[:] self.sensor_data = sensor_data def __set_ode_solver(self, solver): """Sets the ODE solver to be used in the simulation. Parameters ---------- solver : str, ``scipy.integrate.OdeSolver`` Integration method to use to solve the equations of motion ODE, or a custom ``scipy.integrate.OdeSolver``. """ if isinstance(solver, OdeSolver): self._solver = solver else: try: self._solver = ODE_SOLVER_MAP[solver] except KeyError as e: # pragma: no cover raise ValueError( f"Invalid ``ode_solver`` input: {solver}. " f"Available options are: {', '.join(ODE_SOLVER_MAP.keys())}" ) from e self.__is_lsoda = issubclass(self._solver, LSODA) @cached_property def effective_1rl(self): """Original rail length minus the distance measured from nozzle exit to the upper rail button. It assumes the nozzle to be aligned with the beginning of the rail.""" nozzle = self.rocket.nozzle_position try: rail_buttons = self.rocket.rail_buttons[0] upper_r_button = ( rail_buttons.component.buttons_distance * self.rocket._csys + rail_buttons.position.z ) except IndexError: # No rail buttons defined upper_r_button = nozzle effective_1rl = self.rail_length - abs(nozzle - upper_r_button) return effective_1rl @cached_property def effective_2rl(self): """Original rail length minus the distance measured from nozzle exit to the lower rail button. It assumes the nozzle to be aligned with the beginning of the rail.""" nozzle = self.rocket.nozzle_position try: rail_buttons = self.rocket.rail_buttons[0] lower_r_button = rail_buttons.position.z except IndexError: # No rail buttons defined lower_r_button = nozzle effective_2rl = self.rail_length - abs(nozzle - lower_r_button) return effective_2rl @cached_property def frontal_surface_wind(self): """Frontal wind velocity at the surface level. The frontal wind is defined as the wind blowing in the direction of the rocket's heading. Returns ------- float Wind velocity in the frontal direction at the surface level. """ wind_u = self.env.wind_velocity_x.get_value_opt(self.env.elevation) wind_v = self.env.wind_velocity_y.get_value_opt(self.env.elevation) heading_rad = self.heading * np.pi / 180 return wind_u * np.sin(heading_rad) + wind_v * np.cos(heading_rad) @cached_property def lateral_surface_wind(self): """Lateral wind velocity at the surface level. The lateral wind is defined as the wind blowing perpendicular to the rocket's heading. Returns ------- float Wind velocity in the lateral direction at the surface level. """ wind_u = self.env.wind_velocity_x.get_value_opt(self.env.elevation) wind_v = self.env.wind_velocity_y.get_value_opt(self.env.elevation) heading_rad = self.heading * np.pi / 180 return -wind_u * np.cos(heading_rad) + wind_v * np.sin(heading_rad) def __compute_drag_7d_inputs( self, stream_velocity_body, stream_speed, stream_mach, density, dynamic_viscosity, ): """Build drag-model inputs in the 7D order used by Rocket drag functions.""" aerodynamic_stream_velocity = -stream_velocity_body alpha = np.arctan2( aerodynamic_stream_velocity[1], aerodynamic_stream_velocity[2] ) beta = np.arctan2( aerodynamic_stream_velocity[0], aerodynamic_stream_velocity[2] ) reynolds = ( density * stream_speed * (2 * self.rocket.radius) / dynamic_viscosity if dynamic_viscosity > 0 else 0 ) return alpha, beta, stream_mach, reynolds
[docs] def udot_rail1(self, t, u, post_processing=False): """Calculates derivative of u state vector with respect to time when rocket is flying in 1 DOF motion in the rail. 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]. 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 : list State vector defined by u_dot = [vx, vy, vz, ax, ay, az, e0dot, e1dot, e2dot, e3dot, alpha1, alpha2, alpha3]. """ # Retrieve integration data _, _, z, vx, vy, vz, e0, e1, e2, e3, _, _, _ = u # Retrieve important quantities # Mass total_mass_at_t = self.rocket.total_mass.get_value_opt(t) # Get freestream speed free_stream_velocity = Vector( [ self.env.wind_velocity_x.get_value_opt(z) - vx, self.env.wind_velocity_y.get_value_opt(z) - vy, -vz, ] ) free_stream_speed = abs(free_stream_velocity) free_stream_mach = free_stream_speed / self.env.speed_of_sound.get_value_opt(z) rho = self.env.density.get_value_opt(z) stream_velocity_body = ( Matrix.transformation([e0, e1, e2, e3]).transpose @ free_stream_velocity ) dynamic_viscosity = self.env.dynamic_viscosity.get_value_opt(z) alpha, beta, mach, reynolds = self.__compute_drag_7d_inputs( stream_velocity_body, free_stream_speed, free_stream_mach, rho, dynamic_viscosity, ) drag_coeff = self.rocket.power_on_drag_7d(alpha, beta, mach, reynolds, 0, 0, 0) # Calculate Forces pressure = self.env.pressure.get_value_opt(z) net_thrust = max( self.rocket.motor.thrust.get_value_opt(t) + self.rocket.motor.pressure_thrust(pressure), 0, ) R3 = -0.5 * rho * (free_stream_speed**2) * self.rocket.area * (drag_coeff) # Calculate Linear acceleration a3 = (R3 + net_thrust) / total_mass_at_t - ( e0**2 - e1**2 - e2**2 + e3**2 ) * self.env.gravity.get_value_opt(z) if a3 > 0: ax = 2 * (e1 * e3 + e0 * e2) * a3 ay = 2 * (e2 * e3 - e0 * e1) * a3 az = (1 - 2 * (e1**2 + e2**2)) * a3 else: ax, ay, az = 0, 0, 0 if post_processing: # Use u_dot post processing code for forces, moments and env data self.u_dot_generalized(t, u, post_processing=True) # Save feasible accelerations self.__post_processed_variables[-1][1:7] = [ax, ay, az, 0, 0, 0] return [vx, vy, vz, ax, ay, az, 0, 0, 0, 0, 0, 0, 0]
[docs] def udot_rail2(self, t, u, post_processing=False): # pragma: no cover """[Still not implemented] Calculates derivative of u state vector with respect to time when rocket is flying in 3 DOF motion in the rail. 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]. post_processing : bool, optional If True, adds flight data information directly to self variables such as self.angle_of_attack, by default False Returns ------- u_dot : list State vector defined by u_dot = [vx, vy, vz, ax, ay, az, e0dot, e1dot, e2dot, e3dot, alpha1, alpha2, alpha3]. """ # Hey! We will finish this function later, now we just can use u_dot return self.u_dot_generalized(t, u, post_processing=post_processing)
[docs] def u_dot(self, t, u, post_processing=False): # pylint: disable=too-many-locals,too-many-statements """Calculates derivative of u state vector with respect to time when rocket is flying in 6 DOF motion during ascent out of rail and descent without parachute. 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]. post_processing : bool, optional If True, adds flight data information directly to self variables such as self.angle_of_attack, by default False Returns ------- u_dot : list State vector defined by u_dot = [vx, vy, vz, ax, ay, az, e0dot, e1dot, e2dot, e3dot, alpha1, alpha2, alpha3]. """ # Retrieve integration data _, _, z, vx, vy, vz, e0, e1, e2, e3, omega1, omega2, omega3 = u # Determine lift force and moment omega1, omega2, omega3 = 0, 0, 0 R1, R2, M1, M2, M3 = 0, 0, 0, 0, 0 # Thrust correction parameters pressure = self.env.pressure.get_value_opt(z) # Determine current behavior if self.rocket.motor.burn_start_time < t < self.rocket.motor.burn_out_time: # Motor burning # Retrieve important motor quantities # Inertias motor_I_33_at_t = self.rocket.motor.I_33.get_value_opt(t) motor_I_11_at_t = self.rocket.motor.I_11.get_value_opt(t) motor_I_33_derivative_at_t = self.rocket.motor.I_33.differentiate( t, dx=1e-6 ) motor_I_11_derivative_at_t = self.rocket.motor.I_11.differentiate( t, dx=1e-6 ) # Mass mass_flow_rate_at_t = self.rocket.motor.mass_flow_rate.get_value_opt(t) propellant_mass_at_t = self.rocket.motor.propellant_mass.get_value_opt(t) # Thrust net_thrust = max( self.rocket.motor.thrust.get_value_opt(t) + self.rocket.motor.pressure_thrust(pressure), 0, ) # Off center moment M1 += self.rocket.thrust_eccentricity_y * net_thrust M2 -= self.rocket.thrust_eccentricity_x * net_thrust else: # Motor stopped # Inertias ( motor_I_33_at_t, motor_I_11_at_t, motor_I_33_derivative_at_t, motor_I_11_derivative_at_t, ) = (0, 0, 0, 0) # Mass mass_flow_rate_at_t, propellant_mass_at_t = 0, 0 # thrust net_thrust = 0 # Retrieve important quantities # Inertias rocket_dry_I_33 = self.rocket.dry_I_33 rocket_dry_I_11 = self.rocket.dry_I_11 # Mass rocket_dry_mass = self.rocket.dry_mass # already with motor's dry mass total_mass_at_t = propellant_mass_at_t + rocket_dry_mass mu = (propellant_mass_at_t * rocket_dry_mass) / ( propellant_mass_at_t + rocket_dry_mass ) # Geometry # b = -self.rocket.distance_rocket_propellant b = ( -( self.rocket.center_of_propellant_position.get_value_opt(0) - self.rocket.center_of_dry_mass_position ) * self.rocket._csys ) c = self.rocket.nozzle_to_cdm nozzle_radius = self.rocket.motor.nozzle_radius # Prepare transformation matrix a11 = 1 - 2 * (e2**2 + e3**2) a12 = 2 * (e1 * e2 - e0 * e3) a13 = 2 * (e1 * e3 + e0 * e2) a21 = 2 * (e1 * e2 + e0 * e3) a22 = 1 - 2 * (e1**2 + e3**2) a23 = 2 * (e2 * e3 - e0 * e1) a31 = 2 * (e1 * e3 - e0 * e2) a32 = 2 * (e2 * e3 + e0 * e1) a33 = 1 - 2 * (e1**2 + e2**2) # Transformation matrix: (123) -> (XYZ) K = Matrix([[a11, a12, a13], [a21, a22, a23], [a31, a32, a33]]) Kt = K.transpose # Calculate Forces and Moments # Get freestream speed wind_velocity_x = self.env.wind_velocity_x.get_value_opt(z) wind_velocity_y = self.env.wind_velocity_y.get_value_opt(z) speed_of_sound = self.env.speed_of_sound.get_value_opt(z) free_stream_velocity = Vector([wind_velocity_x - vx, wind_velocity_y - vy, -vz]) free_stream_speed = abs(free_stream_velocity) free_stream_mach = free_stream_speed / speed_of_sound stream_velocity_body = Kt @ free_stream_velocity # Determine aerodynamics forces # Determine Drag Force rho = self.env.density.get_value_opt(z) dynamic_viscosity = self.env.dynamic_viscosity.get_value_opt(z) alpha, beta, mach, reynolds = self.__compute_drag_7d_inputs( stream_velocity_body, free_stream_speed, free_stream_mach, rho, dynamic_viscosity, ) if t < self.rocket.motor.burn_out_time: drag_coeff = self.rocket.power_on_drag_7d( alpha, beta, mach, reynolds, omega1, omega2, omega3, ) else: drag_coeff = self.rocket.power_off_drag_7d( alpha, beta, mach, reynolds, omega1, omega2, omega3, ) R3 = -0.5 * rho * (free_stream_speed**2) * self.rocket.area * drag_coeff for air_brakes in self.rocket.air_brakes: if air_brakes.deployment_level > 0: air_brakes_cd = air_brakes.drag_coefficient.get_value_opt( air_brakes.deployment_level, free_stream_mach ) air_brakes_force = ( -0.5 * rho * (free_stream_speed**2) * air_brakes.reference_area * air_brakes_cd ) if air_brakes.override_rocket_drag: R3 = air_brakes_force # Substitutes rocket drag coefficient else: R3 += air_brakes_force # Off center moment M1 += self.rocket.cp_eccentricity_y * R3 M2 -= self.rocket.cp_eccentricity_x * R3 # Get rocket velocity in body frame vx_b = a11 * vx + a21 * vy + a31 * vz vy_b = a12 * vx + a22 * vy + a32 * vz vz_b = a13 * vx + a23 * vy + a33 * vz # Calculate lift and moment for each component of the rocket velocity_in_body_frame = Vector([vx_b, vy_b, vz_b]) w = Vector([omega1, omega2, omega3]) for aero_surface, _ in self.rocket.aerodynamic_surfaces: # Component cp relative to CDM in body frame comp_cp = self.rocket.surfaces_cp_to_cdm[aero_surface] # Component absolute velocity in body frame comp_vb = velocity_in_body_frame + (w ^ comp_cp) # Wind velocity at component altitude comp_z = z + (K @ comp_cp).z comp_wind_vx = self.env.wind_velocity_x.get_value_opt(comp_z) comp_wind_vy = self.env.wind_velocity_y.get_value_opt(comp_z) # Component freestream velocity in body frame comp_wind_vb = Kt @ Vector([comp_wind_vx, comp_wind_vy, 0]) comp_stream_velocity = comp_wind_vb - comp_vb comp_stream_speed = abs(comp_stream_velocity) comp_stream_mach = comp_stream_speed / speed_of_sound # Reynolds at component altitude # TODO: Reynolds is only used in generic surfaces. This calculation # should be moved to the surface class for efficiency comp_density = self.env.density.get_value_opt(comp_z) comp_dynamic_viscosity = self.env.dynamic_viscosity.get_value_opt(comp_z) comp_reynolds = ( comp_density * comp_stream_speed * aero_surface.reference_length / comp_dynamic_viscosity ) # Forces and moments X, Y, Z, M, N, L = aero_surface.compute_forces_and_moments( comp_stream_velocity, comp_stream_speed, comp_stream_mach, rho, comp_cp, w, comp_reynolds, ) R1 += X R2 += Y R3 += Z M1 += M M2 += N M3 += L # Off center moment M3 += self.rocket.cp_eccentricity_x * R2 - self.rocket.cp_eccentricity_y * R1 # Calculate derivatives # Angular acceleration alpha1 = ( M1 - ( omega2 * omega3 * ( rocket_dry_I_33 + motor_I_33_at_t - rocket_dry_I_11 - motor_I_11_at_t - mu * b**2 ) + omega1 * ( ( motor_I_11_derivative_at_t + mass_flow_rate_at_t * (rocket_dry_mass - 1) * (b / total_mass_at_t) ** 2 ) - mass_flow_rate_at_t * ((nozzle_radius / 2) ** 2 + (c - b * mu / rocket_dry_mass) ** 2) ) ) ) / (rocket_dry_I_11 + motor_I_11_at_t + mu * b**2) alpha2 = ( M2 - ( omega1 * omega3 * ( rocket_dry_I_11 + motor_I_11_at_t + mu * b**2 - rocket_dry_I_33 - motor_I_33_at_t ) + omega2 * ( ( motor_I_11_derivative_at_t + mass_flow_rate_at_t * (rocket_dry_mass - 1) * (b / total_mass_at_t) ** 2 ) - mass_flow_rate_at_t * ((nozzle_radius / 2) ** 2 + (c - b * mu / rocket_dry_mass) ** 2) ) ) ) / (rocket_dry_I_11 + motor_I_11_at_t + mu * b**2) alpha3 = ( M3 - omega3 * ( motor_I_33_derivative_at_t - mass_flow_rate_at_t * (nozzle_radius**2) / 2 ) ) / (rocket_dry_I_33 + motor_I_33_at_t) # Euler parameters derivative e0dot = 0.5 * (-omega1 * e1 - omega2 * e2 - omega3 * e3) e1dot = 0.5 * (omega1 * e0 + omega3 * e2 - omega2 * e3) e2dot = 0.5 * (omega2 * e0 - omega3 * e1 + omega1 * e3) e3dot = 0.5 * (omega3 * e0 + omega2 * e1 - omega1 * e2) # Linear acceleration L = [ ( R1 - b * propellant_mass_at_t * (omega2**2 + omega3**2) - 2 * c * mass_flow_rate_at_t * omega2 ) / total_mass_at_t, ( R2 + b * propellant_mass_at_t * (alpha3 + omega1 * omega2) + 2 * c * mass_flow_rate_at_t * omega1 ) / total_mass_at_t, (R3 - b * propellant_mass_at_t * (alpha2 - omega1 * omega3) + net_thrust) / total_mass_at_t, ] ax, ay, az = K @ Vector(L) az -= self.env.gravity.get_value_opt(z) # Include gravity # Coriolis acceleration _, w_earth_y, w_earth_z = self.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) # Create u_dot u_dot = [ vx, vy, vz, ax, ay, az, e0dot, e1dot, e2dot, e3dot, alpha1, alpha2, alpha3, ] if post_processing: self.__post_processed_variables.append( [ t, ax, ay, az, alpha1, alpha2, alpha3, R1, R2, R3, M1, M2, M3, net_thrust, ] ) return u_dot
[docs] def u_dot_generalized_3dof(self, t, u, post_processing=False): """Calculates derivative of u state vector with respect to time when the rocket is flying in 3 DOF motion in space and significant mass variation effects exist. Includes a weathercocking model that evolves the body axis direction toward the relative wind direction. Parameters ---------- t : float Time in seconds. u : list State vector: [x, y, z, vx, vy, vz, e0, e1, e2, e3, omega1, omega2, omega3]. post_processing : bool, optional If True, adds flight data to self variables like self.angle_of_attack. Returns ------- list Derivative state vector: [vx, vy, vz, ax, ay, az, e0_dot, e1_dot, e2_dot, e3_dot, alpha1, alpha2, alpha3]. """ # Unpack state _, _, z, vx, vy, vz, e0, e1, e2, e3, omega1, omega2, omega3 = u # Define vectors v = Vector([vx, vy, vz]) e = [e0, e1, e2, e3] w = Vector([omega1, omega2, omega3]) # Mass and transformation total_mass = self.rocket.total_mass.get_value_opt(t) K = Matrix.transformation(e) Kt = K.transpose # Atmospheric and wind data rho = self.env.density.get_value_opt(z) wind_vx = self.env.wind_velocity_x.get_value_opt(z) wind_vy = self.env.wind_velocity_y.get_value_opt(z) wind_velocity = Vector([wind_vx, wind_vy, 0]) free_stream_velocity = wind_velocity - v free_stream_speed = abs(free_stream_velocity) speed_of_sound = self.env.speed_of_sound.get_value_opt(z) mach = free_stream_speed / speed_of_sound stream_velocity_body = Kt @ free_stream_velocity dynamic_viscosity = self.env.dynamic_viscosity.get_value_opt(z) alpha, beta, mach, reynolds = self.__compute_drag_7d_inputs( stream_velocity_body, free_stream_speed, mach, rho, dynamic_viscosity, ) # Drag computation if t < self.rocket.motor.burn_out_time: cd = self.rocket.power_on_drag_7d( alpha, beta, mach, reynolds, omega1, omega2, omega3 ) else: cd = self.rocket.power_off_drag_7d( alpha, beta, mach, reynolds, omega1, omega2, omega3 ) R1, R2 = 0, 0 R3 = -0.5 * rho * free_stream_speed**2 * self.rocket.area * cd for air_brake in self.rocket.air_brakes: if air_brake.deployment_level > 0: ab_cd = air_brake.drag_coefficient.get_value_opt( air_brake.deployment_level, mach ) ab_force = ( -0.5 * rho * free_stream_speed**2 * air_brake.reference_area * ab_cd ) if air_brake.override_rocket_drag: R3 = ab_force else: R3 += ab_force # Velocity in body frame vb_body = Kt @ v for surface, _ in self.rocket.aerodynamic_surfaces: cp = self.rocket.surfaces_cp_to_cdm[surface] vb_component = vb_body + (w ^ cp) comp_z = z + (K @ cp).z wind_cx = self.env.wind_velocity_x.get_value_opt(comp_z) wind_cy = self.env.wind_velocity_y.get_value_opt(comp_z) wind_body = Kt @ Vector([wind_cx, wind_cy, 0]) rel_velocity = wind_body - vb_component rel_speed = abs(rel_velocity) rel_mach = rel_speed / speed_of_sound comp_density = self.env.density.get_value_opt(comp_z) comp_dynamic_viscosity = self.env.dynamic_viscosity.get_value_opt(comp_z) reynolds = ( comp_density * rel_speed * surface.reference_length / comp_dynamic_viscosity ) fx, fy, fz, *_ = surface.compute_forces_and_moments( rel_velocity, rel_speed, rel_mach, rho, cp, w, reynolds ) R1 += fx R2 += fy R3 += fz # Thrust and weight # Calculate net thrust including pressure thrust correction if motor is burning if self.rocket.motor.burn_start_time < t < self.rocket.motor.burn_out_time: pressure = self.env.pressure.get_value_opt(z) net_thrust = max( self.rocket.motor.thrust.get_value_opt(t) + self.rocket.motor.pressure_thrust(pressure), 0, ) else: net_thrust = 0 gravity = self.env.gravity.get_value_opt(z) weight_body = Kt @ Vector([0, 0, -total_mass * gravity]) total_force = Vector([0, 0, net_thrust]) + weight_body + Vector([R1, R2, R3]) # Dynamics v_dot = K @ (total_force / total_mass) r_dot = [vx, vy, vz] # Weathercocking: evolve body axis direction toward relative wind # The body z-axis (attitude vector) should align with -freestream_velocity weathercock_coeff = getattr(self.rocket, "weathercock_coeff", 0.0) if weathercock_coeff > 0 and free_stream_speed > 1e-6: # Current body z-axis in inertial frame (attitude vector) # From rotation matrix: column 3 gives the body z-axis in inertial frame body_z_inertial = Vector( [ 2 * (e1 * e3 + e0 * e2), 2 * (e2 * e3 - e0 * e1), 1 - 2 * (e1**2 + e2**2), ] ) # Desired direction: opposite of freestream velocity (into the wind) # This is the direction the rocket nose should point # Division by free_stream_speed ensures the result is a unit vector desired_direction = -free_stream_velocity / free_stream_speed # Compute rotation axis (cross product of current and desired) rotation_axis = body_z_inertial ^ desired_direction rotation_axis_mag = abs(rotation_axis) # Determine omega_body based on alignment state omega_body = None if rotation_axis_mag > 1e-8: # Normal case: compute angular velocity from misalignment rotation_axis = rotation_axis / rotation_axis_mag # The magnitude of the cross product of two unit vectors equals # the sine of the angle between them sin_angle = min(1.0, max(-1.0, rotation_axis_mag)) # Angular velocity magnitude proportional to misalignment angle omega_mag = weathercock_coeff * sin_angle # Angular velocity in inertial frame, then transform to body frame omega_body = Kt @ (rotation_axis * omega_mag) else: # Check if aligned or anti-aligned using dot product dot = body_z_inertial @ desired_direction if dot < -0.999: # Anti-aligned # Choose an arbitrary perpendicular axis x_axis = Vector([1.0, 0.0, 0.0]) perp_axis = body_z_inertial ^ x_axis if abs(perp_axis) < 1e-6: y_axis = Vector([0.0, 1.0, 0.0]) perp_axis = body_z_inertial ^ y_axis if abs(perp_axis) < 1e-6: raise ValueError( "Cannot determine a valid rotation axis: " "body_z_inertial is parallel to both x and y axes." ) rotation_axis = perp_axis.unit_vector # 180 degree rotation: sin(angle) = 1 omega_mag = weathercock_coeff * 1.0 omega_body = Kt @ (rotation_axis * omega_mag) # else: aligned (dot > 0.999) - no rotation needed, omega_body stays None # Compute quaternion derivatives from omega_body if omega_body is not None: omega1_wc, omega2_wc, omega3_wc = ( omega_body.x, omega_body.y, omega_body.z, ) e0_dot = 0.5 * (-omega1_wc * e1 - omega2_wc * e2 - omega3_wc * e3) e1_dot = 0.5 * (omega1_wc * e0 + omega3_wc * e2 - omega2_wc * e3) e2_dot = 0.5 * (omega2_wc * e0 - omega3_wc * e1 + omega1_wc * e3) e3_dot = 0.5 * (omega3_wc * e0 + omega2_wc * e1 - omega1_wc * e2) e_dot = [e0_dot, e1_dot, e2_dot, e3_dot] else: e_dot = [0, 0, 0, 0] w_dot = [0, 0, 0] # No angular acceleration in 3DOF model else: # No weathercocking or negligible freestream speed e_dot = [0, 0, 0, 0] w_dot = [0, 0, 0] u_dot = [*r_dot, *v_dot, *e_dot, *w_dot] if post_processing: self.__post_processed_variables.append( [t, *v_dot, *w_dot, R1, R2, R3, 0, 0, 0, net_thrust] ) return u_dot
[docs] def u_dot_generalized(self, t, u, post_processing=False): # pylint: disable=too-many-locals,too-many-statements """Calculates derivative of u state vector with respect to time when the rocket is flying in 6 DOF motion in space and significant mass variation effects exist. Typical flight phases include powered ascent after launch rail. Parameters ---------- t : float Time in seconds u : list State vector defined by u = [x, y, z, vx, vy, vz, q0, q1, q2, q3, omega1, omega2, omega3]. post_processing : bool, optional If True, adds flight data information directly to self variables such as self.angle_of_attack, by default False. Returns ------- u_dot : list State vector defined by u_dot = [vx, vy, vz, ax, ay, az, e0_dot, e1_dot, e2_dot, e3_dot, alpha1, alpha2, alpha3]. """ # Retrieve integration data _, _, z, vx, vy, vz, e0, e1, e2, e3, omega1, omega2, omega3 = u # Create necessary vectors # r = Vector([x, y, z]) # CDM position vector v = Vector([vx, vy, vz]) # CDM velocity vector e = [e0, e1, e2, e3] # Euler parameters/quaternions w = Vector([omega1, omega2, omega3]) # Angular velocity vector # Retrieve necessary quantities ## Rocket mass total_mass = self.rocket.total_mass.get_value_opt(t) total_mass_dot = self.rocket.total_mass_flow_rate.get_value_opt(t) total_mass_ddot = self.rocket.total_mass_flow_rate.differentiate_complex_step(t) ## CM position vector and time derivatives relative to CDM in body frame r_CM_z = self.rocket.com_to_cdm_function r_CM_t = r_CM_z.get_value_opt(t) r_CM = Vector([0, 0, r_CM_t]) r_CM_dot = Vector([0, 0, r_CM_z.differentiate_complex_step(t)]) r_CM_ddot = Vector([0, 0, r_CM_z.differentiate(t, order=2)]) ## Nozzle position vector r_NOZ = Vector([0, 0, self.rocket.nozzle_to_cdm]) ## Nozzle gyration tensor S_nozzle = self.rocket.nozzle_gyration_tensor ## Inertia tensor inertia_tensor = self.rocket.get_inertia_tensor_at_time(t) ## Inertia tensor time derivative in the body frame I_dot = self.rocket.get_inertia_tensor_derivative_at_time(t) # Calculate the Inertia tensor relative to CM H = (r_CM.cross_matrix @ -r_CM.cross_matrix) * total_mass I_CM = inertia_tensor - H # Prepare transformation matrices K = Matrix.transformation(e) Kt = K.transpose # Compute aerodynamic forces and moments R1, R2, R3, M1, M2, M3 = 0, 0, 0, 0, 0, 0 ## Drag force rho = self.env.density.get_value_opt(z) wind_velocity_x = self.env.wind_velocity_x.get_value_opt(z) wind_velocity_y = self.env.wind_velocity_y.get_value_opt(z) wind_velocity = Vector([wind_velocity_x, wind_velocity_y, 0]) free_stream_velocity = wind_velocity - v free_stream_speed = abs(free_stream_velocity) speed_of_sound = self.env.speed_of_sound.get_value_opt(z) free_stream_mach = free_stream_speed / speed_of_sound stream_velocity_body = Kt @ free_stream_velocity dynamic_viscosity = self.env.dynamic_viscosity.get_value_opt(z) alpha, beta, mach, reynolds = self.__compute_drag_7d_inputs( stream_velocity_body, free_stream_speed, free_stream_mach, rho, dynamic_viscosity, ) if self.rocket.motor.burn_start_time < t < self.rocket.motor.burn_out_time: pressure = self.env.pressure.get_value_opt(z) net_thrust = max( self.rocket.motor.thrust.get_value_opt(t) + self.rocket.motor.pressure_thrust(pressure), 0, ) drag_coeff = self.rocket.power_on_drag_7d( alpha, beta, mach, reynolds, omega1, omega2, omega3, ) else: net_thrust = 0 drag_coeff = self.rocket.power_off_drag_7d( alpha, beta, mach, reynolds, omega1, omega2, omega3, ) R3 += -0.5 * rho * (free_stream_speed**2) * self.rocket.area * drag_coeff for air_brakes in self.rocket.air_brakes: if air_brakes.deployment_level > 0: air_brakes_cd = air_brakes.drag_coefficient.get_value_opt( air_brakes.deployment_level, free_stream_mach ) air_brakes_force = ( -0.5 * rho * (free_stream_speed**2) * air_brakes.reference_area * air_brakes_cd ) if air_brakes.override_rocket_drag: R3 = air_brakes_force # Substitutes rocket drag coefficient else: R3 += air_brakes_force # Get rocket velocity in body frame velocity_in_body_frame = Kt @ v # Calculate lift and moment for each component of the rocket for aero_surface, _ in self.rocket.aerodynamic_surfaces: # Component cp relative to CDM in body frame comp_cp = self.rocket.surfaces_cp_to_cdm[aero_surface] # Component absolute velocity in body frame comp_vb = velocity_in_body_frame + (w ^ comp_cp) # Wind velocity at component altitude comp_z = z + (K @ comp_cp).z comp_wind_vx = self.env.wind_velocity_x.get_value_opt(comp_z) comp_wind_vy = self.env.wind_velocity_y.get_value_opt(comp_z) # Component freestream velocity in body frame comp_wind_vb = Kt @ Vector([comp_wind_vx, comp_wind_vy, 0]) comp_stream_velocity = comp_wind_vb - comp_vb comp_stream_speed = abs(comp_stream_velocity) comp_stream_mach = comp_stream_speed / speed_of_sound # Reynolds at component altitude # TODO: Reynolds is only used in generic surfaces. This calculation # should be moved to the surface class for efficiency comp_density = self.env.density.get_value_opt(comp_z) comp_dynamic_viscosity = self.env.dynamic_viscosity.get_value_opt(comp_z) comp_reynolds = ( comp_density * comp_stream_speed * aero_surface.reference_length / comp_dynamic_viscosity ) # Forces and moments X, Y, Z, M, N, L = aero_surface.compute_forces_and_moments( comp_stream_velocity, comp_stream_speed, comp_stream_mach, rho, comp_cp, w, comp_reynolds, ) R1 += X R2 += Y R3 += Z M1 += M M2 += N M3 += L # Off center moment M1 += ( self.rocket.cp_eccentricity_y * R3 + self.rocket.thrust_eccentricity_y * net_thrust ) M2 -= ( self.rocket.cp_eccentricity_x * R3 + self.rocket.thrust_eccentricity_x * net_thrust ) M3 += self.rocket.cp_eccentricity_x * R2 - self.rocket.cp_eccentricity_y * R1 weight_in_body_frame = Kt @ Vector( [0, 0, -total_mass * self.env.gravity.get_value_opt(z)] ) T00 = total_mass * r_CM T03 = 2 * total_mass_dot * (r_NOZ - r_CM) - 2 * total_mass * r_CM_dot T04 = ( Vector([0, 0, net_thrust]) - total_mass * r_CM_ddot - 2 * total_mass_dot * r_CM_dot + total_mass_ddot * (r_NOZ - r_CM) ) T05 = total_mass_dot * S_nozzle - I_dot T20 = ( ((w ^ T00) ^ w) + (w ^ T03) + T04 + weight_in_body_frame + Vector([R1, R2, R3]) ) T21 = ( ((inertia_tensor @ w) ^ w) + T05 @ w - (weight_in_body_frame ^ r_CM) + Vector([M1, M2, M3]) ) # Angular velocity derivative w_dot = I_CM.inverse @ (T21 + (T20 ^ r_CM)) # Euler parameters derivative e_dot = [ 0.5 * (-omega1 * e1 - omega2 * e2 - omega3 * e3), 0.5 * (omega1 * e0 + omega3 * e2 - omega2 * e3), 0.5 * (omega2 * e0 - omega3 * e1 + omega1 * e3), 0.5 * (omega3 * e0 + omega2 * e1 - omega1 * e2), ] # Velocity vector derivative + Coriolis acceleration w_earth = Vector(self.env.earth_rotation_vector) v_dot = K @ (T20 / total_mass - (r_CM ^ w_dot)) - 2 * (w_earth ^ v) # Position vector derivative r_dot = [vx, vy, vz] # Create u_dot u_dot = [*r_dot, *v_dot, *e_dot, *w_dot] if post_processing: self.__post_processed_variables.append( [t, *v_dot, *w_dot, R1, R2, R3, M1, M2, M3, net_thrust] ) return u_dot
[docs] def u_dot_parachute(self, t, u, post_processing=False): """Calculates derivative of u state vector with respect to time when rocket is flying under parachute. A 3 DOF approximation is used. 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]. 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 : list State vector defined by u_dot = [vx, vy, vz, ax, ay, az, e0dot, e1dot, e2dot, e3dot, alpha1, alpha2, alpha3]. """ # Get relevant state data z, vx, vy, vz = u[2:6] # Get atmospheric data rho = self.env.density.get_value_opt(z) wind_velocity_x = self.env.wind_velocity_x.get_value_opt(z) wind_velocity_y = self.env.wind_velocity_y.get_value_opt(z) # Get the mass of the rocket mp = self.rocket.dry_mass # to = 1.2 # eta = 1 # Rdot = (6 * R * (1 - eta) / (1.2**6)) * ( # (1 - eta) * t**5 + eta * (to**3) * (t**2) # ) # Rdot = 0 # tf = 8 * nominal diameter / velocity at line stretch # Calculate added mass ma = ( self.parachute_added_mass_coefficient * rho * (2 / 3) * np.pi * self.parachute_radius**2 * self.parachute_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.parachute_cd_s * free_stream_speed # pseudo_drag = pseudo_drag - ka * rho * 4 * np.pi * (R**2) * Rdot Dx = pseudo_drag * freestream_x # add eta efficiency for wake Dy = pseudo_drag * freestream_y Dz = pseudo_drag * freestream_z ax = Dx / (mp + ma) ay = Dy / (mp + ma) az = (Dz - mp * self.env.gravity.get_value_opt(z)) / (mp + ma) # Add coriolis acceleration _, w_earth_y, w_earth_z = self.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) if post_processing: self.__post_processed_variables.append( [t, ax, ay, az, 0, 0, 0, Dx, Dy, Dz, 0, 0, 0, 0] ) return [vx, vy, vz, ax, ay, az, 0, 0, 0, 0, 0, 0, 0]
@cached_property def solution_array(self): """Returns solution array of the rocket flight.""" return np.array(self.solution) @property def function_evaluations_per_time_step(self): """Get the number of function evaluations per time step. This method calculates the difference between consecutive function evaluations during numerical integration and returns it as a list. Returns ------- list The list of differences in function evaluations per time step. """ return np.diff(self.function_evaluations).tolist() @cached_property def time(self): """Returns time array from solution.""" return self.solution_array[:, 0] @cached_property def time_steps(self): """Returns time step array.""" return np.diff(self.time)
[docs] def get_solution_at_time(self, t, atol=1e-3): """Returns the solution state vector at a given time. If the time is not found in the solution, the closest time is used and a warning is raised. Parameters ---------- t : float Time in seconds. atol : float, optional Absolute tolerance for time comparison. Default is 1e-3. If the difference between the time and the closest time in the solution is greater than this value, a warning is raised. Returns ------- solution : np.array Solution state at time t. The array follows the format of the solution array, with the first column being time like this: [t, x, y, z, vx, vy, vz, e0, e1, e2, e3, omega1, omega2, omega3]. """ time_index = find_closest(self.time, t) if abs(self.time[time_index] - t) > atol: warnings.warn( f"Time {t} not found in solution. Closest time is " f"{self.time[time_index]}. Using closest time.", UserWarning, ) return self.solution_array[time_index, :]
# Process first type of outputs - state vector # Transform solution array into Functions @funcify_method("Time (s)", "X (m)", "spline", "constant") def x(self): """Rocket x position relative to the launch pad as a Function of time.""" return self.solution_array[:, [0, 1]] @funcify_method("Time (s)", "Y (m)", "spline", "constant") def y(self): """Rocket y position relative to the launch pad as a Function of time.""" return self.solution_array[:, [0, 2]] @funcify_method("Time (s)", "Z (m)", "spline", "constant") def z(self): """Rocket z position relative to the launch pad as a Function of time.""" return self.solution_array[:, [0, 3]] @funcify_method("Time (s)", "Altitude AGL (m)", "spline", "constant") def altitude(self): """Rocket altitude above ground level as a Function of time. Ground level is defined by the environment elevation.""" return self.z - self.env.elevation @funcify_method("Time (s)", "Vx (m/s)", "spline", "zero") def vx(self): """Velocity of the rocket's center of dry mass in the X (East) direction of the inertial frame as a function of time.""" return self.solution_array[:, [0, 4]] @funcify_method("Time (s)", "Vy (m/s)", "spline", "zero") def vy(self): """Velocity of the rocket's center of dry mass in the Y (North) direction of the inertial frame as a function of time.""" return self.solution_array[:, [0, 5]] @funcify_method("Time (s)", "Vz (m/s)", "spline", "zero") def vz(self): """Velocity of the rocket's center of dry mass in the Z (Up) direction of the inertial frame as a function of time.""" return self.solution_array[:, [0, 6]] @funcify_method("Time (s)", "e0", "spline", "constant") def e0(self): """Rocket quaternion e0 as a Function of time.""" return self.solution_array[:, [0, 7]] @funcify_method("Time (s)", "e1", "spline", "constant") def e1(self): """Rocket quaternion e1 as a Function of time.""" return self.solution_array[:, [0, 8]] @funcify_method("Time (s)", "e2", "spline", "constant") def e2(self): """Rocket quaternion e2 as a Function of time.""" return self.solution_array[:, [0, 9]] @funcify_method("Time (s)", "e3", "spline", "constant") def e3(self): """Rocket quaternion e3 as a Function of time.""" return self.solution_array[:, [0, 10]] @funcify_method("Time (s)", "ω1 (rad/s)", "spline", "zero") def w1(self): """Angular velocity of the rocket in the x direction of the rocket's body frame as a function of time, in rad/s. Sometimes referred to as pitch rate (q).""" return self.solution_array[:, [0, 11]] @funcify_method("Time (s)", "ω2 (rad/s)", "spline", "zero") def w2(self): """Angular velocity of the rocket in the y direction of the rocket's body frame as a function of time, in rad/s. Sometimes referred to as yaw rate (r).""" return self.solution_array[:, [0, 12]] @funcify_method("Time (s)", "ω3 (rad/s)", "spline", "zero") def w3(self): """Angular velocity of the rocket in the z direction of the rocket's body frame as a function of time, in rad/s. Sometimes referred to as roll rate (p).""" return self.solution_array[:, [0, 13]] # Process second type of outputs - accelerations components @funcify_method("Time (s)", "Ax (m/s²)", "spline", "zero") def ax(self): """Acceleration of the rocket's center of dry mass along the X (East) axis in the inertial frame as a function of time.""" return self.__evaluate_post_process[:, [0, 1]] @funcify_method("Time (s)", "Ay (m/s²)", "spline", "zero") def ay(self): """Acceleration of the rocket's center of dry mass along the Y (North) axis in the inertial frame as a function of time.""" return self.__evaluate_post_process[:, [0, 2]] @funcify_method("Time (s)", "Az (m/s²)", "spline", "zero") def az(self): """Acceleration of the rocket's center of dry mass along the Z (Up) axis in the inertial frame as a function of time.""" return self.__evaluate_post_process[:, [0, 3]] @funcify_method("Time (s)", "α1 (rad/s²)", "spline", "zero") def alpha1(self): """Angular acceleration of the rocket in the x direction of the rocket's body frame as a function of time, in rad/s. Sometimes referred to as pitch acceleration.""" return self.__evaluate_post_process[:, [0, 4]] @funcify_method("Time (s)", "α2 (rad/s²)", "spline", "zero") def alpha2(self): """Angular acceleration of the rocket in the y direction of the rocket's body frame as a function of time, in rad/s. Sometimes referred to as yaw acceleration.""" return self.__evaluate_post_process[:, [0, 5]] @funcify_method("Time (s)", "α3 (rad/s²)", "spline", "zero") def alpha3(self): """Angular acceleration of the rocket in the z direction of the rocket's body frame as a function of time, in rad/s. Sometimes referred to as roll acceleration.""" return self.__evaluate_post_process[:, [0, 6]] # Process third type of outputs - Temporary values @funcify_method("Time (s)", "R1 (N)", "spline", "zero") def R1(self): """Aerodynamic force acting along the x-axis of the rocket's body frame as a function of time. Expressed in Newtons (N).""" return self.__evaluate_post_process[:, [0, 7]] @funcify_method("Time (s)", "R2 (N)", "spline", "zero") def R2(self): """Aerodynamic force acting along the y-axis of the rocket's body frame as a function of time. Expressed in Newtons (N).""" return self.__evaluate_post_process[:, [0, 8]] @funcify_method("Time (s)", "R3 (N)", "spline", "zero") def R3(self): """Aerodynamic force acting along the z-axis of the rocket's body frame as a function of time. Expressed in Newtons (N).""" return self.__evaluate_post_process[:, [0, 9]] @funcify_method("Time (s)", "M1 (Nm)", "linear", "zero") def M1(self): """Aerodynamic moment acting along the x-axis of the rocket's body frame as a function of time. Expressed in Newtons (N).""" return self.__evaluate_post_process[:, [0, 10]] @funcify_method("Time (s)", "M2 (Nm)", "linear", "zero") def M2(self): """Aerodynamic moment acting along the y-axis of the rocket's body frame as a function of time. Expressed in Newtons (N).""" return self.__evaluate_post_process[:, [0, 11]] @funcify_method("Time (s)", "M3 (Nm)", "linear", "zero") def M3(self): """Aerodynamic moment acting along the z-axis of the rocket's body frame as a function of time. Expressed in Newtons (N).""" return self.__evaluate_post_process[:, [0, 12]] @funcify_method("Time (s)", "Net Thrust (N)", "linear", "zero") def net_thrust(self): """Net thrust of the rocket as a Function of time. This is the actual thrust force experienced by the rocket. It may be corrected with the atmospheric pressure if a reference pressure is defined.""" return self.__evaluate_post_process[:, [0, 13]] @funcify_method("Time (s)", "Pressure (Pa)", "spline", "constant") def pressure(self): """Air pressure felt by the rocket as a Function of time.""" return [(t, self.env.pressure.get_value_opt(z)) for t, z in self.z] @funcify_method("Time (s)", "Density (kg/m³)", "spline", "constant") def density(self): """Air density felt by the rocket as a Function of time.""" return [(t, self.env.density.get_value_opt(z)) for t, z in self.z] @funcify_method("Time (s)", "Dynamic Viscosity (Pa s)", "spline", "constant") def dynamic_viscosity(self): """Air dynamic viscosity felt by the rocket as a Function of time.""" return [(t, self.env.dynamic_viscosity.get_value_opt(z)) for t, z in self.z] @funcify_method("Time (s)", "Speed of Sound (m/s)", "spline", "constant") def speed_of_sound(self): """Speed of sound in the air felt by the rocket as a Function of time.""" return [(t, self.env.speed_of_sound.get_value_opt(z)) for t, z in self.z] @funcify_method("Time (s)", "Wind Velocity X (East) (m/s)", "spline", "constant") def wind_velocity_x(self): """Wind velocity in the X direction (east) as a Function of time.""" return [(t, self.env.wind_velocity_x.get_value_opt(z)) for t, z in self.z] @funcify_method("Time (s)", "Wind Velocity Y (North) (m/s)", "spline", "constant") def wind_velocity_y(self): """Wind velocity in the Y direction (north) as a Function of time.""" return [(t, self.env.wind_velocity_y.get_value_opt(z)) for t, z in self.z] # Process fourth type of output - values calculated from previous outputs # Frame conversions @cached_property def _stacked_velocity_body_frame(self): """Stacked velocity array at the center of dry mass in the body frame at each time step.""" Kt = self.direction_cosine_matrixes v = np.array( [ self.vx.y_array, self.vy.y_array, self.vz.y_array, ] ).transpose() stacked_velocity_body_frame = np.squeeze(np.matmul(Kt, v[:, :, np.newaxis])) return stacked_velocity_body_frame @funcify_method("Time (s)", "Vx Body Frame (m/s)", "spline", "zero") def vx_body_frame(self): """Velocity of the rocket's center of dry mass along the x axis of the body frame as a function of time.""" return np.array( [self.time, self._stacked_velocity_body_frame[:, 0]] ).transpose() @funcify_method("Time (s)", "Vy Body Frame (m/s)", "spline", "zero") def vy_body_frame(self): """Velocity of the rocket's center of dry mass along the y axis of the body frame as a function of time.""" return np.array( [self.time, self._stacked_velocity_body_frame[:, 1]] ).transpose() @funcify_method("Time (s)", "Vz Body Frame (m/s)", "spline", "zero") def vz_body_frame(self): """Velocity of the rocket's center of dry mass along the z axis of the body frame as a function of time.""" return np.array( [self.time, self._stacked_velocity_body_frame[:, 2]] ).transpose() @cached_property def _stacked_acceleration_body_frame(self): """Stacked acceleration array at the center of dry mass in the body frame at each time step.""" Kt = self.direction_cosine_matrixes a = np.array( [ self.ax.y_array, self.ay.y_array, self.az.y_array, ] ).transpose() stacked_acceleration_body_frame = np.squeeze(np.matmul(Kt, a[:, :, np.newaxis])) return stacked_acceleration_body_frame @funcify_method("Time (s)", "Ax Body Frame (m/s²)", "spline", "zero") def ax_body_frame(self): """Acceleration of the rocket's center of dry mass along the x axis of the body frame as a function of time.""" return np.array( [self.time, self._stacked_acceleration_body_frame[:, 0]] ).transpose() @funcify_method("Time (s)", "Ay Body Frame (m/s²)", "spline", "zero") def ay_body_frame(self): """Acceleration of the rocket's center of dry mass along the y axis of the body frame as a function of time.""" return np.array( [self.time, self._stacked_acceleration_body_frame[:, 1]] ).transpose() @funcify_method("Time (s)", "Az Body Frame (m/s²)", "spline", "zero") def az_body_frame(self): """Acceleration of the rocket's center of dry mass along the z axis of the body frame as a function of time.""" return np.array( [self.time, self._stacked_acceleration_body_frame[:, 2]] ).transpose() # Kinematics functions and values # Velocity Magnitude @funcify_method("Time (s)", "Speed - Velocity Magnitude (m/s)") def speed(self): """Rocket speed, or velocity magnitude, as a Function of time.""" return (self.vx**2 + self.vy**2 + self.vz**2) ** 0.5 @property def out_of_rail_velocity(self): """Velocity at which the rocket leaves the launch rail.""" return self.speed.get_value_opt(self.out_of_rail_time) @cached_property def max_speed_time(self): """Time at which the rocket reaches its maximum speed.""" max_speed_time_index = np.argmax(self.speed.get_source()[:, 1]) return self.speed[max_speed_time_index, 0] @cached_property def max_speed(self): """Maximum speed reached by the rocket.""" return self.speed.get_value_opt(self.max_speed_time) # Accelerations @funcify_method("Time (s)", "acceleration Magnitude (m/s²)") def acceleration(self): """Rocket acceleration magnitude as a Function of time.""" return (self.ax**2 + self.ay**2 + self.az**2) ** 0.5 @funcify_method("Time (s)", "Axial Acceleration (m/s²)", "spline", "zero") def axial_acceleration(self): """Axial acceleration magnitude as a Function of time.""" return ( self.ax * self.attitude_vector_x + self.ay * self.attitude_vector_y + self.az * self.attitude_vector_z ) @cached_property def max_acceleration_power_on_time(self): """Time at which the rocket reaches its maximum acceleration during motor burn.""" burn_out_time_index = find_closest( self.acceleration.source[:, 0], self.rocket.motor.burn_out_time ) if burn_out_time_index == 0: return 0 # the burn out time is before the first time step max_acceleration_time_index = np.argmax( self.acceleration[:burn_out_time_index, 1] ) return self.acceleration[max_acceleration_time_index, 0] @cached_property def max_acceleration_power_on(self): """Maximum acceleration reached by the rocket during motor burn.""" return self.acceleration.get_value_opt(self.max_acceleration_power_on_time) @cached_property def max_acceleration_power_off_time(self): """Time at which the rocket reaches its maximum acceleration after motor burn.""" burn_out_time_index = find_closest( self.acceleration.source[:, 0], self.rocket.motor.burn_out_time ) max_acceleration_time_index = np.argmax( self.acceleration[burn_out_time_index:, 1] ) return self.acceleration[burn_out_time_index + max_acceleration_time_index, 0] @cached_property def max_acceleration_power_off(self): """Maximum acceleration reached by the rocket after motor burn.""" return self.acceleration.get_value_opt(self.max_acceleration_power_off_time) @cached_property def max_acceleration_time(self): """Time at which the rocket reaches its maximum acceleration.""" max_acceleration_time_index = np.argmax(self.acceleration[:, 1]) return self.acceleration[max_acceleration_time_index, 0] @cached_property def max_acceleration(self): """Maximum acceleration reached by the rocket.""" max_acceleration_time_index = np.argmax(self.acceleration[:, 1]) return self.acceleration[max_acceleration_time_index, 1] @funcify_method("Time (s)", "Horizontal Speed (m/s)") def horizontal_speed(self): """Rocket horizontal speed as a Function of time.""" return (self.vx**2 + self.vy**2) ** 0.5 # Path Angle @funcify_method("Time (s)", "Path Angle (°)", "spline", "constant") def path_angle(self): """Rocket path angle as a Function of time.""" path_angle = (180 / np.pi) * np.arctan2( self.vz[:, 1], self.horizontal_speed[:, 1] ) return np.column_stack([self.time, path_angle]) # Attitude Angle @funcify_method("Time (s)", "Attitude Vector X Component") def attitude_vector_x(self): """Rocket attitude vector X component as a Function of time. Same as row 1, column 3 of the rotation matrix that defines the conversion from the body frame to the inertial frame at each time step.""" return 2 * (self.e1 * self.e3 + self.e0 * self.e2) # a13 @funcify_method("Time (s)", "Attitude Vector Y Component") def attitude_vector_y(self): """Rocket attitude vector Y component as a Function of time. Same as row 2, column 3 of the rotation matrix that defines the conversion from the body frame to the inertial frame at each time step.""" return 2 * (self.e2 * self.e3 - self.e0 * self.e1) # a23 @funcify_method("Time (s)", "Attitude Vector Z Component") def attitude_vector_z(self): """Rocket attitude vector Z component as a Function of time. Same as row 3, column 3 of the rotation matrix that defines the conversion from the body frame to the inertial frame at each time step.""" return 1 - 2 * (self.e1**2 + self.e2**2) # a33 @funcify_method("Time (s)", "Attitude Angle (°)") def attitude_angle(self): """Rocket attitude angle as a Function of time.""" horizontal_attitude_proj = ( self.attitude_vector_x**2 + self.attitude_vector_y**2 ) ** 0.5 attitude_angle = (180 / np.pi) * np.arctan2( self.attitude_vector_z[:, 1], horizontal_attitude_proj[:, 1] ) return np.column_stack([self.time, attitude_angle]) # Lateral Attitude Angle @funcify_method("Time (s)", "Lateral Attitude Angle (°)") def lateral_attitude_angle(self): """Rocket lateral attitude angle as a Function of time.""" # TODO: complex method, it should be defined elsewhere. lateral_vector_angle = (np.pi / 180) * (self.heading - 90) lateral_vector_x = np.sin(lateral_vector_angle) lateral_vector_y = np.cos(lateral_vector_angle) attitude_lateral_proj = ( lateral_vector_x * self.attitude_vector_x[:, 1] + lateral_vector_y * self.attitude_vector_y[:, 1] ) attitude_lateral_proj_x = attitude_lateral_proj * lateral_vector_x attitude_lateral_proj_y = attitude_lateral_proj * lateral_vector_y attitude_lateral_plane_proj_x = ( self.attitude_vector_x[:, 1] - attitude_lateral_proj_x ) attitude_lateral_plane_proj_y = ( self.attitude_vector_y[:, 1] - attitude_lateral_proj_y ) attitude_lateral_plane_proj_z = self.attitude_vector_z[:, 1] attitude_lateral_plane_proj = ( attitude_lateral_plane_proj_x**2 + attitude_lateral_plane_proj_y**2 + attitude_lateral_plane_proj_z**2 ) ** 0.5 lateral_attitude_angle = (180 / np.pi) * np.arctan2( attitude_lateral_proj, attitude_lateral_plane_proj ) lateral_attitude_angle = np.column_stack([self.time, lateral_attitude_angle]) return lateral_attitude_angle # Euler Angles @funcify_method("Time (s)", "Precession Angle - ψ (°)", "spline", "constant") def psi(self): """Precession angle as a Function of time.""" psi = quaternions_to_precession( self.e0.y_array, self.e1.y_array, self.e2.y_array, self.e3.y_array ) return np.column_stack([self.time, psi]) @funcify_method("Time (s)", "Spin Angle - φ (°)", "spline", "constant") def phi(self): """Spin angle as a Function of time.""" phi = quaternions_to_spin( self.e0.y_array, self.e1.y_array, self.e2.y_array, self.e3.y_array ) return np.column_stack([self.time, phi]) @funcify_method("Time (s)", "Nutation Angle - θ (°)", "spline", "constant") def theta(self): """Nutation angle as a Function of time.""" theta = quaternions_to_nutation(self.e1.y_array, self.e2.y_array) return np.column_stack([self.time, theta]) # Fluid Mechanics variables # Freestream Velocity @funcify_method("Time (s)", "Freestream Velocity X (m/s)", "spline", "constant") def stream_velocity_x(self): """Freestream velocity X component as a Function of time.""" return np.column_stack((self.time, self.wind_velocity_x[:, 1] - self.vx[:, 1])) @funcify_method("Time (s)", "Freestream Velocity Y (m/s)", "spline", "constant") def stream_velocity_y(self): """Freestream velocity Y component as a Function of time.""" return np.column_stack((self.time, self.wind_velocity_y[:, 1] - self.vy[:, 1])) @funcify_method("Time (s)", "Freestream Velocity Z (m/s)", "spline", "constant") def stream_velocity_z(self): """Freestream velocity Z component as a Function of time.""" return np.column_stack((self.time, -self.vz[:, 1])) @funcify_method("Time (s)", "Freestream Speed (m/s)", "spline", "constant") def free_stream_speed(self): """Freestream speed as a Function of time.""" free_stream_speed = ( self.stream_velocity_x**2 + self.stream_velocity_y**2 + self.stream_velocity_z**2 ) ** 0.5 return free_stream_speed.get_source() # Apogee Freestream speed @cached_property def apogee_freestream_speed(self): """Freestream speed at apogee in m/s.""" return self.free_stream_speed.get_value_opt(self.apogee_time) # Mach Number @funcify_method("Time (s)", "Mach Number", "spline", "zero") def mach_number(self): """Mach number as a Function of time.""" return self.free_stream_speed / self.speed_of_sound @cached_property def max_mach_number_time(self): """Time of maximum Mach number.""" max_mach_number_time_index = np.argmax(self.mach_number[:, 1]) return self.mach_number[max_mach_number_time_index, 0] @cached_property def max_mach_number(self): """Maximum Mach number.""" return self.mach_number.get_value_opt(self.max_mach_number_time) # Stability Margin @cached_property def max_stability_margin_time(self): """Time of maximum stability margin.""" max_stability_margin_time_index = np.argmax(self.stability_margin[:, 1]) return self.stability_margin[max_stability_margin_time_index, 0] @cached_property def max_stability_margin(self): """Maximum stability margin.""" return self.stability_margin.get_value_opt(self.max_stability_margin_time) @cached_property def min_stability_margin_time(self): """Time of minimum stability margin.""" min_stability_margin_time_index = np.argmin(self.stability_margin[:, 1]) return self.stability_margin[min_stability_margin_time_index, 0] @cached_property def min_stability_margin(self): """Minimum stability margin.""" return self.stability_margin.get_value_opt(self.min_stability_margin_time) @property def initial_stability_margin(self): """Stability margin at time 0. Returns ------- float """ return self.stability_margin.get_value_opt(self.time[0]) @property def out_of_rail_stability_margin(self): """Stability margin at the time the rocket leaves the rail. Returns ------- float """ return self.stability_margin.get_value_opt(self.out_of_rail_time) # Reynolds Number @funcify_method("Time (s)", "Reynolds Number", "spline", "zero") def reynolds_number(self): """Reynolds number as a Function of time.""" return (self.density * self.free_stream_speed / self.dynamic_viscosity) * ( 2 * self.rocket.radius ) @cached_property def max_reynolds_number_time(self): """Time of maximum Reynolds number.""" max_reynolds_number_time_index = np.argmax(self.reynolds_number[:, 1]) return self.reynolds_number[max_reynolds_number_time_index, 0] @cached_property def max_reynolds_number(self): """Maximum Reynolds number.""" return self.reynolds_number.get_value_opt(self.max_reynolds_number_time) # Dynamic Pressure @funcify_method("Time (s)", "Dynamic Pressure (Pa)", "spline", "zero") def dynamic_pressure(self): """Dynamic pressure as a Function of time.""" return 0.5 * self.density * self.free_stream_speed**2 @cached_property def max_dynamic_pressure_time(self): """Time of maximum dynamic pressure.""" max_dynamic_pressure_time_index = np.argmax(self.dynamic_pressure[:, 1]) return self.dynamic_pressure[max_dynamic_pressure_time_index, 0] @cached_property def max_dynamic_pressure(self): """Maximum dynamic pressure.""" return self.dynamic_pressure.get_value_opt(self.max_dynamic_pressure_time) # Total Pressure @funcify_method("Time (s)", "Total Pressure (Pa)", "spline", "zero") def total_pressure(self): """Total pressure as a Function of time.""" return self.pressure * (1 + 0.2 * self.mach_number**2) ** (3.5) @cached_property def max_total_pressure_time(self): """Time of maximum total pressure.""" max_total_pressure_time_index = np.argmax(self.total_pressure[:, 1]) return self.total_pressure[max_total_pressure_time_index, 0] @cached_property def max_total_pressure(self): """Maximum total pressure.""" return self.total_pressure.get_value_opt(self.max_total_pressure_time) # Dynamics functions and variables # Aerodynamic Lift and Drag # TODO: These are not lift and drag, they are the aerodynamic forces in # the rocket frame, meaning they are normal and axial forces. They should # be renamed. @funcify_method("Time (s)", "Aerodynamic Lift Force (N)", "spline", "zero") def aerodynamic_lift(self): """Aerodynamic lift force as a Function of time.""" return (self.R1**2 + self.R2**2) ** 0.5 @funcify_method("Time (s)", "Aerodynamic Drag Force (N)", "spline", "zero") def aerodynamic_drag(self): """Aerodynamic drag force as a Function of time.""" return -1 * self.R3 @funcify_method("Time (s)", "Aerodynamic Bending Moment (Nm)", "spline", "zero") def aerodynamic_bending_moment(self): """Aerodynamic bending moment as a Function of time.""" return (self.M1**2 + self.M2**2) ** 0.5 @funcify_method("Time (s)", "Aerodynamic Spin Moment (Nm)", "spline", "zero") def aerodynamic_spin_moment(self): """Aerodynamic spin moment as a Function of time.""" return self.M3 # Energy # Kinetic Energy @funcify_method("Time (s)", "Rotational Kinetic Energy (J)") def rotational_energy(self): """Rotational kinetic energy as a Function of time.""" rotational_energy = 0.5 * ( self.rocket.I_11 * self.w1**2 + self.rocket.I_22 * self.w2**2 + self.rocket.I_33 * self.w3**2 ) rotational_energy.set_discrete_based_on_model(self.w1) return rotational_energy @funcify_method("Time (s)", "Translational Kinetic Energy (J)", "spline", "zero") def translational_energy(self): """Translational kinetic energy as a Function of time.""" # Redefine total_mass time grid to allow for efficient Function algebra total_mass = deepcopy(self.rocket.total_mass) total_mass.set_discrete_based_on_model(self.vz) translational_energy = 0.5 * total_mass * (self.speed**2) return translational_energy @funcify_method("Time (s)", "Kinetic Energy (J)", "spline", "zero") def kinetic_energy(self): """Total kinetic energy as a Function of time.""" return self.rotational_energy + self.translational_energy # Potential Energy @funcify_method("Time (s)", "Potential Energy (J)", "spline", "constant") def potential_energy(self): """Potential energy as a Function of time in relation to sea level.""" # Constants # TODO: this constant should come from Environment. standard_gravitational_parameter = 3.986004418e14 # Redefine total_mass time grid to allow for efficient Function algebra total_mass = deepcopy(self.rocket.total_mass) total_mass.set_discrete_based_on_model(self.z) return ( standard_gravitational_parameter * total_mass * (1 / (self.z + self.env.earth_radius) - 1 / self.env.earth_radius) ) # Total Mechanical Energy @funcify_method("Time (s)", "Mechanical Energy (J)", "spline", "constant") def total_energy(self): """Total mechanical energy as a Function of time.""" return self.kinetic_energy + self.potential_energy # thrust Power @funcify_method("Time (s)", "Thrust Power (W)", "spline", "zero") def thrust_power(self): """Thrust power as a Function of time.""" return self.net_thrust * self.speed # Drag Power @funcify_method("Time (s)", "Drag Power (W)", "spline", "zero") def drag_power(self): """Drag power as a Function of time.""" drag_power = self.R3 * self.speed drag_power.set_outputs("Drag Power (W)") return drag_power # Angle of Attack @cached_property def direction_cosine_matrixes(self): """Direction cosine matrix representing the attitude of the body frame, relative to the inertial frame, at each time step.""" # Stack the y_arrays from e0, e1, e2, and e3 along a new axis stacked_arrays = np.stack( [self.e0.y_array, self.e1.y_array, self.e2.y_array, self.e3.y_array], axis=-1, ) # Apply the transformation to the stacked arrays along the last axis Kt = np.array([Matrix.transformation(row).transpose for row in stacked_arrays]) return Kt @cached_property def stream_velocity_body_frame(self): """Stream velocity array at the center of dry mass in the body frame at each time step.""" Kt = self.direction_cosine_matrixes stream_velocity = np.array( [ self.stream_velocity_x.y_array, self.stream_velocity_y.y_array, self.stream_velocity_z.y_array, ] ).transpose() stream_velocity_body = np.squeeze( np.matmul(Kt, stream_velocity[:, :, np.newaxis]) ) return stream_velocity_body @funcify_method("Time (s)", "Angle of Attack (°)", "spline", "constant") def angle_of_attack(self): """Angle of attack of the rocket with respect to the freestream velocity vector. Sometimes called total angle of attack. Defined as the angle between the freestream velocity vector and the rocket's z-axis. All in the Body Axes Coordinate System.""" # Define stream velocity z component in body frame stream_vz_body = ( -self.attitude_vector_x.y_array * self.stream_velocity_x.y_array - self.attitude_vector_y.y_array * self.stream_velocity_y.y_array - self.attitude_vector_z.y_array * self.stream_velocity_z.y_array ) # Define freestream speed list free_stream_speed = self.free_stream_speed.y_array stream_vz_body_normalized = np.divide( stream_vz_body, free_stream_speed, out=np.zeros_like(stream_vz_body), where=free_stream_speed > 1e-6, ) stream_vz_body_normalized = np.clip(stream_vz_body_normalized, -1, 1) # Calculate angle of attack and convert to degrees angle_of_attack = np.rad2deg(np.arccos(stream_vz_body_normalized)) angle_of_attack = np.nan_to_num(angle_of_attack) return np.column_stack([self.time, angle_of_attack]) @funcify_method("Time (s)", "Partial Angle of Attack (°)", "spline", "constant") def partial_angle_of_attack(self): """Partial angle of attack of the rocket with respect to the stream velocity vector. By partial angle of attack, it is meant the angle between the stream velocity vector in the y-z plane and the rocket's z-axis. All in the Body Axes Coordinate System.""" # Stream velocity in standard aerodynamic frame stream_velocity = -self.stream_velocity_body_frame alpha = np.arctan2( stream_velocity[:, 1], stream_velocity[:, 2], ) # y-z plane return np.column_stack([self.time, np.rad2deg(alpha)]) @funcify_method("Time (s)", "Beta (°)", "spline", "constant") def angle_of_sideslip(self): """Angle of sideslip of the rocket with respect to the stream velocity vector. Defined as the angle between the stream velocity vector in the x-z plane and the rocket's z-axis. All in the Body Axes Coordinate System.""" # Stream velocity in standard aerodynamic frame stream_velocity = -self.stream_velocity_body_frame beta = np.arctan2( stream_velocity[:, 0], stream_velocity[:, 2], ) # x-z plane return np.column_stack([self.time, np.rad2deg(beta)]) # Frequency response and stability variables @funcify_method("Frequency (Hz)", "ω1 Fourier Amplitude", "spline", "zero") def omega1_frequency_response(self): """Angular velocity 1 frequency response as a Function of frequency, as the rocket leaves the launch rail for 5 seconds of flight. """ return self.w1.to_frequency_domain( self.out_of_rail_time, self.out_of_rail_time + 5, 100 ) @funcify_method("Frequency (Hz)", "ω2 Fourier Amplitude", "spline", "zero") def omega2_frequency_response(self): """Angular velocity 2 frequency response as a Function of frequency, as the rocket leaves the launch rail for 5 seconds of flight. """ return self.w2.to_frequency_domain( self.out_of_rail_time, self.out_of_rail_time + 5, 100 ) @funcify_method("Frequency (Hz)", "ω3 Fourier Amplitude", "spline", "zero") def omega3_frequency_response(self): """Angular velocity 3 frequency response as a Function of frequency, as the rocket leaves the launch rail for 5 seconds of flight. """ return self.w3.to_frequency_domain( self.out_of_rail_time, self.out_of_rail_time + 5, 100 ) @funcify_method( "Frequency (Hz)", "Attitude Angle Fourier Amplitude", "spline", "zero" ) def attitude_frequency_response(self): """Attitude frequency response as a Function of frequency, as the rocket leaves the launch rail for 5 seconds of flight.""" return self.attitude_angle.to_frequency_domain( lower=self.out_of_rail_time, upper=self.out_of_rail_time + 5, sampling_frequency=100, ) @cached_property def static_margin(self): """Static margin of the rocket.""" return self.rocket.static_margin @funcify_method("Time (s)", "Stability Margin (c)", "linear", "zero") def stability_margin(self): """Stability margin of the rocket along the flight, it considers the variation of the center of pressure position according to the mach number, as well as the variation of the center of gravity position according to the propellant mass evolution. Parameters ---------- None Returns ------- stability : rocketpy.Function Stability margin as a rocketpy.Function of time. The stability margin is defined as the distance between the center of pressure and the center of gravity, divided by the rocket diameter. """ return [(t, self.rocket.stability_margin(m, t)) for t, m in self.mach_number] # Rail Button Forces @funcify_method("Time (s)", "Upper Rail Button Normal Force (N)", "spline", "zero") def rail_button1_normal_force(self): """Upper rail button normal force as a Function of time. If there's no rail button defined, the function returns a null Function.""" return self.__calculate_rail_button_forces[0] @funcify_method("Time (s)", "Upper Rail Button Shear Force (N)", "spline", "zero") def rail_button1_shear_force(self): """Upper rail button shear force as a Function of time. If there's no rail button defined, the function returns a null Function.""" return self.__calculate_rail_button_forces[1] @funcify_method("Time (s)", "Lower Rail Button Normal Force (N)", "spline", "zero") def rail_button2_normal_force(self): """Lower rail button normal force as a Function of time. If there's no rail button defined, the function returns a null Function.""" return self.__calculate_rail_button_forces[2] @funcify_method("Time (s)", "Lower Rail Button Shear Force (N)", "spline", "zero") def rail_button2_shear_force(self): """Lower rail button shear force as a Function of time. If there's no rail button defined, the function returns a null Function.""" return self.__calculate_rail_button_forces[3] @property def max_rail_button1_normal_force(self): """Maximum upper rail button normal force, in Newtons.""" return np.abs(self.rail_button1_normal_force.y_array).max() @property def max_rail_button1_shear_force(self): """Maximum upper rail button shear force, in Newtons.""" return np.abs(self.rail_button1_shear_force.y_array).max() @property def max_rail_button2_normal_force(self): """Maximum lower rail button normal force, in Newtons.""" return np.abs(self.rail_button2_normal_force.y_array).max() @property def max_rail_button2_shear_force(self): """Maximum lower rail button shear force, in Newtons.""" return np.abs(self.rail_button2_shear_force.y_array).max() @cached_property def calculate_rail_button_bending_moments(self): """Calculate internal bending moments at rail button attachment points. Uses beam theory to determine the internal structural moments for stress analysis of the rail button attachments (fasteners and airframe). The bending moment at each button attachment consists of: 1. Normal force moment: $M = N \\times d$, where $N$ is the normal reaction force and $d$ is the distance from button to center of dry mass. 2. Shear force cantilever moment: $M = S \\times h$, where $S$ is the shear (tangential) force and $h$ is the button standoff height. Returns ------- tuple rail_button1_bending_moment : Function Bending moment at upper rail button as a function of time (N·m). max_rail_button1_bending_moment : float Maximum upper rail button bending moment (N·m). rail_button2_bending_moment : Function Bending moment at lower rail button as a function of time (N·m). max_rail_button2_bending_moment : float Maximum lower rail button bending moment (N·m). Notes ----- - Calculated only during the rail phase of flight - Maximum values use absolute values for worst-case stress analysis - The bending moments represent internal stresses in the rocket airframe at the rail button attachment points **Assumptions:** - Rail buttons act as simple supports: provide reaction forces (normal and shear) but no moment reaction at the rail contact point - The rocket acts as a beam supported at two points (rail buttons) - Bending moments arise from the lever arm effect of reaction forces and the cantilever moment from button standoff height """ # Check if rail buttons exist null_moment = Function(0) if len(self.rocket.rail_buttons) == 0: warnings.warn( "Trying to calculate rail button bending moments without " "rail buttons defined. Setting moments to zero.", UserWarning, ) return (null_moment, 0.0, null_moment, 0.0) # Get rail button geometry rail_buttons_tuple = self.rocket.rail_buttons[0] # Rail button standoff height h_button = rail_buttons_tuple.component.button_height if h_button is None: warnings.warn( "Rail button height not defined. Bending moments cannot be " "calculated. Setting moments to zero.", UserWarning, ) return (null_moment, 0.0, null_moment, 0.0) upper_button_position = ( rail_buttons_tuple.component.buttons_distance + rail_buttons_tuple.position.z ) lower_button_position = rail_buttons_tuple.position.z # Get center of dry mass (handle both callable and property) if callable(self.rocket.center_of_dry_mass_position): cdm = self.rocket.center_of_dry_mass_position(self.rocket._csys) else: cdm = self.rocket.center_of_dry_mass_position # Distances from buttons to center of dry mass d1 = abs(upper_button_position - cdm) d2 = abs(lower_button_position - cdm) # forces N1 = self.rail_button1_normal_force N2 = self.rail_button2_normal_force S1 = self.rail_button1_shear_force S2 = self.rail_button2_shear_force t = N1.source[:, 0] # Calculate bending moments at attachment points # Primary contribution from shear force acting at button height # Secondary contribution from normal force creating moment about attachment m1_values = N2.source[:, 1] * d2 + S1.source[:, 1] * h_button m2_values = N1.source[:, 1] * d1 + S2.source[:, 1] * h_button rail_button1_bending_moment = Function( np.column_stack([t, m1_values]), inputs="Time (s)", outputs="Bending Moment (N·m)", interpolation="linear", ) rail_button2_bending_moment = Function( np.column_stack([t, m2_values]), inputs="Time (s)", outputs="Bending Moment (N·m)", interpolation="linear", ) # Maximum bending moments (absolute value for stress calculations) max_rail_button1_bending_moment = float(np.max(np.abs(m1_values))) max_rail_button2_bending_moment = float(np.max(np.abs(m2_values))) return ( rail_button1_bending_moment, max_rail_button1_bending_moment, rail_button2_bending_moment, max_rail_button2_bending_moment, ) @property def rail_button1_bending_moment(self): """Upper rail button bending moment as a Function of time.""" return self.calculate_rail_button_bending_moments[0] @property def max_rail_button1_bending_moment(self): """Maximum upper rail button bending moment, in N·m.""" return self.calculate_rail_button_bending_moments[1] @property def rail_button2_bending_moment(self): """Lower rail button bending moment as a Function of time.""" return self.calculate_rail_button_bending_moments[2] @property def max_rail_button2_bending_moment(self): """Maximum lower rail button bending moment, in N·m.""" return self.calculate_rail_button_bending_moments[3] @funcify_method( "Time (s)", "Horizontal Distance to Launch Point (m)", "spline", "constant" ) def drift(self): """Rocket horizontal distance to the launch point, in meters, as a Function of time.""" return np.column_stack( (self.time, (self.x[:, 1] ** 2 + self.y[:, 1] ** 2) ** 0.5) ) @funcify_method("Time (s)", "Bearing (°)", "spline", "constant") def bearing(self): """Rocket bearing compass, in degrees, as a Function of time.""" x, y = self.x[:, 1], self.y[:, 1] bearing = (2 * np.pi - np.arctan2(-x, y)) * (180 / np.pi) return np.column_stack((self.time, bearing)) @funcify_method("Time (s)", "Latitude (°)", "linear", "constant") def latitude(self): """Rocket latitude coordinate, in degrees, as a Function of time. """ lat1 = np.deg2rad(self.env.latitude) # Launch lat point converted to radians # Applies the haversine equation to find final lat/lon coordinates latitude = np.rad2deg( np.arcsin( np.sin(lat1) * np.cos(self.drift[:, 1] / self.env.earth_radius) + np.cos(lat1) * np.sin(self.drift[:, 1] / self.env.earth_radius) * np.cos(np.deg2rad(self.bearing[:, 1])) ) ) return np.column_stack((self.time, latitude)) # TODO: haversine should be defined in tools.py so we just invoke it in here. @funcify_method("Time (s)", "Longitude (°)", "linear", "constant") def longitude(self): """Rocket longitude coordinate, in degrees, as a Function of time. """ lat1 = np.deg2rad(self.env.latitude) # Launch lat point converted to radians lon1 = np.deg2rad(self.env.longitude) # Launch lon point converted to radians # Applies the haversine equation to find final lat/lon coordinates longitude = np.rad2deg( lon1 + np.arctan2( np.sin(np.deg2rad(self.bearing[:, 1])) * np.sin(self.drift[:, 1] / self.env.earth_radius) * np.cos(lat1), np.cos(self.drift[:, 1] / self.env.earth_radius) - np.sin(lat1) * np.sin(np.deg2rad(self.latitude[:, 1])), ) ) return np.column_stack((self.time, longitude))
[docs] def get_controller_observed_variables(self): """Retrieve the observed variables related to air brakes from the controllers. If there is only one set of observed variables, it is returned as a list. If there are multiple sets, the list containing all sets is returned.""" observed_variables = [ controller.observed_variables for controller in self._controllers ] return ( observed_variables[0] if len(observed_variables) == 1 else observed_variables )
@cached_property def __calculate_rail_button_forces(self): # TODO: complex method. """Calculate the forces applied to the rail buttons while rocket is still on the launch rail. It will return 0 if no rail buttons are defined. Returns ------- F11: Function Rail Button 1 force in the 1 direction F12: Function Rail Button 1 force in the 2 direction F21: Function Rail Button 2 force in the 1 direction F22: Function Rail Button 2 force in the 2 direction """ # First check for no rail phase or rail buttons null_force = Function(0) if self.out_of_rail_time_index == 0: # No rail phase, no rail button forces warnings.warn( "Trying to calculate rail button forces without a rail phase defined. " + "The rail button forces will be set to zero.", UserWarning, ) return null_force, null_force, null_force, null_force if len(self.rocket.rail_buttons) == 0: warnings.warn( "Trying to calculate rail button forces without rail buttons defined. " + "The rail button forces will be set to zero.", UserWarning, ) return null_force, null_force, null_force, null_force # Distance from Rail Button 1 (upper) to CM rail_buttons_tuple = self.rocket.rail_buttons[0] upper_button_position = ( rail_buttons_tuple.component.buttons_distance + rail_buttons_tuple.position.z ) lower_button_position = rail_buttons_tuple.position.z angular_position_rad = rail_buttons_tuple.component.angular_position_rad D1 = ( upper_button_position - self.rocket.center_of_dry_mass_position ) * self.rocket._csys # Distance from Rail Button 2 (lower) to CM D2 = ( lower_button_position - self.rocket.center_of_dry_mass_position ) * self.rocket._csys F11 = (self.R1 * D2 - self.M2) / (D1 + D2) F11.set_outputs("Upper button force direction 1 (m)") F12 = (self.R2 * D2 + self.M1) / (D1 + D2) F12.set_outputs("Upper button force direction 2 (m)") F21 = (self.R1 * D1 + self.M2) / (D1 + D2) F21.set_outputs("Lower button force direction 1 (m)") F22 = (self.R2 * D1 - self.M1) / (D1 + D2) F22.set_outputs("Lower button force direction 2 (m)") model = Function( F11.get_source()[: self.out_of_rail_time_index + 1, :], interpolation=F11.__interpolation__, ) # Limit force calculation to when rocket is in rail F11.set_discrete_based_on_model(model) F12.set_discrete_based_on_model(model) F21.set_discrete_based_on_model(model) F22.set_discrete_based_on_model(model) rail_button1_normal_force = F11 * np.cos(angular_position_rad) + F12 * np.sin( angular_position_rad ) rail_button1_shear_force = F11 * -np.sin(angular_position_rad) + F12 * np.cos( angular_position_rad ) rail_button2_normal_force = F21 * np.cos(angular_position_rad) + F22 * np.sin( angular_position_rad ) rail_button2_shear_force = F21 * -np.sin(angular_position_rad) + F22 * np.cos( angular_position_rad ) return ( rail_button1_normal_force, rail_button1_shear_force, rail_button2_normal_force, rail_button2_shear_force, ) def __transform_pressure_signals_lists_to_functions(self): """Calculate the pressure signal from the pressure sensor. It creates a signal_function attribute in the parachute object. Parachute works as a subclass of Rocket class. Returns ------- None """ # Transform parachute sensor feed into functions for parachute in self.rocket.parachutes: # TODO: these Functions do not need input validation parachute.clean_pressure_signal_function = Function( parachute.clean_pressure_signal, "Time (s)", "Pressure - Without Noise (Pa)", "linear", ) parachute.noise_signal_function = Function( parachute.noise_signal, "Time (s)", "Pressure Noise (Pa)", "linear" ) parachute.noisy_pressure_signal_function = ( parachute.clean_pressure_signal_function + parachute.noise_signal_function ) @cached_property def __evaluate_post_process(self): """Evaluate all post-processing variables by running the simulation again but with the post-processing flag set to True. Returns ------- np.array An array containing all post-processed variables evaluated at each time step. Each element of the array is a list containing: [t, ax, ay, az, alpha1, alpha2, alpha3, R1, R2, R3, M1, M2, M3, net_thrust] """ self.__post_processed_variables = [] for phase_index, phase in self.time_iterator(self.flight_phases): init_time = phase.t final_time = self.flight_phases[phase_index + 1].t current_derivative = phase.derivative for callback in phase.callbacks: callback(self) for step in self.solution: if init_time < step[0] <= final_time or ( init_time == self.t_initial and step[0] == self.t_initial ): current_derivative(step[0], step[1:], post_processing=True) return np.array(self.__post_processed_variables)
[docs] def calculate_stall_wind_velocity(self, stall_angle): # TODO: move to utilities """Function to calculate the maximum wind velocity before the angle of attack exceeds a desired angle, at the instant of departing rail launch. Can be helpful if you know the exact stall angle of all aerodynamics surfaces. Parameters ---------- stall_angle : float Angle, in degrees, for which you would like to know the maximum wind speed before the angle of attack exceeds it Return ------ None """ v_f = self.out_of_rail_velocity theta = np.radians(self.inclination) stall_angle = np.radians(stall_angle) c = (math.cos(stall_angle) ** 2 - math.cos(theta) ** 2) / math.sin( stall_angle ) ** 2 w_v = ( 2 * v_f * math.cos(theta) / c + ( 4 * v_f * v_f * math.cos(theta) * math.cos(theta) / (c**2) + 4 * 1 * v_f * v_f / c ) ** 0.5 ) / 2 stall_angle = np.degrees(stall_angle) print( "Maximum wind velocity at Rail Departure time before angle" + f" of attack exceeds {stall_angle:.3f}°: {w_v:.3f} m/s" )
[docs] @deprecated( reason="Moved to FlightDataExporter.export_pressures()", version="v1.12.0", alternative="rocketpy.simulation.flight_data_exporter.FlightDataExporter.export_pressures", ) def export_pressures(self, file_name, time_step): """ .. deprecated:: 1.11 Use :class:`rocketpy.simulation.flight_data_exporter.FlightDataExporter` and call ``.export_pressures(...)``. """ return FlightDataExporter(self).export_pressures(file_name, time_step)
[docs] @deprecated( reason="Moved to FlightDataExporter.export_data()", version="v1.12.0", alternative="rocketpy.simulation.flight_data_exporter.FlightDataExporter.export_data", ) def export_data(self, file_name, *variables, time_step=None): """ .. deprecated:: 1.11 Use :class:`rocketpy.simulation.flight_data_exporter.FlightDataExporter` and call ``.export_data(...)``. """ return FlightDataExporter(self).export_data( file_name, *variables, time_step=time_step )
[docs] @deprecated( reason="Moved to FlightDataExporter.export_sensor_data()", version="v1.12.0", alternative="rocketpy.simulation.flight_data_exporter.FlightDataExporter.export_sensor_data", ) def export_sensor_data(self, file_name, sensor=None): """ .. deprecated:: 1.11 Use :class:`rocketpy.simulation.flight_data_exporter.FlightDataExporter` and call ``.export_sensor_data(...)``. """ return FlightDataExporter(self).export_sensor_data(file_name, sensor=sensor)
[docs] @deprecated( reason="Moved to FlightDataExporter.export_kml()", version="v1.12.0", alternative="rocketpy.simulation.flight_data_exporter.FlightDataExporter.export_kml", ) def export_kml( self, file_name="trajectory.kml", time_step=None, extrude=True, color="641400F0", altitude_mode="absolute", ): """ .. deprecated:: 1.11 Use :class:`rocketpy.simulation.flight_data_exporter.FlightDataExporter` and call ``.export_kml(...)``. """ return FlightDataExporter(self).export_kml( file_name=file_name, time_step=time_step, extrude=extrude, color=color, altitude_mode=altitude_mode, )
[docs] def info(self): """Prints out a summary of the data available about the Flight.""" self.prints.all()
[docs] def all_info(self): """Prints out all data and graphs available about the Flight.""" self.info() self.plots.all()
def time_iterator(self, node_list): i = 0 while i < len(node_list) - 1: yield i, node_list[i] i += 1 def to_dict(self, **kwargs): data = { "rocket": self.rocket, "env": self.env, "rail_length": self.rail_length, "inclination": self.inclination, "heading": self.heading, "initial_solution": self.initial_solution, "terminate_on_apogee": self.terminate_on_apogee, "max_time": self.max_time, "max_time_step": self.max_time_step, "min_time_step": self.min_time_step, "rtol": self.rtol, "atol": self.atol, "time_overshoot": self.time_overshoot, "name": self.name, "equations_of_motion": self.equations_of_motion, # The following outputs are essential to run all_info method "solution": self.solution, "out_of_rail_time": self.out_of_rail_time, "out_of_rail_time_index": self.out_of_rail_time_index, "apogee_time": self.apogee_time, "apogee": self.apogee, "parachute_events": self.parachute_events, "impact_state": self.impact_state, "impact_velocity": self.impact_velocity, "x_impact": self.x_impact, "y_impact": self.y_impact, "t_final": self.t_final, "function_evaluations": self.function_evaluations, "ax": self.ax, "ay": self.ay, "az": self.az, "alpha1": self.alpha1, "alpha2": self.alpha2, "alpha3": self.alpha3, "R1": self.R1, "R2": self.R2, "R3": self.R3, "M1": self.M1, "M2": self.M2, "M3": self.M3, "net_thrust": self.net_thrust, } if kwargs.get("include_outputs", False): data.update( { "time": self.time, "out_of_rail_velocity": self.out_of_rail_velocity, "out_of_rail_state": self.out_of_rail_state, "apogee_x": self.apogee_x, "apogee_y": self.apogee_y, "apogee_state": self.apogee_state, "x": self.x, "y": self.y, "z": self.z, "vx": self.vx, "vy": self.vy, "vz": self.vz, "e0": self.e0, "e1": self.e1, "e2": self.e2, "e3": self.e3, "w1": self.w1, "w2": self.w2, "w3": self.w3, "altitude": self.altitude, "mach_number": self.mach_number, "stream_velocity_x": self.stream_velocity_x, "stream_velocity_y": self.stream_velocity_y, "stream_velocity_z": self.stream_velocity_z, "free_stream_speed": self.free_stream_speed, "angle_of_attack": self.angle_of_attack, "static_margin": self.static_margin, "stability_margin": self.stability_margin, "latitude": self.latitude, "longitude": self.longitude, } ) return data @classmethod def from_dict(cls, data): return cls( rocket=data["rocket"], environment=data["env"], rail_length=data["rail_length"], inclination=data["inclination"], heading=data["heading"], initial_solution=None, terminate_on_apogee=data["terminate_on_apogee"], max_time=data["max_time"], max_time_step=data["max_time_step"], min_time_step=data["min_time_step"], rtol=data["rtol"], atol=data["atol"], time_overshoot=data["time_overshoot"], name=data["name"], equations_of_motion=data["equations_of_motion"], )
[docs] class FlightPhases: """Class to handle flight phases. It is used to store the derivatives and callbacks for each flight phase. It is also used to handle the insertion of flight phases in the correct order, according to their initial time. Attributes ---------- list : list A list of FlightPhase objects. See FlightPhase subclass. """
[docs] def __init__(self, init_list=None): init_list = [] if init_list is None else init_list self.list = init_list[:]
def __getitem__(self, index): return self.list[index] def __len__(self): return len(self.list) def __repr__(self): return str(self.list)
[docs] def display_warning(self, *messages): # pragma: no cover """A simple function to print a warning message.""" print("WARNING:", *messages)
[docs] def add(self, flight_phase, index=None): # TODO: quite complex method """Add a flight phase to the list. It will be inserted in the correct position, according to its initial time. If no index is provided, it will be appended to the end of the list. If by any reason the flight phase cannot be inserted in the correct position, a warning will be displayed and the flight phase will be inserted in the closest position possible. Parameters ---------- flight_phase : FlightPhase The flight phase object to be added. See FlightPhase class. index : int, optional The index of the flight phase to be added. If no index is provided, the flight phase will be appended to the end of the list. Default is None. Returns ------- None """ # Handle first phase if len(self.list) == 0: self.list.append(flight_phase) return None # Handle appending to last position if index is None: previous_phase = self.list[-1] if flight_phase.t > previous_phase.t: self.list.append(flight_phase) return None warning_msg = ( ( "Trying to add flight phase starting together with the " "one preceding it. This may be caused by multiple " "parachutes being triggered simultaneously." ) if flight_phase.t == previous_phase.t else ( "Trying to add flight phase starting *before* the one " "*preceding* it. This may be caused by multiple " "parachutes being triggered simultaneously " "or by having a negative parachute lag.", ) ) self.display_warning(*warning_msg) flight_phase.t += 1e-7 if flight_phase.t == previous_phase.t else 0 self.add( flight_phase, -2 if flight_phase.t < previous_phase.t else None ) return None # Handle inserting into intermediary position. # Check if new flight phase respects time next_phase = self.list[index] previous_phase = self.list[index - 1] if previous_phase.t < flight_phase.t < next_phase.t: self.list.insert(index, flight_phase) return None warning_msg = ( ( "Trying to add flight phase starting *together* with the one *preceding* it. ", "This may be caused by multiple parachutes being triggered simultaneously.", ) if flight_phase.t == previous_phase.t else ( ( "Trying to add flight phase starting *together* with the one *proceeding* it. ", "This may be caused by multiple parachutes being triggered simultaneously.", ) if flight_phase.t == next_phase.t else ( ( "Trying to add flight phase starting *before* the one *preceding* it. ", "This may be caused by multiple parachutes being triggered simultaneously", " or by having a negative parachute lag.", ) if flight_phase.t < previous_phase.t else ( "Trying to add flight phase starting *after* the one *proceeding* it.", "This may be caused by multiple parachutes being triggered simultaneously.", ) ) ) ) self.display_warning(*warning_msg) adjust = 1e-7 if flight_phase.t in {previous_phase.t, next_phase.t} else 0 new_index = ( index - 1 if flight_phase.t < previous_phase.t else index + 1 if flight_phase.t > next_phase.t else index ) flight_phase.t += adjust self.add(flight_phase, new_index)
[docs] def add_phase(self, t, derivatives=None, callback=None, clear=True, index=None): """Add a new flight phase to the list, with the specified characteristics. This method creates a new FlightPhase instance and adds it to the flight phases list, either at the specified index position or appended to the end. See FlightPhases.add() for more information. Parameters ---------- t : float The initial time of the new flight phase. derivatives : function, optional A function representing the derivatives of the flight phase. Default is None. callback : list of functions, optional A list of callback functions to be executed during the flight phase. Default is None. You can also pass an empty list. clear : bool, optional A flag indicating whether to clear the solution after the phase. Default is True. index : int, optional The index at which the new flight phase should be inserted. If not provided, the flight phase will be appended to the end of the list. Default is None. Returns ------- None """ self.add(self.FlightPhase(t, derivatives, callback, clear), index)
[docs] def flush_after(self, index): """This function deletes all flight phases after a given index. Parameters ---------- index : int The index of the last flight phase to be kept. Returns ------- None """ del self.list[index + 1 :]
[docs] class FlightPhase: """Class to store a flight phase. It stores the initial time, the derivative function, the callback functions and a flag to clear the solution after the phase. Attributes ---------- t : float The initial time of the flight phase. derivative : function A function representing the derivatives of the flight phase. callbacks : list of functions A list of callback functions to be executed during the flight phase. clear : bool A flag indicating whether to clear the solution after the phase. """ # TODO: add a "name" optional argument to the FlightPhase. Really helps.
[docs] def __init__(self, t, derivative=None, callbacks=None, clear=True): self.t = t self.derivative = derivative self.callbacks = callbacks[:] if callbacks is not None else [] self.clear = clear
def __repr__(self): name = "None" if self.derivative is None else self.derivative.__name__ return ( f"<FlightPhase(t= {self.t}, derivative= {name}, " f"callbacks= {self.callbacks}, clear= {self.clear})>" )
[docs] class TimeNodes: """TimeNodes is a class that stores all the time nodes of a simulation. It is meant to work like a python list, but it has some additional methods that are useful for the simulation. Them items stored in are TimeNodes object are instances of the TimeNode class. """
[docs] def __init__(self, init_list=None): if not init_list: init_list = [] self.list = init_list[:]
def __getitem__(self, index): return self.list[index] def __len__(self): return len(self.list) def __repr__(self): return str(self.list) def add(self, time_node): self.list.append(time_node) def add_node(self, t, parachutes, controllers, sensors): self.list.append(self.TimeNode(t, parachutes, controllers, sensors)) def add_parachutes(self, parachutes, t_init, t_end): for parachute in parachutes: # Calculate start of sampling time nodes sampling_interval = 1 / parachute.sampling_rate parachute_node_list = [ self.TimeNode(i * sampling_interval, [parachute], [], []) for i in range( math.ceil(t_init / sampling_interval), math.floor(t_end / sampling_interval) + 1, ) ] self.list += parachute_node_list def add_controllers(self, controllers, t_init, t_end): for controller in controllers: # Calculate start of sampling time nodes controller_time_step = 1 / controller.sampling_rate controller_node_list = [ self.TimeNode(i * controller_time_step, [], [controller], []) for i in range( math.ceil(t_init / controller_time_step), math.floor(t_end / controller_time_step) + 1, ) ] self.list += controller_node_list def add_sensors(self, sensors, t_init, t_end): # Iterate over sensors for sensor_component_tuple in sensors: # Calculate start of sampling time nodes sensor_time_step = 1 / sensor_component_tuple.component.sampling_rate sensor_node_list = [ self.TimeNode( i * sensor_time_step, [], [], [sensor_component_tuple] ) for i in range( math.ceil(t_init / sensor_time_step), math.floor(t_end / sensor_time_step) + 1, ) ] self.list += sensor_node_list def sort(self): self.list.sort()
[docs] def merge(self): """Merge all the time nodes that have the same time. This is made to avoid multiple evaluations of the same time node. This method does not guarantee the order of the nodes in the list, so it is recommended to sort the list before or after using this method. """ tmp_dict = {} for node in self.list: time = round(node.t, 7) try: # Try to access the node and merge if it exists tmp_dict[time].parachutes += node.parachutes tmp_dict[time]._controllers += node._controllers tmp_dict[time].callbacks += node.callbacks tmp_dict[time]._component_sensors += node._component_sensors except KeyError: # If the node does not exist, add it to the dictionary tmp_dict[time] = node self.list = list(tmp_dict.values())
def flush_after(self, index): del self.list[index + 1 :]
[docs] class TimeNode: """TimeNode is a class that represents a time node in the time nodes list. It stores the time, the parachutes and the controllers that are active at that time. This class is supposed to work exclusively within the TimeNodes class. """
[docs] def __init__(self, t, parachutes, controllers, sensors): """Create a TimeNode object. Parameters ---------- t : float Initial time of the time node. parachutes : list[Parachute] List containing all the parachutes that should be evaluated at this time node. controllers : list[_Controller] List containing all the controllers that should be evaluated at this time node. sensors : list[ComponentSensor] List containing all the sensors that should be evaluated at this time node. """ self.t = t self.parachutes = parachutes self.callbacks = [] self._controllers = controllers self._component_sensors = sensors
def __repr__(self): return ( f"<TimeNode(" f"t: {self.t}, " f"parachutes: {len(self.parachutes)}, " f"controllers: {len(self._controllers)}, " f"sensors: {len(self._component_sensors)})>" )
[docs] def __lt__(self, other): """Allows the comparison of two TimeNode objects based on their initial time. This is particularly useful for sorting a list of TimeNode objects. Parameters ---------- other : TimeNode Another TimeNode object to compare with. Returns ------- bool True if the initial time of the current TimeNode is less than the initial time of the other TimeNode, False otherwise. """ return self.t < other.t