Eigensolvers and the operator contract¶
Every SCF iteration in vibe-qc ends with a generalized Hermitian eigenvalue problem, \(\mathbf F\mathbf C = \mathbf S\mathbf C\boldsymbol\varepsilon\), and the response, CI and band-structure codes solve their own Hermitian problems. Since v0.17.16 these problems go through one boundary: an operator object that says what it is (a Fock matrix, a Kohn-Sham matrix, a response or CI Hamiltonian), how to apply it (an explicit matrix or a callback), in which metric, and at which k point and spin. This tutorial runs the operator examples that ship with vibe-qc, explains each printed line, and shows the requests the framework refuses and why.
Status
The operator contract and the dense, davidson and hermitian_davidson
solvers are production: every migrated SCF driver uses them. lobpcg is
production for explicit real problems outside run_job. Jacobi-Davidson
(jd), the harmonic-Davidson variant (harmonic_davidson, also reachable
under its historical name gplhr) and lanczos are experimental. The
label vocabulary is defined in
Methods: theory and status.
Prerequisites. A working vibe-qc build and basic linear algebra. The solver guide is the reference for every class and option used here. The numbers below were measured with the vibe-qc 0.17.17 development build (main, 2026-10-14); the timings are microseconds on a laptop and only show the ratio between cold and warm solves.
Run the example¶
The script needs no input and writes no files:
python examples/tutorials/unified_operators.py --size 80
cold: seconds=0.000511, iterations=6, vector_applications=27, eigenvalue_error=5.107e-15
warm: seconds=0.000116, iterations=1, vector_applications=9, eigenvalue_error=5.107e-15
Residual convergence is not a completeness certificate: False
complex callback: iterations=6, block_calls=8, vector_applications=29, eigenvalue_error=3.553e-15
factorized AO callback: vector_applications=23, eigenvalue_error=3.553e-15, AO_residual=7.131e-12, projected_residual=6.927e-12
Complex AO diagnostics: {'converged': True, 'eigenvalues': [-0.7217287688002687, -0.06960895668316142], 'residuals': [1.97e-16, 2.63e-16], 'projected_residuals': [2.29e-16, 2.60e-16], 'residual_frame': 'original', 'orthogonality_error': 8.35e-16, 'retained_rank': 3, 'selection_verified': True, ...}
Same-subspace principal cosines: [1. 1.]
Verified nearest roots: [0.40667953 0.46573986]
CI roots: [-1.00317978 -0.49682022]
The diagnostics line is abbreviated here. The sections below take the output one block at a time.
A matrix-free Davidson solve, cold and warm¶
The first block builds an 80 by 80 test matrix, a diagonal from -2 to 3 with a small symmetric perturbation, and hands the solver only its action:
from vibeqc.solvers.eigensolver import SolverOptions
from vibeqc.solvers.operators import HermitianOperator, OperatorMetadata, solve_operator
op = HermitianOperator(
matvec=lambda v: matrix @ v,
matmat=lambda v: matrix @ v,
n=n, diagonal=matrix.diagonal(),
metadata=OperatorMetadata(kind="response", basis_id="example-response"),
)
opts = SolverOptions(n_roots=3, tol=1e-9, n_guess=8)
cold = solve_operator(op, opts, solver="davidson")
warm = solve_operator(op, opts, solver="davidson", previous=cold)
The cold solve takes six iterations and 27 operator applications for the three lowest roots; the warm solve, started from the previous eigenvectors, needs one iteration and nine applications. Both agree with the dense eigenvalues to 5e-15. Three details matter:
The diagonal is required. Davidson preconditions its correction vectors with the diagonal, and a callback cannot reveal its diagonal cheaply, so the operator must supply it. Without it the solve is refused rather than probed.
Warm starts must match the frame.
previous=is accepted only if the earlier result belongs to the same operator kind, basis, spin, k point and block. Across SCF iterations the Fock matrix changes but the frame stays, which is exactly the case warm starts serve.Converged is not the same as complete. The third line prints
selection_verified, which isFalse: small residuals prove that the returned vectors are eigenvectors, not that they are the three lowest ones. An iterative solver can converge onto an eigenvector while missing a lower one that its search space never touched. Only a dense solve, which sees the whole spectrum, sets the flag. Passrequire_selection=Truewhen a missing root would be a wrong answer; an iterative backend then refuses instead of guessing.
Complex operators and block callbacks¶
Bloch Hamiltonians at a general k point are complex Hermitian. The second
block adds an antisymmetric imaginary part and supplies only a block action,
matmat, which the complex solver calls with several vectors at once:
complex callback: iterations=6, block_calls=8, vector_applications=29, eigenvalue_error=3.553e-15
The callback was entered 8 times and applied to 29 vectors in total. Each
Python crossing is expensive compared with a matrix-vector product in native
code, so block actions keep the number of crossings small. The real
davidson backend refuses complex input; hermitian_davidson handles both.
An AO operator with an overlap metric¶
In a Gaussian basis the Fock matrix lives in the non-orthogonal AO basis with overlap \(\mathbf S\). The third block builds a factorised operator \(\mathbf A = \operatorname{diag}(\mathbf h) + \mathbf B\mathbf B^\dagger\) whose action never forms \(\mathbf A\), together with an orthogonaliser \(\mathbf X\) (\(\mathbf X^\dagger\mathbf S\mathbf X = \mathbf 1\)) and the diagonal of \(\mathbf X^\dagger\mathbf A\mathbf X\), which the provider computes from its own factors:
factorized AO callback: vector_applications=23, eigenvalue_error=3.553e-15, AO_residual=7.131e-12, projected_residual=6.927e-12
Two residuals are reported because there are two frames. The projected residual is measured in the orthogonal frame where the solver iterates; the AO residual \(\lVert \mathbf A\mathbf c - \varepsilon\mathbf S\mathbf c\rVert\) is measured in the original frame. They agree here because \(\mathbf X\) keeps the full rank of \(\mathbf S\). When canonical orthogonalisation drops near-linearly-dependent directions they can differ, and the AO residual is the one that tells you whether the original problem was solved.
Diagnostics for a complex Kohn-Sham matrix¶
The fourth block solves a three-function complex Kohn-Sham matrix with an
explicit overlap and prints the portable diagnostic record of
vibeqc.output.eigensolver.eigensolver_diagnostics:
Field |
Value |
Meaning |
|---|---|---|
|
about 2e-16 |
AO-frame residual norms per root |
|
about 2e-16 |
orthogonal-frame residual norms |
|
|
which frame |
|
8e-16 |
Frobenius error of \(\mathbf C^\dagger\mathbf S\mathbf C - \mathbf 1\) |
|
3 |
directions kept by the orthogonaliser |
|
|
a dense solve enumerated the spectrum |
|
kind, spin, k point, basis id |
what was solved, for the log |
subspace_overlap then compares two solutions as subspaces, through the
principal cosines in the \(\mathbf S\) metric. Comparing a subspace with itself
gives cosines of one. This is the right test for degenerate orbitals, whose
individual vectors have no fixed orientation.
Roots nearest a shift¶
The fifth block asks for the two roots closest to \(\sigma = 0.4248\) with
require_selection=True. The dense solver enumerates the full spectrum
and selects the two nearest values, 0.4067 and 0.4657, so the selection is
certified. No shift-and-invert step is involved, so a root exactly at
\(\sigma\) is as well defined as any other.
For large matrices, where a dense solve is too expensive, the experimental interior solvers aim at the same roots iteratively. On test matrices of increasing size and coupling the solver writer measured:
Size |
Coupling |
Jacobi-Davidson iterations |
Harmonic-Davidson iterations |
|---|---|---|---|
60 |
0.01 |
87 |
7 |
60 |
0.05 |
75 |
35 |
120 |
0.01 |
76 |
17 |
120 |
0.05 |
119 |
483 |
300 |
0.01 |
88 |
82 |
300 |
0.05 |
306 |
not converged in 500 |
Both converged roots matched the nearest dense eigenvalue to 1e-15, but neither can certify its selection, and the harmonic variant stalled on the largest, most strongly coupled case. It raises instead of returning an unconverged value:
RuntimeError: Experimental GPLHR did not converge 1 root(s) near sigma=0.4248 after
500 iterations (maximum residual=5.066e-02, tol=1.000e-08). Try solver='jd' or
increase max_subspace/max_iter.
The kind keeps the mathematics honest¶
The last block solves a two-determinant CI matrix as a HermitianOperator
with kind="ci". Trying to wrap the same matrix as a FockOperator is
refused:
ValueError: A CI or response operator is not a Fock operator
The kind decides which checks apply. A Fock or Kohn-Sham operator may carry an overlap, spin and k point and is solved for orbitals; a CI or response operator is not, and a non-Hermitian operator such as a Casida matrix or a coupled-cluster Jacobian is refused outright and needs its own solver.
What else is refused¶
The framework refuses an unsupported request instead of silently doing something else. A selection of the messages, measured on this build:
Request |
Message |
|---|---|
|
|
|
|
|
|
|
|
|
|
a callback Davidson below its restart floor |
|
|
|
|
|
|
|
run_job(solver="lobpcg") deserves a note: older versions accepted it and
silently ran Davidson. It now raises, because a result labelled LOBPCG must
have been computed by LOBPCG.
Theory¶
Davidson iteration. For the lowest roots of a large Hermitian matrix \(\mathbf A\), Davidson’s method grows a search space \(\mathbf V\), solves the small projected problem \(\mathbf V^\dagger\mathbf A\mathbf V\,\mathbf y = \theta\mathbf y\), forms the residual \(\mathbf r = \mathbf A\mathbf u - \theta\mathbf u\) of each Ritz vector \(\mathbf u = \mathbf V\mathbf y\) and adds the preconditioned correction
orthogonalised against \(\mathbf V\) (Davidson, Ernest R. (1975). *The iterative calculation of a few of the lowest eigenvalues and corresponding eigenvectors of large real-symmetric matrices.* Journal of Computational Physics, **17**(1), 87–94. doi:[10.1016/0021-9991(75)90065-0](https://doi.org/10.1016/0021-9991(75)90065-0)). The method converges fast when \(\mathbf A\) is diagonally dominant, which Fock matrices in a good orbital basis are, and restarts from the current Ritz vectors when the space reaches its size limit. LOBPCG (Knyazev, Andrew V. (2001). *Toward the Optimal Preconditioned Eigensolver: Locally Optimal Block Preconditioned Conjugate Gradient Method.* SIAM Journal on Scientific Computing, **23**(2), 517–541. doi:[10.1137/S1064827500366124](https://doi.org/10.1137/S1064827500366124)) keeps a fixed three-block space of the current vectors, their preconditioned residuals and the previous step.
Generalized problems. \(\mathbf F\mathbf C = \mathbf S\mathbf C
\boldsymbol\varepsilon\) is reduced to standard form with an orthogonaliser,
\(\mathbf F' = \mathbf X^\dagger\mathbf F\mathbf X\), \(\mathbf C = \mathbf X
\mathbf C'\). Canonical orthogonalisation takes \(\mathbf X = \mathbf U
\mathbf s^{-1/2}\) from the eigenpairs of \(\mathbf S\) and drops eigenvalues
below a threshold, which removes near-linear dependencies
(Löwdin, Per-Olov (1970). *On the Nonorthogonality Problem.* Advances in Quantum Chemistry, **5**, 185–199. doi:[10.1016/S0065-3276(08)60339-1](https://doi.org/10.1016/S0065-3276(08)60339-1)); the dropped count is the difference
between the basis size and retained_rank.
Interior eigenvalues. Rayleigh-Ritz converges to the ends of the
spectrum. Jacobi-Davidson (Sleijpen, Gerard L. G. and Van der Vorst, Henk A. (1996). *A Jacobi-Davidson Iteration Method for Linear Eigenvalue Problems.* SIAM Journal on Matrix Analysis and Applications, **17**, 401-425. doi:[10.1137/S0895479894270427](https://doi.org/10.1137/S0895479894270427)) targets an
interior value by solving the projected correction equation
\((\mathbf 1 - \mathbf u\mathbf u^\dagger)(\mathbf A - \theta\mathbf 1)
(\mathbf 1 - \mathbf u\mathbf u^\dagger)\mathbf t = -\mathbf r\) approximately.
Harmonic Rayleigh-Ritz extracts approximations from the shifted problem
\((\mathbf A - \sigma\mathbf 1)^{-1}\) without inverting it, which favours the
eigenvalues nearest \(\sigma\); vibe-qc’s harmonic_davidson is a variant
in the family of GPLHR (Zuev, Dmitry, Vecharynski, Eugene, et al. (2015). *New algorithms for iterative matrix-free eigensolvers in quantum chemistry.* Journal of Computational Chemistry, **36**, 273-284. doi:[10.1002/jcc.23800](https://doi.org/10.1002/jcc.23800)), not a full
implementation of the published algorithm.
Variations¶
Run
--size 120for a larger matrix: the cold solve needs 29 applications and the warm one still 9.Replace
solver="davidson"withsolver="dense"and compareselection_verified.Set
opts = SolverOptions(n_roots=3, tol=1e-9, n_guess=1)and read the refusal: the guess space must hold at least as many vectors as roots.
Pitfalls¶
Do not read convergence as completeness. Use a dense reference or
require_selection=Truewhen a missed root matters.Supply the diagonal of the frame you solve in. For an AO callback with an orthogonaliser that is the diagonal of \(\mathbf X^\dagger\mathbf A\mathbf X\), computed by the provider, not the AO diagonal.
Callbacks must not mutate their input. The framework copies their output, but the state they capture (densities, coefficients) must stay fixed during a solve.
See also¶
Eigensolvers and the operator contract, the reference for every class, option and driver.
SCF convergence for what happens around the eigensolver in each iteration.