Skip to content

Commit 68902bd

Browse files
authored
Merge pull request #287 from nipreps/enh/sessions-tsv-age
ENH: Extract participant ages from BIDS sources
2 parents cda42ff + 42ad1ba commit 68902bd

File tree

12 files changed

+285
-174
lines changed

12 files changed

+285
-174
lines changed

.circleci/bcp_anat_outputs.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ logs/CITATION.html
88
logs/CITATION.md
99
logs/CITATION.tex
1010
sub-01
11-
sub-01.html
1211
sub-01/ses-1mo
1312
sub-01/ses-1mo/anat
1413
sub-01/ses-1mo/anat/sub-01_ses-1mo_run-001_desc-aparcaseg_dseg.nii.gz
@@ -49,3 +48,4 @@ sub-01/ses-1mo/anat/sub-01_ses-1mo_run-001_space-MNIInfant_cohort-1_label-CSF_pr
4948
sub-01/ses-1mo/anat/sub-01_ses-1mo_run-001_space-MNIInfant_cohort-1_label-GM_probseg.nii.gz
5049
sub-01/ses-1mo/anat/sub-01_ses-1mo_run-001_space-MNIInfant_cohort-1_label-WM_probseg.nii.gz
5150
sub-01/ses-1mo/anat/sub-01_ses-1mo_run-001_space-T1w_desc-preproc_T2w.nii.gz
51+
sub-01_ses-1mo.html

.circleci/bcp_full_outputs.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ logs/CITATION.html
88
logs/CITATION.md
99
logs/CITATION.tex
1010
sub-01
11-
sub-01.html
1211
sub-01/ses-1mo
1312
sub-01/ses-1mo/anat
1413
sub-01/ses-1mo/anat/sub-01_ses-1mo_run-001_desc-aparcaseg_dseg.nii.gz
@@ -72,3 +71,4 @@ sub-01/ses-1mo/func/sub-01_ses-1mo_task-rest_acq-PA_run-001_space-MNIInfant_coho
7271
sub-01/ses-1mo/func/sub-01_ses-1mo_task-rest_acq-PA_run-001_space-MNIInfant_cohort-1_desc-brain_mask.nii.gz
7372
sub-01/ses-1mo/func/sub-01_ses-1mo_task-rest_acq-PA_run-001_space-MNIInfant_cohort-1_desc-preproc_bold.json
7473
sub-01/ses-1mo/func/sub-01_ses-1mo_task-rest_acq-PA_run-001_space-MNIInfant_cohort-1_desc-preproc_bold.nii.gz
74+
sub-01_ses-1mo.html

docs/usage.md

