Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/develop' into feature/fmriprep_i…
Browse files Browse the repository at this point in the history
…ngress
  • Loading branch information
e-kenneally committed Sep 25, 2023
2 parents 0ef2bfe + abbacaf commit 68ee445
Show file tree
Hide file tree
Showing 12 changed files with 153 additions and 67 deletions.
3 changes: 2 additions & 1 deletion .github/Dockerfiles/base-standard.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ RUN apt-get update \
&& apt-get install --no-install-recommends -y bc \
&& yes | mamba install tcsh \
&& yes | mamba clean --all \
&& cp -l `which tcsh` /bin/tcsh
&& cp -l `which tcsh` /bin/tcsh \
&& cp -l `which tcsh` /bin/csh
ENV FREESURFER_HOME="/usr/lib/freesurfer" \
NO_FSFAST=1
ENV PATH="$FREESURFER_HOME/bin:$PATH" \
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Added

- Some automatic handling of user-provided BIDSy atlas names.
- `sig_imports` static method decorator for `Function` nodes, to accommodate type hinting in signatures of `Function` node functions.

## Fixed

Expand Down
16 changes: 2 additions & 14 deletions CPAC/func_preproc/func_motion.py
Original file line number Diff line number Diff line change
Expand Up @@ -577,18 +577,7 @@ def motion_correct_mcflirt(wf, cfg, strat_pool, pipe_num):


def motion_correct_connections(wf, cfg, strat_pool, pipe_num, opt):
"""Check opt for valid option, then connect that option.
Parameters
----------
passed through from node block
Returns
-------
wf : pe.Workflow
outputs : dict
"""
"""Check opt for valid option, then connect that option."""
if opt not in motion_correct_options:
raise KeyError("\n\n[!] Error: The 'tool' parameter of the "
"'motion_correction' workflow must be one of "
Expand Down Expand Up @@ -626,8 +615,7 @@ def motion_correct_connections(wf, cfg, strat_pool, pipe_num, opt):
"motion-filter-info": {},
"motion-filter-plot": {}})
def motion_estimate_filter(wf, cfg, strat_pool, pipe_num, opt=None):
'''
Filter motion parameters.
'''Filter motion parameters.
.. versionchanged:: 1.8.6
Beginning with version 1.8.6, C-PAC outputs both the unfiltered
Expand Down
42 changes: 21 additions & 21 deletions CPAC/func_preproc/func_preproc.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,8 +171,8 @@ def anat_refined_mask(init_bold_mask=True, wf_name='init_bold_mask'):


def anat_based_mask(wf_name='bold_mask'):
# reference DCAN lab BOLD mask
# https://github.com/DCAN-Labs/DCAN-HCP/blob/master/fMRIVolume/scripts/DistortionCorrectionAndEPIToT1wReg_FLIRTBBRAndFreeSurferBBRbased.sh
"""reference `DCAN lab BOLD mask <https://github.com/DCAN-Labs/DCAN-HCP/blob/master/fMRIVolume/scripts/DistortionCorrectionAndEPIToT1wReg_FLIRTBBRAndFreeSurferBBRbased.sh>`_
"""
wf = pe.Workflow(name=wf_name)

