Skip to main content

feax.assembler

Assembler functions that work with Problem and TracedParams.

This module provides the main assembler API for finite element analysis with separated internal variables. It handles the assembly of residual vectors and Jacobian matrices for both volume and surface integrals, supporting various physics kernels (Laplace, mass, surface, and universal).

classify_volume_var

def classify_volume_var(problem: 'Problem', var: np.ndarray)

Classify a volume internal variable by its storage kind.

The decision is made from the original global array shape (before per-cell gathering), so it is unambiguous and resolved statically at trace-build time.

Returns a (kind, fe_idx) pair where kind is one of _VAR_NODE/_VAR_CELL/_VAR_QUAD (or None for an unrecognized layout → caller falls back to legacy sniffing), and fe_idx is the index of the finite-element variable whose shape functions interpolate a node-based variable. fe_idx matters only for _VAR_NODE (node-based fields are interpolated with that variable's shape functions); it is 0 otherwise. This is what makes mixed element-type multi-variable problems interpolate each internal variable on its own mesh rather than always on variable 0's.

Operator Objects

class Operator()

Element-level operator for quadrature-point computations.

Provides methods for interpolating solutions, computing gradients, interpolating internal variables, and integrating over quadrature points. Used internally by kernel functions to eliminate code duplication.

Parameters

  • problem (Problem): The finite element problem.
  • fe_index (int): Index of the finite element variable (default 0).

eval

def eval(cell_sol: np.ndarray, shape_vals: np.ndarray = None) -> np.ndarray

Interpolate nodal solution to quadrature points.

Parameters

  • cell_sol (np.ndarray): Nodal solution values, shape (num_nodes, vec).
  • shape_vals (np.ndarray, optional): Shape function values. Uses volume shape functions if None.

Returns

np.ndarray Solution at quadrature points, shape (num_quads, vec).

grad

def grad(cell_sol: np.ndarray, cell_shape_grads: np.ndarray) -> np.ndarray

Compute solution gradient at quadrature points.

Parameters

  • cell_sol (np.ndarray): Nodal solution values, shape (num_nodes, vec).
  • cell_shape_grads (np.ndarray): Shape function gradients, shape (num_quads, num_nodes, dim).

Returns

np.ndarray Gradient at quadrature points, shape (num_quads, vec, dim).

hess

def hess(cell_sol: np.ndarray, cell_shape_hessians: np.ndarray) -> np.ndarray

Compute solution Hessian (second spatial derivatives) at quadrature points.

Parameters

  • cell_sol (np.ndarray): Nodal solution values, shape (num_nodes, vec).
  • cell_shape_hessians (np.ndarray): Shape function second derivatives in physical coordinates, shape (num_quads, num_nodes, dim, dim).

Returns

np.ndarray Hessian at quadrature points, shape (num_quads, vec, dim, dim). H[q, i, K, L] = sum_a u[a, i] * d²h_a/(dX_K dX_L) at quad point q.

interpolate_var

def interpolate_var(var: np.ndarray,
kind: str = None,
fe_idx: int = 0) -> np.ndarray

Interpolate a single internal variable to quadrature points.

Handles node-based (shape function interpolation), cell-based (broadcast), and quad-based (pass-through) variables.

Parameters

  • var (np.ndarray): Internal variable for a single element (post-gather, post-slice).
  • kind (str, optional): Static storage tag (_VAR_NODE/_VAR_CELL/_VAR_QUAD) classified from the original global shape via :func:classify_volume_var. When provided, interpolation dispatches on the tag with no runtime shape inspection. When None, falls back to the legacy shape-sniffing path (kept for backward compatibility / unclassifiable shapes).
  • fe_idx (int, default 0): For _VAR_NODE variables, the finite-element variable whose shape functions interpolate this field. Lets a node-based internal var tied to variable i be interpolated on variable i's mesh even when this Operator belongs to a different variable (mixed element types). Ignored for cell/quad layouts.

Returns

np.ndarray Values at quadrature points, shape (num_quads,).

interpolate_vars

def interpolate_vars(cell_internal_vars: Tuple[np.ndarray, ...],
kinds: Tuple = None) -> List[np.ndarray]

Interpolate all internal variables to quadrature points.

Parameters

  • cell_internal_vars (tuple of np.ndarray): Internal variables for a single element.
  • kinds (tuple of (str, int), optional): Static (kind, fe_idx) descriptors, one per variable, from :func:classify_volume_var (see :meth:interpolate_var). When None, every variable uses the legacy sniffing path.

Returns

list of np.ndarray Interpolated values at quadrature points.

integrate_grad

def integrate_grad(quad_values: np.ndarray,
cell_v_grads_JxW: np.ndarray) -> np.ndarray

Integrate tensor quad-point values against test function gradients.

Computes: sum_q sigma(q) : grad_v(q) * JxW(q)

Parameters

  • quad_values (np.ndarray): Physics values at quad points, shape (num_quads, vec, dim).
  • cell_v_grads_JxW (np.ndarray): Pre-multiplied test function gradients × JxW, shape (num_quads, num_nodes, vec, dim).

Returns

np.ndarray Element contribution, shape (num_nodes, vec).

integrate_val

def integrate_val(quad_values: np.ndarray,
cell_JxW: np.ndarray,
shape_vals: np.ndarray = None) -> np.ndarray

Integrate scalar/vector quad-point values with shape functions.

Computes: sum_q f(q) * N(q) * JxW(q)

Parameters

  • quad_values (np.ndarray): Physics values at quad points, shape (num_quads, vec).
  • cell_JxW (np.ndarray): Jacobian determinant × quadrature weights, shape (num_quads,).
  • shape_vals (np.ndarray, optional): Shape function values. Uses volume shape functions if None.

Returns

np.ndarray Element contribution, shape (num_nodes, vec).

gather_internal_vars

@staticmethod
def gather_internal_vars(problem: 'Problem',
traced_params: Tuple[np.ndarray, ...],
ts=None) -> List[np.ndarray]

Gather global internal variables to per-cell format.

Transforms node-based variables from global (num_nodes,) arrays to per-cell (num_cells, num_nodes_per_elem) arrays using element connectivity. Cell-based and quad-based variables are passed through.

Parameters

  • problem (Problem): The finite element problem with connectivity information.
  • traced_params (tuple of np.ndarray): Global internal variables.

Returns

list of np.ndarray Per-cell internal variables ready for vmapped kernels.

interpolate_to_quad_points

def interpolate_to_quad_points(var: np.ndarray, shape_vals: np.ndarray,
num_cells: int, num_quads: int) -> np.ndarray

Interpolate node-based or cell-based values to quadrature points.

.. deprecated:: Use :meth:Operator.interpolate_var instead.

This function handles three cases:

  1. Node-based: shape (num_nodes,) -> interpolate using shape functions
  2. Cell-based: shape (num_cells,) -> broadcast to all quad points in cell
  3. Quad-based: shape (num_cells, num_quads) -> pass through (legacy)

Parameters

  • var (np.ndarray): Variable to interpolate. Can be: - (num_nodes,) for node-based - (num_cells,) for cell-based - (num_cells, num_quads) for quad-based (legacy)
  • shape_vals (np.ndarray): Shape function values at quadrature points, shape (num_quads, num_nodes)
  • num_cells (int): Number of cells/elements
  • num_quads (int): Number of quadrature points per cell

Returns

np.ndarray Values at quadrature points, shape (num_quads,)

get_laplace_kernel

def get_laplace_kernel(problem: 'Problem',
tensor_map: Callable,
var_kinds: Tuple[str, ...] = None) -> Callable

Create Laplace kernel function for gradient-based physics.

The Laplace kernel handles gradient-based terms in the weak form, such as those arising in elasticity, heat conduction, and diffusion problems. It implements the integral term: ∫ σ(∇u) : ∇v dΩ where σ is the stress/flux tensor computed from the gradient.

Parameters

  • problem (Problem): The finite element problem containing mesh and element information.
  • tensor_map (Callable): Function that maps gradient tensor to stress/flux tensor. Signature: (u_grad: ndarray, *traced_params) -> ndarray where u_grad has shape (vec, dim) and returns (vec, dim).

Returns

Callable Laplace kernel function that computes the contribution to the weak form from gradient-based physics.

get_mass_kernel

def get_mass_kernel(problem: 'Problem',
mass_map: Callable,
var_kinds: Tuple[str, ...] = None) -> Callable

Create mass kernel function for non-gradient terms.

The mass kernel handles terms without derivatives in the weak form, such as mass matrices, reaction terms, or body forces. It implements the integral term: ∫ m(u, x) · v dΩ where m is a mass-like term.

Parameters

  • problem (Problem): The finite element problem containing mesh and element information.
  • mass_map (Callable): Function that computes the mass term. Signature: (u: ndarray, x: ndarray, *traced_params) -> ndarray where u has shape (vec,), x has shape (dim,), and returns (vec,).

Returns

Callable Mass kernel function that computes the contribution to the weak form from non-gradient physics.

get_surface_kernel

def get_surface_kernel(problem: 'Problem', surface_map: Callable) -> Callable

Create surface kernel function for boundary integrals.

The surface kernel handles boundary integrals in the weak form, such as surface tractions, pressures, or fluxes. It implements the integral term: ∫ t(u, x) · v dΓ where t is the surface load/flux.

Parameters

  • problem (Problem): The finite element problem containing mesh and element information.
  • surface_map (Callable): Function that computes the surface traction/flux. Signature: (u: ndarray, x: ndarray, *traced_params) -> ndarray where u has shape (vec,), x has shape (dim,), and returns (vec,).

Returns

Callable Surface kernel function that computes the contribution to the weak form from boundary loads/fluxes.

create_volume_kernel

def create_volume_kernel(problem: 'Problem',
var_kinds: Tuple = None) -> Callable

Create the unified volume kernel for residual / Jacobian assembly.

Composition rules (consistent for single- and multi-variable problems):

  1. Base element residual (full physics, chosen exclusively):

    • get_universal_kernel() — if defined, it is the complete element residual (full low-level control); the standard pieces below are not added. This is the same "escape hatch" meaning in both single- and multi-variable problems.
    • otherwise, for multi-variable problems: get_weak_form() (raises if neither is defined).
    • otherwise, for single-variable problems: get_tensor_map() (or get_energy_density() differentiated via jax.grad) plus get_mass_map() — these are complementary standard pieces and are summed.
  2. Extra additive kernel (optional): get_extra_kernel() is always added on top of the base, regardless of which base was chosen. Use it for complementary low-level terms (e.g. a regularization or stabilization term layered on standard physics) — the additive counterpart of the full-replacement get_universal_kernel().

Parameters

  • problem (Problem): Defines some subset of get_tensor_map / get_energy_density / get_mass_map / get_weak_form / get_universal_kernel / get_extra_kernel.

Returns

Callable Element kernel base(...) + extra(...).

create_surface_kernel

def create_surface_kernel(problem: 'Problem', surface_index: int) -> Callable

Create unified surface kernel for a specific boundary.

This function creates a kernel that combines contributions from standard surface maps and universal surface kernels for a specific boundary surface identified by surface_index.

Parameters

  • problem (Problem): The finite element problem that may define get_surface_maps() and/or get_universal_kernels_surface() methods.
  • surface_index (int): Index identifying which boundary surface this kernel is for. Corresponds to the index in problem.location_fns.

Returns

Callable Combined surface kernel function for the specified boundary.

Notes

Multiple boundaries can have different physics. The surface_index parameter selects which surface map and universal kernel to use.

For multi-variable problems, only universal_kernels_surface should be used, as get_surface_maps() only supports single-variable problems.

split_and_compute_cell

def split_and_compute_cell(problem: 'Problem',
cells_sol_flat: np.ndarray,
jac_flag: bool,
internal_vars_volume: Tuple[np.ndarray, ...],
ts=None) -> Any

Compute volume integrals for residual or Jacobian assembly.

This function evaluates volume integrals over all elements, optionally computing the Jacobian via forward-mode automatic differentiation. It uses batching to manage memory for large meshes.

Parameters

  • problem (Problem): The finite element problem containing mesh and quadrature data.
  • cells_sol_flat (np.ndarray): Flattened solution values at element nodes. Shape: (num_cells, num_nodes * vec).
  • jac_flag (bool): If True, compute both values and Jacobian. If False, compute only values.
  • internal_vars_volume (tuple of np.ndarray): Material properties at quadrature points for each variable. Each array has shape (num_cells, num_quads).

Returns

np.ndarray or tuple of np.ndarray If jac_flag is False: weak form values with shape (num_cells, num_dofs). If jac_flag is True: tuple of (values, jacobian) where jacobian has shape (num_cells, num_dofs, num_dofs).

Notes

The function splits computation into batches (default 20) to avoid memory issues with large meshes. This is particularly important for 3D problems.

compute_face

def compute_face(problem: 'Problem',
cells_sol_flat: np.ndarray,
jac_flag: bool,
internal_vars_surfaces: List[Tuple[np.ndarray, ...]],
ts=None) -> Any

Compute surface integrals for residual or Jacobian assembly.

This function evaluates surface integrals over all boundary faces, optionally computing the Jacobian via forward-mode automatic differentiation.

Parameters

  • problem (Problem): The finite element problem containing boundary information.
  • cells_sol_flat (np.ndarray): Flattened solution values at element nodes. Shape: (num_cells, num_nodes * vec).
  • jac_flag (bool): If True, compute both values and Jacobian. If False, compute only values.
  • internal_vars_surfaces (list of tuple of np.ndarray): Surface variables for each boundary. Each entry corresponds to one boundary surface and contains arrays with shape (num_surface_faces, num_face_quads).

Returns

list of np.ndarray or list of tuple If jac_flag is False: list of weak form values for each boundary. If jac_flag is True: list of (values, jacobian) tuples for each boundary.

Notes

Each boundary surface can have different loading conditions or physics, handled through separate surface kernels and internal variables.

compute_residual_vars_helper

def compute_residual_vars_helper(problem: 'Problem',
weak_form_flat: np.ndarray,
weak_form_face_flat: List[np.ndarray],
ts=None) -> List[np.ndarray]

Assemble residual from element and face contributions.

This helper function assembles the global residual vector by accumulating contributions from volume and surface integrals at the appropriate nodes.

Parameters

  • problem (Problem): The finite element problem containing connectivity information.
  • weak_form_flat (np.ndarray): Flattened weak form values from volume integrals. Shape: (num_cells, num_dofs_per_cell).
  • weak_form_face_flat (list of np.ndarray): Weak form values from surface integrals for each boundary. Each array has shape (num_boundary_faces, num_dofs_per_face).

Returns

list of np.ndarray Global residual for each solution variable. Each array has shape (num_total_nodes, vec).

Notes

Volume and all boundary contributions are concatenated into one value array and reduced with a single sorted segment_sum over the precomputed DOF scatter map (problem.res_perm / res_sorted_dofs). This replaces the 1 + num_boundaries scatter-adds with one deterministic reduction (no atomics), matching the CSR-direct Jacobian assembly.

get_jacobian

def get_jacobian(problem: 'Problem',
sol_list: List[np.ndarray],
traced_params: TracedParams,
ts=None) -> 'CSRMatrix'

Assemble the global tangent (Jacobian) as a :class:feax.csr.CSRMatrix.

Companion to :func:get_res (which assembles the global residual): this assembles the element tangents into the global Jacobian without applying Dirichlet boundary conditions, returned as the deduplicated CSR triple (data, indptr, indices) wrapped in a :class:~feax.csr.CSRMatrix. It is the entry point for callers that need the raw assembled operator — e.g. building the material/geometric stiffness pair (K, K_g) for the linear-buckling eigensolver (:func:feax.solvers.eigen.create_linear_buckling_solver).

The assembly is already CSR-native (:func:_get_J_csr); returning the CSR matrix directly avoids the redundant BCOO round-trip (stack to COO-style indices here, re-sort back to CSR in the consumer). CSRMatrix supports @ (mat-vec), .todense(), .T, etc.

For the solver stack, prefer the CSR-direct, BC-applied assembly (:func:get_res9); for cheap statistics without materializing the matrix, use :func:``0.

Parameters

  • problem (Problem): The finite element problem containing mesh and physics definitions.
  • sol_list (list of np.ndarray): Solution arrays for each variable.
  • traced_params (TracedParams): Container with material properties and loading parameters.
  • ts (TracedStructure, optional): When given, assembles on the TracedStructure path (avoids the no-TracedStructure host slot maps / deprecation warning). Omit it on a problem whose host scratch is still alive (TracedStructure.from_problem(problem, free_scratch=False)).

Returns

feax.csr.CSRMatrix The assembled global Jacobian (no BCs applied), shape (num_total_dofs_all_vars, num_total_dofs_all_vars), with nse deduplicated nonzeros.

get_jacobian_info

def get_jacobian_info(problem: 'Problem', sol_list: List[np.ndarray],
traced_params: TracedParams) -> dict

Get Jacobian matrix information without full matrix construction.

This function provides safe access to Jacobian statistics that works correctly with JIT-compiled solvers and cuDSS backend. Unlike the full assembly in :func:get_jacobian, this does not cause GPU memory conflicts.

Parameters

  • problem (Problem): The finite element problem definition.
  • sol_list (list of np.ndarray): Solution arrays for each variable.
  • traced_params (TracedParams): Internal variables container.

Returns

dict Dictionary containing:

  • 'nnz': Number of non-zero entries (int)
  • 'shape': Matrix shape (tuple)
  • 'matrix_view': Matrix storage format (MatrixView enum)

Examples

>>> info = get_jacobian_info(problem, sol_list, traced_params)
>>> print(f"Jacobian NNZ: {`info['nnz']:,`}")
>>> print(f"Matrix view: {`info['matrix_view'].name`}")

Notes

This function is safe to call from user code and does not interfere with JIT-compiled solvers using cuDSS backend.

get_res

def get_res(problem: 'Problem',
sol_list: List[np.ndarray],
traced_params: TracedParams,
ts=None) -> List[np.ndarray]

Compute residual vector with separated internal variables.

Assembles the global residual vector by evaluating the weak form at the current solution state. Includes contributions from both volume and surface integrals.

Parameters

  • problem (Problem): The finite element problem containing mesh and physics definitions.
  • sol_list (list of np.ndarray): Solution arrays for each variable. Each array has shape (num_total_nodes, vec).
  • traced_params (TracedParams): Container with material properties and loading parameters.

Returns

list of np.ndarray Residual arrays for each solution variable. Each array has shape (num_total_nodes, vec).

Examples

>>> residual = get_res(problem, [solution], traced_params)
>>> res_norm = np.linalg.norm(jax.flatten_util.ravel_pytree(residual)[0])
>>> print(f"Residual norm: {`res_norm`}")

Notes

The residual represents the imbalance in the weak form equations. For converged solutions, the residual should be near zero.

create_J_bc_csr_function

def create_J_bc_csr_function(
problem: 'Problem',
bc: 'DirichletBC',
symmetric: bool = True
) -> Callable[[np.ndarray, TracedParams], 'CSRMatrix']

Assemble the BC-applied Jacobian directly as a deduplicated CSRMatrix.

Returns (sol_flat, traced_params) -> CSRMatrix that assembles the BC-applied Jacobian straight into deduplicated CSR form — no BCOO, no per-solve sum_duplicates sort — using the slot map precomputed in :meth:Problem._build_csr_assembly_structure, ready for direct backends (cuDSS/cholmod/umfpack/spsolve) without conversion.

create_J_bc_csr_parametric

def create_J_bc_csr_parametric(problem: 'Problem',
symmetric: bool = True) -> Callable

Parametric form of :func:create_J_bc_csr_function.

Like :func:create_J_bc_csr_function but takes bc as an explicit third argument so it traces through the BC pytree (vmap / per-step BC values).

create_res_J_bc_csr_parametric

def create_res_J_bc_csr_parametric(problem: 'Problem',
symmetric: bool = True) -> Callable

Fused BC-applied residual + CSR Jacobian, bc as an explicit argument.

Returns (sol_flat, traced_params, bc) -> (res_bc, J_bc_csr) computed from a single element-kernel pass (see :func:_get_res_J_csr) — used by the Newton step so it does not evaluate the volume kernel twice (once for the residual, once for the Jacobian).

create_matfree_res_J_parametric

def create_matfree_res_J_parametric(problem: 'Problem',
symmetric: bool = True) -> Callable

Matrix-free counterpart of :func:create_res_J_bc_csr_parametric (Krylov).

Returns (sol_flat, traced_params, bc) -> (res_bc, J_matvec) where J_matvec(v) applies the BC-eliminated tangent without assembling it: the bulk action K @ v is a forward-mode jax.jvp of the residual, and the Dirichlet rows/columns are handled by masking. It reproduces apply_boundary_to_J_csr(_get_J_csr(...)) @ v exactly. For symmetric BC the operator is symmetric (Jᵀ = J), so the same matvec serves the adjoint.

This is the operator the Krylov (cg/bicgstab/gmres) solvers consume — they never need the matrix entries, only this matvec, so the CSR assembly is skipped entirely on the iterative path.

create_matfree_Kt_parametric

def create_matfree_Kt_parametric(problem: 'Problem') -> Callable

Matrix-free K_bulk^T (un-eliminated residual transpose) for the symmetric-BC adjoint correction.

Returns (sol_flat, traced_params) -> (w -> K_bulk^T @ w) via reverse-mode jax.vjp of the residual — used to recover the correct bc_vals gradient without assembling the bulk Jacobian.

create_res_bc_function

def create_res_bc_function(
problem: 'Problem',
bc: 'DirichletBC') -> Callable[[np.ndarray, TracedParams], np.ndarray]

Create residual function with Dirichlet BC applied.

Returns a function that computes the residual vector with Dirichlet boundary conditions enforced. The BC application zeros out residuals at constrained DOFs.

Parameters

  • problem (Problem): The finite element problem definition.
  • bc (DirichletBC): Dirichlet boundary condition specifications.

Returns

Callable Function with signature (sol_flat, traced_params) -> np.ndarray that returns the BC-modified residual vector.

Notes

The returned function is used in Newton solvers to find solutions that satisfy both the weak form equations and boundary conditions.

create_res_bc_parametric

def create_res_bc_parametric(problem: 'Problem') -> Callable

Create a residual function that takes bc as an explicit argument.

Unlike :func:create_res_bc_function which captures bc in a closure, this version accepts bc as a third argument. This enables a single JIT-compiled function to be reused across time steps where only the prescribed BC values change (same DOF locations, different values).

Parameters

  • problem (Problem): The finite element problem definition.

Returns

Callable Function with signature (sol_flat, traced_params, bc) -> np.ndarray.

create_energy_fn

def create_energy_fn(problem) -> Callable

Create a total-energy integration function from a feax Problem.

Builds a pure JAX function that integrates the problem's energy density over the domain::

E(u) = ∫ ψ(∇u, *traced_params) dΩ

The energy density is obtained from problem.get_energy_density(). This is the same density the residual assembler differentiates (tensor_map = jax.grad(energy_density)) — :func:create_energy_fn exposes the scalar energy itself, which is useful for objective evaluation (e.g. compliance / stored energy) and energy-based post-processing.

Parameters

  • problem (feax.Problem): A problem defining get_energy_density() (must return non-None).

Returns

  • energy (callable): energy(u_flat) or energy(u_flat, traced_params) returning a scalar. Without traced_params the density receives only ∇u; with it, each volume variable is interpolated to quadrature points (node-based via shape functions, cell-based by broadcast) and passed as extra arguments ψ(∇u, var0_q, var1_q, …).

Raises

ValueError If problem.get_energy_density() returns None.