from __future__ import annotations
import numpy as np
from bsym import SymmetryOperation
from itertools import product
from bsym.configuration import Configuration
from numpy.typing import NDArray
[docs]
class SymmetryGroup:
"""
:any:`SymmetryGroup` class.
A :any:`SymmetryGroup` object contains a set of :any:`SymmetryOperation` objects.
e.g.::
SymmetryGroup( symmetry_operations=[s1, s2, s3])
where `s1`, `s2`, and `s3` are :any:`SymmetryOperation` objects.
:any:`SymmetryGroup` objects can also be created from files using the class methods::
SymmetryGroup.read_from_file(filename)
and::
SymmetryGroup.read_from_file_with_labels(filename)
"""
class_str = 'SymmetryGroup'
def __init__(self, symmetry_operations: list[SymmetryOperation] | None = None):
"""
Create a :any:`SymmetryGroup` object.
Args:
symmetry_operations: A list of :any:`SymmetryOperation` objects.
Returns:
None
"""
self.symmetry_operations = symmetry_operations if symmetry_operations is not None else []
# Cache for batched operations
self._stacked_mappings: NDArray[np.int_] | None = None
self._unique_mappings: NDArray[np.int_] | None = None
@property
def stacked_index_mappings(self) -> NDArray[np.int_]:
"""
Stack all index mappings from symmetry operations.
Returns:
np.ndarray: Array of shape (n_operations, n_sites) containing all index mappings.
"""
if self._stacked_mappings is None:
self._stacked_mappings = np.array([
op.index_mapping for op in self.symmetry_operations
])
return self._stacked_mappings
@property
def unique_index_mappings(self) -> NDArray[np.int_]:
"""
Get unique index mappings, removing duplicates.
Returns:
np.ndarray: Array of shape (n_unique, n_sites) containing only unique index mappings.
"""
if self._unique_mappings is None:
self._unique_mappings = np.unique(
self.stacked_index_mappings,
axis=0
)
return self._unique_mappings
[docs]
def operate_on(self,
configuration: Configuration,
minimal_set: bool=False) -> list[Configuration]:
"""
Returns a list of Configurations generated by applying every symmetry operation in this symmetry group.
Args:
configuration: (Configuration): The configuration / occupation vector to operate on.
minimal_set: (:obj:`bool`, optional): Specifies whether to return the minimal set of resulting Configurations. Default is False.
Returns:
list(Configuration)
"""
# Choose which mappings to use
mappings = self.unique_index_mappings if minimal_set else self.stacked_index_mappings
# Apply all operations at once using batched indexing
transformed_vectors = configuration.vector[mappings]
# Wrap in Configuration objects
all_configs = [Configuration(vec) for vec in transformed_vectors]
# Remove duplicates if minimal_set requested
# (unique_index_mappings removes duplicate operations, but can still produce duplicate results)
if minimal_set:
all_configs = list(set(all_configs))
return all_configs
[docs]
@classmethod
def read_from_file( cls, filename ):
"""
Create a :any:`SymmetryGroup` object from a file.
The file format should be a series of numerical mappings representing each symmetry operation.
e.g. for a pair of equivalent sites::
# example input file to define the spacegroup for a pair of equivalent sites
1 2
2 1
Args:
filename (str): Name of the file to be read in.
Returns:
spacegroup (SymmetryGroup)
"""
data = np.loadtxt( filename, dtype=int )
symmetry_operations = [ SymmetryOperation.from_vector( row.tolist() ) for row in data ]
return( cls( symmetry_operations = symmetry_operations ) )
[docs]
@classmethod
def read_from_file_with_labels(cls, filename: str) -> SymmetryGroup:
"""
Create a :any:`SymmetryGroup` object from a file, with labelled symmetry operations.
The file format should be a series of numerical mappings representing each
symmetry operation, prepended with a string that will be used as a label.
e.g. for a pair of equivalent sites::
# example input file to define the spacegroup for a pair of equivalent sites
E 1 2
C2 2 1
Args:
filename (str): Name of the file to be read in.
Returns:
SymmetryGroup: The symmetry group read from the file.
"""
data = np.genfromtxt(filename, dtype=str)
labels = [row[0] for row in data]
vectors = [[int(s) for s in row[1:]] for row in data]
symmetry_operations = [SymmetryOperation.from_vector(v) for v in vectors]
# Set labels on each symmetry operation
for label, so in zip(labels, symmetry_operations):
so.set_label(label)
return cls(symmetry_operations=symmetry_operations)
[docs]
def save_symmetry_operation_vectors_to(
self,
filename: str
) -> None:
"""
Save the set of vectors describing each symmetry operation in this :any:`SymmetryGroup` to a file.
Args:
filename (str): Name of the file to save to.
Returns:
None
"""
operation_list = []
for symmetry_operation in self.symmetry_operations:
operation_list.append(symmetry_operation.as_vector())
np.savetxt(filename, np.array( operation_list ), fmt='%i')
[docs]
def extend(
self,
symmetry_operations_list: list[SymmetryOperation]
) -> SymmetryGroup:
"""
Extend the list of symmetry operations in this :any:`SymmetryGroup`.
Args:
symmetry_operations_list (list): A list of :any:`SymmetryOperation` objects.
Returns:
self (:any:`SymmetryGroup`)
"""
self.symmetry_operations.extend( symmetry_operations_list )
self._stacked_mappings = None
self._unique_mappings = None
return self
[docs]
def append(
self,
symmetry_operation: SymmetryOperation
) -> SymmetryGroup:
"""
Append a :any:`SymmetryOperation` to this :any:`SymmetryGroup`.
Args:
symmetry_operation (:any:`SymmetryOperation`): The :any:`SymmetryOperation` to add.
Returns:
self (:any:`SymmetryGroup`)
"""
self.symmetry_operations.append(symmetry_operation)
self._stacked_mappings = None
self._unique_mappings = None
return self
[docs]
def by_label(
self,
label: str
) -> SymmetryOperation | None:
"""
Returns the :any:`SymmetryOperation` with a matching label.
Args:
label (str): The label identifying the chosen symmetry operation.
Returns:
(:any:`SymmetryOperation`): The symmetry operation that matches this label.
"""
return next((so for so in self.symmetry_operations if so.label == label), None)
@property
def labels(self) -> list[str | None]:
"""
A list of labels for each :any:`SymmetryOperation` in this spacegroup.
Returns:
A list of label strings.
"""
return [so.label for so in self.symmetry_operations]
def __repr__(self) -> str:
to_return: str
to_return = '{}\n'.format(self.__class__.class_str)
for so in self.symmetry_operations:
to_return += "{}\t{}\n".format(so.label, so.as_vector())
return to_return
@property
def size(self) -> int:
return len(self.symmetry_operations)
def __mul__(self, other):
"""
Direct product.
Note: the return type is determined by the left operand, so
``SpaceGroup * SymmetryGroup`` returns a ``SpaceGroup`` while
``SymmetryGroup * SpaceGroup`` returns a ``SymmetryGroup``.
"""
return type(self)([s1 * s2 for s1, s2 in product(self.symmetry_operations, other.symmetry_operations)])