Skip to content

Commit

Permalink
Merge pull request #287 from nipreps/enh/sessions-tsv-age
Browse files Browse the repository at this point in the history
ENH: Extract participant ages from BIDS sources
  • Loading branch information
mgxd authored May 26, 2023
2 parents cda42ff + 42ad1ba commit 68902bd
Show file tree
Hide file tree
Showing 12 changed files with 285 additions and 174 deletions.
2 changes: 1 addition & 1 deletion .circleci/bcp_anat_outputs.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ logs/CITATION.html
logs/CITATION.md
logs/CITATION.tex
sub-01
sub-01.html
sub-01/ses-1mo
sub-01/ses-1mo/anat
sub-01/ses-1mo/anat/sub-01_ses-1mo_run-001_desc-aparcaseg_dseg.nii.gz
Expand Down Expand Up @@ -49,3 +48,4 @@ sub-01/ses-1mo/anat/sub-01_ses-1mo_run-001_space-MNIInfant_cohort-1_label-CSF_pr
sub-01/ses-1mo/anat/sub-01_ses-1mo_run-001_space-MNIInfant_cohort-1_label-GM_probseg.nii.gz
sub-01/ses-1mo/anat/sub-01_ses-1mo_run-001_space-MNIInfant_cohort-1_label-WM_probseg.nii.gz
sub-01/ses-1mo/anat/sub-01_ses-1mo_run-001_space-T1w_desc-preproc_T2w.nii.gz
sub-01_ses-1mo.html
2 changes: 1 addition & 1 deletion .circleci/bcp_full_outputs.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ logs/CITATION.html
logs/CITATION.md
logs/CITATION.tex
sub-01
sub-01.html
sub-01/ses-1mo
sub-01/ses-1mo/anat
sub-01/ses-1mo/anat/sub-01_ses-1mo_run-001_desc-aparcaseg_dseg.nii.gz
Expand Down Expand Up @@ -72,3 +71,4 @@ sub-01/ses-1mo/func/sub-01_ses-1mo_task-rest_acq-PA_run-001_space-MNIInfant_coho
sub-01/ses-1mo/func/sub-01_ses-1mo_task-rest_acq-PA_run-001_space-MNIInfant_cohort-1_desc-brain_mask.nii.gz
sub-01/ses-1mo/func/sub-01_ses-1mo_task-rest_acq-PA_run-001_space-MNIInfant_cohort-1_desc-preproc_bold.json
sub-01/ses-1mo/func/sub-01_ses-1mo_task-rest_acq-PA_run-001_space-MNIInfant_cohort-1_desc-preproc_bold.nii.gz
sub-01_ses-1mo.html
57 changes: 29 additions & 28 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,19 @@ The input dataset is required to be in valid
{abbr}`BIDS (The Brain Imaging Data Structure)` format,
and it must include at least one T1-weighted and
one T2-weighted structural image and
(unless disabled with a flag) a BOLD series.
a BOLD series (unless using the `--anat-only` flag).

