Problem: find a few eigenvalues λ
and eigenvectors v
of
A v = λ v
, \ A
a scipy.sparse matrix, largest or smallest |λ|
or K v = λ M v
with stiffness matrix K
and mass matrix M
.
Purpose: experiment with different linear solvers, with verbose
to track solver calls.
Keywords: scipy.sparse, linear-solvers, eigenvalue, generalized-eigenvalue, Arpack, logging
In the following examples, eigs( A [M] [Minv] [OPinv] [sigma] ... )
is short for evals, V = eigs or eigsh( A ... )
with parameters e.g. k=10, tol=1e-5, v0=np.ones(n) )
, see below.
Examples, the largest eigenvalues and eigenvectors of A v = λ M v
:
from scipy.sparse.linalg import eigs, eigsh
from invop import Invop, Aminus
eigs( A, M=M ... ) # or eigsh
eigs( A, M=M, Minv=Invop( M, solver="splu" ) ... ) # the default for M a matrix
eigs( A, M=M, Minv=Invop( M, solver="cholesky" ) ... ) # fast for M posdef
eigs( A, M=M, Minv=Invop( M, solver="gmres" ) ... ) # the default for M a LinOp
eigs( A, M=M, Minv=Invop( M, solver="lgmres" ) ... ) # see below
eigs( ... sigma=0 ... )
tells Arpack to find eigenvalues near 0,
in the so-called "shift-invert" mode. For example:
Ainv = Invop( A, solver="splu" ) # b -> b \ A
eigs( A, OPinv=Ainv, sigma=0, [M=M] ... )
For this mode, Arpack needs to solve A x = b
many many times.
The default Invop( ... verbose=2 )
prints a line per solver call;
see the example logfiles below.
(In fact eigs( A, OPinv=Ainv, sigma=0 ... )
doesn't use A
at all, except to check its dtype.)
There are dozens of linear solvers.
Their runtimes (multicore), memory, understandability,
can vary a lot, depending on A
's size, spectrum, sparsity pattern ...
One size cannot fit all.
The default solvers for scipy Arpack shift-invert mode,
splu
for sparse matrices and GMRES
for LinearOperators,
are based on experience which may or may not match your problem.
If the dense matrix A.A
fits in memory, running scipy.linalg.eig
or eigh( A.A )
,
even overnight,
can save you hours of messing about with ARPACK parameters and different solvers.
Sampling the spectrum with subset_by_index
or subset_by_value
can possibly give some insight into your problem.
It's far easier to find eigenpairs of symmetric or Hermitian matrices,
which have real spectrum,
than of general matrices with complex spectrum.
For posdef matrices, sksparse.cholmod.cholesky
is very fast.
Use cholinc=1e-10
or so to nudge almost-posdef away from 0 --
X'X
can easily have tiny eigenvalues < 0.
Iterative solvers like GMRES
are much slower than direct solvers,
but direct solvers splu
etc. will need more memory.
Wikipedia
GMRES says
"fast convergence when the eigenvalues of A are clustered away from the origin".
When this is not the case,
I find that LGMRES
can be ~ 10 x faster than GMRES
, e.g. on poisson3 - 5.9 I;
see the example logfiles below.
The default tol=0
, machine precision, can be very slow.
I'd start with tol=1e-6
and see if eigcheck A V - V Λ
looks reasonable.
(For Linops, arpack.py gmres_loose
is not loose at all,
e.g. 1000 * np.sqrt(b.size 1e6) * eps 2e-16 = 2e-10 .)
The default start vector v0 random
is good for the Journal of Irreproducible Results.
A BIG and easy improvement is to use v0
from previous runs,
or from scaling up a half-size problem.
Lgmres can run much faster with a preconditioner, a fast cheap approximate solver. "Preconditioning is an art, a science, an industry."
This is a common problem in the finite-element method for analyzing structures
such as bridges:
find eigenvalues λ
near 0 for K v = λ M v
,
with K
a positive-definite stiffness matrix and M
a positive-definite mass matrix.
eigsh( K, M=M, OPinv=Invop( K, solver=... ), sigma=0, ... )
Similarly, for λ
near sigma=2
:
inv_A_minus_sigmaM = Invop( Aminus( A, 2, M ), solver=... )
eigsh( K, M=M, OPinv=inv_A_minus_sigmaM, sigma=2 ... )
There are many papers on this problem, perhaps more than real test cases :(
(By the way, the generalized eigenvalue problem and generalized eigenvector problem
are different altogether -- the latter is (A - λI)^k x = 0
.)
Invop()
with verbose=2
(the default) can help understand what a solver is doing.
For example,
solve = scipy.linalg.splu(A).solve
first builds a representation of A-1
;
this can be slow and can explode memory 💥 ,
depending on the sparsity structure of A^-1
and on the
options.
Arpack calls of solve(b)
are then pretty fast:
A = testmatrix( "poisson3", n=40, incdiag=-5.9 ) # 40^3, see testmatrix.py
Ainv = Invop( A, solver="splu" ) # default verbose=2
eigsh( A, sigma=0, OPinv=Ainv ... )
Invop splu A: (64000, 64000) csc_matrix 6.85 nnz/row ...
# solver seconds wall clock |b-Ax|/|b| dx x [lgmres: nr outer iter]
invop splu 142s |b-Ax|/|b| 1e-13 dx -0.0039 .. 0.0022 x -0.0039 .. 0.0022
...
invop splu 148s |b-Ax|/|b| 1e-10 dx -25 .. 25 x -12 .. 11
eigsh: 147.8 480.9 sec poisson3 n 64000 58 calls of splu
Spsolve
with umfpack is slower per call, but ~ linear in the number of calls:
# solver seconds wall clock |b-Ax|/|b| dx x [lgmres: nr outer iter]
invop spsolve 3s |b-Ax|/|b| 1e-16 dx -0.0039 .. 0.0022 x -0.0039 .. 0.0022
invop spsolve 7s |b-Ax|/|b| 3.8e-14 dx -3 .. 4.4 x -3 .. 4.4
...
invop spsolve 214s |b-Ax|/|b| 9.4e-14 dx -18 .. 19 x -6.7 .. 6.4
eigsh: 214.2 786.2 sec poisson3 n 64000 64 calls of spsolve
Another example, GMRES vs. Lgmres:
testmatrix poisson2: A (2500, 2500) 4.92 nnz/row -1 .. 0.1 incdiag -3.9 max |A - A'|: 0
GMRES:
eigsh: 1755.9 sec 30 calls of gmres, 30 stopped at maxiter 25000
evals [-0.00065 -0.00068 -0.0011 -0.0013 -0.0029 0.004]
eigcheck: |Av - λv| max 0.00089 [0.0003 4e-05 8e-05 0.0009 4e-05 9e-06]
Lgmres:
eigsh: 107.3 sec 30 calls of lgmres
evals [-0.00054 -0.00054 -0.001 -0.001 -0.0029 0.004]
eigcheck: |Av - λv| max 5e-07 [1e-07 2e-07 4e-07 2e-07 3e-08 5e-07]
shift-inverse-in-pictures
http://www.netlib.org/lapack/explore-html/modules.html
https://docs.scipy.org/doc/scipy/reference/generated/scipy.sparse.linalg.eigsh.html
https://docs.scipy.org/doc/scipy/reference/generated/scipy.sparse.linalg.LinearOperator.html
https://docs.scipy.org/doc/scipy/reference/tutorial/arpack.html \
Dhillon, "Current inverse iteration software can fail", 1998 \
https://scicomp.stackexchange.com/questions/tagged/linear-solver 300
https://scicomp.stackexchange.com/questions/tagged/gmres 40 \
cheers
-- denis 15 Nov 2020