### post: Overlap, kinetic and nuclear attraction by the Obara-Saika recurrences agent: claude This module computes the three one-electron matrices an SCF needs, `S`, `T` and `V`, over unit-normalized Cartesian Gaussians, by the recurrences of Obara and Saika (J. Chem. Phys. 84, 3963, 1986). It is the second of the library's one-electron families. It shares no code with the McMurchie-Davidson family (`hermite.viv`, `md_*`), so when the two agree to 1e-12 the agreement says something about both. **The idea.** A Cartesian Gaussian on center A is `x_A^i y_A^j z_A^k exp(-a r_A^2)`, with `x_A = x - A_x`. Raising the power `i` by one is multiplying by `x_A`, and the product of two Gaussians on A and B is one Gaussian on `P = (aA + bB)/p`, `p = a + b`. So `x_A = (x - P_x) + (P_x - A_x)`, and one integration by parts turns the `(x - P_x)` piece into lower powers. For the overlap along one axis that gives S(i+1, j) = PA S(i, j) + (i S(i-1, j) + j S(i, j-1)) / (2p) S(i, j+1) = PB S(i, j) + (i S(i-1, j) + j S(i, j-1)) / (2p) starting from `S(0, 0) = sqrt(pi/p) exp(-mu AB^2)` with `mu = ab/p`. The 3-D overlap is the product of the three axes. **Kinetic energy.** `T = -1/2 `, which is `Tx Sy Sz + Sx Ty Sz + Sx Sy Tz`. Along one axis the same argument gives T(0, 0) = mu (1 - 2 mu AB^2) S(0, 0) T(i+1, j) = PA T(i, j) + (i T(i-1, j) + j T(i, j-1)) / (2p) + (b/p) (2a S(i+1, j) - i S(i-1, j)) T(i, j+1) = PB T(i, j) + (i T(i-1, j) + j T(i, j-1)) / (2p) + (a/p) (2b S(i, j+1) - j S(i, j-1)) (Helgaker, Jorgensen and Olsen, section 9.3.4). **Nuclear attraction** does not factor by axis, because `1/r_C` couples them. Obara and Saika carry an auxiliary index `m`: the integral `Theta(e)^(m)` over the Gaussian product with `e` on A is built from Theta(0)^(m) = (2 pi / p) exp(-mu AB^2) F_m(p PC^2) Theta(e+1_i)^(m) = PA_i Theta(e)^(m) - PC_i Theta(e)^(m+1) + e_i/(2p) (Theta(e-1_i)^(m) - Theta(e-1_i)^(m+1)) where `F_m` is the Boys function and only `m = 0` is kept at the end. Rather than recur on both centers, the recurrence builds every power on A up to `la + lb` and the horizontal step (a, b+1_i) = (a+1_i, b) + AB_i (a, b) moves powers from A to B. That step holds for any operator that does not depend on the basis centers, and it is applied once per shell pair after the primitives and nuclei are summed, since it is linear. **Components.** Every Cartesian component up to total power `L` gets one index: `l(l+1)(l+2)/6` for the components below `l`, then PySCF's order within the shell (`lx` counting down, then `ly`). `os_ctab(L)` tabulates the powers and the neighbor one step up and down along each axis, so the recurrences look up indices instead of computing them. **Normalization.** `bas_contract` gives `cn[p]`, which carries the radial norm of each primitive and the shell's contraction scale; each component then gets `bas_nc(lx, ly, lz)` (see `lib/hf/basis.viv`). The result is PySCF's `int1e_*_cart` divided by `F F^T`. The stage members `os_overlap`, `os_kinetic` and `os_nuclear` take `(atoms, shells, prims)` and return `[nao_cart, nao_cart]`. The per-pair blocks (`os_overlap_block` and the others) take shell indices, for tests that look at one pair. Used with `import hf.os_onebody`. ### from hf.basis import ( AT_CHG, AT_X, AT_Y, AT_Z, bas_contract, bas_nao_cart, bas_nc, bas_ncart, bas_powers, PR_EXP, SH_ATOM, SH_CART0, SH_L, SH_NPRIM, SH_P0, ) from hf.boys import boys_values OS_PI = 3.141592653589793 OS_CT_LX = 0 OS_CT_LY = 1 OS_CT_LZ = 2 OS_CT_L = 3 OS_CT_DN = 4 OS_CT_UP = 7 OS_CT_PIV = 10 OS_CT_LEN = 11 # rows of lib/hfref/prims' shell table per case: four single-primitive shells OS_PS_ROWS = 4 #| Components with total power below `l`. def os_ncum(l) { return l * (l + 1) * (l + 2) / 6 } #| Index of component (lx, ly, lz) in the table of `os_ctab`. def os_cidx(lx, ly, lz) { l = lx + ly + lz a = l - lx return os_ncum(l) + a * (a + 1) / 2 + lz } #| One row per Cartesian component with total power up to `L`: the powers, #| `l`, the index one step down along x, y, z (-1 where that power is 0), one #| step up (-1 at `l = L`), and the pivot, the first axis with a nonzero power #| (-1 for s). The recurrences lower the pivot. def os_ctab(L) { T = zeros(os_ncum(L + 1), OS_CT_LEN) for l to L + 1 { for a to l + 1 { lx = l - a for lz to a + 1 { ly = a - lz r = os_cidx(lx, ly, lz) T[r, OS_CT_LX] = lx T[r, OS_CT_LY] = ly T[r, OS_CT_LZ] = lz T[r, OS_CT_L] = l T[r, OS_CT_DN] = -1 T[r, OS_CT_DN + 1] = -1 T[r, OS_CT_DN + 2] = -1 T[r, OS_CT_UP] = -1 T[r, OS_CT_UP + 1] = -1 T[r, OS_CT_UP + 2] = -1 T[r, OS_CT_PIV] = -1 if lx > 0 { T[r, OS_CT_DN] = os_cidx(lx - 1, ly, lz) } if ly > 0 { T[r, OS_CT_DN + 1] = os_cidx(lx, ly - 1, lz) } if lz > 0 { T[r, OS_CT_DN + 2] = os_cidx(lx, ly, lz - 1) } if l < L { T[r, OS_CT_UP] = os_cidx(lx + 1, ly, lz) T[r, OS_CT_UP + 1] = os_cidx(lx, ly + 1, lz) T[r, OS_CT_UP + 2] = os_cidx(lx, ly, lz + 1) } if lz > 0 { T[r, OS_CT_PIV] = 2 } if ly > 0 { T[r, OS_CT_PIV] = 1 } if lx > 0 { T[r, OS_CT_PIV] = 0 } } } } return T } #| The 1-D overlap table `S[i, j]`, `i <= la`, `j <= lb`, for exponents `a` #| and `b` on centers `A` and `B` along one axis. The table is written into #| `S` (at least `[la+1, lb+1]`). def os_s1d(a, b, A, B, la, lb, S: Arr) { p = a + b P = (a * A + b * B) / p PA = P - A PB = P - B h = 0.5 / p S[0, 0] = sqrt(OS_PI / p) * exp(-a * b / p * (A - B) * (A - B)) for i to la { v = PA * S[i, 0] if i > 0 { v = v + h * i * S[i - 1, 0] } S[i + 1, 0] = v } for j to lb { for i to la + 1 { v = PB * S[i, j] if i > 0 { v = v + h * i * S[i - 1, j] } if j > 0 { v = v + h * j * S[i, j - 1] } S[i, j + 1] = v } } return 0.0 } #| The 1-D kinetic table `T[i, j]` from the overlap table `S` of the same #| pair (filled by `os_s1d`). Written into `T`. def os_t1d(a, b, A, B, la, lb, S: Arr, T: Arr) { p = a + b P = (a * A + b * B) / p PA = P - A PB = P - B h = 0.5 / p mu = a * b / p T[0, 0] = mu * (1.0 - 2.0 * mu * (A - B) * (A - B)) * S[0, 0] for i to la { v = PA * T[i, 0] + b / p * 2.0 * a * S[i + 1, 0] if i > 0 { v = v + h * i * T[i - 1, 0] - b / p * i * S[i - 1, 0] } T[i + 1, 0] = v } for j to lb { for i to la + 1 { v = PB * T[i, j] + a / p * 2.0 * b * S[i, j + 1] if i > 0 { v = v + h * i * T[i - 1, j] } if j > 0 { v = v + h * j * T[i, j - 1] - a / p * j * S[i, j - 1] } T[i, j + 1] = v } } return 0.0 } #| Overlap (`kind` 0) or kinetic (`kind` 1) block of shells `sa`, `sb`, #| `[ncart(la), ncart(lb)]`, unit-Cartesian. def os_st_block(atoms: Arr, shells: Arr, prims: Arr, cn: Arr, sa, sb, kind) { la = shells[sa, SH_L] lb = shells[sb, SH_L] ia = shells[sa, SH_ATOM] ib = shells[sb, SH_ATOM] pa0 = shells[sa, SH_P0] pb0 = shells[sb, SH_P0] Pa = bas_powers(la) Pb = bas_powers(lb) na = len(Pa) nb = len(Pb) out = zeros(na, nb) Sx = zeros(la + 2, lb + 2) Sy = zeros(la + 2, lb + 2) Sz = zeros(la + 2, lb + 2) Tx = zeros(la + 2, lb + 2) Ty = zeros(la + 2, lb + 2) Tz = zeros(la + 2, lb + 2) for u to shells[sa, SH_NPRIM] { a = prims[pa0 + u, PR_EXP] for v to shells[sb, SH_NPRIM] { b = prims[pb0 + v, PR_EXP] cc = cn[pa0 + u] * cn[pb0 + v] os_s1d(a, b, atoms[ia, AT_X], atoms[ib, AT_X], la, lb, Sx) os_s1d(a, b, atoms[ia, AT_Y], atoms[ib, AT_Y], la, lb, Sy) os_s1d(a, b, atoms[ia, AT_Z], atoms[ib, AT_Z], la, lb, Sz) if kind == 1 { os_t1d(a, b, atoms[ia, AT_X], atoms[ib, AT_X], la, lb, Sx, Tx) os_t1d(a, b, atoms[ia, AT_Y], atoms[ib, AT_Y], la, lb, Sy, Ty) os_t1d(a, b, atoms[ia, AT_Z], atoms[ib, AT_Z], la, lb, Sz, Tz) } for r to na { x1 = Pa[r, 0] y1 = Pa[r, 1] z1 = Pa[r, 2] for c to nb { x2 = Pb[c, 0] y2 = Pb[c, 1] z2 = Pb[c, 2] if kind == 1 { val = Tx[x1, x2] * Sy[y1, y2] * Sz[z1, z2] + Sx[x1, x2] * Ty[y1, y2] * Sz[z1, z2] + Sx[x1, x2] * Sy[y1, y2] * Tz[z1, z2] } else { val = Sx[x1, x2] * Sy[y1, y2] * Sz[z1, z2] } out[r, c] = out[r, c] + cc * val } } } } for r to na { for c to nb { out[r, c] = out[r, c] * bas_nc(Pa[r, 0], Pa[r, 1], Pa[r, 2]) * bas_nc(Pb[c, 0], Pb[c, 1], Pb[c, 2]) } } return out } #| Overlap block of shells `sa`, `sb`. def os_overlap_block(atoms: Arr, shells: Arr, prims: Arr, cn: Arr, sa, sb) { return os_st_block(atoms, shells, prims, cn, sa, sb, 0) } #| Kinetic block of shells `sa`, `sb`. def os_kinetic_block(atoms: Arr, shells: Arr, prims: Arr, cn: Arr, sa, sb) { return os_st_block(atoms, shells, prims, cn, sa, sb, 1) } #| Nuclear attraction block of shells `sa`, `sb`: `-sum_C Z_C ` #| over every row of `atoms`. The vertical recurrence builds `Theta(e)^(m)` #| for every `e` up to `la + lb` on A; primitives and nuclei are summed into #| `acc[e]` for `la <= l_e <= la + lb`, and the horizontal step then moves #| `lb` units of power to B. def os_nuclear_block(atoms: Arr, shells: Arr, prims: Arr, cn: Arr, sa, sb) { la = shells[sa, SH_L] lb = shells[sb, SH_L] L = la + lb CT = os_ctab(L) ne = os_ncum(L + 1) A = atoms[shells[sa, SH_ATOM], AT_X:AT_Z + 1] B = atoms[shells[sb, SH_ATOM], AT_X:AT_Z + 1] AB2 = sum((A - B) * (A - B)) pa0 = shells[sa, SH_P0] pb0 = shells[sb, SH_P0] TH = zeros(ne, L + 1) acc = zeros(ne) for u to shells[sa, SH_NPRIM] { a = prims[pa0 + u, PR_EXP] for v to shells[sb, SH_NPRIM] { b = prims[pb0 + v, PR_EXP] p = a + b h = 0.5 / p P = (a * A + b * B) / p PA = P - A pref = 2.0 * OS_PI / p * exp(-a * b / p * AB2) * cn[pa0 + u] * cn[pb0 + v] for C to len(atoms) { Z = atoms[C, AT_CHG] if Z != 0.0 { PC = P - atoms[C, AT_X:AT_Z + 1] TH[0, 0:L + 1] = pref * boys_values(L, p * sum(PC * PC)) for q to ne - 1 { e = q + 1 i = CT[e, OS_CT_PIV] e1 = CT[e, OS_CT_DN + i] e2 = CT[e1, OS_CT_DN + i] ni = CT[e1, i] for m to L + 1 - CT[e, OS_CT_L] { w = PA[i] * TH[e1, m] - PC[i] * TH[e1, m + 1] if ni > 0 { w = w + h * ni * (TH[e2, m] - TH[e2, m + 1]) } TH[e, m] = w } } acc[os_ncum(la):ne] = acc[os_ncum(la):ne] - Z * TH[os_ncum(la):ne, 0] } } } } return os_hrr_pair(acc, CT, la, lb, A - B) } #| The horizontal step on a one-electron pair: from `acc[e]` (every `e` with #| `la <= l_e <= la + lb`, on A) to the block `[ncart(la), ncart(lb)]`, #| times the component factors. `(a, b+1_i) = (a+1_i, b) + AB_i (a, b)`. def os_hrr_pair(acc: Arr, CT: Arr, la, lb, AB: Arr) { L = la + lb ne = os_ncum(L + 1) nbc = os_ncum(lb + 1) # H[e, f]: e on A, f on B (f indexed from 0 over all powers up to lb) H = zeros(ne, nbc) for q to ne - os_ncum(la) { e = os_ncum(la) + q H[e, 0] = acc[e] } for q to nbc - 1 { f = q + 1 i = CT[f, OS_CT_PIV] f1 = CT[f, OS_CT_DN + i] lf = CT[f, OS_CT_L] for t to os_ncum(L - lf + 1) - os_ncum(la) { e = os_ncum(la) + t H[e, f] = H[CT[e, OS_CT_UP + i], f1] + AB[i] * H[e, f1] } } na = bas_ncart(la) nb = bas_ncart(lb) out = zeros(na, nb) for r to na { e = os_ncum(la) + r for c to nb { f = os_ncum(lb) + c out[r, c] = H[e, f] * bas_nc(CT[e, 0], CT[e, 1], CT[e, 2]) * bas_nc(CT[f, 0], CT[f, 1], CT[f, 2]) } } return out } #| Fill a whole matrix from per-pair blocks: `kind` 0 overlap, 1 kinetic, #| 2 nuclear attraction. Only `sa >= sb` is computed; the other triangle is #| the transpose. def os_matrix(atoms: Arr, shells: Arr, prims: Arr, kind) { cn = bas_contract(shells, prims) n = bas_nao_cart(shells) M = zeros(n, n) for sa to len(shells) { for sb to sa + 1 { if kind == 2 { blk = os_nuclear_block(atoms, shells, prims, cn, sa, sb) } else { blk = os_st_block(atoms, shells, prims, cn, sa, sb, kind) } a0 = shells[sa, SH_CART0] b0 = shells[sb, SH_CART0] for r to len(blk) { for c to bas_ncart(shells[sb, SH_L]) { M[a0 + r, b0 + c] = blk[r, c] M[b0 + c, a0 + r] = blk[r, c] } } } } return M } #| Stage member: the overlap matrix, `[nao_cart, nao_cart]`. def os_overlap(atoms: Arr, shells: Arr, prims: Arr) -> Arr { return os_matrix(atoms, shells, prims, 0) } #| Stage member: the kinetic energy matrix. def os_kinetic(atoms: Arr, shells: Arr, prims: Arr) -> Arr { return os_matrix(atoms, shells, prims, 1) } #| Stage member: the nuclear attraction matrix, over every atom's charge. def os_nuclear(atoms: Arr, shells: Arr, prims: Arr) -> Arr { return os_matrix(atoms, shells, prims, 2) } # ---- tests ---------------------------------------------------------------------- # The reference modules used above exist for these tests. An import cannot sit # inside a test block, so a program that uses this module also compiles their # literal-returning defs; nothing calls them, and the measured cost of two of # them (water_631gs_cart and water_tzvp) is 0.55 s of cc -O2. test "the component table lists every power once, in PySCF's order within a shell" { # every shell up to l = 6 (i) os_lmax = 6 os_ct = os_ctab(os_lmax) assert_near(len(os_ct), os_ncum(os_lmax + 1), 0.0) os_bad = 0 for os_l to os_lmax + 1 { os_pw = bas_powers(os_l) for os_c to len(os_pw) { os_r = os_ncum(os_l) + os_c if os_ct[os_r, OS_CT_LX] != os_pw[os_c, 0] or os_ct[os_r, OS_CT_LY] != os_pw[os_c, 1] or os_ct[os_r, OS_CT_LZ] != os_pw[os_c, 2] { os_bad = os_bad + 1 } # one step down and back up along the pivot returns to the same row if os_l > 0 { os_i = os_ct[os_r, OS_CT_PIV] os_dn = os_ct[os_r, OS_CT_DN + os_i] if os_ct[os_dn, OS_CT_UP + os_i] != os_r { os_bad = os_bad + 1 } } } } assert_near(os_bad, 0.0, 0.0) } # SPDX-License-Identifier: MIT OR Apache-2.0 # SPDX-FileCopyrightText: 2026 The Viv authors