# -*- coding: utf-8 -*-
"""
===============================================================================
Crystal structure classes (:mod:`sknano.core.crystallography._xtal_structures`)
===============================================================================
.. currentmodule:: sknano.core.crystallography._xtal_structures
"""
from __future__ import absolute_import, division, print_function, \
unicode_literals
__docformat__ = 'restructuredtext en'
from functools import total_ordering
import numpy as np
from sknano.core import BaseClass
from sknano.core.atoms import BasisAtom, BasisAtoms, StructureAtoms
from sknano.core.refdata import lattice_parameters as lattparams
from ._extras import pymatgen_structure, supercell_lattice_points
from ._xtal_cells import CrystalCell, UnitCell, SuperCell
from ._xtal_lattices import Crystal2DLattice, Crystal3DLattice
__all__ = ['BaseStructureMixin', 'BaseStructure', 'StructureData',
'CrystalStructureBase', 'Crystal2DStructure',
'CrystalStructure', 'Crystal3DStructure',
'CaesiumChlorideStructure', 'CsClStructure',
'DiamondStructure',
'RocksaltStructure', 'RockSaltStructure', 'NaClStructure',
'SphaleriteStructure', 'ZincblendeStructure', 'ZincBlendeStructure',
'BCCStructure', 'FCCStructure', 'Iron', 'Copper', 'Gold',
'CubicClosePackedStructure', 'CCPStructure',
'HexagonalClosePackedStructure', 'HCPStructure',
'HexagonalStructure', 'AlphaQuartz', 'MoS2']
class BaseStructureMixin:
"""Mixin class for crystal structures."""
# def __deepcopy__(self, memo):
# from copy import deepcopy
# cp = self.__class__()
# memo[id(self)] = cp
# for attr in dir(self):
# if not attr.startswith('_'):
# setattr(cp, attr, deepcopy(getattr(self, attr), memo))
# return cp
def __getattr__(self, name):
try:
return getattr(self.atoms, name)
except AttributeError:
try:
return getattr(self.crystal_cell, name)
except AttributeError:
return super().__getattr__(name)
@property
def atoms(self):
"""Structure :class:`~sknano.core.atoms.StructureAtoms`."""
return self._atoms
@property
def crystal_cell(self):
"""Structure :class:`~sknano.core.crystallography.CrystalCell`."""
return self._crystal_cell
@crystal_cell.setter
def crystal_cell(self, value):
self._crystal_cell = value
@property
def basis(self):
"""Structure :class:`~sknano.core.atoms.BasisAtoms`."""
return self.crystal_cell.basis
@basis.setter
def basis(self, value):
self.crystal_cell.basis = value
@property
def lattice(self):
"""Structure :class:`~sknano.core.crystallography.Crystal3DLattice`."""
return self.crystal_cell.lattice
@lattice.setter
def lattice(self, value):
self.crystal_cell.lattice = value
self.atoms.lattice = self.crystal_cell.lattice
@property
def scaling_matrix(self):
""":attr:`CrystalCell.scaling_matrix`."""
return self.crystal_cell.scaling_matrix
@scaling_matrix.setter
def scaling_matrix(self, value):
self.crystal_cell.scaling_matrix = value
@property
def unit_cell(self):
"""Structure :class:`~sknano.core.crystallography.UnitCell`."""
return self.crystal_cell.unit_cell
@unit_cell.setter
def unit_cell(self, value):
self.crystal_cell.unit_cell = value
@property
def structure(self):
"""Pointer to self."""
return self
@property
def structure_data(self):
"""Alias for :attr:`BaseStructureMixin.structure`."""
return self
def clear(self):
"""Clear list of :attr:`BaseStructureMixin.atoms`."""
self.atoms.clear()
def make_supercell(self, scaling_matrix, wrap_coords=False):
"""Make supercell."""
return SuperCell(self.unit_cell, scaling_matrix,
wrap_coords=wrap_coords)
def rotate(self, **kwargs):
"""Rotate crystal cell lattice, basis, and unit cell."""
self.crystal_cell.rotate(**kwargs)
self.atoms.rotate(**kwargs)
def translate(self, t, fix_anchor_points=True):
"""Translate crystal cell basis."""
self.crystal_cell.translate(t, fix_anchor_points=fix_anchor_points)
self.atoms.translate(t, fix_anchor_points=fix_anchor_points)
def transform_lattice(self, scaling_matrix, wrap_coords=False, pbc=None):
if self.lattice is None:
return
self.lattice = self.lattice.__class__(
cell_matrix=np.asmatrix(scaling_matrix) * self.lattice.matrix)
if wrap_coords:
self.crystal_cell.basis.wrap_coords(pbc=pbc)
self.atoms.wrap_coords(pbc=pbc)
# tvecs = \
# np.asarray(
# np.asmatrix(supercell_lattice_points(scaling_matrix)) *
# self.lattice.matrix)
# if self.crystal_cell.basis is not None:
# basis = self.crystal_cell.basis[:]
# self.crystal_cell.basis = BasisAtoms()
# for atom in basis:
# xs, ys, zs = \
# self.lattice.cartesian_to_fractional(atom.r)
# if wrap_coords:
# xs, ys, zs = \
# self.lattice.wrap_fractional_coordinate([xs, ys, zs])
# self.crystal_cell.basis.append(
# BasisAtom(atom.element, lattice=self.lattice,
# xs=xs, ys=ys, zs=zs))
def read_data(self, *args, **kwargs):
from sknano.io import DATAReader
return DATAReader(*args, **kwargs)
def read_dump(self, *args, **kwargs):
from sknano.io import DUMPReader
return DUMPReader(*args, **kwargs)
def read_xyz(self, *args, **kwargs):
from sknano.io import XYZReader
return XYZReader.read(*args, **kwargs)
def write_data(self, **kwargs):
from sknano.io import DATAWriter
DATAWriter.write(**kwargs)
def write_dump(self, **kwargs):
from sknano.io import DUMPWriter
DUMPWriter.write(**kwargs)
def write_xyz(self, **kwargs):
from sknano.io import XYZWriter
XYZWriter.write(**kwargs)
class BaseStructure(BaseStructureMixin):
"""Base structure class for structure data."""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._atoms = StructureAtoms()
self._crystal_cell = CrystalCell()
StructureData = BaseStructure
@total_ordering
class CrystalStructureBase(BaseStructure, BaseClass):
"""Base class for abstract representions of crystal structures.
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
scaling_matrix : {:class:`~python:int`, :class:`~python:list`}, optional
"""
def __init__(self, lattice=None, basis=None, coords=None,
cartesian=False, scaling_matrix=None, **kwargs):
super().__init__(**kwargs)
self.unit_cell = UnitCell(lattice=lattice, basis=basis,
coords=coords, cartesian=cartesian)
self.scaling_matrix = scaling_matrix
self.fmtstr = self.unit_cell.fmtstr + \
", scaling_matrix={scaling_matrix!r}"
def __dir__(self):
return dir(self.crystal_cell)
def __eq__(self, other):
if isinstance(other, CrystalStructureBase):
return self is other or self.crystal_cell == other.crystal_cell
def __lt__(self, other):
if isinstance(other, CrystalStructureBase):
return self.crystal_cell < other.crystal_cell
def todict(self):
attrdict = self.unit_cell.todict()
attrdict.update(dict(scaling_matrix=self.scaling_matrix.tolist()))
return attrdict
[docs]class Crystal2DStructure(CrystalStructureBase):
"""Base class for 2D crystal structures.
.. warning:: The implementation of this class is not complete.
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
scaling_matrix : {:class:`~python:int`, :class:`~python:list`}, optional
structure : `Crystal3DStructure`, optional
"""
def __init__(self, lattice=None, basis=None, coords=None, cartesian=False,
scaling_matrix=None, structure=None, **kwargs):
if structure is not None:
lattice = structure.lattice
basis = structure.basis
if not isinstance(lattice, Crystal2DLattice):
lattice = Crystal2DLattice(cell_matrix=lattice)
super().__init__(lattice=lattice, basis=basis, coords=coords,
cartesian=cartesian, scaling_matrix=scaling_matrix,
**kwargs)
@classmethod
[docs] def from_pymatgen_structure(cls, structure):
atoms = BasisAtoms()
for site in structure.sites:
atoms.append(BasisAtom(element=site.specie.symbol,
x=site.x, y=site.y, z=site.z))
return cls(lattice=Crystal2DLattice(
cell_matrix=structure.lattice.matrix), basis=atoms)
@classmethod
[docs] def from_spacegroup(cls, sg, lattice=None, basis=None, coords=None,
**kwargs):
"""Return a `Crystal2DStructure` from a spacegroup number/symbol.
Parameters
----------
sg : :class:`~python:int` or :class:`~python:str`
lattice : :class:`Crystal2DLattice`
basis : :class:`~python:list` of :class:`~python:str`\ s
coords : :class:`~python:list` of :class:`~python:float`\ s
Returns
-------
:class:`Crystal2DStructure`
Notes
-----
Under the hood this method first s a pymatgen
:class:`~pymatgen:Structure`
See Also
--------
pymatgen_structure
"""
if not isinstance(basis, list):
basis = [basis]
if len(basis) != len(coords):
if isinstance(coords, list) and len(coords) != 0 and \
isinstance(coords[0], (int, float)):
coords = [coords]
cls.from_spacegroup(sg, lattice, basis, coords)
structure = \
pymatgen_structure(sg, lattice.cell_matrix, basis, coords,
classmethod='from_spacegroup')
return cls.from_pymatgen_structure(structure)
[docs]class Crystal3DStructure(CrystalStructureBase):
"""Base class for 3D crystal structures.
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
scaling_matrix : {:class:`~python:int`, :class:`~python:list`}, optional
structure : `Crystal3DStructure`, optional
"""
def __init__(self, lattice=None, basis=None, coords=None, cartesian=False,
scaling_matrix=None, structure=None, **kwargs):
if structure is not None:
lattice = structure.lattice
basis = structure.basis
if not isinstance(lattice, Crystal3DLattice):
lattice = Crystal3DLattice(cell_matrix=lattice)
super().__init__(lattice=lattice, basis=basis, coords=coords,
cartesian=cartesian, scaling_matrix=scaling_matrix,
**kwargs)
@classmethod
[docs] def from_pymatgen_structure(cls, structure):
"""Return a `Crystal3DStructure` from a \
:class:`pymatgen:pymatgen.core.Structure`.
Parameters
----------
structure : :class:`pymatgen:pymatgen.core.Structure`
Returns
-------
:class:`Crystal3DStructure`
"""
atoms = BasisAtoms()
for site in structure.sites:
atoms.append(BasisAtom(site.specie.symbol,
x=site.x, y=site.y, z=site.z))
return cls(lattice=Crystal3DLattice(
cell_matrix=structure.lattice.matrix), basis=atoms)
@classmethod
[docs] def from_spacegroup(cls, sg, lattice=None, basis=None, coords=None,
**kwargs):
"""Return a `Crystal3DStructure` from a spacegroup number/symbol.
Parameters
----------
sg : :class:`~python:int` or :class:`~python:str`
lattice : :class:`Crystal3DLattice`
basis : :class:`~python:list` of :class:`~python:str`\ s
coords : :class:`~python:list` of :class:`~python:float`\ s
Returns
-------
:class:`Crystal3DStructure`
Notes
-----
Under the hood this method generates a
:class:`~pymatgen:pymatgen.core.Structure`
See Also
--------
pymatgen_structure
"""
if not isinstance(basis, list):
basis = [basis]
if len(basis) != len(coords):
if isinstance(coords, list) and len(coords) != 0 and \
isinstance(coords[0], (int, float)):
coords = [coords]
cls.from_spacegroup(sg, lattice=lattice, basis=basis,
coords=coords, **kwargs)
structure = \
pymatgen_structure(sg, lattice.cell_matrix, basis, coords,
classmethod='from_spacegroup')
return cls.from_pymatgen_structure(structure, **kwargs)
CrystalStructure = Crystal3DStructure
[docs]class MoS2(Crystal3DStructure):
"""Molybdenum disulphide structure class."""
def __init__(self, a=lattparams['molybdenum_disulphide']['a'],
c=lattparams['molybdenum_disulphide']['c'],
basis=['Mo', 'S'], **kwargs):
molybdenum_disulphide = \
pymatgen_structure(194,
Crystal3DLattice.hexagonal(a, c).cell_matrix,
basis, [[1/3, 2/3, 1/4], [1/3, 2/3, 0.621]],
classmethod='from_spacegroup')
molybdenum_disulphide = \
Crystal3DStructure.from_pymatgen_structure(molybdenum_disulphide)
super().__init__(structure=molybdenum_disulphide, **kwargs)
[docs]class CaesiumChlorideStructure(Crystal3DStructure):
"""Abstract representation of caesium chloride structure."""
def __init__(self, a=lattparams['caesium_chloride'], basis=['Cs', 'Cl'],
**kwargs):
caesium_chloride = \
pymatgen_structure(221, Crystal3DLattice.cubic(a).cell_matrix,
basis, [[0, 0, 0], [0.5, 0.5, 0.5]],
classmethod='from_spacegroup')
caesium_chloride = \
Crystal3DStructure.from_pymatgen_structure(caesium_chloride)
super().__init__(structure=caesium_chloride, **kwargs)
CsClStructure = CaesiumChlorideStructure
[docs]class DiamondStructure(Crystal3DStructure):
"""Abstract representation of diamond structure."""
def __init__(self, a=lattparams['diamond'], basis=['C'], **kwargs):
diamond = \
pymatgen_structure(227, Crystal3DLattice.cubic(a).cell_matrix,
basis, [[0, 0, 0]],
classmethod='from_spacegroup')
diamond = \
Crystal3DStructure.from_pymatgen_structure(diamond)
super().__init__(structure=diamond, **kwargs)
[docs]class RocksaltStructure(Crystal3DStructure):
"""Abstract representation of caesium chloride structure."""
def __init__(self, a=lattparams['rock_salt'], basis=['Na', 'Cl'],
**kwargs):
rock_salt = \
pymatgen_structure(225, Crystal3DLattice.cubic(a).cell_matrix,
basis, [[0, 0, 0], [0.5, 0.5, 0.5]],
classmethod='from_spacegroup')
rock_salt = \
Crystal3DStructure.from_pymatgen_structure(rock_salt)
super().__init__(structure=rock_salt, **kwargs)
NaClStructure = RockSaltStructure = RocksaltStructure
[docs]class ZincblendeStructure(Crystal3DStructure):
"""Abstract representation of caesium chloride structure."""
def __init__(self, a=lattparams['zincblende'], basis=['Zn', 'Fe'],
**kwargs):
zincblende = \
pymatgen_structure(216, Crystal3DLattice.cubic(a).cell_matrix,
basis, [[0, 0, 0], [0.25, 0.25, 0.25]],
classmethod='from_spacegroup')
zincblende = \
Crystal3DStructure.from_pymatgen_structure(zincblende)
super().__init__(structure=zincblende, **kwargs)
SphaleriteStructure = ZincBlendeStructure = ZincblendeStructure
class CubicStructure(Crystal3DStructure):
"""Base class for a cubic `Crystal3DStructure`.
Parameters
----------
centering : :class:`~python:str`
lattice : :class:`Crystal3DLattice`, optional
a : float, optional
basis : :class:`~python:list`, optional
coords : :class:`~python:list`, optional
scaling_matrix : :class:`~python:int` or :class:`~python:list`, optional
structure : :class:`Crystal3DStructure`, optional
"""
def __init__(self, *args, a=None, centering=None, lattice=None,
basis=None, coords=None, scaling_matrix=None, structure=None,
**kwargs):
if len(args) == 1 and basis is None:
basis = args[0]
self.centering = centering
if lattice is None and structure is None:
lattice = \
Crystal3DLattice.cubic(
CubicStructure.get_lattice_parameter(
a=a, basis=basis, centering=centering))
super().__init__(lattice=lattice, basis=basis, coords=coords,
scaling_matrix=scaling_matrix, structure=structure,
**kwargs)
@classmethod
def get_lattice_parameter(cls, a=None, basis=None, centering=None):
if a is not None:
return a
if basis is None or centering is None:
raise ValueError('\nBoth the `basis` and `centering` kwargs '
'are required')
elif isinstance(basis, (tuple, list)):
if len(basis) != 1:
raise ValueError('Expected a single element basis')
else:
basis = basis[0]
if not isinstance(basis, str):
raise ValueError('Expected `str` object for basis')
if basis not in lattparams['cubic'][centering]:
raise ValueError('Specify lattice constant `a` for '
'given basis {}'.format(basis))
return lattparams['cubic'][centering][basis]
@classmethod
def from_spacegroup(cls, *args, lattice=None, basis=None, coords=None,
a=None, centering=None, **kwargs):
if len(args) == 2 and basis is None:
sg, basis = args
if len(args) == 1:
sg = args[0]
if lattice is None:
lattice = \
Crystal3DLattice.cubic(
CubicStructure.get_lattice_parameter(a=a, basis=basis,
centering=centering))
if coords is None:
coords = [[0, 0, 0]]
return super().from_spacegroup(sg, lattice=lattice, basis=basis,
coords=coords)
[docs]class BCCStructure(CubicStructure):
"""BCC structure class."""
def __init__(self, *args, **kwargs):
kwargs['centering'] = 'BCC'
structure = CubicStructure.from_spacegroup(229, *args, **kwargs)
super().__init__(*args, structure=structure, **kwargs)
[docs]class FCCStructure(CubicStructure):
"""FCC structure class."""
def __init__(self, *args, **kwargs):
kwargs['centering'] = 'FCC'
structure = CubicStructure.from_spacegroup(225, *args, **kwargs)
super().__init__(*args, structure=structure, **kwargs)
[docs]class Iron(BCCStructure):
"""Iron structure."""
def __init__(self, **kwargs):
kwargs['basis'] = 'Fe'
super().__init__(**kwargs)
[docs]class Gold(FCCStructure):
"""Gold structure."""
def __init__(self, **kwargs):
kwargs['basis'] = 'Au'
super().__init__(**kwargs)
[docs]class Copper(FCCStructure):
"""Copper structure."""
def __init__(self, **kwargs):
kwargs['basis'] = 'Cu'
super().__init__(**kwargs)
[docs]class HexagonalClosePackedStructure(Crystal3DStructure):
"""Abstract representation of hexagonal close-packed structure."""
pass
HCPStructure = HexagonalClosePackedStructure
[docs]class CubicClosePackedStructure(Crystal3DStructure):
"""Abstract representation of cubic close-packed structure."""
pass
CCPStructure = CubicClosePackedStructure
class HexagonalStructure(Crystal3DStructure):
@classmethod
def from_spacegroup(cls, sg, a, c, basis, coords):
lattice = Crystal3DLattice.hexagonal(a, c)
return super().from_spacegroup(sg, lattice=lattice, basis=basis,
coords=coords)
[docs]class AlphaQuartz(HexagonalStructure):
"""Alpha quartz structure class."""
def __init__(self, a=lattparams['alpha_quartz']['a'],
c=lattparams['alpha_quartz']['c'], **kwargs):
lattice = Crystal3DLattice.hexagonal(a, c)
basis = BasisAtoms(3 * ["Si"] + 6 * ["O"])
coords = [[0.4697, 0.0000, 0.0000],
[0.0000, 0.4697, 0.6667],
[0.5305, 0.5303, 0.3333],
[0.4133, 0.2672, 0.1188],
[0.2672, 0.4133, 0.5479],
[0.7328, 0.1461, 0.7855],
[0.5867, 0.8539, 0.2145],
[0.8539, 0.5867, 0.4521],
[0.1461, 0.7328, 0.8812]]
alpha_quartz = pymatgen_structure(lattice.cell_matrix,
basis.symbols, coords)
alpha_quartz = \
Crystal3DStructure.from_pymatgen_structure(alpha_quartz)
# alpha_quartz = \
# HexagonalStructure.from_spacegroup(154, a, c, ["Si", "O"],
# [[0.4697, 0.0000, 0.0000],
# [0.4135, 0.2669, 0.1191]],
# scaling_matrix=scaling_matrix)
super().__init__(structure=alpha_quartz, **kwargs)