Skip to Content

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 Xtμ0X_t - \mu_0. Subtract a drift allowance kk on every step so the sum decays under pure noise. When the sum crosses a threshold hh, 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:

St=max(0,  St1+(Xtμ0)k)S_t = \max\bigl(0,\; S_{t-1} + (X_t - \mu_0) - k\bigr)
  • μ0\mu_0 — reference value under H0H_0 (“no drift”).
  • kk — drift allowance; small values make CUSUM sensitive, large values suppress noise. A standard choice is k=0.5(μ1μ0)k = 0.5 \cdot (\mu_1 - \mu_0) where μ1\mu_1 is the smallest drift worth detecting.
  • hh — decision threshold. Alert iff St>hS_t > h.

Under H0H_0, StS_t drifts toward 0 (negative expectation per step). Under H1H_1, StS_t grows linearly. Average run length to detection for a true drift of magnitude δ\delta is roughly h/(δk)h / (\delta - k).

Use (a) — Hover stabilizer

Treat per-step cursor displacement dtd_t (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.

crates/ftui-core/src/hover_stabilizer.rs
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:

FieldDefaultMeaning
drift_allowance0.5Sub-cell noise is absorbed.
detection_threshold2.0Two consistent cell-sized steps swap the target.
hysteresis_cells1Extra deadband inside the swap logic.

Use (b) — Allocation budget (with e-process dual)

The allocation-budget detector tracks frame-over-budget deviations Xt=max(0,tframetbudget)X_t = \max(0, t_{\text{frame}} - t_{\text{budget}}). A CUSUM over Xtμ0kX_t - \mu_0 - k triggers fast for gross overshoots; an e-process over the same stream provides anytime-valid guarantees for the slow-leak case.

StCUSUM=max(0,St1CUSUM+Xtμ0k)Wte-proc=Wt1e-proc(1+λt(Xtμ0))S_t^{\text{CUSUM}} = \max(0,\, S_{t-1}^{\text{CUSUM}} + X_t - \mu_0 - k) \quad\parallel\quad W_t^{\text{e-proc}} = W_{t-1}^{\text{e-proc}} (1 + \lambda_t (X_t - \mu_0))

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?

avg(last 30) vs threshold T: - too-small window → flickers on noise - too-large window → misses short bursts - cold start → undefined for < window

How 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 -40

Pitfalls

kk and hh are coupled. Halving kk does not just double sensitivity — it halves the ARL under H0H_0, 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.

Where next