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/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