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
1"""Calculator for the Embedded Atom Method Potential"""
3# eam.py
4# Embedded Atom Method Potential
5# These routines integrate with the ASE simulation environment
6# Paul White (Oct 2012)
7# UNCLASSIFIED
8# License: See accompanying license files for details
10import os
11import numpy as np
13from ase.neighborlist import NeighborList
14from ase.calculators.calculator import Calculator, all_changes
15from scipy.interpolate import InterpolatedUnivariateSpline as spline
16from ase.units import Bohr, Hartree
19class EAM(Calculator):
20 r"""
22 EAM Interface Documentation
24Introduction
25============
27The Embedded Atom Method (EAM) [1]_ is a classical potential which is
28good for modelling metals, particularly fcc materials. Because it is
29an equiaxial potential the EAM does not model directional bonds
30well. However, the Angular Dependent Potential (ADP) [2]_ which is an
31extended version of EAM is able to model directional bonds and is also
32included in the EAM calculator.
34Generally all that is required to use this calculator is to supply a
35potential file or as a set of functions that describe the potential.
36The files containing the potentials for this calculator are not
37included but many suitable potentials can be downloaded from The
38Interatomic Potentials Repository Project at
39https://www.ctcms.nist.gov/potentials/
41Theory
42======
44A single element EAM potential is defined by three functions: the
45embedded energy, electron density and the pair potential. A two
46element alloy contains the individual three functions for each element
47plus cross pair interactions. The ADP potential has two additional
48sets of data to define the dipole and quadrupole directional terms for
49each alloy and their cross interactions.
51The total energy `E_{\rm tot}` of an arbitrary arrangement of atoms is
52given by the EAM potential as
54.. math::
55 E_\text{tot} = \sum_i F(\bar\rho_i) + \frac{1}{2}\sum_{i\ne j} \phi(r_{ij})
57and
59.. math::
60 \bar\rho_i = \sum_j \rho(r_{ij})
62where `F` is an embedding function, namely the energy to embed an atom `i` in
63the combined electron density `\bar\rho_i` which is contributed from
64each of its neighbouring atoms `j` by an amount `\rho(r_{ij})`,
65`\phi(r_{ij})` is the pair potential function representing the energy
66in bond `ij` which is due to the short-range electro-static
67interaction between atoms, and `r_{ij}` is the distance between an
68atom and its neighbour for that bond.
70The ADP potential is defined as
72.. math::
73 E_\text{tot} = \sum_i F(\bar\rho_i) + \frac{1}{2}\sum_{i\ne j} \phi(r_{ij})
74 + \frac{1}{2} \sum_{i,\alpha} (\mu_i^\alpha)^2
75 + \frac{1}{2} \sum_{i,\alpha,\beta} (\lambda_i^{\alpha\beta})^2
76 - \frac{1}{6} \sum_i \nu_i^2
78where `\mu_i^\alpha` is the dipole vector, `\lambda_i^{\alpha\beta}`
79is the quadrupole tensor and `\nu_i` is the trace of
80`\lambda_i^{\alpha\beta}`.
82The fs potential is defined as
84.. math::
85 E_i = F_\alpha (\sum_{j\neq i} \rho_{\alpha \beta}(r_{ij}))
86 + \frac{1}{2}\sum_{j\neq i}\phi_{\alpha \beta}(r_{ij})
88where `\alpha` and `\beta` are element types of atoms. This form is similar to
89original EAM formula above, except that `\rho` and `\phi` are determined
90by element types.
92Running the Calculator
93======================
95EAM calculates the cohesive atom energy and forces. Internally the
96potential functions are defined by splines which may be directly
97supplied or created by reading the spline points from a data file from
98which a spline function is created. The LAMMPS compatible ``.alloy``, ``.fs``
99and ``.adp`` formats are supported. The LAMMPS ``.eam`` format is
100slightly different from the ``.alloy`` format and is currently not
101supported.
103For example::
105 from ase.calculators.eam import EAM
107 mishin = EAM(potential='Al99.eam.alloy')
108 mishin.write_potential('new.eam.alloy')
109 mishin.plot()
111 slab.calc = mishin
112 slab.get_potential_energy()
113 slab.get_forces()
115The breakdown of energy contribution from the indvidual components are
116stored in the calculator instance ``.results['energy_components']``
118Arguments
119=========
121========================= ====================================================
122Keyword Description
123========================= ====================================================
124``potential`` file of potential in ``.eam``, ``.alloy``, ``.adp`` or ``.fs``
125 format or file object (This is generally all you need to supply).
126 In case of file object the ``form`` argument is required
128``elements[N]`` array of N element abbreviations
130``embedded_energy[N]`` arrays of embedded energy functions
132``electron_density[N]`` arrays of electron density functions
134``phi[N,N]`` arrays of pair potential functions
136``d_embedded_energy[N]`` arrays of derivative embedded energy functions
138``d_electron_density[N]`` arrays of derivative electron density functions
140``d_phi[N,N]`` arrays of derivative pair potentials functions
142``d[N,N], q[N,N]`` ADP dipole and quadrupole function
144``d_d[N,N], d_q[N,N]`` ADP dipole and quadrupole derivative functions
146``skin`` skin distance passed to NeighborList(). If no atom
147 has moved more than the skin-distance since the last
148 call to the ``update()`` method then the neighbor
149 list can be reused. Defaults to 1.0.
151``form`` the form of the potential ``eam``, ``alloy``, ``adp`` or
152 ``fs``. This will be determined from the file suffix
153 or must be set if using equations or file object
155========================= ====================================================
158Additional parameters for writing potential files
159=================================================
161The following parameters are only required for writing a potential in
162``.alloy``, ``.adp`` or ``fs`` format file.
164========================= ====================================================
165Keyword Description
166========================= ====================================================
167``header`` Three line text header. Default is standard message.
169``Z[N]`` Array of atomic number of each element
171``mass[N]`` Atomic mass of each element
173``a[N]`` Array of lattice parameters for each element
175``lattice[N]`` Lattice type
177``nrho`` No. of rho samples along embedded energy curve
179``drho`` Increment for sampling density
181``nr`` No. of radial points along density and pair
182 potential curves
184``dr`` Increment for sampling radius
186========================= ====================================================
188Special features
189================
191``.plot()``
192 Plots the individual functions. This may be called from multiple EAM
193 potentials to compare the shape of the individual curves. This
194 function requires the installation of the Matplotlib libraries.
196Notes/Issues
197=============
199* Although currently not fast, this calculator can be good for trying
200 small calculations or for creating new potentials by matching baseline
201 data such as from DFT results. The format for these potentials is
202 compatible with LAMMPS_ and so can be used either directly by LAMMPS or
203 with the ASE LAMMPS calculator interface.
205* Supported formats are the LAMMPS_ ``.alloy`` and ``.adp``. The
206 ``.eam`` format is currently not supported. The form of the
207 potential will be determined from the file suffix.
209* Any supplied values will override values read from the file.
211* The derivative functions, if supplied, are only used to calculate
212 forces.
214* There is a bug in early versions of scipy that will cause eam.py to
215 crash when trying to evaluate splines of a potential with one
216 neighbor such as caused by evaluating a dimer.
218.. _LAMMPS: http://lammps.sandia.gov/
220.. [1] M.S. Daw and M.I. Baskes, Phys. Rev. Letters 50 (1983)
221 1285.
223.. [2] Y. Mishin, M.J. Mehl, and D.A. Papaconstantopoulos,
224 Acta Materialia 53 2005 4029--4041.
227End EAM Interface Documentation
228 """
230 implemented_properties = ['energy', 'forces']
232 default_parameters = dict(
233 skin=1.0,
234 potential=None,
235 header=[b'EAM/ADP potential file\n',
236 b'Generated from eam.py\n',
237 b'blank\n'])
239 def __init__(self, restart=None,
240 ignore_bad_restart_file=Calculator._deprecated,
241 label=os.curdir, atoms=None, form=None, **kwargs):
243 self.form = form
245 if 'potential' in kwargs:
246 self.read_potential(kwargs['potential'])
248 Calculator.__init__(self, restart, ignore_bad_restart_file,
249 label, atoms, **kwargs)
251 valid_args = ('potential', 'elements', 'header', 'drho', 'dr',
252 'cutoff', 'atomic_number', 'mass', 'a', 'lattice',
253 'embedded_energy', 'electron_density', 'phi',
254 # derivatives
255 'd_embedded_energy', 'd_electron_density', 'd_phi',
256 'd', 'q', 'd_d', 'd_q', # adp terms
257 'skin', 'Z', 'nr', 'nrho', 'mass')
259 # set any additional keyword arguments
260 for arg, val in self.parameters.items():
261 if arg in valid_args:
262 setattr(self, arg, val)
263 else:
264 raise RuntimeError('unknown keyword arg "%s" : not in %s'
265 % (arg, valid_args))
267 def set_form(self, name):
268 """set the form variable based on the file name suffix"""
269 extension = os.path.splitext(name)[1]
271 if extension == '.eam':
272 self.form = 'eam'
273 elif extension == '.alloy':
274 self.form = 'alloy'
275 elif extension == '.adp':
276 self.form = 'adp'
277 elif extension == '.fs':
278 self.form = 'fs'
279 else:
280 raise RuntimeError('unknown file extension type: %s' % extension)
282 def read_potential(self, filename):
283 """Reads a LAMMPS EAM file in alloy or adp format
284 and creates the interpolation functions from the data
285 """
287 if isinstance(filename, str):
288 with open(filename) as fd:
289 self._read_potential(fd)
290 else:
291 fd = filename
292 self._read_potential(fd)
294 def _read_potential(self, fd):
295 if self.form is None:
296 self.set_form(fd.name)
298 lines = fd.readlines()
300 def lines_to_list(lines):
301 """Make the data one long line so as not to care how its formatted
302 """
303 data = []
304 for line in lines:
305 data.extend(line.split())
306 return data
308 if self.form == 'eam': # single element eam file (aka funcfl)
309 self.header = lines[:1]
311 data = lines_to_list(lines[1:])
313 # eam form is just like an alloy form for one element
315 self.Nelements = 1
316 self.Z = np.array([data[0]], dtype=int)
317 self.mass = np.array([data[1]])
318 self.a = np.array([data[2]])
319 self.lattice = [data[3]]
321 self.nrho = int(data[4])
322 self.drho = float(data[5])
323 self.nr = int(data[6])
324 self.dr = float(data[7])
325 self.cutoff = float(data[8])
327 n = 9 + self.nrho
328 self.embedded_data = np.array([np.float_(data[9:n])])
330 self.rphi_data = np.zeros([self.Nelements, self.Nelements,
331 self.nr])
333 effective_charge = np.float_(data[n:n + self.nr])
334 # convert effective charges to rphi according to
335 # http://lammps.sandia.gov/doc/pair_eam.html
336 self.rphi_data[0, 0] = Bohr * Hartree * (effective_charge**2)
338 self.density_data = np.array(
339 [np.float_(data[n + self.nr:n + 2 * self.nr])])
341 elif self.form in ['alloy', 'adq']:
342 self.header = lines[:3]
343 i = 3
345 data = lines_to_list(lines[i:])
347 self.Nelements = int(data[0])
348 d = 1
349 self.elements = data[d:d + self.Nelements]
350 d += self.Nelements
352 self.nrho = int(data[d])
353 self.drho = float(data[d + 1])
354 self.nr = int(data[d + 2])
355 self.dr = float(data[d + 3])
356 self.cutoff = float(data[d + 4])
358 self.embedded_data = np.zeros([self.Nelements, self.nrho])
359 self.density_data = np.zeros([self.Nelements, self.nr])
360 self.Z = np.zeros([self.Nelements], dtype=int)
361 self.mass = np.zeros([self.Nelements])
362 self.a = np.zeros([self.Nelements])
363 self.lattice = []
364 d += 5
366 # reads in the part of the eam file for each element
367 for elem in range(self.Nelements):
368 self.Z[elem] = int(data[d])
369 self.mass[elem] = float(data[d + 1])
370 self.a[elem] = float(data[d + 2])
371 self.lattice.append(data[d + 3])
372 d += 4
374 self.embedded_data[elem] = np.float_(
375 data[d:(d + self.nrho)])
376 d += self.nrho
377 self.density_data[elem] = np.float_(data[d:(d + self.nr)])
378 d += self.nr
380 # reads in the r*phi data for each interaction between elements
381 self.rphi_data = np.zeros([self.Nelements, self.Nelements,
382 self.nr])
384 for i in range(self.Nelements):
385 for j in range(i + 1):
386 self.rphi_data[j, i] = np.float_(data[d:(d + self.nr)])
387 d += self.nr
389 elif self.form == 'fs':
390 self.header = lines[:3]
391 i = 3
393 data = lines_to_list(lines[i:])
395 self.Nelements = int(data[0])
396 d = 1
397 self.elements = data[d:d + self.Nelements]
398 d += self.Nelements
400 self.nrho = int(data[d])
401 self.drho = float(data[d + 1])
402 self.nr = int(data[d + 2])
403 self.dr = float(data[d + 3])
404 self.cutoff = float(data[d + 4])
406 self.embedded_data = np.zeros([self.Nelements, self.nrho])
407 self.density_data = np.zeros([self.Nelements, self.Nelements,
408 self.nr])
409 self.Z = np.zeros([self.Nelements], dtype=int)
410 self.mass = np.zeros([self.Nelements])
411 self.a = np.zeros([self.Nelements])
412 self.lattice = []
413 d += 5
415 # reads in the part of the eam file for each element
416 for elem in range(self.Nelements):
417 self.Z[elem] = int(data[d])
418 self.mass[elem] = float(data[d + 1])
419 self.a[elem] = float(data[d + 2])
420 self.lattice.append(data[d + 3])
421 d += 4
423 self.embedded_data[elem] = np.float_(
424 data[d:(d + self.nrho)])
425 d += self.nrho
426 self.density_data[elem, :, :] = np.float_(
427 data[d:(d + self.nr*self.Nelements)]).reshape([self.Nelements, self.nr])
428 d += self.nr*self.Nelements
430 # reads in the r*phi data for each interaction between elements
431 self.rphi_data = np.zeros([self.Nelements, self.Nelements,
432 self.nr])
434 for i in range(self.Nelements):
435 for j in range(i + 1):
436 self.rphi_data[j, i] = np.float_(data[d:(d + self.nr)])
437 d += self.nr
439 self.r = np.arange(0, self.nr) * self.dr
440 self.rho = np.arange(0, self.nrho) * self.drho
442 # choose the set_splines method according to the type
443 if self.form == 'fs':
444 self.set_fs_splines()
445 else:
446 self.set_splines()
448 if (self.form == 'adp'):
449 self.read_adp_data(data, d)
450 self.set_adp_splines()
452 def set_splines(self):
453 # this section turns the file data into three functions (and
454 # derivative functions) that define the potential
455 self.embedded_energy = np.empty(self.Nelements, object)
456 self.electron_density = np.empty(self.Nelements, object)
457 self.d_embedded_energy = np.empty(self.Nelements, object)
458 self.d_electron_density = np.empty(self.Nelements, object)
460 for i in range(self.Nelements):
461 self.embedded_energy[i] = spline(self.rho,
462 self.embedded_data[i], k=3)
463 self.electron_density[i] = spline(self.r,
464 self.density_data[i], k=3)
465 self.d_embedded_energy[i] = self.deriv(self.embedded_energy[i])
466 self.d_electron_density[i] = self.deriv(self.electron_density[i])
468 self.phi = np.empty([self.Nelements, self.Nelements], object)
469 self.d_phi = np.empty([self.Nelements, self.Nelements], object)
471 # ignore the first point of the phi data because it is forced
472 # to go through zero due to the r*phi format in alloy and adp
473 for i in range(self.Nelements):
474 for j in range(i, self.Nelements):
475 self.phi[i, j] = spline(
476 self.r[1:],
477 self.rphi_data[i, j][1:] / self.r[1:], k=3)
479 self.d_phi[i, j] = self.deriv(self.phi[i, j])
481 if j != i:
482 self.phi[j, i] = self.phi[i, j]
483 self.d_phi[j, i] = self.d_phi[i, j]
485 def set_fs_splines(self):
486 self.embedded_energy = np.empty(self.Nelements, object)
487 self.electron_density = np.empty(
488 [self.Nelements, self.Nelements], object)
489 self.d_embedded_energy = np.empty(self.Nelements, object)
490 self.d_electron_density = np.empty(
491 [self.Nelements, self.Nelements], object)
493 for i in range(self.Nelements):
494 self.embedded_energy[i] = spline(self.rho,
495 self.embedded_data[i], k=3)
496 self.d_embedded_energy[i] = self.deriv(self.embedded_energy[i])
497 for j in range(self.Nelements):
498 self.electron_density[i, j] = spline(
499 self.r, self.density_data[i, j], k=3)
500 self.d_electron_density[i, j] = self.deriv(
501 self.electron_density[i, j])
503 self.phi = np.empty([self.Nelements, self.Nelements], object)
504 self.d_phi = np.empty([self.Nelements, self.Nelements], object)
506 for i in range(self.Nelements):
507 for j in range(i, self.Nelements):
508 self.phi[i, j] = spline(
509 self.r[1:],
510 self.rphi_data[i, j][1:] / self.r[1:], k=3)
512 self.d_phi[i, j] = self.deriv(self.phi[i, j])
514 if j != i:
515 self.phi[j, i] = self.phi[i, j]
516 self.d_phi[j, i] = self.d_phi[i, j]
518 def set_adp_splines(self):
519 self.d = np.empty([self.Nelements, self.Nelements], object)
520 self.d_d = np.empty([self.Nelements, self.Nelements], object)
521 self.q = np.empty([self.Nelements, self.Nelements], object)
522 self.d_q = np.empty([self.Nelements, self.Nelements], object)
524 for i in range(self.Nelements):
525 for j in range(i, self.Nelements):
526 self.d[i, j] = spline(self.r[1:], self.d_data[i, j][1:], k=3)
527 self.d_d[i, j] = self.deriv(self.d[i, j])
528 self.q[i, j] = spline(self.r[1:], self.q_data[i, j][1:], k=3)
529 self.d_q[i, j] = self.deriv(self.q[i, j])
531 # make symmetrical
532 if j != i:
533 self.d[j, i] = self.d[i, j]
534 self.d_d[j, i] = self.d_d[i, j]
535 self.q[j, i] = self.q[i, j]
536 self.d_q[j, i] = self.d_q[i, j]
538 def read_adp_data(self, data, d):
539 """read in the extra adp data from the potential file"""
541 self.d_data = np.zeros([self.Nelements, self.Nelements, self.nr])
542 # should be non symmetrical combinations of 2
543 for i in range(self.Nelements):
544 for j in range(i + 1):
545 self.d_data[j, i] = data[d:d + self.nr]
546 d += self.nr
548 self.q_data = np.zeros([self.Nelements, self.Nelements, self.nr])
549 # should be non symmetrical combinations of 2
550 for i in range(self.Nelements):
551 for j in range(i + 1):
552 self.q_data[j, i] = data[d:d + self.nr]
553 d += self.nr
555 def write_potential(self, filename, nc=1, numformat='%.8e'):
556 """Writes out the potential in the format given by the form
557 variable to 'filename' with a data format that is nc columns
558 wide. Note: array lengths need to be an exact multiple of nc
559 """
561 with open(filename, 'wb') as fd:
562 self._write_potential(fd, nc=nc, numformat=numformat)
564 def _write_potential(self, fd, nc, numformat):
565 assert self.nr % nc == 0
566 assert self.nrho % nc == 0
568 for line in self.header:
569 fd.write(line)
571 fd.write('{0} '.format(self.Nelements).encode())
572 fd.write(' '.join(self.elements).encode() + b'\n')
574 fd.write(('%d %f %d %f %f \n' %
575 (self.nrho, self.drho, self.nr,
576 self.dr, self.cutoff)).encode())
578 # start of each section for each element
579# rs = np.linspace(0, self.nr * self.dr, self.nr)
580# rhos = np.linspace(0, self.nrho * self.drho, self.nrho)
582 rs = np.arange(0, self.nr) * self.dr
583 rhos = np.arange(0, self.nrho) * self.drho
585 for i in range(self.Nelements):
586 fd.write(('%d %f %f %s\n' %
587 (self.Z[i], self.mass[i],
588 self.a[i], str(self.lattice[i]))).encode())
589 np.savetxt(fd,
590 self.embedded_energy[i](rhos).reshape(self.nrho // nc,
591 nc),
592 fmt=nc * [numformat])
593 if self.form == 'fs':
594 for j in range(self.Nelements):
595 np.savetxt(fd,
596 self.electron_density[i, j](rs).reshape(
597 self.nr // nc, nc),
598 fmt=nc * [numformat])
599 else:
600 np.savetxt(fd,
601 self.electron_density[i](rs).reshape(
602 self.nr // nc, nc),
603 fmt=nc * [numformat])
605 # write out the pair potentials in Lammps DYNAMO setfl format
606 # as r*phi for alloy format
607 for i in range(self.Nelements):
608 for j in range(i, self.Nelements):
609 np.savetxt(fd,
610 (rs * self.phi[i, j](rs)).reshape(self.nr // nc,
611 nc),
612 fmt=nc * [numformat])
614 if self.form == 'adp':
615 # these are the u(r) or dipole values
616 for i in range(self.Nelements):
617 for j in range(i + 1):
618 np.savetxt(fd, self.d_data[i, j])
620 # these are the w(r) or quadrupole values
621 for i in range(self.Nelements):
622 for j in range(i + 1):
623 np.savetxt(fd, self.q_data[i, j])
625 def update(self, atoms):
626 # check all the elements are available in the potential
627 self.Nelements = len(self.elements)
628 elements = np.unique(atoms.get_chemical_symbols())
629 unavailable = np.logical_not(
630 np.array([item in self.elements for item in elements]))
632 if np.any(unavailable):
633 raise RuntimeError('These elements are not in the potential: %s' %
634 elements[unavailable])
636 # cutoffs need to be a vector for NeighborList
637 cutoffs = self.cutoff * np.ones(len(atoms))
639 # convert the elements to an index of the position
640 # in the eam format
641 self.index = np.array([self.elements.index(el)
642 for el in atoms.get_chemical_symbols()])
643 self.pbc = atoms.get_pbc()
645 # since we need the contribution of all neighbors to the
646 # local electron density we cannot just calculate and use
647 # one way neighbors
648 self.neighbors = NeighborList(cutoffs,
649 skin=self.parameters.skin,
650 self_interaction=False,
651 bothways=True)
652 self.neighbors.update(atoms)
654 def calculate(self, atoms=None, properties=['energy'],
655 system_changes=all_changes):
656 """EAM Calculator
658 atoms: Atoms object
659 Contains positions, unit-cell, ...
660 properties: list of str
661 List of what needs to be calculated. Can be any combination
662 of 'energy', 'forces'
663 system_changes: list of str
664 List of what has changed since last calculation. Can be
665 any combination of these five: 'positions', 'numbers', 'cell',
666 'pbc', 'initial_charges' and 'initial_magmoms'.
667 """
669 Calculator.calculate(self, atoms, properties, system_changes)
671 # we shouldn't really recalc if charges or magmos change
672 if len(system_changes) > 0: # something wrong with this way
673 self.update(self.atoms)
674 self.calculate_energy(self.atoms)
676 if 'forces' in properties:
677 self.calculate_forces(self.atoms)
679 # check we have all the properties requested
680 for property in properties:
681 if property not in self.results:
682 if property == 'energy':
683 self.calculate_energy(self.atoms)
685 if property == 'forces':
686 self.calculate_forces(self.atoms)
688 # we need to remember the previous state of parameters
689# if 'potential' in parameter_changes and potential != None:
690# self.read_potential(potential)
692 def calculate_energy(self, atoms):
693 """Calculate the energy
694 the energy is made up of the ionic or pair interaction and
695 the embedding energy of each atom into the electron cloud
696 generated by its neighbors
697 """
699 pair_energy = 0.0
700 embedding_energy = 0.0
701 mu_energy = 0.0
702 lam_energy = 0.0
703 trace_energy = 0.0
705 self.total_density = np.zeros(len(atoms))
706 if (self.form == 'adp'):
707 self.mu = np.zeros([len(atoms), 3])
708 self.lam = np.zeros([len(atoms), 3, 3])
710 for i in range(len(atoms)): # this is the atom to be embedded
711 neighbors, offsets = self.neighbors.get_neighbors(i)
712 offset = np.dot(offsets, atoms.get_cell())
714 rvec = (atoms.positions[neighbors] + offset -
715 atoms.positions[i])
717 # calculate the distance to the nearest neighbors
718 r = np.sqrt(np.sum(np.square(rvec), axis=1)) # fast
719# r = np.apply_along_axis(np.linalg.norm, 1, rvec) # sloow
721 nearest = np.arange(len(r))[r <= self.cutoff]
722 for j_index in range(self.Nelements):
723 use = self.index[neighbors[nearest]] == j_index
724 if not use.any():
725 continue
726 pair_energy += np.sum(self.phi[self.index[i], j_index](
727 r[nearest][use])) / 2.
729 if self.form == 'fs':
730 density = np.sum(
731 self.electron_density[j_index, self.index[i]](r[nearest][use]))
732 else:
733 density = np.sum(
734 self.electron_density[j_index](r[nearest][use]))
735 self.total_density[i] += density
737 if self.form == 'adp':
738 self.mu[i] += self.adp_dipole(
739 r[nearest][use],
740 rvec[nearest][use],
741 self.d[self.index[i], j_index])
743 self.lam[i] += self.adp_quadrupole(
744 r[nearest][use],
745 rvec[nearest][use],
746 self.q[self.index[i], j_index])
748 # add in the electron embedding energy
749 embedding_energy += self.embedded_energy[self.index[i]](
750 self.total_density[i])
752 components = dict(pair=pair_energy, embedding=embedding_energy)
754 if self.form == 'adp':
755 mu_energy += np.sum(self.mu ** 2) / 2.
756 lam_energy += np.sum(self.lam ** 2) / 2.
758 for i in range(len(atoms)): # this is the atom to be embedded
759 trace_energy -= np.sum(self.lam[i].trace() ** 2) / 6.
761 adp_result = dict(adp_mu=mu_energy,
762 adp_lam=lam_energy,
763 adp_trace=trace_energy)
764 components.update(adp_result)
766 self.positions = atoms.positions.copy()
767 self.cell = atoms.get_cell().copy()
769 energy = 0.0
770 for i in components.keys():
771 energy += components[i]
773 self.energy_free = energy
774 self.energy_zero = energy
776 self.results['energy_components'] = components
777 self.results['energy'] = energy
779 def calculate_forces(self, atoms):
780 # calculate the forces based on derivatives of the three EAM functions
782 self.update(atoms)
783 self.results['forces'] = np.zeros((len(atoms), 3))
785 for i in range(len(atoms)): # this is the atom to be embedded
786 neighbors, offsets = self.neighbors.get_neighbors(i)
787 offset = np.dot(offsets, atoms.get_cell())
788 # create a vector of relative positions of neighbors
789 rvec = atoms.positions[neighbors] + offset - atoms.positions[i]
790 r = np.sqrt(np.sum(np.square(rvec), axis=1))
791 nearest = np.arange(len(r))[r < self.cutoff]
793 d_embedded_energy_i = self.d_embedded_energy[
794 self.index[i]](self.total_density[i])
795 urvec = rvec.copy() # unit directional vector
797 for j in np.arange(len(neighbors)):
798 urvec[j] = urvec[j] / r[j]
800 for j_index in range(self.Nelements):
801 use = self.index[neighbors[nearest]] == j_index
802 if not use.any():
803 continue
804 rnuse = r[nearest][use]
805 density_j = self.total_density[neighbors[nearest][use]]
806 if self.form == 'fs':
807 scale = (self.d_phi[self.index[i], j_index](rnuse) +
808 (d_embedded_energy_i *
809 self.d_electron_density[j_index, self.index[i]](rnuse)) +
810 (self.d_embedded_energy[j_index](density_j) *
811 self.d_electron_density[self.index[i], j_index](rnuse)))
812 else:
813 scale = (self.d_phi[self.index[i], j_index](rnuse) +
814 (d_embedded_energy_i *
815 self.d_electron_density[j_index](rnuse)) +
816 (self.d_embedded_energy[j_index](density_j) *
817 self.d_electron_density[self.index[i]](rnuse)))
819 self.results['forces'][i] += np.dot(scale, urvec[nearest][use])
821 if (self.form == 'adp'):
822 adp_forces = self.angular_forces(
823 self.mu[i],
824 self.mu[neighbors[nearest][use]],
825 self.lam[i],
826 self.lam[neighbors[nearest][use]],
827 rnuse,
828 rvec[nearest][use],
829 self.index[i],
830 j_index)
832 self.results['forces'][i] += adp_forces
834 def angular_forces(self, mu_i, mu, lam_i, lam, r, rvec, form1, form2):
835 # calculate the extra components for the adp forces
836 # rvec are the relative positions to atom i
837 psi = np.zeros(mu.shape)
838 for gamma in range(3):
839 term1 = (mu_i[gamma] - mu[:, gamma]) * self.d[form1][form2](r)
841 term2 = np.sum((mu_i - mu) *
842 self.d_d[form1][form2](r)[:, np.newaxis] *
843 (rvec * rvec[:, gamma][:, np.newaxis] /
844 r[:, np.newaxis]), axis=1)
846 term3 = 2 * np.sum((lam_i[:, gamma] + lam[:, :, gamma]) *
847 rvec * self.q[form1][form2](r)[:, np.newaxis],
848 axis=1)
849 term4 = 0.0
850 for alpha in range(3):
851 for beta in range(3):
852 rs = rvec[:, alpha] * rvec[:, beta] * rvec[:, gamma]
853 term4 += ((lam_i[alpha, beta] + lam[:, alpha, beta]) *
854 self.d_q[form1][form2](r) * rs) / r
856 term5 = ((lam_i.trace() + lam.trace(axis1=1, axis2=2)) *
857 (self.d_q[form1][form2](r) * r +
858 2 * self.q[form1][form2](r)) * rvec[:, gamma]) / 3.
860 # the minus for term5 is a correction on the adp
861 # formulation given in the 2005 Mishin Paper and is posted
862 # on the NIST website with the AlH potential
863 psi[:, gamma] = term1 + term2 + term3 + term4 - term5
865 return np.sum(psi, axis=0)
867 def adp_dipole(self, r, rvec, d):
868 # calculate the dipole contribution
869 mu = np.sum((rvec * d(r)[:, np.newaxis]), axis=0)
871 return mu # sign to agree with lammps
873 def adp_quadrupole(self, r, rvec, q):
874 # slow way of calculating the quadrupole contribution
875 r = np.sqrt(np.sum(rvec ** 2, axis=1))
877 lam = np.zeros([rvec.shape[0], 3, 3])
878 qr = q(r)
879 for alpha in range(3):
880 for beta in range(3):
881 lam[:, alpha, beta] += qr * rvec[:, alpha] * rvec[:, beta]
883 return np.sum(lam, axis=0)
885 def deriv(self, spline):
886 """Wrapper for extracting the derivative from a spline"""
887 def d_spline(aspline):
888 return spline(aspline, 1)
890 return d_spline
892 def plot(self, name=''):
893 """Plot the individual curves"""
895 import matplotlib.pyplot as plt
897 if self.form == 'eam' or self.form == 'alloy' or self.form == 'fs':
898 nrow = 2
899 elif self.form == 'adp':
900 nrow = 3
901 else:
902 raise RuntimeError('Unknown form of potential: %s' % self.form)
904 if hasattr(self, 'r'):
905 r = self.r
906 else:
907 r = np.linspace(0, self.cutoff, 50)
909 if hasattr(self, 'rho'):
910 rho = self.rho
911 else:
912 rho = np.linspace(0, 10.0, 50)
914 plt.subplot(nrow, 2, 1)
915 self.elem_subplot(rho, self.embedded_energy,
916 r'$\rho$', r'Embedding Energy $F(\bar\rho)$',
917 name, plt)
919 plt.subplot(nrow, 2, 2)
920 if self.form == 'fs':
921 self.multielem_subplot(r, self.electron_density,
922 r'$r$', r'Electron Density $\rho(r)$', name, plt, half=False)
923 else:
924 self.elem_subplot(r, self.electron_density,
925 r'$r$', r'Electron Density $\rho(r)$', name, plt)
927 plt.subplot(nrow, 2, 3)
928 self.multielem_subplot(r, self.phi,
929 r'$r$', r'Pair Potential $\phi(r)$', name, plt)
930 plt.ylim(-1.0, 1.0) # need reasonable values
932 if self.form == 'adp':
933 plt.subplot(nrow, 2, 5)
934 self.multielem_subplot(r, self.d,
935 r'$r$', r'Dipole Energy', name, plt)
937 plt.subplot(nrow, 2, 6)
938 self.multielem_subplot(r, self.q,
939 r'$r$', r'Quadrupole Energy', name, plt)
941 plt.plot()
943 def elem_subplot(self, curvex, curvey, xlabel, ylabel, name, plt):
944 plt.xlabel(xlabel)
945 plt.ylabel(ylabel)
946 for i in np.arange(self.Nelements):
947 label = name + ' ' + self.elements[i]
948 plt.plot(curvex, curvey[i](curvex), label=label)
949 plt.legend()
951 def multielem_subplot(self, curvex, curvey, xlabel, ylabel, name, plt, half=True):
952 plt.xlabel(xlabel)
953 plt.ylabel(ylabel)
954 for i in np.arange(self.Nelements):
955 for j in np.arange((i + 1) if half else self.Nelements):
956 label = name + ' ' + self.elements[i] + '-' + self.elements[j]
957 plt.plot(curvex, curvey[i, j](curvex), label=label)
958 plt.legend()