Source code for mchammer.observers.cluster_count_observer

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_to_arrays
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 keeps track of how often each decorated cluster occurs along the trajectory sampled by a Monte Carlo (MC) simulation. Given several canonical MC simulations at different temperatures, for example, it gives access to the temperature dependence of the number of nearest neighbors of a particular species. The counts are stored in the data container in one column per decorated cluster. The columns are named ``0_Al``, ``0_Cu``, ``1_Al_Al``, ``1_Al_Cu``, ``1_Cu_Al``, ``1_Cu_Cu`` and so on, where the number is the orbit index and the symbols are the species on the sites of the cluster. The count is the number of clusters of that orbit in the structure that carry that decoration. Parameters ---------- cluster_space Cluster space that defines the clusters to be counted. structure Supercell the observer works on. interval Observation interval in trial steps. By default the ensemble the observer is attached to sets the interval to the number of sites in the structure. orbit_indices Indices of the orbits whose clusters are counted. By default all orbits are included. Example ------- The following snippet counts nearest and next-nearest neighbor pairs along a canonical Monte Carlo trajectory of an Ising-like cluster expansion:: >>> from ase.build import bulk >>> from icet import ClusterExpansion, ClusterSpace >>> from mchammer.calculators import ClusterExpansionCalculator >>> from mchammer.ensembles import CanonicalEnsemble >>> from mchammer.observers import ClusterCountObserver >>> # prepare cluster expansion >>> prim = bulk('Au') >>> cs = ClusterSpace(prim, cutoffs=[4.3], chemical_symbols=['Ag', 'Au']) >>> ce = ClusterExpansion(cs, [0, 0, 0.1, -0.02]) >>> # prepare initial configuration >>> structure = prim.repeat(3) >>> for k in range(5): ... structure[k].symbol = 'Ag' >>> # set up the simulation and attach the observer >>> calculator = ClusterExpansionCalculator(structure, ce) >>> mc = CanonicalEnsemble(structure=structure, calculator=calculator, ... temperature=600) >>> observer = ClusterCountObserver(cs, structure, interval=len(structure)) >>> mc.attach_observer(observer) >>> mc.run(1000) >>> # the counts are available from the data container, for example the >>> # number of nearest neighbor Ag-Au pairs along the trajectory >>> counts = mc.data_container.get('1_Ag_Au') The counts of a single structure are available as a data frame:: >>> print(observer.get_cluster_counts(structure)) """ def __init__(self, cluster_space: ClusterSpace, structure: Atoms, interval: int | None = None, orbit_indices: list[int] | None = 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_to_arrays(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: """ Returns the number of clusters of each orbit and decoration in a structure. Parameters ---------- structure Atomic configuration to count clusters in. Returns ------- pandas.DataFrame One row per decorated cluster with the columns ``dc_tag`` (the column name used in the data container), ``occupation`` (the species on the sites of the cluster), ``cluster_count``, ``orbit_index``, and ``order``. """ rows = [] occupations = structure.get_atomic_numbers() for i in self._orbit_indices: orbit = self._full_orbit_list.get_orbit(i) cluster_counts = orbit.get_cluster_counts(occupations) 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 cluster counts of a configuration. Parameters ---------- structure Atomic configuration to observe. Returns ------- dict The number of clusters for each decorated cluster, keyed by the column name used in the data container. """ counts = self.get_cluster_counts(structure) count_dict = {row['dc_tag']: row['cluster_count'] for i, row in counts.iterrows()} return count_dict