Coverage for eminus/gga.py: 100.00%
42 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: 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(xp.einsum("sj,sijr,sijr->si", atoms.occ.f[ik], dYrs.conj(), dYrs))
111 )
114def calc_Vtau(scf, ik, spin, W, vtau):
115 """Calculate the tau-dependent potential contribution for meta-GGAs.
117 Reference: J. Chem. Phys. 145, 204114.
119 Args:
120 scf: SCF object.
121 ik: k-point index.
122 spin: Spin variable to track whether to do the calculation for spin up or down.
123 W: Expansion coefficients of unconstrained wave functions in reciprocal space.
124 vtau: Kinetic energy density potential derivative.
126 Returns:
127 Tau-dependent potential contribution in reciprocal space.
128 """
129 atoms = scf.atoms
131 if scf.xc_type != "meta-gga": # Only calculate the contribution for meta-GGAs
132 return xp.zeros((len(atoms.Gk2c[ik]), W[ik].shape[-1]), dtype=complex)
134 # The "intuitive" way is the one commented out below (without k-point dependency)
135 # Sadly, this implementation is really slow for various reasons so use the faster one below
137 # GVpsi = np.empty((len(atoms.G2c), 3), dtype=complex)
138 # Gc = atoms.G[atoms.active]
139 # Wrs = atoms.I(W)
140 # for i in range(atoms.occ.Nstate):
141 # dWrs = get_grad_field(atoms, Wrs[..., i], real=False)
142 # for r in range(3):
143 # GVpsi[:, r] = atoms.J(vtau[spin] * dWrs[spin, :, r], full=False)
144 # Vpsi[:, i] = -0.5 * 1j * np.sum(Gc * GVpsi, axis=1)
146 GVpsi = xp.empty((len(atoms.Gk2c[ik]), W[ik].shape[-1], 3), dtype=complex)
147 dWrs = xp.empty((atoms.Ns, W[ik].shape[-1], 3), dtype=complex)
148 # Calculate the active G vectors and broadcast to the needed shape
149 Gkc = atoms.G[atoms.active[ik]][:, None, :] + scf.kpts.k[ik]
150 # We only calculate Vtau for one spin channel, index, and reshape before the loop
151 vtau_spin = vtau[spin, :, None]
152 W_spin = W[ik][spin]
153 for dim in range(3):
154 # Calculate the gradients of W in the active(!) space and transform to real-space
155 dWrs[..., dim] = atoms.I(1j * Gkc[..., dim] * W_spin, ik)
156 # Calculate dexc/dtau * gradpsi and transform to the active reciprocal space
157 GVpsi[..., dim] = atoms.J(vtau_spin * dWrs[..., dim], ik, full=False)
158 # Sum over dimensions
159 # Calculate -0.5 Nabla dot Gvpsi (compare with gradient_correction)
160 # Vpsi = -0.5 * 1j * np.sum(Gkc * GVpsi, axis=2)
161 Vpsi = -0.5 * xp.einsum("ior,ijr->ij", 1j * Gkc, GVpsi)
162 return atoms.O(Vpsi)