Coverage for gpaw/tddft/__init__.py: 79%

273 statements  

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

1"""This module implements a class for (true) time-dependent density 

2functional theory calculations. 

3 

4""" 

5import time 

6import warnings 

7from math import log 

8 

9import numpy as np 

10 

11from gpaw.calculator import GPAW 

12from gpaw.mixer import DummyMixer 

13from gpaw.preconditioner import Preconditioner 

14from gpaw.tddft.units import (attosec_to_autime, autime_to_attosec, 

15 aufrequency_to_eV) 

16from gpaw.tddft.utils import MultiBlas 

17from gpaw.tddft.solvers import create_solver 

18from gpaw.tddft.propagators import \ 

19 create_propagator, \ 

20 AbsorptionKick 

21from gpaw.tddft.tdopers import \ 

22 TimeDependentHamiltonian, \ 

23 TimeDependentOverlap, \ 

24 TimeDependentWaveFunctions, \ 

25 TimeDependentDensity, \ 

26 AbsorptionKickHamiltonian 

27from gpaw.wavefunctions.fd import FD 

28 

29from gpaw.tddft.spectrum import photoabsorption_spectrum 

30from gpaw.lcaotddft.dipolemomentwriter import DipoleMomentWriter 

31from gpaw.lcaotddft.magneticmomentwriter import MagneticMomentWriter 

32from gpaw.lcaotddft.restartfilewriter import RestartFileWriter 

33 

34 

35__all__ = ['TDDFT', 'photoabsorption_spectrum', 

36 'DipoleMomentWriter', 'MagneticMomentWriter', 

37 'RestartFileWriter'] 

38 

39 

40# T^-1 

41# Bad preconditioner 

42class KineticEnergyPreconditioner: 

43 def __init__(self, gd, kin, dtype): 

44 self.preconditioner = Preconditioner(gd, kin, dtype) 

45 self.preconditioner.allocate() 

46 

47 def apply(self, kpt, psi, psin): 

48 for i in range(len(psi)): 

49 psin[i][:] = self.preconditioner(psi[i], kpt.phase_cd, None, None) 

50 

51 

52# S^-1 

53class InverseOverlapPreconditioner: 

54 """Preconditioner for TDDFT.""" 

55 def __init__(self, overlap): 

56 self.overlap = overlap 

57 

58 def apply(self, kpt, psi, psin): 

59 self.overlap.apply_inverse(psi, psin, kpt) 

60# ^^^^^^^^^^ 

61 

62 

63class FDTDDFTMode(FD): 

64 def __call__(self, *args, **kwargs): 

65 reuse_wfs_method = kwargs.pop('reuse_wfs_method', None) 

66 assert reuse_wfs_method is None 

67 return TimeDependentWaveFunctions(self.nn, *args, **kwargs) 

68 

69 

70class TDDFT(GPAW): 

71 """Time-dependent density functional theory calculation based on GPAW. 

72 

73 This class is the core class of the time-dependent density functional 

74 theory implementation and is the only class which a user has to use. 

75 """ 

76 

77 def __init__(self, filename: str, *, 

78 td_potential: object = None, 

79 calculate_energy: bool = True, 

80 propagator: dict = None, 

81 solver: dict = None, 

82 tolerance: float = None, # deprecated 

83 parallel: dict = None, 

84 communicator: object = None, 

85 txt: str = '-'): 

86 """ 

87 Parameters 

88 ---------- 

89 filename 

90 File containing ground state or time-dependent state to propagate. 

91 td_potential 

92 Function class for the time-dependent potential. Must have a method 

93 ``strength(time)`` which returns the strength of 

94 the linear potential to each direction as a vector of three floats. 

95 calculate_energy 

96 Whether to calculate energy during propagation. 

97 propagator 

98 Time propagator for the Kohn-Sham wavefunctions. 

99 solver 

100 The iterative linear equations solver for propagator. 

101 tolerance 

102 Deprecated. Do not use this, but use solver dictionary instead. 

103 Tolerance for the linear solver. 

104 parallel 

105 Parallelization options 

106 communicator 

107 MPI communicator 

108 txt 

109 Text output 

110 """ 

111 # Default values 

112 if propagator is None: 

113 propagator = dict(name='SICN') 

114 if solver is None: 

115 solver = dict(name='CSCG', tolerance=1e-8) 

116 

117 assert filename is not None 

118 

119 # For communicating with observers 

120 self.action = '' 

121 

122 self.time = 0.0 

123 self.kick_strength = np.array([0.0, 0.0, 0.0], dtype=float) 

