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
@@ -3,7 +3,7 @@
3
3
  # Authors: Synchon Mandal <s.mandal@fz-juelich.de>
4
4
  # License: AGPL
5
5
 
6
- import warnings
6
+ from pathlib import Path
7
7
 
8
8
  import nibabel
9
9
  import numpy as np
@@ -14,6 +14,12 @@ from nilearn.maskers import NiftiSpheresMasker
14
14
  from numpy.testing import assert_array_equal
15
15
 
16
16
  from junifer.external.nilearn import JuniferNiftiSpheresMasker
17
+ from junifer.external.nilearn.junifer_nifti_spheres_masker import (
18
+ _apply_mask_and_get_affinity,
19
+ _get_sphere_voxels,
20
+ _iter_signals_from_spheres,
21
+ )
22
+ from junifer.testing import config_override
17
23
 
18
24
 
19
25
  # New BSD License
@@ -262,13 +268,16 @@ def test_standardization() -> None:
262
268
  data = np.random.RandomState(42).random_sample((3, 3, 3, 5))
263
269
  img = nibabel.Nifti1Image(data, np.eye(4))
264
270
 
265
- # test zscore
266
- masker = JuniferNiftiSpheresMasker(seeds=[(1, 1, 1)], standardize="zscore")
271
+ # test zscore_sample
272
+ masker = JuniferNiftiSpheresMasker(
273
+ seeds=[(1, 1, 1)], standardize="zscore_sample"
274
+ )
267
275
  # Test the fit
268
276
  s = masker.fit_transform(img)
269
277
 
270
278
  np.testing.assert_almost_equal(s.mean(), 0)
271
- np.testing.assert_almost_equal(s.std(), 1)
279
+ # Uses the sample standard deviation
280
+ np.testing.assert_almost_equal(s.std(ddof=1), 1)
272
281
 
273
282
  # test psc
274
283
  masker = JuniferNiftiSpheresMasker(seeds=[(1, 1, 1)], standardize="psc")
@@ -294,10 +303,10 @@ def test_nifti_spheres_masker_inverse_transform() -> None:
294
303
 
295
304
 
296
305
  def test_nifti_spheres_masker_io_shapes() -> None:
297
- """Ensure that masker handles 1D/2D/3D/4D data appropriately.
306
+ """Ensure that masker handles 3D/4D data appropriately.
298
307
 
299
- transform(4D image) --> 2D output, no warning
300
- transform(3D image) --> 2D output, DeprecationWarning
308
+ transform(4D image) --> 2D output
309
+ transform(3D image) --> 1D output
301
310
 
