From aaf08a9b79750f15cf3bf02a65970e74e81f3ffc Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Fri, 7 Jul 2023 15:49:24 -0400 Subject: [PATCH] Keep working. --- aslprep/utils/doc.py | 194 ++++++++++++++++++++++++++ aslprep/utils/misc.py | 12 ++ aslprep/workflows/asl/base.py | 20 +-- aslprep/workflows/asl/cbf.py | 76 +++++----- aslprep/workflows/asl/confounds.py | 74 ++++++---- aslprep/workflows/asl/ge_utils.py | 22 +++ aslprep/workflows/asl/gecbf.py | 4 - aslprep/workflows/asl/hmc.py | 14 +- aslprep/workflows/asl/outputs.py | 15 +- aslprep/workflows/asl/plotting.py | 12 +- aslprep/workflows/asl/qc.py | 42 +++--- aslprep/workflows/asl/registration.py | 54 +++---- aslprep/workflows/asl/util.py | 90 ++++++++---- 13 files changed, 448 insertions(+), 181 deletions(-) create mode 100644 aslprep/utils/doc.py diff --git a/aslprep/utils/doc.py b/aslprep/utils/doc.py new file mode 100644 index 000000000..6e6911322 --- /dev/null +++ b/aslprep/utils/doc.py @@ -0,0 +1,194 @@ +# -*- coding: utf-8 -*- +"""Functions related to the documentation. + +docdict contains the standard documentation entries used across xcp_d. + +source: Eric Larson and MNE-python team. +https://github.com/mne-tools/mne-python/blob/main/mne/utils/docs.py +""" +import sys + +################################### +# Standard documentation entries +# +docdict = dict() + +docdict[ + "omp_nthreads" +] = """ +omp_nthreads : :obj:`int` + Maximum number of threads an individual process may use. +""" + +docdict[ + "mem_gb" +] = """ +mem_gb : :obj:`float` + Memory limit, in gigabytes. +""" + +docdict[ + "output_dir" +] = """ +output_dir : :obj:`str` + Path to the output directory for ``aslprep`` derivatives. + This should not include the ``aslprep`` folder. + For example, "/path/to/dset/derivatives/". +""" + +docdict[ + "work_dir" +] = """ +work_dir : :obj:`str` + Directory in which to store workflow execution state and temporary files. +""" + +docdict[ + "analysis_level" +] = """ +analysis_level : {"participant"} + The analysis level for ``aslprep``. Must be specified as "participant" since ASLPrep + performs analyses at the participant level. +""" + +docdict[ + "basil" +] = """ +basil : :obj:`bool` + Run BASIL, FSL utils to compute CBF with spatial regularization and partial volume correction. + BASIL will not be run if the ASL file only contains pre-calculated CBF images. +""" + +docdict[ + "scorescrub" +] = """ +scorescrub : :obj:`bool` + Run SCORE and SCRUB, Sudipto Dolui's algorithms for denoising CBF. + SCORE and SCRUB will not be run if the ASL file is short (i.e., if the GE workflow is used). +""" + +docdict[ + "m0_scale" +] = """ +m0_scale : :obj:`float`, optional + Relative scale between ASL (delta-M) and M0 volumes. + The M0 volumes will be multiplied by ``m0_scale`` when CBF is calculated. + The default is 1 (no scaling). +""" + +docdict[ + "smooth_kernel" +] = """ +smooth_kernel : :obj:`float` + Kernel size for smoothing M0. +""" + +docdict[ + "processing_target" +] = """ +processing_target : {"controllabel", "deltam", "cbf"} + The target image types from the ASL file to process. +""" + +docdict[ + "dummy_vols" +] = """ +dummy_vols : :obj:`int` + Number of label-control volume pairs to delete before CBF computation. +""" + +docdict[ + "name" +] = """ +name : :obj:`str`, optional + Name of the workflow. This is used for working directories and workflow graphs. +""" + +docdict[ + "aslcontext" +] = """ +aslcontext : :obj:`str` + Path to the ASL context file. +""" + +docdict[ + "name_source" +] = """ +name_source : :obj:`str` + Path to the raw ASL file. Used as the base name for derivatives. +""" + +docdict_indented = {} + + +def _indentcount_lines(lines): + """Minimum indent for all lines in line list. + + >>> lines = [' one', ' two', ' three'] + >>> _indentcount_lines(lines) + 1 + >>> lines = [] + >>> _indentcount_lines(lines) + 0 + >>> lines = [' one'] + >>> _indentcount_lines(lines) + 1 + >>> _indentcount_lines([' ']) + 0 + + """ + indentno = sys.maxsize + for line in lines: + stripped = line.lstrip() + if stripped: + indentno = min(indentno, len(line) - len(stripped)) + if indentno == sys.maxsize: + return 0 + return indentno + + +def fill_doc(f): + """Fill a docstring with docdict entries. + + Parameters + ---------- + f : callable + The function to fill the docstring of. Will be modified in place. + + Returns + ------- + f : callable + The function, potentially with an updated ``__doc__``. + + """ + docstring = f.__doc__ + if not docstring: + return f + lines = docstring.splitlines() + # Find the minimum indent of the main docstring, after first line + if len(lines) < 2: + icount = 0 + else: + icount = _indentcount_lines(lines[1:]) + # Insert this indent to dictionary docstrings + try: + indented = docdict_indented[icount] + except KeyError: + indent = " " * icount + docdict_indented[icount] = indented = {} + for name, dstr in docdict.items(): + lines = dstr.splitlines() + try: + newlines = [lines[0]] + for line in lines[1:]: + newlines.append(indent + line) + indented[name] = "\n".join(newlines) + except IndexError: + indented[name] = dstr + try: + f.__doc__ = docstring % indented + except (TypeError, ValueError, KeyError) as exp: + funcname = f.__name__ + funcname = docstring.split("\n")[0] if funcname is None else funcname + raise RuntimeError(f"Error documenting {funcname}:\n{str(exp)}") + return f diff --git a/aslprep/utils/misc.py b/aslprep/utils/misc.py index 853b39e77..89284ca4e 100644 --- a/aslprep/utils/misc.py +++ b/aslprep/utils/misc.py @@ -137,6 +137,18 @@ def _select_last_in_list(lst): return lst[-1] +def _pick_gm(files): + return files[0] + + +def _pick_wm(files): + return files[1] + + +def _pick_csf(files): + return files[2] + + def _conditional_downsampling(in_file, in_mask, zoom_th=4.0): """Downsample the input dataset for sloppy mode.""" from pathlib import Path diff --git a/aslprep/workflows/asl/base.py b/aslprep/workflows/asl/base.py index 54b7578c9..e1afcc938 100644 --- a/aslprep/workflows/asl/base.py +++ b/aslprep/workflows/asl/base.py @@ -96,7 +96,7 @@ def init_asl_preproc_wf(asl_file): mean_cbf_score_t1 mean score cbf in T1w space mean_cbf_scrub_t1, mean_cbf_gm_basil_t1, mean_cbf_basil_t1 - scrub, parital volume corrected and basil cbf in T1w space + scrub, partial volume corrected, and basil cbf in T1w space cbf_ts_std cbf times series in template space mean_cbf_std @@ -607,9 +607,7 @@ def init_asl_preproc_wf(asl_file): (asl_reg_wf, syn_unwarp_report_wf, [ ("outputnode.anat_to_aslref_xfm", "inputnode.in_xfm"), ]), - (asl_sdc_wf, syn_unwarp_report_wf, [ - ("outputnode.syn_ref", "inputnode.in_post"), - ]), + (asl_sdc_wf, syn_unwarp_report_wf, [("outputnode.syn_ref", "inputnode.in_post")]), ]) # fmt:on @@ -707,7 +705,9 @@ def init_asl_preproc_wf(asl_file): # NOTE: Can this be bundled into the ASL-T1w transform workflow? aslmask_to_t1w = pe.Node( - ApplyTransforms(interpolation="MultiLabel"), name="aslmask_to_t1w", mem_gb=0.1 + ApplyTransforms(interpolation="MultiLabel"), + name="aslmask_to_t1w", + mem_gb=0.1, ) # fmt:off @@ -763,11 +763,7 @@ def init_asl_preproc_wf(asl_file): # fmt:on # For GE data, asl-asl, asl-T1, and asl-std should all have "identity" for HMC/SDC. - # fmt:off - workflow.connect([ - (asl_split, asl_std_trans_wf, [("out_files", "inputnode.asl_split")]), - ]) - # fmt:on + workflow.connect([(asl_split, asl_std_trans_wf, [("out_files", "inputnode.asl_split")])]) # asl_derivatives_wf internally parametrizes over snapshotted spaces. for cbf_deriv in cbf_derivs: @@ -821,9 +817,7 @@ def init_asl_preproc_wf(asl_file): for cbf_deriv in cbf_derivs: # fmt:off workflow.connect([ - (compute_cbf_wf, plot_cbf_wf, [ - (f"outputnode.{cbf_deriv}", f"inputnode.{cbf_deriv}"), - ]), + (compute_cbf_wf, plot_cbf_wf, [(f"outputnode.{cbf_deriv}", f"inputnode.{cbf_deriv}")]), ]) # fmt:on diff --git a/aslprep/workflows/asl/cbf.py b/aslprep/workflows/asl/cbf.py index 06a466c55..171b65079 100644 --- a/aslprep/workflows/asl/cbf.py +++ b/aslprep/workflows/asl/cbf.py @@ -1,6 +1,8 @@ # emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- # vi: set ft=python sts=4 ts=4 sw=4 et: """Workflows for calculating CBF.""" +import os + import pandas as pd from nipype.interfaces import utility as niu from nipype.interfaces.fsl import Info, MultiImageMaths @@ -27,8 +29,11 @@ pcasl_or_pasl, ) from aslprep.utils.atlas import get_atlas_names, get_atlas_nifti +from aslprep.utils.doc import fill_doc +from aslprep.utils.misc import _pick_csf, _pick_gm, _pick_wm +@fill_doc def init_compute_cbf_wf( name_source, processing_target, @@ -65,20 +70,24 @@ def init_compute_cbf_wf( dummy_vols=0, scorescrub=True, basil=True, + m0_scale=1, + smooth_kernel=5, + name="compute_cbf_wf", ) Parameters ---------- - name_source : :obj:`str` - Path to the raw ASL file. + %(name_source)s + %(processing_target)s metadata : :obj:`dict` - BIDS metadata for asl file - scorescrub - basil - m0_scale - smooth_kernel - name : :obj:`str` - Name of workflow (default: ``compute_cbf_wf``) + BIDS metadata for ASL file. + %(dummy_vols)s + %(scorescrub)s + %(basil)s + %(m0_scale)s + %(smooth_kernel)s + %(name)s + Default is ``compute_cbf_wf``. Inputs ------ @@ -251,20 +260,6 @@ def init_compute_cbf_wf( # fmt:on # Warp tissue probability maps to ASL space - def _pick_gm(files): - return files[0] - - def _pick_wm(files): - return files[1] - - def _pick_csf(files): - return files[2] - - def _getfiledir(file): - import os - - return os.path.dirname(file) - gm_tfm = pe.Node( ApplyTransforms(interpolation="NearestNeighbor", float=True), name="gm_tfm", @@ -463,7 +458,7 @@ def _getfiledir(file): workflow.connect([ (refine_mask, basilcbf, [("out_mask", "mask")]), (extract_deltam, basilcbf, [ - (("m0_file", _getfiledir), "out_basename"), + (("m0_file", os.path.dirname), "out_basename"), ("out_file", "deltam"), ("m0_file", "mzero"), ("m0tr", "m0tr"), @@ -485,6 +480,7 @@ def _getfiledir(file): return workflow +@fill_doc def init_compute_cbf_ge_wf( name_source, aslcontext, @@ -518,7 +514,23 @@ def init_compute_cbf_ge_wf( aslcontext=str(perf_dir / "sub-01_aslcontext.tsv"), metadata=metadata, mem_gb=0.1, + m0_scale=1, + scorescrub=True, + basil=True, + name="compute_cbf_wf", ) + + Parameters + ---------- + %(name_source)s + aslcontext + metadata + %(mem_gb)s + %(m0_scale)s + %(scorescrub)s + %(basil)s + %(name)s + Default is "compute_cbf_wf", """ workflow = Workflow(name=name) workflow.__desc__ = """\ @@ -572,20 +584,6 @@ def init_compute_cbf_ge_wf( name="outputnode", ) - def _pick_gm(files): - return files[0] - - def _pick_wm(files): - return files[1] - - def _pick_csf(files): - return files[2] - - def _getfiledir(file): - import os - - return os.path.dirname(file) - # convert tmps to asl_space # extract probability maps csf_tfm = pe.Node( @@ -832,7 +830,7 @@ def _getfiledir(file): # fmt:off workflow.connect([ (inputnode, basilcbf, [ - (("asl_mask", _getfiledir), "out_basename"), + (("asl_mask", os.path.dirname), "out_basename"), ("m0_file", "mzero"), ("m0tr", "m0tr"), ]), diff --git a/aslprep/workflows/asl/confounds.py b/aslprep/workflows/asl/confounds.py index d58954cdd..0184a7b67 100644 --- a/aslprep/workflows/asl/confounds.py +++ b/aslprep/workflows/asl/confounds.py @@ -39,6 +39,7 @@ def init_asl_confounds_wf( wf = init_asl_confounds_wf( mem_gb=1, + name="asl_confounds_wf", ) Parameters @@ -52,7 +53,6 @@ def init_asl_confounds_wf( name : :obj:`str` Name of workflow (default: ``asl_confounds_wf``) - Inputs ------ asl @@ -99,12 +99,13 @@ def init_asl_confounds_wf( "t1w_mask", "t1w_tpms", "anat_to_aslref_xfm", - ] + ], ), name="inputnode", ) outputnode = pe.Node( - niu.IdentityInterface(fields=["confounds_file", "confounds_metadata"]), name="outputnode" + niu.IdentityInterface(fields=["confounds_file", "confounds_metadata"]), + name="outputnode", ) # DVARS @@ -113,9 +114,18 @@ def init_asl_confounds_wf( name="dvars", mem_gb=mem_gb, ) + # fmt:off + workflow.connect([ + (inputnode, dvars, [ + ("asl", "in_file"), + ("asl_mask", "in_mask"), + ]), + ]) + # fmt:on # Frame displacement fdisp = pe.Node(nac.FramewiseDisplacement(parameter_source="SPM"), name="fdisp", mem_gb=mem_gb) + workflow.connect([(inputnode, fdisp, [("movpar_file", "in_file")])]) # Global and segment regressors # signals_class_labels = ["csf", "white_matter", "global_signal"] @@ -127,46 +137,42 @@ def init_asl_confounds_wf( mem_gb=0.01, run_without_submitting=True, ) + workflow.connect([(dvars, add_dvars_header, [("out_nstd", "in_file")])]) + add_std_dvars_header = pe.Node( AddTSVHeader(columns=["std_dvars"]), name="add_std_dvars_header", mem_gb=0.01, run_without_submitting=True, ) + workflow.connect([(dvars, add_std_dvars_header, [("out_std", "in_file")])]) + add_motion_headers = pe.Node( AddTSVHeader(columns=["trans_x", "trans_y", "trans_z", "rot_x", "rot_y", "rot_z"]), name="add_motion_headers", mem_gb=0.01, run_without_submitting=True, ) + workflow.connect([(inputnode, add_motion_headers, [("movpar_file", "in_file")])]) + add_rmsd_header = pe.Node( AddTSVHeader(columns=["rmsd"]), name="add_rmsd_header", mem_gb=0.01, run_without_submitting=True, ) + workflow.connect([(inputnode, add_rmsd_header, [("rmsd_file", "in_file")])]) + concat = pe.Node(GatherConfounds(), name="concat", mem_gb=0.01, run_without_submitting=True) # Expand model to include derivatives and quadratics # fmt:off workflow.connect([ - # Connect inputnode to each non-anatomical confound node - (inputnode, dvars, [ - ("asl", "in_file"), - ("asl_mask", "in_mask"), - ]), - (inputnode, fdisp, [("movpar_file", "in_file")]), - # Collate computed confounds together - (inputnode, add_motion_headers, [("movpar_file", "in_file")]), - (inputnode, add_rmsd_header, [("rmsd_file", "in_file")]), - (dvars, add_dvars_header, [("out_nstd", "in_file")]), - (dvars, add_std_dvars_header, [("out_std", "in_file")]), (fdisp, concat, [("out_file", "fd")]), (add_motion_headers, concat, [("out_file", "motion")]), (add_rmsd_header, concat, [("out_file", "rmsd")]), (add_dvars_header, concat, [("out_file", "dvars")]), (add_std_dvars_header, concat, [("out_file", "std_dvars")]), - # Set outputs (concat, outputnode, [("confounds_file", "confounds_file")]), ]) # fmt:on @@ -224,6 +230,15 @@ def init_carpetplot_wf(mem_gb, metadata, name="carpetplot_wf"): # List transforms mrg_xfms = pe.Node(niu.Merge(2), name="mrg_xfms") + # fmt:off + workflow.connect([ + (inputnode, mrg_xfms, [ + ("anat_to_aslref_xfm", "in1"), + ("template_to_anat_xfm", "in2"), + ]), + ]) + # fmt:on + # Warp segmentation into EPI space resample_parc = pe.Node( ApplyTransforms( @@ -244,6 +259,13 @@ def init_carpetplot_wf(mem_gb, metadata, name="carpetplot_wf"): name="resample_parc", ) + # fmt:off + workflow.connect([ + (inputnode, resample_parc, [("asl_mask", "reference_image")]), + (mrg_xfms, resample_parc, [("out", "transforms")]), + ]) + # fmt:on + # Carpetplot and confounds plot conf_plot = pe.Node( ASLSummary( @@ -253,30 +275,24 @@ def init_carpetplot_wf(mem_gb, metadata, name="carpetplot_wf"): name="conf_plot", mem_gb=mem_gb, ) - ds_report_asl_conf = pe.Node( - DerivativesDataSink(desc="carpetplot", datatype="figures", keep_dtype=True), - name="ds_report_asl_conf", - run_without_submitting=True, - mem_gb=DEFAULT_MEMORY_MIN_GB, - ) # fmt:off workflow.connect([ - (inputnode, mrg_xfms, [ - ("anat_to_aslref_xfm", "in1"), - ("template_to_anat_xfm", "in2"), - ]), - (inputnode, resample_parc, [("asl_mask", "reference_image")]), - (mrg_xfms, resample_parc, [("out", "transforms")]), - # Carpetplot (inputnode, conf_plot, [ ("asl", "in_func"), ("asl_mask", "in_mask"), ("confounds_file", "confounds_file"), ]), (resample_parc, conf_plot, [("output_image", "in_segm")]), - (conf_plot, ds_report_asl_conf, [("out_file", "in_file")]), ]) # fmt:on + ds_report_asl_conf = pe.Node( + DerivativesDataSink(desc="carpetplot", datatype="figures", keep_dtype=True), + name="ds_report_asl_conf", + run_without_submitting=True, + mem_gb=DEFAULT_MEMORY_MIN_GB, + ) + workflow.connect([(conf_plot, ds_report_asl_conf, [("out_file", "in_file")])]) + return workflow diff --git a/aslprep/workflows/asl/ge_utils.py b/aslprep/workflows/asl/ge_utils.py index 2e7804d70..9f49c123b 100644 --- a/aslprep/workflows/asl/ge_utils.py +++ b/aslprep/workflows/asl/ge_utils.py @@ -10,12 +10,14 @@ from aslprep import config from aslprep.interfaces import DerivativesDataSink from aslprep.interfaces.ge import GeReferenceFile +from aslprep.utils.misc import fill_doc from aslprep.workflows.asl.registration import init_fsl_bbr_wf DEFAULT_MEMORY_MIN_GB = config.DEFAULT_MEMORY_MIN_GB LOGGER = config.loggers.workflow +@fill_doc def init_asl_reference_ge_wf( metadata, aslcontext, @@ -43,7 +45,16 @@ def init_asl_reference_ge_wf( wf = init_asl_reference_ge_wf( metadata=metadata, aslcontext=str(perf_dir / "sub-01_aslcontext.tsv"), + smooth_kernel=5, + name="asl_reference_ge_wf", ) + + Parameters + ---------- + metadata + %(aslcontext)s + %(smooth_kernel)s + %(name)s """ workflow = Workflow(name=name) workflow.__desc__ = """\ @@ -128,6 +139,7 @@ def init_asl_reference_ge_wf( return workflow +@fill_doc def init_asl_reg_ge_wf( use_bbr, asl2t1w_dof, @@ -150,6 +162,16 @@ def init_asl_reg_ge_wf( asl2t1w_dof=9, asl2t1w_init="register", ) + + Parameters + ---------- + use_bbr + asl2t1w_dof + asl2t1w_init + sloppy + write_report + %(name)s: + Default is "asl_reg_ge_wf". """ workflow = Workflow(name=name) inputnode = pe.Node( diff --git a/aslprep/workflows/asl/gecbf.py b/aslprep/workflows/asl/gecbf.py index c20871590..b00471e99 100644 --- a/aslprep/workflows/asl/gecbf.py +++ b/aslprep/workflows/asl/gecbf.py @@ -260,10 +260,7 @@ def init_asl_gepreproc_wf(asl_file): basil=basil, output_confounds=False, # GE workflow doesn't generate volume-wise confounds ) - - # fmt:off workflow.connect([(inputnode, asl_derivatives_wf, [("asl_file", "inputnode.source_file")])]) - # fmt:on # begin workflow # Extract averaged, smoothed M0 image and reference image (which is generally the M0 image). @@ -288,7 +285,6 @@ def init_asl_gepreproc_wf(asl_file): # can be applied with other transforms in single shots. # This will be useful for GE/non-GE integration. asl_split = pe.Node(Split(dimension="t"), name="asl_split", mem_gb=mem_gb["filesize"] * 3) - workflow.connect([(inputnode, asl_split, [("asl_file", "in_file")])]) # Set HMC xforms and fieldwarp to "identity" since neither is performed for GE data. diff --git a/aslprep/workflows/asl/hmc.py b/aslprep/workflows/asl/hmc.py index 9338e33e4..497c3d3c9 100644 --- a/aslprep/workflows/asl/hmc.py +++ b/aslprep/workflows/asl/hmc.py @@ -14,8 +14,10 @@ PairwiseRMSDiff, SplitOutVolumeType, ) +from aslprep.utils.misc import fill_doc +@fill_doc def init_asl_hmc_wf( processing_target, m0type, @@ -46,12 +48,10 @@ def init_asl_hmc_wf( Parameters ---------- - processing_target : {"controllabel", "deltam", "cbf"} + %(processing_target)s m0type : {"Separate", "Included", "Absent", "Estimate"} - mem_gb : :obj:`float` - Size of ASL file in GB - omp_nthreads : :obj:`int` - Maximum number of threads an individual process may use + %(mem_gb)s + %(omp_nthreads)s name : :obj:`str` Name of workflow (default: ``asl_hmc_wf``) @@ -61,8 +61,7 @@ def init_asl_hmc_wf( Control-label pair series NIfTI file. If an ASL run contains M0 volumes, deltaM volumes, or CBF volumes, those volumes should be removed before running this workflow. - aslcontext - ASL context TSV file. + %(aslcontext)s raw_ref_image Reference image to which ASL series is motion corrected @@ -132,7 +131,6 @@ def init_asl_hmc_wf( CombineMotionParameters(m0type=m0type, processing_target=processing_target), name="combine_motpars", ) - workflow.connect([(inputnode, combine_motpars, [("aslcontext", "aslcontext")])]) files_to_mcflirt = [] diff --git a/aslprep/workflows/asl/outputs.py b/aslprep/workflows/asl/outputs.py index 1b950d384..954fdd3c0 100644 --- a/aslprep/workflows/asl/outputs.py +++ b/aslprep/workflows/asl/outputs.py @@ -9,8 +9,10 @@ from aslprep import config from aslprep.interfaces import DerivativesDataSink +from aslprep.utils.misc import fill_doc +@fill_doc def init_asl_derivatives_wf( bids_root, metadata, @@ -30,8 +32,7 @@ def init_asl_derivatives_wf( Original BIDS dataset path. metadata : :obj:`dict` Metadata dictionary associated to the ASL run. - output_dir : :obj:`str` - Where derivatives should be written out to. + %(output_dir)s spaces : :py:class:`~niworkflows.utils.spaces.SpatialReferences` A container for storing, organizing, and parsing spatial normalizations. Composed of :py:class:`~niworkflows.utils.spaces.Reference` objects representing spatial references. @@ -43,8 +44,11 @@ def init_asl_derivatives_wf( would lead to resampling on a 2mm resolution of the space). is_multi_pld : :obj:`bool` True if data are multi-delay, False otherwise. - name : :obj:`str` - This workflow's identifier (default: ``func_derivatives_wf``). + output_confounds + %(scorescrub)s + %(basil)s + %(name)s + Default is "asl_derivatives_wf". """ nonstd_spaces = set(spaces.get_nonstandard()) workflow = Workflow(name=name) @@ -119,10 +123,7 @@ def init_asl_derivatives_wf( raw_sources = pe.Node(niu.Function(function=_bids_relative), name="raw_sources") raw_sources.inputs.bids_root = bids_root - - # fmt:off workflow.connect([(inputnode, raw_sources, [("source_file", "in_files")])]) - # fmt:on if output_confounds: ds_confounds = pe.Node( diff --git a/aslprep/workflows/asl/plotting.py b/aslprep/workflows/asl/plotting.py index 04ae2d9d3..a7178dd1f 100644 --- a/aslprep/workflows/asl/plotting.py +++ b/aslprep/workflows/asl/plotting.py @@ -9,9 +9,10 @@ from aslprep.interfaces import DerivativesDataSink from aslprep.interfaces.ants import ApplyTransforms from aslprep.interfaces.plotting import CBFByTissueTypePlot, CBFSummary, CBFtsSummary -from aslprep.utils.misc import get_template_str +from aslprep.utils.misc import fill_doc, get_template_str +@fill_doc def init_plot_cbf_wf( metadata, plot_timeseries=True, @@ -31,6 +32,15 @@ def init_plot_cbf_wf( wf = init_plot_cbf_wf( metadata={"RepetitionTimePreparation": 4}, ) + + Parameters + ---------- + metadata + plot_timeseries + %(scorescrub)s + %(basil)s + %(name)s + Default is "plot_cbf_wf". """ workflow = Workflow(name=name) diff --git a/aslprep/workflows/asl/qc.py b/aslprep/workflows/asl/qc.py index 0c32936ca..ca4b4d1b3 100644 --- a/aslprep/workflows/asl/qc.py +++ b/aslprep/workflows/asl/qc.py @@ -10,9 +10,16 @@ from aslprep.interfaces.ants import ApplyTransforms from aslprep.interfaces.bids import DerivativesDataSink from aslprep.interfaces.qc import ComputeCBFQC -from aslprep.utils.misc import _select_last_in_list +from aslprep.utils.misc import ( + _pick_csf, + _pick_gm, + _pick_wm, + _select_last_in_list, + fill_doc, +) +@fill_doc def init_compute_cbf_qc_wf( is_ge, output_dir, @@ -40,11 +47,11 @@ def init_compute_cbf_qc_wf( Parameters ---------- is_ge : bool - output_dir : str - scorescrub : bool - basil : bool - name : :obj:`str` - Name of workflow (default: "compute_cbf_qc_wf") + %(output_dir)s + %(scorescrub)s + %(basil)s + %(name)s + Default is "compute_cbf_qc_wf". Inputs ------ @@ -95,15 +102,6 @@ def init_compute_cbf_qc_wf( ) outputnode = pe.Node(niu.IdentityInterface(fields=["qc_file"]), name="outputnode") - def _pick_gm(files): - return files[0] - - def _pick_wm(files): - return files[1] - - def _pick_csf(files): - return files[2] - gm_tfm = pe.Node( ApplyTransforms(interpolation="NearestNeighbor", float=True), name="gm_tfm", @@ -168,19 +166,17 @@ def _pick_csf(files): ]) # fmt:on - brain_mask = str( - get_template("MNI152NLin2009cAsym", resolution=2, desc="brain", suffix="mask") - ) - resample = pe.Node( - Resample(in_file=brain_mask, outputtype="NIFTI_GZ"), + Resample( + in_file=str( + get_template("MNI152NLin2009cAsym", resolution=2, desc="brain", suffix="mask") + ), + outputtype="NIFTI_GZ", + ), name="resample", mem_gb=0.1, ) - - # fmt:off workflow.connect([(inputnode, resample, [(("asl_mask_std", _select_last_in_list), "master")])]) - # fmt:on compute_qc_metrics = pe.Node( ComputeCBFQC(tpm_threshold=0.7), diff --git a/aslprep/workflows/asl/registration.py b/aslprep/workflows/asl/registration.py index ce218c959..45af69fcf 100644 --- a/aslprep/workflows/asl/registration.py +++ b/aslprep/workflows/asl/registration.py @@ -18,13 +18,18 @@ from aslprep import config from aslprep.interfaces import DerivativesDataSink from aslprep.interfaces.ants import ApplyTransforms -from aslprep.utils.misc import _conditional_downsampling, _select_first_in_list +from aslprep.utils.misc import ( + _conditional_downsampling, + _select_first_in_list, + fill_doc, +) from aslprep.workflows.asl.util import init_asl_reference_wf DEFAULT_MEMORY_MIN_GB = config.DEFAULT_MEMORY_MIN_GB LOGGER = config.loggers.workflow +@fill_doc def init_asl_reg_wf( use_bbr, asl2t1w_dof, @@ -61,16 +66,11 @@ def init_asl_reg_wf( asl2t1w_init : str, 'header' or 'register' If ``'header'``, use header information for initialization of ASL and T1 images. If ``'register'``, align volumes by their centers. - mem_gb : :obj:`float` - Size of ASL file in GB - omp_nthreads : :obj:`int` - Maximum number of threads an individual process may use - name : :obj:`str` - Name of workflow (default: ``asl_reg_wf``) - use_compression : :obj:`bool` - Save registered ASL series as ``.nii.gz`` + sloppy write_report : :obj:`bool` Whether a reportlet should be stored + %(name)s : :obj:`str` + Default is "asl_reg_wf". Inputs ------ @@ -104,7 +104,7 @@ def init_asl_reg_wf( "ref_asl_brain", "t1w_brain", "t1w_dseg", - ] + ], ), name="inputnode", ) @@ -115,7 +115,7 @@ def init_asl_reg_wf( "aslref_to_anat_xfm", "anat_to_aslref_xfm", "fallback", - ] + ], ), name="outputnode", ) @@ -165,6 +165,7 @@ def _asl_reg_suffix(fallback): # noqa: U100 return workflow +@fill_doc def init_asl_t1_trans_wf( mem_gb, omp_nthreads, @@ -200,16 +201,20 @@ def init_asl_t1_trans_wf( Size of ASL file in GB omp_nthreads : :obj:`int` Maximum number of threads an individual process may use + is_multi_pld + %(scorescrub)s + %(basil)s + generate_reference + output_t1space use_compression : :obj:`bool` Save registered ASL series as ``.nii.gz`` - name : :obj:`str` - Name of workflow (default: ``asl_reg_wf``) + %(name)s + Default is "asl_t1_trans_wf". Inputs ------ - name_source - ASL series NIfTI file - Used to recover original information lost during processing + %(name_source)s + Used to recover original information lost during processing. ref_asl_brain Reference image to which ASL series is aligned If ``fieldwarp == True``, ``ref_asl_brain`` should be unwarped @@ -243,7 +248,6 @@ def init_asl_t1_trans_wf( See also -------- * :py:func:`~aslprep.workflows.asl.registration.init_fsl_bbr_wf` - """ workflow = Workflow(name=name) @@ -327,7 +331,6 @@ def init_asl_t1_trans_wf( mem_gb=mem_gb * 3 * omp_nthreads, n_procs=omp_nthreads, ) - workflow.connect([(gen_ref, asl_to_t1w_transform, [("out_file", "reference_image")])]) # Merge transforms, placing the head motion correction last @@ -457,6 +460,7 @@ def init_asl_t1_trans_wf( return workflow +@fill_doc def init_fsl_bbr_wf(use_bbr, asl2t1w_dof, asl2t1w_init, sloppy=False, name="fsl_bbr_wf"): """Build a workflow to run FSL's ``flirt``. @@ -498,8 +502,9 @@ def init_fsl_bbr_wf(use_bbr, asl2t1w_dof, asl2t1w_init, sloppy=False, name="fsl_ asl2t1w_init : str, 'header' or 'register' If ``'header'``, use header information for initialization of ASL and T1 images. If ``'register'``, align volumes by their centers. - name : :obj:`str`, optional - Workflow name (default: fsl_bbr_wf) + sloppy + %(name)s + Default is "fsl_bbr_wf". Inputs ------ @@ -510,7 +515,6 @@ def init_fsl_bbr_wf(use_bbr, asl2t1w_dof, asl2t1w_init, sloppy=False, name="fsl_ t1w_dseg FAST segmentation of ``t1w_brain`` - Outputs ------- aslref_to_anat_xfm @@ -521,7 +525,6 @@ def init_fsl_bbr_wf(use_bbr, asl2t1w_dof, asl2t1w_init, sloppy=False, name="fsl_ Reportlet for assessing registration quality fallback Boolean indicating whether BBR was rejected (rigid FLIRT registration returned) - """ workflow = Workflow(name=name) workflow.__desc__ = f"""\ @@ -538,7 +541,7 @@ def init_fsl_bbr_wf(use_bbr, asl2t1w_dof, asl2t1w_init, sloppy=False, name="fsl_ "in_file", "t1w_dseg", "t1w_brain", - ] + ], ), name="inputnode", ) @@ -549,7 +552,7 @@ def init_fsl_bbr_wf(use_bbr, asl2t1w_dof, asl2t1w_init, sloppy=False, name="fsl_ "anat_to_aslref_xfm", "out_report", "fallback", - ] + ], ), name="outputnode", ) @@ -573,8 +576,7 @@ def init_fsl_bbr_wf(use_bbr, asl2t1w_dof, asl2t1w_init, sloppy=False, name="fsl_ mem_gb=DEFAULT_MEMORY_MIN_GB, ) - # ASL to T1 transform matrix is from fsl, using c3 tools to convert to - # something ANTs will like. + # ASL to T1 transform matrix is from FSL, using c3 tools to convert to something ANTs will like fsl2itk_fwd = pe.Node( c3.C3dAffineTool(fsl2ras=True, itk_transform=True), name="fsl2itk_fwd", diff --git a/aslprep/workflows/asl/util.py b/aslprep/workflows/asl/util.py index 651a4b105..7e0045062 100644 --- a/aslprep/workflows/asl/util.py +++ b/aslprep/workflows/asl/util.py @@ -90,12 +90,15 @@ def init_enhance_and_skullstrip_asl_wf(pre_mask=False, name="enhance_and_skullst reportlet for the skull-stripping """ workflow = Workflow(name=name) + inputnode = pe.Node(niu.IdentityInterface(fields=["in_file", "pre_mask"]), name="inputnode") outputnode = pe.Node( niu.IdentityInterface(fields=["mask_file", "skull_stripped_file", "bias_corrected_file"]), name="outputnode", ) + # This line is only included so we don't get warnings about unused arguments. + # I (TS) don't know why pre_mask is an argument, but modified sdcflows functions use it. pre_mask = pre_mask # Ensure mask's header matches reference's @@ -110,8 +113,20 @@ def init_enhance_and_skullstrip_asl_wf(pre_mask=False, name="enhance_and_skullst ) n4_correct.inputs.rescale_intensities = True + # fmt:off + workflow.connect([ + (inputnode, n4_correct, [ + ("in_file", "mask_image"), + ("in_file", "input_image"), + ]), + (n4_correct, outputnode, [("output_image", "bias_corrected_file")]), + ]) + # fmt:on + # Create a generous BET mask out of the bias-corrected EPI skullstrip_first_pass = pe.Node(fsl.BET(frac=0.2, mask=True), name="skullstrip_first_pass") + workflow.connect([(n4_correct, skullstrip_first_pass, [("output_image", "in_file")])]) + bet_dilate = pe.Node( fsl.DilateImage( operation="max", @@ -121,8 +136,17 @@ def init_enhance_and_skullstrip_asl_wf(pre_mask=False, name="enhance_and_skullst ), name="skullstrip_first_dilate", ) + workflow.connect([(skullstrip_first_pass, bet_dilate, [("mask_file", "in_file")])]) + bet_mask = pe.Node(fsl.ApplyMask(), name="skullstrip_first_mask") + # fmt:off + workflow.connect([ + (bet_dilate, bet_mask, [("out_file", "mask_file")]), + (skullstrip_first_pass, bet_mask, [("out_file", "in_file")]), + ]) + # fmt:on + # Use AFNI's unifize for T2 constrast & fix header unifize = pe.Node( afni.Unifize( @@ -135,46 +159,56 @@ def init_enhance_and_skullstrip_asl_wf(pre_mask=False, name="enhance_and_skullst ), name="unifize", ) + workflow.connect([(bet_mask, unifize, [("out_file", "in_file")])]) + fixhdr_unifize = pe.Node(CopyXForm(), name="fixhdr_unifize", mem_gb=0.1) + # fmt:off + workflow.connect([ + (inputnode, fixhdr_unifize, [("in_file", "hdr_file")]), + (unifize, fixhdr_unifize, [("out_file", "in_file")]), + ]) + # fmt:on + # Run AFNI's 3dAutomask to extract a refined brain mask skullstrip_second_pass = pe.Node( afni.Automask(dilate=1, outputtype="NIFTI_GZ"), name="skullstrip_second_pass", ) + workflow.connect([(fixhdr_unifize, skullstrip_second_pass, [("out_file", "in_file")])]) + fixhdr_skullstrip2 = pe.Node(CopyXForm(), name="fixhdr_skullstrip2", mem_gb=0.1) + # fmt:off + workflow.connect([ + (inputnode, fixhdr_skullstrip2, [("in_file", "hdr_file")]), + (skullstrip_second_pass, fixhdr_skullstrip2, [("out_file", "in_file")]), + ]) + # fmt:on + # Take intersection of both masks combine_masks = pe.Node(fsl.BinaryMaths(operation="mul"), name="combine_masks") + # fmt:off + workflow.connect([ + (skullstrip_first_pass, combine_masks, [("mask_file", "in_file")]), + (fixhdr_skullstrip2, combine_masks, [("out_file", "operand_file")]), + (combine_masks, outputnode, [("out_file", "mask_file")]), + ]) + # fmt:on + # Compute masked brain apply_mask = pe.Node(fsl.ApplyMask(), name="apply_mask") # binarize_mask = pe.Node(Binarize(thresh_low=brainmask_thresh), name="binarize_mask") - # fmt: off + # fmt:off workflow.connect([ - (inputnode, n4_correct, [("in_file", "mask_image")]), - (inputnode, n4_correct, [("in_file", "input_image")]), - (inputnode, fixhdr_unifize, [("in_file", "hdr_file")]), - (inputnode, fixhdr_skullstrip2, [("in_file", "hdr_file")]), - (n4_correct, skullstrip_first_pass, [("output_image", "in_file")]), - (skullstrip_first_pass, bet_dilate, [("mask_file", "in_file")]), - (bet_dilate, bet_mask, [("out_file", "mask_file")]), - (skullstrip_first_pass, bet_mask, [("out_file", "in_file")]), - (bet_mask, unifize, [("out_file", "in_file")]), - (unifize, fixhdr_unifize, [("out_file", "in_file")]), - (fixhdr_unifize, skullstrip_second_pass, [("out_file", "in_file")]), - (skullstrip_first_pass, combine_masks, [("mask_file", "in_file")]), - (skullstrip_second_pass, fixhdr_skullstrip2, [("out_file", "in_file")]), - (fixhdr_skullstrip2, combine_masks, [("out_file", "operand_file")]), (fixhdr_unifize, apply_mask, [("out_file", "in_file")]), (combine_masks, apply_mask, [("out_file", "mask_file")]), - (combine_masks, outputnode, [("out_file", "mask_file")]), (apply_mask, outputnode, [("out_file", "skull_stripped_file")]), - (n4_correct, outputnode, [("output_image", "bias_corrected_file")]), ]) - # fmt: on + # fmt:on return workflow @@ -243,7 +277,6 @@ def init_validate_asl_wf(asl_file=None, name="validate_asl_wf"): mem_gb=DEFAULT_MEMORY_MIN_GB, iterfield=["in_file"], ) - workflow.connect([(inputnode, val_asl, [(("asl_file", listify), "in_file")])]) asl_1st = pe.Node(niu.Select(index=[0]), name="asl_1st", run_without_submitting=True) @@ -337,11 +370,6 @@ def init_asl_reference_wf( Skull-stripping mask of reference image validation_report : str HTML reportlet indicating whether ``asl_file`` had a valid affine - - - Subworkflows - * :py:func:`~niworkflows.func.util.init_enhance_and_skullstrip_wf` - """ workflow = Workflow(name=name) workflow.__desc__ = """\ @@ -404,7 +432,6 @@ def init_asl_reference_wf( mem_gb=DEFAULT_MEMORY_MIN_GB, iterfield=["in_file"], ) - workflow.connect([(select_reference_volumes, val_asl, [(("out_file", listify), "in_file")])]) gen_ref = pe.Node(EstimateReferenceImage(), name="gen_ref", mem_gb=1) @@ -439,6 +466,7 @@ def init_asl_reference_wf( run_without_submitting=True, mem_gb=DEFAULT_MEMORY_MIN_GB, ) + # fmt:off workflow.connect([ (inputnode, calc_dummy_scans, [("dummy_scans", "dummy_scans")]), @@ -458,12 +486,12 @@ def init_asl_reference_wf( validate_1st = pe.Node(niu.Select(index=[0]), name="validate_1st", run_without_submitting=True) - # fmt: off + # fmt:off workflow.connect([ (val_asl, validate_1st, [(("out_report", listify), "inlist")]), (validate_1st, outputnode, [("out", "validation_report")]), ]) - # fmt: on + # fmt:on if sbref_files: nsbrefs = 0 @@ -478,12 +506,12 @@ def init_asl_reference_wf( mem_gb=DEFAULT_MEMORY_MIN_GB, iterfield=["in_file"], ) - # fmt: off + # fmt:off workflow.connect([ (inputnode, val_sbref, [(("sbref_file", listify), "in_file")]), (val_sbref, gen_ref, [("out_file", "sbref_file")]), ]) - # fmt: on + # fmt:on # Edit the boilerplate as the SBRef will be the reference workflow.__desc__ = f"""\ @@ -494,13 +522,13 @@ def init_asl_reference_wf( if gen_report: mask_reportlet = pe.Node(SimpleShowMaskRPT(), name="mask_reportlet") - # fmt: off + # fmt:off workflow.connect([ (enhance_and_skullstrip_asl_wf, mask_reportlet, [ ("outputnode.bias_corrected_file", "background_file"), ("outputnode.mask_file", "mask_file"), ]), ]) - # fmt: on + # fmt:on return workflow