Skip to content

Commit

Permalink
Merge pull request #6 from loriab/qcngtestsm
Browse files Browse the repository at this point in the history
add multilevel testing from qcengine
  • Loading branch information
bennybp authored Mar 26, 2024
2 parents 971b07c + fe41fdc commit ff6fd99
Show file tree
Hide file tree
Showing 12 changed files with 802 additions and 79 deletions.
15 changes: 8 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,12 @@ jobs:
- label: Py-min
python-version: "3.8"
runs-on: ubuntu-latest
pytest: ""
pytest: "-m 'not addon'"

- label: Py-max
python-version: "3.12"
runs-on: ubuntu-latest
pytest: "-k 'not (he4 and all)'"
pytest: "-k 'not (he4 and (3b or 4b))'"

name: "🐍 ${{ matrix.cfg.python-version }} • ${{ matrix.cfg.label }} • ${{ matrix.cfg.runs-on }}"
runs-on: ${{ matrix.cfg.runs-on }}
Expand Down Expand Up @@ -58,15 +58,14 @@ jobs:
- pytest-cov
- codecov
# Testing CMS
- nwchem
- networkx
- psi4
#- nwchem # lands on different he4 soln
#- networkx
EOF
if [[ "${{ runner.os }}" == "Linux" ]]; then
:
if [[ "${{ matrix.cfg.label }}" == "Py-min" ]]; then
sed -i "s;pydantic;pydantic=1;g" export.yaml
sed -i "s;- nwchem;#- nwchem;g" export.yaml
sed -i "s;- networkx;#- networkx;g" export.yaml
fi
fi
# model sed for L/W
Expand Down Expand Up @@ -109,7 +108,9 @@ jobs:
- name: PyTest
run: |
pytest -rws -v ${{ matrix.cfg.pytest }} --cov=qcmanybody --color=yes --cov-report=xml --durations 50 --durations-min 5 qcmanybody/ tests/
pytest -rws -v ${{ matrix.cfg.pytest }} \
--cov=qcmanybody --cov-report=xml \
--color=yes --durations 50 --durations-min 20
- name: CodeCov
uses: codecov/codecov-action@v3
Expand Down
22 changes: 22 additions & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[pytest]
minversion = 7.0
addopts = --import-mode=importlib

# locations from which test helper fls can be imported
pythonpath =
tests/
qcmanybody/models

# test fl locations rel to this fl to use if paths not specified
testpaths =
tests/
qcmanybody/models

markers =
addon: "tests require external non-required software"

cfour: "tests using CFOUR software; skip if unavailable"
gamess: "tests using GAMESS software; skip if unavailable"
nwchem: "tests using classic NWChem software; skip if unavailable"
psi4: "tests using Psi4 software; skip if unavailable"

