junifer 0.0.5.dev240__py3-none-any.whl → 0.0.6__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 (279) hide show
  1. junifer/__init__.py +2 -31
  2. junifer/__init__.pyi +37 -0
  3. junifer/_version.py +9 -4
  4. junifer/api/__init__.py +3 -5
  5. junifer/api/__init__.pyi +4 -0
  6. junifer/api/decorators.py +14 -19
  7. junifer/api/functions.py +165 -109
  8. junifer/api/py.typed +0 -0
  9. junifer/api/queue_context/__init__.py +2 -4
  10. junifer/api/queue_context/__init__.pyi +5 -0
  11. junifer/api/queue_context/gnu_parallel_local_adapter.py +22 -6
  12. junifer/api/queue_context/htcondor_adapter.py +23 -6
  13. junifer/api/queue_context/py.typed +0 -0
  14. junifer/api/queue_context/tests/test_gnu_parallel_local_adapter.py +3 -3
  15. junifer/api/queue_context/tests/test_htcondor_adapter.py +3 -3
  16. junifer/api/tests/test_functions.py +168 -74
  17. junifer/cli/__init__.py +24 -0
  18. junifer/cli/__init__.pyi +3 -0
  19. junifer/{api → cli}/cli.py +141 -125
  20. junifer/cli/parser.py +235 -0
  21. junifer/cli/py.typed +0 -0
  22. junifer/{api → cli}/tests/test_cli.py +8 -8
  23. junifer/{api/tests/test_api_utils.py → cli/tests/test_cli_utils.py} +5 -4
  24. junifer/{api → cli}/tests/test_parser.py +2 -2
  25. junifer/{api → cli}/utils.py +6 -16
  26. junifer/configs/juseless/__init__.py +2 -2
  27. junifer/configs/juseless/__init__.pyi +3 -0
  28. junifer/configs/juseless/datagrabbers/__init__.py +2 -12
  29. junifer/configs/juseless/datagrabbers/__init__.pyi +13 -0
  30. junifer/configs/juseless/datagrabbers/ixi_vbm.py +2 -2
  31. junifer/configs/juseless/datagrabbers/py.typed +0 -0
  32. junifer/configs/juseless/datagrabbers/tests/test_ucla.py +2 -2
  33. junifer/configs/juseless/datagrabbers/ucla.py +4 -4
  34. junifer/configs/juseless/py.typed +0 -0
  35. junifer/conftest.py +25 -0
  36. junifer/data/__init__.py +2 -42
  37. junifer/data/__init__.pyi +29 -0
  38. junifer/data/_dispatch.py +248 -0
  39. junifer/data/coordinates/__init__.py +9 -0
  40. junifer/data/coordinates/__init__.pyi +5 -0
  41. junifer/data/coordinates/_ants_coordinates_warper.py +104 -0
  42. junifer/data/coordinates/_coordinates.py +385 -0
  43. junifer/data/coordinates/_fsl_coordinates_warper.py +81 -0
  44. junifer/data/{tests → coordinates/tests}/test_coordinates.py +26 -33
  45. junifer/data/masks/__init__.py +9 -0
  46. junifer/data/masks/__init__.pyi +6 -0
  47. junifer/data/masks/_ants_mask_warper.py +177 -0
  48. junifer/data/masks/_fsl_mask_warper.py +106 -0
  49. junifer/data/masks/_masks.py +802 -0
  50. junifer/data/{tests → masks/tests}/test_masks.py +67 -63
  51. junifer/data/parcellations/__init__.py +9 -0
  52. junifer/data/parcellations/__init__.pyi +6 -0
  53. junifer/data/parcellations/_ants_parcellation_warper.py +166 -0
  54. junifer/data/parcellations/_fsl_parcellation_warper.py +89 -0
  55. junifer/data/parcellations/_parcellations.py +1388 -0
  56. junifer/data/{tests → parcellations/tests}/test_parcellations.py +165 -295
  57. junifer/data/pipeline_data_registry_base.py +76 -0
  58. junifer/data/py.typed +0 -0
  59. junifer/data/template_spaces.py +44 -79
  60. junifer/data/tests/test_data_utils.py +1 -2
  61. junifer/data/tests/test_template_spaces.py +8 -4
  62. junifer/data/utils.py +109 -4
  63. junifer/datagrabber/__init__.py +2 -26
  64. junifer/datagrabber/__init__.pyi +27 -0
  65. junifer/datagrabber/aomic/__init__.py +2 -4
  66. junifer/datagrabber/aomic/__init__.pyi +5 -0
  67. junifer/datagrabber/aomic/id1000.py +81 -52
  68. junifer/datagrabber/aomic/piop1.py +83 -55
  69. junifer/datagrabber/aomic/piop2.py +85 -56
  70. junifer/datagrabber/aomic/py.typed +0 -0
  71. junifer/datagrabber/aomic/tests/test_id1000.py +19 -12
  72. junifer/datagrabber/aomic/tests/test_piop1.py +52 -18
  73. junifer/datagrabber/aomic/tests/test_piop2.py +50 -17
  74. junifer/datagrabber/base.py +22 -18
  75. junifer/datagrabber/datalad_base.py +71 -34
  76. junifer/datagrabber/dmcc13_benchmark.py +31 -18
  77. junifer/datagrabber/hcp1200/__init__.py +2 -3
  78. junifer/datagrabber/hcp1200/__init__.pyi +4 -0
  79. junifer/datagrabber/hcp1200/datalad_hcp1200.py +3 -3
  80. junifer/datagrabber/hcp1200/hcp1200.py +26 -15
  81. junifer/datagrabber/hcp1200/py.typed +0 -0
  82. junifer/datagrabber/hcp1200/tests/test_hcp1200.py +8 -2
  83. junifer/datagrabber/multiple.py +14 -9
  84. junifer/datagrabber/pattern.py +132 -96
  85. junifer/datagrabber/pattern_validation_mixin.py +206 -94
  86. junifer/datagrabber/py.typed +0 -0
  87. junifer/datagrabber/tests/test_datalad_base.py +27 -12
  88. junifer/datagrabber/tests/test_dmcc13_benchmark.py +28 -11
  89. junifer/datagrabber/tests/test_multiple.py +48 -2
  90. junifer/datagrabber/tests/test_pattern_datalad.py +1 -1
  91. junifer/datagrabber/tests/test_pattern_validation_mixin.py +6 -6
  92. junifer/datareader/__init__.py +2 -2
  93. junifer/datareader/__init__.pyi +3 -0
  94. junifer/datareader/default.py +6 -6
  95. junifer/datareader/py.typed +0 -0
  96. junifer/external/nilearn/__init__.py +2 -3
  97. junifer/external/nilearn/__init__.pyi +4 -0
  98. junifer/external/nilearn/junifer_connectivity_measure.py +25 -17
  99. junifer/external/nilearn/junifer_nifti_spheres_masker.py +4 -4
  100. junifer/external/nilearn/py.typed +0 -0
  101. junifer/external/nilearn/tests/test_junifer_connectivity_measure.py +17 -16
  102. junifer/external/nilearn/tests/test_junifer_nifti_spheres_masker.py +2 -3
  103. junifer/markers/__init__.py +2 -38
  104. junifer/markers/__init__.pyi +37 -0
  105. junifer/markers/base.py +11 -14
  106. junifer/markers/brainprint.py +12 -14
  107. junifer/markers/complexity/__init__.py +2 -18
  108. junifer/markers/complexity/__init__.pyi +17 -0
  109. junifer/markers/complexity/complexity_base.py +9 -11
  110. junifer/markers/complexity/hurst_exponent.py +7 -7
  111. junifer/markers/complexity/multiscale_entropy_auc.py +7 -7
  112. junifer/markers/complexity/perm_entropy.py +7 -7
  113. junifer/markers/complexity/py.typed +0 -0
  114. junifer/markers/complexity/range_entropy.py +7 -7
  115. junifer/markers/complexity/range_entropy_auc.py +7 -7
  116. junifer/markers/complexity/sample_entropy.py +7 -7
  117. junifer/markers/complexity/tests/test_complexity_base.py +1 -1
  118. junifer/markers/complexity/tests/test_hurst_exponent.py +5 -5
  119. junifer/markers/complexity/tests/test_multiscale_entropy_auc.py +5 -5
  120. junifer/markers/complexity/tests/test_perm_entropy.py +5 -5
  121. junifer/markers/complexity/tests/test_range_entropy.py +5 -5
  122. junifer/markers/complexity/tests/test_range_entropy_auc.py +5 -5
  123. junifer/markers/complexity/tests/test_sample_entropy.py +5 -5
  124. junifer/markers/complexity/tests/test_weighted_perm_entropy.py +5 -5
  125. junifer/markers/complexity/weighted_perm_entropy.py +7 -7
  126. junifer/markers/ets_rss.py +12 -11
  127. junifer/markers/falff/__init__.py +2 -3
  128. junifer/markers/falff/__init__.pyi +4 -0
  129. junifer/markers/falff/_afni_falff.py +38 -45
  130. junifer/markers/falff/_junifer_falff.py +16 -19
  131. junifer/markers/falff/falff_base.py +7 -11
  132. junifer/markers/falff/falff_parcels.py +9 -9
  133. junifer/markers/falff/falff_spheres.py +8 -8
  134. junifer/markers/falff/py.typed +0 -0
  135. junifer/markers/falff/tests/test_falff_spheres.py +3 -1
  136. junifer/markers/functional_connectivity/__init__.py +2 -12
  137. junifer/markers/functional_connectivity/__init__.pyi +13 -0
  138. junifer/markers/functional_connectivity/crossparcellation_functional_connectivity.py +9 -8
  139. junifer/markers/functional_connectivity/edge_functional_connectivity_parcels.py +8 -8
  140. junifer/markers/functional_connectivity/edge_functional_connectivity_spheres.py +7 -7
  141. junifer/markers/functional_connectivity/functional_connectivity_base.py +13 -12
  142. junifer/markers/functional_connectivity/functional_connectivity_parcels.py +8 -8
  143. junifer/markers/functional_connectivity/functional_connectivity_spheres.py +7 -7
  144. junifer/markers/functional_connectivity/py.typed +0 -0
  145. junifer/markers/functional_connectivity/tests/test_edge_functional_connectivity_parcels.py +1 -2
  146. junifer/markers/functional_connectivity/tests/test_edge_functional_connectivity_spheres.py +1 -2
  147. junifer/markers/functional_connectivity/tests/test_functional_connectivity_parcels.py +6 -6
  148. junifer/markers/functional_connectivity/tests/test_functional_connectivity_spheres.py +5 -5
  149. junifer/markers/parcel_aggregation.py +22 -17
  150. junifer/markers/py.typed +0 -0
  151. junifer/markers/reho/__init__.py +2 -3
  152. junifer/markers/reho/__init__.pyi +4 -0
  153. junifer/markers/reho/_afni_reho.py +29 -35
  154. junifer/markers/reho/_junifer_reho.py +13 -14
  155. junifer/markers/reho/py.typed +0 -0
  156. junifer/markers/reho/reho_base.py +7 -11
  157. junifer/markers/reho/reho_parcels.py +10 -10
  158. junifer/markers/reho/reho_spheres.py +9 -9
  159. junifer/markers/sphere_aggregation.py +22 -17
  160. junifer/markers/temporal_snr/__init__.py +2 -3
  161. junifer/markers/temporal_snr/__init__.pyi +4 -0
  162. junifer/markers/temporal_snr/py.typed +0 -0
  163. junifer/markers/temporal_snr/temporal_snr_base.py +11 -10
  164. junifer/markers/temporal_snr/temporal_snr_parcels.py +8 -8
  165. junifer/markers/temporal_snr/temporal_snr_spheres.py +7 -7
  166. junifer/markers/tests/test_ets_rss.py +3 -3
  167. junifer/markers/tests/test_parcel_aggregation.py +24 -24
  168. junifer/markers/tests/test_sphere_aggregation.py +6 -6
  169. junifer/markers/utils.py +3 -3
  170. junifer/onthefly/__init__.py +2 -1
  171. junifer/onthefly/_brainprint.py +138 -0
  172. junifer/onthefly/read_transform.py +5 -8
  173. junifer/pipeline/__init__.py +2 -10
  174. junifer/pipeline/__init__.pyi +13 -0
  175. junifer/{markers/collection.py → pipeline/marker_collection.py} +8 -14
  176. junifer/pipeline/pipeline_component_registry.py +294 -0
  177. junifer/pipeline/pipeline_step_mixin.py +15 -11
  178. junifer/pipeline/py.typed +0 -0
  179. junifer/{markers/tests/test_collection.py → pipeline/tests/test_marker_collection.py} +2 -3
  180. junifer/pipeline/tests/test_pipeline_component_registry.py +200 -0
  181. junifer/pipeline/tests/test_pipeline_step_mixin.py +36 -37
  182. junifer/pipeline/tests/test_update_meta_mixin.py +4 -4
  183. junifer/pipeline/tests/test_workdir_manager.py +43 -0
  184. junifer/pipeline/update_meta_mixin.py +21 -17
  185. junifer/pipeline/utils.py +6 -6
  186. junifer/pipeline/workdir_manager.py +19 -5
  187. junifer/preprocess/__init__.py +2 -10
  188. junifer/preprocess/__init__.pyi +11 -0
  189. junifer/preprocess/base.py +10 -10
  190. junifer/preprocess/confounds/__init__.py +2 -2
  191. junifer/preprocess/confounds/__init__.pyi +3 -0
  192. junifer/preprocess/confounds/fmriprep_confound_remover.py +243 -64
  193. junifer/preprocess/confounds/py.typed +0 -0
  194. junifer/preprocess/confounds/tests/test_fmriprep_confound_remover.py +121 -14
  195. junifer/preprocess/py.typed +0 -0
  196. junifer/preprocess/smoothing/__init__.py +2 -2
  197. junifer/preprocess/smoothing/__init__.pyi +3 -0
  198. junifer/preprocess/smoothing/_afni_smoothing.py +40 -40
  199. junifer/preprocess/smoothing/_fsl_smoothing.py +22 -32
  200. junifer/preprocess/smoothing/_nilearn_smoothing.py +35 -14
  201. junifer/preprocess/smoothing/py.typed +0 -0
  202. junifer/preprocess/smoothing/smoothing.py +11 -13
  203. junifer/preprocess/warping/__init__.py +2 -2
  204. junifer/preprocess/warping/__init__.pyi +3 -0
  205. junifer/preprocess/warping/_ants_warper.py +136 -32
  206. junifer/preprocess/warping/_fsl_warper.py +73 -22
  207. junifer/preprocess/warping/py.typed +0 -0
  208. junifer/preprocess/warping/space_warper.py +39 -11
  209. junifer/preprocess/warping/tests/test_space_warper.py +5 -9
  210. junifer/py.typed +0 -0
  211. junifer/stats.py +5 -5
  212. junifer/storage/__init__.py +2 -10
  213. junifer/storage/__init__.pyi +11 -0
  214. junifer/storage/base.py +47 -13
  215. junifer/storage/hdf5.py +95 -33
  216. junifer/storage/pandas_base.py +12 -11
  217. junifer/storage/py.typed +0 -0
  218. junifer/storage/sqlite.py +11 -11
  219. junifer/storage/tests/test_hdf5.py +86 -4
  220. junifer/storage/tests/test_sqlite.py +2 -2
  221. junifer/storage/tests/test_storage_base.py +5 -2
  222. junifer/storage/tests/test_utils.py +33 -7
  223. junifer/storage/utils.py +95 -9
  224. junifer/testing/__init__.py +2 -3
  225. junifer/testing/__init__.pyi +4 -0
  226. junifer/testing/datagrabbers.py +10 -11
  227. junifer/testing/py.typed +0 -0
  228. junifer/testing/registry.py +4 -7
  229. junifer/testing/tests/test_testing_registry.py +9 -17
  230. junifer/tests/test_stats.py +2 -2
  231. junifer/typing/__init__.py +9 -0
  232. junifer/typing/__init__.pyi +31 -0
  233. junifer/typing/_typing.py +68 -0
  234. junifer/utils/__init__.py +2 -12
  235. junifer/utils/__init__.pyi +18 -0
  236. junifer/utils/_config.py +110 -0
  237. junifer/utils/_yaml.py +16 -0
  238. junifer/utils/helpers.py +6 -6
  239. junifer/utils/logging.py +117 -8
  240. junifer/utils/py.typed +0 -0
  241. junifer/{pipeline → utils}/singleton.py +19 -14
  242. junifer/utils/tests/test_config.py +59 -0
  243. {junifer-0.0.5.dev240.dist-info → junifer-0.0.6.dist-info}/METADATA +43 -38
  244. junifer-0.0.6.dist-info/RECORD +350 -0
  245. {junifer-0.0.5.dev240.dist-info → junifer-0.0.6.dist-info}/WHEEL +1 -1
  246. junifer-0.0.6.dist-info/entry_points.txt +2 -0
  247. junifer/api/parser.py +0 -118
  248. junifer/data/coordinates.py +0 -408
  249. junifer/data/masks.py +0 -670
  250. junifer/data/parcellations.py +0 -1828
  251. junifer/pipeline/registry.py +0 -177
  252. junifer/pipeline/tests/test_registry.py +0 -150
  253. junifer-0.0.5.dev240.dist-info/RECORD +0 -275
  254. junifer-0.0.5.dev240.dist-info/entry_points.txt +0 -2
  255. /junifer/{api → cli}/tests/data/gmd_mean.yaml +0 -0
  256. /junifer/{api → cli}/tests/data/gmd_mean_htcondor.yaml +0 -0
  257. /junifer/{api → cli}/tests/data/partly_cloudy_agg_mean_tian.yml +0 -0
  258. /junifer/data/{VOIs → coordinates/VOIs}/meta/AutobiographicalMemory_VOIs.txt +0 -0
  259. /junifer/data/{VOIs → coordinates/VOIs}/meta/CogAC_VOIs.txt +0 -0
  260. /junifer/data/{VOIs → coordinates/VOIs}/meta/CogAR_VOIs.txt +0 -0
  261. /junifer/data/{VOIs → coordinates/VOIs}/meta/DMNBuckner_VOIs.txt +0 -0
  262. /junifer/data/{VOIs → coordinates/VOIs}/meta/Dosenbach2010_MNI_VOIs.txt +0 -0
  263. /junifer/data/{VOIs → coordinates/VOIs}/meta/Empathy_VOIs.txt +0 -0
  264. /junifer/data/{VOIs → coordinates/VOIs}/meta/Motor_VOIs.txt +0 -0
  265. /junifer/data/{VOIs → coordinates/VOIs}/meta/MultiTask_VOIs.txt +0 -0
  266. /junifer/data/{VOIs → coordinates/VOIs}/meta/PhysioStress_VOIs.txt +0 -0
  267. /junifer/data/{VOIs → coordinates/VOIs}/meta/Power2011_MNI_VOIs.txt +0 -0
  268. /junifer/data/{VOIs → coordinates/VOIs}/meta/Power2013_MNI_VOIs.tsv +0 -0
  269. /junifer/data/{VOIs → coordinates/VOIs}/meta/Rew_VOIs.txt +0 -0
  270. /junifer/data/{VOIs → coordinates/VOIs}/meta/Somatosensory_VOIs.txt +0 -0
  271. /junifer/data/{VOIs → coordinates/VOIs}/meta/ToM_VOIs.txt +0 -0
  272. /junifer/data/{VOIs → coordinates/VOIs}/meta/VigAtt_VOIs.txt +0 -0
  273. /junifer/data/{VOIs → coordinates/VOIs}/meta/WM_VOIs.txt +0 -0
  274. /junifer/data/{VOIs → coordinates/VOIs}/meta/eMDN_VOIs.txt +0 -0
  275. /junifer/data/{VOIs → coordinates/VOIs}/meta/eSAD_VOIs.txt +0 -0
  276. /junifer/data/{VOIs → coordinates/VOIs}/meta/extDMN_VOIs.txt +0 -0
  277. {junifer-0.0.5.dev240.dist-info → junifer-0.0.6.dist-info/licenses}/AUTHORS.rst +0 -0
  278. {junifer-0.0.5.dev240.dist-info → junifer-0.0.6.dist-info/licenses}/LICENSE.md +0 -0
  279. {junifer-0.0.5.dev240.dist-info → junifer-0.0.6.dist-info}/top_level.txt +0 -0
