Coverage for gpaw/response/fxc_kernels.py: 99%

85 statements  

« prev     ^ index     » next       coverage.py v7.7.1, created at 2025-07-08 00:17 +0000

1"""Contains methods for calculating local LR-TDDFT kernels.""" 

2 

3from functools import partial 

4from pathlib import Path 

5 

6import numpy as np 

7 

8from gpaw.response import timer 

9from gpaw.response.pw_parallelization import Blocks1D 

10from gpaw.response.dyson import PWKernel 

11from gpaw.response.localft import (LocalFTCalculator, 

12 add_LDA_dens_fxc, add_LSDA_trans_fxc) 

13 

14 

15class FXCKernel(PWKernel): 

16 r"""Adiabatic local exchange-correlation kernel in a plane-wave basis. 

17 

18 In real-space, the adiabatic local xc-kernel matrix is given by: 

19 

20 K_xc^μν(r, r', t-t') = f_xc^μν(r) δ(r-r') δ(t-t') 

21 

22 where the local xc kernel is given by: 

23 

24 ∂^2[ϵ_xc(n,m)n] | 

25 f_xc^μν(r) = ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ | 

26 ∂n^μ ∂n^ν |n=n(r),m=m(r) 

27 

28 In the plane-wave basis (and frequency domain), 

29 

30 1 

31 K_xc^μν(G, G') = ‾‾ f_xc^μν(G-G'), 

32 V0 

33 

34 where V0 is the cell volume. 

35 

36 Because the set of unique reciprocal lattice vector differences 

37 dG = G-G' is much more compact than the full kernel matrix structure 

38 Kxc_GG', we store data internally in the dG representation and only 

39 unfold the data into the full kernel matrix when requested. 

40 """ 

41 

42 def __init__(self, fxc_dG, dG_K, GG_shape, volume): 

43 """Construct the fxc kernel.""" 

44 assert np.prod(GG_shape) == len(dG_K), \ 

45 "The K index should be a flattened (G,G') composite index'" 

46 

47 self._fxc_dG = fxc_dG 

48 self._dG_K = dG_K 

49 self.GG_shape = GG_shape 

50 self.volume = volume 

51 

52 def get_number_of_plane_waves(self): 

53 assert self.GG_shape[0] == self.GG_shape[1] 

54 return self.GG_shape[0] 

55 

56 def _add_to(self, x_GG): 

57 """Add Kxc_GG to input array.""" 

58 x_GG[:] += self.get_Kxc_GG() 

59 

60 def get_Kxc_GG(self): 

61 """Unfold the fxc(G-G') kernel into the Kxc_GG' kernel matrix.""" 

62 # Kxc(G-G') = 1 / V0 * fxc(G-G') 

63 Kxc_dG = 1 / self.volume * self._fxc_dG 

64 

65 # Unfold Kxc(G-G') to the kernel matrix structure Kxc_GG' 

66 Kxc_GG = Kxc_dG[self._dG_K].reshape(self.GG_shape) 

67 

68 return Kxc_GG 

69 

70 def save(self, path: Path): 

71 """Save the fxc kernel in a .npz file.""" 

72 assert path.suffix == '.npz' 

73 with open(str(path), 'wb') as fd: 

74 np.savez(fd, 

75 fxc_dG=self._fxc_dG, 

76 dG_K=self._dG_K, 

77 GG_shape=self.GG_shape, 

78 volume=self.volume) 

79 

80 @staticmethod 

81 def from_file(path: Path): 

82 """Construct an fxc kernel from a previous calculation.""" 

83 assert path.suffix == '.npz' 

84 npzfile = np.load(path) 

85 

86 args = [npzfile[key] 

87 for key in ['fxc_dG', 'dG_K', 'GG_shape', 'volume']] 

88 

89 return FXCKernel(*args) 

90 

91 

92class AdiabaticFXCCalculator: 

93 """Calculator for adiabatic local exchange-correlation kernels.""" 

94 

95 def __init__(self, localft_calc: LocalFTCalculator): 

96 """Contruct the fxc calculator based on a local FT calculator.""" 

97 self.localft_calc = localft_calc 

98 

99 self.gs = localft_calc.gs 

100 self.context = localft_calc.context 

101 

102 @staticmethod 

103 def from_rshe_parameters(*args, **kwargs): 

104 return AdiabaticFXCCalculator( 

105 LocalFTCalculator.from_rshe_parameters(*args, **kwargs)) 

106 

107 @timer('Calculate XC kernel') 

108 def __call__(self, fxc, spincomponent, qpd): 

109 """Calculate fxc(G-G'), which defines the kernel matrix Kxc_GG'. 

110 

111 The fxc kernel is calculated for all unique dG = G-G' reciprocal 

112 lattice vectors and returned as an FXCKernel instance which can unfold 

113 itself to produce the full kernel matrix Kxc_GG'. 

114 """ 

115 # Generate a large_qpd to encompass all G-G' in qpd 

116 large_ecut = 4 * qpd.ecut # G = 1D grid of |G|^2/2 < ecut 

117 large_qpd = qpd.copy_with(ecut=large_ecut, 

118 gammacentered=True, 

119 gd=self.gs.finegd) 

120 

121 # Calculate fxc(Q) on the large plane-wave grid (Q = large grid index) 

