Coverage for rhodent/calculators/dipole.py: 57%

158 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-08-01 16:57 +0000

1from __future__ import annotations 

2 

3import numpy as np 

4from typing import Any, Generator 

5from numpy.typing import NDArray 

6 

7from gpaw.tddft.units import au_to_eA, au_to_eV 

8 

9from .base import BaseObservableCalculator 

10from ..density_matrices.base import WorkMetadata 

11from ..utils import ResultKeys, Result 

12 

13 

14class DipoleCalculator(BaseObservableCalculator): 

15 

16 r""" Calculate the induced dipole moment in the time or frequency domain. 

17 

18 The induced dipole moment (i.e. the dipole moment minus the permanent 

19 component) is to first order given by 

20 

21 .. math:: 

22 

23 \delta\boldsymbol{\mu} = -2 \sum_{ia}^\text{eh} 

24 \boldsymbol{\mu}_{ia} \mathrm{Re}\:\delta\rho_{ia}, 

25 

26 where :math:`\boldsymbol{\mu}_{ia}` is the dipole matrix element of ground state Kohn-Sham 

27 pair :math:`ia` 

28 

29 .. math:: 

30 

31 \boldsymbol{\mu}_{ia} = \int \psi^{(0)}_i(\boldsymbol{r}) \boldsymbol{r} 

32 \psi^{(0)}_a(\boldsymbol{r}) \mathrm{d}\boldsymbol{r}, 

33 

34 and :math:`\delta\rho_{ia}` the induced Kohn-Sham density matrix. 

35 

36 In the frequency domain, this calculator calculates the polarizability, i.e. the Fourier 

37 transform of the dipole moment divided by the perturbation. 

38 

39 .. math:: 

40 

41 \boldsymbol{\alpha}(\omega) = -2 \sum_{ia}^\text{eh} 

42 \boldsymbol{\mu}_{ia} \frac{\mathcal{F}\left[\mathrm{Re}\:\delta\rho_{ia}\right](\omega)}{v(\omega)}. 

43 

44 The absorption spectrum in units of dipole strength function is the imaginary part 

45 of the polarizability times a prefactor 

46 

47 .. math:: 

48 

49 \boldsymbol{S}(\omega) = \frac{2\omega}{\pi} \mathrm{Im}\:\boldsymbol{\alpha}(\omega). 

50 

51 This class can also compute projections of the above on Voronoi weights :math:`w_{ia}`. 

52 

53 Parameters 

54 ---------- 

55 response 

56 Response object. 

57 voronoi 

58 Voronoi weights object. 

59 energies_occ 

60 Energy grid in units of eV for occupied levels (holes). 

61 energies_unocc 

62 Energy grid in units of eV for unoccupied levels (electrons) 

63 sigma 

64 Gaussian broadening width for energy grid in units of eV. 

65 times 

66 Compute induced dipole in the time domain, for these times (or as close to them as possible). 

67 In units of as. 

68 

69 May not be used together with :attr:`frequencies` or :attr:`frequency_broadening`. 

70 pulses 

71 Compute induced dipole in the time domain, in response to these pulses. 

72 If none, then no pulse convolution is performed. 

73 

74 May not be used together with :attr:`frequencies` or :attr:`frequency_broadening`. 

75 frequencies 

76 Compute polarizability in the frequency domain, for these frequencies. In units of eV. 

77 

78 May not be used together with :attr:`times` or :attr:`pulses`. 

79 frequency_broadening 

80 Compute polarizability in the frequency domain, with Gaussian broadening of this width. 

81 In units of eV. 

82 

83 May not be used together with :attr:`times` or :attr:`pulses`. 

84 """ 

85 

86 def get_result_keys(self, 

87 yield_total_ia: bool = False, 

88 yield_proj_ia: bool = False, 

89 yield_total_ou: bool = False, 

90 yield_proj_ou: bool = False, 

91 decompose_v: bool = True, 

92 v: int | None = None, 

93 ) -> ResultKeys: 

94 r""" Get the keys that each result will contain, and dimensions thereof. 

95 

96 Parameters 

97 ---------- 

98 yield_total_ia 

99 The results should include the total dipole contributions in the electron-hole basis 

100 :math:`-2 \boldsymbol{\mu}_{ia} \mathrm{Re}\:\delta\rho_{ia}`. 

101 yield_proj_ia 

102 The results should include projections of the dipole contributions in the electron-hole basis 

103 :math:`-2 \boldsymbol{\mu}_{ia} \mathrm{Re}\:\delta\rho_{ia} w_{ia}`. 

104 yield_total_ou 

105 The results should include the total dipole contributions on the energy grid. 

106 yield_proj_ou 

107 The results should include projections of the dipole contributions on the energy grid. 

108 decompose_v 

109 The results should include the dipole moment and/or its contributions decomposed 

110 by Cartesian direction. 

111 v 

112 If not None, then the results should include the v:th Cartesian component 

113 of the dipole moment and its contributions. 

114 """ 

