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 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") return len(self.errors) > 0
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 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 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 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 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 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_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): 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 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
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 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)
# coding: utf-8 import os from atomate.qchem.workflows.base.double_FF_opt import get_wf_double_FF_opt from fireworks.core.launchpad import LaunchPad from pymatgen.io.qchem.outputs import QCOutput out_file = os.path.join("/global/cscratch1/sd/sblau", "FF_working", "test.qout.opt_0") qc_out = QCOutput(filename=out_file) act_mol = qc_out.data["initial_molecule"] wf = get_wf_double_FF_opt(molecule=act_mol, pcm_dielectric=10.0, max_cores=32, qchem_input_params={ "basis_set": "6-311++g**", "overwrite_inputs": { "rem": { "sym_ignore": "true" } } }) lp = LaunchPad.auto_load() lp.add_wf(wf)
def get_unfinished_jobs(self, sp_params, name_pre="single_point", dirs=None, max_cores=32): """ Look for jobs where optimization and frequency calculations have successfully completed, but single-point has not. Then, for these cases, construct a workflow which will only run the sp job. :param sp_params: dict containing input parameters for single-point job :param name_pre: str representing prefix for all jobs. :param dirs: list of subdirectories to check for unfinished jobs. Default None, meaning that all subdirectories will be checked. :param max_cores: max_cores (int): Maximum number of cores to parallelize over. Defaults to 24. :return: """ if not self.subdirs: raise RuntimeError("Cannot run get_reaction_set_workflow();" "Need reactions components to be isolated in" "different subdirectories.") fws = [] all_dirs = [ d for d in listdir(self.base_dir) if isdir(join(self.base_dir, d)) ] molecules_cleared = [] appropriate_dirs = all_dirs if dirs is not None: appropriate_dirs = [d for d in appropriate_dirs if d in dirs] for d in appropriate_dirs: path = join(self.base_dir, d) file_map = associate_qchem_to_mol(self.base_dir, d) for key, values in file_map.items(): mol_id = extract_id(key) if mol_id in molecules_cleared: continue freq_complete = False sp_complete = False in_files = values["in"] out_files = values["out"] # Check if this molecule has finished freq, sp # If there is no sp output file, or if the sp output file did # not complete, then we may proceed for out_file in out_files: if "freq" in out_file: freq_out = QCOutput(join(path, out_file)) if freq_out.data.get("completion", []): freq_complete = True elif "sp" in out_file: sp_out = QCOutput(join(path, out_file)) if sp_out.data.get("completion", []): sp_complete = True if freq_complete and not sp_complete: # Check if there is already an sp input file freq_in_file = None for in_file in in_files: if "freq" in in_file: freq_in_file = in_file if freq_in_file is None: # We could parse output files to get previous input # information, but we should try to keep all input # files in the same directory continue else: infile = join(path, key.replace(".mol", "") + ".in") outfile = join(path, key.replace(".mol", "") + ".out") qclogfile = join(path, key.replace(".mol", "") + ".qclog") freq_in_file = QCInput.from_file( join(path, freq_in_file)) mol = freq_in_file.molecule fw = SinglePointFW(molecule=mol, name="{}: {}/{}".format( name_pre, d, mol_id), qchem_cmd="qchem -slurm", multimode="openmp", input_file=infile, output_file=outfile, qclog_file=qclogfile, max_cores=max_cores, sp_params=sp_params) fws.append(fw) molecules_cleared.append(mol_id) return Workflow(fws)
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 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", "test.qout.opt_0") qc_out = QCOutput(filename=out_file) self.act_mol = qc_out.data["initial_molecule"] super(TestSinglePointFW, self).setUp(lpad=False)
def associate_qchem_to_mol(base_dir, directory): """ Assign all .in and .out files in a directory to one of the .mol files in that directory, based on the non-H atoms in those molecules. :param base_dir: Directory to begin search in. :param directory: Subdirectory of interes :return: """ base_path = join(base_dir, directory) all_files = [ e for e in listdir(base_path) if not e.startswith(".") and not e.endswith("swp") ] mol_files = [ f for f in all_files if isfile(join(base_path, f)) and f.endswith(".mol") and not f.startswith(".") ] # Note: This will catch .in and .out files for incomplete computations in_files = [ f for f in all_files if isfile(join(base_path, f)) and ( ".in" in f or ".qin" in f) and not f.startswith("atomate") ] out_files = [ f for f in all_files if isfile(join(base_path, f)) and ( ".out" in f or ".qout" in f) and not f.startswith("atomate") ] mapping = {mol: {"in": [], "out": []} for mol in mol_files} for file in in_files: qcin = QCInput.from_file(join(base_path, file)) file_mol = qcin.molecule # Remove H because mol files may not begin with H included file_species = [str(s) for s in file_mol.species if str(s) != "H"] for mf in mol_files: mol_mol = Molecule.from_file(join(base_path, mf)) mol_species = [str(s) for s in mol_mol.species if str(s) != "H"] # Preserve initial order because that gives a better guarantee # That the two are actually associated if mol_species == file_species: mapping[mf]["in"].append(file) break for file in out_files: qcout = QCOutput(join(base_path, file)) file_mol = qcout.data["initial_molecule"] file_species = [str(s) for s in file_mol.species if str(s) != "H"] for mf in mol_files: mol_mol = Molecule.from_file(join(base_path, mf)) mol_species = [str(s) for s in mol_mol.species if str(s) != "H"] # Preserve initial order because that gives a better guarantee # That the two are actually associated if mol_species == file_species: mapping[mf]["out"].append(file) break return mapping
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)
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 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)
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, **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`. """ if not os.path.exists(input_file): raise AssertionError("Input file must be present!") if linked: 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["geom_opt_hessian"] = "read" opt_rem["scf_guess_always"] = True first = True energy_history = [] 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), scratch_dir=os.getcwd(), save_scratch=True, save_name="chain_scratch", backup=first, **QCJob_kwargs)) opt_outdata = QCOutput(output_file + ".opt_" + str(ii)).data first = False 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: 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), scratch_dir=os.getcwd(), save_scratch=True, save_name="chain_scratch", 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 elif (abs(outdata.get("frequencies")[0]) < 15.0 and outdata.get("frequencies")[1] > 0.0): print( "One negative frequency smaller than 15.0 - not worth further flattening!" ) break else: if len(energy_history) > 1: if (abs(energy_history[-1] - energy_history[-2]) < energy_diff_cutoff): print("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) if os.path.exists(os.path.join(os.getcwd(), "chain_scratch")): shutil.rmtree(os.path.join(os.getcwd(), "chain_scratch")) else: 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 history = [] 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)) opt_outdata = QCOutput(output_file + ".opt_" + 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"]): 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, 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 outdata.get("frequencies")[0] > 0.0: print("All frequencies positive!") if opt_outdata.get("final_energy") > orig_energy: print( "WARNING: Energy increased during frequency flattening!" ) break else: 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"] = [] history.append(hist) ref_mol = history[-1]["molecule"] geom_to_perturb = history[-1]["geometry"] negative_freq_vecs = history[-1][ "frequency_mode_vectors"][0] 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"][0] 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: make_good_child_next_parent = False 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...") elif (good_child["energy"] < parent_hist["energy"]): make_good_child_next_parent = True elif (vector_list_diff( good_child[ "frequency_mode_vectors"] [0], parent_hist[ "frequency_mode_vectors"] [0], ) > 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"][0] 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: 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=orig_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)