@@ -8,21 +8,22 @@
8
8
  from typing import (
9
9
  Any,
10
10
  ClassVar,
11
- Dict,
12
- List,
13
11
  Optional,
14
- Set,
15
- Tuple,
16
12
  Union,
17
13
  )
18
14
 
15
+ import nibabel as nib
19
16
  import numpy as np
20
17
  import pandas as pd
21
18
  from nilearn import image as nimg
22
19
  from nilearn._utils.niimg_conversions import check_niimg_4d
20
+ from nilearn.interfaces.fmriprep.load_confounds_components import _load_scrub
21
+ from nilearn.interfaces.fmriprep.load_confounds_utils import prepare_output
23
22
 
24
23
  from ...api.decorators import register_preprocessor
25
- from ...data import get_mask
24
+ from ...data import get_data
25
+ from ...pipeline import WorkDirManager
26
+ from ...typing import Dependencies
26
27
  from ...utils import logger, raise_error
27
28
  from ..base import BasePreprocessor
28
29
 
@@ -93,6 +94,9 @@ FMRIPREP_VALID_NAMES = [
93
94
  for t_list in x.values()
94
95
  for elem in t_list
95
96
  ]
97
+ # NOTE: Check with @fraimondo about the spike mapping intent
98
+ # Add spike_name to FMRIPREP_VALID_NAMES
99
+ FMRIPREP_VALID_NAMES.append("framewise_displacement")
96
100
 
97
101
 
98
102
  @register_preprocessor
@@ -108,13 +112,15 @@ class fMRIPrepConfoundRemover(BasePreprocessor):
108
112
  ----------
109
113
  strategy : dict, optional
110
114
  The strategy to use for each component. If None, will use the *full*
111
- strategy for all components (default None).
115
+ strategy for all components except ``"scrubbing"`` which will be set
116
+ to False (default None).
112
117
  The keys of the dictionary should correspond to names of noise
113
118
  components to include:
114
119
 
115
120
  * ``motion``
116
121
  * ``wm_csf``
117
122
  * ``global_signal``
123
+ * ``scrubbing``
118
124
 
119
125
  The values of dictionary should correspond to types of confounds
120
126
  extracted from each signal:
@@ -124,10 +130,29 @@ class fMRIPrepConfoundRemover(BasePreprocessor):
124
130
  * ``derivatives`` : signal + derivatives
125
131
  * ``full`` : signal + deriv. + quadratic terms + power2 deriv.
126
132
 
133
+ except ``scrubbing`` which needs to be bool.
127
134
  spike : float, optional
128
135
  If None, no spike regressor is added. If spike is a float, it will
129
136
  add a spike regressor for every point at which framewise displacement
130
137
  exceeds the specified float (default None).
