fix(adr-120): 7-tick majority smoothing — stops UI label flicker
After hybrid priority fix (442c03da) the W-MLP labels reach the live UI
but at ~10 Hz tick rate they flip between adjacent classes (transition /
present_still / present_moving) too fast to read. Adds majority-vote
smoothing over last 7 ticks (~700ms window) — snappy enough for real-
time feedback, stable enough that the displayed label persists long
enough to be readable.
Implementation: static ADAPTIVE_LABEL_HISTORY VecDeque + helper
adaptive_label_smooth() called at end of adaptive_override after the
model emits its raw decision. Mode of last 7 raw labels wins; ties
break sticky to the previous committed label.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
442c03da3b
commit
3e12686ae9
|
|
@ -2713,13 +2713,56 @@ fn adaptive_override(state: &AppStateInner, features: &FeatureInfo, classificati
|
||||||
model.classify(&feat_arr)
|
model.classify(&feat_arr)
|
||||||
};
|
};
|
||||||
|
|
||||||
classification.motion_level = label.to_string();
|
// ADR-120 follow-up: majority-vote smoothing across ~700 ms of
|
||||||
classification.presence = label != "absent";
|
// history. Stops the per-tick flicker that made the live label
|
||||||
|
// unreadable. Hybrid priority downstream re-checks via
|
||||||
|
// adaptive_owns_class on the smoothed label, so waving/transition
|
||||||
|
// ownership is preserved.
|
||||||
|
let smoothed = adaptive_label_smooth(&label);
|
||||||
|
classification.motion_level = smoothed.clone();
|
||||||
|
classification.presence = smoothed != "absent";
|
||||||
// Blend model confidence with existing smoothed confidence.
|
// Blend model confidence with existing smoothed confidence.
|
||||||
classification.confidence = (conf * 0.7 + classification.confidence * 0.3).clamp(0.0, 1.0);
|
classification.confidence = (conf * 0.7 + classification.confidence * 0.3).clamp(0.0, 1.0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// ADR-120 follow-up: majority-vote smoothing buffer for the adaptive
|
||||||
|
/// classifier output. At the broadcast tick rate (~10 Hz) the model emits
|
||||||
|
/// a fresh decision every ~100 ms, and adjacent decisions can disagree
|
||||||
|
/// even when reality is stable (UI flicker). We keep the last 7 ticks
|
||||||
|
/// (~700 ms) and display the mode. Snappy enough for live UX, stable
|
||||||
|
/// enough that the user can read the label without it changing mid-read.
|
||||||
|
const ADAPTIVE_SMOOTH_WIN: usize = 7;
|
||||||
|
|
||||||
|
static ADAPTIVE_LABEL_HISTORY: OnceLock<Mutex<VecDeque<String>>> = OnceLock::new();
|
||||||
|
|
||||||
|
fn adaptive_label_history_init() -> &'static Mutex<VecDeque<String>> {
|
||||||
|
ADAPTIVE_LABEL_HISTORY.get_or_init(|| Mutex::new(VecDeque::with_capacity(ADAPTIVE_SMOOTH_WIN)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Push `label` into the rolling history and return the mode (most-
|
||||||
|
/// frequent value) over the current window. Ties broken by keeping the
|
||||||
|
/// previous committed label (sticky behaviour).
|
||||||
|
fn adaptive_label_smooth(label: &str) -> String {
|
||||||
|
let mut buf = adaptive_label_history_init().lock().unwrap();
|
||||||
|
buf.push_back(label.to_string());
|
||||||
|
while buf.len() > ADAPTIVE_SMOOTH_WIN { buf.pop_front(); }
|
||||||
|
// Mode.
|
||||||
|
let mut counts: std::collections::HashMap<&str, usize> = std::collections::HashMap::new();
|
||||||
|
for v in buf.iter() {
|
||||||
|
*counts.entry(v.as_str()).or_insert(0) += 1;
|
||||||
|
}
|
||||||
|
// Prefer the same label as previous committed (sticky tie-break).
|
||||||
|
let prev = buf.front().map(|s| s.as_str()).unwrap_or(label);
|
||||||
|
let mut best = (label, 0usize);
|
||||||
|
for (k, v) in &counts {
|
||||||
|
if *v > best.1 || (*v == best.1 && *k == prev) {
|
||||||
|
best = (*k, *v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
best.0.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
/// ADR-120: classes that ONLY the adaptive W-MLP model can produce.
|
/// ADR-120: classes that ONLY the adaptive W-MLP model can produce.
|
||||||
/// The rule-based amp_presence_override / amp_classify_from_latest paths
|
/// The rule-based amp_presence_override / amp_classify_from_latest paths
|
||||||
/// know only {absent, present_still, present_moving, active}; if the
|
/// know only {absent, present_still, present_moving, active}; if the
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue