Source code for polyhedral_analysis.octahedral_analysis

from polyhedral_analysis.coordination_polyhedron import CoordinationPolyhedron
from polyhedral_analysis.atom import Atom
import numpy as np # type: ignore

# Functions for analysing octahedra

[docs] def check_octahedra(polyhedron: CoordinationPolyhedron) -> None: """ Check whether a polyhedron is considered an octahedron. Args: polyhedron (:obj:`CoordinationPolyhedron`): The :obj:`CoordinationPolyhedron` to be tested. Returns: None Raises: ValueError: If the "best fit" geometry (lowest continuous symmetry measure) is not an octahedron, a ValueError is raised. """ if polyhedron.best_fit_geometry['geometry'] != 'Octahedron': raise ValueError( 'This polyhedron is not recognised a an octahedron' )
[docs] def adjacent_vertex_pairs(polyhedron: CoordinationPolyhedron, check: bool = True) -> list[tuple[Atom, Atom]]: """ Find pairs of adjacent vertices in an octahedral polyhedron. Args: polyhedron (CoordinationPolyhedron): The polyhedron to be analysed. check (bool, optional): Whether to check if the polyhedron is octahedral. Default is True. Returns: list[tuple[Atom, Atom]]: A list of tuples, each containing a pair of adjacent Atom objects. Raises: ValueError: If the polyhedron is not recognized as an octahedron. """ if check: check_octahedra(polyhedron) edge_indices = polyhedron.edge_vertex_indices() adjacent_pairs = [] for i, j in edge_indices: atom_i = next(v for v in polyhedron.vertices if v.index == i) atom_j = next(v for v in polyhedron.vertices if v.index == j) adjacent_pairs.append((atom_i, atom_j)) return adjacent_pairs
[docs] def opposite_vertex_pairs(polyhedron: CoordinationPolyhedron, check: bool = True) -> list[tuple[Atom, Atom]]: """For an octahedral polyhedron, find the pairs of vertices opposite each other. Args: polyhedron (:obj:`CoordinationPolyhedron`): The polyhedron to be analysed. check: (Optional, :obj:`bool`): Optional flag to set whether to check that this polyhedron is an octahedron. Default is `True`. Returns: (tuple): 3 pairs of vertex atoms. """ if check: check_octahedra(polyhedron) assert {len(n) for n in polyhedron.edge_graph.values()} == {4}, "Edge graph does not describe an octahedron." vertex_pairs = [] seen_indices: list[int] = [] for v1 in polyhedron.vertices: if v1.index in seen_indices: continue v1_neighbours = polyhedron.vertices_by_indices(v1.neighbours[polyhedron.index]) v2 = next(v for v in polyhedron.vertices if v not in [v1, *v1_neighbours]) vertex_pairs.append((v1, v2)) seen_indices.extend([v1.index, v2.index]) return [vertex_pairs[0], vertex_pairs[1], vertex_pairs[2]]
[docs] def opposite_vertex_distances(polyhedron: CoordinationPolyhedron, check: bool = True) -> tuple[float, float, float]: """For an octahedral polyhedron, return the distances between pairs of opposite (trans) vertices. Args: polyhedron (:obj:`CoordinationPolyhedron`): The polyhedron to be analysed. check: (Optional, :obj:`bool`): Optional flag to set whether to check that this polyhedron is an octahedron. Default is `True`. Returns: (tuple): a length-3 tuple of distances. """ distances = tuple(vp[0].distance(vp[1]) for vp in opposite_vertex_pairs(polyhedron, check=check)) return (distances[0], distances[1], distances[2])
[docs] def trans_vertex_vectors(polyhedron: CoordinationPolyhedron, check: bool = True) -> list[np.ndarray]: """For an octahedral polyhedron, return the vectors between pairs of trans vertices. Args: polyhedron (:obj:`CoordinationPolyhedron`): The polyhedron to be analysed. check (Optional, :obj:`bool`): Optional flag to set whether to check that this polyhedron is an octahedron. Default is ``True``. Returns: list[np.ndarray]: A list of three numpy arrays, each representing a vector between a pair of trans vertices. Raises: ValueError: If the polyhedron is not recognized as an octahedron. """ if check: check_octahedra(polyhedron) vertex_pairs = opposite_vertex_pairs(polyhedron, check=False) vectors = [polyhedron.vertex_vectors()[polyhedron.vertex_internal_index_from_global_index(v2.index)] - polyhedron.vertex_vectors()[polyhedron.vertex_internal_index_from_global_index(v1.index)] for v1, v2 in vertex_pairs] return vectors
[docs] def isomer_is_trans(polyhedron: CoordinationPolyhedron, check: bool = True) -> bool: """For an octahedral polyhedron with 2+4 coordination, return True for trans coordination, and False for cis coordination. Args: polyhedron (:obj:`CoordinationPolyhedron`): The polyhedron to be analysed. check: (Optional, :obj:`bool`): Optional flag to set whether to check that this polyhedron is an octahedron. Default is `True`. Return: (bool): `True` for trans, `False` for cis. """ coord = list(polyhedron.vertex_count.values()) if sorted(coord) != [2, 4]: raise ValueError(f'cis/trans not defined for {polyhedron.vertex_count.values()} coordination.') to_return = bool(np.all([vp[0].label == vp[1].label for vp in opposite_vertex_pairs(polyhedron, check=check)])) return to_return
[docs] def isomer_is_cis(polyhedron: CoordinationPolyhedron, check: bool = True) -> bool: """For an octahedral polyhedron with 2+4 coordination, return True for cis coordination, and False for trans coordination. Args: polyhedron (:obj:`CoordinationPolyhedron`): The polyhedron to be analysed. check: (Optional, :obj:`bool`): Optional flag to set whether to check that this polyhedron is an octahedron. Default is `True`. Return: (bool): `True` for cis, `False` for trans. """ return not isomer_is_trans(polyhedron, check=check)
[docs] def isomer_is_fac(polyhedron: CoordinationPolyhedron, check: bool = True) -> bool: """For an octahedral polyhedron with 3+3 coordination, return True for fac coordination, and False for mer coordination. Args: polyhedron (:obj:`CoordinationPolyhedron`): The polyhedron to be analysed. check: (Optional, :obj:`bool`): Optional flag to set whether to check that this polyhedron is an octahedron. Default is `True`. Return: (bool): `True` for fac, `False` for mer. """ coord = list(polyhedron.vertex_count.values()) if sorted(coord) != [3, 3]: raise ValueError(f'fac/mer not defined for {polyhedron.vertex_count.values()} coordination.') to_return = bool(np.all([vp[0].label != vp[1].label for vp in opposite_vertex_pairs(polyhedron, check=check)])) return to_return
[docs] def isomer_is_mer(polyhedron: CoordinationPolyhedron, check: bool = True) -> bool: """For an octahedral polyhedron with 3+3 coordination, return True for mer coordination, and False for fac coordination. Args: polyhedron (:obj:`CoordinationPolyhedron`): The polyhedron to be analysed. check: (Optional, :obj:`bool`): Optional flag to set whether to check that this polyhedron is an octahedron. Default is `True`. Return: (bool): `True` for fac, `False` for mer. """ return not isomer_is_fac(polyhedron, check=check)
[docs] def trans_vector_orthogonality(polyhedron: CoordinationPolyhedron, check: bool = True) -> tuple[float, float, float]: """ For each trans pair vector, calculate the smallest angle between the vector and the plane defined by the other two vectors. Args: polyhedron (CoordinationPolyhedron): The polyhedron to be analysed. check (bool, optional): Optional flag to set whether to check that this polyhedron is an octahedron. Default is True. Returns: tuple[float, float, float]: A tuple containing the three smallest angles (in degrees) between each vector and the plane defined by the other two vectors. Raises: ValueError: If the polyhedron is not recognized as an octahedron. """ if check: check_octahedra(polyhedron) trans_vectors: list[np.ndarray] = trans_vertex_vectors(polyhedron, check=False) angles = [] for i in range(3): v1 = trans_vectors[i] v2 = trans_vectors[(i+1)%3] v3 = trans_vectors[(i+2)%3] # Calculate the normal vector to the plane containing v2 and v3 normal = np.cross(v2, v3) normal = normal / np.linalg.norm(normal) # Normalize the normal vector # Calculate the angle between v1 and the normal v1_normalized = v1 / np.linalg.norm(v1) dot_product = np.dot(normal, v1_normalized) angle = np.arccos(np.clip(abs(dot_product), -1.0, 1.0)) angle_degrees = np.degrees(angle) angles.append(angle_degrees) return tuple(angles)