Coverage for gpaw/lcaotddft/qed.py: 91%

128 statements  

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

1from __future__ import annotations 

2 

3import warnings 

4from abc import ABC, abstractmethod 

5 

6import numpy as np 

7from ase.units import alpha, Hartree, Bohr 

8from gpaw.external import ConstantElectricField 

9from gpaw.lcaotddft.hamiltonian import KickHamiltonian 

10 

11 

12def create_environment(environment='waveguide', **kwargs): 

13 if isinstance(environment, Environment): 

14 assert not kwargs, 'kwargs must not be given here' 

15 return environment 

16 

17 if isinstance(environment, dict): 

18 assert not kwargs, 'please do not give kwargs together with dict' 

19 kwargs = dict(environment) 

20 environment = kwargs.pop('environment', 'waveguide') 

21 

22 if environment == 'waveguide': 

23 return WaveguideEnvironment(**kwargs) 

24 

25 raise ValueError(f'Unknown environment {environment}') 

26 

27 

28def forward_finite_difference(coefficients: list[int], 

29 data_tx: np.ndarray): 

30 """ Calculate derivatives by forward difference. 

31 

32 The data corresponding to the last time is repeated several times, 

33 in order to obtain a result of the same shape as the original array. 

34 

35 Parameters 

36 ---------- 

37 coefficients 

38 List of coefficients. 

39 Check e.g. https://en.wikipedia.org/wiki/Finite_difference_coefficient 

40 data_tx 

41 Array of data, the first axis being the time axis. 

42 

43 """ 

44 N = len(data_tx) 

45 

46 padded_shape = (N + len(coefficients) - 1, ) + data_tx.shape[1:] 

47 padded_data_tx = np.zeros(padded_shape, dtype=data_tx.dtype) 

48 padded_data_tx[:len(coefficients)] = data_tx[:1] 

49 padded_data_tx[len(coefficients):] = data_tx[1:] 

50 

51 result_tx = np.zeros_like(data_tx) 

52 

53 for start, coefficient in enumerate(coefficients): 

54 result_tx[:] += coefficient * padded_data_tx[start:start + N] 

55 

56 return result_tx 

57 

58 

59def calculate_first_derivative(timestep, data_tx): 

60 """ Calculate the first derivative by first order forward difference. 

61 

62 The data corresponding to the last time is repeated several times, 

63 in order to obtain a result of the same shape as the original array. 

64 

65 Parameters 

66 ---------- 

67 timestep 

68 Time step 

69 data_tx 

70 Array of data, the first axis being the time axis. 

71 

72 Returns 

73 ------- 

74 First derivative, array same shape as :attr:`data_tx`. 

75 """ 

76 if len(data_tx) == 1 and timestep is None: 

77 return np.zeros_like(data_tx) 

78 

79 # Coefficients from oldest times to newest times 

80 coefficients = [-1, 1] 

81 return forward_finite_difference(coefficients, data_tx) / timestep 

82 

83 

84def calculate_third_derivative(timestep, data_tx): 

85 """ Calculate the third derivative by first order forward difference. 

86 

87 The data corresponding to the last time is repeated several times, 

88 in order to obtain a result of the same shape as the original array. 

89 

90 Parameters 

91 ---------- 

92 timestep 

93 Time step 

94 data_tx 

95 Array of data, the first axis being the time axis. 

96 

97 Returns 

98 ------- 

99 Third derivative, array same shape as :attr:`data_tx`. 

100 """ 

101 if len(data_tx) == 1 and timestep is None: 

102 return np.zeros_like(data_tx) 

103 

104 # Coefficients from oldest times to newest times 

105 coefficients = [-1, 3, -3, 1] 

106 # For a second order accuracy, the coefficients would be 

107 # [-5/2, 9, -12, 7, -3/2] 

108 return forward_finite_difference(coefficients, data_tx) / timestep ** 3 

109 

110 

111class RRemission: 

