Skip to content

Commit 8e77801

Browse files
authored
ENH: Treat session as first-class identifier (#193)
* ENH: Re-enable session-level processing * ENH: Treat session as first-class identifier * FIX: Account for session when working with layout * FIX: Patch niworkflows `Report` out_filename generation * PIN: Test sdcflows session change * CI: Remove deprecated pip flag * STY: black * FIX: Use empty list if no sessions * FIX: Ensure product is not empty if no sessions * FIX: use altered parameter name
1 parent 9c83434 commit 8e77801

File tree

9 files changed

+176
-49
lines changed

9 files changed

+176
-49
lines changed

.github/workflows/pytest.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ jobs:
2828
python -m venv /tmp/venv
2929
source /tmp/venv/bin/activate
3030
python -m pip install -U pip
31-
python -m pip install --use-feature=in-tree-build ".[test]"
31+
python -m pip install ".[test]"
3232
- name: Run Pytest
3333
run: |
3434
source /tmp/venv/bin/activate

nibabies/cli/parser.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ def _to_gb(value):
4646
def _drop_sub(value):
4747
return value[4:] if value.startswith("sub-") else value
4848

49+
def _drop_ses(value):
50+
return value[4:] if value.startswith("ses-") else value
51+
4952
def _filter_pybids_none_any(dct):
5053
import bids
5154

@@ -134,9 +137,14 @@ def _slice_time_ref(value, parser):
134137
help="a space delimited list of participant identifiers or a single "
135138
"identifier (the sub- prefix can be removed)",
136139
)
137-
# Re-enable when option is actually implemented
138-
# g_bids.add_argument('-s', '--session-id', action='store', default='single_session',
139-
# help='select a specific session to be processed')
140+
g_bids.add_argument(
141+
"-s",
142+
"--session-id",
143+
action="store",
144+
nargs="+",
145+
type=_drop_ses,
146+
help="a space delimited list of session identifiers or a single identifier",
147+
)
140148
# Re-enable when option is actually implemented
141149
# g_bids.add_argument('-r', '--run-id', action='store', default='single_run',
142150
# help='select a specific run to be processed')

