Source code for buffalo_panel.families.dof_maps

"""Degree-of-freedom maps for assembling panel-family systems."""

from __future__ import annotations

from abc import ABC, abstractmethod
from dataclasses import dataclass

import numpy as np
from buffalo_core import as_int_array

from buffalo_panel.type_aliases import FloatArray, IntArray


[docs] class DofMap(ABC): """ Base class for mapping family-local quantities into global systems. Concrete DOF maps define both how local influence columns scatter into a global matrix and how solved global coefficients expand back into the family-local coefficient space used by kernels. """
[docs] @abstractmethod def n_local_dofs(self, n_elements: int) -> int: """ Return the number of family-local coefficients represented by this map. Parameters ---------- n_elements : int Number of elements owned by the family using this DOF map. Returns ------- int Number of local coefficients expected by the kernel block builder. """
[docs] @abstractmethod def scatter_columns( self, local_block: FloatArray, global_matrix: FloatArray, ) -> None: """ Accumulate a local family block into a global matrix. Parameters ---------- local_block : FloatArray Family-local influence block with one column per family element. global_matrix : FloatArray Global matrix that receives the mapped family contribution. """
[docs] @abstractmethod def local_coefficients( self, x: FloatArray, n_elements: int, ) -> FloatArray: """ Expand solved coefficients into the family-local coefficient space. Parameters ---------- x : FloatArray Global solved coefficient vector. n_elements : int Number of elements owned by the family using this DOF map. Returns ------- FloatArray One solved coefficient value per family-local degree of freedom. """
[docs] @abstractmethod def lift_local_row( self, local_weights: FloatArray, n_unknowns: int, ) -> FloatArray: """ Lift one family-local linear row into the global unknown space. Parameters ---------- local_weights : FloatArray One-dimensional row of local coefficient weights. n_unknowns : int Length of the global solve vector. Returns ------- FloatArray Global row equivalent to applying ``local_weights`` in the family's local coefficient space. """
[docs] @dataclass(slots=True) class PerElementDofMap(DofMap): """ Assign one global degree of freedom to each element. Each family element contributes to and reads from its own distinct entry in the global solved coefficient vector. """ global_indices: IntArray """Global coefficient index associated with each family element.""" def __post_init__(self) -> None: """Normalize and validate the stored global indices.""" global_indices = as_int_array(self.global_indices) if global_indices.ndim != 1: raise ValueError("global_indices must be a one-dimensional array.") self.global_indices = global_indices
[docs] def n_local_dofs(self, n_elements: int) -> int: """ Return the number of family-local coefficients for this map. Parameters ---------- n_elements : int Number of elements owned by the family using this map. Returns ------- int Number of family-local coefficients. Raises ------ ValueError If the stored index count does not match ``n_elements``. """ if self.global_indices.size != n_elements: raise ValueError( "Expected one global index per element in PerElementDofMap." ) return int(self.global_indices.size)
[docs] def scatter_columns( self, local_block: FloatArray, global_matrix: FloatArray, ) -> None: """ Accumulate one local column per element into the global matrix. Parameters ---------- local_block : FloatArray Family-local influence block with one column per owned element. global_matrix : FloatArray Global matrix that receives the columns at ``global_indices``. Raises ------ ValueError If ``local_block`` does not have one column per stored global index. """ if local_block.shape[1] != self.global_indices.size: raise ValueError( "Expected one local column per global index in " "PerElementDofMap." ) global_matrix[:, self.global_indices] += local_block
[docs] def local_coefficients( self, x: FloatArray, n_elements: int, ) -> FloatArray: """ Return the solved coefficient for each family-local degree of freedom. Parameters ---------- x : FloatArray Global solved coefficient vector. n_elements : int Number of elements owned by the family using this map. Returns ------- FloatArray One solved coefficient per family-local degree of freedom, gathered from ``global_indices``. Raises ------ ValueError If the number of stored global indices does not match ``n_elements``. """ if self.global_indices.size != n_elements: raise ValueError( "Expected one global index per element in PerElementDofMap." ) return x[self.global_indices]
[docs] def lift_local_row( self, local_weights: FloatArray, n_unknowns: int, ) -> FloatArray: """ Lift one per-element local row into the global unknown space. Parameters ---------- local_weights : FloatArray One-dimensional local weight row with one entry per stored global index. n_unknowns : int Length of the global solve vector. Returns ------- FloatArray Global row with the local weights placed at ``global_indices``. Raises ------ ValueError If ``local_weights`` is not one-dimensional, does not match the number of stored global indices, or ``n_unknowns`` is too small. """ if local_weights.ndim != 1: raise ValueError("local_weights must be a one-dimensional array.") if local_weights.size != self.global_indices.size: raise ValueError( "Expected one local weight per global index in " "PerElementDofMap." ) if np.any(self.global_indices >= n_unknowns): raise ValueError("n_unknowns is too small for PerElementDofMap.") row = np.zeros(n_unknowns, dtype=np.float64) row[self.global_indices] = local_weights return row
[docs] @dataclass(slots=True) class SharedGlobalDofMap(DofMap): """ Collapse many element-local columns into one shared global degree. Every owned element contributes to a common global coefficient and reads the same solved strength during post-processing. """ global_index: int """Shared global coefficient index used by all owned elements.""" n_elements: int """Number of family elements collapsed into the shared coefficient.""" def __post_init__(self) -> None: """Validate the stored shared-DOF metadata.""" if self.global_index < 0: raise ValueError("global_index must be non-negative.") if self.n_elements < 0: raise ValueError("n_elements must be non-negative.")
[docs] def n_local_dofs(self, n_elements: int) -> int: """ Return the expanded local coefficient count for this shared map. Parameters ---------- n_elements : int Number of elements owned by the family using this map. Returns ------- int Number of expanded family-local coefficients. Raises ------ ValueError If ``self.n_elements`` does not match ``n_elements``. """ if self.n_elements != n_elements: raise ValueError( "SharedGlobalDofMap n_elements must match family n_elements." ) return self.n_elements
[docs] def scatter_columns( self, local_block: FloatArray, global_matrix: FloatArray, ) -> None: """ Accumulate a shared element family into one global column. Parameters ---------- local_block : FloatArray Family-local influence block with one column per owned element. global_matrix : FloatArray Global matrix that receives the summed contribution in the shared column. Raises ------ ValueError If ``local_block`` does not have one column per owned element. """ if local_block.shape[1] != self.n_elements: raise ValueError( "Expected one local column per element for shared DOF collapse." ) global_matrix[:, self.global_index] += np.sum(local_block, axis=1)
[docs] def local_coefficients( self, x: FloatArray, n_elements: int, ) -> FloatArray: """ Return the expanded shared coefficient for each local kernel column. Parameters ---------- x : FloatArray Global solved coefficient vector. n_elements : int Number of elements owned by the family using this map. Returns ------- FloatArray Length-``n_elements`` vector filled with the shared solved coefficient. Raises ------ ValueError If ``self.n_elements`` does not match ``n_elements``. """ if self.n_elements != n_elements: raise ValueError( "SharedGlobalDofMap n_elements must match family n_elements." ) return np.full( n_elements, float(x[self.global_index]), dtype=np.float64, )
[docs] def lift_local_row( self, local_weights: FloatArray, n_unknowns: int, ) -> FloatArray: """ Lift one shared local row into the global unknown space. Parameters ---------- local_weights : FloatArray One-dimensional local weight row with one entry per owned element. n_unknowns : int Length of the global solve vector. Returns ------- FloatArray Global row whose shared coefficient equals the sum of the local weights. Raises ------ ValueError If ``local_weights`` is not one-dimensional, does not match ``n_elements``, or ``n_unknowns`` is too small. """ if local_weights.ndim != 1: raise ValueError("local_weights must be a one-dimensional array.") if local_weights.size != self.n_elements: raise ValueError( "Expected one local weight per element for shared DOF lift." ) if self.global_index >= n_unknowns: raise ValueError("n_unknowns is too small for SharedGlobalDofMap.") row = np.zeros(n_unknowns, dtype=np.float64) row[self.global_index] = float(np.sum(local_weights)) return row