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,154 +5,158 @@
5
5
  # Synchon Mandal <s.mandal@fz-juelich.de>
6
6
  # License: AGPL
7
7
 
8
+ import glob
8
9
  import re
10
+ from collections import defaultdict
9
11
  from copy import deepcopy
12
+ from enum import Enum
10
13
  from pathlib import Path
11
- from typing import Optional, Union
14
+ from typing import ClassVar
12
15
 
13
- import numpy as np
16
+ from aenum import Enum as AEnum
17
+ from aenum import extend_enum
18
+ from pydantic import Field
14
19
 
15
20
  from ..api.decorators import register_datagrabber
16
21
  from ..typing import DataGrabberPatterns, Elements
17
- from ..utils import logger, raise_error
18
- from .base import BaseDataGrabber
22
+ from ..utils import ensure_list, raise_error
23
+ from .base import BaseDataGrabber, logger
19
24
  from .pattern_validation_mixin import PatternValidationMixin
20
25
 
21
26
 
22
- __all__ = ["PatternDataGrabber"]
27
+ __all__ = [
28
+ "ConfoundsFormat",
29
+ "PatternDataGrabber",
30
+ "register_confounds_format",
31
+ ]
23
32
 
24
33
 
25
- # Accepted formats for confounds specification
26
- _CONFOUNDS_FORMATS = ("fmriprep", "adhoc")
34
+ class ConfoundsFormat(str, AEnum):
35
+ """Accepted confounds format."""
27
36
 
37
+ FMRIPrep = "fmriprep"
38
+ AdHoc = "adhoc"
28
39
 
29
- @register_datagrabber
30
- class PatternDataGrabber(BaseDataGrabber, PatternValidationMixin):
31
- """Concrete implementation for pattern-based data fetching.
32
40
 
33
- Implements a DataGrabber that understands patterns to grab data.
41
+ def register_confounds_format(name: str, alias: str) -> None:
42
+ """Register custom confounds format.
34
43
 
35
44
  Parameters
36
45
  ----------
37
- types : list of str
38
- The types of data to be grabbed.
39
- patterns : dict
40
- Data type patterns as a dictionary. It has the following schema:
41
-
42
- * ``"T1w"`` :
43
-
44
- .. code-block:: none
45
-
46
- {
47
- "mandatory": ["pattern", "space"],
48
- "optional": {
49
- "mask": {
50
- "mandatory": ["pattern", "space"],
51
- "optional": []
52
- }
53
- }
54
- }
55
-
56
- * ``"T2w"`` :
46
+ name : str
47
+ The confounds format name to be referred.
48
+ alias : str
49
+ The confounds format alias for string representation.
57
50
 
58
- .. code-block:: none
51
+ """
52
+ extend_enum(ConfoundsFormat, name, alias)
59
53
 
60
- {
61
- "mandatory": ["pattern", "space"],
62
- "optional": {
63
- "mask": {
64
- "mandatory": ["pattern", "space"],
65
- "optional": []
66
- }
67
- }
68
- }
69
54
 
70
- * ``"BOLD"`` :
71
-
72
- .. code-block:: none
73
-
74
- {
75
- "mandatory": ["pattern", "space"],
76
- "optional": {
77
- "mask": {
78
- "mandatory": ["pattern", "space"],
79
- "optional": []
80
- }
81
- "confounds": {
82
- "mandatory": ["pattern", "format"],
83
- "optional": []
84
- }
85
- }
86
- }
55
+ def _value(value: object) -> object:
56
+ """Get the value of an enum member, or the value itself.
87
57
 
88
- * ``"Warp"`` :
58
+ Parameters
59
+ ----------
60
+ value : object
61
+ The value.
89
62
 
90
- .. code-block:: none
63
+ Returns
64
+ -------
65
+ object
66
+ The value.
91
67
 
