Source code for sknano.core.crystallography._xtal_lattices

# -*- coding: utf-8 -*-
"""
=============================================================================
Crystal lattice classes (:mod:`sknano.core.crystallography._xtal_lattices`)
=============================================================================

.. currentmodule:: sknano.core.crystallography._xtal_lattices

"""
from __future__ import absolute_import, division, print_function, \
    unicode_literals
__docformat__ = 'restructuredtext en'

# from abc import ABCMeta, abstractproperty
from functools import total_ordering

import numpy as np

from sknano.core import BaseClass
from sknano.core.math import Vector, Point, zhat, rotation_matrix

__all__ = ['LatticeBase', 'ReciprocalLatticeBase',
           'DirectLatticeMixin', 'ReciprocalLatticeMixin',
           'Direct2DLatticeMixin', 'Direct3DLatticeMixin',
           'Reciprocal2DLatticeMixin', 'Reciprocal3DLatticeMixin',
           'Crystal2DLattice', 'Reciprocal2DLattice',
           'CrystalLattice', 'ReciprocalLattice',
           'Crystal3DLattice', 'Reciprocal3DLattice']
# __all__ += ['BravaisLattice', 'Bravais3DLattice',
#            'SimpleCubicLattice', 'BodyCenteredCubicLattice',
#            'FaceCenteredCubicLattice']


@total_ordering
class LatticeBase(BaseClass):
    """Base class for crystallographic lattice objects.

    Parameters
    ----------
    nd : int
    cell_matrix : array_like
    orientation_matrix : array_like, optional
    offset : array_like, optional

    """

    def __init__(self, nd=None, cell_matrix=None, orientation_matrix=None,
                 offset=None):
        super().__init__()

        self.nd = nd
        self.offset = Point(offset, nd=3)
        if cell_matrix is not None and orientation_matrix is None:
            orientation_matrix = cell_matrix.T * self.fractional_matrix

        if orientation_matrix is None:
            orientation_matrix = np.asmatrix(np.identity(3))

        self.orientation_matrix = np.asmatrix(orientation_matrix)
        self.lattice_type = None

    def __dir__(self):
        return ['nd', 'offset', 'orientation_matrix']

    def __eq__(self, other):
        if isinstance(other, type(self)):
            return self is other or \
                all([np.allclose(getattr(self, attr), getattr(other, attr))
                     for attr in dir(self)])

    def __lt__(self, other):
        if isinstance(other, type(self)):
            try:
                return self.cell_volume < other.cell_volume
            except AttributeError:
                return self.cell_area < other.cell_area

    @property
    def cell_matrix(self):
        """Matrix of lattice row vectors.

        Same as :attr:`Crystal2DLattice.ortho_matrix`\ .T or
        :attr:`Crystal3DLattice.ortho_matrix`\ .T.

        """
        return (self.orientation_matrix * self.ortho_matrix).T

    @property
    def matrix(self):
        """Alias for \
            :attr:`~sknano.core.crystallography.LatticeBase.cell_matrix`."""
        return self.cell_matrix

    @property
    def fractional_matrix(self):
        """Transformation matrix to convert from cartesian coordinates to \
            fractional coordinates."""
        return np.linalg.inv(self.ortho_matrix)

    @property
    def metric_tensor(self):
        """Metric tensor."""
        return self.cell_matrix * self.cell_matrix.T

    def fractional_to_cartesian(self, fcoords):
        """Convert fractional coordinate to cartesian coordinate.

        Parameters
        ----------
        fcoords : array_like

        Returns
        -------
        :class:`~numpy:numpy.ndarray`

        """
        ccoords = self.orientation_matrix * self.ortho_matrix * \
            np.asmatrix(fcoords).T + self.offset.column_matrix
        try:
            return ccoords.T.A.reshape((3, ))
        except ValueError:
            return ccoords.T.A.reshape((len(fcoords), 3))

    def cartesian_to_fractional(self, ccoords):
        """Convert cartesian coordinate to fractional coordinate.

        Parameters
        ----------
        ccoords : array_like

        Returns
        -------
        :class:`~numpy:numpy.ndarray`

        """
        fcoords = np.linalg.inv(self.ortho_matrix) * \
            np.linalg.inv(self.orientation_matrix) * \
            (np.asmatrix(ccoords).T - self.offset.column_matrix)
        try:
            return fcoords.T.A.reshape((3, ))
        except ValueError:
            return fcoords.T.A.reshape((len(ccoords), 3))

    def wrap_fractional_coordinate(self, p, epsilon=1e-6, pbc=None):
        """Wrap fractional coordinate to lie within unit cell.

        Parameters
        ----------
        p : array_like

        Returns
        -------
        :class:`~numpy:numpy.ndarray`

        """
        if pbc is None:
            pbc = np.asarray(np.ones(3), dtype=bool)

        p = np.ma.array(p, mask=~pbc)
        p = np.ma.fmod(p, 1)
        p[np.ma.where(p < 0)] += 1
        p[np.ma.where(p > 1 - epsilon)] -= 1
        p[np.ma.where(np.logical_or((p > 1 - epsilon), (p < epsilon)))] = 0
        p.mask = np.ma.nomask
        return p.tolist()

    def wrap_cartesian_coordinate(self, p, pbc=None):
        """Wrap cartesian coordinate to lie within unit cell.

        Parameters
        ----------
        p : array_like

        Returns
        -------
        :class:`~numpy:numpy.ndarray`

        """
        return self.fractional_to_cartesian(
            self.wrap_fractional_coordinate(self.cartesian_to_fractional(p),
                                            pbc=pbc))

    def rotate(self, angle=None, axis=None, anchor_point=None,
               rot_point=None, from_vector=None, to_vector=None, degrees=False,
               transform_matrix=None, verbose=False, **kwargs):
        """Rotate unit cell.

        Parameters
        ----------
        angle : float
        axis : :class:`~sknano.core.math.Vector`, optional
        anchor_point : :class:`~sknano.core.math.Point`, optional
        rot_point : :class:`~sknano.core.math.Point`, optional
        from_vector, to_vector : :class:`~sknano.core.math.Vector`, optional
        degrees : bool, optional
        transform_matrix : :class:`~numpy:numpy.ndarray`

        See Also
        --------
        core.math.rotate

        """
        if self.nd == 2:
            axis = 'z'
        if transform_matrix is None:
            transform_matrix = \
                np.asmatrix(
                    rotation_matrix(angle=angle, axis=axis,
                                    anchor_point=anchor_point,
                                    rot_point=rot_point,
                                    from_vector=from_vector,
                                    to_vector=to_vector, degrees=degrees,
                                    verbose=verbose, **kwargs))
            # print('transform_matrix: {}'.format(transform_matrix))

            # transform_matrix = \
            #     transformation_matrix(angle=angle, axis=axis,
            #                           anchor_point=anchor_point,
            #                           rot_point=rot_point,
            #                           from_vector=from_vector,
            #                           to_vector=to_vector, degrees=degrees,
            #                           verbose=verbose, **kwargs)

        self.orientation_matrix = \
            transform_matrix * self.orientation_matrix

    def translate(self, t):
        """Translate lattice.

        Parameters
        ----------
        t : :class:`Vector`

        See Also
        --------
        core.math.translate

        """
        self.offset.translate(t)


