Coverage for /builds/kinetik161/ase/ase/calculators/eam.py: 82.56%
407 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-12-10 11:04 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-12-10 11:04 +0000
1# flake8: noqa
2"""Calculator for the Embedded Atom Method Potential"""
4# eam.py
5# Embedded Atom Method Potential
6# These routines integrate with the ASE simulation environment
7# Paul White (Oct 2012)
8# UNCLASSIFIED
9# License: See accompanying license files for details
11import os
13import numpy as np
14from scipy.interpolate import InterpolatedUnivariateSpline as spline
16from ase.calculators.calculator import Calculator, all_changes
17from ase.neighborlist import NeighborList
18from ase.units import Bohr, Hartree
21class EAM(Calculator):
22 r"""
24 EAM Interface Documentation
26Introduction
27============
29The Embedded Atom Method (EAM) [1]_ is a classical potential which is
30good for modelling metals, particularly fcc materials. Because it is
31an equiaxial potential the EAM does not model directional bonds
32well. However, the Angular Dependent Potential (ADP) [2]_ which is an
33extended version of EAM is able to model directional bonds and is also
34included in the EAM calculator.
36Generally all that is required to use this calculator is to supply a
37potential file or as a set of functions that describe the potential.
38The files containing the potentials for this calculator are not
39included but many suitable potentials can be downloaded from The
40Interatomic Potentials Repository Project at
41https://www.ctcms.nist.gov/potentials/
43Theory
44======
46A single element EAM potential is defined by three functions: the
47embedded energy, electron density and the pair potential. A two
48element alloy contains the individual three functions for each element
49plus cross pair interactions. The ADP potential has two additional
50sets of data to define the dipole and quadrupole directional terms for
51each alloy and their cross interactions.
53The total energy `E_{\rm tot}` of an arbitrary arrangement of atoms is
54given by the EAM potential as
56.. math::
57 E_\text{tot} = \sum_i F(\bar\rho_i) + \frac{1}{2}\sum_{i\ne j} \phi(r_{ij})
59and
61.. math::
62 \bar\rho_i = \sum_j \rho(r_{ij})
64where `F` is an embedding function, namely the energy to embed an atom `i` in
65the combined electron density `\bar\rho_i` which is contributed from
66each of its neighbouring atoms `j` by an amount `\rho(r_{ij})`,
67`\phi(r_{ij})` is the pair potential function representing the energy
68in bond `ij` which is due to the short-range electro-static
69interaction between atoms, and `r_{ij}` is the distance between an
70atom and its neighbour for that bond.
72The ADP potential is defined as
74.. math::
75 E_\text{tot} = \sum_i F(\bar\rho_i) + \frac{1}{2}\sum_{i\ne j} \phi(r_{ij})
76 + \frac{1}{2} \sum_{i,\alpha} (\mu_i^\alpha)^2
77 + \frac{1}{2} \sum_{i,\alpha,\beta} (\lambda_i^{\alpha\beta})^2
78 - \frac{1}{6} \sum_i \nu_i^2
80where `\mu_i^\alpha` is the dipole vector, `\lambda_i^{\alpha\beta}`
81is the quadrupole tensor and `\nu_i` is the trace of
82`\lambda_i^{\alpha\beta}`.
84The fs potential is defined as
86.. math::
87 E_i = F_\alpha (\sum_{j\neq i} \rho_{\alpha \beta}(r_{ij}))
88 + \frac{1}{2}\sum_{j\neq i}\phi_{\alpha \beta}(r_{ij})
90where `\alpha` and `\beta` are element types of atoms. This form is similar to
91original EAM formula above, except that `\rho` and `\phi` are determined
92by element types.
94Running the Calculator
95======================
97EAM calculates the cohesive atom energy and forces. Internally the
98potential functions are defined by splines which may be directly
99supplied or created by reading the spline points from a data file from
100which a spline function is created. The LAMMPS compatible ``.alloy``, ``.fs``
101and ``.adp`` formats are supported. The LAMMPS ``.eam`` format is
102slightly different from the ``.alloy`` format and is currently not
103supported.
105For example::
107 from ase.calculators.eam import EAM
109 mishin = EAM(potential='Al99.eam.alloy')
110 mishin.write_potential('new.eam.alloy')
111 mishin.plot()
113 slab.calc = mishin
114 slab.get_potential_energy()
115 slab.get_forces()
117The breakdown of energy contribution from the indvidual components are
118stored in the calculator instance ``.results['energy_components']``
120Arguments
121=========
123========================= ====================================================
124Keyword Description
125========================= ====================================================
126``potential`` file of potential in ``.eam``, ``.alloy``, ``.adp`` or ``.fs``
127 format or file object
128 (This is generally all you need to supply).
129 For file object the ``form`` argument is required
131``elements[N]`` array of N element abbreviations
133``embedded_energy[N]`` arrays of embedded energy functions
135``electron_density[N]`` arrays of electron density functions
137``phi[N,N]`` arrays of pair potential functions
139``d_embedded_energy[N]`` arrays of derivative embedded energy functions
141``d_electron_density[N]`` arrays of derivative electron density functions
143``d_phi[N,N]`` arrays of derivative pair potentials functions
145``d[N,N], q[N,N]`` ADP dipole and quadrupole function
147``d_d[N,N], d_q[N,N]`` ADP dipole and quadrupole derivative functions
149``skin`` skin distance passed to NeighborList(). If no atom
150 has moved more than the skin-distance since the last
151 call to the ``update()`` method then the neighbor
152 list can be reused. Defaults to 1.0.
154``form`` the form of the potential
155 ``eam``, ``alloy``, ``adp`` or
156 ``fs``. This will be determined from the file suffix
157 or must be set if using equations or file object
159========================= ====================================================
162Additional parameters for writing potential files
163=================================================
165The following parameters are only required for writing a potential in
166``.alloy``, ``.adp`` or ``fs`` format file.
168========================= ====================================================
169Keyword Description
170========================= ====================================================
171``header`` Three line text header. Default is standard message.
173``Z[N]`` Array of atomic number of each element
175``mass[N]`` Atomic mass of each element
177``a[N]`` Array of lattice parameters for each element
179``lattice[N]`` Lattice type
181``nrho`` No. of rho samples along embedded energy curve
183``drho`` Increment for sampling density
185``nr`` No. of radial points along density and pair
186 potential curves
188``dr`` Increment for sampling radius
190========================= ====================================================
192Special features
193================
195``.plot()``
196 Plots the individual functions. This may be called from multiple EAM
197 potentials to compare the shape of the individual curves. This
198 function requires the installation of the Matplotlib libraries.
200Notes/Issues
201=============
203* Although currently not fast, this calculator can be good for trying
204 small calculations or for creating new potentials by matching baseline
205 data such as from DFT results. The format for these potentials is
206 compatible with LAMMPS_ and so can be used either directly by LAMMPS or
207 with the ASE LAMMPS calculator interface.
209* Supported formats are the LAMMPS_ ``.alloy`` and ``.adp``. The
210 ``.eam`` format is currently not supported. The form of the
211 potential will be determined from the file suffix.
213* Any supplied values will override values read from the file.
215* The derivative functions, if supplied, are only used to calculate
216 forces.
218* There is a bug in early versions of scipy that will cause eam.py to
219 crash when trying to evaluate splines of a potential with one
220 neighbor such as caused by evaluating a dimer.
222.. _LAMMPS: http://lammps.sandia.gov/
224.. [1] M.S. Daw and M.I. Baskes, Phys. Rev. Letters 50 (1983)
225 1285.
227.. [2] Y. Mishin, M.J. Mehl, and D.A. Papaconstantopoulos,
228 Acta Materialia 53 2005 4029--4041.
231End EAM Interface Documentation
232 """
234 implemented_properties = ['energy', 'forces']
236 default_parameters = dict(
237 skin=1.0,
238 potential=None,
239 header=[b'EAM/ADP potential file\n',
240 b'Generated from eam.py\n',
241 b'blank\n'])
243 def __init__(self, restart=None,
244 ignore_bad_restart_file=Calculator._deprecated,
245 label=os.curdir, atoms=None, form=None, **kwargs):
247 self.form = form
249 if 'potential' in kwargs:
250 self.read_potential(kwargs['potential'])
252 Calculator.__init__(self, restart, ignore_bad_restart_file,
253 label, atoms, **kwargs)
255 valid_args = ('potential', 'elements', 'header', 'drho', 'dr',
256 'cutoff', 'atomic_number', 'mass', 'a', 'lattice',
257 'embedded_energy', 'electron_density', 'phi',
258 # derivatives
259 'd_embedded_energy', 'd_electron_density', 'd_phi',
260 'd', 'q', 'd_d', 'd_q', # adp terms
261 'skin', 'Z', 'nr', 'nrho', 'mass')
263 # set any additional keyword arguments
264 for arg, val in self.parameters.items():
265 if arg in valid_args:
266 setattr(self, arg, val)
267 else:
268 raise RuntimeError(
269 f'unknown keyword arg "{arg}" : not in {valid_args}')
271 def set_form(self, name):
272 """set the form variable based on the file name suffix"""
273 extension = os.path.splitext(name)[1]
275 if extension == '.eam':
276 self.form = 'eam'
277 elif extension == '.alloy':
278 self.form = 'alloy'
279 elif extension == '.adp':
280 self.form = 'adp'
281 elif extension == '.fs':
282 self.form = 'fs'
283 else:
284 raise RuntimeError(f'unknown file extension type: {extension}')
286 def read_potential(self, filename):
287 """Reads a LAMMPS EAM file in alloy or adp format
288 and creates the interpolation functions from the data
289 """
291 if isinstance(filename, str):
292 with open(filename) as fd:
293 self._read_potential(fd)
294 else:
295 fd = filename
296 self._read_potential(fd)
298 def _read_potential(self, fd):
299 if self.form is None:
300 self.set_form(fd.name)
302 lines = fd.readlines()
304 def lines_to_list(lines):
305 """Make the data one long line so as not to care how its formatted
306 """
307 data = []
308 for line in lines:
309 data.extend(line.split())
310 return data
312 if self.form == 'eam': # single element eam file (aka funcfl)
313 self.header = lines[:1]
315 data = lines_to_list(lines[1:])
317 # eam form is just like an alloy form for one element
319 self.Nelements = 1
320 self.Z = np.array([data[0]], dtype=int)
321 self.mass = np.array([data[1]])
322 self.a = np.array([data[2]])
323 self.lattice = [data[3]]
325 self.nrho = int(data[4])
326 self.drho = float(data[5])
327 self.nr = int(data[6])
328 self.dr = float(data[7])
329 self.cutoff = float(data[8])
331 n = 9 + self.nrho
332 self.embedded_data = np.array([np.float_(data[9:n])])
334 self.rphi_data = np.zeros([self.Nelements, self.Nelements,
335 self.nr])
337 effective_charge = np.float_(data[n:n + self.nr])
338 # convert effective charges to rphi according to
339 # http://lammps.sandia.gov/doc/pair_eam.html
340 self.rphi_data[0, 0] = Bohr * Hartree * (effective_charge**2)
342 self.density_data = np.array(
343 [np.float_(data[n + self.nr:n + 2 * self.nr])])
345 elif self.form in ['alloy', 'adp']:
346 self.header = lines[:3]
347 i = 3
349 data = lines_to_list(lines[i:])
351 self.Nelements = int(data[0])
352 d = 1
353 self.elements = data[d:d + self.Nelements]
354 d += self.Nelements
356 self.nrho = int(data[d])
357 self.drho = float(data[d + 1])
358 self.nr = int(data[d + 2])
359 self.dr = float(data[d + 3])
360 self.cutoff = float(data[d + 4])
362 self.embedded_data = np.zeros([self.Nelements, self.nrho])
363 self.density_data = np.zeros([self.Nelements, self.nr])
364 self.Z = np.zeros([self.Nelements], dtype=int)
365 self.mass = np.zeros([self.Nelements])
366 self.a = np.zeros([self.Nelements])
367 self.lattice = []
368 d += 5
370 # reads in the part of the eam file for each element
371 for elem in range(self.Nelements):
372 self.Z[elem] = int(data[d])
373 self.mass[elem] = float(data[d + 1])
374 self.a[elem] = float(data[d + 2])
375 self.lattice.append(data[d + 3])
376 d += 4
378 self.embedded_data[elem] = np.float_(
379 data[d:(d + self.nrho)])
380 d += self.nrho
381 self.density_data[elem] = np.float_(data[d:(d + self.nr)])
382 d += self.nr
384 # reads in the r*phi data for each interaction between elements
385 self.rphi_data = np.zeros([self.Nelements, self.Nelements,
386 self.nr])
388 for i in range(self.Nelements):
389 for j in range(i + 1):
390 self.rphi_data[j, i] = np.float_(data[d:(d + self.nr)])
391 d += self.nr
393 elif self.form == 'fs':
394 self.header = lines[:3]
395 i = 3
397 data = lines_to_list(lines[i:])
399 self.Nelements = int(data[0])
400 d = 1
401 self.elements = data[d:d + self.Nelements]
402 d += self.Nelements
404 self.nrho = int(data[d])
405 self.drho = float(data[d + 1])
406 self.nr = int(data[d + 2])
407 self.dr = float(data[d + 3])
408 self.cutoff = float(data[d + 4])
410 self.embedded_data = np.zeros([self.Nelements, self.nrho])
411 self.density_data = np.zeros([self.Nelements, self.Nelements,
412 self.nr])
413 self.Z = np.zeros([self.Nelements], dtype=int)
414 self.mass = np.zeros([self.Nelements])
415 self.a = np.zeros([self.Nelements])
416 self.lattice = []
417 d += 5
419 # reads in the part of the eam file for each element
420 for elem in range(self.Nelements):
421 self.Z[elem] = int(data[d])
422 self.mass[elem] = float(data[d + 1])
423 self.a[elem] = float(data[d + 2])
424 self.lattice.append(data[d + 3])
425 d += 4
427 self.embedded_data[elem] = np.float_(
428 data[d:(d + self.nrho)])
429 d += self.nrho
430 self.density_data[elem, :, :] = np.float_(
431 data[d:(d + self.nr * self.Nelements)]).reshape([
432 self.Nelements, self.nr])
433 d += self.nr * self.Nelements
435 # reads in the r*phi data for each interaction between elements
436 self.rphi_data = np.zeros([self.Nelements, self.Nelements,
437 self.nr])
439 for i in range(self.Nelements):
440 for j in range(i + 1):
441 self.rphi_data[j, i] = np.float_(data[d:(d + self.nr)])
442 d += self.nr
444 self.r = np.arange(0, self.nr) * self.dr
445 self.rho = np.arange(0, self.nrho) * self.drho
447 # choose the set_splines method according to the type
448 if self.form == 'fs':
449 self.set_fs_splines()
450 else:
451 self.set_splines()
453 if self.form == 'adp':
454 self.read_adp_data(data, d)
455 self.set_adp_splines()
457 def set_splines(self):
458 # this section turns the file data into three functions (and
459 # derivative functions) that define the potential
460 self.embedded_energy = np.empty(self.Nelements, object)
461 self.electron_density = np.empty(self.Nelements, object)
462 self.d_embedded_energy = np.empty(self.Nelements, object)
463 self.d_electron_density = np.empty(self.Nelements, object)
465 for i in range(self.Nelements):
466 self.embedded_energy[i] = spline(self.rho,
467 self.embedded_data[i], k=3)
468 self.electron_density[i] = spline(self.r,
469 self.density_data[i], k=3)
470 self.d_embedded_energy[i] = self.deriv(self.embedded_energy[i])
471 self.d_electron_density[i] = self.deriv(self.electron_density[i])
473 self.phi = np.empty([self.Nelements, self.Nelements], object)
474 self.d_phi = np.empty([self.Nelements, self.Nelements], object)
476 # ignore the first point of the phi data because it is forced
477 # to go through zero due to the r*phi format in alloy and adp
478 for i in range(self.Nelements):
479 for j in range(i, self.Nelements):
480 self.phi[i, j] = spline(
481 self.r[1:],
482 self.rphi_data[i, j][1:] / self.r[1:], k=3)
484 self.d_phi[i, j] = self.deriv(self.phi[i, j])
486 if j != i:
487 self.phi[j, i] = self.phi[i, j]
488 self.d_phi[j, i] = self.d_phi[i, j]
490 def set_fs_splines(self):
491 self.embedded_energy = np.empty(self.Nelements, object)
492 self.electron_density = np.empty(
493 [self.Nelements, self.Nelements], object)
494 self.d_embedded_energy = np.empty(self.Nelements, object)
495 self.d_electron_density = np.empty(
496 [self.Nelements, self.Nelements], object)
498 for i in range(self.Nelements):
499 self.embedded_energy[i] = spline(self.rho,
500 self.embedded_data[i], k=3)
501 self.d_embedded_energy[i] = self.deriv(self.embedded_energy[i])
502 for j in range(self.Nelements):
503 self.electron_density[i, j] = spline(
504 self.r, self.density_data[i, j], k=3)
505 self.d_electron_density[i, j] = self.deriv(
506 self.electron_density[i, j])
508 self.phi = np.empty([self.Nelements, self.Nelements], object)
509 self.d_phi = np.empty([self.Nelements, self.Nelements], object)
511 for i in range(self.Nelements):
512 for j in range(i, self.Nelements):
513 self.phi[i, j] = spline(
514 self.r[1:],
515 self.rphi_data[i, j][1:] / self.r[1:], k=3)
517 self.d_phi[i, j] = self.deriv(self.phi[i, j])
519 if j != i:
520 self.phi[j, i] = self.phi[i, j]
521 self.d_phi[j, i] = self.d_phi[i, j]
523 def set_adp_splines(self):
524 self.d = np.empty([self.Nelements, self.Nelements], object)
525 self.d_d = np.empty([self.Nelements, self.Nelements], object)
526 self.q = np.empty([self.Nelements, self.Nelements], object)
527 self.d_q = np.empty([self.Nelements, self.Nelements], object)
529 for i in range(self.Nelements):
530 for j in range(i, self.Nelements):
531 self.d[i, j] = spline(self.r[1:], self.d_data[i, j][1:], k=3)
532 self.d_d[i, j] = self.deriv(self.d[i, j])
533 self.q[i, j] = spline(self.r[1:], self.q_data[i, j][1:], k=3)
534 self.d_q[i, j] = self.deriv(self.q[i, j])
536 # make symmetrical
537 if j != i:
538 self.d[j, i] = self.d[i, j]
539 self.d_d[j, i] = self.d_d[i, j]
540 self.q[j, i] = self.q[i, j]
541 self.d_q[j, i] = self.d_q[i, j]
543 def read_adp_data(self, data, d):
544 """read in the extra adp data from the potential file"""
546 self.d_data = np.zeros([self.Nelements, self.Nelements, self.nr])
547 # should be non symmetrical combinations of 2
548 for i in range(self.Nelements):
549 for j in range(i + 1):
550 self.d_data[j, i] = data[d:d + self.nr]
551 d += self.nr
553 self.q_data = np.zeros([self.Nelements, self.Nelements, self.nr])
554 # should be non symmetrical combinations of 2
555 for i in range(self.Nelements):
556 for j in range(i + 1):
557 self.q_data[j, i] = data[d:d + self.nr]
558 d += self.nr
560 def write_potential(self, filename, nc=1, numformat='%.8e'):
561 """Writes out the potential in the format given by the form
562 variable to 'filename' with a data format that is nc columns
563 wide. Note: array lengths need to be an exact multiple of nc
564 """
566 with open(filename, 'wb') as fd:
567 self._write_potential(fd, nc=nc, numformat=numformat)
569 def _write_potential(self, fd, nc, numformat):
570 assert self.nr % nc == 0
571 assert self.nrho % nc == 0
573 for line in self.header:
574 fd.write(line)
576 fd.write(f'{self.Nelements} '.encode())
577 fd.write(' '.join(self.elements).encode() + b'\n')
579 fd.write(('%d %f %d %f %f \n' %
580 (self.nrho, self.drho, self.nr,
581 self.dr, self.cutoff)).encode())
583 # start of each section for each element
584# rs = np.linspace(0, self.nr * self.dr, self.nr)
585# rhos = np.linspace(0, self.nrho * self.drho, self.nrho)
587 rs = np.arange(0, self.nr) * self.dr
588 rhos = np.arange(0, self.nrho) * self.drho
590 for i in range(self.Nelements):
591 fd.write(('%d %f %f %s\n' %
592 (self.Z[i], self.mass[i],
593 self.a[i], str(self.lattice[i]))).encode())
594 np.savetxt(fd,
595 self.embedded_energy[i](rhos).reshape(self.nrho // nc,
596 nc),
597 fmt=nc * [numformat])
598 if self.form == 'fs':
599 for j in range(self.Nelements):
600 np.savetxt(fd,
601 self.electron_density[i, j](rs).reshape(
602 self.nr // nc, nc),
603 fmt=nc * [numformat])
604 else:
605 np.savetxt(fd,
606 self.electron_density[i](rs).reshape(
607 self.nr // nc, nc),
608 fmt=nc * [numformat])
610 # write out the pair potentials in Lammps DYNAMO setfl format
611 # as r*phi for alloy format
612 for i in range(self.Nelements):
613 for j in range(i, self.Nelements):
614 np.savetxt(fd,
615 (rs * self.phi[i, j](rs)).reshape(self.nr // nc,
616 nc),
617 fmt=nc * [numformat])
619 if self.form == 'adp':
620 # these are the u(r) or dipole values
621 for i in range(self.Nelements):
622 for j in range(i + 1):
623 np.savetxt(fd, self.d_data[i, j])
625 # these are the w(r) or quadrupole values
626 for i in range(self.Nelements):
627 for j in range(i + 1):
628 np.savetxt(fd, self.q_data[i, j])
630 def update(self, atoms):
631 # check all the elements are available in the potential
632 self.Nelements = len(self.elements)
633 elements = np.unique(atoms.get_chemical_symbols())
634 unavailable = np.logical_not(
635 np.array([item in self.elements for item in elements]))
637 if np.any(unavailable):
638 raise RuntimeError(
639 f'These elements are not in the potential: {elements[unavailable]}')
641 # cutoffs need to be a vector for NeighborList
642 cutoffs = self.cutoff * np.ones(len(atoms))
644 # convert the elements to an index of the position
645 # in the eam format
646 self.index = np.array([self.elements.index(el)
647 for el in atoms.get_chemical_symbols()])
648 self.pbc = atoms.get_pbc()
650 # since we need the contribution of all neighbors to the
651 # local electron density we cannot just calculate and use
652 # one way neighbors
653 self.neighbors = NeighborList(cutoffs,
654 skin=self.parameters.skin,
655 self_interaction=False,
656 bothways=True)
657 self.neighbors.update(atoms)
659 def calculate(self, atoms=None, properties=['energy'],
660 system_changes=all_changes):
661 """EAM Calculator
663 atoms: Atoms object
664 Contains positions, unit-cell, ...
665 properties: list of str
666 List of what needs to be calculated. Can be any combination
667 of 'energy', 'forces'
668 system_changes: list of str
669 List of what has changed since last calculation. Can be
670 any combination of these five: 'positions', 'numbers', 'cell',
671 'pbc', 'initial_charges' and 'initial_magmoms'.
672 """
674 Calculator.calculate(self, atoms, properties, system_changes)
676 # we shouldn't really recalc if charges or magmos change
677 if len(system_changes) > 0: # something wrong with this way
678 self.update(self.atoms)
679 self.calculate_energy(self.atoms)
681 if 'forces' in properties:
682 self.calculate_forces(self.atoms)
684 # check we have all the properties requested
685 for property in properties:
686 if property not in self.results:
687 if property == 'energy':
688 self.calculate_energy(self.atoms)
690 if property == 'forces':
691 self.calculate_forces(self.atoms)
693 # we need to remember the previous state of parameters
694# if 'potential' in parameter_changes and potential != None:
695# self.read_potential(potential)
697 def calculate_energy(self, atoms):
698 """Calculate the energy
699 the energy is made up of the ionic or pair interaction and
700 the embedding energy of each atom into the electron cloud
701 generated by its neighbors
702 """
704 pair_energy = 0.0
705 embedding_energy = 0.0
706 mu_energy = 0.0
707 lam_energy = 0.0
708 trace_energy = 0.0
710 self.total_density = np.zeros(len(atoms))
711 if self.form == 'adp':
712 self.mu = np.zeros([len(atoms), 3])
713 self.lam = np.zeros([len(atoms), 3, 3])
715 for i in range(len(atoms)): # this is the atom to be embedded
716 neighbors, offsets = self.neighbors.get_neighbors(i)
717 offset = np.dot(offsets, atoms.get_cell())
719 rvec = (atoms.positions[neighbors] + offset -
720 atoms.positions[i])
722 # calculate the distance to the nearest neighbors
723 r = np.sqrt(np.sum(np.square(rvec), axis=1)) # fast
724# r = np.apply_along_axis(np.linalg.norm, 1, rvec) # sloow
726 nearest = np.arange(len(r))[r <= self.cutoff]
727 for j_index in range(self.Nelements):
728 use = self.index[neighbors[nearest]] == j_index
729 if not use.any():
730 continue
731 pair_energy += np.sum(self.phi[self.index[i], j_index](
732 r[nearest][use])) / 2.
734 if self.form == 'fs':
735 density = np.sum(
736 self.electron_density[j_index,
737 self.index[i]](r[nearest][use]))
738 else:
739 density = np.sum(
740 self.electron_density[j_index](r[nearest][use]))
741 self.total_density[i] += density
743 if self.form == 'adp':
744 self.mu[i] += self.adp_dipole(
745 r[nearest][use],
746 rvec[nearest][use],
747 self.d[self.index[i], j_index])
749 self.lam[i] += self.adp_quadrupole(
750 r[nearest][use],
751 rvec[nearest][use],
752 self.q[self.index[i], j_index])
754 # add in the electron embedding energy
755 embedding_energy += self.embedded_energy[self.index[i]](
756 self.total_density[i])
758 components = dict(pair=pair_energy, embedding=embedding_energy)
760 if self.form == 'adp':
761 mu_energy += np.sum(self.mu ** 2) / 2.
762 lam_energy += np.sum(self.lam ** 2) / 2.
764 for i in range(len(atoms)): # this is the atom to be embedded
765 trace_energy -= np.sum(self.lam[i].trace() ** 2) / 6.
767 adp_result = dict(adp_mu=mu_energy,
768 adp_lam=lam_energy,
769 adp_trace=trace_energy)
770 components.update(adp_result)
772 self.positions = atoms.positions.copy()
773 self.cell = atoms.get_cell().copy()
775 energy = 0.0
776 for i in components:
777 energy += components[i]
779 self.energy_free = energy
780 self.energy_zero = energy
782 self.results['energy_components'] = components
783 self.results['energy'] = energy
785 def calculate_forces(self, atoms):
786 # calculate the forces based on derivatives of the three EAM functions
788 self.update(atoms)
789 self.results['forces'] = np.zeros((len(atoms), 3))
791 for i in range(len(atoms)): # this is the atom to be embedded
792 neighbors, offsets = self.neighbors.get_neighbors(i)
793 offset = np.dot(offsets, atoms.get_cell())
794 # create a vector of relative positions of neighbors
795 rvec = atoms.positions[neighbors] + offset - atoms.positions[i]
796 r = np.sqrt(np.sum(np.square(rvec), axis=1))
797 nearest = np.arange(len(r))[r < self.cutoff]
799 d_embedded_energy_i = self.d_embedded_energy[
800 self.index[i]](self.total_density[i])
801 urvec = rvec.copy() # unit directional vector
803 for j in np.arange(len(neighbors)):
804 urvec[j] = urvec[j] / r[j]
806 for j_index in range(self.Nelements):
807 use = self.index[neighbors[nearest]] == j_index
808 if not use.any():
809 continue
810 rnuse = r[nearest][use]
811 density_j = self.total_density[neighbors[nearest][use]]
812 if self.form == 'fs':
813 scale = (self.d_phi[self.index[i], j_index](rnuse) +
814 (d_embedded_energy_i *
815 self.d_electron_density[j_index,
816 self.index[i]](rnuse)) +
817 (self.d_embedded_energy[j_index](density_j) *
818 self.d_electron_density[self.index[i],
819 j_index](rnuse)))
820 else:
821 scale = (self.d_phi[self.index[i], j_index](rnuse) +
822 (d_embedded_energy_i *
823 self.d_electron_density[j_index](rnuse)) +
824 (self.d_embedded_energy[j_index](density_j) *
825 self.d_electron_density[self.index[i]](rnuse)))
827 self.results['forces'][i] += np.dot(scale, urvec[nearest][use])
829 if self.form == 'adp':
830 adp_forces = self.angular_forces(
831 self.mu[i],
832 self.mu[neighbors[nearest][use]],
833 self.lam[i],
834 self.lam[neighbors[nearest][use]],
835 rnuse,
836 rvec[nearest][use],
837 self.index[i],
838 j_index)
840 self.results['forces'][i] += adp_forces
842 def angular_forces(self, mu_i, mu, lam_i, lam, r, rvec, form1, form2):
843 # calculate the extra components for the adp forces
844 # rvec are the relative positions to atom i
845 psi = np.zeros(mu.shape)
846 for gamma in range(3):
847 term1 = (mu_i[gamma] - mu[:, gamma]) * self.d[form1][form2](r)
849 term2 = np.sum((mu_i - mu) *
850 self.d_d[form1][form2](r)[:, np.newaxis] *
851 (rvec * rvec[:, gamma][:, np.newaxis] /
852 r[:, np.newaxis]), axis=1)
854 term3 = 2 * np.sum((lam_i[:, gamma] + lam[:, :, gamma]) *
855 rvec * self.q[form1][form2](r)[:, np.newaxis],
856 axis=1)
857 term4 = 0.0
858 for alpha in range(3):
859 for beta in range(3):
860 rs = rvec[:, alpha] * rvec[:, beta] * rvec[:, gamma]
861 term4 += ((lam_i[alpha, beta] + lam[:, alpha, beta]) *
862 self.d_q[form1][form2](r) * rs) / r
864 term5 = ((lam_i.trace() + lam.trace(axis1=1, axis2=2)) *
865 (self.d_q[form1][form2](r) * r +
866 2 * self.q[form1][form2](r)) * rvec[:, gamma]) / 3.
868 # the minus for term5 is a correction on the adp
869 # formulation given in the 2005 Mishin Paper and is posted
870 # on the NIST website with the AlH potential
871 psi[:, gamma] = term1 + term2 + term3 + term4 - term5
873 return np.sum(psi, axis=0)
875 def adp_dipole(self, r, rvec, d):
876 # calculate the dipole contribution
877 mu = np.sum((rvec * d(r)[:, np.newaxis]), axis=0)
879 return mu # sign to agree with lammps
881 def adp_quadrupole(self, r, rvec, q):
882 # slow way of calculating the quadrupole contribution
883 r = np.sqrt(np.sum(rvec ** 2, axis=1))
885 lam = np.zeros([rvec.shape[0], 3, 3])
886 qr = q(r)
887 for alpha in range(3):
888 for beta in range(3):
889 lam[:, alpha, beta] += qr * rvec[:, alpha] * rvec[:, beta]
891 return np.sum(lam, axis=0)
893 def deriv(self, spline):
894 """Wrapper for extracting the derivative from a spline"""
895 def d_spline(aspline):
896 return spline(aspline, 1)
898 return d_spline
900 def plot(self, name=''):
901 """Plot the individual curves"""
903 import matplotlib.pyplot as plt
905 if self.form == 'eam' or self.form == 'alloy' or self.form == 'fs':
906 nrow = 2
907 elif self.form == 'adp':
908 nrow = 3
909 else:
910 raise RuntimeError(f'Unknown form of potential: {self.form}')
912 if hasattr(self, 'r'):
913 r = self.r
914 else:
915 r = np.linspace(0, self.cutoff, 50)
917 if hasattr(self, 'rho'):
918 rho = self.rho
919 else:
920 rho = np.linspace(0, 10.0, 50)
922 plt.subplot(nrow, 2, 1)
923 self.elem_subplot(rho, self.embedded_energy,
924 r'$\rho$', r'Embedding Energy $F(\bar\rho)$',
925 name, plt)
927 plt.subplot(nrow, 2, 2)
928 if self.form == 'fs':
929 self.multielem_subplot(
930 r, self.electron_density,
931 r'$r$', r'Electron Density $\rho(r)$', name, plt, half=False)
932 else:
933 self.elem_subplot(
934 r, self.electron_density,
935 r'$r$', r'Electron Density $\rho(r)$', name, plt)
937 plt.subplot(nrow, 2, 3)
938 self.multielem_subplot(r, self.phi,
939 r'$r$', r'Pair Potential $\phi(r)$', name, plt)
940 plt.ylim(-1.0, 1.0) # need reasonable values
942 if self.form == 'adp':
943 plt.subplot(nrow, 2, 5)
944 self.multielem_subplot(r, self.d,
945 r'$r$', r'Dipole Energy', name, plt)
947 plt.subplot(nrow, 2, 6)
948 self.multielem_subplot(r, self.q,
949 r'$r$', r'Quadrupole Energy', name, plt)
951 plt.plot()
953 def elem_subplot(self, curvex, curvey, xlabel, ylabel, name, plt):
954 plt.xlabel(xlabel)
955 plt.ylabel(ylabel)
956 for i in np.arange(self.Nelements):
957 label = name + ' ' + self.elements[i]
958 plt.plot(curvex, curvey[i](curvex), label=label)
959 plt.legend()
961 def multielem_subplot(self, curvex, curvey, xlabel,
962 ylabel, name, plt, half=True):
963 plt.xlabel(xlabel)
964 plt.ylabel(ylabel)
965 for i in np.arange(self.Nelements):
966 for j in np.arange((i + 1) if half else self.Nelements):
967 label = name + ' ' + self.elements[i] + '-' + self.elements[j]
968 plt.plot(curvex, curvey[i, j](curvex), label=label)
969 plt.legend()