Source code for bladex.profiles

"""
Derived module from profilebase.py to provide the airfoil coordinates.
"""
from scipy.interpolate import splev, splrep
import numpy as np
from .profilebase import ProfileBase


[docs]class CustomProfile(ProfileBase): """ Provide custom profile for the airfoil coordinates. :param numpy.ndarray xup: 1D array that contains the X-components of the airfoil's upper surface :param numpy.ndarray xdown: 1D array that contains the X-components of the airfoil's lower surface :param numpy.ndarray yup: 1D array that contains the Y-components of the airfoil's upper surface :param numpy.ndarray ydown: 1D array that contains the Y-components of the airfoil's lower surface """ def __init__(self, xup, yup, xdown, ydown): super(CustomProfile, self).__init__() self.xup_coordinates = xup self.yup_coordinates = yup self.xdown_coordinates = xdown self.ydown_coordinates = ydown self._check_coordinates()
[docs] def _check_coordinates(self): """ Private method that checks whether the airfoil coordinates defined are provided correctly. We note that each array of coordinates must be consistent with the other arrays. The upper and lower surfaces should start from exactly the same point, the leading edge, and proceed on the way till the trailing edge. The trailing edge might have a non-zero thickness as in the case of some NACA-airfoils. In case of an open trailing edge, the average coordinate between upper and lower part is taken as the unique value. :raises ValueError: if either xup, xdown, yup, ydown is None :raises ValueError: if the 1D arrays xup, yup or xdown, ydown do not have the same length :raises ValueError: if array yup not greater than or equal array ydown element-wise :raises ValueError: if xdown[0] != xup[0] or ydown[0] != yup[0] or xdown[-1] != xup[-1] """ if self.xup_coordinates is None: raise ValueError( 'object "xup_coordinates" refers to an empty array.') if self.xdown_coordinates is None: raise ValueError( 'object "xdown_coordinates" refers to an empty array.') if self.yup_coordinates is None: raise ValueError( 'object "yup_coordinates" refers to an empty array.') if self.ydown_coordinates is None: raise ValueError( 'object "ydown_coordinates" refers to an empty array.') if not isinstance(self.xup_coordinates, np.ndarray): self.xup_coordinates = np.asarray(self.xup_coordinates, dtype=float) if not isinstance(self.xdown_coordinates, np.ndarray): self.xdown_coordinates = np.asarray( self.xdown_coordinates, dtype=float) if not isinstance(self.yup_coordinates, np.ndarray): self.yup_coordinates = np.asarray(self.yup_coordinates, dtype=float) if not isinstance(self.ydown_coordinates, np.ndarray): self.ydown_coordinates = np.asarray( self.ydown_coordinates, dtype=float) # Therefore the arrays xup_coordinates and yup_coordinates must have # the same length = N, same holds for the arrays xdown_coordinates # and ydown_coordinates. if self.xup_coordinates.shape != self.yup_coordinates.shape: raise ValueError( 'xup_coordinates and yup_coordinates must have same shape.') if self.xdown_coordinates.shape != self.ydown_coordinates.shape: raise ValueError( 'xdown_coordinates and ydown_coordinates must have same shape.') # The condition yup_coordinates >= ydown_coordinates must be satisfied # element-wise to the whole elements in the mentioned arrays. if not all( np.greater_equal(self.yup_coordinates, self.ydown_coordinates)): raise ValueError( 'yup_coordinates is not >= ydown_coordinates elementwise.') if not self.xdown_coordinates[0] == self.xup_coordinates[0]: raise ValueError( '(xdown_coordinates[0]=xup_coordinates[0]) not satisfied.') if not self.ydown_coordinates[0] == self.yup_coordinates[0]: raise ValueError( '(ydown_coordinates[0]=yup_coordinates[0]) not satisfied.') if not self.xdown_coordinates[-1] == self.xup_coordinates[-1]: raise ValueError( '(xdown_coordinates[-1]=xup_coordinates[-1]) not satisfied.')
[docs]class NacaProfile(ProfileBase): """ Generate 4- and 5-digit NACA profiles. The NACA airfoils are airfoil shapes for aircraft wings developed by the National Advisory Committee for Aeronautics (NACA). The shape of the NACA airfoils is described using a series of digits following the word "NACA". The parameters in the numerical code can be entered into equations to precisely generate the cross-section of the airfoil and calculate its properties. The NACA four-digit series describes airfoil by the format MPTT, where: - M/100: indicates the maximum camber in percentage, with respect to the chord length. - P/10: indicates the location of the maximum camber measured from the leading edge. The location is normalized by the chord length. - TT/100: the maximum thickness as fraction of the chord length. The profile 00TT refers to a symmetrical NACA airfoil. The NACA five-digit series describes more complex airfoil shapes. Its format is: LPSTT, where: - L: the theoretical optimum lift coefficient at ideal angle-of-attack = 0.15*L - P: the x-coordinate of the point of maximum camber (max camber at x = 0.05*P) - S: indicates whether the camber is simple (S=0) or reflex (S=1) TT/100: the maximum thickness in percent of chord, as in a four-digit NACA airfoil code References: - Moran, Jack (2003). An introduction to theoretical and computational aerodynamics. Dover. p. 7. ISBN 0-486-42879-6. - Abbott, Ira (1959). Theory of Wing Sections: Including a Summary of Airfoil Data. New York: Dover Publications. p. 115. ISBN 978-0486605869. :param str digits: 4 or 5 digits that describes the NACA profile :param int n_points: number of discrete points that represents the airfoil profile. Default value is 240 :param bool cosine_spacing: if True, then a cosine spacing is used for the airfoil coordinate distribution, otherwise linear spacing is used. Default value is True :raises ValueError: if n_points is not positive :raises TypeError: if n_points is not of type int :raises SyntaxError: if digits is not a string :raises Exception: if digits is not of length 4 or 5 """ def __init__(self, digits, n_points=240, cosine_spacing=True): super(NacaProfile, self).__init__() self.digits = digits self.n_points = n_points self.cosine_spacing = cosine_spacing self._check_args() self._generate_coordinates()
[docs] def _check_args(self): """ Private method to check that the number of the airfoil discrete points is a positive integer. """ if not isinstance(self.digits, str): raise TypeError('digits must be of type string.') if isinstance(self.n_points, float): self.n_points = int(self.n_points) if not isinstance(self.n_points, int): raise TypeError('n_points must be of type integer.') if self.n_points < 0: raise ValueError('n_points must be positive.')
[docs] def _generate_coordinates(self): """ Private method that generates the coordinates of the NACA 4 or 5 digits airfoil profile. The method assumes a zero-thickness trailing edge, and no half-cosine spacing. """ a0 = +0.2969 a1 = -0.1260 a2 = -0.3516 a3 = +0.2843 a4 = -0.1036 # zero thickness TE x = np.linspace(0.0, 1.0, num=self.n_points) if len(self.digits) == 4: # Returns n+1 points in [0 1] for the given 4-digits NACA string m = float(self.digits[0]) / 100.0 p = float(self.digits[1]) / 10.0 t = float(self.digits[2:]) / 100.0 # half-thickness distribution yt = 5 * t * (a0 * np.sqrt(x) + a1 * x + a2 * np.power(x, 2) + a3 * np.power(x, 3) + a4 * np.power(x, 4)) if p == 0: # Symmetric foil self.xup_coordinates = np.linspace(0.0, 1.0, num=self.n_points) self.yup_coordinates = yt self.xdown_coordinates = np.linspace( 0.0, 1.0, num=self.n_points) self.ydown_coordinates = -yt else: # Cambered foil xc1 = np.asarray([xx for xx in x if xx <= p]) xc2 = np.asarray([xx for xx in x if xx > p]) yc1 = m / np.power(p, 2) * xc1 * (2 * p - xc1) yc2 = m / np.power(1 - p, 2) * (1 - 2 * p + xc2) * (1 - xc2) # Y-coordinates of camber line yc = np.append(yc1, yc2) if self.cosine_spacing: # points are generated according to cosine distribution of # the X-coordinates of the chord dyc1_dx = m / np.power(p, 2) * (2 * p - 2 * xc1) dyc2_dx = m / np.power(1 - p, 2) * (2 * p - 2 * xc2) dyc_dx = np.append(dyc1_dx, dyc2_dx) theta = np.arctan(dyc_dx) self.xup_coordinates = x - yt * np.sin(theta) self.yup_coordinates = yc + yt * np.cos(theta) self.xdown_coordinates = x + yt * np.sin(theta) self.ydown_coordinates = yc - yt * np.cos(theta) else: # Linear spacing distribution of the foil coordinates self.xup_coordinates = np.linspace( 0.0, 1.0, num=self.n_points) self.xdown_coordinates = np.linspace( 0.0, 1.0, num=self.n_points) self.yup_coordinates = yc + yt self.ydown_coordinates = yc - yt elif len(self.digits) == 5: # Returns n+1 points in [0 1] for the given 5-digits NACA string cld = float(self.digits[0]) * 0.15 p = 5.0 * float(self.digits[1]) / 100.0 s = float(self.digits[2]) t = float(self.digits[3:]) / 100.0 # half-thickness distribution yt = 5 * t * (a0 * np.sqrt(x) + a1 * x + a2 * np.power(x, 2) + a3 * np.power(x, 3) + a4 * np.power(x, 4)) if s == 1: # Relfex camber P = np.array([0.1, 0.15, 0.2, 0.25]) M = np.array([0.13, 0.2170, 0.318, 0.441]) K = np.array([51.99, 15.793, 6.520, 3.191]) elif s == 0: # Standard camber P = np.array([0.05, 0.1, 0.15, 0.2, 0.25]) M = np.array([0.0580, 0.1260, 0.2025, 0.2900, 0.3910]) K = np.array([361.4, 51.64, 15.957, 6.643, 3.230]) else: raise ValueError( 'For NACA "LPSTT" the value of "S" can be either 0 or 1.') if p == 0: # Symmetric foil self.xup_coordinates = np.linspace(0.0, 1.0, num=self.n_points) self.yup_coordinates = yt self.xdown_coordinates = np.linspace( 0.0, 1.0, num=self.n_points) self.ydown_coordinates = -yt else: # Cambered foil spl_m = splrep(P, M) spl_k = splrep(M, K) m = splev(p, spl_m) k1 = splev(m, spl_k) xc1 = np.asarray([xx for xx in x if xx <= m]) xc2 = np.asarray([xx for xx in x if xx > m]) yc1 = k1 / 6.0 * (np.power(xc1, 3) - 3 * m * np.power(xc1, 2) + np.power(m, 2) * (3 - m) * xc1) yc2 = k1 / 6.0 * np.power(m, 3) * (1 - xc2) yc = np.append(yc1, yc2) if self.cosine_spacing: # points are generated according to cosine distribution of # the X-coordinates of the chord zc = cld / 0.3 * yc dyc1_dx = 1.0 / 6.0 * k1 * ( 3 * np.power(xc1, 2) - 6 * m * xc1 + np.power(m, 2) * (3 - m)) dyc2_dx = np.tile(-1.0 / 6.0 * k1 * np.power(m, 3), len(xc2)) dyc_dx = np.append(dyc1_dx, dyc2_dx) theta = np.arctan(dyc_dx) self.xup_coordinates = x - yt * np.sin(theta) self.yup_coordinates = zc + yt * np.cos(theta) self.xdown_coordinates = x + yt * np.sin(theta) self.ydown_coordinates = zc - yt * np.cos(theta) else: # Linear spacing distribution of the foil coordinates self.xup_coordinates = np.linspace( 0.0, 1.0, num=self.n_points) self.xdown_coordinates = np.linspace( 0.0, 1.0, num=self.n_points) self.yup_coordinates = yc + yt self.ydown_coordinates = yc - yt else: raise Exception