Source code for buffalo_wings.airfoil.internal.naca5_camber

"""NACA 5-digit airfoil camber relations."""

import numpy as np
from scipy.optimize import root_scalar

from buffalo_wings.type_aliases import FloatArray, FloatScalar

from .camber_base import (
    CLASSIC_NACA_5DIGIT_IDEAL_LIFT_INDEX,
    CLASSIC_NACA_5DIGIT_LIFT_MAX,
    CLASSIC_NACA_5DIGIT_LIFT_MIN,
    CLASSIC_NACA_5DIGIT_LOCATION_MAX,
    CLASSIC_NACA_5DIGIT_LOCATION_MIN,
    CLASSIC_NACA_5DIGIT_PARAMETERS,
    REFLEXED_NACA_5DIGIT_PARAMETERS,
    Camber,
)


[docs] class Naca5DigitCamberClassic(Camber): """ Class for the classic NACA 5-digit airfoil camber. Notes ----- The only valid values for the relative chord location of maximum camber term are 1, 2, 3, 4, and 5. The only valid value for the ideal lift coefficient term is 2. """
[docs] def __init__(self, lci: float, mci: float) -> None: """ Initialize a classic NACA 5-digit camber line. Parameters ---------- lci : float Ideal-lift-coefficient index from the designation. mci : float Maximum-camber-location index from the designation. Raises ------ ValueError If either designation index is unsupported. """ self._m = 0.2 self._k1 = 0.0 self._p_setter(mci) self._lci_setter(lci)
@property def lift_coefficient_index(self) -> float: """ Return the ideal-lift-coefficient index. Returns ------- float Ideal lift coefficient expressed as a designation index. """ return (20.0 / 3.0) * self._lci @lift_coefficient_index.setter def lift_coefficient_index(self, lci: float) -> None: """ Update the ideal lift coefficient index. Parameters ---------- lci : float Ideal-lift-coefficient index from the designation. """ self._lci_setter(lci) @property def max_camber_index(self) -> float: """ Return the maximum-camber-location index. Returns ------- float Chordwise location of maximum camber as a designation index. """ return 20.0 * self._p @max_camber_index.setter def max_camber_index(self, mci: float) -> None: """ Update the maximum-camber-location index. Parameters ---------- mci : float Maximum-camber-location index from the designation. """ self._p_setter(mci) @property def m(self) -> float: """ Return the camber transition location. Returns ------- float Relative chord location of the camber transition point. """ return self._m @property def k1(self) -> float: """ Return the primary camber scale factor. Returns ------- float Scale factor applied to the camber relation. """ return self._k1
[docs] def joints(self) -> list[float]: """ Return the locations of any joints/discontinuities in the camber line. Returns ------- List[float] Xi-coordinates of any discontinuities. """ return [0.0, self.m, 1.0]
[docs] def max_camber_parameter(self) -> FloatScalar: """ Return parameter where the camber is maximum. Returns ------- float Parameter where camber is maximum. """ return self._p
def _y(self, t: FloatArray) -> FloatArray: """ Return the camber location at specified chord location. Parameters ---------- t : numpy.ndarray Chord location of interest. Returns ------- numpy.ndarray Camber at specified point. """ t = np.asarray(t, dtype=np.float64) def fore(t: FloatArray) -> FloatArray: m = self.m return (self.k1 / 6) * (t**3 - 3 * m * t**2 + m**2 * (3 - m) * t) def aft(t: FloatArray) -> FloatArray: return (self.k1 * self.m**3 / 6) * (1 - t) return np.piecewise(t, [t <= self.m, t > self.m], [fore, aft]) def _y_t(self, t: FloatArray) -> FloatArray: """ Return first derivative of camber at specified chord location. Parameters ---------- t : numpy.ndarray Chord location of interest. Returns ------- numpy.ndarray First derivative of camber at specified point. """ t = np.asarray(t, dtype=np.float64) m = self.m def fore(t: FloatArray) -> FloatArray: return (self.k1 / 6) * (3 * t**2 - 6 * m * t + m**2 * (3 - m)) def aft(t: FloatArray) -> FloatArray: return -(self.k1 * m**3 / 6) * np.ones_like(t) return np.piecewise(t, [t <= m, t > m], [fore, aft]) def _y_tt(self, t: FloatArray) -> FloatArray: """ Return second derivative of camber at specified chord location. Parameters ---------- t : numpy.ndarray Chord location of interest. Returns ------- numpy.ndarray Second derivative of camber at specified point. """ t = np.asarray(t, dtype=np.float64) def fore(t: FloatArray) -> FloatArray: return (self.k1) * (t - self.m) def aft(t: FloatArray) -> FloatArray: return np.zeros_like(t) return np.piecewise(t, [t <= self.m, t > self.m], [fore, aft]) def _y_ttt(self, t: FloatArray) -> FloatArray: """ Return third derivative of camber at specified chord location. Parameters ---------- t : numpy.ndarray Chord location of interest. Returns ------- numpy.ndarray Third derivative of camber at specified point. """ t = np.asarray(t, dtype=np.float64) def fore(t: FloatArray) -> FloatArray: return self.k1 * np.ones_like(t) def aft(t: FloatArray) -> FloatArray: return np.zeros_like(t) return np.piecewise(t, [t <= self.m, t > self.m], [fore, aft]) def _p_setter(self, mci: float) -> None: """ Map a classic 5-digit camber index to tabulated parameters. Parameters ---------- mci : float Maximum-camber-location index from the designation. Raises ------ ValueError If ``mci`` is not a supported classic 5-digit index. """ parameters = CLASSIC_NACA_5DIGIT_PARAMETERS.get(int(mci)) if parameters is None: raise ValueError( f"Invalid NACA 5-digit maximum camber location: {mci}." ) self._m, self._k1 = parameters self._p = mci / 20.0 def _lci_setter(self, lci: float) -> None: """ Validate and store the classic 5-digit lift index. Parameters ---------- lci : float Ideal-lift-coefficient index from the designation. Raises ------ ValueError If ``lci`` is not the supported classic 5-digit index. """ if lci != CLASSIC_NACA_5DIGIT_IDEAL_LIFT_INDEX: raise ValueError( f"Invalid NACA 5-Digit ideal lift coefficient parameter: {lci}." ) self._lci = (3.0 / 20.0) * lci
[docs] class Naca5DigitCamberParams(Naca5DigitCamberClassic): """ Camber for the regular NACA 5-digit airfoils. The valid range of the relative chord location of maximum camber term is [1, 6). The valid range for the ideal lift coefficient term is [1, 4). """
[docs] def __init__(self, lci: float, mci: float) -> None: """ Initialize a continuous-index NACA 5-digit camber line. Parameters ---------- lci : float Ideal-lift-coefficient index. mci : float Maximum-camber-location index. """ # Need to bootstrap initialization self._lci = (3.0 / 20.0) * lci super().__init__(lci=lci, mci=mci)
def _p_setter(self, mci: float) -> None: """ Solve for continuous-index 5-digit camber parameters from ``mci``. Parameters ---------- mci : float Maximum-camber-location index. Raises ------ ValueError If ``mci`` is outside the supported continuous-index 5-digit range. """ if ( mci < CLASSIC_NACA_5DIGIT_LOCATION_MIN or mci >= CLASSIC_NACA_5DIGIT_LOCATION_MAX ): raise ValueError( "Invalid NACA 5-digit maximum camber location parameter: " f"{mci}." ) self._p = mci / 20.0 def camber_slope(m: float) -> float: return 3 * self._p**2 - 6 * m * self._p + m**2 * (3 - m) root = root_scalar(camber_slope, bracket=(self._p, 2 * self._p)) self._m = float(root.root) self._determine_k1() def _lci_setter(self, lci: float) -> None: """ Validate and store the continuous-index 5-digit lift index. Parameters ---------- lci : float Ideal-lift-coefficient index. Raises ------ ValueError If ``lci`` is outside the supported continuous-index 5-digit range. """ if lci < CLASSIC_NACA_5DIGIT_LIFT_MIN or lci >= ( CLASSIC_NACA_5DIGIT_LIFT_MAX ): raise ValueError( f"Invalid NACA 5-digit ideal lift coefficient parameter: {lci}." ) self._lci = (3.0 / 20.0) * lci self._determine_k1() def _determine_k1(self) -> None: """ Compute the continuous-index 5-digit camber scale factor. Returns ------- None This method updates the stored scale factor in place. """ self._k1 = ( 6 * self._lci / ( -3 / 2 * (1 - 2 * self._m) * np.arccos(1 - 2 * self._m) + (4 * self._m**2 - 4 * self._m + 3) * np.sqrt(self._m * (1 - self._m)) ) )
[docs] class Naca5DigitCamberReflexedClassic(Camber): """ Class for the classic NACA 5-digit reflexed camber airfoil. Notes ----- The only valid values for the relative chord location of maximum camber term are 1, 2, 3, 4, and 5. The only valid value for the ideal lift coefficient term is 2. """
[docs] def __init__( self, lci: float, mci: float, ) -> None: """ Initialize a classic reflexed NACA 5-digit camber line. Parameters ---------- lci : float Ideal-lift-coefficient index from the designation. mci : float Maximum-camber-location index from the designation. Raises ------ ValueError If either designation index is unsupported. """ self._m = 0.2 self._k1 = 0.0 self._k2 = 0.0 self._p_setter(mci) self._lci_setter(lci)
@property def lift_coefficient_index(self) -> float: """ Return the ideal-lift-coefficient index. Returns ------- float Ideal lift coefficient expressed as a designation index. """ return (20.0 / 3.0) * self._lci @lift_coefficient_index.setter def lift_coefficient_index(self, lci: float) -> None: """ Update the ideal lift coefficient index. Parameters ---------- lci : float Ideal-lift-coefficient index from the designation. """ self._lci_setter(lci) @property def max_camber_index(self) -> float: """ Return the maximum-camber-location index. Returns ------- float Chordwise location of maximum camber as a designation index. """ return 20.0 * self._p @max_camber_index.setter def max_camber_index(self, mci: float) -> None: """ Update the maximum-camber-location index. Parameters ---------- mci : float Maximum-camber-location index from the designation. """ self._p_setter(mci) @property def m(self) -> float: """ Return the camber transition location. Returns ------- float Relative chord location of the camber transition point. """ return self._m @property def k1(self) -> float: """ Return the primary camber scale factor. Returns ------- float First scale factor applied to the reflexed camber relation. """ return self._k1 @property def k2(self) -> float: """ Return the secondary camber scale factor. Returns ------- float Second scale factor applied to the reflexed camber relation. """ return self._k2
[docs] def joints(self) -> list[float]: """ Return the locations of any joints/discontinuities in the camber line. Returns ------- List[float] Xi-coordinates of any discontinuities. """ return [0.0, self.m, 1.0]
[docs] def max_camber_parameter(self) -> FloatScalar: """ Return parameter where the camber is maximum. Returns ------- float Parameter where camber is maximum. """ return self._p
def _y(self, t: FloatArray) -> FloatArray: """ Return the camber location at specified chord location. Parameters ---------- t : numpy.ndarray Chord location of interest. Returns ------- numpy.ndarray Camber at specified point. """ t = np.asarray(t, dtype=np.float64) m = self.m k1 = self.k1 k2ok1 = self.k2 / k1 def fore(t: FloatArray) -> FloatArray: return (k1 / 6) * ( (t - m) ** 3 - k2ok1 * (1 - m) ** 3 * t + m**3 * (1 - t) ) def aft(t: FloatArray) -> FloatArray: return (k1 / 6) * ( k2ok1 * (t - m) ** 3 - k2ok1 * (1 - m) ** 3 * t + m**3 * (1 - t) ) return np.piecewise(t, [t <= m, t > m], [fore, aft]) def _y_t(self, t: FloatArray) -> FloatArray: """ Return first derivative of camber at specified chord location. Parameters ---------- t : numpy.ndarray Chord location of interest. Returns ------- numpy.ndarray First derivative of camber at specified point. """ t = np.asarray(t, dtype=np.float64) m = self.m k1 = self.k1 k2ok1 = self.k2 / k1 def fore(t: FloatArray) -> FloatArray: return (k1 / 6) * (3 * (t - m) ** 2 - k2ok1 * (1 - m) ** 3 - m**3) def aft(t: FloatArray) -> FloatArray: return (k1 / 6) * ( 3 * k2ok1 * (t - m) ** 2 - k2ok1 * (1 - m) ** 3 - m**3 ) return np.piecewise(t, [t <= m, t > m], [fore, aft]) def _y_tt(self, t: FloatArray) -> FloatArray: """ Return second derivative of camber at specified chord location. Parameters ---------- t : numpy.ndarray Chord location of interest. Returns ------- numpy.ndarray Second derivative of camber at specified point. """ t = np.asarray(t, dtype=np.float64) def fore(t: FloatArray) -> FloatArray: return (self.k1) * (t - self.m) def aft(t: FloatArray) -> FloatArray: return (self.k2) * (t - self.m) return np.piecewise(t, [t <= self.m, t > self.m], [fore, aft]) def _y_ttt(self, t: FloatArray) -> FloatArray: """ Return third derivative of camber at specified chord location. Parameters ---------- t : numpy.ndarray Chord location of interest. Returns ------- numpy.ndarray Third derivative of camber at specified point. """ t = np.asarray(t, dtype=np.float64) def fore(t: FloatArray) -> FloatArray: return self.k1 * np.ones_like(t) def aft(t: FloatArray) -> FloatArray: return self.k2 * np.ones_like(t) return np.piecewise(t, [t <= self.m, t > self.m], [fore, aft]) def _p_setter(self, mci: float) -> None: """ Map a reflexed 5-digit camber index to tabulated parameters. Parameters ---------- mci : float Maximum-camber-location index from the designation. Raises ------ ValueError If ``mci`` is not a supported reflexed 5-digit index. """ parameters = REFLEXED_NACA_5DIGIT_PARAMETERS.get(int(mci)) if parameters is None: raise ValueError( f"Invalid NACA 5-digit reflexed maximum camber location: {mci}." ) self._m, self._k1, self._k2 = parameters self._p = mci / 20.0 def _lci_setter(self, lci: float) -> None: """ Validate and store the reflexed 5-digit lift index. Parameters ---------- lci : float Ideal-lift-coefficient index from the designation. Raises ------ ValueError If ``lci`` is not the supported reflexed 5-digit index. """ if lci != CLASSIC_NACA_5DIGIT_IDEAL_LIFT_INDEX: raise ValueError( "Invalid NACA 5-Digit reflexed ideal lift " f"coefficient parameter: {lci}." ) self._lci = (3.0 / 20.0) * lci
[docs] class Naca5DigitCamberReflexedParams(Naca5DigitCamberReflexedClassic): """ Camber for the regular NACA 5-digit reflexed airfoils. The valid range of the relative chord location of maximum camber term is [1, 6). The valid range for the ideal lift coefficient term is [1, 4). """
[docs] def __init__(self, lci: float, mci: float) -> None: """ Initialize a continuous-index reflexed NACA 5-digit camber line. Parameters ---------- lci : float Ideal-lift-coefficient index. mci : float Maximum-camber-location index. """ # Need to bootstrap initialization self._lci = (3.0 / 20.0) * lci super().__init__(lci=lci, mci=mci)
def _p_setter(self, mci: float) -> None: """ Solve for continuous-index reflexed 5-digit parameters from ``mci``. Parameters ---------- mci : float Maximum-camber-location index. Raises ------ ValueError If ``mci`` is outside the supported continuous-index reflexed range. """ if ( mci < CLASSIC_NACA_5DIGIT_LOCATION_MIN or mci >= CLASSIC_NACA_5DIGIT_LOCATION_MAX ): raise ValueError( f"Invalid NACA 5-digit reflexed maximum camber location: {mci}" ) self._p = mci / 20.0 def m_fun(m: float) -> float: k1 = 1.0 k2ok1 = self._k2ok1(m, self._p) cl_id = self._cl_id(m, k1, k2ok1) return -0.25 * cl_id + (k1 / 192) * ( 3 * k2ok1 * np.pi + 3 * (1 - k2ok1) * np.arccos(1 - 2 * m) + 2 * (1 - k2ok1) * (1 - 2 * m) * (8 * m**2 - 8 * m - 3) * np.sqrt(m * (1 - m)) ) root = root_scalar(m_fun, bracket=(self._p, 3 * self._p)) self._m = float(root.root) self._determine_k1k2() def _lci_setter(self, lci: float) -> None: """ Validate and store the continuous-index reflexed lift index. Parameters ---------- lci : float Ideal-lift-coefficient index. Raises ------ ValueError If ``lci`` is outside the supported continuous-index reflexed range. """ if lci < CLASSIC_NACA_5DIGIT_LIFT_MIN or lci >= ( CLASSIC_NACA_5DIGIT_LIFT_MAX ): raise ValueError( "Invalid NACA 5-Digit reflexed ideal lift " f"coefficient parameter: {lci}." ) self._lci = (3.0 / 20.0) * lci self._determine_k1k2() def _determine_k1k2(self) -> None: """ Compute the continuous-index reflexed camber scale factors. Returns ------- None This method updates the stored scale factors in place. """ k2ok1 = self._k2ok1(self.m, self._p) self._k1 = self._lci / self._cl_id(self.m, 1, k2ok1) self._k2 = k2ok1 * self._k1 @staticmethod def _cl_id(m: float, k1: float, k2ok1: float) -> float: """ Return the ideal lift coefficient for a reflexed camber line. Parameters ---------- m : float Camber transition location. k1 : float Primary camber scale factor. k2ok1 : float Ratio of the secondary and primary scale factors. Returns ------- float Ideal lift coefficient associated with the reflexed camber line. """ return (k1 / 12) * ( 3 * k2ok1 * (2 * m - 1) * np.pi + 3 * (1 - k2ok1) * (2 * m - 1) * np.arccos(1 - 2 * m) + 2 * (1 - k2ok1) * (4 * m**2 - 4 * m + 3) * np.sqrt(m * (1 - m)) ) @staticmethod def _k2ok1(m: float, p: float) -> float: """ Return the reflex-parameter ratio ``k2 / k1``. Parameters ---------- m : float Camber transition location. p : float Chordwise location of maximum camber. Returns ------- float Ratio of the secondary and primary camber scale factors. """ return (3 * (p - m) ** 2 - m**3) / (1 - m) ** 3