Пример #1
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
Пример #2
0
    def wing_aerodynamics(
        self,
        wing: Wing,
    ):
        """
        Estimates the aerodynamic forces, moments, and derivatives on a wing in isolation.

        Moments are given with the reference at Wing [0, 0, 0].

        Args:

            wing: A Wing object that you wish to analyze.

            op_point: The OperatingPoint that you wish to analyze the fuselage at.

        TODO account for wing airfoil pitching moment

        Returns:

        """
        ##### Alias a few things for convenience
        op_point = self.op_point
        wing_options = self.get_options(wing)

        ##### Compute general wing properties and things to be used in sectional analysis.
        sweep = wing.mean_sweep_angle()
        AR = wing.aspect_ratio()
        mach = op_point.mach()
        q = op_point.dynamic_pressure()
        CL_over_Cl = aerolib.CL_over_Cl(aspect_ratio=AR,
                                        mach=mach,
                                        sweep=sweep)
        oswalds_efficiency = aerolib.oswalds_efficiency(
            taper_ratio=wing.taper_ratio(),
            aspect_ratio=AR,
            sweep=sweep,
            fuselage_diameter_to_span_ratio=0  # an assumption
        )
        areas = wing.area(_sectional=True)
        aerodynamic_centers = wing.aerodynamic_center(_sectional=True)

        F_g = [0, 0, 0]
        M_g = [0, 0, 0]

        ##### Iterate through the wing sections.
        for sect_id in range(len(wing.xsecs) - 1):

            ##### Identify the wing cross sections adjacent to this wing section.
            xsec_a = wing.xsecs[sect_id]
            xsec_b = wing.xsecs[sect_id + 1]

            ##### When linearly interpolating, weight things by the relative chord.
            a_weight = xsec_a.chord / (xsec_a.chord + xsec_b.chord)
            b_weight = xsec_b.chord / (xsec_a.chord + xsec_b.chord)

            ##### Compute the local frame of this section, and put the z (normal) component into wind axes.
            xg_local, yg_local, zg_local = wing._compute_frame_of_section(
                sect_id)
            sect_aerodynamic_center = aerodynamic_centers[sect_id]

            sect_z_w = op_point.convert_axes(
                x_from=zg_local[0],
                y_from=zg_local[1],
                z_from=zg_local[2],
                from_axes="geometry",
                to_axes="wind",
            )

            ##### Compute the generalized angle of attack, so the geometric alpha that the wing section "sees".
            velocity_vector_b_from_freestream = op_point.convert_axes(
                x_from=-op_point.velocity,
                y_from=0,
                z_from=0,
                from_axes="wind",
                to_axes="body")
            velocity_vector_b_from_rotation = np.cross(op_point.convert_axes(
                sect_aerodynamic_center[0],
                sect_aerodynamic_center[1],
                sect_aerodynamic_center[2],
                from_axes="geometry",
                to_axes="body"), [op_point.p, op_point.q, op_point.r],
                                                       manual=True)
            velocity_vector_b = [
                velocity_vector_b_from_freestream[i] +
                velocity_vector_b_from_rotation[i] for i in range(3)
            ]
            velocity_mag_b = np.sqrt(
                sum([comp**2 for comp in velocity_vector_b]))
            velocity_dir_b = [
                velocity_vector_b[i] / velocity_mag_b for i in range(3)
            ]
            sect_z_b = op_point.convert_axes(
                x_from=zg_local[0],
                y_from=zg_local[1],
                z_from=zg_local[2],
                from_axes="geometry",
                to_axes="body",
            )
            vel_dot_normal = np.dot(velocity_dir_b, sect_z_b, manual=True)

            sect_alpha_generalized = 90 - np.arccosd(vel_dot_normal)

            def get_deflection(xsec):
                n_surfs = len(xsec.control_surfaces)
                if n_surfs == 0:
                    return 0
                elif n_surfs == 1:
                    surf = xsec.control_surfaces[0]
                    return surf.deflection
                else:
                    raise NotImplementedError(
                        "AeroBuildup currently cannot handle multiple control surfaces attached to a given WingXSec."
                    )

            ##### Compute sectional lift at cross sections using lookup functions. Merge them linearly to get section CL.
            xsec_a_Cl_incompressible = xsec_a.airfoil.CL_function(
                alpha=sect_alpha_generalized,
                Re=op_point.reynolds(xsec_a.chord),
                mach=
                0,  # Note: this is correct, mach correction happens in 2D -> 3D step
                deflection=get_deflection(xsec_a))
            xsec_b_Cl_incompressible = xsec_b.airfoil.CL_function(
                alpha=sect_alpha_generalized,
                Re=op_point.reynolds(xsec_b.chord),
                mach=
                0,  # Note: this is correct, mach correction happens in 2D -> 3D step
                deflection=get_deflection(xsec_b))
            sect_CL = (xsec_a_Cl_incompressible * a_weight +
                       xsec_b_Cl_incompressible * b_weight) * CL_over_Cl

            ##### Compute sectional drag at cross sections using lookup functions. Merge them linearly to get section CD.
            xsec_a_Cd_profile = xsec_a.airfoil.CD_function(
                alpha=sect_alpha_generalized,
                Re=op_point.reynolds(xsec_a.chord),
                mach=mach,
                deflection=get_deflection(xsec_a))
            xsec_b_Cd_profile = xsec_b.airfoil.CD_function(
                alpha=sect_alpha_generalized,
                Re=op_point.reynolds(xsec_b.chord),
                mach=mach,
                deflection=get_deflection(xsec_b))
            sect_CDp = (xsec_a_Cd_profile * a_weight +
                        xsec_b_Cd_profile * b_weight)

            ##### Compute induced drag from local CL and full-wing properties (AR, e)
            sect_CDi = (sect_CL**2 / (np.pi * AR * oswalds_efficiency))

            ##### Total the drag.
            sect_CD = sect_CDp + sect_CDi

            ##### Go to dimensional quantities using the area.
            area = areas[sect_id]
            sect_L = q * area * sect_CL
            sect_D = q * area * sect_CD

            ##### Compute the direction of the lift by projecting the section's normal vector into the plane orthogonal to the freestream.
            sect_L_direction_w = (np.zeros_like(sect_z_w[0]), sect_z_w[1] /
                                  np.sqrt(sect_z_w[1]**2 + sect_z_w[2]**2),
                                  sect_z_w[2] /
                                  np.sqrt(sect_z_w[1]**2 + sect_z_w[2]**2))
            sect_L_direction_g = op_point.convert_axes(*sect_L_direction_w,
                                                       from_axes="wind",
                                                       to_axes="geometry")

            ##### Compute the direction of the drag by aligning the drag vector with the freestream vector.
            sect_D_direction_w = (-1, 0, 0)
            sect_D_direction_g = op_point.convert_axes(*sect_D_direction_w,
                                                       from_axes="wind",
                                                       to_axes="geometry")

            ##### Compute the force vector in geometry axes.
            sect_F_g = [
                sect_L * sect_L_direction_g[i] + sect_D * sect_D_direction_g[i]
                for i in range(3)
            ]

            ##### Compute the moment vector in geometry axes.
            sect_M_g = np.cross(sect_aerodynamic_center, sect_F_g, manual=True)

            ##### Add section forces and moments to overall forces and moments
            F_g = [F_g[i] + sect_F_g[i] for i in range(3)]
            M_g = [M_g[i] + sect_M_g[i] for i in range(3)]

            ##### Treat symmetry
            if wing.symmetric:
                ##### Compute the local frame of this section, and put the z (normal) component into wind axes.

                sym_sect_aerodynamic_center = aerodynamic_centers[sect_id]
                sym_sect_aerodynamic_center[1] *= -1

                sym_sect_z_w = op_point.convert_axes(
                    x_from=zg_local[0],
                    y_from=-zg_local[1],
                    z_from=zg_local[2],
                    from_axes="geometry",
                    to_axes="wind",
                )

                ##### Compute the generalized angle of attack, so the geometric alpha that the wing section "sees".
                sym_velocity_vector_b_from_freestream = op_point.convert_axes(
                    x_from=-op_point.velocity,
                    y_from=0,
                    z_from=0,
                    from_axes="wind",
                    to_axes="body")
                sym_velocity_vector_b_from_rotation = np.cross(
                    op_point.convert_axes(sym_sect_aerodynamic_center[0],
                                          sym_sect_aerodynamic_center[1],
                                          sym_sect_aerodynamic_center[2],
                                          from_axes="geometry",
                                          to_axes="body"),
                    [op_point.p, op_point.q, op_point.r],
                    manual=True)
                sym_velocity_vector_b = [
                    sym_velocity_vector_b_from_freestream[i] +
                    sym_velocity_vector_b_from_rotation[i] for i in range(3)
                ]
                sym_velocity_mag_b = np.sqrt(
                    sum([comp**2 for comp in sym_velocity_vector_b]))
                sym_velocity_dir_b = [
                    sym_velocity_vector_b[i] / sym_velocity_mag_b
                    for i in range(3)
                ]
                sym_sect_z_b = op_point.convert_axes(
                    x_from=zg_local[0],
                    y_from=-zg_local[1],
                    z_from=zg_local[2],
                    from_axes="geometry",
                    to_axes="body",
                )
                sym_vel_dot_normal = np.dot(sym_velocity_dir_b,
                                            sym_sect_z_b,
                                            manual=True)

                sym_sect_alpha_generalized = 90 - np.arccosd(
                    sym_vel_dot_normal)

                def get_deflection(xsec):
                    n_surfs = len(xsec.control_surfaces)
                    if n_surfs == 0:
                        return 0
                    elif n_surfs == 1:
                        surf = xsec.control_surfaces[0]
                        return surf.deflection if surf.symmetric else -surf.deflection
                    else:
                        raise NotImplementedError(
                            "AeroBuildup currently cannot handle multiple control surfaces attached to a given WingXSec."
                        )

                ##### Compute sectional lift at cross sections using lookup functions. Merge them linearly to get section CL.
                sym_xsec_a_Cl_incompressible = xsec_a.airfoil.CL_function(
                    alpha=sym_sect_alpha_generalized,
                    Re=op_point.reynolds(xsec_a.chord),
                    mach=
                    0,  # Note: this is correct, mach correction happens in 2D -> 3D step
                    deflection=get_deflection(xsec_a))
                sym_xsec_b_Cl_incompressible = xsec_b.airfoil.CL_function(
                    alpha=sym_sect_alpha_generalized,
                    Re=op_point.reynolds(xsec_b.chord),
                    mach=
                    0,  # Note: this is correct, mach correction happens in 2D -> 3D step
                    deflection=get_deflection(xsec_b))
                sym_sect_CL = (
                    sym_xsec_a_Cl_incompressible * a_weight +
                    sym_xsec_b_Cl_incompressible * b_weight) * CL_over_Cl

                ##### Compute sectional drag at cross sections using lookup functions. Merge them linearly to get section CD.
                sym_xsec_a_Cd_profile = xsec_a.airfoil.CD_function(
                    alpha=sym_sect_alpha_generalized,
                    Re=op_point.reynolds(xsec_a.chord),
                    mach=mach,
                    deflection=get_deflection(xsec_a))
                sym_xsec_b_Cd_profile = xsec_b.airfoil.CD_function(
                    alpha=sym_sect_alpha_generalized,
                    Re=op_point.reynolds(xsec_b.chord),
                    mach=mach,
                    deflection=get_deflection(xsec_b))
                sym_sect_CDp = (sym_xsec_a_Cd_profile * a_weight +
                                sym_xsec_b_Cd_profile * b_weight)

                ##### Compute induced drag from local CL and full-wing properties (AR, e)
                sym_sect_CDi = (sym_sect_CL**2 /
                                (np.pi * AR * oswalds_efficiency))

                ##### Total the drag.
                sym_sect_CD = sym_sect_CDp + sym_sect_CDi

                ##### Go to dimensional quantities using the area.
                area = areas[sect_id]
                sym_sect_L = q * area * sym_sect_CL
                sym_sect_D = q * area * sym_sect_CD

                ##### Compute the direction of the lift by projecting the section's normal vector into the plane orthogonal to the freestream.
                sym_sect_L_direction_w = (
                    np.zeros_like(sym_sect_z_w[0]), sym_sect_z_w[1] /
                    np.sqrt(sym_sect_z_w[1]**2 + sym_sect_z_w[2]**2),
                    sym_sect_z_w[2] /
                    np.sqrt(sym_sect_z_w[1]**2 + sym_sect_z_w[2]**2))
                sym_sect_L_direction_g = op_point.convert_axes(
                    *sym_sect_L_direction_w,
                    from_axes="wind",
                    to_axes="geometry")

                ##### Compute the direction of the drag by aligning the drag vector with the freestream vector.
                sym_sect_D_direction_w = (-1, 0, 0)
                sym_sect_D_direction_g = op_point.convert_axes(
                    *sym_sect_D_direction_w,
                    from_axes="wind",
                    to_axes="geometry")

                ##### Compute the force vector in geometry axes.
                sym_sect_F_g = [
                    sym_sect_L * sym_sect_L_direction_g[i] +
                    sym_sect_D * sym_sect_D_direction_g[i] for i in range(3)
                ]

                ##### Compute the moment vector in geometry axes.
                sym_sect_M_g = np.cross(sym_sect_aerodynamic_center,
                                        sym_sect_F_g,
                                        manual=True)

                ##### Add section forces and moments to overall forces and moments
                F_g = [F_g[i] + sym_sect_F_g[i] for i in range(3)]
                M_g = [M_g[i] + sym_sect_M_g[i] for i in range(3)]

        ##### Convert F_g and M_g to body and wind axes for reporting.
        F_b = op_point.convert_axes(*F_g, from_axes="geometry", to_axes="body")
        F_w = op_point.convert_axes(*F_b, from_axes="body", to_axes="wind")
        M_b = op_point.convert_axes(*M_g, from_axes="geometry", to_axes="body")
        M_w = op_point.convert_axes(*M_b, from_axes="body", to_axes="wind")

        return {
            "F_g": F_g,
            "F_b": F_b,
            "F_w": F_w,
            "M_g": M_g,
            "M_b": M_b,
            "M_w": M_w,
            "L": -F_w[2],
            "Y": F_w[1],
            "D": -F_w[0],
            "l_b": M_b[0],
            "m_b": M_b[1],
            "n_b": M_b[2]
        }
Пример #3
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]
        }
Пример #4
0
    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],
        }