Source code for buffalo_panel.formulations.hess_smith

"""Public Hess-Smith formulation entry points."""

from __future__ import annotations

from dataclasses import dataclass

import numpy as np
from buffalo_core import as_int_array

from buffalo_panel.assembly import Freestream2D
from buffalo_panel.assembly.internal.system import (
    build_hess_smith_open_trailing_edge_system,
    build_hess_smith_system,
)
from buffalo_panel.basis.descriptors import LINE_CONSTANT
from buffalo_panel.families.dof_maps import PerElementDofMap, SharedGlobalDofMap
from buffalo_panel.families.element_family import ElementFamily
from buffalo_panel.geometry.line2d import LinePanelGeometry2D
from buffalo_panel.geometry.supports import LINE_2D
from buffalo_panel.kernels.registry import KernelRegistry
from buffalo_panel.post import BodyReference2D, PanelSolution2D
from buffalo_panel.post.internal.hess_smith import recover_hess_smith_solution
from buffalo_panel.prescribed.internal.point_element_particle_2d import (
    PointElementParticle2D,
)
from buffalo_panel.singularities.descriptors import SOURCE, VORTEX
from buffalo_panel.solvers.dense import solve_dense
from buffalo_panel.type_aliases import FloatArray


@dataclass(slots=True)
class OpenTrailingEdgeFamilies:
    """Required trailing-edge families for open-geometry Hess-Smith cases."""

    source: ElementFamily
    """Trailing-edge source family."""

    vortex: ElementFamily
    """Trailing-edge vortex family."""


[docs] @dataclass(slots=True) class HessSmithFormulation: """Solve the classic Hess-Smith source-plus-vortex formulation.""" geometry: LinePanelGeometry2D """Geometry used in the Hess-Smith solver.""" registry: KernelRegistry """Registry of computational kernels to use in Hess-Smith solver.""" backend: str = "python" """Specific computational kernel backend to use.""" body_reference: BodyReference2D | None = None """Reference quantities for integrated coefficient recovery."""
[docs] def build_solution_families( self, ) -> tuple[tuple[ElementFamily, ...], tuple[ElementFamily, ...]]: """ Build the source and vortex families used by this formulation. Returns ------- tuple[tuple[ElementFamily, ...], tuple[ElementFamily, ...]] Source-family tuple followed by vortex-family tuple in the order required by the current Hess-Smith assembly and recovery paths. """ source_family = self.build_body_source_family() vortex_family = self.build_body_vortex_family() source_families: list[ElementFamily] = [source_family] vortex_families: list[ElementFamily] = [vortex_family] if not self.geometry.is_closed: te_families = self.build_open_trailing_edge_families() source_families.append(te_families.source) vortex_families.append(te_families.vortex) return tuple(source_families), tuple(vortex_families)
[docs] def build_body_source_family(self) -> ElementFamily: """ Build the constant source family for the body panels. Returns ------- ElementFamily Body source panel assembly for this formulation. """ n_body_panels = self.geometry.n_body_panels body_source_index = np.arange(n_body_panels, dtype=np.int32) return ElementFamily( name="body_sources", support=LINE_2D, singularity=SOURCE, basis=LINE_CONSTANT, n_elements=n_body_panels, panel_indices=self.geometry.body_panel_indices, dof_map=PerElementDofMap(global_indices=body_source_index), metadata={"role": "body"}, )
[docs] def build_body_vortex_family(self) -> ElementFamily: """ Build the shared constant vortex family for the body panels. Returns ------- ElementFamily Body vortex panel assembly for this formulation. """ n_body_panels = self.geometry.n_body_panels body_vortex_index = n_body_panels return ElementFamily( name="body_vortex_shared", support=LINE_2D, singularity=VORTEX, basis=LINE_CONSTANT, n_elements=n_body_panels, panel_indices=self.geometry.body_panel_indices, dof_map=SharedGlobalDofMap( global_index=body_vortex_index, n_elements=n_body_panels, ), metadata={"role": "body", "shared_gamma": True}, )
[docs] def build_trailing_edge_source_family(self) -> ElementFamily | None: """ Build the constant source family for the TE closure panel. Returns ------- ElementFamily | None Trailing edge source panel assembly for this formulation, if geometry is open, otherwise `None`. """ te_panel_index = self.geometry.trailing_edge_panel_index if te_panel_index is None: return None n_body_panels = self.geometry.n_body_panels te_source_index = as_int_array([n_body_panels + 1]) return ElementFamily( name="trailing_edge_source", support=LINE_2D, singularity=SOURCE, basis=LINE_CONSTANT, n_elements=1, panel_indices=as_int_array([te_panel_index]), dof_map=PerElementDofMap(global_indices=te_source_index), metadata={"role": "trailing_edge"}, )
[docs] def build_trailing_edge_vortex_family(self) -> ElementFamily | None: """ Build the constant vortex family for the TE closure panel. Returns ------- ElementFamily | None Trailing edge vortex panel assembly for this formulation, if geometry is open, otherwise `None`. """ te_panel_index = self.geometry.trailing_edge_panel_index if te_panel_index is None: return None n_body_panels = self.geometry.n_body_panels te_vortex_index = as_int_array([n_body_panels + 2]) return ElementFamily( name="trailing_edge_vortex", support=LINE_2D, singularity=VORTEX, basis=LINE_CONSTANT, n_elements=1, panel_indices=as_int_array([te_panel_index]), dof_map=PerElementDofMap(global_indices=te_vortex_index), metadata={"role": "trailing_edge", "shared_gamma": False}, )
[docs] def build_open_trailing_edge_families(self) -> OpenTrailingEdgeFamilies: """ Build the required trailing-edge families for an open geometry. Returns ------- OpenTrailingEdgeFamilies Trailing-edge source and vortex families for the closure panel. Raises ------ ValueError If called for a closed geometry that has no trailing-edge panel. """ te_source_family = self.build_trailing_edge_source_family() te_vortex_family = self.build_trailing_edge_vortex_family() if te_source_family is None or te_vortex_family is None: raise ValueError( "Open trailing-edge families require a geometry with a " "trailing-edge closure panel." ) return OpenTrailingEdgeFamilies( source=te_source_family, vortex=te_vortex_family, )
[docs] def solve( self, freestream: Freestream2D, point_particles: tuple[PointElementParticle2D, ...] = (), ) -> tuple[FloatArray, PanelSolution2D]: """ Assemble, solve, and recover surface results for one freestream. Parameters ---------- freestream : Freestream2D Freestream conditions for this solution. point_particles : tuple[PointElementParticle2D, ...], optional Prescribed point particles whose induced fields contribute known forcing to the body boundary condition and to recovered flow fields. Returns ------- FloatArray Raw solution from the system of equations. PanelSolution2D Processed results information from solution. """ source_families, vortex_families = self.build_solution_families() source_family = source_families[0] vortex_family = vortex_families[0] if self.geometry.is_closed: matrix, rhs = build_hess_smith_system( source_family, vortex_family, self.geometry, freestream, self.registry, self.backend, point_particles, ) else: te_source_family = source_families[1] te_vortex_family = vortex_families[1] matrix, rhs = build_hess_smith_open_trailing_edge_system( source_family, vortex_family, te_source_family, te_vortex_family, self.geometry, freestream, self.registry, self.backend, point_particles, ) x = solve_dense(matrix, rhs) results = recover_hess_smith_solution( x, source_families, vortex_families, self.geometry, freestream, self.registry, self.backend, self.body_reference, point_particles, ) return x, results