Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ENH: Generate anatomical derivatives useful for resampling #3081

Merged
merged 7 commits into from
Aug 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/pre-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
strategy:
matrix:
os: ['ubuntu-latest']
python-version: [3.9, '3.10', '3.11']
python-version: ['3.10', '3.11']
install: ['pip']
check: ['tests']
pip-flags: ['PRE_PIP_FLAGS']
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/stable.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:
strategy:
matrix:
os: ['ubuntu-latest']
python-version: [3.9, '3.10', '3.11']
python-version: ['3.10', '3.11']
install: ['pip']
check: ['tests']
pip-flags: ['']
Expand Down
9 changes: 9 additions & 0 deletions fmriprep/cli/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,15 @@ def _slice_time_ref(value, parser):

g_subset = parser.add_argument_group("Options for performing only a subset of the workflow")
g_subset.add_argument("--anat-only", action="store_true", help="Run anatomical workflows only")
g_subset.add_argument(
"--level",
action="store",
default="full",
choices=["minimal", "resampling", "full"],
help="Processing level; may be 'minimal' (nothing that can be recomputed), "
"'resampling' (recomputable targets that aid in resampling) "
"or 'full' (all target outputs).",
)
g_subset.add_argument(
"--boilerplate-only",
"--boilerplate_only",
Expand Down
1 change: 0 additions & 1 deletion fmriprep/cli/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ def main():
"""Entry point."""
import gc
import sys
import warnings
from multiprocessing import Manager, Process
from os import EX_SOFTWARE
from pathlib import Path
Expand Down
2 changes: 2 additions & 0 deletions fmriprep/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -544,6 +544,8 @@ class workflow(_Config):
"""Run FreeSurfer ``recon-all`` with the ``-hires`` flag."""
ignore = None
"""Ignore particular steps for *fMRIPrep*."""
level = None
"""Level of preprocessing to complete. One of ['minimal', 'resampling', 'full']."""
longitudinal = False
"""Run FreeSurfer ``recon-all`` with the ``-logitudinal`` flag."""
medial_surface_nan = None
Expand Down
272 changes: 170 additions & 102 deletions fmriprep/workflows/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import warnings
from copy import deepcopy

import bids
from nipype.interfaces import utility as niu
from nipype.pipeline import engine as pe
from niworkflows.utils.connections import listify
Expand Down Expand Up @@ -196,112 +197,28 @@
])
# fmt:on

# Overwrite ``out_path_base`` of smriprep's DataSinks
for node in workflow.list_node_names():
if node.split('.')[-1].startswith('ds_'):
workflow.get_node(node).interface.out_path_base = ""

if config.workflow.anat_only:
return workflow

from sdcflows import fieldmaps as fm

fmap_estimators = []
estimator_map = {}

if any(
(
"fieldmaps" not in config.workflow.ignore,
config.workflow.use_syn_sdc,
config.workflow.force_syn,
)
):
from sdcflows.utils.wrangler import find_estimators

# SDC Step 1: Run basic heuristics to identify available data for fieldmap estimation
# For now, no fmapless
filters = None
if config.execution.bids_filters is not None:
filters = config.execution.bids_filters.get("fmap")

# In the case where fieldmaps are ignored and `--use-syn-sdc` is requested,
# SDCFlows `find_estimators` still receives a full layout (which includes the fmap modality)
# and will not calculate fmapless schemes.
# Similarly, if fieldmaps are ignored and `--force-syn` is requested,
# `fmapless` should be set to True to ensure BOLD targets are found to be corrected.
fmapless = bool(config.workflow.use_syn_sdc) or (
"fieldmaps" in config.workflow.ignore and config.workflow.force_syn
)
force_fmapless = config.workflow.force_syn or (
"fieldmaps" in config.workflow.ignore and config.workflow.use_syn_sdc
)
return clean_datasinks(workflow)

Check warning on line 201 in fmriprep/workflows/base.py

View check run for this annotation

Codecov / codecov/patch

fmriprep/workflows/base.py#L201

Added line #L201 was not covered by tests

fmap_estimators, estimator_map = map_fieldmap_estimation(

Check warning on line 203 in fmriprep/workflows/base.py

View check run for this annotation

Codecov / codecov/patch

fmriprep/workflows/base.py#L203

Added line #L203 was not covered by tests
layout=config.execution.layout,
subject_id=subject_id,
bold_data=subject_data['bold'],
ignore_fieldmaps="fieldmaps" in config.workflow.ignore,
use_syn=config.workflow.use_syn_sdc,
force_syn=config.workflow.force_syn,
filters=config.execution.get().get('bids_filters', {}).get('fmap'),
)

fmap_estimators = find_estimators(
layout=config.execution.layout,
subject=subject_id,
fmapless=fmapless,
force_fmapless=force_fmapless,
bids_filters=filters,
if fmap_estimators:
config.loggers.workflow.info(

Check warning on line 214 in fmriprep/workflows/base.py

View check run for this annotation

Codecov / codecov/patch

fmriprep/workflows/base.py#L213-L214

Added lines #L213 - L214 were not covered by tests
"B0 field inhomogeneity map will be estimated with the following "
f"{len(fmap_estimators)} estimator(s): "
f"{[e.method for e in fmap_estimators]}."
)

if config.workflow.use_syn_sdc and not fmap_estimators:
message = (
"Fieldmap-less (SyN) estimation was requested, but PhaseEncodingDirection "
"information appears to be absent."
)
config.loggers.workflow.error(message)
if config.workflow.use_syn_sdc == "error":
raise ValueError(message)

if "fieldmaps" in config.workflow.ignore and any(
f.method == fm.EstimatorType.ANAT for f in fmap_estimators
):
config.loggers.workflow.info(
'Option "--ignore fieldmaps" was set, but either "--use-syn-sdc" '
'or "--force-syn" were given, so fieldmap-less estimation will be executed.'
)
fmap_estimators = [f for f in fmap_estimators if f.method == fm.EstimatorType.ANAT]

# Do not calculate fieldmaps that we will not use
if fmap_estimators:
all_ids = {fmap.bids_id for fmap in fmap_estimators}
bold_files = (listify(bold_file)[0] for bold_file in subject_data['bold'])

all_estimators = {
bold_file: [
fmap_id
for fmap_id in get_estimator(config.execution.layout, bold_file)
if fmap_id in all_ids
]
for bold_file in bold_files
}

for bold_file, estimator_key in all_estimators.items():
if len(estimator_key) > 1:
config.loggers.workflow.warning(
f"Several fieldmaps <{', '.join(estimator_key)}> are "
f"'IntendedFor' <{bold_file}>, using {estimator_key[0]}"
)
estimator_key[1:] = []

# Final, 1-1 map, dropping uncorrected BOLD
estimator_map = {
bold_file: estimator_key[0]
for bold_file, estimator_key in all_estimators.items()
if estimator_key
}

fmap_estimators = [f for f in fmap_estimators if f.bids_id in estimator_map.values()]

if fmap_estimators:
config.loggers.workflow.info(
"B0 field inhomogeneity map will be estimated with "
f"the following {len(fmap_estimators)} estimator(s): "
f"{[e.method for e in fmap_estimators]}."
)

if fmap_estimators:
from niworkflows.interfaces.utility import KeySelect
from sdcflows import fieldmaps as fm

Check warning on line 221 in fmriprep/workflows/base.py

View check run for this annotation

Codecov / codecov/patch

fmriprep/workflows/base.py#L221

Added line #L221 was not covered by tests
from sdcflows.workflows.base import init_fmap_preproc_wf

fmap_wf = init_fmap_preproc_wf(
Expand Down Expand Up @@ -439,7 +356,74 @@
])
# fmt:on

return workflow
if config.workflow.level == "minimal":
return clean_datasinks(workflow)

Check warning on line 360 in fmriprep/workflows/base.py

View check run for this annotation

Codecov / codecov/patch

fmriprep/workflows/base.py#L359-L360

Added lines #L359 - L360 were not covered by tests

if config.workflow.run_reconall:
from smriprep.workflows.outputs import init_ds_surfaces_wf
from smriprep.workflows.surfaces import (

Check warning on line 364 in fmriprep/workflows/base.py

View check run for this annotation

Codecov / codecov/patch

fmriprep/workflows/base.py#L362-L364

Added lines #L362 - L364 were not covered by tests
init_anat_ribbon_wf,
init_fsLR_reg_wf,
init_gifti_surfaces_wf,
)

gifti_surfaces_wf = init_gifti_surfaces_wf(

Check warning on line 370 in fmriprep/workflows/base.py

View check run for this annotation

Codecov / codecov/patch

fmriprep/workflows/base.py#L370

Added line #L370 was not covered by tests
surfaces=["white", "pial", "midthickness"],
)
gifti_spheres_wf = init_gifti_surfaces_wf(

Check warning on line 373 in fmriprep/workflows/base.py

View check run for this annotation

Codecov / codecov/patch

fmriprep/workflows/base.py#L373

Added line #L373 was not covered by tests
surfaces=["sphere_reg"], to_scanner=False, name="gifti_spheres_wf"
)
fsLR_reg_wf = init_fsLR_reg_wf()
ds_surfaces_wf = init_ds_surfaces_wf(

Check warning on line 377 in fmriprep/workflows/base.py

View check run for this annotation

Codecov / codecov/patch

fmriprep/workflows/base.py#L376-L377

Added lines #L376 - L377 were not covered by tests
bids_root=str(config.execution.bids_dir),
output_dir=str(config.execution.output_dir),
surfaces=["white", "pial", "midthickness", "sphere_reg", "sphere_reg_fsLR"],
)
anat_ribbon_wf = init_anat_ribbon_wf()

