Skip to content

Commit 3194cad

Browse files
committed
Runtime σ.p integral evaluation via SigmaPEngine (include/libint2/sigma_p.h)
2 parents d616cd1 + f606f07 commit 3194cad

2 files changed

Lines changed: 220 additions & 98 deletions

File tree

export/tests/unit/test-2body.cc

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -742,6 +742,109 @@ TEST_CASE("RKB Coulomb integrals", "[engine][2-body]") {
742742
}
743743
}
744744

745+
SECTION("Gaunt-type (SL|SL)") {
746+
// Need standard ERI support at max_l+1 for AM-shifted integrals
747+
if (LIBINT2_MAX_AM_eri < max_l + 1) return;
748+
749+
libint2::SigmaPEngine sp_engine_gaunt(max_nprim, max_l, BraKet::xx_xx);
750+
751+
const auto nshell = obs.size();
752+
for (int s0 = 0; s0 != nshell; ++s0) {
753+
for (int s1 = 0; s1 != nshell; ++s1) {
754+
for (int s2 = 0; s2 != nshell; ++s2) {
755+
for (int s3 = 0; s3 != nshell; ++s3) {
756+
// ([σ.p a] b | [σ.p c] d) — 4 quaternion components
757+
const auto &results = sp_engine_gaunt.compute(
758+
sigma_p(obs[s0]), obs[s1], sigma_p(obs[s2]), obs[s3]);
759+
assert(results.size() == 4);
760+
761+
LIBINT2_REF_REALTYPE Aref[3], Bref[3], Cref[3], Dref[3];
762+
for (int i = 0; i < 3; ++i) Aref[i] = obs[s0].O[i];
763+
for (int i = 0; i < 3; ++i) Bref[i] = obs[s1].O[i];
764+
for (int i = 0; i < 3; ++i) Cref[i] = obs[s2].O[i];
765+
for (int i = 0; i < 3; ++i) Dref[i] = obs[s3].O[i];
766+
767+
int ijkl = 0;
768+
769+
int l0, m0, n0;
770+
FOR_CART(l0, m0, n0, obs[s0].contr[0].l)
771+
int l1, m1, n1;
772+
FOR_CART(l1, m1, n1, obs[s1].contr[0].l)
773+
int l2, m2, n2;
774+
FOR_CART(l2, m2, n2, obs[s2].contr[0].l)
775+
int l3, m3, n3;
776+
FOR_CART(l3, m3, n3, obs[s3].contr[0].l)
777+
778+
std::array<LIBINT2_REF_REALTYPE, 4> ref{};
779+
ref.fill(0.0);
780+
781+
for (uint p0 = 0; p0 < obs[s0].nprim(); p0++) {
782+
for (uint p1 = 0; p1 < obs[s1].nprim(); p1++) {
783+
for (uint p2 = 0; p2 < obs[s2].nprim(); p2++) {
784+
for (uint p3 = 0; p3 < obs[s3].nprim(); p3++) {
785+
const LIBINT2_REF_REALTYPE c0123 =
786+
obs[s0].contr[0].coeff[p0] *
787+
obs[s1].contr[0].coeff[p1] *
788+
obs[s2].contr[0].coeff[p2] * obs[s3].contr[0].coeff[p3];
789+
790+
constexpr int X = 0, Y = 1, Z = 2;
791+
// D(r1, r2) = ∂²ERI/∂A_r1 ∂C_r2
792+
auto D = [&](int r1, int r2) {
793+
typedef std::array<unsigned int, 12> didx;
794+
didx d = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
795+
d[0 + r1] = 1; // ∂/∂A_r1 (bra center 1)
796+
d[6 + r2] = 1; // ∂/∂C_r2 (ket center 1)
797+
return eri(d.data(), l0, m0, n0, obs[s0].alpha[p0], Aref,
798+
l1, m1, n1, obs[s1].alpha[p1], Bref, l2, m2,
799+
n2, obs[s2].alpha[p2], Cref, l3, m3, n3,
800+
obs[s3].alpha[p3], Dref, 0);
801+
};
802+
803+
// Quaternion components from (σ.p_a)(σ.p_c) decomposition
804+
// S: dot product
805+
ref[0] += c0123 * (D(X, X) + D(Y, Y) + D(Z, Z));
806+
// X: cross_x
807+
ref[1] += c0123 * (D(Y, Z) - D(Z, Y));
808+
// Y: cross_y
809+
ref[2] += c0123 * (D(Z, X) - D(X, Z));
810+
// Z: cross_z
811+
ref[3] += c0123 * (D(X, Y) - D(Y, X));
812+
}
813+
}
814+
}
815+
}
816+
817+
const double ABSOLUTE_DEVIATION_THRESHOLD = 5.0E-14;
818+
const double RELATIVE_DEVIATION_THRESHOLD = 1.0E-9;
819+
820+
for (auto comp = 0; comp < 4; ++comp) {
821+
auto abs_err = abs(ref[comp] - results[comp][ijkl]);
822+
auto rel_err = abs(abs_err / (ref[comp] + 1e-50));
823+
824+
bool not_ok = rel_err > RELATIVE_DEVIATION_THRESHOLD &&
825+
abs_err > ABSOLUTE_DEVIATION_THRESHOLD;
826+
827+
if (not_ok) {
828+
std::cout << "(SL|SL) (" << s0 << " " << s1 << " | " << s2
829+
<< " " << s3 << ") elem=" << ijkl << " comp=" << comp
830+
<< " ref=" << ref[comp]
831+
<< " sp=" << results[comp][ijkl]
832+
<< " abs_err=" << abs_err << std::endl;
833+
}
834+
REQUIRE(!not_ok);
835+
}
836+
837+
++ijkl;
838+
END_FOR_CART
839+
END_FOR_CART
840+
END_FOR_CART
841+
END_FOR_CART
842+
}
843+
}
844+
}
845+
}
846+
}
847+
745848
SECTION("3-center RKB (K|LS)") {
746849
std::vector<Shell> dfbs{
747850
Shell{{3.0}, {{0, false, {1.0}}}, {{0.0, 0.0, 0.0}}},

include/libint2/sigma_p.h

Lines changed: 117 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,12 @@ inline SigmaPShell sigma_p(const Shell& s) { return {s}; }
5151
/// BraKet::xs_xx — 3-center (K | a b)
5252
/// BraKet::xx_xs — 3-center (a b | K)
5353
///
54-
/// Component ordering: index = bra_q * n_ket_comp + ket_q
55-
/// For 2 (σ.p) on a particle: quaternion {S=0, X=1, Y=2, Z=3}
56-
/// For 1 (σ.p): Cartesian {x=0, y=1, z=2}
54+
/// Component structure based on total (σ.p) count:
55+
/// 0 (σ.p) → 1 component (standard integral)
56+
/// 1 (σ.p) → 3 Cartesian components {x=0, y=1, z=2} (3-center DF only)
57+
/// 2 (σ.p) → 4 quaternion components {S=0, X=1, Y=2, Z=3}
58+
/// (same or different particles, e.g. (LL|SS) or (SL|SL))
59+
/// 4 (σ.p) → 16 components, index = 4*bra_q + ket_q (SS|SS)
5760
class SigmaPEngine {
5861
public:
5962
using real_t = double;
@@ -164,17 +167,20 @@ class SigmaPEngine {
164167
return cart_idx(am + delta, nx, ny);
165168
}
166169

167-
/// Number of output components for a particle with n (σ.p) centers
168-
static int particle_ncomp(int n_sp) {
169-
switch (n_sp) {
170+
/// Number of output components based on TOTAL (σ.p) count.
171+
/// 0 → 1, 1 → 3 Cartesian, 2 → 4 quaternion, 4 → 16 (two pairs)
172+
static int total_ncomp(int total_sp) {
173+
switch (total_sp) {
170174
case 0:
171175
return 1;
172176
case 1:
173177
return 3;
174178
case 2:
175179
return 4;
180+
case 4:
181+
return 16;
176182
default:
177-
assert(false);
183+
assert(false && "unsupported (σ.p) count");
178184
return 0;
179185
}
180186
}
@@ -210,18 +216,16 @@ class SigmaPEngine {
210216
const int nc = ncart(lc), nd = ncart(ld);
211217
const int n_out = na * nb * nc * nd;
212218

213-
const int bra_sp = (sp_a ? 1 : 0) + (sp_b ? 1 : 0);
214-
const int ket_sp = (sp_c ? 1 : 0) + (sp_d ? 1 : 0);
215-
const int n_bra = particle_ncomp(bra_sp);
216-
const int n_ket = particle_ncomp(ket_sp);
217-
const int n_comp = n_bra * n_ket;
219+
const int total_sp =
220+
(sp_a ? 1 : 0) + (sp_b ? 1 : 0) + (sp_c ? 1 : 0) + (sp_d ? 1 : 0);
221+
const int n_comp = total_ncomp(total_sp);
218222

219223
buf_.assign(n_comp * n_out, 0.0);
220224
targets_.resize(n_comp);
221225
for (int q = 0; q < n_comp; ++q) targets_[q] = buf_.data() + q * n_out;
222226

223227
// No (σ.p) → standard ERI
224-
if (n_comp == 1) {
228+
if (total_sp == 0) {
225229
const auto& r = coulomb_engine_.compute(a, b, c, d);
226230
if (r[0]) std::memcpy(buf_.data(), r[0], n_out * sizeof(real_t));
227231
return targets_;
@@ -300,73 +304,65 @@ class SigmaPEngine {
300304
}
301305

302306
// ---- Contraction ----
303-
for (int bq = 0; bq < n_bra; ++bq) {
304-
for (int kq = 0; kq < n_ket; ++kq) {
305-
int comp = bq * n_ket + kq;
306-
real_t* out = buf_.data() + comp * n_out;
307-
308-
// T4 sign (only for 2+2 (σ.p) case)
309-
int t4 = (bra_sp == 2 && ket_sp == 2 && bq > 0 && kq > 0) ? -1 : 1;
310-
311-
// Get direction pairs for bra and ket
312-
DirPair bp[3], kp[3];
313-
int nbp, nkp;
314-
get_particle_pairs(sp_a, sp_b, bra_sp, bq, bp, nbp);
315-
get_particle_pairs(sp_c, sp_d, ket_sp, kq, kp, nkp);
316-
317-
for (int ib = 0; ib < nbp; ++ib) {
318-
// Directions: r[i] for center i. -1 means "not (σ.p), no direction"
319-
int r[4];
320-
r[0] = sp_a ? bp[ib].r1 : -1;
321-
r[1] = sp_b ? bp[ib].r2 : -1;
322-
const int bsign = bp[ib].sign;
323-
324-
for (int ik = 0; ik < nkp; ++ik) {
325-
r[2] = sp_c ? kp[ik].r1 : -1;
326-
r[3] = sp_d ? kp[ik].r2 : -1;
327-
const int overall_sign = bsign * kp[ik].sign * t4;
328-
329-
// Loop over AM-shift combinations for each (σ.p) center
330-
// delta[i]: +1 (up) or -1 (down). 0 if no (σ.p).
331-
// di: 0=up, 1=down in the eri index
332-
for (int d0 = 0; d0 <= (sp[0] ? 1 : 0); ++d0) {
333-
if (d0 == 1 && !dn[0]) continue;
334-
const int delta0 = sp[0] ? (d0 == 0 ? +1 : -1) : 0;
335-
336-
for (int d1 = 0; d1 <= (sp[1] ? 1 : 0); ++d1) {
337-
if (d1 == 1 && !dn[1]) continue;
338-
const int delta1 = sp[1] ? (d1 == 0 ? +1 : -1) : 0;
339-
340-
for (int d2 = 0; d2 <= (sp[2] ? 1 : 0); ++d2) {
341-
if (d2 == 1 && !dn[2]) continue;
342-
const int delta2 = sp[2] ? (d2 == 0 ? +1 : -1) : 0;
343-
344-
for (int d3 = 0; d3 <= (sp[3] ? 1 : 0); ++d3) {
345-
if (d3 == 1 && !dn[3]) continue;
346-
const int delta3 = sp[3] ? (d3 == 0 ? +1 : -1) : 0;
347-
348-
// Down-shifts contribute -1 sign each
349-
const int shift_sign =
350-
(delta0 < 0 ? -1 : 1) * (delta1 < 0 ? -1 : 1) *
351-
(delta2 < 0 ? -1 : 1) * (delta3 < 0 ? -1 : 1);
352-
353-
const int eidx = d0 * 8 + d1 * 4 + d2 * 2 + d3;
354-
const real_t* e = eri[eidx];
355-
if (!e) continue;
356-
357-
const int sns = eri_nb[eidx] * eri_nc[eidx] * eri_nd[eidx];
358-
const int snc = eri_nc[eidx] * eri_nd[eidx];
359-
const int snd = eri_nd[eidx];
360-
361-
const int deltas[4] = {delta0, delta1, delta2, delta3};
362-
const int sign = overall_sign * shift_sign;
363-
364-
// Inner loop over output CGFs
365-
accumulate(out, e, sign, shells, sp, am, r, deltas, na, nb,
366-
nc, nd, sns, snc, snd);
367-
}
368-
}
369-
}
307+
// Identify which centers have (σ.p) — these form quaternion pairs.
308+
// sp_centers[i] = index of i-th (σ.p) center in {0=a,1=b,2=c,3=d}
309+
int sp_centers[4], n_sp_centers = 0;
310+
for (int i = 0; i < 4; ++i)
311+
if (sp[i]) sp_centers[n_sp_centers++] = i;
312+
313+
if (total_sp == 1) {
314+
// Single unpaired (σ.p): 3 Cartesian components (DF only)
315+
const int ci = sp_centers[0]; // the (σ.p) center
316+
for (int r_dir = 0; r_dir < 3; ++r_dir) {
317+
real_t* out = buf_.data() + r_dir * n_out;
318+
int r[4] = {-1, -1, -1, -1};
319+
r[ci] = r_dir;
320+
contract_shifts(out, eri, eri_nb, eri_nc, eri_nd, sp, dn, am, r, 1, na,
321+
nb, nc, nd);
322+
}
323+
} else if (total_sp == 2) {
324+
// One quaternion pair: 4 components {S, X, Y, Z}
325+
const int c1 = sp_centers[0], c2 = sp_centers[1];
326+
for (int q = 0; q < 4; ++q) {
327+
real_t* out = buf_.data() + q * n_out;
328+
DirPair pairs[3];
329+
int npairs;
330+
get_qpairs(q, pairs, npairs);
331+
for (int ip = 0; ip < npairs; ++ip) {
332+
int r[4] = {-1, -1, -1, -1};
333+
r[c1] = pairs[ip].r1;
334+
r[c2] = pairs[ip].r2;
335+
contract_shifts(out, eri, eri_nb, eri_nc, eri_nd, sp, dn, am, r,
336+
pairs[ip].sign, na, nb, nc, nd);
337+
}
338+
}
339+
} else if (total_sp == 4) {
340+
// Two quaternion pairs: 16 components, index = 4*bra_q + ket_q
341+
// Bra pair: first two (σ.p) centers, Ket pair: last two
342+
// With standard ordering (a,b,c,d), bra=(0,1), ket=(2,3)
343+
const int b1 = sp_centers[0], b2 = sp_centers[1];
344+
const int k1 = sp_centers[2], k2 = sp_centers[3];
345+
for (int bq = 0; bq < 4; ++bq) {
346+
DirPair bp[3];
347+
int nbp;
348+
get_qpairs(bq, bp, nbp);
349+
for (int kq = 0; kq < 4; ++kq) {
350+
int comp = bq * 4 + kq;
351+
real_t* out = buf_.data() + comp * n_out;
352+
// T4 sign: -1 when both quaternion indices are cross-product
353+
int t4 = (bq > 0 && kq > 0) ? -1 : 1;
354+
DirPair kp[3];
355+
int nkp;
356+
get_qpairs(kq, kp, nkp);
357+
for (int ib = 0; ib < nbp; ++ib) {
358+
for (int ik = 0; ik < nkp; ++ik) {
359+
int r[4] = {-1, -1, -1, -1};
360+
r[b1] = bp[ib].r1;
361+
r[b2] = bp[ib].r2;
362+
r[k1] = kp[ik].r1;
363+
r[k2] = kp[ik].r2;
364+
contract_shifts(out, eri, eri_nb, eri_nc, eri_nd, sp, dn, am, r,
365+
bp[ib].sign * kp[ik].sign * t4, na, nb, nc, nd);
370366
}
371367
}
372368
}
@@ -376,30 +372,53 @@ class SigmaPEngine {
376372
return targets_;
377373
}
378374

379-
/// Get direction pairs for a particle based on (σ.p) config
380-
static void get_particle_pairs(bool sp_f1, bool sp_f2, int n_sp, int q_idx,
381-
DirPair* pairs, int& npairs) {
382-
if (n_sp == 0) {
383-
pairs[0] = {0, 0, 1};
384-
npairs = 1;
385-
} else if (n_sp == 1) {
386-
// Single (σ.p): q_idx is the Cartesian direction (x=0, y=1, z=2)
387-
if (sp_f1) {
388-
pairs[0] = {q_idx, 0, 1};
389-
} else {
390-
pairs[0] = {0, q_idx, 1};
375+
/// Contract AM-shift terms for a fixed set of directions r[0..3].
376+
/// Loops over all up/down shift combinations and accumulates into out.
377+
void contract_shifts(real_t* out, const std::array<const real_t*, 16>& eri,
378+
const std::array<int, 16>& eri_nb,
379+
const std::array<int, 16>& eri_nc,
380+
const std::array<int, 16>& eri_nd, const bool sp[4],
381+
const Shell* dn[4], const int am[4], const int r[4],
382+
int dir_sign, int na, int nb, int nc, int nd) {
383+
for (int d0 = 0; d0 <= (sp[0] ? 1 : 0); ++d0) {
384+
if (d0 == 1 && !dn[0]) continue;
385+
const int delta0 = sp[0] ? (d0 == 0 ? +1 : -1) : 0;
386+
for (int d1 = 0; d1 <= (sp[1] ? 1 : 0); ++d1) {
387+
if (d1 == 1 && !dn[1]) continue;
388+
const int delta1 = sp[1] ? (d1 == 0 ? +1 : -1) : 0;
389+
for (int d2 = 0; d2 <= (sp[2] ? 1 : 0); ++d2) {
390+
if (d2 == 1 && !dn[2]) continue;
391+
const int delta2 = sp[2] ? (d2 == 0 ? +1 : -1) : 0;
392+
for (int d3 = 0; d3 <= (sp[3] ? 1 : 0); ++d3) {
393+
if (d3 == 1 && !dn[3]) continue;
394+
const int delta3 = sp[3] ? (d3 == 0 ? +1 : -1) : 0;
395+
396+
const int shift_sign =
397+
(delta0 < 0 ? -1 : 1) * (delta1 < 0 ? -1 : 1) *
398+
(delta2 < 0 ? -1 : 1) * (delta3 < 0 ? -1 : 1);
399+
400+
const int eidx = d0 * 8 + d1 * 4 + d2 * 2 + d3;
401+
const real_t* e = eri[eidx];
402+
if (!e) continue;
403+
404+
const int sns = eri_nb[eidx] * eri_nc[eidx] * eri_nd[eidx];
405+
const int snc = eri_nc[eidx] * eri_nd[eidx];
406+
const int snd = eri_nd[eidx];
407+
const int deltas[4] = {delta0, delta1, delta2, delta3};
408+
409+
accumulate(out, e, dir_sign * shift_sign, sp, am, r, deltas, na, nb,
410+
nc, nd, sns, snc, snd);
411+
}
412+
}
391413
}
392-
npairs = 1;
393-
} else {
394-
get_qpairs(q_idx, pairs, npairs);
395414
}
396415
}
397416

398417
/// Accumulate one AM-shift term into the output buffer
399418
void accumulate(real_t* out, const real_t* eri_buf, int sign,
400-
const Shell* shells[4], const bool sp[4], const int am[4],
401-
const int r[4], const int delta[4], int na, int nb, int nc,
402-
int nd, int sns, int snc, int snd) {
419+
const bool sp[4], const int am[4], const int r[4],
420+
const int delta[4], int na, int nb, int nc, int nd, int sns,
421+
int snc, int snd) {
403422
int oa = 0;
404423
for (int ax = am[0]; ax >= 0; --ax) {
405424
for (int ay = am[0] - ax; ay >= 0; --ay) {

0 commit comments

Comments
 (0)