Statistiques
| Révision :

root / ase / vibrations.py @ 13

Historique | Voir | Annoter | Télécharger (15,57 ko)

1
# -*- coding: utf-8 -*-
2

    
3
"""Vibrational modes."""
4

    
5
import pickle
6
from math import sin, pi, sqrt
7
from os import remove
8
from os.path import isfile
9
import sys
10

    
11
import numpy as np
12

    
13
import ase.units as units
14
from ase.io.trajectory import PickleTrajectory
15
from ase.parallel import rank, barrier, paropen
16

    
17

    
18
class Vibrations:
19
    """Class for calculating vibrational modes using finite difference.
20

21
    The vibrational modes are calculated from a finite difference
22
    approximation of the Hessian matrix.
23

24
    The *summary()*, *get_energies()* and *get_frequencies()*
25
    methods all take an optional *method* keyword.  Use
26
    method='Frederiksen' to use the method described in:
27

28
      T. Frederiksen, M. Paulsson, M. Brandbyge, A. P. Jauho:
29
      "Inelastic transport theory from first-principles: methodology
30
      and applications for nanoscale devices", 
31
      Phys. Rev. B 75, 205413 (2007) 
32

33
    atoms: Atoms object
34
        The atoms to work on.
35
    indices: list of int
36
        List of indices of atoms to vibrate.  Default behavior is
37
        to vibrate all atoms.
38
    name: str
39
        Name to use for files.
40
    delta: float
41
        Magnitude of displacements.
42
    nfree: int
43
        Number of displacements per atom and cartesian coordinate,
44
        2 and 4 are supported. Default is 2 which will displace 
45
        each atom +delta and -delta for each cartesian coordinate.
46

47
    Example:
48

49
    >>> from ase import Atoms
50
    >>> from ase.calculators.emt import EMT
51
    >>> from ase.optimizers import BFGS
52
    >>> from ase.vibrations import Vibrations
53
    >>> n2 = Atoms('N2', [(0, 0, 0), (0, 0, 1.1)],
54
    ...            calculator=EMT())
55
    >>> BFGS(n2).run(fmax=0.01)
56
    BFGS:   0  19:16:06        0.042171       2.9357
57
    BFGS:   1  19:16:07        0.104197       3.9270
58
    BFGS:   2  19:16:07        0.000963       0.4142
59
    BFGS:   3  19:16:07        0.000027       0.0698
60
    BFGS:   4  19:16:07        0.000000       0.0010
61
    >>> vib = Vibrations(n2)
62
    >>> vib.run()
63
    >>> vib.summary()
64
    ---------------------
65
      #    meV     cm^-1
66
    ---------------------
67
      0    0.0i      0.0i
68
      1    0.0i      0.0i
69
      2    0.0i      0.0i
70
      3    1.6      13.1 
71
      4    1.6      13.1 
72
      5  232.7    1877.2 
73
    ---------------------
74
    Zero-point energy: 0.118 eV
75
    Thermodynamic properties at 298.00 K
76
    Enthalpy: 0.050 eV
77
    Entropy : 0.648 meV/K
78
    T*S     : 0.193 eV
79
    E->G    : -0.025 eV
80

81
    >>> vib.write_mode(-1)  # write last mode to trajectory file
82

83
    """
84
    def __init__(self, atoms, indices=None, name='vib', delta=0.01, nfree=2):
85
        assert nfree in [2, 4]
86
        self.atoms = atoms
87
        if indices is None:
88
            indices = range(len(atoms))
89
        self.indices = np.asarray(indices)
90
        self.name = name
91
        self.delta = delta
92
        self.nfree = nfree
93
        self.H = None
94
        self.ir = None
95

    
96
    def run(self):
97
        """Run the vibration calculations.
98

99
        This will calculate the forces for 6 displacements per atom
100
        ±x, ±y, ±z.  Only those calculations that are not already done
101
        will be started. Be aware that an interrupted calculation may
102
        produce an empty file (ending with .pckl), which must be deleted
103
        before restarting the job. Otherwise the forces will not be
104
        calculated for that displacement."""
105
        filename = self.name + '.eq.pckl'
