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 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, 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/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", }, ), 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/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() 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..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.forward_xfm = fake / "fwd_xfm.nii.gz" - m.inverse_xfm = fake / "inverse_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 @@ -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