Coverage for /builds/kinetik161/ase/ase/thermochemistry.py: 95.18%

415 statements  

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

1"""Modules for calculating thermochemical information from computational 

2outputs.""" 

3 

4import os 

5import sys 

6from warnings import warn 

7 

8import numpy as np 

9 

10from ase import units 

11 

12 

13class ThermoChem: 

14 """Base class containing common methods used in thermochemistry 

15 calculations.""" 

16 

17 def get_ZPE_correction(self): 

18 """Returns the zero-point vibrational energy correction in eV.""" 

19 return 0.5 * np.sum(self.vib_energies) 

20 

21 def _vibrational_energy_contribution(self, temperature): 

22 """Calculates the change in internal energy due to vibrations from 

23 0K to the specified temperature for a set of vibrations given in 

24 eV and a temperature given in Kelvin. Returns the energy change 

25 in eV.""" 

26 kT = units.kB * temperature 

27 dU = 0. 

28 for energy in self.vib_energies: 

29 dU += energy / (np.exp(energy / kT) - 1.) 

30 return dU 

31 

32 def _vibrational_entropy_contribution(self, temperature): 

33 """Calculates the entropy due to vibrations for a set of vibrations 

34 given in eV and a temperature given in Kelvin. Returns the entropy 

35 in eV/K.""" 

36 kT = units.kB * temperature 

37 S_v = 0. 

38 for energy in self.vib_energies: 

39 x = energy / kT 

40 S_v += x / (np.exp(x) - 1.) - np.log(1. - np.exp(-x)) 

41 S_v *= units.kB 

42 return S_v 

43 

44 def _vprint(self, text): 

45 """Print output if verbose flag True.""" 

46 if self.verbose: 

47 sys.stdout.write(text + os.linesep) 

48 

49 

50class HarmonicThermo(ThermoChem): 

51 """Class for calculating thermodynamic properties in the approximation 

52 that all degrees of freedom are treated harmonically. Often used for 

53 adsorbates. 

54 

55 Inputs: 

56 

57 vib_energies : list 

58 a list of the harmonic energies of the adsorbate (e.g., from 

59 ase.vibrations.Vibrations.get_energies). The number of 

60 energies should match the number of degrees of freedom of the 

61 adsorbate; i.e., 3*n, where n is the number of atoms. Note that 

62 this class does not check that the user has supplied the correct 

63 number of energies. Units of energies are eV. 

64 potentialenergy : float 

65 the potential energy in eV (e.g., from atoms.get_potential_energy) 

66 (if potentialenergy is unspecified, then the methods of this 

67 class can be interpreted as the energy corrections) 

68 ignore_imag_modes : bool 

69 If True, any imaginary frequencies will be ignored in the calculation 

70 of the thermochemical properties. If False (default), an error will 

71 be raised if any imaginary frequencies are present. 

72 """ 

73 

74 def __init__(self, vib_energies, potentialenergy=0., 

75 ignore_imag_modes=False): 

76 self.ignore_imag_modes = ignore_imag_modes 

77 

78 # Check for imaginary frequencies. 

79 vib_energies, n_imag = _clean_vib_energies( 

80 vib_energies, ignore_imag_modes=ignore_imag_modes 

81 ) 

82 self.vib_energies = vib_energies 

83 self.n_imag = n_imag 

84 

85 self.potentialenergy = potentialenergy 

86 

87 def get_internal_energy(self, temperature, verbose=True): 

88 """Returns the internal energy, in eV, in the harmonic approximation 

89 at a specified temperature (K).""" 

90 

91 self.verbose = verbose 

92 write = self._vprint 

93 fmt = '%-15s%13.3f eV' 

94 write('Internal energy components at T = %.2f K:' % temperature) 

95 write('=' * 31) 

96 

97 U = 0. 

98 

99 write(fmt % ('E_pot', self.potentialenergy)) 

100 U += self.potentialenergy 

101 

102 zpe = self.get_ZPE_correction() 

103 write(fmt % ('E_ZPE', zpe)) 

104 U += zpe 

105 

106 dU_v = self._vibrational_energy_contribution(temperature) 

107 write(fmt % ('Cv_harm (0->T)', dU_v)) 

108 U += dU_v 

109 

110 write('-' * 31) 

111 write(fmt % ('U', U)) 

112 write('=' * 31) 

113 return U 

114 

115 def get_entropy(self, temperature, verbose=True): 

