Source code for femorph_solver.solvers.linear._base
"""Common base class for pluggable linear solvers."""
from __future__ import annotations
from typing import ClassVar
import numpy as np
import scipy.sparse as sp
# Re-export ``SolverUnavailableError`` from the canonical exceptions
# module so callers can ``except SolverUnavailableError`` from either
# import path; new code should prefer ``from femorph_solver.exceptions
# import SolverUnavailableError``.
from femorph_solver.exceptions import SolverUnavailableError # noqa: F401
[docs]
class LinearSolverBase:
"""Protocol every linear-solver backend implements.
Concrete subclasses set the class attributes (name, kind, spd_only,
install_hint), override :meth:`available`, and implement
:meth:`_factor` and :meth:`solve`.
"""
#: Short identifier used by ``get_linear_solver("...")``.
name: ClassVar[str] = ""
#: ``"direct"`` (factor + forward/back solve) or ``"iterative"``.
kind: ClassVar[str] = "direct"
#: ``True`` if the solver requires a symmetric positive-definite matrix.
spd_only: ClassVar[bool] = False
#: User-facing hint printed when the backend is unavailable.
install_hint: ClassVar[str] = ""
def __init__(
self,
A: sp.spmatrix | sp.sparray,
*,
assume_spd: bool = False,
assume_hermitian: bool = False,
mixed_precision: bool | None = False,
) -> None:
"""``mixed_precision``:
* ``False`` (default) — factor and solve in double precision.
* ``True`` — factor in single precision and refine in double
(for backends that support it; no-op elsewhere). Halves the
factor footprint when the backend honours the request.
* ``None`` — let the backend choose the default based on the
problem (``assume_spd`` + size heuristic); the
:class:`PardisoSolver` is currently the only backend that
opts into MP here, and only when the input is large enough
for the memory saving to matter.
``assume_hermitian``:
* ``False`` (default) — treat ``A`` as general (real
unsymmetric or complex unsymmetric).
* ``True`` — caller asserts ``A`` is Hermitian (real
symmetric or complex Hermitian). Backends that expose a
Hermitian fast path (Pardiso ``mtype=4`` for complex
Hermitian; ``mtype=-2`` for real symmetric indefinite)
may pick it up. Real SPD callers should keep using
``assume_spd=True`` instead — that's a stricter assertion
(positive-definite implies Hermitian) and unlocks the
Cholesky path.
"""
self._n = A.shape[0]
self._assume_spd = assume_spd
self._assume_hermitian = assume_hermitian
self._mixed_precision = mixed_precision
self._factor(A)
[docs]
@staticmethod
def available() -> bool:
"""Return ``True`` if the solver can be constructed on this install."""
return True
def _factor(self, A: sp.spmatrix | sp.sparray) -> None:
"""Precompute any factorization / preconditioner.
Subclasses override. Called once by :meth:`__init__`.
"""
raise NotImplementedError
[docs]
def solve(self, b: np.ndarray) -> np.ndarray:
"""Solve ``A x = b`` for a single RHS."""
raise NotImplementedError
[docs]
def refactor(self, A: sp.spmatrix | sp.sparray) -> None:
"""Update the numeric factor for a new ``A`` with the same sparsity.
Contract: ``A`` must have **identical sparsity pattern**
(``indptr`` + ``indices``) to the matrix this solver was
originally constructed on; only ``A.data`` differs. Used by
sweep-style callers (e.g. cyclic-symmetry harmonics, parametric
modal sweeps) to amortise the symbolic factor across many
numeric refactorisations.
Default implementation re-runs :meth:`_factor` from scratch —
backends that expose a numeric-only refactor hook (Pardiso
phase 22, UMFPACK ``numeric()`` reusing ``symbolic()``)
override and skip the analysis cost.
Callers must not mutate ``A``'s pattern; backends that override
won't notice and will produce wrong results silently.
"""
self._factor(A)