[ENH]: Introduce JuniferConnectivityMeasure #348

Merged
synchon merged 10 commits from feat/junifer-connectivity-measure into main 2024-07-19 10:08:21 +00:00
19 changed files with 1866 additions and 221 deletions

View file

@ -0,0 +1 @@
For :class:`.CrossParcellationFC`, ``aggregation_method`` and ``correlation_method`` have been renamed to ``agg_method`` and ``corr_method`` respectively and ``agg_method_params`` has been added; for ``FunctionalConnectivityBase``, :class:`.FunctionalConnectivityParcels`, :class:`.FunctionalConnectivitySpheres`, :class:`.EdgeCentricFCParcels` and :class:`.EdgeCentricFCSpheres`, ``cor_method`` and ``cor_method_params`` have been renamed to ``conn_method`` and ``conn_method_params`` by `Synchon Mandal`_

View file

@ -0,0 +1 @@
``FunctionalConnectivity``-family Markers now use :class:`sklearn.covariance.EmpiricalCovariance` as the default covariance estimator and ``correlation`` as the default connecivity matrix kind by `Synchon Mandal`_

View file

@ -0,0 +1 @@
Introduce :class:`.JuniferConnectivityMeasure` for customising functional connectivity matrix kinds and measurements by `Synchon Mandal`_

View file

@ -170,6 +170,7 @@ numpydoc_xref_ignore = {
"Engine",
"positive",
"negative",
"estimator",
}
# numpydoc_validation_checks = {
# "all",

View file

@ -5,3 +5,4 @@ sepulcre
arange
sinc
whit
amin

View file

@ -4,6 +4,7 @@
# License: AGPL
from .junifer_nifti_spheres_masker import JuniferNiftiSpheresMasker
from .junifer_connectivity_measure import JuniferConnectivityMeasure
__all__ = ["JuniferNiftiSpheresMasker"]
__all__ = ["JuniferNiftiSpheresMasker", "JuniferConnectivityMeasure"]

View file

