TwoPy 0.2.1__tar.gz → 0.3.1__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 (250) hide show
  1. {twopy-0.2.1 → twopy-0.3.1}/PKG-INFO +1 -1
  2. {twopy-0.2.1 → twopy-0.3.1}/pyproject.toml +1 -1
  3. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/__init__.py +0 -16
  4. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/analysis/__init__.py +12 -0
  5. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/analysis/persistence.py +156 -70
  6. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/analysis/response_maps.py +57 -7
  7. twopy-0.3.1/src/twopy/analysis/response_plotting.py +334 -0
  8. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/analysis_cache.py +84 -22
  9. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/conversion/alignment_crop.py +18 -13
  10. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/conversion/api.py +10 -4
  11. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/conversion/hdf5_writing.py +27 -4
  12. twopy-0.3.1/src/twopy/conversion/orientation.py +80 -0
  13. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/conversion/types.py +1 -1
  14. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/converted.py +26 -3
  15. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/custom/__init__.py +12 -7
  16. twopy-0.2.1/src/twopy/custom/types.py → twopy-0.3.1/src/twopy/custom/context.py +12 -484
  17. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/custom/discovery.py +3 -6
  18. twopy-0.3.1/src/twopy/custom/metadata.py +228 -0
  19. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/custom/parameters.py +6 -5
  20. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/custom/provenance.py +84 -7
  21. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/custom/registry.py +2 -1
  22. twopy-0.3.1/src/twopy/custom/results.py +116 -0
  23. twopy-0.3.1/src/twopy/custom/runner.py +247 -0
  24. twopy-0.3.1/src/twopy/custom/workflows.py +98 -0
  25. twopy-0.3.1/src/twopy/napari/__init__.py +75 -0
  26. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/napari/controls.py +90 -33
  27. twopy-0.3.1/src/twopy/napari/database_favorite_editor.py +106 -0
  28. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/napari/database_favorites.py +56 -5
  29. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/napari/database_search.py +223 -2
  30. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/napari/dims.py +5 -9
  31. twopy-0.3.1/src/twopy/napari/display.py +106 -0
  32. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/napari/group_matching/fov_assignment.py +1 -8
  33. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/napari/group_matching/images.py +4 -8
  34. twopy-0.3.1/src/twopy/napari/group_matching/protocols.py +27 -0
  35. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/napari/group_matching/response_preview.py +1 -1
  36. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/napari/group_matching/responses.py +1 -1
  37. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/napari/group_matching/roi_assignment.py +2 -8
  38. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/napari/group_matching/roi_cards.py +4 -9
  39. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/napari/group_matching/window.py +9 -16
  40. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/napari/interactive.py +4 -16
  41. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/napari/live_analysis.py +1 -1
  42. twopy-0.3.1/src/twopy/napari/load_controller.py +400 -0
  43. twopy-0.3.1/src/twopy/napari/load_progress.py +168 -0
  44. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/napari/load_workflow.py +225 -83
  45. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/napari/loaded_recordings_csv.py +65 -24
  46. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/napari/loading.py +21 -3
  47. twopy-0.3.1/src/twopy/napari/plotting/__init__.py +52 -0
  48. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/napari/plotting/data.py +16 -289
  49. twopy-0.3.1/src/twopy/napari/plotting/docks/__init__.py +52 -0
  50. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/napari/plotting/docks/api.py +20 -6
  51. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/napari/plotting/docks/dynamic_options.py +70 -15
  52. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/napari/plotting/docks/plot_area.py +1 -1
  53. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/napari/plotting/docks/response_map_area.py +5 -8
  54. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/napari/plotting/docks/response_plot_widget.py +119 -91
  55. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/napari/plotting/docks/visibility_state.py +1 -1
  56. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/napari/plotting/export.py +15 -28
  57. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/napari/plotting/export_controls.py +1 -1
  58. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/napari/plotting/options.py +49 -4
  59. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/napari/plotting/preview_strip.py +1 -1
  60. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/napari/plotting/widgets.py +1 -1
  61. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/napari/protocols.py +44 -0
  62. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/napari/responses.py +4 -2
  63. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/napari/roi.py +145 -23
  64. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/napari/session.py +36 -0
  65. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/napari/trial_timeline.py +11 -24
  66. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/napari/viewer.py +206 -67
  67. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/roi.py +54 -5
  68. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/spatial.py +3 -3
  69. twopy-0.3.1/src/twopy/spatial_orientation.py +78 -0
  70. {twopy-0.2.1 → twopy-0.3.1}/src/twopy.egg-info/PKG-INFO +1 -1
  71. {twopy-0.2.1 → twopy-0.3.1}/src/twopy.egg-info/SOURCES.txt +14 -1
  72. {twopy-0.2.1 → twopy-0.3.1}/tests/test_background_subtraction.py +22 -1
  73. {twopy-0.2.1 → twopy-0.3.1}/tests/test_conversion.py +114 -11
  74. {twopy-0.2.1 → twopy-0.3.1}/tests/test_converted.py +64 -2
  75. {twopy-0.2.1 → twopy-0.3.1}/tests/test_custom_workflows.py +174 -2
  76. {twopy-0.2.1 → twopy-0.3.1}/tests/test_function_inventory.py +45 -4
  77. twopy-0.3.1/tests/test_import.py +121 -0
  78. {twopy-0.2.1 → twopy-0.3.1}/tests/test_kernels.py +125 -1
  79. {twopy-0.2.1 → twopy-0.3.1}/tests/test_load_workflow.py +58 -51
  80. {twopy-0.2.1 → twopy-0.3.1}/tests/test_napari.py +54 -40
  81. {twopy-0.2.1 → twopy-0.3.1}/tests/test_napari_database_search.py +329 -3
  82. {twopy-0.2.1 → twopy-0.3.1}/tests/test_napari_display.py +12 -31
  83. {twopy-0.2.1 → twopy-0.3.1}/tests/test_napari_export.py +5 -3
  84. twopy-0.3.1/tests/test_napari_load_controller.py +257 -0
  85. twopy-0.3.1/tests/test_napari_load_progress.py +48 -0
  86. {twopy-0.2.1 → twopy-0.3.1}/tests/test_napari_load_tab.py +145 -8
  87. {twopy-0.2.1 → twopy-0.3.1}/tests/test_napari_loaded_recordings.py +41 -4
  88. {twopy-0.2.1 → twopy-0.3.1}/tests/test_napari_path_resolution.py +67 -7
  89. {twopy-0.2.1 → twopy-0.3.1}/tests/test_napari_plot_data.py +6 -4
  90. {twopy-0.2.1 → twopy-0.3.1}/tests/test_napari_plot_widgets.py +2 -2
  91. {twopy-0.2.1 → twopy-0.3.1}/tests/test_napari_response_workflow.py +4 -2
  92. {twopy-0.2.1 → twopy-0.3.1}/tests/test_napari_roi_generation.py +133 -3
  93. {twopy-0.2.1 → twopy-0.3.1}/tests/test_napari_roi_visibility.py +32 -1
  94. {twopy-0.2.1 → twopy-0.3.1}/tests/test_persistence.py +125 -1
  95. {twopy-0.2.1 → twopy-0.3.1}/tests/test_response_maps.py +115 -11
  96. {twopy-0.2.1 → twopy-0.3.1}/tests/test_roi.py +60 -1
  97. twopy-0.2.1/src/twopy/napari/__init__.py +0 -27
  98. twopy-0.2.1/src/twopy/napari/display.py +0 -181
  99. twopy-0.2.1/src/twopy/napari/plotting/__init__.py +0 -21
  100. twopy-0.2.1/src/twopy/napari/plotting/docks/__init__.py +0 -23
  101. twopy-0.2.1/tests/test_import.py +0 -26
  102. {twopy-0.2.1 → twopy-0.3.1}/README.md +0 -0
  103. {twopy-0.2.1 → twopy-0.3.1}/setup.cfg +0 -0
  104. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/_segments.py +0 -0
  105. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/_version.py +0 -0
  106. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/analysis/background_subtraction.py +0 -0
  107. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/analysis/dff.py +0 -0
  108. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/analysis/dff_options.py +0 -0
  109. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/analysis/epoch_mapping.py +0 -0
  110. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/analysis/group_matching.py +0 -0
  111. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/analysis/kernels/__init__.py +0 -0
  112. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/analysis/kernels/_fitting.py +0 -0
  113. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/analysis/kernels/_stimulus.py +0 -0
  114. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/analysis/motion.py +0 -0
  115. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/analysis/response_context.py +0 -0
  116. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/analysis/response_processing/__init__.py +0 -0
  117. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/analysis/response_processing/apply.py +0 -0
  118. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/analysis/response_processing/options.py +0 -0
  119. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/analysis/response_processing/persistence.py +0 -0
  120. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/analysis/response_processing/qc.py +0 -0
  121. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/analysis/response_processing/signals.py +0 -0
  122. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/analysis/response_window_options.py +0 -0
  123. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/analysis/responses.py +0 -0
  124. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/analysis/timing.py +0 -0
  125. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/analysis/trials.py +0 -0
  126. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/analysis/workflow.py +0 -0
  127. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/api.py +0 -0
  128. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/calibrations/pixel_calibration_profiles.csv +0 -0
  129. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/calibrations/pixel_size.csv +0 -0
  130. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/config.py +0 -0
  131. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/conversion/__init__.py +0 -0
  132. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/conversion/matlab_values.py +0 -0
  133. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/conversion/source_loading.py +0 -0
  134. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/conversion/stimulus_code.py +0 -0
  135. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/custom/native.py +0 -0
  136. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/custom/native_workflows/__init__.py +0 -0
  137. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/custom/native_workflows/direction_selectivity.py +0 -0
  138. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/custom/native_workflows/response_kernels.py +0 -0
  139. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/database/__init__.py +0 -0
  140. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/database/cache.py +0 -0
  141. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/database/catalog.py +0 -0
  142. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/database/connection.py +0 -0
  143. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/database/generic.py +0 -0
  144. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/database/modeled.py +0 -0
  145. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/database/search.py +0 -0
  146. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/database/sql.py +0 -0
  147. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/database/types.py +0 -0
  148. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/filenames.py +0 -0
  149. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/frame_ranges.py +0 -0
  150. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/hdf5_utils.py +0 -0
  151. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/inspection.py +0 -0
  152. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/matlab.py +0 -0
  153. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/napari/__main__.py +0 -0
  154. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/napari/custom_tab.py +0 -0
  155. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/napari/display_paths.py +0 -0
  156. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/napari/errors.py +0 -0
  157. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/napari/group_matching/__init__.py +0 -0
  158. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/napari/group_matching/cards.py +0 -0
  159. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/napari/group_matching/fov_cards.py +0 -0
  160. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/napari/group_matching/style.py +0 -0
  161. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/napari/group_matching/tables.py +0 -0
  162. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/napari/latest_worker.py +0 -0
  163. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/napari/launcher.py +0 -0
  164. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/napari/movie.py +0 -0
  165. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/napari/paths.py +0 -0
  166. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/napari/plotting/dff_options.py +0 -0
  167. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/napari/plotting/docks/options_panel.py +0 -0
  168. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/napari/plotting/docks/paths.py +0 -0
  169. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/napari/plotting/docks/save_actions.py +0 -0
  170. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/napari/plotting/form_controls.py +0 -0
  171. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/napari/plotting/label_visibility.py +0 -0
  172. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/napari/plotting/normalization_options.py +0 -0
  173. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/napari/plotting/panels.py +0 -0
  174. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/napari/plotting/processing_options.py +0 -0
  175. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/napari/plotting/response_map_colors.py +0 -0
  176. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/napari/plotting/response_map_display.py +0 -0
  177. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/napari/plotting/response_map_options.py +0 -0
  178. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/napari/plotting/response_window_options.py +0 -0
  179. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/napari/plotting/roi_generation/__init__.py +0 -0
  180. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/napari/plotting/roi_generation/actions.py +0 -0
  181. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/napari/plotting/roi_generation/controls.py +0 -0
  182. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/napari/plotting/roi_generation/options.py +0 -0
  183. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/napari/roi_signatures.py +0 -0
  184. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/napari/sidebar.py +0 -0
  185. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/napari/state.py +0 -0
  186. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/napari/text.py +0 -0
  187. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/napari/types.py +0 -0
  188. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/parity/__init__.py +0 -0
  189. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/parity/dff.py +0 -0
  190. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/parity/psycho5.py +0 -0
  191. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/parity/roi.py +0 -0
  192. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/parity/roi_extraction.py +0 -0
  193. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/parity/saved_analysis.py +0 -0
  194. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/photodiode.py +0 -0
  195. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/photodiode_classification.py +0 -0
  196. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/pixel_calibration.py +0 -0
  197. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/pixel_calibration_profiles.py +0 -0
  198. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/response_roi_extraction.py +0 -0
  199. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/roi_extraction.py +0 -0
  200. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/roi_mask_cleanup.py +0 -0
  201. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/session.py +0 -0
  202. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/stimulus.py +0 -0
  203. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/synchronization.py +0 -0
  204. {twopy-0.2.1 → twopy-0.3.1}/src/twopy/typing_guards.py +0 -0
  205. {twopy-0.2.1 → twopy-0.3.1}/src/twopy.egg-info/dependency_links.txt +0 -0
  206. {twopy-0.2.1 → twopy-0.3.1}/src/twopy.egg-info/entry_points.txt +0 -0
  207. {twopy-0.2.1 → twopy-0.3.1}/src/twopy.egg-info/requires.txt +0 -0
  208. {twopy-0.2.1 → twopy-0.3.1}/src/twopy.egg-info/top_level.txt +0 -0
  209. {twopy-0.2.1 → twopy-0.3.1}/tests/test_api.py +0 -0
  210. {twopy-0.2.1 → twopy-0.3.1}/tests/test_config.py +0 -0
  211. {twopy-0.2.1 → twopy-0.3.1}/tests/test_database.py +0 -0
  212. {twopy-0.2.1 → twopy-0.3.1}/tests/test_dff.py +0 -0
  213. {twopy-0.2.1 → twopy-0.3.1}/tests/test_epoch_mapping.py +0 -0
  214. {twopy-0.2.1 → twopy-0.3.1}/tests/test_filenames.py +0 -0
  215. {twopy-0.2.1 → twopy-0.3.1}/tests/test_frame_ranges.py +0 -0
  216. {twopy-0.2.1 → twopy-0.3.1}/tests/test_group_matching.py +0 -0
  217. {twopy-0.2.1 → twopy-0.3.1}/tests/test_hdf5_utils.py +0 -0
  218. {twopy-0.2.1 → twopy-0.3.1}/tests/test_inspection.py +0 -0
  219. {twopy-0.2.1 → twopy-0.3.1}/tests/test_matlab.py +0 -0
  220. {twopy-0.2.1 → twopy-0.3.1}/tests/test_motion.py +0 -0
  221. {twopy-0.2.1 → twopy-0.3.1}/tests/test_napari_group_matching_responses.py +0 -0
  222. {twopy-0.2.1 → twopy-0.3.1}/tests/test_napari_group_matching_roi_cards.py +0 -0
  223. {twopy-0.2.1 → twopy-0.3.1}/tests/test_napari_live_controller.py +0 -0
  224. {twopy-0.2.1 → twopy-0.3.1}/tests/test_napari_plot_controls.py +0 -0
  225. {twopy-0.2.1 → twopy-0.3.1}/tests/test_napari_processing_controls.py +0 -0
  226. {twopy-0.2.1 → twopy-0.3.1}/tests/test_napari_response_map_display.py +0 -0
  227. {twopy-0.2.1 → twopy-0.3.1}/tests/test_napari_text.py +0 -0
  228. {twopy-0.2.1 → twopy-0.3.1}/tests/test_napari_timeline.py +0 -0
  229. {twopy-0.2.1 → twopy-0.3.1}/tests/test_packaging.py +0 -0
  230. {twopy-0.2.1 → twopy-0.3.1}/tests/test_parity.py +0 -0
  231. {twopy-0.2.1 → twopy-0.3.1}/tests/test_parity_psycho5.py +0 -0
  232. {twopy-0.2.1 → twopy-0.3.1}/tests/test_parity_roi_extraction.py +0 -0
  233. {twopy-0.2.1 → twopy-0.3.1}/tests/test_photodiode_classification.py +0 -0
  234. {twopy-0.2.1 → twopy-0.3.1}/tests/test_pixel_calibration.py +0 -0
  235. {twopy-0.2.1 → twopy-0.3.1}/tests/test_pixel_calibration_profiles.py +0 -0
  236. {twopy-0.2.1 → twopy-0.3.1}/tests/test_real_fixture.py +0 -0
  237. {twopy-0.2.1 → twopy-0.3.1}/tests/test_recording_timing.py +0 -0
  238. {twopy-0.2.1 → twopy-0.3.1}/tests/test_response_map_area.py +0 -0
  239. {twopy-0.2.1 → twopy-0.3.1}/tests/test_response_processing.py +0 -0
  240. {twopy-0.2.1 → twopy-0.3.1}/tests/test_response_roi_extraction.py +0 -0
  241. {twopy-0.2.1 → twopy-0.3.1}/tests/test_response_window_options.py +0 -0
  242. {twopy-0.2.1 → twopy-0.3.1}/tests/test_responses.py +0 -0
  243. {twopy-0.2.1 → twopy-0.3.1}/tests/test_roi_extraction.py +0 -0
  244. {twopy-0.2.1 → twopy-0.3.1}/tests/test_roi_mask_cleanup.py +0 -0
  245. {twopy-0.2.1 → twopy-0.3.1}/tests/test_saved_analysis.py +0 -0
  246. {twopy-0.2.1 → twopy-0.3.1}/tests/test_session.py +0 -0
  247. {twopy-0.2.1 → twopy-0.3.1}/tests/test_stimulus.py +0 -0
  248. {twopy-0.2.1 → twopy-0.3.1}/tests/test_synchronization.py +0 -0
  249. {twopy-0.2.1 → twopy-0.3.1}/tests/test_trials.py +0 -0
  250. {twopy-0.2.1 → twopy-0.3.1}/tests/test_workflow.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: twopy
