"""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