Source code for buffalo_wings.airfoil.internal.naca4_airfoil

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