@ -0,0 +1,483 @@
"""Provide JuniferConnectivityMeasure class."""
# Authors: Synchon Mandal <s.mandal@fz-juelich.de>
# License: AGPL
from typing import Callable, List, Optional
import numpy as np
from nilearn import signal
from nilearn.connectome import (
ConnectivityMeasure,
cov_to_corr,
prec_to_partial,
sym_matrix_to_vec,
)
from scipy import linalg
from sklearn.base import clone
from sklearn.covariance import EmpiricalCovariance
from ...utils import logger, raise_error, warn_with_log
__all__ = ["JuniferConnectivityMeasure"]
DEFAULT_COV_ESTIMATOR = EmpiricalCovariance(store_precision=False)
# New BSD License
# Copyright (c) The nilearn developers.
# All rights reserved.
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# a. Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# b. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# c. Neither the name of the nilearn developers nor the names of
# its contributors may be used to endorse or promote products
# derived from this software without specific prior written
# permission.
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
# DAMAGE.
def _check_square(matrix: np.ndarray) -> None:
"""Raise a ValueError if the input matrix is square.
Parameters
----------
matrix : numpy.ndarray
Input array.
Raises
------
ValueError
If ``matrix`` is not a square matrix.
"""
if matrix.ndim != 2 or (matrix.shape[0] != matrix.shape[-1]):
raise_error(
f"Expected a square matrix, got array of shape {matrix.shape}."
)
def is_spd(M: np.ndarray, decimal: int = 15) -> bool: # noqa: N803
"""Check that input matrix is symmetric positive definite.
``M`` must be symmetric down to specified ``decimal`` places.
The check is performed by checking that all eigenvalues are positive.
Parameters
----------
M : numpy.ndarray
Input matrix to check for symmetric positive definite.
decimal : int, optional
Decimal places to check (default 15).
Returns
-------
bool
True if matrix is symmetric positive definite, False otherwise.
"""
if not np.allclose(M, M.T, atol=0, rtol=10**-decimal):
logger.debug(f"matrix not symmetric to {decimal:d} decimals")
return False
eigvalsh = np.linalg.eigvalsh(M)
ispd = eigvalsh.min() > 0
if not ispd:
logger.debug(f"matrix has a negative eigenvalue: {eigvalsh.min():.3f}")
return ispd
def _check_spd(matrix: np.ndarray) -> None:
"""Check ``matrix`` is symmetric positive definite.
Parameters
----------
matrix : numpy.ndarray
Input array.
Raises
------
ValueError
If the input matrix is not symmetric positive definite.
"""
if not is_spd(matrix, decimal=7):
raise_error("Expected a symmetric positive definite matrix.")
def _form_symmetric(
function: Callable[[np.ndarray], np.ndarray],
eigenvalues: np.ndarray,
eigenvectors: np.ndarray,
) -> np.ndarray:
"""Return the symmetric matrix.
Apply ``function`` to ``eigenvalues``, construct symmetric matrix with it
and ``eigenvectors`` and return the constructed symmetric matrix.
Parameters
----------
function : callable (function numpy.ndarray -> numpy.ndarray)
The transform to apply to the eigenvalues.
eigenvalues : numpy.ndarray of shape (n_features, )
Input argument of the function.
eigenvectors : numpy.ndarray of shape (n_features, n_features)
Unitary matrix.
Returns
-------
numpy.ndarray of shape (n_features, n_features)
The symmetric matrix obtained after transforming the eigenvalues, while
keeping the same eigenvectors.
"""
return np.dot(eigenvectors * function(eigenvalues), eigenvectors.T)
def _map_eigenvalues(
function: Callable[[np.ndarray], np.ndarray], symmetric: np.ndarray
) -> np.ndarray:
"""Matrix function, for real symmetric matrices.
The function is applied to the eigenvalues of ``symmetric``.
Parameters
----------
function : callable (function numpy.ndarray -> numpy.ndarray)
The transform to apply to the eigenvalues.
symmetric : numpy.ndarray of shape (n_features, n_features)
The input symmetric matrix.
Returns
-------
numpy.ndarray of shape (n_features, n_features)
The new symmetric matrix obtained after transforming the eigenvalues,
while keeping the same eigenvectors.
Notes
-----
If input matrix is not real symmetric, no error is reported but result will
be wrong.
"""
eigenvalues, eigenvectors = linalg.eigh(symmetric)
return _form_symmetric(function, eigenvalues, eigenvectors)
def _geometric_mean(
matrices: List[np.ndarray],
init: Optional[np.ndarray] = None,
max_iter: int = 10,
tol: Optional[float] = 1e-7,
) -> np.ndarray:
"""Compute the geometric mean of symmetric positive definite matrices.
The geometric mean of ``n`` positive definite matrices
``M_1, ..., M_n`` is the minimizer of the sum of squared distances from an
arbitrary matrix to each input matrix ``M_k``
.. math:: gmean(M_1, ..., M_n) = argmin_X sum_{k=1}^N dist(X, M_k)^2
where the used distance is related to matrices logarithm
.. math:: dist(X, M_k) = ||log(X^{-1/2} M_k X^{-1/2)}||
In case of positive numbers, this mean is the usual geometric mean.
See Algorithm 3 of [1]_ .
Parameters
----------
matrices : list of numpy.ndarray, all of shape (n_features, n_features)
List of matrices whose geometric mean to compute. Raise an error if the
matrices are not all symmetric positive definite of the same shape.
init : numpy.ndarray of shape (n_features, n_features), optional
Initialization matrix, default to the arithmetic mean of matrices.
Raise an error if the matrix is not symmetric positive definite of the
same shape as the elements of matrices (default None).
max_iter : int, optional
Maximal number of iterations (default 10).
tol : positive float or None, optional
The tolerance to declare convergence: if the gradient norm goes below
this value, the gradient descent is stopped. If None, no check is
performed (default 1e-7).
Returns
-------
gmean : numpy.ndarray of shape (n_features, n_features)
Geometric mean of the matrices.
References
----------
.. [1] Fletcher, T., P., Joshi, S.
Riemannian geometry for the statistical analysis of diffusion tensor
data.
Signal Processing, Volume 87, Issue 2, 2007, Pages 250-262
https://doi.org/10.1016/j.sigpro.2005.12.018.
"""
# Shape and symmetry positive definiteness checks
n_features = matrices[0].shape[0]
for matrix in matrices:
_check_square(matrix)
if matrix.shape[0] != n_features:
raise_error("Matrices are not of the same shape.")
_check_spd(matrix)
# Initialization
matrices = np.array(matrices)
if init is None:
gmean = np.mean(matrices, axis=0)
else:
_check_square(init)
if init.shape[0] != n_features:
raise_error("Initialization has incorrect shape.")
_check_spd(init)
gmean = init
norm_old = np.inf
step = 1.0
# Gradient descent
for _ in range(max_iter):
# Computation of the gradient
vals_gmean, vecs_gmean = linalg.eigh(gmean)
gmean_inv_sqrt = _form_symmetric(np.sqrt, 1.0 / vals_gmean, vecs_gmean)
whitened_matrices = [
gmean_inv_sqrt.dot(matrix).dot(gmean_inv_sqrt)
for matrix in matrices
]
logs = [_map_eigenvalues(np.log, w_mat) for w_mat in whitened_matrices]
# Covariant derivative is - gmean.dot(logms_mean)
logs_mean = np.mean(logs, axis=0)
if np.any(np.isnan(logs_mean)):
raise_error(
klass=FloatingPointError,
msg="Nan value after logarithm operation.",
)
# Norm of the covariant derivative on the tangent space at point gmean
norm = np.linalg.norm(logs_mean)
# Update of the minimizer
vals_log, vecs_log = linalg.eigh(logs_mean)
gmean_sqrt = _form_symmetric(np.sqrt, vals_gmean, vecs_gmean)
# Move along the geodesic
gmean = gmean_sqrt.dot(
_form_symmetric(np.exp, vals_log * step, vecs_log)
).dot(gmean_sqrt)
# Update the norm and the step size
if norm < norm_old:
norm_old = norm
elif norm > norm_old:
step = step / 2.0
norm = norm_old
if tol is not None and norm / gmean.size < tol:
break
if tol is not None and norm / gmean.size >= tol:
warn_with_log(
f"Maximum number of iterations {max_iter} reached without "
f"getting to the requested tolerance level {tol}."
)
return gmean
class JuniferConnectivityMeasure(ConnectivityMeasure):
"""Class for custom ConnectivityMeasure.
Differs from :class:`nilearn.connectome.ConnectivityMeasure` in the
following ways:
* default ``cov_estimator`` is
:class:`sklearn.covariance.EmpiricalCovariance`
* default ``kind`` is ``"correlation"``
Parameters
----------
cov_estimator : estimator object, optional
The covariance estimator
(default ``EmpiricalCovariance(store_precision=False)``).
kind : {"covariance", "correlation", "partial correlation", \
"tangent", "precision"}, optional
The matrix kind. For the use of ``"tangent"`` see [1]_
(default "correlation").
vectorize : bool, optional
If True, connectivity matrices are reshaped into 1D arrays and only
their flattened lower triangular parts are returned (default False).
discard_diagonal : bool, optional
If True, vectorized connectivity coefficients do not include the
matrices diagonal elements. Used only when vectorize is set to True
(default False).
standardize : bool, optional
If standardize is True, the data are centered and normed: their mean
is put to 0 and their variance is put to 1 in the time dimension
(default True).
.. note::
Added to control passing value to ``standardize`` of
``signal.clean`` to call new behavior since passing ``"zscore"`` or
True (default) is deprecated. This parameter will be deprecated in
version 0.13 and removed in version 0.15.
Attributes
----------
cov_estimator_ : estimator object
A new covariance estimator with the same parameters as
``cov_estimator``.
mean_ : numpy.ndarray
The mean connectivity matrix across subjects. For ``"tangent"`` kind,
it is the geometric mean of covariances (a group covariance
matrix that captures information from both correlation and partial
correlation matrices). For other values for ``kind``, it is the
mean of the corresponding matrices.
whitening_ : numpy.ndarray
The inverted square-rooted geometric mean of the covariance matrices.
References
----------
.. [1] Varoquaux, G., Baronnet, F., Kleinschmidt, A. et al.
Detection of brain functional-connectivity difference in
post-stroke patients using group-level covariance modeling.
In Tianzi Jiang, Nassir Navab, Josien P. W. Pluim, and
Max A. Viergever, editors, Medical image computing and
computer-assisted intervention - MICCAI 2010, Lecture notes
in computer science, Pages 200-208. Berlin, Heidelberg, 2010.
Springer.
doi:10/cn2h9c.
"""
def __init__(
self,
cov_estimator=DEFAULT_COV_ESTIMATOR,
kind="correlation",
vectorize=False,
discard_diagonal=False,
standardize=True,
):
super().__init__(
cov_estimator=cov_estimator,
kind=kind,
vectorize=vectorize,
discard_diagonal=discard_diagonal,
standardize=standardize,
)
def _fit_transform(
self,
X, # noqa: N803
do_transform=False,
do_fit=False,
confounds=None,
):
"""Avoid duplication of computation."""
self._check_input(X, confounds=confounds)
if do_fit:
self.cov_estimator_ = clone(self.cov_estimator)
# Compute all the matrices, stored in "connectivities"
if self.kind == "correlation":
covariances_std = [
self.cov_estimator_.fit(
signal.standardize_signal(
x,
detrend=False,
standardize=self.standardize,
)
).covariance_
for x in X
]
connectivities = [cov_to_corr(cov) for cov in covariances_std]
else:
covariances = [self.cov_estimator_.fit(x).covariance_ for x in X]
if self.kind in ("covariance", "tangent"):
connectivities = covariances
elif self.kind == "precision":
connectivities = [linalg.inv(cov) for cov in covariances]
elif self.kind == "partial correlation":
connectivities = [
prec_to_partial(linalg.inv(cov)) for cov in covariances
]
else:
allowed_kinds = (
"correlation",
"partial correlation",
"tangent",
"covariance",
"precision",
)
raise_error(
f"Allowed connectivity kinds are {allowed_kinds}. "
f"Got kind {self.kind}."
)
# Store the mean
if do_fit:
if self.kind == "tangent":
self.mean_ = _geometric_mean(
covariances, max_iter=30, tol=1e-7
)
self.whitening_ = _map_eigenvalues(
lambda x: 1.0 / np.sqrt(x), self.mean_
)
else:
self.mean_ = np.mean(connectivities, axis=0)
# Fight numerical instabilities: make symmetric
self.mean_ = self.mean_ + self.mean_.T
self.mean_ *= 0.5
# Compute the vector we return on transform
if do_transform:
if self.kind == "tangent":
connectivities = [
_map_eigenvalues(
np.log, self.whitening_.dot(cov).dot(self.whitening_)
)
for cov in connectivities
]
connectivities = np.array(connectivities)
if confounds is not None and not self.vectorize:
error_message = (
"'confounds' are provided but vectorize=False. "
"Confounds are only cleaned on vectorized matrices "
"as second level connectome regression "
"but not on symmetric matrices."
)
raise_error(error_message)
if self.vectorize:
connectivities = sym_matrix_to_vec(
connectivities, discard_diagonal=self.discard_diagonal
)
if confounds is not None:
connectivities = signal.clean(
connectivities, confounds=confounds
)
return connectivities

