def solar_azimuth_angle(latitude, day_of_year, time): """ Azimuth angle of the sun [degrees] for a local observer. :param latitude: Latitude [degrees] :param day_of_year: Julian day (1 == Jan. 1, 365 == Dec. 31) :param time: Time after local solar noon [seconds] :return: Solar azimuth angle [degrees] (the compass direction from which the sunlight is coming). """ # Solar azimuth angle (including seasonality, latitude, and time of day) # Source: https://www.pveducation.org/pvcdrom/properties-of-sunlight/azimuth-angle declination = declination_angle(day_of_year) sdec = np.sind(declination) cdec = np.cosd(declination) slat = np.sind(latitude) clat = np.cosd(latitude) ctime = np.cosd(time / 86400 * 360) elevation = solar_elevation_angle(latitude, day_of_year, time) cele = np.cosd(elevation) cos_azimuth = (sdec * clat - cdec * slat * ctime) / cele cos_azimuth = np.clip(cos_azimuth, -1, 1) azimuth_raw = np.arccosd(cos_azimuth) is_solar_morning = np.mod(time, 86400) > 43200 solar_azimuth_angle = np.where( is_solar_morning, azimuth_raw, 360 - azimuth_raw ) return solar_azimuth_angle
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] }
def fuselage_aerodynamics( self, fuselage: Fuselage, ): """ Estimates the aerodynamic forces, moments, and derivatives on a fuselage in isolation. Assumes: * The fuselage is a body of revolution aligned with the x_b axis. * The angle between the nose and the freestream is less than 90 degrees. Moments are given with the reference at Fuselage [0, 0, 0]. Uses methods from Jorgensen, Leland Howard. "Prediction of Static Aerodynamic Characteristics for Slender Bodies Alone and with Lifting Surfaces to Very High Angles of Attack". NASA TR R-474. 1977. Args: fuselage: A Fuselage object that you wish to analyze. Returns: """ ##### Alias a few things for convenience op_point = self.op_point Re = op_point.reynolds(reference_length=fuselage.length()) fuse_options = self.get_options(fuselage) ####### Reference quantities (Set these 1 here, just so we can follow Jorgensen syntax.) # Outputs of this function should be invariant of these quantities, if normalization has been done correctly. S_ref = 1 # m^2 c_ref = 1 # m ####### Fuselage zero-lift drag estimation ### Forebody drag C_f_forebody = aerolib.Cf_flat_plate(Re_L=Re) ### Base Drag C_D_base = 0.029 / np.sqrt(C_f_forebody) * fuselage.area_base() / S_ref ### Skin friction drag C_D_skin = C_f_forebody * fuselage.area_wetted() / S_ref ### Wave drag if self.include_wave_drag: sears_haack_drag = transonic.sears_haack_drag_from_volume( volume=fuselage.volume(), length=fuselage.length()) C_D_wave = transonic.approximate_CD_wave( mach=op_point.mach(), mach_crit=critical_mach( fineness_ratio_nose=fuse_options["nose_fineness_ratio"]), CD_wave_at_fully_supersonic=fuse_options["E_wave_drag"] * sears_haack_drag, ) else: C_D_wave = 0 ### Total zero-lift drag C_D_zero_lift = C_D_skin + C_D_base + C_D_wave ####### Jorgensen model ### First, merge the alpha and beta into a single "generalized alpha", which represents the degrees between the fuselage axis and the freestream. x_w, y_w, z_w = op_point.convert_axes(1, 0, 0, from_axes="body", to_axes="wind") generalized_alpha = np.arccosd(x_w / (1 + 1e-14)) sin_generalized_alpha = np.sind(generalized_alpha) cos_generalized_alpha = x_w # ### Limit generalized alpha to -90 < alpha < 90, for now. # generalized_alpha = np.clip(generalized_alpha, -90, 90) # # TODO make the drag/moment functions not give negative results for alpha > 90. alpha_fractional_component = -z_w / np.sqrt( y_w**2 + z_w**2 + 1e-16 ) # The fraction of any "generalized lift" to be in the direction of alpha beta_fractional_component = y_w / np.sqrt( y_w**2 + z_w**2 + 1e-16 ) # The fraction of any "generalized lift" to be in the direction of beta ### Compute normal quantities ### Note the (N)ormal, (A)ligned coordinate system. (See Jorgensen for definitions.) # M_n = sin_generalized_alpha * op_point.mach() Re_n = sin_generalized_alpha * Re # V_n = sin_generalized_alpha * op_point.velocity q = op_point.dynamic_pressure() x_nose = fuselage.xsecs[0].xyz_c[0] x_m = 0 - x_nose x_c = fuselage.x_centroid_projected() - x_nose ##### Potential flow crossflow model C_N_p = ( # Normal force coefficient due to potential flow. (Jorgensen Eq. 2.12, part 1) fuselage.area_base() / S_ref * np.sind(2 * generalized_alpha) * np.cosd(generalized_alpha / 2)) C_m_p = ((fuselage.volume() - fuselage.area_base() * (fuselage.length() - x_m)) / (S_ref * c_ref) * np.sind(2 * generalized_alpha) * np.cosd(generalized_alpha / 2)) ##### Viscous crossflow model C_d_n = np.where( Re_n != 0, aerolib.Cd_cylinder( Re_D=Re_n ), # Replace with 1.20 from Jorgensen Table 1 if not working well 0) eta = jorgensen_eta(fuselage.fineness_ratio()) C_N_v = ( # Normal force coefficient due to viscous crossflow. (Jorgensen Eq. 2.12, part 2) eta * C_d_n * fuselage.area_projected() / S_ref * sin_generalized_alpha**2) C_m_v = (eta * C_d_n * fuselage.area_projected() / S_ref * (x_m - x_c) / c_ref * sin_generalized_alpha**2) ##### Total C_N model C_N = C_N_p + C_N_v C_m_generalized = C_m_p + C_m_v ##### Total C_A model C_A = C_D_zero_lift * cos_generalized_alpha * np.abs( cos_generalized_alpha) ##### Convert to lift, drag C_L_generalized = C_N * cos_generalized_alpha - C_A * sin_generalized_alpha C_D = C_N * sin_generalized_alpha + C_A * cos_generalized_alpha ### Set proper directions C_L = C_L_generalized * alpha_fractional_component C_Y = -C_L_generalized * beta_fractional_component C_l = 0 C_m = C_m_generalized * alpha_fractional_component C_n = -C_m_generalized * beta_fractional_component ### Un-normalize L = C_L * q * S_ref Y = C_Y * q * S_ref D = C_D * q * S_ref l_w = C_l * q * S_ref * c_ref m_w = C_m * q * S_ref * c_ref n_w = C_n * q * S_ref * c_ref ### Convert to axes coordinates for reporting F_w = (-D, Y, -L) F_b = op_point.convert_axes(*F_w, from_axes="wind", to_axes="body") F_g = op_point.convert_axes(*F_b, from_axes="body", to_axes="geometry") M_w = ( l_w, m_w, n_w, ) M_b = op_point.convert_axes(*M_w, from_axes="wind", to_axes="body") M_g = op_point.convert_axes(*M_b, from_axes="body", to_axes="geometry") 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] }
def fuselage_aerodynamics(self, fuselage: Fuselage, ) -> Dict[str, Any]: """ Estimates the aerodynamic forces, moments, and derivatives on a fuselage in isolation. Assumes: * The fuselage is a body of revolution aligned with the x_b axis. * The angle between the nose and the freestream is less than 90 degrees. Moments are given with the reference at Fuselage [0, 0, 0]. Uses methods from Jorgensen, Leland Howard. "Prediction of Static Aerodynamic Characteristics for Slender Bodies Alone and with Lifting Surfaces to Very High Angles of Attack". NASA TR R-474. 1977. Args: fuselage: A Fuselage object that you wish to analyze. Returns: """ ##### Alias a few things for convenience op_point = self.op_point Re = op_point.reynolds(reference_length=fuselage.length()) fuse_options = self.get_options(fuselage) ####### Jorgensen model ### First, merge the alpha and beta into a single "generalized alpha", which represents the degrees between the fuselage axis and the freestream. x_w, y_w, z_w = op_point.convert_axes( 1, 0, 0, from_axes="body", to_axes="wind" ) generalized_alpha = np.arccosd(x_w / (1 + 1e-14)) sin_generalized_alpha = (y_w ** 2 + z_w ** 2 + 1e-14) ** 0.5 # sin_generalized_alpha = np.sind(generalized_alpha) cos_generalized_alpha = x_w sin_squared_generalized_alpha = y_w ** 2 + z_w ** 2 # ### Limit generalized alpha to -90 < alpha < 90, for now. # generalized_alpha = np.clip(generalized_alpha, -90, 90) # # TODO make the drag/moment functions not give negative results for alpha > 90. alpha_fractional_component = -z_w / np.sqrt( # This is positive when alpha is positive y_w ** 2 + z_w ** 2 + 1e-16) # The fraction of any "generalized lift" to be in the direction of alpha beta_fractional_component = -y_w / np.sqrt( # This is positive when beta is positive y_w ** 2 + z_w ** 2 + 1e-16) # The fraction of any "generalized lift" to be in the direction of beta ### Compute normal quantities ### Note the (N)ormal, (A)ligned coordinate system. (See Jorgensen for definitions.) q = op_point.dynamic_pressure() eta = jorgensen_eta(fuselage.fineness_ratio()) def forces_on_fuselage_section( xsec_a: FuselageXSec, xsec_b: FuselageXSec, ): ### Some metrics, like effective force location, are area-weighted. Here, we compute those weights. r_a = xsec_a.radius r_b = xsec_b.radius x_a = xsec_a.xyz_c[0] x_b = xsec_b.xyz_c[0] area_a = xsec_a.xsec_area() area_b = xsec_b.xsec_area() total_area = area_a + area_b a_weight = area_a / total_area b_weight = area_b / total_area delta_x = x_b - x_a mean_geometric_radius = (r_a + r_b) / 2 mean_aerodynamic_radius = r_a * a_weight + r_b * b_weight force_x_location = x_a * a_weight + x_b * b_weight ##### Inviscid Forces force_potential_flow = q * ( # From Munk, via Jorgensen np.sind(2 * generalized_alpha) * (area_b - area_a) ) # Matches Drela, Flight Vehicle Aerodynamics Eqn. 6.75 in the small-alpha limit. # Note that no delta_x should be here; dA/dx * dx = dA. # Direction of force is midway between the normal to the axis of revolution of the body and the # normal to the free-stream velocity, according to: # Ward, via Jorgensen force_normal_potential_flow = force_potential_flow * np.cosd(generalized_alpha / 2) force_axial_potential_flow = -force_potential_flow * np.sind(generalized_alpha / 2) # Reminder: axial force is defined positive-aft ##### Viscous Forces Re_n = sin_generalized_alpha * op_point.reynolds(reference_length=2 * mean_aerodynamic_radius) M_n = sin_generalized_alpha * op_point.mach() C_d_n = np.where( Re_n != 0, aerolib.Cd_cylinder( Re_D=Re_n, mach=M_n ), # Replace with 1.20 from Jorgensen Table 1 if this isn't working well 0, ) force_viscous_flow = delta_x * q * ( 2 * eta * C_d_n * sin_squared_generalized_alpha * mean_geometric_radius ) # Viscous crossflow acts exactly normal to vehicle axis, definitionally. (Axial forces accounted for on a total-body basis) force_normal_viscous_flow = force_viscous_flow force_axial_viscous_flow = 0 normal_force = force_normal_potential_flow + force_normal_viscous_flow axial_force = force_axial_potential_flow + force_axial_viscous_flow return normal_force, axial_force, force_x_location normal_force_contributions = [] axial_force_contributions = [] force_x_locations = [] for xsec_a, xsec_b in zip( fuselage.xsecs[:-1], fuselage.xsecs[1:] ): normal_force_contribution, axial_force_contribution, force_x_location = \ forces_on_fuselage_section( xsec_a, xsec_b ) normal_force_contributions.append(normal_force_contribution) axial_force_contributions.append(axial_force_contribution) force_x_locations.append(force_x_location) ##### Add up all forces normal_force = sum(normal_force_contributions) axial_force = sum(axial_force_contributions) generalized_pitching_moment = sum( [ -force * x for force, x in zip(normal_force_contributions, force_x_locations) ] ) ##### Add in profile drag: viscous drag forces and wave drag forces ### Base Drag base_drag_coefficient = fuselage_base_drag_coefficient(mach=op_point.mach()) drag_base = base_drag_coefficient * fuselage.area_base() * q * cos_generalized_alpha ** 2 # One cosine from q dependency, one cosine from direction of drag force ### Skin friction drag C_f_forebody = aerolib.Cf_flat_plate(Re_L=Re) drag_skin = C_f_forebody * fuselage.area_wetted() * q ### Wave drag S_ref = 1 # Does not matter here, just for accounting. if self.include_wave_drag: sears_haack_drag_area = transonic.sears_haack_drag_from_volume( volume=fuselage.volume(), length=fuselage.length() ) # Units of area sears_haack_C_D_wave = sears_haack_drag_area / S_ref C_D_wave = transonic.approximate_CD_wave( mach=op_point.mach(), mach_crit=critical_mach( fineness_ratio_nose=fuse_options["nose_fineness_ratio"] ), CD_wave_at_fully_supersonic=fuse_options["E_wave_drag"] * sears_haack_C_D_wave, ) else: C_D_wave = 0 drag_wave = C_D_wave * q * S_ref ### Sum up the profile drag drag_profile = drag_base + drag_skin + drag_wave ##### Convert Normal/Axial to Lift/Drag, but still in generalized (2D-esque) coordinates L_generalized = normal_force * cos_generalized_alpha - axial_force * sin_generalized_alpha D = normal_force * sin_generalized_alpha + axial_force * cos_generalized_alpha + drag_profile ##### Convert from generalized (2D-esque) coordinates to full 3D L = L_generalized * alpha_fractional_component Y = -L_generalized * beta_fractional_component l_w = 0 # No roll moment m_w = generalized_pitching_moment * alpha_fractional_component n_w = -generalized_pitching_moment * beta_fractional_component ##### Convert to various axes coordinates for reporting F_w = ( -D, Y, -L ) F_b = op_point.convert_axes(*F_w, from_axes="wind", to_axes="body") F_g = op_point.convert_axes(*F_b, from_axes="body", to_axes="geometry") M_w = ( l_w, m_w, n_w, ) M_b = op_point.convert_axes(*M_w, from_axes="wind", to_axes="body") M_g = op_point.convert_axes(*M_b, from_axes="body", to_axes="geometry") ##### Return 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" : L, "Y" : Y, "D" : D, "l_b": M_b[0], "m_b": M_b[1], "n_b": M_b[2], }