302
311
  """
303
312
  n_regions, n_volumes = 2, 5
@@ -318,20 +327,11 @@ def test_nifti_spheres_masker_io_shapes() -> None:
318
327
  )
319
328
  masker.fit()
320
329
 
321
- # DeprecationWarning *should* be raised for 3D inputs
322
- with pytest.warns(DeprecationWarning, match=r"Starting in version 0.12"):
323
- test_data = masker.transform(img_3d)
324
- assert test_data.shape == (1, n_regions)
325
-
326
- # DeprecationWarning should *not* be raised for 4D inputs
327
- with warnings.catch_warnings():
328
- warnings.filterwarnings(
329
- "error",
330
- message="Starting in version 0.12",
331
- category=DeprecationWarning,
332
- )
333
- test_data = masker.transform(img_4d)
334
- assert test_data.shape == (n_volumes, n_regions)
330
+ test_data = masker.transform(img_3d)
331
+ assert test_data.shape == (n_regions,)
332
+
333
+ test_data = masker.transform(img_4d)
334
+ assert test_data.shape == (n_volumes, n_regions)
335
335
 
336
336
 
337
337
  @pytest.mark.parametrize(
@@ -405,3 +405,213 @@ def test_junifer_and_nilearn_mean_agg_are_equal(
405
405
  # Checks
406
406
  assert junifer_output.shape == nilearn_output.shape
407
407
  np.testing.assert_almost_equal(junifer_output, nilearn_output)
408
+
409
+
410
+ def _spheres_data(
411
+ tmp_path: Path, dtype: str = "float32", with_nan: bool = True
412
+ ) -> tuple[nibabel.Nifti1Image, nibabel.Nifti1Image, list]:
413
+ """Create a file-backed 4D image, a mask and seeds for the spheres."""
414
+ rng = np.random.default_rng(42)
415
+ affine = np.diag([3.0, 3.0, 3.0, 1.0])
416
+ affine[:3, 3] = -30
417
+ data = rng.normal(100, 10, size=(21, 21, 21, 23))
418
+ if with_nan:
419
+ data[10, 10, 10, 3] = np.nan
420
+ nibabel.save(
421
+ nibabel.Nifti1Image(data.astype(dtype), affine), tmp_path / "data.nii"
422
+ )
423
+ mask = np.zeros((21, 21, 21), dtype="int8")
424
+ mask[4:17, 3:18, 5:16] = 1
425
+ mask_img = nibabel.Nifti1Image(mask, affine)
426
+ # The last seed is outside the image, so its sphere is empty
427
+ seeds = [(0, 0, 0), (12, -9, 6), (-24, 24, 24), (100, 100, 100)]
428
+ return nibabel.load(tmp_path / "data.nii"), mask_img, seeds
429
+
430
+
431
+ @pytest.mark.parametrize("chunk_size", [1, 5, 23, 100])
432
+ @pytest.mark.parametrize("masked", [True, False])
433
+ @pytest.mark.parametrize(
434
+ "dtype, with_nan",
435
+ [
436
+ ("float32", True),
437
+ ("float32", False),
438
+ ("float64", True),
439
+ ("int16", False),
440
+ ],
441
+ )
442
+ def test_iter_signals_from_spheres_chunks(
443
+ tmp_path: Path, chunk_size: int, masked: bool, dtype: str, with_nan: bool
444
+ ) -> None:
445
+ """Test the signals of the spheres read in chunks of volumes.
446
+
447
+ They are the same as the ones extracted from the whole data.
448
+
449
+ Parameters
450
+ ----------
451
+ tmp_path : pathlib.Path
452
+ The path to the test directory.
453
+ chunk_size : int
454
+ The parametrized number of volumes to read at once.
455
+ masked : bool
456
+ The parametrized use of a mask.
457
+ dtype : str
458
+ The parametrized data type.
459
+ with_nan : bool
460
+ The parametrized presence of a NaN value in the data.
461
+
462
+ """
463
+ img, mask_img, seeds = _spheres_data(tmp_path, dtype, with_nan)
464
+ mask_img = mask_img if masked else None
465
+ X, A = _apply_mask_and_get_affinity(
466
+ seeds, img, radius=4.0, allow_overlap=True, mask_img=mask_img
467
+ )
468
+ with config_override("markers.spheres.chunksize", chunk_size):
469
+ signals = list(
470
+ _iter_signals_from_spheres(
471
+ seeds, img, radius=4.0, allow_overlap=True, mask_img=mask_img
472
+ )
473
+ )
474
+ assert len(signals) == len(seeds)
475
+ assert signals[-1].shape == (23, 0)
476
+ for row, t_signals in zip(A.rows, signals, strict=True):
477
+ assert t_signals.dtype == X.dtype
478
+ assert_array_equal(t_signals, X[:, row])
479
+
480
+
481
+ def test_JuniferNiftiSpheresMasker_input_unchanged(tmp_path: Path) -> None:
482
+ """Test the masker does not change the input data.
483
+
484
+ Parameters
485
+ ----------
486
+ tmp_path : pathlib.Path
487
+ The path to the test directory.
488
+
489
+ """
490
+ img, _, seeds = _spheres_data(tmp_path)
491
+ data = np.asarray(img.dataobj).copy()
492
+ in_memory = nibabel.Nifti1Image(data.copy(), img.affine)
493
+ with pytest.warns(RuntimeWarning, match="NaN values"):
494
+ JuniferNiftiSpheresMasker(
495
+ seeds=seeds, radius=4.0, allow_overlap=True
496
+ ).fit_transform(in_memory)
497
+ # The NaN is still there
498
+ assert_array_equal(np.asarray(in_memory.dataobj), data)
499
+
500
+
501
+ @pytest.mark.parametrize("chunk_size", [0, -1, 1.5, True, "10"])
502
+ def test_JuniferNiftiSpheresMasker_chunk_size_error(
503
+ tmp_path: Path, chunk_size: object
504
+ ) -> None:
505
+ """Test the masker with an invalid number of volumes to read at once.
506
+
507
+ Parameters
508
+ ----------
509
+ tmp_path : pathlib.Path
510
+ The path to the test directory.
511
+ chunk_size : object
512
+ The parametrized invalid number of volumes.
513
+
514
+ """
515
+ img, _, seeds = _spheres_data(tmp_path)
516
+ with config_override("markers.spheres.chunksize", chunk_size):
517
+ with pytest.raises(ValueError, match="must be a positive integer"):
518
+ JuniferNiftiSpheresMasker(seeds=seeds, radius=4.0).fit_transform(
519
+ img
520
+ )
521
+
522
+
523
+ def test_JuniferNiftiSpheresMasker_reports() -> None:
524
+ """Test the reports are disabled by default, as they load the data."""
525
+ assert JuniferNiftiSpheresMasker(seeds=[(1, 1, 1)]).reports is False
526
+ masker = JuniferNiftiSpheresMasker(seeds=[(1, 1, 1)], reports=True)
527
+ assert masker.reports is True
528
+
529
+
530
+ def test_get_sphere_voxels_mask_only(tmp_path: Path) -> None:
531
+ """Test the voxels of the spheres with only a mask, without an image.
532
+
533
+ Parameters
534
+ ----------
535
+ tmp_path : pathlib.Path
536
+ The path to the test directory.
537
+
538
+ """
539
+ img, mask_img, seeds = _spheres_data(tmp_path)
540
+ voxels, A = _get_sphere_voxels(
541
+ seeds, img, radius=4.0, allow_overlap=True, mask_img=mask_img
542
+ )
543
+ mask_voxels, mask_A = _get_sphere_voxels(
544
+ seeds, None, radius=4.0, allow_overlap=True, mask_img=mask_img
545
+ )
546
+ assert_array_equal(mask_voxels, voxels)
547
+ assert mask_A.rows.tolist() == A.rows.tolist()
548
+ # No signals without an image
549
+ X, _ = _apply_mask_and_get_affinity(
550
+ seeds, None, radius=4.0, allow_overlap=True, mask_img=mask_img
551
+ )
552
+ assert X is None
553
+ with pytest.raises(ValueError, match="Either a niimg or a mask_img"):
554
+ _get_sphere_voxels(seeds, None, radius=4.0, allow_overlap=True)
555
+
556
+
557
+ @pytest.mark.parametrize("dtype", ["auto", "float64"])
558
+ def test_JuniferNiftiSpheresMasker_dtype(tmp_path: Path, dtype: str) -> None:
559
+ """Test the data type of the signals is the same as with nilearn.
560
+
561
+ Parameters
562
+ ----------
563
+ tmp_path : pathlib.Path
564
+ The path to the test directory.
565
+ dtype : str
566
+ The parametrized data type of the extraction.
567
+
568
+ """
569
+ img, _, seeds = _spheres_data(tmp_path, "float32", with_nan=False)
570
+ junifer_signals = JuniferNiftiSpheresMasker(
571
+ seeds=seeds[:3], radius=4.0, allow_overlap=True, dtype=dtype
572
+ ).fit_transform(img)
573
+ nilearn_signals = NiftiSpheresMasker(
574
+ seeds=seeds[:3], radius=4.0, allow_overlap=True, dtype=dtype
575
+ ).fit_transform(img)
576
+ assert junifer_signals.dtype == nilearn_signals.dtype
577
+ np.testing.assert_allclose(junifer_signals, nilearn_signals, rtol=1e-6)
578
+
579
+
580
+ @pytest.mark.parametrize("chunk_size", [5, 100])
581
+ @pytest.mark.parametrize("masked", [True, False])
582
+ @pytest.mark.parametrize("with_nan", [True, False])
583
+ def test_junifer_and_nilearn_spheres_are_equal(
584
+ tmp_path: Path, chunk_size: int, masked: bool, with_nan: bool
585
+ ) -> None:
586
+ """Test the signals of the spheres are the same as with nilearn.
587
+
588
+ The signals are read in chunks of volumes from a file-backed image.
589
+
590
+ Parameters
591
+ ----------
592
+ tmp_path : pathlib.Path
593
+ The path to the test directory.
594
+ chunk_size : int
595
+ The parametrized number of volumes to read at once.
596
+ masked : bool
597
+ The parametrized use of a mask.
598
+ with_nan : bool
599
+ The parametrized presence of a NaN value in the data.
600
+
601
+ """
602
+ img, mask_img, seeds = _spheres_data(tmp_path, with_nan=with_nan)
603
+ # Only the seeds with voxels in the mask, as nilearn fails for empty
604
+ # spheres
605
+ params = {
606
+ "seeds": seeds[:2],
607
+ "radius": 4.0,
608
+ "allow_overlap": True,
609
+ "mask_img": mask_img if masked else None,
610
+ }
611
+ with config_override("markers.spheres.chunksize", chunk_size):
612
+ junifer_signals = JuniferNiftiSpheresMasker(**params).fit_transform(
613
+ img
614
+ )
615
+ nilearn_signals = NiftiSpheresMasker(**params).fit_transform(img)
616
+ assert junifer_signals.dtype == nilearn_signals.dtype
617
+ np.testing.assert_allclose(junifer_signals, nilearn_signals, rtol=1e-6)
File without changes
@@ -11,9 +11,11 @@ __all__ = [
11
11
  "EdgeCentricFCMaps",
12
12
  "EdgeCentricFCParcels",
13
13
  "EdgeCentricFCSpheres",
14
+ "ReHoImpl",
14
15
  "ReHoMaps",
15
16
  "ReHoParcels",
16
17
  "ReHoSpheres",
18
+ "ALFFImpl",
17
19
  "ALFFMaps",
18
20
  "ALFFParcels",
19
21
  "ALFFSpheres",
@@ -21,9 +23,10 @@ __all__ = [
21
23
  "TemporalSNRParcels",
22
24
  "TemporalSNRSpheres",
23
25
  "BrainPrint",
26
+ "logger",
24
27
  ]
25
28
 
26
- from .base import BaseMarker
29
+ from .base import BaseMarker, logger
27
30
  from .ets_rss import RSSETSMarker
28
31
  from .maps_aggregation import MapsAggregation
29
32
  from .parcel_aggregation import ParcelAggregation
@@ -37,8 +40,8 @@ from .functional_connectivity import (
37
40
  EdgeCentricFCParcels,
38
41
  EdgeCentricFCSpheres,
39
42
  )
40
- from .reho import ReHoMaps, ReHoParcels, ReHoSpheres
41
- from .falff import ALFFMaps, ALFFParcels, ALFFSpheres
43
+ from .reho import ReHoImpl, ReHoMaps, ReHoParcels, ReHoSpheres
44
+ from .falff import ALFFImpl, ALFFMaps, ALFFParcels, ALFFSpheres
42
45
  from .temporal_snr import (
43
46
  TemporalSNRMaps,
44
47
  TemporalSNRParcels,
junifer/markers/base.py CHANGED
@@ -6,17 +6,27 @@
6
6
 
7
7
  from abc import ABC, abstractmethod
8
8
  from copy import deepcopy
9
- from typing import Any, ClassVar, Optional, Union
9
+ from typing import Annotated, Any, ClassVar
10
10
 
11
+ import structlog
12
+ from pydantic import BaseModel, BeforeValidator, ConfigDict
13
+
14
+ from ..datagrabber import DataType
11
15
  from ..pipeline import PipelineStepMixin, UpdateMetaMixin
16
+ from ..storage import StorageType
12
17
  from ..typing import MarkerInOutMappings, StorageLike
13
- from ..utils import logger, raise_error
18
+ from ..utils import ensure_list_or_none, raise_error
19
+ from .utils import _changed_paths, _fingerprint
14
20
 
15
21
 
16
22
  __all__ = ["BaseMarker"]
17
23
 
18
24
 
19
- class BaseMarker(ABC, PipelineStepMixin, UpdateMetaMixin):
25
+ _log = structlog.get_logger("junifer")
26
+ logger = _log.bind(pkg="markers", step="marker")
27
+
28
+
29
+ class BaseMarker(BaseModel, ABC, PipelineStepMixin, UpdateMetaMixin):
20
30
  """Abstract base class for marker.