112 r""" 

113 Radiation-reaction potential according to Schaefer et al. 

114 [https://doi.org/10.1103/PhysRevLett.128.156402] and 

115 Schaefer [https://doi.org/10.48550/arXiv.2204.01602]. 

116 The potential accounts for the friction 

117 forces acting on the radiating system of oscillating charges 

118 emitting into a single dimension. A more elegant 

119 formulation would use the current instead of the dipole. 

120 Please contact christian.schaefer.physics@gmail.com if any problems 

121 should appear or you would like to consider more complex systems. 

122 Big thanks to Tuomas Rossi and Jakub Fojt for their help. 

123 

124 Parameters 

125 ---------- 

126 environment 

127 Environment, or dictionary with environment parameters. 

128 

129 Notes 

130 ----- 

131 A legacy form for constructing the RRemission object is supported 

132 for backwards compatibility. In this form, two parameters are given. 

133 

134 quantization_plane: float 

135 value of :math:`quantization_plane` in units of Å^2 

136 cavity_polarization: array 

137 value of :math:`cavity_polarization`; dimensionless (directional) 

138 """ 

139 

140 def __init__(self, *args): 

141 if len(args) == 1: 

142 self.environment = create_environment(args[0]) 

143 elif len(args) == 2: 

144 # Legacy syntax 

145 self.environment = create_environment( 

146 'waveguide', 

147 quantization_plane=args[0], 

148 cavity_polarization=args[1]) 

149 warnings.warn( 

150 "Use RRemission({'environment': 'waveguide', " 

151 "'quantization_plane': quantization_plane, " 

152 "'cavity_polarization': cavity_polarization})) instead of " 

153 'RRemission(quantization_plane, cavity_polarization).', 

154 FutureWarning) 

155 else: 

156 raise TypeError('Please provide only one argument ' 

157 '(two for legacy syntax)') 

158 

159 # Recorded dipole moment over time 

160 # The entries all correspond to unique times 

161 # If there is a kick, the dipole after the kick is recorded 

162 # but not the dipole just before the kick 

163 self.dipole_tv = np.zeros((0, 3)) 

164 # Time of last record 

165 self.time = 0 

166 

167 # Time step in TDDFT 

168 self.timestep = None 

169 

170 def read(self, reader): 

171 self.dipole_tv = reader.recorded_dipole / Bohr 

172 self.timestep = reader.timestep 

173 

174 def write(self, writer): 

175 writer.write(recorded_dipole=self.dipole_tv * Bohr) 

176 writer.write(timestep=self.timestep) 

177 self.environment.write(writer) 

178 

179 @classmethod 

180 def from_reader(cls, reader): 

181 parameters = reader.parameters.asdict() 

182 

183 rremission = cls(parameters) 

184 rremission.read(reader) 

185 

186 return rremission 

187 

188 def initialize(self, paw): 

189 self.density = paw.density 

190 self.wfs = paw.wfs 

191 self.hamiltonian = paw.hamiltonian 

192 self.time = paw.time 

193 dipole_v = paw.density.calculate_dipole_moment() 

194 if len(self.dipole_tv) == 0: 

195 # If we are not reading from a restart file, this will be empty 

196 self.record_dipole(dipole_v) 

197 

198 # Set up three external fields summing to the polarization cavity 

199 strength_v = self.environment.cavity_polarization * Hartree / Bohr 

200 assert len(strength_v) == 3 

201 ext_x = ConstantElectricField(strength_v[0], [1, 0, 0]) 

202 ext_y = ConstantElectricField(strength_v[1], [0, 1, 0]) 

203 ext_z = ConstantElectricField(strength_v[2], [0, 0, 1]) 

204 ext_i = [ext_x, ext_y, ext_z] 

205 

206 # Set up the dipole matrix 

207 get_matrix = self.wfs.eigensolver.calculate_hamiltonian_matrix 

208 self.V_iuMM = [] 

209 for ext in ext_i: 

210 V_uMM = [] 

211 hamiltonian = KickHamiltonian(self.hamiltonian, self.density, ext) 

212 for kpt in self.wfs.kpt_u: 

213 V_MM = get_matrix(hamiltonian, self.wfs, kpt, 

214 add_kinetic=False, root=-1) 

215 V_uMM.append(V_MM) 

216 self.V_iuMM.append(V_uMM) 

217 

218 def record_dipole(self, dipole_v): 

