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
예제 #2
0
def indicial_gust_response(
        reduced_time: Union[float, np.ndarray],
        gust_velocity: float,
        plate_velocity: float,
        angle_of_attack: float = 0,  # In degrees
        chord: float = 1):
    """
    Computes the evolution of the lift coefficient of a flat plate entering a 
    an infinitely long, sharp step gust (Heaveside function) at a constant angle of attack. 
    Reduced_time = 0 corresponds to the instance the gust is entered
    
    
    (Leishman, Principles of Helicopter Aerodynamics, S8.10,S8.11)
    
    Args:
        reduced_time (float,np.ndarray) : Reduced time, equal to the number of semichords travelled. See function reduced_time
        gust_velocity (float) : velocity in m/s of the top hat gust
        velocity (float) : velocity of the thin airfoil entering the gust
        angle_of_attack (float) : The angle of attack, in degrees
        chord (float) : The chord of the plate in meters
    """
    angle_of_attack_radians = np.deg2rad(angle_of_attack)
    offset = chord / 2 * (1 - np.cos(angle_of_attack_radians))
    return (2 * np.pi * np.arctan(gust_velocity / plate_velocity) *
            np.cos(angle_of_attack_radians) *
            kussners_function(reduced_time - offset))
예제 #3
0
def sigmoid(
        x,
        sigmoid_type: str = "tanh",
        normalization_range: Tuple[Union[float, int], Union[float, int]] = (0, 1)
):
    """
    A sigmoid function. From Wikipedia (https://en.wikipedia.org/wiki/Sigmoid_function):
        A sigmoid function is a mathematical function having a characteristic "S"-shaped curve
        or sigmoid curve.

    Args:
        x: The input
        sigmoid_type: Type of sigmoid function to use [str]. Can be one of:
            * "tanh" or "logistic" (same thing)
            * "arctan"
            * "polynomial"
        normalization_type: Range in which to normalize the sigmoid, shorthanded here in the
            documentation as "N". This parameter is given as a two-element tuple (min, max).

            After normalization:
                >>> sigmoid(-Inf) == normalization_range[0]
                >>> sigmoid(Inf) == normalization_range[1]

            * In the special case of N = (0, 1):
                >>> sigmoid(-Inf) == 0
                >>> sigmoid(Inf) == 1
                >>> sigmoid(0) == 0.5
                >>> d(sigmoid)/dx at x=0 == 0.5
            * In the special case of N = (-1, 1):
                >>> sigmoid(-Inf) == -1
                >>> sigmoid(Inf) == 1
                >>> sigmoid(0) == 0
                >>> d(sigmoid)/dx at x=0 == 1

    Returns: The value of the sigmoid.
    """
    ### Sigmoid equations given here under the (-1, 1) normalization:
    if sigmoid_type == ("tanh" or "logistic"):
        # Note: tanh(x) is simply a scaled and shifted version of a logistic curve; after
        #   normalization these functions are identical.
        s = _np.tanh(x)
    elif sigmoid_type == "arctan":
        s = 2 / _np.pi * _np.arctan(_np.pi / 2 * x)
    elif sigmoid_type == "polynomial":
        s = x / (1 + x ** 2) ** 0.5
    else:
        raise ValueError("Bad value of parameter 'type'!")

    ### Normalize
    min = normalization_range[0]
    max = normalization_range[1]
    s_normalized = s * (max - min) / 2 + (max + min) / 2

    return s_normalized
예제 #4
0
def calculate_kussner_lift_coefficient(reduced_time: Union[float, np.ndarray],
                                       gust_strength: float, velocity: float):
    """
    Computes the evolution of the lift coefficient of a flat plate entering a sharp, transverse, top hat gust
    Reduced_time = 0 corresponds to the instance the gust is entered
    
    (Leishman, Principles of Helicopter Aerodynamics, S8.10,S8.11)
    
    Args:
        reduced_time (float,np.ndarray) : Reduced time, equal to the number of semichords travelled. See function reduced_time
        gust_strength (float) : velocity in m/s of the top hat gust
        velocity (float) : velocity of the thin airfoil entering the gust

    """
    return 2 * np.pi * np.arctan(
        gust_strength / velocity) * kussners_function(reduced_time)
