From 442c03da3bcc68b406ce3d8b443e921497793a40 Mon Sep 17 00:00:00 2001 From: arsen Date: Mon, 18 May 2026 01:16:27 +0700 Subject: [PATCH] =?UTF-8?q?fix(adr-120):=20hybrid=20priority=20=E2=80=94?= =?UTF-8?q?=20adaptive=20owns=20waving/transition?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit W-MLP claimed 90.4% training accuracy in ADR-120 but live UI kept showing only the 4 baseline classes (absent/still/moving/active). Root cause: 3 amp_presence_override / amp_classify_from_latest call sites ALWAYS overwrite classification.motion_level after adaptive_override runs, regardless of what the model decided. The rule-based path only knows 4 classes; the 2 new ones (waving, transition) emitted by the adaptive W-MLP were silently clobbered every tick. Hybrid priority: rule-based wins → absent / present_still / present_moving / active (ESPectre-style F1>96%, battle-tested) adaptive wins → waving / transition (exclusive to ADR-120 W-MLP) Implementation: new helper adaptive_owns_class() + ADAPTIVE_EXCLUSIVE_CLASSES constant. Each of the 3 rule-based override blocks (multi-BSSID tick, feature_state path, per-node loop) now guards on `if !adaptive_owns_class( classification.motion_level)`. Skips the overwrite when the adaptive model has just emitted a new class. Live verification (30s sample): transition: 14/30 (47%) — visible in live UI for the first time present_still: 10/30 (33%) present_moving: 1/30 absent: 1/30 Co-Authored-By: Claude Opus 4.7 --- .../wifi-densepose-sensing-server/src/main.rs | 61 +++++++++++++------ 1 file changed, 44 insertions(+), 17 deletions(-) diff --git a/v2/crates/wifi-densepose-sensing-server/src/main.rs b/v2/crates/wifi-densepose-sensing-server/src/main.rs index 35d0a212..d3e2e99f 100644 --- a/v2/crates/wifi-densepose-sensing-server/src/main.rs +++ b/v2/crates/wifi-densepose-sensing-server/src/main.rs @@ -2720,6 +2720,19 @@ fn adaptive_override(state: &AppStateInner, features: &FeatureInfo, classificati } } +/// ADR-120: classes that ONLY the adaptive W-MLP model can produce. +/// The rule-based amp_presence_override / amp_classify_from_latest paths +/// know only {absent, present_still, present_moving, active}; if the +/// adaptive model has just emitted `waving` or `transition`, we must NOT +/// overwrite it with the rule-based output. Hybrid priority: rule-based +/// wins for the 4 baseline classes (it's battle-tested at F1 > 96%); +/// adaptive wins exclusively when emitting a class outside that set. +const ADAPTIVE_EXCLUSIVE_CLASSES: &[&str] = &["waving", "transition"]; + +fn adaptive_owns_class(label: &str) -> bool { + ADAPTIVE_EXCLUSIVE_CLASSES.iter().any(|&c| c == label) +} + /// ADR-120: push the current frame's feature vector into the rolling /// window buffer, evicting the oldest entry when at capacity. Called /// once per tick from the broadcast tick task where `&mut AppStateInner` @@ -3035,12 +3048,16 @@ async fn windows_wifi_task(state: SharedState, tick_ms: u64) { // reference, see #[allow(dead_code)]). With gain-lock active (ADR-100) // CV of broadband mean amplitude separates EMPTY/STILL/WALK by 3-6× // on this deployment, where RSSI MAD-Δ overlapped within ±0.03. - if let Some((level, presence, conf)) = - amp_presence_override(frame.node_id, &frame.amplitudes) - { - classification.motion_level = level; - classification.presence = presence; - classification.confidence = conf; + // ADR-120: skip the rule-based override when the adaptive model + // has emitted a class only it can produce (waving / transition). + if !adaptive_owns_class(&classification.motion_level) { + if let Some((level, presence, conf)) = + amp_presence_override(frame.node_id, &frame.amplitudes) + { + classification.motion_level = level; + classification.presence = presence; + classification.confidence = conf; + } } // ADR-104 phase-domain: update phase drift score for this node // alongside the amplitude classifier. No-op if no phase baseline. @@ -6045,10 +6062,14 @@ async fn udp_receiver_task(state: SharedState, udp_port: u16) { // ADR-101: inherit the raw-amplitude classifier from the // CSI path (this feature_state path doesn't carry amps). - if let Some((level, presence, conf)) = amp_classify_from_latest() { - classification.motion_level = level; - classification.presence = presence; - classification.confidence = conf; + // ADR-120: skip when adaptive model produced a class only + // it knows (waving / transition). + if !adaptive_owns_class(&classification.motion_level) { + if let Some((level, presence, conf)) = amp_classify_from_latest() { + classification.motion_level = level; + classification.presence = presence; + classification.confidence = conf; + } } // ADR-112: prefer multistatic-derived signal_field @@ -6282,18 +6303,24 @@ async fn udp_receiver_task(state: SharedState, udp_port: u16) { classification.confidence = (conf * 0.7 + classification.confidence * 0.3).clamp(0.0, 1.0); } - // ADR-101: amp classifier wins over the legacy adaptive model. + // ADR-101: amp classifier wins over the legacy adaptive + // model for absent/still/moving/active. ADR-120: but the + // adaptive W-MLP retains exclusive ownership of the new + // classes (waving / transition) — skip the override when + // the model has already emitted one. let amps_now = ns.frame_history.back().cloned().unwrap_or_default(); - if !amps_now.is_empty() { - if let Some((level, presence, conf)) = amp_presence_override(node_id, &s_now) { + if !adaptive_owns_class(&classification.motion_level) { + if !amps_now.is_empty() { + if let Some((level, presence, conf)) = amp_presence_override(node_id, &s_now) { + classification.motion_level = level; + classification.presence = presence; + classification.confidence = conf; + } + } else if let Some((level, presence, conf)) = amp_classify_from_latest() { classification.motion_level = level; classification.presence = presence; classification.confidence = conf; } - } else if let Some((level, presence, conf)) = amp_classify_from_latest() { - classification.motion_level = level; - classification.presence = presence; - classification.confidence = conf; } // ADR-104 phase-domain: update phase drift if a // phase baseline is loaded and the latest frame