21
31
 
22
32
  For every marker, one needs to provide a concrete
@@ -24,12 +34,17 @@ class BaseMarker(ABC, PipelineStepMixin, UpdateMetaMixin):
24
34
 
25
35
  Parameters
26
36
  ----------
27
- on : str or list of str or None, optional
28
- The data type to apply the marker on. If None,
29
- will work on all available data types (default None).
30
- name : str, optional
31
- The name of the marker. If None, will use the class name as the
32
- name of the marker (default None).
37
+ on : :enum:`.DataType` or list of variants or None, optional
38
+ The data type(s) to apply the marker on.
39
+ If None, will work on all available data types.
40
+ Check :enum:`.DataType` for valid values (default None).
41
+ name : str or None, optional
42
+ The name of the marker.
43
+ If None, will use the class name (default None).
44
+
45
+ Attributes
46
+ ----------
47
+ valid_inputs
33
48
 
34
49
  Raises
35
50
  ------
@@ -42,11 +57,15 @@ class BaseMarker(ABC, PipelineStepMixin, UpdateMetaMixin):
42
57
 
43
58
  _MARKER_INOUT_MAPPINGS: ClassVar[MarkerInOutMappings]
44
59
 
45
- def __init__(
46
- self,
47
- on: Optional[Union[list[str], str]] = None,
48
- name: Optional[str] = None,
49
- ) -> None:
60
+ model_config = ConfigDict(extra="forbid", use_enum_values=True)
61
+
62
+ on: Annotated[
63
+ DataType | list[DataType] | None,
64
+ BeforeValidator(ensure_list_or_none),
65
+ ] = None
66
+ name: str | None = None
67
+
68
+ def model_post_init(self, context: Any): # noqa: D102
50
69
  # Check for missing mapping attribute
