ethograph 0.2.45__tar.gz → 0.2.47__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.
- {ethograph-0.2.45 → ethograph-0.2.47}/.gitignore +1 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/CLAUDE.md +20 -5
- {ethograph-0.2.45 → ethograph-0.2.47}/PKG-INFO +6 -2
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/__init__.py +5 -1
- ethograph-0.2.47/ethograph/features/bursts.py +312 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/features/movement.py +6 -1
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/features/neural.py +94 -61
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/app_state.py +58 -25
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/cover_page.py +27 -31
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/dialog_collected_data.py +2 -0
- ethograph-0.2.47/ethograph/gui/dialog_feature_labels.py +821 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/dialog_label_gridview.py +82 -26
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/dialog_onset_model.py +12 -10
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/dialog_pose_labelling.py +75 -231
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/dialog_settings.py +1 -1
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/dialog_tag_sheet.py +3 -3
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/dialog_video_grid.py +18 -9
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/heatmap_sort.py +60 -5
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/label_drawing_mixin.py +25 -12
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/main_window.py +3 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/plots_base.py +11 -1
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/plots_console.py +11 -1
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/plots_container.py +9 -25
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/plots_heatmap.py +141 -35
- ethograph-0.2.47/ethograph/gui/plots_labelribbon.py +154 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/plots_lineplot.py +5 -1
- ethograph-0.2.47/ethograph/gui/plots_raster.py +357 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/plots_space.py +100 -16
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/pose_annotate.py +4 -19
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/pose_detect.py +4 -8
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/pose_fill.py +9 -241
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/pose_project_mode.py +2 -2
- ethograph-0.2.47/ethograph/gui/pose_second_view.py +683 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/pose_suggest.py +7 -62
- ethograph-0.2.47/ethograph/gui/pose_two_view.py +174 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/proxy_manager.py +19 -18
- ethograph-0.2.47/ethograph/gui/raster_render.py +192 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/right_context.py +19 -3
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/shortcuts.py +8 -2
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/source_popup.py +14 -2
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/theme.py +2 -2
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/top_bar.py +88 -10
- ethograph-0.2.47/ethograph/gui/triangulation.py +121 -0
- ethograph-0.2.47/ethograph/gui/unit_events.py +207 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/video_manager.py +7 -4
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/widgets_bottom_bar.py +27 -2
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/widgets_curation.py +83 -7
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/widgets_data.py +25 -63
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/widgets_ephys.py +692 -371
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/widgets_labels.py +72 -10
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/widgets_meta.py +63 -9
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/widgets_navigation.py +40 -13
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/widgets_plot_settings.py +51 -13
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/widgets_psth.py +24 -66
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/io/catalog.py +9 -11
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/io/data_loader.py +2 -2
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/io/metadata_table.py +20 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/io/validation.py +0 -27
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/io/video_proxy.py +22 -12
- ethograph-0.2.47/ethograph/labels/feature_events.py +506 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/labels/intervals.py +6 -1
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/labels/predictions.py +36 -1
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/spot/pose_batch.py +1 -3
- ethograph-0.2.47/ethograph/triangulate/__init__.py +30 -0
- ethograph-0.2.47/ethograph/triangulate/calibration.py +138 -0
- ethograph-0.2.47/ethograph/triangulate/frame.py +143 -0
- ethograph-0.2.47/ethograph/triangulate/geometry.py +109 -0
- ethograph-0.2.47/ethograph/triangulate/points.py +76 -0
- ethograph-0.2.47/ethograph/triangulate/session.py +386 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/utils/paths.py +1 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph.egg-info/PKG-INFO +6 -2
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph.egg-info/SOURCES.txt +20 -3
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph.egg-info/requires.txt +6 -1
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph.egg-info/scm_file_list.json +47 -13
- ethograph-0.2.47/ethograph.egg-info/scm_version.json +8 -0
- ethograph-0.2.47/notes/Lit_active-periods.md +209 -0
- ethograph-0.2.47/notes/Lit_unsupervised-proposals.md +291 -0
- ethograph-0.2.47/notes/adr/0015-periods-from-thresholds-and-burst-detectors.md +111 -0
- ethograph-0.2.47/notes/adr/0016-aniposelib-does-the-geometry.md +62 -0
- ethograph-0.2.47/notes/drafts/pynapple_burst_detection.py +513 -0
- ethograph-0.2.47/notes/drafts/pynapple_burst_r_to_python.html +271 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/projects/crowlab/mapping.txt +1 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/projects/crowlab/sessions.txt +2 -1
- {ethograph-0.2.45 → ethograph-0.2.47}/pyproject.toml +29 -15
- {ethograph-0.2.45 → ethograph-0.2.47}/uv.lock +643 -66
- ethograph-0.2.45/ethograph/gui/dialog_pose_refinement.py +0 -1177
- ethograph-0.2.45/ethograph/gui/plots_labelribbon.py +0 -82
- ethograph-0.2.45/ethograph/gui/plots_raster.py +0 -298
- ethograph-0.2.45/ethograph/gui/pose_refine.py +0 -709
- ethograph-0.2.45/ethograph.egg-info/scm_version.json +0 -8
- ethograph-0.2.45/test.ipynb +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/.claude/settings.local.json +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/.gitattributes +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/.github/copilot-instructions.md +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/.github/workflows/docs_build_and_deploy.yml +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/.github/workflows/test_and_deploy.yml +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/.readthedocs.yaml +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/LICENSE +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/MANIFEST.in +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/README.md +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/THIRD_PARTY_NOTICES.md +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/TODO.md +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/__main__.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/_vendor/__init__.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/_vendor/vocalseg/LICENSE +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/_vendor/vocalseg/NOTICE.md +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/_vendor/vocalseg/__init__.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/_vendor/vocalseg/continuity_filtering.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/_vendor/vocalseg/dynamic_thresholding.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/_vendor/vocalseg/utils.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/assets/icon.ico +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/assets/icon.png +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/assets/menu.json +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/assets/space/moll2025.yaml +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/cli.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/datasets.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/defaults/README.md +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/defaults/mapping.txt +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/defaults/octron/octron.yaml +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/defaults/segment/segment.yaml +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/defaults/spot/spot.yaml +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/features/__init__.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/features/audio_changepoints.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/features/changepoints.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/features/columns.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/features/energy.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/features/geometry.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/features/label_inputs.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/features/oscillatory.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/features/preprocessing.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/__init__.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/active_panel.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/app_constants.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/assets/templates/birdpark0.png +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/assets/templates/birdpark1.png +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/assets/templates/canary.png +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/assets/templates/cricket0.png +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/assets/templates/cricket1.png +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/assets/templates/cricket2.png +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/assets/templates/lockbox1.png +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/assets/templates/lockbox2.gif +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/assets/templates/moll1.png +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/assets/templates/moll2.png +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/assets/wizard/video_setup_free_running.png +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/assets/wizard/video_setup_triggered.png +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/audio_clock.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/audio_player.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/box_annotate.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/box_overlay.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/dialog_ambiguous_session.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/dialog_box_labelling.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/dialog_bulk_labels.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/dialog_busy_progress.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/dialog_curation_workflow.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/dialog_curator_feedback.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/dialog_fit.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/dialog_function_params.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/dialog_keypoint_filter.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/dialog_label_inconsistencies.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/dialog_label_table.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/dialog_pose_video_matcher.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/dialog_review_histogram.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/dialog_screen_recorder.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/dialog_select_template.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/dialog_session_folder.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/dialog_skeleton_editor.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/dialog_space_geometry.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/dialog_spot_crop.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/file_dialogs.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/grid_section_container.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/individual_colors.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/left_sidebar.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/make_pretty.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/napari.yaml +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/notify.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/plots_audiotrace.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/plots_ephystrace.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/plots_overlay.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/plots_psth.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/plots_radial.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/plots_skeleton.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/plots_spectrogram.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/pose_convert.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/pose_detect_preview.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/pose_edit_mixin.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/pose_overlay.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/pose_render.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/pose_static_group.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/pose_tagsheet.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/pose_training_export.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/project.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/pygfx_video.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/session_folder.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/space_shapes.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/table_filter.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/templates/notebook/discover.py.j2 +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/templates/notebook/open.md.j2 +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/templates/notebook/parameters.py.j2 +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/templates/notebook/timing_offset.py.j2 +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/templates/notebook/timing_onsets.py.j2 +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/templates/notebook/timing_pulses_free.py.j2 +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/templates/notebook/timing_pulses_triggered.py.j2 +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/templates/notebook/write_neuroconv.py.j2 +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/templates/notebook/write_pair.py.j2 +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/templates/wizard_nwb_codegen.j2 +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/video_sync.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/widget_trials.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/widgets_changepoints.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/widgets_frame_extract.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/widgets_help.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/widgets_io.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/widgets_transform.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/wizard_boris.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/wizard_media_files.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/wizard_multi_builder.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/wizard_multi_codegen.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/wizard_multi_tabs.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/wizard_multi_timeline.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/wizard_multi_trials.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/wizard_notebook.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/wizard_nwb.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/wizard_overview.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/wizard_pages.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/wizard_single.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/gui/wizard_state.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/io/audio_extract.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/io/dataset.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/io/derived.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/io/ephys_loader.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/io/external_features.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/io/image_sequence.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/io/metadata_edit.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/io/nc_drop.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/io/netcdf.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/io/nwb_alignment.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/io/nwb_import.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/io/overlay_source.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/io/pairing.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/io/plot_sources.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/io/pynapple.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/io/schema.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/io/session_layout.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/io/time_model.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/io/time_sources.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/io/trialtree.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/io/video_decode.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/io/video_feature_files.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/io/video_probe.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/labels/__init__.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/labels/boris.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/labels/check_labels.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/labels/confidence.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/labels/converters.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/labels/crowsetta_format.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/labels/curation.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/labels/export.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/labels/exporters/__init__.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/labels/exporters/crowlab.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/labels/label_inputs.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/labels/ml.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/labels/octron_project.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/labels/onset_curves.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/labels/onset_model.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/labels/plots.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/labels/pose_project.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/labels/rescore.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/labels/review_metrics.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/labels/tsv_store.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/labels/workflow.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/__init__.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/augment.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/config.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/crossval.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/dataset.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/dlc2action/LICENSE.APGPL +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/dlc2action/NOTICE.md +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/dlc2action/__init__.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/dlc2action/colors.txt +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/dlc2action/config/losses.yaml +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/dlc2action/config/model/asformer.yaml +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/dlc2action/config/model/c2f_tcn.yaml +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/dlc2action/config/model/c2f_transformer.yaml +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/dlc2action/config/model/edtcn.yaml +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/dlc2action/config/model/mlp.yaml +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/dlc2action/config/model/motionbert.yaml +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/dlc2action/config/model/ms_tcn3.yaml +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/dlc2action/config/model/transformer.yaml +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/dlc2action/config/training.yaml +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/dlc2action/loss/__init__.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/dlc2action/loss/asymmetric_loss.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/dlc2action/loss/ms_tcn.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/dlc2action/loss/mse.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/dlc2action/loss/tcc.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/dlc2action/model/__init__.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/dlc2action/model/asformer.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/dlc2action/model/base_model.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/dlc2action/model/c2f_tcn.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/dlc2action/model/c2f_tcn_par.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/dlc2action/model/c2f_transformer.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/dlc2action/model/c3d.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/dlc2action/model/edtcn.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/dlc2action/model/mlp.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/dlc2action/model/motionbert.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/dlc2action/model/motionbert_modules.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/dlc2action/model/ms_tcn.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/dlc2action/model/ms_tcn_modules.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/dlc2action/model/transformer.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/dlc2action/utils.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/dlc2action/version.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/feral.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/feral_defaults/NOTICE.md +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/feral_defaults/default_config.yaml +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/feral_defaults/presets.yaml +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/inference.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/joints.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/lady/LICENSE +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/lady/NOTICE.md +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/lady/__init__.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/lady/config/defaults.yaml +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/lady/generalized_coordinates.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/lady/loss_fn/ECloss.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/lady/loss_fn/__init__.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/lady/models/LaDy.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/lady/models/SP.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/lady/models/__init__.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/lady/models/dynamics.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/lady/models/graph/__init__.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/lady/models/graph/tools.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/lady/models/tcn.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/losses.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/materialise.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/metrics.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/models/__init__.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/models/config/rnn.yaml +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/models/rnn.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/models/skeleton_graph.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/models/vendored.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/plotting.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/postprocess.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/prediction_sets.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/preprocess.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/project.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/roles.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/samples.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/search.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/sessions.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/specscalpel/LICENSE +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/specscalpel/NOTICE.md +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/specscalpel/__init__.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/specscalpel/config/defaults.yaml +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/specscalpel/models/SP.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/specscalpel/models/SpecScalpel.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/specscalpel/models/__init__.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/specscalpel/models/graph/__init__.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/specscalpel/models/graph/tools.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/specscalpel/models/tcn.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/train.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/video_features.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/segment/windows.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/shortcuts.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/skeleton/CREDITS.md +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/skeleton/__init__.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/skeleton/config.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/skeleton/graph.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/skeleton/library.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/skeleton/renderers/__init__.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/skeleton/renderers/base.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/skeleton/renderers/precomputed.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/skeleton/shapes.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/skeleton/state.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/skeleton/templates.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/spot/__init__.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/spot/confidence.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/spot/config.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/spot/crossval.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/spot/dataset.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/spot/e2espot/LICENSE +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/spot/e2espot/NOTICE.md +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/spot/e2espot/__init__.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/spot/e2espot/dataset/__init__.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/spot/e2espot/dataset/frame.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/spot/e2espot/dataset/transform.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/spot/e2espot/model/__init__.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/spot/e2espot/model/common.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/spot/e2espot/model/impl/__init__.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/spot/e2espot/model/impl/asformer.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/spot/e2espot/model/impl/gsm.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/spot/e2espot/model/impl/tsm.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/spot/e2espot/model/modules.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/spot/e2espot/model/shift.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/spot/e2espot/test_e2e.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/spot/e2espot/train_e2e.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/spot/e2espot/util/__init__.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/spot/e2espot/util/dataset.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/spot/e2espot/util/eval.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/spot/e2espot/util/io.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/spot/e2espot/util/score.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/spot/features.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/spot/inference.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/spot/metrics.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/spot/msagsm.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/spot/prediction_sets.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/spot/project.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/spot/stream.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/spot/vendored.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/utils/__init__.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/utils/arraytools.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/utils/audio.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/utils/configkit.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/utils/device.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/utils/download.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/utils/ffmpeg.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/utils/logging.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/utils/nwb.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/utils/qt.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/utils/sequences.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/utils/stream_durations.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/utils/system_check.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/utils/xr_utils.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/video_features/__init__.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/video_features/base.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/video_features/checkpoint/S3D_kinetics400_torchified.pt +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/video_features/extract.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/video_features/frames.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/video_features/plan.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/video_features/s3d.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/video_features/select.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph/video_features/timm_extract.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph.egg-info/dependency_links.txt +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph.egg-info/entry_points.txt +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/ethograph.egg-info/top_level.txt +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/examples/README.md +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/examples/birdpark_dev/birdpark_pose.ipynb +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/examples/convert_dataset_birdpark.ipynb +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/examples/convert_dataset_pair24.ipynb +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/examples/convert_datset_lockbox.ipynb +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/examples/create_dataset_Moll25.ipynb +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/examples/create_dataset_cricket.ipynb +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/examples/segement_s3d_features.ipynb +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/examples/segment_changepoint_features.ipynb +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/notes/Bench_neural-decoding.md +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/notes/Bench_smoothalpha.md +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/notes/Lit_curator-feedback.md +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/notes/Lit_dlc2action.md +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/notes/Lit_feralbeast.md +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/notes/Lit_precise-event-spotting.md +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/notes/adr/0001-vendor-dlc2action-architectures.md +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/notes/adr/0002-literature-dataset-layout.md +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/notes/adr/0003-advisory-variable-schema.md +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/notes/adr/0004-scripted-not-cli.md +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/notes/adr/0005-search-then-cross-validate.md +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/notes/adr/0006-postprocess-from-gui-settings.md +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/notes/adr/0007-distil-inside-the-vendored-trainer.md +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/notes/adr/0008-features-not-graphs.md +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/notes/adr/0009-extractors-pip-installed-not-vendored.md +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/notes/adr/0009-spikes-as-a-config-transform.md +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/notes/adr/0010-multilabel-targets-are-tracks.md +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/notes/adr/0011-labels-as-inputs-across-branches.md +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/notes/adr/0012-feral-is-an-external-extractor.md +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/notes/adr/0013-no-learned-boundary-model-over-pose.md +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/notes/adr/0013-session-is-a-folder-media-are-absolute.md +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/notes/adr/0014-pose-project-as-two-sessions.md +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/notes/adr/0015-flat-project-folder-and-libraries.md +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/notes/feedback/audio_clock_dac_timestamps.md +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/notes/feedback/debug_launcher_clock_patch.md +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/notes/feedback/ethograph_annotation_feasibility_report.md +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/notes/feedback/ethograph_windows_author_overview_2026-08-18.md +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/notes/feedback/processed/ehtograph_feedback_report.md +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/notes/feedback/processed/handoff_playback_and_video_freeze.md +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/notes/feedback/reverted_pynapple_trials_timing.diff +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/projects/crowlab/ReadMe.md +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/projects/crowlab/config/space/moll2025.yaml +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/projects/crowlab/mapping_setup_audio.txt +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/projects/crowlab/old config/changepoint_settings_old.yaml +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/projects/crowlab/old config/mapping_Ivy.txt +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/projects/crowlab/old config/old segmentation config/Crow1_inference_20260330_161224.json +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/projects/crowlab/old config/old segmentation config/Crow1_inference_20260330_161259.json +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/projects/crowlab/old config/old segmentation config/Freddy_inference_20260330_160621.json +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/projects/crowlab/old config/old segmentation config/Freddy_train_20251021_164220.json +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/projects/crowlab/old config/old segmentation config/Freddy_train_20251021_164220_epoch-100.model +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/projects/crowlab/old config/old segmentation config/Ivy_CV_20260130_173800.json +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/projects/crowlab/old config/old segmentation config/Ivy_inference_20260203_092442.json +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/projects/crowlab/old config/old segmentation config/Ivy_inference_20260204_141527.json +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/projects/crowlab/old config/old segmentation config/Ivy_inference_20260206_122331.json +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/projects/crowlab/old config/old segmentation config/Ivy_train_20260202_191138_epoch-100.model +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/projects/crowlab/old config/old segmentation config/Ivy_train_20260203_153754.json +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/projects/crowlab/old config/old segmentation config/Poppy_inference_20260312_100707.json +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/projects/crowlab/old config/old segmentation config/Poppy_inference_20260327_102414.json +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/projects/crowlab/old config/old segmentation config/Poppy_inference_20260330_140808.json +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/projects/crowlab/old config/old segmentation config/Poppy_inference_20260402_104151.json +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/projects/crowlab/old config/old segmentation config/Poppy_train_20260326_190925.json +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/projects/crowlab/old config/old segmentation config/Poppy_train_20260331_175332.json +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/projects/crowlab/old config/old segmentation config/ReadMe - model Freddy_train_20251021_164220_epoch-100.txt +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/projects/crowlab/project.yaml +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/projects/crowlab/wizard/CrowBench.ipynb +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/projects/crowlab/wizard/SENTINEL.txt +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/projects/crowlab/workflows/01 PES Curation.yaml +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/projects/crowlab/workflows/Freddy_GBM.yaml +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/projects/crowlab/workflows/Ivy_GBM.yaml +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/projects/crowlab/workflows/Poppy_GBM.yaml +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/projects/neural/decoding.yaml +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/projects/paper/labels.yaml +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/projects/paper/spot/spot.yaml +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/scripts/Developer.md +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/scripts/_sweep.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/scripts/bench.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/scripts/bench_search.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/scripts/bench_videofeat.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/scripts/bench_watch.ps1 +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/scripts/compare.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/scripts/dandi/dandi_ibl-brainwide.ipynb.ipynb +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/scripts/dandi/nwb_video_testing.ipynb +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/scripts/describe_sessions.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/scripts/ephys/ephys_drift_diagnosis.ipynb +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/scripts/ephys/ephys_loader.ipynb +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/scripts/export_frames.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/scripts/export_neural_rate.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/scripts/feral/export_feral.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/scripts/feral/reinfer_feral.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/scripts/feral/run_feral.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/scripts/feral/score_feral.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/scripts/neuroconv.ipynb +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/scripts/paper1/README.md +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/scripts/paper1/changepoint_bars.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/scripts/paper1/crossval_bars.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/scripts/paper1/crossval_label_counts.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/scripts/paper1/embedding_grid.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/scripts/paper1/embedding_heatmaps.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/scripts/paper1/figure_combined.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/scripts/paper1/figure_neural.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/scripts/paper1/figure_review.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/scripts/paper1/figure_review_curves.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/scripts/paper1/figure_review_speed.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/scripts/paper1/s3d_ablation_bars.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/scripts/paper1/session.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/scripts/paper1/style/style ppt.mplstyle +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/scripts/paper1/style/style.mplstyle +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/scripts/release.ps1 +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/scripts/run.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/scripts/run_sleep.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/scripts/screenshot_gui.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/scripts/spot.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/scripts/test.ipynb +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/scripts/train_spot.py +0 -0
- {ethograph-0.2.45 → ethograph-0.2.47}/setup.cfg +0 -0
|
@@ -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,
|
|
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,
|
|
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
|
|
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 dataset's individual dim names the individuals first; the session record adds anyone it does not track, never hides one.
|
|
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/
|
|
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 `
|
|
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.
|
|
3
|
+
Version: 0.2.47
|
|
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-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
# ---------------------------------------------------------------------------
|