116 """Returns the entropy, in eV/K, in the harmonic approximation 

117 at a specified temperature (K).""" 

118 

119 self.verbose = verbose 

120 write = self._vprint 

121 fmt = '%-15s%13.7f eV/K%13.3f eV' 

122 write('Entropy components at T = %.2f K:' % temperature) 

123 write('=' * 49) 

124 write('%15s%13s %13s' % ('', 'S', 'T*S')) 

125 

126 S = 0. 

127 

128 S_v = self._vibrational_entropy_contribution(temperature) 

129 write(fmt % ('S_harm', S_v, S_v * temperature)) 

130 S += S_v 

131 

132 write('-' * 49) 

133 write(fmt % ('S', S, S * temperature)) 

134 write('=' * 49) 

135 return S 

136 

137 def get_helmholtz_energy(self, temperature, verbose=True): 

138 """Returns the Helmholtz free energy, in eV, in the harmonic 

139 approximation at a specified temperature (K).""" 

140 

141 self.verbose = True 

142 write = self._vprint 

143 

144 U = self.get_internal_energy(temperature, verbose=verbose) 

145 write('') 

146 S = self.get_entropy(temperature, verbose=verbose) 

147 F = U - temperature * S 

148 

149 write('') 

150 write('Free energy components at T = %.2f K:' % temperature) 

151 write('=' * 23) 

152 fmt = '%5s%15.3f eV' 

153 write(fmt % ('U', U)) 

154 write(fmt % ('-T*S', -temperature * S)) 

155 write('-' * 23) 

156 write(fmt % ('F', F)) 

157 write('=' * 23) 

158 return F 

159 

160 

161class HinderedThermo(ThermoChem): 

162 """Class for calculating thermodynamic properties in the hindered 

163 translator and hindered rotor model where all but three degrees of 

164 freedom are treated as harmonic vibrations, two are treated as 

165 hindered translations, and one is treated as a hindered rotation. 

166 

167 Inputs: 

168 

169 vib_energies : list 

170 a list of all the vibrational energies of the adsorbate (e.g., from 

171 ase.vibrations.Vibrations.get_energies). If atoms is not provided, 

172 then the number of energies must match the number of degrees of freedom 

173 of the adsorbate; i.e., 3*n, where n is the number of atoms. Note 

174 that this class does not check that the user has supplied the 

175 correct number of energies. 

176 Units of energies are eV. 

177 trans_barrier_energy : float 

178 the translational energy barrier in eV. This is the barrier for an 

179 adsorbate to diffuse on the surface. 

180 rot_barrier_energy : float 

181 the rotational energy barrier in eV. This is the barrier for an 

182 adsorbate to rotate about an axis perpendicular to the surface. 

183 sitedensity : float 

184 density of surface sites in cm^-2 

185 rotationalminima : integer 

186 the number of equivalent minima for an adsorbate's full rotation. 

187 For example, 6 for an adsorbate on an fcc(111) top site 

188 potentialenergy : float 

189 the potential energy in eV (e.g., from atoms.get_potential_energy) 

190 (if potentialenergy is unspecified, then the methods of this class 

191 can be interpreted as the energy corrections) 

192 mass : float 

193 the mass of the adsorbate in amu (if mass is unspecified, then it will 

194 be calculated from the atoms class) 

195 inertia : float 

196 the reduced moment of inertia of the adsorbate in amu*Ang^-2 

197 (if inertia is unspecified, then it will be calculated from the 

198 atoms class) 

199 atoms : an ASE atoms object 

200 used to calculate rotational moments of inertia and molecular mass 

201 symmetrynumber : integer 

202 symmetry number of the adsorbate. This is the number of symmetric arms 

203 of the adsorbate and depends upon how it is bound to the surface. 

204 For example, propane bound through its end carbon has a symmetry 

205 number of 1 but propane bound through its middle carbon has a symmetry 

206 number of 2. (if symmetrynumber is unspecified, then the default is 1) 

207 ignore_imag_modes : bool 

208 If True, any imaginary frequencies present after the 3N-3 cut will not 

209 be included in the calculation of the thermochemical properties. If 

210 False (default), an error will be raised if imaginary frequencies are 

211 present after the 3N-3 cut. 

212 """ 

213 

214 def __init__(self, vib_energies, trans_barrier_energy, rot_barrier_energy, 

215 sitedensity, rotationalminima, potentialenergy=0., 

216 mass=None, inertia=None, atoms=None, symmetrynumber=1, 

217 ignore_imag_modes=False): 

