Source code for bsym.configuration

from __future__ import annotations

import numpy as np
from numpy.typing import NDArray
from typing import TYPE_CHECKING, Any
import json
if TYPE_CHECKING:
    from bsym.symmetry_operation import SymmetryOperation


[docs] class Configuration: """ A :any:`Configuration` describes a specific arrangement of objects in the vector space of possible positions. Objects are represented by integers, with indistinguishable objects denoted by identical integers. Internally, the configuration is stored as a ``numpy.int8`` array. Attributes: count (int): If symmetry-inequivalent configurations have been generated for a `configuration space`, this records the number of configurations equivalent to this one. Value at initialisation is ``None``. lowest_numeric_representation (int): If the numeric representations for the set of equivalent configurations are calculated, this can be used to store the lowest valued numeric representation, for use as a simple hash. Example: >>> Configuration([1, 1, 0]) Configuration([1, 1, 0]) """ def __init__(self, vector: list[int] | NDArray[np.int_]) -> None: self.count: int | None = None self.lowest_numeric_representation: int | None = None self.vector: np.ndarray = np.asarray(vector, dtype=np.int8) def __eq__(self, other: object) -> bool: if not isinstance(other, Configuration): return NotImplemented return np.array_equal(self.vector, other.vector) def __hash__(self) -> int: return hash(self.vector.tobytes())
[docs] def matches(self, test_configuration: Configuration) -> bool: """ Test whether this configuration is equal to another configuration. Args: test_configuration (:any:`Configuration`): The configuration to compare against. Returns: (bool): True | False. """ if not isinstance(test_configuration, Configuration): raise TypeError return self == test_configuration
[docs] def is_equivalent_to( self, test_configuration: Configuration, symmetry_operations: list[SymmetryOperation], ) -> bool: """ Test whether this configuration is equivalent to another configuration under one or more of a set of symmetry operations. Args: test_configuration (Configuration): The configuration to compare against. symmetry_operations (list(SymmetryOperation): A list of SymmetryOperation objects. Returns: (bool): True | False """ for symmetry_operation in symmetry_operations: if symmetry_operation.operate_on(self) == test_configuration: return True else: return False
[docs] def is_in_list(self, the_list: list[Configuration]) -> bool: """ Test whether this configuration is in a list of configurations. Args: list (list(bsym.Comfiguration)): A list of Configuration instances. Returns: (bool): True | False """ return next((True for c in the_list if self.matches(c)), False)
[docs] def has_equivalent_in_list( self, the_list: list[Configuration], symmetry_operations: list[SymmetryOperation], ) -> bool: """ Test whether this configuration is equivalent by symmetry to one or more in a list of configurations. Args: list (list(bsym.Configuration)): A list of :any:`Configuration` instances. symmetry_operations (list(bsym.SymmetryOperation)): A list of :any:`SymmetryOperation` objects. Returns: (bool): True | False """ return next( (True for c in the_list if self.is_equivalent_to(c, symmetry_operations)), False, )
[docs] def set_lowest_numeric_representation( self, symmetry_operations: list[SymmetryOperation] ) -> None: """ Sets `self.lowest_numeric_representation` to the lowest value numeric representation of this configuration when permutated under a set of symmetry operations. Args: symmetry_operations (list): A list of :any:`SymmetryOperation` instances. Returns: None """ self.lowest_numeric_representation = min( [ symmetry_operation.operate_on(self).as_number for symmetry_operation in symmetry_operations ] )
[docs] def numeric_equivalents(self, symmetry_operations: list[SymmetryOperation]) -> list[int]: """ Returns a list of all symmetry equivalent configurations generated by a set of symmetry operations with each configuration given in numeric representation. Args: symmetry_operations (list): A list of :any:`SymmetryOperation` instances. Returns: (list(int)): A list of numbers representing each equivalent configuration. """ return [ symmetry_operation.operate_on(self).as_number for symmetry_operation in symmetry_operations ]
@property def as_number(self) -> int: """ A numeric representation of this configuration. Examples: >>> c = Configuration([1, 2, 0]) >>> c.as_number 120 """ return as_number(self.vector)
[docs] @classmethod def from_tuple(cls, configuration_tuple): """ Create a Configuration from a tuple. Configurations are stored as int8 arrays, supporting species labels 0-255. Args: configuration_tuple: Tuple of configuration values (0-255). Returns: Configuration: New configuration object. """ return cls(np.array(configuration_tuple, dtype=np.int8))
[docs] def tolist(self) -> list[int]: """ Returns the configuration data as a list. Args: None Returns: (List) """ return self.vector.tolist() # type: ignore[no-any-return]
[docs] def pprint(self) -> None: print(" ".join([str(e) for e in self.tolist()]))
[docs] def position(self, label: int) -> list[int]: """ Returns the vector indices where elements are equal to `label`. Args: label (int): The label used to select the vector positions. Returns: (list): A list of all positions that match `label`. """ return [i for i, x in enumerate(self.tolist()) if x == label]
def __repr__(self) -> str: to_return = "Configuration({})\n".format(self.vector) return to_return
[docs] def map_objects(self, objects: list) -> dict[int, Any]: """ Returns a dict of objects sorted according to this configuration. Args: objects [list]: A list of objects. Returns: sorted_objects [dict]: A dictionary of sorted objects. """ if len(objects) != len(self.vector): raise ValueError sorted_objects = {} for key in set(self.vector): sorted_objects[key] = [o for k, o in zip(self.vector, objects) if k == key] return sorted_objects
[docs] @staticmethod def tuple_to_bytes(tup: tuple) -> bytes: """ Convert configuration tuple to bytes. Used for initial permutation checking in enumerate_configurations. Args: tup: Configuration as tuple. Returns: bytes: Byte representation for hashing. """ return np.array(tup, dtype=np.int8).tobytes()
[docs] @staticmethod def array_to_bytes(arr: np.ndarray) -> bytes: """ Convert configuration array to bytes. Assumes array is already int8. Used for transformations. Args: arr: Configuration as int8 numpy array. Returns: bytes: Byte representation for hashing. """ return arr.tobytes()
[docs] def as_bytes(self) -> bytes: """Get byte representation of this configuration.""" return Configuration.array_to_bytes(self.vector)
[docs] def get_byte_equivalents(self, symmetry_group) -> set[bytes]: """Get byte representations of all symmetry-equivalent configurations.""" transformed_vectors = self.vector[symmetry_group.unique_index_mappings] byte_equivalents = set( Configuration.array_to_bytes(vec) for vec in transformed_vectors ) return byte_equivalents
[docs] def to_dict(self) -> dict: """Convert to JSON-serialisable dictionary. Returns: Dictionary with 'vector' key containing the configuration as a list. """ return {'vector': self.tolist()}
[docs] @classmethod def from_dict(cls, d: dict) -> 'Configuration': """Create Configuration from dictionary. Args: d: Dictionary with 'vector' key containing configuration values. Returns: New Configuration instance. """ return cls(d['vector'])
[docs] def as_number(a: list[int] | NDArray[np.int_]) -> int: tot = 0 for num in a: tot *= 10 tot += int(num) return tot
[docs] def save_configurations(configurations: list[Configuration], filename: str) -> None: """Save configurations to a JSON file. Args: configurations: List of Configuration objects to save. filename: Path to output file. """ data = [config.to_dict() for config in configurations] with open(filename, 'w') as f: json.dump(data, f)
[docs] def load_configurations(filename: str) -> list[Configuration]: """Load configurations from a JSON file. Args: filename: Path to input file. Returns: List of Configuration objects. """ with open(filename) as f: data = json.load(f) return [Configuration.from_dict(d) for d in data]