junifer 0.0.7.dev332__py3-none-any.whl → 0.0.8__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 (293) hide show
  1. junifer/_version.py +9 -19
  2. junifer/api/__init__.pyi +17 -2
  3. junifer/api/functions.py +222 -39
  4. junifer/api/queue_context/__init__.pyi +11 -3
  5. junifer/api/queue_context/gnu_parallel_local_adapter.py +81 -120
  6. junifer/api/queue_context/htcondor_adapter.py +118 -181
  7. junifer/api/queue_context/queue_context_adapter.py +51 -7
  8. junifer/api/queue_context/tests/test_gnu_parallel_local_adapter.py +10 -35
  9. junifer/api/queue_context/tests/test_htcondor_adapter.py +13 -50
  10. junifer/api/res/fsl/run_fsl_docker.sh +4 -0
  11. junifer/api/tests/test_functions.py +863 -36
  12. junifer/cli/__init__.py +1 -7
  13. junifer/cli/cli.py +41 -22
  14. junifer/cli/parser.py +23 -147
  15. junifer/cli/tests/data/gmd_mean.yaml +6 -6
  16. junifer/cli/tests/data/gmd_mean_htcondor.yaml +6 -5
  17. junifer/cli/tests/data/partly_cloudy_agg_mean_tian.yml +1 -0
  18. junifer/cli/tests/test_cli.py +120 -4
  19. junifer/cli/tests/test_cli_utils.py +23 -6
  20. junifer/cli/utils.py +14 -9
  21. junifer/configs/__init__.py +5 -0
  22. junifer/configs/__init__.pyi +0 -0
  23. junifer/configs/juseless/datagrabbers/__init__.pyi +4 -2
  24. junifer/configs/juseless/datagrabbers/aomic_id1000_vbm.py +18 -24
  25. junifer/configs/juseless/datagrabbers/camcan_vbm.py +20 -27
  26. junifer/configs/juseless/datagrabbers/ixi_vbm.py +44 -52
  27. junifer/configs/juseless/datagrabbers/tests/test_ixi_vbm.py +0 -7
  28. junifer/configs/juseless/datagrabbers/tests/test_ucla.py +2 -19
  29. junifer/configs/juseless/datagrabbers/ucla.py +111 -104
  30. junifer/configs/juseless/datagrabbers/ukb_vbm.py +19 -25
  31. junifer/configs/py.typed +0 -0
  32. junifer/conftest.py +63 -24
  33. junifer/data/_dispatch.py +9 -19
  34. junifer/data/coordinates/_ants_coordinates_warper.py +4 -1
  35. junifer/data/coordinates/_coordinates.py +51 -6
  36. junifer/data/coordinates/_fsl_coordinates_warper.py +4 -1
  37. junifer/data/coordinates/tests/test_coordinates.py +15 -3
  38. junifer/data/coordinates/tests/test_coordinates_names.py +37 -0
  39. junifer/data/maps/_ants_maps_warper.py +18 -12
  40. junifer/data/maps/_fsl_maps_warper.py +7 -3
  41. junifer/data/maps/_maps.py +12 -8
  42. junifer/data/maps/tests/test_maps.py +13 -21
  43. junifer/data/masks/_ants_mask_warper.py +18 -12
  44. junifer/data/masks/_fsl_mask_warper.py +7 -3
  45. junifer/data/masks/_masks.py +15 -12
  46. junifer/data/masks/tests/test_masks.py +20 -17
  47. junifer/data/parcellations/_ants_parcellation_warper.py +18 -12
  48. junifer/data/parcellations/_fsl_parcellation_warper.py +7 -5
  49. junifer/data/parcellations/_parcellations.py +125 -91
  50. junifer/data/parcellations/tests/test_parcellations.py +122 -1
  51. junifer/data/template_spaces.py +117 -50
  52. junifer/data/tests/test_template_spaces.py +10 -7
  53. junifer/data/utils.py +10 -8
  54. junifer/datagrabber/__init__.pyi +38 -5
  55. junifer/datagrabber/aomic/__init__.pyi +8 -1
  56. junifer/datagrabber/aomic/_types.py +22 -0
  57. junifer/datagrabber/aomic/id1000.py +193 -183
  58. junifer/datagrabber/aomic/piop1.py +215 -259
  59. junifer/datagrabber/aomic/piop2.py +211 -244
  60. junifer/datagrabber/aomic/tests/test_id1000.py +70 -53
  61. junifer/datagrabber/aomic/tests/test_piop1.py +98 -102
  62. junifer/datagrabber/aomic/tests/test_piop2.py +88 -85
  63. junifer/datagrabber/base.py +253 -79
  64. junifer/datagrabber/datalad_base.py +127 -76
  65. junifer/datagrabber/dmcc13_benchmark.py +204 -289
  66. junifer/datagrabber/hcp1200/__init__.pyi +7 -2
  67. junifer/datagrabber/hcp1200/datalad_hcp1200.py +61 -40
  68. junifer/datagrabber/hcp1200/hcp1200.py +151 -164
  69. junifer/datagrabber/hcp1200/tests/test_hcp1200.py +116 -128
  70. junifer/datagrabber/multiple.py +183 -63
  71. junifer/datagrabber/pattern.py +358 -227
  72. junifer/datagrabber/pattern_datalad.py +84 -124
  73. junifer/datagrabber/pattern_validation_mixin.py +22 -50
  74. junifer/datagrabber/tests/test_base.py +17 -4
  75. junifer/datagrabber/tests/test_datalad_base.py +20 -27
  76. junifer/datagrabber/tests/test_dmcc13_benchmark.py +110 -122
  77. junifer/datagrabber/tests/test_multiple.py +293 -31
  78. junifer/datagrabber/tests/test_pattern.py +577 -18
  79. junifer/datagrabber/tests/test_pattern_datalad.py +177 -107
  80. junifer/datagrabber/tests/test_pattern_validation_mixin.py +9 -51
  81. junifer/datareader/default.py +8 -5
  82. junifer/datareader/tests/test_default_reader.py +1 -1
  83. junifer/external/BrainPrint/.codecov.yml +2 -0
  84. junifer/external/BrainPrint/.codespellignore +2 -0
  85. junifer/external/BrainPrint/.github/ISSUE_TEMPLATE/bug-report.md +45 -0
  86. junifer/external/BrainPrint/.github/ISSUE_TEMPLATE/documentation.md +12 -0
  87. junifer/external/BrainPrint/.github/ISSUE_TEMPLATE/feature-request.md +24 -0
  88. junifer/external/BrainPrint/.github/ISSUE_TEMPLATE/questions-help-support.md +28 -0
  89. junifer/external/BrainPrint/.github/dependabot.yml +6 -0
  90. junifer/external/BrainPrint/.github/workflows/build.yml +53 -0
  91. junifer/external/BrainPrint/.github/workflows/code-style.yml +44 -0
  92. junifer/external/BrainPrint/.github/workflows/doc.yml +65 -0
  93. junifer/external/BrainPrint/.github/workflows/publish.yml +31 -0
  94. junifer/external/BrainPrint/.github/workflows/pytest.yml +54 -0
  95. junifer/external/BrainPrint/.gitignore +123 -0
  96. junifer/external/BrainPrint/LICENSE +21 -0
  97. junifer/external/BrainPrint/README.md +108 -0
  98. junifer/external/BrainPrint/brainprint/cli/__init__.py +11 -0
  99. junifer/external/BrainPrint/brainprint/cli/help_text.py +169 -0
  100. junifer/external/BrainPrint/brainprint/cli/parser.py +150 -0
  101. junifer/external/BrainPrint/brainprint/cli/utils.py +14 -0
  102. junifer/external/BrainPrint/brainprint/commands/__init__.py +0 -0
  103. junifer/external/BrainPrint/brainprint/commands/sys_info.py +18 -0
  104. junifer/external/BrainPrint/brainprint/utils/tests/__init__.py +0 -0
  105. junifer/external/BrainPrint/brainprint/utils/tests/test_config.py +33 -0
  106. junifer/external/BrainPrint/doc/Makefile +20 -0
  107. junifer/external/BrainPrint/doc/_static/.gitkeep +0 -0
  108. junifer/external/BrainPrint/doc/_templates/autosummary/class.rst +10 -0
  109. junifer/external/BrainPrint/doc/_templates/autosummary/function.rst +8 -0
  110. junifer/external/BrainPrint/doc/_templates/autosummary/module.rst +6 -0
  111. junifer/external/BrainPrint/doc/api/index.rst +11 -0
  112. junifer/external/BrainPrint/doc/conf.py +272 -0
  113. junifer/external/BrainPrint/doc/index.rst +140 -0
  114. junifer/external/BrainPrint/doc/links.inc +36 -0
  115. junifer/external/BrainPrint/doc/make.bat +35 -0
  116. junifer/external/BrainPrint/doc/references.bib +23 -0
  117. junifer/external/BrainPrint/pyproject.toml +170 -0
  118. junifer/external/BrainPrint/setup.py +3 -0
  119. junifer/external/__init__.py +5 -0
  120. junifer/external/__init__.pyi +0 -0
  121. junifer/external/nilearn/junifer_connectivity_measure.py +95 -39
  122. junifer/external/nilearn/junifer_nifti_spheres_masker.py +226 -67
  123. junifer/external/nilearn/tests/test_junifer_connectivity_measure.py +39 -27
  124. junifer/external/nilearn/tests/test_junifer_nifti_spheres_masker.py +231 -21
  125. junifer/external/py.typed +0 -0
  126. junifer/markers/__init__.pyi +6 -3
  127. junifer/markers/base.py +126 -76
  128. junifer/markers/brainprint.py +30 -39
  129. junifer/markers/complexity/__init__.py +20 -3
  130. junifer/markers/complexity/complexity_base.py +40 -41
  131. junifer/markers/complexity/hurst_exponent.py +22 -35
  132. junifer/markers/complexity/multiscale_entropy_auc.py +53 -41
  133. junifer/markers/complexity/perm_entropy.py +22 -36
  134. junifer/markers/complexity/range_entropy.py +22 -35
  135. junifer/markers/complexity/range_entropy_auc.py +27 -36
  136. junifer/markers/complexity/sample_entropy.py +22 -36
  137. junifer/markers/complexity/tests/test_complexity_registry.py +48 -0
  138. junifer/markers/complexity/tests/test_hurst_exponent.py +47 -36
  139. junifer/markers/complexity/tests/test_multiscale_entropy_auc.py +50 -36
  140. junifer/markers/complexity/tests/test_perm_entropy.py +48 -36
  141. junifer/markers/complexity/tests/test_range_entropy.py +47 -36
  142. junifer/markers/complexity/tests/test_range_entropy_auc.py +47 -36
  143. junifer/markers/complexity/tests/test_sample_entropy.py +48 -36
  144. junifer/markers/complexity/tests/test_weighted_perm_entropy.py +50 -36
  145. junifer/markers/complexity/weighted_perm_entropy.py +22 -36
  146. junifer/markers/ets_rss.py +35 -31
  147. junifer/markers/falff/__init__.pyi +7 -1
  148. junifer/markers/falff/_afni_falff.py +22 -9
  149. junifer/markers/falff/_junifer_falff.py +47 -31
  150. junifer/markers/falff/falff_base.py +80 -60
  151. junifer/markers/falff/falff_maps.py +17 -41
  152. junifer/markers/falff/falff_parcels.py +26 -51
  153. junifer/markers/falff/falff_spheres.py +34 -61
  154. junifer/markers/falff/tests/test_falff_maps.py +20 -23
  155. junifer/markers/falff/tests/test_falff_mask.py +99 -0
  156. junifer/markers/falff/tests/test_falff_parcels.py +14 -10
  157. junifer/markers/falff/tests/test_falff_spheres.py +14 -10
  158. junifer/markers/functional_connectivity/crossparcellation_functional_connectivity.py +32 -34
  159. junifer/markers/functional_connectivity/edge_functional_connectivity_maps.py +12 -25
  160. junifer/markers/functional_connectivity/edge_functional_connectivity_parcels.py +19 -34
  161. junifer/markers/functional_connectivity/edge_functional_connectivity_spheres.py +24 -44
  162. junifer/markers/functional_connectivity/functional_connectivity_base.py +50 -40
  163. junifer/markers/functional_connectivity/functional_connectivity_maps.py +12 -25
  164. junifer/markers/functional_connectivity/functional_connectivity_parcels.py +19 -34
  165. junifer/markers/functional_connectivity/functional_connectivity_spheres.py +24 -45
  166. junifer/markers/functional_connectivity/tests/test_crossparcellation_functional_connectivity.py +21 -22
  167. junifer/markers/functional_connectivity/tests/test_edge_functional_connectivity_maps.py +8 -7
  168. junifer/markers/functional_connectivity/tests/test_edge_functional_connectivity_parcels.py +8 -4
  169. junifer/markers/functional_connectivity/tests/test_edge_functional_connectivity_spheres.py +8 -4
  170. junifer/markers/functional_connectivity/tests/test_functional_connectivity_maps.py +8 -7
  171. junifer/markers/functional_connectivity/tests/test_functional_connectivity_parcels.py +9 -5
  172. junifer/markers/functional_connectivity/tests/test_functional_connectivity_spheres.py +8 -12
  173. junifer/markers/maps_aggregation.py +77 -55
  174. junifer/markers/parcel_aggregation.py +91 -70
  175. junifer/markers/reho/__init__.pyi +7 -1
  176. junifer/markers/reho/_afni_reho.py +23 -14
  177. junifer/markers/reho/_junifer_reho.py +112 -123
  178. junifer/markers/reho/reho_base.py +115 -37
  179. junifer/markers/reho/reho_maps.py +18 -32
  180. junifer/markers/reho/reho_parcels.py +26 -41
  181. junifer/markers/reho/reho_spheres.py +33 -54
  182. junifer/markers/reho/tests/test_junifer_reho.py +178 -0
  183. junifer/markers/reho/tests/test_reho_maps.py +27 -25
  184. junifer/markers/reho/tests/test_reho_parcels.py +28 -22
  185. junifer/markers/reho/tests/test_reho_spheres.py +16 -12
  186. junifer/markers/sphere_aggregation.py +88 -72
  187. junifer/markers/temporal_snr/temporal_snr_base.py +32 -30
  188. junifer/markers/temporal_snr/temporal_snr_maps.py +9 -18
  189. junifer/markers/temporal_snr/temporal_snr_parcels.py +19 -27
  190. junifer/markers/temporal_snr/temporal_snr_spheres.py +27 -41
  191. junifer/markers/temporal_snr/tests/test_temporal_snr_base.py +2 -2
  192. junifer/markers/temporal_snr/tests/test_temporal_snr_maps.py +17 -22
  193. junifer/markers/temporal_snr/tests/test_temporal_snr_parcels.py +11 -3
  194. junifer/markers/temporal_snr/tests/test_temporal_snr_spheres.py +9 -9
  195. junifer/markers/tests/test_brainprint.py +10 -9
  196. junifer/markers/tests/test_ets_rss.py +7 -3
  197. junifer/markers/tests/test_lost_regions.py +279 -0
  198. junifer/markers/tests/test_maps_aggregation.py +67 -114
  199. junifer/markers/tests/test_marker_utils.py +40 -1
  200. junifer/markers/tests/test_markers_base.py +275 -6
  201. junifer/markers/tests/test_parcel_aggregation.py +91 -53
  202. junifer/markers/tests/test_sphere_aggregation.py +53 -47
  203. junifer/markers/utils.py +267 -5
  204. junifer/onthefly/__init__.py +2 -3
  205. junifer/onthefly/__init__.pyi +4 -0
  206. junifer/onthefly/_brainprint.py +3 -5
  207. junifer/onthefly/py.typed +0 -0
  208. junifer/onthefly/read_transform.py +11 -9
  209. junifer/onthefly/tests/test_read_transform.py +7 -7
  210. junifer/pipeline/__init__.pyi +4 -2
  211. junifer/pipeline/marker_collection.py +14 -10
  212. junifer/pipeline/pipeline_component_registry.py +25 -6
  213. junifer/pipeline/pipeline_step_mixin.py +5 -12
  214. junifer/pipeline/tests/test_data_object_dumper.py +10 -10
  215. junifer/pipeline/tests/test_marker_collection.py +145 -168
  216. junifer/pipeline/tests/test_pipeline_component_registry.py +52 -8
  217. junifer/pipeline/tests/test_pipeline_step_mixin.py +12 -26
  218. junifer/pipeline/tests/test_update_meta_mixin.py +4 -5
  219. junifer/pipeline/tests/test_workdir_manager.py +39 -0
  220. junifer/pipeline/update_meta_mixin.py +13 -10
  221. junifer/pipeline/utils.py +22 -17
  222. junifer/pipeline/workdir_manager.py +41 -32
  223. junifer/preprocess/__init__.pyi +9 -4
  224. junifer/preprocess/_temporal_filter.py +34 -33
  225. junifer/preprocess/_temporal_slicer.py +17 -30
  226. junifer/preprocess/base.py +99 -55
  227. junifer/preprocess/confounds/__init__.pyi +10 -2
  228. junifer/preprocess/confounds/fmriprep_confound_remover.py +87 -109
  229. junifer/preprocess/confounds/tests/test_fmriprep_confound_remover.py +64 -45
  230. junifer/preprocess/smoothing/__init__.pyi +2 -2
  231. junifer/preprocess/smoothing/_afni_smoothing.py +8 -7
  232. junifer/preprocess/smoothing/_fsl_smoothing.py +6 -4
  233. junifer/preprocess/smoothing/_nilearn_smoothing.py +7 -5
  234. junifer/preprocess/smoothing/smoothing.py +59 -40
  235. junifer/preprocess/smoothing/tests/test_smoothing.py +28 -8
  236. junifer/preprocess/tests/test_preprocess_base.py +17 -9
  237. junifer/preprocess/tests/test_temporal_filter.py +37 -16
  238. junifer/preprocess/tests/test_temporal_slicer.py +12 -12
  239. junifer/preprocess/warping/__init__.pyi +2 -2
  240. junifer/preprocess/warping/_ants_warper.py +56 -40
  241. junifer/preprocess/warping/_fsl_warper.py +49 -36
  242. junifer/preprocess/warping/_utils.py +106 -0
  243. junifer/preprocess/warping/space_warper.py +99 -98
  244. junifer/preprocess/warping/tests/test_space_warper.py +46 -41
  245. junifer/preprocess/warping/tests/test_warping_utils.py +163 -0
  246. junifer/stats.py +46 -34
  247. junifer/storage/__init__.pyi +8 -4
  248. junifer/storage/base.py +129 -64
  249. junifer/storage/hdf5.py +307 -199
  250. junifer/storage/pandas_base.py +46 -24
  251. junifer/storage/sqlite.py +99 -62
  252. junifer/storage/tests/data/collected_h5io_fork.hdf5 +0 -0
  253. junifer/storage/tests/test_hdf5.py +147 -12
  254. junifer/storage/tests/test_repeated_storage.py +293 -0
  255. junifer/storage/tests/test_sqlite.py +9 -20
  256. junifer/storage/tests/test_storage_base.py +18 -21
  257. junifer/storage/tests/test_utils.py +83 -22
  258. junifer/storage/utils.py +58 -38
  259. junifer/testing/__init__.pyi +2 -2
  260. junifer/testing/datagrabbers.py +480 -44
  261. junifer/testing/registry.py +12 -0
  262. junifer/testing/tests/test_adhdtesting_datagrabber.py +24 -0
  263. junifer/testing/tests/test_oasisvmbtesting_datagrabber.py +5 -0
  264. junifer/testing/tests/test_partlycloudytesting_datagrabber.py +52 -0
  265. junifer/testing/tests/test_spmauditory_datagrabber.py +16 -0
  266. junifer/testing/tests/test_testing_registry.py +31 -0
  267. junifer/testing/tests/test_testing_utils.py +35 -0
  268. junifer/testing/tests/test_uclacnpvbmtesting_datagrabber.py +81 -0
  269. junifer/testing/utils.py +33 -1
  270. junifer/tests/test_stats.py +1 -5
  271. junifer/typing/__init__.pyi +2 -0
  272. junifer/typing/_typing.py +34 -19
  273. junifer/typing/py.typed +0 -0
  274. junifer/utils/__init__.pyi +10 -1
  275. junifer/utils/_config.py +1 -2
  276. junifer/utils/helpers.py +68 -2
  277. junifer/utils/logging.py +31 -7
  278. junifer/utils/tests/test_helpers.py +36 -1
  279. {junifer-0.0.7.dev332.dist-info → junifer-0.0.8.dist-info}/METADATA +43 -35
  280. junifer-0.0.8.dist-info/RECORD +402 -0
  281. {junifer-0.0.7.dev332.dist-info → junifer-0.0.8.dist-info}/WHEEL +1 -1
  282. junifer/cli/tests/test_parser.py +0 -280
  283. junifer/external/h5io/h5io/__init__.py +0 -15
  284. junifer/external/h5io/h5io/_h5io.py +0 -767
  285. junifer/external/h5io/h5io/_version.py +0 -1
  286. junifer/external/h5io/h5io/chunked_array.py +0 -94
  287. junifer/external/h5io/h5io/chunked_list.py +0 -65
  288. junifer/markers/complexity/__init__.pyi +0 -17
  289. junifer-0.0.7.dev332.dist-info/RECORD +0 -353
  290. {junifer-0.0.7.dev332.dist-info → junifer-0.0.8.dist-info}/entry_points.txt +0 -0
  291. {junifer-0.0.7.dev332.dist-info → junifer-0.0.8.dist-info}/licenses/AUTHORS.rst +0 -0
  292. {junifer-0.0.7.dev332.dist-info → junifer-0.0.8.dist-info}/licenses/LICENSE.md +0 -0
  293. {junifer-0.0.7.dev332.dist-info → junifer-0.0.8.dist-info}/top_level.txt +0 -0
@@ -5,21 +5,48 @@
5
5
  # License: AGPL
6
6
 
7
7
  import tempfile
8
+ from collections.abc import Callable
9
+ from enum import Enum, StrEnum
8
10
  from pathlib import Path
11
+ from typing import Any, ClassVar
12
+ from uuid import uuid4
9
13
 
10
14
  import nibabel as nib
15
+ import numpy as np
16
+ import pandas as pd
11
17
  from nilearn import datasets, image
12
18
 
13
- from ..datagrabber.base import BaseDataGrabber
19
+ from ..datagrabber import BaseDataGrabber, DataType
14
20
 
15
21
 
16
22
  __all__ = [
23
+ "ADHDTestingDataGrabber",
17
24
  "OasisVBMTestingDataGrabber",
25
+ "PartlyCloudyAgeGroup",
18
26
  "PartlyCloudyTestingDataGrabber",
19
27
  "SPMAuditoryTestingDataGrabber",
28
+ "UCLACNPVBMTestingDataGrabber",
20
29
  ]
21
30
 
22
31
 
32
+ def _save_atomic(out_path: Path, save: Callable[[Path], None]) -> None:
33
+ """Save a file by writing to a temporary file and renaming it.
34
+
35
+ Concurrent test workers never read a partially written file.
36
+
37
+ Parameters
38
+ ----------
39
+ out_path : pathlib.Path
40
+ The path to save to.
41
+ save : callable
42
+ Function that writes the file to the path it is given.
43
+
44
+ """
45
+ tmp_path = out_path.with_name(f".{uuid4().hex}.{out_path.name}")
46
+ save(tmp_path)
47
+ tmp_path.replace(out_path)
48
+
49
+
23
50
  class OasisVBMTestingDataGrabber(BaseDataGrabber):