92
- {
93
- "mandatory": ["pattern", "src", "dst", "warper"],
94
- "optional": []
95
- }
96
-
97
- * ``"VBM_GM"`` :
68
+ """
69
+ return value.value if isinstance(value, Enum) else value
70
+
71
+
72
+ def _join_elements(
73
+ left: set[tuple[str, ...]],
74
+ left_keys: list[str],
75
+ right: set[tuple[str, ...]],
76
+ right_keys: list[str],
77
+ ) -> tuple[set[tuple[str, ...]], list[str]]:
78
+ """Join two sets of elements on their common keys.
79
+
80
+ Each data type (or pattern) has its own elements, with the values of the
81
+ replacements in its pattern. The elements of the datagrabber are the ones
82
+ available for all the data types, so the elements of two data types are
83
+ joined like database tables: an element of the first set is combined with
84
+ each element of the second set that has the same values for the common
85
+ keys, adding the values of the keys that only the second set has. An
86
+ element without a match in the other set is dropped.
87
+
88
+ For example, with BOLD elements ``(subject, session, task)``:
89
+ ``(sub-01, ses-1, rest)``, ``(sub-01, ses-1, movie)`` and
90
+ ``(sub-02, ses-1, rest)``:
91
+
92
+ * VBM elements ``(subject)``: ``(sub-01)`` and ``(sub-03)`` keep the BOLD
93
+ elements of ``sub-01``, as the VBM data only depends on the subject.
94
+ * DWI elements ``(subject, run)``: ``(sub-01, 1)`` and ``(sub-01, 2)``
95
+ give the elements ``(subject, session, task, run)`` for each BOLD
96
+ element of ``sub-01`` with each run, i.e., all the combinations.
97
+
98
+ The result is the same regardless of which set is first (except for the
99
+ order of the keys), so the order of the data types does not matter.
98
100
 
99
- .. code-block:: none
101
+ Parameters
102
+ ----------
103
+ left : set of tuple of str
104
+ The first elements, as tuples of the values of ``left_keys``.
105
+ left_keys : list of str
106
+ The keys of the first elements.
107
+ right : set of tuple of str
108
+ The second elements, as tuples of the values of ``right_keys``.
109
+ right_keys : list of str
110
+ The keys of the second elements.
111
+
112
+ Returns
113
+ -------
114
+ set of tuple of str
115
+ The elements with the values of the first and the second elements that
116
+ have the same values for the common keys.
117
+ list of str
118
+ The keys of the elements: ``left_keys`` and the other ``right_keys``.
100
119
 
101
- {
102
- "mandatory": ["pattern", "space"],
103
- "optional": []
104
- }
120
+ """
121
+ common = [k for k in right_keys if k in left_keys]
122
+ other = [i for i, k in enumerate(right_keys) if k not in left_keys]
123
+ # Values of the other keys of the second elements, by the common values
124
+ right_by_common = defaultdict(list)
125
+ for element in right:
126
+ common_values = tuple(element[right_keys.index(k)] for k in common)
127
+ right_by_common[common_values].append(tuple(element[i] for i in other))
128
+ joined = {
129
+ element + other_values
130
+ for element in left
131
+ for other_values in right_by_common[
132
+ tuple(element[left_keys.index(k)] for k in common)
133
+ ]
134
+ }
135
+ return joined, left_keys + [right_keys[i] for i in other]
105
136
 
106
- * ``"VBM_WM"`` :
107
137
 
108
- .. code-block:: none
138
+ @register_datagrabber
139
+ class PatternDataGrabber(BaseDataGrabber, PatternValidationMixin):
140
+ """Concrete implementation for pattern-based data fetching.
109
141
 
110
- {
111
- "mandatory": ["pattern", "space"],
112
- "optional": []
113
- }
142
+ Implements a DataGrabber that understands patterns to grab data.
114
143
 
