junifer 0.0.4.dev829__py3-none-any.whl → 0.0.5__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 (206) hide show
  1. junifer/__init__.py +17 -0
  2. junifer/_version.py +2 -2
  3. junifer/api/__init__.py +4 -1
  4. junifer/api/cli.py +91 -1
  5. junifer/api/decorators.py +9 -0
  6. junifer/api/functions.py +56 -10
  7. junifer/api/parser.py +3 -0
  8. junifer/api/queue_context/__init__.py +4 -1
  9. junifer/api/queue_context/gnu_parallel_local_adapter.py +16 -6
  10. junifer/api/queue_context/htcondor_adapter.py +16 -5
  11. junifer/api/queue_context/tests/test_gnu_parallel_local_adapter.py +41 -12
  12. junifer/api/queue_context/tests/test_htcondor_adapter.py +48 -15
  13. junifer/api/res/afni/run_afni_docker.sh +1 -1
  14. junifer/api/res/ants/run_ants_docker.sh +1 -1
  15. junifer/api/res/freesurfer/mri_binarize +3 -0
  16. junifer/api/res/freesurfer/mri_mc +3 -0
  17. junifer/api/res/freesurfer/mri_pretess +3 -0
  18. junifer/api/res/freesurfer/mris_convert +3 -0
  19. junifer/api/res/freesurfer/run_freesurfer_docker.sh +61 -0
  20. junifer/api/res/fsl/run_fsl_docker.sh +1 -1
  21. junifer/api/res/{run_conda.sh → run_conda.bash} +1 -1
  22. junifer/api/res/run_conda.zsh +23 -0
  23. junifer/api/res/run_venv.bash +22 -0
  24. junifer/api/res/{run_venv.sh → run_venv.zsh} +1 -1
  25. junifer/api/tests/test_api_utils.py +4 -2
  26. junifer/api/tests/test_cli.py +83 -0
  27. junifer/api/tests/test_functions.py +27 -2
  28. junifer/configs/__init__.py +1 -1
  29. junifer/configs/juseless/__init__.py +4 -1
  30. junifer/configs/juseless/datagrabbers/__init__.py +10 -1
  31. junifer/configs/juseless/datagrabbers/aomic_id1000_vbm.py +4 -3
  32. junifer/configs/juseless/datagrabbers/camcan_vbm.py +3 -0
  33. junifer/configs/juseless/datagrabbers/ixi_vbm.py +4 -3
  34. junifer/configs/juseless/datagrabbers/tests/test_ucla.py +1 -3
  35. junifer/configs/juseless/datagrabbers/ucla.py +12 -9
  36. junifer/configs/juseless/datagrabbers/ukb_vbm.py +3 -0
  37. junifer/data/__init__.py +21 -1
  38. junifer/data/coordinates.py +10 -19
  39. junifer/data/masks/ukb/UKB_15K_GM_template.nii.gz +0 -0
  40. junifer/data/masks.py +58 -87
  41. junifer/data/parcellations.py +14 -3
  42. junifer/data/template_spaces.py +4 -1
  43. junifer/data/tests/test_masks.py +26 -37
  44. junifer/data/utils.py +3 -0
  45. junifer/datagrabber/__init__.py +18 -1
  46. junifer/datagrabber/aomic/__init__.py +3 -0
  47. junifer/datagrabber/aomic/id1000.py +70 -37
  48. junifer/datagrabber/aomic/piop1.py +69 -36
  49. junifer/datagrabber/aomic/piop2.py +71 -38
  50. junifer/datagrabber/aomic/tests/test_id1000.py +44 -100
  51. junifer/datagrabber/aomic/tests/test_piop1.py +65 -108
  52. junifer/datagrabber/aomic/tests/test_piop2.py +45 -102
  53. junifer/datagrabber/base.py +13 -6
  54. junifer/datagrabber/datalad_base.py +13 -1
  55. junifer/datagrabber/dmcc13_benchmark.py +36 -53
  56. junifer/datagrabber/hcp1200/__init__.py +3 -0
  57. junifer/datagrabber/hcp1200/datalad_hcp1200.py +3 -0
  58. junifer/datagrabber/hcp1200/hcp1200.py +4 -1
  59. junifer/datagrabber/multiple.py +45 -6
  60. junifer/datagrabber/pattern.py +170 -62
  61. junifer/datagrabber/pattern_datalad.py +25 -12
  62. junifer/datagrabber/pattern_validation_mixin.py +388 -0
  63. junifer/datagrabber/tests/test_datalad_base.py +4 -4
  64. junifer/datagrabber/tests/test_dmcc13_benchmark.py +46 -19
  65. junifer/datagrabber/tests/test_multiple.py +161 -84
  66. junifer/datagrabber/tests/test_pattern.py +45 -0
  67. junifer/datagrabber/tests/test_pattern_datalad.py +4 -4
  68. junifer/datagrabber/tests/test_pattern_validation_mixin.py +249 -0
  69. junifer/datareader/__init__.py +4 -1
  70. junifer/datareader/default.py +95 -43
  71. junifer/external/BrainPrint/brainprint/__init__.py +4 -0
  72. junifer/external/BrainPrint/brainprint/_version.py +3 -0
  73. junifer/external/BrainPrint/brainprint/asymmetry.py +91 -0
  74. junifer/external/BrainPrint/brainprint/brainprint.py +441 -0
  75. junifer/external/BrainPrint/brainprint/surfaces.py +258 -0
  76. junifer/external/BrainPrint/brainprint/utils/__init__.py +1 -0
  77. junifer/external/BrainPrint/brainprint/utils/_config.py +112 -0
  78. junifer/external/BrainPrint/brainprint/utils/utils.py +188 -0
  79. junifer/external/__init__.py +1 -1
  80. junifer/external/nilearn/__init__.py +5 -1
  81. junifer/external/nilearn/junifer_connectivity_measure.py +483 -0
  82. junifer/external/nilearn/junifer_nifti_spheres_masker.py +23 -9
  83. junifer/external/nilearn/tests/test_junifer_connectivity_measure.py +1089 -0
  84. junifer/external/nilearn/tests/test_junifer_nifti_spheres_masker.py +76 -1
  85. junifer/markers/__init__.py +23 -1
  86. junifer/markers/base.py +68 -28
  87. junifer/markers/brainprint.py +459 -0
  88. junifer/markers/collection.py +10 -2
  89. junifer/markers/complexity/__init__.py +10 -0
  90. junifer/markers/complexity/complexity_base.py +26 -43
  91. junifer/markers/complexity/hurst_exponent.py +3 -0
  92. junifer/markers/complexity/multiscale_entropy_auc.py +3 -0
  93. junifer/markers/complexity/perm_entropy.py +3 -0
  94. junifer/markers/complexity/range_entropy.py +3 -0
  95. junifer/markers/complexity/range_entropy_auc.py +3 -0
  96. junifer/markers/complexity/sample_entropy.py +3 -0
  97. junifer/markers/complexity/tests/test_hurst_exponent.py +11 -3
  98. junifer/markers/complexity/tests/test_multiscale_entropy_auc.py +11 -3
  99. junifer/markers/complexity/tests/test_perm_entropy.py +11 -3
  100. junifer/markers/complexity/tests/test_range_entropy.py +11 -3
  101. junifer/markers/complexity/tests/test_range_entropy_auc.py +11 -3
  102. junifer/markers/complexity/tests/test_sample_entropy.py +11 -3
  103. junifer/markers/complexity/tests/test_weighted_perm_entropy.py +11 -3
  104. junifer/markers/complexity/weighted_perm_entropy.py +3 -0
  105. junifer/markers/ets_rss.py +27 -42
  106. junifer/markers/falff/__init__.py +3 -0
  107. junifer/markers/falff/_afni_falff.py +5 -2
  108. junifer/markers/falff/_junifer_falff.py +3 -0
  109. junifer/markers/falff/falff_base.py +20 -46
  110. junifer/markers/falff/falff_parcels.py +56 -27
  111. junifer/markers/falff/falff_spheres.py +60 -29
  112. junifer/markers/falff/tests/test_falff_parcels.py +39 -23
  113. junifer/markers/falff/tests/test_falff_spheres.py +39 -23
  114. junifer/markers/functional_connectivity/__init__.py +9 -0
  115. junifer/markers/functional_connectivity/crossparcellation_functional_connectivity.py +63 -60
  116. junifer/markers/functional_connectivity/edge_functional_connectivity_parcels.py +45 -32
  117. junifer/markers/functional_connectivity/edge_functional_connectivity_spheres.py +49 -36
  118. junifer/markers/functional_connectivity/functional_connectivity_base.py +71 -70
  119. junifer/markers/functional_connectivity/functional_connectivity_parcels.py +34 -25
  120. junifer/markers/functional_connectivity/functional_connectivity_spheres.py +40 -30
  121. junifer/markers/functional_connectivity/tests/test_crossparcellation_functional_connectivity.py +11 -7
  122. junifer/markers/functional_connectivity/tests/test_edge_functional_connectivity_parcels.py +27 -7
  123. junifer/markers/functional_connectivity/tests/test_edge_functional_connectivity_spheres.py +28 -12
  124. junifer/markers/functional_connectivity/tests/test_functional_connectivity_parcels.py +35 -11
  125. junifer/markers/functional_connectivity/tests/test_functional_connectivity_spheres.py +36 -62
  126. junifer/markers/parcel_aggregation.py +47 -61
  127. junifer/markers/reho/__init__.py +3 -0
  128. junifer/markers/reho/_afni_reho.py +5 -2
  129. junifer/markers/reho/_junifer_reho.py +4 -1
  130. junifer/markers/reho/reho_base.py +8 -27
  131. junifer/markers/reho/reho_parcels.py +28 -17
  132. junifer/markers/reho/reho_spheres.py +27 -18
  133. junifer/markers/reho/tests/test_reho_parcels.py +8 -3
  134. junifer/markers/reho/tests/test_reho_spheres.py +8 -3
  135. junifer/markers/sphere_aggregation.py +43 -59
  136. junifer/markers/temporal_snr/__init__.py +3 -0
  137. junifer/markers/temporal_snr/temporal_snr_base.py +23 -32
  138. junifer/markers/temporal_snr/temporal_snr_parcels.py +9 -6
  139. junifer/markers/temporal_snr/temporal_snr_spheres.py +9 -6
  140. junifer/markers/temporal_snr/tests/test_temporal_snr_parcels.py +6 -3
  141. junifer/markers/temporal_snr/tests/test_temporal_snr_spheres.py +6 -3
  142. junifer/markers/tests/test_brainprint.py +58 -0
  143. junifer/markers/tests/test_collection.py +9 -8
  144. junifer/markers/tests/test_ets_rss.py +15 -9
  145. junifer/markers/tests/test_markers_base.py +17 -18
  146. junifer/markers/tests/test_parcel_aggregation.py +93 -32
  147. junifer/markers/tests/test_sphere_aggregation.py +72 -19
  148. junifer/onthefly/__init__.py +4 -1
  149. junifer/onthefly/read_transform.py +3 -0
  150. junifer/pipeline/__init__.py +9 -1
  151. junifer/pipeline/pipeline_step_mixin.py +21 -4
  152. junifer/pipeline/registry.py +3 -0
  153. junifer/pipeline/singleton.py +3 -0
  154. junifer/pipeline/tests/test_registry.py +1 -1
  155. junifer/pipeline/update_meta_mixin.py +3 -0
  156. junifer/pipeline/utils.py +67 -1
  157. junifer/pipeline/workdir_manager.py +3 -0
  158. junifer/preprocess/__init__.py +10 -2
  159. junifer/preprocess/base.py +6 -3
  160. junifer/preprocess/confounds/__init__.py +3 -0
  161. junifer/preprocess/confounds/fmriprep_confound_remover.py +47 -60
  162. junifer/preprocess/confounds/tests/test_fmriprep_confound_remover.py +72 -113
  163. junifer/preprocess/smoothing/__init__.py +9 -0
  164. junifer/preprocess/smoothing/_afni_smoothing.py +119 -0
  165. junifer/preprocess/smoothing/_fsl_smoothing.py +116 -0
  166. junifer/preprocess/smoothing/_nilearn_smoothing.py +69 -0
  167. junifer/preprocess/smoothing/smoothing.py +174 -0
  168. junifer/preprocess/smoothing/tests/test_smoothing.py +94 -0
  169. junifer/preprocess/warping/__init__.py +3 -0
  170. junifer/preprocess/warping/_ants_warper.py +3 -0
  171. junifer/preprocess/warping/_fsl_warper.py +3 -0
  172. junifer/stats.py +4 -1
  173. junifer/storage/__init__.py +9 -1
  174. junifer/storage/base.py +40 -1
  175. junifer/storage/hdf5.py +71 -9
  176. junifer/storage/pandas_base.py +3 -0
  177. junifer/storage/sqlite.py +3 -0
  178. junifer/storage/tests/test_hdf5.py +82 -10
  179. junifer/storage/utils.py +9 -0
  180. junifer/testing/__init__.py +4 -1
  181. junifer/testing/datagrabbers.py +13 -6
  182. junifer/testing/tests/test_partlycloudytesting_datagrabber.py +7 -7
  183. junifer/testing/utils.py +3 -0
  184. junifer/utils/__init__.py +13 -2
  185. junifer/utils/fs.py +3 -0
  186. junifer/utils/helpers.py +32 -1
  187. junifer/utils/logging.py +33 -4
  188. junifer/utils/tests/test_logging.py +8 -0
  189. {junifer-0.0.4.dev829.dist-info → junifer-0.0.5.dist-info}/METADATA +17 -16
  190. junifer-0.0.5.dist-info/RECORD +275 -0
  191. {junifer-0.0.4.dev829.dist-info → junifer-0.0.5.dist-info}/WHEEL +1 -1
  192. junifer/datagrabber/tests/test_datagrabber_utils.py +0 -218
  193. junifer/datagrabber/utils.py +0 -230
  194. junifer/preprocess/ants/__init__.py +0 -4
  195. junifer/preprocess/ants/ants_apply_transforms_warper.py +0 -185
  196. junifer/preprocess/ants/tests/test_ants_apply_transforms_warper.py +0 -56
  197. junifer/preprocess/bold_warper.py +0 -265
  198. junifer/preprocess/fsl/__init__.py +0 -4
  199. junifer/preprocess/fsl/apply_warper.py +0 -179
  200. junifer/preprocess/fsl/tests/test_apply_warper.py +0 -45
  201. junifer/preprocess/tests/test_bold_warper.py +0 -159
  202. junifer-0.0.4.dev829.dist-info/RECORD +0 -257
  203. {junifer-0.0.4.dev829.dist-info → junifer-0.0.5.dist-info}/AUTHORS.rst +0 -0
  204. {junifer-0.0.4.dev829.dist-info → junifer-0.0.5.dist-info}/LICENSE.md +0 -0
  205. {junifer-0.0.4.dev829.dist-info → junifer-0.0.5.dist-info}/entry_points.txt +0 -0
  206. {junifer-0.0.4.dev829.dist-info → junifer-0.0.5.dist-info}/top_level.txt +0 -0
