root / ase / vibrations.py @ 15
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() |