124 self.kick_gauge = '' 

125 self.niter = 0 

126 self.dm_file = None # XXX remove and use observer instead 

127 

128 # Parallelization dictionary should default to strided bands 

129 # but it does not work XXX 

130 # self.default_parallel = GPAW.default_parallel.copy() 

131 # self.default_parallel['stridebands'] = True 

132 

133 self.default_parameters = GPAW.default_parameters.copy() 

134 self.default_parameters['mixer'] = DummyMixer() 

135 self.default_parameters['experimental']['reuse_wfs_method'] = None 

136 

137 # NB: TDDFT restart files contain additional information which 

138 # will override the initial settings for time/kick/niter. 

139 GPAW.__init__(self, filename, parallel=parallel, 

140 communicator=communicator, txt=txt) 

141 if len(self.symmetry.op_scc) > 1: 

142 raise ValueError('Symmetries are not allowed for TDDFT. ' 

143 'Run the ground state calculation with ' 

144 'symmetry={"point_group": False}.') 

145 

146 assert isinstance(self.wfs, TimeDependentWaveFunctions) 

147 assert isinstance(self.wfs.overlap, TimeDependentOverlap) 

148 

149 self.set_positions() 

150 

151 # Don't be too strict 

152 self.density.charge_eps = 1e-5 

153 

154 self.rank = self.wfs.world.rank 

155 

156 self.calculate_energy = calculate_energy 

157 if self.hamiltonian.xc.name.startswith('GLLB'): 

158 self.log('GLLB model potential. Not updating energy.') 

159 self.calculate_energy = False 

160 

161 # Time-dependent variables and operators 

162 self.td_hamiltonian = TimeDependentHamiltonian(self.wfs, self.spos_ac, 

163 self.hamiltonian, 

164 td_potential) 

165 self.td_density = TimeDependentDensity(self) 

166 

167 # Solver for linear equations 

168 if isinstance(solver, str): 

169 solver = dict(name=solver) 

170 if tolerance is not None: 

171 warnings.warn( 

172 "Please specify the solver tolerance using dictionary " 

173 "solver={'name': name, 'tolerance': tolerance}. " 

174 "Confirm the used tolerance from the output file. " 

175 "The old syntax will throw an error in the future.", 

176 FutureWarning) 

177 solver.update(tolerance=tolerance) 

178 self.solver = create_solver(solver) 

179 

180 # Preconditioner 

181 # No preconditioner as none good found 

182 self.preconditioner = None # TODO! check out SSOR preconditioning 

183 # self.preconditioner = InverseOverlapPreconditioner(self.overlap) 

184 # self.preconditioner = KineticEnergyPreconditioner( 

185 # self.wfs.gd, self.td_hamiltonian.hamiltonian.kin, complex) 

186 

187 # Time propagator 

188 if isinstance(propagator, str): 

189 propagator = dict(name=propagator) 

190 self.propagator = create_propagator(propagator) 

191 

192 self.hpsit = None 

193 self.eps_tmp = None 

194 self.mblas = MultiBlas(self.wfs.gd) 

195 

196 self.tddft_initialized = False 

197 

198 def tddft_init(self): 

199 if self.tddft_initialized: 

200 return 

201 

202 self.log('') 

203 self.log('') 

204 self.log('------------------------------------------') 

205 self.log(' Time-propagation TDDFT ') 

206 self.log('------------------------------------------') 

207 self.log('') 

208 

209 self.log('Charge epsilon:', self.density.charge_eps) 

210 

211 # Density mixer 

212 self.td_density.density.set_mixer(DummyMixer()) 

213 

214 # Solver 

215 self.solver.initialize(self.wfs.gd, self.timer) 

216 self.log('Solver:', self.solver.todict()) 

217 

218 # Preconditioner 

219 self.log('Preconditioner:', self.preconditioner) 

220 

221 # Propagator 

222 self.propagator.initialize(self.td_density, self.td_hamiltonian, 

223 self.wfs.overlap, self.solver, 

224 self.preconditioner, 

225 self.wfs.gd, self.timer) 

226 self.log('Propagator:', self.propagator.todict()) 

227 

228 # Parallelization 

229 wfs = self.wfs 

230 if self.rank == 0: 

231 if wfs.kd.comm.size > 1: 

232 if wfs.nspins == 2: 

233 self.log('Parallelization Over Spin') 

234 

235 if wfs.gd.comm.size > 1: 

236 self.log('Using Domain Decomposition: %d x %d x %d' % 

237 tuple(wfs.gd.parsize_c)) 

238 

239 if wfs.bd.comm.size > 1: 

