Skip to content

ENH: Registration fixes - #118

Merged
aylward merged 6 commits into
Project-MONAI:mainfrom
aylward:fix_greedy
Aug 7, 2026
Merged

ENH: Registration fixes#118
aylward merged 6 commits into
Project-MONAI:mainfrom
aylward:fix_greedy

Conversation

@aylward

@aylward aylward commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

BUG: Convert Greedy transforms from RAS to LPS, unify registration seeding

Greedy reports its affine in RAS while ITK is LPS, but the 4x4 was copied
straight into an itk.AffineTransform, negating x and y. Recovering a known
(6, -4, 3) mm shift returned (+5.96, -4.01, -2.92) instead of (-6, +4, -3),
and warping by the result scored below the unregistered pair (foreground NCC
0.21 vs 0.32). Rigid, Affine and Deformable were all affected; the
displacement field was already LPS and is left alone. Existing Greedy tests
only asserted the transforms were non-None, so the sign error survived.

  • Change basis in RegisterImagesGreedy._matrix_to_itk_affine
  • Add known-shift accuracy tests for Greedy, ANTs and ICON (a KnownShiftCase
    helper in conftest); ANTs and ICON were audited and are correct
  • Replace per-backend initial_forward_transform handling, which pre-warped
    in ANTs, pre-warped only the image in ICON, and double-applied in Greedy,
    with one RegisterImagesBase.register_from()
  • Add TransformTools.invert_transform, preferring the analytic inverse over a
    displacement field that is only defined on the reference grid
  • Remove prior_weight from RegisterTimeSeriesImages, the reconstruction
    workflow and its CLI flag
  • Offer explicit Rigid/Similarity/Affine modes in RegisterModelsICP, with
    bounding-box scale estimation before ICP
  • Add ImageTools.pad_image; drop icon_iterations from
    RegisterModelsDistanceMaps.register()
  • Fix PhaseSampleDataset caching (0 now means unbounded, as documented) and
    include Case8Deploy in tutorial 09's case discovery
  • Regenerate registration_time_series_images baselines

Summary by CodeRabbit

  • New Features

    • Added anatomy-aware VTK-to-USD conversion with per-structure materials and object naming.
    • Added registration refinement from an existing transform.
    • Added image padding and improved PCA-based registration options.
    • Added a lung distance-map ICON fine-tuning tutorial.
    • Expanded ICP registration support for rigid, similarity, and affine modes.
  • Updates

    • Renamed PCA component CLI options for consistency.
    • Removed temporal prior-weight configuration from 4D CT reconstruction.
    • Improved distance-map processing and transform inversion.
  • Documentation

    • Updated tutorials, conversion guidance, and testing instructions.

aylward added 2 commits August 7, 2026 06:31
…tive

RegisterModelsPCA previously maximized mean intensity sampled from the fixed
distance map through ITK's LinearInterpolateImageFunction, with the optimizer
estimating gradients by finite differences. The objective is now stated and
minimized directly:

    mean distance(model -> target)
  + w * mean distance(target -> model)     # symmetric term
  + lambda * sum(b_i^2)                    # Mahalanobis shape prior

Because the PCA deformation is linear in the coefficients b, the gradient is
analytic and handed to the optimizer instead of being estimated, which removes
one objective evaluation per coefficient per step. The post-PCA transform is
folded into the mode directions so the gradient stays exact; when that
transform is not affine its Jacobian is not constant, so the analytic gradient
is disabled and finite differences are used with a logged warning.

- Add symmetric_weight (default 0.5) so partial target coverage is penalized,
  and pca_prior_weight (default 0.0, disabled) for the Mahalanobis prior
- Sample the distance map and its gradient with scipy.ndimage.map_coordinates,
  and build the target-to-model term with a scipy.spatial.cKDTree
- Replace the cached ITK interpolator and _create_itk_points with
  _prepare_sampling, which builds the arrays the objective is made of once
- Add ContourTools.sample_mesh_faces for face-density-aware point sampling and
  a negative_inside option on the signed distance map
- Log transform fidelity after computing the PCA transforms

Tests cover the pieces that were previously unverified: the analytic gradient
against finite differences, recovery of known coefficients, the symmetric term
penalizing partial coverage, the prior shrinking coefficients, eigenvector
scaling by standard deviation, deformation happening in the template frame,
and a transform round trip.
…eding

Greedy reports its affine in RAS while ITK is LPS, but the 4x4 was copied
straight into an itk.AffineTransform, negating x and y. Recovering a known
(6, -4, 3) mm shift returned (+5.96, -4.01, -2.92) instead of (-6, +4, -3),
and warping by the result scored below the unregistered pair (foreground NCC
0.21 vs 0.32). Rigid, Affine and Deformable were all affected; the
displacement field was already LPS and is left alone. Existing Greedy tests
only asserted the transforms were non-None, so the sign error survived.

- Change basis in RegisterImagesGreedy._matrix_to_itk_affine
- Add known-shift accuracy tests for Greedy, ANTs and ICON (a KnownShiftCase
  helper in conftest); ANTs and ICON were audited and are correct
- Replace per-backend initial_forward_transform handling, which pre-warped
  in ANTs, pre-warped only the image in ICON, and double-applied in Greedy,
  with one RegisterImagesBase.register_from()
- Add TransformTools.invert_transform, preferring the analytic inverse over a
  displacement field that is only defined on the reference grid
- Remove prior_weight from RegisterTimeSeriesImages, the reconstruction
  workflow and its CLI flag
- Offer explicit Rigid/Similarity/Affine modes in RegisterModelsICP, with
  bounding-box scale estimation before ICP
- Add ImageTools.pad_image; drop icon_iterations from
  RegisterModelsDistanceMaps.register()
- Fix PhaseSampleDataset caching (0 now means unbounded, as documented) and
  include Case8Deploy in tutorial 09's case discovery
- Regenerate registration_time_series_images baselines
Copilot AI lite review requested due to automatic review settings August 7, 2026 10:44
@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@aylward, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 20 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: e002d0d6-cbd7-4260-af6c-f1b7d441e6a3

📥 Commits

Reviewing files that changed from the base of the PR and between 5b29e24 and f66fd0c.

📒 Files selected for processing (6)
  • AGENTS.md
  • CLAUDE.md
  • docs/tutorials.rst
  • src/physiotwin4d/workflow_fit_statistical_model_to_patient.py
  • tutorials/tutorial_02_lung_distancemap_finetune_icon.py
  • tutorials/tutorial_02_lung_finetune_icon.py

Walkthrough

This PR updates registration APIs and model-fitting logic, expands VTK-to-USD surface metadata handling, adds a lung ICON fine-tuning tutorial, renames PCA options, removes temporal prior weighting, and replaces automated experiment tests with tutorial-focused testing guidance.

Changes

Registration and model fitting

Layer / File(s) Summary
Seeded registration and registration backends
src/physiotwin4d/register_images_*.py, tests/test_register_images_*.py, docs/developer/registration_images.rst
Seeded registration now uses register_from() with prewarping and transform composition. Chained, ANTs, Greedy, and ICON paths use the updated API.
ICP, PCA, and distance-map registration
src/physiotwin4d/register_models_*.py, tests/test_register_models_pca.py
ICP adds similarity and staged affine modes. PCA registration adds distance-based objectives, gradients, symmetry, priors, and post-transforms. Distance-map registration adds configurable normalization and ICON checkpoint support.
Registration utilities and workflows
src/physiotwin4d/image_tools.py, src/physiotwin4d/transform_tools.py, src/physiotwin4d/workflow_*.py
Image padding and transform inversion are added. PCA workflow integration is updated. Time-series prior-weight configuration is removed.

Surface conversion and tutorials

