Coverage for /builds/kinetik161/ase/ase/vibrations/franck_condon.py: 96.28%

242 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-12-10 11:04 +0000

1from functools import reduce 

2from itertools import chain, combinations 

3from math import factorial 

4from operator import mul 

5 

6import numpy as np 

7 

8from ase.units import C, _hbar, kB, kg 

9from ase.vibrations import Vibrations 

10 

11 

12class Factorial: 

13 def __init__(self): 

14 self._fac = [1] 

15 self._inv = [1.] 

16 

17 def __call__(self, n): 

18 try: 

19 return self._fac[n] 

20 except IndexError: 

21 for i in range(len(self._fac), n + 1): 

22 self._fac.append(i * self._fac[i - 1]) 

23 try: 

24 self._inv.append(float(1. / self._fac[-1])) 

25 except OverflowError: 

26 self._inv.append(0.) 

27 return self._fac[n] 

28 

29 def inv(self, n): 

30 self(n) 

31 return self._inv[n] 

32 

33 

34class FranckCondonOverlap: 

35 """Evaluate squared overlaps depending on the Huang-Rhys parameter.""" 

36 

37 def __init__(self): 

38 self.factorial = Factorial() 

39 

40 def directT0(self, n, S): 

41 """|<0|n>|^2 

42 

43 Direct squared Franck-Condon overlap corresponding to T=0. 

44 """ 

45 return np.exp(-S) * S**n * self.factorial.inv(n) 

46 

47 def direct(self, n, m, S_in): 

48 """|<n|m>|^2 

49 

50 Direct squared Franck-Condon overlap. 

51 """ 

52 if n > m: 

53 # use symmetry 

54 return self.direct(m, n, S_in) 

55 

56 S = np.array([S_in]) 

57 mask = np.where(S == 0) 

58 S[mask] = 1 # hide zeros 

59 s = 0 

60 for k in range(n + 1): 

61 s += (-1)**(n - k) * S**float(-k) / ( 

62 self.factorial(k) * 

63 self.factorial(n - k) * self.factorial(m - k)) 

64 res = np.exp(-S) * S**(n + m) * s**2 * ( 

65 self.factorial(n) * self.factorial(m)) 

66 # use othogonality 

67 res[mask] = int(n == m) 

68 return res[0] 

69 

70 def direct0mm1(self, m, S): 

71 """<0|m><m|1>""" 

72 sum = S**m 

73 if m: 

74 sum -= m * S**(m - 1) 

75 return np.exp(-S) * np.sqrt(S) * sum * self.factorial.inv(m) 

76 

77 def direct0mm2(self, m, S): 

78 """<0|m><m|2>""" 

79 sum = S**(m + 1) 

80 if m >= 1: 

81 sum -= 2 * m * S**m 

82 if m >= 2: 

83 sum += m * (m - 1) * S**(m - 1) 

84 return np.exp(-S) / np.sqrt(2) * sum * self.factorial.inv(m) 

85 

86 

87class FranckCondonRecursive: 

88 """Recursive implementation of Franck-Condon overlaps 

89 

90 Notes 

91 ----- 

92 The overlaps are signed according to the sign of the displacements. 

93 

94 Reference 

95 --------- 

96 Julien Guthmuller 

97 The Journal of Chemical Physics 144, 064106 (2016); doi: 10.1063/1.4941449 

98 """ 

99 

100 def __init__(self): 

101 self.factorial = Factorial() 

102 

103 def ov0m(self, m, delta): 

104 if m == 0: 

105 return np.exp(-0.25 * delta**2) 

106 else: 

107 assert m > 0 

108 return - delta / np.sqrt(2 * m) * self.ov0m(m - 1, delta) 

109 

110 def ov1m(self, m, delta): 

111 sum = delta * self.ov0m(m, delta) / np.sqrt(2.) 

112 if m == 0: 

113 return sum 

114 else: 

115 assert m > 0 

116 return sum + np.sqrt(m) * self.ov0m(m - 1, delta) 

117 

118 def ov2m(self, m, delta): 

119 sum = delta * self.ov1m(m, delta) / 2 

