# 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.
from __future__ import print_function, unicode_literals
import io
import pkgutil
from functools import lru_cache
import numpy as np
import pandas
__author__ = "Joerg Neugebauer, Sudarsan Surendralal, Martin Boeckmann"
__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"
pandas.options.mode.chained_assignment = None
MENDELEEV_PROPERTY_LIST = [
"abundance_crust",
"abundance_sea",
"atomic_number",
"atomic_radius",
"atomic_radius_rahm",
"atomic_volume",
"atomic_weight",
"atomic_weight_uncertainty",
"block",
"boiling_point",
"c6",
"c6_gb",
"cas",
"covalent_radius",
"covalent_radius_bragg",
"covalent_radius_cordero",
"covalent_radius_pyykko",
"covalent_radius_pyykko_double",
"covalent_radius_pyykko_triple",
"cpk_color",
"density",
"description",
"dipole_polarizability",
"dipole_polarizability_unc",
"discoverers",
"discovery_location",
"discovery_year",
"ec",
"econf",
"electron_affinity",
"electronegativity",
"electronegativity_allen",
"electronegativity_allred_rochow",
"electronegativity_cottrell_sutton",
"electronegativity_ghosh",
"electronegativity_gordy",
"electronegativity_li_xue",
"electronegativity_martynov_batsanov",
"electronegativity_mullay",
"electronegativity_mulliken",
"electronegativity_nagle",
"electronegativity_pauling",
"electronegativity_sanderson",
"electronegativity_scales",
"electrons",
"electrophilicity",
"en_allen",
"en_ghosh",
"en_gunnarsson_lundqvist",
"en_miedema",
"en_mullay",
"en_pauling",
"en_robles_bartolotti",
"evaporation_heat",
"fusion_heat",
"gas_basicity",
"geochemical_class",
"glawe_number",
"goldschmidt_class",
"group",
"group_id",
"hardness",
"heat_of_formation",
"inchi",
"init_on_load",
"ionenergies",
"ionic_radii",
"is_monoisotopic",
"is_radioactive",
"isotope",
"isotopes",
"jmol_color",
"lattice_constant",
"lattice_structure",
"mass",
"mass_number",
"mass_str",
"melting_point",
"mendeleev_number",
"metadata",
"metallic_radius",
"metallic_radius_c12",
"miedema_electron_density",
"miedema_molar_volume",
"molar_heat_capacity",
"molcas_gv_color",
"name",
"name_origin",
"neutrons",
"nist_webbook_url",
"nvalence",
"oxidation_states",
"oxides",
"oxistates",
"period",
"pettifor_number",
"phase_transitions",
"political_stability_of_top_producer",
"political_stability_of_top_reserve_holder",
"price_per_kg",
"production_concentration",
"proton_affinity",
"protons",
"recycling_rate",
"registry",
"relative_supply_risk",
"reserve_distribution",
"scattering_factors",
"sconst",
"screening_constants",
"series",
"softness",
"sources",
"specific_heat",
"specific_heat_capacity",
"substitutability",
"symbol",
"thermal_conductivity",
"top_3_producers",
"top_3_reserve_holders",
"uses",
"vdw_radius",
"vdw_radius_alvarez",
"vdw_radius_batsanov",
"vdw_radius_bondi",
"vdw_radius_dreiding",
"vdw_radius_mm3",
"vdw_radius_rt",
"vdw_radius_truhlar",
"vdw_radius_uff",
"zeff",
]
[docs]
@lru_cache(maxsize=118)
def element(*args):
import mendeleev
return mendeleev.element(*args)
[docs]
class ChemicalElement(object):
"""
An Object which contains the element specific parameters
"""
[docs]
def __init__(self, sub):
"""
Constructor: assign PSE dictionary to object
"""
self._dataset = None
self.sub = sub
self._element_str = None
stringtypes = str
if isinstance(self.sub, stringtypes):
self._element_str = self.sub
elif "Parent" in self.sub.index and isinstance(self.sub.Parent, stringtypes):
self._element_str = self.sub.Parent
elif len(self.sub) > 0:
self._element_str = self.sub.Abbreviation
self._mendeleev_translation_dict = {
"AtomicNumber": "atomic_number",
"AtomicRadius": "covalent_radius_cordero",
"AtomicMass": "mass",
"Color": "cpk_color",
"CovalentRadius": "covalent_radius",
"CrystalStructure": "lattice_structure",
"Density": "density",
"DiscoveryYear": "discovery_year",
"ElectronAffinity": "electron_affinity",
"Electronegativity": "electronegativity",
"Group": "group_id",
"Name": "name",
"Period": "period",
"StandardName": "name",
"VanDerWaalsRadius": "vdw_radius",
"MeltingPoint": "melting_point",
}
self.el = None
def __getattr__(self, item):
if item in ["__array_struct__", "__array_interface__", "__array__"]:
raise AttributeError
return self[item]
def __getitem__(self, item):
if item in self._mendeleev_translation_dict.keys():
item = self._mendeleev_translation_dict[item]
if item in MENDELEEV_PROPERTY_LIST:
return getattr(element(self._element_str), item)
if item in self.sub.index:
return self.sub[item]
def __setstate__(self, state):
self.__dict__.update(state)
def __getstate__(self):
# Only necessary to support pickling in python <3.11
# https://docs.python.org/release/3.11.2/library/pickle.html#object.__getstate__
return self.__dict__
def __eq__(self, other):
if self is other:
return True
elif isinstance(other, self.__class__):
conditions = list()
conditions.append(self.sub.to_dict() == other.sub.to_dict())
return all(conditions)
elif isinstance(other, (np.ndarray, list)):
conditions = list()
for sp in other:
conditions.append(self.sub.to_dict() == sp.sub.to_dict())
return any(conditions)
def __ne__(self, other):
return not self.__eq__(other)
def __gt__(self, other):
if self != other:
if self["AtomicNumber"] != other["AtomicNumber"]:
return self["AtomicNumber"] > other["AtomicNumber"]
else:
return self["Abbreviation"] > other["Abbreviation"]
else:
return False
def __ge__(self, other):
if self != other:
return self > other
else:
return True
def __hash__(self):
return hash(repr(self))
@property
def tags(self):
if "tags" not in self.sub.keys() or self.sub["tags"] is None:
return dict()
return self.sub["tags"]
def __dir__(self):
return list(self.sub.index) + super(ChemicalElement, self).__dir__()
def __str__(self):
return str([self._dataset, self.sub])
def to_dict(self):
hdf_el = {}
# TODO: save all parameters that are different from the parent (e.g. modified mass)
if self.Parent is not None:
self._dataset = {"Parameter": ["Parent"], "Value": [self.Parent]}
hdf_el["elementData"] = self._dataset
# "Dictionary of element tag static"
hdf_el.update({"tagData/" + key: self.tags[key] for key in self.tags.keys()})
return hdf_el
def from_dict(self, obj_dict):
pse = PeriodicTable()
elname = self.sub.name
if "elementData" in obj_dict.keys():
element_data = obj_dict["elementData"]
for key, val in zip(element_data["Parameter"], element_data["Value"]):
if key in "Parent":
self.sub = pse.dataframe.loc[val]
self.sub["Parent"] = val
self._element_str = val
else:
self.sub["Parent"] = None
self._element_str = elname
self.sub.name = elname
if "tagData" in obj_dict.keys():
self.sub["tags"] = obj_dict["tagData"]
[docs]
def to_hdf(self, hdf):
"""
saves the element with his parameters into his hdf5 job file
Args:
hdf (Hdfio): Hdfio object which will be used
"""
chemical_element_dict_to_hdf(
data_dict=self.to_dict(), hdf=hdf, group_name=self.Abbreviation
)
[docs]
def from_hdf(self, hdf):
"""
loads an element with his parameters from the hdf5 job file and store it into its specific pandas series
Args:
hdf (Hdfio): Hdfio object which will be used to read a hdf5 file
"""
elname = self.sub.name
with hdf.open(elname) as hdf_el:
self.from_dict(obj_dict=hdf_el.read_dict_from_hdf(recursive=True))
[docs]
class PeriodicTable:
"""
An Object which stores an elementary table which can be modified for the current session
"""
[docs]
def __init__(self, file_name=None): # PSE_dat_file = None):
"""
Args:
file_name (str): Possibility to choose an source hdf5 file
"""
self.dataframe = self._get_periodic_table_df(file_name)
if "Abbreviation" not in self.dataframe.columns.values:
self.dataframe["Abbreviation"] = None
if not all(self.dataframe["Abbreviation"].values):
for item in self.dataframe.index.values:
if self.dataframe["Abbreviation"][item] is None:
self.dataframe["Abbreviation"][item] = item
self._parent_element = None
self.el = None
def __getattr__(self, item):
return self[item]
def __getitem__(self, item):
if item in self.dataframe.columns.values:
return self.dataframe[item]
if item in self.dataframe.index.values:
return self.dataframe.loc[item]
def __setstate__(self, state):
"""
Used by (cloud)pickle; force the state update to avoid recursion pickling Atoms
"""
self.__dict__.update(state)
def __getstate__(self):
# Only necessary to support pickling in python <3.11
# https://docs.python.org/release/3.11.2/library/pickle.html#object.__getstate__
return self.__dict__
def from_dict(self, obj_dict):
for el, el_dict in obj_dict.items():
sub = pandas.Series(dtype=object)
new_element = ChemicalElement(sub)
new_element.sub.name = el
new_element.from_dict(obj_dict=el_dict)
new_element.sub["Abbreviation"] = el
if "sub_tags" in new_element.tags:
if not new_element.tags["sub_tags"]:
del new_element.tags["sub_tags"]
if new_element.Parent is None:
if not (el in self.dataframe.index.values):
raise AssertionError()
if len(new_element.sub["tags"]) > 0:
raise ValueError("Element cannot get tag-assignment twice")
if "tags" not in self.dataframe.keys():
self.dataframe["tags"] = None
self.dataframe["tags"][el] = new_element.tags
else:
self.dataframe = pandas.concat(
[self.dataframe, new_element.sub.to_frame().T]
)
self.dataframe["tags"] = self.dataframe["tags"].apply(
lambda x: None if pandas.isnull(x) else x
)
self.dataframe["Parent"] = self.dataframe["Parent"].apply(
lambda x: None if pandas.isnull(x) else x
)
[docs]
def from_hdf(self, hdf):
"""
loads an element with his parameters from the hdf5 job file by creating an Object of the ChemicalElement type.
The new element will be stored in the current periodic table.
Changes in the tags will also be modified inside the periodic table.
Args:
hdf (Hdfio): Hdfio object which will be used to read the data from a hdf5 file
Returns:
"""
self.from_dict(obj_dict=hdf.read_dict_from_hdf(recursive=True))
[docs]
def element(self, arg, **qwargs):
"""
The method searches through the periodic table. If the table contains the element,
it will return an Object of the type ChemicalElement containing all parameters from the periodic table.
The option **qwargs allows a direct modification of the tag-values during the creation process
Args:
arg (str, ChemicalElement): sort of element
**qwargs: e.g. a dictionary of tags
Returns element (ChemicalElement): a element with all its properties (Abbreviation, AtomicMass, Weight, ...)
"""
stringtypes = str
if isinstance(arg, stringtypes):
if arg in self.dataframe.index.values:
self.el = arg
else:
raise KeyError(arg)
elif isinstance(arg, int):
if arg in list(self.dataframe["AtomicNumber"]):
index = list(self.dataframe["AtomicNumber"]).index(arg)
self.el = self.dataframe.iloc[index].name
else:
raise ValueError("type not defined: " + str(type(arg)))
if len(qwargs.values()) > 0:
if "tags" not in self.dataframe.columns.values:
self.dataframe["tags"] = None
self.dataframe["tags"][self.el] = qwargs
element = self.dataframe.loc[self.el]
# element['CovalentRadius'] /= 100
return ChemicalElement(element)
[docs]
def is_element(self, symbol):
"""
Compares the Symbol with the Abbreviations of elements inside the periodic table
Args:
symbol (str): name of element, str
Returns boolean: true for the same element, false otherwise
"""
return symbol in self.dataframe["Abbreviation"]
[docs]
def atomic_number_to_abbreviation(self, atom_no):
"""
Args:
atom_no:
Returns:
"""
if not isinstance(atom_no, int):
raise ValueError("type not defined: " + str(type(atom_no)))
return self.Abbreviation[
np.nonzero(self.AtomicNumber.to_numpy() == atom_no)[0][0]
]
[docs]
def add_element(
self, parent_element, new_element, use_parent_potential=False, **qwargs
):
"""
Add "additional" chemical elements to the Periodic Table. These can be used to distinguish between the various
potentials which may exist for a given species or to introduce artificial elements such as pseudohydrogen. For
this case set use_parent_potential = False and add in the directory containing the potential files a new file
which is derived from the name new element.
This function may be also used to provide additional information for the identical chemical element, e.g., to
define a Fe_up and Fe_down to perform the correct symmetry search as well as initialization.
Args:
parent_element (str): name of parent element
new_element (str): name of new element
use_parent_potential: True: use the potential from the parent species
**qwargs: define tags and their values, e.g. spin = "up", relax = [True, True, True]
Returns: new element (ChemicalElement)
"""
pandas.options.mode.chained_assignment = None
parent_element_data_series = self.dataframe.loc[parent_element]
parent_element_data_series["Abbreviation"] = new_element
parent_element_data_series["Parent"] = parent_element
parent_element_data_series.name = new_element
if new_element not in self.dataframe.T.columns:
self.dataframe = pandas.concat(
[self.dataframe, parent_element_data_series.to_frame().T],
)
else:
self.dataframe.loc[new_element] = parent_element_data_series
if use_parent_potential:
self._parent_element = parent_element
return self.element(new_element, **qwargs)
@staticmethod
@lru_cache(maxsize=1)
def _get_periodic_table_df(file_name):
"""
Args:
file_name:
Returns:
"""
if not file_name:
return pandas.read_csv(
io.BytesIO(
pkgutil.get_data("pyiron_atomistics", "data/periodic_table.csv")
),
index_col=0,
)
else:
if file_name.endswith(".h5"):
return pandas.read_hdf(file_name, mode="r")
elif file_name.endswith(".csv"):
return pandas.read_csv(file_name, index_col=0)
raise TypeError(
"PeriodicTable file format not recognised: "
+ file_name
+ " supported file formats are csv, h5."
)
[docs]
def chemical_element_dict_to_hdf(data_dict, hdf, group_name):
with hdf.open(group_name) as hdf_el:
if "elementData" in data_dict.keys():
hdf_el["elementData"] = data_dict["elementData"]
with hdf_el.open("tagData") as hdf_tag:
if "tagData" in data_dict.keys():
for k, v in data_dict["tagData"].items():
hdf_tag[k] = v