def test_basic_math(types):
    for x in types["all"]:
        for y in types["all"]:
            ### Arithmetic
            x + y
            x - y
            x * y
            x / y
            np.sum(x)  # Sum of all entries of array-like object x

            ### Exponentials & Powers
            x**y
            np.power(x, y)
            np.exp(x)
            np.log(x)
            np.log10(x)
            np.sqrt(x)  # Note: do x ** 0.5 rather than np.sqrt(x).

            ### Trig
            np.sin(x)
            np.cos(x)
            np.tan(x)
            np.arcsin(x)
            np.arccos(x)
            np.arctan(x)
            np.arctan2(y, x)
            np.sinh(x)
            np.cosh(x)
            np.tanh(x)
            np.arcsinh(x)
            np.arccosh(x)
            np.arctanh(x - 0.5)  # `- 0.5` to give valid argument
Ejemplo n.º 2
0
    def draw(
        self,
        scalar_to_plot:
        str = "potential",  # "potential", "streamfunction", "xvel", "yvel", "velmag", "Cp"
        x_points: np.ndarray = np.linspace(-10, 10, 400),
        y_points: np.ndarray = np.linspace(-10, 10, 300),
        percentiles_to_include=99.7,
        show=True,
    ):
        X, Y = np.meshgrid(x_points, y_points)
        X_r = np.reshape(X, -1)
        Y_r = np.reshape(Y, -1)
        points = np.vstack((X_r, Y_r)).T

        if scalar_to_plot == "potential":
            scalar_to_plot_value = sum(
                [object.get_potential_at(points) for object in self.objects])
        elif scalar_to_plot == "streamfunction":
            scalar_to_plot_value = sum([
                object.get_streamfunction_at(points) for object in self.objects
            ])
        elif scalar_to_plot == "xvel":
            scalar_to_plot_value = sum(
                [object.get_x_velocity_at(points) for object in self.objects])
        elif scalar_to_plot == "yvel":
            scalar_to_plot_value = sum(
                [object.get_y_velocity_at(points) for object in self.objects])
        elif scalar_to_plot == "velmag":
            x_vels = sum(
                [object.get_x_velocity_at(points) for object in self.objects])
            y_vels = sum(
                [object.get_y_velocity_at(points) for object in self.objects])
            scalar_to_plot_value = np.sqrt(x_vels**2 + y_vels**2)
        elif scalar_to_plot == "Cp":
            x_vels = sum(
                [object.get_x_velocity_at(points) for object in self.objects])
            y_vels = sum(
                [object.get_y_velocity_at(points) for object in self.objects])
            V = np.sqrt(x_vels**2 + y_vels**2)
            scalar_to_plot_value = 1 - V**2
        else:
            raise ValueError("Bad value of `scalar_to_plot`!")

        min = np.nanpercentile(scalar_to_plot_value,
                               50 - percentiles_to_include / 2)
        max = np.nanpercentile(scalar_to_plot_value,
                               50 + percentiles_to_include / 2)

        contour(x_points,
                y_points,
                scalar_to_plot_value.reshape(X.shape),
                levels=np.linspace(min, max, 80),
                linelabels=False,
                cmap=plt.get_cmap("rainbow"),
                contour_kwargs={
                    "linestyles": 'solid',
                    "alpha": 0.4
                })
        plt.gca().set_aspect("equal", adjustable='box')
        show_plot(f"Potential Flow: {scalar_to_plot}", "$x$", "$y$", show=show)
Ejemplo n.º 3
0
 def indicated_airspeed(self):
     """
     Returns the indicated airspeed associated with the current flight condition, in meters per second.
     """
     return np.sqrt(2 *
                    (self.total_pressure() - self.atmosphere.pressure()) /
                    self.atmosphere.density())
Ejemplo n.º 4
0
def test_opti_poorly_scaled_constraints(
        constraint_jacobian_condition_number=1e10):
    # Constants
    a = 1
    b = 100

    # Set up an optimization environment
    opti = asb.Opti()

    # Define optimization variables
    x = opti.variable(init_guess=10)
    y = opti.variable(init_guess=10)

    c = np.sqrt(constraint_jacobian_condition_number)

    # Define constraints
    opti.subject_to([x * c <= 0.9 * c, y / c <= 0.9 / c])

    # Define objective
    f = (a - x)**2 + b * (y - x**2)**2
    opti.minimize(f)

    # Optimize
    sol = opti.solve()

    # Check
    assert sol.value(x) == pytest.approx(0.9, abs=1e-4)
    assert sol.value(y) == pytest.approx(0.81, abs=1e-4)
