A benchmark for the oldest sanity check in EEG: posterior alpha power (8–12 Hz) should rise when the eyes close (Berger, 1929). Give it one eyes-open and one eyes-closed recording from any headset and it returns PASS, FAIL or INCONCLUSIVE with the numbers behind the verdict. On all 109 subjects of PhysioNet EEGMMIDB, eyes closed raises posterior power by +7.5 dB at 10.0 Hz, and 90 of 109 subjects pass.
git clone https://github.com/Tommycode3/eeg-alpha-bench && cd eeg-alpha-bench
python3 -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
# record ~2 min eyes open (fixating a point) and ~2 min eyes closed, relaxed, then:
alphabench --eo eyes_open.edf --ec eyes_closed.edf --line-freq 50 --report report.htmlPASS
- Alpha reactivity detected on Po7, Po3, Poz, Po4, Po8, O1, Oz, O2 (8/8 eligible channels).
report: report.html
The report is one self-contained HTML file; see examples/S004_report.html for PhysioNet subject S004. Readers exist for EDF/BDF/FIF/BrainVision/EEGLAB (via MNE), generic CSV (--format csv, a time column or --sfreq), Muse (muselsl record and Mind Monitor) and the OpenBCI GUI .txt. Other flags: --channels O1,O2, --line-freq 60, --json out.json, and --strict, which exits 1 unless PASS so you can put it in a hardware CI job. A new device needs one function path -> Recording (data in µV) registered in READERS in src/alphabench/io.py.
from alphabench import analyze, load, Config
res = analyze(load("eo.csv"), load("ec.csv"), Config(line_freq=50))
res.verdict, res.reasons, res.channels # str, list[str], pandas.DataFramepytest runs 40 tests in ~7 s against synthetic signals with known truth: bursting 10 Hz alpha must PASS with IAF within 0.3 Hz, while pure 1/f noise, EOG-only eyes-open, a 2× gain change and frontal-only alpha must never PASS.
109 subjects, 64-channel BCI2000 cap, 160 Hz, run R01 (1 min eyes open) vs R02 (1 min eyes closed), default config with line_freq=60, verdict channels PO7, PO3, POz, PO4, PO8, O1, Oz, O2 fixed in advance. No subject excluded.
| metric | value |
|---|---|
| PASS / INCONCLUSIVE / FAIL | 90 / 14 / 5 |
| pass rate (Wilson 95 % CI) | 82.6 % (74.4–88.5 %) |
| posterior alpha reactivity EC/EO, median [IQR] | 3.39 [1.95–10.58] |
| subjects with reactivity > 1 / ≥ 1.5 | 97.2 % / 85.3 % |
| Wilcoxon signed-rank, |
p = 8.9 × 10⁻²⁰ |
| IAF, peak, mean ± sd (n with a ≥ 1 dB peak) | 10.15 ± 0.86 Hz (n = 107) |
| IAF, centre of gravity, mean ± sd | 10.07 ± 0.72 Hz (n = 109) |
python validation/run_physionet.py # ~2.5 min on a laptop
python validation/make_figures.py # redraw figures from the committed CSVsPer-subject and per-channel CSVs are in validation/results/. Don't expect 100 %. The 19 non-PASS subjects mostly still have an eyes-closed alpha peak 5–6 dB above the 1/f background, but their alpha barely changes with eye state (median reactivity 0.93× for FAIL, 1.50× for INCONCLUSIVE). Test a device on someone you already know is a strong reactor. With 1 minute per condition (≈ 14 epochs) the Bonferroni-corrected test is the binding criterion: dropping it alone would raise the pass rate to 93.6 %, so recording 2–3 minutes per condition is the cheapest gain in sensitivity.
The verdict thresholds in config.py were written before any real data were seen. One thing changed after: the first trial run on subjects 1–4 used a fixed 200 µV rejection threshold, which threw away most eyes-closed epochs: their median peak-to-peak is 300–400 µV, largely alpha itself. It was replaced by the per-condition robust rule below, with no verdict threshold touched.
Each channel is detrended, notched at mains and harmonics (IIR,
Making the rule relative to each condition is what keeps strong eyes-closed alpha from being rejected for being large. The PSD is Welch's (2 s Hann segments, 50 % overlap,
A better electrode contact or a gain change between sessions scales every band by the same
A channel is eligible if it is not frontal and has at least 10 clean epochs per condition. PASS needs one eligible channel with
Relative alpha
If the 1–4 Hz band holds a fraction
So the verdict uses absolute alpha, requires relative_only and eog_suspect channels. A Muse (TP9, AF7, AF8, TP10) can pass only on TP9/TP10.
- Epochs from one recording are not strictly independent. The p-value is a single-session device check, not population inference.
- EO and EC are separate recordings, so anything else that changes between them (contact, posture, drowsiness) is confounded with eye state.
$S$ removes broadband changes, not band-specific ones. - Fixed 8–12 Hz band. Subjects with IAF near 8 or 12 Hz lose part of their alpha.
- No ocular correction (ICA or regression). Channels with unknown labels (e.g.
EXG0) can't be recognised as frontal; rename them to 10-20 labels. - Tested on one wet-gel research dataset. That says nothing about any consumer or dry-electrode device; run the tool on that device.
- Artifact rejection is amplitude-based. It catches pops and saturation, not subtle EMG.
Cite via CITATION.cff, and cite the PhysioNet dataset (Schalk et al., 2004; Goldberger et al., 2000).
- Barry, R. J., Clarke, A. R., Johnstone, S. J., Magee, C. A., & Rushby, J. A. (2007). EEG differences between eyes-closed and eyes-open resting conditions. Clinical Neurophysiology, 118(12), 2765–2773.
- Berger, H. (1929). Über das Elektrenkephalogramm des Menschen. Archiv für Psychiatrie und Nervenkrankheiten, 87, 527–570.
- Cliff, N. (1993). Dominance statistics: Ordinal analyses to answer ordinal questions. Psychological Bulletin, 114(3), 494–509.
- Corcoran, A. W., Alday, P. M., Schlesewsky, M., & Bornkessel-Schlesewsky, I. (2018). Toward a reliable, automated method of individual alpha frequency (IAF) quantification. Psychophysiology, 55(7), e13064.
- Donoghue, T., Haller, M., Peterson, E. J., et al. (2020). Parameterizing neural power spectra into periodic and aperiodic components. Nature Neuroscience, 23, 1655–1665.
- Goldberger, A. L., Amaral, L. A. N., Glass, L., et al. (2000). PhysioBank, PhysioToolkit, and PhysioNet: Components of a new research resource for complex physiologic signals. Circulation, 101(23), e215–e220.
- Gramfort, A., Luessi, M., Larson, E., et al. (2013). MEG and EEG data analysis with MNE-Python. Frontiers in Neuroscience, 7, 267.
- Iglewicz, B., & Hoaglin, D. C. (1993). How to Detect and Handle Outliers. ASQC Quality Press.
- Klimesch, W. (1999). EEG alpha and theta oscillations reflect cognitive and memory performance: a review and analysis. Brain Research Reviews, 29(2–3), 169–195.
- Mann, H. B., & Whitney, D. R. (1947). On a test of whether one of two random variables is stochastically larger than the other. Annals of Mathematical Statistics, 18(1), 50–60.
- Phipson, B., & Smyth, G. K. (2010). Permutation p-values should never be zero: calculating exact p-values when permutations are randomly drawn. Statistical Applications in Genetics and Molecular Biology, 9(1), Article 39.
- Schalk, G., McFarland, D. J., Hinterberger, T., Birbaumer, N., & Wolpaw, J. R. (2004). BCI2000: A general-purpose brain-computer interface (BCI) system. IEEE Transactions on Biomedical Engineering, 51(6), 1034–1043.
- Virtanen, P., Gommers, R., Oliphant, T. E., et al. (2020). SciPy 1.0: Fundamental algorithms for scientific computing in Python. Nature Methods, 17, 261–272.
- Welch, P. D. (1967). The use of fast Fourier transform for the estimation of power spectra: A method based on time averaging over short, modified periodograms. IEEE Transactions on Audio and Electroacoustics, 15(2), 70–73.
Code: MIT, © 2026 Tommaso Maggiolo. The PhysioNet EEGMMIDB data are not redistributed here; they are downloaded from PhysioNet under their own license (ODC Attribution). Committed results are derived summary statistics.

