feat(gui): a hardware raster in Session Review - #178
Merged
Merged
Conversation
Adds TimelineBar, the widget that replaces the single-lane ethogram bar with a stack of lanes sharing one time axis and one playhead: the ethogram becomes one lane, the hardware the rig drove becomes the lanes below it. The widget's internal axis is time, but selection_changed still emits frames so every stats table below it keeps working unmodified. Moves behavior_qcolor and behavior_order out of analysis_window.py into timeline_bar.py (analysis_window imports them back) to avoid a circular import once analysis_window pulls in TimelineBar in a later task.
_paint_behavior kept the ethogram bar's frame-linear column edges, which only agree with _x_of_ms when the ms bounds exactly match the frame map's range. The axis deliberately spans pre-flow device writes, so they usually do not: a session with 30s of device init ahead of the camera drew the ethogram across the whole bar while the camera occupied its last quarter, and the playhead sat on a band showing the wrong behaviour. Columns are now sliced in milliseconds and converted through the frame map; the frame-linear path stays for the ethogram-only case, where the axis really is frames. frame_bounds now falls back to the behaviour lanes' own frame range rather than to self._view. build_timeline(None, view) yields no frame map and no ms bounds, so set_timeline without a companion set_view reported (0, 0), collapsing every column onto frame 0 and flattening the ethogram to one colour. The selection overlay goes back to shading what is EXCLUDED, with the comment recording why: tinting the selection dragged every behaviour inside it toward the accent, and the usual selection is the whole session. Its right edge is _x_of(end + 1), which the accent version cut a column short. Both pixel tests now assert actual colours; each was verified to fail with its painter stubbed to a no-op.
Session Review loaded `ethogram_csv.parent` as the recording directory, which is off by exactly one level: an apply run writes the ethogram into `<output>/<video stem>/`, and the canonical layout buries it under `sessions/<id>/analysis/` with the CSVs a level above. Discovery found no `# GLIDER ...` markers in either shape, so no GLIDER path ever drew a hardware lane at all. Rather than a fourth path resolver, ask what the session loader already resolved: the video it found (via run.json where there is one) sits in the recording folder, and otherwise walk up the same three levels session_view searches. The first candidate that actually holds an artifact wins; none does, and the timeline is ethogram-only as before. Each folder is parsed once and cached, so stepping through a cohort no longer re-reads the same recording on the GUI thread per click, and build_timeline moved inside the guard — a KeyError from an events CSV missing a column was reaching the GUI. Three more the raster brought with it: - Behaviour lanes painted colour outside their own frames. The clamp for a column falling between two rows is right for an interior column and wrong past either end, where it handed over the first or last label: a 201-frame scored window in a 600-frame recording coloured the whole bar. Out-of-range columns now show the background. - The bout table's chips disagreed with the bar's stripes. The bar pools labels across every behaviour lane; the table re-derived an order from the ethogram alone, so with a tracking lane present a bout wore two different colours. The bar now exposes its pooled order and the table and the bout picker use it. - sizeHint ignored the gap _rows() advances by, so hardware rows were short two pixels per behaviour lane. The vertical policy is Fixed, so at seven behaviour lanes to one device the raster was given zero height and vanished silently. And FrameMap.frame_at documented itself as "nearest" when searchsorted makes it a ceiling. Docstring only; downstream depends on the ceiling.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Session Review has only ever opened a behaviour ethogram. Run an experiment that drives hardware and there was nowhere in GLIDER to open the result and see what the rig did.
The event log already held everything a raster needs — every pin edge and every commanded write, timestamped, with all recorders sharing one session epoch so
elapsed_msis joinable across files. Nothing had ever drawn it.This is P1 of three. The ethogram becomes one lane; the hardware becomes lanes below it; they share one time axis and one playhead.
What changed
glider/analysis/timeline.py(new, Qt-free) —FrameMap(frame ↔ flow-relative ms),Segment/Marker/Lanebuilt by zero-order hold,BehaviorLane,Timeline, and the builders.glider/gui/widgets/timeline_bar.py(new) — the widget. Also now ownsbehavior_qcolor/behavior_order, whichanalysis_windowre-exports.glider/gui/behavior/analysis_window.py—EthogramBardeleted,TimelineBarswapped in, timeline resolved and fed in_adopt.Tests: 5138 → 5168.
ruffandblackclean.Design decisions
The axis. The widget works internally in time, but
selection_changedstill emits frames — every table below it consumes a frame range (zone_rows,cohort_rows,segment_stats), so converting at the signal boundary leaves the whole panel untouched. Hardware events are timestamped in ms and theirframecell is empty for anything before the first camera frame, which is exactly where device-init writes live.The frame↔ms map resolves in order: the tracking CSV's own
frame/elapsed_mspairs (empirical, stays correct across dropped frames, which a nominal-fps calculation does not) →frame_rate× index → neither, in which case there is no ms axis.t=0 is flow start. The axis spans everything drawable, so pre-flow setup writes stay visible rather than being clipped — a device already in the wrong state before the run began is the most common question a hardware session raises.
Lanes. One per
device_id. Each event sets that device's value and it holds until the device's next event, so one renderer covers digital, PWM and servo. Bar height normalises against the pin type's full scale (DIGITAL 1, PWM 255, SERVO 180, ANALOG 1023), falling back to the observed maximum where a lane exceeds it — a 12-bit board reads ANALOG to 4095, and clipping at 1023 would draw the session as one saturated row.Flow markers are vertical rules across every lane, not a lane. Behaviour lanes come from both sources when both exist — the classifier ethogram and the tracking CSV's
behavioral_stateare different things at different quality, and one lane per tracked subject, since collapsing them silently showed only object 0.Reviewer notes — two known issues
Both are small and validated, left for this review rather than a further round:
_paint_behaviorfabricates behaviour outside the frame map's ms range. A 5 s pre-flow lead-in paints the first frame's label across ~200 px; a camera that stops before the flow tears down paints the last label across the final third. Same class as a clamp already fixed here, reached through the ms clamp instead. A 3-line fix (gating thehi <= lobranch on the ms range as well as the frame range) was prototyped and passed 579/579.Nonepoisons a directory for the window's lifetime — load before the recording exists, add it, and the same window still shows nothing.self._recordings.clear()inload()andload_many().Also noted, not blocking: the bout picker now lists tracking-only labels the ethogram lacks (stepping to one is a no-op);
_timeline_forfalls through to an ancestor recording ifbuild_timelineraises for the true folder, where it should stop;sizeHintgives the ethogram-only bar 48 px where the docstring promises 46.Not in P1
Multi-camera anything, session-folder unification (
sessions/<id>/), offset calibration, and sensor traces fromdata.csv. Those are P2 and P3.There are three independent file-resolution mechanisms in the tree today —
core.Session/core.Project(a finished four-phase subsystem no GUI code imports),SessionView, and_io.discover. P2 collapses them into one, which is what makes multi-cam grouping fall out for free rather than needing to be solved.