@@ -7,10 +7,15 @@
7
7
 
8
8
  from typing import Dict, List, Tuple, Union
9
9
 
10
- from ..utils import raise_error
10
+ from ..api.decorators import register_datagrabber
11
+ from ..utils import deep_update, raise_error
11
12
  from .base import BaseDataGrabber
12
13
 
13
14
 
15
+ __all__ = ["MultipleDataGrabber"]
16
+
17
+
18
+ @register_datagrabber
14
19
  class MultipleDataGrabber(BaseDataGrabber):
15
20
  """Concrete implementation for multi sourced data fetching.
16
21
 
@@ -24,19 +29,53 @@ class MultipleDataGrabber(BaseDataGrabber):
24
29
  **kwargs
25
30
  Keyword arguments passed to superclass.
26
31
 
32
+ Raises
33
+ ------
34
+ RuntimeError
35
+ If ``datagrabbers`` have different element keys or
36
+ overlapping data types or nested data types.
37
+
27
38
  """
28
39
 
29
40
  def __init__(self, datagrabbers: List[BaseDataGrabber], **kwargs) -> None:
30
41
  # Check datagrabbers consistency
31
- # 1) same element keys
42
+ # Check for same element keys
32
43
  first_keys = datagrabbers[0].get_element_keys()
33
44
  for dg in datagrabbers[1:]:
