diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f9fad7ef..2455e66b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -253,7 +253,7 @@ jobs: matrix: os: [ubuntu-latest] gcc_v: [10] - python_v: ['3.8', '3.9', '3.10', '3.11', '3.12'] + python_v: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13', '3.14'] env: FC: gfortran @@ -270,12 +270,21 @@ jobs: - run: pip3 install coverage - name: Install dependencies + if: ((matrix.python_v != '3.14') && (matrix.python_v != '3.13')) uses: mamba-org/setup-micromamba@v1 with: environment-file: assets/ci/python-env.yaml create-args: | python=${{ matrix.python_v }} + - name: Install dependencies + if: ((matrix.python_v == '3.14') || (matrix.python_v == '3.13')) + uses: mamba-org/setup-micromamba@v1 + with: + environment-file: assets/ci/python-env-alt.yaml + create-args: | + python=${{ matrix.python_v }} + - name: Install GCC (OSX) if: ${{ contains(matrix.os, 'macos') }} run: | diff --git a/.github/workflows/wheel.yml b/.github/workflows/wheel.yml index bd41112d..dccb4e14 100644 --- a/.github/workflows/wheel.yml +++ b/.github/workflows/wheel.yml @@ -80,8 +80,19 @@ jobs: with: python-version: '3.x' + - name: Set cibuildwheel cache path + run: echo "CIBW_CACHE_PATH=${{ github.workspace }}/.cibw-cache" >> $GITHUB_ENV + + - uses: actions/cache@v4 + with: + path: ${{ github.workspace }}/.cibw-cache + key: cibw-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('.github/workflows/*.yml') }} + restore-keys: | + cibw-${{ runner.os }}-${{ runner.arch }}- + cibw-${{ runner.os }}- + - name: Install cibuildwheel - run: python -m pip install cibuildwheel==2.21.1 + run: python -m pip install "cibuildwheel==2.23.4" - name: Download sdist uses: actions/download-artifact@v4 @@ -91,9 +102,10 @@ jobs: - name: Build wheels run: python -m cibuildwheel --output-dir . *.tar.gz env: + CIBW_CACHE_PATH: ${{ github.workspace }}/.cibw-cache CIBW_CONFIG_SETTINGS: setup-args="-Ds-dftd3:openmp=false" CIBW_ARCHS: auto64 - CIBW_SKIP: "{cp38-macosx_arm64,pp37-win_amd64,pp39-win_amd64,pp310-win_amd64}" + CIBW_SKIP: "{cp38-macosx_arm64,cp39-macosx_arm64,pp37-win_amd64,pp39-win_amd64,pp310-win_amd64}" CIBW_BUILD_VERBOSITY: 1 CIBW_ARCHS_MACOS: arm64 CIBW_ENVIRONMENT_MACOS: > diff --git a/assets/ci/python-env-alt.yaml b/assets/ci/python-env-alt.yaml new file mode 100644 index 00000000..44a9e69e --- /dev/null +++ b/assets/ci/python-env-alt.yaml @@ -0,0 +1,20 @@ +name: python +channels: + - conda-forge +dependencies: + - python + - pip + - python-build + - pkgconfig + - setuptools + - pytest + - pytest-cov + - coverage + - cffi + - numpy + - ase + - conda-forge/label/qcelemental_dev::qcelemental>=0.50.0rc3 + - matplotlib-base + - meson >=1.2.3,!=1.8.0 + - meson-python + - ninja diff --git a/python/dftd3/qcschema.py b/python/dftd3/qcschema.py index 8a29b3be..3dfb06be 100644 --- a/python/dftd3/qcschema.py +++ b/python/dftd3/qcschema.py @@ -19,6 +19,12 @@ Integration with the `QCArchive infrastructure `_. +If the QCElemental package is installed the ``dftd3.qcschema`` module becomes +importable and provides the ``run_qcschema`` function supporting QCSchema v1. +If the QCElemental package is >=0.50.0, ``dftd3.qcschema`` supports QCSchema v1 +and v2, returning whichever version was submitted. Note that Python 3.14+ only +works with QCSchema v2 due to Pydantic restrictions. + This module provides a way to translate QCSchema or QCElemental Atomic Input into a format understandable by the ``dftd3`` API which in turn provides the calculation results in a QCSchema compatible format. @@ -173,15 +179,29 @@ def run_qcschema( - input_data: Union[dict, qcel.models.AtomicInput] -) -> qcel.models.AtomicResult: + input_data: Union[dict, qcel.models.AtomicInput, "qcel.models.v2.AtomicInput"] +) -> Union[qcel.models.AtomicResult, "qcel.models.v2.AtomicResult"]: """Perform disperson correction based on an atomic inputmodel""" - if not isinstance(input_data, qcel.models.AtomicInput): - atomic_input = qcel.models.AtomicInput(**input_data) - else: + v2_available = hasattr(qcel.models, "v2") + + if v2_available and isinstance(input_data, qcel.models.v2.AtomicInput): atomic_input = input_data - ret_data = atomic_input.dict() + elif isinstance(input_data, qcel.models.AtomicInput): + atomic_input = input_data + elif v2_available and input_data.get("specification"): + atomic_input = qcel.models.v2.AtomicInput(**input_data) + else: + atomic_input = qcel.models.AtomicInput(**input_data) + + if (schver := atomic_input.schema_version) == 1: + from qcelemental.models import AtomicInput, AtomicResult, ComputeError + + ret_data = atomic_input.dict() + elif schver == 2: + from qcelemental.models.v2 import AtomicInput, AtomicResult, ComputeError, FailedOperation + + ret_data = {"input_data": atomic_input, "extras": {}, "molecule": atomic_input.molecule} provenance = { "creator": "s-dftd3", @@ -194,24 +214,29 @@ def run_qcschema( # Since it is a level hint we a forgiving if it is not present, # we are much less forgiving if the wrong level is hinted here. - _level = atomic_input.keywords.get("level_hint", "d3bj") + atin_keywords = atomic_input.keywords if schver == 1 else atomic_input.specification.keywords + _level = atin_keywords.get("level_hint", "d3bj") if _level.lower() not in _available_levels: - ret_data.update( - provenance=provenance, - success=success, - properties=properties, - return_result=return_result, - error=qcel.models.ComputeError( - error_type="input error", - error_message="Level '{}' is invalid for this dispersion correction".format( - _level - ), + error=ComputeError( + error_type="input error", + error_message="Level '{}' is invalid for this dispersion correction".format( + _level ), ) - return qcel.models.AtomicResult(**ret_data) + if schver == 1: + ret_data.update( + provenance=provenance, + success=success, + properties=properties, + return_result=return_result, + error=error, + ) + return AtomicResult(**ret_data) + elif schver == 2: + return FailedOperation(input_data=atomic_input, error=error) # Check if the method is provided and strip the “dashlevel” from the method - _method = atomic_input.model.method.split("-") + _method = atomic_input.model.method.split("-") if schver == 1 else atomic_input.specification.model.method.split("-") if _method[-1].lower().translate(_clean_dashlevel) == _level.lower(): _method.pop() _method = "-".join(_method) @@ -219,7 +244,7 @@ def run_qcschema( _method = None # Obtain the parameters for the damping function - _input_param = atomic_input.keywords.get("params_tweaks", {"method": _method}) + _input_param = atin_keywords.get("params_tweaks", {"method": _method}) try: param = _damping_param[_level]( @@ -231,13 +256,14 @@ def run_qcschema( atomic_input.molecule.geometry[atomic_input.molecule.real], ) + driver = atomic_input.driver if schver == 1 else atomic_input.specification.driver res = disp.get_dispersion( param=param, - grad=atomic_input.driver == "gradient", + grad=driver == "gradient", ) extras = {"dftd3": res} - if atomic_input.driver == "gradient": + if driver == "gradient": if all(atomic_input.molecule.real): fullgrad = res.get("gradient") else: @@ -247,18 +273,18 @@ def run_qcschema( properties.update(return_energy=res.get("energy")) - if atomic_input.keywords.get("pair_resolved", False): + if atin_keywords.get("pair_resolved", False): res = disp.get_pairwise_dispersion(param=param) extras["dftd3"].update(res) - success = atomic_input.driver in _supported_drivers - if atomic_input.driver == "energy": + success = driver in _supported_drivers + if driver == "energy": return_result = properties["return_energy"] - elif atomic_input.driver == "gradient": + elif driver == "gradient": return_result = fullgrad else: ret_data.update( - error=qcel.models.ComputeError( + error=ComputeError( error_type="input error", error_message="Calculation succeeded but invalid driver request provided", ), @@ -268,7 +294,7 @@ def run_qcschema( except (RuntimeError, TypeError) as e: ret_data.update( - error=qcel.models.ComputeError( + error=ComputeError( error_type="input error", error_message=str(e) ), ), @@ -280,4 +306,7 @@ def run_qcschema( return_result=return_result, ) - return qcel.models.AtomicResult(**ret_data) + if schver == 2 and "error" in ret_data: + return FailedOperation(input_data=atomic_input, error=ret_data["error"]) + + return AtomicResult(**ret_data) diff --git a/python/dftd3/test_qcschema.py b/python/dftd3/test_qcschema.py index 408b6871..bd0c8124 100644 --- a/python/dftd3/test_qcschema.py +++ b/python/dftd3/test_qcschema.py @@ -14,6 +14,7 @@ # You should have received a copy of the Lesser GNU General Public License # along with s-dftd3. If not, see . +import sys import numpy as np import pytest from pytest import approx, mark @@ -24,14 +25,20 @@ except ModuleNotFoundError: qcel = None +v1_available = mark.skipif(sys.version_info >= (3, 14), reason="QCSchema v1 not available for py314+") +v2_available = mark.skipif(not hasattr(qcel.models, "v2"), reason="QCSchema v2 not available in current QCElemental") + @pytest.mark.skipif(qcel is None, reason="requires qcelemental") +@mark.parametrize("schver", [ + pytest.param(1, marks=v1_available), + pytest.param(2, marks=v2_available), +]) @mark.parametrize("atm", [True, False]) -def test_energy_r2scan_d3bj(atm): +def test_energy_r2scan_d3bj(atm, schver): thr = 1e-9 - atomic_input = qcel.models.AtomicInput( - molecule={ + molecule={ "symbols": "C C C C C C I H H H H H S H C H H H".split(" "), "geometry": [ [-1.42754169820131, -1.50508961850828, -1.93430551124333], @@ -53,18 +60,38 @@ def test_energy_r2scan_d3bj(atm): [-5.07177399637298, 10.99164969235585, -2.10739192258756], [-6.35955320518616, 14.08073002965080, -1.68204314084441], ], - }, - driver="energy", - model={ - "method": "", - }, - keywords={ - "params_tweaks": { - "method": "r2scan", - "atm": atm, + } + + if schver == 1: + atomic_input = qcel.models.AtomicInput( + molecule=molecule, + driver="energy", + model={ + "method": "", }, - }, - ) + keywords={ + "params_tweaks": { + "method": "r2scan", + "atm": atm, + }, + } + ) + elif schver == 2: + atomic_input = qcel.models.v2.AtomicInput( + molecule=molecule, + specification=qcel.models.v2.AtomicSpecification( + driver="energy", + model={ + "method": "", + }, + keywords={ + "params_tweaks": { + "method": "r2scan", + "atm": atm, + }, + }, + ) + ) ref = -0.005790963570050724 if atm else -0.005784012374055654 atomic_result = run_qcschema(atomic_input) @@ -74,12 +101,15 @@ def test_energy_r2scan_d3bj(atm): @pytest.mark.skipif(qcel is None, reason="requires qcelemental") +@mark.parametrize("schver", [ + pytest.param(1, marks=v1_available), + pytest.param(2, marks=v2_available), +]) @mark.parametrize("atm", [True, False]) -def test_energy_bp_d3zero(atm): +def test_energy_bp_d3zero(atm, schver): thr = 1e-9 - atomic_input = qcel.models.AtomicInput( - molecule={ + molecule={ "symbols": "C C C C C C I H H H H H S H C H H H".split(" "), "geometry": [ [-1.42754169820131, -1.50508961850828, -1.93430551124333], @@ -101,18 +131,38 @@ def test_energy_bp_d3zero(atm): [-5.07177399637298, 10.99164969235585, -2.10739192258756], [-6.35955320518616, 14.08073002965080, -1.68204314084441], ], - }, - driver="energy", - model={"method": ""}, - keywords={ - "params_tweaks": { - "s8": 1.683, - "rs6": 1.139, - "s9": 1.0 if atm else 0.0, + } + + if schver == 1: + atomic_input = qcel.models.AtomicInput( + molecule=molecule, + driver="energy", + model={"method": ""}, + keywords={ + "params_tweaks": { + "s8": 1.683, + "rs6": 1.139, + "s9": 1.0 if atm else 0.0, + }, + "level_hint": "d3zero", }, - "level_hint": "d3zero", - }, - ) + ) + elif schver == 2: + atomic_input = qcel.models.v2.AtomicInput( + molecule=molecule, + specification={ + "driver": "energy", + "model": {"method": ""}, + "keywords": { + "params_tweaks": { + "s8": 1.683, + "rs6": 1.139, + "s9": 1.0 if atm else 0.0, + }, + "level_hint": "d3zero", + }, + }, + ) ref = -0.01410721853585842 if atm else -0.014100267345314462 atomic_result = run_qcschema(atomic_input) @@ -122,39 +172,57 @@ def test_energy_bp_d3zero(atm): @pytest.mark.skipif(qcel is None, reason="requires qcelemental") -def test_gradient_b97d_d3bj(): +@mark.parametrize("schver", [ + pytest.param(1, marks=v1_available), + pytest.param(2, marks=v2_available), +]) +def test_gradient_b97d_d3bj(schver): thr = 1e-9 - atomic_input = qcel.models.AtomicInput( - molecule={ - "symbols": "C C C C C C I H H H H H S H C H H H".split(" "), - "geometry": [ - [-1.42754169820131, -1.50508961850828, -1.93430551124333], - [+1.19860572924150, -1.66299114873979, -2.03189643761298], - [+2.65876001301880, +0.37736955363609, -1.23426391650599], - [+1.50963368042358, +2.57230374419743, -0.34128058818180], - [-1.12092277855371, +2.71045691257517, -0.25246348639234], - [-2.60071517756218, +0.67879949508239, -1.04550707592673], - [-2.86169588073340, +5.99660765711210, +1.08394899986031], - [+2.09930989272956, -3.36144811062374, -2.72237695164263], - [+2.64405246349916, +4.15317840474646, +0.27856972788526], - [+4.69864865613751, +0.26922271535391, -1.30274048619151], - [-4.63786461351839, +0.79856258572808, -0.96906659938432], - [-2.57447518692275, -3.08132039046931, -2.54875517521577], - [-5.88211879210329, 11.88491819358157, +2.31866455902233], - [-8.18022701418703, 10.95619984550779, +1.83940856333092], - [-5.08172874482867, 12.66714386256482, -0.92419491629867], - [-3.18311711399702, 13.44626574330220, -0.86977613647871], - [-5.07177399637298, 10.99164969235585, -2.10739192258756], - [-6.35955320518616, 14.08073002965080, -1.68204314084441], - ], - }, - driver="gradient", - model={ - "method": "b97d-d3(bj)", - }, - keywords={}, - ) + molecule={ + "symbols": "C C C C C C I H H H H H S H C H H H".split(" "), + "geometry": [ + [-1.42754169820131, -1.50508961850828, -1.93430551124333], + [+1.19860572924150, -1.66299114873979, -2.03189643761298], + [+2.65876001301880, +0.37736955363609, -1.23426391650599], + [+1.50963368042358, +2.57230374419743, -0.34128058818180], + [-1.12092277855371, +2.71045691257517, -0.25246348639234], + [-2.60071517756218, +0.67879949508239, -1.04550707592673], + [-2.86169588073340, +5.99660765711210, +1.08394899986031], + [+2.09930989272956, -3.36144811062374, -2.72237695164263], + [+2.64405246349916, +4.15317840474646, +0.27856972788526], + [+4.69864865613751, +0.26922271535391, -1.30274048619151], + [-4.63786461351839, +0.79856258572808, -0.96906659938432], + [-2.57447518692275, -3.08132039046931, -2.54875517521577], + [-5.88211879210329, 11.88491819358157, +2.31866455902233], + [-8.18022701418703, 10.95619984550779, +1.83940856333092], + [-5.08172874482867, 12.66714386256482, -0.92419491629867], + [-3.18311711399702, 13.44626574330220, -0.86977613647871], + [-5.07177399637298, 10.99164969235585, -2.10739192258756], + [-6.35955320518616, 14.08073002965080, -1.68204314084441], + ], + } + if schver == 1: + atomic_input = qcel.models.AtomicInput( + molecule=molecule, + driver="gradient", + model={ + "method": "b97d-d3(bj)", + }, + keywords={}, + ) + elif schver == 2: + atomic_input = qcel.models.v2.AtomicInput( + molecule=molecule, + specification={ + "driver": "gradient", + "model": { + "method": "b97d-d3(bj)", + }, + "keywords": {}, + } + ) + gradient = np.array( [ [-2.2443259092095252e-4, -5.9115746657000033e-4, -2.3329260776706518e-4], @@ -186,36 +254,56 @@ def test_gradient_b97d_d3bj(): @pytest.mark.skipif(qcel is None, reason="requires qcelemental") -def test_gradient_tpss_d3zero(): +@mark.parametrize("schver", [ + pytest.param(1, marks=v1_available), + pytest.param(2, marks=v2_available), +]) +def test_gradient_tpss_d3zero(schver): thr = 1.0e-9 - atomic_input = qcel.models.AtomicInput( - molecule={ - "symbols": "O C C F O F H".split(), - "geometry": [ - [+4.877023733, -3.909030492, +1.796260143], - [+6.112318716, -2.778558610, +0.091330457], - [+7.360520527, -4.445334728, -1.932830640], - [+7.978801077, -6.767751279, -1.031771494], - [+6.374499300, -0.460299457, -0.213142194], - [+5.637581753, -4.819746139, -3.831249370], - [+9.040657008, -3.585225944, -2.750722946], - ], - "molecular_charge": -1, + molecule={ + "symbols": "O C C F O F H".split(), + "geometry": [ + [+4.877023733, -3.909030492, +1.796260143], + [+6.112318716, -2.778558610, +0.091330457], + [+7.360520527, -4.445334728, -1.932830640], + [+7.978801077, -6.767751279, -1.031771494], + [+6.374499300, -0.460299457, -0.213142194], + [+5.637581753, -4.819746139, -3.831249370], + [+9.040657008, -3.585225944, -2.750722946], + ], + "molecular_charge": -1, + } + keywords={ + "params_tweaks": { + "sr6": 1.166, + "s8": 1.105, + "alpha6": 14.0, }, - driver="gradient", - model={ - "method": "", - }, - keywords={ - "params_tweaks": { - "sr6": 1.166, - "s8": 1.105, - "alpha6": 14.0, + "level_hint": "d3zero", + } + + if schver == 1: + atomic_input = qcel.models.AtomicInput( + molecule=molecule, + driver="gradient", + model={ + "method": "", }, - "level_hint": "d3zero", - }, - ) + keywords=keywords, + ) + elif schver == 2: + atomic_input = qcel.models.v2.AtomicInput( + molecule=molecule, + specification=qcel.models.v2.AtomicSpecification( + driver="gradient", + model={ + "method": "", + }, + keywords=keywords, + ) + ) + gradient = np.array( [ [+8.5996134689276694e-5, +1.3305341130186383e-4, -4.9354141710140030e-5], @@ -239,29 +327,45 @@ def test_gradient_tpss_d3zero(): @pytest.mark.skipif(qcel is None, reason="requires qcelemental") -def test_error_noargs(): - atomic_input = qcel.models.AtomicInput( - molecule={ - "symbols": "C C C C N C S H H H H H".split(), - "geometry": [ - [-2.56745685564671, -0.02509985979910, 0.00000000000000], - [-1.39177582455797, +2.27696188880014, 0.00000000000000], - [+1.27784995624894, +2.45107479759386, 0.00000000000000], - [+2.62801937615793, +0.25927727028120, 0.00000000000000], - [+1.41097033661123, -1.99890996077412, 0.00000000000000], - [-1.17186102298849, -2.34220576284180, 0.00000000000000], - [-2.39505990368378, -5.22635838332362, 0.00000000000000], - [+2.41961980455457, -3.62158019253045, 0.00000000000000], - [-2.51744374846065, +3.98181713686746, 0.00000000000000], - [+2.24269048384775, +4.24389473203647, 0.00000000000000], - [+4.66488984573956, +0.17907568006409, 0.00000000000000], - [-4.60044244782237, -0.17794734637413, 0.00000000000000], - ], - }, - driver="energy", - model={"method": ""}, - keywords={}, - ) +@mark.parametrize("schver", [ + pytest.param(1, marks=v1_available), + pytest.param(2, marks=v2_available), +]) +def test_error_noargs(schver): + molecule={ + "symbols": "C C C C N C S H H H H H".split(), + "geometry": [ + [-2.56745685564671, -0.02509985979910, 0.00000000000000], + [-1.39177582455797, +2.27696188880014, 0.00000000000000], + [+1.27784995624894, +2.45107479759386, 0.00000000000000], + [+2.62801937615793, +0.25927727028120, 0.00000000000000], + [+1.41097033661123, -1.99890996077412, 0.00000000000000], + [-1.17186102298849, -2.34220576284180, 0.00000000000000], + [-2.39505990368378, -5.22635838332362, 0.00000000000000], + [+2.41961980455457, -3.62158019253045, 0.00000000000000], + [-2.51744374846065, +3.98181713686746, 0.00000000000000], + [+2.24269048384775, +4.24389473203647, 0.00000000000000], + [+4.66488984573956, +0.17907568006409, 0.00000000000000], + [-4.60044244782237, -0.17794734637413, 0.00000000000000], + ], + } + + if schver == 1: + atomic_input = qcel.models.AtomicInput( + molecule=molecule, + driver="energy", + model={"method": ""}, + keywords={}, + ) + elif schver == 2: + atomic_input = qcel.models.v2.AtomicInput( + molecule=molecule, + specification=qcel.models.v2.AtomicSpecification( + driver="energy", + model={"method": ""}, + keywords={}, + ) + ) atomic_result = run_qcschema(atomic_input) @@ -270,34 +374,61 @@ def test_error_noargs(): @pytest.mark.skipif(qcel is None, reason="requires qcelemental") -def test_error_nomethod(): - atomic_input = qcel.models.AtomicInput( - molecule={ - "symbols": "C C C C N C S H H H H H".split(), - "geometry": [ - [-2.56745685564671, -0.02509985979910, 0.00000000000000], - [-1.39177582455797, +2.27696188880014, 0.00000000000000], - [+1.27784995624894, +2.45107479759386, 0.00000000000000], - [+2.62801937615793, +0.25927727028120, 0.00000000000000], - [+1.41097033661123, -1.99890996077412, 0.00000000000000], - [-1.17186102298849, -2.34220576284180, 0.00000000000000], - [-2.39505990368378, -5.22635838332362, 0.00000000000000], - [+2.41961980455457, -3.62158019253045, 0.00000000000000], - [-2.51744374846065, +3.98181713686746, 0.00000000000000], - [+2.24269048384775, +4.24389473203647, 0.00000000000000], - [+4.66488984573956, +0.17907568006409, 0.00000000000000], - [-4.60044244782237, -0.17794734637413, 0.00000000000000], - ], - }, - driver="energy", - model={ - "method": "this-method-does-not-exist", - }, - keywords={ - "level_hint": "d3bj", - }, - ) - error = qcel.models.ComputeError( +@mark.parametrize("schver", [ + pytest.param(1, marks=v1_available), + pytest.param(2, marks=v2_available), +]) +def test_error_nomethod(schver): + molecule={ + "symbols": "C C C C N C S H H H H H".split(), + "geometry": [ + [-2.56745685564671, -0.02509985979910, 0.00000000000000], + [-1.39177582455797, +2.27696188880014, 0.00000000000000], + [+1.27784995624894, +2.45107479759386, 0.00000000000000], + [+2.62801937615793, +0.25927727028120, 0.00000000000000], + [+1.41097033661123, -1.99890996077412, 0.00000000000000], + [-1.17186102298849, -2.34220576284180, 0.00000000000000], + [-2.39505990368378, -5.22635838332362, 0.00000000000000], + [+2.41961980455457, -3.62158019253045, 0.00000000000000], + [-2.51744374846065, +3.98181713686746, 0.00000000000000], + [+2.24269048384775, +4.24389473203647, 0.00000000000000], + [+4.66488984573956, +0.17907568006409, 0.00000000000000], + [-4.60044244782237, -0.17794734637413, 0.00000000000000], + ], + } + + if schver == 1: + from qcelemental.models import ComputeError + + atomic_input = qcel.models.AtomicInput( + molecule=molecule, + driver="energy", + model={ + "method": "this-method-does-not-exist", + }, + keywords={ + "level_hint": "d3bj", + }, + ) + elif schver == 2: + from qcelemental.models.v2 import ComputeError + + atomic_input = qcel.models.v2.AtomicInput( + molecule=molecule, + specification=qcel.models.v2.AtomicSpecification( + driver="energy", + model={ + "method": "this-method-does-not-exist", + }, + keywords={ + "level_hint": "d3bj", + }, + ) + ) + else: + raise RuntimeError(f"QCSchema v{schver} NYI") + + error = ComputeError( error_type="input error", error_message="No entry for 'this-method-does-not-exist' present", ) @@ -309,34 +440,61 @@ def test_error_nomethod(): @pytest.mark.skipif(qcel is None, reason="requires qcelemental") -def test_error_level(): - atomic_input = qcel.models.AtomicInput( - molecule={ - "symbols": "C C C C N C S H H H H H".split(), - "geometry": [ - [-2.56745685564671, -0.02509985979910, 0.00000000000000], - [-1.39177582455797, +2.27696188880014, 0.00000000000000], - [+1.27784995624894, +2.45107479759386, 0.00000000000000], - [+2.62801937615793, +0.25927727028120, 0.00000000000000], - [+1.41097033661123, -1.99890996077412, 0.00000000000000], - [-1.17186102298849, -2.34220576284180, 0.00000000000000], - [-2.39505990368378, -5.22635838332362, 0.00000000000000], - [+2.41961980455457, -3.62158019253045, 0.00000000000000], - [-2.51744374846065, +3.98181713686746, 0.00000000000000], - [+2.24269048384775, +4.24389473203647, 0.00000000000000], - [+4.66488984573956, +0.17907568006409, 0.00000000000000], - [-4.60044244782237, -0.17794734637413, 0.00000000000000], - ], - }, - driver="energy", - model={ - "method": "SCAN", - }, - keywords={ - "level_hint": "D42", - }, - ) - error = qcel.models.ComputeError( +@mark.parametrize("schver", [ + pytest.param(1, marks=v1_available), + pytest.param(2, marks=v2_available), +]) +def test_error_level(schver): + molecule={ + "symbols": "C C C C N C S H H H H H".split(), + "geometry": [ + [-2.56745685564671, -0.02509985979910, 0.00000000000000], + [-1.39177582455797, +2.27696188880014, 0.00000000000000], + [+1.27784995624894, +2.45107479759386, 0.00000000000000], + [+2.62801937615793, +0.25927727028120, 0.00000000000000], + [+1.41097033661123, -1.99890996077412, 0.00000000000000], + [-1.17186102298849, -2.34220576284180, 0.00000000000000], + [-2.39505990368378, -5.22635838332362, 0.00000000000000], + [+2.41961980455457, -3.62158019253045, 0.00000000000000], + [-2.51744374846065, +3.98181713686746, 0.00000000000000], + [+2.24269048384775, +4.24389473203647, 0.00000000000000], + [+4.66488984573956, +0.17907568006409, 0.00000000000000], + [-4.60044244782237, -0.17794734637413, 0.00000000000000], + ], + } + + if schver == 1: + from qcelemental.models import AtomicInput, ComputeError + + atomic_input = AtomicInput( + molecule=molecule, + driver="energy", + model={ + "method": "SCAN", + }, + keywords={ + "level_hint": "D42", + }, + ) + elif schver == 2: + from qcelemental.models.v2 import AtomicInput, AtomicSpecification, ComputeError + + atomic_input = AtomicInput( + molecule=molecule, + specification=AtomicSpecification( + driver="energy", + model={ + "method": "SCAN", + }, + keywords={ + "level_hint": "D42", + }, + ) + ) + else: + raise RuntimeError(f"QCSchema v{schver} NYI") + + error = ComputeError( error_type="input error", error_message="Level 'D42' is invalid for this dispersion correction", ) @@ -348,30 +506,48 @@ def test_error_level(): @pytest.mark.skipif(qcel is None, reason="requires qcelemental") -def test_ghost_pbe_d3bj(): +@mark.parametrize("schver", [ + pytest.param(1, marks=v1_available), + pytest.param(2, marks=v2_available), +]) +def test_ghost_pbe_d3bj(schver): thr = 1e-9 - atomic_input = qcel.models.AtomicInput( - molecule={ - "symbols": "Pb H H H H Bi H H H".split(), - "geometry": [ - [-0.00000020988889, -4.98043478877778, +0.00000000000000], - [+3.06964045311111, -6.06324400177778, +0.00000000000000], - [-1.53482054188889, -6.06324400177778, -2.65838526500000], - [-1.53482054188889, -6.06324400177778, +2.65838526500000], - [-0.00000020988889, -1.72196703577778, +0.00000000000000], - [-0.00000020988889, +4.77334244722222, +0.00000000000000], - [+1.35700257511111, +6.70626379422222, -2.35039772300000], - [-2.71400388988889, +6.70626379422222, +0.00000000000000], - [+1.35700257511111, +6.70626379422222, +2.35039772300000], - ], - "real": [True] * 5 + [False] * 4, - }, - driver="gradient", - model={ - "method": "pbe", - }, - ) + molecule={ + "symbols": "Pb H H H H Bi H H H".split(), + "geometry": [ + [-0.00000020988889, -4.98043478877778, +0.00000000000000], + [+3.06964045311111, -6.06324400177778, +0.00000000000000], + [-1.53482054188889, -6.06324400177778, -2.65838526500000], + [-1.53482054188889, -6.06324400177778, +2.65838526500000], + [-0.00000020988889, -1.72196703577778, +0.00000000000000], + [-0.00000020988889, +4.77334244722222, +0.00000000000000], + [+1.35700257511111, +6.70626379422222, -2.35039772300000], + [-2.71400388988889, +6.70626379422222, +0.00000000000000], + [+1.35700257511111, +6.70626379422222, +2.35039772300000], + ], + "real": [True] * 5 + [False] * 4, + } + + if schver == 1: + atomic_input = qcel.models.AtomicInput( + molecule=molecule, + driver="gradient", + model={ + "method": "pbe", + }, + ) + elif schver == 2: + atomic_input = qcel.models.v2.AtomicInput( + molecule=molecule, + specification=qcel.models.v2.AtomicSpecification( + driver="gradient", + model={ + "method": "pbe", + }, + ) + ) + gradient = np.array( [ [+0.00000000e-0, +1.15093229e-7, +0.00000000e-0],