115
- Basically, for each data type, one needs to provide ``mandatory`` keys
116
- and can choose to also provide ``optional`` keys. The value for each
117
- key is a string. So, one needs to provide necessary data types as a
118
- dictionary, for example:
119
-
120
- .. code-block:: none
121
-
122
- {
123
- "BOLD": {
124
- "pattern": "...",
125
- "space": "...",
126
- },
127
- "T1w": {
128
- "pattern": "...",
129
- "space": "...",
130
- },
131
- }
132
-
133
- except ``Warp``, which needs to be a list of dictionaries as there can
134
- be multiple spaces to warp (for example, with fMRIPrep):
135
-
136
- .. code-block:: none
137
-
138
- {
139
- "Warp": [
140
- {
141
- "pattern": "...",
142
- "src": "...",
143
- "dst": "...",
144
- "warper": "...",
145
- },
146
- ],
147
- }
148
-
149
- taken from :class:`.HCP1200`.
150
- replacements : str or list of str
151
- Replacements in the ``pattern`` key of each data type. The value needs
152
- to be a list of all possible replacements.
153
- datadir : str or pathlib.Path
154
- The directory where the data is / will be stored.
155
- confounds_format : {"fmriprep", "adhoc"} or None, optional
144
+ Parameters
145
+ ----------
146
+ types : :enum:`.DataType` or list of variants
147
+ The data type(s) to grab.
148
+ datadir : pathlib.Path
149
+ The path where the data is stored.
150
+ patterns : ``DataGrabberPatterns``
151
+ The datagrabber patterns. Check :class:`.DataTypeSchema` for the \
152
+ schema.
153
+ replacements : list of str or dict
154
+ All possible replacements in ``patterns.<data_type>.pattern``. As a
155
+ dictionary, the keys are the replacements and the values are the lists
156
+ of values to grab (or None to grab all of them), e.g.,
157
+ ``{"subject": None, "task": ["rest", "movie"]}`` to grab all the
158
+ subjects and only these tasks.
159
+ confounds_format : :enum:`.ConfoundsFormat` or None, optional
156
160
  The format of the confounds for the dataset (default None).
157
161
  partial_pattern_ok : bool, optional
158
162
  Whether to raise error if partial pattern for a data type is found.
@@ -162,52 +166,57 @@ class PatternDataGrabber(BaseDataGrabber, PatternValidationMixin):
162
166
  powerful when used with :class:`.MultipleDataGrabber`
163
167
  (default True).
164
168
 
165
- Raises
166
- ------
167
- ValueError
168
- If ``confounds_format`` is invalid.
169
+ Attributes
170
+ ----------
171
+ skip_file_check
169
172
 
