From 0602e716b2522f07aa6d493f8e65fa4b2a34254b Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Wed, 5 Jul 2023 15:43:37 +0200 Subject: [PATCH 01/11] feature: add support for AICHA v1 and v2 parcellations in junifer.data.parcellations --- junifer/data/parcellations.py | 154 ++++++++++++++++++++++++++++++++++ 1 file changed, 154 insertions(+) diff --git a/junifer/data/parcellations.py b/junifer/data/parcellations.py index b246863c7..82573d333 100644 --- a/junifer/data/parcellations.py +++ b/junifer/data/parcellations.py @@ -7,6 +7,7 @@ import io import shutil +import tarfile import tempfile import zipfile from pathlib import Path @@ -17,6 +18,7 @@ import numpy as np import pandas as pd import requests from nilearn import datasets, image +from requests.exceptions import ConnectionError, HTTPError, ReadTimeout from ..utils.logging import logger, raise_error, warn_with_log from .utils import closest_resolution @@ -74,6 +76,12 @@ for scale in range(1, 5): "magneticfield": "3T", "space": "MNInonlinear2009cAsym", } +# Add AICHA parcellation info +for version in (1, 2): + _available_parcellations[f"AICHA_v{version}"] = { + "family": "AICHA", + "version": version, + } def register_parcellation( @@ -278,6 +286,9 @@ def _retrieve_parcellation( ``space`` : {"MNI", "SUIT"}, optional Space of parcellation (default "MNI"). (For more information see http://www.diedrichsenlab.org/imaging/suit.htm). + * AICHA : + ``version`` : {1, 2}, optional + Version of parcellation (default 2). Returns ------- @@ -323,6 +334,12 @@ def _retrieve_parcellation( resolution=resolution, **kwargs, ) + elif family == "AICHA": + parcellation_fname, parcellation_labels = _retrieve_aicha( + parcellations_dir=parcellations_dir, + resolution=resolution, + **kwargs, + ) else: raise_error( f"The provided parcellation name {family} cannot be retrieved." @@ -707,6 +724,143 @@ def _retrieve_suit( return parcellation_fname, labels +def _retrieve_aicha( + parcellations_dir: Path, + resolution: Optional[float] = None, + version: int = 2, +) -> Tuple[Path, List[str]]: + """Retrieve AICHA parcellation. + + Parameters + ---------- + parcellations_dir : pathlib.Path + The path to the parcellation data directory. + resolution : float, optional + The desired resolution of the parcellation to load. If it is not + available, the closest resolution will be loaded. Preferably, use a + resolution higher than the desired one. By default, will load the + highest one (default None). Available resolution for this + parcellation is 2mm. + version : {1, 2}, optional + The version of the parcellation to use (default 2). + + Returns + ------- + pathlib.Path + File path to the parcellation image. + list of str + Parcellation labels. + + Raises + ------ + ValueError + If invalid value is provided for ``version`` or if there is a problem + fetching the parcellation. + + 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. + + """ + # show parameters to user + logger.info("Parcellation parameters:") + logger.info(f"\tversion: {version}") + + # Check version value + _valid_version = (1, 2) + if version not in _valid_version: + raise_error( + f"The parameter `version` ({version}) needs to be one of the " + f"following: {_valid_version}" + ) + + _valid_resolutions = [1] + resolution = closest_resolution(resolution, _valid_resolutions) + + # Define image file + parcellation_fname = ( + parcellations_dir / f"AICHA_v{version}" / "AICHA" / "AICHA.nii" + ) + + # Define label file name according to version + if version == 1: + parcellation_lname = ( + parcellations_dir + / f"AICHA_v{version}" + / "AICHA" + / "AICHA_vol1.txt" + ) + elif version == 2: + parcellation_lname = ( + parcellations_dir + / f"AICHA_v{version}" + / "AICHA" + / "AICHA_vol3.txt" + ) + + # Check existence of parcellation + if not (parcellation_fname.exists() and parcellation_lname.exists()): + logger.info( + "At least one of the parcellation files are missing, fetching." + ) + + # Set file name on server according to version + if version == 1: + server_filename = "aicha_v1.zip" + elif version == 2: + server_filename = "AICHA_v2.tar.zip" + + # Set URL + url = f"http://www.gin.cnrs.fr/wp-content/uploads/{server_filename}" + + logger.info(f"Downloading AICHA v{version} from {url}") + with tempfile.TemporaryDirectory() as tmpdir: + # Make HTTP request + try: + resp = requests.get(url) + resp.raise_for_status() + except (ConnectionError, ReadTimeout, HTTPError) as err: + raise_error( + f"Failed to download AICHA v{version} due to: {err}" + ) + else: + parcellation_zip_path = Path(tmpdir) / server_filename + with open(parcellation_zip_path, "wb") as f: + f.write(resp.content) + + # Extract zipfile + with zipfile.ZipFile(parcellation_zip_path, "r") as zip_ref: + if version == 1: + zip_ref.extractall( + (parcellations_dir / "AICHA_v1").as_posix() + ) + elif version == 2: + zip_ref.extractall(Path(tmpdir).as_posix()) + # Extract tarfile for v2 + with tarfile.TarFile( + Path(tmpdir) / "AICHA_v2.tar", "r" + ) as tar_ref: + tar_ref.extractall( + (parcellations_dir / "AICHA_v2").as_posix() + ) + + # Cleanup after unzipping + if (parcellations_dir / f"AICHA_v{version}" / "__MACOSX").exists(): + shutil.rmtree( + ( + parcellations_dir / f"AICHA_v{version}" / "__MACOSX" + ).as_posix() + ) + + # Load labels + labels = pd.read_csv( + parcellation_lname, sep="\t", header=None, skiprows=[0] # type: ignore + )[0].to_list() + + return parcellation_fname, labels + + def merge_parcellations( parcellations_list: List["Nifti1Image"], parcellations_names: List[str], -- 2.52.0 From b2439a6d97f58c107f1b4e2e73b121c381f75387 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Wed, 5 Jul 2023 15:44:15 +0200 Subject: [PATCH 02/11] update: add unit tests for AICHA v1 and v2 parcellations --- junifer/data/tests/test_parcellations.py | 43 ++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/junifer/data/tests/test_parcellations.py b/junifer/data/tests/test_parcellations.py index 780ad3b01..b7a9ce00f 100644 --- a/junifer/data/tests/test_parcellations.py +++ b/junifer/data/tests/test_parcellations.py @@ -15,6 +15,7 @@ from nilearn.image import new_img_like from numpy.testing import assert_array_almost_equal, assert_array_equal from junifer.data.parcellations import ( + _retrieve_aicha, _retrieve_parcellation, _retrieve_schaefer, _retrieve_suit, @@ -168,6 +169,8 @@ def test_register_parcellation( @pytest.mark.parametrize( "parcellation_name", [ + "AICHA_v1", + "AICHA_v2", "SUITxSUIT", "SUITxMNI", "Schaefer100x7", @@ -552,6 +555,46 @@ def test_retrieve_tian_incorrect_scale(tmp_path: Path) -> None: ) +@pytest.mark.parametrize("version", [1, 2]) +def test_aicha(tmp_path: Path, version: int) -> None: + """Test AICHA parcellation. + + Parameters + ---------- + tmp_path : pathlib.Path + The path to the test directory. + version : int + The parametrized version values. + + """ + parcellations = list_parcellations() + assert f"AICHA_v{version}" in parcellations + # Load parcellation + img, label, img_path = load_parcellation( + name=f"AICHA_v{version}", parcellations_dir=tmp_path + ) + assert img is not None + assert img_path.name == "AICHA.nii" + assert len(label) == 384 + assert_array_equal(img.header["pixdim"][1:4], [2, 2, 2]) # type: ignore + + +def test_retrieve_aicha_incorrect_version(tmp_path: Path) -> None: + """Test retrieve AICHA with incorrect version. + + Parameters + ---------- + tmp_path : pathlib.Path + The path to the test directory. + + """ + with pytest.raises(ValueError, match="The parameter `version`"): + _retrieve_aicha( + parcellations_dir=tmp_path, + version=100, + ) + + def test_merge_parcellations() -> None: """Test merging parcellations.""" # load some parcellations for testing -- 2.52.0 From 0d17d6166fbff5c9bacafa832aa9e2a0bac2e5a1 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Wed, 5 Jul 2023 15:44:49 +0200 Subject: [PATCH 03/11] chore: improve docstring and fix typo in junifer.data.parcellations._retrieve_parcellation() --- junifer/data/parcellations.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/junifer/data/parcellations.py b/junifer/data/parcellations.py index 82573d333..51af25d80 100644 --- a/junifer/data/parcellations.py +++ b/junifer/data/parcellations.py @@ -253,8 +253,8 @@ def _retrieve_parcellation( Parameters ---------- - family : str - The name of the parcellation's family, e.g. 'Schaefer'. + family : {"Schaefer", "SUIT", "Tian", "AICHA"} + The name of the parcellation family. parcellations_dir : str or pathlib.Path, optional Path where the retrieved parcellations file are stored. The default location is "$HOME/junifer/data/parcellations" (default None). @@ -317,19 +317,19 @@ def _retrieve_parcellation( # Retrieval details per family if family == "Schaefer": - parcellation_fname, parcellation_labesl = _retrieve_schaefer( + parcellation_fname, parcellation_labels = _retrieve_schaefer( parcellations_dir=parcellations_dir, resolution=resolution, **kwargs, ) elif family == "SUIT": - parcellation_fname, parcellation_labesl = _retrieve_suit( + parcellation_fname, parcellation_labels = _retrieve_suit( parcellations_dir=parcellations_dir, resolution=resolution, **kwargs, ) elif family == "Tian": - parcellation_fname, parcellation_labesl = _retrieve_tian( + parcellation_fname, parcellation_labels = _retrieve_tian( parcellations_dir=parcellations_dir, resolution=resolution, **kwargs, @@ -345,7 +345,7 @@ def _retrieve_parcellation( f"The provided parcellation name {family} cannot be retrieved." ) - return parcellation_fname, parcellation_labesl + return parcellation_fname, parcellation_labels def _retrieve_schaefer( -- 2.52.0 From 77cbe672da12253d3f9155a9ca3e87f9409b7203 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Wed, 5 Jul 2023 15:45:54 +0200 Subject: [PATCH 04/11] chore: improve docstring for junifer.data.parcellations._retrieve_schaefer() --- junifer/data/parcellations.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/junifer/data/parcellations.py b/junifer/data/parcellations.py index 51af25d80..71c84e8f8 100644 --- a/junifer/data/parcellations.py +++ b/junifer/data/parcellations.py @@ -359,7 +359,7 @@ def _retrieve_schaefer( Parameters ---------- parcellations_dir : pathlib.Path - The path to the parcallations' data directory. + The path to the parcellation data directory. resolution : float, optional The desired resolution of the parcellation to load. If it is not available, the closest resolution will be loaded. Preferably, use a @@ -381,7 +381,7 @@ def _retrieve_schaefer( Raises ------ ValueError - If invalid value is provided for `n_rois` or `yeo_networks` or if + If invalid value is provided for ``n_rois`` or ``yeo_networks`` or if there is a problem fetching the parcellation. """ -- 2.52.0 From eedbffbba6c7033febd5932e6fb6736a13ca28d4 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Wed, 5 Jul 2023 15:46:15 +0200 Subject: [PATCH 05/11] chore: improve docstring for junifer.data.parcellations._retrieve_tian() --- junifer/data/parcellations.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/junifer/data/parcellations.py b/junifer/data/parcellations.py index 71c84e8f8..4ce07b292 100644 --- a/junifer/data/parcellations.py +++ b/junifer/data/parcellations.py @@ -464,7 +464,6 @@ def _retrieve_tian( ---------- parcellations_dir : pathlib.Path The path to the parcellation data directory. - resolution : float, optional resolution : float, optional The desired resolution of the parcellation to load. If it is not available, the closest resolution will be loaded. Preferably, use a @@ -489,8 +488,8 @@ def _retrieve_tian( Raises ------ ValueError - If invalid value is provided for `scale` or `magneticfield` or `space` - or if there is a problem fetching the parcellation. + If invalid value is provided for ``scale`` or ``magneticfield`` or + ``space`` or if there is a problem fetching the parcellation. """ # show parameters to user -- 2.52.0 From 400b43a934b65f8c5ee7a11a2776a6884452f2c8 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Wed, 5 Jul 2023 15:46:34 +0200 Subject: [PATCH 06/11] chore: improve docstring for junifer.data.parcellations._retrieve_suit() --- junifer/data/parcellations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/junifer/data/parcellations.py b/junifer/data/parcellations.py index 4ce07b292..9b18cffa0 100644 --- a/junifer/data/parcellations.py +++ b/junifer/data/parcellations.py @@ -648,7 +648,7 @@ def _retrieve_suit( Raises ------ ValueError - If invalid value is provided for `space` or if there is a problem + If invalid value is provided for ``space`` or if there is a problem fetching the parcellation. """ -- 2.52.0 From 07318ccfea33fc2e24e7ac52daefbb3b2afc2f91 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Wed, 5 Jul 2023 15:48:44 +0200 Subject: [PATCH 07/11] chore: remove resolved comment in junifer.data.parcellations --- junifer/data/parcellations.py | 1 - 1 file changed, 1 deletion(-) diff --git a/junifer/data/parcellations.py b/junifer/data/parcellations.py index 9b18cffa0..6556ddbf5 100644 --- a/junifer/data/parcellations.py +++ b/junifer/data/parcellations.py @@ -159,7 +159,6 @@ def list_parcellations() -> List[str]: # return resolution -# TODO: keyword arguments are not passed, check def load_parcellation( name: str, parcellations_dir: Union[str, Path, None] = None, -- 2.52.0 From 6b1092acfdd1873e828184a8da9ddf85ebe9b1d1 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Wed, 5 Jul 2023 15:55:09 +0200 Subject: [PATCH 08/11] chore: make junifer.api.cli pass ruff D411 --- junifer/api/cli.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/junifer/api/cli.py b/junifer/api/cli.py index a4d27fc01..62426da2b 100644 --- a/junifer/api/cli.py +++ b/junifer/api/cli.py @@ -137,6 +137,7 @@ def run(filepath: click.Path, element: str, verbose: Union[str, int]) -> None: """Run command for CLI. \f + Parameters ---------- filepath : click.Path @@ -185,6 +186,7 @@ def collect(filepath: click.Path, verbose: Union[str, int]) -> None: """Collect command for CLI. \f + Parameters ---------- filepath : click.Path @@ -228,6 +230,7 @@ def queue( """Queue command for CLI. \f + Parameters ---------- filepath : click.Path @@ -266,6 +269,7 @@ def wtf(long_: bool) -> None: """Wtf command for CLI. \f + Parameters ---------- long_ : bool @@ -288,6 +292,7 @@ def selftest(subpkg: str) -> None: """Selftest command for CLI. \f + Parameters ---------- subpkg : {"all", "api", "configs", "data", "datagrabber", "datareader", -- 2.52.0 From 7d2affbcd57f6e30c452e3be82bf8d92a6ee7829 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Wed, 5 Jul 2023 16:05:09 +0200 Subject: [PATCH 09/11] docs: add AICHA to available parcellations in builtin.rst --- docs/builtin.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/builtin.rst b/docs/builtin.rst index 3398b0a0c..24b6be9f4 100644 --- a/docs/builtin.rst +++ b/docs/builtin.rst @@ -285,6 +285,14 @@ Available | unveiled with functional connectivity gradients. | Nature Neuroscience, Volume 23, Pages 1421–1432 (2020). | https://doi.org/10.1038/s41593-020-00711-6 + * - AICHA + - ``version`` + - ``AICHA_v1``, ``AICHA_v2`` + - 0.0.3 + - | Joliot, M., Jobard, G., Naveau, M. et al. + | AICHA: An atlas of intrinsic connectivity of homotopic areas. + | Journal of Neuroscience Methods, Volume 254, Pages 46-59 (2015). + | https://doi.org/10.1016/j.jneumeth.2015.07.013 Planned -- 2.52.0 From a485874b61be7a5d4986ea24a825d1e8cd0a62a5 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Wed, 5 Jul 2023 16:09:12 +0200 Subject: [PATCH 10/11] chore: add changelog 173.feature --- docs/changes/newsfragments/173.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 docs/changes/newsfragments/173.feature diff --git a/docs/changes/newsfragments/173.feature b/docs/changes/newsfragments/173.feature new file mode 100644 index 000000000..5f28763c8 --- /dev/null +++ b/docs/changes/newsfragments/173.feature @@ -0,0 +1 @@ +Add ``AICHA v1`` and ``AICHA v2`` parcellations to ``junifer.data`` by `Synchon Mandal`_ -- 2.52.0 From f2620ad5c2ed61e80c58336d6c5f8818b992efd9 Mon Sep 17 00:00:00 2001 From: Synchon Mandal Date: Thu, 6 Jul 2023 08:13:47 +0200 Subject: [PATCH 11/11] fix: use correct tar file name for AICHA v2 retrieval --- junifer/data/parcellations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/junifer/data/parcellations.py b/junifer/data/parcellations.py index 6556ddbf5..29621d66b 100644 --- a/junifer/data/parcellations.py +++ b/junifer/data/parcellations.py @@ -837,7 +837,7 @@ def _retrieve_aicha( zip_ref.extractall(Path(tmpdir).as_posix()) # Extract tarfile for v2 with tarfile.TarFile( - Path(tmpdir) / "AICHA_v2.tar", "r" + Path(tmpdir) / "aicha_v2.tar", "r" ) as tar_ref: tar_ref.extractall( (parcellations_dir / "AICHA_v2").as_posix() -- 2.52.0