Ejemplo n.º 5
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
Ejemplo n.º 6
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
Ejemplo n.º 7
0
def expansion_ratio_from_pressure(chamber_pressure, exit_pressure, gamma,
                                  oxamide_fraction):
    """Find the nozzle expansion ratio from the chamber and exit pressures.

    See :ref:`expansion-ratio-tutorial-label` for a physical description of the
    expansion ratio.

    Reference: Rocket Propulsion Elements, 8th Edition, Equation 3-25

    Arguments:
        chamber_pressure (scalar): Nozzle stagnation chamber pressure [units: pascal].
        exit_pressure (scalar): Nozzle exit static pressure [units: pascal].
        gamma (scalar): Exhaust gas ratio of specific heats [units: dimensionless].

    Returns:
        scalar: Expansion ratio :math:`\\epsilon = A_e / A_t` [units: dimensionless]
    """
    chamber_pressure = np.fmax(
        chamber_pressure, dubious_min_combustion_pressure(oxamide_fraction))
    chamber_pressure = np.fmax(chamber_pressure, exit_pressure * 1.5)

    AtAe = ((gamma + 1) / 2) ** (1 / (gamma - 1)) \
           * (exit_pressure / chamber_pressure) ** (1 / gamma) \
           * np.sqrt((gamma + 1) / (gamma - 1) * (1 - (exit_pressure / chamber_pressure) ** ((gamma - 1) / gamma)))
    er = 1 / AtAe
    return er
Ejemplo n.º 8
0
def propeller_shaft_power_from_thrust(
        thrust_force,
        area_propulsive,
        airspeed,
        rho,
        propeller_coefficient_of_performance=0.8,
):
    """
    Using dynamic disc actuator theory, gives the shaft power required to generate
    a certain amount of thrust.

    Source: https://web.mit.edu/16.unified/www/FALL/thermodynamics/notes/node86.html

    :param thrust_force: Thrust force [N]
    :param area_propulsive: Total disc area of all propulsive surfaces [m^2]
    :param airspeed: Airspeed [m/s]
    :param rho: Air density [kg/m^3]
    :param propeller_coefficient_of_performance: propeller coeff. of performance (due to viscous losses) [unitless]
    :return: Shaft power [W]
    """
    return 0.5 * thrust_force * airspeed * (
            np.sqrt(
                thrust_force / (area_propulsive * airspeed ** 2 * rho / 2) + 1
            ) + 1
    ) / propeller_coefficient_of_performance
Ejemplo n.º 9
0
def test_opti_hanging_chain_with_callback(plot=False):
    N = 40
    m = 40 / N
    D = 70 * N
    g = 9.81
    L = 1

    opti = asb.Opti()

    x = opti.variable(init_guess=np.linspace(-2, 2, N))
    y = opti.variable(
        init_guess=1,
        n_vars=N,
    )

    distance = np.sqrt(  # Distance from one node to the next
        np.diff(x)**2 + np.diff(y)**2)

    potential_energy_spring = 0.5 * D * np.sum((distance - L / N)**2)
    potential_energy_gravity = g * m * np.sum(y)
    potential_energy = potential_energy_spring + potential_energy_gravity

    opti.minimize(potential_energy)

    # Add end point constraints
    opti.subject_to([x[0] == -2, y[0] == 1, x[-1] == 2, y[-1] == 1])

    # Add a ground constraint
    opti.subject_to(y >= np.cos(0.1 * x) - 0.5)

    # Add a callback

    if plot:

        def my_callback(iter: int):
            plt.plot(opti.debug.value(x),
                     opti.debug.value(y),
                     ".-",
                     label=f"Iter {iter}",
                     zorder=3 + iter)

        fig, ax = plt.subplots(1, 1, figsize=(6.4, 4.8), dpi=200)
        x_ground = np.linspace(-2, 2, N)
        y_ground = np.cos(0.1 * x_ground) - 0.5
        plt.plot(x_ground, y_ground, "--k", zorder=2)

    else:

        def my_callback(iter: int):
            print(f"Iter {iter}")
            print(f"\tx = {opti.debug.value(x)}")
            print(f"\ty = {opti.debug.value(y)}")

    sol = opti.solve(callback=my_callback)

    assert sol.value(potential_energy) == pytest.approx(626.462, abs=1e-3)

    if plot:
        plt.show()