24
51
  """DataGrabber for Oasis VBM testing data.
25
52
 
@@ -27,12 +54,9 @@ class OasisVBMTestingDataGrabber(BaseDataGrabber):
27
54
 
28
55
  """
29
56
 
30
- def __init__(self) -> None:
31
- # Create temporary directory
32
- datadir = tempfile.mkdtemp()
33
- # Define types
34
- types = ["VBM_GM"]
35
- super().__init__(types=types, datadir=datadir)
57
+ types: list[DataType] = [DataType.VBM_GM] # noqa: RUF012
58
+ datadir: Path = Path(tempfile.mkdtemp())
59
+ _dataset: Any = None
36
60
 
37
61
  def get_element_keys(self) -> list[str]:
38
62
  """Get element keys.
@@ -91,19 +115,198 @@ class OasisVBMTestingDataGrabber(BaseDataGrabber):
91
115
  return [f"sub-{x:02d}" for x in list(range(1, 11))]
92
116
 
93
117
 
118
+ class UCLACNPVBMTestingDataGrabber(BaseDataGrabber):
119
+ """DataGrabber for UCLA CNP VBM testing data.
120
+
121
+ Grey matter probability maps (computed with fMRIPrep) of 10 control
122
+ subjects of the UCLA Consortium for Neuropsychiatric Phenomics LA5c
123
+ study (OpenNeuro ds000030, release R1.0.4), downloaded with
124
+ :func:`nilearn.datasets.fetch_openneuro_dataset`.
125
+
126
+ The maps are in MNI152NLin2009cAsym at 1mm. Other resolutions are
127
+ resampled (linear interpolation) to the corresponding templateflow grid,
128
+ so templates and masks at that resolution do not need resampling. They
129
+ are cached (uncompressed) next to the original files in the nilearn data
130
+ directory.
131
+
132
+ Parameters
133
+ ----------
134
+ resolution : float, optional
135
+ The resolution (in mm) of the maps (default 2.0).
136
+
137
+ """
138
+
139
+ types: list[DataType] = [DataType.VBM_GM] # noqa: RUF012
140
+ datadir: Path = Path(tempfile.mkdtemp())
141
+ resolution: float = 2.0
142
+
143
+ _BASE_URL: ClassVar[str] = (
144
+ "https://s3.amazonaws.com/openneuro/ds000030/ds000030_R1.0.4/"
145
+ "uncompressed"
146
+ )
147
+ # Controls spread across the age range (21 to 50 years)
148
+ _SUBJECTS: ClassVar[list[str]] = [
149
+ "sub-10206",
150
+ "sub-10339",
151
+ "sub-10388",
152
+ "sub-10429",
153
+ "sub-10460",
154
+ "sub-10471",
155
+ "sub-10527",
156
+ "sub-10696",
157
+ "sub-11059",
158
+ "sub-11105",
159
+ ]
160
+
161
+ def __enter__(self) -> "UCLACNPVBMTestingDataGrabber":
162
+ """Implement context entry.
163
+
164
+ Returns
165
+ -------
166
+ UCLACNPVBMTestingDataGrabber
167
+
168
+ """
169
+ urls = [f"{self._BASE_URL}/participants.tsv"] + [
170
+ f"{self._BASE_URL}/derivatives/fmriprep/{sub}/anat/"
171
+ f"{sub}_T1w_space-MNI152NLin2009cAsym_class-GM_probtissue.nii.gz"
172
+ for sub in self._SUBJECTS
173
+ ]
174
+ _, files = datasets.fetch_openneuro_dataset(urls=urls, verbose=0)
175
+ files = {Path(f).name: Path(f) for f in files}
176
+ self._participants_path = files["participants.tsv"]
177
+ self._paths = {
178
+ sub: next(f for name, f in files.items() if name.startswith(sub))
179
+ for sub in self._SUBJECTS
180
+ }
181
+ return self
182
+
183
+ def get_element_keys(self) -> list[str]:
184
+ """Get element keys.
185
+
186
+ Returns
187
+ -------
188
+ list of str
189
+ The element keys.
190
+
191
+ """
192
+ return ["subject"]
193
+
194
+ def get_elements(self) -> list[str]:
195
+ """Get elements.
196
+
197
+ Returns
198
+ -------
199
+ list of str
200
+ List of elements that can be grabbed.
201
+
202
+ """
203
+ return list(self._SUBJECTS)
204
+
205
+ def get_participants(self) -> pd.DataFrame:
206
+ """Get the participants information.
207
+
208
+ Returns
209
+ -------
210
+ pandas.DataFrame
211
+ The participants information (e.g. age and gender), indexed by
212
+ subject.
213
+
214
+ """
215
+ participants = pd.read_csv(self._participants_path, sep="\t")
216
+ participants = participants.set_index("participant_id")
217
+ return participants.loc[self._SUBJECTS]
218
+
219
+ def get_item(self, subject: str) -> dict[str, dict]:
220
+ """Implement indexing support.
221
+
222
+ Parameters
223
+ ----------
224
+ subject : str
225
+ The subject to retrieve.
226
+
227
+ Returns
228
+ -------
229
+ dict
230
+ The data along with the metadata.
231
+
232
+ """
233
+ path = self._paths[subject]
234
+ return {
235
+ "VBM_GM": {
236
+ "path": self._get_resampled(path),
237
+ "space": "MNI152NLin2009cAsym",
238
+ }
239
+ }
240
+
241
+ def _get_resampled(self, path: Path) -> Path:
242
+ """Get the map resampled to the requested resolution.
243
+
244
+ The resampled map is cached next to the original one, so it is
245
+ computed only once.
246
+
247
+ Parameters
248
+ ----------
249
+ path : pathlib.Path
250
+ The path to the original map.
251
+
252
+ Returns
253
+ -------
254
+ pathlib.Path
255
+ The path to the resampled map.
256
+
257
+ """
258
+ res = self.resolution
259
+ # The data is on the 1mm templateflow grid; the grids at other
260
+ # resolutions keep the same bounding box
261
+ target_affine = np.array(
262
+ [
263
+ [res, 0, 0, -96 - (res - 1) / 2],
264
+ [0, res, 0, -132 - (res - 1) / 2],
265
+ [0, 0, res, -78 - (res - 1) / 2],
266
+ [0, 0, 0, 1],
267
+ ]
268
+ )
269
+ target_shape = tuple(int(np.ceil(n / res)) for n in (193, 229, 193))
270
+ img = nib.load(path)
271
+ # Original data is already in the requested grid
272
+ if img.shape[:3] == target_shape and np.allclose(
273
+ img.affine, target_affine
274
+ ):
275
+ return path
276
+
277
+ # Store uncompressed so it can be memory-mapped
278
+ out_path = path.with_name(
279
+ path.name.replace(".nii.gz", f"_res-{res:g}mm.nii")
280
+ )
281
+ if not out_path.exists():
282
+ img = image.resample_img(
283
+ img,
284
+ target_affine=target_affine,
285
+ target_shape=target_shape,
286
+ interpolation="linear",
287
+ )
288
+ img.set_data_dtype(np.float32)
289
+ _save_atomic(out_path, lambda x: nib.save(img, x))
290
+ return out_path
291
+
292
+
94
293
  class SPMAuditoryTestingDataGrabber(BaseDataGrabber):
95
294
  """DataGrabber for SPM Auditory dataset.
