Coverage for gpaw/response/screened_interaction.py: 85%

240 statements  

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

1import numpy as np 

2from math import pi 

3from gpaw.response.q0_correction import Q0Correction 

4from ase.units import Ha 

5from ase.dft.kpoints import monkhorst_pack 

6from gpaw.kpt_descriptor import KPointDescriptor 

7from gpaw.response.temp import DielectricFunctionCalculator 

8from gpaw.response.hilbert import GWHilbertTransforms 

9from gpaw.response.mpa_interpolation import RESolver 

10from gpaw.cgpaw import evaluate_mpa_poly 

11 

12 

13class GammaIntegrationMode: 

14 def __init__(self, gamma_integration): 

15 if isinstance(gamma_integration, GammaIntegrationMode): 

16 self.type = gamma_integration.type 

17 self.reduced = gamma_integration.reduced 

18 self._N = gamma_integration._N 

19 return 

20 

21 defaults = {'sphere': {'type': 'sphere'}, 

22 'reciprocal': {'type': 'reciprocal'}, 

23 'reciprocal2D': {'type': 'reciprocal', 'reduced': True}, 

24 '1BZ': {'type': '1BZ'}, 

25 '1BZ2D': {'type': '1BZ', 'reduced': True}, 

26 'WS': {'type': 'WS'}} 

27 

28 if isinstance(gamma_integration, int): 

29 raise TypeError("gamma_integration=INT is no longer supported. " 

30 "Please start using the new notations, as is given" 

31 " in the documentation in gpaw/response/g0w0.py" 

32 " of __init__ of class G0W0.") 

33 

34 if isinstance(gamma_integration, str): 

35 gamma_integration = defaults[gamma_integration] 

36 

37 self.type = gamma_integration['type'] 

38 self.reduced = gamma_integration.get('reduced', False) 

39 self._N = gamma_integration.get('N', 100) 

40 

41 if self.type not in {'sphere', 'reciprocal', '1BZ', 'WS'}: 

42 raise TypeError('type in gamma_integration should be one of sphere' 

43 ', reciprocal, 1BZ, or WS.') 

44 

45 if not self.is_numerical: 

46 if gamma_integration.get('reduced', False): 

47 raise TypeError('reduced key being True is only supported for ' 

48 'type reciprocal or 1BZ.') 

49 

50 def __repr__(self): 

51 return f'type: {self.type} reduced: {self.reduced}' 

52 

53 @property 

54 def is_analytical(self): 

55 return self.type == 'sphere' 

56 

57 @property 

58 def is_numerical(self): 

59 return self.type in {'reciprocal', '1BZ'} 

60 

61 @property 

62 def is_Wigner_Seitz(self): 

63 return self.type == 'WS' 

64 

65 @property 

66 def to_1bz(self): 

67 return self.type == '1BZ' 

68 

69 @property 

70 def N(self): 

71 return self._N 

72 

73 

74class QPointDescriptor(KPointDescriptor): 

75 

76 @staticmethod 

77 def from_gs(gs): 

78 kd, atoms = gs.kd, gs.atoms 

79 # Find q-vectors and weights in the IBZ: 

80 assert -1 not in kd.bz2bz_ks 

81 offset_c = 0.5 * ((kd.N_c + 1) % 2) / kd.N_c 

82 bzq_qc = monkhorst_pack(kd.N_c) + offset_c 

83 qd = KPointDescriptor(bzq_qc) 

84 qd.set_symmetry(atoms, kd.symmetry) 

85 return qd 

86 

87 

88def initialize_w_calculator(chi0calc, context, *, 

89 coulomb, 

90 xc='RPA', # G0W0Kernel arguments 

91 mpa=None, E0=Ha, eta=None, 

92 integrate_gamma=GammaIntegrationMode('sphere'), 

93 q0_correction=False): 

94 """Initialize a WCalculator from a Chi0Calculator. 

95 

96 Parameters 

97 ---------- 

98 chi0calc : Chi0Calculator 

99 xc : str 

100 Kernel to use when including vertex corrections. 

101 

102 Remaining arguments: See WCalculator 

103 """ 

104 from gpaw.response.g0w0_kernels import G0W0Kernel 

105 

106 gs = chi0calc.gs 

107 qd = QPointDescriptor.from_gs(gs) 

108 

