Choosing linear solvers and reusing factors
Configure local and global algebra independently on your existing problem:
from pymhm import SolverConfig, assemble
system = assemble(
problem,
solvers=SolverConfig(local_solver="scipy", global_solver="scipy"),
)
solution = system.solve()
A local mean-constrained Neumann operator and the global skeleton system are generally indefinite. Choose a solver that accepts their actual matrix structure; an elliptic AMG preset is intended for an admissible positive-definite block or projected complement. The table below distinguishes those capabilities.
Select a backend
solve_linear(A, b, solver=...) accepts a finite square dense or sparse matrix
and a vector or multiple right-hand sides. Inputs are normalized to float64 or
complex128. Every returned column must satisfy
This residual check is independent of the backend's stopping report. It does not bound forward error for an ill-conditioned operator. Singular direct solves, iteration failures, nonfinite values and incompatible dimensions raise errors. Requested optional solvers never silently fall back to another backend.
UFL/DOLFINx assembly and numerical solver selection are independent. The
native FEM adapters return SciPy/NumPy operators without importing PETSc;
SolverConfig(local_solver="scipy", global_solver="scipy") uses the same
portable solvers as coefficient-defined problems. Optional solver choices
retain the restrictions below. MPI runtime requirements for DOLFINx meshes
remain even when SciPy or PARDISO performs the solve; distributed PETSc assembly
is a separate capability.
A small residual alone does not establish uniqueness: an unstable trace/local
space combination can have a compatible right-hand side and an almost singular
skeleton. SciPy LU therefore equilibrates rows and columns explicitly and checks
that every pivot exceeds eps * n * max(abs(U)). This is a numerical rank
diagnostic, not proof of exact singularity. Severely ill-conditioned systems may
also be rejected. validate_invertible(A) exposes the same sparse diagnostic for
other backends, and the global hybrid solve applies it before an optional backend
solve. Highly unequal physical scales are balanced before the check; the final
residual is still evaluated in the original equations.
| Backend | Method | Restrictions |
|---|---|---|
scipy |
SuperLU direct LU | Portable real and complex baseline. |
cg |
SciPy conjugate gradients | Hermitian positive definite; symmetry is checked. |
minres |
SciPy MINRES | Real symmetric; indefinite operators are permitted. |
gmres |
SciPy GMRES | Nonsymmetric operators are permitted. |
pypardiso |
Intel MKL PARDISO | Optional native runtime; this wrapper supports real matrices/RHS only. |
pypardiso-symmetric |
Intel MKL PARDISO, symmetric indefinite | Explicit mtype=-2; checked real symmetry and full-original-matrix residuals. Does not require positive definiteness. |
pypardiso-symmetric-matching |
Intel MKL PARDISO, symmetric indefinite | Explicit weighted matching, scaling and 1×1/2×2 pivot profile; original residual checks. |
petsc |
PETSc/MUMPS pivoted LU | Requires PETSc built with MUMPS; uses COMM_SELF. Complex data requires a complex PETSc build. |
petsc-symmetric |
PETSc/MUMPS pivoted symmetric-indefinite LDLt | Checked real symmetry; explicitly disables the SPD assumption. Uses COMM_SELF and checks the full original equations. |
cupy |
CUDA sparse QR | Optional CuPy/CUDA, includes host/device transfers. |
cudss |
NVIDIA cuDSS through nvmath | Optional CuPy/nvmath/cuDSS; reusable device factorization. |
pyamg |
CPU smoothed aggregation + CG | Positive-definite elliptic operators; optional near-nullspace candidates. |
amgx |
GPU aggregation AMG + FGMRES | Real positive-definite elliptic operators; separately installed AmgX/pyamgx. |
Positive diagonals and symmetry are checked for the two AMG presets, but these checks do not prove positive definiteness. Their intended application is an elliptic block or a constrained positive-definite local problem. The full MHM skeleton/kernel system is generally indefinite. Its solution requires an appropriate saddle-point solver or block preconditioner; passing it directly to an elliptic AMG preset is not a supported shortcut.
Native library initialization and worker limits
preload_solver_backend(name) loads only the explicitly selected backend's
libraries. It constructs no explicit numerical factors, AMG hierarchy,
communicator or GPU session. PETSc preparation imports its Python package;
PETSc/MPI initialization remains in the existing numerical adapter.
Call it before a new threadpool_limits context when managing native thread
counts directly, so that every loaded BLAS/OpenMP pool receives that policy.
The ordered local executors prepare their named solver before operator assembly
and before applying native_threads. Providers, compilers and custom solvers can
supply an idempotent prepare_runtime() hook for the same library initialization
step. Numerical resources remain local to their worker and are released through
close() after execution. The hook is optional; portable callables need no
additional protocol. Preparation does not change precision, native parameter
profiles or the residual criterion.
Library preparation leaves operator capability checks in their numerical owners.
An unavailable optional library is reported when an admissible operator actually
requires that backend; unused local spaces need no native solver initialization.
PARDISO native construction, factorization, solve and cleanup entries are serialized within each process. Separate factors retain independent lifetimes; a single factor must not be used concurrently. Independent spawned processes have independent native locks. Linux native tests exercise serial, thread and spawn execution. Windows uses the same portable execution and cleanup contracts; its native runtime qualification is reported by Windows CI separately.
Reusing direct factorizations
import numpy as np
from scipy import sparse
from pymhm.linalg.linear import factorize
A = sparse.csr_matrix([[3.0, 1.0], [1.0, 2.0]])
with factorize(A, solver="scipy") as factors:
first = factors.solve(np.array([1.0, 2.0]))
second = factors.solve(np.eye(2))
factorize supports SciPy, PARDISO, PETSc and cuDSS. The matrix is copied and
factorized once. Repeated RHS solves reuse the factorization. The context manager
releases resources on both success and failure. A factorization is mutable backend
state and must not be used concurrently by several threads. CuPy sparse QR and
AMG/Krylov methods are exposed through solve_linear, not represented as LU
factorizations.
pypardiso-symmetric explicitly selects symmetric-indefinite factorization for
real saddle matrices. It supplies the upper triangle with all diagonal entries,
including structural zeros in multiplier rows. Symmetry is checked relative to
the matrix scale. This profile retains MKL's automatic parameter defaults.
pypardiso-symmetric-matching selects an explicit profile with scaling, weighted
matching, 1×1/2×2 symmetric pivots, METIS ordering, matrix checks and up to five
native refinement steps, with a pivot-perturbation threshold of \(10^{-13}\).
The profiles are separate choices: matching is not uniformly more accurate,
and neither is an automatic fallback for the other. Residuals and correction
equations use the complete original matrix and the requested tolerance.
petsc-symmetric requests the MUMPS symmetric-indefinite factorization through
PETSc's Cholesky interface, with MAT_SPD=False. This selects pivoted LDLt;
it does not assume that a saddle matrix is positive definite. The distinction
follows the PETSc MUMPS adapter.
The ordinary petsc backend retains its LU factorization.
Reusable direct factors also accept equilibration="symmetric". Five Ruiz (2001)
infinity-row sweeps construct a positive diagonal \(D\), and the backend solves
\(DADy=Db\), followed by \(x=Dy\). This explicit congruence preserves symmetry
and the equations; it adds no regularization. Symmetry and nonzero rows are
required. The final residual and all correction equations are checked against
\(A\) and \(b\) in their original units. The default is equilibration="none";
the option is independent of the selected backend's internal scaling. It is also
available through solve_linear for reusable direct backends.
Direct factors also accept
factors.solve(rhs, refinement_precision="extended"). The factorization and
correction solves remain in their backend's double precision; accumulated
corrections and the returned solution use a wider NumPy long-double type.
By default, at most two corrections are attempted, with the same original
residual criterion. The mode is explicit and unavailable on platforms where long double
has no additional precision. It does not change pivot diagnostics, tolerances
or the selected backend. For the bound variational workflow, select
SolverConfig(local_refinement_precision="extended"); local lifts and
reconstructed fields retain their additional digits. Local AMG
projection does not support this option. The same explicit mode is available
through solve_linear(..., refinement_precision="extended") for both Krylov
backends and reusable direct factors (SciPy, PARDISO, PETSc and cuDSS).
The explicit refinement_steps argument controls the maximum number of outer
defect corrections in factors.solve and solve_linear. It accepts a
nonnegative integer and defaults to two. Zero checks only the initial backend
solution; a larger limit reuses the same factorization or Krylov preconditioner.
For example, factors.solve(rhs, refinement_precision="extended",
refinement_steps=8) permits eight corrections while preserving the requested
residual criterion. Acceptance can occur earlier; exhaustion, stagnation or
divergence still raises LinearSolveError. This limit is separate from the
backend's internal refinement settings. AmgX reuses its prepared GPU hierarchy
for these original-equation corrections. CuPy sparse QR exposes no outer
correction loop and rejects nondefault limits.
HybridSystem.solve(refinement_precision="extended") independently enables
extended accumulation for the global solve and reconstructed coefficients.
Its optional rtol sets the global algebraic tolerance explicitly. With rtol=None,
a new factor uses the default tolerance and an existing prepared factor preserves
its own tolerance; an explicit conflicting tolerance is rejected. Extended
accumulation alone does not force corrections after the global residual has
already passed. Local physical balances must still meet their separate checks.
Local and global refinement precision are independent:
SolverConfig(global_refinement_precision="extended") selects wider global
accumulation in the bound workflow. Wider accumulation does not make the
factorization exact or guarantee accuracy for every condition number.
refine_hybrid(system, solution, ...) optionally corrects the complete original
local and weak-trace equations after condensation. It supports distinct test
and trial spaces, several retained modes, prescribed trace coefficients and
physical mean constraints. Supply the same boundary_load, fixed data and
moments=[(local_weights, target), ...] used in the original problem. The defect
loads are solved with the original local operators and harmonic lifts; the
method and approximation spaces are unchanged. Local factors are released
cellwise, and the full uncondensed matrix is never assembled.
Acceptance uses the norm of the original local rows, free weak-trace rows and
supplied physical moments. Its right-hand-side norm includes prescribed-trace
elimination in the same input units. This algebraic quantity is separate from
finite-element error norms. HybridRefinement.residual_norms records the initial
residual and each correction; solution holds the resulting fields. Persist
these fields together with their evaluation basis. Reconstructing only from the
corrected trace/coarse coefficients and the original lifts omits the additional
defect-source responses. The returned solution.residual evaluates the original
free weak-trace and retained-test rows on the corrected fields; it excludes
artificial gauge rows. Gauge multipliers accumulate all corrections. max_steps=0
checks the initial state only. Exhaustion raises LinearSolveError with the
achieved residual and threshold. The default hybrid solve does not invoke this
additional work. Explicit refinement_precision="extended" retains correction
digits and has the same platform requirement as extended direct refinement.
For prescribed trace values, the final physical-equation check includes a componentwise bound on rounding in their elimination. In row \(i\), with \(m_i\) prescribed nonzero entries, the bound is \(\gamma_{m_i+1}(|A_{FD}|\,|x_D|)_i\), where \(\gamma_n=n\epsilon/(1-n\epsilon)\). This accounts for cancellation of balanced Neumann data without assigning a relative tolerance to each large boundary contribution. Incompatible physical loads above this rounding bound remain errors, including when the resulting net load is small.
The PETSc adapter explicitly selects MUMPS. Local kernel constraints, mixed formulations and the global pressure gauge introduce saddle blocks with zero or structurally absent diagonal entries. PETSc's internal sequential LU is not a general replacement for a pivoted factorization of these matrices. The adapter checks that PETSc was built with MUMPS and raises an actionable error if it was not; it does not shift the diagonal or substitute another backend. This package does not expose PETSc options that change this preset. See the official PETSc MUMPS interface.
The cuDSS preset enables maximum diagonal-product matching and five iterative refinement steps for zero-diagonal saddle blocks. Every solution must satisfy the same residual criterion as the other backends.
For local multiscale basis construction, one factorization solves the source and all trace right-hand sides together. A GPU factorization can therefore amortize setup over several local basis vectors, but transfer and setup costs remain significant for small local problems. See the measured performance cases.
CPU algebraic multigrid
import numpy as np
import pyamg
from pymhm.linalg.linear import solve_linear
A = pyamg.gallery.poisson((32, 32), format="csr")
b = np.ones(A.shape[0])
x = solve_linear(
A,
b,
solver="pyamg",
near_nullspace=np.ones((A.shape[0], 1)),
)
The candidate columns are the slowly varying modes that the hierarchy should represent on coarse levels. Scalar diffusion often uses constants; elasticity needs appropriate rigid-motion candidates after essential constraints have been applied. These are near-nullspace candidates, not automatic removal of an exact singular kernel. MHM mean constraints must still be imposed consistently.
The preset constructs one smoothed-aggregation hierarchy for all supplied RHS columns and applies its V-cycle as a SciPy CG preconditioner. It uses local Jacobi prolongator weighting to avoid the random spectral-radius estimate used by the default diagonal-weighted prolongator. This is a fixed, inspectable preset; problem-dependent AMG tuning and block saddle preconditioners remain separate work. See PyAMG's aggregation API.
GPU algebraic multigrid
solver="amgx" uses NVIDIA AmgX through pyamgx with device-double mode dDDI,
FGMRES, one aggregation V-cycle per preconditioner application, block-Jacobi
smoothing and a dense coarse solve. Matrix/RHS transfer, hierarchy setup and
resource destruction are included in the call. One hierarchy is reused across
RHS columns and independently checked true-residual corrections. The original
operator and each original RHS define the residual criterion, including zero
forcing. Relative-tolerance solves normalize every nonzero column by its largest
absolute entry and undo this scale in the solution. This preserves the equations
while keeping small physical loads above the native convergence test's
absolute stopping floor.
The original residual acceptance criterion is unchanged.
For repeated loads on one fixed elliptic operator, prepare the hierarchy explicitly:
from pymhm.linalg.linear import prepare_amgx
with prepare_amgx(A, rtol=1e-10) as prepared:
solution = prepared.solve(rhs_columns)
another_solution = prepared.solve(another_rhs, refinement_steps=4)
The hierarchy belongs to that copied operator. A different material matrix needs a different setup. It accepts real symmetric operators with positive diagonals; the caller must establish positive definiteness. Closing the context releases all native vectors, solver, matrix, resource and configuration handles.
For MHM Neumann locals, the shared condensation owner projects compatibility,
pins one independent coordinate per declared kernel mode and restores the
declared physical moment constraints after each correction. AmgX receives the
pinned elliptic operator. Source, oriented trace and finite-precision retained
mode responses share one hierarchy across every projected correction pass.
Compatibility pairings, source moments and gauge restoration use the same
extended or compensated accumulator as the original residual checks; native
corrections and response storage remain double precision. Every permitted
correction, including the fourth, is checked before acceptance.
Their projected original-equation target is 1e-12; each pinned solve keeps its
1e-10 criterion. Reconstructed fields also require an independent check of
the original physical equations; the reduced global residual is insufficient.
Trial and test kernels/moments must match, and a general coarse basis is outside
this local AMG interface.
AmgX and pyamgx have separate native build requirements. They are not silently installed by the standard Python package extra. Follow the NVIDIA build instructions and pyamgx installation guide. A working CUDA runtime and matching native library are required.
The adapter owns AmgX's process-global initialization/finalization and serializes
its own resource lifecycles. Do not overlap it with an independently managed
pyamgx session in the same process. Each prepared hierarchy uses one GPU.
Several independent local problems can be assigned to separate spawned GPU
processes, each setting CUDA_VISIBLE_DEVICES before its first CUDA import.
Within one process the lifecycle lock remains held until the prepared hierarchy
is closed. The adapter uses the stated fixed preset and does not accept external
AmgX configuration dictionaries.
Combine solver choice with execution
Pass SolverConfig(local_solver=..., global_solver=...) to assemble(problem).
The CPU guide configures serial, thread or spawned work;
MPI distributes the global algebra, and
CUDA assigns local matrices to devices.
For unchanged operators and changing loads, use
offline preparation.
Solver selection does not change quadrature, approximation spaces or material.
AMG block preconditioners for mixed saddle systems
pymhm.linalg.block.SaddleBlockSolver accepts a real symmetric matrix partitioned as
The split argument identifies the contiguous first block. GMRES acts on the
original matrix. Its block-diagonal preconditioner uses
Both preconditioner blocks must be numerically positive definite. The explicit
augmentation changes only the preconditioner. In particular, it can control the
kernel of a semidefinite first block through the coupling. The automatic positive
scale balances the largest entry of A against the largest diagonal of BB.T;
augmentation= may specify a different positive scale. Unsupported block
structures are rejected. This is not a general AMG solver for indefinite inputs.
from pymhm.linalg.block import SaddleBlockSolver
with SaddleBlockSolver(system.matrix, system.trace_size, backend="pyamg") as block:
solution = system.solve(factorization=block.as_factorization())
The example applies directly to an unconstrained condensed Darcy system whose skeleton/retained ordering has that block form. For eliminated boundary DOFs or additional gauges, prepare the actual constrained matrix and its appropriate block split; the exact-matrix factorization contract rejects a mismatched matrix.
backend="pyamg" retains a CPU smoothed-aggregation hierarchy. backend="amgx"
retains an NVIDIA AmgX aggregation hierarchy on the GPU. Each preconditioner
application performs one V-cycle on P; S uses a reusable sparse LU. Outer
Krylov operations, the Schur block and numerical checks remain on the CPU.
The GPU path uploads/downloads block vectors at every application. Native GPU
resources belong to the solver context and are released on exit.
For several source columns, reuse the same prepared block solver through its context manager. Its factorization must match the actual constrained matrix; selecting a block split from an unconstrained layout after boundary elimination is invalid. Original-equation residual checks apply to every returned column.
Performance measurements and physical applications are in the Gallery. This guide specifies solver capabilities and configuration rather than selecting a fastest backend for all problem sizes.