From 42e5a8b28067200309f2ac6c873bc27bcccabc0e Mon Sep 17 00:00:00 2001 From: LeSasse Date: Thu, 5 Jan 2023 15:01:38 +0100 Subject: [PATCH 01/17] first version of base tsnr class --- .../markers/temporal_snr/temporal_snr_base.py | 116 ++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 junifer/markers/temporal_snr/temporal_snr_base.py diff --git a/junifer/markers/temporal_snr/temporal_snr_base.py b/junifer/markers/temporal_snr/temporal_snr_base.py new file mode 100644 index 000000000..20700796e --- /dev/null +++ b/junifer/markers/temporal_snr/temporal_snr_base.py @@ -0,0 +1,116 @@ +"""Provide abstract base class for temporal signal-to-noise ratio (tSNR).""" + +# Authors: Leonard Sasse +# License: AGPL + + +from abc import abstractmethod +from typing import Any, Dict, List, Optional + +from ...utils import raise_error +from ..utils import _voxelwise_tsnr +from ..base import BaseMarker + + +class TemporalSNRBase(BaseMarker): + """Abstract base class for temporal SNR markers. + + Parameters + ---------- + agg_method : str, optional + The method to perform aggregation using. Check valid options in + :func:`junifer.stats.get_aggfunc_by_name` (default "mean"). + agg_method_params : dict, optional + Parameters to pass to the aggregation function. Check valid options in + :func:`junifer.stats.get_aggfunc_by_name` (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). + name : str, optional + The name of the marker. If None, will use the class name (default + None). + + """ + + _DEPENDENCIES = {"nilearn"} + + def __init__( + self, + agg_method: str = "mean", + agg_method_params: Optional[Dict] = None, + mask: Optional[str] = None, + name: Optional[str] = None, + ) -> None: + self.agg_method = agg_method + self.agg_method_params = agg_method_params + + self.mask = mask + super().__init__(on="BOLD", name=name) + + @abstractmethod + def aggregate(self, input: Dict[str, Any]) -> Dict[str, Any]: + """Perform aggregation.""" + raise_error( + msg="Concrete classes need to implement aggregate().", + klass=NotImplementedError, + ) + + 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, Any], + 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. + + Returns + ------- + dict + The computed result as dictionary. The following keys will be + included in the dictionary: + + * ``data`` : functional connectivity matrix 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" + + """ + # calculate voxelwise temporal snr in an image + input = _voxelwise_tsnr(input) + + # Perform necessary aggregation and return + return self.aggregate(input) -- 2.52.0 From fcf0e239f95d77c144bed5dea6830f848f19e8ae Mon Sep 17 00:00:00 2001 From: LeSasse Date: Thu, 5 Jan 2023 15:02:01 +0100 Subject: [PATCH 02/17] implementation of voxelwise tsnr private function --- junifer/markers/utils.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/junifer/markers/utils.py b/junifer/markers/utils.py index 2b5f3ded0..a5e3ee2ba 100644 --- a/junifer/markers/utils.py +++ b/junifer/markers/utils.py @@ -12,6 +12,7 @@ from typing import Any, Callable, Dict, List, Optional, Tuple, Type, Union import numpy as np import pandas as pd from scipy.stats import zscore +from nilearn import image from ..utils import raise_error @@ -156,3 +157,34 @@ def _correlate_dataframes( .corr(method=method) # type: ignore .loc["df2", "df1"] ) + + +def _voxelwise_tsnr(input: Dict) -> Dict: + """Compute temporal signal-to-noise ratio per voxel. + + Parameters + ---------- + input : dict + A single input from the pipeline data object for which to compute + voxelwise tSNR. + + Returns + ------- + dict + A pipeline data object containing a 3D image with the voxelwise + tSNR map. + + """ + img = input["data"] + mean_img = image.math_img("img.mean(axis=-1).squeeze()", img=img) + stdv_img = image.math_img("img.std(axis=-1).squeeze()", img=img) + mask_img = image.math_img("(stdv_img != 0)", stdv_img=stdv_img) + + # this returns the tsnr voxelwise image + input["data"] = image.math_img( + "np.divide(mean_img, stdv_img, where=mask_img.astype(bool))", + mean_img=mean_img, + stdv_img=stdv_img, + mask_img=mask_img, + ) + return input -- 2.52.0 From c51f6e334101f99f44c46be09eb3df42eb275bf5 Mon Sep 17 00:00:00 2001 From: LeSasse Date: Thu, 5 Jan 2023 15:08:24 +0100 Subject: [PATCH 03/17] add concrete tSNR implementations for spheres and parcels --- .../temporal_snr/temporal_snr_parcels.py | 64 ++++++++++++++++ .../temporal_snr/temporal_snr_spheres.py | 75 +++++++++++++++++++ 2 files changed, 139 insertions(+) create mode 100644 junifer/markers/temporal_snr/temporal_snr_parcels.py create mode 100644 junifer/markers/temporal_snr/temporal_snr_spheres.py diff --git a/junifer/markers/temporal_snr/temporal_snr_parcels.py b/junifer/markers/temporal_snr/temporal_snr_parcels.py new file mode 100644 index 000000000..e55ff6b6a --- /dev/null +++ b/junifer/markers/temporal_snr/temporal_snr_parcels.py @@ -0,0 +1,64 @@ +"""Provide class for temporal SNR using parcels.""" + +# Authors: Leonard Sasse +# License: AGPL + +from typing import Any, Dict, List, Optional, Union + +from ...api.decorators import register_marker +from ..parcel_aggregation import ParcelAggregation +from .temporal_snr_base import TemporalSNRBase + + +@register_marker +class TemporalSNRParcels(TemporalSNRBase): + """Class for temporal signal-to-noise ratio using parcellations. + + 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`. + agg_method : str, optional + The method to perform aggregation using. Check valid options in + :func:`junifer.stats.get_aggfunc_by_name` (default "mean"). + agg_method_params : dict, optional + Parameters to pass to the aggregation function. Check valid options in + :func:`junifer.stats.get_aggfunc_by_name` (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). + name : str, optional + The name of the marker. If None, will use the class name (default + None). + + """ + + def __init__( + self, + parcellation: Union[str, List[str]], + agg_method: str = "mean", + agg_method_params: Optional[Dict] = None, + mask: Optional[str] = None, + name: Optional[str] = None, + ) -> None: + self.parcellation = parcellation + super().__init__( + agg_method=agg_method, + agg_method_params=agg_method_params, + mask=mask, + name=name, + ) + + def aggregate(self, input: Dict[str, Any]) -> Dict: + """Perform parcel aggregation.""" + parcel_aggregation = ParcelAggregation( + parcellation=self.parcellation, + method=self.agg_method, + method_params=self.agg_method_params, + mask=self.mask, + on="BOLD", + ) + # Return the 2D timeseries after parcel aggregation + return parcel_aggregation.compute(input) diff --git a/junifer/markers/temporal_snr/temporal_snr_spheres.py b/junifer/markers/temporal_snr/temporal_snr_spheres.py new file mode 100644 index 000000000..c4cf1be61 --- /dev/null +++ b/junifer/markers/temporal_snr/temporal_snr_spheres.py @@ -0,0 +1,75 @@ +"""Provide class for temporal SNR using spheres.""" + +# Authors: Leonard Sasse +# License: AGPL + +from typing import Any, Dict, Optional + +from ...api.decorators import register_marker +from ..sphere_aggregation import SphereAggregation +from ..utils import raise_error +from .temporal_snr_base import TemporalSNRBase + + +@register_marker +class TemporalSNRSpheres(TemporalSNRBase): + """Class for temporal signal-to-noise ratio using coordinates (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). + agg_method : str, optional + The aggregation method to use. + See :func:`junifer.stats.get_aggfunc_by_name` for more information + (default None). + agg_method_params : dict, optional + The parameters to pass to the aggregation method (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). + 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). + + """ + + def __init__( + self, + coords: str, + radius: Optional[float] = None, + agg_method: str = "mean", + agg_method_params: Optional[Dict] = None, + mask: Optional[str] = None, + name: Optional[str] = None, + ) -> None: + self.coords = coords + self.radius = radius + if radius is None or radius <= 0: + raise_error(f"radius should be > 0: provided {radius}") + super().__init__( + agg_method=agg_method, + agg_method_params=agg_method_params, + mask=mask, + name=name, + ) + + def aggregate(self, input: Dict[str, Any]) -> Dict: + """Perform sphere aggregation.""" + sphere_aggregation = SphereAggregation( + coords=self.coords, + radius=self.radius, + method=self.agg_method, + method_params=self.agg_method_params, + mask=self.mask, + on="BOLD", + ) + # Return the 2D timeseries after sphere aggregation + return sphere_aggregation.compute(input) -- 2.52.0 From 1e357fcb5d548b40f8e0db22876e6073f9c622ba Mon Sep 17 00:00:00 2001 From: LeSasse Date: Thu, 5 Jan 2023 15:17:40 +0100 Subject: [PATCH 04/17] add test for abstract tsnr base class --- .../temporal_snr/tests/test_temporal_snr_base.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 junifer/markers/temporal_snr/tests/test_temporal_snr_base.py diff --git a/junifer/markers/temporal_snr/tests/test_temporal_snr_base.py b/junifer/markers/temporal_snr/tests/test_temporal_snr_base.py new file mode 100644 index 000000000..396dd6300 --- /dev/null +++ b/junifer/markers/temporal_snr/tests/test_temporal_snr_base.py @@ -0,0 +1,15 @@ +"""Provide tests for base temporal SNR marker.""" + +# Authors: Leonard Sasse +# License: AGPL + +import pytest + +# done to keep line length 79 +import junifer.markers.temporal_snr as tsnr + + +def test_base_temporal_snr_marker_abstractness() -> None: + """Test TemporalSNRBase is an abstract base class.""" + with pytest.raises(TypeError, match="abstract"): + tsnr.temporal_snr_base.TemporalSNRBase() -- 2.52.0 From c1e256c2711ccaabfc73bfb3fdb435a39ecb5c54 Mon Sep 17 00:00:00 2001 From: LeSasse Date: Thu, 5 Jan 2023 15:20:14 +0100 Subject: [PATCH 05/17] add imports to __init__.py --- junifer/markers/temporal_snr/__init__.py | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 junifer/markers/temporal_snr/__init__.py diff --git a/junifer/markers/temporal_snr/__init__.py b/junifer/markers/temporal_snr/__init__.py new file mode 100644 index 000000000..ca150f982 --- /dev/null +++ b/junifer/markers/temporal_snr/__init__.py @@ -0,0 +1,7 @@ +"""Provide imports for temporal signal-to-noise ratio sub-package.""" + +# Authors: Leonard Sasse +# License: AGPL + +from .temporal_snr_parcels import TemporalSNRParcels +from .temporal_snr_spheres import TemporalSNRSpheres -- 2.52.0 From 519bcc6d2a9584c54dac1aa0700cd08e5b46a3b3 Mon Sep 17 00:00:00 2001 From: LeSasse Date: Thu, 5 Jan 2023 15:43:22 +0100 Subject: [PATCH 06/17] add some more linting and testing --- .../markers/temporal_snr/temporal_snr_base.py | 3 +- .../tests/test_temporal_snr_parcels.py | 54 ++++++++++++++++ .../tests/test_temporal_snr_spheres.py | 64 +++++++++++++++++++ junifer/markers/utils.py | 2 +- 4 files changed, 120 insertions(+), 3 deletions(-) create mode 100644 junifer/markers/temporal_snr/tests/test_temporal_snr_parcels.py create mode 100644 junifer/markers/temporal_snr/tests/test_temporal_snr_spheres.py diff --git a/junifer/markers/temporal_snr/temporal_snr_base.py b/junifer/markers/temporal_snr/temporal_snr_base.py index 20700796e..10882dcb4 100644 --- a/junifer/markers/temporal_snr/temporal_snr_base.py +++ b/junifer/markers/temporal_snr/temporal_snr_base.py @@ -8,8 +8,8 @@ from abc import abstractmethod from typing import Any, Dict, List, Optional from ...utils import raise_error -from ..utils import _voxelwise_tsnr from ..base import BaseMarker +from ..utils import _voxelwise_tsnr class TemporalSNRBase(BaseMarker): @@ -106,7 +106,6 @@ class TemporalSNRBase(BaseMarker): * ``data`` : functional connectivity matrix 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" """ # calculate voxelwise temporal snr in an image diff --git a/junifer/markers/temporal_snr/tests/test_temporal_snr_parcels.py b/junifer/markers/temporal_snr/tests/test_temporal_snr_parcels.py new file mode 100644 index 000000000..85de50b18 --- /dev/null +++ b/junifer/markers/temporal_snr/tests/test_temporal_snr_parcels.py @@ -0,0 +1,54 @@ +"""Provide tests for temporal signal-to-noise ratio using parcellation.""" + +# Authors: Leonard Sasse +# License: AGPL + +from pathlib import Path + +from nilearn import datasets, image + +from junifer.markers.temporal_snr import TemporalSNRParcels +from junifer.storage import SQLiteFeatureStorage + + +def test_TemporalSNRParcels(tmp_path: Path) -> None: + """Test TemporalSNRParcels. + + Parameters + ---------- + tmp_path : pathlib.Path + The path to the test directory. + + """ + # get a dataset + ni_data = datasets.fetch_spm_auditory(subject_id="sub001") + fmri_img = image.concat_imgs(ni_data.func) # type: ignore + + tsnr_parcels = TemporalSNRParcels(parcellation="Schaefer100x7") + all_out = tsnr_parcels.fit_transform( + {"BOLD": {"data": fmri_img, "meta": {}}} + ) + + out = all_out["BOLD"] + + assert "data" in out + assert "columns" in out + + assert out["data"].shape[0] == 1 + assert out["data"].shape[1] == 100 + assert len(set(out["columns"])) == 100 + + # check correct output + assert tsnr_parcels.get_output_type("BOLD") == "table" + + uri = tmp_path / "test_tsnr_parcellation.sqlite" + # Single storage, must be the uri + storage = SQLiteFeatureStorage(uri=uri, upsert="ignore") + meta = {"element": {"subject": "test"}, "dependencies": {"numpy"}} + input = {"BOLD": {"data": fmri_img, "meta": meta}} + all_out = tsnr_parcels.fit_transform(input, storage=storage) + + features = storage.list_features() + assert any( + x["name"] == "BOLD_TemporalSNRParcels" for x in features.values() + ) diff --git a/junifer/markers/temporal_snr/tests/test_temporal_snr_spheres.py b/junifer/markers/temporal_snr/tests/test_temporal_snr_spheres.py new file mode 100644 index 000000000..bdb179547 --- /dev/null +++ b/junifer/markers/temporal_snr/tests/test_temporal_snr_spheres.py @@ -0,0 +1,64 @@ +"""Provide tests for temporal signal-to-noise spheres.""" + +# Authors: Leonard Sasse +# License: AGPL + +from pathlib import Path + +import pytest +from nilearn import datasets, image + +from junifer.markers.temporal_snr import TemporalSNRSpheres +from junifer.storage import SQLiteFeatureStorage + + +def test_TemporalSNRSpheres(tmp_path: Path) -> None: + """Test TemporalSNRSpheres. + + Parameters + ---------- + tmp_path : pathlib.Path + The path to the test directory. + + """ + # get a dataset + ni_data = datasets.fetch_spm_auditory(subject_id="sub001") + fmri_img = image.concat_imgs(ni_data.func) # type: ignore + + tsnr_spheres = TemporalSNRSpheres(coords="DMNBuckner", radius=5.0) + all_out = tsnr_spheres.fit_transform( + {"BOLD": {"data": fmri_img, "meta": {}}} + ) + + out = all_out["BOLD"] + + assert "data" in out + assert "columns" in out + assert out["data"].shape[0] == 1 + assert out["data"].shape[1] == 6 + assert len(set(out["columns"])) == 6 + + # check correct output + assert tsnr_spheres.get_output_type("BOLD") == "table" + + uri = tmp_path / "test_tsnr_coords.sqlite" + # Single storage, must be the uri + storage = SQLiteFeatureStorage(uri=uri, upsert="ignore") + meta = { + "element": {"subject": "test"}, + "dependencies": {"numpy", "nilearn"}, + } + input = {"BOLD": {"data": fmri_img, "meta": meta}} + all_out = tsnr_spheres.fit_transform(input, storage=storage) + + features = storage.list_features() + assert any( + x["name"] == "BOLD_TemporalSNRSpheres" + for x in features.values() + ) + + +def test_TemporalSNRSpheres_error() -> None: + """Test TemporalSNRSpheres errors.""" + with pytest.raises(ValueError, match="radius should be > 0"): + TemporalSNRSpheres(coords="DMNBuckner", radius=-0.1) diff --git a/junifer/markers/utils.py b/junifer/markers/utils.py index a5e3ee2ba..4021a3bc3 100644 --- a/junifer/markers/utils.py +++ b/junifer/markers/utils.py @@ -11,8 +11,8 @@ from typing import Any, Callable, Dict, List, Optional, Tuple, Type, Union import numpy as np import pandas as pd -from scipy.stats import zscore from nilearn import image +from scipy.stats import zscore from ..utils import raise_error -- 2.52.0 From fb84e8207b7d1ebeb31a5cfcb20b97eeb3288a01 Mon Sep 17 00:00:00 2001 From: LeSasse Date: Thu, 5 Jan 2023 15:50:51 +0100 Subject: [PATCH 07/17] add entry to latest --- docs/changes/latest.inc | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/changes/latest.inc b/docs/changes/latest.inc index a54440683..ff0da3932 100644 --- a/docs/changes/latest.inc +++ b/docs/changes/latest.inc @@ -43,6 +43,9 @@ Enhancements - Add fMRIPrep brain masks to the datagrabber patterns for all datagrabbers in the aomic sub-package (:gh:`177` by `Leonard Sasse`_). +- Add :class:`junifer.markers.TemporalSNRParcels` and :class:`junifer.markers.TemporalSNRSpheres` + (:gh:`163` by `Leonard Sasse`_). + Bugs ~~~~ -- 2.52.0 From 30b85bc82d3d2d2e37cdf65c72dc0e68907af269 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Tue, 28 Feb 2023 15:41:03 +0100 Subject: [PATCH 08/17] update: rename mask to masks in TemporalSNR markers --- .../markers/temporal_snr/temporal_snr_base.py | 15 +++++++-------- .../markers/temporal_snr/temporal_snr_parcels.py | 14 +++++++------- .../markers/temporal_snr/temporal_snr_spheres.py | 16 ++++++++-------- 3 files changed, 22 insertions(+), 23 deletions(-) diff --git a/junifer/markers/temporal_snr/temporal_snr_base.py b/junifer/markers/temporal_snr/temporal_snr_base.py index 10882dcb4..c968bdca1 100644 --- a/junifer/markers/temporal_snr/temporal_snr_base.py +++ b/junifer/markers/temporal_snr/temporal_snr_base.py @@ -5,7 +5,7 @@ from abc import abstractmethod -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List, Optional, Union from ...utils import raise_error from ..base import BaseMarker @@ -23,10 +23,10 @@ class TemporalSNRBase(BaseMarker): agg_method_params : dict, optional Parameters to pass to the aggregation function. Check valid options in :func:`junifer.stats.get_aggfunc_by_name` (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). + 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 ` 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). @@ -39,13 +39,12 @@ class TemporalSNRBase(BaseMarker): self, agg_method: str = "mean", agg_method_params: Optional[Dict] = None, - mask: Optional[str] = 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.mask = mask + self.masks = masks super().__init__(on="BOLD", name=name) @abstractmethod diff --git a/junifer/markers/temporal_snr/temporal_snr_parcels.py b/junifer/markers/temporal_snr/temporal_snr_parcels.py index e55ff6b6a..55d1507fd 100644 --- a/junifer/markers/temporal_snr/temporal_snr_parcels.py +++ b/junifer/markers/temporal_snr/temporal_snr_parcels.py @@ -25,10 +25,10 @@ class TemporalSNRParcels(TemporalSNRBase): agg_method_params : dict, optional Parameters to pass to the aggregation function. Check valid options in :func:`junifer.stats.get_aggfunc_by_name` (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). + 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 ` 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). @@ -40,14 +40,14 @@ class TemporalSNRParcels(TemporalSNRBase): parcellation: Union[str, List[str]], agg_method: str = "mean", agg_method_params: Optional[Dict] = None, - mask: Optional[str] = None, + masks: Optional[str] = None, name: Optional[str] = None, ) -> None: self.parcellation = parcellation super().__init__( agg_method=agg_method, agg_method_params=agg_method_params, - mask=mask, + masks=masks, name=name, ) @@ -57,7 +57,7 @@ class TemporalSNRParcels(TemporalSNRBase): parcellation=self.parcellation, method=self.agg_method, method_params=self.agg_method_params, - mask=self.mask, + masks=self.masks, on="BOLD", ) # Return the 2D timeseries after parcel aggregation diff --git a/junifer/markers/temporal_snr/temporal_snr_spheres.py b/junifer/markers/temporal_snr/temporal_snr_spheres.py index c4cf1be61..6d9f7bc54 100644 --- a/junifer/markers/temporal_snr/temporal_snr_spheres.py +++ b/junifer/markers/temporal_snr/temporal_snr_spheres.py @@ -3,7 +3,7 @@ # Authors: Leonard Sasse # License: AGPL -from typing import Any, Dict, Optional +from typing import Any, Dict, List, Optional, Union from ...api.decorators import register_marker from ..sphere_aggregation import SphereAggregation @@ -30,10 +30,10 @@ class TemporalSNRSpheres(TemporalSNRBase): (default None). agg_method_params : dict, optional The parameters to pass to the aggregation method (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). + 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 ` 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 @@ -47,7 +47,7 @@ class TemporalSNRSpheres(TemporalSNRBase): radius: Optional[float] = None, agg_method: str = "mean", agg_method_params: Optional[Dict] = None, - mask: Optional[str] = None, + masks: Union[str, Dict, List[Union[Dict, str]], None] = None, name: Optional[str] = None, ) -> None: self.coords = coords @@ -57,7 +57,7 @@ class TemporalSNRSpheres(TemporalSNRBase): super().__init__( agg_method=agg_method, agg_method_params=agg_method_params, - mask=mask, + masks=masks, name=name, ) @@ -68,7 +68,7 @@ class TemporalSNRSpheres(TemporalSNRBase): radius=self.radius, method=self.agg_method, method_params=self.agg_method_params, - mask=self.mask, + masks=self.masks, on="BOLD", ) # Return the 2D timeseries after sphere aggregation -- 2.52.0 From 571ff3ea6285d7d6a39d945a30e066034a37f9ad Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Tue, 28 Feb 2023 15:41:36 +0100 Subject: [PATCH 09/17] update: add TemporalSNR markers in markers.__init__ --- junifer/markers/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/junifer/markers/__init__.py b/junifer/markers/__init__.py index badbd2f30..26f259edf 100644 --- a/junifer/markers/__init__.py +++ b/junifer/markers/__init__.py @@ -22,3 +22,7 @@ from .falff import ( AmplitudeLowFrequencyFluctuationParcels, AmplitudeLowFrequencyFluctuationSpheres, ) +from .temporal_snr import ( + TemporalSNRParcels, + TemporalSNRSpheres, +) -- 2.52.0 From 03b930125bf4b559aeb192b475344fb9dbbda07e Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Tue, 28 Feb 2023 15:42:06 +0100 Subject: [PATCH 10/17] update: rename table to vector in TemporalSNRBase --- junifer/markers/temporal_snr/temporal_snr_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/junifer/markers/temporal_snr/temporal_snr_base.py b/junifer/markers/temporal_snr/temporal_snr_base.py index c968bdca1..9eb180688 100644 --- a/junifer/markers/temporal_snr/temporal_snr_base.py +++ b/junifer/markers/temporal_snr/temporal_snr_base.py @@ -79,7 +79,7 @@ class TemporalSNRBase(BaseMarker): The storage type output by the marker. """ - return "table" + return "vector" def compute( self, -- 2.52.0 From a8bac56778709599eed1e5af2d60572cc03513c6 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Tue, 28 Feb 2023 15:42:25 +0100 Subject: [PATCH 11/17] chore: update docstring for TemporalSNRBase --- junifer/markers/temporal_snr/temporal_snr_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/junifer/markers/temporal_snr/temporal_snr_base.py b/junifer/markers/temporal_snr/temporal_snr_base.py index 9eb180688..05e0218d5 100644 --- a/junifer/markers/temporal_snr/temporal_snr_base.py +++ b/junifer/markers/temporal_snr/temporal_snr_base.py @@ -104,7 +104,7 @@ class TemporalSNRBase(BaseMarker): included in the dictionary: * ``data`` : functional connectivity matrix as a ``numpy.ndarray``. - * ``columns`` : the column labels for the computed values as a list + * ``col_names`` : the column labels for the computed values as list """ # calculate voxelwise temporal snr in an image -- 2.52.0 From d7b16840724579c532071ee3d82b1db5277d91a2 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Tue, 28 Feb 2023 15:44:46 +0100 Subject: [PATCH 12/17] update: fix tests for TemporalSNR markers --- .../temporal_snr/tests/test_temporal_snr_parcels.py | 6 +++--- .../temporal_snr/tests/test_temporal_snr_spheres.py | 9 ++++----- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/junifer/markers/temporal_snr/tests/test_temporal_snr_parcels.py b/junifer/markers/temporal_snr/tests/test_temporal_snr_parcels.py index 85de50b18..e0ef668f4 100644 --- a/junifer/markers/temporal_snr/tests/test_temporal_snr_parcels.py +++ b/junifer/markers/temporal_snr/tests/test_temporal_snr_parcels.py @@ -32,14 +32,14 @@ def test_TemporalSNRParcels(tmp_path: Path) -> None: out = all_out["BOLD"] assert "data" in out - assert "columns" in out + assert "col_names" in out assert out["data"].shape[0] == 1 assert out["data"].shape[1] == 100 - assert len(set(out["columns"])) == 100 + assert len(set(out["col_names"])) == 100 # check correct output - assert tsnr_parcels.get_output_type("BOLD") == "table" + assert tsnr_parcels.get_output_type("BOLD") == "vector" uri = tmp_path / "test_tsnr_parcellation.sqlite" # Single storage, must be the uri diff --git a/junifer/markers/temporal_snr/tests/test_temporal_snr_spheres.py b/junifer/markers/temporal_snr/tests/test_temporal_snr_spheres.py index bdb179547..448de5e54 100644 --- a/junifer/markers/temporal_snr/tests/test_temporal_snr_spheres.py +++ b/junifer/markers/temporal_snr/tests/test_temporal_snr_spheres.py @@ -33,13 +33,13 @@ def test_TemporalSNRSpheres(tmp_path: Path) -> None: out = all_out["BOLD"] assert "data" in out - assert "columns" in out + assert "col_names" in out assert out["data"].shape[0] == 1 assert out["data"].shape[1] == 6 - assert len(set(out["columns"])) == 6 + assert len(set(out["col_names"])) == 6 # check correct output - assert tsnr_spheres.get_output_type("BOLD") == "table" + assert tsnr_spheres.get_output_type("BOLD") == "vector" uri = tmp_path / "test_tsnr_coords.sqlite" # Single storage, must be the uri @@ -53,8 +53,7 @@ def test_TemporalSNRSpheres(tmp_path: Path) -> None: features = storage.list_features() assert any( - x["name"] == "BOLD_TemporalSNRSpheres" - for x in features.values() + x["name"] == "BOLD_TemporalSNRSpheres" for x in features.values() ) -- 2.52.0 From d94314b58fa9a67a66ee2fbb5e87f96f83606362 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Fri, 3 Mar 2023 15:54:59 +0100 Subject: [PATCH 13/17] update: correct type annotation for TemporalSNRParcels constructor --- junifer/markers/temporal_snr/temporal_snr_parcels.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/junifer/markers/temporal_snr/temporal_snr_parcels.py b/junifer/markers/temporal_snr/temporal_snr_parcels.py index 55d1507fd..6a5e378a8 100644 --- a/junifer/markers/temporal_snr/temporal_snr_parcels.py +++ b/junifer/markers/temporal_snr/temporal_snr_parcels.py @@ -40,7 +40,7 @@ class TemporalSNRParcels(TemporalSNRBase): parcellation: Union[str, List[str]], agg_method: str = "mean", agg_method_params: Optional[Dict] = None, - masks: Optional[str] = None, + masks: Union[str, Dict, List[Union[Dict, str]], None] = None, name: Optional[str] = None, ) -> None: self.parcellation = parcellation -- 2.52.0 From 3a248a1875b8b960956d0e918431047e82e66f35 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Fri, 3 Mar 2023 16:16:30 +0100 Subject: [PATCH 14/17] refactor: move voxelwise tSNR calculation to TemporalSNRBase --- .../markers/temporal_snr/temporal_snr_base.py | 20 +++++++++--- junifer/markers/utils.py | 32 ------------------- 2 files changed, 16 insertions(+), 36 deletions(-) diff --git a/junifer/markers/temporal_snr/temporal_snr_base.py b/junifer/markers/temporal_snr/temporal_snr_base.py index 05e0218d5..2a11f6dae 100644 --- a/junifer/markers/temporal_snr/temporal_snr_base.py +++ b/junifer/markers/temporal_snr/temporal_snr_base.py @@ -7,9 +7,10 @@ from abc import abstractmethod from typing import Any, Dict, List, Optional, Union +from nilearn import image as nimg + from ...utils import raise_error from ..base import BaseMarker -from ..utils import _voxelwise_tsnr class TemporalSNRBase(BaseMarker): @@ -107,8 +108,19 @@ class TemporalSNRBase(BaseMarker): * ``col_names`` : the column labels for the computed values as list """ - # calculate voxelwise temporal snr in an image - input = _voxelwise_tsnr(input) - + # Calculate voxelwise temporal signal-to-noise ratio in an image + mean_img = nimg.math_img( + "img.mean(axis=-1).squeeze()", img=input["data"] + ) + stdv_img = nimg.math_img( + "img.std(axis=-1).squeeze()", img=input["data"] + ) + mask_img = nimg.math_img("(stdv_img != 0)", stdv_img=stdv_img) + input["data"] = nimg.math_img( + "np.divide(mean_img, stdv_img, where=mask_img.astype(bool))", + mean_img=mean_img, + stdv_img=stdv_img, + mask_img=mask_img, + ) # Perform necessary aggregation and return return self.aggregate(input) diff --git a/junifer/markers/utils.py b/junifer/markers/utils.py index 4021a3bc3..2b5f3ded0 100644 --- a/junifer/markers/utils.py +++ b/junifer/markers/utils.py @@ -11,7 +11,6 @@ from typing import Any, Callable, Dict, List, Optional, Tuple, Type, Union import numpy as np import pandas as pd -from nilearn import image from scipy.stats import zscore from ..utils import raise_error @@ -157,34 +156,3 @@ def _correlate_dataframes( .corr(method=method) # type: ignore .loc["df2", "df1"] ) - - -def _voxelwise_tsnr(input: Dict) -> Dict: - """Compute temporal signal-to-noise ratio per voxel. - - Parameters - ---------- - input : dict - A single input from the pipeline data object for which to compute - voxelwise tSNR. - - Returns - ------- - dict - A pipeline data object containing a 3D image with the voxelwise - tSNR map. - - """ - img = input["data"] - mean_img = image.math_img("img.mean(axis=-1).squeeze()", img=img) - stdv_img = image.math_img("img.std(axis=-1).squeeze()", img=img) - mask_img = image.math_img("(stdv_img != 0)", stdv_img=stdv_img) - - # this returns the tsnr voxelwise image - input["data"] = image.math_img( - "np.divide(mean_img, stdv_img, where=mask_img.astype(bool))", - mean_img=mean_img, - stdv_img=stdv_img, - mask_img=mask_img, - ) - return input -- 2.52.0 From e6e978c93792a80303e752d409d4df441b98c235 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Fri, 3 Mar 2023 16:18:19 +0100 Subject: [PATCH 15/17] update: allow extra_input parameter to be passed to the aggregation in tSNR markers --- .../markers/temporal_snr/temporal_snr_base.py | 6 ++-- .../temporal_snr/temporal_snr_parcels.py | 31 +++++++++++++++++-- .../temporal_snr/temporal_snr_spheres.py | 31 +++++++++++++++++-- 3 files changed, 60 insertions(+), 8 deletions(-) diff --git a/junifer/markers/temporal_snr/temporal_snr_base.py b/junifer/markers/temporal_snr/temporal_snr_base.py index 2a11f6dae..3edce9dad 100644 --- a/junifer/markers/temporal_snr/temporal_snr_base.py +++ b/junifer/markers/temporal_snr/temporal_snr_base.py @@ -49,7 +49,9 @@ class TemporalSNRBase(BaseMarker): super().__init__(on="BOLD", name=name) @abstractmethod - def aggregate(self, input: Dict[str, Any]) -> Dict[str, Any]: + def aggregate( + self, input: Dict[str, Any], extra_input: Optional[Dict] = None + ) -> Dict[str, Any]: """Perform aggregation.""" raise_error( msg="Concrete classes need to implement aggregate().", @@ -123,4 +125,4 @@ class TemporalSNRBase(BaseMarker): mask_img=mask_img, ) # Perform necessary aggregation and return - return self.aggregate(input) + return self.aggregate(input=input, extra_input=extra_input) diff --git a/junifer/markers/temporal_snr/temporal_snr_parcels.py b/junifer/markers/temporal_snr/temporal_snr_parcels.py index 6a5e378a8..0521527de 100644 --- a/junifer/markers/temporal_snr/temporal_snr_parcels.py +++ b/junifer/markers/temporal_snr/temporal_snr_parcels.py @@ -51,8 +51,33 @@ class TemporalSNRParcels(TemporalSNRBase): name=name, ) - def aggregate(self, input: Dict[str, Any]) -> Dict: - """Perform parcel aggregation.""" + def aggregate( + self, input: Dict[str, Any], extra_input: Optional[Dict] = None + ) -> Dict: + """Perform parcel aggregation. + + Parameters + ---------- + input : dict + A single input from the pipeline data object in which the data + is the voxelwise temporal SNR map. + 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`` : ROI-wise temporal SNR as a ``numpy.ndarray`` + * ``col_names`` : the ROI labels for the computed values as list + + """ parcel_aggregation = ParcelAggregation( parcellation=self.parcellation, method=self.agg_method, @@ -61,4 +86,4 @@ class TemporalSNRParcels(TemporalSNRBase): on="BOLD", ) # Return the 2D timeseries after parcel aggregation - return parcel_aggregation.compute(input) + return parcel_aggregation.compute(input=input, extra_input=extra_input) diff --git a/junifer/markers/temporal_snr/temporal_snr_spheres.py b/junifer/markers/temporal_snr/temporal_snr_spheres.py index 6d9f7bc54..402d4aa47 100644 --- a/junifer/markers/temporal_snr/temporal_snr_spheres.py +++ b/junifer/markers/temporal_snr/temporal_snr_spheres.py @@ -61,8 +61,33 @@ class TemporalSNRSpheres(TemporalSNRBase): name=name, ) - def aggregate(self, input: Dict[str, Any]) -> Dict: - """Perform sphere aggregation.""" + def aggregate( + self, input: Dict[str, Any], extra_input: Optional[Dict] = None + ) -> Dict: + """Perform sphere aggregation. + + Parameters + ---------- + input : dict + A single input from the pipeline data object in which the data + is the voxelwise temporal SNR map. + 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`` : VOI-wise temporal SNR as a ``numpy.ndarray`` + * ``col_names`` : the VOI labels for the computed values as list + + """ sphere_aggregation = SphereAggregation( coords=self.coords, radius=self.radius, @@ -72,4 +97,4 @@ class TemporalSNRSpheres(TemporalSNRBase): on="BOLD", ) # Return the 2D timeseries after sphere aggregation - return sphere_aggregation.compute(input) + return sphere_aggregation.compute(input=input, extra_input=extra_input) -- 2.52.0 From 8678ec5c80205d3897b8c32f51336f990f7bc003 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Fri, 3 Mar 2023 16:19:18 +0100 Subject: [PATCH 16/17] chore: improve docstring for TemporalSNRBase --- junifer/markers/temporal_snr/temporal_snr_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/junifer/markers/temporal_snr/temporal_snr_base.py b/junifer/markers/temporal_snr/temporal_snr_base.py index 3edce9dad..7679c6c8e 100644 --- a/junifer/markers/temporal_snr/temporal_snr_base.py +++ b/junifer/markers/temporal_snr/temporal_snr_base.py @@ -106,7 +106,7 @@ class TemporalSNRBase(BaseMarker): The computed result as dictionary. The following keys will be included in the dictionary: - * ``data`` : functional connectivity matrix as a ``numpy.ndarray``. + * ``data`` : the computed values as a ``numpy.ndarray`` * ``col_names`` : the column labels for the computed values as list """ -- 2.52.0 From 1fdcb8cacdec162be98d1e6a17964c279dff397c Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Wed, 8 Mar 2023 11:23:28 +0100 Subject: [PATCH 17/17] docs: add entries for temporal SNR markers to builtin.rst --- docs/builtin.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/builtin.rst b/docs/builtin.rst index 2f12e04ae..bc6876bef 100644 --- a/docs/builtin.rst +++ b/docs/builtin.rst @@ -192,6 +192,14 @@ Available as found in `Jo et al. (2021) `_ - Done - 0.0.2 + * - :class:`junifer.markers.TemporalSNRParcels` + - Calculate temporal signal-to-noise ratio using parcellations + - Done + - 0.0.2 + * - :class:`junifer.markers.TemporalSNRSpheres` + - Calculate temporal signal-to-noise ratio using spheres placed on coordinates + - Done + - 0.0.2 Planned ~~~~~~~ -- 2.52.0