def plot_Cf_flat_plates(): from aerosandbox.tools.pretty_plots import plt, show_plot Res = np.geomspace(1e3, 1e8, 500) for method in [ "blasius", "turbulent", "hybrid-cengel", "hybrid-schlichting", "hybrid-sharpe-convex", "hybrid-sharpe-nonconvex", ]: plt.loglog(Res, aero.Cf_flat_plate(Res, method=method), label=method) plt.ylim(1e-3, 1e-1) show_plot( "Models for Mean Skin Friction Coefficient of Flat Plate", r"$Re$", r"$C_f$", )
def plot_Cf_flat_plates(): sns.set(palette=sns.color_palette("husl")) fig, ax = plt.subplots(1, 1, figsize=(6.4, 4.8), dpi=200) Res = np.geomspace(1e3, 1e8, 500) for method in [ "blasius", "turbulent", "hybrid-cengel", "hybrid-schlichting", "hybrid-sharpe-convex", "hybrid-sharpe-nonconvex", ]: plt.loglog( Res, aero.Cf_flat_plate(Res, method=method), label=method ) plt.xlabel(r"$Re$") plt.ylabel(r"$C_f$") plt.ylim(1e-3, 1e-1) plt.title(r"Models for Mean Skin Friction Coefficient of Flat Plate") plt.tight_layout() plt.legend() plt.show()
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 __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 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], }