예제 #5
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)
예제 #6
0
    def draw_bending(self,
                     show=True,
                     for_print=False,
                     equal_scale=True,
                     ):
        """
        Draws a figure that illustrates some bending properties. Must be called on a solved object (i.e. using the substitute_sol method).
        :param show: Whether or not to show the figure [boolean]
        :param for_print: Whether or not the figure should be shaped for printing in a paper [boolean]
        :param equal_scale: Whether or not to make the displacement plot have equal scale (i.e. true deformation only)
        :return:
        """
        import matplotlib.pyplot as plt
        import seaborn as sns
        sns.set(font_scale=1)

        fig, ax = plt.subplots(
            2 if not for_print else 3,
            3 if not for_print else 2,
            figsize=(
                10 if not for_print else 6,
                6 if not for_print else 6
            ),
            dpi=200
        )

        plt.subplot(231) if not for_print else plt.subplot(321)
        plt.plot(self.x, self.u, '.-')
        plt.xlabel(r"$x$ [m]")
        plt.ylabel(r"$u$ [m]")
        plt.title("Displacement (Bending)")
        if equal_scale:
            plt.axis("equal")

        plt.subplot(232) if not for_print else plt.subplot(322)
        plt.plot(self.x, np.arctan(self.du) * 180 / np.pi, '.-')
        plt.xlabel(r"$x$ [m]")
        plt.ylabel(r"Local Slope [deg]")
        plt.title("Slope")

        plt.subplot(233) if not for_print else plt.subplot(323)
        plt.plot(self.x, self.force_per_unit_length, '.-')
        plt.xlabel(r"$x$ [m]")
        plt.ylabel(r"$q$ [N/m]")
        plt.title("Local Load per Unit Span")

        plt.subplot(234) if not for_print else plt.subplot(324)
        plt.plot(self.x, self.stress_axial / 1e6, '.-')
        plt.xlabel(r"$x$ [m]")
        plt.ylabel("Axial Stress [MPa]")
        plt.title("Axial Stress")

        plt.subplot(235) if not for_print else plt.subplot(325)
        plt.plot(self.x, self.dEIddu, '.-')
        plt.xlabel(r"$x$ [m]")
        plt.ylabel(r"$F$ [N]")
        plt.title("Shear Force")

        plt.subplot(236) if not for_print else plt.subplot(326)
        plt.plot(self.x, self.nominal_diameter, '.-')
        plt.xlabel(r"$x$ [m]")
        plt.ylabel("Diameter [m]")
        plt.title("Optimal Spar Diameter")
        plt.tight_layout()

        plt.show() if show else None
예제 #7
0
    def performance(
        self,
        speed,
        throttle_state=1,
        grade=0,
        headwind=0,
    ):
        ##### Figure out electric thrust force

        wheel_radius = self.wheel_diameter / 2
        wheel_rads_per_sec = speed / wheel_radius
        wheel_rpm = wheel_rads_per_sec * 30 / np.pi
        motor_rpm = wheel_rpm / self.gear_ratio

        ### Limit performance by either max voltage or max current
        perf_via_max_voltage = self.motor.performance(
            voltage=self.max_voltage,
            rpm=motor_rpm,
        )
        perf_via_throttle = self.motor.performance(
            current=self.max_current * throttle_state,
            rpm=motor_rpm,
        )

        if perf_via_max_voltage['torque'] > perf_via_throttle['torque']:
            perf = perf_via_throttle
        else:
            perf = perf_via_max_voltage

        motor_torque = perf['torque']
        wheel_torque = motor_torque / self.gear_ratio
        wheel_force = wheel_torque / wheel_radius

        thrust = wheel_force

        ##### Gravity

        gravity_drag = 9.81 * np.sin(np.arctan(grade)) * self.mass

        ##### Rolling Resistance

        # Crr = 0.0020  # Concrete
        Crr = 0.0050  # Asphalt
        # Crr = 0.0060  # Gravel
        # Crr = 0.0070  # Grass
        # Crr = 0.0200  # Off-road
        # Crr = 0.0300  # Sand
        rolling_drag = 9.81 * np.cos(np.arctan(grade)) * self.mass * Crr

        ##### Aerodynamics
        # CDA = 0.408  # Tops
        CDA = 0.324  # Hoods
        # CDA = 0.307  # Drops
        # CDA = 0.2914  # Aerobars

        eff_speed = speed + headwind
        air_drag = 0.5 * atmo.density() * eff_speed * np.abs(eff_speed) * CDA

        ##### Summation
        net_force = thrust - gravity_drag - rolling_drag - air_drag
        net_accel = net_force / self.mass

        return {
            "net acceleration": net_accel,
            "motor state": perf,
        }