138
+ scrub : int, optional
139
+ After accounting for time frames with excessive motion, further remove
140
+ segments shorter than the given number. When the value is 0, remove
141
+ time frames based on excessive framewise displacement and DVARS only.
142
+ If None and no ``"scrubbing"`` in ``strategy``, no scrubbing is
143
+ performed, else the default value is 0. The default value is referred
144
+ as full scrubbing (default None).
145
+ fd_threshold : float, optional
146
+ Framewise displacement threshold for scrub in mm. If None no
147
+ ``"scrubbing"`` in ``strategy``, no scrubbing is performed, else the
148
+ default value is 0.5 (default None).
149
+ std_dvars_threshold : float, optional
150
+ Standardized DVARS threshold for scrub. DVARs is defined as root mean
151
+ squared intensity difference of volume N to volume N+1. D referring to
152
+ temporal derivative of timecourses, VARS referring to root mean squared
153
+ variance over voxels. If None and no ``"scrubbing"`` in ``strategy``,
154
+ no scrubbing is performed, else the default value is 1.5
155
+ (default None).
131
156
  detrend : bool, optional
132
157
  If True, detrending will be applied on timeseries, before confound
133
158
  removal (default True).
@@ -149,18 +174,21 @@ class fMRIPrepConfoundRemover(BasePreprocessor):
149
174
 