109 xckernel = G0W0Kernel(xc=xc, ecut=chi0calc.chi0_body_calc.ecut, 

110 gs=gs, qd=qd, 

111 context=context) 

112 

113 kwargs = dict() 

114 if mpa: 

115 wcalc_cls = MPACalculator 

116 kwargs['mpa'] = mpa 

117 else: 

118 wcalc_cls = WCalculator 

119 

120 return wcalc_cls(gs, context, qd=qd, 

121 coulomb=coulomb, xckernel=xckernel, 

122 integrate_gamma=integrate_gamma, eta=eta, 

123 q0_correction=q0_correction, **kwargs) 

124 

125 

126class WBaseCalculator(): 

127 

128 def __init__(self, gs, context, *, qd, 

129 coulomb, xckernel, 

130 integrate_gamma=GammaIntegrationMode('sphere'), 

131 eta=None, 

132 q0_correction=False): 

133 """ 

134 Base class for W Calculator including basic initializations and Gamma 

135 Gamma handling. 

136 

137 Parameters 

138 ---------- 

139 gs : ResponseGroundStateAdapter 

140 context : ResponseContext 

141 qd : QPointDescriptor 

142 coulomb : CoulombKernel 

143 xckernel : G0W0Kernel 

144 integrate_gamma: GammaIntegrationMode 

145 q0_correction : bool 

146 Analytic correction to the q=0 contribution applicable to 2D 

147 systems. 

148 """ 

149 self.gs = gs 

150 self.context = context 

151 self.qd = qd 

152 self.coulomb = coulomb 

153 self.xckernel = xckernel 

154 self.integrate_gamma = integrate_gamma 

155 self.eta = eta 

156 

157 if q0_correction: 

158 assert self.coulomb.truncation == '2D' 

159 self.q0_corrector = Q0Correction( 

160 cell_cv=self.gs.gd.cell_cv, 

161 bzk_kc=self.gs.kd.bzk_kc, 

162 N_c=self.qd.N_c) 

163 

164 npts_c = self.q0_corrector.npts_c 

165 self.context.print('Applying analytical 2D correction to W:', 

166 flush=False) 

167 self.context.print(' Evaluating Gamma point contribution to W ' 

168 + 'on a %dx%dx%d grid' % tuple(npts_c)) 

169 else: 

170 self.q0_corrector = None 

171 

172 def get_V0sqrtV0(self, chi0): 

173 """ 

174 Integrated Coulomb kernels. 

175 """ 

176 V0 = None 

177 sqrtV0 = None 

178 if self.integrate_gamma.is_numerical: 

179 reduced = self.integrate_gamma.reduced 

180 tofirstbz = self.integrate_gamma.to_1bz 

181 N = self.integrate_gamma.N 

182 V0, sqrtV0 = self.coulomb.integrated_kernel(qpd=chi0.qpd, 

183 reduced=reduced, 

184 tofirstbz=tofirstbz, 

185 N=N) 

186 elif self.integrate_gamma.is_analytical: 

187 if chi0.optical_limit: 

188 # The volume of reciprocal cell occupied by a single q-point 

189 bzvol = (2 * np.pi)**3 / self.gs.volume / self.qd.nbzkpts 

190 # Radius of a sphere with a volume of the bzvol above 

191 Rq0 = (3 * bzvol / (4 * np.pi))**(1. / 3.) 

192 # Analytical integral of Coulomb interaction over the sphere 

193 # defined above centered at q=0. 

194 # V0 = 1/|bzvol| int_bzvol dq 4 pi / |q^2| 

195 V0 = 16 * np.pi**2 * Rq0 / bzvol 

196 # Analytical integral of square root of Coulomb interaction 

197 # over the same sphere 

198 # sqrtV0 = 1/|bzvol| int_bzvol dq sqrt(4 pi / |q^2|) 

199 sqrtV0 = (4 * np.pi)**(1.5) * Rq0**2 / bzvol / 2 

200 else: 

201 raise KeyError('Unknown integrate_gamma option:' 

202 f'{self.integrate_gamma}.') 

203 return V0, sqrtV0 

204 

205 def apply_gamma_correction(self, W_GG, einv_GG, V0, sqrtV0, sqrtV_G): 

206 """ 

207 Replacing q=0, (G,G')= (0,0), (0,:), (:,0) with corresponding 

208 matrix elements calculated with an average of the (diverging) 

209 Coulomb interaction. 

210 XXX: Understand and document exact expressions 

211 """ 

