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/system.py
ADDED
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
"""High-level convenience layer over :class:`~symvb.molecule.Molecule`.
|
|
2
|
+
|
|
3
|
+
This is a thin, *additive* facade. The low-level symbolic core is unchanged
|
|
4
|
+
and every object it returns (the SymPy ``H``/``S`` matrices, the ground-state
|
|
5
|
+
expression, the structure vectors) stays directly inspectable. The facade
|
|
6
|
+
just removes the boilerplate and the two recurring footguns:
|
|
7
|
+
|
|
8
|
+
* building the Hamiltonian no longer means three calls plus a hand-combine,
|
|
9
|
+
and you never scale the two-electron matrix by ``U`` yourself (it already
|
|
10
|
+
carries the integral name -- doing so silently squares it);
|
|
11
|
+
* expressing a VB structure as a basis vector no longer means re-deriving the
|
|
12
|
+
fermion sign of the canonical reordering by hand.
|
|
13
|
+
|
|
14
|
+
Typical use::
|
|
15
|
+
|
|
16
|
+
from symvb import System
|
|
17
|
+
sys = System.ring(6) # benzene pi ring, Hubbard U
|
|
18
|
+
H, S = sys.hamiltonian() # 400x400 SymPy matrices
|
|
19
|
+
E, c = System.from_structures(mol, [cov, ion]).ground_state()
|
|
20
|
+
w = sys.weights(groups=...) # Chirgwin-Coulson weights
|
|
21
|
+
|
|
22
|
+
Or compose the standalone helpers with a Molecule you built yourself::
|
|
23
|
+
|
|
24
|
+
from symvb.system import hamiltonian, ground_state, chirgwin_coulson
|
|
25
|
+
H, S = hamiltonian(mol, basis)
|
|
26
|
+
E, c = ground_state(H, S)
|
|
27
|
+
w = chirgwin_coulson(c, S)
|
|
28
|
+
"""
|
|
29
|
+
import sympy as sp
|
|
30
|
+
|
|
31
|
+
from .molecule import Molecule
|
|
32
|
+
from .fixed_psi import FixedPsi, generate_dets
|
|
33
|
+
from .functions import standardize_det
|
|
34
|
+
from .slaterdet import SlaterDet
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
'System', 'hamiltonian', 'ground_state', 'chirgwin_coulson',
|
|
38
|
+
'structure_vector',
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
# reference point for picking the ground root of a symbolic GHEP
|
|
42
|
+
_DEFAULT_REF = {'h': -1, 's': 0, 'U': 1, 'J': 0, 'K': 0, 'M': 0,
|
|
43
|
+
'h_s': -1, 'h_l': sp.Rational(-3, 10)}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _standardize_full(det_string):
|
|
47
|
+
"""Map ``det_string`` to its ``generate_det_strings``-format twin + sign.
|
|
48
|
+
|
|
49
|
+
Returns ``(canonical, sign)`` with ``|det_string> = sign * |canonical>``,
|
|
50
|
+
where ``canonical`` is the unique basis-format string for the same set of
|
|
51
|
+
spin-orbitals: alphabetically sorted alpha and beta labels interleaved
|
|
52
|
+
``uLuL...`` with the majority-spin extras appended, exactly as
|
|
53
|
+
:func:`symvb.functions.generate_det_strings` emits it.
|
|
54
|
+
|
|
55
|
+
:func:`symvb.functions.standardize_det` alone is not that map: it fixes
|
|
56
|
+
the spin *pattern* but its pairwise flips can jump a creation operator
|
|
57
|
+
over same-spin neighbours, permuting the alphabetical order within the
|
|
58
|
+
alpha/beta blocks (e.g. ``'abcAB' -> 'aAcBb'``, not ``'aAbBc'``), so its
|
|
59
|
+
output is a valid det string of the same determinant that need not be in
|
|
60
|
+
the generated basis. Composing it with the within-block label sort of
|
|
61
|
+
:meth:`symvb.slaterdet.SlaterDet.get_sorted` (spin pattern unchanged),
|
|
62
|
+
with both fermion signs folded in, gives the true canonical map. A no-op
|
|
63
|
+
(sign ``+1``) on a string that is already in basis format.
|
|
64
|
+
"""
|
|
65
|
+
std, flips = standardize_det(det_string)
|
|
66
|
+
fp = SlaterDet(std).get_sorted()
|
|
67
|
+
return fp.dets[0].det_string, ((-1) ** flips) * fp.coefs[0]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _det_string(b):
|
|
71
|
+
"""The canonical determinant string of a basis entry (FixedPsi/SlaterDet/str).
|
|
72
|
+
|
|
73
|
+
Returned in symvb's standard (interleaved, block-sorted) creation order so
|
|
74
|
+
that the determinant index this builds agrees with the standardized basis
|
|
75
|
+
that matrix construction and :func:`structure_vector` use. A no-op on an
|
|
76
|
+
entry that is already canonical.
|
|
77
|
+
"""
|
|
78
|
+
if isinstance(b, str):
|
|
79
|
+
raw = b
|
|
80
|
+
elif hasattr(b, 'dets'): # FixedPsi
|
|
81
|
+
raw = b.dets[0].det_string
|
|
82
|
+
else: # SlaterDet
|
|
83
|
+
raw = b.det_string
|
|
84
|
+
return _standardize_full(raw)[0]
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# --------------------------------------------------------------------------
|
|
88
|
+
# standalone helpers (usable without a System)
|
|
89
|
+
# --------------------------------------------------------------------------
|
|
90
|
+
def hamiltonian(molecule, basis, two_electron=True):
|
|
91
|
+
"""Return ``(H, S)`` over ``basis``, with the two-electron block folded in.
|
|
92
|
+
|
|
93
|
+
``basis`` is a list of ``FixedPsi`` / ``SlaterDet`` / determinant strings.
|
|
94
|
+
The two-electron block is **always** folded into ``H`` -- under the names
|
|
95
|
+
declared in ``subst_2e`` when the molecule has them, otherwise under the
|
|
96
|
+
default ``T_<abcd>`` integral names. The integrals are already inside
|
|
97
|
+
``H``; do **not** multiply the two-electron matrix by ``U`` yourself.
|
|
98
|
+
|
|
99
|
+
Pass ``two_electron=False`` for an intentionally one-electron model
|
|
100
|
+
(a Hueckel-level ``H``): this returns the bare ``build_matrix(op='H')``
|
|
101
|
+
and skips the two-electron build, which dominates the cost on large
|
|
102
|
+
bases.
|
|
103
|
+
"""
|
|
104
|
+
H = molecule.build_matrix(basis, op='H')
|
|
105
|
+
S = molecule.build_matrix(basis, op='S')
|
|
106
|
+
if two_electron:
|
|
107
|
+
H = H + molecule.o2_matrix(basis)
|
|
108
|
+
return H, S
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def structure_vector(structure, basis_dets):
|
|
112
|
+
"""Expand a VB structure as a column vector over ``basis_dets``.
|
|
113
|
+
|
|
114
|
+
``structure`` is a ``FixedPsi`` (e.g. a Rumer/Heitler-London structure built
|
|
115
|
+
with ``coupled_pairs``); ``basis_dets`` is the list of determinant strings
|
|
116
|
+
of the target basis (any creation order). Both sides are brought to the
|
|
117
|
+
same canonical form: each basis string and each structure determinant is
|
|
118
|
+
mapped through the full standardization (spin pattern via
|
|
119
|
+
:func:`symvb.functions.standardize_det`, then within-block label sort),
|
|
120
|
+
with both fermion signs folded into the coefficient. Alpha-alpha-beta-beta
|
|
121
|
+
and other non-canonical spin patterns, including the unequal-filling cases
|
|
122
|
+
where ``standardize_det`` alone leaves the generated basis (e.g. 3 alpha /
|
|
123
|
+
2 beta hole determinants), are therefore placed correctly. Use this to
|
|
124
|
+
project a structure onto an explicit determinant basis (e.g. an FCI ground
|
|
125
|
+
state for weights); to build ``(H, S)`` over the structures themselves,
|
|
126
|
+
``hamiltonian`` / ``build_matrix`` now canonicalize internally.
|
|
127
|
+
"""
|
|
128
|
+
fp = FixedPsi(structure)
|
|
129
|
+
fp.canonicalize()
|
|
130
|
+
idx = {}
|
|
131
|
+
for i, b in enumerate(basis_dets):
|
|
132
|
+
key, bsign = _standardize_full(b)
|
|
133
|
+
if key in idx:
|
|
134
|
+
raise ValueError(
|
|
135
|
+
"basis determinants %r and %r are the same determinant "
|
|
136
|
+
"(canonical form %r)" % (basis_dets[idx[key][0]], b, key))
|
|
137
|
+
idx[key] = (i, bsign)
|
|
138
|
+
v = sp.zeros(len(basis_dets), 1)
|
|
139
|
+
for d, c in fp:
|
|
140
|
+
key, dsign = _standardize_full(d.det_string)
|
|
141
|
+
if key not in idx:
|
|
142
|
+
raise ValueError(
|
|
143
|
+
"structure determinant %r (canonical form %r) is not in the "
|
|
144
|
+
"target basis" % (d.det_string, key))
|
|
145
|
+
i, bsign = idx[key]
|
|
146
|
+
v[i] += dsign * bsign * c
|
|
147
|
+
return v
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def chirgwin_coulson(c, S, groups=None, simplify=False):
|
|
151
|
+
"""Chirgwin-Coulson weights of coefficient vector ``c`` under metric ``S``.
|
|
152
|
+
|
|
153
|
+
``w_i = c_i (S c)_i / (c^T S c)`` (the metric makes them sum to one even for
|
|
154
|
+
non-orthonormal ``c``). If ``groups`` (a list of index lists) is given, the
|
|
155
|
+
summed weight per group is returned instead. Accepts either SymPy
|
|
156
|
+
matrices/vectors or NumPy arrays and returns the matching type. ``simplify``
|
|
157
|
+
is off by default: simplifying raw symbolic weights can be very slow, and
|
|
158
|
+
it is usually cheaper to substitute numeric values first; pass
|
|
159
|
+
``simplify=True`` only for small closed forms.
|
|
160
|
+
"""
|
|
161
|
+
try:
|
|
162
|
+
import numpy as np
|
|
163
|
+
if isinstance(c, np.ndarray): # numeric path keyed on the coefficient vector
|
|
164
|
+
c = np.asarray(c, float).ravel()
|
|
165
|
+
S = np.asarray(S, float)
|
|
166
|
+
Sc = S @ c
|
|
167
|
+
w = c * Sc / (c @ Sc)
|
|
168
|
+
if groups is None:
|
|
169
|
+
return w
|
|
170
|
+
return np.array([w[list(g)].sum() for g in groups])
|
|
171
|
+
except ImportError:
|
|
172
|
+
pass
|
|
173
|
+
c = sp.Matrix(c)
|
|
174
|
+
S = sp.Matrix(S) # accept a numpy metric alongside a symbolic c
|
|
175
|
+
Sc = S * c
|
|
176
|
+
norm = (c.T * Sc)[0]
|
|
177
|
+
w = [c[i] * Sc[i] / norm for i in range(c.rows)]
|
|
178
|
+
if groups is not None:
|
|
179
|
+
w = [sum(w[i] for i in g) for g in groups]
|
|
180
|
+
if simplify:
|
|
181
|
+
w = [sp.simplify(x) for x in w]
|
|
182
|
+
return sp.Matrix(w)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _ref_subs(H, S, ref):
|
|
186
|
+
syms = H.free_symbols | S.free_symbols
|
|
187
|
+
user = {}
|
|
188
|
+
if ref:
|
|
189
|
+
for k, val in ref.items():
|
|
190
|
+
user[sp.Symbol(k) if isinstance(k, str) else k] = val
|
|
191
|
+
out = {}
|
|
192
|
+
for sym in syms:
|
|
193
|
+
if sym in user:
|
|
194
|
+
out[sym] = user[sym]
|
|
195
|
+
elif str(sym) in _DEFAULT_REF:
|
|
196
|
+
out[sym] = _DEFAULT_REF[str(sym)]
|
|
197
|
+
else:
|
|
198
|
+
out[sym] = sp.Rational(1, 10) # neutral nonzero default
|
|
199
|
+
return out
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def ground_state(H, S, ref=None, subs=None):
|
|
203
|
+
"""Ground state of ``H c = E S c``.
|
|
204
|
+
|
|
205
|
+
**Symbolic** (``subs=None``, default): solves the characteristic polynomial
|
|
206
|
+
for a SMALL block (2x2, 3x3). Returns ``(E, c)`` as SymPy expressions, ``E``
|
|
207
|
+
simplified, ``c`` the (un-normalized) ground eigenvector (the metric is
|
|
208
|
+
applied later by :func:`chirgwin_coulson`, and simplifying an eigenvector
|
|
209
|
+
full of nested radicals is expensive, so we don't). The ground root is the
|
|
210
|
+
one numerically lowest at the reference point ``ref`` -- a ``{symbol-or-name:
|
|
211
|
+
value}`` dict; defaults are ``h=-1, s=0`` and any electron-repulsion integral
|
|
212
|
+
``= 1``. The symbolic solve is only practical for a few dimensions.
|
|
213
|
+
|
|
214
|
+
**Numeric** (``subs`` given, a substitution dict): the matrices are
|
|
215
|
+
evaluated and the lowest eigenpair is found with ``scipy.linalg.eigh`` --
|
|
216
|
+
use this for anything larger, e.g. a full determinant (FCI) basis. Returns
|
|
217
|
+
``(E_float, c_ndarray)``.
|
|
218
|
+
"""
|
|
219
|
+
if subs is not None:
|
|
220
|
+
import numpy as np
|
|
221
|
+
from scipy.linalg import eigh as _eigh
|
|
222
|
+
Hn = np.array(sp.Matrix(H).subs(subs).tolist(), float)
|
|
223
|
+
Sn = np.array(sp.Matrix(S).subs(subs).tolist(), float)
|
|
224
|
+
w, v = _eigh(Hn, Sn, subset_by_index=[0, 0])
|
|
225
|
+
return float(w[0]), v[:, 0]
|
|
226
|
+
H = sp.Matrix(H)
|
|
227
|
+
S = sp.Matrix(S)
|
|
228
|
+
E = sp.Dummy('E')
|
|
229
|
+
roots = sp.solve((H - E * S).det(), E)
|
|
230
|
+
if not roots:
|
|
231
|
+
raise ValueError("det(H - E S) = 0 has no roots solvable in closed form")
|
|
232
|
+
sub = _ref_subs(H, S, ref)
|
|
233
|
+
E_gs = min(roots, key=lambda r: float(sp.re(r.subs(sub))))
|
|
234
|
+
null = (H - E_gs * S).nullspace()
|
|
235
|
+
if not null:
|
|
236
|
+
raise ValueError("no eigenvector found for the ground root")
|
|
237
|
+
return sp.simplify(E_gs), null[0]
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
# --------------------------------------------------------------------------
|
|
241
|
+
# the System facade
|
|
242
|
+
# --------------------------------------------------------------------------
|
|
243
|
+
class System:
|
|
244
|
+
"""A :class:`Molecule` plus a determinant (or structure) basis.
|
|
245
|
+
|
|
246
|
+
Construct directly with ``System(molecule, basis)``, from a structure list
|
|
247
|
+
with ``System.from_structures(molecule, structures)``, or from a topology
|
|
248
|
+
with ``System.ring(L)`` / ``System.chain(n)``.
|
|
249
|
+
"""
|
|
250
|
+
|
|
251
|
+
def __init__(self, molecule, basis, two_electron=True):
|
|
252
|
+
self.m = molecule
|
|
253
|
+
self.basis = list(basis)
|
|
254
|
+
self.det_strings = [_det_string(b) for b in self.basis]
|
|
255
|
+
self.two_electron = two_electron
|
|
256
|
+
self._H = None
|
|
257
|
+
self._S = None
|
|
258
|
+
|
|
259
|
+
# ---- constructors ----
|
|
260
|
+
@classmethod
|
|
261
|
+
def from_structures(cls, molecule, structures, two_electron=True):
|
|
262
|
+
"""A System whose basis is an explicit list of VB structures."""
|
|
263
|
+
return cls(molecule, structures, two_electron=two_electron)
|
|
264
|
+
|
|
265
|
+
@classmethod
|
|
266
|
+
def ring(cls, L, n_alpha=None, n_beta=None, hubbard=True,
|
|
267
|
+
two_electron=True, **kw):
|
|
268
|
+
"""Cyclic ``L``-orbital ring with Hubbard ``U``.
|
|
269
|
+
|
|
270
|
+
Defaults to ``floor(L/2)`` electrons of each spin (the Sz=0 reference,
|
|
271
|
+
e.g. 3 + 3 for benzene). For an ion or an odd ring pass ``n_alpha`` /
|
|
272
|
+
``n_beta`` explicitly (the cyclopentadienyl anion is
|
|
273
|
+
``System.ring(5, n_alpha=3, n_beta=3)``).
|
|
274
|
+
"""
|
|
275
|
+
m = Molecule.ring(L, hubbard=hubbard, **kw)
|
|
276
|
+
na = L // 2 if n_alpha is None else n_alpha
|
|
277
|
+
nb = na if n_beta is None else n_beta
|
|
278
|
+
return cls(m, generate_dets(na, nb, L), two_electron=two_electron)
|
|
279
|
+
|
|
280
|
+
@classmethod
|
|
281
|
+
def chain(cls, n, n_alpha=None, n_beta=None, hubbard=True,
|
|
282
|
+
two_electron=True, **kw):
|
|
283
|
+
"""Linear ``n``-orbital chain with Hubbard ``U``; same filling convention
|
|
284
|
+
as :meth:`ring` (``floor(n/2)`` electrons of each spin by default)."""
|
|
285
|
+
m = Molecule.chain(n, hubbard=hubbard, **kw)
|
|
286
|
+
na = n // 2 if n_alpha is None else n_alpha
|
|
287
|
+
nb = na if n_beta is None else n_beta
|
|
288
|
+
return cls(m, generate_dets(na, nb, n), two_electron=two_electron)
|
|
289
|
+
|
|
290
|
+
# ---- matrices ----
|
|
291
|
+
def hamiltonian(self):
|
|
292
|
+
"""``(H, S)`` over the basis (cached). The 2e block is folded into H
|
|
293
|
+
unless the System was built with ``two_electron=False``."""
|
|
294
|
+
if self._H is None:
|
|
295
|
+
self._H, self._S = hamiltonian(self.m, self.basis,
|
|
296
|
+
two_electron=self.two_electron)
|
|
297
|
+
return self._H, self._S
|
|
298
|
+
|
|
299
|
+
@property
|
|
300
|
+
def H(self):
|
|
301
|
+
return self.hamiltonian()[0]
|
|
302
|
+
|
|
303
|
+
@property
|
|
304
|
+
def S(self):
|
|
305
|
+
return self.hamiltonian()[1]
|
|
306
|
+
|
|
307
|
+
# ---- VB structures ----
|
|
308
|
+
def structure_vector(self, structure):
|
|
309
|
+
"""Column vector of ``structure`` over this System's determinant basis."""
|
|
310
|
+
return structure_vector(structure, self.det_strings)
|
|
311
|
+
|
|
312
|
+
# ---- solving ----
|
|
313
|
+
def ground_state(self, ref=None, subs=None):
|
|
314
|
+
"""``(E, c)`` of the ground state over this basis.
|
|
315
|
+
|
|
316
|
+
Symbolic for small bases; pass ``subs`` (a numeric substitution dict) to
|
|
317
|
+
solve a large/FCI basis numerically with scipy (returns floats/arrays).
|
|
318
|
+
"""
|
|
319
|
+
H, S = self.hamiltonian()
|
|
320
|
+
return ground_state(H, S, ref=ref, subs=subs)
|
|
321
|
+
|
|
322
|
+
def weights(self, structures=None, groups=None, ref=None, subs=None):
|
|
323
|
+
"""Chirgwin-Coulson weights of the ground state.
|
|
324
|
+
|
|
325
|
+
With no ``structures``, weights are per basis function (optionally summed
|
|
326
|
+
by ``groups``). With ``structures`` (a list of ``FixedPsi``), the ground
|
|
327
|
+
state is projected onto that (possibly non-orthogonal) structure space
|
|
328
|
+
and weights are returned per structure, normalized over that space (the
|
|
329
|
+
composition of the part of the wavefunction the structures span).
|
|
330
|
+
|
|
331
|
+
Symbolic by default and only practical for small bases (the ground-state
|
|
332
|
+
solve is a symbolic characteristic polynomial). For an FCI-sized basis
|
|
333
|
+
pass ``subs`` (a numeric dict): the ground state is found numerically and
|
|
334
|
+
the weights come back as a NumPy array.
|
|
335
|
+
"""
|
|
336
|
+
H, S = self.hamiltonian()
|
|
337
|
+
E, c = ground_state(H, S, ref=ref, subs=subs)
|
|
338
|
+
if subs is None:
|
|
339
|
+
S_eff = S
|
|
340
|
+
else:
|
|
341
|
+
import numpy as np
|
|
342
|
+
S_eff = np.array(sp.Matrix(S).subs(subs).tolist(), float)
|
|
343
|
+
if structures is None:
|
|
344
|
+
return chirgwin_coulson(c, S_eff, groups=groups)
|
|
345
|
+
V = sp.Matrix.hstack(*[self.structure_vector(st) for st in structures])
|
|
346
|
+
if subs is None:
|
|
347
|
+
G = V.T * S_eff * V # structure-space metric
|
|
348
|
+
a = G.solve(V.T * S_eff * c) # ground state in the structure basis
|
|
349
|
+
else:
|
|
350
|
+
import numpy as np
|
|
351
|
+
Vn = np.array(V, float)
|
|
352
|
+
G = Vn.T @ S_eff @ Vn
|
|
353
|
+
a = np.linalg.solve(G, Vn.T @ S_eff @ np.asarray(c, float))
|
|
354
|
+
return chirgwin_coulson(a, G, groups=groups)
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
"""Determinant-convention consistency between standardize_det and the
|
|
2
|
+
generate_dets/generate_det_strings basis.
|
|
3
|
+
|
|
4
|
+
standardize_det fixes the spin *pattern* (uLuL... + majority-spin tail) but
|
|
5
|
+
its pairwise flips can permute the alphabetical order within the alpha/beta
|
|
6
|
+
blocks, so on unequal-filling (and beta-block-leading) determinants its output
|
|
7
|
+
string need not be a member of the generated basis (e.g. 'abcAB' -> 'aAcBb',
|
|
8
|
+
not 'aAbBc'). Its output convention is load-bearing in matrix construction and
|
|
9
|
+
must not change; the *lookup* side (symvb.system.structure_vector) therefore
|
|
10
|
+
standardizes both the basis strings and the structure determinants through the
|
|
11
|
+
full canonical map (spin pattern + within-block sort, fermion signs folded).
|
|
12
|
+
These tests pin the census of the mismatch, the fixed lookup at every affected
|
|
13
|
+
filling, sign correctness against an independent inversion-parity reference,
|
|
14
|
+
and exact no-regression on previously-working columns.
|
|
15
|
+
"""
|
|
16
|
+
import string
|
|
17
|
+
import unittest
|
|
18
|
+
from itertools import combinations
|
|
19
|
+
|
|
20
|
+
import numpy as np
|
|
21
|
+
import sympy as sp
|
|
22
|
+
|
|
23
|
+
from symvb import FixedPsi, Molecule
|
|
24
|
+
from symvb.fixed_psi import generate_dets
|
|
25
|
+
from symvb.functions import generate_det_strings, standardize_det
|
|
26
|
+
from symvb.system import _standardize_full, hamiltonian, structure_vector
|
|
27
|
+
|
|
28
|
+
FILLINGS = [(2, 1, 4), (2, 2, 3), (2, 2, 4), (3, 2, 4), (3, 2, 6), (3, 3, 6)]
|
|
29
|
+
|
|
30
|
+
# dets (with alphabetically sorted alpha and beta blocks, i.e. exactly the
|
|
31
|
+
# strings FixedPsi.canonicalize can emit) whose bare standardize_det output
|
|
32
|
+
# is NOT in generate_det_strings(Na, Nb, Norb)
|
|
33
|
+
KNOWN_MISS_COUNTS = {(2, 1, 4): 0, (2, 2, 3): 9, (2, 2, 4): 36,
|
|
34
|
+
(3, 2, 4): 48, (3, 2, 6): 600, (3, 3, 6): 2800}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _canonicalized_space(Na, Nb, Norb):
|
|
38
|
+
"""All det strings with sorted alpha block and sorted beta block, over all
|
|
39
|
+
C(Na+Nb, Na) spin patterns: the input space structure_vector feeds into
|
|
40
|
+
the standardization after FixedPsi.canonicalize()."""
|
|
41
|
+
lo = string.ascii_lowercase[:Norb]
|
|
42
|
+
up = string.ascii_uppercase[:Norb]
|
|
43
|
+
out = []
|
|
44
|
+
for a in combinations(lo, Na):
|
|
45
|
+
for b in combinations(up, Nb):
|
|
46
|
+
for pos in combinations(range(Na + Nb), Na):
|
|
47
|
+
s = [''] * (Na + Nb)
|
|
48
|
+
ia = ib = 0
|
|
49
|
+
for i in range(Na + Nb):
|
|
50
|
+
if i in pos:
|
|
51
|
+
s[i] = a[ia]
|
|
52
|
+
ia += 1
|
|
53
|
+
else:
|
|
54
|
+
s[i] = b[ib]
|
|
55
|
+
ib += 1
|
|
56
|
+
out.append(''.join(s))
|
|
57
|
+
return out
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _ref_canonical(det_string, basis_by_soset):
|
|
61
|
+
"""Independent reference: the basis string over the same spin-orbital set
|
|
62
|
+
and the inversion parity of the permutation relating the two creation
|
|
63
|
+
orders."""
|
|
64
|
+
so = [(c.lower(), 0 if c.islower() else 1) for c in det_string]
|
|
65
|
+
tgt = basis_by_soset[frozenset(so)]
|
|
66
|
+
tso = [(c.lower(), 0 if c.islower() else 1) for c in tgt]
|
|
67
|
+
pos = {v: i for i, v in enumerate(tso)}
|
|
68
|
+
idx = [pos[v] for v in so]
|
|
69
|
+
inv = sum(1 for i in range(len(idx)) for j in range(i + 1, len(idx))
|
|
70
|
+
if idx[i] > idx[j])
|
|
71
|
+
return tgt, (-1) ** inv
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _soset_index(basis):
|
|
75
|
+
return {frozenset((c.lower(), 0 if c.islower() else 1) for c in b): b
|
|
76
|
+
for b in basis}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _old_structure_vector(structure, basis_dets):
|
|
80
|
+
"""The pre-fix structure_vector algorithm (bare standardize_det lookup),
|
|
81
|
+
kept here as the no-regression reference on fillings where it worked."""
|
|
82
|
+
fp = FixedPsi(structure)
|
|
83
|
+
fp.canonicalize()
|
|
84
|
+
idx = {d: i for i, d in enumerate(basis_dets)}
|
|
85
|
+
v = sp.zeros(len(basis_dets), 1)
|
|
86
|
+
for d, c in fp:
|
|
87
|
+
std, flips = standardize_det(d.det_string)
|
|
88
|
+
if std not in idx:
|
|
89
|
+
raise ValueError(std)
|
|
90
|
+
v[idx[std]] += (-1) ** flips * c
|
|
91
|
+
return v
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class TestFillingCensus(unittest.TestCase):
|
|
95
|
+
def test_standardize_det_miss_counts(self):
|
|
96
|
+
# pin the characterization: standardize_det's output leaves the
|
|
97
|
+
# generated basis exactly this often, per filling, over the
|
|
98
|
+
# canonicalized (sorted-block) input space; it is a no-op on the
|
|
99
|
+
# basis strings themselves.
|
|
100
|
+
for Na, Nb, Norb in FILLINGS:
|
|
101
|
+
basis = set(generate_det_strings(Na, Nb, Norb))
|
|
102
|
+
for b in basis:
|
|
103
|
+
self.assertEqual(standardize_det(b), (b, 0))
|
|
104
|
+
miss = sum(1 for d in _canonicalized_space(Na, Nb, Norb)
|
|
105
|
+
if standardize_det(d)[0] not in basis)
|
|
106
|
+
self.assertEqual(miss, KNOWN_MISS_COUNTS[(Na, Nb, Norb)],
|
|
107
|
+
msg=f"filling ({Na},{Nb},{Norb})")
|
|
108
|
+
|
|
109
|
+
def test_full_standardization_lands_in_basis_with_correct_sign(self):
|
|
110
|
+
# the fixed lookup map: every canonicalized-space det lands ON the
|
|
111
|
+
# basis, with the sign of the independent inversion-parity reference
|
|
112
|
+
for Na, Nb, Norb in FILLINGS:
|
|
113
|
+
basis = generate_det_strings(Na, Nb, Norb)
|
|
114
|
+
bset = set(basis)
|
|
115
|
+
ref_idx = _soset_index(basis)
|
|
116
|
+
for d in _canonicalized_space(Na, Nb, Norb):
|
|
117
|
+
key, sgn = _standardize_full(d)
|
|
118
|
+
self.assertIn(key, bset,
|
|
119
|
+
msg=f"({Na},{Nb},{Norb}): {d!r} -> {key!r}")
|
|
120
|
+
self.assertEqual((key, sgn), _ref_canonical(d, ref_idx),
|
|
121
|
+
msg=f"({Na},{Nb},{Norb}): {d!r}")
|
|
122
|
+
|
|
123
|
+
def test_structure_vector_lookup_succeeds_at_all_fillings(self):
|
|
124
|
+
# end-to-end: structure_vector places every sampled det (including
|
|
125
|
+
# the ones bare standardize_det mishandles) at the right position
|
|
126
|
+
# with the right sign, and never raises
|
|
127
|
+
for Na, Nb, Norb in FILLINGS:
|
|
128
|
+
basis = generate_det_strings(Na, Nb, Norb)
|
|
129
|
+
ref_idx = _soset_index(basis)
|
|
130
|
+
space = _canonicalized_space(Na, Nb, Norb)
|
|
131
|
+
bset = set(basis)
|
|
132
|
+
missing = [d for d in space if standardize_det(d)[0] not in bset]
|
|
133
|
+
step = max(1, len(space) // 20)
|
|
134
|
+
sample = space[::step] + missing[::max(1, len(missing) // 20)]
|
|
135
|
+
for d in sample:
|
|
136
|
+
v = structure_vector(FixedPsi(d), basis)
|
|
137
|
+
key, sgn = _ref_canonical(d, ref_idx)
|
|
138
|
+
nz = {basis[i]: v[i] for i in range(len(basis)) if v[i] != 0}
|
|
139
|
+
self.assertEqual(nz, {key: sgn},
|
|
140
|
+
msg=f"({Na},{Nb},{Norb}): {d!r}")
|
|
141
|
+
|
|
142
|
+
def test_outside_basis_raises_named_valueerror(self):
|
|
143
|
+
basis = generate_det_strings(2, 1, 4) # orbitals a..d only
|
|
144
|
+
with self.assertRaises(ValueError) as cm:
|
|
145
|
+
structure_vector(FixedPsi('aEb'), basis)
|
|
146
|
+
self.assertIn('aEb', str(cm.exception))
|
|
147
|
+
|
|
148
|
+
def test_duplicate_basis_det_raises(self):
|
|
149
|
+
# 'abA' is the same determinant as 'aAb' in a different creation order
|
|
150
|
+
with self.assertRaises(ValueError):
|
|
151
|
+
structure_vector(FixedPsi('aAb'), ['aAb', 'abA'])
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class TestHoleBasisRoundTrip326(unittest.TestCase):
|
|
155
|
+
"""(H2)3+ sigma-hole diabatics on the (3,2,6) basis: structure_vector must
|
|
156
|
+
reproduce the low-level to_standard mapping that
|
|
157
|
+
examples/h2h2h2_plus_diabatic.py hand-rolls (positions AND signs)."""
|
|
158
|
+
|
|
159
|
+
ORBS = 'abcdef'
|
|
160
|
+
PAIRS = [('a', 'b'), ('c', 'd'), ('e', 'f')]
|
|
161
|
+
|
|
162
|
+
def _to_standard(self, det_string):
|
|
163
|
+
# replicated from examples/h2h2h2_plus_diabatic.py
|
|
164
|
+
so_list = [2 * self.ORBS.index(c.lower()) + (0 if c.islower() else 1)
|
|
165
|
+
for c in det_string]
|
|
166
|
+
if len(set(so_list)) != len(so_list):
|
|
167
|
+
return None, 0
|
|
168
|
+
alphas = sorted(c for c in det_string if c.islower())
|
|
169
|
+
betas = sorted(c for c in det_string if c.isupper())
|
|
170
|
+
std = ''
|
|
171
|
+
na, nb = len(alphas), len(betas)
|
|
172
|
+
for i in range(min(na, nb)):
|
|
173
|
+
std += alphas[i] + betas[i]
|
|
174
|
+
std += ''.join(alphas[nb:]) + ''.join(betas[na:])
|
|
175
|
+
target = [2 * self.ORBS.index(c.lower()) + (0 if c.islower() else 1)
|
|
176
|
+
for c in std]
|
|
177
|
+
pos = {v: i for i, v in enumerate(target)}
|
|
178
|
+
idx = [pos[v] for v in so_list]
|
|
179
|
+
inv = sum(1 for i in range(len(idx)) for j in range(i + 1, len(idx))
|
|
180
|
+
if idx[i] > idx[j])
|
|
181
|
+
return std, (-1 if inv % 2 else 1)
|
|
182
|
+
|
|
183
|
+
def _hole_raws(self, hole_pair):
|
|
184
|
+
hole_atoms = self.PAIRS[hole_pair]
|
|
185
|
+
full_idx = [i for i in range(3) if i != hole_pair]
|
|
186
|
+
f0, f1 = self.PAIRS[full_idx[0]], self.PAIRS[full_idx[1]]
|
|
187
|
+
return [h_a + f0a + f0b.upper() + f1a + f1b.upper()
|
|
188
|
+
for h_a in hole_atoms
|
|
189
|
+
for f0a in f0 for f0b in f0
|
|
190
|
+
for f1a in f1 for f1b in f1]
|
|
191
|
+
|
|
192
|
+
def test_hole_diabatics_match_to_standard_route(self):
|
|
193
|
+
basis = generate_det_strings(3, 2, 6)
|
|
194
|
+
ds_to_idx = {d: i for i, d in enumerate(basis)}
|
|
195
|
+
cols = []
|
|
196
|
+
n_outside = 0
|
|
197
|
+
for hp in range(3):
|
|
198
|
+
raws = self._hole_raws(hp)
|
|
199
|
+
self.assertEqual(len(raws), 32)
|
|
200
|
+
n_outside += sum(1 for r in raws
|
|
201
|
+
if standardize_det(r)[0] not in ds_to_idx)
|
|
202
|
+
v_ref = np.zeros(len(basis))
|
|
203
|
+
for raw in raws:
|
|
204
|
+
std, sgn = self._to_standard(raw)
|
|
205
|
+
self.assertIsNotNone(std)
|
|
206
|
+
v_ref[ds_to_idx[std]] += sgn
|
|
207
|
+
fp = FixedPsi()
|
|
208
|
+
for raw in raws:
|
|
209
|
+
fp.add_str_det(raw, coef=1)
|
|
210
|
+
v = np.array(structure_vector(fp, basis), float).ravel()
|
|
211
|
+
self.assertTrue(np.array_equal(v, v_ref), msg=f"hole {hp}")
|
|
212
|
+
cols.append(v)
|
|
213
|
+
# the audited mismatch: 64 of the 96 raw hole dets are exactly the
|
|
214
|
+
# strings the pre-fix lookup raised on
|
|
215
|
+
self.assertEqual(n_outside, 64)
|
|
216
|
+
# the three diabatics are orthonormal at s = 0 (norm^2 = 32 raw dets
|
|
217
|
+
# collapsing pairwise onto 32 basis dets of unit coefficient)
|
|
218
|
+
Phi = np.column_stack(cols)
|
|
219
|
+
self.assertTrue(np.allclose(Phi.T @ Phi / 32.0, np.eye(3),
|
|
220
|
+
atol=1e-14))
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
class TestNoRegression(unittest.TestCase):
|
|
224
|
+
"""Columns the pre-fix code produced correctly must be reproduced exactly."""
|
|
225
|
+
|
|
226
|
+
def test_223_longbond_column_exact(self):
|
|
227
|
+
# the (2,2,3) long-bond column pinned by TestStructureVector in
|
|
228
|
+
# test_system.py, and the full old-vs-new comparison on all three
|
|
229
|
+
# allyl Rumer structures
|
|
230
|
+
basis = [p.dets[0].det_string for p in generate_dets(2, 2, 3)]
|
|
231
|
+
structs = [FixedPsi('aBcC', coupled_pairs=[(0, 1)]),
|
|
232
|
+
FixedPsi('aAbC', coupled_pairs=[(2, 3)]),
|
|
233
|
+
FixedPsi('abBC', coupled_pairs=[(0, 3)])]
|
|
234
|
+
for st in structs:
|
|
235
|
+
self.assertEqual(structure_vector(st, basis),
|
|
236
|
+
_old_structure_vector(st, basis))
|
|
237
|
+
v = structure_vector(structs[2], basis)
|
|
238
|
+
nz = {basis[i]: v[i] for i in range(len(basis)) if v[i] != 0}
|
|
239
|
+
self.assertEqual(nz, {'aBbC': -1, 'bAcB': -1})
|
|
240
|
+
|
|
241
|
+
def test_214_hole_columns_exact(self):
|
|
242
|
+
# (2,1,4) is a filling where bare standardize_det never leaves the
|
|
243
|
+
# basis (census count 0), so the old algorithm was fully correct
|
|
244
|
+
# there; the new lookup must agree determinant-for-determinant
|
|
245
|
+
basis = [p.dets[0].det_string for p in generate_dets(2, 1, 4)]
|
|
246
|
+
hole0 = FixedPsi()
|
|
247
|
+
for ha in 'ab':
|
|
248
|
+
for fa in 'cd':
|
|
249
|
+
for fb in 'CD':
|
|
250
|
+
hole0.add_str_det(ha + fa + fb, coef=1)
|
|
251
|
+
hole1 = FixedPsi()
|
|
252
|
+
for ha in 'cd':
|
|
253
|
+
for fa in 'ab':
|
|
254
|
+
for fb in 'AB':
|
|
255
|
+
hole1.add_str_det(fa + fb + ha, coef=1)
|
|
256
|
+
for st in [hole0, hole1, FixedPsi('acC'), FixedPsi('aCc')]:
|
|
257
|
+
self.assertEqual(structure_vector(st, basis),
|
|
258
|
+
_old_structure_vector(st, basis))
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
class TestProjectionConsistency(unittest.TestCase):
|
|
262
|
+
def test_vthv_matches_direct_hamiltonian_unequal_filling(self):
|
|
263
|
+
# (H2)2+ dimer, Na=2/Nb=1/4 orbitals: projecting the FCI (H, S)
|
|
264
|
+
# through structure_vector columns must equal (H, S) built directly
|
|
265
|
+
# over the structures
|
|
266
|
+
m = Molecule(zero_ii=True, interacting_orbs=['ab', 'cd', 'bc'],
|
|
267
|
+
subst={'h': ('H_ab', 'H_cd'), 't': ('H_bc',),
|
|
268
|
+
's': ('S_ab', 'S_cd'), 'sg': ('S_bc',)},
|
|
269
|
+
subst_2e={'U': ('1111',)}, max_2e_centers=1)
|
|
270
|
+
dets = generate_dets(2, 1, 4)
|
|
271
|
+
strs = [p.dets[0].det_string for p in dets]
|
|
272
|
+
hole0 = FixedPsi()
|
|
273
|
+
for ha in 'ab':
|
|
274
|
+
for fa in 'cd':
|
|
275
|
+
for fb in 'CD':
|
|
276
|
+
hole0.add_str_det(ha + fa + fb, coef=1)
|
|
277
|
+
hole1 = FixedPsi()
|
|
278
|
+
for ha in 'cd':
|
|
279
|
+
for fa in 'ab':
|
|
280
|
+
for fb in 'AB':
|
|
281
|
+
hole1.add_str_det(fa + fb + ha, coef=1)
|
|
282
|
+
structs = [hole0, hole1]
|
|
283
|
+
|
|
284
|
+
Hfci, Sfci = hamiltonian(m, dets)
|
|
285
|
+
V = sp.Matrix.hstack(*[structure_vector(st, strs) for st in structs])
|
|
286
|
+
H_dir, S_dir = hamiltonian(m, structs)
|
|
287
|
+
self.assertTrue(sp.simplify(V.T * Hfci * V - H_dir).is_zero_matrix)
|
|
288
|
+
self.assertTrue(sp.simplify(V.T * Sfci * V - S_dir).is_zero_matrix)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
if __name__ == '__main__':
|
|
292
|
+
unittest.main()
|