Examples
Start with the smallest public examples first. They show the intended import style and the current user-facing entry points.
Airfoil Basics
These examples introduce the public airfoil construction workflow from the simplest entry points upward.
Airfoil From Designation
This example creates a NACA 2412 airfoil from a designation and samples a few surface coordinates.
Run it with:
uv run python examples/airfoil/create_naca4_from_designation.py
"""Create and inspect a NACA 4-digit airfoil from a designation."""
import numpy as np
import buffalo_wings.airfoil as bwa
def main() -> None:
"""Create a NACA 2412 airfoil and print basic geometry data."""
airfoil = bwa.AirfoilFactory.naca4_from_designation("2412")
t = np.linspace(-1.0, 1.0, 9, dtype=np.float64)
x, y = airfoil.xy_from_u(t)
leading_edge = airfoil.leading_edge()
trailing_edge = airfoil.trailing_edge()
print("Airfoil: NACA 2412")
print(f"Chord: {airfoil.chord():.6f}")
print(
"Leading edge:",
f"({leading_edge[0]:.6f}, {leading_edge[1]:.6f})",
)
print(
"Trailing edge:",
f"({trailing_edge[0]:.6f}, {trailing_edge[1]:.6f})",
)
print("Sampled surface coordinates:")
for ti, xi, yi in zip(t, x, y, strict=True):
print(f" t={ti: .2f} -> x={xi:.6f}, y={yi:.6f}")
if __name__ == "__main__":
main()
Expected output:
Airfoil: NACA 2412
Chord: 1.000000
Leading edge: (0.000000, 0.000000)
Trailing edge: (1.000000, 0.000000)
Sampled surface coordinates:
t=-1.00 -> x=0.999916, y=-0.001257
t=-0.75 -> x=0.561623, y=-0.030053
t=-0.50 -> x=0.252226, y=-0.042183
t=-0.25 -> x=0.065781, y=-0.033127
t= 0.00 -> x=0.000000, y=0.000000
t= 0.25 -> x=0.059219, y=0.044650
t= 0.50 -> x=0.247774, y=0.076558
t= 0.75 -> x=0.563377, y=0.067119
t= 1.00 -> x=1.000084, y=0.001257
Airfoil From Spec
This example creates the same airfoil from a public schema object and passes it through AirfoilFactory.from_spec(...).
For the currently supported schema-backed runtime families, the resulting airfoil can also be converted back to the same schema form with airfoil.to_spec().
Run it with:
uv run python examples/airfoil/create_naca4_from_spec.py
"""Create and inspect a NACA 4-digit airfoil from a public spec."""
import numpy as np
import buffalo_wings.airfoil as bwa
def main() -> None:
"""Create a NACA 2412 airfoil from a public schema object."""
spec = bwa.Naca4AirfoilSpec(designation="2412")
airfoil = bwa.AirfoilFactory.from_spec(spec)
t = np.linspace(-1.0, 1.0, 5, dtype=np.float64)
x, y = airfoil.xy_from_u(t)
print("Spec:")
print(spec)
print(f"Surface length: {airfoil.length:.6f}")
print("Sampled coordinates from AirfoilFactory.from_spec:")
for ti, xi, yi in zip(t, x, y, strict=True):
print(f" t={ti: .2f} -> x={xi:.6f}, y={yi:.6f}")
if __name__ == "__main__":
main()
Expected output:
Spec:
Naca4AirfoilSpec(type='naca4', designation='2412', params=None)
Surface length: 2.041402
Sampled coordinates from AirfoilFactory.from_spec:
t=-1.00 -> x=0.999916, y=-0.001257
t=-0.50 -> x=0.252226, y=-0.042183
t= 0.00 -> x=0.000000, y=0.000000
t= 0.50 -> x=0.247774, y=0.076558
t= 1.00 -> x=1.000084, y=0.001257
Ellipse Airfoil From Spec
This example creates a normalized ellipse airfoil from a public schema object and samples a few representative coordinates.
Run it with:
uv run python examples/airfoil/create_ellipse_from_spec.py
"""Create a normalized ellipse airfoil from a schema spec."""
from __future__ import annotations
import buffalo_wings.airfoil as bwa
def main() -> None:
"""Build and sample an ellipse airfoil from its schema spec."""
spec = bwa.EllipseAirfoilSpec(
params=bwa.EllipseAirfoilParamsSpec(max_thickness=0.12)
)
airfoil = bwa.AirfoilFactory.from_spec(spec)
x, y = airfoil.xy_from_u([-1.0, -0.5, 0.0, 0.5, 1.0])
print(f"Constructed {type(airfoil).__name__} from AirfoilFactory.from_spec")
print("Sampled coordinates:")
for x_i, y_i in zip(x, y, strict=True):
print(f" x={x_i:.6f}, y={y_i:.6f}")
if __name__ == "__main__":
main()
Expected output:
Constructed EllipseAirfoil from AirfoilFactory.from_spec
Sampled coordinates:
x=1.000000, y=-0.000000
x=0.500000, y=-0.060000
x=0.000000, y=0.000000
x=0.500000, y=0.060000
x=1.000000, y=0.000000
CST Airfoil From Spec
This example creates a canonical CST airfoil from a public schema object and samples a few representative coordinates.
Because canonical CstAirfoil uses the curve parameterization x = u^2, the interior sample points appear at x = 0.25 for u = ±0.5 rather than at x = 0.5.
Run it with:
uv run python examples/airfoil/create_cst_from_spec.py
"""Create a CST airfoil from a schema spec."""
from __future__ import annotations
import buffalo_wings.airfoil as bwa
def main() -> None:
"""Build and sample a canonical CST airfoil from its schema spec."""
spec = bwa.CstAirfoilSpec(
trailing_edge_thickness=0.0025,
upper=bwa.CstSurfaceSpec(a=[0.2, 0.1, 0.0]),
lower=bwa.CstSurfaceSpec(a=[-0.1, -0.05, 0.0]),
)
airfoil = bwa.AirfoilFactory.from_spec(spec)
# Canonical CST uses the curve parameterization x = u**2.
x, y = airfoil.xy_from_u([-1.0, -0.5, 0.0, 0.5, 1.0])
print(f"Constructed {type(airfoil).__name__} from AirfoilFactory.from_spec")
print("Sampled coordinates:")
for x_i, y_i in zip(x, y, strict=True):
print(f" x={x_i:.6f}, y={y_i:.6f}")
if __name__ == "__main__":
main()
Expected output:
Constructed CstAirfoil from AirfoilFactory.from_spec
Sampled coordinates:
x=1.000000, y=-0.001250
x=0.250000, y=-0.028438
x=0.000000, y=0.000000
x=0.250000, y=0.056563
x=1.000000, y=0.001250
Canonical And Noncanonical CST Runtime Paths
This example shows how the public factory chooses between CstAirfoil and CstGeometry.
Canonical n1 = 0.5, n2 = 1.0 exponents construct CstAirfoil, while other exponent choices construct CstGeometry.
Run it with:
uv run python examples/airfoil/compare_cst_runtime_variants.py
"""Compare canonical and noncanonical CST runtime construction paths."""
from __future__ import annotations
import buffalo_wings.airfoil as bwa
def _print_airfoil(
label: str,
spec: bwa.CstAirfoilSpec,
) -> None:
"""Build one CST spec and print the resulting runtime family."""
airfoil = bwa.AirfoilFactory.from_spec(spec)
x, y = airfoil.xy_from_u([-0.5, 0.5])
print(label)
print(f" runtime={type(airfoil).__name__}")
print(f" sample x={x[0]:.6f}, y={y[0]:.6f}")
print(f" sample x={x[1]:.6f}, y={y[1]:.6f}")
def main() -> None:
"""Show how CST exponent choices affect runtime construction."""
canonical = bwa.CstAirfoilSpec(
upper=bwa.CstSurfaceSpec(a=[0.2, 0.1, 0.0]),
lower=bwa.CstSurfaceSpec(a=[-0.1, -0.05, 0.0]),
)
noncanonical = bwa.CstAirfoilSpec(
upper=bwa.CstSurfaceSpec(n1=0.6, n2=1.0, a=[0.2, 0.1, 0.0]),
lower=bwa.CstSurfaceSpec(n1=0.6, n2=1.0, a=[-0.1, -0.05, 0.0]),
)
_print_airfoil("Canonical CST exponents", canonical)
_print_airfoil("Noncanonical CST exponents", noncanonical)
if __name__ == "__main__":
main()
Expected output:
Canonical CST exponents
runtime=CstAirfoil
sample x=0.250000, y=-0.028125
sample x=0.250000, y=0.056250
Noncanonical CST exponents
runtime=CstGeometry
sample x=0.500000, y=-0.016494
sample x=0.500000, y=0.032988
Spline Airfoil From Spec
This example creates a Bezier-backed spline airfoil from a public schema object and samples a few representative coordinates.
Run it with:
uv run python examples/airfoil/create_spline_from_spec.py
"""Create a Bezier-backed spline airfoil from a schema definition."""
from __future__ import annotations
import buffalo_wings.airfoil as bwa
def main() -> None:
"""Build one spline airfoil and print a few representative values."""
spec = bwa.SplineAirfoilSpec(
upper=bwa.SplineSurfaceSpec(
control_points=[(0.0, 0.0), (0.4, 0.12), (1.0, 0.0)]
),
lower=bwa.SplineSurfaceSpec(
control_points=[(0.0, 0.0), (0.4, -0.08), (1.0, 0.0)]
),
provenance={"source_type": "cst", "exact": True},
)
airfoil = bwa.AirfoilFactory.from_spec(spec)
x_le, y_le = airfoil.xy_from_u(0.0)
x_upper_mid, y_upper_mid = airfoil.xy_from_u(0.5)
x_lower_mid, y_lower_mid = airfoil.xy_from_u(-0.5)
print(type(airfoil).__name__)
print("leading edge:", float(x_le), float(y_le))
print("upper mid:", float(x_upper_mid), float(y_upper_mid))
print("lower mid:", float(x_lower_mid), float(y_lower_mid))
if __name__ == "__main__":
main()
Expected output:
SplineAirfoil
leading edge: 0.0 0.0
upper mid: 0.45 0.06
lower mid: 0.45 -0.04
Convert Canonical CST To Spline
This example shows the exact canonical CST closure path through the public API.
It converts a canonical CstAirfoil into a runtime SplineAirfoil, exports the corresponding type: spline schema, and reconstructs the spline airfoil through AirfoilFactory.from_spec(...).
Run it with:
uv run python examples/airfoil/convert_cst_to_spline.py
"""Convert a canonical CST airfoil into an exact Bezier spline airfoil."""
from __future__ import annotations
import numpy as np
import buffalo_wings.airfoil as bwa
def main() -> None:
"""Build one canonical CST airfoil and convert it explicitly."""
cst_airfoil = bwa.CstAirfoil(
upper=bwa.CstAirfoilSide(
shape=bwa.BezierCurve1D(coefficients=[0.23, 0.14, 0.18, 0.11]),
),
lower=bwa.CstAirfoilSide(
shape=bwa.BezierCurve1D(coefficients=[-0.19, -0.08, -0.12, -0.05]),
),
trailing_edge_thickness=0.01,
)
spline_airfoil = bwa.cst_airfoil_to_spline(
cst_airfoil,
include_source_spec=True,
)
spline_spec = bwa.cst_airfoil_to_spline_spec(
cst_airfoil,
include_source_spec=True,
)
rebuilt = bwa.AirfoilFactory.from_spec(spline_spec)
x = np.linspace(0.0, 1.0, 5, dtype=np.float64)
upper_u = spline_airfoil.u_from_x(x, surface="upper")
rebuilt_upper_u = rebuilt.u_from_x(x, surface="upper")
_, y = spline_airfoil.xy_from_u(upper_u)
_, rebuilt_y = rebuilt.xy_from_u(rebuilt_upper_u)
print(type(spline_airfoil).__name__)
print("upper control points:", spline_airfoil.upper.control_points)
print("upper y(x):", y)
print("provenance:", spline_airfoil.provenance)
print("spec representation:", spline_spec.representation)
print("rebuilt class:", type(rebuilt).__name__)
print("rebuilt matches runtime:", np.allclose(rebuilt_y, y))
if __name__ == "__main__":
main()
Expected output:
SplineAirfoil
upper control points: [[0. 0. ]
[0. 0.02555556]
[0.02777778 0.05125 ]
[0.08333333 0.07113095]
[0.16666667 0.07924603]
[0.27777778 0.07488095]
[0.41666667 0.06779762]
[0.58333333 0.06597222]
[0.77777778 0.02833333]
[1. 0.005 ]]
upper y(x): [0. 0.06992187 0.05995243 0.03527874 0.005 ]
provenance: {'source_type': 'cst', 'construction': 'exact_conversion', 'exact': True, 'source_spec': {'type': 'cst', 'trailing_edge_thickness': 0.01, 'upper': {'n1': 0.5, 'n2': 1.0, 'a': [0.23, 0.14, 0.18, 0.11]}, 'lower': {'n1': 0.5, 'n2': 1.0, 'a': [-0.19, -0.08, -0.12, -0.05]}}}
spec representation: bezier
rebuilt class: SplineAirfoil
rebuilt matches runtime: True
Fit A NACA 4-Digit Airfoil To CST
This example fits a NACA 4-digit runtime airfoil to a canonical CstAirfoil and prints the fitted coefficients together with the main fit errors.
Run it with:
uv run python examples/airfoil/fit_naca4_to_cst.py
"""Fit one NACA 4-digit airfoil to a canonical CST airfoil."""
from __future__ import annotations
import buffalo_wings.airfoil as bwa
from buffalo_wings.airfoil.internal.conversion.cst_fit import (
fit_airfoil_to_cst,
)
def main() -> None:
"""Fit one NACA airfoil and print a compact summary."""
designation = "2412"
airfoil = bwa.AirfoilFactory.naca4_from_designation(designation)
fit = fit_airfoil_to_cst(
airfoil,
degree=6,
points_per_surface=101,
)
print(f"Target: NACA {designation}")
print(f"Fitted runtime: {type(fit.airfoil).__name__}")
print(f"Trailing-edge thickness: {float(fit.trailing_edge_thickness):.6e}")
print(f"Max abs error: {float(fit.max_abs_error):.6e}")
print(f"RMS error: {float(fit.rms_error):.6e}")
print("Upper coefficients:")
print([float(value) for value in fit.upper.coefficients])
print("Lower coefficients:")
print([float(value) for value in fit.lower.coefficients])
if __name__ == "__main__":
main()
Expected output:
Target: NACA 2412
Fitted runtime: CstAirfoil
Trailing-edge thickness: 2.514419e-03
Max abs error: 1.556267e-03
RMS error: 1.837731e-04
Upper coefficients:
[0.1927428684255282, 0.19573682126187048, 0.22987400554722293, 0.17934694403665646, 0.21358923013801168, 0.19706082924079915, 0.2117796340843133]
Lower coefficients:
[-0.15083670279889042, -0.11673373053275263, -0.09007659900060164, -0.10326343039472233, -0.0754090185236381, -0.07759712095119299, -0.07328578348002399]
Advanced CST Fitting Inspection Tool
For advanced CST fitting workflows that are intentionally kept under the internal conversion layer, use tools/airfoil_fitting/inspect_cst_fit.py.
That tool can inspect analytic runtime fits and point-backed fits, print the fitted coefficients and richer fit metrics, compare error display modes, and run adaptive degree search against a target tolerance.
To inspect a runtime fit with adaptive degree selection:
uv run python tools/airfoil_fitting/inspect_cst_fit.py --designation 2420 --target-tolerance 1e-3 --target-metric auto
To inspect a point-backed fit:
uv run python tools/airfoil_fitting/inspect_cst_fit.py --points-file tools/data/rae2822.dat --target-tolerance 1e-4 --target-metric auto
Airfoil Validation And Diagnostics
These examples show how to validate schema-backed airfoil definitions and inspect structured diagnostics before constructing runtime geometry.
Inspect Factory Spec Helpers
This example demonstrates the public factory helpers that support editor, CLI, and reporting workflows without requiring runtime construction.
It covers family discovery, starter specs, human-readable summaries, and exact or approximate source normalization for dual-source families such as naca16.
Run it with:
uv run python examples/airfoil/inspect_airfoil_factory_specs.py
"""Inspect factory-backed schema helpers without building geometry."""
from __future__ import annotations
import buffalo_wings.airfoil as bwa
def main() -> None:
"""Show discovery, starter-spec, summary, and switch helpers."""
family = next(
family
for family in bwa.AirfoilFactory.supported_families()
if family.type_name == "naca16"
)
starter = bwa.AirfoilFactory.default_spec("naca16", source="params")
summary = bwa.AirfoilFactory.describe_spec(starter)
choices = bwa.AirfoilFactory.exact_source_choices("naca16")
exact = bwa.AirfoilFactory.switch_source_exact(
{
"type": "naca16",
"params": {
"ideal_lift_coefficient": 0.3,
"max_thickness": 0.12,
},
},
"designation",
)
approximate = bwa.AirfoilFactory.switch_source_approx_result(
{
"type": "naca16",
"params": {
"ideal_lift_coefficient": 0.34,
"max_thickness": 0.137,
},
},
"designation",
)
print(f"Family: {family.display_name}")
print(f"Constructable: {family.constructable}")
print(f"Default source: {family.default_source}")
print(f"Exact source choices: {choices}")
print(f"Starter summary: {summary.short}")
print(f"Exact conversion: {exact}")
print(f"Approximate conversion: {approximate.spec}")
print("Approximation deltas:")
for delta in approximate.approximated_fields:
print(
f" {delta.field_name}: "
f"{delta.requested_value} -> {delta.approximated_value}"
)
if __name__ == "__main__":
main()
Expected output:
Family: NACA 16-Series
Constructable: False
Default source: designation
Exact source choices: ('designation', 'params')
Starter summary: NACA 16-212-style airfoil (explicit params)
Exact conversion: Naca16AirfoilSpec(type='naca16', designation='16-312', params=None)
Approximate conversion: Naca16AirfoilSpec(type='naca16', designation='16-314', params=None)
Approximation deltas:
ideal_lift_coefficient: 0.34 -> 0.3
max_thickness: 0.137 -> 0.14
Airfoil Schema Validation
This example validates supported, malformed, and unsupported airfoil schema objects and prints the structured diagnostics that explain each result.
Run it with:
uv run python examples/airfoil/validate_schema_airfoils.py
"""Show schema validation diagnostics across several outcomes."""
from __future__ import annotations
from typing import Any, cast
import buffalo_wings.airfoil as bwa
def _print_validation(
label: str,
spec: bwa.AirfoilDefinitionSpec,
) -> None:
"""Print one validation result."""
result = bwa.validate_spec(spec)
print(label)
print(f" is_valid={result.is_valid}")
for diagnostic in result.diagnostics.entries:
location = diagnostic.location
print(f" - {diagnostic.severity.value}: {diagnostic.code}")
print(f" {diagnostic.message}")
if location is not None:
print(
" "
f"object_path={location.object_path}, "
f"field_path={location.field_path}"
)
if location.geometry_region is not None:
print(f" geometry_region={location.geometry_region}")
def main() -> None:
"""Run the validation example."""
_print_validation(
"Supported NACA 4-digit spec",
bwa.Naca4AirfoilSpec(
params=bwa.Naca4AirfoilParamsSpec(
m=0.02,
p=0.4,
max_thickness=0.12,
)
),
)
_print_validation(
"Malformed NACA 4-digit spec",
bwa.Naca4AirfoilSpec(
params=bwa.Naca4AirfoilParamsSpec(
m=0.02,
p=0.0,
max_thickness=0.12,
)
),
)
_print_validation(
"Schema-valid but unimplemented PARSEC spec",
bwa.ParsecAirfoilSpec(
leading_edge_radius=bwa.ParsecLeadingEdgeRadiusSpec(
upper=0.005,
lower=0.005,
),
trailing_edge=bwa.ParsecTrailingEdgeSpec(
thickness=0.0025,
location=0.0,
direction_angle=0.0,
wedge_angle=8.0,
),
upper_surface_max=bwa.ParsecSurfaceExtremumSpec(
location=(0.4, 0.08),
curvature=-0.8,
),
lower_surface_min=bwa.ParsecSurfaceExtremumSpec(
location=(0.25, -0.02),
curvature=0.04,
),
),
)
_print_validation(
"Malformed and unimplemented file spec",
bwa.FileAirfoilSpec(path="", format=cast(Any, "csv")),
)
if __name__ == "__main__":
main()
Expected output:
Supported NACA 4-digit spec
is_valid=True
Malformed NACA 4-digit spec
is_valid=False
- error: airfoil.validation.naca4.p_required_for_camber
NACA 4-digit parameter p must be positive when m > 0.
object_path=airfoil, field_path=params.p
Schema-valid but unimplemented PARSEC spec
is_valid=False
- error: airfoil.support.unsupported_type
Airfoil runtime does not support ParsecAirfoilSpec yet.
object_path=airfoil, field_path=type
Malformed and unimplemented file spec
is_valid=False
- error: airfoil.validation.file.path_required
File airfoil specs require a non-empty path.
object_path=airfoil, field_path=path
- error: airfoil.validation.file.format_invalid
File airfoil format must be one of auto, selig, lednicer, surface_curve, or upper_lower.
object_path=airfoil, field_path=format
- error: airfoil.support.unsupported_type
Airfoil runtime does not support FileAirfoilSpec yet.
object_path=airfoil, field_path=type
File-Backed Airfoil Source Validation
This example validates file-backed airfoil sources across both informational and error outcomes, shows the detected input format, and demonstrates how structured diagnostics localize results to file-source paths.
Run it with:
uv run python examples/airfoil/validate_file_airfoil_source.py
"""Show file-backed airfoil validation across info and error outcomes."""
from __future__ import annotations
from pathlib import Path
from tempfile import TemporaryDirectory
import buffalo_wings.airfoil as bwa
def _print_result(
label: str,
result: bwa.AirfoilValidationResult,
) -> None:
"""Print one file-source validation result."""
print(label)
print(f" is_valid={result.is_valid}")
print(f" has_infos={result.diagnostics.has_infos}")
print(f" has_errors={result.diagnostics.has_errors}")
for diagnostic in result.diagnostics.entries:
print(f" - {diagnostic.severity.value}: {diagnostic.code}")
print(f" {diagnostic.message}")
print(f" {diagnostic.location}")
def main() -> None:
"""Run the file-source validation example."""
with TemporaryDirectory() as tmp_dir:
valid_file_path = Path(tmp_dir) / "valid.json"
valid_file_path.write_text(
"\n".join([
"{",
' "format": "upper_lower",',
' "upper": [[0.0, 0.0], [1.0, 0.0]],',
' "lower": [[0.0, 0.0], [1.0, 0.0]]',
"}",
]),
encoding="utf-8",
)
invalid_file_path = Path(tmp_dir) / "invalid.json"
invalid_file_path.write_text(
"\n".join([
"{",
' "format": "upper_lower",',
' "upper": [[0.0, 0.0]],',
' "lower": [[0.0, 0.0]]',
"}",
]),
encoding="utf-8",
)
valid_result = bwa.validate_file_airfoil_source(
bwa.FileAirfoilSpec(path=str(valid_file_path), format="auto")
)
_print_result("Auto-detected valid structured file", valid_result)
invalid_result = bwa.validate_file_airfoil_source(
bwa.FileAirfoilSpec(path=str(invalid_file_path), format="auto")
)
_print_result(
"Auto-detected invalid structured file",
invalid_result,
)
if __name__ == "__main__":
main()
Expected output:
Auto-detected valid structured file
is_valid=True
has_infos=True
has_errors=False
- info: airfoil.info.file.detected_format
Detected structured point-file format upper_lower.
DiagnosticLocation(object_path='airfoil.file_source', field_path='format', geometry_region=None)
Auto-detected invalid structured file
is_valid=False
has_infos=True
has_errors=True
- info: airfoil.info.file.detected_format
Detected structured point-file format upper_lower.
DiagnosticLocation(object_path='airfoil.file_source', field_path='format', geometry_region=None)
- error: airfoil.validation.points.upper_points_too_short
Upper-surface point definitions must contain at least 2 points.
DiagnosticLocation(object_path='airfoil.file_source', field_path='contents.upper', geometry_region=None)
- error: airfoil.validation.points.lower_points_too_short
Lower-surface point definitions must contain at least 2 points.
DiagnosticLocation(object_path='airfoil.file_source', field_path='contents.lower', geometry_region=None)
Create An Airfoil Document Spec
This example creates a top-level AirfoilDocumentSpec with multiple named airfoils and prints the document-owned names.
Use this workflow when a schema file should store several reusable airfoil definitions in one place.
Run it with:
uv run python examples/airfoil/create_airfoil_document_spec.py
"""Create a top-level airfoil schema document with named definitions."""
from __future__ import annotations
import buffalo_wings.airfoil as bwa
def main() -> None:
"""Build an airfoil document whose mapping keys own the names."""
document = bwa.AirfoilDocumentSpec(
schema_version=1,
airfoils={
"naca2412": bwa.Naca4AirfoilSpec(designation="2412"),
"ellipse": bwa.EllipseAirfoilSpec(
params=bwa.EllipseAirfoilParamsSpec(max_thickness=0.12)
),
},
curves={
"naca2412_camber": bwa.AirfoilCamberCurveSpec(airfoil="naca2412"),
"wall": bwa.SplineCurveSpec(
control_points=[(0.0, 0.0), (0.3, 0.08), (1.0, 0.0)]
),
},
)
print(f"Schema version: {document.schema_version}")
print("Named airfoils:")
for name, airfoil_definition in document.airfoils.items():
print(f" {name}: {airfoil_definition.type}")
print("Named curves:")
for name, curve_definition in document.curves.items():
if isinstance(curve_definition, bwa.AirfoilCamberCurveSpec):
print(
f" {name}: {curve_definition.type} -> "
f"{curve_definition.airfoil}"
)
continue
if isinstance(curve_definition, bwa.SplineCurveSpec):
print(
f" {name}: {curve_definition.type} -> "
f"{curve_definition.representation}"
)
continue
print(
f" {name}: {curve_definition.type} -> "
f"{len(curve_definition.points)} points"
)
if __name__ == "__main__":
main()
Expected output:
Schema version: 1
Named airfoils:
naca2412: naca4
ellipse: ellipse
Named curves:
naca2412_camber: airfoil_camber -> naca2412
wall: spline_curve -> bezier
Airfoil Schema Metadata
This example inspects the structured {"value", "label"} choice metadata
attached directly to the airfoil schema dataclass fields through
buffalo_core.schema.
Run it with:
uv run python examples/airfoil/inspect_schema_metadata_choices.py
"""Inspect structured schema metadata choices exposed by Buffalo Wings."""
from __future__ import annotations
from typing import cast
import buffalo_wings.airfoil as bwa
def _print_choices(spec_cls: type[object], field_name: str) -> None:
metadata = bwa.schema_field_metadata(spec_cls)[field_name]
choices = cast(tuple[dict[str, object], ...], metadata.get("choices", ()))
print(f"{spec_cls.__name__}.{field_name}")
for choice in choices:
print(f" value={choice['value']!r} label={choice['label']!r}")
def main() -> None:
"""Print representative structured choice metadata."""
_print_choices(bwa.Naca4AirfoilSpec, "type")
_print_choices(bwa.PointsAirfoilSpec, "format")
if __name__ == "__main__":
main()
Expected output:
Naca4AirfoilSpec.type
value='naca4' label='NACA 4-Digit'
PointsAirfoilSpec.format
value='surface_curve' label='Surface Curve'
value='upper_lower' label='Upper/Lower'
List Supported Airfoil Families
This example prints the public family-discovery metadata exposed by AirfoilFactory.supported_families().
It is useful for downstream editor or GUI workflows that need to know which families are currently constructable and which source branches they support.
Run it with:
uv run python examples/airfoil/list_supported_airfoil_families.py
"""Print the public airfoil-family discovery metadata."""
from __future__ import annotations
from buffalo_wings.airfoil import AirfoilFactory
def main() -> None:
"""Print supported-family metadata in a compact readable form."""
for family in AirfoilFactory.supported_families():
print(
family.type_name,
f"constructable={family.constructable}",
f"designation={family.supports_designation}",
f"params={family.supports_params}",
)
if __name__ == "__main__":
main()
Expected output:
naca4 constructable=True designation=True params=True
naca4_modified constructable=True designation=True params=True
naca5 constructable=True designation=True params=True
naca5_modified constructable=True designation=True params=True
naca16 constructable=False designation=True params=True
flat_plate constructable=False designation=False params=False
biconvex constructable=False designation=False params=True
biconvex_parabola constructable=False designation=False params=True
polygon constructable=False designation=False params=True
ellipse constructable=True designation=False params=True
circular_arc constructable=False designation=False params=True
joukowski constructable=False designation=False params=True
naca6 constructable=False designation=True params=True
naca6a constructable=False designation=True params=True
file constructable=False designation=False params=False
points constructable=False designation=False params=False
spline constructable=True designation=False params=False
cst constructable=True designation=False params=False
parsec constructable=False designation=False params=False
Airfoil Sampling
These examples focus on derived runtime data prepared from constructed airfoils.
Airfoil Surface Samples
This example uses the public sample_airfoil(...) API to prepare deterministic downstream surface samples and metadata.
Run it with:
uv run python examples/airfoil/sample_surface_sampling.py
"""Sample an airfoil with the public downstream surface API."""
import buffalo_wings.airfoil as bwa
def main() -> None:
"""Create a NACA 2412 airfoil and print surface-sample metadata."""
airfoil = bwa.AirfoilFactory.naca4_from_designation("2412")
query_x = 0.4
result = bwa.sample_airfoil_result(
airfoil,
num_points_per_surface=5,
spacing="cosine",
warning_policy="diagnostics_only",
)
samples = result.value
print("Airfoil: NACA 2412")
print(
"Curve parameter at x=0.4:",
airfoil.u_from_x(query_x, surface="lower")[()],
airfoil.u_from_x(query_x, surface="upper")[()],
)
print(f"Sampling warnings: {result.has_warnings}")
print(f"Diagnostic count: {len(result.diagnostics.entries)}")
print(f"Coordinate ordering: {samples.coordinate_ordering}")
print(f"Parameter ordering: {samples.parameter_ordering}")
print(f"Arc-length ordering: {samples.arc_length_ordering}")
print(f"Trailing-edge handling: {samples.trailing_edge_handling}")
print(f"Lower surface length: {samples.lower.surface_length:.6f}")
print(f"Upper surface length: {samples.upper.surface_length:.6f}")
print("Lower surface samples:")
for xi, x_value, y_value, slope_value in zip(
samples.lower.xi,
samples.lower.coordinates[:, 0],
samples.lower.coordinates[:, 1],
samples.lower.slope,
strict=True,
):
print(
f" xi={xi:.6f} -> x={x_value:.6f}, "
f"y={y_value:.6f}, dy/dx={slope_value:.6f}"
)
print("Upper surface samples:")
for xi, x_value, y_value, slope_value in zip(
samples.upper.xi,
samples.upper.coordinates[:, 0],
samples.upper.coordinates[:, 1],
samples.upper.slope,
strict=True,
):
print(
f" xi={xi:.6f} -> x={x_value:.6f}, "
f"y={y_value:.6f}, dy/dx={slope_value:.6f}"
)
if __name__ == "__main__":
main()
Expected output:
Airfoil: NACA 2412
Curve parameter at x=0.4: -0.6324555320336752 0.6324555320338004
Sampling warnings: True
Diagnostic count: 1
Coordinate ordering: surface_leading_edge_to_trailing_edge
Parameter ordering: xi_0_to_1
Arc-length ordering: surface_arc_length_0_to_length
Trailing-edge handling: preserve_surface_points
Lower surface length: 1.013370
Upper surface length: 1.028032
Lower surface samples:
xi=0.000000 -> x=0.000000, y=0.000000, dy/dx=-10.000000
xi=0.146447 -> x=0.149805, y=-0.041013, dy/dx=-0.043089
xi=0.500000 -> x=0.499412, y=-0.033493, dy/dx=0.052332
xi=0.853553 -> x=0.852541, y=-0.011510, dy/dx=0.067296
xi=1.000000 -> x=0.999916, y=-0.001257, dy/dx=0.072674
Upper surface samples:
xi=0.000000 -> x=0.000000, y=0.000000, dy/dx=-10.000000
xi=0.146447 -> x=0.143088, y=0.064941, dy/dx=0.168488
xi=0.500000 -> x=0.500588, y=0.072381, dy/dx=-0.073901
xi=0.853553 -> x=0.854565, y=0.028653, dy/dx=-0.168962
xi=1.000000 -> x=1.000084, y=0.001257, dy/dx=-0.208593
Airfoil Boundary Samples
This example uses the public sample_airfoil_boundary(...) API to prepare one ordered point distribution for downstream consumers that do not want to reconstruct a boundary loop from separate upper and lower surfaces.
Run it with:
uv run python examples/airfoil/sample_boundary_sampling.py
"""Sample an airfoil as one downstream boundary point distribution."""
import buffalo_wings.airfoil as bwa
def main() -> None:
"""Create a sharp NACA 0012 and print boundary-sample metadata."""
airfoil = bwa.AirfoilFactory.naca4_from_params(
m=0.0,
p=0.0,
t=0.12,
trailing_edge="sharp",
leading_edge_radius="exact",
)
boundary = bwa.sample_airfoil_boundary(
airfoil,
num_points_per_surface=5,
spacing="cosine",
quantities=("arc_length", "tangent", "normal", "curvature"),
warning_policy="ignore",
)
print("Airfoil: sharp NACA 0012")
print(f"Coordinate ordering: {boundary.coordinate_ordering}")
print(f"Trailing edge closed: {boundary.trailing_edge_is_closed}")
print(f"Leading-edge index: {boundary.leading_edge_index}")
print(f"Boundary point count: {boundary.coordinates.shape[0]}")
print("Boundary samples:")
for index in range(boundary.coordinates.shape[0]):
x_value = boundary.coordinates[index, 0]
y_value = boundary.coordinates[index, 1]
print(f" i={index}: x={x_value:.6f}, y={y_value:.6f}")
if __name__ == "__main__":
main()
Expected output:
Airfoil: sharp NACA 0012
Coordinate ordering: lower_to_upper
Trailing edge closed: True
Leading-edge index: 4
Boundary point count: 9
Boundary samples:
i=0: x=1.000000, y=0.000000
i=1: x=0.853553, y=-0.019066
i=2: x=0.500000, y=-0.052791
i=3: x=0.146447, y=-0.053024
i=4: x=0.000000, y=0.000000
i=5: x=0.146447, y=0.053024
i=6: x=0.500000, y=0.052791
i=7: x=0.853553, y=0.019066
i=8: x=1.000000, y=0.000000
Export Airfoil Surface Samples
This example uses the typed sampling API, converts the result to JSON, and writes a Selig coordinate file from the same public sampling surface. Use this workflow when JSON-ready output or coordinate-file output is the end product rather than an in-memory typed sampling object.
Run it with:
uv run python examples/airfoil/export_surface_sampling.py
"""Export sampled airfoil data as JSON and write a Selig file."""
from __future__ import annotations
import warnings
from pathlib import Path
from tempfile import TemporaryDirectory
import buffalo_wings.airfoil as bwa
def main() -> None:
"""Export a sampled NACA 2412 airfoil through the public API."""
airfoil = bwa.AirfoilFactory.naca4_from_designation("2412")
with warnings.catch_warnings():
warnings.simplefilter("ignore", RuntimeWarning)
samples = bwa.sample_airfoil(
airfoil,
num_points_per_surface=3,
spacing="uniform",
)
payload = bwa.airfoil_surface_samples_to_json(
samples,
include_derivatives=False,
)
with TemporaryDirectory() as temp_directory:
path = Path(temp_directory) / "naca2412.dat"
bwa.write_sampled_airfoil(
airfoil,
path=path,
num_points_per_surface=3,
spacing="uniform",
file_format="selig",
title="NACA 2412",
)
selig = path.read_text(encoding="utf-8")
print("Airfoil: NACA 2412")
print(payload)
print("Selig export:")
print(selig, end="")
if __name__ == "__main__":
main()
Expected output:
Airfoil: NACA 2412
{
"chord_length": 1.0,
"diagnostics": {
"entries": [
{
"code": "airfoil.surface_sampling.slope_singularity",
"location": {
"field_path": "slope",
"geometry_region": "leading_edge",
"object_path": "airfoil.sample"
},
"message": "Surface samples report slope as dy/dx. Airfoil surfaces can contain singular slope locations where the reported slope will appear as inf, -inf, or nan. This commonly occurs near the leading edge. For cambered airfoils, the upper-surface singular slope can occur at an x-location less than 0.",
"severity": "warning"
}
],
"has_errors": false,
"has_infos": false,
"has_warnings": true
},
"leading_edge": [
0.0,
0.0
],
"lower": {
"arc_length": [
0.0,
0.5118230792395474,
1.013370215524385
],
"coordinates": [
[
0.0,
0.0
],
[
0.4994118112845964,
-0.03349253994189072
],
[
0.99991618604674,
-0.0012572092988993215
]
],
"curve_u": [
-0.0,
-0.7071067811865476,
-1.0
],
"surface_length": 1.013370215524385,
"xi": [
0.0,
0.5,
1.0
]
},
"trailing_edge_lower": [
0.99991618604674,
-0.0012572092988993215
],
"trailing_edge_midpoint": [
1.0,
0.0
],
"trailing_edge_upper": [
1.00008381395326,
0.0012572092988993215
],
"upper": {
"arc_length": [
0.0,
0.5231536663541477,
1.0280315375499638
],
"coordinates": [
[
0.0,
0.0
],
[
0.5005881887154038,
0.07238142883077961
],
[
1.00008381395326,
0.0012572092988993215
]
],
"curve_u": [
0.0,
0.7071067811865476,
1.0
],
"surface_length": 1.0280315375499638,
"xi": [
0.0,
0.5,
1.0
]
}
}
Selig export:
NACA 2412
1.00008381395326 0.001257209298899322
0.5005881887154038 0.07238142883077961
0 0
0.4994118112845964 -0.03349253994189072
0.99991618604674 -0.001257209298899322
Plot One Or Two Airfoils
This example gives a lightweight visual check for one or two airfoils without stepping up to a GUI workflow.
It can compare simple NACA 4-digit inputs directly or compare named airfoils loaded from a schema document, including CST and spline-backed definitions.
It overlays the sampled shapes with matplotlib, can save a PNG for headless use, and can also open an interactive window for quick inspection.
Before running it, install the optional example dependencies:
uv sync --extra examples
Run it with:
uv run python examples/airfoil/plot_airfoils.py --airfoil 2412 --airfoil 0012
To compare named airfoils from a schema document:
uv run python examples/airfoil/plot_airfoils.py --document examples/airfoil/compare.yaml --airfoil naca_ref --airfoil cst_fit
To save a file instead of opening a window:
uv run python examples/airfoil/plot_airfoils.py --airfoil 2412 --airfoil 0012 --output /tmp/airfoils.png --no-show
Representative output:

"""Plot one or two schema-backed airfoils for quick visual inspection."""
from __future__ import annotations
import argparse
import warnings
from collections.abc import Mapping, Sequence
from pathlib import Path
from typing import Any, cast
import numpy as np
import yaml # type: ignore[import-untyped]
import buffalo_wings.airfoil as bwa
DEFAULT_DESIGNATIONS = ("2412", "0012")
DEFAULT_POINTS_PER_SURFACE = 200
MAX_AIRFOIL_COUNT = 2
MIN_AIRFOIL_COUNT = 1
MIN_POINTS_PER_SURFACE = 2
POINT_COORDINATE_COUNT = 2
def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace:
"""Parse command-line options for the plotting example."""
parser = argparse.ArgumentParser(
description=("Overlay one or two airfoils for quick visual inspection.")
)
parser.add_argument(
"--document",
type=Path,
default=None,
help=(
"Optional YAML airfoil document. When provided, use "
"--airfoil to select one or two named entries."
),
)
parser.add_argument(
"--airfoil",
dest="airfoil_names",
action="append",
default=None,
help=(
"Airfoil name from --document, or a NACA 4-digit designation "
"when --document is omitted. Repeat once to compare two."
),
)
parser.add_argument(
"--output",
type=Path,
default=None,
help="Optional PNG path to save instead of only showing the figure.",
)
parser.add_argument(
"--points-per-surface",
type=int,
default=DEFAULT_POINTS_PER_SURFACE,
help="Number of sample points per surface side.",
)
parser.add_argument(
"--no-show",
action="store_true",
help="Do not open an interactive window.",
)
args = parser.parse_args(argv)
names = tuple(args.airfoil_names or DEFAULT_DESIGNATIONS)
if not MIN_AIRFOIL_COUNT <= len(names) <= MAX_AIRFOIL_COUNT:
msg = "Provide one or two --airfoil values."
raise ValueError(msg)
if args.points_per_surface < MIN_POINTS_PER_SURFACE:
msg = "--points-per-surface must be at least 2."
raise ValueError(msg)
args.airfoil_names = names
return args
def _load_pyplot(*, use_agg: bool) -> Any:
"""Import matplotlib lazily so the example stays optional."""
import matplotlib
if use_agg:
matplotlib.use("Agg")
import matplotlib.pyplot as plt
return plt
def _require_mapping(value: object, *, name: str) -> Mapping[str, object]:
"""Require one YAML value to be a string-keyed mapping."""
if not isinstance(value, Mapping):
msg = f"{name} must be a mapping."
raise ValueError(msg)
mapping_obj = cast(Mapping[object, object], value)
if not all(isinstance(key, str) for key in mapping_obj):
msg = f"{name} keys must be strings."
raise ValueError(msg)
return cast(Mapping[str, object], mapping_obj)
def _require_sequence(
payload: object,
*,
name: str,
) -> Sequence[object]:
"""Require one YAML value to be a non-string sequence."""
if not isinstance(payload, Sequence) or isinstance(payload, str | bytes):
msg = f"{name} must be a sequence."
raise ValueError(msg)
return cast(Sequence[object], payload)
def _float_value(value: object, *, name: str) -> float:
"""Convert one YAML scalar into a floating-point value."""
if isinstance(value, bool):
msg = f"{name} must be numeric."
raise ValueError(msg)
if not isinstance(value, int | float | str):
msg = f"{name} must be numeric."
raise ValueError(msg)
return float(value)
def _int_value(value: object, *, name: str) -> int:
"""Convert one YAML scalar into an integer value."""
if isinstance(value, bool):
msg = f"{name} must be an integer."
raise ValueError(msg)
if not isinstance(value, int | str):
msg = f"{name} must be an integer."
raise ValueError(msg)
return int(value)
def _str_value(value: object, *, name: str) -> str:
"""Convert one YAML scalar into a string value."""
if not isinstance(value, str):
msg = f"{name} must be a string."
raise ValueError(msg)
return value
def _float_list(
payload: object,
*,
name: str,
) -> list[float]:
"""Convert one sequence of numeric values into a float list."""
sequence = _require_sequence(payload, name=name)
return [_float_value(value, name=f"{name}[]") for value in sequence]
def _point_list(
payload: object,
*,
name: str,
) -> list[bwa.Point2DSpec]:
"""Convert one YAML point list into typed 2D point tuples."""
points: list[bwa.Point2DSpec] = []
for index, point in enumerate(_require_sequence(payload, name=name)):
point_values = _require_sequence(point, name=f"{name}[{index}]")
if len(point_values) != POINT_COORDINATE_COUNT:
msg = f"{name}[{index}] must contain exactly 2 values."
raise ValueError(msg)
points.append((
_float_value(point_values[0], name=f"{name}[{index}][0]"),
_float_value(point_values[1], name=f"{name}[{index}][1]"),
))
return points
def _parse_cst_surface(
payload: object,
*,
name: str,
) -> bwa.CstSurfaceSpec:
"""Build one CST surface spec from a YAML mapping."""
mapping = _require_mapping(payload, name=name)
coefficients = mapping.get("a", mapping.get("coefficients", ()))
return bwa.CstSurfaceSpec(
n1=_float_value(mapping.get("n1", 0.5), name=f"{name}.n1"),
n2=_float_value(mapping.get("n2", 1.0), name=f"{name}.n2"),
a=_float_list(coefficients, name=f"{name}.a"),
)
def _parse_spline_surface(
payload: object,
*,
name: str,
) -> bwa.SplineSurfaceSpec:
"""Build one spline surface spec from a YAML mapping."""
mapping = _require_mapping(payload, name=name)
return bwa.SplineSurfaceSpec(
control_points=_point_list(
mapping.get("control_points", ()),
name=f"{name}.control_points",
)
)
def _parse_airfoil_spec(payload: object) -> bwa.AirfoilDefinitionSpec:
"""Build one supported airfoil schema object from YAML data."""
mapping = _require_mapping(payload, name="airfoil definition")
airfoil_type = mapping.get("type")
if not isinstance(airfoil_type, str):
msg = "airfoil definition type must be a string."
raise ValueError(msg)
if airfoil_type == "naca4":
return bwa.Naca4AirfoilSpec(
designation=(
_str_value(mapping["designation"], name="designation")
if "designation" in mapping
else None
)
)
if airfoil_type == "naca4_modified":
return bwa.Naca4ModifiedAirfoilSpec(
designation=(
_str_value(mapping["designation"], name="designation")
if "designation" in mapping
else None
)
)
if airfoil_type == "naca5":
return bwa.Naca5AirfoilSpec(
designation=(
_str_value(mapping["designation"], name="designation")
if "designation" in mapping
else None
)
)
if airfoil_type == "naca5_modified":
return bwa.Naca5ModifiedAirfoilSpec(
designation=(
_str_value(mapping["designation"], name="designation")
if "designation" in mapping
else None
)
)
if airfoil_type == "ellipse":
params = _require_mapping(
mapping.get("params", {}),
name="ellipse.params",
)
return bwa.EllipseAirfoilSpec(
params=bwa.EllipseAirfoilParamsSpec(
max_thickness=_float_value(
params.get("max_thickness", 0.0),
name="ellipse.params.max_thickness",
)
)
)
if airfoil_type == "cst":
return bwa.CstAirfoilSpec(
trailing_edge_thickness=_float_value(
mapping.get("trailing_edge_thickness", 0.0),
name="trailing_edge_thickness",
),
upper=_parse_cst_surface(mapping.get("upper", {}), name="upper"),
lower=_parse_cst_surface(mapping.get("lower", {}), name="lower"),
)
if airfoil_type == "spline":
representation = mapping.get("representation", "bezier")
if representation != "bezier":
msg = "plot-document spline representation must be 'bezier'."
raise ValueError(msg)
return bwa.SplineAirfoilSpec(
representation="bezier",
upper=_parse_spline_surface(mapping.get("upper", {}), name="upper"),
lower=_parse_spline_surface(mapping.get("lower", {}), name="lower"),
provenance=(
dict(_require_mapping(mapping["provenance"], name="provenance"))
if "provenance" in mapping
else None
),
)
msg = f"Unsupported plot-document airfoil type: {airfoil_type}"
raise ValueError(msg)
def _load_document(path: Path) -> bwa.AirfoilDocumentSpec:
"""Load one YAML airfoil document for plotting."""
payload = yaml.safe_load(path.read_text(encoding="utf-8"))
mapping = _require_mapping(payload, name="document")
airfoils_payload = _require_mapping(
mapping.get("airfoils", {}),
name="document.airfoils",
)
return bwa.AirfoilDocumentSpec(
schema_version=_int_value(
mapping.get("schema_version", 1),
name="schema_version",
),
airfoils={
name: _parse_airfoil_spec(spec_payload)
for name, spec_payload in airfoils_payload.items()
},
)
def _build_airfoils_from_document(
document: bwa.AirfoilDocumentSpec,
*,
names: Sequence[str],
) -> list[tuple[str, bwa.Airfoil]]:
"""Build selected named airfoils from one schema document."""
built_airfoils: list[tuple[str, bwa.Airfoil]] = []
for name in names:
try:
spec = document.airfoils[name]
except KeyError as exc:
msg = f"Unknown airfoil name in document: {name}"
raise KeyError(msg) from exc
validation = bwa.validate_spec(spec)
if not validation.is_valid:
msg = f"Document airfoil {name!r} failed validation."
raise ValueError(msg)
built_airfoils.append((name, bwa.AirfoilFactory.from_spec(spec)))
return built_airfoils
def _build_airfoils(
*,
document_path: Path | None,
names: Sequence[str],
) -> list[tuple[str, bwa.Airfoil]]:
"""Build one or two airfoils from either a document or NACA names."""
if document_path is None:
return [
(
f"NACA {designation}",
bwa.AirfoilFactory.naca4_from_designation(designation),
)
for designation in names
]
document = _load_document(document_path)
return _build_airfoils_from_document(document, names=names)
def _surface_outline(
airfoil: bwa.Airfoil,
*,
points_per_surface: int,
) -> tuple[np.ndarray[Any, np.dtype[np.float64]], ...]:
"""
Return a full airfoil outline ordered around the surface.
Notes
-----
This visualizer only needs coordinates, so it samples the airfoil curve
curve directly rather than calling ``sample_airfoil(...)``.
That avoids unnecessary slope, curvature, and arc-length evaluation near
leading-edge singularities for shapes such as canonical CST airfoils.
"""
theta = np.linspace(0.0, np.pi, points_per_surface, dtype=np.float64)
xi = 0.5 * (1.0 - np.cos(theta))
u_lower = -xi[::-1]
u_upper = xi[1:]
u_outline = np.concatenate((u_lower, u_upper))
with warnings.catch_warnings():
warnings.simplefilter("error", RuntimeWarning)
x_values, y_values = airfoil.xy_from_u(u_outline)
outline = np.column_stack((x_values, y_values))
return outline[:, 0], outline[:, 1]
def _display_path(path: Path) -> str:
"""Format one output path relative to the current working directory."""
resolved = path.resolve()
cwd = Path.cwd().resolve()
try:
return str(resolved.relative_to(cwd))
except ValueError:
return str(resolved)
def main(argv: Sequence[str] | None = None) -> None:
"""Plot one or two airfoils using the public airfoil API."""
args = parse_args(argv)
plt = _load_pyplot(use_agg=args.no_show)
fig, ax = plt.subplots(figsize=(10.0, 4.0))
airfoils = _build_airfoils(
document_path=args.document,
names=args.airfoil_names,
)
max_abs_y = 0.0
for label, airfoil in airfoils:
x_values, y_values = _surface_outline(
airfoil,
points_per_surface=args.points_per_surface,
)
max_abs_y = max(max_abs_y, float(np.max(np.abs(y_values))))
ax.plot(x_values, y_values, linewidth=2.0, label=label)
y_margin = max(0.02, 0.15 * max_abs_y)
ax.set_xlim(-0.02, 1.02)
ax.set_ylim(-max_abs_y - y_margin, max_abs_y + y_margin)
ax.set_aspect("equal", adjustable="box")
ax.set_xlabel("x / chord")
ax.set_ylabel("y / chord")
ax.set_title("Airfoil Shape Comparison")
ax.grid(True)
ax.legend()
fig.tight_layout()
print("Plotting airfoils:")
for label, _ in airfoils:
print(f" - {label}")
if args.output is not None:
args.output.parent.mkdir(parents=True, exist_ok=True)
fig.savefig(args.output, dpi=200, bbox_inches="tight")
print(f"Saved figure to: {_display_path(args.output)}")
if args.no_show:
plt.close(fig)
return
print("Close the plot window to exit.")
plt.show()
if __name__ == "__main__":
main()
Wing Examples
These examples introduce the current public wing workflow surface.
Rectangular Wing
This example builds a simple one-panel rectangular wing through the public wing schema and evaluates one span station and section.
Run it with:
uv run python examples/wing/create_rectangular_wing.py
"""Create and inspect a simple rectangular wing from a public spec."""
import numpy as np
import buffalo_wings.airfoil as bwa
import buffalo_wings.wing as bww
def main() -> None:
"""Build a one-panel rectangular wing and print sampled outputs."""
zero_offset = bww.PiecewiseLinearDistribution(
data=[(0.0, 0.0), (1.0, 0.0)],
)
constant_chord = bww.PiecewiseLinearDistribution(
data=[(0.0, 1.5), (1.0, 1.5)],
)
zero_twist = bww.PiecewiseLinearDistribution(
data=[(0.0, 0.0), (1.0, 0.0)],
)
wing = bww.WingDefinitionSpec(
symmetry="mirror_y",
half_span=5.0,
reference_axis="quarter_chord",
twist_axis="quarter_chord",
panels=[
bww.PanelSpec(
id="P0",
eta_range=(0.0, 1.0),
ref_line=bww.RefLineSpec(
x_ref=zero_offset,
z_ref=zero_offset,
),
chord=constant_chord,
twist=zero_twist,
airfoil=bww.SingleAirfoilRef(name="root_section"),
)
],
)
spec = bww.WingSpec(
schema_version=2,
units=bww.UnitsSpec(length="m", angle="deg"),
wing=wing,
airfoils={
"root_section": bwa.Naca4AirfoilSpec(designation="0012"),
},
)
canonical = bww.WingCanonical.from_spec(spec)
station = canonical.evaluate_panel("P0", 0.5)
chordwise_samples = np.linspace(0.0, 1.0, 5, dtype=np.float64)
section = canonical.section_curves(
"P0",
eta=0.5,
chordwise_samples=chordwise_samples,
)
span_stations = canonical.sample_span_stations("P0", 5, spacing="uniform")
print("Rectangular wing example")
print(f"Panel: {station.panel_id}")
print(f"Half span: {spec.wing.half_span:.3f} {spec.units.length}")
print(f"Station eta: {station.eta:.3f}")
print(f"Station y: {station.y:.3f} {spec.units.length}")
print(f"Station chord: {station.chord:.3f} {spec.units.length}")
print(f"Station twist: {station.twist_rad:.6f} rad")
print("Uniform eta stations:", span_stations)
print("Upper surface sample points at eta=0.5:")
for point in section.upper_curve:
print(f" ({point[0]:.6f}, {point[1]:.6f}, {point[2]:.6f})")
if __name__ == "__main__":
main()
Expected output:
Rectangular wing example
Panel: P0
Half span: 5.000 m
Station eta: 0.500
Station y: 2.500 m
Station chord: 1.500 m
Station twist: 0.000000 rad
Uniform eta stations: [0. 0.25 0.5 0.75 1. ]
Upper surface sample points at eta=0.5:
(-0.375000, 2.500000, 0.000000)
(-0.000000, 2.500000, 0.089119)
(0.375000, 2.500000, 0.079410)
(0.750000, 2.500000, 0.047405)
(1.125000, 2.500000, 0.001890)
Wing Schema GUI Prototype
This example is a refactored scaffold for a future desktop GUI application. It demonstrates a schema edit, validation, undo/redo, debounced rebuild, and PyVista viewport loop in a compact prototype script. Before running it, install optional example dependencies:
uv sync --extra examples
Run it with:
uv run python examples/wing/wing_schema_gui_prototype.py
On Linux, this prototype defaults to xcb for better Qt/VTK stability.
If you need to override it, use --qt-platform xcb or --qt-platform wayland.
You can also use --no-viewport to run the schema editor without PyVista.
"""Prototype wing schema GUI starter for future desktop application work."""
from examples.wing.wing_schema_gui_prototype_app import main
if __name__ == "__main__":
main()