Merge pull request #177 from isayevlab/feat/conda-no-aimnet #198
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Tests | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| concurrency: | |
| group: tests-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| test: | |
| name: pytest (py${{ matrix.python }}, ani=${{ matrix.ani }}) | |
| runs-on: ubuntu-latest | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| python: ["3.11", "3.12"] | |
| ani: [false] | |
| include: | |
| - python: "3.12" | |
| ani: true | |
| steps: | |
| - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 | |
| - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 | |
| with: | |
| python-version: ${{ matrix.python }} | |
| - name: Install | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install -e ".[ase,dev]" # dev provides pytest/pytest-cov | |
| - name: Install ani extra | |
| if: matrix.ani | |
| run: pip install -e ".[ani]" | |
| - name: Verify every test module imports | |
| # --continue-on-collection-errors on the fast run hides a module that | |
| # fails to import, so assert collection cleanliness separately. | |
| run: pytest tests/ --collect-only -q | |
| - name: Run fast tests | |
| run: pytest tests/ -q -m "not slow" --continue-on-collection-errors | |
| # Here rather than in `lint`, because this job is the only one that | |
| # installs the dependencies. mypy resolves a third-party import to `Any` | |
| # when the package is absent, so a dep-free run cannot see a wrong tensor, | |
| # array or ASE argument at all -- it reported 40 errors where a run with | |
| # the extras present reports 66, and the 26 it missed are precisely the | |
| # ones a type checker is worth having here for. | |
| # | |
| # Only on the `ani` leg: three near-identical reports are noise, and this | |
| # is the leg with the most complete type information. Running it under | |
| # 3.12 does not weaken the floor -- `python_version = "3.11"` in | |
| # pyproject.toml is what decides the semantics checked, not the | |
| # interpreter. | |
| - name: mypy (non-blocking until type debt is cleared) | |
| if: matrix.ani | |
| run: mypy src/Auto3D/ || true | |
| no-aimnet: | |
| # The conda-forge package will ship Auto3D's dependency set minus aimnet | |
| # plus torchani and ase -- aimnet cannot be packaged for conda-forge (its | |
| # dependency nvalchemi-toolkit-ops is pip-only). conda-recipe/meta.yaml is | |
| # updated to that shape as part of the 3.1.1 release. This job emulates | |
| # that environment and is the enforcement mechanism for the no-aimnet | |
| # path: without it, the path rots the first time someone adds a | |
| # module-level `import aimnet`. | |
| # | |
| # `pip uninstall -y aimnet` below leaves aimnet's own transitive | |
| # dependencies installed, so this emulation cannot catch code that leans | |
| # on a package that only arrives via aimnet's dependency tree -- the | |
| # genuine conda build is the true gate for that. | |
| name: no-aimnet (conda package environment) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 | |
| - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 | |
| with: | |
| python-version: "3.12" | |
| - name: Install, then remove aimnet | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install -e ".[ani,ase,dev]" | |
| pip uninstall -y aimnet | |
| - name: Bare import works without aimnet | |
| run: python -c "import Auto3D" | |
| - name: Whole-suite collection stays clean without aimnet | |
| run: pytest tests/ --collect-only -q | |
| - name: No-aimnet behavior tests | |
| run: pytest tests/test_no_aimnet.py -q | |
| - name: ANI2xt still constructs (bundled weights, no download) | |
| run: pytest "tests/test_model_factory.py" -q -k "ani2xt" | |
| slow: | |
| name: slow tests (NNP integration) | |
| runs-on: ubuntu-latest | |
| # These tests are NNP inference on CPU, so their wall time tracks whatever | |
| # share of the runner we get. Three consecutive runs of the same commit | |
| # took 6m21s, 17m57s and 6m52s: the middle one exceeded the old 20-minute | |
| # ceiling and was cancelled *after* all 58 tests had passed, reporting a | |
| # red slow tier for code that was fine. A job that fails on runner luck | |
| # teaches people to ignore it, which is worse than a job that is slow. | |
| # 45 gives roughly 2.5x headroom over the worst observed run while still | |
| # catching a genuine hang. | |
| timeout-minutes: 45 | |
| steps: | |
| - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 | |
| - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 | |
| with: | |
| python-version: "3.12" | |
| - name: Cache downloaded model weights | |
| uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 | |
| with: | |
| # AIMNet2 weights download to ~/.cache/aimnet; torchani/torch.hub | |
| # weights (ANI2x) land in ~/.cache/torch. Cache both so the slow | |
| # suite does not re-download on every run. | |
| path: | | |
| ~/.cache/aimnet | |
| ~/.cache/torch | |
| key: nnp-model-cache-v1 | |
| - name: Install | |
| run: | | |
| python -m pip install --upgrade pip | |
| # ase for thermo, ani for the torchani-gated slow tests, dev for pytest | |
| pip install -e ".[ase,ani,dev]" | |
| - name: Warm the AIMNet2 model cache (hard failure if unreachable) | |
| # An unavailable gate must never be reportable as a passing gate. If the | |
| # registry or network is down, fail here loudly rather than letting the | |
| # suite skip or silently pass (audit: the remediation gate depends on | |
| # this model being present). | |
| run: | | |
| python - <<'PY' | |
| import sys | |
| import torch | |
| from Auto3D.engines.model_factory import create_model | |
| try: | |
| create_model("AIMNET", torch.device("cpu")) | |
| except Exception as exc: | |
| print(f"::error::AIMNet2 model unavailable, gate cannot run: {exc}") | |
| sys.exit(1) | |
| print("AIMNet2 model cache warm") | |
| PY | |
| - name: Run slow NNP + pipeline tests | |
| # Previously limited to three files because the heavy end-to-end modules | |
| # were flaky under combined ordering. test_auto3D.py now runs every | |
| # pipeline test through the job_dir/isolated_input fixtures and | |
| # test_thermo.py stages its opt_geometry inputs in tmp_path, so those | |
| # two no longer write job directories or output SDFs into | |
| # tests/files/. The remaining three (test_SPE.py, test_isomer_engine.py, | |
| # test_tauto.py) still do not use the fixtures, so for them the | |
| # protection is unchanged: main()'s microsecond-resolution job naming, | |
| # strictly serial CI execution, and the autouse GPU-teardown fixture | |
| # applied via the `slow` marker -- assessed at 70-80% confidence, not | |
| # guaranteed (audit M31). | |
| run: > | |
| pytest tests/ -m slow -q | |
| # Executes one notebook for real. Here rather than in the docs workflow | |
| # because every one of the 20 needs a neural network potential, and this | |
| # is the only job with the model cache and the `ani` extra. | |
| # | |
| # One, and the selection was measured on a CPU box with CUDA hidden -- | |
| # which is what a runner is -- rather than on a machine with a GPU. The | |
| # dividing line is not notebook size, it is whether the notebook runs an | |
| # optimization: | |
| # | |
| # single_point_energy (calc_spe, 4 records) 5.9 s | |
| # geometry_optimization (opt_geometry, 4 records) > 600 s | |
| # | |
| # The same optimization notebook takes 74 s on a GPU, which is how it | |
| # first looked affordable. Every other notebook either optimizes or runs | |
| # `main()` end to end, so all of them land on the wrong side of that line; | |
| # those paths are covered by the slow test tier above instead. | |
| # | |
| # What this closes: this notebook calling a function Auto3D no longer has, | |
| # or passing an argument it no longer accepts, now fails here. What it | |
| # does not: the other 19 are still parse-checked only, so the guard in | |
| # tests/test_notebook_properties.py remains the only thing covering all | |
| # 20 -- and it catches a wrong SD property name, not a wrong call. | |
| - name: Execute the single-point notebook | |
| run: | | |
| pip install nbmake | |
| pytest --nbmake --nbmake-timeout=600 example/single_point_energy.ipynb | |
| lint: | |
| name: ruff | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 | |
| - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 | |
| with: | |
| python-version: "3.12" | |
| # ruff needs no dependencies installed; mypy does, so it runs in the | |
| # `test` job instead. See the note on that step. | |
| # Pinned exactly, and matching the `dev` extra in pyproject.toml, because | |
| # `ruff format --check` is a gate: ruff promotes formatting-style changes | |
| # into its stable style across minor releases, with settings unchanged. | |
| # An unpinned install turns any such release into a red build on whichever | |
| # unrelated pull request happens to be open that day -- the same | |
| # fails-on-luck failure mode the slow job's timeout comment describes. | |
| # | |
| # Bumping it is a deliberate act: raise both pins together, and if the new | |
| # version reformats anything, that reformat is its own commit and its own | |
| # `.git-blame-ignore-revs` entry. | |
| - name: Install tooling | |
| run: pip install ruff==0.15.9 | |
| - name: ruff check | |
| run: ruff check src/Auto3D/ tests/ | |
| # CONTRIBUTING.md has told contributors to run `ruff format src/ tests/` | |
| # since long before anything checked. Nothing did, so the tree drifted | |
| # until the documented command produced a 169-file diff -- at which point | |
| # nobody could follow the instruction on an ordinary pull request, and the | |
| # drift compounded. The gate is the half that makes the instruction true. | |
| # | |
| # Unlike the mypy step, this one does NOT end in `|| true`. It starts from | |
| # zero and the fix is one command, so there is no debt to grandfather. | |
| - name: ruff format | |
| run: ruff format --check src/ tests/ |