TwoPy 0.2.0__tar.gz → 0.3.0__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 (243) hide show
  1. {twopy-0.2.0 → twopy-0.3.0}/PKG-INFO +8 -4
  2. {twopy-0.2.0 → twopy-0.3.0}/README.md +5 -3
  3. {twopy-0.2.0 → twopy-0.3.0}/pyproject.toml +9 -1
  4. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/analysis/background_subtraction.py +120 -21
  5. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/analysis/dff.py +33 -0
  6. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/analysis/kernels/__init__.py +2 -2
  7. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/analysis/persistence.py +19 -4
  8. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/analysis/response_maps.py +57 -7
  9. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/analysis_cache.py +86 -23
  10. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/config.py +47 -8
  11. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/conversion/alignment_crop.py +18 -13
  12. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/conversion/api.py +21 -5
  13. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/conversion/hdf5_writing.py +27 -4
  14. twopy-0.3.0/src/twopy/conversion/orientation.py +80 -0
  15. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/conversion/types.py +1 -1
  16. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/converted.py +26 -3
  17. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/custom/__init__.py +8 -0
  18. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/custom/native_workflows/direction_selectivity.py +13 -12
  19. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/custom/native_workflows/response_kernels.py +2 -7
  20. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/custom/parameters.py +4 -3
  21. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/custom/provenance.py +14 -0
  22. twopy-0.3.0/src/twopy/custom/runner.py +244 -0
  23. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/custom/types.py +247 -8
  24. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/database/search.py +1 -1
  25. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/napari/controls.py +147 -26
  26. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/napari/custom_tab.py +168 -20
  27. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/napari/database_search.py +41 -1
  28. twopy-0.3.0/src/twopy/napari/display.py +106 -0
  29. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/napari/display_paths.py +26 -0
  30. twopy-0.3.0/src/twopy/napari/group_matching/__init__.py +29 -0
  31. twopy-0.3.0/src/twopy/napari/group_matching/cards.py +127 -0
  32. twopy-0.3.0/src/twopy/napari/group_matching/fov_assignment.py +679 -0
  33. twopy-0.3.0/src/twopy/napari/group_matching/fov_cards.py +143 -0
  34. twopy-0.2.0/src/twopy/napari/group_matching_images.py → twopy-0.3.0/src/twopy/napari/group_matching/images.py +2 -2
  35. twopy-0.3.0/src/twopy/napari/group_matching/response_preview.py +334 -0
  36. twopy-0.3.0/src/twopy/napari/group_matching/responses.py +130 -0
  37. twopy-0.2.0/src/twopy/napari/group_matching_roi.py → twopy-0.3.0/src/twopy/napari/group_matching/roi_assignment.py +590 -532
  38. twopy-0.3.0/src/twopy/napari/group_matching/roi_cards.py +371 -0
  39. twopy-0.3.0/src/twopy/napari/group_matching/style.py +269 -0
  40. twopy-0.3.0/src/twopy/napari/group_matching/tables.py +27 -0
  41. twopy-0.3.0/src/twopy/napari/group_matching/window.py +267 -0
  42. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/napari/live_analysis.py +75 -32
  43. twopy-0.3.0/src/twopy/napari/load_controller.py +400 -0
  44. twopy-0.3.0/src/twopy/napari/load_progress.py +168 -0
  45. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/napari/load_workflow.py +225 -83
  46. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/napari/loaded_recordings_csv.py +35 -4
  47. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/napari/loading.py +59 -3
  48. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/napari/plotting/dff_options.py +1 -0
  49. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/napari/plotting/docks/dynamic_options.py +60 -14
  50. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/napari/plotting/docks/options_panel.py +15 -16
  51. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/napari/plotting/docks/paths.py +5 -6
  52. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/napari/plotting/docks/plot_area.py +42 -0
  53. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/napari/plotting/docks/response_map_area.py +5 -8
  54. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/napari/plotting/docks/response_plot_widget.py +255 -101
  55. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/napari/plotting/docks/visibility_state.py +29 -2
  56. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/napari/plotting/export.py +11 -19
  57. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/napari/plotting/export_controls.py +5 -6
  58. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/napari/plotting/normalization_options.py +1 -0
  59. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/napari/plotting/options.py +1 -1
  60. twopy-0.3.0/src/twopy/napari/plotting/panels.py +188 -0
  61. twopy-0.3.0/src/twopy/napari/plotting/preview_strip.py +218 -0
  62. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/napari/plotting/processing_options.py +1 -0
  63. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/napari/plotting/widgets.py +403 -26
  64. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/napari/responses.py +58 -1
  65. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/napari/roi.py +91 -22
  66. twopy-0.3.0/src/twopy/napari/roi_signatures.py +20 -0
  67. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/napari/session.py +20 -3
  68. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/napari/sidebar.py +27 -14
  69. twopy-0.3.0/src/twopy/napari/state.py +192 -0
  70. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/napari/viewer.py +206 -67
  71. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/roi.py +26 -5
  72. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/spatial.py +3 -3
  73. twopy-0.3.0/src/twopy/spatial_orientation.py +78 -0
  74. {twopy-0.2.0 → twopy-0.3.0}/src/twopy.egg-info/PKG-INFO +8 -4
  75. {twopy-0.2.0 → twopy-0.3.0}/src/twopy.egg-info/SOURCES.txt +23 -3
  76. {twopy-0.2.0 → twopy-0.3.0}/tests/test_background_subtraction.py +22 -1
  77. {twopy-0.2.0 → twopy-0.3.0}/tests/test_config.py +28 -0
  78. {twopy-0.2.0 → twopy-0.3.0}/tests/test_conversion.py +113 -10
  79. {twopy-0.2.0 → twopy-0.3.0}/tests/test_converted.py +64 -2
  80. {twopy-0.2.0 → twopy-0.3.0}/tests/test_custom_workflows.py +301 -19
  81. {twopy-0.2.0 → twopy-0.3.0}/tests/test_dff.py +33 -3
  82. twopy-0.3.0/tests/test_function_inventory.py +196 -0
  83. {twopy-0.2.0 → twopy-0.3.0}/tests/test_kernels.py +132 -1
  84. {twopy-0.2.0 → twopy-0.3.0}/tests/test_load_workflow.py +58 -51
  85. {twopy-0.2.0 → twopy-0.3.0}/tests/test_napari.py +229 -46
  86. {twopy-0.2.0 → twopy-0.3.0}/tests/test_napari_database_search.py +60 -2
  87. {twopy-0.2.0 → twopy-0.3.0}/tests/test_napari_display.py +12 -31
  88. {twopy-0.2.0 → twopy-0.3.0}/tests/test_napari_export.py +77 -5
  89. twopy-0.3.0/tests/test_napari_group_matching_responses.py +140 -0
  90. twopy-0.3.0/tests/test_napari_group_matching_roi_cards.py +156 -0
  91. twopy-0.3.0/tests/test_napari_load_progress.py +48 -0
  92. {twopy-0.2.0 → twopy-0.3.0}/tests/test_napari_load_tab.py +207 -4
  93. {twopy-0.2.0 → twopy-0.3.0}/tests/test_napari_loaded_recordings.py +406 -18
  94. {twopy-0.2.0 → twopy-0.3.0}/tests/test_napari_path_resolution.py +42 -2
  95. {twopy-0.2.0 → twopy-0.3.0}/tests/test_napari_plot_controls.py +104 -1
  96. twopy-0.3.0/tests/test_napari_plot_widgets.py +1808 -0
  97. {twopy-0.2.0 → twopy-0.3.0}/tests/test_napari_processing_controls.py +100 -3
  98. {twopy-0.2.0 → twopy-0.3.0}/tests/test_napari_response_workflow.py +4 -2
  99. {twopy-0.2.0 → twopy-0.3.0}/tests/test_napari_roi_generation.py +76 -3
  100. {twopy-0.2.0 → twopy-0.3.0}/tests/test_napari_roi_visibility.py +26 -0
  101. {twopy-0.2.0 → twopy-0.3.0}/tests/test_persistence.py +35 -1
  102. {twopy-0.2.0 → twopy-0.3.0}/tests/test_response_maps.py +115 -11
  103. {twopy-0.2.0 → twopy-0.3.0}/tests/test_roi.py +31 -1
  104. twopy-0.2.0/src/twopy/napari/display.py +0 -181
  105. twopy-0.2.0/src/twopy/napari/group_matching.py +0 -596
  106. twopy-0.2.0/src/twopy/napari/plotting/panels.py +0 -103
  107. twopy-0.2.0/src/twopy/napari/state.py +0 -106
  108. twopy-0.2.0/tests/test_napari_plot_widgets.py +0 -799
  109. {twopy-0.2.0 → twopy-0.3.0}/setup.cfg +0 -0
  110. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/__init__.py +0 -0
  111. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/_segments.py +0 -0
  112. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/_version.py +0 -0
  113. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/analysis/__init__.py +0 -0
  114. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/analysis/dff_options.py +0 -0
  115. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/analysis/epoch_mapping.py +0 -0
  116. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/analysis/group_matching.py +0 -0
  117. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/analysis/kernels/_fitting.py +0 -0
  118. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/analysis/kernels/_stimulus.py +0 -0
  119. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/analysis/motion.py +0 -0
  120. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/analysis/response_context.py +0 -0
  121. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/analysis/response_processing/__init__.py +0 -0
  122. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/analysis/response_processing/apply.py +0 -0
  123. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/analysis/response_processing/options.py +0 -0
  124. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/analysis/response_processing/persistence.py +0 -0
  125. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/analysis/response_processing/qc.py +0 -0
  126. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/analysis/response_processing/signals.py +0 -0
  127. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/analysis/response_window_options.py +0 -0
  128. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/analysis/responses.py +0 -0
  129. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/analysis/timing.py +0 -0
  130. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/analysis/trials.py +0 -0
  131. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/analysis/workflow.py +0 -0
  132. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/api.py +0 -0
  133. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/calibrations/pixel_calibration_profiles.csv +0 -0
  134. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/calibrations/pixel_size.csv +0 -0
  135. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/conversion/__init__.py +0 -0
  136. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/conversion/matlab_values.py +0 -0
  137. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/conversion/source_loading.py +0 -0
  138. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/conversion/stimulus_code.py +0 -0
  139. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/custom/discovery.py +0 -0
  140. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/custom/native.py +0 -0
  141. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/custom/native_workflows/__init__.py +0 -0
  142. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/custom/registry.py +0 -0
  143. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/database/__init__.py +0 -0
  144. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/database/cache.py +0 -0
  145. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/database/catalog.py +0 -0
  146. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/database/connection.py +0 -0
  147. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/database/generic.py +0 -0
  148. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/database/modeled.py +0 -0
  149. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/database/sql.py +0 -0
  150. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/database/types.py +0 -0
  151. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/filenames.py +0 -0
  152. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/frame_ranges.py +0 -0
  153. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/hdf5_utils.py +0 -0
  154. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/inspection.py +0 -0
  155. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/matlab.py +0 -0
  156. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/napari/__init__.py +0 -0
  157. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/napari/__main__.py +0 -0
  158. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/napari/database_favorites.py +0 -0
  159. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/napari/dims.py +0 -0
  160. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/napari/errors.py +0 -0
  161. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/napari/interactive.py +0 -0
  162. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/napari/latest_worker.py +0 -0
  163. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/napari/launcher.py +0 -0
  164. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/napari/movie.py +0 -0
  165. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/napari/paths.py +0 -0
  166. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/napari/plotting/__init__.py +0 -0
  167. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/napari/plotting/data.py +0 -0
  168. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/napari/plotting/docks/__init__.py +0 -0
  169. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/napari/plotting/docks/api.py +0 -0
  170. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/napari/plotting/docks/save_actions.py +0 -0
  171. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/napari/plotting/form_controls.py +0 -0
  172. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/napari/plotting/label_visibility.py +0 -0
  173. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/napari/plotting/response_map_colors.py +0 -0
  174. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/napari/plotting/response_map_display.py +0 -0
  175. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/napari/plotting/response_map_options.py +0 -0
  176. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/napari/plotting/response_window_options.py +0 -0
  177. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/napari/plotting/roi_generation/__init__.py +0 -0
  178. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/napari/plotting/roi_generation/actions.py +0 -0
  179. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/napari/plotting/roi_generation/controls.py +0 -0
  180. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/napari/plotting/roi_generation/options.py +0 -0
  181. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/napari/protocols.py +0 -0
  182. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/napari/text.py +0 -0
  183. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/napari/trial_timeline.py +0 -0
  184. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/napari/types.py +0 -0
  185. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/parity/__init__.py +0 -0
  186. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/parity/dff.py +0 -0
  187. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/parity/psycho5.py +0 -0
  188. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/parity/roi.py +0 -0
  189. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/parity/roi_extraction.py +0 -0
  190. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/parity/saved_analysis.py +0 -0
  191. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/photodiode.py +0 -0
  192. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/photodiode_classification.py +0 -0
  193. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/pixel_calibration.py +0 -0
  194. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/pixel_calibration_profiles.py +0 -0
  195. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/response_roi_extraction.py +0 -0
  196. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/roi_extraction.py +0 -0
  197. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/roi_mask_cleanup.py +0 -0
  198. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/session.py +0 -0
  199. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/stimulus.py +0 -0
  200. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/synchronization.py +0 -0
  201. {twopy-0.2.0 → twopy-0.3.0}/src/twopy/typing_guards.py +0 -0
  202. {twopy-0.2.0 → twopy-0.3.0}/src/twopy.egg-info/dependency_links.txt +0 -0
  203. {twopy-0.2.0 → twopy-0.3.0}/src/twopy.egg-info/entry_points.txt +0 -0
  204. {twopy-0.2.0 → twopy-0.3.0}/src/twopy.egg-info/requires.txt +0 -0
  205. {twopy-0.2.0 → twopy-0.3.0}/src/twopy.egg-info/top_level.txt +0 -0
  206. {twopy-0.2.0 → twopy-0.3.0}/tests/test_api.py +0 -0
  207. {twopy-0.2.0 → twopy-0.3.0}/tests/test_database.py +0 -0
  208. {twopy-0.2.0 → twopy-0.3.0}/tests/test_epoch_mapping.py +0 -0
  209. {twopy-0.2.0 → twopy-0.3.0}/tests/test_filenames.py +0 -0
  210. {twopy-0.2.0 → twopy-0.3.0}/tests/test_frame_ranges.py +0 -0
  211. {twopy-0.2.0 → twopy-0.3.0}/tests/test_group_matching.py +0 -0
  212. {twopy-0.2.0 → twopy-0.3.0}/tests/test_hdf5_utils.py +0 -0
  213. {twopy-0.2.0 → twopy-0.3.0}/tests/test_import.py +0 -0
  214. {twopy-0.2.0 → twopy-0.3.0}/tests/test_inspection.py +0 -0
  215. {twopy-0.2.0 → twopy-0.3.0}/tests/test_matlab.py +0 -0
  216. {twopy-0.2.0 → twopy-0.3.0}/tests/test_motion.py +0 -0
  217. {twopy-0.2.0 → twopy-0.3.0}/tests/test_napari_live_controller.py +0 -0
  218. {twopy-0.2.0 → twopy-0.3.0}/tests/test_napari_plot_data.py +0 -0
  219. {twopy-0.2.0 → twopy-0.3.0}/tests/test_napari_response_map_display.py +0 -0
  220. {twopy-0.2.0 → twopy-0.3.0}/tests/test_napari_text.py +0 -0
  221. {twopy-0.2.0 → twopy-0.3.0}/tests/test_napari_timeline.py +0 -0
  222. {twopy-0.2.0 → twopy-0.3.0}/tests/test_packaging.py +0 -0
  223. {twopy-0.2.0 → twopy-0.3.0}/tests/test_parity.py +0 -0
  224. {twopy-0.2.0 → twopy-0.3.0}/tests/test_parity_psycho5.py +0 -0
  225. {twopy-0.2.0 → twopy-0.3.0}/tests/test_parity_roi_extraction.py +0 -0
  226. {twopy-0.2.0 → twopy-0.3.0}/tests/test_photodiode_classification.py +0 -0
  227. {twopy-0.2.0 → twopy-0.3.0}/tests/test_pixel_calibration.py +0 -0
  228. {twopy-0.2.0 → twopy-0.3.0}/tests/test_pixel_calibration_profiles.py +0 -0
  229. {twopy-0.2.0 → twopy-0.3.0}/tests/test_real_fixture.py +0 -0
  230. {twopy-0.2.0 → twopy-0.3.0}/tests/test_recording_timing.py +0 -0
  231. {twopy-0.2.0 → twopy-0.3.0}/tests/test_response_map_area.py +0 -0
  232. {twopy-0.2.0 → twopy-0.3.0}/tests/test_response_processing.py +0 -0
  233. {twopy-0.2.0 → twopy-0.3.0}/tests/test_response_roi_extraction.py +0 -0
  234. {twopy-0.2.0 → twopy-0.3.0}/tests/test_response_window_options.py +0 -0
  235. {twopy-0.2.0 → twopy-0.3.0}/tests/test_responses.py +0 -0
  236. {twopy-0.2.0 → twopy-0.3.0}/tests/test_roi_extraction.py +0 -0
  237. {twopy-0.2.0 → twopy-0.3.0}/tests/test_roi_mask_cleanup.py +0 -0
  238. {twopy-0.2.0 → twopy-0.3.0}/tests/test_saved_analysis.py +0 -0
  239. {twopy-0.2.0 → twopy-0.3.0}/tests/test_session.py +0 -0
  240. {twopy-0.2.0 → twopy-0.3.0}/tests/test_stimulus.py +0 -0
  241. {twopy-0.2.0 → twopy-0.3.0}/tests/test_synchronization.py +0 -0
  242. {twopy-0.2.0 → twopy-0.3.0}/tests/test_trials.py +0 -0
  243. {twopy-0.2.0 → twopy-0.3.0}/tests/test_workflow.py +0 -0