34
45
  if dg.get_element_keys() != first_keys:
35
- raise_error("DataGrabbers have different element keys.")
36
- # 2) no overlapping types
46
+ raise_error(
47
+ msg="DataGrabbers have different element keys",
48
+ klass=RuntimeError,
49
+ )
50
+ # Check for no overlapping types (and nested data types)
37
51
  types = [x for dg in datagrabbers for x in dg.get_types()]
38
52
  if len(types) != len(set(types)):
39
- raise_error("DataGrabbers have overlapping types.")
53
+ if all(hasattr(dg, "patterns") for dg in datagrabbers):
54
+ first_patterns = datagrabbers[0].patterns
55
+ for dg in datagrabbers[1:]:
56
+ for data_type in set(types):
57
+ dtype_pattern = dg.patterns.get(data_type)
58
+ if dtype_pattern is None:
59
+ continue
60
+ # Check if first-level keys of data type are same
61
+ if (
62
+ dtype_pattern.keys()
63
+ == first_patterns[data_type].keys()
64
+ ):
65
+ raise_error(
66
+ msg=(
67
+ "DataGrabbers have overlapping mandatory "
68
+ "and / or optional key(s) for data type: "
69
+ f"`{data_type}`"
70
+ ),
71
+ klass=RuntimeError,
72
+ )
73
+ else:
74
+ # Can't check further
75
+ raise_error(
76
+ msg="DataGrabbers have overlapping types",
77
+ klass=RuntimeError,
78
+ )
40
79
  self._datagrabbers = datagrabbers