150
175
  """
151
176
 
152
- _DEPENDENCIES: ClassVar[Set[str]] = {"numpy", "nilearn"}
177
+ _DEPENDENCIES: ClassVar[Dependencies] = {"numpy", "nilearn"}
153
178
 
154
179
  def __init__(
155
180
  self,
156
- strategy: Optional[Dict[str, str]] = None,
181
+ strategy: Optional[dict[str, Union[str, bool]]] = None,
157
182
  spike: Optional[float] = None,
183
+ scrub: Optional[int] = None,
184
+ fd_threshold: Optional[float] = None,
185
+ std_dvars_threshold: Optional[float] = None,
158
186
  detrend: bool = True,
159
187
  standardize: bool = True,
160
188
  low_pass: Optional[float] = None,
161
189
  high_pass: Optional[float] = None,
162
190
  t_r: Optional[float] = None,
163
- masks: Union[str, Dict, List[Union[Dict, str]], None] = None,
191
+ masks: Union[str, dict, list[Union[dict, str]], None] = None,
164
192
  ) -> None:
165
193
  """Initialize the class."""
166
194
  if strategy is None:
@@ -168,9 +196,13 @@ class fMRIPrepConfoundRemover(BasePreprocessor):
168
196
  "motion": "full",
169
197
  "wm_csf": "full",
170
198
  "global_signal": "full",
199
+ "scrubbing": False,
171
200
  }
172
201
  self.strategy = strategy
173
202
  self.spike = spike
203
+ self.scrub = scrub
204
+ self.fd_threshold = fd_threshold
205
+ self.std_dvars_threshold = std_dvars_threshold
174
206
  self.detrend = detrend
175
207
  self.standardize = standardize
176
208
  self.low_pass = low_pass
@@ -178,13 +210,22 @@ class fMRIPrepConfoundRemover(BasePreprocessor):
178
210
  self.t_r = t_r
179
211
  self.masks = masks
180
212
 
181
- self._valid_components = ["motion", "wm_csf", "global_signal"]
213
+ self._valid_components = [
214
+ "motion",
215
+ "wm_csf",
216
+ "global_signal",
217
+ "scrubbing",
218
+ ]
182
219
  self._valid_confounds = ["basic", "power2", "derivatives", "full"]
183
220
 
184
221
  if any(not isinstance(k, str) for k in strategy.keys()):
185
222
  raise_error("Strategy keys must be strings", ValueError)
186
223
 
187
- if any(not isinstance(v, str) for v in strategy.values()):
224
+ if any(
225
+ not isinstance(v, str)
226
+ for k, v in strategy.items()
227
+ if k != "scrubbing"
228
+ ):
188
229
  raise_error("Strategy values must be strings", ValueError)
189
230
 
190
231
  if any(x not in self._valid_components for x in strategy.keys()):
@@ -197,7 +238,11 @@ class fMRIPrepConfoundRemover(BasePreprocessor):
197
238
  klass=ValueError,
198
239
  )
199
240
 
200
- if any(x not in self._valid_confounds for x in strategy.values()):
241
+ if any(
242
+ v not in self._valid_confounds
243
+ for k, v in strategy.items()
244
+ if k != "scrubbing"
245
+ ):
201
246
  raise_error(
202
247
  msg=f"Invalid confound types {list(strategy.values())}. "
203
248
  f"Valid confound types are {self._valid_confounds}.\n"
@@ -208,7 +253,7 @@ class fMRIPrepConfoundRemover(BasePreprocessor):
208
253
  )
209
254
  super().__init__(on="BOLD", required_data_types=["BOLD"])
210
255
 
211
- def get_valid_inputs(self) -> List[str]:
256
+ def get_valid_inputs(self) -> list[str]:
212
257
  """Get valid data types for input.
