SCF convergence, initial guesses, and TRAH: getting the reference to converge¶
Every wavefunction and DFT calculation in OpenQP begins by solving the
self-consistent field (SCF) equations: you guess a set of molecular orbitals,
build the Fock matrix they imply, diagonalize it to get new orbitals, and repeat
until the orbitals stop changing. Most closed-shell molecules converge in a dozen
iterations without any thought. But stretched bonds, transition-metal complexes,
diradicals, and near-degenerate states can make the default extrapolation stall,
oscillate, or slide into the wrong solution. This tutorial shows the two knobs that
fix almost all of those cases — the initial guess ([guess] type) and the
converger ([scf] converger_type, including SOSCF and TRAH) — on a
plain water/PBE calculation you can run in a second.
A little theory¶
The SCF cycle is a fixed-point iteration, and a naive "plug the new orbitals straight back in" scheme frequently diverges. OpenQP gives you a ladder of convergers, each doing more work per iteration but converging more robustly:
- DIIS (the default) extrapolates the next Fock matrix from a history of previous ones. Cheap and excellent for well-behaved, closed-shell cases.
- SOSCF (second-order SCF) uses curvature — an approximate orbital-rotation Hessian — to take Newton-like steps once you are near the solution, so it finishes the tail of a hard convergence faster than DIIS alone. It is the converger used in this tutorial.
- TRAH (trust-region augmented-Hessian SCF) is the heavy artillery: a genuine second-order method wrapped in a trust region so the Newton step is only taken as far as it can be trusted. It converges cases where DIIS and SOSCF both fail — strongly correlated references, tricky open shells — at a higher per-iteration cost.
The other half of the battle is where you start. A better initial guess puts
you inside the basin of the right solution, which cuts iterations and avoids
converging to an excited or symmetry-broken state. OpenQP's default is the
extended-Hückel guess (type=huckel); it also offers hcore, sap, minao,
modhuckel, and JSON-restart guesses. For the full contract behind each option,
see the [scf]
and [guess]
keyword pages.
Input-file style¶
The runnable deck is inputs/h2o_scf_soscf.inp — water
in the 6-31G basis, a spin-restricted PBE reference, converged with the SOSCF
converger starting from a Hückel guess. Annotated:
[input]
system=
8 0.000000000 0.000000000 -0.041061554 # O (Angstrom)
1 -0.533194329 0.533194329 -0.614469223 # H
1 0.533194329 -0.533194329 -0.614469223 # H
charge=0
method=hf # SCF engine (HF machinery; DFT is switched on by functional)
basis=6-31g
runtype=energy # single-point SCF energy
functional=pbe # non-empty -> Kohn-Sham DFT with the PBE functional
d4=false # no DFT-D4 dispersion correction
[guess]
type=huckel # extended-Hückel initial orbital guess (the default)
save_mol=true # keep the converged orbitals/molecule data for restart
[scf]
type=rhf # closed-shell restricted reference (rhf | uhf | rohf)
multiplicity=1 # singlet ground state
converger_type=soscf # second-order SCF converger (vs. the default diis / or trah)
maxit=60 # max SCF iterations before giving up
conv=1.0e-6 # energy/density convergence threshold (Hartree)
[dftgrid]
rad_npts=96 # radial quadrature points for the DFT XC grid
ang_npts=302 # angular (Lebedev) points per radial shell
pruned= # pruning scheme; empty = use the default (unpruned) grid
Key points:
- The reference is DFT, not bare HF.
method=hfselects the SCF machinery, but a non-emptyfunctional(pbe) promotes it to Kohn-Sham DFT. Leavingfunctionalempty would give a Hartree-Fock reference instead. [guess] typechooses where the SCF starts.huckelis the robust default; switching tohcore,sap, orminaochanges only the starting orbitals, not the final answer, but can change how many iterations (and whether) you converge.save_mol=truewrites the converged orbitals so a later run can restart from them.[scf] converger_typechooses how it gets there. This deck usessoscf. Change one line to switch strategy:
[scf]
converger_type=diis # default first-order DIIS extrapolation
[scf]
converger_type=trah # trust-region augmented-Hessian: for hard, stalling cases
All three converge this easy water case to the same energy; the difference shows
up on difficult references, where diis may stall and trah still converges.
- maxit and conv are the stopping rules. Raise maxit (here 60, above the
default of 30) for slow cases; tighten conv (e.g. 1.0e-8) when you need
high-precision energies or gradients.
- [dftgrid] sets the XC integration grid. rad_npts × ang_npts is a fine
(96, 302) grid; a coarse grid can itself cause the SCF energy to jitter and fail
to converge, so grid quality and SCF convergence are linked.
Python style¶
The equivalent calculation with the OpenQP Python API is
inputs/h2o_scf_soscf.py. job.theory.dft(...) sets the
[input] functional, the basis, and the [scf] reference in one call; any extra
keyword (here converger_type, maxit, conv) is forwarded straight into the
[scf] section, so it lands exactly where the input-file deck puts it.
from oqp.openqp import OpenQP
job = OpenQP("h2o_scf_soscf", silent=1)
job.molecule(
"""
O 0.000000000 0.000000000 -0.041061554
H -0.533194329 0.533194329 -0.614469223
H 0.533194329 -0.533194329 -0.614469223
""",
charge=0,
multiplicity=1,
)
# DFT/PBE reference; extra kwargs are written to [scf].
# converger_type="soscf" == [scf] converger_type=soscf
job.theory.dft(
functional="pbe",
basis="6-31g",
reference="rhf", # [scf] type=rhf
converger_type="soscf", # second-order SCF instead of DIIS
maxit=60, # [scf] maxit
conv=1.0e-6, # [scf] conv
)
mol = job.run()
print("SCF energy:", mol.get_scf_energy())
print("Results:", mol.get_results())
To try a different converger, change only the forwarded keyword:
job.theory.dft(functional="pbe", basis="6-31g", reference="rhf", converger_type="diis") # DIIS
job.theory.dft(functional="pbe", basis="6-31g", reference="rhf", converger_type="trah") # TRAH
The full script is inputs/h2o_scf_soscf.py.
Run it¶
Input-file style (from the inputs/ folder):
cd scf-convergence/inputs
openqp h2o_scf_soscf.inp
Python style:
cd scf-convergence/inputs
python h2o_scf_soscf.py
Both need OpenQP installed (pip install openqp) and produce the same SCF energy.
Reading the output¶
This is a single-point SCF run, so the number you want is the converged SCF (Kohn-Sham) total energy.
- In the log file (
<project>.log) watch the iteration table: each row prints the energy and the density/gradient change, and you want to see that change shrink monotonically pastconv=1.0e-6and the run report converged withinmaxititerations. If it hitsmaxitwithout converging, that is the signal to switchconverger_type(trytrah) or improve the[guess]. - From Python,
mol.get_scf_energy()returns the converged SCF total energy, andmol.get_results()returns the results dictionary (theenergyfield matches what is written to<project>.json). - Because
[guess] save_mol=true, the converged orbitals are also saved, so a follow-up calculation can restart from them (via ajson/autoguess) instead of re-converging from Hückel.
The physical energy is identical no matter which converger you pick — DIIS, SOSCF, and TRAH only differ in how reliably and how fast they reach it. The point of this tutorial is to know which knob to turn when the default DIIS run refuses to converge.
Manual¶
[scf]keyword reference (converger_type=diis|soscf|trah, plustype,multiplicity,maxit,conv): https://open-quantum-platform.github.io/openqp-docs/keywords/scf/[guess]keyword reference (type=huckel|hcore|sap|minao| … , andsave_molrestart): https://open-quantum-platform.github.io/openqp-docs/keywords/guess/[dftgrid]keyword reference (rad_npts,ang_npts,pruned): https://open-quantum-platform.github.io/openqp-docs/keywords/dftgrid/- HF / DFT workflow overview: https://open-quantum-platform.github.io/openqp-docs/workflows/hf-dft/