Source code for buffalo_panel.geometry.line2d

"""Geometry information needed for 2D line-element discretizations."""

from __future__ import annotations

from dataclasses import dataclass
from typing import Protocol, runtime_checkable

import numpy as np
from buffalo_core import as_float_array

from buffalo_panel.type_aliases import FloatArray, IntArray

_SMALL_NUMBER = 1e-8

_MIN_NUM_THICK_NODES = 3
_THICK_COLLOCATION_LOC = 0.5
_THICK_ELEMENT_LOC = 0.5

_MIN_NUM_THIN_NODES = 2
_THIN_COLLOCATION_LOC = 0.75
_THIN_ELEMENT_LOC = 0.25


[docs] @runtime_checkable class LineKernelGeometry2D(Protocol): """Structural protocol for geometries used by 2D line kernels.""" @property def n_panels(self) -> int: """Return the number of panels.""" ... @property def x_col(self) -> FloatArray: """Return the per-panel collocation x-coordinates.""" ... @property def y_col(self) -> FloatArray: """Return the per-panel collocation y-coordinates.""" ... @property def length(self) -> FloatArray: """Return the per-panel lengths.""" ... @property def s_x(self) -> FloatArray: """Return the per-panel tangent x-components.""" ... @property def s_y(self) -> FloatArray: """Return the per-panel tangent y-components.""" ... @property def n_x(self) -> FloatArray: """Return the per-panel normal x-components.""" ... @property def n_y(self) -> FloatArray: """Return the per-panel normal y-components.""" ...
[docs] @runtime_checkable class PointElementLineGeometry2D(LineKernelGeometry2D, Protocol): """ Structural protocol for line-panel geometries with point elements. Point-element kernels require the shared line-kernel geometry data plus explicit per-panel point-element coordinates. Concrete geometries may provide those coordinates through a dedicated geometry class or through artifact-rebuilt runtime objects. """ @property def x_element(self) -> FloatArray: """Return the per-panel point-element x-coordinates.""" ... @property def y_element(self) -> FloatArray: """Return the per-panel point-element y-coordinates.""" ... @property def source_points(self) -> FloatArray: """Return the per-panel point-element coordinates.""" ...
[docs] @dataclass(slots=True) class LinePanelGeometry2D: """ Shared line-panel geometry information for two-dimensional discretizations. This base class stores the panel-node, panel-segment, collocation-point, and local-frame data shared by both thick-body and thin-body discretizations. Subclasses provide the semantic interpretation of those points. """ geometry_id: int """Unique-orchestrator identifier for this geometry.""" geometry_name: str """Display name for this geometry.""" x: FloatArray """Geometry x-coordinates.""" y: FloatArray """Geometry y-coordinates.""" connectivity: IntArray """Connectivity mapping each panel to its start and end points.""" x_start: FloatArray """Starting x-coordinates for every panel.""" y_start: FloatArray """Starting y-coordinates for every panel.""" x_end: FloatArray """Ending x-coordinates for every panel.""" y_end: FloatArray """Ending y-coordinates for every panel.""" x_col: FloatArray """Collocation x-coordinates for every panel.""" y_col: FloatArray """Collocation y-coordinates for every panel.""" length: FloatArray """Length of every panel.""" s_x: FloatArray """Unit tangent vector x-component for every panel.""" s_y: FloatArray """Unit tangent vector y-component for every panel.""" n_x: FloatArray """Unit normal vector x-component for every panel.""" n_y: FloatArray """Unit tangent vector y-component for every panel.""" body_panel_indices: IntArray """Body panel index for every panel.""" trailing_edge_panel_index: int | None """Index of the trailing edge panel, or None if there isn't one.""" @property def n_panels(self) -> int: """Return the number of panels.""" return int(self.length.size) @property def n_body_panels(self) -> int: """Return the number of body panels.""" return int(self.body_panel_indices.size) @property def nodes(self) -> FloatArray: """Return the x,y coordinates for the panel nodes.""" return np.column_stack((self.x, self.y)) @property def collocation_points(self) -> FloatArray: """Return the x,y coordinates for the collocation points.""" return np.column_stack((self.x_col, self.y_col)) @property def tangents(self) -> FloatArray: """Return the unit tangent vectors for each panel.""" return np.column_stack((self.s_x, self.s_y)) @property def normals(self) -> FloatArray: """Return the unit normal vectors for each panel.""" return np.column_stack((self.n_x, self.n_y)) @property def is_closed(self) -> bool: """Return whether the geometry is closed.""" return bool( np.abs(self.x[0] - self.x[-1]) < _SMALL_NUMBER and np.abs(self.y[0] - self.y[-1]) < _SMALL_NUMBER )
class BaseBodyLineGeometry2D(LinePanelGeometry2D): """Base class for 2d bodies composed of line geometry.""" def __init__( self, x: FloatArray, y: FloatArray, *, geometry_id: int, geometry_name: str | None, wrap_open_curve: bool, collocation_fraction: float, ) -> None: x_arr = as_float_array(x) y_arr = as_float_array(y) if x_arr.ndim != 1 or y_arr.ndim != 1 or x_arr.size != y_arr.size: raise ValueError( "x and y must be one-dimensional arrays of equal length." ) if geometry_id < 0: raise ValueError("geometry_id must be non-negative.") if not (0.0 < collocation_fraction < 1.0): raise ValueError( "collocation_fraction must be strictly between 0 and 1." ) resolved_geometry_name = ( geometry_name if geometry_name is not None else f"Geometry {geometry_id}" ) input_is_closed = ( np.abs(x_arr[0] - x_arr[-1]) < _SMALL_NUMBER and np.abs(y_arr[0] - y_arr[-1]) < _SMALL_NUMBER ) n_nodes = x_arr.size body_panel_indices = np.arange(n_nodes - 1, dtype=np.int32) if input_is_closed: connectivity = np.column_stack(( np.arange(n_nodes - 1, dtype=np.int32), np.arange(1, n_nodes, dtype=np.int32), )) trailing_edge_panel_index = None body_panel_indices = np.arange( connectivity.shape[0], dtype=np.int32 ) elif wrap_open_curve: connectivity = np.column_stack(( np.arange(n_nodes, dtype=np.int32), np.arange(1, n_nodes + 1, dtype=np.int32), )) connectivity[-1, 1] = 0 trailing_edge_panel_index = n_nodes - 1 body_panel_indices = np.arange(n_nodes - 1, dtype=np.int32) else: connectivity = np.column_stack(( np.arange(n_nodes - 1, dtype=np.int32), np.arange(1, n_nodes, dtype=np.int32), )) trailing_edge_panel_index = None body_panel_indices = np.arange( connectivity.shape[0], dtype=np.int32 ) x_start = x_arr[connectivity[:, 0]] y_start = y_arr[connectivity[:, 0]] x_end = x_arr[connectivity[:, 1]] y_end = y_arr[connectivity[:, 1]] dx = x_end - x_start dy = y_end - y_start length = np.sqrt(dx * dx + dy * dy) if np.any(length <= 0.0): raise ValueError("Degenerate panel with zero length detected.") tx = dx / length ty = dy / length nx = -ty ny = tx x_col = x_start + collocation_fraction * dx y_col = y_start + collocation_fraction * dy super().__init__( geometry_id=geometry_id, geometry_name=resolved_geometry_name, x=x_arr, y=y_arr, connectivity=connectivity, x_start=x_start, y_start=y_start, x_end=x_end, y_end=y_end, x_col=x_col, y_col=y_col, length=length, s_x=tx, s_y=ty, n_x=nx, n_y=ny, body_panel_indices=body_panel_indices, trailing_edge_panel_index=trailing_edge_panel_index, ) self._collocation_fraction = collocation_fraction @property def collocation_fraction(self) -> float: """Uniform panel-collocation fraction used by this geometry.""" return self._collocation_fraction
[docs] class ThickBodyLineGeometry2D(BaseBodyLineGeometry2D): """ Line-panel geometry representing a thick-body boundary discretization. Panels lie on the physical body surface and collocation points represent boundary-condition enforcement locations on that surface. """ def __init__( self, x: FloatArray, y: FloatArray, *, geometry_id: int = 0, geometry_name: str | None = None, ) -> None: """ Construct one thick-body line-panel geometry from a set of points. Parameters ---------- x : FloatArray X-coordinates of panel nodes. y : FloatArray Y-coordinates of panel nodes. geometry_id : int, default=0 Integer identifier for this geometry. geometry_name : str | None, default=None Optional display name for this geometry. Defaults to ``"Geometry {geometry_id}"``. collocation_fraction : float, default=0.5 Fractional location of each panel collocation point measured from the panel start node toward the end node. Returns ------- ThickBodyLineGeometry2D Thick-body panel geometry. """ if as_float_array(x).size < _MIN_NUM_THICK_NODES: raise ValueError("At least three boundary points are required.") super().__init__( x, y, geometry_id=geometry_id, geometry_name=geometry_name, collocation_fraction=_THICK_COLLOCATION_LOC, wrap_open_curve=True, )
[docs] class ThinBodyLineGeometry2D(BaseBodyLineGeometry2D): """ Line-panel geometry representing a thin-body camber-line discretization. Panels lie on the body reference curve. The intended default convention is quarter-panel element placement with three-quarter-panel collocation, though the shared stored fields remain the same as the thick-body geometry. """ def __init__( self, x: FloatArray, y: FloatArray, *, geometry_id: int = 0, geometry_name: str | None = None, point_element: bool = True, ) -> None: """ Construct one thin-body line-panel geometry from a set of points. Thin-body geometry uses the fixed three-quarter-panel collocation convention intended to pair with quarter-panel element placement. Parameters ---------- x : FloatArray X-coordinates of panel nodes. y : FloatArray Y-coordinates of panel nodes. geometry_id : int, default=0 Integer identifier for this geometry. geometry_name : str | None, default=None Optional display name for this geometry. Defaults to ``"Geometry {geometry_id}"``. point_element : bool, default=True Whether element on the panel will be a point element. If it is a point element, then the element location is set to the quarter panel location and collocation point to the three-quarter panel location. Otherwise, both set to the mid-panel location. Returns ------- ThinBodyLineGeometry2D Thin-body panel geometry. """ if as_float_array(x).size < _MIN_NUM_THIN_NODES: raise ValueError("At least three boundary points are required.") if point_element: collocation_fraction = _THIN_COLLOCATION_LOC element_fraction = _THIN_ELEMENT_LOC else: collocation_fraction = _THICK_COLLOCATION_LOC element_fraction = _THICK_ELEMENT_LOC super().__init__( x, y, geometry_id=geometry_id, geometry_name=geometry_name, collocation_fraction=collocation_fraction, wrap_open_curve=False, ) dx = self.x_end - self.x_start dy = self.y_end - self.y_start self._element_fraction = element_fraction self._x_element = self.x_start + element_fraction * dx self._y_element = self.y_start + element_fraction * dy @property def x_element(self) -> FloatArray: """Panel element x-coordinates for every panel.""" return self._x_element @property def y_element(self) -> FloatArray: """Panel element y-coordinates for every panel.""" return self._y_element @property def source_points(self) -> FloatArray: """Element source x,y coordinates.""" return np.column_stack((self.x_element, self.y_element)) @property def element_fraction(self) -> float: """Uniform point-element fraction used by this geometry.""" return self._element_fraction