File diff suppressed because it is too large Load diff

View file

@ -28,18 +28,24 @@ class CrossParcellationFC(BaseMarker):
The name of the first parcellation.
parcellation_two : str
The name of the second parcellation.
aggregation_method : str, optional
The aggregation method (default "mean").
correlation_method : str, optional
agg_method : str, optional
The method to perform aggregation using.
See :func:`.get_aggfunc_by_name` for options
(default "mean").
agg_method_params : dict, optional
Parameters to pass to the aggregation function.
See :func:`.get_aggfunc_by_name` for options
(default None).
corr_method : str, optional
Any method that can be passed to
:any:`pandas.DataFrame.corr` (default "pearson").
:meth:`pandas.DataFrame.corr` (default "pearson").
masks : str, dict or list of dict or str, optional
The specification of the masks to apply to regions before extracting
signals. Check :ref:`Using Masks <using_masks>` for more details.
If None, will not apply any mask (default None).
name : str, optional
The name of the marker. If None, will use the class name
(default None).
The name of the marker. If None, will use
``BOLD_CrossParcellationFC`` (default None).
"""
@ -55,8 +61,9 @@ class CrossParcellationFC(BaseMarker):
self,
parcellation_one: str,
parcellation_two: str,
aggregation_method: str = "mean",
correlation_method: str = "pearson",
agg_method: str = "mean",
agg_method_params: Optional[Dict] = None,
corr_method: str = "pearson",
masks: Union[str, Dict, List[Union[Dict, str]], None] = None,
name: Optional[str] = None,
) -> None:
@ -66,8 +73,9 @@ class CrossParcellationFC(BaseMarker):
)
self.parcellation_one = parcellation_one
self.parcellation_two = parcellation_two
self.aggregation_method = aggregation_method
self.correlation_method = correlation_method
self.agg_method = agg_method
self.agg_method_params = agg_method_params
self.corr_method = corr_method
self.masks = masks
super().__init__(on=["BOLD"], name=name)
@ -115,13 +123,17 @@ class CrossParcellationFC(BaseMarker):
# Perform aggregation using two parcellations
aggregation_parcellation_one = ParcelAggregation(
parcellation=self.parcellation_one,
method=self.aggregation_method,
method=self.agg_method,
method_params=self.agg_method_params,
masks=self.masks,
on="BOLD",
).compute(input, extra_input=extra_input)
aggregation_parcellation_two = ParcelAggregation(
parcellation=self.parcellation_two,
method=self.aggregation_method,
method=self.agg_method,
method_params=self.agg_method_params,
masks=self.masks,
on="BOLD",
).compute(input, extra_input=extra_input)
return {
@ -133,7 +145,7 @@ class CrossParcellationFC(BaseMarker):
pd.DataFrame(
aggregation_parcellation_two["aggregation"]["data"]
),
method=self.correlation_method,
method=self.corr_method,
).values,
# Columns should be named after parcellation 1
"col_names": aggregation_parcellation_one["aggregation"][

View file

@ -22,36 +22,40 @@ class EdgeCentricFCParcels(FunctionalConnectivityBase):
Parameters
----------
parcellation : str or list of str
The name(s) of the parcellation(s). Check valid options by calling
:func:`.list_parcellations`.
The name(s) of the parcellation(s) to use.
See :func:`.list_parcellations` for options.
agg_method : str, optional
The method to perform aggregation of BOLD time series.
Check valid options in :func:`.get_aggfunc_by_name`
The method to perform aggregation using.
See :func:`.get_aggfunc_by_name` for options
(default "mean").
agg_method_params : dict, optional
Parameters to pass to the aggregation function. Check valid options in
:func:`.get_aggfunc_by_name` (default None).
cor_method : str, optional
The method to perform correlation. Check valid options in
:class:`nilearn.connectome.ConnectivityMeasure`
(default "covariance").
cor_method_params : dict, optional
Parameters to pass to the correlation function. Check valid options in
:class:`nilearn.connectome.ConnectivityMeasure` (default None).
Parameters to pass to the aggregation function.
See :func:`.get_aggfunc_by_name` for options
(default None).
conn_method : str, optional
The method to perform connectivity measure using.
See :class:`.JuniferConnectivityMeasure` for options
(default "correlation").
conn_method_params : dict, optional
Parameters to pass to :class:`.JuniferConnectivityMeasure`.
If None, ``{"empirical": True}`` will be used, which would mean
:class:`sklearn.covariance.EmpiricalCovariance` is used to compute
covariance. If usage of :class:`sklearn.covariance.LedoitWolf` is
desired, ``{"empirical": False}`` should be passed
(default None).
masks : str, dict or list of dict or str, optional
The specification of the masks to apply to regions before extracting
signals. Check :ref:`Using Masks <using_masks>` for more details.
If None, will not apply any mask (default None).
name : str, optional
The name of the marker. If None, will use the class name (default
None).
The name of the marker. If None, will use
``BOLD_EdgeCentricFCParcels`` (default None).
References
----------
.. [1] Jo et al. (2021)
Subject identification using
edge-centric functional connectivity
doi: https://doi.org/10.1016/j.neuroimage.2021.118204
Subject identification using edge-centric functional connectivity.
https://doi.org/10.1016/j.neuroimage.2021.118204
"""
@ -60,8 +64,8 @@ class EdgeCentricFCParcels(FunctionalConnectivityBase):
parcellation: Union[str, List[str]],
agg_method: str = "mean",
agg_method_params: Optional[Dict] = None,
cor_method: str = "covariance",
cor_method_params: Optional[Dict] = None,
conn_method: str = "correlation",
conn_method_params: Optional[Dict] = None,
masks: Union[str, Dict, List[Union[Dict, str]], None] = None,
name: Optional[str] = None,
) -> None:
@ -69,8 +73,8 @@ class EdgeCentricFCParcels(FunctionalConnectivityBase):
super().__init__(
agg_method=agg_method,
agg_method_params=agg_method_params,
cor_method=cor_method,
cor_method_params=cor_method_params,
conn_method=conn_method,
conn_method_params=conn_method_params,
masks=masks,
name=name,
)

