Coverage for gpaw/lrtddft2/ks_singles.py: 94%

211 statements  

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

1import os 

2 

3import io 

4 

5import numpy as np 

6 

7import ase.units 

8 

9import gpaw.mpi 

10 

11from gpaw.utilities.tools import coordinates 

12from gpaw.utilities.tools import pick 

13from gpaw.utilities import pack_density 

14 

15from gpaw.fd_operators import Gradient 

16 

17 

18class KohnShamSingleExcitation: 

19 def __init__(self, gs_calc, kpt_ind, occ_ind, unocc_ind): 

20 self.calc = gs_calc 

21 self.kpt_ind = kpt_ind 

22 self.spin_ind = self.calc.wfs.kpt_u[self.kpt_ind].s 

23 self.occ_ind = occ_ind 

24 self.unocc_ind = unocc_ind 

25 self.energy_diff = None 

26 self.pop_diff = None 

27 self.dip_mom_r = None 

28 self.magn_mom = None 

29 self.pair_density = None 

30 

31 # Calculates pair density of (i,p) transition (and given kpt) 

32 # dnt_Gp: pair density without compensation charges on coarse grid 

33 # dnt_gp: pair density without compensation charges on fine grid (XC) 

34 # drhot_gp: pair density with compensation charges on fine grid (poisson) 

35 def calculate_pair_density(self, dnt_Gip, dnt_gip, drhot_gip): 

36 if self.pair_density is None: 

37 # FIXME: USE EXISTING PairDensity method 

38 self.pair_density = LRiPairDensity(self.calc.density) 

39 self.pair_density.initialize(self.calc.wfs.kpt_u[self.kpt_ind], 

40 self.occ_ind, self.unocc_ind) 

41 self.pair_density.get(dnt_Gip, dnt_gip, drhot_gip) 

42 

43 def __str__(self): 

44 if self.dip_mom_r is not None and self.dip_mom_v is not None\ 

45 and self.magn_mom is not None: 

46 str = "# KS single excitation from state %05d to state %05d:"\ 

47 " dE_pi = %18.12lf, f_pi = %18.12lf,"\ 

48 " dmr_ip = (%18.12lf, %18.12lf, %18.12lf),"\ 

49 " dmv_ip = (%18.12lf, %18.12lf, %18.12lf),"\ 

50 " dmm_ip = %18.12lf" \ 

51 % (self.occ_ind, 

52 self.unocc_ind, 

53 self.energy_diff, 

54 self.pop_diff, 

55 self.dip_mom_r[0], self.dip_mom_r[1], self.dip_mom_r[2], 

56 self.dip_mom_v[0], self.dip_mom_v[1], self.dip_mom_v[2], 

57 self.magn_mom) 

58 elif self.energy_diff is not None and self.pop_diff is not None: 

59 str = "# KS single excitation from state %05d to state %05d:"\ 

60 " dE_pi = %18.12lf, f_pi = %18.12lf" \ 

61 % (self.occ_ind, 

62 self.unocc_ind, 

63 self.energy_diff, 

64 self.pop_diff) 

65 elif self.occ_ind is not None and self.unocc_ind is not None: 

66 str = "# KS single excitation from state %05d to state %05d" \ 

67 % (self.occ_ind, self.unocc_ind) 

68 else: 

69 raise RuntimeError("Uninitialized KSSingle") 

70 return str 

71 

72 

73class KohnShamSingles: 

74 def __init__(self, basefilename, gs_calc, kpt_index, min_occ, max_occ, 

75 min_unocc, max_unocc, max_energy_diff, min_pop_diff, lr_comms, 

76 txt): 

77 self.basefilename = basefilename 

78 self.calc = gs_calc 

79 self.min_occ = min_occ 

80 self.max_occ = max_occ 

81 self.min_unocc = min_unocc 

82 self.max_unocc = max_unocc 

83 self.max_energy_diff = max_energy_diff 

84 self.min_pop_diff = min_pop_diff 

85 self.kpt_ind = kpt_index 

86 self.lr_comms = lr_comms 

87 self.txt = txt 

88 

89 self.kss_list = None 

90 self.kss_list_ready = False 

91 

92 self.kss_prop = None 

93 self.kss_prop_ready = False 

94 

