TwoPy 0.3.0__tar.gz → 0.3.2__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 (257) hide show
  1. {twopy-0.3.0 → twopy-0.3.2}/PKG-INFO +17 -3
  2. {twopy-0.3.0 → twopy-0.3.2}/README.md +16 -2
  3. {twopy-0.3.0 → twopy-0.3.2}/pyproject.toml +2 -2
  4. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/__init__.py +0 -16
  5. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/analysis/__init__.py +12 -0
  6. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/analysis/persistence.py +147 -69
  7. twopy-0.3.2/src/twopy/analysis/response_plotting.py +342 -0
  8. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/analysis/response_processing/__init__.py +2 -2
  9. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/analysis/response_processing/apply.py +17 -15
  10. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/analysis/response_processing/options.py +12 -11
  11. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/analysis/response_processing/persistence.py +48 -20
  12. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/analysis/workflow.py +2 -2
  13. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/analysis_cache.py +121 -43
  14. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/api.py +4 -3
  15. twopy-0.3.2/src/twopy/cache_inventory.py +708 -0
  16. twopy-0.3.2/src/twopy/config.example.yml +64 -0
  17. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/config.py +239 -9
  18. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/conversion/api.py +127 -22
  19. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/custom/__init__.py +10 -11
  20. twopy-0.3.0/src/twopy/custom/types.py → twopy-0.3.2/src/twopy/custom/context.py +12 -484
  21. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/custom/discovery.py +3 -6
  22. twopy-0.3.2/src/twopy/custom/metadata.py +228 -0
  23. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/custom/native_workflows/direction_selectivity.py +2 -2
  24. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/custom/parameters.py +2 -2
  25. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/custom/provenance.py +84 -7
  26. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/custom/registry.py +2 -1
  27. twopy-0.3.2/src/twopy/custom/results.py +116 -0
  28. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/custom/runner.py +9 -6
  29. twopy-0.3.2/src/twopy/custom/workflows.py +98 -0
  30. twopy-0.3.2/src/twopy/napari/__init__.py +75 -0
  31. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/napari/controls.py +38 -19
  32. twopy-0.3.2/src/twopy/napari/database_favorite_editor.py +106 -0
  33. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/napari/database_favorites.py +56 -5
  34. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/napari/database_search.py +225 -3
  35. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/napari/dims.py +5 -9
  36. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/napari/display_paths.py +125 -13
  37. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/napari/group_matching/fov_assignment.py +1 -8
  38. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/napari/group_matching/images.py +4 -8
  39. twopy-0.3.2/src/twopy/napari/group_matching/protocols.py +27 -0
  40. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/napari/group_matching/response_preview.py +1 -1
  41. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/napari/group_matching/responses.py +1 -1
  42. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/napari/group_matching/roi_assignment.py +2 -8
  43. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/napari/group_matching/roi_cards.py +4 -9
  44. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/napari/group_matching/window.py +9 -16
  45. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/napari/interactive.py +40 -12
  46. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/napari/launcher.py +196 -8
  47. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/napari/live_analysis.py +1 -1
  48. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/napari/load_controller.py +111 -38
  49. twopy-0.3.2/src/twopy/napari/load_progress.py +758 -0
  50. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/napari/load_workflow.py +33 -6
  51. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/napari/loaded_recordings_csv.py +35 -53
  52. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/napari/loading.py +410 -46
  53. twopy-0.3.2/src/twopy/napari/output_routing.py +121 -0
  54. twopy-0.3.2/src/twopy/napari/plotting/__init__.py +52 -0
  55. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/napari/plotting/data.py +17 -289
  56. twopy-0.3.2/src/twopy/napari/plotting/docks/__init__.py +52 -0
  57. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/napari/plotting/docks/api.py +31 -7
  58. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/napari/plotting/docks/dynamic_options.py +22 -8
  59. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/napari/plotting/docks/paths.py +18 -4
  60. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/napari/plotting/docks/plot_area.py +1 -1
  61. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/napari/plotting/docks/response_plot_widget.py +415 -133
  62. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/napari/plotting/docks/save_actions.py +39 -10
  63. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/napari/plotting/docks/visibility_state.py +1 -1
  64. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/napari/plotting/export.py +4 -9
  65. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/napari/plotting/export_controls.py +64 -7
  66. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/napari/plotting/normalization_options.py +19 -11
  67. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/napari/plotting/options.py +49 -4
  68. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/napari/plotting/preview_strip.py +1 -1
  69. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/napari/plotting/roi_generation/__init__.py +8 -0
  70. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/napari/plotting/roi_generation/actions.py +35 -2
  71. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/napari/plotting/roi_generation/controls.py +137 -16
  72. twopy-0.3.2/src/twopy/napari/plotting/roi_generation/options.py +348 -0
  73. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/napari/plotting/widgets.py +1 -1
  74. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/napari/protocols.py +44 -0
  75. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/napari/responses.py +4 -2
  76. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/napari/roi.py +59 -3
  77. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/napari/session.py +75 -1
  78. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/napari/trial_timeline.py +11 -24
  79. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/napari/viewer.py +9 -1
  80. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/roi.py +98 -6
  81. {twopy-0.3.0 → twopy-0.3.2}/src/twopy.egg-info/PKG-INFO +17 -3
  82. {twopy-0.3.0 → twopy-0.3.2}/src/twopy.egg-info/SOURCES.txt +12 -1
  83. twopy-0.3.2/tests/test_cache_inventory.py +399 -0
  84. {twopy-0.3.0 → twopy-0.3.2}/tests/test_config.py +147 -5
  85. {twopy-0.3.0 → twopy-0.3.2}/tests/test_conversion.py +76 -4
  86. {twopy-0.3.0 → twopy-0.3.2}/tests/test_custom_workflows.py +23 -2
  87. twopy-0.3.2/tests/test_import.py +121 -0
  88. {twopy-0.3.0 → twopy-0.3.2}/tests/test_load_workflow.py +31 -6
  89. {twopy-0.3.0 → twopy-0.3.2}/tests/test_napari_database_search.py +329 -3
  90. {twopy-0.3.0 → twopy-0.3.2}/tests/test_napari_export.py +6 -12
  91. {twopy-0.3.0 → twopy-0.3.2}/tests/test_napari_live_controller.py +2 -1
  92. twopy-0.3.2/tests/test_napari_load_controller.py +402 -0
  93. twopy-0.3.2/tests/test_napari_load_progress.py +379 -0
  94. {twopy-0.3.0 → twopy-0.3.2}/tests/test_napari_load_tab.py +192 -5
  95. {twopy-0.3.0 → twopy-0.3.2}/tests/test_napari_loaded_recordings.py +31 -4
  96. {twopy-0.3.0 → twopy-0.3.2}/tests/test_napari_path_resolution.py +493 -17
  97. {twopy-0.3.0 → twopy-0.3.2}/tests/test_napari_plot_controls.py +3 -3
  98. {twopy-0.3.0 → twopy-0.3.2}/tests/test_napari_plot_data.py +21 -4
  99. {twopy-0.3.0 → twopy-0.3.2}/tests/test_napari_plot_widgets.py +13 -4
  100. {twopy-0.3.0 → twopy-0.3.2}/tests/test_napari_processing_controls.py +6 -2
  101. {twopy-0.3.0 → twopy-0.3.2}/tests/test_napari_response_workflow.py +345 -9
  102. {twopy-0.3.0 → twopy-0.3.2}/tests/test_napari_roi_generation.py +457 -2
  103. {twopy-0.3.0 → twopy-0.3.2}/tests/test_napari_roi_visibility.py +26 -3
  104. twopy-0.3.2/tests/test_packaging.py +232 -0
  105. {twopy-0.3.0 → twopy-0.3.2}/tests/test_persistence.py +147 -2
  106. {twopy-0.3.0 → twopy-0.3.2}/tests/test_response_processing.py +41 -17
  107. {twopy-0.3.0 → twopy-0.3.2}/tests/test_roi.py +77 -0
  108. twopy-0.3.0/src/twopy/napari/__init__.py +0 -27
  109. twopy-0.3.0/src/twopy/napari/load_progress.py +0 -168
  110. twopy-0.3.0/src/twopy/napari/plotting/__init__.py +0 -21
  111. twopy-0.3.0/src/twopy/napari/plotting/docks/__init__.py +0 -23
  112. twopy-0.3.0/src/twopy/napari/plotting/roi_generation/options.py +0 -62
  113. twopy-0.3.0/tests/test_import.py +0 -26
  114. twopy-0.3.0/tests/test_napari_load_progress.py +0 -48
  115. twopy-0.3.0/tests/test_packaging.py +0 -55
  116. {twopy-0.3.0 → twopy-0.3.2}/setup.cfg +0 -0
  117. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/_segments.py +0 -0
  118. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/_version.py +0 -0
  119. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/analysis/background_subtraction.py +0 -0
  120. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/analysis/dff.py +0 -0
  121. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/analysis/dff_options.py +0 -0
  122. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/analysis/epoch_mapping.py +0 -0
  123. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/analysis/group_matching.py +0 -0
  124. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/analysis/kernels/__init__.py +0 -0
  125. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/analysis/kernels/_fitting.py +0 -0
  126. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/analysis/kernels/_stimulus.py +0 -0
  127. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/analysis/motion.py +0 -0
  128. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/analysis/response_context.py +0 -0
  129. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/analysis/response_maps.py +0 -0
  130. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/analysis/response_processing/qc.py +0 -0
  131. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/analysis/response_processing/signals.py +0 -0
  132. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/analysis/response_window_options.py +0 -0
  133. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/analysis/responses.py +0 -0
  134. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/analysis/timing.py +0 -0
  135. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/analysis/trials.py +0 -0
  136. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/calibrations/pixel_calibration_profiles.csv +0 -0
  137. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/calibrations/pixel_size.csv +0 -0
  138. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/conversion/__init__.py +0 -0
  139. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/conversion/alignment_crop.py +0 -0
  140. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/conversion/hdf5_writing.py +0 -0
  141. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/conversion/matlab_values.py +0 -0
  142. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/conversion/orientation.py +0 -0
  143. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/conversion/source_loading.py +0 -0
  144. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/conversion/stimulus_code.py +0 -0
  145. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/conversion/types.py +0 -0
  146. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/converted.py +0 -0
  147. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/custom/native.py +0 -0
  148. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/custom/native_workflows/__init__.py +0 -0
  149. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/custom/native_workflows/response_kernels.py +0 -0
  150. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/database/__init__.py +0 -0
  151. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/database/cache.py +0 -0
  152. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/database/catalog.py +0 -0
  153. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/database/connection.py +0 -0
  154. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/database/generic.py +0 -0
  155. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/database/modeled.py +0 -0
  156. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/database/search.py +0 -0
  157. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/database/sql.py +0 -0
  158. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/database/types.py +0 -0
  159. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/filenames.py +0 -0
  160. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/frame_ranges.py +0 -0
  161. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/hdf5_utils.py +0 -0
  162. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/inspection.py +0 -0
  163. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/matlab.py +0 -0
  164. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/napari/__main__.py +0 -0
  165. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/napari/custom_tab.py +0 -0
  166. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/napari/display.py +0 -0
  167. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/napari/errors.py +0 -0
  168. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/napari/group_matching/__init__.py +0 -0
  169. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/napari/group_matching/cards.py +0 -0
  170. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/napari/group_matching/fov_cards.py +0 -0
  171. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/napari/group_matching/style.py +0 -0
  172. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/napari/group_matching/tables.py +0 -0
  173. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/napari/latest_worker.py +0 -0
  174. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/napari/movie.py +0 -0
  175. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/napari/paths.py +0 -0
  176. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/napari/plotting/dff_options.py +0 -0
  177. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/napari/plotting/docks/options_panel.py +0 -0
  178. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/napari/plotting/docks/response_map_area.py +0 -0
  179. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/napari/plotting/form_controls.py +0 -0
  180. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/napari/plotting/label_visibility.py +0 -0
  181. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/napari/plotting/panels.py +0 -0
  182. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/napari/plotting/processing_options.py +0 -0
  183. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/napari/plotting/response_map_colors.py +0 -0
  184. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/napari/plotting/response_map_display.py +0 -0
  185. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/napari/plotting/response_map_options.py +0 -0
  186. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/napari/plotting/response_window_options.py +0 -0
  187. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/napari/roi_signatures.py +0 -0
  188. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/napari/sidebar.py +0 -0
  189. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/napari/state.py +0 -0
  190. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/napari/text.py +0 -0
  191. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/napari/types.py +0 -0
  192. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/parity/__init__.py +0 -0
  193. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/parity/dff.py +0 -0
  194. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/parity/psycho5.py +0 -0
  195. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/parity/roi.py +0 -0
  196. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/parity/roi_extraction.py +0 -0
  197. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/parity/saved_analysis.py +0 -0
  198. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/photodiode.py +0 -0
  199. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/photodiode_classification.py +0 -0
  200. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/pixel_calibration.py +0 -0
  201. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/pixel_calibration_profiles.py +0 -0
  202. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/response_roi_extraction.py +0 -0
  203. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/roi_extraction.py +0 -0
  204. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/roi_mask_cleanup.py +0 -0
  205. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/session.py +0 -0
  206. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/spatial.py +0 -0
  207. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/spatial_orientation.py +0 -0
  208. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/stimulus.py +0 -0
  209. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/synchronization.py +0 -0
  210. {twopy-0.3.0 → twopy-0.3.2}/src/twopy/typing_guards.py +0 -0
  211. {twopy-0.3.0 → twopy-0.3.2}/src/twopy.egg-info/dependency_links.txt +0 -0
  212. {twopy-0.3.0 → twopy-0.3.2}/src/twopy.egg-info/entry_points.txt +0 -0
  213. {twopy-0.3.0 → twopy-0.3.2}/src/twopy.egg-info/requires.txt +0 -0
  214. {twopy-0.3.0 → twopy-0.3.2}/src/twopy.egg-info/top_level.txt +0 -0
  215. {twopy-0.3.0 → twopy-0.3.2}/tests/test_api.py +0 -0
  216. {twopy-0.3.0 → twopy-0.3.2}/tests/test_background_subtraction.py +0 -0
  217. {twopy-0.3.0 → twopy-0.3.2}/tests/test_converted.py +0 -0
  218. {twopy-0.3.0 → twopy-0.3.2}/tests/test_database.py +0 -0
  219. {twopy-0.3.0 → twopy-0.3.2}/tests/test_dff.py +0 -0
  220. {twopy-0.3.0 → twopy-0.3.2}/tests/test_epoch_mapping.py +0 -0
  221. {twopy-0.3.0 → twopy-0.3.2}/tests/test_filenames.py +0 -0
  222. {twopy-0.3.0 → twopy-0.3.2}/tests/test_frame_ranges.py +0 -0
  223. {twopy-0.3.0 → twopy-0.3.2}/tests/test_function_inventory.py +0 -0
  224. {twopy-0.3.0 → twopy-0.3.2}/tests/test_group_matching.py +0 -0
  225. {twopy-0.3.0 → twopy-0.3.2}/tests/test_hdf5_utils.py +0 -0
  226. {twopy-0.3.0 → twopy-0.3.2}/tests/test_inspection.py +0 -0
  227. {twopy-0.3.0 → twopy-0.3.2}/tests/test_kernels.py +0 -0
  228. {twopy-0.3.0 → twopy-0.3.2}/tests/test_matlab.py +0 -0
  229. {twopy-0.3.0 → twopy-0.3.2}/tests/test_motion.py +0 -0
  230. {twopy-0.3.0 → twopy-0.3.2}/tests/test_napari.py +0 -0
  231. {twopy-0.3.0 → twopy-0.3.2}/tests/test_napari_display.py +0 -0
  232. {twopy-0.3.0 → twopy-0.3.2}/tests/test_napari_group_matching_responses.py +0 -0
  233. {twopy-0.3.0 → twopy-0.3.2}/tests/test_napari_group_matching_roi_cards.py +0 -0
  234. {twopy-0.3.0 → twopy-0.3.2}/tests/test_napari_response_map_display.py +0 -0
  235. {twopy-0.3.0 → twopy-0.3.2}/tests/test_napari_text.py +0 -0
  236. {twopy-0.3.0 → twopy-0.3.2}/tests/test_napari_timeline.py +0 -0
  237. {twopy-0.3.0 → twopy-0.3.2}/tests/test_parity.py +0 -0
  238. {twopy-0.3.0 → twopy-0.3.2}/tests/test_parity_psycho5.py +0 -0
  239. {twopy-0.3.0 → twopy-0.3.2}/tests/test_parity_roi_extraction.py +0 -0
  240. {twopy-0.3.0 → twopy-0.3.2}/tests/test_photodiode_classification.py +0 -0
  241. {twopy-0.3.0 → twopy-0.3.2}/tests/test_pixel_calibration.py +0 -0
  242. {twopy-0.3.0 → twopy-0.3.2}/tests/test_pixel_calibration_profiles.py +0 -0
  243. {twopy-0.3.0 → twopy-0.3.2}/tests/test_real_fixture.py +0 -0
  244. {twopy-0.3.0 → twopy-0.3.2}/tests/test_recording_timing.py +0 -0
  245. {twopy-0.3.0 → twopy-0.3.2}/tests/test_response_map_area.py +0 -0
  246. {twopy-0.3.0 → twopy-0.3.2}/tests/test_response_maps.py +0 -0
  247. {twopy-0.3.0 → twopy-0.3.2}/tests/test_response_roi_extraction.py +0 -0
  248. {twopy-0.3.0 → twopy-0.3.2}/tests/test_response_window_options.py +0 -0
  249. {twopy-0.3.0 → twopy-0.3.2}/tests/test_responses.py +0 -0
  250. {twopy-0.3.0 → twopy-0.3.2}/tests/test_roi_extraction.py +0 -0
  251. {twopy-0.3.0 → twopy-0.3.2}/tests/test_roi_mask_cleanup.py +0 -0
  252. {twopy-0.3.0 → twopy-0.3.2}/tests/test_saved_analysis.py +0 -0
  253. {twopy-0.3.0 → twopy-0.3.2}/tests/test_session.py +0 -0
  254. {twopy-0.3.0 → twopy-0.3.2}/tests/test_stimulus.py +0 -0
  255. {twopy-0.3.0 → twopy-0.3.2}/tests/test_synchronization.py +0 -0
  256. {twopy-0.3.0 → twopy-0.3.2}/tests/test_trials.py +0 -0
  257. {twopy-0.3.0 → twopy-0.3.2}/tests/test_workflow.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: twopy