Layer / File(s) Summary
Surface metadata and USD conversion
src/physiotwin4d/contour_tools.py, src/physiotwin4d/*usd*.py, tests/test_*convert_vtk_to_usd.py
Merged surfaces preserve per-cell segmentation IDs. USD conversion supports structure-derived names, anatomy material resolution, explicit overrides, and fallbacks.
Tutorial pipelines and fine-tuning
tutorials/*, tests/test_tutorials.py
Tutorials use per-structure surfaces and renamed PCA parameters. A lung distance-map ICON fine-tuning tutorial is added. PhysicsNeMo case discovery and validation reporting are updated.

Testing and experiment execution

Layer / File(s) Summary
Test categories and CI guidance
.github/workflows/*, tests/README.md, tests/conftest.py, docs/testing.rst, statistics.md
The experiment marker and command are removed. Tutorial tests are the documented opt-in end-to-end suite.
Manual experiment scripts
experiments/*, experiments/README.md, pyproject.toml
Experiment scripts no longer use test-mode shortcuts, subsampling, early exits, or visualization guards. Documentation identifies them as manually executed exploratory scripts.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

✨ Finishing Touches 💡 1
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch fix_greedy
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Copilot AI left a comment

Copy link
Copy Markdown

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 fixes several registration correctness issues (notably Greedy’s RAS↔LPS affine basis mismatch) and standardizes how registrations are seeded across backends by introducing a single RegisterImagesBase.register_from() composition path. It also expands registration validation with “known shift” accuracy tests, improves PCA/ICP model-registration behavior, and removes prior_weight-based time-series smoothing in favor of independent per-frame registration.

Changes:

  • Fix Greedy affine conversion by changing basis from Greedy’s RAS convention into the project-wide ITK LPS convention, and add known-shift accuracy tests for Greedy/ANTs/ICON.
  • Introduce RegisterImagesBase.register_from() to pre-warp + refine + compose consistently across backends; update chain/backends/tests/docs accordingly.
  • Enhance model registration tooling (PCA objective/gradient, ICP modes including Similarity, distance-map registration knobs), and remove prior_weight from time-series workflows/CLI/tests with regenerated baselines.

Reviewed changes

Copilot reviewed 35 out of 35 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
tutorials/tutorial_09_lung_train_physicsnemo_mgn.py Clarifies dataset splitting/validation behavior; logs skipped case reasons; includes Case8Deploy discovery.
tutorials/tutorial_07_lung_fit_statistical_model_to_patient.py Makes PCA mode count explicit, adjusts mask dilation, and saves an additional PCA-registered surface artifact.
tutorials/tutorial_06_lung_create_statistical_model.py Renames PCA mode variable and changes how many modes are visualized/saved.
tests/test_register_time_series_images.py Removes prior_weight usage and updates a test scenario/name and output artifacts.
tests/test_register_models_pca.py Adds extensive PCA registrar unit tests (mode validation, deformation math, gradients, priors, symmetric term, transform fidelity).
tests/test_register_images_icon.py Adds known-shift recovery test; updates seeding test to use register_from().
tests/test_register_images_greedy.py Adds known-shift recovery test across transform types; regression guard for RAS/LPS sign errors.
tests/test_register_images_chain.py Updates chain behavior assertions to validate pre-warp + composed transform behavior numerically.
tests/test_register_images_ants.py Adds known-shift recovery test; migrates initial transform tests to register_from().
tests/conftest.py Adds KnownShiftCase helper + session fixture for deterministic translation-based accuracy tests.
tests/baselines/registration_time_series_images/transform_application_time_series_0.mha Updates baseline pointer after time-series registration behavior change.
tests/baselines/registration_time_series_images/prior_time_series_registered_0.mha Removes obsolete baseline (prior-weight path removed).
tests/baselines/registration_time_series_images/prior_forward_transform_0.hdf Removes obsolete baseline (prior-weight path removed).
tests/baselines/registration_time_series_images/middle_frame_forward_transform_0.hdf Adds new baseline artifact for “middle reference frame” test.
tests/baselines/registration_time_series_images/basic_time_series_registered_0.mha Updates baseline pointer after registration behavior change.
tests/baselines/registration_time_series_images/basic_forward_transform_0.hdf Updates baseline pointer after registration behavior change.
src/physiotwin4d/workflow_reconstruct_highres_4d_ct.py Removes prior_weight parameter/plumbing and associated logging.
src/physiotwin4d/workflow_fit_statistical_model_to_patient.py Fixes PCA frame handling (post-PCA transform usage) and refines labelmap/model transform composition; pads patient image for distance-map registration.
src/physiotwin4d/transform_tools.py Adds generic invert_transform() (analytic when possible; fallback to field inversion) and tightens displacement-field inversion controls.
src/physiotwin4d/train_physicsnemo_mgn.py Adds processor gradient-checkpoint segment configuration to trade compute for GPU memory.
src/physiotwin4d/register_time_series_images.py Removes prior-based smoothing/selection logic; registers frames independently; updates docs/notes.
src/physiotwin4d/register_models_pca.py Overhauls PCA registration objective (distance-map minimization), adds symmetric term + priors, analytic gradient, and improves transform/field fidelity reporting.
src/physiotwin4d/register_models_icp.py Adds explicit Rigid/Similarity/Affine pipelines with bounding-box scaling and refactors ICP staging.
src/physiotwin4d/register_models_distance_maps.py Adds distance normalization knob and changes mask creation behavior; removes ICON iteration parameter from the API.
src/physiotwin4d/register_images_icon.py Removes backend-specific initial-transform pre-warp/composition (now centralized in register_from()).
src/physiotwin4d/register_images_greedy.py Fixes RAS→LPS affine conversion; removes backend-specific initial-transform handling; clarifies deformable seeding.
src/physiotwin4d/register_images_chain.py Updates chaining semantics to pre-warp and refine via register_from() composition rather than passing an “initial transform” into backends.
src/physiotwin4d/register_images_base.py Removes initial_forward_transform from the backend method contracts and introduces register_from() + shared pre-warp/composition helpers.
src/physiotwin4d/register_images_ants.py Removes backend-specific initial-transform handling; updates examples to register_from().
src/physiotwin4d/physicsnemo_tools.py Fixes caching semantics so _cache_max_samples == 0 means unbounded (as documented).
src/physiotwin4d/image_tools.py Adds ImageTools.pad_image() with correct origin handling and flexible per-axis padding specification.
src/physiotwin4d/contour_tools.py Adds face sampling for mesh rasterization; updates distance-map creation to optionally use face samples for smoother maps.
src/physiotwin4d/cli/reconstruct_highres_4d_ct.py Removes --prior-weight CLI flag and associated validation/plumbing.
experiments/Heart-Create_Statistical_Model/README.md Updates guidance for adjusting ICON iterations now that icon_iterations param was removed.
docs/developer/registration_images.rst Documents seeding via register_from() and updates chain description accordingly.
Suppressed comments (1)

src/physiotwin4d/register_models_distance_maps.py:258

  • Same issue as the fixed-side debug writes: moving_mask_image can be None when mask_dilation_mm <= 0, so itk.imwrite(self.moving_mask_image, ...) will raise. Debug file writes should be gated/optional and avoid writing None images.
        itk.imwrite(
            self.moving_mask_image, "debug_moving_mask_image.nii.gz", compression=True
        )
        itk.imwrite(
            self.moving_distance_map_image,

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

Comment thread src/physiotwin4d/register_models_distance_maps.py Outdated
Comment thread tutorials/tutorial_06_lung_create_statistical_model.py
Comment thread src/physiotwin4d/register_images_base.py
Comment thread src/physiotwin4d/register_models_distance_maps.py
@codecov

codecov Bot commented Aug 7, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 62.22222% with 221 lines in your changes missing coverage. Please review.
✅ Project coverage is 42.18%. Comparing base (62cd18c) to head (f66fd0c).

Files with missing lines Patch % Lines
src/physiotwin4d/register_models_icp.py 7.14% 52 Missing ⚠️
src/physiotwin4d/image_tools.py 10.25% 35 Missing ⚠️
src/physiotwin4d/register_models_pca.py 86.63% 29 Missing ⚠️
...win4d/workflow_fit_statistical_model_to_patient.py 17.14% 29 Missing ⚠️
src/physiotwin4d/register_images_greedy.py 4.16% 23 Missing ⚠️
src/physiotwin4d/register_models_distance_maps.py 7.14% 13 Missing ⚠️
src/physiotwin4d/register_images_base.py 72.97% 10 Missing ⚠️
src/physiotwin4d/register_time_series_images.py 0.00% 10 Missing ⚠️
src/physiotwin4d/workflow_convert_vtk_to_usd.py 90.19% 5 Missing ⚠️
src/physiotwin4d/contour_tools.py 92.30% 4 Missing ⚠️
... and 5 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #118      +/-   ##
==========================================
+ Coverage   36.60%   42.18%   +5.58%     
==========================================
  Files          72       72              
  Lines        8510     8742     +232     
==========================================
+ Hits         3115     3688     +573     
+ Misses       5395     5054     -341     
Flag Coverage Δ
integration-tests 41.99% <62.22%> (?)
unittests 42.18% <62.22%> (+5.58%) ⬆️

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

☔ View full report in Codecov by Harness.
📢 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.

1. Per-structure USD export

ENH: Name USD prims per structure and pick materials from those names

save_combined_surfaces now tags each cell with a SegmentationLabelIds
array so structure identity survives the merge, and ConvertVTKToUSD
splits on that array in addition to boundary_labels.

ConvertVTKToUSD gains object_names for the static-merge layout;
WorkflowConvertVTKToUSD derives them from each mesh's
SegmentationLabelNames. anatomy_type now defaults to None, which
resolves a material per prim from the prim name, falling back to the
object's AnatomyGroup and then to "other" -- so ventricle_left,
myocardium, and the great vessels each get their own look instead of
one shared heart material.

Adds USDAnatomyTools.resolve_anatomy_type so callers can test a name
before applying it rather than catching ValueError.

2. Composite transform flattening (bug)

BUG: Splice nested composites when chaining registration transforms

itk.HDF5TransformIO refuses to write a CompositeTransform holding
another CompositeTransform, which every multi-stage registration
produced: RegisterImagesGreedy returns an affine+warp composite, and
composing a residual onto it nested that composite.

_add_transform_flattened splices sub-transforms in at the position
their composite occupied, leaving the mapping unchanged since
CompositeTransform applies its queue back to front either way.

3. Retire experiment test harness

ENH: Drop the experiment test harness; tutorials are the e2e suite

Removes tests/test_experiments.py, the "experiment" marker, and
--run-experiments. Experiment scripts are exploratory: they assume
interactive display, full-resolution parameters, and data layouts that
only exist on the author's machine. The test-mode branches in those
scripts go away with the harness that drove them, and experiments/ is
omitted from coverage.

tests/test_tutorials.py --run-tutorials is now the only end-to-end
suite; CI comments and READMEs point there.

4. PCA parameter rename (breaking)

ENH: Rename PCA count parameters to number_of_pca_components

pca_number_of_components, pca_number_of_modes, and --pca-components /
--pca-number-of-modes all become number_of_pca_components, so the
workflows, the CLIs, and the docs use one name. Breaking change to
both the Python and the CLI interface.

5. Distance-map finetuning tutorial

ENH: Add tutorial 2 finetuning uniGradICON on lung distance maps

RegisterModelsDistanceMaps feeds ICON rasterized signed squared
distance maps, not CT intensities, so stock uniGradICON is out of
distribution for that stage. The new tutorial finetunes on exactly
that representation using DIR-Lab 4D CT lung segmentations, holding
Case 1 out for landmark TRE and Dice evaluation.

RegisterModelsDistanceMaps.set_icon_weights_path and
WorkflowFitStatisticalModelToPatient
.set_labelmap_to_labelmap_icon_weights_path plumb the resulting
checkpoint into the labelmap-to-labelmap stage; the labelmap-to-image
stage keeps stock weights since it registers the image itself.
Copilot AI review requested due to automatic review settings August 7, 2026 16:53

Copilot AI left a comment

Copy link
Copy Markdown

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 89 out of 89 changed files in this pull request and generated no new comments.

Suppressed comments (4)

tutorials/tutorial_06_lung_create_statistical_model.py:171

  • mode_count is currently set to number_of_pca_components without checking how many PCA components/eigenvalues were actually produced. If the workflow reduced components due to limited sample count, the loop will raise IndexError when indexing eigenvalues[mode_idx] / components[mode_idx]. Cap the loop by the available lengths.
    src/physiotwin4d/register_models_distance_maps.py:233
  • These debug itk.imwrite(...) calls will crash when mask_dilation_mm <= 0 because self.fixed_mask_image is None, and they also write large files to the current working directory unconditionally. Gate them behind debug logging and only write the mask if it exists.
    src/physiotwin4d/register_models_distance_maps.py:274
  • Same issue as the fixed-mask debug write above: self.moving_mask_image can be None (when mask_dilation_mm <= 0), but it is still passed to itk.imwrite, which will error. Also consider keeping these writes debug-only to avoid unexpected I/O during normal library use.
    tests/test_workflow_convert_vtk_to_usd.py:20
  • Type hints in this repo use Optional[T] rather than T | None. Using str | None here violates the project typing convention (and strict mypy config), and can be fixed by switching to Optional[str] and importing Optional.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 8

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/physiotwin4d/register_models_distance_maps.py (1)

126-139: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Document distance_squared_max in the __init__ Args.

The new constructor parameter appears in the class Attributes list at line 89 but not in the __init__ Args block. State its meaning and the unit, since callers derive it from the dilation radius, for example (1.25 * mask_dilation_mm) ** 2 in workflow_fit_statistical_model_to_patient.py.

📝 Proposed docstring addition
             reference_image: ITK image providing coordinate frame (origin, spacing, direction)
                 for mask generation. Typically the patient CT/MRI image.
+            distance_squared_max: Squared distance in mm^2 used to normalize the
+                signed distance maps to [-1, 1]. Default: 50.0
             mask_dilation_mm: Dilation amount in millimeters for binary registration
-                mask generation. Default: 20mm
+                mask generation. Pass 0 or a negative value to skip mask
+                generation entirely. Default: 20mm
🤖 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/physiotwin4d/register_models_distance_maps.py` around lines 126 - 139,
Update the constructor docstring’s Args section for __init__ to document
distance_squared_max, describing it as the maximum squared distance threshold
and specifying that its unit is squared millimeters.

Source: Coding guidelines

src/physiotwin4d/register_time_series_images.py (1)

26-45: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Remove the stale prior-propagation claims.

Prior-transform initialization is gone. Three statements still describe it:

  • Line 28-29: "It can propagate information from prior registrations to initialize subsequent ones."
  • Line 36-37: "This bidirectional approach helps maintain temporal coherence in the registration results."
  • Lines 171-173: "the method can optionally use the transform from the previous image to initialize the registration, which can improve convergence and temporal coherence."

The Note at lines 208-209 now states the opposite. Align the class docstring and the method docstring with the independent-per-frame behavior.

📝 Proposed docstring fix
     This class extends RegisterImagesBase to provide sequential registration
     of multiple images (time series) to a fixed image, using a
-    caller-supplied registration backend. It can propagate information from
-    prior registrations to initialize subsequent ones.
+    caller-supplied registration backend. Every frame is registered
+    independently of the others.
 
     The registration proceeds in two passes from a reference frame:
 
     1. Forward pass: from reference_frame to the end of the series
     2. Backward pass: from reference_frame-1 to the beginning
-
-    This bidirectional approach helps maintain temporal coherence in the
-    registration results.
         This method registers an ordered sequence of images to a common fixed
         frame. Registration proceeds bidirectionally from a reference frame:
         forward to the end and backward to the beginning.
 
-        For each image after the reference image, the method can optionally use
-        the transform from the previous image to initialize the registration,
-        which can improve convergence and temporal coherence.
+        Each image is registered independently of the others.

Also applies to: 171-173

🤖 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/physiotwin4d/register_time_series_images.py` around lines 26 - 45, Update
the class docstring and the affected method docstring in the time-series
registration implementation to remove claims about propagating or reusing prior
transforms, initialization benefits, and temporal-coherence guarantees. Keep the
documentation aligned with the current independent-per-frame behavior and the
existing note that states prior-transform initialization is not used.

Source: Coding guidelines

src/physiotwin4d/register_images_ants.py (1)

537-555: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Remove the stale initial-transform documentation.

registration_method no longer accepts or composes an initial transform. The Note block still states that this method pre-warps the moving image with the initial transform and composes it with the registration result. Line 555 also claims that initial transforms are converted from ITK to ANTs format automatically, but the call now passes initial_transform=["identity"]. Point the reader to RegisterImagesBase.register_from instead.

📝 Proposed docstring fix
         Note:
             For SyN registration, the transformations are approximately inverse
             consistent. The forward and inverse transforms are stored separately
             by ANTs.
 
-            IMPORTANT: the initial transform is applied by pre-warping the
-            moving image onto the fixed grid (the same approach as
-            RegisterImagesICON) rather than via ants.registration's
-            initial_transform argument, which mishandles matrix (affine/
-            translation) initials. This method composes the initial transform
-            with the registration result, so the returned transforms include
-            both the initial alignment and the registration refinement.
+            To seed the registration from a known alignment, call
+            :meth:`RegisterImagesBase.register_from`. This method always runs
+            ANTs from identity.
 
         Implementation details:
             - Uses ANTs registration with configurable transform types
             - Supports multi-resolution optimization
             - Handles masked and unmasked registration
             - Returns ITK-compatible displacement field transforms
-            - Initial transforms are converted from ITK to ANTs format automatically
🤖 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/physiotwin4d/register_images_ants.py` around lines 537 - 555, Remove the
stale initial-transform claims from the docstring for registration_method,
including the Note text about pre-warping/composition and the
implementation-detail bullet about ITK-to-ANTs conversion. Replace them with a
brief reference directing readers to RegisterImagesBase.register_from for
initial-transform handling, while preserving the remaining registration behavior
documentation.

Source: Coding guidelines

🟡 Minor comments (13)
.agents/agents/testing.md-32-32 (1)

32-32: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Wrap the modified Markdown lines to 88 characters or fewer.

  • .agents/agents/testing.md#L32-L32: move the long inline CI explanation to a separate wrapped comment line.
  • CLAUDE.md#L134-L134: wrap the fixture-chain description onto a continuation line.
  • statistics.md#L155-L155: wrap the tutorial marker description onto a continuation line.
🤖 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 @.agents/agents/testing.md at line 32, Wrap the modified Markdown lines to 88
characters or fewer: in .agents/agents/testing.md lines 32-32, move the inline
CI explanation to a separate wrapped comment line; in CLAUDE.md lines 134-134,
place the fixture-chain description on a continuation line; and in statistics.md
lines 155-155, place the tutorial marker description on a continuation line.

Source: Coding guidelines

tests/conftest.py-615-620 (1)

615-620: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Remove the coordinate-frame reference from this docstring.

Describe the expected displacement and sign-check behavior without naming the
RAS/LPS convention. The repository rule prohibits restating fixed
coordinate-frame conventions in docstrings.

As per coding guidelines, "Do not restate fixed ITK shape, axis-order, or LPS
conventions in docstrings, comments, or test docstrings."

🤖 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/conftest.py` around lines 615 - 620, Update the docstring near the
moving/fixed resampling explanation to describe the expected displacement and
sign-check behavior without mentioning RAS/LPS or any fixed coordinate-frame
convention. Preserve the explanation that the transform uses the negated shift
and validates absolute accuracy.

Source: Coding guidelines

tests/README.md-143-143 (1)

143-143: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Wrap this line to 88 characters or fewer.

Line 143 exceeds the repository line-length limit for Markdown files.

As per coding guidelines, "keep lines at or below 88 characters."

🤖 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/README.md` at line 143, Wrap the timing-report sentence in
tests/README.md so each Markdown line is no longer than 88 characters,
preserving the existing wording and meaning.

Source: Coding guidelines

experiments/Heart-Simpleware_Segmentation/simpleware_heart_segmentation.py-61-67 (1)

61-67: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Exit early when the file dialog is canceled.

If filedialog.askopenfilename() returns an empty path, input_image_path is not selected. Add a guard before the later itk.imread(input_image_path) call so the script exits with a clear message.

🤖 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-Simpleware_Segmentation/simpleware_heart_segmentation.py`
around lines 61 - 67, After the file-selection flow around
filedialog.askopenfilename, validate that input_image_path is non-empty before
the later itk.imread call; print a clear cancellation message and exit early
when no file is selected, while preserving normal processing for valid paths.
tutorials/tutorial_02_lung_distancemap_finetune_icon.py-1-2 (1)

1-2: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Document the new distance-map tutorial.

docs/tutorials.rst lists only tutorial_02_lung_finetune_icon.py for Tutorial 2 and still states that the repository has 15 runnable scripts. Add this tutorial to the index, or explicitly identify it as an advanced variant. Otherwise users cannot discover it.

🤖 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_02_lung_distancemap_finetune_icon.py` around lines 1 - 2,
Update the Tutorial 2 documentation in docs/tutorials.rst to include
tutorials/tutorial_02_lung_distancemap_finetune_icon.py, clearly labeling it as
the distance-map or advanced variant if appropriate, and revise the
runnable-script count to match the added tutorial.
tests/test_contour_tools.py-360-386 (1)

360-386: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add a baseline comparison for the merged .vtp output.

The tests write surfaces but only assert array membership and cell counts. Add deterministic baselines under tests/baselines/ for combined.vtp and combined_group.vtp, then compare with TestTools for the full merged output. If exact comparison is not available for .vtp, compare the relevant arrays across multiple deterministic inputs to keep the regression stronger.

🤖 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_contour_tools.py` around lines 360 - 386, Add deterministic
baseline fixtures under tests/baselines for combined.vtp and combined_group.vtp,
and update the corresponding tests around save_combined_surfaces to compare the
full merged output using TestTools. If TestTools lacks exact .vtp comparison
support, compare the relevant output arrays across multiple deterministic
surface inputs instead of only checking label membership and cell counts.

Source: Coding guidelines

tutorials/README.md-31-31 (1)

31-31: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Keep the added Markdown entry within the line limit.

Line 31 exceeds 88 characters. Shorten the table content or move the longer description below the table.

As per coding guidelines, “Use double quotes for strings and docstrings, and keep lines at or below 88 characters.”

🤖 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/README.md` at line 31, Shorten the Markdown table entry for
tutorial_02_lung_distancemap_finetune_icon.py so the entire line is no longer
than 88 characters, while preserving the tutorial link and essential
description.

Source: Coding guidelines

src/physiotwin4d/convert_vtk_to_usd.py-957-960 (1)

957-960: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use the workflow logging helper.

Replace self.logger.warning(...) with self.log_warning(...). This keeps logging behavior in PhysioTwin4DBase.

As per coding guidelines, “All classes must inherit from PhysioTwin4DBase; use self.log_info() or self.log_debug() for logging and never use print().”

🤖 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/physiotwin4d/convert_vtk_to_usd.py` around lines 957 - 960, Update the
warning call in the segmentation-label handling flow to use the inherited
PhysioTwin4DBase helper self.log_warning(...) instead of
self.logger.warning(...), preserving the existing message and behavior.

Source: Coding guidelines

tests/test_workflow_convert_vtk_to_usd.py-172-173 (1)

172-173: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Remove the direct test-file runner.

Run this test through the project test command. The direct pytest.main(...) path bypasses the documented suite invocation.

As per coding guidelines, “Use py -m pytest tests/ -v for fast tests.”

🤖 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_workflow_convert_vtk_to_usd.py` around lines 172 - 173, Remove the
__main__ block that directly invokes pytest.main in the test file, so the test
is run only through the documented project command, py -m pytest tests/ -v.

Source: Coding guidelines

tests/test_workflow_convert_vtk_to_usd.py-16-25 (1)

16-25: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use Optional[str] for the nullable parameter.

The repository guideline requires nullable Python annotations to use Optional[X] instead of str | None.

🤖 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_workflow_convert_vtk_to_usd.py` around lines 16 - 25, Update the
nullable group parameter in _labeled_sphere to use Optional[str] instead of str
| None, and add or reuse the required Optional import while preserving the
existing default value and behavior.

Source: Coding guidelines

src/physiotwin4d/register_models_pca.py-466-490 (1)

466-490: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Probe the transform inside the model's own extent.

_affine_of_transform probes the origin, the three unit basis vectors, and one point near the unit cube. A DisplacementFieldTransform returns the identity outside its field support. If the model sits far from the origin, all five probes fall outside that support, the linearity check passes, and the transform is accepted as the identity affine. _prepare_sampling then folds an identity matrix into the modes and _apply_post_pca_transform returns the points unchanged, so the post-PCA deformation is silently dropped instead of triggering the documented finite-difference fallback.

Probe points drawn from pca_template_model.bounds instead of the unit cube keeps the check inside the region the transform is actually applied to.

🤖 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/physiotwin4d/register_models_pca.py` around lines 466 - 490, Update
_affine_of_transform to derive its probe points from pca_template_model.bounds
rather than the origin, unit basis vectors, and unit-cube probe. Ensure the
offset, matrix, and nonlinearity check use points within the model’s extent,
while preserving affine-transform detection and returning None for non-affine
transforms so _prepare_sampling uses the documented finite-difference fallback.
tests/test_register_models_pca.py-295-329 (1)

295-329: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Assert that the forward field actually displaces the points.

registered_model_pca_deformation is std[0] * mode, and mode is normalized over all points, so each per-point displacement is far below 1 mm. An identity forward transform therefore also satisfies field_rms < 1.5 and round_trip_rms < 1.0. The test passes whether or not compute_pca_transforms produced a usable field.

Add a lower-bound check on the displacement, or scale the deformation so the tolerances discriminate.

💚 Proposed strengthening
     assert field_rms < 1.5
     assert round_trip_rms < 1.0
+    # Guard against a no-op field: the forward transform must move the points.
+    assert np.linalg.norm(mapped - template_points, axis=1).max() > 0.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 `@tests/test_register_models_pca.py` around lines 295 - 329, Strengthen
test_pca_transforms_round_trip so it verifies that the forward transform
produces a meaningful displacement, not just accurate round-tripping. Add a
lower-bound assertion for the measured displacement, or scale the
registered_model_pca_deformation before computing expected, while retaining the
existing upper-bound and round-trip checks.
src/physiotwin4d/register_models_pca.py-110-144 (1)

110-144: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Validate symmetric_weight against its documented range.

The docstring states symmetric_weight is a "Weight in [0, 1]", but the constructor does not check it. _objective_and_gradient computes (1.0 - weight) * forward_distance + weight * reverse_distance. A value above 1.0 makes the model-to-target term negative, so the optimizer is rewarded for moving the model away from the target. The constructor already validates the eigenvector shapes and mode counts, so add the same guard here.

🛡️ Proposed validation
         if self.pca_eigenvectors.shape[0] != self.pca_std_deviations.shape[0]:
             raise ValueError(
                 f"Mode count mismatch: {self.pca_eigenvectors.shape[0]} eigenvectors "
                 f"but {self.pca_std_deviations.shape[0]} standard deviations"
             )
+        if not 0.0 <= symmetric_weight <= 1.0:
+            raise ValueError(
+                f"symmetric_weight must be in [0, 1], got {symmetric_weight}"
+            )
🤖 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/physiotwin4d/register_models_pca.py` around lines 110 - 144, Validate
symmetric_weight in the constructor before storing or using it, requiring it to
be within the documented inclusive range [0, 1]. Raise the constructor’s
established validation error type for out-of-range values, consistent with the
existing eigenvector and mode-count checks, while preserving valid behavior in
_objective_and_gradient.
🧹 Nitpick comments (8)
tests/test_contour_tools.py (1)

344-349: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use the concrete mesh return type.

_annotated_sphere() returns a pv.PolyData. Replace Any with pv.PolyData so strict mypy preserves the helper contract. Verify this against the installed PyVista stubs.

As per coding guidelines, “Use full type hints compatible with strict mypy.”

🤖 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_contour_tools.py` around lines 344 - 349, Update the
_annotated_sphere helper’s return annotation from Any to the concrete
pv.PolyData type, matching the object returned by pv.Sphere and the installed
PyVista stubs while preserving its existing behavior.

Source: Coding guidelines

tests/test_workflow_convert_vtk_to_usd.py (1)

36-37: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove the pytest test class or document an exemption.

TestAnatomyAppearance does not inherit from PhysioTwin4DBase. Do not add that base class directly, because its initializer can stop pytest from collecting the class. Convert these methods to module-level tests, or add an explicit test-class exemption to the guideline.

As per coding guidelines, “All classes must inherit from PhysioTwin4DBase.”

🤖 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_workflow_convert_vtk_to_usd.py` around lines 36 - 37, Remove or
refactor the TestAnatomyAppearance class so the tests become module-level
functions, avoiding a non-PhysioTwin4DBase test class; alternatively, add the
project’s explicit exemption for this class without making it inherit from
PhysioTwin4DBase.

Source: Coding guidelines

src/physiotwin4d/workflow_fit_statistical_model_to_patient.py (2)

302-315: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Validate the checkpoint path when it is set.

set_labelmap_to_labelmap_icon_weights_path stores the path without checking it. The path is first used in register_labelmap_to_labelmap, which is stage 3. RegisterModelsDistanceMaps.set_icon_weights_path raises FileNotFoundError there. A typo in the path therefore fails only after ICP and PCA registration have completed. Check the path here so the error surfaces before any work starts.

♻️ Proposed fail-fast check
         Args:
             weights_path: Path to an existing uniGradICON checkpoint.
+
+        Raises:
+            FileNotFoundError: If weights_path does not exist.
         """
+        if not Path(weights_path).exists():
+            raise FileNotFoundError(f"ICON weights not found: {weights_path}")
         self.l2l_icon_weights_path = weights_path
🤖 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/physiotwin4d/workflow_fit_statistical_model_to_patient.py` around lines
302 - 315, Update set_labelmap_to_labelmap_icon_weights_path to validate that
weights_path exists before assigning it to l2l_icon_weights_path, raising
FileNotFoundError consistently with
RegisterModelsDistanceMaps.set_icon_weights_path. Preserve the existing
assignment for valid checkpoint paths so invalid paths fail when configured,
before registration work begins.

695-710: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Derive the padding margin from spacing rather than hard-coding 50 voxels.

pad_voxels=[50, 50, 50] fixes the margin in voxels, so the physical margin scales with the image resolution: 50 mm at 1 mm spacing, 25 mm at 0.5 mm spacing. The margin needs to cover the template surface that falls outside the patient image, which is a physical distance, not a voxel count.

The cost is also material. Padding a 256-voxel axis by 50 per side grows it to 356, so the distance map and the ICON deformable stage run on roughly 2.7 times the voxels. ImageTools.pad_image accepts pad_portion for exactly this case, and mask_dilation_mm already expresses the relevant physical scale.

Consider deriving the margin from self.mask_dilation_mm and the image spacing, or exposing it through a setter so callers can tune it.

🤖 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/physiotwin4d/workflow_fit_statistical_model_to_patient.py` around lines
695 - 710, Update the padding setup in the workflow around
ImageTools().pad_image to derive the margin from the patient image spacing and
the physical scale represented by self.mask_dilation_mm, rather than using the
fixed pad_voxels=[50, 50, 50]. Prefer the existing pad_portion interface when
appropriate, or expose a tunable padding configuration while ensuring the
resulting padding covers the intended physical margin on each axis.
src/physiotwin4d/register_models_pca.py (1)

999-1034: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Subsample or gate the fidelity check.

_log_transform_fidelity runs a Python loop over every template point and calls TransformPoint twice per point. compute_pca_transforms calls it unconditionally. For a large template this adds hundreds of thousands of interpreter-level ITK calls purely to produce two log lines. Subsample the points, or run the check only when the log level is DEBUG.

♻️ Proposed change
-        self._log_transform_fidelity(template_points)
+        # Cap the fidelity check: the RMS of a few thousand points is
+        # representative and keeps this off the hot path for large templates.
+        step = max(1, len(template_points) // 2000)
+        self._log_transform_fidelity(template_points[::step])
🤖 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/physiotwin4d/register_models_pca.py` around lines 999 - 1034, Reduce the
cost of _log_transform_fidelity, which currently performs two ITK TransformPoint
calls for every template point and is invoked unconditionally by
compute_pca_transforms. Either gate the fidelity calculation behind DEBUG-level
logging or subsample template_points before the loop, while preserving the
existing RMS log outputs for the points that are checked.
src/physiotwin4d/image_tools.py (1)

306-413: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add tests for ImageTools.pad_image.

pad_image exposes three invalid-input ValueError cases and an origin-preservation guarantee, but tests/test_image_tools.py has no coverage for pad_image, _per_axis_values, or the origin re-anchoring behavior.

🤖 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/physiotwin4d/image_tools.py` around lines 306 - 413, Add focused tests in
the image-tools test suite for pad_image and _per_axis_values: cover mutually
exclusive or missing padding arguments, negative values, and sequences with
incorrect dimensionality raising ValueError. Also verify padding preserves the
input voxels’ physical positions by checking the returned image origin and
expected padded dimensions for representative voxel and portion inputs.

Source: Coding guidelines

src/physiotwin4d/register_images_greedy.py (1)

366-375: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Annotate kwargs_aff explicitly for strict mypy.

mypy infers the value type of kwargs_aff from the initial literal, which holds SimpleITK images. Line 375 then assigns None to kwargs_aff["aff_init"]. Strict mypy rejects that assignment. _registration_method_affine_or_rigid already declares kwargs: dict[str, Any] at line 320; use the same annotation here.

♻️ Proposed annotation
-        kwargs_aff = {
+        kwargs_aff: dict[str, Any] = {
             "fixed": fixed_sitk,
             "moving": moving_sitk,
         }
🤖 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/physiotwin4d/register_images_greedy.py` around lines 366 - 375, Annotate
the kwargs_aff dictionary declaration in the affine registration setup with
dict[str, Any], matching the existing _registration_method_affine_or_rigid
kwargs annotation, so the later aff_init assignment of None passes strict mypy.

Source: Coding guidelines

src/physiotwin4d/register_time_series_images.py (1)

306-328: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider collapsing the two-pass loop.

No state carries between frames now, so the forward pass and the backward pass produce the same result as a single pass over every index other than reference_frame. The two-pass structure only adds complexity. Keep it if you plan to restore prior propagation; otherwise a single loop is clearer.

♻️ Proposed simplification
-        # Register forward and backward from reference frame
-        for step, start_idx, end_idx in [
-            (1, reference_frame + 1, num_images),  # Forward pass
-            (-1, reference_frame - 1, -1),  # Backward pass
-        ]:
-            for img_idx in range(start_idx, end_idx, step):
-                moving_image = moving_images[img_idx]
-                moving_mask = (
-                    moving_masks[img_idx] if moving_masks is not None else None
-                )
-                moving_labelmap = (
-                    moving_labelmaps[img_idx] if moving_labelmaps is not None else None
-                )
-
-                result = self.registrar.register(
-                    moving_image=moving_image,
-                    moving_mask=moving_mask,
-                    moving_labelmap=moving_labelmap,
-                )
-
-                forward_transforms[img_idx] = result["forward_transform"]
-                inverse_transforms[img_idx] = result["inverse_transform"]
-                losses[img_idx] = cast(float, result["loss"])
+        # Every frame is registered independently, so the order does not matter.
+        for img_idx in range(num_images):
+            if img_idx == reference_frame:
+                continue
+            result = self.registrar.register(
+                moving_image=moving_images[img_idx],
+                moving_mask=(
+                    moving_masks[img_idx] if moving_masks is not None else None
+                ),
+                moving_labelmap=(
+                    moving_labelmaps[img_idx] if moving_labelmaps is not None else None
+                ),
+            )
+
+            forward_transforms[img_idx] = result["forward_transform"]
+            inverse_transforms[img_idx] = result["inverse_transform"]
+            losses[img_idx] = cast(float, result["loss"])
🤖 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/physiotwin4d/register_time_series_images.py` around lines 306 - 328,
Replace the separate forward and backward iteration in the registration flow
with one loop over every image index except reference_frame, while preserving
the existing moving-image, mask, labelmap, registration, and result-assignment
logic in the loop.
🤖 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/physiotwin4d/contour_tools.py`:
- Around line 355-365: Update the face-sampling flow around the samples
collection and create_distance_map() so generated triangle samples are processed
and rasterized in bounded batches rather than appended to samples for the entire
mesh. Ensure each batch is released after rasterization, and preserve the
existing barycentric sampling behavior and final distance-map results without
retaining all face coordinates simultaneously.

In `@src/physiotwin4d/convert_vtk_to_usd.py`:
- Around line 168-177: Update the initializer validation for object_names to
enforce valid USD prim-name path components and reject duplicate names before
assigning self.object_names. Preserve the existing length validation, and make
the resulting ValueError clearly mention that names must be unique and comply
with USD prim-name rules.

In `@src/physiotwin4d/register_images_base.py`:
- Around line 396-411: Update register_from around the self.register(...) call
to restore self.moving_image to the original moving_image after seeded
registration, matching the composed transform’s input. Also reset
self.moving_image_registered so get_registered_image() cannot reuse the cache
produced for the pre-warped image.

In `@src/physiotwin4d/register_models_distance_maps.py`:
- Around line 230-237: Remove the unconditional fixed-image debug writes around
register_models_distance_maps.py lines 230-237, or guard them with
self.log_level == logging.DEBUG and self.fixed_mask_image is not None. Apply the
same removal or guard to the moving-image writes at lines 271-278, using
self.moving_mask_image, so disabled mask dilation cannot pass None to
itk.imwrite or write files by default.

In `@src/physiotwin4d/workflow_convert_vtk_to_usd.py`:
- Around line 213-234: Update the object_groups construction in the static-merge
naming flow so anatomy-group annotations remain mapped when object_names is
None. Use the same effective positional names generated from annotations (the
project-name/index fallback) as the map keys, while preserving custom
object_names when available, allowing positional prim names to resolve their
anatomy groups instead of defaulting to “other”.
- Around line 139-150: Update _read_object_annotations() to convert supported
raw vtk.vtkDataSet objects with pv.wrap() before checking field_data
annotations. Preserve the existing non-dataset fallback, then read
SegmentationLabelNames and AnatomyGroup from the wrapped dataset so automatic
names and anatomy groups are retained.

In `@src/physiotwin4d/workflow_fit_statistical_model_to_patient.py`:
- Around line 611-625: Update compute_pca_transforms() and its
create_deformation_field() input so the deformation-field grid covers the
un-aligned pca_template_model.points as well as the patient image, or use an
axis-aligned template-frame grid before ICP composition. Preserve PCA
deformation for use_surface and template-labelmap resampling even when the
un-aligned template lies outside the patient-image bounds.

In `@tutorials/tutorial_06_lung_create_statistical_model.py`:
- Line 170: Update mode_count before the mode loop to use the minimum of
number_of_pca_components, len(components), and len(eigenvalues), ensuring the
loop only indexes available PCA components and eigenvalues.

---

Outside diff comments:
In `@src/physiotwin4d/register_images_ants.py`:
- Around line 537-555: Remove the stale initial-transform claims from the
docstring for registration_method, including the Note text about
pre-warping/composition and the implementation-detail bullet about ITK-to-ANTs
conversion. Replace them with a brief reference directing readers to
RegisterImagesBase.register_from for initial-transform handling, while
preserving the remaining registration behavior documentation.

In `@src/physiotwin4d/register_models_distance_maps.py`:
- Around line 126-139: Update the constructor docstring’s Args section for
__init__ to document distance_squared_max, describing it as the maximum squared
distance threshold and specifying that its unit is squared millimeters.

In `@src/physiotwin4d/register_time_series_images.py`:
- Around line 26-45: Update the class docstring and the affected method
docstring in the time-series registration implementation to remove claims about
propagating or reusing prior transforms, initialization benefits, and
temporal-coherence guarantees. Keep the documentation aligned with the current
independent-per-frame behavior and the existing note that states prior-transform
initialization is not used.

---

Minor comments:
In @.agents/agents/testing.md:
- Line 32: Wrap the modified Markdown lines to 88 characters or fewer: in
.agents/agents/testing.md lines 32-32, move the inline CI explanation to a
separate wrapped comment line; in CLAUDE.md lines 134-134, place the
fixture-chain description on a continuation line; and in statistics.md lines
155-155, place the tutorial marker description on a continuation line.

In `@experiments/Heart-Simpleware_Segmentation/simpleware_heart_segmentation.py`:
- Around line 61-67: After the file-selection flow around
filedialog.askopenfilename, validate that input_image_path is non-empty before
the later itk.imread call; print a clear cancellation message and exit early
when no file is selected, while preserving normal processing for valid paths.

In `@src/physiotwin4d/convert_vtk_to_usd.py`:
- Around line 957-960: Update the warning call in the segmentation-label
handling flow to use the inherited PhysioTwin4DBase helper self.log_warning(...)
instead of self.logger.warning(...), preserving the existing message and
behavior.

In `@src/physiotwin4d/register_models_pca.py`:
- Around line 466-490: Update _affine_of_transform to derive its probe points
from pca_template_model.bounds rather than the origin, unit basis vectors, and
unit-cube probe. Ensure the offset, matrix, and nonlinearity check use points
within the model’s extent, while preserving affine-transform detection and
returning None for non-affine transforms so _prepare_sampling uses the
documented finite-difference fallback.
- Around line 110-144: Validate symmetric_weight in the constructor before
storing or using it, requiring it to be within the documented inclusive range
[0, 1]. Raise the constructor’s established validation error type for
out-of-range values, consistent with the existing eigenvector and mode-count
checks, while preserving valid behavior in _objective_and_gradient.

In `@tests/conftest.py`:
- Around line 615-620: Update the docstring near the moving/fixed resampling
explanation to describe the expected displacement and sign-check behavior
without mentioning RAS/LPS or any fixed coordinate-frame convention. Preserve
the explanation that the transform uses the negated shift and validates absolute
accuracy.

In `@tests/README.md`:
- Line 143: Wrap the timing-report sentence in tests/README.md so each Markdown
line is no longer than 88 characters, preserving the existing wording and
meaning.

In `@tests/test_contour_tools.py`:
- Around line 360-386: Add deterministic baseline fixtures under tests/baselines
for combined.vtp and combined_group.vtp, and update the corresponding tests
around save_combined_surfaces to compare the full merged output using TestTools.
If TestTools lacks exact .vtp comparison support, compare the relevant output
arrays across multiple deterministic surface inputs instead of only checking
label membership and cell counts.

In `@tests/test_register_models_pca.py`:
- Around line 295-329: Strengthen test_pca_transforms_round_trip so it verifies
that the forward transform produces a meaningful displacement, not just accurate
round-tripping. Add a lower-bound assertion for the measured displacement, or
scale the registered_model_pca_deformation before computing expected, while
retaining the existing upper-bound and round-trip checks.

In `@tests/test_workflow_convert_vtk_to_usd.py`:
- Around line 172-173: Remove the __main__ block that directly invokes
pytest.main in the test file, so the test is run only through the documented
project command, py -m pytest tests/ -v.
- Around line 16-25: Update the nullable group parameter in _labeled_sphere to
use Optional[str] instead of str | None, and add or reuse the required Optional
import while preserving the existing default value and behavior.

In `@tutorials/README.md`:
- Line 31: Shorten the Markdown table entry for
tutorial_02_lung_distancemap_finetune_icon.py so the entire line is no longer
than 88 characters, while preserving the tutorial link and essential
description.

In `@tutorials/tutorial_02_lung_distancemap_finetune_icon.py`:
- Around line 1-2: Update the Tutorial 2 documentation in docs/tutorials.rst to
include tutorials/tutorial_02_lung_distancemap_finetune_icon.py, clearly
labeling it as the distance-map or advanced variant if appropriate, and revise
the runnable-script count to match the added tutorial.

---

Nitpick comments:
In `@src/physiotwin4d/image_tools.py`:
- Around line 306-413: Add focused tests in the image-tools test suite for
pad_image and _per_axis_values: cover mutually exclusive or missing padding
arguments, negative values, and sequences with incorrect dimensionality raising
ValueError. Also verify padding preserves the input voxels’ physical positions
by checking the returned image origin and expected padded dimensions for
representative voxel and portion inputs.

In `@src/physiotwin4d/register_images_greedy.py`:
- Around line 366-375: Annotate the kwargs_aff dictionary declaration in the
affine registration setup with dict[str, Any], matching the existing
_registration_method_affine_or_rigid kwargs annotation, so the later aff_init
assignment of None passes strict mypy.

In `@src/physiotwin4d/register_models_pca.py`:
- Around line 999-1034: Reduce the cost of _log_transform_fidelity, which
currently performs two ITK TransformPoint calls for every template point and is
invoked unconditionally by compute_pca_transforms. Either gate the fidelity
calculation behind DEBUG-level logging or subsample template_points before the
loop, while preserving the existing RMS log outputs for the points that are
checked.

In `@src/physiotwin4d/register_time_series_images.py`:
- Around line 306-328: Replace the separate forward and backward iteration in
the registration flow with one loop over every image index except
reference_frame, while preserving the existing moving-image, mask, labelmap,
registration, and result-assignment logic in the loop.

In `@src/physiotwin4d/workflow_fit_statistical_model_to_patient.py`:
- Around line 302-315: Update set_labelmap_to_labelmap_icon_weights_path to
validate that weights_path exists before assigning it to l2l_icon_weights_path,
raising FileNotFoundError consistently with
RegisterModelsDistanceMaps.set_icon_weights_path. Preserve the existing
assignment for valid checkpoint paths so invalid paths fail when configured,
before registration work begins.
- Around line 695-710: Update the padding setup in the workflow around
ImageTools().pad_image to derive the margin from the patient image spacing and
the physical scale represented by self.mask_dilation_mm, rather than using the
fixed pad_voxels=[50, 50, 50]. Prefer the existing pad_portion interface when
appropriate, or expose a tunable padding configuration while ensuring the
resulting padding covers the intended physical margin on each axis.

In `@tests/test_contour_tools.py`:
- Around line 344-349: Update the _annotated_sphere helper’s return annotation
from Any to the concrete pv.PolyData type, matching the object returned by
pv.Sphere and the installed PyVista stubs while preserving its existing
behavior.

In `@tests/test_workflow_convert_vtk_to_usd.py`:
- Around line 36-37: Remove or refactor the TestAnatomyAppearance class so the
tests become module-level functions, avoiding a non-PhysioTwin4DBase test class;
alternatively, add the project’s explicit exemption for this class without
making it inherit from PhysioTwin4DBase.
🪄 Autofix

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 Plus

Run ID: 11412ee5-e4d4-4f99-84d2-a32ae9bacc59

📥 Commits

Reviewing files that changed from the base of the PR and between 55dc2a8 and b6d90fc.

📒 Files selected for processing (89)
  • .agents/agents/testing.md
  • .github/workflows/README.md
  • .github/workflows/ci.yml
  • .github/workflows/nightly-health.yml
  • AGENTS.md
  • CLAUDE.md
  • docs/cli_scripts/create_statistical_model.rst
  • docs/cli_scripts/fit_statistical_model_to_patient.rst
  • docs/cli_scripts/vtk_to_usd.rst
  • docs/contributing.rst
  • docs/developer/core.rst
  • docs/developer/registration_images.rst
  • docs/testing.rst
  • docs/tutorials.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/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/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-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/README.md
  • experiments/Reconstruct4DCT/reconstruct_4d_ct.py
  • experiments/Reconstruct4DCT/reconstruct_4d_ct_class.py
  • pyproject.toml
  • src/physiotwin4d/cli/convert_vtk_to_usd.py
  • src/physiotwin4d/cli/create_statistical_model.py
  • src/physiotwin4d/cli/fit_statistical_model_to_patient.py
  • src/physiotwin4d/cli/reconstruct_highres_4d_ct.py
  • src/physiotwin4d/contour_tools.py
  • src/physiotwin4d/convert_vtk_to_usd.py
  • src/physiotwin4d/image_tools.py
  • src/physiotwin4d/physicsnemo_tools.py
  • src/physiotwin4d/register_images_ants.py
  • src/physiotwin4d/register_images_base.py
  • src/physiotwin4d/register_images_chain.py
  • src/physiotwin4d/register_images_greedy.py
  • src/physiotwin4d/register_images_icon.py
  • src/physiotwin4d/register_models_distance_maps.py
  • src/physiotwin4d/register_models_icp.py
  • src/physiotwin4d/register_models_pca.py
  • src/physiotwin4d/register_time_series_images.py
  • src/physiotwin4d/train_physicsnemo_mgn.py
  • src/physiotwin4d/transform_tools.py
  • src/physiotwin4d/usd_anatomy_tools.py
  • src/physiotwin4d/workflow_convert_vtk_to_usd.py
  • src/physiotwin4d/workflow_create_statistical_model.py
  • src/physiotwin4d/workflow_fit_statistical_model_to_patient.py
  • src/physiotwin4d/workflow_reconstruct_highres_4d_ct.py
  • statistics.md
  • tests/README.md
  • tests/baselines/registration_time_series_images/basic_forward_transform_0.hdf
  • tests/baselines/registration_time_series_images/basic_time_series_registered_0.mha
  • tests/baselines/registration_time_series_images/middle_frame_forward_transform_0.hdf
  • tests/baselines/registration_time_series_images/prior_forward_transform_0.hdf
  • tests/baselines/registration_time_series_images/prior_time_series_registered_0.mha
  • tests/baselines/registration_time_series_images/transform_application_time_series_0.mha
  • tests/conftest.py
  • tests/test_contour_tools.py
  • tests/test_convert_vtk_to_usd.py
  • tests/test_experiments.py
  • tests/test_register_images_ants.py
  • tests/test_register_images_chain.py
  • tests/test_register_images_greedy.py
  • tests/test_register_images_icon.py
  • tests/test_register_models_pca.py
  • tests/test_register_time_series_images.py
  • tests/test_tutorials.py
  • tests/test_workflow_convert_vtk_to_usd.py
  • tutorials/README.md
  • tutorials/tutorial_02_lung_distancemap_finetune_icon.py
  • tutorials/tutorial_02_lung_finetune_icon.py
  • tutorials/tutorial_04_heart_ct_to_vtk.py
  • tutorials/tutorial_04_lung_ct_to_vtk.py
  • tutorials/tutorial_05_heart_vtk_to_usd.py
  • tutorials/tutorial_06_heart_create_statistical_model.py
  • tutorials/tutorial_06_lung_create_statistical_model.py
  • tutorials/tutorial_07_lung_fit_statistical_model_to_patient.py
  • tutorials/tutorial_08_lung_fit_model_to_4d_patients.py
  • tutorials/tutorial_09_lung_train_physicsnemo_mgn.py
💤 Files with no reviewable changes (6)
  • tests/baselines/registration_time_series_images/prior_time_series_registered_0.mha
  • tests/baselines/registration_time_series_images/prior_forward_transform_0.hdf
  • tests/test_experiments.py
  • src/physiotwin4d/register_images_icon.py
  • src/physiotwin4d/workflow_reconstruct_highres_4d_ct.py
  • src/physiotwin4d/cli/reconstruct_highres_4d_ct.py

Comment thread src/physiotwin4d/contour_tools.py
Comment thread src/physiotwin4d/convert_vtk_to_usd.py Outdated
Comment thread src/physiotwin4d/register_images_base.py
Comment thread src/physiotwin4d/register_models_distance_maps.py Outdated
Comment thread src/physiotwin4d/workflow_convert_vtk_to_usd.py
Comment thread src/physiotwin4d/workflow_convert_vtk_to_usd.py
Comment thread src/physiotwin4d/workflow_fit_statistical_model_to_patient.py
Comment thread tutorials/tutorial_06_lung_create_statistical_model.py Outdated
Copilot AI review requested due to automatic review settings August 7, 2026 17:54

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 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/physiotwin4d/register_models_pca.py`:
- Around line 219-224: Validate pca_prior_weight in the initializer before
assigning it, ensuring the value is finite and non-negative; raise ValueError
for invalid inputs. Use the existing pca_prior_weight parameter and validation
pattern near symmetric_weight, while preserving valid values unchanged.

In `@src/physiotwin4d/workflow_fit_statistical_model_to_patient.py`:
- Around line 608-622: Update the three itk.imwrite calls in the log_level ==
logging.DEBUG block to pass compression=True while preserving their existing
images and output filenames.
- Around line 319-321: Update the weights_path validation in the setter
containing self.l2l_icon_weights_path to use Path(weights_path).is_file()
instead of exists(), so directories are rejected and only valid checkpoint files
are accepted before assignment.
🪄 Autofix

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 Plus

Run ID: 90532f65-0703-4f4a-9baf-4284796afe58

📥 Commits

Reviewing files that changed from the base of the PR and between b6d90fc and 5b29e24.

📒 Files selected for processing (23)
  • .agents/agents/testing.md
  • CLAUDE.md
  • docs/tutorials.rst
  • experiments/Heart-Simpleware_Segmentation/simpleware_heart_segmentation.py
  • experiments/Reconstruct4DCT/reconstruct_4d_ct.py
  • src/physiotwin4d/convert_vtk_to_usd.py
  • src/physiotwin4d/register_images_ants.py
  • src/physiotwin4d/register_images_base.py
  • src/physiotwin4d/register_images_greedy.py
  • src/physiotwin4d/register_models_distance_maps.py
  • src/physiotwin4d/register_models_pca.py
  • src/physiotwin4d/register_time_series_images.py
  • src/physiotwin4d/transform_tools.py
  • src/physiotwin4d/workflow_convert_vtk_to_usd.py
  • src/physiotwin4d/workflow_fit_statistical_model_to_patient.py
  • statistics.md
  • tests/README.md
  • tests/conftest.py
  • tests/test_contour_tools.py
  • tests/test_register_models_pca.py
  • tests/test_workflow_convert_vtk_to_usd.py
  • tutorials/README.md
  • tutorials/tutorial_06_lung_create_statistical_model.py
🚧 Files skipped from review as they are similar to previous changes (15)
  • tutorials/tutorial_06_lung_create_statistical_model.py
  • statistics.md
  • docs/tutorials.rst
  • tests/test_contour_tools.py
  • .agents/agents/testing.md
  • src/physiotwin4d/convert_vtk_to_usd.py
  • src/physiotwin4d/transform_tools.py
  • tests/README.md
  • CLAUDE.md
  • experiments/Heart-Simpleware_Segmentation/simpleware_heart_segmentation.py
  • tutorials/README.md
  • src/physiotwin4d/register_images_base.py
  • src/physiotwin4d/workflow_convert_vtk_to_usd.py
  • src/physiotwin4d/register_time_series_images.py
  • tests/test_register_models_pca.py

Comment on lines +219 to +224
self.pca_prior_weight = pca_prior_weight
if not 0.0 <= symmetric_weight <= 1.0:
raise ValueError(
f"symmetric_weight must be in [0, 1]; got {symmetric_weight}"
)
self.symmetric_weight = symmetric_weight

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reject invalid PCA prior weights.

A negative pca_prior_weight rewards large coefficient magnitudes instead of
penalizing them. A non-finite value makes the optimization objective non-finite.
Validate that the value is finite and non-negative.

Proposed fix
+        if not np.isfinite(pca_prior_weight) or pca_prior_weight < 0.0:
+            raise ValueError(
+                "pca_prior_weight must be finite and greater than or equal to 0"
+            )
         self.pca_prior_weight = pca_prior_weight
📝 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
self.pca_prior_weight = pca_prior_weight
if not 0.0 <= symmetric_weight <= 1.0:
raise ValueError(
f"symmetric_weight must be in [0, 1]; got {symmetric_weight}"
)
self.symmetric_weight = symmetric_weight
if not np.isfinite(pca_prior_weight) or pca_prior_weight < 0.0:
raise ValueError(
"pca_prior_weight must be finite and greater than or equal to 0"
)
self.pca_prior_weight = pca_prior_weight
if not 0.0 <= symmetric_weight <= 1.0:
raise ValueError(
f"symmetric_weight must be in [0, 1]; got {symmetric_weight}"
)
self.symmetric_weight = symmetric_weight
🤖 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/physiotwin4d/register_models_pca.py` around lines 219 - 224, Validate
pca_prior_weight in the initializer before assigning it, ensuring the value is
finite and non-negative; raise ValueError for invalid inputs. Use the existing
pca_prior_weight parameter and validation pattern near symmetric_weight, while
preserving valid values unchanged.

Comment on lines +319 to +321
if not Path(weights_path).exists():
raise FileNotFoundError(f"ICON weights not found: {weights_path}")
self.l2l_icon_weights_path = weights_path

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Require a checkpoint file.

Path.exists() accepts directories. A directory then passes this setter and fails
later when ICON loads the checkpoint. Use Path(weights_path).is_file().

Proposed fix
-        if not Path(weights_path).exists():
+        if not Path(weights_path).is_file():
             raise FileNotFoundError(f"ICON weights not found: {weights_path}")
📝 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
if not Path(weights_path).exists():
raise FileNotFoundError(f"ICON weights not found: {weights_path}")
self.l2l_icon_weights_path = weights_path
if not Path(weights_path).is_file():
raise FileNotFoundError(f"ICON weights not found: {weights_path}")
self.l2l_icon_weights_path = weights_path
🤖 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/physiotwin4d/workflow_fit_statistical_model_to_patient.py` around lines
319 - 321, Update the weights_path validation in the setter containing
self.l2l_icon_weights_path to use Path(weights_path).is_file() instead of
exists(), so directories are rejected and only valid checkpoint files are
accepted before assignment.

Comment on lines 608 to 622
if self.log_level == logging.DEBUG:
tfm_arr = itk.GetArrayFromImage(
self.pca_forward_point_transform.GetDisplacementField()
)
tfm_field = self.pca_forward_point_transform.GetDisplacementField()
tfm_arr = itk.GetArrayFromImage(tfm_field)
tfm_x_arr = tfm_arr[:, :, :, 0]
tfm_y_arr = tfm_arr[:, :, :, 1]
tfm_z_arr = tfm_arr[:, :, :, 2]
tfm_x_img = itk.GetImageFromArray(tfm_x_arr)
tfm_y_img = itk.GetImageFromArray(tfm_y_arr)
tfm_z_img = itk.GetImageFromArray(tfm_z_arr)
tfm_x_img.CopyInformation(self.patient_image)
tfm_y_img.CopyInformation(self.patient_image)
tfm_z_img.CopyInformation(self.patient_image)
tfm_x_img.CopyInformation(tfm_field)
tfm_y_img.CopyInformation(tfm_field)
tfm_z_img.CopyInformation(tfm_field)
itk.imwrite(tfm_x_img, "pca_forward_point_transform_x.nii.gz")
itk.imwrite(tfm_y_img, "pca_forward_point_transform_y.nii.gz")
itk.imwrite(tfm_z_img, "pca_forward_point_transform_z.nii.gz")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Enable compression for debug ITK writes.

Pass compression=True to each itk.imwrite call in this block.

As per coding guidelines, “Persist ITK images with
itk.imwrite(..., compression=True).”

Proposed fix
-            itk.imwrite(tfm_x_img, "pca_forward_point_transform_x.nii.gz")
-            itk.imwrite(tfm_y_img, "pca_forward_point_transform_y.nii.gz")
-            itk.imwrite(tfm_z_img, "pca_forward_point_transform_z.nii.gz")
+            itk.imwrite(
+                tfm_x_img, "pca_forward_point_transform_x.nii.gz", compression=True
+            )
+            itk.imwrite(
+                tfm_y_img, "pca_forward_point_transform_y.nii.gz", compression=True
+            )
+            itk.imwrite(
+                tfm_z_img, "pca_forward_point_transform_z.nii.gz", compression=True
+            )
📝 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
if self.log_level == logging.DEBUG:
tfm_arr = itk.GetArrayFromImage(
self.pca_forward_point_transform.GetDisplacementField()
)
tfm_field = self.pca_forward_point_transform.GetDisplacementField()
tfm_arr = itk.GetArrayFromImage(tfm_field)
tfm_x_arr = tfm_arr[:, :, :, 0]
tfm_y_arr = tfm_arr[:, :, :, 1]
tfm_z_arr = tfm_arr[:, :, :, 2]
tfm_x_img = itk.GetImageFromArray(tfm_x_arr)
tfm_y_img = itk.GetImageFromArray(tfm_y_arr)
tfm_z_img = itk.GetImageFromArray(tfm_z_arr)
tfm_x_img.CopyInformation(self.patient_image)
tfm_y_img.CopyInformation(self.patient_image)
tfm_z_img.CopyInformation(self.patient_image)
tfm_x_img.CopyInformation(tfm_field)
tfm_y_img.CopyInformation(tfm_field)
tfm_z_img.CopyInformation(tfm_field)
itk.imwrite(tfm_x_img, "pca_forward_point_transform_x.nii.gz")
itk.imwrite(tfm_y_img, "pca_forward_point_transform_y.nii.gz")
itk.imwrite(tfm_z_img, "pca_forward_point_transform_z.nii.gz")
if self.log_level == logging.DEBUG:
tfm_field = self.pca_forward_point_transform.GetDisplacementField()
tfm_arr = itk.GetArrayFromImage(tfm_field)
tfm_x_arr = tfm_arr[:, :, :, 0]
tfm_y_arr = tfm_arr[:, :, :, 1]
tfm_z_arr = tfm_arr[:, :, :, 2]
tfm_x_img = itk.GetImageFromArray(tfm_x_arr)
tfm_y_img = itk.GetImageFromArray(tfm_y_arr)
tfm_z_img = itk.GetImageFromArray(tfm_z_arr)
tfm_x_img.CopyInformation(tfm_field)
tfm_y_img.CopyInformation(tfm_field)
tfm_z_img.CopyInformation(tfm_field)
itk.imwrite(
tfm_x_img, "pca_forward_point_transform_x.nii.gz", compression=True
)
itk.imwrite(
tfm_y_img, "pca_forward_point_transform_y.nii.gz", compression=True
)
itk.imwrite(
tfm_z_img, "pca_forward_point_transform_z.nii.gz", compression=True
)
🤖 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/physiotwin4d/workflow_fit_statistical_model_to_patient.py` around lines
608 - 622, Update the three itk.imwrite calls in the log_level == logging.DEBUG
block to pass compression=True while preserving their existing images and output
filenames.

Source: Coding guidelines

Copilot AI left a comment

Copy link
Copy Markdown

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 89 out of 89 changed files in this pull request and generated no new comments.

Suppressed comments (2)

src/physiotwin4d/convert_vtk_to_usd.py:974

  • When a mesh has a per-cell label array but none of its IDs match mask_ids (e.g., a merged mesh tagged with SegmentationLabelIds==0 for every cell), _split_by_labels() later returns an empty dict and _convert_with_labels() ends up emitting no geometry. It would be safer to treat an all-zero (or otherwise non-matching) label array as “unlabeled” and fall back to the unified mesh path.
    src/physiotwin4d/register_images_base.py:413
  • register_from() restores self.moving_image after registering the pre-warped data, but it leaves self.moving_image_pre / self.moving_mask / self.moving_labelmap pointing at the pre-warped inputs from the inner register() call. That makes the instance state inconsistent (moving_image is original, other moving_* fields are warped) and could confuse downstream logic/debugging or any subclass that inspects self.moving_mask/labelmap after register_from(). Consider restoring/clearing the other moving_* fields as well.

Copilot AI review requested due to automatic review settings August 7, 2026 18:27
Signed-off-by: Stephen R. Aylward <stephen@aylward.org>

Copilot AI left a comment

Copy link
Copy Markdown

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 89 out of 89 changed files in this pull request and generated no new comments.

Suppressed comments (2)

src/physiotwin4d/workflow_convert_vtk_to_usd.py:172

  • When separate_by_cell_type=True, ConvertVTKToUSD names prims like {object}_Triangle (see vtk_to_usd/mesh_utils.py:37-38). _anatomy_candidates() only strips the _objectN suffix, so it will try to resolve materials for ..._Triangle and fail to fall back to the correct AnatomyGroup (because object_groups is keyed by the unsuffixed object name). This causes anatomy appearance to incorrectly fall back to the "other" material for cell-type-split meshes.
    src/physiotwin4d/register_images_base.py:490
  • The comment says "the total is the initial transform followed by that residual", but the next lines correctly explain that (because CompositeTransform applies the last-added transform first) the residual is applied first and the initial is applied second. Rewording this avoids confusion when reasoning about transform order.

Copilot AI review requested due to automatic review settings August 7, 2026 18:35

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@aylward
aylward merged commit 743938d into Project-MONAI:main Aug 7, 2026
12 checks passed
@aylward
aylward deleted the fix_greedy branch August 7, 2026 19:14
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