TwoPy 0.1.7__tar.gz → 0.1.8__tar.gz

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 (167) hide show
  1. {twopy-0.1.7 → twopy-0.1.8}/PKG-INFO +3 -3
  2. {twopy-0.1.7 → twopy-0.1.8}/README.md +2 -2
  3. {twopy-0.1.7 → twopy-0.1.8}/pyproject.toml +4 -1
  4. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/__init__.py +70 -0
  5. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/analysis/__init__.py +6 -1
  6. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/analysis/background_subtraction.py +53 -2
  7. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/analysis/dff_options.py +9 -4
  8. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/analysis/persistence.py +19 -73
  9. twopy-0.1.8/src/twopy/analysis/response_context.py +280 -0
  10. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/analysis/response_processing/persistence.py +19 -48
  11. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/analysis/trials.py +39 -0
  12. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/analysis/workflow.py +174 -142
  13. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/analysis_cache.py +2 -1
  14. twopy-0.1.8/src/twopy/calibrations/pixel_calibration_profiles.csv +4 -0
  15. twopy-0.1.8/src/twopy/calibrations/pixel_size.csv +21 -0
  16. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/config.py +36 -2
  17. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/conversion/api.py +3 -4
  18. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/conversion/hdf5_writing.py +6 -7
  19. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/converted.py +15 -49
  20. twopy-0.1.8/src/twopy/filenames.py +42 -0
  21. twopy-0.1.8/src/twopy/hdf5_utils.py +147 -0
  22. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/napari/controls.py +54 -23
  23. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/napari/database_search.py +1 -1
  24. twopy-0.1.8/src/twopy/napari/dims.py +113 -0
  25. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/napari/display_paths.py +42 -1
  26. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/napari/interactive.py +41 -11
  27. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/napari/launcher.py +5 -1
  28. twopy-0.1.8/src/twopy/napari/live_analysis.py +362 -0
  29. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/napari/loading.py +1 -1
  30. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/napari/paths.py +6 -5
  31. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/napari/plotting/data.py +48 -3
  32. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/napari/plotting/dff_options.py +43 -8
  33. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/napari/plotting/docks/options_panel.py +53 -20
  34. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/napari/plotting/docks/paths.py +14 -7
  35. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/napari/plotting/docks/plot_area.py +20 -2
  36. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/napari/plotting/docks/response_plot_widget.py +209 -11
  37. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/napari/plotting/docks/save_actions.py +3 -2
  38. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/napari/plotting/export.py +18 -28
  39. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/napari/plotting/export_controls.py +4 -0
  40. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/napari/plotting/label_visibility.py +58 -20
  41. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/napari/plotting/panels.py +24 -21
  42. twopy-0.1.8/src/twopy/napari/plotting/roi_generation/__init__.py +26 -0
  43. twopy-0.1.8/src/twopy/napari/plotting/roi_generation/actions.py +130 -0
  44. twopy-0.1.8/src/twopy/napari/plotting/roi_generation/controls.py +706 -0
  45. twopy-0.1.8/src/twopy/napari/plotting/roi_generation/options.py +62 -0
  46. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/napari/plotting/widgets.py +12 -0
  47. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/napari/responses.py +2 -0
  48. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/napari/roi.py +58 -1
  49. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/napari/session.py +49 -2
  50. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/napari/sidebar.py +7 -0
  51. twopy-0.1.8/src/twopy/napari/trial_timeline.py +719 -0
  52. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/napari/types.py +1 -0
  53. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/napari/viewer.py +14 -1
  54. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/parity/__init__.py +12 -0
  55. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/parity/psycho5.py +88 -2
  56. twopy-0.1.8/src/twopy/parity/roi_extraction.py +302 -0
  57. twopy-0.1.8/src/twopy/pixel_calibration.py +573 -0
  58. twopy-0.1.8/src/twopy/pixel_calibration_profiles.py +563 -0
  59. twopy-0.1.8/src/twopy/response_roi_extraction.py +750 -0
  60. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/roi.py +3 -21
  61. twopy-0.1.8/src/twopy/roi_extraction.py +550 -0
  62. twopy-0.1.8/src/twopy/roi_mask_cleanup.py +74 -0
  63. {twopy-0.1.7 → twopy-0.1.8}/src/twopy.egg-info/PKG-INFO +3 -3
  64. {twopy-0.1.7 → twopy-0.1.8}/src/twopy.egg-info/SOURCES.txt +26 -1
  65. {twopy-0.1.7 → twopy-0.1.8}/tests/test_background_subtraction.py +29 -0
  66. {twopy-0.1.7 → twopy-0.1.8}/tests/test_config.py +28 -0
  67. twopy-0.1.8/tests/test_filenames.py +102 -0
  68. twopy-0.1.8/tests/test_hdf5_utils.py +83 -0
  69. {twopy-0.1.7 → twopy-0.1.8}/tests/test_napari.py +1279 -23
  70. {twopy-0.1.7 → twopy-0.1.8}/tests/test_parity_psycho5.py +43 -0
  71. twopy-0.1.8/tests/test_parity_roi_extraction.py +183 -0
  72. twopy-0.1.8/tests/test_pixel_calibration.py +224 -0
  73. twopy-0.1.8/tests/test_pixel_calibration_profiles.py +226 -0
  74. twopy-0.1.8/tests/test_response_roi_extraction.py +354 -0
  75. twopy-0.1.8/tests/test_roi_extraction.py +171 -0
  76. twopy-0.1.8/tests/test_roi_mask_cleanup.py +65 -0
  77. {twopy-0.1.7 → twopy-0.1.8}/tests/test_trials.py +20 -0
  78. {twopy-0.1.7 → twopy-0.1.8}/tests/test_workflow.py +138 -0
  79. twopy-0.1.7/src/twopy/napari/constants.py +0 -13
  80. {twopy-0.1.7 → twopy-0.1.8}/setup.cfg +0 -0
  81. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/_segments.py +0 -0
  82. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/_version.py +0 -0
  83. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/analysis/dff.py +0 -0
  84. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/analysis/epoch_mapping.py +0 -0
  85. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/analysis/motion.py +0 -0
  86. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/analysis/response_processing/__init__.py +0 -0
  87. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/analysis/response_processing/apply.py +0 -0
  88. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/analysis/response_processing/options.py +0 -0
  89. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/analysis/response_processing/qc.py +0 -0
  90. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/analysis/response_processing/signals.py +0 -0
  91. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/analysis/response_window_options.py +0 -0
  92. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/analysis/responses.py +0 -0
  93. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/api.py +0 -0
  94. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/conversion/__init__.py +0 -0
  95. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/conversion/alignment_crop.py +0 -0
  96. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/conversion/matlab_values.py +0 -0
  97. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/conversion/source_loading.py +0 -0
  98. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/conversion/stimulus_code.py +0 -0
  99. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/conversion/types.py +0 -0
  100. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/database/__init__.py +0 -0
  101. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/database/cache.py +0 -0
  102. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/database/catalog.py +0 -0
  103. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/database/connection.py +0 -0
  104. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/database/generic.py +0 -0
  105. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/database/modeled.py +0 -0
  106. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/database/search.py +0 -0
  107. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/database/sql.py +0 -0
  108. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/database/types.py +0 -0
  109. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/frame_ranges.py +0 -0
  110. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/inspection.py +0 -0
  111. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/matlab.py +0 -0
  112. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/napari/__init__.py +0 -0
  113. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/napari/__main__.py +0 -0
  114. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/napari/display.py +0 -0
  115. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/napari/errors.py +0 -0
  116. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/napari/movie.py +0 -0
  117. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/napari/plotting/__init__.py +0 -0
  118. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/napari/plotting/docks/__init__.py +0 -0
  119. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/napari/plotting/docks/api.py +0 -0
  120. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/napari/plotting/docks/dynamic_options.py +0 -0
  121. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/napari/plotting/docks/visibility_state.py +0 -0
  122. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/napari/plotting/form_controls.py +0 -0
  123. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/napari/plotting/normalization_options.py +0 -0
  124. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/napari/plotting/options.py +0 -0
  125. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/napari/plotting/processing_options.py +0 -0
  126. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/napari/plotting/response_window_options.py +0 -0
  127. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/napari/protocols.py +0 -0
  128. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/napari/state.py +0 -0
  129. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/napari/text.py +0 -0
  130. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/parity/dff.py +0 -0
  131. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/parity/roi.py +0 -0
  132. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/parity/saved_analysis.py +0 -0
  133. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/photodiode.py +0 -0
  134. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/photodiode_classification.py +0 -0
  135. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/session.py +0 -0
  136. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/spatial.py +0 -0
  137. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/stimulus.py +0 -0
  138. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/synchronization.py +0 -0
  139. {twopy-0.1.7 → twopy-0.1.8}/src/twopy/typing_guards.py +0 -0
  140. {twopy-0.1.7 → twopy-0.1.8}/src/twopy.egg-info/dependency_links.txt +0 -0
  141. {twopy-0.1.7 → twopy-0.1.8}/src/twopy.egg-info/entry_points.txt +0 -0
  142. {twopy-0.1.7 → twopy-0.1.8}/src/twopy.egg-info/requires.txt +0 -0
  143. {twopy-0.1.7 → twopy-0.1.8}/src/twopy.egg-info/top_level.txt +0 -0
  144. {twopy-0.1.7 → twopy-0.1.8}/tests/test_api.py +0 -0
  145. {twopy-0.1.7 → twopy-0.1.8}/tests/test_conversion.py +0 -0
  146. {twopy-0.1.7 → twopy-0.1.8}/tests/test_converted.py +0 -0
  147. {twopy-0.1.7 → twopy-0.1.8}/tests/test_database.py +0 -0
  148. {twopy-0.1.7 → twopy-0.1.8}/tests/test_dff.py +0 -0
  149. {twopy-0.1.7 → twopy-0.1.8}/tests/test_epoch_mapping.py +0 -0
  150. {twopy-0.1.7 → twopy-0.1.8}/tests/test_frame_ranges.py +0 -0
  151. {twopy-0.1.7 → twopy-0.1.8}/tests/test_import.py +0 -0
  152. {twopy-0.1.7 → twopy-0.1.8}/tests/test_inspection.py +0 -0
  153. {twopy-0.1.7 → twopy-0.1.8}/tests/test_matlab.py +0 -0
  154. {twopy-0.1.7 → twopy-0.1.8}/tests/test_motion.py +0 -0
  155. {twopy-0.1.7 → twopy-0.1.8}/tests/test_packaging.py +0 -0
  156. {twopy-0.1.7 → twopy-0.1.8}/tests/test_parity.py +0 -0
  157. {twopy-0.1.7 → twopy-0.1.8}/tests/test_persistence.py +0 -0
  158. {twopy-0.1.7 → twopy-0.1.8}/tests/test_photodiode_classification.py +0 -0
  159. {twopy-0.1.7 → twopy-0.1.8}/tests/test_real_fixture.py +0 -0
  160. {twopy-0.1.7 → twopy-0.1.8}/tests/test_response_processing.py +0 -0
  161. {twopy-0.1.7 → twopy-0.1.8}/tests/test_response_window_options.py +0 -0
  162. {twopy-0.1.7 → twopy-0.1.8}/tests/test_responses.py +0 -0
  163. {twopy-0.1.7 → twopy-0.1.8}/tests/test_roi.py +0 -0
  164. {twopy-0.1.7 → twopy-0.1.8}/tests/test_saved_analysis.py +0 -0
  165. {twopy-0.1.7 → twopy-0.1.8}/tests/test_session.py +0 -0
  166. {twopy-0.1.7 → twopy-0.1.8}/tests/test_stimulus.py +0 -0
  167. {twopy-0.1.7 → twopy-0.1.8}/tests/test_synchronization.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: twopy