Ejemplo n.º 10
0
def CL_over_Cl(aspect_ratio: float,
               mach: float = 0.,
               sweep: float = 0.) -> float:
    """
    Returns the ratio of 3D lift coefficient (with compressibility) to 2D lift coefficient (incompressible).
    :param aspect_ratio: Aspect ratio
    :param mach: Mach number
    :param sweep: Sweep angle [deg]
    :return:
    """
    beta_squared = np.sqrt((1 - mach)**2 + 0.02)
    # return aspect_ratio / (aspect_ratio + 2) # Equivalent to equation in Drela's FVA in incompressible, 2*pi*alpha limit.
    # return aspect_ratio / (2 + np.sqrt(4 + aspect_ratio ** 2))  # more theoretically sound at low aspect_ratio
    eta = 0.95
    return aspect_ratio / (
        2 + np.sqrt(4 + (aspect_ratio**2 * beta_squared / eta**2) *
                    (1 + np.tand(sweep)**2 / beta_squared))
    )  # From Raymer, Sect. 12.4.1; citing DATCOM
Ejemplo n.º 11
0
    def mean_free_path(self):
        """
        Returns the mean free path of an air molecule, in meters.

        To find the collision radius, assumes "a hard-sphere gas that has the same viscosity as the actual gas being considered".

        From Vincenti, W. G. and Kruger, C. H. (1965). Introduction to physical gas dynamics. Krieger Publishing Company. p. 414.

        """
        return self.dynamic_viscosity() / self.pressure() * np.sqrt(
            np.pi * gas_constant_universal * self.temperature() /
            (2 * molecular_mass_air))
Ejemplo n.º 12
0
    def _compute_frame_of_WingXSec(
            self, index: int) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
        """
        Computes the local reference frame associated with a particular cross section (XSec) of this wing.

        Args:

            index: Which cross section (as indexed in Wing.xsecs) should we get the frame of?

        Returns:

            A tuple of (xg_local, yg_local, zg_local), where each entry refers to the respective (normalized) axis of
            the local reference frame of the WingXSec. Given in geometry axes.

        """
        ### Compute the untwisted reference frame
        xg_local = np.array([1, 0, 0])
        if index == 0:
            span_vector = self.xsecs[1].xyz_le - self.xsecs[0].xyz_le
            span_vector[0] = 0
            yg_local = span_vector / np.linalg.norm(span_vector)
            z_scale = 1
        elif index == len(self.xsecs) - 1 or index == -1:
            span_vector = self.xsecs[-1].xyz_le - self.xsecs[-2].xyz_le
            span_vector[0] = 0
            yg_local = span_vector / np.linalg.norm(span_vector)
            z_scale = 1
        else:
            vector_before = self.xsecs[index].xyz_le - self.xsecs[index -
                                                                  1].xyz_le
            vector_after = self.xsecs[index +
                                      1].xyz_le - self.xsecs[index].xyz_le
            vector_before[0] = 0  # Project onto YZ plane.
            vector_after[0] = 0  # Project onto YZ plane.
            vector_before = vector_before / np.linalg.norm(vector_before)
            vector_after = vector_after / np.linalg.norm(vector_after)
            span_vector = (vector_before + vector_after) / 2
            yg_local = span_vector / np.linalg.norm(span_vector)
            cos_vectors = np.linalg.inner(vector_before, vector_after)
            z_scale = np.sqrt(2 / (cos_vectors + 1))

        zg_local = np.cross(xg_local, yg_local) * z_scale

        ### Twist the reference frame by the WingXSec twist angle
        rot = np.rotation_matrix_3D(self.xsecs[index].twist * pi / 180,
                                    yg_local)
        xg_local = rot @ xg_local
        zg_local = rot @ zg_local

        return xg_local, yg_local, zg_local