120 if m == 0: 

121 return sum 

122 else: 

123 assert m > 0 

124 return sum + np.sqrt(m / 2.) * self.ov1m(m - 1, delta) 

125 

126 def ov3m(self, m, delta): 

127 sum = delta * self.ov2m(m, delta) / np.sqrt(6.) 

128 if m == 0: 

129 return sum 

130 else: 

131 assert m > 0 

132 return sum + np.sqrt(m / 3.) * self.ov2m(m - 1, delta) 

133 

134 def ov0mm1(self, m, delta): 

135 if m == 0: 

136 return delta / np.sqrt(2) * self.ov0m(m, delta)**2 

137 else: 

138 return delta / np.sqrt(2) * ( 

139 self.ov0m(m, delta)**2 - self.ov0m(m - 1, delta)**2) 

140 

141 def direct0mm1(self, m, delta): 

142 """direct and fast <0|m><m|1>""" 

143 S = delta**2 / 2. 

144 sum = S**m 

145 if m: 

146 sum -= m * S**(m - 1) 

147 return np.where(S == 0, 0, 

148 (np.exp(-S) * delta / np.sqrt(2) * sum * 

149 self.factorial.inv(m))) 

150 

151 def ov0mm2(self, m, delta): 

152 if m == 0: 

153 return delta**2 / np.sqrt(8) * self.ov0m(m, delta)**2 

154 elif m == 1: 

155 return delta**2 / np.sqrt(8) * ( 

156 self.ov0m(m, delta)**2 - 2 * self.ov0m(m - 1, delta)**2) 

157 else: 

158 return delta**2 / np.sqrt(8) * ( 

159 self.ov0m(m, delta)**2 - 2 * self.ov0m(m - 1, delta)**2 + 

160 self.ov0m(m - 2, delta)**2) 

161 

162 def direct0mm2(self, m, delta): 

163 """direct and fast <0|m><m|2>""" 

164 S = delta**2 / 2. 

165 sum = S**(m + 1) 

166 if m >= 1: 

167 sum -= 2 * m * S**m 

168 if m >= 2: 

169 sum += m * (m - 1) * S**(m - 1) 

170 return np.exp(-S) / np.sqrt(2) * sum * self.factorial.inv(m) 

171 

172 def ov1mm2(self, m, delta): 

173 p1 = delta**3 / 4. 

174 sum = p1 * self.ov0m(m, delta)**2 

175 if m == 0: 

176 return sum 

177 p2 = delta - 3. * delta**3 / 4 

178 sum += p2 * self.ov0m(m - 1, delta)**2 

179 if m == 1: 

180 return sum 

181 sum -= p2 * self.ov0m(m - 2, delta)**2 

182 if m == 2: 

183 return sum 

184 return sum - p1 * self.ov0m(m - 3, delta)**2 

185 

186 def direct1mm2(self, m, delta): 

187 S = delta**2 / 2. 

188 sum = S**2 

189 if m > 0: 

190 sum -= 2 * m * S 

191 if m > 1: 

192 sum += m * (m - 1) 

193 with np.errstate(divide='ignore', invalid='ignore'): 

194 return np.where(S == 0, 0, 

195 (np.exp(-S) * S**(m - 1) / delta 

196 * (S - m) * sum * self.factorial.inv(m))) 

197 

198 def direct0mm3(self, m, delta): 

199 S = delta**2 / 2. 

200 with np.errstate(divide='ignore', invalid='ignore'): 

201 return np.where( 

202 S == 0, 0, 

203 (np.exp(-S) * S**(m - 1) / delta * np.sqrt(12.) * 

204 (S**3 / 6. - m * S**2 / 2 

205 + m * (m - 1) * S / 2. - m * (m - 1) * (m - 2) / 6) 

206 * self.factorial.inv(m))) 

207 

208 

209class FranckCondon: 

210 def __init__(self, atoms, vibname, minfreq=-np.inf, maxfreq=np.inf): 

211 """Input is a atoms object and the corresponding vibrations. 

212 With minfreq and maxfreq frequencies can 

213 be excluded from the calculation""" 