95 def update_list(self): 

96 # shorthands 

97 eps_n = self.calc.wfs.kpt_u[self.kpt_ind].eps_n # eigen energies 

98 f_n = self.calc.wfs.kpt_u[self.kpt_ind].f_n # occupations 

99 

100 # Create Kohn-Sham single excitation list with energy filter 

101 old_kss_list = self.kss_list # save old list for later 

102 self.kss_list = [] # create a completely new list 

103 # Occupied loop 

104 for i in range(self.min_occ, self.max_occ + 1): 

105 # Unoccupied loop 

106 for p in range(self.min_unocc, self.max_unocc + 1): 

107 deps_pi = eps_n[p] - eps_n[i] # energy diff 

108 df_ip = f_n[i] - f_n[p] # population diff 

109 # Filter it 

110 if (np.abs(deps_pi) <= self.max_energy_diff 

111 and df_ip > self.min_pop_diff): 

112 # i, p, deps, df, mur, muv, magn 

113 kss = KohnShamSingleExcitation(self.calc, self.kpt_ind, i, 

114 p) 

115 kss.energy_diff = deps_pi 

116 kss.pop_diff = df_ip 

117 self.kss_list.append(kss) 

118 

119 # Sort by energy diff: 

120 self.kss_list = sorted(self.kss_list, key=lambda x: x.energy_diff) 

121 

122 # Remove old transitions and add new 

123 # BUT only add to the end of the list (otherwise lower triangle 

124 # matrix is not filled completely) 

125 if old_kss_list is not None: 

126 new_kss_list = self.kss_list # required list 

127 self.kss_list = [] # final list with correct order 

128 # Old list first 

129 # If in new and old lists 

130 for kss_o in old_kss_list: 

131 found = False 

132 for kss_n in new_kss_list: 

133 if (kss_o.occ_ind == kss_n.occ_ind 

134 and kss_o.unocc_ind == kss_n.unocc_ind): 

135 found = True 

136 break 

137 if found: 

138 self.kss_list.append(kss_o) # Found, add to final list 

139 else: 

140 pass # else drop 

141 

142 # Now old transitions which are not in new list where dropped 

143 

144 # If only in new list 

145 app_list = [] 

146 for kss_n in new_kss_list: 

147 found = False 

148 for kss in self.kss_list: 

149 if (kss.occ_ind == kss_n.occ_ind 

150 and kss.unocc_ind == kss_n.unocc_ind): 

151 found = True 

152 break 

153 if not found: 

154 app_list.append(kss_n) # Not found, add to final list 

155 else: 

156 pass # else skip to avoid duplicates 

157 

158 # Create the final list 

159 self.kss_list += app_list 

160 

161 self.txt.write('Number of electron-hole pairs: %d\n' % 

162 len(self.kss_list)) 

163 self.txt.write('Maximum energy difference: %6.3lf\n' % 

164 (self.kss_list[-1].energy_diff * ase.units.Hartree)) 

165 

166 # Prevent repeated work 

167 self.kss_list_ready = True 

168 

169 def read(self): 

170 # Read KS_singles file if exists 

171 # occ_index | unocc index | energy diff | population diff | 

172 # dmx | dmy | dmz | magnx | magny | magnz 

173 kss_file = self.basefilename + '.KS_singles' 

174 if os.path.exists(kss_file) and os.path.isfile(kss_file): 

175 self.kss_list = [] 

176 

177 # root reads and broadcasts 

178 # a bit ugly and slow but works 

179 data = None 

180 if self.lr_comms.parent_comm.rank == 0: 

181 with open(kss_file, 'r', 1024 * 1024) as fd: 

182 data = fd.read() 

183 data = gpaw.mpi.broadcast_string(data, 

184 root=0, 

185 comm=self.lr_comms.parent_comm) 

186 

187 # just parse 

188 for line in io.StringIO(data): 

189 line = line.split() 

190 i, p = int(line[0]), int(line[1]) 

191 ediff, fdiff = float(line[2]), float(line[3]) 

192 dm = np.array([float(line[4]), float(line[5]), float(line[6])]) 

193 mm = np.array([float(line[7]), float(line[8]), float(line[9])]) 

194 kss = KohnShamSingleExcitation(self.calc, self.kpt_ind, i, p) 

