Source code for mchammer.ensembles.target_cluster_vector_annealing

from ase import Atoms
from mchammer.calculators.target_vector_calculator import TargetVectorCalculator
from .canonical_ensemble import CanonicalEnsemble
from .canonical_annealing import _cooling_exponential
import numpy as np
import random
from icet.input_output.logging_tools import logger
logger = logger.getChild('target_cluster_vector_annealing')


[docs] class TargetClusterVectorAnnealing: """ Instances of this class allow one to carry out simulated annealing towards a target cluster vector. Because it is impossible to know *a priori* which supercell shape accomodates the best match, this ensemble allows the annealing to be done for multiple :class:`Atoms <ase.Atoms>` objects at the same time. Parameters ---------- structure Atomic configurations to be used in the Monte Carlo simulation; also defines the initial occupation vectors. calculators Calculators corresponding to each :class:`Atoms <ase.Atoms>` object. T_start Artificial temperature at which annealing is started. T_stop Artificial temperature at which annealing is stopped. random_seed Seed for random number generator used in the Monte Carlo simulation. """ def __init__(self, structure: list[Atoms], calculators: list[TargetVectorCalculator], T_start: float = 5.0, T_stop: float = 0.001, random_seed: int | None = None) -> None: if isinstance(structure, Atoms): raise ValueError( 'A list of ASE Atoms (supercells) must be provided') if len(structure) != len(calculators): raise ValueError('There must be as many supercells as there ' 'are calculators ({} != {})'.format(len(structure), len(calculators))) logger.info('Initializing target cluster vector annealing ' 'with {} supercells'.format(len(structure))) # random number generator if random_seed is None: self._random_seed = random.randint(0, int(1e16)) else: self._random_seed = random_seed random.seed(a=self._random_seed) # Initialize an ensemble for each supercell sub_ensembles = [] for ens_id, (supercell, calculator) in enumerate(zip(structure, calculators)): sub_ensembles.append(CanonicalEnsemble(structure=supercell, calculator=calculator, random_seed=random.randint( 0, int(1e16)), user_tag='ensemble_{}'.format( ens_id), temperature=T_start, dc_filename=None)) self._sub_ensembles = sub_ensembles self._current_score = self._sub_ensembles[0].calculator.calculate_total( self._sub_ensembles[0].configuration.occupations) self._best_score = self._current_score self._best_structure = structure[0].copy() self._temperature = T_start self._T_start = T_start self._T_stop = T_stop self._total_trials = 0 self._accepted_trials = 0 self._n_steps = 42
[docs] def generate_structure(self, number_of_trial_steps: int | None = None) -> Atoms: """ Runs a structure annealing simulation. Parameters ---------- number_of_trial_steps Total number of trial steps to perform. If ``None`` run (on average) 3000 steps per supercell. """ if number_of_trial_steps is None: self._n_steps = 3000 * len(self._sub_ensembles) else: self._n_steps = number_of_trial_steps self._temperature = self._T_start self._total_trials = 0 self._accepted_trials = 0 while self.total_trials < self.n_steps: if self._total_trials % 1000 == 0: logger.info('MC step {}/{} ({} accepted trials, ' 'temperature {:.3f}), ' 'best score: {:.3f}'.format(self.total_trials, self.n_steps, self.accepted_trials, self.temperature, self.best_score)) self._do_trial_step() return self.best_structure
def _do_trial_step(self): """ Carries out one Monte Carlo trial step. """ self._temperature = _cooling_exponential( self.total_trials, self.T_start, self.T_stop, self.n_steps) self._total_trials += 1 # Choose a supercell ensemble = random.choice(self._sub_ensembles) # Choose two sites and swap sublattice_index = ensemble.get_random_sublattice_index( ensemble._swap_sublattice_probabilities) sites, species = ensemble.configuration.get_swapped_state( sublattice_index) # Update occupations so that the cluster vector (and its score) # can be calculated ensemble.configuration.update_occupations(sites, species) new_score = ensemble.calculator.calculate_total( ensemble.configuration.occupations) if self._acceptance_condition(new_score - self.current_score): self._current_score = new_score self._accepted_trials += 1 # Since we are looking for the best structures we want to # keep track of the best one we have found as yet (the # current one may have a worse score) if self._current_score < self._best_score: self._best_structure = ensemble.structure self._best_score = self._current_score else: ensemble.configuration.update_occupations( sites, list(reversed(species))) def _acceptance_condition(self, potential_diff: float) -> bool: """ Evaluates Metropolis acceptance criterion. Parameters ---------- potential_diff Change in the thermodynamic potential associated with the trial step. """ if potential_diff < 0: return True elif abs(self.temperature) < 1e-6: # temperature is numerically zero return False else: p = np.exp(-potential_diff / self.temperature) return p > random.random() @property def temperature(self) -> float: """ Current temperature """ return self._temperature @property def T_start(self) -> float: """ Starting temperature. """ return self._T_start @property def T_stop(self) -> float: """ Stop temperature. """ return self._T_stop @property def n_steps(self) -> int: """ Number of steps to carry out. """ return self._n_steps @property def total_trials(self) -> int: """ Number of steps carried out so far. """ return self._total_trials @property def accepted_trials(self) -> int: """ Number of accepted trials carried out so far. """ return self._accepted_trials @property def current_score(self) -> float: """ Current target vector score. """ return self._current_score @property def best_score(self) -> float: """ Best target vector score found so far. """ return self._best_score @property def best_structure(self) -> float: """ Structure most closely matching target vector so far. """ return self._best_structure