ethograph 0.2.45__tar.gz → 0.2.46__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 (545) hide show
  1. {ethograph-0.2.45 → ethograph-0.2.46}/.gitignore +1 -0
  2. {ethograph-0.2.45 → ethograph-0.2.46}/CLAUDE.md +20 -5
  3. {ethograph-0.2.45 → ethograph-0.2.46}/PKG-INFO +6 -2
  4. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/__init__.py +5 -1
  5. ethograph-0.2.46/ethograph/features/bursts.py +312 -0
  6. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/features/movement.py +6 -1
  7. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/features/neural.py +94 -61
  8. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/app_state.py +40 -19
  9. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/cover_page.py +27 -31
  10. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/dialog_collected_data.py +2 -0
  11. ethograph-0.2.46/ethograph/gui/dialog_feature_labels.py +821 -0
  12. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/dialog_label_gridview.py +82 -26
  13. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/dialog_onset_model.py +12 -10
  14. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/dialog_pose_labelling.py +75 -231
  15. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/dialog_settings.py +1 -1
  16. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/dialog_tag_sheet.py +3 -3
  17. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/dialog_video_grid.py +18 -9
  18. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/heatmap_sort.py +60 -5
  19. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/label_drawing_mixin.py +25 -12
  20. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/main_window.py +3 -0
  21. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/plots_base.py +11 -1
  22. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/plots_console.py +11 -1
  23. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/plots_container.py +9 -25
  24. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/plots_heatmap.py +141 -35
  25. ethograph-0.2.46/ethograph/gui/plots_labelribbon.py +154 -0
  26. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/plots_lineplot.py +5 -1
  27. ethograph-0.2.46/ethograph/gui/plots_raster.py +357 -0
  28. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/plots_space.py +100 -16
  29. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/pose_annotate.py +4 -19
  30. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/pose_detect.py +4 -8
  31. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/pose_fill.py +9 -241
  32. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/pose_project_mode.py +2 -2
  33. ethograph-0.2.46/ethograph/gui/pose_second_view.py +683 -0
  34. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/pose_suggest.py +7 -62
  35. ethograph-0.2.46/ethograph/gui/pose_two_view.py +174 -0
  36. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/proxy_manager.py +19 -18
  37. ethograph-0.2.46/ethograph/gui/raster_render.py +192 -0
  38. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/right_context.py +19 -3
  39. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/shortcuts.py +8 -2
  40. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/source_popup.py +14 -2
  41. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/theme.py +2 -2
  42. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/top_bar.py +88 -10
  43. ethograph-0.2.46/ethograph/gui/triangulation.py +121 -0
  44. ethograph-0.2.46/ethograph/gui/unit_events.py +207 -0
  45. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/video_manager.py +7 -4
  46. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/widgets_bottom_bar.py +27 -2
  47. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/widgets_curation.py +83 -7
  48. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/widgets_data.py +25 -63
  49. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/widgets_ephys.py +692 -371
  50. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/widgets_labels.py +72 -10
  51. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/widgets_meta.py +63 -9
  52. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/widgets_navigation.py +40 -13
  53. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/widgets_plot_settings.py +51 -13
  54. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/widgets_psth.py +24 -66
  55. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/io/catalog.py +1 -9
  56. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/io/data_loader.py +2 -2
  57. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/io/metadata_table.py +20 -0
  58. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/io/validation.py +0 -27
  59. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/io/video_proxy.py +22 -12
  60. ethograph-0.2.46/ethograph/labels/feature_events.py +506 -0
  61. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/labels/intervals.py +6 -1
  62. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/labels/predictions.py +36 -1
  63. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/spot/pose_batch.py +1 -3
  64. ethograph-0.2.46/ethograph/triangulate/__init__.py +30 -0
  65. ethograph-0.2.46/ethograph/triangulate/calibration.py +138 -0
  66. ethograph-0.2.46/ethograph/triangulate/frame.py +143 -0
  67. ethograph-0.2.46/ethograph/triangulate/geometry.py +109 -0
  68. ethograph-0.2.46/ethograph/triangulate/points.py +76 -0
  69. ethograph-0.2.46/ethograph/triangulate/session.py +386 -0
  70. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/utils/paths.py +1 -0
  71. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph.egg-info/PKG-INFO +6 -2
  72. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph.egg-info/SOURCES.txt +20 -3
  73. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph.egg-info/requires.txt +6 -1
  74. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph.egg-info/scm_file_list.json +46 -13
  75. ethograph-0.2.46/ethograph.egg-info/scm_version.json +8 -0
  76. ethograph-0.2.46/notes/Lit_active-periods.md +209 -0
  77. ethograph-0.2.46/notes/Lit_unsupervised-proposals.md +291 -0
  78. ethograph-0.2.46/notes/adr/0015-periods-from-thresholds-and-burst-detectors.md +111 -0
  79. ethograph-0.2.46/notes/adr/0016-aniposelib-does-the-geometry.md +62 -0
  80. ethograph-0.2.46/notes/drafts/pynapple_burst_detection.py +513 -0
  81. ethograph-0.2.46/notes/drafts/pynapple_burst_r_to_python.html +271 -0
  82. {ethograph-0.2.45 → ethograph-0.2.46}/projects/crowlab/sessions.txt +2 -1
  83. {ethograph-0.2.45 → ethograph-0.2.46}/pyproject.toml +29 -15
  84. {ethograph-0.2.45 → ethograph-0.2.46}/uv.lock +643 -66
  85. ethograph-0.2.45/ethograph/gui/dialog_pose_refinement.py +0 -1177
  86. ethograph-0.2.45/ethograph/gui/plots_labelribbon.py +0 -82
  87. ethograph-0.2.45/ethograph/gui/plots_raster.py +0 -298
  88. ethograph-0.2.45/ethograph/gui/pose_refine.py +0 -709
  89. ethograph-0.2.45/ethograph.egg-info/scm_version.json +0 -8
  90. ethograph-0.2.45/test.ipynb +0 -0
  91. {ethograph-0.2.45 → ethograph-0.2.46}/.claude/settings.local.json +0 -0
  92. {ethograph-0.2.45 → ethograph-0.2.46}/.gitattributes +0 -0
  93. {ethograph-0.2.45 → ethograph-0.2.46}/.github/copilot-instructions.md +0 -0
  94. {ethograph-0.2.45 → ethograph-0.2.46}/.github/workflows/docs_build_and_deploy.yml +0 -0
  95. {ethograph-0.2.45 → ethograph-0.2.46}/.github/workflows/test_and_deploy.yml +0 -0
  96. {ethograph-0.2.45 → ethograph-0.2.46}/.readthedocs.yaml +0 -0
  97. {ethograph-0.2.45 → ethograph-0.2.46}/LICENSE +0 -0
  98. {ethograph-0.2.45 → ethograph-0.2.46}/MANIFEST.in +0 -0
  99. {ethograph-0.2.45 → ethograph-0.2.46}/README.md +0 -0
  100. {ethograph-0.2.45 → ethograph-0.2.46}/THIRD_PARTY_NOTICES.md +0 -0
  101. {ethograph-0.2.45 → ethograph-0.2.46}/TODO.md +0 -0
  102. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/__main__.py +0 -0
  103. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/_vendor/__init__.py +0 -0
  104. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/_vendor/vocalseg/LICENSE +0 -0
  105. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/_vendor/vocalseg/NOTICE.md +0 -0
  106. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/_vendor/vocalseg/__init__.py +0 -0
  107. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/_vendor/vocalseg/continuity_filtering.py +0 -0
  108. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/_vendor/vocalseg/dynamic_thresholding.py +0 -0
  109. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/_vendor/vocalseg/utils.py +0 -0
  110. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/assets/icon.ico +0 -0
  111. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/assets/icon.png +0 -0
  112. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/assets/menu.json +0 -0
  113. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/assets/space/moll2025.yaml +0 -0
  114. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/cli.py +0 -0
  115. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/datasets.py +0 -0
  116. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/defaults/README.md +0 -0
  117. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/defaults/mapping.txt +0 -0
  118. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/defaults/octron/octron.yaml +0 -0
  119. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/defaults/segment/segment.yaml +0 -0
  120. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/defaults/spot/spot.yaml +0 -0
  121. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/features/__init__.py +0 -0
  122. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/features/audio_changepoints.py +0 -0
  123. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/features/changepoints.py +0 -0
  124. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/features/columns.py +0 -0
  125. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/features/energy.py +0 -0
  126. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/features/geometry.py +0 -0
  127. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/features/label_inputs.py +0 -0
  128. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/features/oscillatory.py +0 -0
  129. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/features/preprocessing.py +0 -0
  130. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/__init__.py +0 -0
  131. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/active_panel.py +0 -0
  132. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/app_constants.py +0 -0
  133. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/assets/templates/birdpark0.png +0 -0
  134. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/assets/templates/birdpark1.png +0 -0
  135. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/assets/templates/canary.png +0 -0
  136. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/assets/templates/cricket0.png +0 -0
  137. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/assets/templates/cricket1.png +0 -0
  138. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/assets/templates/cricket2.png +0 -0
  139. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/assets/templates/lockbox1.png +0 -0
  140. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/assets/templates/lockbox2.gif +0 -0
  141. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/assets/templates/moll1.png +0 -0
  142. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/assets/templates/moll2.png +0 -0
  143. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/assets/wizard/video_setup_free_running.png +0 -0
  144. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/assets/wizard/video_setup_triggered.png +0 -0
  145. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/audio_clock.py +0 -0
  146. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/audio_player.py +0 -0
  147. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/box_annotate.py +0 -0
  148. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/box_overlay.py +0 -0
  149. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/dialog_ambiguous_session.py +0 -0
  150. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/dialog_box_labelling.py +0 -0
  151. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/dialog_bulk_labels.py +0 -0
  152. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/dialog_busy_progress.py +0 -0
  153. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/dialog_curation_workflow.py +0 -0
  154. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/dialog_curator_feedback.py +0 -0
  155. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/dialog_fit.py +0 -0
  156. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/dialog_function_params.py +0 -0
  157. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/dialog_keypoint_filter.py +0 -0
  158. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/dialog_label_inconsistencies.py +0 -0
  159. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/dialog_label_table.py +0 -0
  160. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/dialog_pose_video_matcher.py +0 -0
  161. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/dialog_review_histogram.py +0 -0
  162. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/dialog_screen_recorder.py +0 -0
  163. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/dialog_select_template.py +0 -0
  164. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/dialog_session_folder.py +0 -0
  165. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/dialog_skeleton_editor.py +0 -0
  166. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/dialog_space_geometry.py +0 -0
  167. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/dialog_spot_crop.py +0 -0
  168. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/file_dialogs.py +0 -0
  169. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/grid_section_container.py +0 -0
  170. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/individual_colors.py +0 -0
  171. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/left_sidebar.py +0 -0
  172. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/make_pretty.py +0 -0
  173. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/napari.yaml +0 -0
  174. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/notify.py +0 -0
  175. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/plots_audiotrace.py +0 -0
  176. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/plots_ephystrace.py +0 -0
  177. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/plots_overlay.py +0 -0
  178. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/plots_psth.py +0 -0
  179. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/plots_radial.py +0 -0
  180. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/plots_skeleton.py +0 -0
  181. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/plots_spectrogram.py +0 -0
  182. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/pose_convert.py +0 -0
  183. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/pose_detect_preview.py +0 -0
  184. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/pose_edit_mixin.py +0 -0
  185. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/pose_overlay.py +0 -0
  186. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/pose_render.py +0 -0
  187. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/pose_static_group.py +0 -0
  188. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/pose_tagsheet.py +0 -0
  189. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/pose_training_export.py +0 -0
  190. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/project.py +0 -0
  191. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/pygfx_video.py +0 -0
  192. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/session_folder.py +0 -0
  193. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/space_shapes.py +0 -0
  194. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/table_filter.py +0 -0
  195. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/templates/notebook/discover.py.j2 +0 -0
  196. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/templates/notebook/open.md.j2 +0 -0
  197. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/templates/notebook/parameters.py.j2 +0 -0
  198. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/templates/notebook/timing_offset.py.j2 +0 -0
  199. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/templates/notebook/timing_onsets.py.j2 +0 -0
  200. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/templates/notebook/timing_pulses_free.py.j2 +0 -0
  201. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/templates/notebook/timing_pulses_triggered.py.j2 +0 -0
  202. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/templates/notebook/write_neuroconv.py.j2 +0 -0
  203. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/templates/notebook/write_pair.py.j2 +0 -0
  204. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/templates/wizard_nwb_codegen.j2 +0 -0
  205. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/video_sync.py +0 -0
  206. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/widget_trials.py +0 -0
  207. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/widgets_changepoints.py +0 -0
  208. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/widgets_frame_extract.py +0 -0
  209. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/widgets_help.py +0 -0
  210. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/widgets_io.py +0 -0
  211. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/widgets_transform.py +0 -0
  212. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/wizard_boris.py +0 -0
  213. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/wizard_media_files.py +0 -0
  214. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/wizard_multi_builder.py +0 -0
  215. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/wizard_multi_codegen.py +0 -0
  216. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/wizard_multi_tabs.py +0 -0
  217. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/wizard_multi_timeline.py +0 -0
  218. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/wizard_multi_trials.py +0 -0
  219. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/wizard_notebook.py +0 -0
  220. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/wizard_nwb.py +0 -0
  221. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/wizard_overview.py +0 -0
  222. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/wizard_pages.py +0 -0
  223. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/wizard_single.py +0 -0
  224. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/gui/wizard_state.py +0 -0
  225. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/io/audio_extract.py +0 -0
  226. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/io/dataset.py +0 -0
  227. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/io/derived.py +0 -0
  228. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/io/ephys_loader.py +0 -0
  229. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/io/external_features.py +0 -0
  230. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/io/image_sequence.py +0 -0
  231. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/io/metadata_edit.py +0 -0
  232. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/io/nc_drop.py +0 -0
  233. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/io/netcdf.py +0 -0
  234. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/io/nwb_alignment.py +0 -0
  235. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/io/nwb_import.py +0 -0
  236. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/io/overlay_source.py +0 -0
  237. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/io/pairing.py +0 -0
  238. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/io/plot_sources.py +0 -0
  239. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/io/pynapple.py +0 -0
  240. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/io/schema.py +0 -0
  241. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/io/session_layout.py +0 -0
  242. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/io/time_model.py +0 -0
  243. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/io/time_sources.py +0 -0
  244. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/io/trialtree.py +0 -0
  245. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/io/video_decode.py +0 -0
  246. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/io/video_feature_files.py +0 -0
  247. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/io/video_probe.py +0 -0
  248. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/labels/__init__.py +0 -0
  249. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/labels/boris.py +0 -0
  250. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/labels/check_labels.py +0 -0
  251. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/labels/confidence.py +0 -0
  252. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/labels/converters.py +0 -0
  253. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/labels/crowsetta_format.py +0 -0
  254. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/labels/curation.py +0 -0
  255. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/labels/export.py +0 -0
  256. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/labels/exporters/__init__.py +0 -0
  257. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/labels/exporters/crowlab.py +0 -0
  258. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/labels/label_inputs.py +0 -0
  259. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/labels/ml.py +0 -0
  260. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/labels/octron_project.py +0 -0
  261. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/labels/onset_curves.py +0 -0
  262. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/labels/onset_model.py +0 -0
  263. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/labels/plots.py +0 -0
  264. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/labels/pose_project.py +0 -0
  265. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/labels/rescore.py +0 -0
  266. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/labels/review_metrics.py +0 -0
  267. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/labels/tsv_store.py +0 -0
  268. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/labels/workflow.py +0 -0
  269. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/__init__.py +0 -0
  270. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/augment.py +0 -0
  271. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/config.py +0 -0
  272. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/crossval.py +0 -0
  273. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/dataset.py +0 -0
  274. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/dlc2action/LICENSE.APGPL +0 -0
  275. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/dlc2action/NOTICE.md +0 -0
  276. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/dlc2action/__init__.py +0 -0
  277. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/dlc2action/colors.txt +0 -0
  278. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/dlc2action/config/losses.yaml +0 -0
  279. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/dlc2action/config/model/asformer.yaml +0 -0
  280. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/dlc2action/config/model/c2f_tcn.yaml +0 -0
  281. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/dlc2action/config/model/c2f_transformer.yaml +0 -0
  282. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/dlc2action/config/model/edtcn.yaml +0 -0
  283. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/dlc2action/config/model/mlp.yaml +0 -0
  284. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/dlc2action/config/model/motionbert.yaml +0 -0
  285. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/dlc2action/config/model/ms_tcn3.yaml +0 -0
  286. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/dlc2action/config/model/transformer.yaml +0 -0
  287. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/dlc2action/config/training.yaml +0 -0
  288. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/dlc2action/loss/__init__.py +0 -0
  289. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/dlc2action/loss/asymmetric_loss.py +0 -0
  290. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/dlc2action/loss/ms_tcn.py +0 -0
  291. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/dlc2action/loss/mse.py +0 -0
  292. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/dlc2action/loss/tcc.py +0 -0
  293. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/dlc2action/model/__init__.py +0 -0
  294. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/dlc2action/model/asformer.py +0 -0
  295. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/dlc2action/model/base_model.py +0 -0
  296. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/dlc2action/model/c2f_tcn.py +0 -0
  297. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/dlc2action/model/c2f_tcn_par.py +0 -0
  298. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/dlc2action/model/c2f_transformer.py +0 -0
  299. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/dlc2action/model/c3d.py +0 -0
  300. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/dlc2action/model/edtcn.py +0 -0
  301. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/dlc2action/model/mlp.py +0 -0
  302. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/dlc2action/model/motionbert.py +0 -0
  303. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/dlc2action/model/motionbert_modules.py +0 -0
  304. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/dlc2action/model/ms_tcn.py +0 -0
  305. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/dlc2action/model/ms_tcn_modules.py +0 -0
  306. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/dlc2action/model/transformer.py +0 -0
  307. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/dlc2action/utils.py +0 -0
  308. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/dlc2action/version.py +0 -0
  309. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/feral.py +0 -0
  310. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/feral_defaults/NOTICE.md +0 -0
  311. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/feral_defaults/default_config.yaml +0 -0
  312. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/feral_defaults/presets.yaml +0 -0
  313. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/inference.py +0 -0
  314. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/joints.py +0 -0
  315. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/lady/LICENSE +0 -0
  316. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/lady/NOTICE.md +0 -0
  317. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/lady/__init__.py +0 -0
  318. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/lady/config/defaults.yaml +0 -0
  319. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/lady/generalized_coordinates.py +0 -0
  320. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/lady/loss_fn/ECloss.py +0 -0
  321. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/lady/loss_fn/__init__.py +0 -0
  322. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/lady/models/LaDy.py +0 -0
  323. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/lady/models/SP.py +0 -0
  324. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/lady/models/__init__.py +0 -0
  325. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/lady/models/dynamics.py +0 -0
  326. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/lady/models/graph/__init__.py +0 -0
  327. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/lady/models/graph/tools.py +0 -0
  328. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/lady/models/tcn.py +0 -0
  329. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/losses.py +0 -0
  330. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/materialise.py +0 -0
  331. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/metrics.py +0 -0
  332. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/models/__init__.py +0 -0
  333. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/models/config/rnn.yaml +0 -0
  334. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/models/rnn.py +0 -0
  335. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/models/skeleton_graph.py +0 -0
  336. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/models/vendored.py +0 -0
  337. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/plotting.py +0 -0
  338. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/postprocess.py +0 -0
  339. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/prediction_sets.py +0 -0
  340. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/preprocess.py +0 -0
  341. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/project.py +0 -0
  342. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/roles.py +0 -0
  343. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/samples.py +0 -0
  344. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/search.py +0 -0
  345. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/sessions.py +0 -0
  346. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/specscalpel/LICENSE +0 -0
  347. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/specscalpel/NOTICE.md +0 -0
  348. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/specscalpel/__init__.py +0 -0
  349. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/specscalpel/config/defaults.yaml +0 -0
  350. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/specscalpel/models/SP.py +0 -0
  351. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/specscalpel/models/SpecScalpel.py +0 -0
  352. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/specscalpel/models/__init__.py +0 -0
  353. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/specscalpel/models/graph/__init__.py +0 -0
  354. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/specscalpel/models/graph/tools.py +0 -0
  355. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/specscalpel/models/tcn.py +0 -0
  356. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/train.py +0 -0
  357. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/video_features.py +0 -0
  358. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/segment/windows.py +0 -0
  359. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/shortcuts.py +0 -0
  360. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/skeleton/CREDITS.md +0 -0
  361. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/skeleton/__init__.py +0 -0
  362. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/skeleton/config.py +0 -0
  363. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/skeleton/graph.py +0 -0
  364. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/skeleton/library.py +0 -0
  365. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/skeleton/renderers/__init__.py +0 -0
  366. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/skeleton/renderers/base.py +0 -0
  367. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/skeleton/renderers/precomputed.py +0 -0
  368. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/skeleton/shapes.py +0 -0
  369. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/skeleton/state.py +0 -0
  370. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/skeleton/templates.py +0 -0
  371. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/spot/__init__.py +0 -0
  372. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/spot/confidence.py +0 -0
  373. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/spot/config.py +0 -0
  374. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/spot/crossval.py +0 -0
  375. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/spot/dataset.py +0 -0
  376. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/spot/e2espot/LICENSE +0 -0
  377. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/spot/e2espot/NOTICE.md +0 -0
  378. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/spot/e2espot/__init__.py +0 -0
  379. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/spot/e2espot/dataset/__init__.py +0 -0
  380. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/spot/e2espot/dataset/frame.py +0 -0
  381. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/spot/e2espot/dataset/transform.py +0 -0
  382. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/spot/e2espot/model/__init__.py +0 -0
  383. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/spot/e2espot/model/common.py +0 -0
  384. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/spot/e2espot/model/impl/__init__.py +0 -0
  385. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/spot/e2espot/model/impl/asformer.py +0 -0
  386. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/spot/e2espot/model/impl/gsm.py +0 -0
  387. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/spot/e2espot/model/impl/tsm.py +0 -0
  388. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/spot/e2espot/model/modules.py +0 -0
  389. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/spot/e2espot/model/shift.py +0 -0
  390. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/spot/e2espot/test_e2e.py +0 -0
  391. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/spot/e2espot/train_e2e.py +0 -0
  392. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/spot/e2espot/util/__init__.py +0 -0
  393. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/spot/e2espot/util/dataset.py +0 -0
  394. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/spot/e2espot/util/eval.py +0 -0
  395. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/spot/e2espot/util/io.py +0 -0
  396. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/spot/e2espot/util/score.py +0 -0
  397. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/spot/features.py +0 -0
  398. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/spot/inference.py +0 -0
  399. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/spot/metrics.py +0 -0
  400. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/spot/msagsm.py +0 -0
  401. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/spot/prediction_sets.py +0 -0
  402. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/spot/project.py +0 -0
  403. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/spot/stream.py +0 -0
  404. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/spot/vendored.py +0 -0
  405. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/utils/__init__.py +0 -0
  406. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/utils/arraytools.py +0 -0
  407. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/utils/audio.py +0 -0
  408. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/utils/configkit.py +0 -0
  409. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/utils/device.py +0 -0
  410. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/utils/download.py +0 -0
  411. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/utils/ffmpeg.py +0 -0
  412. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/utils/logging.py +0 -0
  413. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/utils/nwb.py +0 -0
  414. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/utils/qt.py +0 -0
  415. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/utils/sequences.py +0 -0
  416. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/utils/stream_durations.py +0 -0
  417. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/utils/system_check.py +0 -0
  418. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/utils/xr_utils.py +0 -0
  419. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/video_features/__init__.py +0 -0
  420. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/video_features/base.py +0 -0
  421. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/video_features/checkpoint/S3D_kinetics400_torchified.pt +0 -0
  422. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/video_features/extract.py +0 -0
  423. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/video_features/frames.py +0 -0
  424. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/video_features/plan.py +0 -0
  425. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/video_features/s3d.py +0 -0
  426. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/video_features/select.py +0 -0
  427. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph/video_features/timm_extract.py +0 -0
  428. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph.egg-info/dependency_links.txt +0 -0
  429. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph.egg-info/entry_points.txt +0 -0
  430. {ethograph-0.2.45 → ethograph-0.2.46}/ethograph.egg-info/top_level.txt +0 -0
  431. {ethograph-0.2.45 → ethograph-0.2.46}/examples/README.md +0 -0
  432. {ethograph-0.2.45 → ethograph-0.2.46}/examples/birdpark_dev/birdpark_pose.ipynb +0 -0
  433. {ethograph-0.2.45 → ethograph-0.2.46}/examples/convert_dataset_birdpark.ipynb +0 -0
  434. {ethograph-0.2.45 → ethograph-0.2.46}/examples/convert_dataset_pair24.ipynb +0 -0
  435. {ethograph-0.2.45 → ethograph-0.2.46}/examples/convert_datset_lockbox.ipynb +0 -0
  436. {ethograph-0.2.45 → ethograph-0.2.46}/examples/create_dataset_Moll25.ipynb +0 -0
  437. {ethograph-0.2.45 → ethograph-0.2.46}/examples/create_dataset_cricket.ipynb +0 -0
  438. {ethograph-0.2.45 → ethograph-0.2.46}/examples/segement_s3d_features.ipynb +0 -0
  439. {ethograph-0.2.45 → ethograph-0.2.46}/examples/segment_changepoint_features.ipynb +0 -0
  440. {ethograph-0.2.45 → ethograph-0.2.46}/notes/Bench_neural-decoding.md +0 -0
  441. {ethograph-0.2.45 → ethograph-0.2.46}/notes/Bench_smoothalpha.md +0 -0
  442. {ethograph-0.2.45 → ethograph-0.2.46}/notes/Lit_curator-feedback.md +0 -0
  443. {ethograph-0.2.45 → ethograph-0.2.46}/notes/Lit_dlc2action.md +0 -0
  444. {ethograph-0.2.45 → ethograph-0.2.46}/notes/Lit_feralbeast.md +0 -0
  445. {ethograph-0.2.45 → ethograph-0.2.46}/notes/Lit_precise-event-spotting.md +0 -0
  446. {ethograph-0.2.45 → ethograph-0.2.46}/notes/adr/0001-vendor-dlc2action-architectures.md +0 -0
  447. {ethograph-0.2.45 → ethograph-0.2.46}/notes/adr/0002-literature-dataset-layout.md +0 -0
  448. {ethograph-0.2.45 → ethograph-0.2.46}/notes/adr/0003-advisory-variable-schema.md +0 -0
  449. {ethograph-0.2.45 → ethograph-0.2.46}/notes/adr/0004-scripted-not-cli.md +0 -0
  450. {ethograph-0.2.45 → ethograph-0.2.46}/notes/adr/0005-search-then-cross-validate.md +0 -0
  451. {ethograph-0.2.45 → ethograph-0.2.46}/notes/adr/0006-postprocess-from-gui-settings.md +0 -0
  452. {ethograph-0.2.45 → ethograph-0.2.46}/notes/adr/0007-distil-inside-the-vendored-trainer.md +0 -0
  453. {ethograph-0.2.45 → ethograph-0.2.46}/notes/adr/0008-features-not-graphs.md +0 -0
  454. {ethograph-0.2.45 → ethograph-0.2.46}/notes/adr/0009-extractors-pip-installed-not-vendored.md +0 -0
  455. {ethograph-0.2.45 → ethograph-0.2.46}/notes/adr/0009-spikes-as-a-config-transform.md +0 -0
  456. {ethograph-0.2.45 → ethograph-0.2.46}/notes/adr/0010-multilabel-targets-are-tracks.md +0 -0
  457. {ethograph-0.2.45 → ethograph-0.2.46}/notes/adr/0011-labels-as-inputs-across-branches.md +0 -0
  458. {ethograph-0.2.45 → ethograph-0.2.46}/notes/adr/0012-feral-is-an-external-extractor.md +0 -0
  459. {ethograph-0.2.45 → ethograph-0.2.46}/notes/adr/0013-no-learned-boundary-model-over-pose.md +0 -0
  460. {ethograph-0.2.45 → ethograph-0.2.46}/notes/adr/0013-session-is-a-folder-media-are-absolute.md +0 -0
  461. {ethograph-0.2.45 → ethograph-0.2.46}/notes/adr/0014-pose-project-as-two-sessions.md +0 -0
  462. {ethograph-0.2.45 → ethograph-0.2.46}/notes/adr/0015-flat-project-folder-and-libraries.md +0 -0
  463. {ethograph-0.2.45 → ethograph-0.2.46}/notes/feedback/audio_clock_dac_timestamps.md +0 -0
  464. {ethograph-0.2.45 → ethograph-0.2.46}/notes/feedback/debug_launcher_clock_patch.md +0 -0
  465. {ethograph-0.2.45 → ethograph-0.2.46}/notes/feedback/ethograph_annotation_feasibility_report.md +0 -0
  466. {ethograph-0.2.45 → ethograph-0.2.46}/notes/feedback/ethograph_windows_author_overview_2026-08-18.md +0 -0
  467. {ethograph-0.2.45 → ethograph-0.2.46}/notes/feedback/processed/ehtograph_feedback_report.md +0 -0
  468. {ethograph-0.2.45 → ethograph-0.2.46}/notes/feedback/processed/handoff_playback_and_video_freeze.md +0 -0
  469. {ethograph-0.2.45 → ethograph-0.2.46}/notes/feedback/reverted_pynapple_trials_timing.diff +0 -0
  470. {ethograph-0.2.45 → ethograph-0.2.46}/projects/crowlab/ReadMe.md +0 -0
  471. {ethograph-0.2.45 → ethograph-0.2.46}/projects/crowlab/config/space/moll2025.yaml +0 -0
  472. {ethograph-0.2.45 → ethograph-0.2.46}/projects/crowlab/mapping.txt +0 -0
  473. {ethograph-0.2.45 → ethograph-0.2.46}/projects/crowlab/mapping_setup_audio.txt +0 -0
  474. {ethograph-0.2.45 → ethograph-0.2.46}/projects/crowlab/old config/changepoint_settings_old.yaml +0 -0
  475. {ethograph-0.2.45 → ethograph-0.2.46}/projects/crowlab/old config/mapping_Ivy.txt +0 -0
  476. {ethograph-0.2.45 → ethograph-0.2.46}/projects/crowlab/old config/old segmentation config/Crow1_inference_20260330_161224.json +0 -0
  477. {ethograph-0.2.45 → ethograph-0.2.46}/projects/crowlab/old config/old segmentation config/Crow1_inference_20260330_161259.json +0 -0
  478. {ethograph-0.2.45 → ethograph-0.2.46}/projects/crowlab/old config/old segmentation config/Freddy_inference_20260330_160621.json +0 -0
  479. {ethograph-0.2.45 → ethograph-0.2.46}/projects/crowlab/old config/old segmentation config/Freddy_train_20251021_164220.json +0 -0
  480. {ethograph-0.2.45 → ethograph-0.2.46}/projects/crowlab/old config/old segmentation config/Freddy_train_20251021_164220_epoch-100.model +0 -0
  481. {ethograph-0.2.45 → ethograph-0.2.46}/projects/crowlab/old config/old segmentation config/Ivy_CV_20260130_173800.json +0 -0
  482. {ethograph-0.2.45 → ethograph-0.2.46}/projects/crowlab/old config/old segmentation config/Ivy_inference_20260203_092442.json +0 -0
  483. {ethograph-0.2.45 → ethograph-0.2.46}/projects/crowlab/old config/old segmentation config/Ivy_inference_20260204_141527.json +0 -0
  484. {ethograph-0.2.45 → ethograph-0.2.46}/projects/crowlab/old config/old segmentation config/Ivy_inference_20260206_122331.json +0 -0
  485. {ethograph-0.2.45 → ethograph-0.2.46}/projects/crowlab/old config/old segmentation config/Ivy_train_20260202_191138_epoch-100.model +0 -0
  486. {ethograph-0.2.45 → ethograph-0.2.46}/projects/crowlab/old config/old segmentation config/Ivy_train_20260203_153754.json +0 -0
  487. {ethograph-0.2.45 → ethograph-0.2.46}/projects/crowlab/old config/old segmentation config/Poppy_inference_20260312_100707.json +0 -0
  488. {ethograph-0.2.45 → ethograph-0.2.46}/projects/crowlab/old config/old segmentation config/Poppy_inference_20260327_102414.json +0 -0
  489. {ethograph-0.2.45 → ethograph-0.2.46}/projects/crowlab/old config/old segmentation config/Poppy_inference_20260330_140808.json +0 -0
  490. {ethograph-0.2.45 → ethograph-0.2.46}/projects/crowlab/old config/old segmentation config/Poppy_inference_20260402_104151.json +0 -0
  491. {ethograph-0.2.45 → ethograph-0.2.46}/projects/crowlab/old config/old segmentation config/Poppy_train_20260326_190925.json +0 -0
  492. {ethograph-0.2.45 → ethograph-0.2.46}/projects/crowlab/old config/old segmentation config/Poppy_train_20260331_175332.json +0 -0
  493. {ethograph-0.2.45 → ethograph-0.2.46}/projects/crowlab/old config/old segmentation config/ReadMe - model Freddy_train_20251021_164220_epoch-100.txt +0 -0
  494. {ethograph-0.2.45 → ethograph-0.2.46}/projects/crowlab/project.yaml +0 -0
  495. {ethograph-0.2.45 → ethograph-0.2.46}/projects/crowlab/wizard/CrowBench.ipynb +0 -0
  496. {ethograph-0.2.45 → ethograph-0.2.46}/projects/crowlab/wizard/SENTINEL.txt +0 -0
  497. {ethograph-0.2.45 → ethograph-0.2.46}/projects/crowlab/workflows/01 PES Curation.yaml +0 -0
  498. {ethograph-0.2.45 → ethograph-0.2.46}/projects/crowlab/workflows/Freddy_GBM.yaml +0 -0
  499. {ethograph-0.2.45 → ethograph-0.2.46}/projects/crowlab/workflows/Ivy_GBM.yaml +0 -0
  500. {ethograph-0.2.45 → ethograph-0.2.46}/projects/crowlab/workflows/Poppy_GBM.yaml +0 -0
  501. {ethograph-0.2.45 → ethograph-0.2.46}/projects/neural/decoding.yaml +0 -0
  502. {ethograph-0.2.45 → ethograph-0.2.46}/projects/paper/labels.yaml +0 -0
  503. {ethograph-0.2.45 → ethograph-0.2.46}/projects/paper/spot/spot.yaml +0 -0
  504. {ethograph-0.2.45 → ethograph-0.2.46}/scripts/Developer.md +0 -0
  505. {ethograph-0.2.45 → ethograph-0.2.46}/scripts/_sweep.py +0 -0
  506. {ethograph-0.2.45 → ethograph-0.2.46}/scripts/bench.py +0 -0
  507. {ethograph-0.2.45 → ethograph-0.2.46}/scripts/bench_search.py +0 -0
  508. {ethograph-0.2.45 → ethograph-0.2.46}/scripts/bench_videofeat.py +0 -0
  509. {ethograph-0.2.45 → ethograph-0.2.46}/scripts/bench_watch.ps1 +0 -0
  510. {ethograph-0.2.45 → ethograph-0.2.46}/scripts/compare.py +0 -0
  511. {ethograph-0.2.45 → ethograph-0.2.46}/scripts/dandi/dandi_ibl-brainwide.ipynb.ipynb +0 -0
  512. {ethograph-0.2.45 → ethograph-0.2.46}/scripts/dandi/nwb_video_testing.ipynb +0 -0
  513. {ethograph-0.2.45 → ethograph-0.2.46}/scripts/describe_sessions.py +0 -0
  514. {ethograph-0.2.45 → ethograph-0.2.46}/scripts/ephys/ephys_drift_diagnosis.ipynb +0 -0
  515. {ethograph-0.2.45 → ethograph-0.2.46}/scripts/ephys/ephys_loader.ipynb +0 -0
  516. {ethograph-0.2.45 → ethograph-0.2.46}/scripts/export_frames.py +0 -0
  517. {ethograph-0.2.45 → ethograph-0.2.46}/scripts/export_neural_rate.py +0 -0
  518. {ethograph-0.2.45 → ethograph-0.2.46}/scripts/feral/export_feral.py +0 -0
  519. {ethograph-0.2.45 → ethograph-0.2.46}/scripts/feral/reinfer_feral.py +0 -0
  520. {ethograph-0.2.45 → ethograph-0.2.46}/scripts/feral/run_feral.py +0 -0
  521. {ethograph-0.2.45 → ethograph-0.2.46}/scripts/feral/score_feral.py +0 -0
  522. {ethograph-0.2.45 → ethograph-0.2.46}/scripts/neuroconv.ipynb +0 -0
  523. {ethograph-0.2.45 → ethograph-0.2.46}/scripts/paper1/README.md +0 -0
  524. {ethograph-0.2.45 → ethograph-0.2.46}/scripts/paper1/changepoint_bars.py +0 -0
  525. {ethograph-0.2.45 → ethograph-0.2.46}/scripts/paper1/crossval_bars.py +0 -0
  526. {ethograph-0.2.45 → ethograph-0.2.46}/scripts/paper1/crossval_label_counts.py +0 -0
  527. {ethograph-0.2.45 → ethograph-0.2.46}/scripts/paper1/embedding_grid.py +0 -0
  528. {ethograph-0.2.45 → ethograph-0.2.46}/scripts/paper1/embedding_heatmaps.py +0 -0
  529. {ethograph-0.2.45 → ethograph-0.2.46}/scripts/paper1/figure_combined.py +0 -0
  530. {ethograph-0.2.45 → ethograph-0.2.46}/scripts/paper1/figure_neural.py +0 -0
  531. {ethograph-0.2.45 → ethograph-0.2.46}/scripts/paper1/figure_review.py +0 -0
  532. {ethograph-0.2.45 → ethograph-0.2.46}/scripts/paper1/figure_review_curves.py +0 -0
  533. {ethograph-0.2.45 → ethograph-0.2.46}/scripts/paper1/figure_review_speed.py +0 -0
  534. {ethograph-0.2.45 → ethograph-0.2.46}/scripts/paper1/s3d_ablation_bars.py +0 -0
  535. {ethograph-0.2.45 → ethograph-0.2.46}/scripts/paper1/session.py +0 -0
  536. {ethograph-0.2.45 → ethograph-0.2.46}/scripts/paper1/style/style ppt.mplstyle +0 -0
  537. {ethograph-0.2.45 → ethograph-0.2.46}/scripts/paper1/style/style.mplstyle +0 -0
  538. {ethograph-0.2.45 → ethograph-0.2.46}/scripts/release.ps1 +0 -0
  539. {ethograph-0.2.45 → ethograph-0.2.46}/scripts/run.py +0 -0
  540. {ethograph-0.2.45 → ethograph-0.2.46}/scripts/run_sleep.py +0 -0
  541. {ethograph-0.2.45 → ethograph-0.2.46}/scripts/screenshot_gui.py +0 -0
  542. {ethograph-0.2.45 → ethograph-0.2.46}/scripts/spot.py +0 -0
  543. {ethograph-0.2.45 → ethograph-0.2.46}/scripts/test.ipynb +0 -0
  544. {ethograph-0.2.45 → ethograph-0.2.46}/scripts/train_spot.py +0 -0
  545. {ethograph-0.2.45 → ethograph-0.2.46}/setup.cfg +0 -0