122 add_fxc = create_add_fxc(fxc, spincomponent) 

123 fxc_Q = self.localft_calc(large_qpd, add_fxc) 

124 

125 # Create a mapping from the large plane-wave grid to an unfoldable mesh 

126 # of all unique dG = G-G' reciprocal lattice vector differences on the 

127 # qpd plane-wave representation 

128 GG_shape, dG_K, Q_dG = self.create_unfoldable_Q_dG_mapping( 

129 qpd, large_qpd) 

130 

131 # Map the calculated kernel fxc(Q) onto the unfoldable grid of unique 

132 # reciprocal lattice vector differences fxc(dG) 

133 fxc_dG = fxc_Q[Q_dG] 

134 

135 # Return the calculated kernel as an fxc kernel object 

136 fxc_kernel = FXCKernel(fxc_dG, dG_K, GG_shape, qpd.gd.volume) 

137 

138 return fxc_kernel 

139 

140 @timer('Create unfoldable Q_dG mapping') 

141 def create_unfoldable_Q_dG_mapping(self, qpd, large_qpd): 

142 """Create mapping from Q index to the kernel matrix indeces GG'. 

143 

144 The mapping is split into two parts: 

145 * Mapping from the large plane-wave representation index Q (of 

146 large_qpd) to an index dG representing all unique reciprocal lattice 

147 vector differences (G-G') of the original plane-wave representation 

148 qpd 

149 * A mapping from the dG index to a flattened K = (G, G') composite 

150 kernel matrix index 

151 

152 Lastly the kernel matrix shape GG_shape is returned so that an array in 

153 index K can easily be reshaped to a kernel matrix Kxc_GG'. 

154 """ 

155 # Calculate all (G-G') reciprocal lattice vectors 

156 dG_GGv = calculate_dG_GGv(qpd) 

157 GG_shape = dG_GGv.shape[:2] 

158 

159 # Reshape to composite K = (G, G') index 

160 dG_Kv = dG_GGv.reshape(-1, dG_GGv.shape[-1]) 

161 

162 # Find unique dG-vectors 

163 # We need tight control of the decimals to avoid precision artifacts 

164 dG_dGv, dG_K = np.unique(dG_Kv.round(decimals=6), 

165 return_inverse=True, axis=0) 

166 

167 # Create the mapping from Q-index to dG-index 

168 Q_dG = self.create_Q_dG_map(large_qpd, dG_dGv) 

169 

170 return GG_shape, dG_K, Q_dG 

171 

172 @timer('Create Q_dG map') 

173 def create_Q_dG_map(self, large_qpd, dG_dGv): 

174 """Create mapping between (G-G') index dG and large_qpd index Q.""" 

175 G_Qv = large_qpd.get_reciprocal_vectors(add_q=False) 

176 # Make sure to match the precision of dG_dGv 

177 G_Qv = G_Qv.round(decimals=6) 

178 

179 # Distribute dG over world 

180 # This is necessary because the next step is to create a K_QdGv buffer 

181 # of which the norm is taken. When the number of plane-wave 

182 # coefficients is large, this step becomes a memory bottleneck, hence 

183 # the distribution. 

184 dGblocks = Blocks1D(self.context.comm, dG_dGv.shape[0]) 

185 dG_mydGv = dG_dGv[dGblocks.myslice] 

186 

187 # Determine Q index for each dG index 

188 diff_QmydG = np.linalg.norm(G_Qv[:, np.newaxis] - dG_mydGv[np.newaxis], 

189 axis=2) 

190 Q_mydG = np.argmin(diff_QmydG, axis=0) 

191 

192 # Check that all the identified Q indices produce identical reciprocal 

193 # lattice vectors 

194 assert np.allclose(np.diagonal(diff_QmydG[Q_mydG]), 0.), \ 

195 'Could not find a perfect matching reciprocal wave vector in '\ 

196 'large_qpd for all dG_dGv' 

197 

198 # Collect the global Q_dG map 

199 Q_dG = dGblocks.all_gather(Q_mydG) 

200 

201 return Q_dG 

202 

203 

204def create_add_fxc(fxc: str, spincomponent: str): 

205 """Create an add_fxc function according to the requested functional and 

206 spin component.""" 

207 assert fxc in ['ALDA_x', 'ALDA_X', 'ALDA'] 

208 

209 if spincomponent in ['00', 'uu', 'dd']: 

210 add_fxc = partial(add_LDA_dens_fxc, fxc=fxc) 

211 elif spincomponent in ['+-', '-+']: 

212 add_fxc = partial(add_LSDA_trans_fxc, fxc=fxc) 

213 else: 

214 raise ValueError(spincomponent) 

215 

216 return add_fxc 

217 

218 

219def calculate_dG_GGv(qpd): 

220 """Calculate dG_GG' = (G-G') for the plane wave basis in qpd.""" 

221 nG = qpd.ngmax 

222 G_Gv = qpd.get_reciprocal_vectors(add_q=False) 

223 

224 dG_GGv = np.zeros((nG, nG, 3)) 

225 for v in range(3): 

226 dG_GGv[:, :, v] = np.subtract.outer(G_Gv[:, v], G_Gv[:, v]) 

227 

228 return dG_GGv