Source code for gg.modifiers.add

import random
from typing import Union
from itertools import combinations
from ase.collections import g2
from ase.build import molecule
from ase import Atoms
from ase.data import chemical_symbols
from gg.utils import (
    custom_copy,
    NoReasonableStructureFound,
)
from gg.utils_add import generate_add_mono, rotate_mono, rotate_bi, generate_add_bi
from gg.utils_graph import get_unique_atoms
from gg.sites import Sites
from gg.modifiers.modifiers import ParentModifier

from gg.data import adsorbates


[docs] class Add(ParentModifier): """Modifier that adds an monodentate adsorbate""" @staticmethod def _unique_symbols(symbols: list) -> list: return list(dict.fromkeys(symbols)) @staticmethod def _make_adsorbate_contiguous(atoms: Atoms) -> Atoms: """Move adsorbate atoms to a contiguous image under periodic boundary conditions.""" ads = atoms.copy() if len(ads) < 2 or not ads.pbc.any(): return ads positions = ads.get_positions() contiguous_positions = positions.copy() placed = {0} unplaced = set(range(1, len(ads))) while unplaced: best_pair = None best_vector = None best_distance = float("inf") for source in placed: targets = list(unplaced) vectors = ads.get_distances(source, targets, mic=True, vector=True) distances = ads.get_distances(source, targets, mic=True) for target, vector, distance in zip(targets, vectors, distances): if distance < best_distance: best_pair = (source, target) best_vector = vector best_distance = distance source, target = best_pair contiguous_positions[target] = contiguous_positions[source] + best_vector placed.add(target) unplaced.remove(target) ads.set_positions(contiguous_positions) return ads def __init__( self, surface_sites: Sites, ads: str, surf_coord: int, surf_sym: list, ads_id: Union[str] = None, ads_dist: Union[float] = None, print_movie: bool = False, unique: bool = True, ads_rotate: bool = True, weight: float = 1, normal_method: str = "svd", unique_method: str = "fullgraph", unique_depth: int = 3, tag: bool = True, ): """ Args: surface_sites (gg.Sites): Class that figures out surface sites ads (str) or (ase.Atoms): Adsorbate to add surf_coord (list[int]): How many bonds the adsorbate will make with the surface surf_sym (list[str]): Surface elements where adsorbate can add ads_id (list[float]): Strings denoting chemical symbol of adsorbate atom Defaults to None ads_dist (str, optional): Distance of adsorbate from substrate. Defaults to covalent radii of ads_id from ase database. print_movie (bool, optional): return a movie of all sites or one random site. Defaults to False. unique (bool, optional): return only unique sites. Defaults to True. normal_method (str): Determines how normals are calculated. It could be "svd" or "mean" Defaults to "svd" unique_method (str): Determines how uniqueness is calculated. User can specify atom symbols/mol to construct subgraphs e.g: unique_method = ["CO"] Defaults to "fullgraph" unique_depth (int): Determines the depth of subgraphs created to calculate uniqueness. Defaults to 3. If unique_method is "fullgraph" the value is ignored tag (bool): add to tag=-1 to the adsorbate (imp for clusters) Defaults to True. weight (float): weight for gcbh. Defaults to 1. """ super().__init__(weight) self.ss = surface_sites if isinstance(ads, str): if ads in adsorbates: self.ads = adsorbates[ads] elif ads in g2.names: self.ads = molecule(ads) elif ads in chemical_symbols: self.ads = Atoms(ads, positions=[(0, 0, 0)]) else: raise RuntimeError(f"Cannot convert string to Formula {ads}") elif isinstance(ads, Atoms): self.ads = custom_copy(ads) self.ads = self._make_adsorbate_contiguous(self.ads) if isinstance(surf_sym, list): self.surf_sym = self._unique_symbols(surf_sym) else: self.surf_sym = surf_sym if isinstance(surf_coord, int): self.surf_coord = [surf_coord] elif isinstance(surf_coord, list): self.surf_coord = surf_coord else: raise NoReasonableStructureFound("Please enter proper value for surf_coord") if ads_id: if isinstance(ads_id, list) and all(isinstance(item, str) for item in ads_id): self.ads_id = self._unique_symbols(ads_id) else: self.ads_id = ads_id if ads_dist: self.ads_dist = ads_dist else: self.ads_dist = ads_id self.print_movie = print_movie self.unique = unique self.ads_rotate = ads_rotate self.method = normal_method self.tag = tag self.unique_method = unique_method self.unique_depth = unique_depth def get_all_adsorbates(self, atoms: Atoms, chem_symbol_list) -> list: """ Args: atoms (ase.Atoms): Returns: list: """ ads_list = [] ads_dist_list = [] for ind, atom in enumerate(atoms): if atom.symbol in chem_symbol_list: ads = rotate_mono(atoms.copy(), ind) ads_list.append(ads) if isinstance(self.ads_dist, float) or isinstance(self.ads_dist, int): ads_dist_list.append(self.ads_dist) else: ads_dist_list.append(atom.symbol) return ads_list, ads_dist_list
[docs] def get_modified_atoms(self, atoms: Atoms) -> Atoms: """ Args: atoms (ase.Atoms): The atoms object on which the adsorbate will be added Returns: ase.Atoms if print_movie = True list[ase.Atoms] if print_movie = False """ # Check multiple possibilities of adsorbate if isinstance(self.ads_id, list): if all(isinstance(item, str) for item in self.ads_id): ads_list, ad_dist_list = self.get_all_adsorbates(self.ads, self.ads_id) else: ads_list = [self.ads] ad_dist_list = [self.ads_dist] self.atoms = atoms df_ind = self.ss.get_sites(self.atoms) g = self.ss.get_graph(self.atoms) index = [ind for ind in df_ind if self.atoms[ind].symbol in self.surf_sym] if not index: raise NoReasonableStructureFound( "No surface sites found, check your Sites Class" ) movie = [] for i, ads in enumerate(ads_list): # Read gg.utils_add to understand the working movie += generate_add_mono( self.atoms, ads, g, index, self.surf_coord, ad_dist=ad_dist_list[i], contact_error=self.ss.contact_error, method=self.method, tag=self.tag, ) if not movie: raise NoReasonableStructureFound( "Movie was empty, most likely due to issues with atoms touching in Add Modifier" ) if self.print_movie: if self.unique: return get_unique_atoms( movie, max_bond=self.ss.max_bond, max_bond_ratio=self.ss.max_bond_ratio, unique_method=self.unique_method, depth=self.unique_depth, ) else: return movie else: return random.sample(movie, 1)[0]
[docs] class AddBi(Add): """Modifier that adds an adsorbate at certain specific sites""" def __init__( self, surface_sites: Sites, ads: str, surf_coord: int, surf_sym: list, ads_id: list, ads_dist: Union[float, str] = None, print_movie: bool = False, unique: bool = True, ads_rotate: bool = True, add_ads_error: float = 0.5, normal_method: str = "mean", tag: bool = True, unique_method: str = "fullgraph", unique_depth: int = 3, weight: float = 1, ): """ Args: surface_sites (gg.Sites): Class that figures out surface sites. ads (str) or (ase.Atoms): Adsorbate to add. surf_coord (list[int]): How many bonds the adsorbate will make with the surface. surf_sym (list[str]): Surface elements where adsorbate can add. ads_id (list of [int or str]): Strings denoting chemical symbol of adsorbate atom. ads_dist (list of [float or str, optional]): Distance of adsorbate from surface site. If its string denoting chemical symbol of adsorbate atom, then distance is set by atomic radii. Defaults to covalent radii of atoms mentioned in ads_id. print_movie (bool, optional): Return a movie of all sites or one random site. Defaults to False. unique (bool, optional): Return only unique sites. Defaults to True. ads_rotate (bool,optional): Rotate atoms such that they point in +z direction. Defaults to True. add_ads_error (float): The error in distance between bidentate adsorbate sites. Defaults to 0.5 (equivalent to 50%) normal_method (str): Determines how normals are calculated. It could be "svd" or "mean" Defaults to "mean" unique_method (str): Determines how uniqueness is calculated. User can specify atom symbols/mol to construct subgraphs e.g: unique_method = ["C"] Defaults to "fullgraph" unique_depth (int): Determines the depth of subgraphs created to calculate uniqueness. Defaults to 3. If unique_method is "fullgraph" the value is ignored tag (bool): add to tag=-1 to the adsorbate (imp for clusters) Defaults to 1. weight (float): weight for gcbh. Defaults to 1. """ super().__init__( surface_sites=surface_sites, ads=ads, surf_coord=surf_coord, surf_sym=surf_sym, ads_dist=ads_dist, print_movie=print_movie, unique=unique, ads_rotate=ads_rotate, weight=weight, normal_method=normal_method, tag=tag, unique_method=unique_method, unique_depth=unique_depth, ) if isinstance(ads_id, list) and all(isinstance(item, str) for item in ads_id): self.ads_id_list = self._unique_symbols(ads_id) else: self.ads_id_list = ads_id if all(isinstance(item, str) for item in self.ads_id_list): self.ads_id_list, self.ads, self.ads_dist_list = self.get_all_adsorbates( self.ads, self.ads_id_list ) self.ads_add_error = add_ads_error def get_all_adsorbates(self, atoms: Atoms, chem_symbol_list) -> list: """ Args: atoms (ase.Atoms): """ list_ads = [] ads_list = [] ads_dist_list = [] possible = list(combinations(range(len(atoms)), 2)) for ind in possible: ind_1, ind_2 = ind ads_id = [ind_1, ind_2] if ( atoms[ind_1].symbol in chem_symbol_list and atoms[ind_2].symbol in chem_symbol_list ): ads = rotate_bi(atoms.copy(), ads_id) list_ads.append(ads_id) ads_list.append(ads) if isinstance(self.ads_dist, float) or isinstance(self.ads_dist, int): ads_dist_list.append([self.ads_dist, self.ads_dist]) else: ads_dist_list.append( [atoms[ads_id[0]].symbol, atoms[ads_id[1]].symbol] ) return list_ads, ads_list, ads_dist_list
[docs] def get_modified_atoms(self, atoms: Atoms) -> Atoms: """ Args: atoms (ase.Atoms): The atoms object on which the adsorbate will be added Returns: ase.Atoms if print_movie = True, list[ase.Atoms] if print_movie = False """ self.atoms = atoms df_ind = self.ss.get_sites(self.atoms) g = self.ss.get_graph(self.atoms) index = [ind for ind in df_ind if self.atoms[ind].symbol in self.surf_sym] if not index: raise NoReasonableStructureFound( "No surface sites found, check your Sites Class" ) movie = [] for i, ads_id in enumerate(self.ads_id_list): # Read gg.utils_add to understand the working movie += generate_add_bi( self.atoms, self.ads[i], g, index, self.surf_coord, ad_dist=self.ads_dist_list[i], ads_index=ads_id, contact_error=self.ss.contact_error, ads_add_error=self.ads_add_error, method=self.method, tag=self.tag, ) if not movie: raise NoReasonableStructureFound( "Movie was empty, most likely due to issues with atoms touching in Add Modifier" ) if self.print_movie: if self.unique: return get_unique_atoms( movie, max_bond=self.ss.max_bond, max_bond_ratio=self.ss.max_bond_ratio, depth=self.unique_depth, ) else: return movie else: return random.sample(movie, 1)[0]