41
80
 
42
81
  def __getitem__(self, element: Union[str, Tuple]) -> Dict:
@@ -62,7 +101,7 @@ class MultipleDataGrabber(BaseDataGrabber):
62
101
  metas = []
63
102
  for dg in self._datagrabbers:
64
103
  t_out = dg[element]
65
- out.update(t_out)
104
+ deep_update(out, t_out)
66
105
  # Now get the meta for this datagrabber
67
106
  t_meta = {}
68
107
  dg.update_meta(t_meta, "datagrabber")
@@ -6,6 +6,7 @@
6
6
  # License: AGPL
7
7
 
8
8
  import re
9
+ from copy import deepcopy
9
10
  from pathlib import Path
10
11
  from typing import Dict, List, Optional, Tuple, Union
11
12
 
@@ -14,7 +15,10 @@ import numpy as np
14
15
  from ..api.decorators import register_datagrabber
15
16
  from ..utils import logger, raise_error
16
17
  from .base import BaseDataGrabber
17
- from .utils import validate_patterns, validate_replacements
18
+ from .pattern_validation_mixin import PatternValidationMixin
19
+
20
+
21
+ __all__ = ["PatternDataGrabber"]
18
22
 
19
23
 
20
24
  # Accepted formats for confounds specification
@@ -22,7 +26,7 @@ _CONFOUNDS_FORMATS = ("fmriprep", "adhoc")
22
26
 
