示例#1
0
    def _pressure_isa(self):
        """
        Computes the pressure at the Atmosphere's altitude based on the International Standard Atmosphere.

        Uses the Barometric formula, as implemented here: https://en.wikipedia.org/wiki/Barometric_formula

        Returns: Pressure [Pa]

        """
        alt = self.altitude
        pressure = 0 * alt  # Initialize the pressure to all zeros.

        for i in range(len(isa_table)):
            pressure = np.where(
                alt > isa_base_altitude[i],
                barometric_formula(P_b=isa_pressure[i],
                                   T_b=isa_base_temperature[i],
                                   L_b=isa_lapse_rate[i],
                                   h=alt,
                                   h_b=isa_base_altitude[i]), pressure)

        ### Add lower bound case
        pressure = np.where(
            alt <= isa_base_altitude[0],
            barometric_formula(P_b=isa_pressure[0],
                               T_b=isa_base_temperature[0],
                               L_b=isa_lapse_rate[0],
                               h=alt,
                               h_b=isa_base_altitude[0]), pressure)

        return pressure
def test_logic(types):
    for option_set in [
            types["scalar"],
            types["vector"],
            types["matrix"],
    ]:
        for x in option_set:
            for y in option_set:
                ### Comparisons
                """
                Note: if warnings appear here, they're from `np.array(1) == cas.MX(1)` - 
                sensitive to order, as `cas.MX(1) == np.array(1)` is fine.
                
                However, checking the outputs, these seem to be yielding correct results despite
                the warning sooo...
                """
                x == y  # Warnings coming from here
                x != y  # Warnings coming from here
                x > y
                x >= y
                x < y
                x <= y

                ### Conditionals
                np.where(x > 1, x**2, 0)

                ### Elementwise min/max
                np.fmax(x, y)
                np.fmin(x, y)

    for x in types["all"]:
        np.fabs(x)
        np.floor(x)
        np.ceil(x)
        np.clip(x, 0, 1)
示例#3
0
def pressure_isa(altitude):
    """
    Computes the pressure at a given altitude based on the International Standard Atmosphere.

    Uses the Barometric formula, as implemented here: https://en.wikipedia.org/wiki/Barometric_formula

    Args:
        altitude: Geopotential altitude [m]

    Returns: Pressure [Pa]

    """
    pressure = 0 * altitude  # Initialize the pressure to all zeros.

    for i in range(len(isa_table)):
        pressure = np.where(
            altitude > isa_base_altitude[i],
            barometric_formula(P_b=isa_pressure[i],
                               T_b=isa_base_temperature[i],
                               L_b=isa_lapse_rate[i],
                               h=altitude,
                               h_b=isa_base_altitude[i]), pressure)

    ### Add lower bound case
    pressure = np.where(
        altitude <= isa_base_altitude[0],
        barometric_formula(P_b=isa_pressure[0],
                           T_b=isa_base_temperature[0],
                           L_b=isa_lapse_rate[0],
                           h=altitude,
                           h_b=isa_base_altitude[0]), pressure)

    return pressure
示例#4
0
def lower_bound(n):
    '''
    finds a lower bound for instance n
    '''

    opti = asb.Opti()
    coeffs = opti.variable(init_guess=np.zeros(degree + 1))
    n_new = np.where(n == None, 1, n)
    n_new = np.array(n_new)
    y_model = model(coeffs * n_new)
    error = loss(y_model, y_data)
    opti.minimize(error + eta * np.sum(np.where(n == None, 0, n)))
    sol = opti.solve(verbose=False)
    return sol.value(error) + eta * np.sum(np.where(n == None, 0, n))
def test_where_casadi():
    a = cas.GenDM_ones(4)
    b = 2 * cas.GenDM_ones(4)

    c = np.where(cas.DM([1, 0, 1, 0]), a, b)

    assert np.all(c == cas.DM([1, 2, 1, 2]))
def test_where_numpy():
    a = np.ones(4)
    b = 2 * np.ones(4)

    c = np.where(np.array([True, False, True, False]), a, b)

    assert np.all(c == np.array([1, 2, 1, 2]))
示例#7
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
示例#8
0
def kussners_function(reduced_time: Union[np.ndarray, float]):
    """ 
    A commonly used approximation to Kussner's function (Sears and Sparks 1941)
    
    Args:
        reduced_time (float,np.ndarray) : This is equal to the number of semichords travelled. See function calculate_reduced_time
    """
    kussner = (1 - 0.5 * np.exp(-0.13 * reduced_time) -
               0.5 * np.exp(-reduced_time)) * np.where(reduced_time >= 0, 1, 0)
    return kussner
