junifer 0.0.3.dev188__py3-none-any.whl → 0.0.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. junifer/_version.py +14 -2
  2. junifer/api/cli.py +162 -17
  3. junifer/api/functions.py +87 -419
  4. junifer/api/parser.py +24 -0
  5. junifer/api/queue_context/__init__.py +8 -0
  6. junifer/api/queue_context/gnu_parallel_local_adapter.py +258 -0
  7. junifer/api/queue_context/htcondor_adapter.py +365 -0
  8. junifer/api/queue_context/queue_context_adapter.py +60 -0
  9. junifer/api/queue_context/tests/test_gnu_parallel_local_adapter.py +192 -0
  10. junifer/api/queue_context/tests/test_htcondor_adapter.py +257 -0
  11. junifer/api/res/afni/run_afni_docker.sh +6 -6
  12. junifer/api/res/ants/ResampleImage +3 -0
  13. junifer/api/res/ants/antsApplyTransforms +3 -0
  14. junifer/api/res/ants/antsApplyTransformsToPoints +3 -0
  15. junifer/api/res/ants/run_ants_docker.sh +39 -0
  16. junifer/api/res/fsl/applywarp +3 -0
  17. junifer/api/res/fsl/flirt +3 -0
  18. junifer/api/res/fsl/img2imgcoord +3 -0
  19. junifer/api/res/fsl/run_fsl_docker.sh +39 -0
  20. junifer/api/res/fsl/std2imgcoord +3 -0
  21. junifer/api/res/run_conda.sh +4 -4
  22. junifer/api/res/run_venv.sh +22 -0
  23. junifer/api/tests/data/partly_cloudy_agg_mean_tian.yml +16 -0
  24. junifer/api/tests/test_api_utils.py +21 -3
  25. junifer/api/tests/test_cli.py +232 -9
  26. junifer/api/tests/test_functions.py +211 -439
  27. junifer/api/tests/test_parser.py +1 -1
  28. junifer/configs/juseless/datagrabbers/aomic_id1000_vbm.py +6 -1
  29. junifer/configs/juseless/datagrabbers/camcan_vbm.py +6 -1
  30. junifer/configs/juseless/datagrabbers/ixi_vbm.py +6 -1
  31. junifer/configs/juseless/datagrabbers/tests/test_ucla.py +8 -8
  32. junifer/configs/juseless/datagrabbers/ucla.py +44 -26
  33. junifer/configs/juseless/datagrabbers/ukb_vbm.py +6 -1
  34. junifer/data/VOIs/meta/AutobiographicalMemory_VOIs.txt +23 -0
  35. junifer/data/VOIs/meta/Power2013_MNI_VOIs.tsv +264 -0
  36. junifer/data/__init__.py +4 -0
  37. junifer/data/coordinates.py +298 -31
  38. junifer/data/masks.py +360 -28
  39. junifer/data/parcellations.py +621 -188
  40. junifer/data/template_spaces.py +190 -0
  41. junifer/data/tests/test_coordinates.py +34 -3
  42. junifer/data/tests/test_data_utils.py +1 -0
  43. junifer/data/tests/test_masks.py +202 -86
  44. junifer/data/tests/test_parcellations.py +266 -55
  45. junifer/data/tests/test_template_spaces.py +104 -0
  46. junifer/data/utils.py +4 -2
  47. junifer/datagrabber/__init__.py +1 -0
  48. junifer/datagrabber/aomic/id1000.py +111 -70
  49. junifer/datagrabber/aomic/piop1.py +116 -53
  50. junifer/datagrabber/aomic/piop2.py +116 -53
  51. junifer/datagrabber/aomic/tests/test_id1000.py +27 -27
  52. junifer/datagrabber/aomic/tests/test_piop1.py +27 -27
  53. junifer/datagrabber/aomic/tests/test_piop2.py +27 -27
  54. junifer/datagrabber/base.py +62 -10
  55. junifer/datagrabber/datalad_base.py +0 -2
  56. junifer/datagrabber/dmcc13_benchmark.py +372 -0
  57. junifer/datagrabber/hcp1200/datalad_hcp1200.py +5 -0
  58. junifer/datagrabber/hcp1200/hcp1200.py +30 -13
  59. junifer/datagrabber/pattern.py +133 -27
  60. junifer/datagrabber/pattern_datalad.py +111 -13
  61. junifer/datagrabber/tests/test_base.py +57 -6
  62. junifer/datagrabber/tests/test_datagrabber_utils.py +204 -76
  63. junifer/datagrabber/tests/test_datalad_base.py +0 -6
  64. junifer/datagrabber/tests/test_dmcc13_benchmark.py +256 -0
  65. junifer/datagrabber/tests/test_multiple.py +43 -10
  66. junifer/datagrabber/tests/test_pattern.py +125 -178
  67. junifer/datagrabber/tests/test_pattern_datalad.py +44 -25
  68. junifer/datagrabber/utils.py +151 -16
  69. junifer/datareader/default.py +36 -10
  70. junifer/external/nilearn/junifer_nifti_spheres_masker.py +6 -0
  71. junifer/markers/base.py +25 -16
  72. junifer/markers/collection.py +35 -16
  73. junifer/markers/complexity/__init__.py +27 -0
  74. junifer/markers/complexity/complexity_base.py +149 -0
  75. junifer/markers/complexity/hurst_exponent.py +136 -0
  76. junifer/markers/complexity/multiscale_entropy_auc.py +140 -0
  77. junifer/markers/complexity/perm_entropy.py +132 -0
  78. junifer/markers/complexity/range_entropy.py +136 -0
  79. junifer/markers/complexity/range_entropy_auc.py +145 -0
  80. junifer/markers/complexity/sample_entropy.py +134 -0
  81. junifer/markers/complexity/tests/test_complexity_base.py +19 -0
  82. junifer/markers/complexity/tests/test_hurst_exponent.py +69 -0
  83. junifer/markers/complexity/tests/test_multiscale_entropy_auc.py +68 -0
  84. junifer/markers/complexity/tests/test_perm_entropy.py +68 -0
  85. junifer/markers/complexity/tests/test_range_entropy.py +69 -0
  86. junifer/markers/complexity/tests/test_range_entropy_auc.py +69 -0
  87. junifer/markers/complexity/tests/test_sample_entropy.py +68 -0
  88. junifer/markers/complexity/tests/test_weighted_perm_entropy.py +68 -0
  89. junifer/markers/complexity/weighted_perm_entropy.py +133 -0
  90. junifer/markers/falff/_afni_falff.py +153 -0
  91. junifer/markers/falff/_junifer_falff.py +142 -0
  92. junifer/markers/falff/falff_base.py +91 -84
  93. junifer/markers/falff/falff_parcels.py +61 -45
  94. junifer/markers/falff/falff_spheres.py +64 -48
  95. junifer/markers/falff/tests/test_falff_parcels.py +89 -121
  96. junifer/markers/falff/tests/test_falff_spheres.py +92 -127
  97. junifer/markers/functional_connectivity/crossparcellation_functional_connectivity.py +1 -0
  98. junifer/markers/functional_connectivity/edge_functional_connectivity_parcels.py +1 -0
  99. junifer/markers/functional_connectivity/functional_connectivity_base.py +1 -0
  100. junifer/markers/functional_connectivity/tests/test_crossparcellation_functional_connectivity.py +46 -44
  101. junifer/markers/functional_connectivity/tests/test_edge_functional_connectivity_parcels.py +34 -39
  102. junifer/markers/functional_connectivity/tests/test_edge_functional_connectivity_spheres.py +40 -52
  103. junifer/markers/functional_connectivity/tests/test_functional_connectivity_parcels.py +62 -70
  104. junifer/markers/functional_connectivity/tests/test_functional_connectivity_spheres.py +99 -85
  105. junifer/markers/parcel_aggregation.py +60 -38
  106. junifer/markers/reho/_afni_reho.py +192 -0
  107. junifer/markers/reho/_junifer_reho.py +281 -0
  108. junifer/markers/reho/reho_base.py +69 -34
  109. junifer/markers/reho/reho_parcels.py +26 -16
  110. junifer/markers/reho/reho_spheres.py +23 -9
  111. junifer/markers/reho/tests/test_reho_parcels.py +93 -92
  112. junifer/markers/reho/tests/test_reho_spheres.py +88 -86
  113. junifer/markers/sphere_aggregation.py +54 -9
  114. junifer/markers/temporal_snr/temporal_snr_base.py +1 -0
  115. junifer/markers/temporal_snr/tests/test_temporal_snr_parcels.py +38 -37
  116. junifer/markers/temporal_snr/tests/test_temporal_snr_spheres.py +34 -38
  117. junifer/markers/tests/test_collection.py +43 -42
  118. junifer/markers/tests/test_ets_rss.py +29 -37
  119. junifer/markers/tests/test_parcel_aggregation.py +587 -468
  120. junifer/markers/tests/test_sphere_aggregation.py +209 -157
  121. junifer/markers/utils.py +2 -40
  122. junifer/onthefly/read_transform.py +13 -6
  123. junifer/pipeline/__init__.py +1 -0
  124. junifer/pipeline/pipeline_step_mixin.py +105 -41
  125. junifer/pipeline/registry.py +17 -0
  126. junifer/pipeline/singleton.py +45 -0
  127. junifer/pipeline/tests/test_pipeline_step_mixin.py +139 -51
  128. junifer/pipeline/tests/test_update_meta_mixin.py +1 -0
  129. junifer/pipeline/tests/test_workdir_manager.py +104 -0
  130. junifer/pipeline/update_meta_mixin.py +8 -2
  131. junifer/pipeline/utils.py +154 -15
  132. junifer/pipeline/workdir_manager.py +246 -0
  133. junifer/preprocess/__init__.py +3 -0
  134. junifer/preprocess/ants/__init__.py +4 -0
  135. junifer/preprocess/ants/ants_apply_transforms_warper.py +185 -0
  136. junifer/preprocess/ants/tests/test_ants_apply_transforms_warper.py +56 -0
  137. junifer/preprocess/base.py +96 -69
  138. junifer/preprocess/bold_warper.py +265 -0
  139. junifer/preprocess/confounds/fmriprep_confound_remover.py +91 -134
  140. junifer/preprocess/confounds/tests/test_fmriprep_confound_remover.py +106 -111
  141. junifer/preprocess/fsl/__init__.py +4 -0
  142. junifer/preprocess/fsl/apply_warper.py +179 -0
  143. junifer/preprocess/fsl/tests/test_apply_warper.py +45 -0
  144. junifer/preprocess/tests/test_bold_warper.py +159 -0
  145. junifer/preprocess/tests/test_preprocess_base.py +6 -6
  146. junifer/preprocess/warping/__init__.py +6 -0
  147. junifer/preprocess/warping/_ants_warper.py +167 -0
  148. junifer/preprocess/warping/_fsl_warper.py +109 -0
  149. junifer/preprocess/warping/space_warper.py +213 -0
  150. junifer/preprocess/warping/tests/test_space_warper.py +198 -0
  151. junifer/stats.py +18 -4
  152. junifer/storage/base.py +9 -1
  153. junifer/storage/hdf5.py +8 -3
  154. junifer/storage/pandas_base.py +2 -1
  155. junifer/storage/sqlite.py +1 -0
  156. junifer/storage/tests/test_hdf5.py +2 -1
  157. junifer/storage/tests/test_sqlite.py +8 -8
  158. junifer/storage/tests/test_utils.py +6 -6
  159. junifer/storage/utils.py +1 -0
  160. junifer/testing/datagrabbers.py +11 -7
  161. junifer/testing/utils.py +1 -0
  162. junifer/tests/test_stats.py +2 -0
  163. junifer/utils/__init__.py +1 -0
  164. junifer/utils/helpers.py +53 -0
  165. junifer/utils/logging.py +14 -3
  166. junifer/utils/tests/test_helpers.py +35 -0
  167. {junifer-0.0.3.dev188.dist-info → junifer-0.0.4.dist-info}/METADATA +59 -28
  168. junifer-0.0.4.dist-info/RECORD +257 -0
  169. {junifer-0.0.3.dev188.dist-info → junifer-0.0.4.dist-info}/WHEEL +1 -1
  170. junifer/markers/falff/falff_estimator.py +0 -334
  171. junifer/markers/falff/tests/test_falff_estimator.py +0 -238
  172. junifer/markers/reho/reho_estimator.py +0 -515
  173. junifer/markers/reho/tests/test_reho_estimator.py +0 -260
  174. junifer-0.0.3.dev188.dist-info/RECORD +0 -199
  175. {junifer-0.0.3.dev188.dist-info → junifer-0.0.4.dist-info}/AUTHORS.rst +0 -0
  176. {junifer-0.0.3.dev188.dist-info → junifer-0.0.4.dist-info}/LICENSE.md +0 -0
  177. {junifer-0.0.3.dev188.dist-info → junifer-0.0.4.dist-info}/entry_points.txt +0 -0
  178. {junifer-0.0.3.dev188.dist-info → junifer-0.0.4.dist-info}/top_level.txt +0 -0
@@ -9,18 +9,20 @@ import io
9
9
  import shutil
10
10
  import tarfile
11
11
  import tempfile
12
+ import typing
12
13
  import zipfile
13
14
  from pathlib import Path
14
15
  from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union
15
16
 
17
+ import httpx
16
18
  import nibabel as nib
17
19
  import numpy as np
18
20
  import pandas as pd
19
- import requests
20
21
  from nilearn import datasets, image
21
- from requests.exceptions import ConnectionError, HTTPError, ReadTimeout
22
22
 
23
- from ..utils.logging import logger, raise_error, warn_with_log
23
+ from ..pipeline import WorkDirManager
24
+ from ..utils import logger, raise_error, run_ext_cmd, warn_with_log
25
+ from .template_spaces import get_template, get_xfm
24
26
  from .utils import closest_resolution
25
27
 
26
28
 
@@ -33,6 +35,7 @@ if TYPE_CHECKING:
33
35
 
34
36
  # Each entry is a dictionary that must contain at least the following keys:
35
37
  # * 'family': the parcellation's family name (e.g. 'Schaefer', 'SUIT')
38
+ # * 'space': the parcellation's space (e.g., 'MNI', 'SUIT')
36
39
 
37
40
  # Optional keys:
38
41
  # * 'valid_resolutions': a list of valid resolutions for the parcellation
@@ -41,7 +44,7 @@ if TYPE_CHECKING:
41
44
  # TODO: have separate dictionary for built-in
42
45
  _available_parcellations: Dict[str, Dict[Any, Any]] = {
43
46
  "SUITxSUIT": {"family": "SUIT", "space": "SUIT"},
44
- "SUITxMNI": {"family": "SUIT", "space": "MNI"},
47
+ "SUITxMNI": {"family": "SUIT", "space": "MNI152NLin6Asym"},
45
48
  }
46
49
 
47
50
  # Add Schaefer parcellation info
@@ -52,6 +55,7 @@ for n_rois in range(100, 1001, 100):
52
55
  "family": "Schaefer",
53
56
  "n_rois": n_rois,
54
57
  "yeo_networks": t_net,
58
+ "space": "MNI152NLin6Asym",
55
59
  }
56
60
  # Add Tian parcellation info
57
61
  for scale in range(1, 5):
@@ -60,27 +64,28 @@ for scale in range(1, 5):
60
64
  "family": "Tian",
61
65
  "scale": scale,
62
66
  "magneticfield": "7T",
63
- "space": "MNI6thgeneration",
67
+ "space": "MNI152NLin6Asym",
64
68
  }
65
69
  t_name = f"TianxS{scale}x3TxMNI6thgeneration"
66
70
  _available_parcellations[t_name] = {
67
71
  "family": "Tian",
68
72
  "scale": scale,
69
73
  "magneticfield": "3T",
70
- "space": "MNI6thgeneration",
74
+ "space": "MNI152NLin6Asym",
71
75
  }
72
76
  t_name = f"TianxS{scale}x3TxMNInonlinear2009cAsym"
73
77
  _available_parcellations[t_name] = {
74
78
  "family": "Tian",
75
79
  "scale": scale,
76
80
  "magneticfield": "3T",
77
- "space": "MNInonlinear2009cAsym",
81
+ "space": "MNI152NLin2009cAsym",
78
82
  }
79
83
  # Add AICHA parcellation info
80
84
  for version in (1, 2):
81
85
  _available_parcellations[f"AICHA_v{version}"] = {
82
86
  "family": "AICHA",
83
87
  "version": version,
88
+ "space": "IXI549Space",
84
89
  }
85
90
  # Add Shen parcellation info
86
91
  for year in (2013, 2015, 2019):
@@ -90,18 +95,21 @@ for year in (2013, 2015, 2019):
90
95
  "family": "Shen",
91
96
  "year": 2013,
92
97
  "n_rois": n_rois,
98
+ "space": "MNI152NLin2009cAsym",
93
99
  }
94
100
  elif year == 2015:
95
101
  _available_parcellations["Shen_2015_268"] = {
96
102
  "family": "Shen",
97
103
  "year": 2015,
98
104
  "n_rois": 268,
105
+ "space": "MNI152NLin2009cAsym",
99
106
  }
100
107
  elif year == 2019:
101
108
  _available_parcellations["Shen_2019_368"] = {
102
109
  "family": "Shen",
103
110
  "year": 2019,
104
111
  "n_rois": 368,
112
+ "space": "MNI152NLin2009cAsym",
105
113
  }
106
114
  # Add Yan parcellation info
107
115
  for n_rois in range(100, 1001, 100):
@@ -111,12 +119,21 @@ for n_rois in range(100, 1001, 100):
111
119
  "family": "Yan",
112
120
  "n_rois": n_rois,
113
121
  "yeo_networks": yeo_network,
122
+ "space": "MNI152NLin6Asym",
114
123
  }
115
124
  # Add Kong networks
116
125
  _available_parcellations[f"Yan{n_rois}xKong17"] = {
117
126
  "family": "Yan",
118
127
  "n_rois": n_rois,
119
128
  "kong_networks": 17,
129
+ "space": "MNI152NLin6Asym",
130
+ }
131
+ # Add Brainnetome parcellation info
132
+ for threshold in [0, 25, 50]:
133
+ _available_parcellations[f"Brainnetome_thr{threshold}"] = {
134
+ "family": "Brainnetome",
135
+ "threshold": threshold,
136
+ "space": "MNI152NLin6Asym",
120
137
  }
