Coverage for eminus/dft.py: 96.08%
153 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-21 12:19 +0000
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-21 12:19 +0000
1# SPDX-FileCopyrightText: 2022 The eminus developers
2# SPDX-License-Identifier: Apache-2.0
3"""Main DFT functions based on the DFT++ formulation."""
5import math
7from numpy.random import Generator, SFC64
9from . import backend as xp
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 * math.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 @ xp.linalg.inv(xp.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 = [xp.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 @ xp.linalg.inv(xp.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 xp.sum(n_spin, axis=0)
94 # n = (IW) F (IW)dag
95 n = xp.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 += xp.sum(
100 atoms.occ.f[ik, spin]
101 * atoms.kpts.wk[ik]
102 * xp.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 = xp.empty((atoms.occ.Nspin, atoms.Ns))
124 for spin in range(atoms.occ.Nspin):
125 n[spin] = xp.sum(
126 atoms.occ.f[ik, spin] * atoms.kpts.wk[ik] * xp.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 = xp.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] * xp.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 = xp.asarray(atoms.occ.F[ik][spin], dtype=complex)
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 = xp.linalg.inv(U)
176 U12 = xp.sqrtm(invU)
177 # Htilde = U^-0.5 Wdag H(W) U^-0.5
178 Ht = xp.linalg.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 = xp.linalg.multi_dot([OW, invU, WHW])
181 return atoms.kpts.wk[ik] * (
182 xp.linalg.multi_dot([HW - tmp, U12, F, U12])
183 + xp.linalg.multi_dot([OW, U12, Q(Ht @ F - F @ Ht, U)])
184 )
187def H(scf, ik, spin, W, dn_spin=None, phi=None, vxc=None, vsigma=None, vtau=None):
188 """Left-hand side of the eigenvalue equation.
190 Reference: Comput. Phys. Commun. 128, 1.
192 Args:
193 scf: SCF object.
194 ik: k-point index.
195 spin: Spin variable to track whether to do the calculation for spin up or down.
196 W: Expansion coefficients of unconstrained wave functions in reciprocal space.
198 Keyword Args:
199 dn_spin: Real-space gradient of densities per spin channel.
200 phi: Hartree field.
201 vxc: Exchange-correlation potential.
202 vsigma: Contracted gradient potential derivative.
203 vtau: Kinetic energy gradient potential derivative.
205 Returns:
206 Hamiltonian applied on W.
207 """
208 atoms = scf.atoms
210 # If dn_spin is None all other keyword arguments are None by design
211 # In that case precompute values from the SCF class
212 if phi is None:
213 dn_spin, phi, vxc, vsigma, vtau = H_precompute(scf, W)
215 # This calculates the XC potential in the reciprocal space
216 Gvxc = atoms.J(vxc[spin])
217 # Calculate the gradient correction to the potential if a (meta-)GGA functional is used
218 if "gga" in scf.xc_type:
219 Gvxc = Gvxc - gradient_correction(atoms, spin, dn_spin, vsigma)
220 # Vkin = -0.5 L(W)
221 Vkin_psi = -0.5 * atoms.L(W[ik][spin], ik)
222 # Veff = Jdag(Vion) + Jdag(O(J(vxc))) + Jdag(O(phi))
223 # We get the full potential in the functional definition (different to the DFT++ notation)
224 # Normally Vxc = Jdag(O(J(exc))) + diag(exc') Jdag(O(J(n))) (for LDA functionals)
225 Veff = scf.Vloc + atoms.Jdag(atoms.O(Gvxc + phi))
226 Vnonloc_psi = calc_Vnonloc(scf, ik, spin, W)
227 Vtau_psi = calc_Vtau(scf, ik, spin, W, vtau)
228 # H = Vkin + Idag(diag(Veff))I + Vnonloc (+ Vtau)
229 # Diag(a) * B can be written as a * B if a is a column vector
230 return (
231 Vkin_psi + atoms.Idag(Veff[:, None] * atoms.I(W[ik][spin], ik), ik) + Vnonloc_psi + Vtau_psi
232 )
235def H_precompute(scf, W):
236 """Create precomputed values as intermediate results.
238 Args:
239 scf: SCF object.
240 W: Expansion coefficients of unconstrained wave functions in reciprocal space.
242 Returns:
243 dn_spin, phi, vxc, vsigma, and vtau.
244 """
245 # We are calculating everything here over both spin channels
246 # We would be fine/could improve performance by only calculating the currently used spin channel
247 atoms = scf.atoms
248 Y = orth(atoms, W)
249 n_spin = get_n_spin(atoms, Y)
250 n = get_n_total(atoms, Y, n_spin)
251 if "gga" in scf.xc_type:
252 dn_spin = get_grad_field(atoms, n_spin)
253 else:
254 dn_spin = None
255 if scf.xc_type == "meta-gga":
256 tau = get_tau(atoms, Y)
257 else:
258 tau = None
259 phi = get_phi(atoms, n)
260 vxc, vsigma, vtau = get_vxc(scf.xc, n_spin, atoms.occ.Nspin, dn_spin, tau, scf.xc_params)
261 return dn_spin, phi, vxc, vsigma, vtau
264def Q(inp, U):
265 """Operator needed to calculate gradients with non-constant occupations.
267 Reference: Comput. Phys. Commun. 128, 1.
269 Args:
270 inp: Coefficients input array.
271 U: Overlap of wave functions.
273 Returns:
274 Q operator result.
275 """
276 mu, V = xp.linalg.eig(U)
277 mu = mu[:, None]
278 denom = xp.sqrt(mu) @ xp.ones((1, len(mu)), dtype=complex)
279 denom2 = denom + denom.conj().T
280 # return V @ ((V.conj().T @ inp @ V) / denom2) @ V.conj().T
281 tmp = xp.linalg.multi_dot([V.conj().T, inp, V])
282 return xp.linalg.multi_dot([V, tmp / denom2, V.conj().T])
285def get_psi(scf, W, **kwargs):
286 """Calculate eigenstates from H.
288 Reference: Comput. Phys. Commun. 128, 1.
290 Args:
291 scf: SCF object.
292 W: Expansion coefficients of unconstrained wave functions in reciprocal space.
294 Keyword Args:
295 **kwargs: See :func:`H`.
297 Returns:
298 Eigenstates in reciprocal space.
299 """
300 if W is None:
301 log.warning('The provided wave function is "None".')
302 return None
304 atoms = scf.atoms
305 Y = orth(atoms, W)
306 psi = [xp.empty_like(Yk) for Yk in Y]
307 for ik in range(atoms.kpts.Nk):
308 for spin in range(atoms.occ.Nspin):
309 mu = Y[ik][spin].conj().T @ H(scf, ik, spin, Y, **kwargs)
310 _, D = xp.linalg.eigh(mu)
311 psi[ik][spin] = Y[ik][spin] @ D
312 return psi
315def get_epsilon(scf, W, **kwargs):
316 """Calculate eigenvalues from H.
318 Reference: Comput. Phys. Commun. 128, 1.
320 Args:
321 scf: SCF object.
322 W: Expansion coefficients of unconstrained wave functions in reciprocal space.
324 Keyword Args:
325 **kwargs: See :func:`H`.
327 Returns:
328 Eigenvalues.
329 """
330 if W is None:
331 log.warning('The provided wave function is "None".')
332 return None
334 atoms = scf.atoms
335 Y = orth(atoms, W)
336 epsilon = xp.empty((atoms.kpts.Nk, atoms.occ.Nspin, Y[0].shape[-1]))
337 for ik in range(atoms.kpts.Nk):
338 for spin in range(atoms.occ.Nspin):
339 mu = Y[ik][spin].conj().T @ H(scf, ik, spin, Y, **kwargs)
340 epsilon[ik][spin] = xp.sort(xp.linalg.eigvalsh(mu))
341 return epsilon
344def get_epsilon_unocc(scf, W, Z, **kwargs):
345 """Calculate eigenvalues from H of unoccupied states.
347 Reference: Comput. Phys. Commun. 128, 1.
349 Args:
350 scf: SCF object.
351 W: Expansion coefficients of unconstrained wave functions in reciprocal space.
352 Z: Expansion coefficients of unconstrained wave functions in reciprocal space.
354 Keyword Args:
355 **kwargs: See :func:`H`.
357 Returns:
358 Eigenvalues.
359 """
360 if W is None or Z is None:
361 log.warning('The provided wave function is "None".')
362 return None
364 atoms = scf.atoms
365 Y = orth(atoms, W)
366 D = orth_unocc(atoms, Y, Z)
367 epsilon = xp.empty((atoms.kpts.Nk, atoms.occ.Nspin, D[0].shape[-1]))
368 for ik in range(atoms.kpts.Nk):
369 for spin in range(atoms.occ.Nspin):
370 mu = D[ik][spin].conj().T @ H(scf, ik, spin, D, **kwargs)
371 epsilon[ik][spin] = xp.sort(xp.linalg.eigvalsh(mu))
372 return epsilon
375def guess_random(scf, Nstate=None, seed=42, symmetric=False):
376 """Generate random initial-guess coefficients as starting values.
378 Args:
379 scf: SCF object.
381 Keyword Args:
382 Nstate: Number of states.
383 seed: Seed to initialize the random number generator.
384 symmetric: Whether to use the same guess for both spin channels.
386 Returns:
387 Initial-guess orthogonal wave functions in reciprocal space.
388 """
389 atoms = scf.atoms
390 if Nstate is None:
391 Nstate = atoms.occ.Nstate
393 rng = Generator(SFC64(seed))
394 W = []
395 for ik in range(atoms.kpts.Nk):
396 if symmetric:
397 W_ik = xp.asarray(
398 rng.standard_normal((len(atoms.Gk2c[ik]), Nstate))
399 + 1j * rng.standard_normal((len(atoms.Gk2c[ik]), Nstate))
400 )
401 W.append(xp.stack([W_ik] * atoms.occ.Nspin))
402 else:
403 W_ik = rng.standard_normal(
404 (atoms.occ.Nspin, len(atoms.Gk2c[ik]), Nstate)
405 ) + 1j * rng.standard_normal((atoms.occ.Nspin, len(atoms.Gk2c[ik]), Nstate))
406 W.append(xp.asarray(W_ik))
407 return orth(atoms, W)
410def guess_pseudo(scf, Nstate=None, seed=1234, symmetric=False):
411 """Generate initial-guess coefficients using pseudo-random starting values.
413 Args:
414 scf: SCF object.
416 Keyword Args:
417 Nstate: Number of states.
418 seed: Seed to initialize the random number generator.
419 symmetric: Whether to use the same guess for both spin channels.
421 Returns:
422 Initial-guess orthogonal wave functions in reciprocal space.
423 """
424 atoms = scf.atoms
425 if Nstate is None:
426 Nstate = atoms.occ.Nstate
428 W = []
429 for ik in range(atoms.kpts.Nk):
430 if symmetric:
431 W_ik = pseudo_uniform((1, len(atoms.Gk2c[ik]), Nstate), seed=seed)
432 W.append(xp.stack([W_ik[0]] * atoms.occ.Nspin))
433 else:
434 W_ik = pseudo_uniform((atoms.occ.Nspin, len(atoms.Gk2c[ik]), Nstate), seed=seed)
435 W.append(W_ik)
436 return orth(atoms, W)