Skip to content

PR-6 - feat: add C shell loop binding for 2-electron ERI#254

Draft
San1357 wants to merge 3 commits into
theochem:san1357-devfrom
San1357:pr-6/eri-shellloop
Draft

PR-6 - feat: add C shell loop binding for 2-electron ERI#254
San1357 wants to merge 3 commits into
theochem:san1357-devfrom
San1357:pr-6/eri-shellloop

Conversation

@San1357

@San1357 San1357 commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Summary

Changes

libcint_wrap.c

  • Added eri_shellloop — dedicated C function that loops over all 4 shells (I, J, K, L) internally and fills the complete 4-dimensional ERI tensor in one call
  • Uses 8-fold permutation symmetry to fill all equivalent elements
  • Registered in PyMethodDef

libcint.py

  • Added electron_repulsion() method in CBasis class that calls eri_shellloop directly
  • Applies transpose(0, 2, 1, 3) to convert from chemist to physicist notation, matching the existing electron_repulsion_integral() output

Performance Results

Tested on H, He, Li with cc-pVDZ basis set (nbfn=24):

Integral Python Loop C Loop Speedup
overlap 0.792 ms 0.061 ms ~13.0x
kinetic 0.788 ms 0.071 ms ~11.2x
nuclear 0.788 ms 0.080 ms ~9.9x
rinv 0.772 ms 0.063 ms ~12.3x
ERI 119.656 ms 21.552 ms ~5.6x

Correctness Results

Tested on H, He, Li with cc-pVDZ basis set (nbfn=24), atol=1e-4:

Integral Match
overlap ✅ True
kinetic ✅ True
nuclear ✅ True
rinv ✅ True
ERI ✅ True

How To Test

Build locally:

pip install -e . --no-build-isolation

Correctness test:

python3 << 'EOF'
from gbasis.integrals.libcint import CBasis
from gbasis.parsers import make_contractions, parse_nwchem
import numpy as np

basis_dict = parse_nwchem("tests/data_ccpvdz.nwchem")
atcoords = np.eye(3) / 0.5291772083
atsyms = ["H", "He", "Li"]
py_basis = make_contractions(basis_dict, atsyms, atcoords, coord_types="spherical")
cb = CBasis(py_basis, atsyms, atcoords)

print("eri match:", np.allclose(cb.electron_repulsion_integral(), cb.electron_repulsion(), atol=1e-4))
EOF

Output:


eri match: True


Speed Test

python3 << 'EOF'
from gbasis.integrals.libcint import CBasis
from gbasis.parsers import make_contractions, parse_nwchem
import numpy as np
import time

basis_dict = parse_nwchem("tests/data_ccpvdz.nwchem")
atcoords = np.eye(3) / 0.5291772083
atsyms = ["H", "He", "Li"]
py_basis = make_contractions(basis_dict, atsyms, atcoords, coord_types="spherical")
cb = CBasis(py_basis, atsyms, atcoords)

# Warm-up
cb.electron_repulsion_integral()
cb.electron_repulsion()

tests = [
    ("overlap", cb.overlap_integral, cb.overlap),
    ("kinetic", cb.kinetic_energy_integral, cb.kinetic_energy),
    ("nuclear", cb.nuclear_attraction_integral, cb.nuclear_attraction),
    ("rinv", lambda: cb.r_inv_integral(origin=np.zeros(3)), cb.rinv),
    ("eri", cb.electron_repulsion_integral, cb.electron_repulsion),
]

for name, py_fn, c_fn in tests:
    t0 = time.perf_counter()
    S_py = py_fn()
    t1 = time.perf_counter()
    t2 = time.perf_counter()
    S_c = c_fn()
    t3 = time.perf_counter()
    py_time = (t1-t0)*1000
    c_time = (t3-t2)*1000
    speedup = py_time/c_time
    print(f"{name:8s} | Python: {py_time:8.3f} ms | C: {c_time:8.3f} ms | Speedup: {speedup:6.1f}x")
EOF

Output:

Screenshot 2026-06-24 at 3 40 30 PM

@San1357 San1357 requested a review from msricher June 24, 2026 10:36
Comment thread gbasis/integrals/libcint.py Outdated
out, self.natm, self.atm, self.nbas,
self.bas, self.env, self._offs, self.nbfn
)
return out.transpose(0, 2, 1, 3)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you should fill the two-electron integral array in C such that it doesn't need transposing.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

I've updated the C code to fill the ERI array directly in physicist notation by swapping the j and k indices, so there's no need for the transpose on the Python side anymore.

Comment thread gbasis/integrals/src/libcint_wrap.c Outdated
* Loops over shells I, J; calls libcint_func per shell pair; fills the
* symmetric output matrix using PyArray_GETPTR (NumPy C-API).
*/
#define DEFINE_SHELLLOOP_INT1e(func_name, libcint_func) \

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again let's rename this to something more descriptive, like DEFINE_INT1E_ARRAY_FN. Same for the 2-electron integrals.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new C-level shell-loop wrapper for computing full 4-index 2-electron ERI tensors via libcint, and exposes it as a new CBasis.electron_repulsion() Python method to reduce Python-level looping overhead.

Changes:

  • Renamed/standardized C macro names for 1-electron array wrappers and 1-electron shell-loop wrappers.
  • Added eri_shellloop C binding that loops over (I,J,K,L) shells and fills the ERI tensor using 8-fold symmetry.
  • Added CBasis.electron_repulsion() that allocates the ERI tensor and calls the new C binding.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
gbasis/integrals/src/libcint_wrap.c Adds the new ERI shell-loop C binding and registers it with the Python module; also renames the 1e wrapper macros for consistency.
gbasis/integrals/libcint.py Adds CBasis.electron_repulsion() which calls into the new eri_shellloop binding.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +232 to +235
int shls[4];
double buf[100000] = {0};

int ipos = 0;
Comment on lines +1329 to +1334
out = np.zeros((self.nbfn, self.nbfn, self.nbfn, self.nbfn), dtype=c_double)
libcint_bindings.eri_shellloop(
out, self.natm, self.atm, self.nbas,
self.bas, self.env, self._offs, self.nbfn
)
return out
Comment on lines +1312 to +1316
def electron_repulsion(self):
r"""
Compute the electron repulsion integrals.

The two-electron repulsion integral between basis functions
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants