root / ase / infrared.py @ 18
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')
|