P2 client w-burst-detector

Burst Detector

Threshold an alpha amplitude envelope on 60 seconds of real eyes-closed EEG and watch "sustained alpha" resolve into a handful of events — with the burst count moving elevenfold on the threshold alone.

5 claims on this page are unverified. TODO(confirm) marks a specific statement the author has not yet checked against a primary source. Everything else on this page has been reviewed. Treat a marked claim as provisional and go to the cited source rather than quoting the sentence.

Modes: default — the widget below runs in default. Use Share state to put the exact view in the URL.

Burst Detector

mode: default
Loading Burst Detector…

Data: ds-lemon , subject sub-010002, run RSEEG (raw release), 378.027–438.027 s · license CC-BY-4.0 · DOI 10.1038/sdata.2018.308 · labels: algorithmic · a cropped, re-referenced or filtered derivative of the source recording.

What it does

Sixty seconds of real eyes-closed EEG, one occipital channel. The widget band-passes it, forms the analytic envelope, thresholds the envelope, and reports the burst statistics — count, rate, mean and median duration, duty cycle, mean in-burst amplitude, onset-to-onset interval — recomputed live as the band, the threshold and the minimum duration move.

Despite its file name, the shipped asset holds the time series, not an envelope: the band and the threshold are live, so the widget runs filter-Hilbert itself. A sidecar claiming to carry a precomputed envelope is refused with a message rather than treated as a signal.

A sweep panel repeats the statistics at 19 threshold percentiles from 50 to 95, with the band, the minimum duration and the signal held fixed, so the sensitivity to the one free parameter is on screen next to the single number.

Controls

ControlWhat it sets
Band presetdelta 1–4, theta 4–8, alpha 8–13 (the default), beta 13–30, low gamma 30–45, and a narrow 9.5–10.5 Hz preset
Lower / upper edgeThe band directly; ordered automatically and forced at least 0.5 Hz wide
Threshold unitsPercentile of the envelope · multiple of its median · absolute µV
ThresholdWhichever the current unit uses; the value in µV is always shown
Minimum duration unitsCycles of the band centre, or milliseconds
Minimum durationClamped against the unit in force
Window / startThe visible span only — the statistics always use the whole analysed span
Show the unfiltered traceThe raw trace on its own auto-scaled axis
Exclude the filter’s edgesOn by default
Show the threshold sweepThe three sensitivity charts

What it computes

  1. Band-pass — windowed-sinc FIR (firwin, Hamming) applied forward and backward, so the filter is zero-phase and burst onsets are not shifted. Transition bands follow MNE-Python’s band-pass defaults and the tap count follows its “auto” rule applied to the narrower of the two edges, rounded up to odd: 413 taps for 8–13 Hz at 250 Hz.
  2. Envelope — the magnitude of the analytic signal, with scipy.signal.hilbert semantics.
  3. Edges — a zero-phase FIR of N taps contaminates (N−1)/2 samples at each end; with the default the statistics use the interior only and the excluded span is shaded (pf-tf-edge-effects).
  4. Threshold — a percentile of the envelope (numpy’s linear interpolation), a multiple of its median, or an absolute value, measured over the analysed span rather than the whole record.
  5. Detection — runs of consecutive samples strictly above the threshold, with runs shorter than the minimum duration rejected and counted separately. A run must cover the requested minimum, so the sample count is ceil(minimum × fs).
  6. Statistics and sweep — as listed above, and the same statistics at 19 thresholds.

What the widget does not claim. A burst is the output of a decision rule with three free parameters. The widget shows the rule’s output and how far it moves; it does not assert that any of these events is physiological. A single-threshold rule is the simplest one; dual-threshold rules and cycle-by-cycle waveform methods are different rules with different answers, and they live in the notebook.

What to look for

  • Put the unfiltered trace beside the 8–13 Hz band-passed one. The band-passed trace looks like a continuous rhythm; the envelope above it does not — it rises and falls. “Sustained alpha” is a description of the filtered trace, not of the envelope.
  • Drag the threshold and watch every number in the table change while the signal underneath does not. On this segment, at the 50th / 75th / 95th percentile of the envelope with a 3-cycle minimum and the edges excluded: 33 / 19 / 3 bursts and duty cycles of 40.3 / 17.8 / 2.5 %. Nothing about the data changed; only the decision rule did.
  • Read the sweep panel — burst rate and duty cycle against threshold percentile, band and minimum duration held fixed. If a result you would report moves by a factor of several across a defensible range of that one free parameter, the threshold is doing the reporting.
  • Set the minimum duration to zero, then to several cycles. Short crossings are noise excursions as often as they are rhythm; requiring a few cycles is the usual defence, and it is also a choice you have to state.
  • Narrow the band to the 9.5–10.5 Hz preset. The filtered trace looks more rhythmic and the envelope smoother — a narrow filter makes almost anything look like an oscillation (pf-narrowband-filter-oscillation) — and the filter-length read-out shows what it costs: a longer filter and a wider contaminated edge.
  • The shaded strip at each end is the filter’s own reach. The zero-phase FIR mixes samples from beyond the record there, so bursts inside it are not trustworthy and the statistics exclude them by default.
  • Before quoting band power at all, check that there is a peak in the band. w-aperiodic-explorer separates a real peak from a change in the aperiodic slope, and a band with no peak has no rhythm to burst. For this channel there is one: a specparam fit of this exact segment puts it at 10.272 Hz.