96
295
 
97
- Wrapper for :func:`nilearn.datasets.fetch_spm_auditory`.
296
+ Wrapper for :func:`nilearn.datasets.fetch_spm_auditory`. The dataset has
297
+ a single subject, which is provided for each of the elements.
298
+
299
+ Parameters
300
+ ----------
301
+ n_timepoints : int or None, optional
302
+ The number of BOLD timepoints to keep. If None, all the 84
303
+ timepoints are kept (default None).
98
304
 
99
305
  """
100
306
 
101
- def __init__(self) -> None:
102
- # Create temporary directory
103
- datadir = tempfile.mkdtemp()
104
- # Define types
105
- types = ["BOLD", "T1w"] # TODO: Check that they are T1w
106
- super().__init__(types=types, datadir=datadir)
307
+ types: list[DataType] = [DataType.BOLD, DataType.T1w] # noqa: RUF012
308
+ datadir: Path = Path(tempfile.mkdtemp())
309
+ n_timepoints: int | None = None
107
310
 
108
311
  def get_element_keys(self) -> list[str]:
109
312
  """Get element keys.
@@ -142,19 +345,42 @@ class SPMAuditoryTestingDataGrabber(BaseDataGrabber):
142
345
 
143
346
  """
144
347
  out = {}
145
- nilearn_data = datasets.fetch_spm_auditory(subject_id=subject)
146
- fmri_img = image.concat_imgs(nilearn_data.func) # type: ignore
147
- anat_img = image.concat_imgs(nilearn_data.anat) # type: ignore
148
-
149
- fmri_fname = self.datadir / f"{subject}_bold.nii.gz"
348
+ tp_suffix = (
349
+ f"_tp-{self.n_timepoints}" if self.n_timepoints is not None else ""
350
+ )
351
+ fmri_fname = self.datadir / f"{subject}{tp_suffix}_bold.nii.gz"
150
352
  anat_fname = self.datadir / f"{subject}_T1w.nii.gz"
151
- nib.save(fmri_img, fmri_fname)
152
- nib.save(anat_img, anat_fname)
353
+ # Files only depend on the parameters, so reuse them if already there
354
+ if not fmri_fname.exists() or not anat_fname.exists():
355
+ nilearn_data = datasets.fetch_spm_auditory(verbose=0)
356
+ if not fmri_fname.exists():
357
+ # Keep only the timepoints needed
358
+ fmri_img = image.index_img(
359
+ nilearn_data.func[0], slice(None, self.n_timepoints)
360
+ )
361
+ _save_atomic(fmri_fname, lambda x: nib.save(fmri_img, x))
362
+ if not anat_fname.exists():
363
+ anat_img = nib.load(nilearn_data.anat)
364
+ _save_atomic(anat_fname, lambda x: nib.save(anat_img, x))
153
365
  out["BOLD"] = {"path": fmri_fname, "space": "MNI152Lin"}
154
366
  out["T1w"] = {"path": anat_fname, "space": "native"}
155
367
  return out
156
368
 
157
369
 
370
+ class PartlyCloudyAgeGroup(StrEnum):
371
+ """Age group to fetch.
372
+
373
+ * ``Adult`` : fetch adults only (n=33, ages 18-39)
374
+ * ``Child`` : fetch children only (n=122, ages 3-12)
375
+ * ``Both`` : fetch full sample (n=155)
376
+
377
+ """
378
+
379
+ Adult = "adult"
380
+ Child = "child"
381
+ Both = "both"
382
+
383
+
158
384
  class PartlyCloudyTestingDataGrabber(BaseDataGrabber):
159
385
  """DataGrabber for Partly Cloudy dataset.
