diff --git a/.github/workflows/alltests.yml b/.github/workflows/alltests.yml index a2b8f551c..ea8d95f42 100644 --- a/.github/workflows/alltests.yml +++ b/.github/workflows/alltests.yml @@ -8,6 +8,13 @@ on: - master workflow_dispatch: +# CodeQL "Workflow does not contain permissions": restrict the GITHUB_TOKEN to +# the minimum. These jobs only read the repo -- checkout, conda/python setup, +# caching, and tests. Codecov uploads authenticate with CODECOV_TOKEN, not the +# GITHUB_TOKEN, so no write scope is needed. +permissions: + contents: read + concurrency: # Keep push and pull_request runs separate so same-SHA PR updates do not inherit cancelled push checks. group: alltests-${{ github.event_name }}-${{ github.event.pull_request.head.repo.full_name || github.repository }}-${{ github.head_ref || github.ref_name }} @@ -68,9 +75,36 @@ jobs: - uses: conda-incubator/setup-miniconda@v3 with: miniconda-version: "latest" - auto-activate-base: true + python-version: ${{ matrix.python-version }} + channels: conda-forge + auto-activate-base: false + # Name the env explicitly: without this the active environment is + # ambiguous (base vs the auto-created `test`), which can leave pip + # and python pointing at different prefixes. + activate-environment: qmcpy-ci conda-remove-defaults: true - use-only-tar-bz2: true + + # Without python-version above, matrix.python-version reached only the job + # name and every job ran the conda base interpreter. Steps touching Python + # must use a profile-loading shell (bash -el / pwsh) to see the env. + - name: Verify interpreter matches the matrix (Unix) + if: runner.os != 'Windows' + shell: bash -el {0} + run: | + echo "which python : $(which python)" + echo "which pip : $(which pip)" + conda info --envs || true + python -c "import sys; print('prefix:',sys.prefix); print(sys.version)" + python -c "import sys; got='.'.join(map(str,sys.version_info[:2])); want='${{ matrix.python-version }}'; assert got==want, 'matrix says %s but interpreter is %s'%(want,got)" + + - name: Verify interpreter matches the matrix (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + Get-Command python | Format-List + conda info --envs + python -c "import sys; print('prefix:',sys.prefix); print(sys.version)" + python -c "import sys; got='.'.join(map(str,sys.version_info[:2])); want='${{ matrix.python-version }}'; assert got==want, 'matrix says %s but interpreter is %s'%(want,got)" # ----------------------------------------------------------- # Clean old coverage files @@ -222,18 +256,22 @@ jobs: - name: Build and cache wheels (Linux) if: runner.os == 'Linux' + shell: bash -el {0} run: | - python -m pip wheel -w ./.wheels .[test,test_torch,test_gpytorch,test_botorch,test_umbridge] || true + python -m pip wheel -w ./.wheels ".[test,test_torch,test_gpytorch,test_botorch,test_umbridge]" || true - name: Install Python dependencies (Linux) if: runner.os == 'Linux' + shell: bash -el {0} run: | pip install --find-links ./.wheels -e ".[test,test_torch,test_gpytorch,test_botorch,test_umbridge]" - name: Build and cache wheels (macOS) if: runner.os == 'macOS' + shell: bash -el {0} run: | - python -m pip wheel -w ./.wheels .[test,test_torch,test_gpytorch,test_botorch] || true + python -m pip wheel -w ./.wheels ".[test,test_torch,test_gpytorch,test_botorch]" || true - name: Install Python dependencies (macOS) if: runner.os == 'macOS' + shell: bash -el {0} run: | pip install --find-links ./.wheels -e ".[test,test_torch,test_gpytorch,test_botorch]" - name: Build and cache wheels (Windows) @@ -246,12 +284,25 @@ jobs: shell: pwsh run: | pip install --find-links ./.wheels -e '.[test,test_torch,test_gpytorch,test_botorch]' - - name: Install MPMC dependencies - run: | - qmcpy-install-mpmc - - name: Validate MPMC dependencies - run: | - python -c "import torch, pyg_lib, torch_geometric; print(f'torch={torch.__version__}'); print('MPMC dependencies ready')" + - name: Install MPMC dependencies (Unix) + if: runner.os != 'Windows' + shell: bash -el {0} + run: qmcpy-install-mpmc + + - name: Install MPMC dependencies (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: qmcpy-install-mpmc + + - name: Validate MPMC dependencies (Unix) + if: runner.os != 'Windows' + shell: bash -el {0} + run: python -c "import torch, pyg_lib, torch_geometric; print(f'torch={torch.__version__}'); print('MPMC dependencies ready')" + + - name: Validate MPMC dependencies (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: python -c "import torch, pyg_lib, torch_geometric; print(f'torch={torch.__version__}'); print('MPMC dependencies ready')" # ----------------------------------------------------------- # Install minimal LaTeX required by Jupyter notebooks (OS-specific) # ----------------------------------------------------------- @@ -375,7 +426,7 @@ jobs: Invoke-WithRetry -Description "Refresh MiKTeX package database" -Script { mpm --admin --update-db } - $packages = @('latexmk','dvipng','cm-super','lmodern','type1cm','tex-gyre') + $packages = @('latexmk','dvipng','cm-super','lmodern','type1cm','tex-gyre','l3backend') foreach ($pkg in $packages) { Invoke-WithRetry -Description "Install MiKTeX package $pkg" -Script { mpm --admin --install=$pkg @@ -386,6 +437,10 @@ jobs: Invoke-WithRetry -Description "Refresh MiKTeX filename database" -Script { initexmf --admin --update-fndb } + $L3BackendPath = kpsewhich l3backend-dvips.def + if ([string]::IsNullOrWhiteSpace($L3BackendPath)) { + throw "MiKTeX install did not provide l3backend-dvips.def" + } latex --version latexmk -v dvipng --version @@ -393,12 +448,60 @@ jobs: # ----------------------------------------------------------- # Run doctests (OS-specific) # ----------------------------------------------------------- - - run: pip freeze - - run: make doctests_minimal - - run: make doctests_torch - - run: make doctests_gpytorch - - run: make doctests_botorch - - run: make doctests_markdown + - name: pip freeze (Unix) + if: runner.os != 'Windows' + shell: bash -el {0} + run: pip freeze + + - name: pip freeze (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: pip freeze + - name: doctests_minimal (Unix) + if: runner.os != 'Windows' + shell: bash -el {0} + run: make doctests_minimal + + - name: doctests_minimal (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: make doctests_minimal + - name: doctests_torch (Unix) + if: runner.os != 'Windows' + shell: bash -el {0} + run: make doctests_torch + + - name: doctests_torch (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: make doctests_torch + - name: doctests_gpytorch (Unix) + if: runner.os != 'Windows' + shell: bash -el {0} + run: make doctests_gpytorch + + - name: doctests_gpytorch (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: make doctests_gpytorch + - name: doctests_botorch (Unix) + if: runner.os != 'Windows' + shell: bash -el {0} + run: make doctests_botorch + + - name: doctests_botorch (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: make doctests_botorch + - name: doctests_markdown (Unix) + if: runner.os != 'Windows' + shell: bash -el {0} + run: make doctests_markdown + + - name: doctests_markdown (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: make doctests_markdown - name: Run umbridge doctests on Linux full sweeps when Docker is available shell: bash -l {0} run: | @@ -411,12 +514,26 @@ jobs: else echo "Skipping umbridge doctests because Docker is not available on this runner" fi - - name: Run MPMC doctests + - name: Run MPMC doctests (Unix) + if: runner.os != 'Windows' + shell: bash -el {0} + run: make doctests_mpmc + + - name: Run MPMC doctests (Windows) + if: runner.os == 'Windows' + shell: pwsh run: make doctests_mpmc # ----------------------------------------------------------- # Run unittests for Python source files # ----------------------------------------------------------- - - name: Run unittests (parallel) + - name: Run unittests (parallel, Unix) + if: runner.os != 'Windows' + shell: bash -el {0} + run: make unittests + + - name: Run unittests (parallel, Windows) + if: runner.os == 'Windows' + shell: pwsh run: make unittests # ----------------------------------------------------------- diff --git a/.github/workflows/unittests.yml b/.github/workflows/unittests.yml index 14342777c..1ef7bace0 100644 --- a/.github/workflows/unittests.yml +++ b/.github/workflows/unittests.yml @@ -11,6 +11,13 @@ on: - master workflow_dispatch: +# CodeQL "Workflow does not contain permissions": restrict the GITHUB_TOKEN to +# the minimum. These jobs only read the repo -- checkout, conda/python setup, +# caching, and tests. Codecov uploads authenticate with CODECOV_TOKEN, not the +# GITHUB_TOKEN, so no write scope is needed. +permissions: + contents: read + concurrency: group: unittests-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true @@ -21,23 +28,19 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: + # These versions are real: python-version is passed to + # setup-miniconda and asserted by the "Verify interpreter" step. + # Floor is 3.10 -- the `test` extra needs pytest >= 9.0.3 and + # parsl >= 2026.01.05, which both require 3.10+. Older interpreters + # run in the core-tests job below. Each version appears once; the + # full install set resolves on all three OSes for 3.10-3.14. include: - - os: macos-latest - python-version: '3.5' - - os: macos-latest - python-version: '3.8' - os: macos-latest python-version: '3.11' - os: macos-latest python-version: '3.14' - - os: ubuntu-latest - python-version: '3.6' - - os: ubuntu-latest - python-version: '3.9' - os: ubuntu-latest python-version: '3.12' - - os: windows-latest - python-version: '3.7' - os: windows-latest python-version: '3.10' - os: windows-latest @@ -48,9 +51,37 @@ jobs: - uses: conda-incubator/setup-miniconda@v3 with: miniconda-version: "latest" - auto-activate-base: true + python-version: ${{ matrix.python-version }} + channels: conda-forge + auto-activate-base: false + # Name the env explicitly: without this the active environment is + # ambiguous (base vs the auto-created `test`), which can leave pip + # and python pointing at different prefixes. + activate-environment: qmcpy-ci conda-remove-defaults: true - use-only-tar-bz2: true + + # Guards the failure mode this matrix used to have: the job name claimed a + # Python version while every job actually ran the conda base interpreter. + # Steps that touch Python must use a profile-loading shell (bash -el / pwsh) + # or they will see base rather than the activated env. + - name: Verify interpreter matches the matrix (Unix) + if: runner.os != 'Windows' + shell: bash -el {0} + run: | + echo "which python : $(which python)" + echo "which pip : $(which pip)" + conda info --envs || true + python -c "import sys; print('prefix:',sys.prefix); print(sys.version)" + python -c "import sys; got='.'.join(map(str,sys.version_info[:2])); want='${{ matrix.python-version }}'; assert got==want, 'matrix says %s but interpreter is %s'%(want,got)" + + - name: Verify interpreter matches the matrix (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + Get-Command python | Format-List + conda info --envs + python -c "import sys; print('prefix:',sys.prefix); print(sys.version)" + python -c "import sys; got='.'.join(map(str,sys.version_info[:2])); want='${{ matrix.python-version }}'; assert got==want, 'matrix says %s but interpreter is %s'%(want,got)" # ----------------------------------------------------------- # Clean old coverage files @@ -194,21 +225,25 @@ jobs: - name: Build and cache wheels (Linux) if: runner.os == 'Linux' + shell: bash -el {0} run: | - python -m pip wheel -w ./.wheels .[test,test_torch,test_gpytorch,test_botorch,test_umbridge] || true + python -m pip wheel -w ./.wheels ".[test,test_torch,test_gpytorch,test_botorch,test_umbridge]" || true - name: Install Python dependencies (Linux) if: runner.os == 'Linux' + shell: bash -el {0} run: | pip install --find-links ./.wheels -e ".[test,test_torch,test_gpytorch,test_botorch,test_umbridge]" - name: Build and cache wheels (macOS) if: runner.os == 'macOS' + shell: bash -el {0} run: | - python -m pip wheel -w ./.wheels .[test,test_torch,test_gpytorch,test_botorch] || true + python -m pip wheel -w ./.wheels ".[test,test_torch,test_gpytorch,test_botorch]" || true - name: Install Python dependencies (macOS) if: runner.os == 'macOS' + shell: bash -el {0} run: | pip install --find-links ./.wheels -e ".[test,test_torch,test_gpytorch,test_botorch]" @@ -227,5 +262,145 @@ jobs: # ----------------------------------------------------------- # Run unittests for Python source files # ----------------------------------------------------------- - - name: Run unittests (parallel) + - name: Run unittests (parallel, Unix) + if: runner.os != 'Windows' + shell: bash -el {0} run: make unittests + + - name: Run unittests (parallel, Windows) + if: runner.os == 'Windows' + shell: pwsh + run: make unittests + + # Exercise the supported 3.9 floor without the `test` extra's Python 3.10+ + # dependencies. First verify the built wheel as a user would install it; + # then run test/test_*.py against the slim `test_core` extra. Modules needing + # an optional stack skip themselves via pytest.importorskip. + # The 3.9 support claim is OS-independent, so prove it on every OS we ship + # for. qmctoolscl publishes exactly one wheel (cp312, win_amd64), so all three + # legs build it from its sdist -- which is precisely the risk being covered. + core-tests: + name: Core Unit Tests on ${{ matrix.os }} (Python ${{ matrix.python-version }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + python-version: ['3.9'] + steps: + - uses: actions/checkout@v4 + + - uses: conda-incubator/setup-miniconda@v3 + with: + miniconda-version: "latest" + python-version: ${{ matrix.python-version }} + channels: conda-forge + auto-activate-base: false + activate-environment: qmcpy-core + conda-remove-defaults: true + + - name: Verify interpreter matches the matrix (Unix) + if: runner.os != 'Windows' + shell: bash -el {0} + run: | + echo "which python : $(which python)" + conda info --envs || true + python -c "import sys; print('prefix:',sys.prefix); print(sys.version)" + python -c "import sys; got='.'.join(map(str,sys.version_info[:2])); want='${{ matrix.python-version }}'; assert got==want, 'matrix says %s but interpreter is %s'%(want,got)" + + - name: Verify interpreter matches the matrix (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + conda info --envs + python -c "import sys; print('prefix:',sys.prefix); print(sys.version)" + python -c "import sys; got='.'.join(map(str,sys.version_info[:2])); want='${{ matrix.python-version }}'; assert got==want, 'matrix says %s but interpreter is %s'%(want,got)" + + - name: Build and test the user wheel (Unix) + if: runner.os != 'Windows' + shell: bash -el {0} + run: | + python -m pip install build + python -m build --wheel + python -m pip install dist/*.whl + python -m pip check + python -c "import os,tempfile; os.chdir(tempfile.gettempdir()); import qmcpy; print(qmcpy.__file__)" + + - name: Build and test the user wheel (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + python -m pip install build + python -m build --wheel + $wheel = (Get-ChildItem dist/*.whl | Select-Object -First 1).FullName + python -m pip install $wheel + python -m pip check + python -c "import os,tempfile; os.chdir(tempfile.gettempdir()); import qmcpy; print(qmcpy.__file__)" + + - name: Install qmcpy and minimal test dependencies (Unix) + if: runner.os != 'Windows' + shell: bash -el {0} + run: python -m pip install -e ".[test_core]" + + - name: Install qmcpy and minimal test dependencies (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: python -m pip install -e ".[test_core]" + + - name: Run core unit tests (Unix) + if: runner.os != 'Windows' + shell: bash -el {0} + run: make unittests_core + + - name: Run core unit tests (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: make unittests_core + + # Early-warning job for the next Python. 3.15.0-rc.1 is published in + # actions/python-versions but NOT in conda-forge, so this uses setup-python + # rather than setup-miniconda like the jobs above. + # + # Non-blocking by design: at time of writing scipy (a core dependency) and + # scikit-learn publish no cp315 wheels, so the install is expected to fail + # until the scientific stack catches up. The job exists to tell us the day + # that changes -- when it goes green, promote 3.15 into the `tests` matrix. + prerelease-tests: + name: Core Unit Tests (Python ${{ matrix.python-version }}, pre-release) + runs-on: ubuntu-latest + # Never gate a merge on an unreleased interpreter. + continue-on-error: true + strategy: + fail-fast: false + matrix: + python-version: ['3.15.0-rc.1'] + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + allow-prereleases: true + + - name: Report interpreter + run: python -VV + + # Step-level continue-on-error is still needed: without it a failed + # install would skip the reporting step below. + - name: Install qmcpy and minimal test dependencies + id: install + continue-on-error: true + run: pip install -e ".[test_core]" + + - name: Run core unit tests + if: steps.install.outcome == 'success' + run: make unittests_core + + - name: Report pre-release ecosystem not ready + if: steps.install.outcome != 'success' + run: | + echo "::warning title=Python ${{ matrix.python-version }} not installable::\ + qmcpy could not be installed on Python ${{ matrix.python-version }}. \ + This is expected while the scientific stack lacks cp315 wheels (scipy \ + and scikit-learn in particular). See the install log above; when this \ + job passes, promote 3.15 into the tests matrix." diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a4dc7a59a..d3ee3a831 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -60,10 +60,7 @@ While `dev` contains the most complete set of install dependencies, a number of pip install -e ".[dev]" ~~~ -The `dev` extra includes QMCPy's PyPI-hosted MPMC dependencies. MPMC additionally -requires a platform-specific `pyg_lib` wheel that is not available from PyPI. -After installing `dev`, let the QMCPy installer select the wheel page matching -the installed PyTorch build: +The `dev` extra includes QMCPy's PyPI-hosted MPMC dependencies. MPMC additionally requires a platform-specific `pyg_lib` wheel that is not available from PyPI. After installing `dev`, let the QMCPy installer select the wheel page matching the installed PyTorch build: ~~~bash qmcpy-install-mpmc @@ -76,6 +73,27 @@ pip install -e ".[mpmc]" qmcpy-install-mpmc ~~~ +### Minimum Python Version by Role + +`requires-python` covers a bare install; the optional dependency groups in `pyproject.toml` raise it. Each row shows the strictest floor among that role's pinned dependencies. Rows marked `+` add a capability to the Application-user install; unmarked rows are self-contained role profiles. + +| Role | Install command | Binding constraint | Minimum Python | +|---|---|---|---| +| Application user | `pip install qmcpy` | QMCPy support policy | 3.9 | +| + torch / GP features | `pip install "qmcpy[torch,gpytorch]"` | inherits the QMCPy floor | 3.9 | +| + MPMC | `pip install "qmcpy[mpmc]"`, then `qmcpy-install-mpmc` | `torch >= 2.10.0` | 3.10 | +| + Bayesian optimization | `pip install "qmcpy[botorch]"` | `botorch >= 0.10.0` | 3.9 | +| Course instructor (`class`) | `pip install -e ".[class]"` | `arviz >= 0.17`, `matplotlib >= 3.9.0`, `statsmodels >= 0.14.3` | 3.9 | +| Test developer | `pip install -e ".[test]"` | `pytest >= 9.0.3`, `parsl >= 2026.01.05` | 3.10 | +| Documentation developer | `pip install -e ".[docs]"` | inherits `test`; `pylint >= 4.0.5` | 3.10 | +| Release / core developer | `pip install -e ".[dev]"` | inherits `docs` / `test` | 3.10 | + +Using `qmcpy` needs Python **3.9+**; contributing code, running tests, or building docs needs **3.10+**. We recommend 3.13 for development. + +Python 3.9 is a deliberate QMCPy **support-policy floor**, not a claim about source syntax or `qmctoolscl`'s declared floor. It is the oldest interpreter whose current runtime stack QMCPy commits to support and test; earlier versions are outside that policy even if a particular toolchain can install them. + +CI measures the lower tier rather than assuming it: `unittests.yml`'s `core-tests` job builds the QMCPy wheel on Python 3.9 on Linux, macOS, and Windows, installs it with no extras, checks its dependencies, and imports it from outside the source tree. The 3.9 claim is OS-independent, and `qmctoolscl` ships only one wheel (cp312, `win_amd64`), so every leg builds it from its source distribution. It then runs `make unittests_core` with the slim `test_core` extra and no notebook stack. Its main `tests` job runs the full suite on 3.10-3.14, each version on one operating system. Every conda matrix asserts the running interpreter before any test runs. Test modules self-skip via `pytest.importorskip` when an optional stack (torch, gpytorch, PyG) is absent, so each interpreter runs what applies to it. + ## 📚 Using `qmcpy` In Courses (`class` Extra) `qmcpy` provides a `class` optional dependency group that installs a complete teaching environment (JupyterLab, plotting, statistics, and utilities) in addition to `qmcpy` itself. diff --git a/README.md b/README.md index b55375193..5d31f0817 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,8 @@ The [QMCPy documentation](https://QMCSoftware.github.io/QMCSoftware/) contains a pip install qmcpy ``` +Requires Python >= 3.9. Contributing code, running tests, or building the documentation requires Python >= 3.10 — see the [Minimum Python Version by Role](https://qmcsoftware.github.io/QMCSoftware/CONTRIBUTING/#minimum-python-version-by-role) table in the contributing guidelines for the full breakdown. + To install from source, please see the [contributing guidelines](https://qmcsoftware.github.io/QMCSoftware/CONTRIBUTING/). ## Citation @@ -64,4 +66,4 @@ Want to contribute to QMCPy? Please see our [guidelines for contributors](https: This software would not be possible without the efforts of the [QMCPy community](https://qmcsoftware.github.io/QMCSoftware/community) including our steering council, collaborators, contributors, and sponsors. -QMCPy is distributed under an [Apache 2.0 license from the Illinois Institute of Technology](https://github.com/QMCSoftware/QMCSoftware/blob/master/LICENSE). \ No newline at end of file +QMCPy is distributed under an [Apache 2.0 license from the Illinois Institute of Technology](https://github.com/QMCSoftware/QMCSoftware/blob/master/LICENSE). diff --git a/docs/ci-testing.md b/docs/ci-testing.md index 5f33765b4..97956fdb0 100644 --- a/docs/ci-testing.md +++ b/docs/ci-testing.md @@ -6,9 +6,11 @@ This page summarizes QMCPy's current GitHub Actions CI layout. | Workflow | Trigger | Runner / Python | Main work | |---|---|---|---| -| `alltests.yml` | Feature-branch `push` | `ubuntu`, Python `3.13` | | -| `alltests.yml` | `push` to `develop` or `master`; PR into `develop` or `master`; `workflow_dispatch` | `ubuntu`, `macos`, `windows`; Python `3.13` | | -| `unittests.yml` | `push` to `develop` or `master`; PR into `develop` or `master`; `workflow_dispatch` | `ubuntu`, `macos`, `windows`; Python `3.5` to `3.14` | | +| `alltests.yml` | Feature-branch `push` | `ubuntu`, Python `3.13` | | +| `alltests.yml` | `push` to `develop` or `master`; PR into `develop` or `master`; branch name ending in `choi`; `workflow_dispatch` | `ubuntu`, `macos`, `windows`; Python `3.13` | | +| `unittests.yml` (`tests` job) | `push` to `develop` or `master`; PR into `develop` or `master`; `workflow_dispatch` | `ubuntu`, `macos`, `windows`; Python `3.10` to `3.14` | | +| `unittests.yml` (`core-tests` job) | same as above | `ubuntu`, `macos`, `windows`; Python `3.9` | | +| `unittests.yml` (`prerelease-tests` job) | same as above | `ubuntu`; Python `3.15.0-rc.1` | | | `docs.yml` | `push` to `master` | `ubuntu`, Python `3.13` | | | `pep8.yml` | `push` to `develop` or `master`; `workflow_dispatch` | `ubuntu`, Python `3.13` | | | `pypi-stats.yml` | Weekly schedule; `workflow_dispatch` | `ubuntu`, Python `3.13` | | @@ -18,13 +20,29 @@ There is no nightly CI schedule. ## Policy - Linux is the default feedback path and runs on every push. -- macOS and Windows in `alltests.yml` are reserved for `develop`/`master` pushes, pull requests into those branches, and manual runs. +- macOS and Windows in `alltests.yml` are reserved for `develop`/`master` pushes, pull requests into those branches, branches whose name ends in `choi`, and manual runs. - `concurrency` cancels superseded runs in both workflows; in `alltests.yml`, `push` and `pull_request` use separate groups so a PR does not inherit cancelled sibling checks from a same-SHA push. -- `alltests.yml` pins Miniconda base Python to `3.13`; `unittests.yml` still uses the base environment without explicitly passing `matrix.python-version` into `setup-miniconda`. +- Both `unittests.yml` and `alltests.yml` pass `matrix.python-version` to `setup-miniconda` and assert the running interpreter before any test runs, so their version labels are real. Steps that touch Python use a profile-loading shell (`bash -el {0}` on Unix, `pwsh` on Windows); the default non-login shell silently falls back to the conda base interpreter, which is how these matrices previously went green without testing the versions they named. - Booktests are skipped on feature-branch pushes and run only in the full sweep. +- `unittests.yml` is tiered: `tests` installs the full `test` extra (needing Python `3.10`+ via `pytest >= 9.0.3` and `parsl >= 2026.01.05`), `core-tests` verifies the built no-extra wheel before installing the slim `test_core` extra, and `prerelease-tests` looks ahead to the next interpreter. Test modules self-skip through `pytest.importorskip` when an optional stack is missing. +- The pre-release tier is informational and never gates a merge. Promote a version out of it into the `tests` matrix once the job passes; `Programming Language :: Python :: 3.15` is deliberately **not** in `pyproject.toml` classifiers until then. - UMBridge doctests run only on Linux full sweeps with Docker available. +- MPMC steps in `alltests.yml` are **not** OS-gated: they run on every OS the matrix selects. See [MPMC Coverage by OS](#mpmc-coverage-by-os). - `workflow_dispatch` means manually triggered workflow. +## MPMC Coverage by OS + +MPMC needs a platform-specific `pyg_lib` wheel that PyPI does not carry, installed separately by `qmcpy-install-mpmc`. Only `alltests.yml` does that, and its MPMC steps carry no `if: runner.os` condition, so they run on every OS the matrix selects. + +| Workflow / trigger | Python | Ubuntu | macOS | Windows | +|---|---|---|---|---| +| `alltests.yml`, full sweep | `3.13` | Run | Run | Run | +| `alltests.yml`, feature-branch `push` | `3.13` | Run | Not in matrix | Not in matrix | +| `unittests.yml` (`tests`) | `3.10`-`3.14` | Skipped | Skipped | Skipped | +| `unittests.yml` (`core-tests`) | `3.9` | Skipped | Skipped | Skipped | + +"Run" covers both the MPMC doctests (`make doctests_mpmc`) and the MPMC unit tests in `test/test_dd_mpmc.py`. `unittests.yml` never calls `qmcpy-install-mpmc`, so those tests skip there via `pytest.importorskip("pyg_lib")` and its jobs pass without exercising MPMC — treat `alltests.yml` as the only source of MPMC signal. See [mpmc-compatibility.md](mpmc-compatibility.md) for the version-support policy behind this split. + ## Related Docs - [tests.md](tests.md): local Makefile targets and coverage commands. diff --git a/docs/mpmc-compatibility.md b/docs/mpmc-compatibility.md index 92d6f1526..ebe1939c9 100644 --- a/docs/mpmc-compatibility.md +++ b/docs/mpmc-compatibility.md @@ -7,7 +7,7 @@ - Treat MPMC as an optional feature, not part of the minimum QMCPy dependency set. - Prefer `pyg_lib` plus `torch-geometric`; do not require `torch-cluster` as a separate dependency. - For reproducible local work and future CI pinning, prefer a modern PyTorch line with matching `data.pyg.org` wheels installed by `qmcpy-install-mpmc`. -- Keep older Python jobs in `unittests.yml` for core QMCPy coverage, but do not require them to run MPMC. +- `unittests.yml` runs the full suite on `3.10`-`3.14` plus a slim `core-tests` tier on `3.9` (see [Minimum Python Version by Role](CONTRIBUTING.md#minimum-python-version-by-role)); neither installs MPMC. ## Support Policy @@ -17,7 +17,8 @@ | `3.13` | Target | Supported | `torch >= 2.10`, `torch-geometric >= 2.6.1`, `pyg_lib >= 0.6.0` | Run MPMC doctests and unit tests | | `3.12` | Target | Supported | `torch >= 2.10`, `torch-geometric >= 2.6.1`, `pyg_lib >= 0.6.0` | Run MPMC doctests and unit tests | | `3.10` to `3.11` | Best effort | Not a release blocker for MPMC | May work with matching PyTorch / PyG wheels, but not required by current CI policy | Optional manual testing only | -| `3.5` to `3.9` | Legacy core-package coverage only | Not supported for MPMC | Do not spend CI budget trying to keep MPMC running here | No MPMC doctests or unit tests | + +Python `3.9` is covered only by the slim `core-tests` tier, which never installs MPMC's PyTorch Geometric stack (see [Minimum Python Version by Role](CONTRIBUTING.md#minimum-python-version-by-role)). The distinction is intentional: @@ -26,27 +27,25 @@ The distinction is intentional: ## CI Policy -The current CI split should be: +The current CI split is: + +- `alltests.yml`: the only workflow that installs the MPMC stack (`qmcpy-install-mpmc`) and runs `make doctests_mpmc` plus the MPMC unit tests, on Python `3.13`. The steps are not OS-gated: Ubuntu alone on feature-branch pushes, all three OSes on full sweeps. +- `unittests.yml`: `3.10`-`3.14` on all three OSes, plus a `core-tests` tier on Ubuntu for `3.9`. Neither calls `qmcpy-install-mpmc`, so `test/test_dd_mpmc.py` skips throughout via `pytest.importorskip("pyg_lib")`. This workflow gives **no** MPMC coverage. -- `alltests.yml`: full-sweep validation on Linux, macOS, and Windows for Python `3.13`, including `make doctests_mpmc` and the standard unit-test suite. -- `unittests.yml`: a broader version sampler for the repository, with explicit MPMC jobs on Python `3.12`, `3.13`, and `3.14`. -- Older `unittests.yml` jobs: keep them for core QMCPy regressions, but do not require MPMC there. +See [MPMC Coverage by OS](ci-testing.md#mpmc-coverage-by-os) for the per-operating-system breakdown. -This gives one place to enforce modern MPMC compatibility without forcing the entire repository to abandon older Python jobs immediately. +This keeps MPMC enforcement in one place. The trade-off: MPMC regressions are invisible to `unittests.yml`, so raising MPMC coverage means adding a job to `alltests.yml`, not widening the `unittests.yml` matrix. ## Local Developer Commands -Install the usual test and MPMC extras first, then add the platform-specific -PyG runtime with QMCPy's installed helper command: +Install the usual test and MPMC extras first, then add the platform-specific PyG runtime with QMCPy's installed helper command: ```bash python -m pip install -e ".[test,test_torch,test_gpytorch,test_botorch,mpmc]" qmcpy-install-mpmc ``` -The `mpmc` extra contains dependencies available from PyPI. The helper handles -`pyg_lib` separately because its wheel page depends on the installed PyTorch -version and accelerator build, which standard project metadata cannot select. +The `mpmc` extra contains dependencies available from PyPI. The helper handles `pyg_lib` separately because its wheel page depends on the installed PyTorch version and accelerator build, which standard project metadata cannot select. Then run the MPMC-specific checks: diff --git a/makefile b/makefile index 57e4f66e6..c81bea6df 100644 --- a/makefile +++ b/makefile @@ -122,6 +122,21 @@ unittests: ensure_artifacts --no-header \ test/ -W ignore::DeprecationWarning +# Core unit tests only: skips test/booktests/ (needs the notebook stack); other +# modules self-skip via pytest.importorskip. Pairs with the `test_core` extra so +# interpreters at the `requires-python` floor can run this. Unlike `unittests` +# this omits -x: on a compatibility run the full list of failures is the point. +unittests_core: ensure_artifacts + @mkdir -p $(UNIT_COV_DIR) + COVERAGE_FILE=$(UNIT_COV_DIR)/.coverage \ + python -m pytest $(PYTEST_XDIST) $(PYTEST_EXTRA_ARGS) \ + --cov=qmcpy \ + --cov-report term \ + --cov-report json:$(UNIT_COV_DIR)/coverage.json \ + --no-header -rs \ + --ignore=test/booktests \ + test/ -W ignore::DeprecationWarning + tests_no_docker_no_mpmc: doctests_no_docker_no_mpmc unittests coverage ########################################################## diff --git a/pyproject.toml b/pyproject.toml index dac48d235..570ba971d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,6 +27,14 @@ classifiers= [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Science/Research", "Programming Language :: Python :: 3", + # Keep these aligned with `requires-python` and the tested interpreter + # matrix. See CONTRIBUTING.md. + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Topic :: Scientific/Engineering :: Mathematics", ] readme = "README.md" @@ -45,7 +53,9 @@ keywords=[ ] license = {file = "LICENSE"} dynamic = ["version"] -requires-python = ">= 3.5" +# Deliberate QMCPy support-policy floor, not a source-language or transitive +# dependency floor. CI verifies the built wheel on 3.9; see CONTRIBUTING.md. +requires-python = ">= 3.9" dependencies = [ "numpy >= 1.17.0", "scipy >= 1.1.0", @@ -92,6 +102,18 @@ test = [ "nbconvert >= 7.2.9", "pytest-xdist >= 3.8.0", ] +# Minimal set for `make unittests_core` (test/test_*.py only). Omits the +# notebook/booktest stack (parsl, testbook, ...) whose 3.10 floor would +# otherwise force every unit-test job onto 3.10+, so CI can exercise the +# published `requires-python` floor. See CONTRIBUTING.md. +test_core = [ + "pytest >= 7.0", + "pytest-cov >= 4.0", + "pytest-xdist >= 3.0", + "scikit-learn >= 1.0.0", + "pandas >= 1.3.0", + "pyyaml >= 6.0", + ] test_torch = [ "torch >= 2.7.0, < 2.13", # kept in sync with the mpmc extra: PyG pyg_lib wheels stop at torch 2.12 ] diff --git a/qmcpy/discrete_distribution/digital_net_b2/digital_net_b2.py b/qmcpy/discrete_distribution/digital_net_b2/digital_net_b2.py index 0a463507f..89887d578 100644 --- a/qmcpy/discrete_distribution/digital_net_b2/digital_net_b2.py +++ b/qmcpy/discrete_distribution/digital_net_b2/digital_net_b2.py @@ -323,6 +323,8 @@ def __init__( repos = DataSource() if repos.exists(local_root + generating_matrices): datafile = repos.open(local_root + generating_matrices) + elif repos.exists(generating_matrices): + datafile = repos.open(generating_matrices) elif repos.exists( "https://raw.githubusercontent.com/QMCSoftware/LDData/refs/heads/main/dnet/" + generating_matrices @@ -354,8 +356,6 @@ def __init__( "https://raw.githubusercontent.com/QMCSoftware/" + generating_matrices ) - elif repos.exists(generating_matrices): - datafile = repos.open(generating_matrices) else: raise ParameterError("LDData path %s not found" % generating_matrices) contents = [line.rstrip("\n").strip() for line in datafile.readlines()] diff --git a/test/test_accumulate_data.py b/test/test_accumulate_data.py index 8de9d3ebd..dc17f4f13 100644 --- a/test/test_accumulate_data.py +++ b/test/test_accumulate_data.py @@ -2,8 +2,15 @@ from unittest.mock import patch import numpy as np +import pytest from qmcpy import CubBayesNetG, DigitalNetB2, Keister + +# `pf_gp_ci` imports torch and gpytorch at module level, so skip rather than fail +# collection where those optional stacks are absent (as test_dd_mpmc.py does). +pytest.importorskip("torch") +pytest.importorskip("gpytorch") + from qmcpy.stopping_criterion.pf_gp_ci import PFGPCIData