input_node = pe.Node(util.IdentityInterface(fields=['func',
Expand Down Expand Up @@ -252,22 +252,24 @@ def anat_based_mask(wf_name='bold_mask'):

def create_scale_func_wf(scaling_factor, wf_name='scale_func'):
"""Workflow to scale func data.
Parameters
----------
scaling_factor : float
Scale the size of the dataset voxels by the factor.
wf_name : string
name of the workflow
Workflow Inputs::
inputspec.func : func file or a list of func/rest nifti file
User input functional(T2*) Image
Workflow Outputs::
outputspec.scaled_func : string (nifti file)
outputspec.scaled_func : str (nifti file)
Path to Output image with scaled data
Order of commands:
- Scale the size of the dataset voxels by the factor 'fac'. For details see `3dcalc <https://afni.nimh.nih.gov/pub/dist/doc/program_help/3drefit.html>`_::
3drefit -xyzscale fac rest.nii.gz
Parameters
----------
scaling_factor : float
Scale the size of the dataset voxels by the factor.
wf_name : str
name of the workflow
"""

# allocate a workflow object
Expand Down Expand Up @@ -306,15 +308,15 @@ def create_wf_edit_func(wf_name="edit_func"):
inputspec.func : func file or a list of func/rest nifti file
User input functional(T2*) Image
inputspec.start_idx : string
inputspec.start_idx : str
Starting volume/slice of the functional image (optional)
inputspec.stop_idx : string
inputspec.stop_idx : str
Last volume/slice of the functional image (optional)
Workflow Outputs::
outputspec.edited_func : string (nifti file)
outputspec.edited_func : str (nifti file)
Path to Output image with the initial few slices dropped
Expand Down Expand Up @@ -463,7 +465,7 @@ def get_idx(in_files, stop_idx=None, start_idx=None):
Parameters
----------
in_file : string (nifti file)
in_file : str (nifti file)
Path to input functional run
stop_idx : int
Expand Down Expand Up @@ -913,8 +915,9 @@ def form_thr_string(thr):
outputs=['space-bold_desc-brain_mask', 'desc-ref_bold']
)
def bold_mask_fsl_afni(wf, cfg, strat_pool, pipe_num, opt=None):
# fMRIPrep-style BOLD mask
# Ref: https://github.com/nipreps/niworkflows/blob/maint/1.3.x/niworkflows/func/util.py#L246-L514
"""fMRIPrep-style BOLD mask
`Ref <https://github.com/nipreps/niworkflows/blob/maint/1.3.x/niworkflows/func/util.py#L246-L514>`_
"""

# Initialize transforms with antsAI
init_aff = pe.Node(
Expand Down Expand Up @@ -1210,8 +1213,7 @@ def bold_mask_anatomical_refined(wf, cfg, strat_pool, pipe_num, opt=None):
)
def bold_mask_anatomical_based(wf, cfg, strat_pool, pipe_num, opt=None):
'''Generate the BOLD mask by basing it off of the anatomical brain mask.
Adapted from DCAN Lab's BOLD mask method from the ABCD pipeline.
https://github.com/DCAN-Labs/DCAN-HCP/blob/master/fMRIVolume/scripts/DistortionCorrectionAndEPIToT1wReg_FLIRTBBRAndFreeSurferBBRbased.sh
Adapted from `DCAN Lab's BOLD mask method from the ABCD pipeline <https://github.com/DCAN-Labs/DCAN-HCP/blob/master/fMRIVolume/scripts/DistortionCorrectionAndEPIToT1wReg_FLIRTBBRAndFreeSurferBBRbased.sh>`_.
'''

# 0. Take single volume of func
Expand Down Expand Up @@ -1303,8 +1305,7 @@ def bold_mask_anatomical_based(wf, cfg, strat_pool, pipe_num, opt=None):
)
def bold_mask_anatomical_resampled(wf, cfg, strat_pool, pipe_num, opt=None):
'''Resample anatomical brain mask in standard space to get BOLD brain mask in standard space
Adapted from DCAN Lab's BOLD mask method from the ABCD pipeline.
https://github.com/DCAN-Labs/DCAN-HCP/blob/master/fMRIVolume/scripts/OneStepResampling.sh#L121-L132
Adapted from `DCAN Lab's BOLD mask method from the ABCD pipeline <https://github.com/DCAN-Labs/DCAN-HCP/blob/master/fMRIVolume/scripts/OneStepResampling.sh#L121-L132>`_.
'''

# applywarp --rel --interp=spline -i ${T1wImage} -r ${ResampRefIm} --premat=$FSLDIR/etc/flirtsch/ident.mat -o ${WD}/${T1wImageFile}.${FinalfMRIResolution}
Expand Down Expand Up @@ -1372,8 +1373,7 @@ def bold_mask_anatomical_resampled(wf, cfg, strat_pool, pipe_num, opt=None):
)
def bold_mask_ccs(wf, cfg, strat_pool, pipe_num, opt=None):
'''Generate the BOLD mask by basing it off of the anatomical brain.
Adapted from the BOLD mask method from the CCS pipeline.
https://github.com/TingsterX/CCS/blob/master/ccs_01_funcpreproc.sh#L89-L110
Adapted from `the BOLD mask method from the CCS pipeline <https://github.com/TingsterX/CCS/blob/master/ccs_01_funcpreproc.sh#L89-L110>`_.
'''

