root / ase / thermochemistry.py
Historique | Voir | Annoter | Télécharger (14,07 ko)
1 |
#!/usr/bin/env python
|
---|---|
2 |
"""Modules for calculating thermochemical information from computational
|
3 |
outputs."""
|
4 |
|
5 |
import os |
6 |
import sys |
7 |
import numpy as np |
8 |
|
9 |
from ase import units |
10 |
|
11 |
def rotationalinertia(atoms): |
12 |
"""Calculates the three principle moments of inertia for an ASE atoms
|
13 |
object. This uses the atomic masses from ASE, which (if not explicitly
|
14 |
specified by the user) gives an inexact approximation of an isotopically
|
15 |
averaged result. Units are in amu*angstroms**2."""
|
16 |
|
17 |
# Calculate the center of mass.
|
18 |
xcm, ycm, zcm = atoms.get_center_of_mass() |
19 |
masses = atoms.get_masses() |
20 |
|
21 |
# Calculate moments of inertia in the current frame of reference.
|
22 |
Ixx = 0.
|
23 |
Iyy = 0.
|
24 |
Izz = 0.
|
25 |
Ixy = 0.
|
26 |
Ixz = 0.
|
27 |
Iyz = 0.
|
28 |
for index, atom in enumerate(atoms): |
29 |
m = masses[index] |
30 |
x = atom.get_x() - xcm |
31 |
y = atom.get_y() - ycm |
32 |
z = atom.get_z() - zcm |
33 |
Ixx += m * (y**2. + z**2.) |
34 |
Iyy += m * (x**2. + z**2.) |
35 |
Izz += m * (x**2. + y**2.) |
36 |
Ixy += m * x * y |
37 |
Ixz += m * x * z |
38 |
Iyz += m * y * z |
39 |
# Create the inertia tensor in the current frame of reference.
|
40 |
I_ = np.matrix([[ Ixx, -Ixy, -Ixz], |
41 |
[-Ixy, Iyy, -Iyz], |
42 |
[-Ixz, -Iyz, Izz]]) |
43 |
# Find the eigenvalues, which are the principle moments of inertia.
|
44 |
I = np.linalg.eigvals(I_) |
45 |
return I
|
46 |
|
47 |
class ThermoChem: |
48 |
"""Base class containing common methods used in thermochemistry
|
49 |
calculations."""
|
50 |
|
51 |
def get_ZPE_correction(self): |
52 |
"""Returns the zero-point vibrational energy correction in eV."""
|
53 |
zpe = 0.
|
54 |
for energy in self.vib_energies: |
55 |
zpe += 0.5 * energy
|
56 |
return zpe
|
57 |
|
58 |
def _vibrational_energy_contribution(self, temperature): |
59 |
"""Calculates the change in internal energy due to vibrations from
|
60 |
0K to the specified temperature for a set of vibrations given in
|
61 |
inverse centimeters and a temperature given in Kelvin. Returns the
|
62 |
energy change in eV."""
|
63 |
kT = units.kB * temperature |
64 |
dU = 0.
|
65 |
for energy in self.vib_energies: |
66 |
dU += energy / (np.exp(energy/kT) - 1.)
|
67 |
return dU
|
68 |
|
69 |
def _vibrational_entropy_contribution(self, temperature): |
70 |
"""Calculates the entropy due to vibrations for a set of vibrations
|
71 |
given in inverse centimeters and a temperature given in Kelvin.
|
72 |
Returns the entropy in eV/K."""
|
73 |
kT = units.kB * temperature |
74 |
S_v = 0.
|
75 |
for energy in self.vib_energies: |
76 |
x = energy / kT |
77 |
S_v += x / (np.exp(x)-1.) - np.log(1-np.exp(-x)) |
78 |
S_v *= units.kB |
79 |
return S_v
|
80 |
|
81 |
def _vprint(self, text): |
82 |
"""Print output if verbose flag True."""
|
83 |
if self.verbose: |
84 |
sys.stdout.write(text + os.linesep) |
85 |
|
86 |
class HarmonicThermo(ThermoChem): |
87 |
"""Class for calculating thermodynamic properties in the approximation
|
88 |
that all degrees of freedom are treated harmonically. Often used for
|
89 |
adsorbates.
|
90 |
|
91 |
Inputs:
|
92 |
|
93 |
vib_energies : list
|
94 |
a list of the harmonic energies of the adsorbate (e.g., from
|
95 |
ase.vibrations.Vibrations.get_energies). The number of
|
96 |
energies should match the number of degrees of freedom of the
|
97 |
adsorbate; i.e., 3*n, where n is the number of atoms. Note that
|
98 |
this class does not check that the user has supplied the correct
|
99 |
number of energies. Units of energies are eV.
|
100 |
electronicenergy : float
|
101 |
the electronic energy in eV
|
102 |
(If the electronicenergy is unspecified, then the methods of this
|
103 |
class can be interpreted as the energy corrections.)
|
104 |
"""
|
105 |
|
106 |
def __init__(self, vib_energies, electronicenergy=None): |
107 |
self.vib_energies = vib_energies
|
108 |
# Check for imaginary frequencies.
|
109 |
if sum(np.iscomplex(self.vib_energies)): |
110 |
raise ValueError('Imaginary vibrational energies are present.') |
111 |
else:
|
112 |
self.vib_energies = np.real(self.vib_energies) # clear +0.j |
113 |
|
114 |
if electronicenergy:
|
115 |
self.electronicenergy = electronicenergy
|
116 |
else:
|
117 |
self.electronicenergy = 0. |
118 |
|
119 |
def get_internal_energy(self, temperature, verbose=True): |
120 |
"""Returns the internal energy, in eV, in the harmonic approximation
|
121 |
at a specified temperature (K)."""
|
122 |
|
123 |
self.verbose = verbose
|
124 |
write = self._vprint
|
125 |
fmt = '%-15s%13.3f eV'
|
126 |
write('Internal energy components at T = %.2f K:' % temperature)
|
127 |
write('='*31) |
128 |
|
129 |
U = 0.
|
130 |
|
131 |
write(fmt % ('E_elec', self.electronicenergy)) |
132 |
U += self.electronicenergy
|
133 |
|
134 |
zpe = self.get_ZPE_correction()
|
135 |
write(fmt % ('E_ZPE', zpe))
|
136 |
U += zpe |
137 |
|
138 |
dU_v = self._vibrational_energy_contribution(temperature)
|
139 |
write(fmt % ('Cv_harm (0->T)', dU_v))
|
140 |
U += dU_v |
141 |
|
142 |
write('-'*31) |
143 |
write(fmt % ('U', U))
|
144 |
write('='*31) |
145 |
return U
|
146 |
|
147 |
def get_entropy(self, temperature, verbose=True): |
148 |
"""Returns the entropy, in eV/K, in the harmonic approximation
|
149 |
at a specified temperature (K)."""
|
150 |
|
151 |
self.verbose = verbose
|
152 |
write = self._vprint
|
153 |
fmt = '%-15s%13.7f eV/K%13.3f eV'
|
154 |
write('Entropy components at T = %.2f K:' % temperature)
|
155 |
write('='*49) |
156 |
write('%15s%13s %13s'%('', 'S', 'T*S')) |
157 |
|
158 |
S = 0.
|
159 |
|
160 |
S_v = self._vibrational_entropy_contribution(temperature)
|
161 |
write(fmt%('S_harm', S_v, S_v*temperature))
|
162 |
S += S_v |
163 |
|
164 |
write('-'*49) |
165 |
write(fmt%('S', S, S*temperature))
|
166 |
write('='*49) |
167 |
return S
|
168 |
|
169 |
def get_free_energy(self, temperature, verbose=True): |
170 |
"""Returns the free energy, in eV, in the harmonic approximation
|
171 |
at a specified temperature (K)."""
|
172 |
|
173 |
self.verbose = True |
174 |
write = self._vprint
|
175 |
|
176 |
U = self.get_internal_energy(temperature, verbose=verbose)
|
177 |
write('')
|
178 |
S = self.get_entropy(temperature, verbose=verbose)
|
179 |
G = U - temperature * S |
180 |
|
181 |
write('')
|
182 |
write('Free energy components at T = %.2f K:' % temperature)
|
183 |
write('='*23) |
184 |
fmt = '%5s%15.3f eV'
|
185 |
write(fmt % ('U', U))
|
186 |
write(fmt % ('-T*S', -temperature * S))
|
187 |
write('-'*23) |
188 |
write(fmt % ('G', G))
|
189 |
write('='*23) |
190 |
return G
|
191 |
|
192 |
class IdealGasThermo(ThermoChem): |
193 |
"""Class for calculating thermodynamic properties of a molecule
|
194 |
based on statistical mechanical treatments in the ideal gas
|
195 |
approximation.
|
196 |
|
197 |
Inputs for enthalpy calculations:
|
198 |
|
199 |
vib_energies : list
|
200 |
a list of the vibrational energies of the molecule (e.g., from
|
201 |
ase.vibrations.Vibrations.get_energies). The number of vibrations
|
202 |
used is automatically calculated by the geometry and the number of
|
203 |
atoms. If more are specified than are needed, then the lowest
|
204 |
numbered vibrations are neglected. If either atoms or natoms is
|
205 |
unspecified, then uses the entire list. Units are eV.
|
206 |
geometry : 'monatomic', 'linear', or 'nonlinear'
|
207 |
geometry of the molecule
|
208 |
electronicenergy : float
|
209 |
the electronic energy in eV
|
210 |
(If electronicenergy is unspecified, then the methods of this
|
211 |
class can be interpreted as the enthalpy and free energy
|
212 |
corrections.)
|
213 |
natoms : integer
|
214 |
the number of atoms, used along with 'geometry' to determine how
|
215 |
many vibrations to use. (Not needed if an atoms object is supplied
|
216 |
in 'atoms' or if the user desires the entire list of vibrations
|
217 |
to be used.)
|
218 |
|
219 |
Extra inputs needed for for entropy / free energy calculations:
|
220 |
|
221 |
atoms : an ASE atoms object
|
222 |
used to calculate rotational moments of inertia and molecular mass
|
223 |
symmetrynumber : integer
|
224 |
symmetry number of the molecule. See, for example, Table 10.1 and
|
225 |
Appendix B of C. Cramer "Essentials of Computational Chemistry",
|
226 |
2nd Ed.
|
227 |
spin : float
|
228 |
the total electronic spin. (0 for molecules in which all electrons
|
229 |
are paired, 0.5 for a free radical with a single unpaired electron,
|
230 |
1.0 for a triplet with two unpaired electrons, such as O_2.)
|
231 |
"""
|
232 |
|
233 |
def __init__(self, vib_energies, geometry, electronicenergy=None, |
234 |
atoms=None, symmetrynumber=None, spin=None, natoms=None): |
235 |
if electronicenergy == None: |
236 |
self.electronicenergy = 0. |
237 |
else:
|
238 |
self.electronicenergy = electronicenergy
|
239 |
self.geometry = geometry
|
240 |
self.atoms = atoms
|
241 |
self.sigma = symmetrynumber
|
242 |
self.spin = spin
|
243 |
if natoms == None: |
244 |
if atoms:
|
245 |
natoms = len(atoms)
|
246 |
# Cut the vibrations to those needed from the geometry.
|
247 |
if natoms:
|
248 |
if geometry == 'nonlinear': |
249 |
self.vib_energies = vib_energies[-(3*natoms-6):] |
250 |
elif geometry == 'linear': |
251 |
self.vib_energies = vib_energies[-(3*natoms-5):] |
252 |
elif geometry == 'monatomic': |
253 |
self.vib_energies = []
|
254 |
else:
|
255 |
self.vib_energies = vib_energies
|
256 |
# Make sure no imaginary frequencies remain.
|
257 |
if sum(np.iscomplex(self.vib_energies)): |
258 |
raise ValueError('Imaginary frequencies are present.') |
259 |
else:
|
260 |
self.vib_energies = np.real(self.vib_energies) # clear +0.j |
261 |
self.referencepressure = 101325. # Pa |
262 |
|
263 |
def get_enthalpy(self, temperature, verbose=True): |
264 |
"""Returns the enthalpy, in eV, in the ideal gas approximation
|
265 |
at a specified temperature (K)."""
|
266 |
|
267 |
self.verbose = verbose
|
268 |
write = self._vprint
|
269 |
fmt = '%-15s%13.3f eV'
|
270 |
write('Enthalpy components at T = %.2f K:' % temperature)
|
271 |
write('='*31) |
272 |
|
273 |
H = 0.
|
274 |
|
275 |
write(fmt % ('E_elec', self.electronicenergy)) |
276 |
H += self.electronicenergy
|
277 |
|
278 |
zpe = self.get_ZPE_correction()
|
279 |
write(fmt % ('E_ZPE', zpe))
|
280 |
H += zpe |
281 |
|
282 |
Cv_t = 3./2. * units.kB # translational heat capacity (3-d gas) |
283 |
write(fmt % ('Cv_trans (0->T)', Cv_t * temperature))
|
284 |
H += Cv_t * temperature |
285 |
|
286 |
if self.geometry == 'nonlinear': # rotational heat capacity |
287 |
Cv_r = 3./2.*units.kB |
288 |
elif self.geometry == 'linear': |
289 |
Cv_r = units.kB |
290 |
elif self.geometry == 'monatomic': |
291 |
Cv_r = 0.
|
292 |
write(fmt % ('Cv_rot (0->T)', Cv_r * temperature))
|
293 |
H += Cv_r*temperature |
294 |
|
295 |
dH_v = self._vibrational_energy_contribution(temperature)
|
296 |
write(fmt % ('Cv_vib (0->T)', dH_v))
|
297 |
H += dH_v |
298 |
|
299 |
Cp_corr = units.kB * temperature |
300 |
write(fmt % ('(C_v -> C_p)', Cp_corr))
|
301 |
H += Cp_corr |
302 |
|
303 |
write('-'*31) |
304 |
write(fmt % ('H', H))
|
305 |
write('='*31) |
306 |
return H
|
307 |
|
308 |
def get_entropy(self, temperature, pressure, verbose=True): |
309 |
"""Returns the entropy, in eV/K, in the ideal gas approximation
|
310 |
at a specified temperature (K) and pressure (Pa)."""
|
311 |
|
312 |
if self.atoms == None or self.sigma == None or self.spin == None: |
313 |
raise RuntimeError('atoms, symmetrynumber, and spin must be ' |
314 |
'specified for entropy and free energy '
|
315 |
'calculations.')
|
316 |
self.verbose = verbose
|
317 |
write = self._vprint
|
318 |
fmt = '%-15s%13.7f eV/K%13.3f eV'
|
319 |
write('Entropy components at T = %.2f K and P = %.1f Pa:' %
|
320 |
(temperature, pressure)) |
321 |
write('='*49) |
322 |
write('%15s%13s %13s' % ('', 'S', 'T*S')) |
323 |
|
324 |
S = 0.
|
325 |
|
326 |
# Translational entropy (term inside the log is in SI units).
|
327 |
mass = sum(self.atoms.get_masses()) * units._amu # kg/molecule |
328 |
S_t = (2*np.pi*mass*units._k*temperature/units._hplanck**2)**(3./2) |
329 |
S_t *= units._k * temperature / self.referencepressure
|
330 |
S_t = units.kB * (np.log(S_t) + 5./2.) |
331 |
write(fmt % ('S_trans (1 atm)', S_t, S_t * temperature))
|
332 |
S += S_t |
333 |
|
334 |
# Rotational entropy (term inside the log is in SI units).
|
335 |
if self.geometry == 'monatomic': |
336 |
S_r = 0.
|
337 |
elif self.geometry == 'nonlinear': |
338 |
inertias = (rotationalinertia(self.atoms) * units._amu /
|
339 |
(10.**10)**2) # kg m^2 |
340 |
S_r = np.sqrt(np.pi*np.product(inertias)) / self.sigma
|
341 |
S_r *= (8. * np.pi**2 * units._k * temperature / |
342 |
units._hplanck**2)**(3./2.) |
343 |
S_r = units.kB * (np.log(S_r) + 3./2.) |
344 |
elif self.geometry == 'linear': |
345 |
inertias = (rotationalinertia(self.atoms) * units._amu /
|
346 |
(10.**10)**2) # kg m^2 |
347 |
inertia = max(inertias) # should be two identical and one zero |
348 |
S_r = (8 * np.pi**2 * inertia * units._k * temperature / |
349 |
self.sigma / units._hplanck**2) |
350 |
S_r = units.kB * ( np.log(S_r) + 1.)
|
351 |
write(fmt % ('S_rot', S_r, S_r*temperature))
|
352 |
S += S_r |
353 |
|
354 |
# Electronic entropy.
|
355 |
S_e = units.kB * np.log(2*self.spin+1) |
356 |
write(fmt % ('S_elec', S_e, S_e * temperature))
|
357 |
S += S_e |
358 |
|
359 |
# Vibrational entropy.
|
360 |
S_v = self._vibrational_entropy_contribution(temperature)
|
361 |
write(fmt % ('S_vib', S_v, S_v* temperature))
|
362 |
S += S_v |
363 |
|
364 |
# Pressure correction to translational entropy.
|
365 |
S_p = - units.kB * np.log(pressure/self.referencepressure)
|
366 |
write(fmt % ('S (1 atm -> P)', S_p, S_p * temperature))
|
367 |
S += S_p |
368 |
|
369 |
write('-'*49) |
370 |
write(fmt % ('S', S, S * temperature))
|
371 |
write('='*49) |
372 |
return S
|
373 |
|
374 |
def get_free_energy(self, temperature, pressure, verbose=True): |
375 |
"""Returns the free energy, in eV, in the ideal gas approximation
|
376 |
at a specified temperature (K) and pressure (Pa)."""
|
377 |
|
378 |
self.verbose = verbose
|
379 |
write = self._vprint
|
380 |
|
381 |
H = self.get_enthalpy(temperature, verbose=verbose)
|
382 |
write('')
|
383 |
S = self.get_entropy(temperature, pressure, verbose=verbose)
|
384 |
G = H - temperature * S |
385 |
|
386 |
write('')
|
387 |
write('Free energy components at T = %.2f K and P = %.1f Pa:' %
|
388 |
(temperature, pressure)) |
389 |
write('='*23) |
390 |
fmt = '%5s%15.3f eV'
|
391 |
write(fmt % ('H', H))
|
392 |
write(fmt % ('-T*S', -temperature * S))
|
393 |
write('-'*23) |
394 |
write(fmt % ('G', G))
|
395 |
write('='*23) |
396 |
return G
|
397 |
|