Source code for custodian.qchem.handlers

# coding: utf-8

"""
This module implements new error handlers for QChem runs.
"""

import os
from pymatgen.io.qchem.inputs import QCInput
from pymatgen.io.qchem.outputs import QCOutput
from custodian.custodian import ErrorHandler
from custodian.utils import backup

__author__ = "Samuel Blau, Brandon Wood, Shyam Dwaraknath"
__copyright__ = "Copyright 2018, The Materials Project"
__version__ = "0.1"
__maintainer__ = "Samuel Blau"
__email__ = "samblau1@gmail.com"
__status__ = "Alpha"
__date__ = "3/26/18"
__credits__ = "Xiaohui Qu"


[docs]class QChemErrorHandler(ErrorHandler): """ Master QChemErrorHandler class that handles a number of common errors that occur during QChem runs. """ is_monitor = False def __init__( self, input_file="mol.qin", output_file="mol.qout", scf_max_cycles=200, geom_max_cycles=200, ): """ Initializes the error handler from a set of input and output files. Args: input_file (str): Name of the QChem input file. output_file (str): Name of the QChem output file. scf_max_cycles (int): The max iterations to set to fix SCF failure. geom_max_cycles (int): The max iterations to set to fix geometry optimization failure. """ self.input_file = input_file self.output_file = output_file self.scf_max_cycles = scf_max_cycles self.geom_max_cycles = geom_max_cycles self.outdata = None self.errors = [] self.opt_error_history = []
[docs] def check(self): """ Checks output file for errors """ self.outdata = QCOutput(self.output_file).data self.errors = self.outdata.get("errors") self.warnings = self.outdata.get("warnings") # If we aren't out of optimization cycles, but we were in the past, reset the history if "out_of_opt_cycles" not in self.errors and len(self.opt_error_history) > 0: self.opt_error_history = [] # If we're out of optimization cycles and we have unconnected fragments, no need to handle any errors if ( "out_of_opt_cycles" in self.errors and self.outdata["structure_change"] == "unconnected_fragments" ): return False return len(self.errors) > 0
[docs] 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 "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_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}
[docs]class QChemSCFErrorHandler(ErrorHandler): """ QChem ErrorHandler class that addresses SCF non-convergence. """ is_monitor = False def __init__( self, input_file="mol.qin", output_file="mol.qout", rca_gdm_thresh=1.0e-3, scf_max_cycles=200, ): """ Initializes the error handler from a set of input and output files. Args: input_file (str): Name of the QChem input file. output_file (str): Name of the QChem output file. rca_gdm_thresh (float): The threshold for the prior scf algorithm. If last deltaE is larger than the threshold try RCA_DIIS first, else, try DIIS_GDM first. scf_max_cycles (int): The max iterations to set to fix SCF failure. """ self.input_file = input_file self.output_file = output_file self.scf_max_cycles = scf_max_cycles self.qcinp = QCInput.from_file(self.input_file) self.outdata = None self.errors = None
[docs] def check(self): """ Checks output file for errors """ self.outdata = QCOutput(self.output_file).data self.errors = self.outdata.get("errors") return len(self.errors) > 0
[docs] def correct(self): """ Corrects errors, but it hasn't been implemented yet """ print("This hasn't been implemented yet!") return {"errors": self.errors, "actions": None}