218 

219 self.trans_barrier_energy = trans_barrier_energy * units._e 

220 self.rot_barrier_energy = rot_barrier_energy * units._e 

221 self.area = 1. / sitedensity / 100.0**2 

222 self.rotationalminima = rotationalminima 

223 self.potentialenergy = potentialenergy 

224 self.atoms = atoms 

225 self.symmetry = symmetrynumber 

226 self.ignore_imag_modes = ignore_imag_modes 

227 

228 # Sort the vibrations 

229 vib_energies = list(vib_energies) 

230 vib_energies.sort(key=np.abs) 

231 

232 # Keep only the relevant vibrational energies (3N-3) 

233 if atoms: 

234 vib_energies = vib_energies[-(3 * len(atoms) - 3):] 

235 else: 

236 vib_energies = vib_energies[-(len(vib_energies) - 3):] 

237 

238 # Check for imaginary frequencies. 

239 vib_energies, n_imag = _clean_vib_energies( 

240 vib_energies, ignore_imag_modes=ignore_imag_modes 

241 ) 

242 self.vib_energies = vib_energies 

243 self.n_imag = n_imag 

244 

245 if (mass or atoms) and (inertia or atoms): 

246 if mass: 

247 self.mass = mass * units._amu 

248 elif atoms: 

249 self.mass = np.sum(atoms.get_masses()) * units._amu 

250 if inertia: 

251 self.inertia = inertia * units._amu / units.m**2 

252 elif atoms: 

253 self.inertia = (atoms.get_moments_of_inertia()[2] * 

254 units._amu / units.m**2) 

255 else: 

256 raise RuntimeError('Either mass and inertia of the ' 

257 'adsorbate must be specified or ' 

258 'atoms must be specified.') 

259 

260 # Calculate hindered translational and rotational frequencies 

261 self.freq_t = np.sqrt(self.trans_barrier_energy / (2 * self.mass * 

262 self.area)) 

263 self.freq_r = 1. / (2 * np.pi) * np.sqrt(self.rotationalminima**2 * 

264 self.rot_barrier_energy / 

265 (2 * self.inertia)) 

266 

267 def get_internal_energy(self, temperature, verbose=True): 

268 """Returns the internal energy (including the zero point energy), 

269 in eV, in the hindered translator and hindered rotor model at a 

270 specified temperature (K).""" 

271 

272 from scipy.special import iv 

273 

274 self.verbose = verbose 

275 write = self._vprint 

276 fmt = '%-15s%13.3f eV' 

277 write('Internal energy components at T = %.2f K:' % temperature) 

278 write('=' * 31) 

279 

280 U = 0. 

281 

282 write(fmt % ('E_pot', self.potentialenergy)) 

283 U += self.potentialenergy 

284 

285 # Translational Energy 

286 T_t = units._k * temperature / (units._hplanck * self.freq_t) 

287 R_t = self.trans_barrier_energy / (units._hplanck * self.freq_t) 

288 dU_t = 2 * (-1. / 2 - 1. / T_t / (2 + 16 * R_t) + R_t / 2 / T_t - 

289 R_t / 2 / T_t * 

290 iv(1, R_t / 2 / T_t) / iv(0, R_t / 2 / T_t) + 

291 1. / T_t / (np.exp(1. / T_t) - 1)) 

292 dU_t *= units.kB * temperature 

293 write(fmt % ('E_trans', dU_t)) 

294 U += dU_t 

295 

296 # Rotational Energy 

297 T_r = units._k * temperature / (units._hplanck * self.freq_r) 

298 R_r = self.rot_barrier_energy / (units._hplanck * self.freq_r) 

299 dU_r = (-1. / 2 - 1. / T_r / (2 + 16 * R_r) + R_r / 2 / T_r - 

300 R_r / 2 / T_r * 

301 iv(1, R_r / 2 / T_r) / iv(0, R_r / 2 / T_r) + 

302 1. / T_r / (np.exp(1. / T_r) - 1)) 

303 dU_r *= units.kB * temperature 

304 write(fmt % ('E_rot', dU_r)) 

305 U += dU_r 

306 

307 # Vibrational Energy 

308 dU_v = self._vibrational_energy_contribution(temperature) 

309 write(fmt % ('E_vib', dU_v)) 

310 U += dU_v 

311 

312 # Zero Point Energy 