示例#9
0
def wagners_function(reduced_time: Union[np.ndarray, float]):
    """ 
    A commonly used approximation to Wagner's function 
    (Jones, R.T. The Unsteady Lift of a Finite Wing; Technical Report NACA TN-682; NACA: Washington, DC, USA, 1939)
    
    Args:
        reduced_time (float,np.ndarray) : Equal to the number of semichords travelled. See function calculate_reduced_time
    """
    wagner = (1 - 0.165 * np.exp(-0.0455 * reduced_time) - 0.335 *
              np.exp(-0.3 * reduced_time)) * np.where(reduced_time >= 0, 1, 0)
    return wagner
示例#10
0
    def _temperature_isa(self):
        """
        Computes the temperature at the Atmosphere's altitude based on the International Standard Atmosphere.
        Returns: Temperature [K]

        """
        alt = self.altitude
        temp = 0 * alt  # Initialize the temperature to all zeros.

        for i in range(len(isa_table)):
            temp = np.where(alt > isa_base_altitude[i],
                            (alt - isa_base_altitude[i]) * isa_lapse_rate[i] +
                            isa_base_temperature[i], temp)

        ### Add lower bound case
        temp = np.where(alt <= isa_base_altitude[0],
                        (alt - isa_base_altitude[0]) * isa_lapse_rate[0] +
                        isa_base_temperature[0], temp)

        return temp
示例#11
0
def temperature_isa(altitude):
    """
    Computes the temperature at a given altitude based on the International Standard Atmosphere.

    Args:
        altitude: Geopotential altitude [m]

    Returns: Temperature [K]

    """
    temp = 0 * altitude  # Initialize the temperature to all zeros.

    for i in range(len(isa_table)):
        temp = np.where(altitude > isa_base_altitude[i],
                        (altitude - isa_base_altitude[i]) * isa_lapse_rate[i] +
                        isa_base_temperature[i], temp)

    ### Add lower bound case
    temp = np.where(altitude <= isa_base_altitude[0],
                    (altitude - isa_base_altitude[0]) * isa_lapse_rate[0] +
                    isa_base_temperature[0], temp)

    return temp
示例#12
0
def Cd_wave_Korn(Cl, t_over_c, mach, sweep=0, kappa_A=0.95):
    """
    Wave drag_force coefficient prediction using the (very) low-fidelity Korn Equation method; derived in "Configuration Aerodynamics" by W.H. Mason, Sect. 7.5.2, pg. 7-18
    :param Cl: Sectional lift coefficient
    :param t_over_c: thickness-to-chord ratio
    :param sweep: sweep angle, in degrees
    :param kappa_A: Airfoil technology factor (0.95 for supercritical section, 0.87 for NACA 6-series)
    :return: Wave drag coefficient
    """
    mach = np.fmax(mach, 0)
    Mdd = kappa_A / np.cosd(sweep) - t_over_c / np.cosd(sweep)**2 - Cl / (
        10 * np.cosd(sweep)**3)
    Mcrit = Mdd - (0.1 / 80)**(1 / 3)
    Cd_wave = np.where(mach > Mcrit, 20 * (mach - Mcrit)**4, 0)

    return Cd_wave
示例#13
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 = np.where(1 - mach**2 >= 0, np.fmax(1 - mach**2, 0)**0.5, 0)
    # return aspect_ratio / (aspect_ratio + 2) # Equivalent to equation in Drela's FVA in incompressible, 2*pi*alpha limit.
    # return aspect_ratio / (2 + cas.sqrt(4 + aspect_ratio ** 2))  # more theoretically sound at low aspect_ratio
    eta = 0.95
    return aspect_ratio / (2 + np.sqrt(4 + (aspect_ratio * beta / eta)**2 *
                                       (1 + (np.tand(sweep) / beta)**2))
                           )  # From Raymer, Sect. 12.4.1; citing DATCOM
示例#14
0
def dynamics(t, y):
    dyn = asb.FreeBodyDynamics(
        *y,
        g=1,
    )
    aero = asb.AeroBuildup(
        airplane=airplane,
        op_point=dyn.op_point
    ).run()
    dyn.X, dyn.Y, dyn.Z = aero["F_b"]
    dyn.L, dyn.M, dyn.N = aero["M_b"]
    derivatives = dyn.state_derivatives()
    derivatives["u"] = np.where(
        dyn.u < 0,
        0,
        derivatives["u"]
    )

    return np.array(list(dyn.state_derivatives().values()))
