"""Classic and parametric NACA 4/5-digit thickness relations."""
import numpy as np
from buffalo_wings.internal.numeric import as_float_array
from buffalo_wings.type_aliases import FloatArray, FloatInput
from .thickness_base import (
MAX_THICKNESS_INDEX_LIMIT,
OPEN_TRAILING_EDGE_THICKNESS,
Thickness,
)
[docs]
class Naca45DigitThicknessClassic(Thickness):
"""
Class for the classic NACA 4-digit and 5-digit airfoil thickness.
The thickness is parameterized on the square root of the chord location
where the thickness is desired. This is to remove singularities that
occur at the leading edge for the typical chord length parameterization.
Notes
-----
To obtain a classic thickness profile the maximum thickness should be set
to an integer value, i.e. 12. However, any floating point value can be
passed in, i.e. 12.3, if a more accurate maximum thickness is needed to
be specified.
"""
[docs]
def __init__(self, mti: float) -> None:
"""
Initialize a classic NACA thickness distribution.
Parameters
----------
mti : float
Maximum-thickness index as a percent of chord.
Raises
------
ValueError
If ``mti`` is outside the supported classic NACA range.
"""
self._a = (
np.array([0.29690, -0.12600, -0.35160, 0.28430, -0.10150]) / 0.20
)
self.max_thickness_index = mti
@property
def max_thickness_index(self) -> float:
"""
Return the maximum-thickness index.
Returns
-------
float
Maximum thickness as a percent-chord index.
"""
return 100 * self._tmax
@max_thickness_index.setter
def max_thickness_index(self, mti: float) -> None:
if mti < 0 or mti >= MAX_THICKNESS_INDEX_LIMIT:
raise ValueError(f"Invalid NACA 4/5-digit maximum thickness: {mti}")
self._tmax = mti / 100.0
@property
def a(self) -> FloatArray:
"""
Return the polynomial coefficients.
Returns
-------
FloatArray
Coefficients for the classic thickness relation.
"""
return self._a
[docs]
def discontinuities(self) -> list[float]: # noqa: PLR6301
"""
Return the locations of any discontinuities in the thickness.
Returns
-------
List[float]
Parametric coordinates of any discontinuities.
"""
return []
[docs]
def max_thickness_parameter(self) -> float: # noqa: PLR6301
"""
Return parameter coordinate of maximum thickness.
Returns
-------
float
Parameter coordinate of maximum thickness.
"""
return np.sqrt(0.3)
[docs]
def delta(self, t: FloatInput) -> FloatArray:
"""
Return the thickness at specified parameter location.
Parameters
----------
t : numpy.ndarray
Parameter location of interest. Equal to the square root of the
desired chord location.
Returns
-------
numpy.ndarray
Thickness at specified parameter.
"""
t = as_float_array(t)
t2 = t**2
return (
self._tmax
* t
* (
self.a[0]
+ t
* (
self.a[1]
+ t2 * (self.a[2] + t2 * (self.a[3] + t2 * self.a[4]))
)
)
)
[docs]
def delta_t(self, t: FloatInput) -> FloatArray:
"""
Return first derivative of thickness at specified parameter location.
Parameters
----------
t : numpy.ndarray
Parameter location of interest. Equal to the square root of the
desired chord location.
Returns
-------
numpy.ndarray
First derivative of thickness at specified parameter.
"""
t = as_float_array(t)
t2 = t**2
return self._tmax * (
self.a[0]
+ 2
* t
* (
self.a[1]
+ t2
* (2 * self.a[2] + t2 * (3 * self.a[3] + 4 * t2 * self.a[4]))
)
)
[docs]
def delta_tt(self, t: FloatInput) -> FloatArray:
"""
Return second derivative of thickness at specified parameter location.
Parameters
----------
t : numpy.ndarray
Parameter location of interest. Equal to the square root of the
desired chord location.
Returns
-------
numpy.ndarray
Second derivative of thickness at specified parameter.
"""
t = as_float_array(t)
t2 = t**2
return (
2
* self._tmax
* (
self.a[1]
+ t2
* (6 * self.a[2] + t2 * (15 * self.a[3] + 28 * self.a[4] * t2))
)
)
[docs]
class Naca45DigitThicknessParams(Naca45DigitThicknessClassic):
"""
Parametric NACA 4-digit and 5-digit airfoil thickness.
The thickness is parameterized on the square root of the chord location
where the thickness is desired. This is to remove singularities that
occur at the leading edge for the typical chord length parameterization.
This class extends the standard thickness distribution relation by
solving for coefficients from the original constraints even for
non-integer parameters, allowing a closed trailing edge, and allowing
the leading-edge radius condition to be enforced directly.
Notes
-----
Specifying the same parameters as the classic thickness profile will not
result in an identical thickness profile because the canonical
coefficients do not match the stated constraints in the original source
from Jacobs, Ward, and Pinkerton (1933).
"""
[docs]
def __init__(self, mti: float, closed_te: bool, use_radius: bool) -> None:
"""
Initialize the parametric NACA thickness distribution.
Parameters
----------
mti : float
Maximum-thickness index as a percent of chord.
closed_te : bool
Whether the trailing edge closes to zero thickness.
use_radius : bool
Whether to enforce the classic leading-edge radius condition.
"""
super().__init__(mti=mti)
self._closed_te = closed_te
self._use_radius = use_radius
self._calculate_a()
@property
def closed_trailing_edge(self) -> bool:
"""
Return the trailing-edge closure flag.
Returns
-------
bool
``True`` when the trailing edge closes to zero thickness.
"""
return self._closed_te
@closed_trailing_edge.setter
def closed_trailing_edge(self, closed_te: bool) -> None:
self._closed_te = closed_te
self._calculate_a()
@property
def use_leading_edge_radius(self) -> bool:
"""
Return the leading-edge treatment flag.
Returns
-------
bool
``True`` when the classic leading-edge radius is enforced.
"""
return self._use_radius
@use_leading_edge_radius.setter
def use_leading_edge_radius(self, use_radius: bool) -> None:
self._use_radius = use_radius
self._calculate_a()
def _calculate_a(self) -> None:
"""
Recompute the classic-thickness polynomial coefficients.
Returns
-------
None
This method updates the stored coefficient array in place.
"""
# solve for new values of the coefficients using chord based
# parameterization
b_matrix = np.zeros([5, 5])
r = np.zeros([5, 1])
# first row is leading edge condition
i = 0
if self._use_radius:
b_matrix[i, :] = [1, 0, 0, 0, 0]
r[i] = 0.29690
else:
xi_1c = 0.1
t_1c = 0.078
b_matrix[i, :] = [
np.sqrt(xi_1c),
xi_1c,
xi_1c**2,
xi_1c**3,
xi_1c**4,
]
r[i] = t_1c
# second row is the maximum thickness at 0.3c
i = 1
xi_max = 0.3
b_matrix[i, :] = [
0.5 / np.sqrt(xi_max),
1,
2 * xi_max,
3 * xi_max**2,
4 * xi_max**3,
]
r[i] = 0.0
# third row is the maximum thickness of 0.2c
i = 2
t_max = 0.1
b_matrix[i, :] = [
np.sqrt(xi_max),
xi_max,
xi_max**2,
xi_max**3,
xi_max**4,
]
r[i] = t_max
# fourth row is trailing edge slope
i = 3
te_slope = -0.234
b_matrix[i, :] = [0.5, 1, 2, 3, 4]
r[i] = te_slope
# fith row is trailing edge thickness
i = 4
t_te = 0.0 if self._closed_te else OPEN_TRAILING_EDGE_THICKNESS
b_matrix[i, :] = [1, 1, 1, 1, 1]
r[i] = t_te
self._a = (np.linalg.solve(b_matrix, r).transpose()[0]) / 0.20