240 self.log('Parallelization Over bands on %d Processors' % 

241 wfs.bd.comm.size) 

242 self.log('States per processor =', wfs.bd.mynbands) 

243 

244 # Restarting an FDTD run generates hamiltonian.fdtd_poisson, which 

245 # now overwrites hamiltonian.poisson 

246 if hasattr(self.hamiltonian, 'fdtd_poisson'): 

247 self.hamiltonian.poisson = self.hamiltonian.fdtd_poisson 

248 self.hamiltonian.poisson.set_grid_descriptor(self.density.finegd) 

249 

250 # For electrodynamics mode 

251 if self.hamiltonian.poisson.get_description() == 'FDTD+TDDFT': 

252 self.hamiltonian.poisson.set_density(self.density) 

253 self.hamiltonian.poisson.print_messages(self.log) 

254 self.log.flush() 

255 

256 # Update density and Hamiltonian 

257 self.propagator.update_time_dependent_operators(self.time) 

258 

259 # XXX remove dipole moment handling and use observer instead 

260 self._dm_args0 = (self.density.finegd.integrate(self.density.rhot_g), 

261 self.calculate_dipole_moment()) 

262 

263 # Call observers 

264 self.action = 'init' 

265 self.call_observers(self.niter) 

266 

267 self.tddft_initialized = True 

268 

269 def create_wave_functions(self, mode, *args, **kwargs): 

270 mode = FDTDDFTMode(mode.nn, mode.interpolation, True) 

271 GPAW.create_wave_functions(self, mode, *args, **kwargs) 

272 

273 def read(self, filename): 

274 reader = GPAW.read(self, filename) 

275 if 'tddft' in reader: 

276 r = reader.tddft 

277 self.time = r.time 

278 self.niter = r.niter 

279 self.kick_strength = r.kick_strength 

280 

281 def _write(self, writer, mode): 

282 GPAW._write(self, writer, mode) 

283 w = writer.child('tddft') 

284 w.write(time=self.time, 

285 niter=self.niter, 

286 kick_strength=self.kick_strength) 

287 

288 def propagate(self, time_step: float, iterations: int, 

289 dipole_moment_file: str = None, # deprecated 

290 restart_file: str = None, # deprecated 

291 dump_interval: int = None, # deprecated 

292 ): 

293 """Propagates wavefunctions. 

294 

295 Parameters 

296 ---------- 

297 time_step 

298 Time step in attoseconds (10^-18 s). 

299 iterations 

300 Number of iterations. 

301 dipole_moment_file 

302 Deprecated. Do not use this. 

303 Name of the data file where to the time-dependent dipole 

304 moment is saved. 

305 restart_file 

306 Deprecated. Do not use this. 

307 Name of the restart file. 

308 dump_interval 

309 Deprecated. Do not use this. 

310 After how many iterations restart data is dumped. 

311 """ 

312 self.tddft_init() 

313 

314 if self.propagator.todict()['name'] in ['EFSICN', 'EFSICN_HGH']: 

315 msg = ("You are using propagator for Ehrenfest dynamics. " 

316 "Please use regular propagator.") 

317 raise RuntimeError(msg) 

318 

319 def warn_deprecated(parameter, observer): 

320 warnings.warn( 

321 f"The {parameter} parameter is deprecated. " 

322 f"Please use {observer} observer instead. " 

323 "The old syntax will throw an error in the future.", 

324 FutureWarning) 

325 

326 if dipole_moment_file is not None: 

327 warn_deprecated('dipole_moment_file', 'DipoleMomentWriter') 

328 

329 if restart_file is not None: 

330 warn_deprecated('restart_file', 'RestartFileWriter') 

331 

332 if dump_interval is not None: 

333 warn_deprecated('dump_interval', 'RestartFileWriter') 

334 else: 

335 dump_interval = 100 

336 

337 if self.rank == 0: 

338 self.log() 

339 self.log('Starting time: %7.2f as' 

340 % (self.time * autime_to_attosec)) 

341 self.log('Time step: %7.2f as' % time_step) 

342 header = """\ 

343 Simulation Total log10 Iterations: 

344 Time time Energy (eV) Norm Propagator""" 

345 self.log() 

346 self.log(header) 

347 

348 # Convert to atomic units 

349 time_step = time_step * attosec_to_autime 

350 

351 if dipole_moment_file is not None: 

352 self.initialize_dipole_moment_file(dipole_moment_file) 

353 

354 # Set these as class properties for use of observers 

355 self.time_step = time_step 

356 self.dump_interval = dump_interval # XXX remove, deprecated 