195 kss.energy_diff = ediff 

196 kss.pop_diff = fdiff 

197 kss.dip_mom_r = dm 

198 kss.magn_mom = mm 

199 # assert self.index_of_kss(i,p) is None, 'KS transition'\ 

200 # '%d->%d found twice in KS_singles files.' % (i,p) 

201 self.kss_list.append(kss) 

202 

203 # if none read 

204 if len(self.kss_list) <= 0: 

205 self.kss_list = None 

206 

207 # Calculate dipole moment and magnetic moment for noninteracting 

208 # Kohn-Sham transitions 

209 # FIXME: Do it in parallel, currently doing repeated work 

210 # Should distribute kss_list over eh_comm 

211 def calculate(self): 

212 # Check that kss_list is up-to-date 

213 if not self.kss_list_ready: 

214 self.update_list() 

215 self.kss_prop_ready = False 

216 

217 # Check if we already have properties for all singles 

218 if self.kss_prop_ready: 

219 return 

220 self.kss_prop_ready = True 

221 for kss_ip in self.kss_list: 

222 if kss_ip.dip_mom_r is None or kss_ip.magn_mom is None: 

223 self.kss_prop_ready = False 

224 break 

225 # If had, done 

226 if self.kss_prop_ready: 

227 return 

228 

229 # self.timer.start('Calculate KS properties') 

230 

231 # Init pair densities 

232 dnt_Gip = self.calc.wfs.gd.empty() 

233 dnt_gip = self.calc.density.finegd.empty() 

234 drhot_gip = self.calc.density.finegd.empty() 

235 

236 # Init gradients of wfs 

237 grad_psit2_G = [ 

238 self.calc.wfs.gd.empty(), 

239 self.calc.wfs.gd.empty(), 

240 self.calc.wfs.gd.empty() 

241 ] 

242 

243 # Init gradient operators 

244 grad = [] 

245 dtype = pick(self.calc.wfs.kpt_u[self.kpt_ind].psit_nG, 0).dtype 

246 for c in range(3): 

247 grad.append(Gradient(self.calc.wfs.gd, c, dtype=dtype, n=2)) 

248 

249 # Coordinate vector r 

250 R0 = 0.5 * np.diag(self.calc.wfs.gd.cell_cv) 

251 # R0 = np.array([0.0,0.0,0.0]) # old_version 

252 # print 'R0', R0 

253 r_cG, r2_G = coordinates(self.calc.wfs.gd, origin=R0) 

254 r_cg, r2_g = coordinates(self.calc.density.finegd, origin=R0) 

255 

256 # Loop over all KS single excitations 

257 # 

258 # Transition dipole moment, mu_ip = <p| (-e r) |i> 

259 # Magnetic transition dipole, m_ip = -(1/2c) <i|L|p> 

260 # For total m_0I = -m_I0 = -(m_0I)^*, but not for m_ip (right?) 

261 # R_0I = Im[mu_0I * m_0I] 

262 # mu_ip^0I = omega_0I^(-1/2) D_ip S^(-1/2) F_0I 

263 # m_ip^0I = -omega_0I^(+1/2) M_ip C^-1 S^(+1/2) F_0I 

264 # 

265 # S_ip,ip = - (eps_p - eps_i) / (n_p - n_i) (note: n_p < n_i) 

266 # C_ip,ip = 1 / (n_p - n_i) 

267 # 

268 # See: 

269 # WIREs Comput Mol Sci 2012, 2: 150-166 doi: 10.1002/wcms.55 

270 # J. Chem. Phys., Vol. 116, 6930 (2002) 

271 for kss_ip in self.kss_list: 

272 # If have dipole moment and magnetic moment, already done and skip 

273 if (kss_ip.dip_mom_r is not None and kss_ip.magn_mom is not None): 

274 continue 

275 

276 # Transition dipole moment, mu_ip = <p| (-r) |i> 

277 kss_ip.calculate_pair_density(dnt_Gip, dnt_gip, drhot_gip) 

278 # kss_ip.dip_mom_r 

279 # = self.calc.density.finegd.calculate_dipole_moment(drhot_gip) 

280 # kss_ip.dip_mom_r 

281 # = self.calc.density.finegd.calculate_dipole_moment(drhot_gip) 

282 kss_ip.dip_mom_r = np.zeros(3) 

283 kss_ip.dip_mom_r[0] = -self.calc.density.finegd.integrate( 

284 r_cg[0] * drhot_gip) 

285 kss_ip.dip_mom_r[1] = -self.calc.density.finegd.integrate( 

286 r_cg[1] * drhot_gip) 

287 kss_ip.dip_mom_r[2] = -self.calc.density.finegd.integrate( 

288 r_cg[2] * drhot_gip) 

289 

290 # Magnetic transition dipole, 

291 # m_ip = -(1/2c) <i|L|p> = i/2c <i|r x p|p> 

292 # see Autschbach et al., J. Chem. Phys., 116, 6930 (2002) 

293 

294 # Gradients 

295 for c in range(3): 

296 grad[c].apply(kss_ip.pair_density.psit2_G, grad_psit2_G[c], 

297 self.calc.wfs.kpt_u[self.kpt_ind].phase_cd) 

298 

299 # <psi1|r x grad|psi2> 

300 # i j k 

301 # x y z = (y pz - z py)i + (z px - x pz)j + (x py - y px) 

302 # px py pz 

303 rxnabla_g = np.zeros(3) 

304 rxnabla_g[0] = self.calc.wfs.gd.integrate( 

305 kss_ip.pair_density.psit1_G * 

306 (r_cG[1] * grad_psit2_G[2] - r_cG[2] * grad_psit2_G[1])) 

307 rxnabla_g[1] = self.calc.wfs.gd.integrate( 

308 kss_ip.pair_density.psit1_G * 

309 (r_cG[2] * grad_psit2_G[0] - r_cG[0] * grad_psit2_G[2])) 

310 rxnabla_g[2] = self.calc.wfs.gd.integrate( 

311 kss_ip.pair_density.psit1_G * 

312 (r_cG[0] * grad_psit2_G[1] - r_cG[1] * grad_psit2_G[0])) 

313 

314 # augmentation contributions to magnetic moment 

315 # <psi1| r x nabla |psi2> = <psi1| (r-Ra+Ra) x nabla |psi2> 

316 # = <psi1| (r-Ra) x nabla |psi2> + Ra x <psi1| nabla |psi2> 

317 rxnabla_a = np.zeros(3) 

318 # <psi1| (r-Ra) x nabla |psi2> 

319 for a, P_ni in self.calc.wfs.kpt_u[self.kpt_ind].P_ani.items(): 

320 Pi_i = P_ni[kss_ip.occ_ind] 

321 Pp_i = P_ni[kss_ip.unocc_ind] 

322 rxnabla_iiv = self.calc.wfs.setups[a].rxnabla_iiv 

323 for c in range(3): 

324 for i1, Pi in enumerate(Pi_i): 

325 for i2, Pp in enumerate(Pp_i): 

326 rxnabla_a[c] += Pi * Pp * rxnabla_iiv[i1, i2, c] 

327 

328 self.calc.wfs.gd.comm.sum(rxnabla_a) # sum up from different procs 

329 

330 # Ra x <psi1| nabla |psi2> 

331 Rxnabla_a = np.zeros(3) 

332 for a, P_ni in self.calc.wfs.kpt_u[self.kpt_ind].P_ani.items(): 

333 Pi_i = P_ni[kss_ip.occ_ind] 

334 Pp_i = P_ni[kss_ip.unocc_ind] 

335 nabla_iiv = self.calc.wfs.setups[a].nabla_iiv 

336 Ra = (self.calc.atoms[a].position / ase.units.Bohr) - R0 

337 for i1, Pi in enumerate(Pi_i): 

338 for i2, Pp in enumerate(Pp_i): 

339 # (y pz - z py)i + (z px - x pz)j + (x py - y px)k 

340 Rxnabla_a[0] += Pi * Pp * ( 

341 Ra[1] * nabla_iiv[i1, i2, 2] - 

342 Ra[2] * nabla_iiv[i1, i2, 1]) 

343 Rxnabla_a[1] += Pi * Pp * ( 

344 Ra[2] * nabla_iiv[i1, i2, 0] - 

345 Ra[0] * nabla_iiv[i1, i2, 2]) 

