# 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 types
from functools import wraps
import numpy as np
from ase.build import (
add_adsorbate,
add_vacuum,
bcc100,
bcc110,
bcc111,
bcc111_root,
diamond100,
diamond111,
fcc100,
fcc110,
fcc111,
fcc111_root,
fcc211,
hcp0001,
hcp0001_root,
hcp10m10,
mx2,
root_surface,
root_surface_analysis,
)
from ase.build import (
surface as ase_surf,
)
from pyiron_base import PyironFactory, state
from pyiron_snippets.deprecate import deprecate
from structuretoolkit.build import (
get_high_index_surface_info,
high_index_surface,
)
from structuretoolkit.common import pymatgen_read_from_file
from pyiron_atomistics.atomistics.structure.atoms import (
Atoms,
CrystalStructure,
ase_to_pyiron,
ovito_to_pyiron,
pymatgen_to_pyiron,
)
from pyiron_atomistics.atomistics.structure.factories.aimsgb import AimsgbFactory
from pyiron_atomistics.atomistics.structure.factories.ase import AseFactory
from pyiron_atomistics.atomistics.structure.factories.atomsk import (
_ATOMSK_EXISTS,
AtomskFactory,
)
from pyiron_atomistics.atomistics.structure.factories.compound import CompoundFactory
from pyiron_atomistics.atomistics.structure.factories.materialsproject import (
MaterialsProjectFactory,
)
from pyiron_atomistics.atomistics.structure.periodic_table import PeriodicTable
from pyiron_atomistics.atomistics.structure.pyironase import (
publication as publication_ase,
)
from pyiron_atomistics.atomistics.structure.pyxtal import (
publication as publication_pyxtal,
)
from pyiron_atomistics.atomistics.structure.pyxtal import (
pyxtal,
)
__author__ = "Sudarsan Surendralal"
__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__ = "May 1, 2020"
[docs]
class StructureFactory(PyironFactory):
[docs]
def __init__(self):
self._ase = AseFactory()
if _ATOMSK_EXISTS:
self._atomsk = AtomskFactory()
self._aimsgb = AimsgbFactory()
self._materialsproject = MaterialsProjectFactory()
self._compound = CompoundFactory()
@property
def ase(self):
return self._ase
if _ATOMSK_EXISTS:
@property
def atomsk(self):
return self._atomsk
@property
def aimsgb(self):
return self._aimsgb
@property
def compound(self):
return self._compound
[docs]
def cut(self, *args, **kwargs):
return self.ase.cut(*args, **kwargs)
cut.__doc__ = AseFactory.cut.__doc__
[docs]
def stack(self, *args, **kwargs):
return self.ase.stack(*args, **kwargs)
stack.__doc__ = AseFactory.stack.__doc__
[docs]
def read(self, *args, **kwargs):
# This looks weird, but it's because ASE doesn't handle selective dynamics properly, so we replace it with the pymatgen read equivalent.
try:
structure = self.read_using_pymatgen(*args, **kwargs)
except (ValueError, TypeError):
# For stuff that pymatgen can't read it raises
# ValueError: Unrecognized file extension!
# backwards compatibility for notebooks.
# TypeError: IStructure.from_file() got an unexpected keyword argument 'format'
structure = self.read_using_ase(*args, **kwargs)
return structure
read.__doc__ = AseFactory.read.__doc__
def read_using_pymatgen(self, *args, **kwargs):
return ase_to_pyiron(pymatgen_read_from_file(*args, **kwargs))
def read_using_ase(self, *args, **kwargs):
return self.ase.read(*args, **kwargs)
[docs]
@deprecate(message="Please use .read or .ase.read", version="0.2.2")
def ase_read(self, *args, **kwargs):
return self.ase.read(*args, **kwargs)
ase_read.__doc__ = AseFactory.read.__doc__
[docs]
@deprecate(message="Please use .bulk or .ase.bulk", version="0.2.2")
def ase_bulk(self, *args, **kwargs):
return self.ase.bulk(*args, **kwargs)
ase_bulk.__doc__ = AseFactory.bulk.__doc__
[docs]
def bulk(
self,
name,
crystalstructure=None,
a=None,
c=None,
covera=None,
u=None,
orthorhombic=False,
cubic=False,
):
"""
Creating bulk systems (using ASE bulk module). Crystal structure and lattice constant(s) will be guessed if not
provided.
name (str): Chemical symbol or symbols as in 'MgO' or 'NaCl'.
crystalstructure (str): Must be one of sc, fcc, bcc, hcp, diamond, zincblende,
rocksalt, cesiumchloride, fluorite or wurtzite.
a (float): Lattice constant.
c (float): Lattice constant.
c_over_a (float): c/a ratio used for hcp. Default is ideal ratio: sqrt(8/3).
u (float): Internal coordinate for Wurtzite structure.
orthorhombic (bool): Construct orthorhombic unit cell instead of primitive cell which is the default.
cubic (bool): Construct cubic unit cell if possible.
Returns:
pyiron.atomistics.structure.atoms.Atoms: Required bulk structure
"""
return self.ase.bulk(
name=name,
crystalstructure=crystalstructure,
a=a,
c=c,
covera=covera,
u=u,
orthorhombic=orthorhombic,
cubic=cubic,
)
[docs]
@staticmethod
def surface(
element,
surface_type,
size=(1, 1, 1),
vacuum=1.0,
center=False,
pbc=True,
**kwargs,
):
"""
Generate a surface based on the ase.build.surface module.
Args:
element (str): Element name
surface_type (str): The string specifying the surface type generators available through ase (fcc111,
hcp0001 etc.)
size (tuple): Size of the surface
vacuum (float): Length of vacuum layer added to the surface along the z direction
center (bool): Tells if the surface layers have to be at the center or at one end along the z-direction
pbc (list/numpy.ndarray): List of booleans specifying the periodic boundary conditions along all three
directions.
**kwargs: Additional, arguments you would normally pass to the structure generator like 'a', 'b',
'orthogonal' etc.
Returns:
pyiron_atomistics.atomistics.structure.atoms.Atoms instance: Required surface
"""
# https://gitlab.com/ase/ase/blob/master/ase/lattice/surface.py
if pbc is None:
pbc = True
state.publications.add(publication_ase())
for surface_class in [
add_adsorbate,
add_vacuum,
bcc100,
bcc110,
bcc111,
diamond100,
diamond111,
fcc100,
fcc110,
fcc111,
fcc211,
hcp0001,
hcp10m10,
mx2,
hcp0001_root,
fcc111_root,
bcc111_root,
root_surface,
root_surface_analysis,
ase_surf,
]:
if surface_type == surface_class.__name__:
surface_type = surface_class
break
if isinstance(surface_type, types.FunctionType):
if center:
surface = surface_type(
symbol=element, size=size, vacuum=vacuum, **kwargs
)
else:
surface = surface_type(symbol=element, size=size, **kwargs)
z_max = np.max(surface.positions[:, 2])
surface.cell[2, 2] = z_max + vacuum
surface.pbc = pbc
return ase_to_pyiron(surface)
else:
raise ValueError(f"Surface type {surface_type} not recognized.")
[docs]
@staticmethod
def surface_hkl(lattice, hkl, layers, vacuum=1.0, center=False, pbc=True):
"""
Use ase.build.surface to build a surface with surface normal (hkl).
Args:
lattice (pyiron_atomistics.atomistics.structure.atoms.Atoms/str): bulk Atoms
instance or str, e.g. "Fe", from which to build the surface
hkl (list): miller indices of surface to be created
layers (int): # of atomic layers in the surface
vacuum (float): vacuum spacing
center (bool): shift all positions to center the surface
in the cell
Returns:
pyiron_atomistics.atomistics.structure.atoms.Atoms instance: Required surface
"""
# https://gitlab.com/ase/ase/blob/master/ase/lattice/surface.py
state.publications.add(publication_ase())
surface = ase_surf(lattice, hkl, layers)
z_max = np.max(surface.positions[:, 2])
surface.cell[2, 2] = z_max + vacuum
if center:
surface.positions += 0.5 * surface.cell[2] - [0, 0, z_max / 2]
surface.pbc = pbc
return ase_to_pyiron(surface)
[docs]
@staticmethod
def crystal(element, bravais_basis, lattice_constant):
"""
Create a crystal structure using pyiron's native crystal structure generator
Args:
element (str): Element name
bravais_basis (str): Basis type
lattice_constant (float/list): Lattice constants
Returns:
pyiron.atomistics.structure.atoms.Atoms: The required crystal structure
"""
return CrystalStructure(
element=element,
bravais_basis=bravais_basis,
lattice_constants=[lattice_constant],
)
[docs]
@staticmethod
def atoms(
symbols=None,
positions=None,
numbers=None,
tags=None,
momenta=None,
masses=None,
magmoms=None,
charges=None,
scaled_positions=None,
cell=None,
pbc=None,
celldisp=None,
constraint=None,
calculator=None,
info=None,
indices=None,
elements=None,
dimension=None,
species=None,
**qwargs,
):
"""
Creates a atomistics.structure.atoms.Atoms instance.
Args:
elements (list/numpy.ndarray): List of strings containing the elements or a list of
atomistics.structure.periodic_table.ChemicalElement instances
numbers (list/numpy.ndarray): List of atomic numbers of elements
symbols (list/numpy.ndarray): List of chemical symbols
positions (list/numpy.ndarray): List of positions
scaled_positions (list/numpy.ndarray): List of scaled positions (relative coordinates)
pbc (boolean): Tells if periodic boundary conditions should be applied
cell (list/numpy.ndarray): A 3x3 array representing the lattice vectors of the structure
momenta (list/numpy.ndarray): List of momentum values
tags (list/numpy.ndarray): A list of tags
masses (list/numpy.ndarray): A list of masses
magmoms (list/numpy.ndarray): A list of magnetic moments
charges (list/numpy.ndarray): A list of point charges
celldisp:
constraint (list/numpy.ndarray): A list of constraints
calculator: ASE calculator
info (list/str): ASE compatibility
indices (list/numpy.ndarray): The list of species indices
dimension (int): Dimension of the structure
species (list): List of species
Returns:
pyiron.atomistics.structure.atoms.Atoms: The required structure instance
"""
if pbc is None:
pbc = True
return Atoms(
symbols=symbols,
positions=positions,
numbers=numbers,
tags=tags,
momenta=momenta,
masses=masses,
magmoms=magmoms,
charges=charges,
scaled_positions=scaled_positions,
cell=cell,
pbc=pbc,
celldisp=celldisp,
constraint=constraint,
calculator=calculator,
info=info,
indices=indices,
elements=elements,
dimension=dimension,
species=species,
**qwargs,
)
[docs]
@staticmethod
def element(parent_element, new_element_name=None, spin=None, potential_file=None):
"""
Args:
parent_element (str, int): The parent element eq. "N", "O", "Mg" etc.
new_element_name (str): The name of the new parent element (can be arbitrary)
spin (float): Value of the magnetic moment (with sign)
potential_file (str): Location of the new potential file if necessary
Returns:
atomistics.structure.periodic_table.ChemicalElement instance
"""
periodic_table = PeriodicTable()
if new_element_name is None:
if spin is not None:
new_element_name = (
parent_element + "_spin_" + str(spin).replace(".", "_")
)
else:
new_element_name = parent_element + "_1"
if potential_file is not None:
if spin is not None:
periodic_table.add_element(
parent_element=parent_element,
new_element=new_element_name,
spin=str(spin),
pseudo_potcar_file=potential_file,
)
else:
periodic_table.add_element(
parent_element=parent_element,
new_element=new_element_name,
pseudo_potcar_file=potential_file,
)
elif spin is not None:
periodic_table.add_element(
parent_element=parent_element,
new_element=new_element_name,
spin=str(spin),
)
else:
periodic_table.add_element(
parent_element=parent_element, new_element=new_element_name
)
return periodic_table.element(new_element_name)
[docs]
@deprecate(message="Use .aimsgb.info", version="0.2.2")
def aimsgb_info(self, axis, max_sigma):
return self.aimsgb.info(axis=axis, max_sigma=max_sigma)
aimsgb_info.__doc__ = AimsgbFactory.info.__doc__
[docs]
@deprecate(message="Use .aimsgb.build", version="0.2.2")
def aimsgb_build(
self,
axis,
sigma,
plane,
initial_struct,
to_primitive=False,
delete_layer="0b0t0b0t",
add_if_dist=0.0,
):
return self.aimsgb.build(
axis=axis,
sigma=sigma,
plane=plane,
initial_struct=initial_struct,
to_primitive=to_primitive,
delete_layer=delete_layer,
add_if_dist=add_if_dist,
)
aimsgb_build.__doc__ = AimsgbFactory.build.__doc__
@staticmethod
@wraps(ase_to_pyiron)
def from_ase(ase_atoms):
return ase_to_pyiron(ase_atoms)
@staticmethod
@wraps(pymatgen_to_pyiron)
def from_pymatgen(pymatgen_obj):
return pymatgen_to_pyiron(pymatgen_obj)
@staticmethod
@wraps(ovito_to_pyiron)
def from_ovito(ovito_obj):
return ovito_to_pyiron(ovito_obj)
[docs]
def high_index_surface_info(
self,
element,
crystal_structure,
lattice_constant,
terrace_orientation=None,
step_orientation=None,
kink_orientation=None,
step_down_vector=None,
length_step=3,
length_terrace=3,
length_kink=1,
):
"""
Gives the miller indices of high index surface required to create a stepped and kink surface, based
on the general orientation and length of terrace, step and kinks respectively. The microfacet notation used is
based on the work of Van Hove et al.,[1].
[1] Van Hove, M. A., and G. A. Somorjai. "A new microfacet notation for high-Miller-index surfaces of cubic
materials with terrace, step and kink structures." Surface Science 92.2-3 (1980): 489-518.
Args:
element (str): The parent element eq. "N", "O", "Mg" etc.
crystal_structure (str): The crystal structure of the lattice
lattice_constant (float): The lattice constant
terrace_orientation (list): The miller index of the terrace eg., [1,1,1]
step_orientation (list): The miller index of the step eg., [1,1,0]
kink_orientation (list): The miller index of the kink eg., [1,1,1]
step_down_vector (list): The direction for stepping down from the step to next terrace eg., [1,1,0]
length_terrace (int): The length of the terrace along the kink direction in atoms eg., 3
length_step (int): The length of the step along the step direction in atoms eg., 3
length_kink (int): The length of the kink along the kink direction in atoms eg., 1
Returns:
high_index_surface: The high miller index surface which can be used to create slabs
fin_kink_orientation: The kink orientation lying in the terrace
fin_step_orientation: The step orientation lying in the terrace
"""
return get_high_index_surface_info(
element=element,
crystal_structure=crystal_structure,
lattice_constant=lattice_constant,
terrace_orientation=terrace_orientation,
step_orientation=step_orientation,
kink_orientation=kink_orientation,
step_down_vector=step_down_vector,
length_step=length_step,
length_terrace=length_terrace,
length_kink=length_kink,
)
[docs]
def high_index_surface(
self,
element,
crystal_structure,
lattice_constant,
terrace_orientation=None,
step_orientation=None,
kink_orientation=None,
step_down_vector=None,
length_step=3,
length_terrace=3,
length_kink=1,
layers=60,
vacuum=10,
):
"""
Gives a slab positioned at the bottom with the high index surface computed by high_index_surface_info().
Args:
element (str): The parent element eq. "N", "O", "Mg" etc.
crystal_structure (str): The crystal structure of the lattice
lattice_constant (float): The lattice constant
terrace_orientation (list): The miller index of the terrace. default: [1,1,1]
step_orientation (list): The miller index of the step. default: [1,1,0]
kink_orientation (list): The miller index of the kink. default: [1,1,1]
step_down_vector (list): The direction for stepping down from the step to next terrace. default: [1,1,0]
length_terrace (int): The length of the terrace along the kink direction in atoms. default: 3
length_step (int): The length of the step along the step direction in atoms. default: 3
length_kink (int): The length of the kink along the kink direction in atoms. default: 1
layers (int): Number of layers of the high_index_surface. default: 60
vacuum (float): Thickness of vacuum on the top of the slab. default:10
Returns:
slab: pyiron_atomistics.atomistics.structure.atoms.Atoms instance Required surface
"""
return ase_to_pyiron(
high_index_surface(
element=element,
crystal_structure=crystal_structure,
lattice_constant=lattice_constant,
terrace_orientation=terrace_orientation,
step_orientation=step_orientation,
kink_orientation=kink_orientation,
step_down_vector=step_down_vector,
length_step=length_step,
length_terrace=length_terrace,
length_kink=length_kink,
layers=layers,
vacuum=vacuum,
)
)
@property
def materialsproject(self):
return self._materialsproject
@staticmethod
@wraps(pyxtal)
def pyxtal(*args, **kwargs):
state.publications.add(publication_pyxtal)
return pyxtal(*args, **kwargs)