Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 0 additions & 8 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,3 @@
*.mha filter=lfs diff=lfs merge=lfs -text
docs/assets/*.gif filter=lfs diff=lfs merge=lfs -text
docs/assets/*.png filter=lfs diff=lfs merge=lfs -text
tutorials/network_weights/physicsnemo_mgn_heart/*.pt filter=lfs diff=lfs merge=lfs -text
tutorials/network_weights/physicsnemo_mgn_heart/*.vtu filter=lfs diff=lfs merge=lfs -text
tutorials/network_weights/physicsnemo_mgn_heart/*.vtp filter=lfs diff=lfs merge=lfs -text
tutorials/network_weights/physicsnemo_mgn_heart/pca_model.json filter=lfs diff=lfs merge=lfs -text
tutorials/network_weights/physicsnemo_mgn_lung_motion/*.pt filter=lfs diff=lfs merge=lfs -text
tutorials/network_weights/physicsnemo_mgn_lung_motion/*.vtu filter=lfs diff=lfs merge=lfs -text
tutorials/network_weights/physicsnemo_mgn_lung_motion/*.vtp filter=lfs diff=lfs merge=lfs -text
tutorials/network_weights/physicsnemo_mgn_lung_motion/pca_model.json filter=lfs diff=lfs merge=lfs -text
17 changes: 8 additions & 9 deletions .github/scripts/build_dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,17 @@
--timestamp "2026-03-31T07:05:42Z" \\
--health-outcome "success"

Artifact publishing:
``status.json`` is uploaded by ``nightly-health.yml`` as a standalone
artifact named ``nightly-status-json`` (90-day retention). ``docs.yml``
downloads that artifact during its ``deploy`` job and copies
``status.json`` into the Pages output directory so that the file is
served at the live URL:
Status publishing:
``nightly-health.yml`` uploads the whole output directory as a
``health-dashboard`` artifact, then force-pushes ``status.json`` alone to
the orphan ``nightly-status`` branch. ``docs.yml`` fetches it from that
branch through the GitHub contents API during its ``deploy`` job and copies
it into the Pages output, so the file is served at the live URL:

https://<pages-root>/status.json

The copy step uses ``continue-on-error: true`` so the first docs deploy
(before any nightly run has produced the artifact) succeeds without
``status.json`` being present.
The fetch tolerates a missing branch, so the first docs deploy (before any
nightly run has pushed ``status.json``) succeeds without it.
"""

from __future__ import annotations
Expand Down
22 changes: 20 additions & 2 deletions .github/workflows/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Runs on every push and pull request to main branches. Includes:
- **unit-tests**: Cross-platform unit tests
- Runs on Ubuntu and Windows
- Python 3.11 and 3.12
- Uses PyTorch CPU version to avoid GPU dependencies
- Installs `.[test]` only, so no CUDA toolchain is pulled in
- Excludes slow tests and tests requiring external data
- Generates coverage reports

Expand All @@ -35,7 +35,8 @@ Runs on every push and pull request to main branches. Includes:

### `test-slow.yml` - Long-Running Tests

Runs nightly at 2 AM UTC or on manual trigger. Includes:
Manual trigger only (`workflow_dispatch`); the nightly schedule lives in
`nightly-health.yml`. Includes:

- **test-slow-gpu**: Slow tests requiring GPU
- Tests marked with `slow` marker
Expand All @@ -61,6 +62,23 @@ Two-job workflow for building and deploying Sphinx documentation:

This separation ensures PRs can build and validate docs without triggering environment protection rules.

The `deploy` job also fetches `status.json` from the orphan `nightly-status`
branch (via the GitHub contents API) and copies it into the Pages output, so the
nightly-health badge in the top-level `README.md` resolves.

### `nightly-health.yml` - Nightly Full-Suite Health Check

Runs at 07:00 UTC daily, or on manual trigger with a `reason` input. On the
self-hosted Windows GPU runner it installs
`.[test,docs,cuda13,dev,physicsnemo]` and runs the entire suite with
`--run-all`, which enables every opt-in bucket. The run itself is
`continue-on-error`, so a failing test records a red status rather than
failing the workflow.

A second `build-dashboard` job turns the JUnit XML and coverage JSON into an
HTML dashboard via `.github/scripts/build_dashboard.py`, then force-pushes
`status.json` to the orphan `nightly-status` branch for `docs.yml` to pick up.

### `release.yml` - Build and Publish Distributions

Builds the wheel and source distribution, validates them with Twine, and
Expand Down
10 changes: 5 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ jobs:
- name: Run USD conversion tests
run: |
xvfb-run -a --server-args="-screen 0 1024x768x24" \
pytest tests/test_convert_vtk_to_usd_polymesh.py -v --cov=physiotwin4d --cov-append --cov-report=xml
pytest tests/test_convert_vtk_to_usd.py -v --cov=physiotwin4d --cov-append --cov-report=xml
Comment thread
aylward marked this conversation as resolved.
continue-on-error: true

- name: Run USD utility tests
Expand Down Expand Up @@ -408,21 +408,21 @@ jobs:
# The following tests are excluded from CI and should be run locally:
#
# Slow/GPU-intensive tests:
# - tests/test_register_images_ANTS.py (slow, computationally intensive)
# - tests/test_register_images_ICON.py (requires CUDA for ICON)
# - tests/test_register_images_ants.py (slow, computationally intensive)
# - tests/test_register_images_icon.py (requires CUDA for ICON)
# - tests/test_transform_tools.py (depends on slow registration tests)
# - tests/test_segment_chest_total_segmentator.py (requires CUDA for TotalSegmentator)
#
# Tutorial tests (SLOW - hours to complete):
# - tests/test_tutorials.py (runs every script in tutorials/ end-to-end)
# - tests/test_tutorials.py (runs all 29 tutorial scripts end-to-end)
# These tests are NEVER run in the PR CI and must be opted into
# They execute end-to-end workflows that may take multiple hours
#
# To run locally:
# pytest tests/ -v --run-slow # Run all slow tests
# pytest tests/ -v --run-gpu --run-slow # GPU + slow (typical local dev profile)
# pytest tests/ -v --run-simpleware --run-gpu --run-slow # Full Simpleware coverage
# pytest tests/test_register_images_ANTS.py -v --run-slow
# pytest tests/test_register_images_ants.py -v --run-slow
#
# Self-hosted GPU runner enables ALL buckets via --run-all
# (--run-gpu --run-slow --run-simpleware --run-physicsnemo --run-tutorials).
Expand Down
14 changes: 6 additions & 8 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,15 @@ pr*review_summary.md
docs/_build/
docs/_static/.buildinfo

# Tutorial videos (GIF versions are tracked in LFS instead)
# Tutorial videos (GIF versions are tracked in LFS instead). The MP4 sources
# are kept beside the GIFs in docs/assets/mp4/ and are never tracked.
docs/assets/*.mp4
docs/assets/mp4/

# Network weights
# Network weights. Weights are produced by running Tutorials 2 and 9, not
# distributed with the repository, so nothing under network_weights is tracked.
network_weights

# Track the MGN heart and lung model weights (other network_weights stay ignored)
!tutorials/network_weights/
tutorials/network_weights/*
!tutorials/network_weights/physicsnemo_mgn_heart/
!tutorials/network_weights/physicsnemo_mgn_lung_motion/
tutorials/network_weights/

# Data files
*.gz
Expand Down
21 changes: 9 additions & 12 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1,24 +1,21 @@
# Include essential documentation
# Include essential documentation. AGENTS.md is the cross-tool sibling of
# CLAUDE.md; both describe repository conventions and ship together.
include README.md
include LICENSE
include CLAUDE.md
include CHANGELOG.md
include AGENTS.md

# Include configuration files
# Include configuration files. The build is pure PEP 621, so there is no
# setup.py to ship.
include pyproject.toml
include setup.py

# Include example scripts (but not in the package itself)
recursive-include scripts *.py
recursive-include scripts *.md

# Include all source code
recursive-include src *.py

# Include network weights and models
recursive-include src/physiotwin4d/network_weights *

# Include data files
# Include data files. src/ currently holds no non-Python package data; these
# rules are kept so that any future bundled config ships automatically. Network
# weights are deliberately excluded - they are produced by running Tutorials 2
# and 9, not distributed with the package.
recursive-include src/physiotwin4d *.json
recursive-include src/physiotwin4d *.yaml
recursive-include src/physiotwin4d *.yml
Expand Down
30 changes: 25 additions & 5 deletions data/Chest-CT/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,19 +31,39 @@ data/Chest-CT/

## Overview

A routine, clinical, 3D chest CT scan. Unlike the gated 4D datasets in this
directory, it is a single static volume — one acquisition, no temporal
phases — so it stands in for the everyday clinical scan a patient-specific
model is fitted to.
A single-acquisition 3D chest CT from the AREN0534 pediatric Wilms tumor
trial. Unlike the gated 4D datasets in this directory, it is a single static
volume — one acquisition, no temporal phases — so it stands in for the ungated
clinical scan a patient-specific model is fitted to.

### Dataset Details

- **Format**: `.mha` (compressed MetaImage)
- **Dimensionality**: 3D, single time point
- **Size**: ~200 MB
- **Content**: Routine clinical chest CT
- **Content**: Ungated chest CT
- **Anatomy**: Lungs, heart, mediastinum, thoracic skeleton

### Acknowledgement

Data provided by The Cancer Imaging Archive (TCIA):
https://www.cancerimagingarchive.net/

Released under the NCTN Data Archive License; see the collection page linked
from the DOI below for the terms.

### Citation

Dataset: https://doi.org/10.7937/TCIA.5M9S-6Y97

If you use this dataset, please cite:

> Ehrlich, P., Chi, Y. Y., Chintagumpala, M. M., Hoffer, F. A., Perlman, E. J., Kalapurakal, J. A., Warwick, A., Shamberger, R. C., Khanna, G., Hamilton, T. E., Gow, K. W., Paulino, A. C., Gratias, E. J., Mullen, E. A., Geller, J. I., Grundy, P. E., Fernandez, C. V., Ritchey, M. L., & Dome, J. S. (2021). Combination Chemotherapy and Surgery in Treating Young Patients With Wilms Tumor (AREN0534) [Data set]. The Cancer Imaging Archive. DOI: [10.7937/TCIA.5M9S-6Y97](https://doi.org/10.7937/TCIA.5M9S-6Y97)

TCIA's data usage policy also asks that the archive itself be cited:

> Clark, K., Vendt, B., Smith, K., Freymann, J., Kirby, J., Koppel, P., Moore, S., Phillips, S., Maffitt, D., Pringle, M., Tarbox, L., & Prior, F. (2013). The Cancer Imaging Archive (TCIA): Maintaining and Operating a Public Information Repository. *Journal of Digital Imaging*, 26(6), 1045-1057. DOI: [10.1007/s10278-013-9622-7](https://doi.org/10.1007/s10278-013-9622-7)

## Using This Dataset

- Patient image for
Expand Down
17 changes: 11 additions & 6 deletions data/Duke-Heart-4DLabelmaps/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ Gated 4D cardiac labelmaps acquired at Duke University by Dr. Paul Segars.

## Availability

This dataset is **not currently available**. It is being considered for public
release; until that happens it cannot be downloaded, and it is not distributed
with this repository.
This dataset is **scheduled for public release soon**. It is not distributed
with this repository and has no automatic downloader yet.

In the meantime, contact Stephen Aylward (<saylward@nvidia.com>) to request
access.

## Effect on the tutorials

Expand All @@ -15,9 +17,12 @@ their organ field, for example:

- `tutorials/tutorial_02_duke_heart_distancemap_finetune_icon.py`

These `duke_heart` tutorials will not run without the data. Every other
tutorial uses a publicly available dataset and is unaffected — see
[../README.md](../README.md) for download instructions.
There are ten of them. Nine form their own chain: Tutorial 4 (duke heart) -> 5
-> 6 -> 7 -> 8 -> 9 -> 10 -> 11 -> 12. The tenth, Tutorial 2 (duke heart), is a
separate optional ICON finetuning variant that the chain does not require.
They will not run until the data is
available. The other 19 tutorial scripts use publicly available datasets and are
unaffected — see [../README.md](../README.md) for download instructions.

Downstream tutorials that consume `duke_heart` outputs (such as the finetuned
distance-map ICON weights used by
Expand Down
2 changes: 1 addition & 1 deletion data/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ the source of truth.
| `DirLab-4DCT/` | 4D lung CT respiratory motion benchmark | DIR-Lab, MD Anderson Cancer Center / Emory University | Manual | [DirLab-4DCT/README.md](DirLab-4DCT/README.md) |
| `KCL-Heart-Model/` | Statistical shape model of the heart | King's College London (KCL) | Automatic | [KCL-Heart-Model/README.md](KCL-Heart-Model/README.md) |
| `CHOP-Valve4D/` | 4D valve reconstruction models | Jolley Lab, CHOP (original FEBio model) | Automatic | [CHOP-Valve4D/README.md](CHOP-Valve4D/README.md) |
| `Chest-CT/` | Routine clinical 3D chest CT scan | PhysioTwin4D GitHub release | Automatic | [Chest-CT/README.md](Chest-CT/README.md) |
| `Chest-CT/` | Ungated 3D chest CT, single static volume | AREN0534 trial, The Cancer Imaging Archive (TCIA) | Automatic | [Chest-CT/README.md](Chest-CT/README.md) |
| `test/` | pytest-managed cache; not a downloadable dataset | — | N/A | [test/README.md](test/README.md) |

## Automatic Download
Expand Down
2 changes: 1 addition & 1 deletion data/Slicer-Heart-CT/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ assert DataDownloadTools.VerifySlicerHeartCTData("data/Slicer-Heart-CT")
```

This fetches a single ~1.2 GB file from
[github.com/SlicerHeart/SlicerHeart](https://github.com/SlicerHeart/SlicerHeart/releases/download/TestingData/TruncalValve_4DCT.seq.nrrd),
[the PhysioTwin4D release assets](https://github.com/Project-MONAI/physiotwin4d/releases/download/2026.07.1/TruncalValve_4DCT.seq.nrrd),
then splits it into per-phase 3D `slice_???.mha` volumes in the same
directory via `ConvertImage4DTo3D`. An existing non-empty `.seq.nrrd` is
reused, and the split is skipped once the `slice_???.mha` files are present —
Expand Down
7 changes: 7 additions & 0 deletions docs/api/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -43,16 +43,23 @@ By Category
* :class:`~physiotwin4d.WorkflowTrainPhysicsNeMo` - Train a mesh-stage model
* :class:`~physiotwin4d.WorkflowInferPhysicsNeMo` - Predict per-point targets
* :class:`~physiotwin4d.WorkflowInferMovement` - Turn predictions back into geometry
* :class:`~physiotwin4d.WorkflowEvaluateMovement` - Score predictions against the acquired frames

**Segmentation**
* :class:`~physiotwin4d.SegmentAnatomyBase` - Base segmentation class
* :class:`~physiotwin4d.SegmentChestTotalSegmentator` - TotalSegmentator
* :class:`~physiotwin4d.SegmentChestTotalSegmentatorWithContrast` - TotalSegmentator for contrast-enhanced CT
* :class:`~physiotwin4d.SegmentHeartSimpleware` - Simpleware cardiac segmentation
* :class:`~physiotwin4d.SegmentHeartSimplewareTrimmedBranches` - Simpleware with trimmed great vessels
* :class:`~physiotwin4d.SegmentNVSegmentCTMRI` - NV-Segment-CTMR, CT *and* MRI

**Image Registration**
* :class:`~physiotwin4d.RegisterImagesBase` - Base registration class
* :class:`~physiotwin4d.RegisterImagesANTS` - ANTs registration
* :class:`~physiotwin4d.RegisterImagesGreedy` - Greedy classical deformable registration
* :class:`~physiotwin4d.RegisterImagesICON` - Icon deep learning registration
* :class:`~physiotwin4d.RegisterImagesChain` - Run registrations back to back
* :class:`~physiotwin4d.RegisterImagesGreedyICON` - Greedy then ICON, as a preset chain
* :class:`~physiotwin4d.RegisterTimeSeriesImages` - 4D time series registration

**Model Registration**
Expand Down
73 changes: 73 additions & 0 deletions docs/api/physicsnemo/evaluate.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
==========================================
Scoring a Mesh-Stage Model Against Images
==========================================

.. module:: physiotwin4d.workflow_evaluate_movement
.. currentmodule:: physiotwin4d

A mm error against the surfaces a registration produced says how well the
network reproduces that registration. :class:`WorkflowEvaluateMovement` asks the
other question: how close are the size and shape of the inferred anatomy to the
anatomy that was actually imaged, structure by structure. See Tutorial 11 in
:doc:`../../tutorials`.

For every gated time point it carries the reference frame's labelmap into that
time point with the network's own deformation, and compares the result to the
labelmap of the frame that was acquired: volume difference, Dice and surface
RMSE per lung lobe or per heart chamber.

Per-structure scoring
=====================

.. autoclass:: WorkflowEvaluateMovement
:members:
:undoc-members:
:show-inheritance:

Example
=======

.. code-block:: python

from physiotwin4d import (
WorkflowEvaluateMovement,
WorkflowInferMovement,
WorkflowInferPhysicsNeMo,
)

evaluate = WorkflowEvaluateMovement(
movement_workflow=WorkflowInferMovement(
WorkflowInferPhysicsNeMo(model_directory=model_dir)
),
label_names={28: "lung_upper_lobe_left", 29: "lung_lower_lobe_left"},
)
result = evaluate.process(
case_id="Case1Pack",
shape_parameters=pca_coefficients_file,
reference_mesh=ssm_surface_file,
reference_labelmap=reference_labelmap,
ground_truth_labelmaps={0.0: frame_00, 0.1: frame_10},
output_directory=out_dir,
)
print(result["report_file"], result["csv_file"])

Notes
=====

**Why labelmaps rather than the model's surface.** The lung shape model carries
its five lobes as per-cell labels, but the heart model is a single structure ---
the whole heart minus its chamber cavities --- so its chambers exist only in the
acquired labelmaps. Warping those labelmaps scores every structure the
acquisition contains, whether or not the shape model represents it separately.

**The evaluation grid.** Everything is measured on one isotropic grid built
around the reference anatomy, so a case whose gated frames carry different slice
pitches is still scored on a single, stated voxel volume. Its pitch sets both
that voxel volume and the memory the per-stage deformation fields take, which
grows with its cube.

See Also
========

* :doc:`infer`
* :doc:`train`
8 changes: 6 additions & 2 deletions docs/api/physicsnemo/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ PhysioTwin4D trains and runs PhysicsNeMo mesh-stage models: given a subject's
shape parameters and a stage (a point in the cardiac or respiratory cycle),
predict a per-vertex target on the shared template mesh. When that target is a
displacement, the prediction replaces a per-phase registration solve with one
forward pass — see Tutorials 9 and 10 in :doc:`../../tutorials`.
forward pass — see Tutorials 9 through 13 in :doc:`../../tutorials`.

The layer follows the same has-a shape as the rest of the workflow tier: a
workflow owns the data and the artifacts, and a *method* object owns the
Expand All @@ -24,7 +24,10 @@ network.
- Loads a trained model and predicts raw per-point targets
* - :class:`~physiotwin4d.WorkflowInferMovement`
- Interprets 3-component targets as displacements: deformed meshes, mm
error statistics, rasterized deformation fields
error statistics, rasterized deformation fields, warped images and USD
* - :class:`~physiotwin4d.WorkflowEvaluateMovement`
- Scores those predictions per structure against the acquired frames:
volume difference, Dice and surface RMSE
* - :class:`~physiotwin4d.TrainPhysicsNeMoMGN` /
:class:`~physiotwin4d.TrainPhysicsNeMoMLP`
- The networks to train: MeshGraphNet or fully connected
Expand All @@ -46,3 +49,4 @@ imports happen lazily inside the methods that need them.
manifest
train
infer
evaluate
Loading
Loading