Statistiques
| Révision :

root / ase / infrared.py @ 19

Historique | Voir | Annoter | Télécharger (11,78 ko)

1

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

    
4
"""Infrared intensities"""
5

    
6
import pickle
7
from math import sin, pi, sqrt, exp, log
8
from os import remove
9
from os.path import isfile
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, parprint
16
from ase.vibrations import Vibrations
17

    
18

    
19
class InfraRed(Vibrations):
20
    """Class for calculating vibrational modes and infrared intensities
21
    using finite difference.
22

23
    The vibrational modes are calculated from a finite difference
24
    approximation of the Dynamical matrix and the IR intensities from
25
    a finite difference approximation of the gradient of the dipole
26
    moment. The method is described in:
27

28
      D. Porezag, M. R. Peterson:
29
      "Infrared intensities and Raman-scattering activities within
30
      density-functional theory",
31
      Phys. Rev. B 54, 7830 (1996)
32

33
    The calculator object (calc) linked to the Atoms object (atoms) must 
34
    have the attribute:
35
    
36
    >>> calc.get_dipole_moment(atoms)
37

38
    In addition to the methods included in the ``Vibrations`` class
39
    the ``InfraRed`` class introduces two new methods;
40
    *get_spectrum()* and *write_spectra()*. The *summary()*, *get_energies()*, 
41
    *get_frequencies()*, *get_spectrum()* and *write_spectra()*
42
    methods all take an optional *method* keyword.  Use
43
    method='Frederiksen' to use the method described in:
44

45
      T. Frederiksen, M. Paulsson, M. Brandbyge, A. P. Jauho:
46
      "Inelastic transport theory from first-principles: methodology
47
      and applications for nanoscale devices", 
48
      Phys. Rev. B 75, 205413 (2007) 
49

50
    atoms: Atoms object
51
        The atoms to work on.
52
    indices: list of int
53
        List of indices of atoms to vibrate.  Default behavior is
54
        to vibrate all atoms.
55
    name: str
56
        Name to use for files.
57
    delta: float
58
        Magnitude of displacements.
59
    nfree: int
60
        Number of displacements per degree of freedom, 2 or 4 are
61
        supported. Default is 2 which will displace each atom +delta
62
        and -delta in each cartesian direction.
63
    directions: list of int
64
        Cartesian coordinates to calculate the gradient of the dipole moment in. 
65
        For example directions = 2 only dipole moment in the z-direction will
66
        be considered, whereas for directions = [0, 1] only the dipole
67
        moment in the xy-plane will be considered. Default behavior is to
68
        use the dipole moment in all directions.
69

70
    Example:
71
    
72
    >>> from ase.io import read
73
    >>> from ase.calculators.vasp import Vasp
74
    >>> from ase.infrared import InfraRed
75
    >>> water = read('water.traj')  # read pre-relaxed structure of water molecule
76
    >>> calc = Vasp(prec='Accurate',
77
    ...             ediff=1E-8,
78
    ...             isym=0,
79
    ...             idipol=4,       # calculate the total dipole moment
80
    ...             dipol=water.get_center_of_mass(scaled=True),
81
    ...             ldipol=True)
82
    >>> water.set_calculator(calc)
83
    >>> ir = InfraRed(water)
84
    >>> ir.run()
85
    >>> ir.summary()
86
    -------------------------------------
87
    Mode    Frequency        Intensity
88
    #    meV     cm^-1   (D/Å)^2 amu^-1
89
    -------------------------------------
90
    0   16.9i    136.2i     1.6108
91
    1   10.5i     84.9i     2.1682
92
    2    5.1i     41.1i     1.7327
93
    3    0.3i      2.2i     0.0080
94
    4    2.4      19.0      0.1186
95
    5   15.3     123.5      1.4956
96
    6  195.5    1576.7      1.6437
97
    7  458.9    3701.3      0.0284
98
    8  473.0    3814.6      1.1812
99
    -------------------------------------
100
    Zero-point energy: 0.573 eV
101
    Static dipole moment: 1.833 D
102
    Maximum force on atom in `eqiulibrium`: 0.0026 eV/Å
103

104

105

106
    This interface now also works for calculator 'siesta', 
107
    (added get_dipole_moment for siesta).
108

109
    Example:
110

111
    >>> #!/usr/bin/env python
112

113
    >>> from ase.io import read
114
    >>> from ase.calculators.siesta import Siesta
115
    >>> from ase.infrared import InfraRed
116

117
    >>> bud = read('bud1.xyz')
118

119
    >>> calc = Siesta(label='bud',
120
    ...       meshcutoff=250 * Ry,
121
    ...       basis='DZP',
122
    ...       kpts=[1, 1, 1])
123

124
    >>> calc.set_fdf('DM.MixingWeight', 0.08)
125
    >>> calc.set_fdf('DM.NumberPulay', 3)
126
    >>> calc.set_fdf('DM.NumberKick', 20)
127
    >>> calc.set_fdf('DM.KickMixingWeight', 0.15)
128
    >>> calc.set_fdf('SolutionMethod',      'Diagon')
129
    >>> calc.set_fdf('MaxSCFIterations', 500)
130
    >>> calc.set_fdf('PAO.BasisType',  'split')
131
    >>> #50 meV = 0.003674931 * Ry
132
    >>> calc.set_fdf('PAO.EnergyShift', 0.003674931 * Ry )
133
    >>> calc.set_fdf('LatticeConstant', 1.000000 * Ang)
134
    >>> calc.set_fdf('WriteCoorXmol',       'T')
135

136
    >>> bud.set_calculator(calc)
137

138
    >>> ir = InfraRed(bud)
139
    >>> ir.run()
140
    >>> ir.summary()
141

142

143

144

145
    """
