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/fixed_psi.py
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import symvb
|
|
2
|
+
from symvb.functions import generate_det_strings, standardize_det, standardize_det_2
|
|
3
|
+
from symvb.functions import attempt_int
|
|
4
|
+
import copy
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class FixedPsi:
|
|
8
|
+
# Linear combination of determinants
|
|
9
|
+
def __init__(self, x=None, coupled_pairs=None):
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
"""
|
|
13
|
+
self.Nel = 0
|
|
14
|
+
|
|
15
|
+
self.dets = []
|
|
16
|
+
self.coefs = []
|
|
17
|
+
|
|
18
|
+
if x is None:
|
|
19
|
+
return
|
|
20
|
+
self.__iadd__(x)
|
|
21
|
+
|
|
22
|
+
if coupled_pairs is not None:
|
|
23
|
+
for i, j in coupled_pairs:
|
|
24
|
+
self.couple_orbitals(i, j)
|
|
25
|
+
|
|
26
|
+
def __iadd__(self, other):
|
|
27
|
+
if other.__class__.__name__ == 'str':
|
|
28
|
+
self.add_str_det(other)
|
|
29
|
+
elif other.__class__.__name__ == 'SlaterDet':
|
|
30
|
+
self.add_det(other)
|
|
31
|
+
elif other.__class__.__name__ == 'FixedPsi':
|
|
32
|
+
self.add_fixedpsi(other)
|
|
33
|
+
return self
|
|
34
|
+
|
|
35
|
+
def __add__(self, other):
|
|
36
|
+
result = FixedPsi(self)
|
|
37
|
+
result += other
|
|
38
|
+
return result
|
|
39
|
+
|
|
40
|
+
def __sub__(self, other):
|
|
41
|
+
result = FixedPsi(self)
|
|
42
|
+
result += (-1)*other
|
|
43
|
+
return result
|
|
44
|
+
|
|
45
|
+
def __rsub__(self, other):
|
|
46
|
+
result = (-1) * FixedPsi(self)
|
|
47
|
+
result += other
|
|
48
|
+
return result
|
|
49
|
+
|
|
50
|
+
def __neg__(self):
|
|
51
|
+
return (-1) * FixedPsi(self)
|
|
52
|
+
|
|
53
|
+
def __mul__(self, other):
|
|
54
|
+
if isinstance(other, int) or isinstance(other, float):
|
|
55
|
+
result = FixedPsi(self)
|
|
56
|
+
for i in range(len(result)):
|
|
57
|
+
result.coefs[i] = attempt_int(result.coefs[i]*other)
|
|
58
|
+
return result
|
|
59
|
+
|
|
60
|
+
if other.__class__.__name__ == 'SlaterDet':
|
|
61
|
+
result = FixedPsi()
|
|
62
|
+
for d, c in self:
|
|
63
|
+
result.add_det(d * other, c)
|
|
64
|
+
return result
|
|
65
|
+
|
|
66
|
+
if other.__class__.__name__ == 'FixedPsi':
|
|
67
|
+
result = FixedPsi()
|
|
68
|
+
for dS, cS in self:
|
|
69
|
+
for dO, cO in other:
|
|
70
|
+
d = dS * dO
|
|
71
|
+
if d.Nel == 0:
|
|
72
|
+
continue
|
|
73
|
+
fp1 = d.get_sorted()
|
|
74
|
+
c1, d1 = fp1.coefs[0], fp1.dets[0]
|
|
75
|
+
fp2 = standardize_det_2(d1)
|
|
76
|
+
result = result + (c1*cS*cO)*fp2
|
|
77
|
+
# result.add_det(d, coef = cS * cO)
|
|
78
|
+
return result
|
|
79
|
+
return NotImplemented
|
|
80
|
+
|
|
81
|
+
def __rmul__(self, other):
|
|
82
|
+
if isinstance(other, int) or isinstance(other, float):
|
|
83
|
+
result = FixedPsi(self)
|
|
84
|
+
for i in range(len(result)):
|
|
85
|
+
result.coefs[i] = attempt_int(result.coefs[i]*other)
|
|
86
|
+
return result
|
|
87
|
+
|
|
88
|
+
def __getitem__(self, item):
|
|
89
|
+
return self.dets[item]
|
|
90
|
+
|
|
91
|
+
def __len__(self):
|
|
92
|
+
return len(self.dets)
|
|
93
|
+
|
|
94
|
+
def __contains__(self, item):
|
|
95
|
+
for d, c in self:
|
|
96
|
+
if d.det_string == item:
|
|
97
|
+
return True
|
|
98
|
+
return False
|
|
99
|
+
|
|
100
|
+
def __iter__(self):
|
|
101
|
+
return zip(self.dets, self.coefs)
|
|
102
|
+
|
|
103
|
+
def add_det(self, det, coef=+1):
|
|
104
|
+
assert det.__class__.__name__ == 'SlaterDet'
|
|
105
|
+
if det.Nel == 0:
|
|
106
|
+
return
|
|
107
|
+
|
|
108
|
+
# Note: mixed-Nel FixedPsis are allowed (used by N-changing
|
|
109
|
+
# second-quantized operators like η± and bare c/c†). `self.Nel`
|
|
110
|
+
# records the Nel of the first non-zero det added; callers that
|
|
111
|
+
# require a uniform-N state should check this themselves.
|
|
112
|
+
|
|
113
|
+
for i in range(len(self)):
|
|
114
|
+
if det.det_string == self.dets[i].det_string:
|
|
115
|
+
self.coefs[i] += coef
|
|
116
|
+
return
|
|
117
|
+
|
|
118
|
+
self.dets.append(det)
|
|
119
|
+
cf = attempt_int(coef)
|
|
120
|
+
self.coefs.append(cf)
|
|
121
|
+
if self.Nel == 0:
|
|
122
|
+
self.Nel = det.Nel
|
|
123
|
+
|
|
124
|
+
def add_str_det(self, det_string, coef=+1):
|
|
125
|
+
sd = symvb.SlaterDet(det_string)
|
|
126
|
+
self.add_det(sd, coef=coef)
|
|
127
|
+
|
|
128
|
+
def add_fixedpsi(self, p, coef=1.0):
|
|
129
|
+
for d, c in p:
|
|
130
|
+
merged = False
|
|
131
|
+
# check if d is already in self
|
|
132
|
+
for i in range(len(self)):
|
|
133
|
+
if self.dets[i].det_string == d.det_string:
|
|
134
|
+
self.coefs[i] += c
|
|
135
|
+
# if it turns to 0, shift left the remaining dets
|
|
136
|
+
if self.coefs[i] == 0:
|
|
137
|
+
for j in range(i, len(self) - 1):
|
|
138
|
+
self.dets[j] = self.dets[j + 1]
|
|
139
|
+
self.coefs[j] = self.coefs[j + 1]
|
|
140
|
+
self.dets = self.dets[:-1]
|
|
141
|
+
self.coefs = self.coefs[:-1]
|
|
142
|
+
merged = True
|
|
143
|
+
break
|
|
144
|
+
if not merged:
|
|
145
|
+
self.add_det(d, c * coef)
|
|
146
|
+
|
|
147
|
+
def canonicalize(self):
|
|
148
|
+
"""
|
|
149
|
+
Rewrite self so that every Slater determinant has its alpha- and beta-
|
|
150
|
+
orbital labels in alphabetic order. The permutation sign is folded into
|
|
151
|
+
the coefficient, and dets that collapse to identical canonical strings
|
|
152
|
+
are merged. Determinants whose coefficients cancel to zero are dropped.
|
|
153
|
+
|
|
154
|
+
After canonicalize(), every alpha_string matches a key produced by
|
|
155
|
+
generate_det_strings, so the precomputed-half-dets fast path in
|
|
156
|
+
Molecule.op_det works on this FixedPsi.
|
|
157
|
+
"""
|
|
158
|
+
new_dets = []
|
|
159
|
+
new_coefs = []
|
|
160
|
+
for d, c in zip(self.dets, self.coefs):
|
|
161
|
+
sorted_fp = d.get_sorted()
|
|
162
|
+
cd = sorted_fp.dets[0]
|
|
163
|
+
cc = sorted_fp.coefs[0]
|
|
164
|
+
signed = attempt_int(c * cc)
|
|
165
|
+
|
|
166
|
+
merged = False
|
|
167
|
+
for i, existing in enumerate(new_dets):
|
|
168
|
+
if existing.det_string == cd.det_string:
|
|
169
|
+
new_coefs[i] = attempt_int(new_coefs[i] + signed)
|
|
170
|
+
merged = True
|
|
171
|
+
break
|
|
172
|
+
if not merged:
|
|
173
|
+
new_dets.append(cd)
|
|
174
|
+
new_coefs.append(signed)
|
|
175
|
+
|
|
176
|
+
self.dets = [d for d, c in zip(new_dets, new_coefs) if c != 0]
|
|
177
|
+
self.coefs = [c for c in new_coefs if c != 0]
|
|
178
|
+
if not self.dets:
|
|
179
|
+
self.Nel = 0
|
|
180
|
+
return self
|
|
181
|
+
|
|
182
|
+
def standardize(self):
|
|
183
|
+
"""Rewrite self so that every determinant is in symvb's canonical
|
|
184
|
+
creation order (the interleaved ``uLuL...`` form of
|
|
185
|
+
:func:`symvb.functions.standardize_det`). The permutation sign is
|
|
186
|
+
folded into the coefficient and determinants that collapse to identical
|
|
187
|
+
canonical strings are merged; determinants whose coefficients cancel to
|
|
188
|
+
zero are dropped.
|
|
189
|
+
|
|
190
|
+
This is the transformation matrix construction needs. Unlike
|
|
191
|
+
:meth:`canonicalize` (which only sorts orbital labels *within* the alpha
|
|
192
|
+
and beta blocks and leaves the spin pattern alone), this also brings the
|
|
193
|
+
spin pattern to the standard alpha-before-beta interleaving. A structure
|
|
194
|
+
written in a natural creation order, e.g. the alpha-alpha-beta-beta
|
|
195
|
+
pattern produced by ``coupled_pairs`` on a long bond, therefore ends up
|
|
196
|
+
in the same convention as the determinant basis, so its couplings are no
|
|
197
|
+
longer silently dropped by the spin-pattern matching in
|
|
198
|
+
:meth:`Molecule.build_matrix` / :meth:`Molecule.o2_matrix`.
|
|
199
|
+
|
|
200
|
+
On a determinant that is already in canonical order this is a no-op
|
|
201
|
+
(zero flips, unchanged string).
|
|
202
|
+
"""
|
|
203
|
+
new_dets = []
|
|
204
|
+
new_coefs = []
|
|
205
|
+
for d, c in zip(self.dets, self.coefs):
|
|
206
|
+
std, flips = standardize_det(d.det_string)
|
|
207
|
+
signed = attempt_int(c * ((-1) ** flips))
|
|
208
|
+
|
|
209
|
+
merged = False
|
|
210
|
+
for i, existing in enumerate(new_dets):
|
|
211
|
+
if existing.det_string == std:
|
|
212
|
+
new_coefs[i] = attempt_int(new_coefs[i] + signed)
|
|
213
|
+
merged = True
|
|
214
|
+
break
|
|
215
|
+
if not merged:
|
|
216
|
+
new_dets.append(symvb.SlaterDet(std))
|
|
217
|
+
new_coefs.append(signed)
|
|
218
|
+
|
|
219
|
+
self.dets = [d for d, c in zip(new_dets, new_coefs) if c != 0]
|
|
220
|
+
self.coefs = [c for c in new_coefs if c != 0]
|
|
221
|
+
if not self.dets:
|
|
222
|
+
self.Nel = 0
|
|
223
|
+
return self
|
|
224
|
+
|
|
225
|
+
def couple_orbitals(self, o1, o2):
|
|
226
|
+
# generate determinants that represent a singlet bonding coupling between two orbitals.
|
|
227
|
+
# Orbital numbering starts from 0
|
|
228
|
+
determinants = self.dets.copy()
|
|
229
|
+
coefs = self.coefs.copy()
|
|
230
|
+
for d, coef in zip(determinants, coefs):
|
|
231
|
+
# Flip spins
|
|
232
|
+
ds = d.det_string
|
|
233
|
+
c1, c2 = [c.lower() if c.isupper() else c.upper() for c in [ds[o1], ds[o2]]]
|
|
234
|
+
assert c1.lower() != c2.lower(), 'Cannot couple the same orbital'
|
|
235
|
+
# Flip positions
|
|
236
|
+
ds2 = ds[:o1] + c2 + ds[(o1 + 1):o2] + c1 + ds[(o2 + 1):]
|
|
237
|
+
self.add_str_det(ds2, coef=coef)
|
|
238
|
+
|
|
239
|
+
def __repr__(self):
|
|
240
|
+
s = ''
|
|
241
|
+
for d, cf in self:
|
|
242
|
+
dc = attempt_int(cf)
|
|
243
|
+
|
|
244
|
+
if dc > 0:
|
|
245
|
+
if dc == 1.0:
|
|
246
|
+
s += '+'
|
|
247
|
+
else:
|
|
248
|
+
s += '+%s' % dc
|
|
249
|
+
elif dc < 0:
|
|
250
|
+
if dc == -1.0:
|
|
251
|
+
s += '-'
|
|
252
|
+
else:
|
|
253
|
+
s += '%s' % dc
|
|
254
|
+
s += str(d)
|
|
255
|
+
if len(s) > 0 and s[0] == '+':
|
|
256
|
+
s = s[1:]
|
|
257
|
+
return s
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def generate_dets(Nela, Nelb, Norb):
|
|
261
|
+
"""
|
|
262
|
+
Generate all possible determinants for a given number of electrons and atomic orbitals.
|
|
263
|
+
:param Nela: Number of alpha electrons
|
|
264
|
+
:param Nelb: Number of beta electrons
|
|
265
|
+
:param Norb: Number of atomic orbitals
|
|
266
|
+
:return: List of FixedPsi objects, each containing one determinant
|
|
267
|
+
"""
|
|
268
|
+
L = generate_det_strings(Nela, Nelb, Norb)
|
|
269
|
+
PP = [None,]*len(L)
|
|
270
|
+
for i in range(len(L)):
|
|
271
|
+
PP[i] = FixedPsi(L[i])
|
|
272
|
+
return PP
|
symvb/functions.py
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import string
|
|
2
|
+
from itertools import combinations
|
|
3
|
+
|
|
4
|
+
from scipy.stats import rankdata
|
|
5
|
+
import sympy
|
|
6
|
+
|
|
7
|
+
import symvb
|
|
8
|
+
from symvb.data import hperm
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def generate_det_strings(Na, Nb, Norbs):
|
|
12
|
+
"""
|
|
13
|
+
Generate all possible determinant strings for a given number of electrons and atomic orbitals.
|
|
14
|
+
:param Na: Number of alpha electrons
|
|
15
|
+
:param Nb: Number of beta electrons
|
|
16
|
+
:param Norbs: Number of atomic orbitals
|
|
17
|
+
:return: list of determinant strings
|
|
18
|
+
"""
|
|
19
|
+
result = []
|
|
20
|
+
for a in combinations(string.ascii_lowercase[:Norbs], Na):
|
|
21
|
+
for b in combinations(string.ascii_uppercase[:Norbs], Nb):
|
|
22
|
+
s = ''
|
|
23
|
+
for i in range(min(Na, Nb)):
|
|
24
|
+
s += a[i] + b[i]
|
|
25
|
+
for i in range(Nb, Na): # if Na > Nb
|
|
26
|
+
s += a[i]
|
|
27
|
+
for i in range(Na, Nb): # if Nb > Na
|
|
28
|
+
s += b[i]
|
|
29
|
+
result.append(s)
|
|
30
|
+
return result
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def attempt_int(x):
|
|
34
|
+
"""
|
|
35
|
+
Convert a float to int if it is numerically equivalent
|
|
36
|
+
Parameters
|
|
37
|
+
----------
|
|
38
|
+
x: a number (float)
|
|
39
|
+
Returns
|
|
40
|
+
-------
|
|
41
|
+
int or float
|
|
42
|
+
"""
|
|
43
|
+
try:
|
|
44
|
+
if x == int(x):
|
|
45
|
+
return int(x)
|
|
46
|
+
except TypeError:
|
|
47
|
+
# symbolic (sympy) coefficients pass through unchanged
|
|
48
|
+
pass
|
|
49
|
+
return x
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def place_low(s, i):
|
|
53
|
+
"""
|
|
54
|
+
Flips two characters in the string to place the next available lower-case character at i-th position.
|
|
55
|
+
E.g. place_low('aBcD',1) will return 'acBD'
|
|
56
|
+
Parameters
|
|
57
|
+
----------
|
|
58
|
+
s: a string containing upper and lower case characters
|
|
59
|
+
i: position in the string
|
|
60
|
+
|
|
61
|
+
Returns
|
|
62
|
+
-------
|
|
63
|
+
a list containing two elements:
|
|
64
|
+
1) new string
|
|
65
|
+
2) 1 if the flip was performed; 0 otherwise
|
|
66
|
+
"""
|
|
67
|
+
new_s = s
|
|
68
|
+
if new_s[i].islower(): # alpha-orbital, no swap needed;
|
|
69
|
+
return new_s, 0
|
|
70
|
+
else:
|
|
71
|
+
j = 1
|
|
72
|
+
while new_s[i + j].isupper():
|
|
73
|
+
j += 1
|
|
74
|
+
new_s = new_s[:i] + new_s[i + j] + new_s[i + 1:i + j] + new_s[i] + new_s[i + j + 1:]
|
|
75
|
+
return new_s, 1
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def place_high(s, i):
|
|
79
|
+
"""
|
|
80
|
+
Flips two characters in the string to place the next available upper-case character at i-th position.
|
|
81
|
+
E.g. place_low('aBcD',0) will return 'BacD'
|
|
82
|
+
Parameters
|
|
83
|
+
----------
|
|
84
|
+
s: a string containing upper and lower case characters
|
|
85
|
+
i: position in the string
|
|
86
|
+
|
|
87
|
+
Returns
|
|
88
|
+
-------
|
|
89
|
+
a list containing two elements:
|
|
90
|
+
1) new string
|
|
91
|
+
2) 1 if the flip was performed; 0 otherwise
|
|
92
|
+
"""
|
|
93
|
+
new_s = s
|
|
94
|
+
if new_s[i].isupper(): # beta-orbital, no swap needed;
|
|
95
|
+
return new_s, 0
|
|
96
|
+
else:
|
|
97
|
+
j = 1
|
|
98
|
+
while new_s[i + j].islower():
|
|
99
|
+
j += 1
|
|
100
|
+
new_s = new_s[:i] + new_s[i + j] + new_s[i + 1:i + j] + new_s[i] + new_s[i + j + 1:]
|
|
101
|
+
return new_s, 1
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def standardize_det(s):
|
|
105
|
+
"""
|
|
106
|
+
Rearranges the orbitals in a given determinat string to the standard format,
|
|
107
|
+
that is 'uLuLuL', 'uLuLuLuuu' or 'uLuLuLLLL'
|
|
108
|
+
Parameters
|
|
109
|
+
----------
|
|
110
|
+
s: determinant string
|
|
111
|
+
|
|
112
|
+
Returns
|
|
113
|
+
-------
|
|
114
|
+
1) Determinant string in a standard format
|
|
115
|
+
2) Number of pairwise flips required to obtain the standard format
|
|
116
|
+
"""
|
|
117
|
+
new_s = s
|
|
118
|
+
Nup, Ndown = 0, 0
|
|
119
|
+
for i in range(len(s)):
|
|
120
|
+
if s[i].isupper():
|
|
121
|
+
Ndown += 1
|
|
122
|
+
else:
|
|
123
|
+
Nup += 1
|
|
124
|
+
|
|
125
|
+
arr_up = 0
|
|
126
|
+
arr_down = 0
|
|
127
|
+
complete = False
|
|
128
|
+
i = 0
|
|
129
|
+
flips = 0
|
|
130
|
+
while arr_down < Ndown and arr_up < Nup:
|
|
131
|
+
new_s, flip = place_low(new_s, i)
|
|
132
|
+
flips += flip
|
|
133
|
+
arr_up += 1
|
|
134
|
+
i += 1
|
|
135
|
+
|
|
136
|
+
new_s, flip = place_high(new_s, i)
|
|
137
|
+
flips += flip
|
|
138
|
+
i += 1
|
|
139
|
+
arr_down += 1
|
|
140
|
+
return new_s, flips
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def standardize_det_2(d):
|
|
144
|
+
s, nflips = standardize_det(d.det_string)
|
|
145
|
+
result = symvb.FixedPsi()
|
|
146
|
+
if nflips % 2 ==0:
|
|
147
|
+
coeff = 1
|
|
148
|
+
else:
|
|
149
|
+
coeff = -1
|
|
150
|
+
# store the STANDARDIZED determinant with the fold sign; keeping the
|
|
151
|
+
# original string here (pre-2026-07-04 behavior) double-counted the
|
|
152
|
+
# permutation sign and returned sign-flipped products from FixedPsi.__mul__
|
|
153
|
+
result.add_det(symvb.SlaterDet(s), coef=coeff)
|
|
154
|
+
return result
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def sort_ind(v):
|
|
158
|
+
z = rankdata(v, method='ordinal')
|
|
159
|
+
h = {}
|
|
160
|
+
for i in range(len(z)):
|
|
161
|
+
h[z[i]] = v[i]
|
|
162
|
+
z2 = hperm[tuple(z)]
|
|
163
|
+
|
|
164
|
+
result = [0, ] * len(z2)
|
|
165
|
+
for i in range(len(z2)):
|
|
166
|
+
result[i] = h[z2[i]]
|
|
167
|
+
return result
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def canonical_chemist_iv(iv):
|
|
171
|
+
"""Canonicalise a 4-AO physicist-notation tuple <i j | k l> under the
|
|
172
|
+
8-fold permutational symmetry of the corresponding chemist integral
|
|
173
|
+
(ik|jl), valid for real orbitals:
|
|
174
|
+
|
|
175
|
+
(ik|jl) = (ki|jl) = (ik|lj) = (ki|lj)
|
|
176
|
+
= (jl|ik) = (lj|ik) = (jl|ki) = (lj|ki)
|
|
177
|
+
|
|
178
|
+
The corresponding action on the physicist iv = (i, j, k, l) is
|
|
179
|
+
generated by:
|
|
180
|
+
alpha: swap slots 0 and 2 (within chemist bra-pair)
|
|
181
|
+
beta : swap slots 1 and 3 (within chemist ket-pair)
|
|
182
|
+
gamma: swap (slot 0, slot 1) with (slot 2, slot 3), i.e. (01)(23)
|
|
183
|
+
(chemist bra-pair <-> ket-pair)
|
|
184
|
+
|
|
185
|
+
The 8 group elements yield the same orbit structure as the existing
|
|
186
|
+
`symvb.data.hperm` table (which encodes the same group). This function
|
|
187
|
+
returns the lex-smallest of the 8 equivalent iv tuples.
|
|
188
|
+
|
|
189
|
+
Unlike `sort_ind`, this works correctly for tuples with repeated AO
|
|
190
|
+
labels: e.g. (a, a, b, c) and (a, a, c, b) both map to the same
|
|
191
|
+
canonical tuple, so they receive the same default integral symbol
|
|
192
|
+
name. `sort_ind`'s ordinal rankdata path assigns distinct ranks to
|
|
193
|
+
repeated values, missing some chemist orbit identifications.
|
|
194
|
+
"""
|
|
195
|
+
i, j, k, l = iv
|
|
196
|
+
forms = (
|
|
197
|
+
(i, j, k, l), # identity
|
|
198
|
+
(k, j, i, l), # alpha: swap slots 0, 2
|
|
199
|
+
(i, l, k, j), # beta : swap slots 1, 3
|
|
200
|
+
(k, l, i, j), # alpha * beta
|
|
201
|
+
(j, i, l, k), # gamma: (01)(23)
|
|
202
|
+
(l, i, j, k), # alpha * gamma
|
|
203
|
+
(j, k, l, i), # beta * gamma
|
|
204
|
+
(l, k, j, i), # alpha * beta * gamma
|
|
205
|
+
)
|
|
206
|
+
return min(forms)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def simplify_matrix(mtx, factor=False):
|
|
210
|
+
result = sympy.zeros(mtx.shape[0])
|
|
211
|
+
for i in range(mtx.shape[0]):
|
|
212
|
+
for j in range(mtx.shape[0]):
|
|
213
|
+
if factor:
|
|
214
|
+
result[i, j] = sympy.factor(mtx[i, j])
|
|
215
|
+
else:
|
|
216
|
+
result[i, j] = sympy.simplify(mtx[i, j])
|
|
217
|
+
return result
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def sorti(s):
|
|
221
|
+
s2 = s
|
|
222
|
+
nperms = 0
|
|
223
|
+
for i in range(len(s) - 1):
|
|
224
|
+
jmin = i
|
|
225
|
+
for j in range(i + 1, len(s)):
|
|
226
|
+
if s2[j] < s2[jmin]:
|
|
227
|
+
jmin = j
|
|
228
|
+
if jmin != i:
|
|
229
|
+
s2 = s2[:i] + s2[jmin] + s2[i + 1:jmin] + s2[i] + s2[jmin + 1:]
|
|
230
|
+
nperms += 1
|
|
231
|
+
return s2, nperms
|