Sparse shift-invert eigensolver¶
qscat.linalg.ShiftInvertEigs — the k eigenvalues and eigenvectors of a large
sparse complex-symmetric matrix nearest a complex shift \(\sigma\).
Validated in 1-D only. Nothing here is a claim about 2-D grids.
Why shift-invert¶
A resonance is an interior eigenvalue: it sits in the lower half of the
complex energy plane, surrounded by the discretized rotated continuum, at
neither end of the spectrum. Krylov methods (Arnoldi/Lanczos) converge to
extremal eigenvalues, so applied to H directly they return the edges of the
spectrum and never the pole.
The standard remedy is the shift-invert spectral transform. Run Arnoldi on
OP = (A - sigma*I)^-1
whose extremal eigenvalues \(1/(E - \sigma)\) correspond to \(A\)’s eigenvalues \(E\)
nearest \(\sigma\). ARPACK returns Ritz values of OP; SciPy converts them
back, so what comes out are eigenvalues of A.
Why it is cheap here¶
The transform costs one sparse solve per matrix-vector product — which is
exactly qscat.linalg.SparseLU’s job, so this inherits the complex-symmetric
MUMPS SYM=2 backend and its SuperLU fallback for free
(docs/physics/mumps-sparse-backend.md).
More usefully: \(\sigma\mathbb{1}\) touches only the diagonal, so \(A - \sigma\mathbb{1}\) has the
same sparsity pattern for every shift. SparseLU.refactor therefore applies —
on the MUMPS backend it reuses the symbolic analysis and skips the SCOTCH
ordering; on the scipy backend it re-runs splu (correct, but with no reuse).
A resonance hunt is a sweep of shifts through the complex plane, so it gets the
same discount the time-independent energy sweep gets from the identical trick
(docs/physics/ti-energy-sweep-reuse.md).
ShiftInvertEigs is a class rather than a function precisely so that the
factorization survives between shifts: the first near(sigma) call factors, every
later one refactors.
Why ARPACK rather than a complex-symmetric Lanczos¶
An ECS Hamiltonian is complex symmetric (\(A = A^{T}\), not \(A = A^{\dagger}\)), and a Lanczos iteration built on the bilinear c-product exploits that with a three-term recurrence, where ARPACK’s general non-Hermitian Arnoldi carries a full Hessenberg basis and ignores the symmetry.
ARPACK is used anyway, for two reasons. The iteration is not the cost here — one
sparse factorization dominates, and every Arnoldi step after it is a pair of
triangular solves. And a complex-symmetric Lanczos can break down (the c-norm
vᵀv can vanish for a non-null vector, which has no Hermitian analogue), so it
needs look-ahead to be robust. Trading a robust library iteration for a fragile
hand-rolled one to optimize the cheap half of the computation is the wrong
trade until measurement says otherwise. If the iteration ever does dominate at
2-D scale, ARPACK is then the oracle a c-product Lanczos would be validated
against.
Conventions¶
Three choices that are easy to get wrong, and are pinned by tests.
The shift sign is \(A - \sigma\mathbb{1}\). SciPy’s OPinv must solve
\((A - \sigma\mathbb{1})\,x = b\) — not the driven solver’s \(E\mathbb{1} - H\). The wrong sign does
not raise; it returns eigenvalues reflected about the shift. Measured on a
diagonal spectrum [0, 1, 2, 3, 10, 11, 12] with \(\sigma = 9\):
|
returned |
|---|---|
\(A - \sigma\mathbb{1}\) (correct) |
|
\(\sigma\mathbb{1} - A\) (wrong) |
|
Both look entirely plausible. test_shift_sign_convention_is_A_minus_sigma_I
exists for this reason, and was itself verified to fail under the wrong sign.
Eigenvalues come back sorted by \(|E - \sigma|\), nearest first — not by
Re E. A shift-invert result is a local window around the shift, so distance
from the shift is its meaningful order. qscat.dvr.eigen returns a whole
spectrum and keeps its ascending-Re E order; neither convention should be
changed to match the other.
Eigenvectors come back Euclidean-normalized (\(v^{\dagger}v = 1\)), exactly as
qscat.dvr.eigen returns them. ECS observables need the bilinear
qscat.linalg.c_product normalization instead, and the region to normalize over
is the caller’s decision (the LCP code, for instance, normalizes over the real
region only), so this class does not presume it.
Validation¶
The oracle is dense np.linalg.eig on the same matrix.
Synthetic: sparse complex-symmetric matrices (
n = 150–300). Eigenvalues match tortol = 1e-9; eigenvectors match to \(|v^{T}w| = 1\) within1e-6after normalizing both to unit c-norm \(\sqrt{v^{T}v}\) (the right notion of “equal up to scale” for a complex-symmetric operator).Physical: the N₂ electronic FEM-DVR-ECS Hamiltonian at fixed
R = 2.02(T + diag(V_surface),n = 113, the same buildqscat.core.lcpuses). Every eigenvalue the sparse solver returns at a given ECS angle is one the dense solver returns, to the samertol = 1e-9; and the two-angle pole built from sparse spectra equals the one built from dense spectra to that tolerance.
rtol = 1e-9 rather than something tighter is deliberate: the comparison runs
through a sparse factorization, and pinning sparse-solve agreement tighter has
failed CI on a different BLAS.
The pole found this way is the physical one. At R = 2.02 the sparse
two-angle pole is \(E = -0.661315 - 0.008333i\) Ha, i.e.
quantity |
this solver |
|
|---|---|---|
|
2.441 eV |
2.445 eV |
\(\Gamma\) |
0.4535 eV |
0.455 eV |
Note the subtraction: an ECS eigenvalue is absolute, carrying
\(v_0(R) = -0.751\) Ha, while the literature quotes \(E_\mathrm{res}\) measured from the
neutral curve. A shift seeded at the literature value rather than at
v0 + E_res finds nothing.
Measured working range¶
On that N₂ electronic Hamiltonian (n = 113), seeding at
\(\sigma = E_\mathrm{pole} + \mathrm{offset}\,(1+i)\) and asking whether the pole appears among the
k returned:
offset (Ha) |
k=2 |
k=4 |
k=6 |
k=8 |
k=16 |
|---|---|---|---|---|---|
0.001 |
found |
found |
found |
found |
found |
0.01 |
found |
found |
found |
found |
found |
0.05 |
found |
found |
found |
found |
found |
0.1 |
found |
found |
found |
found |
found |
0.2 |
found |
found |
found |
found |
found |
0.5 |
raises |
found |
found |
found |
found |
1.0 |
raises |
raises |
raises |
found |
found |
2.0 |
absent |
absent |
absent |
found |
found |
Three things this says:
The seed is forgiving. A shift
0.2 Haaway — an order of magnitude larger than \(\Gamma = 0.0167\) Ha — still finds the pole at anyk >= 2. A BO/LCP level is a far better guess than that, which is what makes the 2-D plan’s “seed fromresonance_levels” step credible.When it fails, it raises. The failure mode at a distant shift with a small Krylov space is
ConvergenceError(“ARPACK did not converge … 1 of 2 eigenvalues converged”), not a plausible wrong answer. The one silent outcome —offset = 2.0, smallk— is not an error at all: the pole genuinely is not among theknearest eigenvalues to a shift that far away.k = 8was reliable everywhere tested.k = 2is not worth using.
Cost note. On the scipy backend a second near() is not faster than the
first (measured 1.9 ms vs 2.3 ms at n = 113 — noise at this size), because
SparseLU.refactor re-runs splu there. The reuse is a MUMPS-backend property
and it is the large-matrix case that will show it; no speedup should be claimed
from these 1-D numbers.
Limits¶
1-D only. The primitive has not been run on a 2-D tensor Hamiltonian. The N₂ electronic matrix is
n = 113; the N₂ 2-D working deck is ~143k and H₂⁺ is ~1.15M, where the factorization, not ARPACK, is expected to dominate.Selection is not solved here. Within a single shift-invert window several rotated-continuum eigenvalues are narrower in \(|\operatorname{Im} E|\) than the pole itself, so “the narrowest state” is not a selector. Only the two-angle criterion (
qscat.ecs.find_resonance_pole/match_angle_stable) separates them, and the tests here use it exactly that way. A shift parked in the rotated continuum at \(\sigma = -0.30 - 0.40i\) yields no angle-stable state, which is the correct and useful negative.Near-singularity was not observed to be a problem. \(A - \sigma\mathbb{1}\) is ill-conditioned by construction — that is the amplification mechanism — and at every offset tested down to
0.001 Hathe results matched dense to1e-9. MUMPSSYM=2behaviour at a shift pathologically close to an eigenvalue has not been probed; these measurements ran on the scipy/SuperLU backend.