def _verify_inputs(self): user_qin = QCInput.from_file(os.path.join(os.getcwd(), "mol.qin")) # Check mol.qin ref_qin = QCInput.from_file(os.path.join(self["ref_dir"], "mol.qin")) np.testing.assert_equal(ref_qin.molecule.species, user_qin.molecule.species) np.testing.assert_allclose( ref_qin.molecule.cart_coords, user_qin.molecule.cart_coords, atol=0.0001) for key in ref_qin.rem: if user_qin.rem.get(key) != ref_qin.rem.get(key): raise ValueError("Rem key {} is inconsistent!".format(key)) if ref_qin.opt is not None: for key in ref_qin.opt: if user_qin.opt.get(key) != ref_qin.opt.get(key): raise ValueError("Opt key {} is inconsistent!".format(key)) if ref_qin.pcm is not None: for key in ref_qin.pcm: if user_qin.pcm.get(key) != ref_qin.pcm.get(key): raise ValueError("PCM key {} is inconsistent!".format(key)) if ref_qin.solvent is not None: for key in ref_qin.solvent: if user_qin.solvent.get(key) != ref_qin.solvent.get(key): raise ValueError( "Solvent key {} is inconsistent!".format(key)) logger.info("RunQChemFake: verified input successfully")
def test_write_file_from_OptSet(self): from pymatgen.io.qchem.sets import OptSet odd_dict = loadfn(os.path.join(os.path.dirname(__file__), "odd.json")) odd_mol = odd_dict["spec"]["_tasks"][0]["molecule"] qcinp = OptSet(odd_mol) qcinp.write_file(os.path.join(os.path.dirname(__file__), "test.qin")) test_dict = QCInput.from_file(os.path.join(os.path.dirname(__file__), "test.qin")).as_dict() test_ref_dict = QCInput.from_file(os.path.join(os.path.dirname(__file__), "test_ref.qin")).as_dict() for key in test_dict: self.assertEqual(test_dict[key], test_ref_dict[key]) os.remove(os.path.join(os.path.dirname(__file__), "test.qin"))
def test_full_init(self): test_molecule = QCInput.from_file( os.path.join(test_dir, "new_qchem_files/pcm.qin")).molecule test_DictSet = QChemDictSet( molecule=test_molecule, job_type='opt', basis_set='6-31g*', scf_algorithm='diis', dft_rung=1, pcm_dielectric=10.0, max_scf_cycles=35) self.assertEqual( test_DictSet.rem, { 'job_type': 'opt', 'gen_scfman': 'true', 'basis': '6-31g*', 'max_scf_cycles': 35, 'exchange': 'b3lyp', 'geom_opt_max_cycles': 200, 'scf_algorithm': 'diis', 'solvent_method': 'pcm' }) self.assertEqual( test_DictSet.pcm, { 'heavypoints': '194', 'hpoints': '194', 'radii': 'uff', 'theory': 'cpcm', 'vdwscale': '1.1' }) self.assertEqual(test_DictSet.solvent, {'dielectric': 10.0}) self.assertEqual(test_DictSet.molecule, test_molecule)
def test_read_opt(self): str_opt = """$opt CONSTRAINT tors 2 3 4 5 25.0 bend 2 1 4 110.0 ENDCONSTRAINT FIXED x y 2 4 5 ENDFIXED DUMMY M 2 3 4 5 ENDDUMMY CONNECT 4 3 2 3 5 6 ENDCONNECT $end""" opt_test = QCInput.read_opt(str_opt) opt_actual = { "CONSTRAINT": ["tors 2 3 4 5 25.0", "bend 2 1 4 110.0"], "FIXED": ["x y 2 4 5"], "DUMMY": ["M 2 3 4 5"], "CONNECT": ["4 3 2 3 5 6"] } self.assertDictEqual(opt_actual, opt_test)
def test_read_negative(self): str_molecule = """$molecule -1 1 S -1.1516880000 0.8568110000 -0.0787470000 S 1.1527500000 -0.8580450000 -0.0786430000 O -1.6523520000 1.8607750000 -1.0252100000 O -0.9052880000 1.2448490000 1.3156410000 O 0.9072410000 -1.2461780000 1.3158760000 O 1.6543670000 -1.8616640000 -1.0249090000 C -2.5841130000 -0.3746500000 0.0297340000 C 2.5833220000 0.3755850000 0.0296900000 F -3.6480730000 0.2204040000 0.6112110000 F -2.2609850000 -1.4531020000 0.7616580000 F -2.9656640000 -0.7966010000 -1.1900330000 F 3.6467050000 -0.2152590000 0.6163310000 F 2.2560700000 1.4560310000 0.7568190000 F 2.9672080000 0.7933560000 -1.1908790000 N -0.0001900000 -0.0016540000 -0.8250640000 $end $rem job_type = opt basis = 6-311++g* max_scf_cycles = 200 gen_scfman = true scf_algorithm = diis method = wb97xd geom_opt_max_cycles = 200 $end """ qcinp = QCInput.from_string(str_molecule) self.assertEqual(str_molecule,str(qcinp))
def test_read_only_rem(self): str_rem = """Trying to break you! $rem job_type opt method wb97m-v basis def2-qzvppd max_scf_cycles 300 gen_scfman = true $end $pcm heavypoints 194 hpoints 194 radii uff theory cpcm vdwscale 1.1 $end $solvent dielectric 10.0 $end """ rem_test = QCInput.read_rem(str_rem) rem_actual = { "job_type": "opt", "method": "wb97m-v", "basis": "def2-qzvppd", "max_scf_cycles": "300", "gen_scfman": "true" } self.assertDictEqual(rem_actual, rem_test)
def test_solvent_template(self): solvent_params = {"dielectric": "5.0"} solvent_test = QCInput.solvent_template(solvent_params) solvent_actual = """$solvent dielectric 5.0 $end""" self.assertEqual(solvent_actual, solvent_test)
def test_pcm_template(self): pcm_params = {"theory": "cpcm"} pcm_test = QCInput.pcm_template(pcm_params) pcm_actual = """$pcm theory cpcm $end""" self.assertEqual(pcm_actual, pcm_test)
def test_double_FF_opt(self): # location of test files test_double_FF_files = os.path.join(module_dir, "..", "..", "test_files", "double_FF_wf") # define starting molecule and workflow object initial_qcin = QCInput.from_file( os.path.join(test_double_FF_files, "block", "launcher_first", "mol.qin.opt_0")) initial_mol = initial_qcin.molecule real_wf = get_wf_double_FF_opt( molecule=initial_mol, pcm_dielectric=10.0, qchem_input_params={ "basis_set": "6-311++g**", "scf_algorithm": "diis", "overwrite_inputs": { "rem": { "sym_ignore": "true" } } }) # use powerup to replace run with fake run ref_dirs = { "first_FF_no_pcm": os.path.join(test_double_FF_files, "block", "launcher_first"), "second_FF_with_pcm": os.path.join(test_double_FF_files, "block", "launcher_second") } fake_wf = use_fake_qchem(real_wf, ref_dirs) self.lp.add_wf(fake_wf) rapidfire( self.lp, fworker=FWorker(env={"max_cores": 32, "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()])) first_FF = self.get_task_collection().find_one({ "task_label": "first_FF_no_pcm" }) self.assertEqual(first_FF["calcs_reversed"][0]["input"]["solvent"], None) self.assertEqual(first_FF["num_frequencies_flattened"], 1) first_FF_final_mol = Molecule.from_dict( first_FF["output"]["optimized_molecule"]) second_FF = self.get_task_collection().find_one({ "task_label": "second_FF_with_pcm" }) self.assertEqual(second_FF["calcs_reversed"][0]["input"]["solvent"], {"dielectric": "10.0"}) self.assertEqual(second_FF["num_frequencies_flattened"], 1) second_FF_initial_mol = Molecule.from_dict( second_FF["input"]["initial_molecule"]) self.assertEqual(first_FF_final_mol, second_FF_initial_mol)
def test_smx_template(self): smx_params = {"solvent": "water"} smx_test = QCInput.smx_template(smx_params) smx_actual = """$smx solvent water $end""" self.assertEqual(smx_actual, smx_test)
def test_pcm_init(self): test_molecule = QCInput.from_file( os.path.join(test_dir, "new_qchem_files/pcm.qin")).molecule test_SPSet = SinglePointSet( molecule=test_molecule, pcm_dielectric=10.0) self.assertEqual( test_SPSet.rem, { 'job_type': 'sp', 'gen_scfman': 'true', 'basis': '6-311++g*', 'max_scf_cycles': 200, 'method': 'wb97xd', 'scf_algorithm': 'gdm', 'solvent_method': 'pcm' }) self.assertEqual( test_SPSet.pcm, { 'heavypoints': '194', 'hpoints': '194', 'radii': 'uff', 'theory': 'cpcm', 'vdwscale': '1.1' }) self.assertEqual(test_SPSet.solvent, {'dielectric': 10.0}) self.assertEqual(test_SPSet.molecule, test_molecule)
def test_read_bad_smx(self): str_smx = """Once again, I'm trying to break you! $solvent solvent = water $end""" smx_test = QCInput.read_smx(str_smx) smx_actual = {} self.assertDictEqual(smx_actual, smx_test)
def test_read_bad_solvent(self): str_solvent = """Once again, I'm trying to break you! $solvent dielectric = 5.0 $end""" solvent_test = QCInput.read_solvent(str_solvent) solvent_actual = {} self.assertDictEqual(solvent_actual, solvent_test)
def test_from_multi_jobs_file(self): job_list_test = QCInput.from_multi_jobs_file( os.path.join(test_dir, "pt_n2_wb97mv_0.0.in")) species = ["S", "C", "H", "C", "H", "C", "H", "C", "C", "C", "H", "C", "H", "C", "H", "S"] coords = [[-0.00250959, -0.05817469, -0.02921636], [1.70755408, -0.03033788, -0.01382912], [2.24317221, -0.05215019, 0.92026728], [2.21976393, 0.01718014, -1.27293235], [3.27786220, 0.04082146, -1.48539646], [1.20867399, 0.04478540, -2.27007793], [1.40292257, 0.10591684, -3.33110912], [-0.05341046, 0.01577217, -1.74839343], [-1.32843436, 0.03545064, -2.45531187], [-1.55195156, 0.08743920, -3.80184635], [-0.75245172, 0.10267657, -4.52817967], [-2.93293778, 0.08408786, -4.13352169], [-3.31125108, 0.11340328, -5.14405819], [-3.73173288, 0.02741365, -3.03412864], [-4.80776535, 0.00535688, -2.99564645], [-2.81590978, -0.00516172, -1.58990580]] molecule_1_actual = Molecule(species, coords) rem_1_actual = { "job_type": "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" } opt_1_actual = {"CONSTRAINT": ["tors 6 8 9 10 0.0"]} self.assertEqual(molecule_1_actual, job_list_test[0].molecule) self.assertEqual(rem_1_actual, job_list_test[0].rem) self.assertEqual(opt_1_actual, job_list_test[0].opt) molecule_2_actual = "read" rem_2_actual = { "job_type": "sp", "method": "wb97m-v", "basis": "def2-tzvppd", "gen_scfman": "true", "geom_opt_max_cycles": "75", "max_scf_cycles": "300", "scf_algorithm": "diis", "scf_guess": "read", "sym_ignore": "true", "symmetry": "false", "thresh": "14" } self.assertEqual(molecule_2_actual, job_list_test[1].molecule) self.assertEqual(rem_2_actual, job_list_test[1].rem)
def test_OptFF(self): myjob = QCJob.opt_with_frequency_flattener(qchem_command="qchem", max_cores=32, input_file="test.qin", output_file="test.qout") expected_next = QCJob( qchem_command="qchem", max_cores=32, multimode="openmp", input_file="test.qin", output_file="test.qout", suffix=".opt_0", backup=True).as_dict() self.assertEqual(next(myjob).as_dict(),expected_next) expected_next = QCJob( qchem_command="qchem", max_cores=32, multimode="openmp", input_file="test.qin", output_file="test.qout", suffix=".freq_0", backup=False).as_dict() self.assertEqual(next(myjob).as_dict(),expected_next) self.assertEqual(QCInput.from_file(os.path.join(test_dir,"FF_working/test.qin.freq_0")).as_dict(),QCInput.from_file(os.path.join(scr_dir,"test.qin")).as_dict()) expected_next = QCJob( qchem_command="qchem", max_cores=32, multimode="openmp", input_file="test.qin", output_file="test.qout", suffix=".opt_1", backup=False).as_dict() self.assertEqual(next(myjob).as_dict(),expected_next) self.assertEqual(QCInput.from_file(os.path.join(test_dir,"FF_working/test.qin.opt_1")).as_dict(),QCInput.from_file(os.path.join(scr_dir,"test.qin")).as_dict()) expected_next = QCJob( qchem_command="qchem", max_cores=32, multimode="openmp", input_file="test.qin", output_file="test.qout", suffix=".freq_1", backup=False).as_dict() self.assertEqual(next(myjob).as_dict(),expected_next) self.assertEqual(QCInput.from_file(os.path.join(test_dir,"FF_working/test.qin.freq_1")).as_dict(),QCInput.from_file(os.path.join(scr_dir,"test.qin")).as_dict()) self.assertRaises(StopIteration,myjob.__next__)
def test_read_bad_pcm(self): str_pcm = """I'm once again trying to break you! $pcm theory = cpcm radii = uff vdwscale = 1.1 $end""" pcm_test = QCInput.read_pcm(str_pcm) pcm_actual = {} self.assertDictEqual(pcm_actual, pcm_test)
def test_read_molecule(self): str_molecule = """$molecule 0 1 C -9.5782000000 0.6241500000 0.0000000000 O -7.5827400000 0.5127000000 -0.0000000000 $end""" molecule_test = QCInput.read_molecule(str_molecule) species = ["C", "O"] coords = [[-9.5782000000, 0.6241500000, 0.0000000000], [-7.5827400000, 0.5127000000, -0.0000000000]] molecule_actual = Molecule(species, coords) self.assertEqual(molecule_actual, molecule_test)
def test_molecule_template(self): species = ["C", "O"] coords = [[-9.5782000000, 0.6241500000, 0.0000000000], [-7.5827400000, 0.5127000000, -0.0000000000]] mol = Molecule(species=species, coords=coords) molecule_test = QCInput.molecule_template(mol) molecule_actual = """$molecule 0 1 C -9.5782000000 0.6241500000 0.0000000000 O -7.5827400000 0.5127000000 -0.0000000000 $end""" self.assertEqual(molecule_actual, molecule_test)
def test_rem_template(self): rem_params = { "job_type": "opt", "method": "wb97m-v", "basis": "def2-qzvppd", "max_scf_cycles": 300, "gen_scfman": "true" } rem_test = QCInput.rem_template(rem_params).split("\n") rem_actual_list = ["$rem", " job_type = opt", " method = wb97m-v", " basis = def2-qzvppd", " max_scf_cycles = 300", " gen_scfman = true", "$end"] for i_rem in rem_actual_list: self.assertIn(i_rem, rem_test)
def test_read_pcm(self): str_pcm = """I'm once again trying to break you! $pcm theory cpcm radii uff vdwscale 1.1 $end""" pcm_test = QCInput.read_pcm(str_pcm) pcm_actual = { "theory": "cpcm", "radii": "uff", "vdwscale": "1.1" } self.assertDictEqual(pcm_actual, pcm_test)
def test_opt_template(self): opt_params = { "CONSTRAINT": ["tors 2 3 4 5 25.0", "bend 2 1 4 110.0"], "FIXED": ["x y 2 4 5"], "DUMMY": ["M 2 3 4 5"], "CONNECT": ["4 3 2 3 5 6"] } opt_test = QCInput.opt_template(opt_params).split("\n") opt_actual_list = ["$opt", "CONSTRAINT", " tors 2 3 4 5 25.0", " bend 2 1 4 110.0", "ENDCONSTRAINT", "FIXED", " x y 2 4 5", "ENDFIXED", "DUMMY", " M 2 3 4 5", "ENDDUMMY", "CONNECT", " 4 3 2 3 5 6", "ENDCONNECT", "$end"] for i_opt in opt_actual_list: self.assertIn(i_opt, opt_test)
def test_smd_init(self): test_molecule = QCInput.from_file( os.path.join(test_dir, "new_qchem_files/pcm.qin")).molecule test_SPSet = SinglePointSet(molecule=test_molecule, smd_solvent='water') self.assertEqual( test_SPSet.rem, { 'job_type': 'sp', 'gen_scfman': 'true', 'basis': '6-311++g*', 'max_scf_cycles': 200, 'method': 'wb97xd', 'scf_algorithm': 'gdm', 'solvent_method': 'smd' }) self.assertEqual(test_SPSet.smx, {'solvent': 'water'}) self.assertEqual(test_SPSet.molecule, test_molecule)
def test_init(self): test_molecule = QCInput.from_file( os.path.join(test_dir, "new_qchem_files/pcm.qin")).molecule test_FreqSet = FreqSet(molecule=test_molecule) self.assertEqual( test_FreqSet.rem, { 'job_type': 'freq', 'gen_scfman': 'true', 'basis': '6-311++g*', 'max_scf_cycles': 200, 'method': 'wb97xd', 'scf_algorithm': 'gdm' }) self.assertEqual(test_FreqSet.pcm, {}) self.assertEqual(test_FreqSet.solvent, {}) self.assertEqual(test_FreqSet.molecule, test_molecule)
def test_find_sections(self): str_single_job_input = """$molecule 0 1 S -0.00250959 -0.05817469 -0.02921636 C 1.70755408 -0.03033788 -0.01382912 H 2.24317221 -0.05215019 0.92026728 C 2.21976393 0.01718014 -1.27293235 H 3.27786220 0.04082146 -1.48539646 C 1.20867399 0.04478540 -2.27007793 H 1.40292257 0.10591684 -3.33110912 C -0.05341046 0.01577217 -1.74839343 C -1.32843436 0.03545064 -2.45531187 C -1.55195156 0.08743920 -3.80184635 H -0.75245172 0.10267657 -4.52817967 C -2.93293778 0.08408786 -4.13352169 H -3.31125108 0.11340328 -5.14405819 C -3.73173288 0.02741365 -3.03412864 H -4.80776535 0.00535688 -2.99564645 S -2.81590978 -0.00516172 -1.58990580 $end $rem job_type = 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 $end $opt CONSTRAINT tors 6 8 9 10 0.0 ENDCONSTRAINT $end """ sections_test = QCInput.find_sections(str_single_job_input) section_actual = ["molecule", "rem", "opt"] self.assertEqual(section_actual, sections_test)
def test_overwrite_input_addition(self): test_molecule = QCInput.from_file( os.path.join(test_dir, "new_qchem_files/pcm.qin")).molecule overwrite_inputs = {"rem": {'thresh': 14}} test_OptSet = OptSet( molecule=test_molecule, overwrite_inputs=overwrite_inputs) act_rem = { 'job_type': 'opt', 'gen_scfman': 'true', 'basis': '6-311++g*', 'max_scf_cycles': 200, 'method': 'wb97xd', 'scf_algorithm': 'gdm', 'geom_opt_max_cycles': 200, 'thresh': 14 } self.assertDictEqual(act_rem, test_OptSet.rem)
def test_read_rem(self): str_rem = """Trying to break you! $rem job_type opt method wb97m-v basis def2-qzvppd max_scf_cycles 300 gen_scfman = true $end""" rem_test = QCInput.read_rem(str_rem) rem_actual = { "job_type": "opt", "method": "wb97m-v", "basis": "def2-qzvppd", "max_scf_cycles": "300", "gen_scfman": "true" } self.assertDictEqual(rem_actual, rem_test)
def test_double_solvation(self): test_molecule = QCInput.from_file( os.path.join(test_dir, "new_qchem_files/pcm.qin")).molecule raised_error = False dict_set = None try: dict_set = QChemDictSet(molecule=test_molecule, job_type='opt', basis_set='6-31g*', scf_algorithm='diis', dft_rung=1, pcm_dielectric=10.0, smd_solvent="water", max_scf_cycles=35) except ValueError: raised_error = True self.assertTrue(raised_error) self.assertEqual(dict_set, None)
def test_init(self): test_molecule = QCInput.from_file( os.path.join(test_dir, "new_qchem_files/pcm.qin")).molecule test_DictSet = QChemDictSet( molecule=test_molecule, job_type='opt', basis_set='6-31G*', scf_algorithm='diis') self.assertEqual( test_DictSet.rem, { 'job_type': 'opt', 'gen_scfman': 'true', 'basis': '6-31g*', 'max_scf_cycles': 200, 'method': 'wb97xd', 'scf_algorithm': 'diis', 'geom_opt_max_cycles': 200 }) self.assertEqual(test_DictSet.pcm, {}) self.assertEqual(test_DictSet.solvent, {}) self.assertEqual(test_DictSet.molecule, test_molecule)
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 test_Fragmentation(self): with patch("atomate.qchem.firetasks.fragmenter.FWAction") as FWAction_patch: mock_FWAction = MagicMock() FWAction_patch.return_value = mock_FWAction mock_FWAction.as_dict.return_value = {'stored_data': {}, 'exit': False, 'update_spec': {}, 'mod_spec': [], 'additions': [], 'detours': [], 'defuse_children': False, 'defuse_workflow': False} # location of test files test_FF_then_fragment_files = os.path.join(module_dir, "..", "..", "test_files", "FF_then_fragment_wf") # define starting molecule and workflow object initial_qcin = QCInput.from_file( os.path.join(test_FF_then_fragment_files, "block", "launcher_first", "mol.qin.opt_0")) initial_mol = initial_qcin.molecule real_wf = get_fragmentation_wf(molecule=initial_mol, depth=0, do_triplets=False) # use powerup to replace run with fake run ref_dirs = { "first FF": os.path.join(test_FF_then_fragment_files, "block", "launcher_first"), "fragment and FF_opt": os.path.join(test_FF_then_fragment_files, "block", "launcher_second") } fake_wf = use_fake_qchem(real_wf, ref_dirs) self.lp.add_wf(fake_wf) rapidfire( self.lp, fworker=FWorker(env={"max_cores": 32, "db_file": os.path.join(db_dir, "db.json")}), pdb_on_exception=True) first_FF = self.get_task_collection().find_one({ "task_label": "first FF" }) self.assertEqual(first_FF["calcs_reversed"][0]["input"]["solvent"], None) self.assertEqual(first_FF["num_frequencies_flattened"], 0) self.assertEqual(len(FWAction_patch.call_args[1]["additions"]), 5 * 3)
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 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? 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, save_final_scratch=False, **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. linked (bool): Whether or not to use the linked flattener. Defaults to True. 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 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), save_scratch=True, backup=first, **QCJob_kwargs ) ) opt_outdata = QCOutput(output_file + ".opt_" + str(ii)).data opt_indata = QCInput.from_file(input_file + ".opt_" + 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"] ): print( "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 outdata.get("frequencies")[0] > 0.0: print("All frequencies positive!") break if ( 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 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 not save_final_scratch: shutil.rmtree(os.path.join(os.getcwd(), "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 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 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..." ) if ( 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)
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, reversed_direction=False, ignore_connectivity=False, **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. reversed_direction (bool): Whether to reverse the direction of the vibrational frequency vectors. Defaults to False. ignore_connectivity (bool): Whether to ignore differences in connectivity introduced by structural perturbation. Defaults to False. **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 msc = MoleculeStructureComparator() 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 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": print( "Unstable molecule broke into unconnected fragments! 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: negative_freq_vecs = outdata.get( "frequency_mode_vectors")[0] old_coords = outdata.get("initial_geometry") old_molecule = outdata.get("initial_molecule") 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=old_coords, 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 msc.are_equal(old_molecule, new_molecule) or ignore_connectivity: structure_successfully_perturbed = True break if not structure_successfully_perturbed: raise Exception( "Unable to perturb coordinates to remove negative frequency without changing the bonding structure" ) 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_full_init(self): test_molecule = QCInput.from_file( os.path.join(test_dir, "new_qchem_files/pcm.qin")).molecule test_DictSet = QChemDictSet(molecule=test_molecule, job_type='opt', basis_set='6-31g*', scf_algorithm='diis', dft_rung=1, pcm_dielectric=10.0, max_scf_cycles=35) self.assertEqual( test_DictSet.rem, { 'job_type': 'opt', 'gen_scfman': 'true', 'basis': '6-31g*', 'max_scf_cycles': 35, 'method': 'b3lyp', 'geom_opt_max_cycles': 200, 'scf_algorithm': 'diis', 'xc_grid': '3', 'solvent_method': 'pcm', 'symmetry': 'false', 'sym_ignore': 'true', 'resp_charges': 'true' }) self.assertEqual( test_DictSet.pcm, { 'heavypoints': '194', 'hpoints': '194', 'radii': 'uff', 'theory': 'cpcm', 'vdwscale': '1.1' }) self.assertEqual(test_DictSet.solvent, {'dielectric': 10.0}) self.assertEqual(test_DictSet.molecule, test_molecule) test_DictSet = QChemDictSet(molecule=test_molecule, job_type='opt', basis_set='6-31g*', scf_algorithm='diis', dft_rung=1, smd_solvent='water', max_scf_cycles=35) self.assertEqual( test_DictSet.rem, { 'job_type': 'opt', 'gen_scfman': 'true', 'basis': '6-31g*', 'max_scf_cycles': 35, 'method': 'b3lyp', 'geom_opt_max_cycles': 200, 'scf_algorithm': 'diis', 'xc_grid': '3', 'solvent_method': 'smd', 'ideriv': '1', 'symmetry': 'false', 'sym_ignore': 'true', 'resp_charges': 'true' }) self.assertEqual(test_DictSet.smx, {'solvent': 'water'})
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, vdw_mode=orig_input.vdw_mode, van_der_waals=orig_input.van_der_waals, ) 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, vdw_mode=orig_input.vdw_mode, van_der_waals=orig_input.van_der_waals, ) 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, vdw_mode=orig_input.vdw_mode, van_der_waals=orig_input.van_der_waals, ) 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, vdw_mode=orig_input.vdw_mode, van_der_waals=orig_input.van_der_waals, ) 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, vdw_mode=orig_opt_input.vdw_mode, van_der_waals=orig_opt_input.van_der_waals, ) 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, vdw_mode=orig_opt_input.vdw_mode, van_der_waals=orig_opt_input.van_der_waals, ) new_opt_QCInput.write_file(input_file)
def test_read_nbo(self): str_molecule = """$molecule 0 2 C -2.0338520000 0.0865500000 -1.4158570000 C -1.2819580000 0.3850830000 -0.1564990000 C -2.0067300000 1.1271820000 0.9225950000 C 0.1219120000 -0.0366190000 0.0148810000 C 0.6767790000 -1.0507090000 -0.7802400000 C 2.0072450000 -1.4517610000 -0.6185380000 C 2.8079970000 -0.8434840000 0.3427930000 C 2.2778880000 0.1645690000 1.1416530000 C 0.9468200000 0.5630060000 0.9784410000 H -1.3919850000 0.1591240000 -2.2995570000 H -2.4671570000 -0.9174600000 -1.3722490000 H -2.8505080000 0.8017250000 -1.5613060000 H -3.0889210000 0.9823990000 0.8362370000 H -1.7216740000 0.7761670000 1.9194500000 H -1.8021560000 2.1999010000 0.8510710000 H 0.0793240000 -1.5592640000 -1.5324310000 H 2.4136820000 -2.2421190000 -1.2440900000 H 3.8415290000 -1.1539430000 0.4689660000 H 2.8984450000 0.6464300000 1.8925800000 H 0.5733200000 1.3632210000 1.6120990000 $end $rem job_type = sp max_scf_cycles = 200 gen_scfman = true xc_grid = 3 scf_algorithm = diis method = wb97xv basis = def2-tzvp symmetry = false sym_ignore = true nbo = true $end $nbo $end """ qcinp = QCInput.from_string(str_molecule) self.assertEqual(str_molecule, str(qcinp)) str_molecule = """$molecule 0 2 C -2.0338520000 0.0865500000 -1.4158570000 C -1.2819580000 0.3850830000 -0.1564990000 C -2.0067300000 1.1271820000 0.9225950000 C 0.1219120000 -0.0366190000 0.0148810000 C 0.6767790000 -1.0507090000 -0.7802400000 C 2.0072450000 -1.4517610000 -0.6185380000 C 2.8079970000 -0.8434840000 0.3427930000 C 2.2778880000 0.1645690000 1.1416530000 C 0.9468200000 0.5630060000 0.9784410000 H -1.3919850000 0.1591240000 -2.2995570000 H -2.4671570000 -0.9174600000 -1.3722490000 H -2.8505080000 0.8017250000 -1.5613060000 H -3.0889210000 0.9823990000 0.8362370000 H -1.7216740000 0.7761670000 1.9194500000 H -1.8021560000 2.1999010000 0.8510710000 H 0.0793240000 -1.5592640000 -1.5324310000 H 2.4136820000 -2.2421190000 -1.2440900000 H 3.8415290000 -1.1539430000 0.4689660000 H 2.8984450000 0.6464300000 1.8925800000 H 0.5733200000 1.3632210000 1.6120990000 $end $rem job_type = sp max_scf_cycles = 200 gen_scfman = true xc_grid = 3 scf_algorithm = diis method = wb97xv basis = def2-tzvp symmetry = false sym_ignore = true nbo = true $end $nbo print = 1 $end """ qcinp = QCInput.from_string(str_molecule) self.assertEqual(str_molecule, str(qcinp))
def test_OptFF(self): myjob = QCJob.opt_with_frequency_flattener( qchem_command="qchem -slurm", max_cores=32, input_file="mol.qin", output_file="mol.qout", linked=False, ) expected_next = QCJob( qchem_command="qchem -slurm", max_cores=32, multimode="openmp", input_file="mol.qin", output_file="mol.qout", suffix=".opt_0", backup=True, ).as_dict() self.assertEqual(next(myjob).as_dict(), expected_next) expected_next = QCJob( qchem_command="qchem -slurm", max_cores=32, multimode="openmp", input_file="mol.qin", output_file="mol.qout", suffix=".freq_0", backup=False, ).as_dict() self.assertEqual(next(myjob).as_dict(), expected_next) self.assertEqual( QCInput.from_file( os.path.join(test_dir, "6004_frag12/mol.qin.freq_0")).as_dict(), QCInput.from_file(os.path.join(scr_dir, "mol.qin")).as_dict(), ) expected_next = QCJob( qchem_command="qchem -slurm", max_cores=32, multimode="openmp", input_file="mol.qin", output_file="mol.qout", suffix=".opt_1", backup=False, ).as_dict() self.assertEqual(next(myjob).as_dict(), expected_next) self.assertEqual( QCInput.from_file( os.path.join(test_dir, "6004_frag12/mol.qin.opt_1")).as_dict(), QCInput.from_file(os.path.join(scr_dir, "mol.qin")).as_dict(), ) expected_next = QCJob( qchem_command="qchem -slurm", max_cores=32, multimode="openmp", input_file="mol.qin", output_file="mol.qout", suffix=".freq_1", backup=False, ).as_dict() self.assertEqual(next(myjob).as_dict(), expected_next) self.assertEqual( QCInput.from_file( os.path.join(test_dir, "6004_frag12/mol.qin.freq_1")).as_dict(), QCInput.from_file(os.path.join(scr_dir, "mol.qin")).as_dict(), ) expected_next = QCJob( qchem_command="qchem -slurm", max_cores=32, multimode="openmp", input_file="mol.qin", output_file="mol.qout", suffix=".opt_2", backup=False, ).as_dict() self.assertEqual(next(myjob).as_dict(), expected_next) self.assertEqual( QCInput.from_file( os.path.join(test_dir, "6004_frag12/mol.qin.opt_2")).as_dict(), QCInput.from_file(os.path.join(scr_dir, "mol.qin")).as_dict(), ) expected_next = QCJob( qchem_command="qchem -slurm", max_cores=32, multimode="openmp", input_file="mol.qin", output_file="mol.qout", suffix=".freq_2", backup=False, ).as_dict() self.assertEqual(next(myjob).as_dict(), expected_next) self.assertEqual( QCInput.from_file( os.path.join(test_dir, "6004_frag12/mol.qin.freq_2")).as_dict(), QCInput.from_file(os.path.join(scr_dir, "mol.qin")).as_dict(), )
def ts_with_frequency_flattener(cls, qchem_command, multimode="openmp", input_file="mol.qin", output_file="mol.qout", qclog_file="mol.qclog", ts_guess_method="fsm", max_iterations=10, max_molecule_perturb_scale=0.3, check_connectivity=True, **QCJob_kwargs): """ Optimize the transition state for a reaction based on reactant and product geometries 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. qclog_file (str): Name of the QChem log file. ts_guess_method (str): Name of the method to be used to generate a guess for the transition state. By default, this will be "fsm", meaning that the Freezing-String Method will be used. In the future, "gsm" for the Growing-String Method will also be supported. 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 orig_ts_input = QCInput.from_file(input_file) orig_ts_rem = copy.deepcopy(orig_ts_input.rem) orig_ts_rem["job_type"] = "ts" del orig_ts_rem["fsm_ngrad"] del orig_ts_rem["fsm_nnode"] del orig_ts_rem["fsm_mode"] del orig_ts_rem["fsm_opt_mode"] orig_freq_rem = copy.deepcopy(orig_ts_input.rem) orig_freq_rem["job_type"] = "freq" orig_ts_rem["geom_opt_max_cycles"] = 250 if not os.path.exists(input_file): raise AssertionError('Input file must be present!') # First job should be the transition state guess, via FSM or GSM yield (QCJob(qchem_command=qchem_command, multimode=multimode, input_file=input_file, output_file=output_file, qclog_file=qclog_file, suffix="." + ts_guess_method, backup=True, **QCJob_kwargs)) fsm_outdata = QCOutput(output_file + "." + ts_guess_method).data #TODO: Consider possible errors or other problems which might arise in an FSM job ts_QCInput = QCInput(molecule=fsm_outdata.get("molecule_from_last_geometry"), rem=orig_ts_rem, opt=orig_ts_input.opt, pcm=orig_ts_input.pcm, solvent=orig_ts_input.solvent, smx=orig_ts_input.smx) ts_QCInput.write_file(input_file) 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=".ts_" + str(ii), backup=False, **QCJob_kwargs)) first = False ts_outdata = QCOutput(output_file + ".ts_" + str(ii)).data if ts_outdata["structure_change"] == "unconnected_fragments" and not ts_outdata["completion"]: print("Unstable molecule broke into unconnected fragments which failed to optimize! Exiting...") break else: freq_QCInput = QCInput( molecule=ts_outdata.get("molecule_from_optimized_geometry"), rem=orig_freq_rem, opt=orig_ts_input.opt, pcm=orig_ts_input.pcm, solvent=orig_ts_input.solvent, smx=orig_ts_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!") 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_ts_QCInput = QCInput( molecule=new_molecule, rem=orig_ts_rem, opt=orig_ts_input.opt, pcm=orig_ts_input.pcm, solvent=orig_ts_input.solvent) new_ts_QCInput.write_file(input_file)
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"}) else: self.opt_error_history += [self.outdata["structure_change"]] if len(self.opt_error_history) > 1: if self.opt_error_history[-1] == "no_change": 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 test_OptFF(self): myjob = QCJob.opt_with_frequency_flattener( qchem_command="qchem -slurm", max_cores=32, input_file="mol.qin", output_file="mol.qout", linked=True, ) expected_next = QCJob( qchem_command="qchem -slurm", max_cores=32, multimode="openmp", input_file="mol.qin", output_file="mol.qout", suffix=".opt_0", save_scratch=True, backup=True, ).as_dict() self.assertEqual(next(myjob).as_dict(), expected_next) expected_next = QCJob( qchem_command="qchem -slurm", max_cores=32, multimode="openmp", input_file="mol.qin", output_file="mol.qout", suffix=".freq_0", save_scratch=True, backup=False, ).as_dict() self.assertEqual(next(myjob).as_dict(), expected_next) self.assertEqual( QCInput.from_file( os.path.join(test_dir, "small_neg_freq/mol.qin.freq_0")).as_dict(), QCInput.from_file(os.path.join(scr_dir, "mol.qin")).as_dict(), ) shutil.copyfile( os.path.join(scr_dir, "mol.qin"), os.path.join(scr_dir, "mol.qin.freq_0"), ) expected_next = QCJob( qchem_command="qchem -slurm", max_cores=32, multimode="openmp", input_file="mol.qin", output_file="mol.qout", suffix=".opt_1", save_scratch=True, backup=False, ).as_dict() self.assertEqual(next(myjob).as_dict(), expected_next) self.assertEqual( QCInput.from_file( os.path.join(test_dir, "small_neg_freq/mol.qin.opt_1")).as_dict(), QCInput.from_file(os.path.join(scr_dir, "mol.qin")).as_dict(), ) shutil.copyfile( os.path.join(scr_dir, "mol.qin"), os.path.join(scr_dir, "mol.qin.opt_1"), ) expected_next = QCJob( qchem_command="qchem -slurm", max_cores=32, multimode="openmp", input_file="mol.qin", output_file="mol.qout", suffix=".freq_1", save_scratch=True, backup=False, ).as_dict() self.assertEqual(next(myjob).as_dict(), expected_next) self.assertEqual( QCInput.from_file( os.path.join(test_dir, "small_neg_freq/mol.qin.freq_1")).as_dict(), QCInput.from_file(os.path.join(scr_dir, "mol.qin")).as_dict(), ) shutil.copyfile( os.path.join(scr_dir, "mol.qin"), os.path.join(scr_dir, "mol.qin.freq_1"), ) expected_next = QCJob( qchem_command="qchem -slurm", max_cores=32, multimode="openmp", input_file="mol.qin", output_file="mol.qout", suffix=".opt_2", save_scratch=True, backup=False, ).as_dict() self.assertEqual(next(myjob).as_dict(), expected_next) self.assertEqual( QCInput.from_file( os.path.join(test_dir, "small_neg_freq/mol.qin.opt_2")).as_dict(), QCInput.from_file(os.path.join(scr_dir, "mol.qin")).as_dict(), ) shutil.copyfile( os.path.join(scr_dir, "mol.qin"), os.path.join(scr_dir, "mol.qin.opt_2"), ) expected_next = QCJob( qchem_command="qchem -slurm", max_cores=32, multimode="openmp", input_file="mol.qin", output_file="mol.qout", suffix=".freq_2", save_scratch=True, backup=False, ).as_dict() self.assertEqual(next(myjob).as_dict(), expected_next) self.assertEqual( QCInput.from_file( os.path.join(test_dir, "small_neg_freq/mol.qin.freq_2")).as_dict(), QCInput.from_file(os.path.join(scr_dir, "mol.qin")).as_dict(), ) shutil.copyfile( os.path.join(scr_dir, "mol.qin"), os.path.join(scr_dir, "mol.qin.freq_2"), ) self.assertRaises(StopIteration, myjob.__next__)
def test_from_string(self): string = """$molecule 0 1 S -0.00250959 -0.05817469 -0.02921636 C 1.70755408 -0.03033788 -0.01382912 H 2.24317221 -0.05215019 0.92026728 C 2.21976393 0.01718014 -1.27293235 H 3.27786220 0.04082146 -1.48539646 C 1.20867399 0.04478540 -2.27007793 H 1.40292257 0.10591684 -3.33110912 C -0.05341046 0.01577217 -1.74839343 C -1.32843436 0.03545064 -2.45531187 C -1.55195156 0.08743920 -3.80184635 H -0.75245172 0.10267657 -4.52817967 C -2.93293778 0.08408786 -4.13352169 H -3.31125108 0.11340328 -5.14405819 C -3.73173288 0.02741365 -3.03412864 H -4.80776535 0.00535688 -2.99564645 S -2.81590978 -0.00516172 -1.58990580 $end $rem 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 $end $opt CONSTRAINT tors 6 8 9 10 0.0 ENDCONSTRAINT $end """ qcinput_test = QCInput.from_string(string) species = [ "S", "C", "H", "C", "H", "C", "H", "C", "C", "C", "H", "C", "H", "C", "H", "S", ] coords = [ [-0.00250959, -0.05817469, -0.02921636], [1.70755408, -0.03033788, -0.01382912], [2.24317221, -0.05215019, 0.92026728], [2.21976393, 0.01718014, -1.27293235], [3.27786220, 0.04082146, -1.48539646], [1.20867399, 0.04478540, -2.27007793], [1.40292257, 0.10591684, -3.33110912], [-0.05341046, 0.01577217, -1.74839343], [-1.32843436, 0.03545064, -2.45531187], [-1.55195156, 0.08743920, -3.80184635], [-0.75245172, 0.10267657, -4.52817967], [-2.93293778, 0.08408786, -4.13352169], [-3.31125108, 0.11340328, -5.14405819], [-3.73173288, 0.02741365, -3.03412864], [-4.80776535, 0.00535688, -2.99564645], [-2.81590978, -0.00516172, -1.58990580], ] molecule_actual = Molecule(species, coords) self.assertEqual(molecule_actual, qcinput_test.molecule) rem_actual = { "job_type": "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", } self.assertDictEqual(rem_actual, qcinput_test.rem) opt_actual = {"CONSTRAINT": ["tors 6 8 9 10 0.0"]} self.assertDictEqual(opt_actual, qcinput_test.opt)
def test_multi_job_string(self): species = [ "S", "C", "H", "C", "H", "C", "H", "C", "C", "C", "H", "C", "H", "C", "H", "S", ] coords = [ [-0.00250959, -0.05817469, -0.02921636], [1.70755408, -0.03033788, -0.01382912], [2.24317221, -0.05215019, 0.92026728], [2.21976393, 0.01718014, -1.27293235], [3.27786220, 0.04082146, -1.48539646], [1.20867399, 0.04478540, -2.27007793], [1.40292257, 0.10591684, -3.33110912], [-0.05341046, 0.01577217, -1.74839343], [-1.32843436, 0.03545064, -2.45531187], [-1.55195156, 0.08743920, -3.80184635], [-0.75245172, 0.10267657, -4.52817967], [-2.93293778, 0.08408786, -4.13352169], [-3.31125108, 0.11340328, -5.14405819], [-3.73173288, 0.02741365, -3.03412864], [-4.80776535, 0.00535688, -2.99564645], [-2.81590978, -0.00516172, -1.58990580], ] molecule_1 = Molecule(species, coords) rem_1 = { "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", } opt_1 = {"CONSTRAINT": ["tors 6 8 9 10 0.0"]} job_1 = QCInput(molecule=molecule_1, rem=rem_1, opt=opt_1) molecule_2 = "read" rem_2 = { "jobtype": "sp", "method": "wb97m-v", "basis": "def2-tzvppd", "gen_scfman": "true", "geom_opt_max_cycles": "75", "max_scf_cycles": "300", "scf_algorithm": "diis", "scf_guess": "read", "sym_ignore": "true", "symmetry": "false", "thresh": "14", } job_2 = QCInput(molecule=molecule_2, rem=rem_2) job_list = [job_1, job_2] multi_job_str_test = QCInput.multi_job_string( job_list=job_list).split("\n") multi_job_str_actual_list = [ "$molecule", " 0 1", " S -0.0025095900 -0.0581746900 -0.0292163600", " C 1.7075540800 -0.0303378800 -0.0138291200", " H 2.2431722100 -0.0521501900 0.9202672800", " C 2.2197639300 0.0171801400 -1.2729323500", " H 3.2778622000 0.0408214600 -1.4853964600", " C 1.2086739900 0.0447854000 -2.2700779300", " H 1.4029225700 0.1059168400 -3.3311091200", " C -0.0534104600 0.0157721700 -1.7483934300", " C -1.3284343600 0.0354506400 -2.4553118700", " C -1.5519515600 0.0874392000 -3.8018463500", " H -0.7524517200 0.1026765700 -4.5281796700", " C -2.9329377800 0.0840878600 -4.1335216900", " H -3.3112510800 0.1134032800 -5.1440581900", " C -3.7317328800 0.0274136500 -3.0341286400", " H -4.8077653500 0.0053568800 -2.9956464500", " S -2.8159097800 -0.0051617200 -1.5899058000", "$end", "$rem", " job_type = 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", "$end", "$opt", "CONSTRAINT", " tors 6 8 9 10 0.0", "ENDCONSTRAINT", "$end", "@@@", "$molecule", " read", "$end", "$rem", " job_type = 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", "$end", ] for i_str in multi_job_str_actual_list: self.assertIn(i_str, multi_job_str_test)
def test_from_multi_jobs_file(self): job_list_test = QCInput.from_multi_jobs_file( os.path.join(PymatgenTest.TEST_FILES_DIR, "qchem", "pt_n2_wb97mv_0.0.in")) species = [ "S", "C", "H", "C", "H", "C", "H", "C", "C", "C", "H", "C", "H", "C", "H", "S", ] coords = [ [-0.00250959, -0.05817469, -0.02921636], [1.70755408, -0.03033788, -0.01382912], [2.24317221, -0.05215019, 0.92026728], [2.21976393, 0.01718014, -1.27293235], [3.27786220, 0.04082146, -1.48539646], [1.20867399, 0.04478540, -2.27007793], [1.40292257, 0.10591684, -3.33110912], [-0.05341046, 0.01577217, -1.74839343], [-1.32843436, 0.03545064, -2.45531187], [-1.55195156, 0.08743920, -3.80184635], [-0.75245172, 0.10267657, -4.52817967], [-2.93293778, 0.08408786, -4.13352169], [-3.31125108, 0.11340328, -5.14405819], [-3.73173288, 0.02741365, -3.03412864], [-4.80776535, 0.00535688, -2.99564645], [-2.81590978, -0.00516172, -1.58990580], ] molecule_1_actual = Molecule(species, coords) rem_1_actual = { "job_type": "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", } opt_1_actual = {"CONSTRAINT": ["tors 6 8 9 10 0.0"]} self.assertEqual(molecule_1_actual, job_list_test[0].molecule) self.assertEqual(rem_1_actual, job_list_test[0].rem) self.assertEqual(opt_1_actual, job_list_test[0].opt) molecule_2_actual = "read" rem_2_actual = { "job_type": "sp", "method": "wb97m-v", "basis": "def2-tzvppd", "gen_scfman": "true", "geom_opt_max_cycles": "75", "max_scf_cycles": "300", "scf_algorithm": "diis", "scf_guess": "read", "sym_ignore": "true", "symmetry": "false", "thresh": "14", } self.assertEqual(molecule_2_actual, job_list_test[1].molecule) self.assertEqual(rem_2_actual, job_list_test[1].rem)
def test_FFopt_and_critic(self): # location of test files test_files = os.path.join( module_dir, "..", "..", "test_files", "critic_test_files" ) # define starting molecule and workflow object initial_qcin = QCInput.from_file( os.path.join(test_files, "FFopt", "mol.qin.orig") ) initial_mol = initial_qcin.molecule real_wf = get_wf_FFopt_and_critic( molecule=initial_mol, suffix="testing", qchem_input_params={ "dft_rung": 4, "smd_solvent": "custom", "custom_smd": "18.5,1.415,0.00,0.735,20.2,0.00,0.00", "overwrite_inputs": { "rem": {"thresh": "14", "scf_guess_always": "True"} }, }, ) # use powerup to replace run with fake run ref_dirs = { "{}:{}".format( initial_mol.composition.alphabetical_formula, "FFopt_testing" ): os.path.join(test_files, "FFopt"), "{}:{}".format( initial_mol.composition.alphabetical_formula, "CC2_testing" ): os.path.join(test_files, "critic_example"), } fake_wf = use_fake_qchem(real_wf, ref_dirs) self.lp.add_wf(fake_wf) rapidfire( self.lp, fworker=FWorker( env={"max_cores": 32, "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()])) FFopt = self.get_task_collection().find_one( { "task_label": "{}:{}".format( initial_mol.composition.alphabetical_formula, "FFopt_testing" ) } ) self.assertEqual(FFopt["calcs_reversed"][0]["input"]["smx"]["solvent"], "other") self.assertEqual(FFopt["num_frequencies_flattened"], 0) FFopt_final_mol = Molecule.from_dict(FFopt["output"]["optimized_molecule"]) CC2 = self.get_task_collection().find_one( { "task_label": "{}:{}".format( initial_mol.composition.alphabetical_formula, "CC2_testing" ) } ) CC2_initial_mol = Molecule.from_dict(CC2["input"]["initial_molecule"]) self.assertEqual(FFopt_final_mol, CC2_initial_mol) self.assertEqual(CC2["output"]["job_type"], "sp") self.assertEqual(CC2["output"]["final_energy"], -343.4820411597) critic2_drone_ref = loadfn( os.path.join(test_files, "critic_example", "critic2_drone_ref.json") ) self.assertEqual(CC2["critic2"], critic2_drone_ref)
def test_OptFF(self): myjob = QCJob.opt_with_frequency_flattener( qchem_command="qchem", max_cores=32, input_file="test.qin", output_file="test.qout", linked=False, ) expected_next = QCJob( qchem_command="qchem", max_cores=32, multimode="openmp", input_file="test.qin", output_file="test.qout", suffix=".opt_0", backup=True, ).as_dict() self.assertEqual(next(myjob).as_dict(), expected_next) expected_next = QCJob( qchem_command="qchem", max_cores=32, multimode="openmp", input_file="test.qin", output_file="test.qout", suffix=".freq_0", backup=False, ).as_dict() self.assertEqual(next(myjob).as_dict(), expected_next) self.assertEqual( QCInput.from_file( os.path.join(test_dir, "FF_working/test.qin.freq_0")).as_dict(), QCInput.from_file(os.path.join(scr_dir, "test.qin")).as_dict(), ) expected_next = QCJob( qchem_command="qchem", max_cores=32, multimode="openmp", input_file="test.qin", output_file="test.qout", suffix=".opt_1", backup=False, ).as_dict() self.assertEqual(next(myjob).as_dict(), expected_next) self.assertEqual( QCInput.from_file( os.path.join(test_dir, "FF_working/test.qin.opt_1")).as_dict(), QCInput.from_file(os.path.join(scr_dir, "test.qin")).as_dict(), ) expected_next = QCJob( qchem_command="qchem", max_cores=32, multimode="openmp", input_file="test.qin", output_file="test.qout", suffix=".freq_1", backup=False, ).as_dict() self.assertEqual(next(myjob).as_dict(), expected_next) self.assertEqual( QCInput.from_file( os.path.join(test_dir, "FF_working/test.qin.freq_1")).as_dict(), QCInput.from_file(os.path.join(scr_dir, "test.qin")).as_dict(), ) self.assertRaises(StopIteration, myjob.__next__)
def generate_doc(self, dir_name, qcinput_files, qcoutput_files, multirun): try: fullpath = os.path.abspath(dir_name) d = jsanitize(self.additional_fields, strict=True) d["schema"] = { "code": "atomate", "version": QChemDrone.__version__ } d["dir_name"] = fullpath # If a saved "orig" input file is present, parse it incase the error handler made changes # to the initial input molecule or rem params, which we might want to filter for later if len(qcinput_files) > len(qcoutput_files): orig_input = QCInput.from_file(os.path.join(dir_name, qcinput_files.pop("orig"))) d["orig"] = {} d["orig"]["molecule"] = orig_input.molecule.as_dict() d["orig"]["molecule"]["charge"] = int(d["orig"]["molecule"]["charge"]) d["orig"]["rem"] = orig_input.rem d["orig"]["opt"] = orig_input.opt d["orig"]["pcm"] = orig_input.pcm d["orig"]["solvent"] = orig_input.solvent d["orig"]["smx"] = orig_input.smx if multirun: d["calcs_reversed"] = self.process_qchem_multirun( dir_name, qcinput_files, qcoutput_files) else: d["calcs_reversed"] = [ self.process_qchemrun(dir_name, taskname, qcinput_files.get(taskname), output_filename) for taskname, output_filename in qcoutput_files.items() ] # reverse the calculations data order so newest calc is first d["calcs_reversed"].reverse() d["structure_change"] = [] d["warnings"] = {} for entry in d["calcs_reversed"]: if "structure_change" in entry and "structure_change" not in d["warnings"]: if entry["structure_change"] != "no_change": d["warnings"]["structure_change"] = True if "structure_change" in entry: d["structure_change"].append(entry["structure_change"]) for key in entry["warnings"]: if key not in d["warnings"]: d["warnings"][key] = True d_calc_init = d["calcs_reversed"][-1] d_calc_final = d["calcs_reversed"][0] d["input"] = { "initial_molecule": d_calc_init["initial_molecule"], "job_type": d_calc_init["input"]["rem"]["job_type"] } d["output"] = { "initial_molecule": d_calc_final["initial_molecule"], "job_type": d_calc_final["input"]["rem"]["job_type"], "mulliken": d_calc_final["Mulliken"][-1] } if "RESP" in d_calc_final: d["output"]["resp"] = d_calc_final["RESP"][-1] elif "ESP" in d_calc_final: d["output"]["esp"] = d_calc_final["ESP"][-1] if d["output"]["job_type"] == "opt" or d["output"]["job_type"] == "optimization": if "molecule_from_optimized_geometry" in d_calc_final: d["output"]["optimized_molecule"] = d_calc_final[ "molecule_from_optimized_geometry"] d["output"]["final_energy"] = d_calc_final["final_energy"] else: d["output"]["final_energy"] = "unstable" if d_calc_final["opt_constraint"]: d["output"]["constraint"] = [ d_calc_final["opt_constraint"][0], float(d_calc_final["opt_constraint"][6]) ] if d["output"]["job_type"] == "freq" or d["output"]["job_type"] == "frequency": d["output"]["frequencies"] = d_calc_final["frequencies"] d["output"]["enthalpy"] = d_calc_final["total_enthalpy"] d["output"]["entropy"] = d_calc_final["total_entropy"] if d["input"]["job_type"] == "opt" or d["input"]["job_type"] == "optimization": d["output"]["optimized_molecule"] = d_calc_final[ "initial_molecule"] d["output"]["final_energy"] = d["calcs_reversed"][1][ "final_energy"] if "final_energy" not in d["output"]: if d_calc_final["final_energy"] != None: d["output"]["final_energy"] = d_calc_final["final_energy"] else: d["output"]["final_energy"] = d_calc_final["SCF"][-1][-1][0] # else: # print(d_calc_final) if d_calc_final["completion"]: total_cputime = 0.0 total_walltime = 0.0 for calc in d["calcs_reversed"]: if calc["walltime"] is not None: total_walltime += calc["walltime"] if calc["cputime"] is not None: total_cputime += calc["cputime"] d["walltime"] = total_walltime d["cputime"] = total_cputime else: d["walltime"] = None d["cputime"] = None comp = d["output"]["initial_molecule"].composition d["formula_pretty"] = comp.reduced_formula d["formula_anonymous"] = comp.anonymized_formula d["formula_alphabetical"] = comp.alphabetical_formula d["chemsys"] = "-".join(sorted(set(d_calc_final["species"]))) if d_calc_final["point_group"] != None: d["pointgroup"] = d_calc_final["point_group"] else: try: d["pointgroup"] = PointGroupAnalyzer(d["output"]["initial_molecule"]).sch_symbol except ValueError: d["pointgroup"] = "PGA_error" bb = BabelMolAdaptor(d["output"]["initial_molecule"]) pbmol = bb.pybel_mol smiles = pbmol.write(str("smi")).split()[0] d["smiles"] = smiles d["state"] = "successful" if d_calc_final["completion"] else "unsuccessful" if "special_run_type" in d: if d["special_run_type"] == "frequency_flattener": opt_traj = [] for entry in d["calcs_reversed"]: if entry["input"]["rem"]["job_type"] == "opt" or entry["input"]["rem"]["job_type"] == "optimization": doc = {"initial": {}, "final": {}} doc["initial"]["molecule"] = entry["initial_molecule"] doc["final"]["molecule"] = entry["molecule_from_last_geometry"] doc["initial"]["total_energy"] = entry["energy_trajectory"][0] doc["final"]["total_energy"] = entry["energy_trajectory"][-1] doc["initial"]["scf_energy"] = entry["SCF"][0][-1][0] doc["final"]["scf_energy"] = entry["SCF"][-1][-1][0] doc["structure_change"] = entry["structure_change"] opt_traj.append(doc) opt_traj.reverse() opt_trajectory = {"trajectory": opt_traj, "structure_change": [[ii, entry["structure_change"]] for ii,entry in enumerate(opt_traj)], "energy_increase": []} for ii, entry in enumerate(opt_traj): if entry["final"]["total_energy"] > entry["initial"]["total_energy"]: opt_trajectory["energy_increase"].append([ii, entry["final"]["total_energy"]-entry["initial"]["total_energy"]]) if ii != 0: if entry["final"]["total_energy"] > opt_traj[ii-1]["final"]["total_energy"]: opt_trajectory["energy_increase"].append([ii-1, ii, entry["final"]["total_energy"]-opt_traj[ii-1]["final"]["total_energy"]]) struct_change = check_for_structure_changes(opt_traj[ii-1]["final"]["molecule"], entry["final"]["molecule"]) if struct_change != entry["structure_change"]: opt_trajectory["structure_change"].append([ii-1, ii, struct_change]) d["warnings"]["between_iteration_structure_change"] = True if "linked" in d: if d["linked"] == True: opt_trajectory["discontinuity"] = {"structure": [], "scf_energy": [], "total_energy": []} for ii, entry in enumerate(opt_traj): if ii != 0: if entry["initial"]["molecule"] != opt_traj[ii-1]["final"]["molecule"]: opt_trajectory["discontinuity"]["structure"].append([ii-1,ii]) d["warnings"]["linked_structure_discontinuity"] = True if entry["initial"]["total_energy"] != opt_traj[ii-1]["final"]["total_energy"]: opt_trajectory["discontinuity"]["total_energy"].append([ii-1,ii]) if entry["initial"]["scf_energy"] != opt_traj[ii-1]["final"]["scf_energy"]: opt_trajectory["discontinuity"]["scf_energy"].append([ii-1,ii]) d["opt_trajectory"] = opt_trajectory if d["state"] == "successful": orig_num_neg_freq = sum(1 for freq in d["calcs_reversed"][-2]["frequencies"] if freq < 0) orig_energy = d_calc_init["final_energy"] final_num_neg_freq = sum(1 for freq in d_calc_final["frequencies"] if freq < 0) final_energy = d["calcs_reversed"][1]["final_energy"] d["num_frequencies_flattened"] = orig_num_neg_freq - final_num_neg_freq if final_num_neg_freq > 0: # If a negative frequency remains, # and it's too large to ignore, if final_num_neg_freq > 1 or abs(d["output"]["frequencies"][0]) >= 15.0: d["state"] = "unsuccessful" # then the flattening was unsuccessful if final_energy > orig_energy: d["warnings"]["energy_increased"] = True d["last_updated"] = datetime.datetime.utcnow() return d except Exception: logger.error(traceback.format_exc()) logger.error("Error in " + os.path.abspath(dir_name) + ".\n" + traceback.format_exc()) raise
def test_double_FF_opt(self): # location of test files test_double_FF_files = os.path.join(module_dir, "..", "..", "test_files", "double_FF_wf") # define starting molecule and workflow object initial_qcin = QCInput.from_file( os.path.join(test_double_FF_files, "block", "launcher_first", "mol.qin.opt_0")) initial_mol = initial_qcin.molecule real_wf = get_wf_double_FF_opt( molecule=initial_mol, pcm_dielectric=10.0, qchem_input_params={ "basis_set": "6-311++g**", "scf_algorithm": "diis", "overwrite_inputs": { "rem": { "sym_ignore": "true" } }, }, ) # use powerup to replace run with fake run ref_dirs = { "first_FF_no_pcm": os.path.join(test_double_FF_files, "block", "launcher_first"), "second_FF_with_pcm": os.path.join(test_double_FF_files, "block", "launcher_second"), } fake_wf = use_fake_qchem(real_wf, ref_dirs) self.lp.add_wf(fake_wf) rapidfire( self.lp, fworker=FWorker(env={ "max_cores": 32, "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()])) first_FF = self.get_task_collection().find_one( {"task_label": "first_FF_no_pcm"}) self.assertEqual(first_FF["calcs_reversed"][0]["input"]["solvent"], None) self.assertEqual(first_FF["num_frequencies_flattened"], 1) first_FF_final_mol = Molecule.from_dict( first_FF["output"]["optimized_molecule"]) second_FF = self.get_task_collection().find_one( {"task_label": "second_FF_with_pcm"}) self.assertEqual(second_FF["calcs_reversed"][0]["input"]["solvent"], {"dielectric": "10.0"}) self.assertEqual(second_FF["num_frequencies_flattened"], 1) second_FF_initial_mol = Molecule.from_dict( second_FF["input"]["initial_molecule"]) self.assertEqual(first_FF_final_mol, second_FF_initial_mol)
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 opt_with_frequency_flattener(cls, qchem_command, multimode="openmp", input_file="mol.qin", output_file="mol.qout", qclog_file="mol.qclog", sp_params=None, 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 qclog_file (str): Name of the QChem log file. sp_params (dict): Dictionary with any special parameters for the single-point calculation. 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.new_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, 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!") 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, smx=orig_opt_input.smx) new_opt_QCInput.write_file(input_file) if sp_params is not None: sp_input = QCInput( molecule=opt_outdata.get("molecule_from_optimized_geometry"), rem=sp_params.get("rem", {"method": "wb97x-d", "basis": "6-311++g(d,p)"}), opt=sp_params.get("opt", None), pcm=sp_params.get("pcm", None), solvent=sp_params.get("solvent", None), smx=sp_params.get("smx", None)) else: orig_sp_rem = copy.deepcopy(orig_opt_input.rem) orig_sp_rem["job_type"] = "sp" sp_input = QCInput( molecule=opt_outdata.get("molecule_from_optimized_geometry"), rem=orig_sp_rem, opt=orig_opt_input.opt, pcm=orig_opt_input.pcm, solvent=orig_opt_input.solvent, smx=orig_opt_input.smx) sp_input.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=".sp", **QCJob_kwargs))