Coverage for /builds/ase/ase/ase/thermochemistry.py: 94.23%
416 statements
« prev ^ index » next coverage.py v7.5.3, created at 2025-08-02 00:12 +0000
« prev ^ index » next coverage.py v7.5.3, created at 2025-08-02 00:12 +0000
1# fmt: off
3"""Modules for calculating thermochemical information from computational
4outputs."""
6import os
7import sys
8from warnings import warn
10import numpy as np
11from scipy.integrate import trapezoid
13from ase import units
16class ThermoChem:
17 """Base class containing common methods used in thermochemistry
18 calculations."""
20 def get_ZPE_correction(self):
21 """Returns the zero-point vibrational energy correction in eV."""
22 return 0.5 * np.sum(self.vib_energies)
24 def _vibrational_energy_contribution(self, temperature):
25 """Calculates the change in internal energy due to vibrations from
26 0K to the specified temperature for a set of vibrations given in
27 eV and a temperature given in Kelvin. Returns the energy change
28 in eV."""
29 kT = units.kB * temperature
30 dU = 0.
31 for energy in self.vib_energies:
32 dU += energy / (np.exp(energy / kT) - 1.)
33 return dU
35 def _vibrational_entropy_contribution(self, temperature):
36 """Calculates the entropy due to vibrations for a set of vibrations
37 given in eV and a temperature given in Kelvin. Returns the entropy
38 in eV/K."""
39 kT = units.kB * temperature
40 S_v = 0.
41 for energy in self.vib_energies:
42 x = energy / kT
43 S_v += x / (np.exp(x) - 1.) - np.log(1. - np.exp(-x))
44 S_v *= units.kB
45 return S_v
47 def _vprint(self, text):
48 """Print output if verbose flag True."""
49 if self.verbose:
50 sys.stdout.write(text + os.linesep)
53class HarmonicThermo(ThermoChem):
54 """Class for calculating thermodynamic properties in the approximation
55 that all degrees of freedom are treated harmonically. Often used for
56 adsorbates.
58 Inputs:
60 vib_energies : list
61 a list of the harmonic energies of the adsorbate (e.g., from
62 ase.vibrations.Vibrations.get_energies). The number of
63 energies should match the number of degrees of freedom of the
64 adsorbate; i.e., 3*n, where n is the number of atoms. Note that
65 this class does not check that the user has supplied the correct
66 number of energies. Units of energies are eV.
67 potentialenergy : float
68 the potential energy in eV (e.g., from atoms.get_potential_energy)
69 (if potentialenergy is unspecified, then the methods of this
70 class can be interpreted as the energy corrections)
71 ignore_imag_modes : bool
72 If True, any imaginary frequencies will be ignored in the calculation
73 of the thermochemical properties. If False (default), an error will
74 be raised if any imaginary frequencies are present.
75 """
77 def __init__(self, vib_energies, potentialenergy=0.,
78 ignore_imag_modes=False):
79 self.ignore_imag_modes = ignore_imag_modes
81 # Check for imaginary frequencies.
82 vib_energies, n_imag = _clean_vib_energies(
83 vib_energies, ignore_imag_modes=ignore_imag_modes
84 )
85 self.vib_energies = vib_energies
86 self.n_imag = n_imag
88 self.potentialenergy = potentialenergy
90 def get_internal_energy(self, temperature, verbose=True):
91 """Returns the internal energy, in eV, in the harmonic approximation
92 at a specified temperature (K)."""
94 self.verbose = verbose
95 write = self._vprint
96 fmt = '%-15s%13.3f eV'
97 write('Internal energy components at T = %.2f K:' % temperature)
98 write('=' * 31)
100 U = 0.
102 write(fmt % ('E_pot', self.potentialenergy))
103 U += self.potentialenergy
105 zpe = self.get_ZPE_correction()
106 write(fmt % ('E_ZPE', zpe))
107 U += zpe
109 dU_v = self._vibrational_energy_contribution(temperature)
110 write(fmt % ('Cv_harm (0->T)', dU_v))
111 U += dU_v
113 write('-' * 31)
114 write(fmt % ('U', U))
115 write('=' * 31)
116 return U
118 def get_entropy(self, temperature, verbose=True):
119 """Returns the entropy, in eV/K, in the harmonic approximation
120 at a specified temperature (K)."""
122 self.verbose = verbose
123 write = self._vprint
124 fmt = '%-15s%13.7f eV/K%13.3f eV'
125 write('Entropy components at T = %.2f K:' % temperature)
126 write('=' * 49)
127 write('%15s%13s %13s' % ('', 'S', 'T*S'))
129 S = 0.
131 S_v = self._vibrational_entropy_contribution(temperature)
132 write(fmt % ('S_harm', S_v, S_v * temperature))
133 S += S_v
135 write('-' * 49)
136 write(fmt % ('S', S, S * temperature))
137 write('=' * 49)
138 return S
140 def get_helmholtz_energy(self, temperature, verbose=True):
141 """Returns the Helmholtz free energy, in eV, in the harmonic
142 approximation at a specified temperature (K)."""
144 self.verbose = True
145 write = self._vprint
147 U = self.get_internal_energy(temperature, verbose=verbose)
148 write('')
149 S = self.get_entropy(temperature, verbose=verbose)
150 F = U - temperature * S
152 write('')
153 write('Free energy components at T = %.2f K:' % temperature)
154 write('=' * 23)
155 fmt = '%5s%15.3f eV'
156 write(fmt % ('U', U))
157 write(fmt % ('-T*S', -temperature * S))
158 write('-' * 23)
159 write(fmt % ('F', F))
160 write('=' * 23)
161 return F
164class HinderedThermo(ThermoChem):
165 """Class for calculating thermodynamic properties in the hindered
166 translator and hindered rotor model where all but three degrees of
167 freedom are treated as harmonic vibrations, two are treated as
168 hindered translations, and one is treated as a hindered rotation.
170 Inputs:
172 vib_energies : list
173 a list of all the vibrational energies of the adsorbate (e.g., from
174 ase.vibrations.Vibrations.get_energies). If atoms is not provided,
175 then the number of energies must match the number of degrees of freedom
176 of the adsorbate; i.e., 3*n, where n is the number of atoms. Note
177 that this class does not check that the user has supplied the
178 correct number of energies.
179 Units of energies are eV.
180 trans_barrier_energy : float
181 the translational energy barrier in eV. This is the barrier for an
182 adsorbate to diffuse on the surface.
183 rot_barrier_energy : float
184 the rotational energy barrier in eV. This is the barrier for an
185 adsorbate to rotate about an axis perpendicular to the surface.
186 sitedensity : float
187 density of surface sites in cm^-2
188 rotationalminima : integer
189 the number of equivalent minima for an adsorbate's full rotation.
190 For example, 6 for an adsorbate on an fcc(111) top site
191 potentialenergy : float
192 the potential energy in eV (e.g., from atoms.get_potential_energy)
193 (if potentialenergy is unspecified, then the methods of this class
194 can be interpreted as the energy corrections)
195 mass : float
196 the mass of the adsorbate in amu (if mass is unspecified, then it will
197 be calculated from the atoms class)
198 inertia : float
199 the reduced moment of inertia of the adsorbate in amu*Ang^-2
200 (if inertia is unspecified, then it will be calculated from the
201 atoms class)
202 atoms : an ASE atoms object
203 used to calculate rotational moments of inertia and molecular mass
204 symmetrynumber : integer
205 symmetry number of the adsorbate. This is the number of symmetric arms
206 of the adsorbate and depends upon how it is bound to the surface.
207 For example, propane bound through its end carbon has a symmetry
208 number of 1 but propane bound through its middle carbon has a symmetry
209 number of 2. (if symmetrynumber is unspecified, then the default is 1)
210 ignore_imag_modes : bool
211 If True, any imaginary frequencies present after the 3N-3 cut will not
212 be included in the calculation of the thermochemical properties. If
213 False (default), an error will be raised if imaginary frequencies are
214 present after the 3N-3 cut.
215 """
217 def __init__(self, vib_energies, trans_barrier_energy, rot_barrier_energy,
218 sitedensity, rotationalminima, potentialenergy=0.,
219 mass=None, inertia=None, atoms=None, symmetrynumber=1,
220 ignore_imag_modes=False):
222 self.trans_barrier_energy = trans_barrier_energy * units._e
223 self.rot_barrier_energy = rot_barrier_energy * units._e
224 self.area = 1. / sitedensity / 100.0**2
225 self.rotationalminima = rotationalminima
226 self.potentialenergy = potentialenergy
227 self.atoms = atoms
228 self.symmetry = symmetrynumber
229 self.ignore_imag_modes = ignore_imag_modes
231 # Sort the vibrations
232 vib_energies = list(vib_energies)
233 vib_energies.sort(key=np.abs)
235 # Keep only the relevant vibrational energies (3N-3)
236 if atoms:
237 vib_energies = vib_energies[-(3 * len(atoms) - 3):]
238 else:
239 vib_energies = vib_energies[-(len(vib_energies) - 3):]
241 # Check for imaginary frequencies.
242 vib_energies, n_imag = _clean_vib_energies(
243 vib_energies, ignore_imag_modes=ignore_imag_modes
244 )
245 self.vib_energies = vib_energies
246 self.n_imag = n_imag
248 if (mass or atoms) and (inertia or atoms):
249 if mass:
250 self.mass = mass * units._amu
251 elif atoms:
252 self.mass = np.sum(atoms.get_masses()) * units._amu
253 if inertia:
254 self.inertia = inertia * units._amu / units.m**2
255 elif atoms:
256 self.inertia = (atoms.get_moments_of_inertia()[2] *
257 units._amu / units.m**2)
258 else:
259 raise RuntimeError('Either mass and inertia of the '
260 'adsorbate must be specified or '
261 'atoms must be specified.')
263 # Calculate hindered translational and rotational frequencies
264 self.freq_t = np.sqrt(self.trans_barrier_energy / (2 * self.mass *
265 self.area))
266 self.freq_r = 1. / (2 * np.pi) * np.sqrt(self.rotationalminima**2 *
267 self.rot_barrier_energy /
268 (2 * self.inertia))
270 def get_internal_energy(self, temperature, verbose=True):
271 """Returns the internal energy (including the zero point energy),
272 in eV, in the hindered translator and hindered rotor model at a
273 specified temperature (K)."""
275 from scipy.special import iv
277 self.verbose = verbose
278 write = self._vprint
279 fmt = '%-15s%13.3f eV'
280 write('Internal energy components at T = %.2f K:' % temperature)
281 write('=' * 31)
283 U = 0.
285 write(fmt % ('E_pot', self.potentialenergy))
286 U += self.potentialenergy
288 # Translational Energy
289 T_t = units._k * temperature / (units._hplanck * self.freq_t)
290 R_t = self.trans_barrier_energy / (units._hplanck * self.freq_t)
291 dU_t = 2 * (-1. / 2 - 1. / T_t / (2 + 16 * R_t) + R_t / 2 / T_t -
292 R_t / 2 / T_t *
293 iv(1, R_t / 2 / T_t) / iv(0, R_t / 2 / T_t) +
294 1. / T_t / (np.exp(1. / T_t) - 1))
295 dU_t *= units.kB * temperature
296 write(fmt % ('E_trans', dU_t))
297 U += dU_t
299 # Rotational Energy
300 T_r = units._k * temperature / (units._hplanck * self.freq_r)
301 R_r = self.rot_barrier_energy / (units._hplanck * self.freq_r)
302 dU_r = (-1. / 2 - 1. / T_r / (2 + 16 * R_r) + R_r / 2 / T_r -
303 R_r / 2 / T_r *
304 iv(1, R_r / 2 / T_r) / iv(0, R_r / 2 / T_r) +
305 1. / T_r / (np.exp(1. / T_r) - 1))
306 dU_r *= units.kB * temperature
307 write(fmt % ('E_rot', dU_r))
308 U += dU_r
310 # Vibrational Energy
311 dU_v = self._vibrational_energy_contribution(temperature)
312 write(fmt % ('E_vib', dU_v))
313 U += dU_v
315 # Zero Point Energy
316 dU_zpe = self.get_zero_point_energy()
317 write(fmt % ('E_ZPE', dU_zpe))
318 U += dU_zpe
320 write('-' * 31)
321 write(fmt % ('U', U))
322 write('=' * 31)
323 return U
325 def get_zero_point_energy(self, verbose=True):
326 """Returns the zero point energy, in eV, in the hindered
327 translator and hindered rotor model"""
329 zpe_t = 2 * (1. / 2 * self.freq_t * units._hplanck / units._e)
330 zpe_r = 1. / 2 * self.freq_r * units._hplanck / units._e
331 zpe_v = self.get_ZPE_correction()
332 zpe = zpe_t + zpe_r + zpe_v
333 return zpe
335 def get_entropy(self, temperature, verbose=True):
336 """Returns the entropy, in eV/K, in the hindered translator
337 and hindered rotor model at a specified temperature (K)."""
339 from scipy.special import iv
341 self.verbose = verbose
342 write = self._vprint
343 fmt = '%-15s%13.7f eV/K%13.3f eV'
344 write('Entropy components at T = %.2f K:' % temperature)
345 write('=' * 49)
346 write('%15s%13s %13s' % ('', 'S', 'T*S'))
348 S = 0.
350 # Translational Entropy
351 T_t = units._k * temperature / (units._hplanck * self.freq_t)
352 R_t = self.trans_barrier_energy / (units._hplanck * self.freq_t)
353 S_t = 2 * (-1. / 2 + 1. / 2 * np.log(np.pi * R_t / T_t) -
354 R_t / 2 / T_t *
355 iv(1, R_t / 2 / T_t) / iv(0, R_t / 2 / T_t) +
356 np.log(iv(0, R_t / 2 / T_t)) +
357 1. / T_t / (np.exp(1. / T_t) - 1) -
358 np.log(1 - np.exp(-1. / T_t)))
359 S_t *= units.kB
360 write(fmt % ('S_trans', S_t, S_t * temperature))
361 S += S_t
363 # Rotational Entropy
364 T_r = units._k * temperature / (units._hplanck * self.freq_r)
365 R_r = self.rot_barrier_energy / (units._hplanck * self.freq_r)
366 S_r = (-1. / 2 + 1. / 2 * np.log(np.pi * R_r / T_r) -
367 np.log(self.symmetry) -
368 R_r / 2 / T_r * iv(1, R_r / 2 / T_r) / iv(0, R_r / 2 / T_r) +
369 np.log(iv(0, R_r / 2 / T_r)) +
370 1. / T_r / (np.exp(1. / T_r) - 1) -
371 np.log(1 - np.exp(-1. / T_r)))
372 S_r *= units.kB
373 write(fmt % ('S_rot', S_r, S_r * temperature))
374 S += S_r
376 # Vibrational Entropy
377 S_v = self._vibrational_entropy_contribution(temperature)
378 write(fmt % ('S_vib', S_v, S_v * temperature))
379 S += S_v
381 # Concentration Related Entropy
382 N_over_A = np.exp(1. / 3) * (10.0**5 /
383 (units._k * temperature))**(2. / 3)
384 S_c = 1 - np.log(N_over_A) - np.log(self.area)
385 S_c *= units.kB
386 write(fmt % ('S_con', S_c, S_c * temperature))
387 S += S_c
389 write('-' * 49)
390 write(fmt % ('S', S, S * temperature))
391 write('=' * 49)
392 return S
394 def get_helmholtz_energy(self, temperature, verbose=True):
395 """Returns the Helmholtz free energy, in eV, in the hindered
396 translator and hindered rotor model at a specified temperature
397 (K)."""
399 self.verbose = True
400 write = self._vprint
402 U = self.get_internal_energy(temperature, verbose=verbose)
403 write('')
404 S = self.get_entropy(temperature, verbose=verbose)
405 F = U - temperature * S
407 write('')
408 write('Free energy components at T = %.2f K:' % temperature)
409 write('=' * 23)
410 fmt = '%5s%15.3f eV'
411 write(fmt % ('U', U))
412 write(fmt % ('-T*S', -temperature * S))
413 write('-' * 23)
414 write(fmt % ('F', F))
415 write('=' * 23)
416 return F
419class IdealGasThermo(ThermoChem):
420 """Class for calculating thermodynamic properties of a molecule
421 based on statistical mechanical treatments in the ideal gas
422 approximation.
424 Inputs for enthalpy calculations:
426 vib_energies : list
427 a list of the vibrational energies of the molecule (e.g., from
428 ase.vibrations.Vibrations.get_energies). The number of vibrations
429 used is automatically calculated by the geometry and the number of
430 atoms. If more are specified than are needed, then the lowest
431 numbered vibrations are neglected. If either atoms or natoms is
432 unspecified, then uses the entire list. Units are eV.
433 geometry : 'monatomic', 'linear', or 'nonlinear'
434 geometry of the molecule
435 potentialenergy : float
436 the potential energy in eV (e.g., from atoms.get_potential_energy)
437 (if potentialenergy is unspecified, then the methods of this
438 class can be interpreted as the energy corrections)
439 natoms : integer
440 the number of atoms, used along with 'geometry' to determine how
441 many vibrations to use. (Not needed if an atoms object is supplied
442 in 'atoms' or if the user desires the entire list of vibrations
443 to be used.)
445 Extra inputs needed for entropy / free energy calculations:
447 atoms : an ASE atoms object
448 used to calculate rotational moments of inertia and molecular mass
449 symmetrynumber : integer
450 symmetry number of the molecule. See, for example, Table 10.1 and
451 Appendix B of C. Cramer "Essentials of Computational Chemistry",
452 2nd Ed.
453 spin : float
454 the total electronic spin. (0 for molecules in which all electrons
455 are paired, 0.5 for a free radical with a single unpaired electron,
456 1.0 for a triplet with two unpaired electrons, such as O_2.)
457 ignore_imag_modes : bool
458 If True, any imaginary frequencies present after the 3N-5/3N-6 cut
459 will not be included in the calculation of the thermochemical
460 properties. If False (default), a ValueError will be raised if
461 any imaginary frequencies remain after the 3N-5/3N-6 cut.
462 """
464 def __init__(self, vib_energies, geometry, potentialenergy=0.,
465 atoms=None, symmetrynumber=None, spin=None, natoms=None,
466 ignore_imag_modes=False):
467 self.potentialenergy = potentialenergy
468 self.geometry = geometry
469 self.atoms = atoms
470 self.sigma = symmetrynumber
471 self.spin = spin
472 self.ignore_imag_modes = ignore_imag_modes
473 if natoms is None and atoms:
474 natoms = len(atoms)
475 self.natoms = natoms
477 # Sort the vibrations
478 vib_energies = list(vib_energies)
479 vib_energies.sort(key=np.abs)
481 # Cut the vibrations to those needed from the geometry.
482 if natoms:
483 if geometry == 'nonlinear':
484 vib_energies = vib_energies[-(3 * natoms - 6):]
485 elif geometry == 'linear':
486 vib_energies = vib_energies[-(3 * natoms - 5):]
487 elif geometry == 'monatomic':
488 vib_energies = []
489 else:
490 raise ValueError(f"Unsupported geometry: {geometry}")
492 # Check for imaginary frequencies.
493 vib_energies, n_imag = _clean_vib_energies(
494 vib_energies, ignore_imag_modes=ignore_imag_modes
495 )
496 self.vib_energies = vib_energies
497 self.n_imag = n_imag
499 self.referencepressure = 1.0e5 # Pa
501 def get_enthalpy(self, temperature, verbose=True):
502 """Returns the enthalpy, in eV, in the ideal gas approximation
503 at a specified temperature (K)."""
505 self.verbose = verbose
506 write = self._vprint
507 fmt = '%-15s%13.3f eV'
508 write('Enthalpy components at T = %.2f K:' % temperature)
509 write('=' * 31)
511 H = 0.
513 write(fmt % ('E_pot', self.potentialenergy))
514 H += self.potentialenergy
516 zpe = self.get_ZPE_correction()
517 write(fmt % ('E_ZPE', zpe))
518 H += zpe
520 Cv_t = 3. / 2. * units.kB # translational heat capacity (3-d gas)
521 write(fmt % ('Cv_trans (0->T)', Cv_t * temperature))
522 H += Cv_t * temperature
524 if self.geometry == 'nonlinear': # rotational heat capacity
525 Cv_r = 3. / 2. * units.kB
526 elif self.geometry == 'linear':
527 Cv_r = units.kB
528 elif self.geometry == 'monatomic':
529 Cv_r = 0.
530 write(fmt % ('Cv_rot (0->T)', Cv_r * temperature))
531 H += Cv_r * temperature
533 dH_v = self._vibrational_energy_contribution(temperature)
534 write(fmt % ('Cv_vib (0->T)', dH_v))
535 H += dH_v
537 Cp_corr = units.kB * temperature
538 write(fmt % ('(C_v -> C_p)', Cp_corr))
539 H += Cp_corr
541 write('-' * 31)
542 write(fmt % ('H', H))
543 write('=' * 31)
544 return H
546 def get_entropy(self, temperature, pressure, verbose=True):
547 """Returns the entropy, in eV/K, in the ideal gas approximation
548 at a specified temperature (K) and pressure (Pa)."""
550 if self.atoms is None or self.sigma is None or self.spin is None:
551 raise RuntimeError('atoms, symmetrynumber, and spin must be '
552 'specified for entropy and free energy '
553 'calculations.')
554 self.verbose = verbose
555 write = self._vprint
556 fmt = '%-15s%13.7f eV/K%13.3f eV'
557 write('Entropy components at T = %.2f K and P = %.1f Pa:' %
558 (temperature, pressure))
559 write('=' * 49)
560 write('%15s%13s %13s' % ('', 'S', 'T*S'))
562 S = 0.0
564 # Translational entropy (term inside the log is in SI units).
565 mass = sum(self.atoms.get_masses()) * units._amu # kg/molecule
566 S_t = (2 * np.pi * mass * units._k *
567 temperature / units._hplanck**2)**(3.0 / 2)
568 S_t *= units._k * temperature / self.referencepressure
569 S_t = units.kB * (np.log(S_t) + 5.0 / 2.0)
570 write(fmt % ('S_trans (1 bar)', S_t, S_t * temperature))
571 S += S_t
573 # Rotational entropy (term inside the log is in SI units).
574 if self.geometry == 'monatomic':
575 S_r = 0.0
576 elif self.geometry == 'nonlinear':
577 inertias = (self.atoms.get_moments_of_inertia() * units._amu /
578 (10.0**10)**2) # kg m^2
579 S_r = np.sqrt(np.pi * np.prod(inertias)) / self.sigma
580 S_r *= (8.0 * np.pi**2 * units._k * temperature /
581 units._hplanck**2)**(3.0 / 2.0)
582 S_r = units.kB * (np.log(S_r) + 3.0 / 2.0)
583 elif self.geometry == 'linear':
584 inertias = (self.atoms.get_moments_of_inertia() * units._amu /
585 (10.0**10)**2) # kg m^2
586 inertia = max(inertias) # should be two identical and one zero
587 S_r = (8 * np.pi**2 * inertia * units._k * temperature /
588 self.sigma / units._hplanck**2)
589 S_r = units.kB * (np.log(S_r) + 1.)
590 write(fmt % ('S_rot', S_r, S_r * temperature))
591 S += S_r
593 # Electronic entropy.
594 S_e = units.kB * np.log(2 * self.spin + 1)
595 write(fmt % ('S_elec', S_e, S_e * temperature))
596 S += S_e
598 # Vibrational entropy.
599 S_v = self._vibrational_entropy_contribution(temperature)
600 write(fmt % ('S_vib', S_v, S_v * temperature))
601 S += S_v
603 # Pressure correction to translational entropy.
604 S_p = - units.kB * np.log(pressure / self.referencepressure)
605 write(fmt % ('S (1 bar -> P)', S_p, S_p * temperature))
606 S += S_p
608 write('-' * 49)
609 write(fmt % ('S', S, S * temperature))
610 write('=' * 49)
611 return S
613 def get_gibbs_energy(self, temperature, pressure, verbose=True):
614 """Returns the Gibbs free energy, in eV, in the ideal gas
615 approximation at a specified temperature (K) and pressure (Pa)."""
617 self.verbose = verbose
618 write = self._vprint
620 H = self.get_enthalpy(temperature, verbose=verbose)
621 write('')
622 S = self.get_entropy(temperature, pressure, verbose=verbose)
623 G = H - temperature * S
625 write('')
626 write('Free energy components at T = %.2f K and P = %.1f Pa:' %
627 (temperature, pressure))
628 write('=' * 23)
629 fmt = '%5s%15.3f eV'
630 write(fmt % ('H', H))
631 write(fmt % ('-T*S', -temperature * S))
632 write('-' * 23)
633 write(fmt % ('G', G))
634 write('=' * 23)
635 return G
638class CrystalThermo(ThermoChem):
639 """Class for calculating thermodynamic properties of a crystalline
640 solid in the approximation that a lattice of N atoms behaves as a
641 system of 3N independent harmonic oscillators.
643 Inputs:
645 phonon_DOS : list
646 a list of the phonon density of states,
647 where each value represents the phonon DOS at the vibrational energy
648 value of the corresponding index in phonon_energies.
650 phonon_energies : list
651 a list of the range of vibrational energies (hbar*omega) over which
652 the phonon density of states has been evaluated. This list should be
653 the same length as phonon_DOS and integrating phonon_DOS over
654 phonon_energies should yield approximately 3N, where N is the number
655 of atoms per unit cell. If the first element of this list is
656 zero-valued it will be deleted along with the first element of
657 phonon_DOS. Units of vibrational energies are eV.
659 potentialenergy : float
660 the potential energy in eV (e.g., from atoms.get_potential_energy)
661 (if potentialenergy is unspecified, then the methods of this
662 class can be interpreted as the energy corrections)
664 formula_units : int
665 the number of formula units per unit cell. If unspecified, the
666 thermodynamic quantities calculated will be listed on a
667 per-unit-cell basis.
668 """
670 def __init__(self, phonon_DOS, phonon_energies,
671 formula_units=None, potentialenergy=0.):
672 self.phonon_energies = phonon_energies
673 self.phonon_DOS = phonon_DOS
675 if formula_units:
676 self.formula_units = formula_units
677 self.potentialenergy = potentialenergy / formula_units
678 else:
679 self.formula_units = 0
680 self.potentialenergy = potentialenergy
682 def get_internal_energy(self, temperature, verbose=True):
683 """Returns the internal energy, in eV, of crystalline solid
684 at a specified temperature (K)."""
686 self.verbose = verbose
687 write = self._vprint
688 fmt = '%-15s%13.4f eV'
689 if self.formula_units == 0:
690 write('Internal energy components at '
691 'T = %.2f K,\non a per-unit-cell basis:' % temperature)
692 else:
693 write('Internal energy components at '
694 'T = %.2f K,\non a per-formula-unit basis:' % temperature)
695 write('=' * 31)
697 U = 0.
699 omega_e = self.phonon_energies
700 dos_e = self.phonon_DOS
701 if omega_e[0] == 0.:
702 omega_e = np.delete(omega_e, 0)
703 dos_e = np.delete(dos_e, 0)
705 write(fmt % ('E_pot', self.potentialenergy))
706 U += self.potentialenergy
708 zpe_list = omega_e / 2.
709 if self.formula_units == 0:
710 zpe = trapezoid(zpe_list * dos_e, omega_e)
711 else:
712 zpe = trapezoid(zpe_list * dos_e, omega_e) / self.formula_units
713 write(fmt % ('E_ZPE', zpe))
714 U += zpe
716 B = 1. / (units.kB * temperature)
717 E_vib = omega_e / (np.exp(omega_e * B) - 1.)
718 if self.formula_units == 0:
719 E_phonon = trapezoid(E_vib * dos_e, omega_e)
720 else:
721 E_phonon = trapezoid(E_vib * dos_e, omega_e) / self.formula_units
722 write(fmt % ('E_phonon', E_phonon))
723 U += E_phonon
725 write('-' * 31)
726 write(fmt % ('U', U))
727 write('=' * 31)
728 return U
730 def get_entropy(self, temperature, verbose=True):
731 """Returns the entropy, in eV/K, of crystalline solid
732 at a specified temperature (K)."""
734 self.verbose = verbose
735 write = self._vprint
736 fmt = '%-15s%13.7f eV/K%13.4f eV'
737 if self.formula_units == 0:
738 write('Entropy components at '
739 'T = %.2f K,\non a per-unit-cell basis:' % temperature)
740 else:
741 write('Entropy components at '
742 'T = %.2f K,\non a per-formula-unit basis:' % temperature)
743 write('=' * 49)
744 write('%15s%13s %13s' % ('', 'S', 'T*S'))
746 omega_e = self.phonon_energies
747 dos_e = self.phonon_DOS
748 if omega_e[0] == 0.:
749 omega_e = np.delete(omega_e, 0)
750 dos_e = np.delete(dos_e, 0)
752 B = 1. / (units.kB * temperature)
753 S_vib = (omega_e / (temperature * (np.exp(omega_e * B) - 1.)) -
754 units.kB * np.log(1. - np.exp(-omega_e * B)))
755 if self.formula_units == 0:
756 S = trapezoid(S_vib * dos_e, omega_e)
757 else:
758 S = trapezoid(S_vib * dos_e, omega_e) / self.formula_units
760 write('-' * 49)
761 write(fmt % ('S', S, S * temperature))
762 write('=' * 49)
763 return S
765 def get_helmholtz_energy(self, temperature, verbose=True):
766 """Returns the Helmholtz free energy, in eV, of crystalline solid
767 at a specified temperature (K)."""
769 self.verbose = True
770 write = self._vprint
772 U = self.get_internal_energy(temperature, verbose=verbose)
773 write('')
774 S = self.get_entropy(temperature, verbose=verbose)
775 F = U - temperature * S
777 write('')
778 if self.formula_units == 0:
779 write('Helmholtz free energy components at '
780 'T = %.2f K,\non a per-unit-cell basis:' % temperature)
781 else:
782 write('Helmholtz free energy components at '
783 'T = %.2f K,\non a per-formula-unit basis:' % temperature)
784 write('=' * 23)
785 fmt = '%5s%15.4f eV'
786 write(fmt % ('U', U))
787 write(fmt % ('-T*S', -temperature * S))
788 write('-' * 23)
789 write(fmt % ('F', F))
790 write('=' * 23)
791 return F
794def _clean_vib_energies(vib_energies, ignore_imag_modes=False):
795 """Checks for presence of imaginary vibrational modes and removes
796 them if ignore_imag_modes is True. Also removes +0.j from real
797 vibrational energies.
799 Inputs:
801 vib_energies : list
802 a list of the vibrational energies
804 ignore_imag_modes : bool
805 If True, any imaginary frequencies will be removed. If False,
806 (default), an error will be raised if imaginary frequencies are
807 present.
809 Outputs:
811 vib_energies : list
812 a list of the cleaned vibrational energies with imaginary frequencies
813 removed if ignore_imag_modes is True.
815 n_imag : int
816 the number of imaginary frequencies removed
817 """
818 if ignore_imag_modes:
819 n_vib_energies = len(vib_energies)
820 vib_energies = [v for v in vib_energies if np.real(v) > 0]
821 n_imag = n_vib_energies - len(vib_energies)
822 if n_imag > 0:
823 warn(f"{n_imag} imag modes removed", UserWarning)
824 else:
825 if sum(np.iscomplex(vib_energies)):
826 raise ValueError('Imaginary vibrational energies are present.')
827 n_imag = 0
828 vib_energies = np.real(vib_energies) # clear +0.j
830 return vib_energies, n_imag