160
386
 
@@ -169,28 +395,23 @@ class PartlyCloudyTestingDataGrabber(BaseDataGrabber):
169
395
  purpose of having realistic examples. Depending on your research
170
396
  question, other confounds might be more appropriate.
171
397
  If False, returns all :term:`fMRIPrep` confounds (default True).
172
-
173
398
  age_group : {"adult", "child", "both"}, optional
174
- Which age group to fetch:
175
-
176
- * ``adult`` : fetch adults only (n=33, ages 18-39)
177
- * ``child`` : fetch children only (n=122, ages 3-12)
178
- * ``both`` : fetch full sample (n=155)
179
-
180
- (default "both")
399
+ Age group to fetch (default ``PartlyCloudyAgeGroup.Both``).
400
+ n_subjects : int, optional
401
+ The number of subjects to use (default 10).
402
+ n_timepoints : int or None, optional
403
+ The number of timepoints to keep. The truncated BOLD images and
404
+ confounds are cached next to the original files in the nilearn data
405
+ directory. If None, all the 168 timepoints are kept (default None).
181
406
 
182
407
  """
183
408
 
184
- def __init__(
185
- self, reduce_confounds: bool = True, age_group: str = "both"
186
- ) -> None:
187
- """Initialize the class."""
188
- datadir = tempfile.mkdtemp()
189
- # Define types
190
- types = ["BOLD"]
191
- self.reduce_confounds = reduce_confounds
192
- self.age_group = age_group
193
- super().__init__(types=types, datadir=datadir)
409
+ types: list[DataType] = [DataType.BOLD] # noqa: RUF012
410
+ datadir: Path = Path(tempfile.mkdtemp())
411
+ reduce_confounds: bool = True
412
+ age_group: PartlyCloudyAgeGroup = PartlyCloudyAgeGroup.Both
413
+ n_subjects: int = 10
414
+ n_timepoints: int | None = None
194
415
 
195
416
  def __enter__(self) -> "PartlyCloudyTestingDataGrabber":
196
417
  """Implement context entry.