Check warning on line 382 in fmriprep/workflows/base.py

View check run for this annotation

Codecov / codecov/patch

fmriprep/workflows/base.py#L382

Added line #L382 was not covered by tests

# fmt:off
workflow.connect([

Check warning on line 385 in fmriprep/workflows/base.py

View check run for this annotation

Codecov / codecov/patch

fmriprep/workflows/base.py#L385

Added line #L385 was not covered by tests
(anat_fit_wf, gifti_surfaces_wf, [
("outputnode.subjects_dir", "inputnode.subjects_dir"),
("outputnode.subject_id", "inputnode.subject_id"),
("outputnode.fsnative2t1w_xfm", "inputnode.fsnative2t1w_xfm"),
]),
(anat_fit_wf, gifti_spheres_wf, [
("outputnode.subjects_dir", "inputnode.subjects_dir"),
("outputnode.subject_id", "inputnode.subject_id"),
# No transform for spheres, following HCP pipelines' lead
]),
(gifti_spheres_wf, fsLR_reg_wf, [
("outputnode.sphere_reg", "inputnode.sphere_reg"),
]),
(anat_fit_wf, anat_ribbon_wf, [
("outputnode.t1w_mask", "inputnode.t1w_mask"),
]),
(gifti_surfaces_wf, anat_ribbon_wf, [
("outputnode.white", "inputnode.white"),
("outputnode.pial", "inputnode.pial"),
]),
(anat_fit_wf, ds_surfaces_wf, [
("outputnode.t1w_valid_list", "inputnode.source_files"),
]),
(gifti_surfaces_wf, ds_surfaces_wf, [
("outputnode.white", "inputnode.white"),
("outputnode.pial", "inputnode.pial"),
("outputnode.midthickness", "inputnode.midthickness"),
]),
(gifti_spheres_wf, ds_surfaces_wf, [
("outputnode.sphere_reg", "inputnode.sphere_reg"),
]),
(fsLR_reg_wf, ds_surfaces_wf, [
("outputnode.sphere_reg_fsLR", "inputnode.sphere_reg_fsLR"),
]),
])
# fmt:on

if config.workflow.level == "resampling":
return clean_datasinks(workflow)

Check warning on line 424 in fmriprep/workflows/base.py

View check run for this annotation

Codecov / codecov/patch

fmriprep/workflows/base.py#L423-L424

Added lines #L423 - L424 were not covered by tests

return clean_datasinks(workflow)

Check warning on line 426 in fmriprep/workflows/base.py

View check run for this annotation

Codecov / codecov/patch

fmriprep/workflows/base.py#L426

Added line #L426 was not covered by tests


def init_single_subject_wf(subject_id: str):
Expand Down Expand Up @@ -921,5 +905,89 @@
return workflow


def map_fieldmap_estimation(
layout: bids.BIDSLayout,
subject_id: str,
bold_data: list,
ignore_fieldmaps: bool,
use_syn: bool | str,
force_syn: bool,
filters: dict | None,
) -> tuple[list, dict]:
if not any((not ignore_fieldmaps, use_syn, force_syn)):
return [], {}

Check warning on line 918 in fmriprep/workflows/base.py

View check run for this annotation

Codecov / codecov/patch

fmriprep/workflows/base.py#L917-L918

Added lines #L917 - L918 were not covered by tests

from sdcflows import fieldmaps as fm
from sdcflows.utils.wrangler import find_estimators

Check warning on line 921 in fmriprep/workflows/base.py

View check run for this annotation

Codecov / codecov/patch

fmriprep/workflows/base.py#L920-L921

Added lines #L920 - L921 were not covered by tests

# In the case where fieldmaps are ignored and `--use-syn-sdc` is requested,
# SDCFlows `find_estimators` still receives a full layout (which includes the fmap modality)
# and will not calculate fmapless schemes.
# Similarly, if fieldmaps are ignored and `--force-syn` is requested,
# `fmapless` should be set to True to ensure BOLD targets are found to be corrected.
fmap_estimators = find_estimators(

Check warning on line 928 in fmriprep/workflows/base.py

View check run for this annotation

Codecov / codecov/patch

fmriprep/workflows/base.py#L928

Added line #L928 was not covered by tests
layout=layout,
subject=subject_id,
fmapless=bool(use_syn) or ignore_fieldmaps and force_syn,
force_fmapless=force_syn or ignore_fieldmaps and use_syn,
bids_filters=filters,
)

