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)
Exemplo n.º 2
0
    def span(
        self,
        type: str = "wetted",
        _sectional: bool = False,
    ) -> float:
        """
        Returns the span, with options for various ways of measuring this.
         * wetted: Adds up YZ-distances of each section piece by piece
         * y: Adds up the Y-distances of each section piece by piece
         * z: Adds up the Z-distances of each section piece by piece
         * y-full: Y-distance between the XZ plane and the tip of the wing. (Can't be used with _sectional).
        If symmetric, this is doubled left/right to obtain the full span.

        Args:
            type: One of the above options, as a string.
            _sectional: A boolean. If False, returns the total span. If True, returns a list of spans for each of the
                `n-1` lofted sections (between the `n` wing cross sections in wing.xsec).
        """
        if type == "y-full":
            if _sectional:
                raise ValueError(
                    "Cannot use `_sectional` with the parameter type as `y-full`!"
                )
            return self.xsecs[-1].quarter_chord()[1]

        sectional_spans = []

        for inner_xsec, outer_xsec in zip(self.xsecs[:-1], self.xsecs[1:]):
            quarter_chord_vector = outer_xsec.quarter_chord(
            ) - inner_xsec.quarter_chord()

            if type == "wetted":
                section_span = (quarter_chord_vector[1]**2 +
                                quarter_chord_vector[2]**2)**0.5
            elif type == "y":
                section_span = (np.fabs(quarter_chord_vector[1]))
            elif type == "z":
                section_span = (np.fabs(quarter_chord_vector[2]))
            else:
                raise ValueError("Bad value of 'type'!")

            sectional_spans.append(section_span)

        span = sum(sectional_spans)

        if self.symmetric:
            span *= 2

        if _sectional:
            return sectional_spans
        else:
            return span
Exemplo n.º 3
0
def Cl_flat_plate(alpha, Re_c):
    """
    Returns the approximate lift coefficient of a flat plate, following thin airfoil theory.
    :param alpha: Angle of attack [deg]
    :param Re_c: Reynolds number, normalized to the length of the flat plate.
    :return: Approximate lift coefficient.
    """
    Re_c = np.fabs(Re_c)
    alpha_rad = alpha * np.pi / 180
    return 2 * np.pi * alpha_rad
Exemplo n.º 4
0
def wind_speed_conus_summer_99(altitude, latitude):
    """
    Returns the 99th-percentile wind speed magnitude over the continental United States (CONUS) in July-Aug. Aggregate of data from 1972 to 2019.
    Fits at C:\Projects\GitHub\Wind_Analysis
    :param altitude: altitude [m]
    :param latitude: latitude [deg]
    :return: 99th-percentile wind speed over the continental United States in the summertime. [m/s]
    """
    l = (latitude - 37.5) / 11.5
    a = (altitude - 24200) / 24200

    agc = -0.5363486000267786
    agh = 1.9569754777072828
    ags = 0.1458701999734713
    aqc = -1.4645014948089652
    c0 = -0.5169694086686631
    c12 = 0.0849519807021402
    c21 = -0.0252010113059998
    c4a = 0.0225856848053377
    c4c = 1.0281877353734501
    cg = 0.8050736230004489
    cgc = 0.2786691793571486
    cqa = 0.1866078047914259
    cql = 0.0165126852561671
    cqla = -0.1361667658248024
    lgc = 0.6943655538727291
    lgh = 2.0777449841036777
    lgs = 0.9805766577269118
    lqc = 4.0356834595743214

    s = c0 + cql * (l -
                    lqc)**2 + cqa * (a - aqc)**2 + cqla * a * l + cg * np.exp(
                        -(np.fabs(l - lgc)**lgh /
                          (2 * lgs**2) + np.fabs(a - agc)**agh /
                          (2 * ags**2) + cgc * a * l)) + c4a * (
                              a - c4c)**4 + c12 * l * a**2 + c21 * l**2 * a

    speed = s * 56 + 7
    return speed
def model(x, p):
    l = x["lats_scaled"]
    a = x["alts_scaled"]

    agc = p["agc"]
    agh = p["agh"]
    ags = p["ags"]
    aqc = p["aqc"]
    c0 = p["c0"]
    c12 = p["c12"]
    c21 = p["c21"]
    c4a = p["c4a"]
    c4c = p["c4c"]
    cg = p["cg"]
    cgc = p["cgc"]
    cqa = p["cqa"]
    cql = p["cql"]
    cqla = p["cqla"]
    lgc = p["lgc"]
    lgh = p["lgh"]
    lgs = p["lgs"]
    lqc = p["lqc"]

    return (c0  # Constant
            + cql * (l - lqc)**2  # Quadratic in latitude
            + cqa * (a - aqc)**2  # Quadratic in altitude
            + cqla * a * l  # Quadratic cross-term
            + cg * np.exp(-(  # Gaussian bump
                np.fabs(l - lgc)**lgh /
                (2 * lgs**2) +  # Center/Spread in latitude
                np.fabs(a - agc)**agh /
                (2 * ags**2) +  # Center/Spread in altitude
                cgc * a * l  # Gaussian cross-term
            )) + c4a * (a - c4c)**4  # Altitude quartic
            + c12 * l * a**2  # Altitude linear-quadratic
            + c21 * l**2 * a  # Latitude linear-quadratic
            )
