diff --git a/CHANGELOG.md b/CHANGELOG.md index d78f28460a..e972b1a04b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added +- **`scripts/fake_esp32_csi_sender.py` — host-side CSI UDP test sender.** Emits ADR-018 CSI frames (magic `0xC511_0001`, 20-byte header + I/Q pairs) to the sensing-server's UDP port, so the `--source esp32` ingestion path (node registration, `/api/v1/nodes`, the Sensing-tab `LIVE — ESP32 HARDWARE` state) can be exercised without physical hardware. Configurable node count, subcarriers, frame rate, and synthesized breathing/motion. - **ADR-261: RuVector graph-ANN index — a real HNSW baseline + a SymphonyQG-style quantized variant, MEASURED (honest negative).** Closes the [ADR-156 §5 #1](docs/adr/ADR-156-ruvector-fusion-beyond-sota.md) gap: the SymphonyQG (SIGMOD 2025) **3.5–17× QPS-over-HNSW** claim was CLAIMED-only because **no HNSW baseline existed to compare against**. This adds one. New pure-Rust, `--no-default-features`-buildable modules in `wifi-densepose-ruvector`: `hnsw.rs` (a correct float HNSW — Malkov & Yashunin: multi-layer NSW graph, `ef_construction`/`ef_search`, Algorithm-4 neighbour selection, **seeded-deterministic** level assignment via SplitMix64, L2 + cosine, full degenerate-case guards), `hnsw_quantized.rs` (the SymphonyQG-style variant — the **same** graph traversed by a cheap **1-bit Hamming** score over the RaBitQ Pass-2 rotated sign code, then **exact-float rerank**), `ann_measure.rs` + `benches/ann_bench.rs` (one shared deterministic planted-cluster fixture; the `ann_bench_report` test is the source of truth). **MEASURED (dim=128, N=10k, K=10, `--release`):** float HNSW = **~25× QPS over linear scan at recall ≥0.99** (the baseline this gap needed; recall@10 correctness gate ≥0.95 holds, L2 + cosine). **Honest negative:** the 1-bit quantized traversal is **too coarse to beat float HNSW at equal recall at this scale** — its best recall is **0.738**, never reaching the ≥0.90 equal-recall point, so there is **no QPS win** over float HNSW; the 3.5–17× is **not reproduced** by our 1-bit construction here. The recall gate also **caught a real index-out-of-bounds bug** in the insert path (disclosed in ADR-261 §4). Caveat: this is **our** HNSW + **our** 1-bit quant, not SymphonyQG's exact system — it tests the *direction* of the claim, with the expected crossover at large N + a multi-bit traversal code. **We did not tune to manufacture a speedup.** +20 tests (ruvector lib 131→151, 0 failed). ADR-156 §5 #1 / §8 backlog: CLAIMED → **MEASURED-direction-tested**. Python deterministic proof unchanged (off the signal proof path). - **ADR-260: RuField MFS — the open specification for camera-free multimodal field sensing.** A common event / tensor / calibration / privacy / provenance model that sits *above* WiFi CSI/CIR/BFLD, UWB, BLE Channel Sounding, mmWave radar, ultrasound, subsonic, infrared, and future quantum sensors (each modality emits a normalized `FieldEvent` → `FieldTensor` → `FusionGraph` → `PrivacyClass` → `ProvenanceReceipt`). Published as a **standalone repo** [`ruvnet/rufield`](https://github.com/ruvnet/rufield) and vendored here as the `vendor/rufield` submodule (the `vendor/rvcsi` pattern — not a `v2/` workspace member). The v0.1 reference stack is a self-contained 6-crate Rust workspace (`rufield-core`, `-provenance` [sha256 + ed25519], `-privacy` [P0–P5 guard], `-adapters` [deterministic `SyntheticSim` across wifi_csi/mmwave_radar/infrared_thermal], `-fusion` [graph + TOML weighted-Bayes rules → 7 room-state inferences], `-bench` [deterministic runner + the §31 acceptance test]). **60 tests / 0 failed, clippy-clean.** §27 acceptance criteria 1–8 and 10 PASS; the live dashboard (9) is deferred. **All benchmark metrics are SYNTHETIC** (scored against the simulator's own ground truth — presence/breathing/bed_exit/room_transition F1 = 1.000, nocturnal_scratch 0.923 reported honestly, p95 latency ~0.01 ms, provenance coverage 100%, 0 privacy violations) — they prove the pipeline recovers known truth, **not** field accuracy; real hardware adapters (ESP32 CSI, mmWave, thermal IR) are a documented roadmap item, none validated in v0.1. The Python deterministic proof is unchanged (rufield is off the signal-processing proof path). diff --git a/scripts/fake_esp32_csi_sender.py b/scripts/fake_esp32_csi_sender.py new file mode 100644 index 0000000000..0409f0c5fa --- /dev/null +++ b/scripts/fake_esp32_csi_sender.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 +"""Fake ESP32 CSI UDP sender — emits ADR-018 binary frames to the sensing-server. + +Wire format (matches parse_esp32_frame in wifi-densepose-sensing-server/src/main.rs +and firmware csi_collector.c): + + [0..3] magic = 0xC511_0001 (u32 LE) + [4] node_id (u8) + [5] n_antennas (u8) + [6..7] n_subcarriers (u16 LE) + [8..11] freq_mhz (u32 LE) + [12..15] sequence (u32 LE) + [16] rssi (i8) + [17] noise_floor (i8) + [18] ppdu_type (u8, 0 = HT/legacy) + [19] reserved + [20..] I/Q data: n_antennas * n_subcarriers pairs of (i8 I, i8 Q) + +Use this to exercise the --source esp32 ingestion path without real hardware. +""" +import argparse +import math +import socket +import struct +import time + +MAGIC = 0xC511_0001 + + +def build_frame(node_id, n_antennas, n_subcarriers, freq_mhz, sequence, + rssi, noise_floor, t, breath_hz, motion): + hdr = struct.pack( + " {args.host}:{args.port} | " + f"{args.nodes} node(s), {args.subcarriers} subcarriers, {args.rate} Hz") + try: + while True: + t = time.time() - start + for node_id in range(1, args.nodes + 1): + frame = build_frame( + node_id, args.antennas, args.subcarriers, args.freq_mhz, + seq, args.rssi, args.noise, t, args.breath_hz, args.motion, + ) + sock.sendto(frame, (args.host, args.port)) + seq += 1 + if seq % int(args.rate) == 0: + print(f" t={t:5.1f}s sent seq={seq} ({len(frame)} B/frame)") + if args.duration and t >= args.duration: + break + time.sleep(period) + except KeyboardInterrupt: + pass + print(f"Done. Sent {seq} sequences to {args.nodes} node(s).") + + +if __name__ == "__main__": + main()