Two-electron integrals & J/K strategies#

The single biggest performance lever in a quantum-chemistry calculation is how the two-electron integrals are handled. There are \(\mathcal{O}(N^4)\) of them, and every SCF cycle contracts them into the Coulomb (J) and exchange (K) matrices. qc-rs exposes the choice through one keyword, ints(eri=...), and the choice interacts with every parallelism level from the last three chapters. This chapter maps the options so you can pick the right one.

Theory: why integral handling dominates#

For \(N\) basis functions there are formally \(N^4/8\) unique two-electron integrals \((\mu\nu|\lambda\sigma)\). At \(N = 500\) that is ~8 billion numbers — often too many to store, and expensive to recompute. Every method that follows (SCF, MP2, …) is bottlenecked here, so the strategy question is really: store them, recompute them, put them on disk, or approximate them? Each answer trades memory against CPU time differently, which is why there is no single best choice — it depends on your molecule and your hardware.

The unified eri= axis#

All of it is one keyword on ints(...), in two families.

The 4-center family — exact integrals#

These use the true \((\mu\nu|\lambda\sigma)\) integrals (no approximation); they give bit-identical energies and differ only in where the integrals live:

eri=

strategy

best for

4c-auto (default)

walks a memory ladder: incore → ramdisk → disk → direct

just works

4c-incore

store all integrals in RAM

small/medium, plenty of RAM (fastest)

4c-direct

recompute integrals every cycle, store nothing

large, RAM-limited

4c-disk

spool integrals to disk once, stream each cycle

out-of-core, a fast scratch disk

4c-ramdisk

the sparse disk encoding kept in RAM

large/sparse, distributes across MPI ranks

4c-auto is the safe default — it picks the most memory-hungry option that fits and falls back gracefully. It never selects RI (below); RI is only ever chosen explicitly.

The RI (density-fitting) family — approximate, much cheaper#

RI (resolution of identity, from the post-SCF chapter) factors the four-index integrals through an auxiliary basis, collapsing the cost dramatically at the price of a tiny, controlled fitting error. Two sub-families differ in whether the fitted factor is stored or recomputed:

eri=

strategy

note

ri-ram

build the whitened RI factor once, keep it in RAM (μ-distributed under MPI)

incore-speed, recompute-memory — the store-B default at scale

ri-recomp

recompute the 3-center integrals every cycle, store no factor

the memory-frugal RI

ri-recomp-disk

recompute + spool the smaller occupied factor to disk

out-of-core RI

The GPU members (ri-cuda, ri-recomp-cuda, ri-ram-cuda) live in the GPU chapter. RI needs an auxiliary basis; with no rijk= given, qc-rs auto-derives a default JK-fit aux from your orbital basis.

Exact vs RI: the fitting error, verified#

The 4-center strategies are exact and agree to the last digit; the RI strategies carry a small fitting error but are far cheaper. For water/cc-pVDZ (RHF):

import qc
water = "O 0 0 0.117; H 0 0.757 -0.469; H 0 -0.757 -0.469"

for eri in ("4c-incore", "4c-direct", "ri-ram", "ri-recomp"):
    e = qc.chk.new(atom=water, ao="cc-pvdz", unit="angstrom",
                   rijk="cc-pvdz-jkfit").ints(eri=eri).scf(ref="r").run().scf.energy
    print(f"{eri:11} {e:.6f}")
# 4c-incore   -76.026794   ← exact
# 4c-direct   -76.026794   ← exact (bit-identical to incore)
# ri-ram      -76.026773   ← RI: +2.1e-5 fitting error
# ri-recomp   -76.026773   ← RI: identical to ri-ram

The two 4-center strategies are bit-identical (−76.026794); the two RI strategies are identical to each other (−76.026773) and differ from exact by only 2.1 × 10⁻⁵ Ha — the RI fitting error, far below chemical accuracy. So RI buys a large speed/memory win for a negligible, controlled error.

Choosing a strategy#

A practical decision guide:

  • Default / unsure4c-auto. It picks well and falls back if RAM is tight.

  • Small–medium, plenty of RAM, want exact and fastest4c-incore.

  • Large, RAM-limited, want exact4c-direct (recompute) or 4c-disk (out-of-core).

  • Large, want the big speed/memory win and can accept ~1e-5 Ha → the RI family (ri-ram if the factor fits in RAM, ri-recomp if not). This is the usual choice at scale, and the only path for RI-MP2.

  • GPU available4c-cuda / ri-cuda.

RI also composes with MPI to break the one-node memory wall (ri-ram distributes the factor by AO index, so per-rank memory ≈ total / nranks, MPI chapter).

Worked example#

import qc
water = "O 0 0 0.117; H 0 0.757 -0.469; H 0 -0.757 -0.469"

exact = qc.chk.new(atom=water, ao="cc-pvdz", unit="angstrom").ints(eri="4c-incore").scf(ref="r").run()
ri    = qc.chk.new(atom=water, ao="cc-pvdz", unit="angstrom",
                   rijk="cc-pvdz-jkfit").ints(eri="ri-ram").scf(ref="r").run()

print(f"exact (4c) : {exact.scf.energy:.6f}")   # -76.026794
print(f"RI         : {ri.scf.energy:.6f}")       # -76.026773
print(f"fitting err: {abs(exact.scf.energy - ri.scf.energy):.2e} Ha")   # 2.1e-05

Exercise 20

  1. Your cc-pVTZ calculation on a big molecule dies with an out-of-memory error using 4c-incore. Give two different eri= strategies that would let it run, and the trade-off of each.

  2. Why do ri-ram and ri-recomp give the same energy as each other but a slightly different energy from 4c-incore?

  3. You want to run RI-MP2. Which eri= family is mandatory, and which extra basis keyword must you set?

That completes Part IV. You can now scale a qc-rs calculation across CPU threads, MPI ranks on a cluster, and an NVIDIA GPU, and choose the integral strategy that fits your molecule and hardware. From here, the Reference sections give the precise details of every input and option.