nibabies/cli/run.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ def main():
149149
# Generate reports phase
150150
generate_reports(
151151
config.execution.participant_label,
152+
config.execution.session_id,
152153
config.execution.nibabies_dir,
153154
config.execution.run_uuid,
154155
config=pkgrf("nibabies", "data/reports-spec.yml"),

nibabies/cli/workflow.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,12 @@ def build_workflow(config_file, retval):
4646
subject_list = collect_participants(
4747
config.execution.layout, participant_label=config.execution.participant_label
4848
)
49+
subjects_sessions = {
50+
subject: config.execution.session_id
51+
or config.execution.layout.get_sessions(scope='raw', subject=subject)
52+
or [None]
53+
for subject in subject_list
54+
}
4955

5056
# Called with reports only
5157
if config.execution.reports_only:
@@ -78,7 +84,7 @@ def build_workflow(config_file, retval):
7884
* Pre-run FreeSurfer's SUBJECTS_DIR: {config.execution.fs_subjects_dir}."""
7985
build_log.log(25, init_msg)
8086

81-
retval["workflow"] = init_nibabies_wf()
87+
retval["workflow"] = init_nibabies_wf(subjects_sessions)
8288

8389
# Check for FS license after building the workflow
8490
if not check_valid_fs_license():

nibabies/config.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,8 @@ class execution(_Config):
404404
"""Unique identifier of this particular run."""
405405
segmentation_atlases_dir = None
406406
"""Directory with atlases to use for JLF segmentations"""
407+
session_id = None
408+
"""List of session identifiers that are to be preprocessed."""
407409
participant_label = None
408410
"""List of participant identifiers that are to be preprocessed."""
409411
task_id = None

nibabies/reports/core.py

Lines changed: 64 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,44 @@
1+
from itertools import product
12
from pathlib import Path
23

34
from niworkflows.reports.core import Report as _Report
5+
from pkg_resources import resource_filename as pkgrf
46

57

68
class Report(_Report):
9+
# niworkflows patch to preserve `out_filename` even if subject_id is present
10+
def __init__(
11+
self,
12+
out_dir,
13+
run_uuid,
14+
config=None,
15+
out_filename=None,
16+
packagename=None,
17+
reportlets_dir=None,
18+
subject_id=None,
19+
):
20+
self.root = Path(reportlets_dir or out_dir)
21+
22+
# Initialize structuring elements
23+
self.sections = []
24+
self.errors = []
25+
self.out_dir = Path(out_dir)
26+
self.run_uuid = run_uuid
27+
self.packagename = packagename
28+
self.subject_id = subject_id
29+
if subject_id is not None:
30+
self.subject_id = subject_id[4:] if subject_id.startswith("sub-") else subject_id
31+
# ensure set output filename is preserved
32+
if not out_filename:
33+
out_filename = f"sub-{self.subject_id}.html"
34+
35+
self.out_filename = out_filename or "report.html"
36+
37+
# Default template from niworkflows
38+
self.template_path = Path(pkgrf("niworkflows", "reports/report.tpl"))
39+
self._load_config(Path(config or pkgrf("niworkflows", "reports/default.yml")))
40+
assert self.template_path.exists()
41+
742
# TODO: Upstream ``Report._load_config`` to niworkflows
843
def _load_config(self, config):
944
from yaml import safe_load as load
@@ -33,6 +68,7 @@ def run_reports(
3368
subject_label,
3469
run_uuid,
3570
config=None,
71+
out_filename='report.html',
3672
reportlets_dir=None,
3773
packagename=None,
3874
):
@@ -43,30 +79,47 @@ def run_reports(
4379
out_dir,
4480
run_uuid,
4581
config=config,
82+
out_filename=out_filename,
4683
subject_id=subject_label,
4784
packagename=packagename,
4885
reportlets_dir=reportlets_dir,
4986
).generate_report()
5087

5188

5289
def generate_reports(
53-
subject_list, output_dir, run_uuid, config=None, work_dir=None, packagename=None
90+
subject_list,
91+
sessions_list,
92+
output_dir,
93+
run_uuid,
94+
config=None,
95+
work_dir=None,
96+
packagename=None,
5497
):
5598
"""Execute run_reports on a list of subjects."""
5699
reportlets_dir = None
57100
if work_dir is not None:
58101
reportlets_dir = Path(work_dir) / "reportlets"
59-
report_errors = [
60-
run_reports(
61-
output_dir,
62-
subject_label,
63-
run_uuid,
64-
config=config,
65-
packagename=packagename,
66-
reportlets_dir=reportlets_dir,
102+
103+
if sessions_list is None:
104+
sessions_list = [None]
105+
106+
report_errors = []
107+
for subject_label, session in product(subject_list, sessions_list):
108+
html_report = f"sub-{subject_label}"
109+
if session:
110+
html_report += f"_ses-{session}"
111+
html_report += ".html"
112+
report_errors.append(
113+
run_reports(
114+
output_dir,
115+
subject_label,
116+
run_uuid,
117+
config=config,
118+
out_filename=html_report,
119+
packagename=packagename,
120+
reportlets_dir=reportlets_dir,
121+
)
67122
)
68-
for subject_label in subject_list
69-
]
70123

71124
errno = sum(report_errors)
72125
if errno:

nibabies/utils/bids.py

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ def _unique(inlist):
107107
return {k: _unique(v) for k, v in entities.items()}
108108

109109

110-
def group_bolds_ref(*, layout, subject):
110+
def group_bolds_ref(*, layout, subject, session_id):
111111
"""
112112
Extracts BOLD files from a BIDS dataset and combines them into buckets.
113113
Files in a bucket share:
@@ -123,6 +123,8 @@ def group_bolds_ref(*, layout, subject):
123123
Initialized BIDSLayout
124124
subject : str
125125
The subject ID
126+
session_id : :obj:`str`, None, or ``False``
127+
The session identifier. If ``False``, all sessions will be used (default).
126128
127129
Outputs
128130
-------
@@ -153,14 +155,14 @@ def group_bolds_ref(*, layout, subject):
153155
# list of all BOLDS encountered
154156
all_bolds = []
155157

156-
for ses, suffix in sorted(
157-
product(
158-
layout.get_sessions(subject=subject, scope="raw") or (None,),
159-
{
160-
"bold",
161-
},
162-
)
163-
):
158+
# TODO: Simplify with pybids.layout.Query.OPTIONAL
159+
sessions = (
160+
[session_id]
161+
if session_id is not False
162+
else layout.get_sessions(subject=subject, scope="raw")
163+
)
164+
165+
for ses, suffix in sorted(product(sessions or (None,), {"bold"})):
164166
# bold files same session
165167
bolds = layout.get(suffix=suffix, session=ses, **base_entities)
166168
# some sessions may not have BOLD scans

nibabies/workflows/base.py

Lines changed: 78 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,36 @@
11
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*-
22
# vi: set ft=python sts=4 ts=4 sw=4 et:
3+
#
4+
# STATEMENT OF CHANGES: This file is derived from sources licensed under the Apache-2.0 terms,
5+
# and this file has been changed.
6+
# The original file this work derives from is found at:
7+
# https://github.com/nipreps/fmriprep/blob/a4fd718/fmriprep/workflows/bold/base.py
8+
#
9+
# [January 2022] CHANGES:
10+
# * `init_nibabies_wf` now takes in a dictionary composed of participant/session key/values.
11+
# * `init_single_subject_wf` now differentiates between participant sessions.
12+
# This change is to treat sessions as a "first-class" identifier, to better handle the
13+
# potential rapid changing of brain morphometry.
14+
#
15+
# Copyright 2021 The NiPreps Developers <[email protected]>
16+
#
17+
# Licensed under the Apache License, Version 2.0 (the "License");
18+
# you may not use this file except in compliance with the License.
19+
# You may obtain a copy of the License at
20+
#
21+
# http://www.apache.org/licenses/LICENSE-2.0
22+
#
23+
# Unless required by applicable law or agreed to in writing, software
24+
# distributed under the License is distributed on an "AS IS" BASIS,
25+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
26+
# See the License for the specific language governing permissions and
27+
# limitations under the License.
28+
#
29+
# We support and encourage derived works from this project, please read
30+
# about our expectations at
31+
#
32+
# https://www.nipreps.org/community/licensing/
33+
#
334
"""
435
NiBabies base processing workflows
536
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -24,7 +55,7 @@
2455
from .bold import init_func_preproc_wf
2556

2657

27-
def init_nibabies_wf():
58+
def init_nibabies_wf(participants_table):
2859
"""
2960
Build *NiBabies*'s pipeline.
3061
@@ -44,6 +75,10 @@ def init_nibabies_wf():
4475
with mock_config():
4576
wf = init_nibabies_wf()
4677
78+
Parameters
79+
----------
80+
participants_table: :obj:`dict`
81+
Keys of participant labels and values of the sessions to process.
4782
"""
4883
from niworkflows.engine.workflows import LiterateWorkflow as Workflow
4984
from niworkflows.interfaces.bids import BIDSFreeSurferDir
@@ -66,32 +101,40 @@ def init_nibabies_wf():
66101
if config.execution.fs_subjects_dir is not None:
67102
fsdir.inputs.subjects_dir = str(config.execution.fs_subjects_dir.absolute())
68103

69-
for subject_id in config.execution.participant_label:
70-
single_subject_wf = init_single_subject_wf(subject_id)
104+
for subject_id, sessions in participants_table.items():
105+
for session_id in sessions:
106+
single_subject_wf = init_single_subject_wf(subject_id, session_id=session_id)
71107

72-
single_subject_wf.config["execution"]["crashdump_dir"] = str(
73-
config.execution.nibabies_dir / f"sub-{subject_id}" / "log" / config.execution.run_uuid
74-
)
75-
for node in single_subject_wf._get_all_nodes():
76-
node.config = deepcopy(single_subject_wf.config)
77-
if freesurfer:
78-
nibabies_wf.connect(fsdir, "subjects_dir", single_subject_wf, "inputnode.subjects_dir")
79-
else:
80-
nibabies_wf.add_nodes([single_subject_wf])
81-
82-
# Dump a copy of the config file into the log directory
83-
log_dir = (
84-
config.execution.nibabies_dir / f"sub-{subject_id}" / "log" / config.execution.run_uuid
85-
)
86-
log_dir.mkdir(exist_ok=True, parents=True)
87-
config.to_filename(log_dir / "nibabies.toml")
108+
bids_level = [f"sub-{subject_id}"]
109+
if session_id:
110+
bids_level.append(f"ses-{session_id}")
111+
112+
log_dir = (
113+
config.execution.nibabies_dir.joinpath(*bids_level)
114+
/ "log"
115+
/ config.execution.run_uuid
116+
)
117+
118+
single_subject_wf.config["execution"]["crashdump_dir"] = str(log_dir)
119+
for node in single_subject_wf._get_all_nodes():
120+
node.config = deepcopy(single_subject_wf.config)
121+
if freesurfer:
122+
nibabies_wf.connect(
123+
fsdir, "subjects_dir", single_subject_wf, "inputnode.subjects_dir"
124+
)
125+
else:
126+
nibabies_wf.add_nodes([single_subject_wf])
127+
128+
# Dump a copy of the config file into the log directory
129+
log_dir.mkdir(exist_ok=True, parents=True)
130+
config.to_filename(log_dir / "nibabies.toml")
88131

89132
return nibabies_wf
90133

91134

92-
def init_single_subject_wf(subject_id):
135+
def init_single_subject_wf(subject_id, session_id=None):
93136
"""
94-
Organize the preprocessing pipeline for a single subject.
137+
Organize the preprocessing pipeline for a single subject, at a single session.
95138
96139
It collects and reports information about the subject, and prepares
97140
sub-workflows to perform anatomical and functional preprocessing.
@@ -114,6 +157,8 @@ def init_single_subject_wf(subject_id):
114157
----------
115158
subject_id : :obj:`str`
116159
Subject label for this single-subject workflow.
160+
session_id : :obj:`str` or None
161+
Session identifier.
117162
118163
Inputs
119164
------
@@ -130,10 +175,15 @@ def init_single_subject_wf(subject_id):
130175
from ..utils.misc import fix_multi_source_name
131176
from .anatomical import init_infant_anat_wf
132177

133-
name = "single_subject_%s_wf" % subject_id
178+
name = (
179+
f"single_subject_{subject_id}_{session_id}_wf"
180+
if session_id
181+
else f"single_subject_{subject_id}_wf"
182+
)
134183
subject_data = collect_data(
135184
config.execution.layout,
136185
subject_id,
186+
session_id=session_id,
137187
task=config.execution.task_id,
138188
echo=config.execution.echo_idx,
139189
bids_filters=config.execution.bids_filters,
@@ -350,6 +400,7 @@ def init_single_subject_wf(subject_id):
350400
fmap_estimators = find_estimators(
351401
layout=config.execution.layout,
352402
subject=subject_id,
403+
sessions=session_id,
353404
fmapless=False, # config.workflow.use_syn,
354405
force_fmapless=False, # config.workflow.force_syn,
355406
)
@@ -374,7 +425,11 @@ def init_single_subject_wf(subject_id):
374425
# 3) total readout time
375426
from niworkflows.workflows.epi.refmap import init_epi_reference_wf
376427

377-
_, bold_groupings = group_bolds_ref(layout=config.execution.layout, subject=subject_id)
428+
_, bold_groupings = group_bolds_ref(
429+
layout=config.execution.layout,
430+
subject=subject_id,
431+
session_id=session_id,
432+
)
378433
if any(not x for x in bold_groupings):
379434
print("No BOLD files found for one or more reference groupings")
380435
return workflow

0 commit comments

Comments
 (0)