@@ -1,9 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: twopy
3
- Version: 0.2.0
3
+ Version: 0.3.0
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
7
+ Project-URL: Homepage, https://twopy.gumadeiras.com
8
+ Project-URL: Repository, https://github.com/gumadeiras/twopy
7
9
  Requires-Python: <3.14,>=3.13
8
10
  Description-Content-Type: text/markdown
9
11
  Requires-Dist: h5py<4,>=3.16
@@ -73,9 +75,11 @@ When a source recording has not been converted yet, twopy converts it first. Con
73
75
 
74
76
  ## Docs
75
77
 
76
- - [GUI guide](docs/gui.md): napari loading, ROI editing, plotting, and saved outputs.
77
- - [Python API guide](docs/python_api.md): find recordings, convert data, run analysis, and open napari from scripts.
78
- - [Custom workflow guide](docs/custom_workflows.md): add versioned local analyses to the napari Custom tab.
78
+ - [Full documentation](https://twopy.gumadeiras.com)
79
+ - [Getting started](docs/getting_started.md): install, configure, launch, and save your first analysis.
80
+ - [GUI guide](docs/gui/index.md): one page per task — loading, ROIs, plots, custom workflows, group matching, saving.
81
+ - [Python API guide](docs/python_api.md): find recordings, convert data, extract traces, fit kernels, and open napari from scripts.
82
+ - [Writing custom workflows](docs/writing_custom_workflows.md): add your own analysis to the napari Custom tab.
79
83
  - [Development guide](docs/development.md): source setup and local checks.
80
84
  - [Input data spec](docs/input_data_spec.md): short recording folder contract.
81
85
  - [Recording file reference](docs/recording_file_schema.md): detailed source and converted file schema.
@@ -53,9 +53,11 @@ When a source recording has not been converted yet, twopy converts it first. Con
53
53
 
54
54
  ## Docs
55
55
 
56
- - [GUI guide](docs/gui.md): napari loading, ROI editing, plotting, and saved outputs.
57
- - [Python API guide](docs/python_api.md): find recordings, convert data, run analysis, and open napari from scripts.
58
- - [Custom workflow guide](docs/custom_workflows.md): add versioned local analyses to the napari Custom tab.
56
+ - [Full documentation](https://twopy.gumadeiras.com)
57
+ - [Getting started](docs/getting_started.md): install, configure, launch, and save your first analysis.
58
+ - [GUI guide](docs/gui/index.md): one page per task — loading, ROIs, plots, custom workflows, group matching, saving.
59
+ - [Python API guide](docs/python_api.md): find recordings, convert data, extract traces, fit kernels, and open napari from scripts.
60
+ - [Writing custom workflows](docs/writing_custom_workflows.md): add your own analysis to the napari Custom tab.
59
61
  - [Development guide](docs/development.md): source setup and local checks.
60
62
  - [Input data spec](docs/input_data_spec.md): short recording folder contract.
61
63
  - [Recording file reference](docs/recording_file_schema.md): detailed source and converted file schema.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "twopy"
7
- version = "0.2.0"
7
+ version = "0.3.0"
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"
@@ -29,6 +29,10 @@ dependencies = [
29
29
  [project.scripts]
30
30
  twopy = "twopy.napari:main"
31
31
 
32
+ [project.urls]
33
+ Homepage = "https://twopy.gumadeiras.com"
34
+ Repository = "https://github.com/gumadeiras/twopy"
35
+
32
36
  [tool.setuptools.packages.find]
33
37
  where = ["src"]
34
38
 
@@ -43,11 +47,15 @@ target-version = "py313"
43
47
  [tool.ruff.lint]
44
48
  select = [
45
49
  "ANN",
50
+ "ARG",
46
51
  "B",
52
+ "C4",
47
53
  "D",
48
54
  "E",
49
55
  "F",
50
56
  "I",
57
+ "NPY",
58
+ "PTH",
51
59
  "SIM",
52
60
  "UP",
53
61
  "W",
@@ -15,6 +15,7 @@ from typing import Literal
15
15
 
16
16
  import numpy as np
17
17
  import numpy.typing as npt
18
+ from scipy import sparse
18
19
 
19
20
  from twopy.converted import RecordingData
20
21
  from twopy.frame_ranges import normalize_frame_range
@@ -278,6 +279,7 @@ def _extract_mean_traces_from_indices(
278
279
  rather than total recording length.
279
280
  """
280
281
  traces = np.empty((frame_stop - frame_start, len(pixel_indices_by_trace)))
282
+ projection = _mean_projection(pixel_indices_by_trace)
281
283
  _check_cancelled(check_cancelled)
282
284
  for chunk_start, chunk_stop, frames in recording.movie.iter_frame_batches(
283
285
  chunk_frames=chunk_frames,
@@ -289,10 +291,7 @@ def _extract_mean_traces_from_indices(
289
291
  chunk_offset = chunk_start - frame_start
290
292
  chunk_slice = slice(chunk_offset, chunk_offset + (chunk_stop - chunk_start))
291
293
  chunk_flat = frames.reshape(frames.shape[0], -1)
292
- for trace_index, pixel_indices in enumerate(pixel_indices_by_trace):
293
- traces[chunk_slice, trace_index] = chunk_flat[:, pixel_indices].mean(
294
- axis=1,
295
- )
294
+ traces[chunk_slice, :] = _project_trace_pixels(chunk_flat, projection)
296
295
  _check_cancelled(check_cancelled)
297
296
 
298
297
  return traces
@@ -344,6 +343,7 @@ def _extract_movie_global_percentile_corrected_traces(
344
343
  )
345
344
  frame_count = frame_stop - frame_start
346
345
  roi_count = len(roi_pixel_indices)
346
+ projection = _mean_projection(roi_pixel_indices)
347
347
  raw_values = np.empty((frame_count, roi_count), dtype=np.float64)
348
348
  background_values = np.empty((frame_count, roi_count), dtype=np.float64)
349
349
  corrected_values = np.empty((frame_count, roi_count), dtype=np.float64)
@@ -359,14 +359,19 @@ def _extract_movie_global_percentile_corrected_traces(
359
359
  chunk_slice = slice(chunk_offset, chunk_offset + (chunk_stop - chunk_start))
360
360
  chunk_flat = frames.reshape(frames.shape[0], -1)
361
361
  background_trace = chunk_flat[:, background_indices].mean(axis=1)
362
- for roi_index, pixel_indices in enumerate(roi_pixel_indices):
363
- roi_pixels = chunk_flat[:, pixel_indices]
364
- raw_values[chunk_slice, roi_index] = roi_pixels.mean(axis=1)
365
- background_values[chunk_slice, roi_index] = background_trace
366
- # Clamp pixels before averaging so dim pixels cannot pull the
367
- # corrected ROI trace below zero.
368
- corrected_pixels = np.maximum(roi_pixels - background_trace[:, None], 0.0)
369
- corrected_values[chunk_slice, roi_index] = corrected_pixels.mean(axis=1)
362
+ roi_pixels = chunk_flat[:, projection.pixel_indices]
363
+ raw_values[chunk_slice, :] = _project_selected_pixels(roi_pixels, projection)
364
+ background_values[chunk_slice, :] = background_trace[:, np.newaxis]
365
+ # Clamp pixels before averaging so dim pixels cannot pull the
366
+ # corrected ROI trace below zero.
367
+ corrected_pixels = np.maximum(
368
+ roi_pixels - background_trace[:, np.newaxis],
369
+ 0.0,
370
+ )
371
+ corrected_values[chunk_slice, :] = _project_selected_pixels(
372
+ corrected_pixels,
373
+ projection,
374
+ )
370
375
  _check_cancelled(check_cancelled)
371
376
 
372
377
  metadata: dict[str, BackgroundMetadataValue] = {
@@ -440,6 +445,7 @@ def _extract_movie_y_stripe_percentile_corrected_traces(
440
445
  )
441
446
  frame_count = frame_stop - frame_start
442
447
  roi_count = len(roi_pixel_indices)
448
+ projection = _mean_projection(roi_pixel_indices)
443
449
  raw_values = np.empty((frame_count, roi_count), dtype=np.float64)
444
450
  background_values = np.empty((frame_count, roi_count), dtype=np.float64)
445
451
  corrected_values = np.empty((frame_count, roi_count), dtype=np.float64)
@@ -460,15 +466,21 @@ def _extract_movie_y_stripe_percentile_corrected_traces(
460
466
  )
461
467
  chunk_flat = frames.reshape(frames.shape[0], -1)
462
468
  background_flat = background_field.reshape(background_field.shape[0], -1)
463
- for roi_index, pixel_indices in enumerate(roi_pixel_indices):
464
- roi_pixels = chunk_flat[:, pixel_indices]
465
- roi_background_pixels = background_flat[:, pixel_indices]
466
- raw_values[chunk_slice, roi_index] = roi_pixels.mean(axis=1)
467
- background_values[chunk_slice, roi_index] = roi_background_pixels.mean(
468
- axis=1,
469
- )
470
- corrected_pixels = np.maximum(roi_pixels - roi_background_pixels, 0.0)
471
- corrected_values[chunk_slice, roi_index] = corrected_pixels.mean(axis=1)
469
+ roi_pixels = chunk_flat[:, projection.pixel_indices]
470
+ roi_background_pixels = background_flat[:, projection.pixel_indices]
471
+ raw_values[chunk_slice, :] = _project_selected_pixels(roi_pixels, projection)
472
+ background_values[chunk_slice, :] = _project_selected_pixels(
473
+ roi_background_pixels,
474
+ projection,
475
+ )
476
+ corrected_pixels = np.maximum(
477
+ roi_pixels - roi_background_pixels,
478
+ 0.0,
479
+ )
480
+ corrected_values[chunk_slice, :] = _project_selected_pixels(
481
+ corrected_pixels,
482
+ projection,
483
+ )
472
484
  _check_cancelled(check_cancelled)
473
485
 
474
486
  metadata: dict[str, BackgroundMetadataValue] = {
@@ -593,6 +605,93 @@ def _roi_pixel_indices_from_masks(
593
605
  return tuple(np.flatnonzero(mask).astype(np.int64) for mask in flat_masks)
594
606
 
595
607
 
608
+ @dataclass(frozen=True)
609
+ class _MeanProjection:
610
+ """Sparse averaging matrix plus selected flattened pixel indices."""
611
+
612
+ pixel_indices: npt.NDArray[np.int64]
613
+ matrix: sparse.csc_matrix
614
+
615
+
616
+ def _mean_projection(
617
+ pixel_indices_by_trace: tuple[npt.NDArray[np.int64], ...],
618
+ ) -> _MeanProjection:
619
+ """Return selected pixels and a sparse pixel-to-trace averaging matrix.
620
+
621
+ Args:
622
+ pixel_indices_by_trace: One flattened pixel-index array per trace.
623
+
624
+ Returns:
625
+ Selected flattened pixel indices plus a sparse matrix shaped
626
+ ``(selected_pixels, traces)`` whose columns average each trace.
627
+
628
+ Pixel indices are concatenated per trace instead of deduplicated. That
629
+ preserves the existing behavior for overlapping ROIs, where the same movie
630
+ pixel can contribute independently to more than one ROI mean.
631
+ """
632
+ selected_pixel_indices: list[npt.NDArray[np.int64]] = []
633
+ rows: list[npt.NDArray[np.int64]] = []
634
+ columns: list[npt.NDArray[np.int64]] = []
635
+ values: list[npt.NDArray[np.float64]] = []
636
+ row_start = 0
637
+ for trace_index, pixel_indices in enumerate(pixel_indices_by_trace):
638
+ if pixel_indices.size == 0:
639
+ continue
640
+ selected_pixel_indices.append(pixel_indices)
641
+ rows.append(
642
+ np.arange(
643
+ row_start,
644
+ row_start + pixel_indices.size,
645
+ dtype=np.int64,
646
+ ),
647
+ )
648
+ row_start += pixel_indices.size
649
+ columns.append(
650
+ np.full(pixel_indices.shape, trace_index, dtype=np.int64),
651
+ )
652
+ values.append(
653
+ np.full(
654
+ pixel_indices.shape,
655
+ 1.0 / float(pixel_indices.size),
656
+ dtype=np.float64,
657
+ ),
658
+ )
659
+ if len(selected_pixel_indices) == 0:
660
+ return _MeanProjection(
661
+ pixel_indices=np.array([], dtype=np.int64),
662
+ matrix=sparse.csc_matrix((0, len(pixel_indices_by_trace))),
663
+ )
664
+ return _MeanProjection(
665
+ pixel_indices=np.concatenate(selected_pixel_indices),
666
+ matrix=sparse.csc_matrix(
667
+ (
668
+ np.concatenate(values),
669
+ (np.concatenate(rows), np.concatenate(columns)),
670
+ ),
671
+ shape=(row_start, len(pixel_indices_by_trace)),
672
+ ),
673
+ )
674
+
675
+
676
+ def _project_trace_pixels(
677
+ chunk_flat: npt.NDArray[np.float64],
678
+ projection: _MeanProjection,
679
+ ) -> npt.NDArray[np.float64]:
680
+ """Return trace means from a flattened movie chunk and projection."""
681
+ return _project_selected_pixels(chunk_flat[:, projection.pixel_indices], projection)
682
+
683
+
684
+ def _project_selected_pixels(
685
+ selected_pixels: npt.NDArray[np.float64],
686
+ projection: _MeanProjection,
687
+ ) -> npt.NDArray[np.float64]:
688
+ """Return trace means from selected pixel columns."""
689
+ return np.asarray(
690
+ selected_pixels @ projection.matrix,
691
+ dtype=np.float64,
692
+ )
693
+
694
+
596
695
  def _check_cancelled(check_cancelled: Callable[[], None] | None) -> None:
597
696
  """Raise ``CancelledError`` when an interactive caller superseded this work."""
598
697
  if check_cancelled is None:
@@ -115,6 +115,10 @@ def compute_roi_delta_f_over_f(
115
115
  data_rate_hz=data_rate_hz,
116
116
  baseline_sample_seconds=baseline_sample_seconds,
117
117
  )
118
+ _validate_baseline_samples_for_background_method(
119
+ traces=traces,
120
+ baseline_samples=baseline_samples,
121
+ )
118
122
  tau = _fit_shared_tau(
119
123
  frame_numbers=baseline_samples.frame_numbers,
120
124
  mean_fluorescence=baseline_samples.values.mean(axis=1),
@@ -274,6 +278,35 @@ def _baseline_samples(
274
278
  )
275
279
 
276
280
 
281
+ def _validate_baseline_samples_for_background_method(
282
+ *,
283
+ traces: BackgroundCorrectedRoiTraces,
284
+ baseline_samples: _BaselineSamples,
285
+ ) -> None:
286
+ """Reject background-corrected baselines that cannot support dF/F."""
287
+ if traces.method != "roi_y_stripe_percentile":
288
+ return
289
+
290
+ invalid = ~np.isfinite(baseline_samples.values) | (baseline_samples.values <= 0.0)
291
+ if not np.any(invalid):
292
+ return
293
+
294
+ roi_indices = np.flatnonzero(np.any(invalid, axis=0))
295
+ labels = ", ".join(traces.labels[index] for index in roi_indices[:5])
296
+ if roi_indices.size > 5:
297
+ labels = f"{labels}, ..."
298
+ minimum = float(np.nanmin(baseline_samples.values[:, roi_indices]))
299
+ msg = (
300
+ "ROI y-stripe P% background subtraction over-subtracted baseline "
301
+ f"fluorescence for {labels}; minimum corrected baseline sample is "
302
+ f"{minimum:.6g}. dF/F needs positive baseline fluorescence, so the "
303
+ "local y-stripe pixels are not a valid additive background for those "
304
+ "ROIs. Use shared y-stripe P%, global percentile, or revise the ROIs "
305
+ "to leave dim local background pixels."
306
+ )
307
+ raise ValueError(msg)
308
+
309
+
277
310
  def _validate_baseline_local_range(
278
311
  *,
279
312
  window: FrameWindow,
@@ -80,8 +80,8 @@ class StimulusKernelOptions:
80
80
  baseline_epoch_number: int = 1
81
81
  discard_first_stimulus_epoch: bool = True
82
82
  selected_epoch_numbers: tuple[int, ...] | None = None
83
- num_stim_past: int = 150
84
- num_stim_future: int = 25
83
+ num_stim_past: int = 100
84
+ num_stim_future: int = 10
85
85
  method: KernelFitMethod = "ols"
86
86
  hemisphere: Hemisphere | None = None
87
87
 
@@ -48,6 +48,10 @@ from twopy.hdf5_utils import (
48
48
  write_string_dataset as _write_string_dataset,
49
49
  )
50
50
  from twopy.roi import RoiSet, TraceStatistic, make_roi_set
51
+ from twopy.spatial_orientation import (
52
+ require_twopy_spatial_orientation,
53
+ write_twopy_spatial_orientation,
54
+ )
51
55
  from twopy.typing_guards import (
52
56
  require_bool_array,
53
57
  require_float64_array,
@@ -238,7 +242,9 @@ def load_analysis_outputs(path: Path) -> LoadedAnalysisOutputs:
238
242
  return LoadedAnalysisOutputs(
239
243
  path=input_path,
240
244
  roi_set=(
241
- _read_roi_set(h5_file["roi_set"]) if "roi_set" in h5_file else None
245
+ _read_roi_set(h5_file["roi_set"], path=input_path)
246
+ if "roi_set" in h5_file
247
+ else None
242
248
  ),
243
249
  traces=(
244
250
  _read_background_traces(h5_file["traces"])
@@ -347,19 +353,28 @@ def _write_roi_set(group: h5py.Group, roi_set: RoiSet) -> None:
347
353
  Returns:
348
354
  None.
349
355
  """
350
- group.create_dataset("masks", data=roi_set.masks, compression="gzip")
356
+ write_twopy_spatial_orientation(group)
357
+ masks = group.create_dataset("masks", data=roi_set.masks, compression="gzip")
358
+ write_twopy_spatial_orientation(masks)
351
359
  _write_string_dataset(group, "labels", roi_set.labels)
352
360
 
353
361
 
354
- def _read_roi_set(group: h5py.Group) -> RoiSet:
362
+ def _read_roi_set(group: h5py.Group, *, path: Path) -> RoiSet:
355
363
  """Read a persisted ROI set.
356
364
 
357
365
  Args:
358
366
  group: HDF5 ``roi_set`` group.
367
+ path: Analysis output path used in validation errors.
359
368
 
360
369
  Returns:
361
370
  ``RoiSet`` with validated masks and labels.
362
371
  """
372
+ require_twopy_spatial_orientation(group, path=path, label="roi_set")
373
+ require_twopy_spatial_orientation(
374
+ group["masks"],
375
+ path=path,
376
+ label="roi_set/masks",
377
+ )
363
378
  return make_roi_set(
364
379
  require_bool_array(group["masks"][()], name="roi_set/masks", ndim=3),
365
380
  labels=_read_string_dataset(group, "labels"),
@@ -869,7 +884,7 @@ def _trial_response_row(
869
884
  "stop_frame": trial.stop_frame,
870
885
  "frame_count": trial.stop_frame - trial.start_frame,
871
886
  }
872
- row.update({column: "" for column in time_columns})
887
+ row.update(dict.fromkeys(time_columns, ""))
873
888
  for time_seconds, value in zip(
874
889
  trial.time_seconds,
875
890
  trial.values[:, roi_index],
@@ -43,6 +43,10 @@ from twopy.analysis.timing import resolve_recording_timing
43
43
  from twopy.analysis.trials import EpochFrameWindow, is_baseline_epoch_name
44
44
  from twopy.converted import RecordingData, recording_frame_rate_hz
45
45
  from twopy.spatial import SpatialCrop
46
+ from twopy.spatial_orientation import (
47
+ require_twopy_spatial_orientation,
48
+ write_twopy_spatial_orientation,
49
+ )
46
50
 
47
51
  __all__ = [
48
52
  "EpochResponseMap",
@@ -247,29 +251,33 @@ def save_response_map_data(path: Path, map_data: ResponseMapData) -> None:
247
251
  recording-level movie products. Values are stored as normalized signed maps
248
252
  with the original dF/F scale in ``response_scale`` for audit.
249
253
  """
254
+ _require_response_map_shapes(map_data)
250
255
  output_path = path.expanduser()
251
256
  output_path.parent.mkdir(parents=True, exist_ok=True)
252
257
  with h5py.File(output_path, "w") as h5_file:
253
258
  h5_file.attrs["twopy_format"] = RESPONSE_MAP_FILE_FORMAT
254
259
  h5_file.attrs["response_scale"] = map_data.response_scale
260
+ write_twopy_spatial_orientation(h5_file)
255
261
  _write_response_map_options(h5_file.create_group("options"), map_data.options)
256
262
  _write_spatial_crop(h5_file.create_group("spatial_crop"), map_data.spatial_crop)
257
- h5_file.create_dataset(
263
+ mean_image = h5_file.create_dataset(
258
264
  "mean_image",
259
265
  data=map_data.mean_image,
260
266
  compression="gzip",
261
267
  )
268
+ write_twopy_spatial_orientation(mean_image)
262
269
  epochs_group = h5_file.create_group("epochs")
263
270
  for index, epoch in enumerate(map_data.epochs):
264
271
  epoch_group = epochs_group.create_group(f"{index:04d}")
265
272
  epoch_group.attrs["epoch_name"] = epoch.epoch_name
266
273
  epoch_group.attrs["epoch_number"] = epoch.epoch_number
267
274
  epoch_group.attrs["trial_count"] = epoch.trial_count
268
- epoch_group.create_dataset(
275
+ response_values = epoch_group.create_dataset(
269
276
  "response_values",
270
277
  data=epoch.response_values,
271
278
  compression="gzip",
272
279
  )
280
+ write_twopy_spatial_orientation(response_values)
273
281
 
274
282
 
275
283
  def load_response_map_data(path: Path) -> ResponseMapData:
@@ -287,21 +295,34 @@ def load_response_map_data(path: Path) -> ResponseMapData:
287
295
  input_path = path.expanduser()
288
296
  with h5py.File(input_path, "r") as h5_file:
289
297
  _require_response_map_format(h5_file, input_path)
298
+ require_twopy_spatial_orientation(
299
+ h5_file,
300
+ path=input_path,
301
+ label="response heatmap file",
302
+ )
290
303
  options = _read_response_map_options(h5_file["options"])
291
304
  spatial_crop = _read_spatial_crop(h5_file["spatial_crop"])
292
- mean_image = np.asarray(h5_file["mean_image"], dtype=np.float64)
305
+ mean_image_dataset = h5_file["mean_image"]
306
+ require_twopy_spatial_orientation(
307
+ mean_image_dataset,
308
+ path=input_path,
309
+ label="mean_image",
310
+ )
311
+ mean_image = np.asarray(mean_image_dataset, dtype=np.float64)
293
312
  epochs_group = h5_file["epochs"]
294
313
  epochs = tuple(
295
- _read_epoch_response_map(epochs_group[name])
314
+ _read_epoch_response_map(epochs_group[name], path=input_path)
296
315
  for name in sorted(epochs_group.keys())
297
316
  )
298
- return ResponseMapData(
317
+ map_data = ResponseMapData(
299
318
  mean_image=mean_image,
300
319
  epochs=epochs,
301
320
  options=options,
302
321
  spatial_crop=spatial_crop,
303
322
  response_scale=float(h5_file.attrs["response_scale"]),
304
323
  )
324
+ _require_response_map_shapes(map_data)
325
+ return map_data
305
326
 
306
327
 
307
328
  def _write_response_map_options(
@@ -357,16 +378,45 @@ def _read_spatial_crop(group: h5py.Group) -> SpatialCrop:
357
378
  )
358
379
 
359
380
 
360
- def _read_epoch_response_map(group: h5py.Group) -> EpochResponseMap:
381
+ def _read_epoch_response_map(group: h5py.Group, *, path: Path) -> EpochResponseMap:
361
382
  """Read one persisted epoch response map."""
383
+ response_values = group["response_values"]
384
+ require_twopy_spatial_orientation(
385
+ response_values,
386
+ path=path,
387
+ label=f"{group.name}/response_values",
388
+ )
362
389
  return EpochResponseMap(
363
390
  epoch_name=_string_attr(group, "epoch_name"),
364
391
  epoch_number=int(group.attrs["epoch_number"]),
365
- response_values=np.asarray(group["response_values"], dtype=np.float64),
392
+ response_values=np.asarray(response_values, dtype=np.float64),
366
393
  trial_count=int(group.attrs["trial_count"]),
367
394
  )
368
395
 
369
396
 
397
+ def _require_response_map_shapes(map_data: ResponseMapData) -> None:
398
+ """Require heatmap spatial arrays to match their recorded crop."""
399
+ if map_data.mean_image.ndim != 2:
400
+ msg = (
401
+ f"response heatmap mean_image must be 2-D; got {map_data.mean_image.shape}"
402
+ )
403
+ raise ValueError(msg)
404
+ if map_data.mean_image.shape != map_data.spatial_crop.shape:
405
+ msg = (
406
+ "response heatmap mean_image shape must match spatial_crop shape; "
407
+ f"got {map_data.mean_image.shape} and {map_data.spatial_crop.shape}"
408
+ )
409
+ raise ValueError(msg)
410
+ for epoch in map_data.epochs:
411
+ if epoch.response_values.shape != map_data.mean_image.shape:
412
+ msg = (
413
+ "response heatmap response_values shape must match mean_image "
414
+ f"shape; got {epoch.response_values.shape} for "
415
+ f"{epoch.epoch_name!r} and {map_data.mean_image.shape}"
416
+ )
417
+ raise ValueError(msg)
418
+
419
+
370
420
  def _require_response_map_format(h5_file: h5py.File, path: Path) -> None:
371
421
  """Raise when an HDF5 file is not a twopy response heatmap file."""
372
422
  actual = _string_attr(h5_file, "twopy_format")