Resting state (eyes-open / eyes-closed) — operator SOP + how it works¶
A ~6-minute, tone-cued eyes-open / eyes-closed recording with no flicker at all. It is the odd duck of the toolbox — every other protocol is a flickering-target design — but it runs through exactly the same path as any other protocol, and that is the point of how it's built.
⚠️ REB. Resting-state recording and auditory cueing are not in the approved protocol (V6 approves visual high-frequency VEP only). Amendment A10 covers it — see
docs/REB_AMENDMENTS.md. No collection until it's approved in writing.
1. Why bother — it has no flicker¶
Two jobs, both about other recordings:
- PAF calibration. The participant's individual peak alpha frequency. Alpha (~8–13 Hz) and its 2nd harmonic sit right in the low-frequency SSVEP band; a design that puts flicker on a participant's own PAF is measuring their alpha rhythm as much as a driven response. Knowing PAF lets a design steer around it.
- Negative control + posterior QC. There is nothing to decode in a resting run, so a well-behaved SSVEP decoder must sit at chance on it. And because eyes-closed alpha reliably exceeds eyes-open (alpha reactivity), the eyes-closed/eyes-open alpha ratio is a direct test of whether the occipital electrodes are reading cortex at all — a ratio near 1 condemns the montage before an SSVEP session is spent on it. (sub-903/904: dry systems decoded at chance in the high band and the posterior contact failure was only found in analysis, afterwards.)
2. Operator SOP¶
Identical to any other protocol — that's deliberate:
- Build run tab — Preset → "Resting state (eyes open/closed, PAF — no flicker)" → Load
preset. The target table is replaced by the resting block parameters; the preview pane shows the
fixation cross. Build run has no hardware fields (#147) — a resting run is saved with no
acquisitionblock, exactly like a flicker run. - Hit Preview resting to see the cross and hear both cue tones (2 short blocks, records nothing). Do this once per rig — it is the cheapest way to catch dead audio.
- Save… the protocol (Run mode wants a saved, versioned protocol).
- Build Protocol tab — add the saved resting run to a protocol, then Set up acquisition hardware to set the amplifier / sampling rate / headset for the whole protocol (#147) — it applies to a resting run exactly as it does to a flicker protocol. Save the protocol.
- Set up Session tab — subject / session / run / operator / consent; Load protocol…, Resolve / refresh and the as-run hardware fields (correctable at the bench) all live here, next to 🔒 Lock protocol and session details (#147 — #126 had put load/lock on Run Session; #147 moved them back beside the fields the lock freezes).
- Run Session tab — the checklist is already there once locked. Expand the collapsible QA check pane and run the impedance check as usual; optionally click Preview EEG on the live-EEG pane below it to check signal quality first, then Start selected run → Start recording (auto-starts the preview too if you skipped it). If the machine can't play the cue tones you get a warning before anything starts; you can cue verbally instead, but fix the audio.
Tell the participant: "You'll hear a high beep, or the screen will flash white — open your eyes and look at the cross. A low beep, and the cross disappears — close your eyes and rest. Don't fall asleep."
The eyes-open cue is a white screen flash as well as a tone (2026-07-15). The lab PC has no
speakers, and an eyes-closed participant cannot see the fixation cross reappear — so on a mute rig
they had no cue to open at all, for the whole run. A bright full-screen flash is visible through
closed eyelids, so it works with or without audio. The tones stay for rigs that have sound; the
audio warning above still fires, because a tone is the better cue when it is available.
Configurable via design.resting.eyes_open_flash_ms (400 ms default; 0 disables).
What you're watching during the run¶
The Run Session tab's decode panel is replaced by live alpha reactivity (there are no frequencies to decode). You should see the eyes-closed blocks push the ratio above ~1.5×. If it sits near 1.0 the occipital electrodes are not reading cortex — stop and fix contact rather than finishing a run that can't answer anything.
3. Reading the report¶
BIDS/code/02_run_offline_pipeline.py routes task-rest runs to the PAF pipeline automatically
(no flag needed) and writes the usual *_report.html + *_metrics.json to
derivatives/ssvep-analysis/sub-XXX/ses-YYY/, linked from the derivatives index (docs/BIDS.md).
| Field | Meaning |
|---|---|
peak_alpha_hz |
PAF — the eyes-closed occipital spectral peak. Steer SSVEP flicker away from this and its 2nd harmonic. |
cog_alpha_hz |
Power-weighted mean frequency in band; steadier than the raw peak when alpha is broad/bimodal. |
ec_eo_alpha_ratio |
The QC. ≥1.5 healthy · 1.15–1.5 marginal · ~1.0 the montage isn't reading cortex. |
occipital_channels |
What the estimate averaged over. A montage with no 10-20 names (the colour-coded occipital array) falls back to all channels — already occipital there, but check. |
4. How it works (and why it's a manifest paradigm)¶
Resting is expressed as a manifest paradigm, not a special-case dialog:
paradigm: "resting"(schema v1.2; absent ⇒"ssvep", so every older manifest still reads).stimuli: []— literally no flicker. That absence is the design.design.resting— block length, count, start state, cue-tone frequencies, eyes-open flash, lead-in, settle, fixation cross, alpha band. The authoritative record of the structure.run.task: "rest"→ BIDStask-rest, cleanly separate fromtask-ssvep.
Everything else falls out of that: the runtime picks ssvep.stim.resting_renderer instead of
ssvep.stim.renderer (orchestrator.renderer_command) and the rest of the chain — EEG bridge, LSL
recorder, XDF, session sidecar, consent gating, provenance, impedance snapshots — is unchanged.
Analysis dispatches on the same field. Eye-state is read from each marker's condition field, never
inferred from the signal, so the alpha contrast is an honest test rather than a circular one.
History: why it's built this way (2026-07-15)¶
The first version put a "Resting-state…" button on the Design tab that ran the paradigm from a dialog. Three things were wrong with it, and they're worth remembering because they were all the same mistake:
- It saved nothing. It launched a bare renderer subprocess — no EEG bridge, no recorder, no XDF writer. The "Stream LSL block markers" checkbox opened a marker outlet that nothing subscribed to. A whole sub-904 run was presented and no data was ever written (nor could it have been).
- It bypassed everything. No subject, no consent, no impedance check, no provenance, no lock — and it ignored the Design tab's own device/montage/rate selections.
- It was silent.
tones.play_toneimportedAudioFormatfrompyglet.media, which pyglet 2.1 moved topyglet.media.codecs; a bareexceptswallowed theImportErrorand every cue tone no-oped on every machine. The test only assertedcallable(play_tone), so it passed throughout.
The root cause of the first two is that resting wasn't a protocol — it was a dialog that
side-stepped the architecture. Making it a manifest paradigm fixes them by construction: there is no
resting-specific run path left to forget to wire a recorder into. Design previews still render (that
has always been fine — Preview session does it too); what Design never does is collect.
5. Pointers¶
src/ssvep/stim/resting.py— protocol + frame-indexed schedule + markers (pure).src/ssvep/stim/resting_renderer.py— the pyglet shell (manifest-driven).src/ssvep/stim/tones.py— cue-tone synthesis + guarded playback +probe_audio().src/ssvep/analysis/resting_paf.py— PAF + alpha reactivity (array API: offline == online).src/ssvep/runtime/online.py—LiveAlphaMonitor, the live QC.docs/REB_AMENDMENTS.md— A10, required before collection.
Lesson from the first real resting run (sub-902/ses-002 run-008, 2026-07-15)¶
It produced a textbook result — PAF 9.5 Hz, eyes-closed/eyes-open ratio 4.89x — on half a cap. The protocol declared the 64-ch actiCAP while the 32-ch posterior cap was physically on the head, so the bridge faithfully streamed 64 channels of which 32-63 were floating, including 5 of the 11 occipital channels the PAF averaged over (PO7/PO3/POz/PO4/PO8).
The number survived only because a constant channel contributes no variance to a band-power average. That is luck, not correctness — rerun it before trusting it.
Three things to take from this:
- The montage is baked into the protocol at design time. The resting preset ships with the 64-ch
actiCAP; if a different cap is on the head, the protocol is wrong, not the code.
--n-eegin the bridge does exactly what the manifest says. - Run mode now blocks this (
signal_quality.check_montage+ therecord_runpre-roll gate), with an explicit override that is stamped into provenance. - A disconnected actiCHamp channel sits at ~+410 mV DC, not at zero — and
np.stdon such a channel in float32 returns plausible garbage. Detect by DC offset, in float64. Seedocs/HANDOFF_2026-07-15_ses-002-review.md§3.3.