51
70
  if not hasattr(self, "_MARKER_INOUT_MAPPINGS"):
52
71
  raise_error(
@@ -54,18 +73,51 @@ class BaseMarker(ABC, PipelineStepMixin, UpdateMetaMixin):
54
73
  klass=AttributeError,
55
74
  )
56
75
  # Use all data types if not provided
57
- if on is None:
58
- on = self.get_valid_inputs()
59
- # Convert data types to list
60
- if not isinstance(on, list):
61
- on = [on]
76
+ if self.on is None:
77
+ self.on = self.valid_inputs
78
+ else:
79
+ # Convert to correct data type
80
+ self.on = [
81
+ DataType(t) if isinstance(t, str) else t for t in self.on
82
+ ]
83
+ # Check if required input data types are provided
84
+ if any(x not in self.valid_inputs for x in self.on):
85
+ wrong_on = [
86
+ x.value for x in self.on if x not in self.valid_inputs
87
+ ]
88
+ raise_error(
89
+ f"{self.__class__.__name__} cannot be computed on "
90
+ f"{wrong_on}"
91
+ )
92
+ # Run extra validation for markers and fail early if needed
93
+ self.validate_marker_params()
62
94
  # Set default name if not provided
63
- self.name = self.__class__.__name__ if name is None else name
64
- # Check if required inputs are found
65
- if any(x not in self.get_valid_inputs() for x in on):
66
- wrong_on = [x for x in on if x not in self.get_valid_inputs()]
67
- raise_error(f"{self.name} cannot be computed on {wrong_on}")
68
- self._on = on
95
+ self.name = self.__class__.__name__ if self.name is None else self.name
96
+ logger.info(
97
+ f"Parameters: {self.model_dump(mode='json')}", component=self.name
98
+ )
99
+
100
+ @property
101
+ def valid_inputs(self) -> list[DataType]:
102
+ """Valid data types to operate on.
103
+
104
+ Returns
105
+ -------
106
+ list of :enum:`.DataType`
107
+ The list of data types that can be used as input for this marker.
108
+
109
+ """
110
+ return [
111
+ DataType(x) if isinstance(x, str) else x
112
+ for x in self._MARKER_INOUT_MAPPINGS.keys()
113
+ ]
114
+
115
+ def validate_marker_params(self) -> None:
116
+ """Run extra logical validation for marker.
117
+
118
+ Subclasses can override to provide validation.
119
+ """
120
+ pass
69
121
 
