diff --git a/docs/builtin.rst b/docs/builtin.rst index 24b6be9f4..720621c44 100644 --- a/docs/builtin.rst +++ b/docs/builtin.rst @@ -293,6 +293,21 @@ Available | 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 + * - Shen + - ``year``, ``n_rois`` + - | ``Shen_2013_50``, ``Shen_2013_100``, ``Shen_2013_150``, + | ``Shen_2015_268``, ``Shen_2019_368`` + - 0.0.3 + - | Shen, X., Tokoglu, F., Papademetris, X., Constable, R.T. + | Groupwise whole-brain parcellation from resting-state fMRI data + | for network node identification. + | Neuroimage, Volume 82 (2013). + | https://doi.org/10.1016/j.neuroimage.2013.05.081. + | Finn, E.S., Shen, X., Scheinost, D., et al. + | Functional connectome fingerprinting: identifying individuals using + | patterns of brain connectivity. + | Nature Neuroscience, Volume 18(11), Pages 1664-1671 (2015). + | https://doi:10.1038/nn.4135 Planned @@ -320,12 +335,6 @@ Planned | Automated anatomical labelling atlas 3. | Neuroimage, Volume 206 (2020). | https://doi.org/10.1016/j.neuroimage.2019.116189 - * - Shen - - | Shen, X., Tokoglu, F., Papademetris, X., Constable, R.T. - | Groupwise whole-brain parcellation from resting-state fMRI data - | for network node identification. - | Neuroimage, Volume 82 (2013). - | https://doi.org/10.1016/j.neuroimage.2013.05.081. * - Mindboggle 101 - | Klein, A., & Tourville, J. | 101 labeled brain images and a consistent human cortical labeling diff --git a/docs/changes/newsfragments/184.feature b/docs/changes/newsfragments/184.feature new file mode 100644 index 000000000..5747528d3 --- /dev/null +++ b/docs/changes/newsfragments/184.feature @@ -0,0 +1 @@ +Add ``Shen 2013``, ``Shen 2015`` and ``Shen 2019`` parcellations to ``junifer.data`` by `Synchon Mandal`_ diff --git a/junifer/data/parcellations.py b/junifer/data/parcellations.py index 29621d66b..0228ab59f 100644 --- a/junifer/data/parcellations.py +++ b/junifer/data/parcellations.py @@ -82,6 +82,27 @@ for version in (1, 2): "family": "AICHA", "version": version, } +# Add Shen parcellation info +for year in (2013, 2015, 2019): + if year == 2013: + for n_rois in (50, 100, 150): + _available_parcellations[f"Shen_{year}_{n_rois}"] = { + "family": "Shen", + "year": 2013, + "n_rois": n_rois, + } + elif year == 2015: + _available_parcellations["Shen_2015_268"] = { + "family": "Shen", + "year": 2015, + "n_rois": 268, + } + elif year == 2019: + _available_parcellations["Shen_2019_368"] = { + "family": "Shen", + "year": 2019, + "n_rois": 368, + } def register_parcellation( @@ -252,7 +273,7 @@ def _retrieve_parcellation( Parameters ---------- - family : {"Schaefer", "SUIT", "Tian", "AICHA"} + family : {"Schaefer", "SUIT", "Tian", "AICHA", "Shen"} The name of the parcellation family. parcellations_dir : str or pathlib.Path, optional Path where the retrieved parcellations file are stored. The default @@ -288,6 +309,13 @@ def _retrieve_parcellation( * AICHA : ``version`` : {1, 2}, optional Version of parcellation (default 2). + * Shen : + ``year`` : {2013, 2015, 2019}, optional + Year of the parcellation to use (default 2015). + ``n_rois`` : int, optional + 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``. Returns ------- @@ -339,6 +367,12 @@ def _retrieve_parcellation( resolution=resolution, **kwargs, ) + elif family == "Shen": + parcellation_fname, parcellation_labels = _retrieve_shen( + parcellations_dir=parcellations_dir, + resolution=resolution, + **kwargs, + ) else: raise_error( f"The provided parcellation name {family} cannot be retrieved." @@ -859,6 +893,202 @@ def _retrieve_aicha( return parcellation_fname, labels +def _retrieve_shen( # noqa: C901 + parcellations_dir: Path, + resolution: Optional[float] = None, + year: int = 2015, + n_rois: int = 268, +) -> Tuple[Path, List[str]]: + """Retrieve Shen 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 for ``year = 2013`` and ``year = 2015`` but fixed to + 1mm for ``year = 2019``. + year : {2013, 2015, 2019}, optional + The year of the parcellation to use (default 2015). + n_rois : int, optional + Number of ROIs. Can be ``50, 100, or 150`` for ``year = 2013`` but is + fixed at ``268`` for ``year = 2015`` and at ``368`` for + ``year = 2019``. + + Returns + ------- + pathlib.Path + File path to the parcellation image. + list of str + Parcellation labels. + + Raises + ------ + ValueError + If invalid value or combination is provided for ``year`` and ``n_rois`` + or if there is a problem fetching the parcellation. + + """ + # show parameters to user + logger.info("Parcellation parameters:") + logger.info(f"\tyear: {year}") + logger.info(f"\tn_rois: {n_rois}") + + # Check resolution + _valid_resolutions = [1, 2] + resolution = closest_resolution(resolution, _valid_resolutions) + + # Check year value + _valid_year = (2013, 2015, 2019) + if year not in _valid_year: + raise_error( + f"The parameter `year` ({year}) needs to be one of the " + f"following: {_valid_year}" + ) + + # Check n_rois value + _valid_n_rois = [50, 100, 150, 268, 368] + 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}" + ) + + # Check combinations + if resolution == 2 and year == 2019: + raise_error( + "The parameter combination `resolution = 2` and `year = 2019` is " + "invalid" + ) + if n_rois in (268, 368) and year == 2013: + raise_error( + f"The parameter combination `resolution = {resolution}` and " + "`year = 2013` is invalid" + ) + if n_rois in (50, 100, 150) and year in (2015, 2019): + raise_error( + f"The parameter combination `resolution = {resolution}` and " + f"`year = {year}` is invalid" + ) + if (n_rois == 268 and year == 2019) or (n_rois == 368 and year == 2015): + raise_error( + f"The parameter combination `resolution = {resolution}` and " + f"`year = {year}` is invalid" + ) + + # Define image file according to constraints + if year == 2013: + parcellation_fname = ( + parcellations_dir + / "Shen_2013" + / "shenetal_neuroimage2013" + / f"fconn_atlas_{n_rois}_{resolution}mm.nii" + ) + parcellation_lname = ( + parcellations_dir + / "Shen_2013" + / "shenetal_neuroimage2013" + / f"Group_seg{n_rois}_BAindexing_setA.txt" + ) + elif year == 2015: + parcellation_fname = ( + parcellations_dir + / "Shen_2015" + / f"shen_{resolution}mm_268_parcellation.nii.gz" + ) + elif year == 2019: + parcellation_fname = ( + parcellations_dir + / "Shen_2019" + / "Shen_1mm_368_parcellation.nii.gz" + ) + + # Check existence of parcellation + if not parcellation_fname.exists(): + logger.info( + "At least one of the parcellation files are missing, fetching." + ) + + # Set URL based on year + url = "" + if year == 2013: + url = "https://www.nitrc.org/frs/download.php/5785/shenetal_neuroimage2013_funcatlas.zip" + elif year == 2015: + # Set URL based on resolution + if resolution == 1: + url = "https://www.nitrc.org/frs/download.php/7976/shen_1mm_268_parcellation.nii.gz" + elif resolution == 2: + url = "https://www.nitrc.org/frs/download.php/7977/shen_2mm_268_parcellation.nii.gz" + elif year == 2019: + url = "https://www.nitrc.org/frs/download.php/11629/shen_368.zip" + + logger.info(f"Downloading Shen {year} 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 Shen {year} due to {err}") + else: + if year in (2013, 2019): + parcellation_zip_path = Path(tmpdir) / f"Shen{year}.zip" + with open(parcellation_zip_path, "wb") as f: + f.write(resp.content) + + # Extract zipfile + with zipfile.ZipFile( + parcellation_zip_path, "r" + ) as zip_ref: + zip_ref.extractall( + (parcellations_dir / f"Shen_{year}").as_posix() + ) + + # Cleanup after unzipping + if ( + parcellations_dir / f"Shen_{year}" / "__MACOSX" + ).exists(): + shutil.rmtree( + ( + parcellations_dir / f"Shen_{year}" / "__MACOSX" + ).as_posix() + ) + + elif year == 2015: + img_dir_path = parcellations_dir / "Shen_2015" + img_dir_path.mkdir(parents=True, exist_ok=True) + img_path = ( + img_dir_path + / f"shen_{resolution}mm_268_parcellation.nii.gz" + ) + img_path.touch(exist_ok=True) + with open(img_path.as_posix(), "wb") as f: + f.write(resp.content) + + # Load labels based on year + if year == 2013: + labels = ( + pd.read_csv( + parcellation_lname, # type: ignore + sep=",", # type: ignore + header=None, # type: ignore + skiprows=[0], # type: ignore + )[1] + .map(lambda x: x.strip()) # fix formatting + .to_list() + ) + elif year == 2015: + labels = list(range(1, 269)) + elif year == 2019: + labels = list(range(1, 369)) + + 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 b7a9ce00f..f8b415af7 100644 --- a/junifer/data/tests/test_parcellations.py +++ b/junifer/data/tests/test_parcellations.py @@ -18,6 +18,7 @@ from junifer.data.parcellations import ( _retrieve_aicha, _retrieve_parcellation, _retrieve_schaefer, + _retrieve_shen, _retrieve_suit, _retrieve_tian, list_parcellations, @@ -595,6 +596,138 @@ def test_retrieve_aicha_incorrect_version(tmp_path: Path) -> None: ) +@pytest.mark.parametrize( + "resolution, year, n_rois, n_labels, img_name", + [ + (1.0, 2013, 50, 93, "fconn_atlas"), + (1.0, 2013, 50, 93, "fconn_atlas"), + (1.0, 2013, 100, 184, "fconn_atlas"), + (1.0, 2013, 100, 184, "fconn_atlas"), + (1.0, 2013, 150, 278, "fconn_atlas"), + (1.0, 2013, 150, 278, "fconn_atlas"), + (1.0, 2015, 268, 268, "268_parcellation"), + (1.0, 2015, 268, 268, "268_parcellation"), + (1.0, 2019, 368, 368, "368_parcellation"), + ], +) +def test_shen( + tmp_path: Path, + resolution: float, + year: int, + n_rois: int, + n_labels: int, + img_name: str, +) -> None: + """Test Shen parcellation. + + Parameters + ---------- + tmp_path : pathlib.Path + The path to the test directory. + resolution : float + The parametrized resolution values. + year : int + The parametrized year values. + n_rois : int + The parametrized ROI count values. + n_labels : int + The parametrized label count values. + img_name : str + The parametrized partial file names. + + """ + parcellations = list_parcellations() + assert f"Shen_{year}_{n_rois}" in parcellations + # Load parcellation + img, label, img_path = load_parcellation( + name=f"Shen_{year}_{n_rois}", parcellations_dir=tmp_path + ) + assert img is not None + assert img_name in img_path.name + assert len(label) == n_labels + assert_array_equal( + img.header["pixdim"][1:4], 3 * [resolution] # type: ignore + ) + + +def test_retrieve_shen_incorrect_year(tmp_path: Path) -> None: + """Test retrieve Shen with incorrect year. + + Parameters + ---------- + tmp_path : pathlib.Path + The path to the test directory. + + """ + with pytest.raises(ValueError, match="The parameter `year`"): + _retrieve_shen( + parcellations_dir=tmp_path, + year=1969, + ) + + +def test_retrieve_shen_incorrect_n_rois(tmp_path: Path) -> None: + """Test retrieve Shen with incorrect ROIs. + + Parameters + ---------- + tmp_path : pathlib.Path + The path to the test directory. + + """ + with pytest.raises(ValueError, match="The parameter `n_rois`"): + _retrieve_shen( + parcellations_dir=tmp_path, + year=2015, + n_rois=10, + ) + + +@pytest.mark.parametrize( + "resolution, year, n_rois", + [ + (2.0, 2019, 368), + (1.0, 2013, 268), + (1.0, 2013, 368), + (1.0, 2015, 50), + (1.0, 2015, 100), + (1.0, 2015, 150), + (1.0, 2019, 50), + (1.0, 2019, 100), + (1.0, 2019, 150), + (1.0, 2015, 368), + (1.0, 2019, 268), + ], +) +def test_retrieve_shen_incorrect_param_combo( + tmp_path: Path, + resolution: float, + year: int, + n_rois: int, +) -> None: + """Test retrieve Shen with incorrect parameter combinations. + + Parameters + ---------- + tmp_path : pathlib.Path + The path to the test directory. + resolution : float + The parametrized resolution values. + year : int + The parametrized year values. + n_rois : int + The parametrized ROI count values. + + """ + with pytest.raises(ValueError, match="The parameter combination"): + _retrieve_shen( + parcellations_dir=tmp_path, + resolution=resolution, + year=year, + n_rois=n_rois, + ) + + def test_merge_parcellations() -> None: """Test merging parcellations.""" # load some parcellations for testing