Source code for buffalo_panel.config.internal.solving

"""Runtime adapters that solve panel cases from structured config specs."""

from __future__ import annotations

import buffalo_wings.airfoil as bwa
import numpy as np

from buffalo_panel.assembly import Freestream2D
from buffalo_panel.config.internal.schema import (
    AirfoilBody2DSpec,
    PanelCaseSpec,
)
from buffalo_panel.formulations import (
    HessSmithFormulation,
    LumpedVortexFormulation,
)
from buffalo_panel.geometry import (
    LinePanelGeometry2D,
    ThickBodyLineGeometry2D,
    ThinBodyLineGeometry2D,
)
from buffalo_panel.kernels.registry_builds import build_2d_registry
from buffalo_panel.post import (
    BodyReference2D,
    PanelSolution2D,
    SolvedCaseArtifact,
    build_hess_smith_artifact,
    build_lumped_vortex_artifact,
)
from buffalo_panel.type_aliases import FloatArray

_SUPPORTED_SCHEMA_VERSION = 1


[docs] def solve_panel_case(spec: PanelCaseSpec) -> tuple[FloatArray, PanelSolution2D]: """ Solve a supported structured panel case. Parameters ---------- spec : PanelCaseSpec Structured case description using Buffalo Panel solver settings and Buffalo Wings airfoil schema objects. Returns ------- FloatArray Raw linear-system solution vector. PanelSolution2D Recovered surface, integrated, and on-demand field result interface. Raises ------ NotImplementedError If the case requests a solver or geometry multiplicity that is part of the schema but not yet wired into the runtime solve path. ValueError If the config contains unsupported unit declarations for this solve path. Notes ----- This runtime adapter intentionally supports one embedded-airfoil body with either the Hess-Smith thick-body formulation or the lumped-vortex thin-body formulation. The schema is broader than this implementation so user-facing case files can grow without changing the top-level model shape. """ _require_supported_solver(spec) body = _single_supported_body(spec) geometry = _build_airfoil_geometry(spec, body) body_reference = _build_body_reference(spec, body) formulation = _build_formulation( spec, geometry, body_reference, ) return formulation.solve(_build_freestream(spec))
[docs] def solve_panel_case_artifact(spec: PanelCaseSpec) -> SolvedCaseArtifact: """ Solve one supported structured panel case and persist its solved state. Parameters ---------- spec : PanelCaseSpec Structured case description using Buffalo Panel solver settings and Buffalo Wings airfoil schema objects. Returns ------- SolvedCaseArtifact Versioned solved-state artifact that can later be loaded into the post-processing layer. """ _require_supported_solver(spec) body = _single_supported_body(spec) geometry = _build_airfoil_geometry(spec, body) body_reference = _build_body_reference(spec, body) formulation = _build_formulation( spec, geometry, body_reference, ) freestream = _build_freestream(spec) solution_vector, _results = formulation.solve(freestream) if isinstance(formulation, HessSmithFormulation): source_families, vortex_families = formulation.build_solution_families() return build_hess_smith_artifact( spec=spec, geometry=geometry, freestream=freestream, body_reference=body_reference, backend=spec.solver.backend, solution_vector=solution_vector, source_families=source_families, vortex_families=vortex_families, ) vortex_families = (formulation.build_body_vortex_family(),) return build_lumped_vortex_artifact( spec=spec, geometry=geometry, freestream=freestream, body_reference=body_reference, backend=spec.solver.backend, solution_vector=solution_vector, vortex_families=vortex_families, )
def _require_supported_solver(spec: PanelCaseSpec) -> None: """Validate the solver controls supported by the config adapter.""" if spec.schema_version != _SUPPORTED_SCHEMA_VERSION: raise NotImplementedError( "Only panel case schema_version 1 is supported." ) if spec.solver.formulation not in {"hess_smith", "lumped_vortex"}: raise NotImplementedError( "Only the hess_smith and lumped_vortex formulations are supported." ) if spec.units.length not in {"m", "ft"}: raise ValueError("Length units must be 'm' or 'ft'.") if spec.units.angle not in {"deg", "rad"}: raise ValueError("Angle units must be 'deg' or 'rad'.") if spec.solver.formulation == "hess_smith": unsupported_bodies = [ body.id for body in spec.geometry.bodies if body.discretization != "thick_body" ] if unsupported_bodies: body_list = ", ".join(unsupported_bodies) raise NotImplementedError( "Only thick_body airfoil discretization is currently " "supported by the hess_smith solver path. Unsupported " f"bodies: {body_list}." ) return unsupported_bodies = [ body.id for body in spec.geometry.bodies if body.discretization != "thin_body" ] if unsupported_bodies: body_list = ", ".join(unsupported_bodies) raise NotImplementedError( "Only thin_body airfoil discretization is currently supported " "by the lumped_vortex solver path. Unsupported bodies: " f"{body_list}." ) def _single_supported_body(spec: PanelCaseSpec) -> AirfoilBody2DSpec: """Return the single supported airfoil body from a case spec.""" bodies = spec.geometry.bodies if len(bodies) != 1: raise NotImplementedError("Exactly one airfoil body is supported.") return bodies[0] def _build_airfoil_geometry( spec: PanelCaseSpec, body: AirfoilBody2DSpec ) -> LinePanelGeometry2D: """Sample a Buffalo Wings airfoil body and build line-panel geometry.""" airfoil = bwa.AirfoilFactory.from_spec(body.airfoil) if body.discretization == "thick_body": return _build_thick_airfoil_geometry(spec, body, airfoil) return _build_thin_airfoil_geometry(spec, body, airfoil) def _build_formulation( spec: PanelCaseSpec, geometry: LinePanelGeometry2D, body_reference: BodyReference2D, ) -> HessSmithFormulation | LumpedVortexFormulation: """Build the configured runtime formulation object.""" registry = build_2d_registry() if spec.solver.formulation == "hess_smith": return HessSmithFormulation( geometry=geometry, registry=registry, backend=spec.solver.backend, body_reference=body_reference, ) if not isinstance(geometry, ThinBodyLineGeometry2D): raise TypeError( "The lumped_vortex formulation requires thin-body geometry." ) return LumpedVortexFormulation( geometry=geometry, registry=registry, backend=spec.solver.backend, body_reference=body_reference, ) def _build_thick_airfoil_geometry( spec: PanelCaseSpec, body: AirfoilBody2DSpec, airfoil: bwa.Airfoil, ) -> ThickBodyLineGeometry2D: """Build thick-body panel geometry from sampled airfoil boundary points.""" boundary = bwa.sample_airfoil_boundary( airfoil, num_points_per_surface=body.sampling.num_points_per_surface, spacing=body.sampling.spacing, order="lower_to_upper", warning_policy="ignore", ) x, y = _place_airfoil_coordinates( boundary.coordinates[:, 0], boundary.coordinates[:, 1], body, angle_unit=spec.units.angle, ) return ThickBodyLineGeometry2D(x, y, geometry_name=body.id) def _build_thin_airfoil_geometry( spec: PanelCaseSpec, body: AirfoilBody2DSpec, airfoil: bwa.Airfoil, ) -> ThinBodyLineGeometry2D: """Build thin-body panel geometry from the airfoil camber curve.""" xi = _sampling_parameter_values( num_points=body.sampling.num_points_per_surface, spacing=body.sampling.spacing, ) camber = airfoil.camber_curve( num_points=body.sampling.num_points_per_surface, spacing=body.sampling.spacing, ) x_camber, y_camber = camber.curve.xy_from_u(xi) x, y = _place_airfoil_coordinates( x_camber, y_camber, body, angle_unit=spec.units.angle, ) return ThinBodyLineGeometry2D(x, y, geometry_name=body.id) def _sampling_parameter_values( *, num_points: int, spacing: str, ) -> FloatArray: """Return one-dimensional sample parameters for airfoil body curves.""" 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)) def _place_airfoil_coordinates( x: FloatArray, y: FloatArray, body: AirfoilBody2DSpec, *, angle_unit: str, ) -> tuple[FloatArray, FloatArray]: """Apply the configured airfoil scale, rotation, and translation.""" scale = body.placement.scale if scale <= 0.0: raise ValueError("Airfoil placement scale must be positive.") theta = body.placement.rotation theta_rad = float(np.deg2rad(theta)) if angle_unit == "deg" else theta cos_theta = float(np.cos(theta_rad)) sin_theta = float(np.sin(theta_rad)) rotation_point_x, rotation_point_y = body.placement.rotation_point translation_x, translation_y = body.placement.translation x_scaled = scale * x y_scaled = scale * y pivot_x = scale * rotation_point_x pivot_y = scale * rotation_point_y x_centered = x_scaled - pivot_x y_centered = y_scaled - pivot_y x_rotated = pivot_x + cos_theta * x_centered - sin_theta * y_centered y_rotated = pivot_y + sin_theta * x_centered + cos_theta * y_centered x_placed = translation_x + x_rotated y_placed = translation_y + y_rotated return x_placed, y_placed def _build_body_reference( spec: PanelCaseSpec, body: AirfoilBody2DSpec, ) -> BodyReference2D: """Convert case reference metadata to post-processing reference values.""" moment_reference_x, moment_reference_y = spec.reference.moment_point reference_length = ( spec.reference.length if spec.reference.length is not None else body.placement.scale ) return BodyReference2D( reference_length=reference_length, moment_reference_x=moment_reference_x, moment_reference_y=moment_reference_y, ) def _build_freestream(spec: PanelCaseSpec) -> Freestream2D: """Convert freestream config values to the runtime freestream model.""" alpha_deg = ( spec.freestream.alpha if spec.units.angle == "deg" else float(np.rad2deg(spec.freestream.alpha)) ) return Freestream2D(speed=spec.freestream.speed, alpha_deg=alpha_deg)