From 8df6aea8117ac1f7d5ddc9e8b2cde13ff8a71f63 Mon Sep 17 00:00:00 2001 From: Daniel Smith Date: Thu, 28 Feb 2019 09:36:09 -0500 Subject: [PATCH 01/13] Docs: Adds API docs --- .gitignore | 1 + docs/source/api.rst | 13 +++++++++++++ docs/source/community.rst | 1 + docs/source/conf.py | 9 ++++++++- docs/source/index.rst | 20 +++++++++++++++++--- docs/source/install.rst | 8 +++----- qcengine/compute.py | 31 ++++++++++++++++++------------- 7 files changed, 61 insertions(+), 22 deletions(-) create mode 100644 docs/source/api.rst diff --git a/.gitignore b/.gitignore index a5cb681ad..03d479d5e 100644 --- a/.gitignore +++ b/.gitignore @@ -56,6 +56,7 @@ coverage.xml # Sphinx documentation docs/_build/ +docs/source/api/ # PyBuilder target/ diff --git a/docs/source/api.rst b/docs/source/api.rst new file mode 100644 index 000000000..9f4579c37 --- /dev/null +++ b/docs/source/api.rst @@ -0,0 +1,13 @@ +============ +QCEngine API +============ + +.. automodapi:: qcengine + +.. automodapi:: qcengine.compute + +.. automodapi:: qcengine.config + +.. automodapi:: qcengine.util + +.. automodapi:: qcengine.programs diff --git a/docs/source/community.rst b/docs/source/community.rst index a42ee576f..f64489370 100644 --- a/docs/source/community.rst +++ b/docs/source/community.rst @@ -13,6 +13,7 @@ Discussion - The QCArchive GitHub repositories contain future roadmaps, current code updates, and a list of issues that are being worked and provide an excellent overview of the development status of the project. See the following links: - `QCSchema `_ + - `QCElemental `_ - `QCEngine `_ - `QCFractal `_ diff --git a/docs/source/conf.py b/docs/source/conf.py index 4571902d3..5d833dace 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -55,6 +55,8 @@ 'sphinx.ext.napoleon', 'sphinx.ext.intersphinx', 'sphinx.ext.extlinks', + 'sphinx_automodapi.automodapi', + 'sphinx_automodapi.automodsumm', ] napoleon_google_docstring = False @@ -185,7 +187,12 @@ # -- Options for intersphinx extension --------------------------------------- # Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = {'https://docs.python.org/': None} +intersphinx_mapping = {'python': ('https://docs.python.org/3.7', None), + 'numpy': ('https://docs.scipy.org/doc/numpy/', None), + 'scipy': ('https://docs.scipy.org/doc/scipy/reference/', None), + 'matplotlib': ('https://matplotlib.org/', None), + 'qcelemental': ('https://qcelemental.readthedocs.io/en/latest/', None) + } # -- Options for todo extension ---------------------------------------------- diff --git a/docs/source/index.rst b/docs/source/index.rst index e3b88a095..656c52d96 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -107,19 +107,33 @@ The QCArchive project's primary support comes from `The Molecular Sciences Softw ======== -Table of Contents -================= +Index +----- + +**Getting Started** + +* :doc:`install` +* :doc:`community` .. toctree:: - :maxdepth: 2 + :maxdepth: 1 + :hidden: :caption: Getting Started install community + +**Developer Documentationd** + +* :doc:`api` +* :doc:`changelog` +* :doc:`dev_guidelines` + .. toctree:: :maxdepth: 2 :caption: Developer Documentation + api changelog dev_guidelines diff --git a/docs/source/install.rst b/docs/source/install.rst index 5f4ca19f5..bff089dd8 100644 --- a/docs/source/install.rst +++ b/docs/source/install.rst @@ -10,9 +10,7 @@ You can update qcengine using `conda `_:: conda install qcengine -c conda-forge -This installs qcengine and the NumPy dependancy. - -The qcengine package is maintained on the +This command installs qcengine and its dependencies. The qcengine package is maintained on the `conda-forge channel `_. @@ -42,7 +40,7 @@ or use ``pip`` for a local install:: Test ---- -Test qcengine with ``py.test``:: +Test qcengine with ``pytest``:: cd qcengine - py.test + pytest -v diff --git a/qcengine/compute.py b/qcengine/compute.py index ab451f20f..4bb535511 100644 --- a/qcengine/compute.py +++ b/qcengine/compute.py @@ -1,15 +1,21 @@ """ Integrates the computes together """ - +from typing import Any, Dict, Optional, Union from qcelemental.models import ComputeError, FailedOperation, Optimization, OptimizationInput, ResultInput from .config import get_config from .programs import get_program from .util import compute_wrapper, get_module_function, handle_output_metadata, model_wrapper +__all__ = ["compute", "compute_procedure"] -def compute(input_data, program, raise_error=False, capture_output=True, local_options=None, return_dict=True): +def compute(input_data: Union[Dict[str, Any], 'ResultInput'], + program: str, + raise_error: bool=False, + capture_output: bool=True, + local_options: Optional[Dict[str, str]]=None, + return_dict: bool=True) -> 'Result': """Executes a single quantum chemistry program given a QC Schema input. The full specification can be found at: @@ -17,24 +23,23 @@ def compute(input_data, program, raise_error=False, capture_output=True, local_o Parameters ---------- - input_data : dict or qcelemental.models.ResultInput + input_data: 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 + program: + The program to execute the input with + raise_error: Determines if compute should raise an error or not. - capture_output : bool, optional + capture_output: Determines if stdout/stderr should be captured. - local_options : dict, optional + local_options: A dictionary of local configuration options - return_dict : bool, optional, default True + return_dict: 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 + : Result + A QC Schema output or type depending on return_dict key """ @@ -119,7 +124,7 @@ def compute_procedure(input_data, # Older QCElemental compat, can be removed in v0.6 if "extras" not in geometric_input["input_specification"]: - geometric_input["input_specification"]["extras"] = {} + geometric_input["input_specification"]["extras"] = {} geometric_input["input_specification"]["extras"]["_qcengine_local_config"] = config.dict() From 6279155ce17329c80cd834cbc4ca0bf8d89f4929 Mon Sep 17 00:00:00 2001 From: Daniel Smith Date: Thu, 28 Feb 2019 10:40:24 -0500 Subject: [PATCH 02/13] Docs: Starts environment stubs --- docs/source/environment.rst | 54 +++++++++++++ docs/source/index.rst | 157 +++++++++++++++++------------------- 2 files changed, 128 insertions(+), 83 deletions(-) create mode 100644 docs/source/environment.rst diff --git a/docs/source/environment.rst b/docs/source/environment.rst new file mode 100644 index 000000000..cf3e8012b --- /dev/null +++ b/docs/source/environment.rst @@ -0,0 +1,54 @@ +Environment Detection +====================== + +QCEngine can inspect the current compute environment to determine the resources available to it. + +Node Description +---------------- + +QCEngine can detect node descriptions to obtain general information about the current node. + +.. code:: python + + >>> qcng.config.get_node_descriptor() + + +Config +------ + +The configuration file operated based on the current node descriptor and can be overridden: + +.. code:: python + + >>> qcng.get_config() + + + >>> qcng.get_config(local_options={"scratch_directory": "/tmp"}) + + + >>> os.environ["SCRATCH"] = "/my_scratch" + >>> qcng.get_config(local_options={"scratch_directory": "$SCRATCH"}) + + +Global Environment +------------------- + +The global environment can also be inspected directly. + +.. code:: python + + >>> qcng.config.get_global() + { + 'hostname': 'qcarchive.molssi.org', + 'memory': 5.568, + 'username': 'user', + 'ncores': 4, + 'cpuinfo': { + 'python_version': '3.6.7.final.0 (64 bit)', + 'brand': 'Intel(R) Core(TM) i7-7820HQ CPU @ 2.90GHz', + 'hz_advertised': '2.9000 GHz', + ... + }, + 'cpu_brand': 'Intel(R) Core(TM) i7-7820HQ CPU @ 2.90GHz' + } diff --git a/docs/source/index.rst b/docs/source/index.rst index 656c52d96..6b0442df2 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -16,92 +16,71 @@ A simple example of QCEngine's capabilities is as follows: .. code:: python - >>> import qcengine - - >>> job = { - "schema_name": "qc_schema_input", - "schema_version": 1, - "molecule": { - "geometry": [ - 0.0, 0.0, -0.129, - 0.0, -1.494, 1.027, - 0.0, 1.494, 1.027 - ], - "symbols": [ "O", "H", "H"], - }, - "driver": "energy", - "model": { - "method": "SCF", - "basis": "sto-3g" - }, - "keywords": { - "scf_type": "df" - } - } - - >>> qcengine.compute(job, "psi4") - { - ..., - "provenance": { - "creator": "Psi4", - "version": "1.2", - "routine": "psi4.json.run_json", - "memory": 2.235, - "nthreads": 2, - "cpu": "Intel(R) Core(TM) i7-7820HQ CPU @ 2.90GHz", - "hostname": "xxx.xxx.com", - "username": "username", - "wall_time": 0.8911292552947998 - }, - "return_result": -74.96475169985112, - "properties": { - "calcinfo_nbasis": 7, - "calcinfo_nmo": 7, - "calcinfo_nalpha": 5, - "calcinfo_nbeta": 5, - "calcinfo_natom": 3, - "scf_one_electron_energy": -121.67648822935482, - "scf_two_electron_energy": 37.91027446887428, - "nuclear_repulsion_energy": 8.80146206062943, - "scf_dipole_moment": [0.0, 0.0, 1.668684476563345], - "scf_iterations": 6, - "scf_total_energy": -74.96475169985112, - "return_energy": -74.96475169985112 - } - } - - -The QCEngine middleware can automatically determine: + >>> import qcengine as qcng + >>> import qcelemental as qcel + + >>> mol = qcel.models.Molecule.from_data(""" + O 0.0 0.000 -0.129 + H 0.0 -1.494 1.027 + H 0.0 1.494 1.027 + """) + + >>> inp = qcel.models.ResultInput( + molecule=mol, + driver="energy", + model={"method": "SCF", "basis": "sto-3g"}, + keywords={"scf_type": "df"} + ) + +These input specifications can be executed with the ``compute`` syntax along with a program specifier: + +.. code:: python + + >>> ret = qcng.compute(job, "psi4", return_dict=False) + +The results contain a complete record of the computation: + +.. code:: python + + >>> ret.return_result + -74.45994963230625 + + >>> ret.properties.scf_dipole_moment + [0.0, 0.0, 0.6635967188869244] + + >>> ret.provenance.cpu + Intel(R) Core(TM) i7-7820HQ CPU @ 2.90GHz + +Configuration Determination +--------------------------- + +In addition, QCEngine can automatically determine the following quantites: - The number of physical cores on the system and to use. - The amount of physical memory on the system and the amount to use. - The provenance of a computation (hardware, software versions, and compute resources). -- Locations of scratch disk space. -- Locations of quantum chemistry programs. +- Location of scratch disk space. +- Location of quantum chemistry programs binaries or Python modules. +Each of these options can be specified by the user as well. -QCArchive ---------- - -This module is part of the QCArchive project which sets out to answer the -the fundamental question of "How do we compile, aggregate, query, and share quantum -chemistry data to accelerate the understanding of new method performance, -fitting of novel force fields, and supporting the incredible data needs of -machine learning for computational molecular science?" +.. code:: python -The QCArchive project is made up of three primary modules: + >>> qcng.get_config() + -- `QCSchema `_ - A key/value schema for quantum chemistry. -- `QCEngine `_ - A computational middleware to provide IO to a variety of quantum chemistry programs. -- `QCFractal `_ - A distributed compute and database platform powered by QCEngine and QCSchema. + >>> qcng.get_config(local_options={"scratch_directory": "/tmp"}) + -The QCArchive project's primary support comes from `The Molecular Sciences Software Institute `_. + >>> os.environ["SCRATCH"] = "/my_scratch" + >>> qcng.get_config(local_options={"scratch_directory": "$SCRATCH"}) + ======== .. toctree:: - :maxdepth: 2 - :caption: Contents: + :maxdepth: 2 + :caption: Contents: @@ -116,12 +95,24 @@ Index * :doc:`community` .. toctree:: - :maxdepth: 1 - :hidden: - :caption: Getting Started + :maxdepth: 1 + :hidden: + :caption: Getting Started + + install + community + + +**User Interface** + +* :doc:`environment` + +.. toctree:: + :maxdepth: 1 + :hidden: + :caption: User Interface - install - community + environment **Developer Documentationd** @@ -131,9 +122,9 @@ Index * :doc:`dev_guidelines` .. toctree:: - :maxdepth: 2 - :caption: Developer Documentation + :maxdepth: 2 + :caption: Developer Documentation - api - changelog - dev_guidelines + api + changelog + dev_guidelines From 9d5d7464c0979ccbe3f7fa8e73da79cacdfd6189 Mon Sep 17 00:00:00 2001 From: Daniel Smith Date: Thu, 28 Feb 2019 11:08:15 -0500 Subject: [PATCH 03/13] Docs: Updates the README to be slightly more verbose for GitHub + PyPI quick reference --- README.md | 44 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 996622b1c..b97aadfad 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ QCEngine -============================== +======== [![Travis Build Status](https://travis-ci.org/MolSSI/QCEngine.png)](https://travis-ci.org/MolSSI/QCEngine) [![codecov](https://codecov.io/gh/MolSSI/QCEngine/branch/master/graph/badge.svg)](https://codecov.io/gh/MolSSI/QCEngine/branch/master) [![Language grade: Python](https://img.shields.io/lgtm/grade/python/g/MolSSI/QCEngine.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/MolSSI/QCEngine/context:python) @@ -19,6 +19,48 @@ Available Procedures: - [Geometric](https://github.com/leeping/geomeTRIC) +# Example + +A simple example of QCEngine's capabilities is as follows: + +```python +>>> import qcengine as qcng +>>> import qcelemental as qcel + +>>> mol = qcel.models.Molecule.from_data(""" +O 0.0 0.000 -0.129 +H 0.0 -1.494 1.027 +H 0.0 1.494 1.027 +""") + +>>> inp = qcel.models.ResultInput( + molecule=mol, + driver="energy", + model={"method": "SCF", "basis": "sto-3g"}, + keywords={"scf_type": "df"} + ) +``` + +These input specifications can be executed with the ``compute`` function along with a program specifier: + +```python +>>> ret = qcng.compute(inp, "psi4", return_dict=False) +``` + +The results contain a complete record of the computation: + + +```python +>>> ret.return_result +-74.45994963230625 + +>>> ret.properties.scf_dipole_moment +[0.0, 0.0, 0.6635967188869244] + +>>> ret.provenance.cpu +Intel(R) Core(TM) i7-7820HQ CPU @ 2.90GHz +``` + See the [documentation](https://qcengine.readthedocs.io/en/latest/) for more information. # License From 966238f43996dc2a64515d9131065b012fac7c21 Mon Sep 17 00:00:00 2001 From: Daniel Smith Date: Thu, 28 Feb 2019 11:20:54 -0500 Subject: [PATCH 04/13] Compute: Returns objects by default, warning breaking change --- qcengine/compute.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qcengine/compute.py b/qcengine/compute.py index 4bb535511..1f23efe0c 100644 --- a/qcengine/compute.py +++ b/qcengine/compute.py @@ -15,7 +15,7 @@ def compute(input_data: Union[Dict[str, Any], 'ResultInput'], raise_error: bool=False, capture_output: bool=True, local_options: Optional[Dict[str, str]]=None, - return_dict: bool=True) -> 'Result': + return_dict: bool=False) -> 'Result': """Executes a single quantum chemistry program given a QC Schema input. The full specification can be found at: From 5473be73422414e759c73b5da83a7aa489d5f9db Mon Sep 17 00:00:00 2001 From: Daniel Smith Date: Thu, 28 Feb 2019 11:21:58 -0500 Subject: [PATCH 05/13] Docs: Adds some docs on single compute --- docs/source/index.rst | 28 +++++++++- docs/source/single_compute.rst | 98 ++++++++++++++++++++++++++++++++++ 2 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 docs/source/single_compute.rst diff --git a/docs/source/index.rst b/docs/source/index.rst index 6b0442df2..d3d9c1628 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -36,7 +36,7 @@ These input specifications can be executed with the ``compute`` syntax along wit .. code:: python - >>> ret = qcng.compute(job, "psi4", return_dict=False) + >>> ret = qcng.compute(inp, "psi4") The results contain a complete record of the computation: @@ -51,6 +51,30 @@ The results contain a complete record of the computation: >>> ret.provenance.cpu Intel(R) Core(TM) i7-7820HQ CPU @ 2.90GHz +Backends +-------- + +Currently available compute backends for single results are as follow: + +- Quantum Chemistry: + + - `Psi4 `_ + +- AI Evaluation: + + - `TorchANI `_ + +- Molecule Mechanics: + + - `RDKit `_ + +In addition, several procedures are available: + +- Geometry Optimization: + + - `geomeTRIC `_ + + Configuration Determination --------------------------- @@ -105,6 +129,7 @@ Index **User Interface** +* :doc:`single_compute` * :doc:`environment` .. toctree:: @@ -112,6 +137,7 @@ Index :hidden: :caption: User Interface + single_compute environment diff --git a/docs/source/single_compute.rst b/docs/source/single_compute.rst new file mode 100644 index 000000000..66e1ac611 --- /dev/null +++ b/docs/source/single_compute.rst @@ -0,0 +1,98 @@ +Single Compute +============== + +QCEngine's primary purpose is to consume the MolSSI `QCSchema `_ and produce +QCSchema results for a variety of quantum chemistry, semiempirical, and molecular mechanics programs. Single QCSchema representation +comprises of a single ``energy``, ``gradient``, ``hessian``, or ``properties`` evaluation. + +Input Description +----------------- + +An input description has the following fields: + +- ``molecule`` - A QCSchema compliant dictionary or Molecule model. +- ``driver`` - The ``energy``, ``gradient``, ``hessian``, or ``properties`` option. +- ``model`` - A description of the evaluation model, for quantum chemistry this is typically ``method`` and ``basis``. However, + non-quantum chemistry models are often a simple ``method`` as in ``method = 'UFF'`` for forcefield evaluation. +- ``keywords`` - a dictionary of keywords to pass to the underlying program, these are program-specific keywords. + +An example input is as follows: + +.. code:: python + + >>> import qcengine as qcng + >>> import qcelemental as qcel + + >>> mol = qcel.models.Molecule.from_data(""" + O 0.0 0.000 -0.129 + H 0.0 -1.494 1.027 + H 0.0 1.494 1.027 + """) + + >>> inp = qcel.models.ResultInput( + molecule=mol, + driver="energy", + model={"method": "SCF", "basis": "sto-3g"}, + keywords={"scf_type": "df"} + ) + + +Computation +----------- + +A single computation can be evaluated with the ``compute`` function as follows: + +.. code:: python + + >>> ret = qcng.compute(inp, "psi4") + +By default the job is given resources relating to the compute environment it in; however, these variables can be overridden: + +.. code:: python + + >>> ret = qcng.compute(inp, "psi4", local_options={"memory": 2, "ncores": 3}) + + + +Results +------- + +The results contain a complete record of the computation: + +.. code:: python + + >>> ret.return_result + -74.45994963230625 + + >>> ret.properties.scf_dipole_moment + [0.0, 0.0, 0.6635967188869244] + + >>> ret.provenance.cpu + Intel(R) Core(TM) i7-7820HQ CPU @ 2.90GHz + +A short description of the fields is as follow: + +- ``return_result`` - the direct return of the ``driver`` input. That is energy and gradient for a driver ``energy`` and ``gradient`` call, respectively. +- ``properties`` - Values associated with the ``return_result`` such as the ``scf_one_electron_energy``. +- ``stdout`` - The ``stdout`` or log of a programs run. +- ``provenance`` - A description of the calling program, version, wall time, etc. + +A complete description of the input is also available in the output: + +.. code:: python + + >>> ret.driver + energy + + +Fields +------ + +A list of all fields is available through the ``fields`` property on the input and output: + +.. code:: python + + >>> ret.driver + ['molecule', 'driver', 'model', 'id', 'schema_name', 'schema_version', 'keywords', + 'extras', 'provenance', 'return_result', 'success', 'properties', 'stdout', 'stderr', 'error'] + From f55a2f1f60155fde72319cccb7a7049818390aec Mon Sep 17 00:00:00 2001 From: Daniel Smith Date: Thu, 28 Feb 2019 12:41:19 -0500 Subject: [PATCH 06/13] Objects: Always returns objects over dictionaries, fixes #20 --- qcengine/compute.py | 19 +++++++++---------- qcengine/programs/tests/test_molpro.py | 10 +++++++--- qcengine/tests/test_compute.py | 22 +++++++++------------- qcengine/tests/test_procedures.py | 24 +++++++++++------------- qcengine/util.py | 1 - 5 files changed, 36 insertions(+), 40 deletions(-) diff --git a/qcengine/compute.py b/qcengine/compute.py index 1f23efe0c..610b1e3a5 100644 --- a/qcengine/compute.py +++ b/qcengine/compute.py @@ -10,6 +10,7 @@ __all__ = ["compute", "compute_procedure"] + def compute(input_data: Union[Dict[str, Any], 'ResultInput'], program: str, raise_error: bool=False, @@ -43,7 +44,7 @@ def compute(input_data: Union[Dict[str, Any], 'ResultInput'], """ - # Validate input + # Build the model and validate input_data = model_wrapper(input_data, ResultInput, raise_error) if isinstance(input_data, FailedOperation): if return_dict: @@ -77,12 +78,11 @@ def compute(input_data: Union[Dict[str, Any], 'ResultInput'], 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): +def compute_procedure(input_data: Dict[str, Any], + 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 @@ -93,8 +93,6 @@ def compute_procedure(input_data, 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 @@ -106,6 +104,7 @@ def compute_procedure(input_data, A QC Schema representation of the requested output, type depends on return_dict key. """ + # Build the model and validate input_data = model_wrapper(input_data, OptimizationInput, raise_error) if isinstance(input_data, FailedOperation): if return_dict: @@ -115,7 +114,7 @@ def compute_procedure(input_data, config = get_config(local_options=local_options) # Run the procedure - with compute_wrapper(capture_output=capture_output) as metadata: + 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] if procedure == "geometric": diff --git a/qcengine/programs/tests/test_molpro.py b/qcengine/programs/tests/test_molpro.py index fba33e527..7e05862f6 100644 --- a/qcengine/programs/tests/test_molpro.py +++ b/qcengine/programs/tests/test_molpro.py @@ -21,8 +21,12 @@ def test_molpro_output_parser(test_case): data = molpro_info.get_test_data(test_case) inp = qcel.models.ResultInput.parse_raw(data["input.json"]) - output = get_program('molpro').parse_output(data, inp) + output = get_program('molpro').parse_output(data, inp).dict() + output.pop("provenance", None) - output_ref = qcel.models.Result.parse_raw(data["output.json"]) + output_ref = qcel.models.Result.parse_raw(data["output.json"]).dict() + output_ref.pop("provenance", None) - assert compare_recursive(output_ref.dict(), output.dict()) \ No newline at end of file + # TODO add `skip` to compare_recusive + check = compare_recursive(output_ref, output) + assert check, check \ No newline at end of file diff --git a/qcengine/tests/test_compute.py b/qcengine/tests/test_compute.py index d57242b06..b2857b9f9 100644 --- a/qcengine/tests/test_compute.py +++ b/qcengine/tests/test_compute.py @@ -15,8 +15,8 @@ def test_missing_key(): ret = qcng.compute({"hello": "hi"}, "bleh") - assert ret["success"] is False - assert "hello" in ret or ("input_data" in ret and "hello" in ret["input_data"]) + assert ret.success is False + assert "hello" in ret.input_data def test_missing_key_raises(): @@ -32,7 +32,7 @@ def test_psi4_task(): json_data["model"] = {"method": "SCF", "basis": "sto-3g"} json_data["keywords"] = {"scf_type": "df"} - ret = qcng.compute(json_data, "psi4", raise_error=True, capture_output=False) + ret = qcng.compute(json_data, "psi4", raise_error=True, capture_output=False, return_dict=True) assert ret["driver"] == "energy" assert "provenance" in ret @@ -79,7 +79,7 @@ def test_rdkit_task(): ret = qcng.compute(json_data, "rdkit", raise_error=True) - assert ret["success"] is True + assert ret.success is True @testing.using_rdkit @@ -92,9 +92,8 @@ def test_rdkit_connectivity_error(): del json_data["molecule"]["connectivity"] ret = qcng.compute(json_data, "rdkit") - assert ret["success"] is False - assert "error" in ret - assert "connectivity" in ret["error"]["error_message"] + assert ret.success is False + assert "connectivity" in ret.error.error_message with pytest.raises(ValueError): qcng.compute(json_data, "rdkit", raise_error=True) @@ -110,9 +109,8 @@ def test_torchani_task(): ret = qcng.compute(json_data, "torchani", raise_error=True) - assert ret["success"] is True - assert ret["driver"] == "gradient" - assert "provenance" in ret + assert ret.success is True + assert ret.driver == "gradient" @testing.using_dftd3 @@ -122,9 +120,7 @@ def test_dftd3_task(): json_data["driver"] = "energy" json_data["model"] = {"method": "b3lyp-d3", "basis": ""} - ret = qcng.compute(json_data, "dftd3", raise_error=True, capture_output=True) - import pprint - pprint.pprint(ret) + ret = qcng.compute(json_data, "dftd3", raise_error=True, capture_output=True, return_dict=True) assert ret["driver"] == "energy" assert "provenance" in ret diff --git a/qcengine/tests/test_procedures.py b/qcengine/tests/test_procedures.py index 9116a49d6..75aaf25ee 100644 --- a/qcengine/tests/test_procedures.py +++ b/qcengine/tests/test_procedures.py @@ -58,10 +58,9 @@ def test_geometric_psi4(): inp = OptimizationInput(**inp) ret = qcng.compute_procedure(inp, "geometric", raise_error=True) - assert 10 > len(ret["trajectory"]) > 1 + assert 10 > len(ret.trajectory) > 1 - geom = ret["final_molecule"]["geometry"] - assert pytest.approx(_bond_dist(geom, 0, 1), 1.e-4) == 1.3459150737 + assert pytest.approx(ret.final_molecule.measure([0, 1]), 1.e-4) == 1.3459150737 @testing.using_psi4 @@ -77,11 +76,11 @@ def test_geometric_local_options(): # Set some extremely large number to test ret = qcng.compute_procedure(inp, "geometric", raise_error=True, local_options={"memory": "5000"}) - assert pytest.approx(ret["trajectory"][0]["provenance"]["memory"], 1) == 4900 + assert pytest.approx(ret.trajectory[0].provenance.memory, 1) == 4900 # Make sure we cleaned up - assert "_qcengine_local_config" not in ret["input_specification"] - assert "_qcengine_local_config" not in ret["trajectory"][0] + assert "_qcengine_local_config" not in ret.input_specification + assert "_qcengine_local_config" not in ret.trajectory[0].extras @testing.using_rdkit @@ -96,9 +95,8 @@ def test_geometric_stdout(): inp = OptimizationInput(**inp) ret = qcng.compute_procedure(inp, "geometric", raise_error=True) - assert ret["success"] is True - assert "Converged!" in ret["stdout"] - assert ret.pop("stderr", None) is None + assert ret.success is True + assert "Converged!" in ret.stdout with pytest.raises(ValueError): _ = qcng.compute_procedure(inp, "rdkit", raise_error=True) @@ -116,8 +114,8 @@ def test_geometric_rdkit_error(): inp = OptimizationInput(**inp) ret = qcng.compute_procedure(inp, "geometric") - assert ret["success"] is False - assert isinstance(ret["error"]["error_message"], str) + assert ret.success is False + assert isinstance(ret.error.error_message, str) with pytest.raises(ValueError): _ = qcng.compute_procedure(inp, "rdkit", raise_error=True) @@ -133,5 +131,5 @@ def test_geometric_torchani(): inp["keywords"]["program"] = "torchani" ret = qcng.compute_procedure(inp, "geometric", raise_error=True) - assert ret["success"] is True - assert "Converged!" in ret["stdout"] + assert ret.success is True + assert "Converged!" in ret.stdout diff --git a/qcengine/util.py b/qcengine/util.py index 660d8997b..6879214bf 100644 --- a/qcengine/util.py +++ b/qcengine/util.py @@ -90,7 +90,6 @@ def compute_wrapper(capture_output: bool=True) -> Dict[str, Any]: if capture_output: sys.stdout = old_stdout sys.stderr = old_stderr - # Pull over values metadata["stdout"] = new_stdout.getvalue() or None metadata["stderr"] = new_stderr.getvalue() or None From 80017a02faf25ee08ec8b39e0bf3a775ff52ab33 Mon Sep 17 00:00:00 2001 From: Daniel Smith Date: Thu, 28 Feb 2019 12:56:58 -0500 Subject: [PATCH 07/13] Programs: Lists all available programs, closes #15 --- qcengine/programs/__init__.py | 39 +++++++++++++++++++++++----- qcengine/programs/executor.py | 7 +++++ qcengine/programs/molpro.py | 7 +++++ qcengine/programs/psi4.py | 12 +++++++-- qcengine/programs/rdkit.py | 11 ++++++-- qcengine/programs/torchani.py | 15 ++++++----- qcengine/tests/test_program_utils.py | 12 +++++++++ 7 files changed, 86 insertions(+), 17 deletions(-) create mode 100644 qcengine/tests/test_program_utils.py diff --git a/qcengine/programs/__init__.py b/qcengine/programs/__init__.py index 283bcb9b0..2da221748 100644 --- a/qcengine/programs/__init__.py +++ b/qcengine/programs/__init__.py @@ -2,31 +2,56 @@ Imports the various compute backends """ +from typing import List, Set + from .psi4 import Psi4Executor from .rdkit import RDKitExecutor from .torchani import TorchANIExecutor from .molpro import MolproExecutor from .dftd3 import DFTD3Executor -__all__ = ["register_program", "get_program", "list_programs"] +__all__ = ["register_program", "get_program", "list_all_programs", "list_available_programs"] programs = {} -def register_program(entry_point): +def register_program(entry_point: 'ProgramExecutor') -> None: + """ + Register a new ProgramExecutor with QCEngine + """ + name = entry_point.name if name.lower() in programs.keys(): raise ValueError('{} is already a registered program.'.format(name)) - programs[name.lower()] = {'entry_point': entry_point} + programs[name.lower()] = entry_point + + +def get_program(name: str) -> 'ProgramExecutor': + """ + Returns a programs executor class + """ + return programs[name.lower()] + + +def list_all_programs() -> Set[str]: + """ + List all programs registered by QCEngine. + """ + return set(programs.keys()) -def get_program(name): - return programs[name.lower()]['entry_point'] +def list_available_programs() -> Set[str]: + """ + List all programs that can be exectued (found) by QCEngine. + """ + ret = set() + for k, p in programs.keys(): + if p.found(): + ret.add(k) -def list_programs(): - return programs.keys() + return ret register_program(Psi4Executor()) diff --git a/qcengine/programs/executor.py b/qcengine/programs/executor.py index 2eb595ac0..379639512 100644 --- a/qcengine/programs/executor.py +++ b/qcengine/programs/executor.py @@ -22,6 +22,13 @@ class Config: def compute(self, input_data: 'ResultInput', config: 'JobConfig') -> 'Result': pass + @abc.abstractmethod + def found(self) -> bool: + """ + Checks if the program can be found. + """ + pass + ## Utility @staticmethod diff --git a/qcengine/programs/molpro.py b/qcengine/programs/molpro.py index c4f580c9e..69e5ca3a1 100644 --- a/qcengine/programs/molpro.py +++ b/qcengine/programs/molpro.py @@ -161,3 +161,10 @@ def parse_output(self, outfiles: Dict[str, str], input_model: 'ResultInput') -> output_data['success'] = True return Result(**{**input_model.dict(), **output_data}) + + def found(self) -> bool: + + if which('molpro'): + return True + else: + return False diff --git a/qcengine/programs/psi4.py b/qcengine/programs/psi4.py index acbb56bbb..24311098a 100644 --- a/qcengine/programs/psi4.py +++ b/qcengine/programs/psi4.py @@ -31,8 +31,8 @@ def compute(self, input_model: 'ResultInput', config: 'JobConfig') -> 'Result': try: import psi4 - except ImportError: - raise ImportError("Could not find Psi4 in the Python path.") + except ModuleNotFoundError: + raise ModuleNotFoundError("Could not find Psi4 in the Python path.") # Setup the job input_model = input_model.copy().dict() @@ -87,3 +87,11 @@ def compute(self, input_model: 'ResultInput', config: 'JobConfig') -> 'Result': return Result(**output_data) return FailedOperation( success=output_data.pop("success", False), error=output_data.pop("error"), input_model=output_data) + + def found(self) -> bool: + try: + import psi4 + return True + except ModuleNotFoundError: + return False + diff --git a/qcengine/programs/rdkit.py b/qcengine/programs/rdkit.py index 1db97c7db..ae46e0a82 100644 --- a/qcengine/programs/rdkit.py +++ b/qcengine/programs/rdkit.py @@ -35,8 +35,8 @@ def compute(self, input_data: 'ResultInput', config: 'JobConfig') -> 'Result': import rdkit from rdkit import Chem from rdkit.Chem import AllChem - except ImportError: - raise ImportError("Could not find RDKit in the Python path.") + except ModuleNotFoundError: + raise ModuleNotFoundError("Could not find RDKit in the Python path.") # Failure flag ret_data = {"success": False} @@ -117,3 +117,10 @@ def compute(self, input_data: 'ResultInput', config: 'JobConfig') -> 'Result': # 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 found(self) -> bool: + try: + import rdkit + return True + except ModuleNotFoundError: + return False \ No newline at end of file diff --git a/qcengine/programs/torchani.py b/qcengine/programs/torchani.py index fb98fe2e9..c63dcf6c0 100644 --- a/qcengine/programs/torchani.py +++ b/qcengine/programs/torchani.py @@ -54,14 +54,10 @@ def compute(self, input_data: 'ResultInput', config: 'JobConfig') -> 'Result': """ 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.") + except ModuleNotFoundError: + raise ModuleNotFoundError("Could not find TorchANI in the Python path.") device = torch.device('cpu') builtin = torchani.neurochem.Builtins() @@ -115,3 +111,10 @@ def compute(self, input_data: 'ResultInput', config: 'JobConfig') -> 'Result': # 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 found(self) -> bool: + try: + import torchani + return True + except ModuleNotFoundError: + return False \ No newline at end of file diff --git a/qcengine/tests/test_program_utils.py b/qcengine/tests/test_program_utils.py new file mode 100644 index 000000000..c66bbafe3 --- /dev/null +++ b/qcengine/tests/test_program_utils.py @@ -0,0 +1,12 @@ +""" +Tests the DQM compute dispatch module +""" + +import copy + +from qcengine import testing +import qcengine as qcng + +_base_json = {"schema_name": "qcschema_input", "schema_version": 1} + + From 49b2990841df662fefbe447903b2f9bec4625ee1 Mon Sep 17 00:00:00 2001 From: Daniel Smith Date: Thu, 28 Feb 2019 13:10:16 -0500 Subject: [PATCH 08/13] Programs: Test for checking available/listed --- qcengine/__init__.py | 1 + qcengine/programs/__init__.py | 2 +- qcengine/programs/dftd3/runner.py | 43 ++++++++++++++------------- qcengine/programs/molpro.py | 8 ++--- qcengine/programs/tests/test_dftd3.py | 38 +++++++++++------------ qcengine/programs/torchani.py | 3 +- qcengine/testing.py | 17 ++--------- qcengine/tests/test_program_utils.py | 17 +++++++++-- qcengine/util.py | 13 ++++++++ 9 files changed, 76 insertions(+), 66 deletions(-) diff --git a/qcengine/__init__.py b/qcengine/__init__.py index 5ff1dc128..86f5e8abb 100644 --- a/qcengine/__init__.py +++ b/qcengine/__init__.py @@ -4,6 +4,7 @@ from . import config +from .programs import get_program, list_all_programs, list_available_programs from .compute import compute, compute_procedure from .config import get_config from .stock_mols import get_molecule diff --git a/qcengine/programs/__init__.py b/qcengine/programs/__init__.py index 2da221748..e11c15e9e 100644 --- a/qcengine/programs/__init__.py +++ b/qcengine/programs/__init__.py @@ -47,7 +47,7 @@ def list_available_programs() -> Set[str]: """ ret = set() - for k, p in programs.keys(): + for k, p in programs.items(): if p.found(): ret.add(k) diff --git a/qcengine/programs/dftd3/runner.py b/qcengine/programs/dftd3/runner.py index feeec98f3..161c4fcd1 100644 --- a/qcengine/programs/dftd3/runner.py +++ b/qcengine/programs/dftd3/runner.py @@ -16,15 +16,13 @@ import qcelemental as qcel from qcelemental.models import FailedOperation, Result -from ...util import execute -from ...testing import which +from ...util import execute, which from ..executor import ProgramExecutor #from ..pdict import PreservingDict from . import dashparam from .util import provenance_stamp, parse_dertype - class DFTD3Executor(ProgramExecutor): _defaults = { @@ -42,6 +40,9 @@ class Config(ProgramExecutor.Config): def __init__(self, **kwargs): super().__init__(**{**self._defaults, **kwargs}) + def found(self) -> bool: + return which('dftd3', return_bool=True) + def compute(self, input_data: 'ResultInput', config: 'JobConfig') -> 'Result': if not which('dftd3', return_bool=True): @@ -110,27 +111,27 @@ def run_json(jobrec): raise exc jobrec['success'] = True - # for k, v in jobrec["extra"]["qcvars"].items(): + # for k, v in jobrec["extras"]["qcvars"].items(): # v = v.data # if isinstance(v, np.ndarray): # v = v.ravel().tolist() # elif isinstance(v, Decimal): # v = float(v) # - # jobrec["extra"]["qcvars"][k] = v + # jobrec["extras"]["qcvars"][k] = v - jobrec["extra"]["qcvars"]["CURRENT ENERGY"] = jobrec["extra"]['qcvars']['DISPERSION CORRECTION ENERGY'] - jobrec['properties'] = {"return_energy": jobrec["extra"]['qcvars']['CURRENT ENERGY']} + jobrec["extras"]["qcvars"]["CURRENT ENERGY"] = jobrec["extras"]['qcvars']['DISPERSION CORRECTION ENERGY'] + jobrec['properties'] = {"return_energy": jobrec["extras"]['qcvars']['CURRENT ENERGY']} if jobrec['driver'] == 'energy': jobrec["return_result"] = jobrec["properties"]["return_energy"] elif jobrec['driver'] == 'gradient': - jobrec["extra"]['qcvars']['CURRENT GRADIENT'] = copy.deepcopy( - jobrec["extra"]['qcvars']['DISPERSION CORRECTION GRADIENT']) - jobrec["return_result"] = jobrec["extra"]["qcvars"]["CURRENT GRADIENT"] + jobrec["extras"]['qcvars']['CURRENT GRADIENT'] = copy.deepcopy( + jobrec["extras"]['qcvars']['DISPERSION CORRECTION GRADIENT']) + jobrec["return_result"] = jobrec["extras"]["qcvars"]["CURRENT GRADIENT"] jobrec["molecule"]["real"] = list(jobrec["molecule"]["real"]) - # jobrec["extra"] = {"qcvars": jobrec.pop("qcvars"), + # jobrec["extras"] = {"qcvars": jobrec.pop("qcvars"), # "info": jobrec.pop("keywords")} jobrec["keywords"] = kw @@ -175,11 +176,11 @@ def run_dftd3(name, molecule, options, **kwargs): raise exc jobrec['success'] = True - jobrec['extra']['qcvars']['CURRENT ENERGY'] = copy.deepcopy( - jobrec['extra']['qcvars']['DISPERSION CORRECTION ENERGY']) + jobrec['extras']['qcvars']['CURRENT ENERGY'] = copy.deepcopy( + jobrec['extras']['qcvars']['DISPERSION CORRECTION ENERGY']) if jobrec['driver'] == 'gradient': - jobrec['extra']['qcvars']['CURRENT GRADIENT'] = copy.deepcopy( - jobrec['extra']['qcvars']['DISPERSION CORRECTION GRADIENT']) + jobrec['extras']['qcvars']['CURRENT GRADIENT'] = copy.deepcopy( + jobrec['extras']['qcvars']['DISPERSION CORRECTION GRADIENT']) return jobrec @@ -231,11 +232,11 @@ def run_dftd3_from_arrays(molrec, raise exc jobrec['success'] = True - jobrec['extra']['qcvars']['CURRENT ENERGY'] = copy.deepcopy( - jobrec['extra']['qcvars']['DISPERSION CORRECTION ENERGY']) + jobrec['extras']['qcvars']['CURRENT ENERGY'] = copy.deepcopy( + jobrec['extras']['qcvars']['DISPERSION CORRECTION ENERGY']) if jobrec['driver'] == 'gradient': - jobrec['extra']['qcvars']['CURRENT GRADIENT'] = copy.deepcopy( - jobrec['extra']['qcvars']['DISPERSION CORRECTION GRADIENT']) + jobrec['extras']['qcvars']['CURRENT GRADIENT'] = copy.deepcopy( + jobrec['extras']['qcvars']['DISPERSION CORRECTION GRADIENT']) return jobrec @@ -521,8 +522,8 @@ def dftd3_harvest(jobrec, dftd3rec): #text += print_variables(calcinfo) jobrec['stdout'] = text - jobrec['extra'] = {} - jobrec['extra']['qcvars'] = calcinfo + jobrec['extras'] = {} + jobrec['extras']['qcvars'] = calcinfo prov = {} prov['creator'] = 'dftd3' diff --git a/qcengine/programs/molpro.py b/qcengine/programs/molpro.py index 69e5ca3a1..0fac6037c 100644 --- a/qcengine/programs/molpro.py +++ b/qcengine/programs/molpro.py @@ -8,8 +8,8 @@ #from qcelemental.models import ComputeError, FailedOperation, Provenance, Result from qcelemental.models import Result -#from ..units import ureg from .executor import ProgramExecutor +from ..util import which class MolproExecutor(ProgramExecutor): @@ -163,8 +163,4 @@ def parse_output(self, outfiles: Dict[str, str], input_model: 'ResultInput') -> return Result(**{**input_model.dict(), **output_data}) def found(self) -> bool: - - if which('molpro'): - return True - else: - return False + return which('dftd3', return_bool=True) diff --git a/qcengine/programs/tests/test_dftd3.py b/qcengine/programs/tests/test_dftd3.py index c76ab33ba..8de87ea57 100644 --- a/qcengine/programs/tests/test_dftd3.py +++ b/qcengine/programs/tests/test_dftd3.py @@ -562,7 +562,6 @@ def test_dftd3__from_arrays__supplement(): supp = {'chg': {'definitions': {'asdf-d4': {'params': {'s6': 4.05}, 'citation': ' mypaper\n'}}}} res = dftd3.from_arrays(name_hint='asdf-d4', level_hint='chg', dashcoeff_supplement=supp) - print(res) assert compare_recursive(ans, res, atol=1.e-4) with pytest.raises(ValueError) as e: dftd3.from_arrays(name_hint=res['fctldash'], level_hint=res['dashlevel'], param_tweaks=res['dashparams']) @@ -580,7 +579,6 @@ def test_3(): sys = qcel.molparse.from_string(seneyne)['qm'] res = dftd3.run_dftd3_from_arrays(molrec=sys, name_hint='b3lyp', level_hint='d3bj') - print(res) assert compare('B3LYP-D3(BJ)', _compute_key(res['keywords']), 'key') @@ -670,17 +668,17 @@ def test_dftd3__run_dftd3__2body(inp, subjects, request): else: jrec = dftd3.run_dftd3(inp['name'], subject, options={}, ptype='gradient') - assert len(jrec['extra']['qcvars']) == 8 + assert len(jrec['extras']['qcvars']) == 8 - assert compare_values(expected, jrec['extra']['qcvars']['CURRENT ENERGY'], atol=1.e-7) - assert compare_values(expected, jrec['extra']['qcvars']['DISPERSION CORRECTION ENERGY'], atol=1.e-7) - assert compare_values(expected, jrec['extra']['qcvars']['2-BODY DISPERSION CORRECTION ENERGY'], atol=1.e-7) - assert compare_values(expected, jrec['extra']['qcvars'][inp['lbl'] + ' DISPERSION CORRECTION ENERGY'], atol=1.e-7) + assert compare_values(expected, jrec['extras']['qcvars']['CURRENT ENERGY'], atol=1.e-7) + assert compare_values(expected, jrec['extras']['qcvars']['DISPERSION CORRECTION ENERGY'], atol=1.e-7) + assert compare_values(expected, jrec['extras']['qcvars']['2-BODY DISPERSION CORRECTION ENERGY'], atol=1.e-7) + assert compare_values(expected, jrec['extras']['qcvars'][inp['lbl'] + ' DISPERSION CORRECTION ENERGY'], atol=1.e-7) - assert compare_values(gexpected, jrec['extra']['qcvars']['CURRENT GRADIENT'], atol=1.e-7) - assert compare_values(gexpected, jrec['extra']['qcvars']['DISPERSION CORRECTION GRADIENT'], atol=1.e-7) - assert compare_values(gexpected, jrec['extra']['qcvars']['2-BODY DISPERSION CORRECTION GRADIENT'], atol=1.e-7) - assert compare_values(gexpected, jrec['extra']['qcvars'][inp['lbl'] + ' DISPERSION CORRECTION GRADIENT'], atol=1.e-7) + assert compare_values(gexpected, jrec['extras']['qcvars']['CURRENT GRADIENT'], atol=1.e-7) + assert compare_values(gexpected, jrec['extras']['qcvars']['DISPERSION CORRECTION GRADIENT'], atol=1.e-7) + assert compare_values(gexpected, jrec['extras']['qcvars']['2-BODY DISPERSION CORRECTION GRADIENT'], atol=1.e-7) + assert compare_values(gexpected, jrec['extras']['qcvars'][inp['lbl'] + ' DISPERSION CORRECTION GRADIENT'], atol=1.e-7) @using_dftd3_321 @@ -714,14 +712,14 @@ def test_dftd3__run_dftd3__3body(inp, subjects, request): else: jrec = dftd3.run_dftd3(inp['name'], subject, options={}, ptype='gradient') - assert len(jrec['extra']['qcvars']) == 8 + assert len(jrec['extras']['qcvars']) == 8 - assert compare_values(expected, jrec['extra']['qcvars']['CURRENT ENERGY'], atol=1.e-7) - assert compare_values(expected, jrec['extra']['qcvars']['DISPERSION CORRECTION ENERGY'], atol=1.e-7) - assert compare_values(expected, jrec['extra']['qcvars']['3-BODY DISPERSION CORRECTION ENERGY'], atol=1.e-7) - assert compare_values(expected, jrec['extra']['qcvars']['AXILROD-TELLER-MUTO 3-BODY DISPERSION CORRECTION ENERGY'], atol=1.e-7) + assert compare_values(expected, jrec['extras']['qcvars']['CURRENT ENERGY'], atol=1.e-7) + assert compare_values(expected, jrec['extras']['qcvars']['DISPERSION CORRECTION ENERGY'], atol=1.e-7) + assert compare_values(expected, jrec['extras']['qcvars']['3-BODY DISPERSION CORRECTION ENERGY'], atol=1.e-7) + assert compare_values(expected, jrec['extras']['qcvars']['AXILROD-TELLER-MUTO 3-BODY DISPERSION CORRECTION ENERGY'], atol=1.e-7) - assert compare_values(gexpected, jrec['extra']['qcvars']['CURRENT GRADIENT'], atol=1.e-7) - assert compare_values(gexpected, jrec['extra']['qcvars']['DISPERSION CORRECTION GRADIENT'], atol=1.e-7) - assert compare_values(gexpected, jrec['extra']['qcvars']['3-BODY DISPERSION CORRECTION GRADIENT'], atol=1.e-7) - assert compare_values(gexpected, jrec['extra']['qcvars']['AXILROD-TELLER-MUTO 3-BODY DISPERSION CORRECTION GRADIENT'], atol=1.e-7) + assert compare_values(gexpected, jrec['extras']['qcvars']['CURRENT GRADIENT'], atol=1.e-7) + assert compare_values(gexpected, jrec['extras']['qcvars']['DISPERSION CORRECTION GRADIENT'], atol=1.e-7) + assert compare_values(gexpected, jrec['extras']['qcvars']['3-BODY DISPERSION CORRECTION GRADIENT'], atol=1.e-7) + assert compare_values(gexpected, jrec['extras']['qcvars']['AXILROD-TELLER-MUTO 3-BODY DISPERSION CORRECTION GRADIENT'], atol=1.e-7) diff --git a/qcengine/programs/torchani.py b/qcengine/programs/torchani.py index c63dcf6c0..5c56ba384 100644 --- a/qcengine/programs/torchani.py +++ b/qcengine/programs/torchani.py @@ -53,11 +53,12 @@ def compute(self, input_data: 'ResultInput', config: 'JobConfig') -> 'Result': Runs TorchANI in FF typing """ - import numpy as np try: import torchani except ModuleNotFoundError: raise ModuleNotFoundError("Could not find TorchANI in the Python path.") + import torch + import numpy as np device = torch.device('cpu') builtin = torchani.neurochem.Builtins() diff --git a/qcengine/testing.py b/qcengine/testing.py index dfa206ba2..2c926d30e 100644 --- a/qcengine/testing.py +++ b/qcengine/testing.py @@ -8,6 +8,7 @@ import pytest from contextlib import contextmanager +from .util import which @contextmanager @@ -27,19 +28,6 @@ def environ_context(env): os.environ[key] = value -def which(command, return_bool=False): - # environment is $PATH, less any None values - lenv = {'PATH': ':' + os.environ.get('PATH')} - lenv = {k: v for k, v in lenv.items() if v is not None} - - ans = shutil.which(command, mode=os.F_OK | os.X_OK, path=lenv['PATH']) - - if return_bool: - return bool(ans) - else: - return ans - - def _plugin_import(plug): """ Tests to see if a module is available @@ -93,7 +81,8 @@ def is_dftd3_new_enough(version_feature_introduced): _plugin_import("torchani") is False, reason="Could not find TorchAni. Please install the package to enable tests") using_qcdb = pytest.mark.skipif( - _plugin_import("qcdb") is False, reason='Not detecting common driver. Install package if necessary and add to envvar PYTHONPATH') + _plugin_import("qcdb") is False, + reason='Not detecting common driver. Install package if necessary and add to envvar PYTHONPATH') using_dftd3 = pytest.mark.skipif( which('dftd3', return_bool=True) is False, diff --git a/qcengine/tests/test_program_utils.py b/qcengine/tests/test_program_utils.py index c66bbafe3..acdce81c6 100644 --- a/qcengine/tests/test_program_utils.py +++ b/qcengine/tests/test_program_utils.py @@ -2,11 +2,22 @@ Tests the DQM compute dispatch module """ -import copy +import pytest -from qcengine import testing import qcengine as qcng +from qcengine import testing + +def test_list_programs(): + + r = qcng.list_all_programs() + assert r >= {"psi4", "rdkit", "molpro", "dftd3"} -_base_json = {"schema_name": "qcschema_input", "schema_version": 1} +@pytest.mark.parametrize("program", [ + pytest.param("psi4", marks=testing.using_psi4), + pytest.param("torchani", marks=testing.using_torchani), + pytest.param("rdkit", marks=testing.using_rdkit), + ]) +def test_check_program_avail(program): + assert program in qcng.list_available_programs() diff --git a/qcengine/util.py b/qcengine/util.py index 6879214bf..f43a3860b 100644 --- a/qcengine/util.py +++ b/qcengine/util.py @@ -429,3 +429,16 @@ def disk_files(infiles: Dict[str, str], outfiles: Dict[str, None], cwd: Optional LOGGER.info(f'... Writing: {filename}') except (OSError, FileNotFoundError) as err: outfiles[fl] = None + + +def which(command, return_bool=False): + # environment is $PATH, less any None values + lenv = {'PATH': ':' + os.environ.get('PATH')} + lenv = {k: v for k, v in lenv.items() if v is not None} + + ans = shutil.which(command, mode=os.F_OK | os.X_OK, path=lenv['PATH']) + + if return_bool: + return bool(ans) + else: + return ans \ No newline at end of file From aa4b0ab3ee3511250336b38c65a2b5ff7bdaef33 Mon Sep 17 00:00:00 2001 From: Daniel Smith Date: Thu, 28 Feb 2019 14:14:32 -0500 Subject: [PATCH 09/13] Docs: Adds v0.6 changelog --- docs/source/changelog.rst | 25 +++++++++++++++++++++++++ docs/source/index.rst | 2 +- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 0033a9110..2b1b9ee8c 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -13,6 +13,31 @@ Changelog .. Bug Fixes .. +++++++++ +v0.6.0 / 2019-02-28 +------------------- + +Breaking Changes +++++++++++++++++ + +- (:pr:`36`) **breaking change** Model objects are returned by default rather than a dictionary. + +New Features +++++++++++++ + +- (:pr:`18`) Add the ``dftd3`` program to available computers. +- (:pr:`29`) Adds preliminary support for the ``Molpro`` compute engine. +- (:pr:`31`) Moves all computation to ``ProgramExecutor`` to allow for a more flexible input generation, execution, output parsing interface. +- (:pr:`32`) Adds a general ``execute`` process which safely runs subprocess jobs. + +Enhancements +++++++++++++ + +- (:pr:`33`) Moves the ``dftd3`` executor to the new ``ProgramExecutor`` interface. +- (:pr:`34`) Updates models to the more strict QCElemental v0.3.0 model classes. +- (:pr:`35`) Updates CI to avoid pulling CUDA libraries for ``torchani``. +- (:pr:`36`) First pass at documentation. + + v0.5.2 / 2019-02-13 ------------------- diff --git a/docs/source/index.rst b/docs/source/index.rst index d3d9c1628..06aeded96 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -148,7 +148,7 @@ Index * :doc:`dev_guidelines` .. toctree:: - :maxdepth: 2 + :maxdepth: 1 :caption: Developer Documentation api From 8301540b19b5ded5cfb0580ebac9bf6c29b5db5f Mon Sep 17 00:00:00 2001 From: "Lori A. Burns" Date: Thu, 28 Feb 2019 14:30:44 -0500 Subject: [PATCH 10/13] Update community.rst --- docs/source/community.rst | 6 +++--- docs/source/index.rst | 4 ++-- docs/source/single_compute.rst | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/source/community.rst b/docs/source/community.rst index f64489370..8aba4f1b0 100644 --- a/docs/source/community.rst +++ b/docs/source/community.rst @@ -12,7 +12,7 @@ Discussion - QCArchive Slack is a great place to get feedback and advice from the community. Click `here `_ to get started. - The QCArchive GitHub repositories contain future roadmaps, current code updates, and a list of issues that are being worked and provide an excellent overview of the development status of the project. See the following links: - - `QCSchema `_ + - `QCSchema `_ - `QCElemental `_ - `QCEngine `_ - `QCFractal `_ @@ -22,11 +22,11 @@ Work with us! ------------- The QCArchive project is actively looking for early collaborations to use our -tools, help us shake out the bugs, and be evangelist within the computational +tools, help us shake out the bugs, and be evangelists within the computational molecular science community for this code ecosystem. In return you will receive the following benefits: -- Work directly with MolSSI Software Scientist who will discuss your problem and provide ideas. +- Work directly with a MolSSI Software Scientist who will discuss your problem and provide ideas. - Develop the requirements and potential solutions for your use case within the QCArchive ecosystem. - Setup monthly meetings to ensure your project stays on track. - Highlight your project within the QCArchive ecosystem. diff --git a/docs/source/index.rst b/docs/source/index.rst index 06aeded96..4e8c1bbd3 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -64,7 +64,7 @@ Currently available compute backends for single results are as follow: - `TorchANI `_ -- Molecule Mechanics: +- Molecular Mechanics: - `RDKit `_ @@ -141,7 +141,7 @@ Index environment -**Developer Documentationd** +**Developer Documentation** * :doc:`api` * :doc:`changelog` diff --git a/docs/source/single_compute.rst b/docs/source/single_compute.rst index 66e1ac611..714028a9a 100644 --- a/docs/source/single_compute.rst +++ b/docs/source/single_compute.rst @@ -12,9 +12,9 @@ An input description has the following fields: - ``molecule`` - A QCSchema compliant dictionary or Molecule model. - ``driver`` - The ``energy``, ``gradient``, ``hessian``, or ``properties`` option. -- ``model`` - A description of the evaluation model, for quantum chemistry this is typically ``method`` and ``basis``. However, +- ``model`` - A description of the evaluation model. For quantum chemistry this is typically ``method`` and ``basis``. However, non-quantum chemistry models are often a simple ``method`` as in ``method = 'UFF'`` for forcefield evaluation. -- ``keywords`` - a dictionary of keywords to pass to the underlying program, these are program-specific keywords. +- ``keywords`` - a dictionary of keywords to pass to the underlying program. These are program-specific keywords. An example input is as follows: @@ -46,7 +46,7 @@ A single computation can be evaluated with the ``compute`` function as follows: >>> ret = qcng.compute(inp, "psi4") -By default the job is given resources relating to the compute environment it in; however, these variables can be overridden: +By default the job is given resources relating to the compute environment it is in; however, these variables can be overridden: .. code:: python From 382385d940c4f760940f5c178f6a55b624bb2928 Mon Sep 17 00:00:00 2001 From: Daniel Smith Date: Thu, 28 Feb 2019 14:19:18 -0500 Subject: [PATCH 11/13] Lint: YAPF and isort patches --- qcengine/compute.py | 15 +++++----- qcengine/config.py | 8 +++--- qcengine/extras.py | 1 - qcengine/programs/dftd3/dashparam.py | 2 +- qcengine/programs/dftd3/runner.py | 27 +++++++++--------- qcengine/programs/executor.py | 5 ++-- qcengine/programs/molpro.py | 4 +-- qcengine/programs/psi4.py | 2 -- qcengine/programs/rdkit.py | 3 +- qcengine/programs/tests/test_dftd3.py | 5 ++-- qcengine/programs/tests/test_molpro.py | 4 +-- qcengine/programs/torchani.py | 3 +- qcengine/stock_mols.py | 2 +- qcengine/testing.py | 3 +- qcengine/util.py | 39 +++++++++++++------------- 15 files changed, 61 insertions(+), 62 deletions(-) diff --git a/qcengine/compute.py b/qcengine/compute.py index 610b1e3a5..d747b42b0 100644 --- a/qcengine/compute.py +++ b/qcengine/compute.py @@ -2,6 +2,7 @@ Integrates the computes together """ from typing import Any, Dict, Optional, Union + from qcelemental.models import ComputeError, FailedOperation, Optimization, OptimizationInput, ResultInput from .config import get_config @@ -13,10 +14,10 @@ def compute(input_data: Union[Dict[str, Any], 'ResultInput'], program: str, - raise_error: bool=False, - capture_output: bool=True, - local_options: Optional[Dict[str, str]]=None, - return_dict: bool=False) -> 'Result': + raise_error: bool = False, + capture_output: bool = True, + 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: @@ -80,9 +81,9 @@ def compute(input_data: Union[Dict[str, Any], 'ResultInput'], def compute_procedure(input_data: Dict[str, Any], procedure: str, - raise_error: bool=False, - local_options: Optional[Dict[str, str]]=None, - return_dict: bool=False) -> 'BaseModel': + 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 diff --git a/qcengine/config.py b/qcengine/config.py index a62da025e..57d5f9fff 100644 --- a/qcengine/config.py +++ b/qcengine/config.py @@ -7,7 +7,7 @@ import logging import os import socket -from typing import Any, Dict, Union, Optional +from typing import Any, Dict, Optional, Union import pydantic @@ -23,7 +23,7 @@ # Generic globals -def get_global(key: Optional[str]=None) -> Union[str, Dict[str, Any]]: +def get_global(key: Optional[str] = None) -> Union[str, Dict[str, Any]]: import cpuinfo import psutil global _global_values @@ -156,7 +156,7 @@ def global_repr() -> str: return ret -def get_node_descriptor(hostname: Optional[str]=None) -> NodeDescriptor: +def get_node_descriptor(hostname: Optional[str] = None) -> NodeDescriptor: """ Find the correct NodeDescriptor based off current hostname """ @@ -197,7 +197,7 @@ def parse_environment(data: Dict[str, Any]) -> Dict[str, Any]: return ret -def get_config(*, hostname: Optional[str]=None, local_options: Dict[str, Any]=None) -> JobConfig: +def get_config(*, hostname: Optional[str] = None, local_options: Dict[str, Any] = None) -> JobConfig: """ Returns the configuration key for qcengine. """ diff --git a/qcengine/extras.py b/qcengine/extras.py index d27c9a98b..7e858822c 100644 --- a/qcengine/extras.py +++ b/qcengine/extras.py @@ -1,4 +1,3 @@ - """ Misc information and runtime information. """ diff --git a/qcengine/programs/dftd3/dashparam.py b/qcengine/programs/dftd3/dashparam.py index 16683de2e..1499d40ea 100644 --- a/qcengine/programs/dftd3/dashparam.py +++ b/qcengine/programs/dftd3/dashparam.py @@ -1,7 +1,7 @@ """Collect empirical dispersion parameters.""" -import copy import collections +import copy ## ==> Dispersion Aliases and Parameters <== ## diff --git a/qcengine/programs/dftd3/runner.py b/qcengine/programs/dftd3/runner.py index 161c4fcd1..2aea40d57 100644 --- a/qcengine/programs/dftd3/runner.py +++ b/qcengine/programs/dftd3/runner.py @@ -1,26 +1,26 @@ """Compute dispersion correction using Grimme's DFTD3 executable.""" +import copy +import json import os import pathlib -import socket - +import pprint import re +import socket import sys -import copy -import json -import pprint -pp = pprint.PrettyPrinter(width=120, compact=True, indent=1) from decimal import Decimal import numpy as np import qcelemental as qcel from qcelemental.models import FailedOperation, Result -from ...util import execute, which -from ..executor import ProgramExecutor #from ..pdict import PreservingDict from . import dashparam -from .util import provenance_stamp, parse_dertype +from ...util import execute, which +from ..executor import ProgramExecutor +from .util import parse_dertype, provenance_stamp + +pp = pprint.PrettyPrinter(width=120, compact=True, indent=1) class DFTD3Executor(ProgramExecutor): @@ -289,10 +289,11 @@ def module_driver(jobrec, module_label, plant, harvest, verbose=1): env = modulerec.pop('env') blocking_files = modulerec.pop('blocking_files') - ans, dans = execute(command, infiles, outfiles, - **{'scratch_messy': True, - 'environment': env, - 'blocking_files': blocking_files}) + ans, dans = execute(command, infiles, outfiles, **{ + 'scratch_messy': True, + 'environment': env, + 'blocking_files': blocking_files + }) modulerec.update(dans) modulerec.update({'command': command, 'infiles': infiles, 'env': env}) diff --git a/qcengine/programs/executor.py b/qcengine/programs/executor.py index 379639512..0de17c988 100644 --- a/qcengine/programs/executor.py +++ b/qcengine/programs/executor.py @@ -1,5 +1,4 @@ import abc - from typing import Any, Dict, Optional from pydantic import BaseModel @@ -40,9 +39,11 @@ def parse_version(version: str): return parse_version(version) + ## Computers - def build_input(self, input_model: 'ResultInput', config: 'JobConfig', template: Optional[str]=None) -> Dict[str, Any]: + def build_input(self, input_model: 'ResultInput', config: 'JobConfig', + template: Optional[str] = None) -> Dict[str, Any]: raise ValueError("build_input is not implemented for {}.", self.__class__) def execute(self, inputs, extra_outfiles, extra_commands, scratch_name, timeout): diff --git a/qcengine/programs/molpro.py b/qcengine/programs/molpro.py index 0fac6037c..e206eea6b 100644 --- a/qcengine/programs/molpro.py +++ b/qcengine/programs/molpro.py @@ -8,8 +8,8 @@ #from qcelemental.models import ComputeError, FailedOperation, Provenance, Result from qcelemental.models import Result -from .executor import ProgramExecutor from ..util import which +from .executor import ProgramExecutor class MolproExecutor(ProgramExecutor): @@ -33,7 +33,7 @@ def compute(self, input_data: 'ResultInput', config: 'JobConfig') -> 'Result': pass def build_input(self, input_model: 'ResultInput', config: 'JobConfig', - template: Optional[str]=None) -> Dict[str, Any]: + template: Optional[str] = None) -> Dict[str, Any]: input_file = [] posthf_methods = {'mp2', 'ccsd', 'ccsd(t)'} diff --git a/qcengine/programs/psi4.py b/qcengine/programs/psi4.py index 24311098a..9cfb60c3e 100644 --- a/qcengine/programs/psi4.py +++ b/qcengine/programs/psi4.py @@ -56,7 +56,6 @@ def compute(self, input_model: 'ResultInput', config: 'JobConfig') -> 'Result': if mol.multiplicity() != 1: input_model["keywords"]["reference"] = "uks" - output_data = psi4.json_wrapper.run_json(input_model) if "extras" not in output_data: output_data["extras"] = {} @@ -94,4 +93,3 @@ def found(self) -> bool: return True except ModuleNotFoundError: return False - diff --git a/qcengine/programs/rdkit.py b/qcengine/programs/rdkit.py index ae46e0a82..2b60d4924 100644 --- a/qcengine/programs/rdkit.py +++ b/qcengine/programs/rdkit.py @@ -5,7 +5,6 @@ from qcelemental.models import ComputeError, FailedOperation, Provenance, Result from ..units import ureg - from .executor import ProgramExecutor @@ -123,4 +122,4 @@ def found(self) -> bool: import rdkit return True except ModuleNotFoundError: - return False \ No newline at end of file + return False diff --git a/qcengine/programs/tests/test_dftd3.py b/qcengine/programs/tests/test_dftd3.py index 8de87ea57..171cf69dc 100644 --- a/qcengine/programs/tests/test_dftd3.py +++ b/qcengine/programs/tests/test_dftd3.py @@ -1,14 +1,13 @@ import copy -import pytest import numpy as np +import pytest import qcelemental as qcel from qcelemental.testing import compare, compare_recursive, compare_values, tnm from qcengine.programs import dftd3 -from qcengine.testing import using_dftd3, using_dftd3_321, using_psi4, using_qcdb, is_psi4_new_enough - +from qcengine.testing import is_psi4_new_enough, using_dftd3, using_dftd3_321, using_psi4, using_qcdb ## Resources diff --git a/qcengine/programs/tests/test_molpro.py b/qcengine/programs/tests/test_molpro.py index 7e05862f6..71f1c584f 100644 --- a/qcengine/programs/tests/test_molpro.py +++ b/qcengine/programs/tests/test_molpro.py @@ -1,8 +1,8 @@ import copy -import pytest import numpy as np +import pytest import qcelemental as qcel from qcelemental.testing import compare_recursive @@ -29,4 +29,4 @@ def test_molpro_output_parser(test_case): # TODO add `skip` to compare_recusive check = compare_recursive(output_ref, output) - assert check, check \ No newline at end of file + assert check, check diff --git a/qcengine/programs/torchani.py b/qcengine/programs/torchani.py index 5c56ba384..e203fd189 100644 --- a/qcengine/programs/torchani.py +++ b/qcengine/programs/torchani.py @@ -5,7 +5,6 @@ from qcelemental.models import ComputeError, FailedOperation, Provenance, Result from ..units import ureg - from .executor import ProgramExecutor @@ -118,4 +117,4 @@ def found(self) -> bool: import torchani return True except ModuleNotFoundError: - return False \ No newline at end of file + return False diff --git a/qcengine/stock_mols.py b/qcengine/stock_mols.py index 0635a72df..4655ee688 100644 --- a/qcengine/stock_mols.py +++ b/qcengine/stock_mols.py @@ -41,7 +41,7 @@ 0.000000, 0.000000, 0.627352, 0.000000, 0.000000, 3.963929], }, -} +} # yapf: disable def get_molecule(name): diff --git a/qcengine/testing.py b/qcengine/testing.py index 2c926d30e..cd58b0d4f 100644 --- a/qcengine/testing.py +++ b/qcengine/testing.py @@ -5,9 +5,10 @@ import os import shutil import subprocess +from contextlib import contextmanager import pytest -from contextlib import contextmanager + from .util import which diff --git a/qcengine/util.py b/qcengine/util.py index f43a3860b..a3652de3f 100644 --- a/qcengine/util.py +++ b/qcengine/util.py @@ -7,8 +7,8 @@ import json import operator import os -import signal import shutil +import signal import subprocess import sys import tempfile @@ -20,7 +20,7 @@ from qcelemental.models import ComputeError, FailedOperation -from .config import get_provenance_augments, LOGGER +from .config import LOGGER, get_provenance_augments __all__ = ["compute_wrapper", "get_module_function", "model_wrapper", "handle_output_metadata"] @@ -61,7 +61,7 @@ def model_wrapper(input_data: Dict[str, Any], model: 'BaseModel', raise_error: b @contextmanager -def compute_wrapper(capture_output: bool=True) -> Dict[str, Any]: +def compute_wrapper(capture_output: bool = True) -> Dict[str, Any]: """Wraps compute for timing, output capturing, and raise protection """ @@ -128,8 +128,8 @@ def get_module_function(module: str, func_name: str, subpackage=None) -> Callabl 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']: + raise_error: bool = False, + return_dict: bool = True) -> Union[Dict[str, Any], 'BaseModel']: """ Fuses general metadata and output together. @@ -190,7 +190,7 @@ def handle_output_metadata(output_data: Union[Dict[str, Any], 'BaseModel'], return ret -def terminate_process(proc: Any, timeout: int=15) -> None: +def terminate_process(proc: Any, timeout: int = 15) -> None: if proc.poll() is None: # Sigint (keyboard interupt) @@ -210,7 +210,8 @@ def terminate_process(proc: Any, timeout: int=15) -> None: @contextmanager -def popen(args: List[str], append_prefix: bool=False, popen_kwargs: Optional[Dict[str, Any]]=None) -> Dict[str, Any]: +def popen(args: List[str], append_prefix: bool = False, + popen_kwargs: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: """ Opens a background task @@ -256,16 +257,16 @@ def popen(args: List[str], append_prefix: bool=False, popen_kwargs: Optional[Dic def execute(command: List[str], - infiles: Optional[Dict[str, str]]=None, - outfiles: Optional[List[str]]=None, + infiles: Optional[Dict[str, str]] = None, + outfiles: Optional[List[str]] = None, *, - scratch_name: Union[bool, str]=True, - scratch_location: Optional[str]=None, - scratch_messy: bool=False, - blocking_files: Optional[List[str]]=None, - timeout: Optional[int]=None, - interupt_after: Optional[int]=None, - environment: Optional[Dict[str, str]]=None) -> Dict[str, str]: + scratch_name: Union[bool, str] = True, + scratch_location: Optional[str] = None, + scratch_messy: bool = False, + blocking_files: Optional[List[str]] = None, + timeout: Optional[int] = None, + interupt_after: Optional[int] = None, + environment: Optional[Dict[str, str]] = None) -> Dict[str, str]: """ Runs a process in the background until complete. @@ -341,7 +342,7 @@ def execute(command: List[str], @contextmanager -def scratch_directory(child: bool=True, parent: bool=None, messy: bool=False) -> str: +def scratch_directory(child: bool = True, parent: bool = None, messy: bool = False) -> str: """ Parameters @@ -386,7 +387,7 @@ def scratch_directory(child: bool=True, parent: bool=None, messy: bool=False) -> @contextmanager -def disk_files(infiles: Dict[str, str], outfiles: Dict[str, None], cwd: Optional[str]=None) -> Dict[str, str]: +def disk_files(infiles: Dict[str, str], outfiles: Dict[str, None], cwd: Optional[str] = None) -> Dict[str, str]: """ Parameters @@ -441,4 +442,4 @@ def which(command, return_bool=False): if return_bool: return bool(ans) else: - return ans \ No newline at end of file + return ans From 16f6f91ae3909946374379883cb6bc764e896e53 Mon Sep 17 00:00:00 2001 From: Daniel Smith Date: Thu, 28 Feb 2019 14:29:53 -0500 Subject: [PATCH 12/13] Util: Moves DFTD3 util functions to extra, may be moved later- but centralized --- qcengine/compute.py | 3 +- qcengine/config.py | 10 ++-- qcengine/extras.py | 63 +++++++++++++++++++++++++- qcengine/programs/dftd3/runner.py | 2 +- qcengine/programs/dftd3/util.py | 61 ------------------------- qcengine/programs/tests/test_molpro.py | 18 ++++++-- qcengine/tests/test_compute.py | 4 +- 7 files changed, 86 insertions(+), 75 deletions(-) delete mode 100644 qcengine/programs/dftd3/util.py diff --git a/qcengine/compute.py b/qcengine/compute.py index d747b42b0..9f264feb9 100644 --- a/qcengine/compute.py +++ b/qcengine/compute.py @@ -15,7 +15,6 @@ def compute(input_data: Union[Dict[str, Any], 'ResultInput'], program: str, raise_error: bool = False, - capture_output: bool = True, local_options: Optional[Dict[str, str]] = None, return_dict: bool = False) -> 'Result': """Executes a single quantum chemistry program given a QC Schema input. @@ -62,7 +61,7 @@ def compute(input_data: Union[Dict[str, Any], 'ResultInput'], config = get_config(local_options=local_options) # Run the program - with compute_wrapper(capture_output=capture_output) as metadata: + with compute_wrapper(capture_output=False) as metadata: output_data = input_data.copy() # Initial in case of error handling try: diff --git a/qcengine/config.py b/qcengine/config.py index 57d5f9fff..983f9bdc1 100644 --- a/qcengine/config.py +++ b/qcengine/config.py @@ -23,7 +23,7 @@ # Generic globals -def get_global(key: Optional[str] = None) -> Union[str, Dict[str, Any]]: +def get_global(key: Optional[str]=None) -> Union[str, Dict[str, Any]]: import cpuinfo import psutil global _global_values @@ -75,7 +75,7 @@ def __init__(self, **data: Dict[str, Any]) -> 'BaseModel': super().__init__(**data) class Config: - ignore_extra = False + extra = "forbid" class JobConfig(pydantic.BaseModel): @@ -86,7 +86,7 @@ class JobConfig(pydantic.BaseModel): scratch_directory: Optional[str] # What location to use as scratch class Config: - ignore_extra = False + extra = "forbid" def _load_defaults() -> None: @@ -156,7 +156,7 @@ def global_repr() -> str: return ret -def get_node_descriptor(hostname: Optional[str] = None) -> NodeDescriptor: +def get_node_descriptor(hostname: Optional[str]=None) -> NodeDescriptor: """ Find the correct NodeDescriptor based off current hostname """ @@ -197,7 +197,7 @@ def parse_environment(data: Dict[str, Any]) -> Dict[str, Any]: return ret -def get_config(*, hostname: Optional[str] = None, local_options: Dict[str, Any] = None) -> JobConfig: +def get_config(*, hostname: Optional[str]=None, local_options: Dict[str, Any]=None) -> JobConfig: """ Returns the configuration key for qcengine. """ diff --git a/qcengine/extras.py b/qcengine/extras.py index 7e858822c..e57678197 100644 --- a/qcengine/extras.py +++ b/qcengine/extras.py @@ -2,9 +2,11 @@ Misc information and runtime information. """ +import re + from . import _version -__all__ = ["get_information"] +__all__ = ["get_information", "provenance_stamp", "parse_dertype"] versions = _version.get_versions() @@ -20,3 +22,62 @@ def get_information(key): raise KeyError("Information key '{}' not understood.".format(key)) return __info[key] + + +def provenance_stamp(routine): + """Return dictionary satisfying QCSchema, + https://github.com/MolSSI/QCSchema/blob/master/qcschema/dev/definitions.py#L23-L41 + with QCEngine's credentials for creator and version. The + generating routine's name is passed in through `routine`. + + """ + return {'creator': 'QCEngine', 'version': get_information('version'), 'routine': routine} + + +_yes = re.compile(r'^(yes|true|on|1)', re.IGNORECASE) +_no = re.compile(r'^(no|false|off|0)', re.IGNORECASE) +_der0th = re.compile(r'^(0|none|energy)', re.IGNORECASE) +_der1st = re.compile(r'^(1|first|gradient)', re.IGNORECASE) +_der2nd = re.compile(r'^(2|second|hessian)', re.IGNORECASE) +_der3rd = re.compile(r'^(3|third)', re.IGNORECASE) +_der4th = re.compile(r'^(4|fourth)', re.IGNORECASE) +_der5th = re.compile(r'^(5|fifth)', re.IGNORECASE) + + +def parse_dertype(dertype, max_derivative=2): + """Apply generous regex to `dertype` to return regularized integer and driver values for derivative level. + + Parameters + ---------- + dertype : int or str + Interpretable as a derivative level, regardless of case or type. + max_derivative : int, optional + Derivative level above which should throw FeatureNotImplemented error. + + Returns + ------- + (int, {'energy', 'gradient', 'hessian'}) + Returns dertype as an integer and a driver-valid string. + + """ + derdriver = dict(enumerate(['energy', 'gradient', 'hessian', 'third', 'fourth', 'fifth'])) + + if _der0th.match(str(dertype)): + derint = 0 + elif _der1st.match(str(dertype)): + derint = 1 + elif _der2nd.match(str(dertype)): + derint = 2 + elif _der3rd.match(str(dertype)): + derint = 3 + elif _der4th.match(str(dertype)): + derint = 4 + elif _der5th.match(str(dertype)): + derint = 5 + else: + raise ValidationError("""Requested derivative level ({}) not recognized.""".format(dertype)) + + if derint > max_derivative: + raise FeatureNotImplemented("""derivative level ({})""".format(derint)) + + return (derint, derdriver[derint]) diff --git a/qcengine/programs/dftd3/runner.py b/qcengine/programs/dftd3/runner.py index 2aea40d57..08b7f4468 100644 --- a/qcengine/programs/dftd3/runner.py +++ b/qcengine/programs/dftd3/runner.py @@ -18,7 +18,7 @@ from . import dashparam from ...util import execute, which from ..executor import ProgramExecutor -from .util import parse_dertype, provenance_stamp +from ...extras import parse_dertype, provenance_stamp pp = pprint.PrettyPrinter(width=120, compact=True, indent=1) diff --git a/qcengine/programs/dftd3/util.py b/qcengine/programs/dftd3/util.py deleted file mode 100644 index 293f7c25a..000000000 --- a/qcengine/programs/dftd3/util.py +++ /dev/null @@ -1,61 +0,0 @@ -import re - -from ...extras import get_information - -yes = re.compile(r'^(yes|true|on|1)', re.IGNORECASE) -no = re.compile(r'^(no|false|off|0)', re.IGNORECASE) -der0th = re.compile(r'^(0|none|energy)', re.IGNORECASE) -der1st = re.compile(r'^(1|first|gradient)', re.IGNORECASE) -der2nd = re.compile(r'^(2|second|hessian)', re.IGNORECASE) -der3rd = re.compile(r'^(3|third)', re.IGNORECASE) -der4th = re.compile(r'^(4|fourth)', re.IGNORECASE) -der5th = re.compile(r'^(5|fifth)', re.IGNORECASE) - - -def parse_dertype(dertype, max_derivative=2): - """Apply generous regex to `dertype` to return regularized integer and driver values for derivative level. - - Parameters - ---------- - dertype : int or str - Interpretable as a derivative level, regardless of case or type. - max_derivative : int, optional - Derivative level above which should throw FeatureNotImplemented error. - - Returns - ------- - (int, {'energy', 'gradient', 'hessian'}) - Returns dertype as an integer and a driver-valid string. - - """ - derdriver = dict(enumerate(['energy', 'gradient', 'hessian', 'third', 'fourth', 'fifth'])) - - if der0th.match(str(dertype)): - derint = 0 - elif der1st.match(str(dertype)): - derint = 1 - elif der2nd.match(str(dertype)): - derint = 2 - elif der3rd.match(str(dertype)): - derint = 3 - elif der4th.match(str(dertype)): - derint = 4 - elif der5th.match(str(dertype)): - derint = 5 - else: - raise ValidationError("""Requested derivative level ({}) not recognized.""".format(dertype)) - - if derint > max_derivative: - raise FeatureNotImplemented("""derivative level ({})""".format(derint)) - - return (derint, derdriver[derint]) - - -def provenance_stamp(routine): - """Return dictionary satisfying QCSchema, - https://github.com/MolSSI/QCSchema/blob/master/qcschema/dev/definitions.py#L23-L41 - with QCEngine's credentials for creator and version. The - generating routine's name is passed in through `routine`. - - """ - return {'creator': 'QCEngine', 'version': get_information('version'), 'routine': routine} diff --git a/qcengine/programs/tests/test_molpro.py b/qcengine/programs/tests/test_molpro.py index 71f1c584f..758bd3b17 100644 --- a/qcengine/programs/tests/test_molpro.py +++ b/qcengine/programs/tests/test_molpro.py @@ -1,4 +1,3 @@ - import copy import numpy as np @@ -6,7 +5,7 @@ import qcelemental as qcel from qcelemental.testing import compare_recursive -from qcengine.programs import get_program +import qcengine as qcng # qcenginerecords not required, skips whole file qcer = pytest.importorskip("qcenginerecords") @@ -14,6 +13,7 @@ # Prep globals molpro_info = qcer.get_info('molpro') + @pytest.mark.parametrize('test_case', molpro_info.list_test_cases()) def test_molpro_output_parser(test_case): @@ -21,7 +21,7 @@ def test_molpro_output_parser(test_case): data = molpro_info.get_test_data(test_case) inp = qcel.models.ResultInput.parse_raw(data["input.json"]) - output = get_program('molpro').parse_output(data, inp).dict() + output = qcng.get_program('molpro').parse_output(data, inp).dict() output.pop("provenance", None) output_ref = qcel.models.Result.parse_raw(data["output.json"]).dict() @@ -30,3 +30,15 @@ def test_molpro_output_parser(test_case): # TODO add `skip` to compare_recusive check = compare_recursive(output_ref, output) assert check, check + + +@pytest.mark.parametrize('test_case', molpro_info.list_test_cases()) +def test_molpro_input_formatter(test_case): + + # Get output file data + data = molpro_info.get_test_data(test_case) + inp = qcel.models.ResultInput.parse_raw(data["input.json"]) + + # Just test that it runs for now + input_file = qcng.get_program('molpro').build_input(inp, qcng.get_config()) + assert input_file.keys() >= {"commands", "infiles"} \ No newline at end of file diff --git a/qcengine/tests/test_compute.py b/qcengine/tests/test_compute.py index b2857b9f9..a55528b5f 100644 --- a/qcengine/tests/test_compute.py +++ b/qcengine/tests/test_compute.py @@ -32,7 +32,7 @@ def test_psi4_task(): json_data["model"] = {"method": "SCF", "basis": "sto-3g"} json_data["keywords"] = {"scf_type": "df"} - ret = qcng.compute(json_data, "psi4", raise_error=True, capture_output=False, return_dict=True) + ret = qcng.compute(json_data, "psi4", raise_error=True, return_dict=True) assert ret["driver"] == "energy" assert "provenance" in ret @@ -120,7 +120,7 @@ def test_dftd3_task(): json_data["driver"] = "energy" json_data["model"] = {"method": "b3lyp-d3", "basis": ""} - ret = qcng.compute(json_data, "dftd3", raise_error=True, capture_output=True, return_dict=True) + ret = qcng.compute(json_data, "dftd3", raise_error=True, return_dict=True) assert ret["driver"] == "energy" assert "provenance" in ret From 29e6973e7686c7f9b984903894e585d2cdb6a0d8 Mon Sep 17 00:00:00 2001 From: Daniel Smith Date: Thu, 28 Feb 2019 15:21:51 -0500 Subject: [PATCH 13/13] Tests: Standard suite of tests and moves around to distinguish between standard set and program specific --- qcengine/programs/tests/test_dftd3.py | 86 ++++++++++++++----- .../tests/test_programs.py} | 17 ---- qcengine/testing.py | 44 +++++----- qcengine/tests/test_standard_suite.py | 58 +++++++++++++ 4 files changed, 144 insertions(+), 61 deletions(-) rename qcengine/{tests/test_compute.py => programs/tests/test_programs.py} (84%) create mode 100644 qcengine/tests/test_standard_suite.py diff --git a/qcengine/programs/tests/test_dftd3.py b/qcengine/programs/tests/test_dftd3.py index 171cf69dc..564f92fb1 100644 --- a/qcengine/programs/tests/test_dftd3.py +++ b/qcengine/programs/tests/test_dftd3.py @@ -1,4 +1,3 @@ - import copy import numpy as np @@ -6,9 +5,33 @@ import qcelemental as qcel from qcelemental.testing import compare, compare_recursive, compare_values, tnm +import qcengine as qcng from qcengine.programs import dftd3 from qcengine.testing import is_psi4_new_enough, using_dftd3, using_dftd3_321, using_psi4, using_qcdb + +@using_dftd3 +@pytest.mark.parametrize("method", [ + "b3lyp-d3", + "b3lyp-d3m", + "b3lyp-d3bj", + "b3lyp-d3mbj", +]) +def test_dftd3_task(method): + json_data = {"molecule": qcng.get_molecule("eneyne"), "driver": "energy", "model": {"method": method}} + + ret = qcng.compute(json_data, "dftd3", raise_error=True, return_dict=True) + + assert ret["driver"] == "energy" + assert "provenance" in ret + assert "normal termination of dftd3" in ret["stdout"] + + for key in ["cpu", "hostname", "username", "wall_time"]: + assert key in ret["provenance"] + + assert ret["success"] is True + + ## Resources ref = {} @@ -436,7 +459,8 @@ def eneyne_ne_qcschemamols(): ne = qcel.molparse.to_schema(qcel.molparse.from_string(sne)['qm'], dtype=1) mAgB = qcel.molparse.from_string(seneyne)['qm'] - mAgB['real'] = [(iat < mAgB['fragment_separators'][0]) for iat in range(len(mAgB['elem']))] # works b/c chgmult doesn't need refiguring + mAgB['real'] = [(iat < mAgB['fragment_separators'][0]) + for iat in range(len(mAgB['elem']))] # works b/c chgmult doesn't need refiguring mAgB = qcel.molparse.to_schema(mAgB, dtype=1) gAmB = qcel.molparse.from_string(seneyne)['qm'] @@ -531,8 +555,7 @@ def test_dftd3__from_arrays(inp, expected): res = dftd3.from_arrays(**inp[0]) assert compare_recursive(expected, res, atol=1.e-4) assert compare(inp[1], _compute_key(res), 'key') - res = dftd3.from_arrays( - name_hint=res['fctldash'], level_hint=res['dashlevel'], param_tweaks=res['dashparams']) + res = dftd3.from_arrays(name_hint=res['fctldash'], level_hint=res['dashlevel'], param_tweaks=res['dashparams']) assert compare_recursive(expected, res, tnm() + ' idempotent', atol=1.e-4) @@ -583,9 +606,11 @@ def test_3(): @using_dftd3 @pytest.mark.parametrize( - "subjects", [ + "subjects", + [ pytest.param(eneyne_ne_psi4mols, marks=using_psi4), - pytest.param(eneyne_ne_qcdbmols, marks=using_psi4), # needs qcdb.Molecule, presently more common in psi4 than in qcdb + pytest.param(eneyne_ne_qcdbmols, + marks=using_psi4), # needs qcdb.Molecule, presently more common in psi4 than in qcdb ], ids=['qmol', 'pmol']) @pytest.mark.parametrize( @@ -638,9 +663,11 @@ def test_qcdb__energy_d3(): @using_dftd3 @pytest.mark.parametrize( - "subjects", [ + "subjects", + [ pytest.param(eneyne_ne_psi4mols, marks=using_psi4), - pytest.param(eneyne_ne_qcdbmols, marks=using_psi4), # needs qcdb.Molecule, presently more common in psi4 than in qcdb + pytest.param(eneyne_ne_qcdbmols, + marks=using_psi4), # needs qcdb.Molecule, presently more common in psi4 than in qcdb pytest.param(eneyne_ne_qcschemamols), ], ids=['qmol', 'pmol', 'qcmol']) @@ -658,11 +685,15 @@ def test_dftd3__run_dftd3__2body(inp, subjects, request): gexpected = gref[inp['parent']][inp['lbl']][inp['subject']].ravel() if 'qcmol' in request.node.name: - subject.update({'model': {'method': inp['name']}, - 'driver': 'gradient', - 'keywords': {}, - 'schema_name': 'qcschema_input', - 'schema_version': 1}) + subject.update({ + 'model': { + 'method': inp['name'] + }, + 'driver': 'gradient', + 'keywords': {}, + 'schema_name': 'qcschema_input', + 'schema_version': 1 + }) jrec = dftd3.run_json(subject) else: jrec = dftd3.run_dftd3(inp['name'], subject, options={}, ptype='gradient') @@ -677,14 +708,17 @@ def test_dftd3__run_dftd3__2body(inp, subjects, request): assert compare_values(gexpected, jrec['extras']['qcvars']['CURRENT GRADIENT'], atol=1.e-7) assert compare_values(gexpected, jrec['extras']['qcvars']['DISPERSION CORRECTION GRADIENT'], atol=1.e-7) assert compare_values(gexpected, jrec['extras']['qcvars']['2-BODY DISPERSION CORRECTION GRADIENT'], atol=1.e-7) - assert compare_values(gexpected, jrec['extras']['qcvars'][inp['lbl'] + ' DISPERSION CORRECTION GRADIENT'], atol=1.e-7) + assert compare_values( + gexpected, jrec['extras']['qcvars'][inp['lbl'] + ' DISPERSION CORRECTION GRADIENT'], atol=1.e-7) @using_dftd3_321 @pytest.mark.parametrize( - "subjects", [ + "subjects", + [ pytest.param(eneyne_ne_psi4mols, marks=using_psi4), - pytest.param(eneyne_ne_qcdbmols, marks=using_psi4), # needs qcdb.Molecule, presently more common in psi4 than in qcdb + pytest.param(eneyne_ne_qcdbmols, + marks=using_psi4), # needs qcdb.Molecule, presently more common in psi4 than in qcdb pytest.param(eneyne_ne_qcschemamols), ], ids=['qmol', 'pmol', 'qcmol']) @@ -702,11 +736,15 @@ def test_dftd3__run_dftd3__3body(inp, subjects, request): gexpected = gref[inp['parent']][inp['lbl']][inp['subject']].ravel() if 'qcmol' in request.node.name: - subject.update({'model': {'method': inp['name']}, - 'driver': 'gradient', - 'keywords': {}, - 'schema_name': 'qcschema_input', - 'schema_version': 1}) + subject.update({ + 'model': { + 'method': inp['name'] + }, + 'driver': 'gradient', + 'keywords': {}, + 'schema_name': 'qcschema_input', + 'schema_version': 1 + }) jrec = dftd3.run_json(subject) else: jrec = dftd3.run_dftd3(inp['name'], subject, options={}, ptype='gradient') @@ -716,9 +754,11 @@ def test_dftd3__run_dftd3__3body(inp, subjects, request): assert compare_values(expected, jrec['extras']['qcvars']['CURRENT ENERGY'], atol=1.e-7) assert compare_values(expected, jrec['extras']['qcvars']['DISPERSION CORRECTION ENERGY'], atol=1.e-7) assert compare_values(expected, jrec['extras']['qcvars']['3-BODY DISPERSION CORRECTION ENERGY'], atol=1.e-7) - assert compare_values(expected, jrec['extras']['qcvars']['AXILROD-TELLER-MUTO 3-BODY DISPERSION CORRECTION ENERGY'], atol=1.e-7) + assert compare_values( + expected, jrec['extras']['qcvars']['AXILROD-TELLER-MUTO 3-BODY DISPERSION CORRECTION ENERGY'], atol=1.e-7) assert compare_values(gexpected, jrec['extras']['qcvars']['CURRENT GRADIENT'], atol=1.e-7) assert compare_values(gexpected, jrec['extras']['qcvars']['DISPERSION CORRECTION GRADIENT'], atol=1.e-7) assert compare_values(gexpected, jrec['extras']['qcvars']['3-BODY DISPERSION CORRECTION GRADIENT'], atol=1.e-7) - assert compare_values(gexpected, jrec['extras']['qcvars']['AXILROD-TELLER-MUTO 3-BODY DISPERSION CORRECTION GRADIENT'], atol=1.e-7) + assert compare_values( + gexpected, jrec['extras']['qcvars']['AXILROD-TELLER-MUTO 3-BODY DISPERSION CORRECTION GRADIENT'], atol=1.e-7) diff --git a/qcengine/tests/test_compute.py b/qcengine/programs/tests/test_programs.py similarity index 84% rename from qcengine/tests/test_compute.py rename to qcengine/programs/tests/test_programs.py index a55528b5f..a75451903 100644 --- a/qcengine/tests/test_compute.py +++ b/qcengine/programs/tests/test_programs.py @@ -113,20 +113,3 @@ def test_torchani_task(): assert ret.driver == "gradient" -@testing.using_dftd3 -def test_dftd3_task(): - json_data = copy.deepcopy(_base_json) - json_data["molecule"] = qcng.get_molecule("eneyne") - json_data["driver"] = "energy" - json_data["model"] = {"method": "b3lyp-d3", "basis": ""} - - ret = qcng.compute(json_data, "dftd3", raise_error=True, return_dict=True) - - assert ret["driver"] == "energy" - assert "provenance" in ret - assert "normal termination of dftd3" in ret["stdout"] - - for key in ["cpu", "hostname", "username", "wall_time"]: - assert key in ret["provenance"] - - assert ret["success"] is True diff --git a/qcengine/testing.py b/qcengine/testing.py index cd58b0d4f..1b47d86b7 100644 --- a/qcengine/testing.py +++ b/qcengine/testing.py @@ -66,28 +66,30 @@ def is_dftd3_new_enough(version_feature_introduced): return parse_version(candidate_version) >= parse_version(version_feature_introduced) -# Add flags -using_psi4 = pytest.mark.skipif( - is_psi4_new_enough("1.2") is False, - reason="Could not find psi4 or version too old. Please install the package to enable tests") - -using_rdkit = pytest.mark.skipif( - _plugin_import("rdkit") is False, reason="Could not find rdkit. Please install the package to enable tests") - -using_geometric = pytest.mark.skipif( - _plugin_import("geometric") is False, - reason="could not find geomeTRIC. Please install the package to enable tests") +# Figure out what is imported +_programs = { + "dftd3": which('dftd3', return_bool=True), + "geometric": _plugin_import("geometric"), + "psi4": is_psi4_new_enough("1.2"), + "rdkit": _plugin_import("rdkit"), + "qcdb": _plugin_import("qcdb"), + "torchani": _plugin_import("torchani"), +} + +def has_program(name): + return _programs[name] + +def _build_pytest_skip(program): + import_message = "Not detecting module {}. Install package if necessary to enable tests." + return pytest.mark.skipif(has_program(program) is False, reason=import_message.format(program)) -using_torchani = pytest.mark.skipif( - _plugin_import("torchani") is False, reason="Could not find TorchAni. Please install the package to enable tests") - -using_qcdb = pytest.mark.skipif( - _plugin_import("qcdb") is False, - reason='Not detecting common driver. Install package if necessary and add to envvar PYTHONPATH') - -using_dftd3 = pytest.mark.skipif( - which('dftd3', return_bool=True) is False, - reason='Not detecting executable dftd3. Install package if necessary and add to envvar PATH') +# Add flags +using_dftd3 = _build_pytest_skip("dftd3") +using_geometric = _build_pytest_skip("geometric") +using_psi4 = _build_pytest_skip("psi4") +using_rdkit = _build_pytest_skip("rdkit") +using_qcdb = _build_pytest_skip("qcdb") +using_torchani = _build_pytest_skip("torchani") using_dftd3_321 = pytest.mark.skipif( is_dftd3_new_enough("3.2.1") is False, diff --git a/qcengine/tests/test_standard_suite.py b/qcengine/tests/test_standard_suite.py new file mode 100644 index 000000000..3166a6980 --- /dev/null +++ b/qcengine/tests/test_standard_suite.py @@ -0,0 +1,58 @@ +""" +Tests the DQM compute dispatch module +""" + +import copy + +import pytest +from qcelemental.models import Molecule, ResultInput + +import qcengine as qcng +from qcengine import testing + +_canonical_methods = [ + ("dftd3", {"method": "b3lyp-d3"}), + ("psi4", {"method": "hf", "basis": "6-31G"}), + ("rdkit", {"method": "UFF"}), + ("torchani", {"method": "ANI1"}), +] + +@pytest.mark.parametrize("program, model", _canonical_methods) +def test_compute_energy(program, model): + if not testing.has_program(program): + pytest.skip("Program '{}' not found.".format(program)) + + + inp = ResultInput(molecule=qcng.get_molecule("hydrogen"), driver="energy", model=model) + ret = qcng.compute(inp, program, raise_error=True) + + assert ret.success is True + assert isinstance(ret.return_result, float) + + +@pytest.mark.parametrize("program, model", _canonical_methods) +def test_compute_gradient(program, model): + if not testing.has_program(program): + pytest.skip("Program '{}' not found.".format(program)) + + inp = ResultInput(molecule=qcng.get_molecule("hydrogen"), driver="gradient", model=model) + ret = qcng.compute(inp, program, raise_error=True) + + assert ret.success is True + assert isinstance(ret.return_result, list) + + +@pytest.mark.parametrize("program, model", [ + ("dftd3", {"method": "bad"}), + ("psi4", {"method": "bad"}), + ("rdkit", {"method": "bad"}), + ("torchani", {"method": "bad"}), +]) +def test_compute_bad_models(program, model): + if not testing.has_program(program): + pytest.skip("Program '{}' not found.".format(program)) + + inp = ResultInput(molecule=qcng.get_molecule("hydrogen"), driver="energy", model=model) + + with pytest.raises(ValueError) as exc: + ret = qcng.compute(inp, program, raise_error=True)