Source code for polyhedral_analysis.symmetry_measure

from __future__ import annotations

import numpy as np

from polyhedral_analysis.csm import continuous_symmetry_measure
from polyhedral_analysis.reference_geometries import get_reference_geometry


def _compute_reduced_permutations(reference_points: np.ndarray) -> np.ndarray:
    """Compute symmetry-inequivalent permutation representatives for reference points.

    Uses bsym to find the point group of the reference geometry and enumerate
    one representative permutation per orbit under the symmetry group action.

    Args:
        reference_points: An Nx3 array of reference coordinates.

    Returns:
        An MxN integer array where each row is a permutation of [0, ..., N-1]
        and M = N! / |G|, where |G| is the size of the symmetry group.

    Note:
        The point group detection relies on pymatgen's PointGroupAnalyzer
        (via bsym). A known bug (materialsproject/pymatgen#4596) causes
        under-detection of symmetry for the Square-face capped trigonal prism,
        resulting in more representatives than strictly necessary. This does
        not affect correctness.
    """
    from pymatgen.core import Molecule
    from bsym.interface.pymatgen import point_group_from_molecule  # type: ignore[import]
    from bsym import ConfigurationSpace  # type: ignore[import]

    n = len(reference_points)
    mol = Molecule(['H'] * n, reference_points)
    point_group = point_group_from_molecule(mol)
    config_space = ConfigurationSpace(
        objects=list(range(n)),
        symmetry_group=point_group,
    )
    site_distribution = {i: 1 for i in range(n)}
    unique_configs = config_space.unique_configurations(
        site_distribution=site_distribution,
    )
    return np.array(
        [c.tolist() for c in unique_configs],
        dtype=np.intp,
    )


[docs] class SymmetryMeasure: """Continuous symmetry measure calculator for a reference geometry. Wraps a set of ideal reference points and computes the minimum continuous symmetry measure (CSM) over all symmetry-inequivalent vertex permutations. The permutations are computed lazily on first use via :mod:`bsym`. Args: reference_points: An Nx3 array of ideal vertex coordinates for the reference geometry. name: A human-readable name for this geometry (e.g. ``'Octahedron'``). """ def __init__(self, reference_points: np.ndarray, name: str) -> None: self.name = name self.reference_points = reference_points self._permutations: np.ndarray | None = None @property def permutations(self) -> np.ndarray: """Symmetry-inequivalent index permutations for this geometry. Computed lazily on first access from the reference points using bsym. """ if self._permutations is None: self._permutations = _compute_reduced_permutations(self.reference_points) return self._permutations
[docs] def minimum_symmetry_measure(self, distorted_points: np.ndarray) -> float: """Compute the minimum CSM of distorted points against this reference. Iterates over all symmetry-inequivalent permutations and returns the smallest continuous symmetry measure. Args: distorted_points: An Nx3 array of vertex coordinates to compare against the reference geometry. Returns: The minimum symmetry measure value. A value of 0 indicates perfect agreement with the reference geometry. """ return min( continuous_symmetry_measure(distorted_points[perm], self.reference_points).symmetry_measure for perm in self.permutations )
[docs] @classmethod def from_name(cls, name: str) -> SymmetryMeasure: """Construct a SymmetryMeasure from a named reference geometry. Args: name: The name of the reference geometry (e.g. ``'Octahedron'``, ``'Tetrahedron'``). See :func:`~polyhedral_analysis.reference_geometries.get_reference_geometry` for available names. Returns: A new SymmetryMeasure instance. """ reference_points = get_reference_geometry(name) return cls(reference_points, name)
symmetry_measures_to_construct = {4: ['Tetrahedron'], 5: ['Trigonal bipyramid', 'Square pyramid'], 6: ['Octahedron', 'Trigonal prism'], 7: ['Pentagonal bipyramid', 'Square-face capped trigonal prism', 'Face-capped octahedron'], 8: ['Cube', 'Square antiprism', 'Square-face bicapped trigonal prism', 'Triangular-face bicapped trigonal prism', 'Dodecahedron with triangular faces', 'Hexagonal bipyramid', 'Bicapped octahedron (opposed cap faces)', 'Bicapped octahedron (cap faces with one atom in common)', 'Bicapped octahedron (cap faces with one edge in common)']} symmetry_measures_from_coordination: dict[int, dict[str, SymmetryMeasure]] = {} for coordination_number, names in symmetry_measures_to_construct.items(): symmetry_measures_from_coordination[coordination_number] = {} for name in names: symmetry_measures_from_coordination[coordination_number][name] = SymmetryMeasure.from_name(name)