Coverage for eminus/dft.py: 96.41%
167 statements
« prev ^ index » next coverage.py v7.17.0, created at 2026-10-26 09:44 +0000
« prev ^ index » next coverage.py v7.17.0, created at 2026-10-26 09:44 +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 = 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 # Extra parentheses are used to set the optimal ordering, reducing costs for large arrays
181 tmp = OW @ (invU @ WHW)
182 return atoms.kpts.wk[ik] * ((HW - tmp) @ (U12 @ (F @ U12))) + OW @ (U12 @ Q(Ht @ F - F @ Ht, U))
185def H(scf, ik, spin, W, dn_spin=None, phi=None, vxc=None, vsigma=None, vtau=None):
186 """Left-hand side of the eigenvalue equation.
188 Reference: Comput. Phys. Commun. 128, 1.
190 Args:
191 scf: SCF object.
192 ik: k-point index.
193 spin: Spin variable to track whether to do the calculation for spin up or down.
194 W: Expansion coefficients of unconstrained wave functions in reciprocal space.
196 Keyword Args:
197 dn_spin: Real-space gradient of densities per spin channel.
198 phi: Hartree field.
199 vxc: Exchange-correlation potential.
200 vsigma: Contracted gradient potential derivative.
201 vtau: Kinetic energy gradient potential derivative.
203 Returns:
204 Hamiltonian applied on W.
205 """
206 atoms = scf.atoms
208 # If vxc is None all other keyword arguments are None by design
209 # In that case precompute values from the SCF class
210 if vxc is None:
211 dn_spin, phi, vxc, vsigma, vtau = H_precompute(scf, W)
213 # This calculates the XC potential in the reciprocal space
214 Gvxc = atoms.J(vxc[spin])
215 # Calculate the gradient correction to the potential if a (meta-)GGA functional is used
216 if "gga" in scf.xc_type:
217 Gvxc = Gvxc - gradient_correction(atoms, spin, dn_spin, vsigma)
218 # Vkin = -0.5 L(W)
219 Vkin_psi = -0.5 * atoms.L(W[ik][spin], ik)
220 # Veff = Jdag(Vion) + Jdag(O(J(vxc))) + Jdag(O(phi))
221 # We get the full potential in the functional definition (different to the DFT++ notation)
222 # Normally Vxc = Jdag(O(J(exc))) + diag(exc') Jdag(O(J(n))) (for LDA functionals)
223 Veff = scf.Vloc + atoms.Jdag(atoms.O(Gvxc + phi))
224 Vnonloc_psi = calc_Vnonloc(scf, ik, spin, W)
225 Vtau_psi = calc_Vtau(scf, ik, spin, W, vtau)
226 # H = Vkin + Idag(diag(Veff))I + Vnonloc (+ Vtau)
227 # Diag(a) * B can be written as a * B if a is a column vector
228 return (
229 Vkin_psi + atoms.Idag(Veff[:, None] * atoms.I(W[ik][spin], ik), ik) + Vnonloc_psi + Vtau_psi
230 )
233def H_precompute(scf, W):
234 """Create precomputed values as intermediate results.
236 Args:
237 scf: SCF object.
238 W: Expansion coefficients of unconstrained wave functions in reciprocal space.
240 Returns:
241 dn_spin, phi, vxc, vsigma, and vtau.
242 """
243 # We are calculating everything here over both spin channels
244 # We would be fine/could improve performance by only calculating the currently used spin channel
245 atoms = scf.atoms
246 Y = orth(atoms, W)
247 n_spin = get_n_spin(atoms, Y)
248 n = get_n_total(atoms, Y, n_spin)
249 if "gga" in scf.xc_type:
250 dn_spin = get_grad_field(atoms, n_spin)
251 else:
252 dn_spin = None
253 if scf.xc_type == "meta-gga":
254 tau = get_tau(atoms, Y)
255 else:
256 tau = None
257 phi = get_phi(atoms, n)
258 vxc, vsigma, vtau = get_vxc(scf.xc, n_spin, atoms.occ.Nspin, dn_spin, tau, scf.xc_params)
259 return dn_spin, phi, vxc, vsigma, vtau
262def Q(inp, U):
263 """Operator needed to calculate gradients with non-constant occupations.
265 Reference: Comput. Phys. Commun. 128, 1.
267 Args:
268 inp: Coefficients input array.
269 U: Overlap of wave functions.
271 Returns:
272 Q operator result.
273 """
274 mu, V = xp.linalg.eig(U)
275 mu = mu[:, None]
276 denom = xp.sqrt(mu) @ xp.ones((1, len(mu)), dtype=complex)
277 denom2 = denom + denom.conj().T
278 # return V @ ((V.conj().T @ inp @ V) / denom2) @ V.conj().T
279 tmp = V.conj().T @ inp @ V
280 return V @ (tmp / denom2) @ V.conj().T
283def get_psi(scf, W, **kwargs):
284 """Calculate eigenstates from H.
286 Reference: Comput. Phys. Commun. 128, 1.
288 Args:
289 scf: SCF object.
290 W: Expansion coefficients of unconstrained wave functions in reciprocal space.
292 Keyword Args:
293 **kwargs: See :func:`H`.
295 Returns:
296 Eigenstates in reciprocal space.
297 """
298 if W is None:
299 log.warning('The provided wave function is "None".')
300 return None
302 atoms = scf.atoms
303 Y = orth(atoms, W)
304 psi = [xp.empty_like(Yk) for Yk in Y]
305 for ik in range(atoms.kpts.Nk):
306 for spin in range(atoms.occ.Nspin):
307 mu = Y[ik][spin].conj().T @ H(scf, ik, spin, Y, **kwargs)
308 _, D = xp.linalg.eigh(mu)
309 psi[ik][spin] = Y[ik][spin] @ D
310 return psi
313def get_epsilon(scf, W, **kwargs):
314 """Calculate eigenvalues from H.
316 Reference: Comput. Phys. Commun. 128, 1.
318 Args:
319 scf: SCF object.
320 W: Expansion coefficients of unconstrained wave functions in reciprocal space.
322 Keyword Args:
323 **kwargs: See :func:`H`.
325 Returns:
326 Eigenvalues.
327 """
328 if W is None:
329 log.warning('The provided wave function is "None".')
330 return None
332 atoms = scf.atoms
333 Y = orth(atoms, W)
334 epsilon = xp.empty((atoms.kpts.Nk, atoms.occ.Nspin, Y[0].shape[-1]))
335 for ik in range(atoms.kpts.Nk):
336 for spin in range(atoms.occ.Nspin):
337 mu = Y[ik][spin].conj().T @ H(scf, ik, spin, Y, **kwargs)
338 epsilon[ik][spin] = xp.sort(xp.linalg.eigvalsh(mu))
339 return epsilon
342def get_epsilon_unocc(scf, W, Z, **kwargs):
343 """Calculate eigenvalues from H of unoccupied states.
345 Reference: Comput. Phys. Commun. 128, 1.
347 Args:
348 scf: SCF object.
349 W: Expansion coefficients of unconstrained wave functions in reciprocal space.
350 Z: Expansion coefficients of unconstrained wave functions in reciprocal space.
352 Keyword Args:
353 **kwargs: See :func:`H`.
355 Returns:
356 Eigenvalues.
357 """
358 if W is None or Z is None:
359 log.warning('The provided wave function is "None".')
360 return None
362 atoms = scf.atoms
363 Y = orth(atoms, W)
364 D = orth_unocc(atoms, Y, Z)
365 epsilon = xp.empty((atoms.kpts.Nk, atoms.occ.Nspin, D[0].shape[-1]))
366 for ik in range(atoms.kpts.Nk):
367 for spin in range(atoms.occ.Nspin):
368 mu = D[ik][spin].conj().T @ H(scf, ik, spin, D, **kwargs)
369 epsilon[ik][spin] = xp.sort(xp.linalg.eigvalsh(mu))
370 return epsilon
373def guess_random(scf, Nstate=None, seed=42, symmetric=False):
374 """Generate random initial-guess coefficients as starting values.
376 Args:
377 scf: SCF object.
379 Keyword Args:
380 Nstate: Number of states.
381 seed: Seed to initialize the random number generator.
382 symmetric: Whether to use the same guess for both spin channels.
384 Returns:
385 Initial-guess orthogonal wave functions in reciprocal space.
386 """
387 atoms = scf.atoms
388 if Nstate is None:
389 Nstate = atoms.occ.Nstate
391 rng = Generator(SFC64(seed))
392 W = []
393 for ik in range(atoms.kpts.Nk):
394 if symmetric:
395 W_ik = xp.asarray(
396 rng.standard_normal((len(atoms.Gk2c[ik]), Nstate))
397 + 1j * rng.standard_normal((len(atoms.Gk2c[ik]), Nstate))
398 )
399 W.append(xp.stack([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(xp.asarray(W_ik))
405 return orth(atoms, W)
408def guess_filter(scf, Nstate=None, seed=42, symmetric=False):
409 """Generate filtered random initial-guess coefficients as starting values.
411 A weighting corresponding to a Gaussian distribution in G^2 is applied to the random values,
412 which acts similar to a low-pass filter.
414 Reference: https://doi.org/10.1017/CBO9780511609633
416 Args:
417 scf: SCF object.
419 Keyword Args:
420 Nstate: Number of states.
421 seed: Seed to initialize the random number generator.
422 symmetric: Whether to use the same guess for both spin channels.
424 Returns:
425 Initial-guess orthogonal wave functions in reciprocal space.
426 """
427 atoms = scf.atoms
428 if Nstate is None:
429 Nstate = atoms.occ.Nstate
431 rng = Generator(SFC64(seed))
432 W = []
433 for ik in range(atoms.kpts.Nk):
434 weight = 1 / math.sqrt(2 * math.pi) * xp.exp(-atoms.Gk2c[ik] / 2)
435 if symmetric:
436 W_ik = xp.asarray(
437 rng.standard_normal((len(atoms.Gk2c[ik]), Nstate))
438 + 1j * rng.standard_normal((len(atoms.Gk2c[ik]), Nstate))
439 )
440 W.append(weight[:, None] * xp.stack([W_ik] * atoms.occ.Nspin))
441 else:
442 W_ik = rng.standard_normal(
443 (atoms.occ.Nspin, len(atoms.Gk2c[ik]), Nstate)
444 ) + 1j * rng.standard_normal((atoms.occ.Nspin, len(atoms.Gk2c[ik]), Nstate))
445 W.append(weight[:, None] * xp.asarray(W_ik))
446 return orth(atoms, W)
449def guess_pseudo(scf, Nstate=None, seed=1234, symmetric=False):
450 """Generate initial-guess coefficients using pseudo-random starting values.
452 Args:
453 scf: SCF object.
455 Keyword Args:
456 Nstate: Number of states.
457 seed: Seed to initialize the random number generator.
458 symmetric: Whether to use the same guess for both spin channels.
460 Returns:
461 Initial-guess orthogonal wave functions in reciprocal space.
462 """
463 atoms = scf.atoms
464 if Nstate is None:
465 Nstate = atoms.occ.Nstate
467 W = []
468 for ik in range(atoms.kpts.Nk):
469 if symmetric:
470 W_ik = pseudo_uniform((1, len(atoms.Gk2c[ik]), Nstate), seed=seed)
471 W.append(xp.stack([W_ik[0]] * atoms.occ.Nspin))
472 else:
473 W_ik = pseudo_uniform((atoms.occ.Nspin, len(atoms.Gk2c[ik]), Nstate), seed=seed)
474 W.append(W_ik)
475 return orth(atoms, W)