@@ -2,6 +2,7 @@
2
2
  projects/
3
3
  !projects/defaults/*
4
4
  spot_frames/
5
+ *pynapple_burst_r_to_python.html
5
6
  result/
6
7
  data/
7
8
  weights/
@@ -61,9 +61,10 @@ ethograph/gui/
61
61
  plots_{audiotrace,spectrogram,ephystrace,lineplot,heatmap,raster,space,radial,console}.py
62
62
  video_sync.py, video_manager.py, audio_clock.py, label_drawing_mixin.py
63
63
  pose_render.py # Pose loading (NWB + movement), PoseDisplayManager
64
- pose_{annotate,fill,refine,detect,detect_preview,tagsheet,edit_mixin}.py # keypoint labelling + fill
64
+ pose_{annotate,fill,detect,detect_preview,tagsheet,edit_mixin}.py # keypoint labelling + fill
65
+ pose_two_view.py, pose_second_view.py, triangulation.py # a second camera view → 3D
65
66
  box_annotate.py, box_overlay.py, dialog_box_labelling.py # box labelling (OCTRON)
66
- dialog_{pose_labelling,skeleton_editor,pose_refinement,tag_sheet}.py
67
+ dialog_{pose_labelling,skeleton_editor,tag_sheet}.py
67
68
  pose_project_mode.py, widgets_frame_extract.py, dialog_collected_data.py, pose_static_group.py # pose-project mode
68
69
  dialog_{label_gridview,video_grid,label_table,onset_model,curation_workflow}.py
69
70
  right_context.py, main_window.py, top_bar.py, cover_page.py, table_filter.py, file_dialogs.py
@@ -110,6 +111,12 @@ ethograph/spot/ # Pixel point-event spotting (docs: docs/source/mo
110
111
  e2espot/ # Vendored E2E-Spot in upstream's layout — see its NOTICE.md; excluded from ruff/mypy
111
112
  msagsm.py # MultiScaleGatedShift, written from the paper
112
113
 
114
+ ethograph/triangulate/ # 2D points from calibrated cameras → 3D, through aniposelib
115
+ calibration.py # {project}/calibration/{name}.toml (aniposelib's own file) + the DeepLabCut 3D import
116
+ points.py # triangulate_points — the one place aniposelib's triangulation is called
117
+ session.py # A session's per-camera 2D points → position_3d in its .nc
118
+ frame.py, geometry.py # The world frame beside a calibration; static landmarks as a space/ geometry
119
+
113
120
  ethograph/utils/ # io.py, xr_utils.py, sequences.py, device.py, system_check.py
114
121
  configkit.py # Chained YAML + dotted overrides ↔ dataclass tree; segment and spot each pass a Schema
115
122
  ethograph/skeleton/ # PrecomputedRenderer, SkeletonState, config.py, shapes.py
@@ -149,7 +156,7 @@ THIRD_PARTY_NOTICES.md # Index of every vendored tree and adapted file
149
156
  `.nwb` sources are read/edited directly; `.ethograph/alignment.nwb` sidecars exist only for non-NWB sources.
150
157
 
151
158
  - **Trial timing has one source: the alignment NWB trials table.**
152
- - **A session is a folder: it holds `.ethograph/alignment.nwb` or a root `.nwb`, and at most one `.nc` (its dataset).** A second root `.nc` is an old version and refused (`AmbiguousSessionError`) until `project.yaml`'s `ignore` names it; a file named explicitly is always loaded. Every session file is named by the folder through `io/session_layout.py` (`session_dir_of`, never `Path(...).parent`). Media folders are absolute and separate: the pairing table carries full paths (`discover_media` takes no root); the NWB stores basenames per trial and full paths per stream. The session record declares the individuals; a dataset's individual dim may only name a subset.
159
+ - **A session is a folder: it holds `.ethograph/alignment.nwb` or a root `.nwb`, and at most one `.nc` (its dataset).** A second root `.nc` is an old version and refused (`AmbiguousSessionError`) until the user's `ignore_files` (`gui_settings.yaml`, global, never per project) names it; a file named explicitly is always loaded. Every session file is named by the folder through `io/session_layout.py` (`session_dir_of`, never `Path(...).parent`). Media folders are absolute and separate: the pairing table carries full paths (`discover_media` takes no root); the NWB stores basenames per trial and full paths per stream. The session record declares the individuals; a dataset's individual dim may only name a subset.
153
160
  - **A drop makes the dropped folder the session** (`.ethograph/` goes there; several source folders → one popup, `gui/session_folder.py` ranks kinds); a folder that already has an alignment without a drop record is never overwritten.
154
161
  - **Metadata is purely additive** — a tabular file joined on `trial`, resolved explicit `alignment_path` → NWB source trials → sidecar TSV, edited in the trials table and written back to the source it was read from. Media filenames are the alignment's, joined read-only onto the table's right-hand side, never written to a metadata file.
155
162
  - **The trials table's filters are the one trial filter, and every operation runs over `app_state.trials`.** No dialog gets a metadata filter of its own; the label filter is a second, session-only slot intersected after the column filters.
@@ -162,9 +169,9 @@ THIRD_PARTY_NOTICES.md # Index of every vendored tree and adapted file
162
169
 
163
170
  `PoseRenderData` unifies file (movement) and NWB (lazy HDF5) poses; filtering acts on masks, never recreates layers. **The video overlays any catalog feature in the camera's pixels** (`io/overlay_source.py`): a time dim, a `space` dim with x/y, at most keypoint/individual dims; `position` is the default; companions by name (`confidence` filters, `shape` means boxes); a pose file is read only when the dataset has nothing in that camera's pixels. **Colour encodes one axis, chosen by the user** (`app_state.pose_color_by`); text labels carry the other.
164
171
 
165
- **Keypoint labelling's binding design rules live in `docs/source/advanced/keypoint_labelling/`. Read them before editing `gui/pose_*.py`, `dialog_pose_labelling.py`, `dialog_tag_sheet.py` or `table_filter.py`.** Scope: one camera, one trial.
172
+ **Keypoint labelling's binding design rules live in `docs/source/advanced/classroom_pose_estimation/`. Read them before editing `gui/pose_*.py`, `dialog_pose_labelling.py`, `dialog_tag_sheet.py` or `table_filter.py`.** Scope: one camera, one trial per store; a second camera view is a second store beside the first, never a camera axis (`gui/pose_second_view.py`).
166
173
 
167
- Skeleton precedence: `skeleton_config_override` (user-drawn) > the project's `project.yaml` skeleton > NWB config; each recoloured with `skeleton_base_color`. **`project.yaml` holds study defaults** (individuals, cameras, mics, rig, pose software, skeleton); a session's record or dataset overrides them, machine paths never go in it (`gui/project.py`). Anchored shapes (`skeleton/shapes.py`) are templates bound to ≥2 control points.
174
+ Skeleton precedence: `skeleton_config_override` (user-drawn) > the project's `config/skeleton/*.yaml` > NWB config; each recoloured with `skeleton_base_color`. **There is no `project.yaml`**: the project folder holds files (vocabulary, configs, skeletons, runs), the user's additions (`extra_individuals`, `ignore_files`) are global settings, and a legacy `project.yaml` is folded into them once (`gui/project.py`). Anchored shapes (`skeleton/shapes.py`) are templates bound to ≥2 control points.
168
175
 
169
176
  ### Panels are layout instances — no per-plot-type toggles
170
177
 
@@ -227,6 +234,14 @@ Point events learned from video with the vendored E2E-Spot, scripted like `segme
227
234
  - **The pose side is a flat `features:` list, nothing else** (ADR 0008): session variables in segment's `features.columns` spelling, written per trial to `features/{video_id}.npz` and, z-scored on the training split, fed to the model as a second GRU input (run named `{clip}_features`). `graph:`, `fuse:` and a `columns:` key are refused by name. `evaluate(zero_features=True)` measures what the features contribute; `train.features_dropout` is off by default.
228
235
  - **Inference always decodes the video straight into the model** (`stream.py`), mirroring `test_e2e.py`'s windows, padding, transform and score accumulation; the frame folder is training's alone. Worker counts are a machine property, never in the YAML. `check_vram` raises before any run the card cannot hold. `inference()` flags an out-of-order trial, never reorders.
229
236
 
237
+ ### Triangulation (`ethograph/triangulate/`)
238
+
239
+ Qt-free and scripted like `segment` (`eto.triangulate`). **aniposelib does every piece of multi-view geometry; Ethograph only reads 2D points and writes the result.** The world frame is the one exception, because aniposelib has none.
240
+
241
+ - **A calibration is a rig's, so it lives in the project**: `{project}/calibration/{name}.toml`, aniposelib's file untouched; one needs no choosing, several are named (`app_state.calibration_name`, per dataset). Its camera names are the alignment's.
242
+ - **2D points are read as the overlay reads them** — a variable with a `camera` dim, else a pose file per camera — and paired on the trial clock, never by frame index.
243
+ - **The result is an ordinary feature**: `position_3d` + `reprojection_error` in the session's `.nc`; the labelling dialog's live 3D points are never stored.
244
+
230
245
  ### Widget orchestration
231
246
 
232
247
  `MetaWidget` creates all widgets and wires signals; `DataWidget` is the central orchestrator. Flow: `NavigationWidget` → `trial_changed` → `DataWidget.on_trial_changed()` → everything else. The right sidebar is context-sensitive (`_CONTEXT_MAP` in `right_context.py`) with exactly one individual combo. **Guarded shortcuts are disabled while typing, never a no-op.** Neo and Phy trace panels are added on demand, never auto-loaded.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ethograph
3
- Version: 0.2.45
3
+ Version: 0.2.46
4
4
  Summary: A multimodal timeseries GUI for neuroethology
5
5
  Author-email: Akseli Ilmanen <akseli.ilmanen@gmail.com>
6
6
  License-Expression: GPL-3.0-or-later
@@ -40,6 +40,7 @@ Requires-Dist: qtpy; extra == "gui"
40
40
  Requires-Dist: pyqtgraph; extra == "gui"
41
41
  Requires-Dist: PyOpenGL; extra == "gui"
42
42
  Requires-Dist: pygfx>=0.15.3; extra == "gui"
43
+ Requires-Dist: uharfbuzz<0.56; extra == "gui"
43
44
  Requires-Dist: pynaviz>=0.1; extra == "gui"
44
45
  Requires-Dist: rendercanvas; extra == "gui"
45
46
  Requires-Dist: wgpu; extra == "gui"
@@ -54,8 +55,9 @@ Requires-Dist: phylib>=2.6.4; extra == "gui"
54
55
  Requires-Dist: neo; extra == "gui"
55
56
  Requires-Dist: scikit-learn; extra == "gui"
56
57
  Requires-Dist: joblib; extra == "gui"
57
- Requires-Dist: opencv-contrib-python-headless; extra == "gui"
58
+ Requires-Dist: opencv-python-headless<5,>=4.7; extra == "gui"
58
59
  Requires-Dist: pupil-apriltags; extra == "gui"
60
+ Requires-Dist: rastermap; extra == "gui"
59
61
  Provides-Extra: audio
60
62
  Requires-Dist: audioio; extra == "audio"
61
63
  Requires-Dist: soundfile; extra == "audio"
@@ -67,6 +69,8 @@ Provides-Extra: dandi
67
69
  Requires-Dist: dandi; extra == "dandi"
68
70
  Provides-Extra: proxy
69
71
  Requires-Dist: imageio-ffmpeg; extra == "proxy"
72
+ Provides-Extra: triangulate
73
+ Requires-Dist: aniposelib>=0.8; extra == "triangulate"
70
74
  Provides-Extra: model
71
75
  Requires-Dist: tqdm; extra == "model"
72
76
  Requires-Dist: movement>=0.14; extra == "model"
@@ -40,7 +40,7 @@ from ethograph.utils.xr_utils import get_ds_duration, get_time_coord, sel_valid
40
40
 
41
41
 
42
42
  def __getattr__(name: str):
43
- """Expose the model pipelines lazily — they import torch, which the base install lacks."""
43
+ """Expose the optional pipelines lazily — they import torch or aniposelib, which the base install lacks."""
44
44
  if name == "segment":
45
45
  import ethograph.segment as segment
46
46
 
@@ -49,6 +49,10 @@ def __getattr__(name: str):
49
49
  import ethograph.spot as spot
50
50
 
51
51
  return spot
52
+ if name == "triangulate":
53
+ import ethograph.triangulate as triangulate
54
+
55
+ return triangulate
52
56
  raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
53
57
 
54
58
 
@@ -0,0 +1,312 @@
1
+ """Burst detection in spike trains: MaxInterval, logISI and an instantaneous-rate threshold.
2
+
3
+ The two detectors that came out ahead in Cotterill et al. (2016, J Neurophysiol
4
+ 116:306, https://www.ncbi.nlm.nih.gov/pmc/articles/PMC4969396/), as described in
5
+ Cotterill & Eglen (2019, https://arxiv.org/abs/1802.01287), plus the birdsong
6
+ field's definition. Checked against the R reference implementations by
7
+ ``tests/_test_burst_r_reference.py``.
8
+
9
+ Every detector takes a ``Ts`` or a ``TsGroup`` and returns bursts as an
10
+ ``IntervalSet`` from first to last spike with an ``n_spikes`` metadata column
11
+ (a dict keyed by unit for a ``TsGroup``). Detection runs within each epoch of
12
+ the time support, so a burst never spans a gap in it.
13
+
14
+ These are proposed for pynapple; once it ships them this module is an import
15
+ of theirs.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import numbers
21
+ from collections.abc import Callable
22
+ from typing import Any
23
+
24
+ import numpy as np
25
+ import pynapple as nap
26
+
27
+ #: A run of spikes as (first, last) spike indices, both inclusive.
28
+ Run = tuple[int, int]
29
+ #: What a detector does with one epoch's spike times.
30
+ _FindBursts = Callable[[np.ndarray], list[Run]]
31
+
32
+ #: A void parameter at or above this separates two peaks of the log-ISI histogram.
33
+ _VOID_THRESHOLD = 0.7
34
+ #: A valley at or beyond this (seconds) separates bursts from pauses, not spikes within bursts.
35
+ _MAX_VALLEY_S = 1.0
36
+
37
+
38
+ def _check_spikes(spikes: Any) -> None:
39
+ if not isinstance(spikes, (nap.Ts, nap.TsGroup)):
40
+ raise TypeError(f"`spikes` must be `Ts` or `TsGroup`, got {type(spikes)}")
41
+
42
+
43
+ def _check_positive(name: str, value: Any, strict: bool = True) -> None:
44
+ if not isinstance(value, numbers.Real):
45
+ raise TypeError(f"`{name}` must be a number, got {type(value)}")
46
+ if strict and value <= 0:
47
+ raise ValueError(f"`{name}` must be > 0")
48
+ if not strict and value < 0:
49
+ raise ValueError(f"`{name}` must be >= 0")
50
+
51
+
52
+ def _check_min_spikes(name: str, value: Any) -> None:
53
+ if not isinstance(value, numbers.Integral):
54
+ raise TypeError(f"`{name}` must be an integer, got {type(value)}")
55
+ if value < 2:
56
+ raise ValueError(f"`{name}` must be >= 2, a burst has at least two spikes")
57
+
58
+
59
+ def _find_runs(times: np.ndarray, max_isi_start: float, max_isi_burst: float) -> list[Run]:
60
+ """Phase 1: a run starts at the first of two spikes closer than *max_isi_start* and
61
+ ends at the last spike before an interval longer than *max_isi_burst*."""
62
+ runs: list[Run] = []
63
+ start: int | None = None
64
+ for i in range(1, len(times)):
65
+ isi = times[i] - times[i - 1]
66
+ if start is None:
67
+ if isi < max_isi_start:
68
+ start = i - 1
69
+ elif isi > max_isi_burst:
70
+ runs.append((start, i - 1))
71
+ start = None
72
+ if start is not None:
73
+ runs.append((start, len(times) - 1))
74
+ return runs
75
+
76
+
77
+ def _merge_close_runs(times: np.ndarray, runs: list[Run], min_inter_burst_interval: float) -> list[Run]:
78
+ """Phase 2: consecutive runs separated by less than *min_inter_burst_interval*
79
+ (last spike of one to first spike of the next) become one run."""
80
+ merged: list[Run] = []
81
+ for first, last in runs:
82
+ if merged and times[first] - times[merged[-1][1]] < min_inter_burst_interval:
83
+ merged[-1] = (merged[-1][0], last)
84
+ else:
85
+ merged.append((first, last))
86
+ return merged
87
+
88
+
89
+ def _drop_small_runs(
90
+ times: np.ndarray, runs: list[Run], min_burst_duration: float, min_spikes_in_burst: int
91
+ ) -> list[Run]:
92
+ """Phase 3: a run shorter than *min_burst_duration* or with fewer than
93
+ *min_spikes_in_burst* spikes is not a burst."""
94
+ return [
95
+ (first, last)
96
+ for first, last in runs
97
+ if times[last] - times[first] >= min_burst_duration and last - first + 1 >= min_spikes_in_burst
98
+ ]
99
+
100
+
101
+ def _detect_per_epoch(spikes: nap.Ts, find_bursts: _FindBursts) -> nap.IntervalSet:
102
+ """Run *find_bursts* on the spikes of each epoch of the time support, so no burst
103
+ spans a gap in it, and return the bursts as one IntervalSet."""
104
+ starts, ends, n_spikes = [], [], []
105
+ for epoch in spikes.time_support:
106
+ times = spikes.restrict(epoch).t
107
+ for first, last in find_bursts(times):
108
+ # Spikes with identical timestamps span no time and cannot be an interval.
109
+ if times[last] == times[first]:
110
+ continue
111
+ starts.append(times[first])
112
+ ends.append(times[last])
113
+ n_spikes.append(last - first + 1)
114
+ return nap.IntervalSet(start=starts, end=ends, metadata={"n_spikes": n_spikes})
115
+
116
+
117
+ def _apply(spikes: nap.Ts | nap.TsGroup, find_bursts: _FindBursts) -> nap.IntervalSet | dict[Any, nap.IntervalSet]:
118
+ if isinstance(spikes, nap.TsGroup):
119
+ return {k: _detect_per_epoch(spikes[k], find_bursts) for k in spikes}
120
+ return _detect_per_epoch(spikes, find_bursts)
121
+
122
+
123
+ def detect_bursts_max_interval(
124
+ spikes: nap.Ts | nap.TsGroup,
125
+ max_isi_start: float = 0.17,
126
+ max_isi_burst: float = 0.3,
127
+ min_inter_burst_interval: float = 0.2,
128
+ min_burst_duration: float = 0.01,
129
+ min_spikes_in_burst: int = 3,
130
+ ) -> nap.IntervalSet | dict[Any, nap.IntervalSet]:
131
+ """Bursts by the MaxInterval method of NeuroExplorer.
132
+
133
+ A burst starts at the first of two spikes closer than *max_isi_start* and
134
+ continues while consecutive spikes are no further apart than
135
+ *max_isi_burst*. Bursts separated by less than *min_inter_burst_interval*
136
+ are merged; one shorter than *min_burst_duration* or with fewer than
137
+ *min_spikes_in_burst* spikes is discarded. All times are in seconds. The
138
+ parameters are those of figure 3 of Cotterill & Eglen (2019), the
139
+ defaults table 1 of Cotterill et al. (2016).
140
+ """
141
+ _check_spikes(spikes)
142
+ _check_positive("max_isi_start", max_isi_start)
143
+ _check_positive("max_isi_burst", max_isi_burst)
144
+ _check_positive("min_inter_burst_interval", min_inter_burst_interval, strict=False)
145
+ _check_positive("min_burst_duration", min_burst_duration, strict=False)
146
+ _check_min_spikes("min_spikes_in_burst", min_spikes_in_burst)
147
+
148
+ def find_bursts(times: np.ndarray) -> list[Run]:
149
+ runs = _find_runs(times, max_isi_start, max_isi_burst)
150
+ runs = _merge_close_runs(times, runs, min_inter_burst_interval)
151
+ return _drop_small_runs(times, runs, min_burst_duration, min_spikes_in_burst)
152
+
153
+ return _apply(spikes, find_bursts)
154
+
155
+
156
+ def _log_isi_histogram(isi_ms: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
157
+ """The interspike intervals (ms) on a logarithmic axis: 10 bin edges per decade from
158
+ 1 ms to the decade above the longest interval, intervals under 1 ms left out.
159
+
160
+ Returns the bins' left edges (ms) and the counts normalised to sum to one.
161
+ """
162
+ decades = max(1, int(np.ceil(np.log10(isi_ms.max()))))
163
+ edges = np.logspace(0, decades, 10 * decades)
164
+ isi_ms = isi_ms[isi_ms >= 1]
165
+ # Bin k holds edges[k] < isi <= edges[k + 1]; 1 ms itself goes to bin 0.
166
+ bins = np.clip(np.searchsorted(edges, isi_ms, side="left") - 1, 0, None)
167
+ counts = np.bincount(bins, minlength=len(edges) - 1)
168
+ return edges[:-1], counts / counts.sum()
169
+
170
+
171
+ def _find_peaks(density: np.ndarray) -> list[int]:
172
+ """Bins strictly higher than every bin within two bins of them, first and last bin excluded."""
173
+ peaks = []
174
+ for k in range(1, len(density) - 1):
175
+ neighbours = np.delete(density[max(0, k - 2) : k + 3], min(k, 2))
176
+ if np.all(density[k] > neighbours):
177
+ peaks.append(k)
178
+ return peaks
179
+
180
+
181
+ def _log_isi_valley(isi_s: np.ndarray, max_cutoff: float) -> tuple[bool, float | None]:
182
+ """The threshold of Pasquale et al. (2010): the ISI at the first valley that separates
183
+ the intra-burst peak of the log-ISI histogram from a later peak.
184
+
185
+ Returns whether the histogram has an intra-burst peak at all, and the
186
+ threshold in seconds (``None`` without a well-separated valley).
187
+ """
188
+ isi_ms = isi_s * 1000
189
+ if len(isi_ms) == 0 or not np.any(isi_ms >= 1):
190
+ return False, None
191
+ edges, density = _log_isi_histogram(isi_ms)
192
+ peaks = _find_peaks(density)
193
+ intraburst = [k for k in peaks if edges[k] < max_cutoff * 1000]
194
+ if not intraburst:
195
+ return False, None
196
+ peak = max(intraburst, key=lambda k: density[k])
197
+ for later in peaks:
198
+ if later <= peak:
199
+ continue
200
+ between = density[peak : later + 1]
201
+ valley = peak + int(np.argmin(between))
202
+ void = 1 - between.min() / np.sqrt(density[peak] * density[later])
203
+ if void >= _VOID_THRESHOLD:
204
+ return True, float(edges[valley] / 1000)
205
+ return True, None
206
+
207
+
208
+ def _interspike_intervals(spikes: nap.Ts) -> np.ndarray:
209
+ """The intervals between consecutive spikes, within each epoch of the time support only."""
210
+ # pynapple's time_diff() cannot read a train with no spikes at all.
211
+ if len(spikes) == 0:
212
+ return np.empty(0)
213
+ return spikes.time_diff().values
214
+
215
+
216
+ def compute_log_isi_threshold(
217
+ spikes: nap.Ts | nap.TsGroup, max_cutoff: float = 0.1
218
+ ) -> float | None | dict[Any, float | None]:
219
+ """The burst threshold of the logISI method, in seconds.
220
+
221
+ The interval at the valley between the intra-burst peak of the log-ISI
222
+ histogram (its largest peak below *max_cutoff*) and the first later peak
223
+ well separated from it. ``None`` when the histogram has no intra-burst
224
+ peak or no such valley.
225
+ """
226
+ _check_spikes(spikes)
227
+ _check_positive("max_cutoff", max_cutoff)
228
+ if isinstance(spikes, nap.TsGroup):
229
+ return {k: _log_isi_valley(_interspike_intervals(spikes[k]), max_cutoff)[1] for k in spikes}
230
+ return _log_isi_valley(_interspike_intervals(spikes), max_cutoff)[1]
231
+
232
+
233
+ def _extend_to_related_spikes(times: np.ndarray, cores: list[Run], threshold: float) -> list[Run]:
234
+ """Extend each core to the run of spikes with consecutive intervals no longer than
235
+ *threshold* that contains it; cores inside the same run become one burst."""
236
+ runs = _find_runs(times, threshold, threshold)
237
+ extended: list[Run] = []
238
+ for first, last in cores:
239
+ container = next((r for r in runs if r[0] <= first and last <= r[1]), (first, last))
240
+ if container not in extended:
241
+ extended.append(container)
242
+ return extended
243
+
244
+
245
+ def detect_bursts_log_isi(
246
+ spikes: nap.Ts | nap.TsGroup,
247
+ max_cutoff: float = 0.1,
248
+ min_spikes_in_burst: int = 3,
249
+ fallback: bool = True,
250
+ ) -> nap.IntervalSet | dict[Any, nap.IntervalSet]:
251
+ """Bursts by the logISI method of Pasquale et al. (2010).
252
+
253
+ The threshold between intra- and inter-burst intervals is read off each
254
+ train's own log-ISI histogram (:func:`compute_log_isi_threshold`), over
255
+ every epoch; a burst is a run of at least *min_spikes_in_burst* spikes
256
+ with consecutive intervals no longer than it. A threshold above
257
+ *max_cutoff* finds burst cores at *max_cutoff*, merges cores closer than
258
+ the threshold and extends each to the spikes within the threshold of its
259
+ ends. No intra-burst peak means no bursts. No clear valley (or one at a
260
+ second or more) falls back to *max_cutoff* as the threshold — unless
261
+ *fallback* is off, which returns no bursts for such a train and is not
262
+ part of the published method.
263
+ """
264
+ _check_spikes(spikes)
265
+ _check_positive("max_cutoff", max_cutoff)
266
+ _check_min_spikes("min_spikes_in_burst", min_spikes_in_burst)
267
+ if not isinstance(fallback, bool):
268
+ raise TypeError(f"`fallback` must be a bool, got {type(fallback)}")
269
+
270
+ if isinstance(spikes, nap.TsGroup):
271
+ return {k: detect_bursts_log_isi(spikes[k], max_cutoff, min_spikes_in_burst, fallback) for k in spikes}
272
+
273
+ has_peak, threshold = _log_isi_valley(_interspike_intervals(spikes), max_cutoff)
274
+
275
+ def find_bursts(times: np.ndarray) -> list[Run]:
276
+ if not has_peak:
277
+ return []
278
+ if threshold is None or threshold >= _MAX_VALLEY_S:
279
+ if not fallback:
280
+ return []
281
+ runs = _find_runs(times, max_cutoff, max_cutoff)
282
+ return _drop_small_runs(times, runs, 0, min_spikes_in_burst)
283
+ if threshold > max_cutoff:
284
+ cores = _find_runs(times, max_cutoff, max_cutoff)
285
+ cores = _merge_close_runs(times, cores, threshold)
286
+ cores = _drop_small_runs(times, cores, 0, min_spikes_in_burst)
287
+ return _extend_to_related_spikes(times, cores, threshold)
288
+ runs = _find_runs(times, threshold, threshold)
289
+ return _drop_small_runs(times, runs, 0, min_spikes_in_burst)
290
+
291
+ return _detect_per_epoch(spikes, find_bursts)
292
+
293
+
294
+ def detect_bursts_instantaneous_rate(
295
+ spikes: nap.Ts | nap.TsGroup, rate_threshold: float = 125.0, min_spikes_in_burst: int = 2
296
+ ) -> nap.IntervalSet | dict[Any, nap.IntervalSet]:
297
+ """Bursts as the runs of spikes over which the instantaneous rate exceeds *rate_threshold* (Hz).
298
+
299
+ The instantaneous rate between two spikes is the inverse of their
300
+ interval — the burst definition of the birdsong literature, 125 Hz in
301
+ Leonardo & Fee (2005, https://doi.org/10.1523/JNEUROSCI.3036-04.2005).
302
+ """
303
+ _check_spikes(spikes)
304
+ _check_positive("rate_threshold", rate_threshold)
305
+ _check_min_spikes("min_spikes_in_burst", min_spikes_in_burst)
306
+ max_isi = 1.0 / rate_threshold
307
+
308
+ def find_bursts(times: np.ndarray) -> list[Run]:
309
+ runs = _find_runs(times, max_isi, max_isi)
310
+ return _drop_small_runs(times, runs, 0, min_spikes_in_burst)
311
+
312
+ return _apply(spikes, find_bursts)
@@ -14,7 +14,12 @@ from scipy.spatial.distance import cdist
14
14
 
15
15
 
16
16
  class Position3DCalibration:
17
- """Convert 3D positions from DLC to real-world coordinates."""
17
+ """Convert 3D positions from DLC to real-world coordinates.
18
+
19
+ Hand-tuned for the Moll et al., 2025 rig and kept for that dataset's
20
+ example. A new rig gets a world frame beside its calibration instead
21
+ (``ethograph.triangulate.frame``).
22
+ """
18
23
 
19
24
  def __init__(
20
25
  self,
@@ -1,4 +1,4 @@
1
- """Compute firing rates, PCA, and PSTH from spike times using pynapple.
1
+ """Compute firing rates from spike times using pynapple.
2
2
 
3
3
  Assumes spike_times are in seconds and sorted ascending (standard for Kilosort/Phy).
4
4
 
@@ -26,14 +26,13 @@ session open, every time.
26
26
  """
27
27
 
28
28
  from collections.abc import Sequence
29
+ from dataclasses import dataclass
29
30
  from typing import Any, Literal
30
31
 
31
32
  import numpy as np
32
33
  import pynapple as nap
33
34
  import xarray as xr
34
35
 
35
- from ethograph.io import schema
36
-
37
36
 
38
37
  def build_tsgroup(
39
38
  spike_times: np.ndarray,
@@ -76,6 +75,63 @@ def build_tsgroup(
76
75
  return nap.TsGroup(units, time_support=time_support)
77
76
 
78
77
 
78
+ @dataclass(frozen=True)
79
+ class SpikeTable:
80
+ """Every spike of a ``TsGroup`` in one time-sorted table.
81
+
82
+ A window of it is one binary search, however many units there are —
83
+ restricting the group unit by unit is one pynapple call each.
84
+ """
85
+
86
+ times: np.ndarray
87
+ units: np.ndarray
88
+
89
+ @classmethod
90
+ def from_tsgroup(cls, tsgroup: nap.TsGroup) -> "SpikeTable":
91
+ unit_ids = [int(uid) for uid in tsgroup.keys()]
92
+ trains = [np.asarray(tsgroup[uid].times(), dtype=np.float64) for uid in unit_ids]
93
+ if not trains:
94
+ return cls(np.empty(0, dtype=np.float64), np.empty(0, dtype=np.int64))
95
+ times = np.concatenate(trains)
96
+ units = np.repeat(np.asarray(unit_ids, dtype=np.int64), [len(train) for train in trains])
97
+ order = np.argsort(times, kind="stable")
98
+ return cls(times[order], units[order])
99
+
100
+ def window(self, t_start: float, t_stop: float) -> tuple[np.ndarray, np.ndarray]:
101
+ """Spike times and their unit ids inside ``[t_start, t_stop]``."""
102
+ i0 = int(np.searchsorted(self.times, t_start, side="left"))
103
+ i1 = int(np.searchsorted(self.times, t_stop, side="right"))
104
+ return self.times[i0:i1], self.units[i0:i1]
105
+
106
+ def count(
107
+ self, unit_ids: Sequence[int], t_start: float, t_stop: float, bin_size: float
108
+ ) -> tuple[np.ndarray, np.ndarray]:
109
+ """Spike counts of ``unit_ids`` in bins of ``bin_size`` seconds from ``t_start``.
110
+
111
+ Returns the counts, shape ``(n_bins, n_units)`` with the columns in
112
+ the order of ``unit_ids``, and the centre of each bin. Only whole bins
113
+ are counted; a remainder shorter than a bin is left out.
114
+ """
115
+ if bin_size <= 0:
116
+ raise ValueError(f"bin_size must be positive, got {bin_size}")
117
+ columns = np.asarray(unit_ids, dtype=np.int64)
118
+ n_bins = int(np.floor((t_stop - t_start) / bin_size + 1e-9))
119
+ centers = t_start + (np.arange(n_bins) + 0.5) * bin_size
120
+ if n_bins <= 0 or len(columns) == 0:
121
+ return np.zeros((max(n_bins, 0), len(columns)), dtype=np.int64), centers
122
+
123
+ times, units = self.window(t_start, t_start + n_bins * bin_size)
124
+ by_id = np.argsort(columns, kind="stable")
125
+ found = np.searchsorted(columns, units, sorter=by_id)
126
+ found[found == len(columns)] = 0
127
+ column = by_id[found]
128
+ bin_index = np.floor((times - t_start) / bin_size).astype(np.int64)
129
+ keep = (columns[column] == units) & (bin_index < n_bins)
130
+ flat = bin_index[keep] * len(columns) + column[keep]
131
+ counts = np.bincount(flat, minlength=n_bins * len(columns))
132
+ return counts.reshape(n_bins, len(columns)), centers
133
+
134
+
79
135
  def firing_rate_by_cluster(
80
136
  spike_times: np.ndarray,
81
137
  spike_clusters: np.ndarray,
@@ -114,8 +170,10 @@ def firing_rate_by_cluster(
114
170
  Cluster ID for each row, shape ``(n_clusters,)``.
115
171
  """
116
172
  if _tsgroup is not None:
117
- if cluster_ids is None:
118
- cluster_ids = np.array(list(_tsgroup.keys()))
173
+ if cluster_ids is not None:
174
+ # pynapple refuses a subset whose keys are not in its own (sorted) order.
175
+ _tsgroup = _tsgroup[np.sort(np.asarray(cluster_ids))]
176
+ cluster_ids = np.array(list(_tsgroup.keys()))
119
177
  if t_start is None:
120
178
  t_start = float(_tsgroup.time_support.start[0])
121
179
  if t_stop is None:
@@ -144,62 +202,6 @@ def firing_rate_by_cluster(
144
202
  return rates, bin_centers, cluster_ids
145
203
 
146
204
 
147
- def compute_pca(
148
- firing_rate: xr.DataArray,
149
- n_components: int = 3,
150
- zscore: bool = True,
151
- ) -> xr.DataArray:
152
- """Project population firing rates into PCA space via SVD.
153
-
154
- Parameters
155
- ----------
156
- firing_rate : xarray.DataArray
157
- Firing rates with dims ``("cluster_id", "time_fr")``.
158
- n_components : int
159
- Number of principal components to keep.
160
- zscore : bool
161
- Z-score each cluster's firing rate before PCA.
162
-
163
- Returns
164
- -------
165
- xarray.DataArray
166
- Scores with dims ``("time_fr", "pc")``, coords
167
- ``pc=["PC1", "PC2", ...]``, and ``attrs["explained_variance"]``.
168
- """
169
- X = firing_rate.values.T # (time, clusters)
170
-
171
- if zscore:
172
- mean = X.mean(axis=0)
173
- std = X.std(axis=0)
174
- std[std == 0] = 1.0
175
- X = (X - mean) / std
176
-
177
- U, S, _ = np.linalg.svd(X, full_matrices=False)
178
- scores = U[:, :n_components] * S[:n_components]
179
-
180
- total_var = (S**2).sum()
181
- explained = (S[:n_components] ** 2) / total_var
182
-
183
- pc_labels = [f"PC{i + 1}" for i in range(n_components)]
184
-
185
- return schema.describe(
186
- xr.DataArray(
187
- data=scores,
188
- dims=("time_fr", "pc"),
189
- coords={
190
- "time_fr": firing_rate.coords["time_fr"].values,
191
- "pc": pc_labels,
192
- },
193
- attrs={
194
- "explained_variance": explained.tolist(),
195
- "zscore": zscore,
196
- "n_clusters": firing_rate.sizes["cluster_id"],
197
- },
198
- ),
199
- schema.NEURAL_FEATURE,
200
- )
201
-
202
-
203
205
  def firing_rate_to_xarray(
204
206
  spike_times: np.ndarray,
205
207
  spike_clusters: np.ndarray,
@@ -255,6 +257,37 @@ def firing_rate_to_xarray(
255
257
  )
256
258
 
257
259
 
260
+ #: The instantaneous rate above which the birdsong literature calls spikes a burst:
261
+ #: "A burst was defined as the interval over which the instantaneous firing rate
262
+ #: exceeded a threshold of 125 Hz" — Leonardo A, Fee MS (2005). Ensemble coding of
263
+ #: vocal control in birdsong. J Neurosci 25(3):652-661,
264
+ #: https://doi.org/10.1523/JNEUROSCI.3036-04.2005. Other songbird studies use 100 Hz.
265
+ #: A starting point for a threshold the user sets, tuned to RA neurons — not a constant
266
+ #: of nature.
267
+ BURST_INSTANTANEOUS_RATE_HZ = 125.0
268
+
269
+
270
+ def instantaneous_rate(spike_times: np.ndarray, at: np.ndarray) -> np.ndarray:
271
+ """The instantaneous firing rate at each of *at*: ``1 / ISI`` of the inter-spike interval it falls in.
272
+
273
+ The birdsong field's definition — from each spike until the next, the
274
+ rate is the reciprocal of that interval — so the rate is a step function
275
+ with no bin and no smoothing, and a burst reads as high from its first
276
+ spike. Before the first spike and from the last one on there is no
277
+ interval to read, and the rate is 0. *spike_times* is sorted ascending;
278
+ spikes at the same instant count once.
279
+ """
280
+ spikes = np.unique(np.asarray(spike_times, dtype=np.float64))
281
+ at = np.asarray(at, dtype=np.float64)
282
+ rate = np.zeros(len(at), dtype=np.float64)
283
+ if len(spikes) < 2:
284
+ return rate
285
+ interval = np.searchsorted(spikes, at, side="right") - 1
286
+ inside = (interval >= 0) & (interval < len(spikes) - 1)
287
+ rate[inside] = 1.0 / np.diff(spikes)[interval[inside]]
288
+ return rate
289
+
290
+
258
291
  # ---------------------------------------------------------------------------
259
292
  # The segmentation pipeline's spike input: ``features.neural.transform``
260
293
  # ---------------------------------------------------------------------------