3
- Version: 0.3.0
3
+ Version: 0.3.2
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
@@ -30,7 +30,7 @@ Two-photon imaging analysis tool for the Clark Lab output format.
30
30
 
31
31
  twopy lets you open two-photon recordings, draw ROIs, plot responses in real time, process and analyze them, and save them.
32
32
 
33
- When you first load a recording, twopy converts it to a standardized HDF5 format. The converted format includes the aligned movie, mean image, stimulus tables, photodiode signals, and recording metadata. Analysis and the GUI both work from the converted files, so the original source files remain separate from twopy's outputs. By default, converted recordings and interactive analysis outputs are cached locally under `~/.cache/twopy/recordings` for speed; saving ROIs and analysis publishes the changed output files back to the configured `analysis_output` location in the background.
33
+ When you first load a recording, twopy converts it to HDF5 files that it owns. Those files include the aligned movie, mean image, stimulus tables, photodiode signals, and recording metadata. Analysis and the GUI both work from the converted files, so the original source files remain separate from twopy's outputs. By default, converted recordings and interactive analysis outputs are cached locally under `~/.cache/twopy/recordings` for speed; saving ROIs and analysis copies the changed output files back to the configured `analysis_output` location in the background.
34
34
 
35
35
  ## Install
36
36
 
@@ -48,7 +48,19 @@ Verify the install:
48
48
  micromamba run -n twopy python -c 'import twopy; print(twopy.__name__)'