346 Rxnabla_a[2] += Pi * Pp * ( 

347 Ra[0] * nabla_iiv[i1, i2, 1] - 

348 Ra[1] * nabla_iiv[i1, i2, 0]) 

349 

350 self.calc.wfs.gd.comm.sum(Rxnabla_a) # sum up from different procs 

351 

352 # print (kss_ip.occ_ind, kss_ip.unocc_ind), 

353 # kss_ip.dip_mom_r, rxnabla_g, rxnabla_a, Rxnabla_a 

354 

355 # m_ip = -1/2c <i|r x p|p> = i/2c <i|r x nabla|p> 

356 # just imaginary part!!! 

357 kss_ip.magn_mom = ase.units.alpha / 2. * (rxnabla_g + rxnabla_a + 

358 Rxnabla_a) 

359 

360 # Wait... to avoid io problems, and write KS_singles file 

361 self.lr_comms.parent_comm.barrier() 

362 if self.lr_comms.parent_comm.rank == 0: 

363 self.kss_file = open(self.basefilename + '.KS_singles', 'w') 

364 for kss_ip in self.kss_list: 

365 format = '%08d %08d %18.12lf %18.12lf ' 

366 format += '%18.12lf %18.12lf %18.12lf ' 

367 format += '%18.12lf %18.12lf %18.12lf\n' 

368 self.kss_file.write( 

369 format % 

370 (kss_ip.occ_ind, kss_ip.unocc_ind, kss_ip.energy_diff, 

371 kss_ip.pop_diff, kss_ip.dip_mom_r[0], kss_ip.dip_mom_r[1], 

372 kss_ip.dip_mom_r[2], kss_ip.magn_mom[0], 

373 kss_ip.magn_mom[1], kss_ip.magn_mom[2])) 

374 self.kss_file.close() 

375 self.lr_comms.parent_comm.barrier() 

376 

377 self.kss_prop_ready = True # avoid repeated work 

378 # self.timer.stop('Calculate KS properties') 

379 

380 

381# Small utility class 

382 

383# FIXME: USE EXISTING METHOD 

384class LRiPairDensity: 

385 """Pair density calculator class.""" 

386 

387 def __init__(self, density): 

388 """Initialization needs density instance""" 

389 self.density = density 

390 

391 def initialize(self, kpt, n1, n2): 

392 """Set wave function indices.""" 

393 self.n1 = n1 

394 self.n2 = n2 

395 self.spin = kpt.s 

396 self.P_ani = kpt.P_ani 

397 self.psit1_G = pick(kpt.psit_nG, n1) 

398 self.psit2_G = pick(kpt.psit_nG, n2) 

399 

400 def get(self, nt_G, nt_g, rhot_g): 

401 """Get pair densities. 

402 

403 nt_G 

404 Pair density without compensation charges on the coarse grid 

405 

406 nt_g 

407 Pair density without compensation charges on the fine grid 

408 

409 rhot_g 

410 Pair density with compensation charges on the fine grid 

411 """ 

412 # Coarse grid product 

413 np.multiply(self.psit1_G.conj(), self.psit2_G, nt_G) 

414 # Interpolate to fine grid 

415 self.density.interpolator.apply(nt_G, nt_g) 

416 

417 # Determine the compensation charges for each nucleus 

418 Q_aL = {} 

419 for a, P_ni in self.P_ani.items(): 

420 assert P_ni.dtype == float 

421 # Generate density matrix 

422 P1_i = P_ni[self.n1] 

423 P2_i = P_ni[self.n2] 

424 D_ii = np.outer(P1_i.conj(), P2_i) 

425 # Pack with pack_density as it is used in the scalar product with 

426 # the symmetric array Delta_pL 

427 D_p = pack_density(D_ii) 

428 # FIXME: CHECK THIS D_p = pack_density(D_ii, tolerance=1e30) 

429 

430 # Determine compensation charge coefficients: 

431 Q_aL[a] = np.dot(D_p, self.density.setups[a].Delta_pL) 

432 

433 # Add compensation charges 

434 rhot_g[:] = nt_g[:] 

435 self.density.ghat.add(rhot_g, Q_aL) 

436 # print 'dens', self.density.finegd.integrate(rhot_g)