This site is version 0.0.1. Most of the code on this site is drafted by an agent and needs review. I apologize for the AI-speak, we are working on rewriting it!
viv-lang.org

Notebooks · lib/hf/md_direct

Direct J and K, recomputing the integrals instead of storing them

The Fock matrix needs the Coulomb and exchange matrices of a density D:

J[i, j] = sum_kl (ij|kl) D[k, l]
K[i, j] = sum_kl (ik|jl) D[k, l]

With the integrals stored in packed-8 form this is a loop over the vector. The vector grows as n^4 / 8: 18.4 MB at 65 AOs and 172 MB at 114. Direct J/K stores nothing. Every SCF iteration recomputes each shell quartet with mderi_block and adds it into J and K straight away, then throws it away.

One unique integral, eight contributions. mderi_block gives each integral once, for i >= j, k >= l, ij >= kl. Its 8 index orders all carry the same value, and each order adds to J and K in its own place:

order (p q | r s)   adds  g D[r, s] to J[p, q]   and  g D[q, s] to K[p, r]

When indices coincide (i = j, k = l, or ij = kl), some of the 8 orders are the same order counted twice, so g is halved once for each coincidence before the 8 contributions are added. The result is exact for any D, symmetric or not.

Parallel, with the same answer on any thread count. The quartets come from the same flat list as mderi_build, and they are dealt round-robin into MDJK_NCHUNK = 32 chunks. A parallel for runs over the chunks. Each chunk adds into its own copy of J and K, and after the loop the copies are summed in chunk order. A chunk handles the same quartets in the same order whichever thread runs it, so the default build, the OpenMP build and any OMP_NUM_THREADS give the same bits.

The signature is the frozen J/K one, (X1, X2, X3, D, opts) -> [2 ndens, n, n] with atoms, shells and prims in X1 .. X3, so this member can sit in the same registry as the in-core ones. opts[OPT_SCREEN_TOL] is the Schwarz threshold. Everything is over unit-normalized Cartesian AOs.

Density screening (mdjk_direct_screened, the production member). A quartet adds g D terms to J and K, with |g| <= Q_AB Q_CD, so a quartet whose bound times the largest density element it meets is small adds nothing that matters. With Dm[A, B] the largest |D| over the functions of shells A and B (and every density), the quartet (AB|CD) is skipped when

Q_AB Q_CD max(4 Dm[A,B], 4 Dm[C,D], Dm[A,C], Dm[A,D], Dm[B,C], Dm[B,D])

is at or below opts[OPT_DIRECT_TOL], 10−13: the rule and the threshold of PySCF's direct SCF (CVHFnrs8_prescreen with direct_scf_tol = 1e-13; the 4 is J's weight). On a full density this drops few quartets. On the change of density between two SCF iterations, which the incremental Fock build of lib/hf/scf_loop hands it, it drops most of them once the SCF is close to converged. The Schwarz factors Q are a property of the basis: a caller that builds J and K many times computes them once (mderi_schwarz) and passes them (mdjk_screened_q). The quartets are dealt into the chunks by bra pair, so no quartet list is built per call; mdjk_direct, which deals the Schwarz list round-robin and screens by nothing else, checks it.

Used with import hf.md_direct.

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.

[1]lines 61–383