313 dU_zpe = self.get_zero_point_energy() 

314 write(fmt % ('E_ZPE', dU_zpe)) 

315 U += dU_zpe 

316 

317 write('-' * 31) 

318 write(fmt % ('U', U)) 

319 write('=' * 31) 

320 return U 

321 

322 def get_zero_point_energy(self, verbose=True): 

323 """Returns the zero point energy, in eV, in the hindered 

324 translator and hindered rotor model""" 

325 

326 zpe_t = 2 * (1. / 2 * self.freq_t * units._hplanck / units._e) 

327 zpe_r = 1. / 2 * self.freq_r * units._hplanck / units._e 

328 zpe_v = self.get_ZPE_correction() 

329 zpe = zpe_t + zpe_r + zpe_v 

330 return zpe 

331 

332 def get_entropy(self, temperature, verbose=True): 

333 """Returns the entropy, in eV/K, in the hindered translator 

334 and hindered rotor model at a specified temperature (K).""" 

335 

336 from scipy.special import iv 

337 

338 self.verbose = verbose 

339 write = self._vprint 

340 fmt = '%-15s%13.7f eV/K%13.3f eV' 

341 write('Entropy components at T = %.2f K:' % temperature) 

342 write('=' * 49) 

343 write('%15s%13s %13s' % ('', 'S', 'T*S')) 

344 

345 S = 0. 

346 

347 # Translational Entropy 

348 T_t = units._k * temperature / (units._hplanck * self.freq_t) 

349 R_t = self.trans_barrier_energy / (units._hplanck * self.freq_t) 

350 S_t = 2 * (-1. / 2 + 1. / 2 * np.log(np.pi * R_t / T_t) - 

351 R_t / 2 / T_t * 

352 iv(1, R_t / 2 / T_t) / iv(0, R_t / 2 / T_t) + 

353 np.log(iv(0, R_t / 2 / T_t)) + 

354 1. / T_t / (np.exp(1. / T_t) - 1) - 

355 np.log(1 - np.exp(-1. / T_t))) 

356 S_t *= units.kB 

357 write(fmt % ('S_trans', S_t, S_t * temperature)) 

358 S += S_t 

359 

360 # Rotational Entropy 

361 T_r = units._k * temperature / (units._hplanck * self.freq_r) 

362 R_r = self.rot_barrier_energy / (units._hplanck * self.freq_r) 

363 S_r = (-1. / 2 + 1. / 2 * np.log(np.pi * R_r / T_r) - 

364 np.log(self.symmetry) - 

365 R_r / 2 / T_r * iv(1, R_r / 2 / T_r) / iv(0, R_r / 2 / T_r) + 

366 np.log(iv(0, R_r / 2 / T_r)) + 

367 1. / T_r / (np.exp(1. / T_r) - 1) - 

368 np.log(1 - np.exp(-1. / T_r))) 

369 S_r *= units.kB 

370 write(fmt % ('S_rot', S_r, S_r * temperature)) 

371 S += S_r 

372 

373 # Vibrational Entropy 

374 S_v = self._vibrational_entropy_contribution(temperature) 

375 write(fmt % ('S_vib', S_v, S_v * temperature)) 

376 S += S_v 

377 

378 # Concentration Related Entropy 

379 N_over_A = np.exp(1. / 3) * (10.0**5 / 

380 (units._k * temperature))**(2. / 3) 

381 S_c = 1 - np.log(N_over_A) - np.log(self.area) 

382 S_c *= units.kB 

383 write(fmt % ('S_con', S_c, S_c * temperature)) 

384 S += S_c 

385 

386 write('-' * 49) 

387 write(fmt % ('S', S, S * temperature)) 

388 write('=' * 49) 

389 return S 

390 

391 def get_helmholtz_energy(self, temperature, verbose=True): 

392 """Returns the Helmholtz free energy, in eV, in the hindered 

393 translator and hindered rotor model at a specified temperature 

394 (K).""" 

395 

396 self.verbose = True 

397 write = self._vprint 

398 

399 U = self.get_internal_energy(temperature, verbose=verbose) 

400 write('') 

401 S = self.get_entropy(temperature, verbose=verbose) 

402 F = U - temperature * S 

403 

404 write('') 

405 write('Free energy components at T = %.2f K:' % temperature) 

406 write('=' * 23) 

407 fmt = '%5s%15.3f eV' 

408 write(fmt % ('U', U)) 

409 write(fmt % ('-T*S', -temperature * S)) 

