"""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