mirror of
https://github.com/ruvnet/RuView
synced 2026-06-13 10:53:20 +00:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 29e937ef52 | |||
| 41665d3de9 | |||
| c6eacb7ff8 | |||
| 153bc0595b | |||
| 8fd4ee917d | |||
| 5c5112db0e | |||
| e3696da8d8 | |||
| 9457d441b2 | |||
| 626b4b2e97 |
@@ -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.)
|
||||
@@ -0,0 +1,130 @@
|
||||
# ADR-132: HOMECORE-RECORDER — State History + Semantic Search
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Status** | Accepted |
|
||||
| **Date** | 2026-05-25 |
|
||||
| **Deciders** | ruv |
|
||||
| **Codename** | **HOMECORE-RECORDER** |
|
||||
| **Crate** | `v2/crates/homecore-recorder` |
|
||||
| **Relates to** | [ADR-126](ADR-126-ruview-native-ha-port-master.md) (HOMECORE master — series map row ADR-132), [ADR-127](ADR-127-homecore-state-machine-rust.md) (HOMECORE-CORE state machine), [ADR-124](ADR-124-rvagent-mcp-ruvector-npm-integration.md) (ruvector/SENSE-BRIDGE), [ADR-130](ADR-130-homecore-rest-websocket-api.md) (HOMECORE-API query surface, downstream) |
|
||||
| **Tracking issue** | [#800](https://github.com/ruvnet/RuView/pull/800) (HOMECORE intake) |
|
||||
|
||||
> **Documented retroactively (2026-06-12).** The `homecore-recorder` crate shipped under
|
||||
> the ADR-126 series map (which planned an "ADR-132 HOMECORE-RECORDER") but the standalone
|
||||
> ADR file was never written; the crate's `Cargo.toml`, `README.md`, `lib.rs`, `schema.rs`,
|
||||
> and `semantic.rs` all cite "ADR-132". This ADR reverse-documents the decision that the
|
||||
> shipped, tested code already embodies (ADR-164 Gap G3 / Coverage-Gaps Lens §A). It does
|
||||
> **not** introduce new design; it records what is built. Date reflects the crate's intake
|
||||
> era (first commit `e96ebaea8`, 2026-05-25); real-impl pass landed in `7c8071145`
|
||||
> (2026-06-11).
|
||||
|
||||
---
|
||||
|
||||
## 1. Context
|
||||
|
||||
ADR-126 (the HOMECORE master) decided to reimplement Home Assistant (HA) natively in Rust.
|
||||
HA persists every state change to a SQLite *recorder* database; downstream features
|
||||
(history graphs, the logbook, long-term statistics, automation conditions that reference
|
||||
past state) all read that store. HOMECORE therefore needs a durable state-history backbone.
|
||||
|
||||
Two forces shape the decision:
|
||||
|
||||
1. **Migration / coexistence.** Users adopting HOMECORE will have an existing HA
|
||||
`recorder` database. Reusing HA's on-disk schema (rather than inventing a new one) lets
|
||||
HOMECORE read an existing HA `home-assistant_v2.db` directly and lets HA-aware tooling
|
||||
read HOMECORE's store. This is the same trust boundary that `homecore-migrate`
|
||||
(ADR-165) handles for `.storage/*.json`.
|
||||
2. **Semantic queries.** HA history is queried with SQL `BETWEEN`/`WHERE` clauses. The
|
||||
HOMECORE platform already carries ruvector (ADR-124) for vector search, so the recorder
|
||||
can additionally embed state changes and answer natural-language queries
|
||||
("which kitchen devices were warm at 3 PM?") via k-NN — a capability HA does not have.
|
||||
|
||||
The recorder is the **durable-state surface**: if it is wrong, history, logbook, and
|
||||
historical-condition automations are all wrong. ADR-164 flagged it as a CRITICAL coverage
|
||||
gap precisely because such a load-bearing crate had no governing ADR.
|
||||
|
||||
## 2. Decision
|
||||
|
||||
Ship `homecore-recorder` as a SQLite state-history recorder with an HA-compatible schema
|
||||
and an optional ruvector-backed semantic index, in three phases. P1 and P2 are built and
|
||||
tested; P3 is planned.
|
||||
|
||||
### 2.1 Storage — SQLite with the HA recorder schema (P1, shipped)
|
||||
|
||||
- Persist via `sqlx` with the SQLite backend only (no Postgres, no TLS feature set).
|
||||
- Mirror HA recorder **schema v48** so the store is bidirectionally readable
|
||||
(`src/schema.rs`):
|
||||
- `state_attributes` — shared attribute JSON blobs, deduped by an FNV-1a 64-bit hash
|
||||
stored as a signed `i64` (matches HA's dedup key);
|
||||
- `states` — one row per state write (`entity_id`, `state`, `attributes_id` FK,
|
||||
`last_changed_ts`/`last_updated_ts` as REAL Unix seconds, `context_id` UUID);
|
||||
- `events` — domain events (`event_type`, `event_data` JSON, `time_fired_ts`);
|
||||
- `recorder_runs` — boot/shutdown bookends for history-gap detection.
|
||||
- All DDL uses `CREATE TABLE IF NOT EXISTS`, so schema application is idempotent and safe
|
||||
on every startup.
|
||||
- Default persistence path `.homecore/home.db` (configurable).
|
||||
|
||||
### 2.2 Capture — listener on the HOMECORE event bus (P1, shipped)
|
||||
|
||||
- `RecorderListener` subscribes to the HOMECORE event bus (ADR-127) and captures
|
||||
`StateChanged` events, writing snapshots through `Recorder` (`src/listener.rs`,
|
||||
`src/db.rs`).
|
||||
- A `DedupEngine` (`src/dedup.rs`) skips redundant writes when the state hash is unchanged,
|
||||
matching HA's stateful-listener behaviour.
|
||||
|
||||
### 2.3 Semantic search — ruvector HNSW (P2, shipped, feature-gated)
|
||||
|
||||
- Behind the `ruvector` Cargo feature, the `Recorder` additionally calls a `SemanticIndex`
|
||||
implementation (`src/semantic.rs`) that embeds state attributes and stores vectors in a
|
||||
`ruvector-core` HNSW index for k-NN search.
|
||||
- P2 embeddings are **hash-based** (sha2) — a deliberate, honest placeholder. They give a
|
||||
working HNSW surface without claiming sentence-level semantic quality.
|
||||
- When the feature is off, `NullSemanticIndex` satisfies the `SemanticIndex` trait bound
|
||||
with no allocation, so the structural recorder ships independently of ruvector.
|
||||
|
||||
### 2.4 Real sentence embeddings (P3, planned — not yet built)
|
||||
|
||||
- Replace the hash embeddings with ruvector-attention sentence embeddings (dim → 384). Not
|
||||
implemented; tracked as a follow-up. The README and `Cargo.toml` label this P3 explicitly.
|
||||
|
||||
### 2.5 Test evidence (as shipped)
|
||||
|
||||
- P1: 14 tests (`cargo test -p homecore-recorder --no-default-features`).
|
||||
- P2: 20 tests (`cargo test -p homecore-recorder --features ruvector`).
|
||||
|
||||
## 3. Consequences
|
||||
|
||||
**Positive.**
|
||||
|
||||
- HA-schema compatibility makes migration (ADR-165) and coexistence cheap: HOMECORE can
|
||||
read an existing HA `recorder.db`, and any SQLite tool can read HOMECORE's history.
|
||||
- The semantic index is **additive** and feature-gated: the durable structural recorder has
|
||||
no hard dependency on ruvector, so the storage backbone ships first.
|
||||
- Standard SQLite means no proprietary export format; history is directly queryable.
|
||||
|
||||
**Negative / honest limits.**
|
||||
|
||||
- P2 semantic search uses **hash embeddings**, not real sentence embeddings — query quality
|
||||
is limited until P3. This is disclosed in the crate docs and here; it must not be cited as
|
||||
semantic-quality-validated.
|
||||
- No per-crate benchmarks exist yet; the latency figures in the README
|
||||
(state-write p50 < 2 ms, semantic search < 10 ms on 1 M records) are design targets /
|
||||
estimates, **needs verification** with a criterion baseline.
|
||||
- Pinning to HA schema v48 couples HOMECORE to a specific HA recorder schema generation;
|
||||
future HA schema bumps require an explicit migration step.
|
||||
|
||||
**Neutral.**
|
||||
|
||||
- This ADR governs the recorder crate only. The query/REST surface over recorder data is
|
||||
HOMECORE-API (ADR-130, P3); automation conditions on historical state are
|
||||
HOMECORE-automation (ADR-129, P3).
|
||||
|
||||
## 4. Links
|
||||
|
||||
- Crate: `v2/crates/homecore-recorder/` — `Cargo.toml`, `README.md`, `src/lib.rs`,
|
||||
`src/db.rs`, `src/schema.rs`, `src/dedup.rs`, `src/listener.rs`, `src/semantic.rs`.
|
||||
- [ADR-126](ADR-126-ruview-native-ha-port-master.md) — HOMECORE master (series map: ADR-132 = HOMECORE-RECORDER).
|
||||
- [ADR-165](ADR-165-homecore-migrate-from-home-assistant.md) — HOMECORE-MIGRATE (reads HA `.storage`; P2 exports a side-by-side recorder DB).
|
||||
- [ADR-164](ADR-164-adr-corpus-gap-analysis.md) — gap analysis that surfaced this missing ADR (Gap G3).
|
||||
- [Home Assistant Recorder integration](https://www.home-assistant.io/integrations/recorder/).
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Status** | Proposed |
|
||||
| **Status** | Accepted — partial (built + tested building block; integration glue pending — see §8 Implementation Status, commit `11f89727f`) |
|
||||
| **Date** | 2026-05-28 |
|
||||
| **Deciders** | ruv |
|
||||
| **Codebase target** | `wifi-densepose-core` (`types.rs`: `CsiFrame`/`CsiMetadata`); `wifi-densepose-signal/src/ruvsense/mod.rs` (`RuvSensePipeline`, six-stage flow); `v2/Cargo.toml` (workspace topology) |
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Status** | Proposed |
|
||||
| **Status** | Accepted — partial (built + tested building block; integration glue pending — see Implementation Status, commit `4fa3847ac`) |
|
||||
| **Date** | 2026-05-28 |
|
||||
| **Deciders** | ruv |
|
||||
| **Codebase target** | `wifi-densepose-signal` (`ruvsense/multistatic.rs` — `fuse`, `attention_weighted_fusion`); `wifi-densepose-ruvector` (`viewpoint/fusion.rs` — `MultistaticArray`); `wifi-densepose-bfld` (`event.rs`) |
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Status** | Proposed |
|
||||
| **Status** | Accepted — partial (built + tested building block; integration glue pending — see Implementation Status, commit `fc7674bde`) |
|
||||
| **Date** | 2026-05-28 |
|
||||
| **Deciders** | ruv |
|
||||
| **Codebase target** | `wifi-densepose-signal` (`ruvsense/multiband.rs`, `ruvsense/multistatic.rs`); `wifi-densepose-ruvector` (`viewpoint/geometry.rs`, `viewpoint/coherence.rs`, `viewpoint/attention.rs`, `viewpoint/fusion.rs`) |
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Status** | Proposed |
|
||||
| **Status** | Accepted — partial (built + tested building block; integration glue pending — see Implementation Status, commit `521a012d8`) |
|
||||
| **Date** | 2026-05-28 |
|
||||
| **Deciders** | ruv |
|
||||
| **Codebase target** | New module/crate `wifi-densepose-worldgraph` alongside `v2/crates/wifi-densepose-geo` and `v2/crates/homecore`; petgraph bridge pattern from `v2/crates/ruv-neural/ruv-neural-graph/src/petgraph_bridge.rs`; integrates `homecore/src/registry.rs` `area_id` and `wifi-densepose-mat/src/domain/scan_zone.rs` |
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Status** | Proposed |
|
||||
| **Status** | Accepted — partial (built + tested building block; integration glue pending — see Implementation Status, commit `169a355bd`) |
|
||||
| **Date** | 2026-05-28 |
|
||||
| **Deciders** | ruv |
|
||||
| **Codebase target** | `wifi-densepose-sensing-server/src/semantic/` (`bus.rs`, `common.rs`); `homecore/src/state.rs` + `event.rs`; `homecore-assist` |
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Status** | Proposed |
|
||||
| **Status** | Accepted — partial (built + tested building block; integration glue pending — see Implementation Status, commit `7d88eb84c`) |
|
||||
| **Date** | 2026-05-28 |
|
||||
| **Deciders** | ruv |
|
||||
| **Codebase target** | `wifi-densepose-bfld` (new module `mode.rs` + `attestation.rs`; extends `lib.rs` `PrivacyClass`, `sink.rs`, `privacy_gate.rs`, `identity_risk.rs`, `emitter.rs`, `ha_discovery.rs`) |
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Status** | Proposed |
|
||||
| **Status** | Accepted — partial (built + tested building block; integration glue pending — see Implementation Status, commit `1f8e180d6`) |
|
||||
| **Date** | 2026-05-28 |
|
||||
| **Deciders** | ruv |
|
||||
| **Codebase target** | `wifi-densepose-signal` (`ruvsense/longitudinal.rs`, `ruvsense/attractor_drift.rs`, `ruvsense/calibration.rs`, `ruvsense/field_model.rs`, `ruvsense/tomography.rs`); `wifi-densepose-bfld` (`privacy_gate.rs`) |
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Status** | Proposed |
|
||||
| **Status** | Accepted — partial (built + tested building block, v1 fixed-map default; v2 dataset-gated — see Implementation Status, commit `2d4f3dea5`) |
|
||||
| **Date** | 2026-05-28 |
|
||||
| **Deciders** | ruv |
|
||||
| **Codebase target** | `wifi-densepose-signal` (`ruvsense/field_model.rs`, new `ruvsense/rf_slam.rs`); `wifi-densepose-mat` (`tracking/kalman.rs`, `localization/triangulation.rs`); `wifi-densepose-geo`; `wifi-densepose-ruvector` (`mat/triangulation.rs`) |
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Status** | Proposed |
|
||||
| **Status** | Accepted — partial (built + tested building block; no UWB radio in fleet — see Implementation Status, commit `b10bc2e9a`) |
|
||||
| **Date** | 2026-05-28 |
|
||||
| **Deciders** | ruv |
|
||||
| **Codebase target** | `wifi-densepose-hardware` (new UWB driver/parser/auto-detect in `src/`); `wifi-densepose-signal` (`ruvsense/pose_tracker.rs` constraint-aware Kalman update); `wifi-densepose-mat` (`localization/fusion.rs` constraint integration) |
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Status** | Proposed |
|
||||
| **Status** | Accepted — partial (built + tested building block; integration glue pending — see Implementation Status, commit `0f336b7d3`) |
|
||||
| **Date** | 2026-05-28 |
|
||||
| **Deciders** | ruv |
|
||||
| **Codebase target** | `wifi-densepose-train` (`src/eval.rs`, `src/metrics.rs`, `src/ruview_metrics.rs`, `src/proof.rs`); `wifi-densepose-signal` (`src/bin/*_proof_runner.rs`); `wifi-densepose-cli` |
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -51,10 +51,10 @@ Severity: CRITICAL (corpus integrity / tooling-breaking / life-safety / security
|
||||
| ID | Gap | Severity | Affected ADRs | Recommended action |
|
||||
|----|-----|----------|---------------|--------------------|
|
||||
| G1 | 6 duplicate ADR numbers (two ADRs answer to one number; breaks index/`/adr` tooling) | CRITICAL | 050×2, 052×2, 147×3, 148×2, 149×2, 134 (identity split) | renumber 2-of-3 at 147, 1 each at 050/148/149; demote 052-ddd to appendix; resolve 134 identity |
|
||||
| G2 | 3 files with no Status header (cannot triage) | CRITICAL | 147-benchmark-proof, 052-ddd-appendix, 134-CIR | add canonical `## Status`; relocate 147-proof to `benchmarks/`; label 052-ddd as appendix |
|
||||
| G3 | Shipped crates cite a non-existent or wrong-identity governing ADR | CRITICAL | homecore-recorder→"ADR-132" (no file); homecore-migrate→"ADR-134" (file is CIR) | write-missing-ADR (HOMECORE-RECORDER, HOMECORE-MIGRATE) |
|
||||
| G2 | 3 files with no Status header (cannot triage) — **INVESTIGATED in `docs/adr-gap-remediation-1`: only 2 genuinely lack one, both owner-gated** | CRITICAL | 147-benchmark-proof, 052-ddd-appendix, ~~134-CIR~~ | add canonical `## Status`; relocate 147-proof to `benchmarks/`; label 052-ddd as appendix — **NOTE: ADR-134-CIR DOES have a Status (`\| Status \| Proposed \|` in its header table) — mislabeled here. The two real misses (147-benchmark-proof, 052-ddd) are both inside owner-gated duplicate-number collisions (147×3, 052×2), so left untouched pending owner. The early ADRs (048/049/068/070 etc.) use `\| Status \|` not `\| **Status** \|` — different-format-but-present, not missing. Net: 0 headers added.** |
|
||||
| G3 | ~~Shipped crates cite a non-existent or wrong-identity governing ADR~~ **RESOLVED in `docs/adr-gap-remediation-1`** | CRITICAL | homecore-recorder→"ADR-132" (no file); homecore-migrate→"ADR-134" (file is CIR) | ~~write-missing-ADR (HOMECORE-RECORDER, HOMECORE-MIGRATE)~~ DONE: wrote ADR-132 (recorder, Accepted) + ADR-165 (migrate, Accepted — P1 scaffold); repointed migrate's ADR-134 refs → ADR-165 |
|
||||
| G4 | Anti-slop retractions: accuracy/security/function provably false until sweep landed | CRITICAL | 155, 154, 079, 161 (see Contradictions) | already fixed in-code by 154/155/161/162; this ledger records the retraction |
|
||||
| G5 | 10 streaming-engine ADRs marked `Proposed` while §Impl-Status reports Built + commits + tests | HIGH | 136–145 | mark-stale → "Accepted — partial (integration glue pending)" (one batch) |
|
||||
| G5 | ~~10 streaming-engine ADRs marked `Proposed` while §Impl-Status reports Built + commits + tests~~ **RESOLVED in `docs/adr-gap-remediation-1`** | HIGH | 136–145 | ~~mark-stale → "Accepted — partial (integration glue pending)" (one batch)~~ DONE: all 10 (136–145) flipped to "Accepted — partial"; each retains its commit-pinned Implementation-Status note. NB: notes describe *building blocks built + tested*, **not** live-path integration — "partial" is the honest label, not full "Accepted" |
|
||||
| G6 | Stale `Proposed` headers on built+published code | HIGH | 029/030/031, 095/096, 152, 154–157, 024/027/072, 150 | mark-stale; reconcile with downstream/CLAUDE.md evidence |
|
||||
| G7 | Status-graph inversion: Accepted ADR depends on Proposed parent | HIGH | 032→029/030/031; 053→052; 048→045; 077→075/076; 104→103 | promote parents to match built reality, or downgrade dependents |
|
||||
| G8 | ADR-002 supersession not reciprocated by successors; 5 children stranded | HIGH | 002→016/017; children 003/007/008/009/010 | reconcile-docs (add reciprocal language or downgrade); split 002 to "partially superseded" |
|
||||
@@ -91,8 +91,8 @@ The four CRITICAL items are the corpus's load-bearing AI-slop admissions — eac
|
||||
|
||||
## Coverage Gaps (shipped capability, no/broken governing ADR)
|
||||
|
||||
- **CRITICAL — `homecore-recorder`** (SQLite state history + semantic search) cites "ADR-132", which **does not exist**. The durable-state backbone is ungoverned. → write HOMECORE-RECORDER ADR.
|
||||
- **CRITICAL — `homecore-migrate`** (reads untrusted Python-HA `.storage/*.json`) cites "ADR-134", but on-disk ADR-134 is CIR. A data-integrity-sensitive importer governed by a phantom identity. → resolve 134 collision + write HOMECORE-MIGRATE ADR (trust boundary).
|
||||
- ~~**CRITICAL — `homecore-recorder`** (SQLite state history + semantic search) cites "ADR-132", which **does not exist**. The durable-state backbone is ungoverned. → write HOMECORE-RECORDER ADR.~~ **RESOLVED in `docs/adr-gap-remediation-1`:** ADR-132 written (`ADR-132-homecore-recorder-history-semantic-search.md`, Status: Accepted — reverse-documented from the shipped crate).
|
||||
- ~~**CRITICAL — `homecore-migrate`** (reads untrusted Python-HA `.storage/*.json`) cites "ADR-134", but on-disk ADR-134 is CIR. A data-integrity-sensitive importer governed by a phantom identity. → resolve 134 collision + write HOMECORE-MIGRATE ADR (trust boundary).~~ **RESOLVED in `docs/adr-gap-remediation-1`:** ADR-165 written (`ADR-165-homecore-migrate-from-home-assistant.md`, Status: Accepted — P1 scaffold); crate's `ADR-134` refs repointed → ADR-165; on-disk ADR-134 (CIR) left intact. ADR-126's series-map row (which labels the *role* "ADR-134 HOMECORE-MIGRATE") is owner-gated and unchanged.
|
||||
- **HIGH — `wifi-densepose-engine`** composes ADR-135..146 onto the live 20 Hz path but **no ADR governs the integrator contract** (ordering, back-pressure, "one pipeline cycle" boundary).
|
||||
- **MEDIUM — `wasm-edge`** (~70 skills) governed only by remediation ADRs 160/163 — no creation/taxonomy/ABI ADR. **`occworld-candle`** is a Rust-native backend swap ADR-147 explicitly deferred. **`pointcloud`** has only a viewer-deploy ADR (094), no data-format contract.
|
||||
- **MEDIUM — workspace topology:** ~38 crates exist; the CLAUDE.md 15-crate table and 12-step publishing order are stale, and no ADR governs crate-graph/publish boundaries at this scale.
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
# ADR-165: HOMECORE-MIGRATE — Migration Tooling from Python Home Assistant
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Status** | Accepted — P1 scaffold (full conversion deferred to P2) |
|
||||
| **Date** | 2026-05-25 |
|
||||
| **Deciders** | ruv |
|
||||
| **Codename** | **HOMECORE-MIGRATE** |
|
||||
| **Crate** | `v2/crates/homecore-migrate` |
|
||||
| **Relates to** | [ADR-126](ADR-126-ruview-native-ha-port-master.md) (HOMECORE master — series map row "ADR-134 HOMECORE-MIGRATE"), [ADR-127](ADR-127-homecore-state-machine-rust.md) (HOMECORE-CORE), [ADR-132](ADR-132-homecore-recorder-history-semantic-search.md) (HOMECORE-RECORDER — P2 side-by-side export target) |
|
||||
| **Tracking issue** | [#800](https://github.com/ruvnet/RuView/pull/800) (HOMECORE intake) |
|
||||
|
||||
> **Number-collision resolution (2026-06-12).** The HOMECORE series in ADR-126 §4 planned
|
||||
> "ADR-134 = HOMECORE-MIGRATE", and the `homecore-migrate` crate cites "ADR-134" throughout.
|
||||
> But the on-disk `ADR-134-csi-to-cir-time-domain-multipath.md` is a **different, unrelated
|
||||
> decision** (First-Class CIR Support, a signal-processing tier). The migrate crate was
|
||||
> therefore governed by a phantom identity (ADR-164 Gap G3 / Coverage-Gaps Lens §A). This
|
||||
> ADR takes the next free number (**165**) and becomes the real governing record for
|
||||
> HOMECORE-MIGRATE; the `ADR-134` references inside `v2/crates/homecore-migrate/` are
|
||||
> repointed to ADR-165. The real ADR-134 (CIR) is untouched. ADR-126's series-map row still
|
||||
> labels the *role* "ADR-134 HOMECORE-MIGRATE" for historical traceability; that registry
|
||||
> renumber is owner-gated and left for the follow-up. This ADR reverse-documents the shipped
|
||||
> P1 scaffold; it introduces no new design.
|
||||
|
||||
---
|
||||
|
||||
## 1. Context
|
||||
|
||||
ADR-126 decided to reimplement Home Assistant (HA) natively in Rust. A user adopting
|
||||
HOMECORE has an existing HA install whose configuration lives in two places on disk:
|
||||
|
||||
- `.storage/*.json` — versioned JSON envelopes (`{ version, minor_version, data }`) holding
|
||||
the entity registry, device registry, and config entries;
|
||||
- top-level YAML — `secrets.yaml`, `automations.yaml`.
|
||||
|
||||
To migrate, HOMECORE must read this foreign, **untrusted** on-disk state. It is untrusted in
|
||||
the security sense: the schema can drift between HA releases, and silently mis-parsing a
|
||||
registry would corrupt the imported home. ADR-164 flagged this as a CRITICAL coverage gap —
|
||||
a data-integrity-sensitive importer governed by a non-existent ADR identity.
|
||||
|
||||
The decision an ADR must pin here is the **trust boundary and import contract**: which HA
|
||||
files are read, how schema versions are validated, and what happens on an unknown version.
|
||||
|
||||
## 2. Decision
|
||||
|
||||
Ship `homecore-migrate` as a CLI + library that reads an existing HA filesystem and imports
|
||||
its configuration into HOMECORE. P1 is a **scaffold**: it parses and inspects everything and
|
||||
converts the entity registry; full conversion of the remaining artifacts is deferred to P2.
|
||||
|
||||
### 2.1 Storage reader + versioned format gate (P1, shipped)
|
||||
|
||||
- `HaStorageDir` / `HaStorageEnvelope` read HA's `.storage/` directory; `read_envelope(path)`
|
||||
deserializes a `.storage/*.json` envelope (`src/storage.rs`).
|
||||
- Versioned parsers live under `storage_format::v<N>` (e.g. `v13` for the entity registry)
|
||||
(`src/storage_format/`).
|
||||
- **Schema-version validation is the load-bearing safety rule (§6 Q5 of this ADR):** an
|
||||
unknown `minor_version` is a **hard error** (`MigrateError::UnsupportedSchemaVersion`),
|
||||
never a silent best-effort parse. Better to refuse than to corrupt.
|
||||
|
||||
### 2.2 Per-artifact parsers (P1, shipped)
|
||||
|
||||
- `entity_registry::load()` — `core.entity_registry` → `Vec<homecore::EntityEntry>`
|
||||
(ready for import).
|
||||
- `device_registry::load()` — `core.device_registry` → `Vec<DeviceImport>` (P1 diagnostic;
|
||||
full conversion P2).
|
||||
- `config_entries::load()` — `core.config_entries` → domain counts + integration names
|
||||
(the format is undocumented per §6 Q5; treated diagnostically).
|
||||
- `secrets::load_secrets()` — `secrets.yaml` → `HashMap<String, String>` (resolution P2).
|
||||
- `automations::load()` — `automations.yaml` → count + ID/alias list (conversion P2).
|
||||
|
||||
### 2.3 CLI (P1, shipped)
|
||||
|
||||
- `homecore-migrate inspect <ha-dir>` previews what will be migrated (entity/device/config
|
||||
counts, redacted secret/automation lists) (`src/cli.rs`, `src/main.rs`).
|
||||
- `import-entities` and `export-for-sidecar` are declared but their full behaviour is P2.
|
||||
|
||||
### 2.4 Structured errors (P1, shipped)
|
||||
|
||||
- `MigrateError` carries context (`path`, line/field) for I/O, JSON, YAML, missing-field,
|
||||
unsupported-schema-version, and entity-id parse failures (`src/lib.rs`).
|
||||
|
||||
### 2.5 Deferred to P2+ (NOT built — honestly labelled)
|
||||
|
||||
- Convert `config_entries` → HOMECORE plugin manifests.
|
||||
- Convert `automations.yaml` → `homecore-automation` YAML.
|
||||
- Side-by-side runtime mode (requires `homecore-recorder`, ADR-132; behind the `recorder`
|
||||
Cargo feature, currently a no-op stub).
|
||||
- `!secret` reference resolution in non-secrets YAML files.
|
||||
|
||||
### 2.6 Test evidence (as shipped)
|
||||
|
||||
- 19 tests (`cargo test -p homecore-migrate`), per the crate README badge.
|
||||
|
||||
## 3. Consequences
|
||||
|
||||
**Positive.**
|
||||
|
||||
- The trust boundary is explicit: unknown HA schema versions are rejected, not guessed, so a
|
||||
schema drift fails loudly instead of corrupting an imported home.
|
||||
- Reusing HA's own `.storage` and YAML formats means no intermediate export step; the tool
|
||||
reads a live HA install directly.
|
||||
- P1 `inspect` gives users a no-risk dry run before any write.
|
||||
|
||||
**Negative / honest limits.**
|
||||
|
||||
- P1 is a **scaffold**: only the entity registry is conversion-ready. Device registry,
|
||||
config-entry→plugin, automation, and secret-resolution conversions are P2 and **not yet
|
||||
built** — the Status field and crate docs say so.
|
||||
- The side-by-side recorder export depends on ADR-132 and is currently a feature-gated
|
||||
no-op.
|
||||
- Performance figures in the README (envelope parse < 5 ms, 1 000-entity load < 50 ms) are
|
||||
estimates, **needs verification** with a benchmark.
|
||||
|
||||
**Neutral.**
|
||||
|
||||
- This resolves only the *identity* of the migrate decision (134→165). The broader 6-way
|
||||
duplicate-number cleanup (incl. ADR-126's series-map registry row) is owner-gated.
|
||||
|
||||
## 4. Links
|
||||
|
||||
- Crate: `v2/crates/homecore-migrate/` — `Cargo.toml`, `README.md`, `src/lib.rs`,
|
||||
`src/storage.rs`, `src/storage_format/`, `src/entity_registry.rs`,
|
||||
`src/device_registry.rs`, `src/config_entries.rs`, `src/secrets.rs`,
|
||||
`src/automations.rs`, `src/cli.rs`, `src/main.rs`.
|
||||
- [ADR-126](ADR-126-ruview-native-ha-port-master.md) — HOMECORE master (series map: HOMECORE-MIGRATE).
|
||||
- [ADR-132](ADR-132-homecore-recorder-history-semantic-search.md) — HOMECORE-RECORDER (P2 side-by-side export target).
|
||||
- [ADR-134](ADR-134-csi-to-cir-time-domain-multipath.md) — First-Class CIR Support (the *unrelated* decision the crate was mistakenly citing).
|
||||
- [ADR-164](ADR-164-adr-corpus-gap-analysis.md) — gap analysis that surfaced this collision (Gap G3).
|
||||
- [Home Assistant `.storage` format](https://developers.home-assistant.io/docs/storage/).
|
||||
@@ -1,5 +1,6 @@
|
||||
# homecore-migrate — Migration tooling from Python Home Assistant.
|
||||
# Implements ADR-134 (HOMECORE-MIGRATE), P1 scaffold:
|
||||
# Implements ADR-165 (HOMECORE-MIGRATE), P1 scaffold:
|
||||
# (was cited as "ADR-134"; renumbered to ADR-165 — on-disk ADR-134 is CIR. See ADR-164/ADR-165.)
|
||||
# - HaStorageDir + HaStorageEnvelope: reads `.storage/*.json` files
|
||||
# - Versioned format parsers under `storage_format::v<N>`
|
||||
# - entity_registry, device_registry, config_entries parsers
|
||||
@@ -14,7 +15,7 @@ version = "0.1.0-alpha.0"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
authors = ["rUv <ruv@ruv.net>", "HOMECORE Contributors"]
|
||||
description = "Migration tooling from Python Home Assistant to HOMECORE (ADR-134 P1 scaffold)"
|
||||
description = "Migration tooling from Python Home Assistant to HOMECORE (ADR-165 P1 scaffold)"
|
||||
repository = "https://github.com/ruvnet/RuView"
|
||||
|
||||
[[bin]]
|
||||
|
||||
@@ -6,7 +6,7 @@ Migration tooling for importing Home Assistant configuration, entities, and secr
|
||||

|
||||

|
||||
[](https://github.com/ruvnet/RuView)
|
||||
[](../../docs/adr/ADR-134-homecore-migration-from-python-ha.md)
|
||||
[](../../docs/adr/ADR-165-homecore-migrate-from-home-assistant.md)
|
||||
|
||||
Parse and inspect Home Assistant's `.storage/` directory, entity registry, device registry, secrets, and automations. Convert existing HA configurations for import into HOMECORE (full conversion in P2).
|
||||
|
||||
@@ -22,7 +22,7 @@ Parse and inspect Home Assistant's `.storage/` directory, entity registry, devic
|
||||
- **Automations parser** — reads `automations.yaml` and counts/lists automations (full conversion in P2)
|
||||
- **CLI binary** — `homecore-migrate inspect` to preview what will be migrated
|
||||
|
||||
The tool enforces version schema compatibility: unknown HA schema versions are rejected (hard error per ADR-134 §6 Q5) rather than silently corrupting data.
|
||||
The tool enforces version schema compatibility: unknown HA schema versions are rejected (hard error per ADR-165 §6 Q5) rather than silently corrupting data.
|
||||
|
||||
## Features
|
||||
|
||||
@@ -136,7 +136,7 @@ homecore-migrate (import from HA)
|
||||
|
||||
## References
|
||||
|
||||
- [ADR-134: HOMECORE Migration from Python Home Assistant](../../docs/adr/ADR-134-homecore-migration-from-python-ha.md)
|
||||
- [ADR-165: HOMECORE Migration from Python Home Assistant](../../docs/adr/ADR-165-homecore-migrate-from-home-assistant.md)
|
||||
- [ADR-126: HOMECORE Home Assistant Port (master)](../../docs/adr/ADR-126-homecore-home-assistant-port.md)
|
||||
- [Home Assistant .storage/ format](https://developers.home-assistant.io/docs/storage/)
|
||||
- [homecore-migrate CLI source](src/main.rs)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Parser for `core.config_entries` (HA storage schema v1, minor_version varies).
|
||||
//!
|
||||
//! Per ADR-134 §6 Q5, `.storage/core.config_entries` format is undocumented
|
||||
//! Per ADR-165 §6 Q5, `.storage/core.config_entries` format is undocumented
|
||||
//! and version-gated. P1 reads the envelope and emits:
|
||||
//! - count of config entries
|
||||
//! - list of integration domains represented
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
//! homecore-migrate — Migration tooling from Python Home Assistant.
|
||||
//!
|
||||
//! Implements [ADR-134](../../docs/adr/ADR-134-homecore-migration-from-python-ha.md)
|
||||
//! (referenced via ADR-126 §4, series map row ADR-134 HOMECORE-MIGRATE).
|
||||
//! Implements [ADR-165](../../docs/adr/ADR-165-homecore-migrate-from-home-assistant.md)
|
||||
//! (HOMECORE-MIGRATE; ADR-126 §4 series map labels the role "ADR-134 HOMECORE-MIGRATE",
|
||||
//! but on-disk ADR-134 is CIR — the migrate decision was renumbered to ADR-165. See ADR-164).
|
||||
//!
|
||||
//! ## P1 scope
|
||||
//!
|
||||
@@ -56,7 +57,7 @@ pub enum MigrateError {
|
||||
|
||||
/// Fired when the outer `{version, minor_version}` envelope version is
|
||||
/// known but the `minor_version` is not supported by any compiled parser.
|
||||
/// Per ADR-134 §6 Q5: hard error on unknown minor_version.
|
||||
/// Per ADR-165 §6 Q5: hard error on unknown minor_version.
|
||||
#[error(
|
||||
"unsupported schema version in {file}: \
|
||||
version={version} minor_version={minor_version}. \
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
//! adding a new `v<N>.rs` module; the dispatch function in each parser module
|
||||
//! routes to the right implementation.
|
||||
//!
|
||||
//! Per ADR-134 §6 Q5: unknown `minor_version` values produce a hard
|
||||
//! Per ADR-165 §6 Q5: unknown `minor_version` values produce a hard
|
||||
//! `MigrateError::UnsupportedSchemaVersion` — we do NOT silently fall back
|
||||
//! to an older parser, because schema changes can be load-bearing (new fields,
|
||||
//! renamed keys, semantic reinterpretations).
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
//! Runnable demo of the unified [`EdgePipeline`]: constructs every registered
|
||||
//! skill, feeds a short deterministic synthetic CSI frame sequence, and prints
|
||||
//! the per-skill events plus a registration summary.
|
||||
//!
|
||||
//! ```bash
|
||||
//! cd v2/crates/wifi-densepose-wasm-edge
|
||||
//! cargo run --example run_all_skills --features std
|
||||
//! cargo run --example run_all_skills --features std,medical-experimental
|
||||
//! ```
|
||||
//!
|
||||
//! [`EdgePipeline`]: wifi_densepose_wasm_edge::pipeline_all::EdgePipeline
|
||||
|
||||
#[cfg(not(feature = "std"))]
|
||||
fn main() {
|
||||
eprintln!("run_all_skills requires --features std");
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
fn main() {
|
||||
use std::collections::BTreeMap;
|
||||
use wifi_densepose_wasm_edge::pipeline_all::{CsiFrameView, EdgePipeline};
|
||||
|
||||
const N_SC: usize = 32;
|
||||
let mut pipeline = EdgePipeline::new();
|
||||
|
||||
println!("=== EdgePipeline registration ===");
|
||||
println!("registered skills: {}", pipeline.skill_count());
|
||||
let med = pipeline
|
||||
.skills()
|
||||
.iter()
|
||||
.filter(|s| s.medical_experimental)
|
||||
.count();
|
||||
println!(
|
||||
" default tier: {} medical-experimental tier: {}",
|
||||
pipeline.skill_count() - med,
|
||||
med
|
||||
);
|
||||
println!();
|
||||
|
||||
let mut phases = [0.0f32; N_SC];
|
||||
let mut amps = [0.0f32; N_SC];
|
||||
let mut vars = [0.0f32; N_SC];
|
||||
let mut prev = [0.0f32; N_SC];
|
||||
|
||||
// Per-skill event counters over the run.
|
||||
let mut counts: BTreeMap<&'static str, usize> = BTreeMap::new();
|
||||
for s in pipeline.skills() {
|
||||
counts.insert(s.name, 0);
|
||||
}
|
||||
|
||||
let frames = 300usize;
|
||||
for t in 0..frames {
|
||||
let tf = t as f32;
|
||||
let breath = (tf * 2.0 * std::f32::consts::PI * 0.3 / 20.0).sin();
|
||||
let heart = (tf * 2.0 * std::f32::consts::PI * 1.2 / 20.0).sin();
|
||||
let mut vmean = 0.0f32;
|
||||
for i in 0..N_SC {
|
||||
let sc = i as f32;
|
||||
phases[i] = (sc * 0.21 + tf * 0.05).sin() + 0.15 * breath;
|
||||
amps[i] = 1.0 + 0.3 * (sc * 0.11 + tf * 0.03).cos() + 0.1 * heart;
|
||||
vars[i] = 0.02 + 0.01 * (sc * 0.3).sin().abs()
|
||||
+ if (t / 40) % 2 == 0 { 0.05 } else { 0.0 };
|
||||
vmean += vars[i];
|
||||
}
|
||||
vmean /= N_SC as f32;
|
||||
|
||||
let v = CsiFrameView {
|
||||
phases: &phases,
|
||||
amplitudes: &s,
|
||||
variances: &vars,
|
||||
prev_phases: &prev,
|
||||
presence: if (t / 30) % 3 == 0 { 0 } else { 1 },
|
||||
n_persons: ((t / 50) % 3) as i32,
|
||||
motion_energy: 0.3 + 0.2 * (tf * 0.07).sin().abs(),
|
||||
breathing_bpm: 18.0 + 2.0 * (tf * 0.01).sin(),
|
||||
heartrate_bpm: 72.0 + 5.0 * (tf * 0.02).sin(),
|
||||
coherence: 0.5 + 0.4 * (tf * 0.03).cos(),
|
||||
variance_mean: vmean,
|
||||
};
|
||||
|
||||
for e in pipeline.on_frame(&v) {
|
||||
*counts.entry(e.skill).or_insert(0) += 1;
|
||||
// Print the first few events from the last frame to show liveness.
|
||||
if t == frames - 1 {
|
||||
println!(
|
||||
" frame {} | {:<26} event {:>3} = {:.4}",
|
||||
t, e.skill, e.event_id, e.value
|
||||
);
|
||||
}
|
||||
}
|
||||
prev.copy_from_slice(&phases);
|
||||
}
|
||||
|
||||
println!();
|
||||
println!("=== per-skill event totals over {} synthetic frames ===", frames);
|
||||
let total: usize = counts.values().sum();
|
||||
let active = counts.values().filter(|&&c| c > 0).count();
|
||||
for (name, c) in &counts {
|
||||
println!(" {:<28} {}", name, c);
|
||||
}
|
||||
println!();
|
||||
println!(
|
||||
"TOTAL events: {} skills that emitted at least once: {}/{}",
|
||||
total,
|
||||
active,
|
||||
pipeline.skill_count()
|
||||
);
|
||||
}
|
||||
@@ -94,6 +94,18 @@ pub mod ind_structural_vibration;
|
||||
|
||||
pub mod vendor_common;
|
||||
|
||||
// ── Unified edge pipeline (ADR-160 deliverable) ──────────────────────────────
|
||||
//
|
||||
// `EdgePipeline` registers EVERY runtime skill module behind one uniform
|
||||
// `EdgeSkill` trait and runs them all per CSI frame. Host-only (`std`): it uses
|
||||
// Box/Vec for dynamic dispatch; the wasm `no_std` build keeps the small flagship
|
||||
// pipeline in this file. The `med_*` tier is registered only under
|
||||
// `medical-experimental` (preserves the ADR-160 safety gate).
|
||||
#[cfg(feature = "std")]
|
||||
pub mod pipeline_all;
|
||||
#[cfg(feature = "std")]
|
||||
pub mod skill_registry;
|
||||
|
||||
// ── Vendor-integrated modules (ADR-041 Category 7) ──────────────────────────
|
||||
//
|
||||
// 24 modules organised into 7 sub-categories. Each module file lives in
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
//! Unified edge pipeline — registers **every** runtime skill module in the crate
|
||||
//! behind one uniform [`EdgeSkill`] trait and runs them all per CSI frame.
|
||||
//!
|
||||
//! # Why this module exists
|
||||
//!
|
||||
//! Each skill in `src/*.rs` is an independently-loadable DSP module with its own
|
||||
//! bespoke `process_frame` / `on_timer` signature (some take `&[f32]` phases,
|
||||
//! some scalars like `motion_energy`, some `breathing_bpm`/`heartrate_bpm`, etc.).
|
||||
//! On the wasm target only the flagship `gesture + coherence + adversarial`
|
||||
//! pipeline (in `lib.rs`) is on the default `on_frame` path. This module wires
|
||||
//! **all** of them into a single [`EdgePipeline`] so a host can run the whole
|
||||
//! skill library over one CSI frame stream and collect every emitted event,
|
||||
//! tagged by its source skill.
|
||||
//!
|
||||
//! # Design
|
||||
//!
|
||||
//! - [`CsiFrameView`] — a borrowed, host-supplied view of one CSI frame carrying
|
||||
//! every input any skill needs (phase/amplitude/variance slices + the scalar
|
||||
//! features the host derives: presence, n_persons, motion_energy, breathing &
|
||||
//! heart rate, coherence, plus the previous frame's phases for delta skills).
|
||||
//! - [`EdgeSkill`] — the uniform adapter trait. Each skill gets a small adapter
|
||||
//! (see `skill_registry`) that pulls the fields it needs out of the view, calls
|
||||
//! the underlying detector **unchanged**, and returns an aggregated
|
||||
//! `&[(i32, f32)]` event buffer. **No skill DSP is modified.**
|
||||
//! - [`EdgePipeline`] — owns one boxed adapter per skill, dispatches `on_frame`
|
||||
//! to all of them, and aggregates `(skill_name, event_id, value)` triples.
|
||||
//!
|
||||
//! # Feature gating (preserves the ADR-160 safety gate)
|
||||
//!
|
||||
//! The five `med_*` skills are registered **only** under
|
||||
//! `--features medical-experimental`. They are NOT pulled into the default
|
||||
//! pipeline, so they cannot be silently built into a shipping artifact. The
|
||||
//! medical tier is opt-in; see `EdgePipeline::new` and `skills()`.
|
||||
//!
|
||||
//! Requires `std` (uses `Box`/`Vec`); the wasm `no_std` build keeps the small
|
||||
//! flagship `lib.rs` pipeline instead.
|
||||
|
||||
#![cfg(feature = "std")]
|
||||
|
||||
extern crate std;
|
||||
use std::boxed::Box;
|
||||
use std::vec::Vec;
|
||||
|
||||
/// Borrowed view of one CSI frame: every input any registered skill can consume.
|
||||
///
|
||||
/// The host derives these from the Tier-2 DSP output. Slices are
|
||||
/// per-subcarrier; scalars are frame-level aggregates. A skill adapter reads
|
||||
/// only the fields it needs and ignores the rest — heterogeneity is absorbed
|
||||
/// here, not in the skills.
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct CsiFrameView<'a> {
|
||||
/// Per-subcarrier unwrapped phase (radians).
|
||||
pub phases: &'a [f32],
|
||||
/// Per-subcarrier amplitude (linear).
|
||||
pub amplitudes: &'a [f32],
|
||||
/// Per-subcarrier short-window variance.
|
||||
pub variances: &'a [f32],
|
||||
/// Previous frame's phases (for delta/velocity skills like the spiking tracker).
|
||||
pub prev_phases: &'a [f32],
|
||||
/// Presence flag from host (0 = empty, 1 = occupied).
|
||||
pub presence: i32,
|
||||
/// Estimated person count from host.
|
||||
pub n_persons: i32,
|
||||
/// Frame-level motion energy.
|
||||
pub motion_energy: f32,
|
||||
/// Breathing rate estimate (breaths/min); 0 if unavailable.
|
||||
pub breathing_bpm: f32,
|
||||
/// Heart rate estimate (beats/min); 0 if unavailable.
|
||||
pub heartrate_bpm: f32,
|
||||
/// Coherence score [0,1] from the coherence monitor (for gate-style skills).
|
||||
pub coherence: f32,
|
||||
/// Mean variance across `variances` (convenience scalar for skills wanting one).
|
||||
pub variance_mean: f32,
|
||||
}
|
||||
|
||||
impl<'a> CsiFrameView<'a> {
|
||||
/// Mean amplitude across the frame (convenience for scalar-input skills).
|
||||
#[inline]
|
||||
pub fn amplitude_mean(&self) -> f32 {
|
||||
if self.amplitudes.is_empty() {
|
||||
return 0.0;
|
||||
}
|
||||
let mut s = 0.0f32;
|
||||
for &a in self.amplitudes {
|
||||
s += a;
|
||||
}
|
||||
s / self.amplitudes.len() as f32
|
||||
}
|
||||
|
||||
/// Mean phase across the frame.
|
||||
#[inline]
|
||||
pub fn phase_mean(&self) -> f32 {
|
||||
if self.phases.is_empty() {
|
||||
return 0.0;
|
||||
}
|
||||
let mut s = 0.0f32;
|
||||
for &p in self.phases {
|
||||
s += p;
|
||||
}
|
||||
s / self.phases.len() as f32
|
||||
}
|
||||
}
|
||||
|
||||
/// One emitted event, tagged by its source skill.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct SkillEvent {
|
||||
/// Stable name of the skill that produced this event (e.g. `"occupancy"`).
|
||||
pub skill: &'static str,
|
||||
/// Event type id (the registry id from `event_types`).
|
||||
pub event_id: i32,
|
||||
/// Event payload value.
|
||||
pub value: f32,
|
||||
}
|
||||
|
||||
/// Uniform adapter trait over a heterogeneous skill detector.
|
||||
///
|
||||
/// Implementors live in `skill_registry`; each wraps exactly one underlying
|
||||
/// detector and forwards `on_frame` to its real `process_frame`/`on_timer`
|
||||
/// without changing the DSP. `event_ids()` is introspection only.
|
||||
pub trait EdgeSkill {
|
||||
/// Stable skill name (matches the `src/<name>.rs` module).
|
||||
fn name(&self) -> &'static str;
|
||||
/// The event ids this skill can emit (for introspection / docs).
|
||||
fn event_ids(&self) -> &'static [i32];
|
||||
/// Run this skill over one frame, returning its emitted `(event_id, value)`
|
||||
/// pairs. Returns an empty slice if the skill emitted nothing this frame.
|
||||
fn on_frame(&mut self, frame: &CsiFrameView) -> &[(i32, f32)];
|
||||
}
|
||||
|
||||
/// Introspection record for one registered skill.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct SkillInfo {
|
||||
/// Skill name.
|
||||
pub name: &'static str,
|
||||
/// Event ids the skill can emit.
|
||||
pub event_ids: &'static [i32],
|
||||
/// Whether the skill is part of the gated `medical-experimental` tier.
|
||||
pub medical_experimental: bool,
|
||||
}
|
||||
|
||||
/// The unified pipeline: holds one adapter per registered skill and runs them
|
||||
/// all per frame.
|
||||
pub struct EdgePipeline {
|
||||
skills: Vec<Box<dyn EdgeSkill>>,
|
||||
/// Parallel flag marking which entries are the gated medical tier.
|
||||
medical_flags: Vec<bool>,
|
||||
frame_count: u64,
|
||||
}
|
||||
|
||||
impl EdgePipeline {
|
||||
/// Construct the pipeline with **every** registered skill.
|
||||
///
|
||||
/// The five `med_*` skills are included **only** when the crate is built
|
||||
/// with `--features medical-experimental`; otherwise the default
|
||||
/// (non-medical) tier is registered. This preserves the ADR-160 safety gate.
|
||||
pub fn new() -> Self {
|
||||
let mut skills: Vec<Box<dyn EdgeSkill>> = Vec::new();
|
||||
let mut medical_flags: Vec<bool> = Vec::new();
|
||||
|
||||
crate::skill_registry::register_default(&mut skills, &mut medical_flags);
|
||||
#[cfg(feature = "medical-experimental")]
|
||||
crate::skill_registry::register_medical(&mut skills, &mut medical_flags);
|
||||
|
||||
Self {
|
||||
skills,
|
||||
medical_flags,
|
||||
frame_count: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Number of registered skills (default tier, or +medical if that feature is on).
|
||||
pub fn skill_count(&self) -> usize {
|
||||
self.skills.len()
|
||||
}
|
||||
|
||||
/// Run every registered skill over one frame, aggregating all emitted events
|
||||
/// tagged by source skill. Order matches registration order.
|
||||
pub fn on_frame(&mut self, frame: &CsiFrameView) -> Vec<SkillEvent> {
|
||||
self.frame_count += 1;
|
||||
let mut out: Vec<SkillEvent> = Vec::new();
|
||||
for skill in self.skills.iter_mut() {
|
||||
let name = skill.name();
|
||||
for &(event_id, value) in skill.on_frame(frame) {
|
||||
out.push(SkillEvent {
|
||||
skill: name,
|
||||
event_id,
|
||||
value,
|
||||
});
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Total frames processed so far.
|
||||
pub fn frame_count(&self) -> u64 {
|
||||
self.frame_count
|
||||
}
|
||||
|
||||
/// Introspection: list every registered skill with its event ids and tier.
|
||||
pub fn skills(&self) -> Vec<SkillInfo> {
|
||||
let mut out = Vec::with_capacity(self.skills.len());
|
||||
for (i, skill) in self.skills.iter().enumerate() {
|
||||
out.push(SkillInfo {
|
||||
name: skill.name(),
|
||||
event_ids: skill.event_ids(),
|
||||
medical_experimental: self.medical_flags.get(i).copied().unwrap_or(false),
|
||||
});
|
||||
}
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for EdgePipeline {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,630 @@
|
||||
//! Adapters wiring every runtime skill detector to the uniform [`EdgeSkill`]
|
||||
//! trait, plus the registration functions consumed by [`EdgePipeline::new`].
|
||||
//!
|
||||
//! [`EdgePipeline::new`]: crate::pipeline_all::EdgePipeline::new
|
||||
//! [`EdgeSkill`]: crate::pipeline_all::EdgeSkill
|
||||
//!
|
||||
//! # How adapters work
|
||||
//!
|
||||
//! Each underlying detector keeps its own bespoke `process_frame`/`on_timer`
|
||||
//! signature and its owned `events: [(i32,f32); N]` buffer (the ADR-160 M6
|
||||
//! soundness fix). An adapter holds the detector, implements [`EdgeSkill`], and
|
||||
//! in `on_frame` simply pulls the needed fields out of [`CsiFrameView`] and
|
||||
//! forwards the call **unchanged**. The detector returns `&self.events[..n]`;
|
||||
//! the adapter forwards that borrow directly, so no extra buffer or copy is
|
||||
//! needed for the common case.
|
||||
//!
|
||||
//! Three families need a small owned scratch buffer in the adapter instead of a
|
||||
//! direct forward, because the underlying entry point does not itself return a
|
||||
//! `&[(i32,f32)]`:
|
||||
//! - `gesture` (`-> Option<u8>`), `coherence` (`-> f32`), `adversarial`
|
||||
//! (`-> bool`): the adapter synthesizes a single tagged event.
|
||||
//! - `sig_sparse_recovery` (`process_frame(&mut [f32])`): the adapter copies the
|
||||
//! frame amplitudes into an owned scratch slice so the in-place ISTA recovery
|
||||
//! never mutates the shared frame, then forwards the borrow.
|
||||
//! - timer-driven skills (`vital_trend`, `lrn_meta_adapt`, `sig_temporal_compress`,
|
||||
//! `tmp_goap_autonomy`, `tmp_pattern_sequence`): their `on_timer()` is driven
|
||||
//! once per frame here (a frame *is* the tick at the edge), forwarding the
|
||||
//! borrow. `tmp_pattern_sequence` additionally calls its `on_frame(...)`
|
||||
//! accumulator first.
|
||||
//!
|
||||
//! **No skill's DSP is changed.** Only the call wiring lives here.
|
||||
|
||||
#![cfg(feature = "std")]
|
||||
|
||||
extern crate std;
|
||||
use std::boxed::Box;
|
||||
use std::vec::Vec;
|
||||
|
||||
use crate::pipeline_all::{CsiFrameView, EdgeSkill};
|
||||
|
||||
// ── Direct-forward adapter macro ─────────────────────────────────────────────
|
||||
//
|
||||
// Generates an adapter whose `on_frame` forwards directly to a detector method
|
||||
// that already returns `&[(i32, f32)]`. `$call` is an expression over `self.0`
|
||||
// (the detector) and `f` (the `&CsiFrameView`).
|
||||
macro_rules! fwd_skill {
|
||||
($adapter:ident, $detector:path, $name:literal, $ids:expr, |$d:ident, $f:ident| $call:expr) => {
|
||||
pub struct $adapter($detector);
|
||||
impl $adapter {
|
||||
pub fn new() -> Self {
|
||||
Self(<$detector>::new())
|
||||
}
|
||||
}
|
||||
impl EdgeSkill for $adapter {
|
||||
fn name(&self) -> &'static str {
|
||||
$name
|
||||
}
|
||||
fn event_ids(&self) -> &'static [i32] {
|
||||
&$ids
|
||||
}
|
||||
fn on_frame(&mut self, $f: &CsiFrameView) -> &[(i32, f32)] {
|
||||
let $d = &mut self.0;
|
||||
$call
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// ── Synthesized-event adapter macro ──────────────────────────────────────────
|
||||
//
|
||||
// For detectors whose entry point does NOT return `&[(i32, f32)]`. The adapter
|
||||
// owns a tiny scratch buffer; `$body` (over `self`, `f`, and `self.buf`/`self.n`)
|
||||
// fills it and the trait returns the filled prefix.
|
||||
macro_rules! synth_skill {
|
||||
($adapter:ident, $detector:path, $name:literal, $ids:expr, $buf:literal,
|
||||
|$s:ident, $f:ident| $body:block) => {
|
||||
pub struct $adapter {
|
||||
det: $detector,
|
||||
buf: [(i32, f32); $buf],
|
||||
n: usize,
|
||||
}
|
||||
impl $adapter {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
det: <$detector>::new(),
|
||||
buf: [(0, 0.0); $buf],
|
||||
n: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl EdgeSkill for $adapter {
|
||||
fn name(&self) -> &'static str {
|
||||
$name
|
||||
}
|
||||
fn event_ids(&self) -> &'static [i32] {
|
||||
&$ids
|
||||
}
|
||||
fn on_frame(&mut self, $f: &CsiFrameView) -> &[(i32, f32)] {
|
||||
let $s = self;
|
||||
$s.n = 0;
|
||||
$body
|
||||
&$s.buf[..$s.n]
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
use crate::event_types as ev;
|
||||
|
||||
// ── Flagship (synthesized) ───────────────────────────────────────────────────
|
||||
|
||||
synth_skill!(GestureAdapter, crate::gesture::GestureDetector, "gesture",
|
||||
[ev::GESTURE_DETECTED], 1, |s, f| {
|
||||
if let Some(id) = s.det.process_frame(f.phases) {
|
||||
s.buf[0] = (ev::GESTURE_DETECTED, id as f32);
|
||||
s.n = 1;
|
||||
}
|
||||
});
|
||||
|
||||
synth_skill!(CoherenceAdapter, crate::coherence::CoherenceMonitor, "coherence",
|
||||
[ev::COHERENCE_SCORE], 1, |s, f| {
|
||||
let score = s.det.process_frame(f.phases);
|
||||
s.buf[0] = (ev::COHERENCE_SCORE, score);
|
||||
s.n = 1;
|
||||
});
|
||||
|
||||
synth_skill!(AdversarialAdapter, crate::adversarial::AnomalyDetector, "adversarial",
|
||||
[ev::ANOMALY_DETECTED], 1, |s, f| {
|
||||
if s.det.process_frame(f.phases, f.amplitudes) {
|
||||
s.buf[0] = (ev::ANOMALY_DETECTED, 1.0);
|
||||
s.n = 1;
|
||||
}
|
||||
});
|
||||
|
||||
// ── sig_sparse_recovery (needs owned mutable amplitude scratch) ───────────────
|
||||
|
||||
const SPARSE_SC: usize = 64;
|
||||
pub struct SparseRecoveryAdapter {
|
||||
det: crate::sig_sparse_recovery::SparseRecovery,
|
||||
scratch: [f32; SPARSE_SC],
|
||||
}
|
||||
impl SparseRecoveryAdapter {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
det: crate::sig_sparse_recovery::SparseRecovery::new(),
|
||||
scratch: [0.0; SPARSE_SC],
|
||||
}
|
||||
}
|
||||
}
|
||||
impl EdgeSkill for SparseRecoveryAdapter {
|
||||
fn name(&self) -> &'static str {
|
||||
"sig_sparse_recovery"
|
||||
}
|
||||
fn event_ids(&self) -> &'static [i32] {
|
||||
&[ev::RECOVERY_COMPLETE, ev::RECOVERY_ERROR, ev::DROPOUT_RATE]
|
||||
}
|
||||
fn on_frame(&mut self, f: &CsiFrameView) -> &[(i32, f32)] {
|
||||
let n = f.amplitudes.len().min(SPARSE_SC);
|
||||
self.scratch[..n].copy_from_slice(&f.amplitudes[..n]);
|
||||
self.det.process_frame(&mut self.scratch[..n])
|
||||
}
|
||||
}
|
||||
|
||||
// ── Standard direct-forward skills (return &[(i32,f32)]) ─────────────────────
|
||||
|
||||
fwd_skill!(AisBehavioralAdapter, crate::ais_behavioral_profiler::BehavioralProfiler,
|
||||
"ais_behavioral_profiler",
|
||||
[ev::BEHAVIOR_ANOMALY, ev::PROFILE_DEVIATION, ev::NOVEL_PATTERN, ev::PROFILE_MATURITY],
|
||||
|d, f| d.process_frame(f.presence != 0, f.motion_energy, f.n_persons.max(0) as u8));
|
||||
|
||||
fwd_skill!(AisPromptShieldAdapter, crate::ais_prompt_shield::PromptShield,
|
||||
"ais_prompt_shield",
|
||||
[ev::REPLAY_ATTACK, ev::INJECTION_DETECTED, ev::JAMMING_DETECTED, ev::SIGNAL_INTEGRITY],
|
||||
|d, f| d.process_frame(f.phases, f.amplitudes));
|
||||
|
||||
fwd_skill!(AutPsychoAdapter, crate::aut_psycho_symbolic::PsychoSymbolicEngine,
|
||||
"aut_psycho_symbolic",
|
||||
[ev::INFERENCE_RESULT, ev::INFERENCE_CONFIDENCE, ev::RULE_FIRED, ev::CONTRADICTION],
|
||||
|d, f| d.process_frame(f.presence as f32, f.motion_energy, f.breathing_bpm,
|
||||
f.heartrate_bpm, f.n_persons as f32, 0.0));
|
||||
|
||||
fwd_skill!(AutMeshAdapter, crate::aut_self_healing_mesh::SelfHealingMesh,
|
||||
"aut_self_healing_mesh",
|
||||
[ev::NODE_DEGRADED, ev::MESH_RECONFIGURE, ev::COVERAGE_SCORE, ev::HEALING_COMPLETE],
|
||||
|d, f| d.process_frame(f.variances));
|
||||
|
||||
fwd_skill!(BldElevatorAdapter, crate::bld_elevator_count::ElevatorCounter,
|
||||
"bld_elevator_count",
|
||||
[ev::ELEVATOR_COUNT, ev::DOOR_OPEN, ev::DOOR_CLOSE, ev::OVERLOAD_WARNING],
|
||||
|d, f| d.process_frame(f.amplitudes, f.phases, f.motion_energy, f.n_persons));
|
||||
|
||||
fwd_skill!(BldEnergyAdapter, crate::bld_energy_audit::EnergyAuditor,
|
||||
"bld_energy_audit",
|
||||
[ev::SCHEDULE_SUMMARY, ev::AFTER_HOURS_ALERT, ev::UTILIZATION_RATE],
|
||||
|d, f| d.process_frame(f.presence, f.n_persons));
|
||||
|
||||
fwd_skill!(BldHvacAdapter, crate::bld_hvac_presence::HvacPresenceDetector,
|
||||
"bld_hvac_presence",
|
||||
[ev::HVAC_OCCUPIED, ev::ACTIVITY_LEVEL, ev::DEPARTURE_COUNTDOWN],
|
||||
|d, f| d.process_frame(f.presence as f32, f.motion_energy));
|
||||
|
||||
fwd_skill!(BldLightingAdapter, crate::bld_lighting_zones::LightingZoneController,
|
||||
"bld_lighting_zones",
|
||||
[ev::LIGHT_ON, ev::LIGHT_DIM, ev::LIGHT_OFF],
|
||||
|d, f| d.process_frame(f.amplitudes, f.motion_energy));
|
||||
|
||||
fwd_skill!(BldMeetingAdapter, crate::bld_meeting_room::MeetingRoomTracker,
|
||||
"bld_meeting_room",
|
||||
[ev::MEETING_START, ev::MEETING_END, ev::PEAK_HEADCOUNT, ev::ROOM_AVAILABLE],
|
||||
|d, f| d.process_frame(f.presence, f.n_persons, f.motion_energy));
|
||||
|
||||
fwd_skill!(ExoBreathingSyncAdapter, crate::exo_breathing_sync::BreathingSyncDetector,
|
||||
"exo_breathing_sync",
|
||||
[ev::SYNC_DETECTED, ev::SYNC_PAIR_COUNT, ev::GROUP_COHERENCE, ev::SYNC_LOST],
|
||||
|d, f| d.process_frame(f.phases, f.variances, f.breathing_bpm, f.n_persons));
|
||||
|
||||
fwd_skill!(ExoEmotionAdapter, crate::exo_emotion_detect::EmotionDetector,
|
||||
"exo_emotion_detect",
|
||||
[ev::AROUSAL_LEVEL, ev::STRESS_INDEX, ev::CALM_DETECTED, ev::AGITATION_DETECTED],
|
||||
|d, f| d.process_frame(f.breathing_bpm, f.heartrate_bpm, f.motion_energy,
|
||||
f.phase_mean(), f.variance_mean));
|
||||
|
||||
fwd_skill!(ExoDreamAdapter, crate::exo_dream_stage::DreamStageDetector,
|
||||
"exo_dream_stage",
|
||||
[ev::SLEEP_STAGE, ev::SLEEP_QUALITY, ev::REM_EPISODE, ev::DEEP_SLEEP_RATIO],
|
||||
|d, f| d.process_frame(f.breathing_bpm, f.heartrate_bpm, f.motion_energy,
|
||||
f.phase_mean(), f.variance_mean, f.presence));
|
||||
|
||||
fwd_skill!(ExoGestureLangAdapter, crate::exo_gesture_language::GestureLanguageDetector,
|
||||
"exo_gesture_language",
|
||||
[ev::LETTER_RECOGNIZED, ev::LETTER_CONFIDENCE, ev::WORD_BOUNDARY, ev::GESTURE_REJECTED],
|
||||
|d, f| d.process_frame(f.phases, f.amplitudes, f.variance_mean, f.motion_energy, f.presence));
|
||||
|
||||
fwd_skill!(ExoGhostAdapter, crate::exo_ghost_hunter::GhostHunterDetector,
|
||||
"exo_ghost_hunter",
|
||||
[ev::EXO_ANOMALY_DETECTED, ev::EXO_ANOMALY_CLASS, ev::HIDDEN_PRESENCE, ev::ENVIRONMENTAL_DRIFT],
|
||||
|d, f| d.process_frame(f.phases, f.amplitudes, f.variances, f.presence, f.motion_energy));
|
||||
|
||||
fwd_skill!(ExoHappinessAdapter, crate::exo_happiness_score::HappinessScoreDetector,
|
||||
"exo_happiness_score",
|
||||
[ev::HAPPINESS_SCORE, ev::GAIT_ENERGY, ev::AFFECT_VALENCE, ev::SOCIAL_ENERGY, ev::TRANSIT_DIRECTION],
|
||||
|d, f| d.process_frame(f.phases, f.amplitudes, f.variances, f.presence,
|
||||
f.motion_energy, f.breathing_bpm, f.heartrate_bpm));
|
||||
|
||||
fwd_skill!(ExoHyperbolicAdapter, crate::exo_hyperbolic_space::HyperbolicEmbedder,
|
||||
"exo_hyperbolic_space",
|
||||
[ev::HIERARCHY_LEVEL, ev::HYPERBOLIC_RADIUS, ev::LOCATION_LABEL],
|
||||
|d, f| d.process_frame(f.amplitudes));
|
||||
|
||||
fwd_skill!(ExoMusicAdapter, crate::exo_music_conductor::MusicConductorDetector,
|
||||
"exo_music_conductor",
|
||||
[ev::CONDUCTOR_BPM, ev::BEAT_POSITION, ev::DYNAMIC_LEVEL, ev::GESTURE_CUTOFF, ev::GESTURE_FERMATA],
|
||||
|d, f| d.process_frame(f.phase_mean(), f.amplitude_mean(), f.motion_energy, f.variance_mean));
|
||||
|
||||
fwd_skill!(ExoPlantAdapter, crate::exo_plant_growth::PlantGrowthDetector,
|
||||
"exo_plant_growth",
|
||||
[ev::GROWTH_RATE, ev::CIRCADIAN_PHASE, ev::WILT_DETECTED, ev::WATERING_EVENT],
|
||||
|d, f| d.process_frame(f.amplitudes, f.phases, f.variances, f.presence));
|
||||
|
||||
fwd_skill!(ExoRainAdapter, crate::exo_rain_detect::RainDetector,
|
||||
"exo_rain_detect",
|
||||
[ev::RAIN_ONSET, ev::RAIN_INTENSITY, ev::RAIN_CESSATION],
|
||||
|d, f| d.process_frame(f.phases, f.variances, f.amplitudes, f.presence));
|
||||
|
||||
fwd_skill!(ExoTimeCrystalAdapter, crate::exo_time_crystal::TimeCrystalDetector,
|
||||
"exo_time_crystal",
|
||||
[ev::CRYSTAL_DETECTED, ev::CRYSTAL_STABILITY, ev::COORDINATION_INDEX],
|
||||
|d, f| d.process_frame(f.motion_energy));
|
||||
|
||||
fwd_skill!(IndCleanRoomAdapter, crate::ind_clean_room::CleanRoomMonitor,
|
||||
"ind_clean_room",
|
||||
[ev::OCCUPANCY_COUNT, ev::OCCUPANCY_VIOLATION, ev::TURBULENT_MOTION, ev::COMPLIANCE_REPORT],
|
||||
|d, f| d.process_frame(f.n_persons, f.presence, f.motion_energy));
|
||||
|
||||
fwd_skill!(IndConfinedAdapter, crate::ind_confined_space::ConfinedSpaceMonitor,
|
||||
"ind_confined_space",
|
||||
[ev::WORKER_ENTRY, ev::WORKER_EXIT, ev::BREATHING_OK, ev::EXTRACTION_ALERT, ev::IMMOBILE_ALERT],
|
||||
|d, f| d.process_frame(f.presence, f.breathing_bpm, f.motion_energy, f.variance_mean));
|
||||
|
||||
fwd_skill!(IndForkliftAdapter, crate::ind_forklift_proximity::ForkliftProximityDetector,
|
||||
"ind_forklift_proximity",
|
||||
[ev::PROXIMITY_WARNING, ev::VEHICLE_DETECTED, ev::HUMAN_NEAR_VEHICLE],
|
||||
|d, f| d.process_frame(f.phases, f.amplitudes, f.variances, f.motion_energy, f.presence, f.n_persons));
|
||||
|
||||
fwd_skill!(IndLivestockAdapter, crate::ind_livestock_monitor::LivestockMonitor,
|
||||
"ind_livestock_monitor",
|
||||
[ev::ANIMAL_PRESENT, ev::ABNORMAL_STILLNESS, ev::LABORED_BREATHING, ev::ESCAPE_ALERT],
|
||||
|d, f| d.process_frame(f.presence, f.breathing_bpm, f.motion_energy, f.variance_mean));
|
||||
|
||||
fwd_skill!(IndVibrationAdapter, crate::ind_structural_vibration::StructuralVibrationMonitor,
|
||||
"ind_structural_vibration",
|
||||
[ev::SEISMIC_DETECTED, ev::MECHANICAL_RESONANCE, ev::STRUCTURAL_DRIFT, ev::VIBRATION_SPECTRUM],
|
||||
|d, f| d.process_frame(f.phases, f.amplitudes, f.variances, f.presence));
|
||||
|
||||
fwd_skill!(IntrusionAdapter, crate::intrusion::IntrusionDetector,
|
||||
"intrusion",
|
||||
[ev::INTRUSION_ALERT, ev::INTRUSION_ZONE, 202],
|
||||
|d, f| d.process_frame(f.phases, f.amplitudes));
|
||||
|
||||
fwd_skill!(LrnAttractorAdapter, crate::lrn_anomaly_attractor::AttractorDetector,
|
||||
"lrn_anomaly_attractor",
|
||||
[ev::ATTRACTOR_TYPE, ev::LYAPUNOV_EXPONENT, ev::BASIN_DEPARTURE, ev::LEARNING_COMPLETE],
|
||||
|d, f| d.process_frame(f.phases, f.amplitudes, f.motion_energy));
|
||||
|
||||
fwd_skill!(LrnDtwAdapter, crate::lrn_dtw_gesture_learn::GestureLearner,
|
||||
"lrn_dtw_gesture_learn",
|
||||
[ev::GESTURE_LEARNED, ev::GESTURE_MATCHED, ev::LRN_MATCH_DISTANCE, ev::TEMPLATE_COUNT],
|
||||
|d, f| d.process_frame(f.phases, f.motion_energy));
|
||||
|
||||
fwd_skill!(LrnEwcAdapter, crate::lrn_ewc_lifelong::EwcLifelong,
|
||||
"lrn_ewc_lifelong",
|
||||
[ev::KNOWLEDGE_RETAINED, ev::NEW_TASK_LEARNED, ev::FISHER_UPDATE, ev::FORGETTING_RISK],
|
||||
|d, f| d.process_frame(f.variances, f.presence));
|
||||
|
||||
fwd_skill!(OccupancyAdapter, crate::occupancy::OccupancyDetector,
|
||||
"occupancy",
|
||||
[ev::ZONE_OCCUPIED, ev::ZONE_COUNT, ev::ZONE_TRANSITION],
|
||||
|d, f| d.process_frame(f.phases, f.amplitudes));
|
||||
|
||||
fwd_skill!(QntInterferenceAdapter, crate::qnt_interference_search::InterferenceSearch,
|
||||
"qnt_interference_search",
|
||||
[ev::HYPOTHESIS_WINNER, ev::HYPOTHESIS_AMPLITUDE, ev::SEARCH_ITERATIONS],
|
||||
|d, f| d.process_frame(f.presence, f.motion_energy, f.n_persons));
|
||||
|
||||
fwd_skill!(QntCoherenceAdapter, crate::qnt_quantum_coherence::QuantumCoherenceMonitor,
|
||||
"qnt_quantum_coherence",
|
||||
[ev::ENTANGLEMENT_ENTROPY, ev::DECOHERENCE_EVENT, ev::BLOCH_DRIFT],
|
||||
|d, f| d.process_frame(f.phases));
|
||||
|
||||
fwd_skill!(RetFlowAdapter, crate::ret_customer_flow::CustomerFlowTracker,
|
||||
"ret_customer_flow",
|
||||
[ev::INGRESS, ev::EGRESS, ev::NET_OCCUPANCY, ev::HOURLY_TRAFFIC],
|
||||
|d, f| d.process_frame(f.phases, f.amplitudes, f.variance_mean, f.motion_energy));
|
||||
|
||||
fwd_skill!(RetDwellAdapter, crate::ret_dwell_heatmap::DwellHeatmapTracker,
|
||||
"ret_dwell_heatmap",
|
||||
[ev::DWELL_ZONE_UPDATE, ev::HOT_ZONE, ev::COLD_ZONE, ev::SESSION_SUMMARY],
|
||||
|d, f| d.process_frame(f.presence, f.variances, f.motion_energy, f.n_persons));
|
||||
|
||||
fwd_skill!(RetQueueAdapter, crate::ret_queue_length::QueueLengthEstimator,
|
||||
"ret_queue_length",
|
||||
[ev::QUEUE_LENGTH, ev::WAIT_TIME_ESTIMATE, ev::SERVICE_RATE, ev::QUEUE_ALERT],
|
||||
|d, f| d.process_frame(f.presence, f.n_persons, f.variance_mean, f.motion_energy));
|
||||
|
||||
fwd_skill!(RetShelfAdapter, crate::ret_shelf_engagement::ShelfEngagementDetector,
|
||||
"ret_shelf_engagement",
|
||||
[ev::SHELF_BROWSE, ev::SHELF_CONSIDER, ev::SHELF_ENGAGE, ev::REACH_DETECTED],
|
||||
|d, f| d.process_frame(f.presence, f.motion_energy, f.variance_mean, f.phases));
|
||||
|
||||
fwd_skill!(RetTableAdapter, crate::ret_table_turnover::TableTurnoverTracker,
|
||||
"ret_table_turnover",
|
||||
[ev::TABLE_SEATED, ev::TABLE_VACATED, ev::TABLE_AVAILABLE, ev::TURNOVER_RATE],
|
||||
|d, f| d.process_frame(f.presence, f.motion_energy, f.n_persons));
|
||||
|
||||
fwd_skill!(SecLoiteringAdapter, crate::sec_loitering::LoiteringDetector,
|
||||
"sec_loitering",
|
||||
[ev::LOITERING_START, ev::LOITERING_ONGOING, ev::LOITERING_END],
|
||||
|d, f| d.process_frame(f.presence, f.motion_energy));
|
||||
|
||||
fwd_skill!(SecPanicAdapter, crate::sec_panic_motion::PanicMotionDetector,
|
||||
"sec_panic_motion",
|
||||
[ev::PANIC_DETECTED, ev::STRUGGLE_PATTERN, ev::FLEEING_DETECTED],
|
||||
|d, f| d.process_frame(f.motion_energy, f.variance_mean, f.phase_mean(), f.presence));
|
||||
|
||||
fwd_skill!(SecPerimeterAdapter, crate::sec_perimeter_breach::PerimeterBreachDetector,
|
||||
"sec_perimeter_breach",
|
||||
[ev::PERIMETER_BREACH, ev::APPROACH_DETECTED, ev::DEPARTURE_DETECTED, ev::SEC_ZONE_TRANSITION],
|
||||
|d, f| d.process_frame(f.phases, f.amplitudes, f.variances, f.motion_energy));
|
||||
|
||||
fwd_skill!(SecTailgateAdapter, crate::sec_tailgating::TailgateDetector,
|
||||
"sec_tailgating",
|
||||
[ev::TAILGATE_DETECTED, ev::SINGLE_PASSAGE, ev::MULTI_PASSAGE],
|
||||
|d, f| d.process_frame(f.motion_energy, f.presence, f.n_persons, f.variance_mean));
|
||||
|
||||
fwd_skill!(SecWeaponAdapter, crate::sec_weapon_detect::WeaponDetector,
|
||||
"sec_weapon_detect",
|
||||
[ev::METAL_ANOMALY, ev::HIGH_METAL_REFLECTIVITY, ev::CALIBRATION_NEEDED],
|
||||
|d, f| d.process_frame(f.phases, f.amplitudes, f.variances, f.motion_energy, f.presence));
|
||||
|
||||
fwd_skill!(SigCoherenceGateAdapter, crate::sig_coherence_gate::CoherenceGate,
|
||||
"sig_coherence_gate",
|
||||
[ev::GATE_DECISION, ev::SIG_COHERENCE_SCORE, ev::RECALIBRATE_NEEDED],
|
||||
|d, f| d.process_frame(f.phases));
|
||||
|
||||
fwd_skill!(SigFlashAttnAdapter, crate::sig_flash_attention::FlashAttention,
|
||||
"sig_flash_attention",
|
||||
[ev::ATTENTION_PEAK_SC, ev::ATTENTION_SPREAD, ev::SPATIAL_FOCUS_ZONE],
|
||||
|d, f| d.process_frame(f.phases, f.amplitudes));
|
||||
|
||||
fwd_skill!(SigMincutAdapter, crate::sig_mincut_person_match::PersonMatcher,
|
||||
"sig_mincut_person_match",
|
||||
[ev::PERSON_ID_ASSIGNED, ev::PERSON_ID_SWAP, ev::MATCH_CONFIDENCE],
|
||||
|d, f| d.process_frame(f.amplitudes, f.variances, f.n_persons.max(0) as usize));
|
||||
|
||||
fwd_skill!(SigTransportAdapter, crate::sig_optimal_transport::OptimalTransportDetector,
|
||||
"sig_optimal_transport",
|
||||
[ev::WASSERSTEIN_DISTANCE, ev::DISTRIBUTION_SHIFT, ev::SUBTLE_MOTION],
|
||||
|d, f| d.process_frame(f.amplitudes));
|
||||
|
||||
fwd_skill!(SptHnswAdapter, crate::spt_micro_hnsw::MicroHnsw,
|
||||
"spt_micro_hnsw",
|
||||
[ev::NEAREST_MATCH_ID, ev::HNSW_MATCH_DISTANCE, ev::CLASSIFICATION, ev::LIBRARY_SIZE],
|
||||
|d, f| d.process_frame(f.variances));
|
||||
|
||||
fwd_skill!(SptPagerankAdapter, crate::spt_pagerank_influence::PageRankInfluence,
|
||||
"spt_pagerank_influence",
|
||||
[ev::DOMINANT_PERSON, ev::INFLUENCE_SCORE, ev::INFLUENCE_CHANGE],
|
||||
|d, f| d.process_frame(f.phases, f.n_persons.max(0) as usize));
|
||||
|
||||
fwd_skill!(SptSpikingAdapter, crate::spt_spiking_tracker::SpikingTracker,
|
||||
"spt_spiking_tracker",
|
||||
[ev::TRACK_UPDATE, ev::TRACK_VELOCITY, ev::SPIKE_RATE, ev::TRACK_LOST],
|
||||
|d, f| d.process_frame(f.phases, f.prev_phases));
|
||||
|
||||
fwd_skill!(TmpLogicGuardAdapter, crate::tmp_temporal_logic_guard::TemporalLogicGuard,
|
||||
"tmp_temporal_logic_guard",
|
||||
[ev::LTL_VIOLATION, ev::LTL_SATISFACTION, ev::COUNTEREXAMPLE],
|
||||
|d, f| {
|
||||
let input = crate::tmp_temporal_logic_guard::FrameInput {
|
||||
presence: f.presence,
|
||||
n_persons: f.n_persons,
|
||||
motion_energy: f.motion_energy,
|
||||
coherence: f.coherence,
|
||||
breathing_bpm: f.breathing_bpm,
|
||||
heartrate_bpm: f.heartrate_bpm,
|
||||
fall_alert: false,
|
||||
intrusion_alert: false,
|
||||
person_id_active: f.n_persons > 0,
|
||||
vital_signs_active: f.breathing_bpm > 0.0,
|
||||
seizure_detected: false,
|
||||
normal_gait: true,
|
||||
};
|
||||
d.on_frame(&input)
|
||||
});
|
||||
|
||||
// ── Timer-driven skills (driven once per frame) ──────────────────────────────
|
||||
|
||||
fwd_skill!(VitalTrendAdapter, crate::vital_trend::VitalTrendAnalyzer,
|
||||
"vital_trend",
|
||||
// 101-105 = brady/tachypnea, brady/tachycardia, apnea; 110/111 = breathing/heartrate
|
||||
// moving averages (module-local EVENT_BREATHING_AVG / EVENT_HEARTRATE_AVG).
|
||||
[ev::BRADYPNEA, ev::TACHYPNEA, ev::BRADYCARDIA, ev::TACHYCARDIA, ev::APNEA, 110, 111],
|
||||
|d, f| d.on_timer(f.breathing_bpm, f.heartrate_bpm));
|
||||
|
||||
fwd_skill!(LrnMetaAdapter, crate::lrn_meta_adapt::MetaAdapter,
|
||||
"lrn_meta_adapt",
|
||||
[ev::PARAM_ADJUSTED, ev::ADAPTATION_SCORE, ev::ROLLBACK_TRIGGERED, ev::META_LEVEL],
|
||||
|d, _f| d.on_timer());
|
||||
|
||||
fwd_skill!(SigTemporalCompressAdapter, crate::sig_temporal_compress::TemporalCompressor,
|
||||
"sig_temporal_compress",
|
||||
[ev::COMPRESSION_RATIO, ev::TIER_TRANSITION, ev::HISTORY_DEPTH_HOURS],
|
||||
|d, _f| d.on_timer());
|
||||
|
||||
fwd_skill!(TmpGoapAdapter, crate::tmp_goap_autonomy::GoapPlanner,
|
||||
"tmp_goap_autonomy",
|
||||
[ev::GOAL_SELECTED, ev::MODULE_ACTIVATED, ev::MODULE_DEACTIVATED, ev::PLAN_COST],
|
||||
|d, _f| d.on_timer());
|
||||
|
||||
// tmp_pattern_sequence: accumulate via on_frame, then drive on_timer per frame.
|
||||
pub struct TmpPatternAdapter(crate::tmp_pattern_sequence::PatternSequenceAnalyzer);
|
||||
impl TmpPatternAdapter {
|
||||
pub fn new() -> Self {
|
||||
Self(crate::tmp_pattern_sequence::PatternSequenceAnalyzer::new())
|
||||
}
|
||||
}
|
||||
impl EdgeSkill for TmpPatternAdapter {
|
||||
fn name(&self) -> &'static str {
|
||||
"tmp_pattern_sequence"
|
||||
}
|
||||
fn event_ids(&self) -> &'static [i32] {
|
||||
&[ev::PATTERN_DETECTED, ev::PATTERN_CONFIDENCE, ev::ROUTINE_DEVIATION, ev::PREDICTION_NEXT]
|
||||
}
|
||||
fn on_frame(&mut self, f: &CsiFrameView) -> &[(i32, f32)] {
|
||||
self.0.on_frame(f.presence, f.motion_energy, f.n_persons);
|
||||
self.0.on_timer()
|
||||
}
|
||||
}
|
||||
|
||||
// ── Medical tier (gated) ─────────────────────────────────────────────────────
|
||||
|
||||
#[cfg(feature = "medical-experimental")]
|
||||
mod medical {
|
||||
use super::*;
|
||||
|
||||
// Medical event ids verified against each module's local consts (100-199 block).
|
||||
fwd_skill!(MedCardiacAdapter, crate::med_cardiac_arrhythmia::CardiacArrhythmiaDetector,
|
||||
"med_cardiac_arrhythmia",
|
||||
[110, 111, 112, 113],
|
||||
|d, f| d.process_frame(f.heartrate_bpm, f.phase_mean()));
|
||||
|
||||
fwd_skill!(MedGaitAdapter, crate::med_gait_analysis::GaitAnalyzer,
|
||||
"med_gait_analysis",
|
||||
[130, 131, 132, 133, 134],
|
||||
|d, f| d.process_frame(f.phase_mean(), f.amplitude_mean(), f.variance_mean, f.motion_energy));
|
||||
|
||||
fwd_skill!(MedRespiratoryAdapter, crate::med_respiratory_distress::RespiratoryDistressDetector,
|
||||
"med_respiratory_distress",
|
||||
[120, 121, 122, 123],
|
||||
|d, f| d.process_frame(f.breathing_bpm, f.phase_mean(), f.variance_mean));
|
||||
|
||||
fwd_skill!(MedSeizureAdapter, crate::med_seizure_detect::SeizureDetector,
|
||||
"med_seizure_detect",
|
||||
[140, 141, 142, 143],
|
||||
|d, f| d.process_frame(f.phase_mean(), f.amplitude_mean(), f.motion_energy, f.presence));
|
||||
|
||||
fwd_skill!(MedApneaAdapter, crate::med_sleep_apnea::SleepApneaDetector,
|
||||
"med_sleep_apnea",
|
||||
[100, 101, 102],
|
||||
|d, f| d.process_frame(f.breathing_bpm, f.presence, f.variance_mean));
|
||||
|
||||
pub fn register(skills: &mut Vec<Box<dyn EdgeSkill>>, med: &mut Vec<bool>) {
|
||||
macro_rules! push {
|
||||
($a:ty) => {{
|
||||
skills.push(Box::new(<$a>::new()));
|
||||
med.push(true);
|
||||
}};
|
||||
}
|
||||
push!(MedSeizureAdapter);
|
||||
push!(MedCardiacAdapter);
|
||||
push!(MedRespiratoryAdapter);
|
||||
push!(MedApneaAdapter);
|
||||
push!(MedGaitAdapter);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Registration ─────────────────────────────────────────────────────────────
|
||||
|
||||
/// Register every default-tier (non-medical) skill.
|
||||
pub fn register_default(skills: &mut Vec<Box<dyn EdgeSkill>>, med: &mut Vec<bool>) {
|
||||
macro_rules! push {
|
||||
($a:ty) => {{
|
||||
skills.push(Box::new(<$a>::new()));
|
||||
med.push(false);
|
||||
}};
|
||||
}
|
||||
|
||||
// Flagship + synthesized
|
||||
push!(GestureAdapter);
|
||||
push!(CoherenceAdapter);
|
||||
push!(AdversarialAdapter);
|
||||
push!(OccupancyAdapter);
|
||||
push!(IntrusionAdapter);
|
||||
push!(VitalTrendAdapter);
|
||||
|
||||
// Security
|
||||
push!(SecPerimeterAdapter);
|
||||
push!(SecWeaponAdapter);
|
||||
push!(SecTailgateAdapter);
|
||||
push!(SecLoiteringAdapter);
|
||||
push!(SecPanicAdapter);
|
||||
|
||||
// Smart building
|
||||
push!(BldHvacAdapter);
|
||||
push!(BldLightingAdapter);
|
||||
push!(BldElevatorAdapter);
|
||||
push!(BldMeetingAdapter);
|
||||
push!(BldEnergyAdapter);
|
||||
|
||||
// Retail
|
||||
push!(RetQueueAdapter);
|
||||
push!(RetDwellAdapter);
|
||||
push!(RetFlowAdapter);
|
||||
push!(RetTableAdapter);
|
||||
push!(RetShelfAdapter);
|
||||
|
||||
// Industrial
|
||||
push!(IndForkliftAdapter);
|
||||
push!(IndConfinedAdapter);
|
||||
push!(IndCleanRoomAdapter);
|
||||
push!(IndLivestockAdapter);
|
||||
push!(IndVibrationAdapter);
|
||||
|
||||
// Exotic / research
|
||||
push!(ExoTimeCrystalAdapter);
|
||||
push!(ExoHyperbolicAdapter);
|
||||
push!(ExoDreamAdapter);
|
||||
push!(ExoEmotionAdapter);
|
||||
push!(ExoGestureLangAdapter);
|
||||
push!(ExoMusicAdapter);
|
||||
push!(ExoPlantAdapter);
|
||||
push!(ExoGhostAdapter);
|
||||
push!(ExoRainAdapter);
|
||||
push!(ExoBreathingSyncAdapter);
|
||||
push!(ExoHappinessAdapter);
|
||||
|
||||
// Signal intelligence
|
||||
push!(SigCoherenceGateAdapter);
|
||||
push!(SigFlashAttnAdapter);
|
||||
push!(SigTemporalCompressAdapter);
|
||||
push!(SparseRecoveryAdapter);
|
||||
push!(SigMincutAdapter);
|
||||
push!(SigTransportAdapter);
|
||||
|
||||
// Adaptive learning
|
||||
push!(LrnDtwAdapter);
|
||||
push!(LrnAttractorAdapter);
|
||||
push!(LrnMetaAdapter);
|
||||
push!(LrnEwcAdapter);
|
||||
|
||||
// Spatial reasoning
|
||||
push!(SptPagerankAdapter);
|
||||
push!(SptHnswAdapter);
|
||||
push!(SptSpikingAdapter);
|
||||
|
||||
// Temporal analysis
|
||||
push!(TmpPatternAdapter);
|
||||
push!(TmpLogicGuardAdapter);
|
||||
push!(TmpGoapAdapter);
|
||||
|
||||
// AI security
|
||||
push!(AisPromptShieldAdapter);
|
||||
push!(AisBehavioralAdapter);
|
||||
|
||||
// Quantum-inspired
|
||||
push!(QntCoherenceAdapter);
|
||||
push!(QntInterferenceAdapter);
|
||||
|
||||
// Autonomous systems
|
||||
push!(AutPsychoAdapter);
|
||||
push!(AutMeshAdapter);
|
||||
|
||||
let _ = (skills.len(), med.len());
|
||||
}
|
||||
|
||||
/// Register the gated `medical-experimental` tier (5 `med_*` skills).
|
||||
#[cfg(feature = "medical-experimental")]
|
||||
pub fn register_medical(skills: &mut Vec<Box<dyn EdgeSkill>>, med: &mut Vec<bool>) {
|
||||
medical::register(skills, med);
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
//! Integration test for the unified [`EdgePipeline`] (ADR-160 deliverable 1).
|
||||
//!
|
||||
//! Proves that EVERY registered skill executes over a deterministic synthetic
|
||||
//! CSI frame sequence without panicking, that the aggregated event stream is
|
||||
//! well-formed (each event tagged with a known skill name + a declared event
|
||||
//! id), and pins the registered-skill count (default vs +medical-experimental).
|
||||
//!
|
||||
//! Run:
|
||||
//! cargo test --features std --test pipeline_all
|
||||
//! cargo test --features std,medical-experimental --test pipeline_all
|
||||
//!
|
||||
//! [`EdgePipeline`]: wifi_densepose_wasm_edge::pipeline_all::EdgePipeline
|
||||
|
||||
#![cfg(feature = "std")]
|
||||
|
||||
use wifi_densepose_wasm_edge::pipeline_all::{CsiFrameView, EdgePipeline};
|
||||
|
||||
const N_SC: usize = 32;
|
||||
|
||||
/// Deterministic synthetic frame: a moving breathing/heartbeat target plus
|
||||
/// structured per-subcarrier phase/amplitude. No randomness — fully reproducible.
|
||||
fn synth_frame(t: usize, phases: &mut [f32], amps: &mut [f32], vars: &mut [f32]) {
|
||||
let tf = t as f32;
|
||||
// 0.3 Hz breathing modulation @ 20 Hz frame rate -> period ~66 frames.
|
||||
let breath = (tf * 2.0 * core::f32::consts::PI * 0.3 / 20.0).sin();
|
||||
// 1.2 Hz heartbeat.
|
||||
let heart = (tf * 2.0 * core::f32::consts::PI * 1.2 / 20.0).sin();
|
||||
for i in 0..phases.len() {
|
||||
let sc = i as f32;
|
||||
phases[i] = (sc * 0.21 + tf * 0.05).sin() + 0.15 * breath;
|
||||
amps[i] = 1.0 + 0.3 * (sc * 0.11 + tf * 0.03).cos() + 0.1 * heart;
|
||||
// motion-correlated variance, with one occasionally-hot zone.
|
||||
vars[i] = 0.02 + 0.01 * (sc * 0.3).sin().abs() + if (t / 40) % 2 == 0 { 0.05 } else { 0.0 };
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a view over the supplied buffers for frame `t`.
|
||||
fn view<'a>(
|
||||
t: usize,
|
||||
phases: &'a [f32],
|
||||
amps: &'a [f32],
|
||||
vars: &'a [f32],
|
||||
prev_phases: &'a [f32],
|
||||
) -> CsiFrameView<'a> {
|
||||
let tf = t as f32;
|
||||
let motion = 0.3 + 0.2 * (tf * 0.07).sin().abs();
|
||||
let mut vmean = 0.0f32;
|
||||
for &v in vars {
|
||||
vmean += v;
|
||||
}
|
||||
vmean /= vars.len().max(1) as f32;
|
||||
CsiFrameView {
|
||||
phases,
|
||||
amplitudes: amps,
|
||||
variances: vars,
|
||||
prev_phases,
|
||||
presence: if (t / 30) % 3 == 0 { 0 } else { 1 },
|
||||
n_persons: ((t / 50) % 3) as i32,
|
||||
motion_energy: motion,
|
||||
breathing_bpm: 18.0 + 2.0 * (tf * 0.01).sin(),
|
||||
heartrate_bpm: 72.0 + 5.0 * (tf * 0.02).sin(),
|
||||
coherence: 0.5 + 0.4 * (tf * 0.03).cos(),
|
||||
variance_mean: vmean,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_skills_execute_without_panic_over_synthetic_stream() {
|
||||
let mut pipeline = EdgePipeline::new();
|
||||
let n_skills = pipeline.skill_count();
|
||||
assert!(n_skills > 0, "pipeline must register skills");
|
||||
|
||||
let mut phases = [0.0f32; N_SC];
|
||||
let mut amps = [0.0f32; N_SC];
|
||||
let mut vars = [0.0f32; N_SC];
|
||||
let mut prev_phases = [0.0f32; N_SC];
|
||||
|
||||
let known: std::collections::HashSet<&'static str> =
|
||||
pipeline.skills().iter().map(|s| s.name).collect();
|
||||
|
||||
// Feed 300 frames (15 s @ 20 Hz) — enough for calibration windows, DTW
|
||||
// enrollment, periodicity buffers, and timer cadences to fire.
|
||||
let mut total_events = 0usize;
|
||||
for t in 0..300 {
|
||||
synth_frame(t, &mut phases, &mut amps, &mut vars);
|
||||
let v = view(t, &phases, &s, &vars, &prev_phases);
|
||||
let events = pipeline.on_frame(&v);
|
||||
for e in &events {
|
||||
// Every event must be tagged with a registered skill name.
|
||||
assert!(known.contains(e.skill), "unknown skill tag: {}", e.skill);
|
||||
// Value must be finite (no NaN/Inf leaking from the DSP).
|
||||
assert!(e.value.is_finite(), "non-finite value from {}", e.skill);
|
||||
}
|
||||
total_events += events.len();
|
||||
prev_phases.copy_from_slice(&phases);
|
||||
}
|
||||
|
||||
assert_eq!(pipeline.frame_count(), 300);
|
||||
// A real run over 300 frames must emit *some* events across 59+ skills.
|
||||
assert!(
|
||||
total_events > 0,
|
||||
"expected the skill library to emit events over 300 frames, got 0"
|
||||
);
|
||||
println!(
|
||||
"pipeline: {} skills, {} aggregated events over 300 synthetic frames",
|
||||
n_skills, total_events
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_emitted_event_id_is_declared_by_its_skill() {
|
||||
// Stronger well-formedness: each event's id must be one the producing skill
|
||||
// declared in its `event_ids()` introspection list.
|
||||
let mut pipeline = EdgePipeline::new();
|
||||
|
||||
// skill name -> its declared event id set
|
||||
let mut declared: std::collections::HashMap<&'static str, std::collections::HashSet<i32>> =
|
||||
std::collections::HashMap::new();
|
||||
for s in pipeline.skills() {
|
||||
declared.insert(s.name, s.event_ids.iter().copied().collect());
|
||||
}
|
||||
|
||||
let mut phases = [0.0f32; N_SC];
|
||||
let mut amps = [0.0f32; N_SC];
|
||||
let mut vars = [0.0f32; N_SC];
|
||||
let mut prev_phases = [0.0f32; N_SC];
|
||||
|
||||
for t in 0..300 {
|
||||
synth_frame(t, &mut phases, &mut amps, &mut vars);
|
||||
let v = view(t, &phases, &s, &vars, &prev_phases);
|
||||
for e in &pipeline.on_frame(&v) {
|
||||
let set = declared.get(e.skill).expect("skill declared");
|
||||
assert!(
|
||||
set.contains(&e.event_id),
|
||||
"{} emitted undeclared event id {}",
|
||||
e.skill,
|
||||
e.event_id
|
||||
);
|
||||
}
|
||||
prev_phases.copy_from_slice(&phases);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn introspection_lists_every_skill_with_event_ids() {
|
||||
let pipeline = EdgePipeline::new();
|
||||
let infos = pipeline.skills();
|
||||
assert_eq!(infos.len(), pipeline.skill_count());
|
||||
for info in &infos {
|
||||
assert!(!info.name.is_empty());
|
||||
assert!(
|
||||
!info.event_ids.is_empty(),
|
||||
"skill {} declares no event ids",
|
||||
info.name
|
||||
);
|
||||
}
|
||||
// No duplicate skill names.
|
||||
let names: std::collections::HashSet<_> = infos.iter().map(|i| i.name).collect();
|
||||
assert_eq!(names.len(), infos.len(), "duplicate skill registration");
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "medical-experimental"))]
|
||||
#[test]
|
||||
fn default_tier_count_excludes_medical() {
|
||||
let pipeline = EdgePipeline::new();
|
||||
assert_eq!(
|
||||
pipeline.skill_count(),
|
||||
59,
|
||||
"default (non-medical) tier must register exactly 59 skills"
|
||||
);
|
||||
// The ADR-160 safety gate: no med_* skill is present in the default build.
|
||||
for info in pipeline.skills() {
|
||||
assert!(
|
||||
!info.medical_experimental,
|
||||
"medical skill {} leaked into default tier",
|
||||
info.name
|
||||
);
|
||||
assert!(
|
||||
!info.name.starts_with("med_"),
|
||||
"med_* skill {} present without the medical-experimental feature",
|
||||
info.name
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "medical-experimental")]
|
||||
#[test]
|
||||
fn medical_tier_adds_five_skills() {
|
||||
let pipeline = EdgePipeline::new();
|
||||
assert_eq!(
|
||||
pipeline.skill_count(),
|
||||
64,
|
||||
"default 59 + 5 medical = 64 skills"
|
||||
);
|
||||
let med: Vec<_> = pipeline
|
||||
.skills()
|
||||
.into_iter()
|
||||
.filter(|s| s.medical_experimental)
|
||||
.collect();
|
||||
assert_eq!(med.len(), 5, "exactly 5 medical-experimental skills");
|
||||
for m in &med {
|
||||
assert!(
|
||||
m.name.starts_with("med_"),
|
||||
"medical-flagged skill has non-med_ name: {}",
|
||||
m.name
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user