Source code for rocketpy.rocket.aero_surface.generic_surface

import copy
import csv
import math

import numpy as np

from rocketpy.mathutils import Function
from rocketpy.mathutils.vector_matrix import Matrix, Vector


[docs] class GenericSurface: """Defines a generic aerodynamic surface with custom force and moment coefficients. The coefficients can be nonlinear functions of the angle of attack, sideslip angle, Mach number, Reynolds number, pitch rate, yaw rate and roll rate."""
[docs] def __init__( self, reference_area, reference_length, coefficients, center_of_pressure=(0, 0, 0), name="Generic Surface", ): """Create a generic aerodynamic surface, defined by its aerodynamic coefficients. This surface is used to model any aerodynamic surface that does not fit the predefined classes. Important --------- All the aerodynamic coefficients can be input as callable functions of angle of attack, angle of sideslip, Mach number, Reynolds number, pitch rate, yaw rate and roll rate. For CSV files, the header must contain at least one of the following: "alpha", "beta", "mach", "reynolds", "pitch_rate", "yaw_rate" and "roll_rate". The independent variable columns can be provided in any order. See Also -------- :ref:`genericsurfaces`. Parameters ---------- reference_area : int, float Reference area of the aerodynamic surface. Has the unit of meters squared. Commonly defined as the rocket's cross-sectional area. reference_length : int, float Reference length of the aerodynamic surface. Has the unit of meters. Commonly defined as the rocket's diameter. coefficients: dict List of coefficients. If a coefficient is omitted, it is set to 0. The valid coefficients are:\n cL: str, callable, optional Lift coefficient. Can be a path to a CSV file or a callable. Default is 0.\n cQ: str, callable, optional Side force coefficient. Can be a path to a CSV file or a callable. Default is 0.\n cD: str, callable, optional Drag coefficient. Can be a path to a CSV file or a callable. Default is 0.\n cm: str, callable, optional Pitch moment coefficient. Can be a path to a CSV file or a callable. Default is 0.\n cn: str, callable, optional Yaw moment coefficient. Can be a path to a CSV file or a callable. Default is 0.\n cl: str, callable, optional Roll moment coefficient. Can be a path to a CSV file or a callable. Default is 0.\n center_of_pressure : tuple, list, optional Application point of the aerodynamic forces and moments. The center of pressure is defined in the local coordinate system of the aerodynamic surface. The default value is (0, 0, 0). name : str, optional Name of the aerodynamic surface. Default is 'GenericSurface'. """ self.reference_area = reference_area self.reference_length = reference_length self.center_of_pressure = center_of_pressure self.cp = center_of_pressure self.cpx = center_of_pressure[0] self.cpy = center_of_pressure[1] self.cpz = center_of_pressure[2] self.name = name default_coefficients = self._get_default_coefficients() self._check_coefficients(coefficients, default_coefficients) coefficients = self._complete_coefficients(coefficients, default_coefficients) for coeff, coeff_value in coefficients.items(): value = self._process_input(coeff_value, coeff) setattr(self, coeff, value)
[docs] def _get_default_coefficients(self): """Returns default coefficients Returns ------- default_coefficients: dict Dictionary whose keys are the coefficients names and keys are the default values. """ default_coefficients = { "cL": 0, "cQ": 0, "cD": 0, "cm": 0, "cn": 0, "cl": 0, } return default_coefficients
[docs] def _complete_coefficients(self, input_coefficients, default_coefficients): """Creates a copy of the input coefficients dict and fill it with missing keys with default values Parameters ---------- input_coefficients : str, dict Coefficients dictionary passed by the user. If the user only specifies some of the coefficients, the remaining are completed with class default values default_coefficients : dict Default coefficients of the class Returns ------- coefficients : dict Coefficients dictionary used to setup coefficient attributes """ coefficients = copy.deepcopy(input_coefficients) for coeff, value in default_coefficients.items(): if coeff not in coefficients.keys(): coefficients[coeff] = value return coefficients
[docs] def _check_coefficients(self, input_coefficients, default_coefficients): """Check if input coefficients have only valid keys Parameters ---------- input_coefficients : str, dict Coefficients dictionary passed by the user. If the user only specifies some of the coefficients, the remaining are completed with class default values default_coefficients : dict Default coefficients of the class Raises ------ ValueError Raises a value error if the input coefficient has an invalid key """ invalid_keys = set(input_coefficients) - set(default_coefficients) if invalid_keys: raise ValueError( f"Invalid coefficient name(s) used in key(s): {', '.join(invalid_keys)}. " "Check the documentation for valid names." )
[docs] def _compute_from_coefficients( self, rho, stream_speed, alpha, beta, mach, reynolds, pitch_rate, yaw_rate, roll_rate, ): """Compute the aerodynamic forces and moments from the aerodynamic coefficients. Parameters ---------- rho : float Air density. stream_speed : float Magnitude of the airflow speed. alpha : float Angle of attack in radians. beta : float Sideslip angle in radians. mach : float Mach number. reynolds : float Reynolds number. pitch_rate : float Pitch rate in radians per second. yaw_rate : float Yaw rate in radians per second. roll_rate : float Roll rate in radians per second. Returns ------- tuple of float The aerodynamic forces (lift, side_force, drag) and moments (pitch, yaw, roll) in the body frame. """ # Precompute common values dyn_pressure_area = 0.5 * rho * stream_speed**2 * self.reference_area dyn_pressure_area_length = dyn_pressure_area * self.reference_length # Compute aerodynamic forces lift = dyn_pressure_area * self.cL( alpha, beta, mach, reynolds, pitch_rate, yaw_rate, roll_rate ) side = dyn_pressure_area * self.cQ( alpha, beta, mach, reynolds, pitch_rate, yaw_rate, roll_rate ) drag = dyn_pressure_area * self.cD( alpha, beta, mach, reynolds, pitch_rate, yaw_rate, roll_rate ) # Compute aerodynamic moments pitch = dyn_pressure_area_length * self.cm( alpha, beta, mach, reynolds, pitch_rate, yaw_rate, roll_rate ) yaw = dyn_pressure_area_length * self.cn( alpha, beta, mach, reynolds, pitch_rate, yaw_rate, roll_rate ) roll = dyn_pressure_area_length * self.cl( alpha, beta, mach, reynolds, pitch_rate, yaw_rate, roll_rate ) return lift, side, drag, pitch, yaw, roll
[docs] def compute_forces_and_moments( self, stream_velocity, stream_speed, stream_mach, rho, cp, omega, reynolds, ): """Computes the forces and moments acting on the aerodynamic surface. Used in each time step of the simulation. This method is valid for both linear and nonlinear aerodynamic coefficients. Parameters ---------- stream_velocity : tuple of float The velocity of the airflow relative to the surface. stream_speed : float The magnitude of the airflow speed. stream_mach : float The Mach number of the airflow. rho : float Air density. cp : Vector Center of pressure coordinates in the body frame. omega: tuple[float, float, float] Tuple containing angular velocities around the x, y, z axes. reynolds : float Reynolds number. omega: tuple of float Tuple containing angular velocities around the x, y, z axes. Returns ------- tuple of float The aerodynamic forces (lift, side_force, drag) and moments (pitch, yaw, roll) in the body frame. """ # Stream velocity in standard aerodynamic frame stream_velocity = -stream_velocity # Angles of attack and sideslip alpha = np.arctan2(stream_velocity[1], stream_velocity[2]) beta = np.arctan2(stream_velocity[0], stream_velocity[2]) # Compute aerodynamic forces and moments lift, side, drag, pitch, yaw, roll = self._compute_from_coefficients( rho, stream_speed, alpha, beta, stream_mach, reynolds, omega[0], # q omega[1], # r omega[2], # p ) # Conversion from aerodynamic frame to body frame rotation_matrix = Matrix( [ [1, 0, 0], [0, math.cos(alpha), -math.sin(alpha)], [0, math.sin(alpha), math.cos(alpha)], ] ) @ Matrix( [ [math.cos(beta), 0, -math.sin(beta)], [0, 1, 0], [math.sin(beta), 0, math.cos(beta)], ] ) R1, R2, R3 = rotation_matrix @ Vector([side, -lift, -drag]) # Dislocation of the aerodynamic application point to CDM M1, M2, M3 = Vector([pitch, yaw, roll]) + (cp ^ Vector([R1, R2, R3])) return R1, R2, R3, M1, M2, M3
[docs] def _process_input(self, input_data, coeff_name): """Process the input data, either as a CSV file or a callable function. Parameters ---------- input_data : str or callable Input data to be processed, either a path to a CSV or a callable. coeff_name : str Name of the coefficient being processed for error reporting. Returns ------- Function Function object with 7 input arguments (alpha, beta, mach, reynolds, pitch_rate, yaw_rate, roll_rate). """ if isinstance(input_data, str): # Input is assumed to be a file path to a CSV return self.__load_generic_surface_csv(input_data, coeff_name) elif isinstance(input_data, Function): if input_data.__dom_dim__ != 7: raise ValueError( f"{coeff_name} function must have 7 input arguments" " (alpha, beta, mach, reynolds, pitch_rate, yaw_rate, roll_rate)." ) return input_data elif callable(input_data): # Check if callable has 7 inputs (alpha, beta, mach, reynolds, pitch_rate, yaw_rate, roll_rate) if input_data.__code__.co_argcount != 7: raise ValueError( f"{coeff_name} function must have 7 input arguments" " (alpha, beta, mach, reynolds, pitch_rate, yaw_rate, roll_rate)." ) return Function( input_data, [ "alpha", "beta", "mach", "reynolds", "pitch_rate", "yaw_rate", "roll_rate", ], [coeff_name], interpolation="linear", extrapolation="natural", ) elif input_data == 0: return Function( lambda alpha, beta, mach, reynolds, pitch_rate, yaw_rate, roll_rate: 0, [ "alpha", "beta", "mach", "reynolds", "pitch_rate", "yaw_rate", "roll_rate", ], [coeff_name], interpolation="linear", extrapolation="natural", ) else: raise TypeError( f"Invalid input for {coeff_name}: must be a CSV file path" " or a callable." )
def __load_generic_surface_csv(self, file_path, coeff_name): # pylint: disable=too-many-statements,import-outside-toplevel """Load GenericSurface coefficient CSV into a 7D Function. This loader expects header-based CSV data with one or more independent variables among: alpha, beta, mach, reynolds, pitch_rate, yaw_rate, roll_rate. """ independent_vars = [ "alpha", "beta", "mach", "reynolds", "pitch_rate", "yaw_rate", "roll_rate", ] try: with open(file_path, mode="r") as file: reader = csv.reader(file) header = next(reader) except (FileNotFoundError, IOError) as e: raise ValueError(f"Error reading {coeff_name} CSV file: {e}") from e except StopIteration as e: raise ValueError(f"Invalid or empty CSV file for {coeff_name}.") from e if not header: raise ValueError(f"Invalid or empty CSV file for {coeff_name}.") header = [column.strip() for column in header] present_columns = [col for col in independent_vars if col in header] invalid_columns = [col for col in header[:-1] if col not in independent_vars] if invalid_columns: raise ValueError( f"Invalid independent variable(s) in {coeff_name} CSV: " f"{invalid_columns}. Valid options are: {independent_vars}." ) if header[-1] in independent_vars: raise ValueError( f"Last column in {coeff_name} CSV must be the coefficient" " value, not an independent variable." ) if not present_columns: raise ValueError(f"No independent variables found in {coeff_name} CSV.") ordered_present_columns = [ col for col in header[:-1] if col in independent_vars ] csv_func = Function.from_regular_grid_csv( file_path, ordered_present_columns, coeff_name, extrapolation="natural", ) if csv_func is None: csv_func = Function( file_path, interpolation="linear", extrapolation="natural", ) def wrapper(alpha, beta, mach, reynolds, pitch_rate, yaw_rate, roll_rate): args_by_name = { "alpha": alpha, "beta": beta, "mach": mach, "reynolds": reynolds, "pitch_rate": pitch_rate, "yaw_rate": yaw_rate, "roll_rate": roll_rate, } selected_args = [args_by_name[col] for col in ordered_present_columns] return csv_func(*selected_args) return Function( wrapper, independent_vars, [coeff_name], interpolation="linear", extrapolation="natural", )