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

114 statements  

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

1import numpy as np 

2from abc import ABC, abstractmethod 

3 

4from gpaw.utilities.progressbar import ProgressBar 

5from gpaw.typing import Vector 

6 

7from gpaw.response import ResponseGroundStateAdapter, ResponseContext, timer 

8from gpaw.response.frequencies import ComplexFrequencyDescriptor 

9from gpaw.response.symmetry import QSymmetryAnalyzer 

10from gpaw.response.kspair import (KohnShamKPointPair, 

11 KohnShamKPointPairExtractor) 

12from gpaw.response.pw_parallelization import block_partition 

13from gpaw.response.pair_transitions import PairTransitions 

14 

15 

16class PairFunction(ABC): 

17 r"""Pair function data object. 

18 

19 In the GPAW response module, a pair function is understood as any function 

20 which can be written as a sum over the eigenstate transitions with a given 

21 crystal momentum difference q, 

22 __ 

23 \ 

24 pf(q) = / pf_αα' δ_{q,q_{α',α}} 

25 ‾‾ 

26 α,α' 

27 

28 where pf_αα' is an arbitrary matrix element. 

29 """ 

30 

31 def __init__(self, q_c: Vector): 

32 self.q_c = np.asarray(q_c) 

33 self.array = self.zeros() 

34 

35 @abstractmethod 

36 def zeros(self): 

37 """Generate an array of zeros, representing the pair function.""" 

38 

39 

40class DynamicPairFunction(PairFunction): 

41 r"""Dynamic pair function data object. 

42 

43 A dynamic pair function is not only a function of the crystal momentum q, 

44 but also of the complex frequency z = ω + iη: 

45 __ 

46 \ 

47 pf(q,z) = / pf_αα'(z) δ_{q,q_{α',α}} 

48 ‾‾ 

49 α,α' 

50 

51 Typically, this will be some generalized (linear) susceptibility, which is 

52 defined by the Kubo formula, 

53 

54 i ˰ ˰ 

55 χ_BA(t-t') = - ‾ θ(t-t') <[B_0(t-t'), A]>_0 

56 ħ 

57 

58 and can be written in its Lehmann representation as a function of frequency 

59 in the upper half complex frequency plane, 

60 

61 __ ˰ ˰ 

62 \ <α|B|α'><α'|A|α> 

63 χ_BA(z) = / ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ (n_α - n_α') 

64 ‾‾ ħz - (E_α' - E_α) 

65 α,α' 

66 

67 where E_α and n_α are the eigenstate energies and occupations respectively. 

68 

69 For more information, please refer to [Skovhus T., PhD. Thesis, 2021].""" 

70 

71 def __init__(self, q_c: Vector, zd: ComplexFrequencyDescriptor): 

72 self.zd = zd 

73 super().__init__(q_c) 

74 

75 

76class PairFunctionIntegrator(ABC): 

77 r"""Baseclass for computing pair functions in the Kohn-Sham system. 

78 

79 The implementation is currently restricted collinear periodic crystals in 

80 absence of spin-orbit coupling. 

81 

82 In the Kohn-Sham system, pair functions can be constructed 

83 straight-forwardly as a sum over transitions between Kohn-Sham eigenstates 

84 at k and k + q, 

85 __ __ __ __ 

86 1 \ \ \ 1 \ 

87 pf(q) = ‾ / / / pf_nks,n'k+qs'(q) = ‾ / pf_T(q) 

88 V ‾‾ ‾‾ ‾‾ V ‾‾ 

89 k n,n' s,s' T 

90 

91 where V is the crystal volume and T is a composite index encoding all 

92 relevant transitions: 

93 

94 T: (n, k, s) -> (n', k + q, s') 

95 

96 The sum over transitions can be split into two steps: (1) an integral over 

97 k-points k inside the 1st Brillouin Zone and (2) a sum over band and spin 

98 transitions t: 

99 

100 t (composite transition index): (n, s) -> (n', s') 

101 __ __ __ __ 

102 1 \ 1 \ \ 1 \ 

103 pf(q) = ‾ / pf_T(q) = ‾ / / pf_kt(q) = ‾ / (...)_k 

104 V ‾‾ V ‾‾ ‾‾ V ‾‾ 

105 T k t k 

106 

107 In the code, the k-point integral is handled by a KPointPairIntegral 

108 object, while the sum over band and spin transitions t is carried out in 

109 the self.add_integrand() method, which also defines the specific pair 

110 function in question. 

111 

112 KPointPairIntegral: 

113 __ 

114 1 \ 

115 ‾ / (...)_k 

116 V ‾‾ 

117 k 

118 

119 self.add_integrand(): 

120 __ __ __ 

121 \ \ \ 

122 (...)_k = / pf_kt(q) = / / pf_nks,n'k+qs'(q) 

123 ‾‾ ‾‾ ‾‾ 

124 t n,n' s,s' 

125 

126 In practise, the integration is carried out by letting the 

127 KPointPairIntegral extract individual KohnShamKPointPair objects, which 

128 contain all relevant information about the Kohn-Sham eigenstates at k and 

129 k + q for a number of specified spin and band transitions t. 

130 KPointPairIntegral.weighted_kpoint_pairs() generates these kptpairs along 

131 with their integral weights such that self._integrate() can construct the 

132 pair functions in a flexible, yet general manner. 

133 """ 

134 

135 def __init__(self, gs, context, qsymmetry=True, nblocks=1): 

136 """Construct the PairFunctionIntegrator 

137 

138 Parameters 

139 ---------- 

140 gs : ResponseGroundStateAdaptable 

141 context : ResponseContextInput 

142 qsymmetry : QSymmetryInput 

143 nblocks : int 

144 Distribute the pair function into nblocks. Useful when the pair 

145 function itself becomes a large array (read: memory limiting). 

146 """ 

147 self.gs = ResponseGroundStateAdapter.from_input(gs) 

148 self.context = ResponseContext.from_input(context) 

149 self.qsymmetry = QSymmetryAnalyzer.from_input(qsymmetry) 

150 

151 # Communicators for distribution of memory and work 

152 (self.blockcomm, 

153 self.intrablockcomm) = self.create_communicators(nblocks) 

154 self.nblocks = self.blockcomm.size 

155 

156 # The KohnShamKPointPairExtractor class handles extraction of k-point 

157 # pairs from the ground state 

158 self.kptpair_extractor = KohnShamKPointPairExtractor( 

159 self.gs, self.context, 

160 # Distribution of work: 

161 # t-transitions are distributed through blockcomm, 

162 # k-points through intrablockcomm. 

163 transitions_blockcomm=self.blockcomm, 

164 kpts_blockcomm=self.intrablockcomm) 

165 

166 @timer('Integrate pair function') 

167 def _integrate(self, out: PairFunction, transitions: PairTransitions): 

168 """In-place pair function integration 

169 

170 Parameters 

171 ---------- 

172 out : PairFunction 

173 Output data structure 

174 transitions : PairTransitions 

175 Band and spin transitions to integrate. 

176 

177 Returns 

178 ------- 

179 symmetries : QSymmetries 

180 """ 

181 symmetries, generator = self.qsymmetry.analyze( 

182 out.q_c, self.gs.kpoints, self.context) 

183 

184 # Perform the actual integral as a point integral over k-point pairs 

185 integral = KPointPairPointIntegral(self.kptpair_extractor, generator) 

186 weighted_kptpairs = integral.weighted_kpoint_pairs(transitions) 

187 pb = ProgressBar(self.context.fd) # pb with a generator is awkward 

188 for _, _ in pb.enumerate([None] * integral.ni): 

189 kptpair, weight = next(weighted_kptpairs) 

190 if weight is not None: 

191 assert kptpair is not None 

192 self.add_integrand(kptpair, weight, out) 

193 

194 # Sum over the k-points, which have been distributed between processes 

195 with self.context.timer('Sum over distributed k-points'): 

196 self.intrablockcomm.sum(out.array) 

197 

198 return symmetries 

199 

200 @abstractmethod 

201 def add_integrand(self, kptpair: KohnShamKPointPair, weight, 

202 out: PairFunction): 

203 """Add the relevant integrand of the outer k-point integral to the 

204 output data structure 'out', weighted by 'weight' and constructed 

205 from the provided KohnShamKPointPair 'kptpair'. 

206 

207 This method effectively defines the pair function in question. 

208 """ 

209 

210 def create_communicators(self, nblocks): 

211 """Create MPI communicators to distribute the memory needed to store 

212 large arrays and parallelize calculations when possible. 

213 

214 Parameters 

215 ---------- 

216 nblocks : int 

217 Separate large arrays into n different blocks. Each process 

218 allocates memory for the large arrays. By allocating only a 

219 fraction/block of the total arrays, the memory requirements are 

220 eased. 

221 

222 Returns 

223 ------- 

224 blockcomm : gpaw.mpi.Communicator 

225 Communicate between processes belonging to different memory blocks. 

226 In every communicator, there is one process for each block of 

227 memory, so that all blocks are represented. 

228 If nblocks < comm.size, there will be size // nblocks different 

229 processes that allocate memory for the same block of the large 

230 arrays. Thus, there will be also size // nblocks different block 

231 communicators, grouping the processes into sets that allocate the 

232 entire arrays between them. 

233 intrablockcomm : gpaw.mpi.Communicator 

234 Communicate between processes belonging to the same memory block. 

235 There will be size // nblocks processes per memory block. 

236 """ 

237 comm = self.context.comm 

238 blockcomm, intrablockcomm = block_partition(comm, nblocks) 

239 

240 return blockcomm, intrablockcomm 

241 

242 def get_band_and_spin_transitions(self, spincomponent, nbands=None, 

243 bandsummation='pairwise'): 

244 """Get band and spin transitions (n, s) -> (n', s') to integrate.""" 

245 # Defaults to inclusion of all bands in the ground state calculator 

246 if nbands is None: 

247 nbands = self.gs.nbands 

248 assert nbands <= self.gs.nbands 

249 return PairTransitions.from_transitions_domain_arguments( 

250 spincomponent, nbands, self.gs.nocc1, self.gs.nocc2, 

251 self.gs.nspins, bandsummation) 

252 

253 def get_basic_info_string(self): 

254 """Get basic information about the ground state and parallelization.""" 

255 nspins = self.gs.nspins 

256 nbands, nocc1, nocc2 = self.gs.nbands, self.gs.nocc1, self.gs.nocc2 

257 nk = self.gs.kd.nbzkpts 

258 nik = self.gs.kd.nibzkpts 

259 

260 csize = self.context.comm.size 

261 knsize = self.intrablockcomm.size 

262 bsize = self.blockcomm.size 

263 

264 isl = ['', 

265 'The pair function integration is based on a ground state ' 

266 'with:', 

267 f' Number of spins: {nspins}', 

268 f' Number of bands: {nbands}', 

269 f' Number of completely occupied bands: {nocc1}', 

270 f' Number of partially occupied bands: {nocc2}', 

271 f' Number of kpoints: {nk}', 

272 f' Number of irreducible kpoints: {nik}', 

273 '', 

274 'The pair function integration is performed in parallel with:', 

275 f' comm.size: {csize}', 

276 f' intrablockcomm.size: {knsize}', 

277 f' blockcomm.size: {bsize}'] 

278 

279 return '\n'.join(isl) 

280 

281 @staticmethod 

282 def get_band_and_transitions_info_string(nbands, nt): 

283 isl = [] # info string list 

284 if nbands is None: 

285 isl.append(' Bands included: All') 

286 else: 

287 isl.append(f' Number of bands included: {nbands}') 

288 isl.append('Resulting in:') 

289 isl.append(f' A total number of band and spin transitions of: {nt}') 

290 return '\n'.join(isl) 

291 

292 

293class KPointPairIntegral(ABC): 

294 r"""Baseclass for reciprocal space integrals of the first Brillouin Zone, 

295 where the integrand is a sum over transitions between any number of states 

296 at the wave vectors k and k + q (referred to as a k-point pair). 

297 

298 Definition (V is the total crystal hypervolume and D is the dimension of 

299 the crystal): 

300 __ 

301 1 \ 1 / 

302 ‾ / (...)_k = ‾‾‾‾‾‾ |dk (...)_k 

303 V ‾‾ (2π)^D / 

304 k 

305 

306 NB: In the current implementation, the dimension is fixed to 3. This is 

307 sensible for pair functions which are functions of position (such as the 

308 four-component Kohn-Sham susceptibility tensor), and in most circumstances 

309 a change in dimensionality can be accomplished simply by adding an extra 

310 prefactor to the integral elsewhere. 

311 """ 

312 

313 def __init__(self, kptpair_extractor, generator): 

314 """Construct a KPointPairIntegral corresponding to a given q-point. 

315 

316 Parameters 

317 ---------- 

318 kptpair_extractor : KohnShamKPointPairExtractor 

319 Object responsible for extracting all relevant information about 

320 the k-point pairs from the underlying ground state calculation. 

321 generator : KPointDomainGenerator 

322 Object responsible for generating the k-point integration domain 

323 based on the symmetries of the q-point in question. 

324 """ 

325 self.gs = kptpair_extractor.gs 

326 self.kptpair_extractor = kptpair_extractor 

327 self.q_c = generator.symmetries.q_c 

328 

329 # Prepare the k-point pair integral 

330 bzk_kc, weight_k = self.get_kpoint_domain(generator) 

331 bzk_ipc, weight_i = self.slice_kpoint_domain(bzk_kc, weight_k) 

332 self._domain = (bzk_ipc, weight_i) 

333 self.ni = len(weight_i) 

334 

335 def weighted_kpoint_pairs(self, transitions): 

