Source code for femorph_solver.solvers.linear._cholmod
"""CHOLMOD sparse Cholesky via ``scikit-sparse``.
For SPD ``A`` (stiffness matrices after BCs, for example), CHOLMOD's
supernodal Cholesky is ~2-3× faster than SuperLU's LU and uses about
half the memory.
References
----------
- Chen, Y., Davis, T. A., Hager, W. W., & Rajamanickam, S.
"Algorithm 887: CHOLMOD, Supernodal Sparse Cholesky Factorization
and Update/Downdate." ACM Trans. Math. Softw. 35 (3), 22:1-22:14
(2008). [the shipped supernodal algorithm + its update paths]
- Davis, T. A. & Hager, W. W. "Dynamic supernodes in sparse
Cholesky update/downdate and triangular solves." ACM Trans.
Math. Softw. 35 (4), 27:1-27:23 (2009).
[the row-and-column modification path used when updating
factorisations across parametric sweeps]
"""
from __future__ import annotations
from typing import ClassVar
import numpy as np
import scipy.sparse as sp
from ._base import LinearSolverBase
_HAVE = True
try:
# scikit-sparse ≥0.5 renamed the factorization entry point to
# ``cho_factor`` (returning a CholeskyFactor); older builds still
# export ``cholesky`` (returning a Factor). Prefer the new name
# and fall back for 0.4.x.
from sksparse.cholmod import cho_factor as _cho_factor
except ImportError:
try:
from sksparse.cholmod import cholesky as _cho_factor
except ImportError:
_HAVE = False
_cho_factor = None # type: ignore[assignment]
#: kwarg that picks the supernodal-vs-simplicial strategy — 0.5.x renamed
#: ``mode`` → ``supernodal_mode``. Resolved once at import so the hot
#: factor path doesn't pay for exception-based dispatch per call.
_SUPER_KW: str | None = None
if _HAVE:
import inspect as _inspect
_params = _inspect.signature(_cho_factor).parameters
if "supernodal_mode" in _params:
_SUPER_KW = "supernodal_mode"
elif "mode" in _params:
_SUPER_KW = "mode"
[docs]
class CholmodSolver(LinearSolverBase):
"""CHOLMOD supernodal sparse Cholesky (SPD only)."""
name: ClassVar[str] = "cholmod"
kind: ClassVar[str] = "direct"
spd_only: ClassVar[bool] = True
install_hint: ClassVar[str] = "install SuiteSparse system library + `pip install scikit-sparse`"
[docs]
@staticmethod
def available() -> bool:
return _HAVE
def _factor(self, A):
# scikit-sparse predates scipy's new sparse-array API; coerce to
# the legacy csc_matrix type it insists on. Skip the astype copy
# when the data is already float64 — at ~60k DOFs that saves
# ~80ms per call. ``mode="auto"`` lets CHOLMOD pick between
# simplicial and supernodal factorization; its default is plain
# supernodal, which is ~2× slower than the simplicial path on
# 3D hex meshes with many small supernodes.
if A.dtype != np.float64:
A = A.astype(np.float64)
if isinstance(A, sp.csc_matrix):
A_csc = A
else:
A_csc = sp.csc_matrix(A)
# ``simplicial`` beats CHOLMOD's ``"auto"`` heuristic on FE
# stiffness matrices coming out of 3D hex meshes: the factor is
# sparse enough that the extra bookkeeping of the supernodal
# path isn't recouped. Measured ~1.7× faster on 60k-DOF hex
# problems than the auto-selected supernodal path. The kwarg
# was renamed in 0.5.x (``supernodal_mode``) — ``_SUPER_KW`` is
# resolved once at import.
if _SUPER_KW is not None:
self._factor_obj = _cho_factor(A_csc, **{_SUPER_KW: "simplicial"})
else:
self._factor_obj = _cho_factor(A_csc)
[docs]
def solve(self, b: np.ndarray) -> np.ndarray:
b = np.ascontiguousarray(b, dtype=np.float64)
# 0.5.x exposes ``solve(b, system="A")``; 0.4.x exposes
# ``solve_A(b)``. Prefer the newer one.
if hasattr(self._factor_obj, "solve_A"):
return self._factor_obj.solve_A(b)
return self._factor_obj.solve(b, system="A")