Stiff molecular SCF: rescuing oscillation with EDIIS+DIIS

Vanilla Pulay DIIS is the workhorse SCF accelerator for organic- chemistry-style problems with a clear HOMO-LUMO gap. It is not the right tool for broken-symmetry singlets, transition-metal complexes, near-degenerate open-shell radicals, and any system with a small or vanishing gap — in that regime DIIS oscillates or plateaus far from the true minimum, sometimes hours into a calculation that should have taken minutes. The default molecular accelerator in vibe-qc v0.8.0 — SCFAccelerator.EDIIS_DIIS — rescues those cases by switching the SCF history-mixing rule based on how close the iterates are to the fixed point.

This tutorial walks through

  • why DIIS fails on stiff cases (one-paragraph theory),

  • the Garza-Scuseria 2012 EDIIS+DIIS hybrid recipe,

  • a clean reproducer where vanilla DIIS gets stuck and EDIIS+DIIS doesn’t, and

  • the dial-by-dial recommendations for what to change when the hybrid alone isn’t enough.

It’s the molecular companion to the periodic SCF-convergence tutorial.

One-paragraph theory

DIIS (Pulay 1980) extrapolates to the SCF fixed point by minimising the norm of the commutator error vector \(\mathbf{e} = \mathbf{F}\mathbf{D}\mathbf{S} - \mathbf{S}\mathbf{D}\mathbf{F}\) over a sliding window of past Fock + density pairs. It works beautifully when the iterates already sit in the asymptotic regime (roughly \(\lVert\mathbf{e}\rVert_F < 10^{-1}\)). Outside that regime — small HOMO-LUMO gap, broken-symmetry start, transition-metal ligand-field-degenerate orbitals — the commutator is dominated by modes that are physically irrelevant to convergence, and the DIIS extrapolation oscillates between near-degenerate minima.

EDIIS (Energy-DIIS, Kudin, Scuseria, Cancès 2002) replaces the commutator objective with a quadratic upper bound to the energy on the convex hull of the stored (F, D) pairs and minimises that over a simplex. Far from convergence, EDIIS lands on a physically meaningful descent direction even when the commutator is dominated by noise. Up close to the fixed point, however, EDIIS plateaus because the quadratic bound becomes too loose to drive the last few orders of magnitude.

EDIIS+DIIS (Garza, Scuseria 2012) takes the obvious step: use EDIIS while the commutator is large, switch to plain DIIS once \(\lVert\mathbf{e}\rVert_F\) drops below a threshold (default 1e-1, matching PySCF and ORCA). The result is robust convergence on the stiff cases plus the asymptotic quadratic-like behaviour DIIS delivers near the minimum.

The recipe

For molecular SCF in v0.8.0, EDIIS+DIIS is already the default — every RHFOptions, UHFOptions, RKSOptions, UKSOptions instance starts with:

opts.scf_accelerator             = vq.SCFAccelerator.EDIIS_DIIS
opts.ediis_diis_switch_threshold = 1e-1     # ‖e‖_F below this → DIIS
opts.diis_subspace_size          = 8         # rolling history cap

If you’ve been writing scripts that explicitly set scf_accelerator = SCFAccelerator.DIIS — drop the line, the default is now the production hybrid. For PySCF / vibe-qc-pre-v0.8 parity runs that need vanilla DIIS:

opts.scf_accelerator = vq.SCFAccelerator.DIIS

A reproducer where vanilla DIIS struggles

Stretched H₂O at HF/cc-pVDZ is the classic textbook example of a near-singlet-triplet near-degeneracy that breaks vanilla DIIS. At the equilibrium geometry the HOMO-LUMO gap is healthy (>10 eV); stretch the bonds to ~1.6 Å and the gap collapses as the closed-shell wavefunction becomes a poor reference for the multi-reference dissociation limit. DIIS responds by oscillating between two near-degenerate density solutions.

Working directory:

~/vibeqc-runs/stretched-h2o/
    input-stretched.py
    h2o-stretched.xyz

h2o-stretched.xyz:

3
H2O, both bonds stretched to 1.60 Angstrom, HOH angle 104.5 deg
O   0.00000   0.00000   0.00000
H   0.00000   1.26824  -0.97920
H   0.00000  -1.26824  -0.97920

input-stretched.py:

from pathlib import Path
import vibeqc as vq

HERE = Path(__file__).parent

mol = vq.Molecule.from_xyz(HERE / "h2o-stretched.xyz")
basis = vq.BasisSet(mol, "cc-pvdz")

# Baseline: vanilla DIIS — note the explicit override of the
# v0.8.0 EDIIS_DIIS default.
opts_diis = vq.RHFOptions()
opts_diis.scf_accelerator = vq.SCFAccelerator.DIIS
opts_diis.max_iter        = 60
opts_diis.conv_tol_energy = 1e-8

# Default (v0.8.0+) — EDIIS+DIIS hybrid.
opts_hybrid = vq.RHFOptions()
opts_hybrid.max_iter        = 60
opts_hybrid.conv_tol_energy = 1e-8

res_diis   = vq.run_rhf(mol, basis, opts_diis)
res_hybrid = vq.run_rhf(mol, basis, opts_hybrid)

print(f"DIIS         : converged = {res_diis.converged}, "
      f"iters = {res_diis.n_iter}, E = {res_diis.energy:.8f}")
print(f"EDIIS+DIIS   : converged = {res_hybrid.converged}, "
      f"iters = {res_hybrid.n_iter}, E = {res_hybrid.energy:.8f}")

What you should see (numbers fluctuate ±1 iter run-to-run depending on tie-breaks in the simplex solver):

DIIS         : converged = False, iters = 60, E = -75.94...
EDIIS+DIIS   : converged = True,  iters = 17, E = -75.95...

The hybrid not only converges where vanilla DIIS doesn’t, it converges to a slightly lower (correct) energy because vanilla DIIS’s oscillating iterates never sit at the minimum.

Tip

For a sharper demonstration, stretch the bonds further (to 2.0 Å or beyond). Both methods will struggle, but EDIIS+DIIS still has a much better chance — and the gap between them widens.

What changes inside the iteration

Both runs walk through the same SCF inner loop (scf_convergence § Application order):

for iter in 1..max_iter:
    1. D_used  = damping(D_prev, D)
    2. F       = Hcore + G[D_used]
    3. F       = level_shift(F, S, D_used)
    4. F_extra = accelerator.extrapolate(F, ...)   # ← what differs
    5. C, eps  = step(F_extra, ...)
    6. D       = build_density(C)
    7. check convergence

The only thing EDIIS+DIIS swaps out is step 4. While \(\lVert\mathbf{e}\rVert_F > 10^{-1}\) the extrapolator solves the simplex $$ c^\star = \arg\min_{c \in \Delta_n}; \sum_i c_i ,E[\mathbf{F}_i,\mathbf{D}_i]

  • \tfrac{1}{2}\sum_{i,j} c_i c_j ,\mathrm{tr}!\left[(\mathbf{F}_j - \mathbf{F}_i)(\mathbf{D}_j - \mathbf{D}_i)\right] $\( (Kudin et al. 2002, eq. 7) where \)\Delta_n\( is the unit simplex. Once \)\lVert\mathbf{e}\rVert_F$ drops below the threshold the same slot fills with the classic Pulay least-squares fit on the AO commutator. The switch is one-way: once DIIS takes over, the hybrid does not flip back.

In the run above, the EDIIS phase typically runs for the first 6-10 iterations until \(\lVert\mathbf{e}\rVert_F\) falls below \(10^{-1}\); the asymptotic DIIS phase then nails the residual ~7 orders of magnitude in 5-7 iterations. The .scf.jsonl structured log records the per-iteration norm so you can plot the crossover yourself:

import json, matplotlib.pyplot as plt
rows = [json.loads(l) for l in open("output-stretched.scf.jsonl")
        if '"event": "iter"' in l]
plt.semilogy([r["iter"] for r in rows],
             [r["commutator_norm"] for r in rows],
             marker="o")
plt.axhline(0.1, color="gray", ls="--",
            label="EDIIS → DIIS switch threshold")
plt.xlabel("SCF iter")
plt.ylabel("‖F D S − S D F‖_F")
plt.legend()
plt.savefig("ediis-diis-switch.png", dpi=150)

(structured_log=True on run_job writes the .scf.jsonl — see output_files § Structured log.)

When the hybrid alone isn’t enough

EDIIS+DIIS does most of the work for routine stiff systems. For the really hostile cases (transition-metal complexes with ligand-field-degenerate d-shells, large-active-space broken- symmetry singlets) a few more dials matter:

Symptom

Fix

EDIIS+DIIS plateaus near \(10^{-3}\) Ha and won’t go further

Add a second-order finalizeropts.newton_threshold = 1.0 activates the Phase D2c Newton step when \(\lVert\mathbf{e}\rVert_F < 1\). Newton finishes the last 3-5 orders of magnitude in quadratic time.

The hybrid oscillates from iter 1 (degenerate occupations)

Add a level shiftopts.level_shift = 0.2 keeps occupied / virtual blocks apart while the density settles. Compose freely with EDIIS+DIIS.

Wrong SCF stationary point (e.g. an excited determinant)

Pin the initial guess to a better starting density — opts.initial_guess = vq.InitialGuess.SAD instead of the AUTO default, or use the MOM tutorial for excited-state targeting.

Newton’s per-iteration cost is too high (each Newton step does multiple Fock builds)

Use TRAH (opts.trah_threshold = 1.0) — same Hessian, adaptive trust radius, more robust to overshooting — or SOSCF (opts.soscf_threshold = 1.0) for the cheap-per-step alternative on hybrid functionals.

You want vanilla DIIS for parity with a pre-v0.8.0 reference

opts.scf_accelerator = vq.SCFAccelerator.DIIS

The full decision tree, including which finalizer plays well with KS-DFT and what newton_opts.cg_tol / trust_radius to start from, is in user_guide/scf_convergence.

Resources

H₂O / cc-pVDZ at the stretched geometry: ~50 ms per SCF iteration on a modern laptop. The full DIIS run (60 max iters, no convergence) finishes in ~3 s; EDIIS+DIIS (~17 iters, converged) in ~1 s. Peak memory is well under 100 MB.

References

  • DIIS — P. Pulay, “Convergence acceleration of iterative sequences. The case of SCF iteration,” Chem. Phys. Lett. 73, 393 (1980); and “Improved SCF convergence acceleration,” J. Comput. Chem. 3, 556 (1982).

  • EDIIS — K. N. Kudin, G. E. Scuseria, E. Cancès, “A black-box self-consistent field convergence algorithm: One step closer,” J. Chem. Phys. 116, 8255 (2002). doi:10.1063/1.1470195

  • EDIIS+DIIS hybrid — A. J. Garza, G. E. Scuseria, “Comparison of self-consistent field convergence acceleration techniques,” J. Chem. Phys. 137, 054110 (2012). doi:10.1063/1.4740249

Both EDIIS papers ship in the bundled citation database; any run_job that hits the EDIIS or EDIIS_DIIS route will cite them automatically in the run’s .bibtex and .references siblings.

Next

  • scf_convergence — full SCF- convergence reference: KDIIS, level shifts, FMIXING, the four second-order finalizers (Newton / TRAH / SOSCF / quadratic fallback), and the unified iteration order.

  • Initial-guess walkthrough — the other lever for stiff SCFs: start from a better density.

  • Periodic SCF convergence — the periodic-side companion. Damping, DIIS, level shifts for tight-cell crystals.