def model_wrapper(input_data: Dict[str, Any], model: 'BaseModel') -> 'BaseModel': """ Wrap input data in the given model, or return a controlled error """ try: if isinstance(input_data, dict): input_data = model(**input_data) elif isinstance(input_data, model): input_data = input_data.copy() else: raise KeyError("Input type of {} not understood.".format( type(model))) # Older QCElemental compat try: input_data.extras except AttributeError: input_data = input_data.copy(update={"extras": {}}) except Exception: input_data = 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 input_data
def test_repr_failed_op(): fail_op = FailedOperation(error=ComputeError(error_type="random_error", error_message="this is bad")) assert ( str(fail_op) == """FailedOperation(error=ComputeError(error_type='random_error', error_message='this is bad'))""" )
def update_services(self) -> int: """Runs through all active services and examines their current status.""" # Grab current services current_services = self.storage.get_services(status="RUNNING")["data"] # Grab new services if we have open slots open_slots = max(0, self.max_active_services - len(current_services)) if open_slots > 0: new_services = self.storage.get_services(status="WAITING", limit=open_slots)["data"] current_services.extend(new_services) if len(new_services): self.logger.info(f"Starting {len(new_services)} new services.") self.logger.debug(f"Updating {len(current_services)} services.") # Loop over the services and iterate running_services = 0 completed_services = [] for data in current_services: # TODO HACK: remove task_id from 'output'. This is contained in services # created in previous versions. Doing this now, but should do a db migration # at some point if "output" in data: data["output"].pop("task_id", None) # Attempt to iteration and get message try: service = construct_service(self.storage, self.logger, data) finished = service.iterate() except Exception: error_message = "FractalServer Service Build and Iterate Error:\n{}".format(traceback.format_exc()) self.logger.error(error_message) service.status = "ERROR" service.error = ComputeError(error_type="iteration_error", error_message=error_message) finished = False self.storage.update_services([service]) # Mark procedure and service as error if service.status == "ERROR": self.storage.update_service_status("ERROR", id=service.id) if finished is not False: # Add results to procedures, remove complete_ids completed_services.append(service) else: running_services += 1 if len(completed_services): self.logger.info(f"Completed {len(completed_services)} services.") self.logger.debug(f"Done updating services.") # Add new procedures and services self.storage.services_completed(completed_services) return running_services
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 test_repr_compute_error(): ce = ComputeError(error_type="random_error", error_message="this is bad") assert "random_error" in str(ce) assert "random_error" in repr(ce)
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 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 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 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 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 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(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 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)