TwoPy 0.2.0__tar.gz → 0.2.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 (236) hide show
  1. {twopy-0.2.0 → twopy-0.2.1}/PKG-INFO +8 -4
  2. {twopy-0.2.0 → twopy-0.2.1}/README.md +5 -3
  3. {twopy-0.2.0 → twopy-0.2.1}/pyproject.toml +9 -1
  4. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/analysis/background_subtraction.py +120 -21
  5. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/analysis/dff.py +33 -0
  6. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/analysis/kernels/__init__.py +2 -2
  7. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/analysis/persistence.py +1 -1
  8. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/analysis_cache.py +2 -1
  9. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/config.py +47 -8
  10. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/custom/__init__.py +2 -0
  11. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/custom/native_workflows/direction_selectivity.py +13 -12
  12. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/custom/native_workflows/response_kernels.py +2 -7
  13. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/custom/provenance.py +14 -0
  14. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/custom/types.py +247 -8
  15. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/database/search.py +1 -1
  16. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/napari/controls.py +68 -13
  17. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/napari/custom_tab.py +168 -20
  18. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/napari/database_search.py +41 -1
  19. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/napari/display_paths.py +26 -0
  20. twopy-0.2.1/src/twopy/napari/group_matching/__init__.py +29 -0
  21. twopy-0.2.1/src/twopy/napari/group_matching/cards.py +127 -0
  22. twopy-0.2.1/src/twopy/napari/group_matching/fov_assignment.py +679 -0
  23. twopy-0.2.1/src/twopy/napari/group_matching/fov_cards.py +143 -0
  24. twopy-0.2.0/src/twopy/napari/group_matching_images.py → twopy-0.2.1/src/twopy/napari/group_matching/images.py +2 -2
  25. twopy-0.2.1/src/twopy/napari/group_matching/response_preview.py +334 -0
  26. twopy-0.2.1/src/twopy/napari/group_matching/responses.py +130 -0
  27. twopy-0.2.0/src/twopy/napari/group_matching_roi.py → twopy-0.2.1/src/twopy/napari/group_matching/roi_assignment.py +590 -532
  28. twopy-0.2.1/src/twopy/napari/group_matching/roi_cards.py +371 -0
  29. twopy-0.2.1/src/twopy/napari/group_matching/style.py +269 -0
  30. twopy-0.2.1/src/twopy/napari/group_matching/tables.py +27 -0
  31. twopy-0.2.1/src/twopy/napari/group_matching/window.py +267 -0
  32. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/napari/live_analysis.py +75 -32
  33. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/napari/loading.py +37 -1
  34. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/napari/plotting/dff_options.py +1 -0
  35. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/napari/plotting/docks/options_panel.py +15 -16
  36. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/napari/plotting/docks/paths.py +5 -6
  37. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/napari/plotting/docks/plot_area.py +42 -0
  38. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/napari/plotting/docks/response_plot_widget.py +189 -19
  39. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/napari/plotting/docks/visibility_state.py +29 -2
  40. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/napari/plotting/export_controls.py +5 -6
  41. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/napari/plotting/normalization_options.py +1 -0
  42. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/napari/plotting/options.py +1 -1
  43. twopy-0.2.1/src/twopy/napari/plotting/panels.py +188 -0
  44. twopy-0.2.1/src/twopy/napari/plotting/preview_strip.py +218 -0
  45. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/napari/plotting/processing_options.py +1 -0
  46. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/napari/plotting/widgets.py +403 -26
  47. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/napari/responses.py +58 -1
  48. twopy-0.2.1/src/twopy/napari/roi_signatures.py +20 -0
  49. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/napari/session.py +20 -3
  50. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/napari/sidebar.py +27 -14
  51. twopy-0.2.1/src/twopy/napari/state.py +192 -0
  52. {twopy-0.2.0 → twopy-0.2.1}/src/twopy.egg-info/PKG-INFO +8 -4
  53. {twopy-0.2.0 → twopy-0.2.1}/src/twopy.egg-info/SOURCES.txt +17 -3
  54. {twopy-0.2.0 → twopy-0.2.1}/tests/test_config.py +28 -0
  55. {twopy-0.2.0 → twopy-0.2.1}/tests/test_custom_workflows.py +145 -18
  56. {twopy-0.2.0 → twopy-0.2.1}/tests/test_dff.py +33 -3
  57. twopy-0.2.1/tests/test_function_inventory.py +155 -0
  58. {twopy-0.2.0 → twopy-0.2.1}/tests/test_kernels.py +7 -0
  59. {twopy-0.2.0 → twopy-0.2.1}/tests/test_napari.py +175 -6
  60. {twopy-0.2.0 → twopy-0.2.1}/tests/test_napari_database_search.py +60 -2
  61. {twopy-0.2.0 → twopy-0.2.1}/tests/test_napari_export.py +72 -2
  62. twopy-0.2.1/tests/test_napari_group_matching_responses.py +140 -0
  63. twopy-0.2.1/tests/test_napari_group_matching_roi_cards.py +156 -0
  64. {twopy-0.2.0 → twopy-0.2.1}/tests/test_napari_load_tab.py +189 -0
  65. {twopy-0.2.0 → twopy-0.2.1}/tests/test_napari_loaded_recordings.py +396 -18
  66. {twopy-0.2.0 → twopy-0.2.1}/tests/test_napari_path_resolution.py +40 -0
  67. {twopy-0.2.0 → twopy-0.2.1}/tests/test_napari_plot_controls.py +104 -1
  68. twopy-0.2.1/tests/test_napari_plot_widgets.py +1808 -0
  69. {twopy-0.2.0 → twopy-0.2.1}/tests/test_napari_processing_controls.py +100 -3
  70. {twopy-0.2.0 → twopy-0.2.1}/tests/test_napari_roi_visibility.py +18 -0
  71. twopy-0.2.0/src/twopy/napari/group_matching.py +0 -596
  72. twopy-0.2.0/src/twopy/napari/plotting/panels.py +0 -103
  73. twopy-0.2.0/src/twopy/napari/state.py +0 -106
  74. twopy-0.2.0/tests/test_napari_plot_widgets.py +0 -799
  75. {twopy-0.2.0 → twopy-0.2.1}/setup.cfg +0 -0
  76. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/__init__.py +0 -0
  77. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/_segments.py +0 -0
  78. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/_version.py +0 -0
  79. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/analysis/__init__.py +0 -0
  80. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/analysis/dff_options.py +0 -0
  81. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/analysis/epoch_mapping.py +0 -0
  82. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/analysis/group_matching.py +0 -0
  83. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/analysis/kernels/_fitting.py +0 -0
  84. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/analysis/kernels/_stimulus.py +0 -0
  85. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/analysis/motion.py +0 -0
  86. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/analysis/response_context.py +0 -0
  87. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/analysis/response_maps.py +0 -0
  88. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/analysis/response_processing/__init__.py +0 -0
  89. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/analysis/response_processing/apply.py +0 -0
  90. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/analysis/response_processing/options.py +0 -0
  91. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/analysis/response_processing/persistence.py +0 -0
  92. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/analysis/response_processing/qc.py +0 -0
  93. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/analysis/response_processing/signals.py +0 -0
  94. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/analysis/response_window_options.py +0 -0
  95. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/analysis/responses.py +0 -0
  96. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/analysis/timing.py +0 -0
  97. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/analysis/trials.py +0 -0
  98. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/analysis/workflow.py +0 -0
  99. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/api.py +0 -0
  100. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/calibrations/pixel_calibration_profiles.csv +0 -0
  101. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/calibrations/pixel_size.csv +0 -0
  102. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/conversion/__init__.py +0 -0
  103. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/conversion/alignment_crop.py +0 -0
  104. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/conversion/api.py +0 -0
  105. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/conversion/hdf5_writing.py +0 -0
  106. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/conversion/matlab_values.py +0 -0
  107. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/conversion/source_loading.py +0 -0
  108. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/conversion/stimulus_code.py +0 -0
  109. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/conversion/types.py +0 -0
  110. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/converted.py +0 -0
  111. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/custom/discovery.py +0 -0
  112. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/custom/native.py +0 -0
  113. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/custom/native_workflows/__init__.py +0 -0
  114. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/custom/parameters.py +0 -0
  115. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/custom/registry.py +0 -0
  116. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/database/__init__.py +0 -0
  117. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/database/cache.py +0 -0
  118. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/database/catalog.py +0 -0
  119. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/database/connection.py +0 -0
  120. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/database/generic.py +0 -0
  121. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/database/modeled.py +0 -0
  122. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/database/sql.py +0 -0
  123. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/database/types.py +0 -0
  124. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/filenames.py +0 -0
  125. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/frame_ranges.py +0 -0
  126. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/hdf5_utils.py +0 -0
  127. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/inspection.py +0 -0
  128. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/matlab.py +0 -0
  129. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/napari/__init__.py +0 -0
  130. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/napari/__main__.py +0 -0
  131. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/napari/database_favorites.py +0 -0
  132. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/napari/dims.py +0 -0
  133. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/napari/display.py +0 -0
  134. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/napari/errors.py +0 -0
  135. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/napari/interactive.py +0 -0
  136. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/napari/latest_worker.py +0 -0
  137. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/napari/launcher.py +0 -0
  138. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/napari/load_workflow.py +0 -0
  139. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/napari/loaded_recordings_csv.py +0 -0
  140. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/napari/movie.py +0 -0
  141. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/napari/paths.py +0 -0
  142. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/napari/plotting/__init__.py +0 -0
  143. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/napari/plotting/data.py +0 -0
  144. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/napari/plotting/docks/__init__.py +0 -0
  145. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/napari/plotting/docks/api.py +0 -0
  146. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/napari/plotting/docks/dynamic_options.py +0 -0
  147. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/napari/plotting/docks/response_map_area.py +0 -0
  148. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/napari/plotting/docks/save_actions.py +0 -0
  149. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/napari/plotting/export.py +0 -0
  150. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/napari/plotting/form_controls.py +0 -0
  151. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/napari/plotting/label_visibility.py +0 -0
  152. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/napari/plotting/response_map_colors.py +0 -0
  153. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/napari/plotting/response_map_display.py +0 -0
  154. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/napari/plotting/response_map_options.py +0 -0
  155. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/napari/plotting/response_window_options.py +0 -0
  156. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/napari/plotting/roi_generation/__init__.py +0 -0
  157. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/napari/plotting/roi_generation/actions.py +0 -0
  158. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/napari/plotting/roi_generation/controls.py +0 -0
  159. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/napari/plotting/roi_generation/options.py +0 -0
  160. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/napari/protocols.py +0 -0
  161. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/napari/roi.py +0 -0
  162. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/napari/text.py +0 -0
  163. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/napari/trial_timeline.py +0 -0
  164. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/napari/types.py +0 -0
  165. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/napari/viewer.py +0 -0
  166. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/parity/__init__.py +0 -0
  167. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/parity/dff.py +0 -0
  168. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/parity/psycho5.py +0 -0
  169. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/parity/roi.py +0 -0
  170. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/parity/roi_extraction.py +0 -0
  171. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/parity/saved_analysis.py +0 -0
  172. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/photodiode.py +0 -0
  173. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/photodiode_classification.py +0 -0
  174. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/pixel_calibration.py +0 -0
  175. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/pixel_calibration_profiles.py +0 -0
  176. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/response_roi_extraction.py +0 -0
  177. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/roi.py +0 -0
  178. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/roi_extraction.py +0 -0
  179. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/roi_mask_cleanup.py +0 -0
  180. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/session.py +0 -0
  181. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/spatial.py +0 -0
  182. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/stimulus.py +0 -0
  183. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/synchronization.py +0 -0
  184. {twopy-0.2.0 → twopy-0.2.1}/src/twopy/typing_guards.py +0 -0
  185. {twopy-0.2.0 → twopy-0.2.1}/src/twopy.egg-info/dependency_links.txt +0 -0
  186. {twopy-0.2.0 → twopy-0.2.1}/src/twopy.egg-info/entry_points.txt +0 -0
  187. {twopy-0.2.0 → twopy-0.2.1}/src/twopy.egg-info/requires.txt +0 -0
  188. {twopy-0.2.0 → twopy-0.2.1}/src/twopy.egg-info/top_level.txt +0 -0
  189. {twopy-0.2.0 → twopy-0.2.1}/tests/test_api.py +0 -0
  190. {twopy-0.2.0 → twopy-0.2.1}/tests/test_background_subtraction.py +0 -0
  191. {twopy-0.2.0 → twopy-0.2.1}/tests/test_conversion.py +0 -0
  192. {twopy-0.2.0 → twopy-0.2.1}/tests/test_converted.py +0 -0
  193. {twopy-0.2.0 → twopy-0.2.1}/tests/test_database.py +0 -0
  194. {twopy-0.2.0 → twopy-0.2.1}/tests/test_epoch_mapping.py +0 -0
  195. {twopy-0.2.0 → twopy-0.2.1}/tests/test_filenames.py +0 -0
  196. {twopy-0.2.0 → twopy-0.2.1}/tests/test_frame_ranges.py +0 -0
  197. {twopy-0.2.0 → twopy-0.2.1}/tests/test_group_matching.py +0 -0
  198. {twopy-0.2.0 → twopy-0.2.1}/tests/test_hdf5_utils.py +0 -0
  199. {twopy-0.2.0 → twopy-0.2.1}/tests/test_import.py +0 -0
  200. {twopy-0.2.0 → twopy-0.2.1}/tests/test_inspection.py +0 -0
  201. {twopy-0.2.0 → twopy-0.2.1}/tests/test_load_workflow.py +0 -0
  202. {twopy-0.2.0 → twopy-0.2.1}/tests/test_matlab.py +0 -0
  203. {twopy-0.2.0 → twopy-0.2.1}/tests/test_motion.py +0 -0
  204. {twopy-0.2.0 → twopy-0.2.1}/tests/test_napari_display.py +0 -0
  205. {twopy-0.2.0 → twopy-0.2.1}/tests/test_napari_live_controller.py +0 -0
  206. {twopy-0.2.0 → twopy-0.2.1}/tests/test_napari_plot_data.py +0 -0
  207. {twopy-0.2.0 → twopy-0.2.1}/tests/test_napari_response_map_display.py +0 -0
  208. {twopy-0.2.0 → twopy-0.2.1}/tests/test_napari_response_workflow.py +0 -0
  209. {twopy-0.2.0 → twopy-0.2.1}/tests/test_napari_roi_generation.py +0 -0
  210. {twopy-0.2.0 → twopy-0.2.1}/tests/test_napari_text.py +0 -0
  211. {twopy-0.2.0 → twopy-0.2.1}/tests/test_napari_timeline.py +0 -0
  212. {twopy-0.2.0 → twopy-0.2.1}/tests/test_packaging.py +0 -0
  213. {twopy-0.2.0 → twopy-0.2.1}/tests/test_parity.py +0 -0
  214. {twopy-0.2.0 → twopy-0.2.1}/tests/test_parity_psycho5.py +0 -0
  215. {twopy-0.2.0 → twopy-0.2.1}/tests/test_parity_roi_extraction.py +0 -0
  216. {twopy-0.2.0 → twopy-0.2.1}/tests/test_persistence.py +0 -0
  217. {twopy-0.2.0 → twopy-0.2.1}/tests/test_photodiode_classification.py +0 -0
  218. {twopy-0.2.0 → twopy-0.2.1}/tests/test_pixel_calibration.py +0 -0
  219. {twopy-0.2.0 → twopy-0.2.1}/tests/test_pixel_calibration_profiles.py +0 -0
  220. {twopy-0.2.0 → twopy-0.2.1}/tests/test_real_fixture.py +0 -0
  221. {twopy-0.2.0 → twopy-0.2.1}/tests/test_recording_timing.py +0 -0
  222. {twopy-0.2.0 → twopy-0.2.1}/tests/test_response_map_area.py +0 -0
  223. {twopy-0.2.0 → twopy-0.2.1}/tests/test_response_maps.py +0 -0
  224. {twopy-0.2.0 → twopy-0.2.1}/tests/test_response_processing.py +0 -0
  225. {twopy-0.2.0 → twopy-0.2.1}/tests/test_response_roi_extraction.py +0 -0
  226. {twopy-0.2.0 → twopy-0.2.1}/tests/test_response_window_options.py +0 -0
  227. {twopy-0.2.0 → twopy-0.2.1}/tests/test_responses.py +0 -0
  228. {twopy-0.2.0 → twopy-0.2.1}/tests/test_roi.py +0 -0
  229. {twopy-0.2.0 → twopy-0.2.1}/tests/test_roi_extraction.py +0 -0
  230. {twopy-0.2.0 → twopy-0.2.1}/tests/test_roi_mask_cleanup.py +0 -0
  231. {twopy-0.2.0 → twopy-0.2.1}/tests/test_saved_analysis.py +0 -0
  232. {twopy-0.2.0 → twopy-0.2.1}/tests/test_session.py +0 -0
  233. {twopy-0.2.0 → twopy-0.2.1}/tests/test_stimulus.py +0 -0
  234. {twopy-0.2.0 → twopy-0.2.1}/tests/test_synchronization.py +0 -0
  235. {twopy-0.2.0 → twopy-0.2.1}/tests/test_trials.py +0 -0
  236. {twopy-0.2.0 → twopy-0.2.1}/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.2.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
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.2.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"
@@ -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
 