214 

215 self.atoms = atoms 

216 # V = a * v is the combined atom and xyz-index 

217 self.mm05_V = np.repeat(1. / np.sqrt(atoms.get_masses()), 3) 

218 self.minfreq = minfreq 

219 self.maxfreq = maxfreq 

220 self.shape = (len(self.atoms), 3) 

221 

222 vib = Vibrations(atoms, name=vibname) 

223 self.energies = np.real(vib.get_energies(method='frederiksen')) # [eV] 

224 self.frequencies = np.real( 

225 vib.get_frequencies(method='frederiksen')) # [cm^-1] 

226 self.modes = vib.modes 

227 self.H = vib.H 

228 

229 def get_Huang_Rhys_factors(self, forces): 

230 """Evaluate Huang-Rhys factors and corresponding frequencies 

231 from forces on atoms in the exited electronic state. 

232 The double harmonic approximation is used. HR factors are 

233 the first approximation of FC factors, 

234 no combinations or higher quanta (>1) exitations are considered""" 

235 

236 assert forces.shape == self.shape 

237 

238 # Hesse matrix 

239 H_VV = self.H 

240 # sqrt of inverse mass matrix 

241 mm05_V = self.mm05_V 

242 # mass weighted Hesse matrix 

243 Hm_VV = mm05_V[:, None] * H_VV * mm05_V 

244 # mass weighted displacements 

245 Fm_V = forces.flat * mm05_V 

246 X_V = np.linalg.solve(Hm_VV, Fm_V) 

247 # projection onto the modes 

248 modes_VV = self.modes 

249 d_V = np.dot(modes_VV, X_V) 

250 # Huang-Rhys factors S 

251 s = 1.e-20 / kg / C / _hbar**2 # SI units 

252 S_V = s * d_V**2 * self.energies / 2 

253 

254 # reshape for minfreq 

255 indices = np.where(self.frequencies <= self.minfreq) 

256 np.append(indices, np.where(self.frequencies >= self.maxfreq)) 

257 S_V = np.delete(S_V, indices) 

258 frequencies = np.delete(self.frequencies, indices) 

259 

260 return S_V, frequencies 

261 

262 def get_Franck_Condon_factors(self, temperature, forces, order=1): 

263 """Return FC factors and corresponding frequencies up to given order. 

264 

265 Parameters 

266 ---------- 

267 temperature: float 

268 Temperature in K. Vibronic levels are occupied by a 

269 Boltzman distribution. 

270 forces: array 

271 Forces on atoms in the exited electronic state 

272 order: int 

273 number of quanta taken into account, default 

274 

275 Returns 

276 -------- 

277 FC: 3 entry list 

278 FC[0] = FC factors for 0-0 and +-1 vibrational quantum 

279 FC[1] = FC factors for +-2 vibrational quanta 

280 FC[2] = FC factors for combinations 

281 frequencies: 3 entry list 

282 frequencies[0] correspond to FC[0] 

283 frequencies[1] correspond to FC[1] 

284 frequencies[2] correspond to FC[2] 

285 """ 

286 S, f = self.get_Huang_Rhys_factors(forces) 

287 assert order > 0 

288 n = order + 1 

289 T = temperature 

290 freq = np.array(f) 

291 

292 # frequencies and their multiples 

293 freq_n = [[] * i for i in range(n - 1)] 

294 freq_neg = [[] * i for i in range(n - 1)] 

295 

296 for i in range(1, n): 

297 freq_n[i - 1] = freq * i 

298 freq_neg[i - 1] = freq * (-i) 

299 

300 # combinations 

301 freq_nn = [x for x in combinations(chain(*freq_n), 2)] 

302 for i in range(len(freq_nn)): 

303 freq_nn[i] = freq_nn[i][0] + freq_nn[i][1] 

304 

305 indices2 = [] 

306 for i, y in enumerate(freq): 

307 ind = [j for j, x in enumerate(freq_nn) if y == 0 or x % y == 0] 

308 indices2.append(ind) 

309 indices2 = [x for x in chain(*indices2)] 

