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
@@ -6,14 +6,13 @@
6
6
 
7
7
  import json
8
8
  from collections.abc import Sequence
9
- from pathlib import Path
10
- from typing import ClassVar, Optional, Union
9
+ from typing import ClassVar
11
10
 
12
11
  import numpy as np
13
12
  import pandas as pd
14
13
 
15
14
  from ..utils import raise_error
16
- from .base import BaseFeatureStorage
15
+ from .base import BaseFeatureStorage, StorageType
17
16
 
18
17
 
19
18
  __all__ = ["PandasBaseFeatureStorage"]
@@ -38,19 +37,12 @@ class PandasBaseFeatureStorage(BaseFeatureStorage):
38
37
 
39
38
  """
40
39
 
41
- _STORAGE_TYPES: ClassVar[Sequence[str]] = [
42
- "vector",
43
- "timeseries",
44
- "matrix",
40
+ _STORAGE_TYPES: ClassVar[Sequence[StorageType]] = [
41
+ StorageType.Vector,
42
+ StorageType.Timeseries,
43
+ StorageType.Matrix,
45
44
  ]
46
45
 
47
- def __init__(
48
- self,
49
- uri: Union[str, Path],
50
- single_output: bool = True,
51
- ) -> None:
52
- super().__init__(uri=uri, single_output=single_output)
53
-
54
46
  def _meta_row(self, meta: dict, meta_md5: str) -> pd.DataFrame:
55
47
  """Convert the metadata to a pandas DataFrame.
56
48
 
@@ -75,8 +67,8 @@ class PandasBaseFeatureStorage(BaseFeatureStorage):
75
67
 
76
68
  @staticmethod
77
69
  def element_to_index(
78
- element: dict, n_rows: int = 1, rows_col_name: Optional[str] = None
79
- ) -> Union[pd.Index, pd.MultiIndex]:
70
+ element: dict, n_rows: int = 1, rows_col_name: str | None = None
71
+ ) -> pd.Index | pd.MultiIndex:
80
72
  """Convert the element metadata to index.
81
73
 
82
74
  Parameters
@@ -124,7 +116,11 @@ class PandasBaseFeatureStorage(BaseFeatureStorage):
124
116
  return index
125
117
 
126
118
  def store_df(
127
- self, meta_md5: str, element: dict, df: Union[pd.DataFrame, pd.Series]
119
+ self,
120
+ meta_md5: str,
121
+ element: dict,
122
+ df: pd.DataFrame | pd.Series,
123
+ processed_element: dict | None = None,
128
124
  ) -> None:
129
125
  """Implement pandas DataFrame storing.
130
126
 
@@ -136,6 +132,10 @@ class PandasBaseFeatureStorage(BaseFeatureStorage):
136
132
  The element as a dictionary.
137
133
  df : pandas.DataFrame or pandas.Series
138
134
  The pandas DataFrame or Series to store.
135
+ processed_element : dict or None, optional
136
+ The element being processed when the data was computed, used to
137
+ choose the file of each element if ``single_output=False``. If
138
+ None, ``element`` is used (default None).
139
139
 
140
140
  Raises
141
141
  ------
@@ -153,9 +153,10 @@ class PandasBaseFeatureStorage(BaseFeatureStorage):
153
153
  self,
154
154
  meta_md5: str,
155
155
  element: dict,
156
- data: Union[np.ndarray, list],
157
- col_names: Optional[Sequence[str]] = None,
158
- rows_col_name: Optional[str] = None,
156
+ data: np.ndarray | list,
157
+ col_names: Sequence[str] | None = None,
158
+ rows_col_name: str | None = None,
159
+ processed_element: dict | None = None,
159
160
  ) -> None:
160
161
  """Store 2D data.
161
162
 
@@ -173,6 +174,10 @@ class PandasBaseFeatureStorage(BaseFeatureStorage):
173
174
  The column name to use in case number of rows greater than 1.
174
175
  If None and number of rows greater than 1, then the name will be
175
176
  "idx" (default None).
177
+ processed_element : dict or None, optional
178
+ The element being processed when the data was computed, used to
179
+ choose the file of each element if ``single_output=False``. If
180
+ None, ``element`` is used (default None).
176
181
 
177
182
  """