70
122
  def validate_input(self, input: list[str]) -> list[str]:
71
123
  """Validate input.
@@ -88,45 +140,36 @@ class BaseMarker(ABC, PipelineStepMixin, UpdateMetaMixin):
88
140
  If the input does not have the required data.
89
141
 
90
142
  """
91
- if not any(x in input for x in self._on):
143
+ if not any(x in input for x in self.on):
92
144
  raise_error(
93
- "Input does not have the required data."
94
- f"\t Input: {input}"
95
- f"\t Required (any of): {self._on}"
145
+ "Input does not have the required data.\n"
146
+ f"\t Input: {input}\n"
147
+ f"\t Required (any of): {[t.value for t in self.on]}"
96
148
  )
97
- return [x for x in self._on if x in input]
149
+ return [x.value for x in self.on if x in input]
98
150
 
99
- def get_valid_inputs(self) -> list[str]:
100
- """Get valid data types for input.
101
-
102
- Returns
103
- -------
104
- list of str
105
- The list of data types that can be used as input for this marker.
106
-
107
- """
108
- return list(self._MARKER_INOUT_MAPPINGS.keys())
109
-
110
- def storage_type(self, input_type: str, output_feature: str) -> str:
111
- """Get storage type for a feature.
151
+ def storage_type(
152
+ self, input_type: DataType, output_feature: str
153
+ ) -> StorageType:
154
+ """Get :enum:`.StorageType` for a feature.
112
155
 
