"""Spec-backed runtime classes for NACA 4-digit airfoils."""
from __future__ import annotations
from copy import deepcopy
from typing import Literal, Self, cast
from .airfoil import OrthogonalAirfoil
from .airfoil_schema import Naca4AirfoilParamsSpec, Naca4AirfoilSpec
from .naca4_camber import Naca4DigitCamber
from .naca45_thickness import (
Naca45DigitThicknessClassic,
Naca45DigitThicknessParams,
)
NACA4_CODE_LENGTH = 4
NACA4_MAX_CAMBER_LIMIT = 0.1
NACA4_MAX_CAMBER_LOCATION_LIMIT = 0.9
NACA4_MAX_THICKNESS_LIMIT = 0.4
def _validate_naca4_designation(designation: str) -> None:
"""
Validate a classic 4-digit NACA designation string.
Parameters
----------
designation : str
Four-digit NACA designation string.
Raises
------
ValueError
If ``designation`` is not exactly four decimal digits.
"""
if len(designation) != NACA4_CODE_LENGTH or not designation.isdigit():
raise ValueError("NACA 4-digit designations must be a 4-digit string.")
def _validate_naca4_params(params: Naca4AirfoilParamsSpec) -> None:
"""
Validate the explicit NACA 4-digit parameter specification.
Parameters
----------
params : Naca4AirfoilParamsSpec
Explicit NACA 4-digit parameter specification to validate.
Raises
------
ValueError
If any parameter violates the supported NACA 4-digit bounds or
consistency rules.
"""
if not 0.0 <= params.m < NACA4_MAX_CAMBER_LIMIT:
raise ValueError("NACA 4-digit parameter m must satisfy 0 <= m < 0.1.")
if not 0.0 <= params.p <= NACA4_MAX_CAMBER_LOCATION_LIMIT:
raise ValueError("NACA 4-digit parameter p must satisfy 0 <= p <= 0.9.")
if not 0.0 <= params.t <= NACA4_MAX_THICKNESS_LIMIT:
raise ValueError("NACA 4-digit parameter t must satisfy 0 <= t <= 0.4.")
if params.m == 0.0 and params.p != 0.0:
raise ValueError("NACA 4-digit parameter p must be 0 when m is 0.")
if params.m > 0.0 and params.p <= 0.0:
raise ValueError(
"NACA 4-digit parameter p must be positive when m > 0."
)
def _copy_naca4_spec(spec: Naca4AirfoilSpec) -> Naca4AirfoilSpec:
"""
Return a defensive copy of a NACA 4-digit spec.
Parameters
----------
spec : Naca4AirfoilSpec
Source specification to copy.
Returns
-------
Naca4AirfoilSpec
Deep copy of ``spec``.
"""
return deepcopy(spec)
[docs]
class Naca4AirfoilClassic(OrthogonalAirfoil):
"""
Classic NACA 4-digit airfoil built from a designation.
Parameters
----------
spec : Naca4AirfoilSpec
Serialized specification containing a classic 4-digit designation.
Raises
------
ValueError
If ``spec`` does not contain a valid designation-only definition.
"""
[docs]
def __init__(self, spec: Naca4AirfoilSpec) -> None:
"""
Build a classic NACA 4-digit airfoil from its source spec.
Parameters
----------
spec : Naca4AirfoilSpec
Serialized specification containing a classic 4-digit
designation.
Raises
------
ValueError
If ``spec`` does not contain a valid designation-only
definition.
"""
if spec.designation is None or spec.params is not None:
raise ValueError(
"Classic NACA 4-digit airfoils require designation-only specs."
)
_validate_naca4_designation(spec.designation)
camber = Naca4DigitCamber(
mci=float(spec.designation[0]),
lci=float(spec.designation[1]),
)
thickness = Naca45DigitThicknessClassic(
mti=float(spec.designation[2:4])
)
super().__init__(camber=camber, thickness=thickness)
self._spec = _copy_naca4_spec(spec)
[docs]
@classmethod
def from_designation(cls, designation: str) -> Self:
"""
Build a classic NACA 4-digit airfoil from a designation.
Parameters
----------
designation : str
Four-digit NACA designation such as ``"2412"``.
Returns
-------
Self
Runtime airfoil built from ``designation``.
Raises
------
ValueError
If ``designation`` is not a valid 4-digit NACA code.
"""
return cls(Naca4AirfoilSpec(designation=designation))
@property
def spec(self) -> Naca4AirfoilSpec:
"""
Return the source spec for this airfoil.
Returns
-------
Naca4AirfoilSpec
Defensive copy of the serialized source spec.
"""
return _copy_naca4_spec(self._spec)
@property
def camber(self) -> Naca4DigitCamber:
"""Return the concrete NACA 4-digit camber model."""
return cast(Naca4DigitCamber, self._camber)
@property
def thickness(self) -> Naca45DigitThicknessClassic:
"""Return the concrete classic NACA thickness model."""
return cast(Naca45DigitThicknessClassic, self._thickness)
[docs]
def to_spec(self) -> Naca4AirfoilSpec:
"""
Return the schema definition needed to recreate this airfoil.
Returns
-------
Naca4AirfoilSpec
Serialized source spec for this airfoil.
"""
return self.spec
[docs]
class Naca4AirfoilParams(OrthogonalAirfoil):
"""
Parametric NACA 4-digit airfoil built from explicit parameters.
Parameters
----------
spec : Naca4AirfoilSpec
Serialized specification containing explicit 4-digit parameters.
Raises
------
ValueError
If ``spec`` does not contain a valid params-only definition.
"""
[docs]
def __init__(self, spec: Naca4AirfoilSpec) -> None:
"""
Build a parametric NACA 4-digit airfoil from its source spec.
Parameters
----------
spec : Naca4AirfoilSpec
Serialized specification containing explicit 4-digit
parameters.
Raises
------
ValueError
If ``spec`` does not contain a valid params-only definition.
"""
if spec.designation is not None or spec.params is None:
raise ValueError(
"Parametric NACA 4-digit airfoils require params-only specs."
)
_validate_naca4_params(spec.params)
camber = Naca4DigitCamber(
mci=100.0 * spec.params.m,
lci=10.0 * spec.params.p,
)
thickness = Naca45DigitThicknessParams(
mti=100.0 * spec.params.t,
closed_te=spec.params.trailing_edge == "sharp",
use_radius=spec.params.leading_edge_radius == "exact",
)
super().__init__(camber=camber, thickness=thickness)
self._spec = _copy_naca4_spec(spec)
@property
def camber(self) -> Naca4DigitCamber:
"""Return the concrete NACA 4-digit camber model."""
return cast(Naca4DigitCamber, self._camber)
@property
def thickness(self) -> Naca45DigitThicknessParams:
"""Return the concrete parametric NACA thickness model."""
return cast(Naca45DigitThicknessParams, self._thickness)
[docs]
@classmethod
def from_params(
cls,
*,
m: float,
p: float,
t: float,
trailing_edge: Literal["standard", "sharp"] = "standard",
leading_edge_radius: Literal["standard", "exact"] = "standard",
) -> Self:
"""
Build a parametric NACA 4-digit airfoil from explicit params.
Parameters
----------
m : float
Maximum camber as a fraction of chord.
p : float
Chordwise location of maximum camber as a fraction of chord.
t : float
Maximum thickness as a fraction of chord.
trailing_edge : {"standard", "sharp"}, default="standard"
Trailing-edge closure model for the thickness distribution.
leading_edge_radius : {"standard", "exact"}, default="standard"
Leading-edge radius treatment for the thickness distribution.
Returns
-------
Self
Runtime airfoil built from the explicit parameters.
Raises
------
ValueError
If the parameter set falls outside the supported NACA 4-digit
range.
"""
params = Naca4AirfoilParamsSpec(
m=m,
p=p,
t=t,
trailing_edge=trailing_edge,
leading_edge_radius=leading_edge_radius,
)
return cls(Naca4AirfoilSpec(params=params))
@property
def spec(self) -> Naca4AirfoilSpec:
"""
Return the source spec for this airfoil.
Returns
-------
Naca4AirfoilSpec
Defensive copy of the serialized source spec.
"""
return _copy_naca4_spec(self._spec)
[docs]
def to_spec(self) -> Naca4AirfoilSpec:
"""
Return the schema definition needed to recreate this airfoil.
Returns
-------
Naca4AirfoilSpec
Serialized source spec for this airfoil.
"""
return self.spec
__all__ = ["Naca4AirfoilClassic", "Naca4AirfoilParams"]