@@ -201,9 +422,11 @@ class PartlyCloudyTestingDataGrabber(BaseDataGrabber):
201
422
 
202
423
  """
203
424
  self._dataset = datasets.fetch_development_fmri(
204
- n_subjects=10,
425
+ n_subjects=self.n_subjects,
205
426
  reduce_confounds=self.reduce_confounds,
206
- age_group=self.age_group,
427
+ age_group=self.age_group.value
428
+ if isinstance(self.age_group, Enum)
429
+ else self.age_group,
207
430
  )
208
431
  return self
209
432
 
@@ -227,7 +450,7 @@ class PartlyCloudyTestingDataGrabber(BaseDataGrabber):
227
450
  List of elements that can be grabbed.
228
451
 
229
452
  """
230
- return [f"sub-{x:02d}" for x in list(range(1, 11))]
453
+ return [f"sub-{x:02d}" for x in range(1, self.n_subjects + 1)]
231
454
 
232
455
  def get_item(self, subject: str) -> dict[str, dict]:
233
456
  """Implement indexing support.
@@ -245,13 +468,226 @@ class PartlyCloudyTestingDataGrabber(BaseDataGrabber):
245
468
  """
246
469
  out = {}
247
470
  i_sub = int(subject.split("-")[1]) - 1
471
+ bold_path = Path(self._dataset["func"][i_sub])
472
+ confounds_path = Path(self._dataset["confounds"][i_sub])
473
+ if self.n_timepoints is not None:
474
+ bold_path, confounds_path = self._truncate(
475
+ bold_path, confounds_path
476
+ )
248
477
  out["BOLD"] = {
249
- "path": Path(self._dataset["func"][i_sub]),
478
+ "path": bold_path,
250
479
  "space": "MNI152NLin2009cAsym",
251
480
  "confounds": {
252
- "path": Path(self._dataset["confounds"][i_sub]),
481
+ "path": confounds_path,
253
482
  "format": "fmriprep",
254
483
  },
255
484
  }
