Coverage for gpaw/xc/rpa.py: 98%

267 statements  

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

1from __future__ import annotations 

2from dataclasses import dataclass 

3import functools 

4from time import ctime 

5 

6import numpy as np 

7from ase.units import Hartree 

8from scipy.special import p_roots 

9 

10import gpaw.mpi as mpi 

11from gpaw.response import timer 

12from gpaw.response.chi0 import Chi0Calculator 

13from gpaw.response.coulomb_kernels import CoulombKernel 

14from gpaw.response.frequencies import FrequencyDescriptor 

15from gpaw.response.pair import get_gs_and_context 

16 

17 

18def default_ecut_extrapolation(ecut, extrapolate): 

19 return ecut * (1 + 0.5 * np.arange(extrapolate))**(-2 / 3) 

20 

21 

22class GCut: 

23 def __init__(self, cut_G): 

24 self._cut_G = cut_G 

25 

26 @property 

27 def nG(self): 

28 return len(self._cut_G) 

29 

30 def spin_cut(self, array_GG, ns): 

31 # Strange special case for spin-repeated arrays. 

32 # Maybe we can get rid of this. 

33 if self._cut_G is None: 

34 return array_GG 

35 

36 cut_sG = np.tile(self._cut_G, ns) 

37 cut_sG[self.nG:] += len(array_GG) // ns 

38 array_GG = array_GG.take(cut_sG, 0).take(cut_sG, 1) 

39 return array_GG 

40 

41 def cut(self, array, axes=(0,)): 

42 if self._cut_G is None: 

43 return array 

44 

45 for axis in axes: 

46 array = array.take(self._cut_G, axis) 

47 return array 

48 

49 

50def initialize_q_points(kd, qsym): 

51 bzq_qc = kd.get_bz_q_points(first=True) 

52 

53 if not qsym: 

54 ibzq_qc = bzq_qc 

55 weight_q = np.ones(len(bzq_qc)) / len(bzq_qc) 

56 else: 

57 U_scc = kd.symmetry.op_scc 

58 ibzq_qc = kd.get_ibz_q_points(bzq_qc, U_scc)[0] 

59 weight_q = kd.q_weights 

60 return ibzq_qc, weight_q 

61 

62 

63@dataclass 

64class RPAIntegral: 

65 omega_w: np.ndarray 

66 weight_w: np.ndarray 

67 ibzq_qc: np.ndarray 

68 weight_q: np.ndarray 

69 ecut_i: np.ndarray 

70 

71 @property 

72 def nq(self): 

73 """Number of q-points.""" 

74 return len(self.weight_q) 

75 

76 @property 

77 def nw(self): 

78 """Number of frequencies.""" 

79 nw = len(self.omega_w) 

80 assert len(self.weight_w) == nw 

81 return nw 

82 

83 @property 

84 def ni(self): 

85 """Number of plane-wave cutoffs to extrapolate over.""" 

86 return len(self.ecut_i) 

87 

88 def integrate_frequencies(self, in_xwi): 

89 out_xi = self.weight_w @ in_xwi 

90 return out_xi 

91 

92 def integrate_qpoints(self, in_xqi): 

93 out_xi = self.weight_q @ in_xqi 

94 return out_xi 

95 

96 

97@dataclass 

98class RPAData: 

99 integral: RPAIntegral 

100 

101 def __post_init__(self): 

102 self.energy_qwi = np.zeros(self.shape) 

103 

104 @property 

105 def shape(self): 

106 return (self.integral.nq, self.integral.nw, self.integral.ni) 

107 

108 def contribution_from_qpoint(self, q, i=-1): 

109 return self.integral.integrate_frequencies(self.energy_qwi[q, :, i]) 

110 

111 @property 

112 def energy_wqi(self): 

113 return np.swapaxes(self.energy_qwi, 0, 1) 

114 

115 @property 

116 def energy_qi(self): 

117 """Correlation energy contribution, E_c(q).""" 

118 return self.integral.integrate_frequencies(self.energy_qwi) 

