Skip to content

Commit 6608565

Browse files
committed
Refactor to resample template per task
Templates for different takss _could_ be different resolutions. This refactors the resampling such that a template is generated per task for the longitudinal workflow.
1 parent ee0da2a commit 6608565

6 files changed

Lines changed: 72 additions & 38 deletions

File tree

src/rbc/bids/longitudinal/functional.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,9 @@ def resolve_longitudinal_func(
5050
without=["space"],
5151
)
5252

53+
task = func_df["task"].unique()[0]
5354
return {
54-
"template": tpl_q.expect(tpl_df, res="bold", suffix="T1w"),
55+
"template": tpl_q.expect(tpl_df, suffix="T1w", res=task),
5556
"anat_to_template_xfm": tpl_q.expect(
5657
tpl_df,
5758
suffix="xfm",

src/rbc/bids/longitudinal/template.py

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,13 @@ class TemplateInputs(NamedTuple):
2121
sub: Subject label.
2222
sessions: Per-input session labels (parallel to ``files``).
2323
files: Per-session preprocessed T1w brain volumes.
24-
bold_ref: First BOLD volume for grid reference.
24+
bold_ref: Per-session preprocessed BOLD volumes (all tasks).
2525
"""
2626

2727
sub: str
2828
sessions: list[str]
2929
files: list[Path]
30-
bold_ref: Path | None
30+
bold_files: dict[str, Path]
3131

3232

3333
def discover_template_inputs(
@@ -58,17 +58,13 @@ def discover_template_inputs(
5858
# the mri_robust_template invocation.
5959
pl.col("space").is_null(),
6060
)
61-
bold_ref_rows = df.filter(
61+
bold_rows = df.filter(
6262
pl.col("ses") != "longitudinal",
6363
pl.col("datatype") == "func",
64+
pl.col("desc") == "preproc",
6465
pl.col("suffix") == "bold",
6566
pl.col("space").is_null(),
6667
)
67-
bold_ref = (
68-
None
69-
if bold_ref_rows.is_empty()
70-
else Path(bold_ref_rows["root"][0]) / bold_ref_rows["path"][0]
71-
)
7268

7369
inputs: list[TemplateInputs] = []
7470
skipped: list[str] = []
@@ -81,8 +77,18 @@ def discover_template_inputs(
8177
files = [
8278
Path(row["root"]) / row["path"] for row in sub_group.iter_rows(named=True)
8379
]
80+
# Filter for first found session; only single reference per task is needed
81+
sub_bold = bold_rows.filter(
82+
(pl.col("sub") == sub) & (pl.col("ses") == sessions[0])
83+
)
84+
bold_files = {
85+
row["task"]: Path(row["root"]) / row["path"]
86+
for row in sub_bold.iter_rows(named=True)
87+
}
8488
inputs.append(
85-
TemplateInputs(sub=sub, sessions=sessions, files=files, bold_ref=bold_ref)
89+
TemplateInputs(
90+
sub=sub, sessions=sessions, files=files, bold_files=bold_files
91+
)
8692
)
8793
return inputs, skipped
8894

@@ -96,8 +102,8 @@ def export_template(tpl: Bids, outputs: LongitudinalTemplateOutputs) -> None:
96102
outputs: Results from the longitudinal template workflow.
97103
"""
98104
tpl.save(outputs.template, suffix=Suffix.T1W)
99-
if outputs.bold_template is not None:
100-
tpl.save(outputs.bold_template, res="bold", suffix=Suffix.T1W)
105+
for btask, bold_template in outputs.bold_templates.items():
106+
tpl.save(bold_template, res=btask, suffix=Suffix.T1W)
101107
for ses, xfm in zip(outputs.sessions, outputs.transforms, strict=True):
102108
ses_label = bids_safe_label(ses)
103109
tpl.save(

src/rbc/orchestration/longitudinal/template.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ def process_subject(
4949
sub=inputs.sub,
5050
sessions=inputs.sessions,
5151
in_files=inputs.files,
52-
bold_ref=inputs.bold_ref,
52+
bold_files=inputs.bold_files,
5353
)
5454
tpl = pipe_ctx.bids(datatype=Datatype.ANAT).derive(ses="longitudinal")
5555
export_template(tpl, outputs)

src/rbc/workflows/longitudinal/template.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from rbc.core.longitudinal.resampling import resample_img_to_bold_grid
1818

1919
if TYPE_CHECKING:
20-
from collections.abc import Sequence
20+
from collections.abc import Mapping, Sequence
2121
from pathlib import Path
2222

2323
_logger = logging.getLogger("rbc")
@@ -34,7 +34,7 @@ class LongitudinalTemplateOutputs(NamedTuple):
3434
"""
3535

3636
template: Path
37-
bold_template: Path | None
37+
bold_templates: dict[str, Path]
3838
sessions: list[str]
3939
transforms: list[Path]
4040

@@ -43,15 +43,15 @@ def generate_subject_template(
4343
sub: str,
4444
sessions: Sequence[str],
4545
in_files: Sequence[Path],
46-
bold_ref: Path | None = None,
46+
bold_files: Mapping[str, Path],
4747
) -> LongitudinalTemplateOutputs:
4848
"""Build a robust template and ITK transforms for one subject.
4949
5050
Args:
5151
sub: Subject label (without the ``sub-`` prefix).
5252
sessions: Session labels parallel to ``in_files``.
5353
in_files: Per-session preprocessed T1w volumes (e.g. brain-extracted).
54-
bold_ref: Reference bold volume to resample template for functional data.
54+
bold_files: Reference bold volumes to resample template for functional data.
5555
5656
Returns:
5757
:class:`LongitudinalTemplateOutputs` ready for BIDS export.
@@ -70,13 +70,15 @@ def generate_subject_template(
7070
in_xfms=robust.transforms,
7171
)
7272

73-
bold_template = (
74-
resample_img_to_bold_grid(bold_ref, robust.template) if bold_ref else None
75-
)
73+
_logger.info("Creating reference volumes for each functional task")
74+
bold_templates = {
75+
btask: resample_img_to_bold_grid(bfile, robust.template)
76+
for btask, bfile in bold_files.items()
77+
}
7678

7779
return LongitudinalTemplateOutputs(
7880
template=robust.template,
79-
bold_template=bold_template,
81+
bold_templates=bold_templates,
8082
sessions=list(sessions),
8183
transforms=itk_xfms,
8284
)

tests/unit/bids/test_longitudinal_functional.py

Lines changed: 41 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ def _func_row(
2828
sub: str,
2929
ses: str,
3030
suffix: str,
31+
task: str,
3132
desc: str | None = None,
3233
ext: str = ".nii.gz",
3334
space: str | None = None,
@@ -38,11 +39,12 @@ def _func_row(
3839
space_part = f"_space-{space}" if space else ""
3940
path = (
4041
f"sub-{sub}/ses-{ses}/func/"
41-
f"sub-{sub}_ses-{ses}{space_part}{desc_part}_{suffix}{ext}"
42+
f"sub-{sub}_ses-{ses}{space_part}_task-{task}{desc_part}_{suffix}{ext}"
4243
)
4344
return {
4445
"datatype": "func",
4546
"suffix": suffix,
47+
"task": task,
4648
"ext": ext,
4749
"sub": sub,
4850
"ses": ses,
@@ -121,12 +123,17 @@ class TestResolveLongitudinalFunc:
121123
def test_resolves_single_regressor(self, tmp_path: Path) -> None:
122124
"""Single regressor resolves raw regressor file from derivatives."""
123125
func_df = _df(
124-
_func_row(sub="01", ses="baseline", suffix="sbref"),
125-
_func_row(sub="01", ses="baseline", suffix="bold", desc="preproc"),
126-
_func_row(sub="01", ses="baseline", suffix="mask", desc="brain"),
126+
_func_row(sub="01", ses="baseline", task="rest", suffix="sbref"),
127+
_func_row(
128+
sub="01", ses="baseline", task="rest", suffix="bold", desc="preproc"
129+
),
130+
_func_row(
131+
sub="01", ses="baseline", task="rest", suffix="mask", desc="brain"
132+
),
127133
_func_row(
128134
sub="01",
129135
ses="baseline",
136+
task="rest",
130137
suffix="xfm",
131138
desc="linearITK",
132139
ext=".txt",
@@ -139,13 +146,14 @@ def test_resolves_single_regressor(self, tmp_path: Path) -> None:
139146
_func_row(
140147
sub="01",
141148
ses="baseline",
149+
task="rest",
142150
suffix="regressors",
143151
desc="36parameter",
144152
ext=".1D",
145153
),
146154
)
147155
tpl_df = _df(
148-
_anat_row(sub="01", ses="longitudinal", res="bold", suffix="T1w"),
156+
_anat_row(sub="01", ses="longitudinal", res="rest", suffix="T1w"),
149157
_anat_row(
150158
sub="01",
151159
ses="longitudinal",
@@ -189,12 +197,17 @@ def test_resolves_single_regressor(self, tmp_path: Path) -> None:
189197
def test_resolves_multiple_regressors(self, tmp_path: Path) -> None:
190198
"""Multiple regressors each get their own raw regressor file resolved."""
191199
func_df = _df(
192-
_func_row(sub="01", ses="baseline", suffix="sbref"),
193-
_func_row(sub="01", ses="baseline", suffix="bold", desc="preproc"),
194-
_func_row(sub="01", ses="baseline", suffix="mask", desc="brain"),
200+
_func_row(sub="01", ses="baseline", task="rest", suffix="sbref"),
201+
_func_row(
202+
sub="01", ses="baseline", task="rest", suffix="bold", desc="preproc"
203+
),
204+
_func_row(
205+
sub="01", ses="baseline", task="rest", suffix="mask", desc="brain"
206+
),
195207
_func_row(
196208
sub="01",
197209
ses="baseline",
210+
task="rest",
198211
suffix="xfm",
199212
desc="linearITK",
200213
ext=".txt",
@@ -207,20 +220,22 @@ def test_resolves_multiple_regressors(self, tmp_path: Path) -> None:
207220
_func_row(
208221
sub="01",
209222
ses="baseline",
223+
task="rest",
210224
suffix="regressors",
211225
desc="36parameter",
212226
ext=".1D",
213227
),
214228
_func_row(
215229
sub="01",
216230
ses="baseline",
231+
task="rest",
217232
suffix="regressors",
218233
desc="aCompCor",
219234
ext=".1D",
220235
),
221236
)
222237
tpl_df = _df(
223-
_anat_row(sub="01", ses="longitudinal", res="bold", suffix="T1w"),
238+
_anat_row(sub="01", ses="longitudinal", res="rest", suffix="T1w"),
224239
_anat_row(
225240
sub="01",
226241
ses="longitudinal",
@@ -253,12 +268,17 @@ def test_resolves_multiple_regressors(self, tmp_path: Path) -> None:
253268
def test_missing_regressor_raises(self, tmp_path: Path) -> None:
254269
"""Requesting a regressor not present in derivatives raises."""
255270
func_df = _df(
256-
_func_row(sub="01", ses="baseline", suffix="sbref"),
257-
_func_row(sub="01", ses="baseline", suffix="bold", desc="preproc"),
258-
_func_row(sub="01", ses="baseline", suffix="mask", desc="brain"),
271+
_func_row(sub="01", ses="baseline", task="rest", suffix="sbref"),
272+
_func_row(
273+
sub="01", ses="baseline", task="rest", suffix="bold", desc="preproc"
274+
),
275+
_func_row(
276+
sub="01", ses="baseline", task="rest", suffix="mask", desc="brain"
277+
),
259278
_func_row(
260279
sub="01",
261280
ses="baseline",
281+
task="rest",
262282
suffix="xfm",
263283
desc="linearITK",
264284
ext=".txt",
@@ -271,13 +291,14 @@ def test_missing_regressor_raises(self, tmp_path: Path) -> None:
271291
_func_row(
272292
sub="01",
273293
ses="baseline",
294+
task="rest",
274295
suffix="regressors",
275296
desc="36parameter",
276297
ext=".1D",
277298
),
278299
)
279300
tpl_df = _df(
280-
_anat_row(sub="01", ses="longitudinal", res="bold", suffix="T1w"),
301+
_anat_row(sub="01", ses="longitudinal", res="rest", suffix="T1w"),
281302
_anat_row(
282303
sub="01",
283304
ses="longitudinal",
@@ -307,11 +328,14 @@ def test_missing_regressor_raises(self, tmp_path: Path) -> None:
307328
def test_bold_mask_mandatory(self, tmp_path: Path) -> None:
308329
"""bold_mask is now resolved with expect(), so missing raises."""
309330
func_df = _df(
310-
_func_row(sub="01", ses="baseline", suffix="sbref"),
311-
_func_row(sub="01", ses="baseline", suffix="bold", desc="preproc"),
331+
_func_row(sub="01", ses="baseline", task="rest", suffix="sbref"),
332+
_func_row(
333+
sub="01", ses="baseline", task="rest", suffix="bold", desc="preproc"
334+
),
312335
_func_row(
313336
sub="01",
314337
ses="baseline",
338+
task="rest",
315339
suffix="xfm",
316340
desc="linearITK",
317341
ext=".txt",
@@ -324,13 +348,14 @@ def test_bold_mask_mandatory(self, tmp_path: Path) -> None:
324348
_func_row(
325349
sub="01",
326350
ses="baseline",
351+
task="rest",
327352
suffix="regressors",
328353
desc="36parameter",
329354
ext=".1D",
330355
),
331356
)
332357
tpl_df = _df(
333-
_anat_row(sub="01", ses="longitudinal", res="bold", suffix="T1w"),
358+
_anat_row(sub="01", ses="longitudinal", res="rest", suffix="T1w"),
334359
_anat_row(
335360
sub="01",
336361
ses="longitudinal",

tests/unit/bids/test_longitudinal_template.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ def test_writes_template_and_xfms(self, tmp_path: Path) -> None:
155155

156156
outputs = LongitudinalTemplateOutputs(
157157
template=template_src,
158-
bold_template=template_bold_src,
158+
bold_templates={"test": template_bold_src},
159159
sessions=["baseline", "vis2"],
160160
transforms=[xfm_baseline, xfm_vis2],
161161
)

0 commit comments

Comments
 (0)