NF Protocols#
A neurofeedback protocol decides when and how much to reward. It sits between the raw NF feature value (e.g. alpha power) and the feedback signal delivered to the participant. MNE-RT ships ten protocols, covering the full spectrum from simple fixed-threshold designs to adaptive psychophysics staircases, reinforcement-learning thresholds, operant conditioning schedules, cross-session transfer, and double-blind sham control.
All protocols share the same two-value contract:
Choosing a Protocol#
| Protocol | Best for | Key property | Requires baseline? |
|---|---|---|---|
| ThresholdProtocol | Clinical NF, demos, debugging | Fixed or slowly-adapting threshold; most interpretable | No |
| ZScoreProtocol | Within-session adaptation, variable signals | Rewards deviation from running mean — self-calibrating | No (warmup windows) |
| PercentileProtocol | Gamification, progressive training | Threshold tracks own rolling distribution — difficulty auto-scales | No |
| LinearTrendProtocol | Slow-learning paradigms, gradual skill acquisition | Rewards slope, not level — sustained change wins | No |
| ShamProtocol | RCTs, sham control conditions | Wraps any protocol; shuffles feedback on fraction of windows | Depends on inner |
| UpDownStaircaseProtocol | Threshold estimation, psychophysics | Converges threshold to target success rate via n-up/n-down rule | No |
| MultiBandProtocol | Dual-band NF (alpha↑ + theta↓, SMR↑ + theta↓) | AND/OR combination of two independent inner protocols | Depends on inner protocols |
| RLProtocol | Automated threshold search, fully adaptive training | ε-greedy exploration + hit-rate-driven threshold update | No (warmup windows) |
| OperantProtocol | Partial reinforcement, ratio/interval schedules | Wraps any protocol; gates rewards by FR/VR/FI/VI schedule | Depends on inner |
| TransferProtocol | Cross-session transfer, prior-seeded z-score | Seeds running statistics from a prior session file — zero warmup | No (prior file) |
Threshold Protocol#
The simplest and most widely used protocol. A fixed threshold \(\theta\) is placed on the NF feature scale and the participant receives feedback whenever the signal crosses it.
The adapt_rate parameter adds slow threshold drift to maintain a
target success rate over time — increase \(\theta\) after rewarded
windows, decrease after missed windows.
When to use: Quick prototyping, clinical applications where the
threshold is set by an expert, or as the inner protocol for
ShamProtocol or
MultiBandProtocol.
from mne_rt.protocols import ThresholdProtocol
proto = ThresholdProtocol(threshold=1.2e-10, direction="up")
crossed, mag = proto.evaluate(alpha_power)
Z-Score Protocol#
During a short warmup phase (typically 20 windows) the protocol collects enough values to seed its running mean \(\hat\mu\) and standard deviation \(\hat\sigma\) using Welford’s online algorithm. From then on, each new value is z-scored against the participant’s own distribution:
Because the statistics adapt to each individual, the same threshold \(z_\text{thr}\) (e.g. 0.5) produces approximately the same success rate regardless of signal amplitude differences between participants or sessions.
Typical trajectory
window: 1 2 3 4 5 … 20 | 21 22 23 24 25 value: 1 2 1 3 2 … 2 | 3 1 5 2 4 z-score: — — — — — … — |0.2 -0.8 2.1 0.0 1.3 reward: · · · · · … · | · · ✓ · ✓ (z_thr = 0.5) warmup phase ───────────────┘ live phase ──────────
When to use: When signal amplitude varies across sessions or participants and you want consistent difficulty without manually re-setting thresholds.
from mne_rt.protocols import ZScoreProtocol
proto = ZScoreProtocol(direction="up", zscore_threshold=0.5, warmup_windows=20)
for value in nf_stream:
crossed, mag = proto.evaluate(value)
Percentile Protocol#
A rolling buffer of the last history_len values is maintained. The
N-th percentile of this buffer acts as the current threshold:
Because the threshold tracks the participant’s own recent distribution, difficulty automatically scales up during peak performance and relaxes during fatigue — producing a self-calibrating reward rate of approximately \((100 - n)\%\).
When to use: Training paradigms where the goal is relative improvement over recent performance rather than crossing an absolute target. The 75th-percentile default rewards the best ~25 % of windows.
from mne_rt.protocols import PercentileProtocol
proto = PercentileProtocol(percentile=75.0, direction="up", history_len=100)
crossed, mag = proto.evaluate(alpha_power)
Linear-Trend Protocol#
Rather than comparing the current value to a threshold, this protocol
fits an ordinary-least-squares regression over the last window values
and rewards when the slope is in the target direction with sufficient
goodness-of-fit:
This avoids rewarding transient spikes and instead encourages sustained directional change — a more meaningful signal for genuine learning.
Typical trajectory
values over 5-window regression: spike (not rewarded) sustained rise (rewarded) ▲ · ▲ · │ ╭╮ │ ╭─╯ │╭╯╰╮ │ ╭─╯ │╯ ╰── │ ╭─╯ └────────── └────────── slope ≈ 0, R² low slope > 0, R² high
When to use: Slow-learning paradigms (e.g. tinnitus suppression, chronic pain, depression) where the target is gradual signal reorganisation over minutes rather than fast-reacting moment-to-moment control.
from mne_rt.protocols import LinearTrendProtocol
proto = LinearTrendProtocol(direction="up", window=20, slope_threshold=0.0, min_r2=0.3)
crossed, mag = proto.evaluate(alpha_power)
Sham Protocol#
In a double-blind NF design the participant should not be able to tell
when feedback is real and when it is sham. ShamProtocol
intercepts the inner protocol’s output on a configurable fraction of
windows and substitutes a randomly-drawn historical reward value:
window: 1 2 3 4 5 6 7 8 … real: (T,1) (F,0) (T,2) (F,0) (T,3) (F,0) (T,1) (F,0) output: (T,1) (F,0) (T,1) (F,0) (T,3) (T,2) (T,1) (F,0) sham?: no no yes no no yes yes no
The inner protocol’s state always advances correctly — only the
delivered feedback is sometimes replaced. The sham_log attribute
records which windows were sham for post-session unblinding.
Important: the inner protocol must be configured separately; the
ShamProtocol wrapper is agnostic to the inner
protocol type.
When to use: Any experiment that requires a within-session sham
control condition. Set sham_rate=0.5 for a 50/50 real/sham split,
or use rng_seed for exact reproducibility.
from mne_rt.protocols import ZScoreProtocol
from mne_rt.protocols.sham import ShamProtocol
inner = ZScoreProtocol(direction="up")
proto = ShamProtocol(inner, sham_rate=0.5, rng_seed=42)
for value in nf_stream:
crossed, magnitude = proto.evaluate(value)
# After session:
sham_indices = [i for i, s in enumerate(proto.sham_log) if s]
Up-Down Staircase Protocol#
The classic psychophysics staircase of Levitt[1]. The threshold \(\theta\) is adjusted after each window based on the participant’s recent success/failure run:
The step size \(\Delta\) is halved after every
n_reversals_before_halving direction reversals (a reversal is when
the staircase changes direction), zooming in progressively on the
participant’s current threshold.
Convergence levels for common rules:
| Rule | Success rate | Use case |
|---|---|---|
| 1-up / 1-down | 50 % | Equal success/fail balance |
| 1-up / 2-down | 70.7 % | Standard; motivating default |
| 1-up / 3-down | 79.4 % | Easier start, more rewards early |
Typical staircase trajectory
threshold
0.70 · ·─·
0.65 ·─· ·─╯ ╰─·
0.60 ╰─·─·─╯ ╰─·
0.55 ╰─·─·
0.50 initial ╰── (converging)
1 2 3 4 5 6 7 8 9 window
S S F F S S F S F (S=success, F=fail, 1-up/2-down)
reversal points ↑ stored in reversal_thresholds
When to use: When you need an objective, data-driven estimate of the participant’s NF threshold — analogous to threshold tracking in audiometry or psychophysics.
from mne_rt.protocols.staircase import UpDownStaircaseProtocol
proto = UpDownStaircaseProtocol(
initial_threshold=0.5, direction="up",
n_up=1, n_down=2, step_size=0.05,
)
for value in nf_stream:
crossed, mag = proto.evaluate(value)
# After session — estimate perceptual threshold:
threshold_estimate = float(np.mean(proto.reversal_thresholds[-6:]))
Multi-Band Protocol#
Many clinical NF protocols target two frequency bands simultaneously — reward alpha up-regulation while penalising theta up-regulation (focus training), or reward SMR while suppressing theta (ADHD protocol [3]).
MultiBandProtocol wraps two independent inner
protocols and combines their outputs:
The geometric mean ensures both bands contribute equally: a very large alpha reward cannot compensate for zero theta suppression — both must be non-zero to produce a non-zero combined magnitude.
AND vs OR logic
window: 1 2 3 4 alpha↑: (T,2) (T,1) (F,0) (T,2) theta↓: (F,0) (T,1) (T,1) (T,0.5) AND logic: ✗ ✓ ✗ ✓ (both must cross) magnitude: 0 √1×1=1 0 √2×0.5≈1.0 OR logic: ✓ ✓ ✓ ✓ (either crosses)
When to use: Any protocol that targets two brain rhythms
simultaneously. The two inner protocols are independent and can be of
different types — e.g. a ThresholdProtocol for
alpha and a ZScoreProtocol for theta.
from mne_rt.protocols import ZScoreProtocol
from mne_rt.protocols.multiband import MultiBandProtocol
alpha_proto = ZScoreProtocol(direction="up", warmup_windows=20)
theta_proto = ZScoreProtocol(direction="down", warmup_windows=20)
proto = MultiBandProtocol(
protocol_up=alpha_proto,
protocol_down=theta_proto,
require_both=True,
up_label="alpha",
down_label="theta",
)
for alpha_val, theta_val in zip(alpha_stream, theta_stream):
crossed, magnitude = proto.evaluate(alpha_val, theta_val)
References: Sterman and Egner[3]
RL Protocol#
A lightweight reinforcement-learning protocol that finds the right reward threshold automatically, without requiring any manual calibration. During a short warmup phase no rewards are issued while the protocol collects statistics. Afterwards, the threshold \(\theta\) is adjusted after each window to keep the rolling hit rate close to the target:
With probability \(\varepsilon\) (the epsilon parameter) the current
value is treated as a forced hit (exploration step), preventing the threshold
from drifting so high that the participant never succeeds.
When to use: When there is no prior calibration data and you want
the protocol to self-tune from scratch. Works particularly well in
conjunction with ShamProtocol for within-session
sham control.
from mne_rt.protocols import RLProtocol
proto = RLProtocol(
direction="up",
target_hit_rate=0.70,
lr=0.01,
epsilon=0.1,
warmup_windows=20,
history_len=50,
)
for value in nf_stream:
crossed, mag = proto.evaluate(value)
Operant Protocol#
Partial reinforcement schedules are more resistant to extinction than
continuous reinforcement Ferster and Skinner[4].
OperantProtocol wraps any existing NF protocol and
filters its reward output through one of four classical schedules:
| Schedule | Abbreviation | Rule |
|---|---|---|
| Fixed Ratio | FR | Reward on every N-th hit |
| Variable Ratio | VR | Each hit rewarded with probability 1/N |
| Fixed Interval | FI | First hit after exactly T seconds is rewarded |
| Variable Interval | VI | First hit after a random interval (mean T) is rewarded |
The inner protocol’s state always advances regardless of whether the schedule releases a reward — so running statistics (z-score, staircase threshold, etc.) continue to update correctly.
When to use: Whenever reduced reward density is experimentally desirable (e.g. to study partial reinforcement extinction effects in NF, or to maintain engagement over long sessions by preventing saturation).
from mne_rt.protocols import ZScoreProtocol, OperantProtocol
inner = ZScoreProtocol(direction="up", warmup_windows=20)
proto = OperantProtocol(inner, schedule="VR", ratio=3, rng_seed=42)
for value in nf_stream:
crossed, mag = proto.evaluate(value)
References: Ferster and Skinner[4]
Transfer Protocol#
The standard ZScoreProtocol needs a warmup phase to
estimate \(\hat\mu\) and \(\hat\sigma\) before rewards can be issued.
TransferProtocol eliminates warmup by loading the
population statistics from a previous session’s beh.json file and seeding
the Welford accumulators directly:
From the first window onward, each new value is z-scored against this informed prior and updates the statistics via Welford’s algorithm, gradually replacing the prior with session-specific data.
Session file format (BIDS-compatible beh.json):
{
"meta": {"modalities": ["sensor_power"]},
"data": {"sensor_power": [0.12, 0.14, 0.11, …]}
}
Such files are written automatically by save().
When to use: Multi-session training programmes where consistent reward rates across sessions improve participant motivation. Also useful in studies where the first session’s statistics serve as the participant’s personalised baseline.
from mne_rt.protocols import TransferProtocol
proto = TransferProtocol(
fname="sub-01_ses-01_task-nf_beh.json",
modality="sensor_power",
direction="up",
zscore_threshold=0.5,
)
for value in nf_stream:
crossed, mag = proto.evaluate(value)