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 +28 -0
- dopyqo/calc_matrix_elements.py +365 -0
- dopyqo/calc_pseudo_pot.py +936 -0
- dopyqo/colors.py +24 -0
- dopyqo/cube.py +207 -0
- dopyqo/eri_pair_densities.py +795 -0
- dopyqo/fci_vector_matrix.py +162 -0
- dopyqo/hamiltonian.py +1653 -0
- dopyqo/helpers/atoms.py +238 -0
- dopyqo/helpers/config.py +660 -0
- dopyqo/helpers/matrix_elements.py +41 -0
- dopyqo/helpers/printing.py +97 -0
- dopyqo/helpers/tcc_helpers.py +290 -0
- dopyqo/helpers/vqe_helpers.py +26 -0
- dopyqo/info.py +2 -0
- dopyqo/plotting.py +259 -0
- dopyqo/pseudopot.py +142 -0
- dopyqo/scripts/banners.py +108 -0
- dopyqo/scripts/main.py +1228 -0
- dopyqo/transform_matrices.py +48 -0
- dopyqo/units.py +17 -0
- dopyqo/wannier90.py +177 -0
- dopyqo/wfc.py +1377 -0
- dopyqo/wfc_spin.py +289 -0
- dopyqo/xsf.py +103 -0
- dopyqo-0.0.1.dist-info/METADATA +494 -0
- dopyqo-0.0.1.dist-info/RECORD +31 -0
- dopyqo-0.0.1.dist-info/WHEEL +5 -0
- dopyqo-0.0.1.dist-info/entry_points.txt +2 -0
- dopyqo-0.0.1.dist-info/licenses/LICENSE.txt +325 -0
- dopyqo-0.0.1.dist-info/top_level.txt +1 -0
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
|