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/molecule.py
ADDED
|
@@ -0,0 +1,736 @@
|
|
|
1
|
+
from functools import lru_cache
|
|
2
|
+
|
|
3
|
+
import numpy
|
|
4
|
+
import sympy as sp
|
|
5
|
+
from scipy.stats import rankdata
|
|
6
|
+
|
|
7
|
+
from symvb.functions import attempt_int, standardize_det, sort_ind, canonical_chemist_iv, simplify_matrix
|
|
8
|
+
from symvb.numerical import get_coupled
|
|
9
|
+
from symvb.fixed_psi import FixedPsi, generate_dets
|
|
10
|
+
from symvb.numerical import get_combined_from_dict
|
|
11
|
+
from symvb.slaterdet import SlaterDet
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@lru_cache(maxsize=None)
|
|
15
|
+
def _cached_symbol(name):
|
|
16
|
+
return sp.Symbol(name)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
_SP_ZERO = sp.Integer(0)
|
|
20
|
+
_SP_ONE = sp.Integer(1)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Molecule:
|
|
24
|
+
# Contains molecule-specific information
|
|
25
|
+
O2_METHODS = ('direct', 'blocked')
|
|
26
|
+
|
|
27
|
+
def __init__(self, symm_offdiagonal=True, normalized_basis_orbs=True,
|
|
28
|
+
interacting_orbs=None, subst=None, zero_ii=True,
|
|
29
|
+
subst_2e=None, max_2e_centers=4, o2_method='blocked',
|
|
30
|
+
orbitals=None):
|
|
31
|
+
"""
|
|
32
|
+
subst contains a list of substitutions to be made, eg ['S':('S_ab','S_bc','S_cd'),'H':('H_ab','H_bc')]
|
|
33
|
+
zero_ii=True sets all H_ii terms to zero
|
|
34
|
+
interacting_orbs is a list of two-letter lowercase strings, eg ['ab','bc','ad'].
|
|
35
|
+
Only these orbital pairs have non-zero integrals
|
|
36
|
+
symm_offdiagonal = True; symmetric matrix
|
|
37
|
+
normalized_basis_orbs = True; S_ii = 1
|
|
38
|
+
o2_method selects the two-electron implementation: 'blocked' (default,
|
|
39
|
+
spin-block-aware precompute, see symvb/_o2_blocked.py) or 'direct'
|
|
40
|
+
(Loewdin cofactor on the spin-mixed (N-2)-electron string). Both return
|
|
41
|
+
identical matrices; 'blocked' is markedly faster for larger bases.
|
|
42
|
+
orbitals: optional whitelist of allowed orbital labels, e.g. 'abcdef'
|
|
43
|
+
or {'a','b','c'}. When provided, any det string presented to Op/o2/
|
|
44
|
+
build_matrix that uses a label outside this set raises ValueError.
|
|
45
|
+
Also validates interacting_orbs entries at construction. Default None
|
|
46
|
+
keeps the duck-typed behavior (any character accepted).
|
|
47
|
+
"""
|
|
48
|
+
if o2_method not in self.O2_METHODS:
|
|
49
|
+
raise ValueError(
|
|
50
|
+
"o2_method must be one of %r, got %r" % (self.O2_METHODS, o2_method))
|
|
51
|
+
|
|
52
|
+
if orbitals is None:
|
|
53
|
+
self.orbitals = None
|
|
54
|
+
else:
|
|
55
|
+
chars = frozenset(c.lower() for c in orbitals)
|
|
56
|
+
for c in chars:
|
|
57
|
+
if not c.isalpha() or len(c) != 1:
|
|
58
|
+
raise ValueError(
|
|
59
|
+
"orbitals must contain single alphabetic characters; got %r" % c)
|
|
60
|
+
self.orbitals = chars
|
|
61
|
+
if interacting_orbs is not None:
|
|
62
|
+
for pair in interacting_orbs:
|
|
63
|
+
bad = [c for c in pair if c.lower() not in chars]
|
|
64
|
+
if bad:
|
|
65
|
+
raise ValueError(
|
|
66
|
+
"interacting_orbs entry %r references orbital(s) %r "
|
|
67
|
+
"not in whitelist %r" % (pair, bad, sorted(chars)))
|
|
68
|
+
|
|
69
|
+
self.symm_offdiagonal = symm_offdiagonal
|
|
70
|
+
self.normalized_basis_orbs = normalized_basis_orbs
|
|
71
|
+
self.interacting_orbs = interacting_orbs # list of two-letter lowercase strings, eg ['ab','bc','ad']
|
|
72
|
+
|
|
73
|
+
self.subst = {}
|
|
74
|
+
self.subst_2e = {}
|
|
75
|
+
|
|
76
|
+
self.basis = None
|
|
77
|
+
self.basis_a, self.basis_b = None, None
|
|
78
|
+
self.aH, self.aS = None, None
|
|
79
|
+
self.bH, self.bS = None, None
|
|
80
|
+
self.lookup_a, self.lookup_b = {}, {}
|
|
81
|
+
self.precalculated_half_dets = False
|
|
82
|
+
|
|
83
|
+
if subst is None:
|
|
84
|
+
subst = {}
|
|
85
|
+
self.parse_subst(subst)
|
|
86
|
+
|
|
87
|
+
if subst_2e is None:
|
|
88
|
+
subst_2e = {}
|
|
89
|
+
self.parse_subst_2e(subst_2e)
|
|
90
|
+
|
|
91
|
+
self.zero_ii = zero_ii
|
|
92
|
+
self.max_2e_centers = max_2e_centers
|
|
93
|
+
self.o2_method = o2_method
|
|
94
|
+
|
|
95
|
+
self._o1_expr_cache = {}
|
|
96
|
+
self._o2_expr_cache = {}
|
|
97
|
+
|
|
98
|
+
# ------------------------------------------------------------------
|
|
99
|
+
# topology constructors (convenience): build the interacting_orbs and
|
|
100
|
+
# subst/subst_2e dicts for the common ring and chain models, so callers
|
|
101
|
+
# do not spell out every edge by hand.
|
|
102
|
+
# ------------------------------------------------------------------
|
|
103
|
+
@staticmethod
|
|
104
|
+
def _topology_orbitals(L):
|
|
105
|
+
if L > 26:
|
|
106
|
+
raise ValueError("ring/chain helpers support up to 26 orbitals (a-z)")
|
|
107
|
+
return [chr(ord('a') + i) for i in range(L)]
|
|
108
|
+
|
|
109
|
+
@classmethod
|
|
110
|
+
def _from_edges(cls, edges, h, s, U, hubbard, zero_ii, **kw):
|
|
111
|
+
seen = [] # dedupe (e.g. a 2-ring has one edge twice)
|
|
112
|
+
for e in edges:
|
|
113
|
+
if e not in seen:
|
|
114
|
+
seen.append(e)
|
|
115
|
+
edges = seen
|
|
116
|
+
subst = {h: tuple('H_' + e for e in edges),
|
|
117
|
+
s: tuple('S_' + e for e in edges)}
|
|
118
|
+
kwargs = dict(interacting_orbs=edges, subst=subst, zero_ii=zero_ii)
|
|
119
|
+
if hubbard:
|
|
120
|
+
kwargs['subst_2e'] = {U: ('1111',)}
|
|
121
|
+
kwargs['max_2e_centers'] = 1
|
|
122
|
+
kwargs.update(kw)
|
|
123
|
+
return cls(**kwargs)
|
|
124
|
+
|
|
125
|
+
@classmethod
|
|
126
|
+
def ring(cls, L, h='h', s='s', U='U', hubbard=True, zero_ii=True, **kw):
|
|
127
|
+
"""Cyclic ``L``-orbital ring (orbitals ``a, b, c, ...``).
|
|
128
|
+
|
|
129
|
+
Nearest-neighbour resonance ``h`` and overlap ``s`` on every edge, and
|
|
130
|
+
on-site Hubbard ``U`` when ``hubbard`` (the default). Returns a
|
|
131
|
+
configured ``Molecule``; extra keywords pass through to ``__init__``.
|
|
132
|
+
"""
|
|
133
|
+
orbs = cls._topology_orbitals(L)
|
|
134
|
+
edges = [''.join(sorted((orbs[i], orbs[(i + 1) % L]))) for i in range(L)]
|
|
135
|
+
return cls._from_edges(edges, h, s, U, hubbard, zero_ii, **kw)
|
|
136
|
+
|
|
137
|
+
@classmethod
|
|
138
|
+
def chain(cls, n, h='h', s='s', U='U', hubbard=True, zero_ii=True, **kw):
|
|
139
|
+
"""Linear ``n``-orbital chain; same conventions as :meth:`ring` without
|
|
140
|
+
the wrap-around edge."""
|
|
141
|
+
orbs = cls._topology_orbitals(n)
|
|
142
|
+
edges = [''.join(sorted((orbs[i], orbs[i + 1]))) for i in range(n - 1)]
|
|
143
|
+
return cls._from_edges(edges, h, s, U, hubbard, zero_ii, **kw)
|
|
144
|
+
|
|
145
|
+
def generate_basis(self, Na, Nb, Norbs):
|
|
146
|
+
|
|
147
|
+
self.precalculated_half_dets = False
|
|
148
|
+
|
|
149
|
+
self.basis = generate_dets(Na, Nb, Norbs)
|
|
150
|
+
|
|
151
|
+
self.basis_a = generate_dets(Na, 0, Norbs)
|
|
152
|
+
for i in range(len(self.basis_a)):
|
|
153
|
+
self.lookup_a[self.basis_a[i].dets[0].det_string] = i
|
|
154
|
+
|
|
155
|
+
if Na == Nb:
|
|
156
|
+
self.basis_b, self.lookup_b = self.basis_a, self.lookup_a
|
|
157
|
+
else:
|
|
158
|
+
self.basis_b = generate_dets(Nb, 0, Norbs) # all lookups will be by lower case
|
|
159
|
+
for i in range(len(self.basis_b)):
|
|
160
|
+
self.lookup_b[self.basis_b[i].dets[0].det_string] = i
|
|
161
|
+
|
|
162
|
+
self.aH = self.build_matrix(self.basis_a, op='H')
|
|
163
|
+
self.aS = self.build_matrix(self.basis_a, op='S')
|
|
164
|
+
if Na == Nb:
|
|
165
|
+
self.bH, self.bS = self.aH, self.aS
|
|
166
|
+
else:
|
|
167
|
+
self.bH = self.build_matrix(self.basis_b, op='H')
|
|
168
|
+
self.bS = self.build_matrix(self.basis_b, op='S')
|
|
169
|
+
self.precalculated_half_dets = True
|
|
170
|
+
|
|
171
|
+
def parse_subst(self, subst):
|
|
172
|
+
for k, v in subst.items():
|
|
173
|
+
if isinstance(v, str):
|
|
174
|
+
self.subst[v] = k
|
|
175
|
+
else:
|
|
176
|
+
for s in v:
|
|
177
|
+
self.subst[s] = k
|
|
178
|
+
|
|
179
|
+
def parse_subst_2e(self, subst_2e):
|
|
180
|
+
for k, v in subst_2e.items():
|
|
181
|
+
if isinstance(v, str):
|
|
182
|
+
self.subst_2e[v] = k
|
|
183
|
+
else:
|
|
184
|
+
for s in v:
|
|
185
|
+
self.subst_2e[s] = k
|
|
186
|
+
|
|
187
|
+
def get_o1_name(self, a, b, o):
|
|
188
|
+
# sort two orbital indices in the alphabetic order
|
|
189
|
+
if self.symm_offdiagonal and (a > b):
|
|
190
|
+
a, b = b, a
|
|
191
|
+
|
|
192
|
+
# If only certain orbitals are allowed to interact,
|
|
193
|
+
# check if the orbital pair is in the allowed list
|
|
194
|
+
if self.interacting_orbs is not None and (a != b):
|
|
195
|
+
if not (a + b) in self.interacting_orbs:
|
|
196
|
+
return '0' # non-interacting orbitals will always give 0 in any term of the direct product
|
|
197
|
+
|
|
198
|
+
# Replace terms S_xx by 1 if allowed
|
|
199
|
+
if self.normalized_basis_orbs and (a == b) and (o == 'S'):
|
|
200
|
+
return '1'
|
|
201
|
+
|
|
202
|
+
# replace the site energies H_ii by zero if allowed
|
|
203
|
+
if self.zero_ii and (a == b) and (o == 'H'):
|
|
204
|
+
return '0'
|
|
205
|
+
|
|
206
|
+
s = '%s_%s%s' % (o, a, b)
|
|
207
|
+
|
|
208
|
+
# substitute certain AO matrix elements if needed
|
|
209
|
+
if s in self.subst:
|
|
210
|
+
s = self.subst[s]
|
|
211
|
+
|
|
212
|
+
return s
|
|
213
|
+
|
|
214
|
+
def get_o1_expr(self, a, b, o):
|
|
215
|
+
# Cached sympy expression for a one-electron AO matrix element
|
|
216
|
+
key = (a, b, o)
|
|
217
|
+
cached = self._o1_expr_cache.get(key)
|
|
218
|
+
if cached is not None:
|
|
219
|
+
return cached
|
|
220
|
+
name = self.get_o1_name(a, b, o)
|
|
221
|
+
if name == '0':
|
|
222
|
+
expr = _SP_ZERO
|
|
223
|
+
elif name == '1':
|
|
224
|
+
expr = _SP_ONE
|
|
225
|
+
else:
|
|
226
|
+
expr = _cached_symbol(name)
|
|
227
|
+
self._o1_expr_cache[key] = expr
|
|
228
|
+
return expr
|
|
229
|
+
|
|
230
|
+
def Op_Hartree_product(self, L_orbs, R_orbs, op='H'):
|
|
231
|
+
# Computes a matrix element for two orbital products, e.g <A(1)b(2)...|O|A(1)b(2)...>.
|
|
232
|
+
# Returns a sympy expression.
|
|
233
|
+
|
|
234
|
+
nL = len(L_orbs)
|
|
235
|
+
nR = len(R_orbs)
|
|
236
|
+
|
|
237
|
+
if nL != nR:
|
|
238
|
+
return _SP_ZERO
|
|
239
|
+
|
|
240
|
+
if nL == 0:
|
|
241
|
+
return _SP_ONE if op == 'S' else _SP_ZERO
|
|
242
|
+
|
|
243
|
+
lL = L_orbs.lower()
|
|
244
|
+
lR = R_orbs.lower()
|
|
245
|
+
|
|
246
|
+
sum_terms = []
|
|
247
|
+
for i_op in range(nL):
|
|
248
|
+
prod_factors = []
|
|
249
|
+
term_zero = False
|
|
250
|
+
for j in range(nL):
|
|
251
|
+
o = op if i_op == j else 'S'
|
|
252
|
+
s = self.get_o1_expr(lL[j], lR[j], o)
|
|
253
|
+
if s is _SP_ZERO:
|
|
254
|
+
term_zero = True
|
|
255
|
+
break
|
|
256
|
+
if s is _SP_ONE:
|
|
257
|
+
continue
|
|
258
|
+
prod_factors.append(s)
|
|
259
|
+
|
|
260
|
+
if term_zero:
|
|
261
|
+
elem = _SP_ZERO
|
|
262
|
+
elif not prod_factors:
|
|
263
|
+
elem = _SP_ONE
|
|
264
|
+
elif len(prod_factors) == 1:
|
|
265
|
+
elem = prod_factors[0]
|
|
266
|
+
else:
|
|
267
|
+
elem = sp.Mul(*prod_factors)
|
|
268
|
+
|
|
269
|
+
if op == 'S':
|
|
270
|
+
# All Hartree products in <L|S|R> are identical; one is enough.
|
|
271
|
+
return elem
|
|
272
|
+
|
|
273
|
+
if elem is not _SP_ZERO:
|
|
274
|
+
sum_terms.append(elem)
|
|
275
|
+
|
|
276
|
+
if not sum_terms:
|
|
277
|
+
return _SP_ZERO
|
|
278
|
+
if len(sum_terms) == 1:
|
|
279
|
+
return sum_terms[0]
|
|
280
|
+
return sp.Add(*sum_terms)
|
|
281
|
+
|
|
282
|
+
op_orbprod = Op_Hartree_product
|
|
283
|
+
|
|
284
|
+
def op_det(self, L, R, op='H'):
|
|
285
|
+
# Returns the matrix element < L | O | R > as a sympy expression.
|
|
286
|
+
# L, R are instances of SlaterDet
|
|
287
|
+
|
|
288
|
+
if not R.is_compatible(L):
|
|
289
|
+
return _SP_ZERO
|
|
290
|
+
|
|
291
|
+
if self.precalculated_half_dets and op in ('H', 'S'):
|
|
292
|
+
# Fast path is only valid when both dets have the same alpha / beta
|
|
293
|
+
# electron counts as the precomputed basis. Two-electron operator
|
|
294
|
+
# construction synthesises sub-determinants with fewer electrons,
|
|
295
|
+
# which don't live in the precomputed lookup tables -- fall through
|
|
296
|
+
# to the general Hartree-product expansion below.
|
|
297
|
+
iLa = self.lookup_a.get(L.alpha_string)
|
|
298
|
+
iRa = self.lookup_a.get(R.alpha_string)
|
|
299
|
+
iLb = self.lookup_b.get(L.beta_string.lower())
|
|
300
|
+
iRb = self.lookup_b.get(R.beta_string.lower())
|
|
301
|
+
if None not in (iLa, iRa, iLb, iRb):
|
|
302
|
+
if op == 'H':
|
|
303
|
+
return self.aH[iLa, iRa] * self.bS[iLb, iRb] + self.aS[iLa, iRa] * self.bH[iLb, iRb]
|
|
304
|
+
return self.aS[iLa, iRa] * self.bS[iLb, iRb]
|
|
305
|
+
|
|
306
|
+
R_orbs, R_signs = R.get_orbital_permutations()
|
|
307
|
+
terms = []
|
|
308
|
+
for R_orb, R_sign in zip(R_orbs, R_signs):
|
|
309
|
+
elem = self.op_orbprod(L.det_string, R_orb, op=op)
|
|
310
|
+
if elem is _SP_ZERO:
|
|
311
|
+
continue
|
|
312
|
+
terms.append(elem if R_sign == 1 else -elem)
|
|
313
|
+
|
|
314
|
+
if not terms:
|
|
315
|
+
return _SP_ZERO
|
|
316
|
+
if len(terms) == 1:
|
|
317
|
+
return terms[0]
|
|
318
|
+
return sp.Add(*terms)
|
|
319
|
+
|
|
320
|
+
def op_fixed_psi(self, L, R, op='H'):
|
|
321
|
+
if len(L) == 0:
|
|
322
|
+
return _SP_ONE if op == 'S' else _SP_ZERO
|
|
323
|
+
|
|
324
|
+
sum_terms = []
|
|
325
|
+
for detL, cL in L:
|
|
326
|
+
for detR, cR in R:
|
|
327
|
+
elem = self.op_det(detL, detR, op=op)
|
|
328
|
+
if elem is _SP_ZERO:
|
|
329
|
+
continue
|
|
330
|
+
prd = cL * cR
|
|
331
|
+
if prd == 1:
|
|
332
|
+
sum_terms.append(elem)
|
|
333
|
+
elif prd == -1:
|
|
334
|
+
sum_terms.append(-elem)
|
|
335
|
+
else:
|
|
336
|
+
sum_terms.append(prd * elem)
|
|
337
|
+
|
|
338
|
+
if not sum_terms:
|
|
339
|
+
return _SP_ZERO
|
|
340
|
+
if len(sum_terms) == 1:
|
|
341
|
+
return sum_terms[0]
|
|
342
|
+
return sp.Add(*sum_terms)
|
|
343
|
+
|
|
344
|
+
def _check_orbitals_in_psi(self, psi):
|
|
345
|
+
if self.orbitals is None:
|
|
346
|
+
return
|
|
347
|
+
allowed = self.orbitals
|
|
348
|
+
for det in psi.dets:
|
|
349
|
+
for c in det.det_string:
|
|
350
|
+
if c.lower() not in allowed:
|
|
351
|
+
raise ValueError(
|
|
352
|
+
"det %r uses orbital %r outside whitelist %r"
|
|
353
|
+
% (det.det_string, c, sorted(allowed)))
|
|
354
|
+
|
|
355
|
+
def Op(self, L, R, op='H'):
|
|
356
|
+
L = FixedPsi(L)
|
|
357
|
+
R = FixedPsi(R)
|
|
358
|
+
self._check_orbitals_in_psi(L)
|
|
359
|
+
self._check_orbitals_in_psi(R)
|
|
360
|
+
return self.op_fixed_psi(L, R, op=op)
|
|
361
|
+
|
|
362
|
+
def Ops(self, L, R, op='H', find_factors=True):
|
|
363
|
+
z = self.Op(L=L, R=R, op=op)
|
|
364
|
+
if find_factors:
|
|
365
|
+
z = sp.factor(z)
|
|
366
|
+
return z
|
|
367
|
+
|
|
368
|
+
def getS(self, L, R, find_factors=True):
|
|
369
|
+
return self.Ops(L, R, op='S', find_factors=find_factors)
|
|
370
|
+
|
|
371
|
+
def getH(self, L, R, find_factors=True):
|
|
372
|
+
return self.Ops(L, R, op='H', find_factors=find_factors)
|
|
373
|
+
|
|
374
|
+
def build_matrix(self, u, op='H'):
|
|
375
|
+
"""
|
|
376
|
+
Builds a square matrix of integrals for each pair of wavefunctions in a given array
|
|
377
|
+
:param u: array of FixedPsi, SlaterDet, or str
|
|
378
|
+
:param op: the integration operator
|
|
379
|
+
:return: SymPy matrix with integrals
|
|
380
|
+
"""
|
|
381
|
+
N = len(u)
|
|
382
|
+
m = sp.zeros(N)
|
|
383
|
+
if N == 0:
|
|
384
|
+
return m
|
|
385
|
+
|
|
386
|
+
# Coerce each entry to a FixedPsi for uniform handling, standardize it
|
|
387
|
+
# into canonical creation order (so the spin-pattern matching below is
|
|
388
|
+
# correct for hand-built structures written in a natural, non-canonical
|
|
389
|
+
# order), and pre-compute the set of spin patterns it carries. Two
|
|
390
|
+
# FixedPsi are guaranteed to yield zero whenever their spin-pattern sets
|
|
391
|
+
# are disjoint -- skip those pairs entirely instead of running the full
|
|
392
|
+
# Op machinery. Standardizing is a no-op on an already-canonical basis
|
|
393
|
+
# (e.g. generate_dets output), so this does not change existing results;
|
|
394
|
+
# it only repairs the silently-dropped couplings between structures
|
|
395
|
+
# whose raw spin patterns differ (e.g. an alpha-alpha-beta-beta long
|
|
396
|
+
# bond from coupled_pairs against an interleaved Kekule structure).
|
|
397
|
+
psis = [None] * N
|
|
398
|
+
spin_keys = [None] * N
|
|
399
|
+
for i, entry in enumerate(u):
|
|
400
|
+
fp = FixedPsi(entry) # always a fresh copy; never mutate the caller's object
|
|
401
|
+
fp.standardize()
|
|
402
|
+
self._check_orbitals_in_psi(fp)
|
|
403
|
+
psis[i] = fp
|
|
404
|
+
spin_keys[i] = frozenset(d.spins for d in fp.dets)
|
|
405
|
+
|
|
406
|
+
# Inline fast-path: when half-dets are precomputed and every basis entry
|
|
407
|
+
# is a single Slater determinant with unit coefficient, skip the
|
|
408
|
+
# FixedPsi/Op layer and index aH/aS/bH/bS directly.
|
|
409
|
+
fast = (self.precalculated_half_dets and op in ('H', 'S')
|
|
410
|
+
and all(len(p.dets) == 1 and p.coefs[0] == 1 for p in psis))
|
|
411
|
+
|
|
412
|
+
if fast:
|
|
413
|
+
half = [None] * N
|
|
414
|
+
for i, p in enumerate(psis):
|
|
415
|
+
d = p.dets[0]
|
|
416
|
+
half[i] = (self.lookup_a[d.alpha_string],
|
|
417
|
+
self.lookup_b[d.beta_string.lower()],
|
|
418
|
+
d.spins)
|
|
419
|
+
aH, aS, bH, bS = self.aH, self.aS, self.bH, self.bS
|
|
420
|
+
for i in range(N):
|
|
421
|
+
iLa, iLb, sL = half[i]
|
|
422
|
+
for j in range(i, N):
|
|
423
|
+
iRa, iRb, sR = half[j]
|
|
424
|
+
if sL != sR:
|
|
425
|
+
continue
|
|
426
|
+
if op == 'H':
|
|
427
|
+
v = aH[iLa, iRa] * bS[iLb, iRb] + aS[iLa, iRa] * bH[iLb, iRb]
|
|
428
|
+
else:
|
|
429
|
+
v = aS[iLa, iRa] * bS[iLb, iRb]
|
|
430
|
+
m[i, j] = v
|
|
431
|
+
if i != j:
|
|
432
|
+
m[j, i] = v
|
|
433
|
+
return m
|
|
434
|
+
|
|
435
|
+
for i in range(N):
|
|
436
|
+
ki = spin_keys[i]
|
|
437
|
+
for j in range(i, N):
|
|
438
|
+
if ki.isdisjoint(spin_keys[j]):
|
|
439
|
+
continue
|
|
440
|
+
v = self.op_fixed_psi(psis[i], psis[j], op=op)
|
|
441
|
+
m[i, j] = v
|
|
442
|
+
if i != j:
|
|
443
|
+
m[j, i] = v
|
|
444
|
+
return m
|
|
445
|
+
|
|
446
|
+
def energy(self, P, o2=False):
|
|
447
|
+
"""
|
|
448
|
+
Find the energy for the FixedPsi object: E = <P|H|P> / <P|P>
|
|
449
|
+
:param P: A wavefunction: FixedPsi, SlaterDet, or str
|
|
450
|
+
:return: Expression for the normalized energy: N_el * <P | H | P> / <P | P>
|
|
451
|
+
"""
|
|
452
|
+
E = self.Ops(P, P, op='H')
|
|
453
|
+
S = self.Ops(P, P, op='S')
|
|
454
|
+
if o2:
|
|
455
|
+
return (E / S) + sp.simplify(sp.simplify(self.o2_fixed_psi(P, P)) / S)
|
|
456
|
+
else:
|
|
457
|
+
return E / S
|
|
458
|
+
|
|
459
|
+
def couple(self, P=None, mS=None, mH=None, N_tries=10, precision=12, ranges={'h':(-1.0,0.0),'s':(0.0,1.0)}, nums=None):
|
|
460
|
+
"""
|
|
461
|
+
Group the FixedPsi objects that have constant ratios in the lowest energy wave vector
|
|
462
|
+
The constant ratios are found by numerical simulation
|
|
463
|
+
:param P: list of FixedPsi objects
|
|
464
|
+
:param N_tries: number of trials
|
|
465
|
+
:param precision: 10^-precision is the matching threshold
|
|
466
|
+
:return:
|
|
467
|
+
"""
|
|
468
|
+
if mS is None:
|
|
469
|
+
mS = self.build_matrix(P, op='S')
|
|
470
|
+
if mH is None:
|
|
471
|
+
mH = self.build_matrix(P, op='H')
|
|
472
|
+
|
|
473
|
+
ranges2 = {}
|
|
474
|
+
symbols = mH.free_symbols.union(mS.free_symbols)
|
|
475
|
+
for s in symbols:
|
|
476
|
+
ss = str(s)
|
|
477
|
+
if ss in ranges:
|
|
478
|
+
ranges2[s] = ranges[ss]
|
|
479
|
+
else:
|
|
480
|
+
assert nums is not None and ss in nums, "Missing numerical value for the parameter " + ss
|
|
481
|
+
ranges2[s] = (nums[ss], nums[ss])
|
|
482
|
+
|
|
483
|
+
couplings = get_coupled(mS=mS, mH=mH, N_tries=N_tries, precision=precision, ranges=ranges2)
|
|
484
|
+
return get_combined_from_dict(P, couplings)
|
|
485
|
+
|
|
486
|
+
def get_o2_name(self, v):
|
|
487
|
+
"""
|
|
488
|
+
Gets the standardized name of the 2e integral. Uses integral symmetries to sort indices.
|
|
489
|
+
Substitutes the integral by name if provided
|
|
490
|
+
Parameters
|
|
491
|
+
----------
|
|
492
|
+
v: list of one-letter lower-case orbital names
|
|
493
|
+
|
|
494
|
+
Returns
|
|
495
|
+
-------
|
|
496
|
+
string with the integral name
|
|
497
|
+
"""
|
|
498
|
+
if len(numpy.unique((v[0].lower(), v[1].lower(), v[2].lower(), v[3].lower()))) > self.max_2e_centers:
|
|
499
|
+
return '0'
|
|
500
|
+
# Canonical form under the chemist 8-fold permutation symmetry of
|
|
501
|
+
# (ij|kl). Unlike sort_ind, this works correctly when AO labels
|
|
502
|
+
# repeat: (a,a,b,c) and (a,a,c,b) both canonicalise to (a,a,b,c),
|
|
503
|
+
# so they receive the same default T_ name and the same dense-rank
|
|
504
|
+
# subst_2e key. Necessary for symbolic outputs to be uniquely-named
|
|
505
|
+
# at max_2e_centers >= 3.
|
|
506
|
+
lowered = (v[0].lower(), v[1].lower(), v[2].lower(), v[3].lower())
|
|
507
|
+
tiv = canonical_chemist_iv(lowered)
|
|
508
|
+
indices = '%s%s%s%s' % tiv
|
|
509
|
+
int_name = 'T_%s' % indices
|
|
510
|
+
|
|
511
|
+
if self.subst_2e is not None:
|
|
512
|
+
r = '%s%s%s%s' % tuple(rankdata(tiv, method='dense'))
|
|
513
|
+
if r in self.subst_2e:
|
|
514
|
+
int_name = self.subst_2e[r]
|
|
515
|
+
return int_name
|
|
516
|
+
|
|
517
|
+
def get_o2_expr(self, v):
|
|
518
|
+
"""Cached sympy expression for the two-electron integral over v (4 orbitals).
|
|
519
|
+
|
|
520
|
+
Memoised on the lowercased AO 4-tuple (the only data get_o2_name
|
|
521
|
+
actually uses). After the first call for a given (p,q,r,s), all
|
|
522
|
+
subsequent calls are dict lookups, bypassing the rankdata/sort_ind
|
|
523
|
+
canonicalisation. Benefits both 'direct' and 'blocked' o2 paths
|
|
524
|
+
when the same integral pattern recurs many times across the
|
|
525
|
+
basis (e.g. benzene PPP: 6^4 = 1296 distinct entries vs. ~13M
|
|
526
|
+
full-pair contraction queries)."""
|
|
527
|
+
if isinstance(v, tuple):
|
|
528
|
+
key = v
|
|
529
|
+
else:
|
|
530
|
+
key = tuple(v)
|
|
531
|
+
cached = self._o2_expr_cache.get(key)
|
|
532
|
+
if cached is not None:
|
|
533
|
+
return cached
|
|
534
|
+
name = self.get_o2_name(v)
|
|
535
|
+
if name == '0':
|
|
536
|
+
result = _SP_ZERO
|
|
537
|
+
else:
|
|
538
|
+
result = _cached_symbol(name)
|
|
539
|
+
self._o2_expr_cache[key] = result
|
|
540
|
+
return result
|
|
541
|
+
|
|
542
|
+
def o2_det(self, D1, D2):
|
|
543
|
+
"""
|
|
544
|
+
Two-electron matrix element <D1|1/r_12|D2> between two SlaterDets.
|
|
545
|
+
Returns a sympy expression. Uses Slater-Condon rules for arbitrary
|
|
546
|
+
spin-orbital occupations; evaluates in the (possibly non-orthogonal)
|
|
547
|
+
AO basis by computing overlap cofactors for the (N-2)-electron
|
|
548
|
+
sub-determinants via Op(op='S').
|
|
549
|
+
|
|
550
|
+
Dispatches on self.o2_method ('direct' or 'blocked'). The 'direct'
|
|
551
|
+
path is the historical implementation. The 'blocked' path is the
|
|
552
|
+
spin-block-aware reformulation (see symvb/_o2_blocked.py) — opt-in
|
|
553
|
+
until the agreement-test harness validates parity.
|
|
554
|
+
"""
|
|
555
|
+
if self.o2_method == 'blocked':
|
|
556
|
+
from symvb._o2_blocked import o2_det_blocked
|
|
557
|
+
return o2_det_blocked(self, D1, D2)
|
|
558
|
+
|
|
559
|
+
assert D1.Nel == D2.Nel, 'Different number of electrons'
|
|
560
|
+
Nel = D1.Nel
|
|
561
|
+
D1s = D1.det_string
|
|
562
|
+
D2s = D2.det_string
|
|
563
|
+
|
|
564
|
+
terms = []
|
|
565
|
+
for i in range(Nel):
|
|
566
|
+
for j in range(i + 1, Nel):
|
|
567
|
+
s1 = D1s[:i] + D1s[i + 1:j] + D1s[j + 1:]
|
|
568
|
+
c1, c2 = D1s[i], D1s[j]
|
|
569
|
+
sumL = c1.islower() + c2.islower()
|
|
570
|
+
sd1, f1 = standardize_det(s1)
|
|
571
|
+
sd1_obj = SlaterDet(sd1)
|
|
572
|
+
|
|
573
|
+
for k in range(Nel):
|
|
574
|
+
for mm in range(k + 1, Nel):
|
|
575
|
+
s2 = D2s[:k] + D2s[k + 1:mm] + D2s[mm + 1:]
|
|
576
|
+
c3, c4 = D2s[k], D2s[mm]
|
|
577
|
+
|
|
578
|
+
if len(numpy.unique((c1.lower(), c2.lower(),
|
|
579
|
+
c3.lower(), c4.lower()))) > self.max_2e_centers:
|
|
580
|
+
continue
|
|
581
|
+
|
|
582
|
+
sumR = c3.islower() + c4.islower()
|
|
583
|
+
if sumL != sumR:
|
|
584
|
+
continue
|
|
585
|
+
|
|
586
|
+
sd2, f2 = standardize_det(s2)
|
|
587
|
+
sd2_obj = SlaterDet(sd2)
|
|
588
|
+
|
|
589
|
+
opS = self.op_det(sd1_obj, sd2_obj, op='S')
|
|
590
|
+
if opS is _SP_ZERO:
|
|
591
|
+
continue
|
|
592
|
+
|
|
593
|
+
parity = (i + j + k + mm + f1 + f2) % 2
|
|
594
|
+
|
|
595
|
+
if c1.islower() == c3.islower():
|
|
596
|
+
iv = (c1.lower(), c2.lower(), c3.lower(), c4.lower())
|
|
597
|
+
int_sym = self.get_o2_expr(iv)
|
|
598
|
+
if int_sym is not _SP_ZERO:
|
|
599
|
+
sign = 1 if parity == 0 else -1
|
|
600
|
+
terms.append(sign * int_sym * opS)
|
|
601
|
+
|
|
602
|
+
if c1.islower() == c4.islower():
|
|
603
|
+
iv = (c1.lower(), c2.lower(), c4.lower(), c3.lower())
|
|
604
|
+
int_sym = self.get_o2_expr(iv)
|
|
605
|
+
if int_sym is not _SP_ZERO:
|
|
606
|
+
sign = 1 if parity == 1 else -1
|
|
607
|
+
terms.append(sign * int_sym * opS)
|
|
608
|
+
|
|
609
|
+
if not terms:
|
|
610
|
+
return _SP_ZERO
|
|
611
|
+
if len(terms) == 1:
|
|
612
|
+
return terms[0]
|
|
613
|
+
return sp.Add(*terms)
|
|
614
|
+
|
|
615
|
+
def o2_fixed_psi(self, L, R, op='H'):
|
|
616
|
+
if len(L) == 0:
|
|
617
|
+
return _SP_ZERO
|
|
618
|
+
|
|
619
|
+
terms = []
|
|
620
|
+
for detL, cL in L:
|
|
621
|
+
for detR, cR in R:
|
|
622
|
+
elem = self.o2_det(detL, detR)
|
|
623
|
+
if elem is _SP_ZERO:
|
|
624
|
+
continue
|
|
625
|
+
prd = cL * cR
|
|
626
|
+
if prd == 1:
|
|
627
|
+
terms.append(elem)
|
|
628
|
+
elif prd == -1:
|
|
629
|
+
terms.append(-elem)
|
|
630
|
+
else:
|
|
631
|
+
terms.append(prd * elem)
|
|
632
|
+
if not terms:
|
|
633
|
+
return _SP_ZERO
|
|
634
|
+
if len(terms) == 1:
|
|
635
|
+
return terms[0]
|
|
636
|
+
return sp.Add(*terms)
|
|
637
|
+
|
|
638
|
+
def o2(self, L, R, op='H'):
|
|
639
|
+
L = FixedPsi(L)
|
|
640
|
+
L.standardize()
|
|
641
|
+
R = FixedPsi(R)
|
|
642
|
+
R.standardize()
|
|
643
|
+
self._check_orbitals_in_psi(L)
|
|
644
|
+
self._check_orbitals_in_psi(R)
|
|
645
|
+
return self.o2_fixed_psi(L, R)
|
|
646
|
+
|
|
647
|
+
def o2_matrix(self, u):
|
|
648
|
+
Nd = len(u)
|
|
649
|
+
o2 = sp.zeros(Nd)
|
|
650
|
+
if Nd == 0:
|
|
651
|
+
return o2
|
|
652
|
+
# Standardize each basis entry once into canonical creation order, so
|
|
653
|
+
# the two-electron block is built over exactly the same structures as
|
|
654
|
+
# the one-electron build_matrix (both apply the identical, deterministic
|
|
655
|
+
# standardization). A no-op on an already-canonical basis; on a
|
|
656
|
+
# hand-built non-canonical structure it folds the fermion reorder sign
|
|
657
|
+
# into the coefficient so H1 + H2 stays consistent.
|
|
658
|
+
cu = []
|
|
659
|
+
for entry in u:
|
|
660
|
+
fp = FixedPsi(entry)
|
|
661
|
+
fp.standardize()
|
|
662
|
+
self._check_orbitals_in_psi(fp)
|
|
663
|
+
cu.append(fp)
|
|
664
|
+
for i in range(Nd):
|
|
665
|
+
for j in range(i, Nd):
|
|
666
|
+
o2[i, j] = self.o2_fixed_psi(cu[i], cu[j])
|
|
667
|
+
if i != j:
|
|
668
|
+
o2[j, i] = o2[i, j]
|
|
669
|
+
return o2
|
|
670
|
+
|
|
671
|
+
def o2_mo2ao(self, c1, c2, c3, c4):
|
|
672
|
+
s = '0'
|
|
673
|
+
for i1, k1 in c1:
|
|
674
|
+
for i2, k2 in c2:
|
|
675
|
+
for i3, k3 in c3:
|
|
676
|
+
for i4, k4 in c4:
|
|
677
|
+
if i1.det_string.isupper() != i3.det_string.isupper():
|
|
678
|
+
continue
|
|
679
|
+
if i2.det_string.isupper() != i4.det_string.isupper():
|
|
680
|
+
continue
|
|
681
|
+
s += ' + ' + str(k1 * k2 * k3 * k4) + '*' + self.get_o2_name((i1.det_string.lower(),
|
|
682
|
+
i2.det_string.lower(),
|
|
683
|
+
i3.det_string.lower(),
|
|
684
|
+
i4.det_string.lower()))
|
|
685
|
+
return s
|
|
686
|
+
|
|
687
|
+
def get_mo_norm(self, mo):
|
|
688
|
+
# returns a diagonal matrix with the normalization factors on the diagonal
|
|
689
|
+
mo_norm = sp.zeros(len(mo))
|
|
690
|
+
for i in range(len(mo)):
|
|
691
|
+
mo_norm[i, i] = 1 / sp.sqrt(self.Ops(mo[i], mo[i], op='S'))
|
|
692
|
+
return mo_norm
|
|
693
|
+
|
|
694
|
+
def get_fock(self, mo, Nel):
|
|
695
|
+
# example of mo: [|a|+|b|, |A|+|B|, |a|-|b|, |A|-|B|]
|
|
696
|
+
# Construct the Fock matrix
|
|
697
|
+
Nmo = len(mo)
|
|
698
|
+
fock = sp.zeros(Nmo)
|
|
699
|
+
mo_norm = self.get_mo_norm(mo)
|
|
700
|
+
for i in range(Nmo):
|
|
701
|
+
for j in range(Nmo):
|
|
702
|
+
for b in range(Nel):
|
|
703
|
+
norm = mo_norm[i,i] * mo_norm[j,j] * mo_norm[b,b]**2
|
|
704
|
+
|
|
705
|
+
fock[i,j] += sp.simplify(self.o2_mo2ao(mo[i],mo[b],mo[j],mo[b])) * norm
|
|
706
|
+
fock[i,j] -= sp.simplify(self.o2_mo2ao(mo[i],mo[b],mo[b],mo[j])) * norm
|
|
707
|
+
|
|
708
|
+
fock[i,j] = sp.simplify(fock[i,j])
|
|
709
|
+
|
|
710
|
+
return fock
|
|
711
|
+
|
|
712
|
+
def get_rhf_fock(self, mo, Nel):
|
|
713
|
+
# example of mo: [|a|+|b|, |A|+|B|, |a|-|b|, |A|-|B|]
|
|
714
|
+
# Construct the Fock matrix
|
|
715
|
+
Nmo = len(mo)
|
|
716
|
+
fock = sp.zeros(Nmo)
|
|
717
|
+
mo_norm = self.get_mo_norm(mo)
|
|
718
|
+
for mu in range(Nmo):
|
|
719
|
+
for nu in range(Nmo):
|
|
720
|
+
for a in range(Nel // 2):
|
|
721
|
+
norm = mo_norm[mu, mu] * mo_norm[nu, nu] * mo_norm[a, a]**2
|
|
722
|
+
|
|
723
|
+
fock[mu, nu] += sp.simplify(self.o2_mo2ao(mo[mu], mo[a], mo[nu], mo[a])) * 2 * norm
|
|
724
|
+
fock[mu, nu] -= sp.simplify(self.o2_mo2ao(mo[mu], mo[a], mo[a], mo[nu])) * norm
|
|
725
|
+
|
|
726
|
+
fock[mu, nu] = sp.simplify(fock[mu, nu])
|
|
727
|
+
|
|
728
|
+
return fock
|
|
729
|
+
|
|
730
|
+
def get_rhf_mo_energies(self, mo_rhf, Nel):
|
|
731
|
+
mo_rhf_norm = self.get_mo_norm(mo_rhf)
|
|
732
|
+
rhf_o1 = simplify_matrix(mo_rhf_norm * self.build_matrix(mo_rhf, op='H') * mo_rhf_norm)
|
|
733
|
+
rhf_fock = self.get_rhf_fock(mo_rhf, Nel=Nel)
|
|
734
|
+
return (rhf_o1 + rhf_fock).diagonal()
|
|
735
|
+
|
|
736
|
+
|