The Hartree-Fock equations

Quantum Chemistry

On the previous page the helium energy formula was derived for a fixed orbital , which was in turn the STO-3G contraction we wrote down by hand. That's not yet a calculation — it's an evaluation. To do quantum chemistry we have to let the orbital itself become the unknown and ask: of all possible , which one minimizes the energy? The Hartree-Fock equations are the answer to that question.


The variational problem on the determinant

Take the energy of an -electron Slater determinant as a functional of the spin orbitals :

The first sum is the one-electron part (kinetic + nuclear attraction); the second is the two-electron Coulomb minus exchange. We want to minimize with respect to the , with the constraint that they remain orthonormal: . Standard Lagrangian move:

Vary with respect to each and set the variation to zero. The matrix is Hermitian, so we can rotate the spin orbitals among themselves to diagonalize it. The variational equations then collapse to a one-electron eigenvalue problem:

The Fock operator is the one-electron Hamiltonian plus an averaged interaction with all the other occupied spin orbitals:

where the Coulomb operator acts on a state as — the classical Coulomb potential from the charge density of orbital — and the exchange operator is the same thing with the orbitals swapped, a purely quantum object with no classical analogue.

The thing to notice: depends on the we are trying to find. We can't just set up the operator and diagonalize once; the operator is a functional of its own eigenvectors. This is the self-consistency problem, the "self-consistent" in self-consistent field. Pages 6 and 7 are about how to solve it.


Closed-shell simplification

For a closed-shell ground state — every occupied spatial orbital has both spin partners present — the spin sums collapse. Each spatial orbital contributes twice as a Coulomb source (once via , once via ) but only once as an exchange source against any given target spin (because the opposite-spin partner has zero spin overlap, so its contribution vanishes). The result is the closed-shell Fock operator:

For the closed-shell helium of page 4 there is exactly one occupied spatial orbital , so . The matrix element works out to (the same-orbital exchange integral cancels half of the same-orbital Coulomb), which is the orbital energy . We can read off the relation between total and orbital energies:

The total energy is not twice the orbital energy. If you just doubled you would count the electron-electron repulsion twice — once when each electron sits in the averaged field of the other, double-counting the single repulsion event. Subtracting fixes the over-count. This double-counting structure is going to appear in every HF energy expression we write from now on.


The Fock matrix in a basis

Pick a finite basis of single-particle functions (for us, the three primitive Gaussians) and write the orbital as . The Fock matrix in that basis is

where is a primitive electron-repulsion integral and the density matrix in the closed-shell case is

The factor of 2 absorbs the spin pair. The total energy in this matrix form is the symmetric average . Once the four-index tensor is in hand, the rest is just contractions over indices.


The code (new this page)

Three new helpers — the four-index electron-repulsion tensor, the closed-shell Fock-matrix builder, and the energy-from-density expression — plus a sanity check that pushes the helium calculation of page 4 through the new machinery and recovers the same number.

# ---- four-index electron-repulsion tensor over primitive Gaussians ----

def build_eri_tensor(alphas):
    """ERI[i,j,k,l] = (g_i g_j | g_k g_l) for same-centre 1s primitives."""
    n = len(alphas)
    ERI = np.zeros((n, n, n, n))
    for i in range(n):
        for j in range(n):
            for k in range(n):
                for l in range(n):
                    ERI[i, j, k, l] = coulomb_g(alphas[i], alphas[j], alphas[k], alphas[l])
    return ERI


# ---- closed-shell Fock matrix ----

def build_fock(h, ERI, P):
    """F_ij = h_ij + sum_{kl} P_kl [(ij|kl) - 1/2 (ik|jl)].

    Closed-shell: density matrix P is built from the doubly-occupied
    orbitals as P_kl = 2 sum_a c_ka c_la (the factor of 2 is the spin pair).
    """
    J = np.einsum('ijkl,kl->ij', ERI, P)        # Coulomb part
    K = np.einsum('ikjl,kl->ij', ERI, P)        # exchange part
    return h + J - 0.5 * K


def hf_energy_from_density(P, h, F):
    """Closed-shell HF energy:  E = 1/2 sum_ij P_ij (h_ij + F_ij)."""
    return 0.5 * np.sum(P * (h + F))


