Coverage for gpaw/hybrids/wstc.py: 72%
74 statements
« prev ^ index » next coverage.py v7.7.1, created at 2025-07-14 00:18 +0000
« prev ^ index » next coverage.py v7.7.1, created at 2025-07-14 00:18 +0000
1"""Wigner-Seitz truncated coulomb interaction.
3See:
5 Ravishankar Sundararaman and T. A. Arias:
6 Phys. Rev. B 87, 165122 (2013)
8 Regularization of the Coulomb singularity in exact exchange by
9 Wigner-Seitz truncated interactions: Towards chemical accuracy
10 in nontrivial systems
11"""
12from math import pi
14import numpy as np
15from scipy.special import erf
16from ase.units import Bohr
17from ase.utils import seterr
19import gpaw.mpi as mpi
20from gpaw.fftw import get_efficient_fft_size
21from gpaw.grid_descriptor import GridDescriptor
22from gpaw.core import PWDesc, UGDesc, PWArray
23from gpaw.typing import Array1D
26class WignerSeitzTruncatedCoulomb:
27 def __init__(self, cell_cv, nk_c):
28 self.nk_c = nk_c
29 bigcell_cv = cell_cv * nk_c[:, np.newaxis]
30 L_c = (np.linalg.inv(bigcell_cv)**2).sum(0)**-0.5
32 self.rc = 0.5 * L_c.min()
33 self.a = 5 / self.rc
35 nr_c = [get_efficient_fft_size(2 * int(L * self.a * 3.0))
36 for L in L_c]
38 self.gd = GridDescriptor(nr_c, bigcell_cv, comm=mpi.serial_comm)
39 v_ijk = self.gd.empty()
41 pos_ijkv = self.gd.get_grid_point_coordinates().transpose((1, 2, 3, 0))
42 corner_xv = np.dot(np.indices((2, 2, 2)).reshape((3, 8)).T, bigcell_cv)
44 # Ignore division by zero (in 0,0,0 corner):
45 with seterr(invalid='ignore'):
46 # Loop over first dimension to avoid too large ndarrays.
47 for pos_jkv, v_jk in zip(pos_ijkv, v_ijk):
48 # Distances to the 8 corners:
49 d_jkxv = pos_jkv[:, :, np.newaxis] - corner_xv
50 r_jk = (d_jkxv**2).sum(axis=3).min(2)**0.5
51 v_jk[:] = erf(self.a * r_jk) / r_jk
53 # Fix 0/0 corner value:
54 v_ijk[0, 0, 0] = 2 * self.a / pi**0.5
56 self.K_Q = np.fft.fftn(v_ijk) * self.gd.dv
58 def get_description(self):
59 descriptors = []
60 descriptors.append('Inner radius for %dx%dx%d Wigner-Seitz cell: '
61 '%.3f Ang' % (tuple(self.nk_c) + (self.rc * Bohr,)))
62 descriptors.append('Range-separation parameter: %.3f Ang^-1' % (
63 self.a / Bohr))
64 descriptors.append('FFT size for calculating truncated Coulomb: '
65 '%dx%dx%d' % tuple(self.gd.N_c))
67 return '\n'.join(descriptors)
69 def get_potential_new(self, pw: PWDesc, grid: UGDesc) -> PWArray:
70 K_G = pw.zeros()
71 assert isinstance(K_G, PWArray) # !!!
72 size = tuple(grid.size)
73 if pw.dtype == float:
74 size = (size[0], size[1], size[2] // 2 + 1)
75 self._get_potential(pw.indices(size)[pw.ng1:pw.ng2],
76 pw.kpt_c,
77 grid.size_c,
78 pw.dtype,
79 pw.reciprocal_vectors(),
80 K_G.data)
81 return K_G
83 def get_potential(self, pd) -> Array1D:
84 K_G = pd.zeros()
85 self._get_potential(pd.Q_qG[0],
86 pd.kd.bzk_kc[0],
87 pd.gd.N_c,
88 pd.dtype,
89 pd.get_reciprocal_vectors(add_q=True),
90 K_G)
91 return K_G
93 def _get_potential(self, Q_G, q_c, N_c, dtype, qG_Gv, K_G) -> None:
94 shift_c = (q_c * self.nk_c).round().astype(int)
95 max_c = self.gd.N_c // 2
96 if dtype == complex:
97 for G, Q in enumerate(Q_G):
98 Q_c = (np.unravel_index(Q, N_c) + N_c // 2) % N_c - N_c // 2
99 Q_c = Q_c * self.nk_c + shift_c
100 if (abs(Q_c) < max_c).all():
101 K_G[G] = self.K_Q[tuple(Q_c)]
102 else:
103 Nr_c = N_c.copy()
104 Nr_c[2] = N_c[2] // 2 + 1
105 for G, Q in enumerate(Q_G):
106 Q_c = np.array(np.unravel_index(Q, Nr_c))
107 Q_c[:2] += N_c[:2] // 2
108 Q_c[:2] %= N_c[:2]
109 Q_c[:2] -= N_c[:2] // 2
110 if (abs(Q_c) < max_c).all():
111 K_G[G] = self.K_Q[tuple(Q_c)]
113 G2_G = np.sum(qG_Gv**2, axis=1)
114 # G2_G = pd.G2_qG[0]
115 a = self.a
116 G0 = G2_G.argmin()
117 if G2_G[G0] < 1e-11:
118 K0 = K_G[G0] + pi / a**2
119 with np.errstate(invalid='ignore'):
120 K_G += 4 * pi * (1 - np.exp(-G2_G / (4 * a**2))) / G2_G
121 if G2_G[G0] < 1e-11:
122 K_G[G0] = K0