Skip to content

Commit 5176700

Browse files
authored
ENH: Allow data and output directories of tutorials to be specified (#125)
* ENH: Allow data and output directories of tutorials to be specified These directories now consume tens of gigabytes. These changes allow the source and data directories to be separate. Also allows testing to wipe the source directory without having to re-download data. * STYLE: Fix line lengths etc
1 parent 73d3fd9 commit 5176700

52 files changed

Lines changed: 1523 additions & 430 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitattributes

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@
22
*.mha filter=lfs diff=lfs merge=lfs -text
33
docs/assets/*.gif filter=lfs diff=lfs merge=lfs -text
44
docs/assets/*.png filter=lfs diff=lfs merge=lfs -text
5+
tests/baselines/**/*.png filter=lfs diff=lfs merge=lfs -text

.github/workflows/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ GPU tests require self-hosted runners with:
163163
**Option 3: Run Locally**
164164
```bash
165165
# Install with CUDA + PhysicsNeMo (matches the self-hosted GPU runner).
166-
# Requires Python >= 3.11 (nvidia-physicsnemo does not support 3.10).
166+
# Requires Python 3.11-3.13 (the range nvidia-physicsnemo supports).
167167
uv pip install -e ".[test,cuda13,physicsnemo]"
168168

169169
# Run GPU tests
@@ -237,7 +237,7 @@ pytest tests/ -m "unit and not requires_gpu" --cov=physiotwin4d
237237
### GPU Tests
238238
```bash
239239
# Install with CUDA + PhysicsNeMo (matches the self-hosted GPU runner).
240-
# Requires Python >= 3.11 (nvidia-physicsnemo does not support 3.10).
240+
# Requires Python 3.11-3.13 (the range nvidia-physicsnemo supports).
241241
uv pip install -e ".[test,cuda13,physicsnemo]"
242242

243243
# Run GPU tests

.github/workflows/ci.yml

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ jobs:
3535
fail-fast: false
3636
matrix:
3737
os: [ubuntu-latest, windows-latest]
38-
python-version: ['3.11', '3.12']
38+
python-version: ['3.11', '3.12', '3.13']
3939

4040
steps:
4141
- name: Checkout code
@@ -272,7 +272,6 @@ jobs:
272272
timeout-minutes: 30
273273
# Only run GPU tests on manual trigger or if 'run-gpu-tests' label is present
274274
if: github.event_name == 'workflow_dispatch' || contains(github.event.pull_request.labels.*.name, 'run-gpu-tests')
275-
continue-on-error: true
276275

277276
steps:
278277
- name: Checkout code
@@ -281,15 +280,31 @@ jobs:
281280
lfs: true
282281

283282
- name: Create venv in RUNNER_TEMP
284-
# Python 3.11 is required because the [physicsnemo] extra pulls in
285-
# nvidia-physicsnemo, which requires Python >= 3.11.
283+
# The oldest interpreter the project supports, so this job is the one
284+
# that catches a regression at the floor. nightly-health.yml runs the
285+
# newest (3.13) for the opposite reason. Both are on the runner.
286286
run: |
287287
& "C:\Program Files\Python311\python.exe" -m venv "$env:RUNNER_TEMP\physiotwin4d-venv"
288288
echo "$env:RUNNER_TEMP\physiotwin4d-venv\Scripts" >> $env:GITHUB_PATH
289289
290290
- name: Check GPU availability
291291
run: nvidia-smi
292292

293+
- name: Check nvcc availability
294+
# torch-scatter compiles CUDA kernels from source when no matching
295+
# pre-built wheel exists. Without nvcc on PATH the build silently falls
296+
# back to a CPU-only extension that only fails later, at test time, with
297+
# an opaque scatter error. Fail here instead.
298+
run: |
299+
$nvcc = Get-Command nvcc -ErrorAction SilentlyContinue
300+
if (-not $nvcc) {
301+
Write-Error "nvcc not found on PATH; torch-scatter would build CPU-only"
302+
exit 1
303+
}
304+
Write-Output "nvcc found at $($nvcc.Source)"
305+
nvcc --version
306+
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
307+
293308
- name: Cache uv packages
294309
uses: actions/cache@v4
295310
with:
@@ -308,7 +323,9 @@ jobs:
308323
# requires_physicsnemo-marked tests can run on this GPU runner.
309324
# Invoke via python -m uv so uv targets the active venv interpreter.
310325
run: |
311-
python -m uv pip install -e ".[test,cuda13,physicsnemo]"
326+
python -m uv pip install -e ".[cuda13]"
327+
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
328+
python -m uv pip install -e ".[test,cuda13,physicsnemo]" --no-build-isolation-package torch-scatter
312329
313330
- name: Assert CUDA is accessible
314331
run: |
@@ -334,14 +351,26 @@ jobs:
334351
pip list
335352
336353
- name: Run GPU tests
337-
# External self-hosted GPU runner: enable every opt-in bucket via --run-all.
338-
# Tests whose host requirements (e.g. a licensed Simpleware install)
339-
# aren't met on the runner will runtime-skip cleanly via their
340-
# internal availability guards.
354+
# A pre-merge gate, so it runs the GPU buckets only and finishes inside
355+
# timeout-minutes. The exhaustive --run-all sweep, tutorials included,
356+
# belongs to nightly-health.yml: running it here too would duplicate six
357+
# hours of work on this same runner and could never fit the timeout.
358+
#
359+
# --require-tutorial-data turns missing data into a failure rather than a
360+
# skip, so this job cannot report green having run nothing.
341361
run: |
342-
pytest tests/ -v --run-all --cov=physiotwin4d --cov-report=xml --cov-report=term --cov-report=html
362+
pytest tests/ -v --run-gpu --run-physicsnemo --require-tutorial-data --max-test-seconds=600 --timeout=1800 --cov=physiotwin4d --cov-report=xml --cov-report=term --cov-report=html
343363
env:
344364
CUDA_VISIBLE_DEVICES: 0
365+
# The datasets, the results and the trained networks live outside the
366+
# checkout, which actions/checkout wipes on every run. Each root has a
367+
# "test" subtree that this suite reads and writes, so a CI run never
368+
# touches a full run's files. Unset, each falls back to its in-repo
369+
# default; see data/README.md. These are the same paths
370+
# nightly-health.yml uses, because both jobs run on this same runner.
371+
PHYSIOTWIN_INPUT_DATA_DIR: D:\PhysioTwin4D\nightly-runner\data
372+
PHYSIOTWIN_OUTPUT_DATA_DIR: D:\PhysioTwin4D\nightly-runner\output
373+
PHYSIOTWIN_WEIGHTS_DIR: D:\PhysioTwin4D\nightly-runner\network_weights
345374

346375
- name: Upload coverage to Codecov
347376
uses: codecov/codecov-action@v4

.github/workflows/nightly-health.yml

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ jobs:
4242

4343
outputs:
4444
# Captures the pytest step's actual outcome (success / failure / skipped)
45-
# even though the job itself is not failed by a test failure.
45+
# for the dashboard. The job itself is failed by the gate step at the end,
46+
# after the artifacts have been uploaded.
4647
test-outcome: ${{ steps.run-tests.outcome }}
4748

4849
steps:
@@ -70,8 +71,9 @@ jobs:
7071
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
7172
7273
- name: Create venv in RUNNER_TEMP
73-
# Python 3.13 is required because the [physicsnemo] extra pulls in
74-
# nvidia-physicsnemo, which requires Python >= 3.11.
74+
# The newest interpreter the project supports, so this job is the one
75+
# that catches a dependency shipping no wheel for it. ci.yml runs the
76+
# floor (3.11) for the opposite reason. Both are on the runner.
7577
run: |
7678
& "C:\actions-runner\python\WPy64-3.13.12.0\python\python.exe" -m venv "$env:RUNNER_TEMP\physiotwin4d-venv"
7779
echo "$env:RUNNER_TEMP\physiotwin4d-venv\Scripts" >> $env:GITHUB_PATH
@@ -157,14 +159,33 @@ jobs:
157159
# continue-on-error keeps the job running so artifacts are always uploaded.
158160
# The step outcome (success/failure) is still captured and passed downstream.
159161
continue-on-error: true
162+
# --max-test-seconds fails a test that finishes but took too long, so the
163+
# overrun is reported with its duration and the rest of the suite still
164+
# runs. --timeout raises the pytest-timeout backstop above it: on
165+
# Windows that backstop can only kill the whole process, which would
166+
# take the JUnit XML and every other result with it, so it is left to
167+
# catch a genuine hang and nothing else. Both numbers are provisional
168+
# until the per-tutorial timings are measured.
160169
run: |
161-
pytest tests/ -v --run-all `
170+
pytest tests/ -v --run-all --require-tutorial-data `
171+
--max-test-seconds=900 `
172+
--timeout=3600 `
162173
--cov=physiotwin4d `
163174
--cov-report=xml `
164175
--cov-report=json `
165176
--junitxml=test-results.xml
166177
env:
167178
CUDA_VISIBLE_DEVICES: 0
179+
# The datasets, the results and the trained networks live on the
180+
# runner's own disk rather than in the checkout, which
181+
# actions/checkout wipes every run. Each root has a "test" subtree
182+
# that this suite reads and writes, so a nightly run never touches a
183+
# full run's files, and the downsampled subsets the fixtures build
184+
# under <input>/test survive between runs instead of being rebuilt.
185+
# Unset, each falls back to its in-repo default; see data/README.md.
186+
PHYSIOTWIN_INPUT_DATA_DIR: D:\PhysioTwin4D\nightly-runner\data
187+
PHYSIOTWIN_OUTPUT_DATA_DIR: D:\PhysioTwin4D\nightly-runner\output
188+
PHYSIOTWIN_WEIGHTS_DIR: D:\PhysioTwin4D\nightly-runner\network_weights
168189

169190
- name: Upload test results
170191
uses: actions/upload-artifact@v6
@@ -177,6 +198,18 @@ jobs:
177198
coverage.json
178199
retention-days: 90
179200

201+
- name: Fail the job if the test suite did not pass
202+
# The pytest step is continue-on-error so that the artifacts and the
203+
# dashboard are produced whatever happened. Without this gate the job
204+
# would report success even when tests failed or the step timed out,
205+
# and the workflow status badge would say green while nothing passed.
206+
# build-dashboard runs on if: always(), so it still gets its inputs.
207+
if: steps.run-tests.outcome != 'success'
208+
run: |
209+
Write-Output "Health test suite outcome: ${{ steps.run-tests.outcome }}"
210+
Write-Output "See the health-test-results artifact for the JUnit XML."
211+
exit 1
212+
180213
# ──────────────────────────────────────────────────────────────────────────
181214
# 2. Build the HTML dashboard from test results (runs even if tests failed)
182215
# ──────────────────────────────────────────────────────────────────────────

data/README.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,39 @@ downloader — DIR-Lab distributes each case individually and may require
2626
registration, so it must be obtained manually; see
2727
[DirLab-4DCT/README.md](DirLab-4DCT/README.md).
2828

29+
## Keeping the Data Outside the Clone
30+
31+
The tutorials resolve three roots through `ParametersBase` in
32+
[`tutorials/parameters_base.py`](../tutorials/parameters_base.py), each
33+
defaulting to its location in the clone and each overridable by environment
34+
variable. Point them elsewhere when the data should outlive the checkout — on a
35+
CI runner, for instance, where every run starts from a fresh working tree:
36+
37+
| Variable | Default | Holds |
38+
| --- | --- | --- |
39+
| `PHYSIOTWIN_INPUT_DATA_DIR` | `<repo>/data` | The datasets in this directory |
40+
| `PHYSIOTWIN_OUTPUT_DATA_DIR` | `<repo>/tutorials/output` | What the tutorials write |
41+
| `PHYSIOTWIN_WEIGHTS_DIR` | `<repo>/tutorials/network_weights` | Networks the tutorials train |
42+
43+
Each root has a `test` subdirectory used when a tutorial runs under
44+
`PHYSIOTWIN_RUNNING_AS_TEST`, so a test run reads the small downsampled subsets
45+
and writes beside them rather than touching a full run's datasets, results, or
46+
checkpoints. The subsets under `<input root>/test` are built on demand by the
47+
fixtures in `tests/conftest.py`; putting that root outside the clone means they
48+
survive a checkout and are built once rather than every run.
49+
50+
The layout under an overridden input root is the same as here:
51+
52+
```text
53+
<PHYSIOTWIN_INPUT_DATA_DIR>/
54+
DirLab-4DCT/ Case1Pack_T00.mha, ...
55+
Duke-Heart-4DLabelmaps/ pm0027/*_labelmap.nii.gz, *_landmark.mrk.json
56+
Chest-CT/ Chest-CT.mha
57+
KCL-Heart-Model/ average_mesh.vtk, input_meshes/
58+
Slicer-Heart-CT/ slice_000.mha, ...
59+
test/ built by the pytest fixtures
60+
```
61+
2962
## Notes
3063

3164
- Always cite the original data source in publications — see each dataset's

data/test/.gitignore

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
1-
slicer_heart
2-
slicer_heart_small
1+
# Everything here is a pytest-built cache; only this file and the README are
2+
# tracked. See README.md for which fixture builds which directory.
3+
*
4+
!.gitignore
5+
!README.md

data/test/README.md

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,14 @@ This directory is **automatically managed by the pytest infrastructure**
44
(`tests/conftest.py`) — it is a cache, not a dataset you download or
55
maintain by hand. It holds data used to run the unit test suite.
66

7-
This data is **not** used by the workflows, tutorials, or CLIs of the
8-
PhysioTwin4D library; those consume the datasets documented in
9-
[`data/README.md`](../README.md) instead.
7+
It is the `test` subdirectory of the input data root, so setting
8+
`PHYSIOTWIN_INPUT_DATA_DIR` moves it too; see
9+
[`data/README.md`](../README.md#keeping-the-data-outside-the-clone). The paths
10+
below are written relative to that root, which defaults to `data/`.
11+
12+
The workflows, tutorials, and CLIs of the PhysioTwin4D library consume the
13+
full datasets documented in [`data/README.md`](../README.md). They read the
14+
subsets here only when run as tests, under `PHYSIOTWIN_RUNNING_AS_TEST`.
1015

1116
## What Lives Here
1217

@@ -16,10 +21,30 @@ PhysioTwin4D library; those consume the datasets documented in
1621
- `slicer_heart_small/` — the same phases downsampled to 1.5x1.5x1.5 mm,
1722
used by tests that need a smaller/faster image (labelmaps and
1823
transforms computed from this data are cached here too).
19-
20-
Both subdirectories are created on demand by `tests/conftest.py` fixtures
21-
the first time a test needs them, and are `.gitignore`d — do not commit
22-
their contents.
24+
- `KCL-Heart-Model/` — downloaded by the `download_kcl_heart_model` fixture.
25+
- `DirLab-4DCT/` — a few cases from `<input root>/DirLab-4DCT`, downsampled to
26+
3 mm by the `dirlab_test_data` fixture.
27+
- `Duke-Heart-4DLabelmaps/` — a few cases from
28+
`<input root>/Duke-Heart-4DLabelmaps`, their labelmaps downsampled to 2 mm
29+
nearest-neighbour by the `duke_heart_test_data` fixture.
30+
- `Chest-CT/``<input root>/Chest-CT` downsampled to 3 mm by the
31+
`chest_ct_test_data` fixture.
32+
33+
Here `<input root>` is whatever `PHYSIOTWIN_INPUT_DATA_DIR` names, defaulting to
34+
the `data/` directory of the clone — so each subset is built from the full
35+
dataset alongside it, wherever that root has been pointed.
36+
37+
Every subdirectory is created on demand by `tests/conftest.py` fixtures
38+
the first time a test needs them, and is `.gitignore`d — do not commit
39+
their contents. The subsets derived from another dataset are only built
40+
when that source dataset is present under the input root; otherwise the tests
41+
that need them skip, or fail if `--require-tutorial-data` was passed.
42+
43+
The tutorials read these directories rather than the full datasets whenever
44+
`PHYSIOTWIN_RUNNING_AS_TEST` is set, and write to the matching `test` subtree of
45+
the output and weights roots — `tutorials/output/test/` and
46+
`tutorials/network_weights/test/` by default. A test run therefore never reads
47+
or overwrites the datasets, results, or trained checkpoints of a full run.
2348

2449
## Regenerating
2550

docs/faq.rst

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,10 +68,12 @@ torchvision, and torchaudio are sourced from
6868
What Python version is required?
6969
---------------------------------
7070

71-
Python 3.10, 3.11 and 3.12 are supported.
71+
Python 3.11, 3.12 and 3.13 are supported.
7272

73-
The one exception is the optional ``[physicsnemo]`` extra: ``nvidia-physicsnemo``
74-
requires Python >= 3.11, so the AI-surrogate tutorials need 3.11 or 3.12.
73+
The floor is set by the optional ``[physicsnemo]`` extra: ``nvidia-physicsnemo``
74+
supports >= 3.11, < 3.14, and the AI-surrogate tutorials need it. The rest of
75+
the library would run on 3.10, but declaring 3.10 would promise an install
76+
that cannot resolve that extra.
7577

7678
Usage Questions
7779
===============

docs/installation.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ Prerequisites
1010
System Requirements
1111
-------------------
1212

13-
* **Python**: 3.10, 3.11, or 3.12
13+
* **Python**: 3.11, 3.12, or 3.13
1414
* **GPU**: NVIDIA GPU with CUDA 13 — required for full capability and best performance; a CPU-only PyPI installation is a supported fallback, but it is slow, emits a runtime warning, and cannot run the AI-surrogate workflows
1515
* **RAM**: 16GB minimum (32GB+ recommended for large datasets)
1616
* **Storage**: 10GB+ for package and model weights

0 commit comments

Comments
 (0)