49
49
  ```
50
50
 
51
- For a source checkout, copy `config.example.yml` to `config.yml` and edit the paths for your computer. `config.yml` stays local and is not tracked by git.
51
+ Create the config template in your user folder:
52
+
53
+ ```sh
54
+ twopy config setup
55
+ ```
56
+
57
+ twopy prints the config path it wrote. On macOS and Linux the default is `~/.config/twopy/config.yml`; on Windows it is under `%APPDATA%\twopy\config.yml`. Edit that file with your database, data, and output paths before launching twopy. If you launch `twopy` before a config exists, twopy creates the same template, prints the path, and stops so you can edit it.
58
+
59
+ Validate the active config and show its path and file contents:
60
+
61
+ ```sh
62
+ twopy config
63
+ ```
52
64
 
53
65
  ## Start
54
66
 
@@ -57,6 +69,8 @@ micromamba activate twopy
57
69
  twopy
58
70
  ```
59
71
 
72
+ `twopy` validates the config before opening napari. It does not require lab data folders or network drives to be mounted at launch.
73
+
60
74
  Open a source recording or converted recording directly:
61
75
 
62
76
  ```sh
@@ -8,7 +8,7 @@ Two-photon imaging analysis tool for the Clark Lab output format.
8
8
 
9
9
  twopy lets you open two-photon recordings, draw ROIs, plot responses in real time, process and analyze them, and save them.
10
10
 
11
- When you first load a recording, twopy converts it to a standardized HDF5 format. The converted format includes the aligned movie, mean image, stimulus tables, photodiode signals, and recording metadata. Analysis and the GUI both work from the converted files, so the original source files remain separate from twopy's outputs. By default, converted recordings and interactive analysis outputs are cached locally under `~/.cache/twopy/recordings` for speed; saving ROIs and analysis publishes the changed output files back to the configured `analysis_output` location in the background.
11
+ When you first load a recording, twopy converts it to HDF5 files that it owns. Those files include the aligned movie, mean image, stimulus tables, photodiode signals, and recording metadata. Analysis and the GUI both work from the converted files, so the original source files remain separate from twopy's outputs. By default, converted recordings and interactive analysis outputs are cached locally under `~/.cache/twopy/recordings` for speed; saving ROIs and analysis copies the changed output files back to the configured `analysis_output` location in the background.
12
12
 
13
13
  ## Install
14
14
 
@@ -26,7 +26,19 @@ Verify the install:
26
26
  micromamba run -n twopy python -c 'import twopy; print(twopy.__name__)'
27
27
  ```
