TwoPy 0.3.8__tar.gz → 0.3.9__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 (262) hide show
  1. {twopy-0.3.8 → twopy-0.3.9}/PKG-INFO +1 -1
  2. {twopy-0.3.8 → twopy-0.3.9}/pyproject.toml +1 -1
  3. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/conversion/alignment_crop.py +34 -2
  4. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/conversion/hdf5_writing.py +5 -0
  5. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/conversion/types.py +1 -0
  6. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/converted.py +34 -2
  7. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/empty_state.py +2 -2
  8. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/plotting/docks/options_panel.py +6 -0
  9. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/plotting/docks/response_plot_widget.py +3 -0
  10. twopy-0.3.9/src/twopy/napari/plotting/motion_summary.py +324 -0
  11. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/plotting/panels.py +4 -0
  12. {twopy-0.3.8 → twopy-0.3.9}/src/twopy.egg-info/PKG-INFO +1 -1
  13. {twopy-0.3.8 → twopy-0.3.9}/src/twopy.egg-info/SOURCES.txt +1 -0
  14. {twopy-0.3.8 → twopy-0.3.9}/tests/test_background_subtraction.py +4 -0
  15. {twopy-0.3.8 → twopy-0.3.9}/tests/test_conversion.py +33 -0
  16. {twopy-0.3.8 → twopy-0.3.9}/tests/test_converted.py +48 -0
  17. {twopy-0.3.8 → twopy-0.3.9}/tests/test_load_workflow.py +1 -0
  18. {twopy-0.3.8 → twopy-0.3.9}/tests/test_napari.py +4 -0
  19. {twopy-0.3.8 → twopy-0.3.9}/tests/test_napari_plot_controls.py +176 -1
  20. {twopy-0.3.8 → twopy-0.3.9}/tests/test_parity_psycho5.py +1 -0
  21. {twopy-0.3.8 → twopy-0.3.9}/tests/test_real_fixture.py +4 -0
  22. {twopy-0.3.8 → twopy-0.3.9}/tests/test_roi.py +4 -0
  23. {twopy-0.3.8 → twopy-0.3.9}/README.md +0 -0
  24. {twopy-0.3.8 → twopy-0.3.9}/setup.cfg +0 -0
  25. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/__init__.py +0 -0
  26. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/__init__.pyi +0 -0
  27. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/_segments.py +0 -0
  28. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/_version.py +0 -0
  29. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/analysis/__init__.py +0 -0
  30. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/analysis/background_subtraction.py +0 -0
  31. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/analysis/dff.py +0 -0
  32. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/analysis/dff_options.py +0 -0
  33. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/analysis/epoch_mapping.py +0 -0
  34. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/analysis/group_matching.py +0 -0
  35. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/analysis/kernels/__init__.py +0 -0
  36. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/analysis/kernels/_fitting.py +0 -0
  37. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/analysis/kernels/_stimulus.py +0 -0
  38. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/analysis/motion.py +0 -0
  39. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/analysis/persistence.py +0 -0
  40. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/analysis/response_context.py +0 -0
  41. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/analysis/response_maps.py +0 -0
  42. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/analysis/response_plotting.py +0 -0
  43. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/analysis/response_processing/__init__.py +0 -0
  44. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/analysis/response_processing/apply.py +0 -0
  45. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/analysis/response_processing/options.py +0 -0
  46. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/analysis/response_processing/persistence.py +0 -0
  47. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/analysis/response_processing/qc.py +0 -0
  48. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/analysis/response_processing/signals.py +0 -0
  49. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/analysis/response_window_options.py +0 -0
  50. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/analysis/responses.py +0 -0
  51. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/analysis/timing.py +0 -0
  52. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/analysis/trials.py +0 -0
  53. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/analysis/workflow.py +0 -0
  54. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/analysis_cache.py +0 -0
  55. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/api.py +0 -0
  56. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/cache_inventory.py +0 -0
  57. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/calibrations/pixel_calibration_profiles.csv +0 -0
  58. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/calibrations/pixel_size.csv +0 -0
  59. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/config.example.yml +0 -0
  60. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/config.py +0 -0
  61. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/conversion/__init__.py +0 -0
  62. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/conversion/api.py +0 -0
  63. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/conversion/matlab_values.py +0 -0
  64. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/conversion/orientation.py +0 -0
  65. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/conversion/source_loading.py +0 -0
  66. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/conversion/stimulus_code.py +0 -0
  67. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/custom/__init__.py +0 -0
  68. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/custom/context.py +0 -0
  69. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/custom/discovery.py +0 -0
  70. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/custom/metadata.py +0 -0
  71. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/custom/native.py +0 -0
  72. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/custom/native_workflows/__init__.py +0 -0
  73. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/custom/native_workflows/direction_selectivity.py +0 -0
  74. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/custom/native_workflows/response_kernels.py +0 -0
  75. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/custom/parameters.py +0 -0
  76. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/custom/provenance.py +0 -0
  77. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/custom/registry.py +0 -0
  78. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/custom/results.py +0 -0
  79. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/custom/runner.py +0 -0
  80. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/custom/workflows.py +0 -0
  81. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/database/__init__.py +0 -0
  82. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/database/cache.py +0 -0
  83. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/database/catalog.py +0 -0
  84. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/database/connection.py +0 -0
  85. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/database/generic.py +0 -0
  86. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/database/modeled.py +0 -0
  87. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/database/search.py +0 -0
  88. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/database/sql.py +0 -0
  89. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/database/types.py +0 -0
  90. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/filenames.py +0 -0
  91. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/frame_ranges.py +0 -0
  92. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/hdf5_utils.py +0 -0
  93. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/inspection.py +0 -0
  94. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/matlab.py +0 -0
  95. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/__init__.py +0 -0
  96. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/__main__.py +0 -0
  97. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/assets/twopy-app-icon-128.png +0 -0
  98. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/assets/twopy-app-icon-16.png +0 -0
  99. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/assets/twopy-app-icon-256.png +0 -0
  100. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/assets/twopy-app-icon-32.png +0 -0
  101. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/assets/twopy-app-icon-512.png +0 -0
  102. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/assets/twopy-app-icon-64.png +0 -0
  103. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/assets/twopy-app-icon.icns +0 -0
  104. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/assets/twopy-app-icon.png +0 -0
  105. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/controls.py +0 -0
  106. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/custom_tab.py +0 -0
  107. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/database_favorite_editor.py +0 -0
  108. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/database_favorites.py +0 -0
  109. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/database_search.py +0 -0
  110. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/dims.py +0 -0
  111. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/display.py +0 -0
  112. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/display_paths.py +0 -0
  113. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/errors.py +0 -0
  114. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/group_matching/__init__.py +0 -0
  115. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/group_matching/cards.py +0 -0
  116. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/group_matching/fov_assignment.py +0 -0
  117. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/group_matching/fov_cards.py +0 -0
  118. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/group_matching/images.py +0 -0
  119. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/group_matching/protocols.py +0 -0
  120. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/group_matching/response_preview.py +0 -0
  121. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/group_matching/responses.py +0 -0
  122. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/group_matching/roi_assignment.py +0 -0
  123. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/group_matching/roi_cards.py +0 -0
  124. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/group_matching/style.py +0 -0
  125. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/group_matching/tables.py +0 -0
  126. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/group_matching/window.py +0 -0
  127. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/interactive.py +0 -0
  128. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/latest_worker.py +0 -0
  129. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/launcher.py +0 -0
  130. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/live_analysis.py +0 -0
  131. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/load_controller.py +0 -0
  132. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/load_progress.py +0 -0
  133. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/load_workflow.py +0 -0
  134. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/loaded_recordings_csv.py +0 -0
  135. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/loading.py +0 -0
  136. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/movie.py +0 -0
  137. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/output_routing.py +0 -0
  138. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/paths.py +0 -0
  139. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/plotting/__init__.py +0 -0
  140. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/plotting/data.py +0 -0
  141. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/plotting/dff_options.py +0 -0
  142. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/plotting/docks/__init__.py +0 -0
  143. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/plotting/docks/api.py +0 -0
  144. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/plotting/docks/dynamic_options.py +0 -0
  145. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/plotting/docks/paths.py +0 -0
  146. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/plotting/docks/plot_area.py +0 -0
  147. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/plotting/docks/response_map_area.py +0 -0
  148. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/plotting/docks/save_actions.py +0 -0
  149. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/plotting/docks/visibility_state.py +0 -0
  150. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/plotting/export.py +0 -0
  151. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/plotting/export_controls.py +0 -0
  152. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/plotting/form_controls.py +0 -0
  153. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/plotting/label_visibility.py +0 -0
  154. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/plotting/normalization_options.py +0 -0
  155. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/plotting/options.py +0 -0
  156. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/plotting/preview_strip.py +0 -0
  157. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/plotting/processing_options.py +0 -0
  158. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/plotting/response_map_colors.py +0 -0
  159. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/plotting/response_map_display.py +0 -0
  160. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/plotting/response_map_options.py +0 -0
  161. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/plotting/response_window_options.py +0 -0
  162. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/plotting/roi_generation/__init__.py +0 -0
  163. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/plotting/roi_generation/actions.py +0 -0
  164. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/plotting/roi_generation/controls.py +0 -0
  165. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/plotting/roi_generation/options.py +0 -0
  166. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/plotting/widgets.py +0 -0
  167. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/protocols.py +0 -0
  168. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/responses.py +0 -0
  169. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/roi.py +0 -0
  170. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/roi_signatures.py +0 -0
  171. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/session.py +0 -0
  172. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/sidebar.py +0 -0
  173. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/state.py +0 -0
  174. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/text.py +0 -0
  175. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/trial_timeline.py +0 -0
  176. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/types.py +0 -0
  177. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/version_check.py +0 -0
  178. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/napari/viewer.py +0 -0
  179. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/parity/__init__.py +0 -0
  180. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/parity/dff.py +0 -0
  181. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/parity/psycho5.py +0 -0
  182. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/parity/roi.py +0 -0
  183. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/parity/roi_extraction.py +0 -0
  184. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/parity/saved_analysis.py +0 -0
  185. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/photodiode.py +0 -0
  186. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/photodiode_classification.py +0 -0
  187. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/pixel_calibration.py +0 -0
  188. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/pixel_calibration_profiles.py +0 -0
  189. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/response_roi_extraction.py +0 -0
  190. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/roi.py +0 -0
  191. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/roi_extraction.py +0 -0
  192. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/roi_mask_cleanup.py +0 -0
  193. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/session.py +0 -0
  194. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/spatial.py +0 -0
  195. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/spatial_orientation.py +0 -0
  196. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/stimulus.py +0 -0
  197. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/synchronization.py +0 -0
  198. {twopy-0.3.8 → twopy-0.3.9}/src/twopy/typing_guards.py +0 -0
  199. {twopy-0.3.8 → twopy-0.3.9}/src/twopy.egg-info/dependency_links.txt +0 -0
  200. {twopy-0.3.8 → twopy-0.3.9}/src/twopy.egg-info/entry_points.txt +0 -0
  201. {twopy-0.3.8 → twopy-0.3.9}/src/twopy.egg-info/requires.txt +0 -0
  202. {twopy-0.3.8 → twopy-0.3.9}/src/twopy.egg-info/top_level.txt +0 -0
  203. {twopy-0.3.8 → twopy-0.3.9}/tests/test_api.py +0 -0
  204. {twopy-0.3.8 → twopy-0.3.9}/tests/test_cache_inventory.py +0 -0
  205. {twopy-0.3.8 → twopy-0.3.9}/tests/test_config.py +0 -0
  206. {twopy-0.3.8 → twopy-0.3.9}/tests/test_custom_workflows.py +0 -0
  207. {twopy-0.3.8 → twopy-0.3.9}/tests/test_database.py +0 -0
  208. {twopy-0.3.8 → twopy-0.3.9}/tests/test_dff.py +0 -0
  209. {twopy-0.3.8 → twopy-0.3.9}/tests/test_epoch_mapping.py +0 -0
  210. {twopy-0.3.8 → twopy-0.3.9}/tests/test_filenames.py +0 -0
  211. {twopy-0.3.8 → twopy-0.3.9}/tests/test_frame_ranges.py +0 -0
  212. {twopy-0.3.8 → twopy-0.3.9}/tests/test_function_inventory.py +0 -0
  213. {twopy-0.3.8 → twopy-0.3.9}/tests/test_group_matching.py +0 -0
  214. {twopy-0.3.8 → twopy-0.3.9}/tests/test_hdf5_utils.py +0 -0
  215. {twopy-0.3.8 → twopy-0.3.9}/tests/test_import.py +0 -0
  216. {twopy-0.3.8 → twopy-0.3.9}/tests/test_inspection.py +0 -0
  217. {twopy-0.3.8 → twopy-0.3.9}/tests/test_kernels.py +0 -0
  218. {twopy-0.3.8 → twopy-0.3.9}/tests/test_matlab.py +0 -0
  219. {twopy-0.3.8 → twopy-0.3.9}/tests/test_motion.py +0 -0
  220. {twopy-0.3.8 → twopy-0.3.9}/tests/test_napari_database_search.py +0 -0
  221. {twopy-0.3.8 → twopy-0.3.9}/tests/test_napari_display.py +0 -0
  222. {twopy-0.3.8 → twopy-0.3.9}/tests/test_napari_export.py +0 -0
  223. {twopy-0.3.8 → twopy-0.3.9}/tests/test_napari_group_matching_responses.py +0 -0
  224. {twopy-0.3.8 → twopy-0.3.9}/tests/test_napari_group_matching_roi_cards.py +0 -0
  225. {twopy-0.3.8 → twopy-0.3.9}/tests/test_napari_live_controller.py +0 -0
  226. {twopy-0.3.8 → twopy-0.3.9}/tests/test_napari_load_controller.py +0 -0
  227. {twopy-0.3.8 → twopy-0.3.9}/tests/test_napari_load_progress.py +0 -0
  228. {twopy-0.3.8 → twopy-0.3.9}/tests/test_napari_load_tab.py +0 -0
  229. {twopy-0.3.8 → twopy-0.3.9}/tests/test_napari_loaded_recordings.py +0 -0
  230. {twopy-0.3.8 → twopy-0.3.9}/tests/test_napari_path_resolution.py +0 -0
  231. {twopy-0.3.8 → twopy-0.3.9}/tests/test_napari_plot_data.py +0 -0
  232. {twopy-0.3.8 → twopy-0.3.9}/tests/test_napari_plot_widgets.py +0 -0
  233. {twopy-0.3.8 → twopy-0.3.9}/tests/test_napari_processing_controls.py +0 -0
  234. {twopy-0.3.8 → twopy-0.3.9}/tests/test_napari_response_map_display.py +0 -0
  235. {twopy-0.3.8 → twopy-0.3.9}/tests/test_napari_response_workflow.py +0 -0
  236. {twopy-0.3.8 → twopy-0.3.9}/tests/test_napari_roi_generation.py +0 -0
  237. {twopy-0.3.8 → twopy-0.3.9}/tests/test_napari_roi_visibility.py +0 -0
  238. {twopy-0.3.8 → twopy-0.3.9}/tests/test_napari_text.py +0 -0
  239. {twopy-0.3.8 → twopy-0.3.9}/tests/test_napari_timeline.py +0 -0
  240. {twopy-0.3.8 → twopy-0.3.9}/tests/test_packaging.py +0 -0
  241. {twopy-0.3.8 → twopy-0.3.9}/tests/test_parity.py +0 -0
  242. {twopy-0.3.8 → twopy-0.3.9}/tests/test_parity_roi_extraction.py +0 -0
  243. {twopy-0.3.8 → twopy-0.3.9}/tests/test_persistence.py +0 -0
  244. {twopy-0.3.8 → twopy-0.3.9}/tests/test_photodiode_classification.py +0 -0
  245. {twopy-0.3.8 → twopy-0.3.9}/tests/test_pixel_calibration.py +0 -0
  246. {twopy-0.3.8 → twopy-0.3.9}/tests/test_pixel_calibration_profiles.py +0 -0
  247. {twopy-0.3.8 → twopy-0.3.9}/tests/test_recording_timing.py +0 -0
  248. {twopy-0.3.8 → twopy-0.3.9}/tests/test_response_map_area.py +0 -0
  249. {twopy-0.3.8 → twopy-0.3.9}/tests/test_response_maps.py +0 -0
  250. {twopy-0.3.8 → twopy-0.3.9}/tests/test_response_processing.py +0 -0
  251. {twopy-0.3.8 → twopy-0.3.9}/tests/test_response_roi_extraction.py +0 -0
  252. {twopy-0.3.8 → twopy-0.3.9}/tests/test_response_window_options.py +0 -0
  253. {twopy-0.3.8 → twopy-0.3.9}/tests/test_responses.py +0 -0
  254. {twopy-0.3.8 → twopy-0.3.9}/tests/test_roi_extraction.py +0 -0
  255. {twopy-0.3.8 → twopy-0.3.9}/tests/test_roi_mask_cleanup.py +0 -0
  256. {twopy-0.3.8 → twopy-0.3.9}/tests/test_saved_analysis.py +0 -0
  257. {twopy-0.3.8 → twopy-0.3.9}/tests/test_session.py +0 -0
  258. {twopy-0.3.8 → twopy-0.3.9}/tests/test_stimulus.py +0 -0
  259. {twopy-0.3.8 → twopy-0.3.9}/tests/test_synchronization.py +0 -0
  260. {twopy-0.3.8 → twopy-0.3.9}/tests/test_trials.py +0 -0
  261. {twopy-0.3.8 → twopy-0.3.9}/tests/test_version_check.py +0 -0
  262. {twopy-0.3.8 → twopy-0.3.9}/tests/test_workflow.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: twopy
