def test_structural_change(self): t1 = Molecule.from_file(os.path.join(PymatgenTest.TEST_FILES_DIR, "molecules", "structural_change", "t1.xyz")) t2 = Molecule.from_file(os.path.join(PymatgenTest.TEST_FILES_DIR, "molecules", "structural_change", "t2.xyz")) t3 = Molecule.from_file(os.path.join(PymatgenTest.TEST_FILES_DIR, "molecules", "structural_change", "t3.xyz")) thio_1 = Molecule.from_file( os.path.join(PymatgenTest.TEST_FILES_DIR, "molecules", "structural_change", "thiophene1.xyz") ) thio_2 = Molecule.from_file( os.path.join(PymatgenTest.TEST_FILES_DIR, "molecules", "structural_change", "thiophene2.xyz") ) frag_1 = Molecule.from_file( os.path.join( PymatgenTest.TEST_FILES_DIR, "molecules", "new_qchem_files", "test_structure_change", "frag_1.xyz" ) ) frag_2 = Molecule.from_file( os.path.join( PymatgenTest.TEST_FILES_DIR, "molecules", "new_qchem_files", "test_structure_change", "frag_2.xyz" ) ) self.assertEqual(check_for_structure_changes(t1, t1), "no_change") self.assertEqual(check_for_structure_changes(t2, t3), "no_change") self.assertEqual(check_for_structure_changes(t1, t2), "fewer_bonds") self.assertEqual(check_for_structure_changes(t2, t1), "more_bonds") self.assertEqual(check_for_structure_changes(thio_1, thio_2), "unconnected_fragments") self.assertEqual(check_for_structure_changes(frag_1, frag_2), "bond_change")
def opt_with_frequency_flattener( cls, qchem_command, multimode="openmp", input_file="mol.qin", output_file="mol.qout", qclog_file="mol.qclog", max_iterations=10, max_molecule_perturb_scale=0.3, check_connectivity=True, linked=True, transition_state=False, freq_before_opt=False, save_final_scratch=False, **QCJob_kwargs, ): """ Optimize a structure and calculate vibrational frequencies to check if the structure is in a true minima. If there are an inappropriate number of imaginary frequencies (>0 for a minimum-energy structure, >1 for a transition-state), attempt to re-calculate using one of two methods: - Perturb the geometry based on the imaginary frequencies and re-optimize - Use the exact Hessian to inform a subsequent optimization After each geometry optimization, the frequencies are re-calculated to determine if a true minimum (or transition-state) has been found. Note: Very small imaginary frequencies (-15cm^-1 < nu < 0) are allowed if there is only one more than there should be. In other words, if there is one very small imaginary frequency, it is still treated as a minimum, and if there is one significant imaginary frequency and one very small imaginary frequency, it is still treated as a transition-state. Args: qchem_command (str): Command to run QChem. multimode (str): Parallelization scheme, either openmp or mpi. input_file (str): Name of the QChem input file. output_file (str): Name of the QChem output file. max_iterations (int): Number of perturbation -> optimization -> frequency iterations to perform. Defaults to 10. max_molecule_perturb_scale (float): The maximum scaled perturbation that can be applied to the molecule. Defaults to 0.3. check_connectivity (bool): Whether to check differences in connectivity introduced by structural perturbation. Defaults to True. linked (bool): Whether or not to use the linked flattener. If set to True (default), then the explicit Hessians from a vibrational frequency analysis will be used as the initial Hessian of subsequent optimizations. In many cases, this can significantly improve optimization efficiency. transition_state (bool): If True (default False), use a ts optimization (search for a saddle point instead of a minimum) freq_before_opt (bool): If True (default False), run a frequency calculation before any opt/ts searches to improve understanding of the local potential energy surface. save_final_scratch (bool): Whether to save full scratch directory contents at the end of the flattening. Defaults to False. **QCJob_kwargs: Passthrough kwargs to QCJob. See :class:`custodian.qchem.jobs.QCJob`. """ if not os.path.exists(input_file): raise AssertionError("Input file must be present!") if transition_state: opt_method = "ts" perturb_index = 1 else: opt_method = "opt" perturb_index = 0 energy_diff_cutoff = 0.0000001 orig_input = QCInput.from_file(input_file) freq_rem = copy.deepcopy(orig_input.rem) freq_rem["job_type"] = "freq" opt_rem = copy.deepcopy(orig_input.rem) opt_rem["job_type"] = opt_method first = True energy_history = list() if freq_before_opt: if not linked: warnings.warn( "WARNING: This first frequency calculation will not inform subsequent optimization!" ) yield (QCJob( qchem_command=qchem_command, multimode=multimode, input_file=input_file, output_file=output_file, qclog_file=qclog_file, suffix=".freq_pre", save_scratch=True, backup=first, **QCJob_kwargs, )) if linked: opt_rem["geom_opt_hessian"] = "read" opt_rem["scf_guess_always"] = True opt_QCInput = QCInput( molecule=orig_input.molecule, rem=opt_rem, opt=orig_input.opt, pcm=orig_input.pcm, solvent=orig_input.solvent, smx=orig_input.smx, ) opt_QCInput.write_file(input_file) first = False if linked: opt_rem["geom_opt_hessian"] = "read" opt_rem["scf_guess_always"] = True for ii in range(max_iterations): yield (QCJob( qchem_command=qchem_command, multimode=multimode, input_file=input_file, output_file=output_file, qclog_file=qclog_file, suffix=".{}_".format(opt_method) + str(ii), save_scratch=True, backup=first, **QCJob_kwargs, )) opt_outdata = QCOutput(output_file + ".{}_".format(opt_method) + str(ii)).data opt_indata = QCInput.from_file(input_file + ".{}_".format(opt_method) + str(ii)) if opt_indata.rem["scf_algorithm"] != freq_rem["scf_algorithm"]: freq_rem["scf_algorithm"] = opt_indata.rem["scf_algorithm"] opt_rem["scf_algorithm"] = opt_indata.rem["scf_algorithm"] first = False if opt_outdata[ "structure_change"] == "unconnected_fragments" and not opt_outdata[ "completion"]: if not transition_state: warnings.warn( "Unstable molecule broke into unconnected fragments which failed to optimize! Exiting..." ) break energy_history.append(opt_outdata.get("final_energy")) freq_QCInput = QCInput( molecule=opt_outdata.get( "molecule_from_optimized_geometry"), rem=freq_rem, opt=orig_input.opt, pcm=orig_input.pcm, solvent=orig_input.solvent, smx=orig_input.smx, ) freq_QCInput.write_file(input_file) yield (QCJob( qchem_command=qchem_command, multimode=multimode, input_file=input_file, output_file=output_file, qclog_file=qclog_file, suffix=".freq_" + str(ii), save_scratch=True, backup=first, **QCJob_kwargs, )) outdata = QCOutput(output_file + ".freq_" + str(ii)).data indata = QCInput.from_file(input_file + ".freq_" + str(ii)) if indata.rem["scf_algorithm"] != freq_rem["scf_algorithm"]: freq_rem["scf_algorithm"] = indata.rem["scf_algorithm"] opt_rem["scf_algorithm"] = indata.rem["scf_algorithm"] errors = outdata.get("errors") if len(errors) != 0: raise AssertionError( "No errors should be encountered while flattening frequencies!" ) if not transition_state: freq_0 = outdata.get("frequencies")[0] freq_1 = outdata.get("frequencies")[1] if freq_0 > 0.0: warnings.warn("All frequencies positive!") break if abs(freq_0) < 15.0 and freq_1 > 0.0: warnings.warn( "One negative frequency smaller than 15.0 - not worth further flattening!" ) break if len(energy_history) > 1: if abs(energy_history[-1] - energy_history[-2]) < energy_diff_cutoff: warnings.warn("Energy change below cutoff!") break opt_QCInput = QCInput( molecule=opt_outdata.get( "molecule_from_optimized_geometry"), rem=opt_rem, opt=orig_input.opt, pcm=orig_input.pcm, solvent=orig_input.solvent, smx=orig_input.smx, ) opt_QCInput.write_file(input_file) else: freq_0 = outdata.get("frequencies")[0] freq_1 = outdata.get("frequencies")[1] freq_2 = outdata.get("frequencies")[2] if freq_0 < 0.0 < freq_1: warnings.warn("Saddle point found!") break if abs(freq_1) < 15.0 and freq_2 > 0.0: warnings.warn( "Second small imaginary frequency (smaller than 15.0) - not worth further flattening!" ) break opt_QCInput = QCInput( molecule=opt_outdata.get( "molecule_from_optimized_geometry"), rem=opt_rem, opt=orig_input.opt, pcm=orig_input.pcm, solvent=orig_input.solvent, smx=orig_input.smx, ) opt_QCInput.write_file(input_file) if not save_final_scratch: shutil.rmtree(os.path.join(os.getcwd(), "scratch")) else: orig_opt_input = QCInput.from_file(input_file) history = list() for ii in range(max_iterations): yield (QCJob( qchem_command=qchem_command, multimode=multimode, input_file=input_file, output_file=output_file, qclog_file=qclog_file, suffix=".{}_".format(opt_method) + str(ii), backup=first, **QCJob_kwargs, )) opt_outdata = QCOutput(output_file + ".{}_".format(opt_method) + str(ii)).data if first: orig_species = copy.deepcopy(opt_outdata.get("species")) orig_charge = copy.deepcopy(opt_outdata.get("charge")) orig_multiplicity = copy.deepcopy( opt_outdata.get("multiplicity")) orig_energy = copy.deepcopy( opt_outdata.get("final_energy")) first = False if opt_outdata[ "structure_change"] == "unconnected_fragments" and not opt_outdata[ "completion"]: if not transition_state: warnings.warn( "Unstable molecule broke into unconnected fragments which failed to optimize! Exiting..." ) break freq_QCInput = QCInput( molecule=opt_outdata.get( "molecule_from_optimized_geometry"), rem=freq_rem, opt=orig_opt_input.opt, pcm=orig_opt_input.pcm, solvent=orig_opt_input.solvent, smx=orig_opt_input.smx, ) freq_QCInput.write_file(input_file) yield (QCJob( qchem_command=qchem_command, multimode=multimode, input_file=input_file, output_file=output_file, qclog_file=qclog_file, suffix=".freq_" + str(ii), backup=first, **QCJob_kwargs, )) outdata = QCOutput(output_file + ".freq_" + str(ii)).data errors = outdata.get("errors") if len(errors) != 0: raise AssertionError( "No errors should be encountered while flattening frequencies!" ) if not transition_state: freq_0 = outdata.get("frequencies")[0] freq_1 = outdata.get("frequencies")[1] if freq_0 > 0.0: warnings.warn("All frequencies positive!") if opt_outdata.get("final_energy") > orig_energy: warnings.warn( "WARNING: Energy increased during frequency flattening!" ) break if abs(freq_0) < 15.0 and freq_1 > 0.0: warnings.warn( "One negative frequency smaller than 15.0 - not worth further flattening!" ) break if len(energy_history) > 1: if abs(energy_history[-1] - energy_history[-2]) < energy_diff_cutoff: warnings.warn("Energy change below cutoff!") break else: freq_0 = outdata.get("frequencies")[0] freq_1 = outdata.get("frequencies")[1] freq_2 = outdata.get("frequencies")[2] if freq_0 < 0.0 < freq_1: warnings.warn("Saddle point found!") break if abs(freq_1) < 15.0 and freq_2 > 0.0: warnings.warn( "Second small imaginary frequency (smaller than 15.0) - not worth further flattening!" ) break hist = {} hist["molecule"] = copy.deepcopy( outdata.get("initial_molecule")) hist["geometry"] = copy.deepcopy( outdata.get("initial_geometry")) hist["frequencies"] = copy.deepcopy(outdata.get("frequencies")) hist["frequency_mode_vectors"] = copy.deepcopy( outdata.get("frequency_mode_vectors")) hist["num_neg_freqs"] = sum( 1 for freq in outdata.get("frequencies") if freq < 0) hist["energy"] = copy.deepcopy(opt_outdata.get("final_energy")) hist["index"] = len(history) hist["children"] = list() history.append(hist) ref_mol = history[-1]["molecule"] geom_to_perturb = history[-1]["geometry"] negative_freq_vecs = history[-1]["frequency_mode_vectors"][ perturb_index] reversed_direction = False standard = True # If we've found one or more negative frequencies in two consecutive iterations, let's dig in # deeper: if len(history) > 1: # Start by finding the latest iteration's parent: if history[-1]["index"] in history[-2]["children"]: parent_hist = history[-2] history[-1]["parent"] = parent_hist["index"] elif history[-1]["index"] in history[-3]["children"]: parent_hist = history[-3] history[-1]["parent"] = parent_hist["index"] else: raise AssertionError( "ERROR: your parent should always be one or two iterations behind you! Exiting..." ) # if the number of negative frequencies has remained constant or increased from parent to # child, if history[-1]["num_neg_freqs"] >= parent_hist[ "num_neg_freqs"]: # check to see if the parent only has one child, aka only the positive perturbation has # been tried, # in which case just try the negative perturbation from the same parent if len(parent_hist["children"]) == 1: ref_mol = parent_hist["molecule"] geom_to_perturb = parent_hist["geometry"] negative_freq_vecs = parent_hist[ "frequency_mode_vectors"][perturb_index] reversed_direction = True standard = False parent_hist["children"].append(len(history)) # If the parent has two children, aka both directions have been tried, then we have to # get creative: elif len(parent_hist["children"]) == 2: # If we're dealing with just one negative frequency, if parent_hist["num_neg_freqs"] == 1: if history[parent_hist["children"][0]][ "energy"] < history[-1]["energy"]: good_child = copy.deepcopy( history[parent_hist["children"][0]]) else: good_child = copy.deepcopy(history[-1]) if good_child["num_neg_freqs"] > 1: raise Exception( "ERROR: Child with lower energy has more negative frequencies! " "Exiting...") if good_child["energy"] < parent_hist["energy"]: make_good_child_next_parent = True elif (vector_list_diff( good_child["frequency_mode_vectors"] [perturb_index], parent_hist["frequency_mode_vectors"] [perturb_index], ) > 0.2): make_good_child_next_parent = True else: raise Exception( "ERROR: Good child not good enough! Exiting..." ) if make_good_child_next_parent: good_child["index"] = len(history) history.append(good_child) ref_mol = history[-1]["molecule"] geom_to_perturb = history[-1]["geometry"] negative_freq_vecs = history[-1][ "frequency_mode_vectors"][ perturb_index] else: raise Exception( "ERROR: Can't deal with multiple neg frequencies yet! Exiting..." ) else: raise AssertionError( "ERROR: Parent cannot have more than two childen! Exiting..." ) # Implicitly, if the number of negative frequencies decreased from parent to child, # continue normally. if standard: history[-1]["children"].append(len(history)) min_molecule_perturb_scale = 0.1 scale_grid = 10 perturb_scale_grid = (max_molecule_perturb_scale - min_molecule_perturb_scale) / scale_grid structure_successfully_perturbed = False for molecule_perturb_scale in np.arange( max_molecule_perturb_scale, min_molecule_perturb_scale, -perturb_scale_grid, ): new_coords = perturb_coordinates( old_coords=geom_to_perturb, negative_freq_vecs=negative_freq_vecs, molecule_perturb_scale=molecule_perturb_scale, reversed_direction=reversed_direction, ) new_molecule = Molecule( species=orig_species, coords=new_coords, charge=orig_charge, spin_multiplicity=orig_multiplicity, ) if check_connectivity and not transition_state: structure_successfully_perturbed = ( check_for_structure_changes( ref_mol, new_molecule) == "no_change") if structure_successfully_perturbed: break if not structure_successfully_perturbed: raise Exception( "ERROR: Unable to perturb coordinates to remove negative frequency without changing " "the connectivity! Exiting...") new_opt_QCInput = QCInput( molecule=new_molecule, rem=opt_rem, opt=orig_opt_input.opt, pcm=orig_opt_input.pcm, solvent=orig_opt_input.solvent, smx=orig_opt_input.smx, ) new_opt_QCInput.write_file(input_file)
def test_all(self): expected_cmd_options = {"g": "H2O", "c": "2"} expected_energies = [ [ "-13.66580", "-13.66580", "-13.66580", "-13.66580", "-13.66580", "-13.66580", "-13.66580", "-13.66580", "-13.66580", "-13.66580", ], [ "-13.66479", "-13.66479", "-13.66479", "-13.66479", "-13.66479", "-13.66479", "-13.66479", "-13.66479", "-13.66479", "-13.66479", "-13.66479", "-13.66479", "-13.66479", "-13.66479", "-13.66479", "-13.66479", "-13.66479", "-13.66479", "-13.66479", "-13.66479", "-13.66479", "-13.66479", "-13.66479", "-13.66479", "-13.66479", "-13.66479", "-13.66479", ], ] expected_sorted_structures = [[], []] for f in os.listdir(expected_output_dir): if f.endswith("xyz") and "_r" in f: n_conf = int(f.split("_")[0][-1]) n_rot = int(f.split("_")[1].split(".")[0][-1]) m = Molecule.from_file(os.path.join(expected_output_dir, f)) expected_sorted_structures[n_conf].insert(n_rot, m) cout = CRESTOutput(output_filename="crest_out.out", path=test_dir) exp_best = Molecule.from_file( os.path.join(expected_output_dir, "expected_crest_best.xyz")) for i, c in enumerate(cout.sorted_structures_energies): for j, r in enumerate(c): if have_babel: self.assertEqual( check_for_structure_changes( r[0], expected_sorted_structures[i][j]), "no_change") self.assertAlmostEqual(float(r[1]), float(expected_energies[i][j]), 4) self.assertEqual(cout.properly_terminated, True) if have_babel: self.assertEqual( check_for_structure_changes(cout.lowest_energy_structure, exp_best), "no_change") self.assertDictEqual(cout.cmd_options, expected_cmd_options)
def opt_with_frequency_flattener(cls, qchem_command, multimode="openmp", input_file="mol.qin", output_file="mol.qout", qclog_file="mol.qclog", max_iterations=10, max_molecule_perturb_scale=0.3, check_connectivity=True, linked=True, **QCJob_kwargs): """ Optimize a structure and calculate vibrational frequencies to check if the structure is in a true minima. If a frequency is negative, iteratively perturbe the geometry, optimize, and recalculate frequencies until all are positive, aka a true minima has been found. Args: qchem_command (str): Command to run QChem. multimode (str): Parallelization scheme, either openmp or mpi. input_file (str): Name of the QChem input file. output_file (str): Name of the QChem output file. max_iterations (int): Number of perturbation -> optimization -> frequency iterations to perform. Defaults to 10. max_molecule_perturb_scale (float): The maximum scaled perturbation that can be applied to the molecule. Defaults to 0.3. check_connectivity (bool): Whether to check differences in connectivity introduced by structural perturbation. Defaults to True. **QCJob_kwargs: Passthrough kwargs to QCJob. See :class:`custodian.qchem.jobs.QCJob`. """ if not os.path.exists(input_file): raise AssertionError("Input file must be present!") if linked: energy_diff_cutoff = 0.0000001 orig_input = QCInput.from_file(input_file) freq_rem = copy.deepcopy(orig_input.rem) freq_rem["job_type"] = "freq" opt_rem = copy.deepcopy(orig_input.rem) opt_rem["geom_opt_hessian"] = "read" opt_rem["scf_guess_always"] = True first = True energy_history = [] for ii in range(max_iterations): yield (QCJob(qchem_command=qchem_command, multimode=multimode, input_file=input_file, output_file=output_file, qclog_file=qclog_file, suffix=".opt_" + str(ii), scratch_dir=os.getcwd(), save_scratch=True, save_name="chain_scratch", backup=first, **QCJob_kwargs)) opt_outdata = QCOutput(output_file + ".opt_" + str(ii)).data first = False if (opt_outdata["structure_change"] == "unconnected_fragments" and not opt_outdata["completion"]): print( "Unstable molecule broke into unconnected fragments which failed to optimize! Exiting..." ) break else: energy_history.append(opt_outdata.get("final_energy")) freq_QCInput = QCInput( molecule=opt_outdata.get( "molecule_from_optimized_geometry"), rem=freq_rem, opt=orig_input.opt, pcm=orig_input.pcm, solvent=orig_input.solvent, smx=orig_input.smx, ) freq_QCInput.write_file(input_file) yield (QCJob(qchem_command=qchem_command, multimode=multimode, input_file=input_file, output_file=output_file, qclog_file=qclog_file, suffix=".freq_" + str(ii), scratch_dir=os.getcwd(), save_scratch=True, save_name="chain_scratch", backup=first, **QCJob_kwargs)) outdata = QCOutput(output_file + ".freq_" + str(ii)).data errors = outdata.get("errors") if len(errors) != 0: raise AssertionError( "No errors should be encountered while flattening frequencies!" ) if outdata.get("frequencies")[0] > 0.0: print("All frequencies positive!") break elif (abs(outdata.get("frequencies")[0]) < 15.0 and outdata.get("frequencies")[1] > 0.0): print( "One negative frequency smaller than 15.0 - not worth further flattening!" ) break else: if len(energy_history) > 1: if (abs(energy_history[-1] - energy_history[-2]) < energy_diff_cutoff): print("Energy change below cutoff!") break opt_QCInput = QCInput( molecule=opt_outdata.get( "molecule_from_optimized_geometry"), rem=opt_rem, opt=orig_input.opt, pcm=orig_input.pcm, solvent=orig_input.solvent, smx=orig_input.smx, ) opt_QCInput.write_file(input_file) if os.path.exists(os.path.join(os.getcwd(), "chain_scratch")): shutil.rmtree(os.path.join(os.getcwd(), "chain_scratch")) else: if not os.path.exists(input_file): raise AssertionError("Input file must be present!") orig_opt_input = QCInput.from_file(input_file) orig_opt_rem = copy.deepcopy(orig_opt_input.rem) orig_freq_rem = copy.deepcopy(orig_opt_input.rem) orig_freq_rem["job_type"] = "freq" first = True history = [] for ii in range(max_iterations): yield (QCJob(qchem_command=qchem_command, multimode=multimode, input_file=input_file, output_file=output_file, qclog_file=qclog_file, suffix=".opt_" + str(ii), backup=first, **QCJob_kwargs)) opt_outdata = QCOutput(output_file + ".opt_" + str(ii)).data if first: orig_species = copy.deepcopy(opt_outdata.get("species")) orig_charge = copy.deepcopy(opt_outdata.get("charge")) orig_multiplicity = copy.deepcopy( opt_outdata.get("multiplicity")) orig_energy = copy.deepcopy( opt_outdata.get("final_energy")) first = False if (opt_outdata["structure_change"] == "unconnected_fragments" and not opt_outdata["completion"]): print( "Unstable molecule broke into unconnected fragments which failed to optimize! Exiting..." ) break else: freq_QCInput = QCInput( molecule=opt_outdata.get( "molecule_from_optimized_geometry"), rem=orig_freq_rem, opt=orig_opt_input.opt, pcm=orig_opt_input.pcm, solvent=orig_opt_input.solvent, smx=orig_opt_input.smx, ) freq_QCInput.write_file(input_file) yield (QCJob(qchem_command=qchem_command, multimode=multimode, input_file=input_file, output_file=output_file, qclog_file=qclog_file, suffix=".freq_" + str(ii), backup=first, **QCJob_kwargs)) outdata = QCOutput(output_file + ".freq_" + str(ii)).data errors = outdata.get("errors") if len(errors) != 0: raise AssertionError( "No errors should be encountered while flattening frequencies!" ) if outdata.get("frequencies")[0] > 0.0: print("All frequencies positive!") if opt_outdata.get("final_energy") > orig_energy: print( "WARNING: Energy increased during frequency flattening!" ) break else: hist = {} hist["molecule"] = copy.deepcopy( outdata.get("initial_molecule")) hist["geometry"] = copy.deepcopy( outdata.get("initial_geometry")) hist["frequencies"] = copy.deepcopy( outdata.get("frequencies")) hist["frequency_mode_vectors"] = copy.deepcopy( outdata.get("frequency_mode_vectors")) hist["num_neg_freqs"] = sum( 1 for freq in outdata.get("frequencies") if freq < 0) hist["energy"] = copy.deepcopy( opt_outdata.get("final_energy")) hist["index"] = len(history) hist["children"] = [] history.append(hist) ref_mol = history[-1]["molecule"] geom_to_perturb = history[-1]["geometry"] negative_freq_vecs = history[-1][ "frequency_mode_vectors"][0] reversed_direction = False standard = True # If we've found one or more negative frequencies in two consecutive iterations, let's dig in # deeper: if len(history) > 1: # Start by finding the latest iteration's parent: if history[-1]["index"] in history[-2]["children"]: parent_hist = history[-2] history[-1]["parent"] = parent_hist["index"] elif history[-1]["index"] in history[-3][ "children"]: parent_hist = history[-3] history[-1]["parent"] = parent_hist["index"] else: raise AssertionError( "ERROR: your parent should always be one or two iterations behind you! Exiting..." ) # if the number of negative frequencies has remained constant or increased from parent to # child, if (history[-1]["num_neg_freqs"] >= parent_hist["num_neg_freqs"]): # check to see if the parent only has one child, aka only the positive perturbation has # been tried, # in which case just try the negative perturbation from the same parent if len(parent_hist["children"]) == 1: ref_mol = parent_hist["molecule"] geom_to_perturb = parent_hist["geometry"] negative_freq_vecs = parent_hist[ "frequency_mode_vectors"][0] reversed_direction = True standard = False parent_hist["children"].append( len(history)) # If the parent has two children, aka both directions have been tried, then we have to # get creative: elif len(parent_hist["children"]) == 2: # If we're dealing with just one negative frequency, if parent_hist["num_neg_freqs"] == 1: make_good_child_next_parent = False if (history[parent_hist["children"] [0]]["energy"] < history[-1]["energy"]): good_child = copy.deepcopy(history[ parent_hist["children"][0]]) else: good_child = copy.deepcopy( history[-1]) if good_child["num_neg_freqs"] > 1: raise Exception( "ERROR: Child with lower energy has more negative frequencies! " "Exiting...") elif (good_child["energy"] < parent_hist["energy"]): make_good_child_next_parent = True elif (vector_list_diff( good_child[ "frequency_mode_vectors"] [0], parent_hist[ "frequency_mode_vectors"] [0], ) > 0.2): make_good_child_next_parent = True else: raise Exception( "ERROR: Good child not good enough! Exiting..." ) if make_good_child_next_parent: good_child["index"] = len(history) history.append(good_child) ref_mol = history[-1]["molecule"] geom_to_perturb = history[-1][ "geometry"] negative_freq_vecs = history[-1][ "frequency_mode_vectors"][0] else: raise Exception( "ERROR: Can't deal with multiple neg frequencies yet! Exiting..." ) else: raise AssertionError( "ERROR: Parent cannot have more than two childen! Exiting..." ) # Implicitly, if the number of negative frequencies decreased from parent to child, # continue normally. if standard: history[-1]["children"].append(len(history)) min_molecule_perturb_scale = 0.1 scale_grid = 10 perturb_scale_grid = ( max_molecule_perturb_scale - min_molecule_perturb_scale) / scale_grid structure_successfully_perturbed = False for molecule_perturb_scale in np.arange( max_molecule_perturb_scale, min_molecule_perturb_scale, -perturb_scale_grid, ): new_coords = perturb_coordinates( old_coords=geom_to_perturb, negative_freq_vecs=negative_freq_vecs, molecule_perturb_scale=molecule_perturb_scale, reversed_direction=reversed_direction, ) new_molecule = Molecule( species=orig_species, coords=new_coords, charge=orig_charge, spin_multiplicity=orig_multiplicity, ) if check_connectivity: structure_successfully_perturbed = ( check_for_structure_changes( ref_mol, new_molecule) == "no_change") if structure_successfully_perturbed: break if not structure_successfully_perturbed: raise Exception( "ERROR: Unable to perturb coordinates to remove negative frequency without changing " "the connectivity! Exiting...") new_opt_QCInput = QCInput( molecule=new_molecule, rem=orig_opt_rem, opt=orig_opt_input.opt, pcm=orig_opt_input.pcm, solvent=orig_opt_input.solvent, smx=orig_opt_input.smx, ) new_opt_QCInput.write_file(input_file)
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