@@ -869,7 +869,7 @@ def _trial_response_row(
869
869
  "stop_frame": trial.stop_frame,
870
870
  "frame_count": trial.stop_frame - trial.start_frame,
871
871
  }
872
- row.update({column: "" for column in time_columns})
872
+ row.update(dict.fromkeys(time_columns, ""))
873
873
  for time_seconds, value in zip(
874
874
  trial.time_seconds,
875
875
  trial.values[:, roi_index],
@@ -10,6 +10,7 @@ files are removed from the local cache after they copy successfully because the
10
10
  publish destination is their durable location.
11
11
  """
12
12
 
13
+ import os
13
14
  import shutil
14
15
  import tempfile
15
16
  from concurrent.futures import Executor, Future
@@ -266,7 +267,7 @@ def copy_file_atomically(source: Path, target: Path) -> None:
266
267
  temp_path = Path(temp_name)
267
268
  try:
268
269
  with (
269
- open(temp_descriptor, "wb") as temp_file,
270
+ os.fdopen(temp_descriptor, "wb") as temp_file,
270
271
  source.open(
271
272
  "rb",
272
273
  ) as source_file,
@@ -21,6 +21,7 @@ __all__ = [
21
21
  "DatabaseAccess",
22
22
  "TwopyConfig",
23
23
  "data_path_match",
24
+ "data_path_recording_candidates",
24
25
  "load_config",
25
26
  "resolve_data_recording_path",
26
27
  "resolve_analysis_cache_dir",
@@ -84,7 +85,7 @@ def load_config(path: Path = DEFAULT_CONFIG_PATH) -> TwopyConfig:
84
85
  msg = f"twopy config must be a YAML mapping: {config_path}"
85
86
  raise ValueError(msg)
86
87
 
87
- raw_config: dict[object, object] = {key: value for key, value in loaded.items()}
88
+ raw_config: dict[object, object] = dict(loaded.items())
88
89
 
89
90
  return TwopyConfig(
90
91
  database_path=_required_path(raw_config, "database_path", config_path),
@@ -136,13 +137,7 @@ def resolve_data_recording_path(
136
137
  function is the ordered availability check that picks the preferred mounted
137
138
  copy without hiding missing-data errors behind a second abstraction.
138
139
  """
139
- candidates = tuple(
140
- data_root.expanduser().joinpath(*relative_parts)
141
- for data_root in config.data_paths
142
- )
143
- if not candidates:
144
- msg = "twopy config must include at least one data path"
145
- raise ValueError(msg)
140
+ candidates = _recording_candidates_for_relative_parts(config, relative_parts)
146
141
 
147
142
  for candidate in candidates:
148
143
  if candidate.exists():
@@ -177,6 +172,35 @@ def data_path_match(
177
172
  return None
178
173
 
179
174
 
175
+ def data_path_recording_candidates(
176
+ config: TwopyConfig,
177
+ recording_dir: Path,
178
+ ) -> tuple[Path, ...]:
179
+ """Return every configured mirror candidate for one recording path.
180
+
181
+ Args:
182
+ config: Loaded twopy configuration with ordered data roots.
183
+ recording_dir: Recording folder under one configured data root.
184
+
185
+ Returns:
186
+ Candidate recording folders under every configured root with the same
187
+ root-relative path, or an empty tuple when ``recording_dir`` is external.
188
+
189
+ Database loads may fall back to the first configured root when no source
190
+ copy is mounted so cache-only loads can still work. This helper exposes the
191
+ full mirrored candidate set for diagnostics and audits without changing
192
+ that load behavior.
193
+ """
194
+ match = data_path_match(config, recording_dir)
195
+ if match is None:
196
+ return ()
197
+ _matched_root, relative_recording = match
198
+ return _recording_candidates_for_relative_parts(
199
+ config,
200
+ tuple(str(part) for part in relative_recording.parts),
201
+ )
202
+
203
+
180
204
  def resolve_analysis_output_dir(config: TwopyConfig, recording_dir: Path) -> Path:
181
205
  """Resolve the output directory for one recording.
182
206
 
@@ -314,6 +338,21 @@ def _required_path_tuple(
314
338
  return tuple(paths)
315
339
 
316
340
 
341
+ def _recording_candidates_for_relative_parts(
342
+ config: TwopyConfig,
343
+ relative_parts: tuple[str, ...],
344
+ ) -> tuple[Path, ...]:
345
+ """Return ordered data-root candidates for one relative recording path."""
346
+ candidates = tuple(
347
+ data_root.expanduser().joinpath(*relative_parts)
348
+ for data_root in config.data_paths
349
+ )
350
+ if not candidates:
351
+ msg = "twopy config must include at least one data path"
352
+ raise ValueError(msg)
353
+ return candidates
354
+
355
+
317
356
  def _external_analysis_cache_dir(
318
357
  config: TwopyConfig,
319
358
  source_dir: Path,
@@ -31,6 +31,7 @@ from twopy.custom.types import (
31
31
  CustomEpoch,
32
32
  CustomLineBand,
33
33
  CustomLinePlot,
34
+ CustomRecordingMetadata,
34
35
  CustomResult,
35
36
  CustomRunContext,
36
37
  CustomTable,
@@ -43,6 +44,7 @@ __all__ = [
43
44
  "CustomEpoch",
44
45
  "CustomLineBand",
45
46
  "CustomLinePlot",
47
+ "CustomRecordingMetadata",
46
48
  "CustomParameterSpec",
47
49
  "CustomResult",
48
50
  "CustomRunContext",
@@ -25,7 +25,7 @@ class DirectionSelectivityParams:
25
25
  window_stop_seconds: Epoch-relative metric stop time.
26
26
  rectify_responses: Whether negative preferred/null responses are set
27
27
  to zero before computing DSI.
28
- dsi_threshold: Minimum absolute DSI to plot and highlight.
28
+ dsi_threshold: Minimum absolute DSI to show and highlight.
29
29
  output_name: CSV filename under the workflow output folder.
30
30
 
31
31
  Returns:
@@ -88,9 +88,9 @@ class DirectionSelectivityParams:
88
88
  dsi_threshold: float = field(
89
89
  default=0.0,
90
90
  metadata={
91
- "label": "DSI plot threshold",
91
+ "label": "DSI show threshold",
92
92
  "description": (
93
- "Only ROIs with absolute DSI at or above this value are plotted."
93
+ "Only ROIs with absolute DSI at or above this value are shown."
94
94
  ),
95
95
  "max": 1.0,
96
96
  "twopy_role": "table_highlight_threshold",
@@ -125,8 +125,9 @@ def run(
125
125
  params: Custom tab values.
126
126
 
127
127
  Returns:
128
- Table and response plot data for the Custom tab.
128
+ Table output plus ROI visibility rows for the current response plot.
129
129
  """
130
+ selected_roi_indices = ctx.roi_indices_for_selector(params.roi_selector)
130
131
  rois = ctx.rois_for_selector(params.roi_selector)
131
132
  computation = ctx.compute_standard_responses(rois)
132
133
  window_seconds = ctx.epoch_window(
@@ -158,27 +159,27 @@ def run(
158
159
  },
159
160
  roi_labels=rois.labels,
160
161
  )
161
- passing_roi_indices = tuple(
162
+ passing_table_rows = tuple(
162
163
  int(index) for index in np.flatnonzero(np.abs(dsi) >= params.dsi_threshold)
163
164
  )
164
- response_plot_data = ctx.response_plot_data(
165
- computation.grouped_responses,
166
- source_path=csv_path,
167
- roi_indices=passing_roi_indices,
165
+ visible_roi_indices = tuple(
166
+ selected_roi_indices[index]
167
+ for index in passing_table_rows
168
+ if index < len(selected_roi_indices)
168
169
  )
169
170
  return CustomResult(
170
171
  message=(
171
172
  f"Computed DSI for {len(rois.labels)} ROIs; "
172
- f"plotting {len(passing_roi_indices)} above absolute threshold."
173
+ f"showing {len(visible_roi_indices)} above absolute threshold."
173
174
  ),
174
175
  tables=(
175
176
  CustomTable(
176
177
  "Direction selectivity",
177
178
  csv_path,
178
- highlighted_rows=passing_roi_indices,
179
+ highlighted_rows=passing_table_rows,
179
180
  ),
180
181
  ),
181
- response_plot_data=response_plot_data,
182
+ visible_roi_indices=visible_roi_indices,
182
183
  )
183
184
 
184
185
 
@@ -95,7 +95,7 @@ class ResponseKernelParams:
95
95
  },
96
96
  )
97
97
  num_stim_past: int = field(
98
- default=150,
98
+ default=100,
99
99
  metadata={
100
100
  "label": "Past samples",
101
101
  "description": "Stimulus samples before each response sample.",
@@ -103,7 +103,7 @@ class ResponseKernelParams:
103
103
  },
104
104
  )
105
105
  num_stim_future: int = field(
106
- default=25,
106
+ default=10,
107
107
  metadata={
108
108
  "label": "Future samples",
109
109
  "description": "Future stimulus samples used as negative-lag QC.",
@@ -244,7 +244,6 @@ def _olfactory_kernel_result(
244
244
  ipsi_mean, ipsi_band = _mean_sem_plot_series(
245
245
  kernels.ipsilateral[epoch_index],
246
246
  series_index=len(mean_rows),
247
- label=f"{epoch_name} ipsi",
248
247
  )
249
248
  mean_rows.append(ipsi_mean)
250
249
  mean_labels.append(f"{epoch_name} ipsi")
@@ -252,7 +251,6 @@ def _olfactory_kernel_result(
252
251
  contra_mean, contra_band = _mean_sem_plot_series(
253
252
  kernels.contralateral[epoch_index],
254
253
  series_index=len(mean_rows),
255
- label=f"{epoch_name} contra",
256
254
  )
257
255
  mean_rows.append(contra_mean)
258
256
  mean_labels.append(f"{epoch_name} contra")
@@ -324,7 +322,6 @@ def _visual_kernel_result(
324
322
  contrast_mean, contrast_band = _mean_sem_plot_series(
325
323
  contrast,
326
324
  series_index=len(mean_rows),
327
- label=epoch_name,
328
325
  )
329
326
  mean_rows.append(contrast_mean)
330
327
  mean_labels.append(epoch_name)
@@ -355,7 +352,6 @@ def _mean_sem_plot_series(
355
352
  values: np.ndarray,
356
353
  *,
357
354
  series_index: int,
358
- label: str,
359
355
  ) -> tuple[np.ndarray, CustomLineBand]:
360
356
  """Return one mean row and filled SEM band for one kernel matrix."""
361
357
  mean, sem = finite_mean_and_sem(values, axis=0)
@@ -363,7 +359,6 @@ def _mean_sem_plot_series(
363
359
  series_index=series_index,
364
360
  lower=mean - sem,
365
361
  upper=mean + sem,
366
- label=f"{label} SEM",
367
362
  )
368
363
 
369
364
 
@@ -87,6 +87,7 @@ def validate_custom_result(
87
87
  raise ValueError(msg)
88
88
  if result.response_plot_data is not None:
89
89
  _validate_response_plot_data(result.response_plot_data)
90
+ _validate_custom_result_visible_rois(result.visible_roi_indices)
90
91
  return result
91
92
 
92
93
 
@@ -101,6 +102,19 @@ def _validate_table_highlighted_rows(highlighted_rows: object) -> None:
101
102
  raise ValueError(msg)
102
103
 
103
104
 
105
+ def _validate_custom_result_visible_rois(visible_roi_indices: object) -> None:
106
+ """Check optional ROI rows selected by a workflow result."""
107
+ if visible_roi_indices is None:
108
+ return
109
+ if not isinstance(visible_roi_indices, tuple):
110
+ msg = "CustomResult.visible_roi_indices must be a tuple of integers."
111
+ raise ValueError(msg)
112
+ for index in visible_roi_indices:
113
+ if type(index) is not int or index < 0:
114
+ msg = "CustomResult.visible_roi_indices must contain non-negative integers."
115
+ raise ValueError(msg)
116
+
117
+
104
118
  def custom_result_artifact_paths(result: CustomResult) -> tuple[Path, ...]:
105
119
  """Return every output file path declared by a custom result.
106
120