diff --git a/docs/quickstart.rst b/docs/quickstart.rst index e74ac842..077e632c 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -109,38 +109,38 @@ Head motion correction model **************************** Although FSL's ``eddy`` is technically model-free, it is an option for -``--hmc-model`` along with ``3dSHORE`` and ``none``. Choosing ``eddy`` (the -default) runs FSL's ``eddy`` for head motion correction and eddy current -correction. This will work for single-shell and multi-shell sampling schemes. -The ``3dSHORE`` (aka "SHORELine") option works for multi-shell, Cartesian -grid sampling (DSI) and random q-space sampling (CS-DSI). +``--hmc-model`` along with ``3dSHORE``, ``tensor``, ``tortoise`` and ``none``. +Choosing ``eddy`` (the default) runs FSL's ``eddy`` for head motion correction +and eddy current correction. This will work for single-shell and multi-shell +sampling schemes. The ``3dSHORE`` (aka "SHORELine") option works for +multi-shell, Cartesian grid sampling (DSI) and random q-space sampling +(CS-DSI), and ``tensor`` runs the same SHORELine iterations with a tensor +model. The option ``none`` will register all the b=0 images to one another and the b>0 images will have the transform from the nearest b=0 image applied. This is not recommended. Between ``eddy`` and ``3dSHORE``, all sampling schemes can be motion corrected, though eddy-current correction for non-shelled data -requires the DIFFPREP options described below. +requires the DIFFPREP option described below. For non-shelled acquisitions such as compressed-sensing DSI (CS-DSI), FSL ``eddy`` cannot be used, and ``3dSHORE`` corrects motion but does not correct eddy currents. In these cases, TORTOISE DIFFPREP is available via -``--hmc-model``: +``--hmc-model tortoise``. -- ``diffprep_motion`` — rigid head-motion correction only. -- ``diffprep_quadratic`` — rigid motion plus 24-parameter quadratic - eddy-current correction (recommended). -- ``diffprep_cubic`` — rigid motion plus cubic eddy-current correction. +This option runs TORTOISE v4 DIFFPREP, which fits a signal model over +arbitrary q-space and therefore does not require shells. By default it +corrects rigid head motion together with 24-parameter quadratic eddy currents. +Advanced TORTOISE settings can be supplied with ``--diffprep-config``, +including ``"correction_mode"``, which selects between ``"motion"`` (rigid +head motion only), ``"quadratic"`` (the default) and ``"cubic"``. -These options run TORTOISE v4 DIFFPREP, which fits a signal model over -arbitrary q-space and therefore does not require shells. Advanced TORTOISE -settings can be supplied with ``--diffprep-config``. - -The ``diffprep_*`` backends also perform susceptibility distortion correction, +The ``tortoise`` backend also performs susceptibility distortion correction, preferring TORTOISE-native tools: - **Reverse phase-encoded** data is corrected with DRBUDDI - (``--pepolar-method DRBUDDI``; ``TOPUP`` is not supported with the DIFFPREP - backends). This covers both an ``epi`` fieldmap (a blip-up/blip-down b=0 or EPI + (``--pepolar-method DRBUDDI``; ``TOPUP`` is not supported with the tortoise + backend). This covers both an ``epi`` fieldmap (a blip-up/blip-down b=0 or EPI in ``fmap/``) and a reverse phase-encoded *DWI series* (``rpe_series``). For a reverse-PE series, DIFFPREP is run **once per phase-encoding direction** (a single run models one phase axis for the whole file), then the corrected diff --git a/qsiprep/cli/parser.py b/qsiprep/cli/parser.py index d397a968..e961ea7d 100644 --- a/qsiprep/cli/parser.py +++ b/qsiprep/cli/parser.py @@ -718,20 +718,17 @@ def _bids_filter(value, parser): '3dSHORE', 'eddy', 'tensor', - 'diffprep_motion', - 'diffprep_quadratic', - 'diffprep_cubic', + 'tortoise', ], help='model used to generate target images for hmc. If "none" the ' 'non-b0 images will be warped using the same transform as their ' 'nearest b0 image. If "3dSHORE", SHORELine will be used. if "tensor", ' - 'SHORELine iterations with a tensor model will be used. The ' - '"diffprep_*" options run TORTOISE DIFFPREP: "diffprep_motion" ' - 'corrects rigid head motion only, "diffprep_quadratic" adds ' - '24-parameter quadratic eddy-current correction (recommended for ' - 'non-shelled / CS-DSI schemes), "diffprep_cubic" adds cubic eddy ' - 'correction. DIFFPREP works on arbitrary q-space (no shells ' - 'required).', + 'SHORELine iterations with a tensor model will be used. ' + '"tortoise" uses TORTOISE DIFFPREP; ' + 'by default this performs rigid head motion correction and ' + '24-parameter quadratic eddy-current correction. ' + '"tortoise" works on arbitrary q-space (no shells required). ' + 'For fine-grained control over "tortoise" settings, use --diffprep-config.', ) g_moco.add_argument( '--eddy-config', @@ -745,8 +742,10 @@ def _bids_filter(value, parser): '--diffprep-config', action='store', help='path to a json file with settings for the call to TORTOISE ' - 'DIFFPREP (used only when --hmc-model is one of the diffprep_* ' - 'options). If no json is specified, a default one will be used. The ' + 'DIFFPREP (used only when --hmc-model is tortoise). This is also where ' + 'the correction mode is chosen: "correction_mode" may be "motion" ' + '(rigid only), "quadratic" (the default) or "cubic". ' + 'If no json is specified, a default one will be used. The ' 'current default can be found here: ' 'https://github.com/PennLINC/qsiprep/blob/main/qsiprep/data/diffprep_params.json', ) diff --git a/qsiprep/data/diffprep_params.json b/qsiprep/data/diffprep_params.json index 9340cdd6..3e7f9afe 100644 --- a/qsiprep/data/diffprep_params.json +++ b/qsiprep/data/diffprep_params.json @@ -1,4 +1,5 @@ { + "correction_mode": "quadratic", "b0_id": -1, "is_human_brain": true, "rot_eddy_center": "isocenter", diff --git a/qsiprep/interfaces/tortoise.py b/qsiprep/interfaces/tortoise.py index 9cbfd033..289dbaeb 100644 --- a/qsiprep/interfaces/tortoise.py +++ b/qsiprep/interfaces/tortoise.py @@ -802,21 +802,32 @@ def bmtxt_to_fsl(bmtxt_file, working_dir=None): def generate_diffprep_boilerplate(correction_mode): - """Methods boilerplate describing the DIFFPREP HMC backend.""" + """Methods boilerplate describing the DIFFPREP HMC backend. + ``correction_mode`` comes from ``--diffprep-config`` (default + ``quadratic``), so the transform model has to be named from it rather than + assumed. + """ mode_desc = { - 'motion': 'rigid head motion only', - 'quadratic': 'rigid head motion together with quadratic eddy currents', - 'cubic': 'rigid head motion together with cubic eddy currents', - }[correction_mode] + 'motion': ('rigid head motion only', 'rigid-body transform'), + 'quadratic': ( + 'rigid head motion together with quadratic eddy currents', + '24-parameter Okan-quadratic transform', + ), + 'cubic': ( + 'rigid head motion together with cubic eddy currents', + 'cubic transform', + ), + } + corrects, transform = mode_desc[correction_mode] return ( - f'\n\nHead motion correction was performed with DIFFPREP ' - f'[@diffprep], part of the TORTOISE [@tortoisev4] software package, ' - f'in {correction_mode} mode (correcting {mode_desc}). DIFFPREP fits a ' + '\n\nHead motion correction was performed with DIFFPREP ' + '[@diffprep], part of the TORTOISE [@tortoisev4] software package, ' + f'in {correction_mode} mode (correcting {corrects}). DIFFPREP fits a ' 'SHORE/MAPMRI signal model to the data and iteratively registers each ' - "volume to a model-predicted target using TORTOISE's 24-parameter " - 'Okan-quadratic transform. The corrected volumes and motion-rotated ' - 'bmatrix were then passed to the rest of the pipeline.\n\n' + f"volume to a model-predicted target using TORTOISE's {transform}. " + 'The corrected volumes and motion-rotated bmatrix were then passed to ' + 'the rest of the pipeline.\n\n' ) diff --git a/qsiprep/tests/test_cli.py b/qsiprep/tests/test_cli.py index d086bb83..d12de7c4 100644 --- a/qsiprep/tests/test_cli.py +++ b/qsiprep/tests/test_cli.py @@ -324,7 +324,7 @@ def test_diffprep(data_dir, output_dir, working_dir): """TORTOISE DIFFPREP head-motion/eddy correction on non-shelled data. This tests the following features: - - The TORTOISE DIFFPREP HMC backend (--hmc-model diffprep_quadratic) on a + - The TORTOISE DIFFPREP HMC backend (--hmc-model tortoise) on a compressed-sensing DSI (non-shelled) scheme, where FSL eddy cannot run - The fieldmap-less path: with no fieldmap and no T2w, DIFFPREP performs head-motion/eddy correction only and does not error out @@ -349,7 +349,7 @@ def test_diffprep(data_dir, output_dir, working_dir): f'-w={work_dir}', '--sloppy', '--b1-biascorrect-stage=none', - '--hmc-model=diffprep_quadratic', + '--hmc-model=tortoise', '--output-resolution=5', ] @@ -394,7 +394,7 @@ def test_diffprep_drbuddi(data_dir, output_dir, working_dir): '--anat-modality=none', '--denoise-method=none', '--b1-biascorrect-stage=none', - '--hmc-model=diffprep_quadratic', + '--hmc-model=tortoise', '--pepolar-method=DRBUDDI', '--output-resolution=2', ] @@ -442,7 +442,7 @@ def test_diffprep_drbuddi_rpe_series(data_dir, output_dir, working_dir): '--denoise-method=none', '--b0-motion-corr-to=first', '--b1-biascorrect-stage=none', - '--hmc-model=diffprep_quadratic', + '--hmc-model=tortoise', '--pepolar-method=DRBUDDI', '--output-resolution=5', ] @@ -501,7 +501,7 @@ def test_diffprep_csdsi_rpe_series(data_dir, output_dir, working_dir): '--denoise-method=none', '--b0-motion-corr-to=first', '--b1-biascorrect-stage=none', - '--hmc-model=diffprep_quadratic', + '--hmc-model=tortoise', '--pepolar-method=DRBUDDI', '--output-resolution=5', ] @@ -828,8 +828,8 @@ def test_forrest_gump_patch2self(data_dir, output_dir, working_dir): _run_and_generate(TEST_NAME, parameters, test_main=False) -@pytest.mark.parametrize('model', ['diffprep_motion', 'diffprep_quadratic', 'diffprep_cubic']) -def test_parser_accepts_diffprep_hmc_models(model, tmp_path): +def test_parser_accepts_tortoise(tmp_path): + """``tortoise`` is the single --hmc-model value for the DIFFPREP backend.""" from qsiprep.cli.parser import _build_parser parser = _build_parser() @@ -837,9 +837,32 @@ def test_parser_accepts_diffprep_hmc_models(model, tmp_path): bids.mkdir() out = tmp_path / 'out' opts = parser.parse_args( - [str(bids), str(out), 'participant', '--hmc-model', model, '--output-resolution', '2'] + [str(bids), str(out), 'participant', '--hmc-model', 'tortoise', '--output-resolution', '2'] ) - assert opts.hmc_model == model + assert opts.hmc_model == 'tortoise' + + +def test_parser_rejects_removed_diffprep_hmc_models(tmp_path): + """The per-mode values were replaced by "tortoise" + --diffprep-config.""" + from qsiprep.cli.parser import _build_parser + + parser = _build_parser() + bids = tmp_path / 'bids' + bids.mkdir() + out = tmp_path / 'out' + for removed in ('diffprep_motion', 'diffprep_quadratic', 'diffprep_cubic'): + with pytest.raises(SystemExit): + parser.parse_args( + [ + str(bids), + str(out), + 'participant', + '--hmc-model', + removed, + '--output-resolution', + '2', + ] + ) def test_validate_diffprep_config_missing(tmp_path): @@ -856,6 +879,29 @@ def test_validate_diffprep_config_default_is_valid(): validate_diffprep_config(str(load_data('diffprep_params.json'))) +def test_validate_diffprep_config_rejects_bad_correction_mode(tmp_path): + """A typo must fail at parse time, not deep inside workflow construction.""" + import json + + from qsiprep.utils.misc import validate_diffprep_config + + cfg = tmp_path / 'bad_mode.json' + cfg.write_text(json.dumps({'correction_mode': 'quadratik'})) + with pytest.raises(ValueError, match='correction_mode'): + validate_diffprep_config(str(cfg)) + + +def test_validate_diffprep_config_accepts_each_correction_mode(tmp_path): + import json + + from qsiprep.utils.misc import validate_diffprep_config + + for mode in ('motion', 'quadratic', 'cubic'): + cfg = tmp_path / f'{mode}.json' + cfg.write_text(json.dumps({'correction_mode': mode})) + validate_diffprep_config(str(cfg)) + + def _check_arg_specified(argname, arglist): for arg in arglist: if arg.startswith(argname): diff --git a/qsiprep/tests/test_interfaces_diffprep.py b/qsiprep/tests/test_interfaces_diffprep.py index 6b1fb7b6..ae25da3c 100644 --- a/qsiprep/tests/test_interfaces_diffprep.py +++ b/qsiprep/tests/test_interfaces_diffprep.py @@ -197,6 +197,63 @@ def test_diffprep_wf_honours_use_cuda(tmp_path): config.workflow.diffprep_config = orig +def test_diffprep_correction_mode_defaults_to_quadratic(): + """``correction_mode`` is a --diffprep-config key, defaulting to quadratic. + + The CLI exposes one ``--hmc-model tortoise`` rather than a value per mode, + so the config JSON is the only way to reach ``motion`` or ``cubic``. + """ + from qsiprep.workflows.dwi.diffprep import _load_diffprep_config + + assert _load_diffprep_config(None)['correction_mode'] == 'quadratic' + + +def test_diffprep_wf_honours_correction_mode(tmp_path): + """A correction_mode in --diffprep-config reaches the DIFFPREP node.""" + import json as _json + + config = _base_config() + cfg = tmp_path / 'cubic_cfg.json' + cfg.write_text(_json.dumps({'correction_mode': 'cubic'})) + orig = config.workflow.diffprep_config + try: + config.workflow.diffprep_config = str(cfg) + wf = _build(_scan_groups(), t2w_sdc=False, name='mode_cubic') + assert wf.get_node('diffprep').inputs.correction_mode == 'cubic' + + config.workflow.diffprep_config = None + wf_default = _build(_scan_groups(), t2w_sdc=False, name='mode_default') + assert wf_default.get_node('diffprep').inputs.correction_mode == 'quadratic' + finally: + config.workflow.diffprep_config = orig + + +def test_diffprep_boilerplate_describes_the_configured_mode(tmp_path): + """The methods section must describe the mode that actually ran. + + ``correction_mode`` is selectable through --diffprep-config, so boilerplate + that hardcodes "quadratic eddy currents" would misreport a motion-only or + cubic run. + """ + import json as _json + + config = _base_config() + cfg = tmp_path / 'boilerplate_cfg.json' + cfg.write_text(_json.dumps({'correction_mode': 'motion'})) + orig = config.workflow.diffprep_config + try: + config.workflow.diffprep_config = str(cfg) + wf = _build(_scan_groups(), t2w_sdc=False, name='boiler_motion') + assert 'rigid head motion only' in wf.__desc__ + assert 'quadratic eddy' not in wf.__desc__ + + config.workflow.diffprep_config = None + wf_quad = _build(_scan_groups(), t2w_sdc=False, name='boiler_quad') + assert 'quadratic eddy currents' in wf_quad.__desc__ + finally: + config.workflow.diffprep_config = orig + + def _stage_diffprep_outputs(tmp_path, t2wreg): """Recreate the file tree TORTOISEProcess leaves behind in a node's cwd. @@ -761,9 +818,9 @@ def test_t2w_available_for_sdc_requires_a_consuming_backend(t2w_gate_config): """t2w_unfatsat only exists when the backend has a stage that consumes it. ``additional_t2ws`` -- the only thing that makes init_anat_preproc_wf build its - T2w branch -- was gated on --pepolar-method alone. The diffprep_* T2Wreg path + T2w branch -- was gated on --pepolar-method alone. The tortoise T2Wreg path consumes the T2w without any PEPOLAR data and is not gated on that flag, so a - plain ``--hmc-model diffprep_* --ignore fieldmaps`` run requested T2w SDC while + plain ``--hmc-model tortoise --ignore fieldmaps`` run requested T2w SDC while nothing produced the image, and DIFFPREP died with ``epi_mode="T2Wreg" requires a structural_image``. """ @@ -778,13 +835,12 @@ def test_t2w_available_for_sdc_requires_a_consuming_backend(t2w_gate_config): assert _t2w_sdc_backend_enabled() is True assert _t2w_available_for_sdc(T2W_SUBJECT) is True - # The regression: diffprep_* consumes it via --epi T2Wreg regardless of + # The regression: 'tortoise' consumes it via --epi T2Wreg regardless of # --pepolar-method. t2w_gate_config.workflow.pepolar_method = 'TOPUP' - for model in ('diffprep_motion', 'diffprep_quadratic', 'diffprep_cubic'): - t2w_gate_config.workflow.hmc_model = model - assert _t2w_sdc_backend_enabled() is True, model - assert _t2w_available_for_sdc(T2W_SUBJECT) is True, model + t2w_gate_config.workflow.hmc_model = 'tortoise' + assert _t2w_sdc_backend_enabled() is True, 'tortoise' + assert _t2w_available_for_sdc(T2W_SUBJECT) is True, 'tortoise' def test_extended_pepolar_report_t2w_n4_gets_input(): @@ -805,14 +861,6 @@ def test_extended_pepolar_report_t2w_n4_gets_input(): assert wf_no_t2w.get_node('map_seg') is not None -def test_diffprep_order_helper(): - from qsiprep.workflows.dwi.base import _diffprep_order - - assert _diffprep_order('diffprep_motion') == 'motion' - assert _diffprep_order('diffprep_quadratic') == 'quadratic' - assert _diffprep_order('diffprep_cubic') == 'cubic' - - def _base_config(): from qsiprep import config @@ -843,7 +891,6 @@ def _build(scan_groups, t2w_sdc, name='dp'): scan_groups=scan_groups, source_file='/data/sub-01_dwi.nii.gz', t2w_sdc=t2w_sdc, - correction_mode='quadratic', dwi_metadata={'PhaseEncodingDirection': 'j'}, name=name, ) @@ -953,10 +1000,9 @@ def test_init_diffprep_hmc_wf_syn_without_t2w(): def test_cnr_model_label_is_bids_valid(): """The ``model`` entity names the signal model and must be alphanumeric. - ``diffprep_quadratic`` could not be parsed back -- ``_`` is the BIDS entity - separator -- and DIFFPREP emits no CNR of its own, so the diffprep backends - report the MAPMRI model the CNR is actually derived from. Every other - backend must be left exactly as it was. + DIFFPREP emits no CNR of its own, so the ``tortoise`` backend reports the + MAPMRI model the CNR is actually derived from rather than its own name. + Every other backend must be left exactly as it was. """ import re @@ -965,11 +1011,10 @@ def test_cnr_model_label_is_bids_valid(): for unchanged in ('3dSHORE', 'eddy', 'tensor', 'none'): assert _cnr_model_label(unchanged) == unchanged - for diffprep_model in ('diffprep_motion', 'diffprep_quadratic', 'diffprep_cubic'): - assert _cnr_model_label(diffprep_model) == 'MAPMRI' + assert _cnr_model_label('tortoise') == 'MAPMRI' entity = re.compile(r'^[a-zA-Z0-9]+$') - for model in ('3dSHORE', 'eddy', 'tensor', 'none', 'diffprep_quadratic'): + for model in ('3dSHORE', 'eddy', 'tensor', 'none', 'tortoise'): assert entity.match(_cnr_model_label(model)), model @@ -980,7 +1025,7 @@ def test_cnr_description_flags_in_sample_bias(): baseline = _cnr_description('3dSHORE') assert baseline == 'Contrast-to-noise ratio map for the HMC step.' - diffprep_desc = _cnr_description('diffprep_quadratic') + diffprep_desc = _cnr_description('tortoise') assert 'MAPMRI' in diffprep_desc assert 'in-sample' in diffprep_desc assert 'not quantitatively comparable' in diffprep_desc @@ -1243,7 +1288,7 @@ def test_t2wreg_is_recognised_as_sdc_for_reporting(): config = _base_config() try: - config.workflow.hmc_model = 'diffprep_quadratic' + config.workflow.hmc_model = 'tortoise' assert _doing_t2wreg(None, '/path/to/T2w.nii.gz') is True assert _doing_t2wreg('syn', '/path/to/T2w.nii.gz') is True diff --git a/qsiprep/tests/test_t2w_derivatives.py b/qsiprep/tests/test_t2w_derivatives.py index cc7b8d55..3f379c33 100644 --- a/qsiprep/tests/test_t2w_derivatives.py +++ b/qsiprep/tests/test_t2w_derivatives.py @@ -26,7 +26,7 @@ def _config(): config.workflow.anat_modality = 'T1w' config.workflow.anat_biascorrect = 'n4' config.workflow.subject_anatomical_reference = 'unbiased' - config.workflow.hmc_model = 'diffprep_quadratic' + config.workflow.hmc_model = 'tortoise' config.workflow.pepolar_method = 'DRBUDDI' config.workflow.b0_to_anat_transform = 'Rigid' return config diff --git a/qsiprep/tests/test_template_registration_settings.py b/qsiprep/tests/test_template_registration_settings.py index 72788b3a..9f436c6d 100644 --- a/qsiprep/tests/test_template_registration_settings.py +++ b/qsiprep/tests/test_template_registration_settings.py @@ -112,7 +112,7 @@ def _config(**overrides): config.workflow.subject_anatomical_reference = 'unbiased' config.workflow.b0_threshold = 100 config.workflow.hmc_transform = 'Rigid' - config.workflow.hmc_model = 'diffprep_quadratic' + config.workflow.hmc_model = 'tortoise' config.workflow.b0_motion_corr_to = 'iterative' config.workflow.b0_to_anat_transform = 'Rigid' for key, value in overrides.items(): diff --git a/qsiprep/tests/test_tsnr.py b/qsiprep/tests/test_tsnr.py index 9ec62db0..73ae8c91 100644 --- a/qsiprep/tests/test_tsnr.py +++ b/qsiprep/tests/test_tsnr.py @@ -109,7 +109,7 @@ def test_tsnr_is_wired_into_derivatives(tmp_path): from qsiprep.workflows.dwi.derivatives import init_dwi_derivatives_wf config.execution.output_dir = str(tmp_path) - config.workflow.hmc_model = 'diffprep_quadratic' + config.workflow.hmc_model = 'tortoise' config.workflow.write_local_bvecs = False wf = init_dwi_derivatives_wf('/data/sub-01/ses-1/dwi/sub-01_ses-1_dwi.nii.gz') diff --git a/qsiprep/utils/misc.py b/qsiprep/utils/misc.py index 51bcaaa3..52c57cb8 100644 --- a/qsiprep/utils/misc.py +++ b/qsiprep/utils/misc.py @@ -437,7 +437,8 @@ def validate_diffprep_config(diffprep_config): Raises ------ ValueError - If the DIFFPREP configuration file does not exist or is not valid JSON. + If the DIFFPREP configuration file does not exist, is not valid JSON, + or sets an unsupported ``correction_mode``. """ import json import os @@ -445,7 +446,18 @@ def validate_diffprep_config(diffprep_config): if not os.path.exists(diffprep_config): raise ValueError(f'DIFFPREP configuration file {diffprep_config} does not exist.') with open(diffprep_config) as f: - json.load(f) + cfg = json.load(f) + + # Checked here so a typo fails at parse time rather than as a KeyError + # while the DIFFPREP workflow is being built. + valid_modes = ('motion', 'quadratic', 'cubic') + correction_mode = cfg.get('correction_mode', 'quadratic') + if correction_mode not in valid_modes: + raise ValueError( + f'DIFFPREP configuration file {diffprep_config} sets ' + f'correction_mode={correction_mode!r}; must be one of ' + f'{", ".join(valid_modes)}.' + ) return diff --git a/qsiprep/workflows/base.py b/qsiprep/workflows/base.py index b2aa92e7..a294e8de 100644 --- a/qsiprep/workflows/base.py +++ b/qsiprep/workflows/base.py @@ -64,12 +64,13 @@ def _t2w_sdc_backend_enabled(): """Whether the selected backend has a stage that can consume a T2w for SDC. DRBUDDI's multimodal ``--structural`` is reached only when ``--pepolar-method`` - asks for DRBUDDI; TORTOISE ``--epi T2Wreg`` is reached from the ``diffprep_*`` - backends for the fieldmap-less case and is **not** gated on ``--pepolar-method``. + asks for DRBUDDI; TORTOISE ``--epi T2Wreg`` is reached from the ``tortoise`` + backend for the fieldmap-less case and is **not** gated on ``--pepolar-method``. """ - return 'drbuddi' in (config.workflow.pepolar_method or '').lower() or ( - config.workflow.hmc_model or '' - ).startswith('diffprep_') + return ( + 'drbuddi' in (config.workflow.pepolar_method or '').lower() + or config.workflow.hmc_model == 'tortoise' + ) def _t2w_available_for_sdc(subject_data): diff --git a/qsiprep/workflows/dwi/base.py b/qsiprep/workflows/dwi/base.py index 3130e5d9..fa3496ae 100644 --- a/qsiprep/workflows/dwi/base.py +++ b/qsiprep/workflows/dwi/base.py @@ -42,18 +42,12 @@ def _doing_t2wreg(fieldmap_type, t2w_sdc): the reportlet gate and produce no SDC figure. """ return ( - (config.workflow.hmc_model or '').startswith('diffprep_') + config.workflow.hmc_model == 'tortoise' and fieldmap_type in (None, 'syn') and bool(t2w_sdc) ) -def _diffprep_order(hmc_model): - """Map a ``diffprep_*`` hmc_model string to a TORTOISE DIFFPREP correction - mode (``motion`` / ``quadratic`` / ``cubic``).""" - return hmc_model.split('_', 1)[1] - - def init_dwi_preproc_wf( scan_groups, t2w_sdc, @@ -286,7 +280,7 @@ def init_dwi_preproc_wf( name='hmc_sdc_wf', ) - elif config.workflow.hmc_model.startswith('diffprep_'): + elif config.workflow.hmc_model == 'tortoise': # The DIFFPREP backend performs its own SDC internally (DRBUDDI for # reverse-PE, TORTOISE T2Wreg for the fieldmap-less-with-T2w case, or # qsiprep's init_sdc_wf for GRE/phase/SyN) -- exactly as init_fsl_hmc_wf @@ -297,7 +291,6 @@ def init_dwi_preproc_wf( source_file=source_file, dwi_metadata=dwi_metadata, t2w_sdc=t2w_sdc, - correction_mode=_diffprep_order(config.workflow.hmc_model), name='hmc_sdc_wf', ) diff --git a/qsiprep/workflows/dwi/derivatives.py b/qsiprep/workflows/dwi/derivatives.py index 244188d9..7c94f3c2 100644 --- a/qsiprep/workflows/dwi/derivatives.py +++ b/qsiprep/workflows/dwi/derivatives.py @@ -19,16 +19,10 @@ # The BIDS ``model`` entity on the CNR derivative names the *signal model* the # CNR was computed from, not the HMC backend -- for the older backends the two -# strings simply coincide (3dSHORE, tensor, eddy). The ``diffprep_*`` backends -# break that: DIFFPREP itself emits no CNR, so qsiprep derives one from the -# MAPMRI fit it already runs for slice QC. ``diffprep_quadratic`` would also be -# an invalid entity value -- ``_`` is the BIDS entity separator, so -# ``model-diffprep_quadratic_stat-cnr`` cannot be parsed back. -_CNR_MODEL_LABELS = { - 'diffprep_motion': 'MAPMRI', - 'diffprep_quadratic': 'MAPMRI', - 'diffprep_cubic': 'MAPMRI', -} +# strings simply coincide (3dSHORE, tensor, eddy). The ``tortoise`` backend +# breaks that: DIFFPREP itself emits no CNR, so qsiprep derives one from the +# MAPMRI fit it already runs for slice QC, and the entity must name MAPMRI. +_CNR_MODEL_LABELS = {'tortoise': 'MAPMRI'} def _cnr_model_label(hmc_model): @@ -39,7 +33,7 @@ def _cnr_model_label(hmc_model): def _cnr_description(hmc_model): """Sidecar description for the CNR map, flagging DIFFPREP's in-sample fit.""" desc = 'Contrast-to-noise ratio map for the HMC step.' - if hmc_model.startswith('diffprep_'): + if hmc_model == 'tortoise': desc += ( ' DIFFPREP does not emit a CNR map, so this was computed from the ' 'MAPMRI model qsiprep fits to the corrected data for slice-wise QC. ' diff --git a/qsiprep/workflows/dwi/diffprep.py b/qsiprep/workflows/dwi/diffprep.py index 5932ba51..6b2acc61 100644 --- a/qsiprep/workflows/dwi/diffprep.py +++ b/qsiprep/workflows/dwi/diffprep.py @@ -66,6 +66,9 @@ def _load_diffprep_config(config_path): cfg.setdefault('is_human_brain', True) cfg.setdefault('rot_eddy_center', 'isocenter') cfg.setdefault('extra_args', []) + # --hmc-model exposes a single "tortoise" value, so this is the only way to + # reach DIFFPREP's rigid-only ('motion') or 'cubic' eddy modes. + cfg.setdefault('correction_mode', 'quadratic') # No default for "use_cuda": its absence must stay observable so a shipped # default is never mistaken for user intent (see _legacy_use_cuda below). # Opt-in MAPMRI shell synthesis for DRBUDDI's registration target; @@ -197,7 +200,6 @@ def init_diffprep_hmc_wf( scan_groups, source_file, t2w_sdc, - correction_mode='quadratic', dwi_metadata=None, name='diffprep_hmc_wf', ): @@ -221,16 +223,12 @@ def init_diffprep_hmc_wf( Whether a T2w image is available for distortion correction (used for DRBUDDI's multi-modal registration and for the fieldmap-less T2Wreg path). - correction_mode : str - One of ``'motion'`` (rigid only), ``'quadratic'`` (recommended), or - ``'cubic'``. Forwarded to TORTOISE as ``-c``. dwi_metadata : dict, optional BIDS sidecar metadata (used for the PE direction and for SDC). name : str Workflow name. """ workflow = Workflow(name=name) - workflow.__desc__ = generate_diffprep_boilerplate(correction_mode) inputnode = pe.Node( niu.IdentityInterface( @@ -311,6 +309,8 @@ def init_diffprep_hmc_wf( pe_dir = _resolve_phase_encoding((dwi_metadata or {}).get('PhaseEncodingDirection')) + correction_mode = diffprep_cfg['correction_mode'] + # --sloppy uses ONLY --niter 0 plus rigid-only correction. Clearing # ``is_human_brain`` would also disable the iterative pass but is not a # speed knob: it changes DIFFPREP's auto-masking and the T2Wreg structural @@ -330,6 +330,10 @@ def init_diffprep_hmc_wf( else: sloppy_kwargs = {} + # Described after the --sloppy downgrade so the methods section reports the + # correction that actually ran, not the one that was asked for. + workflow.__desc__ = generate_diffprep_boilerplate(effective_correction_mode) + diffprep_kwargs = dict( # num_threads only sets OMP_NUM_THREADS, which TORTOISEProcess ignores # (it sizes its thread pool from hardware concurrency and calls @@ -532,7 +536,7 @@ def init_diffprep_hmc_wf( if 'topup' in config.workflow.pepolar_method.lower(): raise Exception( 'TOPUP-based pepolar correction is not supported with ' - '--hmc-model diffprep_*; choose --pepolar-method DRBUDDI.' + '--hmc-model tortoise; choose --pepolar-method DRBUDDI.' ) # For rpe_series the per-direction DIFFPREP stage above already produced