dopyqo 0.0.1__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.
dopyqo/__init__.py ADDED
@@ -0,0 +1,28 @@
1
+ """
2
+ Dopyqo: Many-body analysis on top of Quantum ESPRESSO calculations
3
+ """
4
+
5
+ from dopyqo.info import __version__, HOMEPAGE
6
+ from dopyqo.helpers.vqe_helpers import *
7
+ from dopyqo.helpers.printing import *
8
+ from dopyqo.helpers.config import *
9
+ from dopyqo.helpers.matrix_elements import *
10
+ from dopyqo.helpers.atoms import elements_to_atomic_number
11
+ from dopyqo.helpers.tcc_helpers import *
12
+ from dopyqo.calc_matrix_elements import nuclear_repulsion_energy_ewald, iTj
13
+ from dopyqo.eri_pair_densities import (
14
+ eri,
15
+ get_frozen_core_energy_pp,
16
+ get_frozen_core_energy_given_pp,
17
+ get_frozen_core_pot_and_energy_given_pp,
18
+ get_frozen_core_pot,
19
+ )
20
+ from dopyqo.calc_pseudo_pot import calc_pps
21
+ from dopyqo.pseudopot import Pseudopot
22
+ from dopyqo.units import *
23
+ from dopyqo.wfc import Wfc
24
+ from dopyqo.hamiltonian import Hamiltonian
25
+ from dopyqo.wannier90 import read_u_mat
26
+ from dopyqo.transform_matrices import transform_one_body_matrix, transform_two_body_matrix, to_density_matrix
27
+ from dopyqo.wfc import runQE
28
+ from dopyqo.scripts.main import run
@@ -0,0 +1,365 @@
1
+ from warnings import warn
2
+ import itertools
3
+ import logging
4
+ from scipy.special import erfc
5
+ import numpy as np
6
+ from numba import jit
7
+ from dopyqo.colors import *
8
+
9
+
10
+ def nuclear_repulsion_energy_ewald(
11
+ atom_positions: np.ndarray,
12
+ atomic_numbers: np.ndarray,
13
+ lattice_vectors: np.ndarray,
14
+ lattice_vectors_reciprocal: np.ndarray,
15
+ cell_volume: float,
16
+ gcut: float,
17
+ sigma: float | None = None,
18
+ rust_impl: bool = True,
19
+ ) -> np.ndarray:
20
+ r"""Calculate the nuclear repulsion energy E between all nuclei in a periodic lattice, i.e.,
21
+ E = 1/2 \sum_I \sum_J \sum_T' Z_I Z_J / |R_I-R_J-T|
22
+ where ' denotes that the term T=0 and I=J is omitted. Z_I are the charges of the nuclei
23
+ and R_I their positions. T are all possible lattice translation vectors.
24
+ This is equivalent to
25
+ E = 1/2 \sum_I Z_I \Phi_I(R_I)
26
+ where \Phi_I(r) is the electrostatic potential generated by all periodic nuclei EXCEPT
27
+ the nucleus Z_I at position R_I (its periodic images are still included).
28
+ To calculate \Phi_I(r) we define charge densities and solve the Poisson equation in
29
+ real- and reciprocal-space. The nuclei Z_I are modelled by delta distribution at positions R_I+T.
30
+ Additionally, Ewalds method is used, i.e., we add and substract Gaussian charge distributions
31
+ at all R_I+T, and subtract a homogenous background charge simulating the electron-charges.
32
+ A short-ranged term including all delta distributions minus all Gaussian charges (except the
33
+ term where T=0 and I=J) can be calculated in real-space. A long-ranged term including
34
+ all Gaussian charges at R_I+T minus the constant background charge can be calculated
35
+ in reciprocal-space. The term describing the subtraction of the Gaussian charge at R_I
36
+ is calculated in real-space and is the so-called self-energy term. The G -> 0 limit
37
+ in the reciprocal space calculation is also calculated separately and is called the
38
+ charged-system term. The value \sigma determining the splitting between the real-space
39
+ and reciprocal-space term is calculated as done in Quantum ESPRESSO (see PW/src/ewald.f90:
40
+ https://github.com/QEF/q-e/blob/de3035747f5d8f2ec9a67869827341ebb43f5b12/PW/src/ewald.f90)
41
+
42
+ We calculate E = E_S + E_L + E_self + E_charged with
43
+
44
+ Short-ranged term calculated in real-space
45
+ E_S = 1/2 \sum_T' \sum_I \sum_J Z_I Z_J / |R_I - R_J - T| erfc(|R_I - R_J - T| \sqrt(\sigma))
46
+ where ' denotes that the term T=0 and I=J is omitted.
47
+
48
+ Long-ranged term calculated in reciprocal-space
49
+ E_L = 1/2 4\pi/V \sum_{k \neq 0} \sum_I \sum_J Z_I Z_J / |k|^2 e^{i k . (R_I - R_J)} e^{- 1/(4 \sigma) |k|^2}
50
+
51
+ Self-energy originated in subtracting the Gaussian charge at position R_I from the periodic charge density
52
+ generated by all atoms (in real-space)
53
+ E_self = - \sum_I (Z_I^2) \sqrt(\sigma) / \sqrt(\pi)
54
+
55
+ Charged-system term from the G=0 term in the reciprocal-space sum
56
+ E_charged = - \pi / (2 V \sigma) (\sum_I Z_I)^2
57
+
58
+ Notes:
59
+ - When pseudopotentials are used Z_I are the valence charges, i.e. the charge of the nucleus that is not
60
+ modelled with the pseudopotential.
61
+ - This function should calculate the same Ewald energy calculated in Quantum ESPRESSO, called
62
+ "ewald contribution" in the pw.x output file and "ewald" in the total_energy section in
63
+ the output-xml.
64
+ - Equivalent to Ewald calculation in PySCF of a cell in pyscf.pbc.get.cell.ewald function
65
+
66
+ References:
67
+ - http://metal.elte.hu/~groma/Anyagtudomany/kittel.pdf Appendix B
68
+ - https://journals.aps.org/prb/pdf/10.1103/PhysRevB.53.1814, equation (16)
69
+ - https://courses.physics.illinois.edu/phys466/sp2013/projects/2003/Team2/ewald_text.htm,
70
+ - S. W. de Leeuw et al. Proc. R. Soc. Lond. A 1980 373, 27-56, doi: 10.1098/rspa.1980.0135, equation (1.5)
71
+ - M. P. Allen and D. J. Tildesley "Computer Simulation of Solids", equation (5.20)
72
+ - http://micro.stanford.edu/mediawiki/images/4/46/Ewald_notes.pdf
73
+ - https://homepages.uc.edu/~becktl/tlb_ewald.pdf
74
+ - Quantum ESPRESSO source code in q-e/PW/src/ewald.f90
75
+
76
+ Args:
77
+ atom_positions (np.ndarray): Array of the coordinates R_I of every atom described
78
+ by the pseudopotential. Shape (#atoms, 3)
79
+ atomic_numbers (np.ndarray): 1D array of atomic number of each atom. Shape (#atoms,)
80
+ lattice_vectors (np.ndarray): 2D array of lattice vectors. Each row is one lattice vector. Shape (3, 3)
81
+ lattice_vectors_reciprocal (np.ndarray): 2D array of reciprocal lattice vectors. Each row is one reciprocal lattice vector. Shape (3, 3)
82
+ cell_volume (float): Cell volume of the computationen real space cell.
83
+ gcut (float): Cutoff of the reciprocal space sum in E_L, i.e., k < gcutrho. In Quantum ESPRESSO the value of the G-cutoff of the density is used.
84
+ sigma (float | None, optional): Value for \sigma used in the Ewald summation to split real- and reciprocal-space sums. If None this is calculated to satisfy
85
+ \sum_I 2 Z_I \sqrt{\frac\sigma\pi} \mathrm{erfc}\left(\sqrt{\frac{\left(4G_\mathrm{cut}\right)^2}{4\sigma}}\right) \leq 10^{-7}
86
+ Defaults to None.
87
+
88
+ Raises:
89
+ RuntimeError: If \sigma could not be calculated.
90
+
91
+ Returns:
92
+ float: Ewald energy
93
+ """
94
+
95
+ if rust_impl:
96
+ try:
97
+ import dopyqo_rs as calc_rs
98
+ except ImportError:
99
+ print(f"{ORANGE}Ewald warning: Could not import dopyqo_rs package. Falling back to python implementation.{RESET_COLOR}")
100
+ rust_impl = False # Set such that variable accurately represents if rust implementation is used
101
+ else: # No exception
102
+ logging.info("Using Rust implementation.")
103
+ return calc_rs.ewald(
104
+ atom_positions,
105
+ atomic_numbers,
106
+ lattice_vectors,
107
+ lattice_vectors_reciprocal,
108
+ cell_volume,
109
+ gcut,
110
+ )
111
+
112
+ if sigma is None:
113
+ sigma = 2.8
114
+ charge = np.sum(atomic_numbers)
115
+ gcutm = gcut**2
116
+ #
117
+ # choose sigma in order to have convergence in the sum over G
118
+ # upperbound is a safe upper bound for the error in the sum over G
119
+ #
120
+ while True:
121
+ if sigma <= 0.0:
122
+ raise RuntimeError("Optimal sigma for Ewald sum not found!")
123
+ upperbound = 2.0 * charge**2 * np.sqrt(sigma / np.pi) * erfc(np.sqrt(gcutm / 4.0 / sigma))
124
+ if upperbound > 1e-7:
125
+ sigma = sigma - 0.1
126
+ else:
127
+ break
128
+
129
+ logging.info("sigma %f", sigma)
130
+
131
+ e_short = 0.0 # real-space sum
132
+ converged_real = False
133
+ # n_max = 5
134
+ n_max_used = 0
135
+
136
+ alat = np.linalg.norm(lattice_vectors[0], ord=2)
137
+ t_vec_max_norm = 4.0 / np.sqrt(sigma) / alat
138
+ b_norms = np.linalg.norm(lattice_vectors_reciprocal, ord=2, axis=1)
139
+ n_max_x, n_max_y, n_max_z = (b_norms * t_vec_max_norm).astype(int) + 2
140
+
141
+ # Generate list or translation vectors ordered by their norm
142
+ # n_unordered = itertools.product(range(-n_max, n_max + 1), repeat=3)
143
+ logging.debug("Generating unordered translation vectors...")
144
+ t_vecs_unordered = [
145
+ [np.dot(lattice_vectors, np.array([nx, ny, nz])), [nx, ny, nz]]
146
+ for nx, ny, nz in itertools.product(
147
+ range(-n_max_x, n_max_x + 1),
148
+ range(-n_max_y, n_max_y + 1),
149
+ range(-n_max_z, n_max_z + 1),
150
+ )
151
+ # for nx, ny, nz in n_unordered
152
+ ]
153
+
154
+ t_vecs = sorted(t_vecs_unordered, key=lambda x: np.linalg.norm(x[0], ord=2))
155
+
156
+ for t_vec, (nx, ny, nz) in t_vecs:
157
+ for i, pos_i in enumerate(atom_positions): # \sum_I
158
+ z_i = atomic_numbers[i]
159
+ for j, pos_j in enumerate(atom_positions): # \sum_J
160
+ if np.all(t_vec == 0.0) and i == j:
161
+ continue
162
+ z_j = atomic_numbers[j]
163
+ r = np.linalg.norm(pos_i - pos_j - t_vec, ord=2)
164
+ e_short += z_i * z_j * erfc(r * np.sqrt(sigma)) / r
165
+
166
+ # with this choice terms up to ZiZj*erfc(4) are counted (erfc(4)=1.5e-8)
167
+ if r >= 4 / np.sqrt(sigma):
168
+ # print(f"Stopping real-sum at {nx}/{ny}/{nz} (sigma={sigma})")
169
+ converged_real = True
170
+ n_max_used = np.max([np.abs(nx), np.abs(ny), np.abs(nz), n_max_used])
171
+ # break
172
+ # if converged_real:
173
+ # break
174
+ if converged_real:
175
+ logging.info("Real-space sum converged! Largest n was %i", n_max_used)
176
+ else:
177
+ logging.warning("Real-space sum NOT converged!")
178
+
179
+ e_short *= 1 / 2
180
+
181
+ e_long = 0.0 # reciprocal-space sum
182
+
183
+ # Estimate size of Miller indices grid using the cutoff-energy and reciprocal lattice vectors
184
+ # A simple, but not accurate enough, estimate would be gcutrho/|b_i| for i \in {1, 2, 3}
185
+ # Here we take the shape of the reciprocal lattice into account:
186
+ # max[ gcutrho/(|b_i| + \sum_{j \neq i} b_i . b_j ) ] for i \in {1, 2, 3}
187
+ # TODO: To use the same grid QE uses we have to read the Miller indices used for the density,
188
+ # which is probably saved in charge-density.dat.
189
+ # Alternatively check how pymatgen chooses the reciprocal grid in its Ewald calculation method
190
+ b1 = lattice_vectors_reciprocal[0]
191
+ b2 = lattice_vectors_reciprocal[1]
192
+ b3 = lattice_vectors_reciprocal[2]
193
+ mill_max = max(
194
+ [
195
+ round(abs(gcut / (np.linalg.norm(b1) + np.dot(b1, b2) + np.dot(b1, b3)))),
196
+ round(abs(gcut / (np.linalg.norm(b2) + np.dot(b2, b1) + np.dot(b2, b3)))),
197
+ round(abs(gcut / (np.linalg.norm(b3) + np.dot(b3, b1) + np.dot(b3, b2)))),
198
+ ]
199
+ )
200
+ x, y, z = np.meshgrid(
201
+ np.arange(-mill_max, mill_max + 1),
202
+ np.arange(-mill_max, mill_max + 1),
203
+ np.arange(-mill_max, mill_max + 1),
204
+ indexing="ij",
205
+ )
206
+ mill_rho = np.stack((x.ravel(), y.ravel(), z.ravel()), axis=1)
207
+ k_vecs = np.einsum("ij, kj -> ki", lattice_vectors_reciprocal.T, mill_rho)
208
+ norms = np.linalg.norm(k_vecs, axis=1)
209
+ k_vecs = k_vecs[norms <= gcut]
210
+ for k_vec in k_vecs:
211
+ k2 = np.linalg.norm(k_vec, ord=2) ** 2
212
+ if np.isclose(k2, 0.0):
213
+ continue
214
+ structure_factor = np.sum(atomic_numbers * np.exp(1.0j * np.dot(atom_positions, k_vec)))
215
+ e_long += np.exp(-k2 / (4 * sigma)) * np.abs(structure_factor) ** 2 / k2
216
+
217
+ e_long *= 4.0 * np.pi / 2.0 / cell_volume
218
+
219
+ logging.debug("Reciprocal-space sum calculated!")
220
+
221
+ # Term arising from substracting the Gaussian charge distribution at R_I
222
+ # in real-space
223
+ e_self = -np.sum(atomic_numbers**2) * np.sqrt(sigma / np.pi)
224
+
225
+ # The analytic limit for G → 0 in the reciprocal sum
226
+ # The G=0 term in the reciprocal term is only partly cancelled by the uniform
227
+ # background charge. The remaining term is the following
228
+ # Also see Martin, 2020, "Electronic Structure" eq. (F.6) at the text afterwards
229
+ # which unfortunately misses a derivation
230
+ e_charged = -np.pi / cell_volume / sigma * np.sum(atomic_numbers) ** 2 / 2
231
+ # e_charged = -np.sum(atomic_numbers) ** 2 / sigma / 2 * np.pi / cell_volume
232
+
233
+ e_tot = e_short + e_long + e_self + e_charged
234
+
235
+ logging.info(
236
+ "e_short: %f, e_long: %f, e_self: %f, e_charged: %f, e_tot: %f",
237
+ e_short,
238
+ e_long,
239
+ e_self,
240
+ e_charged,
241
+ e_tot,
242
+ )
243
+
244
+ return e_tot
245
+
246
+
247
+ def check_symmetry_one_body_matrix(matrix: np.ndarray):
248
+ """Check if given matrix satisfies the symmetries of one-body matrices (hermitian)
249
+
250
+ Args:
251
+ matrix (np.ndarray): Matrix to check symmetries of
252
+
253
+ Returns:
254
+ bool: Bool specifying of symmetry is satisfied
255
+ """
256
+ matrix_hermitian = matrix.T.conj()
257
+ allclose_hermitian = np.allclose(matrix, matrix_hermitian)
258
+ return allclose_hermitian
259
+
260
+
261
+ def check_symmetry_two_body_matrix(matrix: np.ndarray):
262
+ """Check if given matrix satisfies the symmetries of ERIs
263
+
264
+ Args:
265
+ matrix (np.ndarray): Matrix to check symmetries of
266
+
267
+ Returns:
268
+ tuple[bool, bool, bool]: Bools specifying if following symmetries are satisfied: swap symmetry, hermitian symmetry, hermitian+swap symmetry
269
+ """
270
+ matrix_swap = matrix.swapaxes(0, 1).swapaxes(2, 3)
271
+ matrix_hermitian = matrix.T.conj()
272
+ matrix_hemitian_swap = matrix.T.conj().swapaxes(0, 1).swapaxes(2, 3)
273
+
274
+ allclose_swap = np.allclose(matrix, matrix_swap) # , rtol=1e-5, atol=1e-5)
275
+ allclose_hermitian = np.allclose(matrix, matrix_hermitian) # , rtol=1e-5, atol=1e-5)
276
+ allclose_hermitian_swap = np.allclose(matrix, matrix_hemitian_swap) # , rtol=1e-5, atol=1e-5)
277
+
278
+ allclose_ljki = None
279
+ allclose_ikjl = None
280
+ allclose_kilj = None
281
+ allclose_jlik = None
282
+ if np.allclose(matrix.real, matrix):
283
+ logging.info("Two-body matrix is real. Checking real-symmetries...")
284
+ allclose_hermitian = np.allclose(matrix, matrix.T) # ijkl = lkji
285
+ allclose_swap = np.allclose(matrix, matrix.transpose(1, 0, 3, 2)) # ijkl = jilk
286
+ allclose_ljki = np.allclose(matrix, matrix.transpose(3, 1, 2, 0)) # ijkl = ljki
287
+ allclose_ikjl = np.allclose(matrix, matrix.transpose(0, 2, 1, 3)) # ijkl = ikjl
288
+ allclose_hermitian_swap = np.allclose(matrix, matrix.T.transpose(1, 0, 3, 2)) # ijkl = klij
289
+ allclose_kilj = np.allclose(matrix, matrix.transpose(2, 0, 3, 1)) # ijkl = kilj
290
+ allclose_jlik = np.allclose(matrix, matrix.transpose(1, 3, 0, 2)) # ijkl = jlik
291
+
292
+ return (
293
+ allclose_swap,
294
+ allclose_hermitian,
295
+ allclose_hermitian_swap,
296
+ allclose_ljki,
297
+ allclose_ikjl,
298
+ allclose_kilj,
299
+ allclose_jlik,
300
+ )
301
+
302
+
303
+ def iTj(p: np.ndarray, c_ip: np.ndarray) -> np.ndarray: # Calculates <i|T|j>
304
+ """Calculate kinetic energy matrix in Hartree units in the Kohn-Sham basis
305
+
306
+ Args:
307
+ p (np.ndarray): Array of momentum vectors, shape (#waves, 3)
308
+ c_ip (np.ndarray): Array of coefficients describing the Kohn-Sham orbitals
309
+ in the plane wave basis, shape (#bands, #waves)
310
+
311
+ Returns:
312
+ np.ndarray: Kinetic energy matrix
313
+ """
314
+ # Kinetic energy matrix in Hartree units
315
+ p_norm = p[:, 0] ** 2 + p[:, 1] ** 2 + p[:, 2] ** 2 # p^2
316
+ # 1/2 <i|p^2|j>
317
+ return 0.5 * np.einsum("ip, p, jp -> ij", c_ip.conjugate(), p_norm, c_ip)
318
+
319
+
320
+ def iUj(
321
+ p: np.ndarray,
322
+ c_ip: np.ndarray,
323
+ atom_positions: np.ndarray,
324
+ atomic_numbers: np.ndarray,
325
+ cell_volume: float,
326
+ ) -> np.ndarray:
327
+ r"""Calculate nuclear interaction (U(r) = \sum_i Z_i / |R_I-r|) matrix in Hartree units in the Kohn-Sham basis
328
+
329
+ Args:
330
+ p (np.ndarray): Array of momentum vectors
331
+ c_ip (np.ndarray): Array of coefficients describing the Kohn-Sham orbitals in the plane wave basis
332
+ atom_positions (np.ndarray): 2D array of positions of each atom. Shape (#atoms, 3)
333
+ atomic_numbers (np.ndarray): 1D array of atomic number of each ato. Shape (#atoms,)m
334
+ cell_volume (float): Cell volume of the computationen real space cell.
335
+
336
+ Returns:
337
+ np.ndarray: Nuclear interaction matrix
338
+ """
339
+ # Nuclear interaction matrix in Hartree units
340
+ # Calculates <i|U|j>
341
+ # <p|U|q> = 4\pi / cell_volume \sum_I Z_I exp(-i (p-q) . R_I) 1/(q-p)²
342
+ # <i|U|j> = \sum_{p,q,p!=q} c_{p,i}^* c_{q,j} U_pq = 4\pi / cell_volume \sum_{p,q,p!=q} \sum_I Z_I c_{p,i}^* c_{q,j} exp(-i (p-q) . R_I) 1/(q-p)²
343
+ q = p
344
+ Z_I = atomic_numbers # shape (#atoms,)
345
+ R_I = atom_positions # shape (#atoms, 3)
346
+
347
+ q = p
348
+ p_minus_q = p[:, None] - q[None] # shape (#waves, #waves, 3)
349
+ # q[None] is of shape (1, #waves, 3) while p[:, None] is of shape (#waves, 1, 3)
350
+ # so p_minus_q[i,j] = p[i] - q[j]
351
+
352
+ p_minus_q = q[:, None] - p[None] # shape (#waves, #waves, 3)
353
+ p_minus_q_norm = np.linalg.norm(p_minus_q, ord=2, axis=2) # Has zeros on diagonal, shape (#waves, #waves)
354
+ p_minus_q_norm_filtered = p_minus_q_norm.copy() + np.eye(p_minus_q_norm.shape[0]) # Replace zeros on diagonal with ones
355
+
356
+ p_minus_q_dot_R = np.sum(p_minus_q[None] * R_I[:, None, None], axis=3) # sum over 3D-coordinates, shape (#atoms, #waves, #waves)
357
+ # \sum_I 4\pi/V e^{-i (G-G').R_I}
358
+ prefactor = 4 * np.pi / cell_volume * np.sum(Z_I[:, None, None] * np.exp(-1j * p_minus_q_dot_R), axis=0) # sum over atoms, shape (#waves, #waves)
359
+ one_over_p_minus_q_norm_squared = 1 / p_minus_q_norm_filtered**2
360
+ one_over_p_minus_q_norm_squared = one_over_p_minus_q_norm_squared.copy() - np.eye(one_over_p_minus_q_norm_squared.shape[0])
361
+
362
+ u_mat = prefactor * one_over_p_minus_q_norm_squared
363
+ assert (u_mat.diagonal() == 0.0).all()
364
+
365
+ return c_ip.conj() @ u_mat @ c_ip.T