Statistiques
| Révision :

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