213
258
 
214
259
  Returns
@@ -237,7 +282,7 @@ class fMRIPrepConfoundRemover(BasePreprocessor):
237
282
  # Does not add any new keys
238
283
  return input_type
239
284
 
240
- def _map_adhoc_to_fmriprep(self, input: Dict[str, Any]) -> None:
285
+ def _map_adhoc_to_fmriprep(self, input: dict[str, Any]) -> None:
241
286
  """Map the adhoc format to the fmpriprep format spec.
242
287
 
243
288
  Based on the spec, map the column names to match the fmriprep format.
@@ -267,8 +312,8 @@ class fMRIPrepConfoundRemover(BasePreprocessor):
267
312
  confounds_df.rename(columns=confounds_mapping, inplace=True)
268
313
 
269
314
  def _process_fmriprep_spec(
270
- self, input: Dict[str, Any]
271
- ) -> Tuple[List[str], Dict[str, str], Dict[str, str], str]:
315
+ self, input: dict[str, Any]
316
+ ) -> tuple[list[str], dict[str, str], dict[str, str], str]:
272
317
  """Process the fmpriprep format spec from the specified file.
273
318
 
274
319
  Based on the strategy, find the relevant column names in the dataframe,
@@ -300,7 +345,7 @@ class fMRIPrepConfoundRemover(BasePreprocessor):
300
345
 
301
346
  Raises
302
347
  ------
303
- ValueError
348
+ RuntimeError
304
349
  If invalid confounds file is found.
305
350
 
