Skip to content

Commit 671742f

Browse files
committed
wip: add option for sessionwise processing
1 parent 767744a commit 671742f

File tree

4 files changed

+103
-26
lines changed

4 files changed

+103
-26
lines changed

fmriprep/cli/workflow.py

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,11 @@ def build_workflow(config_file, retval):
4141
from niworkflows.utils.bids import collect_participants
4242
from niworkflows.utils.misc import check_valid_fs_license
4343

44+
from fmriprep import config, data
4445
from fmriprep.reports.core import generate_reports
4546
from fmriprep.utils.bids import check_pipeline_version
46-
47-
from .. import config, data
48-
from ..utils.misc import check_deps
49-
from ..workflows.base import init_fmriprep_wf
47+
from fmriprep.utils.misc import check_deps, fmt_subjects_sessions
48+
from fmriprep.workflows.base import init_fmriprep_wf
5049

5150
config.load(config_file)
5251
build_log = config.loggers.workflow
@@ -85,16 +84,27 @@ def build_workflow(config_file, retval):
8584
config.execution.layout, participant_label=config.execution.participant_label
8685
)
8786
session_list = config.execution.session_label or []
88-
subject_session_list = get_subject_session_list()
87+
subject_session_list = create_processing_groups(
88+
config.execution.layout,
89+
subject_list,
90+
session_list,
91+
config.execution.subject_anatomical_reference,
92+
)
93+
config.execution.processing_groups = subject_session_list
8994