212 W_GG[0, 0] = einv_GG[0, 0] * V0 

213 W_GG[0, 1:] = einv_GG[0, 1:] * sqrtV_G[1:] * sqrtV0 

214 W_GG[1:, 0] = einv_GG[1:, 0] * sqrtV0 * sqrtV_G[1:] 

215 

216 

217class WCalculator(WBaseCalculator): 

218 def get_HW_model(self, chi0, fxc_mode, only_correlation=True): 

219 assert only_correlation 

220 W_wGG = self.calculate_W_WgG(chi0, 

221 fxc_mode=fxc_mode, 

222 only_correlation=True) 

223 # HT used to calculate convulution between time-ordered G and W 

224 hilbert_transform = GWHilbertTransforms(chi0.wd.omega_w, self.eta) 

225 with self.context.timer('Hilbert'): 

226 W_xwGG = hilbert_transform(W_wGG) 

227 

228 factor = 1.0 / (self.qd.nbzkpts * 2 * pi * self.gs.volume) 

229 return FullFrequencyHWModel(chi0.wd, W_xwGG, factor) 

230 

231 def calculate_W_WgG(self, chi0, 

232 fxc_mode='GW', 

233 only_correlation=False): 

234 """Calculate the screened interaction in W_wGG or W_WgG representation. 

235 

236 Additional Parameters 

237 ---------- 

238 only_correlation: bool 

239 if true calculate Wc otherwise calculate full W 

240 out_dist: str 

241 specifices output distribution of W array (wGG or WgG) 

242 """ 

243 W_wGG = self.calculate_W_wGG(chi0, fxc_mode, 

244 only_correlation=only_correlation) 

245 

246 W_WgG = chi0.body.blockdist.distribute_as(W_wGG, chi0.body.nw, 'WgG') 

247 return W_WgG 

248 

249 def calculate_W_wGG(self, chi0, fxc_mode='GW', 

250 only_correlation=False): 

251 """In-place calculation of the screened interaction.""" 

252 dfc = DielectricFunctionCalculator(chi0, self.coulomb, 

253 self.xckernel, fxc_mode) 

254 self.context.timer.start('Dyson eq.') 

255 

256 if self.integrate_gamma.is_Wigner_Seitz: 

257 from gpaw.hybrids.wstc import WignerSeitzTruncatedCoulomb 

258 wstc = WignerSeitzTruncatedCoulomb(chi0.qpd.gd.cell_cv, 

259 dfc.coulomb.N_c) 

260 sqrtV_G = wstc.get_potential(chi0.qpd)**0.5 

261 else: 

262 sqrtV_G = dfc.sqrtV_G 

263 V0, sqrtV0 = self.get_V0sqrtV0(chi0) 

264 

265 einv_wGG = dfc.get_epsinv_wGG(only_correlation=False) 

266 W_wGG = np.empty_like(einv_wGG) 

267 for iw, (einv_GG, W_GG) in enumerate(zip(einv_wGG, W_wGG)): 

268 # If only_correlation = True function spits out 

269 # W^c = sqrt(V)(epsinv - delta_GG')sqrt(V). However, full epsinv 

270 # is still needed for q0_corrector. 

271 einvt_GG = (einv_GG - dfc.I_GG) if only_correlation else einv_GG 

272 W_GG[:] = einvt_GG * (sqrtV_G * 

273 sqrtV_G[:, np.newaxis]) 

274 if self.q0_corrector is not None and chi0.optical_limit: 

275 W = dfc.wblocks.a + iw 

276 self.q0_corrector.add_q0_correction(chi0.qpd, W_GG, 

277 einv_GG, 

278 chi0.chi0_WxvG[W], 

279 chi0.chi0_Wvv[W], 

280 sqrtV_G) 

281 elif (self.integrate_gamma.is_analytical and chi0.optical_limit) \ 

282 or self.integrate_gamma.is_numerical: 

283 self.apply_gamma_correction(W_GG, einvt_GG, 

284 V0, sqrtV0, dfc.sqrtV_G) 

285 

286 self.context.timer.stop('Dyson eq.') 

287 return W_wGG 

288 

289 def dyson_and_W_new(self, iq, q_c, chi0, ecut, coulomb): 

290 # assert not self.do_GW_too 

291 assert ecut == chi0.qpd.ecut 

292 assert self.fxc_mode == 'GW' 

