Skip to content

Commit

Permalink
Merge remote-tracking branch 'upstream/master' into enh/sep_anat_func…
Browse files Browse the repository at this point in the history
…_report
  • Loading branch information
celprov committed Jan 30, 2024
2 parents fb6e8c8 + 4d21c37 commit 4182c9e
Show file tree
Hide file tree
Showing 12 changed files with 224 additions and 120 deletions.
21 changes: 0 additions & 21 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -662,27 +662,6 @@ jobs:
--output-spaces MNI152NLin2009cAsym:res-2 anat func \
--mem-mb 14336 --nthreads 4 -vv --debug compcor
- run: *check_outputs
- run:
name: Generate report with one artificial error
command: |
set -x
sudo mv /tmp/${DATASET}/fmriprep/sub-100185.html \
/tmp/${DATASET}/fmriprep/sub-100185_noerror.html
UUID=$(grep uuid /tmp/${DATASET}/work/*/config.toml | cut -d\" -f 2 | tail -n 1)
mkdir -p /tmp/${DATASET}/fmriprep/sub-100185/log/$UUID/
cp /tmp/src/fmriprep/fmriprep/data/tests/crash_files/*.txt \
/tmp/${DATASET}/fmriprep/sub-100185/log/$UUID/
set +e
fmriprep-docker -i nipreps/fmriprep:latest \
-e FMRIPREP_DEV 1 --user $(id -u):$(id -g) \
--config $PWD/nipype.cfg -w /tmp/${DATASET}/work \
/tmp/data/${DATASET} /tmp/${DATASET}/fmriprep participant \
--fs-no-reconall --sloppy --write-graph \
--output-spaces MNI152NLin2009cAsym:res-2 anat func \
--reports-only --config-file /tmp/${DATASET}/work/${UUID}/config.toml -vv
RET=$?
set -e
[[ "$RET" -eq "1" ]]
- run:
name: Clean working directory
when: on_success
Expand Down
76 changes: 59 additions & 17 deletions fmriprep/cli/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def _build_parser(**kwargs):
``kwargs`` are passed to ``argparse.ArgumentParser`` (mainly useful for debugging).
"""
from argparse import ArgumentDefaultsHelpFormatter, ArgumentParser
from argparse import Action, ArgumentDefaultsHelpFormatter, ArgumentParser
from functools import partial
from pathlib import Path

Expand All @@ -40,6 +40,27 @@ def _build_parser(**kwargs):

from .version import check_latest, is_flagged

deprecations = {
# parser attribute name: (replacement flag, version slated to be removed in)
'use_aroma': (None, '24.0.0'),
'aroma_melodic_dim': (None, '24.0.0'),
'aroma_err_on_warn': (None, '24.0.0'),
'bold2t1w_init': ('--bold2anat-init', '24.2.0'),
'bold2t1w_dof': ('--bold2anat-dof', '24.2.0'),
}

class DeprecatedAction(Action):
def __call__(self, parser, namespace, values, option_string=None):
new_opt, rem_vers = deprecations.get(self.dest, (None, None))
msg = (
f"{self.option_strings} has been deprecated and will be removed in "
f"{rem_vers or 'a later version'}."
)
if new_opt:
msg += f" Please use `{new_opt}` instead."
print(msg, file=sys.stderr)
delattr(namespace, self.dest)

def _path_exists(path, parser):
"""Ensure a given path exists."""
if path is None or not Path(path).exists():
Expand Down Expand Up @@ -69,13 +90,24 @@ def _to_gb(value):
def _drop_sub(value):
return value[4:] if value.startswith("sub-") else value

def _filter_pybids_none_any(dct):
def _process_value(value):
import bids

return {
k: bids.layout.Query.NONE if v is None else (bids.layout.Query.ANY if v == "*" else v)
for k, v in dct.items()
}
if value is None:
return bids.layout.Query.NONE
elif value == "*":
return bids.layout.Query.ANY
else:
return value

def _filter_pybids_none_any(dct):
d = {}
for k, v in dct.items():
if isinstance(v, list):
d[k] = [_process_value(val) for val in v]
else:
d[k] = _process_value(v)
return d

def _bids_filter(value, parser):
from json import JSONDecodeError, loads
Expand Down Expand Up @@ -313,19 +345,32 @@ def _slice_time_ref(value, parser):
)
g_conf.add_argument(
"--bold2t1w-init",
action="store",
default="register",
action=DeprecatedAction,
choices=["register", "header"],
help='Either "register" (the default) to initialize volumes at center or "header"'
" to use the header information when coregistering BOLD to T1w images.",
help="Deprecated - use `--bold2anat-init` instead.",
)
g_conf.add_argument(
"--bold2t1w-dof",
action=DeprecatedAction,
choices=[6, 9, 12],
type=int,
help="Deprecated - use `--bold2anat-dof` instead.",
)
g_conf.add_argument(
"--bold2anat-init",
choices=["auto", "t1w", "t2w", "header"],
default="auto",
help="Method of initial BOLD to anatomical coregistration. If `auto`, a T2w image is used "
"if available, otherwise the T1w image. `t1w` forces use of the T1w, `t2w` forces use of "
"the T2w, and `header` uses the BOLD header information without an initial registration.",
)
g_conf.add_argument(
"--bold2anat-dof",
action="store",
default=6,
choices=[6, 9, 12],
type=int,
help="Degrees of freedom when registering BOLD to T1w images. "
help="Degrees of freedom when registering BOLD to anatomical images. "
"6 degrees (rotation and translation) are used by default.",
)
g_conf.add_argument(
Expand Down Expand Up @@ -453,23 +498,20 @@ def _slice_time_ref(value, parser):
g_aroma = parser.add_argument_group("[DEPRECATED] Options for running ICA_AROMA")
g_aroma.add_argument(
"--use-aroma",
action="store_true",
default=False,
action=DeprecatedAction,
help="Deprecated. Will raise an error in 24.0.",
)
g_aroma.add_argument(
"--aroma-melodic-dimensionality",
dest="aroma_melodic_dim",
action="store",
default=0,
action=DeprecatedAction,
type=int,
help="Deprecated. Will raise an error in 24.0.",
)
g_aroma.add_argument(
"--error-on-aroma-warnings",
action="store_true",
action=DeprecatedAction,
dest="aroma_err_on_warn",
default=False,
help="Deprecated. Will raise an error in 24.0.",
)

Expand Down
15 changes: 6 additions & 9 deletions fmriprep/cli/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,17 +228,14 @@ def main():
write_bidsignore(config.execution.fmriprep_dir)

if failed_reports:
config.loggers.cli.error(
"Report generation was not successful for the following participants : %s.",
", ".join(failed_reports),
msg = (
'Report generation was not successful for the following participants '
f': {", ".join(failed_reports)}.'
)
config.loggers.cli.error(msg)
if sentry_sdk is not None:
sentry_sdk.capture_message(msg, level="error")

if sentry_sdk is not None and failed_reports:
sentry_sdk.capture_message(
"Report generation was not successful for the following participants : %s.",
", ".join(failed_reports),
level="error",
)
sys.exit(int((errno + len(failed_reports)) > 0))


Expand Down
28 changes: 19 additions & 9 deletions fmriprep/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -495,12 +495,21 @@ def init(cls):
if cls.bids_filters:
from bids.layout import Query

def _process_value(value):
"""Convert string with "Query" in it to Query object."""
if isinstance(value, list):
return [_process_value(val) for val in value]
else:
return (
getattr(Query, value[7:-4])
if not isinstance(value, Query) and "Query" in value
else value
)

# unserialize pybids Query enum values
for acq, filters in cls.bids_filters.items():
cls.bids_filters[acq] = {
k: getattr(Query, v[7:-4]) if not isinstance(v, Query) and "Query" in v else v
for k, v in filters.items()
}
for k, v in filters.items():
cls.bids_filters[acq][k] = _process_value(v)

if "all" in cls.debug:
cls.debug = list(DEBUG_MODES)
Expand All @@ -527,11 +536,12 @@ class workflow(_Config):
aroma_melodic_dim = None
"""Number of ICA components to be estimated by MELODIC
(positive = exact, negative = maximum)."""
bold2t1w_dof = None
"""Degrees of freedom of the BOLD-to-T1w registration steps."""
bold2t1w_init = "register"
"""Whether to use standard coregistration ('register') or to initialize coregistration from the
BOLD image-header ('header')."""
bold2anat_dof = None
"""Degrees of freedom of the BOLD-to-anatomical registration steps."""
bold2anat_init = "auto"
"""Method of initial BOLD to anatomical coregistration. If `auto`, a T2w image is used
if available, otherwise the T1w image. `t1w` forces use of the T1w, `t2w` forces use of
the T2w, and `header` uses the BOLD header information without an initial registration."""
cifti_output = None
"""Generate HCP Grayordinates, accepts either ``'91k'`` (default) or ``'170k'``."""
dummy_scans = None
Expand Down
2 changes: 1 addition & 1 deletion fmriprep/data/tests/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ write_graph = false
anat_only = false
aroma_err_on_warn = false
aroma_melodic_dim = -200
bold2t1w_dof = 6
bold2anat_dof = 6
fmap_bspline = false
force_syn = false
hires = true
Expand Down
38 changes: 38 additions & 0 deletions fmriprep/interfaces/patches.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@
from random import randint
from time import sleep

import nipype.interfaces.freesurfer as fs
import nipype.interfaces.io as nio
from nipype.algorithms import confounds as nac
from nipype.interfaces.base import File, traits
from numpy.linalg.linalg import LinAlgError


Expand Down Expand Up @@ -77,3 +80,38 @@ def _run_interface(self, runtime):
sleep(randint(start + 4, start + 10))

return runtime


class _MRICoregInputSpec(fs.registration.MRICoregInputSpec):
reference_file = File(
argstr="--ref %s",
desc="reference (target) file",
copyfile=False,
)
subject_id = traits.Str(
argstr="--s %s",
position=1,
requires=["subjects_dir"],
desc="freesurfer subject ID (implies ``reference_mask == "
"aparc+aseg.mgz`` unless otherwise specified)",
)


class MRICoreg(fs.MRICoreg):
"""
Patched that allows setting both a reference file and the subjects dir.
"""

input_spec = _MRICoregInputSpec


class _FSSourceOutputSpec(nio.FSSourceOutputSpec):
T2 = File(desc="Intensity normalized whole-head volume", loc="mri")


class FreeSurferSource(nio.FreeSurferSource):
"""
Patch to allow grabbing the T2 volume, if available
"""

output_spec = _FSSourceOutputSpec
5 changes: 3 additions & 2 deletions fmriprep/interfaces/reports.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,11 +213,12 @@ class FunctionalSummaryInputSpec(TraitedSpec):
6, 9, 12, desc='Registration degrees of freedom', mandatory=True
)
registration_init = traits.Enum(
'register',
't1w',
't2w',
'header',
mandatory=True,
desc='Whether to initialize registration with the "header"'
' or by centering the volumes ("register")',
' or by centering the volumes ("t1w" or "t2w")',
)
tr = traits.Float(desc='Repetition time', mandatory=True)
dummy_scans = traits.Either(traits.Int(), None, desc='number of dummy scans specified by user')
Expand Down
9 changes: 9 additions & 0 deletions fmriprep/workflows/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -637,6 +637,15 @@ def init_single_subject_wf(subject_id: str):
num_bold=len(bold_runs)
)

# Before initializing BOLD workflow, select/verify anatomical target for coregistration
if config.workflow.bold2anat_init in ('auto', 't2w'):
has_t2w = subject_data['t2w'] or 't2w_preproc' in anatomical_cache
if config.workflow.bold2anat_init == 't2w' and not has_t2w:
raise OSError(
"A T2w image is expected for BOLD-to-anatomical coregistration and was not found"
)
config.workflow.bold2anat_init = 't2w' if has_t2w else 't1w'

for bold_series in bold_runs:
bold_file = bold_series[0]
fieldmap_id = estimator_map.get(bold_file)
Expand Down
11 changes: 6 additions & 5 deletions fmriprep/workflows/bold/fit.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ def init_bold_fit_wf(
from fmriprep.utils.misc import estimate_bold_mem_usage

layout = config.execution.layout
bids_filters = config.execution.get().get('bids_filters', {})

# Fitting operates on the shortest echo
# This could become more complicated in the future
Expand All @@ -211,7 +212,7 @@ def init_bold_fit_wf(
# Collect sbref files, sorted by EchoTime
sbref_files = get_sbrefs(
bold_series,
entity_overrides=config.execution.get().get('bids_filters', {}).get('sbref', {}),
entity_overrides=bids_filters.get('sbref', {}),
layout=layout,
)

Expand Down Expand Up @@ -312,8 +313,8 @@ def init_bold_fit_wf(
FunctionalSummary(
distortion_correction="None", # Can override with connection
registration=("FSL", "FreeSurfer")[config.workflow.run_reconall],
registration_dof=config.workflow.bold2t1w_dof,
registration_init=config.workflow.bold2t1w_init,
registration_dof=config.workflow.bold2anat_dof,
registration_init=config.workflow.bold2anat_init,
pe_direction=metadata.get("PhaseEncodingDirection"),
echo_idx=entities.get("echo", []),
tr=metadata["RepetitionTime"],
Expand Down Expand Up @@ -586,8 +587,8 @@ def init_bold_fit_wf(
if not boldref2anat_xform:
# calculate BOLD registration to T1w
bold_reg_wf = init_bold_reg_wf(
bold2t1w_dof=config.workflow.bold2t1w_dof,
bold2t1w_init=config.workflow.bold2t1w_init,
bold2anat_dof=config.workflow.bold2anat_dof,
bold2anat_init=config.workflow.bold2anat_init,
use_bbr=config.workflow.use_bbr,
freesurfer=config.workflow.run_reconall,
omp_nthreads=omp_nthreads,
Expand Down
Loading

0 comments on commit 4182c9e

Please sign in to comment.