def __init__(self, airplane, # type: Airplane op_point, # type: OperatingPoint ): ### Initialize self.airplane = airplane self.op_point = op_point ### Check assumptions assumptions = np.array([ self.op_point.beta == 0, self.op_point.p == 0, self.op_point.q == 0, self.op_point.r == 0, ]) if not assumptions.all(): raise ValueError("The assumptions to use an aero buildup method are not met!") ### Fuselages for fuselage in self.airplane.fuselages: fuselage.Re = self.op_point.reynolds(fuselage.length()) fuselage.CLA = 0 fuselage.CDA = aero.Cf_flat_plate( fuselage.Re * fuselage.area_wetted()) * 1.2 # wetted area with form factor fuselage.lift_force = fuselage.CLA * self.op_point.dynamic_pressure() fuselage.drag_force = fuselage.CDA * self.op_point.dynamic_pressure() fuselage.pitching_moment = 0 ### Wings for wing in self.airplane.wings: wing.alpha = op_point.alpha + wing.mean_twist_angle() # TODO add in allmoving deflections wing.Re = self.op_point.reynolds(wing.mean_aerodynamic_chord()) wing.airfoil = wing.xsecs[0].airfoil ## Lift calculation wing.Cl_incompressible = wing.airfoil.CL_function( alpha=wing.alpha, Re=wing.Re, # TODO finish mach=0, # TODO revisit this - is this right? deflection=0 ) CL_over_Cl = aero.CL_over_Cl( aspect_ratio=wing.aspect_ratio(), mach=op_point.mach(), sweep=wing.mean_sweep_angle() ) wing.CL = wing.Cl_incompressible * CL_over_Cl ## Drag calculation wing.CD_profile = wing.airfoil.CD_function( alpha=wing.alpha, Re=wing.Re, mach=op_point.mach(), deflection=0 ) wing.oswalds_efficiency = aero.oswalds_efficiency( taper_ratio=wing.taper_ratio(), aspect_ratio=wing.aspect_ratio(), sweep=wing.mean_sweep_angle(), ) wing.CD_induced = wing.CL ** 2 / (pi * wing.oswalds_efficiency * wing.aspect_ratio()) ## Moment calculation wing.Cm_incompressible = wing.airfoil.CM_function( alpha=wing.alpha, Re=wing.Re, mach=0, # TODO revisit this - is this right? deflection=0, ) wing.CM = wing.Cm_incompressible * CL_over_Cl ## Force and moment calculation qS = op_point.dynamic_pressure() * wing.area() wing.lift_force = wing.CL * qS wing.drag_force_profile = wing.CD_profile * qS wing.drag_force_induced = wing.CD_induced * qS wing.drag_force = wing.drag_force_profile + wing.drag_force_induced wing.pitching_moment = wing.CM * qS * wing.mean_aerodynamic_chord() ### Total the forces self.lift_force = 0 self.drag_force = 0 self.pitching_moment = 0 for fuselage in self.airplane.fuselages: self.lift_force += fuselage.lift_force self.drag_force += fuselage.drag_force self.pitching_moment += fuselage.pitching_moment for wing in self.airplane.wings: if wing.symmetric: # Only add lift force if the wing is symmetric; a surrogate for "horizontal". self.lift_force += wing.lift_force self.drag_force += wing.drag_force self.pitching_moment += wing.pitching_moment # Raw pitching moment self.pitching_moment += -wing.aerodynamic_center()[0] * wing.lift_force # Pitching moment due to lift ### Calculate nondimensional forces qS = op_point.dynamic_pressure() * self.airplane.s_ref self.CL = self.lift_force / qS self.CD = self.drag_force / qS self.CM = self.pitching_moment / qS / self.airplane.c_ref
def setup( self, verbose=True, # Choose whether or not you want verbose output run_symmetric_if_possible=True, # Choose whether or not you want to run a symmetric_problem analysis about XZ (~4x faster) ): # Runs a point analysis at the specified op-point. ### Fuselages self.fuse_Res = [ self.op_point.compute_reynolds(fuse.length()) for i, fuse in enumerate(self.airplane.fuselages) ] self.CLA_fuses = [0 for i, fuse in enumerate(self.airplane.fuselages)] self.CDA_fuses = [ aero.Cf_flat_plate(self.fuse_Res[i]) * fuse.area_wetted() * 1.2 # wetted area with form factor for i, fuse in enumerate(self.airplane.fuselages) ] self.lift_fuses = [ self.CLA_fuses[i] * self.op_point.dynamic_pressure() for i, fuse in enumerate(self.airplane.fuselages) ] self.drag_fuses = [ self.CDA_fuses[i] * self.op_point.dynamic_pressure() for i, fuse in enumerate(self.airplane.fuselages) ] ### Wings self.wing_Res = [ self.op_point.compute_reynolds(wing.mean_geometric_chord()) for i, wing in enumerate(self.airplane.wings) ] self.wing_airfoils = [ wing.xsecs[0].airfoil # type: asb.Airfoil for i, wing in enumerate(self.airplane.wings) ] self.wing_Cl_incs = [ self.wing_airfoils[i].CL_function( self.op_point.alpha + wing.mean_twist_angle(), self.wing_Res[i], 0, 0) for i, wing in enumerate(self.airplane.wings) ] # Incompressible 2D lift coefficient self.wing_CLs = [ self.wing_Cl_incs[i] * aero.CL_over_Cl(wing.aspect_ratio(), mach=self.op_point.mach, sweep=wing.mean_sweep_angle()) for i, wing in enumerate(self.airplane.wings) ] # Compressible 3D lift coefficient self.lift_wings = [ self.wing_CLs[i] * self.op_point.dynamic_pressure() * wing.area() for i, wing in enumerate(self.airplane.wings) ] self.wing_Cd_profiles = [ self.wing_airfoils[i].CDp_function( self.op_point.alpha + wing.mean_twist_angle(), self.wing_Res[i], self.op_point.mach, 0) for i, wing in enumerate(self.airplane.wings) ] self.drag_wing_profiles = [ self.wing_Cd_profiles[i] * self.op_point.dynamic_pressure() * wing.area() for i, wing in enumerate(self.airplane.wings) ] self.wing_oswalds_efficiencies = [ 0.95 # TODO make this a function of taper ratio for i, wing in enumerate(self.airplane.wings) ] self.drag_wing_induceds = [ self.lift_wings[i]**2 / (self.op_point.dynamic_pressure() * np.pi * wing.span()**2 * self.wing_oswalds_efficiencies[i]) for i, wing in enumerate(self.airplane.wings) ] self.drag_wings = [ self.drag_wing_profiles[i] + self.drag_wing_induceds[i] for i, wing in enumerate(self.airplane.wings) ] self.wing_Cm_incs = [ self.wing_airfoils[i].Cm_function( self.op_point.alpha + wing.mean_twist_angle(), self.wing_Res[i], 0, 0) for i, wing in enumerate(self.airplane.wings) ] # Incompressible 2D moment coefficient self.wing_CMs = [ self.wing_Cm_incs[i] * aero.CL_over_Cl(wing.aspect_ratio(), mach=self.op_point.mach, sweep=wing.mean_sweep_angle()) for i, wing in enumerate(self.airplane.wings) ] # Compressible 3D moment coefficient self.local_moment_wings = [ self.wing_CMs[i] * self.op_point.dynamic_pressure() * wing.area() * wing.mean_geometric_chord() for i, wing in enumerate(self.airplane.wings) ] self.body_moment_wings = [ self.local_moment_wings[i] + wing.approximate_center_of_pressure()[0] * self.lift_wings[i] for i, wing in enumerate(self.airplane.wings) ] # Force totals lift_forces = self.lift_fuses + self.lift_wings drag_forces = self.drag_fuses + self.drag_wings self.lift_force = cas.sum1(cas.vertcat(*lift_forces)) self.drag_force = cas.sum1(cas.vertcat(*drag_forces)) self.side_force = 0 # Moment totals self.pitching_moment = cas.sum1(cas.vertcat(*self.body_moment_wings)) # Calculate nondimensional forces q = self.op_point.dynamic_pressure() s_ref = self.airplane.s_ref b_ref = self.airplane.b_ref c_ref = self.airplane.c_ref self.CL = self.lift_force / q / s_ref self.CD = self.drag_force / q / s_ref self.Cm = self.pitching_moment / q / s_ref / c_ref
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] }