# -*- coding: utf-8 -*-
"""
===========================================================================
Crystal cell classes (:mod:`sknano.core.crystallography.xtal_cells`)
===========================================================================
.. currentmodule:: sknano.core.crystallography.xtal_cells
"""
from __future__ import absolute_import, division, print_function, \
unicode_literals
__docformat__ = 'restructuredtext en'
from functools import total_ordering
# import copy
import numbers
import numpy as np
from sknano.core import BaseClass, TabulateMixin
from sknano.core.atoms import BasisAtom, BasisAtoms
from .extras import supercell_lattice_points
__all__ = ['CrystalCell', 'UnitCell', 'SuperCell']
@total_ordering
[docs]class UnitCell(BaseClass, TabulateMixin):
"""Base class for abstract representations of crystallographic unit cells.
Parameters
----------
lattice : :class:`~sknano.core.crystallography.LatticeBase` sub-class
basis : {:class:`~python:list`, :class:`~sknano.core.atoms.BasisAtoms`}
coords : {:class:`~python:list`}, optional
cartesian : {:class:`~python:bool`}, optional
wrap_coords : {:class:`~python:bool`}, optional
"""
def __init__(self, lattice=None, basis=None, coords=None, cartesian=False,
wrap_coords=False):
super().__init__()
if basis is None:
basis = BasisAtoms()
else:
basis = BasisAtoms(basis)
if lattice is not None:
basis.lattice = lattice
if coords is not None and len(basis) == len(coords):
for atom, pos in zip(basis, coords):
atom.lattice = lattice
if not cartesian:
atom.rs = pos
else:
atom.rs = lattice.cartesian_to_fractional(pos)
self.lattice = lattice
self.basis = basis
self.wrap_coords = wrap_coords
self.fmtstr = "{lattice!r}, {basis!r}, {coords!r}, " + \
"cartesian=False, wrap_coords={wrap_coords!r}"
def __str__(self):
strrep = self._table_title_str()
objstr = self._obj_mro_str()
lattice = self.lattice
if lattice is not None:
title = '.'.join((objstr, lattice.__class__.__qualname__))
strrep = '\n'.join((strrep, title, str(lattice)))
basis = self.basis
if basis.data:
title = '.'.join((objstr, basis.__class__.__qualname__))
strrep = '\n'.join((strrep, title, str(basis)))
return strrep
def __dir__(self):
return ['lattice', 'basis']
def __eq__(self, other):
return self is other or \
all([(getattr(self, attr) == getattr(other, attr)) for attr
in dir(self)])
def __lt__(self, other):
return (self.lattice < other.lattice and self.basis <= other.basis) \
or (self.lattice <= other.lattice and self.basis < other.basis)
def __getattr__(self, name):
try:
return getattr(self.lattice, name)
except AttributeError:
try:
return getattr(self.basis, name)
except AttributeError:
return super().__getattr__(name)
def __iter__(self):
return iter(self.basis)
# @property
# def basis(self):
# return self._basis
# @basis.setter
# def basis(self, value):
# lattice = self.lattice
# coords = self.coords
# if value is None:
# value = BasisAtoms()
# elif value is not None:
# value = BasisAtoms(value, lattice=lattice)
# if coords is not None:
# for atom, pos in zip(basis, coords):
# atom.lattice = lattice
# if not cartesian:
# atom.rs = pos
# else:
# atom.rs = lattice.cartesian_to_fractional(pos)
[docs] def rotate(self, **kwargs):
"""Rotate unit cell lattice vectors and basis."""
if kwargs.get('anchor_point', None) is None:
kwargs['anchor_point'] = self.lattice.offset
self.lattice.rotate(**kwargs)
self.basis.rotate(**kwargs)
[docs] def translate(self, t, fix_anchor_points=True):
"""Translate unit cell basis."""
self.lattice.translate(t)
self.basis.translate(t, fix_anchor_points=fix_anchor_points)
[docs] def todict(self):
"""Return `dict` of :class:`UnitCell` parameters."""
return dict(lattice=self.lattice, basis=self.basis.symbols.tolist(),
coords=self.basis.rs.tolist(),
wrap_coords=self.wrap_coords)
@total_ordering
[docs]class CrystalCell(BaseClass, TabulateMixin):
"""Class representation of crystal structure cell.
Parameters
----------
lattice : :class:`~sknano.core.crystallography.LatticeBase` sub-class
basis : {:class:`~python:list`, :class:`~sknano.core.atoms.BasisAtoms`}
coords : {:class:`~python:list`}, optional
cartesian : {:class:`~python:bool`}, optional
wrap_coords : {:class:`~python:bool`}, optional
unit_cell : :class:`~sknano.core.crystallography.UnitCell`
scaling_matrix : {:class:`~python:int`, :class:`~python:list`}
"""
def __init__(self, lattice=None, basis=None, coords=None, cartesian=False,
wrap_coords=False, unit_cell=None, scaling_matrix=None):
super().__init__()
if unit_cell is None and basis is not None:
basis = BasisAtoms(basis)
if lattice is not None:
basis.lattice = lattice
if coords is not None and len(basis) == len(coords):
for atom, pos in zip(basis, coords):
atom.lattice = lattice
if not cartesian:
atom.rs = pos
else:
atom.rs = lattice.cartesian_to_fractional(pos)
# if basis is None:
# basis = BasisAtoms()
# These attributes may be reset in the `@scaling_matrix.setter`
# method and so they need to be initialized *before* setting
# `self.scaling_matrix`.
self.basis = basis
self.lattice = lattice
self.unit_cell = unit_cell
self.wrap_coords = wrap_coords
self.scaling_matrix = scaling_matrix
self.fmtstr = \
"lattice={lattice!r}, basis={basis!r}, coords={coords!r}, " + \
"cartesian=False, wrap_coords={wrap_coords!r}, " + \
"unit_cell={unit_cell!r}, scaling_matrix={scaling_matrix!r}"
def __dir__(self):
return ['lattice', 'basis', 'unit_cell', 'scaling_matrix']
def __str__(self):
strrep = self._table_title_str()
objstr = self._obj_mro_str()
lattice = self.lattice
unit_cell = self.unit_cell
if lattice is not None:
title = '.'.join((objstr, lattice.__class__.__qualname__))
strrep = '\n'.join((strrep, title, str(lattice)))
if unit_cell is not None:
title = '.'.join((objstr, unit_cell.__class__.__qualname__))
strrep = '\n'.join((strrep, title, str(unit_cell)))
basis = self.basis
if isinstance(basis, BasisAtoms) and basis.data:
title = '.'.join((objstr, basis.__class__.__qualname__))
strrep = '\n'.join((strrep, title, str(basis)))
return strrep
def __eq__(self, other):
if all([attr is not None for attr in
(self.scaling_matrix, self.unit_cell,
other.scaling_matrix, other.unit_cell)]) and \
self.scaling_matrix.shape == other.scaling_matrix.shape:
return self is other or \
(self.unit_cell == other.unit_cell and
np.allclose(self.scaling_matrix, other.scaling_matrix))
elif all([cell is not None for cell in
(self.unit_cell, other.unit_cell)]):
return self is other or \
(self.unit_cell == other.unit_cell and
all([mat is None for mat in
(self.scaling_matrix, other.scaling_matrix)]))
def __lt__(self, other):
return (self.unit_cell < other.unit_cell and
self.scaling_matrix <= other.scaling_matrix) \
or (self.unit_cell <= other.unit_cell and
self.scaling_matrix < other.scaling_matrix)
def __iter__(self):
"""Return iterator over the :attr:`CrystalCell.basis`."""
return iter(self.basis)
def __getattr__(self, name):
if name != 'lattice' and self.lattice is not None:
try:
return getattr(self.lattice, name)
except AttributeError:
pass
if name != 'basis' and self.basis is not None and len(self.basis) != 0:
try:
return getattr(self.basis, name)
except AttributeError:
pass
try:
return getattr(self.unit_cell, name)
except AttributeError:
return super().__getattr__(name)
@property
def basis(self):
""":class:`~sknano.core.atoms.BasisAtoms`."""
return self._basis
@basis.setter
def basis(self, value):
self._basis = value
# if self.unit_cell is not None:
# self.unit_cell.basis[:] = \
# self.basis[:self.unit_cell.basis.Natoms]
@property
def lattice(self):
""":class:`~sknano.core.crystallography.CrystalLattice`."""
return self._lattice
@lattice.setter
def lattice(self, value):
self._lattice = value
if self.basis is not None:
self.basis.lattice = self.lattice
@property
def unit_cell(self):
""":class:`UnitCell`."""
return self._unit_cell
@unit_cell.setter
def unit_cell(self, value):
if value is not None and not isinstance(value, UnitCell):
raise ValueError('Expected a `UnitCell` object')
self._unit_cell = value
if value is not None:
if self.lattice is None:
self._lattice = self.unit_cell.lattice
if self.basis is None or self.basis.Natoms == 0:
self._basis = self.unit_cell.basis
@property
def scaling_matrix(self):
"""Scaling matrix."""
return self._scaling_matrix
@scaling_matrix.setter
def scaling_matrix(self, value):
if value is None:
self._scaling_matrix = np.asmatrix(np.ones(3, dtype=int))
return
if not isinstance(value, (int, float, tuple, list, np.ndarray)):
return
if isinstance(value, np.ndarray) and \
((value.shape == np.ones(3).shape and
np.allclose(value, np.ones(3))) or
(value.shape == np.eye(3).shape and
np.allclose(value, np.eye(3)))):
self._scaling_matrix = np.asmatrix(value)
return
if isinstance(value, numbers.Number):
value = self.lattice.nd * [int(value)]
scaling_matrix = np.asmatrix(value, dtype=int)
# scaling_matrix = np.asmatrix(value)
if scaling_matrix.shape != self.lattice.matrix.shape:
scaling_matrix = np.diagflat(scaling_matrix)
self._scaling_matrix = scaling_matrix
self.lattice = self.lattice.__class__(
cell_matrix=self.scaling_matrix * self.lattice.matrix)
tvecs = \
np.asarray(
np.asmatrix(supercell_lattice_points(self.scaling_matrix)) *
self.lattice.matrix)
basis = self.basis[:]
self.basis = BasisAtoms()
for atom in basis:
for tvec in tvecs:
xs, ys, zs = \
self.lattice.cartesian_to_fractional(atom.r + tvec)
if self.wrap_coords:
xs, ys, zs = \
self.lattice.wrap_fractional_coordinate(
[xs, ys, zs])
self.basis.append(BasisAtom(atom.element, lattice=self.lattice,
xs=xs, ys=ys, zs=zs))
[docs] def rotate(self, **kwargs):
"""Rotate crystal cell lattice, basis, and unit cell."""
if kwargs.get('anchor_point', None) is None:
kwargs['anchor_point'] = self.lattice.offset
if self.lattice is not None:
self.lattice.rotate(**kwargs)
if self.basis is not None:
self.basis.rotate(**kwargs)
self.unit_cell.rotate(**kwargs)
[docs] def translate(self, t, fix_anchor_points=True):
"""Translate crystal cell basis."""
if self.lattice is not None:
self.lattice.translate(t)
if self.basis is not None:
self.basis.translate(t, fix_anchor_points=fix_anchor_points)
self.unit_cell.translate(t, fix_anchor_points=fix_anchor_points)
[docs] def update_basis(self, element, index=None, step=None):
"""Update a crystal cell basis element."""
if index is None:
[self.unit_cell.basis.__setitem__(i, element)
for i in range(len(self.unit_cell.basis))]
[self.basis.__setitem__(i, element)
for i in range(len(self.basis))]
elif isinstance(index, int):
if step is None:
step = self.unit_cell.basis.Natoms
[self.unit_cell.basis.__setitem__(i, element)
for i in range(index, len(self.unit_cell.basis), step)]
[self.basis.__setitem__(i, element)
for i in range(index, len(self.basis), step)]
elif isinstance(index, (list, np.ndarray)):
[self.unit_cell.basis.__setitem__(i, element) for i in index]
[self.basis.__setitem__(i, element) for i in index]
[docs] def todict(self):
""":class:`~python:dict` of :class:`CrystalCell` parameters."""
try:
return dict(lattice=self.lattice,
basis=self.basis.symbols.tolist(),
coords=self.basis.rs.tolist(),
wrap_coords=self.wrap_coords,
unit_cell=self.unit_cell,
scaling_matrix=self.scaling_matrix.tolist())
except AttributeError:
return dict(lattice=self.lattice, basis=None, coords=None,
wrap_coords=self.wrap_coords,
unit_cell=self.unit_cell,
scaling_matrix=self.scaling_matrix.tolist())
[docs]class SuperCell(CrystalCell):
"""Class representation of crystal structure supercell.
Parameters
----------
unit_cell : :class:`~sknano.core.crystallography.UnitCell`
scaling_matrix : {:class:`~python:int`, :class:`~python:list`}
"""
def __init__(self, unit_cell, scaling_matrix, wrap_coords=False):
if not isinstance(unit_cell, UnitCell):
raise ValueError('Expected a `UnitCell` for `unit_cell`.')
if not isinstance(scaling_matrix,
(int, float, tuple, list, np.ndarray)):
raise ValueError('Expected an `int` or `array_like` object of\n'
'integers for `scaling_matrix`')
super().__init__(unit_cell=unit_cell, scaling_matrix=scaling_matrix,
wrap_coords=wrap_coords)