From 37bb36109825a463d62d6f5c25723ffe7ccc198e Mon Sep 17 00:00:00 2001 From: "Lori A. Burns" Date: Mon, 23 Sep 2024 16:16:28 -0400 Subject: [PATCH] start 5 modes testing --- .github/workflows/CI.yml | 2 +- qcengine/programs/tests/test_ghost.py | 34 +++++++--- qcengine/programs/tests/test_nwchem.py | 49 +++++++++++--- .../programs/tests/test_standard_suite_hf.py | 33 ++++++--- qcengine/stock_mols.py | 7 +- qcengine/testing.py | 67 +++++++++++++++++++ qcengine/tests/test_procedures.py | 66 ++++++++++++++++-- 7 files changed, 221 insertions(+), 37 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index b83fbf959..9538cc794 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -148,7 +148,7 @@ jobs: #if: false run: | conda remove qcelemental --force - python -m pip install 'git+https://github.com/loriab/QCElemental.git@csse_pyd2_shimclasses' --no-deps + python -m pip install 'git+https://github.com/loriab/QCElemental.git@loriab:csse_pyd2_converterclasses' --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/qcengine/programs/tests/test_ghost.py b/qcengine/programs/tests/test_ghost.py index 06cee7d74..d9a43d3c2 100644 --- a/qcengine/programs/tests/test_ghost.py +++ b/qcengine/programs/tests/test_ghost.py @@ -8,19 +8,18 @@ import qcengine as qcng from qcengine.programs.tests.test_dftd3_mp2d import eneyne_ne_qcschemamols -from qcengine.testing import using +from qcengine.testing import checkver_and_convert, schema_versions, using @pytest.fixture -def hene(): - smol = """ +def hene_data(): + return """ 0 1 He 0 0 0 @Ne 2.5 0 0 nocom noreorient """ - return qcel.models.Molecule.from_data(smol) @pytest.mark.parametrize("driver", ["energy", "gradient"]) @@ -39,7 +38,10 @@ def hene(): pytest.param("psi4", "aug-cc-pvdz", {"scf_type": "direct"}, marks=using("psi4")), ], ) -def test_simple_ghost(driver, program, basis, keywords, hene): +def test_simple_ghost(driver, program, basis, keywords, hene_data, schema_versions, request): + models, models_out = schema_versions + hene = models.Molecule.from_data(hene_data) + resi = {"molecule": hene, "driver": driver, "model": {"method": "hf", "basis": basis}, "keywords": keywords} if program == "gamess": @@ -47,7 +49,9 @@ def test_simple_ghost(driver, program, basis, keywords, hene): qcng.compute(resi, program, raise_error=True, return_dict=True) pytest.xfail("no ghosts with gamess") + resi = checkver_and_convert(resi, request.node.name, "pre") res = qcng.compute(resi, program, raise_error=True, return_dict=True) + res = checkver_and_convert(res, request.node.name, "post") assert res["driver"] == driver assert "provenance" in res @@ -166,7 +170,9 @@ def test_simple_ghost(driver, program, basis, keywords, hene): ), ], ) -def test_tricky_ghost(driver, qcprog, subject, basis, keywords): +def test_tricky_ghost(driver, qcprog, subject, basis, keywords, schema_versions, request): + models, models_out = schema_versions + dmol = eneyne_ne_qcschemamols()["eneyne"][subject] # Freeze the input orientation so that output arrays are aligned to input # and all programs match gradient. @@ -178,13 +184,13 @@ def test_tricky_ghost(driver, qcprog, subject, basis, keywords): # to the in_mol vs. calc_mol aligner. Gradients don't change much # w/symm geometry but enough that the symmetrizer must be defeated. keywords["geometry__autosym"] = "1d-4" - kmol = qcel.models.Molecule(**dmol) + kmol = models.Molecule(**dmol) ref = bimol_ref["eneyne"] assert len(kmol.symbols) == ref["natom"][subject] assert sum([int(at) for at in kmol.real]) == ref["nreal"][subject] - atin = qcel.models.AtomicInput( + atin = models.AtomicInput( **{"molecule": kmol, "model": {"method": "mp2", "basis": basis}, "driver": driver, "keywords": keywords} ) @@ -193,7 +199,9 @@ def test_tricky_ghost(driver, qcprog, subject, basis, keywords): res = qcng.compute(atin, qcprog, raise_error=True) pytest.xfail("no ghosts with gamess") + atin = checkver_and_convert(atin, request.node.name, "pre") atres = qcng.compute(atin, qcprog) + atres = checkver_and_convert(atres, request.node.name, "post") pprint.pprint(atres.dict(), width=200) assert compare_values( @@ -261,8 +269,10 @@ def test_tricky_ghost(driver, qcprog, subject, basis, keywords): ), ], ) -def test_atom_labels(qcprog, basis, keywords): - kmol = qcel.models.Molecule.from_data( +def test_atom_labels(qcprog, basis, keywords, schema_versions, request): + models, models_out = schema_versions + + kmol = models.Molecule.from_data( """ H 0 0 0 H5 5 0 0 @@ -275,11 +285,13 @@ def test_atom_labels(qcprog, basis, keywords): assert compare(["H", "H", "H", "H"], kmol.symbols, "elem") assert compare(["", "5", "_other", "_4sq"], kmol.atom_labels, "elbl") - atin = qcel.models.AtomicInput( + atin = models.AtomicInput( **{"molecule": kmol, "model": {"method": "mp2", "basis": basis}, "driver": "energy", "keywords": keywords} ) + atin = checkver_and_convert(atin, request.node.name, "pre") atres = qcng.compute(atin, qcprog) + atres = checkver_and_convert(atres, request.node.name, "post") pprint.pprint(atres.dict(), width=200) nre = 1.0828427 diff --git a/qcengine/programs/tests/test_nwchem.py b/qcengine/programs/tests/test_nwchem.py index fceb21d68..bc99a3c19 100644 --- a/qcengine/programs/tests/test_nwchem.py +++ b/qcengine/programs/tests/test_nwchem.py @@ -5,7 +5,7 @@ from qcelemental.testing import compare_values import qcengine as qcng -from qcengine.testing import using +from qcengine.testing import checkver_and_convert, schema_versions, using # Molecule where autoz fails _auto_z_problem = xyz = """C 15.204188380000 -3.519180270000 -10.798726560000 @@ -48,8 +48,8 @@ @pytest.fixture -def nh2(): - smol = """ +def nh2_data(): + return """ # R=1.008 #A=105.0 0 2 N 0.000000000000000 0.000000000000000 -0.145912918634892 @@ -58,14 +58,19 @@ def nh2(): units au symmetry c1 """ - return qcel.models.Molecule.from_data(smol) @using("nwchem") -def test_b3lyp(nh2): +def test_b3lyp(nh2_data, schema_versions, request): + models, models_out = schema_versions + nh2 = models.Molecule.from_data(nh2_data) + # Run NH2 resi = {"molecule": nh2, "driver": "energy", "model": {"method": "b3lyp", "basis": "3-21g"}} + + resi = checkver_and_convert(resi, request.node.name, "pre") res = qcng.compute(resi, "nwchem", raise_error=True, return_dict=True) + res = checkver_and_convert(res, request.node.name, "post") # Make sure the calculation completed successfully assert compare_values(-55.554037, res["return_result"], atol=1e-3) @@ -86,9 +91,16 @@ def test_b3lyp(nh2): @using("nwchem") -def test_hess(nh2): +def test_hess(nh2_data, schema_versions, request): + models, models_out = schema_versions + nh2 = models.Molecule.from_data(nh2_data) + resi = {"molecule": nh2, "driver": "hessian", "model": {"method": "b3lyp", "basis": "3-21g"}} + + resi = checkver_and_convert(resi, request.node.name, "pre") res = qcng.compute(resi, "nwchem", raise_error=True, return_dict=False) + res = checkver_and_convert(res, request.node.name, "post") + assert compare_values(-3.5980754370e-02, res.return_result[0, 0], atol=1e-3) assert compare_values(0, res.return_result[1, 0], atol=1e-3) assert compare_values(0.018208307756, res.return_result[3, 0], atol=1e-3) @@ -98,27 +110,40 @@ def test_hess(nh2): shifted_nh2, _ = nh2.scramble(do_shift=False, do_mirror=False, do_rotate=True, do_resort=False) resi["molecule"] = shifted_nh2 + + resi = checkver_and_convert(resi, request.node.name, "pre") res_shifted = qcng.compute(resi, "nwchem", raise_error=True, return_dict=False) + res_shifted = checkver_and_convert(res_shifted, request.node.name, "post") + assert not np.allclose(res.return_result, res_shifted.return_result, atol=1e-8) assert np.isclose(np.linalg.det(res.return_result), np.linalg.det(res_shifted.return_result)) @using("nwchem") -def test_gradient(nh2): +def test_gradient(nh2_data, schema_versions, request): + models, models_out = schema_versions + nh2 = models.Molecule.from_data(nh2_data) + resi = { "molecule": nh2, "driver": "gradient", "model": {"method": "b3lyp", "basis": "3-21g"}, "keywords": {"dft__convergence__gradient": "1e-6"}, } + resi = checkver_and_convert(resi, request.node.name, "pre") res = qcng.compute(resi, "nwchem", raise_error=True, return_dict=True) + res = checkver_and_convert(res, request.node.name, "post") + assert compare_values(4.22418267e-2, res["return_result"][2], atol=1e-7) # Beyond accuracy of NWChem stdout # Rotate the molecule and verify that the gradient changes shifted_nh2, _ = nh2.scramble(do_shift=False, do_mirror=False, do_rotate=True, do_resort=False) resi["molecule"] = shifted_nh2 + + resi = checkver_and_convert(resi, request.node.name, "pre") res_shifted = qcng.compute(resi, "nwchem", raise_error=True, return_dict=True) + res = checkver_and_convert(res, request.node.name, "post") assert not compare_values(4.22418267e-2, res_shifted["return_result"][2], atol=1e-7) @@ -150,7 +175,7 @@ def h20(): @using("nwchem") -def test_dipole(h20): +def test_dipole(h20, schema_versions, request): # Run NH2 resi = { "molecule": h20, @@ -158,7 +183,10 @@ def test_dipole(h20): "model": {"method": "dft", "basis": "3-21g"}, "keywords": {"dft__xc": "b3lyp", "property__dipole": True}, } + + resi = checkver_and_convert(resi, request.node.name, "pre") res = qcng.compute(resi, "nwchem", raise_error=True, return_dict=True) + res = checkver_and_convert(res, request.node.name, "post") # Make sure the calculation completed successfully assert compare_values(-75.764944, res["return_result"], atol=1e-3) @@ -194,7 +222,7 @@ def h20v2(): @using("nwchem") -def test_homo_lumo(h20v2): +def test_homo_lumo(h20v2, schema_versions, request): # Run NH2 resi = { "molecule": h20v2, @@ -202,7 +230,10 @@ def test_homo_lumo(h20v2): "model": {"method": "dft", "basis": "3-21g"}, "keywords": {"dft__xc": "b3lyp"}, } + + resi = checkver_and_convert(resi, request.node.name, "pre") res = qcng.compute(resi, "nwchem", raise_error=True, return_dict=True) + res = checkver_and_convert(res, request.node.name, "post") # Make sure the calculation completed successfully assert compare_values(-75.968095, res["return_result"], atol=1e-3) diff --git a/qcengine/programs/tests/test_standard_suite_hf.py b/qcengine/programs/tests/test_standard_suite_hf.py index 8a6356acd..6dfacfee8 100644 --- a/qcengine/programs/tests/test_standard_suite_hf.py +++ b/qcengine/programs/tests/test_standard_suite_hf.py @@ -3,24 +3,23 @@ from qcelemental.testing import compare_values import qcengine as qcng -from qcengine.testing import using +from qcengine.testing import checkver_and_convert, schema_versions, using @pytest.fixture -def h2o(): - smol = """ +def h2o_data(): + return """ # R=0.958 A=104.5 H 0.000000000000 1.431430901356 0.984293362719 O 0.000000000000 0.000000000000 -0.124038860300 H 0.000000000000 -1.431430901356 0.984293362719 units au """ - return qcel.models.Molecule.from_data(smol) @pytest.fixture -def nh2(): - smol = """ +def nh2_data(): + return """ # R=1.008 #A=105.0 0 2 N 0.000000000000000 0.000000000000000 -0.145912918634892 @@ -29,7 +28,6 @@ def nh2(): units au symmetry c1 """ - return qcel.models.Molecule.from_data(smol) @pytest.mark.parametrize( @@ -53,14 +51,19 @@ def nh2(): pytest.param("terachem_pbs", "aug-cc-pvdz", {}, marks=using("terachem_pbs")), ], ) -def test_sp_hf_rhf(program, basis, keywords, h2o): +def test_sp_hf_rhf(program, basis, keywords, h2o_data, schema_versions, request): """cfour/sp-rhf-hf/input.dat #! single point HF/adz on water """ + models, models_out = schema_versions + h2o = models.Molecule.from_data(h2o_data) + resi = {"molecule": h2o, "driver": "energy", "model": {"method": "hf", "basis": basis}, "keywords": keywords} + resi = checkver_and_convert(resi, request.node.name, "pre") res = qcng.compute(resi, program, raise_error=True, return_dict=True) + res = checkver_and_convert(res, request.node.name, "post") assert res["driver"] == "energy" assert "provenance" in res @@ -103,10 +106,15 @@ def test_sp_hf_rhf(program, basis, keywords, h2o): pytest.param("turbomole", "aug-cc-pVDZ", {}, marks=using("turbomole")), ], ) -def test_sp_hf_uhf(program, basis, keywords, nh2): +def test_sp_hf_uhf(program, basis, keywords, nh2_data, schema_versions, request): + models, models_out = schema_versions + + nh2 = models.Molecule.from_data(nh2_data) resi = {"molecule": nh2, "driver": "energy", "model": {"method": "hf", "basis": basis}, "keywords": keywords} + resi = checkver_and_convert(resi, request.node.name, "pre") res = qcng.compute(resi, program, raise_error=True, return_dict=True) + res = checkver_and_convert(res, request.node.name, "post") assert res["driver"] == "energy" assert "provenance" in res @@ -142,10 +150,15 @@ def test_sp_hf_uhf(program, basis, keywords, nh2): pytest.param("qchem", "aug-cc-pvdz", {"UNRESTRICTED": False}, marks=using("qchem")), ], ) -def test_sp_hf_rohf(program, basis, keywords, nh2): +def test_sp_hf_rohf(program, basis, keywords, nh2_data, schema_versions, request): + models, models_out = schema_versions + + nh2 = models.Molecule.from_data(nh2_data) resi = {"molecule": nh2, "driver": "energy", "model": {"method": "hf", "basis": basis}, "keywords": keywords} + resi = checkver_and_convert(resi, request.node.name, "pre") res = qcng.compute(resi, program, raise_error=True, return_dict=True) + res = checkver_and_convert(res, request.node.name, "post") assert res["driver"] == "energy" assert "provenance" in res diff --git a/qcengine/stock_mols.py b/qcengine/stock_mols.py index 47522e5af..90592b041 100644 --- a/qcengine/stock_mols.py +++ b/qcengine/stock_mols.py @@ -189,11 +189,14 @@ } -def get_molecule(name): +def get_molecule(name, *, return_dict: bool = False): """ Returns a QC JSON representation of a test molecule. """ if name not in _test_mols: raise KeyError("Molecule name '{}' not found".format(name)) - return Molecule(**copy.deepcopy(_test_mols[name])) + if return_dict: + return copy.deepcopy(_test_mols[name]) + else: + return Molecule(**copy.deepcopy(_test_mols[name])) diff --git a/qcengine/testing.py b/qcengine/testing.py index 4cb31d5e0..b439a1167 100644 --- a/qcengine/testing.py +++ b/qcengine/testing.py @@ -211,3 +211,70 @@ def using(program): _using_cache[program] = skip return _using_cache[program] + + +@pytest.fixture(scope="function", params=[None, "as_v1", "as_v2", "to_v1", "to_v2"]) +def schema_versions(request): + if request.param == "as_v1": + return qcel.models.v1, qcel.models.v1 + elif request.param == "to_v2": + return qcel.models.v1, qcel.models.v2 + elif request.param == "as_v2": + return qcel.models.v2, qcel.models.v2 + elif request.param == "to_v1": + return qcel.models.v2, qcel.models.v1 + else: + return qcel.models, qcel.models + + +def checkver_and_convert(mdl, tnm, prepost): + import json + + import pydantic + + def check_model_v1(m): + assert isinstance(m, pydantic.v1.BaseModel), f"type({m.__class__.__name__}) = {type(m)} !⊆ v1.BaseModel" + assert isinstance( + m, qcel.models.v1.basemodels.ProtoModel + ), f"type({m.__class__.__name__}) = {type(m)} !⊆ v1.ProtoModel" + assert m.schema_version == 1, f"{m.__class__.__name__}.schema_version = {m.schema_version} != 1" + + def check_model_v2(m): + assert isinstance(m, pydantic.BaseModel), f"type({m.__class__.__name__}) = {type(m)} !⊆ BaseModel" + assert isinstance( + m, qcel.models.v2.basemodels.ProtoModel + ), f"type({m.__class__.__name__}) = {type(m)} !⊆ v2.ProtoModel" + assert m.schema_version == 2, f"{m.__class__.__name__}.schema_version = {m.schema_version} != 2" + + if prepost == "pre": + dict_in = isinstance(mdl, dict) + if "as_v1" in tnm or "to_v2" in tnm or "None" in tnm: + if dict_in: + mdl = qcel.models.v1.AtomicInput(**mdl) + check_model_v1(mdl) + elif "as_v2" in tnm or "to_v1" in tnm: + if dict_in: + mdl = qcel.models.v2.AtomicInput(**mdl) + check_model_v2(mdl) + mdl = mdl.convert_v(1) + + if dict_in: + mdl = mdl.model_dump() + + elif prepost == "post": + dict_in = isinstance(mdl, dict) + if "as_v1" in tnm or "to_v1" in tnm or "None" in tnm: + if dict_in: + mdl = qcel.models.v1.AtomicResult(**mdl) + check_model_v1(mdl) + elif "as_v2" in tnm or "to_v2" in tnm: + if dict_in: + mdl = qcel.models.v2.AtomicResult(**mdl) + mdl = mdl.convert_v(2) + check_model_v2(mdl) + + if dict_in: + # imitates compute(..., return_dict=True) + mdl = json.loads(mdl.json()) + + return mdl diff --git a/qcengine/tests/test_procedures.py b/qcengine/tests/test_procedures.py index 256df352c..20b97796a 100644 --- a/qcengine/tests/test_procedures.py +++ b/qcengine/tests/test_procedures.py @@ -8,7 +8,7 @@ from qcelemental.models.procedures import OptimizationSpecification, QCInputSpecification, TDKeywords, TorsionDriveInput import qcengine as qcng -from qcengine.testing import failure_engine, using +from qcengine.testing import failure_engine, schema_versions, using @pytest.fixture(scope="function") @@ -30,20 +30,24 @@ def input_data(): pytest.param("berny", marks=using("berny")), ], ) -def test_geometric_psi4(input_data, optimizer, ncores): +def test_geometric_psi4(input_data, optimizer, ncores, schema_versions, request): + models, models_out = schema_versions - input_data["initial_molecule"] = qcng.get_molecule("hydrogen") + input_data["initial_molecule"] = models.Molecule(**qcng.get_molecule("hydrogen", return_dict=True)) input_data["input_specification"]["model"] = {"method": "HF", "basis": "sto-3g"} input_data["input_specification"]["keywords"] = {"scf_properties": ["wiberg_lowdin_indices"]} input_data["keywords"]["program"] = "psi4" - input_data = OptimizationInput(**input_data) + input_data = models.OptimizationInput(**input_data) task_config = { "ncores": ncores, } + input_data = checkver_and_convert_proc(input_data, request.node.name, "pre") ret = qcng.compute_procedure(input_data, optimizer, raise_error=True, task_config=task_config) + ret = checkver_and_convert_proc(ret, request.node.name, "post") + assert 10 > len(ret.trajectory) > 1 assert pytest.approx(ret.final_molecule.measure([0, 1]), 1.0e-4) == 1.3459150737 @@ -383,3 +387,57 @@ def test_optimization_mrchem(input_data, optimizer): assert pytest.approx(ret.final_molecule.measure([0, 1]), 1.0e-3) == 1.3860734486984705 assert ret.provenance.creator.lower() == optimizer assert ret.trajectory[0].provenance.creator.lower() == "mrchem" + + +def checkver_and_convert_proc(mdl, tnm, prepost): + import json + + import pydantic + import qcelemental as qcel + + def check_model_v1(m): + assert isinstance(m, pydantic.v1.BaseModel), f"type({m.__class__.__name__}) = {type(m)} !⊆ v1.BaseModel" + assert isinstance( + m, qcel.models.v1.basemodels.ProtoModel + ), f"type({m.__class__.__name__}) = {type(m)} !⊆ v1.ProtoModel" + assert m.schema_version == 1, f"{m.__class__.__name__}.schema_version = {m.schema_version} != 1" + + def check_model_v2(m): + assert isinstance(m, pydantic.BaseModel), f"type({m.__class__.__name__}) = {type(m)} !⊆ BaseModel" + assert isinstance( + m, qcel.models.v2.basemodels.ProtoModel + ), f"type({m.__class__.__name__}) = {type(m)} !⊆ v2.ProtoModel" + assert m.schema_version == 2, f"{m.__class__.__name__}.schema_version = {m.schema_version} != 2" + + if prepost == "pre": + dict_in = isinstance(mdl, dict) + if "as_v1" in tnm or "to_v2" in tnm or "None" in tnm: + if dict_in: + mdl = qcel.models.v1.OptimizationInput(**mdl) + check_model_v1(mdl) + elif "as_v2" in tnm or "to_v1" in tnm: + if dict_in: + mdl = qcel.models.v2.OptimizationInput(**mdl) + check_model_v2(mdl) + mdl = mdl.convert_v(1) + + if dict_in: + mdl = mdl.model_dump() + + elif prepost == "post": + dict_in = isinstance(mdl, dict) + if "as_v1" in tnm or "to_v1" in tnm or "None" in tnm: + if dict_in: + mdl = qcel.models.v1.OptimizationResult(**mdl) + check_model_v1(mdl) + elif "as_v2" in tnm or "to_v2" in tnm: + if dict_in: + mdl = qcel.models.v2.OptimizationResult(**mdl) + mdl = mdl.convert_v(2) + check_model_v2(mdl) + + if dict_in: + # imitates compute(..., return_dict=True) + mdl = json.loads(mdl.json()) + + return mdl