Source code for femorph_solver.solvers.eigen._primme

"""PRIMME via ``primme.eigsh`` — a state-of-the-art block-Davidson solver.

Often beats ARPACK on large SPD generalized eigenproblems because of its
restart + soft-locking scheme.  Pip-installable and self-contained (no
extra system libs).

References
----------
- Stathopoulos, A. & McCombs, J. R.  "PRIMME: PReconditioned
  Iterative MultiMethod Eigensolver — Methods and software
  description."  ACM Trans. Math. Softw. 37 (2), 21:1-21:30 (2010).
  [the shipped PRIMME library: block-JD + GD+k restart + the
  soft-locking convergence machinery]
- Stathopoulos, A.  "Nearly Optimal Preconditioned Methods for
  Hermitian Eigenproblems Under Limited Memory.  Part I: Seeking
  One Eigenvalue."  SIAM J. Sci. Comput. 29 (2), 481-514 (2007).
  [the GD+k restart scheme inside PRIMME]
- Davidson, E. R.  "The Iterative Calculation of a Few of the
  Lowest Eigenvalues and Corresponding Eigenvectors of Large
  Real-Symmetric Matrices."  J. Comp. Phys. 17 (1), 87-94 (1975).
  [the original Davidson method PRIMME descends from]
"""

from __future__ import annotations

from typing import ClassVar

import numpy as np
import scipy.sparse as sp

from ._base import EigenSolverBase

try:
    import primme

    _HAVE = True
except ImportError:
    _HAVE = False


[docs] class PrimmeSolver(EigenSolverBase): """primme.eigsh for generalized SPD eigenproblems.""" name: ClassVar[str] = "primme" kind: ClassVar[str] = "sparse-davidson" spd_only: ClassVar[bool] = True install_hint: ClassVar[str] = "`pip install primme`" @staticmethod def available() -> bool: return _HAVE
[docs] @staticmethod def solve( K: sp.spmatrix | sp.sparray, M: sp.spmatrix | sp.sparray, n_modes: int, *, sigma: float = 0.0, linear_solver: str = "auto", tol: float = 1e-10, ) -> tuple[np.ndarray, np.ndarray]: # Map ``sigma=0`` (the modal-default) to PRIMME's pure # Davidson path (``sigma=None``, ``which="SA"``). Passing # ``sigma=0.0`` triggers PRIMME's internal shift-invert: it # then needs to apply ``(K - 0·M)^{-1} = K^{-1}`` on every # restart and, with no ``OPinv`` callback supplied, falls # back to its own iterative inner solver (GMRES/QMR). On a # 500k-DOF SOLID186 rotor with K nnz ≈ 75 M and the bore- # clamp making K ill-conditioned (rcond ≈ 3e-18), that inner # solver returns NaN and PRIMME aborts with error -41 # ("matvec failed"). Pure Davidson on (K, M) avoids the # inner solve entirely — it converges via M-orthogonal # projection alone, which is cheap, robust, and the right # algorithm for the SPD generalized eigenproblem we have. Kcsr = K.tocsr() Mcsr = M.tocsr() if sigma == 0.0 or sigma is None: w, v = primme.eigsh( Kcsr, M=Mcsr, k=n_modes, which="SA", tol=tol, ) else: # Non-zero shift — caller wants modes near σ. PRIMME's # built-in shift-invert needs ``OPinv`` to be useful; # without it we re-route through ARPACK (the autotune # already does this for σ≠0, so reaching this branch # is a bypass-the-autotune call). Pass through and let # PRIMME try its iterative inner solve — caller chose # this path explicitly. w, v = primme.eigsh( Kcsr, M=Mcsr, k=n_modes, which="SM", sigma=sigma, tol=tol, ) order = np.argsort(w) return w[order], v[:, order]