import hf.opts
from hf.basis import bas_nao_cart, bas_nao_sph
from hf.md_direct import mdjk_direct
from hf.mdg_eri import mdg_jk_screened, mdg_schwarz_basis
# the J/K members that read the basis blob (ScfOptions.jk, lib/hf/jk)
INT_JK_DIRECT = 3
INT_JK_DIRECT_FULL = 4
# the blob header: counts first, then each table's row width, the shell-pair
# count, then the packs
INT_BLOB_NATM = 0
INT_BLOB_NSH = 1
INT_BLOB_NPRIM = 2
INT_BLOB_NC = 3
INT_BLOB_NS = 4
INT_BLOB_ATW = 5
INT_BLOB_SHW = 6
INT_BLOB_PRW = 7
INT_BLOB_NPAIR = 8
INT_BLOB_TOP = 9
#| The basis a direct member reads out of a blob.
record IntBasis {
atoms: Arr
shells: Arr
prims: Arr
M: Arr
Q: Arr
spherical: Num
}
#| `1` when J/K member `k` is one of the direct ones, which read a basis blob
#| in place of packed integrals.
def int_jk_is_direct(k) {
if round(k) == INT_JK_DIRECT or round(k) == INT_JK_DIRECT_FULL {
return 1
}
return 0
}
#| The basis blob that the direct members read (layout in the header). `M`
#| is the c2s matrix for a spherical basis; pass `spherical = 0` for
#| Cartesian, and then `M` is not read. The Schwarz factors of the shell
#| pairs are computed here, once.
def int_basis_blob(atoms: Arr, shells: Arr, prims: Arr, M: Arr, spherical) {
natm = len(atoms)
nsh = len(shells)
npr = len(prims)
# the tables are copied row by row, whatever their columns are
atw = shape(atoms)[1]
shw = shape(shells)[1]
prw = shape(prims)[1]
nc = bas_nao_cart(shells)
ns = 0
sph = 0
if spherical > 0.5 {
ns = bas_nao_sph(shells)
sph = 1
}
Q = mdg_schwarz_basis(atoms, shells, prims, sph, M)
npair = len(Q)
b = zeros(INT_BLOB_TOP + atw * natm + shw * nsh + prw * npr + nc * ns + npair)
b[INT_BLOB_NATM] = natm
b[INT_BLOB_NSH] = nsh
b[INT_BLOB_NPRIM] = npr
b[INT_BLOB_NC] = nc
b[INT_BLOB_NS] = ns
b[INT_BLOB_ATW] = atw
b[INT_BLOB_SHW] = shw
b[INT_BLOB_PRW] = prw
b[INT_BLOB_NPAIR] = npair
k = INT_BLOB_TOP
for a to natm {
for c to atw {
b[k] = atoms[a, c]
k = k + 1
}
}
for s to nsh {
for c to shw {
b[k] = shells[s, c]
k = k + 1
}
}
for p to npr {
for c to prw {
b[k] = prims[p, c]
k = k + 1
}
}
for i to nc {
for j to ns {
b[k] = M[i, j]
k = k + 1
}
}
b[k:k + npair] = Q
return b
}
#| One `[rows, cols]` pack read back out of a blob, starting at `k0`.
def int_blob_pack(b: Arr, k0, rows, cols) {
P = zeros(rows, cols)
k = k0
for r to rows {
for c to cols {
P[r, c] = b[k]
k = k + 1
}
}
return P
}
#| The basis a blob holds, as a record (see the header for the layout).
def int_blob_basis(b: Arr) -> IntBasis {
natm = b[INT_BLOB_NATM]
nsh = b[INT_BLOB_NSH]
npr = b[INT_BLOB_NPRIM]
nc = b[INT_BLOB_NC]
ns = b[INT_BLOB_NS]
npair = b[INT_BLOB_NPAIR]
k = INT_BLOB_TOP
atoms = int_blob_pack(b, k, natm, b[INT_BLOB_ATW])
k = k + b[INT_BLOB_ATW] * natm
shells = int_blob_pack(b, k, nsh, b[INT_BLOB_SHW])
k = k + b[INT_BLOB_SHW] * nsh
prims = int_blob_pack(b, k, npr, b[INT_BLOB_PRW])
k = k + b[INT_BLOB_PRW] * npr
sph = 0
M = zeros(1, 1)
if ns > 0 {
sph = 1
M = int_blob_pack(b, k, nc, ns)
}
k = k + nc * ns
return IntBasis { atoms: atoms, shells: shells, prims: prims, M: M, Q: b[k:k + npair], spherical: sph }
}
#| The densities `D[nd, n, n]` in the Cartesian functions the integrals are
#| computed in: `M D M^T` for a spherical basis, `D` as it is otherwise.
def int_dens_cart(B: IntBasis, D: Arr) -> Arr {
if B.spherical < 1 {
return D
}
nd = len(D)
nc = len(B.M)
Dc = zeros(nd, nc, nc)
for d to nd {
setslab(Dc, d, B.M @ slab(D, d) @ transpose(B.M))
}
return Dc
}
#| Cartesian J and K `R[2 nd, nc, nc]` in the run's functions: `M^T R M` for
#| a spherical basis, `R` as it is otherwise.
def int_jk_run(B: IntBasis, R: Arr) -> Arr {
if B.spherical < 1 {
return R
}
ns = shape(B.M)[1]
out = zeros(len(R), ns, ns)
for r to len(R) {
setslab(out, r, transpose(B.M) @ (slab(R, r) @ B.M))
}
return out
}
#| J/K member `INT_JK_DIRECT`, the production one: direct J and K from the
#| basis blob in `X2`, screened by the Schwarz bound and the density, on the
#| generated kernels in the run's functions (lib/hf/mdg_eri). `X1` and
#| `X3` are not read.
def int_jk_direct(X1: Arr, X2: Arr, X3: Arr, D: Arr, opts: Arr) -> Arr {
B = int_blob_basis(X2)
return mdg_jk_screened(B.atoms, B.shells, B.prims, D, opts, B.Q, B.spherical, B.M)
}
#| J/K member `INT_JK_DIRECT_FULL`: direct J and K from the basis blob in
#| `X2`, every quartet the Schwarz test keeps. `X1` and `X3` are not read.
def int_jk_direct_full(X1: Arr, X2: Arr, X3: Arr, D: Arr, opts: Arr) -> Arr {
B = int_blob_basis(X2)
return int_jk_run(B, mdjk_direct(B.atoms, B.shells, B.prims, int_dens_cart(B, D), opts))
}
# SPDX-License-Identifier: MIT OR Apache-2.0
# SPDX-FileCopyrightText: 2026 The Viv authors