28
28
 
29
- For a source checkout, copy `config.example.yml` to `config.yml` and edit the paths for your computer. `config.yml` stays local and is not tracked by git.
29
+ Create the config template in your user folder:
30
+
31
+ ```sh
32
+ twopy config setup
33
+ ```
34
+
35
+ twopy prints the config path it wrote. On macOS and Linux the default is `~/.config/twopy/config.yml`; on Windows it is under `%APPDATA%\twopy\config.yml`. Edit that file with your database, data, and output paths before launching twopy. If you launch `twopy` before a config exists, twopy creates the same template, prints the path, and stops so you can edit it.
36
+
37
+ Validate the active config and show its path and file contents:
38
+
39
+ ```sh
40
+ twopy config
41
+ ```
30
42
 
31
43
  ## Start
32
44
 
@@ -35,6 +47,8 @@ micromamba activate twopy
35
47
  twopy
36
48
  ```
37
49
 
50
+ `twopy` validates the config before opening napari. It does not require lab data folders or network drives to be mounted at launch.
51
+
38
52
  Open a source recording or converted recording directly:
39
53
 
40
54
  ```sh
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "twopy"
7
- version = "0.3.0"
7
+ version = "0.3.2"
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"
@@ -37,7 +37,7 @@ Repository = "https://github.com/gumadeiras/twopy"
37
37
  where = ["src"]
