Source code for buffalo_wings.airfoil.internal.naca5_modified_airfoil

"""Spec-backed runtime classes for modified NACA 5-digit airfoils."""

from __future__ import annotations

from copy import deepcopy
from typing import Literal, Self

from .airfoil import OrthogonalAirfoil
from .airfoil_schema import (
    Naca5ModifiedAirfoilParamsSpec,
    Naca5ModifiedAirfoilSpec,
)
from .camber_base import Camber
from .naca4_modified_airfoil import NACA4_MODIFIED_MIN_THICKNESS_LOCATION
from .naca5_airfoil import (
    NACA5_MAX_CAMBER_LOCATION,
    NACA5_MAX_IDEAL_LIFT_COEFFICIENT,
    NACA5_MIN_CAMBER_LOCATION,
    NACA5_MIN_IDEAL_LIFT_COEFFICIENT,
)
from .naca5_camber import (
    Naca5DigitCamberClassic,
    Naca5DigitCamberParams,
    Naca5DigitCamberReflexedClassic,
    Naca5DigitCamberReflexedParams,
)
from .naca45_modified_thickness import (
    LEADING_EDGE_INDEX_MAX,
    LEADING_EDGE_INDEX_MIN,
    MAX_THICKNESS_LOCATION_MAX,
    MAX_THICKNESS_LOCATION_MIN,
    Naca45DigitModifiedThicknessClassic,
    Naca45DigitModifiedThicknessParams,
)

NACA5_MODIFIED_DESIGNATION_LENGTH = 8
NACA5_MODIFIED_DESIGNATION_DASH_INDEX = 5
NACA5_MAX_THICKNESS_LIMIT = 0.4


def _validate_naca5_modified_designation(designation: str) -> None:
    """
    Validate a modified NACA 5-digit designation string.

    Parameters
    ----------
    designation : str
        Modified NACA 5-digit designation string in ``#####-##`` form.

    Raises
    ------
    ValueError
        If ``designation`` does not match the supported modified-series
        format.
    """
    if len(designation) != NACA5_MODIFIED_DESIGNATION_LENGTH:
        raise ValueError(
            "Modified NACA 5-digit designations must be 8 characters."
        )
    if designation[NACA5_MODIFIED_DESIGNATION_DASH_INDEX] != "-":
        raise ValueError(
            "Modified NACA 5-digit designations must use the form #####-##."
        )
    digits = designation.replace("-", "")
    if not digits.isdigit():
        raise ValueError(
            "Modified NACA 5-digit designations must contain only digits."
        )
    if designation[2] not in {"0", "1"}:
        raise ValueError(
            "Modified NACA 5-digit designations must use 0 or 1 as the "
            "reflex digit."
        )


def _validate_naca5_modified_params(
    params: Naca5ModifiedAirfoilParamsSpec,
) -> None:
    """
    Validate the explicit modified NACA 5-digit parameters.

    Parameters
    ----------
    params : Naca5ModifiedAirfoilParamsSpec
        Explicit modified NACA 5-digit parameter specification to validate.

    Raises
    ------
    ValueError
        If any parameter violates the supported modified-series bounds.
    """
    if not (
        NACA5_MIN_IDEAL_LIFT_COEFFICIENT
        <= params.ideal_lift_coefficient
        < NACA5_MAX_IDEAL_LIFT_COEFFICIENT
    ):
        raise ValueError(
            "Modified NACA 5-digit ideal_lift_coefficient must satisfy "
            "0.15 <= ideal_lift_coefficient < 0.6."
        )
    if not (
        NACA5_MIN_CAMBER_LOCATION
        <= params.max_camber_location
        < NACA5_MAX_CAMBER_LOCATION
    ):
        raise ValueError(
            "Modified NACA 5-digit max_camber_location must satisfy "
            "0.05 <= max_camber_location < 0.3."
        )
    if not 0.0 <= params.t <= NACA5_MAX_THICKNESS_LIMIT:
        raise ValueError(
            "Modified NACA 5-digit parameter t must satisfy 0 <= t <= 0.4."
        )
    if (
        not LEADING_EDGE_INDEX_MIN
        <= params.leading_edge_index
        < LEADING_EDGE_INDEX_MAX
    ):
        raise ValueError(
            "Modified NACA 5-digit leading_edge_index must satisfy "
            "1 <= leading_edge_index < 10."
        )
    if not (
        NACA4_MODIFIED_MIN_THICKNESS_LOCATION
        <= params.max_thickness_location
        < 1.0
    ):
        raise ValueError(
            "Modified NACA 5-digit max_thickness_location must satisfy "
            "0.1 <= max_thickness_location < 1.0."
        )


def _copy_naca5_modified_spec(
    spec: Naca5ModifiedAirfoilSpec,
) -> Naca5ModifiedAirfoilSpec:
    """
    Return a defensive copy of a modified NACA 5-digit spec.

    Parameters
    ----------
    spec : Naca5ModifiedAirfoilSpec
        Source specification to copy.

    Returns
    -------
    Naca5ModifiedAirfoilSpec
        Deep copy of ``spec``.
    """
    return deepcopy(spec)


def _designation_to_lmti(designation: str) -> float:
    """
    Return the max-thickness-location index from a designation.

    Parameters
    ----------
    designation : str
        Modified NACA 5-digit designation string.

    Returns
    -------
    float
        Max-thickness-location index extracted from ``designation``.

    Raises
    ------
    ValueError
        If the designation encodes an unsupported thickness-location index.
    """
    lmti = float(designation[7])
    if not MAX_THICKNESS_LOCATION_MIN <= lmti < MAX_THICKNESS_LOCATION_MAX:
        raise ValueError(
            "Modified NACA 5-digit max thickness location must be 1-9."
        )
    return lmti


