diff --git a/docs/builtin.rst b/docs/builtin.rst index 720621c44..9657f49de 100644 --- a/docs/builtin.rst +++ b/docs/builtin.rst @@ -308,6 +308,26 @@ Available | patterns of brain connectivity. | Nature Neuroscience, Volume 18(11), Pages 1664-1671 (2015). | https://doi:10.1038/nn.4135 + * - Yan + - ``n_rois``, ``yeo_networks``, ``kong_networks`` + - | ``Yan100xYeo7``, ``Yan200xYeo7``, ``Yan300xYeo7``, + | ``Yan400xYeo7``, ``Yan500xYeo7``, ``Yan600xYeo7``, + | ``Yan700xYeo7``, ``Yan800xYeo7``, ``Yan900xYeo7``, + | ``Yan1000xYeo7``, + | ``Yan100xYeo17``, ``Yan200xYeo17``, ``Yan300xYeo17``, + | ``Yan400xYeo17``, ``Yan500xYeo17``, ``Yan600xYeo17``, + | ``Yan700xYeo17``, ``Yan800xYeo17``, ``Yan900xYeo17``, + | ``Yan1000xYeo17``, + | ``Yan100xKong17``, ``Yan200xKong17``, ``Yan300xKong17``, + | ``Yan400xKong17``, ``Yan500xKong17``, ``Yan600xKong17``, + | ``Yan700xKong17``, ``Yan800xKong17``, ``Yan900xKong17``, + | ``Yan1000xKong17`` + - 0.0.3 + - | Yan, X., Kong, R., Xue, A., et al. + | Homotopic local-global parcellation of the human cerebral cortex from + | resting-state functional connectivity. + | NeuroImage, Volume 273 (2023). + | https://doi.org/10.1016/j.neuroimage.2023.120010 Planned diff --git a/docs/changes/newsfragments/225.feature b/docs/changes/newsfragments/225.feature new file mode 100644 index 000000000..8ca78d515 --- /dev/null +++ b/docs/changes/newsfragments/225.feature @@ -0,0 +1 @@ +Add ``Yan 2023`` parcellation to ``junifer.data`` by `Synchon Mandal`_ diff --git a/junifer/data/parcellations.py b/junifer/data/parcellations.py index 0228ab59f..aa67a1f3e 100644 --- a/junifer/data/parcellations.py +++ b/junifer/data/parcellations.py @@ -103,6 +103,21 @@ for year in (2013, 2015, 2019): "year": 2019, "n_rois": 368, } +# Add Yan parcellation info +for n_rois in range(100, 1001, 100): + # Add Yeo networks + for yeo_network in [7, 17]: + _available_parcellations[f"Yan{n_rois}xYeo{yeo_network}"] = { + "family": "Yan", + "n_rois": n_rois, + "yeo_networks": yeo_network, + } + # Add Kong networks + _available_parcellations[f"Yan{n_rois}xKong17"] = { + "family": "Yan", + "n_rois": n_rois, + "kong_networks": 17, + } def register_parcellation( @@ -273,7 +288,7 @@ def _retrieve_parcellation( Parameters ---------- - family : {"Schaefer", "SUIT", "Tian", "AICHA", "Shen"} + family : {"Schaefer", "SUIT", "Tian", "AICHA", "Shen", "Yan"} The name of the parcellation family. parcellations_dir : str or pathlib.Path, optional Path where the retrieved parcellations file are stored. The default @@ -316,6 +331,13 @@ def _retrieve_parcellation( Number of ROIs to use. Can be ``50, 100, or 150`` for ``year = 2013`` but is fixed at ``268`` for ``year = 2015`` and at ``368`` for ``year = 2019``. + * Yan : + ``n_rois`` : {100, 200, 300, 400, 500, 600, 700, 800, 900, 1000} + Granularity of the parcellation to be used. + ``yeo_networks`` : {7, 17}, optional + Number of Yeo networks to use (default None). + ``kong_networks`` : {17}, optional + Number of Kong networks to use (default None). Returns ------- @@ -373,6 +395,12 @@ def _retrieve_parcellation( resolution=resolution, **kwargs, ) + elif family == "Yan": + parcellation_fname, parcellation_labels = _retrieve_yan( + parcellations_dir=parcellations_dir, + resolution=resolution, + **kwargs, + ) else: raise_error( f"The provided parcellation name {family} cannot be retrieved." @@ -1089,6 +1117,188 @@ def _retrieve_shen( # noqa: C901 return parcellation_fname, labels +def _retrieve_yan( + parcellations_dir: Path, + resolution: Optional[float] = None, + n_rois: Optional[int] = None, + yeo_networks: Optional[int] = None, + kong_networks: Optional[int] = None, +) -> Tuple[Path, List[str]]: + """Retrieve Yan 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 resolutions for this + parcellation are 1mm and 2mm. + n_rois : {100, 200, 300, 400, 500, 600, 700, 800, 900, 1000}, optional + Granularity of the parcellation to be used (default None). + yeo_networks : {7, 17}, optional + Number of Yeo networks to use (default None). + kong_networks : {17}, optional + Number of Kong networks to use (default None). + + Returns + ------- + pathlib.Path + File path to the parcellation image. + list of str + Parcellation labels. + + Raises + ------ + ValueError + If invalid value is provided for ``n_rois``, ``yeo_networks`` or + ``kong_networks`` or if there is a problem fetching the parcellation. + + """ + logger.info("Parcellation parameters:") + logger.info(f"\tresolution: {resolution}") + logger.info(f"\tn_rois: {n_rois}") + logger.info(f"\tyeo_networks: {yeo_networks}") + logger.info(f"\tkong_networks: {kong_networks}") + + # Allow single network type + if (not yeo_networks and not kong_networks) or ( + yeo_networks and kong_networks + ): + raise_error( + "Either one of `yeo_networks` or `kong_networks` need to be " + "specified." + ) + + # Check resolution + _valid_resolutions = [1, 2] + resolution = closest_resolution(resolution, _valid_resolutions) + + # Check n_rois value + _valid_n_rois = list(range(100, 1001, 100)) + if n_rois not in _valid_n_rois: + raise_error( + f"The parameter `n_rois` ({n_rois}) needs to be one of the " + f"following: {_valid_n_rois}" + ) + + if yeo_networks: + # Check yeo_networks value + _valid_yeo_networks = [7, 17] + if yeo_networks not in _valid_yeo_networks: + raise_error( + f"The parameter `yeo_networks` ({yeo_networks}) needs to be " + f"one of the following: {_valid_yeo_networks}" + ) + # Define image and label file according to network + parcellation_fname = ( + parcellations_dir + / "Yan_2023" + / ( + f"{n_rois}Parcels_Yeo2011_{yeo_networks}Networks_FSLMNI152_" + f"{resolution}mm.nii.gz" + ) + ) + parcellation_lname = ( + parcellations_dir + / "Yan_2023" + / f"{n_rois}Parcels_Yeo2011_{yeo_networks}Networks_LUT.txt" + ) + elif kong_networks: + # Check kong_networks value + _valid_kong_networks = [17] + if kong_networks not in _valid_kong_networks: + raise_error( + f"The parameter `kong_networks` ({kong_networks}) needs to be " + f"one of the following: {_valid_kong_networks}" + ) + # Define image and label file according to network + parcellation_fname = ( + parcellations_dir + / "Yan_2023" + / ( + f"{n_rois}Parcels_Kong2022_{kong_networks}Networks_FSLMNI152_" + f"{resolution}mm.nii.gz" + ) + ) + parcellation_lname = ( + parcellations_dir + / "Yan_2023" + / f"{n_rois}Parcels_Kong2022_{kong_networks}Networks_LUT.txt" + ) + + # Check for existence of parcellation: + if not parcellation_fname.exists() and not parcellation_lname.exists(): + logger.info( + "At least one of the parcellation files are missing, fetching." + ) + + # Set URL based on network + if yeo_networks: + img_url = ( + "https://raw.githubusercontent.com/ThomasYeoLab/CBIG/" + "master/stable_projects/brain_parcellation/Yan2023_homotopic/" + f"parcellations/MNI/yeo{yeo_networks}/{n_rois}Parcels_Yeo2011" + f"_{yeo_networks}Networks_FSLMNI152_{resolution}mm.nii.gz" + ) + label_url = ( + "https://raw.githubusercontent.com/ThomasYeoLab/CBIG/" + "master/stable_projects/brain_parcellation/Yan2023_homotopic/" + f"parcellations/MNI/yeo{yeo_networks}/freeview_lut/{n_rois}" + f"Parcels_Yeo2011_{yeo_networks}Networks_LUT.txt" + ) + elif kong_networks: + img_url = ( + "https://raw.githubusercontent.com/ThomasYeoLab/CBIG/" + "master/stable_projects/brain_parcellation/Yan2023_homotopic/" + f"parcellations/MNI/kong17/{n_rois}Parcels_Kong2022" + f"_17Networks_FSLMNI152_{resolution}mm.nii.gz" + ) + label_url = ( + "https://raw.githubusercontent.com/ThomasYeoLab/CBIG/" + "master/stable_projects/brain_parcellation/Yan2023_homotopic/" + f"parcellations/MNI/kong17/freeview_lut/{n_rois}Parcels_" + "Kong2022_17Networks_LUT.txt" + ) + + # Initiate a session and make HTTP requests + session = requests.Session() + # Download parcellation file + logger.info(f"Downloading Yan 2023 parcellation from {img_url}") + try: + img_resp = session.get(img_url) + img_resp.raise_for_status() + except (ConnectionError, ReadTimeout, HTTPError) as err: + raise_error( + f"Failed to download Yan 2023 parcellation due to: {err}" + ) + else: + parcellation_img_path = Path(parcellation_fname) + parcellation_img_path.parent.mkdir(parents=True, exist_ok=True) + parcellation_img_path.touch(exist_ok=True) + with open(parcellation_img_path, "wb") as f: + f.write(img_resp.content) + # Download label file + logger.info(f"Downloading Yan 2023 labels from {label_url}") + try: + label_resp = session.get(label_url) + label_resp.raise_for_status() + except (ConnectionError, ReadTimeout, HTTPError) as err: + raise_error(f"Failed to download Yan 2023 labels due to: {err}") + else: + parcellation_labels_path = Path(parcellation_lname) + parcellation_labels_path.touch(exist_ok=True) + with open(parcellation_labels_path, "wb") as f: + f.write(label_resp.content) + + # Load label file + labels = pd.read_csv(parcellation_lname, sep=" ", header=None)[1].to_list() + + return parcellation_fname, labels + + def merge_parcellations( parcellations_list: List["Nifti1Image"], parcellations_names: List[str], diff --git a/junifer/data/tests/test_parcellations.py b/junifer/data/tests/test_parcellations.py index f8b415af7..e1a4a2e46 100644 --- a/junifer/data/tests/test_parcellations.py +++ b/junifer/data/tests/test_parcellations.py @@ -21,6 +21,7 @@ from junifer.data.parcellations import ( _retrieve_shen, _retrieve_suit, _retrieve_tian, + _retrieve_yan, list_parcellations, load_parcellation, merge_parcellations, @@ -728,6 +729,204 @@ def test_retrieve_shen_incorrect_param_combo( ) +@pytest.mark.parametrize( + "resolution, n_rois, yeo_networks, kong_networks", + [ + (1.0, 100, 7, None), + (1.0, 200, 7, None), + (1.0, 300, 7, None), + (1.0, 400, 7, None), + (1.0, 500, 7, None), + (1.0, 600, 7, None), + (1.0, 700, 7, None), + (1.0, 800, 7, None), + (1.0, 900, 7, None), + (1.0, 1000, 7, None), + (2.0, 100, 7, None), + (2.0, 200, 7, None), + (2.0, 300, 7, None), + (2.0, 400, 7, None), + (2.0, 500, 7, None), + (2.0, 600, 7, None), + (2.0, 700, 7, None), + (2.0, 800, 7, None), + (2.0, 900, 7, None), + (2.0, 1000, 7, None), + (1.0, 100, 17, None), + (1.0, 200, 17, None), + (1.0, 300, 17, None), + (1.0, 400, 17, None), + (1.0, 500, 17, None), + (1.0, 600, 17, None), + (1.0, 700, 17, None), + (1.0, 800, 17, None), + (1.0, 900, 17, None), + (1.0, 1000, 17, None), + (2.0, 100, 17, None), + (2.0, 200, 17, None), + (2.0, 300, 17, None), + (2.0, 400, 17, None), + (2.0, 500, 17, None), + (2.0, 600, 17, None), + (2.0, 700, 17, None), + (2.0, 800, 17, None), + (2.0, 900, 17, None), + (2.0, 1000, 17, None), + (1.0, 100, None, 17), + (1.0, 200, None, 17), + (1.0, 300, None, 17), + (1.0, 400, None, 17), + (1.0, 500, None, 17), + (1.0, 600, None, 17), + (1.0, 700, None, 17), + (1.0, 800, None, 17), + (1.0, 900, None, 17), + (1.0, 1000, None, 17), + (2.0, 100, None, 17), + (2.0, 200, None, 17), + (2.0, 300, None, 17), + (2.0, 400, None, 17), + (2.0, 500, None, 17), + (2.0, 600, None, 17), + (2.0, 700, None, 17), + (2.0, 800, None, 17), + (2.0, 900, None, 17), + (2.0, 1000, None, 17), + ], +) +def test_yan( + tmp_path: Path, + resolution: float, + n_rois: int, + yeo_networks: int, + kong_networks: int, +) -> None: + """Test Yan parcellation. + + Parameters + ---------- + tmp_path : pathlib.Path + The path to the test directory. + resolution : float + The parametrized resolution values. + n_rois : int + The parametrized ROI count values. + yeo_networks : int + The parametrized Yeo networks values. + kong_networks : int + The parametrized Kong networks values. + + """ + parcellations = list_parcellations() + if yeo_networks: + parcellation_name = f"Yan{n_rois}xYeo{yeo_networks}" + assert parcellation_name in parcellations + parcellation_file = ( + f"{n_rois}Parcels_Yeo2011_{yeo_networks}Networks_FSLMNI152_" + f"{int(resolution)}mm.nii.gz" + ) + elif kong_networks: + parcellation_name = f"Yan{n_rois}xKong{kong_networks}" + assert parcellation_name in parcellations + parcellation_file = ( + f"{n_rois}Parcels_Kong2022_{kong_networks}Networks_FSLMNI152_" + f"{int(resolution)}mm.nii.gz" + ) + # Load parcellation + img, label, img_path = load_parcellation( + name=parcellation_name, # type: ignore + parcellations_dir=tmp_path, + resolution=resolution, + ) + assert img is not None + assert img_path.name == parcellation_file # type: ignore + assert len(label) == n_rois + assert_array_equal( + img.header["pixdim"][1:4], 3 * [resolution] # type: ignore + ) + + +def test_retrieve_yan_incorrect_networks(tmp_path: Path) -> None: + """Test retrieve Yan with incorrect networks. + + Parameters + ---------- + tmp_path : pathlib.Path + The path to the test directory. + + """ + with pytest.raises( + ValueError, match="Either one of `yeo_networks` or `kong_networks`" + ): + _retrieve_yan( + parcellations_dir=tmp_path, + n_rois=31418, + yeo_networks=100, + kong_networks=100, + ) + + with pytest.raises( + ValueError, match="Either one of `yeo_networks` or `kong_networks`" + ): + _retrieve_yan( + parcellations_dir=tmp_path, + n_rois=31418, + yeo_networks=None, + kong_networks=None, + ) + + +def test_retrieve_yan_incorrect_n_rois(tmp_path: Path) -> None: + """Test retrieve Yan with incorrect ROIs. + + Parameters + ---------- + tmp_path : pathlib.Path + The path to the test directory. + + """ + with pytest.raises(ValueError, match="The parameter `n_rois`"): + _retrieve_yan( + parcellations_dir=tmp_path, + n_rois=31418, + yeo_networks=7, + ) + + +def test_retrieve_yan_incorrect_yeo_networks(tmp_path: Path) -> None: + """Test retrieve Yan with incorrect Yeo networks. + + Parameters + ---------- + tmp_path : pathlib.Path + The path to the test directory. + + """ + with pytest.raises(ValueError, match="The parameter `yeo_networks`"): + _retrieve_yan( + parcellations_dir=tmp_path, + n_rois=100, + yeo_networks=27, + ) + + +def test_retrieve_yan_incorrect_kong_networks(tmp_path: Path) -> None: + """Test retrieve Yan with incorrect Kong networks. + + Parameters + ---------- + tmp_path : pathlib.Path + The path to the test directory. + + """ + with pytest.raises(ValueError, match="The parameter `kong_networks`"): + _retrieve_yan( + parcellations_dir=tmp_path, + n_rois=100, + kong_networks=27, + ) + + def test_merge_parcellations() -> None: """Test merging parcellations.""" # load some parcellations for testing