23
27
 
24
28
  @register_datagrabber
25
- class PatternDataGrabber(BaseDataGrabber):
29
+ class PatternDataGrabber(BaseDataGrabber, PatternValidationMixin):
26
30
  """Concrete implementation for pattern-based data fetching.
27
31
 
28
32
  Implements a DataGrabber that understands patterns to grab data.
@@ -40,7 +44,12 @@ class PatternDataGrabber(BaseDataGrabber):
40
44
 
41
45
  {
42
46
  "mandatory": ["pattern", "space"],
43
- "optional": []
47
+ "optional": {
48
+ "mask": {
49
+ "mandatory": ["pattern", "space"],
50
+ "optional": []
51
+ }
52
+ }
44
53
  }
45
54
 
46
55
  * ``"T2w"`` :
@@ -49,7 +58,12 @@ class PatternDataGrabber(BaseDataGrabber):
49
58
 
50
59
  {
51
60
  "mandatory": ["pattern", "space"],
52
- "optional": []
61
+ "optional": {
62
+ "mask": {
63
+ "mandatory": ["pattern", "space"],
64
+ "optional": []
65
+ }
66
+ }
53
67
  }
54
68
 
55
69
  * ``"BOLD"`` :
@@ -58,7 +72,16 @@ class PatternDataGrabber(BaseDataGrabber):
58
72
 
59
73
  {
60
74
  "mandatory": ["pattern", "space"],
61
- "optional": ["mask_item"]
75
+ "optional": {
76
+ "mask": {
77
+ "mandatory": ["pattern", "space"],
78
+ "optional": []
79
+ }
80
+ "confounds": {
81
+ "mandatory": ["pattern", "format"],
82
+ "optional": []
83
+ }
84
+ }
62
85
  }
63
86
 
64
87
  * ``"Warp"`` :
@@ -70,15 +93,6 @@ class PatternDataGrabber(BaseDataGrabber):
70
93
  "optional": []
71
94
  }
72
95
 
73
- * ``"BOLD_confounds"`` :
74
-
75
- .. code-block:: none
76
-
77
- {
78
- "mandatory": ["pattern", "format"],
79
- "optional": []
80
- }
81
-
82
96
  * ``"VBM_GM"`` :
83
97
 
84
98
  .. code-block:: none
@@ -128,6 +142,13 @@ class PatternDataGrabber(BaseDataGrabber):
128
142
  The directory where the data is / will be stored.
129
143
  confounds_format : {"fmriprep", "adhoc"} or None, optional
130
144
  The format of the confounds for the dataset (default None).
145
+ partial_pattern_ok : bool, optional
146
+ Whether to raise error if partial pattern for a data type is found.
147
+ This allows to bypass mandatory key check and issue a warning
148
+ instead of raising error. This allows one to have a DataGrabber
149
+ with data types without the corresponding mandatory keys and is
150
+ powerful when used with :class:`.MultipleDataGrabber`
151
+ (default True).
131
152
 
132
153
  Raises
133
154
  ------
@@ -143,17 +164,21 @@ class PatternDataGrabber(BaseDataGrabber):
143
164
  replacements: Union[List[str], str],
144
165
  datadir: Union[str, Path],
145
166
  confounds_format: Optional[str] = None,
167
+ partial_pattern_ok: bool = False,
146
168
  ) -> None:
147
- # Validate patterns
148
- validate_patterns(types=types, patterns=patterns)
149
- self.patterns = patterns
150
-
151
169
  # Convert replacements to list if not already
152
170
  if not isinstance(replacements, list):
153
171
  replacements = [replacements]
154
- # Validate replacements
155
- validate_replacements(replacements=replacements, patterns=patterns)
172
+ # Validate patterns
173
+ self.validate_patterns(
174
+ types=types,
175
+ replacements=replacements,
176
+ patterns=patterns,
177
+ partial_pattern_ok=partial_pattern_ok,
178
+ )
156
179
  self.replacements = replacements
180
+ self.patterns = patterns
181
+ self.partial_pattern_ok = partial_pattern_ok
157
182
 
158
183
  # Validate confounds format
159
184
  if (
@@ -204,18 +229,25 @@ class PatternDataGrabber(BaseDataGrabber):
204
229
  t_replacements = [
205
230
  x for x in self.replacements if f"{{{x}}}" in pattern
206
231
  ]
207
-
232
+ # Ops on re_pattern
233
+ # Remove negated unix glob pattern i.e., [!...] for re_pattern
234
+ re_pattern = re.sub(r"\[!.?\]", "", re_pattern)
235
+ # Remove enclosing square brackets from unix glob pattern i.e., [...]
236
+ # for re_pattern
237
+ re_pattern = re.sub(r"\[|\]", "", re_pattern)
238
+ # Iteratively replace the first of each with a named group definition
208
239
  for t_r in t_replacements:
209
- # Replace the first of each with a named group definition
210
240
  re_pattern = re_pattern.replace(f"{{{t_r}}}", f"(?P<{t_r}>.*)", 1)
211
-
241
+ # Iteratively replace the second appearance of each with the named
242
+ # group back reference
212
243
  for t_r in t_replacements:
213
- # Replace the second appearance of each with the named group
214
- # back reference
215
244
  re_pattern = re_pattern.replace(f"{{{t_r}}}", f"(?P={t_r})")
216
-
245
+ # Ops on glob_pattern
246
+ # Iteratively replace replacements with wildcard i.e., *
247
+ # for glob_pattern
217
248
  for t_r in t_replacements:
218
249
  glob_pattern = glob_pattern.replace(f"{{{t_r}}}", "*")
250
+
219
251
  return re_pattern, glob_pattern, t_replacements
220
252
 
221
253
  def _replace_patterns_glob(self, element: Dict, pattern: str) -> str:
@@ -244,8 +276,70 @@ class PatternDataGrabber(BaseDataGrabber):
244
276
  f"The element keys must be {self.replacements}, "
245
277
  f"element has {list(element.keys())}."
246
278
  )
279
+ # Remove negated unix glob pattern i.e., [!...]
280
+ pattern = re.sub(r"\[!.?\]", "", pattern)
281
+ # Remove enclosing square brackets from unix glob pattern i.e., [...]
282
+ pattern = re.sub(r"\[|\]", "", pattern)
247
283
  return pattern.format(**element)
248
284
 
285
+ def _get_path_from_patterns(
286
+ self, element: Dict, pattern: str, data_type: str
287
+ ) -> Path:
288
+ """Get path from resolved patterns.
289
+
290
+ Parameters
291
+ ----------
292
+ element : dict
293
+ The element to be used in the replacement.
294
+ pattern : str
295
+ The pattern to be replaced.
296
+ data_type : str
297
+ The data type of the pattern.
298
+
299
+ Returns
300
+ -------
301
+ pathlib.Path
302
+ The path for the resolved pattern.
303
+
304
+ Raises
305
+ ------
306
+ RuntimeError
307
+ If more than one file matches for a data type's pattern or
308
+ if no file matches for a data type's pattern or
309
+ if file cannot be accessed for an element.
310
+
311
+ """
312
+ # Replace element in the pattern for globbing
313
+ resolved_pattern = self._replace_patterns_glob(element, pattern)
314
+ # Resolve path for wildcard
315
+ if "*" in resolved_pattern:
316
+ t_matches = list(self.datadir.absolute().glob(resolved_pattern))
317
+ # Multiple matches
318
+ if len(t_matches) > 1:
319
+ raise_error(
320
+ f"More than one file matches for {element} / {data_type}:"
321
+ f" {t_matches}",
322
+ klass=RuntimeError,
323
+ )
324
+ # No matches
325
+ elif len(t_matches) == 0:
326
+ raise_error(
327
+ f"No file matches for {element} / {data_type}",
328
+ klass=RuntimeError,
329
+ )
330
+ path = t_matches[0]
331
+ else:
332
+ path = self.datadir / resolved_pattern
333
+ if not self.skip_file_check:
334
+ if not path.exists() and not path.is_symlink():
335
+ raise_error(
336
+ f"Cannot access {data_type} for {element}: "
337
+ f"File {path} does not exist",
338
+ klass=RuntimeError,
339
+ )
340
+
341
+ return path
342
+
249
343
  def get_element_keys(self) -> List[str]:
250
344
  """Get element keys.
