Initial guesses — hands-on walkthrough¶
A practical tour of the initial-guess methods you will reach for most often (HCORE, SAD, SAP, AUTO). Copy each block into a Python interpreter to follow along. By the end you’ll have seen:
The three methods producing the same converged energy on a well-behaved system.
SAD rescuing convergence on a system where HCore lands on the wrong minimum (OH/6-31G*).
SAD rescuing a periodic SCF that HCore drives into the bombing failure mode (NaCl/STO-3G).
AUTO picking the right method for each case automatically.
Background theory + math is in
docs/user_guide/initial_guess.md;
this tutorial is the running tour.
Why an SCF needs a guess¶
Hartree-Fock and Kohn-Sham SCF solve a chicken-and-egg problem. The Fock matrix \(\mathbf{F}\) that defines the orbitals is built from the electron density \(\mathbf{D}\), but \(\mathbf{D}\) is built from those same orbitals. Neither exists without the other, so the iteration has to be primed with a starting guess and then refined until the input and output densities agree (self-consistency).
The guess does not change where a well-behaved SCF ends up: every method below reaches the same converged energy, to better than \(10^{-10}\) Ha. What it changes is the path, namely how many iterations that takes, and occasionally which solution you land on when a system has more than one stationary point (Step 2 is exactly that trap).
The ready methods at a glance¶
This tour walks the four you will reach for most: HCORE, SAD,
SAP, and AUTO. vibe-qc also ships three more molecular guesses
(PATOM, HUECKEL, MINAO); see the note below.
Guess |
Builds |
Idea in one line |
Cost and character |
|---|---|---|---|
|
a Fock matrix |
Drop all electron-electron repulsion and start from \(\mathbf{F} = \mathbf{T} + \mathbf{V}_{\rm ne}\) (kinetic plus nuclear attraction only). |
Free, but the shell structure is wrong; it can drive ionic solids to nonsense energies before recovering. |
|
a density |
Stack pre-converged atomic densities, one block per atom, into a molecular density. |
One tiny atomic SCF per element (cached); robust on every system class vibe-qc supports. |
|
a Fock matrix |
Replace the bare nuclei with screened atomic potentials fit to exact atomic SCFs, so the first orbitals already have the right shell structure, with no atomic SCF to run. |
A handful of integrals per atom; the cheap shell-correct choice, with its biggest iteration savings on heavy atoms (Lehtola 2020). |
|
picks one |
Inspect the system and choose: |
The default in every SCF options struct. |
In one sentence: HCORE is the crude baseline, SAD is the robust
workhorse, SAP is the cheap shell-correct option for closed-shell
molecules, and AUTO chooses between them so you usually do not have
to. The full math for each method, the per-spin UHF / UKS packaging,
and the SAP citation requirement live in
initial_guess.md.
Is there a Hückel guess? (and PATOM, MINAO, READ)¶
Yes. As of the v0.9.x increment that landed them, PATOM, HUECKEL,
and MINAO are implemented molecular guesses you can select directly:
PATOM: SAD followed by one in-field re-polarisation step, so the atomic densities relax in the molecular field before the SCF proper. Good for transition-metal d-shell ordering. Builds on SAD (Van Lenthe 2006); the ORCA PAtom idea.HUECKEL: a parameter-free generalised Wolfsberg-Helmholz (extended-Hückel) guess over computed atomic-orbital energies (Wolfsberg-Helmholz 1952, Hoffmann 1963, Lehtola 2019). Bothinitial_guess="huckel"and"hueckel"are accepted.MINAO: free-atom minimal-basis (ANO-RCC) reference densities projected onto your basis (Knizia 2013).
All three are molecular-only for now: a periodic SCF with any of
them raises a clear “not yet implemented for periodic” error, so use
SAD (the periodic AUTO default) for solids. READ (restart from a
prior calculation, via read_from= or opts.read_path) is now
implemented too, and likewise molecular-only; see
initial_guess.md for its API. Step 6
shows when overriding AUTO is worth it.
Setup¶
import vibeqc as vq
from vibeqc import (
Atom, Molecule, BasisSet,
RHFOptions, UHFOptions, PeriodicRHFOptions,
PeriodicSystem,
run_rhf, run_uhf, run_rhf_periodic_gamma,
)
import numpy as np
A2B = 1.0 / 0.529177210903 # Angstrom → bohr conversion
Step 1 — Parity on H₂O (the well-behaved case)¶
When SCF behaves itself, every guess converges to the same energy. The point of running this is to see that the choice is about efficiency, not correctness.
mol = Molecule([
Atom(8, [0.0, 0.0, 0.0]),
Atom(1, [0.0, 1.43, -0.98]),
Atom(1, [0.0, -1.43, -0.98]),
])
basis = BasisSet(mol, "6-31g*")
def run_with_guess(kind):
opts = RHFOptions()
opts.initial_guess = kind
opts.conv_tol_energy = 1e-10
return run_rhf(mol, basis, opts)
for kind in [vq.InitialGuess.HCORE,
vq.InitialGuess.SAD,
vq.InitialGuess.SAP,
vq.InitialGuess.AUTO]:
r = run_with_guess(kind)
print(f"{kind.name:<6} E = {r.energy:.10f} n_iter = {r.n_iter}")
You’ll see something like:
HCORE E = -76.0107424867 n_iter = 12
SAD E = -76.0107424867 n_iter = 10
SAP E = -76.0107424867 n_iter = 11
AUTO E = -76.0107424867 n_iter = 11
All four reach the same energy to better than \(10^{-10}\) Ha. Iteration counts vary by ±1–2 — well within the noise that DIIS introduces on a small system. AUTO resolved to SAP for this closed-shell light-atom case.
Why SAP didn’t win here: STO-3G / 6-31G* are minimal basis sets where SAP’s shell-structure-correct initial orbitals are nearly identical to what HCore-plus-DIIS produces after one extra iteration. SAP’s advantage grows with basis size and atomic number.
Step 2 — The OH false-minimum trap¶
This is the textbook example where the initial guess changes which minimum the SCF finds. With HCore, OH/6-31G* UHF converges to a local minimum ~0.16 Ha above the true UHF minimum.
mol = Molecule(
[Atom(8, [0.0, 0.0, 0.0]),
Atom(1, [0.97 * A2B, 0.0, 0.0])], # 0.97 Å O-H bond
multiplicity=2, # doublet radical
)
basis = BasisSet(mol, "6-31g*")
def run_uhf_with_guess(kind):
opts = UHFOptions()
opts.initial_guess = kind
opts.conv_tol_energy = 1e-10
opts.max_iter = 300 # HCore needs many iters
return run_uhf(mol, basis, opts)
# The PySCF / textbook reference UHF energy is -75.3809309907.
TRUE_E = -75.3809309907
for kind in [vq.InitialGuess.HCORE, vq.InitialGuess.SAD,
vq.InitialGuess.AUTO]:
r = run_uhf_with_guess(kind)
print(f"{kind.name:<6} E = {r.energy:.6f} "
f"S² = {r.s_squared:.3f} "
f"Δ from true min = {r.energy - TRUE_E:+.6f}")
Typical output:
HCORE E = -75.221182 S² = 0.764 Δ from true min = +0.159749 ← false minimum
SAD E = -75.380931 S² = 0.755 Δ from true min = +0.000000 ← correct
AUTO E = -75.380931 S² = 0.755 Δ from true min = +0.000000 ← AUTO → SAD
AUTO sees is_open_shell=True (because n_alpha != n_beta) and
picks SAD, sidestepping the trap. This is one reason the default
flipped from a hardcoded SAD to AUTO in v0.9 — the engine now picks
the right method automatically without you having to remember.
Step 3 — NaCl, the periodic bombing case¶
This is the failure mode that drove the v0.5.6 → v0.9 default flip. HCore on an ionic insulator can push the first-iteration energy into \(\pm 10^4\) Ha territory, and DIIS may never recover. SAD lives at the unit-cell g=0 cell and gives a sane starting density.
# NaCl rocksalt; STO-3G is too small for production but enough to
# expose the failure mode.
a = 10.7 # bohr (close to 5.64 Å)
lat = a * np.eye(3)
sysp = PeriodicSystem(
3, lat,
[Atom(11, [0.0, 0.0, 0.0]), # Na
Atom(17, [a/2, a/2, a/2])], # Cl
charge=0, multiplicity=1,
)
basis = BasisSet(sysp.unit_cell_molecule(), "sto-3g")
# AUTO → SAD for any periodic system. This is the default now.
opts = PeriodicRHFOptions()
opts.conv_tol_energy = 1e-8
opts.max_iter = 50
# opts.initial_guess defaults to InitialGuess.AUTO
result = run_rhf_periodic_gamma(sysp, basis, opts)
print(f"AUTO → SAD: E/cell = {result.energy:.6f} "
f"n_iter = {result.n_iter} converged = {result.converged}")
# Re-run with explicit HCORE to see the failure (or the slow recovery):
opts.initial_guess = vq.InitialGuess.HCORE
opts.max_iter = 100
try:
result = run_rhf_periodic_gamma(sysp, basis, opts)
print(f"HCORE: E/cell = {result.energy:.6f} "
f"n_iter = {result.n_iter} converged = {result.converged}")
print("(HCore *may* recover on this small basis; on larger "
"basis sets it bombs.)")
except RuntimeError as e:
print(f"HCORE bombed: {e}")
The SAD/AUTO run converges in 5–15 iterations. The HCORE run either recovers slowly or bombs, depending on basis size and damping. On real-research basis sets like pob-TZVP, HCore on NaCl never recovers without level-shift intervention — which is precisely why AUTO defaults to SAD for periodic systems.
Step 4 — Inspecting AUTO’s choice¶
The resolved kind is logged on every SCF run. To see it, look at
result.scf_trace (or just look at the live SCF log if you have
progress=True):
mol = Molecule([
Atom(6, [0.0, 0.0, 0.0]), # C
Atom(8, [0.0, 0.0, 2.20]), # O — CO molecule
])
basis = BasisSet(mol, "cc-pvdz")
r = run_rhf(mol, basis)
# Look at the log output during the run; you'll see:
# [scf] initial guess: SAP (Lehtola/Visscher/Engel 2020, sap_helfem_large)
For programmatic inspection of what AUTO would pick for a given system, you can call the engine directly:
from vibeqc._vibeqc_core import _guess_closed_shell_density
# AUTO returns a density (for SAD-/PATOM-like resolutions) or None
# (for HCORE / Fock-mode kinds where the SCF core fills in). When
# AUTO resolves to SAP, the engine internally diagonalises F_SAP
# and returns the resulting density — non-None.
D = _guess_closed_shell_density(
mol, basis, mol.n_electrons() // 2,
vq.InitialGuess.AUTO, is_periodic=False,
)
print("AUTO yielded a starting density:", D is not None)
Step 5 — The transition-metal nuance¶
Closed-shell SAP can land the wrong d-shell occupation for a TM atom because the underlying atomic potential is averaged over an open d shell. AUTO detects TM atoms by Z range and resolves to SAD:
# Iron pentacarbonyl Fe(CO)5 — closed-shell singlet but with iron Z=26.
# AUTO will route this to SAD even though it's closed-shell.
fe = [Atom(26, [0.0, 0.0, 0.0])] # just iron, for illustration
mol = Molecule(fe, charge=2, multiplicity=1) # Fe²⁺ d⁶ low-spin
basis = BasisSet(mol, "cc-pvdz")
opts = RHFOptions()
opts.initial_guess = vq.InitialGuess.AUTO
# In the log you'll see:
# [scf] initial guess: SAD (superposition of atomic densities)
# because has_transition_metal triggered.
(Once PATOM ships, AUTO will route TM molecules to PATOM, which
re-polarises the SAD atomic densities in the molecular field — the
right answer for d-shell ordering.)
Step 6 — Picking a non-default deliberately¶
Most code shouldn’t override AUTO. Cases where you might:
Unit tests / reference outputs — pin
HCOREfor implementation-independent comparisons.Benchmarking SAP vs SAD on heavy atoms — pin
SAPto measure its iteration-count win.Diagnosing a hard convergence case — switch back to
HCOREto see if the issue is the guess or the SCF body.
# Pinning HCORE for a deterministic regression test:
opts = RHFOptions()
opts.initial_guess = vq.InitialGuess.HCORE
opts.conv_tol_energy = 1e-12
r_ref = run_rhf(mol, basis, opts)
What’s next¶
Theory and math for each method:
initial_guess.md.Tuning SCF convergence (damping, DIIS, EDIIS+DIIS hybrid):
scf_convergence.md.READ(restart a molecular SCF from a prior result,.qvf, or.molden) now ships, alongsidePATOM,HUECKEL, andMINAO. The full initial-guess menu is ininitial_guess.md.
See also¶
Runnable example:
examples/molecular/input-h2o-initial-guess-comparison.py— a longer self-contained version of Steps 1–2.