Coverage for gpaw/response/bse.py: 90%

813 statements  

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

1from dataclasses import dataclass 

2from datetime import timedelta 

3from functools import cached_property 

4from time import time, ctime 

5 

6from ase.units import Hartree, Bohr 

7from ase.dft import monkhorst_pack 

8import numpy as np 

9from scipy.linalg import eigh 

10 

11from gpaw.blacs import BlacsGrid, Redistributor, BlacsDescriptor 

12from gpaw.kpt_descriptor import KPointDescriptor 

13from gpaw.mpi import world, serial_comm 

14from gpaw.response import ResponseContext 

15from gpaw.response.groundstate import CellDescriptor 

16from gpaw.response.chi0 import Chi0Calculator, get_frequency_descriptor 

17from gpaw.response.context import timer 

18from gpaw.response.coulomb_kernels import CoulombKernel 

19from gpaw.response.df import Chi0DysonEquations 

20from gpaw.response.df import write_response_function 

21from gpaw.response.frequencies import FrequencyDescriptor 

22from gpaw.response.pair import KPointPairFactory, get_gs_and_context 

23from gpaw.response.qpd import SingleQPWDescriptor 

24from gpaw.response.screened_interaction import (initialize_w_calculator, 

25 GammaIntegrationMode) 

26from gpaw.utilities.elpa import LibElpa 

27from gpaw.response.pw_parallelization import Blocks1D 

28 

29 

30def decide_whether_tammdancoff(val_m, con_m): 

31 for n in val_m: 

32 if n in con_m: 

33 return False 

34 return True 

35 

36 

37@dataclass 

38class BSEMatrix: 

39 df_S: np.ndarray 

40 H_sS: np.ndarray 

41 deps_S: np.ndarray 

42 deps_max: float 

43 

44 def diagonalize_nontammdancoff(self, bse, deps_max=None): 

45 df_S = self.df_S 

46 H_sS = self.H_sS 

47 if deps_max is None: 

48 deps_max = self.deps_max 

49 excludef_S = np.where(np.abs(df_S) < 0.001)[0] 

50 excludedeps_S = np.where(np.abs(self.deps_S) > deps_max)[0] 

51 exclude_S = np.unique(np.concatenate((excludef_S, excludedeps_S))) 

52 bse.context.print(' Using numpy.linalg.eig...') 

53 bse.context.print(' Eliminated %s pair orbitals' % len( 

54 exclude_S)) 

55 H_SS = bse.collect_A_SS(H_sS) 

56 w_T = np.zeros(bse.nS - len(exclude_S), complex) 

57 v_ST = None 

58 if world.rank == 0: 

59 H_SS = np.delete(H_SS, exclude_S, axis=0) 

60 H_SS = np.delete(H_SS, exclude_S, axis=1) 

61 w_T, v_ST = np.linalg.eig(H_SS) 

62 else: 

63 v_ST = None 

64 world.broadcast(w_T, 0) 

65 return w_T, v_ST, exclude_S 

66 

67 def diagonalize_tammdancoff(self, bse, deps_max=None, elpa=False): 

68 if deps_max is None: 

69 deps_max = self.deps_max 

70 exclude_S = np.where(np.abs(self.deps_S) > deps_max)[0] 

71 H_rr, new_grid_desc = self.exclude_states(bse, exclude_S) 

72 if world.size == 1: 

73 bse.context.print(' Using lapack...') 

74 w_T, v_Rt = eigh(H_rr) 

75 return w_T, v_Rt, exclude_S 

76 nR = bse.nS - len(exclude_S) 

77 w_T = np.empty(nR) 

78 v_rt = new_grid_desc.empty(dtype=complex) 

79 if elpa: 

80 bse.context.print('Using elpa...') 

81 elpa = LibElpa(new_grid_desc) 

82 elpa.diagonalize(H_rr, v_rt, w_T) 

83 else: 

84 bse.context.print('Using scalapack...') 

85 new_grid_desc.diagonalize_dc(H_rr, v_rt, w_T) 

86 

87 # redistribute eigenvectors 

88 # we want them to be parallelized over the last index only 

89 

90 grid_tR = BlacsGrid(world, world.size, 1) 

