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` |