Source code for icet.tools.local_environment_enumeration

"""
This module provides a helper that enumerates training environments
around an event, for the stage of the workflow in which barriers are
computed with a first-principles or machine-learned method.
"""

import math

from itertools import product

import numpy as np

from ase import Atoms

from icet.core.local_cluster_space import LocalClusterSpace
from icet.tools.geometry import chemical_symbols_to_numbers


[docs] def find_event_instance(local_cluster_space: LocalClusterSpace, structure: Atoms) -> tuple: """Finds one instance of the event prototype in a supercell. The instance returned is the one with the lowest site indices. Parameters ---------- local_cluster_space Local cluster space whose event prototype is to be located. structure Supercell in which to search. """ return min(local_cluster_space.find_event_instances(structure))
[docs] def enumerate_event_environments(local_cluster_space: LocalClusterSpace, background: Atoms, event_sites: tuple = None, enumeration_radius: float = None, n_structures: int = None, vacancy_species: str = 'X', seed: int = 42) -> list: """Enumerates symmetrically distinct training environments around an event. Decorations of the environment sites within :attr:`enumeration_radius` of the event are enumerated, the background configuration is held fixed beyond that radius, and duplicates under the point group of the event are removed. Exhaustive enumeration grows combinatorially with the number of enumerated sites, so beyond a size threshold the helper falls back to symmetry-reduced random sampling. The anchor sites of the returned structures are decorated with the migrating species on the first site and the vacancy species on the remaining sites, which is the initial state of the hop for the NEB stage of the workflow. Parameters ---------- local_cluster_space Local cluster space that defines event, environment, and symmetry. background Supercell whose occupations provide everything beyond the enumerated sites. event_sites Indices of the anchor sites in the background. Located automatically when not given. enumeration_radius Maximum distance of an enumerated site from the nearest anchor site. Defaults to the environment cutoff of the local cluster space. n_structures Maximum number of environments to return. When the number of distinct environments exceeds this value a random subset is returned. Required when the number of candidate decorations exceeds 10^5. vacancy_species Chemical symbol used for the vacant anchor sites. The default ``'X'`` is the species of atomic number 0 that :mod:`mchammer` treats as a vacancy, which is what :class:`~mchammer.ensembles.KineticMonteCarloEnsemble` requires on the target site of a hop. seed Seed for the random sampling. Returns ------- list Pairs of (structure, event sites). """ lcs = local_cluster_space if event_sites is None: event_sites = find_event_instance(lcs, background) else: event_sites = tuple(int(site) for site in event_sites) if enumeration_radius is None: enumeration_radius = lcs.environment_cutoff # The mapping call also runs the aliasing guard. environment_indices = lcs.get_environment_site_mapping(background, event_sites) # Select the environment sites within the enumeration radius. anchor_positions = lcs.event.positions(lcs.primitive_structure) enumerated_sites = [] enumerated_indices = [] for site, supercell_index in zip(lcs.environment_sites, environment_indices): position = lcs.get_site_position(site) distance = np.linalg.norm(anchor_positions - position, axis=1).min() if distance <= enumeration_radius: enumerated_sites.append(site) enumerated_indices.append(supercell_index) if not enumerated_sites: raise ValueError('No environment sites within the enumeration radius') # Permutations of the enumerated sites induced by the anchor operations. permutations = lcs.get_site_permutations(enumerated_sites) # Allowed occupations per enumerated site. chemical_symbols = lcs.chemical_symbols allowed = [sorted(chemical_symbols_to_numbers(chemical_symbols[site[0]])) for site in enumerated_sites] # math.prod computes the exact product, where a fixed-width product # would wrap for 63 or more binary sites and dodge the guard below. total = math.prod(len(numbers) for numbers in allowed) if total > 10 ** 5 and n_structures is None: raise ValueError(f'{total} candidate decorations. Pass n_structures' ' to sample instead of enumerating exhaustively') rng = np.random.default_rng(seed) if total <= 10 ** 5: distinct = _distinct_labelings(product(*allowed), permutations) if n_structures is not None and len(distinct) > n_structures: chosen = rng.choice(len(distinct), size=n_structures, replace=False) distinct = [distinct[index] for index in sorted(chosen)] else: distinct = _sample_labelings(allowed, permutations, n_structures, rng) # Build the structures. results = [] migrating_number, vacancy_number = chemical_symbols_to_numbers( [lcs.event.species, vacancy_species]) for labeling in distinct: structure = background.copy() numbers = structure.get_atomic_numbers() for supercell_index, occupation in zip(enumerated_indices, labeling): numbers[supercell_index] = occupation numbers[event_sites[0]] = migrating_number for site in event_sites[1:]: numbers[site] = vacancy_number structure.set_atomic_numbers(numbers) results.append((structure, event_sites)) return results
def _canonical_labeling(labeling: tuple, permutations: list) -> tuple: """Returns the canonical representative of a labeling under permutations.""" return min(tuple(labeling[index] for index in permutation) for permutation in permutations) def _distinct_labelings(candidates, permutations: list) -> list: """Returns the distinct canonical labelings of an iterable of candidates.""" distinct = [] seen = set() for labeling in candidates: canonical = _canonical_labeling(tuple(labeling), permutations) if canonical not in seen: seen.add(canonical) distinct.append(canonical) return distinct def _sample_labelings(allowed: list, permutations: list, n_structures: int, rng) -> list: """Samples distinct canonical labelings at random.""" distinct = [] seen = set() attempts = 0 while len(distinct) < n_structures and attempts < 100 * n_structures: attempts += 1 labeling = tuple(int(rng.choice(numbers)) for numbers in allowed) canonical = _canonical_labeling(labeling, permutations) if canonical not in seen: seen.add(canonical) distinct.append(canonical) return distinct