113
156
  Parameters
114
157
  ----------
115
- input_type : str
158
+ input_type : :enum:`.DataType`
116
159
  The data type input to the marker.
117
160
  output_feature : str
118
161
  The feature output of the marker.
119
162
 
120
163
  Returns
121
164
  -------
122
- str
165
+ :enum:`.StorageType`
123
166
  The storage type output of the marker.
124
167
 
125
168
  """
126
169
  return self._MARKER_INOUT_MAPPINGS[input_type][output_feature]
127
170
 
128
171
  @abstractmethod
129
- def compute(self, input: dict, extra_input: Optional[dict] = None) -> dict:
172
+ def compute(self, input: dict, extra_input: dict | None = None) -> dict:
130
173
  """Compute.
131
174
 
132
175
  Parameters
@@ -155,33 +198,33 @@ class BaseMarker(ABC, PipelineStepMixin, UpdateMetaMixin):
155
198
 
156
199
  def store(
157
200
  self,
158
- type_: str,
201
+ data_type: DataType,
159
202
  feature: str,
160
- out: dict[str, Any],
203
+ output: dict[str, Any],
161
204
  storage: StorageLike,
162
205
  ) -> None:
163
206
  """Store.
164
207
 
165
208
  Parameters
166
209
  ----------
167
- type_ : str
210
+ data_type : :enum:`.DataType`
168
211
  The data type to store.
169
212
  feature : str
170
213
  The feature to store.
171
- out : dict
214
+ output : dict
172
215
  The computed result as a dictionary to store.
173
216
  storage : storage-like
174
- The storage class, for example, SQLiteFeatureStorage.
217
+ The storage class, for example, HDF5FeatureStorage.
175
218
 
176
219
  """
177
- output_type_ = self.storage_type(type_, feature)
178
- logger.debug(f"Storing {output_type_} in {storage}")
179
- storage.store(kind=output_type_, **out)
220
+ s_type = self.storage_type(data_type, feature)
221
+ logger.debug(f"Storing {s_type} in {storage}")
222
+ storage.store(kind=s_type, **output)
180
223
 
