Source code for ccu.structure.symmetry

"""This class defines the SymmetryOperation and Symmetry classes and
subclasses.

Symmetry and SymmetryOperation subclasses can be used as follows:

>>> import ase
>>> from ccu.structure.symmetry import Rotation, RotationSymmetry
>>> rotation1 = Rotation(90, [0, 0, 1])
>>> symmetry1 = RotationSymmetry(rotation1)
>>> co = ase.Atoms('CO', positions=[[0, 0, 0], [1, 0, 0]])
>>> rotated = rotation1.transform(co)
>>> rotated.positions
array([[0.000000e+00, 0.000000e+00, 0.000000e+00],
       [6.123234e-17, 1.000000e+00, 0.000000e+00]])
>>> symmetry1.check_symmetry(co)
False
>>> h2 = ase.Atoms('HH', positions=[[0, 0, 0], [1, 0, 0]])
>>> rotation2 = Rotation(180, [0, 0, 1])
>>> symmetry2 = RotationSymmetry(rotation2)
>>> symmetry2.check_symmetry(h2)
True
"""

import abc
from collections.abc import Iterable

import ase
import numpy as np
from numpy.linalg import norm
from scipy.spatial import transform

from ccu.structure import comparator


[docs] class SymmetryOperation(abc.ABC): """An abstract base class for symmetry operations."""
[docs] @abc.abstractmethod def transform(self, structure: ase.Atoms) -> ase.Atoms: "Subclasses should override this method."
[docs] class Rotation(SymmetryOperation): """A rotation operation. Attributes: angle: A float specifying a rotation angle in degrees. axis: A numpy.array representing the axis of rotation. """ def __init__(self, angle: float, axis: Iterable[float]) -> None: self.angle = angle self.axis = np.array(axis)
[docs] def transform(self, structure: ase.Atoms) -> ase.Atoms: """Rotates the given structure by the angle and about the axis specified as attributes of the Rotation object. Args: structure: An ase.Atoms instance representing structure to be rotated. Returns: A rotated copy of the original ase.Atoms instance. """ new_structure = structure.copy() new_structure.rotate(self.angle, self.axis) return new_structure
[docs] def as_matrix(self) -> np.ndarray: """Returns the rotation operation of this instance as a numpy.ndarray which represents the rotation matrix. """ rotvec = self.angle * (self.axis / norm(self.axis)) rotation = transform.Rotation.from_rotvec(rotvec, degrees=True) return rotation.as_matrix()
[docs] class Inversion(SymmetryOperation): pass
[docs] class Symmetry(abc.ABC): """An abstract base class for molecule symmetries.""" @property @abc.abstractmethod def operation(self) -> SymmetryOperation: "Subclasses should override this method."
[docs] @abc.abstractmethod def check_symmetry(self, structure: ase.Atoms, tol: float) -> bool: "Subclasses should override this method."
[docs] class RotationSymmetry(Symmetry): """A rotational symmetry.""" def __init__(self, operation: Rotation) -> None: self._operation = operation @property def operation(self) -> Rotation: """The Rotation instance associated with this RotationSymmetry instance.""" return self._operation
[docs] def check_symmetry(self, structure: ase.Atoms, tol: float = 5e-2) -> bool: """Determines the symmetry represented by the instance belongs to the given structure. Args: structure: An ase.Atoms instance representing the structure whose symmetry is to be determined. tol: A float specifying the absolute tolerance for positions. Defaults to 5e-2. Returns: A boolean indicating whether or not the given structure possesses the symmetry of the RotationSymmetry object subject to the specified tolerance. """ old_structure = structure # Rotate structure rotated_structure = self._operation.transform(structure) # Check for similarity wrt. tolerance return comparator.Comparator.check_similarity( old_structure, rotated_structure, tol=tol )