def test_cross_1D_input(): a = np.array([1, 1, 1]) b = np.array([1, 2, 3]) cas_a = cas.DM(a) cas_b = cas.DM(b) correct_result = np.cross(a, b) cas_correct_result = cas.DM(correct_result) assert np.all(np.cross(a, cas_b) == cas_correct_result) assert np.all(np.cross(cas_a, b) == cas_correct_result) assert np.all(np.cross(cas_a, cas_b) == cas_correct_result)
def test_cross_2D_input_first_axis(): a = np.tile(np.array([1, 1, 1]), (3, 1)).T b = np.tile(np.array([1, 2, 3]), (3, 1)).T cas_a = cas.DM(a) cas_b = cas.DM(b) correct_result = np.cross(a, b, axis=0) cas_correct_result = cas.DM(correct_result) assert np.all(np.cross(a, cas_b, axis=0) == cas_correct_result) assert np.all(np.cross(cas_a, b, axis=0) == cas_correct_result) assert np.all(np.cross(cas_a, cas_b, axis=0) == cas_correct_result)
def _compute_frame_of_WingXSec(self, index: int): twist = self.xsecs[index].twist if index == len(self.xsecs) - 1: index = len(self.xsecs) - 2 # The last WingXSec has the same frame as the last section. ### Compute the untwisted reference frame xg_local = np.array([1, 0, 0]) xyz_le_a = self.xsecs[index].xyz_le xyz_le_b = self.xsecs[index + 1].xyz_le vector_between = xyz_le_b - xyz_le_a vector_between[0] = 0 # Project it onto the YZ plane. yg_local = vector_between / np.linalg.norm(vector_between) zg_local = np.cross(xg_local, yg_local) ### Twist the reference frame by the WingXSec twist angle rot = np.rotation_matrix_3D( twist * pi / 180, yg_local ) xg_local = rot @ xg_local zg_local = rot @ zg_local return xg_local, yg_local, zg_local
def _compute_frame_of_section(self, index: int) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: """ Computes the local reference frame associated with a particular section. (Note that sections and cross sections are different! Cross sections, or xsecs, are the vertices, and sections are the parts in between. In other words, a wing with N cross sections (xsecs) will always have N-1 sections. Args: index: Which section should we get the frame of? If given `i`, this retrieves the frame of the section between xsecs `i` and `i+1`. Returns: A tuple of (xg_local, yg_local, zg_local), where each entry refers to the respective (normalized) axis of the local reference frame of the section. Given in geometry axes. """ in_front = self._compute_xyz_le_of_WingXSec(index) in_back = self._compute_xyz_te_of_WingXSec(index) out_front = self._compute_xyz_le_of_WingXSec(index + 1) out_back = self._compute_xyz_te_of_WingXSec(index + 1) diag1 = out_back - in_front diag2 = out_front - in_back cross = np.cross(diag1, diag2) zg_local = cross / np.linalg.norm(cross) quarter_chord_vector = ( 0.75 * out_front + 0.25 * out_back ) - ( 0.75 * in_front + 0.25 * in_back ) quarter_chord_vector[0] = 0 yg_local = quarter_chord_vector / np.linalg.norm(quarter_chord_vector) xg_local = np.cross(yg_local, zg_local) return xg_local, yg_local, zg_local
def _compute_frame_of_WingXSec( self, index: int) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: """ Computes the local reference frame associated with a particular cross section (XSec) of this wing. Args: index: Which cross section (as indexed in Wing.xsecs) should we get the frame of? Returns: A tuple of (xg_local, yg_local, zg_local), where each entry refers to the respective (normalized) axis of the local reference frame of the WingXSec. Given in geometry axes. """ ### Compute the untwisted reference frame xg_local = np.array([1, 0, 0]) if index == 0: span_vector = self.xsecs[1].xyz_le - self.xsecs[0].xyz_le span_vector[0] = 0 yg_local = span_vector / np.linalg.norm(span_vector) z_scale = 1 elif index == len(self.xsecs) - 1 or index == -1: span_vector = self.xsecs[-1].xyz_le - self.xsecs[-2].xyz_le span_vector[0] = 0 yg_local = span_vector / np.linalg.norm(span_vector) z_scale = 1 else: vector_before = self.xsecs[index].xyz_le - self.xsecs[index - 1].xyz_le vector_after = self.xsecs[index + 1].xyz_le - self.xsecs[index].xyz_le vector_before[0] = 0 # Project onto YZ plane. vector_after[0] = 0 # Project onto YZ plane. vector_before = vector_before / np.linalg.norm(vector_before) vector_after = vector_after / np.linalg.norm(vector_after) span_vector = (vector_before + vector_after) / 2 yg_local = span_vector / np.linalg.norm(span_vector) cos_vectors = np.linalg.inner(vector_before, vector_after) z_scale = np.sqrt(2 / (cos_vectors + 1)) zg_local = np.cross(xg_local, yg_local) * z_scale ### Twist the reference frame by the WingXSec twist angle rot = np.rotation_matrix_3D(self.xsecs[index].twist * pi / 180, yg_local) xg_local = rot @ xg_local zg_local = rot @ zg_local return xg_local, yg_local, zg_local
def wing_aerodynamics( self, wing: Wing, ): """ Estimates the aerodynamic forces, moments, and derivatives on a wing in isolation. Moments are given with the reference at Wing [0, 0, 0]. Args: wing: A Wing object that you wish to analyze. op_point: The OperatingPoint that you wish to analyze the fuselage at. TODO account for wing airfoil pitching moment Returns: """ ##### Alias a few things for convenience op_point = self.op_point wing_options = self.get_options(wing) ##### Compute general wing properties and things to be used in sectional analysis. sweep = wing.mean_sweep_angle() AR = wing.aspect_ratio() mach = op_point.mach() q = op_point.dynamic_pressure() CL_over_Cl = aerolib.CL_over_Cl(aspect_ratio=AR, mach=mach, sweep=sweep) oswalds_efficiency = aerolib.oswalds_efficiency( taper_ratio=wing.taper_ratio(), aspect_ratio=AR, sweep=sweep, fuselage_diameter_to_span_ratio=0 # an assumption ) areas = wing.area(_sectional=True) aerodynamic_centers = wing.aerodynamic_center(_sectional=True) F_g = [0, 0, 0] M_g = [0, 0, 0] ##### Iterate through the wing sections. for sect_id in range(len(wing.xsecs) - 1): ##### Identify the wing cross sections adjacent to this wing section. xsec_a = wing.xsecs[sect_id] xsec_b = wing.xsecs[sect_id + 1] ##### When linearly interpolating, weight things by the relative chord. a_weight = xsec_a.chord / (xsec_a.chord + xsec_b.chord) b_weight = xsec_b.chord / (xsec_a.chord + xsec_b.chord) ##### Compute the local frame of this section, and put the z (normal) component into wind axes. xg_local, yg_local, zg_local = wing._compute_frame_of_section( sect_id) sect_aerodynamic_center = aerodynamic_centers[sect_id] sect_z_w = op_point.convert_axes( x_from=zg_local[0], y_from=zg_local[1], z_from=zg_local[2], from_axes="geometry", to_axes="wind", ) ##### Compute the generalized angle of attack, so the geometric alpha that the wing section "sees". velocity_vector_b_from_freestream = op_point.convert_axes( x_from=-op_point.velocity, y_from=0, z_from=0, from_axes="wind", to_axes="body") velocity_vector_b_from_rotation = np.cross(op_point.convert_axes( sect_aerodynamic_center[0], sect_aerodynamic_center[1], sect_aerodynamic_center[2], from_axes="geometry", to_axes="body"), [op_point.p, op_point.q, op_point.r], manual=True) velocity_vector_b = [ velocity_vector_b_from_freestream[i] + velocity_vector_b_from_rotation[i] for i in range(3) ] velocity_mag_b = np.sqrt( sum([comp**2 for comp in velocity_vector_b])) velocity_dir_b = [ velocity_vector_b[i] / velocity_mag_b for i in range(3) ] sect_z_b = op_point.convert_axes( x_from=zg_local[0], y_from=zg_local[1], z_from=zg_local[2], from_axes="geometry", to_axes="body", ) vel_dot_normal = np.dot(velocity_dir_b, sect_z_b, manual=True) sect_alpha_generalized = 90 - np.arccosd(vel_dot_normal) def get_deflection(xsec): n_surfs = len(xsec.control_surfaces) if n_surfs == 0: return 0 elif n_surfs == 1: surf = xsec.control_surfaces[0] return surf.deflection else: raise NotImplementedError( "AeroBuildup currently cannot handle multiple control surfaces attached to a given WingXSec." ) ##### Compute sectional lift at cross sections using lookup functions. Merge them linearly to get section CL. xsec_a_Cl_incompressible = xsec_a.airfoil.CL_function( alpha=sect_alpha_generalized, Re=op_point.reynolds(xsec_a.chord), mach= 0, # Note: this is correct, mach correction happens in 2D -> 3D step deflection=get_deflection(xsec_a)) xsec_b_Cl_incompressible = xsec_b.airfoil.CL_function( alpha=sect_alpha_generalized, Re=op_point.reynolds(xsec_b.chord), mach= 0, # Note: this is correct, mach correction happens in 2D -> 3D step deflection=get_deflection(xsec_b)) sect_CL = (xsec_a_Cl_incompressible * a_weight + xsec_b_Cl_incompressible * b_weight) * CL_over_Cl ##### Compute sectional drag at cross sections using lookup functions. Merge them linearly to get section CD. xsec_a_Cd_profile = xsec_a.airfoil.CD_function( alpha=sect_alpha_generalized, Re=op_point.reynolds(xsec_a.chord), mach=mach, deflection=get_deflection(xsec_a)) xsec_b_Cd_profile = xsec_b.airfoil.CD_function( alpha=sect_alpha_generalized, Re=op_point.reynolds(xsec_b.chord), mach=mach, deflection=get_deflection(xsec_b)) sect_CDp = (xsec_a_Cd_profile * a_weight + xsec_b_Cd_profile * b_weight) ##### Compute induced drag from local CL and full-wing properties (AR, e) sect_CDi = (sect_CL**2 / (np.pi * AR * oswalds_efficiency)) ##### Total the drag. sect_CD = sect_CDp + sect_CDi ##### Go to dimensional quantities using the area. area = areas[sect_id] sect_L = q * area * sect_CL sect_D = q * area * sect_CD ##### Compute the direction of the lift by projecting the section's normal vector into the plane orthogonal to the freestream. sect_L_direction_w = (np.zeros_like(sect_z_w[0]), sect_z_w[1] / np.sqrt(sect_z_w[1]**2 + sect_z_w[2]**2), sect_z_w[2] / np.sqrt(sect_z_w[1]**2 + sect_z_w[2]**2)) sect_L_direction_g = op_point.convert_axes(*sect_L_direction_w, from_axes="wind", to_axes="geometry") ##### Compute the direction of the drag by aligning the drag vector with the freestream vector. sect_D_direction_w = (-1, 0, 0) sect_D_direction_g = op_point.convert_axes(*sect_D_direction_w, from_axes="wind", to_axes="geometry") ##### Compute the force vector in geometry axes. sect_F_g = [ sect_L * sect_L_direction_g[i] + sect_D * sect_D_direction_g[i] for i in range(3) ] ##### Compute the moment vector in geometry axes. sect_M_g = np.cross(sect_aerodynamic_center, sect_F_g, manual=True) ##### Add section forces and moments to overall forces and moments F_g = [F_g[i] + sect_F_g[i] for i in range(3)] M_g = [M_g[i] + sect_M_g[i] for i in range(3)] ##### Treat symmetry if wing.symmetric: ##### Compute the local frame of this section, and put the z (normal) component into wind axes. sym_sect_aerodynamic_center = aerodynamic_centers[sect_id] sym_sect_aerodynamic_center[1] *= -1 sym_sect_z_w = op_point.convert_axes( x_from=zg_local[0], y_from=-zg_local[1], z_from=zg_local[2], from_axes="geometry", to_axes="wind", ) ##### Compute the generalized angle of attack, so the geometric alpha that the wing section "sees". sym_velocity_vector_b_from_freestream = op_point.convert_axes( x_from=-op_point.velocity, y_from=0, z_from=0, from_axes="wind", to_axes="body") sym_velocity_vector_b_from_rotation = np.cross( op_point.convert_axes(sym_sect_aerodynamic_center[0], sym_sect_aerodynamic_center[1], sym_sect_aerodynamic_center[2], from_axes="geometry", to_axes="body"), [op_point.p, op_point.q, op_point.r], manual=True) sym_velocity_vector_b = [ sym_velocity_vector_b_from_freestream[i] + sym_velocity_vector_b_from_rotation[i] for i in range(3) ] sym_velocity_mag_b = np.sqrt( sum([comp**2 for comp in sym_velocity_vector_b])) sym_velocity_dir_b = [ sym_velocity_vector_b[i] / sym_velocity_mag_b for i in range(3) ] sym_sect_z_b = op_point.convert_axes( x_from=zg_local[0], y_from=-zg_local[1], z_from=zg_local[2], from_axes="geometry", to_axes="body", ) sym_vel_dot_normal = np.dot(sym_velocity_dir_b, sym_sect_z_b, manual=True) sym_sect_alpha_generalized = 90 - np.arccosd( sym_vel_dot_normal) def get_deflection(xsec): n_surfs = len(xsec.control_surfaces) if n_surfs == 0: return 0 elif n_surfs == 1: surf = xsec.control_surfaces[0] return surf.deflection if surf.symmetric else -surf.deflection else: raise NotImplementedError( "AeroBuildup currently cannot handle multiple control surfaces attached to a given WingXSec." ) ##### Compute sectional lift at cross sections using lookup functions. Merge them linearly to get section CL. sym_xsec_a_Cl_incompressible = xsec_a.airfoil.CL_function( alpha=sym_sect_alpha_generalized, Re=op_point.reynolds(xsec_a.chord), mach= 0, # Note: this is correct, mach correction happens in 2D -> 3D step deflection=get_deflection(xsec_a)) sym_xsec_b_Cl_incompressible = xsec_b.airfoil.CL_function( alpha=sym_sect_alpha_generalized, Re=op_point.reynolds(xsec_b.chord), mach= 0, # Note: this is correct, mach correction happens in 2D -> 3D step deflection=get_deflection(xsec_b)) sym_sect_CL = ( sym_xsec_a_Cl_incompressible * a_weight + sym_xsec_b_Cl_incompressible * b_weight) * CL_over_Cl ##### Compute sectional drag at cross sections using lookup functions. Merge them linearly to get section CD. sym_xsec_a_Cd_profile = xsec_a.airfoil.CD_function( alpha=sym_sect_alpha_generalized, Re=op_point.reynolds(xsec_a.chord), mach=mach, deflection=get_deflection(xsec_a)) sym_xsec_b_Cd_profile = xsec_b.airfoil.CD_function( alpha=sym_sect_alpha_generalized, Re=op_point.reynolds(xsec_b.chord), mach=mach, deflection=get_deflection(xsec_b)) sym_sect_CDp = (sym_xsec_a_Cd_profile * a_weight + sym_xsec_b_Cd_profile * b_weight) ##### Compute induced drag from local CL and full-wing properties (AR, e) sym_sect_CDi = (sym_sect_CL**2 / (np.pi * AR * oswalds_efficiency)) ##### Total the drag. sym_sect_CD = sym_sect_CDp + sym_sect_CDi ##### Go to dimensional quantities using the area. area = areas[sect_id] sym_sect_L = q * area * sym_sect_CL sym_sect_D = q * area * sym_sect_CD ##### Compute the direction of the lift by projecting the section's normal vector into the plane orthogonal to the freestream. sym_sect_L_direction_w = ( np.zeros_like(sym_sect_z_w[0]), sym_sect_z_w[1] / np.sqrt(sym_sect_z_w[1]**2 + sym_sect_z_w[2]**2), sym_sect_z_w[2] / np.sqrt(sym_sect_z_w[1]**2 + sym_sect_z_w[2]**2)) sym_sect_L_direction_g = op_point.convert_axes( *sym_sect_L_direction_w, from_axes="wind", to_axes="geometry") ##### Compute the direction of the drag by aligning the drag vector with the freestream vector. sym_sect_D_direction_w = (-1, 0, 0) sym_sect_D_direction_g = op_point.convert_axes( *sym_sect_D_direction_w, from_axes="wind", to_axes="geometry") ##### Compute the force vector in geometry axes. sym_sect_F_g = [ sym_sect_L * sym_sect_L_direction_g[i] + sym_sect_D * sym_sect_D_direction_g[i] for i in range(3) ] ##### Compute the moment vector in geometry axes. sym_sect_M_g = np.cross(sym_sect_aerodynamic_center, sym_sect_F_g, manual=True) ##### Add section forces and moments to overall forces and moments F_g = [F_g[i] + sym_sect_F_g[i] for i in range(3)] M_g = [M_g[i] + sym_sect_M_g[i] for i in range(3)] ##### Convert F_g and M_g to body and wind axes for reporting. F_b = op_point.convert_axes(*F_g, from_axes="geometry", to_axes="body") F_w = op_point.convert_axes(*F_b, from_axes="body", to_axes="wind") M_b = op_point.convert_axes(*M_g, from_axes="geometry", to_axes="body") M_w = op_point.convert_axes(*M_b, from_axes="body", to_axes="wind") return { "F_g": F_g, "F_b": F_b, "F_w": F_w, "M_g": M_g, "M_b": M_b, "M_w": M_w, "L": -F_w[2], "Y": F_w[1], "D": -F_w[0], "l_b": M_b[0], "m_b": M_b[1], "n_b": M_b[2] }