Source code for mchammer.observers.cluster_count_observer

from typing import Dict, List, Tuple
from collections.abc import Iterable

import pandas as pd

from ase import Atoms
from icet import ClusterSpace
from icet.core.local_orbit_list_generator import LocalOrbitListGenerator
from icet.core.structure import Structure
from icet.tools.geometry import chemical_symbols_to_numbers
from mchammer.observers.base_observer import BaseObserver


[docs] class ClusterCountObserver(BaseObserver): """This class represents a cluster count observer. A cluster count observer enables one to keep track of the occupation of clusters along the trajectory sampled by a Monte Carlo (MC) simulation. For example, given several canonical MC simulations representing different temperatures, this observer can be used to access the temperature dependence of the number of nearest neigbhors of a particular species. Parameters ---------- cluster_space Cluster space to define the clusters to be counted. structure Defines the lattice that the observer will work on. orbit_indices Only include orbits up to the orbit with this index. By default all orbits are included. interval Observation interval. Defaults to ``None`` meaning that if the observer is used in a Monte Carlo simulations, then the :class:`Ensemble` object will determine the interval. """ def __init__(self, cluster_space: ClusterSpace, structure: Atoms, interval: int = None, orbit_indices: List[int] = None) -> None: super().__init__(interval=interval, return_type=dict, tag='ClusterCountObserver') self._cluster_space = cluster_space local_orbit_list_generator = LocalOrbitListGenerator( orbit_list=cluster_space.orbit_list, structure=Structure.from_atoms(structure), fractional_position_tolerance=cluster_space.fractional_position_tolerance) self._full_orbit_list = local_orbit_list_generator.generate_full_orbit_list() if orbit_indices is None: self._orbit_indices = list(range(len(self._full_orbit_list))) elif not isinstance(orbit_indices, Iterable): raise ValueError('Argument orbit_indices should be a list of integers, ' f'not {type(orbit_indices)}') else: self._orbit_indices = orbit_indices self._possible_occupations = self._get_possible_occupations() def _get_possible_occupations(self) -> Dict[int, List[Tuple[str]]]: """ Returns a dictionary containing the possible occupations for each orbit. """ possible_occupations = {} for i in self._orbit_indices: possible_occupations_orbit = self._cluster_space.get_possible_orbit_occupations(i) order = self._full_orbit_list.get_orbit(i).order assert order == len(possible_occupations_orbit[0]), \ f'Order (n={order}) does not match possible occupations' \ f' (n={len(possible_occupations[0])}, {possible_occupations}).' possible_occupations[i] = possible_occupations_orbit return possible_occupations
[docs] def get_cluster_counts(self, structure: Atoms) -> pd.DataFrame: """Counts the number of times different clusters appear in the structure and returns this information as a pandas dataframe. Parameters ---------- structure input atomic structure. """ rows = [] structure_icet = Structure.from_atoms(structure) for i in self._orbit_indices: orbit = self._full_orbit_list.get_orbit(i) cluster_counts = orbit.get_cluster_counts(structure_icet) for chemical_symbols in self._possible_occupations[i]: count = cluster_counts.get(tuple(chemical_symbols_to_numbers(chemical_symbols)), 0) row = {} row['dc_tag'] = '{}_{}'.format(i, '_'.join(chemical_symbols)) row['occupation'] = chemical_symbols row['cluster_count'] = count row['orbit_index'] = i row['order'] = orbit.order rows.append(row) return pd.DataFrame(rows)
[docs] def get_observable(self, structure: Atoms) -> dict: """ Returns the value of the property from a cluster expansion model for a given atomic configuration. Parameters ---------- structure input atomic structure """ counts = self.get_cluster_counts(structure) count_dict = {row['dc_tag']: row['cluster_count'] for i, row in counts.iterrows()} return count_dict