Skip to content

Spec: Biosignal Validation Protocol — Measuring Phantom Activations and Hit Rate on Real Hardware

Field Value
ID spec-biosignal-validation-protocol
Status Proposed
Date 2026-10-30
Modules yazses.biosignal.fieldtest (guided in-situ session and FieldTestAggregate), yazses.biosignal.evaluate (interval maths shared with the offline harness), yazses.biosignal.fprate (the live ledger)
Related ADR-v2-152 ("Falsifiable acceptance criteria", "Kill criteria"), ADR-v2-153, design/specs/biosignal-pipeline.md, ADR-v2-150 (research consent), ADR-011, ADR-019
Research docs/research/muscle-brain-control.md (open question 1: measured false-activation rates)
User page docs/how-to/biosignal-switch.md ("Measure your own rate")

Goal

Every number in yazses.biosignal (detector thresholds, quality-gate limits, the budget, the governor schedule) is a design choice validated on synthetic signal only (ADR-v2-152, "Evidence and what it does not show"). This protocol is how that changes. It specifies how a person with real hardware measures, on their own machine, how often the switch fires when they did not mean it and how often it fires when they did, how those measurements are combined across people and boards without moving any raw signal, and how much evidence is needed before anyone may say anything about them.

It produces upper bounds and lower bounds with their method stated. It does not produce an "accuracy".

Non-goals

  • not a clinical study, a medical evaluation, or a claim about any person's health or condition; YazSes is not a medical device, and nothing here is reviewed by an ethics board;
  • not a way to collect raw signal, recordings, calibration profiles or thresholds (those never leave the machine; see "Reporting back");
  • not a tuning loop: the protocol measures a frozen configuration, it does not search for one;
  • not a replacement for yazses biosignal evaluate on a labelled recording, which scores the detectors offline; this protocol measures the whole chain, live, in the activities where mistakes actually happen;
  • not an identity, emotion, fatigue or health measurement of any kind.

Definitions

These are used identically in the field-test output, in the ADR gates and in the user pages. They follow yazses.biosignal.fieldtest, which is the authority for how each number is computed.

