def Cd_wave_e216(Cl, mach, sweep=0.): r""" A curve fit I did to Eppler 216 airfoil data. Within -0.4 < CL < 0.75 and 0 < mach < ~0.9, has R^2 = 0.9982. See: C:\Projects\GitHub\firefly_aerodynamics\MSES Interface\analysis\e216 :param Cl: Lift coefficient :param mach: Mach number :param sweep: Sweep angle, in deg :return: Wave drag coefficient. """ mach = np.fmax(mach, 0) mach_perpendicular = mach * np.cosd(sweep) # Relation from FVA Eq. 8.176 Cl_perpendicular = Cl / np.cosd(sweep)**2 # Relation from FVA Eq. 8.177 # Coeffs c0 = 7.2685945744797997e-01 c1 = -1.5483144040727698e-01 c3 = 2.1305118052118968e-01 c4 = 7.8812272501525316e-01 c5 = 3.3888938102072169e-03 l0 = 1.5298928303149546e+00 l1 = 5.2389999717540392e-01 m = mach_perpendicular l = Cl_perpendicular Cd_wave = (np.fmax(m - (c0 + c1 * np.sqrt(c3 + (l - c4)**2) + c5 * l), 0) * (l0 + l1 * l))**2 return Cd_wave
def Cd_wave_rae2822(Cl, mach, sweep=0.): r""" A curve fit I did to RAE2822 airfoil data. Within -0.4 < CL < 0.75 and 0 < mach < ~0.9, has R^2 = 0.9982. See: C:\Projects\GitHub\firefly_aerodynamics\MSES Interface\analysis\rae2822 :param Cl: Lift coefficient :param mach: Mach number :param sweep: Sweep angle, in deg :return: Wave drag coefficient. """ mach = np.fmax(mach, 0) mach_perpendicular = mach * np.cosd(sweep) # Relation from FVA Eq. 8.176 Cl_perpendicular = Cl / np.cosd(sweep)**2 # Relation from FVA Eq. 8.177 # Coeffs c2 = 4.5776476424519119e+00 mc0 = 9.5623337929607111e-01 mc1 = 2.0552787101770234e-01 mc2 = 1.1259268018737063e+00 mc3 = 1.9538856688443659e-01 m = mach_perpendicular l = Cl_perpendicular Cd_wave = np.fmax(m - (mc0 - mc1 * np.sqrt(mc2 + (l - mc3)**2)), 0)**2 * c2 return Cd_wave
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 Cd_wave_Korn(Cl, t_over_c, mach, sweep=0, kappa_A=0.95): """ Wave drag_force coefficient prediction using the (very) low-fidelity Korn Equation method; derived in "Configuration Aerodynamics" by W.H. Mason, Sect. 7.5.2, pg. 7-18 :param Cl: Sectional lift coefficient :param t_over_c: thickness-to-chord ratio :param sweep: sweep angle, in degrees :param kappa_A: Airfoil technology factor (0.95 for supercritical section, 0.87 for NACA 6-series) :return: Wave drag coefficient """ mach = np.fmax(mach, 0) Mdd = kappa_A / np.cosd(sweep) - t_over_c / np.cosd(sweep)**2 - Cl / ( 10 * np.cosd(sweep)**3) Mcrit = Mdd - (0.1 / 80)**(1 / 3) Cd_wave = np.where(mach > Mcrit, 20 * (mach - Mcrit)**4, 0) return Cd_wave
def length_day(latitude, day_of_year): """ For what length of time is the sun above the horizon on a given day? :param latitude: Latitude [degrees] :param day_of_year: Julian day (1 == Jan. 1, 365 == Dec. 31) :return: Seconds of sunlight in a given day """ dec = declination_angle(day_of_year) constant = -np.sind(dec) * np.sind(latitude) / (np.cosd(dec) * np.cosd(latitude)) constant = np.clip(constant, -1, 1) sun_time_nondim = 2 * np.arccos(constant) sun_time = sun_time_nondim / (2 * np.pi) * 86400 return sun_time
def declination_angle(day_of_year): """ Declination angle, in degrees, as a func. of day of year. (Seasonality) :param day_of_year: Julian day (1 == Jan. 1, 365 == Dec. 31) :return: Declination angle [deg] """ # Declination (seasonality) # Source: https://www.pveducation.org/pvcdrom/properties-of-sunlight/declination-angle return -23.45 * np.cosd(360 / 365 * (day_of_year + 10)) # in degrees
def solar_elevation_angle(latitude, day_of_year, time): """ Elevation 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 elevation angle [degrees] (angle between horizon and sun). Returns 0 if the sun is below the horizon. """ # Solar elevation angle (including seasonality, latitude, and time of day) # Source: https://www.pveducation.org/pvcdrom/properties-of-sunlight/elevation-angle declination = declination_angle(day_of_year) solar_elevation_angle = np.arcsind( np.sind(declination) * np.sind(latitude) + np.cosd(declination) * np.cosd(latitude) * np.cosd(time / 86400 * 360) ) # in degrees solar_elevation_angle = np.fmax(solar_elevation_angle, 0) return solar_elevation_angle
def incidence_angle_function( latitude: float, day_of_year: float, time: float, panel_azimuth_angle: float = 0, panel_tilt_angle: float = 0, scattering: bool = True, ): """ This website will be useful for accounting for direction of the vertical surface https://www.pveducation.org/pvcdrom/properties-of-sunlight/arbitrary-orientation-and-tilt :param latitude: Latitude [degrees] :param day_of_year: Julian day (1 == Jan. 1, 365 == Dec. 31) :param time: Time since (local) solar noon [seconds] :param panel_azimuth_angle: The azimuth angle of the panel normal, in degrees. (0 degrees if pointing North and 90 if East) :param panel_tilt_angle: The angle between the panel normal and vertical, in degrees. (0 if horizontal and 90 if vertical) :param scattering: Boolean: include scattering effects at very low angles? :returns illumination_factor: Fraction of solar insolation received, relative to what it would get if it were perfectly oriented to the sun. """ solar_elevation = solar_elevation_angle(latitude, day_of_year, time) solar_azimuth = solar_azimuth_angle(latitude, day_of_year, time) cosine_factor = ( np.cosd(solar_elevation) * np.sind(panel_tilt_angle) * np.cosd(panel_azimuth_angle - solar_azimuth) + np.sind(solar_elevation) * np.cosd(panel_tilt_angle) ) if scattering: illumination_factor = cosine_factor * scattering_factor(solar_elevation) else: illumination_factor = cosine_factor illumination_factor = np.fmax(illumination_factor, 0) illumination_factor = np.where( solar_elevation < 0, 0, illumination_factor ) return illumination_factor
def solar_flux_outside_atmosphere_normal(day_of_year): """ Normal solar flux at the top of the atmosphere (variation due to orbital eccentricity) :param day_of_year: Julian day (1 == Jan. 1, 365 == Dec. 31) :return: Solar flux [W/m^2] """ # Space effects # # Source: https://www.itacanet.org/the-sun-as-a-source-of-energy/part-2-solar-energy-reaching-the-earths-surface/#2.1.-The-Solar-Constant # solar_flux_outside_atmosphere_normal = 1367 * (1 + 0.034 * cas.cos(2 * cas.pi * (day_of_year / 365.25))) # W/m^2; variation due to orbital eccentricity # Source: https://www.pveducation.org/pvcdrom/properties-of-sunlight/solar-radiation-outside-the-earths-atmosphere return 1366 * ( 1 + 0.033 * np.cosd(360 * (day_of_year - 2) / 365)) # W/m^2; variation due to orbital eccentricity
def mach_crit_Korn( CL, t_over_c, sweep=0, kappa_A=0.95 ): """ Wave drag_force coefficient prediction using the low-fidelity Korn Equation method; derived in "Configuration Aerodynamics" by W.H. Mason, Sect. 7.5.2, pg. 7-18 Args: CL: Sectional lift coefficient t_over_c: thickness-to-chord ratio sweep: sweep angle, in degrees kappa_A: Airfoil technology factor (0.95 for supercritical section, 0.87 for NACA 6-series) Returns: """ smooth_abs_CL = np.softmax(CL, -CL, hardness=10) M_dd = kappa_A / np.cosd(sweep) - t_over_c / np.cosd(sweep) ** 2 - smooth_abs_CL / (10 * np.cosd(sweep) ** 3) M_crit = M_dd - (0.1 / 80) ** (1 / 3) return M_crit
def airfoil_coefficients_post_stall( airfoil: Airfoil, alpha: float, ): """ Estimates post-stall aerodynamics of an airfoil. Uses methods given in: Truong, V. K. "An analytical model for airfoil aerodynamic characteristics over the entire 360deg angle of attack range". J. Renewable Sustainable Energy. 2020. doi: 10.1063/1.5126055 Args: airfoil: op_point: Returns: """ sina = np.sind(alpha) cosa = np.cosd(alpha) ##### Normal force calulation # Cd90_fp = aerolib.Cd_flat_plate_normal() # TODO implement # Cd90_0 = Cd90_fp - 0.83 * airfoil.LE_radius() - 1.46 / 2 * airfoil.max_thickness() + 1.46 * airfoil.max_camber() # Cd270_0 = Cd90_fp - 0.83 * airfoil.LE_radius() - 1.46 / 2 * airfoil.max_thickness() - 1.46 * airfoil.max_camber() ### Values for NACA0012 Cd90_0 = 2.08 pn2_star = 8.36e-2 pn3_star = 4.06e-1 pt1_star = 9.00e-2 pt2_star = -1.78e-1 pt3_star = -2.98e-1 Cd90 = Cd90_0 + pn2_star * cosa + pn3_star * cosa**2 CN = Cd90 * sina ##### Tangential force calculation CT = (pt1_star + pt2_star * cosa + pt3_star * cosa**3) * sina**2 ##### Conversion to wind axes CL = CN * cosa + CT * sina CD = CN * sina - CT * cosa CM = np.zeros_like(CL) # TODO return CL, CD, CM
def incidence_angle_function(latitude, day_of_year, time, scattering=True): """ What is the fraction of insolation that a horizontal surface will receive as a function of sun position in the sky? :param latitude: Latitude [degrees] :param day_of_year: Julian day (1 == Jan. 1, 365 == Dec. 31) :param time: Time since (local) solar noon [seconds] :param scattering: Boolean: include scattering effects at very low angles? """ # Old description: # To first-order, this is true. In class, Kevin Uleck claimed that you have higher-than-cosine losses at extreme angles, # since you get reflection losses. However, an experiment by Sharma appears to not reproduce this finding, showing only a # 0.4-percentage-point drop in cell efficiency from 0 to 60 degrees. So, for now, we'll just say it's a cosine loss. # Sharma: https://www.ncbi.nlm.nih.gov/pmc/articles/PMC6611928/ elevation_angle = solar_elevation_angle(latitude, day_of_year, time) theta = 90 - elevation_angle # Angle between panel normal and the sun, in degrees cosine_factor = np.cosd(theta) if not scattering: return cosine_factor else: return cosine_factor * scattering_factor(elevation_angle)
def calculate_velocity( self, x_field, y_field, ) -> [np.ndarray, np.ndarray]: ### Analyze the freestream u_freestream = self.op_point.velocity * np.cosd(self.op_point.alpha) v_freestream = self.op_point.velocity * np.sind(self.op_point.alpha) u_field = u_freestream v_field = v_freestream for airfoil in self.airfoils: ### Add in the influence of the vortices and sources on the airfoil surface u_field_induced, v_field_induced = calculate_induced_velocity_line_singularities( x_field=x_field, y_field=y_field, x_panels=airfoil.x(), y_panels=airfoil.y(), gamma=airfoil.gamma, sigma=airfoil.sigma, ) u_field = u_field + u_field_induced v_field = v_field + v_field_induced ### Add in the influence of a source across the open trailing-edge panel. if airfoil.TE_thickness() != 0: u_field_induced_TE, v_field_induced_TE = calculate_induced_velocity_line_singularities( x_field=x_field, y_field=y_field, x_panels=[airfoil.x()[0], airfoil.x()[-1]], y_panels=[airfoil.y()[0], airfoil.y()[-1]], gamma=[0, 0], sigma=[airfoil.gamma[0], airfoil.gamma[-1]]) u_field = u_field + u_field_induced_TE v_field = v_field + v_field_induced_TE if self.ground_effect: ### Add in the influence of the vortices and sources on the airfoil surface u_field_induced, v_field_induced = calculate_induced_velocity_line_singularities( x_field=x_field, y_field=y_field, x_panels=airfoil.x(), y_panels=-airfoil.y(), gamma=-airfoil.gamma, sigma=airfoil.sigma, ) u_field = u_field + u_field_induced v_field = v_field + v_field_induced ### Add in the influence of a source across the open trailing-edge panel. if airfoil.TE_thickness() != 0: u_field_induced_TE, v_field_induced_TE = calculate_induced_velocity_line_singularities( x_field=x_field, y_field=y_field, x_panels=[airfoil.x()[0], airfoil.x()[-1]], y_panels=-1 * np.array( [airfoil.y()[0], airfoil.y()[-1]]), gamma=[0, 0], sigma=[airfoil.gamma[0], airfoil.gamma[-1]]) u_field = u_field + u_field_induced_TE v_field = v_field + v_field_induced_TE return u_field, v_field
def firefly_CLA_and_CDA_fuse_hybrid( # TODO remove fuse_fineness_ratio, fuse_boattail_angle, fuse_TE_diameter, fuse_length, fuse_diameter, alpha, V, mach, rho, mu, ): """ Estimated equiv. lift area and equiv. drag area of the Firefly fuselage, component buildup. :param fuse_fineness_ratio: Fineness ratio of the fuselage nose (length / diameter). 0.5 is hemispherical. :param fuse_boattail_angle: Boattail half-angle [deg] :param fuse_TE_diameter: Diameter of the fuselage's base at the "trailing edge" [m] :param fuse_length: Length of the fuselage [m] :param fuse_diameter: Diameter of the fuselage [m] :param alpha: Angle of attack [deg] :param V: Airspeed [m/s] :param mach: Mach number [unitless] :param rho: Air density [kg/m^3] :param mu: Dynamic viscosity of air [Pa*s] :return: A tuple of (CLA, CDA) [m^2] """ alpha_rad = alpha * np.pi / 180 sin_alpha = np.sind(alpha) cos_alpha = np.cosd(alpha) """ Lift of a truncated fuselage, following slender body theory. """ separation_location = 0.3 # 0 for separation at trailing edge, 1 for separation at start of boat-tail. Calibrate this. diameter_at_separation = ( 1 - separation_location ) * fuse_TE_diameter + separation_location * fuse_diameter fuse_TE_area = np.pi / 4 * diameter_at_separation**2 CLA_slender_body = 2 * fuse_TE_area * alpha_rad # Derived from FVA 6.6.5, Eq. 6.75 """ Crossflow force, following the method of Jorgensen, 1977: "Prediction of Static Aero. Chars. for Slender..." """ V_crossflow = V * sin_alpha Re_crossflow = rho * np.abs(V_crossflow) * fuse_diameter / mu mach_crossflow = mach * np.abs(sin_alpha) eta = 1 # TODO make this a function of overall fineness ratio Cdn = 0.2 # Taken from suggestion for supercritical cylinders in Hoerner's Fluid Dynamic Drag, pg. 3-11 S_planform = fuse_diameter * fuse_length CNA = eta * Cdn * S_planform * sin_alpha * np.abs(sin_alpha) CLA_crossflow = CNA * cos_alpha CDA_crossflow = CNA * sin_alpha CLA = CLA_slender_body + CLA_crossflow r""" Zero-lift_force drag_force Model derived from high-fidelity data at: C:\Projects\GitHub\firefly_aerodynamics\Design_Opt\studies\Circular Firefly Fuse CFD\Zero-Lift Drag """ c = np.array([ 112.57153128951402720758778741583, -7.1720570832587240417410612280946, -0.01765596807595304351679033061373, 0.0026135564778264172743071913629365, 550.8775012129947299399645999074, 3.3166868391027000129156476759817, 11774.081980549422951298765838146, 3073258.204571904614567756652832, 0.0299, ]) # coefficients fuse_Re = rho * np.abs(V) * fuse_length / mu CDA_zero_lift = ( (c[0] * np.exp(c[1] * fuse_fineness_ratio) + c[2] * fuse_boattail_angle + c[3] * fuse_boattail_angle**2 + c[4] * fuse_TE_diameter**2 + c[5]) / c[6] * (fuse_Re / c[7])**(-1 / 7) * (fuse_length * fuse_diameter) / c[8]) r""" Assumes a ring-shaped wake projection into the Trefftz plane with a sinusoidal circulation distribution. This results in a uniform grad(phi) within the ring in the Trefftz plane. Derivation at C:\Projects\GitHub\firefly_aerodynamics\Gists and Ideas\Ring Wing Potential Flow """ fuse_oswalds_efficiency = 0.5 CDiA_slender_body = CLA**2 / ( diameter_at_separation**2 * np.pi * fuse_oswalds_efficiency) / 2 # or equivalently, use the next line # CDiA_slender_body = fuse_TE_area * alpha_rad ** 2 / 2 CDA = CDA_crossflow + CDiA_slender_body + CDA_zero_lift return CLA, CDA
def convert_axes( self, x_from: Union[float, np.ndarray], y_from: Union[float, np.ndarray], z_from: Union[float, np.ndarray], from_axes: str, to_axes: str, ) -> Tuple[float, float, float]: """ Converts a vector [x_from, y_from, z_from], as given in the `from_axes` frame, to an equivalent vector [x_to, y_to, z_to], as given in the `to_axes` frame. Both `from_axes` and `to_axes` should be a string, one of: * "geometry" * "body" * "wind" * "stability" This whole function is vectorized, both over the vector and the OperatingPoint (e.g., a vector of `OperatingPoint.alpha` values) Wind axes rotations are taken from Eq. 6.7 in Sect. 6.2.2 of Drela's Flight Vehicle Aerodynamics textbook, with axes corrections to go from [D, Y, L] to true wind axes (and same for geometry to body axes). Args: x_from: x-component of the vector, in `from_axes` frame. y_from: y-component of the vector, in `from_axes` frame. z_from: z-component of the vector, in `from_axes` frame. from_axes: The axes to convert from. to_axes: The axes to convert to. Returns: The x-, y-, and z-components of the vector, in `to_axes` frame. Given as a tuple. """ if from_axes == "geometry": x_b = -x_from y_b = y_from z_b = -z_from elif from_axes == "body": x_b = x_from y_b = y_from z_b = z_from elif from_axes == "wind": sa = np.sind(self.alpha) ca = np.cosd(self.alpha) sb = np.sind(self.beta) cb = np.cosd(self.beta) x_b = (cb * ca) * x_from + (-sb * ca) * y_from + (-sa) * z_from y_b = (sb) * x_from + ( cb) * y_from # Note: z term is 0; not forgotten. z_b = (cb * sa) * x_from + (-sb * sa) * y_from + (ca) * z_from elif from_axes == "stability": sa = np.sind(self.alpha) ca = np.cosd(self.alpha) x_b = ca * x_from - sa * z_from y_b = y_from z_b = sa * x_from + ca * z_from else: raise ValueError("Bad value of `from_axes`!") if to_axes == "geometry": x_to = -x_b y_to = y_b z_to = -z_b elif to_axes == "body": x_to = x_b y_to = y_b z_to = z_b elif to_axes == "wind": sa = np.sind(self.alpha) ca = np.cosd(self.alpha) sb = np.sind(self.beta) cb = np.cosd(self.beta) x_to = (cb * ca) * x_b + (sb) * y_b + (cb * sa) * z_b y_to = (-sb * ca) * x_b + (cb) * y_b + (-sb * sa) * z_b z_to = (-sa) * x_b + ( ca) * z_b # Note: y term is 0; not forgotten. elif to_axes == "stability": sa = np.sind(self.alpha) ca = np.cosd(self.alpha) x_to = ca * x_b + sa * z_b y_to = y_b z_to = -sa * x_b + ca * z_b else: raise ValueError("Bad value of `to_axes`!") return x_to, y_to, z_to
def wing_aerodynamics(self, wing: Wing, ) -> Dict[str, Any]: """ 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() mach_normal = mach * np.cosd(sweep) q = op_point.dynamic_pressure() CL_over_Cl = aerolib.CL_over_Cl( aspect_ratio=AR, mach=mach, sweep=sweep, Cl_is_compressible=True ) 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 = xsec_a.airfoil.CL_function( alpha=sect_alpha_generalized, Re=op_point.reynolds(xsec_a.chord), mach=mach_normal, deflection=get_deflection(xsec_a) ) xsec_b_Cl = xsec_b.airfoil.CL_function( alpha=sect_alpha_generalized, Re=op_point.reynolds(xsec_b.chord), mach=mach_normal, deflection=get_deflection(xsec_b) ) sect_CL = ( xsec_a_Cl * a_weight + xsec_b_Cl * 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_normal, 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_normal, 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 = xsec_a.airfoil.CL_function( alpha=sym_sect_alpha_generalized, Re=op_point.reynolds(xsec_a.chord), mach=mach_normal, # Note: this is correct, mach correction happens in 2D -> 3D step deflection=get_deflection(xsec_a) ) sym_xsec_b_Cl = xsec_b.airfoil.CL_function( alpha=sym_sect_alpha_generalized, Re=op_point.reynolds(xsec_b.chord), mach=mach_normal, deflection=get_deflection(xsec_b) ) sym_sect_CL = ( sym_xsec_a_Cl * a_weight + sym_xsec_b_Cl * 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_normal, 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_normal, 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 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
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] }