# ---- helium re-done through the Fock-matrix machinery ----

def helium_via_fock():
    """Verify: building F from the STO-3G density gives the same energy."""
    a = STO_3G_HE_ALPHAS
    d = STO_3G_COEFFS
    n = len(a)
    Z = 2

    S = np.array([[overlap_g(a[i], a[j])               for j in range(n)] for i in range(n)])
    T = np.array([[kinetic_g(a[i], a[j])               for j in range(n)] for i in range(n)])
    V = np.array([[nuclear_attraction_g(a[i], a[j], Z) for j in range(n)] for i in range(n)])
    h   = T + V
    ERI = build_eri_tensor(a)

    # normalize the STO-3G contraction so that c.T S c = 1
    c = d / np.sqrt(d @ S @ d)
    P = 2.0 * np.outer(c, c)        # closed-shell density: one occupied orbital

    F = build_fock(h, ERI, P)
    E   = hf_energy_from_density(P, h, F)
    eps = c @ F @ c                  # orbital energy of the (fixed) HOMO

    return E, eps

A note on the einsum calls. 'ijkl,kl->ij' contracts the last two indices of the four-index tensor against the density matrix to make the Coulomb-type matrix ; the 'ikjl,kl->ij' string permutes the index order of the tensor on the fly to give the exchange-type . Index bookkeeping is going to be most of the work in the next several pages, and einsum is what keeps it readable.


Run it

Helium via the Fock-matrix machinery (STO-3G contraction held fixed):
  total energy E   = -2.807784   (matches step 4)
  orbital energy e = -0.876036
  check:  2 e - J  = -2.807784    ✓

Two things to confirm. (1) The total energy is bit-for-bit the same as page 4's ; we've changed the bookkeeping, not the physics. (2) The double-counting relation closes. The orbital energy is the energy it costs to remove the electron — Koopmans' approximation to helium's first ionisation potential, which is experimentally about Hartree (about 24.6 eV). HF gets it within a few percent.

What we still don't have: a way to actually find the optimal . We held it fixed at the STO-3G fit and asked the Fock machinery to evaluate the energy. The next page introduces the Roothaan-Hall equations — the matrix form of the HF eigenvalue problem — and turns the search for the optimal orbital into a generalized eigenvalue solve.

Why does the Lagrangian force a one-electron equation? The original problem was N-electron.

Because the Slater determinant is a separable ansatz. The energy functional, evaluated on a single determinant, reduces to a sum over single-particle expectation values plus pairwise Coulomb-and-exchange terms. When you vary one orbital, only the terms involving it move; the others contribute a fixed averaged potential against which it has to be variationally optimal. That averaged potential is the Fock operator. The N-electron problem hasn't actually been solved — it's been recast as N coupled one-electron problems. The "coupling" is exactly the self-consistency: each orbital sees the others through , and changing any one of them changes for all of them.

Where does exchange come from physically?

From antisymmetry. The Slater determinant is a sum of two products with a relative minus sign; when you compute and expand, the cross terms produce integrals where the orbital labels are swapped between the bra and ket. Those swapped integrals are exchange. The resulting "exchange interaction" is not an interaction in the classical sense — it has no analogue in classical electromagnetism — it's a bookkeeping consequence of having forced the wavefunction to be antisymmetric. Because exchange only acts between same-spin electrons, it produces effects like Hund's rule and the stability of the parallel-spin state in O2; we'll see it concretely on the H2 page when both H atoms contribute orbitals at the same spin.

Why is the orbital energy not just the kinetic-plus-attraction part?

Because each electron lives in the average field of the other, so its single-particle energy includes that field. For helium ; the kinetic-plus-attraction is much more negative (around Hartree), but the Coulomb repulsion the electron feels from its partner pushes the orbital energy up to about . This is also why Koopmans' theorem (orbital energy ≈ ionisation potential) works reasonably well: removing the electron destroys both the kinetic-attraction term and the average repulsion it was feeling, which is exactly the orbital-energy combination — provided the remaining orbital doesn't relax much, which for closed-shell atoms it doesn't, much.