Source code for buffalo_panel.post.internal.results

"""Generic post-processing result containers for two-dimensional solutions."""

from __future__ import annotations

from collections.abc import Callable
from dataclasses import dataclass, field
from typing import Self

import numpy as np

from buffalo_panel.assembly import Freestream2D
from buffalo_panel.geometry.line2d import LinePanelGeometry2D
from buffalo_panel.type_aliases import (
    FloatArray,
    FloatInput,
    FloatScalar,
    IntArray,
)


[docs] @dataclass(frozen=True, slots=True) class FieldPoints2D: """ Field evaluation points for two-dimensional post-processing. A field-point container represents one fixed target set. Inputs are broadcast to a common shape and copied at construction so later caller-side mutations do not alter the points used for evaluation. Methods that compute field values can use the flattened coordinate views and reshape results back to the original field-point shape. """ x: FloatArray """Field-point x-coordinates with the broadcast input shape.""" y: FloatArray """Field-point y-coordinates with the broadcast input shape.""" def __post_init__(self) -> None: """Validate coordinate shapes and make stored arrays read-only.""" if self.x.shape != self.y.shape: raise ValueError("x and y must have matching shapes.") self.x.setflags(write=False) self.y.setflags(write=False)
[docs] @classmethod def from_inputs(cls, x: FloatInput, y: FloatInput) -> Self: """ Build field points from scalar, vector, or array-like coordinates. Parameters ---------- x : FloatInput Field-point x-coordinates. y : FloatInput Field-point y-coordinates. Returns ------- FieldPoints2D Normalized field-point container whose coordinate arrays preserve the broadcast input shape. Raises ------ ValueError If `x` and `y` cannot be broadcast to a common shape. """ x_arr = np.asarray(x, dtype=np.float64) y_arr = np.asarray(y, dtype=np.float64) try: x_b, y_b = np.broadcast_arrays(x_arr, y_arr) except ValueError as exc: raise ValueError( "x and y must be broadcast-compatible field coordinates." ) from exc return cls( x=np.array(x_b, dtype=np.float64, copy=True), y=np.array(y_b, dtype=np.float64, copy=True), )
@property def shape(self) -> tuple[int, ...]: """Broadcast shape of the field points.""" return self.x.shape @property def size(self) -> int: """Number of field points.""" return int(self.x.size) @property def x_flat(self) -> FloatArray: """Flattened x-coordinates for kernel evaluation.""" return self.x.reshape(-1) @property def y_flat(self) -> FloatArray: """Flattened y-coordinates for kernel evaluation.""" return self.y.reshape(-1) @property def coordinates(self) -> FloatArray: """Flattened point coordinates with shape ``(size, 2)``.""" return np.column_stack((self.x_flat, self.y_flat))
[docs] def reshape_values(self, values: FloatInput) -> FloatArray: """ Reshape flat field values back to the field-point shape. Parameters ---------- values : FloatInput Field values with one entry per field point. Returns ------- FloatArray Values reshaped to ``shape``. Raises ------ ValueError If `values` does not contain one entry per field point. """ values_arr = np.asarray(values, dtype=np.float64) if values_arr.size != self.size: raise ValueError("values must contain one entry per field point.") return values_arr.reshape(self.shape)
VelocityEvaluator2D = Callable[[FieldPoints2D], tuple[FloatArray, FloatArray]] """Callable that evaluates two-dimensional velocity at field points.""" ScalarFieldEvaluator2D = Callable[[FieldPoints2D], FloatArray] """Callable that evaluates one scalar field at field points."""
[docs] @dataclass(slots=True) class BodyReference2D: """ Reference quantities used to nondimensionalize body coefficients. The geometry layer does not yet expose body-level chord and moment metadata, so this container carries the minimum information needed by post-processing recovery helpers. """ reference_length: FloatScalar = 1.0 """Reference length used for force and moment coefficients.""" moment_reference_x: FloatScalar = 0.0 """Global x-coordinate of the pitching-moment reference point.""" moment_reference_y: FloatScalar = 0.0 """Global y-coordinate of the pitching-moment reference point."""
[docs] @dataclass(slots=True) class StrengthField2D: """ Solved singularity strengths for one element family. A strength field preserves the mathematical identity and panel ownership of one solved family so callers can inspect formulation-specific unknowns without those unknowns becoming top-level attributes on ``PanelSolution2D``. The values are stored in family-local element order. """ family_name: str """Human-readable family name used by the formulation.""" support: str """Support descriptor name, such as ``"line_2d"``.""" singularity: str """Singularity descriptor name, such as ``"source"`` or ``"vortex"``.""" basis: str """Basis descriptor name, such as ``"constant"``.""" values: FloatArray """Solved strength values in family-local element order.""" panel_indices: IntArray """Geometry panel indices associated with ``values``."""
[docs] @dataclass(slots=True) class SurfaceQuantities2D: """ Flow quantities evaluated on a two-dimensional body surface. Surface quantities are intentionally solver agnostic. They describe flow values at a set of surface locations, not the singularity strengths or formulation-specific unknowns used to obtain those values. """ x: FloatArray """Global x-coordinates where the surface quantities are evaluated.""" y: FloatArray """Global y-coordinates where the surface quantities are evaluated.""" location_kind: str """ Description of the surface sample locations. Common values include ``"collocation"`` for panel collocation points and ``"nodes"`` for panel endpoints. """ tangent_velocity: FloatArray """Velocity component along the local surface tangent direction.""" normal_velocity: FloatArray """Velocity component along the local surface normal direction.""" cp: FloatArray """Pressure coefficient evaluated from the surface speed convention.""" panel_lift_coefficient: FloatArray | None = None """ Per-panel contribution to lift coefficient. This quantity is useful for formulations such as lumped vortex methods where a panelwise load contribution is meaningful but absolute surface pressure and velocity are not. """ has_absolute_velocity: bool = True """Whether the stored surface velocity arrays are physically reportable.""" has_absolute_pressure: bool = True """Whether the stored surface pressure array is physically reportable.""" @property def supports_surface_velocity(self) -> bool: """Return whether absolute surface velocity is available.""" return self.has_absolute_velocity @property def supports_surface_pressure(self) -> bool: """Return whether absolute surface pressure coefficient is available.""" return self.has_absolute_pressure @property def supports_panel_lift_distribution(self) -> bool: """Return whether a per-panel lift distribution is available.""" return self.panel_lift_coefficient is not None @property def speed(self) -> FloatArray: """Velocity magnitude evaluated on the surface.""" if not self.has_absolute_velocity: raise NotImplementedError( "This solution does not provide absolute surface velocity." ) return np.sqrt(self.tangent_velocity**2 + self.normal_velocity**2)
[docs] @dataclass(slots=True) class IntegratedQuantities2D: """ Integrated two-dimensional aerodynamic coefficients. Coefficients are nondimensionalized by the freestream dynamic pressure and the relevant body reference length. Until body reference metadata exists in the geometry layer, formulation recovery helpers must document any temporary reference assumptions they use to populate this container. """ cl_pressure: FloatScalar """Lift coefficient from integrating surface pressure.""" cl_circulation: FloatScalar """Lift coefficient from total circulation.""" cd_pressure: FloatScalar """Drag coefficient from integrating surface pressure.""" cm_pressure: FloatScalar """Pitching-moment coefficient from integrating surface pressure."""
[docs] @dataclass(slots=True) class PanelSolution2D: """ Post-processed solution for a two-dimensional panel calculation. This object groups the information most users need after a solve: the geometry and freestream context, formulation-specific strength fields, solver-agnostic surface quantities, and integrated aerodynamic coefficients. Field quantities such as velocity, potential, and stream function are expected to be computed on demand by future solution methods rather than stored here by default. """ geometry: LinePanelGeometry2D """Geometry used to produce the solution.""" freestream: Freestream2D """Freestream state used to produce the solution.""" strengths: tuple[StrengthField2D, ...] """Solved singularity-strength fields associated with the solution.""" surface: SurfaceQuantities2D """Surface flow quantities recovered from the solved strengths.""" integrated: IntegratedQuantities2D """Integrated aerodynamic coefficients recovered from the solution.""" _velocity_evaluator: VelocityEvaluator2D | None = field( default=None, repr=False ) """Solver-supplied velocity evaluator.""" _potential_evaluator: ScalarFieldEvaluator2D | None = field( default=None, repr=False ) """Solver-supplied velocity-potential evaluator.""" _stream_function_evaluator: ScalarFieldEvaluator2D | None = field( default=None, repr=False ) """Solver-supplied stream-function evaluator."""
[docs] def velocity_at( self, points: FieldPoints2D ) -> tuple[FloatArray, FloatArray]: """ Evaluate velocity at field points. Parameters ---------- points : FieldPoints2D Field evaluation targets. Returns ------- FloatArray Global x-velocity component with shape ``points.shape``. FloatArray Global y-velocity component with shape ``points.shape``. Raises ------ NotImplementedError If this solution does not provide field-velocity evaluation. """ if self._velocity_evaluator is None: raise NotImplementedError( f"{type(self).__name__} does not support velocity evaluation." ) return self._velocity_evaluator(points)
[docs] def potential_at(self, points: FieldPoints2D) -> FloatArray: """ Evaluate velocity potential at field points. Parameters ---------- points : FieldPoints2D Field evaluation targets. Returns ------- FloatArray Velocity potential with shape ``points.shape``. Raises ------ NotImplementedError If this solution does not provide velocity-potential evaluation. """ if self._potential_evaluator is None: raise NotImplementedError( f"{type(self).__name__} does not support potential evaluation." ) return self._potential_evaluator(points)
[docs] def stream_function_at(self, points: FieldPoints2D) -> FloatArray: """ Evaluate stream function at field points. Parameters ---------- points : FieldPoints2D Field evaluation targets. Returns ------- FloatArray Stream function with shape ``points.shape``. Raises ------ NotImplementedError If this solution does not provide stream-function evaluation. """ if self._stream_function_evaluator is None: raise NotImplementedError( f"{type(self).__name__} does not support stream-function " "evaluation." ) return self._stream_function_evaluator(points)
[docs] def pressure_coefficient_at(self, points: FieldPoints2D) -> FloatArray: """ Evaluate pressure at field points. Parameters ---------- points : FieldPoints2D Field evaluation targets. Returns ------- FloatArray Global pressure coefficient with shape ``points.shape``. Raises ------ NotImplementedError If this solution does not provide field-velocity evaluation. """ if self._velocity_evaluator is None: raise NotImplementedError( f"{type(self).__name__} does not support velocity evaluation." ) u, v = self._velocity_evaluator(points) u_inf = np.maximum(self.freestream.speed, 1.0e-14) c_p = 1 - (u**2 + v**2) / u_inf**2 return c_p