251
345
 
@@ -279,47 +373,49 @@ class PatternDataGrabber(BaseDataGrabber):
279
373
  Dictionary of dictionaries for each type of data required for the
280
374
  specified element.
281
375
 
282
- Raises
283
- ------
284
- RuntimeError
285
- If more than one file matches for a data type's pattern or
286
- if no file matches for a data type's pattern or
287
- if file cannot be accessed for an element.
288
-
289
376
  """
290
377
  out = {}
291
378
  for t_type in self.types:
379
+ # Data type dictionary
292
380
  t_pattern = self.patterns[t_type]
293
- t_replace = self._replace_patterns_glob(
294
- element, t_pattern["pattern"]
295
- )
296
- if "*" in t_replace:
297
- t_matches = list(self.datadir.absolute().glob(t_replace))
298
- if len(t_matches) > 1:
299
- raise_error(
300
- f"More than one file matches for {element} / {t_type}:"
301
- f" {t_matches}",
302
- klass=RuntimeError,
381
+ # Copy data type dictionary in output
382
+ out[t_type] = deepcopy(t_pattern)
383
+ # Iterate to check for nested "types" like mask
384
+ for k, v in t_pattern.items():
385
+ # Resolve pattern for base data type
386
+ if k == "pattern":
387
+ logger.info(f"Resolving path from pattern for {t_type}")
388
+ # Resolve pattern
389
+ base_data_type_pattern_path = self._get_path_from_patterns(
390
+ element=element,
391
+ pattern=v,
392
+ data_type=t_type,
303
393
  )
304
- elif len(t_matches) == 0:
305
- raise_error(
306
- f"No file matches for {element} / {t_type}",
307
- klass=RuntimeError,
394
+ # Remove pattern key
395
+ out[t_type].pop("pattern")
396
+ # Add path key
397
+ out[t_type].update({"path": base_data_type_pattern_path})
398
+ # Resolve pattern for nested data type
399
+ if isinstance(v, dict) and "pattern" in v:
400
+ # Set nested type key for easier access
401
+ t_nested_type = f"{t_type}.{k}"
402
+ logger.info(
403
+ f"Resolving path from pattern for {t_nested_type}"
308
404
  )
309
- t_out = t_matches[0]
310
- else:
311
- t_out = self.datadir / t_replace
312
- if not self.skip_file_check:
313
- if not t_out.exists() and not t_out.is_symlink():
314
- raise_error(
315
- f"Cannot access {t_type} for {element}: "
316
- f"File {t_out} does not exist",
317
- klass=RuntimeError,
405
+ # Resolve pattern
406
+ nested_data_type_pattern_path = (
407
+ self._get_path_from_patterns(
408
+ element=element,
409
+ pattern=v["pattern"],
410
+ data_type=t_nested_type,
318
411
  )
319
- # Update path for the element
320
- out[t_type] = t_pattern.copy() # copy data type dictionary
321
- out[t_type].pop("pattern") # remove pattern key
322
- out[t_type].update({"path": t_out}) # add path key
412
+ )
413
+ # Remove pattern key
414
+ out[t_type][k].pop("pattern")
415
+ # Add path key
416
+ out[t_type][k].update(
417
+ {"path": nested_data_type_pattern_path}
418
+ )
323
419
 
324
420
  return out
325
421
 
@@ -351,14 +447,26 @@ class PatternDataGrabber(BaseDataGrabber):
351
447
  for t_idx in reversed(order):
352
448
  t_type = self.types[t_idx]
353
449
  types_element = set()
354
- # Get the pattern
450
+
451
+ # Get the pattern dict
355
452
  t_pattern = self.patterns[t_type]
453
+ # Conditional fetch of base pattern for getting elements
454
+ pattern = None
455
+ # Try for data type pattern
456
+ pattern = t_pattern.get("pattern")
457
+ # Try for nested data type pattern
458
+ if pattern is None and self.partial_pattern_ok:
459
+ for v in t_pattern.values():
460
+ if isinstance(v, dict) and "pattern" in v:
461
+ pattern = v["pattern"]
462
+ break
463
+
356
464
  # Replace the pattern
357
465
  (
358
466
  re_pattern,
359
467
  glob_pattern,
360
468
  t_replacements,
361
- ) = self._replace_patterns_regex(t_pattern["pattern"])
469
+ ) = self._replace_patterns_regex(pattern)
362
470
  for fname in self.datadir.glob(glob_pattern):
363
471
  suffix = fname.relative_to(self.datadir).as_posix()
364
472
  m = re.match(re_pattern, suffix)
@@ -12,6 +12,9 @@ from .datalad_base import DataladDataGrabber
12
12
  from .pattern import PatternDataGrabber
13
13
 
14
14
 
15
+ __all__ = ["PatternDataladDataGrabber"]
16
+
17
+
15
18
  @register_datagrabber
16
19
  class PatternDataladDataGrabber(DataladDataGrabber, PatternDataGrabber):
17
20
  """Concrete implementation for pattern and datalad based data fetching.
