Notebooks · lib/hf/md_eri
Electron repulsion integrals by McMurchie-Davidson, one shell quartet at a time
The two-electron integral over four Cartesian Gaussians is, after the
Hermite expansion of both products (see lib/hf/hermite),
(ab|cd) = 2 pi^(5/2) / (p q sqrt(p + q))
* sum_h E_ab(h) sum_h' (-1)^(t'+u'+v') E_cd(h') R(h + h')
where h = (t, u, v) runs over the Hermite functions of the bra pair,
h' over those of the ket pair, and R is taken at P - Q with exponent
alpha = pq / (p + q). The sign comes from moving the derivatives of the
ket's Hermite Gaussians over to the bra's: d/dQ = -d/dP on a function of
P - Q.
Written as two matrix products. For one shell pair, stack the E values of
every primitive pair side by side: E_ab is [components, primitive pairs x
Hermite functions], with the contraction coefficients, the component factors
and the Gaussian product prefactor folded in. For one shell quartet, the
signed R values for a batch of bra primitive pairs against every ket
primitive pair form a matrix BR, with a row per ket (primitive pair,
Hermite function) and a column per bra (primitive pair, Hermite function).
Then
Z_b = E_cd @ BR_b [ket components, bra primitive-Hermite]
(ab|cd) = E_ab @ Z^T [bra components, ket components]
where Z^T stacks the Z_b^T of every batch, and both sums over primitives
happen inside the products. The batch is as many bra primitive pairs as keep
BR near MDERI_BATCH = 4096 entries: all 36 at once for a contracted (ss|ss)
quartet of oxygen 1s shells, one at a time for a (dd|dd) quartet with 10
primitives per shell, where a single BR over both primitive lists would be
3500 x 3500. Each primitive quartet costs one Boys evaluation and one R
table, shared by every component, and the component work goes to @, which
skips the zeros of E (an entry is zero whenever t passes i + j on its
axis) and runs the same sum in the same order in both engines.
The shell-pair list is built once per basis: for every pair A >= B, the
primitive-pair exponents p, centers P and the stacked E_ab. All of it
lives in one flat array (mderi_store), because Viv has no records.
Schwarz screening. (ab|cd)^2 <= (ab|ab)(cd|cd), so with
Q_AB = sqrt(max (ab|ab)) over the components of a shell pair, a quartet with
Q_AB Q_CD below the threshold contributes less than the threshold to every
integral it holds. mderi_screened skips those quartets (threshold
MDERI_SCREEN_TOL = 10−14), and mderi_full computes every one; the two
agree to the threshold.
One flat quartet list. The build loops over a list of (bra pair, ket
pair) rows with parallel for. Each row writes a disjoint set of packed-8
slots, so the rows are independent, and the default (non-OpenMP) build runs
them in order with the same result.
One integral at a time. mderi_prim1 computes a single (ab|cd) over
four primitive components: E only for those powers, R only inside the box
they need (herm_rfill_box), and a plain Hermite sum. It is for spot checks
where a full block is wasted work, and it shares no matrix code with the
block path, so the tests hold the two against each other.
Measured cost (native, cc -O2, Apple M2 Pro). A full (dd|dd) block of
single primitives, 1296 integrals, takes 12.7 us; a (pp|pp) block, 81
integrals, 1.1 us: about 10 and 14 ns per integral. One (xx,xx|xx,xx)
value through mderi_prim1 takes 0.67 us and one (x,x|x,x) 0.34 us,
against 9.67 us and 1.11 us for the legacy lib/gto_md. The water 6-31G
build (4186 values, 41,050 primitive quartets) takes 2.7 ms against the
legacy 53.8 ms.
PySCF is not always the better reference. For the prims cases, PySCF's
value and a 50-digit mpmath evaluation differ by more than 5 × 10−13 relative
in 7 of 1536 values, by up to 3.5 × 10−11 for an (ff|ff) pair far apart, where
this module is within 5.3 × 10−14 of mpmath. The tests use the mpmath value for
those 7 (mderi_prims_fix).
Output is packed-8 (lib/hf/eri_store) over unit-normalized Cartesian AOs.
Used with import hf.md_eri.
Look inside this program
- Atlas
The program’s map: every function and which calls which.
There’s no recorded run of this program yet.
Shift+Enter in a cell runs it and moves to the next. Running a cell runs every cell above it too, as one program.