From 252d06b4f0ad3b801f9d1684ae018e17d88645de Mon Sep 17 00:00:00 2001 From: Fede Raimondo Date: Tue, 22 Nov 2022 17:58:14 +0100 Subject: [PATCH 1/8] WIP: [ENH]: Add support for masks (GM/WM/etc) #79 --- junifer/data/__init__.py | 3 + junifer/data/masks.py | 186 ++++++++++++++++++ ...XI555_MNI152_TMP_GS_GMprob0.2_clean.nii.gz | Bin 0 -> 88270 bytes ...5_MNI152_TMP_GS_GMprob0.2_clean_3mm.nii.gz | Bin 0 -> 12862 bytes .../GMprob0.2_cortex_3mm_NA_rm.nii.gz | Bin 0 -> 8700 bytes junifer/data/parcellations.py | 42 +--- junifer/data/tests/test_data_utils.py | 33 ++++ junifer/data/tests/test_masks.py | 152 ++++++++++++++ junifer/data/utils.py | 41 ++++ 9 files changed, 419 insertions(+), 38 deletions(-) create mode 100644 junifer/data/masks.py create mode 100644 junifer/data/masks/vickery-patil/CAT12_IXI555_MNI152_TMP_GS_GMprob0.2_clean.nii.gz create mode 100644 junifer/data/masks/vickery-patil/CAT12_IXI555_MNI152_TMP_GS_GMprob0.2_clean_3mm.nii.gz create mode 100644 junifer/data/masks/vickery-patil/GMprob0.2_cortex_3mm_NA_rm.nii.gz create mode 100644 junifer/data/tests/test_data_utils.py create mode 100644 junifer/data/tests/test_masks.py create mode 100644 junifer/data/utils.py diff --git a/junifer/data/__init__.py b/junifer/data/__init__.py index 1ed8977f3..2429a0c62 100644 --- a/junifer/data/__init__.py +++ b/junifer/data/__init__.py @@ -14,3 +14,6 @@ from .parcellations import ( load_parcellation, register_parcellation, ) + + +from . import utils \ No newline at end of file diff --git a/junifer/data/masks.py b/junifer/data/masks.py new file mode 100644 index 000000000..4c84770a4 --- /dev/null +++ b/junifer/data/masks.py @@ -0,0 +1,186 @@ +"""Provide functions for masks.""" + +# Authors: Federico Raimondo +# License: AGPL + +from pathlib import Path +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union + +import nibabel as nib + +from .utils import closest_resolution +from ..utils.logging import logger, raise_error + + +if TYPE_CHECKING: + from nibabel import Nifti1Image + +# Path to the VOIs +_masks_path = Path(__file__).parent / "masks" + +""" +A dictionary containing all supported masks and their respective file or +data. + +The built-in masks are files that are shipped with the package in the +data/masks directory. The user can also register their own masks. +""" +_available_masks: Dict[str, Dict[Any, Any]] = { + "GM_prob0.2": {"family": "Vikery-Patil"}, + "GM_prob0.2_cortex": {"family": "Vikery-Patil"}, +} + + +def register_mask( + name: str, + mask_path: Union[str, Path], + overwrite: bool = False, +) -> None: + """Register a custom user mask. + + Parameters + ---------- + name : str + The name of the mask. + mask_path : str or pathlib.Path + The path to the mask file. + overwrite : bool, optional + If True, overwrite an existing mask with the same name. + Does not apply to built-in mask (default False). + + Raises + ------ + ValueError + If the mask name is already registered and overwrite is set to + False or if the mask name is a built-in mask. + """ + # Check for attempt of overwriting built-in parcellations + if name in _available_masks: + if overwrite is True: + logger.info(f"Overwriting {name} mask") + if (_available_masks[name]["family"] != "CustomUserMask"): + raise_error( + f"Cannot overwrite {name} mask. " + "It is a built-in mask." + ) + else: + raise_error( + f"Mask {name} already registered. Set `overwrite=True`" + "to update its value." + ) + # Convert str to Path + if not isinstance(mask_path, Path): + mask_path = Path(mask_path) + # Add user parcellation info + _available_masks[name] = { + "path": str(mask_path.absolute()), + "family": "CustomUserMask", + } + + +def list_masks() -> List[str]: + """List all the available masks. + + Returns + ------- + list of str + A list with all available masks names. + """ + return sorted(_available_masks.keys()) + + +def load_mask( + name: str, + resolution: Optional[float] = None, + path_only: bool = False, +) -> Tuple[Optional["Nifti1Image"], Path]: + """Load mask. + + Parameters + ---------- + name : str + The name of the mask. + resolution : float, optional + The desired resolution of the mask to load. If it is not + available, the closest resolution will be loaded. Preferably, use a + resolution higher than the desired one. By default, will load the + highest one (default None). + path_only : bool, optional + If True, the mask image will not be loaded (default False). + + Returns + ------- + Nifti1Image or None + Loaded mask image. + pathlib.Path + File path to the mask image. + """ + if name not in _available_masks: + raise_error( + f"Mask {name} not found. " + f"Valid options are: {list_masks()}" + ) + + mask_definition = _available_masks[name].copy() + t_family = mask_definition.pop("family") + + if t_family == "CustomUserMask": + mask_fname = Path(mask_definition["path"]) + elif t_family == 'Vikery-Patil': + mask_fname = _load_vickery_patil_mask(name, resolution) + else: + raise_error( + f"I don't know about the {t_family} mask family." + ) + + logger.info(f"Loading mask {mask_fname.absolute()}") + + mask_img = None + if path_only is False: + mask_img = nib.load(mask_fname) + + return mask_img, mask_fname + + +def _load_vickery_patil_mask( + name: str, + resolution: Optional[float] = None, +) -> Path: + """Load Vikery-Patil mask. + + Parameters + ---------- + name : str + The name of the mask. + resolution : float, optional + The desired resolution of the mask to load. If it is not + available, the closest resolution will be loaded. Preferably, use a + resolution higher than the desired one. By default, will load the + highest one (default None). + + Returns + ------- + pathlib.Path + File path to the mask image. + """ + if name == "GM_prob0.2": + available_resolutions = [1.5, 3.0] + to_load = closest_resolution(resolution, available_resolutions) + if to_load == 3.0: + mask_fname = \ + "CAT12_IXI555_MNI152_TMP_GS_GMprob0.2_clean_3mm.nii.gz" + elif to_load == 1.5: + mask_fname = "CAT12_IXI555_MNI152_TMP_GS_GMprob0.2_clean.nii.gz" + else: + raise_error( + f"Cannot find a GM_prob0.2 mask for resolution {resolution}" + ) + elif name == "GM_prob0.2_cortex": + mask_fname = "GMprob0.2_cortex_3mm_NA_rm.nii.gz" + else: + raise_error( + f"Cannot find a Vikery-Patil mask called {name}" + ) + mask_fname = _masks_path / "vickery-patil" / mask_fname + + return mask_fname diff --git a/junifer/data/masks/vickery-patil/CAT12_IXI555_MNI152_TMP_GS_GMprob0.2_clean.nii.gz b/junifer/data/masks/vickery-patil/CAT12_IXI555_MNI152_TMP_GS_GMprob0.2_clean.nii.gz new file mode 100644 index 0000000000000000000000000000000000000000..329197283a80fb887bbd081e1efd4373d571c7e6 GIT binary patch literal 88270 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 literal 0 HcmV?d00001 diff --git a/junifer/data/masks/vickery-patil/CAT12_IXI555_MNI152_TMP_GS_GMprob0.2_clean_3mm.nii.gz b/junifer/data/masks/vickery-patil/CAT12_IXI555_MNI152_TMP_GS_GMprob0.2_clean_3mm.nii.gz new file mode 100644 index 0000000000000000000000000000000000000000..597c1d44f17ee2621e15918b8fde9e24bade0d22 GIT binary patch literal 12862 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%0D-K&trNWtlc%^+O{W#VdGjYim2>32$Q` zMwV`KG%408n>R+{6JrFgK~$=Id5;|UI#owtgXDXdlF?&G2dBXX81&E^iD1oRuu*3Q z`Mr0p{zI~T8S@C74?J^JZ;{xZtuEqA6{-4KQ6E?0E3C_2FwNtQ`EqiLLdM3ZDQ%JP zoZLWyn~it!D&%hhjEOzdp*1%8z)5pa3Z+?8(bJ9GSR}ix8nt~5jsu&b8h8D{s|x)) zSQ~<7RW#Vj$8=F{xwd-6?`8rCJx zP3zQi(@pVsxsh-kIYAilsTs#X_?gBx7=)8XG6Fpk#(YheHR0R;b1&~z0jFDobv)-m z9(Bq&8p2|a>6&!lWSrx7GX2EnH3>bg#@fjn3ky#(ijvD!^ATP@Xsi#fjJW9Xf9x&P zs#ubLS9T1esxEfYgQUo#7hph5ksxLb2585jr9o}zJ6tcCV3b!Y1A!Pijy^RD=SbLc zMBAYEWe7Ti?D*YJ*YgHb<4_il&~`-f=0$-8zz85+zZnb1BquW{xp#uG7B7;ec<|6C z7BveVrn%j${$_^LUMh|Lk9hnVnRz5%Q#}3gfqwoZQR=FRg++(kKNjmmShf1;>_klE zT8NF$8>2FI@TWKqoeoiag&CPsix`lt$Cl6>O7XtH3KJ1NcN*kJrxVWfnM zI`e4pQAdLwB&(lT%_4NP4*Q6Kd1J_0)`DnxJKDZ^0o61@tNc+IGU-oEx=E0fqI&td z6p#1b))T@r9_jv+V}&BcHvN-4?*VsoYvjCq`Yx4tAfM`{;hXp!d~isg<^BJ1-bg~D zhdP|ZZjQM4K#f|624$Hf3d+@w>DV0>?Zir#>BPN7x3@rdiYy-S=*bdiUoElJ-WZM*KTw$St5-MEqW~mc3yU;F8#k>{jSu*+7^VR zCcNRty?J0)1Nx;PkF~q@{Ljo{e$*R`))H3--Tjo{Jvs2k>4~qe#b`8(-z_?1AXCz- z-1QTt1Z5c~0~D%R{vYf9Y1!k;m=Dn4nzb25(wgRPb`S9!&a7$@(#j6^XiDFbHiB!X z)G-YvKJu4GgdC!6iPHE_EdDD>L#Ags3eX${{O_dy|G}GGwL**n`F!wg^p@ZJADjX> ARR910 literal 0 HcmV?d00001 diff --git a/junifer/data/masks/vickery-patil/GMprob0.2_cortex_3mm_NA_rm.nii.gz b/junifer/data/masks/vickery-patil/GMprob0.2_cortex_3mm_NA_rm.nii.gz new file mode 100644 index 0000000000000000000000000000000000000000..cd749ffbcb1571c1dd94cfe2ad87d6930a8b4342 GIT binary patch literal 8700 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 Union[float, int]: - """Find the closest resolution. - - Parameters - ---------- - resolution : float - The given resolution. - valid_resolution : list of float or np.ndarray - The array of valid resolutions. - - Returns - ------- - float - The closest valid resolution. - """ - # Convert list of int to numpy.ndarray - if not isinstance(valid_resolution, np.ndarray): - valid_resolution = np.array(valid_resolution) - - if resolution is None: - logger.info("Resolution set to None, using highest resolution.") - closest = np.min(valid_resolution) - elif any(x <= resolution for x in valid_resolution): - # Case 1: get the highest closest resolution - closest = np.max(valid_resolution[valid_resolution <= resolution]) - else: - # Case 2: get the lower closest resolution - closest = np.min(valid_resolution) - - return closest - - def _retrieve_schaefer( parcellations_dir: Path, resolution: Optional[float] = None, @@ -406,7 +372,7 @@ def _retrieve_schaefer( f"of the following: {_valid_networks}" ) - resolution = _closest_resolution(resolution, _valid_resolutions) + resolution = closest_resolution(resolution, _valid_resolutions) # define file names parcellation_fname = ( @@ -532,7 +498,7 @@ def _retrieve_tian( f"one of the following: 3T or 7T" ) - resolution = _closest_resolution(resolution, _valid_resolutions) + resolution = closest_resolution(resolution, _valid_resolutions) # define file names if magneticfield == "3T": @@ -667,7 +633,7 @@ def _retrieve_suit( # TODO: Validate this with Vera _valid_resolutions = [1] - resolution = _closest_resolution(resolution, _valid_resolutions) + resolution = closest_resolution(resolution, _valid_resolutions) # define file names parcellation_fname = ( diff --git a/junifer/data/tests/test_data_utils.py b/junifer/data/tests/test_data_utils.py new file mode 100644 index 000000000..e80d06baa --- /dev/null +++ b/junifer/data/tests/test_data_utils.py @@ -0,0 +1,33 @@ +"""Provide tests for data utils.""" + +# Authors: Federico Raimondo +# License: AGPL + +from typing import List + +import pytest +import numpy as np + +from junifer.data.utils import closest_resolution + + +@pytest.mark.parametrize( + "resolution, valid_resolutions, expected", + [ + (1.0, [1.0, 2.0, 3.0], 1.0), + (1.1, [1.0, 2.0, 3.0], 1.0), + (0.9, [1.0, 2.0, 3.0], 1.0), + (2.1, [1.0, 2.0, 3.0], 2.0), + (2.0, [1.0, 2.0, 3.0], 2.0), + (4.0, [1.0, 2.0, 3.0], 3.0), + (None, [1.0, 2.0, 3.0], 1.0), + ], +) +def test_closest_resolution( + resolution: float, valid_resolutions: List[float], expected: float +): + """Test closest_resolution.""" + assert closest_resolution(resolution, valid_resolutions) == expected + assert ( + closest_resolution(resolution, np.array(valid_resolutions)) == expected + ) diff --git a/junifer/data/tests/test_masks.py b/junifer/data/tests/test_masks.py new file mode 100644 index 000000000..b9dfe7239 --- /dev/null +++ b/junifer/data/tests/test_masks.py @@ -0,0 +1,152 @@ +"""Provide tests for masks.""" + +# Authors: Federico Raimondo +# Vera Komeyer +# Synchon Mandal +# License: AGPL + +from pathlib import Path +from typing import List + +import pytest +from numpy.testing import assert_array_almost_equal, assert_array_equal + +from junifer.data.masks import load_mask, register_mask, list_masks + + +def test_register_mask_built_in_check() -> None: + """Test mask registration check for built-in masks.""" + with pytest.raises(ValueError, match=r"built-in mask"): + register_mask( + name="GM_prob0.2", + mask_path="testmask.nii.gz", + overwrite=True, + ) + + +def test_list_masks_incorrect() -> None: + """Test incorrect information check for list masks.""" + masks = list_masks() + assert "testmask" not in masks + + +def test_register_mask_already_registered() -> None: + """Test mask registration check for already registered.""" + # Register custom mask + register_mask( + name="testmask", + mask_path="testmask.nii.gz", + ) + assert ( + load_mask("testmask", path_only=True)[1].name + == "testmask.nii.gz" + ) + + # Try registering again + with pytest.raises(ValueError, match=r"already registered."): + register_mask( + name="testmask", + mask_path="testmask.nii.gz", + ) + register_mask( + name="testmask", + mask_path="testmask2.nii.gz", + overwrite=True, + ) + + assert ( + load_mask("testmask", path_only=True)[1].name + == "testmask2.nii.gz" + ) + + +@pytest.mark.parametrize( + "name, mask_path, overwrite", + [ + ("testmask_1", "testmask_1.nii.gz", True), + ("testmask_2", "testmask_2.nii.gz", True), + ("testmask_3", Path("testmask_3.nii.gz"), True), + ], +) +def test_register_mask( + name: str, + mask_path: str, + overwrite: bool, +) -> None: + """Test mask registration. + + Parameters + ---------- + name : str + The parametrized mask name. + mask_path : str or pathlib.Path + The parametrized mask path. + overwrite : bool + The parametrized mask overwrite value. + + """ + # Register custom mask + register_mask( + name=name, + mask_path=mask_path, + overwrite=overwrite, + ) + # List available mask and check registration + masks = list_masks() + assert name in masks + # Load registered mask + _, fname = load_mask(name=name, path_only=True) + # Check values for registered mask + assert fname.name == f"{name}.nii.gz" + + +@pytest.mark.parametrize( + "mask_name", + [ + "GM_prob0.2", + "GM_prob0.2_cortex", + ], +) +def test_list_masks_correct(mask_name: str) -> None: + """Test correct information check for list masks. + + Parameters + ---------- + mask_name : str + The parametrized mask name. + + """ + masks = list_masks() + assert mask_name in masks + + +def test_load_mask_incorrect() -> None: + """Test loading of invalid masks.""" + with pytest.raises(ValueError, match=r"not found"): + load_mask("wrongmask") + + + +def test_vickery_patil() -> None: + """Test Vickery-Patil mask.""" + mask, fname = load_mask("GM_prob0.2") + assert_array_almost_equal( + mask.header["pixdim"][1:4], [1.5, 1.5, 1.5] # type: ignore + ) + + assert fname.name == "CAT12_IXI555_MNI152_TMP_GS_GMprob0.2_clean.nii.gz" + + mask, fname = load_mask("GM_prob0.2", resolution=3) + assert_array_almost_equal( + mask.header["pixdim"][1:4], [3.0, 3.0, 3.0] # type: ignore + ) + + assert fname.name == \ + "CAT12_IXI555_MNI152_TMP_GS_GMprob0.2_clean_3mm.nii.gz" + + mask, fname = load_mask("GM_prob0.2_cortex") + assert_array_almost_equal( + mask.header["pixdim"][1:4], [3.0, 3.0, 3.0] # type: ignore + ) + + assert fname.name == "GMprob0.2_cortex_3mm_NA_rm.nii.gz" diff --git a/junifer/data/utils.py b/junifer/data/utils.py new file mode 100644 index 000000000..b1e703b38 --- /dev/null +++ b/junifer/data/utils.py @@ -0,0 +1,41 @@ +"""Provide utilities for data module.""" +from typing import Optional, Union, List + +import numpy as np + +from ..utils.logging import logger + + +def closest_resolution( + resolution: Optional[float], + valid_resolution: Union[List[float], List[int], np.ndarray], +) -> Union[float, int]: + """Find the closest resolution. + + Parameters + ---------- + resolution : float + The given resolution. + valid_resolution : list of float or np.ndarray + The array of valid resolutions. + + Returns + ------- + float + The closest valid resolution. + """ + # Convert list of int to numpy.ndarray + if not isinstance(valid_resolution, np.ndarray): + valid_resolution = np.array(valid_resolution) + + if resolution is None: + logger.info("Resolution set to None, using highest resolution.") + closest = np.min(valid_resolution) + elif any(x <= resolution for x in valid_resolution): + # Case 1: get the highest closest resolution + closest = np.max(valid_resolution[valid_resolution <= resolution]) + else: + # Case 2: get the lower closest resolution + closest = np.min(valid_resolution) + + return closest -- 2.52.0 From 7860afecd7f8527465adb77084dfed97dc863ca4 Mon Sep 17 00:00:00 2001 From: Fede Raimondo Date: Tue, 22 Nov 2022 17:58:40 +0100 Subject: [PATCH 2/8] cleanup --- junifer/data/tests/test_masks.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/junifer/data/tests/test_masks.py b/junifer/data/tests/test_masks.py index b9dfe7239..21bd4390a 100644 --- a/junifer/data/tests/test_masks.py +++ b/junifer/data/tests/test_masks.py @@ -6,10 +6,9 @@ # License: AGPL from pathlib import Path -from typing import List import pytest -from numpy.testing import assert_array_almost_equal, assert_array_equal +from numpy.testing import assert_array_almost_equal from junifer.data.masks import load_mask, register_mask, list_masks @@ -126,7 +125,6 @@ def test_load_mask_incorrect() -> None: load_mask("wrongmask") - def test_vickery_patil() -> None: """Test Vickery-Patil mask.""" mask, fname = load_mask("GM_prob0.2") -- 2.52.0 From 898d6c382ecb0015b0a5d152324def7691ae358e Mon Sep 17 00:00:00 2001 From: Fede Raimondo Date: Wed, 23 Nov 2022 10:25:34 +0100 Subject: [PATCH 3/8] Add masks --- docs/builtin.rst | 37 ++++++++++++++ junifer/data/__init__.py | 5 ++ junifer/data/masks.py | 10 ++-- junifer/data/tests/test_masks.py | 25 ++++++---- ...ossparcellation_functional_connectivity.py | 7 +++ junifer/markers/ets_rss.py | 20 ++++++-- .../functional_connectivity_parcels.py | 15 +++--- .../functional_connectivity_spheres.py | 16 +++--- junifer/markers/parcel_aggregation.py | 28 ++++++++++- junifer/markers/sphere_aggregation.py | 16 ++++-- junifer/markers/tests/test_ets_rss.py | 3 +- .../markers/tests/test_parcel_aggregation.py | 50 +++++++++++++++++++ .../markers/tests/test_sphere_aggregation.py | 50 +++++++++++++++++-- 13 files changed, 240 insertions(+), 42 deletions(-) diff --git a/docs/builtin.rst b/docs/builtin.rst index b0a79244f..70b95e5f3 100644 --- a/docs/builtin.rst +++ b/docs/builtin.rst @@ -321,5 +321,42 @@ Available Planned ~~~~~~~ + +Masks +----- + +.. + Provide a list of the masks that are implemented or planned. + + Version added: The Junifer version in which the mask was added. + +Available +~~~~~~~~~ + +.. list-table:: + :widths: auto + :header-rows: 1 + + * - Name + - Keys + - Version added + - Publication + * - Vickery-Patil (Gray Matter) + - | ``GM_prob0.2`` + - 0.0.1 + - | Vickery, Sam, & Patil, Kaustubh. (2022). + | Chimpanzee and Human Gray Matter Masks [Data set]. Zenodo. + | https://doi.org/10.5281/zenodo.6463123 + * - Vickery-Patil (Cortex + Basal Ganglia) + - | ``GM_prob0.2_cortex`` + - 0.0.1 + - | Vickery, Sam, & Patil, Kaustubh. (2022). + | Chimpanzee and Human Gray Matter Masks [Data set]. Zenodo. + | https://doi.org/10.5281/zenodo.6463123 + +Planned +~~~~~~~ + + .. helpful site for creating tables: https://rest-sphinx-memo.readthedocs.io/en/latest/ReST.html#tables diff --git a/junifer/data/__init__.py b/junifer/data/__init__.py index 2429a0c62..f1508945b 100644 --- a/junifer/data/__init__.py +++ b/junifer/data/__init__.py @@ -15,5 +15,10 @@ from .parcellations import ( register_parcellation, ) +from .masks import ( + list_masks, + load_mask, + register_mask, +) from . import utils \ No newline at end of file diff --git a/junifer/data/masks.py b/junifer/data/masks.py index 4c84770a4..639b4ebf3 100644 --- a/junifer/data/masks.py +++ b/junifer/data/masks.py @@ -26,8 +26,8 @@ The built-in masks are files that are shipped with the package in the data/masks directory. The user can also register their own masks. """ _available_masks: Dict[str, Dict[Any, Any]] = { - "GM_prob0.2": {"family": "Vikery-Patil"}, - "GM_prob0.2_cortex": {"family": "Vikery-Patil"}, + "GM_prob0.2": {"family": "Vickery-Patil"}, + "GM_prob0.2_cortex": {"family": "Vickery-Patil"}, } @@ -126,7 +126,7 @@ def load_mask( if t_family == "CustomUserMask": mask_fname = Path(mask_definition["path"]) - elif t_family == 'Vikery-Patil': + elif t_family == 'Vickery-Patil': mask_fname = _load_vickery_patil_mask(name, resolution) else: raise_error( @@ -146,7 +146,7 @@ def _load_vickery_patil_mask( name: str, resolution: Optional[float] = None, ) -> Path: - """Load Vikery-Patil mask. + """Load Vickery-Patil mask. Parameters ---------- @@ -179,7 +179,7 @@ def _load_vickery_patil_mask( mask_fname = "GMprob0.2_cortex_3mm_NA_rm.nii.gz" else: raise_error( - f"Cannot find a Vikery-Patil mask called {name}" + f"Cannot find a Vickery-Patil mask called {name}" ) mask_fname = _masks_path / "vickery-patil" / mask_fname diff --git a/junifer/data/tests/test_masks.py b/junifer/data/tests/test_masks.py index 21bd4390a..13926774f 100644 --- a/junifer/data/tests/test_masks.py +++ b/junifer/data/tests/test_masks.py @@ -10,7 +10,12 @@ from pathlib import Path import pytest from numpy.testing import assert_array_almost_equal -from junifer.data.masks import load_mask, register_mask, list_masks +from junifer.data.masks import ( + load_mask, + register_mask, + list_masks, + _load_vickery_patil_mask, +) def test_register_mask_built_in_check() -> None: @@ -36,10 +41,7 @@ def test_register_mask_already_registered() -> None: name="testmask", mask_path="testmask.nii.gz", ) - assert ( - load_mask("testmask", path_only=True)[1].name - == "testmask.nii.gz" - ) + assert load_mask("testmask", path_only=True)[1].name == "testmask.nii.gz" # Try registering again with pytest.raises(ValueError, match=r"already registered."): @@ -53,10 +55,7 @@ def test_register_mask_already_registered() -> None: overwrite=True, ) - assert ( - load_mask("testmask", path_only=True)[1].name - == "testmask2.nii.gz" - ) + assert load_mask("testmask", path_only=True)[1].name == "testmask2.nii.gz" @pytest.mark.parametrize( @@ -139,8 +138,9 @@ def test_vickery_patil() -> None: mask.header["pixdim"][1:4], [3.0, 3.0, 3.0] # type: ignore ) - assert fname.name == \ - "CAT12_IXI555_MNI152_TMP_GS_GMprob0.2_clean_3mm.nii.gz" + assert ( + fname.name == "CAT12_IXI555_MNI152_TMP_GS_GMprob0.2_clean_3mm.nii.gz" + ) mask, fname = load_mask("GM_prob0.2_cortex") assert_array_almost_equal( @@ -148,3 +148,6 @@ def test_vickery_patil() -> None: ) assert fname.name == "GMprob0.2_cortex_3mm_NA_rm.nii.gz" + + with pytest.raises(ValueError, match=r"find a Vickery-Patil mask "): + _load_vickery_patil_mask("wrong", resolution=2) diff --git a/junifer/markers/crossparcellation_functional_connectivity.py b/junifer/markers/crossparcellation_functional_connectivity.py index 36b2f133e..f24755036 100644 --- a/junifer/markers/crossparcellation_functional_connectivity.py +++ b/junifer/markers/crossparcellation_functional_connectivity.py @@ -32,6 +32,9 @@ class CrossParcellationFC(BaseMarker): correlation_method : str, optional Any method that can be passed to :any:`pandas.DataFrame.corr` (default "pearson"). + 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`. name : str, optional The name of the marker. If None, will use the class name (default None). @@ -43,6 +46,7 @@ class CrossParcellationFC(BaseMarker): parcellation_two: str, aggregation_method: str = "mean", correlation_method: str = "pearson", + mask: Optional[str] = None, name: Optional[str] = None, ) -> None: if parcellation_one == parcellation_two: @@ -53,6 +57,7 @@ class CrossParcellationFC(BaseMarker): self.parcellation_two = parcellation_two self.aggregation_method = aggregation_method self.correlation_method = correlation_method + self.mask = mask super().__init__(on=["BOLD"], name=name) def get_valid_inputs(self) -> List[str]: @@ -145,10 +150,12 @@ class CrossParcellationFC(BaseMarker): parcellation_one_dict = ParcelAggregation( parcellation=self.parcellation_one, method=self.aggregation_method, + mask=self.mask, ).compute(input) parcellation_two_dict = ParcelAggregation( parcellation=self.parcellation_two, method=self.aggregation_method, + mask=self.mask, ).compute(input) parcellated_ts_one = parcellation_one_dict["data"] diff --git a/junifer/markers/ets_rss.py b/junifer/markers/ets_rss.py index 532bbb0e1..0d43b190c 100644 --- a/junifer/markers/ets_rss.py +++ b/junifer/markers/ets_rss.py @@ -29,9 +29,15 @@ class RSSETSMarker(BaseMarker): parcellation : str The name of the parcellation. Check valid options by calling :func:`junifer.data.parcellations.list_parcellations`. - aggregation_method : str, optional + 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`. name : str, optional The name of the marker. If None, will use the class name (default None). @@ -41,11 +47,15 @@ class RSSETSMarker(BaseMarker): def __init__( self, parcellation: str, - aggregation_method: str = "mean", + agg_method: str = "mean", + agg_method_params: Optional[Dict] = None, + mask: Optional[str] = None, name: Optional[str] = None, ) -> None: self.parcellation = parcellation - self.aggregation_method = aggregation_method + self.agg_method = agg_method + self.agg_method_params = agg_method_params + self.mask = mask super().__init__(name=name) def get_valid_inputs(self) -> List[str]: @@ -136,7 +146,9 @@ class RSSETSMarker(BaseMarker): # Initialize a ParcelAggregation parcel_aggregation = ParcelAggregation( parcellation=self.parcellation, - method=self.aggregation_method, + method=self.agg_method, + method_params=self.agg_method_params, + mask=self.mask ) # Compute the parcel aggregation out = parcel_aggregation.compute(input=input, extra_input=extra_input) diff --git a/junifer/markers/functional_connectivity_parcels.py b/junifer/markers/functional_connectivity_parcels.py index 5c354c76d..75e35f3a5 100644 --- a/junifer/markers/functional_connectivity_parcels.py +++ b/junifer/markers/functional_connectivity_parcels.py @@ -40,6 +40,9 @@ class FunctionalConnectivityParcels(BaseMarker): cor_method_params : dict, optional Parameters to pass to the correlation function. Check valid options in :class:`nilearn.connectome.ConnectivityMeasure` (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`. name : str, optional The name of the marker. If None, will use the class name (default None). @@ -52,21 +55,20 @@ class FunctionalConnectivityParcels(BaseMarker): agg_method_params: Optional[Dict] = None, cor_method: str = "covariance", cor_method_params: Optional[Dict] = None, + mask: Optional[str] = None, name: Optional[str] = None, ) -> None: self.parcellation = parcellation self.agg_method = agg_method - self.agg_method_params = ( - {} if agg_method_params is None else agg_method_params - ) + self.agg_method_params = agg_method_params self.cor_method = cor_method - self.cor_method_params = ( - {} if cor_method_params is None else cor_method_params - ) + 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.mask = mask super().__init__(name=name) @@ -131,6 +133,7 @@ class FunctionalConnectivityParcels(BaseMarker): parcellation=self.parcellation, method=self.agg_method, method_params=self.agg_method_params, + mask=self.mask, on="BOLD", ) # get the 2D timeseries after parcel aggregation diff --git a/junifer/markers/functional_connectivity_spheres.py b/junifer/markers/functional_connectivity_spheres.py index ba3c6e3e4..0dc7632d2 100644 --- a/junifer/markers/functional_connectivity_spheres.py +++ b/junifer/markers/functional_connectivity_spheres.py @@ -43,6 +43,9 @@ class FunctionalConnectivitySpheres(BaseMarker): cor_method_params : dict, optional Parameters to pass to the correlation function. Check valid options in :class:`nilearn.connectome.ConnectivityMeasure` (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`. name : str, optional The name of the marker. By default, it will use KIND_FunctionalConnectivitySpheres where KIND is the kind of data it @@ -58,6 +61,7 @@ class FunctionalConnectivitySpheres(BaseMarker): agg_method_params: Optional[Dict] = None, cor_method: str = "covariance", cor_method_params: Optional[Dict] = None, + mask: Optional[str] = None, name: Optional[str] = None, ) -> None: self.coords = coords @@ -65,18 +69,17 @@ class FunctionalConnectivitySpheres(BaseMarker): if radius is None or radius <= 0: raise_error(f"radius should be > 0: provided {radius}") self.agg_method = agg_method - self.agg_method_params = ( - {} if agg_method_params is None else agg_method_params - ) + self.agg_method_params = agg_method_params self.cor_method = cor_method - self.cor_method_params = ( - {} if cor_method_params is None else cor_method_params - ) + 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.mask = mask + super().__init__(name=name) def get_valid_inputs(self) -> List[str]: @@ -142,6 +145,7 @@ class FunctionalConnectivitySpheres(BaseMarker): radius=self.radius, method=self.agg_method, method_params=self.agg_method_params, + mask=self.mask, on="BOLD", ) diff --git a/junifer/markers/parcel_aggregation.py b/junifer/markers/parcel_aggregation.py index deb98e028..ecea76cee 100644 --- a/junifer/markers/parcel_aggregation.py +++ b/junifer/markers/parcel_aggregation.py @@ -11,7 +11,7 @@ from nilearn.image import math_img, resample_to_img from nilearn.maskers import NiftiMasker from ..api.decorators import register_marker -from ..data import load_parcellation +from ..data import load_parcellation, load_mask from ..stats import get_aggfunc_by_name from ..utils import logger from .base import BaseMarker @@ -35,6 +35,9 @@ class ParcelAggregation(BaseMarker): method_params : dict, optional Parameters to pass to the aggregation function. Check valid options in :func:`junifer.stats.get_aggfunc_by_name`. + 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`. on : {"T1w", "BOLD", "VBM_GM", "VBM_WM", "fALFF", "GCOR", "LCOR"} \ or list of the options, optional The data types to apply the marker to. If None, will work on all @@ -49,12 +52,14 @@ class ParcelAggregation(BaseMarker): parcellation: str, method: str, method_params: Optional[Dict[str, Any]] = None, + mask: Optional[str] = None, on: Union[List[str], str, None] = None, name: Optional[str] = None, ) -> None: self.parcellation = parcellation self.method = method - self.method_params = {} if method_params is None else method_params + self.method_params = method_params or {} + self.mask = mask super().__init__(on=on, name=name) def get_valid_inputs(self) -> List[str]: @@ -158,15 +163,34 @@ class ParcelAggregation(BaseMarker): name=self.parcellation, resolution=resolution, ) + parcellation_img_res = resample_to_img( t_parcellation, t_input, interpolation="nearest", + copy=True, ) + parcellation_bin = math_img( "img != 0", img=parcellation_img_res, ) + + if self.mask is not None: + logger.debug(f"Masking with {self.mask}") + mask_img, _ = load_mask(name=self.mask, resolution=resolution) + mask_img = resample_to_img( + mask_img, + t_input, + interpolation="nearest", + copy=True, + ) + parcellation_bin = math_img( + "np.logical_and(img, mask)", + img=parcellation_bin, + mask=mask_img, + ) + logger.debug("Masking") masker = NiftiMasker( parcellation_bin, target_affine=t_input.affine diff --git a/junifer/markers/sphere_aggregation.py b/junifer/markers/sphere_aggregation.py index ba3cff9ef..f066953b7 100644 --- a/junifer/markers/sphere_aggregation.py +++ b/junifer/markers/sphere_aggregation.py @@ -7,7 +7,7 @@ from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union from ..api.decorators import register_marker -from ..data import load_coordinates +from ..data import load_coordinates, load_mask from ..external.nilearn import JuniferNiftiSpheresMasker from ..stats import get_aggfunc_by_name from ..utils import logger @@ -37,6 +37,9 @@ class SphereAggregation(BaseMarker): (default "mean"). 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`. on : {"T1w", "BOLD", "VBM_GM", "VBM_WM", "fALFF", "GCOR", "LCOR"} or \ list of the options, optional The data types to apply the marker to. If None, will work on all @@ -53,13 +56,15 @@ class SphereAggregation(BaseMarker): radius: Optional[float] = None, method: str = "mean", method_params: Optional[Dict[str, Any]] = None, + mask: Optional[str] = None, on: Union[List[str], str, None] = None, name: Optional[str] = None, ) -> None: self.coords = coords self.radius = radius self.method = method - self.method_params = {} if method_params is None else method_params + self.method_params = method_params or {} + self.mask = mask super().__init__(on=on, name=name) def get_valid_inputs(self) -> List[str]: @@ -157,12 +162,17 @@ class SphereAggregation(BaseMarker): agg_func = get_aggfunc_by_name( self.method, func_params=self.method_params ) + # Load mask + mask_img = None + if self.mask is not None: + logger.debug(f"Masking with {self.mask}") + mask_img, _ = load_mask(self.mask) # Get seeds and labels coords, out_labels = load_coordinates(name=self.coords) masker = JuniferNiftiSpheresMasker( seeds=coords, radius=self.radius, - mask_img=None, # TODO: support this (needs #79) + mask_img=mask_img, agg_func=agg_func, ) # Fit and transform the marker on the data diff --git a/junifer/markers/tests/test_ets_rss.py b/junifer/markers/tests/test_ets_rss.py index ed6fd484e..d977b226b 100644 --- a/junifer/markers/tests/test_ets_rss.py +++ b/junifer/markers/tests/test_ets_rss.py @@ -45,7 +45,8 @@ def test_compute() -> None: # Assert the meta meta = ets_rss_marker.get_meta("BOLD")["marker"] assert meta["parcellation"] == "Schaefer100x17" - assert meta["aggregation_method"] == "mean" + assert meta["agg_method"] == "mean" + assert meta["agg_method_params"] is None assert meta["class"] == "RSSETSMarker" diff --git a/junifer/markers/tests/test_parcel_aggregation.py b/junifer/markers/tests/test_parcel_aggregation.py index 0bbc95109..d8abd0d58 100644 --- a/junifer/markers/tests/test_parcel_aggregation.py +++ b/junifer/markers/tests/test_parcel_aggregation.py @@ -12,6 +12,7 @@ from nilearn.maskers import NiftiLabelsMasker, NiftiMasker from numpy.testing import assert_array_almost_equal, assert_array_equal from scipy.stats import trim_mean +from junifer.data import load_mask from junifer.markers.parcel_aggregation import ParcelAggregation @@ -86,6 +87,7 @@ def test_ParcelAggregation_3D() -> None: meta = marker.get_meta("VBM_GM")["marker"] assert meta["method"] == "mean" assert meta["parcellation"] == "Schaefer100x7" + assert meta["mask"] is None assert meta["name"] == "VBM_GM_gmd_schaefer100x7_mean" assert meta["class"] == "ParcelAggregation" assert meta["kind"] == "VBM_GM" @@ -110,6 +112,7 @@ def test_ParcelAggregation_3D() -> None: meta = marker.get_meta("VBM_GM")["marker"] assert meta["method"] == "std" assert meta["parcellation"] == "Schaefer100x7" + assert meta["mask"] is None assert meta["name"] == "VBM_GM_ParcelAggregation" assert meta["class"] == "ParcelAggregation" assert meta["kind"] == "VBM_GM" @@ -142,6 +145,7 @@ def test_ParcelAggregation_3D() -> None: meta = marker.get_meta("VBM_GM")["marker"] assert meta["method"] == "trim_mean" assert meta["parcellation"] == "Schaefer100x7" + assert meta["mask"] is None assert meta["name"] == "VBM_GM_ParcelAggregation" assert meta["class"] == "ParcelAggregation" assert meta["kind"] == "VBM_GM" @@ -175,7 +179,53 @@ def test_ParcelAggregation_4D(): meta = marker.get_meta("BOLD")["marker"] assert meta["method"] == "mean" assert meta["parcellation"] == "Schaefer100x7" + assert meta["mask"] is None assert meta["name"] == "BOLD_ParcelAggregation" assert meta["class"] == "ParcelAggregation" assert meta["kind"] == "BOLD" assert meta["method_params"] == {} + + +def test_ParcelAggregation_3D_mask() -> None: + """Test ParcelAggregation object on 3D images with mask.""" + + # Get the testing parcellation (for nilearn) + parcellation = datasets.fetch_atlas_schaefer_2018(n_rois=100) + + # Get one mask + mask_img, _ = load_mask("GM_prob0.2") + + # Get the oasis VBM data + oasis_dataset = datasets.fetch_oasis_vbm(n_subjects=1) + vbm = oasis_dataset.gray_matter_maps[0] + img = nib.load(vbm) + + # Create NiftiLabelsMasker + nifti_masker = NiftiLabelsMasker( + labels_img=parcellation.maps, + mask_img=mask_img) + auto = nifti_masker.fit_transform(img) + + # Use the ParcelAggregation object + marker = ParcelAggregation( + parcellation="Schaefer100x7", + method="mean", + mask="GM_prob0.2", + name="gmd_schaefer100x7_mean", + on="VBM_GM", + ) # Test passing "on" as a keyword argument + input = dict(VBM_GM=dict(data=img)) + jun_values3d_mean = marker.fit_transform(input)["VBM_GM"]["data"] + + assert jun_values3d_mean.ndim == 2 + assert jun_values3d_mean.shape[0] == 1 + assert_array_almost_equal(auto, jun_values3d_mean) + + meta = marker.get_meta("VBM_GM")["marker"] + assert meta["method"] == "mean" + assert meta["parcellation"] == "Schaefer100x7" + assert meta["mask"] == "GM_prob0.2" + assert meta["name"] == "VBM_GM_gmd_schaefer100x7_mean" + assert meta["class"] == "ParcelAggregation" + assert meta["kind"] == "VBM_GM" + assert meta["method_params"] == {} diff --git a/junifer/markers/tests/test_sphere_aggregation.py b/junifer/markers/tests/test_sphere_aggregation.py index 08ea00066..fd8f538a9 100644 --- a/junifer/markers/tests/test_sphere_aggregation.py +++ b/junifer/markers/tests/test_sphere_aggregation.py @@ -12,7 +12,7 @@ from nilearn.image import concat_imgs from nilearn.maskers import NiftiSpheresMasker from numpy.testing import assert_array_equal -from junifer.data import load_coordinates +from junifer.data import load_coordinates, load_mask from junifer.markers.sphere_aggregation import SphereAggregation from junifer.storage import SQLiteFeatureStorage @@ -44,7 +44,7 @@ def test_SphereAggregation_3D() -> None: vbm = oasis_dataset.gray_matter_maps[0] img = nib.load(vbm) - # Create NiftiLabelsMasker + # Create NiftSpheresMasker nifti_masker = NiftiSpheresMasker(seeds=coordinates, radius=RADIUS) auto4d = nifti_masker.fit_transform(img) @@ -63,6 +63,7 @@ def test_SphereAggregation_3D() -> None: assert meta["method"] == "mean" assert meta["coords"] == COORDS assert meta["radius"] == RADIUS + assert meta["mask"] is None assert meta["name"] == "VBM_GM_SphereAggregation" assert meta["class"] == "SphereAggregation" assert meta["kind"] == "VBM_GM" @@ -72,13 +73,13 @@ def test_SphereAggregation_3D() -> None: def test_SphereAggregation_4D() -> None: """Test SphereAggregation object on 4D images.""" # Get the testing coordinates (for nilearn) - coordinates, labels = load_coordinates(COORDS) + coordinates, _ = load_coordinates(COORDS) # Get the SPM auditory data subject_data = datasets.fetch_spm_auditory() fmri_img = concat_imgs(subject_data.func) # type: ignore - # Create NiftiLabelsMasker + # Create NiftSpheresMasker nifti_masker = NiftiSpheresMasker(seeds=coordinates, radius=RADIUS) auto4d = nifti_masker.fit_transform(fmri_img) @@ -97,6 +98,7 @@ def test_SphereAggregation_4D() -> None: assert meta["method"] == "mean" assert meta["coords"] == COORDS assert meta["radius"] == RADIUS + assert meta["mask"] is None assert meta["name"] == "BOLD_SphereAggregation" assert meta["class"] == "SphereAggregation" assert meta["kind"] == "BOLD" @@ -145,3 +147,43 @@ def test_SphereAggregation_storage(tmp_path: Path) -> None: ) marker.fit_transform(input, storage=storage) + + +def test_SphereAggregation_3D_mask() -> None: + """Test SphereAggregation object on 3D images using mask.""" + # Get the testing coordinates (for nilearn) + coordinates, _ = load_coordinates(COORDS) + + # Get one mask + mask_img, _ = load_mask("GM_prob0.2") + + # Get the oasis VBM data + oasis_dataset = datasets.fetch_oasis_vbm(n_subjects=1) + vbm = oasis_dataset.gray_matter_maps[0] + img = nib.load(vbm) + + # Create NiftSpheresMasker + nifti_masker = NiftiSpheresMasker( + seeds=coordinates, radius=RADIUS, mask_img=mask_img) + auto4d = nifti_masker.fit_transform(img) + + # Create SphereAggregation object + marker = SphereAggregation( + coords=COORDS, method="mean", radius=RADIUS, on="VBM_GM", + mask="GM_prob0.2" + ) + input = {"VBM_GM": {"data": img}} + jun_values4d = marker.fit_transform(input)["VBM_GM"]["data"] + + assert jun_values4d.ndim == 2 + assert_array_equal(auto4d.shape, jun_values4d.shape) + assert_array_equal(auto4d, jun_values4d) + + meta = marker.get_meta("VBM_GM")["marker"] + assert meta["method"] == "mean" + assert meta["coords"] == COORDS + assert meta["radius"] == RADIUS + assert meta["name"] == "VBM_GM_SphereAggregation" + assert meta["class"] == "SphereAggregation" + assert meta["kind"] == "VBM_GM" + assert meta["method_params"] == {} -- 2.52.0 From b2ad80a16b0b52a9d91b106c6e69723c13dbc1c7 Mon Sep 17 00:00:00 2001 From: Fede Raimondo Date: Wed, 23 Nov 2022 10:53:14 +0100 Subject: [PATCH 4/8] forgot this --- docs/api/data.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/api/data.rst b/docs/api/data.rst index 68f77b3b2..842ce47af 100644 --- a/docs/api/data.rst +++ b/docs/api/data.rst @@ -9,3 +9,10 @@ Coordinates .. automodule:: junifer.data.coordinates :members: + + +Masks +===== + +.. automodule:: junifer.data.masks + :members: -- 2.52.0 From be755e1d8a424310cfa6fc285b2a94e1a283160c Mon Sep 17 00:00:00 2001 From: Fede Raimondo Date: Wed, 23 Nov 2022 10:55:17 +0100 Subject: [PATCH 5/8] Change latest.inc --- docs/changes/latest.inc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/changes/latest.inc b/docs/changes/latest.inc index baa2ce7fb..c613a276a 100644 --- a/docs/changes/latest.inc +++ b/docs/changes/latest.inc @@ -87,6 +87,8 @@ Enhancements - Allow custom aggregation method for :class:`junifer.markers.SphereAggregation` (:gh:`102` by `Synchon Mandal`_). +- Add support for "masks" (:gh:`79` by `Fede Raimondo`_). + Bugs ~~~~ -- 2.52.0 From a6ecbbe76153c058ea4b028f7a5530999d9d1c5c Mon Sep 17 00:00:00 2001 From: Fede Date: Wed, 23 Nov 2022 15:37:34 +0100 Subject: [PATCH 6/8] address comments --- junifer/data/masks.py | 2 +- junifer/data/tests/test_data_utils.py | 13 ++++++++++++- .../crossparcellation_functional_connectivity.py | 3 ++- junifer/markers/ets_rss.py | 3 ++- junifer/markers/functional_connectivity_parcels.py | 3 ++- junifer/markers/functional_connectivity_spheres.py | 3 ++- junifer/markers/parcel_aggregation.py | 3 ++- junifer/markers/sphere_aggregation.py | 3 ++- 8 files changed, 25 insertions(+), 8 deletions(-) diff --git a/junifer/data/masks.py b/junifer/data/masks.py index 639b4ebf3..710ad0bde 100644 --- a/junifer/data/masks.py +++ b/junifer/data/masks.py @@ -25,7 +25,7 @@ data. The built-in masks are files that are shipped with the package in the data/masks directory. The user can also register their own masks. """ -_available_masks: Dict[str, Dict[Any, Any]] = { +_available_masks: Dict[str, Dict[str, Any]] = { "GM_prob0.2": {"family": "Vickery-Patil"}, "GM_prob0.2_cortex": {"family": "Vickery-Patil"}, } diff --git a/junifer/data/tests/test_data_utils.py b/junifer/data/tests/test_data_utils.py index e80d06baa..19e12ca57 100644 --- a/junifer/data/tests/test_data_utils.py +++ b/junifer/data/tests/test_data_utils.py @@ -26,7 +26,18 @@ from junifer.data.utils import closest_resolution def test_closest_resolution( resolution: float, valid_resolutions: List[float], expected: float ): - """Test closest_resolution.""" + """Test closest_resolution. + + Parameters + ---------- + + resolution: float + The resolution to test. + valid_resolutions: List[float] + The valid resolutions. + expected: float + The expected result. + """ assert closest_resolution(resolution, valid_resolutions) == expected assert ( closest_resolution(resolution, np.array(valid_resolutions)) == expected diff --git a/junifer/markers/crossparcellation_functional_connectivity.py b/junifer/markers/crossparcellation_functional_connectivity.py index f24755036..4e8ccbad7 100644 --- a/junifer/markers/crossparcellation_functional_connectivity.py +++ b/junifer/markers/crossparcellation_functional_connectivity.py @@ -34,7 +34,8 @@ class CrossParcellationFC(BaseMarker): :any:`pandas.DataFrame.corr` (default "pearson"). 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`. + 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). diff --git a/junifer/markers/ets_rss.py b/junifer/markers/ets_rss.py index 0d43b190c..50aff68bd 100644 --- a/junifer/markers/ets_rss.py +++ b/junifer/markers/ets_rss.py @@ -37,7 +37,8 @@ class RSSETSMarker(BaseMarker): :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`. + 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). diff --git a/junifer/markers/functional_connectivity_parcels.py b/junifer/markers/functional_connectivity_parcels.py index 75e35f3a5..3bb94216a 100644 --- a/junifer/markers/functional_connectivity_parcels.py +++ b/junifer/markers/functional_connectivity_parcels.py @@ -42,7 +42,8 @@ class FunctionalConnectivityParcels(BaseMarker): :class:`nilearn.connectome.ConnectivityMeasure` (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`. + 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). diff --git a/junifer/markers/functional_connectivity_spheres.py b/junifer/markers/functional_connectivity_spheres.py index 0dc7632d2..193510220 100644 --- a/junifer/markers/functional_connectivity_spheres.py +++ b/junifer/markers/functional_connectivity_spheres.py @@ -45,7 +45,8 @@ class FunctionalConnectivitySpheres(BaseMarker): :class:`nilearn.connectome.ConnectivityMeasure` (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`. + 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 diff --git a/junifer/markers/parcel_aggregation.py b/junifer/markers/parcel_aggregation.py index ecea76cee..f8b4ca8bc 100644 --- a/junifer/markers/parcel_aggregation.py +++ b/junifer/markers/parcel_aggregation.py @@ -37,7 +37,8 @@ class ParcelAggregation(BaseMarker): :func:`junifer.stats.get_aggfunc_by_name`. 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`. + Check valid options by calling :func:`junifer.data.masks.list_masks` + (default None). on : {"T1w", "BOLD", "VBM_GM", "VBM_WM", "fALFF", "GCOR", "LCOR"} \ or list of the options, optional The data types to apply the marker to. If None, will work on all diff --git a/junifer/markers/sphere_aggregation.py b/junifer/markers/sphere_aggregation.py index f066953b7..8fa648b4c 100644 --- a/junifer/markers/sphere_aggregation.py +++ b/junifer/markers/sphere_aggregation.py @@ -39,7 +39,8 @@ class SphereAggregation(BaseMarker): 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`. + Check valid options by calling :func:`junifer.data.masks.list_masks` + (default None). on : {"T1w", "BOLD", "VBM_GM", "VBM_WM", "fALFF", "GCOR", "LCOR"} or \ list of the options, optional The data types to apply the marker to. If None, will work on all -- 2.52.0 From 6771e3b98a2d20d7b91855fb858ca9a44d70f62c Mon Sep 17 00:00:00 2001 From: Fede Date: Wed, 23 Nov 2022 15:41:21 +0100 Subject: [PATCH 7/8] last comments --- junifer/data/tests/test_data_utils.py | 1 - junifer/data/utils.py | 9 +++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/junifer/data/tests/test_data_utils.py b/junifer/data/tests/test_data_utils.py index 19e12ca57..6a2306ee5 100644 --- a/junifer/data/tests/test_data_utils.py +++ b/junifer/data/tests/test_data_utils.py @@ -30,7 +30,6 @@ def test_closest_resolution( Parameters ---------- - resolution: float The resolution to test. valid_resolutions: List[float] diff --git a/junifer/data/utils.py b/junifer/data/utils.py index b1e703b38..56450e0dd 100644 --- a/junifer/data/utils.py +++ b/junifer/data/utils.py @@ -14,14 +14,15 @@ def closest_resolution( Parameters ---------- - resolution : float - The given resolution. - valid_resolution : list of float or np.ndarray + resolution : float, optional + The given resolution. If None, will return the highest resolution + (default None). + valid_resolution : list of float or int, or np.ndarray The array of valid resolutions. Returns ------- - float + float or int The closest valid resolution. """ # Convert list of int to numpy.ndarray -- 2.52.0 From 46a7761087855283917fcbc25e57ba7c7be203cc Mon Sep 17 00:00:00 2001 From: Fede Raimondo Date: Wed, 23 Nov 2022 16:37:30 +0100 Subject: [PATCH 8/8] last comment --- junifer/data/tests/test_data_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/junifer/data/tests/test_data_utils.py b/junifer/data/tests/test_data_utils.py index 6a2306ee5..80b2adf66 100644 --- a/junifer/data/tests/test_data_utils.py +++ b/junifer/data/tests/test_data_utils.py @@ -32,7 +32,7 @@ def test_closest_resolution( ---------- resolution: float The resolution to test. - valid_resolutions: List[float] + valid_resolutions: list of float The valid resolutions. expected: float The expected result. -- 2.52.0