3
- Version: 0.3.8
3
+ Version: 0.3.9
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.3.8"
7
+ version = "0.3.9"
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"
@@ -67,6 +67,12 @@ def load_alignment_valid_crop(
67
67
  f"[{frame_start}, {frame_stop}) for {alignment.shape[0]} rows"
68
68
  )
69
69
  raise ValueError(msg)
70
+ if alignment.shape[0] < aligned_movie.shape[0]:
71
+ msg = (
72
+ "Alignment text must have at least one row per aligned movie frame: "
73
+ f"{alignment.shape[0]} rows for {aligned_movie.shape[0]} frames"
74
+ )
75
+ raise ValueError(msg)
70
76
 
71
77
  alignment_window = alignment[frame_start:frame_stop, :]
72
78
  line_scan = _is_line_scan(acquisition)
@@ -79,10 +85,14 @@ def load_alignment_valid_crop(
79
85
  source_x_pixel_count=source_spatial_shape[0],
80
86
  line_scan=line_scan,
81
87
  )
82
- alignment_shift_pixels = _alignment_shift_pixels(
83
- alignment[: aligned_movie.shape[0], :],
88
+ full_alignment = alignment[: aligned_movie.shape[0], :]
89
+ alignment_offset_pixels = _alignment_offset_pixels(
90
+ full_alignment,
84
91
  line_scan=line_scan,
85
92
  )
93
+ alignment_shift_pixels = _alignment_shift_pixels(
94
+ full_alignment, line_scan=line_scan
95
+ )
86
96
  motion_artifact_mask = alignment_shift_pixels > motion_threshold_pixels
87
97
  kept_alignment, over_moved_count = _remove_over_moved_alignment_rows(
88
98
  alignment_window,
@@ -103,6 +113,7 @@ def load_alignment_valid_crop(
103
113
  y_cutoff_pixels=y_cutoff,
104
114
  over_moved_frame_count=over_moved_count,
105
115
  motion_threshold_pixels=motion_threshold_pixels,
116
+ alignment_offset_pixels=alignment_offset_pixels,
106
117
  alignment_shift_pixels=alignment_shift_pixels,
107
118
  motion_artifact_mask=motion_artifact_mask,
108
119
  )
@@ -212,6 +223,27 @@ def _alignment_shift_pixels(
212
223
  return np.sqrt(alignment[:, 0] ** 2 + alignment[:, 1] ** 2)
213
224
 
214
225
 
226
+ def _alignment_offset_pixels(
227
+ alignment: npt.NDArray[np.float64],
228
+ *,
229
+ line_scan: bool,
230
+ ) -> npt.NDArray[np.float64]:
231
+ """Return signed x/y alignment offsets in pixels.
232
+
233
+ Args:
234
+ alignment: Alignment table with x/y offset columns.
235
+ line_scan: Whether this recording is a line scan.
236
+
237
+ Returns:
238
+ Array shaped ``(frames, 2)`` with x then y offsets.
239
+ """
240
+ offsets = np.zeros((alignment.shape[0], 2), dtype=np.float64)
241
+ offsets[:, 0] = alignment[:, 0]
242
+ if not line_scan:
243
+ offsets[:, 1] = alignment[:, 1]
244
+ return offsets
245
+
246
+
215
247
  def _alignment_crop_from_offsets(
216
248
  alignment: npt.NDArray[np.float64],
217
249
  *,
@@ -198,6 +198,11 @@ def _write_alignment_crop_group(
198
198
  crop_group.attrs["y_cutoff_pixels"] = alignment_crop.y_cutoff_pixels
199
199
  crop_group.attrs["over_moved_frame_count"] = alignment_crop.over_moved_frame_count
200
200
  crop_group.attrs["motion_threshold_pixels"] = alignment_crop.motion_threshold_pixels
201
+ crop_group.create_dataset(
202
+ "alignment_offset_pixels",
203
+ data=alignment_crop.alignment_offset_pixels,
204
+ compression="gzip",
205
+ )
201
206
  crop_group.create_dataset(
202
207
  "alignment_shift_pixels",
203
208
  data=alignment_crop.alignment_shift_pixels,
@@ -255,6 +255,7 @@ class AlignmentCropAudit:
255
255
  y_cutoff_pixels: int
256
256
  over_moved_frame_count: int
257
257
  motion_threshold_pixels: float
258
+ alignment_offset_pixels: npt.NDArray[np.float64]
258
259
  alignment_shift_pixels: npt.NDArray[np.float64]
259
260
  motion_artifact_mask: npt.NDArray[np.bool_]
260
261
 
@@ -159,8 +159,8 @@ class RecordingData:
159
159
 
160
160
  Inputs: twopy-converted HDF5 files.
161
161
  Outputs: metadata dictionaries, stimulus arrays with column names,
162
- photodiode vectors, mean image, frame-count audit values, and a lazy movie
163
- reader.
162
+ photodiode vectors, mean image, alignment crop and motion arrays,
163
+ frame-count audit values, and a lazy movie reader.
164
164
 
165
165
  This object is the analysis-facing representation of one converted
166
166
  recording. It deliberately excludes source MATLAB/TIFF ownership.
@@ -181,6 +181,7 @@ class RecordingData:
181
181
  high_res_pd: npt.NDArray[np.float64]
182
182
  mean_image: npt.NDArray[np.float64]
183
183
  alignment_valid_crop: SpatialCrop
184
+ alignment_offset_pixels: npt.NDArray[np.float64] | None
184
185
  alignment_shift_pixels: npt.NDArray[np.float64]
185
186
  motion_artifact_mask: npt.NDArray[np.bool_]
186
187
  frame_counts: FrameCountAudit
@@ -278,6 +279,7 @@ def load_converted_recording(
278
279
  ndim=2,
279
280
  ),
280
281
  alignment_valid_crop=_read_alignment_valid_crop(h5_file),
282
+ alignment_offset_pixels=_read_alignment_offset_pixels(h5_file),
281
283
  alignment_shift_pixels=require_float64_array(
282
284
  h5_file["movie/alignment_valid_crop/alignment_shift_pixels"][()],
283
285
  name="movie/alignment_valid_crop/alignment_shift_pixels",
@@ -440,6 +442,15 @@ def _validate_loaded_recording(recording: RecordingData) -> None:
440
442
  f"{recording.movie.shape[1:]}"
441
443
  )
442
444
  raise ValueError(msg)
445
+ if (
446
+ recording.alignment_offset_pixels is not None
447
+ and recording.alignment_offset_pixels.shape != (frame_count, 2)
448
+ ):
449
+ msg = (
450
+ "alignment_offset_pixels must have x/y values for each movie frame; "
451
+ f"got {recording.alignment_offset_pixels.shape} for {frame_count} frames"
452
+ )
453
+ raise ValueError(msg)
443
454
  if recording.alignment_shift_pixels.shape != (frame_count,):
444
455
  msg = (
445
456
  "alignment_shift_pixels must have one value per movie frame; "
@@ -712,6 +723,27 @@ def _read_alignment_valid_crop(h5_file: h5py.File) -> SpatialCrop:
712
723
  )
713
724
 
714
725
 
726
+ def _read_alignment_offset_pixels(
727
+ h5_file: h5py.File,
728
+ ) -> npt.NDArray[np.float64] | None:
729
+ """Read optional signed x/y alignment offsets from converted HDF5.
730
+
731
+ Args:
732
+ h5_file: Open ``recording_data.h5`` file.
733
+
734
+ Returns:
735
+ Per-frame x/y offsets, or ``None`` for older converted files.
736
+ """
737
+ dataset_name = "movie/alignment_valid_crop/alignment_offset_pixels"
738
+ if dataset_name not in h5_file:
739
+ return None
740
+ return require_float64_array(
741
+ h5_file[dataset_name][()],
742
+ name=dataset_name,
743
+ ndim=2,
744
+ )
745
+
746
+
715
747
  def _normalize_spatial_crop(
716
748
  spatial_shape: tuple[int, int],
717
749
  spatial_crop: SpatialCrop | None,
@@ -135,13 +135,13 @@ def empty_viewer_message(*, update_command: str | None = None) -> str:
135
135
  """Return the empty-viewer text for the current update state."""
136
136
  if not update_command:
137
137
  return _BASE_EMPTY_VIEWER_MESSAGE
138
- return f"{_BASE_EMPTY_VIEWER_MESSAGE}\n\nUpdate\n{update_command}"
138
+ return f"{_BASE_EMPTY_VIEWER_MESSAGE}\n\n{update_command}"
139
139
 
140
140
 
141
141
  def _is_empty_viewer_message(text: str) -> bool:
142
142
  """Return whether text is owned by twopy's empty-recording overlay."""
143
143
  return text == EMPTY_VIEWER_MESSAGE or text.startswith(
144
- f"{EMPTY_VIEWER_MESSAGE}\n\nUpdate\n"
144
+ f"{EMPTY_VIEWER_MESSAGE}\n\n"
145
145
  )
146
146
 
147
147
 
@@ -27,6 +27,7 @@ from twopy.napari.plotting.export_controls import (
27
27
  ResponseExportState,
28
28
  create_response_export_tab,
29
29
  )
30
+ from twopy.napari.plotting.motion_summary import MotionSummaryWidget
30
31
  from twopy.napari.plotting.normalization_options import NormalizationOptionsWidget
31
32
  from twopy.napari.plotting.panels import (
32
33
  SidebarTextLabel,
@@ -56,6 +57,7 @@ class ResponseOptionsPanel:
56
57
  roi_save_path_label: Metadata-tab label showing the ROI output path.
57
58
  update_status_label: Metadata-tab label showing save or processing status.
58
59
  update_notice_label: Metadata-tab label showing the update command.
60
+ motion_summary_widget: Metadata-tab widget showing alignment movement.
59
61
  export_status_label: Export-tab label showing save/export action status.
60
62
  reload_saved_button: Load-tab button that reloads persisted response
61
63
  outputs.
@@ -83,6 +85,7 @@ class ResponseOptionsPanel:
83
85
  roi_save_path_label: SidebarTextLabel
84
86
  update_status_label: SidebarTextLabel
85
87
  update_notice_label: SidebarTextLabel
88
+ motion_summary_widget: MotionSummaryWidget
86
89
  export_status_label: SidebarTextLabel
87
90
  reload_saved_button: QPushButton
88
91
  plot_options_layout: QVBoxLayout
@@ -153,6 +156,7 @@ def create_response_options_panel(
153
156
  roi_save_path_label = SidebarTextLabel(f"ROI output: {DEFAULT_PATH_TEXT}")
154
157
  update_status_label = SidebarTextLabel("")
155
158
  update_notice_label = SidebarTextLabel("")
159
+ motion_summary_widget = MotionSummaryWidget()
156
160
  export_status_label = SidebarTextLabel("Exports save beside the recording.")
157
161
 
158
162
  plot_options_layout = QVBoxLayout()
@@ -209,6 +213,7 @@ def create_response_options_panel(
209
213
  roi_output_label=roi_save_path_label,
210
214
  status_label=update_status_label,
211
215
  update_notice_label=update_notice_label,
216
+ motion_summary_widget=motion_summary_widget,
212
217
  ),
213
218
  "Metadata",
214
219
  )
@@ -232,6 +237,7 @@ def create_response_options_panel(
232
237
  roi_save_path_label=roi_save_path_label,
233
238
  update_status_label=update_status_label,
234
239
  update_notice_label=update_notice_label,
240
+ motion_summary_widget=motion_summary_widget,
235
241
  export_status_label=export_status_label,
236
242
  reload_saved_button=reload_saved_button,
237
243
  plot_options_layout=plot_options_layout,
@@ -275,6 +275,7 @@ class _ResponsePlotWidget(QWidget):
275
275
  self._roi_save_path_label = options_panel.roi_save_path_label
276
276
  self._update_status_label = options_panel.update_status_label
277
277
  self._update_notice_label = options_panel.update_notice_label
278
+ self._motion_summary_widget = options_panel.motion_summary_widget
278
279
  self._export_status_label = options_panel.export_status_label
279
280
  self._reload_saved_button = options_panel.reload_saved_button
280
281
  self._plot_options_layout = options_panel.plot_options_layout
@@ -496,6 +497,7 @@ class _ResponsePlotWidget(QWidget):
496
497
  self._analysis_path_label.setText(f"Analysis output: {DEFAULT_PATH_TEXT}")
497
498
  self._roi_save_path_label.setText(f"ROI output: {DEFAULT_PATH_TEXT}")
498
499
  self._update_status_label.setText("")
500
+ self._motion_summary_widget.set_recording(None)
499
501
  self._export_status_label.setText("Exports save beside the recording.")
500
502
  self._reset_empty_option_tabs()
501
503
  self._set_status("No recording loaded.")
@@ -528,6 +530,7 @@ class _ResponsePlotWidget(QWidget):
528
530
  self._recording = recording
529
531
  self._output_route = output_route or recording_output_route(recording)
530
532
  self._roi_generation_widget.set_recording(recording)
533
+ self._motion_summary_widget.set_recording(recording)
531
534
  self._analysis_path = resolved_analysis_path(None, recording)
532
535
  epoch_names = stimulus_epoch_names_by_number(recording)
533
536
  self._delta_f_over_f_options_widget.set_epoch_choices(
@@ -0,0 +1,324 @@
1
+ """Inline Motion metadata widget for the twopy napari sidebar.
2
+
3
+ Inputs: converted recording alignment crop, per-frame signed movement offsets,
4
+ movement magnitudes, and motion-artifact masks.
5
+ Outputs: a compact Metadata-tab section with crop metrics and a movement plot.
6
+ """
7
+
8
+ import numpy as np
9
+ import numpy.typing as npt
10
+ from qtpy.QtCore import QRect, Qt
11
+ from qtpy.QtGui import QColor, QPainter, QPainterPath, QPaintEvent, QPen
12
+ from qtpy.QtWidgets import QGroupBox, QSizePolicy, QVBoxLayout, QWidget
13
+
14
+ from twopy.converted import RecordingData
15
+ from twopy.napari.plotting.panels import SidebarTextLabel
16
+
17
+ __all__ = ["MotionSummaryWidget"]
18
+
19
+
20
+ class MotionSummaryWidget(QGroupBox):
21
+ """Metadata-tab section that shows alignment movement over recording frames.
22
+
23
+ Inputs: a loaded converted recording.
24
+ Outputs: a compact crop/motion text summary plus a movement plot.
25
+ """
26
+
27
+ def __init__(self) -> None:
28
+ """Create an empty motion summary section."""
29
+ super().__init__("Motion")
30
+ self._summary_label = SidebarTextLabel("No recording loaded.")
31
+ self._plot = _MotionPlotWidget()
32
+ layout = QVBoxLayout()
33
+ layout.setSpacing(4)
34
+ layout.addWidget(self._summary_label)
35
+ layout.addWidget(self._plot)
36
+ self.setLayout(layout)
37
+
38
+ def set_recording(self, recording: RecordingData | None) -> None:
39
+ """Show movement metadata for one recording.
40
+
41
+ Args:
42
+ recording: Loaded converted recording, or ``None`` for empty state.
43
+
44
+ Returns:
45
+ None.
46
+ """
47
+ if recording is None:
48
+ self._summary_label.setText("No recording loaded.")
49
+ self._plot.set_motion(None, None, None)
50
+ return
51
+ self._summary_label.setText(_motion_summary_text(recording))
52
+ self._plot.set_motion(
53
+ recording.alignment_offset_pixels,
54
+ recording.alignment_shift_pixels,
55
+ recording.motion_artifact_mask,
56
+ )
57
+
58
+
59
+ class _MotionPlotWidget(QWidget):
60
+ """Small inline plot for per-frame left-right and up-down movement."""
61
+
62
+ def __init__(self) -> None:
63
+ """Create an empty plot widget sized for the metadata sidebar."""
64
+ super().__init__()
65
+ self._values = np.empty((0, 0), dtype=np.float64)
66
+ self._has_xy_offsets = False
67
+ self._motion_mask = np.array([], dtype=np.bool_)
68
+ self.setFixedHeight(120)
69
+ self.setMinimumWidth(1)
70
+ self.setSizePolicy(QSizePolicy.Policy.Ignored, QSizePolicy.Policy.Fixed)
71
+
72
+ def set_motion(
73
+ self,
74
+ offset_pixels: npt.NDArray[np.float64] | None,
75
+ shift_pixels: npt.NDArray[np.float64] | None,
76
+ motion_mask: npt.NDArray[np.bool_] | None,
77
+ ) -> None:
78
+ """Replace the plotted movement arrays.
79
+
80
+ Args:
81
+ offset_pixels: Optional signed x/y movement offsets per movie frame.
82
+ shift_pixels: Total movement magnitude in pixels per movie frame.
83
+ motion_mask: Boolean mask marking high-motion frames.
84
+
85
+ Returns:
86
+ None.
87
+ """
88
+ values: npt.NDArray[np.float64]
89
+ self._has_xy_offsets = offset_pixels is not None
90
+ if offset_pixels is not None:
91
+ values = np.asarray(offset_pixels, dtype=np.float64)
92
+ if values.ndim != 2 or values.shape[1] != 2:
93
+ values = np.empty((0, 0), dtype=np.float64)
94
+ elif shift_pixels is not None:
95
+ values = np.ravel(np.asarray(shift_pixels, dtype=np.float64))[:, None]
96
+ else:
97
+ self._values = np.empty((0, 0), dtype=np.float64)
98
+ self._motion_mask = np.array([], dtype=np.bool_)
99
+ self.update()
100
+ return
101
+ self._values = (
102
+ values
103
+ if bool(np.all(np.isfinite(values)))
104
+ else np.nan_to_num(values, nan=0.0, posinf=0.0, neginf=0.0)
105
+ )
106
+ if motion_mask is None:
107
+ self._motion_mask = np.zeros(self._values.shape[0], dtype=np.bool_)
108
+ else:
109
+ mask = np.ravel(np.asarray(motion_mask, dtype=np.bool_))
110
+ self._motion_mask = (
111
+ mask
112
+ if mask.shape == (self._values.shape[0],)
113
+ else np.zeros(self._values.shape[0], dtype=np.bool_)
114
+ )
115
+ self.update()
116
+
117
+ def paintEvent(self, a0: QPaintEvent | None) -> None:
118
+ """Draw the movement plot.
119
+
120
+ Args:
121
+ a0: Qt paint event.
122
+
123
+ Returns:
124
+ None.
125
+ """
126
+ del a0
127
+ painter = QPainter(self)
128
+ painter.setRenderHint(QPainter.RenderHint.Antialiasing)
129
+ painter.fillRect(self.rect(), QColor("#20252d"))
130
+ if self._values.size == 0:
131
+ painter.setPen(QColor("#a9b4c0"))
132
+ painter.drawText(
133
+ self.rect(),
134
+ Qt.AlignmentFlag.AlignCenter,
135
+ "No motion data",
136
+ )
137
+ painter.end()
138
+ return
139
+ _draw_motion_plot(
140
+ painter,
141
+ self.rect(),
142
+ self._values,
143
+ self._motion_mask,
144
+ has_xy_offsets=self._has_xy_offsets,
145
+ )
146
+ painter.end()
147
+
148
+
149
+ def _motion_summary_text(recording: RecordingData) -> str:
150
+ """Return compact crop and motion metrics for the Metadata tab."""
151
+ crop = recording.alignment_valid_crop
152
+ top = crop.axis0_start
153
+ bottom = crop.original_shape[0] - crop.axis0_stop
154
+ left = crop.axis1_start
155
+ right = crop.original_shape[1] - crop.axis1_stop
156
+ shifts = np.ravel(recording.alignment_shift_pixels)
157
+ finite_shifts = shifts[np.isfinite(shifts)]
158
+ bad_count = int(np.count_nonzero(recording.motion_artifact_mask))
159
+ frame_count = int(shifts.shape[0])
160
+ p95 = float(np.percentile(finite_shifts, 95)) if finite_shifts.size else 0.0
161
+ max_shift = float(np.max(finite_shifts)) if finite_shifts.size else 0.0
162
+ return (
163
+ f"Visible crop: {crop.shape[1]} x {crop.shape[0]} px "
164
+ f"from a {crop.original_shape[1]} x {crop.original_shape[0]} px frame\n"
165
+ "Removed border: "
166
+ f"left {left} px, right {right} px, top {top} px, bottom {bottom} px\n"
167
+ f"Total movement: 95% of frames moved <= {p95:.2f} px; "
168
+ f"max {max_shift:.2f} px; high-motion {bad_count}/{frame_count} frames\n"
169
+ + _plot_description(recording)
170
+ )
171
+
172
+
173
+ def _plot_description(recording: RecordingData) -> str:
174
+ """Return the plot legend text for available movement data."""
175
+ if recording.alignment_offset_pixels is None:
176
+ return (
177
+ "Plot: total movement only. Reconvert this recording to see "
178
+ "independent x/y pixel movement."
179
+ )
180
+ return "Plot: left-right movement in blue, up-down movement in yellow"
181
+
182
+
183
+ def _draw_motion_plot(
184
+ painter: QPainter,
185
+ widget_rect: QRect,
186
+ values: npt.NDArray[np.float64],
187
+ motion_mask: npt.NDArray[np.bool_],
188
+ *,
189
+ has_xy_offsets: bool,
190
+ ) -> None:
191
+ """Draw axes, movement trace, and high-motion markers."""
192
+ if not has_xy_offsets:
193
+ _draw_magnitude_plot(painter, widget_rect, values[:, 0], motion_mask)
194
+ return
195
+ rect = widget_rect
196
+ plot_left = float(rect.left()) + 42.0
197
+ plot_top = float(rect.top()) + 10.0
198
+ plot_width = max(1.0, float(rect.width()) - 54.0)
199
+ plot_height = max(1.0, float(rect.height()) - 32.0)
200
+ plot_bottom = plot_top + plot_height
201
+ y_mid = plot_top + plot_height / 2.0
202
+ y_abs_max = max(1.0, float(np.max(np.abs(values))))
203
+
204
+ painter.setPen(QPen(QColor("#697584"), 1))
205
+ painter.drawLine(
206
+ int(plot_left),
207
+ int(plot_bottom),
208
+ int(plot_left + plot_width),
209
+ int(plot_bottom),
210
+ )
211
+ painter.drawLine(int(plot_left), int(plot_top), int(plot_left), int(plot_bottom))
212
+ painter.setPen(QColor("#a9b4c0"))
213
+ painter.drawText(2, int(plot_top + 8), f"+{y_abs_max:.1f}")
214
+ painter.drawText(2, int(y_mid + 4), "0")
215
+ painter.drawText(2, int(plot_bottom), f"-{y_abs_max:.1f}")
216
+
217
+ frame_count = int(values.shape[0])
218
+ if frame_count == 1:
219
+ x_values = np.array([plot_left], dtype=np.float64)
220
+ else:
221
+ x_values = np.linspace(plot_left, plot_left + plot_width, frame_count)
222
+
223
+ _draw_offset_line(
224
+ painter,
225
+ x_values=x_values,
226
+ offsets=values[:, 0],
227
+ y_mid=y_mid,
228
+ y_scale=plot_height / (2.0 * y_abs_max),
229
+ color=QColor("#6ec6ff"),
230
+ )
231
+ _draw_offset_line(
232
+ painter,
233
+ x_values=x_values,
234
+ offsets=values[:, 1],
235
+ y_mid=y_mid,
236
+ y_scale=plot_height / (2.0 * y_abs_max),
237
+ color=QColor("#f2c14e"),
238
+ )
239
+ painter.setPen(QColor("#6ec6ff"))
240
+ painter.drawText(int(plot_left + plot_width - 34.0), int(plot_top + 12.0), "x")
241
+ painter.setPen(QColor("#f2c14e"))
242
+ painter.drawText(int(plot_left + plot_width - 18.0), int(plot_top + 12.0), "y")
243
+
244
+ if motion_mask.shape == (frame_count,) and bool(np.any(motion_mask)):
245
+ painter.setPen(QPen(QColor("#ff6b6b"), 1))
246
+ for x_value in x_values[motion_mask]:
247
+ painter.drawLine(
248
+ int(x_value),
249
+ int(plot_top),
250
+ int(x_value),
251
+ int(plot_bottom),
252
+ )
253
+
254
+
255
+ def _draw_magnitude_plot(
256
+ painter: QPainter,
257
+ widget_rect: QRect,
258
+ shift_pixels: npt.NDArray[np.float64],
259
+ motion_mask: npt.NDArray[np.bool_],
260
+ ) -> None:
261
+ """Draw one total movement magnitude trace."""
262
+ rect = widget_rect
263
+ plot_left = float(rect.left()) + 42.0
264
+ plot_top = float(rect.top()) + 10.0
265
+ plot_width = max(1.0, float(rect.width()) - 54.0)
266
+ plot_height = max(1.0, float(rect.height()) - 32.0)
267
+ plot_bottom = plot_top + plot_height
268
+ y_max = max(1.0, float(np.max(shift_pixels)))
269
+
270
+ painter.setPen(QPen(QColor("#697584"), 1))
271
+ painter.drawLine(
272
+ int(plot_left),
273
+ int(plot_bottom),
274
+ int(plot_left + plot_width),
275
+ int(plot_bottom),
276
+ )
277
+ painter.drawLine(int(plot_left), int(plot_top), int(plot_left), int(plot_bottom))
278
+ painter.setPen(QColor("#a9b4c0"))
279
+ painter.drawText(2, int(plot_top + 8), f"{y_max:.1f}")
280
+ painter.drawText(2, int(plot_bottom), "0")
281
+
282
+ frame_count = int(shift_pixels.shape[0])
283
+ if frame_count == 1:
284
+ x_values = np.array([plot_left], dtype=np.float64)
285
+ else:
286
+ x_values = np.linspace(plot_left, plot_left + plot_width, frame_count)
287
+
288
+ y_values = plot_bottom - shift_pixels * (plot_height / y_max)
289
+ path = QPainterPath()
290
+ path.moveTo(float(x_values[0]), float(y_values[0]))
291
+ for x_value, y_value in zip(x_values[1:], y_values[1:], strict=False):
292
+ path.lineTo(float(x_value), float(y_value))
293
+ painter.setPen(QPen(QColor("#6ec6ff"), 1.5))
294
+ painter.drawPath(path)
295
+ painter.drawText(int(plot_left + plot_width - 48.0), int(plot_top + 12.0), "total")
296
+
297
+ if motion_mask.shape == (frame_count,) and bool(np.any(motion_mask)):
298
+ painter.setPen(QPen(QColor("#ff6b6b"), 1))
299
+ for x_value in x_values[motion_mask]:
300
+ painter.drawLine(
301
+ int(x_value),
302
+ int(plot_top),
303
+ int(x_value),
304
+ int(plot_bottom),
305
+ )
306
+
307
+
308
+ def _draw_offset_line(
309
+ painter: QPainter,
310
+ *,
311
+ x_values: npt.NDArray[np.float64],
312
+ offsets: npt.NDArray[np.float64],
313
+ y_mid: float,
314
+ y_scale: float,
315
+ color: QColor,
316
+ ) -> None:
317
+ """Draw one signed alignment-offset line."""
318
+ y_values = y_mid - offsets * y_scale
319
+ path = QPainterPath()
320
+ path.moveTo(float(x_values[0]), float(y_values[0]))
321
+ for x_value, y_value in zip(x_values[1:], y_values[1:], strict=False):
322
+ path.lineTo(float(x_value), float(y_value))
323
+ painter.setPen(QPen(color, 1.5))
324
+ painter.drawPath(path)
@@ -135,6 +135,7 @@ def response_metadata_tab(
135
135
  roi_output_label: SidebarTextLabel,
136
136
  status_label: SidebarTextLabel,
137
137
  update_notice_label: SidebarTextLabel,
138
+ motion_summary_widget: QWidget | None = None,
138
139
  ) -> QWidget:
139
140
  """Create the tab that shows selected-recording metadata.
140
141
 
@@ -146,6 +147,7 @@ def response_metadata_tab(
146
147
  roi_output_label: Label showing the ROI output path.
147
148
  status_label: Label showing the latest app action or update command.
148
149
  update_notice_label: Label showing the copyable update command.
150
+ motion_summary_widget: Optional section showing alignment movement.
149
151
 
150
152
  Returns:
151
153
  Scrollable Qt widget for the Metadata tab.
@@ -158,6 +160,8 @@ def response_metadata_tab(
158
160
  _metadata_group("Outputs", analysis_output_label, roi_output_label),
159
161
  )
160
162
  layout.addWidget(_metadata_group("Status", status_label, update_notice_label))
163
+ if motion_summary_widget is not None:
164
+ layout.addWidget(motion_summary_widget)
161
165
  layout.addStretch(1)
162
166
  return scrolling_tab(layout)
163
167
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: twopy
3
- Version: 0.3.8
3
+ Version: 0.3.9
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
@@ -158,6 +158,7 @@ src/twopy/napari/plotting/export.py
158
158
  src/twopy/napari/plotting/export_controls.py
159
159
  src/twopy/napari/plotting/form_controls.py
160
160
  src/twopy/napari/plotting/label_visibility.py
161
+ src/twopy/napari/plotting/motion_summary.py
161
162
  src/twopy/napari/plotting/normalization_options.py
162
163
  src/twopy/napari/plotting/options.py
163
164
  src/twopy/napari/plotting/panels.py
@@ -580,6 +580,10 @@ class BackgroundSubtractionTest(unittest.TestCase):
580
580
  high_res_pd=np.zeros(0, dtype=np.float64),
581
581
  mean_image=movie_values.mean(axis=0),
582
582
  alignment_valid_crop=alignment_crop,
583
+ alignment_offset_pixels=np.zeros(
584
+ (movie_values.shape[0], 2),
585
+ dtype=np.float64,
586
+ ),
583
587
  alignment_shift_pixels=np.zeros(movie_values.shape[0], dtype=np.float64),
584
588
  motion_artifact_mask=np.zeros(movie_values.shape[0], dtype=np.bool_),
585
589
  frame_counts=FrameCountAudit(