示例#15
0
def incidence_angle_function(
        latitude: float,
        day_of_year: float,
        time: float,
        panel_azimuth_angle: float = 0,
        panel_tilt_angle: float = 0,
        scattering: bool = True,
):
    """
    This website will be useful for accounting for direction of the vertical surface
    https://www.pveducation.org/pvcdrom/properties-of-sunlight/arbitrary-orientation-and-tilt
    :param latitude: Latitude [degrees]
    :param day_of_year: Julian day (1 == Jan. 1, 365 == Dec. 31)
    :param time: Time since (local) solar noon [seconds]
    :param panel_azimuth_angle: The azimuth angle of the panel normal, in degrees. (0 degrees if pointing North and 90 if East)
    :param panel_tilt_angle: The angle between the panel normal and vertical, in degrees. (0 if horizontal and 90 if vertical)
    :param scattering: Boolean: include scattering effects at very low angles?

    :returns
    illumination_factor: Fraction of solar insolation received, relative to what it would get if it were perfectly oriented to the sun.
    """
    solar_elevation = solar_elevation_angle(latitude, day_of_year, time)
    solar_azimuth = solar_azimuth_angle(latitude, day_of_year, time)
    cosine_factor = (
            np.cosd(solar_elevation) *
            np.sind(panel_tilt_angle) *
            np.cosd(panel_azimuth_angle - solar_azimuth)
            + np.sind(solar_elevation) * np.cosd(panel_tilt_angle)
    )
    if scattering:
        illumination_factor = cosine_factor * scattering_factor(solar_elevation)
    else:
        illumination_factor = cosine_factor

    illumination_factor = np.fmax(illumination_factor, 0)
    illumination_factor = np.where(
        solar_elevation < 0,
        0,
        illumination_factor
    )
    return illumination_factor
示例#16
0
        x_data=x,
        y_data=y
    )


    def randspace(start, stop, n=50):
        vals = (stop - start) * np.random.rand(n) + start
        vals = np.concatenate((vals[:-2], np.array([start, stop])))
        # vals = np.sort(vals)
        return vals


    np.random.seed(4)
    X = randspace(-5, 5, 200)
    Y = randspace(-5, 5, 200)
    f = np.where(X > 0, 1, 0) + np.where(Y > 0, 1, 0)
    # f = X ** 2 + Y ** 2
    interp = UnstructuredInterpolatedModel(
        x_data={
            "x": X.flatten(),
            "y": Y.flatten(),
        },
        y_data=f.flatten()
    )

    from aerosandbox.tools.pretty_plots import plt, show_plot

    fig = plt.figure()
    ax = fig.add_subplot(projection='3d')
    # ax.plot_surface(X, Y, f, color="blue", alpha=0.2)
    ax.scatter(X.flatten(), Y.flatten(), f.flatten())
示例#17
0
def approximate_CD_wave(
    mach,
    mach_crit,
    CD_wave_at_fully_supersonic,
):
    """
    An approximate relation for computing transonic wave drag, based on an object's Mach number.

    Considered reasonably valid from Mach 0 up to around Mach 2 or 3-ish.

    Methodology is a combination of:

        * The methodology described in Raymer, "Aircraft Design: A Conceptual Approach", Section 12.5.10 Transonic Parasite Drag (pg. 449 in Ed. 2)

        and

        * The methodology described in W.H. Mason's Configuration Aerodynamics, Chapter 7. Transonic Aerodynamics of Airfoils and Wings.

    Args:

        mach: Mach number at the operating point to be evaluated

        mach_crit: Critical mach number, a function of the body geometry

        CD_wave_at_fully_supersonic: The wave drag coefficient of the body at the speed that it first goes (
        effectively) fully supersonic.

            Here, that is taken to mean at the Mach 1.2 case.

            This value should probably be derived using something similar to a Sears-Haack relation for the body in
            question, with a markup depending on geometry smoothness.

            The CD_wave predicted by this function will match this value exactly at M=1.2 and M=1.05.

            The peak CD_wave that is predicted is ~1.23 * this value, which occurs at M=1.10.

            In the high-Mach limit, this function asymptotes at 0.80 * this value, as empirically stated by Raymer.
            However, this model is only approximate and is likely not valid for high-supersonic flows.

    Returns: The approximate wave drag coefficient at the specified Mach number.

        The reference area is whatever the reference area used in the `CD_wave_at_fully_supersonic` parameter is.

    """
    mach_crit_max = 1 - (0.1 / 80)**(1 / 3)

    mach_crit = -np.softmax(-mach_crit, -mach_crit_max, hardness=50)

    ### The following approximate relation is derived in W.H. Mason, "Configuration Aerodynamics", Chapter 7. Transonic Aerodynamics of Airfoils and Wings.
    ### Equation 7-8 on Page 7-19.
    ### This is in turn based on Lock's proposed empirically-derived shape of the drag rise, from Hilton, W.F., High Speed Aerodynamics, Longmans, Green & Co., London, 1952, pp. 47-49
    mach_dd = mach_crit + (0.1 / 80)**(1 / 3)

    ### Model drag sections and cutoffs:
    return CD_wave_at_fully_supersonic * np.where(
        mach < mach_crit,
        0,
        np.where(
            mach < mach_dd,
            20 * (mach - mach_crit)**4,
            np.where(
                mach < 1.05,
                cubic_hermite_patch(mach,
                                    x_a=mach_dd,
                                    x_b=1.05,
                                    f_a=20 * (0.1 / 80)**(4 / 3),
                                    f_b=1,
                                    dfdx_a=0.1,
                                    dfdx_b=10),
                np.where(mach < 1.2,
                         cubic_hermite_patch(mach,
                                             x_a=1.05,
                                             x_b=1.2,
                                             f_a=1,
                                             f_b=1,
                                             dfdx_a=10,
                                             dfdx_b=-4),
                         np.blend(
                             switch=4 * 2 * (mach - 1.2) / (1.2 - 0.8),
                             value_switch_high=0.8,
                             value_switch_low=1.2,
                         )
                         # 0.8 + 0.2 * np.exp(20 * (1.2 - mach))
                         ))))
