Coverage for eminus / gga.py: 100.00%
42 statements
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-21 14:20 +0000
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-21 14:20 +0000
1# SPDX-FileCopyrightText: 2023 The eminus developers
2# SPDX-License-Identifier: Apache-2.0
3"""DFT functions that are only needed for (meta-)GGA calculations.
5Most functions here have been optimized for performance and can be harder to understand than normal.
6To mitigate this, easier (but slower) implementations have been added as comments.
7"""
9from . import backend as xp
10from .utils import handle_k
13def get_grad_field(atoms, field, real=True):
14 """Calculate the gradient of fields on the grid per spin channel.
16 Args:
17 atoms: Atoms object.
18 field: Real-space field per spin channel.
20 Keyword Args:
21 real: Make the gradient a real array (the gradient of n_spin is real).
23 Returns:
24 Gradients of field per spin channel.
25 """
26 dfield = xp.empty((atoms.occ.Nspin, atoms.Ns, 3), dtype=complex)
27 for spin in range(atoms.occ.Nspin):
28 fieldG = atoms.J(field[spin])
29 for dim in range(3):
30 dfield[spin, :, dim] = atoms.I(1j * atoms.G[:, dim] * fieldG)
31 if real:
32 return xp.real(dfield)
33 return dfield
36def gradient_correction(atoms, spin, dn_spin, vsigma):
37 """Calculate the gradient correction for the exchange-correlation potential.
39 Reference: Chem. Phys. Lett. 199, 557.
41 Args:
42 atoms: Atoms object.
43 spin: Spin variable to track whether to do the calculation for spin up or down.
44 dn_spin: Real-space gradient of densities per spin channel.
45 vsigma: Contracted gradient potential derivative.
47 Returns:
48 Gradient correction in reciprocal space.
49 """
50 # sigma is |dn|^2, while vsigma is n * d exc/d sigma
51 h = xp.empty_like(dn_spin)
52 if not atoms.unrestricted:
53 # In the unpolarized case we have no spin mixing and only one spin density
54 h[0] = 2 * vsigma[0, :, None] * dn_spin[0]
55 else:
56 # In the polarized case we would get for spin up (and similar for spin down)
57 # Vxc_u = vxc_u - Nabla dot (2 vsigma_uu * dn_u + vsigma_ud * dn_d)
58 # h is the expression in the brackets
59 h[0] = 2 * vsigma[0, :, None] * dn_spin[0] + vsigma[1, :, None] * dn_spin[1]
60 h[1] = 2 * vsigma[2, :, None] * dn_spin[1] + vsigma[1, :, None] * dn_spin[0]
62 # Calculate Nabla dot h
63 # Normally we would calculate the correction for each spin, but we only need one at a time in H
64 Gh = xp.empty((len(atoms.G2), 3), dtype=complex)
65 for dim in range(3):
66 Gh[:, dim] = atoms.J(h[spin, :, dim])
67 # return 1j * np.sum(atoms.G * Gh, axis=1)
68 return xp.einsum("ir,ir->i", 1j * atoms.G, Gh)
71@handle_k(mode="reduce")
72def get_tau(atoms, Y, ik):
73 """Calculate the positive-definite kinetic energy densities per spin.
75 Reference: J. Chem. Phys. 109, 2092.
77 Args:
78 atoms: Atoms object.
79 Y: Expansion coefficients of orthogonal wave functions in reciprocal space.
80 ik: k-point index.
82 Returns:
83 Real-space positive-definite kinetic energy density.
84 """
85 # The "intuitive" way is the one commented out below (without k-point dependency)
86 # Sadly, this implementation is really slow for various reasons so use the faster one below
88 # Yrs = atoms.I(Y)
89 # tau = np.zeros((atoms.occ.Nspin, atoms.Ns), dtype=complex)
90 # for i in range(atoms.occ.Nstate):
91 # dYrs = get_grad_field(atoms, Yrs[..., i], real=False)
92 # tau += 0.5 * atoms.occ.f[:, i, None] * np.sum(dYrs.conj() * dYrs, axis=2)
93 # return np.real(tau)
95 dYrs = xp.empty((atoms.occ.Nspin, atoms.Ns, Y.shape[-1], 3), dtype=complex)
96 # Calculate the active G vectors and broadcast to the needed shape
97 Gkc = atoms.G[atoms.active[ik]][:, None, :] + atoms.kpts.k[ik]
98 # Calculate the gradients of Y in the active(!) reciprocal space and transform to real-space
99 for dim in range(3):
100 dYrs[..., dim] = atoms.I(1j * Gkc[..., dim] * Y, ik)
101 # Sum over dimensions (dYx* dYx + dYy* dYy + dYz* dYz)
102 # sumdYrs = np.real(np.sum(dYrs.conj() * dYrs, axis=3))
103 # Sum over all states
104 # Use the definition with a division by two
105 # return 0.5 * np.sum(atoms.occ.f[:, None, :] * sumdYrs, axis=2)
106 # Or in compressed Einstein notation:
107 return (
108 0.5
109 * atoms.kpts.wk[ik]
110 * xp.real(
111 xp.einsum("sj,sijr,sijr->si", xp.astype(atoms.occ.f[ik], complex), dYrs.conj(), dYrs)
112 )
113 )
116def calc_Vtau(scf, ik, spin, W, vtau):
117 """Calculate the tau-dependent potential contribution for meta-GGAs.
119 Reference: J. Chem. Phys. 145, 204114.
121 Args:
122 scf: SCF object.
123 ik: k-point index.
124 spin: Spin variable to track whether to do the calculation for spin up or down.
125 W: Expansion coefficients of unconstrained wave functions in reciprocal space.
126 vtau: Kinetic energy density potential derivative.
128 Returns:
129 Tau-dependent potential contribution in reciprocal space.
130 """
131 atoms = scf.atoms
133 if scf.xc_type != "meta-gga": # Only calculate the contribution for meta-GGAs
134 return xp.zeros((len(atoms.Gk2c[ik]), W[ik].shape[-1]), dtype=complex)
136 # The "intuitive" way is the one commented out below (without k-point dependency)
137 # Sadly, this implementation is really slow for various reasons so use the faster one below
139 # GVpsi = np.empty((len(atoms.G2c), 3), dtype=complex)
140 # Gc = atoms.G[atoms.active]
141 # Wrs = atoms.I(W)
142 # for i in range(atoms.occ.Nstate):
143 # dWrs = get_grad_field(atoms, Wrs[..., i], real=False)
144 # for r in range(3):
145 # GVpsi[:, r] = atoms.J(vtau[spin] * dWrs[spin, :, r], full=False)
146 # Vpsi[:, i] = -0.5 * 1j * np.sum(Gc * GVpsi, axis=1)
148 GVpsi = xp.empty((len(atoms.Gk2c[ik]), W[ik].shape[-1], 3), dtype=complex)
149 dWrs = xp.empty((atoms.Ns, W[ik].shape[-1], 3), dtype=complex)
150 # Calculate the active G vectors and broadcast to the needed shape
151 Gkc = atoms.G[atoms.active[ik]][:, None, :] + scf.kpts.k[ik]
152 # We only calculate Vtau for one spin channel, index, and reshape before the loop
153 vtau_spin = vtau[spin, :, None]
154 W_spin = W[ik][spin]
155 for dim in range(3):
156 # Calculate the gradients of W in the active(!) space and transform to real-space
157 dWrs[..., dim] = atoms.I(1j * Gkc[..., dim] * W_spin, ik)
158 # Calculate dexc/dtau * gradpsi and transform to the active reciprocal space
159 GVpsi[..., dim] = atoms.J(vtau_spin * dWrs[..., dim], ik, full=False)
160 # Sum over dimensions
161 # Calculate -0.5 Nabla dot Gvpsi (compare with gradient_correction)
162 # Vpsi = -0.5 * 1j * np.sum(Gkc * GVpsi, axis=2)
163 Vpsi = -0.5 * xp.einsum("ior,ijr->ij", 1j * Gkc, GVpsi)
164 return atoms.O(Vpsi)