119 

120 @property 

121 def energy_wi(self): 

122 """Correlation energy contribution, E_c(ω).""" 

123 return self.integral.integrate_qpoints(self.energy_wqi) 

124 

125 @property 

126 def energy_i(self): 

127 """Correlation energy E_c as a function of the plane-wave cutoff.""" 

128 return self.integral.integrate_qpoints(self.energy_qi) 

129 

130 

131class RPACalculator: 

132 def __init__( 

133 self, 

134 gs, 

135 *, 

136 context, 

137 ecut, 

138 frequencies, 

139 weights, 

140 qsym=True, 

141 skip_gamma=False, 

142 truncation=None, 

143 nblocks=1, 

144 calculate_q=None 

145 ): 

146 self.gs = gs 

147 self.context = context 

148 

149 # Normalize RPA integral inputs 

150 if isinstance(ecut, (float, int)): 

151 ecut = default_ecut_extrapolation(ecut, extrapolate=6) 

152 ecut_i = np.asarray(np.sort(ecut)) / Hartree 

153 # TODO: We should avoid this requirement. 

154 # thosk notes: it might work now (after some extensive clean-up of the 

155 # frequency integration), but I have not tested it 

156 assert len(frequencies) % nblocks == 0 

157 # We should actually have a kpoint descriptor for the qpoints. 

158 # We are badly failing at making use of the existing tools by reducing 

159 # the qpoints to dumb arrays. 

160 ibzq_qc, weight_q = initialize_q_points(gs.kd, qsym) 

161 # Collect information about the RPA integral on a single object (a.u.) 

162 self.integral = RPAIntegral( 

163 omega_w=frequencies / Hartree, 

164 weight_w=weights / Hartree / (2 * np.pi), 

165 ibzq_qc=ibzq_qc, 

166 weight_q=weight_q, 

167 ecut_i=ecut_i, 

168 ) 

169 

170 self.chi0calc = Chi0Calculator( 

171 self.gs, self.context.with_txt('chi0.txt'), 

172 nblocks=nblocks, 

173 wd=FrequencyDescriptor(1j * self.integral.omega_w), 

174 eta=0.0, 

175 intraband=False, 

176 hilbert=False, 

177 ecut=max(self.integral.ecut_i) * Hartree) 

178 self.coulomb = CoulombKernel.from_gs(gs, truncation=truncation) 

179 

180 self.skip_gamma = skip_gamma 

181 # This is a super weird way of achieving inheritance... 

182 if calculate_q is None: 

183 calculate_q = self.calculate_q_rpa 

184 self.calculate_q = calculate_q 

185 

186 def calculate(self, *, nbands=None) -> np.ndarray: 

187 """Calculate the RPA correlation energy as a function of cutoff.""" 

188 data = self.calculate_all_contributions(nbands=nbands) 

189 return data.energy_i * Hartree # energies in eV 

190 

191 def calculate_all_contributions( 

192 self, *, nbands=None, spin=False) -> RPAData: 

193 """Calculate RPA correlation energy contributions. 

194 

195 nbands: int 

196 Number of bands (defaults to number of plane-waves). 

197 spin: bool 

198 Separate spin in response function. 

199 (Only needed for beyond RPA methods that inherit this function). 

200 """ 

201 

202 p = functools.partial(self.context.print, flush=False) 

203 

204 if nbands is None: 

205 p('Response function bands : Equal to number of plane waves') 

206 else: 

207 p('Response function bands : %s' % nbands) 

208 p('Plane wave cutoffs (eV) :', end='') 

209 for e in self.integral.ecut_i: 

210 p(f' {e * Hartree:.3f}', end='') 

211 p() 

212 p(self.coulomb.description()) 

213 p('', flush=True) 

214 

215 self.context.timer.start('RPA') 

216 

217 data = RPAData(self.integral) 

218 ecutmax = max(self.integral.ecut_i) 

219 for q, q_c in enumerate(self.integral.ibzq_qc): 

