From 0cc64887a7efaf19d8965f01c762d950c08b8553 Mon Sep 17 00:00:00 2001 From: Fede Date: Thu, 26 Jan 2023 12:15:15 +0100 Subject: [PATCH 01/12] Implementation + simple test --- junifer/data/masks.py | 138 ++++++++++++++++++++++++------- junifer/data/tests/test_masks.py | 85 +++++++++++++++++-- 2 files changed, 187 insertions(+), 36 deletions(-) diff --git a/junifer/data/masks.py b/junifer/data/masks.py index 754089796..d7a612430 100644 --- a/junifer/data/masks.py +++ b/junifer/data/masks.py @@ -22,6 +22,7 @@ from nilearn.masking import ( compute_brain_mask, compute_background_mask, compute_epi_mask, + intersect_masks, ) from nilearn.datasets import fetch_icbm152_brain_gm_mask from nilearn.image import resample_to_img @@ -37,13 +38,21 @@ if TYPE_CHECKING: _masks_path = Path(__file__).parent / "masks" -def _fetch_icbm152_brain_gm_mask(target_img: "Nifti1Image", **kwargs): +def _fetch_icbm152_brain_gm_mask( + target_img: "Nifti1Image", + extra_data: Optional[Dict[str, Any]] = None, + **kwargs, +): """Fetch ICBM152 brain mask and resample. Parameters ---------- target_img : nibabel.Nifti1Image The image to which the mask will be resampled. + extra_data : dict, optional + The other fields in the data object. Useful for accessing other data + kinds that needs to be used in the computation of masks. Default is + None. This parameter is not used in this function. **kwargs : dict Keyword arguments to be passed to :func:`nilearn.datasets.fetch_icbm152_brain_gm_mask`. @@ -66,6 +75,9 @@ data. The built-in masks are files that are shipped with the package in the data/masks directory. The user can also register their own masks. + +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"}, @@ -147,19 +159,24 @@ def list_masks() -> List[str]: def get_mask( - mask: Union[str, Dict], + masks: Union[str, Dict, List[Union[Dict, str]]], target_data: Dict[str, Any], + extra_data: Optional[Dict[str, Any]] = None, ) -> "Nifti1Image": """Get mask, tailored for the target image. Parameters ---------- - masks : str or dict + masks : str or list of dict or str The name of the mask, or the name of a callable mask and the parameters of the mask. target_data : dict The corresponding item of the data object to which the mask will be applied. + extra_data : dict, optional + The other fields in the data object. Useful for accessing other data + kinds that needs to be used in the computation of masks. Default is + None. Returns ------- @@ -168,37 +185,100 @@ def get_mask( """ # Get the min of the voxels sizes and use it as the resolution target_img = target_data["data"] + inherited_mask_item = target_data.get("mask_item", None) resolution = np.min(target_img.header.get_zooms()[:3]) - if isinstance(mask, dict): - if len(mask) != 1: - raise_error( - "The mask dictionary must have only one key, " - "the name of the mask." - ) - mask_name = list(mask.keys())[0] - mask_params = mask[mask_name] - else: - mask_name = mask - mask_params = None + if not isinstance(masks, list): + masks = [masks] - mask_object, _ = load_mask( - mask_name, path_only=False, resolution=resolution - ) - if callable(mask_object): - if mask_params is None: - mask_params = {} - mask_img = mask_object(target_img, **mask_params) - else: # Mask is a Nifti1Image - if mask_params is not None: - raise_error("Cannot pass callable params to a non-callable mask.") - mask_img = resample_to_img( - mask_object, - target_img, - interpolation="nearest", - copy=True, + # Check that dicts have only one key + invalid_elements = [ + x for x in masks if isinstance(x, dict) and len(x) != 1 + ] + if len(invalid_elements) > 0: + raise_error( + "Each of the masks dictionary must have only one key, " + "the name of the mask. The following dictionaries are invalid: " + f"{invalid_elements}" ) + # Check params for the intersection function + intersect_params = {} + true_masks = [] + for t_mask in masks: + if isinstance(t_mask, dict): + if "threshold" in t_mask: + intersect_params["threshold"] = t_mask["threshold"] + continue + elif "connected" in t_mask: + intersect_params["connected"] = t_mask["connected"] + continue + # All the other elements are masks + true_masks.append(t_mask) + + if len(true_masks) == 0: + raise_error( + "No mask was passed. At least one mask is required." + ) + # Get all the masks + all_masks = [] + for t_mask in true_masks: + if isinstance(t_mask, dict): + mask_name = list(t_mask.keys())[0] + mask_params = t_mask[mask_name] + else: + mask_name = t_mask + mask_params = None + + if mask_name == "inherit": + if extra_data is None: + raise_error( + "Cannot inherit mask from another data item " + "because no extra data was passed." + ) + 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)." + ) + if inherited_mask_item not in extra_data: + raise_error( + "Cannot inherit mask from another data item " + f"because the item ({inherited_mask_item}) does not exist." + ) + mask_img = extra_data[inherited_mask_item]["data"] + else: + mask_object, _ = load_mask( + mask_name, path_only=False, resolution=resolution + ) + if callable(mask_object): + if mask_params is None: + mask_params = {} + mask_img = mask_object(target_img, **mask_params) + else: # Mask is a Nifti1Image + if mask_params is not None: + raise_error( + "Cannot pass callable params to a non-callable mask." + ) + mask_img = resample_to_img( + mask_object, + target_img, + interpolation="nearest", + copy=True, + ) + all_masks.append(mask_img) + if len(all_masks) > 1: + mask_img = intersect_masks(all_masks, **intersect_params) + else: + if len(intersect_params) > 0: + # Yes, I'm this strict! + raise_error( + "Cannot pass parameters to the intersection function " + "when there is only one mask." + ) + mask_img = all_masks[0] + return mask_img diff --git a/junifer/data/tests/test_masks.py b/junifer/data/tests/test_masks.py index ec3739612..3db8ae867 100644 --- a/junifer/data/tests/test_masks.py +++ b/junifer/data/tests/test_masks.py @@ -62,7 +62,9 @@ def test_register_mask_already_registered() -> None: name="testmask", mask_path="testmask.nii.gz", ) - assert load_mask("testmask", path_only=True)[1].name == "testmask.nii.gz" + out = load_mask("testmask", path_only=True) + assert out[1] is not None + assert out[1].name == "testmask.nii.gz" # Try registering again with pytest.raises(ValueError, match=r"already registered."): @@ -76,7 +78,9 @@ def test_register_mask_already_registered() -> None: overwrite=True, ) - assert load_mask("testmask", path_only=True)[1].name == "testmask2.nii.gz" + out = load_mask("testmask", path_only=True) + assert out[1] is not None + assert out[1].name == "testmask2.nii.gz" @pytest.mark.parametrize( @@ -116,6 +120,7 @@ def test_register_mask( # Load registered mask _, fname = load_mask(name=name, path_only=True) # Check values for registered mask + assert fname is not None assert fname.name == f"{name}.nii.gz" @@ -152,6 +157,7 @@ def test_vickery_patil() -> None: mask.header["pixdim"][1:4], [1.5, 1.5, 1.5] # type: ignore ) + 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) @@ -159,6 +165,7 @@ def test_vickery_patil() -> None: 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" ) @@ -168,6 +175,7 @@ def test_vickery_patil() -> None: 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" with pytest.raises(ValueError, match=r"find a Vickery-Patil mask "): @@ -182,7 +190,7 @@ def test_get_mask() -> None: input = reader.fit_transform(input) vbm_gm = input["VBM_GM"] vbm_gm_img = vbm_gm["data"] - mask = get_mask(mask="GM_prob0.2", target_data=vbm_gm) + mask = get_mask(masks="GM_prob0.2", target_data=vbm_gm) assert mask.shape == vbm_gm_img.shape assert_array_equal(mask.affine, vbm_gm_img.affine) @@ -210,7 +218,7 @@ def test_mask_callable() -> None: input = reader.fit_transform(input) vbm_gm = input["VBM_GM"] vbm_gm_img = vbm_gm["data"] - mask = get_mask(mask="identity", target_data=vbm_gm) + mask = get_mask(masks="identity", target_data=vbm_gm) assert_array_equal(mask.get_fdata(), vbm_gm_img.get_fdata()) @@ -224,11 +232,48 @@ def test_get_mask_errors() -> None: input = dg["sub-01"] input = reader.fit_transform(input) vbm_gm = input["VBM_GM"] + # Test wrong masks definitions (more than one key per dict) with pytest.raises(ValueError, match=r"only one key"): - get_mask(mask={"GM_prob0.2": {}, "Other": {}}, target_data=vbm_gm) + get_mask(masks={"GM_prob0.2": {}, "Other": {}}, target_data=vbm_gm) + # Test wrong masks definitions (pass paramaeters to non-callable mask) with pytest.raises(ValueError, match=r"callable params"): - get_mask(mask={"GM_prob0.2": {"param": 1}}, target_data=vbm_gm) + get_mask(masks={"GM_prob0.2": {"param": 1}}, target_data=vbm_gm) + + # Pass only parametesr to the intersection function + with pytest.raises( + ValueError, match=r" At least one mask is required." + ): + get_mask(masks={"threshold": 1}, target_data=vbm_gm) + + # Pass parameters to the intersection function when only one mask + with pytest.raises( + ValueError, match=r"parameters to the intersection" + ): + get_mask( + masks=["GM_prob0.2", {"threshold": 1}], target_data=vbm_gm + ) + + # Test "inherited" masks errors + + # 1) No extra_data parameter + with pytest.raises(ValueError, match=r"no extra data was passed"): + get_mask(masks="inherit", target_data=vbm_gm) + + extra_data = {"VBM_MASK": {}} + + # 2) No mask_item key in target_data + with pytest.raises(ValueError, match=r"no mask item was specified"): + get_mask( + masks="inherit", target_data=vbm_gm, extra_data=extra_data + ) + + # 3) mask_item not in extra data + with pytest.raises(ValueError, match=r"does not exist"): + vbm_gm["mask_item"] = "wrong" + get_mask( + masks="inherit", target_data=vbm_gm, extra_data=extra_data + ) @pytest.mark.parametrize( @@ -277,7 +322,7 @@ def test_nilearn_compute_masks( else: mask_spec = {mask_name: params} - mask = get_mask(mask=mask_spec, target_data=bold) + mask = get_mask(masks=mask_spec, target_data=bold) assert_array_equal(mask.affine, bold_img.affine) @@ -293,3 +338,29 @@ def test_nilearn_compute_masks( copy=True, ) assert_array_equal(mask.get_fdata(), ni_mask.get_fdata()) + + +def test_get_mask_inherit() -> None: + """Test using the inherit mask functionality.""" + reader = DefaultDataReader() + with SPMAuditoryTestingDatagrabber() as dg: + input = dg["sub001"] + input = reader.fit_transform(input) + # Compute brain mask using nilearn + gm_mask = compute_brain_mask(input["BOLD"]["data"], threshold=0.2) + + # Get mask using the compute_brain_mask function + mask1 = get_mask( + masks={"compute_brain_mask": {"threshold": 0.2}}, + target_data=input["BOLD"]) + + # Now get the mask using the inherit functionality, passing the + # computed mask as extra data + extra_data = {"BOLD_MASK": {"data": gm_mask}} + input["BOLD"]["mask_item"] = "BOLD_MASK" + mask2 = get_mask( + masks="inherit", target_data=input["BOLD"], extra_data=extra_data + ) + + # Both masks should be equal + assert_array_equal(mask1.get_fdata(), mask2.get_fdata()) -- 2.52.0 From cbc781fb3fa1242c24ed3435f1bd612eb55100d0 Mon Sep 17 00:00:00 2001 From: Fede Date: Thu, 26 Jan 2023 15:26:02 +0100 Subject: [PATCH 02/12] Making Leo happy... --- docs/using/index.rst | 16 ++++- docs/using/masks.rst | 67 +++++++++++++++++++ junifer/data/masks.py | 4 +- junifer/markers/ets_rss.py | 16 ++--- junifer/markers/falff/falff_parcels.py | 14 ++-- junifer/markers/falff/falff_spheres.py | 16 ++--- ...ossparcellation_functional_connectivity.py | 16 ++--- .../edge_functional_connectivity_parcels.py | 14 ++-- .../edge_functional_connectivity_spheres.py | 16 ++--- .../functional_connectivity_base.py | 14 ++-- .../functional_connectivity_parcels.py | 14 ++-- .../functional_connectivity_spheres.py | 16 ++--- .../test_functional_connectivity_base.py | 6 +- junifer/markers/parcel_aggregation.py | 18 ++--- junifer/markers/reho/reho_parcels.py | 16 ++--- junifer/markers/reho/reho_spheres.py | 16 ++--- junifer/markers/sphere_aggregation.py | 18 ++--- junifer/markers/tests/test_collection.py | 14 ++-- junifer/markers/tests/test_marker_utils.py | 3 +- .../markers/tests/test_parcel_aggregation.py | 4 +- .../markers/tests/test_sphere_aggregation.py | 2 +- junifer/markers/utils.py | 20 +++--- junifer/preprocess/base.py | 12 +++- .../confounds/fmriprep_confound_remover.py | 56 +++++++++------- .../tests/test_fmriprep_confound_remover.py | 62 ++++++++++++++++- 25 files changed, 315 insertions(+), 155 deletions(-) create mode 100644 docs/using/masks.rst diff --git a/docs/using/index.rst b/docs/using/index.rst index 75ebdd027..c9314f117 100644 --- a/docs/using/index.rst +++ b/docs/using/index.rst @@ -15,4 +15,18 @@ the results. Finally, we will show how to use the ``queue`` command to interact codeless running - queueing \ No newline at end of file + queueing + + +.. _using_components: + +Using junifer common components +=============================== + +The following sections explains common components of junifer that can be used across many steps of the pipeline. + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + masks \ No newline at end of file diff --git a/docs/using/masks.rst b/docs/using/masks.rst new file mode 100644 index 000000000..f6601be1b --- /dev/null +++ b/docs/using/masks.rst @@ -0,0 +1,67 @@ +.. include:: ../links.inc + +.. _using_masks: + +Masks +===== + +Masks are essentially boolean arrays that are used to constraint the extraction of features to voxels that are +meaningful. For example, in an fMRI imaging study, the mask can be used to constrain the extraction of features to +voxels that contain a certain ratio of gray matter, ensuring that the features are not extracted from voxels that +contain mostly white matter or cerebrospinal fluid, which could add noise to the BOLD signal. + +Junifer provides a number of built-in masks, which can be listed using the :func:`junifer.data.masks.list_masks`. Some +masks are images, while other masks can be computed using :ref:`nilearn` functions. + +For markers and steps that accept `masks` as an argument, the mask can be specified as a string, which will be the +name of a built-in mask, or as a dictionary in which the _only_ key is the built-in mask name and the value is a +dictionary of keyword arguments to pass to the mask function. + +For example, the following is a valid mask specification that specified the `GM_prob0.2` mask. + +.. code-block:: yaml + + masks: GM_prob0.2 + +The following is a valid mask specification that specifies the `compute_brain_mask` mask (function from nilearn), +with a threshold of 0.5. + +.. code-block:: yaml + + masks: + compute_brain_mask: + threshold: 0.5 + +Furthermore, junifer allows to combine several masks using :func:`nilearn.masking.intersect_masks`. This is done by +specifying a list of masks, where each mask is a string or dictionary as described above. For example, the following +is a valid mask specification that specifies the intersection of the `GM_prob0.2` and `compute_brain_mask` masks. + +.. code-block:: yaml + + masks: + - GM_prob0.2 + - compute_brain_mask: + threshold: 0.5 + +We can also specify the arguments of :func:`nilearn.masking.intersect_masks` (``threshold`` and ``connected``). The +following example combines the same masks as the previous one, but computing the full intersection. + + +.. code-block:: yaml + + masks: + - GM_prob0.2 + - compute_brain_mask: + threshold: 0.5 + - threshold: 1 # intersection + +Alternatively, we can also compute the union, even if the voxels do not form a connected component: + +.. code-block:: yaml + + masks: + - GM_prob0.2 + - compute_brain_mask: + threshold: 0.5 + - threshold: 0 # union + - connected: False # keep disconnected components \ No newline at end of file diff --git a/junifer/data/masks.py b/junifer/data/masks.py index d7a612430..ae64840f4 100644 --- a/junifer/data/masks.py +++ b/junifer/data/masks.py @@ -167,9 +167,9 @@ def get_mask( Parameters ---------- - masks : str or list of dict or str + masks : str, dict or list of dict or str The name of the mask, or the name of a callable mask and the parameters - of the mask. + of the mask as a dictionary. Several masks can be passed as a list. target_data : dict The corresponding item of the data object to which the mask will be applied. diff --git a/junifer/markers/ets_rss.py b/junifer/markers/ets_rss.py index f82c82343..d52d8269c 100644 --- a/junifer/markers/ets_rss.py +++ b/junifer/markers/ets_rss.py @@ -32,10 +32,10 @@ class RSSETSMarker(BaseMarker): agg_method_params : dict, optional Parameters to pass to the aggregation function. Check valid options in :func:`junifer.stats.get_aggfunc_by_name` (default None). - mask : str, optional - The name of the mask to apply to regions before extracting signals. - Check valid options by calling :func:`junifer.data.masks.list_masks` - (default None). + masks : str, dict or list of dict or str, optional + The specification of the masks to apply to regions before extracting + signals. Check :ref:`Using Masks ` for more details. + If None, will not apply any mask (default None). name : str, optional The name of the marker. If None, will use the class name (default None). @@ -49,13 +49,13 @@ class RSSETSMarker(BaseMarker): parcellation: Union[str, List[str]], agg_method: str = "mean", agg_method_params: Optional[Dict] = None, - mask: Union[str, Dict, None] = None, + masks: Union[str, Dict, List[Union[Dict, str]], None] = None, name: Optional[str] = None, ) -> None: self.parcellation = parcellation self.agg_method = agg_method self.agg_method_params = agg_method_params - self.mask = mask + self.masks = masks super().__init__(name=name) def get_valid_inputs(self) -> List[str]: @@ -127,11 +127,11 @@ class RSSETSMarker(BaseMarker): parcellation=self.parcellation, method=self.agg_method, method_params=self.agg_method_params, - mask=self.mask, + masks=self.masks, ) # Compute the parcel aggregation out = parcel_aggregation.compute(input=input, extra_input=extra_input) - edge_ts = _ets(out["data"]) + edge_ts, _ = _ets(out["data"]) # Compute the RSS out["data"] = np.sum(edge_ts**2, 1) ** 0.5 # Set correct column label diff --git a/junifer/markers/falff/falff_parcels.py b/junifer/markers/falff/falff_parcels.py index 25c3e8962..e61b3f02e 100644 --- a/junifer/markers/falff/falff_parcels.py +++ b/junifer/markers/falff/falff_parcels.py @@ -35,10 +35,10 @@ class AmplitudeLowFrequencyFluctuationParcels( use_afni : bool, optional Whether to use AFNI for computing. If None, will use AFNI only if available (default None). - mask : str, optional - The name of the mask to apply to regions before extracting signals. - Check valid options by calling :func:`junifer.data.masks.list_masks` - (default None). + masks : str, dict or list of dict or str, optional + The specification of the masks to apply to regions before extracting + signals. Check :ref:`Using Masks ` for more details. + If None, will not apply any mask (default None). method : str, optional The method to perform aggregation using. Check valid options in :func:`junifer.stats.get_aggfunc_by_name` (default "mean"). @@ -70,13 +70,13 @@ class AmplitudeLowFrequencyFluctuationParcels( lowpass: float = 0.1, tr: Optional[float] = None, use_afni: Optional[bool] = None, - mask: Union[str, Dict, None] = None, + masks: Union[str, Dict, List[Union[Dict, str]], None] = None, method: str = "mean", method_params: Optional[Dict] = None, name: Optional[str] = None, ) -> None: self.parcellation = parcellation - self.mask = mask + self.masks = masks self.method = method self.method_params = method_params super().__init__( @@ -116,7 +116,7 @@ class AmplitudeLowFrequencyFluctuationParcels( parcellation=self.parcellation, method=self.method, method_params=self.method_params, - mask=self.mask, + masks=self.masks, on="fALFF", ) diff --git a/junifer/markers/falff/falff_spheres.py b/junifer/markers/falff/falff_spheres.py index e614e5320..d6b375ec3 100644 --- a/junifer/markers/falff/falff_spheres.py +++ b/junifer/markers/falff/falff_spheres.py @@ -5,7 +5,7 @@ # Kaustubh R. Patil # License: AGPL -from typing import Dict, Optional, Union +from typing import Dict, Optional, Union, List from ...api.decorators import register_marker from .. import SphereAggregation @@ -39,10 +39,10 @@ class AmplitudeLowFrequencyFluctuationSpheres( use_afni : bool, optional Whether to use AFNI for computing. If None, will use AFNI only if available (default None). - mask : str, optional - The name of the mask to apply to regions before extracting signals. - Check valid options by calling :func:`junifer.data.masks.list_masks` - (default None). + masks : str, dict or list of dict or str, optional + The specification of the masks to apply to regions before extracting + signals. Check :ref:`Using Masks ` for more details. + If None, will not apply any mask (default None). method : str, optional The method to perform aggregation using. Check valid options in :func:`junifer.stats.get_aggfunc_by_name` (default "mean"). @@ -75,14 +75,14 @@ class AmplitudeLowFrequencyFluctuationSpheres( lowpass: float = 0.1, tr: Optional[float] = None, use_afni: Optional[bool] = None, - mask: Union[str, Dict, None] = None, + masks: Union[str, Dict, List[Union[Dict, str]], None] = None, method: str = "mean", method_params: Optional[Dict] = None, name: Optional[str] = None, ) -> None: self.coords = coords self.radius = radius - self.mask = mask + self.masks = masks self.method = method self.method_params = method_params super().__init__( @@ -123,7 +123,7 @@ class AmplitudeLowFrequencyFluctuationSpheres( radius=self.radius, method=self.method, method_params=self.method_params, - mask=self.mask, + masks=self.masks, on="fALFF", ) diff --git a/junifer/markers/functional_connectivity/crossparcellation_functional_connectivity.py b/junifer/markers/functional_connectivity/crossparcellation_functional_connectivity.py index 74baa08c9..fa7389cfb 100644 --- a/junifer/markers/functional_connectivity/crossparcellation_functional_connectivity.py +++ b/junifer/markers/functional_connectivity/crossparcellation_functional_connectivity.py @@ -30,10 +30,10 @@ class CrossParcellationFC(BaseMarker): correlation_method : str, optional Any method that can be passed to :any:`pandas.DataFrame.corr` (default "pearson"). - mask : str, optional - The name of the mask to apply to regions before extracting signals. - Check valid options by calling :func:`junifer.data.masks.list_masks` - (default None). + masks : str, dict or list of dict or str, optional + The specification of the masks to apply to regions before extracting + signals. Check :ref:`Using Masks ` for more details. + If None, will not apply any mask (default None). name : str, optional The name of the marker. If None, will use the class name (default None). @@ -47,7 +47,7 @@ class CrossParcellationFC(BaseMarker): parcellation_two: str, aggregation_method: str = "mean", correlation_method: str = "pearson", - mask: Union[str, Dict, None] = None, + masks: Union[str, Dict, List[Union[Dict, str]], None] = None, name: Optional[str] = None, ) -> None: if parcellation_one == parcellation_two: @@ -58,7 +58,7 @@ class CrossParcellationFC(BaseMarker): self.parcellation_two = parcellation_two self.aggregation_method = aggregation_method self.correlation_method = correlation_method - self.mask = mask + self.masks = masks super().__init__(on=["BOLD"], name=name) def get_valid_inputs(self) -> List[str]: @@ -129,12 +129,12 @@ class CrossParcellationFC(BaseMarker): parcellation_one_dict = ParcelAggregation( parcellation=self.parcellation_one, method=self.aggregation_method, - mask=self.mask, + masks=self.masks, ).compute(input) parcellation_two_dict = ParcelAggregation( parcellation=self.parcellation_two, method=self.aggregation_method, - mask=self.mask, + masks=self.masks, ).compute(input) parcellated_ts_one = parcellation_one_dict["data"] diff --git a/junifer/markers/functional_connectivity/edge_functional_connectivity_parcels.py b/junifer/markers/functional_connectivity/edge_functional_connectivity_parcels.py index eb02f56c0..d0dc749da 100644 --- a/junifer/markers/functional_connectivity/edge_functional_connectivity_parcels.py +++ b/junifer/markers/functional_connectivity/edge_functional_connectivity_parcels.py @@ -35,10 +35,10 @@ class EdgeCentricFCParcels(FunctionalConnectivityBase): cor_method_params : dict, optional Parameters to pass to the correlation function. Check valid options in :class:`nilearn.connectome.ConnectivityMeasure` (default None). - mask : str, optional - The name of the mask to apply to regions before extracting signals. - Check valid options by calling :func:`junifer.data.masks.list_masks` - (default None). + masks : str, dict or list of dict or str, optional + The specification of the masks to apply to regions before extracting + signals. Check :ref:`Using Masks ` for more details. + If None, will not apply any mask (default None). name : str, optional The name of the marker. If None, will use the class name (default None). @@ -59,7 +59,7 @@ class EdgeCentricFCParcels(FunctionalConnectivityBase): agg_method_params: Optional[Dict] = None, cor_method: str = "covariance", cor_method_params: Optional[Dict] = None, - mask: Optional[str] = None, + masks: Union[str, Dict, List[Union[Dict, str]], None] = None, name: Optional[str] = None, ) -> None: self.parcellation = parcellation @@ -68,7 +68,7 @@ class EdgeCentricFCParcels(FunctionalConnectivityBase): agg_method_params=agg_method_params, cor_method=cor_method, cor_method_params=cor_method_params, - mask=mask, + masks=masks, name=name, ) @@ -78,7 +78,7 @@ class EdgeCentricFCParcels(FunctionalConnectivityBase): parcellation=self.parcellation, method=self.agg_method, method_params=self.agg_method_params, - mask=self.mask, + masks=self.masks, on="BOLD", ) diff --git a/junifer/markers/functional_connectivity/edge_functional_connectivity_spheres.py b/junifer/markers/functional_connectivity/edge_functional_connectivity_spheres.py index 6fcabffbe..7284b068e 100644 --- a/junifer/markers/functional_connectivity/edge_functional_connectivity_spheres.py +++ b/junifer/markers/functional_connectivity/edge_functional_connectivity_spheres.py @@ -4,7 +4,7 @@ # Synchon Mandal # License: AGPL -from typing import Any, Dict, Optional +from typing import Any, Dict, Optional, Union, List from ...api.decorators import register_marker from ..sphere_aggregation import SphereAggregation @@ -37,10 +37,10 @@ class EdgeCentricFCSpheres(FunctionalConnectivityBase): cor_method_params : dict, optional Parameters to pass to the correlation function. Check valid options in :class:`nilearn.connectome.ConnectivityMeasure` (default None). - mask : str, optional - The name of the mask to apply to regions before extracting signals. - Check valid options by calling :func:`junifer.data.masks.list_masks` - (default None). + masks : str, dict or list of dict or str, optional + The specification of the masks to apply to regions before extracting + signals. Check :ref:`Using Masks ` for more details. + If None, will not apply any mask (default None). name : str, optional The name of the marker. By default, it will use KIND_EdgeCentricFCSpheres where KIND is the kind of data it @@ -63,7 +63,7 @@ class EdgeCentricFCSpheres(FunctionalConnectivityBase): agg_method_params: Optional[Dict] = None, cor_method: str = "covariance", cor_method_params: Optional[Dict] = None, - mask: Optional[str] = None, + masks: Union[str, Dict, List[Union[Dict, str]], None] = None, name: Optional[str] = None, ) -> None: self.coords = coords @@ -75,7 +75,7 @@ class EdgeCentricFCSpheres(FunctionalConnectivityBase): agg_method_params=agg_method_params, cor_method=cor_method, cor_method_params=cor_method_params, - mask=mask, + masks=masks, name=name, ) @@ -86,7 +86,7 @@ class EdgeCentricFCSpheres(FunctionalConnectivityBase): radius=self.radius, method=self.agg_method, method_params=self.agg_method_params, - mask=self.mask, + masks=self.masks, on="BOLD", ) bold_aggregated = sphere_aggregation.compute(input) diff --git a/junifer/markers/functional_connectivity/functional_connectivity_base.py b/junifer/markers/functional_connectivity/functional_connectivity_base.py index 6de34ab07..d41b6766e 100644 --- a/junifer/markers/functional_connectivity/functional_connectivity_base.py +++ b/junifer/markers/functional_connectivity/functional_connectivity_base.py @@ -32,10 +32,10 @@ class FunctionalConnectivityBase(BaseMarker): cor_method_params : dict, optional Parameters to pass to the correlation function. Check valid options in :class:`nilearn.connectome.ConnectivityMeasure` (default None). - mask : str, optional - The name of the mask to apply to regions before extracting signals. - Check valid options by calling :func:`junifer.data.masks.list_masks` - (default None). + masks : str, dict or list of dict or str, optional + The specification of the masks to apply to regions before extracting + signals. Check :ref:`Using Masks ` for more details. + If None, will not apply any mask (default None). name : str, optional The name of the marker. If None, will use the class name (default None). @@ -50,7 +50,7 @@ class FunctionalConnectivityBase(BaseMarker): agg_method_params: Optional[Dict] = None, cor_method: str = "covariance", cor_method_params: Optional[Dict] = None, - mask: Union[str, Dict, None] = None, + masks: Union[str, Dict, List[Union[Dict, str]], None] = None, name: Optional[str] = None, ) -> None: self.agg_method = agg_method @@ -62,7 +62,7 @@ class FunctionalConnectivityBase(BaseMarker): self.cor_method_params["empirical"] = self.cor_method_params.get( "empirical", False ) - self.mask = mask + self.masks = masks super().__init__(on="BOLD", name=name) @abstractmethod @@ -134,7 +134,7 @@ class FunctionalConnectivityBase(BaseMarker): # Compute correlation if self.cor_method_params["empirical"]: connectivity = ConnectivityMeasure( - cov_estimator=EmpiricalCovariance(), + cov_estimator=EmpiricalCovariance(), # type: ignore kind=self.cor_method, ) else: diff --git a/junifer/markers/functional_connectivity/functional_connectivity_parcels.py b/junifer/markers/functional_connectivity/functional_connectivity_parcels.py index 2742f0212..86f82c1ec 100644 --- a/junifer/markers/functional_connectivity/functional_connectivity_parcels.py +++ b/junifer/markers/functional_connectivity/functional_connectivity_parcels.py @@ -34,10 +34,10 @@ class FunctionalConnectivityParcels(FunctionalConnectivityBase): cor_method_params : dict, optional Parameters to pass to the correlation function. Check valid options in :class:`nilearn.connectome.ConnectivityMeasure` (default None). - mask : str, optional - The name of the mask to apply to regions before extracting signals. - Check valid options by calling :func:`junifer.data.masks.list_masks` - (default None). + masks : str, dict or list of dict or str, optional + The specification of the masks to apply to regions before extracting + signals. Check :ref:`Using Masks ` for more details. + If None, will not apply any mask (default None). name : str, optional The name of the marker. If None, will use the class name (default None). @@ -51,7 +51,7 @@ class FunctionalConnectivityParcels(FunctionalConnectivityBase): agg_method_params: Optional[Dict] = None, cor_method: str = "covariance", cor_method_params: Optional[Dict] = None, - mask: Union[str, Dict, None] = None, + masks: Union[str, Dict, List[Union[Dict, str]], None] = None, name: Optional[str] = None, ) -> None: self.parcellation = parcellation @@ -60,7 +60,7 @@ class FunctionalConnectivityParcels(FunctionalConnectivityBase): agg_method_params=agg_method_params, cor_method=cor_method, cor_method_params=cor_method_params, - mask=mask, + masks=masks, name=name, ) @@ -70,7 +70,7 @@ class FunctionalConnectivityParcels(FunctionalConnectivityBase): parcellation=self.parcellation, method=self.agg_method, method_params=self.agg_method_params, - mask=self.mask, + masks=self.masks, on="BOLD", ) # Return the 2D timeseries after parcel aggregation diff --git a/junifer/markers/functional_connectivity/functional_connectivity_spheres.py b/junifer/markers/functional_connectivity/functional_connectivity_spheres.py index a43122ef5..2e13a310c 100644 --- a/junifer/markers/functional_connectivity/functional_connectivity_spheres.py +++ b/junifer/markers/functional_connectivity/functional_connectivity_spheres.py @@ -5,7 +5,7 @@ # Synchon Mandal # License: AGPL -from typing import Any, Dict, Optional, Union +from typing import Any, Dict, Optional, Union, List from ...api.decorators import register_marker from ..sphere_aggregation import SphereAggregation @@ -38,10 +38,10 @@ class FunctionalConnectivitySpheres(FunctionalConnectivityBase): cor_method_params : dict, optional Parameters to pass to the correlation function. Check valid options in :class:`nilearn.connectome.ConnectivityMeasure` (default None). - mask : str, optional - The name of the mask to apply to regions before extracting signals. - Check valid options by calling :func:`junifer.data.masks.list_masks` - (default None). + masks : str, dict or list of dict or str, optional + The specification of the masks to apply to regions before extracting + signals. Check :ref:`Using Masks ` for more details. + If None, will not apply any mask (default None). name : str, optional The name of the marker. By default, it will use KIND_FunctionalConnectivitySpheres where KIND is the kind of data it @@ -57,7 +57,7 @@ class FunctionalConnectivitySpheres(FunctionalConnectivityBase): agg_method_params: Optional[Dict] = None, cor_method: str = "covariance", cor_method_params: Optional[Dict] = None, - mask: Union[str, Dict, None] = None, + masks: Union[str, Dict, List[Union[Dict, str]], None] = None, name: Optional[str] = None, ) -> None: self.coords = coords @@ -69,7 +69,7 @@ class FunctionalConnectivitySpheres(FunctionalConnectivityBase): agg_method_params=agg_method_params, cor_method=cor_method, cor_method_params=cor_method_params, - mask=mask, + masks=masks, name=name, ) @@ -80,7 +80,7 @@ class FunctionalConnectivitySpheres(FunctionalConnectivityBase): radius=self.radius, method=self.agg_method, method_params=self.agg_method_params, - mask=self.mask, + masks=self.masks, on="BOLD", ) # Return the 2D timeseries after sphere aggregation diff --git a/junifer/markers/functional_connectivity/tests/test_functional_connectivity_base.py b/junifer/markers/functional_connectivity/tests/test_functional_connectivity_base.py index 634267fad..3be96e7dd 100644 --- a/junifer/markers/functional_connectivity/tests/test_functional_connectivity_base.py +++ b/junifer/markers/functional_connectivity/tests/test_functional_connectivity_base.py @@ -6,10 +6,12 @@ import pytest # done to keep line length 79 -import junifer.markers.functional_connectivity as fc +from junifer.markers.functional_connectivity import ( + functional_connectivity_base as fcb +) def test_base_functional_connectivity_marker_abstractness() -> None: """Test FunctionalConnectivityBase is an abstract base class.""" with pytest.raises(TypeError, match="abstract"): - fc.functional_connectivity_base.FunctionalConnectivityBase() + fcb.FunctionalConnectivityBase() # type: ignore diff --git a/junifer/markers/parcel_aggregation.py b/junifer/markers/parcel_aggregation.py index 6115d53bc..e433c4aee 100644 --- a/junifer/markers/parcel_aggregation.py +++ b/junifer/markers/parcel_aggregation.py @@ -32,10 +32,10 @@ class ParcelAggregation(BaseMarker): method_params : dict, optional Parameters to pass to the aggregation function. Check valid options in :func:`junifer.stats.get_aggfunc_by_name`. - mask : str, optional - The name of the mask to apply to regions before extracting signals. - Check valid options by calling :func:`junifer.data.masks.list_masks` - (default None). + masks : str, dict or list of dict or str, optional + The specification of the masks to apply to regions before extracting + signals. Check :ref:`Using Masks ` for more details. + If None, will not apply any mask (default None). on : {"T1w", "BOLD", "VBM_GM", "VBM_WM", "fALFF", "GCOR", "LCOR"} \ or list of the options, optional The data types to apply the marker to. If None, will work on all @@ -52,7 +52,7 @@ class ParcelAggregation(BaseMarker): parcellation: Union[str, List[str]], method: str, method_params: Optional[Dict[str, Any]] = None, - mask: Union[str, Dict, None] = None, + masks: Union[str, Dict, List[Union[Dict, str]], None] = None, on: Union[List[str], str, None] = None, name: Optional[str] = None, ) -> None: @@ -61,7 +61,7 @@ class ParcelAggregation(BaseMarker): self.parcellation = parcellation self.method = method self.method_params = method_params or {} - self.mask = mask + self.masks = masks super().__init__(on=on, name=name) def get_valid_inputs(self) -> List[str]: @@ -184,9 +184,9 @@ class ParcelAggregation(BaseMarker): img=parcellation_img_res, ) - if self.mask is not None: - logger.debug(f"Masking with {self.mask}") - mask_img = get_mask(mask=self.mask, target_data=input) + if self.masks is not None: + logger.debug(f"Masking with {self.masks}") + mask_img = get_mask(masks=self.masks, target_data=input) parcellation_bin = math_img( "np.logical_and(img, mask)", diff --git a/junifer/markers/reho/reho_parcels.py b/junifer/markers/reho/reho_parcels.py index c18814644..104b9e720 100644 --- a/junifer/markers/reho/reho_parcels.py +++ b/junifer/markers/reho/reho_parcels.py @@ -4,7 +4,7 @@ # License: AGPL -from typing import Any, Dict, Optional, Union +from typing import Any, Dict, Optional, Union, List import numpy as np @@ -74,10 +74,10 @@ class ReHoParcels(ReHoBase): agg_method_params : dict, optional Parameters to pass to the aggregation function. Check valid options in :func:`junifer.stats.get_aggfunc_by_name` (default None). - mask : str, optional - The name of the mask to apply to regions before extracting signals. - Check valid options by calling :func:`junifer.data.masks.list_masks` - (default None). + masks : str, dict or list of dict or str, optional + The specification of the masks to apply to regions before extracting + signals. Check :ref:`Using Masks ` for more details. + If None, will not apply any mask (default None). name : str, optional The name of the marker. If None, it will use the class name (default None). @@ -91,14 +91,14 @@ class ReHoParcels(ReHoBase): reho_params: Optional[Dict] = None, agg_method: str = "mean", agg_method_params: Optional[Dict] = None, - mask: Union[str, Dict, None] = None, + masks: Union[str, Dict, List[Union[Dict, str]], None] = None, name: Optional[str] = None, ) -> None: self.parcellation = parcellation self.reho_params = reho_params self.agg_method = agg_method self.agg_method_params = agg_method_params - self.mask = mask + self.masks = masks super().__init__(use_afni=use_afni, name=name) def compute( @@ -137,7 +137,7 @@ class ReHoParcels(ReHoBase): parcellation=self.parcellation, method=self.agg_method, method_params=self.agg_method_params, - mask=self.mask, + masks=self.masks, on="BOLD", ) # Perform aggregation on reho map diff --git a/junifer/markers/reho/reho_spheres.py b/junifer/markers/reho/reho_spheres.py index 4871477a9..98ad6a96c 100644 --- a/junifer/markers/reho/reho_spheres.py +++ b/junifer/markers/reho/reho_spheres.py @@ -4,7 +4,7 @@ # License: AGPL -from typing import Any, Dict, Optional, Union +from typing import Any, Dict, Optional, Union, List import numpy as np @@ -79,10 +79,10 @@ class ReHoSpheres(ReHoBase): (default None). agg_method_params : dict, optional The parameters to pass to the aggregation method (default None). - mask : str, optional - The name of the mask to apply to regions before extracting signals. - Check valid options by calling :func:`junifer.data.masks.list_masks` - (default None). + masks : str, dict or list of dict or str, optional + The specification of the masks to apply to regions before extracting + signals. Check :ref:`Using Masks ` for more details. + If None, will not apply any mask (default None). name : str, optional The name of the marker. If None, it will use the class name (default None). @@ -97,7 +97,7 @@ class ReHoSpheres(ReHoBase): reho_params: Optional[Dict] = None, agg_method: str = "mean", agg_method_params: Optional[Dict] = None, - mask: Union[str, Dict, None] = None, + masks: Union[str, Dict, List[Union[Dict, str]], None] = None, name: Optional[str] = None, ) -> None: self.coords = coords @@ -105,7 +105,7 @@ class ReHoSpheres(ReHoBase): self.reho_params = reho_params self.agg_method = agg_method self.agg_method_params = agg_method_params - self.mask = mask + self.masks = masks super().__init__(use_afni=use_afni, name=name) def compute( @@ -145,7 +145,7 @@ class ReHoSpheres(ReHoBase): radius=self.radius, method=self.agg_method, method_params=self.agg_method_params, - mask=self.mask, + masks=self.masks, on="BOLD", ) # Perform aggregation on reho map diff --git a/junifer/markers/sphere_aggregation.py b/junifer/markers/sphere_aggregation.py index d25dd5a9e..231440cdb 100644 --- a/junifer/markers/sphere_aggregation.py +++ b/junifer/markers/sphere_aggregation.py @@ -34,10 +34,10 @@ class SphereAggregation(BaseMarker): (default "mean"). method_params : dict, optional The parameters to pass to the aggregation method (default None). - mask : str, optional - The name of the mask to apply to regions before extracting signals. - Check valid options by calling :func:`junifer.data.masks.list_masks` - (default None). + masks : str, dict or list of dict or str, optional + The specification of the masks to apply to regions before extracting + signals. Check :ref:`Using Masks ` for more details. + If None, will not apply any mask (default None). on : {"T1w", "BOLD", "VBM_GM", "VBM_WM", "fALFF", "GCOR", "LCOR"} or \ list of the options, optional The data types to apply the marker to. If None, will work on all @@ -56,7 +56,7 @@ class SphereAggregation(BaseMarker): radius: Optional[float] = None, method: str = "mean", method_params: Optional[Dict[str, Any]] = None, - mask: Union[str, Dict, None] = None, + masks: Union[str, Dict, List[Union[Dict, str]], None] = None, on: Union[List[str], str, None] = None, name: Optional[str] = None, ) -> None: @@ -64,7 +64,7 @@ class SphereAggregation(BaseMarker): self.radius = radius self.method = method self.method_params = method_params or {} - self.mask = mask + self.masks = masks super().__init__(on=on, name=name) def get_valid_inputs(self) -> List[str]: @@ -137,9 +137,9 @@ class SphereAggregation(BaseMarker): ) # Load mask mask_img = None - if self.mask is not None: - logger.debug(f"Masking with {self.mask}") - mask_img = get_mask(mask=self.mask, target_data=input) + if self.masks is not None: + logger.debug(f"Masking with {self.masks}") + mask_img = get_mask(masks=self.masks, target_data=input) # Get seeds and labels coords, out_labels = load_coordinates(name=self.coords) masker = JuniferNiftiSpheresMasker( diff --git a/junifer/markers/tests/test_collection.py b/junifer/markers/tests/test_collection.py index 2a8f1ec7b..d64c4e69b 100644 --- a/junifer/markers/tests/test_collection.py +++ b/junifer/markers/tests/test_collection.py @@ -39,7 +39,7 @@ def test_marker_collection_incorrect_markers() -> None: ), ] with pytest.raises(ValueError, match=r"must have different names"): - MarkerCollection(wrong_markers) + MarkerCollection(wrong_markers) # type: ignore def test_marker_collection() -> None: @@ -62,7 +62,7 @@ def test_marker_collection() -> None: name="gmd_schaefer100x7_trim_mean90", ), ] - mc = MarkerCollection(markers=markers) + mc = MarkerCollection(markers=markers) # type: ignore assert mc._markers == markers assert mc._preprocessing is None assert mc._storage is None @@ -96,7 +96,7 @@ def test_marker_collection() -> None: return input mc2 = MarkerCollection( - markers=markers, + markers=markers, # type: ignore preprocessing=BypassPreprocessing(), datareader=DefaultDataReader(), ) @@ -127,7 +127,7 @@ def test_marker_collection_with_preprocessing() -> None: ), ] mc = MarkerCollection( - markers=markers, + markers=markers, # type: ignore preprocessing=fMRIPrepConfoundRemover(), ) assert mc._markers == markers @@ -173,7 +173,7 @@ def test_marker_collection_storage(tmp_path: Path) -> None: uri = tmp_path / "test_marker_collection_storage.sqlite" storage = SQLiteFeatureStorage(uri=uri) mc = MarkerCollection( - markers=markers, + markers=markers, # type: ignore storage=storage, datareader=DefaultDataReader(), ) @@ -185,7 +185,9 @@ def test_marker_collection_storage(tmp_path: Path) -> None: out = mc.fit(input) assert out is None - mc2 = MarkerCollection(markers=markers, datareader=DefaultDataReader()) + mc2 = MarkerCollection( + markers=markers, # type: ignore + datareader=DefaultDataReader()) mc2.validate(dg) assert mc2._storage is None diff --git a/junifer/markers/tests/test_marker_utils.py b/junifer/markers/tests/test_marker_utils.py index bc4b63d3f..37e93b747 100644 --- a/junifer/markers/tests/test_marker_utils.py +++ b/junifer/markers/tests/test_marker_utils.py @@ -27,13 +27,14 @@ def test_ets() -> None: n_edges = int(n_rois * (n_rois - 1) / 2) # test without labels - edge_ts = _ets(bold_ts) + edge_ts, _ = _ets(bold_ts) assert edge_ts.shape == (n_time, n_edges) # test with labels roi_labels = [f"Label_{x}" for x in range(n_rois)] edge_ts, edge_labels = _ets(bold_ts, roi_labels) assert edge_ts.shape == (n_time, n_edges) + assert edge_labels is not None assert len(edge_labels) == n_edges diff --git a/junifer/markers/tests/test_parcel_aggregation.py b/junifer/markers/tests/test_parcel_aggregation.py index 007eccfe1..c0d5d7e0d 100644 --- a/junifer/markers/tests/test_parcel_aggregation.py +++ b/junifer/markers/tests/test_parcel_aggregation.py @@ -229,7 +229,7 @@ def test_ParcelAggregation_3D_mask() -> None: marker = ParcelAggregation( parcellation="Schaefer100x7", method="mean", - mask="GM_prob0.2", + masks="GM_prob0.2", name="gmd_schaefer100x7_mean", on="VBM_GM", ) # Test passing "on" as a keyword argument @@ -274,7 +274,7 @@ def test_ParcelAggregation_3D_mask_computed() -> None: marker = ParcelAggregation( parcellation="Schaefer100x7", method="mean", - mask={"compute_brain_mask": {"threshold": 0.2}}, + masks={"compute_brain_mask": {"threshold": 0.2}}, name="gmd_schaefer100x7_mean", on="VBM_GM", ) # Test passing "on" as a keyword argument diff --git a/junifer/markers/tests/test_sphere_aggregation.py b/junifer/markers/tests/test_sphere_aggregation.py index 0d373faa5..21b9226cd 100644 --- a/junifer/markers/tests/test_sphere_aggregation.py +++ b/junifer/markers/tests/test_sphere_aggregation.py @@ -159,7 +159,7 @@ def test_SphereAggregation_3D_mask() -> None: method="mean", radius=RADIUS, on="VBM_GM", - mask="GM_prob0.2", + masks="GM_prob0.2", ) input = {"VBM_GM": {"data": img, "meta": {}}} jun_values4d = marker.fit_transform(input)["VBM_GM"]["data"] diff --git a/junifer/markers/utils.py b/junifer/markers/utils.py index 270c582d4..b489befd5 100644 --- a/junifer/markers/utils.py +++ b/junifer/markers/utils.py @@ -7,7 +7,7 @@ # Federico Raimondo # License: AGPL -from typing import Any, Callable, Dict, List, Type, Union +from typing import Any, Callable, Tuple, Dict, List, Type, Union, Optional import numpy as np import pandas as pd @@ -56,8 +56,9 @@ def singleton(cls: Type) -> Type: def _ets( - bold_ts: np.ndarray, roi_names: Union[None, List[str]] = None -) -> np.ndarray: + bold_ts: np.ndarray, + roi_names: Union[None, List[str]] = None, +) -> Tuple[np.ndarray, Optional[List[str]]]: """Compute the edge-wise time series based on BOLD time series. Take a timeseries of brain areas, and calculate timeseries for each @@ -80,9 +81,8 @@ def _ets( edge-wise time series, i.e. estimate of functional connectivity at each time point. edge_names : List[str] - List of edge names corresponding to columns in the - edge-wise time series. This is only returned if the roi_names - are specified. + List of edge names corresponding to columns in the edge-wise time + series. If roi_names are not specified, this is None. References ---------- @@ -102,18 +102,18 @@ def _ets( ets = timeseries[:, u] * timeseries[:, v] # Obtain the corresponding edge labels if specified else return if roi_names is None: - return ets + return ets, None else: if len(roi_names) != n_roi: raise_error( "List of roi names does not correspond " "to the number of ROIs in the timeseries!" ) - roi_names = np.array(roi_names) + _roi_names = np.array(roi_names) edge_names = [ - "~".join([x, y]) for x, y in zip(roi_names[u], roi_names[v]) + "~".join([x, y]) for x, y in zip(_roi_names[u], _roi_names[v]) ] - return ets, list(edge_names) + return ets, edge_names def _correlate_dataframes( diff --git a/junifer/preprocess/base.py b/junifer/preprocess/base.py index cbdc03a44..c9f8b97e9 100644 --- a/junifer/preprocess/base.py +++ b/junifer/preprocess/base.py @@ -115,12 +115,22 @@ class BasePreprocessor(ABC, PipelineStepMixin, UpdateMetaMixin): if type_ in input.keys(): logger.info(f"Computing {type_}") t_input = input[type_] - extra_input = input.copy() + + # Pass the other data types as extra input, removing + # the current type + extra_input = input extra_input.pop(type_) key, t_out = self.preprocess( input=t_input, extra_input=extra_input ) + + # Add the output to the Junifer Data object out[key] = t_out + + # In case we are creating a new type, re-add the original input + if key != type_: + out[type_] = t_input + self.update_meta(out[key], "preprocess") return out diff --git a/junifer/preprocess/confounds/fmriprep_confound_remover.py b/junifer/preprocess/confounds/fmriprep_confound_remover.py index c2d3155aa..f555ab896 100644 --- a/junifer/preprocess/confounds/fmriprep_confound_remover.py +++ b/junifer/preprocess/confounds/fmriprep_confound_remover.py @@ -11,10 +11,11 @@ import numpy as np import pandas as pd from nilearn._utils.niimg_conversions import check_niimg_4d from nilearn.image import clean_img -from nilearn.masking import compute_brain_mask + from ...api.decorators import register_preprocessor from ...utils import logger, raise_error +from ...data import get_mask from ..base import BasePreprocessor @@ -134,11 +135,10 @@ class fMRIPrepConfoundRemover(BasePreprocessor): t_r : float, optional Repetition time, in second (sampling period). If None, it will use t_r from nifti header (default None). - mask_img: Niimg-like object, optional - If provided, signal is only cleaned from voxels inside the mask. - If mask is provided, it should have same shape and affine as imgs. - If not provided, a mask is computed using - :func:`nilearn.masking.compute_brain_mask` (default None). + masks : str, dict or list of dict or str, optional + The specification of the masks to apply to regions before extracting + signals. Check :ref:`Using Masks ` for more details. + If None, will not apply any mask (default None). """ @@ -153,7 +153,7 @@ class fMRIPrepConfoundRemover(BasePreprocessor): low_pass: Optional[float] = None, high_pass: Optional[float] = None, t_r: Optional[float] = None, - mask_img: Optional["Nifti1Image"] = None, + masks: Union[str, Dict, List[Union[Dict, str]], None] = None, ) -> None: """Initialise the class.""" if strategy is None: @@ -169,7 +169,7 @@ class fMRIPrepConfoundRemover(BasePreprocessor): self.low_pass = low_pass self.high_pass = high_pass self.t_r = t_r - self.mask_img = mask_img + self.masks = masks self._valid_components = ["motion", "wm_csf", "global_signal"] self._valid_confounds = ["basic", "power2", "derivatives", "full"] @@ -521,19 +521,20 @@ class fMRIPrepConfoundRemover(BasePreprocessor): raise ValueError(f"Invalid confounds format {t_format}") def _remove_confounds( - self, bold_img: "Nifti1Image", confounds_df: pd.DataFrame + self, + input: Dict[str, Any], + extra_input: Optional[Dict[str, Any]] = None, ) -> Union["Nifti1Image", "Nifti2Image", "MGHImage", List]: """Remove confounds from the BOLD image. Parameters ---------- - bold_img : Niimg-like object - 4D image. The signals in the last dimension are filtered - (see http://nilearn.github.io/manipulating_images/input_output.html - for a detailed description of the valid input types). - confounds_df : pd.DataFrame - Dataframe containing confounds to remove. Number of rows should - correspond to number of volumes in the BOLD image. + input : dict + Dictionary containing the ``BOLD`` value from the + Junifer Data object. + extra_input : dict, optional + Dictionary containing the rest of the Junifer Data object. Must + include the ``BOLD_confounds`` key. Returns -------- @@ -541,8 +542,11 @@ class fMRIPrepConfoundRemover(BasePreprocessor): Input image with confounds removed. """ + assert extra_input is not None # Not the case, data is validated + confounds_df = self._pick_confounds(extra_input["BOLD_confounds"]) confounds_array = confounds_df.values + bold_img = input["data"] t_r = self.t_r if t_r is None: logger.info("No `t_r` specified, using t_r from nifti header") @@ -552,10 +556,17 @@ class fMRIPrepConfoundRemover(BasePreprocessor): f"Read t_r from nifti header: {t_r}", ) - mask_img = self.mask_img - if mask_img is None: - logger.info("Computing brain mask from image") - mask_img = compute_brain_mask(bold_img) + mask_img = None + if self.masks is not None: + logger.debug(f"Masking with {self.masks}") + mask_img = get_mask( + masks=self.masks, target_data=input, extra_data=extra_input + ) + # Save the mask in the extra input and link it to the bold data + # this allows to use "inherit" down the pipeline + if extra_input is not None: + extra_input["BOLD_mask"] = {"data": mask_img} + input["mask_item"] = "BOLD_mask" logger.info("Cleaning image") logger.debug(f"\tdetrend: {self.detrend}") @@ -600,8 +611,5 @@ class fMRIPrepConfoundRemover(BasePreprocessor): """ self._validate_data(input, extra_input) - assert extra_input is not None - bold_img = input["data"] - confounds_df = self._pick_confounds(extra_input["BOLD_confounds"]) - input["data"] = self._remove_confounds(bold_img, confounds_df) + input["data"] = self._remove_confounds(input, extra_input=extra_input) return "BOLD", input diff --git a/junifer/preprocess/confounds/tests/test_fmriprep_confound_remover.py b/junifer/preprocess/confounds/tests/test_fmriprep_confound_remover.py index 5d52a24e2..f16c01132 100644 --- a/junifer/preprocess/confounds/tests/test_fmriprep_confound_remover.py +++ b/junifer/preprocess/confounds/tests/test_fmriprep_confound_remover.py @@ -452,10 +452,10 @@ def test_fMRIPrepConfoundRemover__remove_confounds() -> None: with PartlyCloudyTestingDataGrabber(reduce_confounds=False) as dg: input = dg["sub-01"] input = reader.fit_transform(input) - confounds = confound_remover._pick_confounds(input["BOLD_confounds"]) raw_bold = input["BOLD"]["data"] + extra_input = {k: v for k, v in input.items() if k != "BOLD"} clean_bold = confound_remover._remove_confounds( - bold_img=raw_bold, confounds_df=confounds + input=input["BOLD"], extra_input=extra_input ) clean_bold = typing.cast(nib.Nifti1Image, clean_bold) # TODO: Find a better way to test functionality here @@ -533,7 +533,63 @@ def test_fMRIPrepConfoundRemover_fit_transform() -> None: assert t_meta["low_pass"] is None assert t_meta["high_pass"] is None assert t_meta["t_r"] is None - assert t_meta["mask_img"] is None + assert t_meta["masks"] is None + + assert "dependencies" in output["BOLD"]["meta"] + dependencies = output["BOLD"]["meta"]["dependencies"] + assert dependencies == {"numpy", "nilearn"} + + +def test_fMRIPrepConfoundRemover_fit_transform_masks() -> None: + """Test fMRIPrepConfoundRemover with all confounds present.""" + + # need reader for the data + reader = DefaultDataReader() + # All strategies full, no spike + confound_remover = fMRIPrepConfoundRemover( + masks={"compute_brain_mask": {"threshold": 0.2}} + ) + + with PartlyCloudyTestingDataGrabber(reduce_confounds=False) as dg: + input = dg["sub-01"] + input = reader.fit_transform(input) + orig_bold = input["BOLD"]["data"].get_fdata().copy() + output = confound_remover.fit_transform(input) + trans_bold = output["BOLD"]["data"].get_fdata() + # Transformation is in place + assert_array_equal(trans_bold, input["BOLD"]["data"].get_fdata()) + + # Data should have the same shape + assert orig_bold.shape == trans_bold.shape + + # but be different + assert_raises( + AssertionError, assert_array_equal, orig_bold, trans_bold + ) + + assert "meta" in output["BOLD"] + assert "preprocess" in output["BOLD"]["meta"] + t_meta = output["BOLD"]["meta"]["preprocess"] + assert t_meta["class"] == "fMRIPrepConfoundRemover" + # It should have all the default parameters + assert t_meta["strategy"] == confound_remover.strategy + assert t_meta["spike"] is None + assert t_meta["detrend"] is True + assert t_meta["standardize"] is True + assert t_meta["low_pass"] is None + assert t_meta["high_pass"] is None + assert t_meta["t_r"] is None + assert isinstance(t_meta["masks"], dict) + assert t_meta["masks"] is not None + assert len(t_meta["masks"]) == 1 + assert "compute_brain_mask" in t_meta["masks"] + assert len(t_meta["masks"]["compute_brain_mask"]) == 1 + assert "threshold" in t_meta["masks"]["compute_brain_mask"] + assert t_meta["masks"]["compute_brain_mask"]["threshold"] == 0.2 + + assert "BOLD_mask" in output + assert "mask_item" in output["BOLD"] + assert output["BOLD"]["mask_item"] == "BOLD_mask" assert "dependencies" in output["BOLD"]["meta"] dependencies = output["BOLD"]["meta"]["dependencies"] -- 2.52.0 From 6731f5b99a42af1b1184ce26cd635745c35b30b7 Mon Sep 17 00:00:00 2001 From: Fede Raimondo Date: Thu, 23 Feb 2023 12:45:00 +0100 Subject: [PATCH 03/12] The missing test and comments addressed --- docs/using/index.rst | 2 +- docs/using/masks.rst | 19 +++-- junifer/data/masks.py | 18 ++-- junifer/data/tests/test_masks.py | 85 ++++++++++++++++++- junifer/markers/parcel_aggregation.py | 3 +- junifer/markers/sphere_aggregation.py | 4 +- .../confounds/fmriprep_confound_remover.py | 2 +- 7 files changed, 106 insertions(+), 27 deletions(-) diff --git a/docs/using/index.rst b/docs/using/index.rst index c9314f117..c79e90e0d 100644 --- a/docs/using/index.rst +++ b/docs/using/index.rst @@ -21,7 +21,7 @@ the results. Finally, we will show how to use the ``queue`` command to interact .. _using_components: Using junifer common components -=============================== +------------------------------- The following sections explains common components of junifer that can be used across many steps of the pipeline. diff --git a/docs/using/masks.rst b/docs/using/masks.rst index f6601be1b..ced611429 100644 --- a/docs/using/masks.rst +++ b/docs/using/masks.rst @@ -5,16 +5,17 @@ Masks ===== -Masks are essentially boolean arrays that are used to constraint the extraction of features to voxels that are -meaningful. For example, in an fMRI imaging study, the mask can be used to constrain the extraction of features to -voxels that contain a certain ratio of gray matter, ensuring that the features are not extracted from voxels that -contain mostly white matter or cerebrospinal fluid, which could add noise to the BOLD signal. +Masks are essentially boolean arrays that are used to constrain the extraction of features to voxels that are +meaningful. For example, in an fMRI imaging study, a mask can be used to constrain the extraction of features to +voxels that contain a certain ratio of gray matter to white matter / cerebrospinal fluid, ensuring that the features +are not extracted from voxels that contain mostly white matter or cerebrospinal fluid, which could add noise to the +BOLD signal. Junifer provides a number of built-in masks, which can be listed using the :func:`junifer.data.masks.list_masks`. Some masks are images, while other masks can be computed using :ref:`nilearn` functions. -For markers and steps that accept `masks` as an argument, the mask can be specified as a string, which will be the -name of a built-in mask, or as a dictionary in which the _only_ key is the built-in mask name and the value is a +For markers and steps that accept ``masks`` as an argument, the mask can be specified as a string, which will be the +name of a built-in mask, or as a dictionary in which the **only** key is the built-in mask name and the value is a dictionary of keyword arguments to pass to the mask function. For example, the following is a valid mask specification that specified the `GM_prob0.2` mask. @@ -23,7 +24,7 @@ For example, the following is a valid mask specification that specified the `GM_ masks: GM_prob0.2 -The following is a valid mask specification that specifies the `compute_brain_mask` mask (function from nilearn), +The following is a valid mask specification that specifies the ``compute_brain_mask`` mask (function from nilearn), with a threshold of 0.5. .. code-block:: yaml @@ -32,9 +33,9 @@ with a threshold of 0.5. compute_brain_mask: threshold: 0.5 -Furthermore, junifer allows to combine several masks using :func:`nilearn.masking.intersect_masks`. This is done by +Furthermore, junifer allows you to combine several masks using :func:`nilearn.masking.intersect_masks`. This is done by specifying a list of masks, where each mask is a string or dictionary as described above. For example, the following -is a valid mask specification that specifies the intersection of the `GM_prob0.2` and `compute_brain_mask` masks. +is a valid mask specification that specifies the intersection of the ``GM_prob0.2`` and ``compute_brain_mask`` masks. .. code-block:: yaml diff --git a/junifer/data/masks.py b/junifer/data/masks.py index ae64840f4..293d7041f 100644 --- a/junifer/data/masks.py +++ b/junifer/data/masks.py @@ -40,7 +40,6 @@ _masks_path = Path(__file__).parent / "masks" def _fetch_icbm152_brain_gm_mask( target_img: "Nifti1Image", - extra_data: Optional[Dict[str, Any]] = None, **kwargs, ): """Fetch ICBM152 brain mask and resample. @@ -49,10 +48,6 @@ def _fetch_icbm152_brain_gm_mask( ---------- target_img : nibabel.Nifti1Image The image to which the mask will be resampled. - extra_data : dict, optional - The other fields in the data object. Useful for accessing other data - kinds that needs to be used in the computation of masks. Default is - None. This parameter is not used in this function. **kwargs : dict Keyword arguments to be passed to :func:`nilearn.datasets.fetch_icbm152_brain_gm_mask`. @@ -161,7 +156,7 @@ def list_masks() -> List[str]: def get_mask( masks: Union[str, Dict, List[Union[Dict, str]]], target_data: Dict[str, Any], - extra_data: Optional[Dict[str, Any]] = None, + extra_input: Optional[Dict[str, Any]] = None, ) -> "Nifti1Image": """Get mask, tailored for the target image. @@ -173,10 +168,9 @@ def get_mask( target_data : dict The corresponding item of the data object to which the mask will be applied. - extra_data : dict, optional + extra_input : dict, optional The other fields in the data object. Useful for accessing other data - kinds that needs to be used in the computation of masks. Default is - None. + kinds that needs to be used in the computation of masks (default None). Returns ------- @@ -231,7 +225,7 @@ def get_mask( mask_params = None if mask_name == "inherit": - if extra_data is None: + if extra_input is None: raise_error( "Cannot inherit mask from another data item " "because no extra data was passed." @@ -242,12 +236,12 @@ def get_mask( "because no mask item was specified " "(missing `mask_item` key in the data object)." ) - if inherited_mask_item not in extra_data: + 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_data[inherited_mask_item]["data"] + mask_img = extra_input[inherited_mask_item]["data"] else: mask_object, _ = load_mask( mask_name, path_only=False, resolution=resolution diff --git a/junifer/data/tests/test_masks.py b/junifer/data/tests/test_masks.py index 3db8ae867..e40216cb0 100644 --- a/junifer/data/tests/test_masks.py +++ b/junifer/data/tests/test_masks.py @@ -5,11 +5,14 @@ # Synchon Mandal # License: AGPL -from typing import Callable, Dict, Union +from typing import Callable, Dict, Union, List from pathlib import Path import pytest + +import numpy as np + from numpy.testing import ( assert_array_almost_equal, assert_array_equal, @@ -20,6 +23,7 @@ from nilearn.masking import ( compute_brain_mask, compute_background_mask, compute_epi_mask, + intersect_masks, ) from nilearn.datasets import fetch_icbm152_brain_gm_mask @@ -352,7 +356,8 @@ def test_get_mask_inherit() -> None: # Get mask using the compute_brain_mask function mask1 = get_mask( masks={"compute_brain_mask": {"threshold": 0.2}}, - target_data=input["BOLD"]) + target_data=input["BOLD"], + ) # Now get the mask using the inherit functionality, passing the # computed mask as extra data @@ -364,3 +369,79 @@ def test_get_mask_inherit() -> None: # Both masks should be equal assert_array_equal(mask1.get_fdata(), mask2.get_fdata()) + + +@pytest.mark.parametrize( + "masks,params", + [ + (["GM_prob0.2", "compute_brain_mask"], {}), + ( + ["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( + masks: Union[str, Dict, List[Union[Dict, str]]], params: Dict +) -> None: + """Test geting multiple masks.""" + reader = DefaultDataReader() + with SPMAuditoryTestingDatagrabber() as dg: + input = dg["sub001"] + input = reader.fit_transform(input) + if not isinstance(masks, list): + junifer_masks = [masks] + else: + junifer_masks = masks.copy() + if len(params) > 0: + # Convert params to junifer style (one dict per param) + junifer_params = [{k: params[k]} for k in params.keys()] + junifer_masks.extend(junifer_params) + target_img = input["BOLD"]["data"] + resolution = np.min(target_img.header.get_zooms()[:3]) + + computed = get_mask(masks=junifer_masks, target_data=input["BOLD"]) + + masks_names = [ + list(x.keys())[0] if isinstance(x, dict) else x for x in masks + ] + + mask_funcs = [ + x + for x in masks_names + if _available_masks[x]["family"] == "Callable" + ] + mask_files = [ + x + for x in masks_names + if _available_masks[x]["family"] != "Callable" + ] + + mask_imgs = [ + load_mask(t_mask, path_only=False, resolution=resolution)[0] + for t_mask in mask_files + ] + + for t_func in mask_funcs: + mask_imgs.append(_available_masks[t_func]["func"](target_img)) + + mask_imgs = [ + resample_to_img( + t_mask, + target_img, + interpolation="nearest", + copy=True, + ) + for t_mask in mask_imgs + ] + + expected = intersect_masks(mask_imgs, **params) + assert_array_equal(computed.get_fdata(), expected.get_fdata()) diff --git a/junifer/markers/parcel_aggregation.py b/junifer/markers/parcel_aggregation.py index e433c4aee..154963ed6 100644 --- a/junifer/markers/parcel_aggregation.py +++ b/junifer/markers/parcel_aggregation.py @@ -186,7 +186,8 @@ class ParcelAggregation(BaseMarker): if self.masks is not None: logger.debug(f"Masking with {self.masks}") - mask_img = get_mask(masks=self.masks, target_data=input) + mask_img = get_mask( + masks=self.masks, target_data=input, extra_input=extra_input) parcellation_bin = math_img( "np.logical_and(img, mask)", diff --git a/junifer/markers/sphere_aggregation.py b/junifer/markers/sphere_aggregation.py index 231440cdb..0e986ccaa 100644 --- a/junifer/markers/sphere_aggregation.py +++ b/junifer/markers/sphere_aggregation.py @@ -139,7 +139,9 @@ class SphereAggregation(BaseMarker): mask_img = None if self.masks is not None: logger.debug(f"Masking with {self.masks}") - mask_img = get_mask(masks=self.masks, target_data=input) + mask_img = get_mask( + masks=self.masks, target_data=input, extra_input=extra_input + ) # Get seeds and labels coords, out_labels = load_coordinates(name=self.coords) masker = JuniferNiftiSpheresMasker( diff --git a/junifer/preprocess/confounds/fmriprep_confound_remover.py b/junifer/preprocess/confounds/fmriprep_confound_remover.py index f555ab896..514d7954f 100644 --- a/junifer/preprocess/confounds/fmriprep_confound_remover.py +++ b/junifer/preprocess/confounds/fmriprep_confound_remover.py @@ -560,7 +560,7 @@ class fMRIPrepConfoundRemover(BasePreprocessor): if self.masks is not None: logger.debug(f"Masking with {self.masks}") mask_img = get_mask( - masks=self.masks, target_data=input, extra_data=extra_input + masks=self.masks, target_data=input, extra_input=extra_input ) # Save the mask in the extra input and link it to the bold data # this allows to use "inherit" down the pipeline -- 2.52.0 From 63ddb579118e70272a38827638c18745f44c4897 Mon Sep 17 00:00:00 2001 From: Fede Raimondo Date: Thu, 23 Feb 2023 12:48:49 +0100 Subject: [PATCH 04/12] update --- junifer/data/masks.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/junifer/data/masks.py b/junifer/data/masks.py index 5f1370f19..9f36e6ab1 100644 --- a/junifer/data/masks.py +++ b/junifer/data/masks.py @@ -18,19 +18,15 @@ from typing import ( import numpy as np import nibabel as nib + +from nilearn.datasets import fetch_icbm152_brain_gm_mask +from nilearn.image import resample_to_img from nilearn.masking import ( compute_brain_mask, compute_background_mask, compute_epi_mask, intersect_masks, ) -from nilearn.datasets import fetch_icbm152_brain_gm_mask -from nilearn.image import resample_to_img -from nilearn.masking import ( - compute_background_mask, - compute_brain_mask, - compute_epi_mask, -) from ..utils.logging import logger, raise_error from .utils import closest_resolution -- 2.52.0 From 5e2caba165036ccf65dea0088d0e3f0f0557bc92 Mon Sep 17 00:00:00 2001 From: Fede Raimondo Date: Thu, 23 Feb 2023 12:51:19 +0100 Subject: [PATCH 05/12] codespell --- 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 5ccd57e68..5a8b7058b 100644 --- a/junifer/data/tests/test_masks.py +++ b/junifer/data/tests/test_masks.py @@ -389,7 +389,7 @@ def test_get_mask_inherit() -> None: def test_get_mask_multiple( masks: Union[str, Dict, List[Union[Dict, str]]], params: Dict ) -> None: - """Test geting multiple masks.""" + """Test getting multiple masks.""" reader = DefaultDataReader() with SPMAuditoryTestingDatagrabber() as dg: input = dg["sub001"] -- 2.52.0 From 90990d7596b35110d85e5d02e74fb721310fed80 Mon Sep 17 00:00:00 2001 From: Fede Raimondo Date: Fri, 24 Feb 2023 10:29:34 +0100 Subject: [PATCH 06/12] fix typo --- docs/using/masks.rst | 2 +- junifer/external/h5io | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 160000 junifer/external/h5io diff --git a/docs/using/masks.rst b/docs/using/masks.rst index ced611429..dd09d1d42 100644 --- a/docs/using/masks.rst +++ b/docs/using/masks.rst @@ -18,7 +18,7 @@ For markers and steps that accept ``masks`` as an argument, the mask can be spec name of a built-in mask, or as a dictionary in which the **only** key is the built-in mask name and the value is a dictionary of keyword arguments to pass to the mask function. -For example, the following is a valid mask specification that specified the `GM_prob0.2` mask. +For example, the following is a valid mask specification that specified the ``GM_prob0.2`` mask. .. code-block:: yaml diff --git a/junifer/external/h5io b/junifer/external/h5io new file mode 160000 index 000000000..845b12b70 --- /dev/null +++ b/junifer/external/h5io @@ -0,0 +1 @@ +Subproject commit 845b12b70a073b5032cb8856cb8370763cf294dd -- 2.52.0 From 0028084f8b12ace4b0d7edcafe3e863759ee8926 Mon Sep 17 00:00:00 2001 From: Fede Raimondo Date: Fri, 24 Feb 2023 11:14:33 +0100 Subject: [PATCH 07/12] document parameters --- junifer/data/tests/test_masks.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/junifer/data/tests/test_masks.py b/junifer/data/tests/test_masks.py index 5a8b7058b..d90876ce4 100644 --- a/junifer/data/tests/test_masks.py +++ b/junifer/data/tests/test_masks.py @@ -389,7 +389,15 @@ def test_get_mask_inherit() -> None: def test_get_mask_multiple( masks: Union[str, Dict, List[Union[Dict, str]]], params: Dict ) -> None: - """Test getting multiple masks.""" + """Test getting multiple masks. + + Parameters + ---------- + masks : str, dict, list of str or dict + Masks to get, junifer style. + params : dict + Parameters to pass to the intersect_masks function. + """ reader = DefaultDataReader() with SPMAuditoryTestingDatagrabber() as dg: input = dg["sub001"] -- 2.52.0 From a71ff995272f9eb3acce700892004f8955da7c91 Mon Sep 17 00:00:00 2001 From: Fede Raimondo Date: Fri, 24 Feb 2023 11:38:07 +0100 Subject: [PATCH 08/12] Revert "fix typo" This reverts commit 90990d7596b35110d85e5d02e74fb721310fed80. --- docs/using/masks.rst | 2 +- junifer/external/h5io | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) delete mode 160000 junifer/external/h5io diff --git a/docs/using/masks.rst b/docs/using/masks.rst index dd09d1d42..ced611429 100644 --- a/docs/using/masks.rst +++ b/docs/using/masks.rst @@ -18,7 +18,7 @@ For markers and steps that accept ``masks`` as an argument, the mask can be spec name of a built-in mask, or as a dictionary in which the **only** key is the built-in mask name and the value is a dictionary of keyword arguments to pass to the mask function. -For example, the following is a valid mask specification that specified the ``GM_prob0.2`` mask. +For example, the following is a valid mask specification that specified the `GM_prob0.2` mask. .. code-block:: yaml diff --git a/junifer/external/h5io b/junifer/external/h5io deleted file mode 160000 index 845b12b70..000000000 --- a/junifer/external/h5io +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 845b12b70a073b5032cb8856cb8370763cf294dd -- 2.52.0 From f5be7738a67094c517d4bf5a635c63a9e72af923 Mon Sep 17 00:00:00 2001 From: Fede Raimondo Date: Fri, 24 Feb 2023 11:38:51 +0100 Subject: [PATCH 09/12] typo --- docs/using/masks.rst | 2 +- junifer/external/h5io | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 160000 junifer/external/h5io diff --git a/docs/using/masks.rst b/docs/using/masks.rst index ced611429..dd09d1d42 100644 --- a/docs/using/masks.rst +++ b/docs/using/masks.rst @@ -18,7 +18,7 @@ For markers and steps that accept ``masks`` as an argument, the mask can be spec name of a built-in mask, or as a dictionary in which the **only** key is the built-in mask name and the value is a dictionary of keyword arguments to pass to the mask function. -For example, the following is a valid mask specification that specified the `GM_prob0.2` mask. +For example, the following is a valid mask specification that specified the ``GM_prob0.2`` mask. .. code-block:: yaml diff --git a/junifer/external/h5io b/junifer/external/h5io new file mode 160000 index 000000000..845b12b70 --- /dev/null +++ b/junifer/external/h5io @@ -0,0 +1 @@ +Subproject commit 845b12b70a073b5032cb8856cb8370763cf294dd -- 2.52.0 From 91412d522254c8162fc08543536b734574a0fd13 Mon Sep 17 00:00:00 2001 From: Fede Raimondo Date: Fri, 24 Feb 2023 11:58:47 +0100 Subject: [PATCH 10/12] Revert "typo" This reverts commit f5be7738a67094c517d4bf5a635c63a9e72af923. --- docs/using/masks.rst | 2 +- junifer/external/h5io | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) delete mode 160000 junifer/external/h5io diff --git a/docs/using/masks.rst b/docs/using/masks.rst index dd09d1d42..ced611429 100644 --- a/docs/using/masks.rst +++ b/docs/using/masks.rst @@ -18,7 +18,7 @@ For markers and steps that accept ``masks`` as an argument, the mask can be spec name of a built-in mask, or as a dictionary in which the **only** key is the built-in mask name and the value is a dictionary of keyword arguments to pass to the mask function. -For example, the following is a valid mask specification that specified the ``GM_prob0.2`` mask. +For example, the following is a valid mask specification that specified the `GM_prob0.2` mask. .. code-block:: yaml diff --git a/junifer/external/h5io b/junifer/external/h5io deleted file mode 160000 index 845b12b70..000000000 --- a/junifer/external/h5io +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 845b12b70a073b5032cb8856cb8370763cf294dd -- 2.52.0 From eb03e4bb9644b99d41e8b0f3aa8871969c884930 Mon Sep 17 00:00:00 2001 From: Fede Raimondo Date: Fri, 24 Feb 2023 11:59:54 +0100 Subject: [PATCH 11/12] Fix this typo --- docs/using/masks.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/using/masks.rst b/docs/using/masks.rst index ced611429..dd09d1d42 100644 --- a/docs/using/masks.rst +++ b/docs/using/masks.rst @@ -18,7 +18,7 @@ For markers and steps that accept ``masks`` as an argument, the mask can be spec name of a built-in mask, or as a dictionary in which the **only** key is the built-in mask name and the value is a dictionary of keyword arguments to pass to the mask function. -For example, the following is a valid mask specification that specified the `GM_prob0.2` mask. +For example, the following is a valid mask specification that specified the ``GM_prob0.2`` mask. .. code-block:: yaml -- 2.52.0 From 53ac2e2dcdb0afde77c1732fead0467f9de313a2 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Sat, 25 Feb 2023 14:29:56 +0100 Subject: [PATCH 12/12] chore: lint --- junifer/data/masks.py | 10 +++------- junifer/data/tests/test_masks.py | 11 +++-------- junifer/markers/falff/falff_spheres.py | 2 +- .../edge_functional_connectivity_spheres.py | 2 +- .../functional_connectivity_spheres.py | 2 +- .../tests/test_functional_connectivity_base.py | 2 +- junifer/markers/parcel_aggregation.py | 3 ++- junifer/markers/reho/reho_parcels.py | 2 +- junifer/markers/reho/reho_spheres.py | 2 +- junifer/markers/tests/test_collection.py | 4 ++-- junifer/markers/utils.py | 2 +- .../preprocess/confounds/fmriprep_confound_remover.py | 3 +-- 12 files changed, 18 insertions(+), 27 deletions(-) diff --git a/junifer/data/masks.py b/junifer/data/masks.py index 9f36e6ab1..e86988301 100644 --- a/junifer/data/masks.py +++ b/junifer/data/masks.py @@ -15,15 +15,13 @@ from typing import ( Union, ) -import numpy as np - import nibabel as nib - +import numpy as np from nilearn.datasets import fetch_icbm152_brain_gm_mask from nilearn.image import resample_to_img from nilearn.masking import ( - compute_brain_mask, compute_background_mask, + compute_brain_mask, compute_epi_mask, intersect_masks, ) @@ -212,9 +210,7 @@ def get_mask( true_masks.append(t_mask) if len(true_masks) == 0: - raise_error( - "No mask was passed. At least one mask is required." - ) + raise_error("No mask was passed. At least one mask is required.") # Get all the masks all_masks = [] for t_mask in true_masks: diff --git a/junifer/data/tests/test_masks.py b/junifer/data/tests/test_masks.py index d90876ce4..7de819096 100644 --- a/junifer/data/tests/test_masks.py +++ b/junifer/data/tests/test_masks.py @@ -6,16 +6,10 @@ # License: AGPL from pathlib import Path -from typing import Callable, Dict, Union, List - -import pytest +from typing import Callable, Dict, List, Union import numpy as np -from numpy.testing import ( - assert_array_almost_equal, - assert_array_equal, -) - +import pytest from nilearn.datasets import fetch_icbm152_brain_gm_mask from nilearn.image import resample_to_img from nilearn.masking import ( @@ -24,6 +18,7 @@ from nilearn.masking import ( compute_epi_mask, intersect_masks, ) +from numpy.testing import assert_array_almost_equal, assert_array_equal from junifer.data.masks import ( _available_masks, diff --git a/junifer/markers/falff/falff_spheres.py b/junifer/markers/falff/falff_spheres.py index 290cc1a70..933663dce 100644 --- a/junifer/markers/falff/falff_spheres.py +++ b/junifer/markers/falff/falff_spheres.py @@ -5,7 +5,7 @@ # Kaustubh R. Patil # License: AGPL -from typing import Dict, Optional, Union, List +from typing import Dict, List, Optional, Union from ...api.decorators import register_marker from .. import SphereAggregation diff --git a/junifer/markers/functional_connectivity/edge_functional_connectivity_spheres.py b/junifer/markers/functional_connectivity/edge_functional_connectivity_spheres.py index 61b5eb232..adcd065dc 100644 --- a/junifer/markers/functional_connectivity/edge_functional_connectivity_spheres.py +++ b/junifer/markers/functional_connectivity/edge_functional_connectivity_spheres.py @@ -4,7 +4,7 @@ # Synchon Mandal # License: AGPL -from typing import Any, Dict, Optional, Union, List +from typing import Any, Dict, List, Optional, Union from ...api.decorators import register_marker from ..sphere_aggregation import SphereAggregation diff --git a/junifer/markers/functional_connectivity/functional_connectivity_spheres.py b/junifer/markers/functional_connectivity/functional_connectivity_spheres.py index 2e13a310c..f6552c81e 100644 --- a/junifer/markers/functional_connectivity/functional_connectivity_spheres.py +++ b/junifer/markers/functional_connectivity/functional_connectivity_spheres.py @@ -5,7 +5,7 @@ # Synchon Mandal # License: AGPL -from typing import Any, Dict, Optional, Union, List +from typing import Any, Dict, List, Optional, Union from ...api.decorators import register_marker from ..sphere_aggregation import SphereAggregation diff --git a/junifer/markers/functional_connectivity/tests/test_functional_connectivity_base.py b/junifer/markers/functional_connectivity/tests/test_functional_connectivity_base.py index 3be96e7dd..e7d98eed5 100644 --- a/junifer/markers/functional_connectivity/tests/test_functional_connectivity_base.py +++ b/junifer/markers/functional_connectivity/tests/test_functional_connectivity_base.py @@ -7,7 +7,7 @@ import pytest # done to keep line length 79 from junifer.markers.functional_connectivity import ( - functional_connectivity_base as fcb + functional_connectivity_base as fcb, ) diff --git a/junifer/markers/parcel_aggregation.py b/junifer/markers/parcel_aggregation.py index 1f4f6f8f9..f7c9c3774 100644 --- a/junifer/markers/parcel_aggregation.py +++ b/junifer/markers/parcel_aggregation.py @@ -187,7 +187,8 @@ class ParcelAggregation(BaseMarker): if self.masks is not None: logger.debug(f"Masking with {self.masks}") mask_img = get_mask( - masks=self.masks, target_data=input, extra_input=extra_input) + masks=self.masks, target_data=input, extra_input=extra_input + ) parcellation_bin = math_img( "np.logical_and(img, mask)", diff --git a/junifer/markers/reho/reho_parcels.py b/junifer/markers/reho/reho_parcels.py index 8df2c40e9..ea7b40bcc 100644 --- a/junifer/markers/reho/reho_parcels.py +++ b/junifer/markers/reho/reho_parcels.py @@ -4,7 +4,7 @@ # License: AGPL -from typing import Any, Dict, Optional, Union, List +from typing import Any, Dict, List, Optional, Union import numpy as np diff --git a/junifer/markers/reho/reho_spheres.py b/junifer/markers/reho/reho_spheres.py index 74ddbed9b..01ca40282 100644 --- a/junifer/markers/reho/reho_spheres.py +++ b/junifer/markers/reho/reho_spheres.py @@ -4,7 +4,7 @@ # License: AGPL -from typing import Any, Dict, Optional, Union, List +from typing import Any, Dict, List, Optional, Union import numpy as np diff --git a/junifer/markers/tests/test_collection.py b/junifer/markers/tests/test_collection.py index 29ce9dc1b..804d9e3ff 100644 --- a/junifer/markers/tests/test_collection.py +++ b/junifer/markers/tests/test_collection.py @@ -186,8 +186,8 @@ def test_marker_collection_storage(tmp_path: Path) -> None: assert out is None mc2 = MarkerCollection( - markers=markers, # type: ignore - datareader=DefaultDataReader()) + markers=markers, datareader=DefaultDataReader() # type: ignore + ) mc2.validate(dg) assert mc2._storage is None diff --git a/junifer/markers/utils.py b/junifer/markers/utils.py index b489befd5..2b5f3ded0 100644 --- a/junifer/markers/utils.py +++ b/junifer/markers/utils.py @@ -7,7 +7,7 @@ # Federico Raimondo # License: AGPL -from typing import Any, Callable, Tuple, Dict, List, Type, Union, Optional +from typing import Any, Callable, Dict, List, Optional, Tuple, Type, Union import numpy as np import pandas as pd diff --git a/junifer/preprocess/confounds/fmriprep_confound_remover.py b/junifer/preprocess/confounds/fmriprep_confound_remover.py index 514d7954f..df316eebe 100644 --- a/junifer/preprocess/confounds/fmriprep_confound_remover.py +++ b/junifer/preprocess/confounds/fmriprep_confound_remover.py @@ -12,10 +12,9 @@ import pandas as pd from nilearn._utils.niimg_conversions import check_niimg_4d from nilearn.image import clean_img - from ...api.decorators import register_preprocessor -from ...utils import logger, raise_error from ...data import get_mask +from ...utils import logger, raise_error from ..base import BasePreprocessor -- 2.52.0