One convention difference, documented rather than tuned away

The data script’s reference table used a minimum duration of 0.284 s — exactly 71 samples at 250 Hz. The widget’s default is “3 cycles of the band centre”, which for the 8–13 Hz band is 3 / 10.5 Hz = 285.7 ms, and it requires a run to cover that, so ceil(71.43) = 72 samples. At the 1.5 × median threshold that single sample gives 19 bursts where the reference gives 20. Neither is wrong: a one-sample change in the rule moves the count by one, which is exactly what this widget exists to show. Set the minimum duration to 284 ms in the millisecond unit and the widget returns 20. L4.6 quotes the widget’s numbers, because that is what the learner sees, and its tolerance admits the one-count difference.

Used in

  • L4.6 Is it really an oscillation? (default)

Data provenance

ds-lemon — LEMON, MPI-Leipzig Mind-Brain-Body, raw EEG release (Babayan et al., 2019), DOI 10.1038/sdata.2018.308. Acquisition band 0.015–1000 Hz with the hardware notch off; 50 Hz mains; online reference FCz.

The shipped asset is subject sub-010002, channel O2, 60.000 s of eyes-closed rest from t = 378.027 s, resampled 2500 → 250 Hz with an anti-alias low-pass and the per-channel DC offset removed. No band-pass, high-pass or notch — the widget filters it itself. Generated by data/scripts/make_burst_data.py; envelope.json is both the widget index and the §4.5 sidecar of envelope.bin. The .eeg source is 317 MB and was never fetched whole: every read was an HTTP range request for exactly the samples needed.

Three selections, all algorithmic and all recorded. The eyes-closed block was identified from the recording’s own marker codes by which code carries the higher median relative occipital alpha (S210 at 0.968 against S200 at −0.370, in log relative alpha); the block was chosen from the first four eyes-closed blocks with 60 s of room, as the one with the loudest relative alpha among those whose slow drift is tolerable; and the channel was chosen as the occipital site with the largest 8–13 Hz share of 1–40 Hz power — O2 at 3.10, ahead of Oz at 2.93 and O1 at 2.87. label_source: algorithmic throughout. TODO(confirm): what each marker code means is not recorded in the site’s dataset directory, so the eyes-closed identification is inferred from the alpha itself rather than read from the protocol.

What the drawing has to survive. LEMON’s raw release has no online high-pass, so this trace carries 162.8 µV peak to peak, of which 97.2 µV is slow drift (peak-to-peak of a 1-second moving mean) against an alpha standard deviation of 6.5 µV. The widget draws the raw and band-passed traces on separate auto-scaled axes so the drift can never flatten the rhythm being taught, and prints both figures under the raw panel. Nothing is filtered to hide it, and the measurement for every candidate block is in the sidecar.

There is a peak to burst. A specparam fit of this exact 60 s (Welch, 2-s Hann segments, 50 % overlap, fitted over 1–40 Hz with max_n_peaks 6, peak_width_limits [1, 8], min_peak_height 0.1) gives an aperiodic exponent of 1.114 with r² = 0.975 and peaks at 10.272 Hz (power 1.27, bandwidth 2.41 Hz), 18.9 Hz and 25.4 Hz. The sidecar’s individual_alpha.band_hz of 8.27–12.27 Hz is the peak ± 2 Hz — a convention, not a measurement — and the widget does not adopt it; a lesson that wants it passes it in initialState.

The sidecar also carries a reference envelope (median 6.841 µV; p25 4.005, p75 10.373, p95 16.801) and a reference burst table by median multiple, all computed in Python. The widget’s integration test reproduces every digit of them from the shipped .bin, so the browser’s filter-Hilbert and the Python that made the asset agree.

TODO(confirm): the licence is CC BY 4.0 per the data descriptor, but the exact dataset terms are unresolved (§13 item 5), and the sidecar carries that note verbatim. TODO(confirm): the asset is stored at 250 Hz rather than the native 2500 Hz — Nyquist is 125 Hz, above every band the widget offers, and §4.5 names 250 Hz as the default target rate, but whether the full rate is wanted here is an open question; the asset would then be about 600 kB.

Open the code

site/src/components/widgets/w-burst-detector/Widget.svelte (presentation only), bursts.ts (filter design, band-pass, threshold rules, detection, statistics, sweep), hilbert.ts, state.ts, data.ts, synthetic.ts, plots.ts, and 47 tests including an integration test against the real asset. Repository link: TODO(confirm) (GitHub org/repo, §13 item 3).

TODO(confirm): screenshot.png is a labelled placeholder generated with matplotlib, not a capture of the running widget.