[MARKER]: (f)ALFF #35
|
|
@ -174,7 +174,14 @@ Available
|
||||||
- Calculate regional homogeneity over spheres placed on coordinates
|
- Calculate regional homogeneity over spheres placed on coordinates
|
||||||
- Done
|
- Done
|
||||||
- 0.0.1
|
- 0.0.1
|
||||||
|
* - :class:`junifer.markers.AmplitudeLowFrequencyFluctuationParcels`
|
||||||
|
- Calculate (f)ALFF and aggregate using parcellations
|
||||||
|
- Done
|
||||||
|
- 0.0.1
|
||||||
|
* - :class:`junifer.markers.AmplitudeLowFrequencyFluctuationSpheres`
|
||||||
|
- Calculate (f)ALFF and aggregate using spheres placed on coordinates
|
||||||
|
- Done
|
||||||
|
- 0.0.1
|
||||||
|
|
||||||
Planned
|
Planned
|
||||||
~~~~~~~
|
~~~~~~~
|
||||||
|
|
@ -189,9 +196,6 @@ Planned
|
||||||
* - Connectedness
|
* - Connectedness
|
||||||
- Compute connectedness
|
- Compute connectedness
|
||||||
- :gh:`34`
|
- :gh:`34`
|
||||||
* - ALFF and (f)ALFF
|
|
||||||
- Detect amplitude of low-frequency fluctuation (ALFF) for resting-state fMRI
|
|
||||||
- :gh:`35`
|
|
||||||
* - Permutation entropy, Range entropy, Multiscale entropy and Hurst exponent
|
* - Permutation entropy, Range entropy, Multiscale entropy and Hurst exponent
|
||||||
- Calculate Permutation entropy, Range entropy, Multiscale entropy and Hurst exponent
|
- Calculate Permutation entropy, Range entropy, Multiscale entropy and Hurst exponent
|
||||||
- :gh:`61`
|
- :gh:`61`
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,8 @@ Enhancements
|
||||||
|
|
||||||
- Implement :class:`junifer.markers.ReHoParcels` and :class:`junifer.markers.ReHoSpheres` markers (:gh:`36` by `Synchon Mandal`_).
|
- Implement :class:`junifer.markers.ReHoParcels` and :class:`junifer.markers.ReHoSpheres` markers (:gh:`36` by `Synchon Mandal`_).
|
||||||
|
|
||||||
|
- Implement :class:`junifer.markers.AmplitudeLowFrequencyFluctuationParcels` and :class:`junifer.markers.AmplitudeLowFrequencyFluctuationSpheres` markers (:gh:`35` by `Fede Raimondo`_).
|
||||||
|
|
||||||
Bugs
|
Bugs
|
||||||
~~~~
|
~~~~
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -155,6 +155,8 @@ numpydoc_xref_ignore = {
|
||||||
"class",
|
"class",
|
||||||
"objects",
|
"objects",
|
||||||
"Engine",
|
"Engine",
|
||||||
|
"positive",
|
||||||
|
"negative",
|
||||||
}
|
}
|
||||||
# numpydoc_validation_checks = {
|
# numpydoc_validation_checks = {
|
||||||
# "all",
|
# "all",
|
||||||
|
|
|
||||||
3
junifer/api/res/afni/3dRSFC
Executable file
|
|
@ -0,0 +1,3 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
run_afni_docker.sh 3dRSFC "$@"
|
||||||
|
|
@ -13,3 +13,7 @@ from .functional_connectivity_spheres import FunctionalConnectivitySpheres
|
||||||
from .parcel_aggregation import ParcelAggregation
|
from .parcel_aggregation import ParcelAggregation
|
||||||
from .sphere_aggregation import SphereAggregation
|
from .sphere_aggregation import SphereAggregation
|
||||||
from .reho import ReHoParcels, ReHoSpheres
|
from .reho import ReHoParcels, ReHoSpheres
|
||||||
|
from .falff import (
|
||||||
|
AmplitudeLowFrequencyFluctuationParcels,
|
||||||
|
AmplitudeLowFrequencyFluctuationSpheres,
|
||||||
|
)
|
||||||
|
|
|
||||||
7
junifer/markers/falff/__init__.py
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
"""Provide imports for falff sub-package."""
|
||||||
|
|
||||||
|
# Authors: Federico Raimondo <f.raimondo@fz-juelich.de>
|
||||||
|
# License: AGPL
|
||||||
|
|
||||||
|
from .falff_parcels import AmplitudeLowFrequencyFluctuationParcels
|
||||||
|
from .falff_spheres import AmplitudeLowFrequencyFluctuationSpheres
|
||||||
180
junifer/markers/falff/falff_base.py
Normal file
|
|
@ -0,0 +1,180 @@
|
||||||
|
"""Provide abstract class for computing fALFF."""
|
||||||
|
|
|||||||
|
|
||||||
|
# Authors: Federico Raimondo <f.raimondo@fz-juelich.de>
|
||||||
|
# Amir Omidvarnia <a.omidvarnia@fz-juelich.de>
|
||||||
|
# Kaustubh R. Patil <k.patil@fz-juelich.de>
|
||||||
|
# License: AGPL
|
||||||
|
|
||||||
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
|
from abc import abstractmethod
|
||||||
|
|
||||||
|
from ..base import BaseMarker
|
||||||
|
from .falff_estimator import AmplitudeLowFrequencyFluctuationEstimator
|
||||||
|
from ...utils.logging import raise_error
|
||||||
|
|
||||||
|
|
||||||
|
class AmplitudeLowFrequencyFluctuationBase(BaseMarker):
|
||||||
|
"""Base class for (fractional) Amplitude Low Frequency Fluctuation.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
fractional : bool
|
||||||
|
Whether to compute fractional ALFF.
|
||||||
|
highpass : positive float
|
||||||
|
Highpass cutoff frequency.
|
||||||
|
lowpass : positive float
|
||||||
|
Lowpass cutoff frequency.
|
||||||
|
tr : positive float, optional
|
||||||
|
The Repetition Time of the BOLD data. If None, will extract
|
||||||
|
the TR from NIFTI header (default None).
|
||||||
|
use_afni : bool, optional
|
||||||
|
Whether to use AFNI for computing. If None, will use AFNI only
|
||||||
|
if available (default None).
|
||||||
|
name : str, optional
|
||||||
|
The name of the marker. If None, it will use the class name
|
||||||
|
(default None).
|
||||||
|
Please add a newline after this. Please add a newline after this.
done done
|
|||||||
|
|
||||||
|
Notes
|
||||||
|
-----
|
||||||
|
The `tr` parameter is crucial for the correctness of fALFF/ALFF
|
||||||
|
computation. If a dataset is correctly preprocessed, the TR should be
|
||||||
|
extracted from the NIFTI without any issue. However, it has been
|
||||||
|
reported that some preprocessed data might not have the correct TR in
|
||||||
|
the NIFTI header.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
_EXT_DEPENDENCIES = [
|
||||||
|
{
|
||||||
|
"name": "afni",
|
||||||
|
"optional": True,
|
||||||
|
"commands": ["3dRSFC", "3dAFNItoNIFTI"],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
fractional: bool,
|
||||||
|
highpass: float,
|
||||||
|
lowpass: float,
|
||||||
|
tr: Optional[float] = None,
|
||||||
|
use_afni: Optional[bool] = None,
|
||||||
|
name: Optional[str] = None,
|
||||||
|
) -> None:
|
||||||
|
if highpass <= 0:
|
||||||
|
raise_error("Highpass must be positive")
|
||||||
|
if lowpass <= 0:
|
||||||
|
raise_error("Lowpass must be positive")
|
||||||
|
if highpass >= lowpass:
|
||||||
|
raise_error("Highpass must be lower than lowpass")
|
||||||
|
self.highpass = highpass
|
||||||
|
self.lowpass = lowpass
|
||||||
|
self.tr = tr
|
||||||
|
self.use_afni = use_afni
|
||||||
|
self.fractional = fractional
|
||||||
|
|
||||||
|
# Create a name based on the class name if none is provided
|
||||||
|
if name is None:
|
||||||
|
suffix = "_fractional" if fractional else ""
|
||||||
|
name = f"{self.__class__.__name__}{suffix}"
|
||||||
|
super().__init__(on="BOLD", name=name)
|
||||||
|
|
||||||
|
def get_valid_inputs(self) -> List[str]:
|
||||||
|
"""Get valid data types for input.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
list of str
|
||||||
|
The list of data types that can be used as input for this marker.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return ["BOLD"]
|
||||||
|
|
||||||
|
def get_output_type(self, input_type: str) -> str:
|
||||||
|
"""Get output type.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
input_type : str
|
||||||
|
The data type input to the marker.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
str
|
||||||
|
The storage type output by the marker.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return "table"
|
||||||
|
|
||||||
|
def compute(
|
||||||
|
self,
|
||||||
|
input: Dict[str, Dict],
|
||||||
|
extra_input: Optional[Dict] = None,
|
||||||
|
) -> Dict:
|
||||||
|
"""Compute.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
input : dict
|
||||||
|
A single input from the pipeline data object in which to compute
|
||||||
|
the marker.
|
||||||
|
extra_input : dict, optional
|
||||||
|
The other fields in the pipeline data object. Useful for accessing
|
||||||
|
other data kind that needs to be used in the computation. For
|
||||||
|
example, the functional connectivity markers can make use of the
|
||||||
|
confounds if available (default None).
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
dict
|
||||||
|
The computed result as dictionary. This will be either returned
|
||||||
|
to the user or stored in the storage by calling the store method
|
||||||
|
with this as a parameter. The dictionary has the following keys:
|
||||||
|
|
||||||
|
* ``data`` : the actual computed values as a numpy.ndarray
|
||||||
|
* ``columns`` : the column labels for the computed values as a list
|
||||||
|
* ``row_names`` (if more than one row is present in data): "scan"
|
||||||
|
|
||||||
|
"""
|
||||||
|
if self.use_afni is None:
|
||||||
|
raise_error(
|
||||||
|
"Parameter `use_afni` must be set to True or False in order "
|
||||||
|
"to compute this marker. It is currently set to None (default "
|
||||||
|
"behaviour). This is intended to be for auto-detection. In "
|
||||||
|
"order for that to happen, please call the `validate` method "
|
||||||
|
"before calling the `compute` method."
|
||||||
|
)
|
||||||
|
|
||||||
|
estimator = AmplitudeLowFrequencyFluctuationEstimator()
|
||||||
|
|
||||||
|
alff, falff = estimator.fit_transform(
|
||||||
|
use_afni=self.use_afni,
|
||||||
|
input_data=input,
|
||||||
|
highpass=self.highpass,
|
||||||
|
lowpass=self.lowpass,
|
||||||
|
tr=self.tr,
|
||||||
|
)
|
||||||
|
post_data = falff if self.fractional else alff
|
||||||
|
|
||||||
|
post_input = {
|
||||||
|
"data": post_data,
|
||||||
|
"path": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
out = self._postprocess(post_input)
|
||||||
|
|
||||||
|
return out
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def _postprocess(self, input: Dict) -> Dict:
|
||||||
|
"""Postprocess the output of the estimator.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
input : dict
|
||||||
|
The output of the estimator. It must have the following
|
||||||
|
"""
|
||||||
|
raise_error(
|
||||||
|
"_postprocess must be implemented", klass=NotImplementedError
|
||||||
|
)
|
||||||
338
junifer/markers/falff/falff_estimator.py
Normal file
|
|
@ -0,0 +1,338 @@
|
||||||
|
"""Provide estimator class for (f)ALFF."""
|
||||||
|
The docstring needs to be updated. The docstring needs to be updated.
`highpass : positive float`?
`lowpass : positive float`?
`highpass : positive float`?
`lowpass : positive float`?
`tr : positive float, optional`
`tr : positive float, optional`?
`highpass : positive float`?
`lowpass : positive float`?
`tr : positive float, optional`?
`highpass : positive float`?
`lowpass : positive float`?
`tr : positive float, optional`?
|
|||||||
|
|
||||||
|
# Authors: Synchon Mandal <s.mandal@fz-juelich.de>
|
||||||
|
# Federico Raimondo <f.raimondo@fz-juelich.de>
|
||||||
|
# License: AGPL
|
||||||
|
|
||||||
|
import typing
|
||||||
|
from typing import TYPE_CHECKING, Any, Dict, Tuple, Union, Optional
|
||||||
|
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
from functools import lru_cache
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import nibabel as nib
|
||||||
|
import numpy as np
|
||||||
|
from scipy.fft import fft, fftfreq
|
||||||
|
|
||||||
|
from nilearn import image as nimg
|
||||||
|
|
||||||
|
from ...utils import logger, raise_error
|
||||||
|
from ..utils import singleton
|
||||||
|
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from nibabel import Nifti1Image, Nifti2Image
|
||||||
|
|
||||||
|
|
||||||
|
@singleton
|
||||||
|
class AmplitudeLowFrequencyFluctuationEstimator:
|
||||||
|
"""Estimator class for AmplitudeLowFrequencyFluctuationBase.
|
||||||
|
|
||||||
|
This class is a singleton and is used for efficient computation of fALFF,
|
||||||
|
by caching the voxel-wise ALFF map for a given set of file path and
|
||||||
|
computation parameters.
|
||||||
|
|
||||||
|
.. warning:: This class can only be used via
|
||||||
|
:class:`junifer.markers.falff.AmplitudeLowFrequencyFluctuationBase`
|
||||||
|
as it serves a specific purpose.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
use_afni : bool
|
||||||
|
Whether to use afni for computation. If False, will use python.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self._file_path = None
|
||||||
|
# Create temporary directory for intermittent storage of assets during
|
||||||
|
# computation via afni's 3dReHo
|
||||||
|
self.temp_dir_path = Path(tempfile.mkdtemp())
|
||||||
|
|
||||||
|
def __del__(self) -> None:
|
||||||
|
"""Cleanup."""
|
||||||
|
print("Cleaning up temporary directory...")
|
||||||
|
# Delete temporary directory and ignore errors for read-only files
|
||||||
|
shutil.rmtree(self.temp_dir_path, ignore_errors=True)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _run_afni_cmd(cmd: str) -> None:
|
||||||
|
"""Run AFNI command.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
cmd : str
|
||||||
|
AFNI command to be executed.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
RuntimeError
|
||||||
|
If AFNI command fails.
|
||||||
|
"""
|
||||||
|
logger.info(f"AFNI command to be executed: {cmd}")
|
||||||
|
# TODO: Figure out how to capture stdout and stderr
|
||||||
|
process = subprocess.run(
|
||||||
|
cmd,
|
||||||
|
stdin=subprocess.DEVNULL,
|
||||||
|
# stdout=subprocess.STDOUT,
|
||||||
|
# stderr=subprocess.STDOUT,
|
||||||
|
shell=True,
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
if process.returncode == 0:
|
||||||
|
logger.info(
|
||||||
|
"AFNI command succeeded with the following output: "
|
||||||
|
f"{process.stdout}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise_error(
|
||||||
|
msg="AFNI command failed with the following error: "
|
||||||
|
f"{process.stdout}",
|
||||||
|
klass=RuntimeError,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _compute_alff_afni(
|
||||||
|
self,
|
||||||
|
data: Union["Nifti1Image", "Nifti2Image"],
|
||||||
|
highpass: float,
|
||||||
|
lowpass: float,
|
||||||
|
tr: Optional[float],
|
||||||
|
) -> Tuple["Nifti1Image", "Nifti1Image"]:
|
||||||
|
"""Compute ALFF map via afni's commands.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
data : 4D Niimg-like object
|
||||||
|
Images to process.
|
||||||
|
highpass : positive float
|
||||||
|
Highpass cutoff frequency.
|
||||||
|
lowpass : positive float
|
||||||
|
Lowpass cutoff frequency.
|
||||||
|
tr : positive float, optional
|
||||||
|
The Repetition Time of the BOLD data.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
alff: Niimg-like object
|
||||||
|
ALFF map.
|
||||||
|
falff: Niimg-like object
|
||||||
|
fALFF map.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
RuntimeError
|
||||||
|
If the AFNI commands fails due to some issues
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Save niimg to nii.gz
|
||||||
|
nifti_in_file_path = self.temp_dir_path / "input.nii"
|
||||||
|
nib.save(data, nifti_in_file_path)
|
||||||
|
|
||||||
|
params_suffix = f"_{highpass}_{lowpass}_{tr}"
|
||||||
|
alff_fname = self.temp_dir_path / f"alff{params_suffix}.nii"
|
||||||
|
falff_fname = self.temp_dir_path / f"falff{params_suffix}.nii"
|
||||||
|
|
||||||
|
# Use afni's 3dRSFC to compute ALFF and fALFF
|
||||||
|
falff_afni_out_path_prefix = self.temp_dir_path / "temp_falff"
|
||||||
|
|
||||||
|
bp_cmd = (
|
||||||
|
"3dRSFC "
|
||||||
|
f"-prefix {falff_afni_out_path_prefix.resolve()} "
|
||||||
|
f"-input {nifti_in_file_path.resolve()} "
|
||||||
|
f"-band {highpass} {lowpass} "
|
||||||
|
"-no_rsfa -nosat -nodetrend "
|
||||||
|
)
|
||||||
|
if tr is not None:
|
||||||
|
bp_cmd += f"-dt {tr} "
|
||||||
|
self._run_afni_cmd(bp_cmd)
|
||||||
|
|
||||||
|
# Convert afni's output to nifti
|
||||||
|
convert_cmd = (
|
||||||
|
"3dAFNItoNIFTI "
|
||||||
|
f"-prefix {alff_fname.resolve()} "
|
||||||
|
f"{falff_afni_out_path_prefix}_ALFF+tlrc.BRIK "
|
||||||
|
)
|
||||||
|
self._run_afni_cmd(convert_cmd)
|
||||||
|
|
||||||
|
convert_cmd = (
|
||||||
|
"3dAFNItoNIFTI "
|
||||||
|
f"-prefix {falff_fname.resolve()} "
|
||||||
|
f"{falff_afni_out_path_prefix}_fALFF+tlrc.BRIK "
|
||||||
|
)
|
||||||
|
self._run_afni_cmd(convert_cmd)
|
||||||
|
|
||||||
|
# Cleanup intermediate files
|
||||||
|
for fname in self.temp_dir_path.glob("temp_*"):
|
||||||
|
fname.unlink()
|
||||||
|
|
||||||
|
# Load niftis
|
||||||
|
alff_img = nib.load(alff_fname)
|
||||||
|
falff_img = nib.load(falff_fname)
|
||||||
|
|
||||||
|
return alff_img, falff_img
|
||||||
|
|
||||||
|
def _compute_alff_python(
|
||||||
|
self,
|
||||||
|
data: Union["Nifti1Image", "Nifti2Image"],
|
||||||
|
highpass: float,
|
||||||
|
lowpass: float,
|
||||||
|
tr: Optional[float],
|
||||||
|
) -> Tuple["Nifti1Image", "Nifti1Image"]:
|
||||||
|
"""Compute (f)ALFF map.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
data : 4D Niimg-like object
|
||||||
|
Images to process.
|
||||||
|
highpass : positive float
|
||||||
|
Highpass cutoff frequency.
|
||||||
|
lowpass : positive float
|
||||||
|
Lowpass cutoff frequency.
|
||||||
|
tr : positive float, optional
|
||||||
|
The Repetition Time of the BOLD data.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
alff: Niimg-like object
|
||||||
|
ALFF map.
|
||||||
|
falff: Niimg-like object
|
||||||
|
fALFF map.
|
||||||
|
"""
|
||||||
|
timeseries = data.get_fdata().copy()
|
||||||
|
if tr is None:
|
||||||
|
tr = float(data.header["pixdim"][4]) # type: ignore
|
||||||
|
logger.info(f"TR Not provided, using TR from header = {tr}")
|
||||||
|
# bandpass the data within the lowpass and highpass cutoff freqs
|
||||||
|
|
||||||
|
ts_fft = fft(timeseries, axis=-1)
|
||||||
|
ts_fft = typing.cast(np.ndarray, ts_fft)
|
||||||
|
fft_freqs = np.abs(fftfreq(timeseries.shape[-1], tr))
|
||||||
|
|
||||||
|
dFreq = fft_freqs[1] - fft_freqs[0]
|
||||||
|
nyquist = np.max(fft_freqs)
|
||||||
|
nfft = len(fft_freqs)
|
||||||
|
logger.info(
|
||||||
|
f"FFT: nfft = {nfft}, dFreq = {dFreq}, nyquist = {nyquist}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# First compute the denominator on the broadband signal
|
||||||
|
all_freq_mask = fft_freqs > 0
|
||||||
|
denominator = np.sum(np.abs(ts_fft[..., all_freq_mask]), axis=-1)
|
||||||
|
|
||||||
|
# Compute the numerator on the bandpassed signal
|
||||||
|
freq_mask = np.logical_and(fft_freqs > highpass, fft_freqs < lowpass)
|
||||||
|
# Compute ALFF
|
||||||
|
numerator = np.sum(np.abs(ts_fft[..., freq_mask]), axis=-1)
|
||||||
|
|
||||||
|
# Compute fALFF, but avoid division by zero
|
||||||
|
denom_mask = denominator <= 0.000001
|
||||||
|
denominator[denom_mask] = 1 # set to 1 to avoid division by zero
|
||||||
|
python_falff = np.divide(numerator, denominator)
|
||||||
|
# Set the values where denominator is zero to zero
|
||||||
|
python_falff[denom_mask] = 0
|
||||||
|
|
||||||
|
python_alff = numerator / np.sqrt(timeseries.shape[-1])
|
||||||
|
alff_img = nimg.new_img_like(data, python_alff)
|
||||||
|
falff_img = nimg.new_img_like(data, python_falff)
|
||||||
|
return alff_img, falff_img
|
||||||
|
|
||||||
|
@lru_cache(maxsize=None, typed=True) # noqa: B019
|
||||||
|
def _compute(
|
||||||
|
self,
|
||||||
|
use_afni: bool,
|
||||||
|
data: Union["Nifti1Image", "Nifti2Image"],
|
||||||
|
highpass: float,
|
||||||
|
lowpass: float,
|
||||||
|
tr: Optional[float],
|
||||||
|
) -> Tuple["Nifti1Image", "Nifti1Image"]:
|
||||||
|
"""Compute the ALFF map with memorization.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
use_afni : bool
|
||||||
|
Whether to use AFNI for computing.
|
||||||
|
data : 4D Niimg-like object
|
||||||
|
Images to process.
|
||||||
|
highpass : positive float
|
||||||
|
Highpass cutoff frequency.
|
||||||
|
lowpass : positive float
|
||||||
|
Lowpass cutoff frequency.
|
||||||
|
tr : positive float, optional
|
||||||
|
The Repetition Time of the BOLD data.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
alff: Niimg-like object
|
||||||
|
ALFF map.
|
||||||
|
falff: Niimg-like object
|
||||||
|
fALFF map.
|
||||||
|
"""
|
||||||
|
if use_afni:
|
||||||
|
output = self._compute_alff_afni(
|
||||||
|
data=data,
|
||||||
|
highpass=highpass,
|
||||||
|
lowpass=lowpass,
|
||||||
|
tr=tr,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
output = self._compute_alff_python(
|
||||||
|
data, highpass=highpass, lowpass=lowpass, tr=tr
|
||||||
|
)
|
||||||
|
return output
|
||||||
|
|
||||||
|
def fit_transform(
|
||||||
|
self,
|
||||||
|
use_afni: bool,
|
||||||
|
input_data: Dict[str, Any],
|
||||||
|
highpass: float,
|
||||||
|
lowpass: float,
|
||||||
|
tr: Optional[float],
|
||||||
|
) -> Tuple["Nifti1Image", "Nifti1Image"]:
|
||||||
|
"""Fit and transform for the estimator.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
use_afni : bool
|
||||||
|
Whether to use AFNI for computing.
|
||||||
|
input_data : dict
|
||||||
|
The BOLD data as dictionary.
|
||||||
|
highpass : positive float
|
||||||
|
Highpass cutoff frequency.
|
||||||
|
lowpass : positive float
|
||||||
|
Lowpass cutoff frequency.
|
||||||
|
tr : positive float, optional
|
||||||
|
The Repetition Time of the BOLD data.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
alff: Niimg-like object
|
||||||
|
ALFF map.
|
||||||
|
falff: Niimg-like object
|
||||||
|
fALFF map.
|
||||||
|
"""
|
||||||
|
bold_path = input_data["path"]
|
||||||
|
bold_data = input_data["data"]
|
||||||
|
# Clear cache if file path is different from when caching was done
|
||||||
|
if self._file_path != bold_path:
|
||||||
|
logger.info(f"Removing fALFF map cache at {self._file_path}.")
|
||||||
|
# Clear the cache
|
||||||
|
self._compute.cache_clear()
|
||||||
|
# Clear temporary directory files
|
||||||
|
for file_ in self.temp_dir_path.iterdir():
|
||||||
|
file_.unlink(missing_ok=True)
|
||||||
|
# Set the new file path
|
||||||
|
self._file_path = bold_path
|
||||||
|
else:
|
||||||
|
logger.info(f"Using fALFF map cache at {self._file_path}.")
|
||||||
|
# Compute
|
||||||
|
return self._compute(
|
||||||
|
use_afni=use_afni,
|
||||||
|
data=bold_data,
|
||||||
|
highpass=highpass,
|
||||||
|
lowpass=lowpass,
|
||||||
|
tr=tr,
|
||||||
|
)
|
||||||
126
junifer/markers/falff/falff_parcels.py
Normal file
|
|
@ -0,0 +1,126 @@
|
||||||
|
"""Provide class for computing fALFF on parcels."""
|
||||||
|
`highpass : positive float, optional`?
`lowpass : positive float, optional`?
`tr : positive float, optional`?
```
``tr``
```
Extra newline here, can be removed. Extra newline here, can be removed.
|
|||||||
|
|
||||||
|
# Authors: Federico Raimondo <f.raimondo@fz-juelich.de>
|
||||||
|
# Amir Omidvarnia <a.omidvarnia@fz-juelich.de>
|
||||||
|
# Kaustubh R. Patil <k.patil@fz-juelich.de>
|
||||||
|
# License: AGPL
|
||||||
|
|
||||||
|
from typing import Dict, List, Optional, Union
|
||||||
|
|
||||||
|
|
||||||
|
from ...api.decorators import register_marker
|
||||||
|
from .falff_base import AmplitudeLowFrequencyFluctuationBase
|
||||||
|
from .. import ParcelAggregation
|
||||||
|
|
||||||
|
|
||||||
|
@register_marker
|
||||||
|
class AmplitudeLowFrequencyFluctuationParcels(
|
||||||
|
AmplitudeLowFrequencyFluctuationBase
|
||||||
|
):
|
||||||
|
"""Class for computing fALFF/ALFF on parcels.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
parcellation : str or list of str
|
||||||
|
The name(s) of the parcellation(s). Check valid options by calling
|
||||||
|
:func:`junifer.data.parcellations.list_parcellations`.
|
||||||
|
fractional : bool
|
||||||
|
Whether to compute fractional ALFF.
|
||||||
|
highpass : positive float, optional
|
||||||
|
The highpass cutoff frequency for the bandpass filter (default 0.01).
|
||||||
|
lowpass : positive float, optional
|
||||||
|
The lowpass cutoff frequency for the bandpass filter (default 0.1).
|
||||||
|
tr : positive float, optional
|
||||||
|
The Repetition Time of the BOLD data. If None, will extract
|
||||||
|
the TR from NIFTI header (default None).
|
||||||
|
use_afni : bool, optional
|
||||||
|
Whether to use AFNI for computing. If None, will use AFNI only
|
||||||
|
if available (default None).
|
||||||
|
mask : str, optional
|
||||||
|
The name of the mask to apply to regions before extracting signals.
|
||||||
|
Check valid options by calling :func:`junifer.data.masks.list_masks`
|
||||||
|
(default None).
|
||||||
|
method : str, optional
|
||||||
|
The method to perform aggregation using. Check valid options in
|
||||||
|
:func:`junifer.stats.get_aggfunc_by_name` (default "mean").
|
||||||
|
method_params : dict, optional
|
||||||
|
Parameters to pass to the aggregation function. Check valid options in
|
||||||
|
:func:`junifer.stats.get_aggfunc_by_name`.
|
||||||
|
name : str, optional
|
||||||
|
The name of the marker. If None, will use the class name (default
|
||||||
|
None).
|
||||||
|
|
||||||
|
Notes
|
||||||
|
-----
|
||||||
|
The ``tr`` parameter is crucial for the correctness of fALFF/ALFF
|
||||||
|
computation. If a dataset is correctly preprocessed, the TR should be
|
||||||
|
extracted from the NIFTI without any issue. However, it has been
|
||||||
|
reported that some preprocessed data might not have the correct TR in
|
||||||
|
the NIFTI header.
|
||||||
|
|
||||||
|
ALFF/fALFF are computed using a bandpass butterworth filter. See
|
||||||
|
:func:`scipy.signal.butter` and :func:`scipy.signal.filtfilt` for more
|
||||||
|
details.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
parcellation: Union[str, List[str]],
|
||||||
|
fractional: bool,
|
||||||
|
highpass: float = 0.01,
|
||||||
|
lowpass: float = 0.1,
|
||||||
|
tr: Optional[float] = None,
|
||||||
|
use_afni: Optional[bool] = None,
|
||||||
|
mask: Optional[str] = None,
|
||||||
|
method: str = "mean",
|
||||||
|
method_params: Optional[Dict] = None,
|
||||||
|
name: Optional[str] = None,
|
||||||
|
) -> None:
|
||||||
|
self.parcellation = parcellation
|
||||||
|
self.mask = mask
|
||||||
|
self.method = method
|
||||||
|
self.method_params = method_params
|
||||||
|
super().__init__(
|
||||||
|
fractional=fractional,
|
||||||
|
highpass=highpass,
|
||||||
|
lowpass=lowpass,
|
||||||
|
tr=tr,
|
||||||
|
name=name,
|
||||||
|
use_afni=use_afni,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _postprocess(self, input: Dict) -> Dict:
|
||||||
|
"""Compute ALFF and fALFF.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
input : dict
|
||||||
|
A single input from the pipeline data object in which to compute
|
||||||
|
the marker.
|
||||||
|
extra_input : dict, optional
|
||||||
|
The other fields in the pipeline data object. Useful for accessing
|
||||||
|
other data kind that needs to be used in the computation. For
|
||||||
|
example, the functional connectivity markers can make use of the
|
||||||
|
confounds if available (default None).
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
dict
|
||||||
|
The computed ALFF as dictionary. The dictionary has the following
|
||||||
|
keys:
|
||||||
|
|
||||||
|
* ``data`` : the actual computed values as a numpy.ndarray
|
||||||
|
* ``columns`` : the column labels for the computed values as a list
|
||||||
|
"""
|
||||||
|
pa = ParcelAggregation(
|
||||||
|
parcellation=self.parcellation,
|
||||||
|
method=self.method,
|
||||||
|
method_params=self.method_params,
|
||||||
|
mask=self.mask,
|
||||||
|
on="fALFF",
|
||||||
|
)
|
||||||
|
|
||||||
|
# get the 2D timeseries after parcel aggregation
|
||||||
|
out = pa.compute(input)
|
||||||
|
|
||||||
|
return out
|
||||||
134
junifer/markers/falff/falff_spheres.py
Normal file
|
|
@ -0,0 +1,134 @@
|
||||||
|
"""Provide class for computing fALFF on spheres."""
|
||||||
|
`highpass : positive float, optional`?
`lowpass : positive float, optional`?
`tr : positive float, optional`?
Extra newline here, can be removed. Extra newline here, can be removed.
|
|||||||
|
|
||||||
|
# Authors: Federico Raimondo <f.raimondo@fz-juelich.de>
|
||||||
|
# Amir Omidvarnia <a.omidvarnia@fz-juelich.de>
|
||||||
|
# Kaustubh R. Patil <k.patil@fz-juelich.de>
|
||||||
|
# License: AGPL
|
||||||
|
|
||||||
|
from typing import Dict, Optional
|
||||||
|
|
||||||
|
|
||||||
|
from ...api.decorators import register_marker
|
||||||
|
from .falff_base import AmplitudeLowFrequencyFluctuationBase
|
||||||
|
from .. import SphereAggregation
|
||||||
|
|
||||||
|
|
||||||
|
@register_marker
|
||||||
|
class AmplitudeLowFrequencyFluctuationSpheres(
|
||||||
|
AmplitudeLowFrequencyFluctuationBase
|
||||||
|
):
|
||||||
|
"""Class for computing fALFF/ALFF on spheres.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
coords : str
|
||||||
|
The name of the coordinates list to use. See
|
||||||
|
:func:`junifer.data.coordinates.list_coordinates` for options.
|
||||||
|
radius : float, optional
|
||||||
|
The radius of the sphere in mm. If None, the signal will be extracted
|
||||||
|
from a single voxel. See :class:`nilearn.maskers.NiftiSpheresMasker`
|
||||||
|
for more information (default None).
|
||||||
|
fractional : bool
|
||||||
|
Whether to compute fractional ALFF.
|
||||||
|
highpass : positive float, optional
|
||||||
|
The highpass cutoff frequency for the bandpass filter (default 0.01).
|
||||||
|
lowpass : positive float, optional
|
||||||
|
The lowpass cutoff frequency for the bandpass filter (default 0.1).
|
||||||
|
tr : positive float, optional
|
||||||
|
The Repetition Time of the BOLD data. If None, will extract
|
||||||
|
the TR from NIFTI header (default None).
|
||||||
|
use_afni : bool, optional
|
||||||
|
Whether to use AFNI for computing. If None, will use AFNI only
|
||||||
|
if available (default None).
|
||||||
|
mask : str, optional
|
||||||
|
The name of the mask to apply to regions before extracting signals.
|
||||||
|
Check valid options by calling :func:`junifer.data.masks.list_masks`
|
||||||
|
(default None).
|
||||||
|
method : str, optional
|
||||||
|
The method to perform aggregation using. Check valid options in
|
||||||
|
:func:`junifer.stats.get_aggfunc_by_name` (default "mean").
|
||||||
|
method_params : dict, optional
|
||||||
|
Parameters to pass to the aggregation function. Check valid options in
|
||||||
|
:func:`junifer.stats.get_aggfunc_by_name`.
|
||||||
|
name : str, optional
|
||||||
|
The name of the marker. If None, will use the class name (default
|
||||||
|
None).
|
||||||
|
|
||||||
|
Notes
|
||||||
|
-----
|
||||||
|
The ``tr`` parameter is crucial for the correctness of fALFF/ALFF
|
||||||
|
computation. If a dataset is correctly preprocessed, the TR should be
|
||||||
|
extracted from the NIFTI without any issue. However, it has been
|
||||||
|
reported that some preprocessed data might not have the correct TR in
|
||||||
|
the NIFTI header.
|
||||||
|
|
||||||
|
ALFF/fALFF are computed using a bandpass butterworth filter. See
|
||||||
|
:func:`scipy.signal.butter` and :func:`scipy.signal.filtfilt` for more
|
||||||
|
details.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
coords: str,
|
||||||
|
fractional: bool,
|
||||||
|
radius: Optional[float] = None,
|
||||||
|
highpass: float = 0.01,
|
||||||
|
lowpass: float = 0.1,
|
||||||
|
tr: Optional[float] = None,
|
||||||
|
use_afni: Optional[bool] = None,
|
||||||
|
mask: Optional[str] = None,
|
||||||
|
method: str = "mean",
|
||||||
|
method_params: Optional[Dict] = None,
|
||||||
|
name: Optional[str] = None,
|
||||||
|
) -> None:
|
||||||
|
self.coords = coords
|
||||||
|
self.radius = radius
|
||||||
|
self.mask = mask
|
||||||
|
self.method = method
|
||||||
|
self.method_params = method_params
|
||||||
|
super().__init__(
|
||||||
|
fractional=fractional,
|
||||||
|
highpass=highpass,
|
||||||
|
lowpass=lowpass,
|
||||||
|
tr=tr,
|
||||||
|
name=name,
|
||||||
|
use_afni=use_afni,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _postprocess(self, input: Dict) -> Dict:
|
||||||
|
"""Compute ALFF and fALFF.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
input : dict
|
||||||
|
A single input from the pipeline data object in which to compute
|
||||||
|
the marker.
|
||||||
|
extra_input : dict, optional
|
||||||
|
The other fields in the pipeline data object. Useful for accessing
|
||||||
|
other data kind that needs to be used in the computation. For
|
||||||
|
example, the functional connectivity markers can make use of the
|
||||||
|
confounds if available (default None).
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
dict
|
||||||
|
The computed ALFF as dictionary. The dictionary has the following
|
||||||
|
keys:
|
||||||
|
|
||||||
|
* ``data`` : the actual computed values as a numpy.ndarray
|
||||||
|
* ``columns`` : the column labels for the computed values as a list
|
||||||
|
Needs a newline after the listing. Needs a newline after the listing.
|
|||||||
|
|
||||||
|
"""
|
||||||
|
pa = SphereAggregation(
|
||||||
|
coords=self.coords,
|
||||||
|
radius=self.radius,
|
||||||
|
method=self.method,
|
||||||
|
method_params=self.method_params,
|
||||||
|
mask=self.mask,
|
||||||
|
on="fALFF",
|
||||||
|
)
|
||||||
|
|
||||||
|
# get the 2D timeseries after parcel aggregation
|
||||||
|
out = pa.compute(input)
|
||||||
|
|
||||||
|
return out
|
||||||
241
junifer/markers/falff/tests/test_falff_estimator.py
Normal file
|
|
@ -0,0 +1,241 @@
|
||||||
|
"""Provide test for (f)ALFF estimator."""
|
||||||
|
|
||||||
|
# Authors: Federico Raimondo <f.raimondo@fz-juelich.de>
|
||||||
|
# License: AGPL
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import time
|
||||||
|
from scipy.stats import pearsonr
|
||||||
|
from nibabel import Nifti1Image
|
||||||
|
|
||||||
|
from junifer.datareader import DefaultDataReader
|
||||||
|
from junifer.markers.falff.falff_estimator import (
|
||||||
|
AmplitudeLowFrequencyFluctuationEstimator,
|
||||||
|
)
|
||||||
|
from junifer.testing.datagrabbers import (
|
||||||
|
PartlyCloudyTestingDataGrabber,
|
||||||
|
)
|
||||||
|
from junifer.pipeline.utils import _check_afni
|
||||||
|
from junifer.utils import logger
|
||||||
|
|
||||||
|
|
||||||
|
def test_AmplitudeLowFrequencyFluctuationEstimator_cache_python() -> None:
|
||||||
|
"""Test that the cache works properly when using python."""
|
||||||
|
with PartlyCloudyTestingDataGrabber() as dg:
|
||||||
|
input = dg["sub-01"]
|
||||||
|
|
||||||
|
input = DefaultDataReader().fit_transform(input)
|
||||||
|
|
||||||
|
estimator = AmplitudeLowFrequencyFluctuationEstimator()
|
||||||
|
start_time = time.time()
|
||||||
|
alff, falff = estimator.fit_transform(
|
||||||
|
use_afni=False,
|
||||||
|
input_data=input["BOLD"],
|
||||||
|
highpass=0.01,
|
||||||
|
lowpass=0.1,
|
||||||
|
tr=None,
|
||||||
|
)
|
||||||
|
first_time = time.time() - start_time
|
||||||
|
logger.info(f"ALFF Estimator First time: {first_time}")
|
||||||
|
assert isinstance(alff, Nifti1Image)
|
||||||
|
assert isinstance(falff, Nifti1Image)
|
||||||
|
n_files = len([x for x in estimator.temp_dir_path.glob("*")])
|
||||||
|
assert n_files == 0 # no files in python
|
||||||
|
|
||||||
|
# Now fit again, should be faster
|
||||||
|
start_time = time.time()
|
||||||
|
alff, falff = estimator.fit_transform(
|
||||||
|
use_afni=False,
|
||||||
|
input_data=input["BOLD"],
|
||||||
|
highpass=0.01,
|
||||||
|
lowpass=0.1,
|
||||||
|
tr=None,
|
||||||
|
)
|
||||||
|
second_time = time.time() - start_time
|
||||||
|
logger.info(f"ALFF Estimator Second time: {second_time}")
|
||||||
|
assert second_time < (first_time / 1000)
|
||||||
|
n_files = len([x for x in estimator.temp_dir_path.glob("*")])
|
||||||
|
assert n_files == 0 # no files in python
|
||||||
|
|
||||||
|
# Now change a parameter, should compute again, without clearing the
|
||||||
|
# cache
|
||||||
|
start_time = time.time()
|
||||||
|
alff, falff = estimator.fit_transform(
|
||||||
|
use_afni=False,
|
||||||
|
input_data=input["BOLD"],
|
||||||
|
highpass=0.01,
|
||||||
|
lowpass=0.11,
|
||||||
|
tr=None,
|
||||||
|
)
|
||||||
|
third_time = time.time() - start_time
|
||||||
|
logger.info(f"ALFF Estimator Third time: {third_time}")
|
||||||
|
assert third_time > (first_time / 10)
|
||||||
|
n_files = len([x for x in estimator.temp_dir_path.glob("*")])
|
||||||
|
assert n_files == 0 # no files in python
|
||||||
|
|
||||||
|
# Now fit again with the previous params, should be fast
|
||||||
|
start_time = time.time()
|
||||||
|
alff, falff = estimator.fit_transform(
|
||||||
|
use_afni=False,
|
||||||
|
input_data=input["BOLD"],
|
||||||
|
highpass=0.01,
|
||||||
|
lowpass=0.1,
|
||||||
|
tr=None,
|
||||||
|
)
|
||||||
|
fourth = time.time() - start_time
|
||||||
|
logger.info(f"ALFF Estimator Fourth time: {fourth}")
|
||||||
|
assert fourth < (first_time / 1000)
|
||||||
|
n_files = len([x for x in estimator.temp_dir_path.glob("*")])
|
||||||
|
assert n_files == 0 # no files in python
|
||||||
|
|
||||||
|
# Now change the data, it should clear the cache
|
||||||
|
with PartlyCloudyTestingDataGrabber() as dg:
|
||||||
|
input = dg["sub-02"]
|
||||||
|
|
||||||
|
input = DefaultDataReader().fit_transform(input)
|
||||||
|
|
||||||
|
start_time = time.time()
|
||||||
|
alff, falff = estimator.fit_transform(
|
||||||
|
use_afni=False,
|
||||||
|
input_data=input["BOLD"],
|
||||||
|
highpass=0.01,
|
||||||
|
lowpass=0.1,
|
||||||
|
tr=None,
|
||||||
|
)
|
||||||
|
fifth = time.time() - start_time
|
||||||
|
logger.info(f"ALFF Estimator Fifth time: {fifth}")
|
||||||
|
assert fifth > (first_time / 10)
|
||||||
|
n_files = len([x for x in estimator.temp_dir_path.glob("*")])
|
||||||
|
assert n_files == 0 # no files in python
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(
|
||||||
|
_check_afni() is False, reason="requires afni to be in PATH"
|
||||||
|
)
|
||||||
|
def test_AmplitudeLowFrequencyFluctuationEstimator_cache_afni() -> None:
|
||||||
|
"""Test that the cache works properly when using afni."""
|
||||||
|
with PartlyCloudyTestingDataGrabber() as dg:
|
||||||
|
input = dg["sub-01"]
|
||||||
|
|
||||||
|
input = DefaultDataReader().fit_transform(input)
|
||||||
|
|
||||||
|
estimator = AmplitudeLowFrequencyFluctuationEstimator()
|
||||||
|
start_time = time.time()
|
||||||
|
alff, falff = estimator.fit_transform(
|
||||||
|
use_afni=True,
|
||||||
|
input_data=input["BOLD"],
|
||||||
|
highpass=0.01,
|
||||||
|
lowpass=0.1,
|
||||||
|
tr=None,
|
||||||
|
)
|
||||||
|
first_time = time.time() - start_time
|
||||||
|
logger.info(f"ALFF Estimator First time: {first_time}")
|
||||||
|
assert isinstance(alff, Nifti1Image)
|
||||||
|
assert isinstance(falff, Nifti1Image)
|
||||||
|
n_files = len([x for x in estimator.temp_dir_path.glob("*")])
|
||||||
|
assert n_files == 3 # input + alff + falff
|
||||||
|
|
||||||
|
# Now fit again, should be faster
|
||||||
|
start_time = time.time()
|
||||||
|
alff, falff = estimator.fit_transform(
|
||||||
|
use_afni=True,
|
||||||
|
input_data=input["BOLD"],
|
||||||
|
highpass=0.01,
|
||||||
|
lowpass=0.1,
|
||||||
|
tr=None,
|
||||||
|
)
|
||||||
|
second_time = time.time() - start_time
|
||||||
|
logger.info(f"ALFF Estimator Second time: {second_time}")
|
||||||
|
assert second_time < (first_time / 1000)
|
||||||
|
n_files = len([x for x in estimator.temp_dir_path.glob("*")])
|
||||||
|
assert n_files == 3 # input + alff + falff
|
||||||
|
|
||||||
|
# Now change a parameter, should compute again, without clearing the
|
||||||
|
# cache
|
||||||
|
start_time = time.time()
|
||||||
|
alff, falff = estimator.fit_transform(
|
||||||
|
use_afni=True,
|
||||||
|
input_data=input["BOLD"],
|
||||||
|
highpass=0.01,
|
||||||
|
lowpass=0.11,
|
||||||
|
tr=None,
|
||||||
|
)
|
||||||
|
third_time = time.time() - start_time
|
||||||
|
logger.info(f"ALFF Estimator Third time: {third_time}")
|
||||||
|
assert third_time > (first_time / 10)
|
||||||
|
n_files = len([x for x in estimator.temp_dir_path.glob("*")])
|
||||||
|
assert n_files == 5 # input + 2 * alff + 2 * falff
|
||||||
|
|
||||||
|
# Now fit again with the previous params, should be fast
|
||||||
|
start_time = time.time()
|
||||||
|
alff, falff = estimator.fit_transform(
|
||||||
|
use_afni=True,
|
||||||
|
input_data=input["BOLD"],
|
||||||
|
highpass=0.01,
|
||||||
|
lowpass=0.1,
|
||||||
|
tr=None,
|
||||||
|
)
|
||||||
|
fourth = time.time() - start_time
|
||||||
|
logger.info(f"ALFF Estimator Fourth time: {fourth}")
|
||||||
|
assert fourth < (first_time / 1000)
|
||||||
|
n_files = len([x for x in estimator.temp_dir_path.glob("*")])
|
||||||
|
assert n_files == 5 # input + 2 * alff + 2 * falff
|
||||||
|
|
||||||
|
# Now change the data, it should clear the cache
|
||||||
|
with PartlyCloudyTestingDataGrabber() as dg:
|
||||||
|
input = dg["sub-02"]
|
||||||
|
|
||||||
|
input = DefaultDataReader().fit_transform(input)
|
||||||
|
|
||||||
|
start_time = time.time()
|
||||||
|
alff, falff = estimator.fit_transform(
|
||||||
|
use_afni=True,
|
||||||
|
input_data=input["BOLD"],
|
||||||
|
highpass=0.01,
|
||||||
|
lowpass=0.1,
|
||||||
|
tr=None,
|
||||||
|
)
|
||||||
|
fifth = time.time() - start_time
|
||||||
|
logger.info(f"ALFF Estimator Fifth time: {fifth}")
|
||||||
|
assert fifth > (first_time / 10)
|
||||||
|
n_files = len([x for x in estimator.temp_dir_path.glob("*")])
|
||||||
|
assert n_files == 3 # input + alff + falff
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(
|
||||||
|
_check_afni() is False, reason="requires afni to be in PATH"
|
||||||
|
)
|
||||||
|
def test_AmplitudeLowFrequencyFluctuationEstimator_afni_vs_python() -> None:
|
||||||
|
"""Test that the cache works properly when using afni."""
|
||||||
|
Docstring needs to be updated. Docstring needs to be updated.
|
|||||||
|
with PartlyCloudyTestingDataGrabber() as dg:
|
||||||
|
input = dg["sub-01"]
|
||||||
|
|
||||||
|
input = DefaultDataReader().fit_transform(input)
|
||||||
|
estimator = AmplitudeLowFrequencyFluctuationEstimator()
|
||||||
|
|
||||||
|
# Use an arbitrary TR to test the AFNI vs Python implementation
|
||||||
|
afni_alff, afni_falff = estimator.fit_transform(
|
||||||
|
use_afni=True,
|
||||||
|
input_data=input["BOLD"],
|
||||||
|
highpass=0.01,
|
||||||
|
lowpass=0.1,
|
||||||
|
tr=2.5,
|
||||||
|
)
|
||||||
|
|
||||||
|
python_alff, python_falff = estimator.fit_transform(
|
||||||
|
use_afni=False,
|
||||||
|
input_data=input["BOLD"],
|
||||||
|
highpass=0.01,
|
||||||
|
lowpass=0.1,
|
||||||
|
tr=2.5,
|
||||||
|
)
|
||||||
|
|
||||||
|
r, _ = pearsonr(
|
||||||
|
afni_alff.get_fdata().flatten(), python_alff.get_fdata().flatten()
|
||||||
|
)
|
||||||
|
assert r > 0.99
|
||||||
|
|
||||||
|
r, _ = pearsonr(
|
||||||
|
afni_falff.get_fdata().flatten(), python_falff.get_fdata().flatten()
|
||||||
|
)
|
||||||
|
assert r > 0.99
|
||||||
157
junifer/markers/falff/tests/test_falff_parcels.py
Normal file
|
|
@ -0,0 +1,157 @@
|
||||||
|
"""Provide test for parcel-aggregated (f)ALFF."""
|
||||||
|
|
||||||
|
# Authors: Federico Raimondo <f.raimondo@fz-juelich.de>
|
||||||
|
# Synchon Mandal <s.mandal@fz-juelich.de>
|
||||||
|
# License: AGPL
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from numpy.testing import assert_array_equal
|
||||||
|
from scipy.stats import pearsonr
|
||||||
|
|
||||||
|
from junifer.datareader import DefaultDataReader
|
||||||
|
from junifer.markers.falff import AmplitudeLowFrequencyFluctuationParcels
|
||||||
|
from junifer.testing.datagrabbers import PartlyCloudyTestingDataGrabber
|
||||||
|
from junifer.pipeline.utils import _check_afni
|
||||||
|
from junifer.storage import SQLiteFeatureStorage
|
||||||
|
from junifer.utils import logger
|
||||||
|
|
||||||
|
|
||||||
|
As the parcellations used are same, maybe create a module-level variable and reuse it. As the parcellations used are same, maybe create a module-level variable and reuse it.
|
|||||||
|
_PARCELLATION = "Schaefer100x7"
|
||||||
|
|
||||||
|
|
||||||
|
def test_AmplitudeLowFrequencyFluctuationParcels_python() -> None:
|
||||||
|
"""Test AmplitudeLowFrequencyFluctuationParcels using python."""
|
||||||
|
# Get the SPM auditory data:
|
||||||
|
|
||||||
|
with PartlyCloudyTestingDataGrabber() as dg:
|
||||||
|
input = dg["sub-01"]
|
||||||
|
|
||||||
|
input = DefaultDataReader().fit_transform(input)
|
||||||
|
# Create ParcelAggregation object
|
||||||
|
marker = AmplitudeLowFrequencyFluctuationParcels(
|
||||||
|
parcellation=_PARCELLATION,
|
||||||
|
method="mean",
|
||||||
|
use_afni=False,
|
||||||
|
fractional=False,
|
||||||
|
)
|
||||||
|
python_values = marker.fit_transform(input)["BOLD"]["data"]
|
||||||
|
|
||||||
|
assert marker.use_afni is False
|
||||||
|
assert python_values.ndim == 2
|
||||||
|
assert python_values.shape == (1, 100)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(
|
||||||
|
_check_afni() is False, reason="requires afni to be in PATH"
|
||||||
|
)
|
||||||
|
def test_AmplitudeLowFrequencyFluctuationParcels_afni() -> None:
|
||||||
|
"""Test AmplitudeLowFrequencyFluctuationParcels using afni."""
|
||||||
|
# Get the SPM auditory data:
|
||||||
|
with PartlyCloudyTestingDataGrabber() as dg:
|
||||||
|
input = dg["sub-01"]
|
||||||
|
|
||||||
|
input = DefaultDataReader().fit_transform(input)
|
||||||
|
# Create ParcelAggregation object
|
||||||
|
marker = AmplitudeLowFrequencyFluctuationParcels(
|
||||||
|
parcellation=_PARCELLATION,
|
||||||
|
method="mean",
|
||||||
|
use_afni=True,
|
||||||
|
fractional=False,
|
||||||
|
)
|
||||||
|
assert marker.use_afni is True
|
||||||
|
afni_values = marker.fit_transform(input)["BOLD"]["data"]
|
||||||
|
|
||||||
|
assert afni_values.ndim == 2
|
||||||
|
assert afni_values.shape == (1, 100)
|
||||||
|
|
||||||
|
# Again, should be blazing fast
|
||||||
|
marker = AmplitudeLowFrequencyFluctuationParcels(
|
||||||
|
parcellation=_PARCELLATION, method="mean", fractional=False
|
||||||
|
)
|
||||||
|
assert marker.use_afni is None
|
||||||
|
afni_values2 = marker.fit_transform(input)["BOLD"]["data"]
|
||||||
|
assert marker.use_afni is True
|
||||||
|
assert_array_equal(afni_values, afni_values2)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(
|
||||||
|
_check_afni() is False, reason="requires afni to be in PATH"
|
||||||
|
)
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"fractional", [True, False], ids=["fractional", "non-fractional"]
|
||||||
|
)
|
||||||
|
def test_AmplitudeLowFrequencyFluctuationParcels_python_vs_afni(
|
||||||
|
fractional: bool,
|
||||||
|
) -> None:
|
||||||
|
"""Test AmplitudeLowFrequencyFluctuationParcels using python.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
factional : bool
|
||||||
|
Whether to compute fractional ALFF or not.
|
||||||
|
"""
|
||||||
|
|
||||||
|
with PartlyCloudyTestingDataGrabber() as dg:
|
||||||
|
input = dg["sub-01"]
|
||||||
|
|
||||||
|
input = DefaultDataReader().fit_transform(input)
|
||||||
|
# Create ParcelAggregation object
|
||||||
|
marker_python = AmplitudeLowFrequencyFluctuationParcels(
|
||||||
|
parcellation=_PARCELLATION,
|
||||||
|
method="mean",
|
||||||
|
use_afni=False,
|
||||||
|
fractional=fractional,
|
||||||
|
)
|
||||||
|
python_values = marker_python.fit_transform(input)["BOLD"]["data"]
|
||||||
|
|
||||||
|
assert marker_python.use_afni is False
|
||||||
|
assert python_values.ndim == 2
|
||||||
|
assert python_values.shape == (1, 100)
|
||||||
|
|
||||||
|
marker_afni = AmplitudeLowFrequencyFluctuationParcels(
|
||||||
|
parcellation=_PARCELLATION,
|
||||||
|
method="mean",
|
||||||
|
use_afni=True,
|
||||||
|
fractional=fractional,
|
||||||
|
)
|
||||||
|
afni_values = marker_afni.fit_transform(input)["BOLD"]["data"]
|
||||||
|
|
||||||
|
assert marker_afni.use_afni is True
|
||||||
|
assert afni_values.ndim == 2
|
||||||
|
assert afni_values.shape == (1, 100)
|
||||||
|
|
||||||
|
r, p = pearsonr(python_values[0], afni_values[0])
|
||||||
|
logger.info(f"Correlation between python and afni: {r} (p={p})")
|
||||||
|
assert r > 0.99
|
||||||
|
|
||||||
|
|
||||||
|
def test_AmplitudeLowFrequencyFluctuationParcels_storage(
|
||||||
|
tmp_path: Path,
|
||||||
|
) -> None:
|
||||||
|
"""Test AmplitudeLowFrequencyFluctuationParcels storage.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
tmp_path : pathlib.Path
|
||||||
|
The path to the test directory.
|
||||||
|
"""
|
||||||
|
with PartlyCloudyTestingDataGrabber() as dg:
|
||||||
|
# Use first subject
|
||||||
|
input = dg["sub-01"]
|
||||||
|
input = DefaultDataReader().fit_transform(input)
|
||||||
|
# Create ParcelAggregation object
|
||||||
|
marker = AmplitudeLowFrequencyFluctuationParcels(
|
||||||
|
parcellation=_PARCELLATION,
|
||||||
|
method="mean",
|
||||||
|
use_afni=False,
|
||||||
|
fractional=True,
|
||||||
|
)
|
||||||
|
storage = SQLiteFeatureStorage(tmp_path / "alff_parcels.sqlite")
|
||||||
|
|
||||||
|
# Fit transform marker on data with storage
|
||||||
|
marker.fit_transform(
|
||||||
|
input=input,
|
||||||
|
storage=storage,
|
||||||
|
)
|
||||||
165
junifer/markers/falff/tests/test_falff_spheres.py
Normal file
|
|
@ -0,0 +1,165 @@
|
||||||
|
"""Provide test for sphere-aggregated (f)ALFF."""
|
||||||
|
Docstring needs to be updated. Docstring needs to be updated.
|
|||||||
|
|
||||||
|
# Authors: Federico Raimondo <f.raimondo@fz-juelich.de>
|
||||||
|
# Synchon Mandal <s.mandal@fz-juelich.de>
|
||||||
|
# License: AGPL
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from numpy.testing import assert_array_equal
|
||||||
|
from scipy.stats import pearsonr
|
||||||
|
|
||||||
|
from junifer.datareader import DefaultDataReader
|
||||||
|
from junifer.markers.falff import AmplitudeLowFrequencyFluctuationSpheres
|
||||||
|
from junifer.testing.datagrabbers import PartlyCloudyTestingDataGrabber
|
||||||
|
from junifer.pipeline.utils import _check_afni
|
||||||
|
from junifer.storage import SQLiteFeatureStorage
|
||||||
|
from junifer.utils import logger
|
||||||
|
|
||||||
|
|
||||||
|
As the coordinates used are same, maybe create a module-level variable and reuse it. As the coordinates used are same, maybe create a module-level variable and reuse it.
|
|||||||
|
_COORDINATES = "DMNBuckner"
|
||||||
|
|
||||||
|
|
||||||
|
def test_AmplitudeLowFrequencyFluctuationSpheres_python() -> None:
|
||||||
|
"""Test AmplitudeLowFrequencyFluctuationSpheres using python."""
|
||||||
|
# Get the SPM auditory data:
|
||||||
|
|
||||||
|
with PartlyCloudyTestingDataGrabber() as dg:
|
||||||
|
input = dg["sub-01"]
|
||||||
|
|
||||||
|
input = DefaultDataReader().fit_transform(input)
|
||||||
|
# Create ParcelAggregation object
|
||||||
|
marker = AmplitudeLowFrequencyFluctuationSpheres(
|
||||||
|
coords=_COORDINATES,
|
||||||
|
radius=5,
|
||||||
|
method="mean",
|
||||||
|
use_afni=False,
|
||||||
|
fractional=False,
|
||||||
|
)
|
||||||
|
python_values = marker.fit_transform(input)["BOLD"]["data"]
|
||||||
|
|
||||||
|
assert marker.use_afni is False
|
||||||
|
assert python_values.ndim == 2
|
||||||
|
assert python_values.shape == (1, 6)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(
|
||||||
|
_check_afni() is False, reason="requires afni to be in PATH"
|
||||||
|
)
|
||||||
|
def test_AmplitudeLowFrequencyFluctuationSpheres_afni() -> None:
|
||||||
|
"""Test AmplitudeLowFrequencyFluctuationSpheres using afni."""
|
||||||
|
# Get the SPM auditory data:
|
||||||
|
with PartlyCloudyTestingDataGrabber() as dg:
|
||||||
|
input = dg["sub-01"]
|
||||||
|
|
||||||
|
input = DefaultDataReader().fit_transform(input)
|
||||||
|
# Create ParcelAggregation object
|
||||||
|
marker = AmplitudeLowFrequencyFluctuationSpheres(
|
||||||
|
coords=_COORDINATES,
|
||||||
|
radius=5,
|
||||||
|
method="mean",
|
||||||
|
use_afni=True,
|
||||||
|
fractional=False,
|
||||||
|
)
|
||||||
|
assert marker.use_afni is True
|
||||||
|
afni_values = marker.fit_transform(input)["BOLD"]["data"]
|
||||||
|
|
||||||
|
assert afni_values.ndim == 2
|
||||||
|
assert afni_values.shape == (1, 6)
|
||||||
|
|
||||||
|
# Again, should be blazing fast
|
||||||
|
marker = AmplitudeLowFrequencyFluctuationSpheres(
|
||||||
|
coords=_COORDINATES,
|
||||||
|
radius=5,
|
||||||
|
method="mean",
|
||||||
|
fractional=False,
|
||||||
|
)
|
||||||
|
assert marker.use_afni is None
|
||||||
|
afni_values2 = marker.fit_transform(input)["BOLD"]["data"]
|
||||||
|
assert marker.use_afni is True
|
||||||
|
assert_array_equal(afni_values, afni_values2)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(
|
||||||
|
_check_afni() is False, reason="requires afni to be in PATH"
|
||||||
|
)
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"fractional", [True, False], ids=["fractional", "non-fractional"]
|
||||||
|
)
|
||||||
|
def test_AmplitudeLowFrequencyFluctuationSpheres_python_vs_afni(
|
||||||
|
fractional: bool,
|
||||||
|
) -> None:
|
||||||
|
"""Test AmplitudeLowFrequencyFluctuationSpheres python vs afni results.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
fractional : bool
|
||||||
|
Whether to compute fractional ALFF or not.
|
||||||
|
"""
|
||||||
|
with PartlyCloudyTestingDataGrabber() as dg:
|
||||||
|
input = dg["sub-01"]
|
||||||
|
|
||||||
|
input = DefaultDataReader().fit_transform(input)
|
||||||
|
# Create ParcelAggregation object
|
||||||
|
marker_python = AmplitudeLowFrequencyFluctuationSpheres(
|
||||||
|
coords=_COORDINATES,
|
||||||
|
radius=5,
|
||||||
|
method="mean",
|
||||||
|
use_afni=False,
|
||||||
|
fractional=fractional,
|
||||||
|
)
|
||||||
|
python_values = marker_python.fit_transform(input)["BOLD"]["data"]
|
||||||
|
|
||||||
|
assert marker_python.use_afni is False
|
||||||
|
assert python_values.ndim == 2
|
||||||
|
assert python_values.shape == (1, 6)
|
||||||
|
|
||||||
|
marker_afni = AmplitudeLowFrequencyFluctuationSpheres(
|
||||||
|
coords=_COORDINATES,
|
||||||
|
radius=5,
|
||||||
|
method="mean",
|
||||||
|
use_afni=True,
|
||||||
|
fractional=fractional,
|
||||||
|
)
|
||||||
|
afni_values = marker_afni.fit_transform(input)["BOLD"]["data"]
|
||||||
|
|
||||||
|
assert marker_afni.use_afni is True
|
||||||
|
assert afni_values.ndim == 2
|
||||||
|
assert afni_values.shape == (1, 6)
|
||||||
|
|
||||||
|
r, p = pearsonr(python_values[0], afni_values[0])
|
||||||
|
logger.info(f"Correlation between python and afni: {r} (p={p})")
|
||||||
|
assert r > 0.99
|
||||||
|
|
||||||
|
|
||||||
|
def test_AmplitudeLowFrequencyFluctuationSpheres_storage(
|
||||||
|
tmp_path: Path,
|
||||||
|
) -> None:
|
||||||
|
"""Test AmplitudeLowFrequencyFluctuationSpheres storage.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
tmp_path : pathlib.Path
|
||||||
|
The path to the test directory.
|
||||||
|
"""
|
||||||
|
with PartlyCloudyTestingDataGrabber() as dg:
|
||||||
|
# Use first subject
|
||||||
|
input = dg["sub-01"]
|
||||||
|
input = DefaultDataReader().fit_transform(input)
|
||||||
|
# Create ParcelAggregation object
|
||||||
|
marker = AmplitudeLowFrequencyFluctuationSpheres(
|
||||||
|
coords=_COORDINATES,
|
||||||
|
radius=5,
|
||||||
|
method="mean",
|
||||||
|
use_afni=False,
|
||||||
|
fractional=True,
|
||||||
|
)
|
||||||
|
storage = SQLiteFeatureStorage(tmp_path / "alff_parcels.sqlite")
|
||||||
|
|
||||||
|
# Fit transform marker on data with storage
|
||||||
|
marker.fit_transform(
|
||||||
|
input=input,
|
||||||
|
storage=storage,
|
||||||
|
)
|
||||||
Please add a newline after this for consistency.
highpass : positive float?lowpass : positive float?tr : positive float, optional?Maybe return the list directly?
I don't think the class is abstract.
Use
raise_error?