def translate( self, translate_x: float = 0., translate_y: float = 0., ) -> 'Airfoil': """ Translates an Airfoil by a given amount. Args: translate_x: Amount to translate in the x-direction translate_y: Amount to translate in the y-direction Returns: The translated Airfoil. """ x = self.x() + translate_x y = self.y() + translate_y return Airfoil(name=self.name, coordinates=stack_coordinates(x, y))
def scale( self, scale_x: float = 1., scale_y: float = 1., ) -> 'Airfoil': """ Scales an Airfoil about the origin. Args: scale_x: Amount to scale in the x-direction. scale_y: Amount to scale in the y-direction. Returns: The scaled Airfoil. """ x = self.x() * scale_x y = self.y() * scale_y if scale_y < 0: x = x[::-1] y = y[::-1] return Airfoil(name=self.name, coordinates=stack_coordinates(x, y))
def repanel( self, n_points_per_side: int = 100, ) -> 'Airfoil': """ Returns a repaneled version of the airfoil with cosine-spaced coordinates on the upper and lower surfaces. :param n_points_per_side: Number of points per side (upper and lower) of the airfoil [int] Notes: The number of points defining the final airfoil will be n_points_per_side*2-1, since one point (the leading edge point) is shared by both the upper and lower surfaces. :return: Returns the new airfoil. """ upper_original_coors = self.upper_coordinates( ) # Note: includes leading edge point, be careful about duplicates lower_original_coors = self.lower_coordinates( ) # Note: includes leading edge point, be careful about duplicates # Find distances between coordinates, assuming linear interpolation upper_distances_between_points = ( (upper_original_coors[:-1, 0] - upper_original_coors[1:, 0])**2 + (upper_original_coors[:-1, 1] - upper_original_coors[1:, 1])** 2)**0.5 lower_distances_between_points = ( (lower_original_coors[:-1, 0] - lower_original_coors[1:, 0])**2 + (lower_original_coors[:-1, 1] - lower_original_coors[1:, 1])** 2)**0.5 upper_distances_from_TE = np.hstack( (0, np.cumsum(upper_distances_between_points))) lower_distances_from_LE = np.hstack( (0, np.cumsum(lower_distances_between_points))) upper_distances_from_TE_normalized = upper_distances_from_TE / upper_distances_from_TE[ -1] lower_distances_from_LE_normalized = lower_distances_from_LE / lower_distances_from_LE[ -1] distances_from_TE_normalized = np.hstack( (upper_distances_from_TE_normalized, 1 + lower_distances_from_LE_normalized[1:])) # Generate a cosine-spaced list of points from 0 to 1 cosspaced_points = np.cosspace(0, 1, n_points_per_side) s = np.hstack(( cosspaced_points, 1 + cosspaced_points[1:], )) # Check that there are no duplicate points in the airfoil. if np.any(np.diff(distances_from_TE_normalized) == 0): raise ValueError( "This airfoil has a duplicated point (i.e. two adjacent points with the same (x, y) coordinates), so you can't repanel it!" ) x = interp1d( distances_from_TE_normalized, self.x(), kind="cubic", )(s) y = interp1d( distances_from_TE_normalized, self.y(), kind="cubic", )(s) return Airfoil(name=self.name, coordinates=stack_coordinates(x, y))
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)