def check(self): """ Checks output file for errors """ self.outdata = QCOutput(self.output_file).data self.errors = self.outdata.get("errors") return len(self.errors) > 0
def test_in_database_with_actual_database(self): db_file = os.path.join(db_dir, "db.json") dir2620 = os.path.join(module_dir, "..", "..", "test_files", "2620_complete") mol2620 = QCOutput(os.path.join( dir2620, "mol.qout.opt_0")).data["initial_molecule"] parse_firetask = QChemToDb(calc_dir=dir2620, db_file=db_file) parse_firetask.run_task({}) with patch("atomate.qchem.firetasks.fragmenter.FWAction" ) as FWAction_patch: ft = FragmentMolecule( molecule=self.neg_tfsi, db_file=db_file, depth=0, additional_charges=[-2, 1], do_triplets=False, ) ft.run_task({}) self.assertEqual(ft.check_db, True) self.assertEqual(ft.charges, [-1, 0, -2, 1]) self.assertEqual(len(FWAction_patch.call_args[1]["additions"]), 623) self.assertEqual(ft._in_database(mol2620), True) mol2620.set_charge_and_spin(charge=0) self.assertEqual(ft._in_database(mol2620), False)
def check(self): # Checks output file for errors. self.outdata = QCOutput(self.output_file).data self.errors = self.outdata.get("errors") if "out_of_opt_cycles" not in self.errors and len( self.opt_error_history) > 0: self.opt_error_history = [] if "out_of_opt_cycles" in self.errors: if self.outdata["structure_change"] == "unconnected_fragments": return False return len(self.errors) > 0
def run_QChem(label, encode=None, rem=None, pcm=None, solvent=None, more_info=None, self_correct=True): inname = label + '.inp' outname = label + '.out' logname = label + '.log' command = 'qchem' handlers = [QChemErrorHandler(input_file=inname, output_file=outname)] """If no encoding provided, assume this is the first Firework in workflow and that input file is already written. 'label' is the name of the file without the extension (e.g. .inp, .out). otherwise, take encoding, form new QCInput and write input file, then run. """ if encode != None: qcin = encode_to_QCInput(encode=encode, rem=rem, pcm=pcm, solvent=solvent) qcin.write_file(inname) if self_correct: jobs = [ QCJob(input_file=inname, output_file=outname, qchem_command=command, max_cores=multiprocessing.cpu_count(), qclog_file=logname) ] c = Custodian(handlers, jobs, max_errors=10) c.run() else: job = QCJob(input_file=inname, output_file=outname, qchem_command=command, max_cores=multiprocessing.cpu_count(), qclog_file=logname) job.setup() p = job.run() p.wait() """ qclog = open(logname, "w") current_command = ['qchem', '-nt', '20',inname] print(current_command) subprocess.run(current_command, stdout=qclog, shell=True) """ try: output = [QCOutput(filename=outname)] except: output = QCOutput.multiple_outputs_from_file(QCOutput, filename) return QCOutput_to_encode(output, more_info=more_info)
def check(self): # Checks output file for errors. self.outdata = QCOutput(self.output_file).data self.errors = self.outdata.get("errors") self.warnings = self.outdata.get("warnings") # If we aren't out of optimization cycles, but we were in the past, reset the history if "out_of_opt_cycles" not in self.errors and len(self.opt_error_history) > 0: self.opt_error_history = [] # If we're out of optimization cycles and we have unconnected fragments, no need to handle any errors if "out_of_opt_cycles" in self.errors and self.outdata["structure_change"] == "unconnected_fragments": return False return len(self.errors) > 0
def test_all(self): single_outs = dict() for file in single_job_out_names: single_outs[file] = QCOutput(os.path.join(test_dir, file)).data multi_outs = dict() for file in multi_job_out_names: multi_outs[file] = QCOutput.multiple_outputs_from_file( QCOutput, os.path.join(test_dir, file), keep_sub_files=False) for key in property_list: print('Testing ', key) self._test_property(key, single_outs, multi_outs)
def test_all(self): self.maxDiff = None single_outs = {} for file in single_job_out_names: single_outs[file] = QCOutput(os.path.join(PymatgenTest.TEST_FILES_DIR, "molecules", file)).data multi_outs = {} for file in multi_job_out_names: multi_outs[file] = QCOutput.multiple_outputs_from_file( QCOutput, os.path.join(PymatgenTest.TEST_FILES_DIR, "molecules", file), keep_sub_files=False ) for key in property_list: print("Testing ", key) self._test_property(key, single_outs, multi_outs)
def test_NBO5_vs_NBO7_hybridization_character(self): data5 = QCOutput(os.path.join(PymatgenTest.TEST_FILES_DIR, "molecules", "new_qchem_files", "nbo5_1.qout")).data data7 = QCOutput(os.path.join(PymatgenTest.TEST_FILES_DIR, "molecules", "new_qchem_files", "nbo7_1.qout")).data self.assertEqual( len(data5["nbo_data"]["hybridization_character"]), len(data7["nbo_data"]["hybridization_character"]) ) self.assertEqual( data5["nbo_data"]["hybridization_character"][3]["atom 2 pol coeff"][9], data7["nbo_data"]["hybridization_character"][3]["atom 2 pol coeff"][9], ) self.assertEqual( data5["nbo_data"]["hybridization_character"][0]["s"][0], data7["nbo_data"]["hybridization_character"][0]["s"][0], ) self.assertEqual(data5["nbo_data"]["hybridization_character"][1]["bond index"][7], "149") self.assertEqual(data7["nbo_data"]["hybridization_character"][1]["bond index"][7], "21")
def setUp(self): self.opt_freq_params = { "rem": { "job_type": "opt", "method": "wb97x-d", "basis": "6-31g*", "max_scf_cycles": 200, "gen_scfman": True, "scf_algorithm": "diis", "geom_opt_max_cycles": 200 } } self.sp_params = { "rem": { "job_type": "sp", "method": "wb97x-d", "basis": "6-311++g(d,p)", "max_scf_cycles": 200, "gen_scfman": True, "scf_algorithm": "diis", "solvent_method": "pcm" }, "pcm": { "theory": "iefpcm" }, "solvent": { "dielectric": 80.4 } } out_file = os.path.join(files_dir, "qchem", "test.qout.opt_0") qc_out = QCOutput(filename=out_file) self.act_mol = qc_out.data["initial_molecule"] super(TestOptFreqSPFW, self).setUp(lpad=False)
def process_qchem_multirun(dir_name, input_files, output_files): """ Process a QChem run which is known to include multiple calculations in a single input/output pair. """ if len(input_files) != 1: raise ValueError( "ERROR: The drone can only process a directory containing a single input/output pair when each include multiple calculations." ) else: for key in input_files: to_return = [] qchem_input_file = os.path.join(dir_name, input_files.get(key)) qchem_output_file = os.path.join(dir_name, output_files.get(key)) multi_out = QCOutput.multiple_outputs_from_file( QCOutput, qchem_output_file, keep_sub_files=False) multi_in = QCInput.from_multi_jobs_file(qchem_input_file) for ii, out in enumerate(multi_out): d = out.data d["input"] = {} d["input"]["molecule"] = multi_in[ii].molecule d["input"]["rem"] = multi_in[ii].rem d["input"]["opt"] = multi_in[ii].opt d["input"]["pcm"] = multi_in[ii].pcm d["input"]["solvent"] = multi_in[ii].solvent d["task"] = {"type": key, "name": "calc" + str(ii)} to_return.append(d) return to_return
def test_in_database_with_actual_database(self): db_file=os.path.join(db_dir, "db.json") dir2620=os.path.join(module_dir, "..", "..", "test_files", "2620_complete") mol2620=QCOutput(os.path.join(dir2620,"mol.qout.opt_0")).data["initial_molecule"] parse_firetask = QChemToDb(calc_dir=dir2620, db_file=db_file) parse_firetask.run_task({}) with patch("atomate.qchem.firetasks.fragmenter.FWAction" ) as FWAction_patch: ft = FragmentMolecule(molecule=self.neg_tfsi, db_file=db_file, depth=0, additional_charges=[-2,1], do_triplets=False) ft.run_task({}) self.assertEqual(ft.check_db,True) self.assertEqual(ft.charges,[-1,0,-2,1]) self.assertEqual( len(FWAction_patch.call_args[1]["additions"]), 623) self.assertEqual(ft._in_database(mol2620),True) mol2620.set_charge_and_spin(charge=0) self.assertEqual(ft._in_database(mol2620),False)
def setUp(self, lpad=False): os.chdir(os.path.join(module_dir, "..", "..", "test_files", "critic_test_files", "small_critic_example")) out_file = "mol.qout" qc_out = QCOutput(filename=out_file) self.mol = qc_out.data["initial_molecule"] self.cube_file = "dens.0.cube" super(TestRunCritic2, self).setUp(lpad=False)
def generate_single_job_dict(): """ Used to generate test dictionary for single jobs. """ single_job_dict = {} for file in single_job_out_names: single_job_dict[file] = QCOutput(os.path.join(test_dir, file)).data dumpfn(single_job_dict, "single_job.json")
def generate_single_job_dict(): """ Used to generate test dictionary for single jobs. """ single_job_dict = {} for file in single_job_out_names: single_job_dict[file] = QCOutput(os.path.join(PymatgenTest.TEST_FILES_DIR, "molecules", file)).data dumpfn(single_job_dict, "single_job.json")
def test_NBO7_parsing(self): data = QCOutput(os.path.join(PymatgenTest.TEST_FILES_DIR, "molecules", "new_qchem_files", "nbo7_1.qout")).data self.assertEqual(data["nbo_data"]["perturbation_energy"][0]["perturbation energy"][9], 15.73) self.assertEqual(len(data["nbo_data"]["perturbation_energy"][0]["donor bond index"].keys()), 84) self.assertEqual(len(data["nbo_data"]["perturbation_energy"][1]["donor bond index"].keys()), 29) data = QCOutput(os.path.join(PymatgenTest.TEST_FILES_DIR, "molecules", "new_qchem_files", "nbo7_2.qout")).data self.assertEqual(data["nbo_data"]["perturbation_energy"][0]["perturbation energy"][13], 32.93) self.assertEqual(data["nbo_data"]["perturbation_energy"][0]["acceptor type"][13], "LV") self.assertEqual(data["nbo_data"]["perturbation_energy"][0]["acceptor type"][12], "RY") self.assertEqual(data["nbo_data"]["perturbation_energy"][0]["acceptor atom 1 symbol"][12], "Mg") data = QCOutput(os.path.join(PymatgenTest.TEST_FILES_DIR, "molecules", "new_qchem_files", "nbo7_3.qout")).data self.assertEqual(data["nbo_data"]["perturbation_energy"][0]["perturbation energy"][13], 34.54) self.assertEqual(data["nbo_data"]["perturbation_energy"][0]["acceptor type"][13], "BD*") self.assertEqual(data["nbo_data"]["perturbation_energy"][0]["acceptor atom 1 symbol"][13], "B") self.assertEqual(data["nbo_data"]["perturbation_energy"][0]["acceptor atom 2 symbol"][13], "Mg") self.assertEqual(data["nbo_data"]["perturbation_energy"][0]["acceptor atom 2 number"][13], 3)
def setUp(self, lpad=False): os.chdir(os.path.join(module_dir, "..", "..", "test_files", "critic_test_files", "critic_example")) out_file = "mol.qout" qc_out = QCOutput(filename=out_file) self.mol = qc_out.data["initial_molecule"] self.cube_file = "dens.0.cube.gz" shutil.move("bonding.json","bonding_correct.json") shutil.move("processed_critic2.json","processed_correct.json") super(TestProcessCritic2, self).setUp(lpad=False)
def test_RunQChemFake(self): os.chdir(os.path.join(module_dir, "..", "fake_run")) firetask = RunQChemFake(ref_dir=os.path.join(module_dir, "..", "..", "test_files", "real_run")) firetask.run_task(fw_spec={}) ref_out = QCOutput( os.path.join(module_dir, "..", "..", "test_files", "real_run", "mol.qout")).data this_out = QCOutput("mol.qout").data for key in ref_out: try: self.assertEqual(ref_out[key], this_out[key]) except ValueError: np.testing.assert_array_equal(ref_out[key], this_out[key]) for filename in os.listdir( os.path.join(module_dir, "..", "..", "test_files", "real_run")): self.assertEqual(os.path.isfile(filename), True) os.chdir(module_dir)
def test_NBO_parsing(self): data = QCOutput(os.path.join(PymatgenTest.TEST_FILES_DIR, "molecules", "new_qchem_files", "nbo.qout")).data self.assertEqual(len(data["nbo_data"]["natural_populations"]), 3) self.assertEqual(len(data["nbo_data"]["hybridization_character"]), 4) self.assertEqual(len(data["nbo_data"]["perturbation_energy"]), 2) self.assertEqual(data["nbo_data"]["natural_populations"][0]["Density"][5], -0.08624) self.assertEqual(data["nbo_data"]["hybridization_character"][-1]["atom 2 pol coeff"][35], "-0.7059") next_to_last = list(data["nbo_data"]["perturbation_energy"][-1]["fock matrix element"].keys())[-2] self.assertEqual(data["nbo_data"]["perturbation_energy"][-1]["fock matrix element"][next_to_last], 0.071) self.assertEqual(data["nbo_data"]["perturbation_energy"][0]["acceptor type"][0], "RY*")
def check(self): # Checks output file for errors. self.outdata = QCOutput(self.output_file).data self.errors = self.outdata.get("errors") # If we aren't out of optimization cycles, but we were in the past, reset the history if "out_of_opt_cycles" not in self.errors and len(self.opt_error_history) > 0: self.opt_error_history = [] # If we're out of optimization cycles and we have unconnected fragments, no need to handle any errors if "out_of_opt_cycles" in self.errors and self.outdata["structure_change"] == "unconnected_fragments": return False return len(self.errors) > 0
def generate_multi_job_dict(): """ Used to generate test dictionary for multiple jobs. """ multi_job_dict = {} for file in multi_job_out_names: outputs = QCOutput.multiple_outputs_from_file( QCOutput, os.path.join(test_dir, file), keep_sub_files=False) data = [] for sub_output in outputs: data.append(sub_output.data) multi_job_dict[file] = data dumpfn(multi_job_dict, "multi_job.json")
def _test_property(self, key): for file in single_job_out_names: try: self.assertEqual( QCOutput(os.path.join(test_dir, file)).data.get(key), single_job_dict[file].get(key)) except ValueError: self.assertArrayEqual( QCOutput(os.path.join(test_dir, file)).data.get(key), single_job_dict[file].get(key)) for file in multi_job_out_names: outputs = QCOutput.multiple_outputs_from_file(QCOutput, os.path.join( test_dir, file), keep_sub_files=False) for ii, sub_output in enumerate(outputs): try: self.assertEqual(sub_output.data.get(key), multi_job_dict[file][ii].get(key)) except ValueError: self.assertArrayEqual(sub_output.data.get(key), multi_job_dict[file][ii].get(key))
def test_all(self): single_outs = dict() for file in single_job_out_names: single_outs[file] = QCOutput(os.path.join(test_dir, file)).data multi_outs = dict() for file in multi_job_out_names: multi_outs[file] = QCOutput.multiple_outputs_from_file(QCOutput, os.path.join(test_dir, file), keep_sub_files=False) for key in property_list: print('Testing ', key) self._test_property(key, single_outs, multi_outs)
def process_qchemrun(dir_name, taskname, input_file, output_file): """ Process a QChem calculation, aka an input/output pair. """ qchem_input_file = os.path.join(dir_name, input_file) qchem_output_file = os.path.join(dir_name, output_file) d = QCOutput(qchem_output_file).data temp_input = QCInput.from_file(qchem_input_file) d["input"] = {} d["input"]["molecule"] = temp_input.molecule d["input"]["rem"] = temp_input.rem d["input"]["opt"] = temp_input.opt d["input"]["pcm"] = temp_input.pcm d["input"]["solvent"] = temp_input.solvent d["task"] = {"type": taskname, "name": taskname} return d
class QChemSCFErrorHandler(ErrorHandler): """ QChem ErrorHandler class that addresses SCF non-convergence. """ is_monitor = False def __init__( self, input_file="mol.qin", output_file="mol.qout", rca_gdm_thresh=1.0e-3, scf_max_cycles=200, ): """ Initializes the error handler from a set of input and output files. Args: input_file (str): Name of the QChem input file. output_file (str): Name of the QChem output file. rca_gdm_thresh (float): The threshold for the prior scf algorithm. If last deltaE is larger than the threshold try RCA_DIIS first, else, try DIIS_GDM first. scf_max_cycles (int): The max iterations to set to fix SCF failure. """ self.input_file = input_file self.output_file = output_file self.scf_max_cycles = scf_max_cycles self.qcinp = QCInput.from_file(self.input_file) self.outdata = None self.errors = None def check(self): """ Checks output file for errors """ self.outdata = QCOutput(self.output_file).data self.errors = self.outdata.get("errors") return len(self.errors) > 0 def correct(self): """ Corrects errors, but it hasn't been implemented yet """ print("This hasn't been implemented yet!") return {"errors": self.errors, "actions": None}
def _test_property(self, key): for file in single_job_out_names: try: self.assertEqual(QCOutput(os.path.join(test_dir, file)).data.get( key), single_job_dict[file].get(key)) except ValueError: self.assertArrayEqual(QCOutput(os.path.join(test_dir, file)).data.get( key), single_job_dict[file].get(key)) for file in multi_job_out_names: outputs = QCOutput.multiple_outputs_from_file( QCOutput, os.path.join(test_dir, file), keep_sub_files=False) for ii, sub_output in enumerate(outputs): try: self.assertEqual(sub_output.data.get( key), multi_job_dict[file][ii].get(key)) except ValueError: self.assertArrayEqual(sub_output.data.get( key), multi_job_dict[file][ii].get(key))
class QChemSCFErrorHandler(ErrorHandler): """ QChem ErrorHandler class that addresses SCF non-convergence. """ is_monitor = False def __init__(self, input_file="mol.qin", output_file="mol.qout", rca_gdm_thresh=1.0E-3, scf_max_cycles=200): """ Initializes the error handler from a set of input and output files. Args: input_file (str): Name of the QChem input file. output_file (str): Name of the QChem output file. rca_gdm_thresh (float): The threshold for the prior scf algorithm. If last deltaE is larger than the threshold try RCA_DIIS first, else, try DIIS_GDM first. scf_max_cycles (int): The max iterations to set to fix SCF failure. """ self.input_file = input_file self.output_file = output_file self.scf_max_cycles = scf_max_cycles self.geom_max_cycles = geom_max_cycles self.qcinp = QCInput.from_file(self.input_file) self.outdata = None self.errors = None self.qchem_job = qchem_job def check(self): # Checks output file for errors. self.outdata = QCOutput(self.output_file).data self.errors = self.outdata.get("errors") return len(self.errors) > 0 def correct(self): print("This hasn't been implemented yet!") return {"errors": self.errors, "actions": None}
def test_NBO7_infinite_e2pert(self): data = QCOutput(os.path.join(PymatgenTest.TEST_FILES_DIR, "molecules", "new_qchem_files", "nbo7_inf.qout")).data self.assertEqual(data["nbo_data"]["perturbation_energy"][0]["perturbation energy"][0], float("inf"))
def check(self): # Checks output file for errors. self.outdata = QCOutput(self.output_file).data self.errors = self.outdata.get("errors") return len(self.errors) > 0
def opt_with_frequency_flattener(cls, qchem_command, multimode="openmp", input_file="mol.qin", output_file="mol.qout", qclog_file="mol.qclog", max_iterations=10, max_molecule_perturb_scale=0.3, check_connectivity=True, **QCJob_kwargs): """ Optimize a structure and calculate vibrational frequencies to check if the structure is in a true minima. If a frequency is negative, iteratively perturbe the geometry, optimize, and recalculate frequencies until all are positive, aka a true minima has been found. Args: qchem_command (str): Command to run QChem. multimode (str): Parallelization scheme, either openmp or mpi. input_file (str): Name of the QChem input file. output_file (str): Name of the QChem output file. max_iterations (int): Number of perturbation -> optimization -> frequency iterations to perform. Defaults to 10. max_molecule_perturb_scale (float): The maximum scaled perturbation that can be applied to the molecule. Defaults to 0.3. check_connectivity (bool): Whether to check differences in connectivity introduced by structural perturbation. Defaults to True. **QCJob_kwargs: Passthrough kwargs to QCJob. See :class:`custodian.qchem.jobs.QCJob`. """ min_molecule_perturb_scale = 0.1 scale_grid = 10 perturb_scale_grid = ( max_molecule_perturb_scale - min_molecule_perturb_scale ) / scale_grid if not os.path.exists(input_file): raise AssertionError('Input file must be present!') orig_opt_input = QCInput.from_file(input_file) orig_opt_rem = copy.deepcopy(orig_opt_input.rem) orig_freq_rem = copy.deepcopy(orig_opt_input.rem) orig_freq_rem["job_type"] = "freq" first = True reversed_direction = False num_neg_freqs = [] for ii in range(max_iterations): yield (QCJob( qchem_command=qchem_command, multimode=multimode, input_file=input_file, output_file=output_file, qclog_file=qclog_file, suffix=".opt_" + str(ii), backup=first, **QCJob_kwargs)) first = False opt_outdata = QCOutput(output_file + ".opt_" + str(ii)).data if opt_outdata["structure_change"] == "unconnected_fragments" and not opt_outdata["completion"]: print("Unstable molecule broke into unconnected fragments which failed to optimize! Exiting...") break else: freq_QCInput = QCInput( molecule=opt_outdata.get("molecule_from_optimized_geometry"), rem=orig_freq_rem, opt=orig_opt_input.opt, pcm=orig_opt_input.pcm, solvent=orig_opt_input.solvent) freq_QCInput.write_file(input_file) yield (QCJob( qchem_command=qchem_command, multimode=multimode, input_file=input_file, output_file=output_file, qclog_file=qclog_file, suffix=".freq_" + str(ii), backup=first, **QCJob_kwargs)) outdata = QCOutput(output_file + ".freq_" + str(ii)).data errors = outdata.get("errors") if len(errors) != 0: raise AssertionError('No errors should be encountered while flattening frequencies!') if outdata.get('frequencies')[0] > 0.0: print("All frequencies positive!") break else: num_neg_freqs += [sum(1 for freq in outdata.get('frequencies') if freq < 0)] if len(num_neg_freqs) > 1: if num_neg_freqs[-1] == num_neg_freqs[-2] and not reversed_direction: reversed_direction = True elif num_neg_freqs[-1] == num_neg_freqs[-2] and reversed_direction: if len(num_neg_freqs) < 3: raise AssertionError("ERROR: This should only be possible after at least three frequency flattening iterations! Exiting...") else: raise Exception("ERROR: Reversing the perturbation direction still could not flatten any frequencies. Exiting...") elif num_neg_freqs[-1] != num_neg_freqs[-2] and reversed_direction: reversed_direction = False negative_freq_vecs = outdata.get("frequency_mode_vectors")[0] structure_successfully_perturbed = False for molecule_perturb_scale in np.arange( max_molecule_perturb_scale, min_molecule_perturb_scale, -perturb_scale_grid): new_coords = perturb_coordinates( old_coords=outdata.get("initial_geometry"), negative_freq_vecs=negative_freq_vecs, molecule_perturb_scale=molecule_perturb_scale, reversed_direction=reversed_direction) new_molecule = Molecule( species=outdata.get('species'), coords=new_coords, charge=outdata.get('charge'), spin_multiplicity=outdata.get('multiplicity')) if check_connectivity: old_molgraph = MoleculeGraph.with_local_env_strategy(outdata.get("initial_molecule"), OpenBabelNN(), reorder=False, extend_structure=False) new_molgraph = MoleculeGraph.with_local_env_strategy(new_molecule, OpenBabelNN(), reorder=False, extend_structure=False) if old_molgraph.isomorphic_to(new_molgraph): structure_successfully_perturbed = True break if not structure_successfully_perturbed: raise Exception( "ERROR: Unable to perturb coordinates to remove negative frequency without changing the connectivity! Exiting..." ) new_opt_QCInput = QCInput( molecule=new_molecule, rem=orig_opt_rem, opt=orig_opt_input.opt, pcm=orig_opt_input.pcm, solvent=orig_opt_input.solvent) new_opt_QCInput.write_file(input_file)
class QChemErrorHandler(ErrorHandler): """ Master QChemErrorHandler class that handles a number of common errors that occur during QChem runs. """ is_monitor = False def __init__(self, input_file="mol.qin", output_file="mol.qout", scf_max_cycles=200, geom_max_cycles=200): """ Initializes the error handler from a set of input and output files. Args: input_file (str): Name of the QChem input file. output_file (str): Name of the QChem output file. scf_max_cycles (int): The max iterations to set to fix SCF failure. geom_max_cycles (int): The max iterations to set to fix geometry optimization failure. """ self.input_file = input_file self.output_file = output_file self.scf_max_cycles = scf_max_cycles self.geom_max_cycles = geom_max_cycles self.outdata = None self.errors = [] self.opt_error_history = [] def check(self): # Checks output file for errors. self.outdata = QCOutput(self.output_file).data self.errors = self.outdata.get("errors") # If we aren't out of optimization cycles, but we were in the past, reset the history if "out_of_opt_cycles" not in self.errors and len(self.opt_error_history) > 0: self.opt_error_history = [] # If we're out of optimization cycles and we have unconnected fragments, no need to handle any errors if "out_of_opt_cycles" in self.errors and self.outdata["structure_change"] == "unconnected_fragments": return False return len(self.errors) > 0 def correct(self): backup({self.input_file, self.output_file}) actions = [] self.qcinp = QCInput.from_file(self.input_file) if "SCF_failed_to_converge" in self.errors: # Check number of SCF cycles. If not set or less than scf_max_cycles, # increase to that value and rerun. If already set, check if # scf_algorithm is unset or set to DIIS, in which case set to GDM. # Otherwise, tell user to call SCF error handler and do nothing. if str(self.qcinp.rem.get("max_scf_cycles")) != str( self.scf_max_cycles): self.qcinp.rem["max_scf_cycles"] = self.scf_max_cycles actions.append({"max_scf_cycles": self.scf_max_cycles}) elif self.qcinp.rem.get("scf_algorithm", "diis").lower() == "diis": self.qcinp.rem["scf_algorithm"] = "gdm" actions.append({"scf_algorithm": "gdm"}) elif self.qcinp.rem.get("scf_algorithm", "gdm").lower() == "gdm": self.qcinp.rem["scf_algorithm"] = "diis_gdm" actions.append({"scf_algorithm": "diis_gdm"}) else: print( "More advanced changes may impact the SCF result. Use the SCF error handler" ) elif "out_of_opt_cycles" in self.errors: # Check number of opt cycles. If less than geom_max_cycles, increase # to that value, set last geom as new starting geom and rerun. if str(self.qcinp.rem.get( "geom_opt_max_cycles")) != str(self.geom_max_cycles): self.qcinp.rem["geom_opt_max_cycles"] = self.geom_max_cycles actions.append({"geom_max_cycles:": self.scf_max_cycles}) if len(self.outdata.get("energy_trajectory")) > 1: self.qcinp.molecule = self.outdata.get( "molecule_from_last_geometry") actions.append({"molecule": "molecule_from_last_geometry"}) # If already at geom_max_cycles, often can just get convergence by restarting # from the geometry of the last cycle. But we'll also save any structural # changes that happened along the way. else: self.opt_error_history += [self.outdata["structure_change"]] if len(self.opt_error_history) > 1: if self.opt_error_history[-1] == "no_change": # If no structural changes occured in two consecutive optimizations, # and we still haven't converged, then just exit. return {"errors": self.errors, "actions": None, "opt_error_history": self.opt_error_history} self.qcinp.molecule = self.outdata.get("molecule_from_last_geometry") actions.append({"molecule": "molecule_from_last_geometry"}) elif "unable_to_determine_lamda" in self.errors: # Set last geom as new starting geom and rerun. If no opt cycles, # use diff SCF strat? Diff initial guess? Change basis? if len(self.outdata.get("energy_trajectory")) > 1: self.qcinp.molecule = self.outdata.get( "molecule_from_last_geometry") actions.append({"molecule": "molecule_from_last_geometry"}) elif self.qcinp.rem.get("scf_algorithm", "diis").lower() == "diis": self.qcinp.rem["scf_algorithm"] = "rca_diis" actions.append({"scf_algorithm": "rca_diis"}) if self.qcinp.rem.get("gen_scfman"): self.qcinp.rem["gen_scfman"] = False actions.append({"gen_scfman": False}) else: print( "Use a different initial guess? Perhaps a different basis?" ) elif "linear_dependent_basis" in self.errors: # DIIS -> RCA_DIIS. If already RCA_DIIS, change basis? if self.qcinp.rem.get("scf_algorithm", "diis").lower() == "diis": self.qcinp.rem["scf_algorithm"] = "rca_diis" actions.append({"scf_algorithm": "rca_diis"}) if self.qcinp.rem.get("gen_scfman"): self.qcinp.rem["gen_scfman"] = False actions.append({"gen_scfman": False}) else: print("Perhaps use a better basis?") elif "failed_to_transform_coords" in self.errors: # Check for symmetry flag in rem. If not False, set to False and rerun. # If already False, increase threshold? if not self.qcinp.rem.get("sym_ignore") or self.qcinp.rem.get( "symmetry"): self.qcinp.rem["sym_ignore"] = True self.qcinp.rem["symmetry"] = False actions.append({"sym_ignore": True}) actions.append({"symmetry": False}) else: print("Perhaps increase the threshold?") elif "input_file_error" in self.errors: print( "Something is wrong with the input file. Examine error message by hand." ) return {"errors": self.errors, "actions": None} elif "failed_to_read_input" in self.errors: # Almost certainly just a temporary problem that will not be encountered again. Rerun job as-is. actions.append({"rerun job as-is"}) elif "IO_error" in self.errors: # Almost certainly just a temporary problem that will not be encountered again. Rerun job as-is. actions.append({"rerun job as-is"}) elif "read_molecule_error" in self.errors: # Almost certainly just a temporary problem that will not be encountered again. Rerun job as-is. actions.append({"rerun job as-is"}) elif "never_called_qchem" in self.errors: # Almost certainly just a temporary problem that will not be encountered again. Rerun job as-is. actions.append({"rerun job as-is"}) elif "unknown_error" in self.errors: print("Examine error message by hand.") return {"errors": self.errors, "actions": None} else: # You should never get here. If correct is being called then errors should have at least one entry, # in which case it should have been caught by the if/elifs above. print( "If you get this message, something has gone terribly wrong!") return {"errors": self.errors, "actions": None} os.rename(self.input_file, self.input_file + ".last") self.qcinp.write_file(self.input_file) return {"errors": self.errors, "actions": actions}
class QChemErrorHandler(ErrorHandler): """ Master QChemErrorHandler class that handles a number of common errors that occur during QChem runs. """ is_monitor = False def __init__( self, input_file="mol.qin", output_file="mol.qout", scf_max_cycles=200, geom_max_cycles=200, ): """ Initializes the error handler from a set of input and output files. Args: input_file (str): Name of the QChem input file. output_file (str): Name of the QChem output file. scf_max_cycles (int): The max iterations to set to fix SCF failure. geom_max_cycles (int): The max iterations to set to fix geometry optimization failure. """ self.input_file = input_file self.output_file = output_file self.scf_max_cycles = scf_max_cycles self.geom_max_cycles = geom_max_cycles self.outdata = None self.errors = [] self.opt_error_history = [] def check(self): """ Checks output file for errors """ self.outdata = QCOutput(self.output_file).data self.errors = self.outdata.get("errors") self.warnings = self.outdata.get("warnings") # If we aren't out of optimization cycles, but we were in the past, reset the history if "out_of_opt_cycles" not in self.errors and len( self.opt_error_history) > 0: self.opt_error_history = [] # If we're out of optimization cycles and we have unconnected fragments, no need to handle any errors if "out_of_opt_cycles" in self.errors and self.outdata[ "structure_change"] == "unconnected_fragments": return False return len(self.errors) > 0 def correct(self): """ Perform corrections """ backup({self.input_file, self.output_file}) actions = [] self.qcinp = QCInput.from_file(self.input_file) if "SCF_failed_to_converge" in self.errors: # Check number of SCF cycles. If not set or less than scf_max_cycles, # increase to that value and rerun. If already set, check if # scf_algorithm is unset or set to DIIS, in which case set to GDM. # Otherwise, tell user to call SCF error handler and do nothing. if str(self.qcinp.rem.get("max_scf_cycles")) != str( self.scf_max_cycles): self.qcinp.rem["max_scf_cycles"] = self.scf_max_cycles actions.append({"max_scf_cycles": self.scf_max_cycles}) elif self.qcinp.rem.get("thresh", "10") != "14": self.qcinp.rem["thresh"] = "14" actions.append({"thresh": "14"}) elif self.qcinp.rem.get("scf_algorithm", "diis").lower() == "diis": self.qcinp.rem["scf_algorithm"] = "diis_gdm" actions.append({"scf_algorithm": "diis_gdm"}) elif self.qcinp.rem.get("scf_algorithm", "diis").lower() == "diis_gdm": self.qcinp.rem["scf_algorithm"] = "gdm" actions.append({"scf_algorithm": "gdm"}) elif self.qcinp.rem.get("scf_guess_always", "none").lower() != "true": self.qcinp.rem["scf_guess_always"] = True actions.append({"scf_guess_always": True}) else: print( "More advanced changes may impact the SCF result. Use the SCF error handler" ) elif "out_of_opt_cycles" in self.errors: # Check number of opt cycles. If less than geom_max_cycles, increase # to that value, set last geom as new starting geom and rerun. if str(self.qcinp.rem.get("geom_opt_max_cycles")) != str( self.geom_max_cycles): self.qcinp.rem["geom_opt_max_cycles"] = self.geom_max_cycles actions.append({"geom_max_cycles:": self.scf_max_cycles}) if len(self.outdata.get("energy_trajectory")) > 1: self.qcinp.molecule = self.outdata.get( "molecule_from_last_geometry") actions.append({"molecule": "molecule_from_last_geometry"}) elif self.qcinp.rem.get("thresh", "10") != "14": self.qcinp.rem["thresh"] = "14" actions.append({"thresh": "14"}) # Will need to try and implement this dmax handler below when I have more time # to fix the tests and the general handling procedure. # elif self.qcinp.rem.get("geom_opt_dmax",300) != 150: # self.qcinp.rem["geom_opt_dmax"] = 150 # actions.append({"geom_opt_dmax": "150"}) # If already at geom_max_cycles, thresh 14, and dmax 150, often can just get convergence # by restarting from the geometry of the last cycle. But we'll also save any structural # changes that happened along the way. else: self.opt_error_history += [self.outdata["structure_change"]] if len(self.opt_error_history) > 1: if self.opt_error_history[-1] == "no_change": # If no structural changes occurred in two consecutive optimizations, # and we still haven't converged, then just exit. return { "errors": self.errors, "actions": None, "opt_error_history": self.opt_error_history, } self.qcinp.molecule = self.outdata.get( "molecule_from_last_geometry") actions.append({"molecule": "molecule_from_last_geometry"}) elif "unable_to_determine_lamda" in self.errors: # Set last geom as new starting geom and rerun. If no opt cycles, # use diff SCF start? Diff initial guess? Change basis? Unclear. if len(self.outdata.get("energy_trajectory")) > 1: self.qcinp.molecule = self.outdata.get( "molecule_from_last_geometry") actions.append({"molecule": "molecule_from_last_geometry"}) elif self.qcinp.rem.get("thresh", "10") != "14": self.qcinp.rem["thresh"] = "14" actions.append({"thresh": "14"}) else: print( "Use a different initial guess? Perhaps a different basis?" ) elif "premature_end_FileMan_error" in self.errors: if self.qcinp.rem.get("thresh", "10") != "14": self.qcinp.rem["thresh"] = "14" actions.append({"thresh": "14"}) elif self.qcinp.rem.get("scf_guess_always", "none").lower() != "true": self.qcinp.rem["scf_guess_always"] = True actions.append({"scf_guess_always": True}) else: print( "We're in a bad spot if we get a FileMan error while always generating a new SCF guess..." ) elif "hessian_eigenvalue_error" in self.errors: if self.qcinp.rem.get("thresh", "10") != "14": self.qcinp.rem["thresh"] = "14" actions.append({"thresh": "14"}) else: print( "Not sure how to fix hessian_eigenvalue_error if thresh is already 14!" ) elif "NLebdevPts" in self.errors: # this error should only be possible if resp_charges or esp_charges is set if self.qcinp.rem.get("resp_charges") or self.qcinp.rem.get( "esp_charges"): # This error is caused by insufficient no. of Lebedev points on # the grid used to compute RESP charges # Increase the density of points on the Lebedev grid using the # esp_surface_density argument (see manual >= v5.4) # the default value is 500 (=0.001 Angstrom) # or disable RESP charges as a last resort if int(self.qcinp.rem.get("esp_surface_density", 500)) >= 500: self.qcinp.rem["esp_surface_density"] = "250" actions.append({"esp_surface_density": "250"}) elif int(self.qcinp.rem.get("esp_surface_density", 250)) >= 250: self.qcinp.rem["esp_surface_density"] = "125" actions.append({"esp_surface_density": "125"}) elif int(self.qcinp.rem.get("esp_surface_density", 125)) >= 125: # switch from Lebedev mode to spherical harmonics mode if self.qcinp.rem.get("resp_charges"): self.qcinp.rem["resp_charges"] = "2" actions.append({"resp_charges": "2"}) if self.qcinp.rem.get("esp_charges"): self.qcinp.rem["esp_charges"] = "2" actions.append({"esp_charges": "2"}) else: if self.qcinp.rem.get("resp_charges"): self.qcinp.rem["resp_charges"] = "false" actions.append({"resp_charges": "false"}) if self.qcinp.rem.get("esp_charges"): self.qcinp.rem["esp_charges"] = "false" actions.append({"esp_charges": "false"}) else: print( "Not sure how to fix NLebdevPts error if resp_charges is disabled!" ) elif "failed_to_transform_coords" in self.errors: # Check for symmetry flag in rem. If not False, set to False and rerun. # If already False, increase threshold? if not self.qcinp.rem.get("sym_ignore") or self.qcinp.rem.get( "symmetry"): self.qcinp.rem["sym_ignore"] = True self.qcinp.rem["symmetry"] = False actions.append({"sym_ignore": True}) actions.append({"symmetry": False}) else: print("Perhaps increase the threshold?") elif "basis_not_supported" in self.errors: print( "Specify a different basis set. At least one of the atoms is not supported." ) return {"errors": self.errors, "actions": None} elif "input_file_error" in self.errors: print( "Something is wrong with the input file. Examine error message by hand." ) return {"errors": self.errors, "actions": None} elif "failed_to_read_input" in self.errors: # Almost certainly just a temporary problem that will not be encountered again. Rerun job as-is. actions.append({"rerun_job_no_changes": True}) elif "read_molecule_error" in self.errors: # Almost certainly just a temporary problem that will not be encountered again. Rerun job as-is. actions.append({"rerun_job_no_changes": True}) elif "never_called_qchem" in self.errors: # Almost certainly just a temporary problem that will not be encountered again. Rerun job as-is. actions.append({"rerun_job_no_changes": True}) elif "licensing_error" in self.errors: # Almost certainly just a temporary problem that will not be encountered again. Rerun job as-is. actions.append({"rerun_job_no_changes": True}) elif "unknown_error" in self.errors: if self.qcinp.rem.get("scf_guess", "none").lower() == "read": del self.qcinp.rem["scf_guess"] actions.append({"scf_guess": "deleted"}) elif self.qcinp.rem.get("thresh", "10") != "14": self.qcinp.rem["thresh"] = "14" actions.append({"thresh": "14"}) else: print("Unknown error. Examine output and log files by hand.") return {"errors": self.errors, "actions": None} else: # You should never get here. If correct is being called then errors should have at least one entry, # in which case it should have been caught by the if/elifs above. print("Errors:", self.errors) print( "Must have gotten an error which is correctly parsed but not included in the handler. FIX!!!" ) return {"errors": self.errors, "actions": None} if { "molecule": "molecule_from_last_geometry" } in actions and str( self.qcinp.rem.get("geom_opt_hessian")).lower() == "read": del self.qcinp.rem["geom_opt_hessian"] actions.append({"geom_opt_hessian": "deleted"}) os.rename(self.input_file, self.input_file + ".last") self.qcinp.write_file(self.input_file) return { "errors": self.errors, "warnings": self.warnings, "actions": actions }
def opt_with_frequency_flattener( cls, qchem_command, multimode="openmp", input_file="mol.qin", output_file="mol.qout", qclog_file="mol.qclog", max_iterations=10, max_molecule_perturb_scale=0.3, check_connectivity=True, linked=True, transition_state=False, freq_before_opt=False, save_final_scratch=False, **QCJob_kwargs, ): """ Optimize a structure and calculate vibrational frequencies to check if the structure is in a true minima. If there are an inappropriate number of imaginary frequencies (>0 for a minimum-energy structure, >1 for a transition-state), attempt to re-calculate using one of two methods: - Perturb the geometry based on the imaginary frequencies and re-optimize - Use the exact Hessian to inform a subsequent optimization After each geometry optimization, the frequencies are re-calculated to determine if a true minimum (or transition-state) has been found. Note: Very small imaginary frequencies (-15cm^-1 < nu < 0) are allowed if there is only one more than there should be. In other words, if there is one very small imaginary frequency, it is still treated as a minimum, and if there is one significant imaginary frequency and one very small imaginary frequency, it is still treated as a transition-state. Args: qchem_command (str): Command to run QChem. multimode (str): Parallelization scheme, either openmp or mpi. input_file (str): Name of the QChem input file. output_file (str): Name of the QChem output file. max_iterations (int): Number of perturbation -> optimization -> frequency iterations to perform. Defaults to 10. max_molecule_perturb_scale (float): The maximum scaled perturbation that can be applied to the molecule. Defaults to 0.3. check_connectivity (bool): Whether to check differences in connectivity introduced by structural perturbation. Defaults to True. linked (bool): Whether or not to use the linked flattener. If set to True (default), then the explicit Hessians from a vibrational frequency analysis will be used as the initial Hessian of subsequent optimizations. In many cases, this can significantly improve optimization efficiency. transition_state (bool): If True (default False), use a ts optimization (search for a saddle point instead of a minimum) freq_before_opt (bool): If True (default False), run a frequency calculation before any opt/ts searches to improve understanding of the local potential energy surface. save_final_scratch (bool): Whether to save full scratch directory contents at the end of the flattening. Defaults to False. **QCJob_kwargs: Passthrough kwargs to QCJob. See :class:`custodian.qchem.jobs.QCJob`. """ if not os.path.exists(input_file): raise AssertionError("Input file must be present!") if transition_state: opt_method = "ts" perturb_index = 1 else: opt_method = "opt" perturb_index = 0 energy_diff_cutoff = 0.0000001 orig_input = QCInput.from_file(input_file) freq_rem = copy.deepcopy(orig_input.rem) freq_rem["job_type"] = "freq" opt_rem = copy.deepcopy(orig_input.rem) opt_rem["job_type"] = opt_method first = True energy_history = list() if freq_before_opt: if not linked: warnings.warn( "WARNING: This first frequency calculation will not inform subsequent optimization!" ) yield (QCJob( qchem_command=qchem_command, multimode=multimode, input_file=input_file, output_file=output_file, qclog_file=qclog_file, suffix=".freq_pre", save_scratch=True, backup=first, **QCJob_kwargs, )) if linked: opt_rem["geom_opt_hessian"] = "read" opt_rem["scf_guess_always"] = True opt_QCInput = QCInput( molecule=orig_input.molecule, rem=opt_rem, opt=orig_input.opt, pcm=orig_input.pcm, solvent=orig_input.solvent, smx=orig_input.smx, ) opt_QCInput.write_file(input_file) first = False if linked: opt_rem["geom_opt_hessian"] = "read" opt_rem["scf_guess_always"] = True for ii in range(max_iterations): yield (QCJob( qchem_command=qchem_command, multimode=multimode, input_file=input_file, output_file=output_file, qclog_file=qclog_file, suffix=".{}_".format(opt_method) + str(ii), save_scratch=True, backup=first, **QCJob_kwargs, )) opt_outdata = QCOutput(output_file + ".{}_".format(opt_method) + str(ii)).data opt_indata = QCInput.from_file(input_file + ".{}_".format(opt_method) + str(ii)) if opt_indata.rem["scf_algorithm"] != freq_rem["scf_algorithm"]: freq_rem["scf_algorithm"] = opt_indata.rem["scf_algorithm"] opt_rem["scf_algorithm"] = opt_indata.rem["scf_algorithm"] first = False if opt_outdata[ "structure_change"] == "unconnected_fragments" and not opt_outdata[ "completion"]: if not transition_state: warnings.warn( "Unstable molecule broke into unconnected fragments which failed to optimize! Exiting..." ) break energy_history.append(opt_outdata.get("final_energy")) freq_QCInput = QCInput( molecule=opt_outdata.get( "molecule_from_optimized_geometry"), rem=freq_rem, opt=orig_input.opt, pcm=orig_input.pcm, solvent=orig_input.solvent, smx=orig_input.smx, ) freq_QCInput.write_file(input_file) yield (QCJob( qchem_command=qchem_command, multimode=multimode, input_file=input_file, output_file=output_file, qclog_file=qclog_file, suffix=".freq_" + str(ii), save_scratch=True, backup=first, **QCJob_kwargs, )) outdata = QCOutput(output_file + ".freq_" + str(ii)).data indata = QCInput.from_file(input_file + ".freq_" + str(ii)) if indata.rem["scf_algorithm"] != freq_rem["scf_algorithm"]: freq_rem["scf_algorithm"] = indata.rem["scf_algorithm"] opt_rem["scf_algorithm"] = indata.rem["scf_algorithm"] errors = outdata.get("errors") if len(errors) != 0: raise AssertionError( "No errors should be encountered while flattening frequencies!" ) if not transition_state: freq_0 = outdata.get("frequencies")[0] freq_1 = outdata.get("frequencies")[1] if freq_0 > 0.0: warnings.warn("All frequencies positive!") break if abs(freq_0) < 15.0 and freq_1 > 0.0: warnings.warn( "One negative frequency smaller than 15.0 - not worth further flattening!" ) break if len(energy_history) > 1: if abs(energy_history[-1] - energy_history[-2]) < energy_diff_cutoff: warnings.warn("Energy change below cutoff!") break opt_QCInput = QCInput( molecule=opt_outdata.get( "molecule_from_optimized_geometry"), rem=opt_rem, opt=orig_input.opt, pcm=orig_input.pcm, solvent=orig_input.solvent, smx=orig_input.smx, ) opt_QCInput.write_file(input_file) else: freq_0 = outdata.get("frequencies")[0] freq_1 = outdata.get("frequencies")[1] freq_2 = outdata.get("frequencies")[2] if freq_0 < 0.0 < freq_1: warnings.warn("Saddle point found!") break if abs(freq_1) < 15.0 and freq_2 > 0.0: warnings.warn( "Second small imaginary frequency (smaller than 15.0) - not worth further flattening!" ) break opt_QCInput = QCInput( molecule=opt_outdata.get( "molecule_from_optimized_geometry"), rem=opt_rem, opt=orig_input.opt, pcm=orig_input.pcm, solvent=orig_input.solvent, smx=orig_input.smx, ) opt_QCInput.write_file(input_file) if not save_final_scratch: shutil.rmtree(os.path.join(os.getcwd(), "scratch")) else: orig_opt_input = QCInput.from_file(input_file) history = list() for ii in range(max_iterations): yield (QCJob( qchem_command=qchem_command, multimode=multimode, input_file=input_file, output_file=output_file, qclog_file=qclog_file, suffix=".{}_".format(opt_method) + str(ii), backup=first, **QCJob_kwargs, )) opt_outdata = QCOutput(output_file + ".{}_".format(opt_method) + str(ii)).data if first: orig_species = copy.deepcopy(opt_outdata.get("species")) orig_charge = copy.deepcopy(opt_outdata.get("charge")) orig_multiplicity = copy.deepcopy( opt_outdata.get("multiplicity")) orig_energy = copy.deepcopy( opt_outdata.get("final_energy")) first = False if opt_outdata[ "structure_change"] == "unconnected_fragments" and not opt_outdata[ "completion"]: if not transition_state: warnings.warn( "Unstable molecule broke into unconnected fragments which failed to optimize! Exiting..." ) break freq_QCInput = QCInput( molecule=opt_outdata.get( "molecule_from_optimized_geometry"), rem=freq_rem, opt=orig_opt_input.opt, pcm=orig_opt_input.pcm, solvent=orig_opt_input.solvent, smx=orig_opt_input.smx, ) freq_QCInput.write_file(input_file) yield (QCJob( qchem_command=qchem_command, multimode=multimode, input_file=input_file, output_file=output_file, qclog_file=qclog_file, suffix=".freq_" + str(ii), backup=first, **QCJob_kwargs, )) outdata = QCOutput(output_file + ".freq_" + str(ii)).data errors = outdata.get("errors") if len(errors) != 0: raise AssertionError( "No errors should be encountered while flattening frequencies!" ) if not transition_state: freq_0 = outdata.get("frequencies")[0] freq_1 = outdata.get("frequencies")[1] if freq_0 > 0.0: warnings.warn("All frequencies positive!") if opt_outdata.get("final_energy") > orig_energy: warnings.warn( "WARNING: Energy increased during frequency flattening!" ) break if abs(freq_0) < 15.0 and freq_1 > 0.0: warnings.warn( "One negative frequency smaller than 15.0 - not worth further flattening!" ) break if len(energy_history) > 1: if abs(energy_history[-1] - energy_history[-2]) < energy_diff_cutoff: warnings.warn("Energy change below cutoff!") break else: freq_0 = outdata.get("frequencies")[0] freq_1 = outdata.get("frequencies")[1] freq_2 = outdata.get("frequencies")[2] if freq_0 < 0.0 < freq_1: warnings.warn("Saddle point found!") break if abs(freq_1) < 15.0 and freq_2 > 0.0: warnings.warn( "Second small imaginary frequency (smaller than 15.0) - not worth further flattening!" ) break hist = {} hist["molecule"] = copy.deepcopy( outdata.get("initial_molecule")) hist["geometry"] = copy.deepcopy( outdata.get("initial_geometry")) hist["frequencies"] = copy.deepcopy(outdata.get("frequencies")) hist["frequency_mode_vectors"] = copy.deepcopy( outdata.get("frequency_mode_vectors")) hist["num_neg_freqs"] = sum( 1 for freq in outdata.get("frequencies") if freq < 0) hist["energy"] = copy.deepcopy(opt_outdata.get("final_energy")) hist["index"] = len(history) hist["children"] = list() history.append(hist) ref_mol = history[-1]["molecule"] geom_to_perturb = history[-1]["geometry"] negative_freq_vecs = history[-1]["frequency_mode_vectors"][ perturb_index] reversed_direction = False standard = True # If we've found one or more negative frequencies in two consecutive iterations, let's dig in # deeper: if len(history) > 1: # Start by finding the latest iteration's parent: if history[-1]["index"] in history[-2]["children"]: parent_hist = history[-2] history[-1]["parent"] = parent_hist["index"] elif history[-1]["index"] in history[-3]["children"]: parent_hist = history[-3] history[-1]["parent"] = parent_hist["index"] else: raise AssertionError( "ERROR: your parent should always be one or two iterations behind you! Exiting..." ) # if the number of negative frequencies has remained constant or increased from parent to # child, if history[-1]["num_neg_freqs"] >= parent_hist[ "num_neg_freqs"]: # check to see if the parent only has one child, aka only the positive perturbation has # been tried, # in which case just try the negative perturbation from the same parent if len(parent_hist["children"]) == 1: ref_mol = parent_hist["molecule"] geom_to_perturb = parent_hist["geometry"] negative_freq_vecs = parent_hist[ "frequency_mode_vectors"][perturb_index] reversed_direction = True standard = False parent_hist["children"].append(len(history)) # If the parent has two children, aka both directions have been tried, then we have to # get creative: elif len(parent_hist["children"]) == 2: # If we're dealing with just one negative frequency, if parent_hist["num_neg_freqs"] == 1: if history[parent_hist["children"][0]][ "energy"] < history[-1]["energy"]: good_child = copy.deepcopy( history[parent_hist["children"][0]]) else: good_child = copy.deepcopy(history[-1]) if good_child["num_neg_freqs"] > 1: raise Exception( "ERROR: Child with lower energy has more negative frequencies! " "Exiting...") if good_child["energy"] < parent_hist["energy"]: make_good_child_next_parent = True elif (vector_list_diff( good_child["frequency_mode_vectors"] [perturb_index], parent_hist["frequency_mode_vectors"] [perturb_index], ) > 0.2): make_good_child_next_parent = True else: raise Exception( "ERROR: Good child not good enough! Exiting..." ) if make_good_child_next_parent: good_child["index"] = len(history) history.append(good_child) ref_mol = history[-1]["molecule"] geom_to_perturb = history[-1]["geometry"] negative_freq_vecs = history[-1][ "frequency_mode_vectors"][ perturb_index] else: raise Exception( "ERROR: Can't deal with multiple neg frequencies yet! Exiting..." ) else: raise AssertionError( "ERROR: Parent cannot have more than two childen! Exiting..." ) # Implicitly, if the number of negative frequencies decreased from parent to child, # continue normally. if standard: history[-1]["children"].append(len(history)) min_molecule_perturb_scale = 0.1 scale_grid = 10 perturb_scale_grid = (max_molecule_perturb_scale - min_molecule_perturb_scale) / scale_grid structure_successfully_perturbed = False for molecule_perturb_scale in np.arange( max_molecule_perturb_scale, min_molecule_perturb_scale, -perturb_scale_grid, ): new_coords = perturb_coordinates( old_coords=geom_to_perturb, negative_freq_vecs=negative_freq_vecs, molecule_perturb_scale=molecule_perturb_scale, reversed_direction=reversed_direction, ) new_molecule = Molecule( species=orig_species, coords=new_coords, charge=orig_charge, spin_multiplicity=orig_multiplicity, ) if check_connectivity and not transition_state: structure_successfully_perturbed = ( check_for_structure_changes( ref_mol, new_molecule) == "no_change") if structure_successfully_perturbed: break if not structure_successfully_perturbed: raise Exception( "ERROR: Unable to perturb coordinates to remove negative frequency without changing " "the connectivity! Exiting...") new_opt_QCInput = QCInput( molecule=new_molecule, rem=opt_rem, opt=orig_opt_input.opt, pcm=orig_opt_input.pcm, solvent=orig_opt_input.solvent, smx=orig_opt_input.smx, ) new_opt_QCInput.write_file(input_file)
def setUp(self, lpad=False): out_file = os.path.join(module_dir, "..", "..", "test_files", "FF_working", "test.qout.opt_0") qc_out = QCOutput(filename=out_file) self.act_mol = qc_out.data["initial_molecule"] super(TestCore, self).setUp(lpad=False)
def setUp(self): out_file = os.path.join(files_dir, "qchem", "test.qout.opt_0") qc_out = QCOutput(filename=out_file) self.act_mol = qc_out.data["initial_molecule"] super(TestFrequencyFlatteningOptimizeFW, self).setUp(lpad=False)
def test_torsion_potential(self): # location of test files test_tor_files = os.path.join(module_dir, "..", "..", "test_files", "torsion_wf") # define starting molecule and torsion potential workflow object initial_qcin = QCInput.from_file( os.path.join(test_tor_files, "initial_opt", "mol.qin")) initial_mol = initial_qcin.molecule atom_indexes = [6, 8, 9, 10] angles = [0.0, 90.0, 180.0] rem = [] # add the first rem section rem.append({ "jobtype": "opt", "method": "wb97m-v", "basis": "def2-tzvppd", "gen_scfman": "true", "geom_opt_max_cycles": 75, "max_scf_cycles": 300, "scf_algorithm": "diis", "scf_guess": "sad", "sym_ignore": "true", "symmetry": "false", "thresh": 14 }) # the second rem section rem.append({ "jobtype": "opt", "method": "wb97m-v", "basis": "def2-tzvppd", "geom_opt_max_cycles": 75, "max_scf_cycles": 300, "scf_algorithm": "diis", "scf_guess": "sad", "sym_ignore": "true", "symmetry": "false", "thresh": 14 }) real_wf = get_wf_torsion_potential(molecule=initial_mol, atom_indexes=atom_indexes, angles=angles, rem=rem, db_file=">>db_file<<") # use powerup to replace run with fake run # def ref_dirs ref_dirs = { "initial_opt": os.path.join(test_tor_files, "initial_opt"), "opt_0": os.path.join(test_tor_files, "opt_0"), "opt_90": os.path.join(test_tor_files, "opt_90"), "opt_180": os.path.join(test_tor_files, "opt_180") } fake_wf = use_fake_qchem(real_wf, ref_dirs) self.lp.add_wf(fake_wf) rapidfire( self.lp, fworker=FWorker(env={"db_file": os.path.join(db_dir, "db.json")})) wf_test = self.lp.get_wf_by_fw_id(1) self.assertTrue( all([s == "COMPLETED" for s in wf_test.fw_states.values()])) # Checking of the inputs happens in fake_run_qchem so there is no point to retest the inputs # Check the output info that gets inserted in the DB init_opt = self.get_task_collection().find_one( {"task_label": "initial_opt"}) init_opt_final_mol = Molecule.from_dict( init_opt["output"]["optimized_molecule"]) init_opt_final_e = init_opt["output"]["final_energy"] # parse output file act_init_opt_out = QCOutput( os.path.join(test_tor_files, "initial_opt", "mol.qout")) act_init_opt_mol = act_init_opt_out.data[ "molecule_from_optimized_geometry"] act_init_opt_final_e = act_init_opt_out.data["final_energy"] np.testing.assert_equal(act_init_opt_mol.species, init_opt_final_mol.species) np.testing.assert_allclose(act_init_opt_mol.cart_coords, init_opt_final_mol.cart_coords, atol=0.0001) np.testing.assert_equal(act_init_opt_final_e, init_opt_final_e) # Optimization of 0 torsion opt_0 = self.get_task_collection().find_one({"task_label": "opt_0"}) opt_0_final_mol = Molecule.from_dict( opt_0["output"]["optimized_molecule"]) opt_0_final_e = opt_0["output"]["final_energy"] # parse output file act_opt_0_out = QCOutput( os.path.join(test_tor_files, "opt_0", "mol.qout")) act_opt_0_mol = act_opt_0_out.data["molecule_from_optimized_geometry"] act_opt_0_final_e = act_opt_0_out.data["final_energy"] np.testing.assert_equal(act_opt_0_mol.species, opt_0_final_mol.species) np.testing.assert_allclose(act_opt_0_mol.cart_coords, opt_0_final_mol.cart_coords, atol=0.0001) np.testing.assert_equal(act_opt_0_final_e, opt_0_final_e) # Optimization of 90 torsion opt_90 = self.get_task_collection().find_one({"task_label": "opt_90"}) opt_90_final_mol = Molecule.from_dict( opt_90["output"]["optimized_molecule"]) opt_90_final_e = opt_90["output"]["final_energy"] # parse output file act_opt_90_out = QCOutput( os.path.join(test_tor_files, "opt_90", "mol.qout")) act_opt_90_mol = act_opt_90_out.data[ "molecule_from_optimized_geometry"] act_opt_90_final_e = act_opt_90_out.data["final_energy"] np.testing.assert_equal(act_opt_90_mol.species, opt_90_final_mol.species) np.testing.assert_allclose(act_opt_90_mol.cart_coords, opt_90_final_mol.cart_coords, atol=0.0001) np.testing.assert_equal(act_opt_90_final_e, opt_90_final_e) # Optimization of 180 torsion opt_180 = self.get_task_collection().find_one( {"task_label": "opt_180"}) opt_180_final_mol = Molecule.from_dict( opt_180["output"]["optimized_molecule"]) opt_180_final_e = opt_180["output"]["final_energy"] # parse output file act_opt_180_out = QCOutput( os.path.join(test_tor_files, "opt_180", "mol.qout")) act_opt_180_mol = act_opt_180_out.data[ "molecule_from_optimized_geometry"] act_opt_180_final_e = act_opt_180_out.data["final_energy"] np.testing.assert_equal(act_opt_180_mol.species, opt_180_final_mol.species) np.testing.assert_allclose(act_opt_180_mol.cart_coords, opt_180_final_mol.cart_coords, atol=0.0001) np.testing.assert_equal(act_opt_180_final_e, opt_180_final_e)
def setUp(self): self.maxDiff = None out_file = os.path.join(files_dir, "qchem", "fsm.qout") qc_out = QCOutput(filename=out_file) self.act_mol = qc_out.data["initial_molecule"] super(TestFrequencyFlatteningTransitionStateFW, self).setUp(lpad=False)
class QChemErrorHandler(ErrorHandler): """ Master QChemErrorHandler class that handles a number of common errors that occur during QChem runs. """ is_monitor = False def __init__(self, input_file="mol.qin", output_file="mol.qout", scf_max_cycles=200, geom_max_cycles=200): """ Initializes the error handler from a set of input and output files. Args: input_file (str): Name of the QChem input file. output_file (str): Name of the QChem output file. scf_max_cycles (int): The max iterations to set to fix SCF failure. geom_max_cycles (int): The max iterations to set to fix geometry optimization failure. """ self.input_file = input_file self.output_file = output_file self.scf_max_cycles = scf_max_cycles self.geom_max_cycles = geom_max_cycles self.outdata = None self.errors = [] self.opt_error_history = [] def check(self): # Checks output file for errors. self.outdata = QCOutput(self.output_file).data self.errors = self.outdata.get("errors") # If we aren't out of optimization cycles, but we were in the past, reset the history if "out_of_opt_cycles" not in self.errors and len( self.opt_error_history) > 0: self.opt_error_history = [] # If we're out of optimization cycles and we have unconnected fragments, no need to handle any errors if "out_of_opt_cycles" in self.errors and self.outdata[ "structure_change"] == "unconnected_fragments": return False return len(self.errors) > 0 def correct(self): backup({self.input_file, self.output_file}) actions = [] self.qcinp = QCInput.from_file(self.input_file) if "SCF_failed_to_converge" in self.errors: # Check number of SCF cycles. If not set or less than scf_max_cycles, # increase to that value and rerun. If already set, check if # scf_algorithm is unset or set to DIIS, in which case set to GDM. # Otherwise, tell user to call SCF error handler and do nothing. if str(self.qcinp.rem.get("max_scf_cycles")) != str( self.scf_max_cycles): self.qcinp.rem["max_scf_cycles"] = self.scf_max_cycles actions.append({"max_scf_cycles": self.scf_max_cycles}) elif self.qcinp.rem.get("scf_algorithm", "diis").lower() == "diis": self.qcinp.rem["scf_algorithm"] = "gdm" actions.append({"scf_algorithm": "gdm"}) elif self.qcinp.rem.get("scf_algorithm", "gdm").lower() == "gdm": self.qcinp.rem["scf_algorithm"] = "diis_gdm" actions.append({"scf_algorithm": "diis_gdm"}) else: print( "More advanced changes may impact the SCF result. Use the SCF error handler" ) elif "out_of_opt_cycles" in self.errors: # Check number of opt cycles. If less than geom_max_cycles, increase # to that value, set last geom as new starting geom and rerun. if str(self.qcinp.rem.get("geom_opt_max_cycles")) != str( self.geom_max_cycles): self.qcinp.rem["geom_opt_max_cycles"] = self.geom_max_cycles actions.append({"geom_max_cycles:": self.scf_max_cycles}) if len(self.outdata.get("energy_trajectory")) > 1: self.qcinp.molecule = self.outdata.get( "molecule_from_last_geometry") actions.append({"molecule": "molecule_from_last_geometry"}) # If already at geom_max_cycles, often can just get convergence by restarting # from the geometry of the last cycle. But we'll also save any structural # changes that happened along the way. else: self.opt_error_history += [self.outdata["structure_change"]] if len(self.opt_error_history) > 1: if self.opt_error_history[-1] == "no_change": # If no structural changes occured in two consecutive optimizations, # and we still haven't converged, then just exit. return { "errors": self.errors, "actions": None, "opt_error_history": self.opt_error_history } self.qcinp.molecule = self.outdata.get( "molecule_from_last_geometry") actions.append({"molecule": "molecule_from_last_geometry"}) elif "unable_to_determine_lamda" in self.errors: # Set last geom as new starting geom and rerun. If no opt cycles, # use diff SCF strat? Diff initial guess? Change basis? if len(self.outdata.get("energy_trajectory")) > 1: self.qcinp.molecule = self.outdata.get( "molecule_from_last_geometry") actions.append({"molecule": "molecule_from_last_geometry"}) elif self.qcinp.rem.get("scf_algorithm", "diis").lower() == "diis": self.qcinp.rem["scf_algorithm"] = "rca_diis" actions.append({"scf_algorithm": "rca_diis"}) if self.qcinp.rem.get("gen_scfman"): self.qcinp.rem["gen_scfman"] = False actions.append({"gen_scfman": False}) else: print( "Use a different initial guess? Perhaps a different basis?" ) elif "linear_dependent_basis" in self.errors: # DIIS -> RCA_DIIS. If already RCA_DIIS, change basis? if self.qcinp.rem.get("scf_algorithm", "diis").lower() == "diis": self.qcinp.rem["scf_algorithm"] = "rca_diis" actions.append({"scf_algorithm": "rca_diis"}) if self.qcinp.rem.get("gen_scfman"): self.qcinp.rem["gen_scfman"] = False actions.append({"gen_scfman": False}) else: print("Perhaps use a better basis?") elif "failed_to_transform_coords" in self.errors: # Check for symmetry flag in rem. If not False, set to False and rerun. # If already False, increase threshold? if not self.qcinp.rem.get("sym_ignore") or self.qcinp.rem.get( "symmetry"): self.qcinp.rem["sym_ignore"] = True self.qcinp.rem["symmetry"] = False actions.append({"sym_ignore": True}) actions.append({"symmetry": False}) else: print("Perhaps increase the threshold?") elif "input_file_error" in self.errors: print( "Something is wrong with the input file. Examine error message by hand." ) return {"errors": self.errors, "actions": None} elif "failed_to_read_input" in self.errors: # Almost certainly just a temporary problem that will not be encountered again. Rerun job as-is. actions.append({"rerun job as-is"}) elif "IO_error" in self.errors: # Almost certainly just a temporary problem that will not be encountered again. Rerun job as-is. actions.append({"rerun job as-is"}) elif "read_molecule_error" in self.errors: # Almost certainly just a temporary problem that will not be encountered again. Rerun job as-is. actions.append({"rerun job as-is"}) elif "never_called_qchem" in self.errors: # Almost certainly just a temporary problem that will not be encountered again. Rerun job as-is. actions.append({"rerun job as-is"}) elif "unknown_error" in self.errors: print("Examine error message by hand.") return {"errors": self.errors, "actions": None} else: # You should never get here. If correct is being called then errors should have at least one entry, # in which case it should have been caught by the if/elifs above. print( "If you get this message, something has gone terribly wrong!") return {"errors": self.errors, "actions": None} os.rename(self.input_file, self.input_file + ".last") self.qcinp.write_file(self.input_file) return {"errors": self.errors, "actions": actions}
def setUp(self): self.maxDiff = None out_file = os.path.join(files_dir, "qchem", "test.qout.opt_0") qc_out = QCOutput(filename=out_file) self.act_mol = qc_out.data["initial_molecule"] super(TestSinglePointFW, self).setUp(lpad=False)