410 write('-' * 23) 

411 write(fmt % ('F', F)) 

412 write('=' * 23) 

413 return F 

414 

415 

416class IdealGasThermo(ThermoChem): 

417 """Class for calculating thermodynamic properties of a molecule 

418 based on statistical mechanical treatments in the ideal gas 

419 approximation. 

420 

421 Inputs for enthalpy calculations: 

422 

423 vib_energies : list 

424 a list of the vibrational energies of the molecule (e.g., from 

425 ase.vibrations.Vibrations.get_energies). The number of vibrations 

426 used is automatically calculated by the geometry and the number of 

427 atoms. If more are specified than are needed, then the lowest 

428 numbered vibrations are neglected. If either atoms or natoms is 

429 unspecified, then uses the entire list. Units are eV. 

430 geometry : 'monatomic', 'linear', or 'nonlinear' 

431 geometry of the molecule 

432 potentialenergy : float 

433 the potential energy in eV (e.g., from atoms.get_potential_energy) 

434 (if potentialenergy is unspecified, then the methods of this 

435 class can be interpreted as the energy corrections) 

436 natoms : integer 

437 the number of atoms, used along with 'geometry' to determine how 

438 many vibrations to use. (Not needed if an atoms object is supplied 

439 in 'atoms' or if the user desires the entire list of vibrations 

440 to be used.) 

441 

442 Extra inputs needed for entropy / free energy calculations: 

443 

444 atoms : an ASE atoms object 

445 used to calculate rotational moments of inertia and molecular mass 

446 symmetrynumber : integer 

447 symmetry number of the molecule. See, for example, Table 10.1 and 

448 Appendix B of C. Cramer "Essentials of Computational Chemistry", 

449 2nd Ed. 

450 spin : float 

451 the total electronic spin. (0 for molecules in which all electrons 

452 are paired, 0.5 for a free radical with a single unpaired electron, 

453 1.0 for a triplet with two unpaired electrons, such as O_2.) 

454 ignore_imag_modes : bool 

455 If True, any imaginary frequencies present after the 3N-5/3N-6 cut 

456 will not be included in the calculation of the thermochemical 

457 properties. If False (default), a ValueError will be raised if 

458 any imaginary frequencies remain after the 3N-5/3N-6 cut. 

459 """ 

460 

461 def __init__(self, vib_energies, geometry, potentialenergy=0., 

462 atoms=None, symmetrynumber=None, spin=None, natoms=None, 

463 ignore_imag_modes=False): 

464 self.potentialenergy = potentialenergy 

465 self.geometry = geometry 

466 self.atoms = atoms 

467 self.sigma = symmetrynumber 

468 self.spin = spin 

469 self.ignore_imag_modes = ignore_imag_modes 

470 if natoms is None and atoms: 

471 natoms = len(atoms) 

472 self.natoms = natoms 

473 

474 # Sort the vibrations 

475 vib_energies = list(vib_energies) 

476 vib_energies.sort(key=np.abs) 

477 

478 # Cut the vibrations to those needed from the geometry. 

479 if natoms: 

480 if geometry == 'nonlinear': 

481 vib_energies = vib_energies[-(3 * natoms - 6):] 

482 elif geometry == 'linear': 

483 vib_energies = vib_energies[-(3 * natoms - 5):] 

484 elif geometry == 'monatomic': 

485 vib_energies = [] 

486 else: 

487 raise ValueError(f"Unsupported geometry: {geometry}") 

488 

489 # Check for imaginary frequencies. 

490 vib_energies, n_imag = _clean_vib_energies( 

491 vib_energies, ignore_imag_modes=ignore_imag_modes 

492 ) 

493 self.vib_energies = vib_energies 

494 self.n_imag = n_imag 

495 

496 self.referencepressure = 1.0e5 # Pa 

497 

498 def get_enthalpy(self, temperature, verbose=True): 

499 """Returns the enthalpy, in eV, in the ideal gas approximation 

500 at a specified temperature (K).""" 

501 

502 self.verbose = verbose 

503 write = self._vprint 

504 fmt = '%-15s%13.3f eV' 

505 write('Enthalpy components at T = %.2f K:' % temperature) 

506 write('=' * 31) 

507 

508 H = 0. 

509 

510 write(fmt % ('E_pot', self.potentialenergy)) 

511 H += self.potentialenergy 

512 

513 zpe = self.get_ZPE_correction() 

