# 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](../20-guide/post-scf.md)) 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](gpu-cuda.md). 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):

```python
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 / unsure** → `4c-auto`. It picks well and falls back if RAM is tight.
- **Small–medium, plenty of RAM, want exact and fastest** → `4c-incore`.
- **Large, RAM-limited, want exact** → `4c-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 available** → `4c-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](mpi-and-interconnects.md)).

## Auxiliary bases and related knobs

```python
qc.chk.new(..., rijk="cc-pvdz-jkfit")     # the JK-fit aux for the RI J/K (default auto-derived)
qc.chk.new(..., ric="cc-pvdz-ri/mp2fit")  # the correlation-fit aux for RI-MP2 (post-scf chapter)
```

`eri="none"` skips the two-electron build entirely (for one-electron-only workflows). Out-of-core knobs
(sparse layout, drop tolerances, I/O mode) are `iop` keys under `integral.disk.*` — see the
[IOP reference](../40-reference/iop.md).

## Worked example

```python
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}
:label: ex-eri

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?
:::

:::{solution} ex-eri
:class: dropdown

1. (a) `4c-direct` — recompute the integrals each cycle (exact, no storage, but more CPU per cycle); (b)
   `ri-ram` or `ri-recomp` — density-fit (much less memory and CPU, at a ~1e-5 Ha fitting error). `4c-disk`
   is a third option if you have fast scratch disk.
2. Both are **RI** (density-fitting) — they use the *same* auxiliary-basis approximation, so they share the
   same fitting error; they differ only in whether the fitted factor is stored or recomputed, which does not
   change the result. `4c-incore` uses the **exact** integrals, so it lacks that fitting error.
3. The **RI** family is mandatory (RI-MP2 has no non-RI path). You must set the **`ric=`** correlation-fitting
   auxiliary basis (e.g. `ric="cc-pvdz-ri/mp2fit"`), distinct from the JK-fit `rijk=`.
:::

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](../40-reference/mol-spec.md) sections give the precise details of every input and option.
