"""Dynamic Voronoi site representation for crystal structure analysis.
This module provides the DynamicVoronoiSite class, which represents a site with
a centre that is dynamically calculated from the positions of a set of reference
atoms.
"""
from __future__ import annotations
import numpy as np
from .site import Site
from .atom import Atom
from site_analysis.pbc_utils import correct_pbc
[docs]
class DynamicVoronoiSite(Site):
"""Site subclass corresponding to Voronoi cells with centres dynamically
calculated from the positions of sets of reference atoms.
Unlike standard VoronoiSite objects, which have fixed centres, the positions
of DynamicVoronoiSite objects adapt to structural changes as the reference
atoms move. The site centre is calculated as the mean position of the reference
atoms, with special handling for periodic boundary conditions.
This makes DynamicVoronoiSite particularly useful for tracking sites in mobile
frameworks where the crystal structure deforms during simulation.
Similar to standard Voronoi sites, a single DynamicVoronoiSite cannot determine
whether an atom is contained within it, as this depends on the positions of all
other sites. The coordination number of a DynamicVoronoiSite is defined as the
number of reference atoms used to calculate its centre.
Attributes:
reference_indices (list[int]): Indices of atoms used as reference to calculate
the dynamic centre of the site.
centre_coords (np.ndarray or None): Fractional coordinates of the dynamically
calculated site centre. This is None initially and calculated on demand.
See Also:
:class:`~site_analysis.site.Site`: Parent class documenting inherited attributes
(index, label, contains_atoms, trajectory, points, transitions, average_occupation).
Important:
This class is designed to be used with
:class:`~site_analysis.dynamic_voronoi_site_collection.DynamicVoronoiSiteCollection`,
which handles the Voronoi tessellation logic.
"""
def __init__(self,
reference_indices: list[int],
label: str | None = None,
reference_center: np.ndarray | None = None) -> None:
"""Create a ``DynamicVoronoiSite`` instance.
Args:
reference_indices: List of atom indices whose positions will be used to dynamically calculate the centre of this site.
label: Optional label for this site.
reference_center: Optional reference centre for PBC handling.
Returns:
None
"""
super(DynamicVoronoiSite, self).__init__(label=label)
self.reference_indices = reference_indices
self._centre_coords: np.ndarray | None = None
self.reference_center = reference_center
def __repr__(self) -> str:
string = ('site_analysis.DynamicVoronoiSite('
f'index={self.index}, '
f'label={self.label}, '
f'reference_indices={self.reference_indices}, '
f'contains_atoms={self.contains_atoms})')
return string
[docs]
def reset(self) -> None:
"""Reset the site state.
Clears the calculated centre coordinates and resets the site occupation data.
Args:
None
Returns:
None
"""
super(DynamicVoronoiSite, self).reset()
self._centre_coords = None
[docs]
def calculate_centre(self,
all_frac_coords: np.ndarray,
lattice_matrix: np.ndarray) -> None:
"""Calculate the centre of this site from reference atom positions.
Handles periodic boundary conditions and wraps the result into
[0, 1).
Args:
all_frac_coords: Full fractional coordinate array from the
structure, shape ``(n_atoms, 3)``.
lattice_matrix: (3, 3) lattice matrix where rows are lattice
vectors.
"""
ref_coords = all_frac_coords[self.reference_indices]
corrected, _ = correct_pbc(ref_coords, self.reference_center, lattice_matrix)
self._centre_coords = np.mean(corrected, axis=0) % 1.0
@property
def centre(self) -> np.ndarray:
"""Returns the centre position of this site.
This method returns the cached centre coordinates or raises an error
if they haven't been calculated yet.
Args:
None
Returns:
np.ndarray: Fractional coordinates of the site centre.
Raises:
RuntimeError: If the centre coordinates have not been calculated yet.
"""
if self._centre_coords is None:
raise RuntimeError("Centre coordinates for this DynamicVoronoiSite have not been calculated yet.")
return self._centre_coords
[docs]
def contains_point(self,
x: np.ndarray) -> bool:
"""A single dynamic Voronoi site cannot determine whether it contains a given point,
because the site boundaries are defined by the set of all dynamic Voronoi sites.
Use DynamicVoronoiSiteCollection.assign_site_occupations() instead.
"""
raise NotImplementedError
[docs]
def as_dict(self) -> dict:
"""Json-serializable dict representation of this DynamicVoronoiSite.
Args:
None
Returns:
dict: dictionary representation of this site.
"""
d = super(DynamicVoronoiSite, self).as_dict()
d['reference_indices'] = self.reference_indices
if self._centre_coords is not None:
d['centre_coords'] = self._centre_coords.tolist()
return d
[docs]
@classmethod
def from_dict(cls, d: dict) -> DynamicVoronoiSite:
"""Create a DynamicVoronoiSite object from a dict representation.
Args:
d (dict): The dict representation of this Site.
Returns:
DynamicVoronoiSite: A new DynamicVoronoiSite instance.
"""
site = cls(reference_indices=d['reference_indices'])
site.label = d.get('label')
site.contains_atoms = d.get('contains_atoms', [])
site.trajectory = d.get('trajectory', [])
site.points = d.get('points', [])
if 'transitions' in d:
site.transitions = d['transitions']
if 'centre_coords' in d:
site._centre_coords = np.array(d['centre_coords'])
return site
@property
def coordination_number(self) -> int:
"""Returns the coordination number of this site.
For a DynamicVoronoiSite, the "coordination number" is the number of reference atoms.
Args:
None
Returns:
int: The number of reference atoms.
"""
return len(self.reference_indices)
@property
def cn(self) -> int:
"""Coordination number for this site, defined as the number of
reference atoms.
Convenience property for coordination_number()
Returns:
int
"""
return self.coordination_number
[docs]
@classmethod
def sites_from_reference_indices(cls,
reference_indices_list: list[list[int]],
label: str | None = None) -> list[DynamicVoronoiSite]:
"""Create a list of DynamicVoronoiSite objects from a list of reference indices.
Args:
reference_indices_list: List of lists, where each inner list contains
reference indices for a site.
label: Optional label for all sites. Default is None.
Returns:
A list of DynamicVoronoiSite objects.
"""
sites = [cls(reference_indices=ri, label=label) for ri in reference_indices_list]
return sites