From da5f7e89789553109df6536d256fb0441c23e629 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Wed, 27 Sep 2023 10:20:15 +0200 Subject: [PATCH 01/86] update: add space information for AOMIC datagrabbers --- junifer/datagrabber/aomic/id1000.py | 4 ++++ junifer/datagrabber/aomic/piop1.py | 4 ++++ junifer/datagrabber/aomic/piop2.py | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/junifer/datagrabber/aomic/id1000.py b/junifer/datagrabber/aomic/id1000.py index 4e0859ff6..bf497d525 100644 --- a/junifer/datagrabber/aomic/id1000.py +++ b/junifer/datagrabber/aomic/id1000.py @@ -121,6 +121,10 @@ class DataladAOMICID1000(PatternDataladDataGrabber): out = super().get_item(subject=subject) if out.get("BOLD"): out["BOLD"]["mask_item"] = "BOLD_mask" + # Add space information + out["BOLD"].update({"space": "MNI"}) if out.get("T1w"): out["T1w"]["mask_item"] = "T1w_mask" + # Add space information + out["T1w"].update({"space": "native"}) return out diff --git a/junifer/datagrabber/aomic/piop1.py b/junifer/datagrabber/aomic/piop1.py index 0a3c1ecf8..400e3d176 100644 --- a/junifer/datagrabber/aomic/piop1.py +++ b/junifer/datagrabber/aomic/piop1.py @@ -166,8 +166,12 @@ class DataladAOMICPIOP1(PatternDataladDataGrabber): out = super().get_item(subject=subject, task=new_task) if out.get("BOLD"): out["BOLD"]["mask_item"] = "BOLD_mask" + # Add space information + out["BOLD"].update({"space": "MNI"}) if out.get("T1w"): out["T1w"]["mask_item"] = "T1w_mask" + # Add space information + out["T1w"].update({"space": "native"}) return out def get_elements(self) -> List: diff --git a/junifer/datagrabber/aomic/piop2.py b/junifer/datagrabber/aomic/piop2.py index 934cd48fb..107f070da 100644 --- a/junifer/datagrabber/aomic/piop2.py +++ b/junifer/datagrabber/aomic/piop2.py @@ -166,6 +166,10 @@ class DataladAOMICPIOP2(PatternDataladDataGrabber): out = super().get_item(subject=subject, task=f"{task}_acq-seq") if out.get("BOLD"): out["BOLD"]["mask_item"] = "BOLD_mask" + # Add space information + out["BOLD"].update({"space": "MNI"}) if out.get("T1w"): out["T1w"]["mask_item"] = "T1w_mask" + # Add space information + out["T1w"].update({"space": "native"}) return out -- 2.52.0 From ae0cfe981d42d02d573756d97a7aeb1a678eef07 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Wed, 27 Sep 2023 10:23:37 +0200 Subject: [PATCH 02/86] update: add space information to testing datagrabbers --- junifer/testing/datagrabbers.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/junifer/testing/datagrabbers.py b/junifer/testing/datagrabbers.py index 280ac3530..da560e87a 100644 --- a/junifer/testing/datagrabbers.py +++ b/junifer/testing/datagrabbers.py @@ -55,7 +55,10 @@ class OasisVBMTestingDataGrabber(BaseDataGrabber): """ out = {} i_sub = int(subject.split("-")[1]) - 1 - out["VBM_GM"] = {"path": Path(self._dataset.gray_matter_maps[i_sub])} + out["VBM_GM"] = { + "path": Path(self._dataset.gray_matter_maps[i_sub]), + "space": "MNI", + } return out @@ -141,8 +144,8 @@ class SPMAuditoryTestingDataGrabber(BaseDataGrabber): anat_fname = self.datadir / f"{subject}_T1w.nii.gz" nib.save(fmri_img, fmri_fname) nib.save(anat_img, anat_fname) - out["BOLD"] = {"path": fmri_fname} - out["T1w"] = {"path": anat_fname} + out["BOLD"] = {"path": fmri_fname, "space": "MNI"} + out["T1w"] = {"path": anat_fname, "space": "native"} return out @@ -236,12 +239,14 @@ class PartlyCloudyTestingDataGrabber(BaseDataGrabber): """ out = {} i_sub = int(subject.split("-")[1]) - 1 - out["BOLD"] = {"path": Path(self._dataset["func"][i_sub])} - conf_format = "fmriprep" - + out["BOLD"] = { + "path": Path(self._dataset["func"][i_sub]), + "space": "MNI", + } out["BOLD_confounds"] = { "path": Path(self._dataset["confounds"][i_sub]), - "format": conf_format, + "format": "fmriprep", + "space": "MNI", } return out -- 2.52.0 From 037016de5a4a1f5b20a41aae0b94f1d343de5313 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Wed, 27 Sep 2023 10:24:18 +0200 Subject: [PATCH 03/86] update: add space information to in-built coordinates --- junifer/data/coordinates.py | 92 +++++++++++++++++++++++++++++-------- 1 file changed, 72 insertions(+), 20 deletions(-) diff --git a/junifer/data/coordinates.py b/junifer/data/coordinates.py index c9acc55e9..32aedc259 100644 --- a/junifer/data/coordinates.py +++ b/junifer/data/coordinates.py @@ -24,31 +24,83 @@ _vois_meta_path = _vois_path / "meta" # A dictionary containing all supported coordinates and their respective file # or data. +# Each entry is a dictionary that must contain at least the following keys: +# * 'space': the coordinates' space (e.g., 'MNI') + # The built-in coordinates are files that are shipped with the package in the # data/VOIs directory. The user can also register their own coordinates, which # will be stored as numpy arrays in the dictionary. _available_coordinates: Dict[ - str, Union[Path, Dict[str, Union[ArrayLike, List[str]]]] + str, Dict[str, Union[Path, ArrayLike, List[str]]] ] = { - "CogAC": _vois_meta_path / "CogAC_VOIs.txt", - "CogAR": _vois_meta_path / "CogAR_VOIs.txt", - "DMNBuckner": _vois_meta_path / "DMNBuckner_VOIs.txt", - "eMDN": _vois_meta_path / "eMDN_VOIs.txt", - "Empathy": _vois_meta_path / "Empathy_VOIs.txt", - "eSAD": _vois_meta_path / "eSAD_VOIs.txt", - "extDMN": _vois_meta_path / "extDMN_VOIs.txt", - "Motor": _vois_meta_path / "Motor_VOIs.txt", - "MultiTask": _vois_meta_path / "MultiTask_VOIs.txt", - "PhysioStress": _vois_meta_path / "PhysioStress_VOIs.txt", - "Rew": _vois_meta_path / "Rew_VOIs.txt", - "Somatosensory": _vois_meta_path / "Somatosensory_VOIs.txt", - "ToM": _vois_meta_path / "ToM_VOIs.txt", - "VigAtt": _vois_meta_path / "VigAtt_VOIs.txt", - "WM": _vois_meta_path / "WM_VOIs.txt", - "Power": _vois_meta_path / "Power2011_MNI_VOIs.txt", - "Power2011": _vois_meta_path / "Power2011_MNI_VOIs.txt", - "Dosenbach": _vois_meta_path / "Dosenbach2010_MNI_VOIs.txt", - "Power2013": _vois_meta_path / "Power2013_MNI_VOIs.tsv", + "CogAC": { + "path": _vois_meta_path / "CogAC_VOIs.txt", + "space": "MNI", + }, + "CogAR": { + "path": _vois_meta_path / "CogAR_VOIs.txt", + "space": "MNI", + }, + "DMNBuckner": { + "path": _vois_meta_path / "DMNBuckner_VOIs.txt", + "space": "MNI", + }, + "eMDN": { + "path": _vois_meta_path / "eMDN_VOIs.txt", + "space": "MNI", + }, + "Empathy": { + "path": _vois_meta_path / "Empathy_VOIs.txt", + "space": "MNI", + }, + "eSAD": { + "path": _vois_meta_path / "eSAD_VOIs.txt", + "space": "MNI", + }, + "extDMN": { + "path": _vois_meta_path / "extDMN_VOIs.txt", + "space": "MNI", + }, + "Motor": { + "path": _vois_meta_path / "Motor_VOIs.txt", + "space": "MNI", + }, + "MultiTask": { + "path": _vois_meta_path / "MultiTask_VOIs.txt", + "space": "MNI", + }, + "PhysioStress": { + "path": _vois_meta_path / "PhysioStress_VOIs.txt", + "space": "MNI", + }, + "Rew": { + "path": _vois_meta_path / "Rew_VOIs.txt", + "space": "MNI", + }, + "Somatosensory": { + "path": _vois_meta_path / "Somatosensory_VOIs.txt", + "space": "MNI", + }, + "ToM": { + "path": _vois_meta_path / "ToM_VOIs.txt", + "space": "MNI", + }, + "VigAtt": { + "path": _vois_meta_path / "VigAtt_VOIs.txt", + "space": "MNI", + }, + "WM": { + "path": _vois_meta_path / "WM_VOIs.txt", + "space": "MNI", + }, + "Power": { + "path": _vois_meta_path / "Power2011_MNI_VOIs.txt", + "space": "MNI", + }, + "Dosenbach": { + "path": _vois_meta_path / "Dosenbach2010_MNI_VOIs.txt", + "space": "MNI", + }, } -- 2.52.0 From f0869c1e2784d6158569fc51af2f9683a723e98d Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Wed, 27 Sep 2023 10:24:59 +0200 Subject: [PATCH 04/86] update: add space parameter to register_coordinates() --- junifer/data/coordinates.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/junifer/data/coordinates.py b/junifer/data/coordinates.py index 32aedc259..b397c8a94 100644 --- a/junifer/data/coordinates.py +++ b/junifer/data/coordinates.py @@ -108,6 +108,7 @@ def register_coordinates( name: str, coordinates: ArrayLike, voi_names: List[str], + space: str, overwrite: Optional[bool] = False, ) -> None: """Register a custom user coordinates. @@ -123,6 +124,8 @@ def register_coordinates( z-coordinates). voi_names : list of str The names of the VOIs. + space : str + The space of the coordinates. overwrite : bool, optional If True, overwrite an existing list of coordinates with the same name. Does not apply to built-in coordinates (default False). @@ -140,7 +143,7 @@ def register_coordinates( """ if name in _available_coordinates: - if isinstance(_available_coordinates[name], Path): + if isinstance(_available_coordinates[name].get("path"), Path): raise_error( f"Coordinates {name} already registered as built-in " "coordinates." @@ -174,6 +177,7 @@ def register_coordinates( _available_coordinates[name] = { "coords": coordinates, "voi_names": voi_names, + "space": space, } -- 2.52.0 From 2ad8c11c6f7c5d9c31daefee5ee26bf6dfe92aeb Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Wed, 27 Sep 2023 10:26:08 +0200 Subject: [PATCH 05/86] update: return space information from load_coordinates() --- junifer/data/coordinates.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/junifer/data/coordinates.py b/junifer/data/coordinates.py index b397c8a94..b4bbf2106 100644 --- a/junifer/data/coordinates.py +++ b/junifer/data/coordinates.py @@ -226,12 +226,12 @@ def get_coordinates( """ # Load the coordinates - seeds, labels = load_coordinates(name=coords) + seeds, labels, _ = load_coordinates(name=coords) return seeds, labels -def load_coordinates(name: str) -> Tuple[ArrayLike, List[str]]: +def load_coordinates(name: str) -> Tuple[ArrayLike, List[str], str]: """Load coordinates. Parameters @@ -245,6 +245,8 @@ def load_coordinates(name: str) -> Tuple[ArrayLike, List[str]]: The coordinates. list of str The names of the VOIs. + str + The space of the coordinates. Raises ------ @@ -277,9 +279,9 @@ def load_coordinates(name: str) -> Tuple[ArrayLike, List[str]]: # Load coordinates t_coord = _available_coordinates[name] - if isinstance(t_coord, Path): + if isinstance(t_coord.get("path"), Path): # Load via pandas - df_coords = pd.read_csv(t_coord, sep="\t", header=None) + df_coords = pd.read_csv(t_coord["path"], sep="\t", header=None) coords = df_coords.iloc[:, [0, 1, 2]].to_numpy() names = list(df_coords.iloc[:, [3]].values[:, 0]) else: @@ -288,4 +290,4 @@ def load_coordinates(name: str) -> Tuple[ArrayLike, List[str]]: names = t_coord["voi_names"] names = typing.cast(List[str], names) - return coords, names + return coords, names, t_coord["space"] -- 2.52.0 From 48b09572678f4c7653843e7262b0303ffabaedd1 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Wed, 27 Sep 2023 10:27:37 +0200 Subject: [PATCH 06/86] update: add space information to in-built parcellations --- junifer/data/parcellations.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/junifer/data/parcellations.py b/junifer/data/parcellations.py index dc5b9a4c4..13b1c9c20 100644 --- a/junifer/data/parcellations.py +++ b/junifer/data/parcellations.py @@ -34,6 +34,7 @@ if TYPE_CHECKING: # Each entry is a dictionary that must contain at least the following keys: # * 'family': the parcellation's family name (e.g. 'Schaefer', 'SUIT') +# * 'space': the parcellation's space (e.g., 'MNI', 'SUIT') # Optional keys: # * 'valid_resolutions': a list of valid resolutions for the parcellation @@ -53,6 +54,7 @@ for n_rois in range(100, 1001, 100): "family": "Schaefer", "n_rois": n_rois, "yeo_networks": t_net, + "space": "MNI", } # Add Tian parcellation info for scale in range(1, 5): @@ -82,6 +84,7 @@ for version in (1, 2): _available_parcellations[f"AICHA_v{version}"] = { "family": "AICHA", "version": version, + "space": "MNI", } # Add Shen parcellation info for year in (2013, 2015, 2019): @@ -91,18 +94,21 @@ for year in (2013, 2015, 2019): "family": "Shen", "year": 2013, "n_rois": n_rois, + "space": "MNI", } elif year == 2015: _available_parcellations["Shen_2015_268"] = { "family": "Shen", "year": 2015, "n_rois": 268, + "space": "MNI", } elif year == 2019: _available_parcellations["Shen_2019_368"] = { "family": "Shen", "year": 2019, "n_rois": 368, + "space": "MNI", } # Add Yan parcellation info for n_rois in range(100, 1001, 100): @@ -112,12 +118,14 @@ for n_rois in range(100, 1001, 100): "family": "Yan", "n_rois": n_rois, "yeo_networks": yeo_network, + "space": "MNI", } # Add Kong networks _available_parcellations[f"Yan{n_rois}xKong17"] = { "family": "Yan", "n_rois": n_rois, "kong_networks": 17, + "space": "MNI", } -- 2.52.0 From fa16022064dbffb1e5a69105827d1d403c66a6f5 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Wed, 27 Sep 2023 10:28:42 +0200 Subject: [PATCH 07/86] update: add space parameter to register_parcellation() --- junifer/data/parcellations.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/junifer/data/parcellations.py b/junifer/data/parcellations.py index 13b1c9c20..aaf13f39c 100644 --- a/junifer/data/parcellations.py +++ b/junifer/data/parcellations.py @@ -133,6 +133,7 @@ def register_parcellation( name: str, parcellation_path: Union[str, Path], parcels_labels: List[str], + space: str, overwrite: bool = False, ) -> None: """Register a custom user parcellation. @@ -145,6 +146,8 @@ def register_parcellation( The path to the parcellation file. parcels_labels : list of str The list of labels for the parcellation. + space : str + The space of the parcellation. overwrite : bool, optional If True, overwrite an existing parcellation with the same name. Does not apply to built-in parcellations (default False). @@ -181,6 +184,7 @@ def register_parcellation( "path": str(parcellation_path.absolute()), "labels": parcels_labels, "family": "CustomUserParcellation", + "space": space, } -- 2.52.0 From 19225e3bff22ec50927df4e0c8045a41624cfd04 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Wed, 27 Sep 2023 10:29:16 +0200 Subject: [PATCH 08/86] update: return space information from load_parcellation() --- junifer/data/parcellations.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/junifer/data/parcellations.py b/junifer/data/parcellations.py index aaf13f39c..8e1c29c3a 100644 --- a/junifer/data/parcellations.py +++ b/junifer/data/parcellations.py @@ -235,7 +235,7 @@ def get_parcellation( all_parcellations = [] all_labels = [] for name in parcellation: - img, labels, _ = load_parcellation( + img, labels, _, _ = load_parcellation( name=name, resolution=resolution, ) @@ -269,7 +269,7 @@ def load_parcellation( parcellations_dir: Union[str, Path, None] = None, resolution: Optional[float] = None, path_only: bool = False, -) -> Tuple[Optional["Nifti1Image"], List[str], Path]: +) -> Tuple[Optional["Nifti1Image"], List[str], Path, str]: """Load a brain parcellation (including a label file). If it is a built-in parcellation and the file is not present in the @@ -299,6 +299,8 @@ def load_parcellation( Parcellation labels. pathlib.Path File path to the parcellation image. + str + The space of the parcellation. Raises ------ @@ -317,6 +319,11 @@ def load_parcellation( # Copy parcellation definition to avoid edits in original object parcellation_definition = _available_parcellations[name].copy() t_family = parcellation_definition.pop("family") + # Remove space conditionally + if t_family not in ["SUIT", "Tian"]: + space = parcellation_definition.pop("space") + else: + space = parcellation_definition["space"] # Check if the parcellation family is custom or built-in if t_family == "CustomUserParcellation": @@ -355,7 +362,7 @@ def load_parcellation( # Type-cast to remove errors parcellation_img = typing.cast("Nifti1Image", parcellation_img) - return parcellation_img, parcellation_labels, parcellation_fname + return parcellation_img, parcellation_labels, parcellation_fname, space def _retrieve_parcellation( -- 2.52.0 From 43d0ff2e14aeaf41b2c2625d820066b6219bb5bc Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Wed, 27 Sep 2023 10:30:24 +0200 Subject: [PATCH 09/86] update: add space information to in-built masks --- junifer/data/masks.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/junifer/data/masks.py b/junifer/data/masks.py index 7f22e907b..8d1eec9f6 100644 --- a/junifer/data/masks.py +++ b/junifer/data/masks.py @@ -66,29 +66,37 @@ def _fetch_icbm152_brain_gm_mask( # A dictionary containing all supported masks and their respective file or # data. +# Each entry is a dictionary that must contain at least the following keys: +# * 'family': the mask's family name (e.g., 'Vickery-Patil', 'Callable') +# * 'space': the mask's space (e.g., 'MNI', 'inherit') + # 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. # Callable masks should be functions that take at least one parameter: # * `target_img`: the image to which the mask will be applied. _available_masks: Dict[str, Dict[str, Any]] = { - "GM_prob0.2": {"family": "Vickery-Patil"}, - "GM_prob0.2_cortex": {"family": "Vickery-Patil"}, + "GM_prob0.2": {"family": "Vickery-Patil", "space": "MNI"}, + "GM_prob0.2_cortex": {"family": "Vickery-Patil", "space": "MNI"}, "compute_brain_mask": { "family": "Callable", "func": compute_brain_mask, + "space": "inherit", }, "compute_background_mask": { "family": "Callable", "func": compute_background_mask, + "space": "inherit", }, "compute_epi_mask": { "family": "Callable", "func": compute_epi_mask, + "space": "inherit", }, "fetch_icbm152_brain_gm_mask": { "family": "Callable", "func": _fetch_icbm152_brain_gm_mask, + "space": "inherit", }, } -- 2.52.0 From 69420eb2d4c6cb3a2020915e37b928c299d38ba7 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Wed, 27 Sep 2023 10:33:18 +0200 Subject: [PATCH 10/86] update: add space parameter to register_masks() --- junifer/data/masks.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/junifer/data/masks.py b/junifer/data/masks.py index 8d1eec9f6..1512d8daf 100644 --- a/junifer/data/masks.py +++ b/junifer/data/masks.py @@ -104,6 +104,7 @@ _available_masks: Dict[str, Dict[str, Any]] = { def register_mask( name: str, mask_path: Union[str, Path], + space: str, overwrite: bool = False, ) -> None: """Register a custom user mask. @@ -114,6 +115,8 @@ def register_mask( The name of the mask. mask_path : str or pathlib.Path The path to the mask file. + space : str + The space of the mask. overwrite : bool, optional If True, overwrite an existing mask with the same name. Does not apply to built-in mask (default False). @@ -144,6 +147,7 @@ def register_mask( _available_masks[name] = { "path": str(mask_path.absolute()), "family": "CustomUserMask", + "space": space, } -- 2.52.0 From 621ac914c1f77df927caf87122ac203f16d6efd2 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Wed, 27 Sep 2023 10:34:10 +0200 Subject: [PATCH 11/86] update: return space information from load_masks() --- junifer/data/masks.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/junifer/data/masks.py b/junifer/data/masks.py index 1512d8daf..0c8fc6f77 100644 --- a/junifer/data/masks.py +++ b/junifer/data/masks.py @@ -3,6 +3,7 @@ # Authors: Federico Raimondo # License: AGPL +import typing from pathlib import Path from typing import ( TYPE_CHECKING, @@ -250,7 +251,7 @@ def get_mask( ) mask_img = extra_input[inherited_mask_item]["data"] else: - mask_object, _ = load_mask( + mask_object, _, _ = load_mask( mask_name, path_only=False, resolution=resolution ) if callable(mask_object): @@ -287,8 +288,8 @@ def load_mask( name: str, resolution: Optional[float] = None, path_only: bool = False, -) -> Tuple[Optional[Union["Nifti1Image", Callable]], Optional[Path]]: - """Load mask. +) -> Tuple[Optional[Union["Nifti1Image", Callable]], Optional[Path], str]: + """Load a mask. Parameters ---------- @@ -308,6 +309,14 @@ def load_mask( Loaded mask image. pathlib.Path or None File path to the mask image. + str + The space of the mask. + + Raises + ------ + ValueError + If the ``name`` is invalid of if the mask family is invalid. + """ mask_img = None if name not in _available_masks: @@ -333,7 +342,9 @@ def load_mask( if path_only is False: mask_img = nib.load(mask_fname) - return mask_img, mask_fname + # Type-cast to remove error + mask_img = typing.cast("Nifti1Image", mask_img) + return mask_img, mask_fname, mask_definition["space"] def _load_vickery_patil_mask( -- 2.52.0 From 5a38b8f997cc36a221f9dec82afb6ec09af4cb22 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Mon, 11 Sep 2023 10:45:28 +0200 Subject: [PATCH 12/86] fix: type cast to remove static type check errors in data.masks --- junifer/data/masks.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/junifer/data/masks.py b/junifer/data/masks.py index 0c8fc6f77..f4ad92d24 100644 --- a/junifer/data/masks.py +++ b/junifer/data/masks.py @@ -281,6 +281,8 @@ def get_mask( ) mask_img = all_masks[0] + # Type-cast to remove errors + mask_img = typing.cast("Nifti1Image", mask_img) return mask_img -- 2.52.0 From 573b6c6a0d0246891276e18e7128d8c8dd678897 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Wed, 27 Sep 2023 10:39:47 +0200 Subject: [PATCH 13/86] chore: fix tests affected by space compulsion change in test_coordinates.py --- junifer/data/tests/test_coordinates.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/junifer/data/tests/test_coordinates.py b/junifer/data/tests/test_coordinates.py index f762c2f79..5c6e8fb77 100644 --- a/junifer/data/tests/test_coordinates.py +++ b/junifer/data/tests/test_coordinates.py @@ -24,6 +24,7 @@ def test_register_coordinates_built_in_check() -> None: name="DMNBuckner", coordinates=np.zeros(2), voi_names=["1", "2"], + space="MNI", overwrite=True, ) @@ -34,6 +35,7 @@ def test_register_coordinates_overwrite() -> None: name="MyList", coordinates=np.zeros((2, 3)), voi_names=["roi1", "roi2"], + space="MNI", overwrite=True, ) with pytest.raises(ValueError, match=r"already registered"): @@ -41,18 +43,21 @@ def test_register_coordinates_overwrite() -> None: name="MyList", coordinates=np.ones((2, 3)), voi_names=["roi2", "roi3"], + space="MNI", ) register_coordinates( name="MyList", coordinates=np.ones((2, 3)), voi_names=["roi2", "roi3"], + space="MNI", overwrite=True, ) - coord, names = load_coordinates("MyList") + coord, names, space = load_coordinates("MyList") assert_array_equal(coord, np.ones((2, 3))) assert names == ["roi2", "roi3"] + assert space == "MNI" def test_register_coordinates_valid_input() -> None: @@ -62,6 +67,7 @@ def test_register_coordinates_valid_input() -> None: name="MyList", coordinates=[1, 2], voi_names=["roi1", "roi2"], + space="MNI", overwrite=True, ) with pytest.raises(ValueError, match=r"2D array"): @@ -69,6 +75,7 @@ def test_register_coordinates_valid_input() -> None: name="MyList", coordinates=np.zeros((2, 3, 4)), voi_names=["roi1", "roi2"], + space="MNI", overwrite=True, ) @@ -77,6 +84,7 @@ def test_register_coordinates_valid_input() -> None: name="MyList", coordinates=np.zeros((2, 4)), voi_names=["roi1", "roi2"], + space="MNI", overwrite=True, ) with pytest.raises(ValueError, match=r"voi_names"): @@ -84,6 +92,7 @@ def test_register_coordinates_valid_input() -> None: name="MyList", coordinates=np.zeros((2, 3)), voi_names=["roi1", "roi2", "roi3"], + space="MNI", overwrite=True, ) @@ -99,9 +108,10 @@ def test_list_coordinates() -> None: def test_load_coordinates() -> None: """Test loading coordinates from file.""" - coord, names = load_coordinates("DMNBuckner") + coord, names, space = load_coordinates("DMNBuckner") assert coord.shape == (6, 3) # type: ignore assert names == ["PCC", "MPFC", "lAG", "rAG", "lHF", "rHF"] + assert space == "MNI" def test_load_coordinates_nonexisting() -> None: -- 2.52.0 From f3d79e84cab8a0d444b84a7deb90c429f6cde6d7 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Wed, 27 Sep 2023 10:41:10 +0200 Subject: [PATCH 14/86] chore: fix tests affected by space compulsion change in test_masks.py --- junifer/data/tests/test_masks.py | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/junifer/data/tests/test_masks.py b/junifer/data/tests/test_masks.py index 85c4e252a..f73bcfd39 100644 --- a/junifer/data/tests/test_masks.py +++ b/junifer/data/tests/test_masks.py @@ -41,6 +41,7 @@ def test_register_mask_built_in_check() -> None: register_mask( name="GM_prob0.2", mask_path="testmask.nii.gz", + space="MNI", overwrite=True, ) @@ -57,6 +58,7 @@ def test_register_mask_already_registered() -> None: register_mask( name="testmask", mask_path="testmask.nii.gz", + space="MNI", ) out = load_mask("testmask", path_only=True) assert out[1] is not None @@ -67,10 +69,12 @@ def test_register_mask_already_registered() -> None: register_mask( name="testmask", mask_path="testmask.nii.gz", + space="MNI", ) register_mask( name="testmask", mask_path="testmask2.nii.gz", + space="MNI", overwrite=True, ) @@ -80,16 +84,17 @@ def test_register_mask_already_registered() -> None: @pytest.mark.parametrize( - "name, mask_path, overwrite", + "name, mask_path, space, overwrite", [ - ("testmask_1", "testmask_1.nii.gz", True), - ("testmask_2", "testmask_2.nii.gz", True), - ("testmask_3", Path("testmask_3.nii.gz"), True), + ("testmask_1", "testmask_1.nii.gz", "MNI", True), + ("testmask_2", "testmask_2.nii.gz", "MNI", True), + ("testmask_3", Path("testmask_3.nii.gz"), "MNI", True), ], ) def test_register_mask( name: str, mask_path: str, + space: str, overwrite: bool, ) -> None: """Test mask registration. @@ -100,6 +105,8 @@ def test_register_mask( The parametrized mask name. mask_path : str or pathlib.Path The parametrized mask path. + space : str + The parametrized mask space. overwrite : bool The parametrized mask overwrite value. @@ -108,16 +115,18 @@ def test_register_mask( register_mask( name=name, mask_path=mask_path, + space=space, 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) + _, fname, mask_space = load_mask(name=name, path_only=True) # Check values for registered mask assert fname is not None assert fname.name == f"{name}.nii.gz" + assert space == mask_space @pytest.mark.parametrize( @@ -191,7 +200,7 @@ def test_get_mask() -> None: assert mask.shape == vbm_gm_img.shape assert_array_equal(mask.affine, vbm_gm_img.affine) - raw_mask_img, _ = load_mask("GM_prob0.2", resolution=1.5) + raw_mask_img, _, _ = load_mask("GM_prob0.2", resolution=1.5) res_mask_img = resample_to_img( raw_mask_img, vbm_gm_img, @@ -207,7 +216,11 @@ def test_mask_callable() -> None: def ident(x): return x - _available_masks["identity"] = {"family": "Callable", "func": ident} + _available_masks["identity"] = { + "family": "Callable", + "func": ident, + "space": "MNI", + } reader = DefaultDataReader() with OasisVBMTestingDataGrabber() as dg: input = dg["sub-01"] -- 2.52.0 From e95672f36b8c0f207bcb14da277805e4791ad3e4 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Wed, 27 Sep 2023 10:41:37 +0200 Subject: [PATCH 15/86] update: parametrize Vickery-Patil mask tests in test_masks.py --- junifer/data/tests/test_masks.py | 78 +++++++++++++++++++++----------- 1 file changed, 52 insertions(+), 26 deletions(-) diff --git a/junifer/data/tests/test_masks.py b/junifer/data/tests/test_masks.py index f73bcfd39..d7b027860 100644 --- a/junifer/data/tests/test_masks.py +++ b/junifer/data/tests/test_masks.py @@ -6,7 +6,7 @@ # License: AGPL from pathlib import Path -from typing import Callable, Dict, List, Union +from typing import Callable, Dict, List, Optional, Union import numpy as np import pytest @@ -155,36 +155,62 @@ 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") +@pytest.mark.parametrize( + "name, resolution, pixdim, fname", + [ + ( + "GM_prob0.2", + None, + [1.5, 1.5, 1.5], + "CAT12_IXI555_MNI152_TMP_GS_GMprob0.2_clean.nii.gz", + ), + ( + "GM_prob0.2", + 3.0, + [3.0, 3.0, 3.0], + "CAT12_IXI555_MNI152_TMP_GS_GMprob0.2_clean_3mm.nii.gz", + ), + ( + "GM_prob0.2_cortex", + None, + [3.0, 3.0, 3.0], + "GMprob0.2_cortex_3mm_NA_rm.nii.gz", + ), + ], +) +def test_vickery_patil( + name: str, + resolution: Optional[float], + pixdim: List[float], + fname: str, +) -> None: + """Test Vickery-Patil mask. + + Parameters + ---------- + name : str + The parametrized name of the mask. + resolution : float or None + The parametrized resolution of the mask. + pixdim : list of float + The parametrized pixel dimensions of the mask. + fname : str + The parametrized name of the mask file. + + """ + mask, mask_fname, space = load_mask(name, resolution=resolution) assert_array_almost_equal( - mask.header["pixdim"][1:4], [1.5, 1.5, 1.5] # type: ignore + mask.header["pixdim"][1:4], pixdim # type: ignore ) + assert space == "MNI" + assert mask_fname is not None + assert mask_fname.name == fname - assert fname is not None - 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 is not None - 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 is not None - assert fname.name == "GMprob0.2_cortex_3mm_NA_rm.nii.gz" +def test_vickery_patil_error() -> None: + """Test error for Vickery-Patil mask.""" with pytest.raises(ValueError, match=r"find a Vickery-Patil mask "): - _load_vickery_patil_mask("wrong", resolution=2) + _load_vickery_patil_mask(name="wrong", resolution=2.0) def test_get_mask() -> None: -- 2.52.0 From eda957e09030b91a99631ca5ed8307ff3f7276d9 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Wed, 27 Sep 2023 10:43:05 +0200 Subject: [PATCH 16/86] chore: fix tests affected by space compulsion change in test_parcellations.py --- junifer/data/tests/test_parcellations.py | 110 +++++++++++++---------- 1 file changed, 61 insertions(+), 49 deletions(-) diff --git a/junifer/data/tests/test_parcellations.py b/junifer/data/tests/test_parcellations.py index 7eae10aa1..a6e9277c0 100644 --- a/junifer/data/tests/test_parcellations.py +++ b/junifer/data/tests/test_parcellations.py @@ -39,6 +39,7 @@ def test_register_parcellation_built_in_check() -> None: name="SUITxSUIT", parcellation_path="testparc.nii.gz", parcels_labels=["1", "2", "3"], + space="SUIT", overwrite=True, ) @@ -56,6 +57,7 @@ def test_register_parcellation_already_registered() -> None: name="testparc", parcellation_path="testparc.nii.gz", parcels_labels=["1", "2", "3"], + space="MNI", ) assert ( load_parcellation("testparc", path_only=True)[2].name @@ -68,11 +70,13 @@ def test_register_parcellation_already_registered() -> None: name="testparc", parcellation_path="testparc.nii.gz", parcels_labels=["1", "2", "3"], + space="MNI", ) register_parcellation( name="testparc", parcellation_path="testparc2.nii.gz", parcels_labels=["1", "2", "3"], + space="MNI", overwrite=True, ) @@ -90,17 +94,19 @@ def test_parcellation_wrong_labels_values(tmp_path: Path) -> None: tmp_path : pathlib.Path The path to the test directory. """ - schaefer, labels, schaefer_path = load_parcellation("Schaefer100x7") + schaefer, labels, schaefer_path, _ = load_parcellation("Schaefer100x7") assert schaefer is not None # Test wrong number of labels - register_parcellation("WrongLabels", schaefer_path, labels[:10]) + register_parcellation("WrongLabels", schaefer_path, labels[:10], "MNI") with pytest.raises(ValueError, match=r"has 100 parcels but 10"): load_parcellation("WrongLabels") # Test wrong number of labels - register_parcellation("WrongLabels2", schaefer_path, [*labels, "wrong"]) + register_parcellation( + "WrongLabels2", schaefer_path, [*labels, "wrong"], "MNI" + ) with pytest.raises(ValueError, match=r"has 100 parcels but 101"): load_parcellation("WrongLabels2") @@ -111,7 +117,7 @@ def test_parcellation_wrong_labels_values(tmp_path: Path) -> None: new_schaefer_img = new_img_like(schaefer, schaefer_data) nib.save(new_schaefer_img, new_schaefer_path) - register_parcellation("WrongValues", new_schaefer_path, labels[:-1]) + register_parcellation("WrongValues", new_schaefer_path, labels[:-1], "MNI") with pytest.raises(ValueError, match=r"the range [0, 99]"): load_parcellation("WrongValues") @@ -121,23 +127,30 @@ def test_parcellation_wrong_labels_values(tmp_path: Path) -> None: new_schaefer_img = new_img_like(schaefer, schaefer_data) nib.save(new_schaefer_img, new_schaefer_path) - register_parcellation("WrongValues2", new_schaefer_path, labels) + register_parcellation("WrongValues2", new_schaefer_path, labels, "MNI") with pytest.raises(ValueError, match=r"the range [0, 100]"): load_parcellation("WrongValues2") @pytest.mark.parametrize( - "name, parcellation_path, parcels_labels, overwrite", + "name, parcellation_path, parcels_labels, space, overwrite", [ - ("testparc_1", "testparc_1.nii.gz", ["1", "2", "3"], True), - ("testparc_2", "testparc_2.nii.gz", ["1", "2", "6"], True), - ("testparc_3", Path("testparc_3.nii.gz"), ["1", "2", "6"], True), + ("testparc_1", "testparc_1.nii.gz", ["1", "2", "3"], "MNI", True), + ("testparc_2", "testparc_2.nii.gz", ["1", "2", "6"], "MNI", True), + ( + "testparc_3", + Path("testparc_3.nii.gz"), + ["1", "2", "6"], + "MNI", + True, + ), ], ) def test_register_parcellation( name: str, parcellation_path: str, parcels_labels: List[str], + space: str, overwrite: bool, ) -> None: """Test parcellation registration. @@ -150,6 +163,8 @@ def test_register_parcellation( The parametrized parcellation path. parcels_labels : list of str The parametrized parcellation labels. + space : str + The parametrized parcellation space. overwrite : bool The parametrized parcellation overwrite value. @@ -159,16 +174,20 @@ def test_register_parcellation( name=name, parcellation_path=parcellation_path, parcels_labels=parcels_labels, + space=space, overwrite=overwrite, ) # List available parcellation and check registration parcellations = list_parcellations() assert name in parcellations # Load registered parcellation - _, lbl, fname = load_parcellation(name=name, path_only=True) + _, lbl, fname, parcellation_space = load_parcellation( + name=name, path_only=True + ) # Check values for registered parcellation assert lbl == parcels_labels assert fname.name == f"{name}.nii.gz" + assert parcellation_space == space @pytest.mark.parametrize( @@ -284,7 +303,7 @@ def test_schaefer( f"{int(resolution)}mm.nii.gz" ) # Load parcellation - img, label, img_path = load_parcellation( + img, label, img_path, space = load_parcellation( name=parcellation_name, parcellations_dir=tmp_path, resolution=resolution, @@ -292,6 +311,7 @@ def test_schaefer( assert img is not None assert img_path.name == parcellation_file assert len(label) == n_rois + assert space == "MNI" assert_array_equal( img.header["pixdim"][1:4], 3 * [resolution] # type: ignore ) @@ -351,12 +371,13 @@ def test_suit(tmp_path: Path, space: str) -> None: parcellations = list_parcellations() assert f"SUITx{space}" in parcellations # Load parcellation - img, label, img_path = load_parcellation( + img, label, img_path, parcellation_space = load_parcellation( name=f"SUITx{space}", parcellations_dir=tmp_path, ) assert img is not None assert img_path.name == f"SUIT_{space}Space_1mm.nii" + assert parcellation_space == space assert len(label) == 34 assert_array_equal(img.header["pixdim"][1:4], [1, 1, 1]) # type: ignore @@ -400,16 +421,17 @@ def test_tian_3T_6thgeneration( assert "TianxS3x3TxMNI6thgeneration" in parcellations assert "TianxS4x3TxMNI6thgeneration" in parcellations # Load parcellation - img, lbl, fname = load_parcellation( + img, lbl, fname, parcellation_space_1 = load_parcellation( name=f"TianxS{scale}x3TxMNI6thgeneration", parcellations_dir=tmp_path ) fname1 = f"Tian_Subcortex_S{scale}_3T_1mm.nii.gz" assert img is not None assert fname.name == fname1 + assert parcellation_space_1 == "MNI6thgeneration" assert len(lbl) == n_label assert_array_equal(img.header["pixdim"][1:4], [1, 1, 1]) # type: ignore # Load parcellation - img, lbl, fname = load_parcellation( + img, lbl, fname, parcellation_space_2 = load_parcellation( name=f"TianxS{scale}x3TxMNI6thgeneration", parcellations_dir=tmp_path, resolution=2, @@ -417,6 +439,7 @@ def test_tian_3T_6thgeneration( fname1 = f"Tian_Subcortex_S{scale}_3T.nii.gz" assert img is not None assert fname.name == fname1 + assert parcellation_space_2 == "MNI6thgeneration" assert len(lbl) == n_label assert_array_equal(img.header["pixdim"][1:4], [2, 2, 2]) # type: ignore @@ -445,13 +468,14 @@ def test_tian_3T_nonlinear2009cAsym( assert "TianxS3x3TxMNInonlinear2009cAsym" in parcellations assert "TianxS4x3TxMNInonlinear2009cAsym" in parcellations # Load parcellation - img, lbl, fname = load_parcellation( + img, lbl, fname, space = load_parcellation( name=f"TianxS{scale}x3TxMNInonlinear2009cAsym", parcellations_dir=tmp_path, ) fname1 = f"Tian_Subcortex_S{scale}_3T_2009cAsym.nii.gz" assert img is not None assert fname.name == fname1 + assert space == "MNInonlinear2009cAsym" assert len(lbl) == n_label assert_array_equal(img.header["pixdim"][1:4], [2, 2, 2]) # type: ignore @@ -480,12 +504,13 @@ def test_tian_7T_6thgeneration( assert "TianxS3x7TxMNI6thgeneration" in parcellations assert "TianxS4x7TxMNI6thgeneration" in parcellations # Load parcellation - img, lbl, fname = load_parcellation( + img, lbl, fname, space = load_parcellation( name=f"TianxS{scale}x7TxMNI6thgeneration", parcellations_dir=tmp_path ) fname1 = f"Tian_Subcortex_S{scale}_7T.nii.gz" assert img is not None assert fname.name == fname1 + assert space == "MNI6thgeneration" assert len(lbl) == n_label assert_array_almost_equal( img.header["pixdim"][1:4], [1.6, 1.6, 1.6] # type: ignore @@ -567,11 +592,12 @@ def test_aicha(tmp_path: Path, version: int) -> None: parcellations = list_parcellations() assert f"AICHA_v{version}" in parcellations # Load parcellation - img, label, img_path = load_parcellation( + img, label, img_path, space = load_parcellation( name=f"AICHA_v{version}", parcellations_dir=tmp_path ) assert img is not None assert img_path.name == "AICHA.nii" + assert space == "MNI" assert len(label) == 384 assert_array_equal(img.header["pixdim"][1:4], [2, 2, 2]) # type: ignore @@ -635,13 +661,14 @@ def test_shen( parcellations = list_parcellations() assert f"Shen_{year}_{n_rois}" in parcellations # Load parcellation - img, label, img_path = load_parcellation( + img, label, img_path, space = load_parcellation( name=f"Shen_{year}_{n_rois}", parcellations_dir=tmp_path, resolution=resolution, ) assert img is not None assert img_name in img_path.name + assert space == "MNI" assert len(label) == n_labels assert_array_equal( img.header["pixdim"][1:4], 3 * [resolution] # type: ignore @@ -830,13 +857,14 @@ def test_yan( f"{int(resolution)}mm.nii.gz" ) # Load parcellation - img, label, img_path = load_parcellation( + img, label, img_path, space = load_parcellation( name=parcellation_name, # type: ignore parcellations_dir=tmp_path, resolution=resolution, ) assert img is not None assert img_path.name == parcellation_file # type: ignore + assert space == "MNI" assert len(label) == n_rois assert_array_equal( img.header["pixdim"][1:4], 3 * [resolution] # type: ignore @@ -927,10 +955,10 @@ def test_retrieve_yan_incorrect_kong_networks(tmp_path: Path) -> None: def test_merge_parcellations() -> None: """Test merging parcellations.""" # load some parcellations for testing - schaefer_parcellation, schaefer_labels, _ = load_parcellation( + schaefer_parcellation, schaefer_labels, _, _ = load_parcellation( "Schaefer100x17" ) - tian_parcellation, tian_labels, _ = load_parcellation( + tian_parcellation, tian_labels, _, _ = load_parcellation( "TianxS2x3TxMNInonlinear2009cAsym" ) # prepare the list of the actual parcellations @@ -963,7 +991,7 @@ def test_merge_parcellations_3D_multiple_non_overlapping( """ # Get the testing parcellation - parcellation, labels, _ = load_parcellation("Schaefer100x7") + parcellation, labels, _, _ = load_parcellation("Schaefer100x7") assert parcellation is not None @@ -984,7 +1012,7 @@ def test_merge_parcellations_3D_multiple_non_overlapping( names = ["high", "low"] labels_lists = [labels1, labels2] - merged_parc, merged_labels = merge_parcellations( + merged_parc, _ = merge_parcellations( parcellation_list, names, labels_lists ) @@ -994,18 +1022,11 @@ def test_merge_parcellations_3D_multiple_non_overlapping( assert len(np.unique(parc_data)) == 101 # 100 + 1 because background 0 -def test_merge_parcellations_3D_multiple_overlapping(tmp_path: Path) -> None: - """Test merge_parcellations with multiple overlapping parcellations. - - Parameters - ---------- - tmp_path : pathlib.Path - The path to the test directory. - - """ +def test_merge_parcellations_3D_multiple_overlapping() -> None: + """Test merge_parcellations with multiple overlapping parcellations.""" # Get the testing parcellation - parcellation, labels, _ = load_parcellation("Schaefer100x7") + parcellation, labels, _, _ = load_parcellation("Schaefer100x7") assert parcellation is not None @@ -1038,20 +1059,11 @@ def test_merge_parcellations_3D_multiple_overlapping(tmp_path: Path) -> None: assert len(np.unique(parc_data)) == 101 # 100 + 1 because background 0 -def test_merge_parcellations_3D_multiple_duplicated_labels( - tmp_path: Path, -) -> None: - """Test merge_parcellations with two parcellations with duplicated labels. - - Parameters - ---------- - tmp_path : pathlib.Path - The path to the test directory. - - """ +def test_merge_parcellations_3D_multiple_duplicated_labels() -> None: + """Test merge_parcellations with duplicated labels.""" # Get the testing parcellation - parcellation, labels, _ = load_parcellation("Schaefer100x7") + parcellation, labels, _, _ = load_parcellation("Schaefer100x7") assert parcellation is not None @@ -1100,7 +1112,7 @@ def test_get_parcellation_single() -> None: assert tailored_parcellation.shape == vbm_gm_img.shape assert_array_equal(tailored_parcellation.affine, vbm_gm_img.affine) # Get raw parcellation - raw_parcellation, raw_labels, _ = load_parcellation( + raw_parcellation, raw_labels, _, _ = load_parcellation( "Schaefer100x7", resolution=1.5, ) @@ -1118,8 +1130,8 @@ def test_get_parcellation_single() -> None: assert tailored_labels == raw_labels -def test_get_parcellation_multi() -> None: - """Test tailored multi parcellation fetch.""" +def test_get_parcellation_multi_same_space() -> None: + """Test tailored multi parcellation fetch in same space.""" reader = DefaultDataReader() with OasisVBMTestingDataGrabber() as dg: element = dg["sub-01"] @@ -1145,7 +1157,7 @@ def test_get_parcellation_multi() -> None: "TianxS2x3TxMNInonlinear2009cAsym", ] for name in parcellations_names: - img, labels, _ = load_parcellation(name=name, resolution=1.5) + img, labels, _, _ = load_parcellation(name=name, resolution=1.5) # Resample raw parcellations resampled_img = resample_to_img( source_img=img, -- 2.52.0 From f4d6f6e85374a68e4d1175e28174e39624dda9ba Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Wed, 27 Sep 2023 10:48:46 +0200 Subject: [PATCH 17/86] chore: fix tests affected by space compulsion change in test_parcel_aggregation.py --- .../markers/tests/test_parcel_aggregation.py | 82 +++++++++++++------ 1 file changed, 56 insertions(+), 26 deletions(-) diff --git a/junifer/markers/tests/test_parcel_aggregation.py b/junifer/markers/tests/test_parcel_aggregation.py index 84d5d4c58..615aad7e9 100644 --- a/junifer/markers/tests/test_parcel_aggregation.py +++ b/junifer/markers/tests/test_parcel_aggregation.py @@ -82,7 +82,7 @@ def test_ParcelAggregation_3D() -> None: name="gmd_schaefer100x7_mean", on="VBM_GM", ) # Test passing "on" as a keyword argument - input = {"VBM_GM": {"data": img, "meta": {}}} + input = {"VBM_GM": {"data": img, "meta": {}, "space": "MNI"}} jun_values3d_mean = marker.fit_transform(input)["VBM_GM"]["data"] assert jun_values3d_mean.ndim == 2 @@ -98,7 +98,7 @@ def test_ParcelAggregation_3D() -> None: # Use the ParcelAggregation object marker = ParcelAggregation(parcellation="Schaefer100x7", method="std") - input = {"VBM_GM": {"data": img, "meta": {}}} + input = {"VBM_GM": {"data": img, "meta": {}, "space": "MNI"}} jun_values3d_std = marker.fit_transform(input)["VBM_GM"]["data"] assert jun_values3d_std.ndim == 2 @@ -122,7 +122,7 @@ def test_ParcelAggregation_3D() -> None: method="trim_mean", method_params={"proportiontocut": 0.1}, ) - input = {"VBM_GM": {"data": img, "meta": {}}} + input = {"VBM_GM": {"data": img, "meta": {}, "space": "MNI"}} jun_values3d_tm = marker.fit_transform(input)["VBM_GM"]["data"] assert jun_values3d_tm.ndim == 2 @@ -147,7 +147,7 @@ def test_ParcelAggregation_4D(): # Create ParcelAggregation object marker = ParcelAggregation(parcellation="Schaefer100x7", method="mean") - input = {"BOLD": {"data": fmri_img, "meta": {}}} + input = {"BOLD": {"data": fmri_img, "meta": {}, "space": "MNI"}} jun_values4d = marker.fit_transform(input)["BOLD"]["data"] assert jun_values4d.ndim == 2 @@ -175,7 +175,7 @@ def test_ParcelAggregation_storage(tmp_path: Path) -> None: "element": {"subject": "sub-01", "session": "ses-01"}, "dependencies": {"nilearn", "nibabel"}, } - input = {"VBM_GM": {"data": img, "meta": meta}} + input = {"VBM_GM": {"data": img, "meta": meta, "space": "MNI"}} marker = ParcelAggregation( parcellation="Schaefer100x7", method="mean", on="VBM_GM" ) @@ -194,7 +194,7 @@ def test_ParcelAggregation_storage(tmp_path: Path) -> None: # Get the SPM auditory data subject_data = datasets.fetch_spm_auditory() fmri_img = concat_imgs(subject_data.func) # type: ignore - input = {"BOLD": {"data": fmri_img, "meta": meta}} + input = {"BOLD": {"data": fmri_img, "meta": meta, "space": "MNI"}} marker = ParcelAggregation( parcellation="Schaefer100x7", method="mean", on="BOLD" ) @@ -234,7 +234,7 @@ def test_ParcelAggregation_3D_mask() -> None: name="gmd_schaefer100x7_mean", on="VBM_GM", ) # Test passing "on" as a keyword argument - input = {"VBM_GM": {"data": img, "meta": {}}} + input = {"VBM_GM": {"data": img, "meta": {}, "space": "MNI"}} jun_values3d_mean = marker.fit_transform(input)["VBM_GM"]["data"] assert jun_values3d_mean.ndim == 2 @@ -279,7 +279,7 @@ def test_ParcelAggregation_3D_mask_computed() -> None: name="gmd_schaefer100x7_mean", on="VBM_GM", ) # Test passing "on" as a keyword argument - input = {"VBM_GM": {"data": img, "meta": {}}} + input = {"VBM_GM": {"data": img, "meta": {}, "space": "MNI"}} jun_values3d_mean = marker.fit_transform(input)["VBM_GM"]["data"] assert jun_values3d_mean.ndim == 2 @@ -301,7 +301,7 @@ def test_ParcelAggregation_3D_multiple_non_overlapping(tmp_path: Path) -> None: """ # Get the testing parcellation - parcellation, labels, _ = load_parcellation("Schaefer100x7") + parcellation, labels, _, _ = load_parcellation("Schaefer100x7") assert parcellation is not None @@ -330,10 +330,18 @@ def test_ParcelAggregation_3D_multiple_non_overlapping(tmp_path: Path) -> None: nib.save(parcellation2_img, parcellation2_path) register_parcellation( - "Schaefer100x7_low", parcellation1_path, labels1, overwrite=True + name="Schaefer100x7_low", + parcellation_path=parcellation1_path, + parcels_labels=labels1, + space="MNI", + overwrite=True, ) register_parcellation( - "Schaefer100x7_high", parcellation2_path, labels2, overwrite=True + name="Schaefer100x7_high", + parcellation_path=parcellation2_path, + parcels_labels=labels2, + space="MNI", + overwrite=True, ) # Use the ParcelAggregation object on the original parcellation @@ -343,7 +351,7 @@ def test_ParcelAggregation_3D_multiple_non_overlapping(tmp_path: Path) -> None: name="gmd_schaefer100x7_mean", on="VBM_GM", ) # Test passing "on" as a keyword argument - input = {"VBM_GM": {"data": img, "meta": {}}} + input = {"VBM_GM": {"data": img, "meta": {}, "space": "MNI"}} orig_mean = marker_original.fit_transform(input)["VBM_GM"] orig_mean_data = orig_mean["data"] @@ -359,7 +367,7 @@ def test_ParcelAggregation_3D_multiple_non_overlapping(tmp_path: Path) -> None: name="gmd_schaefer100x7_mean", on="VBM_GM", ) # Test passing "on" as a keyword argument - input = {"VBM_GM": {"data": img, "meta": {}}} + input = {"VBM_GM": {"data": img, "meta": {}, "space": "MNI"}} # No warnings should be raised with warnings.catch_warnings(): @@ -387,7 +395,7 @@ def test_ParcelAggregation_3D_multiple_overlapping(tmp_path: Path) -> None: """ # Get the testing parcellation - parcellation, labels, _ = load_parcellation("Schaefer100x7") + parcellation, labels, _, _ = load_parcellation("Schaefer100x7") assert parcellation is not None @@ -418,10 +426,18 @@ def test_ParcelAggregation_3D_multiple_overlapping(tmp_path: Path) -> None: nib.save(parcellation2_img, parcellation2_path) register_parcellation( - "Schaefer100x7_low2", parcellation1_path, labels1, overwrite=True + name="Schaefer100x7_low2", + parcellation_path=parcellation1_path, + parcels_labels=labels1, + space="MNI", + overwrite=True, ) register_parcellation( - "Schaefer100x7_high2", parcellation2_path, labels2, overwrite=True + name="Schaefer100x7_high2", + parcellation_path=parcellation2_path, + parcels_labels=labels2, + space="MNI", + overwrite=True, ) # Use the ParcelAggregation object on the original parcellation @@ -431,7 +447,7 @@ def test_ParcelAggregation_3D_multiple_overlapping(tmp_path: Path) -> None: name="gmd_schaefer100x7_mean", on="VBM_GM", ) # Test passing "on" as a keyword argument - input = {"VBM_GM": {"data": img, "meta": {}}} + input = {"VBM_GM": {"data": img, "meta": {}, "space": "MNI"}} orig_mean = marker_original.fit_transform(input)["VBM_GM"] orig_mean_data = orig_mean["data"] @@ -447,7 +463,7 @@ def test_ParcelAggregation_3D_multiple_overlapping(tmp_path: Path) -> None: name="gmd_schaefer100x7_mean", on="VBM_GM", ) # Test passing "on" as a keyword argument - input = {"VBM_GM": {"data": img, "meta": {}}} + input = {"VBM_GM": {"data": img, "meta": {}, "space": "MNI"}} with pytest.warns(RuntimeWarning, match="overlapping voxels"): split_mean = marker_split.fit_transform(input)["VBM_GM"] split_mean_data = split_mean["data"] @@ -481,7 +497,7 @@ def test_ParcelAggregation_3D_multiple_duplicated_labels( """ # Get the testing parcellation - parcellation, labels, _ = load_parcellation("Schaefer100x7") + parcellation, labels, _, _ = load_parcellation("Schaefer100x7") assert parcellation is not None @@ -510,10 +526,18 @@ def test_ParcelAggregation_3D_multiple_duplicated_labels( nib.save(parcellation2_img, parcellation2_path) register_parcellation( - "Schaefer100x7_low", parcellation1_path, labels1, overwrite=True + name="Schaefer100x7_low", + parcellation_path=parcellation1_path, + parcels_labels=labels1, + space="MNI", + overwrite=True, ) register_parcellation( - "Schaefer100x7_high", parcellation2_path, labels2, overwrite=True + name="Schaefer100x7_high", + parcellation_path=parcellation2_path, + parcels_labels=labels2, + space="MNI", + overwrite=True, ) # Use the ParcelAggregation object on the original parcellation @@ -523,7 +547,7 @@ def test_ParcelAggregation_3D_multiple_duplicated_labels( name="gmd_schaefer100x7_mean", on="VBM_GM", ) # Test passing "on" as a keyword argument - input = {"VBM_GM": {"data": img, "meta": {}}} + input = {"VBM_GM": {"data": img, "meta": {}, "space": "MNI"}} orig_mean = marker_original.fit_transform(input)["VBM_GM"] orig_mean_data = orig_mean["data"] @@ -539,7 +563,7 @@ def test_ParcelAggregation_3D_multiple_duplicated_labels( name="gmd_schaefer100x7_mean", on="VBM_GM", ) # Test passing "on" as a keyword argument - input = {"VBM_GM": {"data": img, "meta": {}}} + input = {"VBM_GM": {"data": img, "meta": {}, "space": "MNI"}} with pytest.warns(RuntimeWarning, match="duplicated labels."): split_mean = marker_split.fit_transform(input)["VBM_GM"] @@ -578,7 +602,7 @@ def test_ParcelAggregation_4D_agg_time(): marker = ParcelAggregation( parcellation="Schaefer100x7", method="mean", time_method="mean" ) - input = {"BOLD": {"data": fmri_img, "meta": {}}} + input = {"BOLD": {"data": fmri_img, "meta": {}, "space": "MNI"}} jun_values4d = marker.fit_transform(input)["BOLD"]["data"] assert jun_values4d.ndim == 1 @@ -593,7 +617,7 @@ def test_ParcelAggregation_4D_agg_time(): time_method_params={"pick": [0]}, ) - input = {"BOLD": {"data": fmri_img, "meta": {}}} + input = {"BOLD": {"data": fmri_img, "meta": {}, "space": "MNI"}} jun_values4d = marker.fit_transform(input)["BOLD"]["data"] assert jun_values4d.ndim == 2 @@ -620,5 +644,11 @@ def test_ParcelAggregation_4D_agg_time(): ) with pytest.warns(RuntimeWarning, match="No time dimension to aggregate"): - input = {"BOLD": {"data": fmri_img.slicer[..., 0:1], "meta": {}}} + input = { + "BOLD": { + "data": fmri_img.slicer[..., 0:1], + "meta": {}, + "space": "MNI", + } + } marker.fit_transform(input) -- 2.52.0 From 2a7d09240bd175910113b863fee7ff3722703e42 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Wed, 27 Sep 2023 10:49:24 +0200 Subject: [PATCH 18/86] chore: fix tests affected by space compulsion change in test_sphere_aggregation.py --- .../markers/tests/test_sphere_aggregation.py | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/junifer/markers/tests/test_sphere_aggregation.py b/junifer/markers/tests/test_sphere_aggregation.py index 68ea96714..19b11c03d 100644 --- a/junifer/markers/tests/test_sphere_aggregation.py +++ b/junifer/markers/tests/test_sphere_aggregation.py @@ -37,7 +37,7 @@ def test_SphereAggregation_input_output() -> None: def test_SphereAggregation_3D() -> None: """Test SphereAggregation object on 3D images.""" # Get the testing coordinates (for nilearn) - coordinates, labels = load_coordinates(COORDS) + coordinates, _, _ = load_coordinates(COORDS) # Get the oasis VBM data oasis_dataset = datasets.fetch_oasis_vbm(n_subjects=1) @@ -52,7 +52,7 @@ def test_SphereAggregation_3D() -> None: marker = SphereAggregation( coords=COORDS, method="mean", radius=RADIUS, on="VBM_GM" ) - input = {"VBM_GM": {"data": img, "meta": {}}} + input = {"VBM_GM": {"data": img, "meta": {}, "space": "MNI"}} jun_values4d = marker.fit_transform(input)["VBM_GM"]["data"] assert jun_values4d.ndim == 2 @@ -63,7 +63,7 @@ def test_SphereAggregation_3D() -> None: def test_SphereAggregation_4D() -> None: """Test SphereAggregation object on 4D images.""" # Get the testing coordinates (for nilearn) - coordinates, _ = load_coordinates(COORDS) + coordinates, _, _ = load_coordinates(COORDS) # Get the SPM auditory data subject_data = datasets.fetch_spm_auditory() @@ -75,7 +75,7 @@ def test_SphereAggregation_4D() -> None: # Create SphereAggregation object marker = SphereAggregation(coords=COORDS, method="mean", radius=RADIUS) - input = {"BOLD": {"data": fmri_img, "meta": {}}} + input = {"BOLD": {"data": fmri_img, "meta": {}, "space": "MNI"}} jun_values4d = marker.fit_transform(input)["BOLD"]["data"] assert jun_values4d.ndim == 2 @@ -103,7 +103,7 @@ def test_SphereAggregation_storage(tmp_path: Path) -> None: "element": {"subject": "sub-01", "session": "ses-01"}, "dependencies": {"nilearn", "nibabel"}, } - input = {"VBM_GM": {"data": img, "meta": meta}} + input = {"VBM_GM": {"data": img, "meta": meta, "space": "MNI"}} marker = SphereAggregation( coords=COORDS, method="mean", radius=RADIUS, on="VBM_GM" ) @@ -122,7 +122,7 @@ def test_SphereAggregation_storage(tmp_path: Path) -> None: # Get the SPM auditory data subject_data = datasets.fetch_spm_auditory() fmri_img = concat_imgs(subject_data.func) # type: ignore - input = {"BOLD": {"data": fmri_img, "meta": meta}} + input = {"BOLD": {"data": fmri_img, "meta": meta, "space": "MNI"}} marker = SphereAggregation( coords=COORDS, method="mean", radius=RADIUS, on="BOLD" ) @@ -137,7 +137,7 @@ def test_SphereAggregation_storage(tmp_path: Path) -> None: def test_SphereAggregation_3D_mask() -> None: """Test SphereAggregation object on 3D images using mask.""" # Get the testing coordinates (for nilearn) - coordinates, _ = load_coordinates(COORDS) + coordinates, _, _ = load_coordinates(COORDS) # Get one mask mask_img, _ = load_mask("GM_prob0.2") @@ -161,7 +161,7 @@ def test_SphereAggregation_3D_mask() -> None: on="VBM_GM", masks="GM_prob0.2", ) - input = {"VBM_GM": {"data": img, "meta": {}}} + input = {"VBM_GM": {"data": img, "meta": {}, "space": "MNI"}} jun_values4d = marker.fit_transform(input)["VBM_GM"]["data"] assert jun_values4d.ndim == 2 @@ -172,7 +172,7 @@ def test_SphereAggregation_3D_mask() -> None: def test_SphereAggregation_4D_agg_time() -> None: """Test SphereAggregation object on 4D images, aggregating time.""" # Get the testing coordinates (for nilearn) - coordinates, _ = load_coordinates(COORDS) + coordinates, _, _ = load_coordinates(COORDS) # Get the SPM auditory data subject_data = datasets.fetch_spm_auditory() @@ -187,7 +187,7 @@ def test_SphereAggregation_4D_agg_time() -> None: marker = SphereAggregation( coords=COORDS, method="mean", radius=RADIUS, time_method="mean" ) - input = {"BOLD": {"data": fmri_img, "meta": {}}} + input = {"BOLD": {"data": fmri_img, "meta": {}, "space": "MNI"}} jun_values4d = marker.fit_transform(input)["BOLD"]["data"] assert jun_values4d.ndim == 1 @@ -203,7 +203,7 @@ def test_SphereAggregation_4D_agg_time() -> None: time_method_params={"pick": [0]}, ) - input = {"BOLD": {"data": fmri_img, "meta": {}}} + input = {"BOLD": {"data": fmri_img, "meta": {}, "space": "MNI"}} jun_values4d = marker.fit_transform(input)["BOLD"]["data"] assert jun_values4d.ndim == 2 @@ -232,5 +232,11 @@ def test_SphereAggregation_4D_agg_time() -> None: ) with pytest.warns(RuntimeWarning, match="No time dimension to aggregate"): - input = {"BOLD": {"data": fmri_img.slicer[..., 0:1], "meta": {}}} + input = { + "BOLD": { + "data": fmri_img.slicer[..., 0:1], + "meta": {}, + "space": "MNI", + } + } marker.fit_transform(input) -- 2.52.0 From 44f4702ceab2bd7070f4c80b0a252447451f56dd Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Wed, 27 Sep 2023 10:50:10 +0200 Subject: [PATCH 19/86] update: add coordinates space transformation via FSL FLIRT std2imgcoord --- junifer/data/coordinates.py | 63 +++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/junifer/data/coordinates.py b/junifer/data/coordinates.py index b4bbf2106..26ca2d690 100644 --- a/junifer/data/coordinates.py +++ b/junifer/data/coordinates.py @@ -4,6 +4,8 @@ # Synchon Mandal # License: AGPL +import subprocess +import tempfile import typing from pathlib import Path from typing import Any, Dict, List, Optional, Tuple, Union @@ -228,6 +230,67 @@ def get_coordinates( # Load the coordinates seeds, labels, _ = load_coordinates(name=coords) + # Transform coordinate if not in MNI space + if not target_data["space"].startswith("MNI"): + # Check for extra inputs + if extra_input is None: + raise_error( + "No extra input provided, requires `Warp` and `T1w` " + "data types in particular." + ) + + # Create tempdir + tempdir = Path(tempfile.mkdtemp()) + + # Save existing coordinates + pretransform_coordinates_path = ( + tempdir / "pretransform_coordinates.txt" + ) + np.savetxt(pretransform_coordinates_path, seeds) + + # Create a tempfile for transformed coordinates output + std2imgcoord_out_path = tempdir / "coordinates_transformed.txt" + # Set std2imgcoord command + std2imgcoord_cmd = [ + "std2imgcoord", + f"-img {target_data['reference_path'].resolve()}", + f"-warp {extra_input['Warp']['path'].resolve()}", + f"{pretransform_coordinates_path}", + f"> {std2imgcoord_out_path}", + ] + # Call std2imgcoord + std2imgcoord_cmd_str = " ".join(std2imgcoord_cmd) + logger.info( + f"std2imgcoord command to be executed: {std2imgcoord_cmd_str}" + ) + std2imgcoord_process = subprocess.run( + std2imgcoord_cmd_str, # string needed with shell=True + stdin=subprocess.DEVNULL, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + shell=True, # needed for respecting $PATH + check=False, + ) + # Delete saved coordinates file + pretransform_coordinates_path.unlink() + # Check for success or failure + if std2imgcoord_process.returncode == 0: + logger.info( + "std2imgcoord succeeded with the following output: " + f"{std2imgcoord_process.stdout}" + ) + else: + raise_error( + msg="std2imgcoord failed with the following error: " + f"{std2imgcoord_process.stdout}", + klass=RuntimeError, + ) + + # Load coordinates + seeds = np.loadtxt(std2imgcoord_out_path) + # Delete warped output file + std2imgcoord_out_path.unlink() + return seeds, labels -- 2.52.0 From 42e5999f939e34b0de3891a25e0b497c01d420f5 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Wed, 27 Sep 2023 10:51:22 +0200 Subject: [PATCH 20/86] update: add parcellation warping to other space via FSL FLIRT applywarp --- junifer/data/parcellations.py | 95 ++++++++++++++++++++++++++++++++--- 1 file changed, 89 insertions(+), 6 deletions(-) diff --git a/junifer/data/parcellations.py b/junifer/data/parcellations.py index 8e1c29c3a..d8af436fb 100644 --- a/junifer/data/parcellations.py +++ b/junifer/data/parcellations.py @@ -7,6 +7,7 @@ import io import shutil +import subprocess import tarfile import tempfile import typing @@ -226,6 +227,13 @@ def get_parcellation( list of str Parcellation labels. + Raises + ------ + RuntimeError + If parcellations are in different spaces and they need to be merged. + ValueError + If ``extra_input`` is None when ``target_data``'s space is not MNI. + """ # Get the min of the voxels sizes and use it as the resolution target_img = target_data["data"] @@ -234,8 +242,9 @@ def get_parcellation( # Load the parcellations all_parcellations = [] all_labels = [] + all_spaces = [] for name in parcellation: - img, labels, _, _ = load_parcellation( + img, labels, _, space = load_parcellation( name=name, resolution=resolution, ) @@ -248,17 +257,91 @@ def get_parcellation( ) all_parcellations.append(resampled_img) all_labels.append(labels) + all_spaces.append(space) # Avoid merging if there is only one parcellation if len(all_parcellations) == 1: resampled_parcellation_img = all_parcellations[0] labels = all_labels[0] else: - # Merge the parcellations - resampled_parcellation_img, labels = merge_parcellations( - parcellations_list=all_parcellations, - parcellations_names=parcellation, - labels_lists=all_labels, + # Merge the parcellations only if all parcellations are in the same + # space + if len(set(all_spaces)) == 1 or all( + x.startswith("MNI") for x in all_spaces + ): + resampled_parcellation_img, labels = merge_parcellations( + parcellations_list=all_parcellations, + parcellations_names=parcellation, + labels_lists=all_labels, + ) + else: + raise_error( + msg="Parcellations are in different spaces, unable to merge.", + klass=RuntimeError, + ) + + # Warp parcellation if not in MNI space + if not target_data["space"].startswith("MNI"): + # Check for extra inputs + if extra_input is None: + raise_error( + "No extra input provided, requires `Warp` and `T1w` " + "data types in particular." + ) + + # Create tempdir + tempdir = Path(tempfile.mkdtemp()) + + # Save parcellation image + prewarp_parcellation_path = tempdir / "prewarp_parcellation.nii.gz" + nib.save(resampled_parcellation_img, prewarp_parcellation_path) + + # Create a tempfile for warped output + applywarp_out_path = tempdir / "parcellation_warped.nii.gz" + # Set applywarp command + applywarp_cmd = [ + "applywarp", + "--interp=spline", + f"-i {prewarp_parcellation_path.resolve()}", + # use resampled reference + f"-r {target_data['reference_path'].resolve()}", + f"-w {extra_input['Warp']['path'].resolve()}", + f"-o {applywarp_out_path.resolve()}", + ] + # Call applywarp + applywarp_cmd_str = " ".join(applywarp_cmd) + logger.info(f"applywarp command to be executed: {applywarp_cmd_str}") + applywarp_process = subprocess.run( + applywarp_cmd_str, # string needed with shell=True + stdin=subprocess.DEVNULL, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + shell=True, # needed for respecting $PATH + check=False, + ) + # Delete saved parcellation image + prewarp_parcellation_path.unlink() + # Check for success or failure + if applywarp_process.returncode == 0: + logger.info( + "applywarp succeeded with the following output: " + f"{applywarp_process.stdout}" + ) + else: + raise_error( + msg="applywarp failed with the following error: " + f"{applywarp_process.stdout}", + klass=RuntimeError, + ) + + # Load nifti + resampled_parcellation_img = nib.load(applywarp_out_path) + # Delete warped output file + applywarp_out_path.unlink() + + # Stupid casting + resampled_parcellation_img = typing.cast( + "Nifti1Image", resampled_parcellation_img ) return resampled_parcellation_img, labels -- 2.52.0 From c7199069e1dbe0d46757840e1d1e13e6c06d1331 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Wed, 27 Sep 2023 10:52:11 +0200 Subject: [PATCH 21/86] chore: add tests for multi parcellation fetch in incorrect space via get_parcellation() --- junifer/data/tests/test_parcellations.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/junifer/data/tests/test_parcellations.py b/junifer/data/tests/test_parcellations.py index a6e9277c0..a67ee75c2 100644 --- a/junifer/data/tests/test_parcellations.py +++ b/junifer/data/tests/test_parcellations.py @@ -1179,3 +1179,21 @@ def test_get_parcellation_multi_same_space() -> None: merged_resampled_parcellations.get_fdata(), ) assert tailored_labels == merged_labels + + +def test_get_parcellation_multi_different_space() -> None: + """Test tailored multi parcellation fetch in different space.""" + reader = DefaultDataReader() + with OasisVBMTestingDataGrabber() as dg: + element = dg["sub-01"] + element_data = reader.fit_transform(element) + vbm_gm = element_data["VBM_GM"] + # Get tailored parcellation + with pytest.raises(RuntimeError, match="unable to merge."): + get_parcellation( + parcellation=[ + "Schaefer100x7", + "SUITxSUIT", + ], + target_data=vbm_gm, + ) -- 2.52.0 From b5da8b785de88a44edb8125300132f8a2928334f Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Wed, 27 Sep 2023 10:53:06 +0200 Subject: [PATCH 22/86] update: add mask warping to other space via FSL FLIRT applywarp --- junifer/data/masks.py | 115 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 111 insertions(+), 4 deletions(-) diff --git a/junifer/data/masks.py b/junifer/data/masks.py index f4ad92d24..2db0a9b5f 100644 --- a/junifer/data/masks.py +++ b/junifer/data/masks.py @@ -3,6 +3,8 @@ # Authors: Federico Raimondo # License: AGPL +import subprocess +import tempfile import typing from pathlib import Path from typing import ( @@ -163,7 +165,7 @@ def list_masks() -> List[str]: return sorted(_available_masks.keys()) -def get_mask( +def get_mask( # noqa: C901 masks: Union[str, Dict, List[Union[Dict, str]]], target_data: Dict[str, Any], extra_input: Optional[Dict[str, Any]] = None, @@ -186,6 +188,21 @@ def get_mask( ------- Nifti1Image The mask image. + + Raises + ------ + RuntimeError + If masks are in different spaces and they need to be intersected / + unionized. + ValueError + If extra key is provided in addition to mask name in ``masks`` or + if no mask is provided or if ``masks = "inherit"`` but ``extra_input`` + is None or ``mask_item`` is None or ``mask_items``'s value is not in + ``extra_input`` or if callable parameters are passed to non-callable + mask or if parameters are passed to + :func:`nilearn.masking.intersect_masks` when there is only one mask or + if ``extra_input`` is None when ``target_data``'s space is not MNI. + """ # Get the min of the voxels sizes and use it as the resolution target_img = target_data["data"] @@ -224,6 +241,7 @@ def get_mask( raise_error("No mask was passed. At least one mask is required.") # Get all the masks all_masks = [] + all_spaces = [] for t_mask in true_masks: if isinstance(t_mask, dict): mask_name = next(iter(t_mask.keys())) @@ -232,46 +250,76 @@ def get_mask( mask_name = t_mask mask_params = None + # If mask is being inherited from previous steps like preprocessing if mask_name == "inherit": + # Requires extra input to be passed if extra_input is None: raise_error( "Cannot inherit mask from another data item " "because no extra data was passed." ) + # Missing inherited mask item if inherited_mask_item is None: raise_error( "Cannot inherit mask from another data item " "because no mask item was specified " "(missing `mask_item` key in the data object)." ) + # Missing inherited mask item in extra input if inherited_mask_item not in extra_input: raise_error( "Cannot inherit mask from another data item " f"because the item ({inherited_mask_item}) does not exist." ) mask_img = extra_input[inherited_mask_item]["data"] + # Starting with new mask else: - mask_object, _, _ = load_mask( + # Load mask + mask_object, _, mask_space = load_mask( mask_name, path_only=False, resolution=resolution ) + # If mask is callable like from nilearn if callable(mask_object): if mask_params is None: mask_params = {} mask_img = mask_object(target_img, **mask_params) - else: # Mask is a Nifti1Image + # Mask is a Nifti1Image + else: + # Mask params provided if mask_params is not None: + # Unused params raise_error( "Cannot pass callable params to a non-callable mask." ) + # Resample mask to target image mask_img = resample_to_img( mask_object, target_img, interpolation="nearest", copy=True, ) + all_spaces.append(mask_space) all_masks.append(mask_img) + + # Multiple masks, need intersection / union if len(all_masks) > 1: - mask_img = intersect_masks(all_masks, **intersect_params) + # Intersect / union of masks only if all masks are in the same space + if len(set(all_spaces)) == 1 or all( + x.startswith("MNI") for x in all_spaces + ): + mask_img = intersect_masks(all_masks, **intersect_params) + else: + # Check for inherited masks with target image in MNI space + if "inherit" in all_spaces and target_data["space"].startswith( + "MNI" + ): + mask_img = intersect_masks(all_masks, **intersect_params) + else: + raise_error( + msg="Masks are in different spaces, unable to merge.", + klass=RuntimeError, + ) + # Single mask else: if len(intersect_params) > 0: # Yes, I'm this strict! @@ -281,6 +329,65 @@ def get_mask( ) mask_img = all_masks[0] + # Warp mask if not in MNI space + if not target_data["space"].startswith("MNI"): + # Check for extra inputs + if extra_input is None: + raise_error( + "No extra input provided, requires `Warp` and `T1w` " + "data types in particular." + ) + + # Create tempdir + tempdir = Path(tempfile.mkdtemp()) + + # Save mask image + prewarp_mask_path = tempdir / "prewarp_mask.nii.gz" + nib.save(mask_img, prewarp_mask_path) + + # Create a tempfile for warped output + applywarp_out_path = tempdir / "mask_warped.nii.gz" + # Set applywarp command + applywarp_cmd = [ + "applywarp", + "--interp=spline", + f"-i {prewarp_mask_path.resolve()}", + # use resampled reference + f"-r {target_data['reference_path'].resolve()}", + f"-w {extra_input['Warp']['path'].resolve()}", + f"-o {applywarp_out_path.resolve()}", + ] + # Call applywarp + applywarp_cmd_str = " ".join(applywarp_cmd) + logger.info(f"applywarp command to be executed: {applywarp_cmd_str}") + applywarp_process = subprocess.run( + applywarp_cmd_str, # string needed with shell=True + stdin=subprocess.DEVNULL, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + shell=True, # needed for respecting $PATH + check=False, + ) + # Delete saved mask image + prewarp_mask_path.unlink() + # Check for success or failure + if applywarp_process.returncode == 0: + logger.info( + "applywarp succeeded with the following output: " + f"{applywarp_process.stdout}" + ) + else: + raise_error( + msg="applywarp failed with the following error: " + f"{applywarp_process.stdout}", + klass=RuntimeError, + ) + + # Load nifti + mask_img = nib.load(applywarp_out_path) + # Delete warped output file + applywarp_out_path.unlink() + # Type-cast to remove errors mask_img = typing.cast("Nifti1Image", mask_img) return mask_img -- 2.52.0 From 7f423ee7858d2d373e40ffdd234480a86fdf907a Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Wed, 27 Sep 2023 10:53:24 +0200 Subject: [PATCH 23/86] chore: add tests for multi mask fetch in incorrect space via get_mask() --- junifer/data/tests/test_masks.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/junifer/data/tests/test_masks.py b/junifer/data/tests/test_masks.py index d7b027860..d1c2f2abf 100644 --- a/junifer/data/tests/test_masks.py +++ b/junifer/data/tests/test_masks.py @@ -484,3 +484,19 @@ def test_get_mask_multiple( expected = intersect_masks(mask_imgs, **params) assert_array_equal(computed.get_fdata(), expected.get_fdata()) + + +def test_get_mask_multiple_incorrect_space() -> None: + """Test incorrect space error for getting multiple masks.""" + reader = DefaultDataReader() + with SPMAuditoryTestingDataGrabber() as dg: + input = dg["sub001"] + input = reader.fit_transform(input) + # Edit BOLD space to trigger error + input["BOLD"]["space"] = "SUIT" + + with pytest.raises(RuntimeError, match="unable to merge."): + get_mask( + masks=["GM_prob0.2", "compute_brain_mask"], + target_data=input["BOLD"], + ) -- 2.52.0 From 0d74a27d74781861826f97226b4bc6dc26509352 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Wed, 27 Sep 2023 11:04:56 +0200 Subject: [PATCH 24/86] docs: add information about space for extending parcellations --- docs/extending/parcellations.rst | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/docs/extending/parcellations.rst b/docs/extending/parcellations.rst index 888022097..b059aa781 100644 --- a/docs/extending/parcellations.rst +++ b/docs/extending/parcellations.rst @@ -20,9 +20,9 @@ the easy-to-use :func:`.register_parcellation` function to do just that. Let's try to understand the API reference and then use this function to register our own parcellation. -From the API reference, we can see that it has 3 positional arguments -(``name``, ``parcellation_path``, and ``parcels_labels``) as well as one -optional keyword argument (``overwrite``). +From the API reference, we can see that it has 4 positional arguments +(``name``, ``parcellation_path``, ``parcels_labels`` and ``space``) as well as +one optional keyword argument (``overwrite``). The ``name`` of the parcellation is up to you and will be the name that junifer will use to refer to this particular parcellation. You can think of this as @@ -40,7 +40,7 @@ this parcellation should be indicated by 0, and the labels of ROIs should go from 1 to N (where N is the total number of ROIs in your parcellation). Now, we nearly have everything we need. -Lastly, we also want to make sure that we can associate each integer label with +We also want to make sure that we can associate each integer label with a human readable name (i.e. the name for each ROI). This serves naming the features that parcellation-based markers produce in an unambiguous way, such that a user can easily identify which ROIs were used to produce a specific @@ -51,6 +51,9 @@ list, the label at the i-th position indicates the i-th integer label (i.e. the first label in this list corresponds to the first integer label in the parcellation and so on). +Lastly, we specify the ``space`` that the parcellation is in, for example, +``"MNI"`` or ``"Native"`` (scanner-native space). + Step 1: Prepare code to register a parcellation ----------------------------------------------- @@ -78,7 +81,8 @@ a simple example could look like this: register_parcellation( name="my_custom_parcellation", parcellation_path=path_to_parcellation, - parcels_labels=my_labels + parcels_labels=my_labels, + space="MNI" ) We can run this code and it seems to work, however, how can we actually -- 2.52.0 From 836008e86f952fff2a03c0d1f1467947da52065c Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Wed, 27 Sep 2023 11:05:26 +0200 Subject: [PATCH 25/86] docs: add information about space for extending coordinates --- docs/extending/coordinates.rst | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/docs/extending/coordinates.rst b/docs/extending/coordinates.rst index 7f1580ee4..e9ca8b38a 100644 --- a/docs/extending/coordinates.rst +++ b/docs/extending/coordinates.rst @@ -20,8 +20,8 @@ example if you came up with your own set of coordinates), then junifer provides an easy way for you to register them using the :func:`.register_coordinates` function, so you can use your own set of coordinates within a junifer pipeline. -From the API reference, we can see that it has 3 positional arguments -(``name``, ``coordinates``, and ``voi_names``) as well as one +From the API reference, we can see that it has 4 positional arguments +(``name``, ``coordinates``, ``voi_names`` and ``space``) as well as one optional keyword argument (``overwrite``). The ``name`` argument takes a string indicating the name you want to give to @@ -40,12 +40,15 @@ belong to this set. Note, that junifer (as of yet) only works in MNI space, and so therefore these coordinates should always be real-world coordinates of the MNI space. -Lastly, the ``voi_names`` argument takes a list of strings +The ``voi_names`` argument takes a list of strings indicating the names of each coordinate (i.e. volume-of-interest) in the ``coordinates`` array. Therefore, the length of this list should correspond to the number of rows in the coordinates array. Now, we know everything we need to know to register a set of coordinates. +Lastly, we specify the ``space`` that the coordinates are in, for example, +``"MNI"`` or ``"Native"`` (scanner-native space). + Step 1: Prepare code to register a set of coordinates ----------------------------------------------------- @@ -90,7 +93,8 @@ simply use this to register our coordinates: register_coordinates( name="DMNCustom", coordinates=dmn_coords, - voi_names=voi_names + voi_names=voi_names, + space="MNI" ) Now, when we run this script, junifer registers these coordinates and we can -- 2.52.0 From 3356830fae8d054114fe57717bdf02ca8d8e3281 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Wed, 27 Sep 2023 11:05:38 +0200 Subject: [PATCH 26/86] docs: add information about space for extending masks --- docs/extending/masks.rst | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/extending/masks.rst b/docs/extending/masks.rst index 1950941eb..02657b098 100644 --- a/docs/extending/masks.rst +++ b/docs/extending/masks.rst @@ -15,9 +15,9 @@ learn how to use your own masks. The principle is fairly simple and quite similar to :ref:`adding_parcellations` and :ref:`adding_coordinates`. junifer provides a :func:`.register_mask` -function that lets you register your own custom masks. It consists of two -positional arguments (``name`` and ``mask_path``) and one optional keyword -argument (``overwrite``). +function that lets you register your own custom masks. It consists of three +positional arguments (``name``, ``mask_path`` and ``space``) and one optional +keyword argument (``overwrite``). The ``name`` argument is a string indicating the name of the mask. This name is used to refer to that mask in junifer internally in order to obtain the @@ -29,6 +29,9 @@ The ``mask_path`` should contain the path to a valid NIfTI image with binary voxel values (i.e. 0 or 1). This data can then be used by junifer to mask other MR images. +Lastly, we specify the ``space`` that the coordinates are in, for example, +``"MNI"`` or ``"Native"`` (scanner-native space). + Step 1: Prepare code to register a mask --------------------------------------- @@ -46,7 +49,7 @@ look as follows: # on your system: mask_path = Path("..") / ".." / "my_custom_mask.nii.gz" - register_mask(name="my_custom_mask", mask_path=mask_path) + register_mask(name="my_custom_mask", mask_path=mask_path, space="Native") Simple, right? Now we just have to configure a YAML file to register this mask so we can use it for :ref:`codeless configuration of junifer `. -- 2.52.0 From 30867f5d538fb64d970ac62f618bf9a4d95e3b01 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Wed, 27 Sep 2023 11:20:02 +0200 Subject: [PATCH 27/86] docs: adapt examples/run_compute_parcel_mean.py to allow for space --- examples/run_compute_parcel_mean.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/examples/run_compute_parcel_mean.py b/examples/run_compute_parcel_mean.py index d4d00f046..657b128c1 100644 --- a/examples/run_compute_parcel_mean.py +++ b/examples/run_compute_parcel_mean.py @@ -43,8 +43,12 @@ marker = ParcelAggregation(parcellation="Schaefer100x7", method="mean") ############################################################################### # Prepare the input input = { - "BOLD": {"data": fmri_img, "meta": {"element": "subject1"}}, - "VBM_GM": {"data": vbm_img, "meta": {"element": "subject1"}}, + "BOLD": { + "data": fmri_img, "meta": {"element": "subject1"}, "space": "MNI" + }, + "VBM_GM": { + "data": vbm_img, "meta": {"element": "subject1"}, "space": "MNI" + }, } ############################################################################### -- 2.52.0 From 51a798c61370a9cdcb03f7a6dff6a0cb38ae8473 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Wed, 27 Sep 2023 12:18:36 +0200 Subject: [PATCH 28/86] chore: improve error messages for get_*() functions when space is not MNI --- junifer/data/coordinates.py | 3 ++- junifer/data/masks.py | 3 ++- junifer/data/parcellations.py | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/junifer/data/coordinates.py b/junifer/data/coordinates.py index 26ca2d690..7cc093a70 100644 --- a/junifer/data/coordinates.py +++ b/junifer/data/coordinates.py @@ -236,7 +236,8 @@ def get_coordinates( if extra_input is None: raise_error( "No extra input provided, requires `Warp` and `T1w` " - "data types in particular." + "data types in particular for transformation to " + f"{target_data['space']} space for further computation." ) # Create tempdir diff --git a/junifer/data/masks.py b/junifer/data/masks.py index 2db0a9b5f..69954d493 100644 --- a/junifer/data/masks.py +++ b/junifer/data/masks.py @@ -335,7 +335,8 @@ def get_mask( # noqa: C901 if extra_input is None: raise_error( "No extra input provided, requires `Warp` and `T1w` " - "data types in particular." + "data types in particular for transformation to " + f"{target_data['space']} space for further computation." ) # Create tempdir diff --git a/junifer/data/parcellations.py b/junifer/data/parcellations.py index d8af436fb..2425350e6 100644 --- a/junifer/data/parcellations.py +++ b/junifer/data/parcellations.py @@ -286,7 +286,8 @@ def get_parcellation( if extra_input is None: raise_error( "No extra input provided, requires `Warp` and `T1w` " - "data types in particular." + "data types in particular for transformation to " + f"{target_data['space']} space for further computation." ) # Create tempdir -- 2.52.0 From de5b723b3baf79f0a19d9a7951f06f71f3837be4 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Tue, 10 Oct 2023 11:24:52 +0200 Subject: [PATCH 29/86] fix: update space for _fetch_icbm_brain_gm_mask --- junifer/data/masks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/junifer/data/masks.py b/junifer/data/masks.py index 69954d493..23abccba2 100644 --- a/junifer/data/masks.py +++ b/junifer/data/masks.py @@ -99,7 +99,7 @@ _available_masks: Dict[str, Dict[str, Any]] = { "fetch_icbm152_brain_gm_mask": { "family": "Callable", "func": _fetch_icbm152_brain_gm_mask, - "space": "inherit", + "space": "MNI152NLin2009aAsym", }, } -- 2.52.0 From 55fa62b04ec24ccc72867bcd4201285f0676f86d Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Fri, 13 Oct 2023 10:38:51 +0200 Subject: [PATCH 30/86] update: use WorkDirManager in data.coordinates --- junifer/data/coordinates.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/junifer/data/coordinates.py b/junifer/data/coordinates.py index 7cc093a70..efd5e48b4 100644 --- a/junifer/data/coordinates.py +++ b/junifer/data/coordinates.py @@ -5,7 +5,6 @@ # License: AGPL import subprocess -import tempfile import typing from pathlib import Path from typing import Any, Dict, List, Optional, Tuple, Union @@ -14,6 +13,7 @@ import numpy as np import pandas as pd from numpy.typing import ArrayLike +from ..pipeline import WorkDirManager from ..utils.logging import logger, raise_error, warn_with_log @@ -241,7 +241,7 @@ def get_coordinates( ) # Create tempdir - tempdir = Path(tempfile.mkdtemp()) + tempdir = WorkDirManager().get_tempdir(prefix="coordinates") # Save existing coordinates pretransform_coordinates_path = ( @@ -289,8 +289,9 @@ def get_coordinates( # Load coordinates seeds = np.loadtxt(std2imgcoord_out_path) - # Delete warped output file - std2imgcoord_out_path.unlink() + + # Delete tempdir + WorkDirManager().delete_tempdir(tempdir) return seeds, labels -- 2.52.0 From 3f024a6a59203d7146f7fa3d433b8cd9e8385401 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Fri, 13 Oct 2023 10:43:12 +0200 Subject: [PATCH 31/86] update: use WorkDirManager in data.masks --- junifer/data/masks.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/junifer/data/masks.py b/junifer/data/masks.py index 23abccba2..371393fa6 100644 --- a/junifer/data/masks.py +++ b/junifer/data/masks.py @@ -4,7 +4,6 @@ # License: AGPL import subprocess -import tempfile import typing from pathlib import Path from typing import ( @@ -29,6 +28,7 @@ from nilearn.masking import ( intersect_masks, ) +from ..pipeline import WorkDirManager from ..utils.logging import logger, raise_error from .utils import closest_resolution @@ -340,7 +340,7 @@ def get_mask( # noqa: C901 ) # Create tempdir - tempdir = Path(tempfile.mkdtemp()) + tempdir = WorkDirManager().get_tempdir(prefix="masks") # Save mask image prewarp_mask_path = tempdir / "prewarp_mask.nii.gz" @@ -369,8 +369,6 @@ def get_mask( # noqa: C901 shell=True, # needed for respecting $PATH check=False, ) - # Delete saved mask image - prewarp_mask_path.unlink() # Check for success or failure if applywarp_process.returncode == 0: logger.info( @@ -386,8 +384,9 @@ def get_mask( # noqa: C901 # Load nifti mask_img = nib.load(applywarp_out_path) - # Delete warped output file - applywarp_out_path.unlink() + + # Delete tempdir + WorkDirManager().delete_tempdir(tempdir) # Type-cast to remove errors mask_img = typing.cast("Nifti1Image", mask_img) -- 2.52.0 From 9f5b500d7b9601a23a62ffcb862af9db1d58d63d Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Fri, 13 Oct 2023 10:47:32 +0200 Subject: [PATCH 32/86] update: use WorkDirManager in data.parcellations --- junifer/data/parcellations.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/junifer/data/parcellations.py b/junifer/data/parcellations.py index 2425350e6..19d234a63 100644 --- a/junifer/data/parcellations.py +++ b/junifer/data/parcellations.py @@ -22,6 +22,7 @@ import requests from nilearn import datasets, image from requests.exceptions import ConnectionError, HTTPError, ReadTimeout +from ..pipeline import WorkDirManager from ..utils.logging import logger, raise_error, warn_with_log from .utils import closest_resolution @@ -291,7 +292,7 @@ def get_parcellation( ) # Create tempdir - tempdir = Path(tempfile.mkdtemp()) + tempdir = WorkDirManager().get_tempdir(prefix="parcellations") # Save parcellation image prewarp_parcellation_path = tempdir / "prewarp_parcellation.nii.gz" @@ -320,8 +321,6 @@ def get_parcellation( shell=True, # needed for respecting $PATH check=False, ) - # Delete saved parcellation image - prewarp_parcellation_path.unlink() # Check for success or failure if applywarp_process.returncode == 0: logger.info( @@ -337,8 +336,9 @@ def get_parcellation( # Load nifti resampled_parcellation_img = nib.load(applywarp_out_path) - # Delete warped output file - applywarp_out_path.unlink() + + # Delete tempdir + WorkDirManager().delete_tempdir(tempdir) # Stupid casting resampled_parcellation_img = typing.cast( -- 2.52.0 From 74505092092d1269f43f9625517c72446bee27c4 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Mon, 16 Oct 2023 10:49:14 +0200 Subject: [PATCH 33/86] update: add support for cleaning up element-specific tempdirs in MarkerCollection --- junifer/markers/collection.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/junifer/markers/collection.py b/junifer/markers/collection.py index bf40c8bb2..7fe39dea1 100644 --- a/junifer/markers/collection.py +++ b/junifer/markers/collection.py @@ -9,7 +9,7 @@ from typing import TYPE_CHECKING, Dict, List, Optional from ..datareader.default import DefaultDataReader from ..markers.base import BaseMarker -from ..pipeline import PipelineStepMixin +from ..pipeline import PipelineStepMixin, WorkDirManager from ..preprocess.base import BasePreprocessor from ..storage.base import BaseFeatureStorage from ..utils import logger @@ -98,6 +98,13 @@ class MarkerCollection: out[marker.name] = m_value logger.info("Marker collection fitting done") + # Check to see if the pipeline uses element-specific tempdir; + # if it does, remove it + if WorkDirManager()._elementdir is not None: + WorkDirManager().delete_element_tempdir( + WorkDirManager().elementdir + ) + return None if self._storage else out def validate(self, datagrabber: "BaseDataGrabber") -> None: -- 2.52.0 From 996e3cf4a286ad0b1d98b8634273a36465adfc8b Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Mon, 16 Oct 2023 11:42:03 +0200 Subject: [PATCH 34/86] update: reset the element-specific tempdir in MarkerCollection --- junifer/markers/collection.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/junifer/markers/collection.py b/junifer/markers/collection.py index 7fe39dea1..1a8cb38d6 100644 --- a/junifer/markers/collection.py +++ b/junifer/markers/collection.py @@ -104,6 +104,8 @@ class MarkerCollection: WorkDirManager().delete_element_tempdir( WorkDirManager().elementdir ) + # Reset the element-specific tempdir + WorkDirManager()._elementdir = None return None if self._storage else out -- 2.52.0 From 982c1ce4254f4b7efec243dbb1396e9d4799786e Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Mon, 16 Oct 2023 16:56:05 +0200 Subject: [PATCH 35/86] update: use WorkDirManager.cleanup_elementdir() in MarkerCollection --- junifer/markers/collection.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/junifer/markers/collection.py b/junifer/markers/collection.py index 1a8cb38d6..01c9194a8 100644 --- a/junifer/markers/collection.py +++ b/junifer/markers/collection.py @@ -98,14 +98,8 @@ class MarkerCollection: out[marker.name] = m_value logger.info("Marker collection fitting done") - # Check to see if the pipeline uses element-specific tempdir; - # if it does, remove it - if WorkDirManager()._elementdir is not None: - WorkDirManager().delete_element_tempdir( - WorkDirManager().elementdir - ) - # Reset the element-specific tempdir - WorkDirManager()._elementdir = None + # Cleanup element directory + WorkDirManager().cleanup_elementdir() return None if self._storage else out -- 2.52.0 From 429d104c5d74b9179832ceff3dbb2d599903e770 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Mon, 11 Sep 2023 10:45:05 +0200 Subject: [PATCH 36/86] chore: improve commentary in data.masks --- junifer/data/masks.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/junifer/data/masks.py b/junifer/data/masks.py index 371393fa6..76c980e09 100644 --- a/junifer/data/masks.py +++ b/junifer/data/masks.py @@ -36,7 +36,7 @@ from .utils import closest_resolution if TYPE_CHECKING: from nibabel import Nifti1Image -# Path to the VOIs +# Path to the masks _masks_path = Path(__file__).parent / "masks" @@ -427,15 +427,18 @@ def load_mask( If the ``name`` is invalid of if the mask family is invalid. """ - mask_img = None + # Check for valid mask name if name not in _available_masks: raise_error( f"Mask {name} not found. " f"Valid options are: {list_masks()}" ) + # Copy mask definition to avoid edits in original object mask_definition = _available_masks[name].copy() t_family = mask_definition.pop("family") + # Check if the mask family is custom or built-in + mask_img = None if t_family == "CustomUserMask": mask_fname = Path(mask_definition["path"]) elif t_family == "Vickery-Patil": @@ -494,6 +497,8 @@ def _load_vickery_patil_mask( mask_fname = "GMprob0.2_cortex_3mm_NA_rm.nii.gz" else: raise_error(f"Cannot find a Vickery-Patil mask called {name}") + + # Set path for masks mask_fname = _masks_path / "vickery-patil" / mask_fname return mask_fname -- 2.52.0 From a99b3a21824cc7682589b97e7f3a9fed27a15082 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Mon, 11 Sep 2023 10:46:59 +0200 Subject: [PATCH 37/86] chore: improve docstrings in data.masks --- junifer/data/masks.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/junifer/data/masks.py b/junifer/data/masks.py index 76c980e09..9ad3fb2de 100644 --- a/junifer/data/masks.py +++ b/junifer/data/masks.py @@ -58,6 +58,7 @@ def _fetch_icbm152_brain_gm_mask( ------- nibabel.Nifti1Image The resampled mask. + """ mask = fetch_icbm152_brain_gm_mask(**kwargs) mask = resample_to_img( @@ -129,6 +130,7 @@ def register_mask( 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: @@ -161,6 +163,7 @@ def list_masks() -> List[str]: ------- list of str A list with all available masks names. + """ return sorted(_available_masks.keys()) @@ -403,7 +406,8 @@ def load_mask( Parameters ---------- name : str - The name of the mask. + The name of the mask. Check valid options by calling + :func:`.list_masks`. 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 @@ -467,7 +471,7 @@ def _load_vickery_patil_mask( Parameters ---------- - name : str + name : {'GM_prob0.2', 'GM_prob0.2_cortex'} The name of the mask. resolution : float, optional The desired resolution of the mask to load. If it is not @@ -479,6 +483,13 @@ def _load_vickery_patil_mask( ------- pathlib.Path File path to the mask image. + + Raises + ------ + ValueError + If ``name`` is invalid or if ``resolution`` is invalid for + ``name = 'GM_prob0.2'``. + """ if name == "GM_prob0.2": available_resolutions = [1.5, 3.0] -- 2.52.0 From 1f322b36c1ff3aaffee198fb29cfd414ca6dc919 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Wed, 20 Sep 2023 14:26:04 +0200 Subject: [PATCH 38/86] chore: improve commentary in BaseMarker --- junifer/markers/base.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/junifer/markers/base.py b/junifer/markers/base.py index 87553273e..6a652d200 100644 --- a/junifer/markers/base.py +++ b/junifer/markers/base.py @@ -34,12 +34,15 @@ class BaseMarker(ABC, PipelineStepMixin, UpdateMetaMixin): on: Optional[Union[List[str], str]] = None, name: Optional[str] = None, ) -> None: + # Use all data types if not provided if on is None: on = self.get_valid_inputs() + # Convert data types to list if not isinstance(on, list): on = [on] + # Set default name if not provided self.name = self.__class__.__name__ if name is None else name - + # Check if required inputs are found if any(x not in self.get_valid_inputs() for x in on): wrong_on = [x for x in on if x not in self.get_valid_inputs()] raise ValueError(f"{self.name} cannot be computed on {wrong_on}") -- 2.52.0 From e4dd7da4c66fb522767637100be3508a4994f68f Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Wed, 20 Sep 2023 14:24:21 +0200 Subject: [PATCH 39/86] chore: improve docstring for PatternDataGrabber --- junifer/datagrabber/pattern.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/junifer/datagrabber/pattern.py b/junifer/datagrabber/pattern.py index eba2ebd66..260374038 100644 --- a/junifer/datagrabber/pattern.py +++ b/junifer/datagrabber/pattern.py @@ -42,6 +42,11 @@ class PatternDataGrabber(BaseDataGrabber): confounds_format : {"fmriprep", "adhoc"} or None, optional The format of the confounds for the dataset (default None). + Raises + ------ + ValueError + If ``confounds_format`` is invalid. + """ def __init__( -- 2.52.0 From df84c363c3596b69b0a0ed199267160d538d5b70 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Wed, 20 Sep 2023 14:24:01 +0200 Subject: [PATCH 40/86] chore: improve commentary in PatternDataGrabber --- junifer/datagrabber/pattern.py | 1 + 1 file changed, 1 insertion(+) diff --git a/junifer/datagrabber/pattern.py b/junifer/datagrabber/pattern.py index 260374038..7c60d476a 100644 --- a/junifer/datagrabber/pattern.py +++ b/junifer/datagrabber/pattern.py @@ -60,6 +60,7 @@ class PatternDataGrabber(BaseDataGrabber): # Validate patterns validate_patterns(types=types, patterns=patterns) + # Convert replacements to list if not already if not isinstance(replacements, list): replacements = [replacements] # Validate replacements -- 2.52.0 From 15abb52300093ece500eca0bc83bf42a4d55a937 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Wed, 20 Sep 2023 14:22:54 +0200 Subject: [PATCH 41/86] chore: improve commentary in BaseDataGrabber --- junifer/datagrabber/base.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/junifer/datagrabber/base.py b/junifer/datagrabber/base.py index a34db6bf2..76384d48f 100644 --- a/junifer/datagrabber/base.py +++ b/junifer/datagrabber/base.py @@ -74,12 +74,16 @@ class BaseDataGrabber(ABC, UpdateMetaMixin): """ logger.info(f"Getting element {element}") + # Convert element to tuple if not already if not isinstance(element, tuple): element = (element,) - named_element = dict(zip(self.get_element_keys(), element)) + # Zip through element keys and actual values to construct element + # access dictionary + named_element: Dict = dict(zip(self.get_element_keys(), element)) logger.debug(f"Named element: {named_element}") + # Fetch element out = self.get_item(**named_element) - + # Update metadata for _, t_val in out.items(): self.update_meta(t_val, "datagrabber") t_val["meta"]["element"] = named_element -- 2.52.0 From 0db133ee4db716fad87cc5c0195205d98c31122f Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Wed, 20 Sep 2023 14:21:10 +0200 Subject: [PATCH 42/86] chore: improve docstring for DefaultDataReader --- junifer/datareader/default.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/junifer/datareader/default.py b/junifer/datareader/default.py index 2956e3aec..86708e7d3 100644 --- a/junifer/datareader/default.py +++ b/junifer/datareader/default.py @@ -91,6 +91,11 @@ class DefaultDataReader(PipelineStepMixin, UpdateMetaMixin): The processed output as dictionary. The "data" key is added to each data type dictionary. + Warns + ----- + RuntimeWarning + If input data type has no key called ``"path"``. + """ # For each type of data, try to read it out = input.copy() -- 2.52.0 From 06079d98ab28625bdf5673e7265e2c0e7b9de45d Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Wed, 20 Sep 2023 14:20:47 +0200 Subject: [PATCH 43/86] chore: improve commentary in DefaultDataReader --- junifer/datareader/default.py | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/junifer/datareader/default.py b/junifer/datareader/default.py index 86708e7d3..c2a486e93 100644 --- a/junifer/datareader/default.py +++ b/junifer/datareader/default.py @@ -97,41 +97,59 @@ class DefaultDataReader(PipelineStepMixin, UpdateMetaMixin): If input data type has no key called ``"path"``. """ - # For each type of data, try to read it + # Copy input to not modify the original out = input.copy() + # Set default extra parameters if params is None: params = {} + # For each type of data, try to read it for type_ in input.keys(): + # Check for malformed datagrabber specification if "path" not in input[type_]: warn_with_log( f"Input type {type_} does not provide a path. Skipping." ) continue + + # Retrieve actual path t_path = input[type_]["path"] + # Retrieve loading params for the data type t_params = params.get(type_, {}) - # Convert to Path if datareader is not well done + # Convert str to Path if not isinstance(t_path, Path): t_path = Path(t_path) out[type_]["path"] = t_path - logger.info(f"Reading {type_} from {t_path.as_posix()}") - fread = None + logger.info(f"Reading {type_} from {t_path.as_posix()}") + # Initialize variable for file data + fread = None + # Lowercase path fname = t_path.name.lower() + # Loop through extensions to find the correct one for ext, ftype in _extensions.items(): if fname.endswith(ext): logger.info(f"{type_} is type {ftype}") + # Retrieve reader function reader_func = _readers[ftype]["func"] + # Retrieve reader function params reader_params = _readers[ftype]["params"] + # Update reader function params if reader_params is not None: t_params.update(reader_params) logger.debug(f"Calling {reader_func} with {t_params}") + # Read data fread = reader_func(t_path, **t_params) break + # If no file data is found due to unknown extension if fread is None: logger.info( f"Unknown file type {t_path.as_posix()}, skipping reading" ) + + # Set file data for output out[type_]["data"] = fread + # Update metadata for step self.update_meta(out[type_], "datareader") + return out -- 2.52.0 From 0ff5f810bcd3022427ca8cc97a3ec8f1fd2b54a0 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Wed, 20 Sep 2023 13:19:49 +0200 Subject: [PATCH 44/86] chore: improve docstrings in data.masks --- junifer/data/masks.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/junifer/data/masks.py b/junifer/data/masks.py index 9ad3fb2de..3cfdf3188 100644 --- a/junifer/data/masks.py +++ b/junifer/data/masks.py @@ -471,7 +471,7 @@ def _load_vickery_patil_mask( Parameters ---------- - name : {'GM_prob0.2', 'GM_prob0.2_cortex'} + name : {"GM_prob0.2", "GM_prob0.2_cortex"} The name of the mask. resolution : float, optional The desired resolution of the mask to load. If it is not @@ -488,7 +488,7 @@ def _load_vickery_patil_mask( ------ ValueError If ``name`` is invalid or if ``resolution`` is invalid for - ``name = 'GM_prob0.2'``. + ``name = "GM_prob0.2"``. """ if name == "GM_prob0.2": -- 2.52.0 From dae7e721550928b314ca6933868a89c0c91a28cf Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Mon, 11 Sep 2023 10:47:37 +0200 Subject: [PATCH 45/86] chore: improve commentary in data.masks --- junifer/data/masks.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/junifer/data/masks.py b/junifer/data/masks.py index 3cfdf3188..12c96edfe 100644 --- a/junifer/data/masks.py +++ b/junifer/data/masks.py @@ -453,9 +453,11 @@ def load_mask( else: raise_error(f"I don't know about the {t_family} mask family.") + # Load mask if mask_fname is not None: logger.info(f"Loading mask {mask_fname.absolute()}") if path_only is False: + # Load via nibabel mask_img = nib.load(mask_fname) # Type-cast to remove error -- 2.52.0 From c75be1f5119f067b4614cbb29b0d346a6e88711c Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Mon, 11 Sep 2023 10:47:23 +0200 Subject: [PATCH 46/86] chore: improve error messages in data.masks --- junifer/data/masks.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/junifer/data/masks.py b/junifer/data/masks.py index 12c96edfe..021b29bbc 100644 --- a/junifer/data/masks.py +++ b/junifer/data/masks.py @@ -138,11 +138,11 @@ def register_mask( 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." + f"Cannot overwrite {name} mask. It is a built-in mask." ) else: raise_error( - f"Mask {name} already registered. Set `overwrite=True`" + f"Mask {name} already registered. Set `overwrite=True` " "to update its value." ) # Convert str to Path @@ -434,7 +434,7 @@ def load_mask( # Check for valid mask name if name not in _available_masks: raise_error( - f"Mask {name} not found. " f"Valid options are: {list_masks()}" + f"Mask {name} not found. Valid options are: {list_masks()}" ) # Copy mask definition to avoid edits in original object @@ -455,7 +455,7 @@ def load_mask( # Load mask if mask_fname is not None: - logger.info(f"Loading mask {mask_fname.absolute()}") + logger.info(f"Loading mask {mask_fname.absolute()!s}") if path_only is False: # Load via nibabel mask_img = nib.load(mask_fname) @@ -504,7 +504,7 @@ def _load_vickery_patil_mask( 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}" + f"Cannot find a GM_prob0.2 mask of resolution {resolution}" ) elif name == "GM_prob0.2_cortex": mask_fname = "GMprob0.2_cortex_3mm_NA_rm.nii.gz" -- 2.52.0 From 24b6e3fac3bcaa331dd8dfd661db53e2baa60a0e Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Wed, 18 Oct 2023 17:19:14 +0200 Subject: [PATCH 47/86] chore: add changelogs 268.change and 268.enh --- docs/changes/newsfragments/268.change | 1 + docs/changes/newsfragments/268.enh | 1 + 2 files changed, 2 insertions(+) create mode 100644 docs/changes/newsfragments/268.change create mode 100644 docs/changes/newsfragments/268.enh diff --git a/docs/changes/newsfragments/268.change b/docs/changes/newsfragments/268.change new file mode 100644 index 000000000..78a2f2030 --- /dev/null +++ b/docs/changes/newsfragments/268.change @@ -0,0 +1 @@ +Add ``space`` parameter to :func:`.register_coordinates`, :func:`.register_parcellation` and :func:`.register_mask` and return space from :func:`.load_coordinates`, :func:`.load_parcellation` and :func:`.load_mask` by `Synchon Mandal`_ and `Fede Raimondo`_ diff --git a/docs/changes/newsfragments/268.enh b/docs/changes/newsfragments/268.enh new file mode 100644 index 000000000..f45544395 --- /dev/null +++ b/docs/changes/newsfragments/268.enh @@ -0,0 +1 @@ +Add ``space`` information to existing datagrabbers, masks, parcellations and coordinates by `Synchon Mandal`_ and `Fede Raimondo`_ -- 2.52.0 From be3ab708f4294e746dfae61e43fc0fd07d326088 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Thu, 19 Oct 2023 11:46:39 +0200 Subject: [PATCH 48/86] fix: correct return value count for load_coordinates() in test_coordinates.py --- junifer/data/tests/test_coordinates.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/junifer/data/tests/test_coordinates.py b/junifer/data/tests/test_coordinates.py index 5c6e8fb77..8a1928a12 100644 --- a/junifer/data/tests/test_coordinates.py +++ b/junifer/data/tests/test_coordinates.py @@ -132,7 +132,7 @@ def test_get_coordinates() -> None: coords="DMNBuckner", target_data=vbm_gm ) # Get raw coordinates - raw_coords, raw_labels = load_coordinates("DMNBuckner") + raw_coords, raw_labels, _ = load_coordinates("DMNBuckner") # Both tailored and raw should be same for now assert_array_equal(tailored_coords, raw_coords) assert tailored_labels == raw_labels -- 2.52.0 From e92642e7ca1901648ac452f3de9a8df648eb38bf Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Thu, 19 Oct 2023 14:23:21 +0200 Subject: [PATCH 49/86] fix: add space for tests in test_crossparcellation_functional_connectivity.py --- .../tests/test_crossparcellation_functional_connectivity.py | 1 + 1 file changed, 1 insertion(+) diff --git a/junifer/markers/functional_connectivity/tests/test_crossparcellation_functional_connectivity.py b/junifer/markers/functional_connectivity/tests/test_crossparcellation_functional_connectivity.py index bf2787ac9..720b817fa 100644 --- a/junifer/markers/functional_connectivity/tests/test_crossparcellation_functional_connectivity.py +++ b/junifer/markers/functional_connectivity/tests/test_crossparcellation_functional_connectivity.py @@ -29,6 +29,7 @@ def test_compute() -> None: "data": niimg, "path": out["BOLD"]["path"], "meta": {"element": "sub001"}, + "space": "MNI", } } -- 2.52.0 From 30de67d8aa1a73a95c0d988be86b5406aec0b180 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Thu, 19 Oct 2023 14:24:01 +0200 Subject: [PATCH 50/86] fix: add space for tests in test_edge_functional_connectivity_parcels.py --- .../tests/test_edge_functional_connectivity_parcels.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/junifer/markers/functional_connectivity/tests/test_edge_functional_connectivity_parcels.py b/junifer/markers/functional_connectivity/tests/test_edge_functional_connectivity_parcels.py index 5257980a5..7822652fc 100644 --- a/junifer/markers/functional_connectivity/tests/test_edge_functional_connectivity_parcels.py +++ b/junifer/markers/functional_connectivity/tests/test_edge_functional_connectivity_parcels.py @@ -30,7 +30,9 @@ def test_EdgeCentricFCParcels(tmp_path: Path) -> None: parcellation="TianxS1x3TxMNInonlinear2009cAsym", cor_method_params={"empirical": True}, ) - all_out = efc.fit_transform({"BOLD": {"data": fmri_img, "meta": {}}}) + all_out = efc.fit_transform( + {"BOLD": {"data": fmri_img, "meta": {}, "space": "MNI"}} + ) out = all_out["BOLD"] @@ -51,7 +53,7 @@ def test_EdgeCentricFCParcels(tmp_path: Path) -> None: # 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}} + input = {"BOLD": {"data": fmri_img, "meta": meta, "space": "MNI"}} all_out = efc.fit_transform(input, storage=storage) features = storage.list_features() -- 2.52.0 From 22b343124e8d61fc9fc2475dcf73fbdfcc2f9ddb Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Thu, 19 Oct 2023 14:24:35 +0200 Subject: [PATCH 51/86] fix: add space for tests in test_edge_functional_connectivity_spheres.py --- .../tests/test_edge_functional_connectivity_spheres.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/junifer/markers/functional_connectivity/tests/test_edge_functional_connectivity_spheres.py b/junifer/markers/functional_connectivity/tests/test_edge_functional_connectivity_spheres.py index 6b9ad17f6..8695e55f5 100644 --- a/junifer/markers/functional_connectivity/tests/test_edge_functional_connectivity_spheres.py +++ b/junifer/markers/functional_connectivity/tests/test_edge_functional_connectivity_spheres.py @@ -28,7 +28,9 @@ def test_EdgeCentricFCSpheres(tmp_path: Path) -> None: efc = EdgeCentricFCSpheres( coords="DMNBuckner", radius=5.0, cor_method="correlation" ) - all_out = efc.fit_transform({"BOLD": {"data": fmri_img, "meta": {}}}) + all_out = efc.fit_transform( + {"BOLD": {"data": fmri_img, "meta": {}, "space": "MNI"}} + ) out = all_out["BOLD"] @@ -58,13 +60,15 @@ def test_EdgeCentricFCSpheres(tmp_path: Path) -> None: "element": {"subject": "sub001"}, "dependencies": {"nilearn"}, } - all_out = efc.fit_transform({"BOLD": {"data": fmri_img, "meta": meta}}) + all_out = efc.fit_transform( + {"BOLD": {"data": fmri_img, "meta": meta, "space": "MNI"}} + ) uri = tmp_path / "test_fc_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}} + input = {"BOLD": {"data": fmri_img, "meta": meta, "space": "MNI"}} all_out = efc.fit_transform(input, storage=storage) features = storage.list_features() -- 2.52.0 From e2e16a7b1f07b94b038650c6338d885f71aeac80 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Thu, 19 Oct 2023 14:30:26 +0200 Subject: [PATCH 52/86] fix: add space for tests in test_functional_connectivity_parcels.py --- .../tests/test_functional_connectivity_parcels.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/junifer/markers/functional_connectivity/tests/test_functional_connectivity_parcels.py b/junifer/markers/functional_connectivity/tests/test_functional_connectivity_parcels.py index aa438d8a5..560635f26 100644 --- a/junifer/markers/functional_connectivity/tests/test_functional_connectivity_parcels.py +++ b/junifer/markers/functional_connectivity/tests/test_functional_connectivity_parcels.py @@ -32,7 +32,9 @@ def test_FunctionalConnectivityParcels(tmp_path: Path) -> None: fmri_img = image.concat_imgs(ni_data.func) # type: ignore fc = FunctionalConnectivityParcels(parcellation="Schaefer100x7") - all_out = fc.fit_transform({"BOLD": {"data": fmri_img, "meta": {}}}) + all_out = fc.fit_transform( + {"BOLD": {"data": fmri_img, "meta": {}, "space": "MNI"}} + ) out = all_out["BOLD"] @@ -52,7 +54,7 @@ def test_FunctionalConnectivityParcels(tmp_path: Path) -> None: "element": {"subject": "sub001"}, "dependencies": {"nilearn"}, } - ts = pa.compute({"data": fmri_img, "meta": meta}) + ts = pa.compute({"data": fmri_img, "meta": meta, "space": "MNI"}) # compare with nilearn # Get the testing parcellation (for nilearn) @@ -80,13 +82,15 @@ def test_FunctionalConnectivityParcels(tmp_path: Path) -> None: parcellation="Schaefer100x7", cor_method_params={"empirical": True} ) - all_out = fc.fit_transform({"BOLD": {"data": fmri_img, "meta": meta}}) + all_out = fc.fit_transform( + {"BOLD": {"data": fmri_img, "meta": meta, "space": "MNI"}} + ) uri = tmp_path / "test_fc_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}} + input = {"BOLD": {"data": fmri_img, "meta": meta, "space": "MNI"}} all_out = fc.fit_transform(input, storage=storage) features = storage.list_features() -- 2.52.0 From bceaa5611c186dae6beb3b8600d220cdc38ff44e Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Thu, 19 Oct 2023 14:30:47 +0200 Subject: [PATCH 53/86] fix: add space for tests in test_functional_connectivity_spheres.py --- .../tests/test_functional_connectivity_spheres.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/junifer/markers/functional_connectivity/tests/test_functional_connectivity_spheres.py b/junifer/markers/functional_connectivity/tests/test_functional_connectivity_spheres.py index ddc4c842b..6f695c787 100644 --- a/junifer/markers/functional_connectivity/tests/test_functional_connectivity_spheres.py +++ b/junifer/markers/functional_connectivity/tests/test_functional_connectivity_spheres.py @@ -36,7 +36,9 @@ def test_FunctionalConnectivitySpheres(tmp_path: Path) -> None: fc = FunctionalConnectivitySpheres( coords="DMNBuckner", radius=5.0, cor_method="correlation" ) - all_out = fc.fit_transform({"BOLD": {"data": fmri_img, "meta": {}}}) + all_out = fc.fit_transform( + {"BOLD": {"data": fmri_img, "meta": {}, "space": "MNI"}} + ) out = all_out["BOLD"] @@ -52,7 +54,7 @@ def test_FunctionalConnectivitySpheres(tmp_path: Path) -> None: sa = SphereAggregation( coords="DMNBuckner", radius=5.0, method="mean", on="BOLD" ) - ts = sa.compute({"data": fmri_img, "meta": {}}) + ts = sa.compute({"data": fmri_img, "meta": {}, "space": "MNI"}) # Check that FC are almost equal when using nileran cm = ConnectivityMeasure(kind="correlation") @@ -69,7 +71,7 @@ def test_FunctionalConnectivitySpheres(tmp_path: Path) -> None: "element": {"subject": "test"}, "dependencies": {"numpy", "nilearn"}, } - input = {"BOLD": {"data": fmri_img, "meta": meta}} + input = {"BOLD": {"data": fmri_img, "meta": meta, "space": "MNI"}} all_out = fc.fit_transform(input, storage=storage) features = storage.list_features() @@ -99,7 +101,9 @@ def test_FunctionalConnectivitySpheres_empirical(tmp_path: Path) -> None: cor_method="correlation", cor_method_params={"empirical": True}, ) - all_out = fc.fit_transform({"BOLD": {"data": fmri_img, "meta": {}}}) + all_out = fc.fit_transform( + {"BOLD": {"data": fmri_img, "meta": {}, "space": "MNI"}} + ) out = all_out["BOLD"] @@ -115,7 +119,7 @@ def test_FunctionalConnectivitySpheres_empirical(tmp_path: Path) -> None: sa = SphereAggregation( coords="DMNBuckner", radius=5.0, method="mean", on="BOLD" ) - ts = sa.compute({"data": fmri_img}) + ts = sa.compute({"data": fmri_img, "space": "MNI"}) # Check that FC are almost equal when using nileran cm = ConnectivityMeasure( -- 2.52.0 From e3974eed17452d4fae0ace44851db298d0fbddeb Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Thu, 19 Oct 2023 14:31:42 +0200 Subject: [PATCH 54/86] fix: add space for tests in test_reho_parcels.py --- .../markers/reho/tests/test_reho_parcels.py | 36 ++++++++++++++++--- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/junifer/markers/reho/tests/test_reho_parcels.py b/junifer/markers/reho/tests/test_reho_parcels.py index 488eda53a..6bd7564bd 100644 --- a/junifer/markers/reho/tests/test_reho_parcels.py +++ b/junifer/markers/reho/tests/test_reho_parcels.py @@ -39,7 +39,14 @@ def test_reho_parcels_computation(tmp_path: Path) -> None: reho_parcels_marker = ReHoParcels(parcellation=PARCELLATION) # Fit transform marker on data reho_parcels_output = reho_parcels_marker.fit_transform( - {"BOLD": {"path": "/tmp", "data": fmri_img, "meta": {}}} + { + "BOLD": { + "path": "/tmp", + "data": fmri_img, + "meta": {}, + "space": "MNI", + } + } ) # Get BOLD output reho_parcels_output_bold = reho_parcels_output["BOLD"] @@ -82,7 +89,14 @@ def test_reho_parcels_computation_comparison(tmp_path: Path) -> None: ) # Fit transform marker on data reho_parcels_output_python = reho_parcels_marker_python.fit_transform( - {"BOLD": {"path": "/tmp", "data": fmri_img, "meta": {}}} + { + "BOLD": { + "path": "/tmp", + "data": fmri_img, + "meta": {}, + "space": "MNI", + } + } ) # Get BOLD output reho_parcels_output_bold_python = reho_parcels_output_python["BOLD"] @@ -93,7 +107,14 @@ def test_reho_parcels_computation_comparison(tmp_path: Path) -> None: ) # Fit transform marker on data reho_parcels_output_afni = reho_parcels_marker_afni.fit_transform( - {"BOLD": {"path": "/tmp", "data": fmri_img, "meta": {}}} + { + "BOLD": { + "path": "/tmp", + "data": fmri_img, + "meta": {}, + "space": "MNI", + } + } ) # Get BOLD output reho_parcels_output_bold_afni = reho_parcels_output_afni["BOLD"] @@ -134,6 +155,13 @@ def test_reho_parcels_storage(tmp_path: Path) -> None: } # only requires element key for storing # Fit transform marker on data with storage reho_parcels_marker.fit_transform( - input={"BOLD": {"path": "/tmp", "data": fmri_img, "meta": meta}}, + input={ + "BOLD": { + "path": "/tmp", + "data": fmri_img, + "meta": meta, + "space": "MNI", + } + }, storage=reho_parcels_storage, ) -- 2.52.0 From 528dd745dd5fc507c489e3a3dc357734a38a549d Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Thu, 19 Oct 2023 14:32:08 +0200 Subject: [PATCH 55/86] fix: add space for tests in test_reho_spheres.py --- .../markers/reho/tests/test_reho_spheres.py | 36 ++++++++++++++++--- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/junifer/markers/reho/tests/test_reho_spheres.py b/junifer/markers/reho/tests/test_reho_spheres.py index c662980be..e3eba63e8 100644 --- a/junifer/markers/reho/tests/test_reho_spheres.py +++ b/junifer/markers/reho/tests/test_reho_spheres.py @@ -39,7 +39,14 @@ def test_reho_spheres_computation(tmp_path: Path) -> None: reho_spheres_marker = ReHoSpheres(coords=COORDINATES, radius=10.0) # Fit transform marker on data reho_spheres_output = reho_spheres_marker.fit_transform( - {"BOLD": {"path": "/tmp", "data": fmri_img, "meta": {}}} + { + "BOLD": { + "path": "/tmp", + "data": fmri_img, + "meta": {}, + "space": "MNI", + } + } ) # Get BOLD output reho_spheres_output_bold = reho_spheres_output["BOLD"] @@ -82,7 +89,14 @@ def test_reho_spheres_computation_comparison(tmp_path: Path) -> None: ) # Fit transform marker on data reho_spheres_output_python = reho_spheres_marker_python.fit_transform( - {"BOLD": {"path": "/tmp", "data": fmri_img, "meta": {}}} + { + "BOLD": { + "path": "/tmp", + "data": fmri_img, + "meta": {}, + "space": "MNI", + } + } ) # Get BOLD output reho_spheres_output_bold_python = reho_spheres_output_python["BOLD"] @@ -93,7 +107,14 @@ def test_reho_spheres_computation_comparison(tmp_path: Path) -> None: ) # Fit transform marker on data reho_spheres_output_afni = reho_spheres_marker_afni.fit_transform( - {"BOLD": {"path": "/tmp", "data": fmri_img, "meta": {}}} + { + "BOLD": { + "path": "/tmp", + "data": fmri_img, + "meta": {}, + "space": "MNI", + } + } ) # Get BOLD output reho_spheres_output_bold_afni = reho_spheres_output_afni["BOLD"] @@ -134,6 +155,13 @@ def test_reho_spheres_storage(tmp_path: Path) -> None: } # only requires element key for storing # Fit transform marker on data with storage reho_spheres_marker.fit_transform( - input={"BOLD": {"path": "/tmp", "data": fmri_img, "meta": meta}}, + input={ + "BOLD": { + "path": "/tmp", + "data": fmri_img, + "meta": meta, + "space": "MNI", + } + }, storage=reho_spheres_storage, ) -- 2.52.0 From 874bf654cd6472bcee43e82c08b744d961c263bb Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Thu, 19 Oct 2023 14:32:27 +0200 Subject: [PATCH 56/86] fix: add space for tests in test_temporal_snr_parcels.py --- .../markers/temporal_snr/tests/test_temporal_snr_parcels.py | 4 ++-- 1 file changed, 2 insertions(+), 2 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 e0ef668f4..e888a74ff 100644 --- a/junifer/markers/temporal_snr/tests/test_temporal_snr_parcels.py +++ b/junifer/markers/temporal_snr/tests/test_temporal_snr_parcels.py @@ -26,7 +26,7 @@ def test_TemporalSNRParcels(tmp_path: Path) -> None: tsnr_parcels = TemporalSNRParcels(parcellation="Schaefer100x7") all_out = tsnr_parcels.fit_transform( - {"BOLD": {"data": fmri_img, "meta": {}}} + {"BOLD": {"data": fmri_img, "meta": {}, "space": "MNI"}} ) out = all_out["BOLD"] @@ -45,7 +45,7 @@ def test_TemporalSNRParcels(tmp_path: Path) -> None: # 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}} + input = {"BOLD": {"data": fmri_img, "meta": meta, "space": "MNI"}} all_out = tsnr_parcels.fit_transform(input, storage=storage) features = storage.list_features() -- 2.52.0 From b8119fe3b2aa7dab56ef4f67288b94ec4716239a Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Thu, 19 Oct 2023 14:32:48 +0200 Subject: [PATCH 57/86] fix: add space for tests in test_temporal_snr_spheres.py --- .../markers/temporal_snr/tests/test_temporal_snr_spheres.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 448de5e54..70ad1752e 100644 --- a/junifer/markers/temporal_snr/tests/test_temporal_snr_spheres.py +++ b/junifer/markers/temporal_snr/tests/test_temporal_snr_spheres.py @@ -27,7 +27,7 @@ def test_TemporalSNRSpheres(tmp_path: Path) -> None: tsnr_spheres = TemporalSNRSpheres(coords="DMNBuckner", radius=5.0) all_out = tsnr_spheres.fit_transform( - {"BOLD": {"data": fmri_img, "meta": {}}} + {"BOLD": {"data": fmri_img, "meta": {}, "space": "MNI"}} ) out = all_out["BOLD"] @@ -48,7 +48,7 @@ def test_TemporalSNRSpheres(tmp_path: Path) -> None: "element": {"subject": "test"}, "dependencies": {"numpy", "nilearn"}, } - input = {"BOLD": {"data": fmri_img, "meta": meta}} + input = {"BOLD": {"data": fmri_img, "meta": meta, "space": "MNI"}} all_out = tsnr_spheres.fit_transform(input, storage=storage) features = storage.list_features() -- 2.52.0 From cd06606f96e7e237a61847f715a3a046ea8d163e Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Thu, 19 Oct 2023 14:33:08 +0200 Subject: [PATCH 58/86] fix: add space for tests in test_ets_rss.py --- junifer/markers/tests/test_ets_rss.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/junifer/markers/tests/test_ets_rss.py b/junifer/markers/tests/test_ets_rss.py index 0c6c872c4..04c102893 100644 --- a/junifer/markers/tests/test_ets_rss.py +++ b/junifer/markers/tests/test_ets_rss.py @@ -29,13 +29,17 @@ def test_compute() -> None: # Load BOLD image niimg = image.load_img(str(out["BOLD"]["path"].absolute())) # Create input data - input_dict = {"data": niimg, "path": out["BOLD"]["path"]} + input_dict = { + "data": niimg, + "path": out["BOLD"]["path"], + "space": "MNI", + } # Compute the RSSETSMarker ets_rss_marker = RSSETSMarker(parcellation=PARCELLATION) new_out = ets_rss_marker.compute(input_dict) # Load parcellation - test_parcellation, _, _ = load_parcellation(PARCELLATION) + test_parcellation, _, _, _ = load_parcellation(PARCELLATION) # Compute the NiftiLabelsMasker test_masker = NiftiLabelsMasker(test_parcellation) test_ts = test_masker.fit_transform(niimg) -- 2.52.0 From dfd550452d8d4e5b4304e09fd21ec8fd6669f8af Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Thu, 19 Oct 2023 14:33:26 +0200 Subject: [PATCH 59/86] fix: add space for tests in test_parcel_aggregation.py --- junifer/markers/tests/test_parcel_aggregation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/junifer/markers/tests/test_parcel_aggregation.py b/junifer/markers/tests/test_parcel_aggregation.py index 615aad7e9..12bd01c3f 100644 --- a/junifer/markers/tests/test_parcel_aggregation.py +++ b/junifer/markers/tests/test_parcel_aggregation.py @@ -213,7 +213,7 @@ def test_ParcelAggregation_3D_mask() -> None: parcellation = datasets.fetch_atlas_schaefer_2018(n_rois=100) # Get one mask - mask_img, _ = load_mask("GM_prob0.2") + mask_img, _, _ = load_mask("GM_prob0.2") # Get the oasis VBM data oasis_dataset = datasets.fetch_oasis_vbm(n_subjects=1) -- 2.52.0 From 321a83324b68befbcb1177f6b0a2e4a011deb49a Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Thu, 19 Oct 2023 14:33:45 +0200 Subject: [PATCH 60/86] fix: add space for tests in test_sphere_aggregation.py --- junifer/markers/tests/test_sphere_aggregation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/junifer/markers/tests/test_sphere_aggregation.py b/junifer/markers/tests/test_sphere_aggregation.py index 19b11c03d..04b9de474 100644 --- a/junifer/markers/tests/test_sphere_aggregation.py +++ b/junifer/markers/tests/test_sphere_aggregation.py @@ -140,7 +140,7 @@ def test_SphereAggregation_3D_mask() -> None: coordinates, _, _ = load_coordinates(COORDS) # Get one mask - mask_img, _ = load_mask("GM_prob0.2") + mask_img, _, _ = load_mask("GM_prob0.2") # Get the oasis VBM data oasis_dataset = datasets.fetch_oasis_vbm(n_subjects=1) -- 2.52.0 From 0d5e434b56a8452f23e371f038f979b252a14571 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Thu, 19 Oct 2023 16:29:32 +0200 Subject: [PATCH 61/86] update: add space info to existing parcellations --- junifer/data/parcellations.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/junifer/data/parcellations.py b/junifer/data/parcellations.py index 19d234a63..eb7a011f9 100644 --- a/junifer/data/parcellations.py +++ b/junifer/data/parcellations.py @@ -45,7 +45,7 @@ if TYPE_CHECKING: # TODO: have separate dictionary for built-in _available_parcellations: Dict[str, Dict[Any, Any]] = { "SUITxSUIT": {"family": "SUIT", "space": "SUIT"}, - "SUITxMNI": {"family": "SUIT", "space": "MNI"}, + "SUITxMNI": {"family": "SUIT", "space": "MNI152Lin6Asym"}, } # Add Schaefer parcellation info @@ -56,7 +56,7 @@ for n_rois in range(100, 1001, 100): "family": "Schaefer", "n_rois": n_rois, "yeo_networks": t_net, - "space": "MNI", + "space": "MNI152NLin6Asym", } # Add Tian parcellation info for scale in range(1, 5): @@ -65,28 +65,28 @@ for scale in range(1, 5): "family": "Tian", "scale": scale, "magneticfield": "7T", - "space": "MNI6thgeneration", + "space": "MNI152NLin6Asym", } t_name = f"TianxS{scale}x3TxMNI6thgeneration" _available_parcellations[t_name] = { "family": "Tian", "scale": scale, "magneticfield": "3T", - "space": "MNI6thgeneration", + "space": "MNI152NLin6Asym", } t_name = f"TianxS{scale}x3TxMNInonlinear2009cAsym" _available_parcellations[t_name] = { "family": "Tian", "scale": scale, "magneticfield": "3T", - "space": "MNInonlinear2009cAsym", + "space": "MNI152NLin2009cAsym", } # Add AICHA parcellation info for version in (1, 2): _available_parcellations[f"AICHA_v{version}"] = { "family": "AICHA", "version": version, - "space": "MNI", + "space": "MNI152Lin6Asym", } # Add Shen parcellation info for year in (2013, 2015, 2019): @@ -96,21 +96,21 @@ for year in (2013, 2015, 2019): "family": "Shen", "year": 2013, "n_rois": n_rois, - "space": "MNI", + "space": "MNI152NLin2009cAsym", } elif year == 2015: _available_parcellations["Shen_2015_268"] = { "family": "Shen", "year": 2015, "n_rois": 268, - "space": "MNI", + "space": "MNI152NLin2009cAsym", } elif year == 2019: _available_parcellations["Shen_2019_368"] = { "family": "Shen", "year": 2019, "n_rois": 368, - "space": "MNI", + "space": "MNI152NLin2009cAsym", } # Add Yan parcellation info for n_rois in range(100, 1001, 100): @@ -120,14 +120,14 @@ for n_rois in range(100, 1001, 100): "family": "Yan", "n_rois": n_rois, "yeo_networks": yeo_network, - "space": "MNI", + "space": "MNI152NLin6Asym", } # Add Kong networks _available_parcellations[f"Yan{n_rois}xKong17"] = { "family": "Yan", "n_rois": n_rois, "kong_networks": 17, - "space": "MNI", + "space": "MNI152NLin6Asym", } -- 2.52.0 From cf83c1d91de227dd7fe0062aca7aaad5946b9e7b Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Thu, 19 Oct 2023 16:29:45 +0200 Subject: [PATCH 62/86] update: improve space info for existing masks --- junifer/data/masks.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/junifer/data/masks.py b/junifer/data/masks.py index 021b29bbc..2c5b46225 100644 --- a/junifer/data/masks.py +++ b/junifer/data/masks.py @@ -80,8 +80,11 @@ def _fetch_icbm152_brain_gm_mask( # Callable masks should be functions that take at least one parameter: # * `target_img`: the image to which the mask will be applied. _available_masks: Dict[str, Dict[str, Any]] = { - "GM_prob0.2": {"family": "Vickery-Patil", "space": "MNI"}, - "GM_prob0.2_cortex": {"family": "Vickery-Patil", "space": "MNI"}, + "GM_prob0.2": {"family": "Vickery-Patil", "space": "MNI152Lin6Asym"}, + "GM_prob0.2_cortex": { + "family": "Vickery-Patil", + "space": "MNI152Lin6Asym", + }, "compute_brain_mask": { "family": "Callable", "func": compute_brain_mask, -- 2.52.0 From 2dbb9d9f718508c8c2e0589f23754f49628823bb Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Thu, 19 Oct 2023 16:30:16 +0200 Subject: [PATCH 63/86] docs: add space info for existing parcellations and masks in builtin.rst --- docs/builtin.rst | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/builtin.rst b/docs/builtin.rst index 574b645f3..9c865f0c7 100644 --- a/docs/builtin.rst +++ b/docs/builtin.rst @@ -247,6 +247,7 @@ Available * - Name - Options - Keys + - Spaces - Version added - Publication * - Schaefer @@ -255,6 +256,7 @@ Available | ``Schaefer200x17``, ``Schaefer300x17``, ``Schaefer400x17``, | ``Schaefer500x17``, ``Schaefer600x17``, ``Schaefer700x17``, | ``Schaefer800x17``, ``Schaefer900x17``, ``Schaefer1000x17`` + - ``MNI152NLin6Asym`` - 0.0.1 - | Schaefer, A., Kong, R., Gordon, E.M. et al. | Local-Global Parcellation of the Human Cerebral Cortex from @@ -264,6 +266,7 @@ Available * - SUIT - ``space`` - ``SUITxMNI``, ``SUITxSUIT`` + - ``SUIT``, ``MNI152Lin6Asym`` - 0.0.1 - | Diedrichsen, J. | A spatially unbiased atlas template of the human cerebellum. @@ -279,6 +282,7 @@ Available | ``TianxS2x3TxMNInonlinear2009cAsym``, | ``TianxS3x3TxMNInonlinear2009cAsym``, | ``TianxS4x3TxMNInonlinear2009cAsym`` + - ``MNI152NLin6Asym``, ``MNI152NLin2009cAsym`` - 0.0.1 - | Tian, Y., Margulies, D.S., Breakspear, M. et al. | Topographic organization of the human subcortex @@ -288,6 +292,7 @@ Available * - AICHA - ``version`` - ``AICHA_v1``, ``AICHA_v2`` + - ``MNI152Lin6Asym`` - 0.0.3 - | Joliot, M., Jobard, G., Naveau, M. et al. | AICHA: An atlas of intrinsic connectivity of homotopic areas. @@ -297,6 +302,7 @@ Available - ``year``, ``n_rois`` - | ``Shen_2013_50``, ``Shen_2013_100``, ``Shen_2013_150``, | ``Shen_2015_268``, ``Shen_2019_368`` + - ``MNI152NLin2009cAsym`` - 0.0.3 - | Shen, X., Tokoglu, F., Papademetris, X., Constable, R.T. | Groupwise whole-brain parcellation from resting-state fMRI data @@ -322,6 +328,7 @@ Available | ``Yan400xKong17``, ``Yan500xKong17``, ``Yan600xKong17``, | ``Yan700xKong17``, ``Yan800xKong17``, ``Yan900xKong17``, | ``Yan1000xKong17`` + - ``MNI152NLin6Asym`` - 0.0.3 - | Yan, X., Kong, R., Xue, A., et al. | Homotopic local-global parcellation of the human cerebral cortex from @@ -590,28 +597,33 @@ Available * - Name - Keys + - Spaces - Version added - Description - Publication * - Vickery-Patil (Gray Matter) - | ``GM_prob0.2`` + - ``MNI152Lin6Asym`` - 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`` + - ``MNI152Lin6Asym`` - 0.0.1 - | Vickery, Sam, & Patil, Kaustubh. (2022). | Chimpanzee and Human Gray Matter Masks [Data set]. Zenodo. | https://doi.org/10.5281/zenodo.6463123 * - Nilearn's MNI152 1mm-resolution mask - | ``compute_brain_mask`` + - Adapts to the target data - 0.0.2 - | Compute the whole-brain mask. This mask is calculated using | MNI152 1mm-resolution template mask onto the target image. | See :func:`nilearn.masking.compute_brain_mask` * - Nilearn's mask computed from FMRI data - | ``compute_epi_mask`` + - Adapts to the target data - 0.0.2 - | Compute a brain mask from fMRI data. This is based on an heuristic | proposed by T.Nichols: find the least dense point of the histogram, @@ -619,6 +631,7 @@ Available | image histogram. See :func:`nilearn.masking.compute_epi_mask` * - Nilearn's background mask - | ``compute_background_mask`` + - Adapts to the target data - 0.0.2 - | Compute a brain mask for the images by guessing the value of the | background from the border of the image. @@ -626,6 +639,7 @@ Available * - Nilearn's ICBM152 template gray-matter mask - | ``fetch_icbm152_brain_gm_mask`` + - ``MNI152NLin2009aAsym`` - 0.0.2 - | Compute a gray-matter mask from the asymmetrical ICBM152 2009 template, | release a. -- 2.52.0 From fedaa1dbd11968aa2a54cf0466e0f418366ba237 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Fri, 20 Oct 2023 11:25:55 +0200 Subject: [PATCH 64/86] fix: correct space for tests in test_masks.py --- junifer/data/tests/test_masks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/junifer/data/tests/test_masks.py b/junifer/data/tests/test_masks.py index d1c2f2abf..a0ddba536 100644 --- a/junifer/data/tests/test_masks.py +++ b/junifer/data/tests/test_masks.py @@ -202,7 +202,7 @@ def test_vickery_patil( assert_array_almost_equal( mask.header["pixdim"][1:4], pixdim # type: ignore ) - assert space == "MNI" + assert space == "MNI152Lin6Asym" assert mask_fname is not None assert mask_fname.name == fname -- 2.52.0 From 3ca791d8711d3a9df7da0b87b65623420b337dbf Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Fri, 20 Oct 2023 11:26:37 +0200 Subject: [PATCH 65/86] update: improve space info for existing parcellations --- junifer/data/parcellations.py | 39 ++++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/junifer/data/parcellations.py b/junifer/data/parcellations.py index eb7a011f9..261d6923b 100644 --- a/junifer/data/parcellations.py +++ b/junifer/data/parcellations.py @@ -487,13 +487,13 @@ def _retrieve_parcellation( * Tian : ``scale`` : {1, 2, 3, 4} Scale of parcellation (defines granularity). - ``space`` : {"MNI6thgeneration", "MNInonlinear2009cAsym"}, optional - Space of parcellation (default "MNI6thgeneration"). (For more + ``space`` : {"MNI152NLin6Asym", "MNI152NLin2009cAsym"}, optional + Space of parcellation (default "MNI152NLin6Asym"). (For more information see https://github.com/yetianmed/subcortex) ``magneticfield`` : {"3T", "7T"}, optional Magnetic field (default "3T"). * SUIT : - ``space`` : {"MNI", "SUIT"}, optional + ``space`` : {"MNI152Lin6Asym", "SUIT"}, optional Space of parcellation (default "MNI"). (For more information see http://www.diedrichsenlab.org/imaging/suit.htm). * AICHA : @@ -691,7 +691,7 @@ def _retrieve_tian( parcellations_dir: Path, resolution: Optional[float] = None, scale: Optional[int] = None, - space: str = "MNI6thgeneration", + space: str = "MNI152NLin6Asym", magneticfield: str = "3T", ) -> Tuple[Path, List[str]]: """Retrieve Tian parcellation. @@ -708,8 +708,8 @@ def _retrieve_tian( parcellation depend on the space and magnetic field. scale : {1, 2, 3, 4}, optional Scale of parcellation (defines granularity) (default None). - space : {"MNI6thgeneration", "MNInonlinear2009cAsym"}, optional - Space of parcellation (default "MNI6thgeneration"). (For more + space : {"MNI152NLin6Asym", "MNI152NLin2009cAsym"}, optional + Space of parcellation (default "MNI152NLin6Asym"). (For more information see https://github.com/yetianmed/subcortex) magneticfield : {"3T", "7T"}, optional Magnetic field (default "3T"). @@ -745,10 +745,10 @@ def _retrieve_tian( _valid_resolutions = [] # avoid pylance error if magneticfield == "3T": - _valid_spaces = ["MNI6thgeneration", "MNInonlinear2009cAsym"] - if space == "MNI6thgeneration": + _valid_spaces = ["MNI152NLin6Asym", "MNI152NLin2009cAsym"] + if space == "MNI152NLin6Asym": _valid_resolutions = [1, 2] - elif space == "MNInonlinear2009cAsym": + elif space == "MNI152NLin2009cAsym": _valid_resolutions = [2] else: raise_error( @@ -757,10 +757,10 @@ def _retrieve_tian( ) elif magneticfield == "7T": _valid_resolutions = [1.6] - if space != "MNI6thgeneration": + if space != "MNI152NLin6Asym": raise_error( f"The parameter `space` ({space}) for 7T needs to be " - f"MNI6thgeneration" + f"MNI152NLin6Asym" ) else: raise_error( @@ -778,7 +778,7 @@ def _retrieve_tian( parcellation_lname = parcellation_fname_base_3T / ( f"Tian_Subcortex_S{scale}_3T_label.txt" ) - if space == "MNI6thgeneration": + if space == "MNI152NLin6Asym": parcellation_fname = parcellation_fname_base_3T / ( f"Tian_Subcortex_S{scale}_{magneticfield}.nii.gz" ) @@ -787,7 +787,7 @@ def _retrieve_tian( parcellation_fname_base_3T / f"Tian_Subcortex_S{scale}_{magneticfield}_1mm.nii.gz" ) - elif space == "MNInonlinear2009cAsym": + elif space == "MNI152NLin2009cAsym": space = "2009cAsym" parcellation_fname = parcellation_fname_base_3T / ( f"Tian_Subcortex_S{scale}_{magneticfield}_{space}.nii.gz" @@ -857,7 +857,9 @@ def _retrieve_tian( def _retrieve_suit( - parcellations_dir: Path, resolution: Optional[float], space: str = "MNI" + parcellations_dir: Path, + resolution: Optional[float], + space: str = "MNI152Lin6Asym", ) -> Tuple[Path, List[str]]: """Retrieve SUIT parcellation. @@ -871,8 +873,8 @@ def _retrieve_suit( resolution higher than the desired one. By default, will load the highest one (default None). Available resolutions for this parcellation are 1mm and 2mm. - space : {"MNI", "SUIT"}, optional - Space of parcellation (default "MNI"). (For more information + space : {"MNI152Lin6Asym", "SUIT"}, optional + Space of parcellation (default "MNI152Lin6Asym"). (For more information see http://www.diedrichsenlab.org/imaging/suit.htm). Returns @@ -893,7 +895,7 @@ def _retrieve_suit( logger.info(f"\tresolution: {resolution}") logger.info(f"\tspace: {space}") - _valid_spaces = ["MNI", "SUIT"] + _valid_spaces = ["MNI152Lin6Asym", "SUIT"] # check validity of parameters if space not in _valid_spaces: @@ -908,6 +910,9 @@ def _retrieve_suit( resolution = closest_resolution(resolution, _valid_resolutions) # define file names + # Format space if MNI; required for the file name + if space.startswith("MNI"): + space = "MNI" parcellation_fname = ( parcellations_dir / "SUIT" / (f"SUIT_{space}Space_{resolution}mm.nii") ) -- 2.52.0 From 0e227bb64339f41111c92ba3e67d5bdbf66a7b6c Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Fri, 20 Oct 2023 11:27:04 +0200 Subject: [PATCH 66/86] fix: correct space for tests in test_parcellations.py --- junifer/data/tests/test_parcellations.py | 36 +++++++++++++----------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/junifer/data/tests/test_parcellations.py b/junifer/data/tests/test_parcellations.py index a67ee75c2..fafa8d4f8 100644 --- a/junifer/data/tests/test_parcellations.py +++ b/junifer/data/tests/test_parcellations.py @@ -311,7 +311,7 @@ def test_schaefer( assert img is not None assert img_path.name == parcellation_file assert len(label) == n_rois - assert space == "MNI" + assert space == "MNI152NLin6Asym" assert_array_equal( img.header["pixdim"][1:4], 3 * [resolution] # type: ignore ) @@ -354,29 +354,31 @@ def test_retrieve_schaefer_incorrect_yeo_networks(tmp_path: Path) -> None: @pytest.mark.parametrize( - "space", - ["SUIT", "MNI"], + "space_key, space", + [("SUIT", "SUIT"), ("MNI", "MNI152Lin6Asym")], ) -def test_suit(tmp_path: Path, space: str) -> None: +def test_suit(tmp_path: Path, space_key: str, space: str) -> None: """Test SUIT parcellation. Parameters ---------- tmp_path : pathlib.Path The path to the test directory. + space_key : str + The parametrized space values for the key. space : str The parametrized space values. """ parcellations = list_parcellations() - assert f"SUITx{space}" in parcellations + assert f"SUITx{space_key}" in parcellations # Load parcellation img, label, img_path, parcellation_space = load_parcellation( - name=f"SUITx{space}", + name=f"SUITx{space_key}", parcellations_dir=tmp_path, ) assert img is not None - assert img_path.name == f"SUIT_{space}Space_1mm.nii" + assert img_path.name == f"SUIT_{space_key}Space_1mm.nii" assert parcellation_space == space assert len(label) == 34 assert_array_equal(img.header["pixdim"][1:4], [1, 1, 1]) # type: ignore @@ -427,7 +429,7 @@ def test_tian_3T_6thgeneration( fname1 = f"Tian_Subcortex_S{scale}_3T_1mm.nii.gz" assert img is not None assert fname.name == fname1 - assert parcellation_space_1 == "MNI6thgeneration" + assert parcellation_space_1 == "MNI152NLin6Asym" assert len(lbl) == n_label assert_array_equal(img.header["pixdim"][1:4], [1, 1, 1]) # type: ignore # Load parcellation @@ -439,7 +441,7 @@ def test_tian_3T_6thgeneration( fname1 = f"Tian_Subcortex_S{scale}_3T.nii.gz" assert img is not None assert fname.name == fname1 - assert parcellation_space_2 == "MNI6thgeneration" + assert parcellation_space_2 == "MNI152NLin6Asym" assert len(lbl) == n_label assert_array_equal(img.header["pixdim"][1:4], [2, 2, 2]) # type: ignore @@ -475,7 +477,7 @@ def test_tian_3T_nonlinear2009cAsym( fname1 = f"Tian_Subcortex_S{scale}_3T_2009cAsym.nii.gz" assert img is not None assert fname.name == fname1 - assert space == "MNInonlinear2009cAsym" + assert space == "MNI152NLin2009cAsym" assert len(lbl) == n_label assert_array_equal(img.header["pixdim"][1:4], [2, 2, 2]) # type: ignore @@ -510,7 +512,7 @@ def test_tian_7T_6thgeneration( fname1 = f"Tian_Subcortex_S{scale}_7T.nii.gz" assert img is not None assert fname.name == fname1 - assert space == "MNI6thgeneration" + assert space == "MNI152NLin6Asym" assert len(lbl) == n_label assert_array_almost_equal( img.header["pixdim"][1:4], [1.6, 1.6, 1.6] # type: ignore @@ -531,13 +533,13 @@ def test_retrieve_tian_incorrect_space(tmp_path: Path) -> None: parcellations_dir=tmp_path, resolution=1, scale=1, space="wrong" ) - with pytest.raises(ValueError, match=r"MNI6thgeneration"): + with pytest.raises(ValueError, match=r"MNI152NLin6Asym"): _retrieve_tian( parcellations_dir=tmp_path, resolution=1, scale=1, magneticfield="7T", - space="MNInonlinear2009cAsym", + space="MNI152NLin2009cAsym", ) @@ -573,7 +575,7 @@ def test_retrieve_tian_incorrect_scale(tmp_path: Path) -> None: parcellations_dir=tmp_path, resolution=1, scale=5, - space="MNI6thgeneration", + space="MNI152NLin6Asym", ) @@ -597,7 +599,7 @@ def test_aicha(tmp_path: Path, version: int) -> None: ) assert img is not None assert img_path.name == "AICHA.nii" - assert space == "MNI" + assert space == "MNI152Lin6Asym" assert len(label) == 384 assert_array_equal(img.header["pixdim"][1:4], [2, 2, 2]) # type: ignore @@ -668,7 +670,7 @@ def test_shen( ) assert img is not None assert img_name in img_path.name - assert space == "MNI" + assert space == "MNI152NLin2009cAsym" assert len(label) == n_labels assert_array_equal( img.header["pixdim"][1:4], 3 * [resolution] # type: ignore @@ -864,7 +866,7 @@ def test_yan( ) assert img is not None assert img_path.name == parcellation_file # type: ignore - assert space == "MNI" + assert space == "MNI152NLin6Asym" assert len(label) == n_rois assert_array_equal( img.header["pixdim"][1:4], 3 * [resolution] # type: ignore -- 2.52.0 From 0c518904cc4491abb89faea8c60160697ddc4eed Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Mon, 23 Oct 2023 11:49:11 +0200 Subject: [PATCH 67/86] fix: complete space name for AOMIC datagrabbers --- junifer/datagrabber/aomic/id1000.py | 2 +- junifer/datagrabber/aomic/piop1.py | 2 +- junifer/datagrabber/aomic/piop2.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/junifer/datagrabber/aomic/id1000.py b/junifer/datagrabber/aomic/id1000.py index bf497d525..a955f6b8a 100644 --- a/junifer/datagrabber/aomic/id1000.py +++ b/junifer/datagrabber/aomic/id1000.py @@ -122,7 +122,7 @@ class DataladAOMICID1000(PatternDataladDataGrabber): if out.get("BOLD"): out["BOLD"]["mask_item"] = "BOLD_mask" # Add space information - out["BOLD"].update({"space": "MNI"}) + out["BOLD"].update({"space": "MNI152NLin2009cAsym"}) if out.get("T1w"): out["T1w"]["mask_item"] = "T1w_mask" # Add space information diff --git a/junifer/datagrabber/aomic/piop1.py b/junifer/datagrabber/aomic/piop1.py index 400e3d176..88a59005c 100644 --- a/junifer/datagrabber/aomic/piop1.py +++ b/junifer/datagrabber/aomic/piop1.py @@ -167,7 +167,7 @@ class DataladAOMICPIOP1(PatternDataladDataGrabber): if out.get("BOLD"): out["BOLD"]["mask_item"] = "BOLD_mask" # Add space information - out["BOLD"].update({"space": "MNI"}) + out["BOLD"].update({"space": "MNI152NLin2009cAsym"}) if out.get("T1w"): out["T1w"]["mask_item"] = "T1w_mask" # Add space information diff --git a/junifer/datagrabber/aomic/piop2.py b/junifer/datagrabber/aomic/piop2.py index 107f070da..a973d93e2 100644 --- a/junifer/datagrabber/aomic/piop2.py +++ b/junifer/datagrabber/aomic/piop2.py @@ -167,7 +167,7 @@ class DataladAOMICPIOP2(PatternDataladDataGrabber): if out.get("BOLD"): out["BOLD"]["mask_item"] = "BOLD_mask" # Add space information - out["BOLD"].update({"space": "MNI"}) + out["BOLD"].update({"space": "MNI152NLin2009cAsym"}) if out.get("T1w"): out["T1w"]["mask_item"] = "T1w_mask" # Add space information -- 2.52.0 From 5d7370244dcd8b1fde341a8eb86286cbead3aeed Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Mon, 23 Oct 2023 12:49:04 +0200 Subject: [PATCH 68/86] fix: complete space name for testing datagrabbers --- junifer/testing/datagrabbers.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/junifer/testing/datagrabbers.py b/junifer/testing/datagrabbers.py index da560e87a..aeec31cd8 100644 --- a/junifer/testing/datagrabbers.py +++ b/junifer/testing/datagrabbers.py @@ -57,7 +57,7 @@ class OasisVBMTestingDataGrabber(BaseDataGrabber): i_sub = int(subject.split("-")[1]) - 1 out["VBM_GM"] = { "path": Path(self._dataset.gray_matter_maps[i_sub]), - "space": "MNI", + "space": "MNI152Lin6Asym", } return out @@ -144,7 +144,7 @@ class SPMAuditoryTestingDataGrabber(BaseDataGrabber): anat_fname = self.datadir / f"{subject}_T1w.nii.gz" nib.save(fmri_img, fmri_fname) nib.save(anat_img, anat_fname) - out["BOLD"] = {"path": fmri_fname, "space": "MNI"} + out["BOLD"] = {"path": fmri_fname, "space": "MNI152Lin6Asym"} out["T1w"] = {"path": anat_fname, "space": "native"} return out @@ -241,7 +241,7 @@ class PartlyCloudyTestingDataGrabber(BaseDataGrabber): i_sub = int(subject.split("-")[1]) - 1 out["BOLD"] = { "path": Path(self._dataset["func"][i_sub]), - "space": "MNI", + "space": "MNI152NLin2009cAsym", } out["BOLD_confounds"] = { "path": Path(self._dataset["confounds"][i_sub]), -- 2.52.0 From 44dbfec70c8052cffdf3b70d1c4deae3f27c2978 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Mon, 23 Oct 2023 13:00:55 +0200 Subject: [PATCH 69/86] chore: remove space from PartlyCloudyTestingDataGrabber's BOLD_confounds --- junifer/testing/datagrabbers.py | 1 - 1 file changed, 1 deletion(-) diff --git a/junifer/testing/datagrabbers.py b/junifer/testing/datagrabbers.py index aeec31cd8..ee918b844 100644 --- a/junifer/testing/datagrabbers.py +++ b/junifer/testing/datagrabbers.py @@ -246,7 +246,6 @@ class PartlyCloudyTestingDataGrabber(BaseDataGrabber): out["BOLD_confounds"] = { "path": Path(self._dataset["confounds"][i_sub]), "format": "fmriprep", - "space": "MNI", } return out -- 2.52.0 From 8b94ac0d1a936fbdc2bf3d6bec02e052013dc11f Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Mon, 23 Oct 2023 13:28:16 +0200 Subject: [PATCH 70/86] update: disallow mask merging if multiple masks are not in the same space in masks.py --- junifer/data/masks.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/junifer/data/masks.py b/junifer/data/masks.py index 2c5b46225..42b67ed95 100644 --- a/junifer/data/masks.py +++ b/junifer/data/masks.py @@ -310,9 +310,7 @@ def get_mask( # noqa: C901 # Multiple masks, need intersection / union if len(all_masks) > 1: # Intersect / union of masks only if all masks are in the same space - if len(set(all_spaces)) == 1 or all( - x.startswith("MNI") for x in all_spaces - ): + if len(set(all_spaces)) == 1: mask_img = intersect_masks(all_masks, **intersect_params) else: # Check for inherited masks with target image in MNI space -- 2.52.0 From 4a78ff49ebf46ce618ac8294e7c4bc0e6bd7e231 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Mon, 23 Oct 2023 13:31:23 +0200 Subject: [PATCH 71/86] update: disallow parcellation merging if multiple parcellations are not in the same space in parcellations.py --- junifer/data/parcellations.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/junifer/data/parcellations.py b/junifer/data/parcellations.py index 261d6923b..b04189905 100644 --- a/junifer/data/parcellations.py +++ b/junifer/data/parcellations.py @@ -267,9 +267,7 @@ def get_parcellation( else: # Merge the parcellations only if all parcellations are in the same # space - if len(set(all_spaces)) == 1 or all( - x.startswith("MNI") for x in all_spaces - ): + if len(set(all_spaces)) == 1: resampled_parcellation_img, labels = merge_parcellations( parcellations_list=all_parcellations, parcellations_names=parcellation, -- 2.52.0 From 81b3f8c19dd48544f9e8963eac6ff9bb28470474 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Mon, 23 Oct 2023 13:44:45 +0200 Subject: [PATCH 72/86] fix: correct parcellations for same space get_parcellation() tests --- junifer/data/tests/test_parcellations.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/junifer/data/tests/test_parcellations.py b/junifer/data/tests/test_parcellations.py index fafa8d4f8..7bd9fd4ee 100644 --- a/junifer/data/tests/test_parcellations.py +++ b/junifer/data/tests/test_parcellations.py @@ -1144,7 +1144,7 @@ def test_get_parcellation_multi_same_space() -> None: tailored_parcellation, tailored_labels = get_parcellation( parcellation=[ "Schaefer100x7", - "TianxS2x3TxMNInonlinear2009cAsym", + "TianxS2x3TxMNI6thgeneration", ], target_data=vbm_gm, ) @@ -1156,7 +1156,7 @@ def test_get_parcellation_multi_same_space() -> None: raw_labels = [] parcellations_names = [ "Schaefer100x7", - "TianxS2x3TxMNInonlinear2009cAsym", + "TianxS2x3TxMNI6thgeneration", ] for name in parcellations_names: img, labels, _, _ = load_parcellation(name=name, resolution=1.5) -- 2.52.0 From bc67fb33ec3f9b3b25bf93af35418a159f1ff573 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Wed, 25 Oct 2023 15:17:50 +0200 Subject: [PATCH 73/86] fix: correct space for AICHA and issue warning --- junifer/data/parcellations.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/junifer/data/parcellations.py b/junifer/data/parcellations.py index b04189905..cf51aef79 100644 --- a/junifer/data/parcellations.py +++ b/junifer/data/parcellations.py @@ -86,7 +86,7 @@ for version in (1, 2): _available_parcellations[f"AICHA_v{version}"] = { "family": "AICHA", "version": version, - "space": "MNI152Lin6Asym", + "space": "IXI549Space", } # Add Shen parcellation info for year in (2013, 2015, 2019): @@ -997,12 +997,23 @@ def _retrieve_aicha( If invalid value is provided for ``version`` or if there is a problem fetching the parcellation. + Warns + ----- + RuntimeWarning + Until the authors confirm the space, the warning will be issued. + Notes ----- The resolution of the parcellation is 2mm and although v2 provides 1mm, it is only for display purpose as noted in the release document. """ + # Issue warning until space is confirmed by authors + warn_with_log( + "The current space for AICHA parcellations are IXI549Space, but are " + "not confirmed by authors, until that this warning will be issued." + ) + # show parameters to user logger.info("Parcellation parameters:") logger.info(f"\tresolution: {resolution}") -- 2.52.0 From 1be3cf3d8d244eda60723d509cc1dfde898b2d5f Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Wed, 25 Oct 2023 15:19:01 +0200 Subject: [PATCH 74/86] update: fix tests for AICHA --- junifer/data/tests/test_parcellations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/junifer/data/tests/test_parcellations.py b/junifer/data/tests/test_parcellations.py index 7bd9fd4ee..f48362e9e 100644 --- a/junifer/data/tests/test_parcellations.py +++ b/junifer/data/tests/test_parcellations.py @@ -599,7 +599,7 @@ def test_aicha(tmp_path: Path, version: int) -> None: ) assert img is not None assert img_path.name == "AICHA.nii" - assert space == "MNI152Lin6Asym" + assert space == "IXI549Space" assert len(label) == 384 assert_array_equal(img.header["pixdim"][1:4], [2, 2, 2]) # type: ignore -- 2.52.0 From f49ec60722c97c551bac1632af76bf7cd42565f9 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Wed, 25 Oct 2023 15:20:10 +0200 Subject: [PATCH 75/86] fix: correct space for Vickery-Patil mask --- junifer/data/masks.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/junifer/data/masks.py b/junifer/data/masks.py index 42b67ed95..e9033c60b 100644 --- a/junifer/data/masks.py +++ b/junifer/data/masks.py @@ -80,10 +80,10 @@ def _fetch_icbm152_brain_gm_mask( # Callable masks should be functions that take at least one parameter: # * `target_img`: the image to which the mask will be applied. _available_masks: Dict[str, Dict[str, Any]] = { - "GM_prob0.2": {"family": "Vickery-Patil", "space": "MNI152Lin6Asym"}, + "GM_prob0.2": {"family": "Vickery-Patil", "space": "IXI549Space"}, "GM_prob0.2_cortex": { "family": "Vickery-Patil", - "space": "MNI152Lin6Asym", + "space": "IXI549Space", }, "compute_brain_mask": { "family": "Callable", -- 2.52.0 From d1c55bf619b26d8af95ab7ab6dcac89eba31a630 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Wed, 25 Oct 2023 15:20:34 +0200 Subject: [PATCH 76/86] update: fix tests for Vickery-Patil mask --- junifer/data/tests/test_masks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/junifer/data/tests/test_masks.py b/junifer/data/tests/test_masks.py index a0ddba536..07c2b2eee 100644 --- a/junifer/data/tests/test_masks.py +++ b/junifer/data/tests/test_masks.py @@ -202,7 +202,7 @@ def test_vickery_patil( assert_array_almost_equal( mask.header["pixdim"][1:4], pixdim # type: ignore ) - assert space == "MNI152Lin6Asym" + assert space == "IXI549Space" assert mask_fname is not None assert mask_fname.name == fname -- 2.52.0 From dce62b1b0d08757625e004083001910512c2d1e3 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Wed, 25 Oct 2023 15:28:57 +0200 Subject: [PATCH 77/86] fix: correct space for SUIT MNI --- junifer/data/parcellations.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/junifer/data/parcellations.py b/junifer/data/parcellations.py index cf51aef79..ee0f73efd 100644 --- a/junifer/data/parcellations.py +++ b/junifer/data/parcellations.py @@ -45,7 +45,7 @@ if TYPE_CHECKING: # TODO: have separate dictionary for built-in _available_parcellations: Dict[str, Dict[Any, Any]] = { "SUITxSUIT": {"family": "SUIT", "space": "SUIT"}, - "SUITxMNI": {"family": "SUIT", "space": "MNI152Lin6Asym"}, + "SUITxMNI": {"family": "SUIT", "space": "MNI152NLin6Asym"}, } # Add Schaefer parcellation info @@ -491,7 +491,7 @@ def _retrieve_parcellation( ``magneticfield`` : {"3T", "7T"}, optional Magnetic field (default "3T"). * SUIT : - ``space`` : {"MNI152Lin6Asym", "SUIT"}, optional + ``space`` : {"MNI152NLin6Asym", "SUIT"}, optional Space of parcellation (default "MNI"). (For more information see http://www.diedrichsenlab.org/imaging/suit.htm). * AICHA : @@ -857,7 +857,7 @@ def _retrieve_tian( def _retrieve_suit( parcellations_dir: Path, resolution: Optional[float], - space: str = "MNI152Lin6Asym", + space: str = "MNI152NLin6Asym", ) -> Tuple[Path, List[str]]: """Retrieve SUIT parcellation. @@ -871,9 +871,9 @@ def _retrieve_suit( resolution higher than the desired one. By default, will load the highest one (default None). Available resolutions for this parcellation are 1mm and 2mm. - space : {"MNI152Lin6Asym", "SUIT"}, optional - Space of parcellation (default "MNI152Lin6Asym"). (For more information - see http://www.diedrichsenlab.org/imaging/suit.htm). + space : {"MNI152NLin6Asym", "SUIT"}, optional + Space of parcellation (default "MNI152NLin6Asym"). (For more + information see http://www.diedrichsenlab.org/imaging/suit.htm). Returns ------- @@ -893,7 +893,7 @@ def _retrieve_suit( logger.info(f"\tresolution: {resolution}") logger.info(f"\tspace: {space}") - _valid_spaces = ["MNI152Lin6Asym", "SUIT"] + _valid_spaces = ["MNI152NLin6Asym", "SUIT"] # check validity of parameters if space not in _valid_spaces: @@ -907,10 +907,11 @@ def _retrieve_suit( resolution = closest_resolution(resolution, _valid_resolutions) - # define file names # Format space if MNI; required for the file name - if space.startswith("MNI"): + if space == "MNI152NLin6Asym": space = "MNI" + + # Define parcellation and label file names parcellation_fname = ( parcellations_dir / "SUIT" / (f"SUIT_{space}Space_{resolution}mm.nii") ) @@ -918,7 +919,7 @@ def _retrieve_suit( parcellations_dir / "SUIT" / (f"SUIT_{space}Space_{resolution}mm.tsv") ) - # check existence of parcellation + # Check existence of parcellation if not (parcellation_fname.exists() and parcellation_lname.exists()): parcellation_fname.parent.mkdir(exist_ok=True, parents=True) logger.info( @@ -933,6 +934,7 @@ def _retrieve_suit( url_SUIT = url_basis + "atl-Anatom_space-SUIT_dseg.nii" url_labels = url_basis + "atl-Anatom.tsv" + # TODO: improve HTTP request / response handling if space == "MNI": logger.info(f"Downloading {url_MNI}") parcellation_download = requests.get(url_MNI) -- 2.52.0 From a55520ec45be3ebcbbf19e9390f7b27a64a99aac Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Wed, 25 Oct 2023 15:29:18 +0200 Subject: [PATCH 78/86] update: fix tests for SUIT MNI --- junifer/data/tests/test_parcellations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/junifer/data/tests/test_parcellations.py b/junifer/data/tests/test_parcellations.py index f48362e9e..be190f1f9 100644 --- a/junifer/data/tests/test_parcellations.py +++ b/junifer/data/tests/test_parcellations.py @@ -355,7 +355,7 @@ def test_retrieve_schaefer_incorrect_yeo_networks(tmp_path: Path) -> None: @pytest.mark.parametrize( "space_key, space", - [("SUIT", "SUIT"), ("MNI", "MNI152Lin6Asym")], + [("SUIT", "SUIT"), ("MNI", "MNI152NLin6Asym")], ) def test_suit(tmp_path: Path, space_key: str, space: str) -> None: """Test SUIT parcellation. -- 2.52.0 From ce362cfb2a0e3be1b704ad497022a9aa39a7db11 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Wed, 25 Oct 2023 16:06:57 +0200 Subject: [PATCH 79/86] fix: correctly raise error when mask spaces don't match in get_mask() --- junifer/data/masks.py | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/junifer/data/masks.py b/junifer/data/masks.py index e9033c60b..88fb60353 100644 --- a/junifer/data/masks.py +++ b/junifer/data/masks.py @@ -202,11 +202,13 @@ def get_mask( # noqa: C901 unionized. ValueError If extra key is provided in addition to mask name in ``masks`` or - if no mask is provided or if ``masks = "inherit"`` but ``extra_input`` - is None or ``mask_item`` is None or ``mask_items``'s value is not in - ``extra_input`` or if callable parameters are passed to non-callable - mask or if parameters are passed to - :func:`nilearn.masking.intersect_masks` when there is only one mask or + if no mask is provided or + if ``masks = "inherit"`` but ``extra_input`` is None or ``mask_item`` + is None or ``mask_items``'s value is not in ``extra_input`` or + if callable parameters are passed to non-callable mask or + if multiple masks are provided and their spaces do not match or + if parameters are passed to :func:`nilearn.masking.intersect_masks` + when there is only one mask or if ``extra_input`` is None when ``target_data``'s space is not MNI. """ @@ -309,20 +311,19 @@ def get_mask( # noqa: C901 # Multiple masks, need intersection / union if len(all_masks) > 1: + # Filter out "inherit" and make a set for spaces + filtered_spaces = set(filter(lambda x: x != "inherit", all_spaces)) # Intersect / union of masks only if all masks are in the same space - if len(set(all_spaces)) == 1: + if len(filtered_spaces) == 1: mask_img = intersect_masks(all_masks, **intersect_params) else: - # Check for inherited masks with target image in MNI space - if "inherit" in all_spaces and target_data["space"].startswith( - "MNI" - ): - mask_img = intersect_masks(all_masks, **intersect_params) - else: - raise_error( - msg="Masks are in different spaces, unable to merge.", - klass=RuntimeError, - ) + raise_error( + msg=( + f"Masks are in different spaces: {filtered_spaces}, " + "unable to merge." + ), + klass=RuntimeError, + ) # Single mask else: if len(intersect_params) > 0: -- 2.52.0 From 19a8b5462cfd7edb437ae57e6fb86e18c6ce968a Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Wed, 25 Oct 2023 16:07:31 +0200 Subject: [PATCH 80/86] update: fix tests for get_mask() multiple --- junifer/data/tests/test_masks.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/junifer/data/tests/test_masks.py b/junifer/data/tests/test_masks.py index 07c2b2eee..3e8928ef4 100644 --- a/junifer/data/tests/test_masks.py +++ b/junifer/data/tests/test_masks.py @@ -410,14 +410,6 @@ def test_get_mask_inherit() -> None: ["GM_prob0.2", "compute_brain_mask"], {"threshold": 0.2}, ), - ( - [ - "GM_prob0.2", - "compute_brain_mask", - "fetch_icbm152_brain_gm_mask", - ], - {"threshold": 1, "connected": True}, - ), ], ) def test_get_mask_multiple( -- 2.52.0 From 046f3208da4ed44e0e6f1668ff440267b4e60798 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Wed, 25 Oct 2023 16:18:30 +0200 Subject: [PATCH 81/86] update: fix test for error check for get_mask() mulitple --- junifer/data/tests/test_masks.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/junifer/data/tests/test_masks.py b/junifer/data/tests/test_masks.py index 3e8928ef4..1a530f7ae 100644 --- a/junifer/data/tests/test_masks.py +++ b/junifer/data/tests/test_masks.py @@ -484,11 +484,13 @@ def test_get_mask_multiple_incorrect_space() -> None: with SPMAuditoryTestingDataGrabber() as dg: input = dg["sub001"] input = reader.fit_transform(input) - # Edit BOLD space to trigger error - input["BOLD"]["space"] = "SUIT" with pytest.raises(RuntimeError, match="unable to merge."): get_mask( - masks=["GM_prob0.2", "compute_brain_mask"], + masks=[ + "GM_prob0.2", + "compute_brain_mask", + "fetch_icbm152_brain_gm_mask", + ], target_data=input["BOLD"], ) -- 2.52.0 From eabe5a0674902c673d8eee354f7cc2d4f3e614b6 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Wed, 25 Oct 2023 16:18:58 +0200 Subject: [PATCH 82/86] update: only warp final mask if target space is native in get_mask() --- junifer/data/masks.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/junifer/data/masks.py b/junifer/data/masks.py index 88fb60353..2837130cc 100644 --- a/junifer/data/masks.py +++ b/junifer/data/masks.py @@ -209,7 +209,7 @@ def get_mask( # noqa: C901 if multiple masks are provided and their spaces do not match or if parameters are passed to :func:`nilearn.masking.intersect_masks` when there is only one mask or - if ``extra_input`` is None when ``target_data``'s space is not MNI. + if ``extra_input`` is None when ``target_data``'s space is native. """ # Get the min of the voxels sizes and use it as the resolution @@ -334,8 +334,8 @@ def get_mask( # noqa: C901 ) mask_img = all_masks[0] - # Warp mask if not in MNI space - if not target_data["space"].startswith("MNI"): + # Warp mask if target data is native + if target_data["space"] == "native": # Check for extra inputs if extra_input is None: raise_error( -- 2.52.0 From 5c610b6ef14418cdae619dc38223d708a0c55730 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Wed, 25 Oct 2023 16:32:58 +0200 Subject: [PATCH 83/86] update: only warp final parcellation if target space is native in get_parcellation() --- junifer/data/parcellations.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/junifer/data/parcellations.py b/junifer/data/parcellations.py index ee0f73efd..25b690691 100644 --- a/junifer/data/parcellations.py +++ b/junifer/data/parcellations.py @@ -233,7 +233,7 @@ def get_parcellation( RuntimeError If parcellations are in different spaces and they need to be merged. ValueError - If ``extra_input`` is None when ``target_data``'s space is not MNI. + If ``extra_input`` is None when ``target_data``'s space is native. """ # Get the min of the voxels sizes and use it as the resolution @@ -279,8 +279,8 @@ def get_parcellation( klass=RuntimeError, ) - # Warp parcellation if not in MNI space - if not target_data["space"].startswith("MNI"): + # Warp parcellation if target data is native + if target_data["space"] == "native": # Check for extra inputs if extra_input is None: raise_error( -- 2.52.0 From 857e79ec2379f3ce42b4b12c9a16bd4d9be53f01 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Wed, 25 Oct 2023 16:33:52 +0200 Subject: [PATCH 84/86] update: only transform final coordinates if target space is native in get_coordinates() --- junifer/data/coordinates.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/junifer/data/coordinates.py b/junifer/data/coordinates.py index efd5e48b4..9bd903fa2 100644 --- a/junifer/data/coordinates.py +++ b/junifer/data/coordinates.py @@ -224,14 +224,14 @@ def get_coordinates( Raises ------ ValueError - If ``extra_input`` is None when ``target_data``'s space is not MNI. + If ``extra_input`` is None when ``target_data``'s space is native. """ # Load the coordinates seeds, labels, _ = load_coordinates(name=coords) - # Transform coordinate if not in MNI space - if not target_data["space"].startswith("MNI"): + # Transform coordinate if target data is native + if target_data["space"] == "native": # Check for extra inputs if extra_input is None: raise_error( -- 2.52.0 From 8b85612b76edb83d664e1239762d39d35e4fbd62 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Wed, 25 Oct 2023 16:58:42 +0200 Subject: [PATCH 85/86] fix: update correct spaces for testing datagrabbers --- junifer/testing/datagrabbers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/junifer/testing/datagrabbers.py b/junifer/testing/datagrabbers.py index ee918b844..ffb744cfd 100644 --- a/junifer/testing/datagrabbers.py +++ b/junifer/testing/datagrabbers.py @@ -57,7 +57,7 @@ class OasisVBMTestingDataGrabber(BaseDataGrabber): i_sub = int(subject.split("-")[1]) - 1 out["VBM_GM"] = { "path": Path(self._dataset.gray_matter_maps[i_sub]), - "space": "MNI152Lin6Asym", + "space": "MNI152Lin", } return out @@ -144,7 +144,7 @@ class SPMAuditoryTestingDataGrabber(BaseDataGrabber): anat_fname = self.datadir / f"{subject}_T1w.nii.gz" nib.save(fmri_img, fmri_fname) nib.save(anat_img, anat_fname) - out["BOLD"] = {"path": fmri_fname, "space": "MNI152Lin6Asym"} + out["BOLD"] = {"path": fmri_fname, "space": "MNI152Lin"} out["T1w"] = {"path": anat_fname, "space": "native"} return out -- 2.52.0 From 02494a77868939e8bc8d61504ecea5a38aa26fbc Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Wed, 25 Oct 2023 17:13:51 +0200 Subject: [PATCH 86/86] update: modernize run_compute_parcel_mean.py example --- examples/run_compute_parcel_mean.py | 86 +++++++++++++---------------- 1 file changed, 39 insertions(+), 47 deletions(-) diff --git a/examples/run_compute_parcel_mean.py b/examples/run_compute_parcel_mean.py index 657b128c1..0a82dab96 100644 --- a/examples/run_compute_parcel_mean.py +++ b/examples/run_compute_parcel_mean.py @@ -2,18 +2,21 @@ Computer Parcel Aggregation. ============================ -This example uses a ParcelAggregation marker to compute the mean of each parcel -using the Schaefer parcellations (100 rois, 7 Yeo networks) for both a 3D and -4D nifti +This example uses the ``ParcelAggregation`` marker to compute the mean of each +parcel using the Schaefer parcellations (100 rois, 7 Yeo networks) for both 3D +and 4D NIfTI. -Authors: Federico Raimondo +Authors: Federico Raimondo, Synchon Mandal License: BSD 3 clause """ -import nilearn - -from junifer.markers.parcel_aggregation import ParcelAggregation +from junifer.testing.datagrabbers import ( + OasisVBMTestingDataGrabber, + SPMAuditoryTestingDataGrabber, +) +from junifer.datareader import DefaultDataReader +from junifer.markers import ParcelAggregation from junifer.utils import configure_logging @@ -21,45 +24,34 @@ from junifer.utils import configure_logging # Set the logging level to info to see extra information configure_logging(level="INFO") +############################################################################### +# Perform parcel aggregation on VBM GM data (3D) from OASIS dataset +with OasisVBMTestingDataGrabber() as dg: + # Get the first element + element = dg.get_elements()[0] + # Read the element + element_data = DefaultDataReader().fit_transform(dg[element]) + # Initialize marker + marker = ParcelAggregation(parcellation="Schaefer100x7", method="mean") + # Compute feature + feature = marker.fit_transform(element_data) + # Print the output + print(feature.keys()) + print(feature["VBM_GM"]["data"].shape) # Shape is (1 x parcels) ############################################################################### -# Load the VBM GM data (3d): -# - Fetch the Oasis dataset -oasis_dataset = nilearn.datasets.fetch_oasis_vbm(n_subjects=1) -vbm_fname = oasis_dataset.gray_matter_maps[0] -vbm_img = nilearn.image.load_img(vbm_fname) - -############################################################################### -# Load the functional data (4d): -# - Fetch the SPM auditory dataset -# - Concatenate the functional data into one 4D image -s_func_data = nilearn.datasets.fetch_spm_auditory() -fmri_img = nilearn.image.concat_imgs(s_func_data.func) - -############################################################################### -# Define the marker -marker = ParcelAggregation(parcellation="Schaefer100x7", method="mean") - -############################################################################### -# Prepare the input -input = { - "BOLD": { - "data": fmri_img, "meta": {"element": "subject1"}, "space": "MNI" - }, - "VBM_GM": { - "data": vbm_img, "meta": {"element": "subject1"}, "space": "MNI" - }, -} - -############################################################################### -# Fit transform the data -out = marker.fit_transform(input) - -############################################################################### -# Check the results - -print(out.keys()) -print(out["VBM_GM"]["data"].shape) # Shape is (1 x parcels) - -print(out.keys()) -print(out["BOLD"]["data"].shape) # Shape is (timepoints x parcels) +# Perform parcel aggregation on BOLD data (4D) from SPM Auditory dataset +with SPMAuditoryTestingDataGrabber() as dg: + # Get the first element + element = dg.get_elements()[0] + # Read the element + element_data = DefaultDataReader().fit_transform(dg[element]) + # Initialize marker + marker = ParcelAggregation( + parcellation="Schaefer100x7", method="mean", on="BOLD" + ) + # Compute feature + feature = marker.fit_transform(element_data) + # Print the output + print(feature.keys()) + print(feature["BOLD"]["data"].shape) # Shape is (timepoints x parcels) -- 2.52.0