38
38
 
39
39
  [tool.setuptools.package-data]
40
- twopy = ["calibrations/*.csv"]
40
+ twopy = ["calibrations/*.csv", "config.example.yml"]
41
41
 
42
42
  [tool.ruff]
43
43
  line-length = 88
@@ -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
 
@@ -99,7 +101,8 @@ class LoadedAnalysisOutputs:
99
101
  """Analysis objects loaded from one twopy analysis HDF5 file.
100
102
 
101
103
  Inputs: one file written by ``save_analysis_outputs``.
102
- Outputs: optional ROI, trace, dF/F, window, and response objects.
104
+ Outputs: optional ROI, trace, dF/F, window, response objects, and a warning
105
+ when an old file uses a setting twopy no longer uses.
103
106
 
104
107
  Groups are optional because scripts can save only the products they have
105
108
  computed. Missing groups load as ``None`` or an empty window tuple.
@@ -115,6 +118,7 @@ class LoadedAnalysisOutputs:
115
118
  response_processing_options: ResponseProcessingOptions | None
116
119
  normalization_factors: RoiNormalizationFactors | None
117
120
  correlation_scores: RoiCorrelationScores | None
121
+ load_warning: str | None = None
118
122
 
119
123
 
120
124
  def save_analysis_outputs(
@@ -144,7 +148,7 @@ def save_analysis_outputs(
144
148
  grouped_responses: Optional grouped trial responses.
145
149
  response_processing_options: Optional smoothing, low-pass, and
146
150
  correlation-QC settings used for these outputs.
147
- normalization_factors: Optional per-ROI factors used for epoch-peak
151
+ normalization_factors: Optional per-ROI factors used for response-size
148
152
  normalization.
149
153
  correlation_scores: Optional ROI-level correlation QC scores.
150
154
  response_summary_trials_csv: Optional CSV path for one row per trial
@@ -157,8 +161,11 @@ def save_analysis_outputs(
157
161
  None.
158
162
 
159
163
  The HDF5 file stores arrays with gzip compression where arrays can be large.
160
- The HDF5 file is the complete audit trail. CSV files duplicate response
161
- time series in a spreadsheet-friendly layout for quick plotting and checks.
164
+ The HDF5 file is the complete audit trail. CSV files are derived exports for
165
+ quick spreadsheet plotting and checks, and are written after the HDF5 file.
166
+ Each output file is written through a temporary sibling path so a failed
167
+ write leaves the previous complete file in place; the HDF5 and CSV files are
168
+ not replaced as one multi-file transaction.
162
169
  """
