diatom_scan - Methodology and code
Python imports
[1]:
# Standard library imports
from pathlib import Path
import datetime
from math import floor
from typing import Optional
# http://www.numpy.org/
import numpy as np
# https://ipython.org/
from IPython.display import display, Markdown
# https://github.com/usnistgov/atomman
import atomman as am
import atomman.lammps as lmp
import atomman.unitconvert as uc
from atomman.tools import filltemplate, aslist
# https://github.com/usnistgov/iprPy
import iprPy
from iprPy.tools import read_calc_file
print('Notebook last executed on', datetime.date.today(), 'using iprPy version', iprPy.__version__)
Notebook last executed on 2023-07-31 using iprPy version 0.11.6
Import additional libraries for plotting. The external libraries used are:
[2]:
import bokeh
print('Bokeh version =', bokeh.__version__)
from bokeh.plotting import figure, output_file, show
from bokeh.embed import components
from bokeh.resources import Resources
from bokeh.io import output_notebook
output_notebook()
Bokeh version = 3.1.1
1. Load calculation and view description
1.1. Load the calculation
[3]:
# Load the calculation being demoed
calculation = iprPy.load_calculation('diatom_scan')
1.2. Display calculation description and theory
[4]:
# Display main docs and theory
display(Markdown(calculation.maindoc))
display(Markdown(calculation.theorydoc))
diatom_scan calculation style
Lucas M. Hale, lucas.hale@nist.gov, Materials Science and Engineering Division, NIST.
Introduction
The diatom_scan calculation style evaluates the interaction energy between two atoms at varying distances. This provides a measure of the isolated pair interaction of two atoms providing insights into the strengths of the attraction/repulsion and the effective range of interatomic spacings. This scan also gives insight into the computational smoothness of the potential’s functional form.
Version notes
2019-07-30: Notebook added.
2020-05-22: Version 0.10 update - potentials now loaded from database.
2020-09-22: Setup and parameter definition streamlined. Method and theory expanded.
2022-02-16: Notebook updated to reflect version 0.11.
Additional dependencies
Disclaimers
No 3+ body interactions are explored with this calculation as only two atoms are used.
Method and Theory
Two atoms are placed in an otherwise empty system. The total energy of the system is evaluated for different interatomic spacings. This provides a means of evaluating the pair interaction component of an interatomic potential, which is useful for a variety of reasons
The diatom_scan is a simple calculation that can be used to fingerprint a given interaction. This can be used to help determine if two different implementations produce the same resulting potential when direct comparisons of the potential parameters is not feasible.
For a potential to be suitable for radiation studies, the extreme close-range interaction energies must be prohibitively repulsive while not being so large that the resulting force on the atoms will eject them from the system during integration. The diatom_scan results provide a means of evaluating the close-range interactions.
The smoothness of the potential is also reflected in the diatom_scan energy results. Numerical derivatives of the measured points can determine the order of smoothness as well as the approximate r values where discontinuities occur.
Evaluating large separation values provides a means of identifying the energy of the isolated atoms, given that the separation exceeds the potential’s cutoff. The isolated_atom calculation is an alternative method for obtaining this.
2. Define calculation functions and generate files
This section defines the calculation functions and associated resource files exactly as they exist inside the iprPy package. This allows for the code used to be directly visible and modifiable by anyone looking to see how it works.
2.1. diatom_scan()
This is the primary function for the calculation. The version of this function built in iprPy can be accessed by calling the calc() method of an object of the associated calculation class.
[5]:
def diatom_scan(lammps_command: str,
potential: am.lammps.Potential,
symbols: list,
mpi_command: Optional[str] = None,
rmin: float = uc.set_in_units(0.02, 'angstrom'),
rmax: float = uc.set_in_units(6.0, 'angstrom'),
rsteps: int = 300) -> dict:
"""
Performs a diatom energy scan over a range of interatomic spaces, r.
Parameters
----------
lammps_command :str
Command for running LAMMPS.
potential : atomman.lammps.Potential
The LAMMPS implemented potential to use.
symbols : list
The potential symbols associated with the two atoms in the diatom.
mpi_command : str, optional
The MPI command for running LAMMPS in parallel. If not given, LAMMPS
will run serially.
rmin : float, optional
The minimum r spacing to use (default value is 0.02 angstroms).
rmax : float, optional
The maximum r spacing to use (default value is 6.0 angstroms).
rsteps : int, optional
The number of r spacing steps to evaluate (default value is 300).
Returns
-------
dict
Dictionary of results consisting of keys:
- **'r_values'** (*numpy.array of float*) - All interatomic spacings,
r, explored.
- **'energy_values'** (*numpy.array of float*) - The computed potential
energies for each r value.
"""
# Build lists of values
r_values = np.linspace(rmin, rmax, rsteps)
energy_values = np.empty(rsteps)
# Define atype based on symbols
symbols = aslist(symbols)
if len(symbols) == 1:
atype = [1, 1]
elif len(symbols) == 2:
atype = [1, 2]
else:
raise ValueError('symbols must have one or two values')
# Initialize system (will shift second atom's position later...)
box = am.Box.cubic(a = rmax + 1)
atoms = am.Atoms(atype=atype, pos=[[0.1, 0.1, 0.1], [0.1, 0.1, 0.1]])
system = am.System(atoms=atoms, box=box, pbc=[False, False, False], symbols=symbols)
# Add charges if required
if potential.atom_style == 'charge':
system.atoms.prop_atype('charge', potential.charges(system.symbols))
# Get lammps units
lammps_units = lmp.style.unit(potential.units)
# Define lammps variables
lammps_variables = {}
# Loop over values
for i in range(rsteps):
# Shift second atom's x position
system.atoms.pos[1] = np.array([0.1 + r_values[i], 0.1, 0.1])
# Save configuration
system_info = system.dump('atom_data', f='diatom.dat',
potential=potential)
lammps_variables['atomman_system_pair_info'] = system_info
# Write lammps input script
lammps_script = 'run0.in'
template = read_calc_file('iprPy.calculation.diatom_scan', 'run0.template')
with open(lammps_script, 'w') as f:
f.write(filltemplate(template, lammps_variables, '<', '>'))
# Run lammps and extract data
try:
output = lmp.run(lammps_command, script_name=lammps_script,
mpi_command=mpi_command)
except:
energy_values[i] = np.nan
else:
energy = output.simulations[0]['thermo'].PotEng.values[-1]
energy_values[i] = uc.set_in_units(energy, lammps_units['energy'])
if len(energy_values[np.isfinite(energy_values)]) == 0:
raise ValueError('All LAMMPS runs failed. Potential likely invalid or incompatible.')
# Collect results
results_dict = {}
results_dict['r_values'] = r_values
results_dict['energy_values'] = energy_values
return results_dict
2.2. run0.template file
[6]:
with open('run0.template', 'w') as f:
f.write("""#LAMMPS input script that evaluates a system's energy without relaxing
<atomman_system_pair_info>
thermo_style custom step pe
thermo_modify format float %.13e
run 0""")
3. Specify input parameters
3.1. System-specific paths
lammps_command is the LAMMPS command to use (required).
mpi_command MPI command for running LAMMPS in parallel. A value of None will run simulations serially.
[7]:
lammps_command = 'lmp'
mpi_command = None
# Optional: check that LAMMPS works and show its version
print(f'LAMMPS version = {am.lammps.checkversion(lammps_command)["version"]}')
LAMMPS version = 15 Sep 2022
3.2. Interatomic potential
potential_name gives the name of the potential_LAMMPS reference record in the iprPy library to use for the calculation.
potential is an atomman.lammps.Potential object (required).
[8]:
potential_name = '1999--Mishin-Y--Ni--LAMMPS--ipr1'
# Retrieve potential and parameter file(s) using atomman
potential = am.load_lammps_potential(id=potential_name, getfiles=True)
2.3. Calculation-specific parameters
symbols is the element or pair of element model symbols to use for the diatom.
rmin is the minimum r spacing to use.
rmax is the maximum r spacing to use.
rsteps is the number of r spacing steps to evaluate.
[9]:
symbols = 'Ni'
rmin = uc.set_in_units(0.02, 'angstrom')
rmax = uc.set_in_units(6.0, 'angstrom')
rsteps = 300
4. Run calculation and view results
4.1. Run calculation
All primary calculation method functions take a series of inputs and return a dictionary of outputs.
[10]:
results_dict = diatom_scan(lammps_command, potential, symbols,
mpi_command = mpi_command,
rmin = rmin,
rmax = rmax,
rsteps = rsteps)
print(results_dict.keys())
dict_keys(['r_values', 'energy_values'])
4.2. Report results
Values returned in the results_dict: - ‘r_values’ (numpy.array of float) - All interatomic spacings, r, explored. - ‘energy_values’ (numpy.array of float) - The computed potential energies for each r value.
[12]:
length_unit = 'angstrom'
energy_unit = 'eV'
energy = uc.get_in_units(results_dict['energy_values'], energy_unit)
r = uc.get_in_units(results_dict['r_values'], length_unit)
Emin = floor(energy.min())
if Emin < -10:
Emin = -10
plot = figure(title = f'Diatom energy scan for {potential_name}',
width = 800,
height = 600,
x_range = [uc.get_in_units(rmin, 'angstrom'), uc.get_in_units(rmax, 'angstrom')],
y_range = [Emin, 0],
x_axis_label=f'r ({length_unit})',
y_axis_label=f'Cohesive Energy ({energy_unit}/atom)')
plot.line(r, energy, line_width = 2, legend_label = symbols)
plot.legend.location = "bottom_right"
show(plot)