[docs] class Naca5ModifiedAirfoilClassic(OrthogonalAirfoil): """ Classic modified NACA 5-digit airfoil built from a designation. Parameters ---------- spec : Naca5ModifiedAirfoilSpec Serialized specification containing a modified-series designation. Raises ------ ValueError If ``spec`` does not contain a valid designation-only definition. """
[docs] def __init__(self, spec: Naca5ModifiedAirfoilSpec) -> None: """ Build a classic modified NACA 5-digit airfoil from its spec. Parameters ---------- spec : Naca5ModifiedAirfoilSpec Serialized specification containing a modified-series 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 modified NACA 5-digit airfoils require " "designation-only specs." ) _validate_naca5_modified_designation(spec.designation) lift_index = float(spec.designation[0]) max_camber_index = float(spec.designation[1]) reflexed = spec.designation[2] == "1" camber: Camber if reflexed: camber = Naca5DigitCamberReflexedClassic( lci=lift_index, mci=max_camber_index, ) else: camber = Naca5DigitCamberClassic( lci=lift_index, mci=max_camber_index, ) thickness = Naca45DigitModifiedThicknessClassic( mti=float(spec.designation[3:5]), lei=float(spec.designation[6]), lmti=_designation_to_lmti(spec.designation), ) super().__init__(camber=camber, thickness=thickness) self._spec = _copy_naca5_modified_spec(spec)
[docs] @classmethod def from_designation(cls, designation: str) -> Self: """ Build a classic modified NACA 5-digit airfoil from a designation. Parameters ---------- designation : str Modified NACA 5-digit designation string. Returns ------- Self Runtime airfoil built from ``designation``. Raises ------ ValueError If ``designation`` is not a valid modified NACA 5-digit code. """ return cls(Naca5ModifiedAirfoilSpec(designation=designation))
@property def spec(self) -> Naca5ModifiedAirfoilSpec: """ Return the source spec for this airfoil. Returns ------- Naca5ModifiedAirfoilSpec Defensive copy of the serialized source spec. """ return _copy_naca5_modified_spec(self._spec)
[docs] def to_spec(self) -> Naca5ModifiedAirfoilSpec: """ Return the schema definition needed to recreate this airfoil. Returns ------- Naca5ModifiedAirfoilSpec Serialized source spec for this airfoil. """ return self.spec
[docs] class Naca5ModifiedAirfoilParams(OrthogonalAirfoil): """ Parametric modified NACA 5-digit airfoil built from explicit params. Parameters ---------- spec : Naca5ModifiedAirfoilSpec Serialized specification containing explicit modified-series parameters. Raises ------ ValueError If ``spec`` does not contain a valid params-only definition. """
[docs] def __init__(self, spec: Naca5ModifiedAirfoilSpec) -> None: """ Build a parametric modified NACA 5-digit airfoil from its spec. Parameters ---------- spec : Naca5ModifiedAirfoilSpec Serialized specification containing explicit modified-series 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 modified NACA 5-digit airfoils require " "params-only specs." ) _validate_naca5_modified_params(spec.params) lift_index = (20.0 / 3.0) * spec.params.ideal_lift_coefficient max_camber_index = 20.0 * spec.params.max_camber_location camber: Camber if spec.params.reflexed: camber = Naca5DigitCamberReflexedParams( lci=lift_index, mci=max_camber_index, ) else: camber = Naca5DigitCamberParams( lci=lift_index, mci=max_camber_index, ) thickness = Naca45DigitModifiedThicknessParams( mti=100.0 * spec.params.t, lei=spec.params.leading_edge_index, lmti=10.0 * spec.params.max_thickness_location, closed_te=spec.params.trailing_edge == "sharp", ) super().__init__(camber=camber, thickness=thickness) self._spec = _copy_naca5_modified_spec(spec)
[docs] @classmethod def from_params( cls, *, ideal_lift_coefficient: float, max_camber_location: float, reflexed: bool, t: float, leading_edge_index: float, max_thickness_location: float, trailing_edge: Literal["standard", "sharp"] = "standard", ) -> Self: """ Build a parametric modified NACA 5-digit airfoil from params. Parameters ---------- ideal_lift_coefficient : float Design lift coefficient associated with the camber line. max_camber_location : float Chordwise location of maximum camber as a fraction of chord. reflexed : bool Whether to build the reflexed 5-digit camber-line family. t : float Maximum thickness as a fraction of chord. leading_edge_index : float Modified-series leading-edge shape index. max_thickness_location : float Chordwise location of maximum thickness as a fraction of chord. trailing_edge : {"standard", "sharp"}, default="standard" Trailing-edge closure model for the thickness distribution. Returns ------- Self Runtime airfoil built from the explicit parameters. Raises ------ ValueError If the parameter set falls outside the supported modified-series range. """ params = Naca5ModifiedAirfoilParamsSpec( ideal_lift_coefficient=ideal_lift_coefficient, max_camber_location=max_camber_location, reflexed=reflexed, t=t, leading_edge_index=leading_edge_index, max_thickness_location=max_thickness_location, trailing_edge=trailing_edge, ) return cls(Naca5ModifiedAirfoilSpec(params=params))
@property def spec(self) -> Naca5ModifiedAirfoilSpec: """ Return the source spec for this airfoil. Returns ------- Naca5ModifiedAirfoilSpec Defensive copy of the serialized source spec. """ return _copy_naca5_modified_spec(self._spec)
[docs] def to_spec(self) -> Naca5ModifiedAirfoilSpec: """ Return the schema definition needed to recreate this airfoil. Returns ------- Naca5ModifiedAirfoilSpec Serialized source spec for this airfoil. """ return self.spec
__all__ = [ "Naca5ModifiedAirfoilClassic", "Naca5ModifiedAirfoilParams", ]