if not fmap_estimators:
if use_syn:
message = (

Check warning on line 938 in fmriprep/workflows/base.py

View check run for this annotation

Codecov / codecov/patch

fmriprep/workflows/base.py#L936-L938

Added lines #L936 - L938 were not covered by tests
"Fieldmap-less (SyN) estimation was requested, but PhaseEncodingDirection "
"information appears to be absent."
)
config.loggers.workflow.error(message)
if use_syn == "error":
raise ValueError(message)
return [], {}

Check warning on line 945 in fmriprep/workflows/base.py

View check run for this annotation

Codecov / codecov/patch

fmriprep/workflows/base.py#L942-L945

Added lines #L942 - L945 were not covered by tests

if ignore_fieldmaps and any(f.method == fm.EstimatorType.ANAT for f in fmap_estimators):
config.loggers.workflow.info(

Check warning on line 948 in fmriprep/workflows/base.py

View check run for this annotation

Codecov / codecov/patch

fmriprep/workflows/base.py#L947-L948

Added lines #L947 - L948 were not covered by tests
'Option "--ignore fieldmaps" was set, but either "--use-syn-sdc" '
'or "--force-syn" were given, so fieldmap-less estimation will be executed.'
)
fmap_estimators = [f for f in fmap_estimators if f.method == fm.EstimatorType.ANAT]

Check warning on line 952 in fmriprep/workflows/base.py

View check run for this annotation

Codecov / codecov/patch

fmriprep/workflows/base.py#L952

Added line #L952 was not covered by tests

# Pare down estimators to those that are actually used
# If fmap_estimators == [], all loops/comprehensions terminate immediately
all_ids = {fmap.bids_id for fmap in fmap_estimators}
bold_files = (listify(bold_file)[0] for bold_file in bold_data)

Check warning on line 957 in fmriprep/workflows/base.py

View check run for this annotation

Codecov / codecov/patch

fmriprep/workflows/base.py#L956-L957

Added lines #L956 - L957 were not covered by tests

all_estimators = {

Check warning on line 959 in fmriprep/workflows/base.py

View check run for this annotation

Codecov / codecov/patch

fmriprep/workflows/base.py#L959

Added line #L959 was not covered by tests
bold_file: [fmap_id for fmap_id in get_estimator(layout, bold_file) if fmap_id in all_ids]
for bold_file in bold_files
}

for bold_file, estimator_key in all_estimators.items():
if len(estimator_key) > 1:
config.loggers.workflow.warning(

Check warning on line 966 in fmriprep/workflows/base.py

View check run for this annotation

Codecov / codecov/patch

fmriprep/workflows/base.py#L964-L966

Added lines #L964 - L966 were not covered by tests
f"Several fieldmaps <{', '.join(estimator_key)}> are "
f"'IntendedFor' <{bold_file}>, using {estimator_key[0]}"
)
estimator_key[1:] = []

Check warning on line 970 in fmriprep/workflows/base.py

View check run for this annotation

Codecov / codecov/patch

fmriprep/workflows/base.py#L970

Added line #L970 was not covered by tests

# Final, 1-1 map, dropping uncorrected BOLD
estimator_map = {

Check warning on line 973 in fmriprep/workflows/base.py

View check run for this annotation

Codecov / codecov/patch

fmriprep/workflows/base.py#L973

Added line #L973 was not covered by tests
bold_file: estimator_key[0]
for bold_file, estimator_key in all_estimators.items()
if estimator_key
}

fmap_estimators = [f for f in fmap_estimators if f.bids_id in estimator_map.values()]

Check warning on line 979 in fmriprep/workflows/base.py

View check run for this annotation

Codecov / codecov/patch

fmriprep/workflows/base.py#L979

Added line #L979 was not covered by tests

return fmap_estimators, estimator_map

Check warning on line 981 in fmriprep/workflows/base.py

View check run for this annotation

Codecov / codecov/patch

fmriprep/workflows/base.py#L981

Added line #L981 was not covered by tests


def _prefix(subid):
return subid if subid.startswith('sub-') else f'sub-{subid}'


def clean_datasinks(workflow: pe.Workflow) -> pe.Workflow:
# Overwrite ``out_path_base`` of smriprep's DataSinks
for node in workflow.list_node_names():
if node.split('.')[-1].startswith('ds_'):
workflow.get_node(node).interface.out_path_base = ""
return workflow

Check warning on line 993 in fmriprep/workflows/base.py

View check run for this annotation

Codecov / codecov/patch

fmriprep/workflows/base.py#L990-L993

Added lines #L990 - L993 were not covered by tests
4 changes: 1 addition & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,11 @@ classifiers = [
"Intended Audience :: Science/Research",
"Topic :: Scientific/Engineering :: Image Recognition",
"License :: OSI Approved :: Apache Software License",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
]
license = {file = "LICENSE"}
requires-python = ">=3.9"
requires-python = ">=3.10"
dependencies = [
"looseversion",
"nibabel >= 4.0.1",
Expand Down
Loading