def has_correct_imag_mode(self, calc=None, method=None): """Check that the imaginary mode is 'correct' set the calculation (hessian or optts)""" self.calc = calc if calc is not None else self.calc # By default the high level method is used to check imaginary modes if method is None: method = get_hmethod() # Run a fast check on whether it's likely the mode is correct if not self.could_have_correct_imag_mode(method=method): return False if imag_mode_has_correct_displacement(self.calc, self.bond_rearrangement): logger.info('Displacement of the active atoms in the imaginary ' 'mode bond forms and breaks the correct bonds') return True # Perform displacements over the imaginary mode to ensure the mode # connects reactants and products if imag_mode_links_reactant_products(self.calc, self.reactant, self.product, method=method): logger.info('Imaginary mode does link reactants and products') return True logger.warning('Species does *not* have the correct imaginary mode') return False
def test_method_unavalible(): Config.hcode = None Config.ORCA.path = '/an/incorrect/path' Config.NWChem.path = '/an/incorrect/path' Config.G09.path = '/an/incorrect/path' with pytest.raises(MethodUnavailable): methods.get_hmethod() # Specifying a method that with an executable that doesn't exist should raise an error Config.hcode = 'ORCA' with pytest.raises(MethodUnavailable): methods.get_hmethod()
def find_lowest_energy_ts_conformer(self): """Find the lowest energy transition state conformer by performing constrained optimisations""" atoms, energy = deepcopy(self.atoms), deepcopy(self.energy) calc = deepcopy(self.optts_calc) hmethod = get_hmethod() if Config.hmethod_conformers else None self.find_lowest_energy_conformer(hmethod=hmethod) if len(self.conformers) == 1: logger.warning('Only found a single conformer. ' 'Not rerunning TS optimisation') self.set_atoms(atoms=atoms) self.energy = energy self.optts_calc = calc return None self.optimise(name_ext='optts_conf') if self.is_true_ts() and self.energy < energy: logger.info('Conformer search successful') else: logger.warning(f'Transition state conformer search failed ' f'(∆E = {energy - self.energy:.4f} Ha). Reverting') self.set_atoms(atoms=atoms) self.energy = energy self.optts_calc = calc self.imaginary_frequencies = calc.get_imaginary_freqs() return None
def could_have_correct_imag_mode(self, method=None, threshold=-45): """ Determine if a point on the PES could have the correct imaginary mode. This must have (0) An imaginary frequency (quoted as negative in most EST codes) (1) The most negative(/imaginary) is more negative that a threshold Keywords Arguments: method (autode.wrappers.base.ElectronicStructureMethod): threshold (float): Returns: (bool): """ self._check_reactants_products() # By default the high level method is used to check imaginary modes if method is None: method = get_hmethod() if self.calc is None: logger.info('Calculating the hessian..') self.calc = Calculation(name=self.name + '_hess', molecule=self, method=method, keywords=method.keywords.hess, n_cores=Config.n_cores) self.calc.run() imag_freqs = self.calc.get_imaginary_freqs() if len(imag_freqs) == 0: logger.warning('Hessian had no imaginary modes') return False if len(imag_freqs) > 1: logger.warning(f'Hessian had {len(imag_freqs)} imaginary modes') if imag_freqs[0] > threshold: logger.warning('Imaginary modes were too small to be significant') return False try: _ = self.calc.get_normal_mode_displacements(mode_number=6) except NoNormalModesFound: logger.warning('No normal modes could be found cannot determine if' 'this the correct imaginary mode is found') return None # Check very conservatively for the correct displacement if not imag_mode_has_correct_displacement(self.calc, self.bond_rearrangement, delta_threshold=0.05, req_all=False): logger.warning('Species does not have the correct imaginary mode') return False logger.info('Species could have the correct imaginary mode') return True
def optimise(self, name_ext='optts'): """Optimise this TS to a true TS """ logger.info(f'Optimising {self.name} to a transition state') self._run_opt_ts_calc(method=get_hmethod(), name_ext=name_ext) # A transition state is a first order saddle point i.e. has a single # imaginary frequency if len(self.imaginary_frequencies) == 1: logger.info('Found a TS with a single imaginary frequency') return if len(self.imaginary_frequencies) == 0: logger.error('Transition state optimisation did not return any ' 'imaginary frequencies') return if all([freq > -50 for freq in self.imaginary_frequencies[1:]]): logger.warning('Had small imaginary modes - not displacing along') return # There is more than one imaginary frequency. Will assume that the most # negative is the correct mode.. for disp_magnitude in [1, -1]: logger.info('Displacing along second imaginary mode to try and ' 'remove') dis_name_ext = name_ext + '_dis' if disp_magnitude == 1 else name_ext + '_dis2' atoms, energy, calc = deepcopy(self.atoms), deepcopy( self.energy), deepcopy(self.optts_calc) self.atoms = get_displaced_atoms_along_mode( self.optts_calc, mode_number=7, disp_magnitude=disp_magnitude) self._run_opt_ts_calc(method=get_hmethod(), name_ext=dis_name_ext) if len(self.imaginary_frequencies) == 1: logger.info('Displacement along second imaginary mode ' 'successful. Now have 1 imaginary mode') break self.optts_calc = calc self.atoms = atoms self.energy = energy self.imaginary_frequencies = self.optts_calc.get_imaginary_freqs() return None
def optimise_reacs_prods(self): """Perform a geometry optimisation on all the reactants and products using the method""" h_method = get_hmethod() logger.info(f'Optimising reactants and products with {h_method.name}') for mol in self.reacs + self.prods: mol.optimise(h_method) return None
def calculate_single_points(self): """Perform a single point energy evaluations on all the reactants and products using the hmethod""" h_method = get_hmethod() logger.info(f'Calculating single points with {h_method.name}') for mol in self._reasonable_components_with_energy(): mol.single_point(h_method) return None
def calculate_complexes(self): """Find the lowest energy conformers of reactant and product complexes using optimisation and single points""" h_method = get_hmethod() conf_hmethod = h_method if Config.hmethod_conformers else None for species in [self.reactant, self.product]: species.find_lowest_energy_conformer(hmethod=conf_hmethod) species.optimise(method=h_method) return None
def calculate_single_points(self): """Perform a single point energy evaluations on all the reactants and products using the hmethod""" h_method = get_hmethod() logger.info(f'Calculating single points with {h_method.name}') for mol in self.reacs + self.prods + [self.ts]: if mol is not None: mol.single_point(h_method) return None
def _run_hess_calculation(self, method, temp): """Run a Hessian calculation on this species""" method = method if method is not None else get_hmethod() calc = Calculation(name=f'{self.name}_hess', molecule=self, method=method, keywords=method.keywords.hess, n_cores=Config.n_cores, temp=temp) calc.run() return calc
def find_lowest_energy_conformers(self): """Try and locate the lowest energy conformation using simulated annealing, then optimise them with xtb, then optimise the unique (defined by an energy cut-off) conformers with an electronic structure method""" h_method = get_hmethod() if Config.hmethod_conformers else None for mol in self.reacs + self.prods: # .find_lowest_energy_conformer works in conformers/ mol.find_lowest_energy_conformer(hmethod=h_method) return None
def test_free_energy_profile(): # Use a spoofed Gaussian09 and XTB install Config.lcode = 'xtb' Config.hcode = 'g09' Config.G09.path = here Config.ts_template_folder_path = os.getcwd() method = get_hmethod() assert method.name == 'g09' rxn = reaction.Reaction(Reactant(name='F-', smiles='[F-]'), Reactant(name='CH3Cl', smiles='ClC'), Product(name='Cl-', smiles='[Cl-]'), Product(name='CH3F', smiles='CF'), name='sn2', solvent_name='water') # Don't run the calculation without a working XTB install if shutil.which('xtb') is None or not shutil.which('xtb').endswith('xtb'): return rxn.calculate_reaction_profile(free_energy=True) # Allow ~0.5 kcal mol-1 either side of the true value dg_ts = rxn.calc_delta_g_ddagger() assert 17 < dg_ts * Constants.ha2kcalmol < 18 dg_r = rxn.calc_delta_g() assert -13.2 < dg_r * Constants.ha2kcalmol < -12.3 dh_ts = rxn.calc_delta_h_ddagger() assert 9.2 < dh_ts * Constants.ha2kcalmol < 10.3 dh_r = rxn.calc_delta_h() assert -13.6 < dh_r * Constants.ha2kcalmol < -12.6 # Should be able to plot an enthalpy profile plot_reaction_profile([rxn], units=KcalMol, name='enthalpy', enthalpy=True) assert os.path.exists('enthalpy_reaction_profile.png') os.remove('enthalpy_reaction_profile.png') # Reset the configuration to the default values Config.hcode = None Config.G09.path = None Config.lcode = None Config.XTB.path = None
def calculate_reaction_profile(self, units=KcalMol): """Calculate a multistep reaction profile using the products of step 1 as the reactants of step 2 etc.""" logger.info('Calculating reaction profile') h_method = get_hmethod() @work_in(self.name) def calculate(reaction, prev_products): for mol in reaction.reacs + reaction.prods: # If this molecule is one of the previous products don't # regenerate conformers skip_conformers = False for prod in prev_products: # Has the same atoms & charge etc. as in the unique string if str(mol) == str(prod): mol = prod skip_conformers = True if not skip_conformers: mol.find_lowest_energy_conformer( hmethod=h_method if Config.hmethod_conformers else None ) reaction.optimise_reacs_prods() reaction.find_complexes() reaction.locate_transition_state() reaction.find_lowest_energy_ts_conformer() reaction.calculate_single_points() return None # For all the reactions calculate the reactants, products and TS for i, r in enumerate(self.reactions): r.name = f'{self.name}_step{i}' if i == 0: # First reaction has no previous products calculate(reaction=r, prev_products=[]) else: calculate(reaction=r, prev_products=self.reactions[i - 1].prods) plot_reaction_profile(self.reactions, units=units, name=self.name) return None
def find_lowest_energy_ts_conformer(self, rmsd_threshold=None): """Find the lowest energy transition state conformer by performing constrained optimisations""" logger.info('Finding lowest energy TS conformer') atoms, energy = deepcopy(self.atoms), deepcopy(self.energy) calc = deepcopy(self.optts_calc) hmethod = get_hmethod() if Config.hmethod_conformers else None self.find_lowest_energy_conformer(hmethod=hmethod) # Remove similar TS conformer that are similar to this TS based on root # mean squared differences in their structures thresh = Config.rmsd_threshold if rmsd_threshold is None else rmsd_threshold self.conformers = [ conf for conf in self.conformers if calc_heavy_atom_rmsd(conf.atoms, atoms) > thresh ] logger.info(f'Generated {len(self.conformers)} unique (RMSD > ' f'{thresh} Å) TS conformer(s)') # Optimise the lowest energy conformer to a transition state - will # .find_lowest_energy_conformer will have updated self.atoms etc. if len(self.conformers) > 0: self.optimise(name_ext='optts_conf') if self.is_true_ts() and self.energy < energy: logger.info('Conformer search successful') return None # Ensure the energy has a numerical value, so a difference can be # evaluated self.energy = self.energy if self.energy is not None else 0 logger.warning(f'Transition state conformer search failed ' f'(∆E = {energy - self.energy:.4f} Ha). Reverting') logger.info('Reverting to previously found TS') self.atoms = atoms self.energy = energy self.optts_calc = calc self.imaginary_frequencies = calc.get_imaginary_freqs() return None
def get_ts_guess_function_and_params(reaction, bond_rearr): """Get the functions (1dscan or 2dscan) and parameters required for the function for a TS scan Arguments: reaction (autode.reaction.Reaction): bond_rearr (autode.bond_rearrangement.BondRearrangement): Returns: (list): updated funcs and params list """ name = str(reaction) scan_name = name r, p = reaction.reactant, reaction.product lmethod, hmethod = get_lmethod(), get_hmethod() # Bonds with initial and final distances bbonds = [BreakingBond(pair, r) for pair in bond_rearr.bbonds] scan_name += "_".join(str(bb) for bb in bbonds) fbonds = [ FormingBond(pair, r, final_species=p) for pair in bond_rearr.fbonds ] scan_name += "_".join(str(fb) for fb in fbonds) # Ideally use a transition state template, then only a single constrained # optimisation needs to be run yield get_template_ts_guess, (r, p, bond_rearr, f'{name}_template_{bond_rearr}', hmethod) # otherwise try a nudged elastic band calculation, don't use the low level # method if there are any metals if not any(atom.label in metals for atom in r.atoms): yield get_ts_adaptive_path, (r, p, lmethod, fbonds, bbonds, f'{name}_ll_ad_{bond_rearr}') # Always attempt a high-level NEB yield get_ts_adaptive_path, (r, p, hmethod, fbonds, bbonds, f'{name}_hl_ad_{bond_rearr}') return None
def test_get_hmethod(): Config.hcode = None Config.ORCA.path = here # A path that exists method1 = methods.get_hmethod() assert method1.name == 'orca' methods.Config.hcode = 'orca' method2 = methods.get_hmethod() assert method2.name == 'orca' Config.hcode = 'g09' Config.G09.path = here method3 = methods.get_hmethod() assert method3.name == 'g09' Config.hcode = 'NwChem' Config.NWChem.path = here method4 = methods.get_hmethod() assert method4.name == 'nwchem' with pytest.raises(MethodUnavailable): Config.hcode = 'x' methods.get_hmethod()
def get_ts_guess_function_and_params(reaction, bond_rearr): """Get the functions (1dscan or 2dscan) and parameters required for the function for a TS scan Arguments: reaction (autode.reaction.Reaction): bond_rearr (autode.bond_rearrangement.BondRearrangement): Returns: (list): updated funcs and params list """ name = str(reaction) scan_name = name r, p = reaction.reactant, reaction.product lmethod, hmethod = get_lmethod(), get_hmethod() # Bonds with initial and final distances bbonds = [BreakingBond(pair, r, reaction) for pair in bond_rearr.bbonds] scan_name += "_".join(str(bb) for bb in bbonds) fbonds = [FormingBond(pair, r) for pair in bond_rearr.fbonds] scan_name += "_".join(str(fb) for fb in fbonds) # Ideally use a transition state template, then only a single constrained # optimisation needs to be run... yield get_template_ts_guess, (r, p, bond_rearr, f'{name}_template_{bond_rearr}', hmethod) # Otherwise try a nudged elastic band calculation, don't use the low level # method if there are any metals.. if not any(atom.label in metals for atom in r.atoms): yield get_ts_guess_neb, (r, p, lmethod, fbonds, bbonds, f'{name}_ll_neb_{bond_rearr}') # Always attempt a high-level NEB yield get_ts_guess_neb, (r, p, hmethod, fbonds, bbonds, f'{name}_hl_neb_{bond_rearr}') # Otherwise run 1D or 2D potential energy surface scans to generate a # transition state guess cheap -> most expensive if len(bbonds) == 1 and len(fbonds) == 1 and reaction.type in (Substitution, Elimination): yield get_ts_guess_2d, (r, p, fbonds[0], bbonds[0], f'{scan_name}_ll2d', lmethod, lmethod.keywords.low_opt) yield get_ts_guess_1d, (r, p, bbonds[0], f'{scan_name}_hl1d_bbond', hmethod, hmethod.keywords.low_opt) yield get_ts_guess_1d, (r, p, bbonds[0], f'{scan_name}_hl1d_alt_bbond', hmethod, hmethod.keywords.opt) if len(bbonds) > 0 and len(fbonds) == 1: yield get_ts_guess_1d, (r, p, fbonds[0], f'{scan_name}_hl1d_fbond', hmethod, hmethod.keywords.low_opt) yield get_ts_guess_1d, (r, p, fbonds[0], f'{scan_name}_hl1d_alt_fbond', hmethod, hmethod.keywords.opt) if len(bbonds) >= 1 and len(fbonds) >= 1: for fbond in fbonds: for bbond in bbonds: yield get_ts_guess_2d, (r, p, fbond, bbond, f'{scan_name}_ll2d', lmethod, lmethod.keywords.low_opt) yield get_ts_guess_2d, (r, p, fbond, bbond, f'{scan_name}_hl2d', hmethod, hmethod.keywords.low_opt) if len(bbonds) == 1 and len(fbonds) == 0: yield get_ts_guess_1d, (r, p, bbonds[0], f'{scan_name}_hl1d', hmethod, hmethod.keywords.low_opt) yield get_ts_guess_1d, (r, p, bbonds[0], f'{scan_name}_hl1d_alt', hmethod, hmethod.keywords.opt) if len(fbonds) == 2: yield get_ts_guess_2d, (r, p, fbonds[0], fbonds[1], f'{scan_name}_ll2d_fbonds', lmethod, lmethod.keywords.low_opt) yield get_ts_guess_2d, (r, p, fbonds[0], fbonds[1], f'{scan_name}_hl2d_fbonds', hmethod, hmethod.keywords.low_opt) if len(bbonds) == 2: yield get_ts_guess_2d, (r, p, bbonds[0], bbonds[1], f'{scan_name}_ll2d_bbonds', lmethod, lmethod.keywords.low_opt) yield get_ts_guess_2d, (r, p, bbonds[0], bbonds[1], f'{scan_name}_hl2d_bbonds', hmethod, hmethod.keywords.low_opt) return None