From 390ae77a1e0db02425bccc7e16862f7edf5e326b Mon Sep 17 00:00:00 2001 From: "Lori A. Burns" Date: Tue, 12 Nov 2024 18:17:17 -0500 Subject: [PATCH 01/13] nwchem working --- qcengine/procedures/nwchem_opt/harvester.py | 13 ++++++----- qcengine/programs/model.py | 4 ++-- qcengine/programs/nwchem/runner.py | 7 +++--- qcengine/programs/psi4.py | 2 +- .../programs/tests/standard_suite_runner.py | 9 ++++++++ qcengine/programs/tests/test_ghost.py | 5 ++++- qcengine/programs/tests/test_nwchem.py | 15 ++++++++++--- .../tests/test_standard_suite_ccsd(t).py | 10 +++++++-- .../programs/tests/test_standard_suite_hf.py | 15 ++++++++++--- qcengine/testing.py | 22 +++++-------------- qcengine/tests/test_harness_canonical.py | 2 ++ 11 files changed, 68 insertions(+), 36 deletions(-) diff --git a/qcengine/procedures/nwchem_opt/harvester.py b/qcengine/procedures/nwchem_opt/harvester.py index dad838b8..03f7ef3f 100644 --- a/qcengine/procedures/nwchem_opt/harvester.py +++ b/qcengine/procedures/nwchem_opt/harvester.py @@ -75,18 +75,21 @@ def harvest_as_atomic_result(input_model: OptimizationInput, nwout: str) -> List provenance["module"] = module # Format them inout an output + input_data = input_model.input_specification.model_dump() + input_data["driver"] = "gradient" + input_data["molecule"] = out_mol + output_data = { - "schema_version": 1, + "schema_version": 2, + "input_data": input_data, "molecule": out_mol, - "driver": "gradient", - "extras": input_model.extras.copy(), - "model": input_model.input_specification.model, - "keywords": input_model.input_specification.keywords, + "extras": {}, "properties": atprop, "provenance": provenance, "return_result": nwgrad, "success": True, } + # v2: perhaps lost the OptimizationInput.extras? # got to even out who needs plump/flat/Decimal/float/ndarray/list # Decimal --> str preserves precision diff --git a/qcengine/programs/model.py b/qcengine/programs/model.py index 73c2425e..552374c1 100644 --- a/qcengine/programs/model.py +++ b/qcengine/programs/model.py @@ -76,8 +76,8 @@ def build_input_model( # Note: Someday when the multiple QCSchema versions QCEngine supports are all within the # Pydantic v2 API base class, this can use discriminated unions instead of logic. - v1_model = getattr(qcelemental.models.v1, "AtomicInput") - v2_model = getattr(qcelemental.models.v2, "AtomicInput") + v1_model = qcelemental.models.v1.AtomicInput + v2_model = qcelemental.models.v2.AtomicInput if isinstance(data, v1_model): mdl = model_wrapper(data, v1_model) diff --git a/qcengine/programs/nwchem/runner.py b/qcengine/programs/nwchem/runner.py index 8a45b9b5..0b9f6b74 100644 --- a/qcengine/programs/nwchem/runner.py +++ b/qcengine/programs/nwchem/runner.py @@ -324,9 +324,10 @@ def parse_output( # Format them inout an output output_data = { - "schema_version": 1, + "schema_version": 2, + "input_data": input_model, "molecule": nwmol, # overwrites with outfile Cartesians in case fix_*=F - "extras": {**input_model.extras}, + "extras": {}, "native_files": {k: v for k, v in outfiles.items() if v is not None}, "properties": atprop, "provenance": provenance, @@ -343,4 +344,4 @@ def parse_output( k.upper(): str(v) if isinstance(v, Decimal) else v for k, v in qcvars.items() } - return AtomicResult(**{**input_model.model_dump(), **output_data}) + return AtomicResult(**output_data) diff --git a/qcengine/programs/psi4.py b/qcengine/programs/psi4.py index a17a993a..3e11645f 100644 --- a/qcengine/programs/psi4.py +++ b/qcengine/programs/psi4.py @@ -7,7 +7,7 @@ from pathlib import Path from typing import TYPE_CHECKING, Any, ClassVar, Dict -from qcelemental.models.v1 import AtomicResult as AtomicResult +from qcelemental.models.v1 import AtomicResult from qcelemental.models.v2 import BasisSet from qcelemental.util import deserialize, parse_version, safe_version, which, which_import diff --git a/qcengine/programs/tests/standard_suite_runner.py b/qcengine/programs/tests/standard_suite_runner.py index 37a400a4..cb28148a 100644 --- a/qcengine/programs/tests/standard_suite_runner.py +++ b/qcengine/programs/tests/standard_suite_runner.py @@ -204,6 +204,15 @@ def runner_asserter(inp, ref_subject, method, basis, tnm, scramble, frame, model ref_block = mill_qcvars(ref2out_mill, ref_block) ref_block_conv = mill_qcvars(ref2out_mill, ref_block_conv) + # 3b. input mol in output: `wfn.input_data.molecule` should be exactly `subject` always. If frame=free fails, check + # that the harness is copying AtomicInput bodily rather than duplicating AtomicResult.molecule as convert_v(2) does + + if "v2" in tnm: + with np.printoptions(precision=3, suppress=True): + assert compare_values( + subject.geometry, wfn.input_data.molecule.geometry, atol=5.0e-8 + ), f"coords: atres ({wfn.input_data.molecule.geometry}) != atin ({subject.geometry})" + # <<< Comparison Tests >>> assert wfn.success is True diff --git a/qcengine/programs/tests/test_ghost.py b/qcengine/programs/tests/test_ghost.py index 76e46f86..d51d2407 100644 --- a/qcengine/programs/tests/test_ghost.py +++ b/qcengine/programs/tests/test_ghost.py @@ -53,7 +53,10 @@ def test_simple_ghost(driver, program, basis, keywords, hene_data, schema_versio res = qcng.compute(resi, program, raise_error=True, return_dict=True, return_version=retver) res = checkver_and_convert(res, request.node.name, "post") - assert res["driver"] == driver + if "v2" in request.node.name: + assert res["input_data"]["driver"] == driver + else: + assert res["driver"] == driver assert "provenance" in res assert res["success"] is True diff --git a/qcengine/programs/tests/test_nwchem.py b/qcengine/programs/tests/test_nwchem.py index f53cbf25..62311715 100644 --- a/qcengine/programs/tests/test_nwchem.py +++ b/qcengine/programs/tests/test_nwchem.py @@ -74,7 +74,10 @@ def test_b3lyp(nh2_data, schema_versions, request): # Make sure the calculation completed successfully assert compare_values(-55.554037, res["return_result"], atol=1e-3) - assert res["driver"] == "energy" + if "v2" in request.node.name: + assert res["input_data"]["driver"] == "energy" + else: + assert res["driver"] == "energy" assert "provenance" in res assert res["success"] is True @@ -194,7 +197,10 @@ def test_dipole(h20_data, schema_versions, request): # Make sure the calculation completed successfully assert compare_values(-75.764944, res["return_result"], atol=1e-3) - assert res["driver"] == "properties" + if "v2" in request.node.name: + assert res["input_data"]["driver"] == "properties" + else: + assert res["driver"] == "properties" assert "provenance" in res assert res["success"] is True @@ -244,7 +250,10 @@ def test_homo_lumo(h20v2_data, schema_versions, request): # Make sure the calculation completed successfully assert compare_values(-75.968095, res["return_result"], atol=1e-3) - assert res["driver"] == "energy" + if "v2" in request.node.name: + assert res["input_data"]["driver"] == "energy" + else: + assert res["driver"] == "energy" assert "provenance" in res assert res["success"] is True diff --git a/qcengine/programs/tests/test_standard_suite_ccsd(t).py b/qcengine/programs/tests/test_standard_suite_ccsd(t).py index 67b44f02..29b3ba80 100644 --- a/qcengine/programs/tests/test_standard_suite_ccsd(t).py +++ b/qcengine/programs/tests/test_standard_suite_ccsd(t).py @@ -54,7 +54,10 @@ def test_sp_ccsd_t_rhf_full(program, basis, keywords, h2o_data, schema_versions, res = qcng.compute(resi, program, raise_error=True, return_dict=True, return_version=retver) res = checkver_and_convert(res, request.node.name, "post") - assert res["driver"] == "energy" + if "v2" in request.node.name: + assert res["input_data"]["driver"] == "energy" + else: + assert res["driver"] == "energy" assert "provenance" in res assert res["success"] is True @@ -122,7 +125,10 @@ def test_sp_ccsd_t_rohf_full(program, basis, keywords, nh2_data, schema_versions res = checkver_and_convert(res, request.node.name, "post") res = res.model_dump() - assert res["driver"] == "energy" + if "v2" in request.node.name: + assert res["input_data"]["driver"] == "energy" + else: + assert res["driver"] == "energy" assert "provenance" in res assert res["success"] is True diff --git a/qcengine/programs/tests/test_standard_suite_hf.py b/qcengine/programs/tests/test_standard_suite_hf.py index a8506d3c..554d5d05 100644 --- a/qcengine/programs/tests/test_standard_suite_hf.py +++ b/qcengine/programs/tests/test_standard_suite_hf.py @@ -65,7 +65,10 @@ def test_sp_hf_rhf(program, basis, keywords, h2o_data, schema_versions, request) res = qcng.compute(resi, program, raise_error=True, return_dict=True, return_version=retver) res = checkver_and_convert(res, request.node.name, "post") - assert res["driver"] == "energy" + if "v2" in request.node.name: + assert res["input_data"]["driver"] == "energy" + else: + assert res["driver"] == "energy" assert "provenance" in res assert res["success"] is True @@ -119,7 +122,10 @@ def test_sp_hf_uhf(program, basis, keywords, nh2_data, schema_versions, request) assert res.success is True res = res.model_dump() - assert res["driver"] == "energy" + if "v2" in request.node.name: + assert res["input_data"]["driver"] == "energy" + else: + assert res["driver"] == "energy" assert "provenance" in res assert res["success"] is True @@ -163,7 +169,10 @@ def test_sp_hf_rohf(program, basis, keywords, nh2_data, schema_versions, request res = qcng.compute(resi, program, raise_error=True, return_dict=True, return_version=retver) res = checkver_and_convert(res, request.node.name, "post") - assert res["driver"] == "energy" + if "v2" in request.node.name: + assert res["input_data"]["driver"] == "energy" + else: + assert res["driver"] == "energy" assert "provenance" in res assert res["success"] is True diff --git a/qcengine/testing.py b/qcengine/testing.py index 5abc6ee6..24e7c1f4 100644 --- a/qcengine/testing.py +++ b/qcengine/testing.py @@ -262,19 +262,14 @@ def check_model_v2(m): if prepost == "pre": dict_in = isinstance(mdl, dict) + cast_smodel = cast_dict_as or "AtomicInput" if "as_v1" in tnm or "to_v2" in tnm or "None" in tnm: if dict_in: - if cast_dict_as: - mdl = getattr(qcel.models.v1, cast_dict_as)(**mdl) - else: - mdl = qcel.models.v1.AtomicInput(**mdl) + mdl = getattr(qcel.models.v1, cast_smodel)(**mdl) check_model_v1(mdl) elif "as_v2" in tnm or "to_v1" in tnm: if dict_in: - if cast_dict_as: - mdl = getattr(qcel.models.v2, cast_dict_as)(**mdl) - else: - mdl = qcel.models.v2.AtomicInput(**mdl) + mdl = getattr(qcel.models.v2, cast_smodel)(**mdl) check_model_v2(mdl) # NOW IN COMPUTE mdl = mdl.convert_v(1) @@ -286,19 +281,14 @@ def check_model_v2(m): # for now these always go to v1 in programs/model.py so as_v2 returns wrongly as v1 # follow-up: there are too many ways this can happen, so now it's forestalled by the schema_versions fixture passing 2 to as_v2 dict_in = isinstance(mdl, dict) + cast_smodel = cast_dict_as or "AtomicResult" if "as_v1" in tnm or "to_v1" in tnm or "None" in tnm: if dict_in: - if cast_dict_as: - mdl = getattr(qcel.models.v1, cast_dict_as)(**mdl) - else: - mdl = qcel.models.v1.AtomicResult(**mdl) + mdl = getattr(qcel.models.v1, cast_smodel)(**mdl) check_model_v1(mdl) elif "as_v2" in tnm or "to_v2" in tnm: if dict_in: - if cast_dict_as: - mdl = getattr(qcel.models.v2, cast_dict_as)(**mdl) - else: - mdl = qcel.models.v2.AtomicResult(**mdl) + mdl = getattr(qcel.models.v2, cast_smodel)(**mdl) # NOW IN COMPUTE mdl = mdl.convert_v(2) check_model_v2(mdl) diff --git a/qcengine/tests/test_harness_canonical.py b/qcengine/tests/test_harness_canonical.py index 0aebdb7a..d1a97b17 100644 --- a/qcengine/tests/test_harness_canonical.py +++ b/qcengine/tests/test_harness_canonical.py @@ -38,6 +38,8 @@ ("mctc-gcp", {"method": "dft/sv"}, {}), ("mace", {"method": "small"}, {}), ("aimnet2", {"method": "b973c"}, {}), + ("s-dftd3", {"method": "b3lyp-d3"}, {}), + ("dftd4", {"method": "b3lyp-d4"}, {}), # add as programs available # ("terachem", {"method": "bad"}), ] From a2ee2446e858c80a51aa653caf3ddf86f557c982 Mon Sep 17 00:00:00 2001 From: "Lori A. Burns" Date: Wed, 13 Nov 2024 13:01:36 -0500 Subject: [PATCH 02/13] openmm --- .github/workflows/CI.yml | 2 +- docs/source/changelog.rst | 12 +++++++----- qcengine/programs/openmm.py | 7 +++++-- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 2d0b75c5..c3740142 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -165,7 +165,7 @@ jobs: #if: false run: | conda remove qcelemental --force - python -m pip install 'git+https://github.com/loriab/QCElemental.git@csse_layout_536a' --no-deps + python -m pip install 'git+https://github.com/loriab/QCElemental.git@csse_layout_536b' --no-deps # note: conda remove --force, not mamba remove --force b/c https://github.com/mamba-org/mamba/issues/412 # alt. is micromamba but not yet ready for setup-miniconda https://github.com/conda-incubator/setup-miniconda/issues/75 diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index ee06a064..fbfac2dc 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -70,11 +70,13 @@ Misc. MUST (Unmerged) +++++++++++++++ -- When qcengine.compute() fails and forms a fop = FailedOperation (raise_error=T), with v2, `fop.input_data` will be an <>Input model (when possible; if the error was in forming the model, it'll still be a dict), not always a dict like v1. -- When .compute() fails and collects the input for processing, with v2 it now uses the <>Input model passed to the executor, not the model-or-dict passed into compute(). -- The net result of the two above is that whereas fop.input_data in v1 was reliably a dict and its contents would reflect whether a model or dict was passed to qcengine.compute(), now in v2, fop.input_data is a model whenever possible (to mirror <>Result.input_data) regardless of model or dict passed to qcengine.compute(); the only case where it's a dict is if the error was in forming the model. -- DFTD3 & DFTD4 (new intf) - intercept ``v1.AtomicResult`` with ``success=False`` and ``error`` fields set from QCSchema interfaces and return ``FailedOperation``s. Someday when upstream switches to v2, request packages return FaileOp directly and use ``input_error`` rather than ``input error``. -- ``qcengine run`` learned new argument ``--return-version`` analogous to ``qcengine.compute(..., return_version=1|2)`` so CLI matches API capabilities. Note *not* ported to phasing-out ``qcengine run-procedure``. +If you're missing something from AtomicResult.extras, check AtomicResult.input_data.extras in case it was passed in on input +- (:pr:`459`) OpenMM gained AtomicResult.properties.return_gradient +- (:pr:`458`) When qcengine.compute() fails and forms a fop = FailedOperation (raise_error=T), with v2, `fop.input_data` will be an <>Input model (when possible; if the error was in forming the model, it'll still be a dict), not always a dict like v1. +- (:pr:`458`) When .compute() fails and collects the input for processing, with v2 it now uses the <>Input model passed to the executor, not the model-or-dict passed into compute(). +- (:pr:`458`) The net result of the two above is that whereas fop.input_data in v1 was reliably a dict and its contents would reflect whether a model or dict was passed to qcengine.compute(), now in v2, fop.input_data is a model whenever possible (to mirror <>Result.input_data) regardless of model or dict passed to qcengine.compute(); the only case where it's a dict is if the error was in forming the model. +- (:pr:`458`) DFTD3 & DFTD4 (new intf) - intercept ``v1.AtomicResult`` with ``success=False`` and ``error`` fields set from QCSchema interfaces and return ``FailedOperation``s. Someday when upstream switches to v2, request packages return FaileOp directly and use ``input_error`` rather than ``input error``. +- (:pr:`458`) ``qcengine run`` learned new argument ``--return-version`` analogous to ``qcengine.compute(..., return_version=1|2)`` so CLI matches API capabilities. Note *not* ported to phasing-out ``qcengine run-procedure``. WIP (Unmerged) ++++++++++++++ diff --git a/qcengine/programs/openmm.py b/qcengine/programs/openmm.py index f3f5f4c1..ab0a1ef7 100644 --- a/qcengine/programs/openmm.py +++ b/qcengine/programs/openmm.py @@ -324,13 +324,16 @@ def compute(self, input_model: "AtomicInput", config: "TaskConfig") -> "AtomicRe # Force to gradient ret_data["return_result"] = -1 * q + ret_data["properties"]["return_gradient"] = -1 * q + ret_data["properties"]["calcinfo_natom"] = len(input_model.molecule.symbols) else: raise InputError(f"Driver {input_model.driver} not implemented for OpenMM.") ret_data["success"] = True - ret_data["extras"] = input_model.extras + ret_data["input_data"] = input_model + ret_data["molecule"] = input_model.molecule # should connectivity be added from off_mol? # Move several pieces up a level ret_data["provenance"] = Provenance(creator="openmm", version=openmm.version.short_version, nthreads=nthreads) - return AtomicResult(**{**input_model.dict(), **ret_data}) + return AtomicResult(**ret_data) From a9a12a5beddb6539ce3b8fed196dfd079371bbfb Mon Sep 17 00:00:00 2001 From: "Lori A. Burns" Date: Thu, 14 Nov 2024 00:56:00 -0500 Subject: [PATCH 03/13] xtb --- qcengine/programs/dftd_ng.py | 14 +++++++------- qcengine/programs/tests/test_dftd4.py | 6 ++++++ qcengine/programs/xtb.py | 13 +++++-------- qcengine/tests/test_harness_canonical.py | 6 +++++- 4 files changed, 23 insertions(+), 16 deletions(-) diff --git a/qcengine/programs/dftd_ng.py b/qcengine/programs/dftd_ng.py index 5c4314d4..049f14ab 100644 --- a/qcengine/programs/dftd_ng.py +++ b/qcengine/programs/dftd_ng.py @@ -105,16 +105,16 @@ def compute(self, input_model: AtomicInput, config: TaskConfig) -> AtomicResult: input_data["keywords"]["level_hint"] = level_hint # dftd4 speaks qcsk.v1 - input_model = qcelemental.models.v1.AtomicInput(**input_data) + input_model_v1 = qcelemental.models.v1.AtomicInput(**input_data) # Run the Harness - output = run_qcschema(input_model) + output_v1 = run_qcschema(input_model_v1) # d4 qcschema interface stores error in Result model - if not output.success: - return FailedOperation(input_data=input_data, error=output.error.model_dump()) + if not output_v1.success: + return FailedOperation(input_data=input_data, error=output_v1.error.model_dump()) - output = output.convert_v(2) + output = output_v1.convert_v(2, external_input_data=input_model) if "info" in output.extras: qcvkey = output.extras["info"]["fctldash"].upper() @@ -126,14 +126,14 @@ def compute(self, input_model: AtomicInput, config: TaskConfig) -> AtomicResult: if qcvkey: calcinfo[f"{qcvkey} DISPERSION CORRECTION ENERGY"] = energy - if output.driver == "gradient": + if output.input_data.driver == "gradient": gradient = output.return_result calcinfo["CURRENT GRADIENT"] = gradient calcinfo["DISPERSION CORRECTION GRADIENT"] = gradient if qcvkey: calcinfo[f"{qcvkey} DISPERSION CORRECTION GRADIENT"] = gradient - if output.keywords.get("pair_resolved", False): + if output.input_data.keywords.get("pair_resolved", False): pw2 = output.extras["dftd4"]["additive pairwise energy"] pw3 = output.extras["dftd4"]["non-additive pairwise energy"] assert abs(pw2.sum() + pw3.sum() - energy) < 1.0e-8, f"{pw2.sum()} + {pw3.sum()} != {energy}" diff --git a/qcengine/programs/tests/test_dftd4.py b/qcengine/programs/tests/test_dftd4.py index 99a9d227..3de02291 100644 --- a/qcengine/programs/tests/test_dftd4.py +++ b/qcengine/programs/tests/test_dftd4.py @@ -74,6 +74,7 @@ def test_dftd4_task_tpss_m02(schema_versions, request): }, }, driver="gradient", + extras={"mymsg": "will I pass through the calc?"}, ) atomic_input = checkver_and_convert(atomic_input, request.node.name, "pre") @@ -82,6 +83,11 @@ def test_dftd4_task_tpss_m02(schema_versions, request): assert atomic_result.success assert pytest.approx(atomic_result.return_result, abs=thr) == return_result + if "v2" in request.node.name: + assert "will I pass" in atomic_result.input_data.extras.get("mymsg", "no key!"), "input extras roundtrip fail" + assert "mymsg" not in atomic_result.extras.get("mymsg", "no key!"), "input extras wrongly present in result" + else: + assert "will I pass" in atomic_result.extras.get("mymsg", "no key!"), "input extras roundtrip fail" @using("dftd4") diff --git a/qcengine/programs/xtb.py b/qcengine/programs/xtb.py index c58ad97b..94e27595 100644 --- a/qcengine/programs/xtb.py +++ b/qcengine/programs/xtb.py @@ -66,15 +66,12 @@ def compute(self, input_data: AtomicInput, config: TaskConfig) -> AtomicResult: from xtb.qcschema.harness import run_qcschema # Run the Harness - input_data = input_data.convert_v(1) - output = run_qcschema(input_data) + input_data_v1 = input_data.convert_v(1) + output_v1 = run_qcschema(input_data_v1) # xtb qcschema interface stores error in Result model - if not output.success: - return FailedOperation(input_data=input_data, error=output.error.model_dump()) + if not output_v1.success: + return FailedOperation(input_data=input_data, error=output_v1.error.model_dump()) - output = output.convert_v(2) - - # Make sure all keys from the initial input spec are sent along - output.extras.update(input_data.extras) + output = output_v1.convert_v(2, external_input_data=input_data) return output diff --git a/qcengine/tests/test_harness_canonical.py b/qcengine/tests/test_harness_canonical.py index d1a97b17..181b3b13 100644 --- a/qcengine/tests/test_harness_canonical.py +++ b/qcengine/tests/test_harness_canonical.py @@ -113,7 +113,11 @@ def test_compute_gradient(program, model, keywords, schema_versions, request): assert isinstance(ret.return_result, np.ndarray) assert len(ret.return_result.shape) == 2 assert ret.return_result.shape[1] == 3 - assert "mytag" in ret.extras, ret.extras + if "v2" in request.node.name: + assert "mytag" in ret.input_data.extras, ret.input_data.extras + assert "mytag" not in ret.extras, "input extras wrongly present in result" + else: + assert "mytag" in ret.extras, ret.extras @pytest.mark.parametrize("program, model, keywords", _canonical_methods_qcsk_basis) From d177730fe119a4a41ba12c689e7d8212ae7e8ae0 Mon Sep 17 00:00:00 2001 From: "Lori A. Burns" Date: Thu, 14 Nov 2024 12:26:16 -0500 Subject: [PATCH 04/13] s-dftd3 and gcp input_data --- qcengine/programs/dftd_ng.py | 17 +++++++++-------- qcengine/programs/gcp.py | 14 +++++++++++--- qcengine/tests/test_harness_canonical.py | 1 + 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/qcengine/programs/dftd_ng.py b/qcengine/programs/dftd_ng.py index 049f14ab..f4ec2757 100644 --- a/qcengine/programs/dftd_ng.py +++ b/qcengine/programs/dftd_ng.py @@ -14,7 +14,7 @@ from qcelemental.util import parse_version, safe_version, which_import from ..config import TaskConfig -from ..exceptions import InputError +from ..exceptions import InputError, ResourceError from .empirical_dispersion_resources import from_arrays, get_dispersion_aliases from .model import ProgramHarness @@ -279,16 +279,17 @@ def compute(self, input_model: AtomicInput, config: TaskConfig) -> AtomicResult: input_data["keywords"]["params_tweaks"] = {**planinfo["dashparams"], "s9": 0.0} input_data["keywords"]["level_hint"] = level_hint - input_model = qcelemental.models.v1.AtomicInput(**input_data) + # sdftd3 speaks qcsk.v1 + input_model_v1 = qcelemental.models.v1.AtomicInput(**input_data) # Run the Harness - output = run_qcschema(input_model) + output_v1 = run_qcschema(input_model_v1) # d3 qcschema interface stores error in Result model - if not output.success: - return FailedOperation(input_data=input_data, error=output.error.model_dump()) + if not output_v1.success: + return FailedOperation(input_data=input_data, error=output_v1.error.model_dump()) - output = output.convert_v(2) + output = output_v1.convert_v(2, external_input_data=input_model) if "info" in output.extras: qcvkey = output.extras["info"]["fctldash"].upper() @@ -300,14 +301,14 @@ def compute(self, input_model: AtomicInput, config: TaskConfig) -> AtomicResult: if qcvkey: calcinfo[f"{qcvkey} DISPERSION CORRECTION ENERGY"] = energy - if output.driver == "gradient": + if output.input_data.driver == "gradient": gradient = output.return_result calcinfo["CURRENT GRADIENT"] = gradient calcinfo["DISPERSION CORRECTION GRADIENT"] = gradient if qcvkey: calcinfo[f"{qcvkey} DISPERSION CORRECTION GRADIENT"] = gradient - if output.keywords.get("pair_resolved", False): + if output.input_data.keywords.get("pair_resolved", False): pw2 = output.extras["dftd3"]["additive pairwise energy"] pw3 = output.extras["dftd3"]["non-additive pairwise energy"] assert abs(pw2.sum() + pw3.sum() - energy) < 1.0e-8, f"{pw2.sum()} + {pw3.sum()} != {energy}" diff --git a/qcengine/programs/gcp.py b/qcengine/programs/gcp.py index 5182752e..f47c88ab 100644 --- a/qcengine/programs/gcp.py +++ b/qcengine/programs/gcp.py @@ -261,10 +261,18 @@ def parse_output(self, outfiles: Dict[str, str], input_model: "AtomicInput") -> elif isinstance(retres, np.ndarray): retres = retres.ravel().tolist() + properties = { + "calcinfo_natom": len(input_model.molecule.symbols), + "return_energy": ene, + f"return_{input_model.driver.value}": retres, + } + output_data = { - "extras": input_model.extras, + "input_data": input_model, + "molecule": input_model.molecule, + "extras": {}, "native_files": {k: v for k, v in outfiles.items() if v is not None}, - "properties": {}, + "properties": properties, "provenance": Provenance( creator="GCP", version=self.get_version(), routine=__name__ + "." + sys._getframe().f_code.co_name ), @@ -276,7 +284,7 @@ def parse_output(self, outfiles: Dict[str, str], input_model: "AtomicInput") -> output_data["extras"]["qcvars"] = calcinfo output_data["success"] = True - return AtomicResult(**{**input_model.dict(), **output_data}) + return AtomicResult(**output_data) class MCTCGCPHarness(GCPHarness): diff --git a/qcengine/tests/test_harness_canonical.py b/qcengine/tests/test_harness_canonical.py index 181b3b13..d156e63d 100644 --- a/qcengine/tests/test_harness_canonical.py +++ b/qcengine/tests/test_harness_canonical.py @@ -83,6 +83,7 @@ def test_compute_energy(program, model, keywords, schema_versions, request): assert ret.success is True assert isinstance(ret.return_result, float) + assert ret.return_result == ret.properties.return_energy @pytest.mark.parametrize("program, model, keywords", _canonical_methods) From df2eec7122367eed014adfc8b78abdf6298436a2 Mon Sep 17 00:00:00 2001 From: "Lori A. Burns" Date: Thu, 14 Nov 2024 12:56:01 -0500 Subject: [PATCH 05/13] terachem --- qcengine/programs/terachem.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/qcengine/programs/terachem.py b/qcengine/programs/terachem.py index 31dc9242..29d3a971 100644 --- a/qcengine/programs/terachem.py +++ b/qcengine/programs/terachem.py @@ -191,7 +191,11 @@ def parse_output(self, outfiles: Dict[str, str], input_model: "AtomicInput") -> for extra in input_model.extras.keys(): input_model.extras[extra] = outfiles[extra] - return AtomicResult(**{**input_model.dict(), **output_data}) + output_data["input_data"] = input_model + output_data["molecule"] = input_model.molecule + output_data["provenance"] = input_model.provenance # TODO + + return AtomicResult(**output_data) def execute(self, inputs, extra_outfiles=None, extra_commands=None, scratch_name=None, timeout=None): binaries = [] From cfb01b9309b49c5b2e2b501d49c2ea776fceb875 Mon Sep 17 00:00:00 2001 From: "Lori A. Burns" Date: Thu, 14 Nov 2024 13:49:31 -0500 Subject: [PATCH 06/13] qchem mp2d molpro --- qcengine/programs/molpro.py | 5 +++-- qcengine/programs/mp2d.py | 14 +++++++++++--- qcengine/programs/qchem.py | 14 ++++++++++---- qcengine/programs/tests/test_molpro.py | 3 ++- qcengine/programs/tests/test_qchem.py | 3 ++- qcengine/programs/tests/test_terachem.py | 3 ++- 6 files changed, 30 insertions(+), 12 deletions(-) diff --git a/qcengine/programs/molpro.py b/qcengine/programs/molpro.py index 88c946f2..b655c090 100644 --- a/qcengine/programs/molpro.py +++ b/qcengine/programs/molpro.py @@ -432,7 +432,7 @@ def parse_output(self, outfiles: Dict[str, str], input_model: "AtomicInput") -> raise KeyError(f"Could not find {method} total energy") # Initialize output_data by copying over input_model.dict() - output_data = input_model.dict() + output_data = {"input_data": input_model, "molecule": input_model.molecule} # TODO better mol? # Determining return_result if input_model.driver == "energy": @@ -442,9 +442,10 @@ def parse_output(self, outfiles: Dict[str, str], input_model: "AtomicInput") -> # Final output_data assignments needed for the AtomicResult object output_data["properties"] = properties - output_data["extras"].update(extras) + output_data["extras"] = extras output_data["schema_name"] = "qcschema_output" output_data["stdout"] = outfiles["dispatch.out"] output_data["success"] = True + output_data["provenance"] = input_model.provenance # TODO better stamp? return AtomicResult(**output_data) diff --git a/qcengine/programs/mp2d.py b/qcengine/programs/mp2d.py index df646fa0..e0de53de 100644 --- a/qcengine/programs/mp2d.py +++ b/qcengine/programs/mp2d.py @@ -214,10 +214,18 @@ def parse_output(self, outfiles: Dict[str, str], input_model: "AtomicInput") -> elif isinstance(retres, np.ndarray): retres = retres.ravel().tolist() + properties = { + "calcinfo_natom": len(input_model.molecule.symbols), + "return_energy": ene, + f"return_{input_model.driver.value}": retres, + } + output_data = { - "extras": input_model.extras, + "input_data": input_model, + "molecule": input_model.molecule, + "extras": {}, "native_files": {k: v for k, v in outfiles.items() if v is not None}, - "properties": {}, + "properties": properties, "provenance": Provenance( creator="MP2D", version=self.get_version(), routine=__name__ + "." + sys._getframe().f_code.co_name ), @@ -229,4 +237,4 @@ def parse_output(self, outfiles: Dict[str, str], input_model: "AtomicInput") -> output_data["extras"]["qcvars"] = calcinfo output_data["success"] = True - return AtomicResult(**{**input_model.dict(), **output_data}) + return AtomicResult(**output_data) diff --git a/qcengine/programs/qchem.py b/qcengine/programs/qchem.py index de8d3daf..d2a587ca 100644 --- a/qcengine/programs/qchem.py +++ b/qcengine/programs/qchem.py @@ -306,11 +306,12 @@ def parse_output(self, outfiles: Dict[str, str], input_model: "AtomicInput") -> output_data["properties"].update(props) output_data["stdout"] = outfiles["dispatch.out"] output_data["success"] = True + output_data["input_data"] = input_model + output_data["molecule"] = input_model.molecule - merged_data = {**input_model.convert_v(2).model_dump(), **output_data} - merged_data["extras"]["qcvars"] = qcvars + output_data["extras"] = {"qcvars": qcvars} - return AtomicResult(**merged_data) + return AtomicResult(**output_data) def _parse_logfile_common(self, outtext: str, input_dict: Dict[str, Any]): """ @@ -449,7 +450,12 @@ def parse_logfile(self, outfiles: Dict[str, str]) -> AtomicResult: qcscr_result = self.parse_output(outfiles, AtomicInput(**input_dict)).dict() except KeyError: props, prov = self._parse_logfile_common(outtext, input_dict) - qcscr_result = {"properties": props, "provenance": prov, **input_dict} + qcscr_result = { + "properties": props, + "provenance": prov, + "input_data": input_dict, + "molecule": input_dict["molecule"], + } mobj = re.search(r"\n\s*Total\s+energy in the final basis set =\s+" + NUMBER + r"\s*\n", outtext) if mobj and qcscr_result["properties"].get("scf_total_energy", None) is None: diff --git a/qcengine/programs/tests/test_molpro.py b/qcengine/programs/tests/test_molpro.py index a3a3f4f8..32716541 100644 --- a/qcengine/programs/tests/test_molpro.py +++ b/qcengine/programs/tests/test_molpro.py @@ -15,8 +15,9 @@ def test_molpro_output_parser(test_case): data = molpro_info.get_test_data(test_case) inp = qcel.models.v1.AtomicInput.parse_raw(data["input.json"]) + # only qcng.compute() handles schema versions. test_data returns v1 and parse_output returns v2, so need to convert + inp = inp.convert_v(2) output = qcng.get_program("molpro", check=False).parse_output(data, inp) - # only qcng.compute() handles schema versions. above returns v2, so need to convert output = output.convert_v(1).dict() output.pop("provenance", None) output.pop("schema_version", None) diff --git a/qcengine/programs/tests/test_qchem.py b/qcengine/programs/tests/test_qchem.py index f6d71424..b7c26bec 100644 --- a/qcengine/programs/tests/test_qchem.py +++ b/qcengine/programs/tests/test_qchem.py @@ -29,8 +29,9 @@ def test_qchem_output_parser(test_case): inp = qcel.models.v1.AtomicInput.parse_raw(data["input.json"]) outfiles = qcel.util.deserialize(data["outfiles.msgpack"], "msgpack-ext") + # only qcng.compute() handles schema versions. test_data returns v1 and parse_output returns v2, so need to convert + inp = inp.convert_v(2) output = qcng.get_program("qchem", check=False).parse_output(outfiles, inp) - # only qcng.compute() handles schema versions. above returns v2, so need to convert output = output.convert_v(1).dict() output.pop("provenance", None) diff --git a/qcengine/programs/tests/test_terachem.py b/qcengine/programs/tests/test_terachem.py index 2c5c6856..93c3f3ec 100644 --- a/qcengine/programs/tests/test_terachem.py +++ b/qcengine/programs/tests/test_terachem.py @@ -15,8 +15,9 @@ def test_terachem_output_parser(test_case): data = terachem_info.get_test_data(test_case) inp = qcel.models.v1.AtomicInput.parse_raw(data["input.json"]) + # only qcng.compute() handles schema versions. test_data returns v1 and parse_output returns v2, so need to convert + inp = inp.convert_v(2) output = qcng.get_program("terachem", check=False).parse_output(data, inp) - # only qcng.compute() handles schema versions. above returns v2, so need to convert output = output.convert_v(1).dict() output_ref = qcel.models.v1.AtomicResult.parse_raw(data["output.json"]).dict() From 86e34ab645ddfb02c9bb4cbc24ea5cbae5bf2c45 Mon Sep 17 00:00:00 2001 From: "Lori A. Burns" Date: Thu, 14 Nov 2024 15:40:23 -0500 Subject: [PATCH 07/13] failure_engine and mace --- qcengine/programs/mace.py | 5 +++-- qcengine/testing.py | 17 ++++++++--------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/qcengine/programs/mace.py b/qcengine/programs/mace.py index ab81fd0a..22809358 100644 --- a/qcengine/programs/mace.py +++ b/qcengine/programs/mace.py @@ -133,10 +133,11 @@ def compute(self, input_data: "AtomicInput", config: "TaskConfig") -> Union["Ato else: raise InputError("MACE only supports the energy and gradient driver methods.") - ret_data["extras"] = input_data.extras.copy() + ret_data["input_data"] = input_data + ret_data["molecule"] = input_data.molecule ret_data["provenance"] = Provenance(creator="mace", version=mace.__version__, routine="mace") ret_data["schema_name"] = "qcschema_output" ret_data["success"] = True # Form up a dict first, then sent to BaseModel to avoid repeat kwargs which don't override each other - return AtomicResult(**{**input_data.dict(), **ret_data}) + return AtomicResult(**ret_data) diff --git a/qcengine/testing.py b/qcengine/testing.py index 24e7c1f4..7c744a32 100644 --- a/qcengine/testing.py +++ b/qcengine/testing.py @@ -116,16 +116,15 @@ def compute(self, input_data: "AtomicInput", config: "TaskConfig") -> "AtomicRes grad = [0, 0, -grad_value, 0, 0, grad_value] if mode == "pass": - return schema_versions[2].AtomicResult( + return qcel.models.v2.AtomicResult( **{ - **input_data.dict(), - **{ - "properties": {"return_energy": grad_value}, - "return_result": grad, - "success": True, - "extras": {"ncalls": self.ncalls}, - "provenance": {"creator": "failure_engine", "ncores": config.ncores}, - }, + "input_data": input_data, + "molecule": input_data.molecule, + "properties": {"return_energy": grad_value}, + "return_result": grad, + "success": True, + "extras": {"ncalls": self.ncalls}, + "provenance": {"creator": "failure_engine", "ncores": config.ncores}, } ) elif mode == "random_error": From 47e2e6b75d8a4bc039bc80d3ef98afa149e0e0d7 Mon Sep 17 00:00:00 2001 From: "Lori A. Burns" Date: Thu, 14 Nov 2024 16:21:28 -0500 Subject: [PATCH 08/13] cfour and torchani --- qcengine/programs/cfour/runner.py | 7 ++++--- qcengine/programs/dftd3.py | 6 ++++-- qcengine/programs/tests/test_dftd3_mp2d.py | 5 ++++- qcengine/programs/torchani.py | 19 +++++++++---------- 4 files changed, 21 insertions(+), 16 deletions(-) diff --git a/qcengine/programs/cfour/runner.py b/qcengine/programs/cfour/runner.py index e522ecfa..b7799170 100644 --- a/qcengine/programs/cfour/runner.py +++ b/qcengine/programs/cfour/runner.py @@ -205,9 +205,10 @@ def parse_output( provenance["module"] = module output_data = { - "schema_version": 1, + "schema_version": 2, + "input_data": input_model, "molecule": c4mol, # overwrites with outfile Cartesians in case fix_*=F - "extras": {**input_model.extras}, + "extras": {}, "native_files": {k: v for k, v in outfiles.items() if v is not None}, "properties": atprop, "provenance": provenance, @@ -224,4 +225,4 @@ def parse_output( k.upper(): str(v) if isinstance(v, Decimal) else v for k, v in qcvars.items() } - return AtomicResult(**{**input_model.dict(), **output_data}) + return AtomicResult(**output_data) diff --git a/qcengine/programs/dftd3.py b/qcengine/programs/dftd3.py index d3a11fa7..c47f9024 100644 --- a/qcengine/programs/dftd3.py +++ b/qcengine/programs/dftd3.py @@ -280,7 +280,9 @@ def parse_output(self, outfiles: Dict[str, str], input_model: "AtomicInput") -> retres = retres.ravel().tolist() output_data = { - "extras": input_model.extras, + "input_data": input_model, + "molecule": input_model.molecule, + "extras": {}, "native_files": {k: v for k, v in outfiles.items() if v is not None}, "properties": { "return_energy": calcinfo[f"CURRENT ENERGY"], @@ -301,7 +303,7 @@ def parse_output(self, outfiles: Dict[str, str], input_model: "AtomicInput") -> output_data["extras"]["qcvars"]["2-BODY PAIRWISE DISPERSION CORRECTION ANALYSIS"] = D3pairs output_data["success"] = True - return AtomicResult(**{**input_model.dict(), **output_data}) + return AtomicResult(**output_data) def dftd3_coeff_formatter(dashlvl: str, dashcoeff: Dict) -> str: diff --git a/qcengine/programs/tests/test_dftd3_mp2d.py b/qcengine/programs/tests/test_dftd3_mp2d.py index 38c8824b..b8210432 100644 --- a/qcengine/programs/tests/test_dftd3_mp2d.py +++ b/qcengine/programs/tests/test_dftd3_mp2d.py @@ -26,7 +26,10 @@ def test_dftd3_task(method, schema_versions, request): ret = qcng.compute(json_data, "dftd3", raise_error=True, return_dict=True, return_version=retver) ret = checkver_and_convert(ret, request.node.name, "post") - assert ret["driver"] == "energy" + if "v2" in request.node.name: + assert ret["input_data"]["driver"] == "energy" + else: + assert ret["driver"] == "energy" assert "provenance" in ret assert "normal termination of dftd3" in ret["stdout"] diff --git a/qcengine/programs/torchani.py b/qcengine/programs/torchani.py index 920c8b7d..6615dd4b 100644 --- a/qcengine/programs/torchani.py +++ b/qcengine/programs/torchani.py @@ -168,15 +168,14 @@ def compute(self, input_data: "AtomicInput", config: "TaskConfig") -> "AtomicRes # the reliability of the models in an ensemble, and produce more data # points in the regions where this quantity is below a certain # threshold (inclusion criteria) - ret_data["extras"] = input_data.extras.copy() - ret_data["extras"].update( - { - "ensemble_energies": energy_array.cpu().detach().numpy(), - "ensemble_energy_avg": energy.item(), - "ensemble_energy_std": ensemble_std.item(), - "ensemble_per_root_atom_disagreement": ensemble_scaled_std.item(), - } - ) + ret_data["input_data"] = input_data + ret_data["molecule"] = input_data.molecule + ret_data["extras"] = { + "ensemble_energies": energy_array.cpu().detach().numpy(), + "ensemble_energy_avg": energy.item(), + "ensemble_energy_std": ensemble_std.item(), + "ensemble_per_root_atom_disagreement": ensemble_scaled_std.item(), + } ret_data["provenance"] = Provenance( creator="torchani", version="unknown", routine="torchani.builtin.aev_computer" @@ -186,4 +185,4 @@ def compute(self, input_data: "AtomicInput", config: "TaskConfig") -> "AtomicRes ret_data["success"] = True # Form up a dict first, then sent to BaseModel to avoid repeat kwargs which don't override each other - return AtomicResult(**{**input_data.dict(), **ret_data}) + return AtomicResult(**ret_data) From 6614e8bcc0752b66fc0b123936a8771d3ba7aeec Mon Sep 17 00:00:00 2001 From: "Lori A. Burns" Date: Thu, 14 Nov 2024 17:46:14 -0500 Subject: [PATCH 09/13] input_data: adcc, aimnet2, mopac, rdkit, psi4 --- qcengine/programs/adcc.py | 3 +- qcengine/programs/aimnet2.py | 6 ++-- qcengine/programs/mopac.py | 4 +-- qcengine/programs/psi4.py | 44 +++++++++++------------- qcengine/programs/rdkit.py | 4 ++- qcengine/programs/tests/test_programs.py | 10 ++++-- 6 files changed, 40 insertions(+), 31 deletions(-) diff --git a/qcengine/programs/adcc.py b/qcengine/programs/adcc.py index 560329f9..dfd20b04 100644 --- a/qcengine/programs/adcc.py +++ b/qcengine/programs/adcc.py @@ -116,7 +116,7 @@ def compute(self, input_model: "AtomicInput", config: "TaskConfig") -> "AtomicRe raise UnknownError(str(e)) input_data = input_model.model_dump(encoding="json") - output_data = input_data.copy() + output_data = {"input_data": input_data, "extras": {}, "molecule": mol} output_data["success"] = compute_success if compute_success: @@ -124,6 +124,7 @@ def compute(self, input_model: "AtomicInput", config: "TaskConfig") -> "AtomicRe extract_props = input_model.driver == "properties" qcvars = adcc_state.to_qcvars(recurse=True, properties=extract_props) + qcvars["CURRENT ENERGY"] = adcc_state.excitation_energy[0] atprop = build_atomicproperties(qcvars) output_data["extras"]["qcvars"] = qcvars output_data["properties"] = atprop diff --git a/qcengine/programs/aimnet2.py b/qcengine/programs/aimnet2.py index 4c577cb3..2ba6d204 100644 --- a/qcengine/programs/aimnet2.py +++ b/qcengine/programs/aimnet2.py @@ -93,6 +93,8 @@ def compute(self, input_data: "AtomicInput", config: "TaskConfig"): out = model(aimnet_input) ret_data = { + "input_data": input_data, + "molecule": input_data.molecule, "success": False, "properties": { "return_energy": out["energy"].item() * ureg.conversion_factor("eV", "hartree"), @@ -101,7 +103,7 @@ def compute(self, input_data: "AtomicInput", config: "TaskConfig"): ), "calcinfo_natom": len(input_data.molecule.atomic_numbers), }, - "extras": input_data.extras.copy(), + "extras": {}, } # update with calculated extras ret_data["extras"]["aimnet2"] = { @@ -123,4 +125,4 @@ def compute(self, input_data: "AtomicInput", config: "TaskConfig"): ret_data["success"] = True - return AtomicResult(**{**input_data.dict(), **ret_data}) + return AtomicResult(**ret_data) diff --git a/qcengine/programs/mopac.py b/qcengine/programs/mopac.py index dd5248a1..c6e2d315 100644 --- a/qcengine/programs/mopac.py +++ b/qcengine/programs/mopac.py @@ -278,13 +278,13 @@ def parse_output(self, outfiles: Dict[str, str], input_model: "AtomicInput") -> gradient = data.pop("gradients") - output = input_model.dict() + output = {"input_data": input_model, "molecule": input_model.molecule} output["provenance"] = {"creator": "mopac", "version": data.pop("mopac_version")} output["properties"] = {} output["properties"]["return_energy"] = data["heat_of_formation"] - output["extras"].update(data) + output["extras"] = data if input_model.driver == "energy": output["return_result"] = data["heat_of_formation"] diff --git a/qcengine/programs/psi4.py b/qcengine/programs/psi4.py index 3e11645f..4d474a57 100644 --- a/qcengine/programs/psi4.py +++ b/qcengine/programs/psi4.py @@ -185,11 +185,9 @@ def compute(self, input_model: "AtomicInput", config: "TaskConfig") -> "AtomicRe scratch_directory=tmpdir, ) - output_data = input_data.copy() + output_data = {"input_data": input_model, "extras": {}} if success: output_data = json.loads(output["outfiles"]["data.json"]) - if "extras" not in output_data: - output_data["extras"] = {} # Check QCVars local_qcvars = output_data.pop("psi4:qcvars", None) @@ -218,9 +216,9 @@ def compute(self, input_model: "AtomicInput", config: "TaskConfig") -> "AtomicRe else: # psi4 QCSchema interface only speaks qcsk.v1 # * note that only psiapi=True calcs need this until rearrangement affects all - input_model = input_model.convert_v(1) + input_model_v1 = input_model.convert_v(1) - if input_model.extras.get("psiapi", False): + if input_model_v1.extras.get("psiapi", False): import psi4 orig_scr = psi4.core.IOManager.shared_object().get_default_path() @@ -230,13 +228,13 @@ def compute(self, input_model: "AtomicInput", config: "TaskConfig") -> "AtomicRe if pversion < parse_version("1.6"): # adjust to where DDD merged # slightly dangerous in that if `qcng.compute({..., psiapi=True}, "psi4")` called *from psi4 # session*, session could unexpectedly get its own files cleaned away. - output_data = psi4.schema_wrapper.run_qcschema(input_model).dict() + output_data_v1 = psi4.schema_wrapper.run_qcschema(input_model_v1).dict() else: - output_data = psi4.schema_wrapper.run_qcschema(input_model, postclean=False).dict() + output_data_v1 = psi4.schema_wrapper.run_qcschema(input_model_v1, postclean=False).dict() # success here means execution returned. output_data may yet be qcel.models.AtomicResult or qcel.models.FailedOperation success = True - if output_data.get("success", False): - output_data["extras"]["psiapi_evaluated"] = True + if output_data_v1.get("success", False): + output_data_v1["extras"]["psiapi_evaluated"] = True psi4.core.IOManager.shared_object().set_default_path(orig_scr) else: run_cmd = [ @@ -252,18 +250,18 @@ def compute(self, input_model: "AtomicInput", config: "TaskConfig") -> "AtomicRe ] if config.scratch_messy: run_cmd.append("--messy") - input_files = {"data.msgpack": input_model.serialize("msgpack-ext")} + input_files = {"data.msgpack": input_model_v1.serialize("msgpack-ext")} success, output = execute( run_cmd, input_files, ["data.msgpack"], as_binary=["data.msgpack"], scratch_directory=tmpdir ) if success: - output_data = deserialize(output["outfiles"]["data.msgpack"], "msgpack-ext") + output_data_v1 = deserialize(output["outfiles"]["data.msgpack"], "msgpack-ext") else: - output_data = input_model.dict() + output_data_v1 = input_model_v1 if success: - if output_data.get("success", False) is False: - error_message, error_type = self._handle_errors(output_data) + if output_data_v1.get("success", False) is False: + error_message, error_type = self._handle_errors(output_data_v1) else: compute_success = True else: @@ -294,18 +292,18 @@ def compute(self, input_model: "AtomicInput", config: "TaskConfig") -> "AtomicRe raise UnknownError(error_message) # Reset basis - output_data["model"]["basis"] = old_basis + output_data_v1["model"]["basis"] = old_basis # Move several pieces up a level - output_data["provenance"]["memory"] = round(config.memory, 3) - output_data["provenance"]["nthreads"] = config.ncores - if output_data.get("native_files", None) is None: - output_data["native_files"] = { - "input": json.dumps(json.loads(input_model.json()), indent=1), + output_data_v1["provenance"]["memory"] = round(config.memory, 3) + output_data_v1["provenance"]["nthreads"] = config.ncores + if output_data_v1.get("native_files", None) is None: + output_data_v1["native_files"] = { + "input": json.dumps(json.loads(input_model_v1.json()), indent=1), } # Delete keys - output_data.pop("return_output", None) + output_data_v1.pop("return_output", None) - atres = AtomicResult(**output_data) - return atres.convert_v(2) + atres = AtomicResult(**output_data_v1) + return atres.convert_v(2, external_input_data=input_model) diff --git a/qcengine/programs/rdkit.py b/qcengine/programs/rdkit.py index d971aed8..f6a027a0 100644 --- a/qcengine/programs/rdkit.py +++ b/qcengine/programs/rdkit.py @@ -142,6 +142,8 @@ def compute(self, input_data: "AtomicInput", config: "TaskConfig") -> "AtomicRes ret_data["schema_name"] = "qcschema_output" ret_data["success"] = True + ret_data["input_data"] = input_data + ret_data["molecule"] = jmol # Form up a dict first, then sent to BaseModel to avoid repeat kwargs which don't override each other - return AtomicResult(**{**input_data.dict(), **ret_data}) + return AtomicResult(**ret_data) diff --git a/qcengine/programs/tests/test_programs.py b/qcengine/programs/tests/test_programs.py index aedec5f7..7f9061f9 100644 --- a/qcengine/programs/tests/test_programs.py +++ b/qcengine/programs/tests/test_programs.py @@ -41,7 +41,10 @@ def test_psi4_task(schema_versions, request): ret = qcng.compute(input_data, "psi4", raise_error=True, return_version=retver) ret = checkver_and_convert(ret, request.node.name, "post") - assert ret.driver == "energy" + if "v2" in request.node.name: + assert ret.input_data.driver == "energy" + else: + assert ret.driver == "energy" assert "Final Energy" in ret.stdout prov_keys = {"cpu", "hostname", "username", "wall_time"} @@ -215,7 +218,10 @@ def test_torchani_task(schema_versions, request): ret = checkver_and_convert(ret, request.node.name, "post") assert ret.success is True - assert ret.driver == "gradient" + if "v2" in request.node.name: + assert ret.input_data.driver == "gradient" + else: + assert ret.driver == "gradient" @using("mopac") From 82b9998d9de2dd0e770cade5641b9f657b8ec638 Mon Sep 17 00:00:00 2001 From: "Lori A. Burns" Date: Fri, 15 Nov 2024 01:49:58 -0500 Subject: [PATCH 10/13] input_data mrchem gamess --- docs/source/changelog.rst | 1 + qcengine/programs/gamess/runner.py | 7 ++++--- qcengine/programs/mrchem.py | 8 +++----- qcengine/programs/tests/test_mrchem.py | 15 ++++++++++++--- qcengine/tests/test_procedures.py | 5 ++++- 5 files changed, 24 insertions(+), 12 deletions(-) diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index fbfac2dc..0133be09 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -70,6 +70,7 @@ Misc. MUST (Unmerged) +++++++++++++++ +several got properties.return_energy, retunr_gradient If you're missing something from AtomicResult.extras, check AtomicResult.input_data.extras in case it was passed in on input - (:pr:`459`) OpenMM gained AtomicResult.properties.return_gradient - (:pr:`458`) When qcengine.compute() fails and forms a fop = FailedOperation (raise_error=T), with v2, `fop.input_data` will be an <>Input model (when possible; if the error was in forming the model, it'll still be a dict), not always a dict like v1. diff --git a/qcengine/programs/gamess/runner.py b/qcengine/programs/gamess/runner.py index f0dc6a10..455dd186 100644 --- a/qcengine/programs/gamess/runner.py +++ b/qcengine/programs/gamess/runner.py @@ -253,9 +253,10 @@ def parse_output(self, outfiles: Dict[str, str], input_model: AtomicInput) -> At provenance["module"] = module output_data = { - "schema_version": 1, + "schema_version": 2, + "input_data": input_model, "molecule": gamessmol, # overwrites with outfile Cartesians in case fix_*=F - "extras": {**input_model.extras}, + "extras": {}, "native_files": {k: v for k, v in outfiles.items() if v is not None}, "properties": atprop, "provenance": provenance, @@ -271,7 +272,7 @@ def parse_output(self, outfiles: Dict[str, str], input_model: AtomicInput) -> At k.upper(): str(v) if isinstance(v, Decimal) else v for k, v in qcvars.items() } - return AtomicResult(**{**input_model.dict(), **output_data}) + return AtomicResult(**output_data) @staticmethod def _partition(total: float, fraction_replicated: float, ncores: int) -> Tuple[int, int]: diff --git a/qcengine/programs/mrchem.py b/qcengine/programs/mrchem.py index 4251759e..277ab645 100644 --- a/qcengine/programs/mrchem.py +++ b/qcengine/programs/mrchem.py @@ -101,13 +101,11 @@ def compute(self, input_model: "AtomicInput", config: "TaskConfig") -> "AtomicRe input_data = copy.deepcopy(job_input["mrchem_json"]) output_data = { - "keywords": input_data, "schema_name": "qcschema_output", - "schema_version": 1, - "model": input_model.model, + "schema_version": 2, + "input_data": input_model, "molecule": input_model.molecule, - "driver": input_model.driver, - "extras": input_model.extras, + "extras": {}, } with temporary_directory(parent=parent, suffix="_mrchem_scratch") as tmpdir: diff --git a/qcengine/programs/tests/test_mrchem.py b/qcengine/programs/tests/test_mrchem.py index d0c74a2e..1ba55c8c 100644 --- a/qcengine/programs/tests/test_mrchem.py +++ b/qcengine/programs/tests/test_mrchem.py @@ -54,7 +54,10 @@ def test_energy(h2o, schema_versions, request): # Make sure the calculation completed successfully assert compare_values(-76.4546307, res["return_result"], atol=1e-3) - assert res["driver"] == "energy" + if "v2" in request.node.name: + assert res["input_data"]["driver"] == "energy" + else: + assert res["driver"] == "energy" assert "provenance" in res assert res["success"] is True @@ -92,7 +95,10 @@ def test_dipole(h2o, schema_versions, request): # Make sure the calculation completed successfully assert compare_values([-3.766420e-07, 0.0, 0.720473], res["return_result"]["dipole_moment"]["dip-1"], atol=1e-3) - assert res["driver"] == "properties" + if "v2" in request.node.name: + assert res["input_data"]["driver"] == "properties" + else: + assert res["driver"] == "properties" assert "provenance" in res assert res["success"] is True @@ -140,7 +146,10 @@ def test_gradient(fh, schema_versions, request): res["return_result"], atol=1e-3, ) - assert res["driver"] == "gradient" + if "v2" in request.node.name: + assert res["input_data"]["driver"] == "gradient" + else: + assert res["driver"] == "gradient" assert "provenance" in res assert res["success"] is True diff --git a/qcengine/tests/test_procedures.py b/qcengine/tests/test_procedures.py index bb19d304..2c973c48 100644 --- a/qcengine/tests/test_procedures.py +++ b/qcengine/tests/test_procedures.py @@ -65,7 +65,10 @@ def test_geometric_psi4(input_data, optimizer, ncores, schema_versions, request) # Check keywords passing for single in ret.trajectory: - assert "scf_properties" in single.keywords + if "v2" in request.node.name: + assert "scf_properties" in single.input_data.keywords + else: + assert "scf_properties" in single.keywords assert "WIBERG_LOWDIN_INDICES" in single.extras["qcvars"] or "WIBERG LOWDIN INDICES" in single.extras["qcvars"] # TODO: old WIBERG qcvar used underscore; new one uses space. covering bases here but remove someday From 904a2d539716c5e460addcd1f5d139baaff29b37 Mon Sep 17 00:00:00 2001 From: "Lori A. Burns" Date: Sun, 17 Nov 2024 23:19:34 -0500 Subject: [PATCH 11/13] missed one --- qcengine/programs/tests/test_programs.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/qcengine/programs/tests/test_programs.py b/qcengine/programs/tests/test_programs.py index 7f9061f9..ebea804b 100644 --- a/qcengine/programs/tests/test_programs.py +++ b/qcengine/programs/tests/test_programs.py @@ -70,7 +70,10 @@ def test_psi4_hf3c_task(schema_versions, request): ret = checkver_and_convert(ret, request.node.name, "post") assert ret.success is True - assert ret.model.basis is None + if "v2" in request.node.name: + assert ret.input_data.model.basis is None + else: + assert ret.model.basis is None @using("psi4_runqcsk") From 9723cf3734d9526fff2367794fc48698c834fa7d Mon Sep 17 00:00:00 2001 From: "Lori A. Burns" Date: Sun, 17 Nov 2024 23:26:13 -0500 Subject: [PATCH 12/13] fix data_layout turbomole --- qcengine/programs/turbomole/runner.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qcengine/programs/turbomole/runner.py b/qcengine/programs/turbomole/runner.py index ecda4b9c..1a2cb2b6 100644 --- a/qcengine/programs/turbomole/runner.py +++ b/qcengine/programs/turbomole/runner.py @@ -269,7 +269,8 @@ def parse_output( build_out(qcvars) atprop = build_atomicproperties(qcvars) - output_data = input_model.dict() + output_data = {"input_data": input_model} + output_data["molecule"] = input_model.molecule # TODO better mol? output_data["extras"]["outfiles"] = outfiles output_data["properties"] = atprop output_data["provenance"] = Provenance(creator="Turbomole", version=self.get_version(), routine="turbomole") From d22eb37d4d7b67bf1529386e57e41ce63630b552 Mon Sep 17 00:00:00 2001 From: "Lori A. Burns" Date: Mon, 18 Nov 2024 13:33:52 -0500 Subject: [PATCH 13/13] fix basis --- qcengine/programs/tests/test_programs.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/qcengine/programs/tests/test_programs.py b/qcengine/programs/tests/test_programs.py index ebea804b..d58cbc69 100644 --- a/qcengine/programs/tests/test_programs.py +++ b/qcengine/programs/tests/test_programs.py @@ -71,9 +71,10 @@ def test_psi4_hf3c_task(schema_versions, request): assert ret.success is True if "v2" in request.node.name: - assert ret.input_data.model.basis is None + # prior to 0.50, None, now "" + assert not ret.input_data.model.basis else: - assert ret.model.basis is None + assert not ret.model.basis @using("psi4_runqcsk")