3
- Version: 0.2.1
3
+ Version: 0.3.1
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "twopy"
7
- version = "0.2.1"
7
+ version = "0.3.1"
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"
@@ -94,15 +94,6 @@ from twopy.converted import (
94
94
  recording_frame_rate_hz,
95
95
  recording_hemisphere,
96
96
  )
97
- from twopy.napari import (
98
- NapariRecordingView,
99
- NapariSidebarWidgets,
100
- add_twopy_magicgui_controls,
101
- launch_napari,
102
- open_recording_in_napari,
103
- roi_label_image_from_layer,
104
- save_napari_label_rois,
105
- )
106
97
  from twopy.photodiode_classification import (
107
98
  ClassifiedPhotodiodeEvent,
108
99
  ClassifiedStimulusTiming,
@@ -180,7 +171,6 @@ __all__ = [
180
171
  "AnalysisResponseComputation",
181
172
  "add_manual_roi_match_group",
182
173
  "analyze_recording_responses",
183
- "add_twopy_magicgui_controls",
184
174
  "append_manual_roi_match_rows",
185
175
  "apply_motion_artifact_mask_to_delta_f_over_f",
186
176
  "BackgroundCorrectedRoiTraces",
@@ -227,7 +217,6 @@ __all__ = [
227
217
  "is_baseline_epoch_name",
228
218
  "KernelFitMethod",
229
219
  "KernelStimulusModality",
230
- "launch_napari",
231
220
  "load_analysis_outputs",
232
221
  "load_response_map_data",
233
222
  "load_manual_fov_group_rows",
@@ -254,9 +243,6 @@ __all__ = [
254
243
  "remove_manual_roi_match_group",
255
244
  "replace_manual_roi_match_group",
256
245
  "map_stimulus_specific_column",
257
- "NapariRecordingView",
258
- "NapariSidebarWidgets",
259
- "open_recording_in_napari",
260
246
  "pair_photodiode_events_to_imaging_frames",
261
247
  "PhotodiodeAlignment",
262
248
  "PhotodiodeDurationClass",
@@ -296,13 +282,11 @@ __all__ = [
296
282
  "RoiResponseTrial",
297
283
  "RoiSet",
298
284
  "RoiTraces",
299
- "roi_label_image_from_layer",
300
285
  "roi_set_to_label_image",
301
286
  "save_analysis_outputs",
302
287
  "save_response_map_data",
303
288
  "save_manual_fov_group_rows",
304
289
  "save_manual_roi_match_rows",
305
- "save_napari_label_rois",
306
290
  "save_roi_set",
307
291
  "select_baseline_frame_windows",
308
292
  "select_pixel_calibration_group",
@@ -66,6 +66,13 @@ from twopy.analysis.response_maps import (
66
66
  load_response_map_data,
67
67
  save_response_map_data,
68
68
  )
69
+ from twopy.analysis.response_plotting import (
70
+ EpochResponsePlotData,
71
+ ResponsePlotData,
72
+ filter_response_plot_data_rois,
73
+ response_plot_data_from_grouped,
74
+ response_plot_window_options_from_grouped,
75
+ )
69
76
  from twopy.analysis.response_processing import (
70
77
  CorrelationFilterOptions,
71
78
  LowPassFilterOptions,
@@ -130,6 +137,7 @@ __all__ = [
130
137
  "DeltaFOverFOptions",
131
138
  "DeltaFOverFFitMode",
132
139
  "EpochResponseMap",
140
+ "EpochResponsePlotData",
133
141
  "EpochFrameWindow",
134
142
  "extract_background_corrected_roi_traces",
135
143
  "fit_recording_stimulus_kernels",
@@ -163,6 +171,7 @@ __all__ = [
163
171
  "ResponseMapData",
164
172
  "ResponseMapMode",
165
173
  "ResponseMapOptions",
174
+ "ResponsePlotData",
166
175
  "ResponseProcessingOptions",
167
176
  "RoiCorrelationScores",
168
177
  "RoiResponseSummary",
@@ -181,9 +190,12 @@ __all__ = [
181
190
  "save_manual_roi_match_rows",
182
191
  "select_baseline_frame_windows",
183
192
  "finite_mean_and_sem",
193
+ "filter_response_plot_data_rois",
184
194
  "split_traces_by_frame_windows",
185
195
  "StimulusKernelOptions",
186
196
  "SmoothingOptions",
197
+ "response_plot_data_from_grouped",
198
+ "response_plot_window_options_from_grouped",
187
199
  "summarize_epoch_roi_responses",
188
200
  "summarize_grouped_responses",
189
201
  "TimingSource",
@@ -10,7 +10,9 @@ MATLAB/TIFF files or decide analysis parameters.
10
10
 
11
11
  import csv
12
12
  import json
13
- from collections.abc import Mapping, Sequence
13
+ import os
14
+ import tempfile
15
+ from collections.abc import Callable, Iterator, Mapping, Sequence
14
16
  from dataclasses import dataclass
15
17
  from pathlib import Path
16
18
 
@@ -48,6 +50,10 @@ from twopy.hdf5_utils import (
48
50
  write_string_dataset as _write_string_dataset,
49
51
  )
50
52
  from twopy.roi import RoiSet, TraceStatistic, make_roi_set
53
+ from twopy.spatial_orientation import (
54
+ require_twopy_spatial_orientation,
55
+ write_twopy_spatial_orientation,
56
+ )
51
57
  from twopy.typing_guards import (
52
58
  require_bool_array,
53
59
  require_float64_array,
@@ -153,8 +159,11 @@ def save_analysis_outputs(
153
159
  None.
154
160
 
155
161
  The HDF5 file stores arrays with gzip compression where arrays can be large.
156
- The HDF5 file is the complete audit trail. CSV files duplicate response
157
- time series in a spreadsheet-friendly layout for quick plotting and checks.
162
+ The HDF5 file is the complete audit trail. CSV files are derived exports for
163
+ quick spreadsheet plotting and checks, and are written after the HDF5 file.
164
+ Each output file is written through a temporary sibling path so a failed
165
+ write leaves the previous complete file in place; the HDF5 and CSV files are
166
+ not replaced as one multi-file transaction.
158
167
  """
159
168
  summary_outputs = (
160
169
  response_summary_trials_csv,
@@ -171,35 +180,40 @@ def save_analysis_outputs(
171
180
  if grouped_responses is not None:
172
181
  validate_grouped_roi_responses(grouped_responses)
173
182
 
174
- output_path = path.expanduser()
175
- output_path.parent.mkdir(parents=True, exist_ok=True)
176
- with h5py.File(output_path, "w") as h5_file:
177
- h5_file.attrs["twopy_format"] = ANALYSIS_OUTPUT_FILE_FORMAT
178
- if roi_set is not None:
179
- _write_roi_set(h5_file.create_group("roi_set"), roi_set)
180
- if traces is not None:
181
- _write_background_traces(h5_file.create_group("traces"), traces)
182
- if dff is not None:
183
- _write_dff(h5_file.create_group("dff"), dff)
184
- if len(epoch_windows) > 0:
185
- _write_epoch_windows(h5_file.create_group("epoch_windows"), epoch_windows)
186
- if len(baseline_windows) > 0:
187
- _write_frame_windows(
188
- h5_file.create_group("baseline_windows"),
189
- baseline_windows,
190
- )
191
- if grouped_responses is not None:
192
- _write_grouped_responses(
193
- h5_file.create_group("responses"),
194
- grouped_responses,
195
- )
196
- if response_processing_options is not None:
197
- write_response_processing_group(
198
- h5_file.create_group("response_processing"),
199
- options=response_processing_options,
200
- normalization_factors=normalization_factors,
201
- correlation_scores=correlation_scores,
202
- )
183
+ def write_hdf5(output_path: Path) -> None:
184
+ """Write the analysis HDF5 contents to one already-created path."""
185
+ with h5py.File(output_path, "w") as h5_file:
186
+ h5_file.attrs["twopy_format"] = ANALYSIS_OUTPUT_FILE_FORMAT
187
+ if roi_set is not None:
188
+ _write_roi_set(h5_file.create_group("roi_set"), roi_set)
189
+ if traces is not None:
190
+ _write_background_traces(h5_file.create_group("traces"), traces)
191
+ if dff is not None:
192
+ _write_dff(h5_file.create_group("dff"), dff)
193
+ if len(epoch_windows) > 0:
194
+ _write_epoch_windows(
195
+ h5_file.create_group("epoch_windows"),
196
+ epoch_windows,
197
+ )
198
+ if len(baseline_windows) > 0:
199
+ _write_frame_windows(
200
+ h5_file.create_group("baseline_windows"),
201
+ baseline_windows,
202
+ )
203
+ if grouped_responses is not None:
204
+ _write_grouped_responses(
205
+ h5_file.create_group("responses"),
206
+ grouped_responses,
207
+ )
208
+ if response_processing_options is not None:
209
+ write_response_processing_group(
210
+ h5_file.create_group("response_processing"),
211
+ options=response_processing_options,
212
+ normalization_factors=normalization_factors,
213
+ correlation_scores=correlation_scores,
214
+ )
215
+
216
+ _write_file_atomically(path, write_hdf5)
203
217
 
204
218
  if grouped_responses is not None:
205
219
  if response_summary_trials_csv is not None:
@@ -214,6 +228,58 @@ def save_analysis_outputs(
214
228
  )
215
229
 
216
230
 
231
+ def _write_file_atomically(
232
+ path: Path,
233
+ write_file: Callable[[Path], None],
234
+ ) -> None:
235
+ """Write a file beside its final path, then replace the old file.
236
+
237
+ Args:
238
+ path: Final destination path.
239
+ write_file: Callback that writes a complete file at the provided
240
+ temporary path.
241
+
242
+ Returns:
243
+ None.
244
+
245
+ Existing analysis files are useful audit records. Writing through a sibling
246
+ temporary path keeps a failed save from replacing the last complete output.
247
+ """
248
+ output_path = path.expanduser()
249
+ output_path.parent.mkdir(parents=True, exist_ok=True)
250
+ temp_descriptor, temp_name = tempfile.mkstemp(
251
+ prefix=f".{output_path.name}.",
252
+ suffix=".tmp",
253
+ dir=output_path.parent,
254
+ )
255
+ os.close(temp_descriptor)
256
+ temp_path = Path(temp_name)
257
+ try:
258
+ write_file(temp_path)
259
+ temp_path.chmod(_replacement_file_mode(output_path))
260
+ temp_path.replace(output_path)
261
+ except Exception:
262
+ temp_path.unlink(missing_ok=True)
263
+ raise
264
+
265
+
266
+ def _replacement_file_mode(path: Path) -> int:
267
+ """Return the file mode an atomic replacement should keep.
268
+
269
+ Args:
270
+ path: Final destination path.
271
+
272
+ Returns:
273
+ Existing file permissions, or normal new-file permissions after the
274
+ process umask.
275
+ """
276
+ if path.exists():
277
+ return path.stat().st_mode & 0o777
278
+ current_umask = os.umask(0)
279
+ os.umask(current_umask)
280
+ return 0o666 & ~current_umask
281
+
282
+
217
283
  def load_analysis_outputs(path: Path) -> LoadedAnalysisOutputs:
218
284
  """Load twopy analysis outputs from HDF5.
219
285
 
@@ -238,7 +304,9 @@ def load_analysis_outputs(path: Path) -> LoadedAnalysisOutputs:
238
304
  return LoadedAnalysisOutputs(
239
305
  path=input_path,
240
306
  roi_set=(
241
- _read_roi_set(h5_file["roi_set"]) if "roi_set" in h5_file else None
307
+ _read_roi_set(h5_file["roi_set"], path=input_path)
308
+ if "roi_set" in h5_file
309
+ else None
242
310
  ),
243
311
  traces=(
244
312
  _read_background_traces(h5_file["traces"])
@@ -291,25 +359,31 @@ def write_response_summary_trials_csv(
291
359
 
292
360
  Returns:
293
361
  None.
362
+
363
+ The CSV is written through a temporary sibling path so a failed write leaves
364
+ the previous complete file in place.
294
365
  """
295
- output_path = path.expanduser()
296
- output_path.parent.mkdir(parents=True, exist_ok=True)
297
366
  validate_grouped_roi_responses(grouped_responses)
298
367
  time_columns = _time_columns(grouped_responses.trials)
299
368
  fieldnames = (*_TRIAL_RESPONSE_CSV_BASE_COLUMNS, *time_columns)
300
- with output_path.open("w", encoding="utf-8", newline="") as csv_file:
301
- writer = csv.DictWriter(csv_file, fieldnames=fieldnames)
302
- writer.writeheader()
303
- for trial in grouped_responses.trials:
304
- for roi_index, roi_label in enumerate(grouped_responses.roi_labels):
305
- writer.writerow(
306
- _trial_response_row(
307
- trial,
308
- roi_index=roi_index,
309
- roi_label=roi_label,
310
- time_columns=time_columns,
311
- ),
312
- )
369
+
370
+ def write_csv(output_path: Path) -> None:
371
+ """Write trial-level rows to one complete CSV file."""
372
+ with output_path.open("w", encoding="utf-8", newline="") as csv_file:
373
+ writer = csv.DictWriter(csv_file, fieldnames=fieldnames)
374
+ writer.writeheader()
375
+ for trial in grouped_responses.trials:
376
+ for roi_index, roi_label in enumerate(grouped_responses.roi_labels):
377
+ writer.writerow(
378
+ _trial_response_row(
379
+ trial,
380
+ roi_index=roi_index,
381
+ roi_label=roi_label,
382
+ time_columns=time_columns,
383
+ ),
384
+ )
385
+
386
+ _write_file_atomically(path, write_csv)
313
387
 
314
388
 
315
389
  def write_response_summary_grouped_csv(
@@ -324,17 +398,23 @@ def write_response_summary_grouped_csv(
324
398
 
325
399
  Returns:
326
400
  None.
401
+
402
+ The CSV is written through a temporary sibling path so a failed write leaves
403
+ the previous complete file in place.
327
404
  """
328
- output_path = path.expanduser()
329
- output_path.parent.mkdir(parents=True, exist_ok=True)
330
405
  validate_grouped_roi_responses(grouped_responses)
331
406
  time_columns = _time_columns(grouped_responses.trials)
332
407
  fieldnames = (*_GROUPED_RESPONSE_CSV_BASE_COLUMNS, *time_columns)
333
- with output_path.open("w", encoding="utf-8", newline="") as csv_file:
334
- writer = csv.DictWriter(csv_file, fieldnames=fieldnames)
335
- writer.writeheader()
336
- for row in _grouped_response_rows(grouped_responses, time_columns):
337
- writer.writerow(row)
408
+
409
+ def write_csv(output_path: Path) -> None:
410
+ """Write grouped rows to one complete CSV file."""
411
+ with output_path.open("w", encoding="utf-8", newline="") as csv_file:
412
+ writer = csv.DictWriter(csv_file, fieldnames=fieldnames)
413
+ writer.writeheader()
414
+ for row in _iter_grouped_response_rows(grouped_responses, time_columns):
415
+ writer.writerow(row)
416
+
417
+ _write_file_atomically(path, write_csv)
338
418
 
339
419
 
340
420
  def _write_roi_set(group: h5py.Group, roi_set: RoiSet) -> None:
@@ -347,19 +427,28 @@ def _write_roi_set(group: h5py.Group, roi_set: RoiSet) -> None:
347
427
  Returns:
348
428
  None.
349
429
  """
350
- group.create_dataset("masks", data=roi_set.masks, compression="gzip")
430
+ write_twopy_spatial_orientation(group)
431
+ masks = group.create_dataset("masks", data=roi_set.masks, compression="gzip")
432
+ write_twopy_spatial_orientation(masks)
351
433
  _write_string_dataset(group, "labels", roi_set.labels)
352
434
 
353
435
 
354
- def _read_roi_set(group: h5py.Group) -> RoiSet:
436
+ def _read_roi_set(group: h5py.Group, *, path: Path) -> RoiSet:
355
437
  """Read a persisted ROI set.
356
438
 
357
439
  Args:
358
440
  group: HDF5 ``roi_set`` group.
441
+ path: Analysis output path used in validation errors.
359
442
 
360
443
  Returns:
361
444
  ``RoiSet`` with validated masks and labels.
362
445
  """
446
+ require_twopy_spatial_orientation(group, path=path, label="roi_set")
447
+ require_twopy_spatial_orientation(
448
+ group["masks"],
449
+ path=path,
450
+ label="roi_set/masks",
451
+ )
363
452
  return make_roi_set(
364
453
  require_bool_array(group["masks"][()], name="roi_set/masks", ndim=3),
365
454
  labels=_read_string_dataset(group, "labels"),
@@ -879,10 +968,10 @@ def _trial_response_row(
879
968
  return row
880
969
 
881
970
 
882
- def _grouped_response_rows(
971
+ def _iter_grouped_response_rows(
883
972
  grouped: GroupedRoiResponses,
884
973
  time_columns: tuple[str, ...],
885
- ) -> tuple[dict[str, str | int | float], ...]:
974
+ ) -> Iterator[dict[str, str | int | float]]:
886
975
  """Convert grouped trial responses to mean, SEM, and count CSV rows.
887
976
 
888
977
  Args:
@@ -890,12 +979,12 @@ def _grouped_response_rows(
890
979
  time_columns: Complete CSV time-column union.
891
980
 
892
981
  Returns:
893
- Rows keyed by epoch, ROI, statistic, and relative timepoint.
982
+ Iterator over rows keyed by epoch, ROI, statistic, and relative
983
+ timepoint.
894
984
 
895
985
  Counts are per timepoint rather than one scalar because pre/post context or
896
986
  clipped recording edges can make some trials shorter than others.
897
987
  """
898
- rows: list[dict[str, str | int | float]] = []
899
988
  for epoch_number, epoch_name, trials in _trials_by_epoch(grouped.trials):
900
989
  for roi_index, roi_label in enumerate(grouped.roi_labels):
901
990
  values_by_time = _values_by_time_column(
@@ -903,16 +992,13 @@ def _grouped_response_rows(
903
992
  roi_index=roi_index,
904
993
  time_columns=time_columns,
905
994
  )
906
- rows.extend(
907
- _epoch_roi_statistic_rows(
908
- epoch_number=epoch_number,
909
- epoch_name=epoch_name,
910
- roi_label=roi_label,
911
- values_by_time=values_by_time,
912
- time_columns=time_columns,
913
- ),
995
+ yield from _epoch_roi_statistic_rows(
996
+ epoch_number=epoch_number,
997
+ epoch_name=epoch_name,
998
+ roi_label=roi_label,
999
+ values_by_time=values_by_time,
1000
+ time_columns=time_columns,
914
1001
  )
915
- return tuple(rows)
916
1002
 
917
1003
 
918
1004
  def _trials_by_epoch(
@@ -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")