9095
# Called with reports only
9196
if config.execution.reports_only:
92-
build_log.log(25, 'Running --reports-only on participants %s', ', '.join(subject_list))
93-
session_list = (
94-
config.execution.bids_filters.get('bold', {}).get('session')
95-
if config.execution.bids_filters
96-
else None
97+
build_log.log(
98+
25,
99+
'Running --reports-only on %s',
100+
fmt_subjects_sessions(subject_session_list)
97101
)
102+
if not session_list:
103+
session_list = (
104+
config.execution.bids_filters.get('bold', {}).get('session')
105+
if config.execution.bids_filters
106+
else None
107+
)
98108

99109
failed_reports = generate_reports(
100110
config.execution.participant_label,
@@ -115,7 +125,7 @@ def build_workflow(config_file, retval):
115125
init_msg = [
116126
"Building fMRIPrep's workflow:",
117127
f'BIDS dataset path: {config.execution.bids_dir}.',
118-
f'Participant list: {subject_list}.',
128+
f'Participants and sessions: {fmt_subjects_sessions(subject_session_list)}.',
119129
f'Run identifier: {config.execution.run_uuid}.',
120130
f'Output spaces: {config.execution.output_spaces}.',
121131
]
@@ -240,7 +250,7 @@ def build_boilerplate(config_file, workflow):
240250
config.loggers.cli.warning('Could not generate CITATION.tex file:\n%s', ' '.join(cmd))
241251

242252

243-
def get_subject_session_list(
253+
def create_processing_groups(
244254
layout: 'BIDSLayout',
245255
subject_list: list,
246256
session_list: list | str | None,

fmriprep/config.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,8 @@ class execution(_Config):
434434
"""Only build the reports, based on the reportlets found in a cached working directory."""
435435
run_uuid = f'{strftime("%Y%m%d-%H%M%S")}_{uuid4()}'
436436
"""Unique identifier of this particular run."""
437+
processing_groups = None
438+
"""List of tuples (participant, session(s)) that will be preprocessed."""
437439
participant_label = None
438440
"""List of participant identifiers that are to be preprocessed."""
439441
session_label = None

fmriprep/utils/misc.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,3 +66,28 @@ def estimate_bold_mem_usage(bold_fname: str) -> tuple[int, dict]:
6666
}
6767

6868
return bold_tlen, mem_gb
69+
70+
71+
def fmt_subjects_sessions(subses: list[str], concat_limit: int = 1):
72+
"""
73+
Format a list of subjects and sessions to be printed.
74+
75+
Example
76+
-------
77+
>>> fmt_subjects_sessions([('01', 'A'), ('02', ['A', 'B']), ('03', None), ('04', ['A'])])
78+
'sub-01 ses-A, sub-02 (2 sessions), sub-03, sub-04 ses-A'
79+
"""
80+
output = []
81+
for subject, session in subses:
82+
if isinstance(session, list):
83+
if len(session) > concat_limit:
84+
output.append(f'sub-{subject} ({len(session)} sessions)')
85+
continue
86+
session = session[0]
87+
88+
if session is None:
89+
output.append(f'sub-{subject}')
90+
else:
91+
output.append(f'sub-{subject} ses-{session}')
92+
93+
return ', '.join(output)

fmriprep/workflows/base.py

Lines changed: 54 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ def init_fmriprep_wf():
5252
Build *fMRIPrep*'s pipeline.
5353
5454
This workflow organizes the execution of FMRIPREP, with a sub-workflow for
55-
each subject.
55+
each processing group.
5656
5757
If FreeSurfer's ``recon-all`` is to be run, a corresponding folder is created
5858
and populated with any needed template subjects under the derivatives folder.
@@ -91,12 +91,27 @@ def init_fmriprep_wf():
9191
if config.execution.fs_subjects_dir is not None:
9292
fsdir.inputs.subjects_dir = str(config.execution.fs_subjects_dir.absolute())
9393

94-
for subject_id in config.execution.participant_label:
95-
single_subject_wf = init_single_subject_wf(subject_id)
94+
for subject_id, session_ids in config.execution.processing_groups:
95+
log_dir = config.execution.fmriprep_dir / f'sub-{subject_id}'
96+
sessions = listify(session_ids)
97+
ses_str = ''
9698

97-
single_subject_wf.config['execution']['crashdump_dir'] = str(
98-
config.execution.fmriprep_dir / f'sub-{subject_id}' / 'log' / config.execution.run_uuid
99+
if isinstance(sessions, list):
100+
from smriprep.utils.misc import stringify_sessions
101+
102+
ses_str = stringify_sessions(sessions)
103+
log_dir /= f'ses-{ses_str}'
104+
105+
log_dir /= 'log' / config.execution.run_uuid
106+
107+
wf_name = '_'.join(
108+
('sub', subject_id,) +
109+
(('ses', ses_str) if ses_str else ()) +
110+
('wf',)
99111
)
112+
single_subject_wf = init_single_subject_wf(subject_id, sessions, name=wf_name)
113+
114+
single_subject_wf.config['execution']['crashdump_dir'] = str(log_dir)
100115
for node in single_subject_wf._get_all_nodes():
101116
node.config = deepcopy(single_subject_wf.config)
102117
if freesurfer:
@@ -105,16 +120,17 @@ def init_fmriprep_wf():
105120
fmriprep_wf.add_nodes([single_subject_wf])
106121

107122
# Dump a copy of the config file into the log directory
108-
log_dir = (
109-
config.execution.fmriprep_dir / f'sub-{subject_id}' / 'log' / config.execution.run_uuid
110-
)
111123
log_dir.mkdir(exist_ok=True, parents=True)
112124
config.to_filename(log_dir / 'fmriprep.toml')
113125

114126
return fmriprep_wf
115127

116128

117-
def init_single_subject_wf(subject_id: str):
129+
def init_single_subject_wf(
130+
subject_id: str,
131+
session_id: str | list[str] | None = None,
132+
name: str | None = None,
133+
):
118134
"""
119135
Organize the preprocessing pipeline for a single subject.
120136
@@ -139,6 +155,12 @@ def init_single_subject_wf(subject_id: str):
139155
----------
140156
subject_id : :obj:`str`
141157
Subject label for this single-subject workflow.
158+
session_id
159+
Session label(s) for this workflow.
160+
name
161+
Name of the workflow.
162+
If not provided, will be set to ``sub_{subject_id}_ses_{session_id}_wf``.
163+
142164
143165
Inputs
144166
------
@@ -169,7 +191,10 @@ def init_single_subject_wf(subject_id: str):
169191

170192
from fmriprep.workflows.bold.base import init_bold_wf
171193

172-
workflow = Workflow(name=f'sub_{subject_id}_wf')
194+
if name is None:
195+
name = f'sub_{subject_id}_wf'
196+
197+
workflow = Workflow(name=name)
173198
workflow.__desc__ = f"""
174199
Results included in this manuscript come from preprocessing
175200
performed using *fMRIPrep* {config.environment.version}
@@ -204,6 +229,7 @@ def init_single_subject_wf(subject_id: str):
204229
subject_data = collect_data(
205230
config.execution.layout,
206231
subject_id,
232+
session_id=session_id,
207233
task=config.execution.task_id,
208234
echo=config.execution.echo_idx,
209235
bids_filters=config.execution.bids_filters,
@@ -255,6 +281,7 @@ def init_single_subject_wf(subject_id: str):
255281
derivatives_dir=deriv_dir,
256282
subject_id=subject_id,
257283
std_spaces=std_spaces,
284+
session_id=session_id,
258285
)
259286
)
260287

@@ -265,7 +292,7 @@ def init_single_subject_wf(subject_id: str):
265292
subject_data=subject_data,
266293
anat_only=config.workflow.anat_only,
267294
subject_id=subject_id,
268-
anat_derivatives=anatomical_cache if anatomical_cache else None,
295+
anat_derivatives=anatomical_cache or None,
269296
),
270297
name='bidssrc',
271298
)
@@ -363,7 +390,7 @@ def init_single_subject_wf(subject_id: str):
363390
('roi', 'inputnode.roi'),
364391
('flair', 'inputnode.flair'),
365392
]),
366-
(bids_info, anat_fit_wf, [(('subject', _prefix), 'inputnode.subject_id')]),
393+
(bids_info, anat_fit_wf, [(('subject', _prefix, 'session'), 'inputnode.subject_id')]),
367394
# Reporting connections
368395
(inputnode, summary, [('subjects_dir', 'subjects_dir')]),
369396
(bidssrc, summary, [('t2w', 't2w'), ('bold', 'bold')]),
@@ -929,8 +956,21 @@ def map_fieldmap_estimation(
929956
return fmap_estimators, estimator_map
930957

931958

932-
def _prefix(subid):
933-
return subid if subid.startswith('sub-') else f'sub-{subid}'
959+
def _prefix(subject_id, session_id=None):
960+
"""Create FreeSurfer subject ID."""
961+
if not subject_id.startswith('sub-'):
962+
subject_id = f'sub-{subject_id}'
963+
964+
if session_id:
965+
ses_str = session_id
966+
if isinstance(session_id, list):
967+
from smriprep.utils.misc import stringify_sessions
968+
969+
ses_str = stringify_sessions(session_id)
970+
if not ses_str.startswith('ses-'):
971+
ses_str = f'ses-{ses_str}'
972+
subject_id += f'_{ses_str}'
973+
return subject_id
934974

935975

936976
def clean_datasinks(workflow: pe.Workflow) -> pe.Workflow:

0 commit comments

Comments
 (0)