We highly recommend that you validate your dataset with the free, online
[BIDS Validator](http://bids-standard.github.io/bids-validator/).

The exact command to run *NiBabies* depends on the [Installation](./installation.md) method.
The common parts of the command follow the
[BIDS-Apps](https://github.com/BIDS-Apps) definition.
Example:

```Shell
$ nibabies data/bids_root/ out/ participant -w work/ --participant-id 01 --age-months 12
```
### Participant Ages
*NiBabies* will attempt to automatically extract participant ages (in months) from the BIDS layout.
Specifically, these two files will be checked:
- [Sessions file](https://bids-specification.readthedocs.io/en/stable/03-modality-agnostic-files.html#sessions-file): `<bids-root>/<subject>/subject_sessions.tsv`
- [Participants file](https://bids-specification.readthedocs.io/en/stable/03-modality-agnostic-files.html#participants-file): `<bids-root>/participants.tsv`

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

## The FreeSurfer license

Expand All @@ -33,6 +31,21 @@ To obtain a FreeSurfer license, simply register for free at https://surfer.nmr.m
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.
Otherwise, you will need to use the `--fs-license-file` flag to ensure the license is available.


## Example command

The exact command to run *NiBabies* depends on the [Installation](./installation.md) method.
The common parts of the command follow the
[BIDS-Apps](https://github.com/BIDS-Apps) definition.
Example:

```Shell
$ nibabies data/bids_root/ out/ participant -w work/ --participant-id 01
```

Further information about BIDS and BIDS-Apps can be found at the
[NiPreps portal](https://www.nipreps.org/apps/framework/).

## Command-Line Arguments
```{argparse}
:ref: nibabies.cli.parser._build_parser
Expand All @@ -50,21 +63,9 @@ At minimum, the following *positional* arguments are required.

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

- **`--age-months`** - participant age in months

:::{admonition} Warning
:class: warning

This is required if FreeSurfer is not disabled (`--fs-no-reconall`)
:::

- **`--participant-id`** - participant ID

:::{admonition} Tip
:class: tip

This is recommended when using `--age-months` if age varies across participants.
:::
- **`--session-id`** - session ID

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

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

```
$ nibabies-wrapper docker /path/to/data /path/to/output participant --age-months 12 --fs-license-file /usr/freesurfer/license.txt
$ nibabies-wrapper docker /path/to/data /path/to/output participant --fs-license-file /usr/freesurfer/license.txt
RUNNING: docker run --rm -e DOCKER_VERSION_8395080871=20.10.6 -it -v /path/to/data:/data:ro \
-v /path/to/output:/out -v /usr/freesurfer/license.txt:/opt/freesurfer/license.txt:ro \
nipreps/nibabies:21.0.0 /data /out participant --age-months 12
nipreps/nibabies:23.0.0 /data /out participant
...
```

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

```
$ 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
$ nibabies-wrapper singularity /path/to/data /path/to/output participant -i nibabies-23.0.0.sif --fs-license-file /usr/freesurfer/license.txt
RUNNING: singularity run --cleanenv -B /path/to/data:/data:ro \
-B /path/to/output:/out -B /usr/freesurfer/license.txt:/opt/freesurfer/license.txt:ro \
nibabies-21.0.0.sif /data /out participant --age-months 12
nibabies-23.0.0.sif /data /out participant
...
```

Expand Down
47 changes: 33 additions & 14 deletions nibabies/cli/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -696,20 +696,6 @@ def parse_args(args=None, namespace=None):
config.execution.log_level = int(max(25 - 5 * opts.verbose_count, logging.DEBUG))
config.from_dict(vars(opts))

# Initialize --output-spaces if not defined
if config.execution.output_spaces is None:
from niworkflows.utils.spaces import Reference, SpatialReferences

from ..utils.misc import cohort_by_months

if config.workflow.age_months is None:
parser.error("--age-months must be provided if --output-spaces is not set.")

cohort = cohort_by_months("MNIInfant", config.workflow.age_months)
config.execution.output_spaces = SpatialReferences(
[Reference("MNIInfant", {"res": "native", "cohort": cohort})]
)

# Retrieve logging level
build_log = config.loggers.cli

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

config.execution.participant_label = sorted(participant_label)
config.workflow.skull_strip_template = config.workflow.skull_strip_template[0]
config.execution.unique_labels = compute_subworkflows()

# finally, write config to file
config_file = config.execution.work_dir / config.execution.run_uuid / "config.toml"
config_file.parent.mkdir(exist_ok=True, parents=True)
config.to_filename(config_file)


def compute_subworkflows() -> list:
"""
Query all available participants and sessions, and construct the combinations of the
subworkflows needed.
"""
from niworkflows.utils.bids import collect_participants

from nibabies import config

# consists of (subject_id, session_id) tuples
subworkflows = []

subject_list = collect_participants(
config.execution.layout,
participant_label=config.execution.participant_label,
strict=True,
)

for subject in subject_list:
# Due to rapidly changing morphometry of the population
# Ensure each subject session is processed individually
sessions = (
config.execution.session_id
or config.execution.layout.get_sessions(scope='raw', subject=subject)
or [None]
)
# grab participant age per session
for session in sessions:
subworkflows.append((subject, session))
return subworkflows
3 changes: 1 addition & 2 deletions nibabies/cli/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,8 +141,7 @@ def main():

# Generate reports phase
generate_reports(
config.execution.participant_label,
config.execution.session_id,
config.execution.unique_labels,
config.execution.nibabies_dir,
config.execution.run_uuid,
config=pkgrf("nibabies", "data/reports-spec.yml"),
Expand Down
27 changes: 10 additions & 17 deletions nibabies/cli/workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

def build_workflow(config_file):
"""Create the Nipype Workflow that supports the whole execution graph."""
from niworkflows.utils.bids import check_pipeline_version, collect_participants
from niworkflows.utils.bids import check_pipeline_version
from niworkflows.utils.misc import check_valid_fs_license

from .. import config
Expand Down Expand Up @@ -42,24 +42,17 @@ def build_workflow(config_file):
desc_content = dset_desc_path.read_bytes()
config.execution.bids_description_hash = sha256(desc_content).hexdigest()

# First check that bids_dir looks like a BIDS folder
subject_list = collect_participants(
config.execution.layout, participant_label=config.execution.participant_label
)
subjects_sessions = {
subject: config.execution.session_id
or config.execution.layout.get_sessions(scope='raw', subject=subject)
or [None]
for subject in subject_list
}

# Called with reports only
if config.execution.reports_only:
from pkg_resources import resource_filename as pkgrf

build_logger.log(25, "Running --reports-only on participants %s", ", ".join(subject_list))
build_logger.log(
25,
"Running --reports-only on participants %s",
", ".join(config.execution.unique_labels),
)
retval["return_code"] = generate_reports(
subject_list,
config.execution.unique_labels,
nibabies_dir,
config.execution.run_uuid,
config=pkgrf("nibabies", "data/reports-spec.yml"),
Expand All @@ -71,9 +64,9 @@ def build_workflow(config_file):
init_msg = f"""
Running nibabies version {config.environment.version}:
* BIDS dataset path: {config.execution.bids_dir}.
* Participant list: {subject_list}.
* Participant list: {config.execution.unique_labels}.
* Run identifier: {config.execution.run_uuid}.
* Output spaces: {config.execution.output_spaces}."""
* Output spaces: {config.execution.output_spaces or 'MNIInfant'}."""

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

retval["workflow"] = init_nibabies_wf(subjects_sessions)
retval["workflow"] = init_nibabies_wf(config.execution.unique_labels)

# Check for FS license after building the workflow
if not check_valid_fs_license():
Expand Down
41 changes: 2 additions & 39 deletions nibabies/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,8 @@ class execution(_Config):
"""Select a particular task from all available in the dataset."""
templateflow_home = _templateflow_home
"""The root folder of the TemplateFlow client."""
unique_labels = None
"""Combinations of subject + session identifiers to be preprocessed."""
work_dir = Path("work").absolute()
"""Path to a working directory where intermediate results will be available."""
write_graph = False
Expand Down Expand Up @@ -581,8 +583,6 @@ class workflow(_Config):
instance keeping standard and nonstandard spaces."""
surface_recon_method = "infantfs"
"""Method to use for surface reconstruction."""
topup_max_vols = 5
"""Maximum number of volumes to use with TOPUP, per-series (EPI or BOLD)."""
use_aroma = None
"""Run ICA-:abbr:`AROMA (automatic removal of motion artifacts)`."""
use_bbr = False
Expand Down Expand Up @@ -694,7 +694,6 @@ def load(filename, skip=None):
section = getattr(sys.modules[__name__], sectionname)
ignore = skip.get(sectionname)
section.load(configs, ignore=ignore)
init_spaces()


def get(flat=False):
Expand Down Expand Up @@ -729,42 +728,6 @@ def to_filename(filename):
filename.write_text(dumps())


def init_spaces(checkpoint=True):
"""Initialize the :attr:`~workflow.spaces` setting."""
from niworkflows.utils.spaces import Reference, SpatialReferences

spaces = execution.output_spaces or SpatialReferences()
if not isinstance(spaces, SpatialReferences):
spaces = SpatialReferences(
[ref for s in spaces.split(" ") for ref in Reference.from_string(s)]
)

if checkpoint and not spaces.is_cached():
spaces.checkpoint()

# Ensure user-defined spatial references for outputs are correctly parsed.
# Certain options require normalization to a space not explicitly defined by users.
# These spaces will not be included in the final outputs.
if workflow.use_aroma:
# Make sure there's a normalization to FSL for AROMA to use.
spaces.add(Reference("MNI152NLin6Asym", {"res": "2"}))

if workflow.cifti_output:
# CIFTI grayordinates to corresponding FSL-MNI resolutions.
vol_res = "2" if workflow.cifti_output == "91k" else "1"
spaces.add(Reference("fsaverage", {"den": "164k"}))
spaces.add(Reference("MNI152NLin6Asym", {"res": vol_res}))
# Ensure a non-native version of MNIInfant is added as a target
if workflow.age_months is not None:
from .utils.misc import cohort_by_months

cohort = cohort_by_months("MNIInfant", workflow.age_months)
spaces.add(Reference("MNIInfant", {"cohort": cohort}))

# Make the SpatialReferences object available
workflow.spaces = spaces


def _process_initializer(cwd, omp_nthreads):
"""Initialize the environment of the child process."""
os.chdir(cwd)
Expand Down
17 changes: 6 additions & 11 deletions nibabies/reports/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,7 @@ def run_reports(


def generate_reports(
subject_list,
sessions_list,
sub_ses_list,
output_dir,
run_uuid,
config=None,
Expand All @@ -100,15 +99,11 @@ def generate_reports(
if work_dir is not None:
reportlets_dir = Path(work_dir) / "reportlets"

if sessions_list is None:
sessions_list = [None]

report_errors = []
for subject_label, session in product(subject_list, sessions_list):
html_report = f"sub-{subject_label}"
if session:
html_report += f"_ses-{session}"
html_report += ".html"
for subject_label, session in sub_ses_list:
html_report = ''.join(
[f"sub-{subject_label}", f"_ses-{session}" if session else "", ".html"]
)
report_errors.append(
run_reports(
output_dir,
Expand All @@ -127,7 +122,7 @@ def generate_reports(

logger = logging.getLogger("cli")
error_list = ", ".join(
"%s (%d)" % (subid, err) for subid, err in zip(subject_list, report_errors) if err
"%s (%d)" % (subid, err) for subid, err in zip(sub_ses_list, report_errors) if err
)
logger.error(
"Preprocessing did not finish successfully. Errors occurred while processing "
Expand Down
Loading

0 comments on commit 68902bd

Please sign in to comment.