Term Definition
Activation An event the runner would act on: anything except SQUEEZE_OFF, which closes a hold rather than starting one (the ledger's rule, ADR-v2-153 section 1).
Trigger kinds The activations the field test counts as phantoms. A single BLINK is not counted by default: a natural blink is unmapped and is not a commit signal (spec, event semantics). The per-kind counts, single blinks included, are still listed in the report.
Usable exposure Signal time in which the pipeline reported usable quality and was not suspended, minus the first few seconds of each phase (the design default is 3 s, because reading the next prompt is itself a head movement). Time the quality gate suppressed is excluded from the denominator, so a bad signal cannot flatter the rate by looking like silence.
Phase A prompted activity with a fixed purpose (table below). Every second of exposure is labelled with its phase. A phase with less than the minimum usable signal (design default 30 s) is inconclusive and is left out of every pooled number.
Phantom activation A trigger-kind activation during a phase in which the participant is not trying to trigger anything. Ground truth is the prompt, not the participant's memory, so the cancel-signal proxy of the live ledger is not involved.
Cue / hit / miss / void In a gesture phase the tool schedules cues. A cue is a hit when an expected event kind is accepted inside the cue's response window (design default 3 s), a miss otherwise, and voided (neither) when its window overlaps unusable signal. Activations outside any window in that phase are reported as extra.
Phantom rate Phantom activations per usable hour, per phase, with a one-sided 95% Poisson upper bound (a cube-root approximation, the same as evaluate_idle; about 3 / hours when none was seen).
Hit rate Hits over scored cues, with a two-sided 95% Wilson interval (fprate.wilson_interval). Cue-to-event latency includes the participant's reaction time and is not the detector's own latency.
Stuck-mic event The microphone (or the command key) held open by the switch longer than the max-hold watchdog plus one polling interval, or still held after a pause, suspend, stop or source error. A single one is a failure. The field test does not drive the microphone (nothing is typed or acted on), so this is observed in the stress list and in ordinary use, not in field-test.
Frozen configuration The [biosignal] section, the calibration profile and the yazses version, fixed before the evaluation round begins and written down by the participant (they are not in the report file; see "Reporting back").

Who and what hardware

  • Participants are adults using their own hardware on their own machine, who have read the consent text below. Intended users of the feature (people who cannot press or hold a key reliably, or whose hands are busy) are the population that matters most and are asked for specifically, but are never asked for health information; the protocol records none.
  • Hardware recorded (written by hand beside the shared file, free text, no photographs): board and BrainFlow id, or the source kind for a non-BrainFlow device (lsl: with the publishing app, thinkgear:, bitalino: with the sensor per input, myo:), or "serial YESP sensor"; electrode type, placement in words (for example "left forearm, flexor side"), sampling rate, the mains_hz setting, operating system, the yazses version, and which of squeeze, double blink and jaw clench were enabled (the standard session cues all three gestures; a switch that was off cannot fire and its row is to be ignored). The report file does not carry these.
  • A configuration is a unit of evidence. The same person on a different board, a different electrode site or a re-seated electrode (with a fresh calibration) is a different configuration, and results are never pooled across configurations without also being reported per configuration.
  • Minimum for the ADR gates: see ADR-v2-152, "Falsifiable acceptance criteria" (participant and configuration counts are set there, not here, so the numbers live in one place).

Preconditions

  1. yazses biosignal doctor reports no failing check for the source, and its closing Verdict: line is read before starting. A failing doctor is a result in its own right: record it, do not run phases through it.
  2. For any source other than synthetic or a BrainFlow board already in yazses biosignal boards, yazses biosignal check-source <source> --seconds 30 passes on the participant's machine. A failing clause (gaps, rate drift, stalls) is recorded as a result and the session does not start: the integrity gate would close the switch during the phases and the rates would describe the transport, not the detectors.
  3. A calibration profile made for this configuration exists (yazses biosignal calibrate). The wall-clock time from starting calibrate to a written profile, the number of attempts, and whether it succeeded are noted by the participant (gate A3). The command does not time the person, only the data.
  4. Defaults are the defaults. A participant may run with their own [biosignal] values, but then the result describes those values.
  5. For the evaluation rounds the participant does not edit thresholds, the budget or the mapping. A retune is allowed only between rounds (see "Tuning rounds").

Phases

yazses biosignal field-test walks the participant through these in order, prompting on screen. The tool advances on signal seconds, not on wall time, so a stream with gaps simply takes longer. The no-intent phases come first and the gestures last, so a tired participant still gives the phantom evidence that matters most. Durations are the tool's design defaults, sized for a short session; they are not derived from any measured variance, and --minutes stretches or shrinks the whole sequence proportionally.

Phase (report name) The participant Purpose Default length
rest Sits still, looks at the screen, does nothing on purpose The floor: spurious firing from noise, drift, mains, posture 120 s
speech Reads aloud or talks as in a call The jaw and face confound the speech guard exists for 90 s
chew Chews gum or a snack throughout The confound the speech guard does not cover (ADR-v2-152, known limits) 60 s
hands_busy Types and uses the mouse as in normal work Arm and hand muscle use on or near the electrode, desk vibration 120 s
movement Looks around, turns the head, shifts, stretches Posture, cable motion, electrode shift 60 s
gesture_squeeze, gesture_double_blink, gesture_jaw_clench Makes the named gesture on cue (default 8 cues, 3 s response window) Hit rate and cue-to-event latency per switch 45 s each

A default run is about 7.5 minutes of no-intent phases plus 45 seconds per gesture (about ten minutes for all three). The weights the tool applies to build its workday estimate (rest 0.20, speech 0.25, chew 0.05, hands busy 0.35, movement 0.15) are an illustrative activity mix for a desk-bound day, not data; the per-phase numbers are the evidence and the weighted figure is a convenience.

Phases that do not apply to a configuration are still run and are simply uninformative (a forearm electrode with no jaw channel makes speech and chew informative mostly for the squeeze; a headband makes them central). The jaw-clench switch is off by default and is measured only when the participant turns it on; running it once with the speech guard on and once off measures the unguarded rate the guard is meant to remove. rest is deliberately first, before the participant has watched the live monitor, so that watching the signal does not change how they sit.

The tool's own verdict is not the ADR gate

The command ends with a verdict: usable, promising, needs_tuning, do_not_enable or inconclusive. It is a per-person advisory rung, built from the weighted pooled phantom bound, with hit rate judged on the point estimate (at least 80% of cues) because with the default eight cues a Wilson lower bound above 80% is unattainable (eight of eight gives about 68%). It exists to help one person decide whether to leave a switch on. The ADR gates are stricter and are read on the per-phase numbers and the conservative interval ends; a usable verdict from one session is neither sufficient nor necessary for a gate. The standard session cues every gesture whether or not that switch is on, and the verdict looks at every conclusive gesture phase, so a switch that was off (and so could not fire) can drag the headline toward do_not_enable; in that case read the per-phase rows of the switches that were on, not the headline.

Tuning rounds

Threshold changes are the easiest way to produce a good-looking result that means nothing. The rules:

  • A round is one or more complete field-test sessions on a frozen configuration.
  • Between rounds a maintainer or participant may change thresholds, the budget or the mapping, and must write down what changed and why.
  • Never evaluate on the data used to choose the change. A configuration is judged only on a round that ran after it was frozen. A result from a round whose configuration was chosen by looking at that round's own counts is discarded from the aggregate.
  • The ADR's kill criteria count rounds: a limited number of retuning rounds per configuration is allowed before it is recorded as failing.

Aggregation: FieldTestAggregate

A session produces a report (report_to_dict, schema 1): per phase the usable and excluded seconds, the phantom count and per-kind counts, the cues scored, voided and hit, the extra activations and the cue-to-event latencies (to 0.1 s); plus the verdict, its reasons, the budget, the workday figures and the caveat. FieldTestAggregate (with report_from_dict, which recomputes every derived number from the counts so a stored file cannot disagree with itself) merges reports. What it does, and what anyone aggregating by hand must also do:

  1. Counts and seconds add; rates and bounds are recomputed. Never average per-session rates. A pooled rate is total phantoms over total usable exposure, with the bound computed on the totals. An inconclusive phase in every session can become conclusive once its usable seconds add up.
  2. Like with like. Phases are matched by name. Reports that counted different trigger kinds are refused, and so is a phase that is a gesture phase in one report and not in another.
  3. The aggregate has no notion of participant, configuration or session identity. It does not deduplicate (adding the same file twice counts it twice) and it cannot show a per-participant row. Whoever merges must therefore: build one aggregate per participant per configuration and report those rows beside the pooled one; add each file once; and report the worst participant next to the pool. A pooled bound can hide one participant for whom the switch is unusable, and a pooled pass with a failing individual is read as "not yet" for any claim about people.
  4. Configurations stay separate in the report. A cross-configuration pool is an additional row, never a replacement for the per-configuration ones.
  5. Exposure is the denominator. A session in which the switch was mostly gated or suspended contributes little usable time and so a wide bound; it is not discarded and not up-weighted.
  6. The bound's method is stated, every time: one-sided 95% Poisson upper bound on frozen thresholds, not an accuracy. The workday figure additionally applies a Bonferroni split across the phases used.

Minimum evidence for a claim

What you want to say Minimum evidence What you may not say
"This setup produced N phantom activations in H usable hours of rest." One session. State N, H and the phase. That the switch "has a low false-positive rate".
"The phantom rate for phase P is below X per hour for this configuration." Pooled usable exposure in P of at least 3 / X hours with no phantom (rule of three), more if any were seen (the report prints the extra hours needed), from at least three sessions on different days so one good afternoon does not stand for the setup. For X = 6 that is half an hour of usable signal in that phase; a default session gives about one minute of chew, so this takes many sessions or stretched ones (--minutes). Anything about another board or electrode site.
"...for people" or "...for this board" The previous row for at least the participant and configuration counts the ADR gate names, with no individual failing the same bound by a wide margin (rule 3 above). "Works", "accurate" or "reliable" without the bound and the exposure beside it.
"The hit rate for a switch is at least Y." A Wilson lower bound of at least Y from pooled scored cues. With no misses the lower bound is about n / (n + 3.84): a 90% floor needs 35 scored cues with none missed (more with any miss), an 80% floor needs 16. A default session scores 8. A point estimate on its own.
"The cancelled-activation proxy tracks real phantoms." Sessions where both the field-test phantom counts and the live ledger's cancelled counts were recorded over the same period, shown side by side. Anything about the proxy from synthetic data.
"The stuck-mic safeguard holds." The full stress list below passes on the exact code under test, and ordinary use records no stuck-mic event. That a stuck mic is impossible: zero events in finite runs bound the rate, they do not eliminate it.

Claims are about the configuration and the code revision measured. A later change to a detector, the quality gate or the governor invalidates the evidence for the changed behaviour until it is re-run.

Reporting back without raw signal

yazses biosignal field-test --json prints, and --out <file> also writes (owner-only, never overwriting an existing file), the session report. It contains counts, whole-second durations, interval bounds, the verdict and its reasons, the phase labels and the cue-to-event latencies. It contains no sample values, no recordings, no amplitudes, no thresholds, no profile contents and no event timestamps. It also contains no board, placement or version information: those are written by the participant beside it.

  • The participant reads the file before sharing. It is a small JSON document meant to be read.
  • Sharing is a deliberate act by the participant: attaching the file to an issue or a discussion by hand. The command never sends anything, and no yazses component reads the file back from a network location.
  • Do not attach a recording, a profile or a screenshot of the live signal. If a maintainer needs to see signal to understand a failure, that is a separate, explicit, opt-in contribution governed by the research-consent rules in ADR-v2-150, not part of this protocol.
  • Maintainers publish only aggregates: pooled rows, and per-participant rows under pseudonyms (P1, P2, ...), and only when there are enough participants (the ADR gate's count) that a row does not identify a person. Free-text hardware notes are edited to remove names and anything about health.
  • A participant can withdraw at any time before publication by asking for their file to be removed from the aggregate. After publication only the pooled numbers remain, which cannot be separated.

The command narrates, before the first prompt, that nothing is typed or acted on, that no signal is saved, and that only counts of false activations are reported. The fuller statement for anyone running this as part of a collection effort: what is counted (activations by activity, exposure, hits); what is not stored (signal); that nothing leaves the machine unless the participant shares the JSON by hand; that this is not a medical test and says nothing about their health; that they can stop at any time (Ctrl-C ends the session and reports what was gathered, marked as stopped early) and that stopping early produces a smaller sample, not an error.

Stress tests

Field time cannot cover failure modes that happen once a month. Each of these is injected on purpose, once with the synthetic source or a replay (cheap, repeatable, required) and, for the ones that need it, once on real hardware (required before the ADR's stuck-mic gate can be called met). The pass condition is always the safe behaviour, and for every case a hold, if one is open, is closed exactly once.

Case How it is injected Expected safe behaviour
Cable pull / electrode lift-off Synthetic dropout or flatline on one channel; on hardware, lift the electrode mid-session The channel goes untrusted; no event from it; any open hold is closed; status names the reason; recovery needs the signal to be trusted again, with no event on the way back.
Electrode pop Synthetic step that decays; on hardware, tap the electrode No blink, squeeze or clench from the transient; if a hold was open it is not extended by it.
DC step Synthetic baseline step (electrode shifted) No activation from the step itself; the baseline is not poisoned (a long step does not make the next real squeeze impossible, and does not end an open hold early without cause).
Saturation Synthetic clipping or gain that rails the ADC Channel untrusted, no event.
Mains burst Synthetic 50 or 60 Hz burst, or switching a nearby appliance on Gate marks interference, no event during it; no event on the burst's trailing edge.
Dropped packets Gap in the source clock An open squeeze is closed, filters restart without a transient trigger, a forward gap is reported as a link problem.
USB unplug mid-squeeze Unplug the board during a hold The source stops delivering; the watchdog and the runner's error path close the hold exactly once; the daemon keeps running; plugging back in does not resume the old hold.
Source returns nothing forever / raises Fake source Runner survives, hold closed, no busy loop, status shows the source as stale.
Daemon restart mid-hold Kill the daemon (including SIGKILL) while a hold is open, then start it The dictation turn ends with the process; nothing restarts a hold on startup; with the hands-free global stop on the switch starts paused; the ledger loses at most one autosave interval.
Global pause / resume mid-hold yazses handsfree pause during a squeeze, then resume The turn ends once at the pause; squeezes while paused do nothing; after resume nothing starts until the signal has relaxed.
Governor suspension mid-hold Drive the governor to suspend with a hold open The hold is closed in the same step; the switch is silent until release or its timer; release is one action.
Speech guard failure A guard source that raises or sticks at "speaking" A raising source counts as speaking (no clench); a stuck source is ignored after the watchdog so the switch is not locked out.
Corrupt state files Truncated, oversized, hostile or wrong-schema profile and ledger Loaders return "none" and the feature degrades (no profile means the squeeze stays off), never a crash and never a guess.
Profile for a different montage Profile made for other channels or sampling rate Refused with a reason; squeeze stays off until recalibrated.
Suspend and resume of the laptop Sleep the machine mid-session Treated as a gap; the hold is closed; no activation from the first blocks after wake.

A stress run fails on any stuck-mic event, any activation emitted from an untrusted window, or any exception that reaches the daemon. A fix restarts the whole list on the new code.

Relationship to the HCI × Hugging Face EEG/EOG feasibility protocol

A separate research track (epic #692, draft PR #711, design/hci-huggingface/EEG_EOG_FEASIBILITY_PROTOCOL.md on branch feat/hci-research-metrics; not yet on main, so it is cited by name and not linked) asks the same question from the research side: can EEG or EOG give a coarse, deliberate activation whose false-activation behaviour is comparable to the switches YazSes already studies? The two documents are meant to be read together. Nothing was merged between the branches; this section is the agreed mapping, written on this side only.

Topic That protocol This protocol How they line up
Stages P0 synthetic baseline, P1 injected artifacts, P2 human hardware pilot (gated) The synthetic suite, evaluate and evaluate_idle are P0/P1; field-test on real hardware is P2 Same ladder. P0/P1 results here prove the logic only.
Gate for people P2 needs independent ethics review, informed consent, a privacy and retention plan, owner approval and a safety fallback field-test run by one person on their own device is self-measurement. Pooling reports from several participants is a human pilot That gate applies to any pooled multi-participant use of this protocol. Nothing here replaces it.
Core metric "False activations per hour": activations on control blocks and idle time over scored exposure time "Phantom activations per hour": activations in no-intent phases over usable (quality-gated) exposure The same quantity under two names. Both exclude time with no usable signal.
When an hourly figure is shown Only when scored exposure reaches a minimum frozen before the run (the repository's provisional DEFAULT_MIN_EXPOSURE_S, 600 s in yazses.eyeeval.analysis); below it, the count and the exposure A phase is conclusive at 30 s of usable signal (a design default), and every rate is shown with a one-sided Poisson upper bound Not yet aligned. See the open question below.
Misses and latency Missed switches over intended events; latency p50 and p95 Hit rate with a Wilson bound; median cue-to-event latency in the field test, p95 in evaluate Same content, different spelling.
Artifact classes Per-class fraction of artifacts wrongly passed as an activation The synthetic suite checks the classes it injects (DC step, pop, motion, speech, chewing); the reports do not break results out per class Gap on this side; planned, not done.
Reporting failures Every configuration that failed, reported next to the ones that worked Adopted here as a rule: a tuning round that did not help is reported with the ones that did (see "Tuning rounds") Aligned.
Flicker stimuli None (SSVEP-style paradigms) until a photosensitivity review SSVEP stays rejected (ADR-v2-129, ADR-v2-152); the field test shows no stimulus at all Aligned.
Claims A list of allowed and disallowed statements ("On synthetic signals the pipeline separated injected artifacts with these counts"; never "EEG can control a computer", never thought, emotion or attention inference, never medical effectiveness) The "Minimum evidence for a claim" table above Adopt that list as a ceiling: nothing written from this protocol may exceed it.
Data class Real biosignals are raw sensor data (the programme's Class D): not collected centrally, not attached to issues, not shared without a separate ethics decision record files and calibration profiles are the same class; reports hold aggregate counts only Aligned. A recording is never to be attached to an issue or a PR.
Runtime adapter "Needs its own approved ADR and evidence from real users" The adapter exists in code, dormant ([biosignal] enabled = false) behind ADR-v2-152, which is Proposed Said plainly: code was written ahead of that evidence. Nothing enables it for anyone, and the acceptance gates in ADR-v2-152 are the evidence still owed.

Open question (for the maintainer, shared with that protocol): the minimum exposure before an hourly rate is printed at all. Their provisional value is 600 s of scored exposure per rate. Here the report already carries the upper bound that makes a short session honest, but it still prints a rate figure for a phase with 30 s of usable signal. Adopting their rule would mean showing the count and the exposure below the minimum and the bounded rate only above it. That is a small change to format_report and report_to_dict in fieldtest.py, not made in this round; it should wait until the number is frozen on their side so the two stay one definition.

What the existing suite pins, and what it cannot

The synthetic test suite (tests/test_biosignal_*.py) is the regression net for the logic; it does not substitute for any row above on real hardware. Which rows it covers at the logic level is tracked in design/specs/biosignal-pipeline.md ("Test checklist: what the suite pins, and what remains").

Open questions

  1. Is a default session (about two minutes of rest, one of chew) a useful unit at all for a rare-event process? Reaching an upper bound of 6 per hour in one phase takes about half an hour of usable signal in it, so the evidence comes from many sessions or stretched ones (--minutes). The first reports decide whether a longer default or a per-phase length option is needed; the CLI has no protocol-file option today, only protocol_from_dict in Python.
  2. Should the aggregate carry a participant and a configuration key so that per-participant rows and de-duplication stop being the merger's job? Today they are, and a mistake there is silent.
  3. Is a prompted speech phase representative of speech during real dictation, when the speaker is also holding the switch? Likely not entirely; the live ledger over many days is the cross-check.
  4. The field test acts on nothing, so it cannot observe a stuck microphone. Is the stress list plus the live status enough, or does the protocol need an instrumented dictation phase?
  5. How many participants with the intended-user profile can be reached, given that the protocol asks for no health information and so cannot confirm who they are? Self-description in the issue is the only route, and it is voluntary.