163
170
  summary_outputs = (
164
171
  response_summary_trials_csv,
@@ -175,35 +182,40 @@ def save_analysis_outputs(
175
182
  if grouped_responses is not None:
176
183
  validate_grouped_roi_responses(grouped_responses)
177
184
 
178
- output_path = path.expanduser()
179
- output_path.parent.mkdir(parents=True, exist_ok=True)
180
- with h5py.File(output_path, "w") as h5_file:
181
- h5_file.attrs["twopy_format"] = ANALYSIS_OUTPUT_FILE_FORMAT
182
- if roi_set is not None:
183
- _write_roi_set(h5_file.create_group("roi_set"), roi_set)
184
- if traces is not None:
185
- _write_background_traces(h5_file.create_group("traces"), traces)
186
- if dff is not None:
187
- _write_dff(h5_file.create_group("dff"), dff)
188
- if len(epoch_windows) > 0:
189
- _write_epoch_windows(h5_file.create_group("epoch_windows"), epoch_windows)
190
- if len(baseline_windows) > 0:
191
- _write_frame_windows(
192
- h5_file.create_group("baseline_windows"),
193
- baseline_windows,
194
- )
195
- if grouped_responses is not None:
196
- _write_grouped_responses(
197
- h5_file.create_group("responses"),
198
- grouped_responses,
199
- )
200
- if response_processing_options is not None:
201
- write_response_processing_group(
202
- h5_file.create_group("response_processing"),
203
- options=response_processing_options,
204
- normalization_factors=normalization_factors,
205
- correlation_scores=correlation_scores,
206
- )
185
+ def write_hdf5(output_path: Path) -> None:
186
+ """Write the analysis HDF5 contents to one already-created path."""
187
+ with h5py.File(output_path, "w") as h5_file:
188
+ h5_file.attrs["twopy_format"] = ANALYSIS_OUTPUT_FILE_FORMAT
189
+ if roi_set is not None:
190
+ _write_roi_set(h5_file.create_group("roi_set"), roi_set)
191
+ if traces is not None:
192
+ _write_background_traces(h5_file.create_group("traces"), traces)
193
+ if dff is not None:
194
+ _write_dff(h5_file.create_group("dff"), dff)
195
+ if len(epoch_windows) > 0:
196
+ _write_epoch_windows(
197
+ h5_file.create_group("epoch_windows"),
198
+ epoch_windows,
199
+ )
200
+ if len(baseline_windows) > 0:
201
+ _write_frame_windows(
202
+ h5_file.create_group("baseline_windows"),
203
+ baseline_windows,
204
+ )
205
+ if grouped_responses is not None:
206
+ _write_grouped_responses(
207
+ h5_file.create_group("responses"),
208
+ grouped_responses,
209
+ )
210
+ if response_processing_options is not None:
211
+ write_response_processing_group(
212
+ h5_file.create_group("response_processing"),
213
+ options=response_processing_options,
214
+ normalization_factors=normalization_factors,
215
+ correlation_scores=correlation_scores,
216
+ )
217
+
218
+ _write_file_atomically(path, write_hdf5)
207
219
 
208
220
  if grouped_responses is not None:
209
221
  if response_summary_trials_csv is not None:
@@ -218,6 +230,58 @@ def save_analysis_outputs(
218
230
  )
219
231
 
220
232
 
233
+ def _write_file_atomically(
234
+ path: Path,
235
+ write_file: Callable[[Path], None],
236
+ ) -> None:
237
+ """Write a file beside its final path, then replace the old file.
238
+
239
+ Args:
240
+ path: Final destination path.
241
+ write_file: Callback that writes a complete file at the provided
242
+ temporary path.
243
+
244
+ Returns:
245
+ None.
246
+
247
+ Existing analysis files are useful audit records. Writing through a sibling
248
+ temporary path keeps a failed save from replacing the last complete output.
249
+ """
250
+ output_path = path.expanduser()
251
+ output_path.parent.mkdir(parents=True, exist_ok=True)
252
+ temp_descriptor, temp_name = tempfile.mkstemp(
253
+ prefix=f".{output_path.name}.",
254
+ suffix=".tmp",
255
+ dir=output_path.parent,
256
+ )
257
+ os.close(temp_descriptor)
258
+ temp_path = Path(temp_name)
259
+ try:
260
+ write_file(temp_path)
261
+ temp_path.chmod(_replacement_file_mode(output_path))
262
+ temp_path.replace(output_path)
263
+ except Exception:
264
+ temp_path.unlink(missing_ok=True)
265
+ raise
266
+
267
+
268
+ def _replacement_file_mode(path: Path) -> int:
269
+ """Return the file mode an atomic replacement should keep.
270
+
271
+ Args:
272
+ path: Final destination path.
273
+
274
+ Returns:
275
+ Existing file permissions, or normal new-file permissions after the
276
+ process umask.
277
+ """
278
+ if path.exists():
279
+ return path.stat().st_mode & 0o777
280
+ current_umask = os.umask(0)
281
+ os.umask(current_umask)
282
+ return 0o666 & ~current_umask
283
+
284
+
221
285
  def load_analysis_outputs(path: Path) -> LoadedAnalysisOutputs:
222
286
  """Load twopy analysis outputs from HDF5.
223
287
 
@@ -282,6 +346,11 @@ def load_analysis_outputs(path: Path) -> LoadedAnalysisOutputs:
282
346
  if response_processing is not None
283
347
  else None
284
348
  ),
349
+ load_warning=(
350
+ response_processing.load_warning
351
+ if response_processing is not None
352
+ else None
353
+ ),
285
354
  )
286
355
 
287
356
 
@@ -297,25 +366,31 @@ def write_response_summary_trials_csv(
297
366
 
298
367
  Returns:
299
368
  None.
369
+
370
+ The CSV is written through a temporary sibling path so a failed write leaves
371
+ the previous complete file in place.
300
372
  """
301
- output_path = path.expanduser()
302
- output_path.parent.mkdir(parents=True, exist_ok=True)
303
373
  validate_grouped_roi_responses(grouped_responses)
304
374
  time_columns = _time_columns(grouped_responses.trials)
305
375
  fieldnames = (*_TRIAL_RESPONSE_CSV_BASE_COLUMNS, *time_columns)
306
- with output_path.open("w", encoding="utf-8", newline="") as csv_file:
307
- writer = csv.DictWriter(csv_file, fieldnames=fieldnames)
308
- writer.writeheader()
309
- for trial in grouped_responses.trials:
310
- for roi_index, roi_label in enumerate(grouped_responses.roi_labels):
311
- writer.writerow(
312
- _trial_response_row(
313
- trial,
314
- roi_index=roi_index,
315
- roi_label=roi_label,
316
- time_columns=time_columns,
317
- ),
318
- )
376
+
377
+ def write_csv(output_path: Path) -> None:
378
+ """Write trial-level rows to one complete CSV file."""
379
+ with output_path.open("w", encoding="utf-8", newline="") as csv_file:
380
+ writer = csv.DictWriter(csv_file, fieldnames=fieldnames)
381
+ writer.writeheader()
382
+ for trial in grouped_responses.trials:
383
+ for roi_index, roi_label in enumerate(grouped_responses.roi_labels):
384
+ writer.writerow(
385
+ _trial_response_row(
386
+ trial,
387
+ roi_index=roi_index,
388
+ roi_label=roi_label,
389
+ time_columns=time_columns,
390
+ ),
391
+ )
392
+
393
+ _write_file_atomically(path, write_csv)
319
394
 
320
395
 
321
396
  def write_response_summary_grouped_csv(
@@ -330,17 +405,23 @@ def write_response_summary_grouped_csv(
330
405
 
331
406
  Returns:
332
407
  None.
408
+
409
+ The CSV is written through a temporary sibling path so a failed write leaves
410
+ the previous complete file in place.
333
411
  """
334
- output_path = path.expanduser()
335
- output_path.parent.mkdir(parents=True, exist_ok=True)
336
412
  validate_grouped_roi_responses(grouped_responses)
337
413
  time_columns = _time_columns(grouped_responses.trials)
338
414
  fieldnames = (*_GROUPED_RESPONSE_CSV_BASE_COLUMNS, *time_columns)
339
- with output_path.open("w", encoding="utf-8", newline="") as csv_file:
340
- writer = csv.DictWriter(csv_file, fieldnames=fieldnames)
341
- writer.writeheader()
342
- for row in _grouped_response_rows(grouped_responses, time_columns):
343
- writer.writerow(row)
415
+
416
+ def write_csv(output_path: Path) -> None:
417
+ """Write grouped rows to one complete CSV file."""
418
+ with output_path.open("w", encoding="utf-8", newline="") as csv_file:
419
+ writer = csv.DictWriter(csv_file, fieldnames=fieldnames)
420
+ writer.writeheader()
421
+ for row in _iter_grouped_response_rows(grouped_responses, time_columns):
422
+ writer.writerow(row)
423
+
424
+ _write_file_atomically(path, write_csv)
344
425
 
345
426
 
346
427
  def _write_roi_set(group: h5py.Group, roi_set: RoiSet) -> None:
@@ -894,10 +975,10 @@ def _trial_response_row(
894
975
  return row
895
976
 
896
977
 
897
- def _grouped_response_rows(
978
+ def _iter_grouped_response_rows(
898
979
  grouped: GroupedRoiResponses,
899
980
  time_columns: tuple[str, ...],
900
- ) -> tuple[dict[str, str | int | float], ...]:
981
+ ) -> Iterator[dict[str, str | int | float]]:
901
982
  """Convert grouped trial responses to mean, SEM, and count CSV rows.
902
983
 
903
984
  Args:
@@ -905,12 +986,12 @@ def _grouped_response_rows(
905
986
  time_columns: Complete CSV time-column union.
906
987
 
907
988
  Returns:
908
- Rows keyed by epoch, ROI, statistic, and relative timepoint.
989
+ Iterator over rows keyed by epoch, ROI, statistic, and relative
990
+ timepoint.
909
991
 
910
992
  Counts are per timepoint rather than one scalar because pre/post context or
911
993
  clipped recording edges can make some trials shorter than others.
912
994
  """
913
- rows: list[dict[str, str | int | float]] = []
914
995
  for epoch_number, epoch_name, trials in _trials_by_epoch(grouped.trials):
915
996
  for roi_index, roi_label in enumerate(grouped.roi_labels):
916
997
  values_by_time = _values_by_time_column(
@@ -918,16 +999,13 @@ def _grouped_response_rows(
918
999
  roi_index=roi_index,
919
1000
  time_columns=time_columns,
920
1001
  )
921
- rows.extend(
922
- _epoch_roi_statistic_rows(
923
- epoch_number=epoch_number,
924
- epoch_name=epoch_name,
925
- roi_label=roi_label,
926
- values_by_time=values_by_time,
927
- time_columns=time_columns,
928
- ),
1002
+ yield from _epoch_roi_statistic_rows(
1003
+ epoch_number=epoch_number,
1004
+ epoch_name=epoch_name,
1005
+ roi_label=roi_label,
1006
+ values_by_time=values_by_time,
1007
+ time_columns=time_columns,
929
1008
  )
930
- return tuple(rows)
931
1009
 
932
1010
 
933
1011
  def _trials_by_epoch(