181
224
  def _fit_transform(
182
225
  self,
183
226
  input: dict[str, dict],
184
- storage: Optional[StorageLike] = None,
227
+ storage: StorageLike | None = None,
185
228
  ) -> dict:
186
229
  """Fit and transform.
187
230
 
@@ -190,63 +233,70 @@ class BaseMarker(ABC, PipelineStepMixin, UpdateMetaMixin):
190
233
  input : dict
191
234
  The Junifer Data object.
192
235
  storage : storage-like, optional
193
- The storage class, for example, SQLiteFeatureStorage.
236
+ The storage class, for example, HDF5FeatureStorage.
194
237
 
195
238
  Returns
196
239
  -------
197
240
  dict
198
- The processed output as a dictionary. If `storage` is provided,
241
+ The processed output as a dictionary. If ``storage`` is provided,
199
242
  empty dictionary is returned.
200
243
 
201
244
  """
202
245
  out = {}
203
- for type_ in self._on:
204
- if type_ in input.keys():
205
- logger.info(f"Computing {type_}")
246
+ for t in self.on:
247
+ if t in input.keys():
248
+ logger.info(f"Computing {t}")
206
249
  # Get data dict for data type
207
- t_input = input[type_]
250
+ t_input = input[t]
208
251
  # Pass the other data types as extra input, removing
209
252
  # the current type
210
253
  extra_input = input.copy()
211
- extra_input.pop(type_)
254
+ extra_input.pop(t)
212
255
  logger.debug(
213
256
  f"Extra data type for feature extraction: "
214
257
  f"{extra_input.keys()}"
215
258
  )
216
259
  # Copy metadata
217
260
  t_meta = t_input["meta"].copy()
218
- t_meta["type"] = type_
219
- # Compute marker
261
+ t_meta["type"] = t.value
262
+ # Compute marker, checking it does not change the data, as
263
+ # it is shared with the markers computed afterwards
264
+ fingerprint = _fingerprint(input)
220
265
  t_out = self.compute(input=t_input, extra_input=extra_input)
266
+ changed = _changed_paths(fingerprint, _fingerprint(input))
267
+ if changed:
268
+ raise_error(
269
+ f"Marker {self.name} changed its input data, which is "
270
+ f"shared with the other markers: {', '.join(changed)}",
271
+ klass=RuntimeError,
272
+ )
221
273
  # Initialize empty dictionary if no storage object is provided
222
274
  if storage is None:
223
- out[type_] = {}
275
+ out[t] = {}
224
276
  # Store individual features
225
- for feature_name, feature_data in t_out.items():
277
+ for f_name, f_data in t_out.items():
226
278
  # Make deep copy of the feature data for manipulation
227
- feature_data_copy = deepcopy(feature_data)
279
+ f_data_copy = deepcopy(f_data)
228
280
  # Make deep copy of metadata and add to feature data
229
- feature_data_copy["meta"] = deepcopy(t_meta)
281
+ f_data_copy["meta"] = deepcopy(t_meta)
230
282
  # Update metadata for the feature,
231
283
  # feature data is not manipulated, only meta
232
- self.update_meta(feature_data_copy, "marker")
284
+ self.update_meta(f_data_copy, "marker")
233
285
  # Update marker feature's metadata name
234
- feature_data_copy["meta"]["marker"]["name"] += (
235
- f"_{feature_name}"
236
- )
286
+ f_data_copy["meta"]["marker"]["name"] += f"_{f_name}"
237
287
 
238
288
  if storage is not None:
239
289
  logger.info(f"Storing in {storage}")
240
290
  self.store(
241
- type_=type_,
242
- feature=feature_name,
243
- out=feature_data_copy,
291
+ data_type=t,
292
+ feature=f_name,
293
+ output=f_data_copy,
244
294
  storage=storage,
245
295
  )
246
296
  else:
247
297
  logger.info(
248
298
  "No storage specified, returning dictionary"
249
299
  )
250
- out[type_][feature_name] = feature_data_copy
300
+ out[t][f_name] = f_data_copy
251
301
 
252
302
  return out