115 assert decompose_v or v is not None 

116 

117 nI = self.nproj 

118 imin, imax, amin, amax = self.ksd.ialims() 

119 ni = imax - imin + 1 

120 na = amax - amin + 1 

121 no = len(self.energies_occ) 

122 nu = len(self.energies_unocc) 

123 

124 # Time domain: save dipole moment, which is real 

125 # Frequency domain: save polarizability, which is complex 

126 dtype = float if self._is_time_density_matrices else complex 

127 

128 resultkeys = ResultKeys() 

129 if v is not None: 

130 resultkeys.add_key('dm', dtype=dtype) 

131 if yield_total_ia: 

132 resultkeys.add_key('dm_ia', (ni, na), dtype=dtype) 

133 if yield_total_ou: 

134 resultkeys.add_key('dm_ou', (no, nu), dtype=dtype) 

135 

136 resultkeys.add_key('dm_proj_II', (nI, nI), dtype=dtype) 

137 if yield_proj_ia: 

138 resultkeys.add_key('dm_occ_proj_Iia', (nI, ni, na), dtype=dtype) 

139 resultkeys.add_key('dm_unocc_proj_Iia', (nI, ni, na), dtype=dtype) 

140 if yield_proj_ou: 

141 resultkeys.add_key('dm_occ_proj_Iou', (nI, no, nu), dtype=dtype) 

142 resultkeys.add_key('dm_unocc_proj_Iou', (nI, no, nu), dtype=dtype) 

143 if decompose_v: 

144 resultkeys.add_key('dm_v', 3, dtype=dtype) 

145 if yield_total_ia: 

146 resultkeys.add_key('dm_iav', (ni, na, 3), dtype=dtype) 

147 if yield_total_ou: 

148 resultkeys.add_key('dm_ouv', (no, nu, 3), dtype=dtype) 

149 

150 resultkeys.add_key('dm_proj_IIv', (nI, nI, 3), dtype=dtype) 

151 if yield_proj_ia: 

152 resultkeys.add_key('dm_occ_proj_Iiav', (nI, ni, na, 3), dtype=dtype) 

153 resultkeys.add_key('dm_unocc_proj_Iiav', (nI, ni, na, 3), dtype=dtype) 

154 if yield_proj_ou: 

155 resultkeys.add_key('dm_occ_proj_Iouv', (nI, no, nu, 3), dtype=dtype) 

156 resultkeys.add_key('dm_unocc_proj_Iouv', (nI, no, nu, 3), dtype=dtype) 

157 

158 return resultkeys 

159 

160 def icalculate(self, 

161 yield_total_ia: bool = False, 

162 yield_proj_ia: bool = False, 

163 yield_total_ou: bool = False, 

164 yield_proj_ou: bool = False, 

165 decompose_v: bool = True, 

166 v: int | None = None, 

167 ) -> Generator[tuple[WorkMetadata, Result], None, None]: 

168 """ Iteratively calculate dipole contributions. 

169 

170 Parameters 

171 ---------- 

172 yield_total_ia 

173 The results should include the total dipole contributions in the electron-hole basis 

174 :math:`-2 \\boldsymbol{\\mu}_{ia} \\mathrm{Re}\\:\\delta\\rho_{ia}`. 

175 yield_proj_ia 

176 The results should include projections of the dipole contributions in the electron-hole basis 

177 :math:`-2 \\boldsymbol{\\mu}_{ia} \\mathrm{Re}\\:\\delta\\rho_{ia} w_{ia}`. 

178 yield_total_ou 

179 The results should include the total dipole contributions on the energy grid. 

180 yield_proj_ou 

181 The results should include projections of the dipole contributions on the energy grid. 

182 decompose_v 

183 The results should include the dipole moment and/or its contributions decomposed 

184 by Cartesian direction. 

185 v 

186 If not `None`, then the results should include the v:th Cartesian component \ 

187 of the dipole moment and its contributions. 

188 

189 Yields 

190 ------ 

191 Tuple (work, result) on the root rank of the calculation communicator. \ 

192 Does not yield on non-root ranks of the calculation communicator. 

193 

194 work 

195 An object representing the metadata (time, frequency or pulse) for the work done. 

196 result 

197 Object containg the calculation results for this time, frequency or pulse. 

198 """ 

199 include_energy_dists = (yield_total_ou or yield_proj_ou) 

200 if include_energy_dists: 

201 assert self.sigma is not None 

