def get_optimised_species(calc, method, direction, atoms): """Get the species that is optimised from an initial set of atoms""" species = Molecule(name=f'{calc.name}_{direction}', atoms=atoms, charge=calc.molecule.charge, mult=calc.molecule.mult) # Note that for the surface to be the same the keywords.opt and keywords.hess need to match in the level of theory calc = Calculation(name=f'{calc.name}_{direction}', molecule=species, method=method, keywords=method.keywords.opt, n_cores=Config.n_cores) calc.run() try: species.set_atoms(atoms=calc.get_final_atoms()) species.energy = calc.get_energy() make_graph(species) except AtomsNotFound: logger.error(f'{direction} displacement calculation failed') return species
def get_truncated_ts(reaction, bond_rearr): """Get the TS of a truncated reactant and product complex""" # Truncate the reactant and product complex to the core atoms so the full # TS can be template-d f_reactant = reaction.reactant.copy() f_product = reaction.product.copy() # Set the truncated reactant and product for this reaction reaction.reactant = get_truncated_complex(f_reactant, bond_rearr) reaction.product = get_truncated_complex(f_product, bond_rearr) # Re-find the bond rearrangements, which should exist reaction.name += '_truncated' bond_rearrangs = get_bond_rearrangs(reaction.reactant, reaction.product, name=reaction.name) if bond_rearrangs is None: logger.error('Truncation generated a complex with 0 rearrangements') return None # Find all the possible TSs for bond_rearr in bond_rearrangs: get_ts(reaction, reaction.reactant, bond_rearr, is_truncated=True) # Reset the reactant, product and name of the full reaction reaction.reactant = f_reactant reaction.product = f_product reaction.name = reaction.name.rstrip('_truncated') logger.info('Done with truncation') return None
def get_normal_mode_displacements(self, calc, mode_number): # mode numbers start at 1, not 6 mode_number -= 5 normal_mode_section, displacements = False, [] for j, line in enumerate(calc.output.file_lines): if 'Projected Frequencies' in line: normal_mode_section = True displacements = [] if '------------------------------' in line: normal_mode_section = False if normal_mode_section: if len(line.split()) == 6: mode_numbers = [int(val) for val in line.split()] if mode_number in mode_numbers: col = [i for i in range( len(mode_numbers)) if mode_number == mode_numbers[i]][0] + 1 displacements = [float(disp_line.split()[ col]) for disp_line in calc.output.file_lines[j + 4:j + 3 * calc.molecule.n_atoms + 4]] displacements_xyz = [displacements[i:i + 3] for i in range(0, len(displacements), 3)] if len(displacements_xyz) != calc.molecule.n_atoms: logger.error( 'Something went wrong getting the displacements n != n_atoms') return None return np.array(displacements_xyz)
def _get_energy(self, e=False, h=False, g=False, force=False): """ Get the energy from a completed calculation Keyword Arguments: e (bool): Return the potential energy (E) h (bool): Return the enthalpy (H) at 298 K g (bool): Return the Gibbs free energy (G) at 298 K force (bool): Return the energy even if the calculation errored Returns: (float): Energy in Hartrees, or None """ logger.info(f'Getting energy from {self.output.filename}') if self.terminated_normally() or force: if h: return self.method.get_enthalpy(self) if g: return self.method.get_free_energy(self) if e: return self.method.get_energy(self) logger.error('Calculation did not terminate normally. Energy = None') return None
def optimise(self, method, reset_graph=False, calc=None): """ Optimise the geometry of this conformer Arguments: method (autode.wrappers.base.ElectronicStructureMethod): Keyword Arguments: reset_graph (bool): calc (autode.calculation.Calculation): """ logger.info(f'Running optimisation of {self.name}') if calc is not None or reset_graph: raise NotImplementedError opt = Calculation(name=f'{self.name}_opt', molecule=self, method=method, keywords=method.keywords.low_opt, n_cores=Config.n_cores, distance_constraints=self.dist_consts) opt.run() self.energy = opt.get_energy() try: self.set_atoms(atoms=opt.get_final_atoms()) except AtomsNotFound: logger.error(f'Atoms not found for {self.name} but not critical') self.set_atoms(atoms=None) return None
def get_ts_templates(folder_path=Config.ts_template_folder_path): """Get all the transition state templates from a folder, or the default if folder path is None Keyword Arguments: folder_path (str): /path/to/the/ts/template/library Returns: (list(autode.transition_states.templates.TStemplate)) """ if folder_path is None: logger.info( 'Folder path is not set – getting TS templates from the default path' ) folder_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'lib') logger.info(f'Getting TS templates from {folder_path}') if not os.path.exists(folder_path): logger.error('Folder does not exist') return [] obj_filenames = [ fn for fn in os.listdir(folder_path) if fn.endswith('.obj') ] objects = [ pickle.load(open(os.path.join(folder_path, filename), 'rb')) for filename in obj_filenames ] logger.info(f'Have {len(objects)} TS templates') return objects
def get_free_energy(self, calc): """Get the Gibbs free energy (G) from an ORCA calculation output""" if calc.molecule.n_atoms == 1: logger.warning('ORCA fails to calculate the entropy for a single ' 'atom, returning the correct G in 1 atm') h = self.get_enthalpy(calc) s = calc_atom_entropy(atom_label=calc.molecule.atoms[0].label, temp=calc.input.temp) # J K-1 mol-1 # Calculate H - TS, the latter term from Jmol-1 -> Ha return h - s * calc.input.temp for line in reversed(calc.output.file_lines): if ('Final Gibbs free energy' in line or 'Final Gibbs free enthalpy' in line): try: return float(line.split()[-2]) except ValueError: break logger.error('Could not get the free energy from the calculation. ' 'Was a frequency requested?') return None
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_bond_rearrangs_from_file(filename='bond_rearrangs.txt'): logger.info('Getting bond rearrangements from file') if not os.path.exists(filename): logger.error('No bond rearrangments file') return None bond_rearrangs = [] with open(filename, 'r') as br_file: fbonds_block = False bbonds_block = True fbonds = [] bbonds = [] for line in br_file: if 'fbonds' in line: fbonds_block = True bbonds_block = False if 'bbonds' in line: fbonds_block = False bbonds_block = True if fbonds_block and len(line.split()) == 2: atom_id_string = line.split() fbonds.append((int(atom_id_string[0]), int(atom_id_string[1]))) if bbonds_block and len(line.split()) == 2: atom_id_string = line.split() bbonds.append((int(atom_id_string[0]), int(atom_id_string[1]))) if 'end' in line: bond_rearrangs.append( BondRearrangement(forming_bonds=fbonds, breaking_bonds=bbonds)) fbonds = [] bbonds = [] return bond_rearrangs
def find_lowest_energy_ts_conformer(self): """Find the lowest energy conformer of the transition state""" if self.ts is None: logger.error('No transition state to evaluate the conformer of') return None else: return self.ts.find_lowest_energy_ts_conformer()
def get_point_species(point, species, distance_constraints, name, method, keywords, n_cores, energy_threshold=1): """ On a 2d PES calculate the energy and the structure using a constrained optimisation Arguments: point (tuple(int)): Index of this point e.g. (0, 0) for the first point on a 2D surface species (autode.species.Species): distance_constraints (dict): Keyed with atom indexes and the constraint value as the value name (str): method (autode.wrappers.base.ElectronicStructureMethod): keywords (autode.wrappers.keywords.Keywords): n_cores (int): Number of cores to used for this calculation Keyword Arguments: energy_threshold (float): Above this energy (Ha) the calculation will be disregarded Returns: (autode.species.Species): Species """ logger.info(f'Calculating point {point} on PES surface') species.name = f'{name}_scan_{"-".join([str(p) for p in point])}' original_species = deepcopy(species) # Set up and run the calculation const_opt = Calculation(name=species.name, molecule=species, method=method, n_cores=n_cores, keywords=keywords, distance_constraints=distance_constraints) try: species.optimise(method=method, calc=const_opt) except AtomsNotFound: logger.error(f'Optimisation failed for {point}') return original_species # If the energy difference is > 1 Hartree then likely something has gone # wrong with the EST method we need to be not on the first point to compute # an energy difference.. if not all(p == 0 for p in point): if species.energy is None or np.abs(original_species.energy - species.energy) > energy_threshold: logger.error(f'PES point had a relative energy ' f'> {energy_threshold} Ha. Using the closest') return original_species return species
def get_free_energy(self, calc): """Get the Gibbs free energy (G) from an g09 calculation output""" for line in reversed(calc.output.file_lines): if 'Sum of electronic and thermal Free Energies' in line: return float(line.split()[-1]) logger.error('Could not get the enthalpy from the calculation. ' 'A frequency must be requested') 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 _check_molecule(self): """Ensure the molecule has the required attributes""" assert hasattr(self.molecule, 'n_atoms') assert hasattr(self.molecule, 'atoms') assert hasattr(self.molecule, 'mult') assert hasattr(self.molecule, 'charge') assert hasattr(self.molecule, 'solvent') # The molecule must have > 0 atoms if self.molecule.atoms is None or self.molecule.n_atoms == 0: logger.error('Have no atoms. Can\'t form a calculation') raise ex.NoInputError
def _run_opt_ts_calc(self, method, name_ext): """Run an optts calculation and attempt to set the geometry, energy and normal modes""" if self.bond_rearrangement is None: logger.warning('Cannot add redundant internal coordinates for the ' 'active bonds with no bond rearrangement') bond_ids = None else: bond_ids = self.bond_rearrangement.all self.optts_calc = Calculation( name=f'{self.name}_{name_ext}', molecule=self, method=method, n_cores=Config.n_cores, keywords=method.keywords.opt_ts, bond_ids_to_add=bond_ids, other_input_block=method.keywords.optts_block) self.optts_calc.run() if not self.optts_calc.optimisation_converged(): if self.optts_calc.optimisation_nearly_converged(): logger.info('Optimisation nearly converged') self.calc = self.optts_calc if self.could_have_correct_imag_mode(): logger.info('Still have correct imaginary mode, trying ' 'more optimisation steps') self.atoms = self.optts_calc.get_final_atoms() self.optts_calc = Calculation( name=f'{self.name}_{name_ext}_reopt', molecule=self, method=method, n_cores=Config.n_cores, keywords=method.keywords.opt_ts, bond_ids_to_add=bond_ids, other_input_block=method.keywords.optts_block) self.optts_calc.run() else: logger.info('Lost imaginary mode') else: logger.info('Optimisation did not converge') try: self.imaginary_frequencies = self.optts_calc.get_imaginary_freqs() self.atoms = self.optts_calc.get_final_atoms() self.energy = self.optts_calc.get_energy() except (AtomsNotFound, NoNormalModesFound): logger.error('Transition state optimisation calculation failed') return
def is_isomorphic(graph1, graph2, ignore_active_bonds=False, timeout=5): """Check whether two NX graphs are isomorphic. Contains a timeout because the gm.is_isomorphic() method occasionally gets stuck Arguments: graph1 (nx.Graph): graph 1 graph2 (nx.Graph): graph 2 Keyword Arguments: ignore_active_bonds (bool): timeout (float): Timeout in seconds Returns: (bool): if the graphs are isomorphic """ if ignore_active_bonds: graph1, graph2 = get_graphs_ignoring_active_edges(graph1, graph2) if not isomorphism.faster_could_be_isomorphic(graph1, graph2): return False # Always match on atom types node_match = isomorphism.categorical_node_match('atom_label', 'C') if ignore_active_bonds: gm = isomorphism.GraphMatcher(graph1, graph2, node_match=node_match) else: # Also match on edges edge_match = isomorphism.categorical_edge_match('active', False) gm = isomorphism.GraphMatcher(graph1, graph2, node_match=node_match, edge_match=edge_match) # NX can hang here for not very large graphs, so kill after a timeout def handler(signum, frame): raise TimeoutError signal.signal(signal.SIGALRM, handler) signal.alarm(int(timeout)) try: result = gm.is_isomorphic() # Cancel the timer signal.alarm(0) return result except TimeoutError: logger.error('NX graph matching hanging') return False
def is_saddle(self, idx): """Is an index a saddle point""" if idx == 0 or idx == len(self) -1: logger.warning('Cannot be saddle point, index was at the end') return False if any(self[i].energy is None for i in (idx-1, idx, idx+1)): logger.error(f'Could not determine if point {idx} was a saddle ' f'point, an energy close by was None') return False energy = self[idx].energy return self[idx-1].energy < energy and self[idx+1].energy < energy
def calc_delta(attr, left, right): """Calculate the difference (∆) for a molecular attribute for some L → R""" if any(mol is None for mol in left + right): logger.error('Could not calculate ∆, a molecule was None') return None if any(getattr(mol, attr) is None for mol in left + right): logger.error('Cannot calculate ∆. At least one required attribute' ' was None') return None return (sum([getattr(mol, attr) for mol in right]) - sum([getattr(mol, attr) for mol in left]))
def increment(self): """Increment the counter, and switch on a climbing image""" super().increment() if self[0].iteration < self.wait_iteration: # No need to do anything else return if self.peak_idx is None: logger.error('Lost NEB peak - cannot switch on CI') return logger.info(f'Setting image {self.peak_idx} as the CI') self[self.peak_idx] = CImage(image=self[self.peak_idx]) return None
def get_free_energy(self, calc): """Get the Gibbs free energy (G) from an ORCA calculation output""" for line in reversed(calc.output.file_lines): if 'Final Gibbs free enthalpy' in line: try: return float(line.split()[-2]) except ValueError: break logger.error('Could not get the free energy from the calculation. ' 'Was a frequency requested?') return None
def get_vdw_radius(atom_label): """Get the van der waal's radius of an atom Arguments: atom_label (str): atom label e.g. C or Pd Returns: (float): van der waal's radius of the atom """ if atom_label in vdw_radii.keys(): return vdw_radii[atom_label] else: logger.error(f'Couldn\'t find the VdV radii for {atom_label}. ' f'Guessing at 2.3') return 2.3
def calc_h_cont(self, method=None, calc=None, temp=298.15): """Calculate the free energy contribution for a species""" assert self.energy is not None if calc is None: calc = self._run_hess_calculation(method=method, temp=temp) enthalpy = calc.get_enthalpy() if enthalpy is None: logger.error(f'Could not calculate H for {self.name}, not h_cont') return self.h_cont = enthalpy - self.energy return None
def calc_g_cont(self, method=None, calc=None, temp=298.15): """Calculate the free energy contribution for a species""" assert self.energy is not None if calc is None: calc = self._run_hess_calculation(method=method, temp=temp) free_energy = calc.get_free_energy() if free_energy is None: logger.error('Could not calculate g_cont, free energy not found') return self.g_cont = free_energy - self.energy return None
def get_unique_confs(conformers, energy_threshold_kj=1): """ For a list of conformers return those that are unique based on an energy threshold in kJ mol^-1 Arguments: conformers (list(autode.conformer.Conformer)): energy_threshold_kj (float): Energy threshold in kJ mol-1 Returns: (list(autode.conformers.conformers.Conformer)): List of conformers """ logger.info(f'Stripping conformers with energy ∆E < {energy_threshold_kj} ' f'kJ mol-1 to others') n_conformers = len(conformers) # Conformer.energy is in Hartrees threshold = energy_threshold_kj / Constants.ha2kJmol # The first conformer must be unique, if it has an energy unique_conformers = [] for conformer in conformers: if conformer.energy is None: logger.error('Conformer had no energy. Excluding') continue # Iterate through all the unique conformers already found and check # that the energy is not similar unique = True for other_conformer in unique_conformers: if np.abs(conformer.energy - other_conformer.energy) < threshold: unique = False break if unique: unique_conformers.append(conformer) n_unique_conformers = len(unique_conformers) logger.info(f'Stripped {n_conformers - n_unique_conformers} conformer(s) ' f'from a total of {n_conformers}') if n_unique_conformers == 0: logger.error('Have no conformers!') return unique_conformers
def get_atomic_charges(self, calc): charges_section = False charges = [] for line in reversed(calc.output.file_lines): if 'sum of mulliken charges' in line.lower(): charges_section = True if len(charges) == calc.molecule.n_atoms: return list(reversed(charges)) if charges_section and len(line.split()) == 3: charges.append(float(line.split()[2])) logger.error('Something went wrong finding the atomic charges') return None
def check_bonds(molecule, bonds): """ Ensure the SMILES string and the 3D structure have the same bonds, but don't override Arguments: molecule (autode.molecule.Molecule): bonds (list): """ check_molecule = deepcopy(molecule) make_graph(check_molecule) if len(bonds) != check_molecule.graph.number_of_edges(): logger.error('Bonds and graph do no match') return None
def calc_delta_e(self): """Calculate the ∆Er of a reaction defined as ∆E = E(products) - E(reactants) Returns: (float): Energy difference in Hartrees """ logger.info('Calculating ∆Er') if any(mol.energy is None for mol in self.reacs + self.prods): logger.error('Cannot calculate ∆Er. At least one required energy ' 'was None') return None return (sum([p.energy for p in self.prods]) - sum([r.energy for r in self.reacs]))
def calc_delta_e_ddagger(self): """Calculate the ∆E‡ of a reaction defined as ∆E = E(ts) - E(reactants) Returns: float: energy difference in Hartrees """ logger.info('Calculating ∆E‡') if self.ts is None: logger.error('No TS, cannot calculate ∆E‡') return None if self.ts.energy is None or any(r.energy is None for r in self.reacs): logger.error('TS or reactants had no energy, cannot calculate ∆E‡') return None return self.ts.energy - sum([r.energy for r in self.reacs])
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 find_lowest_energy_ts(self): """From all the transition state objects in Reaction.pes1d choose the lowest energy if there is more than one otherwise return the single transtion state or None if there no TS objects. """ if self.tss is None: logger.error('Could not find a transition state') return None elif len(self.tss) > 1: logger.info('Found more than 1 TS. Choosing the lowest energy') min_ts_energy = min([ts.energy for ts in self.tss]) return [ts for ts in self.tss if ts.energy == min_ts_energy][0] else: return self.tss[0]