diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..f2fc173 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,86 @@ +name: Bug Report +description: File a report of a problem you encountered. +labels: ["bug"] +body: + - type: markdown + attributes: + value: | + # Thanks for taking the time to fill out this bug report! + ### The following information will help us in getting your issue resolved. + - type: textarea + id: what-happened + attributes: + label: What happened? + description: A short description of what went wrong. + validations: + required: true + - type: textarea + id: command + attributes: + label: What command did you use? + description: | + If you're using `fmriprep-docker`, please include the `RUNNING: ...` line that is printed first. + This helps us replicate the problem. This will be automatically formatted into code, so no need for backticks. + render: shell + validations: + required: true + - type: input + id: version + attributes: + label: What version of fMRIPost-AROMA are you running? + validations: + required: true + - type: dropdown + id: environment + attributes: + label: How are you running fMRIPost-AROMA? + options: + - Docker + - Singularity + - Local installation ("bare-metal") + - Other + validations: + required: true + - type: dropdown + id: bids-valid + attributes: + label: Is your data BIDS valid? + description: | + The BIDS validator can be found at: https://bids-standard.github.io/bids-validator/ + Errors should not be present, but warnings are acceptable. + options: + - "Yes" + - "No" + validations: + required: true + - type: dropdown + id: reuse + attributes: + label: Are you reusing any previously computed results? + description: | + You can select multiple options. + We recommend using a fresh working directory when upgrading to a new fMRIPost-AROMA minor series. + multiple: true + options: + - FreeSurfer + - Anatomical derivatives + - Work directory + - "No" + validations: + required: true + - type: textarea + id: logs + attributes: + label: Please copy and paste any relevant log output. + description: | + Can you find some traces of the error reported in the visual report (at the bottom) or in *crashfiles*? + This will be automatically formatted into code, so no need for backticks. + render: shell + - type: textarea + id: addinfo + attributes: + label: Additional information / screenshots + description: | + If you would like to include any further information, such as any visual reports, please include them below. + Alternatively, you can privately share with us at . + Reports do not contain data usable with personal identification or other research purposes. diff --git a/.github/ISSUE_TEMPLATE/documentation.md b/.github/ISSUE_TEMPLATE/documentation.md new file mode 100644 index 0000000..0639afc --- /dev/null +++ b/.github/ISSUE_TEMPLATE/documentation.md @@ -0,0 +1,14 @@ +--- +name: Documentation improvement +about: Is the documentation of something missing, unclear, or lacking? This is the place. +title: '' +labels: 'documentation' +assignees: '' + +--- + diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..0d361c8 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,39 @@ +name: Feature Request +description: Suggest an idea for a new feature or a change to an existing one. +labels: ["enhancement"] +body: + - type: markdown + attributes: + value: | + ## Thank you for your suggestion! + + We welcome any ideas about how to make *fMRIPost-AROMA* better for the community. + Please keep in mind that features may not get implemented immediately. + - type: textarea + id: summary + attributes: + label: What would you like to see added in fMRIPost-AROMA? + description: | + What are you trying to achieve with fMRIPost-AROMA? + Is this a more convenient way to do something that is already possible, or is a workaround currently unfeasible? + Does this feature adhere to the [NiPreps driving principles](https://www.nipreps.org/community/CONTRIBUTING/#driving-principles)? + validations: + required: true + - type: dropdown + id: interest + attributes: + label: Do you have any interest in helping implement the feature? + description: | + We appreciate any help you can offer! + For information on how to contribute, please refer to our [contributing guidelines](https://www.nipreps.org/community/CONTRIBUTING/). + options: + - "Yes" + - Yes, but I would need guidance + - "No" + validations: + required: true + - type: textarea + id: addinfo + attributes: + label: Additional information / screenshots + description: Add any additional information or context about the feature request here. diff --git a/pyproject.toml b/pyproject.toml index 7bcd7bc..50eb30e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,6 +21,7 @@ dependencies = [ "fmriprep @ git+https://github.com/nipreps/fmriprep.git@master", "nipype >= 1.8.5", "nireports @ git+https://github.com/nipreps/nireports.git@main", + "nitransforms == 23.0.1", "niworkflows @ git+https://github.com/nipreps/niworkflows.git@master", "pybids >= 0.15.6", "sdcflows @ git+https://github.com/nipreps/sdcflows.git@master", diff --git a/src/fmripost_aroma/cli/parser.py b/src/fmripost_aroma/cli/parser.py index 2ce6319..1603698 100644 --- a/src/fmripost_aroma/cli/parser.py +++ b/src/fmripost_aroma/cli/parser.py @@ -318,8 +318,8 @@ def _bids_filter(value, parser): required=False, action='store', nargs='+', - default=[], - choices=['fieldmaps', 'slicetiming', 'jacobian'], + default=['fieldmaps'], + choices=['fieldmaps', 'slicetiming', 'fmap-jacobian'], help=( 'Ignore selected aspects of the input dataset to disable corresponding ' 'parts of the resampling workflow (a space delimited list)' diff --git a/src/fmripost_aroma/config.py b/src/fmripost_aroma/config.py index 0a36c94..9b30b77 100644 --- a/src/fmripost_aroma/config.py +++ b/src/fmripost_aroma/config.py @@ -546,10 +546,17 @@ class workflow(_Config): (positive = exact, negative = maximum).""" denoise_method = None """Denoising strategy to be used.""" + ignore = None + """Ignore particular steps for *fMRIPost-AROMA*.""" cifti_output = None """Generate HCP Grayordinates, accepts either ``'91k'`` (default) or ``'170k'``.""" dummy_scans = None """Set a number of initial scans to be considered nonsteady states.""" + slice_time_ref = 0.5 + """The time of the reference slice to correct BOLD values to, as a fraction + acquisition time. 0 indicates the start, 0.5 the midpoint, and 1 the end + of acquisition. The alias `start` corresponds to 0, and `middle` to 0.5. + The default value is 0.5.""" class loggers: diff --git a/src/fmripost_aroma/data/io_spec.json b/src/fmripost_aroma/data/io_spec.json index 8baa7ae..81a9007 100644 --- a/src/fmripost_aroma/data/io_spec.json +++ b/src/fmripost_aroma/data/io_spec.json @@ -78,6 +78,20 @@ "extension": [ ".tsv" ] + }, + "anat_dseg": { + "datatype": "anat", + "task": null, + "run": null, + "space": null, + "res": null, + "den": null, + "desc": null, + "suffix": "dseg", + "extension": [ + ".nii.gz", + ".nii" + ] } }, "transforms": { diff --git a/src/fmripost_aroma/data/reports-spec-func.yml b/src/fmripost_aroma/data/reports-spec-func.yml index 29e8513..179e200 100644 --- a/src/fmripost_aroma/data/reports-spec-func.yml +++ b/src/fmripost_aroma/data/reports-spec-func.yml @@ -10,17 +10,15 @@ sections: reportlets: - bids: {datatype: figures, desc: summary, suffix: bold} - bids: {datatype: figures, desc: validation, suffix: bold} - - bids: {datatype: figures, desc: aroma, suffix: bold} - - bids: {datatype: figures, desc: metrics, suffix: bold} - - bids: {datatype: figures, desc: coreg, suffix: bold} + - bids: {datatype: figures, desc: normalization, suffix: bold} caption: This panel shows the alignment of the reference EPI (BOLD) image to the - anatomical (T1-weighted) image. - The reference EPI has been contrast enhanced and susceptibility-distortion - corrected (if applicable) for improved anatomical fidelity. - The anatomical image has been resampled into EPI space, as well as the - anatomical white matter mask, which appears as a red contour. + MNI152NLin6Asym template. + The anatomical white matter mask has been warped to MNI152NLin6Asym space + and appears as a red contour. static: false - subtitle: Alignment of functional and anatomical MRI data (coregistration) + subtitle: Alignment of functional and template MRI data (normalization) + - bids: {datatype: figures, desc: aroma, suffix: bold} + - bids: {datatype: figures, desc: metrics, suffix: bold} - bids: {datatype: figures, desc: preprocCarpetplot, suffix: bold} title: Preprocessed BOLD - bids: {datatype: figures, desc: nonaggrCarpetplot, suffix: bold} diff --git a/src/fmripost_aroma/data/reports-spec.yml b/src/fmripost_aroma/data/reports-spec.yml index eb346a3..e9c194d 100644 --- a/src/fmripost_aroma/data/reports-spec.yml +++ b/src/fmripost_aroma/data/reports-spec.yml @@ -10,17 +10,15 @@ sections: reportlets: - bids: {datatype: figures, desc: summary, suffix: bold} - bids: {datatype: figures, desc: validation, suffix: bold} - - bids: {datatype: figures, desc: aroma, suffix: bold} - - bids: {datatype: figures, desc: metrics, suffix: bold} - - bids: {datatype: figures, desc: coreg, suffix: bold} + - bids: {datatype: figures, desc: normalization, suffix: bold} caption: This panel shows the alignment of the reference EPI (BOLD) image to the - anatomical (T1-weighted) image. - The reference EPI has been contrast enhanced and susceptibility-distortion - corrected (if applicable) for improved anatomical fidelity. - The anatomical image has been resampled into EPI space, as well as the - anatomical white matter mask, which appears as a red contour. + MNI152NLin6Asym template. + The anatomical white matter mask has been warped to MNI152NLin6Asym space + and appears as a red contour. static: false - subtitle: Alignment of functional and anatomical MRI data (coregistration) + subtitle: Alignment of functional and template MRI data (normalization) + - bids: {datatype: figures, desc: aroma, suffix: bold} + - bids: {datatype: figures, desc: metrics, suffix: bold} - bids: {datatype: figures, desc: preprocCarpetplot, suffix: bold} title: Preprocessed BOLD - bids: {datatype: figures, desc: nonaggrCarpetplot, suffix: bold} diff --git a/src/fmripost_aroma/interfaces/confounds.py b/src/fmripost_aroma/interfaces/confounds.py index 977f0de..63a8797 100644 --- a/src/fmripost_aroma/interfaces/confounds.py +++ b/src/fmripost_aroma/interfaces/confounds.py @@ -101,7 +101,6 @@ def _get_ica_confounds(mixing, aroma_features, skip_vols, newpath=None): # Regress the good components out of the bad time series to get "pure evil" regressors signal_mixing_arr = mixing_arr[signal_ics, :].T - orthaggr_mixing_arr = aggr_mixing_arr.copy() aggr_mixing_arr_z = stats.zscore(aggr_mixing_arr, axis=0) signal_mixing_arr_z = stats.zscore(signal_mixing_arr, axis=0) betas = np.linalg.lstsq(signal_mixing_arr_z, aggr_mixing_arr_z, rcond=None)[0] diff --git a/src/fmripost_aroma/interfaces/nilearn.py b/src/fmripost_aroma/interfaces/nilearn.py index 936d8c8..5a0cda3 100644 --- a/src/fmripost_aroma/interfaces/nilearn.py +++ b/src/fmripost_aroma/interfaces/nilearn.py @@ -21,7 +21,7 @@ class _MeanImageInputSpec(BaseInterfaceInputSpec): ) mask_file = File( exists=True, - mandatory=True, + mandatory=False, desc='A binary brain mask.', ) out_file = File( @@ -46,11 +46,19 @@ class MeanImage(NilearnBaseInterface, SimpleInterface): output_spec = _MeanImageOutputSpec def _run_interface(self, runtime): + import nibabel as nb from nilearn.masking import apply_mask, unmask + from nipype.interfaces.base import isdefined + + if isdefined(self.inputs.mask_file): + data = apply_mask(self.inputs.bold_file, self.inputs.mask_file) + mean_data = data.mean(axis=0) + mean_img = unmask(mean_data, self.inputs.mask_file) + else: + in_img = nb.load(self.inputs.bold_file) + mean_data = in_img.get_fdata().mean(axis=3) + mean_img = nb.Nifti1Image(mean_data, in_img.affine, in_img.header) - data = apply_mask(self.inputs.bold_file, self.inputs.mask_file) - mean_data = data.mean(axis=0) - mean_img = unmask(mean_data, self.inputs.mask_file) self._results['out_file'] = os.path.join(runtime.cwd, self.inputs.out_file) mean_img.to_filename(self._results['out_file']) diff --git a/src/fmripost_aroma/tests/test_base.py b/src/fmripost_aroma/tests/test_base.py index 2044cea..79f7fd3 100644 --- a/src/fmripost_aroma/tests/test_base.py +++ b/src/fmripost_aroma/tests/test_base.py @@ -20,6 +20,7 @@ def test_init_ica_aroma_wf(tmp_path_factory): wf = init_ica_aroma_wf( bold_file='sub-01_task-rest_bold.nii.gz', metadata={'RepetitionTime': 2.0}, + mem_gb={'resampled': 1}, ) assert wf.name == 'aroma_task_rest_wf' diff --git a/src/fmripost_aroma/workflows/aroma.py b/src/fmripost_aroma/workflows/aroma.py index 0b124c0..cb3f267 100644 --- a/src/fmripost_aroma/workflows/aroma.py +++ b/src/fmripost_aroma/workflows/aroma.py @@ -35,6 +35,7 @@ def init_ica_aroma_wf( *, bold_file: str, metadata: dict, + mem_gb: dict, susan_fwhm: float = 6.0, ): """Build a workflow that runs `ICA-AROMA`_. @@ -192,6 +193,7 @@ def init_ica_aroma_wf( output_type='NIFTI' if config.execution.low_mem else 'NIFTI_GZ', ), name='smooth', + mem_gb=mem_gb['resampled'], ) workflow.connect([ (rm_non_steady_state, smooth, [('bold_cut', 'in_file')]), @@ -209,6 +211,7 @@ def init_ica_aroma_wf( dim=config.workflow.melodic_dim, ), name='melodic', + mem_gb=mem_gb['resampled'], ) workflow.connect([ (inputnode, melodic, [('bold_mask_std', 'mask')]), diff --git a/src/fmripost_aroma/workflows/base.py b/src/fmripost_aroma/workflows/base.py index a0acb82..3cf831b 100644 --- a/src/fmripost_aroma/workflows/base.py +++ b/src/fmripost_aroma/workflows/base.py @@ -40,7 +40,6 @@ from fmripost_aroma import config from fmripost_aroma.utils.utils import _get_wf_name, update_dict -from fmripost_aroma.workflows.resampling import init_resample_volumetric_wf def init_fmripost_aroma_wf(): @@ -280,24 +279,36 @@ def init_single_subject_wf(subject_id: str): def init_single_run_wf(bold_file): """Set up a single-run workflow for fMRIPost-AROMA.""" + from fmriprep.utils.misc import estimate_bold_mem_usage from nipype.interfaces import utility as niu from niworkflows.engine.workflows import LiterateWorkflow as Workflow from fmripost_aroma.utils.bids import collect_derivatives, extract_entities from fmripost_aroma.workflows.aroma import init_denoise_wf, init_ica_aroma_wf + from fmripost_aroma.workflows.outputs import init_func_fit_reports_wf spaces = config.workflow.spaces + omp_nthreads = config.nipype.omp_nthreads workflow = Workflow(name=_get_wf_name(bold_file, 'single_run')) + workflow.__desc__ = '' bold_metadata = config.execution.layout.get_metadata(bold_file) - ica_aroma_wf = init_ica_aroma_wf(bold_file=bold_file, metadata=bold_metadata) + mem_gb = estimate_bold_mem_usage(bold_file)[1] entities = extract_entities(bold_file) functional_cache = defaultdict(list, {}) if config.execution.derivatives: # Collect native-space derivatives and transforms + functional_cache = collect_derivatives( + raw_dataset=config.execution.layout, + derivatives_dataset=None, + entities=entities, + fieldmap_id=None, + allow_multiple=False, + spaces=None, + ) for deriv_dir in config.execution.derivatives.values(): functional_cache = update_dict( functional_cache, @@ -354,47 +365,129 @@ def init_single_run_wf(bold_file): ) skip_vols = get_nss(functional_cache['confounds']) + # Run ICA-AROMA + ica_aroma_wf = init_ica_aroma_wf(bold_file=bold_file, metadata=bold_metadata, mem_gb=mem_gb) ica_aroma_wf.inputs.inputnode.confounds = functional_cache['confounds'] ica_aroma_wf.inputs.inputnode.skip_vols = skip_vols - mni6_buffer = pe.Node( - niu.IdentityInterface( - fields=['bold_mni152nlin6asym', 'bold_mask_mni152nlin6asym'], - ), - name='mni6_buffer', - ) - if 'bold_mni152nlin6asym' not in functional_cache: + mni6_buffer = pe.Node(niu.IdentityInterface(fields=['bold', 'bold_mask']), name='mni6_buffer') + + if ('bold_mni152nlin6asym' not in functional_cache) and ('bold_raw' in functional_cache): # Resample to MNI152NLin6Asym:res-2, for ICA-AROMA classification - run_stc = ( - bool(bold_metadata.get('SliceTiming')) and 'slicetiming' not in config.workflow.ignore + from fmriprep.workflows.bold.apply import init_bold_volumetric_resample_wf + from fmriprep.workflows.bold.stc import init_bold_stc_wf + from niworkflows.interfaces.fixes import FixHeaderApplyTransforms as ApplyTransforms + from niworkflows.interfaces.header import ValidateImage + from templateflow.api import get as get_template + + workflow.__desc__ += """\ +Raw BOLD series were resampled to MNI152NLin6Asym:res-2, for ICA-AROMA classification. +""" + + validate_bold = pe.Node( + ValidateImage(in_file=functional_cache['bold_raw']), + name='validate_bold', + ) + + stc_buffer = pe.Node( + niu.IdentityInterface(fields=['bold_file']), + name='stc_buffer', + ) + run_stc = ('SliceTiming' in bold_metadata) and 'slicetiming' not in config.workflow.ignore + if run_stc: + bold_stc_wf = init_bold_stc_wf( + mem_gb=mem_gb, + metadata=bold_metadata, + name='bold_stc_wf', + ) + bold_stc_wf.inputs.inputnode.skip_vols = skip_vols + workflow.connect([ + (validate_bold, bold_stc_wf, [('out_file', 'inputnode.bold_file')]), + (bold_stc_wf, stc_buffer, [('outputnode.stc_file', 'bold_file')]), + ]) # fmt:skip + else: + workflow.connect([(validate_bold, stc_buffer, [('out_file', 'bold_file')])]) + + mni6_mask = str( + get_template( + 'MNI152NLin6Asym', + resolution=2, + desc='brain', + suffix='mask', + extension=['.nii', '.nii.gz'], + ) ) - resample_raw_wf = init_resample_volumetric_wf( - bold_file=bold_file, + bold_MNI6_wf = init_bold_volumetric_resample_wf( metadata=bold_metadata, - functional_cache=functional_cache, - run_stc=run_stc, - name=_get_wf_name(bold_file, 'resample_raw'), + fieldmap_id=None, # XXX: Ignoring the field map for now + omp_nthreads=omp_nthreads, + mem_gb=mem_gb, + jacobian='fmap-jacobian' not in config.workflow.ignore, + name='bold_MNI6_wf', ) + bold_MNI6_wf.inputs.inputnode.motion_xfm = functional_cache['hmc'] + bold_MNI6_wf.inputs.inputnode.boldref2fmap_xfm = functional_cache['boldref2fmap'] + bold_MNI6_wf.inputs.inputnode.boldref2anat_xfm = functional_cache['boldref2anat'] + bold_MNI6_wf.inputs.inputnode.anat2std_xfm = functional_cache['anat2mni152nlin6asym'] + bold_MNI6_wf.inputs.inputnode.resolution = '02' + # use mask as boldref? + bold_MNI6_wf.inputs.inputnode.bold_ref_file = functional_cache['bold_mask_native'] + bold_MNI6_wf.inputs.inputnode.target_mask = mni6_mask + bold_MNI6_wf.inputs.inputnode.target_ref_file = mni6_mask + workflow.connect([ - (resample_raw_wf, mni6_buffer, [ - ('outputnode.bold_std', 'inputnode.bold_mni152nlin6asym'), - ('outputnode.bold_mask_std', 'inputnode.bold_mask_mni152nlin6asym'), - ]), + # Resample BOLD to MNI152NLin6Asym, may duplicate bold_std_wf above + # XXX: Ignoring the field map for now + # (inputnode, bold_MNI6_wf, [ + # ('fmap_ref', 'inputnode.fmap_ref'), + # ('fmap_coeff', 'inputnode.fmap_coeff'), + # ('fmap_id', 'inputnode.fmap_id'), + # ]), + (stc_buffer, bold_MNI6_wf, [('bold_file', 'inputnode.bold_file')]), + (bold_MNI6_wf, mni6_buffer, [('outputnode.bold_file', 'bold')]), ]) # fmt:skip + + # Warp the mask as well + mask_to_mni6 = pe.Node( + ApplyTransforms( + interpolation='MultiLabel', + input_image=functional_cache['bold_mask_native'], + reference_image=mni6_mask, + transforms=[ + functional_cache['anat2mni152nlin6asym'], + functional_cache['boldref2anat'], + ], + ), + name='mask_to_mni6', + ) + workflow.connect([(mask_to_mni6, mni6_buffer, [('output_image', 'bold_mask')])]) + + elif 'bold_mni152nlin6asym' in functional_cache: + workflow.__desc__ += """\ +Preprocessed BOLD series in MNI152NLin6Asym:res-2 space were collected for ICA-AROMA +classification. +""" + mni6_buffer.inputs.bold = functional_cache['bold_mni152nlin6asym'] + mni6_buffer.inputs.bold_mask = functional_cache['bold_mask_mni152nlin6asym'] + else: - mni6_buffer.inputs.bold_mni152nlin6asym = functional_cache['bold_mni152nlin6asym'] - mni6_buffer.inputs.bold_mask_mni152nlin6asym = functional_cache[ - 'bold_mask_mni152nlin6asym' - ] + raise ValueError('No valid BOLD series found for ICA-AROMA classification.') workflow.connect([ (mni6_buffer, ica_aroma_wf, [ - ('bold_mni152nlin6asym', 'inputnode.bold_std'), - ('bold_mask_mni152nlin6asym', 'inputnode.bold_mask_std'), + ('bold', 'inputnode.bold_std'), + ('bold_mask', 'inputnode.bold_mask_std'), ]), ]) # fmt:skip + # Generate reportlets + func_fit_reports_wf = init_func_fit_reports_wf(output_dir=config.execution.output_dir) + func_fit_reports_wf.inputs.inputnode.source_file = bold_file + func_fit_reports_wf.inputs.inputnode.anat2std_xfm = functional_cache['anat2mni152nlin6asym'] + func_fit_reports_wf.inputs.inputnode.anat_dseg = functional_cache['anat_dseg'] + workflow.connect([(mni6_buffer, func_fit_reports_wf, [('bold', 'inputnode.bold_mni6')])]) + if config.workflow.denoise_method: # Now denoise the output-space BOLD data using ICA-AROMA denoise_wf = init_denoise_wf(bold_file=bold_file, metadata=bold_metadata) @@ -405,8 +498,8 @@ def init_single_run_wf(bold_file): workflow.connect([ (mni6_buffer, denoise_wf, [ - ('bold_mni152nlin6asym', 'inputnode.bold_file'), - ('bold_mask_mni152nlin6asym', 'inputnode.bold_mask'), + ('bold', 'inputnode.bold_file'), + ('bold_mask', 'inputnode.bold_mask'), ]), (ica_aroma_wf, denoise_wf, [ ('outputnode.mixing', 'inputnode.mixing'), diff --git a/src/fmripost_aroma/workflows/outputs.py b/src/fmripost_aroma/workflows/outputs.py new file mode 100644 index 0000000..9000d4d --- /dev/null +++ b/src/fmripost_aroma/workflows/outputs.py @@ -0,0 +1,144 @@ +# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- +# vi: set ft=python sts=4 ts=4 sw=4 et: +# +# Copyright The NiPreps Developers +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# We support and encourage derived works from this project, please read +# about our expectations at +# +# https://www.nipreps.org/community/licensing/ +# +"""Writing out derivative files.""" + +from __future__ import annotations + +from fmriprep.utils.bids import dismiss_echo +from nipype.interfaces import utility as niu +from nipype.pipeline import engine as pe +from niworkflows.interfaces.fixes import FixHeaderApplyTransforms as ApplyTransforms +from niworkflows.utils.images import dseg_label + +from fmripost_aroma.config import DEFAULT_MEMORY_MIN_GB +from fmripost_aroma.interfaces.bids import DerivativesDataSink + + +def init_func_fit_reports_wf( + *, + output_dir: str, + name='func_fit_reports_wf', +) -> pe.Workflow: + """Set up a battery of datasinks to store reports in the right location. + + Parameters + ---------- + freesurfer : :obj:`bool` + FreeSurfer was enabled + output_dir : :obj:`str` + Directory in which to save derivatives + name : :obj:`str` + Workflow name (default: func_fit_reports_wf) + + Inputs + ------ + source_file + Input BOLD images + """ + from nireports.interfaces.reporting.base import ( + SimpleBeforeAfterRPT as SimpleBeforeAfter, + ) + from templateflow.api import get as get_template + + from fmripost_aroma.interfaces.nilearn import MeanImage + + workflow = pe.Workflow(name=name) + + inputfields = [ + 'source_file', + 'bold_mni6', + 'anat_dseg', + 'anat2std_xfm', + ] + inputnode = pe.Node(niu.IdentityInterface(fields=inputfields), name='inputnode') + + # Average the BOLD image over time + calculate_mean_bold = pe.Node( + MeanImage(), + name='calculate_mean_bold', + mem_gb=1, + ) + workflow.connect([(inputnode, calculate_mean_bold, [('bold_mni6', 'bold_file')])]) + + # Warp the tissue segmentation to MNI + dseg_to_mni6 = pe.Node( + ApplyTransforms(interpolation='MultiLabel'), + name='dseg_to_mni6', + mem_gb=1, + ) + workflow.connect([ + (inputnode, dseg_to_mni6, [ + ('anat_dseg', 'input_image'), + ('anat2std_xfm', 'transforms'), + ('bold_mni6', 'reference_image'), + ]), + ]) # fmt:skip + + mni6_wm = pe.Node( + niu.Function(function=dseg_label), + name='mni6_wm', + mem_gb=DEFAULT_MEMORY_MIN_GB, + ) + mni6_wm.inputs.label = 2 # BIDS default is WM=2 + workflow.connect([(dseg_to_mni6, mni6_wm, [('output_image', 'in_seg')])]) + + # EPI-MNI registration + epi_mni_report = pe.Node( + SimpleBeforeAfter( + after=str( + get_template( + 'MNI152NLin6Asym', + resolution=2, + desc='brain', + suffix='T1w', + extension=['.nii', '.nii.gz'], + ) + ), + before_label='EPI', + after_label='MNI152NLin6Asym', + dismiss_affine=True, + ), + name='epi_mni_report', + mem_gb=0.1, + ) + workflow.connect([ + (calculate_mean_bold, epi_mni_report, [('out_file', 'before')]), + (mni6_wm, epi_mni_report, [('out', 'wm_seg')]), + ]) # fmt:skip + + ds_epi_mni_report = pe.Node( + DerivativesDataSink( + base_directory=output_dir, + desc='normalization', + suffix='bold', + datatype='figures', + dismiss_entities=dismiss_echo(), + ), + name='ds_epi_mni_report', + ) + workflow.connect([ + (inputnode, ds_epi_mni_report, [('source_file', 'source_file')]), + (epi_mni_report, ds_epi_mni_report, [('out_report', 'in_file')]), + ]) # fmt:skip + + return workflow diff --git a/src/fmripost_aroma/workflows/resampling.py b/src/fmripost_aroma/workflows/resampling.py deleted file mode 100644 index 0c47cd9..0000000 --- a/src/fmripost_aroma/workflows/resampling.py +++ /dev/null @@ -1,97 +0,0 @@ -"""Workflows to resample data.""" - -from nipype.interfaces import utility as niu -from nipype.pipeline import engine as pe - - -def init_resample_volumetric_wf( - bold_file, - metadata, - functional_cache, - run_stc, - name='resample_volumetric_wf', -): - """Resample raw BOLD data to requested volumetric space. - - Parameters - ---------- - bold_file : str - Path to BOLD file. - functional_cache : dict - Dictionary with paths to functional data. - run_stc : bool - Whether to run STC. - name : str - Workflow name. - """ - from fmriprep.workflows.bold.stc import init_bold_stc_wf - from niworkflows.engine.workflows import LiterateWorkflow as Workflow - - from fmripost_aroma.interfaces.resampler import Resampler - - workflow = Workflow(name=name) - - inputnode = pe.Node( - niu.IdentityInterface( - fields=[ - 'bold_file', - 'mask_file', - ], - ), - name='inputnode', - ) - inputnode.inputs.bold_file = bold_file - inputnode.inputs.mask_file = functional_cache['bold_mask_native'] - - outputnode = pe.Node( - niu.IdentityInterface(fields=['bold_std', 'bold_mask_std']), - name='outputnode', - ) - - stc_buffer = pe.Node( - niu.IdentityInterface(fields=['bold_file']), - name='stc_buffer', - ) - if run_stc: - stc_wf = init_bold_stc_wf( - mem_gb={'filesize': 1}, - metadata=metadata, - name='resample_stc_wf', - ) - workflow.connect([ - (inputnode, stc_wf, [ - ('bold_file', 'inputnode.bold_file'), - ('mask_file', 'inputnode.mask_file'), - ]), - (stc_wf, stc_buffer, [('outputnode.bold_file', 'bold_file')]), - ]) # fmt:skip - else: - workflow.connect([(inputnode, stc_buffer, [('bold_file', 'bold_file')])]) - - resample_bold = pe.Node( - Resampler(), - name='resample_bold', - ) - workflow.connect([ - (inputnode, resample_bold, [ - ('space', 'space'), - ('res', 'res'), - ]), - (stc_buffer, resample_bold, [('outputnode.bold_file', 'bold_file')]), - (resample_bold, outputnode, [('output_file', 'bold_std')]), - ]) # fmt:skip - - resample_bold_mask = pe.Node( - Resampler(), - name='resample_bold_mask', - ) - workflow.connect([ - (inputnode, resample_bold_mask, [ - ('space', 'space'), - ('res', 'res'), - ]), - (inputnode, resample_bold_mask, [('mask_file', 'bold_file')]), - (resample_bold_mask, outputnode, [('output_file', 'bold_mask_std')]), - ]) # fmt:skip - - return workflow