From 203f1c9e0470a5b833b62c9314db2cbc1478dfc9 Mon Sep 17 00:00:00 2001 From: oesteban Date: Sat, 8 Feb 2020 15:09:50 -0800 Subject: [PATCH 01/43] WIP/ENH: Config file and BIDSLayout index --- fmriprep/cli/parser.py | 418 ++++++++++++++ fmriprep/cli/run.py | 725 +++--------------------- fmriprep/cli/tests/test_parser.py | 68 +++ fmriprep/cli/workflow.py | 122 ++++ fmriprep/config.py | 358 +++++++++++- fmriprep/utils/bids.py | 8 +- fmriprep/utils/meepi.py | 8 +- fmriprep/utils/misc.py | 13 + fmriprep/utils/sentry.py | 25 +- fmriprep/utils/testing.py | 2 - fmriprep/workflows/base.py | 503 ++-------------- fmriprep/workflows/bold/base.py | 188 ++---- fmriprep/workflows/bold/registration.py | 4 +- fmriprep/workflows/bold/stc.py | 4 +- fmriprep/workflows/bold/t2s.py | 5 +- setup.cfg | 2 +- 16 files changed, 1178 insertions(+), 1275 deletions(-) create mode 100644 fmriprep/cli/parser.py create mode 100644 fmriprep/cli/tests/test_parser.py create mode 100644 fmriprep/cli/workflow.py create mode 100644 fmriprep/utils/misc.py diff --git a/fmriprep/cli/parser.py b/fmriprep/cli/parser.py new file mode 100644 index 000000000..fafdc6936 --- /dev/null +++ b/fmriprep/cli/parser.py @@ -0,0 +1,418 @@ +# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- +# vi: set ft=python sts=4 ts=4 sw=4 et: +"""Parser.""" +import os +from .. import config + + +def _build_parser(): + """Build parser object.""" + from functools import partial + from pathlib import Path + from argparse import ( + ArgumentParser, + ArgumentDefaultsHelpFormatter, + ) + from packaging.version import Version + from .version import check_latest, is_flagged + from niworkflows.utils.spaces import Reference, SpatialReferences, OutputReferencesAction + + def _path_exists(path, parser): + """Ensure a given path exists.""" + if path is None or not Path(path).exists(): + raise parser.error("Path does not exist: <%s>." % path) + return Path(path).absolute() + + def _min_one(value, parser): + """Ensure an argument is not lower than 1.""" + value = int(value) + if value < 1: + raise parser.error("Argument can't be less than one.") + return value + + def _to_gb(value): + scale = {"G": 1, "T": 10**3, "M": 1e-3, "K": 1e-6, "B": 1e-9} + digits = ''.join([c for c in value if c.isdigit()]) + units = value[len(digits):] or "M" + return int(digits) * scale[units[0]] + + def _drop_sub(value): + value = str(value) + return value.lstrip('sub-') + + def _bids_filter(value): + from json import loads + if value and Path(value).exists(): + return loads(Path(value).read_text()) + + verstr = 'fMRIPrep v{}'.format(config.execution.version) + currentv = Version(config.execution.version) + is_release = not any((currentv.is_devrelease, currentv.is_prerelease, currentv.is_postrelease)) + + parser = ArgumentParser( + description='fMRIPrep: fMRI PREProcessing workflows v{}'.format(config.execution.version), + formatter_class=ArgumentDefaultsHelpFormatter) + PathExists = partial(_path_exists, parser=parser) + PositiveInt = partial(_min_one, parser=parser) + + # Arguments as specified by BIDS-Apps + # required, positional arguments + # IMPORTANT: they must go directly with the parser object + parser.add_argument('bids_dir', action='store', type=PathExists, + help='the root folder of a BIDS valid dataset (sub-XXXXX folders should ' + 'be found at the top level in this folder).') + parser.add_argument('output_dir', action='store', type=Path, + help='the output path for the outcomes of preprocessing and visual ' + 'reports') + parser.add_argument('analysis_level', choices=['participant'], + help='processing stage to be run, only "participant" in the case of ' + 'fMRIPrep (see BIDS-Apps specification).') + + # optional arguments + parser.add_argument('--version', action='version', version=verstr) + + g_bids = parser.add_argument_group('Options for filtering BIDS queries') + g_bids.add_argument('--skip_bids_validation', '--skip-bids-validation', action='store_true', + default=False, + help='assume the input dataset is BIDS compliant and skip the validation') + g_bids.add_argument( + '--participant-label', '--participant_label', action='store', nargs='+', type=_drop_sub, + help='a space delimited list of participant identifiers or a single ' + 'identifier (the sub- prefix can be removed)') + # Re-enable when option is actually implemented + # g_bids.add_argument('-s', '--session-id', action='store', default='single_session', + # help='select a specific session to be processed') + # Re-enable when option is actually implemented + # g_bids.add_argument('-r', '--run-id', action='store', default='single_run', + # help='select a specific run to be processed') + g_bids.add_argument('-t', '--task-id', action='store', + help='select a specific task to be processed') + g_bids.add_argument('--echo-idx', action='store', type=int, + help='select a specific echo to be processed in a multiecho series') + g_bids.add_argument( + '--bids-filter-file', dest='bids_filters', action='store', + type=_bids_filter, metavar='PATH', + help='a JSON file describing custom BIDS input filter using pybids ' + '{:{:,...},...} ' + '(https://github.com/bids-standard/pybids/blob/master/bids/layout/config/bids.json)') + + g_perfm = parser.add_argument_group('Options to handle performance') + g_perfm.add_argument( + '--nprocs', '--nthreads', '--n_cpus', '-n-cpus', action='store', type=PositiveInt, + help='maximum number of threads across all processes') + g_perfm.add_argument('--omp-nthreads', action='store', type=PositiveInt, + help='maximum number of threads per-process') + g_perfm.add_argument('--mem', '--mem_mb', '--mem-mb', dest='memory_gb', + action='store', type=_to_gb, + help='upper bound memory limit for fMRIPrep processes') + g_perfm.add_argument('--low-mem', action='store_true', + help='attempt to reduce memory usage (will increase disk usage ' + 'in working directory)') + g_perfm.add_argument('--use-plugin', action='store', default=None, + help='nipype plugin configuration file') + g_perfm.add_argument('--anat-only', action='store_true', + help='run anatomical workflows only') + g_perfm.add_argument('--boilerplate_only', action='store_true', default=False, + help='generate boilerplate only') + g_perfm.add_argument('--md-only-boilerplate', action='store_true', + default=False, + help='skip generation of HTML and LaTeX formatted citation with pandoc') + g_perfm.add_argument('--error-on-aroma-warnings', action='store_true', + dest='aroma_err_on_warn', default=False, + help='Raise an error if ICA_AROMA does not produce sensible output ' + '(e.g., if all the components are classified as signal or noise)') + g_perfm.add_argument("-v", "--verbose", dest="verbose_count", action="count", default=0, + help="increases log verbosity for each occurence, debug level is -vvv") + + g_conf = parser.add_argument_group('Workflow configuration') + g_conf.add_argument( + '--ignore', required=False, action='store', nargs="+", default=[], + choices=['fieldmaps', 'slicetiming', 'sbref'], + help='ignore selected aspects of the input dataset to disable corresponding ' + 'parts of the workflow (a space delimited list)') + g_conf.add_argument( + '--longitudinal', action='store_true', + help='treat dataset as longitudinal - may increase runtime') + g_conf.add_argument( + '--t2s-coreg', action='store_true', + help='If provided with multi-echo BOLD dataset, create T2*-map and perform ' + 'T2*-driven coregistration. When multi-echo data is provided and this ' + 'option is not enabled, standard EPI-T1 coregistration is performed ' + 'using the middle echo.') + g_conf.add_argument( + '--output-spaces', nargs='+', action=OutputReferencesAction, default=SpatialReferences(), + help="""\ +Standard and non-standard spaces to resample anatomical and functional images to. \ +Standard spaces may be specified by the form \ +``[:cohort-