3
- Version: 0.1.7
3
+ Version: 0.1.8
4
4
  Summary: Two-photon imaging analysis tool with a napari interface.
5
5
  Author-email: Gustavo Madeira Santana <fromweb1@gumadeiras.com>
6
6
  License-Expression: MIT
@@ -64,10 +64,10 @@ twopy /path/to/recording_data.h5
64
64
 
65
65
  Basic GUI flow:
66
66
 
67
- 1. Choose a recording.
67
+ 1. Search the database or load a recording manually.
68
68
  2. Draw or edit ROIs in the `rois` Labels layer.
69
69
  3. Update response plots from the current ROIs.
70
- 4. Click Save ROIs + analysis when the plots look right.
70
+ 4. Click Save ROIs + analysis in Export when the plots look right.
71
71
 
72
72
  When a source recording has not been converted yet, twopy converts it first. Converted data includes the aligned movie, mean image, stimulus tables, photodiode signals, and recording metadata.
73
73
 
@@ -44,10 +44,10 @@ twopy /path/to/recording_data.h5
44
44
 
45
45
  Basic GUI flow:
46
46
 
47
- 1. Choose a recording.
47
+ 1. Search the database or load a recording manually.
48
48
  2. Draw or edit ROIs in the `rois` Labels layer.
49
49
  3. Update response plots from the current ROIs.