# Run 3dAutomask to generate func initial mask
Expand Down
12 changes: 7 additions & 5 deletions CPAC/generate_motion_statistics/generate_motion_statistics.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,6 @@
"""Functions to calculate motion statistics"""
import os
import sys
try:
from typing import Literal
except ImportError:
from typing_extensions import Literal
from typing import Optional
import numpy as np
import nibabel as nb
Expand All @@ -30,6 +26,7 @@
from CPAC.pipeline import nipype_pipeline_engine as pe
from CPAC.utils.interfaces.function import Function
from CPAC.utils.pytest import skipif
from CPAC.utils.typing import LITERAL


def motion_power_statistics(name='motion_stats',
Expand Down Expand Up @@ -351,9 +348,14 @@ def calculate_FD_P(in_file):
return out_file


@Function.sig_imports(['import os', 'import sys',
'from typing import Optional',
'import numpy as np',
'from CPAC.utils.pytest import skipif',
'from CPAC.utils.typing import LITERAL'])
@skipif(sys.version_info < (3, 10),
reason="Test requires Python 3.10 or higher")
def calculate_FD_J(in_file: str, calc_from: Literal['affine', 'rms'],
def calculate_FD_J(in_file: str, calc_from: LITERAL['affine', 'rms'],
center: Optional[np.ndarray] = None) -> str:
"""
Method to calculate framewise displacement as per Jenkinson et al. 2002
Expand Down
24 changes: 23 additions & 1 deletion CPAC/pipeline/nodeblock.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,32 @@ def __init__(
self.__name__ = func.__name__
self.__qualname__ = func.__qualname__
self.__annotations__ = func.__annotations__
self.__doc__ = func.__doc__
self.__doc__ = ''.join([_.replace(' ', '') for _ in [
func.__doc__, '', '', NodeBlockFunction.__call__.__doc__
] if _ is not None]).rstrip()

# all node block functions have this signature
def __call__(self, wf, cfg, strat_pool, pipe_num, opt=None):
"""
Parameters
----------
wf : ~nipype.pipeline.engine.workflows.Workflow
cfg : ~CPAC.utils.configuration.Configuration
strat_pool
pipe_num : int
opt : str, optional
Returns
-------
wf : ~nipype.pipeline.engine.workflows.Workflow
out : dict
"""
return self.func(wf, cfg, strat_pool, pipe_num, opt)

def legacy_nodeblock_dict(self):
Expand Down
4 changes: 2 additions & 2 deletions CPAC/pipeline/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,10 @@ def present_outputs(outputs: dict, keys: list) -> dict:
"""
Given an outputs dictionary and a list of output keys, returns
the subset of ``outputs`` that includes all keys in ``keys`` that are
present. I.e., ~:py:func:`CPAC.func_preproc.func_motion.motion_correct_connections`
present. I.e., :py:func:`~CPAC.func_preproc.func_motion.motion_correct_connections`
will have different items in its ``outputs`` dictionary at different
times depending on the ``motion_correction`` configuration;
~:py:func:`CPAC.func_preproc.func_motion.func_motion_estimates` can
:py:func:`~CPAC.func_preproc.func_motion.func_motion_estimates` can
then wrap that ``outputs`` in this function and provide a list of
keys of the desired outputs to include, if they are present in the
provided ``outputs`` dictionary, eliminating the need for multiple
Expand Down
16 changes: 8 additions & 8 deletions CPAC/qc/qcmetrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@ def dc(input1, input2):
"""
input1 = nb.load(input1).get_fdata()
input2 = nb.load(input2).get_fdata()
input1 = np.atleast_1d(input1.astype(np.bool))
input2 = np.atleast_1d(input2.astype(np.bool))
input1 = np.atleast_1d(input1.astype(bool))
input2 = np.atleast_1d(input2.astype(bool))

intersection = np.count_nonzero(input1 & input2)

Expand Down Expand Up @@ -95,8 +95,8 @@ def jc(input1, input2):
"""
input1 = nb.load(input1).get_fdata()
input2 = nb.load(input2).get_fdata()
input1 = np.atleast_1d(input1.astype(np.bool))
input2 = np.atleast_1d(input2.astype(np.bool))
input1 = np.atleast_1d(input1.astype(bool))
input2 = np.atleast_1d(input2.astype(bool))

intersection = np.count_nonzero(input1 & input2)
union = np.count_nonzero(input1 | input2)
Expand All @@ -110,8 +110,8 @@ def crosscorr(input1, input2):
r"""cross correlation: compute cross correction bewteen input masks"""
input1 = nb.load(input1).get_fdata()
input2 = nb.load(input2).get_fdata()
input1 = np.atleast_1d(input1.astype(np.bool)).flatten()
input2 = np.atleast_1d(input2.astype(np.bool)).flatten()
input1 = np.atleast_1d(input1.astype(bool)).flatten()
input2 = np.atleast_1d(input2.astype(bool)).flatten()
cc = np.corrcoef(input1, input2)[0][1]
return cc

Expand All @@ -120,8 +120,8 @@ def coverage(input1, input2):
"""Estimate the coverage between two masks."""
input1 = nb.load(input1).get_fdata()
input2 = nb.load(input2).get_fdata()
input1 = np.atleast_1d(input1.astype(np.bool))
input2 = np.atleast_1d(input2.astype(np.bool))
input1 = np.atleast_1d(input1.astype(bool))
input2 = np.atleast_1d(input2.astype(bool))
intsec = np.count_nonzero(input1 & input2)
if np.sum(input1) > np.sum(input2):
smallv = np.sum(input2)
Expand Down
16 changes: 8 additions & 8 deletions CPAC/qc/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -1262,8 +1262,8 @@ def dc(input1, input2):
-----
This is a real metric.
"""
input1 = numpy.atleast_1d(input1.astype(numpy.bool))
input2 = numpy.atleast_1d(input2.astype(numpy.bool))
input1 = numpy.atleast_1d(input1.astype(bool))
input2 = numpy.atleast_1d(input2.astype(bool))

intersection = numpy.count_nonzero(input1 & input2)

Expand Down Expand Up @@ -1303,8 +1303,8 @@ def jc(input1, input2):
-----
This is a real metric.
"""
input1 = numpy.atleast_1d(input1.astype(numpy.bool))
input2 = numpy.atleast_1d(input2.astype(numpy.bool))
input1 = numpy.atleast_1d(input1.astype(bool))
input2 = numpy.atleast_1d(input2.astype(bool))

intersection = numpy.count_nonzero(input1 & input2)
union = numpy.count_nonzero(input1 | input2)
Expand All @@ -1320,8 +1320,8 @@ def crosscorr(input1,input2):
computer compute cross correction bewteen input mask
"""

input1 = numpy.atleast_1d(input1.astype(numpy.bool))
input2 = numpy.atleast_1d(input2.astype(numpy.bool))
input1 = numpy.atleast_1d(input1.astype(bool))
input2 = numpy.atleast_1d(input2.astype(bool))

from scipy.stats.stats import pearsonr
cc=pearsonr(input1,input2)
Expand All @@ -1331,8 +1331,8 @@ def coverage(input1,input2):
"""
estimate the coverage between two mask
"""
input1 = numpy.atleast_1d(input1.astype(numpy.bool))
input2 = numpy.atleast_1d(input2.astype(numpy.bool))
input1 = numpy.atleast_1d(input1.astype(bool))
input2 = numpy.atleast_1d(input2.astype(bool))

intsec=numpy.count_nonzero(input1 & input2)
if numpy.sum(input1)> numpy.sum(input2):
Expand Down
10 changes: 6 additions & 4 deletions CPAC/surface/tests/test_installation.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,12 @@
from CPAC.utils.monitoring.custom_logging import log_subprocess


@pytest.mark.parametrize("executable", ["bc", "csh"])
@pytest.mark.skipif("FREESURFER_HOME" not in os.environ or
not os.path.exists(os.environ['FREESURFER_HOME']),
reason="We don't need bc if we don't have FreeSurfer.")
def test_bc():
"""Make sure ``bc`` is installed"""
_, exit_code = log_subprocess(['bc', '--version'])
reason="We don't need these dependencies if we don't"
"have FreeSurfer.")
def test_executable(executable):
"""Make sure executable is installed"""
_, exit_code = log_subprocess([executable, "--version"])
assert exit_code == 0
Loading

0 comments on commit 68ee445

Please sign in to comment.