Source code for mchammer.observers.site_occupancy_observer

from ase import Atoms
from icet import ClusterSpace
from icet.core.structure import Structure
from mchammer.observers.base_observer import BaseObserver
from typing import List, Dict
import numpy as np


[docs]class SiteOccupancyObserver(BaseObserver): """ This class represents a site occupation factor (SOF) observer. A SOF observer allows to compute the site occupation factors along the trajectory sampled by a Monte Carlo (MC) simulation. Parameters ---------- cluster_space : icet.ClusterSpace cluster space from which the allowed species are extracted structure : ase.Atoms supercell consistent with primitive structure in cluster space; used to determine which species are allowed on each site sites : dict(str, list(int)) dictionary containing lists of sites that are to be considered; the keys will be taken as the names of the sites; the indices refer to the primitive structure associated with the cluster space interval : int the observation interval, defaults to None meaning that if the observer is used in a Monte Carlo simulation, then the Ensemble object will set the interval. Attributes ---------- tag : str name of observer interval : int observation interval Example ------- The following snippet illustrate how to use the site occupancy factor (SOF) observer in a Monte Carlo simulation of a surface slab. Here, the SOF observer is used to monitor the concentrations of different species at the surface, the first subsurface layer, and the remaining "bulk". A minimal cluster expansion is used with slightly modified surface interactions in order to obtain an example that can be run without much ado. In practice, one should of course use a proper cluster expansion:: >>> from ase.build import fcc111 >>> from icet import ClusterExpansion, ClusterSpace >>> from mchammer.calculators import ClusterExpansionCalculator >>> from mchammer.ensembles import CanonicalEnsemble >>> from mchammer.observers import SiteOccupancyObserver >>> # prepare reference structure >>> prim = fcc111('Au', size=(1, 1, 10), vacuum=10.0) >>> prim.translate((0.1, 0.1, 0.0)) >>> prim.wrap() >>> prim.pbc = True # icet requires pbc in all directions >>> # prepare cluster expansion >>> cs = ClusterSpace(prim, cutoffs=[3.7], chemical_symbols=['Ag', 'Au']) >>> params = [0] + 5 * [0] + 10 * [0.1] >>> params[1] = 0.01 >>> params[6] = 0.12 >>> ce = ClusterExpansion(cs, params) >>> print(ce) >>> # prepare initial configuration based on a 2x2 supercell >>> structure = prim.repeat((2, 2, 1)) >>> for k in range(20): >>> structure[k].symbol = 'Ag' >>> # set up MC simulation >>> calc = ClusterExpansionCalculator(structure, ce) >>> mc = CanonicalEnsemble(structure=structure, calculator=calc, temperature=600, ... dc_filename='myrun_sof.dc') >>> # set up observer and attach it to the MC simulation >>> sites = {'surface': [0, 9], 'subsurface': [1, 8], ... 'bulk': list(range(2, 8))} >>> sof = SiteOccupancyObserver(cs, structure, sites, interval=len(structure)) >>> mc.attach_observer(sof) >>> # run 1000 trial steps >>> mc.run(1000) After having run this snippet one can access the SOFs via the data container:: >>> print(mc.data_container.data) """ def __init__(self, cluster_space: ClusterSpace, structure: Atoms, sites: Dict[str, List[int]], interval: int = None) -> None: super().__init__(interval=interval, return_type=dict, tag='SiteOccupancyObserver') self._sites = {site: sorted(indices) for site, indices in sites.items()} self._set_allowed_species(cluster_space, structure) def _set_allowed_species(self, cluster_space: ClusterSpace, structure: Atoms): """ Set the allowed species for the selected sites in the Atoms object Parameters ---------- cluster_space Cluster space implicitly defining allowed species structure Specific supercell (consistent with cluster_space) whose allowed species are to be determined """ primitive_structure = Structure.from_atoms(cluster_space.primitive_structure) chemical_symbols = cluster_space.chemical_symbols if len(chemical_symbols) == 1: # If the allowed species are the same for all sites no loop is # required allowed_species = {site: chemical_symbols[0] for site in self._sites.keys()} else: # Loop over the lattice sites to find the allowed species allowed_species = {} for site, indices in self._sites.items(): allowed_species[site] = None positions = structure.positions[np.array(indices)] lattice_sites = primitive_structure.find_lattice_sites_by_positions( positions=positions, fractional_position_tolerance=cluster_space.fractional_position_tolerance) for k, lattice_site in enumerate(lattice_sites): species = chemical_symbols[lattice_site.index] # check that the allowed species are equal for all sites if allowed_species[site] is not None and \ species != allowed_species[site]: raise Exception("The allowed species {} for the site" " with index {} differs from the" " result {} for the previous index" " ({})!".format(species, indices[k], allowed_species[site], indices[k-1])) allowed_species[site] = species self._allowed_species = allowed_species
[docs] def get_observable(self, structure: Atoms) -> Dict[str, List[float]]: """ Returns the site occupation factors for a given atomic configuration. Parameters ---------- structure input atomic structure. """ chemical_symbols = np.array(structure.get_chemical_symbols()) sofs = {} for site, indices in self._sites.items(): counts = {species: 0 for species in self._allowed_species[site]} symbols, sym_counts = np.unique(chemical_symbols[indices], return_counts=True) for sym, count in zip(symbols, sym_counts): counts[sym] += count for species in counts.keys(): key = 'sof_{}_{}'.format(site, species) sofs[key] = float(counts[species]) / len(indices) return sofs