202 need_entire_matrix = (yield_total_ou or yield_proj_ou 

203 or yield_total_ia or yield_proj_ia 

204 or self.nproj > 0) 

205 

206 # Time domain: dipole moment in units of eÅ 

207 # Frequency domain: polarizability in units of (eÅ)**2/eV 

208 unit = au_to_eA if self._is_time_density_matrices else au_to_eA**2 / au_to_eV 

209 dm0_iav = -2 * np.moveaxis(np.array([self.ksd.M_ia_from_M_p(dm_p) for dm_p in self.ksd.dm_vp]), 0, -1) * unit 

210 

211 # List all keys that this method computes 

212 # This is necessary to safely send and receive data across ranks 

213 resultkeys = self.get_result_keys(yield_total_ia=yield_total_ia, 

214 yield_proj_ia=yield_proj_ia, 

215 yield_total_ou=yield_total_ou, 

216 yield_proj_ou=yield_proj_ou, 

217 decompose_v=decompose_v, 

218 v=v, 

219 ) 

220 

221 self._read_weights_diagonal() 

222 

223 # Iterate over the pulses and times 

224 for work, dm in self.density_matrices: 

225 if dm.rank > 0: 

226 continue 

227 

228 self.log.start('calculate') 

229 

230 if need_entire_matrix: 

231 if self._is_time_density_matrices: 

232 # Real dipole 

233 dm_iav = dm0_iav * dm.rho_ia[..., None].real 

234 else: 

235 # Complex polarizability 

236 dm_iav = dm0_iav * dm.rho_ia[..., None] 

237 dm_v = np.sum(dm_iav, axis=(0, 1)) 

238 

239 if yield_total_ou: 

240 dm_ouv = self.broaden_ia2ou(dm_iav) 

241 else: 

242 if self._is_time_density_matrices: 

243 # Real dipole 

244 dm_v = np.einsum('iav,ia->v', dm0_iav, dm.rho_ia.real, optimize=True) 

245 else: 

246 # Complex polarizability 

247 dm_v = np.einsum('iav,ia->v', dm0_iav, dm.rho_ia, optimize=True) 

248 

249 result = Result() 

250 if decompose_v: 

251 result['dm_v'] = dm_v 

252 if yield_total_ia: 

253 result['dm_iav'] = dm_iav 

254 if yield_total_ou: 

255 result['dm_ouv'] = dm_ouv 

256 if v is not None: 

257 result['dm'] = dm_v[v] 

258 if yield_total_ia: 

259 result['dm_ia'] = dm_iav[..., v] 

260 if yield_total_ou: 

261 result['dm_ou'] = dm_ouv[..., v] 

262 

263 # Initialize the remaining empty arrays 

264 result.create_all_empty(resultkeys) 

265 

266 # Iterate over projections 

267 for iI, weight_n in enumerate(self._iterate_weights_diagonal): 

268 assert weight_n is not None 

269 weight_i = weight_n[self.flti] 

270 weight_a = weight_n[self.flta] 

271 

272 for iI2, weight2_n in enumerate(self._iterate_weights_diagonal): 

273 assert weight2_n is not None 

274 weight2_a = weight2_n[self.flta] 

275 

276 dm_proj_v = np.einsum('iav,i,a->v', dm_iav, weight_i, weight2_a, optimize=True) 

277 result['dm_proj_IIv'][iI, iI2] = dm_proj_v 

278 

279 if yield_proj_ia or yield_proj_ou: 

280 dm_occ_proj_iav = dm_iav * weight_i[:, None, None] 

281 dm_unocc_proj_iav = dm_iav * weight_a[None, :, None] 

282 

283 if yield_proj_ou: 

284 dm_occ_proj_ouv = self.broaden_ia2ou(dm_occ_proj_iav) 

285 dm_unocc_proj_ouv = self.broaden_ia2ou(dm_unocc_proj_iav) 

286 

287 if decompose_v: 

288 if yield_proj_ia: 

289 result['dm_occ_proj_Iiav'][iI] = dm_occ_proj_iav 

290 result['dm_unocc_proj_Iiav'][iI] = dm_unocc_proj_iav 

291 if yield_proj_ou: 

292 result['dm_occ_proj_Iouv'][iI] = dm_occ_proj_ouv 

293 result['dm_unocc_proj_Iouv'][iI] = dm_unocc_proj_ouv 

294 if v is not None: 

295 if yield_proj_ia: 

296 result['dm_occ_proj_Iia'][iI] = dm_occ_proj_iav[..., v] 

297 result['dm_unocc_proj_Iia'][iI] = dm_unocc_proj_iav[..., v] 

298 if yield_proj_ou: 

299 dm_occ_proj_ouv = self.broaden_ia2ou(dm_occ_proj_iav) 