106
        if not isfile(filename):
107
            barrier()
108
            forces = self.atoms.get_forces()
109
            if self.ir:
110
                dipole = self.calc.get_dipole_moment(self.atoms)
111
            if rank == 0:
112
                fd = open(filename, 'w')
113
                if self.ir:
114
                    pickle.dump([forces, dipole], fd)
115
                    sys.stdout.write(
116
                        'Writing %s, dipole moment = (%.6f %.6f %.6f)\n' % 
117
                        (filename, dipole[0], dipole[1], dipole[2]))
118
                else:
119
                    pickle.dump(forces, fd)
120
                    sys.stdout.write('Writing %s\n' % filename)
121
                fd.close()
122
            sys.stdout.flush()
123
        
124
        p = self.atoms.positions.copy()
125
        for a in self.indices:
126
            for i in range(3):
127
                for sign in [-1, 1]:
128
                    for ndis in range(1, self.nfree//2+1):
129
                        filename = ('%s.%d%s%s.pckl' %
130
                                    (self.name, a, 'xyz'[i], ndis*' +-'[sign]))
131
                        if isfile(filename):
132
                            continue
133
                        barrier()
134
                        self.atoms.positions[a, i] = (p[a, i] +
135
                                                      ndis * sign * self.delta)
136
                        forces = self.atoms.get_forces()
137
                        if self.ir:
138
                            dipole = self.calc.get_dipole_moment(self.atoms)
139
                        if rank == 0:
140
                            fd = open(filename, 'w')
141
                            if self.ir:
142
                                pickle.dump([forces, dipole], fd)
143
                                sys.stdout.write(
144
                                    'Writing %s, ' % filename +
145
                                    'dipole moment = (%.6f %.6f %.6f)\n' % 
146
                                    (dipole[0], dipole[1], dipole[2]))
147
                            else:
148
                                pickle.dump(forces, fd)
149
                                sys.stdout.write('Writing %s\n' % filename)
150
                            fd.close()
151
                        sys.stdout.flush()
152
                        self.atoms.positions[a, i] = p[a, i]
153
        self.atoms.set_positions(p)
154

    
155
    def clean(self):
156
        if isfile(self.name + '.eq.pckl'):
157
            remove(self.name + '.eq.pckl')
158
        
159
        for a in self.indices:
160
            for i in 'xyz':
161
                for sign in '-+':
162
                    for ndis in range(1, self.nfree/2+1):
163
                        name = '%s.%d%s%s.pckl' % (self.name, a, i, ndis*sign)
164
                        if isfile(name):
165
                            remove(name)
166
        
167
    def read(self, method='standard', direction='central'):
168
        self.method = method.lower()
169
        self.direction = direction.lower()
170
        assert self.method in ['standard', 'frederiksen']
171
        assert self.direction in ['central', 'forward', 'backward']
172
        
173
        n = 3 * len(self.indices)
174
        H = np.empty((n, n))
175
        r = 0
176
        if direction != 'central':
177
            feq = pickle.load(open(self.name + '.eq.pckl'))
178
        for a in self.indices:
179
            for i in 'xyz':
180
                name = '%s.%d%s' % (self.name, a, i)
181
                fminus = pickle.load(open(name + '-.pckl'))
182
                fplus = pickle.load(open(name + '+.pckl'))
183
                if self.method == 'frederiksen':
184
                    fminus[a] -= fminus.sum(0)
185
                    fplus[a] -= fplus.sum(0)
186
                if self.nfree == 4:
187
                    fminusminus = pickle.load(open(name + '--.pckl'))
188
                    fplusplus = pickle.load(open(name + '++.pckl'))
189
                    if self.method == 'frederiksen':
190
                        fminusminus[a] -= fminusminus.sum(0)
191
                        fplusplus[a] -= fplusplus.sum(0)
192
                if self.direction == 'central':
193
                    if self.nfree == 2:
194
                        H[r] = .5 * (fminus - fplus)[self.indices].ravel()
195
                    else:
196
                        H[r] = H[r] = (-fminusminus +
197
                                       8 * fminus -
198
                                       8 * fplus +
199
                                       fplusplus)[self.indices].ravel() / 12.0
200
                elif self.direction == 'forward':
201
                    H[r] = (feq - fplus)[self.indices].ravel()
202
                else: # self.direction == 'backward':
203
                    H[r] = (fminus - feq)[self.indices].ravel()
204
                H[r] /= 2 * self.delta
205
                r += 1
206
        H += H.copy().T
207
        self.H = H
208
        m = self.atoms.get_masses()
209
        self.im = np.repeat(m[self.indices]**-0.5, 3)
210
        omega2, modes = np.linalg.eigh(self.im[:, None] * H * self.im)
211
        self.modes = modes.T.copy()
212

    
213
        # Conversion factor:
214
        s = units._hbar * 1e10 / sqrt(units._e * units._amu)
215
        self.hnu = s * omega2.astype(complex)**0.5
216

    
217
    def get_energies(self, method='standard', direction='central'):
218
        """Get vibration energies in eV."""
219
        if (self.H is None or method.lower() != self.method or
220
            direction.lower() != self.direction):
221
            self.read(method, direction)
222
        return self.hnu
223

    
224
    def get_frequencies(self, method='standard', direction='central'):
225
        """Get vibration frequencies in cm^-1."""
226
        s = 0.01 * units._e / units._c / units._hplanck
227
        return s * self.get_energies(method, direction)
228

    
229
    def summary(self, method='standard', direction='central', T=298., 
230
                threshold=10, freq=None, log=sys.stdout):
231
        """Print a summary of the frequencies and derived thermodynamic
232
        properties. The equations for the calculation of the enthalpy and 
233
        entropy diverge for zero frequencies and a threshold value is used
234
        to ignore extremely low frequencies (default = 10 cm^-1).
235

236
        Parameters:
237

238
        method : string
239
            Can be 'standard'(default) or 'Frederiksen'.
240
        direction: string
241
            Direction for finite differences. Can be one of 'central'
242
            (default), 'forward', 'backward'.
243
        T : float
244
            Temperature in K at which thermodynamic properties are calculated.
245
        threshold : float
246
            Threshold value for low frequencies (default 10 cm^-1).
247
        freq : numpy array
248
            Optional. Can be used to create a summary on a set of known
249
            frequencies.
250
        destination : string or object with a .write() method
251
            Where to print the summary info. Default is to print to
252
            standard output. If another string is given, it creates that
253
            text file and writes the output to it. If a file object (or any
254
            object with a .write(string) function) is given, it writes to that
255
            file.
256

257
        Notes:
258

259
        The enthalpy and entropy calculations are very sensitive to low
260
        frequencies and the threshold value must be used with caution."""
261

    
262
        if isinstance(log, str):
263
            log = paropen(log, 'a')
264
        write = log.write
265
        
266
        s = 0.01 * units._e / units._c / units._hplanck
267
        if freq != None:
268
            hnu = freq / s
269
        else:
270
            hnu = self.get_energies(method, direction)
271
        write('---------------------\n')
272
        write('  #    meV     cm^-1\n')
273
        write('---------------------\n')
274
        for n, e in enumerate(hnu):
275
            if e.imag != 0:
276
                c = 'i'
277
                e = e.imag
278
            else:
279
                c = ' '
280
            write('%3d %6.1f%s  %7.1f%s\n' % (n, 1000 * e, c, s * e, c))
281
        write('---------------------\n')
282
        write('Zero-point energy: %.3f eV\n' %
283
              self.get_zero_point_energy(freq=freq))
284
        write('Thermodynamic properties at %.2f K\n' % T)
285
        write('Enthalpy: %.3f eV\n' % self.get_enthalpy(method=method,
286
                                                        direction=direction,
287
                                                        T=T,
288
                                                        threshold=threshold,
289
                                                        freq=freq))
290
        write('Entropy : %.3f meV/K\n' % 
291
              (1E3 * self.get_entropy(method=method,
292
                                      direction=direction,
293
                                      T=T,
294
                                      threshold=threshold,
295
                                      freq=freq)))
296
        write('T*S     : %.3f eV\n' %
297
              (T * self.get_entropy(method=method,
298
                                    direction=direction,
299
                                    T=T,
300
                                    threshold=threshold,
301
                                    freq=freq)))
302
        write('E->G    : %.3f eV\n' %
303
              (self.get_zero_point_energy(freq=freq) +
304
               self.get_enthalpy(method=method,
305
                                 direction=direction,
306
                                 T=T,
307
                                 threshold=threshold,
308
                                 freq=freq) -
309
               T * self.get_entropy(method=method,
310
                                    direction=direction,
311
                                    T=T,
312
                                    threshold=threshold,
313
                                    freq=freq)))
314

    
315
    def get_zero_point_energy(self, freq=None):
316
        if freq is None:
317
            return 0.5 * self.hnu.real.sum()
318
        else:
319
            s = 0.01 * units._e / units._c / units._hplanck
320
            return 0.5 * freq.real.sum() / s
321

    
322
    def get_enthalpy(self, method='standard', direction='central', T=298.0,
323
                     threshold=10, freq=None):
324
        H = 0.0
325
        if freq is None:
326
            freq = self.get_frequencies(method=method, direction=direction)
327
        for f in freq:
328
            if f.imag == 0 and f.real >= threshold:
329
                # The formula diverges for f->0
330
                x = (f.real * 100 * units._hplanck * units._c) / units._k
331
                H += units._k / units._e * x / (np.exp(x / T) - 1)
332
        return H
333

    
334
    def get_entropy(self, method='standard', direction='central', T=298.0,
335
                    threshold=10, freq=None):
336
        S = 0.0
337
        if freq is None:
338
            freq=self.get_frequencies(method=method, direction=direction)
339
        for f in freq:
340
            if f.imag == 0 and f.real >= threshold:
341
                # The formula diverges for f->0
342
                x = (f.real * 100 * units._hplanck * units._c) / units._k
343
                S += (units._k / units._e * (((x / T) / (np.exp(x / T) - 1)) -
344
                                             np.log(1 - np.exp(-x / T))))
345
        return S
346

    
347
    def get_mode(self, n):
348
        mode = np.zeros((len(self.atoms), 3))
349
        mode[self.indices] = (self.modes[n] * self.im).reshape((-1, 3))
350
        return mode
351

    
352
    def write_mode(self, n, kT=units.kB * 300, nimages=30):
353
        """Write mode to trajectory file."""
354
        mode = self.get_mode(n) * sqrt(kT / abs(self.hnu[n]))
355
        p = self.atoms.positions.copy()
356
        n %= 3 * len(self.indices)
357
        traj = PickleTrajectory('%s.%d.traj' % (self.name, n), 'w')
358
        calc = self.atoms.get_calculator()
359
        self.atoms.set_calculator()
360
        for x in np.linspace(0, 2 * pi, nimages, endpoint=False):
361
            self.atoms.set_positions(p + sin(x) * mode)
362
            traj.write(self.atoms)
363
        self.atoms.set_positions(p)
364
        self.atoms.set_calculator(calc)
365
        traj.close()
366

    
367
    def write_jmol(self):
368
        """Writes file for viewing of the modes with jmol."""
369

    
370
        fd = open(self.name + '.xyz', 'w')
371
        symbols = self.atoms.get_chemical_symbols()
372
        f = self.get_frequencies()
373
        for n in range(3 * len(self.atoms)):
374
            fd.write('%6d\n' % len(self.atoms))
375
            if f[n].imag != 0:
376
                c = 'i'
377
                f[n] = f[n].imag
378
            else:
379
                c = ' ' 
380
            fd.write('Mode #%d, f = %.1f%s cm^-1' % (n, f[n], c))
381
            if self.ir:
382
                fd.write(', I = %.4f (D/Å)^2 amu^-1.\n' % self.intensities[n])
383
            else:
384
                fd.write('.\n')
385
            mode = self.get_mode(n)
386
            for i, pos in enumerate(self.atoms.positions):
387
                fd.write('%2s %12.5f %12.5f %12.5f %12.5f %12.5f %12.5f \n' % 
388
                         (symbols[i], pos[0], pos[1], pos[2],
389
                          mode[i,0], mode[i,1], mode[i,2]))
390
        fd.close()