def _calculate_induced_velocity_line_singularity_panel_coordinates(
    xp_field: Union[float, np.ndarray],
    yp_field: Union[float, np.ndarray],
    gamma_start: float = 0.,
    gamma_end: float = 0.,
    sigma_start: float = 0.,
    sigma_end: float = 0.,
    xp_panel_end: float = 1.,
) -> [Union[float, np.ndarray], Union[float, np.ndarray]]:
    """
    Calculates the induced velocity at a point (xp_field, yp_field) in a 2D potential-flow flowfield.

    The `p` suffix in `xp...` and `yp...` denotes the use of the panel coordinate system, where:
        * xp_hat is along the length of the panel
        * yp_hat is orthogonal (90 deg. counterclockwise) to it.

    In this flowfield, there is only one singularity element: A line vortex going from (0, 0) to (xp_panel_end, 0).
    The strength of this vortex varies linearly from:
        * gamma_start at (0, 0), to:
        * gamma_end at (xp_panel_end, 0). # TODO update paragraph

    By convention here, positive gamma induces clockwise swirl in the flow field.
        
    Function returns the 2D velocity u, v in the local coordinate system of the panel.

    Inputs x and y can be 1D ndarrays representing various field points,
    in which case the resulting velocities u and v have corresponding dimensionality.

    Equations from the seminal textbook "Low Speed Aerodynamics" by Katz and Plotkin.
    Vortex equations are Eq. 11.99 and Eq. 11.100.
        * Note: there is an error in equation 11.100 in Katz and Plotkin, at least in the 2nd ed:
        The last term of equation 11.100, which is given as:
            (x_{j+1} - x_j) / z + (theta_{j+1} - theta_j)
        has a sign error and should instead be written as:
            (x_{j+1} - x_j) / z - (theta_{j+1} - theta_j)
    Source equations are Eq. 11.89 and Eq. 11.90.

    """
    ### Modify any incoming floats
    if isinstance(xp_field, (float, int)):
        xp_field = np.array([xp_field])
    if isinstance(yp_field, (float, int)):
        yp_field = np.array([yp_field])

    ### Determine if you can skip either the vortex or source parts
    skip_vortex_math = not (isinstance(gamma_start, cas.MX) or isinstance(
        gamma_end, cas.MX)) and gamma_start == 0 and gamma_end == 0
    skip_source_math = not (isinstance(sigma_start, cas.MX) or isinstance(
        sigma_end, cas.MX)) and sigma_start == 0 and sigma_end == 0

    ### Determine which points are effectively on the panel, necessitating different math:
    is_on_panel = np.fabs(yp_field) <= 1e-8

    ### Do some geometry calculation
    r_1 = (xp_field**2 + yp_field**2)**0.5
    r_2 = ((xp_field - xp_panel_end)**2 + yp_field**2)**0.5

    ### Regularize
    is_on_endpoint = ((r_1 == 0) | (r_2 == 0))
    r_1 = np.where(
        r_1 == 0,
        1,
        r_1,
    )
    r_2 = np.where(r_2 == 0, 1, r_2)

    ### Continue geometry calculation
    theta_1 = np.arctan2(yp_field, xp_field)
    theta_2 = np.arctan2(yp_field, xp_field - xp_panel_end)
    ln_r_2_r_1 = np.log(r_2 / r_1)
    d_theta = theta_2 - theta_1
    tau = 2 * np.pi

    ### Regularize if the point is on the panel.
    yp_field_regularized = np.where(is_on_panel, 1, yp_field)

    ### VORTEX MATH
    if skip_vortex_math:
        u_vortex = 0
        v_vortex = 0
    else:
        d_gamma = gamma_end - gamma_start
        u_vortex_term_1_quantity = (yp_field / tau * d_gamma / xp_panel_end)
        u_vortex_term_2_quantity = (gamma_start * xp_panel_end +
                                    d_gamma * xp_field) / (tau * xp_panel_end)

        # Calculate u_vortex
        u_vortex_term_1 = u_vortex_term_1_quantity * ln_r_2_r_1
        u_vortex_term_2 = u_vortex_term_2_quantity * d_theta
        u_vortex = u_vortex_term_1 + u_vortex_term_2

        # Correct the u-velocity if field point is on the panel
        u_vortex = np.where(is_on_panel, 0, u_vortex)

        # Calculate v_vortex
        v_vortex_term_1 = u_vortex_term_2_quantity * ln_r_2_r_1

        v_vortex_term_2 = np.where(
            is_on_panel,
            d_gamma / tau,
            u_vortex_term_1_quantity *
            (xp_panel_end / yp_field_regularized - d_theta),
        )

        v_vortex = v_vortex_term_1 + v_vortex_term_2

    ### SOURCE MATH
    if skip_source_math:
        u_source = 0
        v_source = 0
    else:
        d_sigma = sigma_end - sigma_start
        v_source_term_1_quantity = (yp_field / tau * d_sigma / xp_panel_end)
        v_source_term_2_quantity = (sigma_start * xp_panel_end +
                                    d_sigma * xp_field) / (tau * xp_panel_end)
        # Calculate v_source
        v_source_term_1 = -v_source_term_1_quantity * ln_r_2_r_1
        v_source_term_2 = v_source_term_2_quantity * d_theta
        v_source = v_source_term_1 + v_source_term_2

        # Correct the v-velocity if field point is on the panel
        v_source = np.where(is_on_panel, 0, v_source)

        # Calculate u_source
        u_source_term_1 = -v_source_term_2_quantity * ln_r_2_r_1

        u_source_term_2 = np.where(
            is_on_panel,
            -d_sigma / tau,
            -v_source_term_1_quantity *
            (xp_panel_end / yp_field_regularized - d_theta),
        )

        u_source = u_source_term_1 + u_source_term_2

    ### Return
    u = u_vortex + u_source
    v = v_vortex + v_source

    ### If the field point is on the endpoint of the panel, replace the NaN with a zero.
    u = np.where(is_on_endpoint, 0, u)
    v = np.where(is_on_endpoint, 0, v)

    return u, v