class ReciprocalLatticeBase(LatticeBase):
    """Base class for crystallographic reciprocal lattice objects.

    Parameters
    ----------
    direct_lattice : :class:`Crystal2DLattice` or :class:`Crystal3DLattice`
    nd : int
    """
    def __init__(self, direct_lattice, nd, offset=None):
        self._direct_lattice = direct_lattice
        super().__init__(
            nd=nd, cell_matrix=self._direct_lattice.cell_matrix,
            orientation_matrix=self._direct_lattice.orientation_matrix,
            offset=offset)

    def __getattr__(self, name):
        if name != '_direct_lattice':
            return getattr(self._direct_lattice, name)


[docs]class Direct2DLatticeMixin: """Mixin class for computing the 2D direct lattice parameters from \ 2D reciprocal lattice parameters.""" @property def cos_gamma(self): """:math:`\\cos\\gamma`""" return np.around(np.acos(np.radians(self.gamma)), decimals=10) @property def sin_gamma(self): """:math:`\\sin\\gamma`""" return np.sqrt(1 - self.cos_gamma ** 2) @property def a1(self): """2D lattice vector :math:`\\mathbf{a}_1=\\mathbf{a}`.""" b2 = Vector() b2[:2] = self.b2 return b2.cross(zhat) / self.cell_area @property def a2(self): """2D lattice vector :math:`\\mathbf{a}_2=\\mathbf{b}`.""" b1 = Vector() b1[:2] = self.b1 return zhat.cross(b1) / self.cell_area
[docs]class Reciprocal2DLatticeMixin: """Mixin class for computing the 2D reciprocal lattice parameters from \ 2D direct lattice parameters.""" @property def cos_gamma_star(self): """:math:`\\cos\\gamma^*`""" return np.around(np.acos(np.radians(self.gamma_star)), decimals=10) @property def sin_gamma_star(self): """:math:`\\sin\\gamma^*`""" return np.sqrt(1 - self.cos_gamma_star ** 2) @property def b1(self): """2D reciprocal lattice vector :math:`\\mathbf{b}_1=\\mathbf{a}^{*}`. """ a2 = Vector() a2[:2] = self.a2 return a2.cross(zhat)[:2] / self.cell_area @property def b2(self): """2D reciprocal lattice vector :math:`\\mathbf{b}_2=\\mathbf{b}^{*}`. """ a1 = Vector() a1[:2] = self.a1 return zhat.cross(a1)[:2] / self.cell_area
[docs]class Direct3DLatticeMixin: """Mixin class for computing the 3D direct lattice parameters from \ 3D reciprocal lattice parameters.""" @property def cos_alpha(self): """:math:`\\cos\\alpha`""" return np.around( (self.cos_beta_star * self.cos_gamma_star - self.cos_alpha_star) / (self.sin_beta_star * self.sin_gamma_star), decimals=10) @property def cos_beta(self): """:math:`\\cos\\beta`""" return np.around( (self.cos_gamma_star * self.cos_alpha_star - self.cos_beta_star) / (self.sin_gamma_star * self.sin_alpha_star), decimals=10) @property def cos_gamma(self): """:math:`\\cos\\gamma`""" print('calling Direct3DLatticeMixin.cos_gamma') return np.around( (self.cos_alpha_star * self.cos_beta_star - self.cos_gamma_star) / (self.sin_alpha_star * self.sin_beta_star), decimals=10) @property def sin_alpha(self): """:math:`\\sin\\alpha`""" return np.sqrt(1 - self.cos_alpha ** 2) @property def sin_beta(self): """:math:`\\sin\\beta`""" return np.sqrt(1 - self.cos_beta ** 2) @property def sin_gamma(self): """:math:`\\sin\\gamma`""" return np.sqrt(1 - self.cos_gamma ** 2) @property def a1(self): """3D lattice vector :math:`\\mathbf{a}_1=\\mathbf{a}`.""" return self.b2.cross(self.b3) / self.cell_volume @property def a2(self): """3D lattice vector :math:`\\mathbf{a}_2=\\mathbf{b}`.""" return self.b3.cross(self.b1) / self.cell_volume @property def a3(self): """3D lattice vector :math:`\\mathbf{a}_3=\\mathbf{c}`.""" return self.b1.cross(self.b2) / self.cell_volume
DirectLatticeMixin = Direct3DLatticeMixin
[docs]class Reciprocal3DLatticeMixin: """Mixin class for computing the 3D reciprocal lattice parameters from \ the 3D direct lattice parameters.""" @property def cos_alpha_star(self): """:math:`\\cos\\alpha^*`""" return np.around((self.cos_beta * self.cos_gamma - self.cos_alpha) / (self.sin_beta * self.sin_gamma), decimals=10) @property def cos_beta_star(self): """:math:`\\cos\\beta^*`""" return np.around((self.cos_gamma * self.cos_alpha - self.cos_beta) / (self.sin_gamma * self.sin_alpha), decimals=10) @property def cos_gamma_star(self): """:math:`\\cos\\gamma^*`""" return np.around((self.cos_alpha * self.cos_beta - self.cos_gamma) / (self.sin_alpha * self.sin_beta), decimals=10) @property def sin_alpha_star(self): """:math:`\\sin\\alpha^*`""" return np.sqrt(1 - self.cos_alpha_star ** 2) @property def sin_beta_star(self): """:math:`\\sin\\beta^*`""" return np.sqrt(1 - self.cos_beta_star ** 2) @property def sin_gamma_star(self): """:math:`\\sin\\gamma^*`""" return np.sqrt(1 - self.cos_gamma_star ** 2) @property def b1(self): """3D reciprocal lattice vector :math:`\\mathbf{b}_1=\\mathbf{a}^{*}`. """ return self.a2.cross(self.a3) / self.cell_volume @property def b2(self): """3D reciprocal lattice vector :math:`\\mathbf{b}_2=\\mathbf{b}^{*}`. """ return self.a3.cross(self.a1) / self.cell_volume @property def b3(self): """3D reciprocal lattice vector :math:`\\mathbf{b}_3=\\mathbf{c}^{*}`. """ return self.a1.cross(self.a2) / self.cell_volume
ReciprocalLatticeMixin = Reciprocal3DLatticeMixin
[docs]class Crystal2DLattice(LatticeBase, Reciprocal2DLatticeMixin): """2D crystal lattice class. Parameters ---------- a, b : float gamma : float a1, a2 : array_like cell_matrix : array_like orientation_matrix : array_like, optional """ def __init__(self, a=None, b=None, gamma=None, a1=None, a2=None, cell_matrix=None, orientation_matrix=None, offset=None): if cell_matrix is not None: cell_matrix = np.asarray(cell_matrix) a1 = np.array(cell_matrix[0, :]) a2 = np.array(cell_matrix[1, :]) if a1 is not None and a2 is not None: a1 = Vector(a1, nd=3) a2 = Vector(a2, nd=3) a = a1.length b = a2.length gamma = np.degrees(a1.angle(a2)) cell_matrix = np.matrix(np.vstack((np.asarray(a1), np.asarray(a2), np.asarray([0, 0, 1])))) self._a = a self._b = b self._gamma = gamma if None not in (a, b, gamma): self._update_ortho_matrix() super().__init__(nd=2, cell_matrix=cell_matrix, orientation_matrix=orientation_matrix, offset=offset) self.fmtstr = "a={a!r}, b={b!r}, gamma={gamma!r}" def __dir__(self): attrs = super().__dir__() attrs.extend(['a', 'b', 'gamma']) return attrs
[docs] def todict(self): """Return `dict` of `Crystal2DLattice` parameters.""" return dict(a=self.a, b=self.b, gamma=self.gamma)
@property def a(self): """Length of lattice vector :math:`\\mathbf{a}`.""" return np.around(self._a, decimals=10) @a.setter def a(self, value): self._a = float(value) self._update_ortho_matrix() @property def b(self): """Length of lattice_vector :math:`\\mathbf{b}`.""" return np.around(self._b, decimals=10) @b.setter def b(self, value): self._b = float(value) self._update_ortho_matrix() @property def gamma(self): """Angle between lattice vectors :math:`\\mathbf{a}` and \ :math:`\\mathbf{b}` in **degrees**.""" try: return np.around(float(self._gamma), decimals=10) except TypeError: return None @gamma.setter def gamma(self, value): self._gamma = float(value) self._update_ortho_matrix() @property def a1(self): """Lattice vector :math:`\\mathbf{a}_1=\\mathbf{a}`.""" return Vector(self.cell_matrix[0, :].A.flatten())[:2] @property def a2(self): """Lattice vector :math:`\\mathbf{a}_2=\\mathbf{b}`.""" return Vector(self.cell_matrix[1, :].A.flatten())[:2] @property def cos_gamma(self): """:math:`\\cos\\gamma`""" return np.around(np.cos(np.radians(self.gamma)), decimals=10) @property def sin_gamma(self): """:math:`\\sin\\gamma`""" return np.around(np.sin(np.radians(self.gamma)), decimals=10) @property def cell_area(self): """Alias for :attr:`~Crystal2DLattice.area`.""" return self.area @property def area(self): """Crystal cell area.""" return np.abs(self.a1.cross(self.a2)) @property def ortho_matrix(self): """Transformation matrix to convert from fractional coordinates to \ cartesian coordinates.""" return self._ortho_matrix def _update_ortho_matrix(self): m11 = self.a m12 = self.b * self.cos_gamma m22 = self.b * self.sin_gamma self._ortho_matrix = np.matrix([[m11, m12, 0.0], [0.0, m22, 0.0], [0.0, 0.0, 1.0]]) @property def reciprocal_lattice(self): """Return `Crystal2DLattice` reciprocal lattice.""" return Reciprocal2DLattice( cell_matrix=np.linalg.inv(self.cell_matrix).T) @classmethod
[docs] def oblique(cls, a, b, gamma): """Generate an oblique 2D lattice with lattice parameters \ :math:`a, b, \\gamma`.""" return cls(a=a, b=b, gamma=gamma)
@classmethod
[docs] def rectangular(cls, a, b): """Generate a rectangular 2D lattice with lattice parameters \ :math:`a, b`.""" return cls(a=a, b=b, gamma=90)
@classmethod
[docs] def square(cls, a): """Generate a square 2D lattice with lattice parameter \ :math:`a`.""" return cls(a=a, b=a, gamma=90)
@classmethod
[docs] def hexagonal(cls, a): """Generate a hexagonal 2D lattice with lattice parameter \ :math:`a`.""" return cls(a=a, b=a, gamma=120)
@property def lengths(self): """Tuple of lattice parameter lengths :math:`a, b`.""" return self.a, self.b @property def angles(self): """Lattice parameter angle \\gamma`.""" return self.gamma @property def lattice_parameters(self): """Tuple of lattice parameters `a`, `b`, `gamma`.""" return self.a, self.b, self.gamma @property def lengths_and_angles(self): """Alias for attr:`Crystal2DLattice.lattice_parameters`.""" return self.lattice_parameters @property def lattice_vectors(self): """Tuple of lattice vectors :math:`\\mathbf{a}_1, \\mathbf{a}_2`.""" return self.a1, self.a2
[docs]class Reciprocal2DLattice(ReciprocalLatticeBase, Direct2DLatticeMixin): """2D reciprocal lattice class. Parameters ---------- a_star, b_star : float gamma_star : float b1, b2 : array_like cell_matrix : array_like orientation_matrix : array_like """ def __init__(self, a_star=None, b_star=None, gamma_star=None, b1=None, b2=None, cell_matrix=None, orientation_matrix=None, offset=None): direct_lattice = \ Crystal2DLattice(a=a_star, b=b_star, gamma=gamma_star, a1=b1, a2=b2, cell_matrix=cell_matrix, orientation_matrix=orientation_matrix) super().__init__(direct_lattice, nd=2, offset=offset) self.fmtstr = "a_star={a_star!r}, b_star={b_star!r}, " + \ "gamma_star={gamma_star!r}" def __dir__(self): attrs = super().__dir__() attrs.extend(['a_star', 'b_star', 'gamma_star']) return attrs
[docs] def todict(self): """Return `dict` of `Reciprocal2DLattice` parameters.""" return dict(a_star=self.a_star, b_star=self.b_star, gamma_star=self.gamma_star)
@property def a_star(self): """Length of reciprocal lattice vector :math:`\\mathbf{a^*}`.""" return self._direct_lattice.a @a_star.setter def a_star(self, value): self._direct_lattice.a = value @property def b_star(self): """Length of reciprocal lattice_vector :math:`\\mathbf{b^*}`.""" return self._direct_lattice.b @b_star.setter def b_star(self, value): self._direct_lattice.b = value @property def gamma_star(self): """Angle between reciprocal lattice vectors \ :math:`\\mathbf{a}^{\\ast}` and :math:`\\mathbf{b}^{\\ast}` in \ **degrees**.""" try: return np.around(self._direct_lattice.gamma, decimals=10) except TypeError: return None @gamma_star.setter def gamma_star(self, value): self._direct_lattice.gamma = value @property def b1(self): """Reciprocal lattice vector :math:`\\mathbf{b}_1=\\mathbf{a}^*`.""" return self._direct_lattice.a1 @property def b2(self): """Reciprocal lattice vector :math:`\\mathbf{b}_2=\\mathbf{b}^*`.""" return self._direct_lattice.a2 @property def cos_gamma_star(self): """:math:`\\cos\\gamma^*`""" return self._direct_lattice.cos_gamma @property def sin_gamma_star(self): """:math:`\\sin\\gamma^*`""" return self._direct_lattice.sin_gamma @property def reciprocal_lattice(self): """Reciprocal lattice of this `Reciprocal2DLattice`.""" return Crystal2DLattice(cell_matrix=np.linalg.inv(self.cell_matrix).T) @classmethod
[docs] def oblique(cls, a_star, b_star, gamma_star): """Generate an oblique 2D reciprocal lattice with lattice parameters \ :math:`a^*, b^*, \\gamma^*`.""" return cls(a_star=a_star, b_star=b_star, gamma_star=gamma_star)
@classmethod
[docs] def rectangular(cls, a_star, b_star): """Generate a rectangular 2D reciprocal lattice with lattice \ parameters :math:`a^*, b^*`.""" return cls(a_star=a_star, b_star=b_star, gamma_star=90)
@classmethod
[docs] def square(cls, a_star): """Generate a square 2D reciprocal lattice with lattice parameter \ :math:`a^*`.""" return cls(a_star=a_star, b_star=a_star, gamma_star=90)
@classmethod
[docs] def hexagonal(cls, a_star): """Generate a hexagonal 2D reciprocal lattice with lattice parameter \ :math:`a^*`.""" return cls(a_star=a_star, b_star=a_star, gamma_star=120)
@property def lengths(self): """Tuple of lattice parameter lengths :math:`a^*, b^*`.""" return self.a_star, self.b_star @property def angles(self): """Lattice parameter angle \\gamma^*`.""" return self.gamma_star @property def lattice_parameters(self): """Tuple of lattice parameters `a^*`, `b^*`, `gamma^*`.""" return self.a_star, self.b_star, self.gamma_star @property def lattice_vectors(self): """Tuple of lattice vectors :math:`\\mathbf{b}_1, \\mathbf{b}_2`.""" return self.b1, self.b2
[docs]class Crystal3DLattice(LatticeBase, ReciprocalLatticeMixin): """3D crystal lattice class. Parameters ---------- a, b, c : float alpha, beta, gamma : float a1, a2, a3 : array_like cell_matrix : array_like orientation_matrix : array_like, optional """ def __init__(self, a=None, b=None, c=None, alpha=None, beta=None, gamma=None, a1=None, a2=None, a3=None, cell_matrix=None, orientation_matrix=None, offset=None): if all([p is None for p in (a, b, c, alpha, beta, gamma, a1, a2, a3, cell_matrix)]): errmsg = 'Expected lattice parameters ' + \ '`a`, `b`, `c`, `alpha`, `beta`, `gamma`\n' + \ 'or lattice vectors `a1`, `a2`, `a3`\n' + \ 'or a 3x3 matrix with lattice vectors for columns.' raise ValueError(errmsg) if cell_matrix is not None: cell_matrix = np.asarray(cell_matrix) a1 = np.array(cell_matrix[0, :]) a2 = np.array(cell_matrix[1, :]) a3 = np.array(cell_matrix[2, :]) if all([v is not None for v in (a1, a2, a3)]): a1 = Vector(a1) a2 = Vector(a2) a3 = Vector(a3) a = a1.length b = a2.length c = a3.length alpha = np.degrees(a2.angle(a3)) beta = np.degrees(a3.angle(a1)) gamma = np.degrees(a1.angle(a2)) cell_matrix = np.matrix(np.vstack((np.asarray(a1), np.asarray(a2), np.asarray(a3)))) self._a = a self._b = b self._c = c self._alpha = alpha self._beta = beta self._gamma = gamma if all([p is not None for p in (a, b, c, alpha, beta, gamma)]): self._update_ortho_matrix() super().__init__(nd=3, cell_matrix=cell_matrix, orientation_matrix=orientation_matrix, offset=offset) self.fmtstr = "a={a!r}, b={b!r}, c={c!r}, " + \ "alpha={alpha!r}, beta={beta!r}, gamma={gamma!r}" def __dir__(self): attrs = super().__dir__() attrs.extend(['a', 'b', 'c', 'alpha', 'beta', 'gamma']) return attrs
[docs] def todict(self): """Return `dict` of `Crystal3DLattice` parameters.""" return dict(a=self.a, b=self.b, c=self.c, alpha=self.alpha, beta=self.beta, gamma=self.gamma)
@property def a(self): """Length of lattice vector :math:`\\mathbf{a}`.""" return np.around(self._a, decimals=10) @a.setter def a(self, value): self._a = float(value) self._update_ortho_matrix() @property def b(self): """Length of lattice_vector :math:`\\mathbf{b}`.""" return np.around(self._b, decimals=10) @b.setter def b(self, value): self._b = float(value) self._update_ortho_matrix() @property def c(self): """Length of lattice vector :math:`\\mathbf{c}`.""" return np.around(self._c, decimals=10) @c.setter def c(self, value): self._c = float(value) self._update_ortho_matrix() @property def alpha(self): """Angle between lattice vectors :math:`\\mathbf{b}` and \ :math:`\\mathbf{c}` in **degrees**.""" return np.around(self._alpha, decimals=10) @alpha.setter def alpha(self, value): self._alpha = float(value) self._update_ortho_matrix() @property def beta(self): """Angle between lattice vectors :math:`\\mathbf{a}` and \ :math:`\\mathbf{c}` in **degrees**.""" return np.around(self._beta, decimals=10) @beta.setter def beta(self, value): self._beta = float(value) self._update_ortho_matrix() @property def gamma(self): """Angle between lattice vectors :math:`\\mathbf{a}` and \ :math:`\\mathbf{b}` in **degrees**.""" return np.around(self._gamma, decimals=10) @gamma.setter def gamma(self, value): self._gamma = float(value) self._update_ortho_matrix() @property def a1(self): """Lattice vector :math:`\\mathbf{a}_1=\\mathbf{a}`.""" return Vector(self.cell_matrix[0, :].A.flatten()) @property def a2(self): """Lattice vector :math:`\\mathbf{a}_2=\\mathbf{b}`.""" return Vector(self.cell_matrix[1, :].A.flatten()) @property def a3(self): """Lattice vector :math:`\\mathbf{a}_3=\\mathbf{c}`.""" return Vector(self.cell_matrix[2, :].A.flatten()) @property def cos_alpha(self): """:math:`\\cos\\alpha`""" return np.around(np.cos(np.radians(self.alpha)), decimals=10) @property def cos_beta(self): """:math:`\\cos\\beta`""" return np.around(np.cos(np.radians(self.beta)), decimals=10) @property def cos_gamma(self): """:math:`\\cos\\gamma`""" return np.around(np.cos(np.radians(self.gamma)), decimals=10) @property def sin_alpha(self): """:math:`\\sin\\alpha`""" return np.around(np.sin(np.radians(self.alpha)), decimals=10) @property def sin_beta(self): """:math:`\\sin\\beta`""" return np.around(np.sin(np.radians(self.beta)), decimals=10) @property def sin_gamma(self): """:math:`\\sin\\gamma`""" return np.around(np.sin(np.radians(self.gamma)), decimals=10) @property def cell_volume(self): """Alias for :attr:`~Crystal3DLattice.volume`.""" return self.volume @property def volume(self): """Crystal cell volume.""" return self.a * self.b * self.c * \ np.sqrt(1 - self.cos_alpha ** 2 - self.cos_beta ** 2 - self.cos_gamma ** 2 + 2 * self.cos_alpha * self.cos_beta * self.cos_gamma) @property def ortho_matrix(self): """Transformation matrix to convert from fractional coordinates to \ cartesian coordinates.""" return self._ortho_matrix def _update_ortho_matrix(self): m11 = self.a m12 = self.b * self.cos_gamma m13 = self.c * self.cos_beta m22 = self.b * self.sin_gamma m23 = self.c * (self.cos_alpha - self.cos_beta * self.cos_gamma) / \ self.sin_gamma m33 = self.c * self.sin_alpha * self.sin_beta * self.sin_gamma_star / \ self.sin_gamma self._ortho_matrix = np.matrix([[m11, m12, m13], [0.0, m22, m23], [0.0, 0.0, m33]]) @property def reciprocal_lattice(self): """Reciprocal lattice of this `Crystal3DLattice`.""" return Reciprocal3DLattice( cell_matrix=np.linalg.inv(self.cell_matrix).T) @classmethod
[docs] def triclinic(cls, a, b, c, alpha, beta, gamma): """Generate a triclinic 3D lattice with lattice parameters \ :math:`a, b, c, \\alpha, \\beta, \\gamma`.""" return cls(a=a, b=b, c=c, alpha=alpha, beta=beta, gamma=gamma)
@classmethod
[docs] def monoclinic(cls, a, b, c, beta): """Generate a monoclinic 3D lattice with lattice parameters \ :math:`a, b, c, \\beta`.""" return cls(a=a, b=b, c=c, alpha=90, beta=beta, gamma=90)
@classmethod
[docs] def orthorhombic(cls, a, b, c): """Generate an orthorhombic 3D lattice with lattice parameters \ :math:`a, b, c`.""" return cls(a=a, b=b, c=c, alpha=90, beta=90, gamma=90)
@classmethod
[docs] def tetragonal(cls, a, c): """Generate a tetragonal 3D lattice with lattice parameters \ :math:`a, c`.""" return cls(a=a, b=a, c=c, alpha=90, beta=90, gamma=90)
@classmethod
[docs] def hexagonal(cls, a, c): """Generate a hexagonal 3D lattice with lattice parameters \ :math:`a, c`.""" return cls(a=a, b=a, c=c, alpha=90, beta=90, gamma=120)
@classmethod
[docs] def cubic(cls, a): """Generate a cubic 3D lattice with lattice parameter \ :math:`a`.""" return cls(a=a, b=a, c=a, alpha=90, beta=90, gamma=90)
@property def lengths(self): """Tuple of lattice vector lengths :math:`a, b, c`.""" return self.a, self.b, self.c @property def angles(self): """Tuple of lattice parameter angles \ :math:`\\alpha, \\beta, \\gamma`.""" return self.alpha, self.beta, self.gamma @property def lattice_parameters(self): """Tuple of lattice parameters \ `a`, `b`, `c`, `alpha`, `beta`, `gamma`.""" return self.a, self.b, self.c, self.alpha, self.beta, self.gamma @property def lengths_and_angles(self): """Alias for attr:`Crystal3DLattice.lattice_parameters`.""" return self.lattice_parameters @property def lattice_vectors(self): """Tuple of lattice vectors \ :math:`\\mathbf{a}_1, \\mathbf{a}_2, \\mathbf{a}_3`.""" return self.a1, self.a2, self.a3
CrystalLattice = Crystal3DLattice
[docs]class Reciprocal3DLattice(ReciprocalLatticeBase, DirectLatticeMixin): """3D reciprocal lattice class. Parameters ---------- a_star, b_star, c_star : float alpha_star, beta_star, gamma_star : float b1, b2, b3 : array_like cell_matrix : array_like orientation_matrix : array_like """ def __init__(self, a_star=None, b_star=None, c_star=None, alpha_star=None, beta_star=None, gamma_star=None, b1=None, b2=None, b3=None, cell_matrix=None, orientation_matrix=None, offset=None): direct_lattice = \ Crystal3DLattice(a=a_star, b=b_star, c=c_star, alpha=alpha_star, beta=beta_star, gamma=gamma_star, a1=b1, a2=b2, a3=b3, cell_matrix=cell_matrix, orientation_matrix=orientation_matrix) super().__init__(direct_lattice, nd=3, offset=offset) self.fmtstr = "a_star={a_star!r}, b_star={b_star!r}, " + \ "c_star={c_star!r}, alpha_star={alpha_star!r}, " + \ "beta_star={beta_star!r}, gamma_star={gamma_star!r}" def __dir__(self): attrs = super().__dir__() attrs.extend(['a_star', 'b_star', 'c_star', 'alpha_star', 'beta_star', 'gamma_star']) return attrs @property def a_star(self): """Length of lattice vector :math:`\\mathbf{a^*}`.""" return self._direct_lattice.a @a_star.setter def a_star(self, value): self._direct_lattice.a = value @property def b_star(self): """Length of lattice_vector :math:`\\mathbf{b^*}`.""" return self._direct_lattice.b @b_star.setter def b_star(self, value): self._direct_lattice.b = value @property def c_star(self): """Length of lattice vector :math:`\\mathbf{c^*}`.""" return self._direct_lattice.c @c_star.setter def c_star(self, value): self._direct_lattice.c = value @property def alpha_star(self): """Angle between lattice vectors :math:`\\mathbf{b}^{\\ast}` and \ :math:`\\mathbf{c}^{\\ast}` in **degrees**.""" return self._direct_lattice.alpha @alpha_star.setter def alpha_star(self, value): self._direct_lattice.alpha = value @property def beta_star(self): """Angle between lattice vectors :math:`\\mathbf{c}^{\\ast}` and \ :math:`\\mathbf{a}^{\\ast}` in **degrees**.""" return self._direct_lattice.beta @beta_star.setter def beta_star(self, value): self._direct_lattice.beta = value @property def gamma_star(self): """Angle between lattice vectors :math:`\\mathbf{a}^{\\ast}` and \ :math:`\\mathbf{b}^{\\ast}` in **degrees**.""" return self._direct_lattice.gamma @gamma_star.setter def gamma_star(self, value): self._direct_lattice.gamma = value @property def b1(self): """Lattice vector :math:`\\mathbf{b}_1=\\mathbf{a}^{\\ast}`.""" return self._direct_lattice.a1 @property def b2(self): """Lattice vector :math:`\\mathbf{b}_2=\\mathbf{b}^{\\ast}`.""" return self._direct_lattice.a2 @property def b3(self): """Lattice vector :math:`\\mathbf{b}_3=\\mathbf{c}^{\\ast}`.""" return self._direct_lattice.a3 @property def cos_alpha_star(self): """:math:`\\cos\\alpha^{\\ast}`""" return self._direct_lattice.cos_alpha @property def cos_beta_star(self): """:math:`\\cos\\beta^{\\ast}`""" return self._direct_lattice.cos_beta @property def cos_gamma_star(self): """:math:`\\cos\\gamma^{\\ast}`""" return self._direct_lattice.cos_gamma @property def sin_alpha_star(self): """:math:`\\sin\\alpha^{\\ast}`""" return self._direct_lattice.sin_alpha @property def sin_beta_star(self): """:math:`\\sin\\beta^{\\ast}`""" return self._direct_lattice.sin_beta @property def sin_gamma_star(self): """:math:`\\sin\\gamma^{\\ast}`""" return self._direct_lattice.sin_gamma @property def reciprocal_lattice(self): """Reciprocal lattice of this `Reciprocal3DLattice`.""" return Crystal3DLattice(cell_matrix=np.linalg.inv(self.cell_matrix).T) @classmethod
[docs] def triclinic(cls, a_star, b_star, c_star, alpha_star, beta_star, gamma_star): """Generate a triclinic 3D lattice with lattice parameters \ :math:`a^*, b^*, c^*, \\alpha^*, \\beta^*, \\gamma^*`.""" return cls(a_star=a_star, b_star=b_star, c_star=c_star, alpha_star=alpha_star, beta_star=beta_star, gamma_star=gamma_star)
@classmethod
[docs] def monoclinic(cls, a_star, b_star, c_star, beta_star): """Generate a monoclinic 3D lattice with lattice parameters \ :math:`a^*, b^*, c^*, \\beta^*`.""" return cls(a_star=a_star, b_star=b_star, c_star=c_star, alpha_star=90, beta_star=beta_star, gamma_star=90)
@classmethod
[docs] def orthorhombic(cls, a_star, b_star, c_star): """Generate an orthorhombic 3D lattice with lattice parameters \ :math:`a^*, b^*, c^*`.""" return cls(a_star=a_star, b_star=b_star, c_star=c_star, alpha_star=90, beta_star=90, gamma_star=90)
@classmethod
[docs] def tetragonal(cls, a_star, c_star): """Generate a tetragonal 3D lattice with lattice parameters \ :math:`a^*, c^*`.""" return cls(a_star=a_star, b_star=a_star, c_star=c_star, alpha_star=90, beta_star=90, gamma_star=90)
@classmethod
[docs] def hexagonal(cls, a_star, c_star): """Generate a hexagonal 3D lattice with lattice parameters \ :math:`a^*, c^*`.""" return cls(a_star=a_star, b_star=a_star, c_star=c_star, alpha_star=90, beta_star=90, gamma_star=120)
@classmethod
[docs] def cubic(cls, a_star): """Generate a cubic 3D lattice with lattice parameter \ :math:`a^*`.""" return cls(a_star=a_star, b_star=a_star, c_star=a_star, alpha_star=90, beta_star=90, gamma_star=90)
[docs] def todict(self): """Return `dict` of `Reciprocal3DLattice` parameters.""" return dict(a_star=self.a_star, b_star=self.b_star, c_star=self.c_star, alpha_star=self.alpha_star, beta_star=self.beta_star, gamma_star=self.gamma_star)
@property def lengths(self): """Tuple of lattice vector lengths :math:`a^*, b^*, c^*`.""" return self.a_star, self.b_star, self.c_star @property def angles(self): """Tuple of lattice parameter angles \ :math:`\\alpha^*, \\beta^*, \\gamma^*`.""" return self.alpha_star, self.beta_star, self.gamma_star @property def lattice_parameters(self): """Tuple of lattice parameters \ `a^*`, `b^*`, `c^*`, `alpha^*`, `beta^*`, `gamma^*`.""" return self.a_star, self.b_star, self.c_star, \ self.alpha_star, self.beta_star, self.gamma_star @property def lattice_vectors(self): """Tuple of lattice vectors \ :math:`\\mathbf{b}_1, \\mathbf{b}_2, \\mathbf{b}_3`.""" return self.b1, self.b2, self.b3
ReciprocalLattice = Reciprocal3DLattice # class Bravais3DLattice: # """Class for bravais lattices.""" # def __init__(self, crystal_system=None, centering=None, # symbol=None): # pass # BravaisLattice = Bravais3DLattice # class SimpleCubicLattice(BravaisLattice): # """Abstract representation of simple cubic lattice.""" # lattice_points = [[0.0, 0.0, 0.0]] # class BodyCenteredCubicLattice(BravaisLattice): # """Abstract representation of body-centered cubic lattice.""" # lattice_points = [[0.0, 0.0, 0.0], # [0.5, 0.5, 0.5]] # class FaceCenteredCubicLattice(BravaisLattice): # """Abstract representation of face-centered cubic lattice.""" # lattice_points = [[0.0, 0.0, 0.0], # [0.5, 0.5, 0.0], # [0.0, 0.5, 0.5], # [0.5, 0.0, 0.5]]