357 self.restart_file = restart_file # XXX remove, deprecated 

358 

359 niterpropagator = 0 

360 self.maxiter = self.niter + iterations 

361 

362 # FDTD requires extra care 

363 if self.hamiltonian.poisson.get_description() == 'FDTD+TDDFT': 

364 self.hamiltonian.poisson.set_time(self.time) 

365 self.hamiltonian.poisson.set_time_step(self.time_step) 

366 

367 # The propagate calculation_mode causes classical part to evolve 

368 # in time when self.hamiltonian.poisson.solve(...) is called 

369 self.hamiltonian.poisson.set_calculation_mode('propagate') 

370 

371 # During each time step, self.hamiltonian.poisson.solve may be 

372 # called several times (depending on the used propagator). 

373 # Using the attached observer one ensures that actual propagation 

374 # takes place only once. This is because the FDTDPoissonSolver 

375 # changes the calculation_mode from propagate to 

376 # something else when the propagation is finished. 

377 self.attach(self.hamiltonian.poisson.set_calculation_mode, 1, 

378 'propagate') 

379 

380 self.timer.start('Propagate') 

381 while self.niter < self.maxiter: 

382 norm = self.density.finegd.integrate(self.density.rhot_g) 

383 

384 # Write dipole moment at every iteration 

385 if dipole_moment_file is not None: 

386 if self._dm_args0 is not None: 

387 self.update_dipole_moment_file(*self._dm_args0) 

388 self._dm_args0 = None 

389 else: 

390 dm = self.calculate_dipole_moment() 

391 self.update_dipole_moment_file(norm, dm) 

392 

393 # Propagate the Kohn-Shame wavefunctions a single timestep 

394 niterpropagator = self.propagator.propagate(self.time, time_step) 

395 self.time += time_step 

396 self.niter += 1 

397 

398 # print output (energy etc.) every 10th iteration 

399 if self.niter % 10 == 0: 

400 self.get_td_energy() 

401 

402 T = time.localtime() 

403 if self.rank == 0: 

404 iter_text = 'iter: %3d %02d:%02d:%02d %11.2f' \ 

405 ' %13.6f %9.1f %10d' 

406 self.log(iter_text % 

407 (self.niter, T[3], T[4], T[5], 

408 self.time * autime_to_attosec, 

409 self.Etot * aufrequency_to_eV, 

410 log(abs(norm) + 1e-16) / log(10), 

411 niterpropagator)) 

412 

413 self.log.flush() 

414 

415 # Call registered callback functions 

416 self.action = 'propagate' 

417 self.call_observers(self.niter) 

418 

419 # Write restart data 

420 if restart_file is not None and self.niter % dump_interval == 0: 

421 self.write(restart_file, 'all') 

422 if self.rank == 0: 

423 print('Wrote restart file.') 

424 print(self.niter, ' iterations done. Current time is ', 

425 self.time * autime_to_attosec, ' as.') 

426 

427 self.timer.stop('Propagate') 

428 

429 # Write final results and close dipole moment file 

430 if dipole_moment_file is not None: 

431 # TODO final iteration is propagated, but nothing is updated 

432 # norm = self.density.finegd.integrate(self.density.rhot_g) 

433 # self.finalize_dipole_moment_file(norm) 

434 self.finalize_dipole_moment_file() 

435 

436 # Finalize FDTDPoissonSolver 

437 if self.hamiltonian.poisson.get_description() == 'FDTD+TDDFT': 

438 self.hamiltonian.poisson.finalize_propagation() 

439 

440 if restart_file is not None: 

441 self.write(restart_file, 'all') 

442 

443 def initialize_dipole_moment_file(self, dipole_moment_file): 

444 if self.rank == 0: 

445 if self.dm_file is not None and not self.dm_file.closed: 

446 raise RuntimeError('Dipole moment file is already open') 

447 

448 if self.time == 0.0: 

449 mode = 'w' 

450 else: 

451 # We probably continue from restart 

452 mode = 'a' 

453 

454 self.dm_file = open(dipole_moment_file, mode) 

455 

456 # If the dipole moment file is empty, add a header 

457 if self.dm_file.tell() == 0: 

458 header = '# Kick = [%22.12le, %22.12le, %22.12le]\n' \ 

459 % (self.kick_strength[0], self.kick_strength[1], 

460 self.kick_strength[2]) 

461 header += '# %15s %15s %22s %22s %22s\n' \ 

462 % ('time', 'norm', 'dmx', 'dmy', 'dmz') 

463 self.dm_file.write(header) 

