[PR-3] feat: implement ETR (Eq. 66) and primitive contraction for OS+HGP pipeline#237
[PR-3] feat: implement ETR (Eq. 66) and primitive contraction for OS+HGP pipeline#237San1357 wants to merge 6 commits intotheochem:masterfrom
Conversation
| from gbasis.utils import factorial2 | ||
|
|
||
| # Cache for factorial2 values to avoid repeated computation | ||
| _FACTORIAL2_CACHE = {} |
There was a problem hiding this comment.
Python has a simple tool for this type of caching since Python 3.9(??):
https://docs.python.org/3.14/library/functools.html#functools.cache
You can just decorate your function:
@functools.cache
def _get_factorial2_norm(angmom_components):
...There was a problem hiding this comment.
Hi @msricher
Done! Replaced _FACTORIAL2_CACHE = {} with @functools.cache decorator.
Since functools.cache requires hashable arguments and NumPy arrays are not hashable, the function now accepts a tuple of tuples instead of an array.
| return _FACTORIAL2_CACHE[key] | ||
|
|
||
|
|
||
| def _optimized_contraction( |
There was a problem hiding this comment.
I wonder if a better function signature would just be def f(integrals, exps, coeffs, angmoms):?
Then you can use it like this, if all of the exps, coeffs, and angmoms come from indexing the corresponding arrays:
f(integrals, exps[(a, b, c, d),], coeffs[(a, b, c, d),], angmoms[(a, b, c, d),])
I'm not sure if the angmoms are extracted from an array, though... but in general using fewer function arguments by applying NumPy's advanced indexing, or by other grouping strategies, is cleaner. Is it faster? Not sure, but the difference is probably not much.
Just some thoughts I had reading this part.
There was a problem hiding this comment.
Done! Simplified the signature as suggested:
_optimized_contraction(integrals_etransf, exps, coeffs, angmoms)
where:
exps = (exps_a, exps_b, exps_c, exps_d)coeffs = (coeffs_a, coeffs_b, coeffs_c, coeffs_d)angmoms = (angmom_a, angmom_b, angmom_c, angmom_d)
The values are unpacked inside the function.
is this ok? @msricher
| Contracted integrals with shape (c_x, c_y, c_z, a_x, a_y, a_z, M_a, M_c, M_b, M_d). | ||
| """ | ||
| # Precompute normalization constants (1D arrays) | ||
| norm_a = (2 * exps_a / np.pi) ** 0.75 * (4 * exps_a) ** (angmom_a / 2) |
There was a problem hiding this comment.
This part would definitely benefit from using arrays and vectorized operations with NumPy.
There was a problem hiding this comment.
angmoms = np.array([l_a, l_b, l_c, l_d])
exps = np.array([e_a, e_b, e_c, e_d])
coeffs = np.array([c_a, c_b, c_c, c_d])
# etc. ...
norms = ((2 / np.pi) * exps) ** 0.75 * (4 * exps) ** (angmoms / 2)
coeffs_norm = coeffs * norms[:, :, np.newaxis] # or something like this, there's some
# broadcasting you can do to make it workI just mean that if you are going to pass these arguments in as single arrays, then there's a bit more vectorization you can do with the (a, b, c, d) centers.
There was a problem hiding this comment.
Understood! @msricher,
I was confused, now I've Vectorized the norm computation over all 4 centers as suggested. Now using NumPy arrays and computing norms in one shot using broadcasting — all 4 centers (a, b, c, d) are handled together instead of separately.
The caller-side passing of single arrays (e.g. exps[(a,b,c,d),]) will be handled in the full pipeline when HRR is integrated. For now, the conversion to arrays is done internally inside the function.
Does this look good to you? @msricher
There was a problem hiding this comment.
Actually after testing , I realized that using np.array stacking breaks when shells have different numbers of primitives (K). For example, if K_a=2 and K_b=1, np.array([exps_a, exps_b]) creates a ragged array and fails. So I've used per-center list comprehension instead — the norm computation is still vectorized using NumPy ops per center, just not across all 4 centers simultaneously
| - zeta_over_eta * integrals_etransf[:, :, c, :, :, 2:, ...] | ||
| ) | ||
|
|
||
| return integrals_etransf |
There was a problem hiding this comment.
The rest of it, the actual contractions and recurrences, seems good to me.
Summary
_electron_transfer_recursion()— ETR (Eq. 66), transfers angular momentum from bra to ket side_optimized_contraction()— primitive-to-contracted contraction usingnp.einsum_get_factorial2_norm()— normalization helper using double factorialWhat Changed
gbasis/integrals/_two_elec_int_improved.pytests/test_two_elec_int_improved.pyMathematical Reference
The Electron Transfer Recursion (Eq. 66) is:
where angular momentum is transferred from center A to center C.
How To Test
Proof That It Works
Tests (9 passed)
Check coverage
Black: Clean & Ruff: Clean
Existing tests: Not broken
300 passed, 279 skipped, 1 xfailed, 4 warnings in 67.86s (0:01:07)
First Checklist
Scope of this PR
_electron_transfer_recursion(ETR)_optimized_contractionwith per-center vectorized norm computationfunctools.cacheto_get_factorial2_normfor performanceSecond Checklist
Type of Changes