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/operators.py
ADDED
|
@@ -0,0 +1,667 @@
|
|
|
1
|
+
"""
|
|
2
|
+
symvb.operators — second-quantized operators acting on Slater determinants.
|
|
3
|
+
|
|
4
|
+
A small expression-tree framework for building physical operators from
|
|
5
|
+
elementary creation/annihilation operators and applying them to symvb
|
|
6
|
+
Slater dets.
|
|
7
|
+
|
|
8
|
+
Scope
|
|
9
|
+
-----
|
|
10
|
+
This module is for **orthogonal-AO, second-quantized** operators —
|
|
11
|
+
spin (S², Sz, S±, Sᵢ·Sⱼ), number / double-occupancy, hopping
|
|
12
|
+
(c†_i c_j), η-pairing, orbital-permutation symmetries, and the
|
|
13
|
+
projectors / VB structures built from them. The Slater determinants
|
|
14
|
+
are assumed orthonormal (i.e. AO overlap s = 0).
|
|
15
|
+
|
|
16
|
+
For non-orthogonal AOs (s ≠ 0) — i.e. for evaluating ⟨D | H | D'⟩
|
|
17
|
+
with one- and two-electron integrals — use the existing
|
|
18
|
+
`symvb.Molecule.build_matrix` pipeline, which goes through the Löwdin
|
|
19
|
+
cofactor expansion. The two paths are complementary, not redundant.
|
|
20
|
+
|
|
21
|
+
Quick tour
|
|
22
|
+
----------
|
|
23
|
+
from symvb import operators as op
|
|
24
|
+
|
|
25
|
+
# primitives
|
|
26
|
+
op.c('a', 'alpha') # c_{a, α}
|
|
27
|
+
op.cdag('a', 'beta') # c†_{a, β}
|
|
28
|
+
|
|
29
|
+
# composites
|
|
30
|
+
op.number('a') # n̂_a = n̂_aα + n̂_aβ
|
|
31
|
+
op.double_occ('a') # n̂_aα n̂_aβ
|
|
32
|
+
op.hop('a', 'b', 'alpha') # c†_{a,α} c_{b,α} + h.c.
|
|
33
|
+
op.s_squared(['a', 'b']) # total S²
|
|
34
|
+
op.s_dot('a', 'b') # S_a · S_b
|
|
35
|
+
|
|
36
|
+
# algebra
|
|
37
|
+
H = -op.hop('a', 'b') + op.double_occ('a') + op.double_occ('b')
|
|
38
|
+
|
|
39
|
+
# action on a state
|
|
40
|
+
H.apply('aB') # → FixedPsi
|
|
41
|
+
H.apply(symvb.FixedPsi('aB')) # also accepted
|
|
42
|
+
|
|
43
|
+
# matrix in any basis (det strings, SlaterDets, FixedPsis):
|
|
44
|
+
H.matrix(['aB', 'bA', 'aA', 'bB']) # → sympy Matrix
|
|
45
|
+
|
|
46
|
+
Convention
|
|
47
|
+
----------
|
|
48
|
+
Determinants are symvb strings: lowercase = α, uppercase = β. Internally
|
|
49
|
+
operators work in the **interleaved canonical form** that matches
|
|
50
|
+
`symvb.functions.generate_det_strings` and `symvb.spin._canon_det` —
|
|
51
|
+
sorted α and β are paired up in lex order, with any unmatched extras
|
|
52
|
+
appended (e.g. canonical for α={a,c}, β={b,d} is `aBcD`).
|
|
53
|
+
|
|
54
|
+
Jordan–Wigner ordering is also interleaved: `(a,α) < (a,β) < (b,α) <
|
|
55
|
+
(b,β) < …`. Operators emit FixedPsi states keyed by canonical strings,
|
|
56
|
+
with coefficients in the *user-string* convention — i.e. they are the
|
|
57
|
+
multipliers FixedPsi will reconstruct the physical state from when
|
|
58
|
+
parsing the string in left-to-right creation order.
|
|
59
|
+
|
|
60
|
+
Compose with `+`, `-`, scalar `*`, and `@` (composition); `A @ B`
|
|
61
|
+
means "B acts first, then A".
|
|
62
|
+
"""
|
|
63
|
+
from __future__ import annotations
|
|
64
|
+
|
|
65
|
+
import numpy as np
|
|
66
|
+
import sympy as sp
|
|
67
|
+
|
|
68
|
+
import symvb
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# ---------------------------------------------------------------------------
|
|
72
|
+
# Canonical det conversion (interleaved; matches generate_det_strings)
|
|
73
|
+
# ---------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
def _parse_det(det_string):
|
|
76
|
+
"""symvb det string → (alpha_set, beta_set, sign).
|
|
77
|
+
|
|
78
|
+
`sign` is the ±1 such that |det_string⟩_user = sign · |canonical(α,β)⟩,
|
|
79
|
+
where |canonical(α,β)⟩ has its creation operators in interleaved JW
|
|
80
|
+
order: (a,α) < (a,β) < (b,α) < (b,β) < … . Concretely, parse the
|
|
81
|
+
string left-to-right (creation order) into a list of (orb, spin)
|
|
82
|
+
spin-orbital tuples and return the parity of inversions in that list
|
|
83
|
+
when compared against tuple lex order.
|
|
84
|
+
"""
|
|
85
|
+
alpha = set()
|
|
86
|
+
beta = set()
|
|
87
|
+
so_seq = []
|
|
88
|
+
for ch in det_string:
|
|
89
|
+
if ch.islower():
|
|
90
|
+
assert ch not in alpha, f"two electrons in spin-orbital ({ch}, α)"
|
|
91
|
+
alpha.add(ch)
|
|
92
|
+
so_seq.append((ch, 0))
|
|
93
|
+
else:
|
|
94
|
+
lc = ch.lower()
|
|
95
|
+
assert lc not in beta, f"two electrons in spin-orbital ({lc}, β)"
|
|
96
|
+
beta.add(lc)
|
|
97
|
+
so_seq.append((lc, 1))
|
|
98
|
+
return frozenset(alpha), frozenset(beta), _inversion_sign(so_seq)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _inversion_sign(seq):
|
|
102
|
+
inv = 0
|
|
103
|
+
n = len(seq)
|
|
104
|
+
for i in range(n):
|
|
105
|
+
for j in range(i + 1, n):
|
|
106
|
+
if seq[i] > seq[j]:
|
|
107
|
+
inv += 1
|
|
108
|
+
return 1 if inv % 2 == 0 else -1
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _canonical_string(alpha, beta):
|
|
112
|
+
"""Interleaved canonical det string for (α, β); matches
|
|
113
|
+
`symvb.functions.generate_det_strings` for any |α| / |β|.
|
|
114
|
+
|
|
115
|
+
Pairs sorted α and β letters in lex order, then appends extras of
|
|
116
|
+
whichever block is longer: e.g. ({a,b,c}, {d}) → 'aDbc'.
|
|
117
|
+
"""
|
|
118
|
+
a = sorted(alpha)
|
|
119
|
+
b = sorted(beta)
|
|
120
|
+
Na, Nb = len(a), len(b)
|
|
121
|
+
s = ''
|
|
122
|
+
for i in range(min(Na, Nb)):
|
|
123
|
+
s += a[i] + b[i].upper()
|
|
124
|
+
for i in range(Nb, Na): # extra alphas
|
|
125
|
+
s += a[i]
|
|
126
|
+
for i in range(Na, Nb): # extra betas
|
|
127
|
+
s += b[i].upper()
|
|
128
|
+
return s
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def canonicalize(det_string):
|
|
132
|
+
"""Return (canonical_string, sign) for a symvb det string.
|
|
133
|
+
|
|
134
|
+
`|det_string⟩_user = sign · |canonical_string⟩_canonical`.
|
|
135
|
+
"""
|
|
136
|
+
a, b, s = _parse_det(det_string)
|
|
137
|
+
return _canonical_string(a, b), s
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# ---------------------------------------------------------------------------
|
|
141
|
+
# Elementary creation/annihilation in interleaved JW order
|
|
142
|
+
# ---------------------------------------------------------------------------
|
|
143
|
+
|
|
144
|
+
def _apply_c(alpha, beta, orb, spin, create):
|
|
145
|
+
"""Apply c[†]_{orb,spin} in interleaved canonical ordering.
|
|
146
|
+
|
|
147
|
+
Returns (new_alpha, new_beta, sign) or (None, None, 0) if annihilated.
|
|
148
|
+
Spin: 0 = α, 1 = β. Interleaved JW order: (orb, α) < (orb, β) <
|
|
149
|
+
(orb', α) < (orb', β) for orb < orb'.
|
|
150
|
+
"""
|
|
151
|
+
occ = (orb in alpha) if spin == 0 else (orb in beta)
|
|
152
|
+
if create == occ:
|
|
153
|
+
return None, None, 0
|
|
154
|
+
|
|
155
|
+
# Count occupied modes strictly before (orb, spin) in interleaved order.
|
|
156
|
+
count = 0
|
|
157
|
+
for x in sorted(alpha | beta):
|
|
158
|
+
if x < orb:
|
|
159
|
+
count += int(x in alpha) + int(x in beta)
|
|
160
|
+
elif x == orb:
|
|
161
|
+
# (x, α) < (x, β); only counts if we are creating/annihilating β
|
|
162
|
+
# and the α-mode on the same orbital is occupied.
|
|
163
|
+
if spin == 1 and x in alpha:
|
|
164
|
+
count += 1
|
|
165
|
+
sgn = 1 if count % 2 == 0 else -1
|
|
166
|
+
|
|
167
|
+
if spin == 0:
|
|
168
|
+
new_a = (alpha | {orb}) if create else (alpha - {orb})
|
|
169
|
+
new_b = beta
|
|
170
|
+
else:
|
|
171
|
+
new_a = alpha
|
|
172
|
+
new_b = (beta | {orb}) if create else (beta - {orb})
|
|
173
|
+
return frozenset(new_a), frozenset(new_b), sgn
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
# ---------------------------------------------------------------------------
|
|
177
|
+
# Operator base class
|
|
178
|
+
# ---------------------------------------------------------------------------
|
|
179
|
+
|
|
180
|
+
def _is_scalar(x):
|
|
181
|
+
return isinstance(x, (int, float, complex, sp.Expr)) and not isinstance(x, bool)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _nonzero(x):
|
|
185
|
+
if isinstance(x, sp.Expr):
|
|
186
|
+
return sp.simplify(x) != 0
|
|
187
|
+
return x != 0
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _state_iter(state):
|
|
191
|
+
"""Yield (det_string, coef) pairs from any supported state form."""
|
|
192
|
+
if isinstance(state, str):
|
|
193
|
+
yield (state, 1)
|
|
194
|
+
return
|
|
195
|
+
# SlaterDet has a det_string attribute but no dets/coefs.
|
|
196
|
+
if hasattr(state, 'det_string') and not hasattr(state, 'dets'):
|
|
197
|
+
yield (state.det_string, 1)
|
|
198
|
+
return
|
|
199
|
+
if hasattr(state, 'dets') and hasattr(state, 'coefs'):
|
|
200
|
+
for d, c in zip(state.dets, state.coefs):
|
|
201
|
+
yield (d.det_string, c)
|
|
202
|
+
return
|
|
203
|
+
if isinstance(state, dict):
|
|
204
|
+
for k, v in state.items():
|
|
205
|
+
yield (k, v)
|
|
206
|
+
return
|
|
207
|
+
raise TypeError(f"unsupported state type: {type(state).__name__}")
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _state_to_canonical_dict(state):
|
|
211
|
+
"""{canonical_string: canonical-basis-coefficient} for any state form.
|
|
212
|
+
|
|
213
|
+
The output dict represents the state in the canonical basis (i.e.
|
|
214
|
+
coefficients on |canonical_string⟩_canonical, the interleaved-JW-ordered
|
|
215
|
+
canonical state). Conversion to user-string FixedPsi multiplies by
|
|
216
|
+
canonicalize(canonical_string)[1].
|
|
217
|
+
"""
|
|
218
|
+
out = {}
|
|
219
|
+
for det_str, c_user in _state_iter(state):
|
|
220
|
+
a, b, sgn = _parse_det(det_str) # |det_str⟩_user = sgn · |canon(a,b)⟩
|
|
221
|
+
key = _canonical_string(a, b)
|
|
222
|
+
out[key] = out.get(key, 0) + c_user * sgn
|
|
223
|
+
return {k: v for k, v in out.items() if _nonzero(v)}
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _canonical_dict_to_fixedpsi(canon_dict):
|
|
227
|
+
"""{canonical_string: canonical-coef} → FixedPsi (user-basis).
|
|
228
|
+
|
|
229
|
+
For each canonical string key, the FixedPsi coefficient is the
|
|
230
|
+
canonical coefficient times the sign relating |canonical_string⟩_user
|
|
231
|
+
(FixedPsi's interpretation) back to |canonical_string⟩_canonical.
|
|
232
|
+
Both ±1, so multiplication and division are the same.
|
|
233
|
+
"""
|
|
234
|
+
psi = symvb.FixedPsi()
|
|
235
|
+
for canon_str, c_canon in canon_dict.items():
|
|
236
|
+
if not _nonzero(c_canon):
|
|
237
|
+
continue
|
|
238
|
+
sgn_user = canonicalize(canon_str)[1]
|
|
239
|
+
c_user = c_canon * sgn_user
|
|
240
|
+
if _nonzero(c_user):
|
|
241
|
+
psi.add_str_det(canon_str, coef=c_user)
|
|
242
|
+
return psi
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
class Operator:
|
|
246
|
+
"""Abstract second-quantized operator.
|
|
247
|
+
|
|
248
|
+
Subclasses must implement
|
|
249
|
+
_apply_internal((alpha_set, beta_set))
|
|
250
|
+
-> dict {(α', β'): canonical-basis coefficient}
|
|
251
|
+
operating in the canonical (interleaved-JW) representation.
|
|
252
|
+
|
|
253
|
+
Public API:
|
|
254
|
+
apply(state) -> FixedPsi
|
|
255
|
+
matrix(basis) -> sympy Matrix in user basis
|
|
256
|
+
expectation(state) -> scalar (assumes orthonormal basis dets)
|
|
257
|
+
Algebra: +, -, scalar *, @ (composition).
|
|
258
|
+
"""
|
|
259
|
+
|
|
260
|
+
# ---- subclass hook ----
|
|
261
|
+
def _apply_internal(self, ab):
|
|
262
|
+
raise NotImplementedError
|
|
263
|
+
|
|
264
|
+
# ---- internal: canonical-basis dict ----
|
|
265
|
+
def _apply_canonical(self, state):
|
|
266
|
+
"""{canonical_string: canonical-basis coefficient} after applying self.
|
|
267
|
+
|
|
268
|
+
Goes through the canonical-basis representation; not part of the
|
|
269
|
+
public API. Used by `apply`, `matrix`, and `expectation`.
|
|
270
|
+
"""
|
|
271
|
+
in_dict = _state_to_canonical_dict(state)
|
|
272
|
+
out = {}
|
|
273
|
+
for canon_in, c_in in in_dict.items():
|
|
274
|
+
a, b, _ = _parse_det(canon_in) # canon_in is its own canonical string
|
|
275
|
+
for (aa, bb), coef in self._apply_internal((a, b)).items():
|
|
276
|
+
key = _canonical_string(aa, bb)
|
|
277
|
+
out[key] = out.get(key, 0) + c_in * coef
|
|
278
|
+
return {k: v for k, v in out.items() if _nonzero(v)}
|
|
279
|
+
|
|
280
|
+
# ---- public API ----
|
|
281
|
+
def apply(self, state):
|
|
282
|
+
"""Action on |state⟩. Returns a FixedPsi (user-basis).
|
|
283
|
+
|
|
284
|
+
`state` can be a symvb det string, a SlaterDet, a FixedPsi, or a
|
|
285
|
+
dict {det_string: coef}. The returned FixedPsi has dets keyed by
|
|
286
|
+
canonical (interleaved) strings.
|
|
287
|
+
"""
|
|
288
|
+
return _canonical_dict_to_fixedpsi(self._apply_canonical(state))
|
|
289
|
+
|
|
290
|
+
def matrix(self, basis):
|
|
291
|
+
"""Build M_ij = ⟨bᵢ| O |bⱼ⟩ as a sympy Matrix.
|
|
292
|
+
|
|
293
|
+
Each `bᵢ` may be a det string, a SlaterDet, or a FixedPsi. The
|
|
294
|
+
underlying symvb dets are assumed orthonormal (s = 0). Use
|
|
295
|
+
`Molecule.build_matrix` for the s ≠ 0 path.
|
|
296
|
+
"""
|
|
297
|
+
N = len(basis)
|
|
298
|
+
basis_canon = [_state_to_canonical_dict(b) for b in basis]
|
|
299
|
+
M = sp.zeros(N, N)
|
|
300
|
+
for j in range(N):
|
|
301
|
+
applied = {}
|
|
302
|
+
for canon_d, c_canon in basis_canon[j].items():
|
|
303
|
+
a, b, _ = _parse_det(canon_d)
|
|
304
|
+
for (aa, bb), coef in self._apply_internal((a, b)).items():
|
|
305
|
+
key = _canonical_string(aa, bb)
|
|
306
|
+
applied[key] = applied.get(key, 0) + c_canon * coef
|
|
307
|
+
for i in range(N):
|
|
308
|
+
inner = 0
|
|
309
|
+
for d, c in applied.items():
|
|
310
|
+
if d in basis_canon[i]:
|
|
311
|
+
inner = inner + sp.conjugate(basis_canon[i][d]) * c
|
|
312
|
+
M[i, j] = M[i, j] + inner
|
|
313
|
+
return M
|
|
314
|
+
|
|
315
|
+
def expectation(self, state):
|
|
316
|
+
"""⟨ψ| O |ψ⟩ assuming `state`'s dets are orthonormal."""
|
|
317
|
+
canon = _state_to_canonical_dict(state)
|
|
318
|
+
total = 0
|
|
319
|
+
for d_j, c_j in canon.items():
|
|
320
|
+
a, b, _ = _parse_det(d_j)
|
|
321
|
+
for (aa, bb), coef in self._apply_internal((a, b)).items():
|
|
322
|
+
d_i = _canonical_string(aa, bb)
|
|
323
|
+
c_i = canon.get(d_i, 0)
|
|
324
|
+
total = total + sp.conjugate(c_i) * c_j * coef
|
|
325
|
+
return sp.simplify(total)
|
|
326
|
+
|
|
327
|
+
# ---- algebra ----
|
|
328
|
+
def __add__(self, other):
|
|
329
|
+
if other == 0:
|
|
330
|
+
return self
|
|
331
|
+
if not isinstance(other, Operator):
|
|
332
|
+
return NotImplemented
|
|
333
|
+
terms = []
|
|
334
|
+
for op in (self, other):
|
|
335
|
+
if isinstance(op, _Sum):
|
|
336
|
+
terms.extend(op.terms)
|
|
337
|
+
else:
|
|
338
|
+
terms.append((1, op))
|
|
339
|
+
return _Sum(terms)
|
|
340
|
+
|
|
341
|
+
def __radd__(self, other):
|
|
342
|
+
if other == 0:
|
|
343
|
+
return self
|
|
344
|
+
return self.__add__(other)
|
|
345
|
+
|
|
346
|
+
def __neg__(self):
|
|
347
|
+
if isinstance(self, _Sum):
|
|
348
|
+
return _Sum([(-c, op) for c, op in self.terms])
|
|
349
|
+
return _Sum([(-1, self)])
|
|
350
|
+
|
|
351
|
+
def __sub__(self, other):
|
|
352
|
+
return self + (-other)
|
|
353
|
+
|
|
354
|
+
def __mul__(self, other):
|
|
355
|
+
# scalar * operator OR operator * operator (composition)
|
|
356
|
+
if isinstance(other, Operator):
|
|
357
|
+
return self.__matmul__(other)
|
|
358
|
+
if _is_scalar(other):
|
|
359
|
+
if isinstance(self, _Sum):
|
|
360
|
+
return _Sum([(c * other, op) for c, op in self.terms])
|
|
361
|
+
return _Sum([(other, self)])
|
|
362
|
+
return NotImplemented
|
|
363
|
+
|
|
364
|
+
def __rmul__(self, other):
|
|
365
|
+
if _is_scalar(other):
|
|
366
|
+
return self.__mul__(other)
|
|
367
|
+
return NotImplemented
|
|
368
|
+
|
|
369
|
+
def __matmul__(self, other):
|
|
370
|
+
if not isinstance(other, Operator):
|
|
371
|
+
return NotImplemented
|
|
372
|
+
factors = []
|
|
373
|
+
if isinstance(self, _Product):
|
|
374
|
+
factors.extend(self.factors)
|
|
375
|
+
else:
|
|
376
|
+
factors.append(self)
|
|
377
|
+
if isinstance(other, _Product):
|
|
378
|
+
factors.extend(other.factors)
|
|
379
|
+
else:
|
|
380
|
+
factors.append(other)
|
|
381
|
+
return _Product(factors)
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
# ---------------------------------------------------------------------------
|
|
385
|
+
# Internal node types
|
|
386
|
+
# ---------------------------------------------------------------------------
|
|
387
|
+
|
|
388
|
+
class _LadderOp(Operator):
|
|
389
|
+
"""Primitive c[†]_{orb,spin}."""
|
|
390
|
+
|
|
391
|
+
def __init__(self, orb, spin, dagger):
|
|
392
|
+
self.orb = orb
|
|
393
|
+
self.spin = spin
|
|
394
|
+
self.dagger = dagger
|
|
395
|
+
|
|
396
|
+
def _apply_internal(self, ab):
|
|
397
|
+
a, b, s = _apply_c(ab[0], ab[1], self.orb, self.spin, self.dagger)
|
|
398
|
+
if a is None:
|
|
399
|
+
return {}
|
|
400
|
+
return {(a, b): s}
|
|
401
|
+
|
|
402
|
+
def __repr__(self):
|
|
403
|
+
sym = 'c†' if self.dagger else 'c'
|
|
404
|
+
sp_label = 'α' if self.spin == 0 else 'β'
|
|
405
|
+
return f"{sym}_{{{self.orb},{sp_label}}}"
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
class _Sum(Operator):
|
|
409
|
+
"""Linear combination Σ_k c_k · O_k."""
|
|
410
|
+
|
|
411
|
+
def __init__(self, terms):
|
|
412
|
+
self.terms = list(terms)
|
|
413
|
+
|
|
414
|
+
def _apply_internal(self, ab):
|
|
415
|
+
out = {}
|
|
416
|
+
for coef, op in self.terms:
|
|
417
|
+
for k, v in op._apply_internal(ab).items():
|
|
418
|
+
out[k] = out.get(k, 0) + coef * v
|
|
419
|
+
return {k: v for k, v in out.items() if _nonzero(v)}
|
|
420
|
+
|
|
421
|
+
def __repr__(self):
|
|
422
|
+
return ' + '.join(f"{c}·{op}" for c, op in self.terms)
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
class _Product(Operator):
|
|
426
|
+
"""Composition F[0] · F[1] · … · F[-1]; rightmost applies first."""
|
|
427
|
+
|
|
428
|
+
def __init__(self, factors):
|
|
429
|
+
self.factors = list(factors)
|
|
430
|
+
|
|
431
|
+
def _apply_internal(self, ab):
|
|
432
|
+
cur = {ab: 1}
|
|
433
|
+
for op in reversed(self.factors):
|
|
434
|
+
new = {}
|
|
435
|
+
for ab_in, c_in in cur.items():
|
|
436
|
+
for ab_out, c_out in op._apply_internal(ab_in).items():
|
|
437
|
+
new[ab_out] = new.get(ab_out, 0) + c_in * c_out
|
|
438
|
+
cur = {k: v for k, v in new.items() if _nonzero(v)}
|
|
439
|
+
if not cur:
|
|
440
|
+
return {}
|
|
441
|
+
return cur
|
|
442
|
+
|
|
443
|
+
def __repr__(self):
|
|
444
|
+
return ' '.join(repr(f) for f in self.factors)
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
class _OrbitalPerm(Operator):
|
|
448
|
+
"""Relabel orbital labels per `orbital_map`. Fermion sign tracked
|
|
449
|
+
using a unified inversion count over the (orb, spin) tuple sequence,
|
|
450
|
+
which is correct for interleaved JW even when the permutation
|
|
451
|
+
crosses the α/β interleave."""
|
|
452
|
+
|
|
453
|
+
def __init__(self, orbital_map):
|
|
454
|
+
self.orbital_map = dict(orbital_map)
|
|
455
|
+
|
|
456
|
+
def _apply_internal(self, ab):
|
|
457
|
+
a, b = ab
|
|
458
|
+
# Original creation order in interleaved canonical JW.
|
|
459
|
+
orig_modes = sorted(
|
|
460
|
+
[(x, 0) for x in a] + [(x, 1) for x in b]
|
|
461
|
+
)
|
|
462
|
+
new_modes = [(self.orbital_map.get(o, o), s) for (o, s) in orig_modes]
|
|
463
|
+
if len(set(new_modes)) != len(new_modes):
|
|
464
|
+
return {}
|
|
465
|
+
new_a = frozenset(o for (o, s) in new_modes if s == 0)
|
|
466
|
+
new_b = frozenset(o for (o, s) in new_modes if s == 1)
|
|
467
|
+
return {(new_a, new_b): _inversion_sign(new_modes)}
|
|
468
|
+
|
|
469
|
+
def __repr__(self):
|
|
470
|
+
items = sorted(self.orbital_map.items())
|
|
471
|
+
return 'P[' + ','.join(f"{k}→{v}" for k, v in items) + ']'
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
class _Identity(Operator):
|
|
475
|
+
def _apply_internal(self, ab):
|
|
476
|
+
return {ab: 1}
|
|
477
|
+
|
|
478
|
+
def __repr__(self):
|
|
479
|
+
return 'I'
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def identity():
|
|
483
|
+
return _Identity()
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
# ---------------------------------------------------------------------------
|
|
487
|
+
# High-level constructors
|
|
488
|
+
# ---------------------------------------------------------------------------
|
|
489
|
+
|
|
490
|
+
def _spin_index(spin):
|
|
491
|
+
if spin in (0, 'a', 'alpha', 'α'):
|
|
492
|
+
return 0
|
|
493
|
+
if spin in (1, 'b', 'beta', 'β'):
|
|
494
|
+
return 1
|
|
495
|
+
raise ValueError(f"unknown spin label: {spin!r}")
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def c(orb, spin='alpha'):
|
|
499
|
+
"""Annihilation operator c_{orb,spin}."""
|
|
500
|
+
return _LadderOp(orb, _spin_index(spin), dagger=False)
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
def cdag(orb, spin='alpha'):
|
|
504
|
+
"""Creation operator c†_{orb,spin}."""
|
|
505
|
+
return _LadderOp(orb, _spin_index(spin), dagger=True)
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def number(orb, spin=None):
|
|
509
|
+
"""Occupation operator n̂_{orb,spin}; spin=None gives n̂_orb = n̂_α + n̂_β."""
|
|
510
|
+
if spin is None:
|
|
511
|
+
return number(orb, 'alpha') + number(orb, 'beta')
|
|
512
|
+
return cdag(orb, spin) @ c(orb, spin)
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def double_occ(orb):
|
|
516
|
+
"""Double-occupancy projector n̂_{orb,α} n̂_{orb,β}."""
|
|
517
|
+
return number(orb, 'alpha') @ number(orb, 'beta')
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def hop(i, j, spin=None, hermitian=True):
|
|
521
|
+
"""Hopping c†_i c_j (+ h.c. if hermitian); spin=None sums α and β."""
|
|
522
|
+
if spin is None:
|
|
523
|
+
return hop(i, j, 'alpha', hermitian) + hop(i, j, 'beta', hermitian)
|
|
524
|
+
fwd = cdag(i, spin) @ c(j, spin)
|
|
525
|
+
if not hermitian or i == j:
|
|
526
|
+
return fwd
|
|
527
|
+
return fwd + cdag(j, spin) @ c(i, spin)
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
# ---- spin operators ----
|
|
531
|
+
|
|
532
|
+
def s_z(orbs):
|
|
533
|
+
"""Total Sz = (1/2) Σᵢ (n̂_{i,α} − n̂_{i,β})."""
|
|
534
|
+
half = sp.Rational(1, 2)
|
|
535
|
+
return sum((half * (number(o, 'alpha') - number(o, 'beta')) for o in orbs),
|
|
536
|
+
start=0)
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def s_plus(orbs):
|
|
540
|
+
"""S₊ = Σᵢ c†_{i,α} c_{i,β}."""
|
|
541
|
+
return sum((cdag(o, 'alpha') @ c(o, 'beta') for o in orbs), start=0)
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
def s_minus(orbs):
|
|
545
|
+
"""S₋ = Σᵢ c†_{i,β} c_{i,α}."""
|
|
546
|
+
return sum((cdag(o, 'beta') @ c(o, 'alpha') for o in orbs), start=0)
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def s_squared(orbs):
|
|
550
|
+
"""S² = S₊ S₋ + S_z² − S_z."""
|
|
551
|
+
Sz = s_z(orbs)
|
|
552
|
+
return s_plus(orbs) @ s_minus(orbs) + Sz @ Sz - Sz
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
def s_dot(i, j):
|
|
556
|
+
"""Sᵢ · Sⱼ = Sᵢ_z Sⱼ_z + (1/2)(Sᵢ₊ Sⱼ₋ + Sᵢ₋ Sⱼ₊)."""
|
|
557
|
+
half = sp.Rational(1, 2)
|
|
558
|
+
Szi = half * (number(i, 'alpha') - number(i, 'beta'))
|
|
559
|
+
Szj = half * (number(j, 'alpha') - number(j, 'beta'))
|
|
560
|
+
Spi = cdag(i, 'alpha') @ c(i, 'beta')
|
|
561
|
+
Smi = cdag(i, 'beta') @ c(i, 'alpha')
|
|
562
|
+
Spj = cdag(j, 'alpha') @ c(j, 'beta')
|
|
563
|
+
Smj = cdag(j, 'beta') @ c(j, 'alpha')
|
|
564
|
+
return Szi @ Szj + half * (Spi @ Smj + Smi @ Spj)
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
# ---- η-pairing ----
|
|
568
|
+
|
|
569
|
+
def eta_plus(site_signs):
|
|
570
|
+
"""η₊ = Σᵢ sᵢ c†_{i,α} c†_{i,β}. `site_signs`: dict {orb: ±1}."""
|
|
571
|
+
return sum((s * (cdag(o, 'alpha') @ cdag(o, 'beta'))
|
|
572
|
+
for o, s in site_signs.items()), start=0)
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
def eta_minus(site_signs):
|
|
576
|
+
"""η₋ = Σᵢ sᵢ c_{i,β} c_{i,α}."""
|
|
577
|
+
return sum((s * (c(o, 'beta') @ c(o, 'alpha'))
|
|
578
|
+
for o, s in site_signs.items()), start=0)
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
def eta_z(orbs):
|
|
582
|
+
"""η_z = (1/2) (N̂ − L), with L = len(orbs)."""
|
|
583
|
+
half = sp.Rational(1, 2)
|
|
584
|
+
Nhat = sum((number(o) for o in orbs), start=0)
|
|
585
|
+
L = len(orbs)
|
|
586
|
+
return half * Nhat + (-half * L) * identity()
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
def eta_squared(site_signs):
|
|
590
|
+
"""η² = η₊ η₋ + η_z² − η_z."""
|
|
591
|
+
orbs = list(site_signs.keys())
|
|
592
|
+
Ez = eta_z(orbs)
|
|
593
|
+
return eta_plus(site_signs) @ eta_minus(site_signs) + Ez @ Ez - Ez
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
# ---- permutation / symmetry ----
|
|
597
|
+
|
|
598
|
+
def transposition(i, j):
|
|
599
|
+
"""Permute orbital labels i ↔ j."""
|
|
600
|
+
return _OrbitalPerm({i: j, j: i})
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
def orbital_perm(orbital_map):
|
|
604
|
+
"""Generic orbital-label permutation; map is a dict {old: new}."""
|
|
605
|
+
return _OrbitalPerm(orbital_map)
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
def _enumerate_orbital_group(generators):
|
|
609
|
+
"""Closure of the group generated by orbital-permutation maps."""
|
|
610
|
+
|
|
611
|
+
def compose(p, q):
|
|
612
|
+
keys = set(p.keys()) | set(q.keys())
|
|
613
|
+
return {k: p.get(q.get(k, k), q.get(k, k)) for k in keys}
|
|
614
|
+
|
|
615
|
+
def normalize(p):
|
|
616
|
+
return tuple(sorted((k, v) for k, v in p.items() if k != v))
|
|
617
|
+
|
|
618
|
+
seen = {(): {}}
|
|
619
|
+
queue = [{}]
|
|
620
|
+
while queue:
|
|
621
|
+
g = queue.pop()
|
|
622
|
+
for gen in generators:
|
|
623
|
+
new = compose(gen, g)
|
|
624
|
+
key = normalize(new)
|
|
625
|
+
if key not in seen:
|
|
626
|
+
seen[key] = new
|
|
627
|
+
queue.append(new)
|
|
628
|
+
return list(seen.values())
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
def reynolds_projector(generators):
|
|
632
|
+
"""P^{(triv)} = (1/|G|) Σ_g g, the trivial-irrep projector.
|
|
633
|
+
|
|
634
|
+
`generators` is a list of orbital-permutation dicts. Group closure
|
|
635
|
+
is computed automatically and fermion signs are tracked, so this
|
|
636
|
+
works on over-half-filled bases (e.g. C₄H₄²⁻) too.
|
|
637
|
+
"""
|
|
638
|
+
group = _enumerate_orbital_group(generators)
|
|
639
|
+
G = len(group)
|
|
640
|
+
inv_G = sp.Rational(1, G)
|
|
641
|
+
return sum((inv_G * orbital_perm(g) for g in group), start=0)
|
|
642
|
+
|
|
643
|
+
|
|
644
|
+
# ---- VB-flavoured composites ----
|
|
645
|
+
|
|
646
|
+
def singlet_proj(i, j):
|
|
647
|
+
"""Spin-singlet projector for two electrons distributed on (i, j).
|
|
648
|
+
|
|
649
|
+
P_S^{ij} = 1/4 − Sᵢ · Sⱼ. On the (1, 1) sector (one electron each on
|
|
650
|
+
i and j) this is the singlet-vs-triplet projector with eigenvalues
|
|
651
|
+
{0, 1}; outside that sector it is *not* a projector — interpret with
|
|
652
|
+
care (e.g. on a closed shell |aA⟩ it returns ¼ |aA⟩).
|
|
653
|
+
"""
|
|
654
|
+
return sp.Rational(1, 4) * identity() - s_dot(i, j)
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
def bond_singlet_creator(i, j):
|
|
658
|
+
"""Bond singlet creator: (1/√2)(c†_{i,α} c†_{j,β} − c†_{i,β} c†_{j,α}).
|
|
659
|
+
|
|
660
|
+
Acting on a state with i and j unoccupied appends a normalized
|
|
661
|
+
two-electron singlet bond on (i, j). Stacked applications build a
|
|
662
|
+
Rumer structure.
|
|
663
|
+
"""
|
|
664
|
+
return (sp.Rational(1, 1) / sp.sqrt(2)) * (
|
|
665
|
+
cdag(i, 'alpha') @ cdag(j, 'beta')
|
|
666
|
+
- cdag(i, 'beta') @ cdag(j, 'alpha')
|
|
667
|
+
)
|