Skip to content

ENH: AnatomyTaxonomy, USD pipeline hardening, tutorials + docs#49

Merged
aylward merged 11 commits into
Project-MONAI:mainfrom
aylward:tutorials_testing
May 13, 2026
Merged

ENH: AnatomyTaxonomy, USD pipeline hardening, tutorials + docs#49
aylward merged 11 commits into
Project-MONAI:mainfrom
aylward:tutorials_testing

Conversation

@aylward
Copy link
Copy Markdown
Collaborator

@aylward aylward commented May 13, 2026

Replaces the per-organ flat dicts on SegmentAnatomyBase and USDAnatomyTools with a shared AnatomyTaxonomy data type so new segmenters can register custom groups via taxonomy.add_organ without subclassing either class. Adds anatomy-type grouping to ConvertVTKToUSD's labeled output, ships tutorials 07-09 (DirLab PCA + PhysicsNeMo), and fixes several USD- pipeline issues that prevented clean rendering in Omniverse Kit.

Anatomy / segmentation

  • AnatomyTaxonomy + AnatomyGroup (new). SegmentAnatomyBase owns one; subclasses populate via taxonomy.add_organ and call _finalize_other_group. Eight legacy *_mask_ids attributes and set_other_and_all_mask_ids removed; label_to_type delegates to the taxonomy. segment() now returns only the groups the segmenter registered (caller-checks contract; tests updated).
  • SegmentHeartSimpleware: graceful fallback when ASCardio omits landmarks.csv; only the groups it actually populates are returned.

USD generation

  • ConvertVTKToUSD: new segmenter= parameter groups labeled prims under /World/{basename}/{type}/{label} and materials under /World/Looks/{type}/{label}_material. Preserves cell-data primvars; new compute_von_mises_stress helper for 9-component stress tensors; framing camera with tight near-clip; stale USD-layer eviction; triangulation face-map propagated to uniform primvars so per-cell arrays survive quad-to-triangle splits.
  • USDAnatomyTools: DEFAULT_RENDER_PARAMS promoted to a module-level constant so CLIs/tests can enumerate types without a stage; per- instance deep copy keeps mutations isolated; enhance_meshes now a single taxonomy loop with organ-level overrides as data instead of imperative carve-outs. MaterialBindingAPI.Apply() so Omniverse RTX consumes the binding (was rendering white).
  • CLI convert_vtk_to_usd: ANATOMY_TYPES sourced from DEFAULT_RENDER_PARAMS (auto-syncs with future additions); "kidney" alias preserved.

Tutorials / experiments

  • Tutorials 07-09 added (DirLab PCA lung-lobe model, time-series propagation, PhysicsNeMo mesh-stage training).
  • Tutorials 02-09 gain if name == "main" guards (Windows nnUNet spawn) matching tutorial_01; experiments gain the same guards plus Path(file)-anchored paths; valve scripts wire compute_von_mises_stress.
  • notebook_utils.py removed (last consumers gone); experiment scripts and READMEs scrubbed of .ipynb references; test runner already on *.py glob.

Tests, docs, packaging

  • New test_anatomy_taxonomy (12 tests); mask_ids / Simpleware assertions updated to the caller-checks contract; tutorial test harness rewritten.
  • Segmentation .rst pages, developer/segmentation.rst, and developer/ usd_generation.rst updated to document AnatomyTaxonomy, the type-grouped layout, von Mises helper, framing camera, and the three ways to register a new group's look.
  • New CLI .rst pages, simpleware/greedy API pages, vtk_to_usd_lib page, data_download / test_tools utility pages; API_MAP.md regenerated.
  • mypy: 7 errors fixed (Optional render-params indexing, redundant np.asarray casts, pyvista Plotter binding via Any cast, matplotlib colormap return typing). Full src/ tree clean.

Other

  • data_download_tools.py extracted (was notebook_utils.py glue).
  • pyproject.toml mypy overrides tightened.
  • Stale baseline PNGs removed (regenerated by test_tools at runtime).

Summary by CodeRabbit

  • New Features

    • Added data download and verification tools for sample datasets.
    • Introduced anatomy taxonomy system for organizing anatomical structures in segmentation outputs.
    • Enhanced USD conversion with anatomy-aware grouping and automatic camera framing.
    • Added von Mises stress computation for visualization.
  • Documentation

    • Upgraded tutorials and experiments from Jupyter notebooks to executable Python scripts with percent-cell format.
    • Restructured API documentation with new module pages and improved navigation.
  • Chores

    • Raised minimum Python requirement to 3.11+.

Replaces the per-organ flat dicts on SegmentAnatomyBase and USDAnatomyTools
with a shared AnatomyTaxonomy data type so new segmenters can register
custom groups via taxonomy.add_organ without subclassing either class.
Adds anatomy-type grouping to ConvertVTKToUSD's labeled output, ships
tutorials 07-09 (DirLab PCA + PhysicsNeMo), and fixes several USD-
pipeline issues that prevented clean rendering in Omniverse Kit.

Anatomy / segmentation
- AnatomyTaxonomy + AnatomyGroup (new). SegmentAnatomyBase owns one;
  subclasses populate via taxonomy.add_organ and call
  _finalize_other_group. Eight legacy *_mask_ids attributes and
  set_other_and_all_mask_ids removed; label_to_type delegates to the
  taxonomy. segment() now returns only the groups the segmenter
  registered (caller-checks contract; tests updated).
- SegmentHeartSimpleware: graceful fallback when ASCardio omits
  landmarks.csv; only the groups it actually populates are returned.

USD generation
- ConvertVTKToUSD: new segmenter= parameter groups labeled prims under
  /World/{basename}/{type}/{label} and materials under
  /World/Looks/{type}/{label}_material. Preserves cell-data primvars;
  new compute_von_mises_stress helper for 9-component stress tensors;
  framing camera with tight near-clip; stale USD-layer eviction;
  triangulation face-map propagated to uniform primvars so per-cell
  arrays survive quad-to-triangle splits.
- USDAnatomyTools: DEFAULT_RENDER_PARAMS promoted to a module-level
  constant so CLIs/tests can enumerate types without a stage; per-
  instance deep copy keeps mutations isolated; enhance_meshes now a
  single taxonomy loop with organ-level overrides as data instead of
  imperative carve-outs. MaterialBindingAPI.Apply() so Omniverse RTX
  consumes the binding (was rendering white).
- CLI convert_vtk_to_usd: ANATOMY_TYPES sourced from
  DEFAULT_RENDER_PARAMS (auto-syncs with future additions); "kidney"
  alias preserved.

Tutorials / experiments
- Tutorials 07-09 added (DirLab PCA lung-lobe model, time-series
  propagation, PhysicsNeMo mesh-stage training).
- Tutorials 02-09 gain if __name__ == "__main__" guards (Windows
  nnUNet spawn) matching tutorial_01; experiments gain the same
  guards plus Path(__file__)-anchored paths; valve scripts wire
  compute_von_mises_stress.
- notebook_utils.py removed (last consumers gone); experiment scripts
  and READMEs scrubbed of .ipynb references; test runner already on
  *.py glob.

Tests, docs, packaging
- New test_anatomy_taxonomy (12 tests); mask_ids / Simpleware
  assertions updated to the caller-checks contract; tutorial test
  harness rewritten.
- Segmentation .rst pages, developer/segmentation.rst, and developer/
  usd_generation.rst updated to document AnatomyTaxonomy, the
  type-grouped layout, von Mises helper, framing camera, and the
  three ways to register a new group's look.
- New CLI .rst pages, simpleware/greedy API pages, vtk_to_usd_lib
  page, data_download / test_tools utility pages; API_MAP.md
  regenerated.
- mypy: 7 errors fixed (Optional render-params indexing, redundant
  np.asarray casts, pyvista Plotter binding via Any cast,
  matplotlib colormap return typing). Full src/ tree clean.

Other
- data_download_tools.py extracted (was notebook_utils.py glue).
- pyproject.toml mypy overrides tightened.
- Stale baseline PNGs removed (regenerated by test_tools at runtime).
Copilot AI review requested due to automatic review settings May 13, 2026 04:04
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 13, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

This PR introduces anatomy taxonomy data modeling for segmentation, adds optional dataset download utilities, refactors segmentation classes to use taxonomy instead of hardcoded mask IDs, enhances USD rendering with anatomy-aware materials and von Mises stress derivation, modernizes tutorials to percent-cell Python scripts, and upgrades minimum Python to 3.11 with corresponding CI/documentation updates.

Changes

Anatomy Taxonomy System & Segmentation Refactoring

Layer / File(s) Summary
Anatomy taxonomy data model
src/physiomotion4d/anatomy_taxonomy.py, src/physiomotion4d/__init__.py, tests/test_anatomy_taxonomy.py
AnatomyTaxonomy and AnatomyGroup provide a pure-data structure for managing organ-label groupings, with methods for registration, lookup, aggregation, and populating unclaimed IDs into a sentinel "other" group. Unit tests cover lazy group creation, ordering preservation, and non-aliasing semantics.
Segmentation base refactoring to use taxonomy
src/physiomotion4d/segment_anatomy_base.py, src/physiomotion4d/segment_chest_total_segmentator.py, src/physiomotion4d/segment_heart_simpleware.py, tests/test_segment_*.py
SegmentAnatomyBase replaces per-organ mask-ID dictionaries with a taxonomy instance, provides _finalize_other_group() to populate unclaimed IDs, and updates the segment() return contract to {"labelmap": ..., **group_masks}. Subclasses register labels via taxonomy.add_organ(...). Tests validate group-based label lookups and output keys.
Segmentation-aware USD conversion
src/physiomotion4d/convert_vtk_to_usd.py, tests/test_convert_vtk_to_usd.py
ConvertVTKToUSD now accepts an optional segmenter parameter to organize labeled meshes and materials under anatomy group hierarchies (e.g., /World/.../heart/... prims with corresponding /World/Looks/heart/... materials). Tests assert correct prim/material path nesting and fallback to flat Anatomy grouping when segmenter is absent.

Data Download & Test Infrastructure Modernization

Layer / File(s) Summary
Dataset download tools
src/physiomotion4d/data_download_tools.py, src/physiomotion4d/__init__.py, tests/test_download_heart_data.py, docs/api/utilities/data_download.rst
DataDownloadTools provides optional download/verification helpers for Slicer-Heart-CT (via streaming urlopen with atomic replacement) and verification predicates for CHOP-Valve4D, DirLab-4DCT, and KCL-Heart-Model datasets. Unit tests exercise verification via minimal filesystem layouts.
Test utilities refactoring
src/physiomotion4d/test_tools.py, src/physiomotion4d/notebook_utils.py (removed), tests/EXPERIMENT_FLAG_USAGE.md, tests/EXPERIMENT_TESTS_GUIDE.md
TestTools constructor signature changes to take class_name first with optional results_dir/baselines_dir; new running_as_test() static method reads PHYSIOMOTION_RUNNING_AS_TEST; new save_screenshot_openusd(...) renders USD off-screen via USDTools.load_usd_as_vtk. Removed notebook_utils.running_as_test() in favor of the unified TestTools helper. Documentation guides updated to reference the new approach.

USD Rendering & Visualization Enhancements

Layer / File(s) Summary
Von Mises stress derivation
src/physiomotion4d/vtk_to_usd/primvar_derivations.py, src/physiomotion4d/convert_vtk_to_usd.py, tests/test_convert_vtk_to_usd.py
New module primvar_derivations.py exports compute_von_mises_stress(...) to derive scalar fields from 9-component stress tensors and PRIMVAR_DERIVATIONS registry for extensibility. ConvertVTKToUSD.compute_von_mises_stress(...) method applies derivation and invalidates cached meshes. Tests verify stress-to-scalar mapping in conversion output.
Framing camera and USD layer management
src/physiomotion4d/vtk_to_usd/usd_utils.py, src/physiomotion4d/vtk_to_usd/converter.py, src/physiomotion4d/usd_tools.py, src/physiomotion4d/transform_tools.py
add_framing_camera(...) new helper creates a camera prim with bounding-box-derived placement; automatically invoked before USD export in converter.py, usd_tools.py merge/save/flatten operations, and transform_tools transform visualization. Pre-clears stale Sdf.Layer cache before stage creation to avoid CreateNew conflicts.
USD-to-VTK round-trip loading
src/physiomotion4d/usd_tools.py, tests/test_vtk_to_usd_library.py
USDTools.load_usd_as_vtk(...) loads USD meshes as PyVista PolyData, samples at optional time_code, transforms to world space, triangulates faces, and attaches per-vertex openusd_rgb colors. Helper _usd_display_color(...) reads/normalizes displayColor primvars with red fallback. Test verifies round-trip render/load of simple mesh.
Anatomy-aware USD material management
src/physiomotion4d/usd_anatomy_tools.py, src/physiomotion4d/vtk_to_usd/material_manager.py, docs/developer/usd_generation.rst
USDAnatomyTools refactored to use module-level DEFAULT_RENDER_PARAMS dict (group + organ-level overrides) instead of per-instance attributes; enhance_meshes() now iterates segmenter taxonomy to build organ→params mapping and applies materials only to matching prim names. MaterialManager.bind_material now properly applies MaterialBindingAPI to prim apiSchemas. Documentation guides anatomy-aware USD conversion and render-params customization.

Tutorial Script Modernization

Layer / File(s) Summary
Tutorial 01–06 refactoring
tutorials/tutorial_0[1-6]_*.py, tests/test_tutorials.py, tutorials/README.md
Tutorials converted from function-based (run_tutorial(...)) with CLI args to guarded __main__ scripts with fixed repo-relative paths. Each script sets test_mode via TestTools.running_as_test(), reads data from data/test/... or full data/..., runs workflow, and writes results and screenshots to tutorials/output/tutorial_0X/. Test runner simplified to execute scripts via runpy.run_path() and compare output. Added _run_tutorial_script() helper and _enable_tutorial_test_mode fixture for environment setup.
Tutorial 07–09 new scripts
tutorials/tutorial_07_dirlab_pca_model.py, tutorials/tutorial_08_dirlab_pca_time_series.py
New scripts build DirLab-based lung PCA models (Tutorial 7: extract lobe surfaces, train PCA, fit to meshes; Tutorial 8: propagate fitted meshes through 4D time-series via RegisterTimeSeriesImages). Both use __main__ guarded execution, test-mode data selection, and produce aggregated tutorial_results dicts for harness consumption.
Experiment script updates
experiments/*/*.py, tests/EXPERIMENT_*.md
All experiment scripts wrapped in if __name__ == "__main__": guards to prevent multiprocessing re-import issues. Switched test-mode detection from running_as_test (removed from notebook_utils) to TestTools.running_as_test(). Updated docstrings/README to describe # %% percent-cell Python scripts instead of notebooks. Test execution now uses subprocess invocation (python <script>.py) with alphanumeric ordering per subdirectory.

Python Version Upgrade & Build Configuration

Layer / File(s) Summary
Python 3.11 requirement
pyproject.toml, .github/workflows/ci.yml, .github/workflows/release.yml, docs/faq.rst, docs/contributing.rst
Minimum Python bumped from 3.10 to 3.11; requires-python = ">=3.11" and classifier updated. CI unit tests run on Python 3.11 and 3.12 (removed 3.10). Release and code-quality jobs now build with Python 3.11. Coverage upload targets Python 3.11. Contributing guide updated to reflect 3.11 requirement. FAQ updated to list supported 3.11/3.12.
Dependency updates
pyproject.toml, data/Slicer-Heart-CT/download_and_convert.py
Added nvidia-physicsnemo>=2.0.0,<3.0.0 and ipykernel>=6.0.0; upgraded pyvista[all] from >=0.43.0 to >=0.47.0. Scripts now use DataDownloadTools instead of hardcoded URLs and urllib.request.

Documentation Reorganization & Guides

Layer / File(s) Summary
API reference restructuring
docs/api/*/, docs/API_MAP.md, docs/README.md
Added 33 new Sphinx API documentation pages (CLI entry-points, registration/segmentation/USD modules) with .. module:: directives for proper Sphinx linking. Introduced docs/api/cli/index.rst overview and docs/api/usd/vtk_to_usd_lib.rst for low-level API. API_MAP.md regenerated to document new/updated signatures (e.g., ConvertVTKToUSD.__init__(..., segmenter=None), TestTools.__init__ reordering, new AnatomyTaxonomy methods). Docs README updated to describe reorganization highlights.
Segmentation & USD developer guides
docs/api/segmentation/base.rst, docs/api/segmentation/index.rst, docs/api/segmentation/simpleware.rst, docs/api/segmentation/totalsegmentator.rst, docs/developer/segmentation.rst, docs/developer/usd_generation.rst
Expanded segmentation contract documentation to describe returned masks dict as {"labelmap": ..., **group_masks} with optional group keys and guidance to check membership. Added "Anatomy Taxonomy" and "Custom Anatomy Looks" sections. USD generation guide covers anatomy-grouped meshes, von Mises stress derivation, camera framing, and material customization via render-params.
README and quickstart updates
README.md, docs/quickstart.rst, data/README.md
Removed emoji headers; updated installation to Python 3.11+; reworded tutorials as nine percent-cell scripts; replaced hardcoded URLs with DataDownloadTools examples. Data README distinguished automatic (Slicer-Heart-CT) from manual (DirLab/KCL/CHOP) downloads and added verification-helper snippets. Quickstart simplified dataset fetch to one-liner DataDownloadTools.DownloadSlicerHeartCTData(...).
Test and CI documentation
tests/README.md, tests/TESTING_GUIDE.md, tests/EXPERIMENT_TESTS_GUIDE.md, tests/EXPERIMENT_TESTS_SUMMARY.md, tests/PARALLEL_EXECUTION_GUIDE.md, .github/workflows/README.md
Comprehensive rewrite from notebook-centric to script-centric test descriptions. Added "Documentation" section with guide links. Updated CI Python versions to 3.11/3.12. Replaced notebook execution (nbconvert, *.ipynb) with subprocess script execution (python *.py, alphanumeric ordering). Clarified opt-in --run-experiments behavior, parallel execution constraints (sequential within subdirectory, parallel across), and fail-fast stopping semantics.
Contributing guidelines
docs/contributing.rst
Expanded developer setup with uv CUDA/docs/test/dev extras options; raised Python to 3.11+; revised "Experiment Scripts" to describe percent-cell # %% workflow; restructured production-vs-research code principles with explicit bullet lists (error handling/testing for production CLI, allowance for hardcoded paths in research experiments).

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes


Possibly related PRs

  • Project-MONAI/physiomotion4d#47: Tutorial refactoring involving removal/replacement of run_tutorial(...) patterns and updates to tutorial execution flow, output shape, and test harness integration.
  • Project-MONAI/physiomotion4d#44: Direct modifications to ConvertVTKToUSD.__init__() and from_files() signatures, mask-ID-driven USD prim path validation, and corresponding test/doc updates.

Poem

🐰 Anatomy grouped, taxonomies born,
Tests refactored, old notebooks worn.
Scripts light the way with percent-cell cheer,
Meshes rendered, cameras clear.
From 3.10's past to 3.11's glow,
PhysioMotion's stage takes center flow! 🎭

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR modernizes PhysioMotion4D’s segmentation and USD export pipeline by introducing a shared AnatomyTaxonomy data model, hardening VTK→USD conversion for Omniverse rendering correctness, and expanding/refreshing tutorials, experiments, docs, and tests to match the new contracts.

Changes:

  • Introduces AnatomyTaxonomy/AnatomyGroup to replace per-segmenter flat dicts and standardize label grouping across segmentation and USD rendering.
  • Hardens USD generation (type-grouped labeled prim layout, material binding via MaterialBindingAPI.Apply, primvar preservation through triangulation, stale-layer eviction, framing camera, derived primvars like von Mises stress).
  • Adds/updates tutorials (07–09) and refreshes documentation + tests, including new dataset download verification helpers and OpenUSD screenshot rendering without USD imaging plugins.

Reviewed changes

Copilot reviewed 117 out of 123 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
tutorials/tutorial_08_dirlab_pca_time_series.py New tutorial to register DirLab phases and propagate PCA-fitted meshes through time series.
tutorials/tutorial_07_dirlab_pca_model.py New tutorial to build and fit a surface PCA model of DirLab lung lobes.
tutorials/tutorial_06_reconstruct_highres_4d_ct.py Refactors tutorial 06 into percent-cell script style and updates test-mode parameters and outputs.
tutorials/tutorial_05_vtk_to_usd.py Refactors tutorial 05 to use WorkflowConvertVTKToUSD + OpenUSD screenshot path.
tutorials/README.md Updates tutorial index and new guidance for percent-cell scripts.
tests/test_vtk_to_usd_library.py Adds OpenUSD screenshot test using VTK-based loader path.
tests/test_segment_heart_simpleware.py Updates assertions to use taxonomy-based group labels and new segment() key contract.
tests/test_segment_chest_total_segmentator.py Updates initialization checks to use taxonomy rather than legacy mask id dicts.
tests/test_download_heart_data.py Adds synthetic tests for DataDownloadTools verification helpers; updates Slicer-Heart directory expectations.
tests/test_convert_vtk_to_usd.py Updates expected labeled prim layout and adds segmenter-type grouping coverage.
tests/test_anatomy_taxonomy.py Adds unit tests for AnatomyTaxonomy/AnatomyGroup and base-class seeding behavior.
tests/PARALLEL_EXECUTION_GUIDE.md Updates experiment execution docs from notebooks to scripts.
tests/EXPERIMENT_TESTS_SUMMARY.md Updates experiment runner documentation for .py script execution.
tests/EXPERIMENT_TESTS_GUIDE.md Updates experiment execution docs and recommends TestTools.running_as_test().
tests/EXPERIMENT_FLAG_USAGE.md Updates environment-flag documentation for new helper (still contains some “notebook” wording).
tests/conftest.py Switches Slicer-Heart download to DataDownloadTools and reorganizes test data caching dirs.
src/physiomotion4d/workflow_convert_heart_gated_ct_to_usd.py Adds save toggles for intermediates, consolidates write helpers, and forwards taxonomy + segmenter into ConvertVTKToUSD.
src/physiomotion4d/workflow_convert_ct_to_vtk.py Moves label lookup to taxonomy via labels_in_group().
src/physiomotion4d/vtk_to_usd/usd_utils.py Adds triangulation face map + framing camera helper; updates docstrings/typing.
src/physiomotion4d/vtk_to_usd/usd_mesh_converter.py Expands uniform primvars through triangulation and preserves cell arrays for time samples.
src/physiomotion4d/vtk_to_usd/primvar_derivations.py New derived-primvar registry; includes von Mises stress derivation for 9-component stress tensors.
src/physiomotion4d/vtk_to_usd/material_manager.py Uses MaterialBindingAPI.Apply so Omniverse RTX honors material bindings.
src/physiomotion4d/vtk_to_usd/converter.py Adds stale layer eviction and framing camera for file facade conversion.
src/physiomotion4d/vtk_to_usd/init.py Reframes vtk_to_usd as a stable low-level API and exports new helpers.
src/physiomotion4d/usd_tools.py Adds VTK-based OpenUSD loader + framing camera + stale layer eviction in merge paths.
src/physiomotion4d/transform_tools.py Adds stale-layer eviction + framing camera when exporting transform visualizations.
src/physiomotion4d/test_tools.py Adds running_as_test() and OpenUSD screenshot rendering via VTK loader; makes baselines/results dirs optional.
src/physiomotion4d/segment_heart_simpleware.py Migrates label definitions to taxonomy and adds optional landmarks.csv handling.
src/physiomotion4d/register_images_ants.py Disables verbose output in ANTs backend calls.
src/physiomotion4d/notebook_utils.py Removes obsolete notebook_utils helper module.
src/physiomotion4d/data_download_tools.py Adds dataset download/verification helpers (Slicer-Heart auto-download + verifiers).
src/physiomotion4d/cli/convert_vtk_to_usd.py Sources anatomy types from DEFAULT_RENDER_PARAMS for automatic sync.
src/physiomotion4d/anatomy_taxonomy.py New pure-stdlib AnatomyTaxonomy/AnatomyGroup implementation.
src/physiomotion4d/init.py Exports AnatomyTaxonomy/AnatomyGroup and DataDownloadTools from top-level package.
README.md Updates tutorial count, examples, segmentation key contract notes, and DataDownloadTools usage.
pyproject.toml Bumps Python requirement to 3.11, adds deps (PhysicsNeMo, ipykernel), and updates mypy python_version.
experiments/Reconstruct4DCT/reconstruct_4d_ct_class.py Switches test-mode flag helper to TestTools.running_as_test().
experiments/README.md Updates experiment docs to scripts + TestTools.running_as_test().
experiments/Lung-GatedCT_To_USD/Experiment_SegReg.py Adds main guard to avoid Windows spawn cascade with nnUNet/TotalSegmentator.
experiments/Lung-GatedCT_To_USD/2-paint_dirlab_models.py Adds main guard and keeps label lookups taxonomy-aligned.
experiments/Lung-GatedCT_To_USD/1-make_dirlab_models.py Adds main guard and forwards segmenter for type-grouped USD layout.
experiments/Heart-VTKSeries_To_USD/1-heart_vtkseries_to_usd.py Adds main guard to prevent Windows spawn cascade; keeps script-format execution.
experiments/Heart-VTKSeries_To_USD/0-download_and_convert_4d_to_3d.py Uses DataDownloadTools instead of urllib download.
experiments/Heart-Statistical_Model_To_Patient/heart_model_to_patient-CHOPValve.py Switches test-mode helper to TestTools.running_as_test().
experiments/Heart-Statistical_Model_To_Patient/heart_model_to_model_registration_pca.py Switches test-mode helper to TestTools.running_as_test().
experiments/Heart-Statistical_Model_To_Patient/heart_model_to_model_icp_itk.py Switches test-mode helper to TestTools.running_as_test().
experiments/Heart-Simpleware_Segmentation/simpleware_heart_segmentation.py Switches to taxonomy label access and TestTools.running_as_test().
experiments/Heart-Simpleware_Segmentation/README.md Updates references from notebook to script and aligns instructions.
experiments/Heart-GatedCT_To_USD/test_compare_registration_speed.py Switches test-mode helper to TestTools.running_as_test().
experiments/Heart-GatedCT_To_USD/3-transform_dynamic_and_static_contours.py Adds main guard and forwards segmenter for type-grouped USD layout.
experiments/Heart-GatedCT_To_USD/0-download_and_convert_4d_to_3d.py Uses DataDownloadTools and Path-based IO.
experiments/Heart-Create_Statistical_Model/README.md Updates pipeline docs from notebooks to scripts.
experiments/Heart-Create_Statistical_Model/5-compute_pca_model.py Switches test-mode helper to TestTools.running_as_test().
experiments/Heart-Create_Statistical_Model/4-surfaces_aligned_correspond_to_pca_inputs.py Switches test-mode helper to TestTools.running_as_test().
experiments/Heart-Create_Statistical_Model/3-registration_based_correspondence.py Switches test-mode helper to TestTools.running_as_test().
experiments/Heart-Create_Statistical_Model/2-input_surfaces_to_surfaces_aligned.py Switches test-mode helper to TestTools.running_as_test().
experiments/Heart-Create_Statistical_Model/1-input_meshes_to_input_surfaces.py Switches test-mode helper to TestTools.running_as_test().
experiments/Convert_VTK_To_USD/convert_chop_tpv25_valve_to_usd.py Anchors paths to script, wires von Mises derivation, updates colormap selection.
experiments/Convert_VTK_To_USD/convert_chop_alterra_valve_to_usd.py Anchors paths to script, wires von Mises derivation, updates test-mode naming.
experiments/Colormap-VTK_To_USD/colormap_vtk_to_usd.py Adds progress prints for colormap conversion.
docs/tutorials.rst Expands tutorial set to 9 and updates execution guidance to script-based usage.
docs/quickstart.rst Updates quickstart to percent-cell tutorial execution and DataDownloadTools usage.
docs/examples.rst Updates dataset download snippet to DataDownloadTools.
docs/developer/usd_generation.rst Documents new labeled layout, von Mises derivation, framing camera, and renderer taxonomy integration.
docs/developer/segmentation.rst Documents taxonomy-driven key contract and how to add new segmenters/groups.
docs/contributing.rst Adds uv install guidance but still mentions Python 3.10+ in checklist.
docs/api/workflows.rst Adds explicit module directives for workflow docs.
docs/api/utilities/test_tools.rst New API page for TestTools.
docs/api/utilities/index.rst Adds test_tools and data_download to utilities index.
docs/api/utilities/data_download.rst New API page for DataDownloadTools.
docs/api/utilities/contour_tools.rst Updates navigation links to include test_tools.
docs/api/usd/vtk_to_usd_lib.rst New docs page for low-level vtk_to_usd subpackage.
docs/api/usd/vtk_conversion.rst Adds module directive for ConvertVTKToUSD API docs.
docs/api/usd/index.rst Adds vtk_to_usd_lib to USD docs index.
docs/api/segmentation/totalsegmentator.rst Adds module directive and clarifies returned keys/contract.
docs/api/segmentation/simpleware.rst New API page for Simpleware segmenter and its returned keys.
docs/api/segmentation/index.rst Adds Simpleware docs and updates guidance to membership checks.
docs/api/segmentation/base.rst Documents AnatomyTaxonomy contract + variability of returned mask keys.
docs/api/registration/time_series.rst Adds module directive for time-series registration docs.
docs/api/registration/index.rst Adds greedy module into registration docs toctree.
docs/api/registration/icon.rst Adds module directive for ICON docs.
docs/api/registration/greedy.rst New API page for Greedy registration backend.
docs/api/registration/base.rst Adds module directive for registration base docs.
docs/api/registration/ants.rst Adds module directive for ANTs docs.
docs/api/model_registration/pca.rst Adds module directive for PCA registration docs.
docs/api/model_registration/icp.rst Adds module directive for ICP docs.
docs/api/model_registration/icp_itk.rst Adds module directive for ITK ICP docs.
docs/api/model_registration/distance_maps.rst Adds module directive for distance-map registration docs.
docs/api/index.rst Adds CLI API section to the Sphinx API index.
docs/api/cli/visualize_pca_modes.rst New API page for visualize_pca_modes CLI module.
docs/api/cli/reconstruct_highres_4d_ct.rst New API page for reconstruct_highres_4d_ct CLI module.
docs/api/cli/index.rst New API index page for CLI entry-point modules.
docs/api/cli/fit_statistical_model_to_patient.rst New API page for fit_statistical_model_to_patient CLI module.
docs/api/cli/create_statistical_model.rst New API page for create_statistical_model CLI module.
docs/api/cli/convert_vtk_to_usd.rst New API page for convert_vtk_to_usd CLI module.
docs/api/cli/convert_heart_gated_ct_to_usd.rst New API page for convert_heart_gated_ct_to_usd CLI module.
docs/api/cli/convert_ct_to_vtk.rst New API page for convert_ct_to_vtk CLI module.
docs/api/base.rst Adds module directive for PhysioMotion4DBase docs.
data/test/.gitignore Changes ignored test-data layout to directory-based (slicer_heart / slicer_heart_small).
data/Slicer-Heart-CT/download_and_convert.py Switches download step to DataDownloadTools.
data/README.md Updates dataset instructions and documents DataDownloadTools verifiers.
.agents/skills/skills/caveman/SKILL.md Adds “caveman” skill markdown documentation.
.agents/.gitignore Ignores additional local agent config artifacts.
Comments suppressed due to low confidence (1)

src/physiomotion4d/data_download_tools.py:107

  • The method name VerityKCLHeartModelData appears to be a typo and duplicates VerifyKCLHeartModelData. If backward compatibility is not required, consider removing it to avoid expanding the public API surface; if it is required, consider marking it deprecated and documenting why it exists.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/physiomotion4d/usd_tools.py Outdated
