Beispiel #1
0
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
Beispiel #2
0
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'))"""
    )
Beispiel #3
0
    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
Beispiel #4
0
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
Beispiel #5
0
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)
Beispiel #6
0
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)
Beispiel #7
0
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)
Beispiel #8
0
    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})
Beispiel #9
0
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})
Beispiel #10
0
    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})
Beispiel #11
0
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)
Beispiel #12
0
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)
Beispiel #13
0
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)