# coding: utf-8
# Distributed under the terms of the MIT License.
""" This submodule implements the Site class for handling
atomic sites.
"""
import numpy as np
from matador.utils.cell_utils import cart2frac, frac2cart, wrap_frac_coords
from matador.orm.orm import DataContainer
[docs]class Site(DataContainer):
""" The Site class contains a description of an individual
site within a 3D periodic Crystal.
"""
# This dictionary defines the map between fields in :obj:`Crystal`
# that correspond to arrays of site properties and between the
# relevant keys the :obj:`Site` object
_crystal_key_map = {
"site_occupancy": "site_occupancy",
"chemical_shielding_isos": "chemical_shielding_iso",
"chemical_shift_isos": "chemical_shift_iso",
"magnetic_shielding_tensors": "magnetic_shielding_tensor",
"chemical_shift_anisos": "chemical_shift_aniso",
"chemical_shift_asymmetries": "chemical_shift_asymmetry",
"quadrupolar_couplings": "quadrupolar_coupling",
"quadrupolar_asymmetries": "quadrupolar_asymmetry",
"voronoi_substructure": "voronoi_substructure"
}
def __init__(self, species: str, position: list, lattice,
position_unit='fractional', mutable: bool = False, **site_data):
""" Initialise a Site object from its species, position and
a reference to the lattice it exists in. Any other keys will be made available
as site-level values.
"""
if site_data.get('voronoi_substructure') is not None:
assert self.species == site_data['voronoi_substructure'][0]
site_data['voronoi_substructure'] = site_data['voronoi_substructure'][1]
# DataContainer will take a copy of all data passed to it, but lets keep
# lattice as a reference so that it can change externally
self._lattice = lattice
super().__init__(
species=species,
position=position,
site_data=site_data,
mutable=mutable
)
self._data["lattice_cart"] = self._lattice
self.set_position(position, position_unit)
self._occupancy = None
self.site_data = {}
self.site_data.update(site_data)
[docs] def get(self, key, default=None):
try:
return self[key]
except (KeyError, AttributeError):
return default
def __getitem__(self, key):
""" Add extra look-up in `self.site_data` to
:class:`DataContainer`'s `__getitem__`.
Parameters:
key (str): name of key or attribute to get.
Raises:
AttributeError: if key or attribute can't be found.
"""
if isinstance(key, int):
raise ValueError("Object does not support indexing")
try:
super().__getitem__(key)
except (AttributeError, KeyError):
pass
try:
return self.site_data[key]
except KeyError:
raise KeyError('Site has no data/site_data or implementation for requested key: "{}"'
.format(key))
def __setitem__(self, key: str, item):
if key not in self.site_data or self.site_data[key] is None:
self.site_data[key] = item
return
elif self.site_data[key] != item:
try:
import math
if (math.isnan(item) and math.isnan(self.site_data[key])):
return
except TypeError:
pass
raise AttributeError('Cannot assign value {} to existing key {} with value {}'
.format(item, key, self.site_data[key]))
def __str__(self):
site_str = '{species} {pos[0]:4.4f} {pos[1]:4.4f} {pos[2]:4.4f}'.format(species=self.species, pos=self.coords)
for key in self.site_data:
try:
site_str += '\n{} = {:4.4f}'.format(key, np.asarray(self.site_data[key]))
except (ValueError, TypeError):
with np.printoptions(precision=2, threshold=6, edgeitems=2):
site_str += (
'\n{} = \n{}'
.format(
key,
"\n".join(f" {row}" for row in np.asarray(self.site_data[key]).__str__().split("\n"))
)
)
site_str += "\n---"
return site_str
def __deepcopy__(self, memo):
from copy import deepcopy
species, position, lattice = (deepcopy(x) for x in (self.species, self.coords, self.lattice))
site_data = deepcopy(self.site_data)
return Site(species, position, lattice, position_unit='fractional', **site_data)
[docs] def set_position(self, position, units):
if len(position) != 3 or not all(isinstance(p, (float, int)) for p in position):
raise RuntimeError('CrystalSite position has wrong shape: {}'.format(position))
if not hasattr(self, '_coords'):
self._coords = dict()
if units == 'fractional':
self._coords['fractional'] = wrap_frac_coords(
[float(pos) for pos in position],
remove=False
)
elif units == 'cartesian':
self._coords['fractional'] = wrap_frac_coords(
cart2frac(self.lattice, self.coords),
remove=False
)
else:
raise RuntimeError('Unit system {} not understood, expecting `fractional`/`cartesian`'.format(units))
@property
def coords(self):
return np.asarray(self._coords['fractional'])
@property
def coords_cartesian(self):
return np.asarray(frac2cart(self.lattice, self.coords))
@property
def species(self):
return self._data['species']
@species.setter
def species(self, value):
self._data['species'] = value
@property
def lattice(self):
try:
return self._lattice.lattice_cart
except AttributeError:
return self._lattice
@property
def occupancy(self):
if "site_occupancy" not in self._data:
self._data["site_occupancy"] = 1.0
return self._data["site_occupancy"]
@property
def coordination(self):
if '_coordination' in self.__dict__:
return self._coordination
if self.voronoi_substructure is None:
raise RuntimeError('Voronoi substructure not found.')
coordination = {}
eps = 0.05
for atom, weight in self.voronoi_substructure:
if weight >= 1 - eps:
if atom not in coordination:
coordination[atom] = 1
else:
coordination[atom] += 1
self._coordination = coordination
return coordination
[docs] def displacement_between_sites(self, other_site):
return self.coords_cartesian - other_site.coords_cartesian
[docs] def distance_between_sites(self, other_site):
return np.linalg.norm(
self.displacement_between_sites(other_site)
)