Coverage for eminus/dft.py: 94.81%
154 statements
« prev ^ index » next coverage.py v7.6.4, created at 2024-11-08 12:59 +0000
« prev ^ index » next coverage.py v7.6.4, created at 2024-11-08 12:59 +0000
1# SPDX-FileCopyrightText: 2021 The eminus developers
2# SPDX-License-Identifier: Apache-2.0
3"""Main DFT functions based on the DFT++ formulation."""
5import numpy as np
6from numpy.linalg import multi_dot
7from numpy.random import Generator, SFC64
8from scipy.linalg import eig, eigh, eigvalsh, inv, sqrtm
10from .gga import calc_Vtau, get_grad_field, get_tau, gradient_correction
11from .gth import calc_Vnonloc
12from .logger import log
13from .utils import handle_k, handle_spin, pseudo_uniform
14from .xc import get_vxc
17def get_phi(atoms, n):
18 """Solve the Poisson equation.
20 Reference: Comput. Phys. Commun. 128, 1.
22 Args:
23 atoms: Atoms object.
24 n: Real-space electronic density.
26 Returns:
27 Hartree field.
28 """
29 # phi = -4 pi Linv(O(J(n)))
30 return -4 * np.pi * atoms.Linv(atoms.O(atoms.J(n)))
33@handle_k
34@handle_spin
35def orth(atoms, W):
36 """Orthogonalize coefficient matrix W.
38 Reference: Comput. Phys. Commun. 128, 1.
40 Args:
41 atoms: Atoms object.
42 W: Expansion coefficients of unconstrained wave functions in reciprocal space.
44 Returns:
45 Orthogonalized wave functions.
46 """
47 # Y = W (Wdag O(W))^-0.5
48 return W @ inv(sqrtm(W.conj().T @ atoms.O(W)))
51def orth_unocc(atoms, Y, Z):
52 """Orthogonalize unoccupied matrix Z while maintaining orthogonality to Y.
54 Reference: Comput. Phys. Commun. 128, 1.
56 Args:
57 atoms: Atoms object.
58 Y: Expansion coefficients of orthogonal wave functions in reciprocal space.
59 Z: Expansion coefficients of unconstrained wave functions in reciprocal space.
61 Returns:
62 Orthogonalized wave functions.
63 """
64 D = [np.empty_like(Zk) for Zk in Z]
65 for ik in range(atoms.kpts.Nk):
66 for spin in range(atoms.occ.Nspin):
67 # rhoZ = (I - Y Ydag O) Z
68 Yocc = Y[ik][spin][:, atoms.occ.f[ik][spin] > 0]
69 rhoZ = Z[ik][spin] - Yocc @ Yocc.conj().T @ atoms.O(Z[ik][spin])
70 # D = rhoZ (rhoZdag O(rhoZ))^-0.5
71 D[ik][spin] = rhoZ @ inv(sqrtm(rhoZ.conj().T @ atoms.O(rhoZ)))
72 return D
75def get_n_total(atoms, Y, n_spin=None):
76 """Calculate the total electronic density.
78 Reference: Comput. Phys. Commun. 128, 1.
80 Args:
81 atoms: Atoms object.
82 Y: Expansion coefficients of orthogonal wave functions in reciprocal space.
84 Keyword Args:
85 n_spin: Real-space electronic densities per spin channel.
87 Returns:
88 Electronic density.
89 """
90 # Return the total density in the spin-paired case
91 if n_spin is not None:
92 return np.sum(n_spin, axis=0)
94 # n = (IW) F (IW)dag
95 n = np.zeros(atoms.Ns)
96 Yrs = atoms.I(Y)
97 for ik in range(atoms.kpts.Nk):
98 for spin in range(atoms.occ.Nspin):
99 n += np.sum(
100 atoms.occ.f[ik, spin]
101 * atoms.kpts.wk[ik]
102 * np.real(Yrs[ik][spin].conj() * Yrs[ik][spin]),
103 axis=1,
104 )
105 return n
108@handle_k(mode="reduce")
109def get_n_spin(atoms, Y, ik):
110 """Calculate the electronic density per spin channel.
112 Reference: Comput. Phys. Commun. 128, 1.
114 Args:
115 atoms: Atoms object.
116 Y: Expansion coefficients of orthogonal wave functions in reciprocal space.
117 ik: k-point index.
119 Returns:
120 Electronic densities per spin channel.
121 """
122 Yrs = atoms.I(Y, ik)
123 n = np.empty((atoms.occ.Nspin, atoms.Ns))
124 for spin in range(atoms.occ.Nspin):
125 n[spin] = np.sum(
126 atoms.occ.f[ik, spin] * atoms.kpts.wk[ik] * np.real(Yrs[spin].conj() * Yrs[spin]),
127 axis=1,
128 )
129 return n
132@handle_k(mode="reduce")
133def get_n_single(atoms, Y, ik):
134 """Calculate the single-electron densities.
136 Args:
137 atoms: Atoms object.
138 Y: Expansion coefficients of orthogonal wave functions in reciprocal space.
139 ik: k-point index.
141 Returns:
142 Single-electron densities.
143 """
144 Yrs = atoms.I(Y, ik)
145 n = np.empty((atoms.occ.Nspin, atoms.Ns, atoms.occ.Nstate))
146 for spin in range(atoms.occ.Nspin):
147 n[spin] = atoms.occ.f[ik, spin] * atoms.kpts.wk[ik] * np.real(Yrs[spin].conj() * Yrs[spin])
148 return n
151def get_grad(scf, ik, spin, W, **kwargs):
152 """Calculate the energy gradient with respect to W.
154 Reference: Comput. Phys. Commun. 128, 1.
156 Args:
157 scf: SCF object.
158 ik: k-point index.
159 spin: Spin variable to track whether to do the calculation for spin up or down.
160 W: Expansion coefficients of unconstrained wave functions in reciprocal space.
162 Keyword Args:
163 **kwargs: See :func:`H`.
165 Returns:
166 Gradient.
167 """
168 atoms = scf.atoms
169 F = atoms.occ.F[ik][spin]
170 HW = H(scf, ik, spin, W, **kwargs)
171 WHW = W[ik][spin].conj().T @ HW
172 # U = Wdag O(W)
173 OW = atoms.O(W[ik][spin])
174 U = W[ik][spin].conj().T @ OW
175 invU = inv(U)
176 U12 = sqrtm(invU)
177 # Htilde = U^-0.5 Wdag H(W) U^-0.5
178 Ht = multi_dot([U12, WHW, U12])
179 # grad E = H(W) - O(W) U^-1 (Wdag H(W)) (U^-0.5 F U^-0.5) + O(W) (U^-0.5 Q(Htilde F - F Htilde))
180 tmp = multi_dot([OW, invU, WHW])
181 return atoms.kpts.wk[ik] * (
182 multi_dot([HW - tmp, U12, F, U12]) + multi_dot([OW, U12, Q(Ht @ F - F @ Ht, U)])
183 )
186def H(scf, ik, spin, W, dn_spin=None, phi=None, vxc=None, vsigma=None, vtau=None):
187 """Left-hand side of the eigenvalue equation.
189 Reference: Comput. Phys. Commun. 128, 1.
191 Args:
192 scf: SCF object.
193 ik: k-point index.
194 spin: Spin variable to track whether to do the calculation for spin up or down.
195 W: Expansion coefficients of unconstrained wave functions in reciprocal space.
197 Keyword Args:
198 dn_spin: Real-space gradient of densities per spin channel.
199 phi: Hartree field.
200 vxc: Exchange-correlation potential.
201 vsigma: Contracted gradient potential derivative.
202 vtau: Kinetic energy gradient potential derivative.
204 Returns:
205 Hamiltonian applied on W.
206 """
207 atoms = scf.atoms
209 # If dn_spin is None all other keyword arguments are None by design
210 # In that case precompute values from the SCF class
211 if phi is None:
212 dn_spin, phi, vxc, vsigma, vtau = H_precompute(scf, W)
214 # This calculates the XC potential in the reciprocal space
215 Gvxc = atoms.J(vxc[spin])
216 # Calculate the gradient correction to the potential if a (meta-)GGA functional is used
217 if "gga" in scf.xc_type:
218 Gvxc = Gvxc - gradient_correction(atoms, spin, dn_spin, vsigma)
219 # Vkin = -0.5 L(W)
220 Vkin_psi = -0.5 * atoms.L(W[ik][spin], ik)
221 # Veff = Jdag(Vion) + Jdag(O(J(vxc))) + Jdag(O(phi))
222 # We get the full potential in the functional definition (different to the DFT++ notation)
223 # Normally Vxc = Jdag(O(J(exc))) + diag(exc') Jdag(O(J(n))) (for LDA functionals)
224 Veff = scf.Vloc + atoms.Jdag(atoms.O(Gvxc + phi))
225 Vnonloc_psi = calc_Vnonloc(scf, ik, spin, W)
226 Vtau_psi = calc_Vtau(scf, ik, spin, W, vtau)
227 # H = Vkin + Idag(diag(Veff))I + Vnonloc (+ Vtau)
228 # Diag(a) * B can be written as a * B if a is a column vector
229 return (
230 Vkin_psi + atoms.Idag(Veff[:, None] * atoms.I(W[ik][spin], ik), ik) + Vnonloc_psi + Vtau_psi
231 )
234def H_precompute(scf, W):
235 """Create precomputed values as intermediate results.
237 Args:
238 scf: SCF object.
239 W: Expansion coefficients of unconstrained wave functions in reciprocal space.
241 Returns:
242 dn_spin, phi, vxc, vsigma, and vtau.
243 """
244 # We are calculating everything here over both spin channels
245 # We would be fine/could improve performance by only calculating the currently used spin channel
246 atoms = scf.atoms
247 Y = orth(atoms, W)
248 n_spin = get_n_spin(atoms, Y)
249 n = get_n_total(atoms, Y, n_spin)
250 if "gga" in scf.xc_type:
251 dn_spin = get_grad_field(atoms, n_spin)
252 else:
253 dn_spin = None
254 if scf.xc_type == "meta-gga":
255 tau = get_tau(atoms, Y)
256 else:
257 tau = None
258 phi = get_phi(atoms, n)
259 vxc, vsigma, vtau = get_vxc(scf.xc, n_spin, atoms.occ.Nspin, dn_spin, tau, scf.xc_params)
260 return dn_spin, phi, vxc, vsigma, vtau
263def Q(inp, U):
264 """Operator needed to calculate gradients with non-constant occupations.
266 Reference: Comput. Phys. Commun. 128, 1.
268 Args:
269 inp: Coefficients input array.
270 U: Overlap of wave functions.
272 Returns:
273 Q operator result.
274 """
275 mu, V = eig(U)
276 mu = mu[:, None]
277 denom = np.sqrt(mu) @ np.ones((1, len(mu)))
278 denom2 = denom + denom.conj().T
279 # return V @ ((V.conj().T @ inp @ V) / denom2) @ V.conj().T
280 tmp = multi_dot([V.conj().T, inp, V])
281 return multi_dot([V, tmp / denom2, V.conj().T])
284def get_psi(scf, W, **kwargs):
285 """Calculate eigenstates from H.
287 Reference: Comput. Phys. Commun. 128, 1.
289 Args:
290 scf: SCF object.
291 W: Expansion coefficients of unconstrained wave functions in reciprocal space.
293 Keyword Args:
294 **kwargs: See :func:`H`.
296 Returns:
297 Eigenstates in reciprocal space.
298 """
299 if W is None:
300 log.warning('The provided wave function is "None".')
301 return None
303 atoms = scf.atoms
304 Y = orth(atoms, W)
305 psi = [np.empty_like(Yk) for Yk in Y]
306 for ik in range(atoms.kpts.Nk):
307 for spin in range(atoms.occ.Nspin):
308 mu = Y[ik][spin].conj().T @ H(scf, ik, spin, Y, **kwargs)
309 _, D = eigh(mu)
310 psi[ik][spin] = Y[ik][spin] @ D
311 return psi
314def get_epsilon(scf, W, **kwargs):
315 """Calculate eigenvalues from H.
317 Reference: Comput. Phys. Commun. 128, 1.
319 Args:
320 scf: SCF object.
321 W: Expansion coefficients of unconstrained wave functions in reciprocal space.
323 Keyword Args:
324 **kwargs: See :func:`H`.
326 Returns:
327 Eigenvalues.
328 """
329 if W is None:
330 log.warning('The provided wave function is "None".')
331 return None
333 atoms = scf.atoms
334 Y = orth(atoms, W)
335 epsilon = np.empty((atoms.kpts.Nk, atoms.occ.Nspin, Y[0].shape[-1]))
336 for ik in range(atoms.kpts.Nk):
337 for spin in range(atoms.occ.Nspin):
338 mu = Y[ik][spin].conj().T @ H(scf, ik, spin, Y, **kwargs)
339 epsilon[ik][spin] = np.sort(eigvalsh(mu))
340 return epsilon
343def get_epsilon_unocc(scf, W, Z, **kwargs):
344 """Calculate eigenvalues from H of unoccupied states.
346 Reference: Comput. Phys. Commun. 128, 1.
348 Args:
349 scf: SCF object.
350 W: Expansion coefficients of unconstrained wave functions in reciprocal space.
351 Z: Expansion coefficients of unconstrained wave functions in reciprocal space.
353 Keyword Args:
354 **kwargs: See :func:`H`.
356 Returns:
357 Eigenvalues.
358 """
359 if W is None or Z is None:
360 log.warning('The provided wave function is "None".')
361 return None
363 atoms = scf.atoms
364 Y = orth(atoms, W)
365 D = orth_unocc(atoms, Y, Z)
366 epsilon = np.empty((atoms.kpts.Nk, atoms.occ.Nspin, D[0].shape[-1]))
367 for ik in range(atoms.kpts.Nk):
368 for spin in range(atoms.occ.Nspin):
369 mu = D[ik][spin].conj().T @ H(scf, ik, spin, D, **kwargs)
370 epsilon[ik][spin] = np.sort(eigvalsh(mu))
371 return epsilon
374def guess_random(scf, Nstate=None, seed=42, symmetric=False):
375 """Generate random initial-guess coefficients as starting values.
377 Args:
378 scf: SCF object.
380 Keyword Args:
381 Nstate: Number of states.
382 seed: Seed to initialize the random number generator.
383 symmetric: Whether to use the same guess for both spin channels.
385 Returns:
386 Initial-guess orthogonal wave functions in reciprocal space.
387 """
388 atoms = scf.atoms
389 if Nstate is None:
390 Nstate = atoms.occ.Nstate
392 rng = Generator(SFC64(seed))
393 W = []
394 for ik in range(atoms.kpts.Nk):
395 if symmetric:
396 W_ik = rng.standard_normal((len(atoms.Gk2c[ik]), Nstate)) + 1j * rng.standard_normal(
397 (len(atoms.Gk2c[ik]), Nstate)
398 )
399 W.append(np.array([W_ik] * atoms.occ.Nspin))
400 else:
401 W_ik = rng.standard_normal(
402 (atoms.occ.Nspin, len(atoms.Gk2c[ik]), Nstate)
403 ) + 1j * rng.standard_normal((atoms.occ.Nspin, len(atoms.Gk2c[ik]), Nstate))
404 W.append(W_ik)
405 return orth(atoms, W)
408def guess_pseudo(scf, Nstate=None, seed=1234, symmetric=False):
409 """Generate initial-guess coefficients using pseudo-random starting values.
411 Args:
412 scf: SCF object.
414 Keyword Args:
415 Nstate: Number of states.
416 seed: Seed to initialize the random number generator.
417 symmetric: Whether to use the same guess for both spin channels.
419 Returns:
420 Initial-guess orthogonal wave functions in reciprocal space.
421 """
422 atoms = scf.atoms
423 if Nstate is None:
424 Nstate = atoms.occ.Nstate
426 W = []
427 for ik in range(atoms.kpts.Nk):
428 if symmetric:
429 W_ik = pseudo_uniform((1, len(atoms.Gk2c[ik]), Nstate), seed=seed)
430 W.append(np.array([W_ik[0]] * atoms.occ.Nspin))
431 else:
432 W_ik = pseudo_uniform((atoms.occ.Nspin, len(atoms.Gk2c[ik]), Nstate), seed=seed)
433 W.append(W_ik)
434 return orth(atoms, W)