Skip to content

Commit 77cf5b1

Browse files
committed
FIX: Propagate output spaces at the subworkflow level
1 parent 9a2abc9 commit 77cf5b1

File tree

2 files changed

+112
-42
lines changed

2 files changed

+112
-42
lines changed

nibabies/workflows/base.py

Lines changed: 111 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -43,18 +43,20 @@
4343
import os
4444
import sys
4545
from copy import deepcopy
46+
from typing import Optional
4647

4748
from nipype.interfaces import utility as niu
4849
from nipype.pipeline import engine as pe
4950
from packaging.version import Version
5051

51-
from .. import config
52-
from ..interfaces import DerivativesDataSink
53-
from ..interfaces.reports import AboutSummary, SubjectSummary
54-
from .bold import init_func_preproc_wf
52+
from nibabies import config
53+
from nibabies.interfaces import DerivativesDataSink
54+
from nibabies.interfaces.reports import AboutSummary, SubjectSummary
55+
from nibabies.utils.bids import parse_bids_for_age_months
56+
from nibabies.workflows.bold import init_func_preproc_wf
5557

5658

57-
def init_nibabies_wf(participants_table):
59+
def init_nibabies_wf(subworkflows_list):
5860
"""
5961
Build *NiBabies*'s pipeline.
6062
@@ -76,8 +78,9 @@ def init_nibabies_wf(participants_table):
7678
7779
Parameters
7880
----------
79-
participants_table: :obj:`dict`
80-
Keys of participant labels and values of the sessions to process.
81+
subworkflows_list: :obj:`list` of :obj:`tuple`
82+
A list of the subworkflows to create.
83+
Each subject session is run as an individual workflow.
8184
"""
8285
from niworkflows.engine.workflows import LiterateWorkflow as Workflow
8386
from niworkflows.interfaces.bids import BIDSFreeSurferDir
@@ -86,52 +89,76 @@ def init_nibabies_wf(participants_table):
8689
nibabies_wf = Workflow(name=f"nibabies_{ver.major}_{ver.minor}_wf")
8790
nibabies_wf.base_dir = config.execution.work_dir
8891

92+
execution_spaces = init_execution_spaces()
93+
8994
freesurfer = config.workflow.run_reconall
9095
if freesurfer:
9196
fsdir = pe.Node(
9297
BIDSFreeSurferDir(
9398
derivatives=config.execution.output_dir,
9499
freesurfer_home=os.getenv("FREESURFER_HOME"),
95-
spaces=config.workflow.spaces.get_fs_spaces(),
100+
spaces=execution_spaces.get_fs_spaces(),
96101
),
97102
name=f"fsdir_run_{config.execution.run_uuid.replace('-', '_')}",
98103
run_without_submitting=True,
99104
)
100105
if config.execution.fs_subjects_dir is not None:
101106
fsdir.inputs.subjects_dir = str(config.execution.fs_subjects_dir.absolute())
102107

103-
for subject_id, sessions in participants_table.items():
104-
for session_id in sessions:
105-
single_subject_wf = init_single_subject_wf(subject_id, session_id=session_id)
108+
for subject_id, session_id in subworkflows_list:
109+
# Calculate the age and age-specific spaces
110+
age = parse_bids_for_age_months(config.execution.bids_dir, subject_id, session_id)
111+
if config.workflow.age_months:
112+
config.loggers.cli.warning(
113+
"`--age-months` is deprecated and will be removed in a future release."
114+
"Please use a `sessions.tsv` or `participants.tsv` file to track participants age."
115+
)
116+
age = config.workflow.age_months
117+
if age is None:
118+
raise RuntimeError(
119+
"Could not find age for sub-{subject}{session}".format(
120+
subject=subject_id, session=f'_ses-{session_id}' if session_id else ''
121+
)
122+
)
123+
output_spaces = init_workflow_spaces(execution_spaces, age)
106124

