Source code for buffalo_panel.families.element_family

"""Element-family metadata used to assemble panel formulations."""

from __future__ import annotations

from dataclasses import dataclass
from typing import Literal

import numpy as np
from buffalo_core import as_float_array, as_int_array

from buffalo_panel.basis.descriptors import (
    LINE_CONSTANT,
    LINE_LINEAR,
    BasisDescriptor,
)
from buffalo_panel.families.dof_maps import DofMap
from buffalo_panel.geometry.supports import SupportDescriptor
from buffalo_panel.singularities.descriptors import SingularityDescriptor
from buffalo_panel.type_aliases import FloatArray, FloatInput, IntInput

_ENDPOINT_TRACE_INDEX_SIZE = 2


[docs] @dataclass(slots=True) class ElementFamily: """ Describe one family of panel elements in an assembled formulation. This dataclass groups the support, singularity, basis, ownership, and degree-of-freedom mapping needed to assemble one logically uniform set of elements into a global linear system. Raises ------ ValueError If ``panel_indices`` is not one-dimensional or if its size does not match ``n_elements``. """ name: str """Human-readable family name used for debugging and reporting.""" support: SupportDescriptor """Support descriptor identifying the geometric element type.""" singularity: SingularityDescriptor """Singularity descriptor defining the family physics.""" basis: BasisDescriptor """Basis descriptor defining how the family is parameterized.""" n_elements: int """Number of elements owned by this family.""" panel_indices: IntInput """Local panel indices owned by this family.""" dof_map: DofMap """Map that places family coefficients into the global solve vector.""" metadata: dict[str, object] | None = None """Optional semantic labels or bookkeeping metadata for the family.""" @property def registry_key(self) -> tuple[str, str, str]: """ Return the backend registry lookup key for this family. Returns ------- tuple[str, str, str] Triple of support, singularity, and basis names used to select the appropriate computational kernel implementation. The triple of support, singularity, and basis names used to select the appropriate computational kernel implementation. """ return (self.support.name, self.singularity.name, self.basis.name) def __post_init__(self) -> None: """Validate and normalize the owned panel indices.""" panel_indices = as_int_array(self.panel_indices) if panel_indices.ndim != 1: raise ValueError("panel_indices must be a one-dimensional array.") if panel_indices.size != self.n_elements: raise ValueError( "n_elements must match the number of panel indices." ) self.panel_indices = panel_indices @property def n_local_dofs(self) -> int: """ Return the number of family-local coefficients used by this family. Returns ------- int Number of family-local coefficients expected by kernel block builders for this family. """ return self.dof_map.n_local_dofs(self.n_elements)
[docs] def local_coefficients(self, x: FloatInput) -> FloatArray: """ Expand solved coefficients into the family-local coefficient space. Parameters ---------- x : FloatInput Global solved coefficient vector. Returns ------- FloatArray One ``float64`` solved coefficient per family-local kernel column. Raises ------ ValueError If the family's degree-of-freedom map is inconsistent with ``family.n_elements``. """ return self.dof_map.local_coefficients( as_float_array(x), self.n_elements, )
[docs] def endpoint_trace_row( self, endpoint: Literal["start", "end"], n_unknowns: int, ) -> FloatArray: """ Build a global row that evaluates family strength at one endpoint. Parameters ---------- endpoint : {"start", "end"} Which support endpoint should be traced. n_unknowns : int Length of the global solve vector. Returns ------- FloatArray Global row that maps solve coefficients to the endpoint strength. Notes ----- This helper currently supports single-element line families with per-element global indexing for constant and linear-line bases. That is enough to assemble the current open-trailing-edge endpoint conditions while leaving a clear extension point for future bases. """ if self.n_elements != 1: raise NotImplementedError( "Endpoint strength traces currently require one element." ) if self.basis == LINE_CONSTANT: return self.dof_map.lift_local_row( np.array([1.0], dtype=np.float64), n_unknowns, ) if self.basis == LINE_LINEAR: local_weights = np.zeros( _ENDPOINT_TRACE_INDEX_SIZE, dtype=np.float64, ) if endpoint == "start": local_weights[0] = 1.0 else: local_weights[1] = 1.0 return self.dof_map.lift_local_row(local_weights, n_unknowns) raise NotImplementedError( f"Endpoint strength trace is not implemented for " f"{self.basis.name!r}." )
[docs] def boundary_trace_row( self, boundary: Literal["lower_te", "upper_te"], n_unknowns: int, ) -> FloatArray: """ Build a global row for one trailing-edge boundary strength trace. Parameters ---------- boundary : {"lower_te", "upper_te"} Requested trailing-edge endpoint trace. ``"lower_te"`` means the start of the first family element, and ``"upper_te"`` means the end of the last family element. n_unknowns : int Length of the global solve vector. Returns ------- FloatArray Global row that maps solve coefficients to the requested boundary strength. Notes ----- This helper currently supports: - constant families with any element count - one-element linear-line families """ if self.basis == LINE_CONSTANT: local_weights = np.zeros(self.n_elements, dtype=np.float64) if boundary == "lower_te": local_weights[0] = 1.0 else: local_weights[-1] = 1.0 return self.dof_map.lift_local_row(local_weights, n_unknowns) if self.basis == LINE_LINEAR and self.n_elements == 1: endpoint: Literal["start", "end"] endpoint = "start" if boundary == "lower_te" else "end" return self.endpoint_trace_row(endpoint, n_unknowns) raise NotImplementedError( f"Boundary trace is not implemented for {self.basis.name!r} with " f"{self.n_elements} elements." )
[docs] def pinned_zero_row(self, n_unknowns: int) -> FloatArray: """ Build fallback row that pins this family's primary coefficient to zero. Parameters ---------- n_unknowns : int Length of the global solve vector. Returns ------- FloatArray Global row used as a temporary fallback constraint. Notes ----- This helper currently supports single-element line families with a per-element DOF map. It intentionally captures the temporary fallback behavior behind the family interface so formulation assembly does not need to inspect the concrete DOF-map type. """ if self.n_elements != 1: raise NotImplementedError( "Pinned zero rows currently require one element." ) if self.basis == LINE_CONSTANT: local_weights = np.array([1.0], dtype=np.float64) elif self.basis == LINE_LINEAR: local_weights = np.array([1.0, 0.0], dtype=np.float64) else: raise NotImplementedError( f"Pinned zero row is not implemented for {self.basis.name!r}." ) return self.dof_map.lift_local_row(local_weights, n_unknowns)