219 """ Record the dipole for the next time step. 

220 

221 Parameters 

222 ---------- 

223 dipole_v 

224 The new dipole moment. 

225 """ 

226 self.dipole_tv = np.vstack((self.dipole_tv, dipole_v)) 

227 

228 def update_dipole(self, time, dipole_v): 

229 """ Record the new dipole 

230 

231 If the time has changed, record the new dipole in a new entry. 

232 

233 If not, then it either means that we are iteratively 

234 performing propagator steps, or that a kick has been done. 

235 Either way, we overwrite the last entry in the record. 

236 

237 Parameters 

238 ---------- 

239 time 

240 The new time 

241 dipole_v 

242 The new dipole moment. 

243 """ 

244 if time > self.time or len(self.dipole_tv) == 0: 

245 self.record_dipole(dipole_v) 

246 timestep = time - self.time 

247 if (self.timestep is not None and 

248 not np.isclose(self.timestep, timestep)): 

249 raise NotImplementedError( 

250 'Variable time step in TDDFT not supported' 

251 f'{self.timestep} != {timestep}') 

252 self.timestep = timestep 

253 self.time = time 

254 else: 

255 self.dipole_tv[-1] = dipole_v 

256 

257 def get_MM(self, field_v, kpt): 

258 """ Get potential matrix. 

259 

260 Parameters 

261 ---------- 

262 field_v 

263 The radiation reaction field. 

264 kpt 

265 kpoint 

266 

267 Returns 

268 ------- 

269 The radiation reaction potential matrix. 

270 """ 

271 kpt_rank, q = self.wfs.kd.get_rank_and_index(kpt.k) 

272 u = q * self.wfs.nspins + kpt.s 

273 

274 Ni = len(self.V_iuMM) 

275 Vrr_MM = -field_v[0] * self.V_iuMM[0][u] 

276 for i in range(1, Ni): 

277 Vrr_MM -= field_v[i] * self.V_iuMM[i][u] 

278 

279 return Vrr_MM 

280 

281 def vradiationreaction(self, kpt, time): 

282 self.update_dipole(time, self.density.calculate_dipole_moment()) 

283 

284 field_v = self.environment.radiation_reaction_field( 

285 self.timestep, self.dipole_tv) 

286 

287 Vrr_MM = self.get_MM(field_v, kpt) 

288 return Vrr_MM 

289 

290 

291class Environment(ABC): 

292 

293 def write(self, writer): 

294 writer.child('parameters').write(**self.todict()) 

295 

296 @abstractmethod 

297 def radiation_reaction_field(self, timestep, dipole_tv): 

298 raise NotImplementedError 

299 

300 @abstractmethod 

301 def todict(self): 

302 raise NotImplementedError 

303 

304 

305class WaveguideEnvironment(Environment): 

306 

307 r""" The radiation reaction potential in a 1D waveguide is 

308 

309 .. math:: 

310 

311 -\frac{4 \pi \alpha}{A} 

312 \boldsymbol{\varepsilon_c} \cdot \partial_t \boldsymbol{R}(t) 

313 \boldsymbol{\varepsilon_c} \cdot \hat{\boldsymbol{r}} 

314 

315 Parameters 

316 ---------- 

317 quantization_plane 

318 Quantization plane :math:`A` in units of Å^2. 

319 cavity_polarization 

320 The polarization of the cavity :math:`\boldsymbol{\varepsilon_c}`. 

321 Direction vector. 

322 """ 

323 

324 def __init__(self, quantization_plane, cavity_polarization): 

325 self.quantization_plane = quantization_plane / Bohr**2 

326 self.cavity_polarization = np.array(cavity_polarization) 

327 

328 def radiation_reaction_field(self, timestep, dipole_tv): 

329 d1_dipole_v = calculate_first_derivative(timestep, dipole_tv)[-1] 

330 

331 field_v = (4.0 * np.pi * alpha / self.quantization_plane 

332 * (d1_dipole_v @ self.cavity_polarization) 

333 * self.cavity_polarization) 

334 

335 return field_v 

336 

337 def todict(self): 

338 return {'environment': 'waveguide', 

339 'quantization_plane': self.quantization_plane * Bohr**2, 

340 'cavity_polarization': list(self.cavity_polarization)}