Comment on lines +65 to +69
if time_code is None:
usd_time = Usd.TimeCode.Default()
else:
usd_time = Usd.TimeCode(time_code)
xform_cache = UsdGeom.XformCache(usd_time)
Comment thread src/physiomotion4d/test_tools.py Outdated
Comment on lines +445 to +449
"""Render OpenUSD mesh geometry off-screen and save a PNG.

The scene is loaded through ``USDTools.load_openusd_as_vtk`` into a
PyVista mesh, rendered with a fixed isometric camera and fixed
``800 x 600`` window, and centered automatically by PyVista.
Comment on lines +53 to +56
@staticmethod
def VeritySlicerHeartCTData(dirname: Union[str, Path]) -> bool: # noqa: N802
"""Return True when Slicer-Heart-CT has the expected 4-D CT file."""
return DataDownloadTools.VerifySlicerHeartCTData(dirname)
Comment on lines +35 to +44
REPO_ROOT = Path(__file__).resolve().parent.parent
TUTORIALS_DIR = Path(__file__).resolve().parent
DATA_DIR = REPO_ROOT / "data"
FULL_DATA_DIR = DATA_DIR
TEST_DATA_DIR = DATA_DIR / "test"
OUTPUT_DIR = TUTORIALS_DIR / "output" / "tutorial_05"
BASELINES_DIR = REPO_ROOT / "tests" / "baselines"
TUTORIAL_02_SURFACE = (
TUTORIALS_DIR / "output" / "tutorial_02" / "patient_surfaces.vtp"
)
Comment on lines +40 to +46
REPO_ROOT = Path(__file__).resolve().parent.parent
TUTORIALS_DIR = Path(__file__).resolve().parent
DATA_DIR = REPO_ROOT / "data"
FULL_DATA_DIR = DATA_DIR / "DirLab-4DCT" / "Case1"
TEST_DATA_DIR = DATA_DIR / "test" / "DirLab-4DCT" / "Case1"
OUTPUT_DIR = TUTORIALS_DIR / "output" / "tutorial_06"
BASELINES_DIR = REPO_ROOT / "tests" / "baselines"
Comment thread pyproject.toml
Comment on lines 64 to 69
# AI/ML and segmentation
"monai>=1.3.0",
"nvidia-physicsnemo>=2.0.0,<3.0.0",
"torch>=2.0.0,<3.0.0",
"transformers>=4.21.0,<5.0.0",
"totalsegmentator>=2.0.0",
Comment thread pyproject.toml
Comment on lines 83 to 86
# Utilities
"ipykernel>=6.0.0",
"matplotlib>=3.5.0",
"typing-extensions>=4.0.0",
Comment thread pyproject.toml
Comment on lines 23 to 27
"Operating System :: OS Independent",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
Comment thread docs/contributing.rst Outdated
Comment on lines +124 to +125
1. ✅ Install Python 3.10+ and create virtual environment
2. ✅ Install development dependencies: ``pip install -e ".[dev]"``
2. ✅ Install development dependencies: ``pip install -e ".[dev]"``,
Comment on lines 144 to 147
## 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 notebooks can use reduced parameters (e.g. fewer iterations, fewer files) and finish faster. Notebooks 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.notebook_utils.running_as_test()` helper.
When experiment tests run (with `--run-experiments`), the test runner also sets **`PHYSIOMOTION_RUNNING_AS_TEST=1`** so that notebooks can use reduced parameters (e.g. fewer iterations, fewer files) and finish faster. Notebooks 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.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 15

🧹 Nitpick comments (16)
src/physiomotion4d/data_download_tools.py (1)

54-56: ⚡ Quick win

Add deprecation warnings to the misspelled compatibility methods.

VeritySlicerHeartCTData and VerityKCLHeartModelData are misspelled compatibility shims (should be "Verify"). Consider adding a warnings.warn(DeprecationWarning) and updating their docstrings to indicate they're deprecated in favor of the correctly-spelled methods, so future maintainers and users are aware these will be removed.

⚠️ Proposed deprecation pattern
+import warnings
+
 class DataDownloadTools:
     ...
     `@staticmethod`
     def VeritySlicerHeartCTData(dirname: Union[str, Path]) -> bool:  # noqa: N802
-        """Return True when Slicer-Heart-CT has the expected 4-D CT file."""
+        """Deprecated: Use VerifySlicerHeartCTData instead (typo fix)."""
+        warnings.warn(
+            "VeritySlicerHeartCTData is deprecated due to typo; "
+            "use VerifySlicerHeartCTData instead",
+            DeprecationWarning,
+            stacklevel=2,
+        )
         return DataDownloadTools.VerifySlicerHeartCTData(dirname)

Apply the same pattern to VerityKCLHeartModelData.

Also applies to: 105-107

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/physiomotion4d/data_download_tools.py` around lines 54 - 56, Add
deprecation warnings and updated docstrings to the misspelled compatibility
shims: in VeritySlicerHeartCTData and VerityKCLHeartModelData, import the
warnings module, emit warnings.warn("Verity... is deprecated; use Verify...
instead", DeprecationWarning, stacklevel=2) at the start of each function,
update the docstring to indicate deprecation and the correct replacement
(VerifySlicerHeartCTData and VerifyKCLHeartModelData), and otherwise keep the
function behavior returning DataDownloadTools.VerifySlicerHeartCTData(...) or
DataDownloadTools.VerifyKCLHeartModelData(...) respectively so callers still
work while getting notified.
src/physiomotion4d/anatomy_taxonomy.py (1)

122-147: 💤 Low value

Consider documenting the bounds choice for range(1, 256).

The default id_range=range(1, 256) implicitly encodes the TotalSegmentator label space (1–255). A short comment or doc reference noting the source of that range, plus the fact that label 0 (background) is intentionally excluded, would make this easier to revisit when integrating other segmenters with different class index spaces.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/physiomotion4d/anatomy_taxonomy.py` around lines 122 - 147, The default
id_range in fill_other_group currently uses range(1, 256) without explanation;
update the function docstring (or add a short inline comment) to state that this
choice corresponds to the TotalSegmentator label space (labels 1–255) and that
label 0 is intentionally excluded as background, and mention that callers can
override id_range for other segmenters; reference the function name
fill_other_group, the OTHER_GROUP constant, and the add_organ call so reviewers
can find where to update the documentation.
src/physiomotion4d/vtk_to_usd/usd_mesh_converter.py (2)

239-253: 💤 Low value

Latent ValueError from triangulation_face_map.max() on empty maps.

If triangulate_face produces an empty source_face_index_per_triangle (e.g. a degenerate input where every face has count ≤ 2 contributes nothing), triangulation_face_map.shape[0] is 0 but array.data (sized to the source face count) is non-empty. The first branch's equality check is False, and the elif evaluates triangulation_face_map.max() on an empty array, which raises ValueError: zero-size array to reduction operation.

This is a degenerate-mesh edge case, but a one-line guard avoids the crash and keeps the existing "fall through and let create_primvar log a size mismatch" behavior.

🛡️ Proposed guard
             if triangulation_face_map is not None and array.interpolation == "uniform":
                 data = np.asarray(array.data)
                 if len(data) == triangulation_face_map.shape[0]:
                     # Already triangle-aligned (e.g. derived primvar that was
                     # built post-triangulation). Leave it alone.
                     pass
-                elif len(data) > 0 and triangulation_face_map.max() < len(data):
+                elif (
+                    len(data) > 0
+                    and triangulation_face_map.size > 0
+                    and triangulation_face_map.max() < len(data)
+                ):
                     expanded_data = data[triangulation_face_map]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/physiomotion4d/vtk_to_usd/usd_mesh_converter.py` around lines 239 - 253,
The branch that checks triangulation_face_map.max() can raise ValueError when
triangulation_face_map is empty; update the conditional in the block handling
triangulation_face_map (the one that inspects array.interpolation and builds
GenericArray) to first ensure triangulation_face_map.size (or shape[0]) > 0
before calling triangulation_face_map.max(), e.g. require
triangulation_face_map.size > 0 in the elif so you only call max() when the map
is non-empty and preserve the existing fall-through behavior that lets
create_primvar handle size mismatches.

386-397: 💤 Low value

Reusing cached map is safe here but the contract is implicit.

create_time_varying_mesh correctly refreshes self._last_triangulation_face_map via the inner create_mesh(first_mesh_data, …) call on line 361, so the cached map matches the topology being authored. The implicit contract — "topology is constant across time samples, therefore the triangulation map computed for sample 0 applies to all samples" — relies on the docstring assumption at line 334. If a future caller violates that assumption (e.g. variable topology meshes), uniform primvars on later samples will be silently misaligned. Consider asserting len(mesh_data.face_vertex_counts) == len(mesh_data_sequence[0].face_vertex_counts) per iteration to fail fast.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/physiomotion4d/vtk_to_usd/usd_mesh_converter.py` around lines 386 - 397,
The code silently reuses self._last_triangulation_face_map across time samples
(created by create_mesh for the first sample) which breaks if topology varies;
inside create_time_varying_mesh, before calling _add_generic_arrays for each
mesh_data/time_code pair, add a fail-fast assertion that the current mesh_data
topology matches the first sample (e.g. assert len(mesh_data.face_vertex_counts)
== len(first_mesh_data.face_vertex_counts) or equivalent comparison) so that
create_mesh/create_time_varying_mesh will raise immediately if topology changes
and avoid misaligned primvars when reusing self._last_triangulation_face_map.
src/physiomotion4d/usd_tools.py (3)

132-162: 💤 Low value

Per-face (uniform) displayColor is silently dropped in favor of the red fallback.

When an authored mesh has displayColor with interpolation="uniform" and the color array has face-count entries (≠ n_points), the function falls through to len(colors) != n_points: return fallback and returns red — visually indistinguishable from "no displayColor authored at all." Meshes authored by apply_colormap_from_primvar and _add_vertex_colors always use vertex interpolation, so this is fine for in-house assets, but third-party USD inputs may trigger it. At minimum, log a warning so the silent red fallback is debuggable.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/physiomotion4d/usd_tools.py` around lines 132 - 162, _in
_openusd_display_color detect when primvar.GetInterpolation() returns
UsdGeom.Tokens.uniform (or "uniform") and instead of falling back to red, expand
the per-face colors to per-vertex colors by mapping each face's color to its
constituent vertices using the mesh topology (use the mesh's
GetFaceVertexCounts() and GetFaceVertexIndices() to build a per-vertex array of
length n_points), then proceed with the existing tiling/length checks; also add
a warning log (e.g., via logging.warning or the module logger) when a uniform
interpolation is encountered and converted so third-party USD inputs are
debuggable. Ensure you still handle the other branches (constant, single color,
vertex-length match) as before and only return the red fallback if conversion or
topology lookup fails.

86-90: 💤 Low value

face_counts/face_indices lookup lacks the same time-sample fallback as points.

Lines 78–84 correctly fall back to the first authored time sample if Get(usd_time) returns None for points. The topology attributes don't have the same fallback — if a mesh authors faceVertexCounts/faceVertexIndices only as time samples (atypical, but possible for meshes generated by some tools), the prim is silently skipped instead of recovered. Consider mirroring the points fallback for consistency.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/physiomotion4d/usd_tools.py` around lines 86 - 90, The code skips meshes
when face_counts or face_indices are None but doesn't apply the same time-sample
fallback used for points; update the logic around GetFaceVertexCountsAttr() and
GetFaceVertexIndicesAttr() to mirror the points fallback: if
mesh.GetFaceVertexCountsAttr().Get(usd_time) or
mesh.GetFaceVertexIndicesAttr().Get(usd_time) returns None, query the
attribute's authored time samples (e.g. via the attr's
GetTimeSamples()/GetTimeSamples method or equivalent) and call Get(first_sample)
to recover the value before deciding to continue; ensure you reference
mesh.GetFaceVertexCountsAttr(), mesh.GetFaceVertexIndicesAttr(), usd_time and
use the same fallback pattern already implemented for points.

91-102: ⚖️ Poor tradeoff

Extract world matrix once and apply batch transformation to all points instead of per-point Python loop.

The per-point Gf.Vec3d.Transform loop creates O(N) Python overhead. For medical anatomy meshes with tens of thousands of points, this dominates load_openusd_as_vtk performance. While usd-core 23.11 does not expose a dedicated vectorized converter for Gf.Matrix4d, you can extract the matrix data using GetArray() (which returns the 16 doubles in row-major order) and use NumPy's batch matrix-vector multiplication to transform all points in a single operation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/physiomotion4d/usd_tools.py` around lines 91 - 102, Replace the per-point
Gf.Vec3d.Transform loop in load_openusd_as_vtk: call
xform_cache.GetLocalToWorldTransform(prim) once to get world_matrix, extract its
flat 16-element array via world_matrix.GetArray(), reshape to a 4x4 NumPy
matrix, build a (N,4) homogeneous array from points_value, apply a single NumPy
matrix multiplication to transform all points at once, then drop the homogeneous
coordinate and cast to float32 into the variable points (instead of calling
Gf.Vec3d.Transform inside the list comprehension).
src/physiomotion4d/vtk_to_usd/usd_utils.py (1)

432-526: 💤 Low value

Consider making add_framing_camera fully idempotent for camera attributes.

The function correctly reuses the translate op when it already exists (avoiding duplicate ops), but CreateClippingRangeAttr, CreateFocalLengthAttr, CreateHorizontalApertureAttr, and CreateFocusDistanceAttr are unconditionally set. If these attributes were customized downstream and the function is called again (e.g., during re-export on a stage with a carried-over camera), those tuned values would be silently replaced.

All current callers in the repo use default parameters, and no staged USD assets author custom camera attributes, so this is not currently an issue. However, for defensive API design, consider only setting these attributes when not already authored, or expose a force: bool = True parameter to let callers opt out of overwriting.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/physiomotion4d/vtk_to_usd/usd_utils.py` around lines 432 - 526, The
add_framing_camera function currently always overwrites camera attributes
(clippingRange, focalLength, horizontalAperture, focusDistance); change it to be
idempotent by either adding an optional parameter force: bool = True (defaulting
to True to preserve current behavior) or by only setting each attribute when it
does not already have an authored value: use
camera.GetClippingRangeAttr()/GetFocalLengthAttr()/GetHorizontalApertureAttr()/GetFocusDistanceAttr()
and conditionally call Set(...) only if attr.HasAuthoredValue() is False (or
force is True), so downstream-customized values are preserved when re-running
the function.
src/physiomotion4d/transform_tools.py (1)

900-909: ⚡ Quick win

Extract stale-layer eviction pattern into a shared helper function.

This same 9-line unlink() + Sdf.Layer.Find().Clear() + del sequence now appears in four places: transform_tools.py (lines 900–909), usd_tools.py (lines 434–441), converter.py (lines 48–58), and convert_vtk_to_usd.py (lines 570–577). Pulling it into a small helper in vtk_to_usd/usd_utils.py (e.g. evict_stale_usd_layer(output_path: Path) -> None) would centralize the comment explaining USD's global identifier cache and keep all call sites in lockstep if the policy ever changes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/physiomotion4d/transform_tools.py` around lines 900 - 909, Extract the
9-line stale-layer eviction pattern (Path.unlink + Sdf.Layer.Find + Clear + del
and accompanying comment) into a shared helper named
evict_stale_usd_layer(output_path: Path) in vtk_to_usd/usd_utils.py, documenting
the USD global identifier cache behavior there; then replace the inline block in
transform_tools.py (around the code using output_filename), usd_tools.py,
converter.py, and convert_vtk_to_usd.py with a single call to
evict_stale_usd_layer(Path(output_filename)) (or the existing Path variable) so
all sites use the centralized implementation and comment.
src/physiomotion4d/test_tools.py (1)

76-80: ⚡ Quick win

Inconsistent directory path handling between baselines_dir and results_dir.

When baselines_dir is provided, line 79 appends class_name to it: baselines_dir = baselines_dir / class_name. However, when results_dir is provided (line 71), it's used exactly as given without appending class_name. This asymmetry can confuse users who expect parallel behavior.

Consider either:

  1. Document this distinction clearly in the docstring (e.g., "baselines_dir: Root directory; class_name subdirectory will be created"), or
  2. Make the behavior symmetric by always appending class_name to both, or never appending to either when explicitly provided.
📝 Proposed docstring clarification
         Args:
             class_name: Subdirectory name for baselines and default results.
-            results_dir: Root directory for result artifacts.
-            baselines_dir: Root directory for baseline artifacts.
+            results_dir: Exact directory for result artifacts when provided;
+                otherwise defaults to ``tests/results/{class_name}``.
+            baselines_dir: Root directory for baseline artifacts; 
+                ``class_name`` subdirectory will always be appended.
             results_output_dir: Optional exact directory for written result
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/physiomotion4d/test_tools.py` around lines 76 - 80, The constructor
currently appends class_name to baselines_dir when baselines_dir is passed but
does not do the same for results_dir, causing asymmetric behavior; update the
logic so results_dir is treated the same: when a results_dir argument is
provided, append / class_name before assigning to self._results_dir (mirror the
existing baselines_dir behavior using the same pattern as in the baselines
block), and update the docstring to state that both baselines_dir and
results_dir are root directories where a class_name subdirectory will be used.
experiments/Convert_VTK_To_USD/convert_chop_alterra_valve_to_usd.py (1)

169-175: 💤 Low value

Hardcoded prim paths may become stale.

Lines 171-173 hardcode specific USD prim paths (/World/AlterraValve/AlterraValve_object3, /World/AlterraValve/AlterraValve_Triangle) that depend on the VTK file's internal structure. If the Alterra mesh topology or connectivity changes, these paths will break. The computed mesh_paths at line 169 could be used dynamically to avoid this brittleness.

♻️ Consider using dynamic path selection
 mesh_paths = usd_tools.list_mesh_paths_under(stage, parent_path="/World/AlterraValve")
 if separate_by == "connectivity":
-    vessel_path = "/World/AlterraValve/AlterraValve_object3"
+    # Pick the largest mesh by vertex count or use a heuristic
+    vessel_path = mesh_paths[0] if mesh_paths else "/World/AlterraValve/Mesh"
 elif separate_by == "cell_type":
-    vessel_path = "/World/AlterraValve/AlterraValve_Triangle"
+    # Pick triangle mesh or fallback
+    vessel_path = next((p for p in mesh_paths if "Triangle" in p), mesh_paths[0] if mesh_paths else "/World/AlterraValve/Mesh")
 else:
     vessel_path = "/World/AlterraValve/Mesh"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@experiments/Convert_VTK_To_USD/convert_chop_alterra_valve_to_usd.py` around
lines 169 - 175, The hardcoded prim paths assigned to vessel_path when handling
separate_by ("connectivity" and "cell_type") are brittle; instead, use the
computed mesh_paths (from usd_tools.list_mesh_paths_under(stage,
parent_path="/World/AlterraValve")) to select the appropriate prim dynamically.
Modify the logic around mesh_paths, separate_by and vessel_path: inspect
mesh_paths for prim names or metadata that indicate connectivity or cell type
(e.g., names containing "object" or "Triangle" or an authored attribute), choose
the matching path from mesh_paths for the "connectivity" and "cell_type"
branches, and fall back to a default mesh_paths[0] or "/World/AlterraValve/Mesh"
if no match is found; keep references to list_mesh_paths_under, mesh_paths,
separate_by and vessel_path to locate where to change the code.
experiments/Convert_VTK_To_USD/convert_chop_tpv25_valve_to_usd.py (1)

166-172: 💤 Low value

Hardcoded vessel paths may be fragile.

The script now uses fixed paths like "/World/TPV25Valve/TPV25Valve_object4" and "/World/TPV25Valve/TPV25Valve_Triangle" for specific separate_by modes. If the VTK file's connectivity structure or cell-type composition changes, these paths will break silently. Additionally, mesh_paths is computed but unused in these branches.

Consider either:

  1. Adding a comment explaining why object4 and Triangle are the correct choices for this specific dataset, or
  2. Selecting from mesh_paths using a heuristic (e.g., largest component, specific substring match) to make the script more robust.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@experiments/Convert_VTK_To_USD/convert_chop_tpv25_valve_to_usd.py` around
lines 166 - 172, The hardcoded vessel_path assignments for separate_by modes
(the separate_by variable and vessel_path binding) are fragile and ignore the
previously computed mesh_paths from usd_tools.list_mesh_paths_under; replace the
fixed literals with a robust selection: inspect mesh_paths (returned by
usd_tools.list_mesh_paths_under(stage, parent_path="/World/TPV25Valve")) and
choose the best match for vessel_path via a heuristic (e.g., prefer paths
containing "object" or "Triangle" when separate_by == "connectivity" or
"cell_type", otherwise pick the largest/first mesh path), and fall back to
"/World/TPV25Valve/Mesh" only if no candidate found; keep the selection logic
encapsulated near where separate_by and vessel_path are set so callers using
vessel_path get a resilient value.
tests/test_vtk_to_usd_library.py (1)

127-154: ⚡ Quick win

Consider adding a clarifying comment about the default color behavior.

The assertion that openusd_rgb equals [255, 0, 0] is correct—load_openusd_as_vtk applies red as the default color when geometry lacks a displayColor primvar (as documented in the function's docstring). Adding a comment in the test linking to this documented behavior would improve clarity for future readers.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_vtk_to_usd_library.py` around lines 127 - 154, Add a short
clarifying comment inside the test_openusd_screenshot_uses_vtk_loader test
explaining why the expected color is red; mention that load_openusd_as_vtk
applies a default red color when geometry lacks a displayColor primvar (per
load_openusd_as_vtk docstring) so the assertion on point_data["openusd_rgb"] ==
[255, 0, 0] is intentional and not test-data-specific.
src/physiomotion4d/workflow_convert_heart_gated_ct_to_usd.py (1)

145-176: 💤 Low value

Consider consistent parameter signature for conditional write helpers.

The helpers have slightly inconsistent signatures:

  • _write_image_if_enabled takes an enabled parameter
  • _write_transform_if_enabled reads self.save_registration_transforms directly

This is not incorrect, but for maintainability, consider making both take an enabled parameter for consistency. This would make the helpers more reusable if additional conditional write scenarios arise.

♻️ Possible refactor for consistency
 def _write_transform_if_enabled(
     self,
     transform: itk.Transform,
     filename: str,
+    enabled: bool,
 ) -> None:
     """Write a transform artifact when transform saving is enabled."""
-    if self.save_registration_transforms:
+    if enabled:
         itk.transformwrite(
             transform,
             self._output_path(filename),
             compression=True,
         )

Then update call sites to pass self.save_registration_transforms.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/physiomotion4d/workflow_convert_heart_gated_ct_to_usd.py` around lines
145 - 176, Make the three conditional write helpers consistent by adding an
enabled: bool parameter to _write_transform_if_enabled and
_write_registered_image_if_enabled (like _write_image_if_enabled) and use that
parameter to gate writes instead of reading instance flags internally; update
all call sites to pass the appropriate flags (pass
self.save_registration_transforms to _write_transform_if_enabled and
self.save_registered_images to _write_registered_image_if_enabled) and keep the
same behavior for writing (itk.transformwrite, itk.imwrite, and
registrar.get_registered_image()).
tutorials/tutorial_04_fit_statistical_model_to_patient.py (1)

89-100: 💤 Low value

Consider extracting duplicate surface extraction logic.

The surface extraction pattern (lines 70-76 and 92-99) is duplicated. For maintainability, consider extracting to a helper function like _extract_surface(data: pv.DataSet) -> pv.PolyData.

♻️ Optional refactor
def _extract_surface(data: pv.DataSet) -> pv.PolyData:
    """Extract PolyData surface from any PyVista dataset."""
    if isinstance(data, pv.PolyData):
        return data
    return cast(
        pv.PolyData,
        data.extract_surface(algorithm="dataset_surface"),
    )

# Usage:
template_model = _extract_surface(cast(pv.DataSet, pv.read(str(template_file))))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tutorials/tutorial_04_fit_statistical_model_to_patient.py` around lines 89 -
100, Duplicate surface-extraction logic for template and patient samples should
be moved into a helper function (e.g., _extract_surface(data: pv.DataSet) ->
pv.PolyData) and then used from both places; implement _extract_surface to
return the input if it's already a pv.PolyData otherwise call
data.extract_surface(algorithm="dataset_surface") and cast to pv.PolyData, then
replace the inline checks in the template_model creation and the patient_models
loop (references: template_model, patient_models, sample_files) to call
_extract_surface(cast(pv.DataSet, pv.read(...))) or
_extract_surface(sample_data).
tests/test_tutorials.py (1)

65-70: 💤 Low value

Improve error message for missing tutorial_results.

If a tutorial script crashes or fails to set tutorial_results, the assertion on line 69 could be more informative by checking for None first.

♻️ Optional improvement
 def _run_tutorial_script(script_name: str) -> dict[str, Any]:
     """Run a tutorial script with no command-line arguments."""
     namespace = runpy.run_path(str(_REPO_ROOT / "tutorials" / script_name))
     results = namespace.get("tutorial_results")
+    if results is None:
+        pytest.fail(
+            f"{script_name} did not set tutorial_results. "
+            "Script may have crashed or exited early."
+        )
     assert isinstance(results, dict), f"{script_name} did not set tutorial_results"
     return results
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_tutorials.py` around lines 65 - 70, The assertion for
tutorial_results should first check for None and raise a clearer error; in
_run_tutorial_script capture the namespace from runpy.run_path, then if
namespace.get("tutorial_results") is None raise an AssertionError that includes
script_name and the namespace keys (e.g. f"{script_name} did not set
tutorial_results; namespace keys: {list(namespace.keys())}"), and only afterward
assert isinstance(results, dict) to ensure it's the expected type; update
_run_tutorial_script accordingly (function name: _run_tutorial_script, variable:
tutorial_results).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.agents/skills/skills/caveman/SKILL.md:
- Line 7: The SKILL.md metadata currently publishes a personal email in the
author field; update the author line (the "author:" metadata in SKILL.md) to
remove the personal email and replace it with a non-PII alternative such as a
team alias (e.g., "NVIDIA ML Team"), a role-based address, or a profile URL
(e.g., a GitHub/GitLab profile) while preserving attribution to Vaibhav Phutane
and Julius Brussee.

In `@docs/api/cli/index.rst`:
- Around line 9-11: Replace the incorrect article before the inline code
reference: change the phrase "an `main()` function" to "a `main()` function" in
the CLI documentation sentence that describes each module exposing a main() that
parses argparse and dispatches to the workflow class; update the inline text
near the existing `main()` mention so the sentence reads "a `main()` function".

In `@experiments/Heart-GatedCT_To_USD/0-download_and_convert_4d_to_3d.py`:
- Around line 23-26: The copy operation uses the wrong source path: after
conv.save_3d_images writes files into data_dir / "slice", shutil.copyfile is
trying to read data_dir / "slice_007.mha" (parent dir) which causes
FileNotFoundError; update the shutil.copyfile source to point to the generated
file inside the slice subdirectory (e.g. data_dir / "slice" / "slice_007.mha")
when copying to output_dir / "slice_fixed.mha", ensuring you join Paths
consistently with the rest of the script and convert to str if required by
shutil.copyfile.

In `@experiments/Heart-GatedCT_To_USD/1-register_images.py`:
- Line 84: The loop currently hardcodes a single slice with for i in range(17,
18, FRAME_STEP) which disables multi-frame processing; change that range to
iterate the full set of frames (e.g., for i in range(0, total_slices,
FRAME_STEP) or for i in range(0, len(slices), FRAME_STEP)) so the variable i and
FRAME_STEP drive processing over all slices rather than just slice 017.

In `@experiments/Heart-GatedCT_To_USD/3-transform_dynamic_and_static_contours.py`:
- Around line 14-20: The test_mode check is defined at module import time
(test_mode = TestTools.running_as_test()) which contradicts the defensive
comment about guarding side-effects; move the TestTools.running_as_test() call
and the test_mode variable assignment inside the if __name__ == "__main__":
block (before any use of seg or any potential seg.segment(...) calls) so the
check runs only when the script is executed as main and not at import time;
update any downstream uses to reference the local test_mode defined inside that
main guard.

In `@pyproject.toml`:
- Around line 25-26: Remove the duplicate Python classifier entry in
pyproject.toml: locate the repeated string "Programming Language :: Python ::
3.11" and delete one of the two identical lines so the classifiers list contains
only a single Python 3.11 entry.

In `@src/physiomotion4d/anatomy_taxonomy.py`:
- Around line 71-85: Docstring for add_organ states cross-group id collisions
should be last-write-wins but group_for_id and group_for_label currently iterate
self._groups.values() in insertion order and therefore return the first match;
choose one fix: (A) update add_organ docstring to state it is first-write-wins,
or (B) change group_for_id and group_for_label to iterate groups in reverse
insertion order (e.g. iterate reversed(list(self._groups.values())) or reversed
tuple of keys) so the most recently added group wins; update unit tests/comments
accordingly to reflect the chosen semantics.

In `@src/physiomotion4d/segment_chest_total_segmentator.py`:
- Around line 277-284: The code currently does mask3 = (labelmap_arr3 == 1)
which collapses all classes into a binary mask so subsequent assignments for IDs
121/122/123 never run; change the logic to preserve per-class masks from
labelmap_arr3 (e.g. compute separate boolean masks labelmap_arr3 == 1,
labelmap_arr3 == 2, labelmap_arr3 == 3, labelmap_arr3 == 4, convert each to
uint8/ITK image and CopyInformation from preprocessed_image as you do now) and
then set final_arr using those per-class masks (using the same final_arr =
np.where(..., value, final_arr) pattern) so classes 121/122/123 are correctly
applied.

In `@src/physiomotion4d/segment_heart_simpleware.py`:
- Around line 293-297: The code calls next(fh) to skip the CSV header but
doesn't guard against an empty/truncated landmarks_file which raises
StopIteration; modify the block around landmarks_file/next(fh)/csv.DictReader to
first check that the file has content (e.g., read the first line via
fh.readline() and only call csv.DictReader on the remainder if that line is
non-empty) or wrap next(fh) in a try/except StopIteration and treat the file as
empty (skip processing) so the segmentation continues; update the logic
referencing landmarks_file, next(fh) and csv.DictReader accordingly.

In `@src/physiomotion4d/usd_anatomy_tools.py`:
- Around line 391-402: The loop over stage primitives uses UsdGeom.Mesh(prim)
and a truthiness check which doesn't filter non-mesh prims; change the logic to
first test the prim type (e.g., use prim.IsA(UsdGeom.Mesh)) or construct
mesh_prim and call mesh_prim.IsDefined() before proceeding, then only look up
organ_params and call apply_anatomy_material_to_prim(prim, params) for valid
mesh prims; update the block around Traverse(), UsdGeom.Mesh,
prim.IsA/IsDefined, organ_params, and apply_anatomy_material_to_prim
accordingly.

In `@src/physiomotion4d/vtk_to_usd/usd_utils.py`:
- Around line 470-483: The camera auto-positioning path that computes bounds
when bounds_min/bounds_max are None currently always offsets the camera along
+Z; change it to query the stage up axis via UsdGeom.GetStageUpAxis(stage) and
choose an offset axis perpendicular to that up axis (e.g., if up is 'Z' offset
along +Y, otherwise offset along +Z) when computing the camera translation from
center and distance; update the code that builds the camera position
(referencing stage, bounds_min, bounds_max, center, distance) to use this
computed offset vector so Z-up stages are framed correctly.

In `@tutorials/tutorial_01_heart_gated_ct_to_usd.py`:
- Around line 100-113: The code is forcing test mode by overwriting
TestTools.running_as_test() with a hardcoded assignment; remove the stray
assignment "test_mode = True" so that test_mode remains set by
TestTools.running_as_test(), allowing data_dir selection (TEST_DATA_DIR vs
FULL_DATA_DIR) and number_of_registration_iterations to behave correctly (1 when
test_mode True, 10 when False); ensure no other parts of the file reassign
test_mode and that subsequent logic uses this single variable (refer to
TestTools.running_as_test(), test_mode, data_dir, and
number_of_registration_iterations).

In `@tutorials/tutorial_03_create_statistical_model.py`:
- Around line 110-111: The JSON dump fails because pca_model contains NumPy
ndarrays; before calling json.dump on model_file, convert NumPy arrays (e.g.,
pca_model["components"] and pca_model["eigenvalues"] or any ndarray values in
pca_model) to native Python types (use .tolist() or recursively map ndarray ->
list), then write the converted dict to disk; you can create a small sanitized
copy (e.g., pca_model_serializable) by checking isinstance(value, numpy.ndarray)
and converting, and pass that to json.dump.

In `@tutorials/tutorial_04_fit_statistical_model_to_patient.py`:
- Around line 69-76: The call to pv.DataSet.extract_surface uses the unsupported
algorithm="dataset_surface" parameter (seen where template_data is converted to
template_model); remove the algorithm="dataset_surface" argument and call
extract_surface() with no parameters (or else upgrade PyVista to >=0.47.0 if you
need that option); search for other uses of extract_surface in this file and
remove the algorithm=... parameter there as well to ensure compatibility with
PyVista 0.43.0.

In `@tutorials/tutorial_08_dirlab_pca_time_series.py`:
- Around line 137-141: The loop using zip over phase_files,
registration_result["forward_transforms"], and
registration_result["inverse_transforms"] can silently drop entries when lengths
differ; before iterating, validate that len(phase_files) equals
len(registration_result["forward_transforms"]) and equals
len(registration_result["inverse_transforms"]) and raise/log a clear error (or
handle the mismatch) if not; update the iteration around the for phase_file,
phase_to_reference, reference_to_phase in zip(...) to either use an explicit
zip_longest with a clear check or bail out with a descriptive exception
mentioning phase_files and the forward_transforms/inverse_transforms keys so
mismatches are detected instead of truncated.

---

Nitpick comments:
In `@experiments/Convert_VTK_To_USD/convert_chop_alterra_valve_to_usd.py`:
- Around line 169-175: The hardcoded prim paths assigned to vessel_path when
handling separate_by ("connectivity" and "cell_type") are brittle; instead, use
the computed mesh_paths (from usd_tools.list_mesh_paths_under(stage,
parent_path="/World/AlterraValve")) to select the appropriate prim dynamically.
Modify the logic around mesh_paths, separate_by and vessel_path: inspect
mesh_paths for prim names or metadata that indicate connectivity or cell type
(e.g., names containing "object" or "Triangle" or an authored attribute), choose
the matching path from mesh_paths for the "connectivity" and "cell_type"
branches, and fall back to a default mesh_paths[0] or "/World/AlterraValve/Mesh"
if no match is found; keep references to list_mesh_paths_under, mesh_paths,
separate_by and vessel_path to locate where to change the code.

In `@experiments/Convert_VTK_To_USD/convert_chop_tpv25_valve_to_usd.py`:
- Around line 166-172: The hardcoded vessel_path assignments for separate_by
modes (the separate_by variable and vessel_path binding) are fragile and ignore
the previously computed mesh_paths from usd_tools.list_mesh_paths_under; replace
the fixed literals with a robust selection: inspect mesh_paths (returned by
usd_tools.list_mesh_paths_under(stage, parent_path="/World/TPV25Valve")) and
choose the best match for vessel_path via a heuristic (e.g., prefer paths
containing "object" or "Triangle" when separate_by == "connectivity" or
"cell_type", otherwise pick the largest/first mesh path), and fall back to
"/World/TPV25Valve/Mesh" only if no candidate found; keep the selection logic
encapsulated near where separate_by and vessel_path are set so callers using
vessel_path get a resilient value.

In `@src/physiomotion4d/anatomy_taxonomy.py`:
- Around line 122-147: The default id_range in fill_other_group currently uses
range(1, 256) without explanation; update the function docstring (or add a short
inline comment) to state that this choice corresponds to the TotalSegmentator
label space (labels 1–255) and that label 0 is intentionally excluded as
background, and mention that callers can override id_range for other segmenters;
reference the function name fill_other_group, the OTHER_GROUP constant, and the
add_organ call so reviewers can find where to update the documentation.

In `@src/physiomotion4d/data_download_tools.py`:
- Around line 54-56: Add deprecation warnings and updated docstrings to the
misspelled compatibility shims: in VeritySlicerHeartCTData and
VerityKCLHeartModelData, import the warnings module, emit
warnings.warn("Verity... is deprecated; use Verify... instead",
DeprecationWarning, stacklevel=2) at the start of each function, update the
docstring to indicate deprecation and the correct replacement
(VerifySlicerHeartCTData and VerifyKCLHeartModelData), and otherwise keep the
function behavior returning DataDownloadTools.VerifySlicerHeartCTData(...) or
DataDownloadTools.VerifyKCLHeartModelData(...) respectively so callers still
work while getting notified.

In `@src/physiomotion4d/test_tools.py`:
- Around line 76-80: The constructor currently appends class_name to
baselines_dir when baselines_dir is passed but does not do the same for
results_dir, causing asymmetric behavior; update the logic so results_dir is
treated the same: when a results_dir argument is provided, append / class_name
before assigning to self._results_dir (mirror the existing baselines_dir
behavior using the same pattern as in the baselines block), and update the
docstring to state that both baselines_dir and results_dir are root directories
where a class_name subdirectory will be used.

In `@src/physiomotion4d/transform_tools.py`:
- Around line 900-909: Extract the 9-line stale-layer eviction pattern
(Path.unlink + Sdf.Layer.Find + Clear + del and accompanying comment) into a
shared helper named evict_stale_usd_layer(output_path: Path) in
vtk_to_usd/usd_utils.py, documenting the USD global identifier cache behavior
there; then replace the inline block in transform_tools.py (around the code
using output_filename), usd_tools.py, converter.py, and convert_vtk_to_usd.py
with a single call to evict_stale_usd_layer(Path(output_filename)) (or the
existing Path variable) so all sites use the centralized implementation and
comment.

In `@src/physiomotion4d/usd_tools.py`:
- Around line 132-162: _in _openusd_display_color detect when
primvar.GetInterpolation() returns UsdGeom.Tokens.uniform (or "uniform") and
instead of falling back to red, expand the per-face colors to per-vertex colors
by mapping each face's color to its constituent vertices using the mesh topology
(use the mesh's GetFaceVertexCounts() and GetFaceVertexIndices() to build a
per-vertex array of length n_points), then proceed with the existing
tiling/length checks; also add a warning log (e.g., via logging.warning or the
module logger) when a uniform interpolation is encountered and converted so
third-party USD inputs are debuggable. Ensure you still handle the other
branches (constant, single color, vertex-length match) as before and only return
the red fallback if conversion or topology lookup fails.
- Around line 86-90: The code skips meshes when face_counts or face_indices are
None but doesn't apply the same time-sample fallback used for points; update the
logic around GetFaceVertexCountsAttr() and GetFaceVertexIndicesAttr() to mirror
the points fallback: if mesh.GetFaceVertexCountsAttr().Get(usd_time) or
mesh.GetFaceVertexIndicesAttr().Get(usd_time) returns None, query the
attribute's authored time samples (e.g. via the attr's
GetTimeSamples()/GetTimeSamples method or equivalent) and call Get(first_sample)
to recover the value before deciding to continue; ensure you reference
mesh.GetFaceVertexCountsAttr(), mesh.GetFaceVertexIndicesAttr(), usd_time and
use the same fallback pattern already implemented for points.
- Around line 91-102: Replace the per-point Gf.Vec3d.Transform loop in
load_openusd_as_vtk: call xform_cache.GetLocalToWorldTransform(prim) once to get
world_matrix, extract its flat 16-element array via world_matrix.GetArray(),
reshape to a 4x4 NumPy matrix, build a (N,4) homogeneous array from
points_value, apply a single NumPy matrix multiplication to transform all points
at once, then drop the homogeneous coordinate and cast to float32 into the
variable points (instead of calling Gf.Vec3d.Transform inside the list
comprehension).

In `@src/physiomotion4d/vtk_to_usd/usd_mesh_converter.py`:
- Around line 239-253: The branch that checks triangulation_face_map.max() can
raise ValueError when triangulation_face_map is empty; update the conditional in
the block handling triangulation_face_map (the one that inspects
array.interpolation and builds GenericArray) to first ensure
triangulation_face_map.size (or shape[0]) > 0 before calling
triangulation_face_map.max(), e.g. require triangulation_face_map.size > 0 in
the elif so you only call max() when the map is non-empty and preserve the
existing fall-through behavior that lets create_primvar handle size mismatches.
- Around line 386-397: The code silently reuses
self._last_triangulation_face_map across time samples (created by create_mesh
for the first sample) which breaks if topology varies; inside
create_time_varying_mesh, before calling _add_generic_arrays for each
mesh_data/time_code pair, add a fail-fast assertion that the current mesh_data
topology matches the first sample (e.g. assert len(mesh_data.face_vertex_counts)
== len(first_mesh_data.face_vertex_counts) or equivalent comparison) so that
create_mesh/create_time_varying_mesh will raise immediately if topology changes
and avoid misaligned primvars when reusing self._last_triangulation_face_map.

In `@src/physiomotion4d/vtk_to_usd/usd_utils.py`:
- Around line 432-526: The add_framing_camera function currently always
overwrites camera attributes (clippingRange, focalLength, horizontalAperture,
focusDistance); change it to be idempotent by either adding an optional
parameter force: bool = True (defaulting to True to preserve current behavior)
or by only setting each attribute when it does not already have an authored
value: use
camera.GetClippingRangeAttr()/GetFocalLengthAttr()/GetHorizontalApertureAttr()/GetFocusDistanceAttr()
and conditionally call Set(...) only if attr.HasAuthoredValue() is False (or
force is True), so downstream-customized values are preserved when re-running
the function.

In `@src/physiomotion4d/workflow_convert_heart_gated_ct_to_usd.py`:
- Around line 145-176: Make the three conditional write helpers consistent by
adding an enabled: bool parameter to _write_transform_if_enabled and
_write_registered_image_if_enabled (like _write_image_if_enabled) and use that
parameter to gate writes instead of reading instance flags internally; update
all call sites to pass the appropriate flags (pass
self.save_registration_transforms to _write_transform_if_enabled and
self.save_registered_images to _write_registered_image_if_enabled) and keep the
same behavior for writing (itk.transformwrite, itk.imwrite, and
registrar.get_registered_image()).

In `@tests/test_tutorials.py`:
- Around line 65-70: The assertion for tutorial_results should first check for
None and raise a clearer error; in _run_tutorial_script capture the namespace
from runpy.run_path, then if namespace.get("tutorial_results") is None raise an
AssertionError that includes script_name and the namespace keys (e.g.
f"{script_name} did not set tutorial_results; namespace keys:
{list(namespace.keys())}"), and only afterward assert isinstance(results, dict)
to ensure it's the expected type; update _run_tutorial_script accordingly
(function name: _run_tutorial_script, variable: tutorial_results).

In `@tests/test_vtk_to_usd_library.py`:
- Around line 127-154: Add a short clarifying comment inside the
test_openusd_screenshot_uses_vtk_loader test explaining why the expected color
is red; mention that load_openusd_as_vtk applies a default red color when
geometry lacks a displayColor primvar (per load_openusd_as_vtk docstring) so the
assertion on point_data["openusd_rgb"] == [255, 0, 0] is intentional and not
test-data-specific.

In `@tutorials/tutorial_04_fit_statistical_model_to_patient.py`:
- Around line 89-100: Duplicate surface-extraction logic for template and
patient samples should be moved into a helper function (e.g.,
_extract_surface(data: pv.DataSet) -> pv.PolyData) and then used from both
places; implement _extract_surface to return the input if it's already a
pv.PolyData otherwise call data.extract_surface(algorithm="dataset_surface") and
cast to pv.PolyData, then replace the inline checks in the template_model
creation and the patient_models loop (references: template_model,
patient_models, sample_files) to call _extract_surface(cast(pv.DataSet,
pv.read(...))) or _extract_surface(sample_data).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0456f41b-43eb-4c42-be07-e525c2edc19e

📥 Commits

Reviewing files that changed from the base of the PR and between 533d0da and 04efe9e.

⛔ Files ignored due to path filters (6)
  • tests/baselines/tutorial_01_heart_gated_ct_to_usd/contours_3d.png is excluded by !**/*.png
  • tests/baselines/tutorial_01_heart_gated_ct_to_usd/reference_frame_axial.png is excluded by !**/*.png
  • tests/baselines/tutorial_01_heart_gated_ct_to_usd/segmentation_overlay.png is excluded by !**/*.png
  • tests/baselines/tutorial_02_ct_to_vtk/segmentation_overlay.png is excluded by !**/*.png
  • tests/baselines/tutorial_02_ct_to_vtk/vtk_surfaces.png is excluded by !**/*.png
  • tests/baselines/tutorial_05_vtk_to_usd/usd_mesh_rendering.png is excluded by !**/*.png
📒 Files selected for processing (117)
  • .agents/.gitignore
  • .agents/skills/skills/caveman/SKILL.md
  • README.md
  • data/README.md
  • data/Slicer-Heart-CT/download_and_convert.py
  • data/test/.gitignore
  • docs/API_MAP.md
  • docs/api/base.rst
  • docs/api/cli/convert_ct_to_vtk.rst
  • docs/api/cli/convert_heart_gated_ct_to_usd.rst
  • docs/api/cli/convert_vtk_to_usd.rst
  • docs/api/cli/create_statistical_model.rst
  • docs/api/cli/fit_statistical_model_to_patient.rst
  • docs/api/cli/index.rst
  • docs/api/cli/reconstruct_highres_4d_ct.rst
  • docs/api/cli/visualize_pca_modes.rst
  • docs/api/index.rst
  • docs/api/model_registration/distance_maps.rst
  • docs/api/model_registration/icp.rst
  • docs/api/model_registration/icp_itk.rst
  • docs/api/model_registration/pca.rst
  • docs/api/registration/ants.rst
  • docs/api/registration/base.rst
  • docs/api/registration/greedy.rst
  • docs/api/registration/icon.rst
  • docs/api/registration/index.rst
  • docs/api/registration/time_series.rst
  • docs/api/segmentation/base.rst
  • docs/api/segmentation/index.rst
  • docs/api/segmentation/simpleware.rst
  • docs/api/segmentation/totalsegmentator.rst
  • docs/api/usd/index.rst
  • docs/api/usd/vtk_conversion.rst
  • docs/api/usd/vtk_to_usd_lib.rst
  • docs/api/utilities/contour_tools.rst
  • docs/api/utilities/data_download.rst
  • docs/api/utilities/index.rst
  • docs/api/utilities/test_tools.rst
  • docs/api/workflows.rst
  • docs/contributing.rst
  • docs/developer/segmentation.rst
  • docs/developer/usd_generation.rst
  • docs/examples.rst
  • docs/quickstart.rst
  • docs/tutorials.rst
  • experiments/Colormap-VTK_To_USD/colormap_vtk_to_usd.py
  • experiments/Convert_VTK_To_USD/convert_chop_alterra_valve_to_usd.py
  • experiments/Convert_VTK_To_USD/convert_chop_tpv25_valve_to_usd.py
  • experiments/Heart-Create_Statistical_Model/1-input_meshes_to_input_surfaces.py
  • experiments/Heart-Create_Statistical_Model/2-input_surfaces_to_surfaces_aligned.py
  • experiments/Heart-Create_Statistical_Model/3-registration_based_correspondence.py
  • experiments/Heart-Create_Statistical_Model/4-surfaces_aligned_correspond_to_pca_inputs.py
  • experiments/Heart-Create_Statistical_Model/5-compute_pca_model.py
  • experiments/Heart-Create_Statistical_Model/README.md
  • experiments/Heart-GatedCT_To_USD/0-download_and_convert_4d_to_3d.py
  • experiments/Heart-GatedCT_To_USD/1-register_images.py
  • experiments/Heart-GatedCT_To_USD/2-generate_segmentation.py
  • experiments/Heart-GatedCT_To_USD/3-transform_dynamic_and_static_contours.py
  • experiments/Heart-GatedCT_To_USD/test_compare_registration_speed.py
  • experiments/Heart-Simpleware_Segmentation/README.md
  • experiments/Heart-Simpleware_Segmentation/simpleware_heart_segmentation.py
  • experiments/Heart-Statistical_Model_To_Patient/heart_model_to_model_icp_itk.py
  • experiments/Heart-Statistical_Model_To_Patient/heart_model_to_model_registration_pca.py
  • experiments/Heart-Statistical_Model_To_Patient/heart_model_to_patient-CHOPValve.py
  • experiments/Heart-Statistical_Model_To_Patient/heart_model_to_patient.py
  • experiments/Heart-VTKSeries_To_USD/0-download_and_convert_4d_to_3d.py
  • experiments/Heart-VTKSeries_To_USD/1-heart_vtkseries_to_usd.py
  • experiments/Lung-GatedCT_To_USD/0-register_dirlab_4dct.py
  • experiments/Lung-GatedCT_To_USD/1-make_dirlab_models.py
  • experiments/Lung-GatedCT_To_USD/2-paint_dirlab_models.py
  • experiments/Lung-GatedCT_To_USD/Experiment_SegReg.py
  • experiments/README.md
  • experiments/Reconstruct4DCT/reconstruct_4d_ct_class.py
  • pyproject.toml
  • src/physiomotion4d/__init__.py
  • src/physiomotion4d/anatomy_taxonomy.py
  • src/physiomotion4d/cli/convert_vtk_to_usd.py
  • src/physiomotion4d/convert_vtk_to_usd.py
  • src/physiomotion4d/data_download_tools.py
  • src/physiomotion4d/notebook_utils.py
  • src/physiomotion4d/register_images_ants.py
  • src/physiomotion4d/segment_anatomy_base.py
  • src/physiomotion4d/segment_chest_total_segmentator.py
  • src/physiomotion4d/segment_heart_simpleware.py
  • src/physiomotion4d/test_tools.py
  • src/physiomotion4d/transform_tools.py
  • src/physiomotion4d/usd_anatomy_tools.py
  • src/physiomotion4d/usd_tools.py
  • src/physiomotion4d/vtk_to_usd/__init__.py
  • src/physiomotion4d/vtk_to_usd/converter.py
  • src/physiomotion4d/vtk_to_usd/material_manager.py
  • src/physiomotion4d/vtk_to_usd/primvar_derivations.py
  • src/physiomotion4d/vtk_to_usd/usd_mesh_converter.py
  • src/physiomotion4d/vtk_to_usd/usd_utils.py
  • src/physiomotion4d/workflow_convert_ct_to_vtk.py
  • src/physiomotion4d/workflow_convert_heart_gated_ct_to_usd.py
  • tests/EXPERIMENT_FLAG_USAGE.md
  • tests/EXPERIMENT_TESTS_GUIDE.md
  • tests/EXPERIMENT_TESTS_SUMMARY.md
  • tests/PARALLEL_EXECUTION_GUIDE.md
  • tests/conftest.py
  • tests/test_anatomy_taxonomy.py
  • tests/test_convert_vtk_to_usd.py
  • tests/test_download_heart_data.py
  • tests/test_segment_chest_total_segmentator.py
  • tests/test_segment_heart_simpleware.py
  • tests/test_tutorials.py
  • tests/test_vtk_to_usd_library.py
  • tutorials/README.md
  • tutorials/tutorial_01_heart_gated_ct_to_usd.py
  • tutorials/tutorial_02_ct_to_vtk.py
  • tutorials/tutorial_03_create_statistical_model.py
  • tutorials/tutorial_04_fit_statistical_model_to_patient.py
  • tutorials/tutorial_05_vtk_to_usd.py
  • tutorials/tutorial_06_reconstruct_highres_4d_ct.py
  • tutorials/tutorial_07_dirlab_pca_model.py
  • tutorials/tutorial_08_dirlab_pca_time_series.py
💤 Files with no reviewable changes (1)
  • src/physiomotion4d/notebook_utils.py

Comment thread .agents/skills/skills/caveman/SKILL.md Outdated
Comment thread docs/api/cli/index.rst
Comment on lines +23 to +26
conv.save_3d_images(str(data_dir / "slice"))

# Save the mid-stroke slice as the fixed/reference image
shutil.copyfile(f"{data_dir}/slice_007.mha", f"{output_dir}/slice_fixed.mha")
shutil.copyfile(data_dir / "slice_007.mha", output_dir / "slice_fixed.mha")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Copy source path missing the slice/ subdirectory will raise FileNotFoundError.

Line 23 writes the per-frame 3D outputs under data_dir / "slice" (e.g. …/Slicer-Heart-CT/slice/slice_007.mha), but line 26 copies from data_dir / "slice_007.mha" — the parent directory. The AI summary also describes this exact path (data_dir / "slice" / "slice_007.mha") as the intended source, so the diff appears to have dropped the subdirectory.

🐛 Proposed fix
 # Save the mid-stroke slice as the fixed/reference image
-shutil.copyfile(data_dir / "slice_007.mha", output_dir / "slice_fixed.mha")
+shutil.copyfile(data_dir / "slice" / "slice_007.mha", output_dir / "slice_fixed.mha")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
conv.save_3d_images(str(data_dir / "slice"))
# Save the mid-stroke slice as the fixed/reference image
shutil.copyfile(f"{data_dir}/slice_007.mha", f"{output_dir}/slice_fixed.mha")
shutil.copyfile(data_dir / "slice_007.mha", output_dir / "slice_fixed.mha")
conv.save_3d_images(str(data_dir / "slice"))
# Save the mid-stroke slice as the fixed/reference image
shutil.copyfile(data_dir / "slice" / "slice_007.mha", output_dir / "slice_fixed.mha")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@experiments/Heart-GatedCT_To_USD/0-download_and_convert_4d_to_3d.py` around
lines 23 - 26, The copy operation uses the wrong source path: after
conv.save_3d_images writes files into data_dir / "slice", shutil.copyfile is
trying to read data_dir / "slice_007.mha" (parent dir) which causes
FileNotFoundError; update the shutil.copyfile source to point to the generated
file inside the slice subdirectory (e.g. data_dir / "slice" / "slice_007.mha")
when copying to output_dir / "slice_fixed.mha", ensuring you join Paths
consistently with the rest of the script and convert to str if required by
shutil.copyfile.

Comment thread experiments/Heart-GatedCT_To_USD/1-register_images.py Outdated
Comment thread experiments/Heart-GatedCT_To_USD/3-transform_dynamic_and_static_contours.py Outdated
Comment thread src/physiomotion4d/vtk_to_usd/usd_utils.py
Comment thread tutorials/tutorial_01_heart_gated_ct_to_usd.py
Comment on lines +110 to +111
with model_file.open("w", encoding="utf-8") as f:
json.dump(pca_model, f, indent=2)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

JSON serialization will fail with NumPy arrays.

The workflow likely returns pca_model with NumPy array values for "components" and "eigenvalues". Calling json.dump(pca_model, f) directly will raise TypeError: Object of type ndarray is not JSON serializable.

🐛 Proposed fix: Convert arrays before serialization
 model_file = output_dir / "pca_model.json"
+
+# Convert numpy arrays to lists for JSON serialization
+pca_model_serializable = {
+    k: v.tolist() if hasattr(v, "tolist") else v
+    for k, v in pca_model.items()
+}
+
 with model_file.open("w", encoding="utf-8") as f:
-    json.dump(pca_model, f, indent=2)
+    json.dump(pca_model_serializable, f, indent=2)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
with model_file.open("w", encoding="utf-8") as f:
json.dump(pca_model, f, indent=2)
model_file = output_dir / "pca_model.json"
# Convert numpy arrays to lists for JSON serialization
pca_model_serializable = {
k: v.tolist() if hasattr(v, "tolist") else v
for k, v in pca_model.items()
}
with model_file.open("w", encoding="utf-8") as f:
json.dump(pca_model_serializable, f, indent=2)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tutorials/tutorial_03_create_statistical_model.py` around lines 110 - 111,
The JSON dump fails because pca_model contains NumPy ndarrays; before calling
json.dump on model_file, convert NumPy arrays (e.g., pca_model["components"] and
pca_model["eigenvalues"] or any ndarray values in pca_model) to native Python
types (use .tolist() or recursively map ndarray -> list), then write the
converted dict to disk; you can create a small sanitized copy (e.g.,
pca_model_serializable) by checking isinstance(value, numpy.ndarray) and
converting, and pass that to json.dump.

Comment on lines +69 to +76
template_data = cast(pv.DataSet, pv.read(str(template_file)))
if isinstance(template_data, pv.PolyData):
template_model = template_data
else:
template_model = cast(
pv.PolyData,
template_data.extract_surface(algorithm="dataset_surface"),
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

PyVista 0.43.0 extract_surface algorithm parameter documentation

💡 Result:

The extract_surface method in PyVista 0.43.0 does not have an 'algorithm' parameter. This parameter was added in a later version (around 0.47.0), as evidenced by its absence in the 0.43.1 example and docs [1][2], and presence in 0.47+ docs [3][4][5]. In PyVista 0.43.0 (and 0.42.3 docs), the signature is: DataSetFilters.extract_surface(pass_pointid=True, pass_cellid=True, nonlinear_subdivision=1, progress_bar=False) -> PolyData [2] Parameters: - pass_pointid: bool, default True. Adds point array "vtkOriginalPointIds" identifying original points [2]. - pass_cellid: bool, default True. Adds cell array "vtkOriginalCellIds" identifying original cells (note: same cell may appear multiple times) [2]. - nonlinear_subdivision: int, default 1. Sets VTK's SetNonlinearSubdivisionLevel for subdividing nonlinear faces in UnstructuredGrids [1][2][6]. - progress_bar: bool, default False [2]. Examples from 0.43.1 docs use it without 'algorithm', e.g., grid.extract_surface or grid.extract_surface(nonlinear_subdivision=5) [1]. The 'algorithm' parameter allows choosing VTK filters ('geometry'/vtkGeometryFilter, 'dataset_surface'/vtkDataSetSurfaceFilter, None/auto) and is documented in later versions like 0.47.1+ [3][4]. In 0.47+, extract_geometry is deprecated in favor of extract_surface(algorithm='geometry' or None) [7][8]. For 0.43.0 specifically, use the above parameters; upgrade for 'algorithm' support.

Citations:


Remove algorithm="dataset_surface" parameter — unsupported in PyVista 0.43.0.

The algorithm parameter was not added to extract_surface() until PyVista 0.47.0. Remove it from lines 75 and any other calls to use the default behavior, or upgrade to PyVista 0.47.0+.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tutorials/tutorial_04_fit_statistical_model_to_patient.py` around lines 69 -
76, The call to pv.DataSet.extract_surface uses the unsupported
algorithm="dataset_surface" parameter (seen where template_data is converted to
template_model); remove the algorithm="dataset_surface" argument and call
extract_surface() with no parameters (or else upgrade PyVista to >=0.47.0 if you
need that option); search for other uses of extract_surface in this file and
remove the algorithm=... parameter there as well to ensure compatibility with
PyVista 0.43.0.

Comment thread tutorials/tutorial_08_dirlab_pca_time_series.py
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@experiments/Heart-Create_Statistical_Model/README.md`:
- Around line 166-168: Update the README entry that currently lists "Jupyter Lab
or Jupyter Notebook" as a requirement and mark it optional (e.g., "Jupyter Lab
or Jupyter Notebook (optional)") or remove it entirely so the doc clearly
reflects the script-first workflow; ensure the surrounding bullets still read
well and that any other mentions of Jupyter in the README are likewise changed
to optional or removed to avoid implying it is required.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f07d8558-d3ea-454c-8fe0-8ce612515046

📥 Commits

Reviewing files that changed from the base of the PR and between 04efe9e and 6378ec8.

📒 Files selected for processing (10)
  • .github/workflows/README.md
  • .github/workflows/ci.yml
  • .github/workflows/release.yml
  • README.md
  • docs/contributing.rst
  • docs/faq.rst
  • experiments/Heart-Create_Statistical_Model/README.md
  • pyproject.toml
  • tests/GITHUB_WORKFLOWS.md
  • tests/TESTING_GUIDE.md
✅ Files skipped from review due to trivial changes (5)
  • tests/TESTING_GUIDE.md
  • .github/workflows/README.md
  • tests/GITHUB_WORKFLOWS.md
  • docs/contributing.rst
  • README.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • pyproject.toml

Comment thread experiments/Heart-Create_Statistical_Model/README.md
save_3d_images: split the single basename argument into (directory,
basename); create the directory if needed. Propagated to all 7 callers
across src/, tests/, experiments/, data/Slicer-Heart-CT, and the
gated-CT workflow.

USDTools: move the module-level load_openusd_as_vtk and
_openusd_display_color helpers onto USDTools as load_usd_as_vtk and
_usd_display_color; update test_tools and test_vtk_to_usd_library
callers.

Tutorial outputs unified under TUTORIALS_DIR / "output" / ... so
tutorials 01/07/08/09 agree with tests/test_tutorials.py.

tutorial_01: drop the stray "test_mode = True" override so
TestTools.running_as_test() actually drives data_dir selection and
registration iteration count.

tutorial_08: validate phase_files, forward_transforms, and
inverse_transforms have equal length and raise ValueError on mismatch
before zipping so dropped frames surface instead of truncating
silently.

experiments/Heart-GatedCT_To_USD/3: move TestTools.running_as_test()
inside the __main__ guard so behavior matches the defensive
Windows-spawn comment.

vtk_to_usd/usd_utils.add_framing_camera: pick the camera offset axis
perpendicular to the stage up axis (+Y for Z-up stages, +Z otherwise)
so Z-up stages frame correctly.

Bump pyvista[all] floor: >=0.43.0 -> >=0.47.0.

Drop legacy Verity{SlicerHeartCT,KCLHeartModel}Data typo shims in
data_download_tools; current code only uses Verify* spellings.

docs/api/cli/index.rst: fix "an main() function" -> "a main() function".

docs/contributing.rst: drop the now-obsolete "If users might copy your
code..." paragraph.

docs/API_MAP.md regenerated.
Copilot AI review requested due to automatic review settings May 13, 2026 11:00
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 125 out of 131 changed files in this pull request and generated 3 comments.

Comment on lines 238 to +246
for array in mesh_data.generic_arrays:
if triangulation_face_map is not None and array.interpolation == "uniform":
data = np.asarray(array.data)
if len(data) == triangulation_face_map.shape[0]:
# Already triangle-aligned (e.g. derived primvar that was
# built post-triangulation). Leave it alone.
pass
elif len(data) > 0 and triangulation_face_map.max() < len(data):
expanded_data = data[triangulation_face_map]
Comment on lines +71 to +79
def add_organ(self, group: str, label_id: int, organ_name: str) -> None:
"""Add one organ label to the named group.