256
485
 
257
486
  return out
487
+
488
+ def _truncate(
489
+ self, bold_path: Path, confounds_path: Path
490
+ ) -> tuple[Path, Path]:
491
+ """Truncate the BOLD image and confounds to ``n_timepoints``.
492
+
493
+ The truncated files are cached next to the original ones, so they
494
+ are computed only once.
495
+
496
+ Parameters
497
+ ----------
498
+ bold_path : pathlib.Path
499
+ The path to the original BOLD image.
500
+ confounds_path : pathlib.Path
501
+ The path to the original confounds file.
502
+
503
+ Returns
504
+ -------
505
+ pathlib.Path
506
+ The path to the truncated BOLD image.
507
+ pathlib.Path
508
+ The path to the truncated confounds file.
509
+
510
+ """
511
+ n_tp = self.n_timepoints
512
+ img = nib.load(bold_path)
513
+ if n_tp >= img.shape[3]:
514
+ return bold_path, confounds_path
515
+
516
+ # Store uncompressed so it can be memory-mapped
517
+ out_bold = bold_path.with_name(
518
+ bold_path.name.replace("_bold.nii.gz", f"_tp-{n_tp}_bold.nii")
519
+ )
520
+ if not out_bold.exists():
521
+ # Keep the raw values and the original scaling; letting nibabel
522
+ # rescale the subset would re-quantize the data
523
+ truncated = nib.Nifti1Image(
524
+ np.asanyarray(img.dataobj.get_unscaled())[..., :n_tp],
525
+ img.affine,
526
+ img.header,
527
+ )
528
+ truncated.header.set_slope_inter(
529
+ img.dataobj.slope, img.dataobj.inter
530
+ )
531
+ _save_atomic(out_bold, lambda x: nib.save(truncated, x))
532
+
533
+ out_confounds = confounds_path.with_name(
534
+ confounds_path.name.replace(
535
+ "_regressors.tsv", f"_tp-{n_tp}_regressors.tsv"
536
+ )
537
+ )
538
+ if not out_confounds.exists():
539
+ # Copy the header and the first rows verbatim
540
+ lines = confounds_path.read_text().splitlines(keepends=True)
541
+ _save_atomic(
542
+ out_confounds,
543
+ lambda x: x.write_text("".join(lines[: n_tp + 1])),
544
+ )
545
+ return out_bold, out_confounds
546
+
547
+
548
+ class ADHDTestingDataGrabber(BaseDataGrabber):
549
+ """DataGrabber for ADHD dataset.
550
+
551
+ Wrapper for :func:`nilearn.datasets.fetch_adhd`. The BOLD images are
552
+ truncated to the first ``n_timepoints``, resampled to the requested
553
+ resolution on the MNI152NLin6Asym (FSL) grid and cached (uncompressed)
554
+ next to the original files in the nilearn data directory.
555
+
556
+ Parameters
557
+ ----------
558
+ resolution : float, optional
559
+ The resolution (in mm) of the BOLD images. The original data is in
560
+ 3mm, so other resolutions are resampled (default 3.0).
561
+ n_timepoints : int or None, optional
562
+ The number of timepoints to keep. If None, all the 176 timepoints
563
+ are kept (default 50).
564
+
565
+ """
566
+
567
+ types: list[DataType] = [DataType.BOLD] # noqa: RUF012
568
+ datadir: Path = Path(tempfile.mkdtemp())
569
+ resolution: float = 3.0
570
+ n_timepoints: int | None = 50
571
+
572
+ def __enter__(self) -> "ADHDTestingDataGrabber":
573
+ """Implement context entry.
574
+
575
+ Returns
576
+ -------
577
+ ADHDTestingDataGrabber
578
+
579
+ """
580
+ self._dataset = datasets.fetch_adhd(n_subjects=10)
581
+ return self
582
+
583
+ def get_element_keys(self) -> list[str]:
584
+ """Get element keys.
585
+
586
+ Returns
587
+ -------
588
+ list of str
589
+ The element keys.
590
+
591
+ """
592
+ return ["subject"]
593
+
594
+ def get_elements(self) -> list[str]:
595
+ """Get elements.
596
+
597
+ Returns
598
+ -------
599
+ list of str
600
+ List of elements that can be grabbed.
601
+
602
+ """
603
+ return [f"sub-{x:02d}" for x in list(range(1, 11))]
604
+
605
+ def get_item(self, subject: str) -> dict[str, dict]:
606
+ """Implement indexing support.
607
+
608
+ Parameters
609
+ ----------
610
+ subject : str
611
+ The subject to retrieve.
612
+
613
+ Returns
614
+ -------
615
+ dict
616
+ The data along with the metadata.
617
+
618
+ """
619
+ out = {}
620
+ i_sub = int(subject.split("-")[1]) - 1
621
+ out["BOLD"] = {
622
+ "path": self._get_bold(Path(self._dataset["func"][i_sub])),
623
+ "space": "MNI152NLin6Asym",
624
+ }
625
+
626
+ return out
627
+
628
+ def _get_bold(self, path: Path) -> Path:
629
+ """Get the BOLD image truncated and resampled as requested.
630
+
631
+ The resulting image is cached next to the original one, so it is
632
+ computed only once.
633
+
634
+ Parameters
635
+ ----------
636
+ path : pathlib.Path
637
+ The path to the original BOLD image.
638
+
639
+ Returns
640
+ -------
641
+ pathlib.Path
642
+ The path to the BOLD image.
643
+
644
+ """
645
+ res = self.resolution
646
+ n_tp = self.n_timepoints
647
+ # FSL MNI152 grid: x is flipped and the bounding box is
648
+ # 180 x 216 x 180 mm, with the origin at (90, -126, -72)
649
+ target_affine = np.array(
650
+ [
651
+ [-res, 0, 0, 90],
652
+ [0, res, 0, -126],
653
+ [0, 0, res, -72],
654
+ [0, 0, 0, 1],
655
+ ]
656
+ )
657
+ target_shape = (
658
+ round(180 / res) + 1,
659
+ round(216 / res) + 1,
660
+ round(180 / res) + 1,
661
+ )
662
+ img = nib.load(path)
663
+ needs_resample = img.shape[:3] != target_shape or not np.allclose(
664
+ img.affine, target_affine
665
+ )
666
+ needs_truncate = n_tp is not None and n_tp < img.shape[3]
667
+ # Original data is already as requested
668
+ if not needs_resample and not needs_truncate:
669
+ return path
670
+
671
+ # Store uncompressed so it can be memory-mapped; otherwise every
672
+ # load decompresses the whole file again
673
+ tp_suffix = f"_tp-{n_tp}" if needs_truncate else ""
674
+ out_path = path.with_name(
675
+ path.name.replace(".nii.gz", f"_res-{res:g}mm{tp_suffix}.nii")
676
+ )
677
+ if not out_path.exists():
678
+ if needs_truncate:
679
+ img = img.slicer[..., :n_tp]
680
+ if needs_resample:
681
+ tr = img.header.get_zooms()[3]
682
+ img = image.resample_img(
683
+ img,
684
+ target_affine=target_affine,
685
+ target_shape=target_shape,
686
+ interpolation="continuous",
687
+ )
688
+ # Resampling creates a new header, so restore the TR
689
+ img.header.set_zooms((res, res, res, tr))
690
+ img.header.set_xyzt_units(xyz="mm", t="sec")
691
+ img.set_data_dtype(np.float32)
692
+ _save_atomic(out_path, lambda x: nib.save(img, x))
693
+ return out_path
@@ -6,9 +6,11 @@
6
6
 
