Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1import warnings
3from ase.units import kJ
5import numpy as np
7from scipy.optimize import curve_fit
10eos_names = ['sj', 'taylor', 'murnaghan', 'birch', 'birchmurnaghan',
11 'pouriertarantola', 'vinet', 'antonschmidt', 'p3']
14def taylor(V, E0, beta, alpha, V0):
15 'Taylor Expansion up to 3rd order about V0'
17 E = E0 + beta / 2 * (V - V0)**2 / V0 + alpha / 6 * (V - V0)**3 / V0
18 return E
21def murnaghan(V, E0, B0, BP, V0):
22 'From PRB 28,5480 (1983'
24 E = E0 + B0 * V / BP * (((V0 / V)**BP) / (BP - 1) + 1) - V0 * B0 / (BP - 1)
25 return E
28def birch(V, E0, B0, BP, V0):
29 """
30 From Intermetallic compounds: Principles and Practice, Vol. I: Principles
31 Chapter 9 pages 195-210 by M. Mehl. B. Klein, D. Papaconstantopoulos
32 paper downloaded from Web
34 case where n=0
35 """
37 E = (E0 +
38 9 / 8 * B0 * V0 * ((V0 / V)**(2 / 3) - 1)**2 +
39 9 / 16 * B0 * V0 * (BP - 4) * ((V0 / V)**(2 / 3) - 1)**3)
40 return E
43def birchmurnaghan(V, E0, B0, BP, V0):
44 """
45 BirchMurnaghan equation from PRB 70, 224107
46 Eq. (3) in the paper. Note that there's a typo in the paper and it uses
47 inversed expression for eta.
48 """
50 eta = (V0 / V)**(1 / 3)
51 E = E0 + 9 * B0 * V0 / 16 * (eta**2 - 1)**2 * (
52 6 + BP * (eta**2 - 1) - 4 * eta**2)
53 return E
56def check_birchmurnaghan():
57 from sympy import symbols, Rational, diff, simplify
58 v, b, bp, v0 = symbols('v b bp v0')
59 x = (v0 / v)**Rational(2, 3)
60 e = 9 * b * v0 * (x - 1)**2 * (6 + bp * (x - 1) - 4 * x) / 16
61 print(e)
62 B = diff(e, v, 2) * v
63 BP = -v * diff(B, v) / b
64 print(simplify(B.subs(v, v0)))
65 print(simplify(BP.subs(v, v0)))
68def pouriertarantola(V, E0, B0, BP, V0):
69 'Pourier-Tarantola equation from PRB 70, 224107'
71 eta = (V / V0)**(1 / 3)
72 squiggle = -3 * np.log(eta)
74 E = E0 + B0 * V0 * squiggle**2 / 6 * (3 + squiggle * (BP - 2))
75 return E
78def vinet(V, E0, B0, BP, V0):
79 'Vinet equation from PRB 70, 224107'
81 eta = (V / V0)**(1 / 3)
83 E = (E0 + 2 * B0 * V0 / (BP - 1)**2 *
84 (2 - (5 + 3 * BP * (eta - 1) - 3 * eta) *
85 np.exp(-3 * (BP - 1) * (eta - 1) / 2)))
86 return E
89def antonschmidt(V, Einf, B, n, V0):
90 """From Intermetallics 11, 23-32 (2003)
92 Einf should be E_infinity, i.e. infinite separation, but
93 according to the paper it does not provide a good estimate
94 of the cohesive energy. They derive this equation from an
95 empirical formula for the volume dependence of pressure,
97 E(vol) = E_inf + int(P dV) from V=vol to V=infinity
99 but the equation breaks down at large volumes, so E_inf
100 is not that meaningful
102 n should be about -2 according to the paper.
104 I find this equation does not fit volumetric data as well
105 as the other equtions do.
106 """
108 E = B * V0 / (n + 1) * (V / V0)**(n + 1) * (np.log(V / V0) -
109 (1 / (n + 1))) + Einf
110 return E
113def p3(V, c0, c1, c2, c3):
114 'polynomial fit'
116 E = c0 + c1 * V + c2 * V**2 + c3 * V**3
117 return E
120def parabola(x, a, b, c):
121 """parabola polynomial function
123 this function is used to fit the data to get good guesses for
124 the equation of state fits
126 a 4th order polynomial fit to get good guesses for
127 was not a good idea because for noisy data the fit is too wiggly
128 2nd order seems to be sufficient, and guarantees a single minimum"""
130 return a + b * x + c * x**2
133class EquationOfState:
134 """Fit equation of state for bulk systems.
136 The following equation is used::
138 sjeos (default)
139 A third order inverse polynomial fit 10.1103/PhysRevB.67.026103
141 ::
143 2 3 -1/3
144 E(V) = c + c t + c t + c t , t = V
145 0 1 2 3
147 taylor
148 A third order Taylor series expansion about the minimum volume
150 murnaghan
151 PRB 28, 5480 (1983)
153 birch
154 Intermetallic compounds: Principles and Practice,
155 Vol I: Principles. pages 195-210
157 birchmurnaghan
158 PRB 70, 224107
160 pouriertarantola
161 PRB 70, 224107
163 vinet
164 PRB 70, 224107
166 antonschmidt
167 Intermetallics 11, 23-32 (2003)
169 p3
170 A third order polynomial fit
172 Use::
174 eos = EquationOfState(volumes, energies, eos='murnaghan')
175 v0, e0, B = eos.fit()
176 eos.plot(show=True)
178 """
179 def __init__(self, volumes, energies, eos='sj'):
180 self.v = np.array(volumes)
181 self.e = np.array(energies)
183 if eos == 'sjeos':
184 eos = 'sj'
185 self.eos_string = eos
186 self.v0 = None
188 def fit(self, warn=True):
189 """Calculate volume, energy, and bulk modulus.
191 Returns the optimal volume, the minimum energy, and the bulk
192 modulus. Notice that the ASE units for the bulk modulus is
193 eV/Angstrom^3 - to get the value in GPa, do this::
195 v0, e0, B = eos.fit()
196 print(B / kJ * 1.0e24, 'GPa')
198 """
200 if self.eos_string == 'sj':
201 return self.fit_sjeos()
203 self.func = globals()[self.eos_string]
205 p0 = [min(self.e), 1, 1]
206 popt, pcov = curve_fit(parabola, self.v, self.e, p0)
208 parabola_parameters = popt
209 # Here I just make sure the minimum is bracketed by the volumes
210 # this if for the solver
211 minvol = min(self.v)
212 maxvol = max(self.v)
214 # the minimum of the parabola is at dE/dV = 0, or 2 * c V +b =0
215 c = parabola_parameters[2]
216 b = parabola_parameters[1]
217 a = parabola_parameters[0]
218 parabola_vmin = -b / 2 / c
220 # evaluate the parabola at the minimum to estimate the groundstate
221 # energy
222 E0 = parabola(parabola_vmin, a, b, c)
223 # estimate the bulk modulus from Vo * E''. E'' = 2 * c
224 B0 = 2 * c * parabola_vmin
226 if self.eos_string == 'antonschmidt':
227 BP = -2
228 else:
229 BP = 4
231 initial_guess = [E0, B0, BP, parabola_vmin]
233 # now fit the equation of state
234 p0 = initial_guess
235 popt, pcov = curve_fit(self.func, self.v, self.e, p0)
237 self.eos_parameters = popt
239 if self.eos_string == 'p3':
240 c0, c1, c2, c3 = self.eos_parameters
241 # find minimum E in E = c0 + c1 * V + c2 * V**2 + c3 * V**3
242 # dE/dV = c1+ 2 * c2 * V + 3 * c3 * V**2 = 0
243 # solve by quadratic formula with the positive root
245 a = 3 * c3
246 b = 2 * c2
247 c = c1
249 self.v0 = (-b + np.sqrt(b**2 - 4 * a * c)) / (2 * a)
250 self.e0 = p3(self.v0, c0, c1, c2, c3)
251 self.B = (2 * c2 + 6 * c3 * self.v0) * self.v0
252 else:
253 self.v0 = self.eos_parameters[3]
254 self.e0 = self.eos_parameters[0]
255 self.B = self.eos_parameters[1]
257 if warn and not (minvol < self.v0 < maxvol):
258 warnings.warn(
259 'The minimum volume of your fit is not in '
260 'your volumes. You may not have a minimum in your dataset!')
262 return self.v0, self.e0, self.B
264 def getplotdata(self):
265 if self.v0 is None:
266 self.fit()
268 x = np.linspace(min(self.v), max(self.v), 100)
269 if self.eos_string == 'sj':
270 y = self.fit0(x**-(1 / 3))
271 else:
272 y = self.func(x, *self.eos_parameters)
274 return self.eos_string, self.e0, self.v0, self.B, x, y, self.v, self.e
276 def plot(self, filename=None, show=False, ax=None):
277 """Plot fitted energy curve.
279 Uses Matplotlib to plot the energy curve. Use *show=True* to
280 show the figure and *filename='abc.png'* or
281 *filename='abc.eps'* to save the figure to a file."""
283 import matplotlib.pyplot as plt
285 plotdata = self.getplotdata()
287 ax = plot(*plotdata, ax=ax)
289 if show:
290 plt.show()
291 if filename is not None:
292 fig = ax.get_figure()
293 fig.savefig(filename)
294 return ax
296 def fit_sjeos(self):
297 """Calculate volume, energy, and bulk modulus.
299 Returns the optimal volume, the minimum energy, and the bulk
300 modulus. Notice that the ASE units for the bulk modulus is
301 eV/Angstrom^3 - to get the value in GPa, do this::
303 v0, e0, B = eos.fit()
304 print(B / kJ * 1.0e24, 'GPa')
306 """
308 fit0 = np.poly1d(np.polyfit(self.v**-(1 / 3), self.e, 3))
309 fit1 = np.polyder(fit0, 1)
310 fit2 = np.polyder(fit1, 1)
312 self.v0 = None
313 for t in np.roots(fit1):
314 if isinstance(t, float) and t > 0 and fit2(t) > 0:
315 self.v0 = t**-3
316 break
318 if self.v0 is None:
319 raise ValueError('No minimum!')
321 self.e0 = fit0(t)
322 self.B = t**5 * fit2(t) / 9
323 self.fit0 = fit0
325 return self.v0, self.e0, self.B
328def plot(eos_string, e0, v0, B, x, y, v, e, ax=None):
329 if ax is None:
330 import matplotlib.pyplot as plt
331 ax = plt.gca()
333 ax.plot(x, y, ls='-', color='C3') # By default red line
334 ax.plot(v, e, ls='', marker='o', mec='C0', mfc='C0') # By default blue marker
336 try:
337 ax.set_xlabel(u'volume [Å$^3$]')
338 ax.set_ylabel(u'energy [eV]')
339 ax.set_title(u'%s: E: %.3f eV, V: %.3f Å$^3$, B: %.3f GPa' %
340 (eos_string, e0, v0,
341 B / kJ * 1.e24))
343 except ImportError: # XXX what would cause this error? LaTeX?
344 import warnings
345 warnings.warn('Could not use LaTeX formatting')
346 ax.set_xlabel(u'volume [L(length)^3]')
347 ax.set_ylabel(u'energy [E(energy)]')
348 ax.set_title(u'%s: E: %.3f E, V: %.3f L^3, B: %.3e E/L^3' %
349 (eos_string, e0, v0, B))
351 return ax
354def calculate_eos(atoms, npoints=5, eps=0.04, trajectory=None, callback=None):
355 """Calculate equation-of-state.
357 atoms: Atoms object
358 System to calculate EOS for. Must have a calculator attached.
359 npoints: int
360 Number of points.
361 eps: float
362 Variation in volume from v0*(1-eps) to v0*(1+eps).
363 trajectory: Trjectory object or str
364 Write configurations to a trajectory file.
365 callback: function
366 Called after every energy calculation.
368 >>> from ase.build import bulk
369 >>> from ase.calculators.emt import EMT
370 >>> a = bulk('Cu', 'fcc', a=3.6)
371 >>> a.calc = EMT()
372 >>> eos = calculate_eos(a, trajectory='Cu.traj')
373 >>> v, e, B = eos.fit()
374 >>> a = (4 * v)**(1 / 3.0)
375 >>> print('{0:.6f}'.format(a))
376 3.589825
377 """
379 # Save original positions and cell:
380 p0 = atoms.get_positions()
381 c0 = atoms.get_cell()
383 if isinstance(trajectory, str):
384 from ase.io import Trajectory
385 trajectory = Trajectory(trajectory, 'w', atoms)
387 if trajectory is not None:
388 trajectory.set_description({'type': 'eos',
389 'npoints': npoints,
390 'eps': eps})
392 try:
393 energies = []
394 volumes = []
395 for x in np.linspace(1 - eps, 1 + eps, npoints)**(1 / 3):
396 atoms.set_cell(x * c0, scale_atoms=True)
397 volumes.append(atoms.get_volume())
398 energies.append(atoms.get_potential_energy())
399 if callback:
400 callback()
401 if trajectory is not None:
402 trajectory.write()
403 return EquationOfState(volumes, energies)
404 finally:
405 atoms.cell = c0
406 atoms.positions = p0
407 if trajectory is not None:
408 trajectory.close()
411class CLICommand:
412 """Calculate EOS from one or more trajectory files.
414 See https://wiki.fysik.dtu.dk/ase/tutorials/eos/eos.html for
415 more information.
416 """
418 @staticmethod
419 def add_arguments(parser):
420 parser.add_argument('trajectories', nargs='+', metavar='trajectory')
421 parser.add_argument('-p', '--plot', action='store_true',
422 help='Plot EOS fit. Default behaviour is '
423 'to write results of fit.')
424 parser.add_argument('-t', '--type', default='sj',
425 help='Type of fit. Must be one of {}.'
426 .format(', '.join(eos_names)))
428 @staticmethod
429 def run(args):
430 from ase.io import read
432 if not args.plot:
433 print('# filename '
434 'points volume energy bulk modulus')
435 print('# '
436 ' [Ang^3] [eV] [GPa]')
437 for name in args.trajectories:
438 if name == '-':
439 # Special case - used by ASE's GUI:
440 import pickle
441 import sys
442 v, e = pickle.load(sys.stdin.buffer)
443 else:
444 if '@' in name:
445 index = None
446 else:
447 index = ':'
448 images = read(name, index=index)
449 v = [atoms.get_volume() for atoms in images]
450 e = [atoms.get_potential_energy() for atoms in images]
451 eos = EquationOfState(v, e, args.type)
452 if args.plot:
453 eos.plot()
454 else:
455 try:
456 v0, e0, B = eos.fit()
457 except ValueError as ex:
458 print('{:30}{:2} {}'
459 .format(name, len(v), ex.message))
460 else:
461 print('{:30}{:2} {:10.3f}{:10.3f}{:14.3f}'
462 .format(name, len(v), v0, e0, B / kJ * 1.0e24))