91 nt = -((-nR) // world.size) 

92 desc_tR = grid_tR.new_descriptor(nR, nR, nt, nR) 

93 v_tR = desc_tR.zeros(dtype=complex) 

94 Redistributor(world, new_grid_desc, 

95 desc_tR).redistribute(v_rt, v_tR) 

96 v_Rt = v_tR.conj().T 

97 return w_T, v_Rt, exclude_S 

98 

99 def exclude_states(self, bse, exclude_S): 

100 """ 

101 Removes pairs from the BSE Hamiltonian. 

102 A pair is removed if the absolute value of the 

103 transition energy eps_c - eps_v is greater than deps_max 

104 """ 

105 H_sS = self.H_sS 

106 grid = BlacsGrid(world, world.size, 1) 

107 nS = bse.nS 

108 ns = bse.ns 

109 desc = grid.new_descriptor(nS, nS, ns, nS) 

110 H_rr, new_desc = parallel_delete(H_sS, exclude_S, desc) 

111 bse.context.print(' Eliminated %s pair orbitals' % len( 

112 exclude_S)) 

113 return H_rr, new_desc 

114 

115 

116def parallel_delete(A_nn: np.ndarray, 

117 deleteN: np.ndarray, 

118 grid_desc: BlacsDescriptor, 

119 new_desc=None): 

120 """ 

121 Removes rows and columns from the distributed square matrix A_nn. 

122 This is done by redistributing the matrix to first make the second index 

123 global (A_nn -> A_mN), and then deleting along the second (global) index; 

124 then repeating the same procedure for the first index. 

125 -------------- 

126 Parameters: 

127 A_nn: distributed matrix 

128 deleteN : list of (global) indices to delete 

129 grid_desc: BlacsDescriptor for A_nn 

130 new_grid: BlacsGrid on which A_nn will be returned; optional. 

131 If None, the output grid will be determined automatically 

132 by the gpaw.matrix.suggest_blocking function. 

133 ------------------- 

134 Returns: 

135 A_rs: np.ndarray 

136 new_desc: BlacsDescriptor 

137 

138 A_rs is the (new) distributed matrix after rows and columns 

139 have been deleted. 

140 new_grid is the grid on which it is distributed. 

141 """ 

142 from gpaw.matrix import suggest_blocking 

143 N = grid_desc.N 

144 assert N == grid_desc.M, 'Matrix must be square' 

145 

146 R = N - len(deleteN) # global matrix dimension after deletion 

147 dtype = A_nn.dtype 

148 # redistribute matrix, so it is distributed over first index only. 

149 # then we can safely delete entries from the second index. 

150 grid_mN = BlacsGrid(world, world.size, 1) 

151 m = -((-N) // world.size) 

152 desc_mN = grid_mN.new_descriptor(N, N, m, N) 

153 A_mN = desc_mN.zeros(dtype=dtype) 

154 Redistributor(world, grid_desc, 

155 desc_mN).redistribute(A_nn, A_mN) 

156 

157 # delete, and ensure that array is still contiguous in memory 

158 A_mR = np.delete(A_mN, deleteN, axis=1) 

159 A_mR = np.ascontiguousarray(A_mR) 

160 desc_mR = grid_mN.new_descriptor(N, R, m, R) 

161 

162 # now distribute over second index, so we can delete entries from 1st 

163 r = -((-R) // world.size) 

164 grid_Nr = BlacsGrid(world, 1, world.size) 

165 desc_Nr = grid_Nr.new_descriptor(N, R, N, r) 

166 A_Nr = desc_Nr.zeros(dtype=dtype) 

167 Redistributor(world, desc_mR, 

168 desc_Nr).redistribute(A_mR, A_Nr) 

169 A_Rr = np.delete(A_Nr, deleteN, axis=0) 

170 A_Rr = np.ascontiguousarray(A_Rr) 

171 desc_Rr = grid_Nr.new_descriptor(R, R, R, r) 

172 

173 # Redistribute to final grid. 

174 # If this is not specified by the user, we try to find the most 

175 # efficient grid using the suggest_blocking function. 

176 if new_desc is None: 

177 nrows, ncols, blocksize = suggest_blocking(R, world.size) 

178 new_grid = BlacsGrid(world, nrows, ncols) 

179 new_desc = new_grid.new_descriptor(R, R, blocksize, blocksize) 

180 A_rr = new_desc.zeros(dtype=dtype) 

181 Redistributor(world, desc_Rr, 

182 new_desc).redistribute(A_Rr, A_rr) 

183 

184 return A_rr, new_desc 

185 

186 

187@dataclass 

188class ScreenedPotential: 

189 pawcorr_q: list 

190 W_qGG: list 

191 qpd_q: list 

192 

193 

194class SpinorData: 

195 def __init__(self, con_m, val_m, e_km, f_km, v_kmn, soc_tol): 

196 self.e_km = e_km 

197 self.f_km = f_km 

198 self.v0_kmn = v_kmn[:, :, ::2] 

199 self.v1_kmn = v_kmn[:, :, 1::2] 

200 

201 mi = val_m[0] 

202 mf = con_m[-1] + 1 

203 tmp_n = np.argwhere(np.abs(v_kmn[:, mi:mf])**2 > soc_tol)[:, 2] 

204 self.ni = np.min(tmp_n) // 2 

205 self.nf = np.max(tmp_n) // 2 + 1 

206 

207 self.vslice_m = slice(val_m[0], val_m[-1] + 1) 

208 self.cslice_m = slice(con_m[0], con_m[-1] + 1) 

209 

210 def _transform_rho(self, K1, K2, slice1, slice2, 

211 rho0_nnG, rho1_nnG, susc_component='00'): 

212 slice_n = slice(self.ni, self.nf) 

213 vec0k1_mn = self.v0_kmn[K1, slice1, slice_n] 

214 vec0k2_mn = self.v0_kmn[K2, slice2, slice_n] 

215 vec1k1_mn = self.v1_kmn[K1, slice1, slice_n] 

216 vec1k2_mn = self.v1_kmn[K2, slice2, slice_n] 

217 if susc_component == '00': 

218 if rho1_nnG is None: 

219 rho1_nnG = rho0_nnG 

220 rho0_mmG = np.dot(vec0k1_mn.conj(), np.dot(vec0k2_mn, rho0_nnG)) 

221 rho1_mmG = np.dot(vec1k1_mn.conj(), np.dot(vec1k2_mn, rho1_nnG)) 

222 return rho0_mmG + rho1_mmG 

223 elif susc_component == '+-': 

224 assert rho1_nnG is None 

225 return np.dot(vec0k1_mn.conj(), np.dot(vec1k2_mn, rho0_nnG)) 

226 elif susc_component == '-+': 

227 assert rho1_nnG is None 

228 return np.dot(vec1k1_mn.conj(), np.dot(vec0k2_mn, rho0_nnG)) 

229 else: 

230 raise NotImplementedError('Susceptibility component not ' 

231 'implemented. Please choose between ' 

232 '"00", "+-" or "-+"') 

233 

234 def rho_valence_valence(self, K1, K2, rho0_nnG, rho1_nnG=None): 

235 return self._transform_rho(K1, K2, self.vslice_m, 

236 self.vslice_m, rho0_nnG, rho1_nnG) 

237 

238 def rho_conduction_conduction(self, K1, K2, rho0_nnG, rho1_nnG=None): 

239 return self._transform_rho(K1, K2, self.cslice_m, self.cslice_m, 

240 rho0_nnG, rho1_nnG) 

241 

242 def rho_valence_conduction(self, K1, K2, rho0_nnG, 

243 rho1_nnG=None, susc_component='00'): 

244 return self._transform_rho(K1, K2, self.vslice_m, self.cslice_m, 

245 rho0_nnG, rho1_nnG, 

246 susc_component=susc_component) 

247 

248 def get_deps(self, K1, K2): 

249 epsv_m = self.e_km[K1, self.vslice_m] 

250 epsc_m = self.e_km[K2, self.cslice_m] 

251 return -(epsv_m[:, np.newaxis] - epsc_m) 

252 

253 def get_df(self, K1, K2): 

254 fv_m = self.f_km[K1, self.vslice_m] 

255 fc_m = self.f_km[K2, self.cslice_m] 

256 return fv_m[:, np.newaxis] - fc_m 

257 

258 

259class BSEBackend: 

260 def __init__(self, *, gs, context, 

261 valence_bands, 

262 conduction_bands, 

263 deps_max=None, 

264 add_soc=False, 

265 soc_tol=0.0001, 

266 ecut=10., 

267 scale=1.0, 

268 nbands=None, 

269 eshift=None, 

270 gw_kn=None, 

271 truncation=None, 

272 integrate_gamma='reciprocal', 

273 mode='BSE', 

274 q_c=[0.0, 0.0, 0.0], 

275 direction=0): 

276 

277 integrate_gamma = GammaIntegrationMode(integrate_gamma) 

278 

279 self.gs = gs 

280 self.q_c = q_c 

281 self.direction = direction 

282 self.context = context 

283 self.add_soc = add_soc 

284 self.scale = scale 

285 

286 assert mode in ['RPA', 'BSE'] 

287 

288 if deps_max is None: 

289 self.deps_max = np.inf 

290 else: 

291 self.deps_max = deps_max / Hartree 

292 self.ecut = ecut / Hartree 

293 self.nbands = nbands 

294 self.mode = mode 

295 

296 if integrate_gamma.is_analytical and truncation is not None: 

297 self.context.print('***WARNING*** Analytical Coulomb integration' + 

298 ' is not expected to work with Coulomb ' + 

299 'truncation. ' + 

300 'Use integrate_gamma=\'reciprocal\'') 

301 self.integrate_gamma = integrate_gamma 

302 

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

304 self.kd = self.gs.kd 

305 if -1 in self.kd.bz2bz_ks: 

306 self.context.print('***WARNING*** Symmetries may not be right. ' 

307 'Use gamma-centered grid to be sure') 

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

309 bzq_qc = monkhorst_pack(self.kd.N_c) + offset_c 

310 self.qd = KPointDescriptor(bzq_qc) 

311 self.qd.set_symmetry(self.gs.atoms, self.kd.symmetry) 

312 

313 # By default calculate the density-density response 

314 self.susc_component = '00' 

315 

316 # Bands and spin 

317 self.nspins = self.gs.nspins 

318 self.val_m = self.parse_bands(valence_bands, 

319 band_type='valence') 

320 self.con_m = self.parse_bands(conduction_bands, 

321 band_type='conduction') 

322 

323 self.use_tammdancoff = decide_whether_tammdancoff(self.val_m, 

324 self.con_m) 

325 

326 self.nK = self.kd.nbzkpts 

327 self.nv = len(self.val_m) 

328 self.nc = len(self.con_m) 

329 if eshift is not None: 

330 eshift /= Hartree 

331 if gw_kn is not None: 

332 assert self.nv + self.nc == len(gw_kn[0]) 

333 assert self.kd.nibzkpts == len(gw_kn) 

334 gw_kn = gw_kn[self.kd.bz2ibz_k] 

335 gw_kn /= Hartree 

336 self.gw_kn = gw_kn 

337 self.eshift = eshift 

338 

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

340 

341 # Distribution of kpoints 

342 self.myKrange, self.myKsize = self.parallelisation_kpoints() 

343 

344 # Number of global and local pair orbitals. Note that self.ns is the 

345 # the same everywhere and adds up to a value that is larger that nS. 

346 # This is required for BlacsGrids in the ScalaPack diagonalization. 

347 self.nS = self.nK * self.nv * self.nc 

348 self.ns = -(-self.nK // world.size) * self.nv * self.nc 

349 

350 # Print all the details 

351 self.print_initialization(self.use_tammdancoff, self.eshift, 

352 self.gw_kn) 

353 

354 # Treat spin-polarized states as spinors without soc 

355 if self.nspins == 2 and not self.add_soc: 

356 self.add_soc = True 

357 self.scale = 0.0 

358 

359 # Setup bands 

360 if self.add_soc: 

361 self.spinors_data = self._spinordata(soc_tol) 

362 # Get a wide range of pair densities to allow for SOC mixing. 

363 # The number of no-SOC states included are determined by soc_tol 

364 # such that components of the soc eigenstates are included if 

365 # their norm square is above soc_tol. 

366 # The no-SOC pair densities are then transformed to the SOC pair 

367 # densities. vi, vf, ci, cf here determines initial and final 

368 # indices of no-SOC valence ond conduction states included. 

369 self.vi = self.spinors_data.ni 

370 self.vf = self.spinors_data.nf 

371 self.ci = self.spinors_data.ni 

372 self.cf = self.spinors_data.nf 

373 else: 

374 # Here we just need the pair densities of the specified bands 

375 self.vi, self.vf = self.val_m[0], self.val_m[-1] + 1 

376 self.ci, self.cf = self.con_m[0], self.con_m[-1] + 1 

377 

378 def parse_bands(self, bands, band_type='valence'): 

379 """Helper function that checks whether bands are correctly specified, 

380 and brings them to the format used later in the code. 

381 

382 Either integers (numbers of desired bands) or lists of band indices 

383 must be provided. For spin-polarized calculations all 

384 valence/condiction bands must be specified as a single list (one 

385 for each) - regardless of spin. Same as if one includes SOC. 

386 

387 band_type is an optional parameter that is relevant when a desired 

388 number of bands is given (rather than a list) to help figure out the 

389 correct band indices. 

390 """ 

391 if hasattr(bands, '__iter__'): 

392 if not isinstance(bands[0], int): 

393 raise ValueError('The bands must be specified as a single ' 

394 'list or an integer (number of bands).') 

395 return bands 

396 

397 n_fully_occupied_bands, n_partially_occupied_bands = \ 

398 self.gs.count_occupied_bands() 

399 

400 if self.nspins == 2: 

401 n_fully_occupied_bands += n_partially_occupied_bands 

402 elif self.add_soc: 

403 n_fully_occupied_bands *= 2 

404 

405 if band_type == 'valence': 

406 bands_m = range(n_fully_occupied_bands - bands, 

407 n_fully_occupied_bands) 

408 elif band_type == 'conduction': 

409 bands_m = range(n_fully_occupied_bands, 

410 n_fully_occupied_bands + bands) 

411 else: 

412 raise ValueError(f'Invalid band type: {band_type}') 

413 

414 return bands_m 

415 

416 def _spinordata(self, soc_tol): 

417 self.context.print('Diagonalizing spin-orbit Hamiltonian') 

418 # We dont need ALL the screening bands to mix in the SOC here. 

419 # This will give us bands up to two times the highest conduction band. 

420 n2 = np.min([self.con_m[-1], self.nbands]) 

421 soc = self.gs.soc_eigenstates(n2=n2, scale=self.scale) 

422 f_km = np.array([wf.f_m for wf in soc]) 

423 e_km = soc.eigenvalues() 

424 e_km /= Hartree 

425 v_kmn = soc.eigenvectors() 

426 return SpinorData(self.con_m, self.val_m, e_km, f_km, v_kmn, soc_tol) 

427 

428 @timer('BSE calculate') 

429 def calculate(self, optical, irreducible=False): 

430 """Calculate the BSE Hamiltonian. This includes setting up all 

431 machinery for pair densities, KS eignevalues and occupation factors. 

432 At the end the direct and indirect interaction are included through 

433 calls to separate functions. 

434 

435 The indices are indicative such that all capital letters imply 

436 global indices and lower case letters imply local ("my CPU") 

437 indices. For example, K, S and k, s are global and local k-point 

438 and pair state indices respectively. 

439 

440 In addition the KS state indices are used such that n represents 

441 states without SOC and m represents states with SOC. This is not always 

442 possible though - the Hamiltonian, for example, is always 

443 denoted H_kmmKmm - also for calculations without SOC. G is reciprocal 

444 lattice index. 

445 

446 The parameter 'irreducible' puts V=0 such that the BSE kernel only 

447 contians W. It is used for BSE+ calculations. 

448 """ 

449 qpd0 = SingleQPWDescriptor.from_q(self.q_c, self.ecut, self.gs.gd) 

450 

451 self.ikq_k = self.kd.find_k_plus_q(self.q_c) 

452 if irreducible: 

453 self.v_G = np.zeros(qpd0.ng_q[0]) 

454 else: 

455 self.v_G = self.coulomb.V(qpd=qpd0, q_v=None) 

456 

457 if optical: 

458 self.v_G[0] = 0.0 

459 

460 context = ResponseContext(txt='pair.txt', timer=self.context.timer, 

461 comm=serial_comm) 

462 kptpair_factory = KPointPairFactory(gs=self.gs, context=context) 

463 pair_calc = kptpair_factory.pair_calculator() 

464 pawcorr = self.gs.pair_density_paw_corrections(qpd0) 

465 

466 if self.mode != 'RPA': 

467 screened_potential = self.calculate_screened_potential() 

468 else: 

469 screened_potential = None 

470 

471 # Calculate pair densities, eigenvalues and occupations 

472 self.context.timer.start('Pair densities') 

473 if self.susc_component != '00': 

474 assert self.nspins == 2 

475 rhomag_KmmG = np.zeros((self.nK, self.nv, 

476 self.nc, len(self.v_G)), complex) 

477 rhoex_KmmG = np.zeros((self.nK, self.nv, 

478 self.nc, len(self.v_G)), complex) 

479 df_Kmm = np.zeros((self.nK, self.nv, 

480 self.nc), float) # (fc - fv) 

481 deps_kmm = np.zeros((self.myKsize, self.nv, 

482 self.nc), float) # (ec - ev) 

483 deps_Kmm = np.zeros((self.nK, self.nv, self.nc), float) 

484 optical_limit = np.allclose(self.q_c, 0.0) 

485 

486 get_pair = kptpair_factory.get_kpoint_pair 

487 get_pair_density = pair_calc.get_pair_density 

488 

489 # Calculate all properties diagonal in k-point 

490 # These include the indirect (exchange) kernel, 

491 # pseudo-energies, and occupation numbers 

492 for ik, iK in enumerate(self.myKrange): 

493 pair0 = get_pair(qpd0, 0, iK, self.vi, self.vf, 

494 self.ci, self.cf) 

495 v_n = np.arange(self.vi, self.vf) 

496 c_n = np.arange(self.ci, self.cf) 

497 iKq = self.gs.kd.find_k_plus_q(self.q_c, [iK])[0] 

498 

499 # Energies 

500 if self.gw_kn is not None: 

501 epsv_m = self.gw_kn[iK, :self.nv] 

502 epsc_m = self.gw_kn[iKq, self.nv:] 

503 deps_kmm[ik] = -(epsv_m[:, np.newaxis] - epsc_m) 

504 elif self.add_soc: 

505 deps_kmm[ik] = self.spinors_data.get_deps(iK, iKq) 

506 else: 

507 deps_kmm[ik] = -pair0.get_transition_energies() 

508 if optical_limit: 

509 deps_kmm[np.where(deps_kmm == 0)] = 1.0e-9 

510 

511 # Occupation factors 

512 if self.add_soc: 

513 df_Kmm[iK] = self.spinors_data.get_df(iK, iKq) 

514 else: 

515 df_Kmm[iK] = pair0.get_occupation_differences() 

516 

517 # Pair densities 

518 rho0_nnG = get_pair_density(qpd0, pair0, v_n, c_n, 

519 pawcorr=pawcorr) 

520 if optical_limit: 

521 n0_nnv = pair_calc.get_optical_pair_density_head( 

522 qpd0, pair0, v_n, c_n) 

523 rho0_nnG[:, :, 0] = n0_nnv[:, :, self.direction] 

524 if self.nspins == 2: 

525 pair1 = get_pair(qpd0, 1, iK, self.vi, self.vf, 

526 self.ci, self.cf) 

527 rho1_nnG = get_pair_density(qpd0, pair1, v_n, c_n, 

528 pawcorr=pawcorr) 

529 if optical_limit: 

530 n1_nnv = pair_calc.get_optical_pair_density_head( 

531 qpd0, pair1, v_n, c_n) 

532 rho1_nnG[:, :, 0] = n1_nnv[:, :, self.direction] 

533 deps1_nn = -pair1.get_transition_energies() 

534 rho1_nnG[:, :, 0] *= deps1_nn 

535 else: 

536 rho1_nnG = None 

537 

538 # Generate the pair density matrix (with soc) used below 

539 if self.add_soc: 

540 if optical_limit: 

541 deps0_nn = -pair0.get_transition_energies() 

542 rho0_nnG[:, :, 0] *= deps0_nn 

543 rhoex_KmmG[iK] = \ 

544 self.spinors_data.rho_valence_conduction( 

545 iK, iKq, rho0_nnG, rho1_nnG) 

546 if optical_limit: 

547 rhoex_KmmG[iK, :, :, 0] /= deps_kmm[ik] 

548 else: 

549 rhoex_KmmG[iK] = rho0_nnG 

550 

551 # Generate the magnetic spin flip pair density for magnons 

552 if self.susc_component != '00': 

553 if self.susc_component == '+-': 

554 s = 0 

555 elif self.susc_component == '-+': 

556 s = 1 

557 pairflip = get_pair(qpd0, s, iK, self.vi, self.vf, 

558 self.ci, self.cf, flipspin=True) 

559 rhoflip_nnG = get_pair_density(qpd0, pairflip, v_n, c_n, 

560 pawcorr=pawcorr) 

561 rhomag_KmmG[iK] = self.spinors_data.rho_valence_conduction( 

562 iK, iKq, rhoflip_nnG, susc_component=self.susc_component) 

563 

564 # Scissors operator shift 

565 if self.eshift is not None: 

566 deps_kmm[np.where(df_Kmm[self.myKrange] > 1e-3)] += self.eshift 

567 deps_kmm[np.where(df_Kmm[self.myKrange] < -1e-3)] -= self.eshift 

568 deps_Kmm[self.myKrange] = deps_kmm 

569 

570 world.sum(deps_Kmm) 

571 world.sum(df_Kmm) 

572 world.sum(rhoex_KmmG) 

573 

574 self.rhoG0_S = np.reshape(rhoex_KmmG[:, :, :, 0], -1) 

575 self.rho_SG = np.reshape(rhoex_KmmG, (len(self.rhoG0_S), -1)) 

576 if self.susc_component != '00': 

577 world.sum(rhomag_KmmG) 

578 self.rhomag_SG = np.reshape(rhomag_KmmG, (self.nS, -1)) 

579 G_Gv = qpd0.get_reciprocal_vectors(add_q=False) 

580 self.G_Gc = np.dot(G_Gv, qpd0.gd.cell_cv.T / (2 * np.pi)) 

581 self.context.timer.stop('Pair densities') 

582 

583 # Calculate Hamiltonian 

584 self.context.timer.start('Calculate Hamiltonian') 

585 t0 = time() 

586 

587 def update_progress(iK1): 

588 dt = time() - t0 

589 tleft = dt * self.myKsize / (iK1 + 1) - dt 

590 

591 self.context.print( 

592 ' Finished %s pair orbitals in %s - Estimated %s left' 

593 % ((iK1 + 1) * self.nv * self.nc * world.size, 

594 timedelta(seconds=round(dt)), 

595 timedelta(seconds=round(tleft)))) 

596 

597 self.context.print('Calculating {} matrix elements at q_c = {}'.format( 

598 self.mode, self.q_c)) 

599 

600 # Hamiltonian buffer array 

601 H_kmmKmm = np.zeros((self.myKsize, self.nv, self.nc, 

602 self.nK, self.nv, self.nc), 

603 complex) 

604 

605 # Add kernels to buffer array 

606 self.add_indirect_kernel(kptpair_factory, rhoex_KmmG, H_kmmKmm) 

607 if self.mode != 'RPA': 

608 self.add_direct_kernel(kptpair_factory, pair_calc, 

609 screened_potential, update_progress, 

610 H_kmmKmm) 

611 H_kmmKmm /= self.gs.volume 

612 self.context.timer.stop('Calculate Hamiltonian') 

613 

614 if self.myKsize > 0: 

615 iS0 = self.myKrange[0] * self.nv * self.nc 

616 

617 # multiply by 2 when spin-paired and no SOC 

618 df_Kmm *= 2.0 / self.nK / (self.add_soc + 1) 

619 df_S = np.reshape(df_Kmm, -1) 

620 self.df_S = df_S 

621 

622 deps_S = np.reshape(deps_Kmm, -1) 

623 deps_s = np.reshape(deps_kmm, -1) 

624 

625 mySsize = self.myKsize * self.nv * self.nc 

626 H_sS = np.reshape(H_kmmKmm, (mySsize, self.nS)) 

627 for iS in range(mySsize): 

628 # Multiply by occupations 

629 H_sS[iS] *= df_S[iS0 + iS] 

630 # add bare transition energies 

631 H_sS[iS, iS0 + iS] += deps_s[iS] 

632 

633 return BSEMatrix(df_S, H_sS, deps_S, self.deps_max) 

634 

635 @timer('add_direct_kernel') 

636 def add_direct_kernel(self, kptpair_factory, pair_calc, screened_potential, 

637 update_progress, H_kmmKmm): 

638 kpf = kptpair_factory 

639 for ik1, iK1 in enumerate(self.myKrange): 

640 kptv1_s = [kpf.get_k_point(s, iK1, self.vi, self.vf) 

641 for s in range(self.nspins)] 

642 kptc1_s = [kpf.get_k_point(s, self.ikq_k[iK1], self.ci, self.cf) 

643 for s in range(self.nspins)] 

644 for Q_c in self.qd.bzk_kc: 

645 iK2 = self.kd.find_k_plus_q(Q_c, [kptv1_s[0].K])[0] 

646 kptv2_s = [kptpair_factory.get_k_point(s, iK2, self.vi, 

647 self.vf) 

648 for s in range(self.nspins)] 

649 kptc2_s = [kptpair_factory.get_k_point(s, self.ikq_k[iK2], 

650 self.ci, self.cf) 

651 for s in range(self.nspins)] 

652 

653 rho3_nnG, iq = self.get_density_matrix( 

654 pair_calc, screened_potential, kptv1_s[0], kptv2_s[0]) 

655 

656 rho4_nnG, iq = self.get_density_matrix( 

657 pair_calc, screened_potential, kptc1_s[0], kptc2_s[0]) 

658 

659 if self.nspins == 2: 

660 rho3s1_nnG, iq = self.get_density_matrix( 

661 pair_calc, screened_potential, kptv1_s[1], kptv2_s[1]) 

662 

663 rho4s1_nnG, iq = self.get_density_matrix( 

664 pair_calc, screened_potential, kptc1_s[1], kptc2_s[1]) 

665 else: 

666 rho3s1_nnG = None 

667 rho4s1_nnG = None 

668 

669 # Here we use n instead of m for the soc indices to save memory 

670 if self.add_soc: 

671 rho3_nnG = self.spinors_data.rho_valence_valence( 

672 kptv1_s[0].K, kptv2_s[0].K, rho3_nnG, rho3s1_nnG) 

673 

674 rho4_nnG = self.spinors_data.rho_conduction_conduction( 

675 kptc1_s[0].K, kptc2_s[0].K, rho4_nnG, rho4s1_nnG) 

676 

677 self.context.timer.start('Screened exchange') 

678 W_mmmm = np.einsum( 

679 'ijk,km,pqm->ipjq', 

680 rho3_nnG.conj(), 

681 screened_potential.W_qGG[iq], 

682 rho4_nnG, 

683 optimize='optimal') 

684 # Only include 0.5*W for spinpaired calculations without soc 

685 H_kmmKmm[ik1, :, :, iK2] -= W_mmmm * (self.add_soc + 1) / 2 

686 self.context.timer.stop('Screened exchange') 

687 

688 if iK1 % (self.myKsize // 5 + 1) == 0: 

689 update_progress(iK1=iK1) 

690 

691 @timer('add_indirect_kernel') 

692 def add_indirect_kernel(self, kptpair_factory, rhoex_KmmG, H_kmmKmm): 

693 for ik1, iK1 in enumerate(self.myKrange): 

694 kptv1 = kptpair_factory.get_k_point( 

695 0, iK1, self.vi, self.vf) 

696 rho1V_mmG = rhoex_KmmG.conj()[iK1, :, :] * self.v_G 

697 for Q_c in self.qd.bzk_kc: 

698 iK2 = self.kd.find_k_plus_q(Q_c, [kptv1.K])[0] 

699 rho2_mmG = rhoex_KmmG[iK2] 

700 self.context.timer.start('Coulomb') 

701 H_kmmKmm[ik1, :, :, iK2, :, :] += np.einsum( 

702 'ijG,mnG->ijmn', rho1V_mmG, rho2_mmG, 

703 optimize='optimal') 

704 self.context.timer.stop('Coulomb') 

705 

706 @timer('get_density_matrix') 

707 def get_density_matrix(self, pair_calc, screened_potential, kpt1, kpt2): 

708 self.context.timer.start('Symop') 

709 from gpaw.response.g0w0 import QSymmetryOp, get_nmG 

710 symop, iq = QSymmetryOp.get_symop_from_kpair(self.kd, self.qd, 

711 kpt1, kpt2) 

712 qpd = screened_potential.qpd_q[iq] 

713 nG = qpd.ngmax 

714 pawcorr0 = screened_potential.pawcorr_q[iq] 

715 pawcorr, I_G = symop.apply_symop_q(qpd, pawcorr0, kpt1, kpt2) 

716 self.context.timer.stop('Symop') 

717 

718 rho_nnG = np.zeros((len(kpt1.eps_n), len(kpt2.eps_n), nG), complex) 

719 for n in range(len(rho_nnG)): 

720 rho_nnG[n] = get_nmG(kpt1, kpt2, pawcorr, n, qpd, I_G, 

721 pair_calc, timer=self.context.timer) 

722 

723 return rho_nnG, iq 

724 

725 @cached_property 

726 def _chi0calc(self): 

727 return Chi0Calculator( 

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

729 wd=FrequencyDescriptor([0.0]), 

730 eta=0.001, 

731 ecut=self.ecut * Hartree, 

732 intraband=False, 

733 hilbert=False, 

734 nbands=self.nbands) 

735 

736 @cached_property 

737 def blockcomm(self): 

738 return self._chi0calc.chi0_body_calc.blockcomm 

739 

740 @cached_property 

741 def wcontext(self): 

742 return ResponseContext(txt='w.txt', comm=world) 

743 

744 @cached_property 

745 def _wcalc(self): 

746 return initialize_w_calculator( 

747 self._chi0calc, self.wcontext, 

748 coulomb=self.coulomb, 

749 integrate_gamma=self.integrate_gamma) 

750 

751 @timer('calculate_screened_potential') 

752 def calculate_screened_potential(self): 

753 """Calculate W_GG(q).""" 

754 

755 pawcorr_q = [] 

756 W_qGG = [] 

757 qpd_q = [] 

758 

759 t0 = time() 

760 self.context.print('Calculating screened potential') 

761 for iq, q_c in enumerate(self.qd.ibzk_kc): 

762 chi0 = self._chi0calc.calculate(q_c) 

763 W_wGG = self._wcalc.calculate_W_wGG(chi0) 

764 W_GG = W_wGG[0] 

765 # This is such a terrible way to access the paw 

766 # corrections. Attributes should not be groped like 

767 # this... Change in the future! XXX 

768 pawcorr_q.append(self._chi0calc.chi0_body_calc.pawcorr) 

769 qpd_q.append(chi0.qpd) 

770 W_qGG.append(W_GG) 

771 

772 if iq % (self.qd.nibzkpts // 5 + 1) == 2: 

773 dt = time() - t0 

774 tleft = dt * self.qd.nibzkpts / (iq + 1) - dt 

775 self.context.print( 

776 ' Finished {} q-points in {} - Estimated {} left'.format( 

777 iq + 1, timedelta(seconds=round(dt)), timedelta( 

778 seconds=round(tleft)))) 

779 

780 return ScreenedPotential(pawcorr_q, W_qGG, qpd_q) 

781 

782 @timer('diagonalize') 

783 def diagonalize_bse_matrix(self, bsematrix): 

784 self.context.print('Diagonalizing Hamiltonian') 

785 if self.use_tammdancoff: 

786 return bsematrix.diagonalize_tammdancoff(self) 

787 else: 

788 return bsematrix.diagonalize_nontammdancoff(self) 

789 

790 @timer('get_bse_matrix') 

791 def get_bse_matrix(self, optical=True, irreducible=False): 

792 """Calculate BSE matrix.""" 

793 return self.calculate(optical=optical, irreducible=irreducible) 

794 

795 @timer('get_spectral_weights') 

796 def get_spectral_weights(self, eig_data, df_S, mode_c): 

797 if mode_c is None: 

798 rho_S = self.rhoG0_S 

799 else: 

800 G_Gc = self.G_Gc 

801 index = np.where(np.all(np.round(G_Gc) == mode_c, axis=1))[0][0] 

802 rho_S = self.rhomag_SG[:, index] 

803 

804 w_T, v_St = eig_data[0], eig_data[1] 

805 exclude_S = eig_data[2] 

806 nS = self.nS - len(exclude_S) 

807 ns = -(-nS // world.size) 

808 dft_S = np.delete(df_S, exclude_S) 

809 rhot_S = np.delete(rho_S, exclude_S) 

810 C_T = np.zeros(nS, complex) 

811 # Calculate the spectral weights C_T 

812 if self.use_tammdancoff: 

813 A_t = np.dot(rhot_S, v_St) 

814 B_t = np.dot(rhot_S * dft_S, v_St) 

815 if world.size == 1: 

816 C_T = B_t.conj() * A_t 

817 else: 

818 grid = BlacsGrid(world, world.size, 1) 

819 desc = grid.new_descriptor(nS, 1, ns, 1) 

820 C_t = desc.empty(dtype=complex) 

821 C_t[:, 0] = B_t.conj() * A_t 

822 C_T = desc.collect_on_master(C_t)[:, 0] 

823 if world.rank != 0: 

824 C_T = np.empty(nS, dtype=complex) 

825 world.broadcast(C_T, 0) 

826 else: 

827 if world.rank == 0: 

828 A_T = np.dot(rhot_S, v_St) 

829 B_T = np.dot(rhot_S * dft_S, v_St) 

830 tmp = np.dot(v_St.conj().T, v_St) 

831 overlap_TT = np.linalg.inv(tmp) 

832 C_T = np.dot(B_T.conj(), overlap_TT.T) * A_T 

833 world.broadcast(C_T, 0) 

834 

835 return w_T, C_T 

836 

837 def _cache_eig_data(self, irreducible, optical, w_w): 

838 if (not hasattr(self, 'eig_data') 

839 or self.eig_data_irreducible != irreducible 

840 or self.eig_data_optical != optical): 

841 bsematrix = self.get_bse_matrix(optical=optical, 

842 irreducible=irreducible) 

843 self.context.print('Calculating response function at %s frequency ' 

844 'points' % len(w_w)) 

845 self.eig_data = self.diagonalize_bse_matrix(bsematrix) 

846 self.eig_data_irreducible = irreducible 

847 self.eig_data_optical = optical 

848 

849 @timer('get_vchi') 

850 def get_vchi(self, w_w=None, eta=0.1, optical=True, write_eig=None, 

851 mode_c=None, irreducible=False): 

852 """Returns v * chi where v is the bare Coulomb interaction""" 

853 

854 vchi_w = np.zeros(len(w_w), dtype=complex) 

855 

856 self._cache_eig_data(irreducible, optical, w_w) 

857 

858 w_T, C_T = self.get_spectral_weights(self.eig_data, 

859 self.df_S, mode_c) 

860 

861 if write_eig is not None: 

862 assert isinstance(write_eig, str) 

863 filename = write_eig 

864 if world.rank == 0: 

865 write_bse_eigenvalues(filename, self.mode, 

866 w_T * Hartree, C_T) 

867 

868 eta /= Hartree 

869 for iw, w in enumerate(w_w / Hartree): 

870 tmp_T = 1. / (w - w_T + 1j * eta) 

871 vchi_w[iw] += np.dot(tmp_T, C_T) 

872 vchi_w *= 4 * np.pi / self.gs.volume 

873 

874 if not np.allclose(self.q_c, 0.0): 

875 cell_cv = self.gs.gd.cell_cv 

876 B_cv = 2 * np.pi * np.linalg.inv(cell_cv).T 

877 q_v = np.dot(self.q_c, B_cv) 

878 vchi_w /= np.dot(q_v, q_v) 

879 

880 # Check f-sum rule 

881 nv = self.gs.nvalence 

882 dw_w = (w_w[1:] - w_w[:-1]) / Hartree 

883 wvchi_w = (w_w[1:] * vchi_w[1:] + w_w[:-1] * vchi_w[:-1]) / Hartree / 2 

884 N = -np.dot(dw_w, wvchi_w.imag) * self.gs.volume / (2 * np.pi**2) 

885 Nt = 2 * np.dot(w_T, C_T).real 

886 self.context.print('', flush=False) 

887 self.context.print('Checking f-sum rule', flush=False) 

888 self.context.print(f' Valence electrons : {nv}', flush=False) 

889 self.context.print(f' Frequency integral: {N:f}', flush=False) 

890 self.context.print(f' Sum of weights : {Nt:f}', flush=False) 

891 self.context.print('') 

892 

893 return vchi_w 

894 

895 @timer('get_chi_wGG') 

896 def get_chi_wGG(self, w_w=None, eta=0.1, readfile=None, optical=True, 

897 irreducible=False): 

898 """Returns chi_wGG'""" 

899 

900 self._cache_eig_data(irreducible, optical, w_w) 

901 

902 w_T, v_Rt, exclude_S = \ 

903 self.eig_data[0], self.eig_data[1], self.eig_data[2] 

904 rho_SG = self.rho_SG 

905 df_S = self.df_S 

906 df_R = np.delete(df_S, exclude_S) 

907 rho_RG = np.delete(rho_SG, exclude_S, axis=0) 

908 

909 nG = rho_RG.shape[-1] 

910 nR = self.nS - len(exclude_S) 

911 nr = -(-nR // world.size) 

912 # nr is the local size of the array 

913 

914 self.context.print('Calculating response function at %s frequency ' 

915 'points' % len(w_w)) 

916 self.blocks = Blocks1D(world, len(w_T)) 

917 w_t = w_T[self.blocks.myslice] 

918 

919 if not self.use_tammdancoff: 

920 if world.rank == 0: 

921 v_RT = v_Rt 

922 A_GT = rho_RG.T @ v_RT 

923 B_GT = rho_RG.T * df_R[np.newaxis] @ v_RT 

924 tmp = v_RT.conj().T @ v_RT 

925 overlap_tt = np.linalg.inv(tmp) 

926 C_tGG = ((B_GT.conj() @ overlap_tt.T).T)[..., np.newaxis] *\ 

927 A_GT.T[:, np.newaxis] 

928 C_tGG = C_tGG[:nR].reshape((nR, nG, nG)) 

929 flat_C_tGG = C_tGG.ravel() 

930 else: 

931 flat_C_tGG = np.empty(nR * nG * nG, dtype=complex) 

932 world.broadcast(flat_C_tGG, 0) 

933 C_tGG = flat_C_tGG.reshape((nR, nG, nG))[self.blocks.myslice] 

934 C_tGG1 = None 

935 else: 

936 A_Gt = rho_RG.T @ v_Rt 

937 B_Gt = (rho_RG.T * df_R[np.newaxis]) @ v_Rt 

938 '''The following computes 

939 C_tGG1 = A_Gt.T.conj()[..., np.newaxis] * B_Gt.T[:, np.newaxis] 

940 C_tGG = B_Gt.T.conj()[..., np.newaxis] * A_Gt.T[:, np.newaxis] 

941 ''' 

942 grid = BlacsGrid(world, world.size, 1) 

943 desc = grid.new_descriptor(nR, nG * nG, nr, nG * nG) 

944 C_tGG = desc.empty(dtype=complex) 

945 np.einsum('Gt,Ht->tGH', B_Gt.conj(), A_Gt, 

946 out=C_tGG.reshape((-1, nG, nG))) 

947 desc1 = grid.new_descriptor(nR, nG * nG, nr, nG * nG) 

948 C_tGG1 = desc1.empty(dtype=complex) 

949 np.einsum('Gt,Ht->tGH', A_Gt.conj(), B_Gt, 

950 out=C_tGG1.reshape((-1, nG, nG))) 

951 print(f'shape is {C_tGG.shape}') 

952 C_tGG = C_tGG[:C_tGG.shape[0]].reshape((C_tGG.shape[0], nG, nG)) 

953 C_tGG1 = C_tGG1[:C_tGG1.shape[0]].reshape( 

954 (C_tGG1.shape[0], nG, nG)) 

955 

956 eta /= Hartree 

957 

958 if C_tGG is not None: 

959 tmp_tw = 1 / (w_w[None, :] / Hartree - w_t[:, None] + 1j * eta) 

960 chi_wGG_local = np.einsum('tw,tAB->wAB', tmp_tw, C_tGG) 

961 

962 if C_tGG1 is not None: 

963 n_tmp_tw = - 1 / (w_w[None, :] / Hartree 

964 + w_t[:, None] + 1j * eta) 

965 chi_wGG_local += np.einsum('tw,tAB->wAB', n_tmp_tw, C_tGG1) 

966 

967 chi_wGG_local *= 1 / self.gs.volume 

968 

969 world.sum(chi_wGG_local) 

970 chi_wGG = chi_wGG_local 

971 

972 return np.swapaxes(chi_wGG, -1, -2) 

973 

974 def get_dielectric_function(self, *args, filename='df_bse.csv', **kwargs): 

975 vchi = self.vchi(*args, optical=True, **kwargs) 

976 return vchi.dielectric_function(filename=filename) 

977 

978 def get_eels_spectrum(self, *args, filename='df_bse.csv', **kwargs): 

979 vchi = self.vchi(*args, optical=False, **kwargs) 

980 return vchi.eels_spectrum(filename=filename) 

981 

982 def get_polarizability(self, *args, filename='pol_bse.csv', **kwargs): 

983 vchi = self.vchi(*args, optical=True, **kwargs) 

984 return vchi.polarizability(filename=filename) 

985 

986 def get_magnetic_susceptibility(self, *args, modes_Gc=[[0, 0, 0]], 

987 susc_component='+-', 

988 write_eig='eig', 

989 filename='susc_+-_bse_', 

990 **kwargs): 

991 """Returns and writes real and imaginary part of the magnetic 

992 susceptibility. 

993 

994 susc_componenet: str 

995 Component of the susceptibility tensor. '+-' and '-+' 

996 are supported. 

997 modes_Gc: list 

998 List of reciprocal lattice vectors in reduced on which the 

999 susceptibility is calculated. Default is the [0, 0, 0] 

1000 component which gives the response over the Brillouin zone, 

1001 but for optical magnons other components are needed. 

1002 """ 

1003 self.susc_component = susc_component 

1004 assert susc_component in ['+-', '-+'] 

1005 chi_Gw = [] 

1006 for mode_c in modes_Gc: 

1007 assert len(mode_c) == 3 

1008 assert all(isinstance(x, int) for x in mode_c) 

1009 file_G = filename + ''.join(str(x) for x in mode_c) + '.csv' 

1010 eig = write_eig + ''.join(str(x) for x in mode_c) + '.dat' 

1011 vchi = self.vchi(*args, optical=False, mode_c=mode_c, 

1012 write_eig=eig, **kwargs) 

1013 chi_Gw.append(vchi.magnetic_susceptibility(filename=file_G)) 

1014 return chi_Gw 

1015 

1016 def vchi(self, w_w=None, eta=0.1, write_eig='eig.dat', 

1017 optical=None, mode_c=None): 

1018 vchi_w = self.get_vchi(w_w=w_w, eta=eta, optical=optical, 

1019 write_eig=write_eig, mode_c=mode_c) 

1020 return VChi(self.gs.cd, self.context, w_w, vchi_w, optical=optical) 

1021 

1022 def collect_A_SS(self, A_sS): 

1023 if world.rank == 0: 

1024 A_SS = np.zeros((self.nS, self.nS), dtype=complex) 

1025 A_SS[:len(A_sS)] = A_sS 

1026 Ntot = len(A_sS) 

1027 for rank in range(1, world.size): 

1028 buf = np.empty((self.ns, self.nS), dtype=complex) 

1029 world.receive(buf, rank, tag=123) 

1030 A_SS[Ntot:Ntot + self.ns] = buf 

1031 Ntot += self.ns 

1032 else: 

1033 world.send(A_sS, 0, tag=123) 

1034 world.barrier() 

1035 if world.rank == 0: 

1036 return A_SS 

1037 

1038 def parallelisation_kpoints(self, rank=None): 

1039 if rank is None: 

1040 rank = world.rank 

1041 nK = self.kd.nbzkpts 

1042 myKsize = -(-nK // world.size) 

1043 myKrange = range(rank * myKsize, 

1044 min((rank + 1) * myKsize, nK)) 

1045 myKsize = len(myKrange) 

1046 return myKrange, myKsize 

1047 

1048 def print_initialization(self, td, eshift, gw_kn): 

1049 isl = ['----------------------------------------------------------', 

1050 f'{self.mode} Hamiltonian', 

1051 '----------------------------------------------------------', 

1052 f'Started at: {ctime()}', '', 

1053 'Atoms : ' 

1054 f'{self.gs.atoms.get_chemical_formula(mode="hill")}', 

1055 f'Ground state XC functional : {self.gs.xcname}', 

1056 f'Valence electrons : {self.gs.nvalence}', 

1057 f'Spinor calculations : {self.add_soc}', 

1058 f'Number of bands : {self.gs.bd.nbands}', 

1059 f'Number of spins : {self.gs.nspins}', 

1060 f'Number of k-points : {self.kd.nbzkpts}', 

1061 f'Number of irreducible k-points : {self.kd.nibzkpts}', 

1062 f'Number of q-points : {self.qd.nbzkpts}', 

1063 f'Number of irreducible q-points : {self.qd.nibzkpts}', ''] 

1064 

1065 for q in self.qd.ibzk_kc: 

1066 isl.append(f' q: [{q[0]:1.4f} {q[1]:1.4f} {q[2]:1.4f}]') 

1067 isl.append('') 

1068 if gw_kn is not None: 

1069 isl.append('User specified BSE bands') 

1070 isl.extend([f'Response PW cutoff : {self.ecut * Hartree} ' 

1071 f'eV', 

1072 f'Screening bands included : {self.nbands}']) 

1073 isl.extend([f'Valence bands : {self.val_m}', 

1074 f'Conduction bands : {self.con_m}']) 

1075 if eshift is not None: 

1076 isl.append(f'Scissors operator : {eshift * Hartree}' 

1077 f'eV') 

1078 isl.extend([ 

1079 f'Tamm-Dancoff approximation : {td}', 

1080 f'Number of pair orbitals : {self.nS}', 

1081 '', 

1082 f'Truncation of Coulomb kernel : {self.coulomb.truncation}']) 

1083 integrate_gamma = self.integrate_gamma.type 

1084 if self.integrate_gamma.reduced: 

1085 integrate_gamma += '2D' 

1086 isl.append( 

1087 f'Coulomb integration scheme : {integrate_gamma}') 

1088 isl.extend([ 

1089 '', 

1090 '----------------------------------------------------------', 

1091 '----------------------------------------------------------', 

1092 '', 

1093 f'Parallelization - Total number of CPUs : {world.size}', 

1094 ' Screened potential', 

1095 f' K-point/band decomposition : {world.size}', 

1096 ' Hamiltonian', 

1097 f' Pair orbital decomposition : {world.size}']) 

1098 self.context.print('\n'.join(isl)) 

1099 

1100 

1101class BSE(BSEBackend): 

1102 def __init__(self, calc=None, timer=None, txt='-', **kwargs): 

1103 """Creates the BSE object 

1104 

1105 calc: str or calculator object 

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

1107 ecut: float 

1108 Plane wave cutoff energy (eV) 

1109 nbands: int 

1110 Number of bands used for the screened interaction 

1111 valence_bands: list or integer 

1112 Valence bands used in the BSE Hamiltonian 

1113 conduction_bands: list or integer 

1114 Conduction bands used in the BSE Hamiltonian 

1115 deps_max: float or None 

1116 Maximum absolute value of transition energy for pair 

1117 to be included in the BSE Hamiltonian 

1118 add_soc: bool 

1119 If True the calculation will included non-selfconsitent SOC. 

1120 All band indices m refers to spinors, while n indices refer to 

1121 states without SOC. 

1122 scale: float 

1123 Scaling of the SOC. A value of scale=1.0 yields a proper SOC 

1124 calculation (id add_soc=True), whereas soc_scale=0 is equivalent 

1125 to having add_soc=False. 

1126 soc_tol: float 

1127 Tolerance for how many non-SOC states are included when the SOC 

1128 states are constructed (if add_soc=True). The SOC pair densities 

1129 are constructed as linear combinations of pair densities without 

1130 SOC. We include all states that contribute by more than soc_tol 

1131 to the corresponding soc eigenstates. 

1132 eshift: float 

1133 Scissors operator opening the gap (eV) 

1134 q_c: list of three floats 

1135 Wavevector in reduced units on which the response is calculated 

1136 direction: int 

1137 if q_c = [0, 0, 0] this gives the direction in cartesian 

1138 coordinates - 0=x, 1=y, 2=z 

1139 gw_kn: list / array 

1140 List or array defining the gw quasiparticle energies in eV 

1141 used in the BSE Hamiltonian. Should match k-points and 

1142 valence + conduction bands 

1143 truncation: str or None 

1144 Coulomb truncation scheme. Can be None or 2D. 

1145 integrate_gamma: dict 

1146 txt: str 

1147 txt output 

1148 mode: str 

1149 Theory level used. can be RPA TDHF or BSE. Only BSE is screened. 

1150 """ 

1151 gs, context = get_gs_and_context( 

1152 calc, txt, world=world, timer=timer) 

1153 

1154 super().__init__(gs=gs, context=context, **kwargs) 

1155 

1156 

1157def write_bse_eigenvalues(filename, mode, w_w, C_w): 

1158 with open(filename, 'w') as fd: 

1159 print('# %s eigenvalues (in eV) and weights' % mode, file=fd) 

1160 print('# Number eig weight', file=fd) 

1161 for iw, (w, C) in enumerate(zip(w_w, C_w)): 

1162 print('%8d %12.6f %12.16f' % (iw, w.real, C.real), 

1163 file=fd) 

1164 

1165 

1166def read_bse_eigenvalues(filename): 

1167 _, w_w, C_w = np.loadtxt(filename, unpack=True) 

1168 return w_w, C_w 

1169 

1170 

1171def write_spectrum(filename, w_w, A_w): 

1172 with open(filename, 'w') as fd: 

1173 for w, A in zip(w_w, A_w): 

1174 print(f'{w:.9f}, {A:.9f}', file=fd) 

1175 

1176 

1177def read_spectrum(filename): 

1178 w_w, A_w = np.loadtxt(filename, delimiter=',', 

1179 unpack=True) 

1180 return w_w, A_w 

1181 

1182 

1183@dataclass 

1184class VChi: 

1185 cd: CellDescriptor 

1186 context: ResponseContext 

1187 w_w: np.ndarray 

1188 vchi_w: np.ndarray 

1189 optical: bool 

1190 

1191 def epsilon(self): 

1192 assert self.optical 

1193 return -self.vchi_w + 1.0 

1194 

1195 def eels(self): 

1196 assert not self.optical 

1197 return -self.vchi_w.imag 

1198 

1199 def alpha(self): 

1200 assert self.optical 

1201 L = self.cd.nonperiodic_hypervolume 

1202 return -L * self.vchi_w / (4 * np.pi) 

1203 

1204 def susceptibility(self): 

1205 assert not self.optical 

1206 return self.vchi_w 

1207 

1208 def dielectric_function(self, filename='df_bse.csv'): 

1209 """Returns and writes real and imaginary part of the dielectric 

1210 function. 

1211 

1212 w_w: list of frequencies (eV) 

1213 Dielectric function is calculated at these frequencies 

1214 eta: float 

1215 Lorentzian broadening of the spectrum (eV) 

1216 filename: str 

1217 data file on which frequencies, real and imaginary part of 

1218 dielectric function is written 

1219 write_eig: str 

1220 File on which the BSE eigenvalues are written 

1221 """ 

1222 

1223 return self._hackywrite(self.epsilon(), filename) 

1224 

1225 # XXX The default filename clashes with that of dielectric function! 

1226 def eels_spectrum(self, filename='df_bse.csv'): 

1227 """Returns and writes real and imaginary part of the dielectric 

1228 function. 

1229 

1230 w_w: list of frequencies (eV) 

1231 Dielectric function is calculated at these frequencies 

1232 eta: float 

1233 Lorentzian broadening of the spectrum (eV) 

1234 filename: str 

1235 data file on which frequencies, real and imaginary part of 

1236 dielectric function is written 

1237 write_eig: str 

1238 File on which the BSE eigenvalues are written 

1239 """ 

1240 return self._hackywrite(self.eels(), filename) 

1241 

1242 def polarizability(self, filename='pol_bse.csv'): 

1243 r"""Calculate the polarizability alpha. 

1244 In 3D the imaginary part of the polarizability is related to the 

1245 dielectric function by Im(eps_M) = 4 pi * Im(alpha). In systems 

1246 with reduced dimensionality the converged value of alpha is 

1247 independent of the cell volume. This is not the case for eps_M, 

1248 which is ill defined. A truncated Coulomb kernel will always give 

1249 eps_M = 1.0, whereas the polarizability maintains its structure. 

1250 pbs should be a list of booleans giving the periodic directions. 

1251 

1252 By default, generate a file 'pol_bse.csv'. The three colomns are: 

1253 frequency (eV), Real(alpha), Imag(alpha). The dimension of alpha 

1254 is \AA to the power of non-periodic directions. 

1255 """ 

1256 return self._hackywrite(self.alpha(), filename) 

1257 

1258 def magnetic_susceptibility(self, filename='susc_+-_0_bse.csv'): 

1259 return self._hackywrite(self.susceptibility(), filename)[1] 

1260 

1261 def _hackywrite(self, array, filename): 

1262 if world.rank == 0 and filename is not None: 

1263 if array.dtype == complex: 

1264 write_response_function(filename, self.w_w, array.real, 

1265 array.imag) 

1266 else: 

1267 assert array.dtype == float 

1268 write_spectrum(filename, self.w_w, array) 

1269 

1270 world.barrier() 

1271 

1272 self.context.print('Calculation completed at:', ctime(), flush=False) 

1273 self.context.print('') 

1274 

1275 return self.w_w, array 

1276 

1277 

1278class BSEPlus: 

1279 

1280 def create_chi0_full_calculator(self): 

1281 chi0calc_full = Chi0Calculator(self.gs, self.context, 

1282 wd=self.wd, 

1283 nbands=self.rpa_nbands, 

1284 intraband=False, 

1285 hilbert=False, 

1286 eta=self.eta, 

1287 ecut=self.ecut, 

1288 eshift=self.eshift) 

1289 

1290 return chi0calc_full 

1291 

1292 def create_bse_calculator(self): 

1293 bse = BSE(self.bse_gpw, 

1294 ecut=self.ecut, 

1295 valence_bands=self.bse_valence_bands, 

1296 conduction_bands=self.bse_conduction_bands, 

1297 nbands=self.bse_nbands, 

1298 eshift=self.eshift, 

1299 mode='BSE', 

1300 truncation=self.truncation, 

1301 q_c=self.q_c, 

1302 direction=self.bse_direction, 

1303 add_soc=self.bse_add_soc, 

1304 txt='bse_calculation.txt') 

1305 

1306 return bse 

1307 

1308 def create_chi0_limited_calculator(self): 

1309 self.gs, self.context = get_gs_and_context( 

1310 self.rpa_gpw, txt=None, world=world, timer=None) 

1311 self.wd = get_frequency_descriptor( 

1312 self.w_w, gs=self.gs, nbands=self.rpa_nbands) 

1313 chi0calc_limited = Chi0Calculator(self.gs, self.context, 

1314 wd=self.wd, 

1315 nbands=slice( 

1316 self.n1_chi0, self.m2_chi0), 

1317 intraband=False, 

1318 hilbert=False, 

1319 eta=self.eta, 

1320 ecut=self.ecut, 

1321 eshift=self.eshift) 

1322 return chi0calc_limited 

1323 

1324 def get_chi_RPA(self, chi0calc, q_c, coulomb_kernel, xc_kernel, 

1325 CellDescriptor, direction): 

1326 self.chi0_data = chi0calc.calculate(q_c) 

1327 dyson_eqs = Chi0DysonEquations(self.chi0_data, coulomb_kernel, 

1328 xc_kernel, CellDescriptor) 

1329 self.v_G = coulomb_kernel.V(self.chi0_data.qpd) 

1330 chi0_wGG = dyson_eqs.get_chi0_wGG(direction) 

1331 chi0_WGG = dyson_eqs.wblocks.all_gather(chi0_wGG) 

1332 del chi0calc, dyson_eqs, chi0_wGG 

1333 return chi0_WGG 

1334 

1335 def __init__(self, 

1336 bse_gpw, 

1337 bse_valence_bands, 

1338 bse_conduction_bands, 

1339 bse_nbands, 

1340 rpa_gpw, 

1341 rpa_nbands, 

1342 w_w, 

1343 eshift=0.0, 

1344 bse_add_soc=False, 

1345 eta=0.1, 

1346 q_c=[0.0, 0.0, 0.0], 

1347 direction=0, 

1348 truncation=None, 

1349 ecut=10): 

1350 

1351 """ BSE+ calculation of chi. BSE+ offers a way to improve 

1352 the convergence of the BSE by including transitions outside 

1353 the active BSE electron-hole subspace at the RPA level in 

1354 the irreducible polarizability. It saves the chi matrix 

1355 calculated with the BSE+, BSE and RPA as npy-files. 

1356 

1357 Parameters 

1358 ---------- 

1359 bse_gpw: Path or str 

1360 Name of the calculator that the BSE calculation should be 

1361 made from (typically a fixed density calculator with less 

1362 kpts) 

1363 bse_valence_bands: range or list of integers 

1364 Number of valence bands to be included in the bse calculation 

1365 bse_conduction_bands: range or list of integers 

1366 Number of conduction bands to be included in the bse calculation 

1367 bse_nbands: integer 

1368 Number of bands used for the screened interaction 

1369 rpa_nbands: integer 

1370 Number of bands to be included in the RPA calculation. 

1371 w_w: list of floats 

1372 Dielectric function is calculated at these frequencies (eV) 

1373 eshift: float 

1374 Scissors operator opening the gap (eV) 

1375 bse_add_soc: bool 

1376 If True the calculation will included non-self-consitent SOC in the 

1377 underlying BSE calculation. SOC is not implemented in the RPA code. 

1378 eta: float 

1379 Lorentzian broadening of the spectrum (eV) 

1380 q_c: list of three floats 

1381 Wavevector in reduced units on which the response is calculated 

1382 direction: int 

1383 If q_c = [0, 0, 0] this gives the direction in cartesian 

1384 coordinates - 0=x, 1=y, 2=z 

1385 truncation: str or None 

1386 Coulomb truncation scheme. Can be None or 2D. 

1387 ecut: float 

1388 Plane wave cutoff energy (eV) 

1389 """ 

1390 

1391 self.bse_gpw = bse_gpw 

1392 self.bse_valence_bands = bse_valence_bands 

1393 self.bse_conduction_bands = bse_conduction_bands 

1394 self.bse_nbands = bse_nbands 

1395 self.rpa_gpw = rpa_gpw 

1396 self.rpa_nbands = rpa_nbands 

1397 self.w_w = w_w 

1398 self.eshift = eshift 

1399 self.bse_add_soc = bse_add_soc 

1400 self.eta = eta 

1401 self.q_c = q_c 

1402 self.bse_direction = direction 

1403 self.rpa_direction = ('x', 'y', 'z')[direction] 

1404 self.truncation = truncation 

1405 self.ecut = ecut 

1406 

1407 self.n1_BSE = self.bse_valence_bands[0] 

1408 self.m2_BSE = self.bse_conduction_bands[-1] 

1409 self.m2_chi0_full = rpa_nbands - 1 

1410 

1411 if bse_add_soc: 

1412 self.n1_chi0 = int(self.n1_BSE / 2) 

1413 self.m2_chi0 = int((self.m2_BSE + 1) / 2) 

1414 else: 

1415 self.n1_chi0 = self.n1_BSE 

1416 self.m2_chi0 = self.m2_BSE + 1 

1417 

1418 assert truncation in [None, '2D'] 

1419 

1420 assert self.m2_chi0 < self.m2_chi0_full, \ 

1421 'Large chi0 calculation should contain more ' \ 

1422 'bands than the BSE calculation' 

1423 

1424 def calculate_chi_wGG(self, optical=True, xc_kernel=None, comm=None, 

1425 bsep_name='chi_BSEPlus', 

1426 save_chi_BSE=False, save_chi_RPA=False): 

1427 

1428 # irreducibale bse chi 

1429 bse = self.create_bse_calculator() 

1430 chi_irr_BSE_WGG = bse.get_chi_wGG( 

1431 eta=self.eta, 

1432 optical=optical, 

1433 irreducible=True, 

1434 w_w=self.w_w) 

1435 del bse 

1436 

1437 # chi0 calculation with the same bands as in the bse 

1438 chi0calc_limited = self.create_chi0_limited_calculator() 

1439 coulomb_kernel = CoulombKernel.from_gs(self.gs, 

1440 truncation=self.truncation) 

1441 chi0_limited_WGG = self.get_chi_RPA(chi0calc_limited, self.q_c, 

1442 coulomb_kernel, xc_kernel, 

1443 self.gs.cd, self.rpa_direction) 

1444 

1445 # chi0 fully converged 

1446 chi0calc_full = self.create_chi0_full_calculator() 

1447 chi0_full_WGG = self.get_chi_RPA(chi0calc_full, self.q_c, 

1448 coulomb_kernel, xc_kernel, 

1449 self.gs.cd, self.rpa_direction) 

1450 

1451 if self.truncation == '2D': 

1452 pbc_c = self.gs.pbc 

1453 assert sum(pbc_c) == 2 

1454 coulomb_kernel_bare = CoulombKernel.from_gs( 

1455 self.gs, truncation=None) 

1456 v_G_bare = coulomb_kernel_bare.V(self.chi0_data.qpd, q_v=None) 

1457 self.v_G = self.v_G / v_G_bare 

1458 if optical: 

1459 v_G_bare[0] = 0.0 

1460 chi0_limited_WGG = chi0_limited_WGG * \ 

1461 v_G_bare[np.newaxis, np.newaxis, :] 

1462 chi0_full_WGG = chi0_full_WGG * v_G_bare[np.newaxis, np.newaxis, :] 

1463 chi_irr_BSE_WGG = chi_irr_BSE_WGG * \ 

1464 v_G_bare[np.newaxis, np.newaxis, :] 

1465 cell_cv = self.gs.gd.cell_cv 

1466 V = np.abs(np.linalg.det(cell_cv[~pbc_c][:, ~pbc_c])) 

1467 V *= Bohr 

1468 elif self.truncation is None and optical: 

1469 self.v_G[0] = 0.0 

1470 

1471 del self.chi0_data 

1472 

1473 if comm is None: 

1474 comm = world 

1475 nR = len(self.w_w) 

1476 self.blocks = Blocks1D(comm, nR) 

1477 

1478 chi_irr_BSE_wGG = chi_irr_BSE_WGG[self.blocks.myslice] 

1479 chi0_full_wGG = chi0_full_WGG[self.blocks.myslice] 

1480 chi0_limited_wGG = chi0_limited_WGG[self.blocks.myslice] 

1481 

1482 chi_irr_BSEPlus_wGG = \ 

1483 chi_irr_BSE_wGG - chi0_limited_wGG + chi0_full_wGG 

1484 eye = np.eye(chi_irr_BSEPlus_wGG.shape[1]) 

1485 

1486 chi_BSEPlus_wGG = \ 

1487 np.linalg.solve(eye - chi_irr_BSEPlus_wGG @ np.diag(self.v_G), 

1488 chi_irr_BSEPlus_wGG) 

1489 

1490 if self.truncation == '2D': 

1491 chi_BSEPlus_wGG *= V / (4 * np.pi) 

1492 

1493 chi_BSEPlus_WGG = self.blocks.gather(chi_BSEPlus_wGG, 0) 

1494 

1495 if world.rank == 0: 

1496 np.save(bsep_name + '.npy', chi_BSEPlus_WGG) 

1497 del chi_BSEPlus_WGG 

1498 del chi_BSEPlus_wGG, chi0_limited_wGG 

1499 

1500 if save_chi_BSE: 

1501 chi_BSE_wGG = \ 

1502 np.linalg.solve(eye - chi_irr_BSE_wGG @ np.diag(self.v_G), 

1503 chi_irr_BSE_wGG) 

1504 

1505 if self.truncation == '2D': 

1506 chi_BSE_wGG *= V / (4 * np.pi) 

1507 

1508 chi_BSE_WGG = self.blocks.gather(chi_BSE_wGG, 0) 

1509 

1510 if world.rank == 0: 

1511 np.save('chi_BSE.npy' if save_chi_BSE is True else 

1512 save_chi_BSE, chi_BSE_WGG) 

1513 del chi_BSE_WGG 

1514 del chi_BSE_wGG 

1515 

1516 if save_chi_RPA: 

1517 chi_full_wGG = \ 

1518 np.linalg.solve(eye - chi0_full_wGG @ np.diag(self.v_G), 

1519 chi0_full_wGG) 

1520 

1521 if self.truncation == '2D': 

1522 chi_full_wGG *= V / (4 * np.pi) 

1523 

1524 chi_full_WGG = self.blocks.gather(chi_full_wGG, 0) 

1525 

1526 if world.rank == 0: 

1527 np.save('chi_RPA.npy' if save_chi_RPA is True else 

1528 save_chi_RPA, chi_full_WGG) 

1529 del chi_full_WGG 

1530 del chi_full_wGG