7
7
  from ..pipeline import PipelineComponentRegistry
8
8
  from .datagrabbers import (
9
+ ADHDTestingDataGrabber,
9
10
  OasisVBMTestingDataGrabber,
10
11
  PartlyCloudyTestingDataGrabber,
11
12
  SPMAuditoryTestingDataGrabber,
13
+ UCLACNPVBMTestingDataGrabber,
12
14
  )
13
15
 
14
16
 
@@ -27,3 +29,13 @@ PipelineComponentRegistry().register(
27
29
  step="datagrabber",
28
30
  klass=PartlyCloudyTestingDataGrabber,
29
31
  )
32
+
33
+ PipelineComponentRegistry().register(
34
+ step="datagrabber",
35
+ klass=ADHDTestingDataGrabber,
36
+ )
37
+
38
+ PipelineComponentRegistry().register(
39
+ step="datagrabber",
40
+ klass=UCLACNPVBMTestingDataGrabber,
41
+ )
@@ -0,0 +1,24 @@
1
+ """Provide tests for ADHDTestingDataGrabber."""
2
+
3
+ # Authors: Federico Raimondo <f.raimondo@fz-juelich.de>
4
+ # License: AGPL
5
+
6
+ import pytest
7
+
8
+ from junifer.testing.datagrabbers import ADHDTestingDataGrabber
9
+
10
+
11
+ pytestmark = pytest.mark.external
12
+
13
+
14
+ def test_ADHDTestingDataGrabber() -> None:
15
+ """Test ADHDTestingDataGrabber."""
16
+ expected_elements = [f"sub-{x:02d}" for x in range(1, 11)]
17
+ with ADHDTestingDataGrabber() as dg:
18
+ all_elements = dg.get_elements()
19
+ assert set(all_elements) == set(expected_elements)
20
+ out = dg["sub-01"]
21
+ assert "BOLD" in out
22
+ assert out["BOLD"]["path"].exists()
23
+ assert out["BOLD"]["path"].is_file()
24
+ assert out["BOLD"]["space"] == "MNI152NLin6Asym"
@@ -3,9 +3,14 @@
3
3
  # Authors: Federico Raimondo <f.raimondo@fz-juelich.de>
4
4
  # License: AGPL
5
5
 
6
+ import pytest
7
+
6
8
  from junifer.testing.datagrabbers import OasisVBMTestingDataGrabber
7
9
 
8
10
 
11
+ pytestmark = pytest.mark.external
12
+
13
+
9
14
  def test_OasisVBMTestingDataGrabber() -> None:
10
15
  """Test OasisVBMTestingDataGrabber."""
11
16
  expected_elements = [