Exemplo n.º 1
0
def plot_Cf_flat_plates():
    from aerosandbox.tools.pretty_plots import plt, show_plot

    Res = np.geomspace(1e3, 1e8, 500)
    for method in [
            "blasius",
            "turbulent",
            "hybrid-cengel",
            "hybrid-schlichting",
            "hybrid-sharpe-convex",
            "hybrid-sharpe-nonconvex",
    ]:
        plt.loglog(Res, aero.Cf_flat_plate(Res, method=method), label=method)
    plt.ylim(1e-3, 1e-1)
    show_plot(
        "Models for Mean Skin Friction Coefficient of Flat Plate",
        r"$Re$",
        r"$C_f$",
    )
Exemplo n.º 2
0
def plot_Cf_flat_plates():
    sns.set(palette=sns.color_palette("husl"))
    fig, ax = plt.subplots(1, 1, figsize=(6.4, 4.8), dpi=200)
    Res = np.geomspace(1e3, 1e8, 500)
    for method in [
        "blasius",
        "turbulent",
        "hybrid-cengel",
        "hybrid-schlichting",
        "hybrid-sharpe-convex",
        "hybrid-sharpe-nonconvex",
    ]:
        plt.loglog(
            Res,
            aero.Cf_flat_plate(Res, method=method),
            label=method
        )
    plt.xlabel(r"$Re$")
    plt.ylabel(r"$C_f$")
    plt.ylim(1e-3, 1e-1)
    plt.title(r"Models for Mean Skin Friction Coefficient of Flat Plate")
    plt.tight_layout()
    plt.legend()
    plt.show()
Exemplo n.º 3
0
    def setup(
        self,
        verbose=True,  # Choose whether or not you want verbose output
        run_symmetric_if_possible=True,
        # Choose whether or not you want to run a symmetric_problem analysis about XZ (~4x faster)
    ):
        # Runs a point analysis at the specified op-point.

        ### Fuselages
        self.fuse_Res = [
            self.op_point.compute_reynolds(fuse.length())
            for i, fuse in enumerate(self.airplane.fuselages)
        ]
        self.CLA_fuses = [0 for i, fuse in enumerate(self.airplane.fuselages)]
        self.CDA_fuses = [
            aero.Cf_flat_plate(self.fuse_Res[i]) * fuse.area_wetted() *
            1.2  # wetted area with form factor
            for i, fuse in enumerate(self.airplane.fuselages)
        ]

        self.lift_fuses = [
            self.CLA_fuses[i] * self.op_point.dynamic_pressure()
            for i, fuse in enumerate(self.airplane.fuselages)
        ]
        self.drag_fuses = [
            self.CDA_fuses[i] * self.op_point.dynamic_pressure()
            for i, fuse in enumerate(self.airplane.fuselages)
        ]

        ### Wings
        self.wing_Res = [
            self.op_point.compute_reynolds(wing.mean_geometric_chord())
            for i, wing in enumerate(self.airplane.wings)
        ]
        self.wing_airfoils = [
            wing.xsecs[0].airfoil  # type: asb.Airfoil
            for i, wing in enumerate(self.airplane.wings)
        ]

        self.wing_Cl_incs = [
            self.wing_airfoils[i].CL_function(
                self.op_point.alpha + wing.mean_twist_angle(),
                self.wing_Res[i], 0, 0)
            for i, wing in enumerate(self.airplane.wings)
        ]  # Incompressible 2D lift coefficient
        self.wing_CLs = [
            self.wing_Cl_incs[i] *
            aero.CL_over_Cl(wing.aspect_ratio(),
                            mach=self.op_point.mach,
                            sweep=wing.mean_sweep_angle())
            for i, wing in enumerate(self.airplane.wings)
        ]  # Compressible 3D lift coefficient
        self.lift_wings = [
            self.wing_CLs[i] * self.op_point.dynamic_pressure() * wing.area()
            for i, wing in enumerate(self.airplane.wings)
        ]

        self.wing_Cd_profiles = [
            self.wing_airfoils[i].CDp_function(
                self.op_point.alpha + wing.mean_twist_angle(),
                self.wing_Res[i], self.op_point.mach, 0)
            for i, wing in enumerate(self.airplane.wings)
        ]
        self.drag_wing_profiles = [
            self.wing_Cd_profiles[i] * self.op_point.dynamic_pressure() *
            wing.area() for i, wing in enumerate(self.airplane.wings)
        ]

        self.wing_oswalds_efficiencies = [
            0.95  # TODO make this a function of taper ratio
            for i, wing in enumerate(self.airplane.wings)
        ]
        self.drag_wing_induceds = [
            self.lift_wings[i]**2 /
            (self.op_point.dynamic_pressure() * np.pi * wing.span()**2 *
             self.wing_oswalds_efficiencies[i])
            for i, wing in enumerate(self.airplane.wings)
        ]

        self.drag_wings = [
            self.drag_wing_profiles[i] + self.drag_wing_induceds[i]
            for i, wing in enumerate(self.airplane.wings)
        ]

        self.wing_Cm_incs = [
            self.wing_airfoils[i].Cm_function(
                self.op_point.alpha + wing.mean_twist_angle(),
                self.wing_Res[i], 0, 0)
            for i, wing in enumerate(self.airplane.wings)
        ]  # Incompressible 2D moment coefficient
        self.wing_CMs = [
            self.wing_Cm_incs[i] *
            aero.CL_over_Cl(wing.aspect_ratio(),
                            mach=self.op_point.mach,
                            sweep=wing.mean_sweep_angle())
            for i, wing in enumerate(self.airplane.wings)
        ]  # Compressible 3D moment coefficient
        self.local_moment_wings = [
            self.wing_CMs[i] * self.op_point.dynamic_pressure() * wing.area() *
            wing.mean_geometric_chord()
            for i, wing in enumerate(self.airplane.wings)
        ]
        self.body_moment_wings = [
            self.local_moment_wings[i] +
            wing.approximate_center_of_pressure()[0] * self.lift_wings[i]
            for i, wing in enumerate(self.airplane.wings)
        ]

        # Force totals
        lift_forces = self.lift_fuses + self.lift_wings
        drag_forces = self.drag_fuses + self.drag_wings
        self.lift_force = cas.sum1(cas.vertcat(*lift_forces))
        self.drag_force = cas.sum1(cas.vertcat(*drag_forces))
        self.side_force = 0

        # Moment totals
        self.pitching_moment = cas.sum1(cas.vertcat(*self.body_moment_wings))

        # Calculate nondimensional forces
        q = self.op_point.dynamic_pressure()
        s_ref = self.airplane.s_ref
        b_ref = self.airplane.b_ref
        c_ref = self.airplane.c_ref
        self.CL = self.lift_force / q / s_ref
        self.CD = self.drag_force / q / s_ref
        self.Cm = self.pitching_moment / q / s_ref / c_ref