107-
bids_level = [f"sub-{subject_id}"]
108-
if session_id:
109-
bids_level.append(f"ses-{session_id}")
125+
# skull strip template cohort
126+
single_subject_wf = init_single_subject_wf(
127+
subject_id,
128+
session_id=session_id,
129+
age=age,
130+
spaces=output_spaces,
131+
)
110132

111-
log_dir = (
112-
config.execution.nibabies_dir.joinpath(*bids_level)
113-
/ "log"
114-
/ config.execution.run_uuid
115-
)
133+
bids_level = [f"sub-{subject_id}"]
134+
if session_id:
135+
bids_level.append(f"ses-{session_id}")
116136

117-
single_subject_wf.config["execution"]["crashdump_dir"] = str(log_dir)
118-
for node in single_subject_wf._get_all_nodes():
119-
node.config = deepcopy(single_subject_wf.config)
120-
if freesurfer:
121-
nibabies_wf.connect(
122-
fsdir, "subjects_dir", single_subject_wf, "inputnode.subjects_dir"
123-
)
124-
else:
125-
nibabies_wf.add_nodes([single_subject_wf])
137+
log_dir = (
138+
config.execution.nibabies_dir.joinpath(*bids_level) / "log" / config.execution.run_uuid
139+
)
126140

127-
# Dump a copy of the config file into the log directory
128-
log_dir.mkdir(exist_ok=True, parents=True)
129-
config.to_filename(log_dir / "nibabies.toml")
141+
single_subject_wf.config["execution"]["crashdump_dir"] = str(log_dir)
142+
for node in single_subject_wf._get_all_nodes():
143+
node.config = deepcopy(single_subject_wf.config)
144+
if freesurfer:
145+
nibabies_wf.connect(fsdir, "subjects_dir", single_subject_wf, "inputnode.subjects_dir")
146+
else:
147+
nibabies_wf.add_nodes([single_subject_wf])
148+
149+
# Dump a copy of the config file into the log directory
150+
log_dir.mkdir(exist_ok=True, parents=True)
151+
config.to_filename(log_dir / "nibabies.toml")
130152

131153
return nibabies_wf
132154

133155

