# coding: utf-8
# Copyright (c) Max-Planck-Institut für Eisenforschung GmbH - Computational Materials Design (CM) Department
# Distributed under the terms of "New BSD License", see the LICENSE file.
import os
import shutil
from typing import List
import pandas as pd
from pyiron_base import GenericParameters, state
from pyiron_snippets.resources import ResourceResolver
from pyiron_atomistics.atomistics.job.potentials import PotentialAbstract
from pyiron_atomistics.atomistics.structure.atoms import Atoms
__author__ = "Joerg Neugebauer, Sudarsan Surendralal, Jan Janssen"
__copyright__ = (
"Copyright 2021, Max-Planck-Institut für Eisenforschung GmbH - "
"Computational Materials Design (CM) Department"
)
__version__ = "1.0"
__maintainer__ = "Sudarsan Surendralal"
__email__ = "surendralal@mpie.de"
__status__ = "production"
__date__ = "Sep 1, 2017"
[docs]
class LammpsPotential(GenericParameters):
"""
This module helps write commands which help in the control of parameters related to the potential used in LAMMPS
simulations
"""
[docs]
def __init__(self, input_file_name=None):
super(LammpsPotential, self).__init__(
input_file_name=input_file_name,
table_name="potential_inp",
comment_char="#",
)
self._potential = None
self._attributes = {}
self._df = None
@property
def df(self):
return self._df
@df.setter
def df(self, new_dataframe):
self._df = new_dataframe
# ToDo: In future lammps should also support more than one potential file - that is currently not implemented.
try:
self.load_string("".join(list(new_dataframe["Config"])[0]))
except IndexError:
raise ValueError(
"Potential not found! "
"Validate the potential name by self.potential in self.list_potentials()."
)
def remove_structure_block(self):
self.remove_keys(["units"])
self.remove_keys(["atom_style"])
self.remove_keys(["dimension"])
@property
def files(self):
env = os.environ
if len(self._df["Filename"].values[0]) > 0 and self._df["Filename"].values[
0
] != [""]:
absolute_file_paths = [
files for files in list(self._df["Filename"])[0] if os.path.isabs(files)
]
relative_file_paths = [
files
for files in list(self._df["Filename"])[0]
if not os.path.isabs(files)
]
for path in relative_file_paths:
absolute_file_paths.append(
LammpsPotentialFile.find_potential_file(path)
)
return absolute_file_paths
def copy_pot_files(self, working_directory):
if self.files is not None:
_ = [shutil.copy(path_pot, working_directory) for path_pot in self.files]
def get_element_lst(self):
return list(self._df["Species"])[0]
def _find_line_by_prefix(self, prefix):
"""
Find a line that starts with the given prefix. Differences in white
space are ignored. Raises a ValueError if not line matches the prefix.
Args:
prefix (str): line prefix to search for
Returns:
list: words of the matching line
Raises:
ValueError: if not matching line was found
"""
def isprefix(prefix, lst):
if len(prefix) > len(lst):
return False
return all(n == l for n, l in zip(prefix, lst))
# compare the line word by word to also match lines that differ only in
# whitespace
prefix = prefix.split()
for parameter, value in zip(self._dataset["Parameter"], self._dataset["Value"]):
words = (parameter + " " + value).strip().split()
if isprefix(prefix, words):
return words
raise ValueError('No line with prefix "{}" found.'.format(" ".join(prefix)))
[docs]
def get_element_id(self, element_symbol):
"""
Return numeric element id for element. If potential does not contain
the element raise a :class:NameError. Only makes sense for potentials
with pair_style "full".
Args:
element_symbol (str): short symbol for element
Returns:
int: id matching the given symbol
Raise:
NameError: if potential does not contain this element
"""
try:
line = "group {} type".format(element_symbol)
return int(self._find_line_by_prefix(line)[3])
except ValueError:
msg = "potential does not contain element {}".format(element_symbol)
raise NameError(msg) from None
[docs]
def get_charge(self, element_symbol):
"""
Return charge for element. If potential does not specify a charge,
raise a :class:NameError. Only makes sense for potentials
with pair_style "full".
Args:
element_symbol (str): short symbol for element
Returns:
float: charge speicified for the given element
Raises:
NameError: if potential does not specify charge for this element
"""
try:
line = "set group {} charge".format(element_symbol)
return float(self._find_line_by_prefix(line)[4])
except ValueError:
msg = "potential does not specify charge for element {}".format(
element_symbol
)
raise NameError(msg) from None
[docs]
def to_dict(self):
super_dict = super(LammpsPotential, self).to_dict()
if self._df is not None:
super_dict.update(
{
"potential/" + key: self._df[key].values[0]
for key in ["Config", "Filename", "Name", "Model", "Species"]
}
)
if "Citations" in self._df.columns.values:
super_dict["potential/Citations"] = self._df["Citations"].values[0]
return super_dict
[docs]
def from_dict(self, obj_dict, version: str = None):
super(LammpsPotential, self).from_dict(obj_dict=obj_dict, version=version)
if "potential" in obj_dict.keys() and "Config" in obj_dict["potential"].keys():
entry_dict = {
key: [obj_dict["potential"][key]]
for key in ["Config", "Filename", "Name", "Model", "Species"]
}
if "Citations" in obj_dict["potential"].keys():
entry_dict["Citations"] = [obj_dict["potential"]["Citations"]]
self._df = pd.DataFrame(entry_dict)
[docs]
def to_hdf(self, hdf, group_name=None):
if self._df is not None:
with hdf.open("potential") as hdf_pot:
hdf_pot["Config"] = self._df["Config"].values[0]
hdf_pot["Filename"] = self._df["Filename"].values[0]
hdf_pot["Name"] = self._df["Name"].values[0]
hdf_pot["Model"] = self._df["Model"].values[0]
hdf_pot["Species"] = self._df["Species"].values[0]
if "Citations" in self._df.columns.values:
hdf_pot["Citations"] = self._df["Citations"].values[0]
super(LammpsPotential, self).to_hdf(hdf, group_name=group_name)
[docs]
def from_hdf(self, hdf, group_name=None):
with hdf.open("potential") as hdf_pot:
try:
entry_dict = {
"Config": [hdf_pot["Config"]],
"Filename": [hdf_pot["Filename"]],
"Name": [hdf_pot["Name"]],
"Model": [hdf_pot["Model"]],
"Species": [hdf_pot["Species"]],
}
if "Citations" in hdf_pot.list_nodes():
entry_dict["Citations"] = [hdf_pot["Citations"]]
self._df = pd.DataFrame(entry_dict)
except ValueError:
pass
super(LammpsPotential, self).from_hdf(hdf, group_name=group_name)
[docs]
class LammpsPotentialFile(PotentialAbstract):
"""
The Potential class is derived from the PotentialAbstract class, but instead of loading the potentials from a list,
the potentials are loaded from a file.
Args:
potential_df:
default_df:
selected_atoms:
"""
resource_plugin_name = "lammps"
@classmethod
def _get_resolver(cls):
env = os.environ
return (
super()
._get_resolver()
.chain(
ResourceResolver(
[env[var] for var in ("CONDA_PREFIX", "CONDA_DIR") if var in env],
"share",
"iprpy",
)
)
)
[docs]
def __init__(self, potential_df=None, default_df=None, selected_atoms=None):
if potential_df is None:
potential_df = self._get_potential_df(
file_name_lst={"potentials_lammps.csv"},
)
super(LammpsPotentialFile, self).__init__(
potential_df=potential_df,
default_df=default_df,
selected_atoms=selected_atoms,
)
if len(self.list()) == 0:
state.logger.warning(
"It looks like your potential database is empty. In order to"
" install the standard pyiron library, run:\n\n"
"conda install -c conda-forge pyiron-data\n\n"
"Depending on the circumstances, you might have to change the"
" RESOURCE_PATHS of your .pyiron file. It is typically located in"
" your home directory. More can be found on the installation page"
" of the pyiron website."
)
def default(self):
if self._default_df is not None:
atoms_str = "_".join(sorted(self._selected_atoms))
return self._default_df[
(self._default_df["Name"] == self._default_df.loc[atoms_str].values[0])
]
return None
[docs]
def find_default(self, element):
"""
Find the potentials
Args:
element (set, str): element or set of elements for which you want the possible LAMMPS potentials
path (bool): choose whether to return the full path to the potential or just the potential name
Returns:
list: of possible potentials for the element or the combination of elements
"""
if isinstance(element, set):
element = element
elif isinstance(element, list):
element = set(element)
elif isinstance(element, str):
element = set([element])
else:
raise TypeError("Only, str, list and set supported!")
element_lst = list(element)
if self._default_df is not None:
merged_lst = list(set(self._selected_atoms + element_lst))
atoms_str = "_".join(sorted(merged_lst))
return self._default_df[
(self._default_df["Name"] == self._default_df.loc[atoms_str].values[0])
]
return None
def __getitem__(self, item):
potential_df = self.find(element=item)
selected_atoms = self._selected_atoms + [item]
return LammpsPotentialFile(
potential_df=potential_df,
default_df=self._default_df,
selected_atoms=selected_atoms,
)
[docs]
class PotentialAvailable(object):
[docs]
def __init__(self, list_of_potentials):
self._list_of_potentials = {
"pot_" + v.replace("-", "_").replace(".", "_"): v
for v in list_of_potentials
}
def __getattr__(self, name):
if name in self._list_of_potentials.keys():
return self._list_of_potentials[name]
else:
raise AttributeError
def __dir__(self):
return list(self._list_of_potentials.keys())
def __repr__(self):
return str(dir(self))
[docs]
def view_potentials(structure: Atoms) -> pd.DataFrame:
"""
List all interatomic potentials for the given atomistic structure including all potential parameters.
To quickly get only the names of the potentials you can use `list_potentials()` instead.
Args:
structure (Atoms): The structure for which to get potentials.
Returns:
pandas.Dataframe: Dataframe including all potential parameters.
"""
list_of_elements = set(structure.get_chemical_symbols())
return LammpsPotentialFile().find(list_of_elements)
[docs]
def list_potentials(structure: Atoms) -> List[str]:
"""
List of interatomic potentials suitable for the given atomic structure.
See `view_potentials` to get more details.
Args:
structure (Atoms): The structure for which to get potentials.
Returns:
list: potential names
"""
return list(view_potentials(structure)["Name"].values)