Skip to content

Commit e1612a7

Browse files
committed
fix: handle different K per center in _optimized_contraction
2 parents 7ec98dd + 60ddebb commit e1612a7

2 files changed

Lines changed: 73 additions & 67 deletions

File tree

gbasis/integrals/_two_elec_int_improved.py

Lines changed: 35 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -15,79 +15,55 @@
1515
- Ahlrichs, R. Phys. Chem. Chem. Phys. 2006, 8, 3072.
1616
"""
1717

18+
import functools
19+
1820
import numpy as np
1921

2022
from gbasis.utils import factorial2
2123

22-
# Cache for factorial2 values to avoid repeated computation
23-
_FACTORIAL2_CACHE = {}
24-
2524

26-
def _get_factorial2_norm(angmom_components):
25+
@functools.cache
26+
def _get_factorial2_norm(angmom_key):
2727
"""Get cached factorial2 normalization for angular momentum components.
2828
2929
Parameters
3030
----------
31-
angmom_components : np.ndarray(n, 3)
32-
Angular momentum components.
31+
angmom_key : tuple of tuples
32+
Angular momentum components as a tuple of tuples, e.g.
33+
((lx1, ly1, lz1), (lx2, ly2, lz2), ...).
3334
3435
Returns
3536
-------
3637
norm : np.ndarray(n,)
3738
Normalization factors 1/sqrt(prod((2*l-1)!!)).
3839
"""
39-
key = tuple(map(tuple, angmom_components))
40-
if key not in _FACTORIAL2_CACHE:
41-
_FACTORIAL2_CACHE[key] = 1.0 / np.sqrt(
42-
np.prod(factorial2(2 * angmom_components - 1), axis=1)
43-
)
44-
return _FACTORIAL2_CACHE[key]
40+
angmom_components = np.array(angmom_key)
41+
return 1.0 / np.sqrt(np.prod(factorial2(2 * angmom_components - 1), axis=1))
4542

4643

47-
def _optimized_contraction(
48-
integrals_etransf,
49-
exps_a,
50-
exps_b,
51-
exps_c,
52-
exps_d,
53-
coeffs_a,
54-
coeffs_b,
55-
coeffs_c,
56-
coeffs_d,
57-
angmom_a,
58-
angmom_b,
59-
angmom_c,
60-
angmom_d,
61-
):
44+
def _optimized_contraction(integrals_etransf, exps, coeffs, angmoms):
6245
"""Optimized primitive contraction using einsum.
6346
6447
Parameters
6548
----------
6649
integrals_etransf : np.ndarray
6750
ETR output with shape (c_x, c_y, c_z, a_x, a_y, a_z, K_d, K_b, K_c, K_a).
68-
exps_a/b/c/d : np.ndarray
69-
Primitive exponents.
70-
coeffs_a/b/c/d : np.ndarray
71-
Contraction coefficients.
72-
angmom_a/b/c/d : int
73-
Angular momenta.
51+
exps : array-like of shape (4, K)
52+
Primitive exponents stacked for all 4 centers (a, b, c, d).
53+
coeffs : array-like of shape (4, K, M)
54+
Contraction coefficients stacked for all 4 centers (a, b, c, d).
55+
angmoms : array-like of shape (4,)
56+
Angular momenta for all 4 centers (a, b, c, d).
7457
7558
Returns
7659
-------
7760
contracted : np.ndarray
7861
Contracted integrals with shape (c_x, c_y, c_z, a_x, a_y, a_z, M_a, M_c, M_b, M_d).
7962
"""
80-
# Precompute normalization constants (1D arrays)
81-
norm_a = (2 * exps_a / np.pi) ** 0.75 * (4 * exps_a) ** (angmom_a / 2)
82-
norm_b = (2 * exps_b / np.pi) ** 0.75 * (4 * exps_b) ** (angmom_b / 2)
83-
norm_c = (2 * exps_c / np.pi) ** 0.75 * (4 * exps_c) ** (angmom_c / 2)
84-
norm_d = (2 * exps_d / np.pi) ** 0.75 * (4 * exps_d) ** (angmom_d / 2)
85-
86-
# Multiply coefficients by normalization (more efficient than per-element)
87-
coeffs_a_norm = coeffs_a * norm_a[:, np.newaxis]
88-
coeffs_b_norm = coeffs_b * norm_b[:, np.newaxis]
89-
coeffs_c_norm = coeffs_c * norm_c[:, np.newaxis]
90-
coeffs_d_norm = coeffs_d * norm_d[:, np.newaxis]
63+
# Compute norms per center (supports different K per center)
64+
norms = [((2 / np.pi) * e) ** 0.75 * (4 * e) ** (ang / 2) for e, ang in zip(exps, angmoms)]
65+
coeffs_norm = [c * n[:, np.newaxis] for c, n in zip(coeffs, norms)]
66+
coeffs_a_norm, coeffs_b_norm, coeffs_c_norm, coeffs_d_norm = coeffs_norm
9167

9268
# Use einsum with optimization for contraction
9369
# Input: (c_x, c_y, c_z, a_x, a_y, a_z, K_d, K_b, K_c, K_a)
@@ -594,10 +570,18 @@ def _horizontal_recursion_relation(
594570
integrals = np.transpose(integrals_horiz_b2, (1, 0, 3, 2, 4, 6, 5, 7))
595571

596572
# Apply factorial2 normalization for angular momentum components
597-
norm_a = _get_factorial2_norm(angmom_components_a).reshape(-1, 1, 1, 1, 1, 1, 1, 1)
598-
norm_b = _get_factorial2_norm(angmom_components_b).reshape(1, -1, 1, 1, 1, 1, 1, 1)
599-
norm_c = _get_factorial2_norm(angmom_components_c).reshape(1, 1, -1, 1, 1, 1, 1, 1)
600-
norm_d = _get_factorial2_norm(angmom_components_d).reshape(1, 1, 1, -1, 1, 1, 1, 1)
573+
norm_a = _get_factorial2_norm(tuple(map(tuple, angmom_components_a))).reshape(
574+
-1, 1, 1, 1, 1, 1, 1, 1
575+
)
576+
norm_b = _get_factorial2_norm(tuple(map(tuple, angmom_components_b))).reshape(
577+
1, -1, 1, 1, 1, 1, 1, 1
578+
)
579+
norm_c = _get_factorial2_norm(tuple(map(tuple, angmom_components_c))).reshape(
580+
1, 1, -1, 1, 1, 1, 1, 1
581+
)
582+
norm_d = _get_factorial2_norm(tuple(map(tuple, angmom_components_d))).reshape(
583+
1, 1, 1, -1, 1, 1, 1, 1
584+
)
601585

602586
integrals = integrals * norm_a * norm_b * norm_c * norm_d
603587

@@ -725,18 +709,9 @@ def compute_two_electron_integrals_os_hgp(
725709
# --- Step 4: Contract primitives ---
726710
integrals_cont = _optimized_contraction(
727711
integrals_etransf,
728-
exps_a,
729-
exps_b,
730-
exps_c,
731-
exps_d,
732-
coeffs_a,
733-
coeffs_b,
734-
coeffs_c,
735-
coeffs_d,
736-
angmom_a,
737-
angmom_b,
738-
angmom_c,
739-
angmom_d,
712+
(exps_a, exps_b, exps_c, exps_d),
713+
(coeffs_a, coeffs_b, coeffs_c, coeffs_d),
714+
(angmom_a, angmom_b, angmom_c, angmom_d),
740715
)
741716

742717
# --- Step 5: HRR (done LAST per HGP scheme) ---

tests/test_two_elec_int_improved.py

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from gbasis.integrals._two_elec_int_improved import (
1414
_electron_transfer_recursion,
1515
_get_factorial2_norm,
16+
_optimized_contraction,
1617
_vertical_recursion_relation,
1718
compute_two_electron_integrals_os_hgp,
1819
)
@@ -161,29 +162,59 @@ def test_etr_output_shape(self):
161162
assert result.shape == expected_shape
162163

163164

165+
class TestOptimizedContraction:
166+
"""Tests for the optimized primitive contraction."""
167+
168+
def test_output_shape(self):
169+
"""Test that contraction output has correct shape."""
170+
K, M = 2, 3
171+
integrals_etransf = np.random.rand(1, 1, 1, 1, 1, 1, K, K, K, K)
172+
exps = np.random.rand(4, K) + 0.1
173+
coeffs = np.random.rand(4, K, M)
174+
angmoms = np.array([0, 0, 0, 0])
175+
176+
result = _optimized_contraction(integrals_etransf, exps, coeffs, angmoms)
177+
178+
expected_shape = (1, 1, 1, 1, 1, 1, M, M, M, M)
179+
assert result.shape == expected_shape
180+
181+
def test_accepts_tuples(self):
182+
"""Test that contraction accepts tuples as well as arrays."""
183+
K, M = 2, 2
184+
integrals_etransf = np.random.rand(1, 1, 1, 1, 1, 1, K, K, K, K)
185+
exps = tuple(np.random.rand(K) + 0.1 for _ in range(4))
186+
coeffs = tuple(np.random.rand(K, M) for _ in range(4))
187+
angmoms = (0, 0, 0, 0)
188+
189+
result = _optimized_contraction(integrals_etransf, exps, coeffs, angmoms)
190+
191+
expected_shape = (1, 1, 1, 1, 1, 1, M, M, M, M)
192+
assert result.shape == expected_shape
193+
194+
164195
class TestFactorial2Norm:
165196
"""Tests for the factorial2 normalization helper."""
166197

167198
def test_s_orbital_norm(self):
168199
"""Test normalization for s-orbital (L=0)."""
169-
s_components = np.array([[0, 0, 0]])
170-
norm = _get_factorial2_norm(s_components)
200+
s_key = ((0, 0, 0),)
201+
norm = _get_factorial2_norm(s_key)
171202
# (2*0-1)!! = (-1)!! = 1, so norm = 1/sqrt(1) = 1
172203
assert np.allclose(norm, 1.0)
173204

174205
def test_p_orbital_norm(self):
175206
"""Test normalization for p-orbital (L=1)."""
176-
p_components = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]])
177-
norm = _get_factorial2_norm(p_components)
207+
p_key = ((1, 0, 0), (0, 1, 0), (0, 0, 1))
208+
norm = _get_factorial2_norm(p_key)
178209
# Each component has one (2*1-1)!! = 1!! = 1 and two (2*0-1)!! = 1
179210
# So norm = 1/sqrt(1*1*1) = 1 for all
180211
assert np.allclose(norm, 1.0)
181212

182213
def test_caching(self):
183214
"""Test that factorial2 normalization is cached."""
184-
d_components = np.array([[2, 0, 0], [1, 1, 0]])
185-
norm1 = _get_factorial2_norm(d_components)
186-
norm2 = _get_factorial2_norm(d_components)
215+
d_key = ((2, 0, 0), (1, 1, 0))
216+
norm1 = _get_factorial2_norm(d_key)
217+
norm2 = _get_factorial2_norm(d_key)
187218
assert np.allclose(norm1, norm2)
188219

189220

0 commit comments

Comments
 (0)