View file

@ -22,42 +22,48 @@ class EdgeCentricFCSpheres(FunctionalConnectivityBase):
Parameters
----------
coords : str
The name of the coordinates list to use. See
:func:`.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).
The name of the coordinates list to use.
See :func:`.list_coordinates` for options.
radius : positive float, optional
The radius of the sphere around each coordinates in millimetres.
If None, the signal will be extracted from a single voxel.
See :class:`.JuniferNiftiSpheresMasker` for more information
(default None).
allow_overlap : bool, optional
Whether to allow overlapping spheres. If False, an error is raised if
the spheres overlap (default is False).
the spheres overlap (default False).
agg_method : str, optional
The aggregation method to use.
See :func:`.get_aggfunc_by_name` for more information
(default None).
The method to perform aggregation using.
See :func:`.get_aggfunc_by_name` for options
(default "mean").
agg_method_params : dict, optional
The parameters to pass to the aggregation method (default None).
cor_method : str, optional
The method to perform correlation using. Check valid options in
:class:`nilearn.connectome.ConnectivityMeasure` (default "covariance").
cor_method_params : dict, optional
Parameters to pass to the correlation function. Check valid options in
:class:`nilearn.connectome.ConnectivityMeasure` (default None).
Parameters to pass to the aggregation function.
See :func:`.get_aggfunc_by_name` for options
(default None).
conn_method : str, optional
The method to perform connectivity measure using.
See :class:`.JuniferConnectivityMeasure` for options
(default "correlation").
conn_method_params : dict, optional
Parameters to pass to :class:`.JuniferConnectivityMeasure`.
If None, ``{"empirical": True}`` will be used, which would mean
:class:`sklearn.covariance.EmpiricalCovariance` is used to compute
covariance. If usage of :class:`sklearn.covariance.LedoitWolf` is
desired, ``{"empirical": False}`` should be passed
(default None).
masks : str, dict or list of dict or str, optional
The specification of the masks to apply to regions before extracting
signals. Check :ref:`Using Masks <using_masks>` for more details.
If None, will not apply any mask (default None).
name : str, optional
The name of the marker. By default, it will use
KIND_EdgeCentricFCSpheres where KIND is the kind of data it
was applied to (default None).
The name of the marker. If None, will use
``BOLD_EdgeCentricFCSpheres`` (default None).
References
----------
.. [1] Jo et al. (2021)
Subject identification using
edge-centric functional connectivity
doi: https://doi.org/10.1016/j.neuroimage.2021.118204
Subject identification using edge-centric functional connectivity.
https://doi.org/10.1016/j.neuroimage.2021.118204
"""
@ -68,8 +74,8 @@ class EdgeCentricFCSpheres(FunctionalConnectivityBase):
allow_overlap: bool = False,
agg_method: str = "mean",
agg_method_params: Optional[Dict] = None,
cor_method: str = "covariance",
cor_method_params: Optional[Dict] = None,
conn_method: str = "correlation",
conn_method_params: Optional[Dict] = None,
masks: Union[str, Dict, List[Union[Dict, str]], None] = None,
name: Optional[str] = None,
) -> None:
@ -81,8 +87,8 @@ class EdgeCentricFCSpheres(FunctionalConnectivityBase):
super().__init__(
agg_method=agg_method,
agg_method_params=agg_method_params,
cor_method=cor_method,
cor_method_params=cor_method_params,
conn_method=conn_method,
conn_method_params=conn_method_params,
masks=masks,
name=name,
)

View file

@ -7,9 +7,9 @@
from abc import abstractmethod
from typing import Any, ClassVar, Dict, List, Optional, Set, Union
from nilearn.connectome import ConnectivityMeasure
from sklearn.covariance import EmpiricalCovariance
from sklearn.covariance import EmpiricalCovariance, LedoitWolf
from ...external.nilearn import JuniferConnectivityMeasure
from ...utils import raise_error
from ..base import BaseMarker
@ -23,25 +23,31 @@ class FunctionalConnectivityBase(BaseMarker):
Parameters
----------
agg_method : str, optional
The method to perform aggregation using. Check valid options in
:func:`.get_aggfunc_by_name` (default "mean").
The method to perform aggregation using.
Check valid options in :func:`.get_aggfunc_by_name`
(default "mean").
agg_method_params : dict, optional
Parameters to pass to the aggregation function. Check valid options in
:func:`.get_aggfunc_by_name` (default None).
cor_method : str, optional
The method to perform correlation using. Check valid options in
:class:`nilearn.connectome.ConnectivityMeasure`
(default "covariance").
cor_method_params : dict, optional
Parameters to pass to the correlation function. Check valid options in
:class:`nilearn.connectome.ConnectivityMeasure` (default None).
Parameters to pass to the aggregation function.
Check valid options in :func:`.get_aggfunc_by_name`
(default None).
conn_method : str, optional
The method to perform connectivity measure using.
Check valid options in :class:`.JuniferConnectivityMeasure`
(default "correlation").
conn_method_params : dict, optional
Parameters to pass to :class:`.JuniferConnectivityMeasure`.
If None, ``{"empirical": True}`` will be used, which would mean
:class:`sklearn.covariance.EmpiricalCovariance` is used to compute
covariance. If usage of :class:`sklearn.covariance.LedoitWolf` is
desired, ``{"empirical": False}`` should be passed
(default None).
masks : str, dict or list of dict or str, optional
The specification of the masks to apply to regions before extracting
signals. Check :ref:`Using Masks <using_masks>` for more details.
If None, will not apply any mask (default None).
name : str, optional
The name of the marker. If None, will use the class name (default
None).
The name of the marker. If None, will use ``BOLD_<class_name>``
(default None).
"""
@ -57,19 +63,18 @@ class FunctionalConnectivityBase(BaseMarker):
self,
agg_method: str = "mean",
agg_method_params: Optional[Dict] = None,
cor_method: str = "covariance",
cor_method_params: Optional[Dict] = None,
conn_method: str = "correlation",
conn_method_params: Optional[Dict] = None,
masks: Union[str, Dict, List[Union[Dict, str]], None] = None,
name: Optional[str] = None,
) -> None:
self.agg_method = agg_method
self.agg_method_params = agg_method_params
self.cor_method = cor_method
self.cor_method_params = cor_method_params or {}
# default to nilearn behavior
self.cor_method_params["empirical"] = self.cor_method_params.get(
"empirical", False
self.conn_method = conn_method
self.conn_method_params = conn_method_params or {}
# Reverse of nilearn behavior
self.conn_method_params["empirical"] = self.conn_method_params.get(
"empirical", True
)
self.masks = masks
super().__init__(on="BOLD", name=name)
@ -121,14 +126,21 @@ class FunctionalConnectivityBase(BaseMarker):
"""
# Perform necessary aggregation
aggregation = self.aggregate(input, extra_input=extra_input)
# Compute correlation
if self.cor_method_params["empirical"]:
connectivity = ConnectivityMeasure(
cov_estimator=EmpiricalCovariance(), # type: ignore
kind=self.cor_method,
)
# Set covariance estimator
if self.conn_method_params["empirical"]:
cov_estimator = EmpiricalCovariance(store_precision=False)
else:
connectivity = ConnectivityMeasure(kind=self.cor_method)
cov_estimator = LedoitWolf(store_precision=False)
# Compute correlation
connectivity = JuniferConnectivityMeasure(
cov_estimator=cov_estimator,
kind=self.conn_method,
**{
k: v
for k, v in self.conn_method_params.items()
if k != "empirical"
},
)
# Create dictionary for output
return {
"functional_connectivity": {

View file

@ -22,28 +22,34 @@ class FunctionalConnectivityParcels(FunctionalConnectivityBase):
Parameters
----------
parcellation : str or list of str
The name(s) of the parcellation(s). Check valid options by calling
:func:`.list_parcellations`.
The name(s) of the parcellation(s) to use.
See :func:`.list_parcellations` for options.
agg_method : str, optional
The method to perform aggregation using. Check valid options in
:func:`.get_aggfunc_by_name` (default "mean").
The method to perform aggregation using.
See :func:`.get_aggfunc_by_name` for options
(default "mean").
agg_method_params : dict, optional
Parameters to pass to the aggregation function. Check valid options in
:func:`.get_aggfunc_by_name` (default None).
cor_method : str, optional
The method to perform correlation using. Check valid options in
:class:`nilearn.connectome.ConnectivityMeasure`
(default "covariance").
cor_method_params : dict, optional
Parameters to pass to the correlation function. Check valid options in
:class:`nilearn.connectome.ConnectivityMeasure` (default None).
Parameters to pass to the aggregation function.
See :func:`.get_aggfunc_by_name` for options
(default None).
conn_method : str, optional
The method to perform connectivity measure using.
See :class:`.JuniferConnectivityMeasure` for options
(default "correlation").
conn_method_params : dict, optional
Parameters to pass to :class:`.JuniferConnectivityMeasure`.
If None, ``{"empirical": True}`` will be used, which would mean
:class:`sklearn.covariance.EmpiricalCovariance` is used to compute
covariance. If usage of :class:`sklearn.covariance.LedoitWolf` is
desired, ``{"empirical": False}`` should be passed
(default None).
masks : str, dict or list of dict or str, optional
The specification of the masks to apply to regions before extracting
signals. Check :ref:`Using Masks <using_masks>` for more details.
If None, will not apply any mask (default None).
name : str, optional
The name of the marker. If None, will use the class name (default
None).
The name of the marker. If None, will use
``BOLD_FunctionalConnectivityParcels`` (default None).
"""
@ -52,8 +58,8 @@ class FunctionalConnectivityParcels(FunctionalConnectivityBase):
parcellation: Union[str, List[str]],
agg_method: str = "mean",
agg_method_params: Optional[Dict] = None,
cor_method: str = "covariance",
cor_method_params: Optional[Dict] = None,
conn_method: str = "correlation",
conn_method_params: Optional[Dict] = None,
masks: Union[str, Dict, List[Union[Dict, str]], None] = None,
name: Optional[str] = None,
) -> None:
@ -61,8 +67,8 @@ class FunctionalConnectivityParcels(FunctionalConnectivityBase):
super().__init__(
agg_method=agg_method,
agg_method_params=agg_method_params,
cor_method=cor_method,
cor_method_params=cor_method_params,
conn_method=conn_method,
conn_method_params=conn_method_params,
masks=masks,
name=name,
)

View file

@ -23,35 +23,42 @@ class FunctionalConnectivitySpheres(FunctionalConnectivityBase):
Parameters
----------
coords : str
The name of the coordinates list to use. See
:func:`.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).
The name of the coordinates list to use.
See :func:`.list_coordinates` for options.
radius : positive float, optional
The radius of the sphere around each coordinates in millimetres.
If None, the signal will be extracted from a single voxel.
See :class:`.JuniferNiftiSpheresMasker` for more information
(default None).
allow_overlap : bool, optional
Whether to allow overlapping spheres. If False, an error is raised if
the spheres overlap (default is False).
the spheres overlap (default False).
agg_method : str, optional
The aggregation method to use.
See :func:`.get_aggfunc_by_name` for more information
(default None).
The method to perform aggregation using.
See :func:`.get_aggfunc_by_name` for options
(default "mean").
agg_method_params : dict, optional
The parameters to pass to the aggregation method (default None).
cor_method : str, optional
The method to perform correlation using. Check valid options in
:class:`nilearn.connectome.ConnectivityMeasure` (default "covariance").
cor_method_params : dict, optional
Parameters to pass to the correlation function. Check valid options in
:class:`nilearn.connectome.ConnectivityMeasure` (default None).
Parameters to pass to the aggregation function.
See :func:`.get_aggfunc_by_name` for options
(default None).
conn_method : str, optional
The method to perform connectivity measure using.
See :class:`.JuniferConnectivityMeasure` for options
(default "correlation").
conn_method_params : dict, optional
Parameters to pass to :class:`.JuniferConnectivityMeasure`.
If None, ``{"empirical": True}`` will be used, which would mean
:class:`sklearn.covariance.EmpiricalCovariance` is used to compute
covariance. If usage of :class:`sklearn.covariance.LedoitWolf` is
desired, ``{"empirical": False}`` should be passed
(default None).
masks : str, dict or list of dict or str, optional
The specification of the masks to apply to regions before extracting
signals. Check :ref:`Using Masks <using_masks>` for more details.
If None, will not apply any mask (default None).
name : str, optional
The name of the marker. By default, it will use
KIND_FunctionalConnectivitySpheres where KIND is the kind of data it
was applied to (default None).
The name of the marker. If None, will use
``BOLD_FunctionalConnectivitySpheres`` (default None).
"""
@ -62,8 +69,8 @@ class FunctionalConnectivitySpheres(FunctionalConnectivityBase):
allow_overlap: bool = False,
agg_method: str = "mean",
agg_method_params: Optional[Dict] = None,
cor_method: str = "covariance",
cor_method_params: Optional[Dict] = None,
conn_method: str = "correlation",
conn_method_params: Optional[Dict] = None,
masks: Union[str, Dict, List[Union[Dict, str]], None] = None,
name: Optional[str] = None,
) -> None:
@ -75,8 +82,8 @@ class FunctionalConnectivitySpheres(FunctionalConnectivityBase):
super().__init__(
agg_method=agg_method,
agg_method_params=agg_method_params,
cor_method=cor_method,
cor_method_params=cor_method_params,
conn_method=conn_method,
conn_method_params=conn_method_params,
masks=masks,
name=name,
)

View file

@ -27,7 +27,7 @@ def test_init() -> None:
CrossParcellationFC(
parcellation_one="a",
parcellation_two="a",
correlation_method="pearson",
corr_method="pearson",
)
@ -58,7 +58,7 @@ def test_compute(tmp_path: Path) -> None:
crossparcellation = CrossParcellationFC(
parcellation_one=parcellation_one,
parcellation_two=parcellation_two,
correlation_method="spearman",
corr_method="spearman",
)
out = crossparcellation.compute(element_data["BOLD"])[
"functional_connectivity"
@ -86,7 +86,7 @@ def test_store(tmp_path: Path) -> None:
crossparcellation = CrossParcellationFC(
parcellation_one=parcellation_one,
parcellation_two=parcellation_two,
correlation_method="spearman",
corr_method="spearman",
)
storage = SQLiteFeatureStorage(
uri=tmp_path / "test_crossparcellation.sqlite", upsert="ignore"

View file

@ -5,6 +5,9 @@
# License: AGPL
from pathlib import Path
from typing import Dict
import pytest
from junifer.datareader import DefaultDataReader
from junifer.markers.functional_connectivity import EdgeCentricFCParcels
@ -12,20 +15,35 @@ from junifer.storage import SQLiteFeatureStorage
from junifer.testing.datagrabbers import PartlyCloudyTestingDataGrabber
def test_EdgeCentricFCParcels(tmp_path: Path) -> None:
@pytest.mark.parametrize(
"conn_method_params",
[
{"empirical": False},
{"empirical": True},
],
)
def test_EdgeCentricFCParcels(
tmp_path: Path,
conn_method_params: Dict[str, bool],
) -> None:
"""Test EdgeCentricFCParcels.
Parameters
----------
tmp_path : pathlib.Path
The path to the test directory.
conn_method_params : dict
The parametrized parameters to connectivity measure method.
"""
with PartlyCloudyTestingDataGrabber() as dg:
# Get element data
element_data = DefaultDataReader().fit_transform(dg["sub-01"])
# Setup marker
marker = EdgeCentricFCParcels(
parcellation="TianxS1x3TxMNInonlinear2009cAsym",
cor_method_params={"empirical": True},
conn_method="correlation",
conn_method_params=conn_method_params,
)
# Check correct output
assert "matrix" == marker.get_output_type(
@ -41,8 +59,7 @@ def test_EdgeCentricFCParcels(tmp_path: Path) -> None:
assert "data" in edge_fc_bold
assert "row_names" in edge_fc_bold
assert "col_names" in edge_fc_bold
assert edge_fc_bold["data"].shape[0] == n_edges
assert edge_fc_bold["data"].shape[1] == n_edges
assert edge_fc_bold["data"].shape == (n_edges, n_edges)
assert len(set(edge_fc_bold["row_names"])) == n_edges
assert len(set(edge_fc_bold["col_names"])) == n_edges

View file

@ -5,6 +5,9 @@
# License: AGPL
from pathlib import Path
from typing import Dict
import pytest
from junifer.datareader import DefaultDataReader
from junifer.markers.functional_connectivity import EdgeCentricFCSpheres
@ -12,19 +15,36 @@ from junifer.storage import SQLiteFeatureStorage
from junifer.testing.datagrabbers import SPMAuditoryTestingDataGrabber
def test_EdgeCentricFCSpheres(tmp_path: Path) -> None:
@pytest.mark.parametrize(
"conn_method_params",
[
{"empirical": False},
{"empirical": True},
],
)
def test_EdgeCentricFCSpheres(
tmp_path: Path,
conn_method_params: Dict[str, bool],
) -> None:
"""Test EdgeCentricFCSpheres.
Parameters
----------
tmp_path : pathlib.Path
The path to the test directory.
conn_method_params : dict
The parametrized parameters to connectivity measure method.
"""
with SPMAuditoryTestingDataGrabber() as dg:
# Get element data
element_data = DefaultDataReader().fit_transform(dg["sub001"])
# Setup marker
marker = EdgeCentricFCSpheres(
coords="DMNBuckner", radius=5.0, cor_method="correlation"
coords="DMNBuckner",
radius=5.0,
conn_method="correlation",
conn_method_params=conn_method_params,
)
# Check correct output
assert "matrix" == marker.get_output_type(
@ -45,13 +65,6 @@ def test_EdgeCentricFCSpheres(tmp_path: Path) -> None:
assert len(set(edge_fc_bold["row_names"])) == n_edges
assert len(set(edge_fc_bold["col_names"])) == n_edges
# Check empirical correlation method parameters
marker = EdgeCentricFCSpheres(
coords="DMNBuckner",
radius=5.0,
cor_method="correlation",
cor_method_params={"empirical": True},
)
# Store
storage = SQLiteFeatureStorage(
uri=tmp_path / "test_edge_fc_spheres.sqlite", upsert="ignore"

View file

@ -6,10 +6,13 @@
# License: AGPL
from pathlib import Path
from typing import TYPE_CHECKING, Dict, Type
import pytest
from nilearn.connectome import ConnectivityMeasure
from nilearn.maskers import NiftiLabelsMasker
from numpy.testing import assert_array_almost_equal
from sklearn.covariance import EmpiricalCovariance, LedoitWolf
from junifer.data import get_parcellation
from junifer.datareader import DefaultDataReader
@ -20,19 +23,42 @@ from junifer.storage import SQLiteFeatureStorage
from junifer.testing.datagrabbers import PartlyCloudyTestingDataGrabber
def test_FunctionalConnectivityParcels(tmp_path: Path) -> None:
if TYPE_CHECKING:
from sklearn.base import BaseEstimator
@pytest.mark.parametrize(
"conn_method_params, cov_estimator",
[
({"empirical": False}, LedoitWolf(store_precision=False)),
({"empirical": True}, EmpiricalCovariance(store_precision=False)),
],
)
def test_FunctionalConnectivityParcels(
tmp_path: Path,
conn_method_params: Dict[str, bool],
cov_estimator: Type["BaseEstimator"],
) -> None:
"""Test FunctionalConnectivityParcels.
Parameters
----------
tmp_path : pathlib.Path
The path to the test directory.
conn_method_params : dict
The parametrized parameters to connectivity measure method.
cov_estimator : estimator object
The parametrized covariance estimator.
"""
with PartlyCloudyTestingDataGrabber() as dg:
# Get element data
element_data = DefaultDataReader().fit_transform(dg["sub-01"])
# Setup marker
marker = FunctionalConnectivityParcels(
parcellation="TianxS1x3TxMNInonlinear2009cAsym"
parcellation="TianxS1x3TxMNInonlinear2009cAsym",
conn_method="correlation",
conn_method_params=conn_method_params,
)
# Check correct output
assert "matrix" == marker.get_output_type(
@ -65,7 +91,7 @@ def test_FunctionalConnectivityParcels(tmp_path: Path) -> None:
)
# Compute the connectivity measure
connectivity_measure = ConnectivityMeasure(
kind="covariance"
cov_estimator=cov_estimator, kind="correlation" # type: ignore
).fit_transform([extracted_timeseries])[0]
# Check that FC are almost equal
@ -73,11 +99,6 @@ def test_FunctionalConnectivityParcels(tmp_path: Path) -> None:
connectivity_measure, fc_bold["data"], decimal=3
)
# Check empirical correlation method parameters
marker = FunctionalConnectivityParcels(
parcellation="TianxS1x3TxMNInonlinear2009cAsym",
cor_method_params={"empirical": True},
)
# Store
storage = SQLiteFeatureStorage(
uri=tmp_path / "test_fc_parcels.sqlite", upsert="ignore"

View file

@ -7,12 +7,13 @@
# License: AGPL
from pathlib import Path
from typing import TYPE_CHECKING, Dict, Type
import pytest
from nilearn.connectome import ConnectivityMeasure
from nilearn.maskers import NiftiSpheresMasker
from numpy.testing import assert_array_almost_equal
from sklearn.covariance import EmpiricalCovariance
from sklearn.covariance import EmpiricalCovariance, LedoitWolf
from junifer.data import get_coordinates
from junifer.datareader import DefaultDataReader
@ -23,19 +24,43 @@ from junifer.storage import SQLiteFeatureStorage
from junifer.testing.datagrabbers import SPMAuditoryTestingDataGrabber
def test_FunctionalConnectivitySpheres(tmp_path: Path) -> None:
if TYPE_CHECKING:
from sklearn.base import BaseEstimator
@pytest.mark.parametrize(
"conn_method_params, cov_estimator",
[
({"empirical": False}, LedoitWolf(store_precision=False)),
({"empirical": True}, EmpiricalCovariance(store_precision=False)),
],
)
def test_FunctionalConnectivitySpheres(
tmp_path: Path,
conn_method_params: Dict[str, bool],
cov_estimator: Type["BaseEstimator"],
) -> None:
"""Test FunctionalConnectivitySpheres.
Parameters
----------
tmp_path : pathlib.Path
The path to the test directory.
conn_method_params : dict
The parametrized parameters to connectivity measure method.
cov_estimator : estimator object
The parametrized covariance estimator.
"""
with SPMAuditoryTestingDataGrabber() as dg:
# Get element data
element_data = DefaultDataReader().fit_transform(dg["sub001"])
# Setup marker
marker = FunctionalConnectivitySpheres(
coords="DMNBuckner", radius=5.0, cor_method="correlation"
coords="DMNBuckner",
radius=5.0,
conn_method="correlation",
conn_method_params=conn_method_params,
)
# Check correct output
assert "matrix" == marker.get_output_type(
@ -67,7 +92,7 @@ def test_FunctionalConnectivitySpheres(tmp_path: Path) -> None:
)
# Compute the connectivity measure
connectivity_measure = ConnectivityMeasure(
kind="correlation"
cov_estimator=cov_estimator, kind="correlation" # type: ignore
).fit_transform([extracted_timeseries])[0]
# Check that FC are almost equal
@ -88,65 +113,9 @@ def test_FunctionalConnectivitySpheres(tmp_path: Path) -> None:
)
def test_FunctionalConnectivitySpheres_empirical(tmp_path: Path) -> None:
"""Test FunctionalConnectivitySpheres with empirical covariance.
Parameters
----------
tmp_path : pathlib.Path
The path to the test directory.
"""
with SPMAuditoryTestingDataGrabber() as dg:
element_data = DefaultDataReader().fit_transform(dg["sub001"])
marker = FunctionalConnectivitySpheres(
coords="DMNBuckner",
radius=5.0,
cor_method="correlation",
cor_method_params={"empirical": True},
)
# Check correct output
assert "matrix" == marker.get_output_type(
input_type="BOLD", output_feature="functional_connectivity"
)
# Fit-transform the data
fc = marker.fit_transform(element_data)
fc_bold = fc["BOLD"]["functional_connectivity"]
assert "data" in fc_bold
assert "row_names" in fc_bold
assert "col_names" in fc_bold
assert fc_bold["data"].shape == (6, 6)
assert len(set(fc_bold["row_names"])) == 6
assert len(set(fc_bold["col_names"])) == 6
# Compare with nilearn
# Load testing coordinates for the target data
testing_coords, _ = get_coordinates(
coords="DMNBuckner", target_data=element_data["BOLD"]
)
# Extract timeseries
nifti_spheres_masker = NiftiSpheresMasker(
seeds=testing_coords, radius=5.0
)
extracted_timeseries = nifti_spheres_masker.fit_transform(
element_data["BOLD"]["data"]
)
# Compute the connectivity measure
connectivity_measure = ConnectivityMeasure(
cov_estimator=EmpiricalCovariance(), kind="correlation" # type: ignore
).fit_transform([extracted_timeseries])[0]
# Check that FC are almost equal
assert_array_almost_equal(
connectivity_measure, fc_bold["data"], decimal=3
)
def test_FunctionalConnectivitySpheres_error() -> None:
"""Test FunctionalConnectivitySpheres errors."""
with pytest.raises(ValueError, match="radius should be > 0"):
FunctionalConnectivitySpheres(
coords="DMNBuckner", radius=-0.1, cor_method="correlation"
coords="DMNBuckner", radius=-0.1, conn_method="correlation"
)