"""Public factory helpers for schema-driven airfoil construction."""
from __future__ import annotations
import math
from collections.abc import Callable, Mapping
from dataclasses import dataclass
from pathlib import PurePath
from typing import Literal, cast
from buffalo_wings.airfoil.internal.analytic.naca import (
Naca4AirfoilClassic,
Naca4AirfoilParams,
Naca4ModifiedAirfoilClassic,
Naca4ModifiedAirfoilParams,
Naca5AirfoilClassic,
Naca5AirfoilParams,
Naca5ModifiedAirfoilClassic,
Naca5ModifiedAirfoilParams,
)
from buffalo_wings.airfoil.internal.base import Airfoil
from buffalo_wings.airfoil.internal.bezier import BezierCurve2D
from buffalo_wings.airfoil.internal.curve import Curve
from buffalo_wings.airfoil.internal.polyline_curve import PolylineCurve
from buffalo_wings.airfoil.internal.runtime_types import (
AirfoilCamberResult,
AirfoilDocumentCurveResult,
)
from buffalo_wings.airfoil.internal.schema import (
AirfoilCamberCurveSpec,
AirfoilDefinitionSpec,
AirfoilDocumentSpec,
CstAirfoilSpec,
CurveDefinitionSpec,
PolylineCurveSpec,
)
from buffalo_wings.airfoil.internal.schema.naca import (
Naca4AirfoilParamsSpec,
Naca4AirfoilSpec,
Naca4ModifiedAirfoilParamsSpec,
Naca4ModifiedAirfoilSpec,
Naca5AirfoilParamsSpec,
Naca5AirfoilSpec,
Naca5ModifiedAirfoilParamsSpec,
Naca5ModifiedAirfoilSpec,
Naca16AirfoilParamsSpec,
Naca16AirfoilSpec,
)
from buffalo_wings.airfoil.internal.spline import SplineCurve
from .default_specs import default_spec as build_default_spec
from .dispatch import from_spec
from .naca import (
naca4_from_designation,
naca4_from_params,
naca4_modified_from_designation,
naca4_modified_from_params,
naca5_from_designation,
naca5_from_params,
naca5_modified_from_designation,
naca5_modified_from_params,
)
type DefaultSpecSource = Literal["designation", "params"]
def _camber_curve_result_from_document_spec(
document: AirfoilDocumentSpec,
*,
curve_name: str,
curve_spec: AirfoilCamberCurveSpec,
) -> AirfoilCamberResult:
"""
Resolve one airfoil-derived document curve into a runtime result.
Parameters
----------
document : AirfoilDocumentSpec
Document containing named airfoils and named curves.
curve_name : str
Name of the curve entry being resolved.
curve_spec : AirfoilCamberCurveSpec
Airfoil-derived curve spec already selected from ``document``.
Returns
-------
AirfoilCamberResult
Exact or approximate runtime camber-curve result.
Raises
------
KeyError
If the referenced source airfoil name is unknown.
"""
try:
airfoil_spec = document.airfoils[curve_spec.airfoil]
except KeyError as exc:
msg = (
"Unknown source airfoil for document curve "
f"{curve_name}: {curve_spec.airfoil}"
)
raise KeyError(msg) from exc
airfoil = from_spec(airfoil_spec)
return airfoil.camber_curve(
spacing=curve_spec.approximation.spacing,
num_points=curve_spec.approximation.num_points,
)
def _curve_result_from_document_spec(
document: AirfoilDocumentSpec,
*,
curve_name: str,
curve_spec: CurveDefinitionSpec,
) -> AirfoilDocumentCurveResult:
"""
Resolve one named document curve into a runtime curve plus metadata.
Parameters
----------
document : AirfoilDocumentSpec
Document containing named airfoils and named curves.
curve_name : str
Name of the curve entry being resolved.
curve_spec : CurveDefinitionSpec
Curve definition already selected from ``document``.
Returns
-------
AirfoilDocumentCurveResult
Runtime curve plus document-local provenance metadata.
"""
if isinstance(curve_spec, AirfoilCamberCurveSpec):
camber_result = _camber_curve_result_from_document_spec(
document,
curve_name=curve_name,
curve_spec=curve_spec,
)
return AirfoilDocumentCurveResult(
name=curve_name,
curve=camber_result.curve,
definition_type=curve_spec.type,
source_airfoil_name=curve_spec.airfoil,
source_family=camber_result.source_family,
camber_mode=camber_result.mode,
)
if isinstance(curve_spec, PolylineCurveSpec):
return AirfoilDocumentCurveResult(
name=curve_name,
curve=PolylineCurve(points=curve_spec.points),
definition_type=curve_spec.type,
source_family="PolylineCurve",
)
return AirfoilDocumentCurveResult(
name=curve_name,
curve=SplineCurve(
curve=BezierCurve2D(
control_points=curve_spec.control_points,
)
),
definition_type=curve_spec.type,
source_family="SplineCurve",
)
[docs]
@dataclass(frozen=True, slots=True)
class AirfoilFamilyInfo:
"""
Public metadata describing one user-facing airfoil family.
Attributes
----------
type_name : str
Stable schema discriminator for the family.
display_name : str
Stable human-readable label for display in downstream UIs.
constructable : bool
Whether ``AirfoilFactory.from_spec(...)`` can build this family
today.
supports_designation : bool
Whether the family supports a designation-string input form.
supports_params : bool
Whether the family supports a nested ``params`` input form.
default_source : {"designation", "params"} | None
Canonical source branch used by
``AirfoilFactory.default_spec(...)`` when ``source=None``.
``None`` indicates a family without a designation/params source
switch.
"""
type_name: str
display_name: str
constructable: bool
supports_designation: bool
supports_params: bool
default_source: DefaultSpecSource | None
[docs]
@dataclass(frozen=True, slots=True)
class ApproximationDelta:
"""One parameter value changed by approximate source conversion."""
field_name: str
requested_value: float
approximated_value: float
[docs]
@dataclass(frozen=True, slots=True)
class AirfoilApproximateSwitchResult:
"""Approximate source-switch result plus approximation metadata."""
spec: AirfoilDefinitionSpec
approximated_fields: tuple[ApproximationDelta, ...] = ()
[docs]
@dataclass(frozen=True, slots=True)
class AirfoilSpecSummary:
"""
Human-readable summary for one airfoil definition spec.
Attributes
----------
family : str
Stable family display name for UI and report use.
source : str | None
Source branch or backing mode used by the spec when applicable.
short : str
Compact one-line description suitable for labels or tables.
long : str
Slightly more descriptive sentence suitable for CLI output and
reports.
"""
family: str
source: str | None
short: str
long: str
type SummaryDescriber = Callable[
[AirfoilDefinitionSpec, AirfoilFamilyInfo],
AirfoilSpecSummary,
]
_SUPPORTED_FAMILY_INFO: tuple[AirfoilFamilyInfo, ...] = (
AirfoilFamilyInfo(
type_name="naca4",
display_name="NACA 4-Digit",
constructable=True,
supports_designation=True,
supports_params=True,
default_source="designation",
),
AirfoilFamilyInfo(
type_name="naca4_modified",
display_name="NACA 4-Digit Modified",
constructable=True,
supports_designation=True,
supports_params=True,
default_source="designation",
),
AirfoilFamilyInfo(
type_name="naca5",
display_name="NACA 5-Digit",
constructable=True,
supports_designation=True,
supports_params=True,
default_source="designation",
),
AirfoilFamilyInfo(
type_name="naca5_modified",
display_name="NACA 5-Digit Modified",
constructable=True,
supports_designation=True,
supports_params=True,
default_source="designation",
),
AirfoilFamilyInfo(
type_name="naca16",
display_name="NACA 16-Series",
constructable=False,
supports_designation=True,
supports_params=True,
default_source="designation",
),
AirfoilFamilyInfo(
type_name="flat_plate",
display_name="Flat Plate",
constructable=False,
supports_designation=False,
supports_params=False,
default_source=None,
),
AirfoilFamilyInfo(
type_name="biconvex",
display_name="Biconvex",
constructable=False,
supports_designation=False,
supports_params=True,
default_source="params",
),
AirfoilFamilyInfo(
type_name="biconvex_parabola",
display_name="Biconvex Parabola",
constructable=False,
supports_designation=False,
supports_params=True,
default_source="params",
),
AirfoilFamilyInfo(
type_name="polygon",
display_name="Polygon",
constructable=False,
supports_designation=False,
supports_params=True,
default_source="params",
),
AirfoilFamilyInfo(
type_name="ellipse",
display_name="Ellipse",
constructable=True,
supports_designation=False,
supports_params=True,
default_source="params",
),
AirfoilFamilyInfo(
type_name="circular_arc",
display_name="Circular Arc",
constructable=False,
supports_designation=False,
supports_params=True,
default_source="params",
),
AirfoilFamilyInfo(
type_name="joukowski",
display_name="Joukowski",
constructable=False,
supports_designation=False,
supports_params=True,
default_source="params",
),
AirfoilFamilyInfo(
type_name="naca6",
display_name="NACA 6-Series",
constructable=False,
supports_designation=True,
supports_params=True,
default_source="designation",
),
AirfoilFamilyInfo(
type_name="naca6a",
display_name="NACA 6A-Series",
constructable=False,
supports_designation=True,
supports_params=True,
default_source="designation",
),
AirfoilFamilyInfo(
type_name="file",
display_name="File",
constructable=False,
supports_designation=False,
supports_params=False,
default_source=None,
),
AirfoilFamilyInfo(
type_name="points",
display_name="Points",
constructable=False,
supports_designation=False,
supports_params=False,
default_source=None,
),
AirfoilFamilyInfo(
type_name="spline",
display_name="Spline",
constructable=True,
supports_designation=False,
supports_params=False,
default_source=None,
),
AirfoilFamilyInfo(
type_name="cst",
display_name="CST",
constructable=True,
supports_designation=False,
supports_params=False,
default_source=None,
),
AirfoilFamilyInfo(
type_name="parsec",
display_name="PARSEC",
constructable=False,
supports_designation=False,
supports_params=False,
default_source=None,
),
)
_SUPPORTED_FAMILY_INFO_BY_TYPE: dict[str, AirfoilFamilyInfo] = {
family.type_name: family for family in _SUPPORTED_FAMILY_INFO
}
def _resolve_default_spec_source(
*,
family: AirfoilFamilyInfo,
source: DefaultSpecSource | None,
) -> DefaultSpecSource | None:
"""
Resolve and validate the public starter-source selection.
Returns
-------
{"designation", "params"} | None
Source branch to use for the canonical starter spec.
"""
if source is None:
return family.default_source
if source == "designation" and family.supports_designation:
return source
if source == "params" and family.supports_params:
return source
if family.supports_designation and family.supports_params:
message = (
f"{family.display_name} default specs support "
"source='designation' and source='params'."
)
elif family.supports_designation:
message = (
f"{family.display_name} default specs support only "
"source='designation'."
)
elif family.supports_params:
message = (
f"{family.display_name} default specs support only source='params'."
)
else:
message = (
f"{family.display_name} default specs do not support source "
"selection."
)
raise ValueError(
f"{message} Received source={source!r} for "
f"type_name={family.type_name!r}."
)
_EXACT_SOURCE_SWITCH_FAMILIES = frozenset({
"naca4",
"naca4_modified",
"naca5",
"naca5_modified",
"naca16",
})
_AIRFOIL_EXACT_COMPARE_TOLERANCE = 1e-9
_NACA5_LIFT_DIGIT_SCALE = 20.0 / 3.0
_NACA_CAMBER_LOCATION_DIGIT_SCALE = 20.0
_NACA4_CAMBER_DIGIT_SCALE = 100.0
_NACA4_LOCATION_DIGIT_SCALE = 10.0
_THICKNESS_DIGIT_SCALE = 100.0
_MODIFIED_LOCATION_DIGIT_SCALE = 10.0
_NACA16_DESIGNATION_LENGTH = 6
def _exact_source_choices(type_name: str) -> tuple[DefaultSpecSource, ...]:
"""Return exact-switch source choices for one airfoil family."""
if type_name in _EXACT_SOURCE_SWITCH_FAMILIES:
return ("designation", "params")
return ()
def _mapping_source(mapping: Mapping[str, object]) -> DefaultSpecSource | None:
"""Return which mutually exclusive source branch one mapping uses."""
has_designation = isinstance(mapping.get("designation"), str)
has_params = isinstance(mapping.get("params"), Mapping)
if has_designation and not has_params:
return "designation"
if has_params and not has_designation:
return "params"
return None
def _switch_source_exact(
mapping: Mapping[str, object],
target_source: DefaultSpecSource,
) -> AirfoilDefinitionSpec:
"""Switch one exact-only NACA family between source branches."""
airfoil_type = mapping.get("type")
if not isinstance(airfoil_type, str):
raise ValueError("Airfoil mapping must include a string 'type'.")
if airfoil_type == "naca4":
return _switch_naca4_source_exact(mapping, target_source)
if airfoil_type == "naca4_modified":
return _switch_naca4_modified_source_exact(mapping, target_source)
if airfoil_type == "naca5":
return _switch_naca5_source_exact(mapping, target_source)
if airfoil_type == "naca5_modified":
return _switch_naca5_modified_source_exact(mapping, target_source)
if airfoil_type == "naca16":
return _switch_naca16_source_exact(mapping, target_source)
raise ValueError(
f"Airfoil type {airfoil_type!r} does not support exact switching."
)
def _switch_source_approx_result(
mapping: Mapping[str, object],
target_source: DefaultSpecSource,
) -> AirfoilApproximateSwitchResult:
"""Switch one airfoil family approximately and report metadata."""
airfoil_type = mapping.get("type")
if not isinstance(airfoil_type, str):
raise ValueError("Airfoil mapping must include a string 'type'.")
if airfoil_type == "naca4":
return _switch_naca4_source_approx(mapping, target_source)
if airfoil_type == "naca4_modified":
return _switch_naca4_modified_source_approx(mapping, target_source)
if airfoil_type == "naca5":
return _switch_naca5_source_approx(mapping, target_source)
if airfoil_type == "naca5_modified":
return _switch_naca5_modified_source_approx(mapping, target_source)
if airfoil_type == "naca16":
return _switch_naca16_source_approx(mapping, target_source)
raise ValueError(
"Approximate source switching currently supports only "
"NACA 4-digit, modified NACA 4-digit, NACA 5-digit, and "
"modified NACA 5-digit, and NACA 16-series airfoils."
)
def _switch_naca4_source_exact(
mapping: Mapping[str, object],
target_source: DefaultSpecSource,
) -> Naca4AirfoilSpec:
current_source = _mapping_source(mapping)
if current_source is None:
raise ValueError(
"Airfoil mapping must use exactly one of designation or params."
)
if current_source == target_source:
if target_source == "designation":
return Naca4AirfoilSpec(
designation=_mapping_string(mapping, "designation"),
)
return Naca4AirfoilSpec(params=_build_naca4_params_spec(mapping))
if target_source == "params":
designation = _mapping_string(mapping, "designation")
if designation == "":
default_spec = AirfoilFactory.default_spec(
"naca4",
source="params",
)
if not isinstance(default_spec, Naca4AirfoilSpec):
raise TypeError("Expected NACA 4 starter spec.")
return default_spec
return Naca4AirfoilSpec(
params=_naca4_params_from_designation(designation)
)
return Naca4AirfoilSpec(
designation=_naca4_designation_from_params(
_build_naca4_params_spec(mapping)
)
)
def _switch_naca4_source_approx(
mapping: Mapping[str, object],
target_source: DefaultSpecSource,
) -> AirfoilApproximateSwitchResult:
"""Switch one classic NACA 4-digit airfoil approximately."""
current_source = _mapping_source(mapping)
if current_source is None:
raise ValueError(
"Airfoil mapping must use exactly one of designation or params."
)
if current_source == target_source:
return AirfoilApproximateSwitchResult(
spec=_switch_naca4_source_exact(mapping, target_source)
)
if target_source == "params":
return AirfoilApproximateSwitchResult(
spec=_switch_naca4_source_exact(mapping, target_source)
)
params_spec = _build_naca4_params_spec(mapping)
designation = _naca4_approximate_designation_from_params(params_spec)
approximated_spec = Naca4AirfoilSpec(designation=designation)
approximated_params = _naca4_params_from_designation(designation)
return AirfoilApproximateSwitchResult(
spec=approximated_spec,
approximated_fields=_naca4_approximation_deltas(
requested=params_spec,
approximated=approximated_params,
),
)
def _switch_naca5_source_approx(
mapping: Mapping[str, object],
target_source: DefaultSpecSource,
) -> AirfoilApproximateSwitchResult:
"""Switch one classic NACA 5-digit airfoil approximately."""
current_source = _mapping_source(mapping)
if current_source is None:
raise ValueError(
"Airfoil mapping must use exactly one of designation or params."
)
if current_source == target_source:
return AirfoilApproximateSwitchResult(
spec=_switch_naca5_source_exact(mapping, target_source)
)
if target_source == "params":
return AirfoilApproximateSwitchResult(
spec=_switch_naca5_source_exact(mapping, target_source)
)
params_spec = _build_naca5_params_spec(mapping)
designation = _naca5_approximate_designation_from_params(params_spec)
approximated_spec = Naca5AirfoilSpec(designation=designation)
approximated_params = _naca5_params_from_designation(designation)
return AirfoilApproximateSwitchResult(
spec=approximated_spec,
approximated_fields=_naca5_approximation_deltas(
requested=params_spec,
approximated=approximated_params,
),
)
def _switch_naca4_modified_source_approx(
mapping: Mapping[str, object],
target_source: DefaultSpecSource,
) -> AirfoilApproximateSwitchResult:
"""Switch one modified NACA 4-digit airfoil approximately."""
current_source = _mapping_source(mapping)
if current_source is None:
raise ValueError(
"Airfoil mapping must use exactly one of designation or params."
)
if current_source == target_source:
return AirfoilApproximateSwitchResult(
spec=_switch_naca4_modified_source_exact(
mapping,
target_source,
)
)
if target_source == "params":
return AirfoilApproximateSwitchResult(
spec=_switch_naca4_modified_source_exact(
mapping,
target_source,
)
)
params_spec = _build_naca4_modified_params_spec(mapping)
designation = _naca4_modified_approximate_designation_from_params(
params_spec
)
approximated_spec = Naca4ModifiedAirfoilSpec(designation=designation)
approximated_params = _naca4_modified_params_from_designation(designation)
return AirfoilApproximateSwitchResult(
spec=approximated_spec,
approximated_fields=_naca4_modified_approximation_deltas(
requested=params_spec,
approximated=approximated_params,
),
)
def _switch_naca5_modified_source_approx(
mapping: Mapping[str, object],
target_source: DefaultSpecSource,
) -> AirfoilApproximateSwitchResult:
"""Switch one modified NACA 5-digit airfoil approximately."""
current_source = _mapping_source(mapping)
if current_source is None:
raise ValueError(
"Airfoil mapping must use exactly one of designation or params."
)
if current_source == target_source:
return AirfoilApproximateSwitchResult(
spec=_switch_naca5_modified_source_exact(
mapping,
target_source,
)
)
if target_source == "params":
return AirfoilApproximateSwitchResult(
spec=_switch_naca5_modified_source_exact(
mapping,
target_source,
)
)
params_spec = _build_naca5_modified_params_spec(mapping)
designation = _naca5_modified_approximate_designation_from_params(
params_spec
)
approximated_spec = Naca5ModifiedAirfoilSpec(designation=designation)
approximated_params = _naca5_modified_params_from_designation(designation)
return AirfoilApproximateSwitchResult(
spec=approximated_spec,
approximated_fields=_naca5_modified_approximation_deltas(
requested=params_spec,
approximated=approximated_params,
),
)
def _switch_naca16_source_approx(
mapping: Mapping[str, object],
target_source: DefaultSpecSource,
) -> AirfoilApproximateSwitchResult:
"""Switch one NACA 16-series airfoil approximately."""
current_source = _mapping_source(mapping)
if current_source is None:
raise ValueError(
"Airfoil mapping must use exactly one of designation or params."
)
if current_source == target_source:
return AirfoilApproximateSwitchResult(
spec=_switch_naca16_source_exact(mapping, target_source)
)
if target_source == "params":
return AirfoilApproximateSwitchResult(
spec=_switch_naca16_source_exact(mapping, target_source)
)
params_spec = _build_naca16_params_spec(mapping)
designation = _naca16_approximate_designation_from_params(params_spec)
approximated_spec = Naca16AirfoilSpec(designation=designation)
approximated_params = _naca16_params_from_designation(designation)
return AirfoilApproximateSwitchResult(
spec=approximated_spec,
approximated_fields=_naca16_approximation_deltas(
requested=params_spec,
approximated=approximated_params,
),
)
def _switch_naca4_modified_source_exact(
mapping: Mapping[str, object],
target_source: DefaultSpecSource,
) -> Naca4ModifiedAirfoilSpec:
current_source = _mapping_source(mapping)
if current_source is None:
raise ValueError(
"Airfoil mapping must use exactly one of designation or params."
)
if current_source == target_source:
if target_source == "designation":
return Naca4ModifiedAirfoilSpec(
designation=_mapping_string(mapping, "designation"),
)
return Naca4ModifiedAirfoilSpec(
params=_build_naca4_modified_params_spec(mapping)
)
if target_source == "params":
designation = _mapping_string(mapping, "designation")
if designation == "":
default_spec = AirfoilFactory.default_spec(
"naca4_modified",
source="params",
)
if not isinstance(default_spec, Naca4ModifiedAirfoilSpec):
raise TypeError("Expected modified NACA 4 starter spec.")
return default_spec
return Naca4ModifiedAirfoilSpec(
params=_naca4_modified_params_from_designation(designation)
)
return Naca4ModifiedAirfoilSpec(
designation=_naca4_modified_designation_from_params(
_build_naca4_modified_params_spec(mapping)
)
)
def _switch_naca5_source_exact(
mapping: Mapping[str, object],
target_source: DefaultSpecSource,
) -> Naca5AirfoilSpec:
current_source = _mapping_source(mapping)
if current_source is None:
raise ValueError(
"Airfoil mapping must use exactly one of designation or params."
)
if current_source == target_source:
if target_source == "designation":
return Naca5AirfoilSpec(
designation=_mapping_string(mapping, "designation"),
)
return Naca5AirfoilSpec(params=_build_naca5_params_spec(mapping))
if target_source == "params":
designation = _mapping_string(mapping, "designation")
if designation == "":
default_spec = AirfoilFactory.default_spec(
"naca5",
source="params",
)
if not isinstance(default_spec, Naca5AirfoilSpec):
raise TypeError("Expected NACA 5 starter spec.")
return default_spec
return Naca5AirfoilSpec(
params=_naca5_params_from_designation(designation)
)
return Naca5AirfoilSpec(
designation=_naca5_designation_from_params(
_build_naca5_params_spec(mapping)
)
)
def _switch_naca5_modified_source_exact(
mapping: Mapping[str, object],
target_source: DefaultSpecSource,
) -> Naca5ModifiedAirfoilSpec:
current_source = _mapping_source(mapping)
if current_source is None:
raise ValueError(
"Airfoil mapping must use exactly one of designation or params."
)
if current_source == target_source:
if target_source == "designation":
return Naca5ModifiedAirfoilSpec(
designation=_mapping_string(mapping, "designation"),
)
return Naca5ModifiedAirfoilSpec(
params=_build_naca5_modified_params_spec(mapping)
)
if target_source == "params":
designation = _mapping_string(mapping, "designation")
if designation == "":
default_spec = AirfoilFactory.default_spec(
"naca5_modified",
source="params",
)
if not isinstance(default_spec, Naca5ModifiedAirfoilSpec):
raise TypeError("Expected modified NACA 5 starter spec.")
return default_spec
return Naca5ModifiedAirfoilSpec(
params=_naca5_modified_params_from_designation(designation)
)
return Naca5ModifiedAirfoilSpec(
designation=_naca5_modified_designation_from_params(
_build_naca5_modified_params_spec(mapping)
)
)
def _switch_naca16_source_exact(
mapping: Mapping[str, object],
target_source: DefaultSpecSource,
) -> Naca16AirfoilSpec:
current_source = _mapping_source(mapping)
if current_source is None:
raise ValueError(
"Airfoil mapping must use exactly one of designation or params."
)
if current_source == target_source:
if target_source == "designation":
return Naca16AirfoilSpec(
designation=_mapping_string(mapping, "designation"),
)
return Naca16AirfoilSpec(params=_build_naca16_params_spec(mapping))
if target_source == "params":
designation = _mapping_string(mapping, "designation")
if designation == "":
default_spec = AirfoilFactory.default_spec(
"naca16",
source="params",
)
if not isinstance(default_spec, Naca16AirfoilSpec):
raise TypeError("Expected NACA 16 starter spec.")
return default_spec
return Naca16AirfoilSpec(
params=_naca16_params_from_designation(designation)
)
return Naca16AirfoilSpec(
designation=_naca16_designation_from_params(
_build_naca16_params_spec(mapping)
)
)
def _params_mapping(mapping: Mapping[str, object]) -> dict[str, object]:
"""Return the nested params mapping or raise a source error."""
params = mapping.get("params")
if not isinstance(params, Mapping):
raise ValueError("Parameter source must use a mapping value.")
raw_params = cast(Mapping[object, object], params)
return {str(key): value for key, value in raw_params.items()}
def _mapping_string(mapping: Mapping[str, object], key: str) -> str:
"""Return one required string field from a mapping."""
value = mapping.get(key)
if not isinstance(value, str):
raise ValueError(f"{key} source must use a string value.")
return value
def _params_trailing_edge(
mapping: Mapping[str, object],
) -> Literal["standard", "sharp"]:
"""Return one trailing-edge mode from a params mapping."""
value = _params_mapping(mapping).get("trailing_edge")
if value not in {"standard", "sharp"}:
raise ValueError(
"Parameter 'trailing_edge' must be 'standard' or 'sharp'."
)
if value == "standard":
return "standard"
return "sharp"
def _params_leading_edge_radius(
mapping: Mapping[str, object],
) -> Literal["standard", "exact"]:
"""Return one leading-edge-radius mode from a params mapping."""
value = _params_mapping(mapping).get("leading_edge_radius")
if value not in {"standard", "exact"}:
raise ValueError(
"Parameter 'leading_edge_radius' must be 'standard' or 'exact'."
)
if value == "standard":
return "standard"
return "exact"
def _params_bool(mapping: Mapping[str, object], key: str) -> bool:
"""Return one required bool-valued params field."""
value = _params_mapping(mapping).get(key)
if not isinstance(value, bool):
raise ValueError(f"Parameter {key!r} must be a bool.")
return value
def _params_float(mapping: Mapping[str, object], key: str) -> float:
"""Return one required numeric params field as a float."""
value = _params_mapping(mapping).get(key)
if isinstance(value, bool) or not isinstance(value, int | float):
raise ValueError(f"Parameter {key!r} must be numeric.")
return float(value)
def _build_naca4_params_spec(
mapping: Mapping[str, object],
) -> Naca4AirfoilParamsSpec:
"""Build one NACA 4 params spec from a serialized params mapping."""
return Naca4AirfoilParamsSpec(
m=_params_float(mapping, "m"),
p=_params_float(mapping, "p"),
max_thickness=_params_float(mapping, "max_thickness"),
trailing_edge=_params_trailing_edge(mapping),
leading_edge_radius=_params_leading_edge_radius(mapping),
)
def _build_naca4_modified_params_spec(
mapping: Mapping[str, object],
) -> Naca4ModifiedAirfoilParamsSpec:
"""Build one modified NACA 4 params spec from serialized params."""
return Naca4ModifiedAirfoilParamsSpec(
m=_params_float(mapping, "m"),
p=_params_float(mapping, "p"),
max_thickness=_params_float(mapping, "max_thickness"),
leading_edge_index=_params_float(mapping, "leading_edge_index"),
max_thickness_location=_params_float(
mapping,
"max_thickness_location",
),
trailing_edge=_params_trailing_edge(mapping),
)
def _build_naca5_params_spec(
mapping: Mapping[str, object],
) -> Naca5AirfoilParamsSpec:
"""Build one NACA 5 params spec from a serialized params mapping."""
return Naca5AirfoilParamsSpec(
ideal_lift_coefficient=_params_float(
mapping,
"ideal_lift_coefficient",
),
max_camber_location=_params_float(
mapping,
"max_camber_location",
),
reflexed=_params_bool(mapping, "reflexed"),
max_thickness=_params_float(mapping, "max_thickness"),
trailing_edge=_params_trailing_edge(mapping),
leading_edge_radius=_params_leading_edge_radius(mapping),
)
def _build_naca5_modified_params_spec(
mapping: Mapping[str, object],
) -> Naca5ModifiedAirfoilParamsSpec:
"""Build one modified NACA 5 params spec from serialized params."""
return Naca5ModifiedAirfoilParamsSpec(
ideal_lift_coefficient=_params_float(
mapping,
"ideal_lift_coefficient",
),
max_camber_location=_params_float(
mapping,
"max_camber_location",
),
reflexed=_params_bool(mapping, "reflexed"),
max_thickness=_params_float(mapping, "max_thickness"),
leading_edge_index=_params_float(mapping, "leading_edge_index"),
max_thickness_location=_params_float(
mapping,
"max_thickness_location",
),
trailing_edge=_params_trailing_edge(mapping),
)
def _build_naca16_params_spec(
mapping: Mapping[str, object],
) -> Naca16AirfoilParamsSpec:
"""Build one NACA 16 params spec from a serialized params mapping."""
return Naca16AirfoilParamsSpec(
ideal_lift_coefficient=_params_float(
mapping,
"ideal_lift_coefficient",
),
max_thickness=_params_float(mapping, "max_thickness"),
)
def _naca4_params_from_designation(
designation: str,
) -> Naca4AirfoilParamsSpec:
"""Convert one NACA 4 designation to exact parameter form."""
AirfoilFactory.naca4_from_designation(designation)
return Naca4AirfoilParamsSpec(
m=int(designation[0]) / _NACA4_CAMBER_DIGIT_SCALE,
p=int(designation[1]) / _NACA4_LOCATION_DIGIT_SCALE,
max_thickness=int(designation[2:4]) / _THICKNESS_DIGIT_SCALE,
trailing_edge="standard",
leading_edge_radius="standard",
)
def _naca4_designation_from_params(
params_spec: Naca4AirfoilParamsSpec,
) -> str:
"""Convert one NACA 4 params spec to an exact designation."""
if params_spec.trailing_edge != "standard":
raise ValueError("Only standard trailing edges are exactly encodable.")
if params_spec.leading_edge_radius != "standard":
raise ValueError(
"Only standard leading-edge radii are exactly encodable."
)
designation = (
f"{_exact_digit(params_spec.m, _NACA4_CAMBER_DIGIT_SCALE)}"
f"{_exact_digit(params_spec.p, _NACA4_LOCATION_DIGIT_SCALE)}"
f"{_exact_digit(params_spec.max_thickness, _THICKNESS_DIGIT_SCALE):02d}"
)
if _naca4_params_from_designation(designation) != params_spec:
raise ValueError(
"Parameters are not exactly representable by designation."
)
return designation
def _naca4_approximate_designation_from_params(
params_spec: Naca4AirfoilParamsSpec,
) -> str:
"""Convert one NACA 4 params spec to the nearest designation."""
if params_spec.trailing_edge != "standard":
raise ValueError("Only standard trailing edges are exactly encodable.")
if params_spec.leading_edge_radius != "standard":
raise ValueError(
"Only standard leading-edge radii are exactly encodable."
)
m_digit = _clamp_int(round(float(params_spec.m) * 100.0), 0, 9)
if m_digit == 0:
p_digit = 0
else:
p_digit = _clamp_int(round(float(params_spec.p) * 10.0), 1, 9)
thickness_digit = _clamp_int(
round(float(params_spec.max_thickness) * 100.0),
0,
99,
)
return f"{m_digit}{p_digit}{thickness_digit:02d}"
def _naca4_approximation_deltas(
*,
requested: Naca4AirfoilParamsSpec,
approximated: Naca4AirfoilParamsSpec,
) -> tuple[ApproximationDelta, ...]:
"""Return field deltas introduced by approximate NACA 4 conversion."""
deltas: list[ApproximationDelta] = []
for field_name in ("m", "p", "max_thickness"):
requested_value = float(getattr(requested, field_name))
approximated_value = float(getattr(approximated, field_name))
if not math.isclose(
requested_value,
approximated_value,
rel_tol=0.0,
abs_tol=_AIRFOIL_EXACT_COMPARE_TOLERANCE,
):
deltas.append(
ApproximationDelta(
field_name=field_name,
requested_value=requested_value,
approximated_value=approximated_value,
)
)
return tuple(deltas)
def _naca4_modified_params_from_designation(
designation: str,
) -> Naca4ModifiedAirfoilParamsSpec:
"""Convert one modified NACA 4 designation to exact parameter form."""
AirfoilFactory.naca4_modified_from_designation(designation)
return Naca4ModifiedAirfoilParamsSpec(
m=int(designation[0]) / _NACA4_CAMBER_DIGIT_SCALE,
p=int(designation[1]) / _NACA4_LOCATION_DIGIT_SCALE,
max_thickness=int(designation[2:4]) / _THICKNESS_DIGIT_SCALE,
leading_edge_index=float(designation[5]),
max_thickness_location=(
int(designation[6]) / _MODIFIED_LOCATION_DIGIT_SCALE
),
trailing_edge="standard",
)
def _naca4_modified_designation_from_params(
params_spec: Naca4ModifiedAirfoilParamsSpec,
) -> str:
"""Convert one modified NACA 4 params spec to exact designation."""
if params_spec.trailing_edge != "standard":
raise ValueError("Only standard trailing edges are exactly encodable.")
leading_edge_digit = _exact_digit(params_spec.leading_edge_index, 1.0)
thickness_location_digit = _exact_digit(
params_spec.max_thickness_location,
_MODIFIED_LOCATION_DIGIT_SCALE,
)
designation = (
f"{_exact_digit(params_spec.m, _NACA4_CAMBER_DIGIT_SCALE)}"
f"{_exact_digit(params_spec.p, _NACA4_LOCATION_DIGIT_SCALE)}"
f"{_exact_digit(params_spec.max_thickness, _THICKNESS_DIGIT_SCALE):02d}"
f"-{leading_edge_digit}"
f"{thickness_location_digit}"
)
if _naca4_modified_params_from_designation(designation) != params_spec:
raise ValueError(
"Parameters are not exactly representable by designation."
)
return designation
def _naca4_modified_approximate_designation_from_params(
params_spec: Naca4ModifiedAirfoilParamsSpec,
) -> str:
"""Convert one modified NACA 4 params spec to the nearest designation."""
if params_spec.trailing_edge != "standard":
raise ValueError("Only standard trailing edges are exactly encodable.")
m_digit = _clamp_int(round(float(params_spec.m) * 100.0), 0, 9)
if m_digit == 0:
p_digit = 0
else:
p_digit = _clamp_int(round(float(params_spec.p) * 10.0), 1, 9)
thickness_digit = _clamp_int(
round(float(params_spec.max_thickness) * _THICKNESS_DIGIT_SCALE),
0,
99,
)
leading_edge_digit = _clamp_int(
round(float(params_spec.leading_edge_index)),
1,
9,
)
thickness_location_digit = _clamp_int(
round(
float(params_spec.max_thickness_location)
* _MODIFIED_LOCATION_DIGIT_SCALE
),
1,
9,
)
return (
f"{m_digit}"
f"{p_digit}"
f"{thickness_digit:02d}"
f"-{leading_edge_digit}"
f"{thickness_location_digit}"
)
def _naca4_modified_approximation_deltas(
*,
requested: Naca4ModifiedAirfoilParamsSpec,
approximated: Naca4ModifiedAirfoilParamsSpec,
) -> tuple[ApproximationDelta, ...]:
"""Return field deltas introduced by approximate NACA 4 conversion."""
deltas: list[ApproximationDelta] = []
for field_name in (
"m",
"p",
"max_thickness",
"leading_edge_index",
"max_thickness_location",
):
requested_value = float(getattr(requested, field_name))
approximated_value = float(getattr(approximated, field_name))
if not math.isclose(
requested_value,
approximated_value,
rel_tol=0.0,
abs_tol=_AIRFOIL_EXACT_COMPARE_TOLERANCE,
):
deltas.append(
ApproximationDelta(
field_name=field_name,
requested_value=requested_value,
approximated_value=approximated_value,
)
)
return tuple(deltas)
def _naca5_params_from_designation(
designation: str,
) -> Naca5AirfoilParamsSpec:
"""Convert one NACA 5 designation to exact parameter form."""
AirfoilFactory.naca5_from_designation(designation)
return Naca5AirfoilParamsSpec(
ideal_lift_coefficient=(int(designation[0]) / _NACA5_LIFT_DIGIT_SCALE),
max_camber_location=(
int(designation[1]) / _NACA_CAMBER_LOCATION_DIGIT_SCALE
),
reflexed=designation[2] == "1",
max_thickness=int(designation[3:5]) / _THICKNESS_DIGIT_SCALE,
trailing_edge="standard",
leading_edge_radius="standard",
)
def _naca5_designation_from_params(
params_spec: Naca5AirfoilParamsSpec,
) -> str:
"""Convert one NACA 5 params spec to an exact designation."""
if params_spec.trailing_edge != "standard":
raise ValueError("Only standard trailing edges are exactly encodable.")
if params_spec.leading_edge_radius != "standard":
raise ValueError(
"Only standard leading-edge radii are exactly encodable."
)
lift_digit = _exact_digit(
params_spec.ideal_lift_coefficient,
_NACA5_LIFT_DIGIT_SCALE,
)
camber_location_digit = _exact_digit(
params_spec.max_camber_location,
_NACA_CAMBER_LOCATION_DIGIT_SCALE,
)
designation = (
f"{lift_digit}"
f"{camber_location_digit}"
f"{1 if params_spec.reflexed else 0}"
f"{_exact_digit(params_spec.max_thickness, _THICKNESS_DIGIT_SCALE):02d}"
)
if _naca5_params_from_designation(designation) != params_spec:
raise ValueError(
"Parameters are not exactly representable by designation."
)
return designation
def _naca5_approximate_designation_from_params(
params_spec: Naca5AirfoilParamsSpec,
) -> str:
"""Convert one NACA 5 params spec to the nearest designation."""
if params_spec.trailing_edge != "standard":
raise ValueError("Only standard trailing edges are exactly encodable.")
if params_spec.leading_edge_radius != "standard":
raise ValueError(
"Only standard leading-edge radii are exactly encodable."
)
lift_digit = _clamp_int(
round(
float(params_spec.ideal_lift_coefficient) * _NACA5_LIFT_DIGIT_SCALE
),
0,
9,
)
camber_location_digit = _clamp_int(
round(
float(params_spec.max_camber_location)
* _NACA_CAMBER_LOCATION_DIGIT_SCALE
),
0,
9,
)
thickness_digit = _clamp_int(
round(float(params_spec.max_thickness) * _THICKNESS_DIGIT_SCALE),
0,
99,
)
return (
f"{lift_digit}"
f"{camber_location_digit}"
f"{1 if params_spec.reflexed else 0}"
f"{thickness_digit:02d}"
)
def _naca5_approximation_deltas(
*,
requested: Naca5AirfoilParamsSpec,
approximated: Naca5AirfoilParamsSpec,
) -> tuple[ApproximationDelta, ...]:
"""Return field deltas introduced by approximate NACA 5 conversion."""
deltas: list[ApproximationDelta] = []
for field_name in (
"ideal_lift_coefficient",
"max_camber_location",
"max_thickness",
):
requested_value = float(getattr(requested, field_name))
approximated_value = float(getattr(approximated, field_name))
if not math.isclose(
requested_value,
approximated_value,
rel_tol=0.0,
abs_tol=_AIRFOIL_EXACT_COMPARE_TOLERANCE,
):
deltas.append(
ApproximationDelta(
field_name=field_name,
requested_value=requested_value,
approximated_value=approximated_value,
)
)
return tuple(deltas)
def _naca5_modified_params_from_designation(
designation: str,
) -> Naca5ModifiedAirfoilParamsSpec:
"""Convert one modified NACA 5 designation to exact parameter form."""
AirfoilFactory.naca5_modified_from_designation(designation)
return Naca5ModifiedAirfoilParamsSpec(
ideal_lift_coefficient=(int(designation[0]) / _NACA5_LIFT_DIGIT_SCALE),
max_camber_location=(
int(designation[1]) / _NACA_CAMBER_LOCATION_DIGIT_SCALE
),
reflexed=designation[2] == "1",
max_thickness=int(designation[3:5]) / _THICKNESS_DIGIT_SCALE,
leading_edge_index=float(designation[6]),
max_thickness_location=(
int(designation[7]) / _MODIFIED_LOCATION_DIGIT_SCALE
),
trailing_edge="standard",
)
def _naca5_modified_designation_from_params(
params_spec: Naca5ModifiedAirfoilParamsSpec,
) -> str:
"""Convert one modified NACA 5 params spec to exact designation."""
if params_spec.trailing_edge != "standard":
raise ValueError("Only standard trailing edges are exactly encodable.")
lift_digit = _exact_digit(
params_spec.ideal_lift_coefficient,
_NACA5_LIFT_DIGIT_SCALE,
)
camber_location_digit = _exact_digit(
params_spec.max_camber_location,
_NACA_CAMBER_LOCATION_DIGIT_SCALE,
)
leading_edge_digit = _exact_digit(params_spec.leading_edge_index, 1.0)
thickness_location_digit = _exact_digit(
params_spec.max_thickness_location,
_MODIFIED_LOCATION_DIGIT_SCALE,
)
designation = (
f"{lift_digit}"
f"{camber_location_digit}"
f"{1 if params_spec.reflexed else 0}"
f"{_exact_digit(params_spec.max_thickness, _THICKNESS_DIGIT_SCALE):02d}"
f"-{leading_edge_digit}"
f"{thickness_location_digit}"
)
if _naca5_modified_params_from_designation(designation) != params_spec:
raise ValueError(
"Parameters are not exactly representable by designation."
)
return designation
def _naca5_modified_approximate_designation_from_params(
params_spec: Naca5ModifiedAirfoilParamsSpec,
) -> str:
"""Convert one modified NACA 5 params spec to the nearest designation."""
if params_spec.trailing_edge != "standard":
raise ValueError("Only standard trailing edges are exactly encodable.")
lift_digit = _clamp_int(
round(
float(params_spec.ideal_lift_coefficient) * _NACA5_LIFT_DIGIT_SCALE
),
0,
9,
)
camber_location_digit = _clamp_int(
round(
float(params_spec.max_camber_location)
* _NACA_CAMBER_LOCATION_DIGIT_SCALE
),
0,
9,
)
thickness_digit = _clamp_int(
round(float(params_spec.max_thickness) * _THICKNESS_DIGIT_SCALE),
0,
99,
)
leading_edge_digit = _clamp_int(
round(float(params_spec.leading_edge_index)),
1,
9,
)
thickness_location_digit = _clamp_int(
round(
float(params_spec.max_thickness_location)
* _MODIFIED_LOCATION_DIGIT_SCALE
),
1,
9,
)
return (
f"{lift_digit}"
f"{camber_location_digit}"
f"{1 if params_spec.reflexed else 0}"
f"{thickness_digit:02d}"
f"-{leading_edge_digit}"
f"{thickness_location_digit}"
)
def _naca5_modified_approximation_deltas(
*,
requested: Naca5ModifiedAirfoilParamsSpec,
approximated: Naca5ModifiedAirfoilParamsSpec,
) -> tuple[ApproximationDelta, ...]:
"""Return field deltas introduced by approximate NACA 5 conversion."""
deltas: list[ApproximationDelta] = []
for field_name in (
"ideal_lift_coefficient",
"max_camber_location",
"max_thickness",
"leading_edge_index",
"max_thickness_location",
):
requested_value = float(getattr(requested, field_name))
approximated_value = float(getattr(approximated, field_name))
if not math.isclose(
requested_value,
approximated_value,
rel_tol=0.0,
abs_tol=_AIRFOIL_EXACT_COMPARE_TOLERANCE,
):
deltas.append(
ApproximationDelta(
field_name=field_name,
requested_value=requested_value,
approximated_value=approximated_value,
)
)
return tuple(deltas)
def _naca16_params_from_designation(
designation: str,
) -> Naca16AirfoilParamsSpec:
"""Convert one NACA 16 designation to exact parameter form."""
if len(
designation
) != _NACA16_DESIGNATION_LENGTH or not designation.startswith("16-"):
raise ValueError(
"NACA 16-series designations must use the form 16-###."
)
thickness_digits = designation[4:6]
if not designation[3].isdigit() or not thickness_digits.isdigit():
raise ValueError(
"NACA 16-series designations must use the form 16-###."
)
return Naca16AirfoilParamsSpec(
ideal_lift_coefficient=int(designation[3]) / 10.0,
max_thickness=int(thickness_digits) / _THICKNESS_DIGIT_SCALE,
)
def _naca16_designation_from_params(
params_spec: Naca16AirfoilParamsSpec,
) -> str:
"""Convert one NACA 16 params spec to an exact designation."""
designation = (
"16-"
f"{_exact_digit(params_spec.ideal_lift_coefficient, 10.0)}"
f"{_exact_digit(params_spec.max_thickness, _THICKNESS_DIGIT_SCALE):02d}"
)
if _naca16_params_from_designation(designation) != params_spec:
raise ValueError(
"Parameters are not exactly representable by designation."
)
return designation
def _naca16_approximate_designation_from_params(
params_spec: Naca16AirfoilParamsSpec,
) -> str:
"""Convert one NACA 16 params spec to the nearest designation."""
lift_digit = _clamp_int(
round(float(params_spec.ideal_lift_coefficient) * 10.0),
0,
9,
)
thickness_digit = _clamp_int(
round(float(params_spec.max_thickness) * _THICKNESS_DIGIT_SCALE),
0,
99,
)
return f"16-{lift_digit}{thickness_digit:02d}"
def _naca16_approximation_deltas(
*,
requested: Naca16AirfoilParamsSpec,
approximated: Naca16AirfoilParamsSpec,
) -> tuple[ApproximationDelta, ...]:
"""Return field deltas introduced by approximate NACA 16 conversion."""
deltas: list[ApproximationDelta] = []
for field_name in ("ideal_lift_coefficient", "max_thickness"):
requested_value = float(getattr(requested, field_name))
approximated_value = float(getattr(approximated, field_name))
if not math.isclose(
requested_value,
approximated_value,
rel_tol=0.0,
abs_tol=_AIRFOIL_EXACT_COMPARE_TOLERANCE,
):
deltas.append(
ApproximationDelta(
field_name=field_name,
requested_value=requested_value,
approximated_value=approximated_value,
)
)
return tuple(deltas)
def _exact_digit(value: object, scale: float) -> int:
"""Return one exact encoded digit value or reject inexact inputs."""
if isinstance(value, bool) or not isinstance(value, int | float):
raise ValueError("Expected one numeric parameter value.")
scaled_value = float(value) * scale
rounded_value = round(scaled_value)
if not math.isclose(
scaled_value,
rounded_value,
rel_tol=0.0,
abs_tol=_AIRFOIL_EXACT_COMPARE_TOLERANCE,
):
raise ValueError(
f"Value {value!r} is not exactly representable in designation."
)
return int(rounded_value)
def _clamp_int(value: int, minimum: int, maximum: int) -> int:
"""Clamp one integer value into the inclusive target range."""
return max(minimum, min(value, maximum))
def _describe_spec(spec: AirfoilDefinitionSpec) -> AirfoilSpecSummary:
"""Return a human-readable summary for one airfoil definition spec."""
family = _SUPPORTED_FAMILY_INFO_BY_TYPE.get(spec.type)
if family is None:
raise TypeError(
f"Unsupported airfoil spec type {spec.type!r} for description."
)
helper: SummaryDescriber | None = None
if spec.type == "naca4":
helper = cast(SummaryDescriber, _describe_naca4_spec)
elif spec.type == "naca4_modified":
helper = cast(SummaryDescriber, _describe_naca4_modified_spec)
elif spec.type == "naca5":
helper = cast(SummaryDescriber, _describe_naca5_spec)
elif spec.type == "naca5_modified":
helper = cast(SummaryDescriber, _describe_naca5_modified_spec)
elif spec.type == "naca16":
helper = cast(SummaryDescriber, _describe_naca16_spec)
elif spec.type == "ellipse":
helper = cast(SummaryDescriber, _describe_ellipse_spec)
elif spec.type == "cst":
helper = cast(SummaryDescriber, _describe_cst_spec)
elif spec.type == "file":
helper = cast(SummaryDescriber, _describe_file_spec)
elif spec.type == "points":
helper = cast(SummaryDescriber, _describe_points_spec)
elif spec.type == "spline":
helper = cast(SummaryDescriber, _describe_spline_spec)
if helper is None:
return _describe_generic_spec(spec, family)
return helper(spec, family)
def _describe_naca4_spec(
spec: Naca4AirfoilSpec,
family: AirfoilFamilyInfo,
) -> AirfoilSpecSummary:
"""Describe one classic NACA 4-digit airfoil spec."""
if spec.designation is not None:
designation = spec.designation
return AirfoilSpecSummary(
family=family.display_name,
source="designation",
short=f"NACA {designation}",
long=(
f"{family.display_name} airfoil defined by designation "
f"{designation}"
),
)
params = spec.params
if params is None:
return _describe_generic_spec(spec, family)
return AirfoilSpecSummary(
family=family.display_name,
source="params",
short="NACA 2412-style airfoil (explicit params)",
long=(
"NACA 4-Digit airfoil defined by explicit params "
f"(m={_format_scalar(params.m)}, "
f"p={_format_scalar(params.p)}, "
f"max_thickness={_format_scalar(params.max_thickness)}, "
f"{params.leading_edge_radius} leading edge radius, "
f"{params.trailing_edge} trailing edge)"
),
)
def _describe_ellipse_spec(
spec: object,
family: AirfoilFamilyInfo,
) -> AirfoilSpecSummary:
"""Describe one ellipse-family spec."""
params = getattr(spec, "params", None)
if params is None:
return _describe_generic_spec(cast(AirfoilDefinitionSpec, spec), family)
max_thickness = getattr(params, "max_thickness", None)
if not isinstance(max_thickness, int | float):
return _describe_generic_spec(cast(AirfoilDefinitionSpec, spec), family)
return AirfoilSpecSummary(
family=family.display_name,
source="params",
short=(
f"{family.display_name} airfoil (t={_format_scalar(max_thickness)})"
),
long=(
f"{family.display_name} airfoil with "
f"max_thickness={_format_scalar(max_thickness)}"
),
)
def _describe_naca5_spec(
spec: Naca5AirfoilSpec,
family: AirfoilFamilyInfo,
) -> AirfoilSpecSummary:
"""Describe one classic NACA 5-digit airfoil spec."""
if spec.designation is not None:
designation = spec.designation
return AirfoilSpecSummary(
family=family.display_name,
source="designation",
short=f"NACA {designation}",
long=(
f"{family.display_name} airfoil defined by designation "
f"{designation}"
),
)
params = spec.params
if params is None:
return _describe_generic_spec(spec, family)
short = "NACA 5-digit airfoil (explicit params)"
try:
style_designation = _naca5_designation_from_params(params)
except ValueError:
pass
else:
short = f"NACA {style_designation}-style airfoil (explicit params)"
reflexed_text = "reflexed" if params.reflexed else "normal camber"
return AirfoilSpecSummary(
family=family.display_name,
source="params",
short=short,
long=(
"NACA 5-Digit airfoil defined by explicit params "
f"(ideal_lift_coefficient="
f"{_format_scalar(params.ideal_lift_coefficient)}, "
f"max_camber_location="
f"{_format_scalar(params.max_camber_location)}, "
f"reflexed={params.reflexed}, "
f"max_thickness={_format_scalar(params.max_thickness)}, "
f"{reflexed_text}, "
f"{params.leading_edge_radius} leading edge radius, "
f"{params.trailing_edge} trailing edge)"
),
)
def _describe_naca4_modified_spec(
spec: Naca4ModifiedAirfoilSpec,
family: AirfoilFamilyInfo,
) -> AirfoilSpecSummary:
"""Describe one modified NACA 4-digit airfoil spec."""
if spec.designation is not None:
designation = spec.designation
return AirfoilSpecSummary(
family=family.display_name,
source="designation",
short=f"NACA {designation}",
long=(
f"{family.display_name} airfoil defined by designation "
f"{designation}"
),
)
params = spec.params
if params is None:
return _describe_generic_spec(spec, family)
short = "NACA 4-digit modified airfoil (explicit params)"
try:
style_designation = _naca4_modified_designation_from_params(params)
except ValueError:
pass
else:
short = f"NACA {style_designation}-style airfoil (explicit params)"
return AirfoilSpecSummary(
family=family.display_name,
source="params",
short=short,
long=(
"NACA 4-Digit Modified airfoil defined by explicit params "
f"(m={_format_scalar(params.m)}, "
f"p={_format_scalar(params.p)}, "
f"max_thickness={_format_scalar(params.max_thickness)}, "
f"leading_edge_index="
f"{_format_scalar(params.leading_edge_index)}, "
f"max_thickness_location="
f"{_format_scalar(params.max_thickness_location)}, "
f"{params.trailing_edge} trailing edge)"
),
)
def _describe_naca5_modified_spec(
spec: Naca5ModifiedAirfoilSpec,
family: AirfoilFamilyInfo,
) -> AirfoilSpecSummary:
"""Describe one modified NACA 5-digit airfoil spec."""
if spec.designation is not None:
designation = spec.designation
return AirfoilSpecSummary(
family=family.display_name,
source="designation",
short=f"NACA {designation}",
long=(
f"{family.display_name} airfoil defined by designation "
f"{designation}"
),
)
params = spec.params
if params is None:
return _describe_generic_spec(spec, family)
short = "NACA 5-digit modified airfoil (explicit params)"
try:
style_designation = _naca5_modified_designation_from_params(params)
except ValueError:
pass
else:
short = f"NACA {style_designation}-style airfoil (explicit params)"
reflexed_text = "reflexed" if params.reflexed else "normal camber"
return AirfoilSpecSummary(
family=family.display_name,
source="params",
short=short,
long=(
"NACA 5-Digit Modified airfoil defined by explicit params "
f"(ideal_lift_coefficient="
f"{_format_scalar(params.ideal_lift_coefficient)}, "
f"max_camber_location="
f"{_format_scalar(params.max_camber_location)}, "
f"reflexed={params.reflexed}, "
f"max_thickness={_format_scalar(params.max_thickness)}, "
f"leading_edge_index="
f"{_format_scalar(params.leading_edge_index)}, "
f"max_thickness_location="
f"{_format_scalar(params.max_thickness_location)}, "
f"{reflexed_text}, "
f"{params.trailing_edge} trailing edge)"
),
)
def _describe_naca16_spec(
spec: Naca16AirfoilSpec,
family: AirfoilFamilyInfo,
) -> AirfoilSpecSummary:
"""Describe one NACA 16-series airfoil spec."""
if spec.designation is not None:
designation = spec.designation
return AirfoilSpecSummary(
family=family.display_name,
source="designation",
short=f"NACA {designation}",
long=(
f"{family.display_name} airfoil defined by designation "
f"{designation}"
),
)
params = spec.params
if params is None:
return _describe_generic_spec(spec, family)
short = "NACA 16-series airfoil (explicit params)"
try:
style_designation = _naca16_designation_from_params(params)
except ValueError:
pass
else:
short = f"NACA {style_designation}-style airfoil (explicit params)"
return AirfoilSpecSummary(
family=family.display_name,
source="params",
short=short,
long=(
"NACA 16-Series airfoil defined by explicit params "
f"(ideal_lift_coefficient="
f"{_format_scalar(params.ideal_lift_coefficient)}, "
f"max_thickness={_format_scalar(params.max_thickness)})"
),
)
def _describe_cst_spec(
spec: CstAirfoilSpec,
family: AirfoilFamilyInfo,
) -> AirfoilSpecSummary:
"""Describe one CST airfoil spec."""
upper_count = _list_length(spec.upper.a)
lower_count = _list_length(spec.lower.a)
return AirfoilSpecSummary(
family=family.display_name,
source="coefficients",
short=(
f"{family.display_name} airfoil "
f"({upper_count} upper, {lower_count} lower coefficients)"
),
long=(
f"{family.display_name} airfoil defined by CST coefficients "
f"({upper_count} upper, {lower_count} lower; "
f"trailing_edge_thickness="
f"{_format_scalar(spec.trailing_edge_thickness)})"
),
)
def _describe_file_spec(
spec: object,
family: AirfoilFamilyInfo,
) -> AirfoilSpecSummary:
"""Describe one file-backed airfoil spec."""
path = getattr(spec, "path", "")
file_format = getattr(spec, "format", "auto")
if not isinstance(path, str) or not isinstance(file_format, str):
return _describe_generic_spec(cast(AirfoilDefinitionSpec, spec), family)
file_name = PurePath(path).name if path else "unnamed source"
return AirfoilSpecSummary(
family=family.display_name,
source="file",
short=f"{family.display_name} airfoil from {file_name}",
long=(
f"{family.display_name} airfoil defined by file source "
f"{path or '<unspecified>'} "
f"(format={file_format})"
),
)
def _describe_points_spec(
spec: object,
family: AirfoilFamilyInfo,
) -> AirfoilSpecSummary:
"""Describe one point-backed airfoil spec."""
point_format = getattr(spec, "format", None)
if not isinstance(point_format, str):
return _describe_generic_spec(cast(AirfoilDefinitionSpec, spec), family)
if point_format == "surface_curve":
points = getattr(spec, "points", None)
count = _list_length(points)
short = (
f"{family.display_name} airfoil ({point_format}, {count} points)"
)
long = (
f"{family.display_name} airfoil defined by {count} inline "
f"points using {point_format} ordering"
)
else:
upper = getattr(spec, "upper", None)
lower = getattr(spec, "lower", None)
upper_count = _list_length(upper)
lower_count = _list_length(lower)
short = (
f"{family.display_name} airfoil "
f"({point_format}, {upper_count}+{lower_count} points)"
)
long = (
f"{family.display_name} airfoil defined by inline upper/lower "
f"points ({upper_count} upper, {lower_count} lower)"
)
return AirfoilSpecSummary(
family=family.display_name,
source="points",
short=short,
long=long,
)
def _describe_spline_spec(
spec: object,
family: AirfoilFamilyInfo,
) -> AirfoilSpecSummary:
"""Describe one spline-backed airfoil spec."""
representation = getattr(spec, "representation", None)
upper = getattr(spec, "upper", None)
lower = getattr(spec, "lower", None)
upper_points = getattr(upper, "control_points", None)
lower_points = getattr(lower, "control_points", None)
upper_count = _list_length(upper_points)
lower_count = _list_length(lower_points)
if not isinstance(representation, str):
return _describe_generic_spec(cast(AirfoilDefinitionSpec, spec), family)
return AirfoilSpecSummary(
family=family.display_name,
source="spline",
short=f"{family.display_name} airfoil ({representation})",
long=(
f"{family.display_name} airfoil defined by "
f"{representation} control points "
f"({upper_count} upper, {lower_count} lower)"
),
)
def _describe_generic_spec(
spec: AirfoilDefinitionSpec,
family: AirfoilFamilyInfo,
) -> AirfoilSpecSummary:
"""Describe one spec using generic family/source formatting."""
source = _summary_source(spec)
designation = getattr(spec, "designation", None)
if isinstance(designation, str) and designation:
short_prefix = _designation_short_prefix(family.display_name)
return AirfoilSpecSummary(
family=family.display_name,
source=source,
short=f"{short_prefix} {designation}",
long=(
f"{family.display_name} airfoil defined by designation "
f"{designation}"
),
)
if source == "params":
return AirfoilSpecSummary(
family=family.display_name,
source=source,
short=f"{family.display_name} airfoil (explicit params)",
long=f"{family.display_name} airfoil defined by explicit params",
)
short = f"{family.display_name} airfoil"
if source is not None:
long = f"{family.display_name} airfoil defined from {source} data"
else:
long = f"{family.display_name} airfoil definition"
return AirfoilSpecSummary(
family=family.display_name,
source=source,
short=short,
long=long,
)
def _summary_source(spec: AirfoilDefinitionSpec) -> str | None:
"""Return a concise source label for one spec."""
designation = getattr(spec, "designation", None)
if isinstance(designation, str) and designation:
return "designation"
params = getattr(spec, "params", None)
if params is not None:
return "params"
if spec.type == "file":
return "file"
if spec.type == "points":
return "points"
if spec.type == "spline":
return "spline"
if spec.type == "cst":
return "coefficients"
return None
def _designation_short_prefix(family_name: str) -> str:
"""Return the compact short-form family prefix for designations."""
if family_name.startswith("NACA "):
return "NACA"
return family_name
def _format_scalar(value: object) -> str:
"""Format one scalar value compactly for human-readable summaries."""
if isinstance(value, bool) or not isinstance(value, int | float):
raise TypeError(f"Expected a numeric scalar, received {value!r}.")
return f"{float(value):.6g}"
def _list_length(value: object) -> int:
"""Return the length of one list-like payload when present."""
if not isinstance(value, list):
return 0
items = cast(list[object], value)
return len(items)
[docs]
class AirfoilFactory:
"""
Construct public airfoil runtime objects from user-facing specs.
This factory centralizes the mapping from serialized airfoil schema
objects to the concrete runtime classes used by the geometry library.
"""
[docs]
@staticmethod
def from_spec(spec: AirfoilDefinitionSpec) -> Airfoil:
"""
Build an airfoil runtime object from a validated schema definition.
Parameters
----------
spec : AirfoilDefinitionSpec
User-facing airfoil definition to dispatch to the appropriate
runtime family.
Returns
-------
Airfoil
Concrete airfoil runtime instance compatible with ``spec``.
Raises
------
TypeError
If ``spec`` does not belong to a supported schema family.
ValueError
If ``spec`` fails family-specific validation during
construction.
"""
return from_spec(spec)
[docs]
@staticmethod
def camber_curve_from_document(
document: AirfoilDocumentSpec,
*,
name: str,
) -> AirfoilCamberResult:
"""
Resolve one named document curve into a runtime camber result.
Parameters
----------
document : AirfoilDocumentSpec
Document containing named airfoils and named curves.
name : str
Name of the curve entry in ``document.curves`` to resolve.
Returns
-------
AirfoilCamberResult
Exact or approximate runtime camber-curve result.
Raises
------
KeyError
If ``name`` or the referenced source airfoil name is unknown.
NotImplementedError
If the referenced airfoil family is not constructable by the
current runtime factory.
ValueError
If the curve approximation settings are invalid.
"""
curve_spec = AirfoilFactory._curve_spec_from_document(
document,
name=name,
)
if not isinstance(curve_spec, AirfoilCamberCurveSpec):
msg = f"Document curve does not resolve to a camber curve: {name}"
raise TypeError(msg)
return _camber_curve_result_from_document_spec(
document,
curve_name=name,
curve_spec=curve_spec,
)
[docs]
@staticmethod
def curve_from_document(
document: AirfoilDocumentSpec,
*,
name: str,
) -> Curve:
"""
Resolve one named document curve into a runtime open-curve object.
Parameters
----------
document : AirfoilDocumentSpec
Document containing named airfoils and named curves.
name : str
Name of the curve entry in ``document.curves`` to resolve.
Returns
-------
Curve
Runtime open-curve object compatible with the named document
entry.
"""
return AirfoilFactory.curve_result_from_document(
document,
name=name,
).curve
[docs]
@staticmethod
def curve_result_from_document(
document: AirfoilDocumentSpec,
*,
name: str,
) -> AirfoilDocumentCurveResult:
"""
Resolve one named document curve plus its provenance metadata.
Parameters
----------
document : AirfoilDocumentSpec
Document containing named airfoils and named curves.
name : str
Name of the curve entry in ``document.curves`` to resolve.
Returns
-------
AirfoilDocumentCurveResult
Runtime curve together with schema and source metadata useful
for downstream UI and analysis workflows.
"""
curve_spec = AirfoilFactory._curve_spec_from_document(
document,
name=name,
)
return _curve_result_from_document_spec(
document,
curve_name=name,
curve_spec=curve_spec,
)
@staticmethod
def _curve_spec_from_document(
document: AirfoilDocumentSpec,
*,
name: str,
) -> CurveDefinitionSpec:
"""Return one named document curve spec or raise a helpful error."""
try:
return document.curves[name]
except KeyError as exc:
msg = f"Unknown document curve: {name}"
raise KeyError(msg) from exc
[docs]
@staticmethod
def describe_spec(spec: AirfoilDefinitionSpec) -> AirfoilSpecSummary:
"""
Return a human-readable summary for one airfoil definition spec.
Parameters
----------
spec : AirfoilDefinitionSpec
User-facing airfoil definition to summarize.
Returns
-------
AirfoilSpecSummary
Immutable compact and verbose descriptions for ``spec``.
"""
return _describe_spec(spec)
[docs]
@staticmethod
def supported_families() -> tuple[AirfoilFamilyInfo, ...]:
"""
Return stable metadata for public airfoil schema families.
Returns
-------
tuple[AirfoilFamilyInfo, ...]
Ordered family descriptors for the currently recognized
public schema surface.
Notes
-----
``constructable`` identifies which families the runtime factory
can build today. Non-constructable families may still be part of
the documented schema surface or planned public roadmap.
"""
return _SUPPORTED_FAMILY_INFO
[docs]
@staticmethod
def default_spec(
type_name: str,
*,
source: DefaultSpecSource | None = None,
) -> AirfoilDefinitionSpec:
"""
Return the canonical starter spec for one public airfoil family.
Parameters
----------
type_name : str
Stable public family discriminator from
``AirfoilFactory.supported_families()``.
source : {"designation", "params"} | None, optional
Requested source branch for families that support multiple
construction forms.
When omitted, this method uses the family's canonical
``default_source`` metadata.
Returns
-------
AirfoilDefinitionSpec
Schema-valid, editor-appropriate starter spec for the
requested family.
Raises
------
ValueError
If ``type_name`` is unsupported or ``source`` is incompatible
with the requested family.
"""
family = _SUPPORTED_FAMILY_INFO_BY_TYPE.get(type_name)
if family is None:
raise ValueError(f"Unsupported airfoil family {type_name!r}.")
resolved_source = _resolve_default_spec_source(
family=family,
source=source,
)
return build_default_spec(type_name, source=resolved_source)
[docs]
@staticmethod
def exact_source_choices(
type_name: str,
) -> tuple[DefaultSpecSource, ...]:
"""Return exact-switch source choices for one airfoil family."""
return _exact_source_choices(type_name)
[docs]
@staticmethod
def mapping_source(
mapping: Mapping[str, object],
) -> DefaultSpecSource | None:
"""Return which mutually exclusive source branch a mapping uses."""
return _mapping_source(mapping)
[docs]
@staticmethod
def switch_source_exact(
mapping: Mapping[str, object],
target_source: DefaultSpecSource,
) -> AirfoilDefinitionSpec:
"""Switch one exact-only NACA family between source branches."""
return _switch_source_exact(mapping, target_source)
[docs]
@staticmethod
def switch_source_approx_result(
mapping: Mapping[str, object],
target_source: DefaultSpecSource,
) -> AirfoilApproximateSwitchResult:
"""Switch one airfoil family approximately and report metadata."""
return _switch_source_approx_result(mapping, target_source)
[docs]
@staticmethod
def switch_source_approx(
mapping: Mapping[str, object],
target_source: DefaultSpecSource,
) -> AirfoilDefinitionSpec:
"""Switch one airfoil family between source branches approximately."""
return _switch_source_approx_result(mapping, target_source).spec
[docs]
@staticmethod
def naca4_from_designation(
designation: str,
) -> Naca4AirfoilClassic:
"""
Build a classic NACA 4-digit airfoil from its designation string.
Parameters
----------
designation : str
Four-digit NACA designation such as ``"2412"``.
Returns
-------
Naca4AirfoilClassic
Runtime NACA 4-digit airfoil matching ``designation``.
Raises
------
ValueError
If ``designation`` is not a valid classic NACA 4-digit code.
"""
return naca4_from_designation(designation)
[docs]
@staticmethod
def naca4_from_params(
*,
m: float,
p: float,
t: float,
trailing_edge: Literal["standard", "sharp"] = "standard",
leading_edge_radius: Literal["standard", "exact"] = "standard",
) -> Naca4AirfoilParams:
"""
Build a parametric NACA 4-digit airfoil from explicit parameters.
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 relation.
leading_edge_radius : {"standard", "exact"}, default="standard"
Leading-edge radius treatment for the thickness relation.
Returns
-------
Naca4AirfoilParams
Runtime NACA 4-digit airfoil built from the supplied
parameters.
Raises
------
ValueError
If any parameter falls outside the supported NACA 4-digit
range.
"""
return naca4_from_params(
m=m,
p=p,
t=t,
trailing_edge=trailing_edge,
leading_edge_radius=leading_edge_radius,
)
[docs]
@staticmethod
def naca4_modified_from_designation(
designation: str,
) -> Naca4ModifiedAirfoilClassic:
"""
Build a classic modified NACA 4-digit airfoil from a designation.
Parameters
----------
designation : str
Modified NACA 4-digit designation string.
Returns
-------
Naca4ModifiedAirfoilClassic
Runtime modified NACA 4-digit airfoil matching
``designation``.
Raises
------
ValueError
If ``designation`` is not a valid modified NACA 4-digit code.
"""
return naca4_modified_from_designation(designation)
[docs]
@staticmethod
def naca4_modified_from_params(
*,
m: float,
p: float,
t: float,
leading_edge_index: float,
max_thickness_location: float,
trailing_edge: Literal["standard", "sharp"] = "standard",
) -> Naca4ModifiedAirfoilParams:
"""
Build a parametric modified NACA 4-digit airfoil from parameters.
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.
leading_edge_index : float
Modified NACA leading-edge index used by the thickness model.
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 relation.
Returns
-------
Naca4ModifiedAirfoilParams
Runtime modified NACA 4-digit airfoil built from the supplied
parameters.
Raises
------
ValueError
If any parameter falls outside the supported modified NACA
4-digit range.
"""
return naca4_modified_from_params(
m=m,
p=p,
t=t,
leading_edge_index=leading_edge_index,
max_thickness_location=max_thickness_location,
trailing_edge=trailing_edge,
)
[docs]
@staticmethod
def naca5_from_designation(
designation: str,
) -> Naca5AirfoilClassic:
"""
Build a classic NACA 5-digit airfoil from its designation string.
Parameters
----------
designation : str
Five-digit NACA designation such as ``"23012"``.
Returns
-------
Naca5AirfoilClassic
Runtime NACA 5-digit airfoil matching ``designation``.
Raises
------
ValueError
If ``designation`` is not a valid classic NACA 5-digit code.
"""
return naca5_from_designation(designation)
[docs]
@staticmethod
def naca5_from_params(
*,
ideal_lift_coefficient: float,
max_camber_location: float,
reflexed: bool,
t: float,
trailing_edge: Literal["standard", "sharp"] = "standard",
leading_edge_radius: Literal["standard", "exact"] = "standard",
) -> Naca5AirfoilParams:
"""
Build a parametric NACA 5-digit airfoil from explicit parameters.
Parameters
----------
ideal_lift_coefficient : float
Design lift coefficient encoded by the NACA 5-digit family.
max_camber_location : float
Chordwise location of maximum camber as a fraction of chord.
reflexed : bool
Whether to use the reflexed camber-line variant.
t : float
Maximum thickness as a fraction of chord.
trailing_edge : {"standard", "sharp"}, default="standard"
Trailing-edge closure model for the thickness relation.
leading_edge_radius : {"standard", "exact"}, default="standard"
Leading-edge radius treatment for the thickness relation.
Returns
-------
Naca5AirfoilParams
Runtime NACA 5-digit airfoil built from the supplied
parameters.
Raises
------
ValueError
If any parameter falls outside the supported NACA 5-digit
range.
"""
return naca5_from_params(
ideal_lift_coefficient=ideal_lift_coefficient,
max_camber_location=max_camber_location,
reflexed=reflexed,
t=t,
trailing_edge=trailing_edge,
leading_edge_radius=leading_edge_radius,
)
[docs]
@staticmethod
def naca5_modified_from_designation(
designation: str,
) -> Naca5ModifiedAirfoilClassic:
"""
Build a classic modified NACA 5-digit airfoil from a designation.
Parameters
----------
designation : str
Modified NACA 5-digit designation string.
Returns
-------
Naca5ModifiedAirfoilClassic
Runtime modified NACA 5-digit airfoil matching
``designation``.
Raises
------
ValueError
If ``designation`` is not a valid modified NACA 5-digit code.
"""
return naca5_modified_from_designation(designation)
[docs]
@staticmethod
def naca5_modified_from_params(
*,
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",
) -> Naca5ModifiedAirfoilParams:
"""
Build a parametric modified NACA 5-digit airfoil from parameters.
Parameters
----------
ideal_lift_coefficient : float
Design lift coefficient encoded by the NACA 5-digit family.
max_camber_location : float
Chordwise location of maximum camber as a fraction of chord.
reflexed : bool
Whether to use the reflexed camber-line variant.
t : float
Maximum thickness as a fraction of chord.
leading_edge_index : float
Modified NACA leading-edge index used by the thickness model.
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 relation.
Returns
-------
Naca5ModifiedAirfoilParams
Runtime modified NACA 5-digit airfoil built from the supplied
parameters.
Raises
------
ValueError
If any parameter falls outside the supported modified NACA
5-digit range.
"""
return naca5_modified_from_params(
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,
)