293 assert not np.allclose(q_c, 0) 

294 

295 nW = len(self.wd) 

296 nG = chi0.qpd.ngmax 

297 

298 from gpaw.response.wgg import Grid 

299 

300 WGG = (nW, nG, nG) 

301 WgG_grid = Grid( 

302 comm=self.blockcomm, 

303 shape=WGG, 

304 cpugrid=(1, self.blockcomm.size, 1)) 

305 assert chi0.chi0_wGG.shape == WgG_grid.myshape 

306 

307 my_gslice = WgG_grid.myslice[1] 

308 

309 dielectric_WgG = chi0.chi0_wGG # XXX 

310 for iw, chi0_GG in enumerate(chi0.chi0_wGG): 

311 sqrtV_G = coulomb.sqrtV(chi0.qpd, q_v=None) 

312 e_GG = np.eye(nG) - chi0_GG * sqrtV_G * sqrtV_G[:, np.newaxis] 

313 e_gG = e_GG[my_gslice] 

314 

315 dielectric_WgG[iw, :, :] = e_gG 

316 

317 wgg_grid = Grid(comm=self.blockcomm, shape=WGG) 

318 

319 dielectric_wgg = wgg_grid.zeros(dtype=complex) 

320 WgG_grid.redistribute(wgg_grid, dielectric_WgG, dielectric_wgg) 

321 

322 assert np.allclose(dielectric_wgg, dielectric_WgG) 

323 

324 wgg_grid.invert_inplace(dielectric_wgg) 

325 

326 wgg_grid.redistribute(WgG_grid, dielectric_wgg, dielectric_WgG) 

327 inveps_WgG = dielectric_WgG 

328 

329 self.context.timer.start('Dyson eq.') 

330 

331 for iw, inveps_gG in enumerate(inveps_WgG): 

332 inveps_gG -= np.identity(nG)[my_gslice] 

333 thing_GG = sqrtV_G * sqrtV_G[:, np.newaxis] 

334 inveps_gG *= thing_GG[my_gslice] 

335 

336 W_WgG = inveps_WgG 

337 Wp_wGG = W_WgG.copy() 

338 Wm_wGG = W_WgG.copy() 

339 return chi0.qpd, Wm_wGG, Wp_wGG # not Hilbert transformed yet 

340 

341 

342class HWModel: 

343 """ 

344 Hilbert Transformed W Model. 

345 """ 

346 

347 def get_HW(self, omega, occ): 

348 """ 

349 Get Hilbert transformed W at frequency omega. 

350 

351 occ: The occupation number for the orbital of the Greens function. 

352 """ 

353 raise NotImplementedError 

354 

355 

356class FullFrequencyHWModel(HWModel): 

357 def __init__(self, wd, HW_swGG, factor): 

358 self.wd = wd 

359 self.HW_swGG = HW_swGG 

360 self.factor = factor 

361 

362 def get_HW(self, omega, occ): 

363 # For more information about how fsign and wsign works, see 

364 # https://backend.orbit.dtu.dk/ws/portalfiles/portal/93075765/hueser_PhDthesis.pdf 

365 # eq. 2.2 endind up to eq. 2.11 

366 # Effectively, the symmetry of time ordered W is used, 

367 # i.e. W(w) = -W(-w). To allow that data is only stored for w>=0. 

368 # Hence, the interpolation happends always to the positive side, but 

369 # the information of true w is keps tract using wsign. 

370 # In addition, whether the orbital in question at G is occupied or 

371 # unoccupied, which then again affects, which Hilbert transform of 

372 # W is chosen, is kept track with fsign. 

373 fsign = np.sign(2 * occ - 1) 

374 o = abs(omega) 

375 wsign = np.sign(omega + 1e-15) 

376 wd = self.wd 

377 # Pick +i*eta or -i*eta: 

378 s = (1 + wsign * np.sign(-fsign)).astype(int) // 2 

379 w = wd.get_floor_index(o, safe=False) 

380 

381 # Interpolation indexes w + 1, therefore - 2 here 

382 if w > len(wd) - 2: 

383 return None, None 

384 

385 o1 = wd.omega_w[w] 

386 o2 = wd.omega_w[w + 1] 

387 

388 C1_GG = self.HW_swGG[s][w] 

389 C2_GG = self.HW_swGG[s][w + 1] 

390 p = self.factor * wsign 

391 