178
183
  # Convert element metadata to index
@@ -186,14 +191,20 @@ class PandasBaseFeatureStorage(BaseFeatureStorage):
186
191
  index=idx, # type: ignore
187
192
  )
188
193
  # Store dataframe
189
- self.store_df(meta_md5=meta_md5, element=element, df=df)
194
+ self.store_df(
195
+ meta_md5=meta_md5,
196
+ element=element,
197
+ df=df,
198
+ processed_element=processed_element,
199
+ )
190
200
 
191
201
  def store_vector(
192
202
  self,
193
203
  meta_md5: str,
194
204
  element: dict,
195
- data: Union[np.ndarray, list],
196
- col_names: Optional[Sequence[str]] = None,
205
+ data: np.ndarray | list,
206
+ col_names: Sequence[str] | None = None,
207
+ processed_element: dict | None = None,
197
208
  ) -> None:
198
209
  """Store vector.
199
210
 
@@ -207,6 +218,10 @@ class PandasBaseFeatureStorage(BaseFeatureStorage):
207
218
  The vector data to store.
208
219
  col_names : list-like of str, optional
209
220
  The column labels (default None).
221
+ processed_element : dict or None, optional
222
+ The element being processed when the data was computed, used to
223
+ choose the file of each element if ``single_output=False``. If
224
+ None, ``element`` is used (default None).
210
225
 
211
226
  """
212
227
  if isinstance(data, list):
@@ -224,6 +239,7 @@ class PandasBaseFeatureStorage(BaseFeatureStorage):
224
239
  element=element,
225
240
  data=data,
226
241
  col_names=col_names,
242
+ processed_element=processed_element,
227
243
  )
228
244
 
229
245
  def store_timeseries(
@@ -231,7 +247,8 @@ class PandasBaseFeatureStorage(BaseFeatureStorage):
231
247
  meta_md5: str,
232
248
  element: dict,
233
249
  data: np.ndarray,
234
- col_names: Optional[Sequence[str]] = None,
250
+ col_names: Sequence[str] | None = None,
251
+ processed_element: dict | None = None,
235
252
  ) -> None:
236
253
  """Store timeseries.
237
254
 
@@ -245,6 +262,10 @@ class PandasBaseFeatureStorage(BaseFeatureStorage):
245
262
  The timeseries data to store.
246
263
  col_names : list-like of str, optional
247
264
  The column labels (default None).
265
+ processed_element : dict or None, optional
266
+ The element being processed when the data was computed, used to
267
+ choose the file of each element if ``single_output=False``. If
268
+ None, ``element`` is used (default None).
248
269
 
249
270
  """
250
271
  self._store_2d(
@@ -253,4 +274,5 @@ class PandasBaseFeatureStorage(BaseFeatureStorage):
253
274
  data=data,
254
275
  col_names=col_names,
255
276
  rows_col_name="timepoint",
277
+ processed_element=processed_element,
256
278
  )
junifer/storage/sqlite.py CHANGED
@@ -5,28 +5,40 @@
5
5
  # License: AGPL
6
6
 
7
7
  import json
8
- from collections.abc import Sequence
9
- from pathlib import Path
10
- from typing import TYPE_CHECKING, Any, Optional, Union
8
+ from enum import StrEnum
9
+ from typing import TYPE_CHECKING, Any, Optional
11
10
 
12
11
  import numpy as np
13
12
  import pandas as pd
14
13
  from pandas.core.base import NoNewAttributesMixin
15
14
  from pandas.io.sql import pandasSQL_builder
16
15
  from sqlalchemy import create_engine, inspect
16
+ from sqlalchemy.pool import NullPool
17
17
  from tqdm import tqdm
18
18
 
19
19
  from ..api.decorators import register_storage
20
- from ..utils import logger, raise_error, warn_with_log
20
+ from ..utils import raise_error, warn_with_log
21
+ from .base import MatrixKind, logger
21
22
  from .pandas_base import PandasBaseFeatureStorage
22
- from .utils import element_to_prefix, matrix_to_vector, store_matrix_checks
23
+ from .utils import (
24
+ element_to_prefix,
25
+ matrix_to_vector,
26
+ store_matrix_checks,
27
+ )
23
28
 
24
29
 
25
30
  if TYPE_CHECKING:
26
31
  from sqlalchemy.engine import Engine
27
32
 
28
33
 
29
- __all__ = ["SQLiteFeatureStorage"]
34
+ __all__ = ["SQLiteFeatureStorage", "Upsert"]
35
+
36
+
37
+ class Upsert(StrEnum):
38
+ """Accepted upsert value."""
39
+
40
+ Update = "update"
41
+ Ignore = "ignore"
30
42
 
31
43
 
32
44
  @register_storage
@@ -44,9 +56,11 @@ class SQLiteFeatureStorage(PandasBaseFeatureStorage):
44
56
  ``uri`` and store all the elements in the same file. This behaviour
45
57
  is only suitable for non-parallel executions. SQLite does not
46
58
  support concurrency (default True).
47
- upsert : {"ignore", "update"}, optional
48
- Upsert mode. If "ignore" is used, the existing elements are ignored.
49
- If "update", the existing elements are updated (default "update").
59
+ upsert : :enum:`.Upsert`, optional
60
+ Upsert mode. If ``Upsert.Ignore`` is used, the existing elements are
61
+ ignored, so the elements that are already stored are not stored
62
+ again. If ``Upsert.Update``, the existing elements are updated
63
+ (default ``Upsert.Ignore``).
50
64
 
51
65
  See Also
52
66
  --------
@@ -55,27 +69,9 @@ class SQLiteFeatureStorage(PandasBaseFeatureStorage):
55
69
 
56
70
  """
57
71
 
58
- def __init__(
59
- self,
60
- uri: Union[str, Path],
61
- single_output: bool = True,
62
- upsert: str = "update",
63
- ) -> None:
64
- # Check and set upsert argument value
65
- if upsert not in ["update", "ignore"]:
66
- raise_error(
67
- msg=(
68
- "Invalid choice for `upsert`. "
69
- "Must be either 'update' or 'ignore'."
70
- )
71
- )
72
- self.upsert = upsert
73
- super().__init__(
74
- uri=uri,
75
- single_output=single_output,
76
- )
72
+ upsert: Upsert = Upsert.Ignore
77
73
 
78
- def get_engine(self, element: Optional[dict] = None) -> "Engine":
74
+ def get_engine(self, element: dict | None = None) -> "Engine":
79
75
  """Get engine.
80
76
 
81
77
  Parameters
@@ -105,11 +101,11 @@ class SQLiteFeatureStorage(PandasBaseFeatureStorage):
105
101
  prefix = element_to_prefix(element)
106
102
  # Format URI for engine creation
107
103
  uri = f"sqlite:///{self.uri.parent}/{prefix}{self.uri.name}"
108
- return create_engine(uri, echo=False)
104
+ return create_engine(uri, echo=False, poolclass=NullPool)
109
105
 
110
106
  def _save_upsert(
111
107
  self,
112
- df: Union[pd.DataFrame, pd.Series],
108
+ df: pd.DataFrame | pd.Series,
113
109
  name: str,
114
110
  engine: Optional["Engine"] = None,
115
111
  if_exists: str = "append",
@@ -225,11 +221,9 @@ class SQLiteFeatureStorage(PandasBaseFeatureStorage):
225
221
 
226
222
  def read(
227
223
  self,
228
- feature_name: Optional[str] = None,
229
- feature_md5: Optional[str] = None,
230
- ) -> dict[
231
- str, Union[str, list[Union[int, str, dict[str, str]]], np.ndarray]
232
- ]:
224
+ feature_name: str | None = None,
225
+ feature_md5: str | None = None,
226
+ ) -> dict[str, str | list[int | str | dict[str, str]] | np.ndarray]:
233
227
  """Read stored feature.
234
228
 
235
229
  Parameters
@@ -261,8 +255,8 @@ class SQLiteFeatureStorage(PandasBaseFeatureStorage):
261
255
 
262
256
  def read_df(
263
257
  self,
264
- feature_name: Optional[str] = None,
265
- feature_md5: Optional[str] = None,
258
+ feature_name: str | None = None,
259
+ feature_md5: str | None = None,
266
260
  ) -> pd.DataFrame:
267
261
  """Implement feature reading into a pandas DataFrame.
268
262
 
@@ -347,7 +341,13 @@ class SQLiteFeatureStorage(PandasBaseFeatureStorage):
347
341
  df = df.set_index(index_names)
348
342
  return df
349
343
 
350
- def store_metadata(self, meta_md5: str, element: dict, meta: dict) -> None:
344
+ def store_metadata(
345
+ self,
346
+ meta_md5: str,
347
+ element: dict,
348
+ meta: dict,
349
+ processed_element: dict | None = None,
350
+ ) -> None:
351
351
  """Implement metadata storing in the storage.
352
352
 
353
353
  Parameters
@@ -358,10 +358,15 @@ class SQLiteFeatureStorage(PandasBaseFeatureStorage):
358
358
  The element as a dictionary.
359
359
  meta : dict
360
360
  The metadata as a dictionary.
361
+ processed_element : dict or None, optional
362
+ The element being processed when the data was computed, used to
363
+ choose the file of each element if ``single_output=False``. If
364
+ None, ``element`` is used (default None).
361
365
 
362
366
  """
363
367
  # Get sqlalchemy engine
364
- engine = self.get_engine(element=element)
368
+ # The file of the element being processed (if single_output=False)
369
+ engine = self.get_engine(element=processed_element or element)
365
370
  table_name = f"meta_{meta_md5}"
366
371
  if table_name not in inspect(engine).get_table_names():
367
372
  # Convert metadata to dataframe
@@ -370,7 +375,11 @@ class SQLiteFeatureStorage(PandasBaseFeatureStorage):
370
375
  self._save_upsert(meta_df, "meta", engine)
371
376
 
372
377
  def store_df(
373
- self, meta_md5: str, element: dict, df: Union[pd.DataFrame, pd.Series]
378
+ self,
379
+ meta_md5: str,
380
+ element: dict,
381
+ df: pd.DataFrame | pd.Series,
382
+ processed_element: dict | None = None,
374
383
  ) -> None:
375
384
  """Implement pandas DataFrame storing.
376
385
 
@@ -382,6 +391,10 @@ class SQLiteFeatureStorage(PandasBaseFeatureStorage):
382
391
  The element as a dictionary.
383
392
  df : pandas.DataFrame or pandas.Series
384
393
  The pandas DataFrame or Series to store.
394
+ processed_element : dict or None, optional
395
+ The element being processed when the data was computed, used to
396
+ choose the file of each element if ``single_output=False``. If
397
+ None, ``element`` is used (default None).
385
398
 
386
399
  Raises
387
400
  ------
@@ -416,7 +429,8 @@ class SQLiteFeatureStorage(PandasBaseFeatureStorage):
416
429
 
417
430
  table_name = f"meta_{meta_md5}"
418
431
  # Get sqlalchemy engine
419
- engine = self.get_engine(element)
432
+ # The file of the element being processed (if single_output=False)
433
+ engine = self.get_engine(element=processed_element or element)
420
434
  # Save data
421
435
  self._save_upsert(df, table_name, engine)
422
436
 
@@ -425,10 +439,11 @@ class SQLiteFeatureStorage(PandasBaseFeatureStorage):
425
439
  meta_md5: str,
426
440
  element: dict,
427
441
  data: np.ndarray,
428
- col_names: Optional[Sequence[str]] = None,
429
- row_names: Optional[Sequence[str]] = None,
430
- matrix_kind: str = "full",
442
+ col_names: list[str] | None = None,
443
+ row_names: list[str] | None = None,
444
+ matrix_kind: MatrixKind = MatrixKind.Full,
431
445
  diagonal: bool = True,
446
+ processed_element: dict | None = None,
432
447
  ) -> None:
433
448
  """Store matrix.
434
449
 
@@ -440,23 +455,19 @@ class SQLiteFeatureStorage(PandasBaseFeatureStorage):
440
455
  The element as a dictionary.
441
456
  data : numpy.ndarray
442
457
  The matrix data to store.
443
- meta : dict
444
- The metadata as a dictionary.
445
458
  col_names : list-like of str, optional
446
459
  The column labels (default None).
447
460
  row_names : list-like of str, optional
448
461
  The row labels (optional None).
449
- matrix_kind : str, optional
450
- The kind of matrix:
451
-
452
- * ``triu`` : store upper triangular only
453
- * ``tril`` : store lower triangular
454
- * ``full`` : full matrix
455
-
456
- (default "full").
462
+ matrix_kind : :enum:`.MatrixKind`, optional
463
+ The matrix kind (default ``MatrixKind.Full``).
457
464
  diagonal : bool, optional
458
- Whether to store the diagonal. If ``matrix_kind = full``, setting
459
- this to False will raise an error (default True).
465
+ Whether to store the diagonal. If ``matrix_kind=MatrixKind.Full``,
466
+ setting this to False will raise an error (default True).
467
+ processed_element : dict or None, optional
468
+ The element being processed when the data was computed, used to
469
+ choose the file of each element if ``single_output=False``. If
470
+ None, ``element`` is used (default None).
460
471
 
461
472
  """
462
473
  # Row data validation
@@ -508,7 +519,12 @@ class SQLiteFeatureStorage(PandasBaseFeatureStorage):
508
519
  data_df.index.names = new_names
509
520
 
510
521
  # Store dataframe
511
- self.store_df(meta_md5=meta_md5, element=element, df=data_df)
522
+ self.store_df(
523
+ meta_md5=meta_md5,
524
+ element=element,
525
+ df=data_df,
526
+ processed_element=processed_element,
527
+ )
512
528
 
513
529
  def collect(self) -> None:
514
530
  """Implement data collection.
@@ -526,9 +542,10 @@ class SQLiteFeatureStorage(PandasBaseFeatureStorage):
526
542
  )
527
543
  logger.info(f"Collecting data from {self.uri.parent}/*{self.uri.name}")
528
544
  # Create new instance
529
- out_storage = SQLiteFeatureStorage(uri=self.uri, upsert="ignore")
545
+ out_storage = SQLiteFeatureStorage(uri=self.uri, upsert=Upsert.Ignore)
530
546
  # Glob files
531
- files = self.uri.parent.glob(f"*{self.uri.name}")
547
+ # Only the files of the elements, not the collected file
548
+ files = self.uri.parent.glob(f"*_{self.uri.name}")
532
549
  for elem in tqdm(files, desc="file"):
533
550
  logger.debug(f"Reading from {elem.absolute()!s}")
534
551
  in_storage = SQLiteFeatureStorage(uri=elem)
@@ -539,6 +556,7 @@ class SQLiteFeatureStorage(PandasBaseFeatureStorage):
539
556
  )
540
557
  # Save metadata
541
558
  out_storage._save_upsert(t_meta_df, "meta")
559
+ in_features = in_storage.list_features()
542
560
  # Save dataframes
543
561
  for meta_md5 in tqdm(t_meta_df.index, desc="feature"):
544
562
  logger.debug(f"Collecting feature {meta_md5}")
@@ -546,8 +564,27 @@ class SQLiteFeatureStorage(PandasBaseFeatureStorage):
546
564
  # properly
547
565
  table_name = f"meta_{meta_md5}"
548
566
  t_df = in_storage.read_df(feature_md5=meta_md5)
567
+ # Only the files of single elements can be collected
568
+ element_keys = in_features[meta_md5]["_element_keys"]
569
+ n_elements = len(
570
+ t_df.index.to_frame(index=False)[
571
+ element_keys
572
+ ].drop_duplicates()
573
+ )
574
+ if n_elements != 1:
575
+ raise_error(
576
+ msg=(
577
+ f"The file {elem} has {n_elements} elements for "
578
+ f"the feature {meta_md5}, but only the files with "
579
+ "one element (as stored for each element with "
580
+ "`single_output=False`) can be collected"
581
+ ),
582
+ klass=RuntimeError,
583
+ )
549
584
  # Save data
550
- out_storage._save_upsert(t_df, table_name, if_exists="nocheck")
585
+ # Skip the elements already collected (e.g., the data of a
586
+ # subject in the files of each of its tasks)
587
+ out_storage._save_upsert(t_df, table_name)
551
588
 
552
589
 
553
590
  # TODO: refactor
@@ -582,7 +619,7 @@ def _generate_update_statements(table, index_col, rows_to_update):
582
619
  for i, (_, keys) in enumerate(pk_indb.iterrows()):
583
620
  stmt = (
584
621
  table.update()
585
- .where(and_(col == keys[j] for j, col in enumerate(pk_cols))) # type: ignore
622
+ .where(and_(col == keys.iloc[j] for j, col in enumerate(pk_cols))) # type: ignore
586
623
  .values(new_records[i])
587
624
  )
588
625
  stmts.append(stmt)
@@ -10,6 +10,7 @@ from pathlib import Path
10
10
  import h5py
11
11
  import numpy as np
12
12
  import pytest
13
+ from h5io import read_hdf5
13
14
  from numpy.testing import assert_array_equal
14
15
  from pandas.testing import assert_frame_equal
15
16
 
@@ -21,18 +22,6 @@ from junifer.storage.utils import (
21
22
  )
22
23
 
23
24
 
24
- def test_get_valid_inputs() -> None:
25
- """Test valid inputs."""
26
- storage = HDF5FeatureStorage(uri="/tmp")
27
- assert set(storage.get_valid_inputs()) == {
28
- "matrix",
29
- "vector",
30
- "timeseries",
31
- "scalar_table",
32
- "timeseries_2d",
33
- }
34
-
35
-
36
25
  def test_single_output(tmp_path: Path) -> None:
37
26
  """Test single output setup.
38
27
 
@@ -1178,3 +1167,149 @@ def test_collect_error_single_output() -> None:
1178
1167
  ):
1179
1168
  storage = HDF5FeatureStorage(uri="/tmp", single_output=True)
1180
1169
  storage.collect()
1170
+
1171
+
1172
+ def test_read_collected_with_h5io_fork() -> None:
1173
+ """Test reading features collected with junifer's fork of h5io.
1174
+
1175
+ The file was collected from three elements (``sub-01`` to ``sub-03``),
1176
+ each with a vector, a matrix and a timeseries computed from the element
1177
+ index ``i``.
1178
+
1179
+ """
1180
+ uri = Path(__file__).parent / "data" / "collected_h5io_fork.hdf5"
1181
+ storage = HDF5FeatureStorage(uri=uri)
1182
+ md5s = {meta["name"]: md5 for md5, meta in storage.list_features().items()}
1183
+ elements = [{"subject": f"sub-0{i}"} for i in range(1, 4)]
1184
+ # The vector data is stored as a "chunkedarray"
1185
+ vector = storage.read(feature_md5=md5s["BOLD_vec"])
1186
+ assert vector["element"] == elements
1187
+ assert vector["data"].dtype == np.float32
1188
+ for i in range(3):
1189
+ assert_array_equal(
1190
+ vector["data"][:, i], np.float32(np.array([0.1, 0.2]) + i)
1191
+ )
1192
+ # The matrix data is stored as a "chunkedarray"
1193
+ matrix = storage.read(feature_md5=md5s["BOLD_mat"])
1194
+ assert matrix["element"] == elements
1195
+ for i in range(3):
1196
+ assert_array_equal(
1197
+ matrix["data"][..., i], np.array([[1.0, 0.5], [0.5, 1.0]]) + i
1198
+ )
1199
+ # The timeseries data is stored as a "chunkedlist"
1200
+ timeseries = storage.read(feature_md5=md5s["BOLD_ts"])
1201
+ assert timeseries["element"] == elements
1202
+ assert len(timeseries["data"]) == 3
1203
+ for i in range(3):
1204
+ assert_array_equal(
1205
+ timeseries["data"][i], np.arange(6.0).reshape(3, 2) + i
1206
+ )
1207
+ assert storage.read_df(feature_md5=md5s["BOLD_ts"]).shape == (9, 2)
1208
+
1209
+
1210
+ def _store_and_collect(uri: Path, force_float32: bool) -> HDF5FeatureStorage:
1211
+ """Store a vector and a timeseries for three elements and collect them.
1212
+
1213
+ Parameters
1214
+ ----------
1215
+ uri : pathlib.Path
1216
+ The path to the collected file.
1217
+ force_float32 : bool
1218
+ Whether to store the data as float32.
1219
+
1220
+ Returns
1221
+ -------
1222
+ HDF5FeatureStorage
1223
+ The storage of the collected file.
1224
+
1225
+ """
1226
+ storage = HDF5FeatureStorage(
1227
+ uri=uri,
1228
+ single_output=False,
1229
+ force_float32=force_float32,
1230
+ chunk_size=2,
1231
+ )
1232
+ for i in range(3):
1233
+ meta = {
1234
+ "element": {"subject": f"sub-0{i}"},
1235
+ "dependencies": [],
1236
+ "type": "BOLD",
1237
+ }
1238
+ storage.store(
1239
+ kind="vector",
1240
+ meta={**meta, "marker": {"name": "vec"}},
1241
+ data=np.array([[0.1, 0.2]]) + i,
1242
+ col_names=["a", "b"],
1243
+ )
1244
+ storage.store(
1245
+ kind="timeseries",
1246
+ meta={**meta, "marker": {"name": "ts"}},
1247
+ data=np.arange(6.0).reshape(3, 2) + i,
1248
+ col_names=["a", "b"],
1249
+ )
1250
+ storage.collect()
1251
+ return HDF5FeatureStorage(uri=uri)
1252
+
1253
+
1254
+ @pytest.mark.parametrize(
1255
+ "force_float32, dtype", [(True, np.float32), (False, np.float64)]
1256
+ )
1257
+ def test_collect_dtype(
1258
+ tmp_path: Path, force_float32: bool, dtype: np.dtype
1259
+ ) -> None:
1260
+ """Test collected data keeps its dtype.
1261
+
1262
+ Parameters
1263
+ ----------
1264
+ tmp_path : pathlib.Path
1265
+ The path to the test directory.
1266
+ force_float32 : bool
1267
+ The parametrized casting of the data to float32.
1268
+ dtype : numpy.dtype
1269
+ The parametrized dtype of the collected data.
1270
+
1271
+ """
1272
+ storage = _store_and_collect(tmp_path / "out.hdf5", force_float32)
1273
+ md5s = {meta["name"]: md5 for md5, meta in storage.list_features().items()}
1274
+ vector = storage.read(feature_md5=md5s["BOLD_vec"])
1275
+ timeseries = storage.read(feature_md5=md5s["BOLD_ts"])
1276
+ assert vector["data"].dtype == dtype
1277
+ assert all(x.dtype == dtype for x in timeseries["data"])
1278
+ # The elements are collected in any order
1279
+ for i_element, element in enumerate(vector["element"]):
1280
+ i = int(element["subject"][-1])
1281
+ expected = np.array([0.1, 0.2]) + i
1282
+ assert_array_equal(
1283
+ vector["data"][:, i_element], expected.astype(dtype)
1284
+ )
1285
+
1286
+
1287
+ def test_collect_h5io_types(tmp_path: Path) -> None:
1288
+ """Test collected data is stored with h5io types and can be re-collected.
1289
+
1290
+ Parameters
1291
+ ----------
1292
+ tmp_path : pathlib.Path
1293
+ The path to the test directory.
1294
+
1295
+ """
1296
+ uri = tmp_path / "out.hdf5"
1297
+ storage = _store_and_collect(uri, force_float32=False)
1298
+ collected = {
1299
+ md5: read_hdf5(uri, title=md5) for md5 in storage.list_features()
1300
+ }
1301
+ # The data is only in the features
1302
+ with h5py.File(uri, mode="r") as fid:
1303
+ assert set(fid.keys()) == {"meta", *collected.keys()}
1304
+ # Collect again
1305
+ _store_and_collect(uri, force_float32=False)
1306
+ for md5, feature in collected.items():
1307
+ recollected = read_hdf5(uri, title=md5)
1308
+ assert recollected["element"] == feature["element"]
1309
+ if isinstance(feature["data"], list):
1310
+ for new, old in zip(
1311
+ recollected["data"], feature["data"], strict=True
1312
+ ):
1313
+ assert_array_equal(new, old)
1314
+ else:
1315
+ assert_array_equal(recollected["data"], feature["data"])