170
173
  """
171
174
 
172
- def __init__(
173
- self,
174
- types: list[str],
175
- patterns: DataGrabberPatterns,
176
- replacements: Union[list[str], str],
177
- datadir: Union[str, Path],
178
- confounds_format: Optional[str] = None,
179
- partial_pattern_ok: bool = False,
180
- ) -> None:
181
- # Convert replacements to list if not already
182
- if not isinstance(replacements, list):
183
- replacements = [replacements]
175
+ patterns: DataGrabberPatterns = Field(frozen=True)
176
+ replacements: list[str] | dict[str, list[str] | None] = Field(frozen=True)
177
+
178
+ # Fields with the values of the replacements to grab, by replacement, e.g.,
179
+ # ``{"task": "tasks"}``
180
+ _REPLACEMENT_FIELDS: ClassVar[dict[str, str]] = {}
181
+ confounds_format: ConfoundsFormat | None = Field(None, frozen=True)
182
+ partial_pattern_ok: bool = Field(False, frozen=True)
183
+
184
+ def validate_datagrabber_params(self) -> None:
185
+ """Run extra logical validation for datagrabber."""
184
186
  # Validate patterns
185
187
  self.validate_patterns(
186
- types=types,
187
- replacements=replacements,
188
- patterns=patterns,
189
- partial_pattern_ok=partial_pattern_ok,
188
+ types=self.types,
189
+ replacements=list(self.replacements),
190
+ patterns=self.patterns,
191
+ partial_pattern_ok=self.partial_pattern_ok,
190
192
  )
191
- self.replacements = replacements
192
- self.patterns = patterns
193
- self.partial_pattern_ok = partial_pattern_ok
194
-
195
- # Validate confounds format
196
- if (
197
- confounds_format is not None
198
- and confounds_format not in _CONFOUNDS_FORMATS
199
- ):
200
- raise_error(
201
- "Invalid value for `confounds_format`, should be one of "
202
- f"{_CONFOUNDS_FORMATS}."
203
- )
204
- self.confounds_format = confounds_format
205
-
206
- super().__init__(types=types, datadir=datadir)
193
+ # Validate the fields with the values of the replacements
194
+ for replacement, field in self._REPLACEMENT_FIELDS.items():
195
+ if replacement not in self.replacements:
196
+ raise_error(
197
+ msg=(
198
+ f"Replacement: `{replacement}` of the field `{field}` "
199
+ f"is not one of the replacements: "
200
+ f"{list(self.replacements)}"
201
+ ),
202
+ klass=ValueError,
203
+ )
204
+ if (
205
+ isinstance(self.replacements, dict)
206
+ and self.replacements[replacement] is not None
207
+ ):
208
+ raise_error(
209
+ msg=(
210
+ f"The values of the replacement `{replacement}` are "
211
+ f"given by the field `{field}`, they cannot be given "
212
+ "in `replacements`"
213
+ ),
214
+ klass=ValueError,
215
+ )
207
216
  logger.debug("Initializing PatternDataGrabber")
208
- logger.debug(f"\tpatterns = {patterns}")
209
- logger.debug(f"\treplacements = {replacements}")
210
- logger.debug(f"\tconfounds_format = {confounds_format}")
217
+ logger.debug(f"\tpatterns = {self.patterns}")
218
+ logger.debug(f"\treplacements = {self.replacements}")
219
+ logger.debug(f"\tconfounds_format = {self.confounds_format}")
211
220
 
212
221
  @property
213
222
  def skip_file_check(self) -> bool:
@@ -242,18 +251,32 @@ class PatternDataGrabber(BaseDataGrabber, PatternValidationMixin):
242
251
  x for x in self.replacements if f"{{{x}}}" in pattern
243
252
  ]
244
253
  # Ops on re_pattern
245
- # Remove negated unix glob pattern i.e., [!...] for re_pattern
254
+ # Remove negated unix glob pattern i.e., [!...] for re_pattern, as the
255
+ # character is part of the following replacement (e.g., [!f]{subject})
246
256
  re_pattern = re.sub(r"\[!.?\]", "", re_pattern)
247
257
  # Remove enclosing square brackets from unix glob pattern i.e., [...]
248
258
  # for re_pattern
249
259
  re_pattern = re.sub(r"\[|\]", "", re_pattern)
250
- # Iteratively replace the first of each with a named group definition
251
- for t_r in t_replacements:
252
- re_pattern = re_pattern.replace(f"{{{t_r}}}", f"(?P<{t_r}>.*)", 1)
253
- # Iteratively replace the second appearance of each with the named
254
- # group back reference
255
- for t_r in t_replacements:
256
- re_pattern = re_pattern.replace(f"{{{t_r}}}", f"(?P={t_r})")
260
+ # Translate the replacements to named groups (the first appearance of
261
+ # each) or back references (the others), the unix glob wildcards to
262
+ # their regular expressions and escape the rest
263
+ parts = []
264
+ seen = set()
265
+ for token in re.split(r"(\{[^{}]*\}|\*|\?)", re_pattern):
266
+ name = token[1:-1]
267
+ if token.startswith("{") and name in t_replacements:
268
+ if name in seen:
269
+ parts.append(f"(?P={name})")
270
+ else:
271
+ parts.append(f"(?P<{name}>.*)")
272
+ seen.add(name)
273
+ elif token == "*":
274
+ parts.append("[^/]*")
275
+ elif token == "?":
276
+ parts.append("[^/]")
277
+ else:
278
+ parts.append(re.escape(token))
279
+ re_pattern = "".join(parts)
257
280
  # Ops on glob_pattern
258
281
  # Iteratively replace replacements with wildcard i.e., *
259
282
  # for glob_pattern
@@ -283,9 +306,9 @@ class PatternDataGrabber(BaseDataGrabber, PatternValidationMixin):
283
306
  If element keys do not match with replacements.
284
307
 
285
308
  """
