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)