220 if np.allclose(q_c, 0.0) and self.skip_gamma: 

221 p('Not calculating E_c(q) at Gamma', end='\n') 

222 continue 

223 

224 chi0_s = [self.chi0calc.create_chi0(q_c)] 

225 if spin: 

226 chi0_s.append(self.chi0calc.create_chi0(q_c)) 

227 

228 qpd = chi0_s[0].qpd 

229 nG = qpd.ngmax 

230 

231 # First not completely filled band: 

232 m1 = self.gs.nocc1 

233 p(f'# {q} - {ctime().split()[-2]}') 

234 p('q = [%1.3f %1.3f %1.3f]' % tuple(q_c)) 

235 

236 for i, ecut in enumerate(self.integral.ecut_i): 

237 if ecut == ecutmax: 

238 # Nothing to cut away: 

239 gcut = GCut(None) 

240 m2 = nbands or nG 

241 else: 

242 gcut = GCut(np.arange(nG)[qpd.G2_qG[0] <= 2 * ecut]) 

243 m2 = gcut.nG 

244 

245 p('E_cut = %d eV / Bands = %d:' % (ecut * Hartree, m2), 

246 end='\n', flush=True) 

247 p('E_c(q) = ', end='', flush=False) 

248 data.energy_qwi[q, :, i] = self.calculate_q( 

249 chi0_s, m1, m2, gcut 

250 ) 

251 energy = data.contribution_from_qpoint(q, i=i) 

252 p('%.3f eV' % (energy * Hartree), flush=True) 

253 m1 = m2 

254 

255 p() 

256 

257 e_i = data.energy_i 

258 p('==========================================================') 

259 p() 

260 p('Total correlation energy:') 

261 for e_cut, e in zip(self.integral.ecut_i, e_i): 

262 p(f'{e_cut * Hartree:6.0f}: {e * Hartree:6.4f} eV') 

263 p() 

264 

265 if len(e_i) > 1: 

266 self.extrapolate(e_i, self.integral.ecut_i) 

267 

268 p('Calculation completed at: ', ctime()) 

269 p() 

270 

271 self.context.timer.stop('RPA') 

272 self.context.write_timer() 

273 

274 return data 

275 

276 @timer('chi0(q)') 

277 def calculate_q_rpa(self, chi0_s, m1, m2, gcut): 

278 chi0 = chi0_s[0] 

279 self.chi0calc.update_chi0( 

280 chi0, m1=m1, m2=m2, spins=range(self.chi0calc.gs.nspins)) 

281 qpd = chi0.qpd 

282 chi0_wGG = chi0.body.get_distributed_frequencies_array() 

283 wblocks = chi0.body.get_distributed_frequencies_blocks1d() 

284 # Calculate RPA energy contributions (as a function of w) 

285 if chi0.qpd.optical_limit: 

286 chi0_wvv = chi0.chi0_Wvv[wblocks.myslice] 

287 chi0_wxvG = chi0.chi0_WxvG[wblocks.myslice] 

288 energy_w = self.calculate_optical_limit_rpa_energies( 

289 qpd, chi0_wGG, chi0_wvv, chi0_wxvG, gcut 

290 ) 

291 else: 

292 energy_w = self.calculate_rpa_energies(qpd, chi0_wGG, gcut) 

293 return wblocks.all_gather(energy_w) 

294 

295 def calculate_optical_limit_rpa_energies( 

296 self, qpd, chi0_wGG, chi0_wvv, chi0_wxvG, gcut): 

297 """Calculate correlation energy from chi0 in the optical limit.""" 

298 from gpaw.response.gamma_int import GammaIntegral 

299 

300 gamma_int = GammaIntegral(self.coulomb, qpd=qpd) 

301 

302 energy_w = [] 

303 for chi0_GG, chi0_vv, chi0_xvG in zip(chi0_wGG, chi0_wvv, chi0_wxvG): 

304 # Integrate over the optical wave vector volume 

305 energy = 0. 

306 for qweight, sqrtV_G, chi0_mapping in gamma_int: 

307 chi0p_GG = chi0_mapping(chi0_GG, chi0_vv, chi0_xvG) 

308 energy += qweight * single_rpa_energy( 

309 chi0p_GG, gcut.cut(sqrtV_G), gcut) 

310 energy_w.append(energy) 

311 return np.array(energy_w) 

312 

313 def calculate_rpa_energies(self, qpd, chi0_wGG, gcut): 

314 """Evaluate correlation energy from chi0 at finite q.""" 

315 sqrtV_G = gcut.cut(self.coulomb.sqrtV(qpd, q_v=None)) 

316 return np.array([ 

317 single_rpa_energy(chi0_GG, sqrtV_G, gcut) for chi0_GG in chi0_wGG 

318 ]) 

319 

320 def extrapolate(self, e_i, ecut_i): 

321 self.context.print('Extrapolated energies:', flush=False) 

322 ex_i = [] 

323 for i in range(len(e_i) - 1): 

324 e1, e2 = e_i[i:i + 2] 

325 x1, x2 = ecut_i[i:i + 2]**-1.5 

326 ex = (e1 * x2 - e2 * x1) / (x2 - x1) 

327 ex_i.append(ex) 

328 

329 self.context.print(' %4.0f -%4.0f: %5.3f eV' % 

330 (ecut_i[i] * Hartree, ecut_i[i + 1] 

331 * Hartree, ex * Hartree), flush=False) 

332 self.context.print('') 

333 

334 return e_i * Hartree 

335 

336 

337def single_rpa_energy(chi0_GG, sqrtV_G, gcut): 

338 nG = len(sqrtV_G) 

339 chi0_GG = gcut.cut(chi0_GG, [0, 1]) 

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

341 e = np.log(np.linalg.det(e_GG)) + nG - np.trace(e_GG) 

342 return e.real 

343 

344 

345def get_gauss_legendre_points(nw=16, frequency_max=800.0, frequency_scale=2.0): 

346 y_w, weights_w = p_roots(nw) 

347 y_w = y_w.real 

348 ys = 0.5 - 0.5 * y_w 

349 ys = ys[::-1] 

350 w = (-np.log(1 - ys))**frequency_scale 

351 w *= frequency_max / w[-1] 

352 alpha = (-np.log(1 - ys[-1]))**frequency_scale / frequency_max 

353 transform = (-np.log(1 - ys))**(frequency_scale - 1) \ 

354 / (1 - ys) * frequency_scale / alpha 

355 return w, weights_w * transform / 2 

356 

357 

358class RPACorrelation(RPACalculator): 

359 def __init__(self, calc, xc='RPA', 

360 nlambda=None, 

361 nfrequencies=16, frequency_max=800.0, frequency_scale=2.0, 

362 frequencies=None, weights=None, 

363 world=mpi.world, 

364 txt='-', 

365 truncation: str | None = None, 

366 **kwargs): 

367 """Creates the RPACorrelation object 

368 

369 calc: str or calculator object 

370 The string should refer to the .gpw file contaning KS orbitals 

371 xc: str 

372 Exchange-correlation kernel. This is only different from RPA when 

373 this object is constructed from a different module - e.g. fxc.py 

374 skip_gamma: bool 

375 If True, skip q = [0,0,0] from the calculation 

376 qsym: bool 

377 Use symmetry to reduce q-points 

378 nlambda: int 

379 Number of lambda points. Only used for numerical coupling 

380 constant integration involved when called from fxc.py 

381 nfrequencies: int 

382 Number of frequency points used in the Gauss-Legendre integration 

383 frequency_max: float 

384 Largest frequency point in Gauss-Legendre integration 

385 frequency_scale: float 

386 Determines density of frequency points at low frequencies. A slight 

387 increase to e.g. 2.5 or 3.0 improves convergence wth respect to 

388 frequency points for metals 

389 frequencies: list 

390 List of frequencies for user-specified frequency integration 

391 weights: list 

392 list of weights (integration measure) for a user specified 

393 frequency grid. Must be specified and have the same length as 

394 frequencies if frequencies is not None 

395 truncation: str or None 

396 Coulomb truncation scheme. Can be None, '0D' or '2D'. If None 

397 and the system is a molecule then '0D' will be used. 

398 world: communicator 

399 nblocks: int 

400 Number of parallelization blocks. Frequency parallelization 

401 can be specified by setting nblocks=nfrequencies and is useful 

402 for memory consuming calculations 

403 ecut: float or list of floats 

404 Plane-wave cutoff(s) in eV. 

405 txt: str 

406 txt file for saving and loading contributions to the correlation 

407 energy from different q-points 

408 """ 