286
- if list(element.keys()) != self.replacements:
309
+ if list(element.keys()) != self.get_element_keys():
287
310
  raise_error(
288
- f"The element keys must be {self.replacements}, "
311
+ f"The element keys must be {self.get_element_keys()}, "
289
312
  f"element has {list(element.keys())}."
290
313
  )
291
314
  # Remove negated unix glob pattern i.e., [!...]
@@ -323,9 +346,18 @@ class PatternDataGrabber(BaseDataGrabber, PatternValidationMixin):
323
346
  """
324
347
  # Replace element in the pattern for globbing
325
348
  resolved_pattern = self._replace_patterns_glob(element, pattern)
326
- # Resolve path for wildcard
327
- if "*" in resolved_pattern:
328
- t_matches = list(self.datadir.absolute().glob(resolved_pattern))
349
+ # Resolve path for wildcards
350
+ if "*" in resolved_pattern or "?" in resolved_pattern:
351
+ # glob.glob (unlike pathlib before Python 3.12) also matches the
352
+ # broken symbolic links, e.g., the files of a DataLad dataset that
353
+ # are not downloaded yet.
354
+ # TODO: go back to Path.glob once Python 3.11 is dropped (end of
355
+ # life in October 2027)
356
+ fulldir = self.fulldir.absolute()
357
+ t_matches = [
358
+ fulldir / x
359
+ for x in glob.glob(resolved_pattern, root_dir=fulldir)
360
+ ]
329
361
  # Multiple matches
330
362
  if len(t_matches) > 1:
331
363
  raise_error(
@@ -341,7 +373,8 @@ class PatternDataGrabber(BaseDataGrabber, PatternValidationMixin):
341
373
  )
342
374
  path = t_matches[0]
343
375
  else:
344
- path = self.datadir / resolved_pattern
376
+ # Absolute path, as for the patterns with wildcards
377
+ path = self.fulldir.absolute() / resolved_pattern
345
378
  if not self.skip_file_check:
346
379
  if not path.exists() and not path.is_symlink():
347
380
  raise_error(
@@ -352,23 +385,131 @@ class PatternDataGrabber(BaseDataGrabber, PatternValidationMixin):
352
385
 
353
386
  return path
354
387
 
388
+ def _get_type_patterns(self, data_type: str) -> list[str]:
389
+ """Get the patterns of a data type, including the nested ones.
390
+
391
+ Parameters
392
+ ----------
393
+ data_type : str
394
+ The data type.
395
+
396
+ Returns
397
+ -------
398
+ list of str
399
+ The patterns.
400
+
401
+ """
402
+ dtype_val = self.patterns[data_type]
403
+ # Conditional for list dtype vals like Warp
404
+ if not isinstance(dtype_val, list):
405
+ dtype_val = [dtype_val]
406
+ patterns = []
407
+ for val in dtype_val:
408
+ if "pattern" in val:
409
+ patterns.append(val["pattern"])
410
+ patterns.extend(
411
+ v["pattern"]
412
+ for v in val.values()
413
+ if isinstance(v, dict) and "pattern" in v
414
+ )
415
+ return patterns
416
+
417
+ def get_type_element_keys(self, data_type: str) -> list[str]:
418
+ """Get the element keys that the data of a data type depends on.
419
+
420
+ Parameters
421
+ ----------
422
+ data_type : str
423
+ The data type.
424
+
425
+ Returns
426
+ -------
427
+ list of str
428
+ The replacements in the patterns of the data type (including the
429
+ nested ones), in the order of ``replacements``.
430
+
431
+ """
432
+ patterns = self._get_type_patterns(data_type)
433
+ return [
434
+ x
435
+ for x in self.replacements
436
+ if any(f"{{{x}}}" in pattern for pattern in patterns)
437
+ ]
438
+
355
439
  def get_element_keys(self) -> list[str]:
356
440
  """Get element keys.
