Skip to content

Epoch reviewer Phase 1: interactive rejection + live ERP + apply/save#224

Open
jdpigeon wants to merge 1 commit into
epoch-reviewer-phase0from
epoch-reviewer-phase1
Open

Epoch reviewer Phase 1: interactive rejection + live ERP + apply/save#224
jdpigeon wants to merge 1 commit into
epoch-reviewer-phase0from
epoch-reviewer-phase1

Conversation

@jdpigeon

@jdpigeon jdpigeon commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

What & why

Phase 1 of the Interactive Epoch Review UI (docs/epoch-review-ui-plan.md) — this is the phase that makes it a fully working, interactive epoch cleaner and delivers the onboarding payoff: click bad epochs and watch the ERP clean up live.

Builds on Phase 0 (static reviewer, PR #223) and the write-back bridge (PR #222). Locked decisions implemented: OQ1 = Canvas 2D, OQ2 = extend dataKey / no RPC, OQ6 = live ERP computed client-side.

What you can now do (QA flow)

  1. Clean screen → Load Dataset → epoch traces render (Phase 0).
  2. Click an epoch column → it greys out (marked for rejection); click again to restore. Prev/Next to page through all epochs; amplitude +/- to scale.
  3. Live ERP pane re-averages the non-rejected epochs per condition instantly on every click — the waveform visibly cleans up. Channel selectable via dropdown.
  4. Clean Data → drops the marked epochs in Python (bit-identical to MNE), saves -cleaned-epo.fif to disk (via the PR Fix broken Clean→Analyze round-trip: persist cleaned epochs to host disk #222 bridge), refreshes stats.
  5. Analyze → the cleaned file is picked up; ERP/topo/PSD reflect the cleaned epochs.

How it works

click epoch → onToggleEpoch(absoluteIndex) → CleanComponent.rejectedEpochs (Set)
   → LiveErpPane: meanTrace() over NON-rejected epochs, per condition, redraw   ← client-side, 0 worker calls
Clean Data → CleanEpochs({dropIndices, badChannels:[]})
   → cleanEpochsEpic: apply_rejection(raw_epochs, [...], []);  ← trailing ';' suppresses the Epochs PyProxy
   → saveEpochs (write-back, PR #222) → requestEpochArrays + GetEpochsInfo (FIFO)

Changes

  • utils.pyapply_rejection(epochs, drop_indices, bad_channels) (epochs.drop + info['bads']), native-tested bit-identical to a manual MNE drop/bads.
  • epochArrays.ts — pure meanTrace(buffer, meta, epochIndices, channel) ERP primitive (unit-tested vs manual mean, empty → zeros).
  • EpochReviewer.tsx — now interactive/controlled: click-to-reject (grey wash + ✕), offset-aware navigation, amplitude gain (clamped, no lane bleed).
  • LiveErpPane.tsx (new) — per-condition live ERP over non-rejected epochs; redraws on every toggle; NaN-guarded empty state.
  • worker/action/epicapplyRejection() (trailing ;), CleanEpochs({dropIndices, badChannels}), cleanEpochsEpic rewritten to apply → save → refresh.

Out of scope (later phases)

Bad-channel flagging UI (Phase 2 — apply_rejection takes the param, UI wires []), auto-flag from drop_log (Phase 2), onboarding/guided mode (Phase 3).

Verification

  • npm run typecheck0 errors
  • eslint0 errors (2 benign warnings, incl. one pre-existing sidebar warning)
  • npx vitest run44/44 (new: meanTrace, EpochReviewer overlay-click → onToggleEpoch)
  • native pytest tests/analysis/14/14 (4 new apply_rejection bit-identical/drop-count/no-op/identity)
  • npm run buildgreen
  • Independent adversarial review traced the interaction→apply path (correct absolute-index mapping across navigation — no wrong-epoch-drop bug), confirmed zero-worker live ERP, bit-identical apply, index-shift safety, and no scope drift. Verdict: PASS.

Stacked on epoch-reviewer-phase0 (PR #223). 🤖 Generated with Claude Code

Second half of the epoch-review UI (docs/epoch-review-ui-plan.md §0/§9) —
turns the static Phase 0 viewer into a working, teach-as-you-go cleaner.

Interaction (EpochReviewer, Canvas 2D + DOM overlay, OQ1):
- Click an epoch column to toggle it rejected (greyed trace + wash + ✕).
- Prev/Next navigation through epoch windows; column→ABSOLUTE index math is
  offset-aware so the right epochs are dropped after paging.
- Amplitude gain control (clamped, traces can't bleed across lanes).

Live ERP preview (OQ6, client-side — the teaching payoff):
- LiveErpPane averages NON-rejected epochs per condition via the pure
  meanTrace() over the Phase-0 buffer and redraws on every toggle. ZERO worker
  round-trips (OQ2=2A, no runPython RPC); empty selection → flat line, no NaN.

Apply at "Clean Data":
- apply_rejection(epochs, drop_indices, bad_channels) in utils.py (epochs.drop
  + info['bads']), native-tested BIT-IDENTICAL to a manual MNE drop/bads.
- CleanEpochs carries {dropIndices, badChannels}; cleanEpochsEpic posts
  apply_rejection (trailing ';' suppresses the Epochs PyProxy) → saves via the
  existing write-back bridge (PR #222) → re-fetches arrays + stats. Local
  selection clears after commit (fresh 0-based indices next round).

Bad-channel UI is Phase 2 — apply_rejection takes the param but the UI wires
badChannels: [] for now.

typecheck 0 · eslint 0 errors · vitest 44/44 · native pytest 14/14 · build green
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant