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
junifer/storage/hdf5.py CHANGED
@@ -4,25 +4,22 @@
4
4
  # Federico Raimondo <f.raimondo@fz-juelich.de>
5
5
  # License: AGPL
6
6
 
7
+ import io
7
8
  from collections import defaultdict
8
9
  from collections.abc import Sequence
9
10
  from pathlib import Path
10
- from typing import Any, ClassVar, Optional, Union
11
+ from typing import Any, ClassVar, Literal
11
12
 
13
+ import h5py
12
14
  import numpy as np
13
15
  import pandas as pd
16
+ from h5io import read_hdf5, write_hdf5
17
+ from pydantic import PositiveInt
14
18
  from tqdm import tqdm
15
19
 
16
20
  from ..api.decorators import register_storage
17
- from ..external.h5io.h5io import (
18
- ChunkedArray,
19
- ChunkedList,
20
- has_hdf5,
21
- read_hdf5,
22
- write_hdf5,
23
- )
24
- from ..utils import logger, raise_error
25
- from .base import BaseFeatureStorage
21
+ from ..utils import raise_error
22
+ from .base import BaseFeatureStorage, MatrixKind, StorageType, logger
26
23
  from .utils import (
27
24
  element_to_prefix,
28
25
  matrix_to_vector,
@@ -35,21 +32,135 @@ from .utils import (
35
32
  __all__ = ["HDF5FeatureStorage"]
36
33
 
37
34
 
38
- def _create_chunk(
35
+ # Types of the data of collected features in files written with junifer's
36
+ # fork of h5io, and the h5io types they are stored as
37
+ _LEGACY_TYPES = {"chunkedarray": "ndarray", "chunkedlist": "list"}
38
+
39
+
40
+ def _node_type(node: h5py.Group | h5py.Dataset) -> str:
41
+ """Get the h5io type of a node.
42
+
43
+ Parameters
44
+ ----------
45
+ node : h5py.Group or h5py.Dataset
46
+ The node.
47
+
48
+ Returns
49
+ -------
50
+ str
51
+ The type.
52
+
53
+ """
54
+ title = node.attrs["TITLE"]
55
+ return title.decode() if isinstance(title, bytes) else title
56
+
57
+
58
+ def _has_hdf5(fname: str, title: str) -> bool:
59
+ """Check whether a title is in an HDF5 file.
60
+
61
+ Parameters
62
+ ----------
63
+ fname : str
64
+ The HDF5 file.
65
+ title : str
66
+ The title.
67
+
68
+ Returns
69
+ -------
70
+ bool
71
+ Whether the title is in the file.
72
+
73
+ """
74
+ with h5py.File(fname, mode="r") as fid:
75
+ return title in fid
76
+
77
+
78
+ def _read_hdf5(fname: str, title: str) -> Any:
79
+ """Read a title of an HDF5 file.
80
+
81
+ Features collected with junifer's fork of h5io have their data stored with
82
+ types that h5io does not know (see ``_LEGACY_TYPES``). To read them, the
83
+ feature is copied in memory with the h5io types.
84
+
85
+ Parameters
86
+ ----------
87
+ fname : str
88
+ The HDF5 file.
89
+ title : str
90
+ The title.
91
+
92
+ Returns
93
+ -------
94
+ Any
95
+ The data.
96
+
97
+ """
98
+ with h5py.File(fname, mode="r") as fid:
99
+ node = fid.get(title)
100
+ data = node.get("key_data") if isinstance(node, h5py.Group) else None
101
+ if data is not None and _node_type(data) in _LEGACY_TYPES:
102
+ with h5py.File(io.BytesIO(), mode="w") as copy:
103
+ fid.copy(node, copy, name=title)
104
+ copy_data = copy[title]["key_data"]
105
+ copy_data.attrs["TITLE"] = _LEGACY_TYPES[_node_type(data)]
106
+ return read_hdf5(copy, title=title, slash="ignore")
107
+ return read_hdf5(fname, title=title, slash="ignore")
108
+
109
+
110
+ def _read_elements(fname: str, feature_md5s: list[str]) -> dict[str, list]:
111
+ """Get the elements that a file has for each feature, without the data.
112
+
113
+ Parameters
114
+ ----------
115
+ fname : str
116
+ The HDF5 file.
117
+ feature_md5s : list of str
118
+ The MD5 of the feature to check.
119
+
120
+ Returns
121
+ -------
122
+ dict
123
+ The elements present in the file for each feature (MD5 -> list of
124
+ elements). Each element is a tuple of its values, e.g.,
125
+ ``("sub-01",)``, as all the elements of a feature have the same keys.
126
+
127
+ """
128
+ out = {}
129
+ with h5py.File(fname, mode="r") as fid:
130
+ for feature_md5 in feature_md5s:
131
+ # Copy only the elements of the feature, not its data
132
+ with h5py.File(io.BytesIO(), mode="w") as copy:
133
+ fid.copy(fid[feature_md5]["key_element"], copy, name="element")
134
+ elements = read_hdf5(copy, title="element", slash="ignore")
135
+ out[feature_md5] = [tuple(e.values()) for e in elements]
136
+ return out
137
+
138
+
139
+ def _write_chunk(
140
+ fname: str,
141
+ key: str,
39
142
  chunk_data: list[np.ndarray],
40
- kind: str,
143
+ kind: StorageType,
41
144
  element_count: int,
42
145
  chunk_size: int,
43
146
  i_chunk: int,
44
- ) -> Union[ChunkedArray, ChunkedList]:
45
- """Create chunked array or list.
147
+ ) -> None:
148
+ """Write a chunk of the data of the elements.
149
+
150
+ The data is written in a dataset (``ndarray`` in h5io) for vectors and
151
+ matrices, with the elements in the last dimension, or in a group of
152
+ datasets (``list`` in h5io) for the other kinds.
46
153
 
47
154
  Parameters
48
155
  ----------
156
+ fname : str
157
+ The HDF5 file.
158
+ key : str
159
+ The name of the dataset or group to write to.
49
160
  chunk_data : list of numpy.ndarray
50
- The data to be chunked.
51
- kind : str
52
- The kind of data to be chunked.
161
+ The data of the elements in the chunk.
162
+ kind : :enum:`.StorageType`
163
+ The kind of data.
53
164
  element_count : int
54
165
  The total number of elements.
55
166
  chunk_size : int
@@ -57,49 +168,29 @@ def _create_chunk(
57
168
  i_chunk : int
58
169
  The chunk index.
59
170
 
60
- Returns
61
- -------
62
- ChunkedArray or ChunkedList
63
- The chunked array or list.
64
-
65
- Raises
66
- ------
67
- ValueError
68
- If `kind` is not one of ['vector', 'matrix', 'timeseries',
69
- 'scalar_table'].
70
-
71
171
  """
72
- if kind in ["vector", "matrix"]:
73
- features_data = np.concatenate(chunk_data, axis=-1)
74
- array_shape = [features_data.shape[0]]
75
- array_chunk_size = [features_data.shape[0]]
76
- # Append second dimension for 3D
77
- if features_data.ndim == 3:
78
- array_shape.append(features_data.shape[1])
79
- array_chunk_size.append(features_data.shape[1])
80
- # Append final dimension of element count
81
- array_shape.append(element_count)
82
- # Append final dimension of chunk size
83
- array_chunk_size.append(chunk_size)
84
- out = ChunkedArray(
85
- data=features_data,
86
- shape=tuple(array_shape),
87
- chunk_size=tuple(array_chunk_size),
88
- n_chunk=i_chunk,
89
- )
90
- elif kind in ["timeseries", "scalar_table", "timeseries_2d"]:
91
- out = ChunkedList(
92
- data=chunk_data,
93
- size=element_count,
94
- offset=i_chunk * chunk_size,
95
- )
96
- else:
97
- raise_error(
98
- f"Invalid kind: {kind}. "
99
- "Must be one of ['vector', 'matrix', 'timeseries', "
100
- "'timeseries_2d', 'scalar_table']."
101
- )
102
- return out
172
+ offset = i_chunk * chunk_size
173
+ with h5py.File(fname, mode="a") as fid:
174
+ if kind in ["vector", "matrix"]:
175
+ features_data = np.concatenate(chunk_data, axis=-1)
176
+ if key not in fid:
177
+ dataset = fid.create_dataset(
178
+ key,
179
+ shape=(*features_data.shape[:-1], element_count),
180
+ dtype=features_data.dtype,
181
+ chunks=(*features_data.shape[:-1], chunk_size),
182
+ )
183
+ dataset.attrs["TITLE"] = "ndarray"
184
+ n_elements = features_data.shape[-1]
185
+ fid[key][..., offset : offset + n_elements] = features_data
186
+ elif kind in ["timeseries", "scalar_table", "timeseries_2d"]:
187
+ group = fid.require_group(key)
188
+ group.attrs["TITLE"] = "list"
189
+ for i, element_data in enumerate(chunk_data):
190
+ dataset = group.create_dataset(
191
+ f"idx_{offset + i}", data=element_data
192
+ )
193
+ dataset.attrs["TITLE"] = "ndarray"
103
194
 
104
195
 
105
196
  @register_storage
@@ -108,7 +199,7 @@ class HDF5FeatureStorage(BaseFeatureStorage):
108
199
 
109
200
  Parameters
110
201
  ----------
111
- uri : str or pathlib.Path
202
+ uri : pathlib.Path
112
203
  The path to the file to be used.
113
204
  single_output : bool, optional
114
205
  If False, will create one HDF5 file per element. The name
@@ -124,7 +215,7 @@ class HDF5FeatureStorage(BaseFeatureStorage):
124
215
  force_float32 : bool, optional
125
216
  Whether to force casting of numpy.ndarray values to float32 if float64
126
217
  values are found (default True).
127
- chunk_size : int, optional
218
+ chunk_size : positive int, optional
128
219
  The chunk size to use when collecting data from element files in
129
220
  :meth:`.collect`. If the file count is smaller than the value, the
130
221
  minimum is used (default 100).
@@ -135,34 +226,21 @@ class HDF5FeatureStorage(BaseFeatureStorage):
135
226
 
136
227
  """
137
228
 
138
- _STORAGE_TYPES: ClassVar[Sequence[str]] = [
139
- "vector",
140
- "timeseries",
141
- "matrix",
142
- "scalar_table",
143
- "timeseries_2d",
229
+ _STORAGE_TYPES: ClassVar[Sequence[StorageType]] = [
230
+ StorageType.Vector,
231
+ StorageType.Timeseries,
232
+ StorageType.Matrix,
233
+ StorageType.ScalarTable,
234
+ StorageType.Timeseries2D,
144
235
  ]
145
236
 
146
- def __init__(
147
- self,
148
- uri: Union[str, Path],
149
- single_output: bool = True,
150
- overwrite: Union[bool, str] = "update",
151
- compression: int = 7,
152
- force_float32: bool = True,
153
- chunk_size: int = 100,
154
- ) -> None:
155
- self.overwrite = overwrite
156
- self.compression = compression
157
- self.force_float32 = force_float32
158
- self.chunk_size = chunk_size
159
- super().__init__(
160
- uri=uri,
161
- single_output=single_output,
162
- )
237
+ overwrite: bool | str = "update"
238
+ compression: Literal[0, 1, 2, 3, 4, 5, 6, 7, 8, 9] = 7
239
+ force_float32: bool = True
240
+ chunk_size: PositiveInt = 100
163
241
 
164
- def _fetch_correct_uri_for_io(self, element: Optional[dict]) -> str:
165
- """Return proper URI for I/O based on ``element``.
242
+ def _fetch_correct_uri_for_io(self, element: dict | None) -> str:
243
+ """Return proper URI for I/O based on `element`.
166
244
 
167
245
  If ``element`` is None, will return ``self.uri``.
168
246
 
@@ -197,7 +275,7 @@ class HDF5FeatureStorage(BaseFeatureStorage):
197
275
  return f"{self.uri.parent}/{prefix}{self.uri.name}"
198
276
 
199
277
  def _read_metadata(
200
- self, element: Optional[dict[str, str]] = None
278
+ self, element: dict[str, str] | None = None
201
279
  ) -> dict[str, dict[str, Any]]:
202
280
  """Read metadata (should not be called directly).
203
281
 
@@ -231,7 +309,7 @@ class HDF5FeatureStorage(BaseFeatureStorage):
231
309
  )
232
310
 
233
311
  # Check if group is found in the storage
234
- if not has_hdf5(fname=uri, title="meta"):
312
+ if not _has_hdf5(fname=uri, title="meta"):
235
313
  raise_error(
236
314
  f"Invalid junifer HDF5 file at: {uri}",
237
315
  klass=RuntimeError,
@@ -239,11 +317,7 @@ class HDF5FeatureStorage(BaseFeatureStorage):
239
317
 
240
318
  # Read metadata
241
319
  logger.debug(f"Loading HDF5 metadata from: {uri}")
242
- metadata = read_hdf5(
243
- fname=uri,
244
- title="meta",
245
- slash="ignore",
246
- )
320
+ metadata = _read_hdf5(fname=uri, title="meta")
247
321
  logger.debug(f"Loaded HDF5 metadata from: {uri}")
248
322
 
249
323
  return metadata
@@ -260,15 +334,11 @@ class HDF5FeatureStorage(BaseFeatureStorage):
260
334
 
261
335
  """
262
336
  # Read metadata
263
- metadata = read_hdf5(
264
- fname=str(self.uri.resolve()),
265
- title="meta",
266
- slash="ignore",
267
- )
337
+ metadata = _read_hdf5(fname=str(self.uri.resolve()), title="meta")
268
338
  return metadata
269
339
 
270
340
  def _read_data(
271
- self, md5: str, element: Optional[dict[str, str]] = None
341
+ self, md5: str, element: dict[str, str] | None = None
272
342
  ) -> dict[str, Any]:
273
343
  """Read data (should not be called directly).
274
344
 
@@ -276,7 +346,7 @@ class HDF5FeatureStorage(BaseFeatureStorage):
276
346
  ----------
277
347
  md5 : str
278
348
  The MD5 used as the HDF5 group name.
279
- element : dict, optional
349
+ element : dict or None, optional
280
350
  The element as dictionary (default None).
281
351
 
282
352
  Returns
@@ -304,7 +374,7 @@ class HDF5FeatureStorage(BaseFeatureStorage):
304
374
  )
305
375
 
306
376
  # Check if group is found in the storage
307
- if not has_hdf5(fname=uri, title=md5):
377
+ if not _has_hdf5(fname=uri, title=md5):
308
378
  raise_error(
309
379
  f"{md5} not found in HDF5 file at: {uri}",
310
380
  klass=RuntimeError,
@@ -312,22 +382,16 @@ class HDF5FeatureStorage(BaseFeatureStorage):
312
382
 
313
383
  # Read data
314
384
  logger.debug(f"Loading HDF5 data for {md5} from: {uri}")
315
- data = read_hdf5(
316
- fname=uri,
317
- title=md5,
318
- slash="ignore",
319
- )
385
+ data = _read_hdf5(fname=uri, title=md5)
320
386
  logger.debug(f"Loaded HDF5 data for {md5} from: {uri}")
321
387
 
322
388
  return data
323
389
 
324
390
  def read(
325
391
  self,
326
- feature_name: Optional[str] = None,
327
- feature_md5: Optional[str] = None,
328
- ) -> dict[
329
- str, Union[str, list[Union[int, str, dict[str, str]]], np.ndarray]
330
- ]:
392
+ feature_name: str | None = None,
393
+ feature_md5: str | None = None,
394
+ ) -> dict[str, str | list[int | str | dict[str, str]] | np.ndarray]:
331
395
  """Read stored feature.
332
396
 
333
397
  Parameters
@@ -371,11 +435,7 @@ class HDF5FeatureStorage(BaseFeatureStorage):
371
435
  )
372
436
  )
373
437
  # Parameter check pass; read metadata
374
- metadata = read_hdf5(
375
- fname=str(self.uri.resolve()),
376
- title="meta",
377
- slash="ignore",
378
- )
438
+ metadata = _read_hdf5(fname=str(self.uri.resolve()), title="meta")
379
439
  # Initialize MD5 variable
380
440
  md5: str = ""
381
441
 
@@ -383,7 +443,7 @@ class HDF5FeatureStorage(BaseFeatureStorage):
383
443
  if feature_md5:
384
444
  logger.debug(
385
445
  f"Validating feature MD5 '{feature_md5}' in metadata "
386
- f"for: {self.uri.resolve()} ..." # type: ignore
446
+ f"for: {self.uri.resolve()} ..."
387
447
  )
388
448
  # Validate MD5
389
449
  if feature_md5 in metadata:
@@ -423,17 +483,13 @@ class HDF5FeatureStorage(BaseFeatureStorage):
423
483
  md5 = feature_name_duplicates_with_different_md5[0]
424
484
 
425
485
  # Read data from HDF5
426
- hdf_data = read_hdf5(
427
- fname=str(self.uri.resolve()),
428
- title=md5,
429
- slash="ignore",
430
- )
486
+ hdf_data = _read_hdf5(fname=str(self.uri.resolve()), title=md5)
431
487
  return hdf_data
432
488
 
433
489
  def read_df(
434
490
  self,
435
- feature_name: Optional[str] = None,
436
- feature_md5: Optional[str] = None,
491
+ feature_name: str | None = None,
492
+ feature_md5: str | None = None,
437
493
  ) -> pd.DataFrame:
438
494
  """Read feature into a pandas.DataFrame.
439
495
 
@@ -613,6 +669,7 @@ class HDF5FeatureStorage(BaseFeatureStorage):
613
669
  meta_md5: str,
614
670
  element: dict[str, str],
615
671
  meta: dict[str, Any],
672
+ processed_element: dict[str, str] | None = None,
616
673
  ) -> None:
617
674
  """Store metadata.
618
675
 
@@ -629,15 +686,20 @@ class HDF5FeatureStorage(BaseFeatureStorage):
629
686
  The element as a dictionary.
630
687
  meta : dict
631
688
  The metadata as a dictionary.
689
+ processed_element : dict or None, optional
690
+ The element being processed when the data was computed, used to
691
+ choose the file of each element if ``single_output=False``. If
692
+ None, ``element`` is used (default None).
632
693
 
633
694
  """
634
- # Get correct URI for element;
695
+ # Get correct URI for the element being processed;
635
696
  # is different from uri if single_output is False
636
- uri = self._fetch_correct_uri_for_io(element=element)
697
+ file_element = processed_element or element
698
+ uri = self._fetch_correct_uri_for_io(element=file_element)
637
699
 
638
700
  # Check if file exists, then read metadata else create empty dictionary
639
701
  if Path(uri).exists():
640
- metadata = self._read_metadata(element=element)
702
+ metadata = self._read_metadata(element=file_element)
641
703
  else:
642
704
  logger.debug(f"Creating new file at {uri} ...")
643
705
  metadata = {}
@@ -670,10 +732,11 @@ class HDF5FeatureStorage(BaseFeatureStorage):
670
732
 
671
733
  def _store_data(
672
734
  self,
673
- kind: str,
735
+ kind: StorageType,
674
736
  meta_md5: str,
675
737
  element: list[dict[str, str]],
676
738
  data: np.ndarray,
739
+ processed_element: dict[str, str] | None = None,
677
740
  **kwargs: Any,
678
741
  ) -> None:
679
742
  """Store data.
@@ -685,7 +748,7 @@ class HDF5FeatureStorage(BaseFeatureStorage):
685
748
 
686
749
  Parameters
687
750
  ----------
688
- kind : {"matrix", "vector", "timeseries", "scalar_table"}
751
+ kind : :enum:`.StorageType`
689
752
  The storage kind.
690
753
  meta_md5 : str
691
754
  The metadata MD5 hash.
@@ -693,18 +756,23 @@ class HDF5FeatureStorage(BaseFeatureStorage):
693
756
  The element as list of dictionary.
694
757
  data : numpy.ndarray
695
758
  The data to store.
759
+ processed_element : dict or None, optional
760
+ The element being processed when the data was computed, used to
761
+ choose the file of each element if ``single_output=False``. If
762
+ None, ``element`` is used (default None).
696
763
  **kwargs : dict
697
764
  Keyword arguments passed from the calling method.
698
765
 
699
766
  """
700
- # Get correct URI for element;
767
+ # Get correct URI for the element being processed;
701
768
  # is different from uri if single_output is False
702
- uri = self._fetch_correct_uri_for_io(element=element[0])
769
+ file_element = processed_element or element[0]
770
+ uri = self._fetch_correct_uri_for_io(element=file_element)
703
771
 
704
772
  # Check if MD5 exists, then read data else create empty dictionary
705
773
  # File should be present here already
706
- if has_hdf5(fname=uri, title=meta_md5):
707
- stored_data = self._read_data(md5=meta_md5, element=element[0])
774
+ if _has_hdf5(fname=uri, title=meta_md5):
775
+ stored_data = self._read_data(md5=meta_md5, element=file_element)
708
776
  else:
709
777
  logger.debug(f"Creating new data map for {meta_md5} ...")
710
778
  stored_data = {}
@@ -814,13 +882,14 @@ class HDF5FeatureStorage(BaseFeatureStorage):
814
882
  def store_matrix(
815
883
  self,
816
884
  meta_md5: str,
817
- element: dict[str, str],
885
+ element: dict,
818
886
  data: np.ndarray,
819
- col_names: Optional[Sequence[str]] = None,
820
- row_names: Optional[Sequence[str]] = None,
821
- matrix_kind: str = "full",
887
+ col_names: Sequence[str] | None = None,
888
+ row_names: Sequence[str] | None = None,
889
+ matrix_kind: MatrixKind = MatrixKind.Full,
822
890
  diagonal: bool = True,
823
891
  row_header_col_name: str = "ROI",
892
+ processed_element: dict[str, str] | None = None,
824
893
  ) -> None:
825
894
  """Store matrix.
826
895
 
@@ -839,28 +908,17 @@ class HDF5FeatureStorage(BaseFeatureStorage):
839
908
  The column labels (default None).
840
909
  row_names : list-like of str, optional
841
910
  The row labels (default None).
842
- matrix_kind : str, optional
843
- The kind of matrix:
844
-
845
- * ``triu`` : store upper triangular only
846
- * ``tril`` : store lower triangular
847
- * ``full`` : full matrix
848
-
849
- (default "full").
911
+ matrix_kind : :enum:`.MatrixKind`, optional
912
+ The matrix kind (default ``MatrixKind.Full``).
850
913
  diagonal : bool, optional
851
- Whether to store the diagonal. If ``matrix_kind`` is "full",
914
+ Whether to store the diagonal. If ``matrix_kind=MatrixKind.Full``,
852
915
  setting this to False will raise an error (default True).
853
916
  row_header_col_name : str, optional
854
917
  The column name for the row header column (default "ROI").
855
-
856
- Raises
857
- ------
858
- ValueError
859
- If invalid ``matrix_kind`` is provided, ``diagonal = False``
860
- for ``matrix_kind = "full"``, non-square data is provided
861
- for ``matrix_kind = {"triu", "tril"}``, length of ``row_names``
862
- do not match data row count, or length of ``col_names`` do not
863
- match data column count.
918
+ processed_element : dict or None, optional
919
+ The element being processed when the data was computed, used to
920
+ choose the file of each element if ``single_output=False``. If
921
+ None, ``element`` is used (default None).
864
922
 
865
923
  """
866
924
  # Row data validation
@@ -879,9 +937,10 @@ class HDF5FeatureStorage(BaseFeatureStorage):
879
937
  )
880
938
  # Store
881
939
  self._store_data(
882
- kind="matrix",
940
+ kind=StorageType.Matrix,
883
941
  meta_md5=meta_md5,
884
942
  element=[element], # convert to list
943
+ processed_element=processed_element,
885
944
  data=data[:, :, np.newaxis], # convert to 3D
886
945
  column_headers=col_names,
887
946
  row_headers=row_names,
@@ -893,9 +952,10 @@ class HDF5FeatureStorage(BaseFeatureStorage):
893
952
  def store_vector(
894
953
  self,
895
954
  meta_md5: str,
896
- element: dict,
897
- data: Union[np.ndarray, list],
898
- col_names: Optional[Sequence[str]] = None,
955
+ element: dict[str, str],
956
+ data: np.ndarray | list,
957
+ col_names: Sequence[str] | None = None,
958
+ processed_element: dict[str, str] | None = None,
899
959
  ) -> None:
900
960
  """Store vector.
901
961
 
@@ -909,6 +969,10 @@ class HDF5FeatureStorage(BaseFeatureStorage):
909
969
  The vector data to store.
910
970
  col_names : list-like of str, optional
911
971
  The column labels (default None).
972
+ processed_element : dict or None, optional
973
+ The element being processed when the data was computed, used to
974
+ choose the file of each element if ``single_output=False``. If
975
+ None, ``element`` is used (default None).
912
976
 
913
977
  """
914
978
  if isinstance(data, list):
@@ -926,9 +990,10 @@ class HDF5FeatureStorage(BaseFeatureStorage):
926
990
  processed_data = data.ravel()
927
991
 
928
992
  self._store_data(
929
- kind="vector",
993
+ kind=StorageType.Vector,
930
994
  meta_md5=meta_md5,
931
995
  element=[element], # convert to list
996
+ processed_element=processed_element,
932
997
  data=processed_data[:, np.newaxis], # convert to 2D
933
998
  column_headers=col_names,
934
999
  )
@@ -938,7 +1003,8 @@ class HDF5FeatureStorage(BaseFeatureStorage):
938
1003
  meta_md5: str,
939
1004
  element: dict,
940
1005
  data: np.ndarray,
941
- col_names: Optional[Sequence[str]] = None,
1006
+ col_names: Sequence[str] | None = None,
1007
+ processed_element: dict[str, str] | None = None,
942
1008
  ) -> None:
943
1009
  """Store timeseries.
944
1010
 
@@ -952,12 +1018,17 @@ class HDF5FeatureStorage(BaseFeatureStorage):
952
1018
  The timeseries data to store.
953
1019
  col_names : list-like of str, optional
954
1020
  The column labels (default None).
1021
+ processed_element : dict or None, optional
1022
+ The element being processed when the data was computed, used to
1023
+ choose the file of each element if ``single_output=False``. If
1024
+ None, ``element`` is used (default None).
955
1025
 
956
1026
  """
957
1027
  self._store_data(
958
- kind="timeseries",
1028
+ kind=StorageType.Timeseries,
959
1029
  meta_md5=meta_md5,
960
1030
  element=[element], # convert to list
1031
+ processed_element=processed_element,
961
1032
  data=[data], # convert to list
962
1033
  column_headers=col_names,
963
1034
  row_header_column_name="timepoint",
@@ -968,8 +1039,9 @@ class HDF5FeatureStorage(BaseFeatureStorage):
968
1039
  meta_md5: str,
969
1040
  element: dict,
970
1041
  data: np.ndarray,
971
- col_names: Optional[Sequence[str]] = None,
972
- row_names: Optional[Sequence[str]] = None,
1042
+ col_names: Sequence[str] | None = None,
1043
+ row_names: Sequence[str] | None = None,
1044
+ processed_element: dict[str, str] | None = None,
973
1045
  ) -> None:
974
1046
  """Store 2D timeseries.
975
1047
 
@@ -985,6 +1057,10 @@ class HDF5FeatureStorage(BaseFeatureStorage):
985
1057
  The column labels (default None).
986
1058
  row_names : list-like of str, optional
987
1059
  The row labels (default None).
1060
+ processed_element : dict or None, optional
1061
+ The element being processed when the data was computed, used to
1062
+ choose the file of each element if ``single_output=False``. If
1063
+ None, ``element`` is used (default None).
988
1064
 
989
1065
  """
990
1066
  store_timeseries_2d_checks(
@@ -993,9 +1069,10 @@ class HDF5FeatureStorage(BaseFeatureStorage):
993
1069
  col_names_len=len(col_names) if col_names is not None else 0,
994
1070
  )
995
1071
  self._store_data(
996
- kind="timeseries_2d",
1072
+ kind=StorageType.Timeseries2D,
997
1073
  meta_md5=meta_md5,
998
1074
  element=[element], # convert to list
1075
+ processed_element=processed_element,
999
1076
  data=[data], # convert to list
1000
1077
  column_headers=col_names,
1001
1078
  row_headers=row_names,
@@ -1006,9 +1083,10 @@ class HDF5FeatureStorage(BaseFeatureStorage):
1006
1083
  meta_md5: str,
1007
1084
  element: dict,
1008
1085
  data: np.ndarray,
1009
- col_names: Optional[Sequence[str]] = None,
1010
- row_names: Optional[Sequence[str]] = None,
1011
- row_header_col_name: Optional[str] = "feature",
1086
+ col_names: Sequence[str] | None = None,
1087
+ row_names: Sequence[str] | None = None,
1088
+ row_header_col_name: str | None = "feature",
1089
+ processed_element: dict[str, str] | None = None,
1012
1090
  ) -> None:
1013
1091
  """Store table with scalar values.
1014
1092
 
@@ -1026,12 +1104,17 @@ class HDF5FeatureStorage(BaseFeatureStorage):
1026
1104
  The row labels (default None).
1027
1105
  row_header_col_name : str, optional
1028
1106
  The column name for the row header column (default "feature").
1107
+ processed_element : dict or None, optional
1108
+ The element being processed when the data was computed, used to
1109
+ choose the file of each element if ``single_output=False``. If
1110
+ None, ``element`` is used (default None).
1029
1111
 
1030
1112
  """
1031
1113
  self._store_data(
1032
- kind="scalar_table",
1114
+ kind=StorageType.ScalarTable,
1033
1115
  meta_md5=meta_md5,
1034
1116
  element=[element], # convert to list
1117
+ processed_element=processed_element,
1035
1118
  data=[data], # convert to list
1036
1119
  column_headers=col_names,
1037
1120
  row_headers=row_names,
@@ -1065,8 +1148,12 @@ class HDF5FeatureStorage(BaseFeatureStorage):
1065
1148
  logger.info(
1066
1149
  f"Collecting metadata from {self.uri.parent}/*_{self.uri.name}"
1067
1150
  )
1068
- # Collect element files per feature MD5
1151
+ # Collect element files per feature MD5, only the ones with elements
1152
+ # of the feature that are not in other files: the data of an element
1153
+ # can be in the files of several elements being processed (e.g., the
1154
+ # data of a subject in the files of each of its tasks)
1069
1155
  elements_per_feature_md5 = defaultdict(list)
1156
+ seen_elements = defaultdict(set)
1070
1157
  out_metadata = {}
1071
1158
  for file_ in tqdm(
1072
1159
  self.uri.parent.glob(f"*_{self.uri.name}"), desc="file-metadata"
@@ -1083,7 +1170,28 @@ class HDF5FeatureStorage(BaseFeatureStorage):
1083
1170
  out_metadata.update(in_metadata)
1084
1171
 
1085
1172
  # Update element files for found MD5s
1086
- for feature_md5 in in_metadata.keys():
1173
+ file_elements = _read_elements(str(file_), list(in_metadata))
1174
+ for feature_md5, elements in file_elements.items():
1175
+ # Only the files of single elements can be collected
1176
+ if len(elements) != 1:
1177
+ raise_error(
1178
+ msg=(
1179
+ f"The file {file_} has {len(elements)} elements "
1180
+ f"for the feature {feature_md5}, but only the "
1181
+ "files with one element (as stored for each "
1182
+ "element with `single_output=False`) can be "
1183
+ "collected"
1184
+ ),
1185
+ klass=RuntimeError,
1186
+ )
1187
+ (element,) = elements
1188
+ if element in seen_elements[feature_md5]:
1189
+ logger.debug(
1190
+ f"Skipping {file_} for {feature_md5}: its element "
1191
+ "is in another file"
1192
+ )
1193
+ continue
1194
+ seen_elements[feature_md5].add(element)
1087
1195
  elements_per_feature_md5[feature_md5].append(file_)
1088
1196
 
1089
1197
  logger.info("Writing metadata to HDF5 file ...")
@@ -1096,7 +1204,7 @@ class HDF5FeatureStorage(BaseFeatureStorage):
1096
1204
 
1097
1205
  # Run loop to collect data per feature per file
1098
1206
  logger.info(
1099
- f"Collecting data from {self.uri.parent}/*_{self.uri.name}" # type: ignore
1207
+ f"Collecting data from {self.uri.parent}/*_{self.uri.name}"
1100
1208
  )
1101
1209
  logger.info(f"Will collect {len(elements_per_feature_md5)} features.")
1102
1210
 
@@ -1119,6 +1227,13 @@ class HDF5FeatureStorage(BaseFeatureStorage):
1119
1227
  elements = []
1120
1228
  static_data = None
1121
1229
  kind = None
1230
+ # The data is written in chunks to a temporary key (h5io would
1231
+ # delete the previous chunks), and moved to the feature at the end
1232
+ fname = str(self.uri.resolve())
1233
+ data_key = f"collecting_{feature_md5}"
1234
+ with h5py.File(fname, mode="a") as fid:
1235
+ if data_key in fid:
1236
+ del fid[data_key]
1122
1237
  for file_ in tqdm(element_files, desc="file-data"):
1123
1238
  logger.debug(
1124
1239
  f"Reading feature MD5: '{feature_md5}' "
@@ -1126,11 +1241,7 @@ class HDF5FeatureStorage(BaseFeatureStorage):
1126
1241
  )
1127
1242
 
1128
1243
  # Read the data
1129
- t_data = read_hdf5(
1130
- fname=str(file_),
1131
- title=feature_md5,
1132
- slash="ignore",
1133
- )
1244
+ t_data = _read_hdf5(fname=str(file_), title=feature_md5)
1134
1245
  if i_file == 0:
1135
1246
  # Store the "static" data
1136
1247
  static_data = {
@@ -1150,35 +1261,32 @@ class HDF5FeatureStorage(BaseFeatureStorage):
1150
1261
  i_file += 1
1151
1262
  if (i_file % t_chunk_size == 0) or i_file == element_count:
1152
1263
  # If we have reached the chunk size or the end of the
1153
- # elements, write the data
1154
-
1155
- # Store one chunk of data
1156
- to_write = static_data.copy()
1157
- to_write["element"] = []
1158
- # Write data in chunks to avoid memory usage spikes
1159
- # Start with the case for 2D
1160
- # Write chunked array
1161
- to_write["data"] = _create_chunk(
1264
+ # elements, write the data in chunks to avoid memory
1265
+ # usage spikes
1266
+ _write_chunk(
1267
+ fname=fname,
1268
+ key=data_key,
1162
1269
  chunk_data=chunk_data,
1163
1270
  kind=kind,
1164
1271
  element_count=element_count,
1165
1272
  chunk_size=t_chunk_size,
1166
1273
  i_chunk=i_chunk,
1167
1274
  )
1168
- if i_file == element_count:
1169
- to_write["element"] = elements
1170
-
1171
- # Write to HDF5
1172
- write_hdf5(
1173
- fname=str(self.uri.resolve()),
1174
- data=to_write,
1175
- overwrite="update",
1176
- compression=0,
1177
- title=feature_md5,
1178
- slash="error",
1179
- use_json=False,
1180
- )
1181
1275
 
1182
1276
  # Increment counters and data
1183
1277
  i_chunk += 1
1184
1278
  chunk_data = []
1279
+
1280
+ # Write the feature without the data
1281
+ write_hdf5(
1282
+ fname=fname,
1283
+ data={**static_data, "element": elements},
1284
+ overwrite="update",
1285
+ compression=0,
1286
+ title=feature_md5,
1287
+ slash="error",
1288
+ use_json=False,
1289
+ )
1290
+ # Move the data to the feature (as written by h5io)
1291
+ with h5py.File(fname, mode="a") as fid:
1292
+ fid.move(data_key, f"{feature_md5}/key_data")