Coverage for gpaw/new/rttddft.py: 31%

148 statements  

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

1from __future__ import annotations 

2 

3from typing import Generator, NamedTuple 

4 

5import numpy as np 

6from numpy.linalg import solve 

7 

8from ase.units import Bohr, Hartree 

9 

10from gpaw.external import ExternalPotential, ConstantElectricField 

11from gpaw.typing import Vector 

12from gpaw.mpi import world 

13from gpaw.new.ase_interface import ASECalculator 

14from gpaw.new.calculation import DFTState, DFTCalculation 

15from gpaw.new.lcao.hamiltonian import HamiltonianMatrixCalculator 

16from gpaw.new.lcao.wave_functions import LCAOWaveFunctions 

17from gpaw.new.gpw import read_gpw 

18from gpaw.new.pot_calc import PotentialCalculator 

19from gpaw.tddft.units import asetime_to_autime, autime_to_asetime, au_to_eA 

20from gpaw.utilities.timing import nulltimer 

21 

22 

23class TDAlgorithm: 

24 

25 def kick(self, 

26 state: DFTState, 

27 pot_calc: PotentialCalculator, 

28 dm_calc: HamiltonianMatrixCalculator): 

29 raise NotImplementedError() 

30 

31 def propagate(self, 

32 time_step: float, 

33 state: DFTState, 

34 pot_calc: PotentialCalculator, 

35 ham_calc: HamiltonianMatrixCalculator): 

36 raise NotImplementedError() 

37 

38 def get_description(self): 

39 return self.__class__.__name__ 

40 

41 

42def propagate_wave_functions_numpy(source_C_nM: np.ndarray, 

43 target_C_nM: np.ndarray, 

44 S_MM: np.ndarray, 

45 H_MM: np.ndarray, 

46 dt: float): 

47 SjH_MM = S_MM + (0.5j * dt) * H_MM 

48 target_C_nM[:] = source_C_nM @ SjH_MM.conj().T 

49 target_C_nM[:] = solve(SjH_MM.T, target_C_nM.T).T 

50 

51 

52class ECNAlgorithm(TDAlgorithm): 

53 

54 def kick(self, 

55 state: DFTState, 

56 pot_calc: PotentialCalculator, 

57 dm_calc: HamiltonianMatrixCalculator): 

58 """Propagate wavefunctions by delta-kick. 

59 

60 :: 

61 

62 0+ 

63 ^ -1 / ^ -1 

64 U(0+, 0) = T exp[-iS | δ(τ) V (r) dτ ] = T exp[-iS V (r)] 

65 / ext ext 

66 0 

67 

68 (1) Calculate propagator U(0+, 0) 

69 (2) Update wavefunctions ψ_n(0+) ← U(0+, 0) ψ_n(0) 

70 (3) Update density and hamiltonian H(0+) 

71 

72 Parameters 

73 ---------- 

74 state 

75 Current state of the wave functions, that is to be updated 

76 pot_calc 

77 The potential calculator 

78 dm_calc 

79 Dipole moment operator calculator, which contains the dipole moment 

80 operator 

81 """ 

82 for wfs in state.ibzwfs: 

83 assert isinstance(wfs, LCAOWaveFunctions) 

84 V_MM = dm_calc.calculate_matrix(wfs) 

85 

86 # Phi_n <- U(0+, 0) Phi_n 

87 nkicks = 10 

88 for i in range(nkicks): 

89 propagate_wave_functions_numpy(wfs.C_nM.data, wfs.C_nM.data, 

90 wfs.S_MM.data, 

91 V_MM.data, 1 / nkicks) 

92 # Update density 

93 state.density.update(state.ibzwfs) 

94 

95 # Calculate Hamiltonian H(t+dt) = H[n[Phi_n]] 

96 state.potential, state.energies, _ = pot_calc.calculate( 

97 state.density, state.potential.vHt_x) 

98 

99 def propagate(self, 

100 time_step: float, 

101 state: DFTState, 

102 pot_calc: PotentialCalculator, 

103 ham_calc: HamiltonianMatrixCalculator): 

104 """ One explicit Crank-Nicolson propagation step, i.e. 

105 

106 (1) Calculate propagator U[H(t)] 

107 (2) Update wavefunctions ψ_n(t+dt) ← U[H(t)] ψ_n(t) 

108 (3) Update density and hamiltonian H(t+dt) 

109 """ 

110 for wfs in state.ibzwfs: 

111 assert isinstance(wfs, LCAOWaveFunctions) 

112 H_MM = ham_calc.calculate_matrix(wfs) 

113 

114 # Phi_n <- U[H(t)] Phi_n 

115 propagate_wave_functions_numpy(wfs.C_nM.data, wfs.C_nM.data, 

116 wfs.S_MM.data, 

117 H_MM.data, time_step) 

118 # Update density 

119 state.density.update(state.ibzwfs) 

120 

121 # Calculate Hamiltonian H(t+dt) = H[n[Phi_n]] 

122 state.potential, state.energies, _ = pot_calc.calculate( 

123 state.density, state.potential.vHt_x) 

124 

125 

126class RTTDDFTHistory: 

127 

128 kick_strength: Vector | None # Kick strength in atomic units 

129 niter: int # Number of propagation steps 

130 time: float # Simulation time in atomic units 

131 

132 def __init__(self): 

133 """Object that keeps track of the RT-TDDFT history, that is 

134 

135 - Has a kick been performed? 

136 - The number of propagation states performed 

137 """ 

138 self.kick_strength = None 

139 self.niter = 0 

140 self.time = 0.0 

141 

142 def absorption_kick(self, 

143 kick_strength: Vector): 

144 """ Store the kick strength in history 

145 

146 At most one kick can be done, and it must happen before any 

147 propagation steps 

148 

149 Parameters 

150 ---------- 

151 kick_strength 

152 Strength of the kick in atomic units 

153 """ 

154 assert self.niter == 0, 'Cannot kick if already propagated' 

155 assert self.kick_strength is None, 'Cannot kick if already kicked' 

156 self.kick_strength = np.array(kick_strength, dtype=float) 

157 

158 def propagate(self, 

159 time_step: float) -> float: 

160 """ Increment the number of propagation steps and simulation time 

161 in history 

162 

163 Parameters 

164 ---------- 

165 time_step 

166 Time step in atomic units 

167 

168 Returns 

169 ------- 

170 The new simulation time in atomic units 

171 """ 

172 self.niter += 1 

173 self.time += time_step 

174 

175 return self.time 

176 

177 def todict(self): 

178 absorption_kick = self.absorption_kick 

179 if absorption_kick is not None: 

180 absorption_kick = absorption_kick.tolist() 

181 return {'niter': self.niter, 'time': self.time, 

182 'absorption_kick': absorption_kick} 

183 

184 

185class RTTDDFTResult(NamedTuple): 

186 

187 """ Results are stored in atomic units, but displayed to the user in 

188 ASE units 

189 """ 

190 

191 time: float # Time in atomic units 

192 dipolemoment: Vector # Dipole moment in atomic units 

193 

194 def __repr__(self): 

195 timestr = f'{self.time * autime_to_asetime:.3f} Å√(u/eV)' 

196 dmstr = ', '.join([f'{dm * au_to_eA:10.4g}' 

197 for dm in self.dipolemoment]) 

198 dmstr = f'[{dmstr}]' 

199 

200 return (f'{self.__class__.__name__}: ' 

201 f'(time: {timestr}, dipolemoment: {dmstr} eÅ)') 

202 

203 

204class RTTDDFT: 

205 def __init__(self, 

206 state: DFTState, 

207 pot_calc: PotentialCalculator, 

208 hamiltonian, 

209 history: RTTDDFTHistory, 

210 propagator: TDAlgorithm | None = None): 

211 if propagator is None: 

212 propagator = ECNAlgorithm() 

213 

214 self.state = state 

215 self.pot_calc = pot_calc 

216 self.propagator = propagator 

217 self.hamiltonian = hamiltonian 

218 self.history = history 

219 

220 self.kick_ext: ExternalPotential | None = None 

221 

222 # Dipole moment operators in each Cartesian direction 

223 self.dm_operator_c: list[HamiltonianMatrixCalculator] | None = None 

224 

225 self.timer = nulltimer 

226 self.log = print 

227 

228 self.ham_calc = hamiltonian.create_hamiltonian_matrix_calculator(state) 

229 

230 @classmethod 

231 def from_dft_calculation(cls, 

232 calc: ASECalculator | DFTCalculation, 

233 propagator: TDAlgorithm | None = None): 

234 

235 if isinstance(calc, DFTCalculation): 

236 dft = calc 

237 else: 

238 assert calc.dft is not None 

239 dft = calc.dft 

240 

241 state = dft.get_state() 

242 pot_calc = dft.pot_calc 

243 hamiltonian = dft.scf_loop.hamiltonian 

244 history = RTTDDFTHistory() 

245 

246 return cls(state, pot_calc, hamiltonian, propagator=propagator, 

247 history=history) 

248 

249 @classmethod 

250 def from_dft_file(cls, 

251 filepath: str, 

252 propagator: TDAlgorithm | None = None): 

253 _, dft, params, builder = read_gpw(filepath, 

254 log='-', 

255 comm=world, 

256 dtype=complex) 

