Source code for acat.ga.group_operators

"""Procreation operators meant to be used in symmetry-constrained
genetic algorithm (SCGA)."""
from ..settings import adsorbate_elements, site_heights
from ..adsorption_sites import (ClusterAdsorptionSites,    
                                SlabAdsorptionSites)
from ..adsorbate_coverage import (ClusterAdsorbateCoverage,
                                  SlabAdsorbateCoverage)
from ..build.action import (add_adsorbate_to_site, 
                            remove_adsorbate_from_site, 
                            remove_adsorbates_from_sites)
from ase.ga.offspring_creator import OffspringCreator
from ase.formula import Formula
from collections import defaultdict
from operator import attrgetter
import warnings
import random


[docs]class Mutation(OffspringCreator): """Base class for all particle mutation type operators. Do not call this class directly.""" def __init__(self, num_muts=1): OffspringCreator.__init__(self, num_muts=num_muts) self.descriptor = 'Mutation' self.min_inputs = 1
[docs]class GroupSubstitute(Mutation): """Substitute all the atoms in a group with a different element. The elemental composition cannot be fixed. Parameters ---------- groups : list of lists, default None The atom indices in each user-divided group. Can be obtained by `acat.build.ordering.SymmetricClusterOrderingGenerator` or `acat.build.ordering.SymmetricSlabOrderingGenerator`. If not provided here, please provide the groups in atoms.info['data']['groups'] in all intial structures. This is useful to mix structures with different group divisions in one GA run. elements : list of strs, default None Only take into account the elements specified in this list. Default is to take all elements into account. num_muts : int, default 1 The number of times to perform this operation. """ def __init__(self, groups=None, elements=None, num_muts=1): Mutation.__init__(self, num_muts=num_muts) self.descriptor = 'GroupSubstitute' self.elements = elements self.groups = groups
[docs] def substitute(self, atoms): """Does the actual substitution""" atoms = atoms.copy() if self.groups is None: groups = atoms.info['data']['groups'] else: groups = self.groups if self.elements is None: e = list(set(atoms.get_chemical_symbols())) else: e = self.elements sorted_elems = sorted(set(atoms.get_chemical_symbols())) if e is not None and sorted(e) != sorted_elems: for group in groups: torem = [] for i in group: if atoms[i].symbol not in e: torem.append(i) for i in torem: group.remove(i) itbms = random.sample(range(len(groups)), self.num_muts) for itbm in itbms: mut_group = groups[itbm] other_elements = [e for e in self.elements if e != atoms[mut_group[0]].symbol] to_element = random.choice(other_elements) atoms.symbols[mut_group] = len(mut_group) * to_element return atoms
[docs] def get_new_individual(self, parents): f = parents[0] indi = self.substitute(f) indi = self.initialize_individual(f, indi) indi.info['data']['parents'] = [f.info['confid']] return (self.finalize_individual(indi), self.descriptor + ':Parent {0}'.format(f.info['confid']))
[docs]class GroupPermutation(Mutation): """Permutes the elements in two random groups. The elemental composition can be fixed. Parameters ---------- groups : list of lists, default None The atom indices in each user-divided group. Can be obtained by `acat.build.ordering.SymmetricClusterOrderingGenerator` or `acat.build.ordering.SymmetricSlabOrderingGenerator`. If not provided here, please provide the groups in atoms.info['data']['groups'] in all intial structures. This is useful to mix structures with different group divisions in one GA run. elements : list of strs, default None Only take into account the elements specified in this list. Default is to take all elements into account. keep_composition : bool, defulat False Whether the elemental composition should be the same as in the parents. num_muts : int, default 1 The number of times to perform this operation. """ def __init__(self, groups=None, elements=None, keep_composition=False, num_muts=1): Mutation.__init__(self, num_muts=num_muts) self.descriptor = 'GroupPermutation' self.elements = elements self.keep_composition = keep_composition self.groups = groups
[docs] def get_new_individual(self, parents): f = parents[0].copy() diffatoms = len(set(f.numbers)) assert diffatoms > 1, 'Permutations with one atomic type is not valid' indi = self.initialize_individual(f) indi.info['data']['parents'] = [f.info['confid']] if self.groups is None: groups = indi.info['data']['groups'] else: groups = self.groups for _ in range(self.num_muts): GroupPermutation.mutate(f, groups, self.elements, self.keep_composition) for atom in f: indi.append(atom) return (self.finalize_individual(indi), self.descriptor + ':Parent {0}'.format(f.info['confid']))
[docs] @classmethod def mutate(cls, atoms, groups, elements=None, keep_composition=False): """Do the actual permutation.""" if elements is None: e = list(set(atoms.get_chemical_symbols())) else: e = elements sorted_elems = sorted(set(atoms.get_chemical_symbols())) if e is not None and sorted(e) != sorted_elems: for group in groups: torem = [] for i in group: if atoms[i].symbol not in e: torem.append(i) for i in torem: group.remove(i) if keep_composition: dd = defaultdict(list) for gi, group in enumerate(groups): dd[len(group)].append(gi) items = list(dd.items()) random.shuffle(items) mut_gis = None for k, v in items: if len(v) > 1: mut_gis = v break if mut_gis is None: return random.shuffle(mut_gis) i1 = mut_gis[0] mut_group1 = groups[i1] options = [i for i in mut_gis[1:] if atoms[mut_group1[0]].symbol != atoms[groups[i][0]].symbol] else: i1 = random.randint(0, len(groups) - 1) mut_group1 = groups[i1] options = [i for i in range(0, len(groups)) if atoms[mut_group1[0]].symbol != atoms[groups[i][0]].symbol] if not options: return i2 = random.choice(options) mut_group2 = groups[i2] atoms.symbols[mut_group1+mut_group2] = len(mut_group1) * atoms[ mut_group2[0]].symbol + len(mut_group2) * atoms[mut_group1[0]].symbol
[docs]class AdsorbateGroupSubstitute(Mutation): """Substitute all the adsorbates (or vacancies) in a site group with a different element. Parameters ---------- adsorbate_species : str or list of strs A list of possible adsorbate species to be added to the surface. site_groups : list of lists, default None The site indices in each user-divided group. Can be obtained by `acat.build.ordering.SymmetricPatternGenerator`. If not provided here, please provide the groups in atoms.info['data']['groups'] in all intial structures. This is useful to mix structures with different group divisions in one GA run. max_species : int, default None The maximum allowed adsorbate species (excluding vacancies) for a single structure. Allow all adsorbatae species if not specified. heights : dict, default acat.settings.site_heights A dictionary that contains the adsorbate height for each site type. Use the default height settings if the height for a site type is not specified. adsorption_sites : acat.adsorption_sites.ClusterAdsorptionSites object or acat.adsorption_sites.SlabAdsorptionSites object, default None Provide the built-in adsorption sites class to accelerate the pattern generation. Make sure all the structures have the same periodicity and atom indexing. If composition_effect=True, you should only provide adsorption_sites when the surface composition is fixed. If this is not provided, the arguments for identifying adsorption sites can still be passed in by **kwargs. remove_neighbor_sites : bool, default True Whether to remove the neighboring sites around each occupied site. num_muts : int, default 1 The number of times to perform this operation. """ def __init__(self, adsorbate_species, site_groups=None, max_species=None, heights=site_heights, adsorption_sites=None, remove_neighbor_sites=True, num_muts=1, **kwargs): Mutation.__init__(self, num_muts=num_muts) self.descriptor = 'AdsorbateGroupSubstitute' self.adsorbate_species = adsorbate_species if is_list_or_tuple( adsorbate_species) else [adsorbate_species] self.site_groups = site_groups self.max_species = max_species self.heights = heights self.adsorption_sites = adsorption_sites self.remove_neighbor_sites = remove_neighbor_sites self.kwargs = {'allow_6fold': False, 'composition_effect': False, 'ignore_bridge_sites': True, 'label_sites': False} self.kwargs.update(kwargs) if self.adsorption_sites is not None: for k in self.kwargs.keys(): self.kwargs[k] = attrgetter(k)(self.adsorption_sites) self.__dict__.update(self.kwargs)
[docs] def substitute(self, atoms): """Does the actual substitution of the adsorbates""" atoms = atoms.copy() if True in atoms.pbc: if self.adsorption_sites is not None: sas = self.adsorption_sites sas.update(atoms) else: sas = SlabAdsorptionSites(atoms, **self.kwargs) sac = SlabAdsorbateCoverage(atoms, sas, dmax=self.dmax) else: if self.adsorption_sites is not None: sas = self.adsorption_sites sas.update(atoms) else: sas = ClusterAdsorptionSites(atoms, **self.kwargs) sac = ClusterAdsorbateCoverage(atoms, sas, dmax=self.dmax) hsl = sac.hetero_site_list if self.site_groups is None: groups = atoms.info['data']['groups'] else: groups = self.site_groups ngroups = len(groups) indices = list(range(ngroups)) if self.remove_neighbor_sites: nsl = sas.get_neighbor_site_list() indexes = indices.copy() random.shuffle(indexes) itbms = set() for j in range(len(indexes)): itbms.add(j) if hsl[groups[j][0]]['occupied']: inset = {nsl[i] for i in groups[j]} ings = [nj for nj in indexes if not set(groups[nj]).isdisjoint(inset)] itbms = set(itbms) - set(ings) if len(itbms) == self.num_muts: break itbms = list(itbms) else: itbms = random.sample(indices, self.num_muts) not_mut = [j for j in indices if j not in itbms] random.shuffle(not_mut) if self.max_species is None: options = self.adsorbate_species + ['vacancy'] else: not_mut_specs = {hsl[j][0]['fragment'] for j in not_mut if hsl[j][0]['occupied']} diff = self.max_species - len(not_mut_specs) if diff > 0: others = [sp for sp in self.adsorbate_species if sp not in not_mut_specs] options = list(not_mut_specs) + random.sample(others, diff) + ['vacancy'] elif diff == 0: options = list(not_mut_specs) + ['vacancy'] else: options = list(not_mut_specs) + ['vacancy'] warnings.warn('The number of adsorbate species exceeds ' + 'the maximum allowed number.') changes = [None] * ngroups newvs = set() indices = itbms + not_mut for idx in indices: group = groups[idx] st0 = hsl[group[0]] if not set(group).isdisjoint(newvs): if st0['occupied']: changes[idx] = 'vacancy' elif idx in itbms: if st0['occupied']: spec = st0['fragment'] else: spec = 'vacancy' other_specs = [sp for sp in options if sp != spec] to_spec = random.choice(other_specs) changes[idx] = to_spec if to_spec == 'vacancy': newvs.update(group) elif self.remove_neighbor_sites: newvs.update([i for k in group for i in nsl[k]]) rmsites = [hsl[j] for idx, to_spec in enumerate(changes) if to_spec is not None for j in groups[idx]] remove_adsorbates_from_sites(atoms, sites=rmsites, remove_fragments=True) for idx, to_spec in enumerate(changes): if to_spec not in [None, 'vacancy']: for j in groups[idx]: st = hsl[j] height = self.heights[st['site']] add_adsorbate_to_site(atoms, to_spec, st, height) if True in atoms.pbc: nsac = SlabAdsorbateCoverage(atoms, sas, dmax=self.dmax) else: nsac = ClusterAdsorbateCoverage(atoms, sas, dmax=self.dmax) atoms.info['data']['adsorbates'] = nsac.get_adsorbates(self.adsorbate_species) return atoms
[docs] def get_new_individual(self, parents): f = parents[0] indi = self.substitute(f) indi = self.initialize_individual(f, indi) indi.info['data']['parents'] = [f.info['confid']] return (self.finalize_individual(indi), self.descriptor + ':Parent {0}'.format(f.info['confid']))
[docs]class AdsorbateGroupPermutation(Mutation): """Permutes the elements in two random site groups. Parameters ---------- adsorbate_species : str or list of strs A list of possible adsorbate species to be added to the surface. site_groups : list of lists, default None The site indices in each user-divided group. Can be obtained by `acat.build.ordering.SymmetricPatternGenerator`. If not provided here, please provide the groups in atoms.info['data']['groups'] in all intial structures. This is useful to mix structures with different group divisions in one GA run. heights : dict, default acat.settings.site_heights A dictionary that contains the adsorbate height for each site type. Use the default height settings if the height for a site type is not specified. adsorption_sites : acat.adsorption_sites.ClusterAdsorptionSites object or acat.adsorption_sites.SlabAdsorptionSites object, default None Provide the built-in adsorption sites class to accelerate the pattern generation. Make sure all the structures have the same periodicity and atom indexing. If composition_effect=True, you should only provide adsorption_sites when the surface composition is fixed. If this is not provided, the arguments for identifying adsorption sites can still be passed in by **kwargs. remove_neighbor_sites : bool, default True Whether to remove the neighboring sites around each occupied site. num_muts : int, default 1 The number of times to perform this operation. """ def __init__(self, adsorbate_species, site_groups, heights=site_heights, adsorption_sites=None, remove_neighbor_sites=True, num_muts=1): Mutation.__init__(self, num_muts=num_muts) self.descriptor = 'AdsorbateGroupPermutation' self.adsorbate_species = adsorbate_species if is_list_or_tuple( adsorbate_species) else [adsorbate_species] self.site_groups = site_groups self.heights = heights self.adsorption_sites = adsorption_sites self.remove_neighbor_sites = remove_neighbor_sites self.kwargs = {'allow_6fold': False, 'composition_effect': False, 'ignore_bridge_sites': True, 'label_sites': False} self.kwargs.update(kwargs) if self.adsorption_sites is not None: for k in self.kwargs.keys(): self.kwargs[k] = attrgetter(k)(self.adsorption_sites) self.__dict__.update(self.kwargs)
[docs] def get_new_individual(self, parents): f = parents[0].copy() diffatoms = len(set(f.numbers)) assert diffatoms > 1, 'Permutations with one atomic type is not valid' indi = self.initialize_individual(f) indi.info['data']['parents'] = [f.info['confid']] if self.site_groups is None: site_groups = indi.info['data']['groups'] else: site_groups = self.site_groups if True in indi.pbc: if self.adsorption_sites is not None: sas = self.adsorption_sites sas.update(indi) else: sas = SlabAdsorptionSites(indi, **self.kwargs) else: if self.adsorption_sites is not None: sas = self.adsorption_sites sas.update(indi) else: sas = ClusterAdsorptionSites(indi, **self.kwargs) for _ in range(self.num_muts): GroupPermutation.mutate(f, site_groups, self.heights, sas, self.remove_neighbor_sites, self.dmax) for atom in f: indi.append(atom) if True in indi.pbc: nsac = SlabAdsorbateCoverage(indi, sas, dmax=self.dmax) else: nsac = ClusterAdsorbateCoverage(indi, sas, dmax=self.dmax) indi.info['data']['adsorbates'] = nsac.get_adsorbates(self.adsorbate_species) return (self.finalize_individual(indi), self.descriptor + ':Parent {0}'.format(f.info['confid']))
[docs] @classmethod def mutate(cls, atoms, groups, heights, adsorption_sites, remove_neighbor_sites, dmax): """Do the actual permutation of the adsorbates.""" if True in atoms.pbc: sac = SlabAdsorbateCoverage(atoms, sas, dmax=dmax) else: sac = ClusterAdsorbateCoverage(atoms, sas, dmax=dmax) hsl = sac.hetero_site_list ngroup = len(groups) i1 = random.randint(0, len(groups) - 1) st01 = hsl[groups[i1][0]] ings = [] if remove_neighbor_sites: nsl = sas.get_neighbor_site_list() if st01['occupied']: ings += [nj for nj in indices if not set(groups[nj]).isdisjoint(nsl[i1])] options = [j for j in range(ngroup) if (j not in ings) and (st01['fragment'] != hsl[groups[j][0]]['fragment'])] if not options: return i2 = random.choice(options) st02 = hsl[groups[i2][0]] if st01['occupied']: to_spec2 = st01['fragment'] else: to_spec2 = 'vacancy' if st02['occupied']: to_spec1 = st02['fragment'] else: to_spec1 = 'vacancy' indices = list(range(ngroups)) not_mut = [j for j in indices if j not in [i1, i2]] random.shuffle(not_mut) changes = [None] * ngroup newvs = set() indices = [i1, i2] + not_mut for idx in indices: group = groups[idx] st0 = hsl[group[0]] if not set(group).isdisjoint(newvs): if st0['occupied']: changes[idx] = 'vacancy' elif idx == i1: changes[idx] = to_spec1 if to_spec1 == 'vacancy': newvs.update(group) elif remove_neighbor_sites: newvs.update([i for k in group for i in nsl[k]]) elif idx == i2: changes[idx] = to_spec2 if to_spec2 == 'vacancy': newvs.update(group) elif remove_neighbor_sites: newvs.update([i for k in group for i in nsl[k]]) rmsites = [hsl[j] for idx, to_spec in enumerate(changes) if to_spec is not None for j in groups[idx]] remove_adsorbates_from_sites(atoms, sites=rmsites, remove_fragments=True) for idx, to_spec in enumerate(changes): if to_spec not in [None, 'vacancy']: for j in groups[idx]: st = hsl[j] height = heights[st['site']] add_adsorbate_to_site(atoms, to_spec, st, height)
[docs]class Crossover(OffspringCreator): """Base class for all particle crossovers. Do not call this class directly.""" def __init__(self): OffspringCreator.__init__(self) self.descriptor = 'Crossover' self.min_inputs = 2
[docs]class GroupCrossover(Crossover): """Merge the elemental distributions in two half groups from different structures together. The elemental composition can be fixed. Parameters ---------- groups : list of lists, default None The atom indices in each user-divided group. Can be obtained by `acat.build.ordering.SymmetricClusterOrderingGenerator` or `acat.build.ordering.SymmetricSlabOrderingGenerator`. If not provided here, please provide the groups in atoms.info['data']['groups'] in all intial structures. This is useful to mix structures with different group divisions in one GA run. Note that for crossover please ensure the parents always have the same groups elements : list of strs, default None Only take into account the elements specified in this list. Default is to take all elements into account. keep_composition : bool, defulat False Whether the elemental composition should be the same as in the parents. """ def __init__(self, groups=None, elements=None, keep_composition=False): Crossover.__init__(self) self.groups = groups self.elements = elements self.keep_composition = keep_composition self.descriptor = 'GroupCrossover'
[docs] def get_new_individual(self, parents): f, m = parents indi = f.copy() if self.groups is None: assert f.info['data']['groups'] == m.info['data']['groups'] groups = indi.info['data']['groups'] else: groups = self.groups.copy() if self.elements is None: e = list(set(f.get_chemical_symbols())) else: e = self.elements sorted_elems = sorted(set(f.get_chemical_symbols())) if e is not None and sorted(e) != sorted_elems: for group in groups: torem = [] for i in group: if f[i].symbol not in e: torem.append(i) for i in torem: group.remove(i) random.shuffle(groups) if self.keep_composition: def fix_composition_swaps(groups1, groups2): indices = sorted([i for j in groups1 for i in j]) zipped = list(map(list, zip(groups1, groups2))) gids = [i for i, (groups1, groups2) in enumerate(zipped) if groups1 != groups2] # If solution not found in 1000 iterations, we say there # is no possible solution at all gids_list = [] for j in range(1000): random.shuffle(gids) if gids in gids_list: continue gids_list.append(gids.copy()) for n, i in enumerate(gids): zipped[i].reverse() if indices == sorted(idx for groups1, _ in zipped for idx in groups1): return gids[:n+1] zipped = list(map(list, zip(groups1, groups2))) return [] fsyms = [list(f.symbols[g]) for g in groups] msyms = [list(m.symbols[g]) for g in groups] swap_ids = fix_composition_swaps(fsyms, msyms) mids = [i for j in swap_ids for i in groups[j]] else: mgroups = groups[len(groups)//2:] mids = [i for group in mgroups for i in group] indi.symbols[mids] = m.symbols[mids] indi = self.initialize_individual(f, indi) indi.info['data']['parents'] = [i.info['confid'] for i in parents] indi.info['data']['operation'] = 'crossover' parent_message = ':Parents {0} {1}'.format(f.info['confid'], m.info['confid']) return (self.finalize_individual(indi), self.descriptor + parent_message)