121
138
 
122
139
 
@@ -124,6 +141,7 @@ def register_parcellation(
124
141
  name: str,
125
142
  parcellation_path: Union[str, Path],
126
143
  parcels_labels: List[str],
144
+ space: str,
127
145
  overwrite: bool = False,
128
146
  ) -> None:
129
147
  """Register a custom user parcellation.
@@ -136,6 +154,8 @@ def register_parcellation(
136
154
  The path to the parcellation file.
137
155
  parcels_labels : list of str
138
156
  The list of labels for the parcellation.
157
+ space : str
158
+ The template space of the parcellation, for e.g., "MNI152NLin6Asym".
139
159
  overwrite : bool, optional
140
160
  If True, overwrite an existing parcellation with the same name.
141
161
  Does not apply to built-in parcellations (default False).
@@ -161,8 +181,8 @@ def register_parcellation(
161
181
  )
162
182
  else:
163
183
  raise_error(
164
- f"Parcellation {name} already registered. Set `overwrite=True`"
165
- "to update its value."
184
+ f"Parcellation {name} already registered. Set "
185
+ "`overwrite=True` to update its value."
166
186
  )
167
187
  # Convert str to Path
168
188
  if not isinstance(parcellation_path, Path):
@@ -172,6 +192,7 @@ def register_parcellation(
172
192
  "path": str(parcellation_path.absolute()),
173
193
  "labels": parcels_labels,
174
194
  "family": "CustomUserParcellation",
195
+ "space": space,
175
196
  }
176
197
 
177
198
 
@@ -187,12 +208,210 @@ def list_parcellations() -> List[str]:
187
208
  return sorted(_available_parcellations.keys())
188
209
 
189
210
 
190
- # def _check_resolution(resolution, valid_resolution):
191
- # if resolution is None:
192
- # return None
193
- # if resolution not in valid_resolution:
194
- # raise ValueError(f'Invalid resolution: {resolution}')
195
- # return resolution
211
+ def get_parcellation(
212
+ parcellation: List[str],
213
+ target_data: Dict[str, Any],
214
+ extra_input: Optional[Dict[str, Any]] = None,
215
+ ) -> Tuple["Nifti1Image", List[str]]:
216
+ """Get parcellation, tailored for the target image.
217
+
218
+ Parameters
219
+ ----------
220
+ parcellation : list of str
221
+ The name(s) of the parcellation(s).
222
+ target_data : dict
223
+ The corresponding item of the data object to which the parcellation
224
+ will be applied.
225
+ extra_input : dict, optional
226
+ The other fields in the data object. Useful for accessing other data
227
+ kinds that needs to be used in the computation of parcellations
228
+ (default None).
229
+
230
+ Returns
231
+ -------
232
+ Nifti1Image
233
+ The parcellation image.
234
+ list of str
235
+ Parcellation labels.
236
+
237
+ Raises
238
+ ------
239
+ RuntimeError
240
+ If warp / transformation file extension is not ".mat" or ".h5".
241
+ ValueError
242
+ If ``extra_input`` is None when ``target_data``'s space is native.
243
+
244
+ """
245
+ # Check pre-requirements for space manipulation
246
+ target_space = target_data["space"]
247
+ # Set target standard space to target space
248
+ target_std_space = target_space
249
+ # Extra data type requirement check if target space is native
250
+ if target_space == "native":
251
+ # Check for extra inputs
252
+ if extra_input is None:
253
+ raise_error(
254
+ "No extra input provided, requires `Warp` and `T1w` "
255
+ "data types in particular for transformation to "
256
+ f"{target_data['space']} space for further computation."
257
+ )
258
+ # Set target standard space to warp file space source
259
+ target_std_space = extra_input["Warp"]["src"]
260
+
261
+ # Get the min of the voxels sizes and use it as the resolution
262
+ target_img = target_data["data"]
263
+ resolution = np.min(target_img.header.get_zooms()[:3])
264
+
265
+ # Create component-scoped tempdir
266
+ tempdir = WorkDirManager().get_tempdir(prefix="parcellations")
267
+ # Create element-scoped tempdir so that warped parcellation is
268
+ # available later as nibabel stores file path reference for
269
+ # loading on computation
270
+ element_tempdir = WorkDirManager().get_element_tempdir(
271
+ prefix="parcellations"
272
+ )
273
+
274
+ # Load the parcellations
275
+ all_parcellations = []
276
+ all_labels = []
277
+ for name in parcellation:
278
+ img, labels, _, space = load_parcellation(
279
+ name=name,
280
+ resolution=resolution,
281
+ )
282
+
283
+ # Convert parcellation spaces if required
284
+ if space != target_std_space:
285
+ # Get xfm file
286
+ xfm_file_path = get_xfm(src=space, dst=target_std_space)
287
+ # Get target standard space template
288
+ target_std_space_template_img = get_template(
289
+ space=target_std_space,
290
+ target_data=target_data,
291
+ extra_input=extra_input,
292
+ )
293
+
294
+ # Save parcellation image to a component-scoped tempfile
295
+ parcellation_path = tempdir / f"{name}.nii.gz"
296
+ nib.save(img, parcellation_path)
297
+
298
+ # Save template
299
+ target_std_space_template_path = (
300
+ tempdir / f"{target_std_space}_T1w_{resolution}.nii.gz"
301
+ )
302
+ nib.save(
303
+ target_std_space_template_img, target_std_space_template_path
304
+ )
305
+
306
+ # Set warped parcellation path
307
+ warped_parcellation_path = element_tempdir / (
308
+ f"{name}_warped_from_{space}_to_" f"{target_std_space}.nii.gz"
309
+ )
310
+
311
+ logger.debug(
312
+ f"Using ANTs to warp {name} "
313
+ f"from {space} to {target_std_space}"
314
+ )
315
+ # Set antsApplyTransforms command
316
+ apply_transforms_cmd = [
317
+ "antsApplyTransforms",
318
+ "-d 3",
319
+ "-e 3",
320
+ "-n 'GenericLabel[NearestNeighbor]'",
321
+ f"-i {parcellation_path.resolve()}",
322
+ f"-r {target_std_space_template_path.resolve()}",
323
+ f"-t {xfm_file_path.resolve()}",
324
+ f"-o {warped_parcellation_path.resolve()}",
325
+ ]
326
+ # Call antsApplyTransforms
327
+ run_ext_cmd(name="antsApplyTransforms", cmd=apply_transforms_cmd)
328
+
329
+ img = nib.load(warped_parcellation_path)
330
+
331
+ # Resample parcellation to target image
332
+ img_to_merge = image.resample_to_img(
333
+ source_img=img,
334
+ target_img=target_img,
335
+ interpolation="nearest",
336
+ copy=True,
337
+ )
338
+
339
+ all_parcellations.append(img_to_merge)
340
+ all_labels.append(labels)
341
+
342
+ # Avoid merging if there is only one parcellation
343
+ if len(all_parcellations) == 1:
344
+ resampled_parcellation_img = all_parcellations[0]
345
+ labels = all_labels[0]
346
+ # Parcellations are already transformed to target standard space
347
+ else:
348
+ resampled_parcellation_img, labels = merge_parcellations(
349
+ parcellations_list=all_parcellations,
350
+ parcellations_names=parcellation,
351
+ labels_lists=all_labels,
352
+ )
353
+
354
+ # Warp parcellation if target space is native
355
+ if target_space == "native":
356
+ # Save parcellation image to a component-scoped tempfile
357
+ prewarp_parcellation_path = tempdir / "prewarp_parcellation.nii.gz"
358
+ nib.save(resampled_parcellation_img, prewarp_parcellation_path)
359
+
360
+ # Create an element-scoped tempfile for warped output
361
+ warped_parcellation_path = (
362
+ element_tempdir / "parcellation_warped.nii.gz"
363
+ )
364
+
365
+ # Check for warp file type to use correct tool
366
+ warp_file_ext = extra_input["Warp"]["path"].suffix
367
+ if warp_file_ext == ".mat":
368
+ logger.debug("Using FSL for parcellation warping")
369
+ # Set applywarp command
370
+ applywarp_cmd = [
371
+ "applywarp",
372
+ "--interp=nn",
373
+ f"-i {prewarp_parcellation_path.resolve()}",
374
+ # use resampled reference
375
+ f"-r {target_data['reference_path'].resolve()}",
376
+ f"-w {extra_input['Warp']['path'].resolve()}",
377
+ f"-o {warped_parcellation_path.resolve()}",
378
+ ]
379
+ # Call applywarp
380
+ run_ext_cmd(name="applywarp", cmd=applywarp_cmd)
381
+
382
+ elif warp_file_ext == ".h5":
383
+ logger.debug("Using ANTs for parcellation warping")
384
+ # Set antsApplyTransforms command
385
+ apply_transforms_cmd = [
386
+ "antsApplyTransforms",
387
+ "-d 3",
388
+ "-e 3",
389
+ "-n 'GenericLabel[NearestNeighbor]'",
390
+ f"-i {prewarp_parcellation_path.resolve()}",
391
+ # use resampled reference
392
+ f"-r {target_data['reference_path'].resolve()}",
393
+ f"-t {extra_input['Warp']['path'].resolve()}",
394
+ f"-o {warped_parcellation_path.resolve()}",
395
+ ]
396
+ # Call antsApplyTransforms
397
+ run_ext_cmd(name="antsApplyTransforms", cmd=apply_transforms_cmd)
398
+
399
+ else:
400
+ raise_error(
401
+ msg=(
402
+ "Unknown warp / transformation file extension: "
403
+ f"{warp_file_ext}"
404
+ ),
405
+ klass=RuntimeError,
406
+ )
407
+
408
+ # Load nifti
409
+ resampled_parcellation_img = nib.load(warped_parcellation_path)
410
+
411
+ # Delete tempdir
412
+ WorkDirManager().delete_tempdir(tempdir)
413
+
414
+ return resampled_parcellation_img, labels # type: ignore
196
415
 
197
416
 
198
417
  def load_parcellation(
@@ -200,11 +419,11 @@ def load_parcellation(
200
419
  parcellations_dir: Union[str, Path, None] = None,
201
420
  resolution: Optional[float] = None,
202
421
  path_only: bool = False,
203
- ) -> Tuple[Optional["Nifti1Image"], List[str], Path]:
422
+ ) -> Tuple[Optional["Nifti1Image"], List[str], Path, str]:
204
423
  """Load a brain parcellation (including a label file).
