def acquire_complete(self) -> Dict[str, Any]: ret = {} # Pull out completed results that match our queue ids cursor = self.client.launches.find( { "fw_id": { "$in": list(self.queue.keys()) }, "state": { "$in": ["COMPLETED", "FIZZLED"] }, }, { "action.stored_data.fw_results": True, "action.stored_data._task.args": True, "action.stored_data._exception": True, "_id": False, "fw_id": True, "state": True }) for tmp_data in cursor: key = self.queue.pop(tmp_data["fw_id"]) if tmp_data["state"] == "COMPLETED": key_data = tmp_data["action"]["stored_data"]["fw_results"] if key_data['success']: # Cast dict to the Result or Optimization based on the schema ret[key] = schema_mapper[key_data['schema_name']]( **key_data) else: ret[key] = FailedOperation(**key_data) else: blob = tmp_data["action"]["stored_data"]["_task"]["args"][0] msg = tmp_data["action"]["stored_data"]["_exception"][ "_stacktrace"] blob["error_message"] = msg blob["success"] = False key_data = blob ret[key] = FailedOperation(**key_data) return ret
def compute(self, input_data: 'ResultInput', config: 'JobConfig') -> 'Result': self.found(raise_error=True) # Set up the job input_data = input_data.copy().dict() input_data["success"] = False output_data = run_json(input_data) if output_data["success"]: return Result(**output_data) return FailedOperation( success=output_data.pop("success", False), error=output_data.pop("error"), input_data=output_data)
def _get_future(future): try: return future.result() except Exception as e: msg = "Caught Executor Error:\n" + traceback.format_exc() ret = FailedOperation( **{ "success": False, "error": { "error_type": e.__class__.__name__, "error_message": msg } }) return ret
def _get_future(future): # if future.exception() is None: # This always seems to return None try: return future.result() except Exception as e: msg = "Caught Parsl Error:\n" + traceback.format_exc() ret = FailedOperation( **{ "success": False, "error": { "error_type": e.__class__.__name__, "error_message": msg } }) return ret
def compute(self, input_data: 'ResultInput', config: 'JobConfig') -> 'Result': if not which('dftd3', return_bool=True): raise ImportError("Could not find dftd3 in the envvar path.") # Setup the job input_data = input_data.copy().dict() input_data["success"] = False output_data = run_json(input_data) if output_data["success"]: return Result(**output_data) return FailedOperation(success=output_data.pop("success", False), error=output_data.pop("error"), input_data=output_data)
def model_wrapper(input_data, model): """ Wrap input data in the given model, or return a controlled error """ try: if isinstance(input_data, dict): input_data = model(**input_data) except Exception: failure = FailedOperation( input_data=input_data, success=False, error=ComputeError( error_type="input_error", error_message=( "Input data could not be processed correctly:\n" + traceback.format_exc()))) return failure return input_data
def compute(self, input_model: "AtomicInput", config: "TaskConfig") -> "AtomicResult": self.found(raise_error=True) job_inputs = self.build_input(input_model, config) success, dexe = self.execute(job_inputs) if success: dexe["outfiles"]["stdout"] = dexe["stdout"] dexe["outfiles"]["stderr"] = dexe["stderr"] output_model = self.parse_output(dexe["outfiles"], input_model) else: output_model = FailedOperation( success=False, error={"error_type": "execution_error", "error_message": dexe["stderr"]}, input_data=input_model.dict(), ) return output_model
def compute(self, input_data: 'ResultInput', config: 'JobConfig') -> 'Result': """ Run Molpro """ # Check if Molpro executable is found self.found(raise_error=True) # Check Molpro version if parse_version(self.get_version()) < parse_version("2018.1"): raise TypeError("Molpro version '{}' not supported".format( self.get_version())) # Setup the job job_inputs = self.build_input(input_data, config) # Run Molpro exe_success, proc = execute( job_inputs["commands"], infiles=job_inputs["infiles"], outfiles=["dispatch.out", "dispatch.xml", "dispatch.wfu"], scratch_location=job_inputs["scratch_location"], timeout=None) # Determine whether the calculation succeeded output_data = {} if not exe_success: output_data["success"] = False output_data["error"] = { "error_type": "internal_error", "error_message": proc["stderr"] } return FailedOperation(success=output_data.pop("success", False), error=output_data.pop("error"), input_data=output_data) # If execution succeeded, collect results result = self.parse_output(proc["outfiles"], input_data) return result
def compute(self, input_model: AtomicInput, config: "TaskConfig") -> "AtomicResult": self.found(raise_error=True) verbose = 1 print_jobrec(f"[1] {self.name} RESULTINPUT PRE-PLANT", input_model.dict(), verbose >= 3) job_inputs = self.qcdb_build_input(input_model, config) print_jobrec(f"[2] {self.name}REC PRE-ENGINE", job_inputs, verbose >= 4) # 'NWCHEM_OMP_NUM_CORES': os.environ.get('NWCHEM_OMP_NUM_CORES'), success, dexe = self.execute(job_inputs) stdin = job_inputs["infiles"]["nwchem.nw"] print_jobrec(f"[3] {self.name}REC POST-ENGINE", dexe, verbose >= 4) if "There is an error in the input file" in dexe["stdout"]: raise InputError(error_stamp(stdin, dexe["stdout"], dexe["stderr"])) if "not compiled" in dexe["stdout"]: # recoverable with a different compilation with optional modules raise InputError(error_stamp(stdin, dexe["stdout"], dexe["stderr"])) if success: dexe["outfiles"]["stdout"] = dexe["stdout"] dexe["outfiles"]["stderr"] = dexe["stderr"] dexe["outfiles"]["input"] = stdin output_model = self.parse_output(dexe["outfiles"], input_model) print_jobrec(f"[4a] {self.name} RESULT POST-HARVEST", output_model.dict(), verbose >= 5) output_model = self.qcdb_post_parse_output(input_model, output_model) print_jobrec(f"[4] {self.name} RESULT POST-POST-HARVEST", output_model.dict(), verbose >= 2) else: ## Check if any of the errors are known #for error in all_errors: # error.detect_error(dexe) output_model = FailedOperation( success=False, error={ "error_type": "execution_error", "error_message": error_stamp(stdin, dexe["stdout"], dexe["stderr"]), }, input_data=input_model.dict(), ) return output_model
def compute( self, input_data: "OptimizationInput", config: "TaskConfig" ) -> Union["OptimizationResult", "FailedOperation"]: try: import berny except ModuleNotFoundError: raise ModuleNotFoundError("Could not find Berny in the Python path.") # Get berny version from the installed package, use setuptools' # pkg_resources for python < 3.8 if sys.version_info >= (3, 8): from importlib.metadata import distribution else: from pkg_resources import get_distribution as distribution berny_version = distribution("pyberny").version # Berny uses the stdlib logging module and by default uses per-module # loggers. For QCEngine, we create one logger per BernyProcedure # instance, by using the instance's id(), and send all logging messages # to a string stream log_stream = StringIO() log = logging.getLogger(f"{__name__}.{id(self)}") log.addHandler(logging.StreamHandler(log_stream)) log.setLevel("INFO") input_data = input_data.dict() geom_qcng = input_data["initial_molecule"] comput = {**input_data["input_specification"], "molecule": geom_qcng} program = input_data["keywords"].pop("program") trajectory = [] output_data = input_data.copy() try: # Pyberny uses angstroms for the Cartesian geometry, but atomic # units for everything else, including the gradients (hartree/bohr). geom_berny = berny.Geometry(geom_qcng["symbols"], geom_qcng["geometry"] / berny.angstrom) opt = berny.Berny(geom_berny, logger=log, **input_data["keywords"]) for geom_berny in opt: geom_qcng["geometry"] = np.stack(geom_berny.coords * berny.angstrom) ret = qcengine.compute(comput, program) if ret.success: trajectory.append(ret.dict()) opt.send((ret.properties.return_energy, ret.return_result)) else: # qcengine.compute returned FailedOperation raise UnknownError("Gradient computation failed") except UnknownError: error = ret.error.dict() # ComputeError except Exception: error = {"error_type": "unknown", "error_message": f"Berny error:\n{traceback.format_exc()}"} else: output_data["success"] = True output_data.update( { "schema_name": "qcschema_optimization_output", "final_molecule": trajectory[-1]["molecule"], "energies": [r["properties"]["return_energy"] for r in trajectory], "trajectory": trajectory, "provenance": {"creator": "Berny", "routine": "berny.Berny", "version": berny_version}, "stdout": log_stream.getvalue(), # collect logged messages } ) return OptimizationResult(**output_data) return FailedOperation(input_data=input_data, error=error)
def compute(self, input_model: 'ResultInput', config: 'JobConfig') -> 'Result': """ Runs Psi4 in API mode """ self.found(raise_error=True) # Setup the job input_data = input_model.json_dict() input_data["nthreads"] = config.ncores input_data["memory"] = int(config.memory * 1024 * 1024 * 1024 * 0.95) # Memory in bytes input_data["success"] = False input_data["return_output"] = True if input_data["schema_name"] == "qcschema_input": input_data["schema_name"] = "qc_schema_input" if config.scratch_directory: input_data["scratch_location"] = config.scratch_directory if parse_version(self.get_version()) > parse_version("1.2"): caseless_keywords = { k.lower(): v for k, v in input_model.keywords.items() } if (input_model.molecule.molecular_multiplicity != 1) and ("reference" not in caseless_keywords): input_data["keywords"]["reference"] = "uhf" # Execute the program success, output = execute([which("psi4"), "--json", "data.json"], {"data.json": json.dumps(input_data)}, ["data.json"]) if success: output_data = json.loads(output["outfiles"]["data.json"]) if "extras" not in output_data: output_data["extras"] = {} output_data["extras"]["local_qcvars"] = output_data.pop( "psi4:qcvars", None) if output_data["success"] is False: if "error_message" not in output_data["error"]: # older c. 1.3 message-only run_json output_data["error"] = { "error_type": "internal_error", "error_message": output_data["error"] } else: output_data = input_data output_data["error"] = { "error_type": "execution_error", "error_message": output["stderr"] } else: raise TypeError("Psi4 version '{}' not understood.".format( self.get_version())) # Reset the schema if required output_data["schema_name"] = "qcschema_output" # Dispatch errors, PSIO Errors are not recoverable for future runs if output_data["success"] is False: if "PSIO Error" in output_data["error"]: raise ValueError(output_data["error"]) # Move several pieces up a level if output_data["success"]: output_data["provenance"]["memory"] = round( output_data.pop("memory") / (1024**3), 3) # Move back to GB output_data["provenance"]["nthreads"] = output_data.pop("nthreads") output_data["stdout"] = output_data.pop("raw_output", None) # Delete keys output_data.pop("return_output", None) output_data.pop("scratch_location", None) return Result(**output_data) else: return FailedOperation(success=output_data.pop("success", False), error=output_data.pop("error"), input_data=output_data)
def compute_procedure(input_data: Union[Dict[str, Any], 'BaseModel'], procedure: str, raise_error: bool = False, local_options: Optional[Dict[str, str]] = None, return_dict: bool = False) -> 'BaseModel': """Runs a procedure (a collection of the quantum chemistry executions) Parameters ---------- input_data : dict or qcelemental.models.OptimizationInput A JSON input specific to the procedure executed in dictionary or model from QCElemental.models procedure : {"geometric"} The name of the procedure to run raise_error : bool, option Determines if compute should raise an error or not. local_options : dict, optional A dictionary of local configuration options return_dict : bool, optional, default True Returns a dict instead of qcelemental.models.ResultInput Returns ------ dict, Optimization, FailedOperation A QC Schema representation of the requested output, type depends on return_dict key. """ procedure = procedure.lower() if procedure not in list_all_procedures(): input_data = FailedOperation( input_data=input_data, error=ComputeError( error_type="not_registered", error_message="QCEngine Call Error:\n" "Procedure {} is not registered with QCEngine".format( procedure))) elif procedure not in list_available_procedures(): input_data = FailedOperation( input_data=input_data, error=ComputeError( error_type="not_available", error_message="QCEngine Call Error:\n" "Procedure {} is registered with QCEngine, but cannot be found" .format(procedure))) error = _process_failure_and_return(input_data, return_dict, raise_error) if error: return error # Grab the executor and build the input model executor = get_procedure(procedure) config = get_config(local_options=local_options) input_data = executor.build_input_model(input_data) error = _process_failure_and_return(input_data, return_dict, raise_error) if error: return error # Run the procedure with compute_wrapper(capture_output=False) as metadata: # Create a base output data in case of errors output_data = input_data.copy() # lgtm [py/multiple-definition] output_data = executor.compute(input_data, config) return handle_output_metadata(output_data, metadata, raise_error=raise_error, return_dict=return_dict)
def handle_output_metadata( output_data: Union[Dict[str, Any], 'BaseModel'], metadata: Dict[str, Any], raise_error: bool = False, return_dict: bool = True) -> Union[Dict[str, Any], 'BaseModel']: """ Fuses general metadata and output together. Returns ------- result : dict or pydantic.models.Result Output type depends on return_dict or a dict if an error was generated in model construction """ if isinstance(output_data, dict): output_fusion = output_data # Error handling else: output_fusion = output_data.dict() # Do not override if computer generates output_fusion["stdout"] = output_fusion.get("stdout", None) or metadata["stdout"] output_fusion["stderr"] = output_fusion.get("stderr", None) or metadata["stderr"] if metadata["success"] is not True: output_fusion["success"] = False output_fusion["error"] = { "error_type": metadata["error_type"], "error_message": metadata["error_message"] } # Raise an error if one exists and a user requested a raise if raise_error and (output_fusion["success"] is not True): msg = "stdout:\n{}".format(output_fusion["stdout"]) msg += "\nstderr:\n{}".format(output_fusion["stderr"]) LOGGER.info(msg) raise ValueError(output_fusion["error"]["error_message"]) # Fill out provenance datadata provenance_augments = get_provenance_augments() provenance_augments["wall_time"] = metadata["wall_time"] if "provenance" in output_fusion: output_fusion["provenance"].update(provenance_augments) else: # Add onto the augments with some missing info provenance_augments["creator"] = "QCEngine" provenance_augments["version"] = provenance_augments[ "qcengine_version"] output_fusion["provenance"] = provenance_augments if metadata["retries"] != 0: output_fusion["provenance"]["retries"] = metadata["retries"] # Make sure pydantic sparsity is upheld for val in ["stdout", "stderr"]: if output_fusion[val] is None: output_fusion.pop(val) # We need to return the correct objects; e.g. Results, Procedures if output_fusion["success"]: # This will only execute if everything went well ret = output_data.__class__(**output_fusion) else: # Should only be reachable on failures ret = FailedOperation(success=output_fusion.pop("success", False), error=output_fusion.pop("error"), input_data=output_fusion) if return_dict: return json.loads( ret.json() ) # Use Pydantic to serialize, then reconstruct as Python dict of Python Primals else: return ret
def psi4(input_model, config): """ Runs Psi4 in API mode """ from pkg_resources import parse_version def _parse_psi_version(version): if "undef" in version: raise TypeError( "Using custom build Psi4 without tags. Please `git pull origin master --tags` and recompile Psi4." ) return parse_version(version) try: import psi4 except ImportError: raise ImportError("Could not find Psi4 in the Python path.") # Setup the job input_data = input_model.copy().dict() input_data["nthreads"] = config.ncores input_data["memory"] = int(config.memory * 1024 * 1024 * 1024 * 0.95) # Memory in bytes input_data["success"] = False reset_schema = False if input_data["schema_name"] == "qcschema_input": input_data["schema_name"] = "qc_schema_input" reset_schema = True scratch = config.scratch_directory if scratch is not None: input_data["scratch_location"] = scratch psi_version = _parse_psi_version(psi4.__version__) if psi_version > parse_version("1.2"): mol = psi4.core.Molecule.from_schema(input_data) if mol.multiplicity() != 1: input_data["keywords"]["reference"] = "uks" output_data = psi4.json_wrapper.run_json(input_data) else: raise TypeError( "Psi4 version '{}' not understood.".format(psi_version)) if reset_schema: output_data["schema_name"] = "qcschema_input" # Dispatch errors, PSIO Errors are not recoverable for future runs if output_data["success"] is False: if "PSIO Error" in output_data["error"]: raise ValueError(output_data["error"]) # Move several pieces up a level if output_data["success"]: output_data["provenance"]["memory"] = round( input_data["memory"] / (1024**3), 3) output_data["provenance"]["nthreads"] = input_data["nthreads"] del output_data["memory"], input_data["nthreads"] return Result(**output_data) return FailedOperation(success=output_data.pop("success", False), error=output_data.pop("error"), input_data=output_data)
def compute(self, input_data: 'ResultInput', config: 'JobConfig') -> 'Result': """ Runs RDKit in FF typing """ try: import rdkit from rdkit import Chem from rdkit.Chem import AllChem except ModuleNotFoundError: raise ModuleNotFoundError( "Could not find RDKit in the Python path.") # Failure flag ret_data = {"success": False} # Build the Molecule jmol = input_data.molecule # Handle errors if abs(jmol.molecular_charge) > 1.e-6: ret_data["error"] = ComputeError( error_type="input_error", error_message= "run_rdkit does not currently support charged molecules") return FailedOperation(input_data=input_data.dict(), **ret_data) if not jmol.connectivity: # Check for empty list ret_data["error"] = ComputeError( error_type="input_error", error_message= "run_rdkit molecule must have a connectivity graph") return FailedOperation(input_data=input_data.dict(), **ret_data) # Build out the base molecule base_mol = Chem.Mol() rw_mol = Chem.RWMol(base_mol) for sym in jmol.symbols: rw_mol.AddAtom(Chem.Atom(sym.title())) # Add in connectivity bond_types = { 1: Chem.BondType.SINGLE, 2: Chem.BondType.DOUBLE, 3: Chem.BondType.TRIPLE } for atom1, atom2, bo in jmol.connectivity: rw_mol.AddBond(atom1, atom2, bond_types[bo]) mol = rw_mol.GetMol() # Write out the conformer natom = len(jmol.symbols) conf = Chem.Conformer(natom) bohr2ang = ureg.conversion_factor("bohr", "angstrom") for line in range(natom): conf.SetAtomPosition(line, (bohr2ang * jmol.geometry[line, 0], bohr2ang * jmol.geometry[line, 1], bohr2ang * jmol.geometry[line, 2])) # yapf: disable mol.AddConformer(conf) Chem.rdmolops.SanitizeMol(mol) if input_data.model.method.lower() == "uff": ff = AllChem.UFFGetMoleculeForceField(mol) all_params = AllChem.UFFHasAllMoleculeParams(mol) else: ret_data["error"] = ComputeError( error_type="input_error", error_message="run_rdkit can only accepts UFF methods") return FailedOperation(input_data=input_data.dict(), **ret_data) if all_params is False: ret_data["error"] = ComputeError( error_type="input_error", error_message= "run_rdkit did not match all parameters to molecule") return FailedOperation(input_data=input_data.dict(), **ret_data) ff.Initialize() ret_data["properties"] = { "return_energy": ff.CalcEnergy() * ureg.conversion_factor("kJ / mol", "hartree") } if input_data.driver == "energy": ret_data["return_result"] = ret_data["properties"]["return_energy"] elif input_data.driver == "gradient": coef = ureg.conversion_factor("kJ / mol", "hartree") * ureg.conversion_factor( "angstrom", "bohr") ret_data["return_result"] = [x * coef for x in ff.CalcGrad()] else: ret_data["error"] = ComputeError( error_type="input_error", error_message="run_rdkit did not understand driver method " "'{}'.".format(ret_data["driver"])) return FailedOperation(input_data=input_data.dict(), **ret_data) ret_data["provenance"] = Provenance( creator="rdkit", version=rdkit.__version__, routine="rdkit.Chem.AllChem.UFFGetMoleculeForceField") ret_data["schema_name"] = "qcschema_output" ret_data["success"] = True # Form up a dict first, then sent to BaseModel to avoid repeat kwargs which don't override each other return Result(**{**input_data.dict(), **ret_data})
def torchani(input_data, config): """ Runs TorchANI in FF typing """ import numpy as np try: import torch except ImportError: raise ImportError("Could not find PyTorch in the Python path.") try: import torchani except ImportError: raise ImportError("Could not find TorchANI in the Python path.") device = torch.device('cpu') builtin = torchani.neurochem.Builtins() # Failure flag ret_data = {"success": False} # Build model model = get_model(input_data.model.method) if model is False: ret_data["error"] = ComputeError( error_type="input_error", error_message="run_torchani only accepts the ANI1 method.") return FailedOperation(input_data=input_data.dict(), **ret_data) # Build species species = "".join(input_data.molecule.symbols) unknown_sym = set(species) - {"H", "C", "N", "O"} if unknown_sym: ret_data["error"] = ComputeError( error_type="input_error", error_message="The '{}' model does not support symbols: {}.". format(input_data.model.method, unknown_sym)) return FailedOperation(input_data=input_data.dict(), **ret_data) species = builtin.consts.species_to_tensor(species).to(device).unsqueeze(0) # Build coord array geom_array = input_data.molecule.geometry.reshape( 1, -1, 3) * ureg.conversion_factor("bohr", "angstrom") coordinates = torch.tensor(geom_array.tolist(), requires_grad=True, device=device) _, energy = model((species, coordinates)) ret_data["properties"] = {"return_energy": energy.item()} if input_data.driver == "energy": ret_data["return_result"] = ret_data["properties"]["return_energy"] elif input_data.driver == "gradient": derivative = torch.autograd.grad(energy.sum(), coordinates)[0].squeeze() ret_data["return_result"] = np.asarray( derivative * ureg.conversion_factor("angstrom", "bohr")).ravel().tolist() else: ret_data["error"] = ComputeError( error_type="input_error", error_message="run_torchani did not understand driver method '{}'." .format(input_data.driver)) return FailedOperation(input_data=input_data.dict(), **ret_data) ret_data["provenance"] = Provenance( creator="torchani", version="unknown", routine='torchani.builtin.aev_computer') ret_data["schema_name"] = "qcschema_output" ret_data["success"] = True # Form up a dict first, then sent to BaseModel to avoid repeat kwargs which don't override each other return Result(**{**input_data.dict(), **ret_data})
def test_repr_failed_op(): fail_op = FailedOperation(error=ComputeError(error_type="random_error", error_message="this is bad")) assert "random_error" in str(fail_op) assert "random_error" in repr(fail_op)
def compute(self, input_data: 'ResultInput', config: 'JobConfig') -> 'Result': """ Runs TorchANI in FF typing """ # Check if existings and version self.found(raise_error=True) if parse_version(self.get_version()) < parse_version("0.5"): ret_data["error"] = ComputeError( error_type="version_error", error_message="QCEngine's TorchANI wrapper requires version 0.5 or greater.") return FailedOperation(input_data=input_data.dict(), **ret_data) import torch import numpy as np device = torch.device('cpu') # Failure flag ret_data = {"success": False} # Build model model = self.get_model(input_data.model.method) if model is False: ret_data["error"] = ComputeError( error_type="input_error", error_message="run_torchani only accepts the ANI1x or ANI1ccx method.") return FailedOperation(input_data=input_data.dict(), **ret_data) # Build species species = "".join(input_data.molecule.symbols) unknown_sym = set(species) - {"H", "C", "N", "O"} if unknown_sym: ret_data["error"] = ComputeError( error_type="input_error", error_message="The '{}' model does not support symbols: {}.".format( input_data.model.method, unknown_sym)) return FailedOperation(input_data=input_data.dict(), **ret_data) species = model.species_to_tensor(species).to(device).unsqueeze(0) # Build coord array geom_array = input_data.molecule.geometry.reshape(1, -1, 3) * ureg.conversion_factor("bohr", "angstrom") coordinates = torch.tensor(geom_array.tolist(), requires_grad=True, device=device) _, energy = model((species, coordinates)) ret_data["properties"] = {"return_energy": energy.item()} if input_data.driver == "energy": ret_data["return_result"] = ret_data["properties"]["return_energy"] elif input_data.driver == "gradient": derivative = torch.autograd.grad(energy.sum(), coordinates)[0].squeeze() ret_data["return_result"] = np.asarray( derivative * ureg.conversion_factor("angstrom", "bohr")).ravel().tolist() else: ret_data["error"] = ComputeError( error_type="input_error", error_message="run_torchani did not understand driver method '{}'.".format(input_data.driver)) return FailedOperation(input_data=input_data.dict(), **ret_data) ret_data["provenance"] = Provenance( creator="torchani", version="unknown", routine='torchani.builtin.aev_computer') ret_data["schema_name"] = "qcschema_output" ret_data["success"] = True # Form up a dict first, then sent to BaseModel to avoid repeat kwargs which don't override each other return Result(**{**input_data.dict(), **ret_data})
def compute(input_data, program, raise_error=False, capture_output=True, local_options=None, return_dict=True): """Executes a single quantum chemistry program given a QC Schema input. The full specification can be found at: http://molssi-qc-schema.readthedocs.io/en/latest/index.html# Parameters ---------- input_data : dict or qcelemental.models.ResultInput A QC Schema input specification in dictionary or model from QCElemental.models program : {"psi4", "rdkit"} The program to run the input under raise_error : bool, optional Determines if compute should raise an error or not. capture_output : bool, optional Determines if stdout/stderr should be captured. local_options : dict, optional A dictionary of local configuration options return_dict : bool, optional, default True Returns a dict instead of qcelemental.models.ResultInput Returns ------- ret : dict, Result, FailedOperation A QC Schema output, type depends on return_dict key A FailedOperation returns """ input_data = model_wrapper(input_data, ResultInput) if isinstance(input_data, FailedOperation): if return_dict: return input_data.dict() return input_data if local_options is None: local_options = {} try: input_engine_options = input_data._qcengine_local_config input_data = input_data.copy(exclude={'_qcengine_local_config'}) except AttributeError: input_engine_options = {} local_options = {**local_options, **input_engine_options} config = get_config(local_options=local_options) # Run the program with compute_wrapper(capture_output=capture_output) as metadata: output_data = input_data.copy() # Initial in case of error handling try: output_data = get_program(program)(input_data, config) except KeyError as e: output_data = FailedOperation( input_data=output_data.dict(), success=False, error=ComputeError( error_type='program_error', error_message= "QCEngine Call Error:\nProgram {} not understood." "\nError Message: {}".format(program, str(e)))) return handle_output_metadata(output_data, metadata, raise_error=raise_error, return_dict=return_dict)
def compute_procedure(input_data, procedure, raise_error=False, capture_output=True, local_options=None, return_dict=True): """Runs a procedure (a collection of the quantum chemistry executions) Parameters ---------- input_data : dict or qcelemental.models.OptimizationInput A JSON input specific to the procedure executed in dictionary or model from QCElemental.models procedure : {"geometric"} The name of the procedure to run raise_error : bool, option Determines if compute should raise an error or not. capture_output : bool, optional Determines if stdout/stderr should be captured. local_options : dict, optional A dictionary of local configuration options return_dict : bool, optional, default True Returns a dict instead of qcelemental.models.ResultInput Returns ------ dict, Optimization, FailedOperation A QC Schema representation of the requested output, type depends on return_dict key. """ input_data = model_wrapper(input_data, OptimizationInput) if isinstance(input_data, FailedOperation): if return_dict: return input_data.dict() return input_data config = get_config(local_options=local_options) # Run the procedure with compute_wrapper(capture_output=capture_output) as metadata: # Create a base output data in case of errors output_data = input_data.copy() # lgtm [py/multiple-definition] if procedure == "geometric": # Augment the input geometric_input = input_data.dict() geometric_input["input_specification"][ "_qcengine_local_config"] = config.dict() # Run the program output_data = get_module_function( procedure, "run_json.geometric_run_json")(geometric_input) output_data["schema_name"] = "qcschema_optimization_output" output_data["input_specification"].pop("_qcengine_local_config", None) output_data = Optimization(**output_data) else: output_data = FailedOperation( input_data=input_data.dict(), success=False, error=ComputeError( error_type="program_error", error_message="QCEngine Call Error:" "\nProcedure {} not understood".format(procedure))) return handle_output_metadata(output_data, metadata, raise_error=raise_error, return_dict=return_dict)
def compute(self, input_model: 'ResultInput', config: 'JobConfig') -> 'Result': """ Runs Psi4 in API mode """ try: import psi4 except ModuleNotFoundError: raise ModuleNotFoundError("Could not find Psi4 in the Python path.") # Setup the job input_model = input_model.copy().dict() input_model["nthreads"] = config.ncores input_model["memory"] = int(config.memory * 1024 * 1024 * 1024 * 0.95) # Memory in bytes input_model["success"] = False input_model["return_output"] = True if input_model["schema_name"] == "qcschema_input": input_model["schema_name"] = "qc_schema_input" scratch = config.scratch_directory if scratch is not None: input_model["scratch_location"] = scratch psi_version = self.parse_version(psi4.__version__) if psi_version > self.parse_version("1.2"): mol = psi4.core.Molecule.from_schema(input_model) if (mol.multiplicity() != 1) and ("reference" not in input_model["keywords"]): input_model["keywords"]["reference"] = "uhf" output_data = psi4.json_wrapper.run_json(input_model) if "extras" not in output_data: output_data["extras"] = {} output_data["extras"]["local_qcvars"] = output_data.pop("psi4:qcvars", None) if output_data["success"] is False: output_data["error"] = {"error_type": "internal_error", "error_message": output_data["error"]} else: raise TypeError("Psi4 version '{}' not understood.".format(psi_version)) # Reset the schema if required output_data["schema_name"] = "qcschema_output" # Dispatch errors, PSIO Errors are not recoverable for future runs if output_data["success"] is False: if "PSIO Error" in output_data["error"]: raise ValueError(output_data["error"]) # Move several pieces up a level if output_data["success"]: output_data["provenance"]["memory"] = round(output_data.pop("memory") / (1024**3), 3) # Move back to GB output_data["provenance"]["nthreads"] = output_data.pop("nthreads") output_data["stdout"] = output_data.pop("raw_output", None) # Delete keys output_data.pop("return_ouput", None) return Result(**output_data) else: return FailedOperation( success=output_data.pop("success", False), error=output_data.pop("error"), input_data=output_data)
def compute(input_data: Union[Dict[str, Any], 'ResultInput'], program: str, raise_error: bool = False, local_options: Optional[Dict[str, str]] = None, return_dict: bool = False) -> 'Result': """Executes a single quantum chemistry program given a QC Schema input. The full specification can be found at: http://molssi-qc-schema.readthedocs.io/en/latest/index.html# Parameters ---------- input_data: A QC Schema input specification in dictionary or model from QCElemental.models program: The program to execute the input with raise_error: Determines if compute should raise an error or not. capture_output: Determines if stdout/stderr should be captured. local_options: A dictionary of local configuration options return_dict: Returns a dict instead of qcelemental.models.ResultInput Returns ------- : Result A QC Schema output or type depending on return_dict key """ program = program.lower() if program not in list_all_programs(): input_data = FailedOperation( input_data=input_data, error=ComputeError( error_type="not_registered", error_message="QCEngine Call Error:\n" "Program {} is not registered with QCEngine".format(program))) elif program not in list_available_programs(): input_data = FailedOperation( input_data=input_data, error=ComputeError( error_type="not_available", error_message="QCEngine Call Error:\n" "Program {} is registered with QCEngine, but cannot be found". format(program))) error = _process_failure_and_return(input_data, return_dict, raise_error) if error: return error # Build the model and validate input_data = model_wrapper(input_data, ResultInput) error = _process_failure_and_return(input_data, return_dict, raise_error) if error: return error # Grab the executor and build the input model executor = get_program(program) # Build out local options if local_options is None: local_options = {} input_engine_options = input_data.extras.pop("_qcengine_local_config", {}) local_options = {**local_options, **input_engine_options} config = get_config(local_options=local_options) # Run the program with compute_wrapper(capture_output=False) as metadata: output_data = input_data.copy() # lgtm [py/multiple-definition] output_data = executor.compute(input_data, config) return handle_output_metadata(output_data, metadata, raise_error=raise_error, return_dict=return_dict)
def handle_output_metadata(output_data, metadata, raise_error=False, return_dict=True): """ Fuses general metadata and output together. Returns ------- result : dict or pydantic.models.Result Output type depends on return_dict or a dict if an error was generated in model construction """ if isinstance(output_data, dict): output_fusion = output_data # Error handling else: output_fusion = output_data.dict() output_fusion["stdout"] = metadata["stdout"] output_fusion["stderr"] = metadata["stderr"] if metadata["success"] is not True: output_fusion["success"] = False output_fusion["error"] = { "error_type": "meta_error", "error_message": metadata["error_message"] } # Raise an error if one exists and a user requested a raise if raise_error and (output_fusion["success"] is not True): msg = "stdout:\n" + output_fusion["stdout"] msg += "\nstderr:\n" + output_fusion["stderr"] print(msg) raise ValueError(output_fusion["error"]["error_message"]) # Fill out provenance datadata wall_time = metadata["wall_time"] provenance_augments = config.get_provenance_augments() provenance_augments["wall_time"] = wall_time if "provenance" in output_fusion: output_fusion["provenance"].update(provenance_augments) else: # Add onto the augments with some missing info provenance_augments["creator"] = "QCEngine" provenance_augments["version"] = provenance_augments[ "qcengine_version"] output_fusion["provenance"] = provenance_augments # We need to return the correct objects; e.g. Results, Procedures if output_fusion["success"]: # This will only execute if everything went well ret = output_data.__class__(**output_fusion) else: # Should only be reachable on failures ret = FailedOperation(success=output_fusion.pop("success", False), error=output_fusion.pop("error"), input_data=output_fusion) if return_dict: return json.loads( ret.json() ) # Use Pydantic to serialize, then reconstruct as Python dict of Python Primals else: return ret