from os import system
from os.path import abspath, join, isdir
from ase.io import read
from .simulator_base import Simulator
from ..utils import templates
from ..utils.data_standard import METADATA_KEY
from ..utils.input_output import safe_write
[docs]
class LAMMPSSimulator(Simulator):
"""
Class for preparing input, running, and processing LAMMPS calculations
Responsible for creating lammps and configuration input files, providing
commands to run LAMMPS, linking KIM potentials to the LAMMPS run, and
parsing the output to extract atomic configurations from trajectories
:param simulator_args: dictionary of parameters to instantiate the
Simulator, such as code_path (executable to use), elements (list of
elements present in the simulation), and input_template (the path to an
input template to build from)
:type simulator_args: dict
"""
[docs]
def __init__(self, simulator_args):
"""
Class for preparing input, running, and processing LAMMPS calculations
:param simulator_args: dictionary of parameters to instantiate the
Simulator, such as code_path (executable to use), elements (list of
elements present in the simulation), and input_template (the path
to an input template to build from)
:type simulator_args: dict
"""
self.code_path = simulator_args.get('code_path')
if self.code_path is None:
raise KeyError(('A path to the LAMMPS executable (code_path) must '
'be provided in simulator_args to instantiate a '
'LAMMPSSimulator'))
self.elements = simulator_args.get('elements')
if self.elements is None:
raise KeyError(('A list of elements (elements) present in the '
'simulations must be provided in simulator_args to'
'instantiate a LAMMPSSimulator'))
self.input_template = simulator_args.get('input_template')
if self.input_template is None:
raise KeyError(('A path to an input template (input_template) must'
' be present in simulator_args to instantiate a '
'LAMMPSSimulator'))
super().__init__(simulator_args)
[docs]
def write_initial_config(self, run_path, atoms):
"""
Write LAMMPS conf file - initial condintion for simulation
In addition to the lammps input file, the inital configuration is
specified in the conf.lmp file. Conf.lmp defines the atomic positions
cell parameters, and atomic types, and is saved in the run_path
Assuming the cell matrix follows Lammps convention:
https://docs.lammps.org/Howto_triclinic.html
:param run_path: path where the configuration file will be written
:type run_path: str
:param atoms: the configuration to write
:type atoms: Atoms
"""
safe_write(join(run_path, 'conf.lmp'), atoms, format='lammps-data')
self.logger.info((f'Completed writing of the initial configuration '
f'file to {run_path}/conf.lmp'))
[docs]
def get_run_command(self, args=None):
"""
return the command to run a LAMMPS calculation
this method formats the run command based on the ``code_path`` internal
variable set at instantiation of the Simulator, which the
:class:`~orchestrator.workflow.workflow_base.Workflow` will execute in
the proper ``run_path``. The args dictionary can be used to pass the
GPU flag, ``gpu_use``, to format the run command for GPU execution.
:param args: dictionary for parameters to decorate or enable the run
command. GPU command is selected with ``gpu_use`` set to True in
``args``. |default| ``None``
:type args: dict
:returns: command to run the simulator
:rtype: str
"""
if args is None:
args = {}
gpu_use = args.get('gpu_use', False)
input_file = args.get('input_file_name', 'lammps.in')
if gpu_use:
num_gpu = args.get('num_gpu', 1)
command = (f'{self.code_path} -sf kk -k on g {num_gpu} t {num_gpu}'
f' -pk kokkos newton on neigh half '
f'-in {input_file} -log lammps.out')
else:
command = f'{self.code_path} -in {input_file} -log lammps.out'
return command
[docs]
def parse_for_storage(self, run_path):
"""
process LAMMPS output to extract data in the format for Storage
Typically, the output of interest from simulators are the calculation
cell and atomic coordinates and type. However, additional information
could also be extracted as properties in the ASE Atoms object.
:param run_path: directory where the lammps output file resides
:type run_path: str
:returns: list of ASE Atoms of the configurations and any attached
properties. Metadata with the configuration source information is
attached to the METADATA_KEY in the info dict.
:rtype: Atoms
"""
output_file = 'dump.lammpstrj'
full_path = run_path + '/' + output_file
# index = ':' will return all trajectories, and always a list
trajectory = read(full_path, index=':', format='lammps-dump-text')
for i, config in enumerate(trajectory):
atom_ids = config.get_atomic_numbers()
atom_labels = self._convert_integer_to_label(atom_ids)
config.set_chemical_symbols(atom_labels)
# each configuration should have it's source recorded
config.info[METADATA_KEY] = {
'data_source': abspath(full_path),
'config_index': i
}
return trajectory
[docs]
def load_potential(self, run_path, model_path):
"""
set up the potential to be used at run_path
Make the trained model accessible for simulations, i.e. through loading
a KIM potential or ensuring the potential files are present in the
requisite folder. If model path is not provided, then the code will
assume that the model has been loaded in the user enviroment of KIM and
is accessible from outside the current directory.
:param run_path: root path where simulations will run and potential
should be loaded/linked
:type run_path: str
:param model_path: path where the model to load is stored
:type model_path: str
"""
if model_path is None:
self.logger.info(('Model path not provided, model should be '
'installed at the user level'))
else:
if isdir(model_path):
# if the potential is a directory with files (i.e. KIM) then
# link the file to run_path with the same name
potential_name = model_path.strip('/').split('/')[-1]
system(f'ln -s `realpath {model_path}` ./{run_path}/'
f'{potential_name}')
else:
# if the potential is not a directory, it is a (set of)
# file(s) which need to be linked
system(f'ln -s `realpath {model_path}`* ./{run_path}/')
# lammps specific helper function
def _convert_label_to_integer(self, atomic_labels):
"""
Converts atomic label (string) to integer
LAMMPS identifies atoms by integer indexes, but atoms are typically
identified by their chemical symbol strings. This helper function
converts from the string labels to integers in a repeatable and
consistent manner.
:param atomic_labels: chemical identity of each atom (str)
:type atomic_labels: list of str
:returns: list of int mapped by ``elements``
:rtype: list
"""
return [self.elements.index(k) + 1 for k in atomic_labels]
def _convert_integer_to_label(self, atomic_ids):
"""
Converts atomic ids (integer) to labels (string)
LAMMPS identifies atoms by integer indexes, but atoms are typically
identified by their chemical symbol strings. This helper function
converts from integers to the string labels in a repeatable and
consistent manner.
:param atomic_ids: chemical ID each atom (int)
:type atomic_ids: list of int
:returns: list of str mapped by ``elements``
:rtype: list
"""
return [self.elements[k - 1] for k in atomic_ids]