Ejemplo n.º 13
0
def test_opti_poorly_scaled_objective(objective_hessian_condition_number=1e10):
    opti = asb.Opti()

    x = opti.variable(init_guess=10)
    y = opti.variable(init_guess=10)

    c = np.sqrt(objective_hessian_condition_number)

    # Define objective
    f = x**4 * c + y**4 / c
    opti.minimize(f)

    # Optimize
    sol = opti.solve()

    # Check
    assert sol.value(x) == pytest.approx(0, abs=1e-2)
    assert sol.value(y) == pytest.approx(0, abs=1e-2)
    assert sol.value(f) == pytest.approx(0, abs=1e-4)
Ejemplo n.º 14
0
def solve():
    opti = asb.Opti()

    ### Env. constants
    g = 9.81  # gravitational acceleration, m/s^2
    mu = 1.775e-5  # viscosity of air, kg/m/s
    rho = 1.23  # density of air, kg/m^3
    rho_f = 817  # density of fuel, kg/m^3

    ### Non-dimensional constants
    C_Lmax = 1.6  # stall CL
    e = 0.92  # Oswald's efficiency factor
    k = 1.17  # form factor
    N_ult = 3.3  # ultimate load factor
    S_wetratio = 2.075  # wetted area ratio
    tau = 0.12  # airfoil thickness to chord ratio
    W_W_coeff1 = 2e-5  # wing weight coefficient 1
    W_W_coeff2 = 60  # wing weight coefficient 2

    ### Dimensional constants
    Range = 1000e3  # aircraft range, m
    TSFC = 0.6 / 3600  # thrust specific fuel consumption, 1/sec
    V_min = 25  # takeoff speed, m/s
    W_0 = 6250  # aircraft weight excluding wing, N

    ### Free variables (same as SimPleAC, with extraneous variables removed)
    AR = opti.variable(init_guess=10, log_transform=True)  # aspect ratio
    S = opti.variable(init_guess=10, log_transform=True)  # total wing area, m^2
    V = opti.variable(init_guess=100, log_transform=True)  # cruise speed, m/s
    W = opti.variable(init_guess=10000, log_transform=True)  # total aircraft weight, N
    C_L = opti.variable(init_guess=1, log_transform=True)  # lift coefficient
    W_f = opti.variable(init_guess=3000, log_transform=True)  # fuel weight, N
    V_f_fuse = opti.variable(init_guess=1, log_transform=True)  # fuel volume in the fuselage, m^3

    ### Wing weight
    W_w_surf = W_W_coeff2 * S
    W_w_strc = W_W_coeff1 / tau * N_ult * AR ** 1.5 * np.sqrt(
        (W_0 + V_f_fuse * g * rho_f) * W * S
    )
    W_w = W_w_surf + W_w_strc

    ### Entire weight
    opti.subject_to(
        W >= W_0 + W_w + W_f
    )

    ### Lift equals weight constraint
    opti.subject_to([
        W_0 + W_w + 0.5 * W_f <= 0.5 * rho * S * C_L * V ** 2,
        W <= 0.5 * rho * S * C_Lmax * V_min ** 2,
    ])

    ### Flight duration
    T_flight = Range / V

    ### Drag
    Re = (rho / mu) * V * (S / AR) ** 0.5
    C_f = 0.074 / Re ** 0.2

    CDA0 = V_f_fuse / 10

    C_D_fuse = CDA0 / S
    C_D_wpar = k * C_f * S_wetratio
    C_D_ind = C_L ** 2 / (np.pi * AR * e)
    C_D = C_D_fuse + C_D_wpar + C_D_ind
    D = 0.5 * rho * S * C_D * V ** 2

    opti.subject_to([
        W_f >= TSFC * T_flight * D,
    ])

    V_f = W_f / g / rho_f
    V_f_wing = 0.03 * S ** 1.5 / AR ** 0.5 * tau  # linear with b and tau, quadratic with chord

    V_f_avail = V_f_wing + V_f_fuse

    opti.subject_to(
        V_f_avail >= V_f
    )

    opti.minimize(W_f)

    sol = opti.solve(verbose=False)
Ejemplo n.º 15
0
def beta(mach):
    return np.sqrt(1 - mach**2)
Ejemplo n.º 16
0
 def equivalent_airspeed(self):
     """
     Returns the equivalent airspeed associated with the current flight condition, in meters per second.
     """
     return self.velocity * np.sqrt(self.atmosphere.density() / Atmosphere(
         altitude=0, method="isa").density())
Ejemplo n.º 17
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]
        }
Ejemplo n.º 18
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]
        }
Ejemplo n.º 19
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],
        }