257 

258 state = dft.get_state() 

259 pot_calc = dft.pot_calc 

260 hamiltonian = builder.create_hamiltonian_operator() 

261 history = RTTDDFTHistory() 

262 

263 return cls(state, pot_calc, hamiltonian, propagator=propagator, 

264 history=history) 

265 

266 def absorption_kick(self, 

267 kick_strength: Vector): 

268 """Kick with a weak electric field. 

269 

270 Parameters 

271 ---------- 

272 kick_strength 

273 Strength of the kick in atomic units 

274 """ 

275 with self.timer('Kick'): 

276 kick_strength = np.array(kick_strength, dtype=float) 

277 self.history.absorption_kick(kick_strength) 

278 

279 magnitude = np.sqrt(np.sum(kick_strength**2)) 

280 direction = kick_strength / magnitude 

281 dirstr = [f'{d:.4f}' for d in direction] 

282 

283 self.log('---- Applying absorption kick') 

284 self.log(f'---- Magnitude: {magnitude:.8f} Hartree/Bohr') 

285 self.log(f'---- Direction: {dirstr}') 

286 

287 # Create Hamiltonian object for absorption kick 

288 cef = ConstantElectricField(magnitude * Hartree / Bohr, direction) 

289 

290 # Propagate kick 

291 self.kick(cef) 

292 

293 def kick(self, 

294 ext: ExternalPotential): 

295 """Kick with any external potential. 

296 

297 Note that unless this function is called by absorption_kick, the kick 

298 is not logged in history 

299 

300 Parameters 

301 ---------- 

302 ext 

303 External potential 

304 """ 

305 with self.timer('Kick'): 

306 self.log('---- Applying kick') 

307 self.log(f'---- {ext}') 

308 

309 dm_operator_calc = self.hamiltonian.create_kick_matrix_calculator( 

310 self.state, ext, self.pot_calc) 

311 

312 self.kick_ext = ext 

313 

314 # Propagate kick 

315 self.propagator.kick(state=self.state, 

316 pot_calc=self.pot_calc, 

317 dm_calc=dm_operator_calc) 

318 

319 def ipropagate(self, 

320 time_step: float = 10.0, 

321 maxiter: int = 2000, 

322 ) -> Generator[RTTDDFTResult, None, None]: 

323 """Propagate the electronic system. 

324 

325 Parameters 

326 ---------- 

327 time_step 

328 Time step in ASE time units Å√(u/eV) 

329 iterations 

330 Number of propagation steps 

331 """ 

332 

333 time_step = time_step * asetime_to_autime 

334 

335 for iteration in range(maxiter): 

336 self.propagator.propagate(time_step, 

337 state=self.state, 

338 pot_calc=self.pot_calc, 

339 ham_calc=self.ham_calc) 

340 time = self.history.propagate(time_step) 

341 # TODO This seems to be broken 

342 # dipolemoment = self.state.density.calculate_dipole_moment( 

343 # self.pot_calc.relpos_ac) 

344 dipolemoment_xv = [ 

345 self.calculate_dipole_moment(wfs) # type: ignore 

346 for wfs in self.state.ibzwfs] 

347 dipolemoment_v = np.sum(dipolemoment_xv, axis=0) 

348 result = RTTDDFTResult(time=time, dipolemoment=dipolemoment_v) 

349 yield result 

350 

351 def calculate_dipole_moment(self, 

352 wfs: LCAOWaveFunctions) -> np.ndarray: 

353 """ Calculates the dipole moment 

354 

355 The dipole moment is calculated as the expectation value of the 

356 dipole moment operator, i.e. the trace of it times the density matrix:: 

357 

358 d = - Σ ρ d 

359 μν μν νμ 

360 

361 """ 

362 if self.dm_operator_c is None: 

363 self.dm_operator_c = [] 

364 

365 # Create external potentials in each direction 

366 ext_c = [ConstantElectricField(Hartree / Bohr, dir) 

367 for dir in np.eye(3)] 

368 dm_operator_c = [self.hamiltonian.create_kick_matrix_calculator( 

369 self.state, ext, self.pot_calc) for ext in ext_c] 

370 self.dm_operator_c = dm_operator_c 

371 

372 dm_v = np.zeros(3) 

373 for c, dm_operator in enumerate(self.dm_operator_c): 

374 rho_MM = wfs.calculate_density_matrix() 

375 dm_MM = dm_operator.calculate_matrix(wfs) 

376 dm = - np.einsum('MN,NM->', rho_MM, dm_MM.data) 

377 assert np.abs(dm.imag) < 1e-20 

378 dm_v[c] = dm.real 

379 

380 return dm_v