From de328e5076e7998dee2fb65a8cdf66a2f3dc1da4 Mon Sep 17 00:00:00 2001 From: Jason Kai <21226986+kaitj@users.noreply.github.com> Date: Thu, 9 Apr 2026 14:53:14 -0400 Subject: [PATCH 1/6] Fix anat-to-tpl xfm grabbed --- src/rbc/bids/functional.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/rbc/bids/functional.py b/src/rbc/bids/functional.py index 222852cc..1991fd18 100644 --- a/src/rbc/bids/functional.py +++ b/src/rbc/bids/functional.py @@ -91,8 +91,8 @@ def resolve_functional( anat_df, suffix="xfm", extra={ - "from": TemplateSpace.MNI152NLIN6ASYM, - "to": "T1w", + "to": TemplateSpace.MNI152NLIN6ASYM, + "from": "T1w", "mode": "image", }, ), From d148fdeadd1c7b24922573ca27b6b36b7edec3b2 Mon Sep 17 00:00:00 2001 From: Jason Kai <21226986+kaitj@users.noreply.github.com> Date: Thu, 9 Apr 2026 15:17:03 -0400 Subject: [PATCH 2/6] Rename xfm attrs to be more clear --- src/rbc/bids/anatomical.py | 4 ++-- src/rbc/bids/longitudinal.py | 20 +++++++++++----- src/rbc/core/anatomical/registration.py | 24 +++++++++---------- src/rbc/orchestration/all.py | 2 +- src/rbc/workflows/anatomical.py | 24 +++++++++---------- src/rbc/workflows/functional.py | 6 ++--- tests/full_pipeline/conftest.py | 4 ++-- tests/unit/bids/test_exports.py | 4 ++-- tests/unit/orchestration/test_functional.py | 8 ++++--- tests/unit/orchestration/test_longitudinal.py | 6 ++--- 10 files changed, 56 insertions(+), 46 deletions(-) diff --git a/src/rbc/bids/anatomical.py b/src/rbc/bids/anatomical.py index 11a0cac1..2db2f57d 100644 --- a/src/rbc/bids/anatomical.py +++ b/src/rbc/bids/anatomical.py @@ -66,7 +66,7 @@ def export_anatomical(anat: Bids, outputs: AnatomicalOutputs) -> None: anat.save(outputs.wm_mask, suffix=Suffix.MASK, desc="wm") anat.save(outputs.wm_bbr_mask, suffix=Suffix.MASK, desc="wmBBR") anat.save( - outputs.forward_xfm, + outputs.anat_to_template_xfm, suffix="xfm", extra={ "from": "T1w", @@ -75,7 +75,7 @@ def export_anatomical(anat: Bids, outputs: AnatomicalOutputs) -> None: }, ) anat.save( - outputs.inverse_xfm, + outputs.template_to_anat_xfm, suffix="xfm", extra={ "from": TemplateSpace.MNI152NLIN6ASYM, diff --git a/src/rbc/bids/longitudinal.py b/src/rbc/bids/longitudinal.py index 729a69b9..b0f52032 100644 --- a/src/rbc/bids/longitudinal.py +++ b/src/rbc/bids/longitudinal.py @@ -4,7 +4,7 @@ from typing import TYPE_CHECKING -from rbc.bids import Suffix +from rbc.bids import Suffix, TemplateSpace if TYPE_CHECKING: from pathlib import Path @@ -88,14 +88,22 @@ def export_longitudinal_anat(aex: Bids, outputs: AnatomicalLongOutputs) -> None: desc="wm", ) aex.save( - outputs.forward_xfm, + outputs.long_to_template_xfm, suffix="xfm", - extra={"from": "T1w", "to": "longitudinal", "mode": "image"}, + extra={ + "from": "longitudinal", + "to": TemplateSpace.MNI152NLIN6ASYM, + "mode": "image", + }, ) aex.save( - outputs.inverse_xfm, + outputs.template_to_long_xfm, suffix="xfm", - extra={"from": "longitudinal", "to": "T1w", "mode": "image"}, + extra={ + "from": TemplateSpace.MNI152NLIN6ASYM, + "to": "longitudinal", + "mode": "image", + }, ) @@ -154,7 +162,7 @@ def export_longitudinal_func(fex: Bids, outputs: FunctionalLongOutputs) -> None: fex.save(outputs.sbref, suffix=Suffix.SBREF) fex.save(outputs.bold, suffix=Suffix.BOLD, desc="preproc") fex.save( - outputs.forward_xfm, + outputs.bold_to_long_xfm, suffix="xfm", desc="composite", extra={"from": "bold", "to": "longitudinal", "mode": "image"}, diff --git a/src/rbc/core/anatomical/registration.py b/src/rbc/core/anatomical/registration.py index 63081d5e..9b9eeb53 100644 --- a/src/rbc/core/anatomical/registration.py +++ b/src/rbc/core/anatomical/registration.py @@ -26,13 +26,13 @@ class RegistrationOutputs(NamedTuple): Attributes: brain: Warped (template-space) skull-stripped brain. - forward: T1w-to-template composite displacement field. - inverse: Template-to-T1w composite displacement field. + forward: anat-to-template composite displacement field. + inverse: Template-to-anat composite displacement field. """ brain: Path - forward: Path - inverse: Path + anat_to_template: Path + template_to_anat: Path def ants_registration( @@ -53,8 +53,8 @@ def ants_registration( (default: MNI152 1 mm). Returns: - Forward (T1w -> template) and inverse (template -> T1w) composite - transforms. + Transformed brain in template space, forward (T1w -> template) and inverse + (template -> T1w) composite transforms. """ registration = ants.ants_registration( stages=[ @@ -148,7 +148,7 @@ def ants_registration( interpolation="LanczosWindowedSinc", output=f"[{_PREFIX}_,{_PREFIX}_Warped.nii.gz]", ) - fwd = ants.ants_apply_transforms( + anat_to_template = ants.ants_apply_transforms( reference_image=registration_template, transform=[ ants.ants_apply_transforms_transform_file_name( @@ -159,11 +159,11 @@ def ants_registration( ), ], output=ants.ants_apply_transforms_composite_displacement_field_output( - composite_displacement_field="forward_xfm.nii.gz", + composite_displacement_field="anat_to_template_xfm.nii.gz", print_out_composite_warp_file=True, ), ) - rev = ants.ants_apply_transforms( + template_to_anat = ants.ants_apply_transforms( reference_image=in_file, transform=[ ants.ants_apply_transforms_transform_file_name( @@ -174,7 +174,7 @@ def ants_registration( ), ], output=ants.ants_apply_transforms_composite_displacement_field_output( - composite_displacement_field="inverse_xfm.nii.gz", + composite_displacement_field="template_to_anat_xfm.nii.gz", print_out_composite_warp_file=True, ), ) @@ -182,6 +182,6 @@ def ants_registration( # does not expose this path, so we construct it from the output root. return RegistrationOutputs( brain=registration.root / f"{_PREFIX}_Warped.nii.gz", - forward=fwd.output.output_image_outfile, - inverse=rev.output.output_image_outfile, + anat_to_template=anat_to_template.output.output_image_outfile, + template_to_anat=template_to_anat.output.output_image_outfile, ) diff --git a/src/rbc/orchestration/all.py b/src/rbc/orchestration/all.py index 0edc89ae..2b920778 100644 --- a/src/rbc/orchestration/all.py +++ b/src/rbc/orchestration/all.py @@ -118,7 +118,7 @@ def run( "brain_mask": anat_outputs.brain_mask, "csf_mask": anat_outputs.csf_mask, "wm_mask": anat_outputs.wm_mask, - "anat_to_template": anat_outputs.inverse_xfm, + "anat_to_template": anat_outputs.anat_to_template_xfm, }, tr=tr, func_template=func_template, diff --git a/src/rbc/workflows/anatomical.py b/src/rbc/workflows/anatomical.py index 4dca4186..d6d4b627 100644 --- a/src/rbc/workflows/anatomical.py +++ b/src/rbc/workflows/anatomical.py @@ -43,8 +43,8 @@ class AnatomicalOutputs(NamedTuple): gm_mask: GM tissue mask. wm_mask: WM tissue mask. wm_bbr_mask: WM boundary mask for BBR coregistration. - forward_xfm: T1w-to-template composite warp. - inverse_xfm: Template-to-T1w composite warp. + anat_to_template_xfm: anat-to-template composite warp. + template_to_anat_xfm: Template-to-anat composite warp. """ brain: Path @@ -54,8 +54,8 @@ class AnatomicalOutputs(NamedTuple): gm_mask: Path wm_mask: Path wm_bbr_mask: Path - forward_xfm: Path - inverse_xfm: Path + anat_to_template_xfm: Path + template_to_anat_xfm: Path def single_session_preprocess( @@ -112,8 +112,8 @@ def single_session_preprocess( gm_mask=tissue_masks.gm, wm_mask=tissue_masks.wm, wm_bbr_mask=wm_bbr, - forward_xfm=transforms.forward, - inverse_xfm=transforms.inverse, + anat_to_template_xfm=transforms.anat_to_template, + template_to_anat_xfm=transforms.template_to_anat, ) @@ -130,8 +130,8 @@ class AnatomicalLongOutputs(NamedTuple): or *None* if not provided. wm_mask: WM tissue mask warped to longitudinal template space, or *None* if not provided. - forward_xfm: Longitudinal template-to-MNI152 composite warp. - inverse_xfm: MNI152-to-longitudinal template composite warp. + long_to_template_xfm: Longitudinal template-to-MNI152 composite warp. + template_to_long_xfm: MNI152-to-longitudinal template composite warp. """ brain: Path @@ -139,8 +139,8 @@ class AnatomicalLongOutputs(NamedTuple): csf_mask: Path | None gm_mask: Path | None wm_mask: Path | None - forward_xfm: Path - inverse_xfm: Path + long_to_template_xfm: Path + template_to_long_xfm: Path def longitudinal_process( @@ -193,6 +193,6 @@ def _xfm(val: Path | None) -> Path | None: csf_mask=_xfm(csf_mask), gm_mask=_xfm(gm_mask), wm_mask=_xfm(wm_mask), - forward_xfm=transforms.forward, - inverse_xfm=transforms.inverse, + long_to_template_xfm=transforms.anat_to_template, + template_to_long_xfm=transforms.template_to_anat, ) diff --git a/src/rbc/workflows/functional.py b/src/rbc/workflows/functional.py index e64d96b9..ce854aef 100644 --- a/src/rbc/workflows/functional.py +++ b/src/rbc/workflows/functional.py @@ -387,14 +387,14 @@ class FunctionalLongOutputs(NamedTuple): """Outputs from the longitudinal functional preprocessing pipeline. Attributes: - forward_xfm: BOLD-to-longitudinal-template composite warp. + bold_to_long_xfm: BOLD-to-longitudinal-template composite warp. sbref: Motion reference volume warped to longitudinal template space. bold: Preprocessed BOLD warped to longitudinal template space. bold_mask: Brain mask warped to longitudinal template space, or *None* if no mask was provided. """ - forward_xfm: Path + bold_to_long_xfm: Path sbref: Path bold: Path bold_mask: Path | None = None @@ -443,5 +443,5 @@ def longitudinal_process( bold_mask=mask_transform(mask=bold_mask, template=template, xfm=bold_to_tpl_xfm) if bold_mask else None, - forward_xfm=bold_to_tpl_xfm, + bold_to_long_xfm=bold_to_tpl_xfm, ) diff --git a/tests/full_pipeline/conftest.py b/tests/full_pipeline/conftest.py index ca43ee3c..167175a5 100644 --- a/tests/full_pipeline/conftest.py +++ b/tests/full_pipeline/conftest.py @@ -105,11 +105,11 @@ def pipeline_data( brain_mask=anat.brain_mask, csf_mask=anat.csf_mask, wm_mask=anat.wm_mask, - anat_to_template=anat.forward_xfm, + anat_to_template=anat.anat_to_template_xfm, metadata=func_metadata, ) template_brain_mask = _warp_mask_to_template( - anat.brain_mask, REGISTRATION_TEMPLATES.brain_2mm, anat.forward_xfm + anat.brain_mask, REGISTRATION_TEMPLATES.brain_2mm, anat.anat_to_template_xfm ) manifest["anat"] = _to_dict(anat) manifest["func"] = _to_dict(func) diff --git a/tests/unit/bids/test_exports.py b/tests/unit/bids/test_exports.py index d7229b44..1c96faab 100644 --- a/tests/unit/bids/test_exports.py +++ b/tests/unit/bids/test_exports.py @@ -47,8 +47,8 @@ def _make_anat_outputs(w: Path) -> AnatomicalOutputs: gm_mask=_dummy(w, "gm_mask.nii.gz"), wm_mask=_dummy(w, "wm_mask.nii.gz"), wm_bbr_mask=_dummy(w, "wm_bbr_mask.nii.gz"), - forward_xfm=_dummy(w, "forward_xfm.nii.gz"), - inverse_xfm=_dummy(w, "inverse_xfm.nii.gz"), + anat_to_template_xfm=_dummy(w, "anat_to_template_xfm.nii.gz"), + template_to_anat_xfm=_dummy(w, "template_to_anat_xfm.nii.gz"), ) diff --git a/tests/unit/orchestration/test_functional.py b/tests/unit/orchestration/test_functional.py index 8e0397cf..c4834a39 100644 --- a/tests/unit/orchestration/test_functional.py +++ b/tests/unit/orchestration/test_functional.py @@ -207,8 +207,8 @@ def test_anat_outputs_forwarded_as_anat_inputs(self, tmp_path: Path) -> None: gm_mask=_FAKE / "gm_mask.nii.gz", wm_mask=_FAKE / "wm_mask.nii.gz", wm_bbr_mask=_FAKE / "wm_bbr.nii.gz", - forward_xfm=_FAKE / "fwd.nii.gz", - inverse_xfm=_FAKE / "inv.nii.gz", + anat_to_template_xfm=_FAKE / "anat_to_template.nii.gz", + template_to_anat_xfm=_FAKE / "template_to_anat.nii.gz", ) raw_df = pl.DataFrame( @@ -260,4 +260,6 @@ def test_anat_outputs_forwarded_as_anat_inputs(self, tmp_path: Path) -> None: assert passed_inputs["csf_mask"] == anat_outputs.csf_mask assert passed_inputs["wm_mask"] == anat_outputs.wm_mask assert passed_inputs["wm_bbr_mask"] == anat_outputs.wm_bbr_mask - assert passed_inputs["anat_to_template"] == anat_outputs.inverse_xfm + assert ( + passed_inputs["anat_to_template"] == anat_outputs.anat_to_template_xfm + ) diff --git a/tests/unit/orchestration/test_longitudinal.py b/tests/unit/orchestration/test_longitudinal.py index dea9cd81..13bb53e2 100644 --- a/tests/unit/orchestration/test_longitudinal.py +++ b/tests/unit/orchestration/test_longitudinal.py @@ -56,8 +56,8 @@ def _mock_anat_outputs() -> Mock: m.csf_mask = fake / "csf_mask.nii.gz" m.gm_mask = fake / "gm_mask.nii.gz" m.wm_mask = fake / "wm_mask.nii.gz" - m.forward_xfm = fake / "fwd_xfm.nii.gz" - m.inverse_xfm = fake / "inverse_xfm.nii.gz" + m.long_to_anat_xfm = fake / "long_to_anat_xfm.nii.gz" + m.anat_to_long_xfm = fake / "anat_to_long_xfm.nii.gz" return m @@ -66,7 +66,7 @@ def _mock_func_outputs(*, with_bold_mask: bool = True) -> Mock: m = Mock() m.sbref = fake / "sbref.nii.gz" m.bold = fake / "bold.nii.gz" - m.forward_xfm = fake / "fwd_xfm.nii.gz" + m.bold_to_long_xfm = fake / "bold_to_long_xfm.nii.gz" m.bold_mask = (fake / "bold_mask.nii.gz") if with_bold_mask else None return m From ca5f1a679e6132a1fc3db869ebd9c406f02ce399 Mon Sep 17 00:00:00 2001 From: Jason Kai <21226986+kaitj@users.noreply.github.com> Date: Thu, 9 Apr 2026 16:23:44 -0400 Subject: [PATCH 3/6] Fix integration assertion --- tests/integration/test_anatomical.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration/test_anatomical.py b/tests/integration/test_anatomical.py index 327adf8c..f5042e6a 100644 --- a/tests/integration/test_anatomical.py +++ b/tests/integration/test_anatomical.py @@ -37,5 +37,5 @@ def test_registration(test_subject: TestSubjectData) -> None: """Test anatomical registration.""" reg_outputs = anatomical.ants_registration(in_file=test_subject.t1w) assert reg_outputs.brain.exists() - assert reg_outputs.forward.exists() - assert reg_outputs.inverse.exists() + assert reg_outputs.anat_to_template.exists() + assert reg_outputs.template_to_anat.exists() From 7aaca11361a8de86f3e47494fe91f5fa1ae34f27 Mon Sep 17 00:00:00 2001 From: Florian Rupprecht Date: Thu, 9 Apr 2026 18:09:23 -0400 Subject: [PATCH 4/6] Pin uv version in full test workflow to avoid manifest fetch The self-hosted runner's network blocks raw.githubusercontent.com, causing setup-uv to fail when fetching the version manifest. Pinning the version skips the network lookup entirely. --- .github/workflows/test_full.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test_full.yaml b/.github/workflows/test_full.yaml index 69d871e7..386a8cb3 100644 --- a/.github/workflows/test_full.yaml +++ b/.github/workflows/test_full.yaml @@ -27,6 +27,7 @@ jobs: - uses: actions/checkout@v6 - uses: astral-sh/setup-uv@v8.0.0 with: + version: "0.11.3" enable-cache: true # not automatic on self-hosted runners - run: uv sync From 3e9949baad14140baed3e2b0f314432460b6c398 Mon Sep 17 00:00:00 2001 From: Florian Rupprecht Date: Thu, 9 Apr 2026 19:12:40 -0400 Subject: [PATCH 5/6] Fix mock attribute names to match AnatomicalLongOutputs fields The mocks used `long_to_anat_xfm`/`anat_to_long_xfm` but the actual NamedTuple fields are `long_to_template_xfm`/`template_to_long_xfm`. Mock() silently creates any attribute, so this mismatch was invisible. --- tests/unit/orchestration/test_longitudinal.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/orchestration/test_longitudinal.py b/tests/unit/orchestration/test_longitudinal.py index 13bb53e2..ca28e510 100644 --- a/tests/unit/orchestration/test_longitudinal.py +++ b/tests/unit/orchestration/test_longitudinal.py @@ -56,8 +56,8 @@ def _mock_anat_outputs() -> Mock: m.csf_mask = fake / "csf_mask.nii.gz" m.gm_mask = fake / "gm_mask.nii.gz" m.wm_mask = fake / "wm_mask.nii.gz" - m.long_to_anat_xfm = fake / "long_to_anat_xfm.nii.gz" - m.anat_to_long_xfm = fake / "anat_to_long_xfm.nii.gz" + m.long_to_template_xfm = fake / "long_to_template_xfm.nii.gz" + m.template_to_long_xfm = fake / "template_to_long_xfm.nii.gz" return m From b0546256ba8ce86bebd5c30dc25cf7c4b9ebdfec Mon Sep 17 00:00:00 2001 From: Florian Rupprecht Date: Thu, 9 Apr 2026 19:22:32 -0400 Subject: [PATCH 6/6] Fix stat overlay using different slices than background in report _render_stat_overlay was calling _build_mosaic independently for the background and stat map. Since _axial_slices picks slices based on non-zero extent, the z-scored maps (which have zeros outside the mask and negative values) computed different slice indices than the template BOLD underlay, causing visible misalignment in the overlay. Now computes slice indices once from the background volume and reuses them for the stat map. --- scripts/visualize_pipeline.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/scripts/visualize_pipeline.py b/scripts/visualize_pipeline.py index 35b83e25..ca954af5 100644 --- a/scripts/visualize_pipeline.py +++ b/scripts/visualize_pipeline.py @@ -146,9 +146,11 @@ def _resample_mosaic( def _build_mosaic( data: np.ndarray, n: int = 7, + slices: list[int] | None = None, ) -> np.ndarray: """Build an axial-slice mosaic resampled to standard dimensions.""" - slices = _axial_slices(data, n) + if slices is None: + slices = _axial_slices(data, n) panels = [data[:, :, z].T for z in slices] mosaic = np.concatenate(panels, axis=1) return _resample_mosaic(mosaic) @@ -285,8 +287,9 @@ def _render_stat_overlay( ) -> None: """Lightbox with thresholded stat map overlaid on background.""" bg_vmax = _robust_vmax(bg_data) - bg_mosaic = _build_mosaic(bg_data, n) - stat_mosaic = _build_mosaic(stat_data, n) + slices = _axial_slices(bg_data, n) + bg_mosaic = _build_mosaic(bg_data, n, slices=slices) + stat_mosaic = _build_mosaic(stat_data, n, slices=slices) ax.imshow( bg_mosaic,