CUSUM — Cumulative Sum Control
What goes wrong with a naive approach
Two different subsystems need the same primitive: “tell me when a small-but-persistent deviation has accumulated enough to act on.”
- Hover stabilizer. Mouse coordinates jitter by ±1 cell because the terminal quantises to a cell grid and trackpads emit sub-cell movements. A naive “hover on the cell under the cursor” flickers during micro-movement.
- Allocation budget. The runtime has a frame-time budget. A single jumbo frame doesn’t matter (we’ll catch it in the conformal layer). What matters is a slow leak — ten frames each 200 µs over budget. A point-wise threshold won’t trigger; only an accumulator does.
A simple count-over-threshold triggers too slowly or too jitterily depending on the threshold. A windowed average smooths but also delays. CUSUM (Page, 1954) is the principled solution: it accumulates deviations and triggers when they add up, subtracting a drift allowance to kill slow noise.
Mental model
Run a one-sided cumulative sum over the centred deviation . Subtract a drift allowance on every step so the sum decays under pure noise. When the sum crosses a threshold , raise an alert.
CUSUM is the tiny brother of the e-process. It is not anytime-valid in the martingale sense, but it is cheap, numerically trivial, and perfect for single-stream drift detection. Where you need type-I error control across continuous peeking, pair CUSUM with the e-process.
The math
One-sided CUSUM:
- — reference value under (“no drift”).
- — drift allowance; small values make CUSUM sensitive, large values suppress noise. A standard choice is where is the smallest drift worth detecting.
- — decision threshold. Alert iff .
Under , drifts toward 0 (negative expectation per step). Under , grows linearly. Average run length to detection for a true drift of magnitude is roughly .
Use (a) — Hover stabilizer
Treat per-step cursor displacement (in cells) as the observation. Hover-cell swaps happen only when CUSUM exceeds a threshold; a hysteresis zone holds the hover target against sub-cell jitter.
use ftui_core::hover_stabilizer::{HoverStabilizer, HoverConfig};
let mut hs = HoverStabilizer::new(HoverConfig {
drift_allowance: 0.5, // k
detection_threshold: 2.0, // h
hysteresis_cells: 1,
});
let Some(new_target) = hs.observe(cursor_cell) else {
return; // inside hysteresis zone; keep the existing hover
};Parameters:
| Field | Default | Meaning |
|---|---|---|
drift_allowance | 0.5 | Sub-cell noise is absorbed. |
detection_threshold | 2.0 | Two consistent cell-sized steps swap the target. |
hysteresis_cells | 1 | Extra deadband inside the swap logic. |
Use (b) — Allocation budget (with e-process dual)
The allocation-budget detector tracks frame-over-budget deviations . A CUSUM over triggers fast for gross overshoots; an e-process over the same stream provides anytime-valid guarantees for the slow-leak case.
Alert if either trigger fires. The dual gives cheap coverage (CUSUM) plus principled coverage (e-process) for the cost of a single extra floating-point multiply per frame.
See e-processes for the martingale side of the pair.
Why CUSUM over windowed average?
Windowed average
avg(last 30) vs threshold T:
- too-small window → flickers on noise
- too-large window → misses short bursts
- cold start → undefined for < windowHow to debug
Hover stabilizer emits cusum_hover lines; the allocation detector
emits alloc_cusum. Pairs well with eprocess_reject when the dual
is used.
{"schema":"cusum_hover","dx":1.2,"S_t":1.9,"threshold":2.0,
"triggered":false,"hover_cell":[34,12]}FTUI_EVIDENCE_SINK=/tmp/ftui.jsonl cargo run -p ftui-demo-showcase
# Trace hover CUSUM during a trackpad swipe:
jq -c 'select(.schema=="cusum_hover") | [.dx, .S_t, .triggered]' \
/tmp/ftui.jsonl | tail -40Pitfalls
and are coupled. Halving does not just double sensitivity — it halves the ARL under , so false alarms explode. Tune both together against a recorded trace; blindly cranking one is how hover flicker comes back.
CUSUM is not anytime-valid. It has no formal type-I error guarantee over continuous peeking. When that matters (budget alerts, flake detection), pair it with the e-process as described above. If both matter, deploy both; they complement more than overlap.
Cross-references
- E-processes — the anytime-valid partner for allocation detection.
- Alpha-investing — how to budget multiple simultaneous CUSUMs without inflating FDR.
/runtime/overview— where the allocation detector sits inside the frame loop.