def test_get_lmethod(): Config.lcode = None Config.XTB.path = here method3 = methods.get_lmethod() assert method3.name == 'xtb' Config.lcode = 'xtb' method4 = methods.get_lmethod() assert method4.name == 'xtb' Config.lcode = 'mopac' Config.MOPAC.path = here method4 = methods.get_lmethod() assert method4.name == 'mopac'
def populate_conformers(self): """ Generate and optimise with a low level method a set of conformers, the number of which is Config.num_complex_sphere_points × Config.num_complex_random_rotations ^ (n molecules in complex - 1) """ n_confs = (Config.num_complex_sphere_points * Config.num_complex_random_rotations * (len(self.molecules) - 1)) logger.info(f'Generating and optimising {n_confs} conformers of ' f'{self.name} with a low-level method') self._generate_conformers() try: lmethod = get_lmethod() for conformer in self.conformers: conformer.optimise(method=lmethod) conformer.print_xyz_file() except MethodUnavailable: logger.error('Could not optimise complex conformers') return None
def get_ts_guess_constrained_opt(reactant, method, keywords, name, distance_consts, product): """Get a TS guess from a constrained optimisation with the active atoms fixed at values defined in distance_consts Arguments: reactant (autode.complex.ReactantComplex): method (autode.wrappers.base.ElectronicStructureMethod): keywords (autode.wrappers.keywords.Keywords): name (str): distance_consts (dict): Distance constraints keyed with a tuple of atom indexes and value of the distance product (autode.complex.ProductComplex): Returns: (autode.ts_guess.TSguess): """ logger.info('Getting TS guess from constrained optimisation') mol_with_constraints = reactant.copy() # Run a low level constrained optimisation first to prevent the DFT being # problematic if there are >1 constraint l_method = get_lmethod() ll_const_opt = Calculation(name=f'{name}_constrained_opt_ll', molecule=mol_with_constraints, method=l_method, keywords=l_method.keywords.low_opt, n_cores=Config.n_cores, distance_constraints=distance_consts) # Try and set the atoms, but continue if they're not found as hopefully the # other method will be fine(?) try: mol_with_constraints.optimise(method=l_method, calc=ll_const_opt) except AtomsNotFound: logger.error('Failed to optimise with the low level method') hl_const_opt = Calculation(name=f'{name}_constrained_opt', molecule=mol_with_constraints, method=method, keywords=keywords, n_cores=Config.n_cores, distance_constraints=distance_consts) # Form a transition state guess from the optimised atoms and set the # corresponding energy try: mol_with_constraints.optimise(method=method, calc=hl_const_opt) except AtomsNotFound: logger.error('Failed to optimise with the high level method') return get_ts_guess(species=mol_with_constraints, reactant=reactant, product=product, name=f'ts_guess_{name}')
def find_lowest_energy_conformer(self, lmethod=None, hmethod=None): """ For a molecule object find the lowest conformer in energy and set the molecule.atoms and molecule.energy Arguments: lmethod (autode.wrappers.ElectronicStructureMethod): hmethod (autode.wrappers.ElectronicStructureMethod): """ logger.info('Finding lowest energy conformer') if self.n_atoms <= 2: logger.warning('Cannot have conformers of a species with 2 atoms ' 'or fewer') return None if lmethod is None: logger.info('Getting the default low level method') lmethod = get_lmethod() methods.add('Low energy conformers located with the') self._generate_conformers() # For all generated conformers optimise with the low level of theory method_string = f'and optimised using {lmethod.name}' if hmethod is not None: method_string += f' then with {hmethod.name}' methods.add(f'{method_string}.') for conformer in self.conformers: conformer.optimise(lmethod) # Strip conformers that are similar based on an energy criteria or # don't have an energy self.conformers = get_unique_confs(conformers=self.conformers) if hmethod is not None: # Re-evaluate the energy of all the conformers with the higher # level of theory for conformer in self.conformers: if Config.hmethod_sp_conformers: assert hmethod.keywords.low_sp is not None conformer.single_point(hmethod) else: # Otherwise run a full optimisation conformer.optimise(hmethod) self._set_lowest_energy_conformer() logger.info(f'Lowest energy conformer found. E = {self.energy}') return None
def find_lowest_energy_conformer(self, lmethod=None, hmethod=None): """ For a molecule object find the lowest conformer in energy and set the molecule.atoms and molecule.energy Arguments: lmethod (autode.wrappers.ElectronicStructureMethod): hmethod (autode.wrappers.ElectronicStructureMethod): """ logger.info('Finding lowest energy conformer') if self.n_atoms <= 2: logger.warning('Cannot have conformers of a species with 2 atoms ' 'or fewer') return None if lmethod is None: logger.info('Getting the default low level method') lmethod = get_lmethod() try: self._generate_conformers() except NotImplementedError: logger.error('Could not generate conformers. generate_conformers()' ' not implemented') return None # For all generated conformers optimise with the low level of theory for conformer in self.conformers: conformer.optimise(lmethod) # Strip conformers that are similar based on an energy criteria or # don't have an energy self.conformers = get_unique_confs(conformers=self.conformers) if hmethod is not None: # Re-optimise all the conformers with the higher level of theory # to get more accurate energies for conformer in self.conformers: conformer.optimise(hmethod) self._set_lowest_energy_conformer() logger.info(f'Lowest energy conformer found. E = {self.energy}') 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 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
def get_interpolated(initial_species, fbonds, bbonds, max_n, method=None, stop_thresh=0.02): """ Generate the end point on the NEB by running a 1D scan, using by default a low-level method. Supprorts using different methods for the starting and final (end) points to the method used for the interpolation. If method is set then this will be used for both the end and intermediate methods Arguments: initial_species (autode.species.Species): fbonds (list(autode.pes.pes.FormingBond)): bbonds (list(autode.pes.pes.BreakingBond)): max_n (int): Maximum number of intermediate species to generate between the initial and final species Keyword Arguments: method (autode.wrappers.base.ElectronicStructureMethod): stop_thresh (float): Energy threshold in Ha to terminate the interpolation if ∆E between two adjacent points is > this and there is a peak in the surface, return the points. default is ~ 10 kcal mol-1 Returns: (list(autode.species.Species)): Set of intermediate species between """ assert fbonds is not None and bbonds is not None logger.info('Generating the interpolated species reactant -> product using' f' a maximum of {max_n} intermediate points') bonds = active_bonds_no_rings(initial_species, fbonds, bbonds) # Calculate the uniform change in each bond distance from initial -> final deltas = [(b.final_dist - b.curr_dist) / (max_n - 1) for b in bonds] # Set a dictionary of bond length constraints consts = {b.atom_indexes: b.curr_dist for b in bonds} species_set = [] # Generate a species with a constrained geometry for each point in the path for i in range(max_n): if i == 0: species = initial_species.copy() else: species = species_set[i - 1].copy() # Add the required change in every bond length to get to the final # distances at step n-1 for j, atom_indexes in enumerate(consts.keys()): consts[atom_indexes] += deltas[j] # Run the constrained optimisation if method is None: method = get_lmethod() opt = Calculation(name=f'{species.name}_constrained_opt{i}', molecule=species, method=method, keywords=method.keywords.opt, n_cores=Config.n_cores, distance_constraints=consts) # Set the optimised atoms - can raise AtomsNotFound species.optimise(method=method, calc=opt) species_set.append(species) # Early stopping if a ~saddle point has already been traversed, must be # in the second half of the scan and above an energy threshold for ∆E if all((i > 1, i > max_n // 2, contains_peak(species_set), species.energy - species_set[i - 1].energy > stop_thresh)): logger.warning(f'Path contained an energy peak and the point ' f'before this one had a lower energy - stopping the' f' interpolation on step {i}') return species_set logger.info('Generated initial NEB path') return species_set
def imag_mode_links_reactant_products(calc, reactant, product, method, disp_mag=1.0): """Displaces atoms along the imaginary mode forwards (f) and backwards (b) to see if products and reactants are made Arguments: calc (autode.calculation.Calculation): reactant (autode.complex.ReactantComplex): product (autode.complex.ProductComplex): method (autode.wrappers.base.ElectronicStructureMethod): Keyword Arguments: disp_mag (int): Distance to be displaced along the imag mode (default: ) Returns: (bool): if the imag mode is correct or not """ logger.info('Displacing along imag modes to check that the TS links ' 'reactants and products') # Generate and optimise conformers with the low level of theory reactant.populate_conformers() product.populate_conformers() # Get the species that is optimised by displacing forwards along the mode f_displaced_atoms = get_displaced_atoms_along_mode(calc, mode_number=6, disp_magnitude=disp_mag) f_displaced_mol = get_optimised_species(calc, method, direction='forwards', atoms=f_displaced_atoms) # Get the species that is optimised by displacing backwards along the mode b_displaced_atoms = get_displaced_atoms_along_mode( calc, mode_number=6, disp_magnitude=-disp_mag) b_displaced_mol = get_optimised_species(calc, method, direction='backwards', atoms=b_displaced_atoms) if f_b_isomorphic_to_r_p(f_displaced_mol, b_displaced_mol, reactant, product): return True # The high and low level methods may not have the same minima, so optimise # and recheck isomorphisms for mol in (f_displaced_mol, b_displaced_mol): mol.optimise(method=get_lmethod(), reset_graph=True) if f_b_isomorphic_to_r_p(f_displaced_mol, b_displaced_mol, reactant, product): return True logger.info(f'Forwards displaced edges {f_displaced_mol.graph.edges}') logger.info(f'Backwards displaced edges {b_displaced_mol.graph.edges}') return False