Creates the group if it does not yet exist. Reassigning the same
label id within the same group is silently allowed (last write wins);
the same id appearing in a different group is also allowed but
:meth:`group_for_id` will then return whichever group was registered
last.

Comment on lines 56 to 74
Args:
class_name: Subdirectory name for baselines and default results.
results_dir: Root directory for result artifacts.
baselines_dir: Root directory for baseline artifacts.
class_name: Subdirectory name for baselines and, by default, results.
results_output_dir: Optional exact directory for result artifacts.
When set, results are read and written there instead of
``results_dir / class_name`` while baselines still use
``baselines_dir / class_name``.
results_output_dir: Optional exact directory for written result
artifacts. Useful when tutorial outputs should live beside
tutorial files while baselines remain under ``baselines_dir``.
log_level: Logging level.
"""
super().__init__(class_name=class_name, log_level=log_level)

self._results_dir = (
results_output_dir
if results_output_dir is not None
else results_dir / class_name
)
self._tests_dir = _REPO_ROOT / "tests"
if results_output_dir is not None:
self._results_dir = results_output_dir
elif results_dir is not None:
self._results_dir = results_dir
else:
self._results_dir = self._tests_dir / "results" / class_name
self._results_dir.mkdir(parents=True, exist_ok=True)
@codecov
Copy link
Copy Markdown

codecov Bot commented May 13, 2026

Codecov Report

❌ Patch coverage is 52.43665% with 244 lines in your changes missing coverage. Please review.
✅ Project coverage is 30.11%. Comparing base (533d0da) to head (1346ac1).

Files with missing lines Patch % Lines
...c/physiomotion4d/vtk_to_usd/primvar_derivations.py 0.00% 44 Missing ⚠️
src/physiomotion4d/usd_tools.py 59.00% 41 Missing ⚠️
src/physiomotion4d/convert_vtk_to_usd.py 41.26% 37 Missing ⚠️
...motion4d/workflow_convert_heart_gated_ct_to_usd.py 14.28% 30 Missing ⚠️
src/physiomotion4d/usd_anatomy_tools.py 17.39% 19 Missing ⚠️
src/physiomotion4d/segment_anatomy_base.py 30.76% 18 Missing ⚠️
src/physiomotion4d/data_download_tools.py 74.54% 14 Missing ⚠️
src/physiomotion4d/segment_heart_simpleware.py 0.00% 12 Missing ⚠️
src/physiomotion4d/transform_tools.py 27.27% 8 Missing ⚠️
.../physiomotion4d/segment_chest_total_segmentator.py 40.00% 6 Missing ⚠️
... and 5 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #49      +/-   ##
==========================================
+ Coverage   27.05%   30.11%   +3.06%     
==========================================
  Files          46       48       +2     
  Lines        6313     6611     +298     
==========================================
+ Hits         1708     1991     +283     
- Misses       4605     4620      +15     
Flag Coverage Δ
integration-tests 30.11% <52.43%> (?)
unittests 29.64% <50.87%> (+2.59%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (1)
src/physiomotion4d/usd_tools.py (1)

113-137: 💤 Low value

Apply the same time-code fallback to face attributes and the xform cache.

When time_code is None and the points attribute has no default-time value, the code falls back to the first authored point sample but still reads GetFaceVertexCountsAttr().Get(usd_time) / GetFaceVertexIndicesAttr().Get(usd_time) at Usd.TimeCode.Default(), and uses the original xform_cache for the transform. For meshes whose topology and/or transform are also authored only at time samples (no default), face_counts becomes None and the mesh is silently skipped — even though the points were successfully recovered. Consider promoting sample_time to drive face attribute lookups and the xform cache when the points fallback fires.

♻️ Proposed refactor
-        if time_code is None:
-            usd_time = Usd.TimeCode.Default()
-        else:
-            usd_time = Usd.TimeCode(time_code)
-        xform_cache = UsdGeom.XformCache(usd_time)
-
-        meshes: list[pvtk.PolyData] = []
-        for prim in Usd.PrimRange(root_prim):
-            if not prim.IsA(UsdGeom.Mesh):
-                continue
-
-            mesh = UsdGeom.Mesh(prim)
-            points_value = mesh.GetPointsAttr().Get(usd_time)
-            if points_value is None and time_code is None:
-                point_samples = mesh.GetPointsAttr().GetTimeSamples()
-                if point_samples:
-                    sample_time = Usd.TimeCode(point_samples[0])
-                    points_value = mesh.GetPointsAttr().Get(sample_time)
-            if points_value is None or len(points_value) == 0:
-                continue
-
-            face_counts = mesh.GetFaceVertexCountsAttr().Get(usd_time)
-            face_indices = mesh.GetFaceVertexIndicesAttr().Get(usd_time)
+        default_time = (
+            Usd.TimeCode.Default() if time_code is None else Usd.TimeCode(time_code)
+        )
+
+        meshes: list[pvtk.PolyData] = []
+        for prim in Usd.PrimRange(root_prim):
+            if not prim.IsA(UsdGeom.Mesh):
+                continue
+
+            mesh = UsdGeom.Mesh(prim)
+            effective_time = default_time
+            points_value = mesh.GetPointsAttr().Get(effective_time)
+            if points_value is None and time_code is None:
+                point_samples = mesh.GetPointsAttr().GetTimeSamples()
+                if point_samples:
+                    effective_time = Usd.TimeCode(point_samples[0])
+                    points_value = mesh.GetPointsAttr().Get(effective_time)
+            if points_value is None or len(points_value) == 0:
+                continue
+
+            xform_cache = UsdGeom.XformCache(effective_time)
+            face_counts = mesh.GetFaceVertexCountsAttr().Get(effective_time)
+            face_indices = mesh.GetFaceVertexIndicesAttr().Get(effective_time)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/physiomotion4d/usd_tools.py` around lines 113 - 137, The points attribute
fallback picks a sample_time but the face attribute lookups and xform cache
still use usd_time (Default); update the logic so when time_code is None and you
fall back to a sampled time (sample_time) you use that same TimeCode for
subsequent reads and transform caching: set usd_time to sample_time (or pass
sample_time explicitly) before calling GetFaceVertexCountsAttr().Get(...) and
GetFaceVertexIndicesAttr().Get(...), and recreate or reinitialize xform_cache =
UsdGeom.XformCache(sample_time) so transforms are read at the same sampled time
as points (use the existing symbols time_code, usd_time, sample_time,
xform_cache, points_value, face_counts, face_indices).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@experiments/Heart-GatedCT_To_USD/1-register_images.py`:
- Around line 101-104: Before doing the unmasked whole-image registration, clear
any previously set fixed-image mask on the reusable `reg` instance so prior
dynamic/static-step masks don't persist; e.g., call the appropriate method to
clear the fixed mask (such as reg.clear_fixed_mask() or
reg.set_fixed_mask(None)) immediately before reg.set_fixed_image(fixed_image)
and reg.register(moving_image) in the block with reg.set_fixed_image and results
= reg.register(moving_image) (and apply the same clear-before-register change to
the other two occurrences around the reg.register calls at the other noted
locations).
- Around line 31-33: The fixed image is being read from the output_dir which may
be empty; change the source to use data_dir so fixed_image_filename is
constructed from data_dir (e.g. point the "slice_fixed.mha" path to data_dir)
and then call itk.imread on that path; update the variables referenced
(fixed_image_filename, fixed_image, output_dir → data_dir) so itk.imread loads
the input data instead of an output file.

In `@src/physiomotion4d/data_download_tools.py`:
- Around line 38-45: The current logic in DataDownloadTools returns any existing
path (data_file / SLICER_HEART_CT_FILENAME) without verifying integrity and uses
urllib.request.urlretrieve without timeout; change this to download to a
temporary file (e.g., data_file.with_suffix(".tmp") or similar), perform the
network request with an explicit timeout (use urllib.request.urlopen or set
socket timeout), write the response to the temp file, verify basic integrity
(e.g., non-zero size or optional checksum if available), then atomically replace
the target file with os.replace or Path.replace only on success; ensure
SLICER_HEART_CT_URL is the source and the final file name remains
SLICER_HEART_CT_FILENAME.

In `@tests/test_tutorials.py`:
- Around line 67-69: The test calls runpy.run_path(...) which by default does
not execute if __name__ == "__main__" blocks, causing tutorial scripts not to
set tutorial_results; update the runpy invocation in tests/test_tutorials.py
(the runpy.run_path call that uses script_name and expects tutorial_results) to
pass run_name="__main__" so the tutorial's main block executes and
tutorial_results is produced.

---

Nitpick comments:
In `@src/physiomotion4d/usd_tools.py`:
- Around line 113-137: The points attribute fallback picks a sample_time but the
face attribute lookups and xform cache still use usd_time (Default); update the
logic so when time_code is None and you fall back to a sampled time
(sample_time) you use that same TimeCode for subsequent reads and transform
caching: set usd_time to sample_time (or pass sample_time explicitly) before
calling GetFaceVertexCountsAttr().Get(...) and
GetFaceVertexIndicesAttr().Get(...), and recreate or reinitialize xform_cache =
UsdGeom.XformCache(sample_time) so transforms are read at the same sampled time
as points (use the existing symbols time_code, usd_time, sample_time,
xform_cache, points_value, face_counts, face_indices).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5c9bbfd7-69e4-40b7-844a-560f5b1fa7e4

📥 Commits

Reviewing files that changed from the base of the PR and between 6378ec8 and 23ab608.

📒 Files selected for processing (25)
  • .github/workflows/ci.yml
  • data/Slicer-Heart-CT/download_and_convert.py
  • docs/API_MAP.md
  • docs/api/cli/index.rst
  • docs/contributing.rst
  • experiments/Heart-GatedCT_To_USD/0-download_and_convert_4d_to_3d.py
  • experiments/Heart-GatedCT_To_USD/1-register_images.py
  • experiments/Heart-GatedCT_To_USD/3-transform_dynamic_and_static_contours.py
  • experiments/Heart-VTKSeries_To_USD/0-download_and_convert_4d_to_3d.py
  • pyproject.toml
  • src/physiomotion4d/convert_nrrd_4d_to_3d.py
  • src/physiomotion4d/convert_vtk_to_usd.py
  • src/physiomotion4d/data_download_tools.py
  • src/physiomotion4d/test_tools.py
  • src/physiomotion4d/usd_tools.py
  • src/physiomotion4d/vtk_to_usd/primvar_derivations.py
  • src/physiomotion4d/vtk_to_usd/usd_utils.py
  • src/physiomotion4d/workflow_convert_heart_gated_ct_to_usd.py
  • tests/conftest.py
  • tests/test_convert_nrrd_4d_to_3d.py
  • tests/test_tutorials.py
  • tests/test_vtk_to_usd_library.py
  • tutorials/tutorial_01_heart_gated_ct_to_usd.py
  • tutorials/tutorial_07_dirlab_pca_model.py
  • tutorials/tutorial_08_dirlab_pca_time_series.py
✅ Files skipped from review due to trivial changes (2)
  • docs/api/cli/index.rst
  • tutorials/tutorial_08_dirlab_pca_time_series.py
🚧 Files skipped from review as they are similar to previous changes (13)
  • data/Slicer-Heart-CT/download_and_convert.py
  • docs/contributing.rst
  • src/physiomotion4d/vtk_to_usd/primvar_derivations.py
  • .github/workflows/ci.yml
  • pyproject.toml
  • src/physiomotion4d/vtk_to_usd/usd_utils.py
  • tutorials/tutorial_07_dirlab_pca_model.py
  • tutorials/tutorial_01_heart_gated_ct_to_usd.py
  • experiments/Heart-GatedCT_To_USD/0-download_and_convert_4d_to_3d.py
  • src/physiomotion4d/workflow_convert_heart_gated_ct_to_usd.py
  • src/physiomotion4d/convert_vtk_to_usd.py
  • experiments/Heart-GatedCT_To_USD/3-transform_dynamic_and_static_contours.py
  • src/physiomotion4d/test_tools.py

Comment on lines +31 to 33
fixed_image_filename = output_dir / "slice_fixed.mha"
fixed_image = itk.imread(str(fixed_image_filename))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use the input data directory for the fixed image source.

Line 31 currently reads slice_fixed.mha from output_dir, which is an output folder and may be empty on first run, causing a read failure. Point this to data_dir instead.

Proposed fix
-    fixed_image_filename = output_dir / "slice_fixed.mha"
+    fixed_image_filename = data_dir / "slice_fixed.mha"
     fixed_image = itk.imread(str(fixed_image_filename))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fixed_image_filename = output_dir / "slice_fixed.mha"
fixed_image = itk.imread(str(fixed_image_filename))
fixed_image_filename = data_dir / "slice_fixed.mha"
fixed_image = itk.imread(str(fixed_image_filename))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@experiments/Heart-GatedCT_To_USD/1-register_images.py` around lines 31 - 33,
The fixed image is being read from the output_dir which may be empty; change the
source to use data_dir so fixed_image_filename is constructed from data_dir
(e.g. point the "slice_fixed.mha" path to data_dir) and then call itk.imread on
that path; update the variables referenced (fixed_image_filename, fixed_image,
output_dir → data_dir) so itk.imread loads the input data instead of an output
file.

Comment on lines +101 to +104
# Register the whole image
reg.set_fixed_image(fixed_image)
results = reg.register(moving_image)
inverse_transform = results["inverse_transform"]
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reset fixed-mask state before whole-image registration.

Because reg is reused, a mask set in prior dynamic/static steps can persist into later iterations. At Line 101+, clear the fixed mask before the unmasked registration pass.

Proposed fix
         # Register the whole image
         reg.set_fixed_image(fixed_image)
+        reg.set_fixed_mask(None)
         results = reg.register(moving_image)

Also applies to: 132-134, 169-171

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@experiments/Heart-GatedCT_To_USD/1-register_images.py` around lines 101 -
104, Before doing the unmasked whole-image registration, clear any previously
set fixed-image mask on the reusable `reg` instance so prior dynamic/static-step
masks don't persist; e.g., call the appropriate method to clear the fixed mask
(such as reg.clear_fixed_mask() or reg.set_fixed_mask(None)) immediately before
reg.set_fixed_image(fixed_image) and reg.register(moving_image) in the block
with reg.set_fixed_image and results = reg.register(moving_image) (and apply the
same clear-before-register change to the other two occurrences around the
reg.register calls at the other noted locations).

Comment thread src/physiomotion4d/data_download_tools.py
Comment thread tests/test_tutorials.py Outdated
Copilot AI review requested due to automatic review settings May 13, 2026 12:07
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 126 out of 132 changed files in this pull request and generated 3 comments.

Comment on lines +504 to +509
up_axis = UsdGeom.GetStageUpAxis(stage)
if up_axis == UsdGeom.Tokens.z:
offset = np.array([0.0, distance, 0.0])
else:
offset = np.array([0.0, 0.0, distance])
camera_position = center + offset
Comment on lines 163 to +170
# ConvertVTKToUSD places prims at /World/{basename}/{part_name}.
# Discover the target prim dynamically so the path stays valid regardless
# of how many connected components or cell types the VTK file produces.
mesh_paths = usd_tools.list_mesh_paths_under(stage, parent_path="/World/TPV25Valve")
if separate_by == "connectivity":
mesh_paths = usd_tools.list_mesh_paths_under(stage, parent_path="/World/TPV25Valve")
candidates = [
p for p in mesh_paths if p.split("/")[-1].startswith("TPV25Valve_object")
]
vessel_path = candidates[-1] if candidates else "/World/TPV25Valve/Mesh"
vessel_path = "/World/TPV25Valve/TPV25Valve_object4"
elif separate_by == "cell_type":
mesh_paths = usd_tools.list_mesh_paths_under(stage, parent_path="/World/TPV25Valve")
triangle_paths = [p for p in mesh_paths if p.split("/")[-1].endswith("_Triangle")]
vessel_path = triangle_paths[0] if triangle_paths else "/World/TPV25Valve/Mesh"
vessel_path = "/World/TPV25Valve/TPV25Valve_Triangle"
Comment on lines 165 to +173
usd_tools = USDTools()
# ConvertVTKToUSD places prims at /World/{basename}/{part_name}.
# Discover the target prim dynamically so the path stays valid regardless
# of how many connected components or cell types the VTK file produces.
mesh_paths = usd_tools.list_mesh_paths_under(stage, parent_path="/World/AlterraValve")
if separate_by == "connectivity":
mesh_paths = usd_tools.list_mesh_paths_under(
stage, parent_path="/World/AlterraValve"
)
candidates = [
p for p in mesh_paths if p.split("/")[-1].startswith("AlterraValve_object")
]
vessel_path = candidates[-1] if candidates else "/World/AlterraValve/Mesh"
vessel_path = "/World/AlterraValve/AlterraValve_object3"
elif separate_by == "cell_type":
mesh_paths = usd_tools.list_mesh_paths_under(
stage, parent_path="/World/AlterraValve"
)
triangle_paths = [p for p in mesh_paths if p.split("/")[-1].endswith("_Triangle")]
vessel_path = triangle_paths[0] if triangle_paths else "/World/AlterraValve/Mesh"
vessel_path = "/World/AlterraValve/AlterraValve_Triangle"
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/physiomotion4d/test_tools.py (1)

462-462: 💤 Low value

Remove redundant import os.

os is already imported at module level (line 12), so this local import is unnecessary. The sys import on the next line is fine since it's only used in this method.

♻️ Proposed fix
-    import os
     import sys
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/physiomotion4d/test_tools.py` at line 462, Remove the redundant local
"import os" inside the function in src/physiomotion4d/test_tools.py and rely on
the module-level os import; keep the local "import sys" as-is. Locate the stray
"import os" statement near the other local imports in the function (adjacent to
the local "import sys") and delete it so there is a single module-level os
import.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/physiomotion4d/data_download_tools.py`:
- Around line 49-67: The tmp_file currently used in DataDownloadTools.download
(tmp_file = data_file.with_suffix(...)) is a fixed path and can be clobbered by
concurrent callers; change the implementation to create a unique temporary file
in the same directory (e.g., use
tempfile.NamedTemporaryFile(dir=data_file.parent, delete=False) or generate a
UUID-based filename) so each call gets its own temp path, write the download
into that unique temp file, verify size and then atomically replace data_file
(tmp.replace(data_file) or os.replace), and ensure the existing cleanup logic
(unlink on exception) removes the unique temp file instead of the constant name.

In `@src/physiomotion4d/test_tools.py`:
- Around line 474-503: When pv.start_xvfb() is called you must ensure
pv.stop_xvfb() runs even if USDTools().load_usd_as_vtk(...) raises; change the
flow so starting Xvfb and setting xvfb_started is immediately followed by a
try/finally that calls pv.stop_xvfb() in the finally, then perform
USDTools.load_usd_as_vtk, plotter creation and screenshot inside that try block;
reference pv.start_xvfb / pv.stop_xvfb and USDTools().load_usd_as_vtk() and
ensure plotter.close() remains in its own finally or the same inner try/finally
so both plotter.close() and pv.stop_xvfb() are always executed.

---

Nitpick comments:
In `@src/physiomotion4d/test_tools.py`:
- Line 462: Remove the redundant local "import os" inside the function in
src/physiomotion4d/test_tools.py and rely on the module-level os import; keep
the local "import sys" as-is. Locate the stray "import os" statement near the
other local imports in the function (adjacent to the local "import sys") and
delete it so there is a single module-level os import.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5a01f19a-8cb9-45cd-ae19-41f827249740

📥 Commits

Reviewing files that changed from the base of the PR and between 23ab608 and 1101821.

📒 Files selected for processing (9)
  • docs/API_MAP.md
  • experiments/Heart-GatedCT_To_USD/1-register_images.py
  • src/physiomotion4d/anatomy_taxonomy.py
  • src/physiomotion4d/data_download_tools.py
  • src/physiomotion4d/test_tools.py
  • src/physiomotion4d/vtk_to_usd/usd_mesh_converter.py
  • src/physiomotion4d/vtk_to_usd/usd_utils.py
  • tests/test_register_time_series_images.py
  • tests/test_tutorials.py
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/physiomotion4d/anatomy_taxonomy.py
  • experiments/Heart-GatedCT_To_USD/1-register_images.py
  • src/physiomotion4d/vtk_to_usd/usd_utils.py
  • tests/test_tutorials.py

Comment thread src/physiomotion4d/data_download_tools.py Outdated
Comment thread src/physiomotion4d/test_tools.py
Copilot AI review requested due to automatic review settings May 13, 2026 13:07
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 126 out of 132 changed files in this pull request and generated 7 comments.

Comment thread tutorials/README.md
Comment on lines +28 to 36
Each tutorial is a standalone percent-cell Python script (`# %%`) that can be
run cell-by-cell in VS Code or Cursor without Jupyter packages. Paths are
defined near the top of each script. By default, data is read from the
repository `data/` directory and outputs are written under `output/`.

```bash
python tutorials/tutorial_01_heart_gated_ct_to_usd.py \
--data-dir ./data --output-dir ./output

python tutorials/tutorial_02_ct_to_vtk.py \
--data-dir ./data --output-dir ./output
# In VS Code/Cursor, open the tutorial and run the final cell:
tutorial_results = run_tutorial()
```
Comment on lines +1 to +6
"""Tutorial 7: Build and fit a PCA lung-lobe model from DirLab 4D CT cases.

