"""Geometry-related functions for atomic structures."""fromcollections.abcimportIterablefromcollections.abcimportSequencefromitertoolsimportcombinationsfromitertoolsimportproductimportmathfromtypingimportTYPE_CHECKINGfromtypingimportNamedTupleimportasefromase.atomsimportAtomsimportnumpyasnpfromnumpy.linalgimportnormfromnumpy.typingimportNDArrayfromccu.structure.axisfinderimportfind_primary_axisfromccu.structure.axisfinderimportfind_secondary_axisifTYPE_CHECKING:fromnumpy.typingimportNDArray
[docs]defcalculate_separation(structure1:ase.Atoms,structure2:ase.Atoms)->float:"""Calculates the separation between two Atoms instances. The distance is defined as the smallest distance between an atom in one structure and an atom in the second structure. Args: structure1: An :class:`~ase.Atoms` instance. structure2: An :class:`~ase.Atoms` instance. Returns: A float representing the separation between the two structures. """minimum_separation=math.infstructures=product(structure1.positions,structure2.positions)forposition1,position2instructures:separation=norm(position1-position2)minimum_separation=np.min([minimum_separation,separation])returnminimum_separation
[docs]defcalculate_norm(points:"list[NDArray[np.floating]]",*,reverse:bool=False,)->"NDArray[np.floating]":"""Calculate the norm for the *average* plane defined by a set of points. Args: points: A list of points on a surface. These points should be approximately coplanar. reverse: Whether or not to reverse the preferred direction for the norm. Defaults to False in which case the norm direction is determined as follows. If the vector does not lie in the xy-plane, then the norm is normalized to have positive z. Otherwise, if the vector has a y component, then it is normalized to have positive y. and if it has no y component, then it is normalized to have positive x. If True, then the norm is normalized to have negative z in the above cases. Returns: A length 3, 1D :class:`numpy.ndarray` representing the unit normal vector for the *average* plane defined by `points`. Raises: ValueError: Less than three non-colinear points provided. """iflen(points)<2:# noqa: PLR2004msg="Unable to calculate cross product for less than 2 vectors"raiseValueError(msg)norms:list[NDArray[np.floating]]=[]forp1,p2,p3incombinations(points,r=3):vec1=p1-p2vec2=p1-p3n=np.cross(vec1,vec2)if(n==0.0).all():continuen/=norm(n)if(n[2]<0or(n[2]==0andn[1]<0)or(n[1]==n[2]==0andn[0]<0)):n*=-1.0ifreverse:n*=-1.0norms.append(n)iflen(norms)==0:msg=f"Unable to find non-colinear lines between points: {points}"raiseValueError(msg)returnnp.mean(norms,axis=0)
[docs]classMolecularOrientation(NamedTuple):r"""The orientation of a molecule. A :class:`~ccu.structure.geometry.MolecularOrientation` tuple contains the information required to unambiguously orient a molecule in space, for example, on an :class:`~ccu.adsorption.sites.AdsorptionSite`. Attributes: directions: A 2-tuple of length 3, 1D, :class:`numpy.ndarrays <numpy.ndarray>` representing the primary and secondary orientation directions. These directions orient an adsorbate in |site space|_. description: A string describing the adsorbate orientation. .. |site space| replace:: **site space** .. _site space: :ref:`site-space` """directions:"tuple[NDArray[np.floating], NDArray[np.floating]]"description:str
[docs]defalign(adsorbate:Atoms,directions:Sequence[Iterable[float]],center:Iterable[float]|None=None,)->Atoms:"""Align a molecule according to its primary and secondary axes. Args: adsorbate: An :class:`~ase.Atoms` representing a molecule. directions: A sequence whose first and second elements are length 3, iterables of floats representing the primary and secondary directions along which `adsorbate` is to be aligned. center: A point to remain fixed while aligning `adsorbate`. Defaults to the center-of-mass of `adsorbate`. Returns: A copy of `adsorbate` aligned such that its primary and secondary axes coincide with `directions[0]` and `directions[1]`, respectively. """center=np.array(adsorbate.get_center_of_mass()ifcenterisNoneelsecenter)v1=np.array(directions[0])v2=np.array(directions[1])new_adsorbate=adsorbate.copy()axis1=find_primary_axis(new_adsorbate)# No first orientation for zero-dimensional moleculeifnp.linalg.norm(axis1)==0:returnnew_adsorbate# Orient along primary orientation axisnew_adsorbate.rotate(axis1,v1,center)# Orient using secondary orientation axisaxis2=find_secondary_axis(new_adsorbate)# No second orientation for one-dimensional moleculeifnp.linalg.norm(axis2)==0:returnnew_adsorbateparallel_component=v1@v2*v1perpendicular_component=v2-parallel_componentnew_adsorbate.rotate(axis2,perpendicular_component,center)returnnew_adsorbate