Exemplo n.º 6
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
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
Exemplo n.º 8
0
def motor_electric_performance(
        voltage=None,
        current=None,
        rpm=None,
        torque=None,
        kv=1000,  # rpm/volt
        resistance=0.1,  # ohms
        no_load_current=0.4  # amps
):
    """
    A function for predicting the performance of an electric motor.
    Performance equations based on Mark Drela's First Order Motor Model:
    http://web.mit.edu/drela/Public/web/qprop/motor1_theory.pdf
    Instructions: Input EXACTLY TWO of the following parameters: voltage, current, rpm, torque.
    Exception: You cannot supply the combination of current and torque - this makes for an ill-posed problem.
    :param voltage: Voltage across motor terminals [Volts]
    :param current: Current through motor [Amps]
    :param rpm: Motor rotation speed [rpm]
    :param torque: Motor torque [N m]
    :param kv: voltage constant, in rpm/volt
    :param resistance: resistance, in ohms
    :param no_load_current: no-load current, in amps
    :return: dict of {voltage, current, rpm, torque, efficiency}
    """
    # Validate inputs
    voltage_known = voltage is not None
    current_known = current is not None
    rpm_known = rpm is not None
    torque_known = torque is not None

    assert (voltage_known + current_known + rpm_known +
            torque_known) == 2, "You must give exactly two input arguments."
    assert not (
        current_known and torque_known
    ), "You cannot supply the combination of current and torque - this makes for an ill-posed problem."

    kv_rads_per_sec_per_volt = kv * np.pi / 30  # rads/sec/volt

    while not (voltage_known and current_known and rpm_known and torque_known):
        if rpm_known:
            if current_known and not voltage_known:
                speed = rpm * np.pi / 30  # rad/sec
                back_EMF_voltage = speed / kv_rads_per_sec_per_volt
                voltage = back_EMF_voltage + current * resistance
                voltage_known = True

        if torque_known:
            if not current_known:
                current = torque * kv_rads_per_sec_per_volt + no_load_current
                current_known = True

        if voltage_known:
            if rpm_known and not current_known:
                speed = rpm * np.pi / 30  # rad/sec
                back_EMF_voltage = speed / kv_rads_per_sec_per_volt
                current = (voltage - back_EMF_voltage) / resistance
                current_known = True
            if not rpm_known and current_known:
                back_EMF_voltage = voltage - (current * resistance)
                speed = back_EMF_voltage * kv_rads_per_sec_per_volt
                rpm = speed * 30 / np.pi
                rpm_known = True

        if current_known:
            if not torque_known:
                torque = (current - no_load_current) / kv_rads_per_sec_per_volt
                torque_known = True

    shaft_power = (rpm * np.pi / 30) * torque
    electrical_power = voltage * current
    efficiency = shaft_power / electrical_power
    waste_heat = np.fabs(electrical_power - shaft_power)

    return {
        "voltage": voltage,
        "current": current,
        "rpm": rpm,
        "torque": torque,
        "shaft power": shaft_power,
        "electrical power": electrical_power,
        "efficiency": efficiency,
        "waste heat": waste_heat,
    }
Exemplo n.º 9
0
def Cf_flat_plate(
        Re_L: float,
        method="hybrid-sharpe-convex"
) -> float:
    """
    Returns the mean skin friction coefficient over a flat plate.

    Don't forget to double it (two sides) if you want a drag coefficient.

    Args:

        Re_L: Reynolds number, normalized to the length of the flat plate.

        method: The method of computing the skin friction coefficient. One of:

            * "blasius": Uses the Blasius solution. Citing Cengel and Cimbala, "Fluid Mechanics: Fundamentals and
            Applications", Table 10-4.

                Valid approximately for Re_L <= 5e5.

            * "turbulent": Uses turbulent correlations for smooth plates. Citing Cengel and Cimbala,
            "Fluid Mechanics: Fundamentals and Applications", Table 10-4.

                Valid approximately for 5e5 <= Re_L <= 1e7.

            * "hybrid-cengel": Uses turbulent correlations for smooth plates, but accounts for a
            non-negligible laminar run at the beginning of the plate. Citing Cengel and Cimbala, "Fluid Mechanics:
            Fundamentals and Applications", Table 10-4. Returns: Mean skin friction coefficient over a flat plate.

                Valid approximately for 5e5 <= Re_L <= 1e7.

            * "hybrid-schlichting": Schlichting's model, that roughly accounts for a non-negligtible laminar run.
            Citing "Boundary Layer Theory" 7th Ed., pg. 644

            * "hybrid-sharpe-convex": A hybrid model that blends the Blasius and Schlichting models. Convex in
            log-log space; however, it may overlook some truly nonconvex behavior near transitional Reynolds numbers.

            * "hybrid-sharpe-nonconvex": A hybrid model that blends the Blasius and Cengel models. Nonconvex in
            log-log-space; however, it may capture some truly nonconvex behavior near transitional Reynolds numbers.

    You can view all of these functions graphically using
    `aerosandbox.library.aerodynamics.test_aerodynamics.test_Cf_flat_plate.py`

    """
    Re_L = np.fabs(Re_L)

    if method == "blasius":
        return 1.328 / Re_L ** 0.5
    elif method == "turbulent":
        return 0.074 / Re_L ** (1 / 5)
    elif method == "hybrid-cengel":
        return 0.074 / Re_L ** (1 / 5) - 1742 / Re_L
    elif method == "hybrid-schlichting":
        return 0.02666 * Re_L ** -0.139
    elif method == "hybrid-sharpe-convex":
        return np.softmax(
            Cf_flat_plate(Re_L, method="blasius"),
            Cf_flat_plate(Re_L, method="hybrid-schlichting"),
            hardness=1e3
        )
    elif method == "hybrid-sharpe-nonconvex":
        return np.softmax(
            Cf_flat_plate(Re_L, method="blasius"),
            Cf_flat_plate(Re_L, method="hybrid-cengel"),
            hardness=1e3
        )