Source code for icet.tools.structure_generation

import itertools
from typing import List, Dict, Tuple, Union
import numpy as np
from mchammer.ensembles import TargetClusterVectorAnnealing
from mchammer.calculators import (TargetVectorCalculator,
                                  compare_cluster_vectors)
from icet.tools import (enumerate_structures,
                        enumerate_supercells)
from icet import ClusterSpace
from icet.input_output.logging_tools import logger
from ase import Atoms
from ase.data import chemical_symbols as periodic_table


[docs] def generate_target_structure_from_supercells(cluster_space: ClusterSpace, supercells: List[Atoms], target_concentrations: dict, target_cluster_vector: List[float], T_start: float = 5.0, T_stop: float = 0.001, n_steps: int = None, optimality_weight: float = 1.0, random_seed: int = None, random_start: bool = True, tol: float = 1e-5) -> Atoms: """ Given a :attr:`cluster_space` and a :attr:`target_cluster_vector` and one or more :attr:`supercells`, generate a structure that as closely as possible matches that cluster vector. Internally the function uses a simulated annealing algorithm and the difference between two cluster vectors is calculated with the measure suggested by A. van de Walle et al. in Calphad **42**, 13-18 (2013) [WalTiwJon13]_ (for more information, see :class:`mchammer.calculators.TargetVectorCalculator`). Parameters ---------- cluster_space A cluster space defining the lattice to be occupied. supercells List of one or more supercells among which an optimal structure will be searched for. target_concentrations Concentration of each species in the target structure, per sublattice (for example ``{'Au': 0.5, 'Pd': 0.5}`` for a single sublattice Au-Pd structure, or ``{'A': {'Au': 0.5, 'Pd': 0.5}, 'B': {'H': 0.25, 'X': 0.75}}`` for a system with two sublattices. The symbols defining sublattices ('A', 'B' etc) can be found by printing the :attr:`cluster_space`. target_cluster_vector Cluster vector that the generated structure should match as closely as possible. T_start Artificial temperature at which the simulated annealing starts. T_stop Artifical temperature at which the simulated annealing stops. n_steps Total number of Monte Carlo steps in the simulation. optimality_weight Controls weighting :math:`L` of perfect correlations, see :class:`mchammer.calculators.TargetVectorCalculator`. random_seed Seed for the random number generator used in the Monte Carlo simulation and used for initializing the occupation of the supercells if random_start is ``True``. random_start Randomly occupy starting structure, can be disabled if the user prefers to pass an initial structure. tol Numerical tolerance. """ target_concentrations = _validate_concentrations(target_concentrations, cluster_space) calculators = [] # Loop over all supercells and intialize # them with a "random" occupation of symbols that # fulfill the target concentrations valid_supercells = [] warning_issued = False for supercell in supercells: supercell_copy = supercell.copy() if random_start: try: occupy_structure_randomly(supercell_copy, cluster_space, target_concentrations, random_seed) except ValueError: if not warning_issued: logger.warning('At least one supercell was not commensurate with the specified ' 'target concentrations.') warning_issued = True continue valid_supercells.append(supercell_copy) calculators.append(TargetVectorCalculator(supercell_copy, cluster_space, target_cluster_vector, optimality_weight=optimality_weight, optimality_tol=tol)) if len(valid_supercells) == 0: raise ValueError('No supercells that may host the specified ' 'target_concentrations were supplied.') ens = TargetClusterVectorAnnealing(structure=valid_supercells, calculators=calculators, T_start=T_start, T_stop=T_stop, random_seed=random_seed) return ens.generate_structure(number_of_trial_steps=n_steps)
[docs] def generate_target_structure(cluster_space: ClusterSpace, max_size: int, target_concentrations: dict, target_cluster_vector: List[float], include_smaller_cells: bool = True, pbc: Union[Tuple[bool, bool, bool], Tuple[int, int, int]] = None, T_start: float = 5.0, T_stop: float = 0.001, n_steps: int = None, optimality_weight: float = 1.0, random_seed: int = None, tol: float = 1e-5) -> Atoms: """ Given a :attr:`cluster_space` and a :attr:`target_cluster_vector`, generate a structure that as closely as possible matches that cluster vector. The search is performed among all inequivalent supercells shapes up to a certain size. Internally the function uses a simulated annealing algorithm and the difference between two cluster vectors is calculated with the measure suggested by A. van de Walle et al. in Calphad **42**, 13-18 (2013) [WalTiwJon13]_ (for more information, see :class:`mchammer.calculators.TargetVectorCalculator`). Parameters ---------- cluster_space Cluster space defining the lattice to be occupied. max_size Maximum supercell size. target_concentrations Concentration of each species in the target structure, per sublattice (for example ``{'Au': 0.5, 'Pd': 0.5}`` for a single sublattice Au-Pd structure, or ``{'A': {'Au': 0.5, 'Pd': 0.5}, 'B': {'H': 0.25, 'X': 0.75}}`` for a system with two sublattices. The symbols defining sublattices ('A', 'B' etc) can be found by printing the :attr:`cluster_space`. target_cluster_vector Cluster vector that the generated structure should match as closely as possible. include_smaller_cells If ``True``, search among all supercell sizes including :attr:`max_size`, else search only among those exactly matching :attr:`max_size` pbc Periodic boundary conditions for each direction, e.g., ``(True, True, False)``. The axes are defined by the cell of :attr:`cluster_space.primitive_structure``. Default is periodic boundary in all directions. T_start Artificial temperature at which the simulated annealing starts. T_stop Artifical temperature at which the simulated annealing stops. n_steps Total number of Monte Carlo steps in the simulation. optimality_weight Controls weighting :math:`L` of perfect correlations, see :class:`mchammer.calculators.TargetVectorCalculator`. random_seed Seed for the random number generator used in the Monte Carlo simulation. tol Numerical tolerance. """ target_concentrations = _validate_concentrations(target_concentrations, cluster_space) if pbc is None: pbc = (True, True, True) prim = cluster_space.primitive_structure prim.set_pbc(pbc) supercells = [] if include_smaller_cells: sizes = list(range(1, max_size + 1)) else: sizes = [max_size] for size in sizes: # For efficiency, make a first check that the current size is # commensurate with all concentrations (example: {'Au': 0.5, # 'Pd': 0.5} would not be commensurate with a supercell with 3 # atoms). supercell = cluster_space.primitive_structure.repeat((size, 1, 1)) if not _concentrations_fit_structure(structure=supercell, cluster_space=cluster_space, concentrations=target_concentrations): continue # Loop over all inequivalent supercells and intialize # them with a "random" occupation of symbols that # fulfill the target concentrations for supercell in enumerate_supercells(prim, [size]): supercell.set_pbc(True) supercells.append(supercell) return generate_target_structure_from_supercells(cluster_space=cluster_space, supercells=supercells, target_concentrations=target_concentrations, target_cluster_vector=target_cluster_vector, T_start=T_start, T_stop=T_stop, n_steps=n_steps, optimality_weight=optimality_weight, random_seed=random_seed, tol=tol)
[docs] def generate_sqs_from_supercells(cluster_space: ClusterSpace, supercells: List[Atoms], target_concentrations: dict, T_start: float = 5.0, T_stop: float = 0.001, n_steps: int = None, optimality_weight: float = 1.0, random_seed: int = None, random_start: bool = True, tol: float = 1e-5) -> Atoms: """ Given a :attr:`cluster_space`` and one or more :attr:`supercells`, generate a special quasirandom structure (SQS), i.e., a structure that for the provided supercells size provides the best possible approximation to a random alloy [ZunWeiFer90]_. In the present case, this means that the generated structure will have a cluster vector that as closely as possible matches the cluster vector of an infintely large randomly occupied supercell. Internally the function uses a simulated annealing algorithm and the difference between two cluster vectors is calculated with the measure suggested by A. van de Walle et al. in Calphad **42**, 13-18 (2013) [WalTiwJon13]_ (for more information, see :class:`mchammer.calculators.TargetVectorCalculator`). Parameters ---------- cluster_space Cluster space defining the lattice to be occupied. supercells List of one or more supercells among which an optimal structure will be searched for. target_concentrations Concentration of each species in the target structure, per sublattice (for example ``{'Au': 0.5, 'Pd': 0.5}`` for a single sublattice Au-Pd structure, or ``{'A': {'Au': 0.5, 'Pd': 0.5}, 'B': {'H': 0.25, 'X': 0.75}}`` for a system with two sublattices. The symbols defining sublattices ('A', 'B' etc) can be found by printing the :attr:`cluster_space`. T_start Artificial temperature at which the simulated annealing starts. T_stop Artifical temperature at which the simulated annealing stops. n_steps Total number of Monte Carlo steps in the simulation. optimality_weight Controls weighting :math:`L` of perfect correlations, see :class:`mchammer.calculators.TargetVectorCalculator`. random_seed Seed for the random number generator used in the Monte Carlo simulation and used for initializing the occupation of the supercells if random_start is ``True``. random_start Randomly occupy starting structure, can be disabled if the user prefers to pass an initial structure. tol Numerical tolerance. """ sqs_vector = _get_sqs_cluster_vector(cluster_space=cluster_space, target_concentrations=target_concentrations) return generate_target_structure_from_supercells(cluster_space=cluster_space, supercells=supercells, target_concentrations=target_concentrations, target_cluster_vector=sqs_vector, T_start=T_start, T_stop=T_stop, n_steps=n_steps, optimality_weight=optimality_weight, random_seed=random_seed, random_start=random_start, tol=tol)
[docs] def generate_sqs(cluster_space: ClusterSpace, max_size: int, target_concentrations: dict, include_smaller_cells: bool = True, pbc: Union[Tuple[bool, bool, bool], Tuple[int, int, int]] = None, T_start: float = 5.0, T_stop: float = 0.001, n_steps: int = None, optimality_weight: float = 1.0, random_seed: int = None, tol: float = 1e-5) -> Atoms: """ Given a :attr:`cluster_space`, generate a special quasirandom structure (SQS), i.e., a structure that for a given supercell size provides the best possible approximation to a random alloy [ZunWeiFer90]_. In the present case, this means that the generated structure will have a cluster vector that as closely as possible matches the cluster vector of an infintely large randomly occupied supercell. Internally the function uses a simulated annealing algorithm and the difference between two cluster vectors is calculated with the measure suggested by A. van de Walle et al. in Calphad **42**, 13-18 (2013) [WalTiwJon13]_ (for more information, see :class:`mchammer.calculators.TargetVectorCalculator`). Parameters ---------- cluster_space Cluster space defining the lattice to be occupied. max_size Maximum supercell size. target_concentrations Concentration of each species in the target structure, per sublattice (for example ``{'Au': 0.5, 'Pd': 0.5}`` for a single sublattice Au-Pd structure, or ``{'A': {'Au': 0.5, 'Pd': 0.5}, 'B': {'H': 0.25, 'X': 0.75}}`` for a system with two sublattices. The symbols defining sublattices ('A', 'B' etc) can be found by printing the :attr:`cluster_space`. include_smaller_cells If ``True``, search among all supercell sizes including :attr:`max_size`, else search only among those exactly matching :attr:`max_size` pbc Periodic boundary conditions for each direction, e.g., ``(True, True, False)``. The axes are defined by the cell of ``cluster_space.primitive_structure``. Default is periodic boundary in all directions. T_start Artificial temperature at which the simulated annealing starts. T_stop Artifical temperature at which the simulated annealing stops. n_steps Total number of Monte Carlo steps in the simulation. optimality_weight Controls weighting :math:`L` of perfect correlations, see :class:`mchammer.calculators.TargetVectorCalculator`. random_seed Seed for the random number generator used in the Monte Carlo simulation. tol Numerical tolerance. """ sqs_vector = _get_sqs_cluster_vector(cluster_space=cluster_space, target_concentrations=target_concentrations) return generate_target_structure(cluster_space=cluster_space, max_size=max_size, target_concentrations=target_concentrations, target_cluster_vector=sqs_vector, include_smaller_cells=include_smaller_cells, pbc=pbc, T_start=T_start, T_stop=T_stop, n_steps=n_steps, optimality_weight=optimality_weight, random_seed=random_seed, tol=tol)
[docs] def generate_sqs_by_enumeration(cluster_space: ClusterSpace, max_size: int, target_concentrations: dict, include_smaller_cells: bool = True, pbc: Union[Tuple[bool, bool, bool], Tuple[int, int, int]] = None, optimality_weight: float = 1.0, tol: float = 1e-5) -> Atoms: """ Given a :attr:`cluster_space`, generate a special quasirandom structure (SQS), i.e., a structure that for a given supercell size provides the best possible approximation to a random alloy [ZunWeiFer90]_. In the present case, this means that the generated structure will have a cluster vector that as closely as possible matches the cluster vector of an infintely large randomly occupied supercell. Internally the function uses a simulated annealing algorithm and the difference between two cluster vectors is calculated with the measure suggested by A. van de Walle et al. in Calphad **42**, 13-18 (2013) [WalTiwJon13]_ (for more information, see :class:`mchammer.calculators.TargetVectorCalculator`). This functions generates SQS cells by exhaustive enumeration, which means that the generated SQS cell is guaranteed to be optimal with regard to the specified measure and cell size. Parameters ---------- cluster_space Cluster space defining the lattice to be occupied. max_size Maximum supercell size. target_concentrations Concentration of each species in the target structure, per sublattice (for example ``{'Au': 0.5, 'Pd': 0.5}`` for a single sublattice Au-Pd structure, or ``{'A': {'Au': 0.5, 'Pd': 0.5}, 'B': {'H': 0.25, 'X': 0.75}}`` for a system with two sublattices. The symbols defining sublattices ('A', 'B' etc) can be found by printing the :attr:`cluster_space`. include_smaller_cells if ``True`` search among all supercell sizes including :attr:`max_size`, else search only among those exactly matching :attr:`max_size`. pbc Periodic boundary conditions for each direction, e.g., ``(True, True, False)``. The axes are defined by the cell of ``cluster_space.primitive_structure``. Default is periodic boundary in all directions. optimality_weight Controls weighting :math:`L` of perfect correlations, see :class:`mchammer.calculators.TargetVectorCalculator`. tol Numerical tolerance. """ target_concentrations = _validate_concentrations(target_concentrations, cluster_space) sqs_vector = _get_sqs_cluster_vector(cluster_space=cluster_space, target_concentrations=target_concentrations) # Translate concentrations to the format required for concentration # restricted enumeration cr: Dict[str, tuple] = {} sublattices = cluster_space.get_sublattices(cluster_space.primitive_structure) for sl in sublattices: mult_factor = len(sl.indices) / len(cluster_space.primitive_structure) if sl.symbol in target_concentrations: sl_conc = target_concentrations[sl.symbol] else: sl_conc = {sl.chemical_symbols[0]: 1.0} for species, value in sl_conc.items(): c = value * mult_factor if species in cr: cr[species] = (cr[species][0] + c, cr[species][1] + c) else: cr[species] = (c, c) # Check to be sure... c_sum = sum(c[0] for c in cr.values()) assert abs(c_sum - 1) < tol # Should never happen, but... as_list = cluster_space.as_list best_score = 1e9 if include_smaller_cells: sizes = list(range(1, max_size + 1)) else: sizes = [max_size] # Prepare primitive structure with the right boundary conditions prim = cluster_space.primitive_structure if pbc is None: pbc = (True, True, True) prim.set_pbc(pbc) # Enumerate and calculate score for each structuer for structure in enumerate_structures(prim, sizes, cluster_space.chemical_symbols, concentration_restrictions=cr): cv = cluster_space.get_cluster_vector(structure) score = compare_cluster_vectors(cv_1=cv, cv_2=sqs_vector, as_list=as_list, optimality_weight=optimality_weight, tol=tol) if score < best_score: best_score = score best_structure = structure return best_structure
[docs] def occupy_structure_randomly(structure: Atoms, cluster_space: ClusterSpace, target_concentrations: dict, random_seed: int = None) -> None: """ Occupy a structure with quasirandom order but fulfilling :attr:`target_concentrations`. Parameters ---------- structure ASE Atoms object that will be occupied randomly. cluster_space Cluster space (needed as it carries information about sublattices). target_concentrations Concentration of each species in the target structure, per sublattice (for example ``{'Au': 0.5, 'Pd': 0.5}`` for a single sublattice Au-Pd structure, or ``{'A': {'Au': 0.5, 'Pd': 0.5}, 'B': {'H': 0.25, 'X': 0.75}}`` for a system with two sublattices. The symbols defining sublattices ('A', 'B' etc) can be found by printing the :attr:`cluster_space`. random_seed Seed for the random number generator. """ rng = np.random.default_rng(random_seed) target_concentrations = _validate_concentrations(cluster_space=cluster_space, concentrations=target_concentrations) if not _concentrations_fit_structure(structure, cluster_space, target_concentrations): raise ValueError('Structure with {} atoms cannot accomodate ' 'target concentrations {}'.format(len(structure), target_concentrations)) symbols_all = [''] * len(structure) for sl in cluster_space.get_sublattices(structure): symbols: List[str] = [] # chemical_symbols in one sublattice chemical_symbols = sl.chemical_symbols if len(chemical_symbols) == 1: symbols += [chemical_symbols[0]] * len(sl.indices) else: sl_conc = target_concentrations[sl.symbol] for chemical_symbol in sl.chemical_symbols: n_symbol = int(round(len(sl.indices) * sl_conc[chemical_symbol])) symbols += [chemical_symbol] * n_symbol # Should not happen but you never know assert len(symbols) == len(sl.indices) # Shuffle to introduce randomness rng.shuffle(symbols) # Assign symbols to the right indices for symbol, lattice_site in zip(symbols, sl.indices): symbols_all[lattice_site] = symbol assert symbols_all.count('') == 0 structure.set_chemical_symbols(symbols_all)
def _validate_concentrations(concentrations: dict, cluster_space: ClusterSpace, tol: float = 1e-5) -> dict: """ Validates concentration specification against a cluster space (raises `ValueError` if they do not match). Parameters ---------- concentrations Concentration specification. cluster_space Cluster space to check against. tol Numerical tolerance. Returns ------- An adapted version of concentrations, which is always a dictionary even if there is only one sublattice. """ sls = cluster_space.get_sublattices(cluster_space.primitive_structure) if not isinstance(list(concentrations.values())[0], dict): concentrations = {'A': concentrations} # Ensure concentrations sum to 1 at each sublattice for sl_conc in concentrations.values(): conc_sum = sum(list(sl_conc.values())) if abs(conc_sum - 1.0) > tol: raise ValueError('Concentrations must sum up ' 'to 1 for each sublattice (not {})'.format(conc_sum)) # Symbols need to match on each sublattice for sl in sls: if sl.symbol not in concentrations: if len(sl.chemical_symbols) > 1: raise ValueError('A sublattice ({}: {}) is missing in ' 'target_concentrations'.format(sl.symbol, list(sl.chemical_symbols))) else: sl_conc = concentrations[sl.symbol] if tuple(sorted(sl.chemical_symbols)) != tuple(sorted(list(sl_conc.keys()))): raise ValueError('Chemical symbols on a sublattice ({}: {}) are ' 'not the same as those in the specified ' 'concentrations {}'.format(sl.symbol, list(sl.chemical_symbols), list(sl_conc.keys()))) return concentrations def _concentrations_fit_structure(structure: Atoms, cluster_space: ClusterSpace, concentrations: Dict[str, Dict[str, float]], tol: float = 1e-5) -> bool: """ Check if specified concentrations are commensurate with a certain supercell (including sublattices). Parameters ---------- structure Atomic configuration to be checked. cluster_space Corresponding cluster space. concentrations Which concentrations, per sublattice, e.g., ``{'A': {'Ag': 0.3, 'Au': 0.7}}``. tol Numerical tolerance. """ # Check that concentrations are OK in each sublattice for sublattice in cluster_space.get_sublattices(structure): if sublattice.symbol in concentrations: sl_conc = concentrations[sublattice.symbol] for conc in sl_conc.values(): n_symbol = conc * len(sublattice.indices) if abs(int(round(n_symbol)) - n_symbol) > tol: return False return True def _get_sqs_cluster_vector(cluster_space: ClusterSpace, target_concentrations: Dict[str, Dict[str, float]]) -> np.ndarray: """ Get the SQS vector for a certain cluster space and certain concentration. Here SQS vector refers to the cluster vector of an infintely large supercell with random occupation. Parameters ---------- cluster_space The kind of lattice to be occupied. target_concentrations Concentration of each species in the target structure, per sublattice (for example `{'A': {'Ag': 0.5, 'Pd': 0.5}}`). """ target_concentrations = _validate_concentrations(concentrations=target_concentrations, cluster_space=cluster_space) sublattice_to_index = {letter: index for index, letter in enumerate('ABCDEFGHIJKLMNOPQRSTUVWXYZ')} all_sublattices = cluster_space.get_sublattices( cluster_space.primitive_structure) # Make a map from chemical symbol to integer, later on used # for evaluating cluster functions. # Internally, icet sorts species according to atomic numbers. # Also check that each symbol only occurs in one sublattice. symbol_to_integer_map = {} found_species: List[str] = [] for sublattice in all_sublattices: if len(sublattice.chemical_symbols) < 2: continue atomic_numbers = [periodic_table.index(sym) for sym in sublattice.chemical_symbols] for i, species in enumerate(sorted(atomic_numbers)): found_species.append(species) symbol_to_integer_map[periodic_table[species]] = i # Target concentrations refer to all atoms, but probabilities only # to the sublattice. probabilities = {} for sl_conc in target_concentrations.values(): if len(sl_conc) == 1: continue for symbol in sl_conc.keys(): probabilities[symbol] = sl_conc[symbol] # For every orbit, calculate average cluster function cv = [1.0] for orbit in cluster_space.as_list: if orbit['order'] < 1: continue # What sublattices are there in this orbit? sublattices = [all_sublattices[sublattice_to_index[letter]] for letter in orbit['sublattices']] # What chemical symbols do these sublattices refer to? symbol_groups = [sublattice.chemical_symbols for sublattice in sublattices] # How many allowed species in each of those sublattices? nbr_of_allowed_species = [len(symbol_group) for symbol_group in symbol_groups] # Calculate contribution from every possible combination of # symbols weighted with their probability cluster_product_average = 0 for symbols in itertools.product(*symbol_groups): cluster_product = 1 for i, symbol in enumerate(symbols): mc_vector_component = orbit['multicomponent_vector'][i] species_i = symbol_to_integer_map[symbol] prod = cluster_space.evaluate_cluster_function(nbr_of_allowed_species[i], mc_vector_component, species_i) cluster_product *= probabilities[symbol] * prod cluster_product_average += cluster_product cv.append(cluster_product_average) return np.array(cv)