205
424
 
206
- If it is a built-in parcellaions and file is not present in the
207
- `parcellations_dir` directory, it will be downloaded.
425
+ If it is a built-in parcellation and the file is not present in the
426
+ ``parcellations_dir`` directory, it will be downloaded.
208
427
 
209
428
  Parameters
210
429
  ----------
@@ -230,18 +449,33 @@ def load_parcellation(
230
449
  Parcellation labels.
231
450
  pathlib.Path
232
451
  File path to the parcellation image.
452
+ str
453
+ The space of the parcellation.
454
+
455
+ Raises
456
+ ------
457
+ ValueError
458
+ If ``name`` is invalid or if the parcellation values and labels
459
+ don't have equal dimension or if the value range is invalid.
233
460
 
234
461
  """
235
- # Invalid parcellation name
462
+ # Check for valid parcellation name
236
463
  if name not in _available_parcellations:
237
464
  raise_error(
238
465
  f"Parcellation {name} not found. "
239
466
  f"Valid options are: {list_parcellations()}"
240
467
  )
241
468
 
469
+ # Copy parcellation definition to avoid edits in original object
242
470
  parcellation_definition = _available_parcellations[name].copy()
243
471
  t_family = parcellation_definition.pop("family")
472
+ # Remove space conditionally
473
+ if t_family not in ["SUIT", "Tian"]:
474
+ space = parcellation_definition.pop("space")
475
+ else:
476
+ space = parcellation_definition["space"]
244
477
 
478
+ # Check if the parcellation family is custom or built-in
245
479
  if t_family == "CustomUserParcellation":
246
480
  parcellation_fname = Path(parcellation_definition["path"])
247
481
  parcellation_labels = parcellation_definition["labels"]
@@ -253,25 +487,32 @@ def load_parcellation(
253
487
  **parcellation_definition,
254
488
  )
255
489
 
490
+ # Load parcellation image and values
256
491
  logger.info(f"Loading parcellation {parcellation_fname.absolute()!s}")
257
-
258
492
  parcellation_img = None
259
493
  if path_only is False:
494
+ # Load image via nibabel
260
495
  parcellation_img = nib.load(parcellation_fname)
496
+ # Get unique values
261
497
  parcel_values = np.unique(parcellation_img.get_fdata())
498
+ # Check for dimension
262
499
  if len(parcel_values) - 1 != len(parcellation_labels):
263
500
  raise_error(
264
501
  f"Parcellation {name} has {len(parcel_values) - 1} parcels "
265
502
  f"but {len(parcellation_labels)} labels."
266
503
  )
504
+ # Sort values
267
505
  parcel_values.sort()
506
+ # Check if value range is invalid
268
507
  if np.any(np.diff(parcel_values) != 1):
269
508
  raise_error(
270
509
  f"Parcellation {name} must have all the values in the range "
271
510
  f"[0, {len(parcel_values)}]."
272
511
  )
273
512
 
274
- return parcellation_img, parcellation_labels, parcellation_fname
513
+ # Type-cast to remove errors
514
+ parcellation_img = typing.cast("Nifti1Image", parcellation_img)
515
+ return parcellation_img, parcellation_labels, parcellation_fname, space
275
516
 
276
517
 
277
518
  def _retrieve_parcellation(
@@ -312,13 +553,13 @@ def _retrieve_parcellation(
312
553
  * Tian :
313
554
  ``scale`` : {1, 2, 3, 4}
314
555
  Scale of parcellation (defines granularity).
315
- ``space`` : {"MNI6thgeneration", "MNInonlinear2009cAsym"}, optional
316
- Space of parcellation (default "MNI6thgeneration"). (For more
556
+ ``space`` : {"MNI152NLin6Asym", "MNI152NLin2009cAsym"}, optional
557
+ Space of parcellation (default "MNI152NLin6Asym"). (For more
317
558
  information see https://github.com/yetianmed/subcortex)
318
559
  ``magneticfield`` : {"3T", "7T"}, optional
319
560
  Magnetic field (default "3T").
320
561
  * SUIT :
321
- ``space`` : {"MNI", "SUIT"}, optional
562
+ ``space`` : {"MNI152NLin6Asym", "SUIT"}, optional
322
563
  Space of parcellation (default "MNI"). (For more information
323
564
  see http://www.diedrichsenlab.org/imaging/suit.htm).
324
565
  * AICHA :
@@ -338,6 +579,9 @@ def _retrieve_parcellation(
338
579
  Number of Yeo networks to use (default None).
339
580
  ``kong_networks`` : {17}, optional
340
581
  Number of Kong networks to use (default None).
582
+ * Brainnetome :
583
+ ``threshold`` : {0, 25, 50}
584
+ Threshold for the probabilistic maps of subregion.
341
585
 
342
586
  Returns
343
587
  -------
@@ -401,6 +645,12 @@ def _retrieve_parcellation(
401
645
  resolution=resolution,
402
646
  **kwargs,
403
647
  )
648
+ elif family == "Brainnetome":
649
+ parcellation_fname, parcellation_labels = _retrieve_brainnetome(
650
+ parcellations_dir=parcellations_dir,
651
+ resolution=resolution,
652
+ **kwargs,
653
+ )
404
654
  else:
405
655
  raise_error(
406
656
  f"The provided parcellation name {family} cannot be retrieved."
@@ -451,24 +701,27 @@ def _retrieve_schaefer(
451
701
  logger.info(f"\tn_rois: {n_rois}")
452
702
  logger.info(f"\tyeo_networks: {yeo_networks}")
453
703
 
704
+ # Check n_rois value
454
705
  _valid_n_rois = [100, 200, 300, 400, 500, 600, 700, 800, 900, 1000]
455
- _valid_networks = [7, 17]
456
- _valid_resolutions = [1, 2]
457
-
458
706
  if n_rois not in _valid_n_rois:
459
707
  raise_error(
460
708
  f"The parameter `n_rois` ({n_rois}) needs to be one of the "
461
709
  f"following: {_valid_n_rois}"
462
710
  )
711
+
712
+ # Check networks
713
+ _valid_networks = [7, 17]
463
714
  if yeo_networks not in _valid_networks:
464
715
  raise_error(
465
716
  f"The parameter `yeo_networks` ({yeo_networks}) needs to be one "
466
717
  f"of the following: {_valid_networks}"
467
718
  )
468
719
 
720
+ # Check resolution
721
+ _valid_resolutions = [1, 2]
469
722
  resolution = closest_resolution(resolution, _valid_resolutions)
470
723
 
471
- # define file names
724
+ # Define parcellation and label file names
472
725
  parcellation_fname = (
473
726
  parcellations_dir
474
727
  / "schaefer_2018"
@@ -483,7 +736,7 @@ def _retrieve_schaefer(
483
736
  / (f"Schaefer2018_{n_rois}Parcels_{yeo_networks}Networks_order.txt")
484
737
  )
485
738
 
486
- # check existence of parcellation
739
+ # Check existence of parcellation
487
740
  if not (parcellation_fname.exists() and parcellation_lname.exists()):
488
741
  logger.info(
489
742
  "At least one of the parcellation files are missing. "
@@ -493,14 +746,9 @@ def _retrieve_schaefer(
493
746
  n_rois=n_rois,
494
747
  yeo_networks=yeo_networks,
495
748
  resolution_mm=resolution, # type: ignore we know it's 1 or 2
496
- data_dir=str(parcellations_dir.absolute()),
749
+ data_dir=parcellations_dir.resolve(),
497
750
  )
498
751
 
499
- if not (
500
- parcellation_fname.exists() and parcellation_lname.exists()
501
- ): # pragma: no cover
502
- raise_error("There was a problem fetching the parcellations.")
503
-
504
752
  # Load labels
505
753
  labels = [
506
754
  "_".join(x.split("_")[1:])
@@ -516,7 +764,7 @@ def _retrieve_tian(
516
764
  parcellations_dir: Path,
517
765
  resolution: Optional[float] = None,
518
766
  scale: Optional[int] = None,
519
- space: str = "MNI6thgeneration",
767
+ space: str = "MNI152NLin6Asym",
520
768
  magneticfield: str = "3T",
521
769
  ) -> Tuple[Path, List[str]]:
522
770
  """Retrieve Tian parcellation.
@@ -533,8 +781,8 @@ def _retrieve_tian(
533
781
  parcellation depend on the space and magnetic field.
534
782
  scale : {1, 2, 3, 4}, optional
535
783
  Scale of parcellation (defines granularity) (default None).
536
- space : {"MNI6thgeneration", "MNInonlinear2009cAsym"}, optional
537
- Space of parcellation (default "MNI6thgeneration"). (For more
784
+ space : {"MNI152NLin6Asym", "MNI152NLin2009cAsym"}, optional
785
+ Space of parcellation (default "MNI152NLin6Asym"). (For more
538
786
  information see https://github.com/yetianmed/subcortex)
539
787
  magneticfield : {"3T", "7T"}, optional
540
788
  Magnetic field (default "3T").
@@ -548,19 +796,20 @@ def _retrieve_tian(
548
796
 
549
797
  Raises
550
798
  ------
799
+ RuntimeError
800
+ If there is a problem fetching files.
551
801
  ValueError
552
802
  If invalid value is provided for ``scale`` or ``magneticfield`` or
553
- ``space`` or if there is a problem fetching the parcellation.
803
+ ``space``.
554
804
 
555
805
  """
556
- # show parameters to user
557
806
  logger.info("Parcellation parameters:")
558
807
  logger.info(f"\tresolution: {resolution}")
559
808
  logger.info(f"\tscale: {scale}")
560
809
  logger.info(f"\tspace: {space}")
561
810
  logger.info(f"\tmagneticfield: {magneticfield}")
562
811
 
563
- # check validity of parameters
812
+ # Check scale
564
813
  _valid_scales = [1, 2, 3, 4]
565
814
  if scale not in _valid_scales:
566
815
  raise_error(
@@ -568,12 +817,13 @@ def _retrieve_tian(
568
817
  f"following: {_valid_scales}"
569
818
  )
570
819
 
820
+ # Check resolution
571
821
  _valid_resolutions = [] # avoid pylance error
572
822
  if magneticfield == "3T":
573
- _valid_spaces = ["MNI6thgeneration", "MNInonlinear2009cAsym"]
574
- if space == "MNI6thgeneration":
823
+ _valid_spaces = ["MNI152NLin6Asym", "MNI152NLin2009cAsym"]
824
+ if space == "MNI152NLin6Asym":
575
825
  _valid_resolutions = [1, 2]
576
- elif space == "MNInonlinear2009cAsym":
826
+ elif space == "MNI152NLin2009cAsym":
577
827
  _valid_resolutions = [2]
578
828
  else:
579
829
  raise_error(
@@ -582,20 +832,19 @@ def _retrieve_tian(
582
832
  )
583
833
  elif magneticfield == "7T":
584
834
  _valid_resolutions = [1.6]
585
- if space != "MNI6thgeneration":
835
+ if space != "MNI152NLin6Asym":
586
836
  raise_error(
587
837
  f"The parameter `space` ({space}) for 7T needs to be "
588
- f"MNI6thgeneration"
838
+ f"MNI152NLin6Asym"
589
839
  )
590
840
  else:
591
841
  raise_error(
592
842
  f"The parameter `magneticfield` ({magneticfield}) needs to be "
593
843
  f"one of the following: 3T or 7T"
594
844
  )
595
-
596
845
  resolution = closest_resolution(resolution, _valid_resolutions)
597
846
 
598
- # define file names
847
+ # Define parcellation and label file names
599
848
  if magneticfield == "3T":
600
849
  parcellation_fname_base_3T = (
601
850
  parcellations_dir / "Tian2020MSA_v1.1" / "3T" / "Subcortex-Only"
@@ -603,7 +852,7 @@ def _retrieve_tian(
603
852
  parcellation_lname = parcellation_fname_base_3T / (
604
853
  f"Tian_Subcortex_S{scale}_3T_label.txt"
605
854
  )
606
- if space == "MNI6thgeneration":
855
+ if space == "MNI152NLin6Asym":
607
856
  parcellation_fname = parcellation_fname_base_3T / (
608
857
  f"Tian_Subcortex_S{scale}_{magneticfield}.nii.gz"
609
858
  )
@@ -612,7 +861,7 @@ def _retrieve_tian(
612
861
  parcellation_fname_base_3T
613
862
  / f"Tian_Subcortex_S{scale}_{magneticfield}_1mm.nii.gz"
614
863
  )
615
- elif space == "MNInonlinear2009cAsym":
864
+ elif space == "MNI152NLin2009cAsym":
616
865
  space = "2009cAsym"
617
866
  parcellation_fname = parcellation_fname_base_3T / (
618
867
  f"Tian_Subcortex_S{scale}_{magneticfield}_{space}.nii.gz"
@@ -644,45 +893,53 @@ def _retrieve_tian(
644
893
  "parcellation. A simple numbering scheme for distinction was "
645
894
  "therefore used."
646
895
  )
647
- else: # pragma: no cover
648
- raise_error("This should not happen. Please report this error.")
649
896
 
650
- # check existence of parcellation
897
+ # Check existence of parcellation
651
898
  if not (parcellation_fname.exists() and parcellation_lname.exists()):
652
899
  logger.info(
653
900
  "At least one of the parcellation files are missing, fetching."
654
901
  )
655
-
656
- url_basis = (
902
+ # Set URL
903
+ url = (
657
904
  "https://www.nitrc.org/frs/download.php/12012/Tian2020MSA_v1.1.zip"
658
905
  )
659
906
 
660
- logger.info(f"Downloading TIAN from {url_basis}")
907
+ logger.info(f"Downloading TIAN from {url}")
908
+ # Store initial download in a tempdir
661
909
  with tempfile.TemporaryDirectory() as tmpdir:
662
- parcellation_download = requests.get(url_basis)
663
- parcellation_zip_fname = Path(tmpdir) / "Tian2020MSA_v1.1.zip"
664
- with open(parcellation_zip_fname, "wb") as f:
665
- f.write(parcellation_download.content)
666
- with zipfile.ZipFile(parcellation_zip_fname, "r") as zip_ref:
667
- zip_ref.extractall(parcellations_dir.as_posix())
668
- # clean after unzipping
669
- if (parcellations_dir / "__MACOSX").exists():
670
- shutil.rmtree((parcellations_dir / "__MACOSX").as_posix())
671
-
672
- labels = pd.read_csv(parcellation_lname, sep=" ", header=None)[
673
- 0
674
- ].to_list()
675
-
676
- if not (parcellation_fname.exists() and parcellation_lname.exists()):
677
- raise_error("There was a problem fetching the parcellations.")
910
+ # Make HTTP request
911
+ try:
912
+ resp = httpx.get(url)
913
+ resp.raise_for_status()
914
+ except httpx.HTTPError as exc:
915
+ raise_error(
916
+ f"Error response {exc.response.status_code} while "
917
+ f"requesting {exc.request.url!r}",
918
+ klass=RuntimeError,
919
+ )
920
+ else:
921
+ # Set tempfile for storing initial content and unzipping
922
+ zip_fname = Path(tmpdir) / "Tian2020MSA_v1.1.zip"
923
+ # Open tempfile and write content
924
+ with open(zip_fname, "wb") as f:
925
+ f.write(resp.content)
926
+ # Unzip tempfile
927
+ with zipfile.ZipFile(zip_fname, "r") as zip_ref:
928
+ zip_ref.extractall(parcellations_dir.as_posix())
929
+ # Clean after unzipping
930
+ if (parcellations_dir / "__MACOSX").exists():
931
+ shutil.rmtree((parcellations_dir / "__MACOSX").as_posix())
678
932
 
933
+ # Load labels
679
934
  labels = pd.read_csv(parcellation_lname, sep=" ", header=None)[0].to_list()
680
935
 
681
936
  return parcellation_fname, labels
682
937
 
683
938
 
684
939
  def _retrieve_suit(
685
- parcellations_dir: Path, resolution: Optional[float], space: str = "MNI"
940
+ parcellations_dir: Path,
941
+ resolution: Optional[float],
942
+ space: str = "MNI152NLin6Asym",
686
943
  ) -> Tuple[Path, List[str]]:
687
944
  """Retrieve SUIT parcellation.
688
945
 
@@ -696,9 +953,9 @@ def _retrieve_suit(
696
953
  resolution higher than the desired one. By default, will load the
697
954
  highest one (default None). Available resolutions for this parcellation
698
955
  are 1mm and 2mm.
699
- space : {"MNI", "SUIT"}, optional
700
- Space of parcellation (default "MNI"). (For more information
701
- see http://www.diedrichsenlab.org/imaging/suit.htm).
956
+ space : {"MNI152NLin6Asym", "SUIT"}, optional
957
+ Space of parcellation (default "MNI152NLin6Asym"). (For more
958
+ information see http://www.diedrichsenlab.org/imaging/suit.htm).
702
959
 
703
960
  Returns
704
961
  -------
@@ -709,30 +966,33 @@ def _retrieve_suit(
709
966
 
710
967
  Raises
711
968
  ------
969
+ RuntimeError
970
+ If there is a problem fetching files.
712
971
  ValueError
713
- If invalid value is provided for ``space`` or if there is a problem
714
- fetching the parcellation.
972
+ If invalid value is provided for ``space``.
715
973
 
716
974
  """
717
975
  logger.info("Parcellation parameters:")
718
976
  logger.info(f"\tresolution: {resolution}")
719
977
  logger.info(f"\tspace: {space}")
720
978
 
721
- _valid_spaces = ["MNI", "SUIT"]
722
-
723
- # check validity of parameters
979
+ # Check space
980
+ _valid_spaces = ["MNI152NLin6Asym", "SUIT"]
724
981
  if space not in _valid_spaces:
725
982
  raise_error(
726
983
  f"The parameter `space` ({space}) needs to be one of the "
727
984
  f"following: {_valid_spaces}"
728
985
  )
729
986
 
730
- # TODO: Validate this with Vera
987
+ # Check resolution
731
988
  _valid_resolutions = [1]
732
-
733
989
  resolution = closest_resolution(resolution, _valid_resolutions)
734
990
 
735
- # define file names
991
+ # Format space if MNI; required for the file name
992
+ if space == "MNI152NLin6Asym":
993
+ space = "MNI"
994
+
995
+ # Define parcellation and label file names
736
996
  parcellation_fname = (
737
997
  parcellations_dir / "SUIT" / (f"SUIT_{space}Space_{resolution}mm.nii")
738
998
  )
@@ -740,45 +1000,61 @@ def _retrieve_suit(
740
1000
  parcellations_dir / "SUIT" / (f"SUIT_{space}Space_{resolution}mm.tsv")
741
1001
  )
742
1002
 
743
- # check existence of parcellation
1003
+ # Check existence of parcellation
744
1004
  if not (parcellation_fname.exists() and parcellation_lname.exists()):
745
- parcellation_fname.parent.mkdir(exist_ok=True, parents=True)
746
1005
  logger.info(
747
1006
  "At least one of the parcellation files is missing, fetching."
748
1007
  )
749
-
1008
+ # Create local directory if not present
1009
+ parcellation_fname.parent.mkdir(exist_ok=True, parents=True)
1010
+ # Set URL
750
1011
  url_basis = (
751
1012
  "https://github.com/DiedrichsenLab/cerebellar_atlases/raw"
752
- "/master/Diedrichsen_2009/"
1013
+ "/master/Diedrichsen_2009"
753
1014
  )
754
- url_MNI = url_basis + "atl-Anatom_space-MNI_dseg.nii"
755
- url_SUIT = url_basis + "atl-Anatom_space-SUIT_dseg.nii"
756
- url_labels = url_basis + "atl-Anatom.tsv"
757
-
758
1015
  if space == "MNI":
759
- logger.info(f"Downloading {url_MNI}")
760
- parcellation_download = requests.get(url_MNI)
761
- with open(parcellation_fname, "wb") as f:
762
- f.write(parcellation_download.content)
1016
+ url = f"{url_basis}/atl-Anatom_space-MNI_dseg.nii"
763
1017
  else: # if not MNI, then SUIT
764
- logger.info(f"Downloading {url_SUIT}")
765
- parcellation_download = requests.get(url_SUIT)
766
- with open(parcellation_fname, "wb") as f:
767
- f.write(parcellation_download.content)
768
-
769
- labels_download = requests.get(url_labels)
770
- labels = pd.read_csv(
771
- io.StringIO(labels_download.content.decode("utf-8")),
772
- sep="\t",
773
- usecols=["name"],
774
- )
1018
+ url = f"{url_basis}/atl-Anatom_space-SUIT_dseg.nii"
1019
+ url_labels = f"{url_basis}/atl-Anatom.tsv"
775
1020
 
776
- labels.to_csv(parcellation_lname, sep="\t", index=False)
777
- if (
778
- not parcellation_fname.exists() and parcellation_lname.exists()
779
- ): # pragma: no cover
780
- raise_error("There was a problem fetching the parcellations.")
1021
+ # Make HTTP requests
1022
+ with httpx.Client(follow_redirects=True) as client:
1023
+ # Download parcellation file
1024
+ logger.info(f"Downloading SUIT parcellation from {url}")
1025
+ try:
1026
+ img_resp = client.get(url)
1027
+ img_resp.raise_for_status()
1028
+ except httpx.HTTPError as exc:
1029
+ raise_error(
1030
+ f"Error response {exc.response.status_code} while "
1031
+ f"requesting {exc.request.url!r}",
1032
+ klass=RuntimeError,
1033
+ )
1034
+ else:
1035
+ with open(parcellation_fname, "wb") as f:
1036
+ f.write(img_resp.content)
1037
+ # Download label file
1038
+ logger.info(f"Downloading SUIT labels from {url_labels}")
1039
+ try:
1040
+ label_resp = client.get(url_labels)
1041
+ label_resp.raise_for_status()
1042
+ except httpx.HTTPError as exc:
1043
+ raise_error(
1044
+ f"Error response {exc.response.status_code} while "
1045
+ f"requesting {exc.request.url!r}",
1046
+ klass=RuntimeError,
1047
+ )
1048
+ else:
1049
+ # Load labels
1050
+ labels = pd.read_csv(
1051
+ io.StringIO(label_resp.content.decode("utf-8")),
1052
+ sep="\t",
1053
+ usecols=["name"],
1054
+ )
1055
+ labels.to_csv(parcellation_lname, sep="\t", index=False)
781
1056
 
1057
+ # Load labels
782
1058
  labels = pd.read_csv(parcellation_lname, sep="\t", usecols=["name"])[
783
1059
  "name"
784
1060
  ].to_list()
@@ -815,9 +1091,15 @@ def _retrieve_aicha(
815
1091
 
816
1092
  Raises
817
1093
  ------
1094
+ RuntimeError
1095
+ If there is a problem fetching files.
818
1096
  ValueError
819
- If invalid value is provided for ``version`` or if there is a problem
820
- fetching the parcellation.
1097
+ If invalid value is provided for ``version``.
1098
+
1099
+ Warns
1100
+ -----
1101
+ RuntimeWarning
1102
+ Until the authors confirm the space, the warning will be issued.
821
1103
 
822
1104
  Notes
823
1105
  -----
@@ -825,28 +1107,33 @@ def _retrieve_aicha(
825
1107
  1mm, it is only for display purpose as noted in the release document.
826
1108
 
827
1109
  """
828
- # show parameters to user
1110
+ # Issue warning until space is confirmed by authors
1111
+ warn_with_log(
1112
+ "The current space for AICHA parcellations are IXI549Space, but are "
1113
+ "not confirmed by authors, until that this warning will be issued."
1114
+ )
1115
+
829
1116
  logger.info("Parcellation parameters:")
830
1117
  logger.info(f"\tresolution: {resolution}")
831
1118
  logger.info(f"\tversion: {version}")
832
1119
 
833
- # Check version value
834
- _valid_version = (1, 2)
1120
+ # Check version
1121
+ _valid_version = [1, 2]
835
1122
  if version not in _valid_version:
836
1123
  raise_error(
837
1124
  f"The parameter `version` ({version}) needs to be one of the "
838
1125
  f"following: {_valid_version}"
839
1126
  )
840
1127
 
1128
+ # Check resolution
841
1129
  _valid_resolutions = [1]
842
1130
  resolution = closest_resolution(resolution, _valid_resolutions)
843
1131
 
844
- # Define image file
1132
+ # Define parcellation and label file names
845
1133
  parcellation_fname = (
846
1134
  parcellations_dir / f"AICHA_v{version}" / "AICHA" / "AICHA.nii"
847
1135
  )
848
-
849
- # Define label file name according to version
1136
+ parcellation_lname = Path()
850
1137
  if version == 1:
851
1138
  parcellation_lname = (
852
1139
  parcellations_dir
@@ -867,54 +1154,60 @@ def _retrieve_aicha(
867
1154
  logger.info(
868
1155
  "At least one of the parcellation files are missing, fetching."
869
1156
  )
870
-
871
1157
  # Set file name on server according to version
1158
+ server_filename = ""
872
1159
  if version == 1:
873
1160
  server_filename = "aicha_v1.zip"
874
1161
  elif version == 2:
875
1162
  server_filename = "AICHA_v2.tar.zip"
876
-
877
1163
  # Set URL
878
1164
  url = f"http://www.gin.cnrs.fr/wp-content/uploads/{server_filename}"
879
1165
 
880
1166
  logger.info(f"Downloading AICHA v{version} from {url}")
1167
+ # Store initial download in a tempdir
881
1168
  with tempfile.TemporaryDirectory() as tmpdir:
882
1169
  # Make HTTP request
883
1170
  try:
884
- resp = requests.get(url)
1171
+ resp = httpx.get(url, follow_redirects=True)
885
1172
  resp.raise_for_status()
886
- except (ConnectionError, ReadTimeout, HTTPError) as err:
1173
+ except httpx.HTTPError as exc:
887
1174
  raise_error(
888
- f"Failed to download AICHA v{version} due to: {err}"
1175
+ f"Error response {exc.response.status_code} while "
1176
+ f"requesting {exc.request.url!r}",
1177
+ klass=RuntimeError,
889
1178
  )
890
1179
  else:
1180
+ # Set tempfile for storing initial content and unzipping
891
1181
  parcellation_zip_path = Path(tmpdir) / server_filename
1182
+ # Open tempfile and write content
892
1183
  with open(parcellation_zip_path, "wb") as f:
893
1184
  f.write(resp.content)
894
-
895
- # Extract zipfile
896
- with zipfile.ZipFile(parcellation_zip_path, "r") as zip_ref:
897
- if version == 1:
898
- zip_ref.extractall(
899
- (parcellations_dir / "AICHA_v1").as_posix()
900
- )
901
- elif version == 2:
902
- zip_ref.extractall(Path(tmpdir).as_posix())
903
- # Extract tarfile for v2
904
- with tarfile.TarFile(
905
- Path(tmpdir) / "aicha_v2.tar", "r"
906
- ) as tar_ref:
907
- tar_ref.extractall(
908
- (parcellations_dir / "AICHA_v2").as_posix()
1185
+ # Unzip tempfile
1186
+ with zipfile.ZipFile(parcellation_zip_path, "r") as zip_ref:
1187
+ if version == 1:
1188
+ zip_ref.extractall(
1189
+ (parcellations_dir / "AICHA_v1").as_posix()
909
1190
  )
910
-
911
- # Cleanup after unzipping
912
- if (parcellations_dir / f"AICHA_v{version}" / "__MACOSX").exists():
913
- shutil.rmtree(
914
- (
915
- parcellations_dir / f"AICHA_v{version}" / "__MACOSX"
916
- ).as_posix()
917
- )
1191
+ elif version == 2:
1192
+ zip_ref.extractall(Path(tmpdir).as_posix())
1193
+ # Extract tarfile for v2
1194
+ with tarfile.TarFile(
1195
+ Path(tmpdir) / "aicha_v2.tar", "r"
1196
+ ) as tar_ref:
1197
+ tar_ref.extractall(
1198
+ (parcellations_dir / "AICHA_v2").as_posix()
1199
+ )
1200
+ # Cleanup after unzipping
1201
+ if (
1202
+ parcellations_dir / f"AICHA_v{version}" / "__MACOSX"
1203
+ ).exists():
1204
+ shutil.rmtree(
1205
+ (
1206
+ parcellations_dir
1207
+ / f"AICHA_v{version}"
1208
+ / "__MACOSX"
1209
+ ).as_posix()
1210
+ )
918
1211
 
919
1212
  # Load labels
920
1213
  labels = pd.read_csv(
@@ -959,12 +1252,13 @@ def _retrieve_shen( # noqa: C901
959
1252
 
960
1253
  Raises
961
1254
  ------
1255
+ RuntimeError
1256
+ If there is a problem fetching files.
962
1257
  ValueError
963
- If invalid value or combination is provided for ``year`` and ``n_rois``
964
- or if there is a problem fetching the parcellation.
1258
+ If invalid value or combination is provided for ``year`` and
1259
+ ``n_rois``.
965
1260
 
966
1261
  """
967
- # show parameters to user
968
1262
  logger.info("Parcellation parameters:")
969
1263
  logger.info(f"\tresolution: {resolution}")
970
1264
  logger.info(f"\tyear: {year}")
@@ -1012,7 +1306,7 @@ def _retrieve_shen( # noqa: C901
1012
1306
  f"`year = {year}` is invalid"
1013
1307
  )
1014
1308
 
1015
- # Define image file according to constraints
1309
+ # Define parcellation and label file names
1016
1310
  if year == 2013:
1017
1311
  parcellation_fname = (
1018
1312
  parcellations_dir
@@ -1059,27 +1353,31 @@ def _retrieve_shen( # noqa: C901
1059
1353
  url = "https://www.nitrc.org/frs/download.php/11629/shen_368.zip"
1060
1354
 
1061
1355
  logger.info(f"Downloading Shen {year} from {url}")
1356
+ # Store initial download in a tempdir
1062
1357
  with tempfile.TemporaryDirectory() as tmpdir:
1063
1358
  # Make HTTP request
1064
1359
  try:
1065
- resp = requests.get(url)
1360
+ resp = httpx.get(url)
1066
1361
  resp.raise_for_status()
1067
- except (ConnectionError, ReadTimeout, HTTPError) as err:
1068
- raise_error(f"Failed to download Shen {year} due to {err}")
1362
+ except httpx.HTTPError as exc:
1363
+ raise_error(
1364
+ f"Error response {exc.response.status_code} while "
1365
+ f"requesting {exc.request.url!r}",
1366
+ klass=RuntimeError,
1367
+ )
1069
1368
  else:
1070
1369
  if year in (2013, 2019):
1071
1370
  parcellation_zip_path = Path(tmpdir) / f"Shen{year}.zip"
1371
+ # Open tempfile and write content
1072
1372
  with open(parcellation_zip_path, "wb") as f:
1073
1373
  f.write(resp.content)
1074
-
1075
- # Extract zipfile
1374
+ # Unzip tempfile
1076
1375
  with zipfile.ZipFile(
1077
1376
  parcellation_zip_path, "r"
1078
1377
  ) as zip_ref:
1079
1378
  zip_ref.extractall(
1080
1379
  (parcellations_dir / f"Shen_{year}").as_posix()
1081
1380
  )
1082
-
1083
1381
  # Cleanup after unzipping
1084
1382
  if (
1085
1383
  parcellations_dir / f"Shen_{year}" / "__MACOSX"
@@ -1089,16 +1387,18 @@ def _retrieve_shen( # noqa: C901
1089
1387
  parcellations_dir / f"Shen_{year}" / "__MACOSX"
1090
1388
  ).as_posix()
1091
1389
  )
1092
-
1093
1390
  elif year == 2015:
1094
1391
  img_dir_path = parcellations_dir / "Shen_2015"
1392
+ # Create local directory if not present
1095
1393
  img_dir_path.mkdir(parents=True, exist_ok=True)
1096
1394
  img_path = (
1097
1395
  img_dir_path
1098
1396
  / f"shen_{resolution}mm_268_parcellation.nii.gz"
1099
1397
  )
1398
+ # Create local file if not present
1100
1399
  img_path.touch(exist_ok=True)
1101
- with open(img_path.as_posix(), "wb") as f:
1400
+ # Open tempfile and write content
1401
+ with open(img_path, "wb") as f:
1102
1402
  f.write(resp.content)
1103
1403
 
1104
1404
  # Load labels based on year
@@ -1156,9 +1456,11 @@ def _retrieve_yan(
1156
1456
 
1157
1457
  Raises
1158
1458
  ------
1459
+ RuntimeError
1460
+ If there is a problem fetching files.
1159
1461
  ValueError
1160
1462
  If invalid value is provided for ``n_rois``, ``yeo_networks`` or
1161
- ``kong_networks`` or if there is a problem fetching the parcellation.
1463
+ ``kong_networks``.
1162
1464
 
1163
1465
  """
1164
1466
  logger.info("Parcellation parameters:")
@@ -1188,6 +1490,8 @@ def _retrieve_yan(
1188
1490
  f"following: {_valid_n_rois}"
1189
1491
  )
1190
1492
 
1493
+ parcellation_fname = Path()
1494
+ parcellation_lname = Path()
1191
1495
  if yeo_networks:
1192
1496
  # Check yeo_networks value
1193
1497
  _valid_yeo_networks = [7, 17]
@@ -1240,6 +1544,8 @@ def _retrieve_yan(
1240
1544
  )
1241
1545
 
1242
1546
  # Set URL based on network
1547
+ img_url = ""
1548
+ label_url = ""
1243
1549
  if yeo_networks:
1244
1550
  img_url = (
1245
1551
  "https://raw.githubusercontent.com/ThomasYeoLab/CBIG/"
@@ -1267,38 +1573,165 @@ def _retrieve_yan(
1267
1573
  "Kong2022_17Networks_LUT.txt"
1268
1574
  )
1269
1575
 
1270
- # Initiate a session and make HTTP requests
1271
- session = requests.Session()
1272
- # Download parcellation file
1273
- logger.info(f"Downloading Yan 2023 parcellation from {img_url}")
1576
+ # Make HTTP requests
1577
+ with httpx.Client() as client:
1578
+ # Download parcellation file
1579
+ logger.info(f"Downloading Yan 2023 parcellation from {img_url}")
1580
+ try:
1581
+ img_resp = client.get(img_url)
1582
+ img_resp.raise_for_status()
1583
+ except httpx.HTTPError as exc:
1584
+ raise_error(
1585
+ f"Error response {exc.response.status_code} while "
1586
+ f"requesting {exc.request.url!r}",
1587
+ klass=RuntimeError,
1588
+ )
1589
+ else:
1590
+ parcellation_img_path = Path(parcellation_fname)
1591
+ # Create local directory if not present
1592
+ parcellation_img_path.parent.mkdir(parents=True, exist_ok=True)
1593
+ # Create local file if not present
1594
+ parcellation_img_path.touch(exist_ok=True)
1595
+ # Open file and write content
1596
+ with open(parcellation_img_path, "wb") as f:
1597
+ f.write(img_resp.content)
1598
+ # Download label file
1599
+ logger.info(f"Downloading Yan 2023 labels from {label_url}")
1600
+ try:
1601
+ label_resp = client.get(label_url)
1602
+ label_resp.raise_for_status()
1603
+ except httpx.HTTPError as exc:
1604
+ raise_error(
1605
+ f"Error response {exc.response.status_code} while "
1606
+ f"requesting {exc.request.url!r}",
1607
+ klass=RuntimeError,
1608
+ )
1609
+ else:
1610
+ parcellation_labels_path = Path(parcellation_lname)
1611
+ # Create local file if not present
1612
+ parcellation_labels_path.touch(exist_ok=True)
1613
+ # Open file and write content
1614
+ with open(parcellation_labels_path, "wb") as f:
1615
+ f.write(label_resp.content)
1616
+
1617
+ # Load label file
1618
+ labels = pd.read_csv(parcellation_lname, sep=" ", header=None)[1].to_list()
1619
+
1620
+ return parcellation_fname, labels
1621
+
1622
+
1623
+ def _retrieve_brainnetome(
1624
+ parcellations_dir: Path,
1625
+ resolution: Optional[float] = None,
1626
+ threshold: Optional[int] = None,
1627
+ ) -> Tuple[Path, List[str]]:
1628
+ """Retrieve Brainnetome parcellation.
1629
+
1630
+ Parameters
1631
+ ----------
1632
+ parcellations_dir : pathlib.Path
1633
+ The path to the parcellation data directory.
1634
+ resolution : {1.0, 1.25, 2.0}, optional
1635
+ The desired resolution of the parcellation to load. If it is not
1636
+ available, the closest resolution will be loaded. Preferably, use a
1637
+ resolution higher than the desired one. By default, will load the
1638
+ highest one (default None). Available resolutions for this
1639
+ parcellation are 1mm, 1.25mm and 2mm.
1640
+ threshold : {0, 25, 50}, optional
1641
+ The threshold for the probabilistic maps of subregion (default None).
1642
+
1643
+ Returns
1644
+ -------
1645
+ pathlib.Path
1646
+ File path to the parcellation image.
1647
+ list of str
1648
+ Parcellation labels.
1649
+
1650
+ Raises
1651
+ ------
1652
+ RuntimeError
1653
+ If there is a problem fetching files.
1654
+ ValueError
1655
+ If invalid value is provided for ``threshold``.
1656
+
1657
+ """
1658
+ logger.info("Parcellation parameters:")
1659
+ logger.info(f"\tresolution: {resolution}")
1660
+ logger.info(f"\tthreshold: {threshold}")
1661
+
1662
+ # Check resolution
1663
+ _valid_resolutions = [1.0, 1.25, 2.0]
1664
+ resolution = closest_resolution(resolution, _valid_resolutions)
1665
+
1666
+ # Check threshold value
1667
+ _valid_threshold = [0, 25, 50]
1668
+ if threshold not in _valid_threshold:
1669
+ raise_error(
1670
+ f"The parameter `threshold` ({threshold}) needs to be one of the "
1671
+ f"following: {_valid_threshold}"
1672
+ )
1673
+ # Correct resolution for further stuff
1674
+ if resolution in [1.0, 2.0]:
1675
+ resolution = int(resolution)
1676
+
1677
+ parcellation_fname = (
1678
+ parcellations_dir
1679
+ / "BNA246"
1680
+ / f"BNA-maxprob-thr{threshold}-{resolution}mm.nii.gz"
1681
+ )
1682
+
1683
+ # Check for existence of parcellation
1684
+ if not parcellation_fname.exists():
1685
+ # Set URL
1686
+ url = f"http://neurovault.org/media/images/1625/BNA-maxprob-thr{threshold}-{resolution}mm.nii.gz"
1687
+
1688
+ logger.info(f"Downloading Brainnetome from {url}")
1689
+ # Make HTTP request
1274
1690
  try:
1275
- img_resp = session.get(img_url)
1276
- img_resp.raise_for_status()
1277
- except (ConnectionError, ReadTimeout, HTTPError) as err:
1691
+ resp = httpx.get(url, follow_redirects=True)
1692
+ resp.raise_for_status()
1693
+ except httpx.HTTPError as exc:
1278
1694
  raise_error(
1279
- f"Failed to download Yan 2023 parcellation due to: {err}"
1695
+ f"Error response {exc.response.status_code} while "
1696
+ f"requesting {exc.request.url!r}",
1697
+ klass=RuntimeError,
1280
1698
  )
1281
1699
  else:
1282
- parcellation_img_path = Path(parcellation_fname)
1283
- parcellation_img_path.parent.mkdir(parents=True, exist_ok=True)
1284
- parcellation_img_path.touch(exist_ok=True)
1285
- with open(parcellation_img_path, "wb") as f:
1286
- f.write(img_resp.content)
1287
- # Download label file
1288
- logger.info(f"Downloading Yan 2023 labels from {label_url}")
1289
- try:
1290
- label_resp = session.get(label_url)
1291
- label_resp.raise_for_status()
1292
- except (ConnectionError, ReadTimeout, HTTPError) as err:
1293
- raise_error(f"Failed to download Yan 2023 labels due to: {err}")
1294
- else:
1295
- parcellation_labels_path = Path(parcellation_lname)
1296
- parcellation_labels_path.touch(exist_ok=True)
1297
- with open(parcellation_labels_path, "wb") as f:
1298
- f.write(label_resp.content)
1700
+ # Create local directory if not present
1701
+ parcellation_fname.parent.mkdir(parents=True, exist_ok=True)
1702
+ # Create file if not present
1703
+ parcellation_fname.touch(exist_ok=True)
1704
+ # Open file and write bytes
1705
+ parcellation_fname.write_bytes(resp.content)
1299
1706
 
1300
- # Load label file
1301
- labels = pd.read_csv(parcellation_lname, sep=" ", header=None)[1].to_list()
1707
+ # Load labels
1708
+ labels = (
1709
+ sorted([f"SFG_L(R)_7_{i}" for i in range(1, 8)] * 2)
1710
+ + sorted([f"MFG_L(R)_7_{i}" for i in range(1, 8)] * 2)
1711
+ + sorted([f"IFG_L(R)_6_{i}" for i in range(1, 7)] * 2)
1712
+ + sorted([f"OrG_L(R)_6_{i}" for i in range(1, 7)] * 2)
1713
+ + sorted([f"PrG_L(R)_6_{i}" for i in range(1, 7)] * 2)
1714
+ + sorted([f"PCL_L(R)_2_{i}" for i in range(1, 3)] * 2)
1715
+ + sorted([f"STG_L(R)_6_{i}" for i in range(1, 7)] * 2)
1716
+ + sorted([f"MTG_L(R)_4_{i}" for i in range(1, 5)] * 2)
1717
+ + sorted([f"ITG_L(R)_7_{i}" for i in range(1, 8)] * 2)
1718
+ + sorted([f"FuG_L(R)_3_{i}" for i in range(1, 4)] * 2)
1719
+ + sorted([f"PhG_L(R)_6_{i}" for i in range(1, 7)] * 2)
1720
+ + sorted([f"pSTS_L(R)_2_{i}" for i in range(1, 3)] * 2)
1721
+ + sorted([f"SPL_L(R)_5_{i}" for i in range(1, 6)] * 2)
1722
+ + sorted([f"IPL_L(R)_6_{i}" for i in range(1, 7)] * 2)
1723
+ + sorted([f"PCun_L(R)_4_{i}" for i in range(1, 5)] * 2)
1724
+ + sorted([f"PoG_L(R)_4_{i}" for i in range(1, 5)] * 2)
1725
+ + sorted([f"INS_L(R)_6_{i}" for i in range(1, 7)] * 2)
1726
+ + sorted([f"CG_L(R)_7_{i}" for i in range(1, 8)] * 2)
1727
+ + sorted([f"MVOcC _L(R)_5_{i}" for i in range(1, 6)] * 2)
1728
+ + sorted([f"LOcC_L(R)_4_{i}" for i in range(1, 5)] * 2)
1729
+ + sorted([f"LOcC_L(R)_2_{i}" for i in range(1, 3)] * 2)
1730
+ + sorted([f"Amyg_L(R)_2_{i}" for i in range(1, 3)] * 2)
1731
+ + sorted([f"Hipp_L(R)_2_{i}" for i in range(1, 3)] * 2)
1732
+ + sorted([f"BG_L(R)_6_{i}" for i in range(1, 7)] * 2)
1733
+ + sorted([f"Tha_L(R)_8_{i}" for i in range(1, 9)] * 2)
1734
+ )
1302
1735
 
1303
1736
  return parcellation_fname, labels
1304
1737