134-
def init_single_subject_wf(subject_id, session_id=None):
156+
def init_single_subject_wf(
157+
subject_id: str,
158+
session_id: Optional[str] = None,
159+
age: Optional[int] = None,
160+
spaces=None,
161+
):
135162
"""
136163
Organize the preprocessing pipeline for a single subject, at a single session.
137164
@@ -158,6 +185,8 @@ def init_single_subject_wf(subject_id, session_id=None):
158185
Subject label for this single-subject workflow.
159186
session_id : :obj:`str` or None
160187
Session identifier.
188+
age: :obj:`int` or None
189+
Age (in months) of subject.
161190
162191
Inputs
163192
------
@@ -196,7 +225,6 @@ def init_single_subject_wf(subject_id, session_id=None):
196225
anat_only = config.workflow.anat_only
197226
derivatives = config.execution.derivatives or {}
198227
anat_modality = "t1w" if subject_data["t1w"] else "t2w"
199-
spaces = config.workflow.spaces
200228
# Make sure we always go through these two checks
201229
if not anat_only and not subject_data["bold"]:
202230
task_id = config.execution.task_id
@@ -315,7 +343,7 @@ def init_single_subject_wf(subject_id, session_id=None):
315343
# Preprocessing of anatomical (includes registration to UNCInfant)
316344
anat_preproc_wf = init_infant_anat_wf(
317345
ants_affine_init=True,
318-
age_months=config.workflow.age_months,
346+
age_months=age,
319347
anat_modality=anat_modality,
320348
t1w=subject_data["t1w"],
321349
t2w=subject_data["t2w"],
@@ -419,7 +447,7 @@ def init_single_subject_wf(subject_id, session_id=None):
419447
func_preproc_wfs = []
420448
has_fieldmap = bool(fmap_estimators)
421449
for bold_file in subject_data['bold']:
422-
func_preproc_wf = init_func_preproc_wf(bold_file, has_fieldmap=has_fieldmap)
450+
func_preproc_wf = init_func_preproc_wf(bold_file, spaces, has_fieldmap=has_fieldmap)
423451
if func_preproc_wf is None:
424452
continue
425453

@@ -526,8 +554,51 @@ def _prefix(subid):
526554
return subid if subid.startswith("sub-") else f"sub-{subid}"
527555

528556

529-
def _select_iter_idx(in_list, idx):
530-
"""Returns a specific index of a list/tuple"""
531-
if isinstance(in_list, (tuple, list)):
532-
return in_list[idx]
533-
raise AttributeError(f"Input {in_list} is incompatible type: {type(in_list)}")
557+
def init_workflow_spaces(execution_spaces, age):
558+
"""
559+
Create output spaces at a per-subworkflow level.
560+
561+
This address the case where a multi-session subject is run, and requires separate template cohorts.
562+
"""
563+
from niworkflows.utils.spaces import Reference
564+
565+
from nibabies.utils.misc import cohort_by_months
566+
567+
spaces = deepcopy(execution_spaces)
568+
569+
if not spaces.references:
570+
# Ensure age specific template is added if nothing is present
571+
cohort = cohort_by_months("MNIInfant", age)
572+
spaces.add(("MNIInfant", {"res": "native", "cohort": cohort}))
573+
574+
if not spaces.is_cached():
575+
spaces.checkpoint()
576+
577+
# Ensure user-defined spatial references for outputs are correctly parsed.
578+
# Certain options require normalization to a space not explicitly defined by users.
579+
# These spaces will not be included in the final outputs.
580+
if config.workflow.use_aroma:
581+
# Make sure there's a normalization to FSL for AROMA to use.
582+
spaces.add(Reference("MNI152NLin6Asym", {"res": "2"}))
583+
584+
if config.workflow.cifti_output:
585+
# CIFTI grayordinates to corresponding FSL-MNI resolutions.
586+
vol_res = "2" if config.workflow.cifti_output == "91k" else "1"
587+
spaces.add(Reference("fsaverage", {"den": "164k"}))
588+
spaces.add(Reference("MNI152NLin6Asym", {"res": vol_res}))
589+
# Ensure a non-native version of MNIInfant is added as a target
590+
cohort = cohort_by_months("MNIInfant", age)
591+
spaces.add(Reference("MNIInfant", {"cohort": cohort}))
592+
593+
return spaces
594+
595+
596+
def init_execution_spaces():
597+
from niworkflows.utils.spaces import Reference, SpatialReferences
598+
599+
spaces = config.execution.output_spaces or SpatialReferences()
600+
if not isinstance(spaces, SpatialReferences):
601+
spaces = SpatialReferences(
602+
[ref for s in spaces.split(" ") for ref in Reference.from_string(s)]
603+
)
604+
return spaces

nibabies/workflows/bold/base.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@
7373
from .t2s import init_bold_t2s_wf, init_t2s_reporting_wf
7474

7575

76-
def init_func_preproc_wf(bold_file, has_fieldmap=False, existing_derivatives=None):
76+
def init_func_preproc_wf(bold_file, spaces, has_fieldmap=False, existing_derivatives=None):
7777
"""
7878
This workflow controls the functional preprocessing stages of *NiBabies*.
7979
@@ -191,7 +191,6 @@ def init_func_preproc_wf(bold_file, has_fieldmap=False, existing_derivatives=Non
191191
# Have some options handy
192192
omp_nthreads = config.nipype.omp_nthreads
193193
freesurfer = config.workflow.run_reconall
194-
spaces = config.workflow.spaces
195194
nibabies_dir = str(config.execution.nibabies_dir)
196195
freesurfer_spaces = spaces.get_fs_spaces()
197196
project_goodvoxels = config.workflow.project_goodvoxels

0 commit comments

Comments
 (0)