514 write(fmt % ('E_ZPE', zpe)) 

515 H += zpe 

516 

517 Cv_t = 3. / 2. * units.kB # translational heat capacity (3-d gas) 

518 write(fmt % ('Cv_trans (0->T)', Cv_t * temperature)) 

519 H += Cv_t * temperature 

520 

521 if self.geometry == 'nonlinear': # rotational heat capacity 

522 Cv_r = 3. / 2. * units.kB 

523 elif self.geometry == 'linear': 

524 Cv_r = units.kB 

525 elif self.geometry == 'monatomic': 

526 Cv_r = 0. 

527 write(fmt % ('Cv_rot (0->T)', Cv_r * temperature)) 

528 H += Cv_r * temperature 

529 

530 dH_v = self._vibrational_energy_contribution(temperature) 

531 write(fmt % ('Cv_vib (0->T)', dH_v)) 

532 H += dH_v 

533 

534 Cp_corr = units.kB * temperature 

535 write(fmt % ('(C_v -> C_p)', Cp_corr)) 

536 H += Cp_corr 

537 

538 write('-' * 31) 

539 write(fmt % ('H', H)) 

540 write('=' * 31) 

541 return H 

542 

543 def get_entropy(self, temperature, pressure, verbose=True): 

544 """Returns the entropy, in eV/K, in the ideal gas approximation 

545 at a specified temperature (K) and pressure (Pa).""" 

546 

547 if self.atoms is None or self.sigma is None or self.spin is None: 

548 raise RuntimeError('atoms, symmetrynumber, and spin must be ' 

549 'specified for entropy and free energy ' 

550 'calculations.') 

551 self.verbose = verbose 

552 write = self._vprint 

553 fmt = '%-15s%13.7f eV/K%13.3f eV' 

554 write('Entropy components at T = %.2f K and P = %.1f Pa:' % 

555 (temperature, pressure)) 

556 write('=' * 49) 

557 write('%15s%13s %13s' % ('', 'S', 'T*S')) 

558 

559 S = 0.0 

560 

561 # Translational entropy (term inside the log is in SI units). 

562 mass = sum(self.atoms.get_masses()) * units._amu # kg/molecule 

563 S_t = (2 * np.pi * mass * units._k * 

564 temperature / units._hplanck**2)**(3.0 / 2) 

565 S_t *= units._k * temperature / self.referencepressure 

566 S_t = units.kB * (np.log(S_t) + 5.0 / 2.0) 

567 write(fmt % ('S_trans (1 bar)', S_t, S_t * temperature)) 

568 S += S_t 

569 

570 # Rotational entropy (term inside the log is in SI units). 

571 if self.geometry == 'monatomic': 

572 S_r = 0.0 

573 elif self.geometry == 'nonlinear': 

574 inertias = (self.atoms.get_moments_of_inertia() * units._amu / 

575 (10.0**10)**2) # kg m^2 

576 S_r = np.sqrt(np.pi * np.prod(inertias)) / self.sigma 

577 S_r *= (8.0 * np.pi**2 * units._k * temperature / 

578 units._hplanck**2)**(3.0 / 2.0) 

579 S_r = units.kB * (np.log(S_r) + 3.0 / 2.0) 

580 elif self.geometry == 'linear': 

581 inertias = (self.atoms.get_moments_of_inertia() * units._amu / 

582 (10.0**10)**2) # kg m^2 

583 inertia = max(inertias) # should be two identical and one zero 

584 S_r = (8 * np.pi**2 * inertia * units._k * temperature / 

585 self.sigma / units._hplanck**2) 

586 S_r = units.kB * (np.log(S_r) + 1.) 

587 write(fmt % ('S_rot', S_r, S_r * temperature)) 

588 S += S_r 

589 

590 # Electronic entropy. 

591 S_e = units.kB * np.log(2 * self.spin + 1) 

592 write(fmt % ('S_elec', S_e, S_e * temperature)) 

593 S += S_e 

594 

595 # Vibrational entropy. 

596 S_v = self._vibrational_entropy_contribution(temperature) 

597 write(fmt % ('S_vib', S_v, S_v * temperature)) 

598 S += S_v 

599 

600 # Pressure correction to translational entropy. 

601 S_p = - units.kB * np.log(pressure / self.referencepressure) 

602 write(fmt % ('S (1 bar -> P)', S_p, S_p * temperature)) 

603 S += S_p 

604 

605 write('-' * 49) 

