diff --git a/.agents/agents/testing.md b/.agents/agents/testing.md index 365f07a..0fd5fcb 100644 --- a/.agents/agents/testing.md +++ b/.agents/agents/testing.md @@ -12,14 +12,18 @@ pytest tests that exercise the library's scientific pipelines. - `tests/conftest.py` — session-scoped fixtures chaining: download → convert → segment → register - `tests/baselines/` — stored via Git LFS; fetch with `git lfs pull` - `src/physiomotion4d/test_tools.py` — baseline comparison utilities -- Markers: `slow`, `requires_gpu`, `requires_data`, `experiment` +- Markers (all opt-in via `--run-`): `slow`, `requires_gpu`, + `requires_simpleware`, `experiment`, `tutorial`. Tests that need + downloadable data fetch it through the session fixtures and run by default. ## Run commands (use `py`, not `python`) ```bash -py -m pytest tests/ -m "not slow and not requires_data" -v # fast, recommended +py -m pytest tests/ -v # fast, recommended (slow/GPU/etc auto-skipped) py -m pytest tests/test_contour_tools.py -v # single file -py -m pytest tests/test_contour_tools.py::TestContourTools -v # single class +py -m pytest tests/test_contour_tools.py::TestContourTools -v # single class +py -m pytest tests/ -v --run-slow # opt into slow tests +py -m pytest tests/ -v --run-gpu --run-slow # typical local GPU profile (CI runner adds --run-simpleware --run-experiments --run-tutorials) py -m pytest tests/ --create-baselines # create missing baselines ``` @@ -28,7 +32,9 @@ py -m pytest tests/ --create-baselines # create missing b 1. Read the implementation file first; understand the public interface. 2. Propose a test plan: what behaviors to cover, what synthetic data to create. 3. Build synthetic `itk.Image` objects or small `pv.PolyData` surfaces — 32–64 voxels/side. - Never depend on real data unless unavoidable; mark those `@pytest.mark.requires_data`. + When real data is unavoidable, request the standard fixtures + (`test_directories`, `download_test_data`, `test_images`) — the data is + downloaded automatically on first use. 4. State image shape and axis order in the test docstring: e.g. `"""...image shape: (64, 64, 32), axes: X, Y, Z."""` 5. Use `test_tools.py` baseline utilities for surface and image regression checks. diff --git a/.agents/skills/test-feature/SKILL.md b/.agents/skills/test-feature/SKILL.md index c4090b3..ff061d6 100644 --- a/.agents/skills/test-feature/SKILL.md +++ b/.agents/skills/test-feature/SKILL.md @@ -1,5 +1,5 @@ --- -description: Inspect a PhysioMotion4D implementation and its existing tests, propose a synthetic-data test plan, then create or update pytest tests. Explains how to run them. +description: Inspect a PhysioMotion4D implementation and its existing tests, propose a real-data-driven test plan with baseline comparisons, then create or update pytest tests. Explains how to run them. --- Write or update tests for the following in PhysioMotion4D: @@ -9,10 +9,38 @@ $ARGUMENTS Instructions: 1. Read the implementation file(s) to understand the public interface. 2. Read the existing test file for this module if one exists (e.g. `tests/test_.py`). -3. Propose a test plan: list the behaviors to cover and the synthetic data to create. -4. Implement tests using synthetic `itk.Image` objects (32–64 voxels/side) or small - `pv.PolyData` surfaces — not real patient data. -5. State image shape and axis order in every test docstring. -6. Mark any test that genuinely requires real data with `@pytest.mark.requires_data`. -7. Show the exact command to run the new tests: - `py -m pytest tests/test_.py -v` +3. Propose a test plan: list the behaviors to cover and the inputs each behavior + needs. +4. **Strongly prefer real (downloaded) test data over synthetic data.** Request + the session fixtures (`test_directories`, `download_test_data`, + `test_images`) so the standard test datasets are pulled automatically on + first use. Real data exercises the production code paths — preprocessing, + resampling, dtype handling, world-frame metadata — that synthetic toy + volumes silently bypass. Only fall back to synthetic `itk.Image` or + `pv.PolyData` inputs when: + - the behavior under test is a pure unit (e.g. axis arithmetic, dict + routing) where real data adds no signal, or + - real data would push the test into a slow / GPU / Simpleware bucket + that doesn't fit the test's purpose. + When using synthetic inputs anyway, keep volumes ≤64 voxels per side and + say so in the docstring. +5. **When a test produces an image or surface as output, compare against a + baseline** using the `test_tools.py` utilities (e.g. `TestTools`) rather + than ad-hoc value assertions. Store baselines under `tests/baselines/` + (Git LFS-tracked). Run with `--create-baselines` to materialize missing + baselines on first use; afterward, regression compares to the stored + baseline. This catches drift that hand-written numeric thresholds miss. +6. State image shape and axis order in every test docstring (e.g. + `"""...image shape: (X, Y, Z, T) = (64, 64, 32, 1), LPS world frame."""`). +7. Mark tests that need a GPU, a slow runtime, or a licensed Simpleware + install with `@pytest.mark.requires_gpu`, `@pytest.mark.slow`, or + `@pytest.mark.requires_simpleware` so they fall into the right opt-in + bucket (`--run-gpu`, `--run-slow`, `--run-simpleware`). Tests that just + need downloadable data need **no** marker — the fixture chain handles it. +8. Show the exact command to run the new tests, including any opt-in flags + the markers require. Examples: + - `py -m pytest tests/test_.py -v` + - `py -m pytest tests/test_.py -v --run-slow` + - `py -m pytest tests/test_.py -v --run-gpu --run-slow` + - `py -m pytest tests/test_.py --create-baselines` (first run, to + materialize new baselines) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c721fe5..1c79fa3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,9 +8,13 @@ name: CI # - GPU tests: Self-hosted runners with CUDA support # - Code quality: Linting and formatting checks # -# Test markers: -# - requires_data: Tests that need external data downloads -# - slow: Tests that are computationally intensive or require GPU +# Test markers (gated by opt-in pytest flags; default = skip): +# - slow -> --run-slow +# - requires_gpu -> --run-gpu +# - requires_simpleware -> --run-simpleware (also implies GPU) +# - experiment -> --run-experiments +# - tutorial -> --run-tutorials +# Tests that need external data download it automatically via fixtures. on: push: @@ -101,16 +105,16 @@ jobs: run: | pip list - - name: Run unit tests (fast, no external data) - Ubuntu + - name: Run unit tests (fast, no GPU/slow/experiment) - Ubuntu if: matrix.os == 'ubuntu-latest' run: | xvfb-run -a --server-args="-screen 0 1024x768x24" \ - pytest tests/ -v -m "not slow and not requires_data and not experiment" --cov=physiomotion4d --cov-report=xml --cov-report=term --cov-report=html + pytest tests/ -v --cov=physiomotion4d --cov-report=xml --cov-report=term --cov-report=html - - name: Run unit tests (fast, no external data) - Windows + - name: Run unit tests (fast, no GPU/slow/experiment) - Windows if: matrix.os == 'windows-latest' run: | - pytest tests/ -v -m "not slow and not requires_data and not experiment" --cov=physiomotion4d --cov-report=xml --cov-report=term --cov-report=html + pytest tests/ -v --cov=physiomotion4d --cov-report=xml --cov-report=term --cov-report=html - name: Upload coverage to Codecov uses: codecov/codecov-action@v4 @@ -224,13 +228,13 @@ jobs: - name: Run contour tools tests run: | - pytest tests/test_contour_tools.py -v -m "not slow" --cov=physiomotion4d --cov-append --cov-report=xml + pytest tests/test_contour_tools.py -v --cov=physiomotion4d --cov-append --cov-report=xml continue-on-error: true - name: Run USD conversion tests run: | xvfb-run -a --server-args="-screen 0 1024x768x24" \ - pytest tests/test_convert_vtk_to_usd_polymesh.py -v -m "not slow" --cov=physiomotion4d --cov-append --cov-report=xml + pytest tests/test_convert_vtk_to_usd_polymesh.py -v --cov=physiomotion4d --cov-append --cov-report=xml continue-on-error: true - name: Run USD utility tests @@ -242,7 +246,7 @@ jobs: - name: Run all integration tests run: | xvfb-run -a --server-args="-screen 0 1024x768x24" \ - pytest tests/ -v -m "not slow and not experiment and not requires_gpu" + pytest tests/ -v continue-on-error: true - name: Upload coverage to Codecov @@ -325,8 +329,12 @@ jobs: pip list - name: Run GPU tests + # External self-hosted GPU runner: enable every opt-in bucket. + # Tests whose host requirements (e.g. a licensed Simpleware install) + # aren't met on the runner will runtime-skip cleanly via their + # internal availability guards. run: | - pytest tests/ -v -m "not slow and not experiment" --cov=physiomotion4d --cov-report=xml --cov-report=term --cov-report=html + pytest tests/ -v --run-gpu --run-slow --run-simpleware --run-experiments --run-tutorials --cov=physiomotion4d --cov-report=xml --cov-report=term --cov-report=html env: CUDA_VISIBLE_DEVICES: 0 @@ -404,11 +412,14 @@ jobs: # They execute end-to-end workflows that may take multiple hours # # To run locally: -# pytest tests/ -v -m "slow" # Run all slow tests -# pytest tests/test_register_images_ants.py -v -# pytest tests/test_register_images_icon.py -v -# pytest tests/test_segment_chest_total_segmentator.py -v +# 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 +# +# Self-hosted GPU runner enables ALL buckets: +# --run-gpu --run-slow --run-simpleware --run-experiments --run-tutorials # # To run experiment tests (manual only, extremely slow): -# pytest tests/test_experiments.py -v -m experiment -# pytest tests/test_experiments.py::test_experiment_heart_gated_ct_to_usd -v +# pytest tests/test_experiments.py -v --run-experiments +# pytest tests/test_experiments.py::test_experiment_heart_gated_ct_to_usd -v --run-experiments diff --git a/.github/workflows/nightly-health.yml b/.github/workflows/nightly-health.yml index 063b1df..f2fb3e2 100644 --- a/.github/workflows/nightly-health.yml +++ b/.github/workflows/nightly-health.yml @@ -108,7 +108,7 @@ jobs: # The step outcome (success/failure) is still captured and passed downstream. continue-on-error: true run: | - pytest tests/ -v --run-experiments ` + pytest tests/ -v --run-experiments --run-tutorials --run-slow --run-gpu --run-simpleware ` --cov=physiomotion4d ` --cov-report=xml ` --cov-report=json ` diff --git a/.github/workflows/test-slow.yml b/.github/workflows/test-slow.yml index 80ebcc7..7e1b2b4 100644 --- a/.github/workflows/test-slow.yml +++ b/.github/workflows/test-slow.yml @@ -72,8 +72,12 @@ jobs: " - name: Run slow tests + # Self-hosted GPU runner: enable every opt-in bucket. + # Tests whose host requirements (e.g. a licensed Simpleware install) + # aren't met on the runner will runtime-skip cleanly via their + # internal availability guards. run: | - pytest tests/ -v -m "slow and not experiment" --cov=physiomotion4d --cov-report=xml --cov-report=term + pytest tests/ -v --run-slow --run-gpu --run-simpleware --run-experiments --run-tutorials --cov=physiomotion4d --cov-report=xml --cov-report=term env: CUDA_VISIBLE_DEVICES: 0 diff --git a/AGENTS.md b/AGENTS.md index 232e327..3edad47 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -29,7 +29,11 @@ Non-Python tools used by contributor workflows: `self.log_debug()` — never `print()`. Standalone scripts may use `print()`. - Single quotes for strings; double quotes for docstrings. 88-char line limit. - Full type hints (`mypy` strict). Use `Optional[X]` not `X | None`. -- Run `py -m pytest tests/ -m "not slow and not requires_data" -v` to verify changes. +- Run `py -m pytest tests/ -v` to verify changes. Slow / GPU / Simpleware / + experiment / tutorial tests are auto-skipped; opt in per bucket with + `--run-slow`, `--run-gpu`, `--run-simpleware`, `--run-experiments`, + `--run-tutorials`. The `requires_data` marker no longer exists — tests that + need external data download it automatically via the session fixtures. - Consult `docs/API_MAP.md` to locate classes and methods before searching manually. ## Implementation role @@ -41,11 +45,27 @@ Non-Python tools used by contributor workflows: ## Testing role -- Prefer synthetic `itk.Image` and small `pv.PolyData` surfaces — not real patient data. -- State image shape and axis order in every test docstring: e.g. `shape (X, Y, Z, T)`. -- Keep synthetic volumes ≤64 voxels per side for speed. -- Mark tests that genuinely need real data with `@pytest.mark.requires_data`. -- Use `test_tools.py` baseline utilities for surface and image regression checks. +- **Strongly prefer real (downloaded) test data over synthetic data.** Request + the session fixtures (`test_directories`, `download_test_data`, + `test_images`) so the standard datasets are fetched automatically on first + use. Real data exercises preprocessing, resampling, dtype handling, and + world-frame metadata paths that synthetic toy volumes silently bypass. +- Only fall back to synthetic `itk.Image` or `pv.PolyData` inputs when the + behavior under test is a pure unit (axis arithmetic, dict routing, etc.) + where real data adds no signal, or when real data would push the test into + a slow/GPU/Simpleware bucket that doesn't fit the test's purpose. Keep + synthetic volumes ≤64 voxels per side and say so in the docstring. +- State image shape and axis order in every test docstring: e.g. + `shape (X, Y, Z, T) = (64, 64, 32, 1), LPS world frame`. +- **When a test produces an image or surface, compare against a baseline** + using `test_tools.py` utilities (e.g. `TestTools`) and store baselines under + `tests/baselines/` (Git LFS-tracked). Run with `--create-baselines` to + materialize missing baselines on first use. +- Mark tests that need a GPU, a slow runtime, or a licensed Simpleware + install with `@pytest.mark.requires_gpu`, `@pytest.mark.slow`, or + `@pytest.mark.requires_simpleware` so they fall into the right opt-in + bucket. Tests that just need downloadable data need no marker — the + fixture chain handles it. ## Documentation role diff --git a/CLAUDE.md b/CLAUDE.md index b7c040c..b33affa 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -38,23 +38,28 @@ mypy src/ tests/ # All pre-commit hooks pre-commit run --all-files -# Fast tests (recommended for development) -py -m pytest tests/ -m "not slow and not requires_data" -v +# Fast tests (recommended for development — slow/GPU/Simpleware/experiment +# /tutorial tests are auto-skipped unless their opt-in flag is passed) +py -m pytest tests/ -v # Single test file or test by name py -m pytest tests/test_contour_tools.py -v py -m pytest tests/test_contour_tools.py::test_extract_surface -v -# Skip GPU-dependent tests -py -m pytest tests/ --ignore=tests/test_segment_chest_total_segmentator.py \ - --ignore=tests/test_register_images_icon.py +# Opt-in buckets (each flag enables one marker family) +py -m pytest tests/ -v --run-slow # tests marked 'slow' +py -m pytest tests/ -v --run-gpu # tests marked 'requires_gpu' +py -m pytest tests/ -v --run-simpleware # tests marked 'requires_simpleware' +py -m pytest tests/ -v --run-experiments # tests marked 'experiment' +py -m pytest tests/ -v --run-tutorials # tests marked 'tutorial' + +# Typical local GPU profile. The self-hosted CI GPU runner enables every +# bucket: --run-gpu --run-slow --run-simpleware --run-experiments --run-tutorials +py -m pytest tests/ -v --run-gpu --run-slow # With coverage py -m pytest tests/ --cov=src/physiomotion4d --cov-report=html -# Experiment script tests (very slow, opt-in) -py -m pytest tests/ --run-experiments - # Create missing baselines py -m pytest tests/ --create-baselines ``` @@ -86,7 +91,9 @@ Regenerate it after any public API change: `py utils/generate_api_map.py` - Baselines in `tests/baselines/` via Git LFS — run `git lfs pull` after cloning - `tests/conftest.py`: session-scoped fixtures chaining download → convert → segment → register - `src/physiomotion4d/test_tools.py`: baseline comparison utilities (`TestTools`, etc.) -- Markers: `slow`, `requires_gpu`, `requires_data`, `experiment`, `tutorial` +- Markers (all opt-in via `--run-`): `slow`, `requires_gpu`, + `requires_simpleware`, `experiment`, `tutorial`. Data-dependent tests no + longer use a marker — they pull data through fixtures and run by default. - Prefer images from `ROOT/data/test/slicer_heart_small` for tests - Prefer storing results in subdirs `./results/` diff --git a/README.md b/README.md index 7a87fff..3a1ff5e 100644 --- a/README.md +++ b/README.md @@ -632,27 +632,34 @@ See `docs/contributing.rst` for complete IDE setup instructions. PhysioMotion4D includes comprehensive tests covering the complete pipeline from data download to USD generation. ```bash -# Run all tests -pytest tests/ - -# Run fast tests only (recommended for development) -pytest tests/ -m "not slow and not requires_data" -v +# Fast tests (recommended for development). +# slow / GPU / Simpleware / experiment / tutorial tests are auto-skipped +# unless their opt-in flag is passed (see below). Tests that need +# downloadable data fetch it automatically via the session fixtures. +pytest tests/ -v + +# Opt-in buckets (each flag enables one marker family) +pytest tests/ -v --run-slow # tests marked 'slow' +pytest tests/ -v --run-gpu # tests marked 'requires_gpu' +pytest tests/ -v --run-simpleware # tests marked 'requires_simpleware' +pytest tests/ -v --run-experiments # tests marked 'experiment' +pytest tests/ -v --run-tutorials # tests marked 'tutorial' + +# Typical local GPU profile. The self-hosted CI GPU runner enables every +# bucket: --run-gpu --run-slow --run-simpleware --run-experiments --run-tutorials +pytest tests/ -v --run-gpu --run-slow # Run specific test categories pytest tests/test_usd_merge.py -v # USD merge functionality pytest tests/test_usd_time_preservation.py -v # Time-varying data preservation -pytest tests/test_register_images_ants.py -v # ANTs registration -pytest tests/test_register_images_greedy.py -v # Greedy registration -pytest tests/test_register_images_icon.py -v # Icon registration -pytest tests/test_register_time_series_images.py -v # Time series registration -pytest tests/test_segment_chest_total_segmentator.py -v # TotalSegmentator +pytest tests/test_register_images_ants.py -v --run-slow # ANTs registration +pytest tests/test_register_images_greedy.py -v # Greedy registration +pytest tests/test_register_images_icon.py -v --run-gpu --run-slow # Icon registration (GPU) +pytest tests/test_register_time_series_images.py -v --run-slow # Time series registration +pytest tests/test_segment_chest_total_segmentator.py -v --run-slow # TotalSegmentator pytest tests/test_contour_tools.py -v # Mesh and contour tools pytest tests/test_image_tools.py -v # Image processing utilities -pytest tests/test_transform_tools.py -v # Transform operations - -# Skip GPU-dependent tests (segmentation and registration) -pytest tests/ --ignore=tests/test_segment_chest_total_segmentator.py \ - --ignore=tests/test_register_images_icon.py +pytest tests/test_transform_tools.py -v --run-slow # Transform operations # Run with coverage report pytest tests/ --cov=src/physiomotion4d --cov-report=html @@ -742,9 +749,9 @@ Use `/test-feature` to get a test plan and a complete pytest file using syntheti /test-feature RegisterImagesANTs with a pair of small synthetic ITK images ``` -The agent will state image shapes and axis orders in every test docstring, mark -any real-data dependency with `@pytest.mark.requires_data`, and show the exact -run command. +The agent will state image shapes and axis orders in every test docstring, wire +real-data dependencies through the session fixtures (so the data is downloaded +on first use), and show the exact run command. --- diff --git a/docs/API_MAP.md b/docs/API_MAP.md index 0a3e8b0..b70fe02 100644 --- a/docs/API_MAP.md +++ b/docs/API_MAP.md @@ -72,7 +72,7 @@ _Re-run `py utils/generate_api_map.py` whenever public APIs change._ ## src/physiomotion4d/cli/convert_image_to_vtk.py -- `def main()` (line 26): CLI entry point for image to VTK conversion. +- `def main()` (line 30): CLI entry point for image to VTK conversion. ## src/physiomotion4d/cli/convert_vtk_to_usd.py @@ -428,19 +428,19 @@ _Re-run `py utils/generate_api_map.py` whenever public APIs change._ ## src/physiomotion4d/workflow_convert_image_to_usd.py -- **class WorkflowConvertImageToUSD** (line 41): Complete workflow for converting 4D CT images to dynamic USD models. - - `def __init__(self, input_filenames, contrast_enhanced, output_directory, project_name, reference_image_filename=None, number_of_registration_iterations=1, segmentation_method='ChestTotalSegmentator', registration_method='ICON', log_level=logging.INFO, save_registered_images=True, save_registration_transforms=True, save_labelmaps=True)` (line 49): Initialize the image-to-USD workflow. - - `def process(self)` (line 214): Execute the complete workflow from 4D CT to dynamic USD models. +- **class WorkflowConvertImageToUSD** (line 42): Complete workflow for converting 4D CT images to dynamic USD models. + - `def __init__(self, input_filenames, contrast_enhanced, output_directory, project_name, reference_image_filename=None, number_of_registration_iterations=1, segmentation_method='ChestTotalSegmentator', registration_method='ICON', log_level=logging.INFO, save_registered_images=True, save_registration_transforms=True, save_labelmaps=True)` (line 61): Initialize the image-to-USD workflow. + - `def process(self)` (line 235): Execute the complete workflow from 4D CT to dynamic USD models. ## src/physiomotion4d/workflow_convert_image_to_vtk.py -- **class WorkflowConvertImageToVTK** (line 58): Segment a CT image and produce per-anatomy-group VTK surfaces and meshes. - - `def __init__(self, segmentation_method='ChestTotalSegmentator', log_level=logging.INFO)` (line 98): Initialize the workflow. - - `def run_workflow(self, input_image, contrast_enhanced_study=False, anatomy_groups=None)` (line 241): Segment the CT image and extract per-anatomy-group VTK objects. - - `def save_surfaces(surfaces, output_dir, prefix='')` (line 344): Save each group surface to its own VTP file. - - `def save_meshes(meshes, output_dir, prefix='')` (line 371): Save each group voxel mesh to its own VTU file. - - `def save_combined_surface(surfaces, output_dir, prefix='')` (line 397): Merge all group surfaces into a single VTP file. - - `def save_combined_mesh(meshes, output_dir, prefix='')` (line 432): Merge all group meshes into a single VTU file. +- **class WorkflowConvertImageToVTK** (line 59): Segment a CT image and produce per-anatomy-group VTK surfaces and meshes. + - `def __init__(self, segmentation_method='ChestTotalSegmentator', log_level=logging.INFO)` (line 105): Initialize the workflow. + - `def run_workflow(self, input_image, contrast_enhanced_study=False, anatomy_groups=None)` (line 255): Segment the CT image and extract per-anatomy-group VTK objects. + - `def save_surfaces(surfaces, output_dir, prefix='')` (line 358): Save each group surface to its own VTP file. + - `def save_meshes(meshes, output_dir, prefix='')` (line 385): Save each group voxel mesh to its own VTU file. + - `def save_combined_surface(surfaces, output_dir, prefix='')` (line 411): Merge all group surfaces into a single VTP file. + - `def save_combined_mesh(meshes, output_dir, prefix='')` (line 446): Merge all group meshes into a single VTU file. ## src/physiomotion4d/workflow_convert_vtk_to_usd.py @@ -459,18 +459,18 @@ _Re-run `py utils/generate_api_map.py` whenever public APIs change._ ## src/physiomotion4d/workflow_fit_statistical_model_to_patient.py - **class WorkflowFitStatisticalModelToPatient** (line 56): Register anatomical models using multi-stage ICP, mask-based, and image-based - - `def __init__(self, template_model, patient_models=None, patient_image=None, segmentation_method='HeartSimpleware', log_level=logging.INFO)` (line 135): Initialize the model-to-image-and-model registration pipeline. - - `def set_mask_dilation_mm(self, mask_dilation_mm)` (line 361): Set mask dilation amount for auto-generated masks. - - `def set_roi_dilation_mm(self, roi_dilation_mm)` (line 370): Set ROI mask dilation amount. - - `def set_use_pca_registration(self, use_pca_registration, pca_model=None, pca_number_of_modes=0, pca_uses_surface=True)` (line 379): Set whether to use PCA-based registration and provide the PCA model. - - `def set_use_mask_to_mask_registration(self, use_mask_to_mask_registration)` (line 414): Set whether to use mask-to-mask registration. - - `def set_use_mask_to_image_registration(self, use_mask_to_image_registration, template_labelmap=None, template_labelmap_organ_mesh_ids=None, template_labelmap_organ_extra_ids=None, template_labelmap_background_ids=None)` (line 425): Set whether to use mask-to-image registration. - - `def register_model_to_model_icp(self)` (line 499): Perform ICP alignment of template model to patient model. - - `def register_model_to_model_pca(self)` (line 557): Perform PCA-based registration after ICP alignment. - - `def register_mask_to_mask(self, use_icon_refinement=False)` (line 683): Perform mask-based deformable registration of model to patient model. - - `def register_labelmap_to_image(self, use_icon_refinement=False)` (line 751): Perform labelmap-to-image refinement. - - `def transform_model(self, base_model=None)` (line 871): Apply registration transforms to the model. - - `def run_workflow(self, use_icon_registration_refinement=False)` (line 936): Execute the complete multi-stage registration workflow. + - `def __init__(self, template_model, patient_models=None, patient_image=None, segmentation_method='HeartSimplewareTrimmedBranches', log_level=logging.INFO)` (line 135): Initialize the model-to-image-and-model registration pipeline. + - `def set_mask_dilation_mm(self, mask_dilation_mm)` (line 363): Set mask dilation amount for auto-generated masks. + - `def set_roi_dilation_mm(self, roi_dilation_mm)` (line 372): Set ROI mask dilation amount. + - `def set_use_pca_registration(self, use_pca_registration, pca_model=None, pca_number_of_modes=0, pca_uses_surface=True)` (line 381): Set whether to use PCA-based registration and provide the PCA model. + - `def set_use_mask_to_mask_registration(self, use_mask_to_mask_registration)` (line 416): Set whether to use mask-to-mask registration. + - `def set_use_mask_to_image_registration(self, use_mask_to_image_registration, template_labelmap=None, template_labelmap_organ_mesh_ids=None, template_labelmap_organ_extra_ids=None, template_labelmap_background_ids=None)` (line 427): Set whether to use mask-to-image registration. + - `def register_model_to_model_icp(self)` (line 501): Perform ICP alignment of template model to patient model. + - `def register_model_to_model_pca(self)` (line 559): Perform PCA-based registration after ICP alignment. + - `def register_mask_to_mask(self, use_icon_refinement=False)` (line 685): Perform mask-based deformable registration of model to patient model. + - `def register_labelmap_to_image(self, use_icon_refinement=False)` (line 753): Perform labelmap-to-image refinement. + - `def transform_model(self, base_model=None)` (line 873): Apply registration transforms to the model. + - `def run_workflow(self, use_icon_registration_refinement=False)` (line 938): Execute the complete multi-stage registration workflow. ## src/physiomotion4d/workflow_reconstruct_highres_4d_ct.py @@ -490,22 +490,22 @@ _Re-run `py utils/generate_api_map.py` whenever public APIs change._ ## tests/conftest.py - `def pytest_addoption(parser)` (line 35): Add custom command-line options for pytest. -- `def pytest_configure(config)` (line 57): Configure pytest with custom markers and settings. -- `def pytest_collection_modifyitems(config, items)` (line 84): Automatically skip experiment and tutorial tests unless their opt-in flags -- `def pytest_runtest_logreport(report)` (line 109): Collect test timing information after each test completes. -- `def pytest_terminal_summary(terminalreporter, exitstatus, config)` (line 134): Print comprehensive test timing report after all tests complete. -- `def test_directories()` (line 296): Set up test directories for data and results. -- `def download_test_data(test_directories)` (line 321): Download Slicer-Heart-CT data. -- `def test_images(download_test_data, test_directories)` (line 348): Convert and resample 4D NRRD data; return pre-resampled time points. -- `def test_labelmaps(segmenter_total_segmentator, test_images, test_directories)` (line 401): Segment each time point with TotalSegmentator and return result dicts. -- `def test_transforms(registrar_ants, test_images, test_directories)` (line 442): Perform ANTs registration and return results. -- `def segmenter_total_segmentator()` (line 497): Create a SegmentChestTotalSegmentator instance. -- `def segmenter_simpleware()` (line 503): Create a SegmentHeartSimpleware instance. -- `def contour_tools()` (line 509): Create a ContourTools instance. -- `def registrar_ants()` (line 515): Create a RegisterImagesANTs instance. -- `def registrar_greedy()` (line 521): Create a RegisterImagesGreedy instance. -- `def registrar_icon()` (line 527): Create a RegisterImagesICON instance. -- `def transform_tools()` (line 533): Create a TransformTools instance. +- `def pytest_configure(config)` (line 78): Configure pytest with custom markers and settings. +- `def pytest_collection_modifyitems(config, items)` (line 110): Automatically skip experiment and tutorial tests unless their opt-in flags +- `def pytest_runtest_logreport(report)` (line 158): Collect test timing information after each test completes. +- `def pytest_terminal_summary(terminalreporter, exitstatus, config)` (line 183): Print comprehensive test timing report after all tests complete. +- `def test_directories()` (line 345): Set up test directories for data and results. +- `def download_test_data(test_directories)` (line 370): Download Slicer-Heart-CT data. +- `def test_images(download_test_data, test_directories)` (line 397): Convert and resample 4D NRRD data; return pre-resampled time points. +- `def test_labelmaps(segmenter_total_segmentator, test_images, test_directories)` (line 450): Segment each time point with TotalSegmentator and return result dicts. +- `def test_transforms(registrar_ants, test_images, test_directories)` (line 491): Perform ANTs registration and return results. +- `def segmenter_total_segmentator()` (line 546): Create a SegmentChestTotalSegmentator instance. +- `def segmenter_simpleware()` (line 552): Create a SegmentHeartSimpleware instance. +- `def contour_tools()` (line 558): Create a ContourTools instance. +- `def registrar_ants()` (line 564): Create a RegisterImagesANTs instance. +- `def registrar_greedy()` (line 570): Create a RegisterImagesGreedy instance. +- `def registrar_icon()` (line 576): Create a RegisterImagesICON instance. +- `def transform_tools()` (line 582): Create a TransformTools instance. ## tests/test_anatomy_taxonomy.py @@ -528,47 +528,47 @@ _Re-run `py utils/generate_api_map.py` whenever public APIs change._ ## tests/test_contour_tools.py -- **class TestContourTools** (line 22): Test suite for ContourTools functionality. - - `def test_contour_tools_initialization(self, contour_tools)` (line 25): Test that ContourTools initializes correctly. - - `def test_extract_contours_from_heart_mask(self, contour_tools, test_labelmaps, test_directories)` (line 30): Test extracting contours from heart mask. - - `def test_extract_contours_from_lung_mask(self, contour_tools, test_labelmaps, test_directories)` (line 63): Test extracting contours from lung mask. - - `def test_extract_contours_multiple_anatomy(self, contour_tools, test_labelmaps, test_directories)` (line 92): Test extracting contours from multiple anatomical structures. - - `def test_create_mask_from_mesh(self, contour_tools, test_labelmaps, test_images, test_directories)` (line 130): Test creating a mask from extracted mesh. - - `def test_merge_meshes(self, contour_tools, test_labelmaps, test_directories)` (line 171): Test merging multiple meshes. - - `def test_transform_contours_identity(self, contour_tools, test_labelmaps, test_directories)` (line 221): Test transforming contours with identity transform. - - `def test_transform_contours_with_deformation(self, contour_tools, test_labelmaps, test_directories)` (line 267): Test transforming contours with deformation magnitude calculation. - - `def test_contours_from_both_time_points(self, contour_tools, test_labelmaps, test_directories)` (line 316): Test extracting contours from both time points. +- **class TestContourTools** (line 21): Test suite for ContourTools functionality. + - `def test_contour_tools_initialization(self, contour_tools)` (line 24): Test that ContourTools initializes correctly. + - `def test_extract_contours_from_heart_mask(self, contour_tools, test_labelmaps, test_directories)` (line 29): Test extracting contours from heart mask. + - `def test_extract_contours_from_lung_mask(self, contour_tools, test_labelmaps, test_directories)` (line 62): Test extracting contours from lung mask. + - `def test_extract_contours_multiple_anatomy(self, contour_tools, test_labelmaps, test_directories)` (line 91): Test extracting contours from multiple anatomical structures. + - `def test_create_mask_from_mesh(self, contour_tools, test_labelmaps, test_images, test_directories)` (line 129): Test creating a mask from extracted mesh. + - `def test_merge_meshes(self, contour_tools, test_labelmaps, test_directories)` (line 170): Test merging multiple meshes. + - `def test_transform_contours_identity(self, contour_tools, test_labelmaps, test_directories)` (line 220): Test transforming contours with identity transform. + - `def test_transform_contours_with_deformation(self, contour_tools, test_labelmaps, test_directories)` (line 266): Test transforming contours with deformation magnitude calculation. + - `def test_contours_from_both_time_points(self, contour_tools, test_labelmaps, test_directories)` (line 315): Test extracting contours from both time points. ## tests/test_convert_image_4d_to_3d.py -- **class TestConvertImage4DTo3D** (line 17): Test suite for converting a 4D image to a 3D time series. - - `def test_convert_4d_to_3d(self, download_test_data, test_directories)` (line 20): Test conversion of 4D image to 3D time series. - - `def test_slice_files_created(self, download_test_data, test_directories)` (line 43): Test that all expected slice files are present after conversion. - - `def test_load_image_4d(self, download_test_data)` (line 66): Test loading a 4D image. - - `def test_save_3d_images(self, download_test_data, test_directories)` (line 77): Test saving 3D images from a 4D source. +- **class TestConvertImage4DTo3D** (line 16): Test suite for converting a 4D image to a 3D time series. + - `def test_convert_4d_to_3d(self, download_test_data, test_directories)` (line 19): Test conversion of 4D image to 3D time series. + - `def test_slice_files_created(self, download_test_data, test_directories)` (line 42): Test that all expected slice files are present after conversion. + - `def test_load_image_4d(self, download_test_data)` (line 65): Test loading a 4D image. + - `def test_save_3d_images(self, download_test_data, test_directories)` (line 76): Test saving 3D images from a 4D source. ## tests/test_convert_vtk_to_usd.py -- **class TestConvertVTKToUSD** (line 37): Test suite for VTK to USD PolyMesh conversion. - - `def contour_meshes(self, contour_tools, test_labelmaps, test_directories)` (line 41): Extract or load contour meshes for USD conversion testing. - - `def test_converter_initialization(self)` (line 79): Test that ConvertVTKToUSD initializes correctly. - - `def test_supports_mesh_type(self, contour_meshes)` (line 90): Test that converter correctly identifies supported mesh types. - - `def test_convert_single_time_point(self, contour_meshes, test_directories)` (line 103): Test converting a single time point to USD. - - `def test_convert_multiple_time_points(self, contour_meshes, test_directories)` (line 136): Test converting multiple time points to USD. - - `def test_convert_with_deformation(self, contour_tools, test_labelmaps, test_directories)` (line 171): Test converting meshes with deformation magnitude. - - `def test_convert_with_colormap(self, contour_meshes, test_directories)` (line 212): Test converting meshes with colormap visualization. - - `def test_convert_unstructured_grid_to_surface(self, test_directories)` (line 251): Test converting UnstructuredGrid to surface mesh. - - `def test_usd_file_structure(self, contour_meshes, test_directories)` (line 299): Test the structure of generated USD file. - - `def test_time_varying_topology(self, contour_meshes, test_directories)` (line 331): Test handling of time-varying topology. - - `def test_batch_conversion(self, contour_tools, test_labelmaps, test_directories)` (line 372): Test converting multiple anatomy structures in batch. -- **class TestSyntheticConversion** (line 425): Synthetic (no-disk-data) tests for ConvertVTKToUSD. - - `def test_single_frame_prim_has_time_sample(self, tmp_path)` (line 438): Single-frame _convert_unified() must author one time sample, not a static prim. - - `def test_static_merge_prim_names_use_data_basename(self, tmp_path)` (line 454): Static-merge prims must be named {data_basename}_{i}, not Mesh_{i}. - - `def test_mask_ids_basic_produces_per_label_prims(self, tmp_path)` (line 486): mask_ids must produce one USD prim per label grouped under /Anatomy - - `def test_structured_grid_extracts_surface(self, tmp_path)` (line 512): StructuredGrid input is surface-extracted when convert_to_surface is true. - - `def test_mask_ids_missing_label_filters_time_codes(self, tmp_path)` (line 528): Time codes for a label must be filtered to frames where it actually appears. - - `def test_mask_ids_missing_boundary_labels_falls_back(self, tmp_path)` (line 563): Mesh without boundary_labels array falls back to a 'default' prim. - - `def test_mask_ids_groups_by_segmenter_type(self, tmp_path)` (line 578): When a segmenter is supplied, labels are grouped under their +- **class TestConvertVTKToUSD** (line 36): Test suite for VTK to USD PolyMesh conversion. + - `def contour_meshes(self, contour_tools, test_labelmaps, test_directories)` (line 40): Extract or load contour meshes for USD conversion testing. + - `def test_converter_initialization(self)` (line 78): Test that ConvertVTKToUSD initializes correctly. + - `def test_supports_mesh_type(self, contour_meshes)` (line 89): Test that converter correctly identifies supported mesh types. + - `def test_convert_single_time_point(self, contour_meshes, test_directories)` (line 102): Test converting a single time point to USD. + - `def test_convert_multiple_time_points(self, contour_meshes, test_directories)` (line 135): Test converting multiple time points to USD. + - `def test_convert_with_deformation(self, contour_tools, test_labelmaps, test_directories)` (line 170): Test converting meshes with deformation magnitude. + - `def test_convert_with_colormap(self, contour_meshes, test_directories)` (line 211): Test converting meshes with colormap visualization. + - `def test_convert_unstructured_grid_to_surface(self, test_directories)` (line 250): Test converting UnstructuredGrid to surface mesh. + - `def test_usd_file_structure(self, contour_meshes, test_directories)` (line 298): Test the structure of generated USD file. + - `def test_time_varying_topology(self, contour_meshes, test_directories)` (line 330): Test handling of time-varying topology. + - `def test_batch_conversion(self, contour_tools, test_labelmaps, test_directories)` (line 371): Test converting multiple anatomy structures in batch. +- **class TestSyntheticConversion** (line 424): Synthetic (no-disk-data) tests for ConvertVTKToUSD. + - `def test_single_frame_prim_has_time_sample(self, tmp_path)` (line 437): Single-frame _convert_unified() must author one time sample, not a static prim. + - `def test_static_merge_prim_names_use_data_basename(self, tmp_path)` (line 453): Static-merge prims must be named {data_basename}_{i}, not Mesh_{i}. + - `def test_mask_ids_basic_produces_per_label_prims(self, tmp_path)` (line 485): mask_ids must produce one USD prim per label grouped under /Anatomy + - `def test_structured_grid_extracts_surface(self, tmp_path)` (line 511): StructuredGrid input is surface-extracted when convert_to_surface is true. + - `def test_mask_ids_missing_label_filters_time_codes(self, tmp_path)` (line 527): Time codes for a label must be filtered to frames where it actually appears. + - `def test_mask_ids_missing_boundary_labels_falls_back(self, tmp_path)` (line 562): Mesh without boundary_labels array falls back to a 'default' prim. + - `def test_mask_ids_groups_by_segmenter_type(self, tmp_path)` (line 577): When a segmenter is supplied, labels are grouped under their ## tests/test_download_heart_data.py @@ -577,9 +577,9 @@ _Re-run `py utils/generate_api_map.py` whenever public APIs change._ - `def test_verify_kcl_heart_model_data(self, tmp_path)` (line 29): Verify KCL data by expected average mesh and input mesh filenames. - `def test_verify_dirlab_4dct_data(self, tmp_path)` (line 41): Verify DirLab data by supported Case1 phase image layouts. - `def test_verify_chop_valve_4d_data(self, tmp_path)` (line 52): Verify CHOP data by expected CT or valve time-series paths. -- **class TestDownloadHeartData** (line 64): Test suite for downloading and converting Slicer-Heart-CT data. - - `def test_directories_created(self, test_directories)` (line 67): Test that directories are created successfully. - - `def test_data_downloaded(self, download_test_data, test_directories)` (line 91): Test that the TruncalValve 4D CT data file is downloaded. +- **class TestDownloadHeartData** (line 63): Test suite for downloading and converting Slicer-Heart-CT data. + - `def test_directories_created(self, test_directories)` (line 66): Test that directories are created successfully. + - `def test_data_downloaded(self, download_test_data, test_directories)` (line 90): Test that the TruncalValve 4D CT data file is downloaded. ## tests/test_experiments.py @@ -588,14 +588,14 @@ _Re-run `py utils/generate_api_map.py` whenever public APIs change._ - `def run_experiment_scripts(subdir_name, timeout_per_script=3600)` (line 176): Run all Python scripts in an experiment subdirectory in alphanumeric order. - `def test_experiment_colormap_vtk_to_usd()` (line 297): Test Colormap-VTK_To_USD experiment scripts. - `def test_experiment_reconstruct_4dct()` (line 329): Test Reconstruct4DCT experiment scripts. -- `def test_experiment_heart_vtk_series_to_usd()` (line 347): Test Heart-VTKSeries_To_USD experiment scripts. -- `def test_experiment_heart_gated_ct_to_usd()` (line 367): Test Heart-GatedCT_To_USD experiment scripts. -- `def test_experiment_convert_vtk_to_usd()` (line 390): Test Convert_VTK_To_USD experiment scripts. -- `def test_experiment_create_statistical_model()` (line 409): Test Heart-Create_Statistical_Model experiment scripts. -- `def test_experiment_heart_statistical_model_to_patient()` (line 433): Test Heart-Statistical_Model_To_Patient experiment scripts. -- `def test_experiment_lung_gated_ct_to_usd()` (line 467): Test Lung-GatedCT_To_USD experiment scripts. -- `def test_experiment_structure()` (line 511): Validate the structure of the experiments directory. -- `def test_list_scripts_in_subdir(subdir_name)` (line 565): List all scripts in each experiment subdirectory. +- `def test_experiment_heart_vtk_series_to_usd()` (line 346): Test Heart-VTKSeries_To_USD experiment scripts. +- `def test_experiment_heart_gated_ct_to_usd()` (line 365): Test Heart-GatedCT_To_USD experiment scripts. +- `def test_experiment_convert_vtk_to_usd()` (line 387): Test Convert_VTK_To_USD experiment scripts. +- `def test_experiment_create_statistical_model()` (line 405): Test Heart-Create_Statistical_Model experiment scripts. +- `def test_experiment_heart_statistical_model_to_patient()` (line 428): Test Heart-Statistical_Model_To_Patient experiment scripts. +- `def test_experiment_lung_gated_ct_to_usd()` (line 461): Test Lung-GatedCT_To_USD experiment scripts. +- `def test_experiment_structure()` (line 505): Validate the structure of the experiments directory. +- `def test_list_scripts_in_subdir(subdir_name)` (line 559): List all scripts in each experiment subdirectory. ## tests/test_image_tools.py @@ -607,17 +607,17 @@ _Re-run `py utils/generate_api_map.py` whenever public APIs change._ - `def test_itk_to_sitk_vector_image(self, image_tools)` (line 133): Test conversion of vector ITK image to SimpleITK. - `def test_sitk_to_itk_vector_image(self, image_tools)` (line 171): Test conversion of vector SimpleITK image to ITK. - `def test_roundtrip_vector_image(self, image_tools)` (line 202): Test roundtrip conversion for vector images: ITK -> SimpleITK -> ITK. - - `def test_imwrite_imread_vd3(self, image_tools, test_transforms, test_images, test_directories)` (line 241): Test reading and writing double precision vector images. -- **class TestFlipImage** (line 334): Unit tests for ImageTools.flip_image (axis flips and direction reset). - - `def image_tools(self)` (line 338) - - `def test_flip_x_flips_along_last_array_axis(self, image_tools)` (line 341): flip_x flips the image along the x (last) array dimension. - - `def test_flip_y_flips_along_middle_array_axis(self, image_tools)` (line 354): flip_y flips the image along the y (middle) array dimension. - - `def test_flip_z_flips_along_first_array_axis(self, image_tools)` (line 368): flip_z flips the image along the z (first) array dimension. - - `def test_flip_xy_combines_flips(self, image_tools)` (line 380): flip_x and flip_y together flip both axes. - - `def test_no_flip_returns_same_image(self, image_tools)` (line 390): With no flip flags, image is returned unchanged. - - `def test_mask_flipped_in_lockstep_with_image(self, image_tools)` (line 401): When a mask is provided, it is flipped with the same axes as the image. - - `def test_flip_and_make_identity_sets_direction_to_identity(self, image_tools)` (line 422): flip_and_make_identity flips as needed and sets direction matrix to identity. - - `def test_flip_and_make_identity_with_mask_sets_both_directions_to_identity(self, image_tools)` (line 438): With mask and flip_and_make_identity, both image and mask get identity direction. + - `def test_imwrite_imread_vd3(self, image_tools, test_transforms, test_images, test_directories)` (line 240): Test reading and writing double precision vector images. +- **class TestFlipImage** (line 333): Unit tests for ImageTools.flip_image (axis flips and direction reset). + - `def image_tools(self)` (line 337) + - `def test_flip_x_flips_along_last_array_axis(self, image_tools)` (line 340): flip_x flips the image along the x (last) array dimension. + - `def test_flip_y_flips_along_middle_array_axis(self, image_tools)` (line 353): flip_y flips the image along the y (middle) array dimension. + - `def test_flip_z_flips_along_first_array_axis(self, image_tools)` (line 367): flip_z flips the image along the z (first) array dimension. + - `def test_flip_xy_combines_flips(self, image_tools)` (line 379): flip_x and flip_y together flip both axes. + - `def test_no_flip_returns_same_image(self, image_tools)` (line 389): With no flip flags, image is returned unchanged. + - `def test_mask_flipped_in_lockstep_with_image(self, image_tools)` (line 400): When a mask is provided, it is flipped with the same axes as the image. + - `def test_flip_and_make_identity_sets_direction_to_identity(self, image_tools)` (line 421): flip_and_make_identity flips as needed and sets direction matrix to identity. + - `def test_flip_and_make_identity_with_mask_sets_both_directions_to_identity(self, image_tools)` (line 437): With mask and flip_and_make_identity, both image and mask get identity direction. ## tests/test_import_public_api.py @@ -625,34 +625,34 @@ _Re-run `py utils/generate_api_map.py` whenever public APIs change._ ## tests/test_register_images_ants.py -- **class TestRegisterImagesANTs** (line 25): Test suite for ANTs-based image registration. - - `def test_registrar_initialization(self, registrar_ants)` (line 28): Test that RegisterImagesANTs initializes correctly. - - `def test_set_modality(self, registrar_ants)` (line 36): Test setting imaging modality. - - `def test_set_fixed_image(self, registrar_ants, test_images)` (line 46): Test setting fixed image. - - `def test_register_without_mask(self, registrar_ants, test_images, test_directories)` (line 59): Test basic registration without masks. - - `def test_register_with_mask(self, registrar_ants, test_images, test_directories)` (line 113): Test registration with binary masks. - - `def test_transform_application(self, registrar_ants, test_images, test_directories)` (line 206): Test applying registration transforms to images. - - `def test_preprocess_images(self, registrar_ants, test_images)` (line 260): Test image preprocessing. - - `def test_registration_with_initial_transform(self, registrar_ants, test_images, test_directories)` (line 278): Test registration with initial transform. - - `def test_multiple_registrations(self, registrar_ants, test_images)` (line 313): Test running multiple registrations in sequence. - - `def test_transform_types(self, registrar_ants, test_images)` (line 341): Test that transforms are correct ITK types. - - `def test_image_conversion_cycle_scalar(self, registrar_ants, test_images)` (line 369): Test round-trip conversion: ITK image -> ANTs -> ITK for scalar images. - - `def test_image_conversion_cycle_different_dtypes(self, registrar_ants, test_images)` (line 445): Test round-trip conversion with different data types. - - `def test_image_conversion_preserves_metadata(self, registrar_ants)` (line 477): Test that image conversion preserves all metadata. - - `def test_transform_conversion_cycle_affine(self, registrar_ants, test_images)` (line 524): Test round-trip conversion: ITK affine transform -> ANTs -> ITK. - - `def test_transform_conversion_cycle_displacement_field(self, registrar_ants, test_images)` (line 630): Test round-trip conversion: ITK displacement field -> ANTs -> ITK. - - `def test_transform_conversion_with_composite(self, registrar_ants, test_images)` (line 714): Test conversion of composite transforms. +- **class TestRegisterImagesANTs** (line 24): Test suite for ANTs-based image registration. + - `def test_registrar_initialization(self, registrar_ants)` (line 27): Test that RegisterImagesANTs initializes correctly. + - `def test_set_modality(self, registrar_ants)` (line 35): Test setting imaging modality. + - `def test_set_fixed_image(self, registrar_ants, test_images)` (line 45): Test setting fixed image. + - `def test_register_without_mask(self, registrar_ants, test_images, test_directories)` (line 58): Test basic registration without masks. + - `def test_register_with_mask(self, registrar_ants, test_images, test_directories)` (line 112): Test registration with binary masks. + - `def test_transform_application(self, registrar_ants, test_images, test_directories)` (line 205): Test applying registration transforms to images. + - `def test_preprocess_images(self, registrar_ants, test_images)` (line 259): Test image preprocessing. + - `def test_registration_with_initial_transform(self, registrar_ants, test_images, test_directories)` (line 277): Test registration with initial transform. + - `def test_multiple_registrations(self, registrar_ants, test_images)` (line 312): Test running multiple registrations in sequence. + - `def test_transform_types(self, registrar_ants, test_images)` (line 340): Test that transforms are correct ITK types. + - `def test_image_conversion_cycle_scalar(self, registrar_ants, test_images)` (line 368): Test round-trip conversion: ITK image -> ANTs -> ITK for scalar images. + - `def test_image_conversion_cycle_different_dtypes(self, registrar_ants, test_images)` (line 444): Test round-trip conversion with different data types. + - `def test_image_conversion_preserves_metadata(self, registrar_ants)` (line 476): Test that image conversion preserves all metadata. + - `def test_transform_conversion_cycle_affine(self, registrar_ants, test_images)` (line 523): Test round-trip conversion: ITK affine transform -> ANTs -> ITK. + - `def test_transform_conversion_cycle_displacement_field(self, registrar_ants, test_images)` (line 629): Test round-trip conversion: ITK displacement field -> ANTs -> ITK. + - `def test_transform_conversion_with_composite(self, registrar_ants, test_images)` (line 713): Test conversion of composite transforms. ## tests/test_register_images_greedy.py -- **class TestRegisterImagesGreedy** (line 22): Test suite for Greedy-based image registration. - - `def test_registrar_initialization(self, registrar_greedy)` (line 25): Test that RegisterImagesGreedy initializes correctly. - - `def test_set_modality(self, registrar_greedy)` (line 35): Test setting imaging modality. - - `def test_set_transform_type_and_metric(self, registrar_greedy)` (line 45): Test setting transform type and metric. - - `def test_set_fixed_image(self, registrar_greedy, test_images)` (line 72): Test setting fixed image. - - `def test_register_affine_without_mask(self, registrar_greedy, test_images, test_directories)` (line 83): Test affine registration without masks. - - `def test_register_affine_with_mask(self, registrar_greedy, test_images, test_directories)` (line 128): Test affine registration with binary masks. - - `def test_transform_application(self, registrar_greedy, test_images, test_directories)` (line 188): Test applying registration transform to moving image. +- **class TestRegisterImagesGreedy** (line 21): Test suite for Greedy-based image registration. + - `def test_registrar_initialization(self, registrar_greedy)` (line 24): Test that RegisterImagesGreedy initializes correctly. + - `def test_set_modality(self, registrar_greedy)` (line 34): Test setting imaging modality. + - `def test_set_transform_type_and_metric(self, registrar_greedy)` (line 44): Test setting transform type and metric. + - `def test_set_fixed_image(self, registrar_greedy, test_images)` (line 71): Test setting fixed image. + - `def test_register_affine_without_mask(self, registrar_greedy, test_images, test_directories)` (line 82): Test affine registration without masks. + - `def test_register_affine_with_mask(self, registrar_greedy, test_images, test_directories)` (line 127): Test affine registration with binary masks. + - `def test_transform_application(self, registrar_greedy, test_images, test_directories)` (line 187): Test applying registration transform to moving image. ## tests/test_register_images_icon.py @@ -680,24 +680,24 @@ _Re-run `py utils/generate_api_map.py` whenever public APIs change._ ## tests/test_register_time_series_images.py -- **class TestRegisterTimeSeriesImages** (line 25): Test suite for time series image registration. - - `def test_registrar_initialization_ants(self)` (line 30): Test that RegisterTimeSeriesImages initializes correctly with ANTs. - - `def test_registrar_initialization_icon(self)` (line 44): Test that RegisterTimeSeriesImages initializes correctly with ICON. - - `def test_registrar_initialization_invalid_method(self)` (line 58): Test that invalid registration method raises error. - - `def test_set_modality(self)` (line 65): Test setting imaging modality. - - `def test_set_fixed_image(self, test_images)` (line 73): Test setting fixed image. - - `def test_set_number_of_iterations(self)` (line 84): Test setting number of iterations. - - `def test_register_time_series_basic(self, test_images, test_directories)` (line 104): Test basic time series registration without prior transform. - - `def test_register_time_series_with_prior(self, test_images, test_directories)` (line 186): Test time series registration with prior transform usage. - - `def test_register_time_series_identity_start(self, test_images)` (line 247): Test time series registration with identity for starting image. - - `def test_register_time_series_different_starting_indices(self, test_images)` (line 273): Test time series registration with different starting indices. - - `def test_register_time_series_error_no_fixed_image(self)` (line 303): Test that error is raised if fixed image not set. - - `def test_register_time_series_error_invalid_starting_index(self, test_images)` (line 314): Test that error is raised for invalid starting index. - - `def test_register_time_series_error_invalid_prior_portion(self, test_images)` (line 337): Test that error is raised for invalid prior portion value. - - `def test_transform_application_time_series(self, test_images, test_directories)` (line 362): Test applying transforms from time series registration. - - `def test_register_time_series_icon(self, test_images)` (line 414): Test time series registration with ICON method. - - `def test_register_time_series_with_mask(self, test_images, test_directories)` (line 439): Test time series registration with fixed image mask. - - `def test_bidirectional_registration(self, test_images)` (line 484): Test that bidirectional registration works correctly. +- **class TestRegisterTimeSeriesImages** (line 24): Test suite for time series image registration. + - `def test_registrar_initialization_ants(self)` (line 29): Test that RegisterTimeSeriesImages initializes correctly with ANTs. + - `def test_registrar_initialization_icon(self)` (line 43): Test that RegisterTimeSeriesImages initializes correctly with ICON. + - `def test_registrar_initialization_invalid_method(self)` (line 57): Test that invalid registration method raises error. + - `def test_set_modality(self)` (line 64): Test setting imaging modality. + - `def test_set_fixed_image(self, test_images)` (line 72): Test setting fixed image. + - `def test_set_number_of_iterations(self)` (line 83): Test setting number of iterations. + - `def test_register_time_series_basic(self, test_images, test_directories)` (line 103): Test basic time series registration without prior transform. + - `def test_register_time_series_with_prior(self, test_images, test_directories)` (line 185): Test time series registration with prior transform usage. + - `def test_register_time_series_identity_start(self, test_images)` (line 246): Test time series registration with identity for starting image. + - `def test_register_time_series_different_starting_indices(self, test_images)` (line 272): Test time series registration with different starting indices. + - `def test_register_time_series_error_no_fixed_image(self)` (line 302): Test that error is raised if fixed image not set. + - `def test_register_time_series_error_invalid_starting_index(self, test_images)` (line 313): Test that error is raised for invalid starting index. + - `def test_register_time_series_error_invalid_prior_portion(self, test_images)` (line 336): Test that error is raised for invalid prior portion value. + - `def test_transform_application_time_series(self, test_images, test_directories)` (line 361): Test applying transforms from time series registration. + - `def test_register_time_series_icon(self, test_images)` (line 413): Test time series registration with ICON method. + - `def test_register_time_series_with_mask(self, test_images, test_directories)` (line 438): Test time series registration with fixed image mask. + - `def test_bidirectional_registration(self, test_images)` (line 483): Test that bidirectional registration works correctly. ## tests/test_segment_chest_total_segmentator.py @@ -712,77 +712,77 @@ _Re-run `py utils/generate_api_map.py` whenever public APIs change._ ## tests/test_segment_heart_simpleware.py -- **class TestSegmentHeartSimpleware** (line 32): Test suite for SegmentHeartSimpleware (Simpleware Medical ASCardio). - - `def test_segmenter_initialization(self, segmenter_simpleware)` (line 35): Test that SegmentHeartSimpleware initializes correctly. - - `def test_set_simpleware_executable_path(self, segmenter_simpleware)` (line 68): Test setting custom Simpleware executable path. - - `def test_segment_single_image(self, segmenter_simpleware, test_images, test_directories)` (line 81): Test segmentation on a cardiac CT time point. - - `def test_anatomy_group_masks(self, segmenter_simpleware, test_images)` (line 145): Test that anatomy group masks are created (heart, vessels, etc.). - - `def test_contrast_detection(self, segmenter_simpleware, test_images)` (line 182): Test contrast mask is returned (base class behavior). - - `def test_postprocessing(self, segmenter_simpleware, test_images)` (line 198): Test that output labelmap matches input size and spacing. +- **class TestSegmentHeartSimpleware** (line 33): Test suite for SegmentHeartSimpleware (Simpleware Medical ASCardio). + - `def test_segmenter_initialization(self, segmenter_simpleware)` (line 36): Test that SegmentHeartSimpleware initializes correctly. + - `def test_set_simpleware_executable_path(self, segmenter_simpleware)` (line 69): Test setting custom Simpleware executable path. + - `def test_segment_single_image(self, segmenter_simpleware, test_images, test_directories)` (line 82): Test segmentation on a cardiac CT time point. + - `def test_anatomy_group_masks(self, segmenter_simpleware, test_images)` (line 146): Test that anatomy group masks are created (heart, vessels, etc.). + - `def test_contrast_detection(self, segmenter_simpleware, test_images)` (line 183): Test contrast mask is returned (base class behavior). + - `def test_postprocessing(self, segmenter_simpleware, test_images)` (line 199): Test that output labelmap matches input size and spacing. ## tests/test_transform_tools.py -- **class TestTransformTools** (line 24): Test suite for TransformTools functionality. - - `def test_contour(self, test_images)` (line 28): Create a simple test contour mesh. - - `def test_transform_tools_initialization(self, transform_tools)` (line 34): Test that TransformTools initializes correctly. - - `def test_transform_image_linear(self, transform_tools, test_transforms, test_images, test_directories)` (line 41): Test transforming image with linear interpolation. - - `def test_transform_image_nearest(self, transform_tools, test_transforms, test_images, test_directories)` (line 81): Test transforming image with nearest neighbor interpolation. - - `def test_transform_image_sinc(self, transform_tools, test_transforms, test_images, test_directories)` (line 115): Test transforming image with sinc interpolation. - - `def test_transform_image_invalid_method(self, transform_tools, test_transforms, test_images)` (line 149): Test that invalid interpolation method raises error. - - `def test_transform_pvcontour_without_deformation(self, transform_tools, test_contour, test_transforms)` (line 172): Test transforming PyVista contour without deformation magnitude. - - `def test_transform_pvcontour_with_deformation(self, transform_tools, test_contour, test_transforms, test_directories)` (line 207): Test transforming PyVista contour with deformation magnitude. - - `def test_transform_dataset_preserves_unstructured_grid_topology(self, transform_tools)` (line 250): Transform UnstructuredGrid points with image shape (Z, Y, X) = (3, 3, 3). - - `def test_convert_transform_to_displacement_field(self, transform_tools, test_transforms, test_images, test_directories)` (line 287): Test converting transform to deformation field image. - - `def test_convert_vtk_matrix_to_itk_transform(self, transform_tools)` (line 328): Test converting VTK matrix to ITK transform. - - `def test_compute_jacobian_determinant_from_field(self, transform_tools, test_transforms, test_images, test_directories)` (line 360): Test computing Jacobian determinant from deformation field. - - `def test_detect_folding_in_field(self, transform_tools, test_transforms, test_images)` (line 411): Test detecting spatial folding in deformation field. - - `def test_interpolate_transforms(self, transform_tools, test_transforms, test_images)` (line 441): Test temporal interpolation between transforms. - - `def test_combine_displacement_field_transforms(self, transform_tools, test_transforms, test_images)` (line 478): Test composing two transforms with various weights. - - `def test_smooth_transform(self, transform_tools, test_transforms, test_images)` (line 592): Test smoothing a transform. - - `def test_combine_transforms_with_masks(self, transform_tools, test_transforms, test_images)` (line 618): Test combining transforms with spatial masks. - - `def test_multiple_transform_applications(self, transform_tools, test_transforms, test_images)` (line 665): Test applying multiple transforms in sequence. - - `def test_identity_transform(self, transform_tools, test_images)` (line 693): Test that identity transform doesn't change the image. +- **class TestTransformTools** (line 23): Test suite for TransformTools functionality. + - `def test_contour(self, test_images)` (line 27): Create a simple test contour mesh. + - `def test_transform_tools_initialization(self, transform_tools)` (line 33): Test that TransformTools initializes correctly. + - `def test_transform_image_linear(self, transform_tools, test_transforms, test_images, test_directories)` (line 40): Test transforming image with linear interpolation. + - `def test_transform_image_nearest(self, transform_tools, test_transforms, test_images, test_directories)` (line 80): Test transforming image with nearest neighbor interpolation. + - `def test_transform_image_sinc(self, transform_tools, test_transforms, test_images, test_directories)` (line 114): Test transforming image with sinc interpolation. + - `def test_transform_image_invalid_method(self, transform_tools, test_transforms, test_images)` (line 148): Test that invalid interpolation method raises error. + - `def test_transform_pvcontour_without_deformation(self, transform_tools, test_contour, test_transforms)` (line 171): Test transforming PyVista contour without deformation magnitude. + - `def test_transform_pvcontour_with_deformation(self, transform_tools, test_contour, test_transforms, test_directories)` (line 206): Test transforming PyVista contour with deformation magnitude. + - `def test_transform_dataset_preserves_unstructured_grid_topology(self, transform_tools)` (line 249): Transform UnstructuredGrid points with image shape (Z, Y, X) = (3, 3, 3). + - `def test_convert_transform_to_displacement_field(self, transform_tools, test_transforms, test_images, test_directories)` (line 286): Test converting transform to deformation field image. + - `def test_convert_vtk_matrix_to_itk_transform(self, transform_tools)` (line 327): Test converting VTK matrix to ITK transform. + - `def test_compute_jacobian_determinant_from_field(self, transform_tools, test_transforms, test_images, test_directories)` (line 359): Test computing Jacobian determinant from deformation field. + - `def test_detect_folding_in_field(self, transform_tools, test_transforms, test_images)` (line 410): Test detecting spatial folding in deformation field. + - `def test_interpolate_transforms(self, transform_tools, test_transforms, test_images)` (line 440): Test temporal interpolation between transforms. + - `def test_combine_displacement_field_transforms(self, transform_tools, test_transforms, test_images)` (line 477): Test composing two transforms with various weights. + - `def test_smooth_transform(self, transform_tools, test_transforms, test_images)` (line 591): Test smoothing a transform. + - `def test_combine_transforms_with_masks(self, transform_tools, test_transforms, test_images)` (line 617): Test combining transforms with spatial masks. + - `def test_multiple_transform_applications(self, transform_tools, test_transforms, test_images)` (line 664): Test applying multiple transforms in sequence. + - `def test_identity_transform(self, transform_tools, test_images)` (line 692): Test that identity transform doesn't change the image. ## tests/test_tutorials.py -- **class TestTutorial01HeartGatedCTToUSD** (line 79): End-to-end test for tutorial_01_heart_gated_ct_to_usd.py. - - `def test_run(self, test_directories)` (line 84) -- **class TestTutorial02CTToVTK** (line 107): End-to-end test for tutorial_02_ct_to_vtk.py. - - `def test_run(self, test_directories)` (line 112) -- **class TestTutorial03CreateStatisticalModel** (line 134): End-to-end test for tutorial_03_create_statistical_model.py. - - `def test_run(self, test_directories)` (line 139) -- **class TestTutorial04FitStatisticalModelToPatient** (line 162): End-to-end test for tutorial_04_fit_statistical_model_to_patient.py. - - `def test_run(self, test_directories)` (line 167) -- **class TestTutorial05VTKToUSD** (line 206): End-to-end test for tutorial_05_vtk_to_usd.py. - - `def test_run(self, test_directories)` (line 211) -- **class TestTutorial06ReconstructHighres4DCT** (line 247): End-to-end test for tutorial_06_reconstruct_highres_4d_ct.py. - - `def test_run(self, test_directories)` (line 252) +- **class TestTutorial01HeartGatedCTToUSD** (line 78): End-to-end test for tutorial_01_heart_gated_ct_to_usd.py. + - `def test_run(self, test_directories)` (line 83) +- **class TestTutorial02CTToVTK** (line 105): End-to-end test for tutorial_02_ct_to_vtk.py. + - `def test_run(self, test_directories)` (line 110) +- **class TestTutorial03CreateStatisticalModel** (line 131): End-to-end test for tutorial_03_create_statistical_model.py. + - `def test_run(self, test_directories)` (line 136) +- **class TestTutorial04FitStatisticalModelToPatient** (line 158): End-to-end test for tutorial_04_fit_statistical_model_to_patient.py. + - `def test_run(self, test_directories)` (line 163) +- **class TestTutorial05VTKToUSD** (line 201): End-to-end test for tutorial_05_vtk_to_usd.py. + - `def test_run(self, test_directories)` (line 206) +- **class TestTutorial06ReconstructHighres4DCT** (line 241): End-to-end test for tutorial_06_reconstruct_highres_4d_ct.py. + - `def test_run(self, test_directories)` (line 246) ## tests/test_usd_merge.py - `def analyze_usd_file(filepath)` (line 17): Analyze a USD file for materials and time samples. -- **class TestUSDMerge** (line 73): Test suite for USD file merging. - - `def test_data_files(self)` (line 77): Locate test USD files with materials and time-varying data. - - `def output_dir(self, tmp_path_factory)` (line 92): Create temporary output directory for test results. - - `def input_stats(self, test_data_files)` (line 98): Analyze input USD files. - - `def test_merge_usd_files_copy_method(self, test_data_files, input_stats, output_dir)` (line 104): Test merge_usd_files() manual copy method. - - `def test_merge_usd_files_flattened_method(self, test_data_files, input_stats, output_dir)` (line 165): Test merge_usd_files_flattened() composition method. - - `def test_both_methods_produce_equivalent_results(self, test_data_files, output_dir)` (line 226): Verify both merge methods produce equivalent results. +- **class TestUSDMerge** (line 72): Test suite for USD file merging. + - `def test_data_files(self)` (line 76): Locate test USD files with materials and time-varying data. + - `def output_dir(self, tmp_path_factory)` (line 91): Create temporary output directory for test results. + - `def input_stats(self, test_data_files)` (line 97): Analyze input USD files. + - `def test_merge_usd_files_copy_method(self, test_data_files, input_stats, output_dir)` (line 103): Test merge_usd_files() manual copy method. + - `def test_merge_usd_files_flattened_method(self, test_data_files, input_stats, output_dir)` (line 164): Test merge_usd_files_flattened() composition method. + - `def test_both_methods_produce_equivalent_results(self, test_data_files, output_dir)` (line 225): Verify both merge methods produce equivalent results. ## tests/test_usd_time_preservation.py - `def get_time_metadata(filepath)` (line 17): Extract time metadata from a USD file. - `def get_mesh_time_samples(filepath, mesh_name='inferior_vena_cava')` (line 41): Get time sample data for a specific mesh in a USD file. -- **class TestUSDTimePreservation** (line 88): Test suite for USD time-varying data preservation. - - `def test_data_files(self)` (line 92): Locate test USD files with time-varying data. - - `def output_dir(self, tmp_path_factory)` (line 107): Create temporary output directory for test results. - - `def source_metadata(self, test_data_files)` (line 113): Get time metadata from source file. - - `def source_time_samples(self, test_data_files)` (line 118): Get time sample data from source file. - - `def test_merge_copy_preserves_time_metadata(self, test_data_files, source_metadata, output_dir)` (line 124): Test that merge_usd_files() preserves time metadata. - - `def test_merge_flattened_preserves_time_metadata(self, test_data_files, source_metadata, output_dir)` (line 156): Test that merge_usd_files_flattened() preserves time metadata. - - `def test_merge_copy_preserves_time_samples(self, test_data_files, source_time_samples, output_dir)` (line 188): Test that merge_usd_files() preserves actual time sample data. - - `def test_merge_flattened_preserves_time_samples(self, test_data_files, source_time_samples, output_dir)` (line 229): Test that merge_usd_files_flattened() preserves actual time sample data. - - `def test_animation_range_matches_actual_motion(self, test_data_files, source_time_samples, output_dir)` (line 270): Test that the full animation range is accessible. +- **class TestUSDTimePreservation** (line 87): Test suite for USD time-varying data preservation. + - `def test_data_files(self)` (line 91): Locate test USD files with time-varying data. + - `def output_dir(self, tmp_path_factory)` (line 106): Create temporary output directory for test results. + - `def source_metadata(self, test_data_files)` (line 112): Get time metadata from source file. + - `def source_time_samples(self, test_data_files)` (line 117): Get time sample data from source file. + - `def test_merge_copy_preserves_time_metadata(self, test_data_files, source_metadata, output_dir)` (line 123): Test that merge_usd_files() preserves time metadata. + - `def test_merge_flattened_preserves_time_metadata(self, test_data_files, source_metadata, output_dir)` (line 155): Test that merge_usd_files_flattened() preserves time metadata. + - `def test_merge_copy_preserves_time_samples(self, test_data_files, source_time_samples, output_dir)` (line 187): Test that merge_usd_files() preserves actual time sample data. + - `def test_merge_flattened_preserves_time_samples(self, test_data_files, source_time_samples, output_dir)` (line 228): Test that merge_usd_files_flattened() preserves actual time sample data. + - `def test_animation_range_matches_actual_motion(self, test_data_files, source_time_samples, output_dir)` (line 269): Test that the full animation range is accessible. ## tests/test_vtk_to_usd_library.py @@ -802,22 +802,25 @@ _Re-run `py utils/generate_api_map.py` whenever public APIs change._ - `def test_inspect_file_reports_empty_mesh(self, tmp_path)` (line 210): inspect_file() reports empty meshes without raising. - `def test_file_primvar_preservation(self, tmp_path)` (line 226): Point arrays in a VTP file are preserved as USD primvars. - `def test_time_series_conversion(self, tmp_path)` (line 245): Multiple VTP files write point time samples and stage time metadata. -- **class TestVTKToUSDConversion** (line 269): Test ConvertVTKToUSD on optional real VTK data. - - `def test_single_file_conversion(self, test_directories, kcl_average_surface)` (line 272): Test converting a single VTK file to USD. - - `def test_conversion_with_material(self, test_directories, kcl_average_surface)` (line 291): Test conversion with a custom solid color material. - - `def test_conversion_settings(self, test_directories, kcl_average_surface)` (line 319): Test that ConvertVTKToUSD applies correct default stage metadata. -- **class TestIntegration** (line 336): Integration tests combining multiple features. - - `def test_end_to_end_conversion(self, test_directories, kcl_average_surface)` (line 339): Test complete conversion workflow with all features. -- **class TestUnitScaling** (line 362): Verify that VTK mm coordinates are converted to USD meter coordinates. - - `def test_mm_to_m_point_scaling(self, tmp_path)` (line 365): Points written to USD must be 0.001x their original mm values. - - `def test_normals_remain_unit_length(self, tmp_path)` (line 385): Normal vectors must not be scaled. - - `def test_stage_meters_per_unit(self, tmp_path)` (line 405): Stage metersPerUnit metadata must be 1.0. +- **class TestVTKToUSDConversion** (line 268): Test ConvertVTKToUSD on optional real VTK data. + - `def test_single_file_conversion(self, test_directories, kcl_average_surface)` (line 271): Test converting a single VTK file to USD. + - `def test_conversion_with_material(self, test_directories, kcl_average_surface)` (line 290): Test conversion with a custom solid color material. + - `def test_conversion_settings(self, test_directories, kcl_average_surface)` (line 318): Test that ConvertVTKToUSD applies correct default stage metadata. +- **class TestIntegration** (line 335): Integration tests combining multiple features. + - `def test_end_to_end_conversion(self, test_directories, kcl_average_surface)` (line 338): Test complete conversion workflow with all features. +- **class TestUnitScaling** (line 361): Verify that VTK mm coordinates are converted to USD meter coordinates. + - `def test_mm_to_m_point_scaling(self, tmp_path)` (line 364): Points written to USD must be 0.001x their original mm values. + - `def test_normals_remain_unit_length(self, tmp_path)` (line 384): Normal vectors must not be scaled. + - `def test_stage_meters_per_unit(self, tmp_path)` (line 404): Stage metersPerUnit metadata must be 1.0. ## tests/test_workflow_fit_statistical_model_to_patient.py -- `def test_auto_generate_mask_accumulates_multilabel_models(monkeypatch)` (line 16): Multi-model masks accumulate label IDs instead of overwriting prior labels. -- `def test_transform_model_applies_staged_transform()` (line 61): Transform helper updates mesh points with image shape (Z, Y, X) = (3, 3, 3). -- `def test_transform_model_preserves_unstructured_grid_topology()` (line 93): Transform helper preserves cells with image shape (Z, Y, X) = (3, 3, 3). +- `def test_auto_generate_mask_accumulates_multilabel_models(monkeypatch)` (line 19): Multi-model masks accumulate label IDs instead of overwriting prior labels. +- `def test_transform_model_applies_staged_transform()` (line 64): Transform helper updates mesh points with image shape (Z, Y, X) = (3, 3, 3). +- `def test_fit_workflow_default_segmentation_method_is_trimmed_branches()` (line 96): Default segmentation_method must match the KCL-Heart-Model fit contract. +- `def test_fit_workflow_routes_default_to_image_to_vtk_with_trimmed_branches(monkeypatch)` (line 106): When patient_models is omitted, the workflow must invoke +- `def test_image_to_vtk_segmenter_dispatch_for_trimmed_branches()` (line 163): WorkflowConvertImageToVTK('HeartSimplewareTrimmedBranches') must +- `def test_transform_model_preserves_unstructured_grid_topology()` (line 180): Transform helper preserves cells with image shape (Z, Y, X) = (3, 3, 3). ## utils/ai_agent_github_reviews.py diff --git a/docs/api/segmentation/base.rst b/docs/api/segmentation/base.rst index 38e2eff..33d223a 100644 --- a/docs/api/segmentation/base.rst +++ b/docs/api/segmentation/base.rst @@ -99,7 +99,10 @@ New runtime segmentation classes should: 5. Document the key set the segmenter produces; downstream callers should check membership rather than assume a fixed schema. -Keep synthetic tests small and mark real-data tests with ``requires_data``. +Keep synthetic tests small. Real-data tests pull data through the session +fixtures (downloaded on first use); mark GPU- or Simpleware-bound tests with +``requires_gpu`` / ``requires_simpleware`` so they fall into the right +opt-in bucket. See Also ======== diff --git a/docs/contributing.rst b/docs/contributing.rst index f852503..95854c6 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -261,8 +261,14 @@ Run Tests # Run with coverage pytest tests/ --cov=src/physiomotion4d --cov-report=html - # Skip slow tests - pytest tests/ -m "not slow and not requires_data" + # Default invocation auto-skips slow/GPU/Simpleware/experiment/tutorial + pytest tests/ + + # Opt into specific buckets + pytest tests/ --run-slow + pytest tests/ --run-gpu --run-slow # typical local GPU profile + # Self-hosted CI GPU runner enables every bucket: + # --run-gpu --run-slow --run-simpleware --run-experiments --run-tutorials Documentation ============= diff --git a/docs/developer/core.rst b/docs/developer/core.rst index 1af3c46..73167ef 100644 --- a/docs/developer/core.rst +++ b/docs/developer/core.rst @@ -48,7 +48,12 @@ For most code changes, run: .. code-block:: bash - py -m pytest tests/ -m "not slow and not requires_data" -v + py -m pytest tests/ -v + +(Slow / GPU / Simpleware / experiment / tutorial tests are auto-skipped; +opt in with ``--run-slow``, ``--run-gpu``, ``--run-simpleware``, +``--run-experiments``, ``--run-tutorials``. Data-dependent tests download +their data through the session fixtures and run by default.) After public API changes, regenerate the API map: diff --git a/docs/developer/extending.rst b/docs/developer/extending.rst index 6260bdc..82ef11f 100644 --- a/docs/developer/extending.rst +++ b/docs/developer/extending.rst @@ -66,12 +66,15 @@ Documentation Requirements Testing Requirements ==================== -Use synthetic ITK images and small PyVista meshes where possible. Mark tests -that require downloaded or manually prepared data with ``requires_data``. +Use synthetic ITK images and small PyVista meshes where possible. When real +data is unavoidable, request the session fixtures (``test_directories``, +``download_test_data``, ``test_images``) — the data is downloaded on first +use. Mark tests that need a GPU, a slow runtime, or a licensed Simpleware +install with ``requires_gpu``, ``slow``, or ``requires_simpleware``. .. code-block:: bash - py -m pytest tests/ -m "not slow and not requires_data" -v + py -m pytest tests/ -v See Also ======== diff --git a/docs/developer/segmentation.rst b/docs/developer/segmentation.rst index a36b50c..9bf628e 100644 --- a/docs/developer/segmentation.rst +++ b/docs/developer/segmentation.rst @@ -102,7 +102,9 @@ Development Notes * Document the key set the segmenter produces; downstream callers should check membership rather than assume a fixed schema. * Keep tests synthetic unless real model/data behavior is being validated. -* Mark real-data tests with ``requires_data``. +* Real-data tests use the session fixtures (data downloads automatically); + mark GPU- or Simpleware-bound tests with ``requires_gpu`` / + ``requires_simpleware`` so they fall into the right opt-in bucket. See Also ======== diff --git a/docs/testing.rst b/docs/testing.rst index b946fcf..fe25efb 100644 --- a/docs/testing.rst +++ b/docs/testing.rst @@ -2,28 +2,45 @@ Testing ======= -Use the fast, no-real-data test subset during development: +Run the fast test suite during development: .. code-block:: bash - pytest tests/ -m "not slow and not requires_data" -v + pytest tests/ -v -Test Categories -=============== +Slow, GPU, Simpleware, experiment, and tutorial tests are auto-skipped unless +their opt-in flag is passed. Tests that depend on downloadable data fetch it +automatically via the session fixtures, so no marker filter is needed for them. + +Opt-in Buckets +============== + +Each ``--run-`` flag enables one marker family: + +.. code-block:: bash + + pytest tests/ -v --run-slow # tests marked 'slow' + pytest tests/ -v --run-gpu # tests marked 'requires_gpu' + pytest tests/ -v --run-simpleware # tests marked 'requires_simpleware' + pytest tests/ -v --run-experiments # tests marked 'experiment' + pytest tests/ -v --run-tutorials # tests marked 'tutorial' -PhysioMotion4D uses pytest markers and command-line flags to keep expensive -work separate from normal development tests. +Flags compose. A typical local GPU profile is: .. code-block:: bash - # Fast development signal - pytest tests/ -m "not slow and not requires_data" -v + pytest tests/ -v --run-gpu --run-slow - # Include tutorial execution tests - pytest tests/test_tutorials.py --run-tutorials -v +The self-hosted CI GPU runner enables every bucket: - # Include experiment tests - pytest tests/ --run-experiments -v +.. code-block:: bash + + pytest tests/ -v --run-gpu --run-slow --run-simpleware --run-experiments --run-tutorials + +Test Categories +=============== + +.. code-block:: bash # CLI help smoke tests pytest tests/test_cli_smoke.py -v @@ -45,15 +62,21 @@ Specific Areas Real Data and GPU Tests ======================= -Tests that require downloaded or manually prepared datasets are marked -``requires_data``. Tutorial tests are opt-in through ``--run-tutorials`` and -preserve tutorial dependencies, such as Tutorial 4 consuming Tutorial 3 output. +Tests that need downloadable data request the session fixtures +(``test_directories``, ``download_test_data``, ``test_images``); the data is +downloaded on first use, so these tests run by default. GPU-bound tests are +marked ``requires_gpu`` (opt-in via ``--run-gpu``); Simpleware-bound tests are +marked ``requires_simpleware`` (opt-in via ``--run-simpleware`` and require a +licensed Simpleware Medical installation locally). Continuous Integration ====================== -CI should run the fast subset by default and keep data-heavy tutorials and -experiments behind explicit flags. +CI runs the fast subset by default. The self-hosted GPU runner invokes pytest +with every opt-in flag enabled +(``--run-gpu --run-slow --run-simpleware --run-experiments --run-tutorials``); +tests whose host requirements aren't met (e.g. a licensed Simpleware install +on a runner without one) runtime-skip cleanly via their internal guards. See Also ======== diff --git a/experiments/Heart-Statistical_Model_To_Patient/heart_model_to_patient-CHOPValve.py b/experiments/Heart-Statistical_Model_To_Patient/heart_model_to_patient-CHOPValve.py index 3a2c53b..9452dfb 100644 --- a/experiments/Heart-Statistical_Model_To_Patient/heart_model_to_patient-CHOPValve.py +++ b/experiments/Heart-Statistical_Model_To_Patient/heart_model_to_patient-CHOPValve.py @@ -51,7 +51,7 @@ registrar = WorkflowFitStatisticalModelToPatient( template_model=template_model, patient_image=patient_image, - segmentation_method="HeartSimpleware", + segmentation_method="HeartSimplewareTrimmedBranches", ) registrar.set_use_pca_registration( diff --git a/pyproject.toml b/pyproject.toml index 11c2f61..a386eb4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -279,9 +279,9 @@ pythonpath = ["."] markers = [ "unit: marks tests as unit tests (fast, isolated)", "integration: marks tests as integration tests (slower, multiple components)", - "slow: marks tests as slow (deselect with '-m \"not slow\"')", - "requires_gpu: marks tests that require GPU/CUDA support", - "requires_data: marks tests that require external data download", + "slow: marks tests as slow (skipped by default; opt-in via --run-slow)", + "requires_gpu: marks tests that require GPU/CUDA support (opt-in via --run-gpu)", + "requires_simpleware: marks tests that require a local Synopsys Simpleware Medical installation (opt-in via --run-simpleware)", "experiment: marks tests that run experiment scripts (extremely slow, manual only)", "tutorial: marks tests that run tutorial scripts (data/GPU gated, manual only)", "xdist_group: marks tests to run in the same pytest-xdist worker (prevents parallel conflicts)" diff --git a/src/physiomotion4d/cli/convert_image_to_usd.py b/src/physiomotion4d/cli/convert_image_to_usd.py index 58c5f0c..3984682 100644 --- a/src/physiomotion4d/cli/convert_image_to_usd.py +++ b/src/physiomotion4d/cli/convert_image_to_usd.py @@ -69,11 +69,17 @@ def main() -> int: ) parser.add_argument( "--segmentation-method", - choices=["ChestTotalSegmentator", "HeartSimpleware"], + choices=[ + "ChestTotalSegmentator", + "HeartSimpleware", + "HeartSimplewareTrimmedBranches", + ], default="ChestTotalSegmentator", help=( - "Segmentation backend to use: ChestTotalSegmentator (default) " - "or HeartSimpleware." + "Segmentation backend to use: ChestTotalSegmentator (default), " + "HeartSimpleware, or HeartSimplewareTrimmedBranches " + "(HeartSimpleware with pulmonary/great-vessel branches trimmed " + "to the cardiac region)." ), ) parser.add_argument( diff --git a/src/physiomotion4d/cli/convert_image_to_vtk.py b/src/physiomotion4d/cli/convert_image_to_vtk.py index ff29872..fe3d2f4 100644 --- a/src/physiomotion4d/cli/convert_image_to_vtk.py +++ b/src/physiomotion4d/cli/convert_image_to_vtk.py @@ -20,7 +20,11 @@ "contrast", ) -SEGMENTATION_METHODS = ("ChestTotalSegmentator", "HeartSimpleware") +SEGMENTATION_METHODS = ( + "ChestTotalSegmentator", + "HeartSimpleware", + "HeartSimplewareTrimmedBranches", +) def main() -> int: @@ -85,7 +89,10 @@ def main() -> int: default="ChestTotalSegmentator", choices=list(SEGMENTATION_METHODS), help=( - "Segmentation backend. ChestTotalSegmentator (default) | HeartSimpleware" + "Segmentation backend. ChestTotalSegmentator (default) | " + "HeartSimpleware | HeartSimplewareTrimmedBranches " + "(HeartSimpleware with pulmonary/great-vessel branches trimmed " + "to the cardiac region)" ), ) parser.add_argument( diff --git a/src/physiomotion4d/segment_heart_simpleware.py b/src/physiomotion4d/segment_heart_simpleware.py index c9eec86..d1af243 100644 --- a/src/physiomotion4d/segment_heart_simpleware.py +++ b/src/physiomotion4d/segment_heart_simpleware.py @@ -101,7 +101,7 @@ def __init__(self, log_level: int | str = logging.INFO): for label_id, organ_name in organs.items(): self.taxonomy.add_organ(group_name, label_id, organ_name) - self._trim_mask = False + self._trim_branches = False self._finalize_other_group() @@ -128,7 +128,7 @@ def set_trim_branches(self, trim_branches: bool) -> None: Args: trim_branches (bool): Whether to trim branches to the cardiac region. """ - self._trim_mask = trim_branches + self._trim_branches = trim_branches def set_simpleware_executable_path(self, path: str) -> None: """Set the path to the Simpleware Medical console executable. @@ -338,7 +338,7 @@ def segmentation_method(self, preprocessed_image: itk.image) -> itk.image: labelmap_image = itk.GetImageFromArray(labelmap_array.astype(np.uint8)) labelmap_image.CopyInformation(preprocessed_image) - if self._trim_mask: + if self._trim_branches: labelmap_image = self.trim_branches(labelmap_image) return labelmap_image diff --git a/src/physiomotion4d/workflow_convert_image_to_usd.py b/src/physiomotion4d/workflow_convert_image_to_usd.py index aa0e77b..443972d 100644 --- a/src/physiomotion4d/workflow_convert_image_to_usd.py +++ b/src/physiomotion4d/workflow_convert_image_to_usd.py @@ -32,6 +32,7 @@ SEGMENTATION_METHODS: tuple[str, ...] = ( "ChestTotalSegmentator", "HeartSimpleware", + "HeartSimplewareTrimmedBranches", ) #: Supported registration backend identifiers. @@ -44,6 +45,17 @@ class WorkflowConvertImageToUSD(PhysioMotion4DBase): This class implements the full workflow from 4D CT images to painted USD files suitable for visualization in NVIDIA Omniverse. + + **Segmentation backends** (``segmentation_method``): + + - ``'ChestTotalSegmentator'`` — :class:`SegmentChestTotalSegmentator`. + - ``'HeartSimpleware'`` — :class:`SegmentHeartSimpleware`. **Behavior + change**: this workflow previously called ``set_trim_branches(True)`` + for this option implicitly. It no longer does — for the trimmed + behavior, use ``'HeartSimplewareTrimmedBranches'``. + - ``'HeartSimplewareTrimmedBranches'`` — :class:`SegmentHeartSimpleware` + with branch trimming enabled, matching the KCL-Heart-Model template + extent. """ def __init__( @@ -78,7 +90,9 @@ def __init__( reference_image_filename (Optional[str]): Path to reference image file number_of_registration_iterations (Optional[int]): Number of registration iterations segmentation_method (str): Segmentation backend to use: - ``'ChestTotalSegmentator'`` (default) or ``'HeartSimpleware'``. + ``'ChestTotalSegmentator'`` (default), ``'HeartSimpleware'``, + or ``'HeartSimplewareTrimmedBranches'`` (HeartSimpleware with + pulmonary/great-vessel branches trimmed to the cardiac region). registration_method (str): Registration method to use: ``'ANTS'`` or ``'ICON'`` (default: ``'ICON'``). log_level: Logging level (default: logging.INFO) @@ -127,10 +141,17 @@ def __init__( chest_segmenter = SegmentChestTotalSegmentator(log_level=log_level) chest_segmenter.contrast_threshold = 500 self.segmenter = chest_segmenter - else: # HeartSimpleware + elif self.segmentation_method in ( + "HeartSimpleware", + "HeartSimplewareTrimmedBranches", + ): heart_segmenter = SegmentHeartSimpleware(log_level=log_level) - heart_segmenter.set_trim_branches(True) + heart_segmenter.set_trim_branches( + self.segmentation_method == "HeartSimplewareTrimmedBranches" + ) self.segmenter = heart_segmenter + else: + raise ValueError(f"Unknown segmentation method: {self.segmentation_method}") # Initialize registration method self.registrar: RegisterImagesBase diff --git a/src/physiomotion4d/workflow_convert_image_to_vtk.py b/src/physiomotion4d/workflow_convert_image_to_vtk.py index 2991ddf..8617701 100644 --- a/src/physiomotion4d/workflow_convert_image_to_vtk.py +++ b/src/physiomotion4d/workflow_convert_image_to_vtk.py @@ -52,6 +52,7 @@ SEGMENTATION_METHODS: tuple[str, ...] = ( "ChestTotalSegmentator", "HeartSimpleware", + "HeartSimplewareTrimmedBranches", ) @@ -63,7 +64,13 @@ class WorkflowConvertImageToVTK(PhysioMotion4DBase): - ``'ChestTotalSegmentator'`` — :class:`SegmentChestTotalSegmentator` (CPU-capable, default). - ``'HeartSimpleware'`` — :class:`SegmentHeartSimpleware` (cardiac only; - requires a Simpleware Medical installation). + requires a Simpleware Medical installation). **Behavior change**: this + workflow previously called ``set_trim_branches(True)`` for this option + implicitly. It no longer does — for the trimmed behavior, use + ``'HeartSimplewareTrimmedBranches'`` below. + - ``'HeartSimplewareTrimmedBranches'`` — :class:`SegmentHeartSimpleware` + with :meth:`SegmentHeartSimpleware.set_trim_branches` set to ``True``, + trimming pulmonary and great-vessel branches to the cardiac region. **Output anatomy groups** @@ -104,7 +111,9 @@ def __init__( Args: segmentation_method: Segmentation backend to use. One of - ``'ChestTotalSegmentator'`` (default) or ``'HeartSimpleware'``. + ``'ChestTotalSegmentator'`` (default), ``'HeartSimpleware'``, + or ``'HeartSimplewareTrimmedBranches'`` (HeartSimpleware with + pulmonary/great-vessel branches trimmed to the cardiac region). log_level: Logging level. Default: ``logging.INFO``. Raises: @@ -144,11 +153,16 @@ def _create_segmenter(self) -> SegmentAnatomyBase: ) return SegmentChestTotalSegmentator(log_level=self.log_level) - if self.segmentation_method_name == "HeartSimpleware": + if self.segmentation_method_name in ( + "HeartSimpleware", + "HeartSimplewareTrimmedBranches", + ): from physiomotion4d.segment_heart_simpleware import SegmentHeartSimpleware segmenter = SegmentHeartSimpleware(log_level=self.log_level) - segmenter.set_trim_branches(True) + segmenter.set_trim_branches( + self.segmentation_method_name == "HeartSimplewareTrimmedBranches" + ) return segmenter raise ValueError( f"Unknown segmentation method: {self.segmentation_method_name}" diff --git a/src/physiomotion4d/workflow_fit_statistical_model_to_patient.py b/src/physiomotion4d/workflow_fit_statistical_model_to_patient.py index 72945bc..3de73d2 100644 --- a/src/physiomotion4d/workflow_fit_statistical_model_to_patient.py +++ b/src/physiomotion4d/workflow_fit_statistical_model_to_patient.py @@ -137,7 +137,7 @@ def __init__( template_model: pv.DataSet, patient_models: list[pv.DataSet] | None = None, patient_image: Optional[itk.Image] = None, - segmentation_method: str = "HeartSimpleware", + segmentation_method: str = "HeartSimplewareTrimmedBranches", log_level: int | str = logging.INFO, ): """Initialize the model-to-image-and-model registration pipeline. @@ -151,9 +151,11 @@ def __init__( via create_reference_image (contour_tools). segmentation_method: Segmentation backend used by WorkflowConvertImageToVTK when patient_models is None and - patient_image is provided. One of ``'HeartSimpleware'`` (default) - or ``'ChestTotalSegmentator'``. Ignored when patient_models is - supplied. + patient_image is provided. One of + ``'HeartSimplewareTrimmedBranches'`` (default — produces + cardiac extent that matches KCL-Heart-Model templates), + ``'HeartSimpleware'``, or ``'ChestTotalSegmentator'``. + Ignored when patient_models is supplied. log_level: Logging level (logging.DEBUG, logging.INFO, logging.WARNING). Default: logging.INFO """ diff --git a/tests/EXPERIMENT_FLAG_USAGE.md b/tests/EXPERIMENT_FLAG_USAGE.md deleted file mode 100644 index 53ac8de..0000000 --- a/tests/EXPERIMENT_FLAG_USAGE.md +++ /dev/null @@ -1,162 +0,0 @@ -# Using the --run-experiments Flag - -This document explains how the `--run-experiments` flag works and why it exists. - -## Purpose - -The `--run-experiments` flag provides protection against accidentally running extremely long-running experiment script tests. These tests can take 20+ hours to complete and are resource-intensive. - -## How It Works - -### Automatic Protection - -Experiment tests are **automatically skipped** in all these scenarios: - -```bash -# All of these will SKIP experiment tests: -pytest tests/ -pytest tests/ -v -pytest tests/test_experiments.py -pytest tests/test_experiments.py -v -pytest tests/ -m experiment # Even with the marker! -pytest -v tests/ -``` - -### Explicit Opt-In Required - -To run experiment tests, you MUST use the `--run-experiments` flag: - -```bash -# This is the ONLY way experiment tests will run: -pytest tests/test_experiments.py -v --run-experiments - -# Or with specific tests: -pytest tests/test_experiments.py::test_experiment_colormap_vtk_to_usd -v --run-experiments - -# Or with marker filters: -pytest tests/test_experiments.py -m "experiment and not requires_gpu" -v --run-experiments -``` - -## Implementation Details - -The protection is implemented in `tests/conftest.py` using pytest hooks: - -1. **`pytest_addoption`**: Registers the `--run-experiments` command-line option -2. **`pytest_configure`**: Registers the `experiment` marker -3. **`pytest_collection_modifyitems`**: Automatically skips tests marked with `@pytest.mark.experiment` unless `--run-experiments` is provided - -### Code Snippet - -```python -def pytest_collection_modifyitems(config, items): - """Automatically skip experiment tests unless --run-experiments is passed.""" - if config.getoption('--run-experiments'): - return # User explicitly requested experiment tests - - # Skip all tests marked with @pytest.mark.experiment - skip_experiments = pytest.mark.skip( - reason='Experiment tests require --run-experiments flag to run' - ) - for item in items: - if 'experiment' in item.keywords: - item.add_marker(skip_experiments) -``` - -## Benefits - -### 1. Prevents Accidental Execution - -Developers and CI/CD systems won't accidentally run 20+ hour test suites: - -```bash -# Safe - won't run experiment tests -pytest tests/ -v -``` - -### 2. Clear Intent Required - -When someone uses `--run-experiments`, it's clear they understand what they're doing: - -```bash -# Intentional - user knows this will take hours -pytest tests/test_experiments.py -v --run-experiments -``` - -### 3. Self-Documenting - -The flag name makes the purpose obvious: -- `--run-experiments` clearly indicates these are experiment-related tests -- Error message tells users what flag to use if they need the tests - -### 4. CI/CD Protection - -CI/CD workflows can never accidentally trigger these tests because they'll never include the `--run-experiments` flag. - -## Viewing Skipped Tests - -To see that experiment tests are being skipped: - -```bash -# Show skipped tests -pytest tests/test_experiments.py -v - -# Sample output: -# tests/test_experiments.py::test_experiment_colormap_vtk_to_usd SKIPPED -# tests/test_experiments.py::test_experiment_reconstruct_4dct SKIPPED -# ... -``` - -To see why they're skipped: - -```bash -# Show skip reasons -pytest tests/test_experiments.py -v -rs - -# Sample output: -# SKIPPED [6] conftest.py:XX: Experiment tests require --run-experiments flag to run -``` - -## Adding More Protected Tests - -To protect additional long-running tests, simply: - -1. Mark them with `@pytest.mark.experiment` -2. They'll automatically be protected by the same mechanism - -```python -@pytest.mark.experiment -@pytest.mark.slow -def test_my_long_running_experiment(): - """This test requires --run-experiments flag.""" - # ... test code ... -``` - -## Removing Protection (Not Recommended) - -If you ever need to disable this protection (not recommended): - -1. Edit `tests/conftest.py` -2. Remove or comment out the `pytest_collection_modifyitems` function -3. Tests will run with standard pytest commands - -**Warning:** Removing this protection means developers might accidentally run 20+ hour test suites. - -## Test-Mode Flag: PHYSIOMOTION_RUNNING_AS_TEST - -When experiment tests run (with `--run-experiments`), the test runner also sets **`PHYSIOMOTION_RUNNING_AS_TEST=1`** so that scripts can use reduced parameters (e.g. fewer iterations, fewer files) and finish faster. Scripts should read this variable and choose quick vs full parameters accordingly. See [EXPERIMENT_TESTS_GUIDE.md](EXPERIMENT_TESTS_GUIDE.md#running-as-test-physiomotion_running_as_test) for the recommended check and the `physiomotion4d.test_tools.TestTools.running_as_test()` helper. - -## Related Documentation - -- **[EXPERIMENT_TESTS_GUIDE.md](EXPERIMENT_TESTS_GUIDE.md)** - Complete guide to experiment tests (including PHYSIOMOTION_RUNNING_AS_TEST) -- **[README.md](README.md)** - Main testing documentation -- **[conftest.py](conftest.py)** - Pytest configuration implementation - -## Summary - -- Experiment tests are **opt-in only** -- Requires explicit `--run-experiments` flag -- Automatically skipped without the flag -- Protected from accidental CI/CD execution -- Clear, self-documenting behavior - -**Remember:** If you see experiment tests being skipped and want to run them, add `--run-experiments` to your pytest command! diff --git a/tests/EXPERIMENT_TESTS_GUIDE.md b/tests/EXPERIMENT_TESTS_GUIDE.md deleted file mode 100644 index 1cbf847..0000000 --- a/tests/EXPERIMENT_TESTS_GUIDE.md +++ /dev/null @@ -1,465 +0,0 @@ -# Experiment Tests Guide - -This guide explains how to use the automated test suite for PhysioMotion4D experiment scripts. - -## Overview - -The `test_experiments.py` module provides automated testing for all percent-cell -Python scripts in the `experiments/` directory. Each subdirectory gets its own -test that executes every `*.py` script in alphanumeric order. - -**WARNING:** These tests are **extremely long-running** and may take multiple -hours to complete. - -**PROTECTION:** Experiment tests are **opt-in only**. They will NOT run with -standard pytest commands like `pytest tests/` or `pytest -v tests/`. You must -explicitly pass the `--run-experiments` flag to run them. - -## Quick Start - -### List Available Experiments - -```bash -# See all scripts that would be run (without executing) -pytest tests/test_experiments.py::test_list_scripts_in_subdir -v -s --run-experiments - -# Validate experiment directory structure -pytest tests/test_experiments.py::test_experiment_structure -v --run-experiments -``` - -### Run All Experiment Tests - -```bash -# Run ALL experiments (may take many hours!) -pytest tests/test_experiments.py -v --run-experiments - -# Or run with marker filter (also works) -pytest tests/test_experiments.py -v -m experiment --run-experiments -``` - -**IMPORTANT:** Experiment tests require the `--run-experiments` flag to run. -Without this flag, they are automatically skipped. - -### Run Individual Experiments - -```bash -# Colormap VTK to USD (~2 hours) -pytest tests/test_experiments.py::test_experiment_colormap_vtk_to_usd -v -s --run-experiments - -# Reconstruct 4D CT (~4 hours, requires GPU) -pytest tests/test_experiments.py::test_experiment_reconstruct_4dct -v -s --run-experiments - -# Heart VTK Series to USD (~3 hours, requires data) -pytest tests/test_experiments.py::test_experiment_heart_vtk_series_to_usd -v -s --run-experiments - -# Heart Gated CT to USD (~6 hours, requires GPU & data) -pytest tests/test_experiments.py::test_experiment_heart_gated_ct_to_usd -v -s --run-experiments - -# Convert VTK to USD (~2 hours, requires data) -pytest tests/test_experiments.py::test_experiment_convert_vtk_to_usd -v -s --run-experiments - -# Create Statistical Model (~3 hours, requires data, includes manual steps) -pytest tests/test_experiments.py::test_experiment_create_statistical_model -v -s --run-experiments - -# Heart Statistical Model to Patient (~4 hours, requires GPU & data) -pytest tests/test_experiments.py::test_experiment_heart_statistical_model_to_patient -v -s --run-experiments - -# Lung Gated CT to USD (~6 hours, requires GPU & data) -pytest tests/test_experiments.py::test_experiment_lung_gated_ct_to_usd -v -s --run-experiments - -# DISABLED (scripts not ready): -# - test_experiment_displacement_field_to_usd -# - test_experiment_lung_vessels_airways -``` - -**NOTE:** All experiment tests require the `--run-experiments` flag. Without -it, they are automatically skipped. - -## Test Details - -### Available Experiment Tests - -| Test Name | Subdirectory | Expected Duration | Requirements | Status | -| ---------------------------------------------------- | ------------------------------------- | ----------------- | -------------- | ---------- | -| `test_experiment_colormap_vtk_to_usd` | `Colormap-VTK_To_USD/` | ~2 hours | Basic | Active | -| `test_experiment_convert_vtk_to_usd` | `Convert_VTK_To_USD/` | ~2 hours | Data | Active | -| ~~`test_experiment_displacement_field_to_usd`~~ | ~~`DisplacementField_To_USD/`~~ | ~~~2 hours~~ | ~~Basic~~ | Disabled | -| `test_experiment_reconstruct_4dct` | `Reconstruct4DCT/` | ~4 hours | GPU | Active | -| `test_experiment_heart_vtk_series_to_usd` | `Heart-VTKSeries_To_USD/` | ~3 hours | Data | Active | -| `test_experiment_heart_gated_ct_to_usd` | `Heart-GatedCT_To_USD/` | ~6 hours | GPU + Data | Active | -| `test_experiment_create_statistical_model` | `Heart-Create_Statistical_Model/` | ~3 hours | Data | Active | -| `test_experiment_heart_statistical_model_to_patient` | `Heart-Statistical_Model_To_Patient/` | ~4 hours | GPU + Data | Active | -| `test_experiment_lung_gated_ct_to_usd` | `Lung-GatedCT_To_USD/` | ~6 hours | GPU + Data | Active | -| ~~`test_experiment_lung_vessels_airways`~~ | ~~`Lung-VesselsAirways/`~~ | ~~~2 hours~~ | ~~GPU + Data~~ | Disabled | - -**Note:** Disabled tests are commented out in the code and will not run. They -can be re-enabled when the scripts are ready. - -### Execution Order - -Within each subdirectory, scripts are executed in **alphanumeric order** -(the runner uses `sorted(subdir.glob("*.py"))`): - -- `0-download_and_convert_4d_to_3d.py` (runs first) -- `1-register_images.py` (runs second) -- `2-generate_segmentation.py` (runs third) -- etc. - -This ensures that scripts with dependencies run in the correct sequence. - -## Requirements - -### System Requirements - -- **CPU:** Multi-core processor (8+ cores recommended) -- **RAM:** 32GB minimum (64GB recommended for large experiments) -- **GPU:** NVIDIA GPU with CUDA support (RTX 3090 or better recommended) -- **Disk:** 100GB+ free space for data and outputs -- **OS:** Linux (Ubuntu 20.04+) or Windows 10/11 - -### Software Requirements - -```bash -# Install all dependencies -pip install -e ".[test]" - -# Or with uv (recommended) -uv pip install -e ".[test]" -``` - -### Data Requirements - -Some experiments require external data downloads: -- Heart experiments: Slicer-Heart-CT dataset (~1.2GB) -- Lung experiments: DirLab 4DCT dataset (varies by case) - -Data is typically downloaded automatically by the first script in each sequence. - -## Test Markers - -All experiment tests are marked with: - -- `@pytest.mark.experiment` - Identifies as experiment test (excludes from CI/CD) -- `@pytest.mark.slow` - Indicates long-running test -- `@pytest.mark.requires_gpu` - Requires CUDA-capable GPU (when applicable) -- `@pytest.mark.requires_data` - Requires external data download (when applicable) -- `@pytest.mark.timeout(seconds)` - Sets maximum execution time - -## Running as Test: PHYSIOMOTION_RUNNING_AS_TEST - -When you run experiment tests with `pytest ... --run-experiments`, the test -runner sets the environment variable **`PHYSIOMOTION_RUNNING_AS_TEST=1`** before -executing each script. Scripts can read this to use **reduced parameters** -(fewer iterations, fewer files, smaller resolution) so test runs complete in -reasonable time. - -### How it works - -- **Test runner** ([test_experiments.py](test_experiments.py)): `execute_script()` - passes `env` with `PHYSIOMOTION_RUNNING_AS_TEST=1` to the subprocess that runs - the script as a standalone Python file, so the script process sees the variable. -- **Scripts**: Early in the script (after imports or with other config), - compute a boolean and use it to choose quick vs full parameters. - -### Recommended check in scripts - -Use either of these: - -**Option 1 - inline (stdlib only):** - -```python -import os - -running_as_test = os.environ.get("PHYSIOMOTION_RUNNING_AS_TEST", "").lower() in ("1", "true", "yes") -``` - -**Option 2 - shared helper (recommended):** - -```python -from physiomotion4d.test_tools import TestTools - -# Then use TestTools.running_as_test() where you need it, e.g.: -quick_run = TestTools.running_as_test() -max_iterations = 100 if TestTools.running_as_test() else 2000 -``` - -### Semantics - -- **Truthy values** (case-insensitive): `1`, `true`, `yes` -> script should use - fast/small parameters. -- **Unset or falsy**: use full parameters (normal interactive or production run). - -Scripts that support this will run quickly when executed as tests and at full -fidelity when run manually. - -## Usage Tips - -### Run with Detailed Output - -```bash -# Show all output from scripts (recommended) -pytest tests/test_experiments.py::test_experiment_heart_gated_ct_to_usd -v -s -``` - -The `-s` flag shows all stdout/stderr, including: -- Script execution progress -- Cell outputs (`# %%` percent-cell scripts are run as ordinary Python) -- Error messages -- Execution summaries - -### Monitor Progress - -Each script execution prints: -``` -================================================================================ -Executing script: 1-register_images.py -Path: experiments/Heart-GatedCT_To_USD/1-register_images.py -Timeout: 5400 seconds (90 minutes) -================================================================================ - -... (script output) ... - -Successfully executed: 1-register_images.py -``` - -### Handle Failures - -If a script fails: -1. Check the error output in the pytest summary -2. Open the script to inspect the failing cell -3. Fix the issue (code, data, environment) -4. Re-run the specific test - -The scripts are executed as ordinary Python (`python