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
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import itertools
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
sys.setrecursionlimit(30000)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class OrbitalPermutations:
|
|
9
|
+
# will return permutations of vector [0,1,..,n-1] together with parity signs
|
|
10
|
+
def __init__(self, n):
|
|
11
|
+
self.n = n
|
|
12
|
+
self.permutations = []
|
|
13
|
+
self.permutation_signs = []
|
|
14
|
+
|
|
15
|
+
self.get_permutation_indices(n)
|
|
16
|
+
|
|
17
|
+
def __iter__(self):
|
|
18
|
+
return zip(self.permutations, self.permutation_signs)
|
|
19
|
+
|
|
20
|
+
def N_flips(self, v):
|
|
21
|
+
# Computes the number of permutations from the consecutive order
|
|
22
|
+
if len(v) == 0:
|
|
23
|
+
return 0
|
|
24
|
+
v2 = list(v).copy()
|
|
25
|
+
|
|
26
|
+
# find 1 and swap elements
|
|
27
|
+
i1 = v2.index(0)
|
|
28
|
+
v2[i1] = v2[0]
|
|
29
|
+
v2[0] = 1
|
|
30
|
+
|
|
31
|
+
v2 = [x - 1 for x in v2]
|
|
32
|
+
add_perm = 0 if i1 == 0 else 1
|
|
33
|
+
|
|
34
|
+
return self.N_flips(v2[1:]) + add_perm
|
|
35
|
+
|
|
36
|
+
def parity_sign(self, v):
|
|
37
|
+
# Returns the parity (+1/-1) of a given permutation vector
|
|
38
|
+
n = self.N_flips(v)
|
|
39
|
+
if (n % 2) == 0:
|
|
40
|
+
return 1
|
|
41
|
+
else:
|
|
42
|
+
return -1
|
|
43
|
+
|
|
44
|
+
def get_permutation_indices(self, n):
|
|
45
|
+
# gets the permutation indices and permutation signs
|
|
46
|
+
if n == 0:
|
|
47
|
+
n = 1
|
|
48
|
+
self.permutations = list(itertools.permutations(range(n)))
|
|
49
|
+
self.permutation_signs = [self.parity_sign(v) for v in self.permutations]
|
symvb/slaterdet.py
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
from symvb.functions import sorti
|
|
2
|
+
from symvb.orbital_permutations import OrbitalPermutations
|
|
3
|
+
import symvb
|
|
4
|
+
import logging
|
|
5
|
+
|
|
6
|
+
logging.basicConfig(format=('%(levelname)-8s: %(message)s'))
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SlaterDet:
|
|
10
|
+
"""
|
|
11
|
+
Parses the Slater determinant string |s| and computes the <det1|O|det2> elements
|
|
12
|
+
"""
|
|
13
|
+
def __init__(self, s=""):
|
|
14
|
+
self.det_string = s
|
|
15
|
+
self.alpha_indices = []
|
|
16
|
+
self.alpha_string = ''
|
|
17
|
+
self.beta_indices = []
|
|
18
|
+
self.beta_string = ''
|
|
19
|
+
self.spins = ''
|
|
20
|
+
self.Nel = 0
|
|
21
|
+
if len(self.det_string) > 0:
|
|
22
|
+
self.parse_det()
|
|
23
|
+
|
|
24
|
+
def __repr__(self):
|
|
25
|
+
s = '|%s|' % self.det_string
|
|
26
|
+
return s
|
|
27
|
+
|
|
28
|
+
# Description of the magic functions: https://docs.python.org/3/reference/datamodel.html
|
|
29
|
+
def __add__(self, other):
|
|
30
|
+
return symvb.FixedPsi(self) + other
|
|
31
|
+
|
|
32
|
+
def __sub__(self, other):
|
|
33
|
+
if other.__class__.__name__ == 'SlaterDet' and self.det_string == other.det_string:
|
|
34
|
+
return SlaterDet()
|
|
35
|
+
return symvb.FixedPsi(self) + (-1) * other
|
|
36
|
+
|
|
37
|
+
def __rsub__(self, other):
|
|
38
|
+
return symvb.FixedPsi(other) + (-1) * symvb.FixedPsi(self)
|
|
39
|
+
|
|
40
|
+
def __neg__(self):
|
|
41
|
+
return (-1) * symvb.FixedPsi(self)
|
|
42
|
+
|
|
43
|
+
def __mul__(self, other):
|
|
44
|
+
if isinstance(other, int) or isinstance(other, float):
|
|
45
|
+
return symvb.FixedPsi(self) * other
|
|
46
|
+
if other.__class__.__name__ == 'SlaterDet':
|
|
47
|
+
# make sure we do not place two electrons on the same orbital
|
|
48
|
+
if len(set(self.alpha_string).intersection(set(other.alpha_string))) > 0:
|
|
49
|
+
return SlaterDet()
|
|
50
|
+
if len(set(self.beta_string).intersection(set(other.beta_string))) > 0:
|
|
51
|
+
return SlaterDet()
|
|
52
|
+
return SlaterDet(self.det_string + other.det_string)
|
|
53
|
+
if other.__class__.__name__ == 'FixedPsi':
|
|
54
|
+
return symvb.FixedPsi(self) * other
|
|
55
|
+
return NotImplemented
|
|
56
|
+
|
|
57
|
+
def __rmul__(self, other):
|
|
58
|
+
if isinstance(other, int) or isinstance(other, float):
|
|
59
|
+
return symvb.FixedPsi(self) * other
|
|
60
|
+
|
|
61
|
+
def parse_det(self):
|
|
62
|
+
s = self.det_string
|
|
63
|
+
if len(s) == 0:
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
i = 0
|
|
67
|
+
for c in s:
|
|
68
|
+
if c.islower():
|
|
69
|
+
assert c not in self.alpha_string # two electrons cannot occupy the same spinorbital
|
|
70
|
+
self.alpha_indices.append(i)
|
|
71
|
+
self.spins += '+'
|
|
72
|
+
self.alpha_string += c
|
|
73
|
+
else:
|
|
74
|
+
assert c not in self.beta_string # two electrons cannot occupy the same spinorbital
|
|
75
|
+
self.beta_indices.append(i)
|
|
76
|
+
self.spins += '-'
|
|
77
|
+
self.beta_string += c
|
|
78
|
+
i = i + 1
|
|
79
|
+
self.Nel = i
|
|
80
|
+
|
|
81
|
+
def get_orbital_permutations(self):
|
|
82
|
+
# gets all spin-restricted permutations of orbital products
|
|
83
|
+
A = OrbitalPermutations(len(self.alpha_indices))
|
|
84
|
+
B = OrbitalPermutations(len(self.beta_indices))
|
|
85
|
+
|
|
86
|
+
dets = []
|
|
87
|
+
signs = []
|
|
88
|
+
for a_orbs, a_sign in A:
|
|
89
|
+
for b_orbs, b_sign in B:
|
|
90
|
+
i_a = 0
|
|
91
|
+
i_b = 0
|
|
92
|
+
s = ''
|
|
93
|
+
for i in range(len(self.det_string)):
|
|
94
|
+
if self.spins[i] == '+':
|
|
95
|
+
c = self.alpha_string[a_orbs[i_a]]
|
|
96
|
+
i_a += 1
|
|
97
|
+
else:
|
|
98
|
+
c = self.beta_string[b_orbs[i_b]]
|
|
99
|
+
i_b += 1
|
|
100
|
+
s = s + c
|
|
101
|
+
dets.append(s)
|
|
102
|
+
signs.append(a_sign * b_sign)
|
|
103
|
+
|
|
104
|
+
return [dets, signs]
|
|
105
|
+
|
|
106
|
+
def is_compatible(self, R):
|
|
107
|
+
if self.Nel != R.Nel:
|
|
108
|
+
# logging.warning('Different number of electrons: %i vs %i' % (self.Nel, R.Nel))
|
|
109
|
+
return False
|
|
110
|
+
if self.spins != R.spins:
|
|
111
|
+
# logging.warning('The determinant spins are incompatible: %s vs %s' % (self.spins, R.spins))
|
|
112
|
+
return False
|
|
113
|
+
return True
|
|
114
|
+
|
|
115
|
+
def get_sorted(self):
|
|
116
|
+
# Sorts orbital labels within alpha and beta blocks into alphabetic order
|
|
117
|
+
# (spin pattern unchanged). Returns FixedPsi with the sign from the
|
|
118
|
+
# required orbital permutation folded into the coefficient.
|
|
119
|
+
sa, ia = sorti(self.alpha_string)
|
|
120
|
+
sb, ib = sorti(self.beta_string)
|
|
121
|
+
|
|
122
|
+
s = self.det_string
|
|
123
|
+
for i in range(len(self.alpha_indices)):
|
|
124
|
+
j = self.alpha_indices[i]
|
|
125
|
+
s = s[:j] + sa[i] + s[j + 1:]
|
|
126
|
+
|
|
127
|
+
for i in range(len(self.beta_indices)):
|
|
128
|
+
j = self.beta_indices[i]
|
|
129
|
+
s = s[:j] + sb[i] + s[j + 1:]
|
|
130
|
+
|
|
131
|
+
# Construct a fresh SlaterDet from the sorted string so internal
|
|
132
|
+
# fields (alpha_indices / alpha_string / etc.) reflect the new ordering.
|
|
133
|
+
d = SlaterDet(s)
|
|
134
|
+
|
|
135
|
+
coef = 1 if (ia + ib) % 2 == 0 else -1
|
|
136
|
+
fp = symvb.FixedPsi()
|
|
137
|
+
fp.add_det(d, coef=coef)
|
|
138
|
+
return fp
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
if __name__ == '__main__':
|
|
142
|
+
print(SlaterDet('AbCd').spins)
|
symvb/spin.py
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Total-spin (S^2) machinery: construct the S^2 matrix over a Slater-det
|
|
3
|
+
basis with fixed Sz and project onto a chosen total-spin eigenspace.
|
|
4
|
+
|
|
5
|
+
For the Sz = 0 subspace,
|
|
6
|
+
|
|
7
|
+
S^2 = S_+ S_- (the S_z^2 - S_z piece vanishes)
|
|
8
|
+
= sum_{i, j} c^+_{i alpha} c_{i beta} c^+_{j beta} c_{j alpha}.
|
|
9
|
+
|
|
10
|
+
The action of S^2 on a Slater det |D> is computed by applying the four
|
|
11
|
+
second-quantized operators in sequence, tracking fermion signs through
|
|
12
|
+
a canonical spin-orbital ordering (alpha_1, beta_1, alpha_2, beta_2, ...).
|
|
13
|
+
The resulting matrix element <D'|S^2|D> is summed over all orbital
|
|
14
|
+
pairs (i, j) to build the full S^2 matrix.
|
|
15
|
+
|
|
16
|
+
Given an S^2 matrix over any Slater-det basis, `project_onto_S(H, S2, S)`
|
|
17
|
+
returns the Hamiltonian block projected onto the S(S+1) eigenspace.
|
|
18
|
+
"""
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import numpy as np
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _canon_det(alpha_set, beta_set):
|
|
25
|
+
"""
|
|
26
|
+
Canonical det string: sort alpha and beta sets, interleave in pairs
|
|
27
|
+
matching symvb.functions.generate_det_strings. Returns None if
|
|
28
|
+
n_alpha != n_beta (our caller will work with balanced dets only).
|
|
29
|
+
"""
|
|
30
|
+
a = sorted(alpha_set)
|
|
31
|
+
b = sorted(beta_set)
|
|
32
|
+
if len(a) != len(b):
|
|
33
|
+
return None
|
|
34
|
+
return ''.join(a[k] + b[k].upper() for k in range(len(a)))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _spin_orbital_list(alpha_set, beta_set):
|
|
38
|
+
"""Canonical ordering: alpha_1, beta_1, alpha_2, beta_2, ... sorted by orbital."""
|
|
39
|
+
orbs = sorted(alpha_set | beta_set)
|
|
40
|
+
return [(orb, s) for orb in orbs for s in (0, 1) if
|
|
41
|
+
(s == 0 and orb in alpha_set) or (s == 1 and orb in beta_set)]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _apply_c(alpha, beta, orb, spin, create):
|
|
45
|
+
"""
|
|
46
|
+
Apply c_{orb, spin} or c^+_{orb, spin} to the det specified by the
|
|
47
|
+
alpha/beta orbital sets. Returns (new_alpha, new_beta, sign) or
|
|
48
|
+
(None, None, 0) if annihilated. sign = (-1)^{# occupied spin-orbitals
|
|
49
|
+
before (orb, spin) in the canonical ordering}.
|
|
50
|
+
|
|
51
|
+
Canonical ordering: for each orbital in alphabetical order, alpha first
|
|
52
|
+
then beta.
|
|
53
|
+
"""
|
|
54
|
+
present = (orb in alpha) if spin == 0 else (orb in beta)
|
|
55
|
+
if create == present: # trying to create an occupied slot, or annihilate empty
|
|
56
|
+
return None, None, 0
|
|
57
|
+
|
|
58
|
+
# Jordan-Wigner sign: count occupied spin-orbitals strictly before (orb, spin)
|
|
59
|
+
count = 0
|
|
60
|
+
for x in sorted(alpha | beta):
|
|
61
|
+
if x < orb:
|
|
62
|
+
count += int(x in alpha) + int(x in beta)
|
|
63
|
+
elif x == orb:
|
|
64
|
+
if spin == 1 and x in alpha:
|
|
65
|
+
count += 1
|
|
66
|
+
sign = 1 if count % 2 == 0 else -1
|
|
67
|
+
|
|
68
|
+
new_alpha = alpha.copy()
|
|
69
|
+
new_beta = beta.copy()
|
|
70
|
+
if spin == 0:
|
|
71
|
+
if create:
|
|
72
|
+
new_alpha.add(orb)
|
|
73
|
+
else:
|
|
74
|
+
new_alpha.discard(orb)
|
|
75
|
+
else:
|
|
76
|
+
if create:
|
|
77
|
+
new_beta.add(orb)
|
|
78
|
+
else:
|
|
79
|
+
new_beta.discard(orb)
|
|
80
|
+
return new_alpha, new_beta, sign
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _apply_s_pl_s_mi(orbs, alpha, beta):
|
|
84
|
+
"""
|
|
85
|
+
Apply S_+ S_- = sum_{i, j} c^+_{i a} c_{i b} c^+_{j b} c_{j a} to |D>.
|
|
86
|
+
Returns a dict { canonical_det_string : signed_coefficient, ... }.
|
|
87
|
+
`orbs` is the list of orbital labels to iterate over.
|
|
88
|
+
"""
|
|
89
|
+
out = {}
|
|
90
|
+
for j in orbs:
|
|
91
|
+
# c_{j alpha}
|
|
92
|
+
a1, b1, s1 = _apply_c(alpha, beta, j, 0, create=False)
|
|
93
|
+
if a1 is None:
|
|
94
|
+
continue
|
|
95
|
+
for i in orbs:
|
|
96
|
+
# c^+_{j beta}
|
|
97
|
+
a2, b2, s2 = _apply_c(a1, b1, j, 1, create=True)
|
|
98
|
+
if a2 is None:
|
|
99
|
+
continue
|
|
100
|
+
# c_{i beta}
|
|
101
|
+
a3, b3, s3 = _apply_c(a2, b2, i, 1, create=False)
|
|
102
|
+
if a3 is None:
|
|
103
|
+
continue
|
|
104
|
+
# c^+_{i alpha}
|
|
105
|
+
a4, b4, s4 = _apply_c(a3, b3, i, 0, create=True)
|
|
106
|
+
if a4 is None:
|
|
107
|
+
continue
|
|
108
|
+
sign = s1 * s2 * s3 * s4
|
|
109
|
+
key = _canon_det(a4, b4)
|
|
110
|
+
if key is None:
|
|
111
|
+
continue
|
|
112
|
+
out[key] = out.get(key, 0) + sign
|
|
113
|
+
return out
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _symvb_to_canonical_sign(det_string, orbital_index):
|
|
117
|
+
"""
|
|
118
|
+
Sign relating the symvb Slater det (creation order = det_string order)
|
|
119
|
+
to the canonical Jordan-Wigner ordering (alphabetical orbital, alpha
|
|
120
|
+
before beta for each orbital).
|
|
121
|
+
|
|
122
|
+
For det_string 'x0 X1 x2 X3 ...' the creation order is
|
|
123
|
+
(x0, alpha), (X1, beta), (x2, alpha), (X3, beta), ...
|
|
124
|
+
To put in canonical ascending order we count inversions in the list
|
|
125
|
+
of spin-orbital indices (2 * orb_index + spin).
|
|
126
|
+
"""
|
|
127
|
+
indices = []
|
|
128
|
+
for pos, c in enumerate(det_string):
|
|
129
|
+
orb = c.lower()
|
|
130
|
+
spin = 0 if c.islower() else 1
|
|
131
|
+
indices.append(2 * orbital_index[orb] + spin)
|
|
132
|
+
inv = 0
|
|
133
|
+
for a in range(len(indices)):
|
|
134
|
+
for b in range(a + 1, len(indices)):
|
|
135
|
+
if indices[a] > indices[b]:
|
|
136
|
+
inv += 1
|
|
137
|
+
return 1 if inv % 2 == 0 else -1
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def s_squared_matrix(det_strings, orbs=None):
|
|
141
|
+
"""
|
|
142
|
+
Build the S^2 matrix in a Slater-det basis at Sz = 0.
|
|
143
|
+
|
|
144
|
+
Thin wrapper around `symvb.operators.s_squared(orbs).matrix(...)`;
|
|
145
|
+
kept for backward compatibility. Returns a numpy float ndarray with
|
|
146
|
+
eigenvalues S(S+1).
|
|
147
|
+
"""
|
|
148
|
+
from symvb import operators as _op
|
|
149
|
+
if orbs is None:
|
|
150
|
+
orbs = sorted({ch.lower() for ds in det_strings for ch in ds})
|
|
151
|
+
return np.array(_op.s_squared(orbs).matrix(det_strings), dtype=float)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _apply_eta_pl_eta_mi(orbs, site_sign, alpha, beta):
|
|
155
|
+
"""
|
|
156
|
+
Apply eta_+ eta_- = sum_{i,j} s_i s_j c^+_{i alpha} c^+_{i beta}
|
|
157
|
+
c_{j beta} c_{j alpha}
|
|
158
|
+
to |D> (alpha, beta occupation sets), where s_i = site_sign[i] = +/-1.
|
|
159
|
+
Returns a dict { canonical_det_string : signed_coefficient, ... }.
|
|
160
|
+
"""
|
|
161
|
+
out = {}
|
|
162
|
+
for j in orbs:
|
|
163
|
+
# c_{j alpha}
|
|
164
|
+
a1, b1, s1 = _apply_c(alpha, beta, j, 0, create=False)
|
|
165
|
+
if a1 is None:
|
|
166
|
+
continue
|
|
167
|
+
# c_{j beta}
|
|
168
|
+
a2, b2, s2 = _apply_c(a1, b1, j, 1, create=False)
|
|
169
|
+
if a2 is None:
|
|
170
|
+
continue
|
|
171
|
+
for i in orbs:
|
|
172
|
+
# c^+_{i beta}
|
|
173
|
+
a3, b3, s3 = _apply_c(a2, b2, i, 1, create=True)
|
|
174
|
+
if a3 is None:
|
|
175
|
+
continue
|
|
176
|
+
# c^+_{i alpha}
|
|
177
|
+
a4, b4, s4 = _apply_c(a3, b3, i, 0, create=True)
|
|
178
|
+
if a4 is None:
|
|
179
|
+
continue
|
|
180
|
+
sign = s1 * s2 * s3 * s4 * site_sign[i] * site_sign[j]
|
|
181
|
+
key = _canon_det(a4, b4)
|
|
182
|
+
if key is None:
|
|
183
|
+
continue
|
|
184
|
+
out[key] = out.get(key, 0) + sign
|
|
185
|
+
return out
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def eta_squared_matrix(det_strings, site_signs, orbs=None):
|
|
189
|
+
"""
|
|
190
|
+
Build the eta-pseudospin squared matrix eta^2 in a Slater-det basis
|
|
191
|
+
at eta_z = (N - L) / 2 = 0 (half-filling on a bipartite lattice).
|
|
192
|
+
|
|
193
|
+
The eta operators are
|
|
194
|
+
eta_+ = sum_i s_i c^+_{i alpha} c^+_{i beta}
|
|
195
|
+
eta_- = (eta_+)^dagger
|
|
196
|
+
eta_z = (N_total - L) / 2
|
|
197
|
+
where s_i = +/-1 is the sublattice sign. On a bipartite nearest-
|
|
198
|
+
neighbor hopping lattice at half-filling, [H, eta_+-] = 0; on adding
|
|
199
|
+
Hubbard U this reduces to [H, eta_z] = 0 only.
|
|
200
|
+
|
|
201
|
+
Parameters
|
|
202
|
+
----------
|
|
203
|
+
det_strings : list of symvb Slater-det strings, all at N = L.
|
|
204
|
+
site_signs : dict {orbital_label: +1 or -1} encoding the bipartition.
|
|
205
|
+
orbs : optional list of orbital labels (inferred from site_signs if
|
|
206
|
+
omitted).
|
|
207
|
+
|
|
208
|
+
Returns
|
|
209
|
+
-------
|
|
210
|
+
eta2 : (N, N) ndarray. Eigenvalues are eta(eta+1) for eta = 0, 1, 2, ...
|
|
211
|
+
|
|
212
|
+
Thin wrapper around `symvb.operators.eta_squared(...).matrix(...)`.
|
|
213
|
+
"""
|
|
214
|
+
from symvb import operators as _op
|
|
215
|
+
return np.array(_op.eta_squared(site_signs).matrix(det_strings), dtype=float)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def project_onto_S(H, S2, target_S, tol=1e-8):
|
|
219
|
+
"""
|
|
220
|
+
Project H onto the eigenspace of S^2 with eigenvalue target_S * (target_S + 1).
|
|
221
|
+
|
|
222
|
+
Returns (H_block, U) where U has orthonormal columns spanning the
|
|
223
|
+
target-S subspace and H_block = U^T H U.
|
|
224
|
+
"""
|
|
225
|
+
target = target_S * (target_S + 1)
|
|
226
|
+
evals, evecs = np.linalg.eigh((S2 + S2.T) / 2)
|
|
227
|
+
mask = np.abs(evals - target) < tol
|
|
228
|
+
if not mask.any():
|
|
229
|
+
raise ValueError(f'no S^2 eigenvalues near S(S+1) = {target} '
|
|
230
|
+
f'(spectrum: {sorted(set(round(e, 6) for e in evals))})')
|
|
231
|
+
U = evecs[:, mask]
|
|
232
|
+
H_block = U.T @ H @ U
|
|
233
|
+
return H_block, U
|