Coverage for /builds/debichem-team/python-ase/ase/calculators/eam.py: 82.84%
408 statements
« prev ^ index » next coverage.py v7.5.3, created at 2025-03-06 04:00 +0000
« prev ^ index » next coverage.py v7.5.3, created at 2025-03-06 04:00 +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.data import chemical_symbols
18from ase.neighborlist import NeighborList
19from ase.units import Bohr, Hartree
22class EAM(Calculator):
23 r"""
25 EAM Interface Documentation
27Introduction
28============
30The Embedded Atom Method (EAM) [1]_ is a classical potential which is
31good for modelling metals, particularly fcc materials. Because it is
32an equiaxial potential the EAM does not model directional bonds
33well. However, the Angular Dependent Potential (ADP) [2]_ which is an
34extended version of EAM is able to model directional bonds and is also
35included in the EAM calculator.
37Generally all that is required to use this calculator is to supply a
38potential file or as a set of functions that describe the potential.
39The files containing the potentials for this calculator are not
40included but many suitable potentials can be downloaded from The
41Interatomic Potentials Repository Project at
42https://www.ctcms.nist.gov/potentials/
44Theory
45======
47A single element EAM potential is defined by three functions: the
48embedded energy, electron density and the pair potential. A two
49element alloy contains the individual three functions for each element
50plus cross pair interactions. The ADP potential has two additional
51sets of data to define the dipole and quadrupole directional terms for
52each alloy and their cross interactions.
54The total energy `E_{\rm tot}` of an arbitrary arrangement of atoms is
55given by the EAM potential as
57.. math::
58 E_\text{tot} = \sum_i F(\bar\rho_i) + \frac{1}{2}\sum_{i\ne j} \phi(r_{ij})
60and
62.. math::
63 \bar\rho_i = \sum_j \rho(r_{ij})
65where `F` is an embedding function, namely the energy to embed an atom `i` in
66the combined electron density `\bar\rho_i` which is contributed from
67each of its neighbouring atoms `j` by an amount `\rho(r_{ij})`,
68`\phi(r_{ij})` is the pair potential function representing the energy
69in bond `ij` which is due to the short-range electro-static
70interaction between atoms, and `r_{ij}` is the distance between an
71atom and its neighbour for that bond.
73The ADP potential is defined as
75.. math::
76 E_\text{tot} = \sum_i F(\bar\rho_i) + \frac{1}{2}\sum_{i\ne j} \phi(r_{ij})
77 + \frac{1}{2} \sum_{i,\alpha} (\mu_i^\alpha)^2
78 + \frac{1}{2} \sum_{i,\alpha,\beta} (\lambda_i^{\alpha\beta})^2
79 - \frac{1}{6} \sum_i \nu_i^2
81where `\mu_i^\alpha` is the dipole vector, `\lambda_i^{\alpha\beta}`
82is the quadrupole tensor and `\nu_i` is the trace of
83`\lambda_i^{\alpha\beta}`.
85The fs potential is defined as
87.. math::
88 E_i = F_\alpha (\sum_{j\neq i} \rho_{\alpha \beta}(r_{ij}))
89 + \frac{1}{2}\sum_{j\neq i}\phi_{\alpha \beta}(r_{ij})
91where `\alpha` and `\beta` are element types of atoms. This form is similar to
92original EAM formula above, except that `\rho` and `\phi` are determined
93by element types.
95Running the Calculator
96======================
98EAM calculates the cohesive atom energy and forces. Internally the
99potential functions are defined by splines which may be directly
100supplied or created by reading the spline points from a data file from
101which a spline function is created. The LAMMPS compatible ``.alloy``, ``.fs``
102and ``.adp`` formats are supported. The LAMMPS ``.eam`` format is
103slightly different from the ``.alloy`` format and is currently not
104supported.
106For example::
108 from ase.calculators.eam import EAM
110 mishin = EAM(potential='Al99.eam.alloy')
111 mishin.write_potential('new.eam.alloy')
112 mishin.plot()
114 slab.calc = mishin
115 slab.get_potential_energy()
116 slab.get_forces()
118The breakdown of energy contribution from the indvidual components are
119stored in the calculator instance ``.results['energy_components']``
121Arguments
122=========
124========================= ====================================================
125Keyword Description
126========================= ====================================================
127``potential`` file of potential in ``.eam``, ``.alloy``, ``.adp`` or ``.fs``
128 format or file object
129 (This is generally all you need to supply).
130 For file object the ``form`` argument is required
132``elements[N]`` array of N element abbreviations
134``embedded_energy[N]`` arrays of embedded energy functions
136``electron_density[N]`` arrays of electron density functions
138``phi[N,N]`` arrays of pair potential functions
140``d_embedded_energy[N]`` arrays of derivative embedded energy functions
142``d_electron_density[N]`` arrays of derivative electron density functions
144``d_phi[N,N]`` arrays of derivative pair potentials functions
146``d[N,N], q[N,N]`` ADP dipole and quadrupole function
148``d_d[N,N], d_q[N,N]`` ADP dipole and quadrupole derivative functions
150``skin`` skin distance passed to NeighborList(). If no atom
151 has moved more than the skin-distance since the last
152 call to the ``update()`` method then the neighbor
153 list can be reused. Defaults to 1.0.
155``form`` the form of the potential
156 ``eam``, ``alloy``, ``adp`` or
157 ``fs``. This will be determined from the file suffix
158 or must be set if using equations or file object
160========================= ====================================================
163Additional parameters for writing potential files
164=================================================
166The following parameters are only required for writing a potential in
167``.alloy``, ``.adp`` or ``fs`` format file.
169========================= ====================================================
170Keyword Description
171========================= ====================================================
172``header`` Three line text header. Default is standard message.
174``Z[N]`` Array of atomic number of each element
176``mass[N]`` Atomic mass of each element
178``a[N]`` Array of lattice parameters for each element
180``lattice[N]`` Lattice type
182``nrho`` No. of rho samples along embedded energy curve
184``drho`` Increment for sampling density
186``nr`` No. of radial points along density and pair
187 potential curves
189``dr`` Increment for sampling radius
191========================= ====================================================
193Special features
194================
196``.plot()``
197 Plots the individual functions. This may be called from multiple EAM
198 potentials to compare the shape of the individual curves. This
199 function requires the installation of the Matplotlib libraries.
201Notes/Issues
202=============
204* Although currently not fast, this calculator can be good for trying
205 small calculations or for creating new potentials by matching baseline
206 data such as from DFT results. The format for these potentials is
207 compatible with LAMMPS_ and so can be used either directly by LAMMPS or
208 with the ASE LAMMPS calculator interface.
210* Supported formats are the LAMMPS_ ``.alloy`` and ``.adp``. The
211 ``.eam`` format is currently not supported. The form of the
212 potential will be determined from the file suffix.
214* Any supplied values will override values read from the file.
216* The derivative functions, if supplied, are only used to calculate
217 forces.
219* There is a bug in early versions of scipy that will cause eam.py to
220 crash when trying to evaluate splines of a potential with one
221 neighbor such as caused by evaluating a dimer.
223.. _LAMMPS: http://lammps.sandia.gov/
225.. [1] M.S. Daw and M.I. Baskes, Phys. Rev. Letters 50 (1983)
226 1285.
228.. [2] Y. Mishin, M.J. Mehl, and D.A. Papaconstantopoulos,
229 Acta Materialia 53 2005 4029--4041.
232End EAM Interface Documentation
233 """
235 implemented_properties = ['energy', 'forces']
237 default_parameters = dict(
238 skin=1.0,
239 potential=None,
240 header=[b'EAM/ADP potential file\n',
241 b'Generated from eam.py\n',
242 b'blank\n'])
244 def __init__(self, restart=None,
245 ignore_bad_restart_file=Calculator._deprecated,
246 label=os.curdir, atoms=None, form=None, **kwargs):
248 self.form = form
250 if 'potential' in kwargs:
251 self.read_potential(kwargs['potential'])
253 Calculator.__init__(self, restart, ignore_bad_restart_file,
254 label, atoms, **kwargs)
256 valid_args = ('potential', 'elements', 'header', 'drho', 'dr',
257 'cutoff', 'atomic_number', 'mass', 'a', 'lattice',
258 'embedded_energy', 'electron_density', 'phi',
259 # derivatives
260 'd_embedded_energy', 'd_electron_density', 'd_phi',
261 'd', 'q', 'd_d', 'd_q', # adp terms
262 'skin', 'Z', 'nr', 'nrho', 'mass')
264 # set any additional keyword arguments
265 for arg, val in self.parameters.items():
266 if arg in valid_args:
267 setattr(self, arg, val)
268 else:
269 raise RuntimeError(
270 f'unknown keyword arg "{arg}" : not in {valid_args}')
272 def set_form(self, name):
273 """set the form variable based on the file name suffix"""
274 extension = os.path.splitext(name)[1]
276 if extension == '.eam':
277 self.form = 'eam'
278 elif extension == '.alloy':
279 self.form = 'alloy'
280 elif extension == '.adp':
281 self.form = 'adp'
282 elif extension == '.fs':
283 self.form = 'fs'
284 else:
285 raise RuntimeError(f'unknown file extension type: {extension}')
287 def read_potential(self, filename):
288 """Reads a LAMMPS EAM file in alloy or adp format
289 and creates the interpolation functions from the data
290 """
292 if isinstance(filename, str):
293 with open(filename) as fd:
294 self._read_potential(fd)
295 else:
296 fd = filename
297 self._read_potential(fd)
299 def _read_potential(self, fd):
300 if self.form is None:
301 self.set_form(fd.name)
303 lines = fd.readlines()
305 def lines_to_list(lines):
306 """Make the data one long line so as not to care how its formatted
307 """
308 data = []
309 for line in lines:
310 data.extend(line.split())
311 return data
313 if self.form == 'eam': # single element eam file (aka funcfl)
314 self.header = lines[:1]
316 data = lines_to_list(lines[1:])
318 # eam form is just like an alloy form for one element
320 self.Nelements = 1
321 self.elements = [chemical_symbols[int(data[0])]]
322 self.Z = np.array([data[0]], dtype=int)
323 self.mass = np.array([data[1]])
324 self.a = np.array([data[2]])
325 self.lattice = [data[3]]
327 self.nrho = int(data[4])
328 self.drho = float(data[5])
329 self.nr = int(data[6])
330 self.dr = float(data[7])
331 self.cutoff = float(data[8])
333 n = 9 + self.nrho
334 self.embedded_data = np.array([np.float64(data[9:n])])
336 self.rphi_data = np.zeros([self.Nelements, self.Nelements,
337 self.nr])
339 effective_charge = np.float64(data[n:n + self.nr])
340 # convert effective charges to rphi according to
341 # http://lammps.sandia.gov/doc/pair_eam.html
342 self.rphi_data[0, 0] = Bohr * Hartree * (effective_charge**2)
344 self.density_data = np.array(
345 [np.float64(data[n + self.nr:n + 2 * self.nr])])
347 elif self.form in ['alloy', 'adp']:
348 self.header = lines[:3]
349 i = 3
351 data = lines_to_list(lines[i:])
353 self.Nelements = int(data[0])
354 d = 1
355 self.elements = data[d:d + self.Nelements]
356 d += self.Nelements
358 self.nrho = int(data[d])
359 self.drho = float(data[d + 1])
360 self.nr = int(data[d + 2])
361 self.dr = float(data[d + 3])
362 self.cutoff = float(data[d + 4])
364 self.embedded_data = np.zeros([self.Nelements, self.nrho])
365 self.density_data = np.zeros([self.Nelements, self.nr])
366 self.Z = np.zeros([self.Nelements], dtype=int)
367 self.mass = np.zeros([self.Nelements])
368 self.a = np.zeros([self.Nelements])
369 self.lattice = []
370 d += 5
372 # reads in the part of the eam file for each element
373 for elem in range(self.Nelements):
374 self.Z[elem] = int(data[d])
375 self.mass[elem] = float(data[d + 1])
376 self.a[elem] = float(data[d + 2])
377 self.lattice.append(data[d + 3])
378 d += 4
380 self.embedded_data[elem] = np.float64(
381 data[d:(d + self.nrho)])
382 d += self.nrho
383 self.density_data[elem] = np.float64(data[d:(d + self.nr)])
384 d += self.nr
386 # reads in the r*phi data for each interaction between elements
387 self.rphi_data = np.zeros([self.Nelements, self.Nelements,
388 self.nr])
390 for i in range(self.Nelements):
391 for j in range(i + 1):
392 self.rphi_data[j, i] = np.float64(data[d:(d + self.nr)])
393 d += self.nr
395 elif self.form == 'fs':
396 self.header = lines[:3]
397 i = 3
399 data = lines_to_list(lines[i:])
401 self.Nelements = int(data[0])
402 d = 1
403 self.elements = data[d:d + self.Nelements]
404 d += self.Nelements
406 self.nrho = int(data[d])
407 self.drho = float(data[d + 1])
408 self.nr = int(data[d + 2])
409 self.dr = float(data[d + 3])
410 self.cutoff = float(data[d + 4])
412 self.embedded_data = np.zeros([self.Nelements, self.nrho])
413 self.density_data = np.zeros([self.Nelements, self.Nelements,
414 self.nr])
415 self.Z = np.zeros([self.Nelements], dtype=int)
416 self.mass = np.zeros([self.Nelements])
417 self.a = np.zeros([self.Nelements])
418 self.lattice = []
419 d += 5
421 # reads in the part of the eam file for each element
422 for elem in range(self.Nelements):
423 self.Z[elem] = int(data[d])
424 self.mass[elem] = float(data[d + 1])
425 self.a[elem] = float(data[d + 2])
426 self.lattice.append(data[d + 3])
427 d += 4
429 self.embedded_data[elem] = np.float64(
430 data[d:(d + self.nrho)])
431 d += self.nrho
432 self.density_data[elem, :, :] = np.float64(
433 data[d:(d + self.nr * self.Nelements)]).reshape([
434 self.Nelements, self.nr])
435 d += self.nr * self.Nelements
437 # reads in the r*phi data for each interaction between elements
438 self.rphi_data = np.zeros([self.Nelements, self.Nelements,
439 self.nr])
441 for i in range(self.Nelements):
442 for j in range(i + 1):
443 self.rphi_data[j, i] = np.float64(data[d:(d + self.nr)])
444 d += self.nr
446 self.r = np.arange(0, self.nr) * self.dr
447 self.rho = np.arange(0, self.nrho) * self.drho
449 # choose the set_splines method according to the type
450 if self.form == 'fs':
451 self.set_fs_splines()
452 else:
453 self.set_splines()
455 if self.form == 'adp':
456 self.read_adp_data(data, d)
457 self.set_adp_splines()
459 def set_splines(self):
460 # this section turns the file data into three functions (and
461 # derivative functions) that define the potential
462 self.embedded_energy = np.empty(self.Nelements, object)
463 self.electron_density = np.empty(self.Nelements, object)
464 self.d_embedded_energy = np.empty(self.Nelements, object)
465 self.d_electron_density = np.empty(self.Nelements, object)
467 for i in range(self.Nelements):
468 self.embedded_energy[i] = spline(self.rho,
469 self.embedded_data[i], k=3)
470 self.electron_density[i] = spline(self.r,
471 self.density_data[i], k=3)
472 self.d_embedded_energy[i] = self.deriv(self.embedded_energy[i])
473 self.d_electron_density[i] = self.deriv(self.electron_density[i])
475 self.phi = np.empty([self.Nelements, self.Nelements], object)
476 self.d_phi = np.empty([self.Nelements, self.Nelements], object)
478 # ignore the first point of the phi data because it is forced
479 # to go through zero due to the r*phi format in alloy and adp
480 for i in range(self.Nelements):
481 for j in range(i, self.Nelements):
482 self.phi[i, j] = spline(
483 self.r[1:],
484 self.rphi_data[i, j][1:] / self.r[1:], k=3)
486 self.d_phi[i, j] = self.deriv(self.phi[i, j])
488 if j != i:
489 self.phi[j, i] = self.phi[i, j]
490 self.d_phi[j, i] = self.d_phi[i, j]
492 def set_fs_splines(self):
493 self.embedded_energy = np.empty(self.Nelements, object)
494 self.electron_density = np.empty(
495 [self.Nelements, self.Nelements], object)
496 self.d_embedded_energy = np.empty(self.Nelements, object)
497 self.d_electron_density = np.empty(
498 [self.Nelements, self.Nelements], object)
500 for i in range(self.Nelements):
501 self.embedded_energy[i] = spline(self.rho,
502 self.embedded_data[i], k=3)
503 self.d_embedded_energy[i] = self.deriv(self.embedded_energy[i])
504 for j in range(self.Nelements):
505 self.electron_density[i, j] = spline(
506 self.r, self.density_data[i, j], k=3)
507 self.d_electron_density[i, j] = self.deriv(
508 self.electron_density[i, j])
510 self.phi = np.empty([self.Nelements, self.Nelements], object)
511 self.d_phi = np.empty([self.Nelements, self.Nelements], object)
513 for i in range(self.Nelements):
514 for j in range(i, self.Nelements):
515 self.phi[i, j] = spline(
516 self.r[1:],
517 self.rphi_data[i, j][1:] / self.r[1:], k=3)
519 self.d_phi[i, j] = self.deriv(self.phi[i, j])
521 if j != i:
522 self.phi[j, i] = self.phi[i, j]
523 self.d_phi[j, i] = self.d_phi[i, j]
525 def set_adp_splines(self):
526 self.d = np.empty([self.Nelements, self.Nelements], object)
527 self.d_d = np.empty([self.Nelements, self.Nelements], object)
528 self.q = np.empty([self.Nelements, self.Nelements], object)
529 self.d_q = np.empty([self.Nelements, self.Nelements], object)
531 for i in range(self.Nelements):
532 for j in range(i, self.Nelements):
533 self.d[i, j] = spline(self.r[1:], self.d_data[i, j][1:], k=3)
534 self.d_d[i, j] = self.deriv(self.d[i, j])
535 self.q[i, j] = spline(self.r[1:], self.q_data[i, j][1:], k=3)
536 self.d_q[i, j] = self.deriv(self.q[i, j])
538 # make symmetrical
539 if j != i:
540 self.d[j, i] = self.d[i, j]
541 self.d_d[j, i] = self.d_d[i, j]
542 self.q[j, i] = self.q[i, j]
543 self.d_q[j, i] = self.d_q[i, j]
545 def read_adp_data(self, data, d):
546 """read in the extra adp data from the potential file"""
548 self.d_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.d_data[j, i] = data[d:d + self.nr]
553 d += self.nr
555 self.q_data = np.zeros([self.Nelements, self.Nelements, self.nr])
556 # should be non symmetrical combinations of 2
557 for i in range(self.Nelements):
558 for j in range(i + 1):
559 self.q_data[j, i] = data[d:d + self.nr]
560 d += self.nr
562 def write_potential(self, filename, nc=1, numformat='%.8e'):
563 """Writes out the potential in the format given by the form
564 variable to 'filename' with a data format that is nc columns
565 wide. Note: array lengths need to be an exact multiple of nc
566 """
568 with open(filename, 'wb') as fd:
569 self._write_potential(fd, nc=nc, numformat=numformat)
571 def _write_potential(self, fd, nc, numformat):
572 assert self.nr % nc == 0
573 assert self.nrho % nc == 0
575 for line in self.header:
576 fd.write(line)
578 fd.write(f'{self.Nelements} '.encode())
579 fd.write(' '.join(self.elements).encode() + b'\n')
581 fd.write(('%d %f %d %f %f \n' %
582 (self.nrho, self.drho, self.nr,
583 self.dr, self.cutoff)).encode())
585 # start of each section for each element
586# rs = np.linspace(0, self.nr * self.dr, self.nr)
587# rhos = np.linspace(0, self.nrho * self.drho, self.nrho)
589 rs = np.arange(0, self.nr) * self.dr
590 rhos = np.arange(0, self.nrho) * self.drho
592 for i in range(self.Nelements):
593 fd.write(('%d %f %f %s\n' %
594 (self.Z[i], self.mass[i],
595 self.a[i], str(self.lattice[i]))).encode())
596 np.savetxt(fd,
597 self.embedded_energy[i](rhos).reshape(self.nrho // nc,
598 nc),
599 fmt=nc * [numformat])
600 if self.form == 'fs':
601 for j in range(self.Nelements):
602 np.savetxt(fd,
603 self.electron_density[i, j](rs).reshape(
604 self.nr // nc, nc),
605 fmt=nc * [numformat])
606 else:
607 np.savetxt(fd,
608 self.electron_density[i](rs).reshape(
609 self.nr // nc, nc),
610 fmt=nc * [numformat])
612 # write out the pair potentials in Lammps DYNAMO setfl format
613 # as r*phi for alloy format
614 for i in range(self.Nelements):
615 for j in range(i, self.Nelements):
616 np.savetxt(fd,
617 (rs * self.phi[i, j](rs)).reshape(self.nr // nc,
618 nc),
619 fmt=nc * [numformat])
621 if self.form == 'adp':
622 # these are the u(r) or dipole values
623 for i in range(self.Nelements):
624 for j in range(i + 1):
625 np.savetxt(fd, self.d_data[i, j])
627 # these are the w(r) or quadrupole values
628 for i in range(self.Nelements):
629 for j in range(i + 1):
630 np.savetxt(fd, self.q_data[i, j])
632 def update(self, atoms):
633 # check all the elements are available in the potential
634 self.Nelements = len(self.elements)
635 elements = np.unique(atoms.get_chemical_symbols())
636 unavailable = np.logical_not(
637 np.array([item in self.elements for item in elements]))
639 if np.any(unavailable):
640 raise RuntimeError(
641 f'These elements are not in the potential: {elements[unavailable]}')
643 # cutoffs need to be a vector for NeighborList
644 cutoffs = self.cutoff * np.ones(len(atoms))
646 # convert the elements to an index of the position
647 # in the eam format
648 self.index = np.array([self.elements.index(el)
649 for el in atoms.get_chemical_symbols()])
650 self.pbc = atoms.get_pbc()
652 # since we need the contribution of all neighbors to the
653 # local electron density we cannot just calculate and use
654 # one way neighbors
655 self.neighbors = NeighborList(cutoffs,
656 skin=self.parameters.skin,
657 self_interaction=False,
658 bothways=True)
659 self.neighbors.update(atoms)
661 def calculate(self, atoms=None, properties=['energy'],
662 system_changes=all_changes):
663 """EAM Calculator
665 atoms: Atoms object
666 Contains positions, unit-cell, ...
667 properties: list of str
668 List of what needs to be calculated. Can be any combination
669 of 'energy', 'forces'
670 system_changes: list of str
671 List of what has changed since last calculation. Can be
672 any combination of these five: 'positions', 'numbers', 'cell',
673 'pbc', 'initial_charges' and 'initial_magmoms'.
674 """
676 Calculator.calculate(self, atoms, properties, system_changes)
678 # we shouldn't really recalc if charges or magmos change
679 if len(system_changes) > 0: # something wrong with this way
680 self.update(self.atoms)
681 self.calculate_energy(self.atoms)
683 if 'forces' in properties:
684 self.calculate_forces(self.atoms)
686 # check we have all the properties requested
687 for property in properties:
688 if property not in self.results:
689 if property == 'energy':
690 self.calculate_energy(self.atoms)
692 if property == 'forces':
693 self.calculate_forces(self.atoms)
695 # we need to remember the previous state of parameters
696# if 'potential' in parameter_changes and potential != None:
697# self.read_potential(potential)
699 def calculate_energy(self, atoms):
700 """Calculate the energy
701 the energy is made up of the ionic or pair interaction and
702 the embedding energy of each atom into the electron cloud
703 generated by its neighbors
704 """
706 pair_energy = 0.0
707 embedding_energy = 0.0
708 mu_energy = 0.0
709 lam_energy = 0.0
710 trace_energy = 0.0
712 self.total_density = np.zeros(len(atoms))
713 if self.form == 'adp':
714 self.mu = np.zeros([len(atoms), 3])
715 self.lam = np.zeros([len(atoms), 3, 3])
717 for i in range(len(atoms)): # this is the atom to be embedded
718 neighbors, offsets = self.neighbors.get_neighbors(i)
719 offset = np.dot(offsets, atoms.get_cell())
721 rvec = (atoms.positions[neighbors] + offset -
722 atoms.positions[i])
724 # calculate the distance to the nearest neighbors
725 r = np.sqrt(np.sum(np.square(rvec), axis=1)) # fast
726# r = np.apply_along_axis(np.linalg.norm, 1, rvec) # sloow
728 nearest = np.arange(len(r))[r <= self.cutoff]
729 for j_index in range(self.Nelements):
730 use = self.index[neighbors[nearest]] == j_index
731 if not use.any():
732 continue
733 pair_energy += np.sum(self.phi[self.index[i], j_index](
734 r[nearest][use])) / 2.
736 if self.form == 'fs':
737 density = np.sum(
738 self.electron_density[j_index,
739 self.index[i]](r[nearest][use]))
740 else:
741 density = np.sum(
742 self.electron_density[j_index](r[nearest][use]))
743 self.total_density[i] += density
745 if self.form == 'adp':
746 self.mu[i] += self.adp_dipole(
747 r[nearest][use],
748 rvec[nearest][use],
749 self.d[self.index[i], j_index])
751 self.lam[i] += self.adp_quadrupole(
752 r[nearest][use],
753 rvec[nearest][use],
754 self.q[self.index[i], j_index])
756 # add in the electron embedding energy
757 embedding_energy += self.embedded_energy[self.index[i]](
758 self.total_density[i])
760 components = dict(pair=pair_energy, embedding=embedding_energy)
762 if self.form == 'adp':
763 mu_energy += np.sum(self.mu ** 2) / 2.
764 lam_energy += np.sum(self.lam ** 2) / 2.
766 for i in range(len(atoms)): # this is the atom to be embedded
767 trace_energy -= np.sum(self.lam[i].trace() ** 2) / 6.
769 adp_result = dict(adp_mu=mu_energy,
770 adp_lam=lam_energy,
771 adp_trace=trace_energy)
772 components.update(adp_result)
774 self.positions = atoms.positions.copy()
775 self.cell = atoms.get_cell().copy()
777 energy = 0.0
778 for i in components:
779 energy += components[i]
781 self.energy_free = energy
782 self.energy_zero = energy
784 self.results['energy_components'] = components
785 self.results['energy'] = energy
787 def calculate_forces(self, atoms):
788 # calculate the forces based on derivatives of the three EAM functions
790 self.update(atoms)
791 self.results['forces'] = np.zeros((len(atoms), 3))
793 for i in range(len(atoms)): # this is the atom to be embedded
794 neighbors, offsets = self.neighbors.get_neighbors(i)
795 offset = np.dot(offsets, atoms.get_cell())
796 # create a vector of relative positions of neighbors
797 rvec = atoms.positions[neighbors] + offset - atoms.positions[i]
798 r = np.sqrt(np.sum(np.square(rvec), axis=1))
799 nearest = np.arange(len(r))[r < self.cutoff]
801 d_embedded_energy_i = self.d_embedded_energy[
802 self.index[i]](self.total_density[i])
803 urvec = rvec.copy() # unit directional vector
805 for j in np.arange(len(neighbors)):
806 urvec[j] = urvec[j] / r[j]
808 for j_index in range(self.Nelements):
809 use = self.index[neighbors[nearest]] == j_index
810 if not use.any():
811 continue
812 rnuse = r[nearest][use]
813 density_j = self.total_density[neighbors[nearest][use]]
814 if self.form == 'fs':
815 scale = (self.d_phi[self.index[i], j_index](rnuse) +
816 (d_embedded_energy_i *
817 self.d_electron_density[j_index,
818 self.index[i]](rnuse)) +
819 (self.d_embedded_energy[j_index](density_j) *
820 self.d_electron_density[self.index[i],
821 j_index](rnuse)))
822 else:
823 scale = (self.d_phi[self.index[i], j_index](rnuse) +
824 (d_embedded_energy_i *
825 self.d_electron_density[j_index](rnuse)) +
826 (self.d_embedded_energy[j_index](density_j) *
827 self.d_electron_density[self.index[i]](rnuse)))
829 self.results['forces'][i] += np.dot(scale, urvec[nearest][use])
831 if self.form == 'adp':
832 adp_forces = self.angular_forces(
833 self.mu[i],
834 self.mu[neighbors[nearest][use]],
835 self.lam[i],
836 self.lam[neighbors[nearest][use]],
837 rnuse,
838 rvec[nearest][use],
839 self.index[i],
840 j_index)
842 self.results['forces'][i] += adp_forces
844 def angular_forces(self, mu_i, mu, lam_i, lam, r, rvec, form1, form2):
845 # calculate the extra components for the adp forces
846 # rvec are the relative positions to atom i
847 psi = np.zeros(mu.shape)
848 for gamma in range(3):
849 term1 = (mu_i[gamma] - mu[:, gamma]) * self.d[form1][form2](r)
851 term2 = np.sum((mu_i - mu) *
852 self.d_d[form1][form2](r)[:, np.newaxis] *
853 (rvec * rvec[:, gamma][:, np.newaxis] /
854 r[:, np.newaxis]), axis=1)
856 term3 = 2 * np.sum((lam_i[:, gamma] + lam[:, :, gamma]) *
857 rvec * self.q[form1][form2](r)[:, np.newaxis],
858 axis=1)
859 term4 = 0.0
860 for alpha in range(3):
861 for beta in range(3):
862 rs = rvec[:, alpha] * rvec[:, beta] * rvec[:, gamma]
863 term4 += ((lam_i[alpha, beta] + lam[:, alpha, beta]) *
864 self.d_q[form1][form2](r) * rs) / r
866 term5 = ((lam_i.trace() + lam.trace(axis1=1, axis2=2)) *
867 (self.d_q[form1][form2](r) * r +
868 2 * self.q[form1][form2](r)) * rvec[:, gamma]) / 3.
870 # the minus for term5 is a correction on the adp
871 # formulation given in the 2005 Mishin Paper and is posted
872 # on the NIST website with the AlH potential
873 psi[:, gamma] = term1 + term2 + term3 + term4 - term5
875 return np.sum(psi, axis=0)
877 def adp_dipole(self, r, rvec, d):
878 # calculate the dipole contribution
879 mu = np.sum((rvec * d(r)[:, np.newaxis]), axis=0)
881 return mu # sign to agree with lammps
883 def adp_quadrupole(self, r, rvec, q):
884 # slow way of calculating the quadrupole contribution
885 r = np.sqrt(np.sum(rvec ** 2, axis=1))
887 lam = np.zeros([rvec.shape[0], 3, 3])
888 qr = q(r)
889 for alpha in range(3):
890 for beta in range(3):
891 lam[:, alpha, beta] += qr * rvec[:, alpha] * rvec[:, beta]
893 return np.sum(lam, axis=0)
895 def deriv(self, spline):
896 """Wrapper for extracting the derivative from a spline"""
897 def d_spline(aspline):
898 return spline(aspline, 1)
900 return d_spline
902 def plot(self, name=''):
903 """Plot the individual curves"""
905 import matplotlib.pyplot as plt
907 if self.form == 'eam' or self.form == 'alloy' or self.form == 'fs':
908 nrow = 2
909 elif self.form == 'adp':
910 nrow = 3
911 else:
912 raise RuntimeError(f'Unknown form of potential: {self.form}')
914 if hasattr(self, 'r'):
915 r = self.r
916 else:
917 r = np.linspace(0, self.cutoff, 50)
919 if hasattr(self, 'rho'):
920 rho = self.rho
921 else:
922 rho = np.linspace(0, 10.0, 50)
924 plt.subplot(nrow, 2, 1)
925 self.elem_subplot(rho, self.embedded_energy,
926 r'$\rho$', r'Embedding Energy $F(\bar\rho)$',
927 name, plt)
929 plt.subplot(nrow, 2, 2)
930 if self.form == 'fs':
931 self.multielem_subplot(
932 r, self.electron_density,
933 r'$r$', r'Electron Density $\rho(r)$', name, plt, half=False)
934 else:
935 self.elem_subplot(
936 r, self.electron_density,
937 r'$r$', r'Electron Density $\rho(r)$', name, plt)
939 plt.subplot(nrow, 2, 3)
940 self.multielem_subplot(r, self.phi,
941 r'$r$', r'Pair Potential $\phi(r)$', name, plt)
942 plt.ylim(-1.0, 1.0) # need reasonable values
944 if self.form == 'adp':
945 plt.subplot(nrow, 2, 5)
946 self.multielem_subplot(r, self.d,
947 r'$r$', r'Dipole Energy', name, plt)
949 plt.subplot(nrow, 2, 6)
950 self.multielem_subplot(r, self.q,
951 r'$r$', r'Quadrupole Energy', name, plt)
953 plt.plot()
955 def elem_subplot(self, curvex, curvey, xlabel, ylabel, name, plt):
956 plt.xlabel(xlabel)
957 plt.ylabel(ylabel)
958 for i in np.arange(self.Nelements):
959 label = name + ' ' + self.elements[i]
960 plt.plot(curvex, curvey[i](curvex), label=label)
961 plt.legend()
963 def multielem_subplot(self, curvex, curvey, xlabel,
964 ylabel, name, plt, half=True):
965 plt.xlabel(xlabel)
966 plt.ylabel(ylabel)
967 for i in np.arange(self.Nelements):
968 for j in np.arange((i + 1) if half else self.Nelements):
969 label = name + ' ' + self.elements[i] + '-' + self.elements[j]
970 plt.plot(curvex, curvey[i, j](curvex), label=label)
971 plt.legend()