Exemplo n.º 4
0
    def __init__(self,
                 airplane,  # type: Airplane
                 op_point,  # type: OperatingPoint
                 ):
        ### Initialize
        self.airplane = airplane
        self.op_point = op_point

        ### Check assumptions
        assumptions = np.array([
            self.op_point.beta == 0,
            self.op_point.p == 0,
            self.op_point.q == 0,
            self.op_point.r == 0,
        ])
        if not assumptions.all():
            raise ValueError("The assumptions to use an aero buildup method are not met!")

        ### Fuselages
        for fuselage in self.airplane.fuselages:
            fuselage.Re = self.op_point.reynolds(fuselage.length())
            fuselage.CLA = 0
            fuselage.CDA = aero.Cf_flat_plate(
                fuselage.Re * fuselage.area_wetted()) * 1.2  # wetted area with form factor

            fuselage.lift_force = fuselage.CLA * self.op_point.dynamic_pressure()
            fuselage.drag_force = fuselage.CDA * self.op_point.dynamic_pressure()
            fuselage.pitching_moment = 0

        ### Wings
        for wing in self.airplane.wings:
            wing.alpha = op_point.alpha + wing.mean_twist_angle()  # TODO add in allmoving deflections
            wing.Re = self.op_point.reynolds(wing.mean_aerodynamic_chord())
            wing.airfoil = wing.xsecs[0].airfoil

            ## Lift calculation
            wing.Cl_incompressible = wing.airfoil.CL_function(
                alpha=wing.alpha,
                Re=wing.Re,  # TODO finish
                mach=0,  # TODO revisit this - is this right?
                deflection=0
            )
            CL_over_Cl = aero.CL_over_Cl(
                aspect_ratio=wing.aspect_ratio(),
                mach=op_point.mach(),
                sweep=wing.mean_sweep_angle()
            )
            wing.CL = wing.Cl_incompressible * CL_over_Cl

            ## Drag calculation
            wing.CD_profile = wing.airfoil.CD_function(
                alpha=wing.alpha,
                Re=wing.Re,
                mach=op_point.mach(),
                deflection=0
            )

            wing.oswalds_efficiency = aero.oswalds_efficiency(
                taper_ratio=wing.taper_ratio(),
                aspect_ratio=wing.aspect_ratio(),
                sweep=wing.mean_sweep_angle(),
            )
            wing.CD_induced = wing.CL ** 2 / (pi * wing.oswalds_efficiency * wing.aspect_ratio())

            ## Moment calculation
            wing.Cm_incompressible = wing.airfoil.CM_function(
                alpha=wing.alpha,
                Re=wing.Re,
                mach=0,  # TODO revisit this - is this right?
                deflection=0,
            )
            wing.CM = wing.Cm_incompressible * CL_over_Cl

            ## Force and moment calculation
            qS = op_point.dynamic_pressure() * wing.area()
            wing.lift_force = wing.CL * qS
            wing.drag_force_profile = wing.CD_profile * qS
            wing.drag_force_induced = wing.CD_induced * qS
            wing.drag_force = wing.drag_force_profile + wing.drag_force_induced
            wing.pitching_moment = wing.CM * qS * wing.mean_aerodynamic_chord()

        ### Total the forces
        self.lift_force = 0
        self.drag_force = 0
        self.pitching_moment = 0

        for fuselage in self.airplane.fuselages:
            self.lift_force += fuselage.lift_force
            self.drag_force += fuselage.drag_force
            self.pitching_moment += fuselage.pitching_moment

        for wing in self.airplane.wings:
            if wing.symmetric:  # Only add lift force if the wing is symmetric; a surrogate for "horizontal".
                self.lift_force += wing.lift_force
            self.drag_force += wing.drag_force
            self.pitching_moment += wing.pitching_moment # Raw pitching moment
            self.pitching_moment += -wing.aerodynamic_center()[0] * wing.lift_force # Pitching moment due to lift

        ### Calculate nondimensional forces
        qS = op_point.dynamic_pressure() * self.airplane.s_ref

        self.CL = self.lift_force / qS
        self.CD = self.drag_force / qS
        self.CM = self.pitching_moment / qS / self.airplane.c_ref
Exemplo n.º 5
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]
        }
Exemplo n.º 6
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],
        }