"""Default GUI case templates and geometry-preview helpers."""
from __future__ import annotations
from collections.abc import Mapping, Sequence
from dataclasses import fields, is_dataclass
from typing import Any, cast
import buffalo_wings.airfoil as bwa
import numpy as np
import numpy.typing as npt
from buffalo_panel.config import AirfoilBody2DSpec, PanelCaseSpec
[docs]
def build_initial_case_mapping() -> dict[str, Any]:
"""Return the default case mapping used to seed the GUI."""
return {
"schema_version": 1,
"units": {"length": "m", "angle": "deg"},
"case": {
"name": "GUI Case",
"description": "Interactive case defined in the GUI",
},
"solver": {
"formulation": "hess_smith",
"backend": "python",
},
"freestream": {
"speed": 1.0,
"alpha": 2.0,
"mach": 0.0,
"reynolds": 1000000.0,
},
"reference": {
"speed": 1.0,
"length": 1.0,
"moment_point": [0.25, 0.0],
},
"geometry": {"bodies": [build_default_body_mapping(body_id="airfoil")]},
"post": {
"surface": {
"quantities": [
"velocity",
"cp",
]
},
"integrated": {
"quantities": [
"cl_pressure",
"cl_circulation",
"cd_pressure",
"cm_pressure",
]
},
},
}
[docs]
def build_default_body_mapping(
body_id: str,
airfoil_name: str | None = None,
) -> dict[str, Any]:
"""Return the default body mapping used for new geometry entries."""
return {
"id": body_id,
"airfoil": build_airfoil_schema_mapping("naca4"),
"discretization": "thick_body",
"sampling": {
"num_points_per_surface": 41,
"spacing": "cosine",
},
"placement": {
"scale": 1.0,
"rotation": 0.0,
"rotation_point": [0.0, 0.0],
"translation": [0.0, 0.0],
},
}
[docs]
def build_airfoil_parameter_mapping(
airfoil_type: str,
) -> dict[str, str | list[float]]:
"""Return the schema-dependent airfoil parameters for one type."""
mapping = build_airfoil_schema_mapping(airfoil_type)
mapping.pop("type", None)
return mapping
[docs]
def build_airfoil_schema_mapping(airfoil_type: str) -> dict[str, Any]:
"""Return one canonical Buffalo Wings starter mapping."""
spec = bwa.AirfoilFactory.default_spec(airfoil_type)
return _serialize_schema_value(spec)
def _serialize_schema_value(value: object) -> Any:
"""Convert one schema dataclass tree to builtin containers."""
if is_dataclass(value):
serialized: dict[str, Any] = {}
for field in fields(value):
field_value = getattr(value, field.name)
if field_value is None:
continue
serialized[field.name] = _serialize_schema_value(field_value)
return serialized
if isinstance(value, Mapping):
value_map = cast(Mapping[object, object], value)
return {
str(key): _serialize_schema_value(item)
for key, item in value_map.items()
}
if isinstance(value, Sequence) and not isinstance(value, str):
items = cast(Sequence[object], value)
return [_serialize_schema_value(item) for item in items]
return value
[docs]
def body_preview_coordinates(
spec: PanelCaseSpec,
body_spec: AirfoilBody2DSpec,
) -> tuple[npt.NDArray[np.float64], npt.NDArray[np.float64]]:
"""Return transformed airfoil coordinates for preview plotting."""
airfoil = bwa.AirfoilFactory.from_spec(body_spec.airfoil)
if body_spec.discretization == "thin_body":
x_raw, y_raw = _camber_preview_coordinates(airfoil, body_spec)
else:
boundary = bwa.sample_airfoil_boundary(
airfoil,
num_points_per_surface=body_spec.sampling.num_points_per_surface,
spacing=body_spec.sampling.spacing,
order="lower_to_upper",
warning_policy="ignore",
)
x_raw = boundary.coordinates[:, 0]
y_raw = boundary.coordinates[:, 1]
scale = body_spec.placement.scale
theta = body_spec.placement.rotation
theta_rad = float(np.deg2rad(theta)) if spec.units.angle == "deg" else theta
cos_t, sin_t = np.cos(theta_rad), np.sin(theta_rad)
rp_x, rp_y = body_spec.placement.rotation_point
tx, ty = body_spec.placement.translation
x_scaled = x_raw * scale
y_scaled = y_raw * scale
pivot_x = scale * rp_x
pivot_y = scale * rp_y
x_centered = x_scaled - pivot_x
y_centered = y_scaled - pivot_y
x_rotated = pivot_x + cos_t * x_centered - sin_t * y_centered
y_rotated = pivot_y + sin_t * x_centered + cos_t * y_centered
x_final = tx + x_rotated
y_final = ty + y_rotated
return x_final, y_final
def _camber_preview_coordinates(
airfoil: bwa.Airfoil,
body_spec: AirfoilBody2DSpec,
) -> tuple[npt.NDArray[np.float64], npt.NDArray[np.float64]]:
"""Return preview coordinates sampled from the airfoil camber curve."""
xi = _sampling_parameter_values(
num_points=body_spec.sampling.num_points_per_surface,
spacing=body_spec.sampling.spacing,
)
camber = airfoil.camber_curve(
num_points=body_spec.sampling.num_points_per_surface,
spacing=body_spec.sampling.spacing,
)
x_coord, y_coord = camber.curve.xy_from_u(xi)
return np.asarray(x_coord, dtype=np.float64), np.asarray(
y_coord,
dtype=np.float64,
)
def _sampling_parameter_values(
*,
num_points: int,
spacing: str,
) -> npt.NDArray[np.float64]:
"""Return one-dimensional sample parameters for previews."""
if spacing == "uniform":
return np.linspace(0.0, 1.0, num_points, dtype=np.float64)
theta = np.linspace(0.0, np.pi, num_points, dtype=np.float64)
return 0.5 * (1.0 - np.cos(theta))