464 self.dm_file.flush() 

465 

466 def calculate_dipole_moment(self): 

467 dm = self.density.finegd.calculate_dipole_moment(self.density.rhot_g) 

468 if self.hamiltonian.poisson.get_description() == 'FDTD+TDDFT': 

469 dm += self.hamiltonian.poisson.get_classical_dipole_moment() 

470 return dm 

471 

472 def update_dipole_moment_file(self, norm, dm): 

473 if self.rank == 0: 

474 line = '%20.8lf %20.8le %22.12le %22.12le %22.12le\n' \ 

475 % (self.time, norm, dm[0], dm[1], dm[2]) 

476 self.dm_file.write(line) 

477 self.dm_file.flush() 

478 

479 def finalize_dipole_moment_file(self, norm=None): 

480 if norm is not None: 

481 dm = self.calculate_dipole_moment() 

482 self.update_dipole_moment_file(norm, dm) 

483 

484 if self.rank == 0: 

485 self.dm_file.close() 

486 self.dm_file = None 

487 

488 def update_eigenvalues(self): 

489 

490 kpt_u = self.wfs.kpt_u 

491 if self.hpsit is None: 

492 self.hpsit = self.wfs.gd.zeros(len(kpt_u[0].psit_nG), 

493 dtype=complex) 

494 if self.eps_tmp is None: 

495 self.eps_tmp = np.zeros(len(kpt_u[0].eps_n), 

496 dtype=complex) 

497 

498 # self.Eband = sum_i <psi_i|H|psi_j> 

499 for kpt in kpt_u: 

500 self.td_hamiltonian.apply(kpt, kpt.psit_nG, self.hpsit, 

501 calculate_P_ani=False) 

502 self.mblas.multi_zdotc(self.eps_tmp, kpt.psit_nG, 

503 self.hpsit, len(kpt_u[0].psit_nG)) 

504 self.eps_tmp *= self.wfs.gd.dv 

505 kpt.eps_n[:] = self.eps_tmp.real 

506 

507 H = self.td_hamiltonian.hamiltonian 

508 

509 # Nonlocal 

510 self.Enlkin = H.xc.get_kinetic_energy_correction() 

511 

512 # PAW 

513 e_band = self.wfs.calculate_band_energy() 

514 self.Ekin = H.e_kinetic0 + e_band + self.Enlkin 

515 self.e_coulomb = H.e_coulomb 

516 self.Eext = H.e_external 

517 self.Ebar = H.e_zero 

518 self.Exc = H.e_xc 

519 self.Etot = self.Ekin + self.e_coulomb + self.Ebar + self.Exc 

520 

521 def get_td_energy(self): 

522 """Calculate the time-dependent total energy""" 

523 self.tddft_init() 

524 

525 if not self.calculate_energy: 

526 self.Etot = 0.0 

527 return 0.0 

528 

529 self.wfs.overlap.update(self.wfs) 

530 self.td_density.update() 

531 self.td_hamiltonian.update(self.td_density.get_density(), 

532 self.time) 

533 self.update_eigenvalues() 

534 

535 return self.Etot 

536 

537 def set_absorbing_boundary(self, absorbing_boundary): 

538 self.td_hamiltonian.set_absorbing_boundary(absorbing_boundary) 

539 

540 # exp(ip.r) psi 

541 def absorption_kick(self, kick_strength): 

542 """Delta absorption kick for photoabsorption spectrum. 

543 

544 Parameters 

545 ---------- 

546 kick_strength 

547 Strength of the kick in atomic units 

548 """ 

549 self.tddft_init() 

550 

551 if self.rank == 0: 

552 self.log('Delta kick =', kick_strength) 

553 

554 self.kick_strength = np.array(kick_strength) 

555 

556 abs_kick_hamiltonian = AbsorptionKickHamiltonian( 

557 self.wfs, self.spos_ac, 

558 np.array(kick_strength, float)) 

559 abs_kick = AbsorptionKick(self.wfs, abs_kick_hamiltonian, 

560 self.wfs.overlap, self.solver, 

561 self.preconditioner, self.wfs.gd, self.timer) 

562 abs_kick.kick() 

563 

564 # Kick the classical part, if it is present 

565 if self.hamiltonian.poisson.get_description() == 'FDTD+TDDFT': 

566 self.hamiltonian.poisson.set_kick(kick=self.kick_strength) 

567 

568 # Update density and Hamiltonian 

569 self.propagator.update_time_dependent_operators(self.time) 

570 

571 # Call observers after kick 

572 self.action = 'kick' 

573 self.call_observers(self.niter)