symvb 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- symvb/__init__.py +18 -0
- symvb/_o2_benchmark.py +163 -0
- symvb/_o2_blocked.py +301 -0
- symvb/_o2_symmetric.py +124 -0
- symvb/data.py +24 -0
- symvb/fixed_psi.py +272 -0
- symvb/functions.py +231 -0
- symvb/huckel.py +322 -0
- symvb/mo_projection.py +252 -0
- symvb/molecule.py +736 -0
- symvb/numerical.py +267 -0
- symvb/operators.py +667 -0
- symvb/orbital_permutations.py +49 -0
- symvb/slaterdet.py +142 -0
- symvb/spin.py +233 -0
- symvb/symmetry.py +467 -0
- symvb/system.py +354 -0
- symvb/test_det_conventions.py +292 -0
- symvb/test_fixed_psi.py +170 -0
- symvb/test_functions.py +95 -0
- symvb/test_molecule.py +467 -0
- symvb/test_non_default_flags.py +124 -0
- symvb/test_numerical.py +72 -0
- symvb/test_o2_agreement.py +548 -0
- symvb/test_o2_symmetric.py +193 -0
- symvb/test_operators.py +227 -0
- symvb/test_slaterdet.py +93 -0
- symvb/test_symmetry_exact.py +234 -0
- symvb/test_system.py +297 -0
- symvb-2.0.0.dist-info/METADATA +152 -0
- symvb-2.0.0.dist-info/RECORD +35 -0
- symvb-2.0.0.dist-info/WHEEL +5 -0
- symvb-2.0.0.dist-info/licenses/LICENSE +21 -0
- symvb-2.0.0.dist-info/top_level.txt +1 -0
- symvb-2.0.0.dist-info/zip-safe +1 -0
symvb/__init__.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from symvb.slaterdet import SlaterDet
|
|
2
|
+
from symvb.fixed_psi import FixedPsi
|
|
3
|
+
from symvb.molecule import Molecule
|
|
4
|
+
from symvb import symmetry
|
|
5
|
+
from symvb import spin
|
|
6
|
+
from symvb import huckel
|
|
7
|
+
from symvb import mo_projection
|
|
8
|
+
from symvb import operators
|
|
9
|
+
from symvb import system
|
|
10
|
+
from symvb.system import (System, hamiltonian, ground_state,
|
|
11
|
+
chirgwin_coulson, structure_vector)
|
|
12
|
+
from symvb.mo_projection import verify_eigenpair, EigenpairResidualError
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
|
|
16
|
+
logging.basicConfig(format='%(levelname)-8s: %(message)s')
|
|
17
|
+
|
|
18
|
+
__version__ = "2.0.0"
|
symvb/_o2_benchmark.py
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Benchmark the two o2_det implementations on a fixed system battery.
|
|
3
|
+
|
|
4
|
+
Runs each (system, method) combination N_TRIALS times, reports the median
|
|
5
|
+
of wall-time and peak memory, plus the total SymPy-op count of the
|
|
6
|
+
resulting matrix as a proxy for output expression complexity.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
cd vbt-3-repo
|
|
10
|
+
PYTHONPATH=. python3 -m symvb._o2_benchmark
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import gc
|
|
15
|
+
import time
|
|
16
|
+
import tracemalloc
|
|
17
|
+
|
|
18
|
+
import sympy as sp
|
|
19
|
+
|
|
20
|
+
from symvb import Molecule
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# --- Cases ---------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
_PPP_2E = {'U': ('1111',), 'J': ('1212',), 'K': ('1122',),
|
|
26
|
+
'M': ('1112', '1121', '1222')}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _h2(cutoff):
|
|
30
|
+
return dict(
|
|
31
|
+
label='H2',
|
|
32
|
+
Na=1, Nb=1, Norbs=2,
|
|
33
|
+
kwargs=dict(subst_2e=_PPP_2E,
|
|
34
|
+
interacting_orbs=['ab'],
|
|
35
|
+
max_2e_centers=cutoff),
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _allyl(cutoff):
|
|
40
|
+
return dict(
|
|
41
|
+
label='allyl-3c4e',
|
|
42
|
+
Na=2, Nb=2, Norbs=3,
|
|
43
|
+
kwargs=dict(subst_2e=_PPP_2E,
|
|
44
|
+
interacting_orbs=['ab', 'bc'],
|
|
45
|
+
max_2e_centers=cutoff),
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _benzene_ppp():
|
|
50
|
+
return dict(
|
|
51
|
+
label='benzene-PPP',
|
|
52
|
+
Na=3, Nb=3, Norbs=6,
|
|
53
|
+
kwargs=dict(
|
|
54
|
+
zero_ii=True,
|
|
55
|
+
interacting_orbs=['ab', 'bc', 'cd', 'de', 'ef', 'af'],
|
|
56
|
+
subst={'h': ('H_ab', 'H_bc', 'H_cd', 'H_de', 'H_ef', 'H_af'),
|
|
57
|
+
's': ('S_ab', 'S_bc', 'S_cd', 'S_de', 'S_ef', 'S_af')},
|
|
58
|
+
subst_2e=_PPP_2E,
|
|
59
|
+
max_2e_centers=2,
|
|
60
|
+
),
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
CASES = [
|
|
65
|
+
(_h2(cutoff=1), 5),
|
|
66
|
+
(_h2(cutoff=2), 5),
|
|
67
|
+
(_allyl(cutoff=1), 5),
|
|
68
|
+
(_allyl(cutoff=2), 5),
|
|
69
|
+
(_allyl(cutoff=3), 3),
|
|
70
|
+
(_allyl(cutoff=4), 3),
|
|
71
|
+
(_benzene_ppp(), 1), # 1 trial; if blocked is fast we can rerun with more
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# --- Bench runner --------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
def _build_matrix_o2(method, case):
|
|
78
|
+
m = Molecule(o2_method=method, **case['kwargs'])
|
|
79
|
+
m.generate_basis(case['Na'], case['Nb'], case['Norbs'])
|
|
80
|
+
return m.o2_matrix(m.basis)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _count_ops(matrix):
|
|
84
|
+
return sum(int(matrix[i, j].count_ops())
|
|
85
|
+
for i in range(matrix.rows) for j in range(matrix.cols))
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _trial(method, case):
|
|
89
|
+
gc.collect()
|
|
90
|
+
tracemalloc.start()
|
|
91
|
+
t0 = time.perf_counter()
|
|
92
|
+
H2 = _build_matrix_o2(method, case)
|
|
93
|
+
wall = time.perf_counter() - t0
|
|
94
|
+
_, peak = tracemalloc.get_traced_memory()
|
|
95
|
+
tracemalloc.stop()
|
|
96
|
+
ops = _count_ops(H2)
|
|
97
|
+
n = H2.rows
|
|
98
|
+
return dict(wall=wall, peak_mb=peak / (1024 * 1024), ops=ops, dim=n)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _median(xs):
|
|
102
|
+
xs = sorted(xs)
|
|
103
|
+
n = len(xs)
|
|
104
|
+
if n == 0:
|
|
105
|
+
return float('nan')
|
|
106
|
+
if n % 2 == 1:
|
|
107
|
+
return xs[n // 2]
|
|
108
|
+
return 0.5 * (xs[n // 2 - 1] + xs[n // 2])
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def benchmark():
|
|
112
|
+
print('=' * 78)
|
|
113
|
+
print('Two-electron matrix benchmark: direct vs blocked')
|
|
114
|
+
print('=' * 78)
|
|
115
|
+
print()
|
|
116
|
+
header = ('| %-15s | %-7s | %-7s | dim | wall (s) | peak (MB) '
|
|
117
|
+
'| ops | speedup |')
|
|
118
|
+
sep = ('|-----------------|---------|---------|-------'
|
|
119
|
+
'|------------|------------|----------|---------|')
|
|
120
|
+
print(header % ('system', 'cutoff', 'method'))
|
|
121
|
+
print(sep)
|
|
122
|
+
for case, n_trials in CASES:
|
|
123
|
+
cutoff = case['kwargs'].get('max_2e_centers', 4)
|
|
124
|
+
results = {}
|
|
125
|
+
for method in ('direct', 'blocked'):
|
|
126
|
+
print(' ... %s cutoff=%d %s (n=%d)'
|
|
127
|
+
% (case['label'], cutoff, method, n_trials), flush=True)
|
|
128
|
+
trials = []
|
|
129
|
+
for ti in range(n_trials):
|
|
130
|
+
t = _trial(method, case)
|
|
131
|
+
trials.append(t)
|
|
132
|
+
print(' trial %d: wall=%.3fs peak=%.1fMB'
|
|
133
|
+
% (ti, t['wall'], t['peak_mb']), flush=True)
|
|
134
|
+
results[method] = dict(
|
|
135
|
+
wall=_median([t['wall'] for t in trials]),
|
|
136
|
+
peak_mb=_median([t['peak_mb'] for t in trials]),
|
|
137
|
+
ops=trials[0]['ops'],
|
|
138
|
+
dim=trials[0]['dim'],
|
|
139
|
+
)
|
|
140
|
+
# Print rows; speedup column shown on the blocked row only.
|
|
141
|
+
for method in ('direct', 'blocked'):
|
|
142
|
+
r = results[method]
|
|
143
|
+
speedup = ''
|
|
144
|
+
if method == 'blocked' and results['direct']['wall'] > 0:
|
|
145
|
+
speedup = '%.2fx' % (results['direct']['wall'] / r['wall'])
|
|
146
|
+
print('| %-15s | %-7d | %-7s | %-5d | %-10.3f | %-10.2f '
|
|
147
|
+
'| %-8d | %-7s |'
|
|
148
|
+
% (case['label'], cutoff, method, r['dim'],
|
|
149
|
+
r['wall'], r['peak_mb'], r['ops'], speedup))
|
|
150
|
+
# Sanity: ops should agree (modulo chemist-symmetry symbol naming).
|
|
151
|
+
ops_diff = results['direct']['ops'] - results['blocked']['ops']
|
|
152
|
+
if ops_diff != 0:
|
|
153
|
+
print('| ^ ops differ by %+d (chemist-symbol naming, not '
|
|
154
|
+
'a correctness issue)' % ops_diff)
|
|
155
|
+
print()
|
|
156
|
+
print('Notes:')
|
|
157
|
+
print(' - wall and peak_mb are medians across N_TRIALS runs.')
|
|
158
|
+
print(' - ops = sum of sympy.count_ops() over all matrix entries.')
|
|
159
|
+
print(' - speedup > 1 means blocked is faster.')
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
if __name__ == '__main__':
|
|
163
|
+
benchmark()
|
symvb/_o2_blocked.py
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Spin-block-aware two-electron matrix element (Phase 1.5: half-pair cache).
|
|
3
|
+
|
|
4
|
+
The historical o2_det in molecule.py expands <D_A | H_2 | D_B> via Loewdin's
|
|
5
|
+
cofactor formula on the full spin-mixed (N-2)-electron overlap and recomputes
|
|
6
|
+
that overlap determinant for every (i, k, j, l) electron-position quadruple.
|
|
7
|
+
Because the AO overlap is spin-diagonal, the (N-2)-electron overlap is itself
|
|
8
|
+
block-diagonal in spin: it factors into per-spin-block determinants. Exploiting
|
|
9
|
+
that structure lets the matrix element be assembled from a small per-pair
|
|
10
|
+
precompute (per-spin-block 1-row and 2-row cofactor tables) plus three spin-
|
|
11
|
+
channel contractions (alpha-alpha, beta-beta, alpha-beta).
|
|
12
|
+
|
|
13
|
+
Half-pair caching (Phase 1.5)
|
|
14
|
+
-----------------------------
|
|
15
|
+
Per-spin-block cofactor tables depend only on the half-string of D_A and the
|
|
16
|
+
half-string of D_B for that spin. Across the basis, many (D_A, D_B) pairs
|
|
17
|
+
share their (alpha-half-pair, beta-half-pair) decomposition: for benzene
|
|
18
|
+
3a+3b in 6 orbitals, there are C(6,3)^2 = 400 dets but only C(6,3)^2 = 400
|
|
19
|
+
distinct alpha-half-strings and 400 distinct beta-half-strings, hence at
|
|
20
|
+
most 400 * 400 = 160,000 (D_A, D_B) full-pair entries spread across only
|
|
21
|
+
20*20 = 400 unique alpha-half-pairs and 400 unique beta-half-pairs. The
|
|
22
|
+
per-half-pair cofactor tables are therefore evaluated once and reused
|
|
23
|
+
~400-fold rather than rebuilt at every full-pair call.
|
|
24
|
+
|
|
25
|
+
The cache is lazy: built on demand inside o2_det_blocked, keyed on the
|
|
26
|
+
sorted (alphabetised-within-block) half-strings, attached to the Molecule
|
|
27
|
+
instance as ._o2_blocked_cache. It populates as the caller walks the basis
|
|
28
|
+
and hits all needed entries naturally during one m.o2_matrix(m.basis) sweep.
|
|
29
|
+
|
|
30
|
+
Algorithm
|
|
31
|
+
---------
|
|
32
|
+
With the user's det_string in possibly-arbitrary creation order, fold the
|
|
33
|
+
parities of (i) the spin-block permutation, (ii) the within-alpha sort,
|
|
34
|
+
and (iii) the within-beta sort into an overall sign and work in canonical
|
|
35
|
+
layout (alpha block first sorted alphabetically, then beta block likewise).
|
|
36
|
+
For the canonical layout:
|
|
37
|
+
|
|
38
|
+
- alpha-alpha pair: D^{AB}_{ik,jl} = det(S_beta) * Gamma^alpha_{ik,jl}
|
|
39
|
+
- beta-beta pair: D^{AB}_{ik,jl} = det(S_alpha) * Gamma^beta_{ik,jl}
|
|
40
|
+
- alpha-beta pair: D^{AB}_{ik,jl} = gamma^alpha_{ij} * gamma^beta_{kl}
|
|
41
|
+
|
|
42
|
+
where gamma^sigma_{ij} = (-1)^{i+j} det(S^sigma with row i col j removed) and
|
|
43
|
+
Gamma^sigma_{ik,jl} = (-1)^{i+k+j+l} det(S^sigma with rows i,k cols j,l
|
|
44
|
+
removed). The cofactor tables are looked up from the half-pair cache;
|
|
45
|
+
the contraction loops then sum over per-block index ranges directly.
|
|
46
|
+
|
|
47
|
+
The Phase 2 agreement-test harness in symvb/test_o2_agreement.py validates
|
|
48
|
+
symbolic parity with the direct path on a battery of cases; that harness
|
|
49
|
+
re-runs unchanged after this refactor because the algorithmic content is
|
|
50
|
+
identical.
|
|
51
|
+
"""
|
|
52
|
+
from __future__ import annotations
|
|
53
|
+
|
|
54
|
+
import sympy as sp
|
|
55
|
+
|
|
56
|
+
from symvb.functions import sorti
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
_SP_ZERO = sp.Integer(0)
|
|
60
|
+
_SP_ONE = sp.Integer(1)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def o2_det_blocked(molecule, D1, D2):
|
|
64
|
+
"""Two-electron matrix element via the spin-block-aware path with
|
|
65
|
+
half-pair caching.
|
|
66
|
+
|
|
67
|
+
Must agree symbolically with Molecule.o2_det(D1, D2) in 'direct' mode
|
|
68
|
+
on every input. See module docstring for algorithm.
|
|
69
|
+
"""
|
|
70
|
+
# --- Sz selection ----------------------------------------------------
|
|
71
|
+
if D1.Nel != D2.Nel:
|
|
72
|
+
return _SP_ZERO
|
|
73
|
+
if (len(D1.alpha_string) != len(D2.alpha_string) or
|
|
74
|
+
len(D1.beta_string) != len(D2.beta_string)):
|
|
75
|
+
return _SP_ZERO
|
|
76
|
+
|
|
77
|
+
# --- Bring each det to canonical (spin-block + alphabetical within
|
|
78
|
+
# block) and capture all three sign contributions per det. -------
|
|
79
|
+
A_alpha_s, A_alpha_inv = sorti(D1.alpha_string)
|
|
80
|
+
A_beta_s, A_beta_inv = sorti(D1.beta_string)
|
|
81
|
+
B_alpha_s, B_alpha_inv = sorti(D2.alpha_string)
|
|
82
|
+
B_beta_s, B_beta_inv = sorti(D2.beta_string)
|
|
83
|
+
|
|
84
|
+
sigma_A = (_spin_block_parity(D1.spins) *
|
|
85
|
+
(1 if A_alpha_inv % 2 == 0 else -1) *
|
|
86
|
+
(1 if A_beta_inv % 2 == 0 else -1))
|
|
87
|
+
sigma_B = (_spin_block_parity(D2.spins) *
|
|
88
|
+
(1 if B_alpha_inv % 2 == 0 else -1) *
|
|
89
|
+
(1 if B_beta_inv % 2 == 0 else -1))
|
|
90
|
+
overall_sign = sigma_A * sigma_B
|
|
91
|
+
|
|
92
|
+
# --- Half-pair cache lookup (lazy; builds on first request) ---------
|
|
93
|
+
alpha_data = _get_half_pair(molecule, 'alpha', A_alpha_s, B_alpha_s)
|
|
94
|
+
beta_data = _get_half_pair(molecule, 'beta', A_beta_s, B_beta_s)
|
|
95
|
+
|
|
96
|
+
# Lowercase canonical strings for integral-symbol queries.
|
|
97
|
+
A_alpha = A_alpha_s.lower()
|
|
98
|
+
A_beta = A_beta_s.lower()
|
|
99
|
+
B_alpha = B_alpha_s.lower()
|
|
100
|
+
B_beta = B_beta_s.lower()
|
|
101
|
+
|
|
102
|
+
n_a = len(A_alpha)
|
|
103
|
+
n_b = len(A_beta)
|
|
104
|
+
|
|
105
|
+
det_S_alpha = alpha_data['det']
|
|
106
|
+
det_S_beta = beta_data['det']
|
|
107
|
+
gamma_alpha = alpha_data['gamma']
|
|
108
|
+
gamma_beta = beta_data['gamma']
|
|
109
|
+
Gamma_alpha = alpha_data['Gamma']
|
|
110
|
+
Gamma_beta = beta_data['Gamma']
|
|
111
|
+
|
|
112
|
+
# --- alpha-alpha channel ---------------------------------------------
|
|
113
|
+
# Bucket Gamma cofactors by their (int_d - int_x) integrand. Skip
|
|
114
|
+
# quadruples whose Gamma cofactor is structurally zero (common when
|
|
115
|
+
# the spin-block overlap has zero rows from non-interacting AOs):
|
|
116
|
+
# iv-tuple construction and integral lookup avoided entirely.
|
|
117
|
+
T_aa = _SP_ZERO
|
|
118
|
+
if n_a >= 2:
|
|
119
|
+
aa_groups = {}
|
|
120
|
+
for i in range(n_a):
|
|
121
|
+
for k in range(i + 1, n_a):
|
|
122
|
+
for j in range(n_a):
|
|
123
|
+
for l in range(j + 1, n_a):
|
|
124
|
+
cof = Gamma_alpha[(i, k, j, l)]
|
|
125
|
+
if cof == _SP_ZERO:
|
|
126
|
+
continue
|
|
127
|
+
iv_d = (A_alpha[i], A_alpha[k], B_alpha[j], B_alpha[l])
|
|
128
|
+
iv_x = (A_alpha[i], A_alpha[k], B_alpha[l], B_alpha[j])
|
|
129
|
+
int_d = molecule.get_o2_expr(iv_d)
|
|
130
|
+
int_x = molecule.get_o2_expr(iv_x)
|
|
131
|
+
integrand = int_d - int_x
|
|
132
|
+
if integrand == _SP_ZERO:
|
|
133
|
+
continue
|
|
134
|
+
prev = aa_groups.get(integrand)
|
|
135
|
+
aa_groups[integrand] = cof if prev is None else prev + cof
|
|
136
|
+
for integrand, coef in aa_groups.items():
|
|
137
|
+
T_aa = T_aa + integrand * coef
|
|
138
|
+
T_aa = T_aa * det_S_beta
|
|
139
|
+
|
|
140
|
+
# --- beta-beta channel -----------------------------------------------
|
|
141
|
+
T_bb = _SP_ZERO
|
|
142
|
+
if n_b >= 2:
|
|
143
|
+
bb_groups = {}
|
|
144
|
+
for i in range(n_b):
|
|
145
|
+
for k in range(i + 1, n_b):
|
|
146
|
+
for j in range(n_b):
|
|
147
|
+
for l in range(j + 1, n_b):
|
|
148
|
+
cof = Gamma_beta[(i, k, j, l)]
|
|
149
|
+
if cof == _SP_ZERO:
|
|
150
|
+
continue
|
|
151
|
+
iv_d = (A_beta[i], A_beta[k], B_beta[j], B_beta[l])
|
|
152
|
+
iv_x = (A_beta[i], A_beta[k], B_beta[l], B_beta[j])
|
|
153
|
+
int_d = molecule.get_o2_expr(iv_d)
|
|
154
|
+
int_x = molecule.get_o2_expr(iv_x)
|
|
155
|
+
integrand = int_d - int_x
|
|
156
|
+
if integrand == _SP_ZERO:
|
|
157
|
+
continue
|
|
158
|
+
prev = bb_groups.get(integrand)
|
|
159
|
+
bb_groups[integrand] = cof if prev is None else prev + cof
|
|
160
|
+
for integrand, coef in bb_groups.items():
|
|
161
|
+
T_bb = T_bb + integrand * coef
|
|
162
|
+
T_bb = T_bb * det_S_alpha
|
|
163
|
+
|
|
164
|
+
# --- alpha-beta channel (direct only; exchange forbidden by spin) ----
|
|
165
|
+
# The integral set in symvb is small (5-10 distinct symbols at typical
|
|
166
|
+
# PPP scale), so many AO quadruples in this 4-loop share the same
|
|
167
|
+
# integral. Two-tier sparse iteration:
|
|
168
|
+
#
|
|
169
|
+
# (a) Reorder loops so gamma_alpha[i][j] sits in the outer pair,
|
|
170
|
+
# letting us skip the entire (k, l) inner loop when gamma_alpha
|
|
171
|
+
# is structurally zero (common when the alpha overlap has zero
|
|
172
|
+
# rows from non-interacting AOs).
|
|
173
|
+
# (b) Same for gamma_beta inside.
|
|
174
|
+
#
|
|
175
|
+
# Then accumulate the gamma_alpha * gamma_beta products per integral-
|
|
176
|
+
# symbol bucket and multiply once at the end: replaces ~n_a^2 * n_b^2
|
|
177
|
+
# three-way SymPy multiplies with the same number of two-way multiplies
|
|
178
|
+
# plus a handful of final symbol-bucket multiplies.
|
|
179
|
+
T_ab = _SP_ZERO
|
|
180
|
+
if n_a >= 1 and n_b >= 1:
|
|
181
|
+
ab_groups = {}
|
|
182
|
+
for i in range(n_a):
|
|
183
|
+
ai = A_alpha[i]
|
|
184
|
+
for j in range(n_a):
|
|
185
|
+
gA = gamma_alpha[i][j]
|
|
186
|
+
if gA == _SP_ZERO:
|
|
187
|
+
continue
|
|
188
|
+
bj = B_alpha[j]
|
|
189
|
+
for k in range(n_b):
|
|
190
|
+
ak = A_beta[k]
|
|
191
|
+
for l in range(n_b):
|
|
192
|
+
gB = gamma_beta[k][l]
|
|
193
|
+
if gB == _SP_ZERO:
|
|
194
|
+
continue
|
|
195
|
+
iv = (ai, ak, bj, B_beta[l])
|
|
196
|
+
int_d = molecule.get_o2_expr(iv)
|
|
197
|
+
if int_d == _SP_ZERO:
|
|
198
|
+
continue
|
|
199
|
+
product = gA * gB
|
|
200
|
+
prev = ab_groups.get(int_d)
|
|
201
|
+
ab_groups[int_d] = product if prev is None else prev + product
|
|
202
|
+
for int_sym, coef in ab_groups.items():
|
|
203
|
+
T_ab = T_ab + int_sym * coef
|
|
204
|
+
|
|
205
|
+
result = overall_sign * (T_aa + T_bb + T_ab)
|
|
206
|
+
if result == 0:
|
|
207
|
+
return _SP_ZERO
|
|
208
|
+
return result
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
# --- Half-pair cache -----------------------------------------------------
|
|
212
|
+
|
|
213
|
+
def _get_half_pair(molecule, spin, A_sorted, B_sorted):
|
|
214
|
+
"""Return cached cofactor tables for the (A_sorted, B_sorted) half-pair
|
|
215
|
+
in the given spin block. Builds on demand; subsequent calls hit the
|
|
216
|
+
cache. The cache is attached to the molecule instance and persists for
|
|
217
|
+
its lifetime."""
|
|
218
|
+
if not hasattr(molecule, '_o2_blocked_cache') or molecule._o2_blocked_cache is None:
|
|
219
|
+
molecule._o2_blocked_cache = {'alpha': {}, 'beta': {}}
|
|
220
|
+
bucket = molecule._o2_blocked_cache[spin]
|
|
221
|
+
key = (A_sorted, B_sorted)
|
|
222
|
+
hit = bucket.get(key)
|
|
223
|
+
if hit is not None:
|
|
224
|
+
return hit
|
|
225
|
+
S = _build_overlap(molecule, A_sorted.lower(), B_sorted.lower())
|
|
226
|
+
n = S.rows
|
|
227
|
+
entry = {
|
|
228
|
+
'det': _safe_det(S),
|
|
229
|
+
'gamma': _one_row_cofactor_table(S) if n >= 1 else None,
|
|
230
|
+
'Gamma': _two_row_cofactor_dict(S) if n >= 2 else None,
|
|
231
|
+
}
|
|
232
|
+
bucket[key] = entry
|
|
233
|
+
return entry
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
# --- helpers -------------------------------------------------------------
|
|
237
|
+
|
|
238
|
+
def _spin_block_parity(spins):
|
|
239
|
+
"""Sign of the permutation that brings a spin pattern (string of '+' for
|
|
240
|
+
alpha and '-' for beta) into canonical (all '+' first, then all '-')
|
|
241
|
+
order. Equals (-1)^(number of beta-before-alpha inversions)."""
|
|
242
|
+
inv = 0
|
|
243
|
+
n = len(spins)
|
|
244
|
+
for i in range(n):
|
|
245
|
+
if spins[i] == '-':
|
|
246
|
+
for j in range(i + 1, n):
|
|
247
|
+
if spins[j] == '+':
|
|
248
|
+
inv += 1
|
|
249
|
+
return 1 if inv % 2 == 0 else -1
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _build_overlap(molecule, A_orbs, B_orbs):
|
|
253
|
+
"""Per-spin-block overlap matrix of pairwise AO overlaps. Both lists
|
|
254
|
+
must be lowercase spatial-orbital labels of equal length."""
|
|
255
|
+
n = len(A_orbs)
|
|
256
|
+
if n == 0:
|
|
257
|
+
return sp.zeros(0, 0)
|
|
258
|
+
return sp.Matrix(n, n, lambda i, j: molecule.get_o1_expr(A_orbs[i], B_orbs[j], 'S'))
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _safe_det(M):
|
|
262
|
+
"""det() that returns 1 for the empty (0x0) matrix without invoking
|
|
263
|
+
sympy.det on it (which can be quirky on degenerate sizes)."""
|
|
264
|
+
if M.rows == 0:
|
|
265
|
+
return _SP_ONE
|
|
266
|
+
return M.det()
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _one_row_cofactor_table(S):
|
|
270
|
+
"""gamma[i][j] = (-1)^{i+j} det(S with row i, col j removed)."""
|
|
271
|
+
n = S.rows
|
|
272
|
+
table = [[None] * n for _ in range(n)]
|
|
273
|
+
for i in range(n):
|
|
274
|
+
for j in range(n):
|
|
275
|
+
sign = 1 if (i + j) % 2 == 0 else -1
|
|
276
|
+
table[i][j] = sign * _minor(S, [i], [j])
|
|
277
|
+
return table
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _two_row_cofactor_dict(S):
|
|
281
|
+
"""Gamma[(i,k,j,l)] = (-1)^{i+k+j+l} det(S with rows i,k, cols j,l removed)
|
|
282
|
+
for i<k, j<l. Keys are quadruples of block-internal indices."""
|
|
283
|
+
n = S.rows
|
|
284
|
+
table = {}
|
|
285
|
+
for i in range(n):
|
|
286
|
+
for k in range(i + 1, n):
|
|
287
|
+
for j in range(n):
|
|
288
|
+
for l in range(j + 1, n):
|
|
289
|
+
sign = 1 if (i + k + j + l) % 2 == 0 else -1
|
|
290
|
+
table[(i, k, j, l)] = sign * _minor(S, [i, k], [j, l])
|
|
291
|
+
return table
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _minor(S, rows_to_remove, cols_to_remove):
|
|
295
|
+
"""Determinant of S with the given rows and columns deleted. Returns 1
|
|
296
|
+
for the resulting empty matrix."""
|
|
297
|
+
rows_keep = [r for r in range(S.rows) if r not in rows_to_remove]
|
|
298
|
+
cols_keep = [c for c in range(S.cols) if c not in cols_to_remove]
|
|
299
|
+
if not rows_keep:
|
|
300
|
+
return _SP_ONE
|
|
301
|
+
return S.extract(rows_keep, cols_keep).det()
|
symvb/_o2_symmetric.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Symmetry-projected two-electron matrix construction.
|
|
3
|
+
|
|
4
|
+
Build H_2 directly in the totally-symmetric (orbit-sum) SALC basis, without
|
|
5
|
+
materialising the full determinant-basis matrix first. The infrastructure
|
|
6
|
+
in symvb.symmetry (apply_orbital_permutation, generate_group,
|
|
7
|
+
totally_symmetric_basis) supplies the orbit decomposition; this module
|
|
8
|
+
contracts the matrix elements using G-invariance of H_2 to skip redundant
|
|
9
|
+
det-pair calls.
|
|
10
|
+
|
|
11
|
+
Algorithm
|
|
12
|
+
---------
|
|
13
|
+
A totally-symmetric SALC over orbit `orb_a` of size `s_a` is the uniform sum
|
|
14
|
+
|
|
15
|
+
psi_a = (1/sqrt(s_a)) * sum_{i in orb_a} D_i
|
|
16
|
+
|
|
17
|
+
Because H_2 commutes with every group element (a precondition the user
|
|
18
|
+
asserts implicitly through their `subst` and `subst_2e` dictionaries
|
|
19
|
+
grouping equivalent integrals), the matrix element between SALCs
|
|
20
|
+
|
|
21
|
+
<psi_a | H_2 | psi_b> = (1/sqrt(s_a s_b)) * sum_{i,j} <D_i | H_2 | D_j>
|
|
22
|
+
|
|
23
|
+
reduces via group invariance to a single sum:
|
|
24
|
+
|
|
25
|
+
<psi_a | H_2 | psi_b> = sqrt(s_a / s_b) * sum_{m in orb_b} <D_seed_a | H_2 | D_m>
|
|
26
|
+
|
|
27
|
+
where seed_a is any representative of orb_a. The double sum collapses from
|
|
28
|
+
s_a * s_b det-pair calls to s_b (or s_a if we pick the other side as seed).
|
|
29
|
+
|
|
30
|
+
For benzene's A_{1g} block (38 orbits, 400 dets), this drops the work from
|
|
31
|
+
~80k det-pair calls (full upper-triangle build) to ~8k (single-orbit-side
|
|
32
|
+
sums per SALC pair).
|
|
33
|
+
|
|
34
|
+
Preconditions
|
|
35
|
+
-------------
|
|
36
|
+
1. The user's subst dictionaries must make H_2 commute with every generator
|
|
37
|
+
in the supplied group. For uniformly-substituted PPP-type Hamiltonians
|
|
38
|
+
on regular topologies (benzene ring, allyl chain), this is automatic.
|
|
39
|
+
If the user uses inequivalent integrals on equivalent atoms, the
|
|
40
|
+
resulting block matrix will silently differ from the U^T H_full U form.
|
|
41
|
+
|
|
42
|
+
2. `orbits` must come from `symvb.symmetry.totally_symmetric_basis(generators, N)`
|
|
43
|
+
on the same `full_basis` length. Signed orbit sums (from
|
|
44
|
+
`signed_totally_symmetric_basis`) are not supported in this routine
|
|
45
|
+
yet -- the formula above assumes uniform +1 weights.
|
|
46
|
+
|
|
47
|
+
Validation against the full build is done by
|
|
48
|
+
test_o2_symmetric.TestSymmetricO2Agreement on small systems where both
|
|
49
|
+
paths are tractable: it constructs the full o2_matrix, projects via
|
|
50
|
+
U^T H U using the unsigned projector, and compares element-wise against
|
|
51
|
+
the direct symmetric build.
|
|
52
|
+
"""
|
|
53
|
+
from __future__ import annotations
|
|
54
|
+
|
|
55
|
+
import sympy as sp
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def o2_matrix_symmetric(molecule, full_basis, orbits):
|
|
59
|
+
"""Build the o2 matrix in the totally-symmetric (orbit-sum) SALC basis.
|
|
60
|
+
|
|
61
|
+
Parameters
|
|
62
|
+
----------
|
|
63
|
+
molecule : symvb.Molecule
|
|
64
|
+
Provides o2_det / o2 (works in either 'direct' or 'blocked' o2_method).
|
|
65
|
+
full_basis : list of FixedPsi
|
|
66
|
+
The full Sz-sector basis, typically `m.basis` after
|
|
67
|
+
`m.generate_basis(Na, Nb, Norbs)`.
|
|
68
|
+
orbits : list of list of int
|
|
69
|
+
Orbit decomposition of full_basis under the symmetry group.
|
|
70
|
+
Output of `symvb.symmetry.totally_symmetric_basis(generators, N)[1]`.
|
|
71
|
+
|
|
72
|
+
Returns
|
|
73
|
+
-------
|
|
74
|
+
H : sympy.Matrix of shape (k, k) with k = len(orbits).
|
|
75
|
+
Element [a, b] equals <psi_a | H_2 | psi_b> where psi_a is the
|
|
76
|
+
unit-normalised orbit-sum SALC for orbit a. Equals U^T @ H_full @ U
|
|
77
|
+
with U from totally_symmetric_basis.
|
|
78
|
+
|
|
79
|
+
Notes
|
|
80
|
+
-----
|
|
81
|
+
The symmetric upper triangle is computed and mirrored. Each off-diagonal
|
|
82
|
+
pair costs min(|orb_a|, |orb_b|) det-pair calls; the diagonal costs
|
|
83
|
+
|orb_a|. For benzene A_{1g} (38 orbits, mean orbit size ~10): ~8k
|
|
84
|
+
det-pair calls total versus ~80k for the full upper-triangle basis
|
|
85
|
+
build.
|
|
86
|
+
"""
|
|
87
|
+
k = len(orbits)
|
|
88
|
+
H = sp.zeros(k)
|
|
89
|
+
sizes = [len(orb) for orb in orbits]
|
|
90
|
+
|
|
91
|
+
# Cache the seed SlaterDet for each orbit to avoid repeated FixedPsi -> det.
|
|
92
|
+
def _seed_det(idx):
|
|
93
|
+
fp = full_basis[orbits[idx][0]]
|
|
94
|
+
return fp.dets[0]
|
|
95
|
+
|
|
96
|
+
seeds = [_seed_det(a) for a in range(k)]
|
|
97
|
+
|
|
98
|
+
for a in range(k):
|
|
99
|
+
size_a = sizes[a]
|
|
100
|
+
for b in range(a, k):
|
|
101
|
+
size_b = sizes[b]
|
|
102
|
+
# Pick the orbit with fewer elements for the inner sum to
|
|
103
|
+
# minimise det-pair calls. Both choices yield the same value
|
|
104
|
+
# by Hermiticity; we pick the cheaper one.
|
|
105
|
+
if size_b <= size_a:
|
|
106
|
+
# Sum over orb_b, fix seed_a; factor sqrt(size_a / size_b).
|
|
107
|
+
seed = seeds[a]
|
|
108
|
+
inner = sp.S.Zero
|
|
109
|
+
for m in orbits[b]:
|
|
110
|
+
other = full_basis[m].dets[0]
|
|
111
|
+
inner = inner + molecule.o2_det(seed, other)
|
|
112
|
+
factor = sp.sqrt(sp.Rational(size_a, size_b))
|
|
113
|
+
else:
|
|
114
|
+
# Sum over orb_a, fix seed_b; factor sqrt(size_b / size_a).
|
|
115
|
+
seed = seeds[b]
|
|
116
|
+
inner = sp.S.Zero
|
|
117
|
+
for m in orbits[a]:
|
|
118
|
+
other = full_basis[m].dets[0]
|
|
119
|
+
inner = inner + molecule.o2_det(other, seed)
|
|
120
|
+
factor = sp.sqrt(sp.Rational(size_b, size_a))
|
|
121
|
+
H[a, b] = factor * inner
|
|
122
|
+
if a != b:
|
|
123
|
+
H[b, a] = H[a, b]
|
|
124
|
+
return H
|
symvb/data.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
hperm = {(1, 2, 3, 4): (1, 2, 3, 4),
|
|
2
|
+
(3, 2, 1, 4): (1, 2, 3, 4),
|
|
3
|
+
(1, 4, 3, 2): (1, 2, 3, 4),
|
|
4
|
+
(3, 4, 1, 2): (1, 2, 3, 4),
|
|
5
|
+
(2, 1, 4, 3): (1, 2, 3, 4),
|
|
6
|
+
(4, 1, 2, 3): (1, 2, 3, 4),
|
|
7
|
+
(2, 3, 4, 1): (1, 2, 3, 4),
|
|
8
|
+
(4, 3, 2, 1): (1, 2, 3, 4),
|
|
9
|
+
(1, 2, 4, 3): (1, 2, 4, 3),
|
|
10
|
+
(4, 2, 1, 3): (1, 2, 4, 3),
|
|
11
|
+
(1, 3, 4, 2): (1, 2, 4, 3),
|
|
12
|
+
(4, 3, 1, 2): (1, 2, 4, 3),
|
|
13
|
+
(2, 1, 3, 4): (1, 2, 4, 3),
|
|
14
|
+
(3, 1, 2, 4): (1, 2, 4, 3),
|
|
15
|
+
(2, 4, 3, 1): (1, 2, 4, 3),
|
|
16
|
+
(3, 4, 2, 1): (1, 2, 4, 3),
|
|
17
|
+
(1, 3, 2, 4): (1, 3, 2, 4),
|
|
18
|
+
(2, 3, 1, 4): (1, 3, 2, 4),
|
|
19
|
+
(1, 4, 2, 3): (1, 3, 2, 4),
|
|
20
|
+
(2, 4, 1, 3): (1, 3, 2, 4),
|
|
21
|
+
(3, 1, 4, 2): (1, 3, 2, 4),
|
|
22
|
+
(4, 1, 3, 2): (1, 3, 2, 4),
|
|
23
|
+
(3, 2, 4, 1): (1, 3, 2, 4),
|
|
24
|
+
(4, 2, 3, 1): (1, 3, 2, 4)}
|