def test_thermal_cont_without_hess_run(): calc = Calculation(name='test', molecule=mol, method=orca, keywords=orca.keywords.hess, temp=298) mol.energy = -1 # Some blank output that exists calc.output.filename = 'test' calc.output.file_lines = [] # Calculating the free energy contribution without a correct Hessian # calculation should not raise an exception(?) mol.calc_g_cont(calc=calc) assert mol.g_cont is None # and similarly with the enthalpic contribution mol.calc_h_cont(calc=calc) assert mol.h_cont is None
def test_links_reacs_prods(): tsguess.calc = Calculation(name=tsguess.name + '_hess', molecule=tsguess, method=method, keywords=method.keywords.hess, n_cores=Config.n_cores) # Should find the completed calculation output tsguess.calc.run() # Spoof an xtb install as reactant/product complex optimisation Config.lcode = 'xtb' # Config.XTB.path = here Config.num_complex_sphere_points = 4 Config.num_complex_random_rotations = 1 assert imag_mode_links_reactant_products(calc=tsguess.calc, reactant=reac_complex, product=product_complex, method=method)
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 has_correct_mode(name, fbonds, bbonds): xyz_path = os.path.join(data_path, f'{name}.xyz') reac = Reactant(name='r', atoms=xyz_file_to_atoms(xyz_path)) calc = Calculation(name=name, molecule=reac, method=orca, keywords=orca.keywords.opt_ts, n_cores=1) output_path = os.path.join(data_path, f'{name}.out') calc.output.filename = output_path calc.output.file_lines = open(output_path, 'r').readlines() bond_rearr = BondRearrangement(breaking_bonds=bbonds, forming_bonds=fbonds) # Don't require all bonds to be breaking/making in a 'could be ts' function return imag_mode_has_correct_displacement(calc, bond_rearr, delta_threshold=0.05, req_all=False)
def optimise(self, method=None, reset_graph=False, calc=None, keywords=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 keywords (autode.wrappers.keywords.Keywords): Raises: (autode.exceptions.CalculationException): """ logger.info(f'Running optimisation of {self.name}') if calc is None: assert method is not None keywords = method.keywords.opt if keywords is None else keywords calc = Calculation(name=f'{self.name}_opt', molecule=self, method=method, keywords=keywords, n_cores=Config.n_cores) else: assert isinstance(calc, Calculation) calc.run() self.energy = calc.get_energy() self.atoms = calc.get_final_atoms() method_name = '' if method is None else method.name self.print_xyz_file( filename=f'{self.name}_optimised_{method_name}.xyz') if reset_graph: make_graph(self) return None
def optimise(self, method=None, reset_graph=False, calc=None, keywords=None): """ Optimise the geometry of this conformer Arguments: method (autode.wrappers.base.ElectronicStructureMethod): Keyword Arguments: reset_graph (bool): calc (autode.calculation.Calculation): keywords (autode.wrappers.keywords.Keywords): """ 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.atoms = opt.get_final_atoms() except AtomsNotFound: logger.error(f'Atoms not found for {self.name} but not critical') self.atoms = None return None
def test_orca_optts_calculation(): methane = SolvatedMolecule(name='methane', smiles='C') methane.qm_solvent_atoms = [] calc = Calculation(name='test_ts_reopt_optts', molecule=methane, method=method, bond_ids_to_add=[(0, 1)], keywords=opt_keywords, other_input_block='%geom\n' 'Calc_Hess true\n' 'Recalc_Hess 40\n' 'Trust 0.2\n' 'MaxIter 100\nend') calc.run() assert os.path.exists('test_ts_reopt_optts_orca.inp') assert calc.get_normal_mode_displacements(mode_number=6) is not None assert calc.terminated_normally() assert calc.optimisation_converged() assert calc.optimisation_nearly_converged() is False assert len(calc.get_imaginary_freqs()) == 1 # Gradients should be an n_atom x 3 array gradients = calc.get_gradients() assert len(gradients) == 5 assert len(gradients[0]) == 3 assert -599.437 < calc.get_enthalpy() < -599.436 assert -599.469 < calc.get_free_energy() < -599.468
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_other_spin_states(): o_singlet = Molecule(atoms=[Atom('O')], mult=1) o_singlet.name = 'molecule' calc = Calculation(name='O_singlet', molecule=o_singlet, method=method, keywords=Config.MOPAC.keywords.sp) calc.run() singlet_energy = calc.get_energy() o_triplet = Molecule(atoms=[Atom('O')], mult=3) o_triplet.name = 'molecule' calc = Calculation(name='O_triplet', molecule=o_triplet, method=method, keywords=Config.MOPAC.keywords.sp) calc.run() triplet_energy = calc.get_energy() assert triplet_energy < singlet_energy h_doublet = Molecule(atoms=[Atom('H')], mult=2) h_doublet.name = 'molecule' calc = Calculation(name='h', molecule=h_doublet, method=method, keywords=Config.MOPAC.keywords.sp) calc.run() # Open shell doublet should work assert calc.get_energy() is not None h_quin = Molecule(atoms=[Atom('H')], mult=5) h_quin.name = 'molecule' with pytest.raises(UnsuppportedCalculationInput): calc = Calculation(name='h', molecule=h_quin, method=method, keywords=Config.MOPAC.keywords.sp) calc.run() os.chdir(here)
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 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 test_input_gen(): xtb = XTB() calc = Calculation(name='tmp', molecule=test_mol, method=xtb, keywords=xtb.keywords.sp) Config.keep_input_files = True calc.generate_input() assert os.path.exists('tmp_xtb.xyz') calc.clean_up() # Clean-up should do nothing if keep_input_files = True assert os.path.exists('tmp_xtb.xyz') # but should be able to be forced calc.clean_up(force=True) assert not os.path.exists('tmp_xtb.xyz') # Test the keywords parsing unsupported_func = Functional('PBE', orca='PBE') calc_kwds = Calculation(name='tmp', molecule=test_mol, method=xtb, keywords=SinglePointKeywords([unsupported_func])) with pytest.raises(ex.UnsuppportedCalculationInput): calc_kwds.generate_input()
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 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
def test_gauss_optts_calc(): os.chdir(os.path.join(here, 'data')) calc = Calculation(name='test_ts_reopt_optts', molecule=test_mol, method=method, keywords=optts_keywords, bond_ids_to_add=[(0, 1)]) calc.run() print(calc.input.added_internals) assert os.path.exists('test_ts_reopt_optts_g09.com') bond_added = False for line in open('test_ts_reopt_optts_g09.com', 'r'): if 'B' in line and len(line.split()) == 3: bond_added = True assert line.split()[0] == 'B' assert line.split()[1] == '1' assert line.split()[2] == '2' assert bond_added assert calc.get_normal_mode_displacements(mode_number=6) is not None assert calc.terminated_normally() assert calc.optimisation_converged() assert calc.optimisation_nearly_converged() is False assert len(calc.get_imaginary_freqs()) == 1 assert -40.324 < calc.get_free_energy() < -40.322 assert -40.301 < calc.get_enthalpy() < -40.299 os.remove('test_ts_reopt_optts_g09.com') os.chdir(here)
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 test_orca_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_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)
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 (Hartrees) the calculation will be disregarded """ 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 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()
# List of energies to be populated energies = [] for r in rs: o_atom, h_atom = water.atoms[:2] curr_r = water.get_distance(0, 1) # current O-H distance # Shift the hydrogen atom to the required distance # vector = (h_atom.coord - o_atom.coord) / curr_r * (r - curr_r) vector = (h_atom.coord - o_atom.coord) * (r / curr_r - 1) h_atom.translate(vector) # Set up and run the calculation calc = Calculation(name=f'H2O_scan_{r:.2f}', molecule=water, method=xtb, keywords=xtb.keywords.sp) calc.run() # Get the potential energy from the calculation energy = calc.get_energy() energies.append(energy) # Plot the relative energy against the distance. 627.5 kcal mol-1 Ha-1 rel_energies = 627.5 * np.array([e - min(energies) for e in energies]) plt.plot(rs, rel_energies, marker='o') plt.ylabel('ΔE / kcal mol$^{-1}$') plt.xlabel('r / Å') plt.savefig('OH_PES_unrelaxed.png')
def test_solvation(): methane = Molecule(name='solvated_methane', smiles='C', solvent_name='water') with pytest.raises(UnsuppportedCalculationInput): # Should raise on unsupported calculation type method.implicit_solvation_type = 'xxx' calc = Calculation(name='broken_solvation', molecule=methane, method=method, keywords=sp_keywords) calc.run() method.implicit_solvation_type = 'CPCM' calc = Calculation(name='methane_cpcm', molecule=methane, method=method, keywords=sp_keywords) calc.generate_input() assert any('cpcm' in line.lower() for line in open('methane_cpcm_orca.inp', 'r')) os.remove('methane_cpcm_orca.inp') method.implicit_solvation_type = 'SMD' calc = Calculation(name='methane_smd', molecule=methane, method=method, keywords=sp_keywords) calc.generate_input() assert any('smd' in line.lower() for line in open('methane_smd_orca.inp', 'r')) os.remove('methane_smd_orca.inp')
def test_fix_unique(): """So calculations with different input but the same name are not skipped autodE checks the input of each previously run calc with the name name""" orca = ORCA() calc = Calculation(name='tmp', molecule=test_mol, method=orca, keywords=orca.keywords.sp) calc._fix_unique() assert calc.name == 'tmp_orca' # Should generate a register assert os.path.exists('.autode_calculations') assert len(open('.autode_calculations', 'r').readlines()) == 1 calc = Calculation(name='tmp', molecule=test_mol, method=orca, keywords=orca.keywords.opt) calc._fix_unique() assert calc.name != 'tmp_orca' assert calc.name == 'tmp_orca0' # no need to fix unique if the name is different calc = Calculation(name='tmp2', molecule=test_mol, method=orca, keywords=orca.keywords.opt) calc._fix_unique() assert calc.name == 'tmp2_orca'
def test_gradients(): h2 = Molecule(name='h2', atoms=[Atom('H'), Atom('H', x=1.0)]) calc = Calculation(name='h2_grad', molecule=h2, method=method, keywords=method.keywords.grad) calc.run() h2.energy = calc.get_energy() delta_r = 1E-8 # Energy of a finite difference approximation h2_disp = Molecule(name='h2_disp', atoms=[Atom('H'), Atom('H', x=1.0 + delta_r)]) calc = Calculation(name='h2_disp', molecule=h2_disp, method=method, keywords=method.keywords.grad) calc.run() h2_disp.energy = calc.get_energy() delta_energy = h2_disp.energy - h2.energy # Ha grad = delta_energy / delta_r # Ha A^-1 calc = Calculation(name='h2_grad', molecule=h2, method=method, keywords=method.keywords.grad) calc.run() diff = calc.get_gradients()[1, 0] - grad # Ha A^-1 # Difference between the absolute and finite difference approximation assert np.abs(diff) < 1E-3
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)
# and constrained optimisations (relaxed) calculations sp_energies, opt_energies = [], [] for r in rs: o_atom, h_atom = water.atoms[:2] curr_r = water.get_distance(0, 1) # current O-H distance # Shift the hydrogen atom to the required distance # vector = (h_atom.coord - o_atom.coord) / curr_r * (r - curr_r) vector = (h_atom.coord - o_atom.coord) * (r / curr_r - 1) h_atom.translate(vector) # Set up and run the single point energy evaluation sp = Calculation(name=f'H2O_scan_unrelaxed_{r:.2f}', molecule=water, method=xtb, keywords=xtb.keywords.sp) sp.run() sp_energies.append(sp.get_energy()) # Set up the constrained optimisation calculation where the distance # constraints are given as a dictionary keyed with a tuple of atom indexes # with the distance as the value opt = Calculation(name=f'H2O_scan_relaxed_{r:.2f}', molecule=water, method=xtb, keywords=xtb.keywords.low_opt, distance_constraints={(0, 1): r}) opt.run() opt_energies.append(opt.get_energy())
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()
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)
# Create arrays for OH distances and their energies rs = np.linspace(0.65, 2.0, num=15) energies = [] # Calculate the energy array for r in rs: o_atom, h_atom = water.atoms[:2] curr_r = water.distance(0, 1) vector = (h_atom.coord - o_atom.coord) * (r / curr_r - 1) h_atom.translate(vector) # Set up and run the calculation calc = Calculation(name=f'H2O_scan_{r:.2f}', molecule=water, method=orca, keywords=keywords) calc.run() # Get the potential energy from the calculation energy = calc.get_energy() energies.append(energy) # Plot the relative energy against the distance. 627.5 kcal mol-1 Ha-1 rel_energies = 627.5 * np.array([e - min(energies) for e in energies]) plt.plot(rs, rel_energies, marker='o', label=dft_name) # Add labels to the plot and save the figure plt.ylabel('ΔE / kcal mol$^{-1}$') plt.xlabel('r / Å') plt.legend()
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()
class TSbase(Species): def _init_graph(self): """Set the molecular graph for this TS object from the reactant""" logger.warning(f'Setting the graph of {self.name} from reactants') self.graph = self.reactant.graph.copy() 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): """ # 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 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 __init__(self, atoms, reactant, product, name='ts_guess'): super().__init__(name=name, atoms=atoms, charge=reactant.charge, mult=reactant.mult) self.solvent = reactant.solvent self.atoms = atoms self.reactant = reactant self.product = product self.calc = None self.bond_rearrangement = None self._init_graph()