import numpy as np
import attr
from attr import field
from pint import Quantity
from nanotools.io.xyz import readxyz_pos
from nanotools.utils import to_quantity, ureg
def converter_frac_positions(x):
if isinstance(x, str):
# _, x = readxyz_pos(x)
return x
else:
return np.array(x).reshape((-1, 3), order="C")
def converter_positions(x):
shape = (-1, 3)
return to_quantity(x, "angstrom", allow_none=True, allow_string=True, shape=shape)
def to_quantity_avec(x):
shape = (3, 3)
return to_quantity(x, "angstrom", allow_none=False, shape=shape)
[docs]
@attr.s
class AtomCell:
"""
``AtomCell`` class.
The ``AtomCell`` class contains information about the positions and types of the atoms,
the lattice vectors defining the cell, and flags indicating the periodic boundary conditions.
Attributes:
fractional_positions (2D array, str):
Fractional coordinates of each atom. May be an array of row-vectors or the path to an xyz formatted file.
Examples::
atomcell.fractional_positions = [[0,0,0],[0.25,0.25,0.25]]
atomcell.fractional_positions = "left_lead.xyz"
positions (2D array, str, tuple):
Cartesian coordinates of each atom. May be an array of row-vectors or the path to an xyz formatted file.
By default, the units are in Angstroms. If a tuple is provided, the first element is the array of positions,
the second element is the unit of the positions.
Examples::
atomcell.positions = [
[1.16170546635838, 3.35586034069663, 16.96215117189000],
[1.16170546635838, 0.67290534069663, 16.96711717189000],
[3.48511646635838, 7.38012834069663, 16.96215117189000],
[3.48511646635838, 4.69717334069663, 16.96711717189000]
]
atomcell.positions = "graphene.xyz"
atomcell.positions = ("ni_graphene.xyz", "bohr")
avec (2D array):
Three vectors defining a parallelepipedic domain.
By default, the units are in Angstroms. If one wants to use another unit, it is necessary to use the pint library.
Examples::
cell.avec = [[4.64682193271677, 0.0, 0.0], [0.0, 8.04853168139326, 0.0], [0.0, 0.0, 30.27076472739540]]
from nanotools.utils import ureg
cell.avec = [[4.64682193271677, 0.0, 0.0], [0.0, 8.04853168139326, 0.0], [0.0, 0.0, 30.27076472739540]] * ureg.bohr
formula (str):
Chemical formula of the system. If atomcell.positions is a path to an xyz file, the formula is automatically read.
If atomcell.positions is an array, the formula must be specified.
Examples::
atomcell.formula = "C2H4"
pbc (list):
List of three boolean values indicating the periodic boundary conditions in the x, y, and z directions, respectively.
For two-probe system, pbc along the transport direction is automatically set to False. Only needed for the relaxation of general system.
Examples::
atomcell.pbc = [False, True, False]
"""
fractional_positions: np.ndarray = attr.ib(
default=None,
converter=attr.converters.optional(converter_frac_positions),
validator=attr.validators.optional(
attr.validators.instance_of((np.ndarray, list, str))
),
)
positions: Quantity = field(
default=None,
converter=attr.converters.optional(converter_positions),
validator=attr.validators.optional(
attr.validators.instance_of((Quantity, tuple, str))
),
)
avec: Quantity = field(
default=None,
converter=to_quantity_avec,
validator=attr.validators.optional(attr.validators.instance_of(Quantity)),
)
formula: str = attr.ib(
default=None,
validator=attr.validators.optional(attr.validators.instance_of(str)),
)
pbc = attr.ib(
default=[True, True, True],
)
def __attrs_post_init__(self):
"""This method is automatically called after the instance has been initialized.
It checks if the positions or fractional_positions are specified, reads the positions and fractional positions,
sets the lattice vectors (avec), and sets the periodic boundary conditions (pbc).
"""
if self.positions is None and self.fractional_positions is None:
Exception("The keyword atomcell.positions must be specified.")
self._read_positions()
self._read_fractional_positions()
self._set_avec(avec=self.avec)
self._set_pbc()
def _read_positions(self):
if self.positions is not None:
unit = None
if isinstance(self.positions, tuple):
self.positions, unit = self.positions[0], self.positions[1]
if isinstance(self.positions, str):
self.formula, self.positions = readxyz_pos(self.positions)
if unit is None:
self.positions = converter_positions(self.positions)
else:
self.positions *= getattr(ureg, unit)
self.formula = compress_formula(self.formula)
return
def _read_fractional_positions(self):
if self.fractional_positions is not None:
if isinstance(self.fractional_positions, str):
self.formula, self.fractional_positions = readxyz_pos(
self.fractional_positions
)
self.formula = compress_formula(self.formula)
return
def _set_avec(self, avec):
self.avec = to_quantity_avec(avec)
return
def _set_pbc(self):
if not isinstance(self.pbc, list) or len(self.pbc) != 3:
raise ValueError(
"The 'pbc' attribute must be a list of three boolean values."
)
def compress_formula(formula):
sform = split_formula(formula)
cform = []
tmp0 = sform[0]
count = 0
for s in sform:
if s == tmp0:
count += 1
else:
if count > 1:
cform.append(f"{tmp0}{count}")
else:
cform.append(f"{tmp0}")
count = 1
tmp0 = s
if count > 1:
cform.append(f"{tmp0}{count}")
else:
cform.append(f"{tmp0}")
return "".join(cform)
def split_formula(formula):
sform = []
i = 0
while i < len(formula):
tmp = formula[i]
i += 1
while i < len(formula) and not formula[i].isupper():
tmp += formula[i]
i += 1
sform.append(tmp)
return sform