Examples
Start with the smallest public examples first. They show the intended import style and the current user-facing entry points.
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(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(...).
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(t)
print("Spec:")
print(spec)
print(f"Surface length: {airfoil.surface_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
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.
This example is intentionally a scaffold, not production application code.
It preserves the core edit-validate-rebuild-render loop while keeping the
implementation small enough to evolve into a dedicated app package.
"""
# pyright: reportUnknownVariableType=false
# pyright: reportUnknownArgumentType=false
# pyright: reportUnknownMemberType=false
# pyright: reportUnknownLambdaType=false
from __future__ import annotations
import argparse
import importlib
import math
import os
import sys
from collections.abc import Callable
from dataclasses import dataclass
from typing import Any
import numpy as np
import numpy.typing as npt
from pydantic import BaseModel, Field
_FloatArray = npt.NDArray[np.float64]
_IntArray = npt.NDArray[np.int64]
class _WingStation(BaseModel):
"""One spanwise station in a simple wing model."""
name: str = "ST"
eta: float = Field(ge=0.0, le=1.0)
chord: float = Field(gt=0.0)
twist_deg: float = 0.0
class _WingPlanform(BaseModel):
"""Simple planform controls used by this prototype."""
span: float = Field(gt=0.0)
sweep_deg: float = 0.0
dihedral_deg: float = 0.0
class _WingModel(BaseModel):
"""Top-level wing model for this prototype."""
planform: _WingPlanform
sections: list[_WingStation]
class _AppSchema(BaseModel):
"""Schema root for this prototype."""
wing: _WingModel
@dataclass(frozen=True)
class _DerivedGeometry:
"""Derived preview geometry for rendering."""
station_lines: tuple[_FloatArray, ...]
points: _FloatArray
faces: _IntArray
@dataclass
class _GuiContext:
"""Mutable GUI runtime state used by this scaffold."""
window: Any
qt_widgets: Any
qt_core: Any
qt_gui: Any
pyvista: Any | None
viewport: Any | None
schema: _AppSchema
undo_stack: list[_AppSchema]
redo_stack: list[_AppSchema]
status_label: Any
rebuild_timer: Any
rebuild_delay_ms: int
def _default_schema() -> _AppSchema:
"""Return a default schema used by the GUI."""
return _AppSchema(
wing=_WingModel(
planform=_WingPlanform(
span=10.0,
sweep_deg=20.0,
dihedral_deg=5.0,
),
sections=[
_WingStation(name="Root", eta=0.0, chord=2.0, twist_deg=2.0),
_WingStation(name="Mid", eta=0.5, chord=1.4, twist_deg=0.0),
_WingStation(name="Tip", eta=1.0, chord=0.8, twist_deg=-2.0),
],
),
)
def _build_preview_geometry(schema: _AppSchema) -> _DerivedGeometry:
"""Build a coarse preview mesh from schema values."""
wing = schema.wing
sections = sorted(wing.sections, key=lambda section: section.eta)
half_span = 0.5 * wing.planform.span
sweep_rad = math.radians(wing.planform.sweep_deg)
dihedral_rad = math.radians(wing.planform.dihedral_deg)
line_list: list[_FloatArray] = []
endpoints: list[tuple[_FloatArray, _FloatArray]] = []
for section in sections:
y_coord = half_span * section.eta
x_le = y_coord * math.tan(sweep_rad)
z_coord = y_coord * math.tan(dihedral_rad)
leading_edge = np.array([x_le, y_coord, z_coord], dtype=np.float64)
trailing_edge = np.array(
[x_le + section.chord, y_coord, z_coord],
dtype=np.float64,
)
line_list.append(np.vstack((leading_edge, trailing_edge)))
endpoints.append((leading_edge, trailing_edge))
points_list: list[_FloatArray] = []
for leading_edge, trailing_edge in endpoints:
points_list.append(leading_edge)
points_list.append(trailing_edge)
points = np.asarray(points_list, dtype=np.float64)
faces_list: list[list[int]] = []
for index in range(len(sections) - 1):
le0 = 2 * index
te0 = le0 + 1
le1 = 2 * (index + 1)
te1 = le1 + 1
faces_list.append([3, le0, te0, te1])
faces_list.append([3, le0, te1, le1])
faces = np.asarray(faces_list, dtype=np.int64).ravel()
return _DerivedGeometry(
station_lines=tuple(line_list),
points=points,
faces=faces,
)
def _load_optional_gui_module(module_name: str) -> Any:
"""Import one optional GUI module by name."""
try:
return importlib.import_module(module_name)
except ModuleNotFoundError as exc:
message = (
"This GUI example needs optional dependencies.\n"
"Install extras: uv sync --extra examples\n"
"Or run once with extras: uv run --extra examples "
"python examples/wing/wing_schema_gui_prototype.py\n"
f"Missing module: {module_name}"
)
raise RuntimeError(message) from exc
def _configure_linux_qt_environment(platform_override: str | None) -> None:
"""Set conservative Qt/GL defaults for Linux desktop stability."""
if not sys.platform.startswith("linux"):
return
if platform_override is not None:
os.environ["QT_QPA_PLATFORM"] = platform_override
if "QT_QPA_PLATFORM" not in os.environ:
# Prefer xcb for this prototype because Qt+VTK/pyvistaqt is more
# reliable there than Wayland on some Linux desktops.
os.environ["QT_QPA_PLATFORM"] = "xcb"
os.environ.setdefault("QT_XCB_GL_INTEGRATION", "none")
os.environ.setdefault("LIBGL_ALWAYS_SOFTWARE", "1")
def _render(ctx: _GuiContext) -> None:
"""Render the current preview geometry in the viewport."""
if ctx.pyvista is None or ctx.viewport is None:
ctx.status_label.setText("Preview rebuilt (viewport disabled)")
return
geom = _build_preview_geometry(ctx.schema)
mesh = ctx.pyvista.PolyData(geom.points, geom.faces)
ctx.viewport.clear()
ctx.viewport.add_mesh(mesh, opacity=0.55)
for line in geom.station_lines:
line_poly = ctx.pyvista.lines_from_points(line)
ctx.viewport.add_mesh(line_poly, line_width=3)
ctx.viewport.add_axes()
ctx.viewport.reset_camera()
ctx.status_label.setText("Preview rebuilt")
def _schedule_rebuild(ctx: _GuiContext) -> None:
"""Start a debounced preview rebuild timer."""
ctx.rebuild_timer.start(ctx.rebuild_delay_ms)
def _apply_update(
ctx: _GuiContext,
mutator: Callable[[_AppSchema], None],
) -> None:
"""Apply one schema update with undo tracking and validation."""
ctx.undo_stack.append(ctx.schema.model_copy(deep=True))
ctx.redo_stack.clear()
updated = ctx.schema.model_copy(deep=True)
mutator(updated)
ctx.schema = _AppSchema.model_validate(updated.model_dump())
ctx.status_label.setText("Schema changed (debounced rebuild)...")
_schedule_rebuild(ctx)
def _make_planform_dock(ctx: _GuiContext) -> Any:
"""Build the planform controls dock."""
dock = ctx.qt_widgets.QDockWidget("Planform", ctx.window)
panel = ctx.qt_widgets.QWidget()
form = ctx.qt_widgets.QFormLayout(panel)
span_box = ctx.qt_widgets.QDoubleSpinBox()
span_box.setRange(0.01, 1e6)
span_box.setDecimals(4)
span_box.setValue(ctx.schema.wing.planform.span)
span_box.valueChanged.connect(
lambda value: _apply_update(
ctx,
lambda model: setattr(model.wing.planform, "span", float(value)),
)
)
form.addRow("Span", span_box)
sweep_box = ctx.qt_widgets.QDoubleSpinBox()
sweep_box.setRange(-89.0, 89.0)
sweep_box.setDecimals(3)
sweep_box.setValue(ctx.schema.wing.planform.sweep_deg)
sweep_box.valueChanged.connect(
lambda value: _apply_update(
ctx,
lambda model: setattr(
model.wing.planform,
"sweep_deg",
float(value),
),
)
)
form.addRow("Sweep (deg)", sweep_box)
dihedral_box = ctx.qt_widgets.QDoubleSpinBox()
dihedral_box.setRange(-45.0, 45.0)
dihedral_box.setDecimals(3)
dihedral_box.setValue(ctx.schema.wing.planform.dihedral_deg)
dihedral_box.valueChanged.connect(
lambda value: _apply_update(
ctx,
lambda model: setattr(
model.wing.planform,
"dihedral_deg",
float(value),
),
)
)
form.addRow("Dihedral (deg)", dihedral_box)
dock.setWidget(panel)
return dock
def _make_stations_dock(ctx: _GuiContext) -> Any:
"""Build the stations controls dock."""
dock = ctx.qt_widgets.QDockWidget("Stations", ctx.window)
scroll = ctx.qt_widgets.QScrollArea()
body = ctx.qt_widgets.QWidget()
layout = ctx.qt_widgets.QVBoxLayout(body)
for index, section in enumerate(ctx.schema.wing.sections):
group = ctx.qt_widgets.QGroupBox(f"Station {index}: {section.name}")
form = ctx.qt_widgets.QFormLayout(group)
eta_box = ctx.qt_widgets.QDoubleSpinBox()
eta_box.setRange(0.0, 1.0)
eta_box.setDecimals(4)
eta_box.setSingleStep(0.01)
eta_box.setValue(section.eta)
eta_box.valueChanged.connect(
lambda value, i=index: _apply_update(
ctx,
lambda model: setattr(
model.wing.sections[i],
"eta",
float(value),
),
)
)
form.addRow("Eta", eta_box)
chord_box = ctx.qt_widgets.QDoubleSpinBox()
chord_box.setRange(0.001, 1e6)
chord_box.setDecimals(4)
chord_box.setValue(section.chord)
chord_box.valueChanged.connect(
lambda value, i=index: _apply_update(
ctx,
lambda model: setattr(
model.wing.sections[i],
"chord",
float(value),
),
)
)
form.addRow("Chord", chord_box)
twist_box = ctx.qt_widgets.QDoubleSpinBox()
twist_box.setRange(-90.0, 90.0)
twist_box.setDecimals(3)
twist_box.setValue(section.twist_deg)
twist_box.valueChanged.connect(
lambda value, i=index: _apply_update(
ctx,
lambda model: setattr(
model.wing.sections[i],
"twist_deg",
float(value),
),
)
)
form.addRow("Twist (deg)", twist_box)
layout.addWidget(group)
layout.addStretch(1)
scroll.setWidget(body)
scroll.setWidgetResizable(True)
dock.setWidget(scroll)
return dock
def _undo(ctx: _GuiContext) -> None:
"""Restore one schema snapshot from undo history."""
if not ctx.undo_stack:
return
ctx.redo_stack.append(ctx.schema.model_copy(deep=True))
ctx.schema = ctx.undo_stack.pop()
ctx.status_label.setText("Undo")
_schedule_rebuild(ctx)
def _redo(ctx: _GuiContext) -> None:
"""Restore one schema snapshot from redo history."""
if not ctx.redo_stack:
return
ctx.undo_stack.append(ctx.schema.model_copy(deep=True))
ctx.schema = ctx.redo_stack.pop()
ctx.status_label.setText("Redo")
_schedule_rebuild(ctx)
def _make_status_dock(ctx: _GuiContext) -> Any:
"""Build the bottom status dock."""
status_dock = ctx.qt_widgets.QDockWidget("Messages", ctx.window)
status_panel = ctx.qt_widgets.QWidget()
status_layout = ctx.qt_widgets.QHBoxLayout(status_panel)
status_layout.addWidget(ctx.status_label)
status_layout.addStretch(1)
rebuild_button = ctx.qt_widgets.QPushButton("Rebuild Now")
rebuild_button.clicked.connect(lambda: _render(ctx))
status_layout.addWidget(rebuild_button)
status_dock.setWidget(status_panel)
return status_dock
def _wire_menu(ctx: _GuiContext) -> None:
"""Create menu actions for undo and redo."""
edit_menu = ctx.window.menuBar().addMenu("&Edit")
undo_action = ctx.qt_gui.QAction("Undo", ctx.window)
undo_action.triggered.connect(lambda: _undo(ctx))
edit_menu.addAction(undo_action)
redo_action = ctx.qt_gui.QAction("Redo", ctx.window)
redo_action.triggered.connect(lambda: _redo(ctx))
edit_menu.addAction(redo_action)
def _parse_args() -> argparse.Namespace:
"""Parse command-line options for this prototype."""
parser = argparse.ArgumentParser(
description="Wing schema GUI prototype scaffold.",
)
parser.add_argument(
"--no-viewport",
action="store_true",
help="Run GUI controls without initializing PyVista/VTK viewport.",
)
parser.add_argument(
"--qt-platform",
choices=("wayland", "xcb"),
default=None,
help="Override QT_QPA_PLATFORM for backend troubleshooting.",
)
return parser.parse_args()
def main() -> None:
"""Run the wing schema GUI prototype."""
args = _parse_args()
_configure_linux_qt_environment(args.qt_platform)
qt_widgets = _load_optional_gui_module("PySide6.QtWidgets")
qt_core = _load_optional_gui_module("PySide6.QtCore")
qt_gui = _load_optional_gui_module("PySide6.QtGui")
pyvista: Any | None = None
pyvistaqt: Any | None = None
if not args.no_viewport:
pyvista = _load_optional_gui_module("pyvista")
pyvistaqt = _load_optional_gui_module("pyvistaqt")
qt_core.QCoreApplication.setAttribute(qt_core.Qt.AA_ShareOpenGLContexts)
if hasattr(qt_core.Qt, "AA_UseSoftwareOpenGL"):
qt_core.QCoreApplication.setAttribute(qt_core.Qt.AA_UseSoftwareOpenGL)
app = qt_widgets.QApplication(sys.argv)
window = qt_widgets.QMainWindow()
window.setWindowTitle("Wing Schema GUI Prototype")
window.resize(1280, 840)
central = qt_widgets.QWidget(window)
central_layout = qt_widgets.QVBoxLayout(central)
central_layout.setContentsMargins(0, 0, 0, 0)
viewport: Any | None
if pyvistaqt is None:
viewport = None
placeholder = qt_widgets.QLabel(
"Viewport disabled.\n"
"Run without --no-viewport after Qt/VTK backend is stable.",
)
placeholder.setAlignment(qt_core.Qt.AlignCenter)
central_layout.addWidget(placeholder)
else:
viewport = pyvistaqt.QtInteractor(central)
central_layout.addWidget(viewport)
window.setCentralWidget(central)
ctx = _GuiContext(
window=window,
qt_widgets=qt_widgets,
qt_core=qt_core,
qt_gui=qt_gui,
pyvista=pyvista,
viewport=viewport,
schema=_default_schema(),
undo_stack=[],
redo_stack=[],
status_label=qt_widgets.QLabel("Ready"),
rebuild_timer=qt_core.QTimer(window),
rebuild_delay_ms=250,
)
ctx.rebuild_timer.setSingleShot(True)
ctx.rebuild_timer.timeout.connect(lambda: _render(ctx))
_wire_menu(ctx)
window.addDockWidget(
qt_core.Qt.LeftDockWidgetArea,
_make_planform_dock(ctx),
)
window.addDockWidget(
qt_core.Qt.RightDockWidgetArea,
_make_stations_dock(ctx),
)
window.addDockWidget(
qt_core.Qt.BottomDockWidgetArea,
_make_status_dock(ctx),
)
window.show()
# Render only after the native window is realized to avoid
# X11/VTK BadWindow errors on some Linux setups.
qt_core.QTimer.singleShot(0, lambda: _render(ctx))
sys.exit(app.exec())
if __name__ == "__main__":
main()