@@ -32,7 +35,12 @@ class PatternDataladDataGrabber(DataladDataGrabber, PatternDataGrabber):
32
35
 
33
36
  {
34
37
  "mandatory": ["pattern", "space"],
35
- "optional": []
38
+ "optional": {
39
+ "mask": {
40
+ "mandatory": ["pattern", "space"],
41
+ "optional": []
42
+ }
43
+ }
36
44
  }
37
45
 
38
46
  * ``"T2w"`` :
@@ -41,7 +49,12 @@ class PatternDataladDataGrabber(DataladDataGrabber, PatternDataGrabber):
41
49
 
42
50
  {
43
51
  "mandatory": ["pattern", "space"],
44
- "optional": []
52
+ "optional": {
53
+ "mask": {
54
+ "mandatory": ["pattern", "space"],
55
+ "optional": []
56
+ }
57
+ }
45
58
  }
46
59
 
47
60
  * ``"BOLD"`` :
@@ -50,7 +63,16 @@ class PatternDataladDataGrabber(DataladDataGrabber, PatternDataGrabber):
50
63
 
51
64
  {
52
65
  "mandatory": ["pattern", "space"],
53
- "optional": ["mask_item"]
66
+ "optional": {
67
+ "mask": {
68
+ "mandatory": ["pattern", "space"],
69
+ "optional": []
70
+ }
71
+ "confounds": {
72
+ "mandatory": ["pattern", "format"],
73
+ "optional": []
74
+ }
75
+ }
54
76
  }
55
77
 
56
78
  * ``"Warp"`` :
@@ -62,15 +84,6 @@ class PatternDataladDataGrabber(DataladDataGrabber, PatternDataGrabber):
62
84
  "optional": []
63
85
  }
64
86
 
65
- * ``"BOLD_confounds"`` :
66
-
67
- .. code-block:: none
68
-
69
- {
70
- "mandatory": ["pattern", "format"],
71
- "optional": []
72
- }
73
-
74
87
  * ``"VBM_GM"`` :
75
88
 
76
89
  .. code-block:: none