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/numerical.py
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import numpy
|
|
2
|
+
import scipy.linalg
|
|
3
|
+
import sympy
|
|
4
|
+
from symvb import FixedPsi
|
|
5
|
+
|
|
6
|
+
# manual control of the parallelization feature
|
|
7
|
+
PARALLEL = False
|
|
8
|
+
|
|
9
|
+
RAY_IMPORTED = False
|
|
10
|
+
if PARALLEL:
|
|
11
|
+
# attempt to import ray
|
|
12
|
+
try:
|
|
13
|
+
import ray
|
|
14
|
+
RAY_IMPORTED = True
|
|
15
|
+
my_decor = ray.remote
|
|
16
|
+
except ImportError as e:
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
if not (PARALLEL and RAY_IMPORTED):
|
|
20
|
+
# No parallelization; use a dummy decorator
|
|
21
|
+
def my_decor(func):
|
|
22
|
+
return func
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def repair_connections(coupled, z):
|
|
26
|
+
# Repair the coupled hash
|
|
27
|
+
# Problem: some elements of z are 0 because of the numerical fluctuations
|
|
28
|
+
# Solution: recover these elements using identities:
|
|
29
|
+
# For a < b < c:
|
|
30
|
+
# z[a,b] = v[b] / v[a]
|
|
31
|
+
# z[a,c] = v[c] / v[a]
|
|
32
|
+
# z[b,c] = v[c] / v[b]
|
|
33
|
+
# Thus,
|
|
34
|
+
# z[a,b] = z[a,c] / z[b,c]
|
|
35
|
+
#
|
|
36
|
+
# E.g., z[0,20] = 0 but z[0,19]=1 and z[19,20]=3.49
|
|
37
|
+
# z[0,20] can be recovered as = z[0,19] / z[20,19]
|
|
38
|
+
repaired = {}
|
|
39
|
+
for k in coupled.keys():
|
|
40
|
+
repaired[k] = {}
|
|
41
|
+
for m in coupled[k].keys():
|
|
42
|
+
repaired[k][m] = coupled[k][m]
|
|
43
|
+
if coupled[k][m] == 0.:
|
|
44
|
+
# find a non-zero valued index to use for repair, z[k][m] = z[k][nz] / z[m][nz] if m < nz
|
|
45
|
+
# if m > nz, z[k][m] = z[k][nz] * z[nz][m]
|
|
46
|
+
fixed = False
|
|
47
|
+
for nz in coupled[k].keys():
|
|
48
|
+
if z[k, nz] != 0.0:
|
|
49
|
+
if m < nz and z[m, nz] != 0.:
|
|
50
|
+
repaired[k][m] = z[k, nz] / z[m, nz]
|
|
51
|
+
fixed = True
|
|
52
|
+
elif m > nz and z[nz, m] != 0.:
|
|
53
|
+
repaired[k][m] = z[k, nz] * z[nz, m]
|
|
54
|
+
fixed = True
|
|
55
|
+
if fixed:
|
|
56
|
+
break
|
|
57
|
+
return repaired
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@my_decor
|
|
61
|
+
def single_trial(mH, mS, nums, precision=12):
|
|
62
|
+
N = mH.shape[0]
|
|
63
|
+
# get the numeric eigenvectors
|
|
64
|
+
H = numpy.array(mH.subs(nums)).astype(numpy.float64)
|
|
65
|
+
S = numpy.array(mS.subs(nums)).astype(numpy.float64)
|
|
66
|
+
vals, vecs = scipy.linalg.eig(H, S)
|
|
67
|
+
v = vecs[:, vals.argmin(0)]
|
|
68
|
+
|
|
69
|
+
# compute the coefficient ratios
|
|
70
|
+
fixed_coefs = numpy.zeros((N, N))
|
|
71
|
+
for i in range(N):
|
|
72
|
+
for j in range(i + 1, N):
|
|
73
|
+
if v[i] == 0.0:
|
|
74
|
+
fixed_coefs[i, j] = numpy.inf # numpy.Inf alias removed in NumPy 2.0
|
|
75
|
+
else:
|
|
76
|
+
fixed_coefs[i, j] = numpy.around(v[j] / v[i], precision)
|
|
77
|
+
return fixed_coefs
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def get_coupled(mS, mH, N_tries=10, precision=12, ranges={'h': (-1.0, 0.0), 's': (0.0, 1.0)}):
|
|
81
|
+
"""
|
|
82
|
+
Get the dictionary showing the components of the lowest energy wave vector
|
|
83
|
+
that have constant ratio independent of the numerical values h, s
|
|
84
|
+
"""
|
|
85
|
+
N = mH.shape[0]
|
|
86
|
+
|
|
87
|
+
nums = [None,]*N_tries
|
|
88
|
+
for i in range(N_tries):
|
|
89
|
+
nums[i] = {}
|
|
90
|
+
for k, v in ranges.items():
|
|
91
|
+
nums[i][k] = numpy.random.uniform(low=v[0], high=v[1])
|
|
92
|
+
|
|
93
|
+
fcs = [None, ] * N_tries
|
|
94
|
+
|
|
95
|
+
if RAY_IMPORTED:
|
|
96
|
+
# Parallel
|
|
97
|
+
ray.init()
|
|
98
|
+
ids = [None, ] * N_tries
|
|
99
|
+
for trial in range(N_tries):
|
|
100
|
+
ids[trial] = single_trial.remote(mH, mS, nums[trial], precision=precision)
|
|
101
|
+
# Block until the tasks are done and get the results.
|
|
102
|
+
fcs = ray.get(ids)
|
|
103
|
+
ray.shutdown()
|
|
104
|
+
else:
|
|
105
|
+
# Single core
|
|
106
|
+
for trial in range(N_tries):
|
|
107
|
+
fcs[trial] = single_trial(mH, mS, nums[trial], precision=precision)
|
|
108
|
+
|
|
109
|
+
# check if the coefficient ratios differ from the first value
|
|
110
|
+
b = numpy.ones((N, N))
|
|
111
|
+
for trial in range(1, N_tries):
|
|
112
|
+
b *= (fcs[0] == fcs[trial])
|
|
113
|
+
z = numpy.triu(b * fcs[0], k=1)
|
|
114
|
+
|
|
115
|
+
# Convert the matrix in the dictionary form
|
|
116
|
+
coupled = {}
|
|
117
|
+
for i in range(z.shape[0]):
|
|
118
|
+
# i shows the row
|
|
119
|
+
for j in range(i + 1, z.shape[0]):
|
|
120
|
+
if z[i, j] == 0:
|
|
121
|
+
continue
|
|
122
|
+
# j is the index for a nonzero coupled i,j
|
|
123
|
+
found = False
|
|
124
|
+
for k, v in coupled.items():
|
|
125
|
+
# k is the reference FixedPsi, it has weight 1.0 by definition
|
|
126
|
+
# v is a dict of the combined FixedPsi index/coef pairs
|
|
127
|
+
if i in v:
|
|
128
|
+
found = True
|
|
129
|
+
if j not in v:
|
|
130
|
+
coupled[k][j] = z[k, j]
|
|
131
|
+
break
|
|
132
|
+
if not found:
|
|
133
|
+
coupled[i] = {i: 1.0, j: z[i, j]}
|
|
134
|
+
|
|
135
|
+
import pickle
|
|
136
|
+
# file = open('z.pickle', 'wb')
|
|
137
|
+
# pickle.dump(z, file)
|
|
138
|
+
# file.close()
|
|
139
|
+
# file = open('coupled.pickle', 'wb')
|
|
140
|
+
# pickle.dump(coupled, file)
|
|
141
|
+
# file.close()
|
|
142
|
+
|
|
143
|
+
return repair_connections(coupled, z)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def get_combined(P, indices, coefs=None):
|
|
147
|
+
"""
|
|
148
|
+
Contract an array of FixedPsi objects
|
|
149
|
+
:param indices: indices of the FixedPsi objects to be combined together
|
|
150
|
+
:param coefs: coefficients of the FixedPsi objectsA contraction scheme. Assumed to be 1.0 if omitted
|
|
151
|
+
:return: A new array of FixedPsi objects with combined determinants
|
|
152
|
+
"""
|
|
153
|
+
if coefs is None:
|
|
154
|
+
coefs = numpy.ones_like(indices)
|
|
155
|
+
|
|
156
|
+
P_new = []
|
|
157
|
+
P_contracted = FixedPsi()
|
|
158
|
+
|
|
159
|
+
# create a combined FixedPsi
|
|
160
|
+
for i in range(len(indices)):
|
|
161
|
+
P_contracted.add_fixedpsi(P[indices[i]], coefs[i])
|
|
162
|
+
P_new.append(P_contracted)
|
|
163
|
+
|
|
164
|
+
# copy the remaining FixedPsi
|
|
165
|
+
for i in range(len(P)):
|
|
166
|
+
if i not in indices:
|
|
167
|
+
P_new.append(P[i])
|
|
168
|
+
|
|
169
|
+
return P_new
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def get_combined_from_dict(P, d):
|
|
173
|
+
"""
|
|
174
|
+
Contract a list of FixedPsi objects
|
|
175
|
+
:param P: original list of FixedPsi
|
|
176
|
+
:param d: dictionary of dictionaries; each sub-dict has FixedPsi indices/coefs as key/value pairs
|
|
177
|
+
:return: A new list of FixedPsi objects with combined determinants
|
|
178
|
+
"""
|
|
179
|
+
|
|
180
|
+
P_new = []
|
|
181
|
+
c_indices = []
|
|
182
|
+
|
|
183
|
+
# create the combined FixedPsi
|
|
184
|
+
for c in d.values():
|
|
185
|
+
P_contracted = FixedPsi()
|
|
186
|
+
for index, coef in c.items():
|
|
187
|
+
P_contracted.add_fixedpsi(P[index], coef)
|
|
188
|
+
c_indices.append(index)
|
|
189
|
+
P_new.append(P_contracted)
|
|
190
|
+
|
|
191
|
+
# copy the remaining FixedPsi
|
|
192
|
+
for i in range(len(P)):
|
|
193
|
+
if i not in c_indices:
|
|
194
|
+
P_new.append(P[i])
|
|
195
|
+
|
|
196
|
+
return P_new
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def validate_solution(expression, mH, mS, N_tries=10, precision=12):
|
|
200
|
+
"""
|
|
201
|
+
Validate that formula for the lowest energy matches the numerical values
|
|
202
|
+
:param expression: Sympy expression for the formula
|
|
203
|
+
:param mH: symbolic Hamiltonian matrix
|
|
204
|
+
:param mS: symbolic overlap matrix
|
|
205
|
+
:param N_tries: number of trials
|
|
206
|
+
:param precision: 10^-precision is the matching threshold
|
|
207
|
+
:return: Boolean value. True if the numerical and analytical solutions produce the same result
|
|
208
|
+
"""
|
|
209
|
+
hv = numpy.random.uniform(low=-1.0, high=0.0, size=N_tries)
|
|
210
|
+
sv = numpy.random.uniform(low=0.0, high=1.0, size=N_tries)
|
|
211
|
+
|
|
212
|
+
for trial in range(N_tries):
|
|
213
|
+
# Numerical result
|
|
214
|
+
H = numpy.array(mH.subs({'h': hv[trial], 's': sv[trial]})).astype(numpy.float64)
|
|
215
|
+
S = numpy.array(mS.subs({'s': sv[trial]})).astype(numpy.float64)
|
|
216
|
+
vals, vecs = scipy.linalg.eig(H, S)
|
|
217
|
+
num_solution = min(vals)
|
|
218
|
+
|
|
219
|
+
# Result from the formula
|
|
220
|
+
formula_solution = expression.evalf(subs={'h': hv[trial], 's': sv[trial]})
|
|
221
|
+
|
|
222
|
+
if abs(num_solution - formula_solution) >= 10.0 ** (-precision):
|
|
223
|
+
return False
|
|
224
|
+
return True
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def decompose_polynomial_matrix(M, var, factor=None):
|
|
228
|
+
"""
|
|
229
|
+
Express a sympy Matrix M as
|
|
230
|
+
|
|
231
|
+
M == factor * sum_q (var ** q) * A_q
|
|
232
|
+
|
|
233
|
+
where A_q are float ndarrays. If factor is None, just decomposes in
|
|
234
|
+
var. Deduplicates by cell expression, so matrices with few distinct
|
|
235
|
+
entries (typical of symvb symbolic Hamiltonians: ~30 unique entries
|
|
236
|
+
in a 400x400 benzene case) decompose in seconds.
|
|
237
|
+
"""
|
|
238
|
+
M_lst = M.tolist()
|
|
239
|
+
n_rows, n_cols = M.shape
|
|
240
|
+
seen, decomp_for = {}, {}
|
|
241
|
+
for i in range(n_rows):
|
|
242
|
+
for j in range(n_cols):
|
|
243
|
+
e = M_lst[i][j]
|
|
244
|
+
if e == 0 or e in seen:
|
|
245
|
+
continue
|
|
246
|
+
seen[e] = True
|
|
247
|
+
P = e.subs(factor, 1) if factor is not None else e
|
|
248
|
+
P = sympy.expand(P)
|
|
249
|
+
terms = []
|
|
250
|
+
while P != 0:
|
|
251
|
+
d = int(sympy.degree(P, var))
|
|
252
|
+
c = P.coeff(var, d)
|
|
253
|
+
terms.append((d, float(c)))
|
|
254
|
+
P = sympy.expand(P - c * var ** d) if d else 0
|
|
255
|
+
decomp_for[e] = terms
|
|
256
|
+
|
|
257
|
+
maxdeg = max((max(d for d, _ in t) for t in decomp_for.values() if t),
|
|
258
|
+
default=0)
|
|
259
|
+
A = [numpy.zeros((n_rows, n_cols)) for _ in range(maxdeg + 1)]
|
|
260
|
+
for i in range(n_rows):
|
|
261
|
+
for j in range(n_cols):
|
|
262
|
+
e = M_lst[i][j]
|
|
263
|
+
if e == 0:
|
|
264
|
+
continue
|
|
265
|
+
for d, c in decomp_for[e]:
|
|
266
|
+
A[d][i, j] = c
|
|
267
|
+
return A
|