357
441
 
358
442
  For each item in the "element" tuple, this functions returns the
359
- corresponding key, that is, the ``replacements`` of patterns defined
360
- in the constructor.
443
+ corresponding key: the replacements in the patterns of the data
444
+ types to grab (see :meth:`.get_type_element_keys`), e.g., without
445
+ ``task`` if only anatomical data types are grabbed.
361
446
 
362
447
  Returns
363
448
  -------
364
449
  list of str
365
- The element keys.
450
+ The element keys, in the order of ``replacements``.
451
+
452
+ """
453
+ keys = {
454
+ k for t in self.get_types() for k in self.get_type_element_keys(t)
455
+ }
456
+ return [x for x in self.replacements if x in keys]
457
+
458
+ def get_replacement_values(self) -> dict[str, list[str]]:
459
+ """Get the values of the replacements to grab.
460
+
461
+ The values are given by ``replacements`` (as a dictionary) or by the
462
+ fields of the replacements (see ``_REPLACEMENT_FIELDS``).
463
+
464
+ Returns
465
+ -------
466
+ dict
467
+ The lists of values of the replacements to grab, by replacement.
468
+ The replacements without values (to grab all of them) are not
469
+ included.
366
470
 
367
471
  """
368
- return self.replacements
472
+ values = {}
473
+ if isinstance(self.replacements, dict):
474
+ values = {
475
+ k: list(v)
476
+ for k, v in self.replacements.items()
477
+ if v is not None
478
+ }
479
+ for replacement, field in self._REPLACEMENT_FIELDS.items():
480
+ field_values = getattr(self, field)
481
+ if field_values is not None:
482
+ values[replacement] = [
483
+ _value(x) for x in ensure_list(field_values)
484
+ ]
485
+ return values
486
+
487
+ def _check_element(self, element: dict) -> None:
488
+ """Check the element has values of the replacements to grab.
489
+
490
+ Parameters
491
+ ----------
492
+ element : dict
493
+ The element, as given to :meth:`.get_item`.
494
+
495
+ Raises
496
+ ------
497
+ ValueError
498
+ If a value of the element is not one of the values to grab.
499
+
500
+ """
501
+ for key, values in self.get_replacement_values().items():
502
+ if key in element and _value(element[key]) not in values:
503
+ raise_error(
504
+ msg=(
505
+ f"The value `{element[key]}` of `{key}` is not one of "
506
+ f"the values to grab: {values}"
507
+ ),
508
+ klass=ValueError,
509
+ )
369
510
 
370
511
  def get_item(self, **element: dict) -> dict[str, dict]:
371
- """Implement single element indexing for the datagrabber.
512
+ """Get the specified item from the dataset.
372
513
 
373
514
  This method constructs a real path to the requested item's data, by
374
515
  replacing the ``patterns`` with actual values passed via ``**element``.
@@ -454,8 +595,8 @@ class PatternDataGrabber(BaseDataGrabber, PatternValidationMixin):
454
595
  """Implement fetching list of elements in the dataset.
455
596
 
456
597
  It will use regex to search for "replacements" in the "patterns" and
457
- return the intersection of the results for each type i.e., build a
458
- list of elements that have all the required types.
598
+ join the results for each type on their common replacements i.e.,
599
+ build a list of elements that have all the required types.
459
600
 
460
601
  Returns
461
602
  -------
@@ -463,22 +604,10 @@ class PatternDataGrabber(BaseDataGrabber, PatternValidationMixin):
463
604
  The list of elements that can be grabbed in the dataset.
464
605
 