606 write(fmt % ('S', S, S * temperature)) 

607 write('=' * 49) 

608 return S 

609 

610 def get_gibbs_energy(self, temperature, pressure, verbose=True): 

611 """Returns the Gibbs free energy, in eV, in the ideal gas 

612 approximation at a specified temperature (K) and pressure (Pa).""" 

613 

614 self.verbose = verbose 

615 write = self._vprint 

616 

617 H = self.get_enthalpy(temperature, verbose=verbose) 

618 write('') 

619 S = self.get_entropy(temperature, pressure, verbose=verbose) 

620 G = H - temperature * S 

621 

622 write('') 

623 write('Free energy components at T = %.2f K and P = %.1f Pa:' % 

624 (temperature, pressure)) 

625 write('=' * 23) 

626 fmt = '%5s%15.3f eV' 

627 write(fmt % ('H', H)) 

628 write(fmt % ('-T*S', -temperature * S)) 

629 write('-' * 23) 

630 write(fmt % ('G', G)) 

631 write('=' * 23) 

632 return G 

633 

634 

635class CrystalThermo(ThermoChem): 

636 """Class for calculating thermodynamic properties of a crystalline 

637 solid in the approximation that a lattice of N atoms behaves as a 

638 system of 3N independent harmonic oscillators. 

639 

640 Inputs: 

641 

642 phonon_DOS : list 

643 a list of the phonon density of states, 

644 where each value represents the phonon DOS at the vibrational energy 

645 value of the corresponding index in phonon_energies. 

646 

647 phonon_energies : list 

648 a list of the range of vibrational energies (hbar*omega) over which 

649 the phonon density of states has been evaluated. This list should be 

650 the same length as phonon_DOS and integrating phonon_DOS over 

651 phonon_energies should yield approximately 3N, where N is the number 

652 of atoms per unit cell. If the first element of this list is 

653 zero-valued it will be deleted along with the first element of 

654 phonon_DOS. Units of vibrational energies are eV. 

655 

656 potentialenergy : float 

657 the potential energy in eV (e.g., from atoms.get_potential_energy) 

658 (if potentialenergy is unspecified, then the methods of this 

659 class can be interpreted as the energy corrections) 

660 

661 formula_units : int 

662 the number of formula units per unit cell. If unspecified, the 

663 thermodynamic quantities calculated will be listed on a 

664 per-unit-cell basis. 

665 """ 

666 

667 def __init__(self, phonon_DOS, phonon_energies, 

668 formula_units=None, potentialenergy=0.): 

669 self.phonon_energies = phonon_energies 

670 self.phonon_DOS = phonon_DOS 

671 

672 if formula_units: 

673 self.formula_units = formula_units 

674 self.potentialenergy = potentialenergy / formula_units 

675 else: 

676 self.formula_units = 0 

677 self.potentialenergy = potentialenergy 

678 

679 def get_internal_energy(self, temperature, verbose=True): 

680 """Returns the internal energy, in eV, of crystalline solid 

681 at a specified temperature (K).""" 

682 

683 self.verbose = verbose 

684 write = self._vprint 

685 fmt = '%-15s%13.4f eV' 

686 if self.formula_units == 0: 

687 write('Internal energy components at ' 

688 'T = %.2f K,\non a per-unit-cell basis:' % temperature) 

689 else: 

690 write('Internal energy components at ' 

691 'T = %.2f K,\non a per-formula-unit basis:' % temperature) 

692 write('=' * 31) 

693 

694 U = 0. 

695 

696 omega_e = self.phonon_energies 

697 dos_e = self.phonon_DOS 

698 if omega_e[0] == 0.: 

699 omega_e = np.delete(omega_e, 0) 

700 dos_e = np.delete(dos_e, 0) 

701 

702 write(fmt % ('E_pot', self.potentialenergy)) 

703 U += self.potentialenergy 

704 

705 zpe_list = omega_e / 2. 

706 if self.formula_units == 0: 

707 zpe = np.trapz(zpe_list * dos_e, omega_e) 

708 else: 

709 zpe = np.trapz(zpe_list * dos_e, omega_e) / self.formula_units 

710 write(fmt % ('E_ZPE', zpe)) 

711 U += zpe 

712 

713 B = 1. / (units.kB * temperature) 

714 E_vib = omega_e / (np.exp(omega_e * B) - 1.) 

715 if self.formula_units == 0: 