392 sigma_GG = ((o - o1) * C2_GG + (o2 - o) * C1_GG) / (o2 - o1) 

393 dsigma_GG = wsign * (C2_GG - C1_GG) / (o2 - o1) 

394 return -1j * p * sigma_GG, -1j * p * dsigma_GG 

395 

396 

397class MPAHWModel(HWModel): 

398 def __init__(self, W_nGG, omegat_nGG, eta, factor): 

399 self.W_nGG = np.ascontiguousarray(W_nGG) 

400 self.omegat_nGG = np.ascontiguousarray(omegat_nGG) 

401 self.eta = eta 

402 self.factor = factor 

403 

404 def get_HW(self, omega, occ): 

405 x_GG = np.empty(self.omegat_nGG.shape[1:], dtype=complex) 

406 dx_GG = np.empty(self.omegat_nGG.shape[1:], dtype=complex) 

407 evaluate_mpa_poly(x_GG, dx_GG, omega, occ, self.omegat_nGG, self.W_nGG, 

408 self.eta, self.factor) 

409 

410 return x_GG.conj(), dx_GG.conj() # Why do we have to do a conjugate 

411 

412 

413class MPACalculator(WBaseCalculator): 

414 def __init__(self, gs, context, *, eta, mpa, **kwargs): 

415 super().__init__(gs, context, **kwargs) 

416 self.eta = eta 

417 self.mpa = mpa 

418 

419 def get_HW_model(self, chi0, 

420 fxc_mode='GW'): 

421 """Calculate the MPA parametrization of screened interaction. 

422 """ 

423 

424 dfc = DielectricFunctionCalculator(chi0, 

425 self.coulomb, 

426 self.xckernel, 

427 fxc_mode) 

428 

429 self.context.timer.start('Dyson eq.') 

430 einv_wGG = dfc.get_epsinv_wGG(only_correlation=True) 

431 einv_WgG = chi0.body.blockdist.distribute_as(einv_wGG, chi0.nw, 'WgG') 

432 

433 solver = RESolver(chi0.wd.omega_w) 

434 E_pGG, R_pGG = solver.solve(einv_WgG) 

435 E_pGG -= 1j * self.eta # DALV: This is just to match the FF results 

436 

437 R_pGG = chi0.body.blockdist.distribute_as(R_pGG, self.mpa['npoles'], 

438 'wGG') 

439 E_pGG = chi0.body.blockdist.distribute_as(E_pGG, 

440 self.mpa['npoles'], 'wGG') 

441 

442 if self.integrate_gamma.is_Wigner_Seitz: 

443 from gpaw.hybrids.wstc import WignerSeitzTruncatedCoulomb 

444 wstc = WignerSeitzTruncatedCoulomb(chi0.qpd.gd.cell_cv, 

445 dfc.coulomb.N_c) 

446 sqrtV_G = wstc.get_potential(chi0.qpd)**0.5 

447 else: 

448 sqrtV_G = dfc.sqrtV_G 

449 

450 W_pGG = pi * R_pGG * sqrtV_G[np.newaxis, :, np.newaxis] \ 

451 * sqrtV_G[np.newaxis, np.newaxis, :] 

452 

453 assert self.q0_corrector is None 

454 if (self.integrate_gamma.is_analytical and chi0.optical_limit)\ 

455 or self.integrate_gamma.is_numerical: 

456 V0, sqrtV0 = self.get_V0sqrtV0(chi0) 

457 for W_GG, R_GG in zip(W_pGG, R_pGG): 

458 self.apply_gamma_correction(W_GG, pi * R_GG, 

459 V0, sqrtV0, 

460 dfc.sqrtV_G) 

461 

462 W_pGG = np.transpose(W_pGG, axes=(0, 2, 1)) # Why the transpose 

463 E_pGG = np.transpose(E_pGG, axes=(0, 2, 1)) 

464 

465 W_pGG = chi0.body.blockdist.distribute_as(W_pGG, self.mpa['npoles'], 

466 'WgG') 

467 E_pGG = chi0.body.blockdist.distribute_as(E_pGG, 

468 self.mpa['npoles'], 'WgG') 

469 

470 self.context.timer.stop('Dyson eq.') 

471 

472 factor = 1.0 / (self.qd.nbzkpts * 2 * pi * self.gs.volume) 

473 return MPAHWModel(W_pGG, E_pGG, self.eta, factor)