50
- 4. Click Save ROIs + analysis when the plots look right.
50
+ 4. Click Save ROIs + analysis in Export when the plots look right.
51
51
 
52
52
  When a source recording has not been converted yet, twopy converts it first. Converted data includes the aligned movie, mean image, stimulus tables, photodiode signals, and recording metadata.
53
53
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "twopy"
7
- version = "0.1.7"
7
+ version = "0.1.8"
8
8
  description = "Two-photon imaging analysis tool with a napari interface."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.13,<3.14"
@@ -32,6 +32,9 @@ twopy = "twopy.napari:main"
32
32
  [tool.setuptools.packages.find]
33
33
  where = ["src"]
34
34
 
35
+ [tool.setuptools.package-data]
36
+ twopy = ["calibrations/*.csv"]
37
+
35
38
  [tool.ruff]
36
39
  line-length = 88
37
40
  src = ["src", "tests"]
@@ -14,6 +14,7 @@ from twopy.analysis import (
14
14
  BackgroundCorrectedRoiTraces,
15
15
  BackgroundCorrectionMethod,
16
16
  CorrelationFilterOptions,
17
+ DeltaFOverFBaselineMode,
17
18
  DeltaFOverFFitMode,
18
19
  DeltaFOverFOptions,
19
20
  EpochFrameWindow,
@@ -35,6 +36,7 @@ from twopy.analysis import (
35
36
  compute_recording_responses,
36
37
  compute_roi_delta_f_over_f,
37
38
  default_baseline_epoch_number,
39
+ default_recording_baseline_epoch_number,
38
40
  extract_background_corrected_roi_traces,
39
41
  frame_windows_from_photodiode_alignment,
40
42
  group_delta_f_over_f_by_epoch,
@@ -43,6 +45,7 @@ from twopy.analysis import (
43
45
  load_analysis_outputs,
44
46
  make_frame_windows,
45
47
  map_stimulus_epochs_to_frame_windows,
48
+ no_baseline_epoch_frame_windows,
46
49
  resolve_baseline_frame_windows,
47
50
  save_analysis_outputs,
48
51
  select_baseline_frame_windows,
@@ -73,6 +76,33 @@ from twopy.photodiode_classification import (
73
76
  PhotodiodeEventType,
74
77
  classify_recording_photodiode_events,
75
78
  )
79
+ from twopy.pixel_calibration import (
80
+ DEFAULT_PIXEL_CALIBRATION_PATH,
81
+ PixelCalibrationResolution,
82
+ PixelCalibrationResolutionMethod,
83
+ PixelCalibrationRow,
84
+ load_pixel_calibrations,
85
+ resolve_pixel_size_um,
86
+ )
87
+ from twopy.pixel_calibration_profiles import (
88
+ DEFAULT_PIXEL_CALIBRATION_PROFILE_PATH,
89
+ PixelCalibrationGroup,
90
+ PixelCalibrationProfile,
91
+ PixelCalibrationProfileMapping,
92
+ PixelCalibrationProfileSource,
93
+ load_pixel_calibration_profile_mappings,
94
+ resolve_pixel_calibration_profile,
95
+ select_pixel_calibration_group,
96
+ )
97
+ from twopy.response_roi_extraction import (
98
+ ResponseNormalization,
99
+ ResponsePolarity,
100
+ ResponseStatistic,
101
+ ResponseWatershedExtraction,
102
+ ResponseWatershedScoreImages,
103
+ extract_response_watershed_rois,
104
+ response_watershed_roi_set,
105
+ )
76
106
  from twopy.roi import (
77
107
  RoiSet,
78
108
  RoiTraces,
@@ -83,6 +113,15 @@ from twopy.roi import (
83
113
  roi_set_to_label_image,
84
114
  save_roi_set,
85
115
  )
116
+ from twopy.roi_extraction import (
117
+ RoiExtractionConfig,
118
+ RoiExtractionMethod,
119
+ extract_rois_from_image,
120
+ grid_roi_set,
121
+ grid_roi_set_microns,
122
+ grid_size_pixels_from_microns,
123
+ watershed_roi_set,
124
+ )
86
125
  from twopy.spatial import SpatialCrop, SpatialDomain, full_frame_crop
87
126
  from twopy.stimulus import (
88
127
  StimulusSpecificColumnMapping,
@@ -119,13 +158,19 @@ __all__ = [
119
158
  "convert_recording_to_twopy",
120
159
  "DEFAULT_RESPONSE_POST_WINDOW_SECONDS",
121
160
  "DEFAULT_RESPONSE_PRE_WINDOW_SECONDS",
161
+ "DEFAULT_PIXEL_CALIBRATION_PATH",
162
+ "DEFAULT_PIXEL_CALIBRATION_PROFILE_PATH",
122
163
  "default_baseline_epoch_number",
164
+ "default_recording_baseline_epoch_number",
123
165
  "detect_photodiode_events",
124
166
  "detect_recording_photodiode_events",
167
+ "DeltaFOverFBaselineMode",
125
168
  "DeltaFOverFFitMode",
126
169
  "DeltaFOverFOptions",
127
170
  "EpochFrameWindow",
128
171
  "extract_background_corrected_roi_traces",
172
+ "extract_response_watershed_rois",
173
+ "extract_rois_from_image",
129
174
  "extract_roi_traces",
130
175
  "find_recordings",
131
176
  "frame_windows_from_photodiode_alignment",
@@ -134,12 +179,17 @@ __all__ = [
134
179
  "group_delta_f_over_f_by_epoch",
135
180
  "GroupedRoiResponseSummary",
136
181
  "GroupedRoiResponses",
182
+ "grid_roi_set",
183
+ "grid_roi_set_microns",
184
+ "grid_size_pixels_from_microns",
137
185
  "InterpolatedEpochMapping",
138
186
  "interpolate_stimulus_epochs_to_frame_windows",
139
187
  "is_baseline_epoch_name",
140
188
  "launch_napari",
141
189
  "load_analysis_outputs",
142
190
  "load_converted_recording",
191
+ "load_pixel_calibrations",
192
+ "load_pixel_calibration_profile_mappings",
143
193
  "load_roi_set",
144
194
  "load_source_conversion_inputs",
145
195
  "LoadedAnalysisOutputs",
@@ -148,6 +198,7 @@ __all__ = [
148
198
  "make_frame_windows",
149
199
  "make_roi_set",
150
200
  "map_stimulus_epochs_to_frame_windows",
201
+ "no_baseline_epoch_frame_windows",
151
202
  "map_stimulus_specific_column",
152
203
  "NapariRecordingView",
153
204
  "NapariSidebarWidgets",
@@ -158,9 +209,26 @@ __all__ = [
158
209
  "PhotodiodeEvent",
159
210
  "PhotodiodeEventType",
160
211
  "PhotodiodeEventSet",
212
+ "PixelCalibrationResolution",
213
+ "PixelCalibrationResolutionMethod",
214
+ "PixelCalibrationGroup",
215
+ "PixelCalibrationProfile",
216
+ "PixelCalibrationProfileMapping",
217
+ "PixelCalibrationProfileSource",
218
+ "PixelCalibrationRow",
219
+ "ResponseNormalization",
220
+ "ResponsePolarity",
161
221
  "ResponseProcessingOptions",
222
+ "ResponseStatistic",
223
+ "ResponseWatershedExtraction",
224
+ "ResponseWatershedScoreImages",
162
225
  "recording_frame_rate_hz",
226
+ "resolve_pixel_size_um",
227
+ "resolve_pixel_calibration_profile",
163
228
  "resolve_baseline_frame_windows",
229
+ "response_watershed_roi_set",
230
+ "RoiExtractionConfig",
231
+ "RoiExtractionMethod",
164
232
  "RoiDeltaFOverF",
165
233
  "RoiCorrelationScores",
166
234
  "RoiResponseSummary",
@@ -173,6 +241,7 @@ __all__ = [
173
241
  "save_napari_label_rois",
174
242
  "save_roi_set",
175
243
  "select_baseline_frame_windows",
244
+ "select_pixel_calibration_group",
176
245
  "split_traces_by_frame_windows",
177
246
  "SmoothingOptions",
178
247
  "SpatialCrop",
@@ -182,6 +251,7 @@ __all__ = [
182
251
  "summarize_epoch_roi_responses",
183
252
  "summarize_grouped_responses",
184
253
  "validate_grouped_roi_responses",
254
+ "watershed_roi_set",
185
255
  "WindowedRoiResponse",
186
256
  "write_response_summary_grouped_csv",
187
257
  "write_response_summary_trials_csv",
@@ -18,7 +18,7 @@ from twopy.analysis.dff import (
18
18
  RoiDeltaFOverF,
19
19
  compute_roi_delta_f_over_f,
20
20
  )
21
- from twopy.analysis.dff_options import DeltaFOverFOptions
21
+ from twopy.analysis.dff_options import DeltaFOverFBaselineMode, DeltaFOverFOptions
22
22
  from twopy.analysis.epoch_mapping import (
23
23
  InterpolatedEpochMapping,
24
24
  interpolate_stimulus_epochs_to_frame_windows,
@@ -32,6 +32,7 @@ from twopy.analysis.persistence import (
32
32
  write_response_summary_grouped_csv,
33
33
  write_response_summary_trials_csv,
34
34
  )
35
+ from twopy.analysis.response_context import default_recording_baseline_epoch_number
35
36
  from twopy.analysis.response_processing import (
36
37
  CorrelationFilterOptions,
37
38
  LowPassFilterOptions,
@@ -58,6 +59,7 @@ from twopy.analysis.trials import (
58
59
  is_baseline_epoch_name,
59
60
  make_frame_windows,
60
61
  map_stimulus_epochs_to_frame_windows,
62
+ no_baseline_epoch_frame_windows,
61
63
  resolve_baseline_frame_windows,
62
64
  select_baseline_frame_windows,
63
65
  split_traces_by_frame_windows,
@@ -85,6 +87,8 @@ __all__ = [
85
87
  "compute_recording_responses",
86
88
  "CorrelationFilterOptions",
87
89
  "default_baseline_epoch_number",
90
+ "default_recording_baseline_epoch_number",
91
+ "DeltaFOverFBaselineMode",
88
92
  "DeltaFOverFOptions",
89
93
  "DeltaFOverFFitMode",
90
94
  "EpochFrameWindow",
@@ -102,6 +106,7 @@ __all__ = [
102
106
  "LowPassFilterOptions",
103
107
  "make_frame_windows",
104
108
  "map_stimulus_epochs_to_frame_windows",
109
+ "no_baseline_epoch_frame_windows",
105
110
  "RoiDeltaFOverF",
106
111
  "ResponseProcessingOptions",
107
112
  "RoiCorrelationScores",
@@ -9,6 +9,7 @@ trace extraction stay in ``twopy.roi``; dF/F and trial analysis can consume the
9
9
  auditable outputs produced here.
10
10
  """
11
11
 
12
+ from collections.abc import Callable
12
13
  from dataclasses import dataclass
13
14
  from typing import Literal
14
15
 
@@ -75,6 +76,7 @@ def extract_background_corrected_roi_traces(
75
76
  local_y_radius: int | None = None,
76
77
  local_percentile: float = 20.0,
77
78
  spatial_domain: SpatialDomain = "alignment_valid_crop",
79
+ check_cancelled: Callable[[], None] | None = None,
78
80
  ) -> BackgroundCorrectedRoiTraces:
79
81
  """Extract ROI traces plus an explicit background correction.
80
82
 
@@ -108,6 +110,8 @@ def extract_background_corrected_roi_traces(
108
110
  uses the alignment-valid crop saved during conversion, matching the
109
111
  crop-domain analysis contract while keeping ROI masks stored in
110
112
  full-frame coordinates.
113
+ check_cancelled: Optional callback that raises ``CancelledError`` when
114
+ an interactive caller has superseded this computation.
111
115
 
112
116
  Returns:
113
117
  ``BackgroundCorrectedRoiTraces`` with raw, background, and corrected
@@ -115,7 +119,10 @@ def extract_background_corrected_roi_traces(
115
119
 
116
120
  Raises:
117
121
  ValueError: If masks do not match the movie or a background mask is
118
- empty.
122
+ empty. ROI y-stripe background correction also raises when the
123
+ selected spatial domain has no unlabeled pixels outside all ROIs,
124
+ which is expected for dense grid ROIs that cover the whole analysis
125
+ crop.
119
126
 
120
127
  The function streams movie chunks from HDF5 and never loads the full movie
121
128
  into memory. Each method records enough metadata to audit how the background
@@ -124,6 +131,7 @@ def extract_background_corrected_roi_traces(
124
131
  _validate_background_method(method)
125
132
  validate_trace_statistic(statistic)
126
133
  _validate_roi_movie_shape(roi_set, recording)
134
+ _check_cancelled(check_cancelled)
127
135
 
128
136
  frame_start, frame_stop = normalize_frame_range(
129
137
  frame_count=recording.movie.shape[0],
@@ -144,6 +152,7 @@ def extract_background_corrected_roi_traces(
144
152
  frame_stop=frame_stop,
145
153
  chunk_frames=chunk_frames,
146
154
  spatial_crop=crop,
155
+ check_cancelled=check_cancelled,
147
156
  )
148
157
  background_values = np.zeros_like(raw_values)
149
158
  corrected_values = raw_values.copy()
@@ -162,6 +171,7 @@ def extract_background_corrected_roi_traces(
162
171
  percentile=global_percentile,
163
172
  spatial_crop=crop,
164
173
  spatial_domain=spatial_domain,
174
+ check_cancelled=check_cancelled,
165
175
  )
166
176
  )
167
177
  elif method == "movie_y_stripe_percentile":
@@ -176,6 +186,7 @@ def extract_background_corrected_roi_traces(
176
186
  percentile=stripe_percentile,
177
187
  spatial_crop=crop,
178
188
  spatial_domain=spatial_domain,
189
+ check_cancelled=check_cancelled,
179
190
  )
180
191
  )
181
192
  elif method == "roi_y_stripe_percentile":
@@ -191,6 +202,7 @@ def extract_background_corrected_roi_traces(
191
202
  local_percentile=local_percentile,
192
203
  spatial_crop=crop,
193
204
  spatial_domain=spatial_domain,
205
+ check_cancelled=check_cancelled,
194
206
  )
195
207
  )
196
208
 
@@ -244,6 +256,7 @@ def _extract_mean_traces_from_indices(
244
256
  frame_stop: int,
245
257
  chunk_frames: int,
246
258
  spatial_crop: SpatialCrop,
259
+ check_cancelled: Callable[[], None] | None = None,
247
260
  ) -> npt.NDArray[np.float64]:
248
261
  """Extract mean traces for arbitrary flattened pixel-index groups.
249
262
 
@@ -255,6 +268,7 @@ def _extract_mean_traces_from_indices(
255
268
  frame_stop: Exclusive stop frame.
256
269
  chunk_frames: Maximum number of frames per HDF5 read.
257
270
  spatial_crop: Spatial domain read from each movie frame.
271
+ check_cancelled: Optional callback that raises when work is obsolete.
258
272
 
259
273
  Returns:
260
274
  Array shaped ``(frames, traces)``.
@@ -264,12 +278,14 @@ def _extract_mean_traces_from_indices(
264
278
  rather than total recording length.
265
279
  """
266
280
  traces = np.empty((frame_stop - frame_start, len(pixel_indices_by_trace)))
281
+ _check_cancelled(check_cancelled)
267
282
  for chunk_start, chunk_stop, frames in recording.movie.iter_frame_batches(
268
283
  chunk_frames=chunk_frames,
269
284
  start=frame_start,
270
285
  stop=frame_stop,
271
286
  spatial_crop=spatial_crop,
272
287
  ):
288
+ _check_cancelled(check_cancelled)
273
289
  chunk_offset = chunk_start - frame_start
274
290
  chunk_slice = slice(chunk_offset, chunk_offset + (chunk_stop - chunk_start))
275
291
  chunk_flat = frames.reshape(frames.shape[0], -1)
@@ -277,6 +293,7 @@ def _extract_mean_traces_from_indices(
277
293
  traces[chunk_slice, trace_index] = chunk_flat[:, pixel_indices].mean(
278
294
  axis=1,
279
295
  )
296
+ _check_cancelled(check_cancelled)
280
297
 
281
298
  return traces
282
299
 
@@ -291,6 +308,7 @@ def _extract_movie_global_percentile_corrected_traces(
291
308
  percentile: float,
292
309
  spatial_crop: SpatialCrop,
293
310
  spatial_domain: SpatialDomain,
311
+ check_cancelled: Callable[[], None] | None = None,
294
312
  ) -> tuple[
295
313
  npt.NDArray[np.float64],
296
314
  npt.NDArray[np.float64],
@@ -308,6 +326,7 @@ def _extract_movie_global_percentile_corrected_traces(
308
326
  percentile: Mean-image percentile used to define background pixels.
309
327
  spatial_crop: Spatial domain used for background and ROI pixels.
310
328
  spatial_domain: User-facing name of ``spatial_crop``.
329
+ check_cancelled: Optional callback that raises when work is obsolete.
311
330
 
312
331
  Returns:
313
332
  Raw ROI traces, repeated background traces, corrected ROI traces, and
@@ -317,6 +336,7 @@ def _extract_movie_global_percentile_corrected_traces(
317
336
  negatives to zero, and then averages corrected pixels inside each ROI.
318
337
  """
319
338
  _validate_percentile(percentile)
339
+ _check_cancelled(check_cancelled)
320
340
  mean_image = spatial_crop.crop_image(recording.mean_image)
321
341
  background_indices, threshold = _global_background_pixel_indices(
322
342
  mean_image,
@@ -334,6 +354,7 @@ def _extract_movie_global_percentile_corrected_traces(
334
354
  stop=frame_stop,
335
355
  spatial_crop=spatial_crop,
336
356
  ):
357
+ _check_cancelled(check_cancelled)
337
358
  chunk_offset = chunk_start - frame_start
338
359
  chunk_slice = slice(chunk_offset, chunk_offset + (chunk_stop - chunk_start))
339
360
  chunk_flat = frames.reshape(frames.shape[0], -1)
@@ -346,6 +367,7 @@ def _extract_movie_global_percentile_corrected_traces(
346
367
  # corrected ROI trace below zero.
347
368
  corrected_pixels = np.maximum(roi_pixels - background_trace[:, None], 0.0)
348
369
  corrected_values[chunk_slice, roi_index] = corrected_pixels.mean(axis=1)
370
+ _check_cancelled(check_cancelled)
349
371
 
350
372
  metadata: dict[str, BackgroundMetadataValue] = {
351
373
  "method": "movie_global_percentile",
@@ -369,6 +391,7 @@ def _extract_movie_y_stripe_percentile_corrected_traces(
369
391
  percentile: float,
370
392
  spatial_crop: SpatialCrop,
371
393
  spatial_domain: SpatialDomain,
394
+ check_cancelled: Callable[[], None] | None = None,
372
395
  ) -> tuple[
373
396
  npt.NDArray[np.float64],
374
397
  npt.NDArray[np.float64],
@@ -388,6 +411,7 @@ def _extract_movie_y_stripe_percentile_corrected_traces(
388
411
  percentile: Percentile used inside each frame stripe.
389
412
  spatial_crop: Spatial domain used for background and ROI pixels.
390
413
  spatial_domain: User-facing name of ``spatial_crop``.
414
+ check_cancelled: Optional callback that raises when work is obsolete.
391
415
 
392
416
  Returns:
393
417
  Raw ROI traces, stripe-field background traces, corrected traces, and
@@ -404,6 +428,7 @@ def _extract_movie_y_stripe_percentile_corrected_traces(
404
428
  near one ROI are safe neuropil reference pixels.
405
429
  """
406
430
  _validate_percentile(percentile)
431
+ _check_cancelled(check_cancelled)
407
432
  crop_shape = spatial_crop.crop_image(recording.mean_image).shape
408
433
  stripe_height = _resolve_stripe_y_height(
409
434
  row_count=crop_shape[0],
@@ -425,6 +450,7 @@ def _extract_movie_y_stripe_percentile_corrected_traces(
425
450
  stop=frame_stop,
426
451
  spatial_crop=spatial_crop,
427
452
  ):
453
+ _check_cancelled(check_cancelled)
428
454
  chunk_offset = chunk_start - frame_start
429
455
  chunk_slice = slice(chunk_offset, chunk_offset + (chunk_stop - chunk_start))
430
456
  background_field = _framewise_y_stripe_background_field(
@@ -443,6 +469,7 @@ def _extract_movie_y_stripe_percentile_corrected_traces(
443
469
  )
444
470
  corrected_pixels = np.maximum(roi_pixels - roi_background_pixels, 0.0)
445
471
  corrected_values[chunk_slice, roi_index] = corrected_pixels.mean(axis=1)
472
+ _check_cancelled(check_cancelled)
446
473
 
447
474
  metadata: dict[str, BackgroundMetadataValue] = {
448
475
  "method": "movie_y_stripe_percentile",
@@ -467,6 +494,7 @@ def _extract_roi_y_stripe_percentile_corrected_traces(
467
494
  local_percentile: float,
468
495
  spatial_crop: SpatialCrop,
469
496
  spatial_domain: SpatialDomain,
497
+ check_cancelled: Callable[[], None] | None = None,
470
498
  ) -> tuple[
471
499
  npt.NDArray[np.float64],
472
500
  npt.NDArray[np.float64],
@@ -487,6 +515,7 @@ def _extract_roi_y_stripe_percentile_corrected_traces(
487
515
  y-stripe.
488
516
  spatial_crop: Spatial domain used for background and ROI pixels.
489
517
  spatial_domain: User-facing name of ``spatial_crop``.
518
+ check_cancelled: Optional callback that raises when work is obsolete.
490
519
 
491
520
  Returns:
492
521
  Raw ROI traces, local background traces, corrected traces, and metadata.
@@ -508,6 +537,7 @@ def _extract_roi_y_stripe_percentile_corrected_traces(
508
537
  local_y_radius=local_y_radius,
509
538
  )
510
539
  _validate_percentile(local_percentile)
540
+ _check_cancelled(check_cancelled)
511
541
  background_indices_by_roi = _roi_y_stripe_background_pixel_indices(
512
542
  roi_masks=roi_masks,
513
543
  mean_image=spatial_crop.crop_image(recording.mean_image),
@@ -521,6 +551,7 @@ def _extract_roi_y_stripe_percentile_corrected_traces(
521
551
  frame_stop=frame_stop,
522
552
  chunk_frames=chunk_frames,
523
553
  spatial_crop=spatial_crop,
554
+ check_cancelled=check_cancelled,
524
555
  )
525
556
  background_values = _extract_mean_traces_from_indices(
526
557
  recording=recording,
@@ -529,6 +560,7 @@ def _extract_roi_y_stripe_percentile_corrected_traces(
529
560
  frame_stop=frame_stop,
530
561
  chunk_frames=chunk_frames,
531
562
  spatial_crop=spatial_crop,
563
+ check_cancelled=check_cancelled,
532
564
  )
533
565
  corrected_values = raw_values - background_values
534
566
  background_counts = tuple(
@@ -561,6 +593,13 @@ def _roi_pixel_indices_from_masks(
561
593
  return tuple(np.flatnonzero(mask).astype(np.int64) for mask in flat_masks)
562
594
 
563
595
 
596
+ def _check_cancelled(check_cancelled: Callable[[], None] | None) -> None:
597
+ """Raise ``CancelledError`` when an interactive caller superseded this work."""
598
+ if check_cancelled is None:
599
+ return
600
+ check_cancelled()
601
+
602
+
564
603
  def _crop_roi_masks_for_domain(
565
604
  roi_set: RoiSet,
566
605
  spatial_crop: SpatialCrop,
@@ -770,6 +809,15 @@ def _roi_y_stripe_background_pixel_indices(
770
809
  """
771
810
  occupied_mask = np.any(roi_masks, axis=0)
772
811
  background_mask = ~occupied_mask
812
+ if not np.any(background_mask):
813
+ msg = (
814
+ "ROI y-stripe P% background subtraction needs unlabeled background "
815
+ "pixels outside all ROIs in the selected spatial domain. The current "
816
+ "ROI masks cover every pixel, which is common for dense grid ROIs; "
817
+ "use shared y-stripe P% or leave unlabeled background pixels."
818
+ )
819
+ raise ValueError(msg)
820
+
773
821
  row_grid = np.arange(background_mask.shape[0])[:, None]
774
822
  background_indices: list[npt.NDArray[np.int64]] = []
775
823
 
@@ -782,7 +830,10 @@ def _roi_y_stripe_background_pixel_indices(
782
830
  if local_candidate_values.size == 0:
783
831
  msg = (
784
832
  f"ROI {roi_index} has no local background candidates within "
785
- f"{local_y_radius} rows"
833
+ f"{local_y_radius} rows. ROI y-stripe P% only uses unlabeled "
834
+ "pixels outside all ROIs; increase the local y radius, use "
835
+ "shared y-stripe P%, or leave unlabeled background pixels near "
836
+ "this ROI."
786
837
  )
787
838
  raise ValueError(msg)
788
839
  local_threshold = float(np.percentile(local_candidate_values, local_percentile))
@@ -7,19 +7,23 @@ without hiding the scientific workflow parameters.
7
7
  """
8
8
 
9
9
  from dataclasses import dataclass
10
+ from typing import Literal
10
11
 
11
12
  from twopy.analysis.background_subtraction import BackgroundCorrectionMethod
12
13
  from twopy.analysis.dff import DeltaFOverFFitMode
13
14
 
14
- __all__ = ["DeltaFOverFOptions"]
15
+ __all__ = ["DeltaFOverFBaselineMode", "DeltaFOverFOptions"]
16
+
17
+ DeltaFOverFBaselineMode = Literal["epoch", "no_baseline_epoch"]
15
18
 
16
19
 
17
20
  @dataclass(frozen=True)
18
21
  class DeltaFOverFOptions:
19
22
  """Options that control ROI dF/F computation before response grouping.
20
23
 
21
- Inputs: baseline epoch selection, background correction method, baseline
22
- window sampling, dF/F fit mode, and motion-artifact masking choice.
24
+ Inputs: baseline epoch selection, baseline mode, background correction
25
+ method, baseline window sampling, dF/F fit mode, and motion-artifact
26
+ masking choice.
23
27
  Outputs: typed parameters that map directly to
24
28
  ``compute_recording_responses`` and ``analyze_recording_responses``.
25
29
 
@@ -27,8 +31,9 @@ class DeltaFOverFOptions:
27
31
  response processing choices such as smoothing and correlation filtering.
28
32
  """
29
33
 
30
- baseline_epoch_number: int = 1
34
+ baseline_epoch_number: int | None = None
31
35
  baseline_epoch_name: str | None = None
36
+ baseline_mode: DeltaFOverFBaselineMode = "epoch"
32
37
  background_method: BackgroundCorrectionMethod = "movie_global_percentile"
33
38
  baseline_sample_seconds: float | None = 1.0
34
39
  fit_mode: DeltaFOverFFitMode = "direct_bounded_tau"
@@ -38,6 +38,15 @@ from twopy.analysis.responses import (
38
38
  validate_grouped_roi_responses,
39
39
  )
40
40
  from twopy.analysis.trials import EpochFrameWindow, FrameWindow
41
+ from twopy.hdf5_utils import (
42
+ plain_hdf5_attr_value,
43
+ )
44
+ from twopy.hdf5_utils import (
45
+ read_string_dataset as _read_string_dataset,
46
+ )
47
+ from twopy.hdf5_utils import (
48
+ write_string_dataset as _write_string_dataset,
49
+ )
41
50
  from twopy.roi import RoiSet, TraceStatistic, make_roi_set
42
51
  from twopy.typing_guards import (
43
52
  require_bool_array,
@@ -418,12 +427,12 @@ def _read_background_traces(group: h5py.Group) -> BackgroundCorrectedRoiTraces:
418
427
  start_frame=int(group.attrs["start_frame"]),
419
428
  stop_frame=int(group.attrs["stop_frame"]),
420
429
  statistic=require_string_choice(
421
- _plain_attr_value(group.attrs["statistic"]),
430
+ plain_hdf5_attr_value(group.attrs["statistic"], scalar_only=True),
422
431
  name="traces statistic",
423
432
  allowed=_TRACE_STATISTICS,
424
433
  ),
425
434
  method=require_string_choice(
426
- _plain_attr_value(group.attrs["method"]),
435
+ plain_hdf5_attr_value(group.attrs["method"], scalar_only=True),
427
436
  name="traces background correction method",
428
437
  allowed=_BACKGROUND_CORRECTION_METHODS,
429
438
  ),
@@ -704,7 +713,9 @@ def _read_response_trial(group: h5py.Group) -> RoiResponseTrial:
704
713
  """
705
714
  return RoiResponseTrial(
706
715
  epoch_number=int(group.attrs["epoch_number"]),
707
- epoch_name=str(_plain_attr_value(group.attrs["epoch_name"])),
716
+ epoch_name=str(
717
+ plain_hdf5_attr_value(group.attrs["epoch_name"], scalar_only=True),
718
+ ),
708
719
  trial_index=int(group.attrs["trial_index"]),
709
720
  window_index=int(group.attrs["window_index"]),
710
721
  start_frame=int(group.attrs["start_frame"]),
@@ -789,41 +800,10 @@ def _read_metadata(group: h5py.Group) -> dict[str, str | int | float | bool]:
789
800
  Returns:
790
801
  Plain Python metadata dictionary.
791
802
  """
792
- return {key: _plain_attr_value(value) for key, value in group.attrs.items()}
793
-
794
-
795
- def _write_string_dataset(
796
- group: h5py.Group,
797
- name: str,
798
- values: tuple[str, ...],
799
- ) -> None:
800
- """Write a tuple of strings as a UTF-8 HDF5 dataset.
801
-
802
- Args:
803
- group: Destination HDF5 group.
804
- name: Dataset name.
805
- values: String values to write.
806
-
807
- Returns:
808
- None.
809
- """
810
- group.create_dataset(
811
- name,
812
- data=np.asarray(values, dtype=h5py.string_dtype("utf-8")),
813
- )
814
-
815
-
816
- def _read_string_dataset(group: h5py.Group, name: str) -> tuple[str, ...]:
817
- """Read a UTF-8 string dataset.
818
-
819
- Args:
820
- group: HDF5 group containing the dataset.
821
- name: Dataset name.
822
-
823
- Returns:
824
- Tuple of decoded strings.
825
- """
826
- return tuple(_decode_string(value) for value in group[name][()])
803
+ return {
804
+ key: plain_hdf5_attr_value(value, scalar_only=True)
805
+ for key, value in group.attrs.items()
806
+ }
827
807
 
828
808
 
829
809
  def _time_columns(trials: Sequence[RoiResponseTrial]) -> tuple[str, ...]:
@@ -1045,41 +1025,7 @@ def _require_analysis_format(h5_file: h5py.File, path: Path) -> None:
1045
1025
  Returns:
1046
1026
  None.
1047
1027
  """
1048
- actual = str(_plain_attr_value(h5_file.attrs.get("twopy_format", "")))
1028
+ actual = str(plain_hdf5_attr_value(h5_file.attrs.get("twopy_format", "")))
1049
1029
  if actual != ANALYSIS_OUTPUT_FILE_FORMAT:
1050
1030
  msg = f"Expected {ANALYSIS_OUTPUT_FILE_FORMAT!r} file at {path}"
1051
1031
  raise ValueError(msg)
1052
-
1053
-
1054
- def _plain_attr_value(value: object) -> str | int | float | bool:
1055
- """Convert one HDF5 attribute value to a plain Python scalar.
1056
-
1057
- Args:
1058
- value: Raw HDF5 attribute value.
1059
-
1060
- Returns:
1061
- Plain string, integer, float, or boolean.
1062
- """
1063
- if isinstance(value, bytes):
1064
- return value.decode("utf-8")
1065
- if isinstance(value, np.bool_ | bool):
1066
- return bool(value)
1067
- if isinstance(value, np.integer | int):
1068
- return int(value)
1069
- if isinstance(value, np.floating | float):
1070
- return float(value)
1071
- return str(value)
1072
-
1073
-
1074
- def _decode_string(value: object) -> str:
1075
- """Decode one HDF5 string scalar.
1076
-
1077
- Args:
1078
- value: Raw scalar read from a string dataset.
1079
-
1080
- Returns:
1081
- Decoded text.
1082
- """
1083
- if isinstance(value, bytes):
1084
- return value.decode("utf-8")
1085
- return str(value)