Source code for ccu.adsorption.adsorbateorientation

"""This module defines the AdsorbateOrientation and AdsorbateOrientationFactory
classes."""

from collections.abc import Iterable
from collections.abc import Sequence

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

from ccu.adsorption import sitefinder
from ccu.structure import axisfinder
from ccu.structure import symmetry


# pylint:disable=too-few-public-methods
[docs] class AdsorbateOrientation: """An orientation of an adsorbate. An AdsorbateOrientation object contains the information required to unambiguously orient an adsorbate in space. Attributes: description: A string describing the adsorbate orientation. vectors: A tuple of numpy.array instances which are the vectors along which an adsorbate will be oriented. The sequence should contain two linearly independent unit vectors. The first vector is the primary orientation axis. The secondary vector is secondary orientation axis. """ def __init__( self, description: str, orientation_vectors: Sequence[np.array] ) -> None: self.description = description self.vectors = orientation_vectors
[docs] class AdsorbateOrientationFactory: """An AdsorbateOrientation factory. An AdsorbateOrientationFactory creates a collection of AdsorbateOrientation objects for a given AdsorptionSite subject to symmetry and orientation specifications. Attributes: site: A sitefinder.AdsorptionSite instance indicating site for which the orientations are to be created. adsorbate: An ase.Atoms instance representing the adsorbate which will assume the orientations. force_symmetry: A boolean indicating whether or not to force the adsorbate to be treated as symmetric. vertical: A boolean indicating whether or not vertical orientations will be created. """ def __init__( self, site: sitefinder.AdsorptionSite, adsorbate: ase.Atoms, force_symmetry: bool = False, vertical: bool = False, ) -> None: self.site = site self.adsorbate = adsorbate self.force_symmetry = force_symmetry self.vertical = vertical
[docs] def create_orientations(self) -> list[AdsorbateOrientation]: """Creates a list of AdsorbateOrientation objects. Returns: A list of AdsorbateOrientation objects. """ orientations = [] # Single orientation for zero dimensional adsorbate if norm(axisfinder.find_primary_axis(self.adsorbate)) == 0: vectors = (self.site.alignments[0].vector, self.site.surface_norm) orientation = AdsorbateOrientation("", vectors) return [orientation] for alignment in self.site.alignments: orientation = AdsorbateOrientation( f"{alignment.description} 1", [alignment.vector, self.site.surface_norm], ) if self.force_symmetry: orientations.append(orientation) else: orientations.extend( self._create_asymmetric_orientations( alignment, orientation ) ) if self.vertical: orientations.extend(self._create_vertical_orientations()) return orientations
def _create_asymmetric_orientations( self, alignment: sitefinder.SiteAlignment, orientation: AdsorbateOrientation, ) -> list[AdsorbateOrientation]: """Creates a list of asymmetric orientations. Args: alignment: A sitefinder.SiteAlignment instance representing the alignment for the adsorption site. orientation: An AdsorbateOrientation instance representing the adsorbate orientation to be combined with the newly created orientations. Returns: The list of AdsorbateOrientation instances representing adsorption sites. """ primary_axis = axisfinder.find_primary_axis(self.adsorbate) orientations = [orientation] for i in range(1, 4): rotation_ = symmetry.Rotation(i * 90, primary_axis) symmetry_ = symmetry.RotationSymmetry(rotation_) if not symmetry_.check_symmetry(self.adsorbate): rot_vec = i * 90 * alignment.vector matrix = transform.Rotation.from_rotvec(rot_vec, degrees=True) vec = matrix.apply(self.site.surface_norm) orientations.append( AdsorbateOrientation( f"{alignment.description} {i + 1}", [alignment.vector, vec], ) ) return orientations + self._create_reverse_orientations( alignment, orientations ) def _create_vertical_orientations(self) -> list[AdsorbateOrientation]: """Creates the vertical adsorbate orientations for a site. Returns: The list of AdsorbateOrientation instances representing the vertical adsorbate orientations. """ orientations = [] secondary_axis = axisfinder.find_secondary_axis(self.adsorbate) no_secondary_axis = norm(secondary_axis) == 0 for i, alignment in enumerate(self.site.alignments): if i != 0 and (no_secondary_axis or self.force_symmetry): break orientations.append( AdsorbateOrientation( f"vertical {i+1}", [self.site.surface_norm, alignment.vector], ) ) reverse_orientations = [] if not self.force_symmetry: reverse_orientations.extend( self._create_reverse_orientations( (self.site.surface_norm, "vertical"), orientations ) ) return orientations + reverse_orientations def _create_reverse_orientations( self, alignment: sitefinder.SiteAlignment | tuple, orientations: Iterable[AdsorbateOrientation], ) -> list[AdsorbateOrientation]: """Creates the reverse of the given adsorbate orientations subject to adsorbate symmetry. Args: alignment: The alignment for the site. This can be supplied as either a sitefinder.SiteAlignment instance or as a two-element tuple whose first and second elements are the alignment vector and description, respectively. orientations: An iterable of AdsorbateOrientation instances representing the adsorbate orientations to be reversed. Returns: A list of AdsorbateOrientation instances representing the reversed adsorbate orientations. """ if isinstance(alignment, sitefinder.SiteAlignment): vector = alignment.vector description = alignment.description else: vector, description = alignment secondary_axis = axisfinder.find_secondary_axis(self.adsorbate) # Deal with linear adsorbates if norm(secondary_axis) == 0: primary_axis = axisfinder.find_primary_axis(self.adsorbate) secondary_axis = cross(primary_axis, [1, 0, 0]) if norm(secondary_axis) == 0: secondary_axis = cross(primary_axis, [0, 1, 0]) secondary_axis = secondary_axis / norm(secondary_axis) rotation_ = symmetry.Rotation(180, secondary_axis) symmetry_ = symmetry.RotationSymmetry(rotation_) reverse_orientations = [] if not symmetry_.check_symmetry(self.adsorbate): vec = -vector start = len(orientations) for i, orientation in enumerate(orientations): reverse_orientations.append( AdsorbateOrientation( f"{description} {start + i + 1}", [vec, orientation.vectors[1]], ) ) return reverse_orientations