示例#1
0
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
示例#2
0
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
示例#3
0
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
示例#4
0
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
示例#5
0
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
示例#6
0
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
示例#7
0
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
示例#8
0
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
示例#9
0
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
示例#10
0
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
示例#11
0
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
示例#12
0
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)
示例#13
0
    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
示例#14
0
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
示例#15
0
    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
示例#16
0
    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]
        }
示例#17
0
        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
示例#18
0
    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]
        }