409 gs, context = get_gs_and_context(calc=calc, txt=txt, world=world, 

410 timer=None) 

411 

412 if frequencies is None: 

413 frequencies, weights = get_gauss_legendre_points(nfrequencies, 

414 frequency_max, 

415 frequency_scale) 

416 user_spec = False 

417 else: 

418 assert weights is not None 

419 user_spec = True 

420 

421 if truncation is None and not gs.pbc.any(): 

422 truncation = '0D' 

423 

424 super().__init__(gs=gs, context=context, 

425 frequencies=frequencies, weights=weights, 

426 truncation=truncation, 

427 **kwargs) 

428 

429 self.print_initialization(xc, frequency_scale, nlambda, user_spec) 

430 

431 def print_initialization(self, xc, frequency_scale, nlambda, user_spec): 

432 p = functools.partial(self.context.print, flush=False) 

433 p('----------------------------------------------------------') 

434 p('Non-self-consistent %s correlation energy' % xc) 

435 p('----------------------------------------------------------') 

436 p('Started at: ', ctime()) 

437 p() 

438 p('Atoms :', 

439 self.gs.atoms.get_chemical_formula(mode='hill')) 

440 p('Ground state XC functional :', self.gs.xcname) 

441 p('Valence electrons :', self.gs.nvalence) 

442 p('Number of bands :', self.gs.bd.nbands) 

443 p('Number of spins :', self.gs.nspins) 

444 p('Number of k-points :', len(self.gs.kd.bzk_kc)) 

445 p('Number of irreducible k-points :', len(self.gs.kd.ibzk_kc)) 

446 p('Number of irreducible q-points :', len(self.integral.ibzq_qc)) 

447 p() 

448 for q, weight in zip(self.integral.ibzq_qc, self.integral.weight_q): 

449 p(' q: [%1.4f %1.4f %1.4f] - weight: %1.3f' % 

450 (q[0], q[1], q[2], weight)) 

451 p() 

452 p('----------------------------------------------------------') 

453 p('----------------------------------------------------------') 

454 p() 

455 if nlambda is None: 

456 p('Analytical coupling constant integration') 

457 else: 

458 p('Numerical coupling constant integration using', nlambda, 

459 'Gauss-Legendre points') 

460 p() 

461 p('Frequencies') 

462 if not user_spec: 

463 p(' Gauss-Legendre integration with %s frequency points' % 

464 len(self.integral.omega_w)) 

465 p(' Transformed from [0,oo] to [0,1] using e^[-aw^(1/B)]') 

466 p(' Highest frequency point at %5.1f eV and B=%1.1f' % 

467 (self.integral.omega_w[-1] * Hartree, frequency_scale)) 

468 else: 

469 p(' User specified frequency integration with', 

470 len(self.integral.omega_w), 'frequency points') 

471 p() 

472 p('Parallelization') 

473 p(' Total number of CPUs : % s' % self.context.comm.size) 

474 blockcomm = self.chi0calc.chi0_body_calc.blockcomm 

475 p(' G-vector decomposition : % s' % blockcomm.size) 

476 kncomm = self.chi0calc.chi0_body_calc.kncomm 

477 p(' K-point/band decomposition : % s' % kncomm.size) 

478 self.context.print('')