146
    def __init__(self, atoms, indices=None, name='ir', delta=0.01, nfree=2, directions=None):
147
        assert nfree in [2, 4]
148
        self.atoms = atoms
149
        if atoms.constraints:
150
            print "WARNING! \n Your Atoms object is constrained. Some forces may be unintended set to zero. \n"
151
        self.calc = atoms.get_calculator()
152
        if indices is None:
153
            indices = range(len(atoms))
154
        self.indices = np.asarray(indices)
155
        self.nfree = nfree
156
        self.name = name+'-d%.3f' % delta
157
        self.delta = delta
158
        self.H = None
159
        if directions is None:
160
            self.directions = np.asarray([0, 1, 2])
161
        else:
162
            self.directions = np.asarray(directions)
163
        self.ir = True
164

    
165
    def read(self, method='standard', direction='central'):
166
        self.method = method.lower()
167
        self.direction = direction.lower()
168
        assert self.method in ['standard', 'frederiksen']
169
        if direction != 'central':
170
            raise NotImplementedError('Only central difference is implemented at the moment.')
171

    
172
        # Get "static" dipole moment and forces
173
        name = '%s.eq.pckl' % self.name
174
        [forces_zero, dipole_zero] = pickle.load(open(name))
175
        self.dipole_zero = (sum(dipole_zero**2)**0.5)*units.Debye
176
        self.force_zero = max([sum((forces_zero[j])**2)**0.5 for j in self.indices])
177

    
178
        ndof = 3 * len(self.indices)
179
        H = np.empty((ndof, ndof))
180
        dpdx = np.empty((ndof, 3))
181
        r = 0
182
        for a in self.indices:
183
            for i in 'xyz':
184
                name = '%s.%d%s' % (self.name, a, i)
185
                [fminus, dminus] = pickle.load(open(name + '-.pckl'))
186
                [fplus, dplus] = pickle.load(open(name + '+.pckl'))
187
                if self.nfree == 4:
188
                    [fminusminus, dminusminus] = pickle.load(open(name + '--.pckl'))
189
                    [fplusplus, dplusplus] = pickle.load(open(name + '++.pckl'))
190
                if self.method == 'frederiksen':
191
                    fminus[a] += -fminus.sum(0)
192
                    fplus[a] += -fplus.sum(0)
193
                    if self.nfree == 4:
194
                        fminusminus[a] += -fminus.sum(0)
195
                        fplusplus[a] += -fplus.sum(0)
196
                if self.nfree == 2:
197
                    H[r] = (fminus - fplus)[self.indices].ravel() / 2.0
198
                    dpdx[r] = (dminus - dplus)
199
                if self.nfree == 4:
200
                    H[r] = (-fminusminus+8*fminus-8*fplus+fplusplus)[self.indices].ravel() / 12.0
201
                    dpdx[r] = (-dplusplus + 8*dplus - 8*dminus +dminusminus) / 6.0
202
                H[r] /= 2 * self.delta
203
                dpdx[r] /= 2 * self.delta
204
                for n in range(3):
205
                    if n not in self.directions:
206
                        dpdx[r][n] = 0
207
                        dpdx[r][n] = 0
208
                r += 1
209
        # Calculate eigenfrequencies and eigenvectors
210
        m = self.atoms.get_masses()
211
        H += H.copy().T
212
        self.H = H
213
        m = self.atoms.get_masses()
214
        self.im = np.repeat(m[self.indices]**-0.5, 3)
215
        omega2, modes = np.linalg.eigh(self.im[:, None] * H * self.im)
