def test_constraints(): os.chdir(os.path.join(here, 'data')) calc = Calculation(name='const_dist_opt', molecule=test_mol, method=method, keywords=opt_keywords, distance_constraints={(0, 1): 1.2}) calc.run() opt_atoms = calc.get_final_atoms() assert 1.199 < np.linalg.norm(opt_atoms[0].coord - opt_atoms[1].coord) < 1.201 calc = Calculation(name='const_cart_opt', molecule=test_mol, method=method, keywords=opt_keywords, cartesian_constraints=[0]) calc.run() opt_atoms = calc.get_final_atoms() assert np.linalg.norm(test_mol.atoms[0].coord - opt_atoms[0].coord) < 1E-3 os.remove('const_cart_opt_g09.com') os.remove('const_dist_opt_g09.com') os.chdir(os.path.join(here))
def test_bad_gauss_output(): calc = Calculation(name='no_output', molecule=test_mol, method=method, keywords=opt_keywords) calc.output_file_lines = [] calc.rev_output_file_lines = [] assert calc.get_energy() is None with pytest.raises(AtomsNotFound): calc.get_final_atoms() with pytest.raises(NoInputError): calc.execute_calculation()
def test_constraints(): calc = Calculation(name='const_dist_opt', molecule=test_mol, method=method, keywords=opt_keywords, distance_constraints={(0, 1): 1.2}) calc.run() opt_atoms = calc.get_final_atoms() assert 1.199 < np.linalg.norm(opt_atoms[0].coord - opt_atoms[1].coord) < 1.201 calc = Calculation(name='const_cart_opt', molecule=test_mol, method=method, keywords=opt_keywords, cartesian_constraints=[0]) calc.run() opt_atoms = calc.get_final_atoms() assert np.linalg.norm(test_mol.atoms[0].coord - opt_atoms[0].coord) < 1E-3
def test_xtb_calculation(): test_mol = Molecule(name='test_mol', smiles='O=C(C=C1)[C@@](C2NC3C=C2)([H])[C@@]3([H])C1=O') calc = Calculation(name='opt', molecule=test_mol, method=method, keywords=Config.XTB.keywords.opt) calc.run() assert os.path.exists('opt_xtb.xyz') is True assert os.path.exists('opt_xtb.out') is True assert len(calc.get_final_atoms()) == 22 assert calc.get_energy() == -36.990267613593 assert calc.output.exists() assert calc.output.file_lines is not None assert calc.input.filename == 'opt_xtb.xyz' assert calc.output.filename == 'opt_xtb.out' with pytest.raises(NotImplementedError): calc.optimisation_nearly_converged() with pytest.raises(NotImplementedError): calc.get_imaginary_freqs() with pytest.raises(NotImplementedError): calc.get_normal_mode_displacements(4) charges = calc.get_atomic_charges() assert len(charges) == 22 assert all(-1.0 < c < 1.0 for c in charges) const_opt = Calculation(name='const_opt', molecule=test_mol, method=method, distance_constraints={(0, 1): 1.2539792}, cartesian_constraints=[0], keywords=Config.XTB.keywords.opt) const_opt.generate_input() assert os.path.exists('const_opt_xtb.xyz') assert os.path.exists('xcontrol_const_opt_xtb') const_opt.clean_up(force=True) assert not os.path.exists('xcontrol_const_opt_xtb') # Write an empty output file open('tmp.out', 'w').close() const_opt.output.filename = 'tmp.out' const_opt.output.set_lines() # cannot get atoms from an empty file with pytest.raises(AtomsNotFound): _ = const_opt.get_final_atoms()
def test_mopac_opt_calculation(): calc = Calculation(name='opt', molecule=methylchloride, method=method, keywords=Config.MOPAC.keywords.opt) calc.run() assert os.path.exists('opt_mopac.mop') is True assert os.path.exists('opt_mopac.out') is True assert len(calc.get_final_atoms()) == 5 # Actual energy in Hartrees energy = Constants.eV2ha * -430.43191 assert energy - 0.0001 < calc.get_energy() < energy + 0.0001 assert calc.output.exists() assert calc.output.file_lines is not None assert calc.input.filename == 'opt_mopac.mop' assert calc.output.filename == 'opt_mopac.out' assert calc.terminated_normally() assert calc.optimisation_converged() is True with pytest.raises(CouldNotGetProperty): _ = calc.get_gradients() with pytest.raises(NotImplementedError): _ = calc.optimisation_nearly_converged() with pytest.raises(NotImplementedError): _ = calc.get_imaginary_freqs() with pytest.raises(NotImplementedError): _ = calc.get_normal_mode_displacements(4)
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 test_opt_calc(): calc = Calculation(name='opt', molecule=test_mol, method=method, keywords=opt_keywords) calc.run() assert os.path.exists('opt_nwchem.nw') assert os.path.exists('opt_nwchem.out') final_atoms = calc.get_final_atoms() assert len(final_atoms) == 5 assert type(final_atoms[0]) is Atom assert -40.4165 < calc.get_energy() < -40.4164 assert calc.output.exists() assert calc.output.file_lines is not None assert calc.get_imaginary_freqs() == [] assert calc.input.filename == 'opt_nwchem.nw' assert calc.output.filename == 'opt_nwchem.out' assert calc.terminated_normally() assert calc.optimisation_converged() assert calc.optimisation_nearly_converged() is False charges = calc.get_atomic_charges() assert len(charges) == 5 assert all(-1.0 < c < 1.0 for c in charges) # Optimisation should result in small gradients gradients = calc.get_gradients() assert len(gradients) == 5 assert all(-0.1 < np.linalg.norm(g) < 0.1 for g in gradients)
def optimise(self, method=None, reset_graph=False, calc=None): """ Optimise the geometry using a method Arguments: method (autode.wrappers.base.ElectronicStructureMethod): Keyword Arguments: reset_graph (bool): Reset the molecular graph calc (autode.calculation.Calculation): Different e.g. constrained optimisation calculation """ logger.info(f'Running optimisation of {self.name}') if calc is None: assert method is not None calc = Calculation(name=f'{self.name}_opt', molecule=self, method=method, keywords=method.keywords.opt, n_cores=Config.n_cores) else: assert isinstance(calc, Calculation) calc.run() self.energy = calc.get_energy() self.set_atoms(atoms=calc.get_final_atoms()) self.print_xyz_file(filename=f'{self.name}_optimised_{method.name}.xyz') if reset_graph: make_graph(self) return None
def test_psi4_opt_calculation(): methylchloride = Molecule(name='CH3Cl', smiles='[H]C([H])(Cl)[H]', solvent_name='water') calc = Calculation(name='opt', molecule=methylchloride, method=method, keywords=opt_keywords) calc.run() assert os.path.exists('opt_psi4.inp') is True assert os.path.exists('opt_orca.out') is True assert len(calc.get_final_atoms()) == 5 assert -499.735 < calc.get_energy() < -499.730 assert calc.output.exists() assert calc.output.file_lines is not None assert calc.get_imaginary_freqs() == [] assert calc.input.filename == 'opt_psi4.inp' assert calc.output.filename == 'opt_psi4.out' assert calc.terminated_normally() assert calc.optimisation_converged() calc = Calculation(name='opt', molecule=methylchloride, method=method, keywords=opt_keywords) # If the calculation is not run with calc.run() then there should be no # input and the calc should raise that there is no input with pytest.raises(NoInputError): execute_calc(calc)
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 test_xtb_6_1_old(): mol = Molecule(name='methane', smiles='C') calc = Calculation(name='test', molecule=mol, method=method, keywords=method.keywords.opt) for filename in ('xtb_6_1_opt.out', 'xtb_no_version_opt.out'): calc.output.filename = filename calc.output.set_lines() assert len(calc.get_final_atoms()) == 5 mol.atoms = calc.get_final_atoms() assert set([atom.label for atom in mol.atoms]) == {'C', 'H'} assert 0.9 < mol.distance(0, 1) < 1.2 calc.output.set_lines()
def test_xtb_6_3_2(): mol = Molecule(name='CH3Cl', smiles='ClC') calc = Calculation(name='test', molecule=mol, method=method, keywords=method.keywords.opt) calc.output.filename = 'xtb_6_3_2_opt.out' calc.output.file_lines = open('xtb_6_3_2_opt.out', 'r').readlines() assert len(calc.get_final_atoms()) == 5
def test_xtb_6_3_2(): mol = Molecule(name='CH3Cl', smiles='ClC') calc = Calculation(name='test', molecule=mol, method=method, keywords=method.keywords.opt) out_path = os.path.join(here, 'data', 'xtb', 'xtb_6_3_2_opt.out') calc.output.filename = out_path calc.output.file_lines = open(out_path, 'r').readlines() assert len(calc.get_final_atoms()) == 5
def test_mopac_with_pc(): calc = Calculation(name='opt_pc', molecule=methylchloride, method=method, keywords=Config.MOPAC.keywords.opt, point_charges=[PointCharge(1, x=4, y=4, z=4)]) calc.run() assert os.path.exists('opt_pc_mopac.mop') is True assert os.path.exists('opt_pc_mopac.out') is True assert len(calc.get_final_atoms()) == 5 # Actual energy in Hartrees without any point charges energy = Constants.eV2ha * -430.43191 assert np.abs(calc.get_energy() - energy) > 0.0001
def test_gauss_opt_calc(): os.chdir(os.path.join(here, 'data')) methylchloride = Molecule(name='CH3Cl', smiles='[H]C([H])(Cl)[H]', solvent_name='water') calc = Calculation(name='opt', molecule=methylchloride, method=method, keywords=opt_keywords) calc.run() assert os.path.exists('opt_g09.com') assert os.path.exists('opt_g09.log') assert len(calc.get_final_atoms()) == 5 assert os.path.exists('opt_g09.xyz') assert calc.get_energy() == -499.729222331 assert calc.output.exists() assert calc.output.file_lines is not None assert calc.get_imaginary_freqs() == [] with pytest.raises(NoNormalModesFound): calc.get_normal_mode_displacements(mode_number=1) assert calc.input.filename == 'opt_g09.com' assert calc.output.filename == 'opt_g09.log' assert calc.terminated_normally() assert calc.optimisation_converged() assert calc.optimisation_nearly_converged() is False charges = calc.get_atomic_charges() assert len(charges) == methylchloride.n_atoms # Should be no very large atomic charges in this molecule assert all(-1.0 < c < 1.0 for c in charges) gradients = calc.get_gradients() assert len(gradients) == methylchloride.n_atoms assert len(gradients[0]) == 3 # Should be no large forces for an optimised molecule assert sum(gradients[0]) < 0.1 os.remove('opt_g09.com') os.chdir(here)
def test_orca_opt_calculation(): os.chdir(os.path.join(here, 'data')) methylchloride = Molecule(name='CH3Cl', smiles='[H]C([H])(Cl)[H]', solvent_name='water') calc = Calculation(name='opt', molecule=methylchloride, method=method, keywords=opt_keywords) calc.run() assert os.path.exists('opt_orca.inp') is True assert os.path.exists('opt_orca.out') is True assert len(calc.get_final_atoms()) == 5 assert -499.735 < calc.get_energy() < -499.730 assert calc.output.exists() assert calc.output.file_lines is not None assert calc.get_imaginary_freqs() == [] assert calc.input.filename == 'opt_orca.inp' assert calc.output.filename == 'opt_orca.out' assert calc.terminated_normally() assert calc.optimisation_converged() assert calc.optimisation_nearly_converged() is False with pytest.raises(NoNormalModesFound): calc.get_normal_mode_displacements(mode_number=0) # Should have a partial atomic charge for every atom charges = calc.get_atomic_charges() assert len(charges) == 5 assert type(charges[0]) == float assert -1.0 < charges[0] < 1.0 calc = Calculation(name='opt', molecule=methylchloride, method=method, keywords=opt_keywords) # If the calculation is not run with calc.run() then there should be no # input and the calc should raise that there is no input with pytest.raises(NoInputError): execute_calc(calc) os.remove('opt_orca.inp') os.chdir(here)
def test_opt_hf_constraints(): keywords = OptKeywords([ 'driver\n gmax 0.002\n grms 0.0005\n' ' xmax 0.01\n xrms 0.007\n eprec 0.00003\nend', 'basis\n * library Def2-SVP\nend', 'task scf optimize' ]) h2o = Molecule(name='water', smiles='O') calc = Calculation(name='opt_water', molecule=h2o, method=method, keywords=keywords, cartesian_constraints=[0], distance_constraints={(0, 1): 0.95}) calc.run() h2o.atoms = calc.get_final_atoms() assert 0.94 < h2o.distance(0, 1) < 0.96
def test_xtb_calculation(): os.chdir(os.path.join(here, 'data')) XTB.available = True test_mol = Molecule(name='test_mol', smiles='O=C(C=C1)[C@@](C2NC3C=C2)([H])[C@@]3([H])C1=O') calc = Calculation(name='opt', molecule=test_mol, method=method, keywords=Config.XTB.keywords.opt) calc.run() assert os.path.exists('opt_xtb.xyz') is True assert os.path.exists('opt_xtb.out') is True assert len(calc.get_final_atoms()) == 22 assert calc.get_energy() == -36.990267613593 assert calc.output.exists() assert calc.output.file_lines is not None assert calc.input.filename == 'opt_xtb.xyz' assert calc.output.filename == 'opt_xtb.out' with pytest.raises(NotImplementedError): calc.optimisation_nearly_converged() with pytest.raises(NotImplementedError): calc.get_imaginary_freqs() with pytest.raises(NotImplementedError): calc.get_normal_mode_displacements(4) charges = calc.get_atomic_charges() assert len(charges) == 22 assert all(-1.0 < c < 1.0 for c in charges) const_opt = Calculation(name='const_opt', molecule=test_mol, method=method, distance_constraints={(0, 1): 1.2539792}, cartesian_constraints=[0], keywords=Config.XTB.keywords.opt) const_opt.generate_input() assert os.path.exists('xcontrol_const_opt_xtb') os.remove('const_opt_xtb.xyz') os.remove('xcontrol_const_opt_xtb') os.remove('opt_xtb.xyz') os.chdir(here)
def test_opt_hf_constraints(): os.chdir(os.path.join(here, 'data')) keywords = OptKeywords([ 'driver\n gmax 0.002\n grms 0.0005\n' ' xmax 0.01\n xrms 0.007\n eprec 0.00003\nend', 'basis\n * library Def2-SVP\nend', 'task scf optimize' ]) h2o = Molecule(name='water', smiles='O') calc = Calculation(name='opt_water', molecule=h2o, method=method, keywords=keywords, cartesian_constraints=[0], distance_constraints={(0, 1): 0.95}) calc.run() h2o.set_atoms(atoms=calc.get_final_atoms()) assert 0.94 < h2o.get_distance(0, 1) < 0.96 os.remove('opt_water_nwchem.nw') os.chdir(here)
class TransitionState(TSbase): @requires_graph() def _update_graph(self): """Update the molecular graph to include all the bonds that are being made/broken""" if self.bond_rearrangement is None: logger.warning('Bond rearrangement not set - molecular graph ' 'updating with no active bonds') active_bonds = [] else: active_bonds = self.bond_rearrangement.all set_active_mol_graph(species=self, active_bonds=active_bonds) logger.info(f'Molecular graph updated with active bonds') return None 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 _generate_conformers(self, n_confs=None): """Generate conformers at the TS """ from autode.conformers.conformer import Conformer from autode.conformers.conf_gen import get_simanl_atoms from autode.conformers.conformers import conf_is_unique_rmsd n_confs = Config.num_conformers if n_confs is None else n_confs self.conformers = [] distance_consts = get_distance_constraints(self) with Pool(processes=Config.n_cores) as pool: results = [ pool.apply_async(get_simanl_atoms, (self, distance_consts, i)) for i in range(n_confs) ] conf_atoms_list = [res.get(timeout=None) for res in results] for i, atoms in enumerate(conf_atoms_list): conf = Conformer(name=f'{self.name}_conf{i}', charge=self.charge, mult=self.mult, atoms=atoms, dist_consts=distance_consts) # If the conformer is unique on an RMSD threshold if conf_is_unique_rmsd(conf, self.conformers): conf.solvent = self.solvent conf.graph = deepcopy(self.graph) self.conformers.append(conf) logger.info(f'Generated {len(self.conformers)} conformer(s)') return None @requires_atoms() def print_imag_vector(self, mode_number=6, name=None): """Print a .xyz file with multiple structures visualising the largest magnitude imaginary mode Keyword Arguments: mode_number (int): Number of the normal mode to visualise, 6 (default) is the lowest frequency vibration i.e. largest magnitude imaginary, if present name (str): """ assert self.optts_calc is not None name = self.name if name is None else name disp = -0.5 for i in range(40): atoms = get_displaced_atoms_along_mode( calc=self.optts_calc, mode_number=int(mode_number), disp_magnitude=disp, atoms=self.atoms) atoms_to_xyz_file(atoms=atoms, filename=f'{name}.xyz', append=True) # Add displacement so the final set of atoms are +0.5 Å displaced # along the mode, then displaced back again sign = 1 if i < 20 else -1 disp += sign * 1.0 / 20.0 return None @requires_atoms() 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 calc_g_cont(self, method=None, calc=None, temp=None): """Calculate the free energy (G) contribution""" return super().calc_g_cont(method=method, calc=self.optts_calc, temp=temp) def calc_h_cont(self, method=None, calc=None, temp=None): """Calculate the enthalpy (H) contribution""" return super().calc_h_cont(method=method, calc=self.optts_calc, temp=temp) 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 is_true_ts(self): """Is this TS a 'true' TS i.e. has at least on imaginary mode in the hessian and is the correct mode""" if self.energy is None: logger.warning('Cannot be true TS with no energy') return False if len(self.imaginary_frequencies) > 0: if self.has_correct_imag_mode(calc=self.optts_calc): logger.info('Found a transition state with the correct ' 'imaginary mode & links reactants and products') return True return False def save_ts_template(self, folder_path=None): """Save a transition state template containing the active bond lengths, solvent and charge in folder_path Keyword Arguments: folder_path (str): folder to save the TS template to (default: {None}) """ if self.bond_rearrangement is None: raise ValueError('Cannot save a TS template without a bond ' 'rearrangement') logger.info(f'Saving TS template for {self.name}') truncated_graph = get_truncated_active_mol_graph(self.graph) for bond in self.bond_rearrangement.all: truncated_graph.edges[bond]['distance'] = self.distance(*bond) ts_template = TStemplate(truncated_graph, species=self) ts_template.save(folder_path=folder_path) logger.info('Saved TS template') return None def __init__(self, ts_guess): """ Transition State Arguments: ts_guess (autode.transition_states.ts_guess.TSguess): """ super().__init__(atoms=ts_guess.atoms, reactant=ts_guess.reactant, product=ts_guess.product, name=f'TS_{ts_guess.name}', charge=ts_guess.charge, mult=ts_guess.mult) self.bond_rearrangement = ts_guess.bond_rearrangement self.conformers = None self.optts_calc = None self.imaginary_frequencies = [] self._update_graph()
def test_calc_class(): xtb = XTB() calc = Calculation(name='-tmp', molecule=test_mol, method=xtb, keywords=xtb.keywords.sp) # Should prepend a dash to appease some EST methods assert not calc.name.startswith('-') assert calc.molecule is not None assert calc.method.name == 'xtb' assert calc.get_energy() is None assert calc.get_enthalpy() is None assert calc.get_free_energy() is None assert not calc.optimisation_converged() assert not calc.optimisation_nearly_converged() with pytest.raises(ex.AtomsNotFound): _ = calc.get_final_atoms() with pytest.raises(ex.CouldNotGetProperty): _ = calc.get_gradients() with pytest.raises(ex.CouldNotGetProperty): _ = calc.get_atomic_charges() # Calculation that has not been run shouldn't have an opt converged assert not calc.optimisation_converged() assert not calc.optimisation_nearly_converged() # With a filename that doesn't exist a NoOutput exception should be raised calc.output.filename = '/a/path/that/does/not/exist/tmp' with pytest.raises(ex.NoCalculationOutput): calc.output.set_lines() # With no output should not be able to get properties calc.output.filename = 'tmp' calc.output.file_lines = [] with pytest.raises(ex.CouldNotGetProperty): _ = calc.get_atomic_charges() # or final atoms with pytest.raises(ex.AtomsNotFound): _ = calc.get_final_atoms() # Should default to a single core assert calc.n_cores == 1 calc_str = str(calc) new_calc = Calculation(name='tmp2', molecule=test_mol, method=xtb, keywords=xtb.keywords.sp) new_calc_str = str(new_calc) # Calculation strings need to be unique assert new_calc_str != calc_str new_calc = Calculation(name='tmp2', molecule=test_mol, method=xtb, keywords=xtb.keywords.sp, temp=5000) assert str(new_calc) != new_calc_str mol_no_atoms = Molecule() with pytest.raises(ex.NoInputError): _ = Calculation(name='tmp2', molecule=mol_no_atoms, method=xtb, keywords=xtb.keywords.sp)
def optimise(molecule, method, keywords, n_cores=None, cartesian_constraints=None): """ Optimise a molecule :param molecule: (object) :param method: (autode.ElectronicStructureMethod) :param keywords: (list(str)) Keywords to use for the electronic structure c alculation e.g. ['Opt', 'PBE', 'def2-SVP'] :param n_cores: (int) Number of cores to use :param cartesian_constraints: (list(int)) List of atom ids to constrain :return: """ logger.info('Running an optimisation calculation') n_cores = Config.n_cores if n_cores is None else int(n_cores) try: from autode.calculation import Calculation from autode.wrappers.XTB import xtb from autode.wrappers.ORCA import orca from autode.wrappers.keywords import OptKeywords except ModuleNotFoundError: logger.error('autode not found. Calculations not available') raise RequiresAutodE if keywords is None: if method == orca: keywords = OptKeywords(['LooseOpt', 'PBE', 'D3BJ', 'def2-SVP']) logger.warning(f'No keywords were set for the optimisation but an' f' ORCA calculation was requested. ' f'Using {str(keywords)}') elif method == xtb: keywords = xtb.keywords.opt else: logger.critical('No keywords were set for the optimisation ' 'calculation') raise Exception else: # If the keywords are specified as a list convert them to a set of # OptKeywords, required for autodE if type(keywords) is list: keywords = OptKeywords(keywords) opt = Calculation(name=molecule.name + '_opt', molecule=molecule, method=method, keywords=keywords, n_cores=n_cores, cartesian_constraints=cartesian_constraints) opt.run() molecule.energy = opt.get_energy() molecule.set_atoms(atoms=opt.get_final_atoms()) return None
class TransitionState(TSbase): @requires_graph() def _update_graph(self): """Update the molecular graph to include all the bonds that are being made/broken""" set_active_mol_graph(species=self, active_bonds=self.bond_rearrangement.all) logger.info(f'Molecular graph updated with ' f'{len(self.bond_rearrangement.all)} active bonds') return None def _run_opt_ts_calc(self, method, name_ext): """Run an optts calculation and attempt to set the geometry, energy and normal modes""" 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=self.bond_rearrangement.all, 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.set_atoms(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=self.bond_rearrangement.all, 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.set_atoms(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 _generate_conformers(self, n_confs=None): """Generate conformers at the TS """ from autode.conformers.conformer import Conformer from autode.conformers.conf_gen import get_simanl_atoms from autode.conformers.conformers import conf_is_unique_rmsd n_confs = Config.num_conformers if n_confs is None else n_confs self.conformers = [] distance_consts = get_distance_constraints(self) with Pool(processes=Config.n_cores) as pool: results = [pool.apply_async(get_simanl_atoms, (self, distance_consts, i)) for i in range(n_confs)] conf_atoms_list = [res.get(timeout=None) for res in results] for i, atoms in enumerate(conf_atoms_list): conf = Conformer(name=f'{self.name}_conf{i}', charge=self.charge, mult=self.mult, atoms=atoms, dist_consts=distance_consts) # If the conformer is unique on an RMSD threshold if conf_is_unique_rmsd(conf, self.conformers): conf.solvent = self.solvent conf.graph = deepcopy(self.graph) self.conformers.append(conf) logger.info(f'Generated {len(self.conformers)} conformer(s)') return None @requires_atoms() 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 None if len(self.imaginary_frequencies) == 0: logger.error('Transition state optimisation did not return any ' 'imaginary frequencies') return None # There is more than one imaginary frequency. Will assume that the most # negative is the correct mode.. for disp_magnitude in [1, -1]: 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.set_atoms(atoms) self.energy = energy self.imaginary_frequencies = self.optts_calc.get_imaginary_freqs() return None 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 is_true_ts(self): """Is this TS a 'true' TS i.e. has at least on imaginary mode in the hessian and is the correct mode""" if self.energy is None: logger.warning('Cannot be true TS with no energy') return False if len(self.imaginary_frequencies) > 0: if self.has_correct_imag_mode(calc=self.optts_calc): logger.info('Found a transition state with the correct ' 'imaginary mode & links reactants and products') return True return False def save_ts_template(self, folder_path=Config.ts_template_folder_path): """Save a transition state template containing the active bond lengths, solvent and charge in folder_path Keyword Arguments: folder_path (str): folder to save the TS template to (default: {None}) """ logger.info(f'Saving TS template for {self.name}') truncated_graph = get_truncated_active_mol_graph(self.graph, active_bonds=self.bond_rearrangement.all) for bond in self.bond_rearrangement.all: truncated_graph.edges[bond]['distance'] = self.get_distance(*bond) ts_template = TStemplate(truncated_graph, solvent=self.solvent, charge=self.charge, mult=self.mult) ts_template.save_object(folder_path=folder_path) logger.info('Saved TS template') return None def __init__(self, ts_guess): """ Transition State Arguments: ts_guess (autode.transition_states.ts_guess.TSguess) """ super().__init__(atoms=ts_guess.atoms, reactant=ts_guess.reactant, product=ts_guess.product, name=f'TS_{ts_guess.name}') self.bond_rearrangement = ts_guess.bond_rearrangement self.conformers = None self.optts_calc = None self.imaginary_frequencies = [] self._update_graph()