示例#19
0
def get_NACA_coordinates(
        name: str = 'naca2412',
        n_points_per_side: int = _default_n_points_per_side) -> np.ndarray:
    """
    Returns the coordinates of a specified 4-digit NACA airfoil.
    Args:
        name: Name of the NACA airfoil.
        n_points_per_side: Number of points per side of the airfoil (top/bottom).

    Returns: The coordinates of the airfoil as a Nx2 ndarray [x, y]

    """
    name = name.lower().strip()

    if not "naca" in name:
        raise ValueError("Not a NACA airfoil!")

    nacanumber = name.split("naca")[1]
    if not nacanumber.isdigit():
        raise ValueError("Couldn't parse the number of the NACA airfoil!")

    if not len(nacanumber) == 4:
        raise NotImplementedError(
            "Only 4-digit NACA airfoils are currently supported!")

    # Parse
    max_camber = int(nacanumber[0]) * 0.01
    camber_loc = int(nacanumber[1]) * 0.1
    thickness = int(nacanumber[2:]) * 0.01

    # Referencing https://en.wikipedia.org/wiki/NACA_airfoil#Equation_for_a_cambered_4-digit_NACA_airfoil
    # from here on out

    # Make uncambered coordinates
    x_t = np.cosspace(0, 1,
                      n_points_per_side)  # Generate some cosine-spaced points
    y_t = 5 * thickness * (
        +0.2969 * x_t**0.5 - 0.1260 * x_t - 0.3516 * x_t**2 + 0.2843 * x_t**3 -
        0.1015 * x_t**4  # 0.1015 is original, #0.1036 for sharp TE
    )

    if camber_loc == 0:
        camber_loc = 0.5  # prevents divide by zero errors for things like naca0012's.

    # Get camber
    y_c = np.where(
        x_t <= camber_loc,
        max_camber / camber_loc**2 * (2 * camber_loc * x_t - x_t**2),
        max_camber / (1 - camber_loc)**2 *
        ((1 - 2 * camber_loc) + 2 * camber_loc * x_t - x_t**2))

    # Get camber slope
    dycdx = np.where(x_t <= camber_loc,
                     2 * max_camber / camber_loc**2 * (camber_loc - x_t),
                     2 * max_camber / (1 - camber_loc)**2 * (camber_loc - x_t))
    theta = np.arctan(dycdx)

    # Combine everything
    x_U = x_t - y_t * np.sin(theta)
    x_L = x_t + y_t * np.sin(theta)
    y_U = y_c + y_t * np.cos(theta)
    y_L = y_c - y_t * np.cos(theta)

    # Flip upper surface so it's back to front
    x_U, y_U = x_U[::-1], y_U[::-1]

    # Trim 1 point from lower surface so there's no overlap
    x_L, y_L = x_L[1:], y_L[1:]

    x = np.hstack((x_U, x_L))
    y = np.hstack((y_U, y_L))

    return stack_coordinates(x, y)