336 r"""Generate all k-point pairs in the integral along with their 

337 integral weights. 

338 

339 The reciprocal space integral is estimated as the sum over a discrete 

340 k-point domain. The domain will genererally depend on the integration 

341 method as well as the symmetry of the crystal. 

342 

343 Definition: 

344 __ 

345 1 / ~ 1 \ (2π)^D 

346 ‾‾‾‾‾‾ |dk (...)_k = ‾‾‾‾‾‾ / ‾‾‾‾‾‾ w_kr (...)_kr 

347 (2π)^D / (2π)^D ‾‾ Nk V0 

348 kr 

349 __ 

350 ~ \ 

351 = / iw_kr (...)_kr 

352 ‾‾ 

353 kr 

354 

355 The sum over kr denotes the reduced k-point domain specified by the 

356 integration method (a reduced selection of Nkr points from the ground 

357 state k-point grid of Nk total points in the entire 1BZ). Each point 

358 is weighted by its k-point volume in the ground state k-point grid 

359 

360 (2π)^D 

361 kpointvol = ‾‾‾‾‾‾, 

362 Nk V0 

363 

364 and an additional individual k-point weight w_kr specific to the 

365 integration method (V0 denotes the cell volume). Together with the 

366 integral prefactor, these make up the integral weight 

367 

368 1 

369 iw_kr = ‾‾‾‾‾‾ kpointvol w_kr 

370 (2π)^D 

371 

372 Parameters 

373 ---------- 

374 transitions : PairTransitions 

375 Band and spin transitions to integrate. 

376 """ 

377 # Calculate prefactors 

378 outer_prefactor = 1 / (2 * np.pi)**3 

379 V = self.crystal_volume() # V = Nk * V0 

380 kpointvol = (2 * np.pi)**3 / V 

381 prefactor = outer_prefactor * kpointvol 

382 

383 # Generate k-point pairs 

384 for k_pc, weight in zip(*self._domain): 

385 if weight is None: 

386 integral_weight = None 

387 else: 

388 integral_weight = prefactor * weight 

389 kptpair = self.kptpair_extractor.get_kpoint_pairs( 

390 k_pc, k_pc + self.q_c, transitions) 

391 yield kptpair, integral_weight 

392 

393 @abstractmethod 

394 def get_kpoint_domain(self, generator): 

395 """Use the KPointDomainGenerator to define and weight the k-integral. 

396 

397 Returns 

398 ------- 

399 bzk_kc : np.array 

400 k-points to integrate in relative coordinates. 

401 weight_k : np.array 

402 Integral weight of each k-point in the integral. 

403 """ 

404 

405 def slice_kpoint_domain(self, bzk_kc, weight_k): 

406 """When integrating over k-points, slice the domain in pieces with one 

407 k-point per process each. 

408 

409 Returns 

410 ------- 

411 bzk_ipc : nd.array 

412 k-points (relative) coordinates for each process for each iteration 

413 """ 

414 comm = self.kptpair_extractor.kpts_blockcomm 

415 rank, size = comm.rank, comm.size 

416 

417 nk = bzk_kc.shape[0] 

418 ni = (nk + size - 1) // size 

419 bzk_ipc = [bzk_kc[i * size:(i + 1) * size] for i in range(ni)] 

420 

421 # Extract the weight corresponding to the process' own k-point pair 

422 weight_ip = [weight_k[i * size:(i + 1) * size] for i in range(ni)] 

423 weight_i = [None] * len(weight_ip) 

424 for i, w_p in enumerate(weight_ip): 

425 if rank in range(len(w_p)): 

426 weight_i[i] = w_p[rank] 

427 

428 return bzk_ipc, weight_i 

429 

430 def crystal_volume(self): 

431 """Calculate the total crystal volume, V = Nk * V0, corresponding to 

432 the ground state k-point grid.""" 

433 return self.gs.kd.nbzkpts * self.gs.volume 

434 

435 

436class KPointPairPointIntegral(KPointPairIntegral): 

437 r"""Reciprocal space integral of k-point pairs in the first Brillouin Zone, 

438 estimated as a point integral over all k-points of the ground state k-point 

439 grid. 

440 

441 Definition: 

442 __ 

443 1 / ~ 1 \ (2π)^D 

444 ‾‾‾‾‾‾ |dk (...)_k = ‾‾‾‾‾‾ / ‾‾‾‾‾‾ (...)_k 

445 (2π)^D / (2π)^D ‾‾ Nk V0 

446 k 

447 

448 """ 

449 

450 def get_kpoint_domain(self, generator): 

451 # Generate k-point domain in relative coordinates 

452 K_gK = generator.group_kpoints() 

453 bzk_kc = np.array([self.gs.kd.bzk_kc[K_K[0]] for 

454 K_K in K_gK]) 

455 # Generate k-point weights 

456 weight_k = np.array([generator.get_kpoint_weight(k_c) 

457 for k_c in bzk_kc]) 

458 return bzk_kc, weight_k