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
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))
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
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)
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)
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
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, }