def __init__(self) -> None: ''' A class to analyze the results of calculations ''' input_file = 'input.json' try: with open(input_file, 'r') as f: input_json = json.load(f) surface_types_and_repeats = input_json['surface_types_and_repeats'] metal_atom = input_json['metal_atom'] self.yamlfile = input_json['yamlfile'] self.facetpaths = IO().get_facetpaths( metal_atom, surface_types_and_repeats.keys()) self.reactions = IO().open_yaml_file(self.yamlfile) self.ev_to_kjmol = 23.06035 * 4.184 self.slab_paths = [ '{}_big_slab_opt.xyz'.format(facetpath) for facetpath in self.facetpaths ] self.current_dir = os.getcwd() except FileNotFoundError: print('! input.json not found. \n' ' Make sure {} matches exactly "input.json"\n' '\n' ' You can use this module only from your main working \n' ' directory - the one with all your input files and \n' ' "facetpath" dir and job_files dir.\n' '\n' ' Your current directory is:\n' ' {}'.format(input_file, os.getcwd())) sys.exit()
def get_barrier_all(self) -> Dict[str, Dict[str, str]]: ''' Get barrier heights for all rxn_names and facetpaths Returns ------- ts_ener : Dict[str,Dict[str,str]] a dictionary with all barrier heights (kj/mol) in a format like below >>> ts_ener = {'Cu_111_OH_O+H': {'TS_00': '155.27', 'TS_01': '157.97'}} ''' ts_ener = {} for facetpath, slab_path in zip(self.facetpaths, self.slab_paths): minima_path = os.path.join(facetpath, 'minima') for rxn in self.reactions: r_name_list, p_name_list = IO.get_reactants_and_products(rxn) rxn_name = IO.get_rxn_name(rxn) ts_path = os.path.join(self.current_dir, facetpath, rxn_name, 'TS_estimate_unique') ts_ener[facetpath + '_' + rxn_name] = self.get_barrier( minima_path, ts_path, facetpath, r_name_list, p_name_list, slab_path) return ts_ener
def prepare_ts_estimate(self, rxn: Dict[str, str], scfactor: float, scfactor_surface: float, pytemplate_xtb: str, species_list: List[str], reacting_atoms: Dict[str, int], metal_atom: str, scaled1: bool, scaled2: bool) -> None: ''' Prepare TS estimates for subsequent xTB calculations Parameters ___________ rxn : dict(yaml[str:str]) a dictionary with info about the paricular reaction. This can be view as a splitted many reaction .yaml file to a single reaction .yaml file scfator : float a scaling factor to scale a bond distance between atoms taking part in the reaction e.g. 1.4 scfactor_surface : float a scaling factor to scale the target bond distance, i.e. the average distance between adsorbed atom and the nearest surface atom. Helpful e.g. when H is far away form the surface in TS, whereas for minima it is close to the surface e.g. 1.0 pytemplate_xtb : python script a template file for penalty function minimization job species_list : List[str] a list of species which atoms take part in the reaction, i.e. for ['CO2'] ['C'] is taking part in reaction e.g. ['O', 'H'] or ['CO2', 'H'] reacting_atoms : Dict[str, int] keys are sybols of atoms that takes part in reaction whereas, values are their indicies metal_atom : str a checmical symbol for the surface atoms (only metallic surfaces are allowed) scaled1 : bool specify whether use the optional scfactor_surface for the species 1 (sp1) scaled2 : bool specify whether use the optional scfactor_surface for the species 2 (sp2) ''' r_name_list, p_name_list = IO.get_reactants_and_products(rxn) rxn_name = IO.get_rxn_name(rxn) ts_estimate_path = os.path.join(self.creation_dir, self.facetpath, rxn_name, self.ts_estimate_dir) self.TS_placer(ts_estimate_path, scfactor, rxn, rxn_name, r_name_list, p_name_list, reacting_atoms) self.filtered_out_equiv_ts_estimates(ts_estimate_path, rxn_name) self.set_up_penalty_xtb(ts_estimate_path, pytemplate_xtb, species_list, reacting_atoms, metal_atom, scaled1, scfactor_surface)
def adjacency_to_3d(self) -> None: ''' Place adsorbates on the surface .. todo:: Add support for a bidentate adsorption ''' all_species_symbols = IO.get_all_unique_species_symbols(self.yamlfile) all_images_with_bonds = IO.get_all_unique_images_with_bonds( self.yamlfile) # prepare surface for placing adsorbates grslab = self.get_grslab() ads_builder = Builder(grslab) # build adsorbates structures = dict() for sp_symbol, unique_images_with_bonds in \ zip(all_species_symbols, all_images_with_bonds.values()): for bond, sp_gratoms in unique_images_with_bonds.items(): if len(sp_gratoms) == 0: continue # which atom connects to the surface bonded = [bond] try: # put adsorbates on the surface structs = ads_builder.add_adsorbate(sp_gratoms, index=-1, bonds=bonded) structures[str(sp_symbol)] = structs except IndexError: print('sp_gratoms, sp_gratoms.edges, sp.gratoms.tags') print(sp_gratoms, sp_gratoms.edges, sp_gratoms.get_tags()) for sp_symbol, adsorbate in structures.items(): # create directory where all adsorbates are stored savedir = os.path.join(self.creation_dir, self.facetpath, 'minima', sp_symbol) os.makedirs(savedir, exist_ok=True) for prefix, structure in enumerate(adsorbate): big_slab_ads = self.big_slab + structure[self.nslab:] # save adsorbates as .xyz and .png write( os.path.join(savedir, '{}.xyz'.format(str(prefix).zfill(2))), big_slab_ads) write( os.path.join(savedir, '{}.png'.format(str(prefix).zfill(2))), big_slab_ads)
def get_ts_out_files(ts_path: str) -> List[str]: ''' Get TS :literal:`*.out` files Parameters ts_path : str a path to main TS directory, e.g. >>> ts_path = 'Cu_111/TS_estimate_unique' Returns ------- ts_out_file_list : List[str] a sorted list with paths to Sella's .out files for each TSs ''' ts_out_file_list = [] unique_ts_prefixes = IO.get_unique_final_ts_prefixes(ts_path) try: for outfile in os.listdir(ts_path): for uq_ts_prefix in unique_ts_prefixes: if outfile.startswith(uq_ts_prefix) and outfile.endswith( 'out'): uq_ts_outfile_path = os.path.join(ts_path, outfile) ts_out_file_list.append(str(uq_ts_outfile_path)) except FileNotFoundError: pass return sorted(ts_out_file_list)
def get_all_distances(self) -> None: ''' Get distances between reacting species for ts, forward and reverse structure. ''' all_rxn_names = IO().get_list_all_rxns_names(self.yamlfile) for rxn_name in all_rxn_names: ts_dist_dict = self.get_ts_dist(rxn_name) f_dist_dict, r_dist_dict = self.get_forward_and_reverse_dist( rxn_name) AfterTS.print_table(ts_dist_dict, f_dist_dict, r_dist_dict)
def plot(self, plot_filename: str = None, max_barrier: float = None, x_size_in: float = 8, y_size_in: float = 6) -> None: ''' Plot all results and automatically detect how many reactions and facet types exist. Parameters ---------- plot_filename: str, optional a name of the file of generated plot, by default ``None`` max_barrier: float, optional all barriers lower then max_barrier will be ploted, by default ``None`` x_size_in: float, optional x size of the plot in inches, by default ``8`` y_size_in: float, optional y size of the plot in inches, by default ``6`` ''' reaction_energies = self.get_reaction_energies_all() activation_barriers = self.get_barrier_all() all_rxn_names = IO().get_list_all_rxns_names(self.yamlfile) n_facets = len(self.facetpaths) n_rxns = len(all_rxn_names) _, axes = plt.subplots(n_facets, n_rxns, squeeze=False) # print(axes) for num, rxn in enumerate(self.reactions): for ax, facetpath, in zip(axes, self.facetpaths): rxn_name = IO().get_rxn_name(rxn) key = facetpath + '_' + rxn_name Results.plot_rxn(key, reaction_energies, activation_barriers, rxn_name, ax, num, plot_filename, max_barrier, x_size_in, y_size_in)
def get_reaction_energies_all(self) -> None: ''' Get reactiom energy (kj/mol) for all facetpaths and reactions Returns ------- reaction_energies : Dict[str:float] a dictionary with reaction energies for a given facetpath and rxn_name, e.g. >>> reaction_energies = {'Cu_111_OH_O+H': 70.81} ''' reaction_energies = {} for facetpath, slab_path in zip(self.facetpaths, self.slab_paths): minima_path = os.path.join(facetpath, 'minima') for rxn in self.reactions: r_name_list, p_name_list = IO.get_reactants_and_products(rxn) rxn_name = IO.get_rxn_name(rxn) key = facetpath + '_' + rxn_name reaction_energies[key] = float( self.get_reaction_energy(minima_path, facetpath, r_name_list, p_name_list, slab_path)) return reaction_energies
def __init__( self, facetpath: str, yamlfile: str, slab: str, repeats: Tuple[int, int, int], creation_dir: PosixPath) -> None: self.facetpath = facetpath self.yamlfile = yamlfile self.repeats = repeats self.creation_dir = creation_dir self.slab_atom_path = os.path.join(self.creation_dir, slab) self.nslab = len(read(self.slab_atom_path) * self.repeats) self.n_kpts = IO().get_kpoints(self.repeats)
def __init__(self, facetpath: str, slab: str, ts_estimate_dir: str, yamlfile: str, repeats: Tuple[int, int, int], creation_dir: PosixPath) -> None: ''' Initializing Parameters ___________ facetpath : str a path to the workflow's main dir e.g. 'Cu_111' slab : str a '.xyz' file name with the optimized slab e.g. 'Cu_111_slab_opt.xyz' ts_estimate_dir : str a path to directory with TSs e.g. 'TS_estimate' yamlfile : str a name of the .yaml file with a reactions list repeats : tuple(int, int, int) how to replicate unit cell in (x, y, z) direction, e.g. (3, 3, 1) creation_dir : posix a posix path to the main working directory ''' self.facetpath = facetpath self.slab = slab self.ts_estimate_dir = ts_estimate_dir self.yamlfile = yamlfile self.repeats = repeats self.creation_dir = creation_dir self.n_kpts = IO().get_kpoints(self.repeats) self.path_to_slab = os.path.join(self.creation_dir, self.slab) big_slab = read(self.path_to_slab) * self.repeats self.nslab = len(big_slab)
def get_ts_guess_and_bonded_idx( self, reacting_idxs: List[int]) -> Tuple[Gratoms, int]: ''' Get ts_guess (Gratom) and index of atom that connects ts_guess to the surface. Currently, only monodentate adsobrtion is supported Parameters ---------- reacting_idxs : List[int] list with indicies of reacting atoms, in order as they appear in .yaml file but not necessary with the same indicies Returns ------- ts_guess_image, s_bonded_idx : Tuple[Gratoms, int] ts_geuss_el is Gratoms representation of the TS guess, whereas s_bonded_idx is the index of adsorbate atom that bonds to surface ''' # get TS guess image ts_guess_image = IO.get_TS_guess_image(self.rxn, self.easier_to_build) # get index of surface bonded atom s_bonded_idx = self.get_s_bonded_idx() # get reacting atoms indices, as they appear in the .yaml file # surface atoms and multiplicity line are ignored tag_react_atom_idx_1, tag_react_atom_idx_2 = reacting_idxs # convert tag indices to indices as they appear in the Gratoms object react_atom_idx_1 = [ atom.index for atom in ts_guess_image if atom.tag == tag_react_atom_idx_1 ][0] react_atom_idx_2 = [ atom.index for atom in ts_guess_image if atom.tag == tag_react_atom_idx_2 ][0] # get atomic indices of all atoms connected to atom2 connected_atoms = self.convert_tag_to_correct_idx( ts_guess_image, tag_react_atom_idx_1, tag_react_atom_idx_2) # add atom2 idx to the list of connected_atoms if react_atom_idx_2 not in connected_atoms: connected_atoms.append(react_atom_idx_2) # get lenght of the bond between reacting atoms bondlen = ts_guess_image.get_distance(react_atom_idx_1, react_atom_idx_2) n_total_ads_atoms = len(ts_guess_image) # create a better TS_guess for a couple of common edge cases if n_total_ads_atoms == 2: # proper diatomic ts_guess_image.rotate(90, 'y') elif n_total_ads_atoms == 3: # proper triatomic remaining_atom_idx = n_total_ads_atoms - \ (react_atom_idx_1 + react_atom_idx_2) if react_atom_idx_2 != 2: # Symetrically not important, but structure looks # better visually ts_guess_image.rotate(-90, 'z') else: ts_guess_image.rotate(90, 'z') # set angle that puts atom_2 closer to the surface ts_guess_image.set_angle(remaining_atom_idx, react_atom_idx_1, react_atom_idx_2, 30, indices=[ remaining_atom_idx, react_atom_idx_1, react_atom_idx_2 ], add=True) else: # continue with defaults # TODO for now it should work bu should be improved later # pass ts_guess_image.rotate(90, 'z') # scale the bond distance between reacting atoms ts_guess_image.set_distance(react_atom_idx_1, react_atom_idx_2, bondlen * self.scfactor, fix=0, indices=connected_atoms) return ts_guess_image, s_bonded_idx
scaled2 = {scaled2} creation_dir = '{creation_dir}' rxn = {rxn} rxn_name = '{rxn_name}' balsam_exe_settings = {balsam_exe_settings} minima_dir = os.path.join(creation_dir, facetpath, 'minima') ts_dir = 'TS_estimate' path_to_ts_estimate = os.path.join(creation_dir, facetpath, rxn_name, ts_dir) ts = TS(facetpath, slab, ts_dir, yamlfile, repeats, creation_dir) ts.prepare_ts_estimate(rxn, scfactor, scfactor_surface, pytemplate_xtb, species_list, reacting_atoms, metal_atom, scaled1, scaled2) dependancy_dict = IO().depends_on(facetpath, yamlfile, creation_dir) jobs_to_be_finished = dependancy_dict[rxn_name] dependency_workflow_name = facetpath + '_01_' + rxn_name workflow_name = facetpath + '_02_' + rxn_name dependent_workflow_name = facetpath + '_03_' + rxn_name pending_simulations_dep = BalsamJob.objects.filter( workflow__contains=dependent_workflow_name).exclude(state="JOB_FINISHED") # for a given rxn_name, get all BalsamJob objects that it depends on pending_simulations = [] for dep_job in jobs_to_be_finished: pending_simulations.append( BalsamJob.objects.filter(name=dep_job).exclude(state="JOB_FINISHED"))
def create_minima_vib_all( self, socket_calculator: str, facetpath: str, yamlfile_path: PosixPath, pytemplate: str, balsam_exe_settings: Dict[str, int], pseudo_dir: str, pseudopotentials: Dict[str, str], calc_keywords: Dict[str, str], creation_dir: PosixPath) -> None: ''' Create all files for frequency calculations for the most stable conformer for a given species. Parameters ---------- facetpath : str a path to the workflow's main dir e.g. ``'Cu_111'`` yamlfile_path : posix a path to .yaml file with all reactions e.g. 'reactions.yaml' pytemplate : str a pytemplate for frequency calculations of minima balsam_exe_settings : Dict[str, int] a dictionary with balsam execute parameters (cores, nodes, etc.), e.g. >>> balsam_exe_settings = {'num_nodes': 1, 'ranks_per_node': 48, 'threads_per_rank': 1} calc_keywords : Dict[str, str] a dictionary with parameters to run DFT package. Quantum Espresso is used as default, e.g. >>> calc_keywords = {'kpts': (3, 3, 1), 'occupations': 'smearing', 'smearing': 'marzari-vanderbilt', 'degauss': 0.01, 'ecutwfc': 40, 'nosym': True, 'conv_thr': 1e-11, 'mixing_mode': 'local-TF'} pseudo_dir : str a path to the QE's pseudopotentials main directory e.g. ``'/home/mgierad/espresso/pseudo'`` pseudopotentials : Dict[str, str] a dictionary with QE pseudopotentials for all species. e.g. >>> dict(Cu='Cu.pbe-spn-kjpaw_psl.1.0.0.UPF', H='H.pbe-kjpaw_psl.1.0.0.UPF', O='O.pbe-n-kjpaw_psl.1.0.0.UPF', C='C.pbe-n-kjpaw_psl.1.0.0.UPF') calc_keywords : Dict[str, str] a dictionary with parameters to run DFT package. Quantum Espresso is used as default, e.g. >>> calc_keywords = {'kpts': (3, 3, 1), 'occupations': 'smearing', 'smearing': 'marzari-vanderbilt', 'degauss': 0.01, 'ecutwfc': 40, 'nosym': True, 'conv_thr': 1e-11, 'mixing_mode':'local-TF'} creation_dir : PosixPath a posix path to the working directory ''' minima_vib_path = os.path.join( self.creation_dir, self.facetpath, 'minima_vib') os.makedirs(minima_vib_path, exist_ok=True) unique_adsorbates_prefixes = IO().get_unique_adsorbates_prefixes( facetpath, yamlfile_path, creation_dir) for adsorbate, unique_prefixes in unique_adsorbates_prefixes.items(): for prefix in unique_prefixes: path_to_minimum_traj = os.path.join( self.minima_path, adsorbate, prefix + '.traj') path_to_vib_species = os.path.join( minima_vib_path, adsorbate, prefix) os.makedirs(path_to_vib_species, exist_ok=True) traj_to_start_vib = os.path.join( path_to_vib_species, '{}_{}.traj'.format(prefix, adsorbate)) shutil.copy2(path_to_minimum_traj, traj_to_start_vib) self.create_minima_vib_py_files( socket_calculator, adsorbate, prefix, traj_to_start_vib, minima_vib_path, pytemplate, balsam_exe_settings, pseudo_dir, pseudopotentials, calc_keywords, creation_dir)
repeats = {repeats} yamlfile = '{yamlfile}' pytemplate = '{pytemplate}' pseudopotentials = {pseudopotentials} pseudo_dir = '{pseudo_dir}' balsam_exe_settings = {balsam_exe_settings} calc_keywords = {calc_keywords} creation_dir = '{creation_dir}' put_adsorbates = Adsorbates(facetpath, slab, repeats, yamlfile, creation_dir) put_adsorbates.adjacency_to_3d() put_adsorbates.create_relax_jobs(socket_calculator, pytemplate, pseudopotentials, pseudo_dir, balsam_exe_settings, calc_keywords) dependancy_dict = IO().depends_on(facetpath, yamlfile, creation_dir) # keep track of all submitted jobs (all unique) all_submitted_jobs = [] # specify dependant 02 for given reactions for rxn_name in dependancy_dict.keys(): workflow_name = facetpath + '_01_' + rxn_name # ts estimate jobs dependent_workflow_name_1 = facetpath + '_02_' + rxn_name # get all dependant workflow BalsamJobs objects (should be one) pending_simulations_dep_1 = BalsamJob.objects.filter( workflow__contains=dependent_workflow_name_1).exclude( state="JOB_FINISHED") # for each reaction keep track of its dependencies
def set_up_ts_vib( self, socket_calculator: str, rxn: Dict[str, str], pytemplate: str, balsam_exe_settings: Dict[str, int], calc_keywords: Dict[str, str], pseudopotentials: Dict[str, str], pseudo_dir: str) -> None: ''' Set up files for TSs vibration calculations Parameters ---------- rxn : Dict[str, str] a dictionary with info about the paricular reaction. This can be view as a splitted many reaction .yaml file to a single reaction :literal:`*.yaml` file pytemplate : python script a template for TS_vib calculations balsam_exe_settings : Dict[str, int], a dictionary with balsam execute parameters (cores, nodes, etc.), e.g. >>> balsam_exe_settings = {'num_nodes': 1, 'ranks_per_node': 48, 'threads_per_rank': 1} calc_keywords : Dict[str, str] a dictionary with parameters to run DFT package. Quantum Espresso is used as default, e.g. >>> calc_keywords = {'kpts': (3, 3, 1), 'occupations': 'smearing', 'smearing': 'marzari-vanderbilt', 'degauss': 0.01, 'ecutwfc': 40, 'nosym': True, 'conv_thr': 1e-11, 'mixing_mode': 'local-TF'} pseudopotentials : Dict[str, str] a dictionary with QE pseudopotentials for all species. e.g. >>> dict(Cu='Cu.pbe-spn-kjpaw_psl.1.0.0.UPF', H='H.pbe-kjpaw_psl.1.0.0.UPF', O='O.pbe-n-kjpaw_psl.1.0.0.UPF', C='C.pbe-n-kjpaw_psl.1.0.0.UPF') pseudo_dir : str a path to the QE's pseudopotentials main directory e.g. ``'/home/mgierad/espresso/pseudo'`` ''' rxn_name = IO().get_rxn_name(rxn) ts_estimate_unique_dir = os.path.join( self.creation_dir, self.facetpath, rxn_name, 'TS_estimate_unique') ts_vib_dir = os.path.join( self.creation_dir, self.facetpath, rxn_name, 'TS_estimate_unique_vib') ts_final_geoms = Path(ts_estimate_unique_dir).glob('**/*final.xyz') for ts_final_geom in ts_final_geoms: ts_final_geom = str(ts_final_geom) prefix = ts_final_geom.split('/')[-2] ts_vib_dir_prefix = os.path.join(ts_vib_dir, prefix) os.makedirs(ts_vib_dir_prefix, exist_ok=True) # copy *ts_final.xyz files to ts_vib_dir_prefix - for debug shutil.copy2(ts_final_geom, ts_vib_dir_prefix) _, geom = os.path.split(ts_final_geom) py_fname = ts_vib_dir_prefix + '_' + \ self.facetpath + '_' + rxn_name + '_ts_vib.py' self.create_ts_vib_py_files( socket_calculator, pytemplate, geom, py_fname, balsam_exe_settings, calc_keywords, pseudopotentials, pseudo_dir)
def prepare_opt_after_ts( self, socket_calculator: str, rxn: Dict[str, str], pytemplate: str, balsam_exe_settings: Dict[str, int], calc_keywords: Dict[str, str], pseudopotentials: Dict[str, str], pseudo_dir: str) -> None: ''' Create files for after_TS calculations - to verify TS structures and get corresponding reactant and product minima Parameters ---------- rxn : Dict[str, str] a dictionary with info about the paricular reaction. This can be view as a splitted many reaction .yaml file to a single reaction :literal:`*.yaml` file pytemplate : python script a template for after_TS calculations balsam_exe_settings : Dict[str, int] a dictionary with balsam execute parameters (cores, nodes, etc.), e.g. >>> balsam_exe_settings = {'num_nodes': 1, 'ranks_per_node': 48, 'threads_per_rank': 1} calc_keywords : Dict[str, str] a dictionary with parameters to run DFT package. Quantum Espresso is used as default, e.g. >>> calc_keywords = {'kpts': (3, 3, 1), 'occupations': 'smearing', 'smearing': 'marzari-vanderbilt', 'degauss': 0.01, 'ecutwfc': 40, 'nosym': True, 'conv_thr': 1e-11, 'mixing_mode': 'local-TF'} pseudopotentials : Dict[str, str] a dictionary with QE pseudopotentials for all species. e.g. >>> dict(Cu='Cu.pbe-spn-kjpaw_psl.1.0.0.UPF', H='H.pbe-kjpaw_psl.1.0.0.UPF', O='O.pbe-n-kjpaw_psl.1.0.0.UPF', C='C.pbe-n-kjpaw_psl.1.0.0.UPF') pseudo_dir : str a path to the QE's pseudopotentials main directory e.g. ``'/home/mgierad/espresso/pseudo'`` ''' rxn_name = IO().get_rxn_name(rxn) ts_vib_dir = os.path.join(self.creation_dir, self.facetpath, rxn_name, 'TS_estimate_unique_vib') vib_traj_files = Path(ts_vib_dir).glob('**/*traj') for vib_traj in vib_traj_files: vib_traj = str(vib_traj) prefix = vib_traj.split('/')[-2] after_ts_dir = os.path.join(self.creation_dir, self.facetpath, rxn_name, 'after_TS', prefix) os.makedirs(after_ts_dir, exist_ok=True) fname = os.path.join( prefix + '_' + self.facetpath + '_' + rxn_name + '_after_ts') fname_forward = os.path.join(after_ts_dir, fname + '_f') fname_reverse = os.path.join(after_ts_dir, fname + '_r') AfterTS.get_forward_and_reverse( vib_traj, fname_forward, fname_reverse) self.create_after_ts_py_files( socket_calculator, pytemplate, fname_forward, fname_reverse, balsam_exe_settings, calc_keywords, pseudopotentials, pseudo_dir)
def create_relax_jobs(self, socket_calculator: str, pytemplate: str, pseudopotentials: Dict[str, str], pseudo_dir: str, balsam_exe_settings: Dict[str, int], calc_keywords: Dict[str, str], shtemplate: str = None) -> None: ''' Create a submit scripts Parameters __________ pytemplate: str a template to prepare submission scripts for adsorbate+surface minimization pseudopotentials: Dict[str, str] a dictionary with QE pseudopotentials for all species. e.g. >>> dict(Cu='Cu.pbe-spn-kjpaw_psl.1.0.0.UPF', H='H.pbe-kjpaw_psl.1.0.0.UPF', O='O.pbe-n-kjpaw_psl.1.0.0.UPF', C='C.pbe-n-kjpaw_psl.1.0.0.UPF') pseudo_dir: str a path to the QE's pseudopotentials main directory e.g. ``'/home/mgierad/espresso/pseudo'`` balsam_exe_settings: Dict[str, int] a dictionary with balsam execute parameters(cores, nodes, etc.), e.g. >>> balsam_exe_settings={'num_nodes': 1, 'ranks_per_node': 48, 'threads_per_rank': 1} calc_keywords: Dict[str, str] a dictionary with parameters to run DFT package. Quantum Espresso is used as default, e.g. >>> calc_keywords={'kpts': (3, 3, 1), 'occupations': 'smearing', 'smearing': 'marzari-vanderbilt', 'degauss': 0.01, 'ecutwfc': 40, 'nosym': True, 'conv_thr': 1e-11, 'mixing_mode': 'local-TF'} shtemplate: str optional, a path to :literal:`*.sh` template(not required by the workflow but possible to specified for special cases) ''' n_kpts = IO().get_kpoints(self.repeats) minimapath = os.path.join(self.creation_dir, self.facetpath, 'minima') with open(pytemplate, 'r') as f: pytemplate = f.read() if shtemplate is not None: with open(shtemplate, 'r') as f: shtemplate = f.read() for species in os.listdir(minimapath): speciespath = os.path.join(minimapath, species) if not os.path.isdir(speciespath): continue for structure in os.listdir(speciespath): if structure.endswith('xyz'): prefix = os.path.splitext(structure)[0] fname = os.path.join( minimapath, self.facetpath + '_' + species + '_' + prefix + '_relax.py') with open(fname, 'w') as f: f.write( pytemplate.format( socket_calculator=socket_calculator, adsorbate=species, prefix=prefix, pseudopotentials=pseudopotentials, pseudo_dir=pseudo_dir, balsam_exe_settings=balsam_exe_settings, calc_keywords=calc_keywords, creation_dir=self.creation_dir, repeats=self.repeats, n_kpts=n_kpts)) if shtemplate is None: continue # optional fname = os.path.join(minimapath, f'{species}_{prefix}_relax.sh') with open(fname, 'w') as f: f.write( shtemplate.format(adsorbate=species, prefix=prefix))
def get_av_dist(path_to_minima: str, species: str, metal_atom: str, scfactor_surface: float, scaled: bool = False) -> float: ''' Get the average bond distance between a given adsorbate atom and the nearest surface atom for all symmetrically distinct minima Parameters ___________ path_to_minima : str a path to minima e.g. ``'Cu_111/minima'`` species : str a species symbol e.g. ``'H'`` or ``'CO'`` metal_atom : str a checmical symbol for the surface atoms (only metallic surfaces are allowed) scfactor_surface : float a scaling factor to scale the target bond distance, i.e. the average distance between adsorbed atom and the nearest surface atom. Helpful e.g. when H is far away form the surface in TS, whereas for minima it is close to the surface e.g. 1.0 scaled : bool specify whether to use the optional scfactor_surface for the given species default = False Returns ________ av_dist : float an average bond distance between the given species and the nearest surface atom for all symmetrically dictinct minima ''' path_to_species = os.path.join(path_to_minima, species) # get unique minima prefixes unique_minima_prefixes = IO.get_unique_prefixes(path_to_species) # choose a representative temp .traj file that will be used to create # surface_atom_idxs and adsorbate_atom_idxs will be created path_to_tmp_traj = os.path.join(path_to_species, '00.traj') tmp_traj = read(path_to_tmp_traj) # create a list with all surface atom indicies surface_atom_idxs = [ atom.index for atom in tmp_traj if atom.symbol == metal_atom ] # create a list with all adosorbate atom indicies adsorbate_atom_idxs = { atom.symbol + '_' + str(atom.index): atom.index for atom in tmp_traj if atom.symbol != metal_atom } # loop through all unique traj files, e.g. 00.traj, 01.traj ... all_dists_bonded = [] for index in unique_minima_prefixes: path_to_unique_minima_traj = os.path.join(path_to_species, '{}.traj'.format(index)) uq_species_atom = read(path_to_unique_minima_traj) ads_atom_surf_dist = {} for key, ads_atom_idx in adsorbate_atom_idxs.items(): # create a dict storing the shortest distance between given # adsorbate index atom and surface atoms ads_atom_surf_dist[key] = min( uq_species_atom.get_distances(ads_atom_idx, surface_atom_idxs)) # the shortest distance in bonded_ads_atom_surf_dist.values() # is considered as a distance between atom bonded to the surface # and the surface bonded_ads_atom_surf_dist = min(ads_atom_surf_dist.values()) all_dists_bonded.append(bonded_ads_atom_surf_dist) # apply the scalling factor if scaled: av_dist = mean(all_dists_bonded) * scfactor_surface else: av_dist = mean(all_dists_bonded) return av_dist