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/molecule.py ADDED
@@ -0,0 +1,736 @@
1
+ from functools import lru_cache
2
+
3
+ import numpy
4
+ import sympy as sp
5
+ from scipy.stats import rankdata
6
+
7
+ from symvb.functions import attempt_int, standardize_det, sort_ind, canonical_chemist_iv, simplify_matrix
8
+ from symvb.numerical import get_coupled
9
+ from symvb.fixed_psi import FixedPsi, generate_dets
10
+ from symvb.numerical import get_combined_from_dict
11
+ from symvb.slaterdet import SlaterDet
12
+
13
+
14
+ @lru_cache(maxsize=None)
15
+ def _cached_symbol(name):
16
+ return sp.Symbol(name)
17
+
18
+
19
+ _SP_ZERO = sp.Integer(0)
20
+ _SP_ONE = sp.Integer(1)
21
+
22
+
23
+ class Molecule:
24
+ # Contains molecule-specific information
25
+ O2_METHODS = ('direct', 'blocked')
26
+
27
+ def __init__(self, symm_offdiagonal=True, normalized_basis_orbs=True,
28
+ interacting_orbs=None, subst=None, zero_ii=True,
29
+ subst_2e=None, max_2e_centers=4, o2_method='blocked',
30
+ orbitals=None):
31
+ """
32
+ subst contains a list of substitutions to be made, eg ['S':('S_ab','S_bc','S_cd'),'H':('H_ab','H_bc')]
33
+ zero_ii=True sets all H_ii terms to zero
34
+ interacting_orbs is a list of two-letter lowercase strings, eg ['ab','bc','ad'].
35
+ Only these orbital pairs have non-zero integrals
36
+ symm_offdiagonal = True; symmetric matrix
37
+ normalized_basis_orbs = True; S_ii = 1
38
+ o2_method selects the two-electron implementation: 'blocked' (default,
39
+ spin-block-aware precompute, see symvb/_o2_blocked.py) or 'direct'
40
+ (Loewdin cofactor on the spin-mixed (N-2)-electron string). Both return
41
+ identical matrices; 'blocked' is markedly faster for larger bases.
42
+ orbitals: optional whitelist of allowed orbital labels, e.g. 'abcdef'
43
+ or {'a','b','c'}. When provided, any det string presented to Op/o2/
44
+ build_matrix that uses a label outside this set raises ValueError.
45
+ Also validates interacting_orbs entries at construction. Default None
46
+ keeps the duck-typed behavior (any character accepted).
47
+ """
48
+ if o2_method not in self.O2_METHODS:
49
+ raise ValueError(
50
+ "o2_method must be one of %r, got %r" % (self.O2_METHODS, o2_method))
51
+
52
+ if orbitals is None:
53
+ self.orbitals = None
54
+ else:
55
+ chars = frozenset(c.lower() for c in orbitals)
56
+ for c in chars:
57
+ if not c.isalpha() or len(c) != 1:
58
+ raise ValueError(
59
+ "orbitals must contain single alphabetic characters; got %r" % c)
60
+ self.orbitals = chars
61
+ if interacting_orbs is not None:
62
+ for pair in interacting_orbs:
63
+ bad = [c for c in pair if c.lower() not in chars]
64
+ if bad:
65
+ raise ValueError(
66
+ "interacting_orbs entry %r references orbital(s) %r "
67
+ "not in whitelist %r" % (pair, bad, sorted(chars)))
68
+
69
+ self.symm_offdiagonal = symm_offdiagonal
70
+ self.normalized_basis_orbs = normalized_basis_orbs
71
+ self.interacting_orbs = interacting_orbs # list of two-letter lowercase strings, eg ['ab','bc','ad']
72
+
73
+ self.subst = {}
74
+ self.subst_2e = {}
75
+
76
+ self.basis = None
77
+ self.basis_a, self.basis_b = None, None
78
+ self.aH, self.aS = None, None
79
+ self.bH, self.bS = None, None
80
+ self.lookup_a, self.lookup_b = {}, {}
81
+ self.precalculated_half_dets = False
82
+
83
+ if subst is None:
84
+ subst = {}
85
+ self.parse_subst(subst)
86
+
87
+ if subst_2e is None:
88
+ subst_2e = {}
89
+ self.parse_subst_2e(subst_2e)
90
+
91
+ self.zero_ii = zero_ii
92
+ self.max_2e_centers = max_2e_centers
93
+ self.o2_method = o2_method
94
+
95
+ self._o1_expr_cache = {}
96
+ self._o2_expr_cache = {}
97
+
98
+ # ------------------------------------------------------------------
99
+ # topology constructors (convenience): build the interacting_orbs and
100
+ # subst/subst_2e dicts for the common ring and chain models, so callers
101
+ # do not spell out every edge by hand.
102
+ # ------------------------------------------------------------------
103
+ @staticmethod
104
+ def _topology_orbitals(L):
105
+ if L > 26:
106
+ raise ValueError("ring/chain helpers support up to 26 orbitals (a-z)")
107
+ return [chr(ord('a') + i) for i in range(L)]
108
+
109
+ @classmethod
110
+ def _from_edges(cls, edges, h, s, U, hubbard, zero_ii, **kw):
111
+ seen = [] # dedupe (e.g. a 2-ring has one edge twice)
112
+ for e in edges:
113
+ if e not in seen:
114
+ seen.append(e)
115
+ edges = seen
116
+ subst = {h: tuple('H_' + e for e in edges),
117
+ s: tuple('S_' + e for e in edges)}
118
+ kwargs = dict(interacting_orbs=edges, subst=subst, zero_ii=zero_ii)
119
+ if hubbard:
120
+ kwargs['subst_2e'] = {U: ('1111',)}
121
+ kwargs['max_2e_centers'] = 1
122
+ kwargs.update(kw)
123
+ return cls(**kwargs)
124
+
125
+ @classmethod
126
+ def ring(cls, L, h='h', s='s', U='U', hubbard=True, zero_ii=True, **kw):
127
+ """Cyclic ``L``-orbital ring (orbitals ``a, b, c, ...``).
128
+
129
+ Nearest-neighbour resonance ``h`` and overlap ``s`` on every edge, and
130
+ on-site Hubbard ``U`` when ``hubbard`` (the default). Returns a
131
+ configured ``Molecule``; extra keywords pass through to ``__init__``.
132
+ """
133
+ orbs = cls._topology_orbitals(L)
134
+ edges = [''.join(sorted((orbs[i], orbs[(i + 1) % L]))) for i in range(L)]
135
+ return cls._from_edges(edges, h, s, U, hubbard, zero_ii, **kw)
136
+
137
+ @classmethod
138
+ def chain(cls, n, h='h', s='s', U='U', hubbard=True, zero_ii=True, **kw):
139
+ """Linear ``n``-orbital chain; same conventions as :meth:`ring` without
140
+ the wrap-around edge."""
141
+ orbs = cls._topology_orbitals(n)
142
+ edges = [''.join(sorted((orbs[i], orbs[i + 1]))) for i in range(n - 1)]
143
+ return cls._from_edges(edges, h, s, U, hubbard, zero_ii, **kw)
144
+
145
+ def generate_basis(self, Na, Nb, Norbs):
146
+
147
+ self.precalculated_half_dets = False
148
+
149
+ self.basis = generate_dets(Na, Nb, Norbs)
150
+
151
+ self.basis_a = generate_dets(Na, 0, Norbs)
152
+ for i in range(len(self.basis_a)):
153
+ self.lookup_a[self.basis_a[i].dets[0].det_string] = i
154
+
155
+ if Na == Nb:
156
+ self.basis_b, self.lookup_b = self.basis_a, self.lookup_a
157
+ else:
158
+ self.basis_b = generate_dets(Nb, 0, Norbs) # all lookups will be by lower case
159
+ for i in range(len(self.basis_b)):
160
+ self.lookup_b[self.basis_b[i].dets[0].det_string] = i
161
+
162
+ self.aH = self.build_matrix(self.basis_a, op='H')
163
+ self.aS = self.build_matrix(self.basis_a, op='S')
164
+ if Na == Nb:
165
+ self.bH, self.bS = self.aH, self.aS
166
+ else:
167
+ self.bH = self.build_matrix(self.basis_b, op='H')
168
+ self.bS = self.build_matrix(self.basis_b, op='S')
169
+ self.precalculated_half_dets = True
170
+
171
+ def parse_subst(self, subst):
172
+ for k, v in subst.items():
173
+ if isinstance(v, str):
174
+ self.subst[v] = k
175
+ else:
176
+ for s in v:
177
+ self.subst[s] = k
178
+
179
+ def parse_subst_2e(self, subst_2e):
180
+ for k, v in subst_2e.items():
181
+ if isinstance(v, str):
182
+ self.subst_2e[v] = k
183
+ else:
184
+ for s in v:
185
+ self.subst_2e[s] = k
186
+
187
+ def get_o1_name(self, a, b, o):
188
+ # sort two orbital indices in the alphabetic order
189
+ if self.symm_offdiagonal and (a > b):
190
+ a, b = b, a
191
+
192
+ # If only certain orbitals are allowed to interact,
193
+ # check if the orbital pair is in the allowed list
194
+ if self.interacting_orbs is not None and (a != b):
195
+ if not (a + b) in self.interacting_orbs:
196
+ return '0' # non-interacting orbitals will always give 0 in any term of the direct product
197
+
198
+ # Replace terms S_xx by 1 if allowed
199
+ if self.normalized_basis_orbs and (a == b) and (o == 'S'):
200
+ return '1'
201
+
202
+ # replace the site energies H_ii by zero if allowed
203
+ if self.zero_ii and (a == b) and (o == 'H'):
204
+ return '0'
205
+
206
+ s = '%s_%s%s' % (o, a, b)
207
+
208
+ # substitute certain AO matrix elements if needed
209
+ if s in self.subst:
210
+ s = self.subst[s]
211
+
212
+ return s
213
+
214
+ def get_o1_expr(self, a, b, o):
215
+ # Cached sympy expression for a one-electron AO matrix element
216
+ key = (a, b, o)
217
+ cached = self._o1_expr_cache.get(key)
218
+ if cached is not None:
219
+ return cached
220
+ name = self.get_o1_name(a, b, o)
221
+ if name == '0':
222
+ expr = _SP_ZERO
223
+ elif name == '1':
224
+ expr = _SP_ONE
225
+ else:
226
+ expr = _cached_symbol(name)
227
+ self._o1_expr_cache[key] = expr
228
+ return expr
229
+
230
+ def Op_Hartree_product(self, L_orbs, R_orbs, op='H'):
231
+ # Computes a matrix element for two orbital products, e.g <A(1)b(2)...|O|A(1)b(2)...>.
232
+ # Returns a sympy expression.
233
+
234
+ nL = len(L_orbs)
235
+ nR = len(R_orbs)
236
+
237
+ if nL != nR:
238
+ return _SP_ZERO
239
+
240
+ if nL == 0:
241
+ return _SP_ONE if op == 'S' else _SP_ZERO
242
+
243
+ lL = L_orbs.lower()
244
+ lR = R_orbs.lower()
245
+
246
+ sum_terms = []
247
+ for i_op in range(nL):
248
+ prod_factors = []
249
+ term_zero = False
250
+ for j in range(nL):
251
+ o = op if i_op == j else 'S'
252
+ s = self.get_o1_expr(lL[j], lR[j], o)
253
+ if s is _SP_ZERO:
254
+ term_zero = True
255
+ break
256
+ if s is _SP_ONE:
257
+ continue
258
+ prod_factors.append(s)
259
+
260
+ if term_zero:
261
+ elem = _SP_ZERO
262
+ elif not prod_factors:
263
+ elem = _SP_ONE
264
+ elif len(prod_factors) == 1:
265
+ elem = prod_factors[0]
266
+ else:
267
+ elem = sp.Mul(*prod_factors)
268
+
269
+ if op == 'S':
270
+ # All Hartree products in <L|S|R> are identical; one is enough.
271
+ return elem
272
+
273
+ if elem is not _SP_ZERO:
274
+ sum_terms.append(elem)
275
+
276
+ if not sum_terms:
277
+ return _SP_ZERO
278
+ if len(sum_terms) == 1:
279
+ return sum_terms[0]
280
+ return sp.Add(*sum_terms)
281
+
282
+ op_orbprod = Op_Hartree_product
283
+
284
+ def op_det(self, L, R, op='H'):
285
+ # Returns the matrix element < L | O | R > as a sympy expression.
286
+ # L, R are instances of SlaterDet
287
+
288
+ if not R.is_compatible(L):
289
+ return _SP_ZERO
290
+
291
+ if self.precalculated_half_dets and op in ('H', 'S'):
292
+ # Fast path is only valid when both dets have the same alpha / beta
293
+ # electron counts as the precomputed basis. Two-electron operator
294
+ # construction synthesises sub-determinants with fewer electrons,
295
+ # which don't live in the precomputed lookup tables -- fall through
296
+ # to the general Hartree-product expansion below.
297
+ iLa = self.lookup_a.get(L.alpha_string)
298
+ iRa = self.lookup_a.get(R.alpha_string)
299
+ iLb = self.lookup_b.get(L.beta_string.lower())
300
+ iRb = self.lookup_b.get(R.beta_string.lower())
301
+ if None not in (iLa, iRa, iLb, iRb):
302
+ if op == 'H':
303
+ return self.aH[iLa, iRa] * self.bS[iLb, iRb] + self.aS[iLa, iRa] * self.bH[iLb, iRb]
304
+ return self.aS[iLa, iRa] * self.bS[iLb, iRb]
305
+
306
+ R_orbs, R_signs = R.get_orbital_permutations()
307
+ terms = []
308
+ for R_orb, R_sign in zip(R_orbs, R_signs):
309
+ elem = self.op_orbprod(L.det_string, R_orb, op=op)
310
+ if elem is _SP_ZERO:
311
+ continue
312
+ terms.append(elem if R_sign == 1 else -elem)
313
+
314
+ if not terms:
315
+ return _SP_ZERO
316
+ if len(terms) == 1:
317
+ return terms[0]
318
+ return sp.Add(*terms)
319
+
320
+ def op_fixed_psi(self, L, R, op='H'):
321
+ if len(L) == 0:
322
+ return _SP_ONE if op == 'S' else _SP_ZERO
323
+
324
+ sum_terms = []
325
+ for detL, cL in L:
326
+ for detR, cR in R:
327
+ elem = self.op_det(detL, detR, op=op)
328
+ if elem is _SP_ZERO:
329
+ continue
330
+ prd = cL * cR
331
+ if prd == 1:
332
+ sum_terms.append(elem)
333
+ elif prd == -1:
334
+ sum_terms.append(-elem)
335
+ else:
336
+ sum_terms.append(prd * elem)
337
+
338
+ if not sum_terms:
339
+ return _SP_ZERO
340
+ if len(sum_terms) == 1:
341
+ return sum_terms[0]
342
+ return sp.Add(*sum_terms)
343
+
344
+ def _check_orbitals_in_psi(self, psi):
345
+ if self.orbitals is None:
346
+ return
347
+ allowed = self.orbitals
348
+ for det in psi.dets:
349
+ for c in det.det_string:
350
+ if c.lower() not in allowed:
351
+ raise ValueError(
352
+ "det %r uses orbital %r outside whitelist %r"
353
+ % (det.det_string, c, sorted(allowed)))
354
+
355
+ def Op(self, L, R, op='H'):
356
+ L = FixedPsi(L)
357
+ R = FixedPsi(R)
358
+ self._check_orbitals_in_psi(L)
359
+ self._check_orbitals_in_psi(R)
360
+ return self.op_fixed_psi(L, R, op=op)
361
+
362
+ def Ops(self, L, R, op='H', find_factors=True):
363
+ z = self.Op(L=L, R=R, op=op)
364
+ if find_factors:
365
+ z = sp.factor(z)
366
+ return z
367
+
368
+ def getS(self, L, R, find_factors=True):
369
+ return self.Ops(L, R, op='S', find_factors=find_factors)
370
+
371
+ def getH(self, L, R, find_factors=True):
372
+ return self.Ops(L, R, op='H', find_factors=find_factors)
373
+
374
+ def build_matrix(self, u, op='H'):
375
+ """
376
+ Builds a square matrix of integrals for each pair of wavefunctions in a given array
377
+ :param u: array of FixedPsi, SlaterDet, or str
378
+ :param op: the integration operator
379
+ :return: SymPy matrix with integrals
380
+ """
381
+ N = len(u)
382
+ m = sp.zeros(N)
383
+ if N == 0:
384
+ return m
385
+
386
+ # Coerce each entry to a FixedPsi for uniform handling, standardize it
387
+ # into canonical creation order (so the spin-pattern matching below is
388
+ # correct for hand-built structures written in a natural, non-canonical
389
+ # order), and pre-compute the set of spin patterns it carries. Two
390
+ # FixedPsi are guaranteed to yield zero whenever their spin-pattern sets
391
+ # are disjoint -- skip those pairs entirely instead of running the full
392
+ # Op machinery. Standardizing is a no-op on an already-canonical basis
393
+ # (e.g. generate_dets output), so this does not change existing results;
394
+ # it only repairs the silently-dropped couplings between structures
395
+ # whose raw spin patterns differ (e.g. an alpha-alpha-beta-beta long
396
+ # bond from coupled_pairs against an interleaved Kekule structure).
397
+ psis = [None] * N
398
+ spin_keys = [None] * N
399
+ for i, entry in enumerate(u):
400
+ fp = FixedPsi(entry) # always a fresh copy; never mutate the caller's object
401
+ fp.standardize()
402
+ self._check_orbitals_in_psi(fp)
403
+ psis[i] = fp
404
+ spin_keys[i] = frozenset(d.spins for d in fp.dets)
405
+
406
+ # Inline fast-path: when half-dets are precomputed and every basis entry
407
+ # is a single Slater determinant with unit coefficient, skip the
408
+ # FixedPsi/Op layer and index aH/aS/bH/bS directly.
409
+ fast = (self.precalculated_half_dets and op in ('H', 'S')
410
+ and all(len(p.dets) == 1 and p.coefs[0] == 1 for p in psis))
411
+
412
+ if fast:
413
+ half = [None] * N
414
+ for i, p in enumerate(psis):
415
+ d = p.dets[0]
416
+ half[i] = (self.lookup_a[d.alpha_string],
417
+ self.lookup_b[d.beta_string.lower()],
418
+ d.spins)
419
+ aH, aS, bH, bS = self.aH, self.aS, self.bH, self.bS
420
+ for i in range(N):
421
+ iLa, iLb, sL = half[i]
422
+ for j in range(i, N):
423
+ iRa, iRb, sR = half[j]
424
+ if sL != sR:
425
+ continue
426
+ if op == 'H':
427
+ v = aH[iLa, iRa] * bS[iLb, iRb] + aS[iLa, iRa] * bH[iLb, iRb]
428
+ else:
429
+ v = aS[iLa, iRa] * bS[iLb, iRb]
430
+ m[i, j] = v
431
+ if i != j:
432
+ m[j, i] = v
433
+ return m
434
+
435
+ for i in range(N):
436
+ ki = spin_keys[i]
437
+ for j in range(i, N):
438
+ if ki.isdisjoint(spin_keys[j]):
439
+ continue
440
+ v = self.op_fixed_psi(psis[i], psis[j], op=op)
441
+ m[i, j] = v
442
+ if i != j:
443
+ m[j, i] = v
444
+ return m
445
+
446
+ def energy(self, P, o2=False):
447
+ """
448
+ Find the energy for the FixedPsi object: E = <P|H|P> / <P|P>
449
+ :param P: A wavefunction: FixedPsi, SlaterDet, or str
450
+ :return: Expression for the normalized energy: N_el * <P | H | P> / <P | P>
451
+ """
452
+ E = self.Ops(P, P, op='H')
453
+ S = self.Ops(P, P, op='S')
454
+ if o2:
455
+ return (E / S) + sp.simplify(sp.simplify(self.o2_fixed_psi(P, P)) / S)
456
+ else:
457
+ return E / S
458
+
459
+ def couple(self, P=None, mS=None, mH=None, N_tries=10, precision=12, ranges={'h':(-1.0,0.0),'s':(0.0,1.0)}, nums=None):
460
+ """
461
+ Group the FixedPsi objects that have constant ratios in the lowest energy wave vector
462
+ The constant ratios are found by numerical simulation
463
+ :param P: list of FixedPsi objects
464
+ :param N_tries: number of trials
465
+ :param precision: 10^-precision is the matching threshold
466
+ :return:
467
+ """
468
+ if mS is None:
469
+ mS = self.build_matrix(P, op='S')
470
+ if mH is None:
471
+ mH = self.build_matrix(P, op='H')
472
+
473
+ ranges2 = {}
474
+ symbols = mH.free_symbols.union(mS.free_symbols)
475
+ for s in symbols:
476
+ ss = str(s)
477
+ if ss in ranges:
478
+ ranges2[s] = ranges[ss]
479
+ else:
480
+ assert nums is not None and ss in nums, "Missing numerical value for the parameter " + ss
481
+ ranges2[s] = (nums[ss], nums[ss])
482
+
483
+ couplings = get_coupled(mS=mS, mH=mH, N_tries=N_tries, precision=precision, ranges=ranges2)
484
+ return get_combined_from_dict(P, couplings)
485
+
486
+ def get_o2_name(self, v):
487
+ """
488
+ Gets the standardized name of the 2e integral. Uses integral symmetries to sort indices.
489
+ Substitutes the integral by name if provided
490
+ Parameters
491
+ ----------
492
+ v: list of one-letter lower-case orbital names
493
+
494
+ Returns
495
+ -------
496
+ string with the integral name
497
+ """
498
+ if len(numpy.unique((v[0].lower(), v[1].lower(), v[2].lower(), v[3].lower()))) > self.max_2e_centers:
499
+ return '0'
500
+ # Canonical form under the chemist 8-fold permutation symmetry of
501
+ # (ij|kl). Unlike sort_ind, this works correctly when AO labels
502
+ # repeat: (a,a,b,c) and (a,a,c,b) both canonicalise to (a,a,b,c),
503
+ # so they receive the same default T_ name and the same dense-rank
504
+ # subst_2e key. Necessary for symbolic outputs to be uniquely-named
505
+ # at max_2e_centers >= 3.
506
+ lowered = (v[0].lower(), v[1].lower(), v[2].lower(), v[3].lower())
507
+ tiv = canonical_chemist_iv(lowered)
508
+ indices = '%s%s%s%s' % tiv
509
+ int_name = 'T_%s' % indices
510
+
511
+ if self.subst_2e is not None:
512
+ r = '%s%s%s%s' % tuple(rankdata(tiv, method='dense'))
513
+ if r in self.subst_2e:
514
+ int_name = self.subst_2e[r]
515
+ return int_name
516
+
517
+ def get_o2_expr(self, v):
518
+ """Cached sympy expression for the two-electron integral over v (4 orbitals).
519
+
520
+ Memoised on the lowercased AO 4-tuple (the only data get_o2_name
521
+ actually uses). After the first call for a given (p,q,r,s), all
522
+ subsequent calls are dict lookups, bypassing the rankdata/sort_ind
523
+ canonicalisation. Benefits both 'direct' and 'blocked' o2 paths
524
+ when the same integral pattern recurs many times across the
525
+ basis (e.g. benzene PPP: 6^4 = 1296 distinct entries vs. ~13M
526
+ full-pair contraction queries)."""
527
+ if isinstance(v, tuple):
528
+ key = v
529
+ else:
530
+ key = tuple(v)
531
+ cached = self._o2_expr_cache.get(key)
532
+ if cached is not None:
533
+ return cached
534
+ name = self.get_o2_name(v)
535
+ if name == '0':
536
+ result = _SP_ZERO
537
+ else:
538
+ result = _cached_symbol(name)
539
+ self._o2_expr_cache[key] = result
540
+ return result
541
+
542
+ def o2_det(self, D1, D2):
543
+ """
544
+ Two-electron matrix element <D1|1/r_12|D2> between two SlaterDets.
545
+ Returns a sympy expression. Uses Slater-Condon rules for arbitrary
546
+ spin-orbital occupations; evaluates in the (possibly non-orthogonal)
547
+ AO basis by computing overlap cofactors for the (N-2)-electron
548
+ sub-determinants via Op(op='S').
549
+
550
+ Dispatches on self.o2_method ('direct' or 'blocked'). The 'direct'
551
+ path is the historical implementation. The 'blocked' path is the
552
+ spin-block-aware reformulation (see symvb/_o2_blocked.py) — opt-in
553
+ until the agreement-test harness validates parity.
554
+ """
555
+ if self.o2_method == 'blocked':
556
+ from symvb._o2_blocked import o2_det_blocked
557
+ return o2_det_blocked(self, D1, D2)
558
+
559
+ assert D1.Nel == D2.Nel, 'Different number of electrons'
560
+ Nel = D1.Nel
561
+ D1s = D1.det_string
562
+ D2s = D2.det_string
563
+
564
+ terms = []
565
+ for i in range(Nel):
566
+ for j in range(i + 1, Nel):
567
+ s1 = D1s[:i] + D1s[i + 1:j] + D1s[j + 1:]
568
+ c1, c2 = D1s[i], D1s[j]
569
+ sumL = c1.islower() + c2.islower()
570
+ sd1, f1 = standardize_det(s1)
571
+ sd1_obj = SlaterDet(sd1)
572
+
573
+ for k in range(Nel):
574
+ for mm in range(k + 1, Nel):
575
+ s2 = D2s[:k] + D2s[k + 1:mm] + D2s[mm + 1:]
576
+ c3, c4 = D2s[k], D2s[mm]
577
+
578
+ if len(numpy.unique((c1.lower(), c2.lower(),
579
+ c3.lower(), c4.lower()))) > self.max_2e_centers:
580
+ continue
581
+
582
+ sumR = c3.islower() + c4.islower()
583
+ if sumL != sumR:
584
+ continue
585
+
586
+ sd2, f2 = standardize_det(s2)
587
+ sd2_obj = SlaterDet(sd2)
588
+
589
+ opS = self.op_det(sd1_obj, sd2_obj, op='S')
590
+ if opS is _SP_ZERO:
591
+ continue
592
+
593
+ parity = (i + j + k + mm + f1 + f2) % 2
594
+
595
+ if c1.islower() == c3.islower():
596
+ iv = (c1.lower(), c2.lower(), c3.lower(), c4.lower())
597
+ int_sym = self.get_o2_expr(iv)
598
+ if int_sym is not _SP_ZERO:
599
+ sign = 1 if parity == 0 else -1
600
+ terms.append(sign * int_sym * opS)
601
+
602
+ if c1.islower() == c4.islower():
603
+ iv = (c1.lower(), c2.lower(), c4.lower(), c3.lower())
604
+ int_sym = self.get_o2_expr(iv)
605
+ if int_sym is not _SP_ZERO:
606
+ sign = 1 if parity == 1 else -1
607
+ terms.append(sign * int_sym * opS)
608
+
609
+ if not terms:
610
+ return _SP_ZERO
611
+ if len(terms) == 1:
612
+ return terms[0]
613
+ return sp.Add(*terms)
614
+
615
+ def o2_fixed_psi(self, L, R, op='H'):
616
+ if len(L) == 0:
617
+ return _SP_ZERO
618
+
619
+ terms = []
620
+ for detL, cL in L:
621
+ for detR, cR in R:
622
+ elem = self.o2_det(detL, detR)
623
+ if elem is _SP_ZERO:
624
+ continue
625
+ prd = cL * cR
626
+ if prd == 1:
627
+ terms.append(elem)
628
+ elif prd == -1:
629
+ terms.append(-elem)
630
+ else:
631
+ terms.append(prd * elem)
632
+ if not terms:
633
+ return _SP_ZERO
634
+ if len(terms) == 1:
635
+ return terms[0]
636
+ return sp.Add(*terms)
637
+
638
+ def o2(self, L, R, op='H'):
639
+ L = FixedPsi(L)
640
+ L.standardize()
641
+ R = FixedPsi(R)
642
+ R.standardize()
643
+ self._check_orbitals_in_psi(L)
644
+ self._check_orbitals_in_psi(R)
645
+ return self.o2_fixed_psi(L, R)
646
+
647
+ def o2_matrix(self, u):
648
+ Nd = len(u)
649
+ o2 = sp.zeros(Nd)
650
+ if Nd == 0:
651
+ return o2
652
+ # Standardize each basis entry once into canonical creation order, so
653
+ # the two-electron block is built over exactly the same structures as
654
+ # the one-electron build_matrix (both apply the identical, deterministic
655
+ # standardization). A no-op on an already-canonical basis; on a
656
+ # hand-built non-canonical structure it folds the fermion reorder sign
657
+ # into the coefficient so H1 + H2 stays consistent.
658
+ cu = []
659
+ for entry in u:
660
+ fp = FixedPsi(entry)
661
+ fp.standardize()
662
+ self._check_orbitals_in_psi(fp)
663
+ cu.append(fp)
664
+ for i in range(Nd):
665
+ for j in range(i, Nd):
666
+ o2[i, j] = self.o2_fixed_psi(cu[i], cu[j])
667
+ if i != j:
668
+ o2[j, i] = o2[i, j]
669
+ return o2
670
+
671
+ def o2_mo2ao(self, c1, c2, c3, c4):
672
+ s = '0'
673
+ for i1, k1 in c1:
674
+ for i2, k2 in c2:
675
+ for i3, k3 in c3:
676
+ for i4, k4 in c4:
677
+ if i1.det_string.isupper() != i3.det_string.isupper():
678
+ continue
679
+ if i2.det_string.isupper() != i4.det_string.isupper():
680
+ continue
681
+ s += ' + ' + str(k1 * k2 * k3 * k4) + '*' + self.get_o2_name((i1.det_string.lower(),
682
+ i2.det_string.lower(),
683
+ i3.det_string.lower(),
684
+ i4.det_string.lower()))
685
+ return s
686
+
687
+ def get_mo_norm(self, mo):
688
+ # returns a diagonal matrix with the normalization factors on the diagonal
689
+ mo_norm = sp.zeros(len(mo))
690
+ for i in range(len(mo)):
691
+ mo_norm[i, i] = 1 / sp.sqrt(self.Ops(mo[i], mo[i], op='S'))
692
+ return mo_norm
693
+
694
+ def get_fock(self, mo, Nel):
695
+ # example of mo: [|a|+|b|, |A|+|B|, |a|-|b|, |A|-|B|]
696
+ # Construct the Fock matrix
697
+ Nmo = len(mo)
698
+ fock = sp.zeros(Nmo)
699
+ mo_norm = self.get_mo_norm(mo)
700
+ for i in range(Nmo):
701
+ for j in range(Nmo):
702
+ for b in range(Nel):
703
+ norm = mo_norm[i,i] * mo_norm[j,j] * mo_norm[b,b]**2
704
+
705
+ fock[i,j] += sp.simplify(self.o2_mo2ao(mo[i],mo[b],mo[j],mo[b])) * norm
706
+ fock[i,j] -= sp.simplify(self.o2_mo2ao(mo[i],mo[b],mo[b],mo[j])) * norm
707
+
708
+ fock[i,j] = sp.simplify(fock[i,j])
709
+
710
+ return fock
711
+
712
+ def get_rhf_fock(self, mo, Nel):
713
+ # example of mo: [|a|+|b|, |A|+|B|, |a|-|b|, |A|-|B|]
714
+ # Construct the Fock matrix
715
+ Nmo = len(mo)
716
+ fock = sp.zeros(Nmo)
717
+ mo_norm = self.get_mo_norm(mo)
718
+ for mu in range(Nmo):
719
+ for nu in range(Nmo):
720
+ for a in range(Nel // 2):
721
+ norm = mo_norm[mu, mu] * mo_norm[nu, nu] * mo_norm[a, a]**2
722
+
723
+ fock[mu, nu] += sp.simplify(self.o2_mo2ao(mo[mu], mo[a], mo[nu], mo[a])) * 2 * norm
724
+ fock[mu, nu] -= sp.simplify(self.o2_mo2ao(mo[mu], mo[a], mo[a], mo[nu])) * norm
725
+
726
+ fock[mu, nu] = sp.simplify(fock[mu, nu])
727
+
728
+ return fock
729
+
730
+ def get_rhf_mo_energies(self, mo_rhf, Nel):
731
+ mo_rhf_norm = self.get_mo_norm(mo_rhf)
732
+ rhf_o1 = simplify_matrix(mo_rhf_norm * self.build_matrix(mo_rhf, op='H') * mo_rhf_norm)
733
+ rhf_fock = self.get_rhf_fock(mo_rhf, Nel=Nel)
734
+ return (rhf_o1 + rhf_fock).diagonal()
735
+
736
+