This tutorial uses one respiratory phase from each available DirLab case. It
segments the five lung lobes, builds a surface PCA model from all but two
cases, then fits that model to every available case.
"""
Comment on lines +1 to +15
"""
Tutorial 8: Propagate DirLab PCA lung-lobe fits through each 4D CT time series.

This tutorial uses the per-case PCA-fitted reference meshes created by Tutorial
7. For each DirLab case, it registers every respiratory phase to the reference
phase used by Tutorial 7, saves the image transforms, and applies the
reference-to-phase transform to the fitted mesh so each time point has a
PCA-derived lung-lobe surface.

Data Required
-------------
See data/README.md for DirLab-4DCT download instructions. Run Tutorial 7 first
so ``output/tutorial_07_dirlab_pca_model/fits`` contains the per-case fitted
reference-stage meshes.
"""
Comment on lines +139 to +150
world_matrix = xform_cache.GetLocalToWorldTransform(prim)
points = np.asarray(
[
tuple(
world_matrix.Transform(
Gf.Vec3d(float(point[0]), float(point[1]), float(point[2]))
)
)
for point in points_value
],
dtype=np.float32,
)
Comment on lines 162 to +170
usd_tools = USDTools()
# ConvertVTKToUSD places prims at /World/{basename}/{part_name}.
# Discover the target prim dynamically so the path stays valid regardless
# of how many connected components or cell types the VTK file produces.
mesh_paths = usd_tools.list_mesh_paths_under(stage, parent_path="/World/TPV25Valve")
if separate_by == "connectivity":
mesh_paths = usd_tools.list_mesh_paths_under(stage, parent_path="/World/TPV25Valve")
candidates = [
p for p in mesh_paths if p.split("/")[-1].startswith("TPV25Valve_object")
]
vessel_path = candidates[-1] if candidates else "/World/TPV25Valve/Mesh"
vessel_path = "/World/TPV25Valve/TPV25Valve_object4"
elif separate_by == "cell_type":
mesh_paths = usd_tools.list_mesh_paths_under(stage, parent_path="/World/TPV25Valve")
triangle_paths = [p for p in mesh_paths if p.split("/")[-1].endswith("_Triangle")]
vessel_path = triangle_paths[0] if triangle_paths else "/World/TPV25Valve/Mesh"
vessel_path = "/World/TPV25Valve/TPV25Valve_Triangle"
Comment on lines 165 to 175
usd_tools = USDTools()
# ConvertVTKToUSD places prims at /World/{basename}/{part_name}.
# Discover the target prim dynamically so the path stays valid regardless
# of how many connected components or cell types the VTK file produces.
mesh_paths = usd_tools.list_mesh_paths_under(stage, parent_path="/World/AlterraValve")
if separate_by == "connectivity":
mesh_paths = usd_tools.list_mesh_paths_under(
stage, parent_path="/World/AlterraValve"
)
candidates = [
p for p in mesh_paths if p.split("/")[-1].startswith("AlterraValve_object")
]
vessel_path = candidates[-1] if candidates else "/World/AlterraValve/Mesh"
vessel_path = "/World/AlterraValve/AlterraValve_object3"
elif separate_by == "cell_type":
mesh_paths = usd_tools.list_mesh_paths_under(
stage, parent_path="/World/AlterraValve"
)
triangle_paths = [p for p in mesh_paths if p.split("/")[-1].endswith("_Triangle")]
vessel_path = triangle_paths[0] if triangle_paths else "/World/AlterraValve/Mesh"
vessel_path = "/World/AlterraValve/AlterraValve_Triangle"
else:
vessel_path = "/World/AlterraValve/Mesh"
Comment on lines 144 to 147
## 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 notebooks can use reduced parameters (e.g. fewer iterations, fewer files) and finish faster. Notebooks 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.notebook_utils.running_as_test()` helper.
When experiment tests run (with `--run-experiments`), the test runner also sets **`PHYSIOMOTION_RUNNING_AS_TEST=1`** so that notebooks can use reduced parameters (e.g. fewer iterations, fewer files) and finish faster. Notebooks 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.

Copilot AI review requested due to automatic review settings May 13, 2026 14:58
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (1)
tests/EXPERIMENT_TESTS_GUIDE.md (1)

211-217: 💤 Low value

Consider adding a language specifier to the fenced code block.

The code block showing script execution output doesn't specify a language. Adding text or console would make the formatting intent clearer and satisfy the markdownlint rule.

📝 Optional improvement
-```
+```text
 ================================================================================
 Executing script: 1-register_images.py
 Path: experiments/Heart-GatedCT_To_USD/1-register_images.py
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/EXPERIMENT_TESTS_GUIDE.md` around lines 211 - 217, The fenced code
block that contains the script execution output (the triple-backtick block
starting with
"================================================================================"
and the "Executing script: 1-register_images.py" lines) lacks a language
specifier; update that opening fence from ``` to ```text (or ```console) so the
block is explicitly marked as plain text and satisfies markdownlint; make the
change in the EXPERIMENT_TESTS_GUIDE.md fenced block that contains the example
output.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@data/README.md`:
- Around line 282-284: Insert a single blank line between the "### Summary of
Download Methods" heading and the following table (the pipe-delimited table
starting with "| Dataset | Auto-Download | ...") so the heading and table are
separated; this fixes markdownlint MD058 by ensuring there is an empty line
before the table.

In `@README.md`:
- Line 812: There are duplicate "## Documentation" headings causing ambiguous
anchors (MD024); locate the second occurrence of the heading (the literal line
"## Documentation") and rename it to a unique title (e.g., "## Additional
Documentation" or "## Documentation (Supplementary)"), then update any internal
links/TOC entries that pointed to the old "documentation" anchor so they
reference the new heading text.

In `@statistics.md`:
- Line 36: The documentation row in statistics.md showing " ~7,800 md" is
incorrect; update the Markdown count in the "Documentation (`docs/` + repo
READMEs)" row to the accurate value (~6,701 or rounded to ~6,700) and adjust the
combined lines and percentage accordingly (update the combined total to ~13,000
lines and the percent to ~31% of 42,000), or alternatively recompute and replace
both the markdown count and the overall combined/percentage values so the row is
consistent and accurate.

In `@tests/EXPERIMENT_TESTS_GUIDE.md`:
- Around line 168-172: The Option 1 example uses os.environ.get but omits the
required import; add "import os" before the inline statement so the example is
runnable—update the snippet containing running_as_test to include the import
line above it.

In `@tests/PARALLEL_EXECUTION_GUIDE.md`:
- Around line 158-160: The fenced code block containing the line "0-download.py
-> 1-process.py -> 2-analyze.py -> 3-visualize.py" is missing a language tag
(triggering markdownlint MD040); update the block start from ``` to ```text so
it becomes a fenced text block (e.g., change the opening fence to ```text) to
satisfy the linter while keeping the content identical.

---

Nitpick comments:
In `@tests/EXPERIMENT_TESTS_GUIDE.md`:
- Around line 211-217: The fenced code block that contains the script execution
output (the triple-backtick block starting with
"================================================================================"
and the "Executing script: 1-register_images.py" lines) lacks a language
specifier; update that opening fence from ``` to ```text (or ```console) so the
block is explicitly marked as plain text and satisfies markdownlint; make the
change in the EXPERIMENT_TESTS_GUIDE.md fenced block that contains the example
output.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 61aff6a2-3b95-45f5-9bd3-4c1ff07d73f9

📥 Commits

Reviewing files that changed from the base of the PR and between 1101821 and 057d98a.

📒 Files selected for processing (31)
  • .github/workflows/README.md
  • CLAUDE.md
  • README.md
  • data/KCL-Heart-Model/README.md
  • data/README.md
  • docs/API_MAP.md
  • docs/README.md
  • docs/cli_scripts/create_statistical_model.rst
  • docs/contributing.rst
  • docs/examples.rst
  • docs/quickstart.rst
  • experiments/Convert_VTK_To_USD/convert_chop_alterra_valve_to_usd.py
  • experiments/Convert_VTK_To_USD/convert_chop_tpv25_valve_to_usd.py
  • experiments/Heart-Create_Statistical_Model/README.md
  • experiments/Heart-Simpleware_Segmentation/README.md
  • experiments/README.md
  • src/physiomotion4d/data_download_tools.py
  • src/physiomotion4d/segment_anatomy_base.py
  • src/physiomotion4d/segment_chest_total_segmentator.py
  • src/physiomotion4d/simpleware_medical/README.md
  • src/physiomotion4d/test_tools.py
  • src/physiomotion4d/usd_tools.py
  • statistics.md
  • tests/EXPERIMENT_FLAG_USAGE.md
  • tests/EXPERIMENT_TESTS_GUIDE.md
  • tests/EXPERIMENT_TESTS_SUMMARY.md
  • tests/PARALLEL_EXECUTION_GUIDE.md
  • tests/README.md
  • tests/TESTING_GUIDE.md
  • tests/test_convert_nrrd_4d_to_3d.py
  • tutorials/README.md
✅ Files skipped from review due to trivial changes (10)
  • src/physiomotion4d/simpleware_medical/README.md
  • CLAUDE.md
  • data/KCL-Heart-Model/README.md
  • docs/cli_scripts/create_statistical_model.rst
  • docs/examples.rst
  • docs/README.md
  • tests/EXPERIMENT_FLAG_USAGE.md
  • experiments/Heart-Create_Statistical_Model/README.md
  • experiments/Heart-Simpleware_Segmentation/README.md
  • docs/quickstart.rst
🚧 Files skipped from review as they are similar to previous changes (10)
  • tests/TESTING_GUIDE.md
  • experiments/README.md
  • tests/test_convert_nrrd_4d_to_3d.py
  • src/physiomotion4d/segment_chest_total_segmentator.py
  • experiments/Convert_VTK_To_USD/convert_chop_tpv25_valve_to_usd.py
  • src/physiomotion4d/test_tools.py
  • src/physiomotion4d/segment_anatomy_base.py
  • src/physiomotion4d/data_download_tools.py
  • experiments/Convert_VTK_To_USD/convert_chop_alterra_valve_to_usd.py
  • src/physiomotion4d/usd_tools.py

Comment thread data/README.md
Comment thread README.md Outdated
Comment thread statistics.md Outdated
Comment thread tests/EXPERIMENT_TESTS_GUIDE.md Outdated
Comment thread tests/PARALLEL_EXECUTION_GUIDE.md Outdated
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 132 out of 138 changed files in this pull request and generated 3 comments.

Comment thread tests/conftest.py
Comment on lines 325 to 333
try:
urllib.request.urlretrieve(input_image_url, str(input_image_filename))
print(f"Downloaded to {input_image_filename}")
except urllib.error.URLError as e:
input_image_filename = DataDownloadTools.DownloadSlicerHeartCTData(data_dir)
print(f"\nSlicer-Heart-CT data ready: {input_image_filename}")
except OSError as e:
msg = (
f"Could not download test data: {e}. "
f"Please manually place TruncalValve_4DCT.seq.nrrd in {data_dir}"
"Please manually place "
f"{DataDownloadTools.SLICER_HEART_CT_FILENAME} in {data_dir}"
)
Comment on lines +113 to +139
if time_code is None:
usd_time = Usd.TimeCode.Default()
else:
usd_time = Usd.TimeCode(time_code)
xform_cache = UsdGeom.XformCache(usd_time)

meshes: list[pvtk.PolyData] = []
for prim in Usd.PrimRange(root_prim):
if not prim.IsA(UsdGeom.Mesh):
continue

mesh = UsdGeom.Mesh(prim)
points_value = mesh.GetPointsAttr().Get(usd_time)
if points_value is None and time_code is None:
point_samples = mesh.GetPointsAttr().GetTimeSamples()
if point_samples:
sample_time = Usd.TimeCode(point_samples[0])
points_value = mesh.GetPointsAttr().Get(sample_time)
if points_value is None or len(points_value) == 0:
continue

face_counts = mesh.GetFaceVertexCountsAttr().Get(usd_time)
face_indices = mesh.GetFaceVertexIndicesAttr().Get(usd_time)
if face_counts is None or face_indices is None:
continue

world_matrix = xform_cache.GetLocalToWorldTransform(prim)
Comment on lines +369 to +385
def triangulate_face(
face_counts: NDArray, face_indices: NDArray
) -> tuple[NDArray, NDArray, NDArray]:
"""Triangulate polygonal faces using fan triangulation.

Args:
face_counts: Array of vertex counts per face
face_indices: Array of vertex indices
face_counts: Array of vertex counts per source face (length F).
face_indices: Flat array of vertex indices.

Returns:
tuple: (triangulated_counts, triangulated_indices)
``(tri_counts, tri_indices, source_face_index_per_triangle)``:
- ``tri_counts``: int32 array, all entries equal to 3.
- ``tri_indices``: int32 flat array of triangle vertex indices.
- ``source_face_index_per_triangle``: int32 array mapping each output
triangle back to its source face in ``face_counts``. Length matches
``tri_counts``. Use this to expand uniform (per-face) primvar data
to match the triangulated face count: ``new_data = old_data[mapping]``.
@aylward aylward merged commit 2c9ffa7 into Project-MONAI:main May 13, 2026
12 checks passed
@aylward aylward deleted the tutorials_testing branch May 13, 2026 15:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants