test(wasm-edge): synthetic-ground-truth validation harness for edge skills (ADR-160)
Plant signals with known answers, run the real detector, MEASURE detection accuracy / precision / recall / rate-error — synthetic-ground-truth ONLY, not field accuracy. MEASURED-on-synthetic (12 tests, all green): - vital_trend, exo_ghost_hunter(hidden breathing), occupancy, intrusion, exo_rain_detect, sig_optimal_transport: acc 1.000 - exo_time_crystal: 1.000 on periodic-vs-aperiodic (its sub-harmonic-vs-clean- period claim is NOT separable by autocorrelation — recorded honestly) - sig_flash_attention: 8/8 peak localization; spt_spiking_tracker: 4/4 zone localization (sparse plant); sig_mincut_person_match: 0 id-swaps/40 frames - lrn_dtw_gesture_learn: enrollment validated (replay-match reported, not asserted) - sig_sparse_recovery: trigger validated; recovery accuracy reported NEGATIVE (-2.2% vs unrecovered baseline) — only its detect/trigger path is validated DATA-GATED (listed, NOT faked): med_seizure/apnea/cardiac/respiratory/gait, sec_weapon_detect, exo_emotion/happiness/dream_stage/gesture_language — each needs real labelled clinical/affect/ASL/metal-object data; no number claimed. benchmarks/edge-skills/RESULTS.md documents every result + reproduce command and the explicit honesty boundary. ADR-160 deferred 'per-skill accuracy validation' item updated to PARTIALLY MEASURED-on-synthetic + DATA-GATED. Suite: 631 passed default / 669 medical, 0 failed. Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
parent
c6eacb7ff8
commit
41665d3de9
|
|
@ -0,0 +1,132 @@
|
|||
# Edge-Skill Synthetic-Ground-Truth Validation — RESULTS
|
||||
|
||||
**Crate:** `v2/crates/wifi-densepose-wasm-edge` (workspace-EXCLUDED — build from its own dir)
|
||||
**Branch:** `feat/edge-skills-synthetic-validation`
|
||||
**ADR:** [ADR-160](../../docs/adr/ADR-160-edge-skill-library-honest-labeling.md)
|
||||
**Date:** 2026-06-13
|
||||
**Harness:** `tests/synthetic_validation.rs`
|
||||
|
||||
> **HONESTY BOUNDARY — read first.** Everything below is **synthetic-ground-truth
|
||||
> validation**: a signal is *planted* with a known answer, the **real** detector
|
||||
> is run, and detection accuracy / precision / recall / rate-error is **measured**.
|
||||
> This is **NOT field accuracy.** A skill that recovers a planted sinusoid here is
|
||||
> proven to do the math it claims on a *constructed* signal; it is **NOT** proven
|
||||
> to work on real CSI in a real room. Skills whose detection target cannot be
|
||||
> honestly planted (clinical, weapon, affect, sleep-stage, sign-language) are
|
||||
> **NOT** given a number — they are listed under **DATA-GATED** with the real
|
||||
> data each would require.
|
||||
|
||||
## Reproduce
|
||||
|
||||
```bash
|
||||
cd v2/crates/wifi-densepose-wasm-edge # workspace-excluded; build here
|
||||
cargo test --features std --test synthetic_validation -- --nocapture
|
||||
# also runs under the medical tier (med_* skills stay DATA-GATED, not validated):
|
||||
cargo test --features std,medical-experimental --test synthetic_validation -- --nocapture
|
||||
```
|
||||
|
||||
Each `MEASURED-on-synthetic | …` line printed by the harness is the source of the
|
||||
table below. Numbers are deterministic (no RNG; pseudo-noise uses a fixed LCG seed).
|
||||
|
||||
---
|
||||
|
||||
## MEASURED-on-synthetic (constructible skills)
|
||||
|
||||
| Skill | What was planted (ground truth) | Result | Grade |
|
||||
|-------|----------------------------------|--------|-------|
|
||||
| **vital_trend** | BPM held N≥6 calls at each threshold band (brady/tachy-pnea <12 / >25, brady/tachy-cardia <50 / >120, apnea breathing<1.0 for ≥20) vs normal | **acc 1.000, prec 1.000, recall 1.000** (TP5 FP0 TN5 FN0) | MEASURED |
|
||||
| **exo_time_crystal** | period-2 coordinated motion vs pseudo-noise + flat | **acc 1.000** (TP1 FP0 TN2 FN0) | MEASURED † |
|
||||
| **exo_ghost_hunter** (hidden breathing) | phase sinusoid at lag-8 (breathing band 5–15) in an empty room vs flat phase | **acc 1.000**; planted score **1.000**, flat **0.000** | MEASURED |
|
||||
| **occupancy** | 220-frame flat-amplitude calibration, then strong per-zone amplitude variance vs flat | **acc 1.000** (TP1 FP0 TN1 FN0) | MEASURED |
|
||||
| **intrusion** | calibrate→arm (330 quiet frames), then per-subcarrier Δphase>1.5 + Δamp≫3σ vs quiet | **acc 1.000** (TP1 FP0 TN1 FN0) | MEASURED |
|
||||
| **exo_rain_detect** | empty room, 60-frame baseline, then broadband variance (8/8 groups, ratio≫2.5) for ≥10 frames vs stable-low | **acc 1.000** (TP1 FP0 TN1 FN0) | MEASURED |
|
||||
| **sig_flash_attention** | sustained high phase+amplitude in each of the 8 subcarrier groups; assert reported attention peak == planted group | **peak-localization 8/8 = 1.000** | MEASURED |
|
||||
| **spt_spiking_tracker** | sparse (2-subcarrier) large phase-delta in each of the 4 zones; assert tracked zone == planted zone | **zone-localization 4/4 = 1.000** | MEASURED ‡ |
|
||||
| **sig_optimal_transport** | sustained large frame-to-frame amplitude-distribution change vs stationary | **acc 1.000** (TP1 FP0 TN1 FN0) | MEASURED |
|
||||
| **sig_mincut_person_match** | 2 persons with distinct stable per-region variance signatures over 40 frames | **person ids assigned, 0 id-swaps / 40 frames** | MEASURED |
|
||||
| **lrn_dtw_gesture_learn** | stillness → 3 identical gesture rehearsals → enrollment | **template enrolled (templates=1)** | MEASURED (enroll) §|
|
||||
| **sig_sparse_recovery** | 30 clean frames to init, then 8/32 (25%) nulled subcarriers | **dropout-detect + recovery-trigger = PASS** | MEASURED (trigger) ¶|
|
||||
|
||||
### Caveats on individual results
|
||||
|
||||
† **exo_time_crystal — honest discriminative limit.** A *pure* periodic signal
|
||||
already has autocorrelation peaks at lag L **and** 2L (natural harmonics), so this
|
||||
"period-doubling" detector cannot separate a true period-2 sub-harmonic from a
|
||||
plain periodic signal — an earlier plant using a clean sine produced a *false
|
||||
positive* (recorded during development). The construct it **can** discriminate
|
||||
with known ground truth is **periodic-coordination vs aperiodic** (noise/flat),
|
||||
which is what is measured (1.000). The original "sub-harmonic vs clean period"
|
||||
claim is **NOT** validatable with this algorithm.
|
||||
|
||||
‡ **spt_spiking_tracker — plant must be sparse.** With weights init'd home=1.0 /
|
||||
cross=0.25, firing all 8 inputs in a zone (8×0.25=2.0 > threshold 1.0) overdrives
|
||||
*every* output neuron and the tracker collapses to zone 0 (measured 1/4 during
|
||||
development). Firing only 2 inputs (home 2.0 fires, cross 0.5 silent) yields clean
|
||||
4/4 zone localization. The validatable claim is *single-zone* localization.
|
||||
|
||||
§ **lrn_dtw_gesture_learn — enrollment validated; replay-match NOT.** The
|
||||
deterministic, constructible part (stillness → 3 identical rehearsals → a template
|
||||
is enrolled) is MEASURED. The DTW *replay match* (731) did **not** fire on the
|
||||
identical replay in this run (`match_same=false`) — replay-recognition accuracy is
|
||||
**reported, not asserted**, and is not claimed as validated.
|
||||
|
||||
¶ **sig_sparse_recovery — trigger validated; recovery accuracy is NEGATIVE.**
|
||||
The dropout-detection + ISTA-recovery *trigger* pipeline fires correctly on >10%
|
||||
planted nulls (asserted). But the **measured recovery accuracy is NOT a win**:
|
||||
recovered RMSE **1.0045** vs unrecovered-null RMSE **0.9830** (**−2.2%**, i.e.
|
||||
slightly *worse* than leaving the nulls at zero) on a neighbor-correlated signal.
|
||||
The tridiagonal correlation model's fixed point does not equal the planted truth.
|
||||
**The recovery's reconstruction quality is therefore NOT validated as effective on
|
||||
synthetic data** — only its detection/trigger path is. Reported honestly; no
|
||||
positive number claimed.
|
||||
|
||||
---
|
||||
|
||||
## DATA-GATED — NOT validatable on synthetic data
|
||||
|
||||
Planting a "seizure-like" / "weapon-like" / "happy-like" synthetic signal and
|
||||
claiming the detector "works" validates **nothing real** and is exactly the
|
||||
AI-slop this project fights. These skills run real DSP (per ADR-160, 0 stubs) and
|
||||
keep their ADR-160 disclaimers, but get **no accuracy number** here. Each needs
|
||||
the specific real, labelled data listed:
|
||||
|
||||
| Skill | Why not constructible on synthetic | Real data required |
|
||||
|-------|------------------------------------|--------------------|
|
||||
| `med_seizure_detect` | "seizure-like" motion is not a seizure; no ground-truth signature exists synthetically | Clinical EEG-/video-labelled tonic-clonic seizure CSI from instrumented patients |
|
||||
| `med_sleep_apnea` | a planted breathing-pause is not clinical apnea (AHI scoring, hypopnea, desaturation) | Polysomnography-labelled (PSG) overnight CSI with scored apnea/hypopnea events |
|
||||
| `med_cardiac_arrhythmia` | a synthetic HR sequence cannot encode true arrhythmia morphology | ECG-labelled CSI (AFib/PVC/etc.) from clinical monitoring |
|
||||
| `med_respiratory_distress` | distress is a clinical gestalt, not a plantable rate | Clinician-labelled respiratory-distress CSI episodes |
|
||||
| `med_gait_analysis` | clinical gait metrics need a reference motion-capture standard | Mocap-/force-plate-labelled gait CSI |
|
||||
| `sec_weapon_detect` | a high variance ratio is RF reflectivity, **not** weapon discrimination (ADR-160 §A3 already renamed the event to `HIGH_METAL_REFLECTIVITY`) | Labelled metal-object-vs-no-object CSI with controlled object classes |
|
||||
| `exo_emotion_detect` | affect is not recoverable from a planted heuristic; outputs are proxies (ADR-160 §A2) | Validated affect-labelled CSI (self-report / physiological ground truth) |
|
||||
| `exo_happiness_score` | "happiness" is a gait-energy proxy, not a measured affect (ADR-160 §A2) | Validated affect/valence-labelled CSI |
|
||||
| `exo_dream_stage` | sleep staging needs PSG reference (EEG/EOG/EMG) | PSG-staged overnight CSI |
|
||||
| `exo_gesture_language` | coarse gesture clusters ≠ true sign language (ADR-160 §A4) | Labelled ASL letter/word CSI dataset |
|
||||
|
||||
> The above are **not failures** — they are the honest boundary. A smaller set of
|
||||
> genuinely-measured skills plus this explicit gated list is the deliverable, per
|
||||
> the prove-everything directive.
|
||||
|
||||
---
|
||||
|
||||
## Skills not in either list
|
||||
|
||||
The remaining edge skills (smart-building / retail / industrial occupancy-style,
|
||||
the other `sig_*`/`lrn_*`/`spt_*`/`tmp_*`/`qnt_*`/`aut_*`/`ais_*` algorithm-named
|
||||
modules) are **wired and exercised live** in the unified pipeline integration test
|
||||
(`tests/pipeline_all.rs`, all 59 default / 64 medical skills run without panic over
|
||||
300 synthetic frames) but were **not** given an individual planted-ground-truth
|
||||
accuracy number here. They are honest REAL-DSP modules (ADR-160) whose physical
|
||||
observable could be planted with more harness work; that is deferred, not claimed.
|
||||
|
||||
## Test counts (full crate suite)
|
||||
|
||||
```
|
||||
DEFAULT (--features std): 631 passed, 0 failed
|
||||
(lib 504; budget 25; honest_labeling 10; pipeline_all 4; synthetic_validation 12; bench 1; vendor 75)
|
||||
MEDICAL (--features std,medical-experimental): 669 passed, 0 failed
|
||||
(lib 542; +16 same new tests; med_* stay DATA-GATED, not validated)
|
||||
```
|
||||
|
||||
(M6 baseline was 615 / 653; the new pipeline_all (4) + synthetic_validation (12)
|
||||
tests add 16 to each tier.)
|
||||
|
|
@ -178,10 +178,33 @@ label or behavior change, consistent with leaving their claim surface intact.)
|
|||
|
||||
## Deferred Backlog (Nothing Dropped)
|
||||
|
||||
- **Per-skill accuracy validation** — **DATA-GATED**. Validating any med_*/affect/
|
||||
sign-language claim requires labelled clinical/affective/ASL data and reference
|
||||
standards that do not exist in this repo. The disclaimers + feature gate are the
|
||||
honest stand-in. Nothing is claimed that is not measured.
|
||||
- **Per-skill accuracy validation** — **PARTIALLY MEASURED-on-synthetic**
|
||||
(2026-06-13). For the subset of skills whose detection target is *constructible*
|
||||
with known ground truth, a synthetic-ground-truth harness
|
||||
(`tests/synthetic_validation.rs`, 12 tests) plants signals with known answers,
|
||||
runs the real detector, and **measures** detection accuracy / rate-error:
|
||||
`vital_trend`, `exo_time_crystal` (periodic-vs-aperiodic — its sub-harmonic-vs-
|
||||
clean-period claim is NOT separable, recorded honestly), `exo_ghost_hunter`
|
||||
(hidden breathing), `occupancy`, `intrusion`, `exo_rain_detect`,
|
||||
`sig_flash_attention` (8/8 peak localization), `spt_spiking_tracker` (4/4 zone
|
||||
localization, sparse plant), `sig_optimal_transport`, `sig_mincut_person_match`
|
||||
(0 id-swaps), `lrn_dtw_gesture_learn` (enrollment) — all 1.000 where claimed;
|
||||
`sig_sparse_recovery`'s recovery accuracy is reported **negative** (−2.2% vs
|
||||
unrecovered baseline) — only its trigger path is validated. Full numbers +
|
||||
reproduce commands in `benchmarks/edge-skills/RESULTS.md`.
|
||||
The **med_*/affect/sign-language/weapon** claims remain **DATA-GATED**:
|
||||
validating them requires labelled clinical/affective/ASL/metal-object data and
|
||||
reference standards that do not exist in this repo. Planting a "seizure-/weapon-/
|
||||
happy-like" synthetic signal validates nothing real and is explicitly refused;
|
||||
RESULTS.md lists each with the real data it needs. The disclaimers + feature gate
|
||||
are the honest stand-in. Nothing is claimed that is not measured.
|
||||
- **Unified edge pipeline** — **MEASURED** (2026-06-13). `src/pipeline_all.rs`
|
||||
(`EdgePipeline`) + `src/skill_registry.rs` register **every** runtime skill
|
||||
behind one uniform `EdgeSkill` trait and run them all per CSI frame; `med_*` are
|
||||
registered only under `--features medical-experimental` (preserves the §A1 gate).
|
||||
`tests/pipeline_all.rs` (4 tests) proves all 59 default / 64 medical skills run
|
||||
without panic over 300 synthetic frames with a well-formed aggregated event
|
||||
stream. `examples/run_all_skills.rs` is a runnable demo. No skill DSP changed.
|
||||
- **Criterion benches for `process_frame` budget claims** — **DONE (host)**
|
||||
(ADR-163, 2026-06-12). `benches/process_frame_bench.rs` benches the heaviest
|
||||
hot paths (`exo_time_crystal` 256×128 autocorrelation, `exo_ghost_hunter`
|
||||
|
|
|
|||
|
|
@ -0,0 +1,762 @@
|
|||
//! Synthetic-ground-truth validation harness (ADR-160 deliverable 2).
|
||||
//!
|
||||
//! For the subset of edge skills whose detection target can be PLANTED with
|
||||
//! known ground truth, we generate N signals with known answers, run the real
|
||||
//! detector, and MEASURE detection rate / precision / recall / rate-error.
|
||||
//!
|
||||
//! # Honesty boundary
|
||||
//!
|
||||
//! This is **synthetic-ground-truth validation, NOT field accuracy.** A skill
|
||||
//! that recovers a planted sinusoid here is proven to do the math it claims on
|
||||
//! a constructed signal; it is NOT proven to work on real CSI in a real room.
|
||||
//!
|
||||
//! Skills whose detection target cannot be honestly planted on synthetic data
|
||||
//! (clinical seizure/apnea/arrhythmia/gait, weapon discrimination, affect/
|
||||
//! emotion/happiness, dream stage, sign language) are **NOT** validated here —
|
||||
//! see RESULTS.md "DATA-GATED" section. Planting a "seizure-like" wiggle and
|
||||
//! claiming the detector works validates nothing real.
|
||||
//!
|
||||
//! Run:
|
||||
//! cargo test --features std --test synthetic_validation -- --nocapture
|
||||
//!
|
||||
//! The printed `MEASURED` lines are the source of `benchmarks/edge-skills/RESULTS.md`.
|
||||
|
||||
#![cfg(feature = "std")]
|
||||
|
||||
use std::f32::consts::PI;
|
||||
|
||||
// ── Confusion-matrix accumulator ─────────────────────────────────────────────
|
||||
|
||||
#[derive(Default, Clone, Copy)]
|
||||
struct Confusion {
|
||||
tp: u32,
|
||||
fp: u32,
|
||||
tn: u32,
|
||||
fn_: u32,
|
||||
}
|
||||
impl Confusion {
|
||||
fn observe(&mut self, predicted_positive: bool, actual_positive: bool) {
|
||||
match (predicted_positive, actual_positive) {
|
||||
(true, true) => self.tp += 1,
|
||||
(true, false) => self.fp += 1,
|
||||
(false, false) => self.tn += 1,
|
||||
(false, true) => self.fn_ += 1,
|
||||
}
|
||||
}
|
||||
fn precision(&self) -> f32 {
|
||||
let d = self.tp + self.fp;
|
||||
if d == 0 {
|
||||
1.0
|
||||
} else {
|
||||
self.tp as f32 / d as f32
|
||||
}
|
||||
}
|
||||
fn recall(&self) -> f32 {
|
||||
let d = self.tp + self.fn_;
|
||||
if d == 0 {
|
||||
1.0
|
||||
} else {
|
||||
self.tp as f32 / d as f32
|
||||
}
|
||||
}
|
||||
fn accuracy(&self) -> f32 {
|
||||
let d = self.tp + self.fp + self.tn + self.fn_;
|
||||
if d == 0 {
|
||||
0.0
|
||||
} else {
|
||||
(self.tp + self.tn) as f32 / d as f32
|
||||
}
|
||||
}
|
||||
fn report(&self, name: &str) {
|
||||
println!(
|
||||
"MEASURED-on-synthetic | {:<34} | acc={:.3} prec={:.3} recall={:.3} | TP={} FP={} TN={} FN={}",
|
||||
name,
|
||||
self.accuracy(),
|
||||
self.precision(),
|
||||
self.recall(),
|
||||
self.tp,
|
||||
self.fp,
|
||||
self.tn,
|
||||
self.fn_
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── 1. vital_trend — rate-threshold detection (directly verified thresholds) ─
|
||||
// Thresholds (from src/vital_trend.rs): BRADYPNEA<12, TACHYPNEA>25,
|
||||
// BRADYCARDIA<50, TACHYCARDIA>120, APNEA at breathing<1.0 for 20 calls;
|
||||
// ALERT_DEBOUNCE=5. Drive on_timer with known BPM, count event presence.
|
||||
|
||||
#[test]
|
||||
fn vital_trend_rate_thresholds() {
|
||||
use wifi_densepose_wasm_edge::vital_trend::VitalTrendAnalyzer;
|
||||
|
||||
// event ids: 101 brady-pnea, 102 tachy-pnea, 103 brady-cardia, 104 tachy-cardia, 105 apnea
|
||||
fn drive_breathing(bpm: f32, n: u32) -> std::collections::HashSet<i32> {
|
||||
let mut det = VitalTrendAnalyzer::new();
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
for _ in 0..n {
|
||||
for &(id, _) in det.on_timer(bpm, 72.0) {
|
||||
seen.insert(id);
|
||||
}
|
||||
}
|
||||
seen
|
||||
}
|
||||
fn drive_heart(bpm: f32, n: u32) -> std::collections::HashSet<i32> {
|
||||
let mut det = VitalTrendAnalyzer::new();
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
for _ in 0..n {
|
||||
for &(id, _) in det.on_timer(16.0, bpm) {
|
||||
seen.insert(id);
|
||||
}
|
||||
}
|
||||
seen
|
||||
}
|
||||
|
||||
// 6 calls > ALERT_DEBOUNCE(5) so a sustained abnormal value fires.
|
||||
let mut c = Confusion::default();
|
||||
// Bradypnea: <12 positive; normal 16 negative.
|
||||
c.observe(drive_breathing(8.0, 6).contains(&101), true);
|
||||
c.observe(drive_breathing(16.0, 6).contains(&101), false);
|
||||
// Tachypnea: >25 positive; normal negative.
|
||||
c.observe(drive_breathing(30.0, 6).contains(&102), true);
|
||||
c.observe(drive_breathing(16.0, 6).contains(&102), false);
|
||||
// Bradycardia: <50.
|
||||
c.observe(drive_heart(40.0, 6).contains(&103), true);
|
||||
c.observe(drive_heart(72.0, 6).contains(&103), false);
|
||||
// Tachycardia: >120.
|
||||
c.observe(drive_heart(140.0, 6).contains(&104), true);
|
||||
c.observe(drive_heart(72.0, 6).contains(&104), false);
|
||||
// Apnea: breathing < 1.0 for >= 20 calls.
|
||||
c.observe(drive_breathing(0.0, 20).contains(&105), true);
|
||||
c.observe(drive_breathing(0.0, 10).contains(&105), false); // only 10 calls -> below APNEA_SECONDS
|
||||
|
||||
c.report("vital_trend (brady/tachy-pnea/cardia, apnea)");
|
||||
// All 5 thresholds + their negatives must classify correctly.
|
||||
assert_eq!(c.accuracy(), 1.0, "vital_trend rate thresholds must be exact");
|
||||
}
|
||||
|
||||
// ── 2. exo_time_crystal — period-doubling (sub-harmonic) detection ───────────
|
||||
// Detects a peak at lag L AND a peak at lag 2L in motion-energy autocorrelation.
|
||||
// PLANT positive: period-2 modulation (alternating amplitude on a base period)
|
||||
// so autocorr has peaks at both L and 2L.
|
||||
// PLANT negative: a single clean period (peak at L only) or noise.
|
||||
|
||||
fn run_time_crystal(motion: &[f32]) -> bool {
|
||||
use wifi_densepose_wasm_edge::exo_time_crystal::TimeCrystalDetector;
|
||||
let mut det = TimeCrystalDetector::new();
|
||||
let mut detected = false;
|
||||
for &m in motion {
|
||||
for &(id, v) in det.process_frame(m) {
|
||||
if id == 680 && v >= 2.0 {
|
||||
detected = true; // CRYSTAL_DETECTED with multiplier 2
|
||||
}
|
||||
}
|
||||
}
|
||||
detected
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exo_time_crystal_period_doubling() {
|
||||
let n = 256usize;
|
||||
// Positive: period-2 subharmonic. Base period P=16; alternate full periods
|
||||
// are scaled differently so the waveform only repeats every 2P=32 (peak at
|
||||
// lag 32) while still correlating at P=16. Plain sine (no abs, which would
|
||||
// itself fold frequency and fake a sub-harmonic).
|
||||
let base_p = 16.0f32;
|
||||
let mut pos = Vec::with_capacity(n);
|
||||
for t in 0..n {
|
||||
let phase = (t as f32) * 2.0 * PI / base_p;
|
||||
let sub = if ((t as f32 / base_p) as i32) % 2 == 0 { 1.0 } else { 0.45 };
|
||||
pos.push(0.6 + 0.35 * phase.sin() * sub);
|
||||
}
|
||||
// HONEST LIMIT (measured below): a *pure* periodic signal already has
|
||||
// autocorrelation peaks at L AND 2L (natural harmonics), so this detector
|
||||
// cannot separate a true period-2 sub-harmonic from a plain periodic signal.
|
||||
// The construct it CAN discriminate with known ground truth is
|
||||
// "periodic-with-coordination vs aperiodic". We validate that.
|
||||
//
|
||||
// Negative 1: incrementing-seed pseudo-noise (no periodicity).
|
||||
let mut noise = Vec::with_capacity(n);
|
||||
let mut s: u32 = 12345;
|
||||
for _ in 0..n {
|
||||
s = s.wrapping_mul(1664525).wrapping_add(1013904223);
|
||||
noise.push(0.3 + 0.4 * ((s >> 8) & 0xffff) as f32 / 65535.0);
|
||||
}
|
||||
// Negative 2: near-constant motion (no oscillation at all).
|
||||
let flat: Vec<f32> = (0..n).map(|t| 0.5 + 1e-4 * (t as f32 * 0.01).sin()).collect();
|
||||
|
||||
let mut c = Confusion::default();
|
||||
c.observe(run_time_crystal(&pos), true); // planted period-2 -> detect
|
||||
c.observe(run_time_crystal(&noise), false); // pseudo-noise -> reject
|
||||
c.observe(run_time_crystal(&flat), false); // flat -> reject
|
||||
c.report("exo_time_crystal (periodic-coordination vs aperiodic)");
|
||||
assert!(
|
||||
run_time_crystal(&pos),
|
||||
"must detect planted period-2 coordinated motion"
|
||||
);
|
||||
assert!(
|
||||
!run_time_crystal(&noise),
|
||||
"must NOT fire on pseudo-noise"
|
||||
);
|
||||
assert!(!run_time_crystal(&flat), "must NOT fire on flat motion");
|
||||
}
|
||||
|
||||
// ── 3. exo_ghost_hunter — hidden breathing (autocorr at breathing-range lag) ─
|
||||
// When presence==0, aggregate phase is autocorrelated at lags 5..=15; a peak
|
||||
// there above HIDDEN_PRESENCE_THRESHOLD(0.3) emits HIDDEN_PRESENCE(652).
|
||||
// PLANT positive: phase sinusoid at a lag in [5,15] across an empty room.
|
||||
// PLANT negative: flat phase (no periodic breathing signature).
|
||||
|
||||
fn run_ghost_hidden_breathing(period: f32, amp: f32, frames: usize) -> f32 {
|
||||
use wifi_densepose_wasm_edge::exo_ghost_hunter::GhostHunterDetector;
|
||||
let mut det = GhostHunterDetector::new();
|
||||
let n_sc = 32usize;
|
||||
let mut max_hidden = 0.0f32;
|
||||
for t in 0..frames {
|
||||
let breath = if period > 0.0 {
|
||||
amp * (t as f32 * 2.0 * PI / period).sin()
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let mut phases = [0.0f32; 32];
|
||||
let mut amps = [0.0f32; 32];
|
||||
let mut vars = [0.0f32; 32];
|
||||
for i in 0..n_sc {
|
||||
// breathing modulates phase uniformly (chest motion -> common phase shift)
|
||||
phases[i] = 0.1 * (i as f32 * 0.2).sin() + breath;
|
||||
amps[i] = 1.0;
|
||||
vars[i] = 0.01;
|
||||
}
|
||||
// presence = 0 (empty room) is required for the hidden-breathing path.
|
||||
for &(id, v) in det.process_frame(&phases, &s, &vars, 0, 0.0) {
|
||||
if id == 652 {
|
||||
if v > max_hidden {
|
||||
max_hidden = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
max_hidden
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exo_ghost_hunter_hidden_breathing() {
|
||||
// Period 8 frames is within the breathing lag window [5,15].
|
||||
let pos = run_ghost_hidden_breathing(8.0, 0.5, 200);
|
||||
// Flat phase (no breathing) -> no hidden-presence event.
|
||||
let neg = run_ghost_hidden_breathing(0.0, 0.0, 200);
|
||||
|
||||
let mut c = Confusion::default();
|
||||
c.observe(pos > 0.0, true);
|
||||
c.observe(neg > 0.0, false);
|
||||
c.report("exo_ghost_hunter (hidden breathing, lag 8)");
|
||||
println!(
|
||||
" detail: planted-breathing hidden-presence score={:.3}, flat-phase score={:.3}",
|
||||
pos, neg
|
||||
);
|
||||
assert!(
|
||||
pos > 0.3,
|
||||
"planted breathing must score above HIDDEN_PRESENCE_THRESHOLD (0.3); got {}",
|
||||
pos
|
||||
);
|
||||
assert!(
|
||||
neg <= 0.0,
|
||||
"flat phase must not emit hidden presence; got {}",
|
||||
neg
|
||||
);
|
||||
}
|
||||
|
||||
// ── 4. occupancy — calibration + variance-driven zone occupancy ──────────────
|
||||
// BASELINE_FRAMES=200 of low-variance amplitudes establish baseline; then
|
||||
// high amplitude-variance per zone (score > ZONE_THRESHOLD=0.02) flips a zone
|
||||
// to occupied (EVENT_ZONE_OCCUPIED=300).
|
||||
|
||||
#[test]
|
||||
fn occupancy_variance_detection() {
|
||||
use wifi_densepose_wasm_edge::occupancy::OccupancyDetector;
|
||||
|
||||
fn run(occupied_signal: bool) -> bool {
|
||||
let mut det = OccupancyDetector::new();
|
||||
let n_sc = 32usize;
|
||||
let mut phases = [0.0f32; 32];
|
||||
// Calibration: 220 frames of near-flat amplitudes (low variance).
|
||||
for t in 0..220 {
|
||||
let mut amps = [1.0f32; 32];
|
||||
for i in 0..n_sc {
|
||||
amps[i] = 1.0 + 1e-3 * ((t + i) as f32 * 0.7).sin();
|
||||
phases[i] = 0.01 * (i as f32).sin();
|
||||
}
|
||||
det.process_frame(&phases, &s);
|
||||
}
|
||||
// Test phase: 60 frames. If occupied, inject strong per-zone amplitude
|
||||
// variance; else keep flat.
|
||||
let mut fired = false;
|
||||
for t in 0..60 {
|
||||
let mut amps = [1.0f32; 32];
|
||||
for i in 0..n_sc {
|
||||
amps[i] = if occupied_signal {
|
||||
// strong structured variance within each zone
|
||||
1.0 + 2.0 * (((i % 4) as f32) - 1.5) + 0.5 * (t as f32 * 0.3 + i as f32).sin()
|
||||
} else {
|
||||
1.0 + 1e-3 * ((t + i) as f32 * 0.7).sin()
|
||||
};
|
||||
}
|
||||
for &(id, _) in det.process_frame(&phases, &s) {
|
||||
if id == 300 {
|
||||
fired = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
fired
|
||||
}
|
||||
|
||||
let mut c = Confusion::default();
|
||||
c.observe(run(true), true);
|
||||
c.observe(run(false), false);
|
||||
c.report("occupancy (zone variance vs flat baseline)");
|
||||
assert!(run(true), "high zone variance after calibration must occupy a zone");
|
||||
assert!(!run(false), "flat amplitude must stay unoccupied");
|
||||
}
|
||||
|
||||
// ── 5. intrusion — calibrate, arm, then disturbance>=0.8 alerts ──────────────
|
||||
// disturbance = 0.6*frac(|Δphase|>1.5) + 0.4*frac(|Δamp|>3σ). Calibrate 200
|
||||
// quiet frames, monitor 100 quiet frames -> Armed, then 3 frames of large
|
||||
// phase+amp disturbance -> EVENT_INTRUSION_ALERT(200).
|
||||
|
||||
#[test]
|
||||
fn intrusion_disturbance_alert() {
|
||||
use wifi_densepose_wasm_edge::intrusion::IntrusionDetector;
|
||||
|
||||
fn run(intrude: bool) -> bool {
|
||||
let mut det = IntrusionDetector::new();
|
||||
let n_sc = 32usize;
|
||||
// Calibration (200) + monitoring quiet (120) -> Armed. Quiet = constant.
|
||||
for _ in 0..330 {
|
||||
let phases = [0.5f32; 32];
|
||||
let amps = [1.0f32; 32];
|
||||
det.process_frame(&phases, &s);
|
||||
}
|
||||
let mut alerted = false;
|
||||
// 10 test frames.
|
||||
for t in 0..10 {
|
||||
let mut phases = [0.5f32; 32];
|
||||
let mut amps = [1.0f32; 32];
|
||||
if intrude {
|
||||
for i in 0..n_sc {
|
||||
// alternate phase by 3.0 (>1.5) and amplitude far from baseline 1.0.
|
||||
phases[i] = if t % 2 == 0 { 0.5 } else { 4.0 };
|
||||
amps[i] = 1.0 + 8.0; // huge deviation vs ~0 baseline variance
|
||||
}
|
||||
}
|
||||
for &(id, _) in det.process_frame(&phases, &s) {
|
||||
if id == 200 {
|
||||
alerted = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
alerted
|
||||
}
|
||||
|
||||
let mut c = Confusion::default();
|
||||
c.observe(run(true), true);
|
||||
c.observe(run(false), false);
|
||||
c.report("intrusion (armed -> disturbance alert vs quiet)");
|
||||
assert!(run(true), "large phase+amplitude disturbance must alert when armed");
|
||||
assert!(!run(false), "quiet environment must not alert");
|
||||
}
|
||||
|
||||
// ── 6. sig_sparse_recovery — ISTA recovery of planted null subcarriers ───────
|
||||
// Initialize correlation on clean frames, then null >10% of subcarriers and
|
||||
// MEASURE how well ISTA recovers them (rate-error style: recovery residual).
|
||||
|
||||
#[test]
|
||||
fn sig_sparse_recovery_recovers_nulls() {
|
||||
use wifi_densepose_wasm_edge::sig_sparse_recovery::SparseRecovery;
|
||||
|
||||
let mut det = SparseRecovery::new();
|
||||
let n_sc = 32usize;
|
||||
// Underlying smooth signal (neighbor-correlated) the model can learn.
|
||||
let truth: Vec<f32> = (0..n_sc).map(|i| 1.0 + 0.5 * (i as f32 * 0.4).sin()).collect();
|
||||
|
||||
// Warm up correlation model with 30 clean frames.
|
||||
for _ in 0..30 {
|
||||
let mut amps: Vec<f32> = truth.clone();
|
||||
det.process_frame(&mut amps);
|
||||
}
|
||||
|
||||
// Null subcarriers 5..13 (8/32 = 25% > MIN_DROPOUT_RATE 0.10).
|
||||
let mut amps: Vec<f32> = truth.clone();
|
||||
let nulled: Vec<usize> = (5..13).collect();
|
||||
for &i in &nulled {
|
||||
amps[i] = 0.0;
|
||||
}
|
||||
// Baseline error if the nulls were left at 0.0 (unrecovered).
|
||||
let mut sse0 = 0.0f32;
|
||||
for &i in &nulled {
|
||||
sse0 += truth[i] * truth[i];
|
||||
}
|
||||
let baseline_rmse = (sse0 / nulled.len() as f32).sqrt();
|
||||
|
||||
let mut recovery_seen = false;
|
||||
for &(id, _) in det.process_frame(&mut amps) {
|
||||
if id == 715 {
|
||||
recovery_seen = true; // RECOVERY_COMPLETE
|
||||
}
|
||||
}
|
||||
// Measure recovery error on the nulled positions (now written back in-place).
|
||||
let mut sse = 0.0f32;
|
||||
for &i in &nulled {
|
||||
let d = amps[i] - truth[i];
|
||||
sse += d * d;
|
||||
}
|
||||
let rmse = (sse / nulled.len() as f32).sqrt();
|
||||
println!(
|
||||
"MEASURED-on-synthetic | {:<34} | dropout-detect+recovery-trigger=PASS | recovered RMSE={:.4} vs unrecovered-null RMSE={:.4} ({:+.1}%) over {} nulled subcarriers",
|
||||
"sig_sparse_recovery (ISTA)",
|
||||
rmse,
|
||||
baseline_rmse,
|
||||
100.0 * (1.0 - rmse / baseline_rmse),
|
||||
nulled.len()
|
||||
);
|
||||
// CONSTRUCTIBLE + MEASURED: the dropout detection and recovery-trigger
|
||||
// pipeline fires correctly on >10% planted nulls. This is the validatable
|
||||
// claim and we assert it.
|
||||
assert!(recovery_seen, "dropout > 10% must trigger ISTA recovery (RECOVERY_COMPLETE)");
|
||||
// HONEST MEASURED RESULT (reported, NOT asserted as a win): on this
|
||||
// neighbor-correlated synthetic signal the tridiagonal-model ISTA recovery
|
||||
// does NOT beat leaving the nulls at zero (RMSE ~1.00 vs ~0.98). The skill's
|
||||
// *recovery accuracy* is therefore NOT validated as effective on synthetic
|
||||
// data — only its dropout-detection/trigger path is. Reported in RESULTS.md.
|
||||
assert!(
|
||||
rmse.is_finite() && rmse < 5.0,
|
||||
"recovered values must be finite and bounded; got {}",
|
||||
rmse
|
||||
);
|
||||
}
|
||||
|
||||
// ── 7. exo_rain_detect — broadband variance onset (empty room) ───────────────
|
||||
// presence=0, MIN_EMPTY_FRAMES=40 baseline, then >=6/8 groups with variance
|
||||
// ratio > 2.5 for ONSET_FRAMES=10 -> EVENT_RAIN_ONSET(660).
|
||||
|
||||
#[test]
|
||||
fn exo_rain_detect_broadband_onset() {
|
||||
use wifi_densepose_wasm_edge::exo_rain_detect::RainDetector;
|
||||
|
||||
fn run(rain: bool) -> bool {
|
||||
let mut det = RainDetector::new();
|
||||
let n_sc = 32usize;
|
||||
let phases = [0.1f32; 32];
|
||||
let amps = [1.0f32; 32];
|
||||
// 60 empty baseline frames with low variance.
|
||||
for _ in 0..60 {
|
||||
let vars = [0.001f32; 32];
|
||||
det.process_frame(&phases, &vars, &s, 0);
|
||||
}
|
||||
let mut onset = false;
|
||||
// 40 frames: broadband-high variance if rain, else stay low.
|
||||
for _ in 0..40 {
|
||||
let vars = if rain { [0.5f32; 32] } else { [0.001f32; 32] };
|
||||
for &(id, _) in det.process_frame(&phases, &vars, &s, 0) {
|
||||
if id == 660 {
|
||||
onset = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
let _ = n_sc;
|
||||
onset
|
||||
}
|
||||
|
||||
let mut c = Confusion::default();
|
||||
c.observe(run(true), true);
|
||||
c.observe(run(false), false);
|
||||
c.report("exo_rain_detect (broadband variance onset)");
|
||||
assert!(run(true), "broadband variance elevation must trigger rain onset");
|
||||
assert!(!run(false), "stable low variance must not trigger rain");
|
||||
}
|
||||
|
||||
// ── 8. sig_flash_attention — peak-attention subcarrier localization ──────────
|
||||
// Q=mean(phase) per group, K=mean(prev_phase), score=Q*K/sqrt(8), softmax peak.
|
||||
// Plant a sustained large phase in a KNOWN group -> assert that group becomes
|
||||
// the reported attention peak (EVENT_ATTENTION_PEAK_SC=700).
|
||||
|
||||
#[test]
|
||||
fn sig_flash_attention_peak_localization() {
|
||||
use wifi_densepose_wasm_edge::sig_flash_attention::FlashAttention;
|
||||
|
||||
fn peak_for_group(target_group: usize) -> i32 {
|
||||
let mut det = FlashAttention::new();
|
||||
let n_sc = 32usize;
|
||||
let subs_per = n_sc / 8;
|
||||
let mut last_peak = -1;
|
||||
// Sustain the spike so both Q (this frame) and K (prev frame) are large
|
||||
// in the target group -> highest score there.
|
||||
for _ in 0..20 {
|
||||
let mut phases = [0.05f32; 32];
|
||||
let mut amps = [1.0f32; 32];
|
||||
for i in (target_group * subs_per)..((target_group + 1) * subs_per) {
|
||||
phases[i] = 3.0;
|
||||
amps[i] = 3.0;
|
||||
}
|
||||
for &(id, v) in det.process_frame(&phases, &s) {
|
||||
if id == 700 {
|
||||
last_peak = v as i32;
|
||||
}
|
||||
}
|
||||
}
|
||||
last_peak
|
||||
}
|
||||
|
||||
let mut correct = 0u32;
|
||||
let total = 8u32;
|
||||
for g in 0..8usize {
|
||||
let got = peak_for_group(g);
|
||||
if got == g as i32 {
|
||||
correct += 1;
|
||||
}
|
||||
println!(" flash_attention: planted group {} -> reported peak {}", g, got);
|
||||
}
|
||||
let acc = correct as f32 / total as f32;
|
||||
println!(
|
||||
"MEASURED-on-synthetic | {:<34} | peak-localization accuracy = {}/{} = {:.3}",
|
||||
"sig_flash_attention", correct, total, acc
|
||||
);
|
||||
assert!(acc >= 0.75, "must localize the planted attention group in >=75% of cases; got {}", acc);
|
||||
}
|
||||
|
||||
// ── 9. spt_spiking_tracker — phase-delta zone localization ───────────────────
|
||||
// LIF neurons fire on |phase - prev_phase|; zone with most spikes is tracked
|
||||
// (EVENT_TRACK_UPDATE=770 carries zone id). Plant motion in a KNOWN zone.
|
||||
|
||||
#[test]
|
||||
fn spt_spiking_tracker_zone_localization() {
|
||||
use wifi_densepose_wasm_edge::spt_spiking_tracker::SpikingTracker;
|
||||
|
||||
fn track_zone(target_zone: usize) -> i32 {
|
||||
let mut det = SpikingTracker::new();
|
||||
let n_sc = 32usize;
|
||||
let per = n_sc / 4; // 4 zones of 8 subcarriers
|
||||
let mut prev = [0.0f32; 32];
|
||||
let mut last_zone = -1;
|
||||
// SPARSE plant: each zone's output neuron sums home-weight 1.0 + cross
|
||||
// 0.25. Firing all 8 inputs (8*0.25=2.0) overdrives EVERY zone, so the
|
||||
// tracker collapses to zone 0. Firing only 2 inputs in the target zone
|
||||
// gives potential 2.0 at home (fires) but 0.5 cross (silent) -> only the
|
||||
// target zone fires. This is the genuinely-constructible localization.
|
||||
let base = target_zone * per;
|
||||
for t in 0..60 {
|
||||
let mut phases = [0.0f32; 32];
|
||||
// 2 subcarriers in the target zone get a large alternating delta.
|
||||
for k in 0..2 {
|
||||
phases[base + k] = if t % 2 == 0 { 0.0 } else { 3.0 };
|
||||
}
|
||||
for &(id, v) in det.process_frame(&phases, &prev) {
|
||||
if id == 770 {
|
||||
last_zone = v as i32;
|
||||
}
|
||||
}
|
||||
prev.copy_from_slice(&phases);
|
||||
}
|
||||
last_zone
|
||||
}
|
||||
|
||||
let mut correct = 0u32;
|
||||
for z in 0..4usize {
|
||||
let got = track_zone(z);
|
||||
if got == z as i32 {
|
||||
correct += 1;
|
||||
}
|
||||
println!(" spiking_tracker: planted zone {} -> tracked zone {}", z, got);
|
||||
}
|
||||
let acc = correct as f32 / 4.0;
|
||||
println!(
|
||||
"MEASURED-on-synthetic | {:<34} | zone-localization accuracy = {}/4 = {:.3}",
|
||||
"spt_spiking_tracker", correct, acc
|
||||
);
|
||||
assert!(acc >= 0.75, "must track the planted motion zone in >=75% of cases; got {}", acc);
|
||||
}
|
||||
|
||||
// ── 10. sig_optimal_transport — distribution-shift detection ─────────────────
|
||||
// Sliced Wasserstein over amplitudes; sustained shift > WASS_SHIFT(0.25) for
|
||||
// SHIFT_DEB(3) -> EVENT_DISTRIBUTION_SHIFT(726). Plant a large vs no shift.
|
||||
|
||||
#[test]
|
||||
fn sig_optimal_transport_distribution_shift() {
|
||||
use wifi_densepose_wasm_edge::sig_optimal_transport::OptimalTransportDetector;
|
||||
|
||||
fn run(shift: bool) -> bool {
|
||||
let mut det = OptimalTransportDetector::new();
|
||||
let n_sc = 32usize;
|
||||
// Establish a reference distribution.
|
||||
let base: Vec<f32> = (0..n_sc).map(|i| i as f32 * 0.1).collect();
|
||||
for _ in 0..10 {
|
||||
let mut a = base.clone();
|
||||
det.process_frame(&mut a);
|
||||
}
|
||||
let mut shifted = false;
|
||||
// The detector compares each frame to the PREVIOUS frame (prev_amps is
|
||||
// updated every frame), so a one-time jump decays. To exceed WASS_SHIFT
|
||||
// (0.25) for SHIFT_DEB(3) consecutive frames we need a sustained large
|
||||
// frame-to-frame change: alternate between two very different
|
||||
// distributions each frame.
|
||||
for t in 0..15 {
|
||||
let mut a: Vec<f32> = if shift {
|
||||
if t % 2 == 0 {
|
||||
base.clone()
|
||||
} else {
|
||||
base.iter().map(|x| 10.0 - x).collect() // reversed + offset
|
||||
}
|
||||
} else {
|
||||
base.clone()
|
||||
};
|
||||
for &(id, _) in det.process_frame(&mut a) {
|
||||
if id == 726 {
|
||||
shifted = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
shifted
|
||||
}
|
||||
|
||||
let mut c = Confusion::default();
|
||||
c.observe(run(true), true);
|
||||
c.observe(run(false), false);
|
||||
c.report("sig_optimal_transport (distribution shift)");
|
||||
assert!(run(true), "large amplitude-distribution shift must be detected");
|
||||
assert!(!run(false), "stationary distribution must not flag a shift");
|
||||
}
|
||||
|
||||
// ── 11. lrn_dtw_gesture_learn — enroll a template, replay match vs reject ────
|
||||
// STILLNESS_FRAMES=60 stillness, then 3 rehearsals of the same gesture
|
||||
// (motion->stillness) -> EVENT_GESTURE_LEARNED(730). Replaying the learned
|
||||
// gesture later (in Idle) -> EVENT_GESTURE_MATCHED(731); replaying a different
|
||||
// gesture -> no match.
|
||||
|
||||
#[test]
|
||||
fn lrn_dtw_gesture_learn_enroll_and_match() {
|
||||
use wifi_densepose_wasm_edge::lrn_dtw_gesture_learn::GestureLearner;
|
||||
|
||||
// A gesture is a phase trajectory across frames; motion_energy gates the
|
||||
// enroll state machine (still < 0.05, moving >= 0.05).
|
||||
fn gesture_frame(kind: u8, step: usize) -> ([f32; 32], f32) {
|
||||
let mut phases = [0.0f32; 32];
|
||||
let s = step as f32;
|
||||
for i in 0..32 {
|
||||
phases[i] = match kind {
|
||||
// distinct trajectories
|
||||
0 => (s * 0.4 + i as f32 * 0.1).sin(),
|
||||
_ => (s * 0.9 + i as f32 * 0.05).cos() * 1.5,
|
||||
};
|
||||
}
|
||||
(phases, 0.5) // moving
|
||||
}
|
||||
|
||||
let mut det = GestureLearner::new();
|
||||
let still = ([0.0f32; 32], 0.0f32);
|
||||
|
||||
// helper to feed N still frames
|
||||
let feed_still = |det: &mut GestureLearner, n: usize| {
|
||||
for _ in 0..n {
|
||||
det.process_frame(&still.0, still.1);
|
||||
}
|
||||
};
|
||||
let feed_gesture = |det: &mut GestureLearner, kind: u8, len: usize| -> bool {
|
||||
let mut learned = false;
|
||||
for s in 0..len {
|
||||
let (ph, me) = gesture_frame(kind, s);
|
||||
for &(id, _) in det.process_frame(&ph, me) {
|
||||
if id == 730 {
|
||||
learned = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
learned
|
||||
};
|
||||
|
||||
// Enroll gesture kind 0: stillness, then 3 identical rehearsals (each
|
||||
// motion burst followed by stillness).
|
||||
feed_still(&mut det, 70);
|
||||
let mut any_learned = false;
|
||||
for _ in 0..3 {
|
||||
any_learned |= feed_gesture(&mut det, 0, 30);
|
||||
feed_still(&mut det, 70);
|
||||
}
|
||||
|
||||
// Replay the SAME gesture during Idle -> expect a match (731).
|
||||
let mut matched_same = false;
|
||||
for s in 0..30 {
|
||||
let (ph, me) = gesture_frame(0, s);
|
||||
for &(id, _) in det.process_frame(&ph, me) {
|
||||
if id == 731 {
|
||||
matched_same = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
feed_still(&mut det, 70);
|
||||
// Replay a DIFFERENT gesture -> ideally no match (731) to the learned one.
|
||||
let mut matched_diff = false;
|
||||
for s in 0..30 {
|
||||
let (ph, me) = gesture_frame(1, s);
|
||||
for &(id, _) in det.process_frame(&ph, me) {
|
||||
if id == 731 {
|
||||
matched_diff = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let tmpl_count = det.template_count();
|
||||
println!(
|
||||
"MEASURED-on-synthetic | {:<34} | learned_event={} templates={} match_same={} match_different={}",
|
||||
"lrn_dtw_gesture_learn", any_learned, tmpl_count, matched_same, matched_diff
|
||||
);
|
||||
// The enroll path must complete (a template is learned from 3 identical
|
||||
// rehearsals). Whether the precise replay matches is the DTW behavior we
|
||||
// measure and report; we assert the deterministic enrollment.
|
||||
assert!(
|
||||
any_learned || tmpl_count > 0,
|
||||
"3 identical rehearsals after stillness must enroll a template"
|
||||
);
|
||||
}
|
||||
|
||||
// ── 12. sig_mincut_person_match — stable id assignment for distinct signatures ─
|
||||
// Per-person feature = top-FEAT_DIM variances in that person's spatial region.
|
||||
// Two persons with DISTINCT, stable variance signatures should get stable ids
|
||||
// (EVENT_PERSON_ID_ASSIGNED=720) with zero swaps across frames.
|
||||
|
||||
#[test]
|
||||
fn sig_mincut_person_stable_ids() {
|
||||
use wifi_densepose_wasm_edge::sig_mincut_person_match::PersonMatcher;
|
||||
|
||||
let mut det = PersonMatcher::new();
|
||||
let n_sc = 32usize;
|
||||
let amplitudes = [1.0f32; 32];
|
||||
let mut swaps = 0u32;
|
||||
let mut assigned = false;
|
||||
|
||||
// 40 frames, 2 persons: person 0 region (0..16) high-variance signature,
|
||||
// person 1 region (16..32) low-variance signature, both stable.
|
||||
for _ in 0..40 {
|
||||
let mut variances = [0.0f32; 32];
|
||||
for i in 0..n_sc {
|
||||
variances[i] = if i < 16 {
|
||||
2.0 + 0.05 * (i as f32).sin()
|
||||
} else {
|
||||
0.2 + 0.01 * (i as f32).cos()
|
||||
};
|
||||
}
|
||||
for &(id, _) in det.process_frame(&litudes, &variances, 2) {
|
||||
if id == 720 {
|
||||
assigned = true;
|
||||
}
|
||||
if id == 721 {
|
||||
swaps += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
println!(
|
||||
"MEASURED-on-synthetic | {:<34} | assigned={} id_swaps_over_40_frames={}",
|
||||
"sig_mincut_person_match", assigned, swaps
|
||||
);
|
||||
assert!(assigned, "distinct stable signatures must assign person ids");
|
||||
assert!(swaps == 0, "stable distinct signatures must not swap ids; got {} swaps", swaps);
|
||||
}
|
||||
Loading…
Reference in New Issue