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/huckel.py
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Symbolic Huckel solver.
|
|
3
|
+
|
|
4
|
+
Given a one-electron tight-binding graph (adjacency + uniform resonance
|
|
5
|
+
integral h, uniform overlap s), produce the molecular orbitals and
|
|
6
|
+
energies symbolically. The MO eigenvectors of the adjacency matrix are
|
|
7
|
+
simultaneously eigenvectors of the GHEP H c = eps S c whenever
|
|
8
|
+
S = I + (s/h) H; in that case the energies follow as
|
|
9
|
+
|
|
10
|
+
eps_k(s) = h * lambda_k / (1 + s * lambda_k), lambda_k = adjacency
|
|
11
|
+
eigenvalue.
|
|
12
|
+
|
|
13
|
+
This module returns those quantities in `HuckelResult`. Coefficients are
|
|
14
|
+
RAW integer / algebraic-number entries (no 1/sqrt(L) prefactor): the
|
|
15
|
+
global normalization is a scalar that cancels in any eigenvalue
|
|
16
|
+
identity, and rationals/surds keep SymPy fast.
|
|
17
|
+
|
|
18
|
+
Special cases are hand-coded for cyclic L in {3, 4, 5, 6} so that the
|
|
19
|
+
coefficient matrices agree with the manuscript scripts (matching
|
|
20
|
+
benzene's real-cos/sin Bloch basis, Cp's golden-ratio entries, etc).
|
|
21
|
+
General graphs fall through to `sympy.Matrix.eigenvects`.
|
|
22
|
+
"""
|
|
23
|
+
from dataclasses import dataclass, field
|
|
24
|
+
from typing import Optional
|
|
25
|
+
|
|
26
|
+
import sympy
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class HuckelGHEPMismatchError(ValueError):
|
|
30
|
+
"""Raised when a user-supplied overlap matrix does not satisfy the
|
|
31
|
+
S = I + (s/h) H precondition that lets MOs of H simultaneously
|
|
32
|
+
diagonalize the GHEP."""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class HuckelResult:
|
|
37
|
+
"""Symbolic output of a Huckel solve.
|
|
38
|
+
|
|
39
|
+
Fields:
|
|
40
|
+
site_labels : tuple of site names (AO ordering used).
|
|
41
|
+
eigenvalues : tuple of adjacency-matrix eigenvalues lambda_k
|
|
42
|
+
(parameter-free sympy expressions; rationals or
|
|
43
|
+
algebraic numbers).
|
|
44
|
+
energies : tuple of MO energies eps_k(h, s) = h*lambda_k /
|
|
45
|
+
(1 + s*lambda_k), as sympy expressions in
|
|
46
|
+
(h_symbol, s_symbol).
|
|
47
|
+
coefficients : sympy Matrix of shape (n_mo, n_sites). Row k is
|
|
48
|
+
MO k expanded over the AO basis, in raw
|
|
49
|
+
integer/algebraic form (NOT AO-orthonormalized).
|
|
50
|
+
h_symbol : the sympy.Symbol for the resonance integral.
|
|
51
|
+
s_symbol : the sympy.Symbol for the AO overlap.
|
|
52
|
+
point_group : optional point-group label (e.g. 'D_6h').
|
|
53
|
+
irrep_labels : optional per-MO irrep tags (e.g. ('a_2u', 'e_1g',
|
|
54
|
+
'e_1g', 'e_2u', 'e_2u', 'b_2g')).
|
|
55
|
+
"""
|
|
56
|
+
site_labels: tuple
|
|
57
|
+
eigenvalues: tuple
|
|
58
|
+
energies: tuple
|
|
59
|
+
coefficients: sympy.Matrix
|
|
60
|
+
h_symbol: sympy.Symbol
|
|
61
|
+
s_symbol: sympy.Symbol
|
|
62
|
+
point_group: Optional[str] = None
|
|
63
|
+
irrep_labels: Optional[tuple] = None
|
|
64
|
+
|
|
65
|
+
def energy_of_occupation(self, occupation):
|
|
66
|
+
"""Return the total one-electron energy for a given MO
|
|
67
|
+
occupation. `occupation` is an iterable of (mo_index,
|
|
68
|
+
n_electrons) pairs OR a flat iterable of length n_mo with
|
|
69
|
+
per-MO electron counts."""
|
|
70
|
+
occ = list(occupation)
|
|
71
|
+
if occ and not isinstance(occ[0], (tuple, list)):
|
|
72
|
+
assert len(occ) == len(self.energies)
|
|
73
|
+
return sympy.Add(*(n * e for n, e in zip(occ, self.energies)))
|
|
74
|
+
return sympy.Add(*(n * self.energies[k] for k, n in occ))
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _resolve_symbol(x, default_name):
|
|
78
|
+
if isinstance(x, sympy.Symbol):
|
|
79
|
+
return x
|
|
80
|
+
return sympy.Symbol(x)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _energies_from_eigenvalues(lambdas, h, s):
|
|
84
|
+
return tuple(h * lam / (1 + s * lam) for lam in lambdas)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _ring_special_case(L):
|
|
88
|
+
"""Return (eigenvalues, coefficients, irrep_labels) for L in
|
|
89
|
+
{3, 4, 5, 6} with a hand-picked real Bloch basis whose entries are
|
|
90
|
+
integers (or, for L=5, golden-ratio surds). Coefficients are NOT
|
|
91
|
+
AO-orthonormalized; row scaling is chosen for memorable rationals.
|
|
92
|
+
|
|
93
|
+
Conventions match the existing manuscript example scripts
|
|
94
|
+
(benzene_1e_analytical_overlap.py, etc).
|
|
95
|
+
"""
|
|
96
|
+
if L == 3:
|
|
97
|
+
# C_3 triangle. lambda = {2, -1, -1}.
|
|
98
|
+
# Real basis: totally symmetric, plus two e-type orthogonal vectors.
|
|
99
|
+
eigs = (sympy.Integer(2), sympy.Integer(-1), sympy.Integer(-1))
|
|
100
|
+
coeffs = sympy.Matrix([
|
|
101
|
+
[1, 1, 1],
|
|
102
|
+
[2, -1, -1],
|
|
103
|
+
[0, 1, -1],
|
|
104
|
+
])
|
|
105
|
+
irreps = ('a', 'e', 'e')
|
|
106
|
+
return eigs, coeffs, irreps
|
|
107
|
+
|
|
108
|
+
if L == 4:
|
|
109
|
+
# C_4 square. lambda = {2, 0, 0, -2}.
|
|
110
|
+
eigs = (sympy.Integer(2), sympy.Integer(0),
|
|
111
|
+
sympy.Integer(0), sympy.Integer(-2))
|
|
112
|
+
coeffs = sympy.Matrix([
|
|
113
|
+
[1, 1, 1, 1],
|
|
114
|
+
[1, 0, -1, 0],
|
|
115
|
+
[0, 1, 0, -1],
|
|
116
|
+
[1, -1, 1, -1],
|
|
117
|
+
])
|
|
118
|
+
irreps = ('a_g', 'e_u', 'e_u', 'b_g')
|
|
119
|
+
return eigs, coeffs, irreps
|
|
120
|
+
|
|
121
|
+
if L == 5:
|
|
122
|
+
# Pentagon. lambda = {2, 2cos(72), 2cos(72), 2cos(144), 2cos(144)}
|
|
123
|
+
# = {2, (sqrt(5)-1)/2, (sqrt(5)-1)/2, -(sqrt(5)+1)/2, -(sqrt(5)+1)/2}
|
|
124
|
+
sqrt5 = sympy.sqrt(5)
|
|
125
|
+
phi_inv = (sqrt5 - 1) / 2 # 2 cos(72)
|
|
126
|
+
neg_phi = -(sqrt5 + 1) / 2 # 2 cos(144)
|
|
127
|
+
eigs = (sympy.Integer(2), phi_inv, phi_inv, neg_phi, neg_phi)
|
|
128
|
+
# Real Bloch coefficients: cos(2 pi m j / 5) and sin(2 pi m j / 5).
|
|
129
|
+
# Using sympy.cos/sin then simplifying gives surds in Q[sqrt(5)].
|
|
130
|
+
coeffs_rows = [[sympy.Integer(1)] * 5]
|
|
131
|
+
for m in (1, 2):
|
|
132
|
+
cos_row = [sympy.simplify(sympy.cos(2 * sympy.pi * m * j / 5))
|
|
133
|
+
for j in range(5)]
|
|
134
|
+
sin_row = [sympy.simplify(sympy.sin(2 * sympy.pi * m * j / 5))
|
|
135
|
+
for j in range(5)]
|
|
136
|
+
coeffs_rows.append(cos_row)
|
|
137
|
+
coeffs_rows.append(sin_row)
|
|
138
|
+
coeffs = sympy.Matrix(coeffs_rows)
|
|
139
|
+
irreps = ('a', 'e_1', 'e_1', 'e_2', 'e_2')
|
|
140
|
+
return eigs, coeffs, irreps
|
|
141
|
+
|
|
142
|
+
if L == 6:
|
|
143
|
+
# Benzene hexagon. lambda = {2, 1, 1, -1, -1, -2}.
|
|
144
|
+
# Real basis with sqrt(3)/2 absorbed into row scaling so all
|
|
145
|
+
# entries are rational (matches benzene_1e_analytical_overlap.py).
|
|
146
|
+
eigs = (sympy.Integer(2),
|
|
147
|
+
sympy.Integer(1), sympy.Integer(1),
|
|
148
|
+
sympy.Integer(-1), sympy.Integer(-1),
|
|
149
|
+
sympy.Integer(-2))
|
|
150
|
+
coeffs = sympy.Matrix([
|
|
151
|
+
[1, 1, 1, 1, 1, 1], # k=0, a_2u
|
|
152
|
+
[sympy.Rational(2), sympy.Integer(1), sympy.Integer(-1),
|
|
153
|
+
sympy.Integer(-2), sympy.Integer(-1), sympy.Integer(1)], # k=1c
|
|
154
|
+
[sympy.Integer(0), sympy.Integer(1), sympy.Integer(1),
|
|
155
|
+
sympy.Integer(0), sympy.Integer(-1), sympy.Integer(-1)], # k=1s
|
|
156
|
+
[sympy.Rational(2), sympy.Integer(-1), sympy.Integer(-1),
|
|
157
|
+
sympy.Integer(2), sympy.Integer(-1), sympy.Integer(-1)], # k=2c
|
|
158
|
+
[sympy.Integer(0), sympy.Integer(1), sympy.Integer(-1),
|
|
159
|
+
sympy.Integer(0), sympy.Integer(1), sympy.Integer(-1)], # k=2s
|
|
160
|
+
[sympy.Integer(1), sympy.Integer(-1), sympy.Integer(1),
|
|
161
|
+
sympy.Integer(-1), sympy.Integer(1), sympy.Integer(-1)], # k=3
|
|
162
|
+
])
|
|
163
|
+
irreps = ('a_2u', 'e_1g', 'e_1g', 'e_2u', 'e_2u', 'b_2g')
|
|
164
|
+
return eigs, coeffs, irreps
|
|
165
|
+
|
|
166
|
+
return None # caller falls through to generic path
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _generic_solve(adjacency_matrix, site_labels, h, s):
|
|
170
|
+
"""Diagonalize an arbitrary adjacency matrix symbolically via
|
|
171
|
+
sympy.Matrix.eigenvects. Returns (eigenvalues, coefficients) with
|
|
172
|
+
rows = MO eigenvectors, sorted by descending eigenvalue."""
|
|
173
|
+
A = sympy.Matrix(adjacency_matrix)
|
|
174
|
+
if A.rows != A.cols or A.rows != len(site_labels):
|
|
175
|
+
raise ValueError(
|
|
176
|
+
f"adjacency shape {A.shape} inconsistent with "
|
|
177
|
+
f"{len(site_labels)} site labels")
|
|
178
|
+
|
|
179
|
+
raw = A.eigenvects() # list of (eigenvalue, multiplicity, basis)
|
|
180
|
+
|
|
181
|
+
triples = []
|
|
182
|
+
for eig, mult, basis in raw:
|
|
183
|
+
for vec in basis:
|
|
184
|
+
triples.append((sympy.simplify(eig), vec))
|
|
185
|
+
|
|
186
|
+
def _sort_key(eig):
|
|
187
|
+
try:
|
|
188
|
+
return -float(eig)
|
|
189
|
+
except (TypeError, ValueError):
|
|
190
|
+
return -float(sympy.N(eig))
|
|
191
|
+
|
|
192
|
+
triples.sort(key=lambda t: _sort_key(t[0]))
|
|
193
|
+
|
|
194
|
+
eigs = tuple(t[0] for t in triples)
|
|
195
|
+
rows = [list(t[1]) for t in triples]
|
|
196
|
+
rows = [[entry[0] if hasattr(entry, '__iter__') else entry
|
|
197
|
+
for entry in row] for row in rows]
|
|
198
|
+
coeffs = sympy.Matrix(rows)
|
|
199
|
+
return eigs, coeffs
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _check_ghep_precondition(adjacency, overlap, h, s):
|
|
203
|
+
"""If user supplies an explicit overlap matrix, verify
|
|
204
|
+
overlap == I + (s/h)*adjacency. Raise HuckelGHEPMismatchError if not."""
|
|
205
|
+
if overlap is None:
|
|
206
|
+
return
|
|
207
|
+
A = sympy.Matrix(adjacency)
|
|
208
|
+
S_user = sympy.Matrix(overlap)
|
|
209
|
+
if S_user.shape != A.shape:
|
|
210
|
+
raise ValueError(f"overlap shape {S_user.shape} != adjacency {A.shape}")
|
|
211
|
+
expected = sympy.eye(A.rows) + (s / h) * A
|
|
212
|
+
diff = sympy.simplify(S_user - expected)
|
|
213
|
+
if diff != sympy.zeros(*A.shape):
|
|
214
|
+
raise HuckelGHEPMismatchError(
|
|
215
|
+
"user-supplied overlap does not satisfy S = I + (s/h)*H. "
|
|
216
|
+
"The MO-direct shortcut requires this form; for richer "
|
|
217
|
+
"overlap structures, solve the full GHEP elsewhere.")
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def solve_ring(L, h='h', s='s', basis='real', site_labels=None):
|
|
221
|
+
"""Solve the cyclic Huckel ring of L sites.
|
|
222
|
+
|
|
223
|
+
Parameters
|
|
224
|
+
----------
|
|
225
|
+
L : int
|
|
226
|
+
Ring size.
|
|
227
|
+
h : str | sympy.Symbol
|
|
228
|
+
Resonance integral symbol (default 'h').
|
|
229
|
+
s : str | sympy.Symbol
|
|
230
|
+
AO overlap symbol (default 's').
|
|
231
|
+
basis : 'real' | 'complex'
|
|
232
|
+
Real (cos/sin) Bloch basis (default) or complex exponentials.
|
|
233
|
+
Currently only 'real' is implemented.
|
|
234
|
+
site_labels : optional sequence of L strings
|
|
235
|
+
Defaults to first L lowercase letters ('a', 'b', ..., chr(96+L)).
|
|
236
|
+
|
|
237
|
+
Returns
|
|
238
|
+
-------
|
|
239
|
+
HuckelResult
|
|
240
|
+
"""
|
|
241
|
+
if basis != 'real':
|
|
242
|
+
raise NotImplementedError("only basis='real' is implemented in v1")
|
|
243
|
+
h_sym = _resolve_symbol(h, 'h')
|
|
244
|
+
s_sym = _resolve_symbol(s, 's')
|
|
245
|
+
if site_labels is None:
|
|
246
|
+
site_labels = tuple(chr(ord('a') + j) for j in range(L))
|
|
247
|
+
else:
|
|
248
|
+
site_labels = tuple(site_labels)
|
|
249
|
+
if len(site_labels) != L:
|
|
250
|
+
raise ValueError(f"need {L} site_labels, got {len(site_labels)}")
|
|
251
|
+
|
|
252
|
+
special = _ring_special_case(L)
|
|
253
|
+
if special is not None:
|
|
254
|
+
eigs, coeffs, irreps = special
|
|
255
|
+
return HuckelResult(
|
|
256
|
+
site_labels=site_labels,
|
|
257
|
+
eigenvalues=eigs,
|
|
258
|
+
energies=_energies_from_eigenvalues(eigs, h_sym, s_sym),
|
|
259
|
+
coefficients=coeffs,
|
|
260
|
+
h_symbol=h_sym,
|
|
261
|
+
s_symbol=s_sym,
|
|
262
|
+
point_group=f'D_{L}h',
|
|
263
|
+
irrep_labels=irreps,
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
# general L: build adjacency of C_L and fall through
|
|
267
|
+
adjacency = sympy.zeros(L, L)
|
|
268
|
+
for j in range(L):
|
|
269
|
+
adjacency[j, (j + 1) % L] = 1
|
|
270
|
+
adjacency[(j + 1) % L, j] = 1
|
|
271
|
+
eigs, coeffs = _generic_solve(adjacency, site_labels, h_sym, s_sym)
|
|
272
|
+
return HuckelResult(
|
|
273
|
+
site_labels=site_labels,
|
|
274
|
+
eigenvalues=eigs,
|
|
275
|
+
energies=_energies_from_eigenvalues(eigs, h_sym, s_sym),
|
|
276
|
+
coefficients=coeffs,
|
|
277
|
+
h_symbol=h_sym,
|
|
278
|
+
s_symbol=s_sym,
|
|
279
|
+
point_group=f'D_{L}h',
|
|
280
|
+
irrep_labels=None,
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def solve(adjacency, site_labels=None, h='h', s='s', overlap=None):
|
|
285
|
+
"""Solve a general Huckel graph.
|
|
286
|
+
|
|
287
|
+
Parameters
|
|
288
|
+
----------
|
|
289
|
+
adjacency : matrix-like
|
|
290
|
+
n x n symmetric adjacency matrix (entries are coefficients of
|
|
291
|
+
h, typically 0/1 or symbolic per-edge weights).
|
|
292
|
+
site_labels : optional sequence of n strings.
|
|
293
|
+
h, s : str | sympy.Symbol
|
|
294
|
+
Resonance and overlap symbols.
|
|
295
|
+
overlap : optional matrix-like
|
|
296
|
+
Explicit overlap matrix. If provided, must equal I + (s/h)*adjacency
|
|
297
|
+
or HuckelGHEPMismatchError is raised. If None, the canonical
|
|
298
|
+
S = I + (s/h)H form is assumed.
|
|
299
|
+
|
|
300
|
+
Returns
|
|
301
|
+
-------
|
|
302
|
+
HuckelResult
|
|
303
|
+
"""
|
|
304
|
+
h_sym = _resolve_symbol(h, 'h')
|
|
305
|
+
s_sym = _resolve_symbol(s, 's')
|
|
306
|
+
A = sympy.Matrix(adjacency)
|
|
307
|
+
if site_labels is None:
|
|
308
|
+
site_labels = tuple(chr(ord('a') + j) for j in range(A.rows))
|
|
309
|
+
else:
|
|
310
|
+
site_labels = tuple(site_labels)
|
|
311
|
+
|
|
312
|
+
_check_ghep_precondition(A, overlap, h_sym, s_sym)
|
|
313
|
+
|
|
314
|
+
eigs, coeffs = _generic_solve(A, site_labels, h_sym, s_sym)
|
|
315
|
+
return HuckelResult(
|
|
316
|
+
site_labels=site_labels,
|
|
317
|
+
eigenvalues=eigs,
|
|
318
|
+
energies=_energies_from_eigenvalues(eigs, h_sym, s_sym),
|
|
319
|
+
coefficients=coeffs,
|
|
320
|
+
h_symbol=h_sym,
|
|
321
|
+
s_symbol=s_sym,
|
|
322
|
+
)
|
symvb/mo_projection.py
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MO -> AO Slater-determinant projection.
|
|
3
|
+
|
|
4
|
+
Given a set of occupied MOs (rows of an AO-coefficient matrix) and an
|
|
5
|
+
ordered AO Slater-determinant basis (symvb's native `m.basis`), expand
|
|
6
|
+
the MO determinant in the AO basis via Slater--Laplace cofactor
|
|
7
|
+
expansion. For each AO determinant |T_alpha; T_beta>,
|
|
8
|
+
|
|
9
|
+
amp(T_alpha, T_beta) = (sign_v * sign_ab) * det(C_alpha[:, T_alpha])
|
|
10
|
+
* det(C_beta [:, T_beta])
|
|
11
|
+
|
|
12
|
+
where C_spin is the submatrix of `mo_coeffs` selecting the rows for
|
|
13
|
+
the spin's occupied MOs, and the two sign factors convert between
|
|
14
|
+
symvb's det-string creation order, spin-block ordering, and the
|
|
15
|
+
canonical interleaved (alpha_j, beta_j) ordering used inside the
|
|
16
|
+
cofactor formula.
|
|
17
|
+
|
|
18
|
+
This is the engine that lets a user write down a candidate eigenvector
|
|
19
|
+
of a 400-dim VB problem (or 22, or 9) without diagonalising it: pick
|
|
20
|
+
the desired MO occupation pattern, hand it to this function, and the
|
|
21
|
+
resulting vector is exact-symbolic in whatever field the MO coefficients
|
|
22
|
+
live in.
|
|
23
|
+
"""
|
|
24
|
+
from itertools import combinations
|
|
25
|
+
|
|
26
|
+
import numpy
|
|
27
|
+
import sympy
|
|
28
|
+
|
|
29
|
+
from symvb.spin import _symvb_to_canonical_sign
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class EigenpairResidualError(AssertionError):
|
|
33
|
+
"""Raised by `verify_eigenpair` when (H - E*S)*v has any nonzero
|
|
34
|
+
component after simplification. Carries the index of the first
|
|
35
|
+
offending entry plus its raw and simplified residual expressions."""
|
|
36
|
+
|
|
37
|
+
def __init__(self, index, residual, simplified):
|
|
38
|
+
self.index = index
|
|
39
|
+
self.residual = residual
|
|
40
|
+
self.simplified = simplified
|
|
41
|
+
super().__init__(
|
|
42
|
+
f"residual entry [{index}] is nonzero: {simplified}")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def verify_eigenpair(H, S, v, E, simplify=None):
|
|
46
|
+
"""Prove that (H - E*S)*v == 0 as a polynomial identity.
|
|
47
|
+
|
|
48
|
+
Forms the residual r = H*v - E*(S*v), simplifies each component
|
|
49
|
+
with `simplify` (default sympy.cancel; pass sympy.simplify for
|
|
50
|
+
surd cases). Returns True if every component is identically zero;
|
|
51
|
+
otherwise raises EigenpairResidualError on the first offender.
|
|
52
|
+
|
|
53
|
+
Parameters
|
|
54
|
+
----------
|
|
55
|
+
H, S : sympy.Matrix
|
|
56
|
+
n x n symbolic Hamiltonian and overlap.
|
|
57
|
+
v : sympy.Matrix (n x 1) or convertible
|
|
58
|
+
Candidate eigenvector.
|
|
59
|
+
E : sympy expression
|
|
60
|
+
Candidate eigenvalue (function of the same symbols as H, S).
|
|
61
|
+
simplify : callable, optional
|
|
62
|
+
Per-entry simplifier. Defaults to `sympy.cancel` (best for
|
|
63
|
+
rational-function residuals); use `sympy.simplify` if surds
|
|
64
|
+
are involved.
|
|
65
|
+
"""
|
|
66
|
+
if simplify is None:
|
|
67
|
+
simplify = sympy.cancel
|
|
68
|
+
residual = sympy.Matrix(H) * sympy.Matrix(v) \
|
|
69
|
+
- E * (sympy.Matrix(S) * sympy.Matrix(v))
|
|
70
|
+
for i in range(residual.rows):
|
|
71
|
+
raw = residual[i, 0]
|
|
72
|
+
simp = simplify(sympy.together(raw))
|
|
73
|
+
if simp != 0:
|
|
74
|
+
raise EigenpairResidualError(i, raw, simp)
|
|
75
|
+
return True
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _spin_orbital_interleave_sign(T_alpha, T_beta):
|
|
79
|
+
"""Parity of the permutation that brings the spin-block ordering
|
|
80
|
+
[a_{i_1}, a_{i_2}, ..., b_{j_1}, b_{j_2}, ...] (all alphas first,
|
|
81
|
+
then all betas, each block sorted by site) into the interleaved
|
|
82
|
+
canonical ordering (alpha_0, beta_0, alpha_1, beta_1, ...).
|
|
83
|
+
|
|
84
|
+
With T_alpha and T_beta sorted, intra-block inversions vanish, so
|
|
85
|
+
only the cross-block term contributes: encoding alpha_j as 2j (even)
|
|
86
|
+
and beta_j as 2j+1 (odd), an inversion across blocks happens iff
|
|
87
|
+
j_alpha > j_beta. This is O(|T_alpha| * |T_beta|) instead of
|
|
88
|
+
O((|T_alpha| + |T_beta|)^2)."""
|
|
89
|
+
inv = sum(1 for j_a in T_alpha for j_b in T_beta if j_a > j_b)
|
|
90
|
+
return 1 if inv % 2 == 0 else -1
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _is_sympy(M):
|
|
94
|
+
return isinstance(M, sympy.MatrixBase)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _site_index_map(basis_dets, site_labels):
|
|
98
|
+
"""Return dict {char -> int} from lowercase site letters to indices.
|
|
99
|
+
If `site_labels` is given, use that order; otherwise infer from the
|
|
100
|
+
lowercase characters of basis_dets in first-appearance order."""
|
|
101
|
+
if site_labels is not None:
|
|
102
|
+
return {c: i for i, c in enumerate(site_labels)}
|
|
103
|
+
seen = []
|
|
104
|
+
for ds in basis_dets:
|
|
105
|
+
for c in ds:
|
|
106
|
+
cl = c.lower()
|
|
107
|
+
if cl not in seen:
|
|
108
|
+
seen.append(cl)
|
|
109
|
+
seen.sort()
|
|
110
|
+
return {c: i for i, c in enumerate(seen)}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def mo_determinant_in_ao(mo_coeffs, occupation, basis_dets,
|
|
114
|
+
site_labels=None):
|
|
115
|
+
"""Expand an MO Slater determinant in the symvb AO det basis.
|
|
116
|
+
|
|
117
|
+
Parameters
|
|
118
|
+
----------
|
|
119
|
+
mo_coeffs : sympy.Matrix or numpy.ndarray, shape (n_mo, n_sites)
|
|
120
|
+
Row k = MO k expanded over the AOs in `site_labels` order.
|
|
121
|
+
Backend (sympy vs numpy) is auto-detected from this argument
|
|
122
|
+
and propagates to the returned vector.
|
|
123
|
+
occupation : (alpha_indices, beta_indices)
|
|
124
|
+
Two iterables of MO row indices (into `mo_coeffs`) listing the
|
|
125
|
+
occupied alpha and beta MOs. For closed-shell, the two are
|
|
126
|
+
equal.
|
|
127
|
+
basis_dets : sequence of str
|
|
128
|
+
AO Slater-det strings in symvb's convention (lowercase = alpha,
|
|
129
|
+
uppercase = beta, creation order = string order). Typically
|
|
130
|
+
`[fp.dets[0].det_string for fp in m.basis]`.
|
|
131
|
+
site_labels : optional sequence of n_sites lowercase chars
|
|
132
|
+
Maps the AO label to a column index of `mo_coeffs`. If None,
|
|
133
|
+
inferred from `basis_dets` (first-appearance, then sorted).
|
|
134
|
+
|
|
135
|
+
Returns
|
|
136
|
+
-------
|
|
137
|
+
Column vector (sympy.Matrix or numpy.ndarray of float) of length
|
|
138
|
+
len(basis_dets), giving the AO-det expansion coefficients of
|
|
139
|
+
|occupation>.
|
|
140
|
+
"""
|
|
141
|
+
alpha_indices = list(occupation[0])
|
|
142
|
+
beta_indices = list(occupation[1])
|
|
143
|
+
n_alpha = len(alpha_indices)
|
|
144
|
+
n_beta = len(beta_indices)
|
|
145
|
+
|
|
146
|
+
site_idx = _site_index_map(basis_dets, site_labels)
|
|
147
|
+
n_sites = len(site_idx)
|
|
148
|
+
|
|
149
|
+
use_sympy = _is_sympy(mo_coeffs)
|
|
150
|
+
|
|
151
|
+
# Submatrices: occupied alpha MOs, occupied beta MOs, all AOs (cols).
|
|
152
|
+
if use_sympy:
|
|
153
|
+
C_alpha = mo_coeffs[alpha_indices, :]
|
|
154
|
+
C_beta = mo_coeffs[beta_indices, :]
|
|
155
|
+
else:
|
|
156
|
+
C = numpy.asarray(mo_coeffs)
|
|
157
|
+
C_alpha = C[alpha_indices, :]
|
|
158
|
+
C_beta = C[beta_indices, :]
|
|
159
|
+
|
|
160
|
+
# Memoize cofactor minors over distinct sorted T-tuples. Each T
|
|
161
|
+
# has size n_alpha (resp. n_beta); for benzene closed-shell this
|
|
162
|
+
# collapses 400 evaluations to C(6, 3) = 20 per spin.
|
|
163
|
+
det_alpha_cache = {}
|
|
164
|
+
det_beta_cache = {}
|
|
165
|
+
|
|
166
|
+
def det_alpha(T):
|
|
167
|
+
if T in det_alpha_cache:
|
|
168
|
+
return det_alpha_cache[T]
|
|
169
|
+
if use_sympy:
|
|
170
|
+
d = C_alpha[:, list(T)].det()
|
|
171
|
+
else:
|
|
172
|
+
d = numpy.linalg.det(C_alpha[:, list(T)])
|
|
173
|
+
det_alpha_cache[T] = d
|
|
174
|
+
return d
|
|
175
|
+
|
|
176
|
+
def det_beta(T):
|
|
177
|
+
if T in det_beta_cache:
|
|
178
|
+
return det_beta_cache[T]
|
|
179
|
+
if use_sympy:
|
|
180
|
+
d = C_beta[:, list(T)].det()
|
|
181
|
+
else:
|
|
182
|
+
d = numpy.linalg.det(C_beta[:, list(T)])
|
|
183
|
+
det_beta_cache[T] = d
|
|
184
|
+
return d
|
|
185
|
+
|
|
186
|
+
# Collect amplitudes in a Python list, then build the result vector
|
|
187
|
+
# in one go. This avoids per-element sympy.Matrix.__setitem__ /
|
|
188
|
+
# numpy item-assignment overhead, which dominates the inner loop
|
|
189
|
+
# for the 400-dim benzene case.
|
|
190
|
+
zero = sympy.Integer(0) if use_sympy else 0.0
|
|
191
|
+
amps = [zero] * len(basis_dets)
|
|
192
|
+
for I, ds in enumerate(basis_dets):
|
|
193
|
+
T_a, T_b = [], []
|
|
194
|
+
for c in ds:
|
|
195
|
+
j = site_idx[c.lower()]
|
|
196
|
+
(T_a if c.islower() else T_b).append(j)
|
|
197
|
+
if len(T_a) != n_alpha or len(T_b) != n_beta:
|
|
198
|
+
# AO det doesn't have the right electron count for this
|
|
199
|
+
# occupation; amplitude is zero by orthogonality.
|
|
200
|
+
continue
|
|
201
|
+
T_a_sorted = tuple(sorted(T_a))
|
|
202
|
+
T_b_sorted = tuple(sorted(T_b))
|
|
203
|
+
|
|
204
|
+
sign_ab = _spin_orbital_interleave_sign(T_a_sorted, T_b_sorted)
|
|
205
|
+
sign_v = _symvb_to_canonical_sign(ds, site_idx)
|
|
206
|
+
|
|
207
|
+
d_a = det_alpha(T_a_sorted)
|
|
208
|
+
d_b = det_beta(T_b_sorted)
|
|
209
|
+
|
|
210
|
+
amps[I] = sign_v * sign_ab * d_a * d_b
|
|
211
|
+
|
|
212
|
+
if use_sympy:
|
|
213
|
+
return sympy.Matrix(amps)
|
|
214
|
+
arr = numpy.asarray(amps)
|
|
215
|
+
if numpy.iscomplexobj(arr):
|
|
216
|
+
arr = arr.real
|
|
217
|
+
return arr.astype(float, copy=False)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def linear_combination_in_ao(mo_coeffs, terms, basis_dets, site_labels=None):
|
|
221
|
+
"""Project a linear combination of MO Slater determinants into the
|
|
222
|
+
AO det basis.
|
|
223
|
+
|
|
224
|
+
Returns sum_i c_i * mo_determinant_in_ao(mo_coeffs, occ_i, ...) for
|
|
225
|
+
each (c_i, occ_i) pair in `terms`. Backend (sympy vs numpy) is
|
|
226
|
+
inherited from `mo_coeffs`.
|
|
227
|
+
|
|
228
|
+
Parameters
|
|
229
|
+
----------
|
|
230
|
+
mo_coeffs : sympy.Matrix or numpy.ndarray, shape (n_mo, n_sites)
|
|
231
|
+
Same as in `mo_determinant_in_ao`.
|
|
232
|
+
terms : iterable of (coefficient, occupation) pairs
|
|
233
|
+
Each `occupation` is `(alpha_indices, beta_indices)`. The
|
|
234
|
+
coefficient is multiplied into the corresponding determinant.
|
|
235
|
+
basis_dets, site_labels : same as in `mo_determinant_in_ao`.
|
|
236
|
+
|
|
237
|
+
Returns
|
|
238
|
+
-------
|
|
239
|
+
Column vector (sympy.Matrix or numpy.ndarray) of length
|
|
240
|
+
len(basis_dets), the AO-det expansion of the linear combination.
|
|
241
|
+
"""
|
|
242
|
+
terms = list(terms)
|
|
243
|
+
if not terms:
|
|
244
|
+
raise ValueError("terms must be non-empty")
|
|
245
|
+
use_sympy = _is_sympy(mo_coeffs)
|
|
246
|
+
coef0, occ0 = terms[0]
|
|
247
|
+
result = coef0 * mo_determinant_in_ao(
|
|
248
|
+
mo_coeffs, occ0, basis_dets, site_labels=site_labels)
|
|
249
|
+
for coef, occ in terms[1:]:
|
|
250
|
+
result = result + coef * mo_determinant_in_ao(
|
|
251
|
+
mo_coeffs, occ, basis_dets, site_labels=site_labels)
|
|
252
|
+
return result
|