Eigensolvers and the operator contract¶
Every self-consistent-field iteration in vibe-qc ends in the same step: a Hermitian matrix, usually a Fock or Kohn-Sham matrix in a nonorthogonal AO basis, is diagonalised, and the lowest eigenvectors become the occupied orbitals. This page documents the solvers that perform that step, the Hermitian operator contract through which the SCF drivers, the response code and the post-SCF semicanonicalisation reach them, and the theory of the algorithms. SCF acceleration (DIIS and its relatives, damping, level shifts, the second-order finalizers) is a separate subsystem that acts on the Fock matrix between diagonalisations; see SCF convergence. Occupation assignment is likewise not part of eigensolution: a solver returns eigenpairs, and the method decides which of them to fill.
Status: production (dense, Davidson, LOBPCG); experimental (Jacobi-Davidson, harmonic-Davidson, Lanczos)
The dense solver and the real and complex Davidson solvers are the
production diagonalisers of every molecular and periodic SCF route, covered
by the T1 release gate through the SCF tests they serve. LOBPCG is a
production eigensolver for explicit real symmetric problems through the
library API; no SCF driver routes through it, and run_job(solver="lobpcg")
raises. Jacobi-Davidson (jd), the GPLHR-derived harmonic-Davidson variant
(harmonic_davidson, alias gplhr) and the Python Lanczos reference are
experimental: solver_capabilities(name).experimental is true, no SCF
driver uses them, and their numbers are research output. The label
vocabulary is defined in
Methods: theory and status.
Choosing the solver in a job¶
For the molecular SCF drivers the choice is the solver keyword of
run_job, or equivalently the use_davidson switch on the options object:
from vibeqc import run_job, Molecule, Atom, RHFOptions
mol = Molecule([Atom(8, [0, 0, 0]),
Atom(1, [0, 1.43, -0.98]),
Atom(1, [0, -1.43, -0.98])]) # bohr
result = run_job(mol, basis="def2-svp", method="rhf", solver="dense") # default
result = run_job(mol, basis="def2-svp", method="rhf", solver="davidson")
opts = RHFOptions()
opts.use_davidson = True # the same request on the options object
opts.davidson_min_dim = 100 # dense below this many AO functions
opts.davidson.conv_tol = 1e-7 # DavidsonOptions: residual threshold per root
opts.davidson.max_iter = 200
run_job accepts exactly "dense" and "davidson". solver="lobpcg"
raises NotImplementedError (“run_job does not implement LOBPCG SCF
diagonalization; use solver=’dense’ or solver=’davidson’”): earlier versions
silently ran Davidson for that request, and the C++ molecular SCF kernels
expose only the use_davidson switch. Any other spelling, including "jd",
raises ValueError. The method-specific size threshold still applies: an
SCF that asks for Davidson on a basis smaller than davidson_min_dim (100
AO functions by default) solves densely. Periodic drivers carry the same
switch on PeriodicRHFOptions.use_davidson with davidson_min_dim and the
davidson controls; complex k-point Fock matrices go to the complex
Hermitian Davidson automatically. Open-shell Roothaan loops (ROHF/ROKS) and
the spin-locked or quadratic updates of Gamma UHF need the complete retained
spectrum for their occupation rules, so a Davidson request there with too
few roots is refused before any Fock build; see ROHF and ROKS.
Davidson pays off when only the occupied space is wanted out of a large basis and the Fock matrix changes little between iterations, because the previous orbitals warm-start the next solve (one to three outer iterations per SCF cycle is typical). Below a few hundred AO functions the dense solve is faster and exact, and it is the only solver whose root selection is certified (see Residuals, selection and warm starts).
The solvers¶
All solvers are reachable by name through the library API. The table lists
the executable capabilities returned by
vibeqc.solvers.operators.solver_capabilities(name) on vibe-qc 0.17.15;
these describe what the strict entry point solve_operator actually
accepts, not aspirational flags.
Name (aliases) |
Algorithm, backend |
Complex |
Matrix-free |
Selectors |
Warm start |
Interior shift |
Status |
|---|---|---|---|---|---|---|---|
|
full diagonalisation, |
yes |
no |
SA, LA, SM, LM, BE, nearest to |
no |
yes, by enumeration |
production |
|
blocked Davidson, C++ |
no |
yes (supplied diagonal) |
SA, LA |
yes |
no |
production |
|
blocked Davidson, complex Hermitian, C++ |
yes |
yes (supplied diagonal) |
SA, LA |
yes |
no |
production |
|
LOBPCG, C++ |
no |
no |
SA, LA |
no |
no |
production (explicit problems; no SCF route) |
|
Jacobi-Davidson with MINRES correction equation, C++ |
no |
no |
SA, LA |
no |
yes |
experimental |
|
restarted harmonic-Davidson for roots near a shift, C++ |
no |
no |
SA |
no |
yes, required |
experimental |
|
Lanczos with full reorthogonalisation, Python |
yes |
yes |
SA, LA |
no |
no |
experimental |
harmonic_davidson and gplhr are two names for one solver class
(get_solver("harmonic_davidson").name() returns "gplhr"). The
implementation in cpp/src/gplhr.cpp uses the harmonic extraction and the
signed shifted-diagonal preconditioner of Zuev and co-workers
(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)) inside a growing, restarted Davidson
search space; it does not implement the paper’s locally harmonic recurrence
with residual-like blocks and a previous-direction block. harmonic_davidson
is therefore the accurate name, gplhr is retained for compatibility, and
the citation records provenance rather than equivalence; the decision is
recorded in the design document.
Each solver accepts a subset of the SolverOptions fields beyond
n_roots, which, sigma and tol; a nondefault value of any other field
raises NotImplementedError at the strict entry point, so an option is
never silently ignored:
Solver |
Accepted options |
|---|---|
|
none further ( |
|
|
|
|
|
|
|
|
|
|
|
|
Partial-spectrum Davidson controls¶
n_roots is the number of eigenpairs returned (0 requests all retained
roots). n_guess sets the starting subspace of davidson and
hermitian_davidson when no guess_vectors are supplied:
|
Meaning |
|---|---|
|
automatic size |
positive, at least |
explicit initial size |
negative, or positive but smaller than |
rejected ( |
max_subspace is the dimension at which the search space collapses to the
current Ritz vectors; it is separate from the starting size, and a value
below the native restart floor min(n, 2 n_roots + 6) is rejected.
preshift bounds the magnitude of the preconditioner denominator
\(\theta - D_{ii}\) while keeping its sign. Partial-spectrum convergence is a
statement about the requested roots, not about the whole spectrum, and
these controls do not decide which SCF state is physically appropriate.
The native molecular RHF driver’s RHFOptions.davidson.n_eig is a separate
control: 0 requests the full retained space and a positive request smaller
than the occupied space is rejected. For an HCORE start whose partial
spectrum could end inside a degenerate frontier, that driver completes the
seed diagonalisation and keeps the complete orbital frame for the first
Fock solve; the
initial-guess reference
describes the equal-fraction seed occupations that follow.
The operator contract¶
The module vibeqc.solvers.operators is the boundary between a method that
builds an operator and a solver that diagonalises it. The builder owns
integrals, densities, image sums and XC approximations; the operator owns one
fixed, immutable representation; the solver owns its iteration space; the
occupation layer owns electron counts and smearing. None substitutes for
another. The full design is in
the operator contract; this section is
the user’s view.
Constructing an operator¶
import numpy as np
from vibeqc.solvers.operators import FockOperator, HermitianOperator, OperatorMetadata
# A dense AO Fock matrix with its overlap: C = X Y with X^H S X = I is built
# internally by canonical orthogonalisation (overlap cutoff 1e-12).
F = np.array([[1.0, 0.2j], [-0.2j, 2.0]])
S = np.array([[1.0, 0.1], [0.1, 1.0]])
ks = FockOperator(matrix=F, overlap=S,
metadata=OperatorMetadata(kind="ks", spin="restricted",
kpoint=(0.25, 0.0, 0.0)))
# A matrix-free operator: supply the action and its diagonal.
n, A = 80, np.diag(np.linspace(-2.0, 3.0, 80))
op = HermitianOperator(matvec=lambda v: A @ v, matmat=lambda V: A @ V,
n=n, diagonal=A.diagonal(),
metadata=OperatorMetadata(kind="response", basis_id="tda"))
HermitianOperator takes one representation: a dense array, a SciPy
sparse matrix, or callbacks (matvec for one vector, matmat for a block)
with an explicit dimension and dtype; a matrix together with a callback is
rejected. Dense and sparse input are copied into immutable storage and
checked for finiteness and Hermiticity; callbacks promise a fixed linear
Hermitian action for the whole solve and are never probed with unit vectors.
FockOperator is the one-particle specialisation: its kind must be
"fock", "ks" or "one_electron", so a CI Hamiltonian or a response
matrix (kind="ci", "response") cannot be passed off as a Fock matrix,
while kind="casida" or any non-Hermitian label is refused outright.
Metric and frame. Supply overlap alone for canonical rank reduction with
overlap_cutoff (default 1e-12, relative to the largest overlap
eigenvalue; an indefinite overlap is an error, not a silent truncation), or
overlap together with a rectangular X that already satisfies
\(X^\dagger S X = I\) (checked to 1e-8), or X alone for an operator the
caller has already orthogonalised. An OperatorFrame bundles a validated
S/X pair with a basis identity, AO ordering and shell-image gauge so
that several solves (both spins, every SCF iteration) share one checked
frame; its identity digest covers the actual S/X bytes, which is what
prevents a changed retained space from reusing an old subspace under the
same basis label.
Matrix-free Davidson needs the diagonal of the operator in the frame it
iterates in. For an orthonormal-frame callback supply diagonal. For an AO
callback or sparse matrix with explicit X, supply reduced_diagonal, the
real diagonal of \(X^\dagger A X\) in that exact retained frame, computed by
the provider from its own representation; it is not the AO diagonal and
cannot be estimated by transforming diag(A). Overlap-only rank reduction
is not admitted for callback Davidson, because the provider cannot know the
column gauge of an X the solver computes. Dense input computes its reduced
diagonal itself and refuses the extra field. Callbacks may return reusable
scratch buffers: apply copies every result, including each column of a
vector-only block application, so a producer that reuses workspace is safe
as long as it does not mutate the input and freezes the density,
coefficient and plan data its action captures.
OperatorMetadata annotates a solve; it never instructs the solver to fill
or rotate anything. kind, basis_id, generation, spin, kpoint,
symmetry_block and optional occupations identify the problem;
frame_id, build_id and density_id tie the snapshot to an evaluated
Fock build; role is "physical", "effective" (a Roothaan ROHF operator)
or "solver", the last requiring a modification string such as the DIIS
extrapolation or level shift that produced it. A new Fock build is a new
snapshot with a new generation.
Solving and reading the diagnostics¶
from vibeqc.solvers.eigensolver import SolverOptions
from vibeqc.solvers.operators import solve_operator
from vibeqc.output.eigensolver import eigensolver_diagnostics
result = solve_operator(ks, SolverOptions(n_roots=1, tol=1e-10)) # dense by default
print(eigensolver_diagnostics(result))
solve_operator(operator, options, solver="dense", previous=None, require_selection=False) validates the request against the capability table
before applying the operator, reduces an AO problem to the orthonormal
frame, dispatches to the named backend and then recomputes the residuals
independently of whatever the backend reported. A solve returns only when
every requested projected residual is at most tol, the backend reported
success, the returned eigenvectors are metric-orthonormal to a Frobenius
error of 1e-7 and the requested number of roots came back; otherwise it
raises RuntimeError. Returned eigenvalues are ascending, eigenvectors are
in the original (AO) frame, and the SolverResult carries:
Field |
Meaning |
|---|---|
|
ascending roots and AO-frame columns \(C = XY\) |
|
\(\lVert A C_i - \varepsilon_i S C_i\rVert_2\) when |
|
\(\lVert X^\dagger A X\,Y_i - \varepsilon_i Y_i\rVert_2\), the quantity the convergence test uses |
|
Frobenius norm of \(C^\dagger S C - I\) |
|
dimension of the retained frame after rank reduction |
|
|
|
iteration count and, for Davidson, the final subspace size and the number of roots under |
|
observed callback vector applications including the diagnostic ones; |
|
the annotations the operator carried |
eigensolver_diagnostics(result) turns this into a JSON-compatible record
with the nested operator_metadata (kind, basis identifier, generation,
spin, k-point, symmetry block, occupations, and the frame/build/role fields
when they are set) beside the flat operator_kind. It is the portable
formatter for solver output; the historical SCF logs and QVF schemas have
not been extended with it. Solver modules write no user files.
Residuals, selection and warm starts¶
Residuals are not a selection certificate. A small residual proves that
the returned pair is an accurate eigenpair; it does not prove that no other
eigenvalue lies closer to the target, or lower, than the ones returned. Only
the dense backend enumerates the complete finite spectrum, so
selection_verified is True only there; require_selection=True makes
solve_operator refuse every iterative backend with NotImplementedError.
Dense selectors SA, LA, SM, LM and BE (both ends, the odd root
going to the upper end) choose the roots and the result is then presented
ascending; a sigma selects the roots nearest the shift, including a root
exactly at sigma, without any inverse. Iterative solvers accept SA and
LA only (harmonic_davidson only SA), and an LA request is solved on
\(-A\) and flipped back.
Projected residuals versus AO residuals. With a truncated metric the two can differ: a solution that is exact in the retained frame can leave a nonzero residual along the discarded overlap directions. The result exposes both and the convergence test uses the projected one; it never presents projected convergence as exactness in directions the frame dropped.
Warm starts. Pass previous=old_result to davidson or
hermitian_davidson. The previous eigenvectors are AO-frame columns, so they
must come from a solve with the same kind, basis_id, spin, kpoint,
symmetry_block and frame_id; the generation may differ, which is what an
SCF iteration does. They are mapped to reduced coordinates by least squares
(\(XY = C_{\text{prev}}\)), which also handles rectangular X. A guess is a
subspace seed, never a root identity: subspace_overlap(previous, current, overlap=S) returns the overlap amplitudes and the principal cosines of two
metric-orthonormal subspaces so a caller can track a cluster of orbitals
without imposing an orientation on a degenerate pair, and it never reorders
occupations. SCFStep.prev_C with solver="dense" is refused rather than
silently ignored.
What is refused¶
Every row below was exercised on vibe-qc 0.17.15 development build (main, 2026-10-11):
Request |
Outcome |
|---|---|
|
|
|
|
|
|
complex matrix with |
|
callback operator with |
|
|
|
|
|
|
|
callback Davidson without |
|
callback Davidson with |
|
|
|
|
|
|
|
|
|
matrix plus callback; |
|
The historical descriptor API (EigenProblem, GeneralizedEigenProblem,
InteriorEigenProblem, SCFStep, solve_eigenproblem, solve_scf_step)
remains available for compatibility and routes an operator argument to
solve_operator. Its legacy strategies still probe unit vectors for a
missing diagonal and may materialise a callback; new integrations use the
strict interface.
Which drivers use it¶
The contract is additive and the migration is incremental; the consumer inventory records every audited call site and the reason for each remaining bypass. In the current tree:
Molecular RHF, UHF, RKS and UKS diagonalise through the native
solve_fock_matrixboundary, including the opt-in MANTA molecular entry points, which select the integral source but not a separate eigensolver.Periodic k-point SCF (native Bloch and periodic SCF kernels, and the shared Python multi-k helper used by the Ewald and GDF multi-k RHF/RKS/UHF/UKS drivers) solves complex Hermitian per-k problems with adjoint transforms; k-point DFTB0 and SCC-DFTB reach the same helper. DFTB0/UDFTB0 use the native generalized one-electron solve with a strict Cholesky metric.
Gamma Ewald and Gamma/PBC GDF HF and KS, Gamma GPW/GAPW including range-separated GAPW, multi-k GPW RKS/UKS/ROKS and GAPW RKS, and the fixed-density band paths use the internal
FockDiagonalizeror the statelesssolve_dense_fockadapter, with method-ownedX/S, rank policies, occupations and Davidson thresholds, separate alpha/beta warm subspaces, and projected DIIS matrices kept in their orthogonal frame.Molecular and Gamma GPW ROHF/ROKS and Gamma Ewald ROHF solve the Roothaan effective Fock through the same adapter and canonicalise with separate dense solves per closed/open/virtual block (ROHF and ROKS); the dense HCORE seed, level shifts and the alpha-energy occupation rule stay with the method.
Iterative TDA passes its sigma action as a
HermitianOperator(kind="response")with the analytic diagonal.Post-SCF semicanonicalisation (Brueckner CCD, FNO-CCSD, reference UMP2/ROHF-MP2) and the DLPNO-MP2/UMP2 pair-PNO Fock rotations use dense solves over named MO partitions; the shared unrestricted PNO helper also serves the experimental neutral CCM UMP2/UCCSD pair build without changing that line’s status.
Beyond eigensolution, several Gamma producers now return an evaluated Fock
state: Gamma Ewald RHF, RKS, UHF and UKS (3D cells and the 2D slab
routes) and the bare native Gamma GDF RHF/RKS expose result.fock_build,
an immutable record of the physical Fock, the density it was built from,
the energy components and the source identity. next_density (RHF/RKS) or
next_density_alpha/next_density_beta (UHF/UKS) and
density_fixed_point_residual describe the orbital fixed-point map
separately, so that at an iteration cap the next density is not mistaken
for the evaluated one when energies are recomputed or a restart is written.
Final orbital energies on those routes are physical Rayleigh quotients, also
on capped exits. DIIS-extrapolated or level-shifted operators carry the
same build identity with role="solver"; the physical operator is never
replaced.
Gamma Ewald ROHF returns the same kind of record: its fock_build holds both
physical spin Focks, their evaluated densities, the energy components and the
source receipt. effective_fock_operator is the Roothaan effective Fock with
the same parent build and density identities and role="effective"; it is not
an extra energy channel. effective_fock_residual reports the projected ROHF
commutator that the constrained convergence test uses, while
fock_build.commutator_norm measures the unconstrained spin commutators,
which need not vanish at ROHF convergence. QVF provenance records both
identities, including on capped and zero-iteration runs.
Two policies are visible at this boundary. Real-only consumers (Gamma Ewald
RHF/RKS/UHF/ROHF and the Gamma GDF HF/KS drivers, which build densities
with a transpose) reject active complex Davidson guesses and complex frames
before any J/K build (#494); complex-capable consumers use adjoints. And a
driver whose occupation rule ranks all candidates (ROHF, the spin-locked
PATTERN_HOLD and quadratic updates of Gamma UHF) refuses a partial Davidson
request before building a Fock. Deliberately outside the contract are the
memory-bounded periodic MANTA RHF kernel with its own Jacobi solver, the
CI and selected-CI sigma engines, the stability-analysis Davidson, metric
and density factorisations, Hessians and non-Hermitian CC Jacobians.
Theory¶
Throughout, \(A\) is an \(n \times n\) Hermitian matrix, \(k\) the number of wanted roots, and the AO problem is \(F C = S C \varepsilon\).
Generalized problems and canonical orthogonalisation. An orthonormal
frame is any \(X\) with \(X^\dagger S X = I\); then \(C = X Y\) and
\(X^\dagger F X\, Y = Y \varepsilon\) is a standard Hermitian problem.
vibe-qc’s rank-reducing choice is Löwdin’s canonical orthogonalisation
(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)): with \(S = U s U^\dagger\),
\(X = U_{\text{keep}}\, s_{\text{keep}}^{-1/2}\) keeps the overlap
eigenvalues above \(\max(c,\, c\, s_{\max})\) with cutoff \(c\) (overlap_cutoff,
1e-12 at the operator boundary; the SCF drivers pass their own
linear_dep_threshold). Discarded directions are near-linear combinations
of basis functions that the basis cannot resolve; the retained rank is
reported, a negative overlap eigenvalue beyond the cutoff is an error, and
the root count must fit the retained rank. Pivoted Cholesky decomposition
of \(S\) (Lehtola, Susi (2019). *Curing basis set overcompleteness with pivoted Cholesky decompositions.* Journal of Chemical Physics, **151**(24), 241102. doi:[10.1063/1.5139948](https://doi.org/10.1063/1.5139948)) is the alternative
that removes functions rather than directions; DFTB0 uses a strict SPD
Cholesky metric with no rank deletion. Every complex transformation uses the
adjoint, which is what makes k-point Fock matrices work in the same code
path.
Davidson subspace iteration (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 block form is Liu’s). Given an orthonormal search space \(V\), form the projected matrix \(H = V^\dagger A V\), diagonalise it to obtain Ritz pairs \((\theta_i, u_i = V y_i)\), and compute the residuals \(r_i = A u_i - \theta_i u_i\). Each unconverged root contributes a correction vector from the diagonal preconditioner,
with the denominator magnitude floored at preshift; the corrections are
orthogonalised against \(V\) (and each other, with conjugate inner products
in the complex kernel) and appended. When \(V\) reaches max_subspace it is
collapsed to the current Ritz vectors. Convergence is declared when all
\(\lVert r_i \rVert \le\) tol. The method converges fast when \(A\) is
diagonally dominant in the frame it is given, which an orthonormalised Fock
matrix of a reasonable guess is, and it needs only \(O(nm)\) storage for a
subspace of size \(m\). A poor diagonal (a Fock matrix in a frame far from its
eigenbasis) can stall it, and the orthogonal-frame diagonal must be supplied
for a matrix-free operator because probing it costs \(n\) applications.
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)). The locally optimal block preconditioned conjugate-gradient method keeps a block of \(b\) iterates \(X\), their preconditioned residuals \(W = T(AX - X\Theta)\) and the previous search directions \(P\), and performs Rayleigh-Ritz on the \(3b\)-dimensional space \([X, W, P]\) at every step; the new \(X\) are the lowest Ritz vectors and \(P\) the part of the update orthogonal to the old \(X\). With a symmetric positive-definite preconditioner \(T\) this is the locally optimal three-term recurrence of conjugate gradients applied to the Rayleigh quotient, and storage stays at \(O(nb)\) with a small projected problem of order \(3b\). vibe-qc’s kernel uses the diagonal preconditioner; it accepts explicit real symmetric matrices only.
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)). The Davidson correction \(t_i\) above is a crude approximation to the Newton step for the eigenvector. Jacobi-Davidson solves instead the projected correction equation for \(t \perp u\),
approximately with an inner Krylov solver (MINRES in vibe-qc, capped by
max_inner), and expands the search space with \(t\). The projection removes
the current eigendirection, so the equation is well posed near convergence,
and no factorisation of \(A - \theta I\) is needed; with a target shift
\(\sigma\), the Ritz values nearest \(\sigma\) are selected at every step. The
outer iteration converges quadratically once the inner solve is accurate,
which is why JD is a candidate for interior roots, at the price of several
operator applications per outer step.
Harmonic Rayleigh-Ritz for interior eigenvalues. Ordinary Ritz values of a growing subspace approach the extremal eigenvalues from inside the spectrum, so an interior eigenvalue near \(\sigma\) is approximated poorly and spurious Ritz values appear between clustered roots. The harmonic extraction (used by the GPLHR family, 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)) replaces the Galerkin condition by a Petrov-Galerkin one on the shifted operator: with \(W = (A - \sigma I) V\), find \((\theta, y)\) such that
The harmonic Ritz values \(\theta\) are reciprocals of ordinary Ritz values
of \((A - \sigma I)^{-1}\) over \(W\), obtained without applying any inverse,
and the ones nearest \(\sigma\) are the reliable interior approximations.
vibe-qc’s harmonic-Davidson variant expands the space with preconditioned
residuals of the shifted problem, using the signed denominator
\((D - \sigma I)^{-1}\) with the preshift floor, selects candidates and
retained vectors by the distance of their harmonic values to \(\sigma\)
(residual size does not change that ordering), restarts while preserving
the target vectors, and reports Rayleigh quotients of the normalised
vectors as eigenvalues. A singular harmonic pencil (\(W^\dagger V\) singular,
for example a diagonal matrix with an eigenvalue exactly at \(\sigma\)) is
rejected rather than dropping that root. Because a zero residual does not
certify the absence of a closer omitted root, selection_verified is
False for every interior iterative solve; the dense nearest-shift solve is
the supported reference for small problems.
Lanczos. The Python reference builds the tridiagonal projection of
\(A\) with full two-pass reorthogonalisation, diagonalises it and returns
Ritz pairs; it has no restart, block or warm-start machinery, and its
converged flag requires the measured residual and the requested root
count. It exists as an exploratory matrix-free reference, not as a
production path.
Interior eigenvalues: Jacobi-Davidson and harmonic-Davidson¶
Status: experimental
solver="jd" and solver="harmonic_davidson" ("gplhr") are experimental
routes reachable only through the library API. No SCF driver uses them,
their numbers are research output, and they are not part of the release
gate. Check every returned pair against \(A x - \lambda x\) before use, and
remember that a converged residual does not prove that the nearest root was
found.
Pass an InteriorEigenProblem with the target shift, or set
SolverOptions.sigma on an operator solve:
import numpy as np
from vibeqc.solvers.eigensolver import (
EigenProblem, InteriorEigenProblem, SolverOptions, solve_eigenproblem,
)
rng = np.random.default_rng(3)
n = 60
A = np.diag(np.linspace(-5.0, 5.0, n)) + 0.01 * rng.standard_normal((n, n))
A = 0.5 * (A + A.T)
sigma = 0.37 # target: the eigenvalue nearest 0.37
problem = InteriorEigenProblem(problem=EigenProblem(matrix=A, n=n), sigma=sigma)
options = SolverOptions(n_roots=1, tol=1e-8, max_iter=500)
jd = solve_eigenproblem(problem, options, solver="jd")
exact = np.linalg.eigvalsh(A)
print(jd.eigenvalues, jd.converged, jd.n_iter)
print(exact[np.argmin(np.abs(exact - sigma))])
try:
hd = solve_eigenproblem(problem, options, solver="harmonic_davidson")
print(hd.eigenvalues, hd.converged, hd.n_iter, hd.residuals)
except RuntimeError as exc: # stagnation or iteration exhaustion
print("harmonic-Davidson did not converge:", exc)
On this 60 by 60 near-diagonal matrix (vibe-qc 0.17.15 development build,
main, 2026-10-11, Apple M-series laptop) Jacobi-Davidson returns
0.42479002 against the dense value 0.424790021 with converged=True after
60 outer iterations and a recomputed residual of 6.6e-9; harmonic-Davidson
returns the same root after 6 iterations with residual 2.1e-9. The two
solvers report failure differently: JD returns converged=False with its
last Ritz value, while harmonic-Davidson raises RuntimeError naming the
shift, the iteration count and the largest residual and suggesting
solver="jd" or a larger max_subspace/max_iter, which is why the
snippet wraps it. Its working block defaults to max(2 n_roots, 4) guard
roots, capped at the dimension (block_size, at least n_roots), and its
search space to min(6 \cdot \text{block} + 10, n) (max_subspace).
The seeded comparison examples/experimental/interior_eigensolvers.py
(six matrices of size 60, 120 and 300 with off-diagonal coupling 0.01 and
0.05, shift 0.4248) gives, on the same build, converged JD roots on all six
cases in 75 to 306 iterations and converged harmonic-Davidson roots on five
of them in 7 to 483 iterations; on the hardest case (n = 300, coupling
0.05) harmonic-Davidson still stops after 500 iterations with a residual of
5e-2 and raises. That failure is retained deliberately: loosening the
tolerance or accepting a nearby converged pair would not be a fix. Without
a shift, Jacobi-Davidson also reaches the lowest eigenvalues, but
davidson and lobpcg are the extremal solvers to use.
Citations in job output¶
Solver citations are routed through vibeqc.output.citations:
assemble(numerics=["eigensolver_davidson"]) and the keys
eigensolver_hermitian_davidson, eigensolver_lobpcg, eigensolver_jd,
eigensolver_harmonic_davidson and eigensolver_gplhr select the entries
quoted above. eigensolver_harmonic_davidson cites the source of the
variant, not a claim of implementing the full published algorithm.
Verification¶
The contract is covered by tests/test_operator_contract.py (complex
generalized roots against SciPy’s independent generalized driver, metric
normalisation and rank truncation, an exact unwanted invariant subspace
showing why a zero residual is not a selection certificate, callback
scratch reuse, and the refusals listed above), tests/test_solver_framework.py
and tests/test_davidson.py (dense against iterative SCF energies and the
partial-spectrum controls), tests/test_tddft_davidson.py (the response
consumer), tests/test_semiempirical_dftb0.py and
tests/test_periodic_bloch_bridge.py (generalized and complex k-point
consumers), and the SCF-route tests that compare each migrated driver’s
Fock spectrum with generalized LAPACK and its converged energies, densities
and spin populations with the previous dense solve:
tests/test_periodic_k_gdf.py, tests/test_periodic_gapw_open_shell.py,
tests/test_periodic_gpw_uks_multik.py, tests/test_rohf.py,
tests/test_rohf_convergence_controls.py, tests/test_periodic_rohf_ewald.py,
tests/test_periodic_uhf_ewald.py, tests/test_rohf_mp2.py and
tests/test_dlpno_mp2.py. These are eigensolver-migration checks; they do
not certify grid or lattice convergence of the experimental periodic
models. The runnable tutorial is
examples/tutorials/unified_operators.py, built
on examples/tutorials/unified_operators.py.
Extending the framework¶
SolverStrategy (ABC)
├── DenseSolver -> numpy.linalg.eigh
├── DavidsonSolver -> _vibeqc_core.davidson_solve (C++)
├── HermitianDavidsonSolver -> _vibeqc_core.davidson_solve_hermitian (C++)
├── LOBPCGSolver -> _vibeqc_core.lobpcg_solve (C++)
├── JDSolver -> _vibeqc_core.jd_solve (C++, experimental)
├── GPLHRSolver -> _vibeqc_core.gplhr_solve (C++, experimental)
└── LanczosSolver -> pure Python
A SolverStrategy subclass registers itself at class-definition time under
its lower-cased class name and the entries of its _aliases list, and is
then available to solve_eigenproblem(..., solver="mysolver"). For the
strict entry point it also needs a row in solver_capabilities and in the
accepted-option table of solve_operator, which is how an unsupported
option becomes a refusal instead of a silent default:
from vibeqc.solvers.eigensolver import SolverStrategy, SolverResult
class MySolver(SolverStrategy):
_aliases = ["mysolver", "my"]
supports_extremal = True
supports_matrix_free = True
def solve(self, problem, options):
... # your algorithm
return SolverResult(eigenvalues=values, eigenvectors=vectors,
n_iter=iterations, converged=converged)
References¶
The database entries cited above are 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), 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), 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), 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), 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) and Lehtola, Susi (2019). *Curing basis set overcompleteness with pivoted Cholesky decompositions.* Journal of Chemical Physics, **151**(24), 241102. doi:[10.1063/1.5139948](https://doi.org/10.1063/1.5139948). Not in the citation database:
Liu, B. The simultaneous expansion method for the iterative solution of several of the lowest eigenvalues and corresponding eigenvectors of large real-symmetric matrices, in Numerical Algorithms in Chemistry: Algebraic Methods, LBL-8158, Lawrence Berkeley Laboratory, 49 (1978). The block Davidson method.
Kresse, G. and Furthmüller, J. Phys. Rev. B 54, 11169 (1996). DOI: 10.1103/PhysRevB.54.11169. Blocked Davidson for Kohn-Sham orbitals.
Vecharynski, E., Yang, C. and Xue, F. SIAM J. Sci. Comput. 38, A500 (2016). DOI: 10.1137/15M1027413. The full generalized preconditioned locally harmonic residual (GPLHR) algorithm that the local variant does not implement.
Lanczos, C. J. Res. Nat. Bur. Stand. 45, 255 (1950). DOI: 10.6028/jres.045.026. Paige, C. C. J. Inst. Math. Appl. 10, 373 (1972). DOI: 10.1093/imamat/10.3.373. The Lanczos recurrence and its reorthogonalisation.
See also¶
SCF convergence: accelerators, level shifts and the second-order finalizers that act between diagonalisations.
Initial guesses: the seed the first solve starts from.
ROHF and ROKS: the Roothaan loop’s complete-spectrum rule.
Operator contract, consumer inventory and the eigensolver literature assessment.
Eigensolvers and operators tutorial, which walks through the runnable script
examples/tutorials/unified_operators.py.