diff --git a/fmriprep/cli/parser.py b/fmriprep/cli/parser.py
index d98ed7994..1db70606b 100644
--- a/fmriprep/cli/parser.py
+++ b/fmriprep/cli/parser.py
@@ -48,6 +48,9 @@ def _build_parser(**kwargs):
'aroma_err_on_warn': (None, '24.0.0'),
'bold2t1w_init': ('--bold2anat-init', '24.2.0'),
'bold2t1w_dof': ('--bold2anat-dof', '24.2.0'),
+ 'force_bbr': ('--force bbr', '26.0.0'),
+ 'force_no_bbr': ('--force no-bbr', '26.0.0'),
+ 'force_syn': ('--force syn-sdc', '26.0.0'),
}
class DeprecatedAction(Action):
@@ -338,6 +341,19 @@ def _slice_time_ref(value, parser):
help='Ignore selected aspects of the input dataset to disable corresponding '
'parts of the workflow (a space delimited list)',
)
+ g_conf.add_argument(
+ '--force',
+ required=False,
+ action='store',
+ nargs='+',
+ default=[],
+ choices=['bbr', 'no-bbr', 'syn-sdc'],
+ help='Force selected processing choices, overriding automatic selections '
+ '(a space delimited list).\n'
+ ' * [no-]bbr: Use/disable boundary-based registration for BOLD-to-T1w coregistration\n'
+ ' (No goodness-of-fit checks)\n'
+ ' * syn-sdc: Calculate SyN-SDC correction *in addition* to other fieldmaps\n',
+ )
g_conf.add_argument(
'--output-spaces',
nargs='*',
@@ -392,17 +408,13 @@ def _slice_time_ref(value, parser):
)
g_conf.add_argument(
'--force-bbr',
- action='store_true',
- dest='use_bbr',
- default=None,
- help='Always use boundary-based registration (no goodness-of-fit checks)',
+ action=DeprecatedAction,
+ help='Deprecated - use `--force bbr` instead.',
)
g_conf.add_argument(
'--force-no-bbr',
- action='store_false',
- dest='use_bbr',
- default=None,
- help='Do not use boundary-based registration (no goodness-of-fit checks)',
+ action=DeprecatedAction,
+ help='Deprecated - use `--force no-bbr` instead.',
)
g_conf.add_argument(
'--slice-time-ref',
@@ -624,10 +636,9 @@ def _slice_time_ref(value, parser):
)
g_syn.add_argument(
'--force-syn',
- action='store_true',
+ action=DeprecatedAction,
default=False,
- help='EXPERIMENTAL/TEMPORARY: Use SyN correction in addition to '
- 'fieldmap correction, if available',
+ help='Deprecated - use `--force syn-sdc` instead.',
)
# FreeSurfer options
@@ -789,6 +800,14 @@ 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), init=['nipype'])
+ # Consistency checks
+ if 'bbr' in config.workflow.force and 'no-bbr' in config.workflow.force:
+ msg = (
+ 'Cannot force and disable boundary-based registration at the same time. '
+ 'Remove `bbr` or `no-bbr` from the `--force` options.'
+ )
+ raise ValueError(msg)
+
if not config.execution.notrack:
import importlib.util
diff --git a/fmriprep/config.py b/fmriprep/config.py
index 3ecd4de67..29221dc56 100644
--- a/fmriprep/config.py
+++ b/fmriprep/config.py
@@ -581,6 +581,8 @@ class workflow(_Config):
"""Adjust pipeline to reuse base template of existing longitudinal freesurfer"""
ignore = None
"""Ignore particular steps for *fMRIPrep*."""
+ force = None
+ """Force particular steps for *fMRIPrep*."""
level = None
"""Level of preprocessing to complete. One of ['minimal', 'resampling', 'full']."""
longitudinal = False
diff --git a/fmriprep/data/reports-spec-func.yml b/fmriprep/data/reports-spec-func.yml
index 04acea2c4..41f6c5eb9 100644
--- a/fmriprep/data/reports-spec-func.yml
+++ b/fmriprep/data/reports-spec-func.yml
@@ -78,7 +78,7 @@ sections:
static: false
subtitle: Susceptibility distortion correction
- bids: {datatype: figures, desc: forcedsyn, suffix: bold}
- caption: The dataset contained some fieldmap information, but the argument --force-syn
+ caption: The dataset contained some fieldmap information, but the argument --force syn-sdc
was used. The higher-priority SDC method was used. Here, we show the results
of performing SyN-based SDC on the EPI for comparison.
static: false
diff --git a/fmriprep/data/reports-spec.yml b/fmriprep/data/reports-spec.yml
index 4ec02bda6..eaffe0b95 100644
--- a/fmriprep/data/reports-spec.yml
+++ b/fmriprep/data/reports-spec.yml
@@ -104,7 +104,7 @@ sections:
static: false
subtitle: Susceptibility distortion correction
- bids: {datatype: figures, desc: forcedsyn, suffix: bold}
- caption: The dataset contained some fieldmap information, but the argument --force-syn
+ caption: The dataset contained some fieldmap information, but the argument --force syn-sdc
was used. The higher-priority SDC method was used. Here, we show the results
of performing SyN-based SDC on the EPI for comparison.
static: false
diff --git a/fmriprep/data/tests/config.toml b/fmriprep/data/tests/config.toml
index b1b7e31b6..3af10cc50 100644
--- a/fmriprep/data/tests/config.toml
+++ b/fmriprep/data/tests/config.toml
@@ -34,6 +34,7 @@ aroma_err_on_warn = false
aroma_melodic_dim = -200
bold2anat_dof = 6
fmap_bspline = false
+force = []
force_syn = false
hires = true
ignore = []
diff --git a/fmriprep/workflows/base.py b/fmriprep/workflows/base.py
index 5cf07de64..9d1d81aec 100644
--- a/fmriprep/workflows/base.py
+++ b/fmriprep/workflows/base.py
@@ -557,7 +557,7 @@ def init_single_subject_wf(subject_id: str):
bold_data=bold_runs,
ignore_fieldmaps='fieldmaps' in config.workflow.ignore,
use_syn=config.workflow.use_syn_sdc,
- force_syn=config.workflow.force_syn,
+ force_syn='syn-sdc' in config.workflow.force,
filters=config.execution.get().get('bids_filters', {}).get('fmap'),
)
@@ -846,7 +846,7 @@ def map_fieldmap_estimation(
# 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,
+ # Similarly, if fieldmaps are ignored and `--force syn-sdc` is requested,
# `fmapless` should be set to True to ensure BOLD targets are found to be corrected.
fmap_estimators = find_estimators(
layout=layout,
@@ -870,7 +870,7 @@ def map_fieldmap_estimation(
if ignore_fieldmaps 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.'
+ 'or "--force syn-sdc" were given, so fieldmap-less estimation will be executed.'
)
fmap_estimators = [f for f in fmap_estimators if f.method == fm.EstimatorType.ANAT]
diff --git a/fmriprep/workflows/bold/fit.py b/fmriprep/workflows/bold/fit.py
index 0445e4de3..1e315aac0 100644
--- a/fmriprep/workflows/bold/fit.py
+++ b/fmriprep/workflows/bold/fit.py
@@ -613,11 +613,18 @@ def init_bold_fit_wf(
]) # fmt:skip
if not boldref2anat_xform:
+ use_bbr = (
+ True
+ if 'bbr' in config.workflow.force
+ else False
+ if 'no-bbr' in config.workflow.force
+ else None
+ )
# calculate BOLD registration to T1w
bold_reg_wf = init_bold_reg_wf(
bold2anat_dof=config.workflow.bold2anat_dof,
bold2anat_init=config.workflow.bold2anat_init,
- use_bbr=config.workflow.use_bbr,
+ use_bbr=use_bbr,
freesurfer=config.workflow.run_reconall,
omp_nthreads=omp_nthreads,
mem_gb=mem_gb['resampled'],
diff --git a/fmriprep/workflows/tests/test_base.py b/fmriprep/workflows/tests/test_base.py
index 1af32eb18..ebbb60c49 100644
--- a/fmriprep/workflows/tests/test_base.py
+++ b/fmriprep/workflows/tests/test_base.py
@@ -106,7 +106,6 @@ def bids_root(tmp_path_factory):
def _make_params(
bold2anat_init: str = 'auto',
- use_bbr: bool | None = None,
dummy_scans: int | None = None,
me_output_echos: bool = False,
medial_surface_nan: bool = False,
@@ -115,18 +114,19 @@ def _make_params(
run_msmsulc: bool = True,
skull_strip_t1w: str = 'auto',
use_syn_sdc: str | bool = False,
- force_syn: bool = False,
freesurfer: bool = True,
ignore: list[str] = None,
+ force: list[str] = None,
bids_filters: dict = None,
):
if ignore is None:
ignore = []
+ if force is None:
+ force = []
if bids_filters is None:
bids_filters = {}
return (
bold2anat_init,
- use_bbr,
dummy_scans,
me_output_echos,
medial_surface_nan,
@@ -135,9 +135,9 @@ def _make_params(
run_msmsulc,
skull_strip_t1w,
use_syn_sdc,
- force_syn,
freesurfer,
ignore,
+ force,
bids_filters,
)
@@ -147,7 +147,6 @@ def _make_params(
@pytest.mark.parametrize(
(
'bold2anat_init',
- 'use_bbr',
'dummy_scans',
'me_output_echos',
'medial_surface_nan',
@@ -156,9 +155,9 @@ def _make_params(
'run_msmsulc',
'skull_strip_t1w',
'use_syn_sdc',
- 'force_syn',
'freesurfer',
'ignore',
+ 'force',
'bids_filters',
),
[
@@ -166,11 +165,11 @@ def _make_params(
_make_params(bold2anat_init='t1w'),
_make_params(bold2anat_init='t2w'),
_make_params(bold2anat_init='header'),
- _make_params(use_bbr=True),
- _make_params(use_bbr=False),
- _make_params(bold2anat_init='header', use_bbr=True),
+ _make_params(force=['bbr']),
+ _make_params(force=['no-bbr']),
+ _make_params(bold2anat_init='header', force=['bbr']),
# Currently disabled
- # _make_params(bold2anat_init="header", use_bbr=False),
+ # _make_params(bold2anat_init="header", force=['no-bbr']),
_make_params(dummy_scans=2),
_make_params(me_output_echos=True),
_make_params(medial_surface_nan=True),
@@ -180,14 +179,14 @@ def _make_params(
_make_params(cifti_output='91k', run_msmsulc=False),
_make_params(skull_strip_t1w='force'),
_make_params(skull_strip_t1w='skip'),
- _make_params(use_syn_sdc='warn', force_syn=True, ignore=['fieldmaps']),
+ _make_params(use_syn_sdc='warn', ignore=['fieldmaps'], force=['syn-sdc']),
_make_params(freesurfer=False),
- _make_params(freesurfer=False, use_bbr=True),
- _make_params(freesurfer=False, use_bbr=False),
+ _make_params(freesurfer=False, force=['bbr']),
+ _make_params(freesurfer=False, force=['no-bbr']),
# Currently unsupported:
# _make_params(freesurfer=False, bold2anat_init="header"),
- # _make_params(freesurfer=False, bold2anat_init="header", use_bbr=True),
- # _make_params(freesurfer=False, bold2anat_init="header", use_bbr=False),
+ # _make_params(freesurfer=False, bold2anat_init="header", force=['bbr']),
+ # _make_params(freesurfer=False, bold2anat_init="header", force=['no-bbr']),
# Regression test for gh-3154:
_make_params(bids_filters={'sbref': {'suffix': 'sbref'}}),
],
@@ -198,7 +197,6 @@ def test_init_fmriprep_wf(
level: str,
anat_only: bool,
bold2anat_init: str,
- use_bbr: bool | None,
dummy_scans: int | None,
me_output_echos: bool,
medial_surface_nan: bool,
@@ -207,16 +205,15 @@ def test_init_fmriprep_wf(
run_msmsulc: bool,
skull_strip_t1w: str,
use_syn_sdc: str | bool,
- force_syn: bool,
freesurfer: bool,
ignore: list[str],
+ force: list[str],
bids_filters: dict,
):
with mock_config(bids_dir=bids_root):
config.workflow.level = level
config.workflow.anat_only = anat_only
config.workflow.bold2anat_init = bold2anat_init
- config.workflow.use_bbr = use_bbr
config.workflow.dummy_scans = dummy_scans
config.execution.me_output_echos = me_output_echos
config.workflow.medial_surface_nan = medial_surface_nan
@@ -226,6 +223,7 @@ def test_init_fmriprep_wf(
config.workflow.cifti_output = cifti_output
config.workflow.run_reconall = freesurfer
config.workflow.ignore = ignore
+ config.workflow.force = force
with patch.dict('fmriprep.config.execution.bids_filters', bids_filters):
wf = init_fmriprep_wf()