216
        self.modes = modes.T.copy()
217

    
218
        # Calculate intensities
219
        dpdq = np.array([dpdx[j]/sqrt(m[self.indices[j/3]]*units._amu/units._me) for j in range(ndof)])
220
        dpdQ = np.dot(dpdq.T, modes)
221
        dpdQ = dpdQ.T
222
        intensities = np.array([sum(dpdQ[j]**2) for j in range(ndof)])
223
        # Conversion factor:
224
        s = units._hbar * 1e10 / sqrt(units._e * units._amu)
225
        self.hnu = s * omega2.astype(complex)**0.5
226
        # Conversion factor from atomic units to (D/Angstrom)^2/amu.
227
        conv = units.Debye**2*units._amu/units._me
228
        self.intensities = intensities*conv
229

    
230
    def summary(self, method='standard', direction='central'):
231
        hnu = self.get_energies(method, direction)
232
        s = 0.01 * units._e / units._c / units._hplanck
233
        parprint('-------------------------------------')
234
        parprint(' Mode    Frequency        Intensity')
235
        parprint('  #    meV     cm^-1   (D/Å)^2 amu^-1')
236
        parprint('-------------------------------------')
237
        for n, e in enumerate(hnu):
238
            if e.imag != 0:
239
                c = 'i'
240
                e = e.imag
241
            else:
242
                c = ' '
243
            parprint('%3d %6.1f%s  %7.1f%s  %9.4f' % 
244
                     (n, 1000 * e, c, s * e, c, self.intensities[n]))
245
        parprint('-------------------------------------')
246
        parprint('Zero-point energy: %.3f eV' % self.get_zero_point_energy())
247
        parprint('Static dipole moment: %.3f D' % self.dipole_zero)
248
        parprint('Maximum force on atom in `eqiulibrium`: %.4f eV/Å' % 
249
                  self.force_zero)
250
        parprint()
251

    
252
    def get_spectrum(self, start=800, end=4000, npts=None, width=4, type='Gaussian', method='standard', direction='central'):
253
        """Get infrared spectrum.
254

255
        The method returns wavenumbers in cm^-1 with corresonding absolute infrared intensity.
256
        Start and end point, and width of the Gaussian/Lorentzian should be given in cm^-1."""
257

    
258
        self.type = type.lower()
259
        assert self.type in ['gaussian', 'lorentzian']
260
        if not npts: 
261
            npts = (end-start)/width*10+1
262
        frequencies = self.get_frequencies(method, direction).real
263
        intensities=self.intensities
264
        if type == 'lorentzian':
265
            intensities = intensities*width*pi/2.
266
        else:
267
            sigma = width/2./sqrt(2.*log(2.))
268
        #Make array with spectrum data
269
        spectrum = np.empty(npts,np.float)
270
        energies = np.empty(npts,np.float)
271
        ediff = (end-start)/float(npts-1)
272
        energies = np.arange(start, end+ediff/2, ediff)
273
        for i, energy in enumerate(energies):
274
            energies[i] = energy
275
            if type == 'lorentzian':
276
                spectrum[i] = (intensities*0.5*width/pi/((frequencies-energy)**2+0.25*width**2)).sum()
277
            else:
278
                spectrum[i] = (intensities*np.exp(-(frequencies - energy)**2/2./sigma**2)).sum()
279
        return [energies, spectrum]
280

    
281
    def write_spectra(self, out='ir-spectra.dat', start=800, end=4000, npts=None, width=10, type='Gaussian', method='standard', direction='central'):
282
        """Write out infrared spectrum to file.
283

284
        First column is the wavenumber in cm^-1, the second column the absolute infrared intensities, and
285
        the third column the absorbance scaled so that data runs from 1 to 0. Start and end 
286
        point, and width of the Gaussian/Lorentzian should be given in cm^-1."""
287
        energies, spectrum = self.get_spectrum(start, end, npts, width, type, method, direction)
288

    
289
        #Write out spectrum in file. First column is absolute intensities. 
290
        #Second column is absorbance scaled so that data runs from 1 to 0
291
        spectrum2 = 1. - spectrum/spectrum.max()
292
        outdata = np.empty([len(energies), 3])
293
        outdata.T[0] = energies
294
        outdata.T[1] = spectrum
295
        outdata.T[2] = spectrum2
296
        fd = open(out, 'w')
297
        for row in outdata:
298
            fd.write('%.3f  %15.5e  %15.5e \n' % (row[0], row[1], row[2]) )
299
        fd.close()
300
        #np.savetxt(out, outdata, fmt='%.3f  %15.5e  %15.5e')