465
606
  """
607
+ # The elements found so far, as tuples of the values of `keys`
466
608
  elements = None
467
-
468
- # Iterate by number of replacements. At least one pattern must have
469
- # all of them.
470
- replacement_in_patterns = [
471
- np.sum([x in self.patterns[t_type] for x in self.replacements])
472
- for t_type in self.types
473
- ]
474
- order = np.argsort(replacement_in_patterns)
475
-
476
- # Start from the one with the most replacements and then
477
- # filter out elements based on the ones with less replacements.
478
- for t_idx in reversed(order):
479
- t_type = self.types[t_idx]
480
- types_element = set()
481
-
609
+ keys = []
610
+ for t_type in self.get_types():
482
611
  # Data type dictionary
483
612
  patterns = self.patterns[t_type]
484
613
  # Conditional for list dtype vals like Warp
@@ -496,46 +625,48 @@ class PatternDataGrabber(BaseDataGrabber, PatternValidationMixin):
496
625
  pattern = v["pattern"]
497
626
  break
498
627
 
499
- # Replace the pattern
628
+ # Find the elements of the pattern
500
629
  (
501
630
  re_pattern,
502
631
  glob_pattern,
503
- t_replacements,
632
+ t_keys,
504
633
  ) = self._replace_patterns_regex(pattern)
505
- for fname in self.datadir.glob(glob_pattern):
506
- suffix = fname.relative_to(self.datadir).as_posix()
634
+ t_elements = set()
635
+ # glob.glob (unlike pathlib before Python 3.12) also matches
636
+ # the broken symbolic links, e.g., the files of a DataLad
637
+ # dataset that are not downloaded yet.
638
+ # TODO: go back to Path.glob once Python 3.11 is dropped (end
639
+ # of life in October 2027)
640
+ for fname in glob.glob(glob_pattern, root_dir=self.fulldir):
641
+ suffix = Path(fname).as_posix()
507
642
  m = re.match(re_pattern, suffix)
508
643
  if m is not None:
509
- # Find the groups of replacements present in the
510
- # pattern. If one replacement is not present, set it
511
- # to None. We will take care of this in the
512
- # intersection.
513
- t_element = tuple([m.group(k) for k in t_replacements])
514
- if len(self.replacements) == 1:
515
- t_element = t_element[0]
516
- types_element.add(t_element)
517
- # TODO: does this make sense as elements is always None
644
+ t_elements.add(tuple(m.group(k) for k in t_keys))
645
+ # Keep the elements with the values to grab
646
+ replacement_values = self.get_replacement_values()
647
+ restricted = [
648
+ (i, set(replacement_values[k]))
649
+ for i, k in enumerate(t_keys)
650
+ if k in replacement_values
651
+ ]
652
+ t_elements = {
653
+ element
654
+ for element in t_elements
655
+ if all(element[i] in values for i, values in restricted)
656
+ }
657
+ # Keep the elements available for all the data types
518
658
  if elements is None:
519
- elements = types_element
659
+ elements, keys = t_elements, t_keys
520
660
  else:
521
- # Do the intersection by filtering out elements in which
522
- # the replacements are not None
523
- if t_replacements == self.replacements:
524
- elements.intersection(types_element)
525
- else:
526
- t_repl_idx = [
527
- i
528
- for i, v in enumerate(self.replacements)
529
- if v in t_replacements
530
- ]
531
- new_elements = set()
532
- for t_element in elements:
533
- if (
534
- tuple(np.array(t_element)[t_repl_idx])
535
- in types_element
536
- ):
537
- new_elements.add(t_element)
538
- elements = new_elements
661
+ elements, keys = _join_elements(
662
+ elements, keys, t_elements, t_keys
663
+ )
539
664
  if elements is None:
540
- elements = set()
541
- return list(elements)
665
+ return []
666
+ # Sort the values as the element keys
667
+ element_keys = self.get_element_keys()
668
+ idx = [keys.index(k) for k in element_keys]
669
+ out = sorted(tuple(element[i] for i in idx) for element in elements)
670
+ if len(element_keys) == 1:
671
+ out = [element[0] for element in out]
672
+ return out