示例#20
0
opti = asb.Opti()

theta = opti.variable(
    init_guess=theta_0,
    n_vars=N,
)
H = opti.variable(
    H_0,
    n_vars=N,
)

Re_theta = ue * theta / nu

H_star = np.where(
    H < 4,
    1.515 + 0.076 * (H - 4) ** 2 / H,
    1.515 + 0.040 * (H - 4) ** 2 / H
)  # From AVF Eq. 4.53
c_f = 2 / Re_theta * np.where(
    H < 6.2,
    -0.066 + 0.066 * np.abs(6.2 - H) ** 1.5 / (H - 1),
    -0.066 + 0.066 * (H - 6.2) ** 2 / (H - 4) ** 2
)  # From AVF Eq. 4.54
c_D = H_star / 2 / Re_theta * np.where(
    H < 4,
    0.207 + 0.00205 * np.abs(4 - H) ** 5.5,
    0.207 - 0.100 * (H - 4) ** 2 / H ** 2
)  # From AVF Eq. 4.55
Re_theta_o = 10 ** (
        2.492 / (H - 1) ** 0.43 +
        0.7 * (
示例#21
0
    def variable(
        self,
        init_guess: Union[float, np.ndarray],
        n_vars: int = None,
        scale: float = None,
        freeze: bool = False,
        log_transform: bool = False,
        category: str = "Uncategorized",
        lower_bound: float = None,
        upper_bound: float = None,
    ) -> cas.MX:
        """
        Initializes a new decision variable (or vector of decision variables). You must pass an initial guess (
        `init_guess`) upon defining a new variable. Dimensionality is inferred from this initial guess, but it can be
        overridden; see below for syntax.

        It is highly, highly recommended that you provide a scale (`scale`) for each variable, especially for
        nonconvex problems, although this is not strictly required.

        Args:

            init_guess: Initial guess for the optimal value of the variable being initialized. This is where in the
            design space the optimizer will start looking.

                This can be either a float or a NumPy ndarray; the dimension of the variable (i.e. scalar,
                vector) that is created will be automatically inferred from the shape of the initial guess you
                provide here. (Although it can also be overridden using the `n_vars` parameter; see below.)

                For scalar variables, your initial guess should be a float:

                >>> opti = asb.Opti()
                >>> scalar_var = opti.variable(init_guess=5) # Initializes a scalar variable at a value of 5

                For vector variables, your initial guess should be either:

                    * a float, in which case you must pass the length of the vector as `n_vars`, otherwise a scalar
                    variable will be created:

                    >>> opti = asb.Opti()
                    >>> vector_var = opti.variable(init_guess=5, n_vars=10) # Initializes a vector variable of length
                    >>> # 10, with all 10 elements set to an initial guess of 5.

                    * a NumPy ndarray, in which case each element will be initialized to the corresponding value in
                    the given array:

                    >>> opti = asb.Opti()
                    >>> vector_var = opti.variable(init_guess=np.linspace(0, 5, 10)) # Initializes a vector variable of
                    >>> # length 10, with all 10 elements initialized to linearly vary between 0 and 5.

                In the case where the variable is to be log-transformed (see `log_transform`), the initial guess
                should not be log-transformed as well - just supply the initial guess as usual. (Log-transform of the
                initial guess happens under the hood.) The initial guess must, of course, be a positive number in
                this case.

            n_vars: [Optional] Used to manually override the dimensionality of the variable to create; if not
            provided, the dimensionality of the variable is inferred from the initial guess `init_guess`.

                The only real case where you need to use this argument would be if you are initializing a vector
                variable to a scalar value, but you don't feel like using `init_guess=value * np.ones(n_vars)`.
                For example:

                    >>> opti = asb.Opti()
                    >>> vector_var = opti.variable(init_guess=5, n_vars=10) # Initializes a vector variable of length
                    >>> # 10, with all 10 elements set to an initial guess of 5.

            scale: [Optional] Approximate scale of the variable.

                For example, if you're optimizing the design of a automobile and setting the tire diameter as an
                optimization variable, you might choose `scale=0.5`, corresponding to 0.5 meters.

                Properly scaling your variables can have a huge impact on solution speed (or even if the optimizer
                converges at all). Although most modern second-order optimizers (such as IPOPT, used here) are
                theoretically scale-invariant, numerical precision issues due to floating-point arithmetic can make
                solving poorly-scaled problems really difficult or impossible. See here for more info:
                https://web.casadi.org/blog/nlp-scaling/

                If not specified, the code will try to pick a sensible value by defaulting to the `init_guess`.

            freeze: [Optional] This boolean tells the optimizer to "freeze" the variable at a specific value. In
            order to select the determine to freeze the variable at, the optimizer will use the following logic:

                    * If you initialize a new variable with the parameter `freeze=True`: the optimizer will freeze
                    the variable at the value of initial guess.

                        >>> opti = Opti()
                        >>> my_var = opti.variable(init_guess=5, freeze=True) # This will freeze my_var at a value of 5.

                    * If the Opti instance is associated with a cache file, and you told it to freeze a specific
                    category(s) of variables that your variable is a member of, and you didn't manually specify to
                    freeze the variable: the variable will be frozen based on the value in the cache file (and ignore
                    the `init_guess`). Example:

                        >>> opti = Opti(cache_filename="my_file.json", variable_categories_to_freeze=["Wheel Sizing"])
                        >>> # Assume, for example, that `my_file.json` was from a previous run where my_var=10.
                        >>> my_var = opti.variable(init_guess=5, category="Wheel Sizing")
                        >>> # This will freeze my_var at a value of 10 (from the cache file, not the init_guess)

                    * If the Opti instance is associated with a cache file, and you told it to freeze a specific
                    category(s) of variables that your variable is a member of, but you then manually specified that
                    the variable should be frozen: the variable will once again be frozen at the value of `init_guess`:

                        >>> opti = Opti(cache_filename="my_file.json", variable_categories_to_freeze=["Wheel Sizing"])
                        >>> # Assume, for example, that `my_file.json` was from a previous run where my_var=10.
                        >>> my_var = opti.variable(init_guess=5, category="Wheel Sizing", freeze=True)
                        >>> # This will freeze my_var at a value of 5 (`freeze` overrides category loading.)

            Motivation for freezing variables:

                The ability to freeze variables is exceptionally useful when designing engineering systems. Let's say
                we're designing an airplane. In the beginning of the design process, we're doing "clean-sheet" design
                - any variable is up for grabs for us to optimize on, because the airplane doesn't exist yet!
                However, the farther we get into the design process, the more things get "locked in" - we may have
                ordered jigs, settled on a wingspan, chosen an engine, et cetera. So, if something changes later (
                let's say that we discover that one of our assumptions was too optimistic halfway through the design
                process), we have to make up for that lost margin using only the variables that are still free. To do
                this, we would freeze the variables that are already decided on.

                By categorizing variables, you can also freeze entire categories of variables. For example,
                you can freeze all of the wing design variables for an airplane but leave all of the fuselage
                variables free.

                This idea of freezing variables can also be used to look at off-design performance - freeze a
                design, but change the operating conditions.

            log_transform: [Optional] Advanced use only. A flag of whether to internally-log-transform this variable
            before passing it to the optimizer. Good for known positive engineering quantities that become nonsensical
            if negative (e.g. mass). Log-transforming these variables can also help maintain convexity.

            category: [Optional] What category of variables does this belong to?

        Usage notes:

            When using vector variables, individual components of this vector of variables can be accessed via normal
            indexing. Example:
                >>> opti = asb.Opti()
                >>> my_var = opti.variable(n_vars = 5)
                >>> opti.subject_to(my_var[3] >= my_var[2])  # This is a valid way of indexing
                >>> my_sum = asb.sum(my_var)  # This will sum up all elements of `my_var`

        Returns:
            The variable itself as a symbolic CasADi variable (MX type).

        """
        ### Set defaults
        if n_vars is None:  # Infer dimensionality from init_guess if it is not provided
            n_vars = np.length(init_guess)
        if scale is None:  # Infer a scale from init_guess if it is not provided
            if log_transform:
                scale = 1
            else:
                scale = np.mean(
                    np.fabs(init_guess)
                )  # Initialize the scale to a heuristic based on the init_guess
                if scale == 0:  # If that heuristic leads to a scale of 0, use a scale of 1 instead.
                    scale = 1

                scale = np.fabs(np.where(init_guess != 0, init_guess, 1))

        # Validate the inputs
        if log_transform:
            if np.any(init_guess <= 0):
                raise ValueError(
                    "If you are initializing a log-transformed variable, the initial guess(es) must all be positive."
                )
        if np.any(scale <= 0):
            raise ValueError("The 'scale' argument must be a positive number.")

        # If the variable is in a category to be frozen, fix the variable at the initial guess.
        is_manually_frozen = freeze
        if category in self.variable_categories_to_freeze:
            freeze = True

        # If the variable is to be frozen, return the initial guess. Otherwise, define the variable using CasADi symbolics.
        if freeze:
            var = self.parameter(n_params=n_vars, value=init_guess)
        else:
            if not log_transform:
                var = scale * super().variable(n_vars)
                self.set_initial(var, init_guess)
            else:
                log_scale = scale / init_guess
                log_var = log_scale * super().variable(n_vars)
                var = np.exp(log_var)
                self.set_initial(log_var, np.log(init_guess))

        # Track the variable
        if category not in self.variables_categorized:  # Add a category if it does not exist
            self.variables_categorized[category] = []
        self.variables_categorized[category].append(var)
        var.is_manually_frozen = is_manually_frozen

        # Apply bounds
        if lower_bound is not None:
            self.subject_to(var >= lower_bound)
        if upper_bound is not None:
            self.subject_to(var <= upper_bound)

        return var
示例#22
0
        def forces_on_fuselage_section(
                xsec_a: FuselageXSec,
                xsec_b: FuselageXSec,
        ):
            ### Some metrics, like effective force location, are area-weighted. Here, we compute those weights.
            r_a = xsec_a.radius
            r_b = xsec_b.radius

            x_a = xsec_a.xyz_c[0]
            x_b = xsec_b.xyz_c[0]

            area_a = xsec_a.xsec_area()
            area_b = xsec_b.xsec_area()
            total_area = area_a + area_b

            a_weight = area_a / total_area
            b_weight = area_b / total_area

            delta_x = x_b - x_a

            mean_geometric_radius = (r_a + r_b) / 2
            mean_aerodynamic_radius = r_a * a_weight + r_b * b_weight

            force_x_location = x_a * a_weight + x_b * b_weight

            ##### Inviscid Forces
            force_potential_flow = q * (  # From Munk, via Jorgensen
                    np.sind(2 * generalized_alpha) *
                    (area_b - area_a)
            ) # Matches Drela, Flight Vehicle Aerodynamics Eqn. 6.75 in the small-alpha limit.
            # Note that no delta_x should be here; dA/dx * dx = dA.

            # Direction of force is midway between the normal to the axis of revolution of the body and the
            # normal to the free-stream velocity, according to:
            # Ward, via Jorgensen
            force_normal_potential_flow = force_potential_flow * np.cosd(generalized_alpha / 2)
            force_axial_potential_flow = -force_potential_flow * np.sind(generalized_alpha / 2)
            # Reminder: axial force is defined positive-aft

            ##### Viscous Forces

            Re_n = sin_generalized_alpha * op_point.reynolds(reference_length=2 * mean_aerodynamic_radius)
            M_n = sin_generalized_alpha * op_point.mach()

            C_d_n = np.where(
                Re_n != 0,
                aerolib.Cd_cylinder(
                    Re_D=Re_n,
                    mach=M_n
                ),  # Replace with 1.20 from Jorgensen Table 1 if this isn't working well
                0,
            )

            force_viscous_flow = delta_x * q * (
                    2 * eta * C_d_n *
                    sin_squared_generalized_alpha *
                    mean_geometric_radius
            )

            # Viscous crossflow acts exactly normal to vehicle axis, definitionally. (Axial forces accounted for on a total-body basis)
            force_normal_viscous_flow = force_viscous_flow
            force_axial_viscous_flow = 0

            normal_force = force_normal_potential_flow + force_normal_viscous_flow
            axial_force = force_axial_potential_flow + force_axial_viscous_flow

            return normal_force, axial_force, force_x_location
示例#23
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]
        }