13 changes: 9 additions & 4 deletions qcmanybody/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ def build_nbody_compute_list(

# Build up compute sets
if "cp" in bsse_type:
# Everything is in full n-mer basis
# Everything is in counterpoise/nfr-mer basis
basis_tuple = tuple(fragment_range)

if supersystem_ie_only:
Expand All @@ -90,13 +90,18 @@ def build_nbody_compute_list(
cp_compute_list[nfragments].add((x, basis_tuple))
else:
for nb in nbodies:
# Note A.1: nb=1 is skipped because the nfr-mer-basis monomer
# contributions cancel at 1-body. These skipped tasks will be
# ordered anyways if higher bodies are requested. Monomers for
# the purpose of total energies use monomer basis, not these
# skipped tasks. See coordinating Note A.2 .
if nb > 1:
for sublevel in range(1, nb + 1):
for x in itertools.combinations(fragment_range, sublevel):
cp_compute_list[nb].add((x, basis_tuple))

if "nocp" in bsse_type:
# Everything in monomer basis
# Everything in natural/n-mer basis
if supersystem_ie_only:
for sublevel in [1, nfragments]:
for x in itertools.combinations(fragment_range, sublevel):
Expand All @@ -121,8 +126,8 @@ def build_nbody_compute_list(
if return_total_data and 1 in nbodies:
# Monomers in monomer basis
nocp_compute_list.setdefault(1, set())
for frag in range(1, nfragments + 1):
nocp_compute_list[1].add(((frag,), (frag,)))
for ifr in fragment_range:
nocp_compute_list[1].add(((ifr,), (ifr,)))

if include_supersystem:
# Add supersystem info to the compute list (nocp only)
Expand Down
13 changes: 10 additions & 3 deletions qcmanybody/manybody.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,8 +170,8 @@ def _assemble_nbody_components(
"""

# which level are we assembling?
delabled = [delabeler(k) for k in component_results.keys()]
mc_level_labels = {x[0] for x in delabled}
delabeled = [delabeler(k) for k in component_results.keys()]
mc_level_labels = {x[0] for x in delabeled}

if len(mc_level_labels) != 1:
raise RuntimeError(f"Multiple model chemistries passed into _assemble_nbody_components: {mc_level_labels}")
Expand Down Expand Up @@ -326,7 +326,14 @@ def _analyze(
filtered_results = {k: v for k, v in property_results.items() if delabeler(k)[0] == mc_label}

if not filtered_results:
raise RuntimeError(f"No data found for model chemistry {mc_label}")
if nbody_list == [1]:
# Note A.2: Note A.1 holds, but for the special case of CP-only
# and rtd=False and multilevel with a separate level for
# 1-body, the skipped tasks run afoul of sanity checks, so
# we'll add a dummy result.
filtered_results = {labeler(mc_label, [1000], [1000]): shaped_zero(property_shape)}
else:
raise RuntimeError(f"No data found for model chemistry {mc_label}")

nb_component_results = self._assemble_nbody_components(property_label, filtered_results)
mc_results[mc_label] = nb_component_results
Expand Down
78 changes: 78 additions & 0 deletions qcmanybody/models/addons.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
from typing import List

import pytest

from qcelemental.util import parse_version, which, which_import
from qcengine.testing import _programs as _programs_qcng


__all__ = [
"using",
"uusing",
]


def is_qcfractal_new_enough(version_feature_introduced):
if not which_import('qcfractal', return_bool=True):
return False
import qcfractal
return parse_version(qcfractal.__version__) >= parse_version(version_feature_introduced)


# Figure out what is imported
# * anything _not_ in QCEngine goes here
_programs = {
"vasp": False,
}


def has_program(name):
# any aliases or merged names go here
if name in _programs:
return _programs[name]
elif name in _programs_qcng:
return _programs_qcng[name]
else:
raise KeyError(f"Program {name} not registered with QCManyBody testing.")


_using_cache = {}


def _using(program: str) -> None:
if program not in _using_cache:
import_message = f"Not detecting module {program}. Install package if necessary to enable tests."
skip = pytest.mark.skipif(has_program(program) is False, reason=import_message)
general = pytest.mark.addon
particular = getattr(pytest.mark, program)

all_marks = (skip, general, particular)
_using_cache[program] = [_compose_decos(all_marks), all_marks]


def _compose_decos(decos):
# thanks, https://stackoverflow.com/a/45517876
def composition(func):
for deco in reversed(decos):
func = deco(func)
return func
return composition


def uusing(program: str):
"""Apply 3 marks: skipif program not detected, label "addon", and label program.
This is the decorator form for whole test functions: `@mark\n@mark`.
"""
_using(program)
return _using_cache[program][0]


def using(program: str) -> List:
"""Apply 3 marks: skipif program not detected, label "addon", and label program.
This is the inline form for parameterizations: `marks=[]`.
In combo, do `marks=[*using(), pytest.mark.quick]`
"""
_using(program)
return _using_cache[program][1]
15 changes: 15 additions & 0 deletions qcmanybody/models/qcng_computer.py
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,11 @@ def from_qcschema_ben(cls, input_model: ManyBodyInput):
)
nb_per_mc = computer_model.nbodies_per_mc_level

print("\n<<< (ZZ 1) QCEngine harness ManyBodyComputerQCNG.from_qcschema_ben >>>")
# v2: pprint.pprint(computer_model.model_dump(), width=200)
pprint.pprint(computer_model.dict(), width=200)
print(f"nbodies_per_mc_level={nb_per_mc}")

comp_levels = {}
for mc_level_idx, mtd in enumerate(computer_model.levels.values()):
for lvl1 in nb_per_mc[mc_level_idx]:
Expand All @@ -401,6 +406,9 @@ def from_qcschema_ben(cls, input_model: ManyBodyInput):
computer_model.supersystem_ie_only,
)

print("\n<<< (ZZ 2) QCManyBody module ManyBodyCalculator >>>")
print(dir(calculator_cls))

component_results = {}

for chem, label, imol in calculator_cls.iterate_molecules():
Expand All @@ -421,11 +429,18 @@ def from_qcschema_ben(cls, input_model: ManyBodyInput):
for p in props:
if hasattr(result.properties, f"return_{p}"):
v = getattr(result.properties, f"return_{p}")
# print(f" {label} {p}: {v}")
if v is not None:
component_results[label][p] = v

print("\n<<< (ZZ 2) QCEngine harness ManyBodyComputerQCNG.from_qcschema_ben component_results >>>")
pprint.pprint(component_results, width=200)

print("start to analyze")
analyze_back = calculator_cls.analyze(component_results)
analyze_back["nbody_number"] = len(component_results)
print("\n<<< (ZZ 3) QCEngine harness ManyBodyComputerQCNG.from_qcschema_ben analyze_back >>>")
pprint.pprint(analyze_back, width=200)

return computer_model.get_results(external_results=analyze_back)

Expand Down
Loading

0 comments on commit ff6fd99

Please sign in to comment.