Lines changed: 29 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,19 @@ The input dataset is required to be in valid
88
{abbr}`BIDS (The Brain Imaging Data Structure)` format,
99
and it must include at least one T1-weighted and
1010
one T2-weighted structural image and
11-
(unless disabled with a flag) a BOLD series.
11+
a BOLD series (unless using the `--anat-only` flag).
12+
1213
We highly recommend that you validate your dataset with the free, online
1314
[BIDS Validator](http://bids-standard.github.io/bids-validator/).
1415

15-
The exact command to run *NiBabies* depends on the [Installation](./installation.md) method.
16-
The common parts of the command follow the
17-
[BIDS-Apps](https://github.com/BIDS-Apps) definition.
18-
Example:
19-
20-
```Shell
21-
$ nibabies data/bids_root/ out/ participant -w work/ --participant-id 01 --age-months 12
22-
```
16+
### Participant Ages
17+
*NiBabies* will attempt to automatically extract participant ages (in months) from the BIDS layout.
18+
Specifically, these two files will be checked:
19+
- [Sessions file](https://bids-specification.readthedocs.io/en/stable/03-modality-agnostic-files.html#sessions-file): `<bids-root>/<subject>/subject_sessions.tsv`
20+
- [Participants file](https://bids-specification.readthedocs.io/en/stable/03-modality-agnostic-files.html#participants-file): `<bids-root>/participants.tsv`
2321

24-
Further information about BIDS and BIDS-Apps can be found at the
25-
[NiPreps portal](https://www.nipreps.org/apps/framework/).
22+
Either file should include `age` (or if you wish to be more explicit: `age_months`) columns, and it is
23+
recommended to have an accompanying JSON file to further describe these fields, and explicitly state the values are in months.
2624

2725
## The FreeSurfer license
2826

@@ -33,6 +31,21 @@ To obtain a FreeSurfer license, simply register for free at https://surfer.nmr.m
3331
FreeSurfer will search for a license key file first using the `$FS_LICENSE` environment variable and then in the default path to the license key file (`$FREESURFER_HOME`/license.txt). If `$FS_LICENSE` is set, the [`nibabies-wrapper`](#using-the-nibabies-wrapper) will automatically handle setting the license within the container.
3432
Otherwise, you will need to use the `--fs-license-file` flag to ensure the license is available.
3533

34+
35+
## Example command
36+
37+
The exact command to run *NiBabies* depends on the [Installation](./installation.md) method.
38+
The common parts of the command follow the
39+
[BIDS-Apps](https://github.com/BIDS-Apps) definition.
40+
Example:
41+
42+
```Shell
43+
$ nibabies data/bids_root/ out/ participant -w work/ --participant-id 01
44+
```
45+
46+
Further information about BIDS and BIDS-Apps can be found at the
47+
[NiPreps portal](https://www.nipreps.org/apps/framework/).
48+
3649
## Command-Line Arguments
3750
```{argparse}
3851
:ref: nibabies.cli.parser._build_parser
@@ -50,21 +63,9 @@ At minimum, the following *positional* arguments are required.
5063

5164
However, as infant brains can vastly differ depending on age, providing the following arguments is highly recommended:
5265

53-
- **`--age-months`** - participant age in months
54-
55-
:::{admonition} Warning
56-
:class: warning
57-
58-
This is required if FreeSurfer is not disabled (`--fs-no-reconall`)
59-
:::
60-
6166
- **`--participant-id`** - participant ID
6267

63-
:::{admonition} Tip
64-
:class: tip
65-
66-
This is recommended when using `--age-months` if age varies across participants.
67-
:::
68+
- **`--session-id`** - session ID
6869

6970
- **`--segmentation-atlases-dir`** - directory containing pre-labeled segmentations to use for Joint Label Fusion.
7071

@@ -85,11 +86,11 @@ For installation instructions, please see [](installation.md#installing-the-niba
8586
### Sample Docker usage
8687

8788
```
88-
$ nibabies-wrapper docker /path/to/data /path/to/output participant --age-months 12 --fs-license-file /usr/freesurfer/license.txt
89+
$ nibabies-wrapper docker /path/to/data /path/to/output participant --fs-license-file /usr/freesurfer/license.txt
8990
9091
RUNNING: docker run --rm -e DOCKER_VERSION_8395080871=20.10.6 -it -v /path/to/data:/data:ro \
9192
-v /path/to/output:/out -v /usr/freesurfer/license.txt:/opt/freesurfer/license.txt:ro \
92-
nipreps/nibabies:21.0.0 /data /out participant --age-months 12
93+
nipreps/nibabies:23.0.0 /data /out participant
9394
...
9495
```
9596

@@ -103,11 +104,11 @@ This can be overridden by using the `-i` flag to specify a particular Docker ima
103104
### Sample Singularity usage
104105

105106
```
106-
$ nibabies-wrapper singularity /path/to/data /path/to/output participant --age-months 12 -i nibabies-21.0.0.sif --fs-license-file /usr/freesurfer/license.txt
107+
$ nibabies-wrapper singularity /path/to/data /path/to/output participant -i nibabies-23.0.0.sif --fs-license-file /usr/freesurfer/license.txt
107108
108109
RUNNING: singularity run --cleanenv -B /path/to/data:/data:ro \
109110
-B /path/to/output:/out -B /usr/freesurfer/license.txt:/opt/freesurfer/license.txt:ro \
110-
nibabies-21.0.0.sif /data /out participant --age-months 12
111+
nibabies-23.0.0.sif /data /out participant
111112
...
112113
```
113114

nibabies/cli/parser.py

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -696,20 +696,6 @@ def parse_args(args=None, namespace=None):
696696
config.execution.log_level = int(max(25 - 5 * opts.verbose_count, logging.DEBUG))
697697
config.from_dict(vars(opts))
698698

699-
# Initialize --output-spaces if not defined
700-
if config.execution.output_spaces is None:
701-
from niworkflows.utils.spaces import Reference, SpatialReferences
702-
703-
from ..utils.misc import cohort_by_months
704-
705-
if config.workflow.age_months is None:
706-
parser.error("--age-months must be provided if --output-spaces is not set.")
707-
708-
cohort = cohort_by_months("MNIInfant", config.workflow.age_months)
709-
config.execution.output_spaces = SpatialReferences(
710-
[Reference("MNIInfant", {"res": "native", "cohort": cohort})]
711-
)
712-
713699
# Retrieve logging level
714700
build_log = config.loggers.cli
715701

@@ -831,8 +817,41 @@ def parse_args(args=None, namespace=None):
831817

832818
config.execution.participant_label = sorted(participant_label)
833819
config.workflow.skull_strip_template = config.workflow.skull_strip_template[0]
820+
config.execution.unique_labels = compute_subworkflows()
834821

835822
# finally, write config to file
836823
config_file = config.execution.work_dir / config.execution.run_uuid / "config.toml"
837824
config_file.parent.mkdir(exist_ok=True, parents=True)
838825
config.to_filename(config_file)
826+
827+
828+
def compute_subworkflows() -> list:
829+
"""
830+
Query all available participants and sessions, and construct the combinations of the
831+
subworkflows needed.
832+
"""
833+
from niworkflows.utils.bids import collect_participants
834+
835+
from nibabies import config
836+
837+
# consists of (subject_id, session_id) tuples
838+
subworkflows = []
839+
840+
subject_list = collect_participants(
841+
config.execution.layout,
842+
participant_label=config.execution.participant_label,
843+
strict=True,
844+
)
845+
846+
for subject in subject_list:
847+
# Due to rapidly changing morphometry of the population
848+
# Ensure each subject session is processed individually
849+
sessions = (
850+
config.execution.session_id
851+
or config.execution.layout.get_sessions(scope='raw', subject=subject)
852+
or [None]
853+
)
854+
# grab participant age per session
855+
for session in sessions:
856+
subworkflows.append((subject, session))
857+
return subworkflows

nibabies/cli/run.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,8 +141,7 @@ def main():
141141

142142
# Generate reports phase
143143
generate_reports(
144-
config.execution.participant_label,
145-
config.execution.session_id,
144+
config.execution.unique_labels,
146145
config.execution.nibabies_dir,
147146
config.execution.run_uuid,
148147
config=pkgrf("nibabies", "data/reports-spec.yml"),

nibabies/cli/workflow.py

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
def build_workflow(config_file):
1414
"""Create the Nipype Workflow that supports the whole execution graph."""
15-
from niworkflows.utils.bids import check_pipeline_version, collect_participants
15+
from niworkflows.utils.bids import check_pipeline_version
1616
from niworkflows.utils.misc import check_valid_fs_license
1717

1818
from .. import config
@@ -42,24 +42,17 @@ def build_workflow(config_file):
4242
desc_content = dset_desc_path.read_bytes()
4343
config.execution.bids_description_hash = sha256(desc_content).hexdigest()
4444

45-
# First check that bids_dir looks like a BIDS folder
46-
subject_list = collect_participants(
47-
config.execution.layout, participant_label=config.execution.participant_label
48-
)
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-
}
55-
5645
# Called with reports only
5746
if config.execution.reports_only:
5847
from pkg_resources import resource_filename as pkgrf
5948

60-
build_logger.log(25, "Running --reports-only on participants %s", ", ".join(subject_list))
49+
build_logger.log(
50+
25,
51+
"Running --reports-only on participants %s",
52+
", ".join(config.execution.unique_labels),
53+
)
6154
retval["return_code"] = generate_reports(
62-
subject_list,
55+
config.execution.unique_labels,
6356
nibabies_dir,
6457
config.execution.run_uuid,
6558
config=pkgrf("nibabies", "data/reports-spec.yml"),
@@ -71,9 +64,9 @@ def build_workflow(config_file):
7164
init_msg = f"""
7265
Running nibabies version {config.environment.version}:
7366
* BIDS dataset path: {config.execution.bids_dir}.
74-
* Participant list: {subject_list}.
67+
* Participant list: {config.execution.unique_labels}.
7568
* Run identifier: {config.execution.run_uuid}.
76-
* Output spaces: {config.execution.output_spaces}."""
69+
* Output spaces: {config.execution.output_spaces or 'MNIInfant'}."""
7770

7871
if config.execution.anat_derivatives:
7972
init_msg += f"""
@@ -84,7 +77,7 @@ def build_workflow(config_file):
8477
* Pre-run FreeSurfer's SUBJECTS_DIR: {config.execution.fs_subjects_dir}."""
8578
build_logger.log(25, init_msg)
8679

87-
retval["workflow"] = init_nibabies_wf(subjects_sessions)
80+
retval["workflow"] = init_nibabies_wf(config.execution.unique_labels)
8881

8982
# Check for FS license after building the workflow
9083
if not check_valid_fs_license():

nibabies/config.py

Lines changed: 2 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,8 @@ class execution(_Config):
425425
"""Select a particular task from all available in the dataset."""
426426
templateflow_home = _templateflow_home
427427
"""The root folder of the TemplateFlow client."""
428+
unique_labels = None
429+
"""Combinations of subject + session identifiers to be preprocessed."""
428430
work_dir = Path("work").absolute()
429431
"""Path to a working directory where intermediate results will be available."""
430432
write_graph = False
@@ -581,8 +583,6 @@ class workflow(_Config):
581583
instance keeping standard and nonstandard spaces."""
582584
surface_recon_method = "infantfs"
583585
"""Method to use for surface reconstruction."""
584-
topup_max_vols = 5
585-
"""Maximum number of volumes to use with TOPUP, per-series (EPI or BOLD)."""
586586
use_aroma = None
587587
"""Run ICA-:abbr:`AROMA (automatic removal of motion artifacts)`."""
588588
use_bbr = False
@@ -694,7 +694,6 @@ def load(filename, skip=None):
694694
section = getattr(sys.modules[__name__], sectionname)
695695
ignore = skip.get(sectionname)
696696
section.load(configs, ignore=ignore)
697-
init_spaces()
698697

699698

700699
def get(flat=False):
@@ -729,42 +728,6 @@ def to_filename(filename):
729728
filename.write_text(dumps())
730729

731730

732-
def init_spaces(checkpoint=True):
733-
"""Initialize the :attr:`~workflow.spaces` setting."""
734-
from niworkflows.utils.spaces import Reference, SpatialReferences
735-
736-
spaces = execution.output_spaces or SpatialReferences()
737-
if not isinstance(spaces, SpatialReferences):
738-
spaces = SpatialReferences(
739-
[ref for s in spaces.split(" ") for ref in Reference.from_string(s)]
740-
)
741-
742-
if checkpoint and not spaces.is_cached():
743-
spaces.checkpoint()
744-
745-
# Ensure user-defined spatial references for outputs are correctly parsed.
746-
# Certain options require normalization to a space not explicitly defined by users.
747-
# These spaces will not be included in the final outputs.
748-
if workflow.use_aroma:
749-
# Make sure there's a normalization to FSL for AROMA to use.
750-
spaces.add(Reference("MNI152NLin6Asym", {"res": "2"}))
751-
752-
if workflow.cifti_output:
753-
# CIFTI grayordinates to corresponding FSL-MNI resolutions.
754-
vol_res = "2" if workflow.cifti_output == "91k" else "1"
755-
spaces.add(Reference("fsaverage", {"den": "164k"}))
756-
spaces.add(Reference("MNI152NLin6Asym", {"res": vol_res}))
757-
# Ensure a non-native version of MNIInfant is added as a target
758-
if workflow.age_months is not None:
759-
from .utils.misc import cohort_by_months
760-
761-
cohort = cohort_by_months("MNIInfant", workflow.age_months)
762-
spaces.add(Reference("MNIInfant", {"cohort": cohort}))
763-
764-
# Make the SpatialReferences object available
765-
workflow.spaces = spaces
766-
767-
768731
def _process_initializer(cwd, omp_nthreads):
769732
"""Initialize the environment of the child process."""
770733
os.chdir(cwd)

nibabies/reports/core.py

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,7 @@ def run_reports(
8787

8888

8989
def generate_reports(
90-
subject_list,
91-
sessions_list,
90+
sub_ses_list,
9291
output_dir,
9392
run_uuid,
9493
config=None,
@@ -100,15 +99,11 @@ def generate_reports(
10099
if work_dir is not None:
101100
reportlets_dir = Path(work_dir) / "reportlets"
102101

103-
if sessions_list is None:
104-
sessions_list = [None]
105-
106102
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"
103+
for subject_label, session in sub_ses_list:
104+
html_report = ''.join(
105+
[f"sub-{subject_label}", f"_ses-{session}" if session else "", ".html"]
106+
)
112107
report_errors.append(
113108
run_reports(
114109
output_dir,
@@ -127,7 +122,7 @@ def generate_reports(
127122

128123
logger = logging.getLogger("cli")
129124
error_list = ", ".join(
130-
"%s (%d)" % (subid, err) for subid, err in zip(subject_list, report_errors) if err
125+
"%s (%d)" % (subid, err) for subid, err in zip(sub_ses_list, report_errors) if err
131126
)
132127
logger.error(
133128
"Preprocessing did not finish successfully. Errors occurred while processing "

0 commit comments

Comments
 (0)