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_gauss_optts_calc(): 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
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_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_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_bad_geometry(): # Calculation with the wrong spin state should fail calc = Calculation(name='h2_overlap_opt', molecule=Molecule(atoms=[Atom('H'), Atom('H')]), method=method, keywords=Config.MOPAC.keywords.opt) calc.output.filename = 'h2_overlap_opt_mopac.out' calc.output.file_lines = open(calc.output.filename, 'r').readlines() assert not calc.terminated_normally() assert calc.get_energy() is None assert not calc.optimisation_converged()
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)
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 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_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)