Skip to content

Epoch reviewer Phase 2: bad-channel flagging, auto-flag suggestions, condition labels#225

Open
jdpigeon wants to merge 4 commits into
epoch-reviewer-phase1from
epoch-reviewer-phase2
Open

Epoch reviewer Phase 2: bad-channel flagging, auto-flag suggestions, condition labels#225
jdpigeon wants to merge 4 commits into
epoch-reviewer-phase1from
epoch-reviewer-phase2

Conversation

@jdpigeon

@jdpigeon jdpigeon commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

What & why

Phase 2 of the Interactive Epoch Review UI (docs/epoch-review-ui-plan.md §0/§9) — full parity with (and beyond) MNE's cleaning GUI. Builds on Phase 1 (PR #224). Locked decisions: OQ4 (dev-gated auto-flag button + settings), OQ5 (bad channels enabled everywhere + 4-ch warning), New-A (auto-flag in Python), New-B (codeToLabel legends).

Three features

1. Bad-channel flagging (OQ5)
Click a channel label → toggles it bad (struck-through red label + red lane wash across all epochs). Bad channels ride the Phase-1 apply path: CleanEpochs({dropIndices, badChannels})apply_rejection sets info['bads']. A shadcn Dialog warns when you mark a 2nd bad channel on a 4-channel (Muse) dataset, nudging re-collection — informational, you can proceed. (8-ch+ devices don't warn.)

2. Auto-flag artifacts (OQ4 + New-A, Python/MNE)
A dev-gated "Auto-flag artifacts" button (visibility via AUTO_FLAG_EXPERIMENTS, a constant set keyed by the EXPERIMENTS enum — devs opt experiments in/out) with an expandable settings panel exposing the peak-to-peak threshold (µV).

click → GetSuggestedRejections(threshold) → suggest_rejections(epochs, threshold)  [Python, peak-to-peak]
   → 'suggestedRejections' dataKey (fire-and-forget, NO runPython RPC)
   → SetSuggestedRejections → componentDidUpdate PRE-MARKS suggested epochs (additive, overridable)

Suggestions are advisory — they pre-mark epochs (with reasons shown) but you can unclick any of them; the actual drop still goes through apply_rejection, so the saved .fif stays MNE-exact.

3. Condition legend labels (New-B)
EpochReviewer + LiveErpPane legends now show human-readable condition labels via buildMarkerRegistry(stimuli).codeToLabel (fallback "Condition {code}").

Changes

  • utils.pysuggest_rejections(epochs, threshold_uv) (peak-to-peak, Marker excluded, drops nothing), native-tested.
  • backboneSuggestedRejection type, Get/SetSuggestedRejections, reducer suggestedRejections (reset on new arrays + cleanup), getSuggestedRejectionsEpic + message branch, requestSuggestRejections, AUTO_FLAG_EXPERIMENTS + DEFAULT_PTP_THRESHOLD_UV.
  • UIEpochReviewer (clickable bad-channel labels + lane wash + codeToLabel legend), LiveErpPane (codeToLabel legend), CleanComponent (badChannels state, warning Dialog, auto-flag button/settings, suggestion merge), CleanContainer (passes params).

Out of scope

Phase 3 (onboarding/guided mode). apply_rejection reused from Phase 1, untouched.

Verification

  • npm run typecheck0 errors · eslint0 errors (2 benign warnings)
  • npx vitest run44/44 · native pytest tests/analysis/17/17 (3 new suggest_rejections)
  • npm run buildgreen
  • Independent adversarial review traced all three features end-to-end; verdict PASS, no CRITICAL/WARNING.

Known behaviors flagged in review (non-blocking, candidate follow-ups)

  1. get_epochs_arrays/suggest_rejections use pick_types(eeg=True) (MNE default exclude='bads') — after a Clean that flags a bad channel, the re-fetched reviewer omits that channel from the display (saved .fif unaffected). Defensible; confirm the UX you want.
  2. Re-running Auto-flag with the same threshold re-adds suggestions the user had manually unclicked (additive merge).
  3. Threshold input has no min guard (0 flags everything).

Stacked on epoch-reviewer-phase1 (PR #224). 🤖 Generated with Claude Code

Dano Morrison added 3 commits July 7, 2026 10:33
Auto-flag = dev-gated one-click button + expandable threshold settings;
suggestions computed in Python (peak-to-peak), pre-marked but overridable.
Bad-channel flagging enabled on all devices with a warning Dialog on the 2nd
bad channel of a 4-ch dataset. Condition legends use codeToLabel.
…bels

Full-parity phase (docs/epoch-review-ui-plan.md §0/§9).

Bad-channel flagging (OQ5):
- Click a channel label to toggle it bad (struck-through red label + red lane
  wash across all epochs). Flows into the Phase-1 apply path:
  CleanEpochs({dropIndices, badChannels}) → apply_rejection sets info['bads'].
- shadcn Dialog warns when marking a 2nd bad channel on a 4-channel (Muse)
  dataset — informational, user can proceed.

Auto-flag (OQ4 + New-A, Python/MNE):
- Dev-gated "Auto-flag artifacts" button (AUTO_FLAG_EXPERIMENTS set, keyed by
  the EXPERIMENTS enum) + expandable peak-to-peak threshold settings.
- suggest_rejections(epochs, threshold_uv) in utils.py (peak-to-peak per epoch,
  Marker excluded; advisory — drops nothing), native-tested. New
  'suggestedRejections' dataKey (fire-and-forget, no runPython RPC) →
  SetSuggestedRejections → CleanComponent PRE-MARKS suggested epochs (additive
  union, fully overridable) and shows reasons. Actual drop still via
  apply_rejection, so the saved .fif stays MNE-exact.

Condition legend (New-B):
- EpochReviewer + LiveErpPane legends use human-readable labels via
  buildMarkerRegistry(stimuli).codeToLabel (fallback to "Condition {code}").

typecheck 0 · eslint 0 errors · vitest 44/44 · native pytest 17/17 · build green
Phase 3 (onboarding layer, OQ3 open — route via office-hours/ceo-review) and
Phase 4 (N-channel/WebGL, a11y, keyboard, perf; OQ7 open) added under Deferred.
Three non-blocking Phase 2 review notes (bad-channel display exclude='bads',
additive auto-flag re-merge, threshold min guard) added under tech debt.

@jdpigeon jdpigeon left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Too many unnecessary comments. Add this repo's preference for avoiding comments outside of function, prop, or data structure definitions to learnings.md

Comment thread src/renderer/actions/pyodideActions.ts Outdated
// eslint-disable-next-line @typescript-eslint/no-explicit-any
ReceiveError: createAction<any, 'RECEIVE_ERROR'>('RECEIVE_ERROR'), // Worker error event — shape is dynamic
SetWorkerReady: createAction('SET_WORKER_READY'),
// Auto-flag: request suggestions (payload = peak-to-peak threshold in µV).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove this comment

// all epochs; amp +/- scale trace amplitude. Rendering stays Canvas 2D + a DOM
// overlay for labels/hit-targets (no canvas hit-testing).
//
// Interactive (Phase 2): channel labels in the left gutter are click-to-flag

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove these process-specific comments. Leaveo only a high level comment that concisely describes the intent and bejavoir of the component.


const REJECTED_TRACE_COLOR = 'rgba(120, 120, 120, 0.5)';
const REJECTED_FILL_COLOR = 'rgba(120, 120, 120, 0.15)';
// Translucent wash over a flagged bad channel's lane (drawn across all epochs).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove

const firstShown = clampedStart + 1;
const lastShown = clampedStart + visibleCount;

// Unique condition codes (sorted) for the legend — mirrors the canvas draw's

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove

return this.state.selectedSubject === subjectFromFilepath;
});

const codeToLabel = buildMarkerRegistry(

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This smells wrong. buildMarkerRegistry sounds like something that should be executed once rather then to get a label on every render

Comment thread src/renderer/constants/constants.ts Outdated

// Experiments where the "Auto-flag artifacts" button is offered on the Clean
// screen. Dev-configurable — add/remove experiments here to opt them in/out.
export const AUTO_FLAG_EXPERIMENTS = new Set<EXPERIMENTS>([

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't love this way of registering auto flag. WHy don't we do it the inverse way: add an optional hideAutoFlagEpochs field to an experiment definition data structure -- maybe experiment params? does that not already exist?

Comment thread src/renderer/epics/pyodideEpics.ts Outdated
action$.pipe(
filter(isActionOf(PyodideActions.GetSuggestedRejections)),
pluck('payload'),
// Fire-and-forget: result returns via pyodideMessageEpic →

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this comment. This knowledge should exist in the pyodide skill if it does not already

Comment thread src/renderer/utils/webworker/index.ts Outdated
});
};

// Auto-flag: ask Python which epochs look like artifacts (peak-to-peak >

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove

- Strip process-narrating / Phase-history comments (EpochReviewer top block,
  pyodideActions, pyodideEpics, webworker index, EpochReviewer color/legend);
  keep only definition-level intent comments.
- Invert auto-flag registration: replace the central AUTO_FLAG_EXPERIMENTS set
  with an opt-out hideAutoFlagEpochs field on ExperimentParameters (default on),
  colocated with the experiment definition.
- Memoize buildMarkerRegistry by stimuli reference so the code->label map isn't
  rebuilt on every CleanComponent render.
- Record the repo's comment-style preference in .llms/learnings.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019uiGzUcGN7j6qDWrfH5MCr
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