300 dm_unocc_proj_ouv = self.broaden_ia2ou(dm_unocc_proj_iav) 

301 

302 if decompose_v: 

303 if yield_proj_ia: 

304 result['dm_occ_proj_Iiav'][iI] = dm_occ_proj_iav 

305 result['dm_unocc_proj_Iiav'][iI] = dm_unocc_proj_iav 

306 if yield_proj_ou: 

307 result['dm_occ_proj_Iouv'][iI] = dm_occ_proj_ouv 

308 result['dm_unocc_proj_Iouv'][iI] = dm_unocc_proj_ouv 

309 if v is not None: 

310 if yield_proj_ia: 

311 result['dm_occ_proj_Iia'][iI] = dm_occ_proj_iav[..., v] 

312 result['dm_unocc_proj_Iia'][iI] = dm_unocc_proj_iav[..., v] 

313 if yield_proj_ou: 

314 result['dm_occ_proj_Iou'][iI] = dm_occ_proj_ouv[..., v] 

315 result['dm_unocc_proj_Iou'][iI] = dm_unocc_proj_ouv[..., v] 

316 

317 self.log_parallel(f'Calculated and broadened dipoles contributions in {self.log.elapsed("calculate"):.2f}s ' 

318 f'for {work.desc}', flush=True) 

319 

320 yield work, result 

321 

322 if self.calc_comm.rank == 0: 

323 self.log_parallel('Finished calculating dipoles contributions', flush=True) 

324 

325 @property 

326 def _need_derivatives_real_imag(self) -> tuple[list[int], bool, bool]: 

327 # Time domain: We only need the real part of the density matrix. 

328 # Frequency domain: We need the (complex) Fourier transform of 

329 # the real part of the density matrix. 

330 return ([0], True, False) 

331 

332 @property 

333 def _voronoi_shapes(self) -> dict[str, tuple[int, ...]]: 

334 nI = self.voronoi.nproj 

335 if nI == 0: 

336 return {} 

337 Nn = self.voronoi.nn 

338 # Diagonal weights only 

339 return {'diagonal': (nI, Nn)} 

340 

341 @property 

342 def oscillator_strength_prefactor(self) -> NDArray[np.float64]: 

343 """ Conversion factor from polarizability to dipole strength function. 

344 

345 """ 

346 from gpaw.tddft.units import eA_to_au, eV_to_au 

347 # Convert polarizability (eÅ**2/eV) to atomic units 

348 # Multiply by 2 omega / pi in atomic units 

349 # Convert to units of dipole strength function (1/eV) 

350 prefactor_w = 2 * (self.frequencies * eV_to_au) / np.pi * eA_to_au ** 2 

351 return prefactor_w 

352 

353 def calculate_and_write(self, 

354 out_fname: str, 

355 write_extra: dict[str, Any] = dict(), 

356 include_tcm: bool = False, 

357 only_one_pulse: bool = True): 

358 """ Calculate induced dipole moments and transition contribution maps. 

359 

360 Dipole moments and contributions are saved in a numpy archive if 

361 the file extension is `.npz` or in an ULM file if the file extension is `.ulm`. 

362 

363 Parameters 

364 ---------- 

365 out_fname 

366 File name of the resulting data file. 

367 write_extra 

368 Dictionary of extra key-value pairs to write to the data file. 

369 include_tcm 

370 Whether the transition contribution map (TCM) to the dipole moments should be computed and saved. 

371 only_one_pulse 

372 If False, group arrays by pulse. Only valid in time domain. 

373 """ 

374 from ..writers.tcm import DipoleWriter 

375 from ..writers.writer import FrequencyResultsCollector, TimeResultsCollector, PulseConvolutionResultsCollector 

376 

377 cls = ((TimeResultsCollector if only_one_pulse else PulseConvolutionResultsCollector) 

378 if self._is_time_density_matrices else FrequencyResultsCollector) 

379 calc_kwargs = dict(yield_total_ou=include_tcm, yield_proj_ou=include_tcm) 

380 

381 out_fname = str(out_fname) 

382 if out_fname[-4:] == '.npz': 

383 writer = DipoleWriter(cls(self, calc_kwargs), only_one_pulse=only_one_pulse) 

384 writer.calculate_and_save_npz(out_fname, write_extra=write_extra) 

385 elif out_fname[-4:] == '.ulm': 

386 writer = DipoleWriter(cls(self, calc_kwargs, exclude=['dm_ouv']), only_one_pulse=only_one_pulse) 

387 writer.calculate_and_save_ulm(out_fname, write_extra=write_extra) 

388 else: 

389 raise ValueError(f'output-file must have ending .npz or .ulm, is {out_fname}')