Source code for bsym.symmetry_group

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)])