310 freq_nn = np.delete(freq_nn, indices2) 

311 

312 frequencies = [[] * x for x in range(3)] 

313 frequencies[0].append(freq_neg[0]) 

314 frequencies[0].append([0]) 

315 frequencies[0].append(freq_n[0]) 

316 frequencies[0] = [x for x in chain(*frequencies[0])] 

317 

318 for i in range(1, n - 1): 

319 frequencies[1].append(freq_neg[i]) 

320 frequencies[1].append(freq_n[i]) 

321 frequencies[1] = [x for x in chain(*frequencies[1])] 

322 

323 frequencies[2] = freq_nn 

324 

325 # Franck-Condon factors 

326 E = freq / 8065.5 

327 f_n = [[] * i for i in range(n)] 

328 

329 for j in range(0, n): 

330 f_n[j] = np.exp(-E * j / (kB * T)) 

331 

332 # partition function 

333 Z = np.empty(len(S)) 

334 Z = np.sum(f_n, 0) 

335 

336 # occupation probability 

337 w_n = [[] * k for k in range(n)] 

338 for lval in range(n): 

339 w_n[lval] = f_n[lval] / Z 

340 

341 # overlap wavefunctions 

342 O_n = [[] * m for m in range(n)] 

343 O_neg = [[] * m for m in range(n)] 

344 for o in range(n): 

345 O_n[o] = [[] * p for p in range(n)] 

346 O_neg[o] = [[] * p for p in range(n - 1)] 

347 for q in range(o, n + o): 

348 a = np.minimum(o, q) 

349 summe = [] 

350 for k in range(a + 1): 

351 s = ((-1)**(q - k) * np.sqrt(S)**(o + q - 2 * k) * 

352 factorial(o) * factorial(q) / 

353 (factorial(k) * factorial(o - k) * factorial(q - k))) 

354 summe.append(s) 

355 summe = np.sum(summe, 0) 

356 O_n[o][q - o] = (np.exp(-S / 2) / 

357 (factorial(o) * factorial(q))**(0.5) * 

358 summe)**2 * w_n[o] 

359 for q in range(n - 1): 

360 O_neg[o][q] = [0 * b for b in range(len(S))] 

361 for q in range(o - 1, -1, -1): 

362 a = np.minimum(o, q) 

363 summe = [] 

364 for k in range(a + 1): 

365 s = ((-1)**(q - k) * np.sqrt(S)**(o + q - 2 * k) * 

366 factorial(o) * factorial(q) / 

367 (factorial(k) * factorial(o - k) * factorial(q - k))) 

368 summe.append(s) 

369 summe = np.sum(summe, 0) 

370 O_neg[o][q] = (np.exp(-S / 2) / 

371 (factorial(o) * factorial(q))**(0.5) * 

372 summe)**2 * w_n[o] 

373 O_neg = np.delete(O_neg, 0, 0) 

374 

375 # Franck-Condon factors 

376 FC_n = [[] * i for i in range(n)] 

377 FC_n = np.sum(O_n, 0) 

378 zero = reduce(mul, FC_n[0]) 

379 FC_neg = [[] * i for i in range(n - 2)] 

380 FC_neg = np.sum(O_neg, 0) 

381 FC_n = np.delete(FC_n, 0, 0) 

382 

383 # combination FC factors 

384 FC_nn = [x for x in combinations(chain(*FC_n), 2)] 

385 for i in range(len(FC_nn)): 

386 FC_nn[i] = FC_nn[i][0] * FC_nn[i][1] 

387 

388 FC_nn = np.delete(FC_nn, indices2) 

389 

390 FC = [[] * x for x in range(3)] 

391 FC[0].append(FC_neg[0]) 

392 FC[0].append([zero]) 

393 FC[0].append(FC_n[0]) 

394 FC[0] = [x for x in chain(*FC[0])] 

395 

396 for i in range(1, n - 1): 

397 FC[1].append(FC_neg[i]) 

398 FC[1].append(FC_n[i]) 

399 FC[1] = [x for x in chain(*FC[1])] 

400 

401 FC[2] = FC_nn 

402 

403 return FC, frequencies