306
351
  """
@@ -313,50 +358,62 @@ class fMRIPrepConfoundRemover(BasePreprocessor):
313
358
  derivatives_to_compute = {} # the dictionary of missing derivatives
314
359
 
315
360
  for t_kind, t_strategy in self.strategy.items():
316
- t_basics = FMRIPREP_BASICS[t_kind]
317
-
318
- if any(x not in available_vars for x in t_basics):
319
- missing = [x for x in t_basics if x not in available_vars]
320
- raise_error(
321
- "Invalid confounds file. Missing basic confounds: "
322
- f"{missing}. "
323
- "Check if this file is really an fmriprep confounds file. "
324
- "You can also modify the confound removal strategy."
325
- )
326
-
327
- to_select.extend(t_basics)
328
-
329
- if t_strategy in ["power2", "full"]:
330
- for x in t_basics:
331
- x_2 = f"{x}_power2"
332
- to_select.append(x_2)
333
- if x_2 not in available_vars:
334
- squares_to_compute[x_2] = x
335
-
336
- if t_strategy in ["derivatives", "full"]:
337
- for x in t_basics:
338
- x_derivative = f"{x}_derivative1"
339
- to_select.append(x_derivative)
340
- if x_derivative not in available_vars:
341
- derivatives_to_compute[x_derivative] = x
342
- if t_strategy == "full":
343
- x_derivative_2 = f"{x_derivative}_power2"
344
- to_select.append(x_derivative_2)
345
- if x_derivative_2 not in available_vars:
346
- squares_to_compute[x_derivative_2] = x_derivative
361
+ if t_kind != "scrubbing":
362
+ t_basics = FMRIPREP_BASICS[t_kind]
363
+
364
+ if any(x not in available_vars for x in t_basics):
365
+ missing = [x for x in t_basics if x not in available_vars]
366
+ raise_error(
367
+ msg=(
368
+ "Invalid confounds file. Missing basic confounds: "
369
+ f"{missing}. "
370
+ "Check if this file is really an fmriprep "
371
+ "confounds file. You can also modify the confound "
372
+ "removal strategy."
373
+ ),
374
+ klass=RuntimeError,
375
+ )
376
+
377
+ to_select.extend(t_basics)
378
+
379
+ if t_strategy in ["power2", "full"]:
380
+ for x in t_basics:
381
+ x_2 = f"{x}_power2"
382
+ to_select.append(x_2)
383
+ if x_2 not in available_vars:
384
+ squares_to_compute[x_2] = x
385
+
386
+ if t_strategy in ["derivatives", "full"]:
387
+ for x in t_basics:
388
+ x_derivative = f"{x}_derivative1"
389
+ to_select.append(x_derivative)
390
+ if x_derivative not in available_vars:
391
+ derivatives_to_compute[x_derivative] = x
392
+ if t_strategy == "full":
393
+ x_derivative_2 = f"{x_derivative}_power2"
394
+ to_select.append(x_derivative_2)
395
+ if x_derivative_2 not in available_vars:
396
+ squares_to_compute[x_derivative_2] = (
397
+ x_derivative
398
+ )
399
+ # Add spike
347
400
  spike_name = "framewise_displacement"
348
401
  if self.spike is not None:
349
402
  if spike_name not in available_vars:
350
403
  raise_error(
351
- "Invalid confounds file. Missing framewise_displacement "
352
- "(spike) confound. "
353
- "Check if this file is really an fmriprep confounds file. "
354
- "You can also deactivate spike (set spike = None)."
404
+ msg=(
405
+ "Invalid confounds file. Missing "
406
+ "framewise_displacement (spike) confound. "
407
+ "Check if this file is really an fmriprep confounds "
408
+ "file. You can also deactivate spike "
409
+ "(set spike = None)."
410
+ ),
411
+ klass=RuntimeError,
355
412
  )
356
413
  out = to_select, squares_to_compute, derivatives_to_compute, spike_name
357
414
  return out
358
415
 
359
- def _pick_confounds(self, input: Dict[str, Any]) -> pd.DataFrame:
416
+ def _pick_confounds(self, input: dict[str, Any]) -> pd.DataFrame:
360
417
  """Select relevant confounds from the specified file.
361
418
 
362
419
  Parameters
@@ -375,14 +432,12 @@ class fMRIPrepConfoundRemover(BasePreprocessor):
375
432
  if confounds_format == "adhoc":
376
433
  self._map_adhoc_to_fmriprep(input)
377
434
 
378
- processed_spec = self._process_fmriprep_spec(input)
379
-
380
435
  (
381
436
  to_select,
382
437
  squares_to_compute,
383
438
  derivatives_to_compute,
384
439
  spike_name,
385
- ) = processed_spec
440
+ ) = self._process_fmriprep_spec(input)
386
441
  # Copy the confounds
387
442
  out_df = input["data"].copy()
388
443
 
@@ -413,9 +468,70 @@ class fMRIPrepConfoundRemover(BasePreprocessor):
413
468
 
414
469
  return out_df
415
470
 
471
+ def _get_scrub_regressors(
472
+ self, confounds_df: pd.DataFrame
473
+ ) -> pd.DataFrame:
474
+ """Get motion outline regressors.
475
+
476
+ Parameters
477
+ ----------
478
+ confounds_df : pandas.DataFrame
479
+ pandas.DataFrame with all confounds.
480
+
481
+ Returns
482
+ -------
483
+ pandas.DataFrame
484
+ pandas.DataFrame of motion outline regressors.
485
+
486
+ Raises
487
+ ------
488
+ RuntimeError
489
+ If ``std_dvars`` and / or ``framewise_displacement`` is not found
490
+ in ``confounds_df``.
491
+
492
+ """
493
+ # Check columns
494
+ if (
495
+ self.std_dvars_threshold is not None
496
+ and "std_dvars" not in confounds_df.columns
497
+ ):
498
+ raise_error(
499
+ msg=(
500
+ "Invalid confounds file. Missing std_dvars "
501
+ "(standardized DVARS) confound. "
502
+ "Check if this file is really an fMRIPrep confounds file. "
503
+ ),
504
+ klass=RuntimeError,
505
+ )
506
+ if (
507
+ self.fd_threshold is not None
508
+ and "framewise_displacement" not in confounds_df.columns
509
+ ):
510
+ raise_error(
511
+ msg=(
512
+ "Invalid confounds file. Missing framewise_displacement "
513
+ "confound. "
514
+ "Check if this file is really an fMRIPrep confounds file. "
515
+ ),
516
+ klass=RuntimeError,
517
+ )
518
+ # Use function from nilearn to not reinvent the wheel
519
+ return _load_scrub(
520
+ confounds_raw=confounds_df,
521
+ scrub=self.scrub if self.scrub is not None else 0,
522
+ fd_threshold=(
523
+ self.fd_threshold if self.fd_threshold is not None else 0.5
524
+ ),
525
+ std_dvars_threshold=(
526
+ self.std_dvars_threshold
527
+ if self.std_dvars_threshold is not None
528
+ else 1.5
529
+ ),
530
+ )
531
+
416
532
  def _validate_data(
417
533
  self,
418
- input: Dict[str, Any],
534
+ input: dict[str, Any],
419
535
  ) -> None:
420
536
  """Validate input data.
421
537
 
@@ -503,9 +619,9 @@ class fMRIPrepConfoundRemover(BasePreprocessor):
503
619
 
504
620
  def preprocess(
505
621
  self,
506
- input: Dict[str, Any],
507
- extra_input: Optional[Dict[str, Any]] = None,
508
- ) -> Tuple[Dict[str, Any], Optional[Dict[str, Dict[str, Any]]]]:
622
+ input: dict[str, Any],
623
+ extra_input: Optional[dict[str, Any]] = None,
624
+ ) -> tuple[dict[str, Any], Optional[dict[str, dict[str, Any]]]]:
509
625
  """Preprocess.
510
626
 
511
627
  Parameters
@@ -539,31 +655,78 @@ class fMRIPrepConfoundRemover(BasePreprocessor):
539
655
  logger.info(
540
656
  f"Read t_r from NIfTI header: {t_r}",
541
657
  )
658
+
659
+ # Create element-specific tempdir for storing generated data
660
+ # and / or mask
661
+ element_tempdir = WorkDirManager().get_element_tempdir(
662
+ prefix="fmriprep_confound_remover"
663
+ )
664
+
542
665
  # Set mask data
543
666
  mask_img = None
544
667
  if self.masks is not None:
668
+ # Generate mask
545
669
  logger.debug(f"Masking with {self.masks}")
546
- mask_img = get_mask(
547
- masks=self.masks, target_data=input, extra_input=extra_input
670
+ mask_img = get_data(
671
+ kind="mask",
672
+ names=self.masks,
673
+ target_data=input,
674
+ extra_input=extra_input,
548
675
  )
549
- # Return the BOLD mask and link it to the BOLD data type dict;
676
+ # Save generated mask for use later
677
+ generated_mask_img_path = element_tempdir / "generated_mask.nii.gz"
678
+ nib.save(mask_img, generated_mask_img_path)
679
+
680
+ # Save BOLD mask and link it to the BOLD data type dict;
550
681
  # this allows to use "inherit" down the pipeline
551
682
  logger.debug("Setting `BOLD.mask`")
552
683
  input.update(
553
684
  {
554
685
  "mask": {
686
+ # Update path to sync with "data"
687
+ "path": generated_mask_img_path,
688
+ # Update data
555
689
  "data": mask_img,
690
+ # Should be in the same space as target data
556
691
  "space": input["space"],
557
692
  }
558
693
  }
559
694
  )
695
+
696
+ signal_clean_kwargs = {}
697
+ # Set up scrubbing mask if needed
698
+ if self.strategy.get("scrubbing", False):
699
+ motion_outline_regressors = self._get_scrub_regressors(
700
+ input["confounds"]["data"]
701
+ )
702
+ # Add regressors to confounds
703
+ confounds_df = pd.concat(
704
+ [confounds_df, motion_outline_regressors], axis=1
705
+ )
706
+ # Get sample mask
707
+ sample_mask, confounds_df = prepare_output(
708
+ confounds=confounds_df, demean=False
709
+ )
710
+ signal_clean_kwargs.update(
711
+ {
712
+ "clean__sample_mask": sample_mask,
713
+ }
714
+ )
560
715
  # Clean image
561
716
  logger.info("Cleaning image using nilearn")
717
+ logger.debug(f"\tstrategy: {self.strategy}")
718
+ logger.debug(f"\tspike: {self.spike}")
719
+ logger.debug(f"\tscrub: {self.scrub}")
720
+ logger.debug(f"\tfd_threshold: {self.fd_threshold}")
721
+ logger.debug(f"\tstd_dvars_threshold: {self.std_dvars_threshold}")
562
722
  logger.debug(f"\tdetrend: {self.detrend}")
563
723
  logger.debug(f"\tstandardize: {self.standardize}")
564
724
  logger.debug(f"\tlow_pass: {self.low_pass}")
565
725
  logger.debug(f"\thigh_pass: {self.high_pass}")
566
- input["data"] = nimg.clean_img(
726
+ logger.debug(f"\tt_r: {self.t_r}")
727
+
728
+ # Deconfound data
729
+ cleaned_img = nimg.clean_img(
567
730
  imgs=bold_img,
568
731
  detrend=self.detrend,
569
732
  standardize=self.standardize,
@@ -572,6 +735,22 @@ class fMRIPrepConfoundRemover(BasePreprocessor):
572
735
  high_pass=self.high_pass,
573
736
  t_r=t_r,
574
737
  mask_img=mask_img,
738
+ **signal_clean_kwargs,
739
+ )
740
+ # Fix t_r as nilearn messes it up
741
+ cleaned_img.header["pixdim"][4] = t_r
742
+ # Save deconfounded data
743
+ deconfounded_img_path = element_tempdir / "deconfounded_data.nii.gz"
744
+ nib.save(cleaned_img, deconfounded_img_path)
745
+
746
+ logger.debug("Updating `BOLD`")
747
+ input.update(
748
+ {
749
+ # Update path to sync with "data"
750
+ "path": deconfounded_img_path,
751
+ # Update data
752
+ "data": cleaned_img,
753
+ }
575
754
  )
576
755
 
577
756
  return input, None
File without changes