716 E_phonon = np.trapz(E_vib * dos_e, omega_e) 

717 else: 

718 E_phonon = np.trapz(E_vib * dos_e, omega_e) / self.formula_units 

719 write(fmt % ('E_phonon', E_phonon)) 

720 U += E_phonon 

721 

722 write('-' * 31) 

723 write(fmt % ('U', U)) 

724 write('=' * 31) 

725 return U 

726 

727 def get_entropy(self, temperature, verbose=True): 

728 """Returns the entropy, in eV/K, of crystalline solid 

729 at a specified temperature (K).""" 

730 

731 self.verbose = verbose 

732 write = self._vprint 

733 fmt = '%-15s%13.7f eV/K%13.4f eV' 

734 if self.formula_units == 0: 

735 write('Entropy components at ' 

736 'T = %.2f K,\non a per-unit-cell basis:' % temperature) 

737 else: 

738 write('Entropy components at ' 

739 'T = %.2f K,\non a per-formula-unit basis:' % temperature) 

740 write('=' * 49) 

741 write('%15s%13s %13s' % ('', 'S', 'T*S')) 

742 

743 omega_e = self.phonon_energies 

744 dos_e = self.phonon_DOS 

745 if omega_e[0] == 0.: 

746 omega_e = np.delete(omega_e, 0) 

747 dos_e = np.delete(dos_e, 0) 

748 

749 B = 1. / (units.kB * temperature) 

750 S_vib = (omega_e / (temperature * (np.exp(omega_e * B) - 1.)) - 

751 units.kB * np.log(1. - np.exp(-omega_e * B))) 

752 if self.formula_units == 0: 

753 S = np.trapz(S_vib * dos_e, omega_e) 

754 else: 

755 S = np.trapz(S_vib * dos_e, omega_e) / self.formula_units 

756 

757 write('-' * 49) 

758 write(fmt % ('S', S, S * temperature)) 

759 write('=' * 49) 

760 return S 

761 

762 def get_helmholtz_energy(self, temperature, verbose=True): 

763 """Returns the Helmholtz free energy, in eV, of crystalline solid 

764 at a specified temperature (K).""" 

765 

766 self.verbose = True 

767 write = self._vprint 

768 

769 U = self.get_internal_energy(temperature, verbose=verbose) 

770 write('') 

771 S = self.get_entropy(temperature, verbose=verbose) 

772 F = U - temperature * S 

773 

774 write('') 

775 if self.formula_units == 0: 

776 write('Helmholtz free energy components at ' 

777 'T = %.2f K,\non a per-unit-cell basis:' % temperature) 

778 else: 

779 write('Helmholtz free energy components at ' 

780 'T = %.2f K,\non a per-formula-unit basis:' % temperature) 

781 write('=' * 23) 

782 fmt = '%5s%15.4f eV' 

783 write(fmt % ('U', U)) 

784 write(fmt % ('-T*S', -temperature * S)) 

785 write('-' * 23) 

786 write(fmt % ('F', F)) 

787 write('=' * 23) 

788 return F 

789 

790 

791def _clean_vib_energies(vib_energies, ignore_imag_modes=False): 

792 """Checks for presence of imaginary vibrational modes and removes 

793 them if ignore_imag_modes is True. Also removes +0.j from real 

794 vibrational energies. 

795 

796 Inputs: 

797 

798 vib_energies : list 

799 a list of the vibrational energies 

800 

801 ignore_imag_modes : bool 

802 If True, any imaginary frequencies will be removed. If False, 

803 (default), an error will be raised if imaginary frequencies are 

804 present. 

805 

806 Outputs: 

807 

808 vib_energies : list 

809 a list of the cleaned vibrational energies with imaginary frequencies 

810 removed if ignore_imag_modes is True. 

811 

812 n_imag : int 

813 the number of imaginary frequencies removed 

814 """ 

815 if ignore_imag_modes: 

816 n_vib_energies = len(vib_energies) 

817 vib_energies = [v for v in vib_energies if np.real(v) > 0] 

818 n_imag = n_vib_energies - len(vib_energies) 

819 if n_imag > 0: 

820 warn(f"{n_imag} imag modes removed", UserWarning) 

821 else: 

822 if sum(np.iscomplex(vib_energies)): 

823 raise ValueError('Imaginary vibrational energies are present.') 

824 n_imag = 0 

825 vib_energies = np.real(vib_energies) # clear +0.j 

826 

827 return vib_energies, n_imag