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/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