mirror of
https://github.com/ruvnet/RuView
synced 2026-07-31 18:51:42 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c5977bef29 |
@@ -29,7 +29,3 @@
|
||||
path = v2/crates/worldgraph
|
||||
url = https://github.com/ruvnet/worldgraph.git
|
||||
branch = main
|
||||
[submodule "vendor/metaharness"]
|
||||
path = vendor/metaharness
|
||||
url = https://github.com/ruvnet/metaharness
|
||||
branch = main
|
||||
|
||||
@@ -9,7 +9,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Added
|
||||
|
||||
- **`wifi-densepose-sar` — coherent wideband RF tomography research crate (ADR-287).** New standalone leaf crate (the `nvsim` pattern; zero coupling to `wifi-densepose-hardware` or any real ingestion path) implementing the synthetic-aperture-radar reconstruction primitive a handheld through-wall RF imaging device would need — motivated by comparison against Applied Electrodynamics' "WaveSight" launch, and explicitly scoped below ADR-278's RISE/DiffRadar/GeRaF reproduction gates. Ships: (1) a stepped-frequency, multi-position complex forward measurement simulator (`y_{m,k} = Σ σ_j/R² · exp(-i·4π·f·R/c) + noise`, deterministic ChaCha20 seeding); (2) delay-and-sum backprojection reconstruction onto a 3D voxel grid, rayon-parallelized over voxels; (3) threshold + local-maximum point-cloud extraction; (4) closed-form range/cross-range resolution and antenna-pose coherence-budget formulas (`ΔR=c/2B`, `δ_CR≈λR/2L`, `Δp≤λ/8`) checked against the reconstruction's *actual* behavior in `tests/physics_validation.rs` rather than merely documented — forward-simulating two targets at controlled separations and proving they resolve or merge exactly where the formulas predict, and that reconstructed focus at a known target degrades as injected antenna-pose error grows. Every number is SYNTHETIC/L0 (ADR-282) — no real wideband RF hardware backs this crate; see the crate README and `docs/tutorials/coherent-rf-tomography-backprojection.md` for the full honesty boundary and a worked walkthrough. `focus_at_point` exploits the evenly-spaced-by-construction frequency sweep (an arithmetic progression in per-term phase) to evaluate each pose's phasor once and advance it by a fixed complex-multiply step per frequency instead of one `sin`/`cos` pair per frequency — **MEASURED ~4.4-4.5x faster** (criterion regression detection, p < 0.001) than the first-shipped direct-computation version, proven equivalent (not just faster) to an independently reimplemented reference across four sweep sizes and on-/off-target points. 25 tests (22 unit + 3 integration), 0 failed, clippy-clean; MEASURED backprojection throughput ~1.7-2.3M voxels/sec (criterion, 21 poses × 32 freq steps).
|
||||
- **HOMECORE platform runtime completion — secure native/Wasmtime plugins, authenticated HAP IP, expanded Home Assistant APIs, durable restoration/migration, and voice protocols.** `homecore-server` now owns deterministic compiled-in native plugin registration plus explicitly configured, path-bounded, Ed25519 publisher-verified Wasm packages executed through Wasmtime with setup/state-change/teardown lifecycle; arbitrary native dynamic libraries remain intentionally unsupported. The optional HAP server implements persisted accessory identity and controller records, SRP-6a Pair-Setup M1–M6, X25519/Ed25519 Pair-Verify M1–M4, HKDF-SHA512/ChaCha20-Poly1305 record framing, authenticated/admin endpoint gates, replay/tamper closure, live entity synchronization, and paired-state `_hap._tcp` mDNS updates (45 focused tests; external Apple certification is not claimed). Startup restores device/entity registries and deterministic latest recorder states before plugins, and migration now atomically preserves forward-compatible device/config-entry fields. The HA-compatible surface adds events, templates, config checks, components, registries, history/logbook with SQL-enforced global response bounds, calendar/camera provider routes, and modern WebSocket negotiation while retaining a machine-readable limitations matrix for integration-specific behavior. Assist adds bounded PCM16, async STT/TTS contracts, an end-to-end speech pipeline, and an authenticated satellite session protocol; real deployments still provide the speech engines.
|
||||
- **`ruview-unified` increment 3 — Gaussian update-loop completion, separable delay-Doppler, and property-tested boundary hardening.** (1) `GaussianMap::merge_overlapping` (ADR-275 step 5: mutual-Mahalanobis + semantic-compatibility dedup catching drift the insert-time gate misses) and lifetime-aware decay (`τ_eff = τ·(1+ln(1+lifetime/τ))` — confirmed structures outlive transients at equal nominal τ). (2) `delay_doppler_map` reimplemented separably (`O(B²S+S²B)`), proven equivalent to the direct reference to <1e-10 and **measured 8.3× faster** (520 µs vs 4.34 ms at 56×8). (3) `tests/security_boundaries.rs` — 8 `proptest` properties over the boundary surfaces (arbitrary values incl. NaN/±inf via `f64::from_bits`) that found and fixed three input-controlled defects: a BLE-CS phase-unwrap infinite loop on non-finite phases and an ~1e299-iteration loop on finite-huge phases (now O(1) modular unwrap + plausibility bound), and a subnormal Gaussian scale overflowing `1/σ²` to NaN density (now physical σ/occupancy bounds). (4) New criterion benches for all increment-2 hot paths (`to_canonical` 38 µs, `ble_cs_range` 481 ns, AoI planner 647 ns/200 regions, coherent fusion 1.5 µs/32 members, factorized pose 521 ns). ruview-unified now 98 tests (87 lib + 3 acceptance + 8 security), 0 failed, clippy-clean.
|
||||
- **`ruview-unified` increment 2 — native frame contract + programmable perception (ADR-279..282).** (1) `RfFrameV2` becomes the authoritative RF record: native complex IQ with explicit validity masks, declared `PhaseState`, TX/RX poses + antenna geometry in one building frame, calibration/quality state, and a provenance rule enforced at construction — `Synthetic ⇒ L0Simulation` and `Measured ⇒ ≥ L1CapturedReplay` can never alias (the public L0–L5 evidence ladder is now a type); the 56-bin canonical tensor is demoted to a derived compatibility view (`to_canonical`, mask-aware gap-filling through the same normalization path as every adapter; native samples proven byte-untouched). (2) Active sensing control plane (`control.rs`): ETSI-ISAC-vocabulary `SensingTask` admission (raw export always refused; identity requires consent), `SensingAction`/`InformationGoal`, an age-of-information `ActiveSensingPlanner` (priority = uncertainty × change rate × criticality ÷ cost; **measured 95% sensing-traffic reduction** vs uniform refresh on a 20-region scenario), fail-closed `CoherentSensorGroup` fusion gates (time/phase/geometry bounds; five denial paths tested), policy-authorized RIS/movable-antenna actuation receipts, and purpose-scoped `TaskSufficientRepresentation` leakage validation. (3) New modality surfaces: BLE Channel Sounding adapter + `ble_cs_range` treating phase-slope and RTT as **separate cross-validated evidence** (exact distance recovery on synthetic tones; relay-style divergence flagged, never averaged), delay-Doppler-native `FieldAxis` + `delay_doppler_map` (unit-peak tone test), IEEE P3162 synthetic-aperture import profile. (4) RePos-factorized pose head (relative skeleton on the content representation, root on the geometry-conditioned one, calibrated per-joint uncertainties): held-out-room MPJPE 0.0003 m vs 0.2534 m for the monolithic baseline in the room-shortcut leakage experiment; ≤2% structured-adapter budget (740 params). (5) Age gate input now `log(1+age_ms)` per the age-aware-CSI recipe (gradient check re-proven); Gaussian primitives gained `first_seen_ns`/`doppler_variance`/bounded `source_receipts` lineage; `PartitionKey` gained a `session` dimension and `SplitManifest` certifies disjointness across all seven dimensions. 87 tests, 0 failed; crate clippy-clean. Docker images unaffected (no shipped binary consumes the crate yet); Python proof re-verified PASS.
|
||||
@@ -25,8 +24,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- **`archive/v1` (the original pure-Python implementation) formally deprecated (ADR-187)** — commits `1fb5397dd`, `b1417fb6e`; refs #509, #1125. Added `archive/v1/DEPRECATED.md` (a loud tombstone) and a `> ⚠️ DEPRECATED` notice atop `archive/v1/README.md`, both pointing at the maintained `v2/` workspace and the `wifi-densepose 2.x` / `ruview` pip wheel (ADR-117). Records the honest fact behind #509: `archive/v1`'s `DensePoseHead` is **architecture-only** — random `kaiming_normal_` init with **zero committed checkpoints** under `archive/v1/` (MEASURED by Glob over `**/*.{pth,onnx,safetensors,pt,ckpt,bin}`). The ADR-028 deterministic proof `archive/v1/data/proof/verify.py` stays live and is explicitly out of scope. The same effort added a **"Model weights: what's real, what's not" three-tier table** to `README.md` + `docs/user-guide.md`, separating real-and-validated checkpoints (presence 82.3% held-out temporal-triplet, MM-Fi pose 82.69% torso-PCK@20, `count_v1`) from the real-but-weak on-device `pose_v1` (PCK@20 = 3.0%, runtime `confidence=0` stub, below the ADR-079 ≥35% target) from the architecture-only `archive/v1` head — and caveated every live single-ESP32 17-keypoint advertisement accordingly. Docs/labeling only; no code or model behavior changed.
|
||||
|
||||
### Fixed
|
||||
- **`docs/huggingface/MODEL_CARD.md` had drifted from the model card actually published on the Hub (issue #1481).** Every filename in its "Files in this repo" table (`pretrained-encoder.onnx`, `pretrained-heads.onnx`, `pretrained.rvf`, `room-profiles.json`) pointed at files never uploaded to `ruvnet/wifi-densepose-pretrained` — only `config.json` existed. Replaced the in-repo card with the content actually live on the Hub (`model.safetensors`, `model-q{2,4,8}.bin`, `node-{1,2}.json`, `presence-head.json`, `csi-embed-v2.*`, honest v1→v2 retraction of the single-class "100%" presence claim) and added a "Using with the Rust sensing server (RVF conversion)" section documenting the `--convert-model`/`--convert-out` and `--model` auto-convert paths that neither card previously mentioned.
|
||||
- **`--convert-model` failed on the published `model.safetensors`: NUL-padded safetensors header rejected by strict JSON parse (issue #1480, #894 follow-up).** The reference safetensors format pads its JSON header to an 8-byte boundary with trailing NUL bytes; `safetensors_to_rvf` (`wifi-densepose-sensing-server/src/model_format.rs`) fed the full declared-length header slice straight to `serde_json::from_slice`, which rejects the padding as "trailing characters." Since the only published full-precision weight file exercises this padding, `--convert-model` could not convert it at all. Fixed by trimming trailing NUL/whitespace bytes before parsing. Pinned by `safetensors_nul_padded_header_converts` (a header padded to the 8-byte boundary, matching the real HF file, converts and round-trips its weights through `ProgressiveLoader`).
|
||||
- **In-server training reconnected — "Start Training" no longer silently no-ops; `/ws/train/progress` streams real progress (ADR-186, issue #1233).** The dashboard's Start Training button POSTed a config, got `success:true`, and nothing happened: `/api/v1/train/start` was a stub that flipped a status string and logged one line, and `/ws/train/progress` 404'd. The full pure-Rust trainer in `training_api.rs` (loads recorded CSI, gradient-descent, exports a `.rvf`) already existed but was **orphaned** — never declared as a module (no `mod training_api;`), so it wasn't compiled at all. Fix (`wifi-densepose-sensing-server`): declared the module, reconciled `AppStateInner` (replaced the `training_status`/`training_config` stub fields with a shared `TrainingState` status handle + cooperative cancel flag + a `training_progress_tx` broadcast), deleted the stub handlers, and merged the real `training_api::routes()` (so `/api/v1/train/{start,stop,status,pretrain,lora}` and `/ws/train/progress` resolve under the existing `/api/v1/*` bearer gate). The training core was decoupled from the ~60-field server state so it is unit-testable. **P5 honesty guarantee:** with `RUVIEW_DISABLE_SERVER_TRAINING` set, start returns a structured `{enabled:false, cli:"wifi-densepose train-room"}` HTTP 409 — never a silent success — and the dashboard disables the Start buttons with a CLI tooltip (enablement is surfaced on `/api/v1/train/status`). Pinned by 8 new tests incl. a **live-socket** test that completes a genuine 101 WebSocket handshake and receives a real progress frame after a POST start, a full POST→poll-status→`.rvf`-exists round-trip, a path-traversal rejection, cancellation, and the disabled-409 path. `cargo test -p wifi-densepose-sensing-server -p wifi-densepose-train --no-default-features` — 0 failed.
|
||||
- **FastAPI health/metrics endpoints event-loop starvation.** Calling `psutil.cpu_percent(interval=1)` blocked the single-threaded async event loop for 1.0 second on every health check or metrics collection tick, stalling all incoming requests and WebSocket operations. Fixed by changing `cpu_percent` to use non-blocking `interval=None` and offloading all blocking OS metrics gathering to background thread pools via `asyncio.to_thread`. Verified event loop responsiveness via concurrency regression tests.
|
||||
- **EngineBridge now honors `WDP_GUARD_INTERVAL_US`/`WDP_SOFT_GUARD_US`/`WDP_TDM_SLOTS`+`WDP_TDM_SLOT_US`** (#1309, PR #1312, @erichkusuki). The governed trust path previously built its multistatic fuser from a hardcoded `MultistaticConfig::default()` (60 ms guard), so multi-node deployments with WiFi/ESP-NOW time sync (10–150 ms drift) failed every governed cycle regardless of configuration — while the startup log claimed the override took effect. New `StreamingEngine::set_multistatic_config()`; `EngineBridge::new()` takes an `Option<MultistaticConfig>` threaded from the same env-derived config as `AppState.multistatic_fuser`. Hardware-verified on a live 2-node ESP32-S3 setup (90 s window, 0 fusion errors; previously every cycle failed).
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
# ADR-286: `wifi-densepose-sar-harness` — a MetaHarness minted via `vendor/metaharness`
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Status** | Accepted — implemented, **published** |
|
||||
| **Date** | 2026-07-30 |
|
||||
| **Parent** | ADR-287 (`wifi-densepose-sar`, the crate this harness assists development on) |
|
||||
| **Relates to** | ADR-182 (`harness/ruview/`, the first MetaHarness-minted harness in this repo), ADR-285 (`harness/homecore/`, the WASM-first pattern this harness's `@metaharness/kernel` dependency follows) |
|
||||
| **Published** | [`wifi-densepose-sar-harness` v0.1.0](https://www.npmjs.com/package/wifi-densepose-sar-harness) on npm (2026-07-31) |
|
||||
|
||||
## 0. PROOF discipline
|
||||
|
||||
Every claim below about what's "real" versus "illustrative"/"SYNTHETIC" is checked by a passing test in this harness's own suite (14 tests: 5 router + 5 flywheel + 4 install-smoke). Nothing here is asserted without a corresponding `__tests__/*.test.ts` file exercising it.
|
||||
|
||||
## 1. Context
|
||||
|
||||
`wifi-densepose-sar` (ADR-287) is a new, narrowly-scoped research crate. Rather than hand-roll a bespoke development-assistance setup for it, `vendor/metaharness` (the `ruvnet/metaharness` generator, vendored as a git submodule alongside this repo's other `vendor/*` submodules) was used to scaffold one directly: `npx metaharness analyze v2/crates/wifi-densepose-sar --scaffold wifi-densepose-sar-harness --host claude-code` recommended and generated `template: vertical:coding` with four agents (architect/implementer/reviewer/test-writer) and `doctor`/`review-diff` commands — the same generator that produced `harness/ruview/` (ADR-182) and `harness/homecore/` (ADR-285).
|
||||
|
||||
The user's ask that shaped this ADR's scope was specific: wire in **darwin, router, and flywheel** — three complementary `@metaharness/*` packages the base scaffold doesn't include by default (only Darwin Mode ships built-in).
|
||||
|
||||
## 2. Decision
|
||||
|
||||
Land the scaffold at `harness/wifi-densepose-sar/`, and add real wiring for the three requested pieces, each as an actual npm dependency (not a stub, not a `try/catch` optional import):
|
||||
|
||||
1. **`@metaharness/darwin`** (devDependency) — wired by the scaffold itself. `npm run evolve` (real sandbox) / `evolve:dry` (mock sandbox) mutates the harness's own operating config and keeps only measurably-improving changes.
|
||||
2. **`@metaharness/router`** — `src/router.ts` wires a real `Router` (k-NN over labelled examples, cost-optimal selection against a quality bar) with two example model tiers (`cheap-tier` $1/MTok, `frontier-tier` $15/MTok). Exposed as a CLI command (`route <e0> <e1> <e2> <e3>`) with a matching `.claude/commands/route.md` guidance file.
|
||||
3. **`@metaharness/flywheel`** — `src/flywheel.ts` wires the real `runFlywheelGenerations` promotion loop (propose → evaluate → gate → promote, Ed25519-signed, independently replayable via `verifyReplayBundle`) with a SYNTHETIC proposer/evaluator (`dataSource: 'SYNTHETIC'`, no live model call). Exposed as `flywheel [generations]` with a matching `.claude/commands/flywheel.md` guidance file.
|
||||
|
||||
Every new CLI subcommand gets a `.claude/commands/<name>.md` file, matching the pattern the base scaffold's `doctor`/`review-diff` already establish — the MCP tool listing (`mcp__wifi-densepose-sar-harness__*`) is derived from these, so a command without one isn't fully wired into the harness's own guidance surface even if the CLI itself works.
|
||||
|
||||
## 3. What this explicitly is NOT
|
||||
|
||||
- **Not evolving the crate.** Darwin/Flywheel mutate the harness's own operating policy (agent prompts, review checklist depth) — not `wifi-densepose-sar`'s Rust code or its runtime performance. Actually optimizing the crate (the incremental-phasor-rotation work, ADR-287 §7) was done directly, not through this harness's self-improvement loop.
|
||||
- **Not a live routing/promotion system.** The router's labelled examples are illustrative seed data, not measured eval-log observations. The flywheel's proposer/evaluator are deterministic stand-ins, not a real model call or a real coding-task benchmark suite. Both are honestly labeled as such in their own source files and in this harness's `CLAUDE.md`.
|
||||
- **Not manifest-verified.** `.harness/manifest.json`/`manifest.sha256` reflect the initial scaffold output and were not regenerated after adding `router.ts`/`flywheel.ts` — this scaffold has no `manifest:update` script (unlike `harness/homecore/`). Documented as a known gap in the harness's own README.
|
||||
|
||||
## 4. A real bug the flywheel wiring found
|
||||
|
||||
The first version of the SYNTHETIC evaluator returned a constant `noopRate`. `@metaharness/flywheel`'s default promotion gate requires `noopRate` to *strictly improve* generation over generation (one of its five conjunctive clauses) — a constant value, however good, fails that clause forever, so nothing could ever be promoted. Fixed by making `noopRate` actually respond to the (synthetic) policy content; every generation promotes now. Kept as a cautionary note in `src/flywheel.ts`'s comments: a flywheel evaluator with a frozen metric is silently broken, not silently fine.
|
||||
|
||||
## 5. Consequences
|
||||
|
||||
- 14 tests (5 router + 5 flywheel + 4 install-smoke), 0 failed; `npm run build` clean under strict TypeScript.
|
||||
- Published to npm as `wifi-densepose-sar-harness` v0.1.0 — `npx wifi-densepose-sar-harness init` works from a cold install.
|
||||
- No risk to any other harness or crate in this repo — this harness only reads/assists on `wifi-densepose-sar`, and its MCP server, memory namespace, and Claude Code plugin are scoped to its own name.
|
||||
@@ -1,67 +0,0 @@
|
||||
# ADR-287: `wifi-densepose-sar` — coherent wideband RF tomography research crate
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Status** | Accepted — implemented (P1), **published** |
|
||||
| **Date** | 2026-07-30 |
|
||||
| **Parent** | ADR-278 (radar inverse rendering research program), ADR-282 (mandatory L0–L5 evidence ladder) |
|
||||
| **Relates to** | ADR-273/274 (`ruview-unified`'s `FmcwRadarCube` adapter, the eventual integration point), ADR-275 (`GaussianMap`, ditto), ADR-286 (`wifi-densepose-sar-harness`, the MetaHarness minted for this crate) |
|
||||
| **Published** | [`wifi-densepose-sar` v0.3.1](https://crates.io/crates/wifi-densepose-sar) on crates.io (2026-07-31) |
|
||||
|
||||
## 0. PROOF discipline
|
||||
|
||||
Every accuracy number this crate produces is **SYNTHETIC / evidence level L0** (ADR-282): generated by the crate's own forward simulator (`measurement::simulate_measurement`), scored against its own known ground truth (`ScatteringTarget` positions). Nothing here has been validated against real wideband RF hardware, and the crate contains no such hardware integration.
|
||||
|
||||
## 1. Context
|
||||
|
||||
A YC-backed company, Applied Electrodynamics ("WaveSight"), publicly launched a handheld "camera that can see through walls" using undisclosed radio-imaging technology. Comparing it against this repo's capabilities surfaced a real gap: `wifi-densepose-signal::ruvsense::tomography` implements *radio tomographic imaging* (Wilson & Patwari 2010) — RSS-based shadowing attenuation on a fixed-link topology, no coherent phase, no multi-frequency stepping, no synthetic aperture. It is a different technique from what a SAR-style through-wall imager needs: coherent, wideband, multi-position backprojection.
|
||||
|
||||
`ruview-unified`'s `FmcwRadarCube` adapter (ADR-274) already normalizes wideband radar cubes into range profiles per position, and ADR-278 already names a radar-cube-output extension of the ADR-276 synthetic world generator as the intended sandbox for any future radar-inverse research. Neither, before this ADR, contained an actual backprojection reconstruction kernel — the primitive every candidate technique (matched-filter SAR, GPR imaging, RISE/DiffRadar-style inversion) is built on.
|
||||
|
||||
## 2. Decision
|
||||
|
||||
Ship `wifi-densepose-sar` as a standalone leaf crate (the `nvsim` pattern: pure Rust, deterministic ChaCha20 seeding, zero coupling to `wifi-densepose-hardware` or any real ingestion path) implementing:
|
||||
|
||||
1. **Forward measurement model** (`measurement.rs`): simulates the complex, stepped-frequency returns a monostatic synthetic-aperture radar would record from known point scatterers — `y_{m,k} = Σ_j σ_j/R_{m,j}² · exp(-i·4π·f_k·R_{m,j}/c) + noise`.
|
||||
2. **Backprojection reconstruction** (`reconstruct.rs`): the matched-filter inverse of (1) onto a 3D voxel grid, parallelized over voxels (rayon).
|
||||
3. **Point-cloud extraction** (`pointcloud.rs`): threshold + local-maximum extraction from the dense voxel image.
|
||||
4. **Closed-form resolution/coherence formulas** (`resolution.rs`): `ΔR = c/2B` (range resolution), `δ_CR ≈ λR/2L` (cross-range/synthetic-aperture resolution), `Δp ≤ λ/8` (antenna-pose coherence budget, derived from a quarter-wavelength round-trip-path tolerance) — checked against the reconstruction's actual behavior in `tests/physics_validation.rs`, not merely documented.
|
||||
|
||||
This is deliberately scoped **one level below** ADR-278's RISE/DiffRadar/GeRaF reproduction program: it is the bare measurement-model + backprojection primitive, not a reproduction of any specific published system, and not a claim about Applied Electrodynamics' undisclosed product (their waveform, antenna count, bandwidth, and algorithm are unknown; this crate applies the same well-established SAR/GPR physics — see Skolnik, *Radar Handbook* — to synthetic data).
|
||||
|
||||
## 3. What this explicitly is NOT
|
||||
|
||||
- Not a hardware driver. No VNA/SDR/wideband-RF-frontend code exists anywhere in this crate or was added to `wifi-densepose-hardware`.
|
||||
- Not wired into `ruview-unified`'s `FmcwRadarCube` adapter or `GaussianMap`. That integration is real future work (§5), deliberately deferred so the reconstruction physics validates in isolation first — the same staging ADR-278 §2.3 already prescribes ("sandbox-first... before hardware").
|
||||
- Not a reproduction of RISE, DiffRadar, or GeRaF. ADR-278's gates (G1–G4) are untouched by this ADR.
|
||||
- Not a real-world through-wall imaging performance claim. The forward model is free-space propagation only — no multipath, no per-material attenuation, no antenna gain pattern, no receiver noise figure. Real-world performance depends on all of these.
|
||||
|
||||
## 4. Simplifications (honesty boundary)
|
||||
|
||||
- **Monostatic, not MIMO.** A single antenna acts as both transmitter and receiver at each synthetic-aperture position (the standard stripmap-SAR simplification), not a multi-element MIMO array. Extending to bistatic/MIMO `(m, n)` transmitter/receiver pairs is straightforward given the existing measurement-model structure but not implemented.
|
||||
- **Isotropic antenna, no gain pattern.** Every antenna position radiates/receives equally in all directions.
|
||||
- **Free-space propagation only.** No multipath, no material transmission/reflection/attenuation (contrast `ruview-unified::synth::room`'s Fresnel material model, which is narrowband-CW and not yet extended to wideband — a natural follow-up, §5).
|
||||
- **`1/R²` two-way amplitude falloff, no calibration.** Real receivers have finite dynamic range, noise figures, and require calibration against a known reference target; none of that is modeled.
|
||||
|
||||
## 5. Follow-up (not in this ADR's scope)
|
||||
|
||||
1. Extend `ruview-unified::synth::room`'s image-method ray tracer to emit wideband stepped-frequency multi-position cubes (per ADR-278 §2.3), and wire `wifi-densepose-sar::reconstruct` against that richer (multipath-aware) synthetic generator instead of the free-space-only model here.
|
||||
2. A `ruview-unified` integration adapter converting `ReflectivityImage`/`PointCloudPoint` output into `RfGaussian`/`GaussianMap` primitives (ADR-278 §2.4's stated integration contract).
|
||||
3. Bistatic/MIMO measurement model.
|
||||
4. Any of ADR-278's actual gated reproductions (RISE first), if and when that program proceeds — this crate would be a component, not a substitute.
|
||||
|
||||
## 6. Consequences
|
||||
|
||||
- The workspace gains a real (if intentionally scoped-down) coherent-imaging primitive where before there was none — useful groundwork for ADR-278 if that research program proceeds, and a direct, honest answer to "could this repo build a WaveSight-like device" (no, not without the hardware program described in the motivating comparison; yes, this is the reconstruction-algorithm groundwork such a program would need).
|
||||
- Zero risk to the existing `wifi-densepose-signal::ruvsense::tomography` (RSS-based RTI) code path or any production pipeline — this crate is not referenced by any of them.
|
||||
- 25 tests (22 unit + 3 integration physics-validation), 0 failed, clippy-clean. Criterion bench: MEASURED 512/4096/32768-voxel backprojection reconstruction throughput (see crate README for the numbers as last recorded). The incremental-phasor-rotation optimization (§7) cut reconstruction time ~4.4-4.5x, proven equivalent to the direct per-frequency computation it replaced.
|
||||
|
||||
## 7. Follow-up optimization: incremental phasor rotation (2026-07-30, MEASURED)
|
||||
|
||||
`focus_at_point` originally called `Complex64::from_polar` (one `sin`/`cos` pair) per (pose, frequency) term. Since [`FrequencySweep::frequencies`](../../v2/crates/wifi-densepose-sar/src/measurement.rs) produces evenly-spaced frequencies by construction, the per-term phase is an arithmetic progression in the frequency index — so the phasor can be evaluated once per pose and advanced by a fixed complex-multiply step per frequency, replacing K trig evaluations with 2. `focus_at_point`'s signature changed from a raw `&[f64]` frequency slice to `&FrequencySweep`, making the evenly-spaced-frequencies precondition this optimization depends on a type-level invariant rather than a caller-observed one.
|
||||
|
||||
**MEASURED (criterion regression detection, p < 0.001): ~4.4-4.5x faster** across 512/4096/32768-voxel grids. **Proven equivalent**, not just faster: `reconstruct::tests::backprojection_incremental_rotation_matches_direct_per_frequency_computation` checks the optimized path against an independently reimplemented direct per-frequency reference, across four sweep sizes (including the `n_steps=1` degenerate case) and both on-target and off-target evaluation points, to <1e-9 relative error.
|
||||
|
||||
## 8. Published (2026-07-31)
|
||||
|
||||
`wifi-densepose-sar` v0.3.1 is live on [crates.io](https://crates.io/crates/wifi-densepose-sar) — `cargo add wifi-densepose-sar` resolves it from any Rust project. A MetaHarness minted for this crate (ADR-286, `wifi-densepose-sar-harness`) is published to npm alongside it. Publishing happened after this ADR's implementation and §7 optimization landed; no code changed as part of publishing itself.
|
||||
+1
-3
@@ -9,7 +9,7 @@ Latest proposed decisions:
|
||||
- [ADR-264: Versioned wire protocol for RTL8720F CFR and Range-FFT reports](ADR-264-rtl8720f-radar-wire-protocol.md)
|
||||
- [ADR-263: Adopt RTL8720F 2.4 GHz FMCW radar as an optional RuView sensing platform](ADR-263-rtl8720f-2-4ghz-fmcw-radar-platform.md)
|
||||
|
||||
This folder contains 210 Architecture Decision Records (ADRs) that document every significant technical choice in the RuView / WiFi-DensePose project. (The index tables below list a curated subset per domain; see the directory listing for the full set.)
|
||||
This folder contains 208 Architecture Decision Records (ADRs) that document every significant technical choice in the RuView / WiFi-DensePose project. (The index tables below list a curated subset per domain; see the directory listing for the full set.)
|
||||
|
||||
## Why ADRs?
|
||||
|
||||
@@ -142,9 +142,7 @@ Statuses: **Proposed** (under discussion), **Accepted** (approved and/or impleme
|
||||
| [ADR-280](ADR-280-active-sensing-programmable-perception.md) | Active sensing & programmable perception control plane | Accepted (implemented) |
|
||||
| [ADR-281](ADR-281-ble-cs-delay-doppler-pose-factorization.md) | BLE Channel Sounding, delay-Doppler tensors, P3162 import, factorized pose | Accepted (implemented) |
|
||||
| [ADR-282](ADR-282-ruview-ecosystem-positioning.md) | Ecosystem positioning + mandatory L0–L5 evidence ladder | Accepted |
|
||||
| [ADR-287](ADR-287-coherent-wideband-rf-tomography-crate.md) | `wifi-densepose-sar` — coherent wideband RF tomography research crate | Accepted (implemented, published) |
|
||||
| [ADR-285](ADR-285-homecore-wasm-first-metaharness.md) | WASM-first Homecore developer metaharness via `npx homecore` | Accepted (implemented and validated) |
|
||||
| [ADR-286](ADR-286-wifi-densepose-sar-harness-via-metaharness.md) | `wifi-densepose-sar-harness` — MetaHarness with darwin/router/flywheel | Accepted (implemented, published) |
|
||||
|
||||
---
|
||||
|
||||
|
||||
+275
-206
@@ -2,266 +2,335 @@
|
||||
license: mit
|
||||
tags:
|
||||
- wifi-sensing
|
||||
- pose-estimation
|
||||
- vital-signs
|
||||
- presence-detection
|
||||
- edge-ai
|
||||
- esp32
|
||||
- onnx
|
||||
- self-supervised
|
||||
- cognitum
|
||||
- csi
|
||||
- through-wall
|
||||
- privacy-preserving
|
||||
- spiking-neural-network
|
||||
- ruvector
|
||||
language:
|
||||
- en
|
||||
library_name: onnxruntime
|
||||
pipeline_tag: other
|
||||
---
|
||||
|
||||
<!--
|
||||
This file mirrors the README.md actually published at
|
||||
https://huggingface.co/ruvnet/wifi-densepose-pretrained (issue #1481: the two
|
||||
had drifted, and every filename in the old "Files in this repo" table pointed
|
||||
at files that were never uploaded). Update this file whenever the Hub README
|
||||
changes — `scripts/publish-huggingface.sh` uploads whatever is in
|
||||
`dist/models/README.md`, which is a separate file from this one, so keeping
|
||||
them in sync is a manual step until that's automated.
|
||||
# WiFi-DensePose: See Through Walls with WiFi + AI
|
||||
|
||||
The "Using with the Rust sensing server" section below is not on the Hub page
|
||||
— it documents the `--convert-model` / `--model` RVF conversion path
|
||||
(issue #894, #1480) that HF users of this repo actually need and that the
|
||||
upstream card doesn't cover.
|
||||
-->
|
||||
**Detect people, track movement, and measure breathing -- through walls, without cameras, using a $27 sensor kit.**
|
||||
|
||||
# RuView — WiFi Sensing Models
|
||||
| | |
|
||||
|---|---|
|
||||
| **License** | MIT |
|
||||
| **Framework** | ONNX Runtime |
|
||||
| **Hardware** | ESP32-S3 ($9) + optional Cognitum Seed ($15) |
|
||||
| **Training** | Self-supervised contrastive learning (no labels needed) |
|
||||
| **Privacy** | No cameras, no images, no personally identifiable data |
|
||||
|
||||
**Turn WiFi signals into spatial intelligence.** Detect people, measure breathing and heart rate, track movement, and monitor rooms — through walls, in the dark, with no cameras. Just radio physics.
|
||||
---
|
||||
|
||||
## What This Does
|
||||
## What is this?
|
||||
|
||||
WiFi signals bounce off people. When someone breathes, their chest moves the air, which subtly changes the WiFi signal. When they walk, the changes are bigger. This model learned to read those changes from a $9 ESP32 chip.
|
||||
This model turns ordinary WiFi signals into a human sensing system. It can detect whether someone is in a room, count how many people are present, classify what they are doing, and even measure their breathing rate -- all without any cameras.
|
||||
|
||||
| What it senses | How well | Without |
|
||||
|----------------|----------|---------|
|
||||
| **Is someone there?** | presence detection (v1 "100%" retracted — single-class) | No camera needed |
|
||||
| **Are they moving?** | Detects typing vs walking vs standing | No wearable needed |
|
||||
| **Breathing rate** | 6-30 BPM, contactless | No chest strap |
|
||||
| **Heart rate** | 40-120 BPM, through clothes | No smartwatch |
|
||||
| **How many people?** | 1-4, via subcarrier graph analysis | No headcount camera |
|
||||
| **Through walls** | Works through drywall, wood, fabric | No line of sight |
|
||||
| **Sleep quality** | Deep/Light/REM/Awake classification | No mattress sensor |
|
||||
| **Fall detection** | <2 second alert | No pendant |
|
||||
**How does it work?** Every WiFi router constantly sends signals that bounce off walls, furniture, and people. When a person moves -- or even just breathes -- those bouncing signals change in tiny but measurable ways. WiFi chips can capture these changes as numbers called *Channel State Information* (CSI). Think of it like ripples in a pond: drop a stone and the ripples tell you something happened, even if you cannot see the stone.
|
||||
|
||||
## 🆕 v2 update — honest re-benchmark + properly-converged encoder (2026-05-31)
|
||||
This model learned to read those "WiFi ripples" and figure out what is happening in the room. It was trained using a technique called *contrastive learning*, which means it taught itself by comparing thousands of WiFi signal snapshots -- no human had to manually label anything.
|
||||
|
||||
The v1 contrastive encoder shipped with a **flat training loss** (every epoch logged the
|
||||
same `0.13517` — the optimizer was not actually learning), and its headline **"100% presence
|
||||
accuracy" was measured on a single-class recording** (an overnight capture of one sleeping
|
||||
person: **6,062 of 6,063** frames are labelled "present", 1 is "absent"). A constant
|
||||
"yes" predictor scores 99.98% on that split — so the number is real but **says nothing about
|
||||
generalization.** We are correcting that publicly rather than leaving it to stand.
|
||||
The result is a small, fast model that runs on a $9 microcontroller and preserves complete privacy because it never captures images or audio.
|
||||
|
||||
**v2 retrains the same `8 -> 64 -> 128` encoder with a working InfoNCE objective** and reports
|
||||
an **honest, label-free, time-disjoint metric**: held-out **temporal-triplet accuracy** =
|
||||
P( d(anchor, temporal-positive) < d(anchor, temporal-negative) ), evaluated on the **last 20%
|
||||
of the recording by time** (no leakage into training).
|
||||
---
|
||||
|
||||
| Encoder | Held-out temporal-triplet accuracy | Notes |
|
||||
|---------|-----------------------------------:|-------|
|
||||
| Raw 8-dim features (no encoder) | 66.4% | baseline |
|
||||
| Random-init encoder | 69.6% | untrained |
|
||||
| **v2 trained encoder** | **82.3%** | **+15.9 pts over raw, properly converged** |
|
||||
## What can it do?
|
||||
|
||||
**Plain language:** the embedding now reliably places two CSI snapshots taken moments apart
|
||||
*closer together* than two taken far apart — i.e. it has learned the temporal structure of the
|
||||
radio environment, which is exactly what a useful self-supervised sensing embedding should do.
|
||||
v1, with its flat loss, was barely better than random on this same test.
|
||||
| Capability | Accuracy | What you need | Notes |
|
||||
|---|---|---|---|
|
||||
| **Presence detection** | >95% | 1x ESP32-S3 ($9) | Is anyone in the room? |
|
||||
| **Motion classification** | >90% | 1x ESP32-S3 ($9) | Still, walking, exercising, fallen |
|
||||
| **Breathing rate** | +/- 2 BPM | 1x ESP32-S3 ($9) | Best when person is sitting or lying still |
|
||||
| **Heart rate estimate** | +/- 5 BPM | 1x ESP32-S3 ($9) | Experimental -- less accurate during movement |
|
||||
| **Person counting** | 1-4 people | 2x ESP32-S3 ($18) | Uses cross-node signal fusion |
|
||||
| **Pose estimation** | 17 COCO keypoints | 2x ESP32-S3 + Seed ($27) | Full skeleton: head, shoulders, elbows, etc. |
|
||||
|
||||
**Technical:** 2-layer FC (BatchNorm + GELU) -> L2-normalized 128-dim embedding, 9,280
|
||||
params, trained with InfoNCE (temperature 0.1, in-batch + temporal-far negatives), AdamW, 60 epochs.
|
||||
Temporal positives within 2 s; negatives >30 s apart. Time-disjoint 80/20 split.
|
||||
|
||||
### v2 files & proof
|
||||
| File | Size | Use |
|
||||
|------|------|-----|
|
||||
| `csi-embed-v2.safetensors` | ~40 KB | fp32 trained encoder |
|
||||
| `csi-embed-v2-int4.bin` | **4.56 KB** | 4-bit packed encoder + fp16 standardizer — **fits the 8 KB ESP32 SRAM budget** |
|
||||
| `csi-embed-v2.py` | <1 KB | `Enc` definition + loader |
|
||||
| `csi-embed-v2-metrics.json` | — | full honest metrics + quantization scales |
|
||||
|
||||
- Encoder weights SHA-256: `3b37bca66e6050c50ccbc0f6e0501824f258bfdd8675dc0f4541b1e2e96feecd`
|
||||
- Repro: `python aether-arena/staging/train_csi_embed.py` in [github.com/ruvnet/RuView](https://github.com/ruvnet/RuView)
|
||||
- Trained on the same local capture (`data/recordings/overnight-1775217646.csi.jsonl`, 6,063 feature frames).
|
||||
|
||||
> **What v2 does *not* claim.** This is one room, one capture, two nodes. The triplet metric
|
||||
> measures embedding quality, not downstream presence/vitals accuracy (which needs multi-class,
|
||||
> multi-room labelled data we don't yet have for this 2.4 GHz feature). For *pose* SOTA on a
|
||||
> public benchmark, see the separate 5 GHz model
|
||||
> [`ruvnet/wifi-densepose-mmfi-pose`](https://huggingface.co/ruvnet/wifi-densepose-mmfi-pose)
|
||||
> (82.69% torso-PCK@20 on MM-Fi).
|
||||
|
||||
## Benchmarks
|
||||
|
||||
Validated on real hardware (Apple M4 Pro + 2x ESP32-S3):
|
||||
|
||||
| Metric | Result | Context |
|
||||
|--------|--------|---------|
|
||||
| **CSI embedding quality** | **82.3% held-out** | Honest temporal-triplet metric; v1 single-class "100% presence" retracted (#882) |
|
||||
| **Inference speed** | **0.008 ms** | 125,000x faster than real-time |
|
||||
| **Throughput** | **164,183 emb/sec** | One laptop handles 1,600+ sensors |
|
||||
| **Contrastive learning** | **51.6% improvement** | Trained on 8 hours of overnight data |
|
||||
| **Model size** | **8 KB** (4-bit quantized) | Fits in ESP32 SRAM |
|
||||
| **Training time** | **12 minutes** | On Mac Mini M4 Pro, no GPU needed |
|
||||
| **Camera required** | **No** | Trained from 10 sensor signals |
|
||||
|
||||
## Models in This Repo
|
||||
|
||||
| File | Size | Use |
|
||||
|------|------|-----|
|
||||
| `model.safetensors` | 48 KB | Full contrastive encoder (128-dim embeddings) |
|
||||
| `model-q4.bin` | 8 KB | **Recommended** — 4-bit quantized, 8x compression |
|
||||
| `model-q2.bin` | 4 KB | Ultra-compact for ESP32 edge inference |
|
||||
| `model-q8.bin` | 16 KB | High quality 8-bit |
|
||||
| `presence-head.json` | 2.6 KB | Presence detection head (v1 "100%" retracted — single-class; #882) |
|
||||
| `node-1.json` | 21 KB | LoRA adapter for room/node 1 |
|
||||
| `node-2.json` | 21 KB | LoRA adapter for room/node 2 |
|
||||
| `config.json` | 586 B | Model configuration |
|
||||
| `training-metrics.json` | 3.1 KB | Loss curves and training history |
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Download models
|
||||
pip install huggingface_hub
|
||||
huggingface-cli download ruv/ruview --local-dir models/
|
||||
### Install
|
||||
|
||||
# Use with RuView sensing pipeline
|
||||
```bash
|
||||
pip install onnxruntime numpy
|
||||
```
|
||||
|
||||
### Run inference
|
||||
|
||||
```python
|
||||
import onnxruntime as ort
|
||||
import numpy as np
|
||||
|
||||
# Load the encoder model
|
||||
session = ort.InferenceSession("pretrained-encoder.onnx")
|
||||
|
||||
# Simulated 8-dim CSI feature vector from ESP32-S3
|
||||
# Dimensions: [amplitude_mean, amplitude_std, phase_slope, doppler_energy,
|
||||
# subcarrier_variance, temporal_stability, csi_ratio, spectral_entropy]
|
||||
features = np.array(
|
||||
[[0.45, 0.30, 0.69, 0.75, 0.50, 0.25, 0.00, 0.54]],
|
||||
dtype=np.float32,
|
||||
)
|
||||
|
||||
# Encode into 128-dim embedding
|
||||
result = session.run(None, {"input": features})
|
||||
embedding = result[0] # shape: (1, 128)
|
||||
print(f"Embedding shape: {embedding.shape}")
|
||||
print(f"First 8 values: {embedding[0][:8]}")
|
||||
```
|
||||
|
||||
### Run task heads
|
||||
|
||||
```python
|
||||
# Load the task heads model
|
||||
heads = ort.InferenceSession("pretrained-heads.onnx")
|
||||
|
||||
# Feed the embedding from the encoder
|
||||
predictions = heads.run(None, {"embedding": embedding})
|
||||
|
||||
presence_score = predictions[0] # 0.0 = empty, 1.0 = occupied
|
||||
person_count = predictions[1] # estimated count (float, round to int)
|
||||
activity_class = predictions[2] # [still, walking, exercise, fallen]
|
||||
vitals = predictions[3] # [breathing_bpm, heart_bpm]
|
||||
|
||||
print(f"Presence: {presence_score[0]:.2f}")
|
||||
print(f"People: {int(round(person_count[0]))}")
|
||||
print(f"Activity: {['still', 'walking', 'exercise', 'fallen'][activity_class.argmax()]}")
|
||||
print(f"Breathing: {vitals[0][0]:.1f} BPM")
|
||||
print(f"Heart: {vitals[0][1]:.1f} BPM")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Model Architecture
|
||||
|
||||
```
|
||||
+-- Presence (binary)
|
||||
|
|
||||
WiFi signals --> ESP32-S3 --> 8-dim features --> Encoder (TCN) --> 128-dim embedding --> Task Heads --+-- Person Count
|
||||
(CSI) (on-device) (~2.5M params) (~100K) |
|
||||
+-- Activity (4 classes)
|
||||
|
|
||||
+-- Vitals (BR + HR)
|
||||
```
|
||||
|
||||
### Encoder
|
||||
|
||||
- **Type:** Temporal Convolutional Network (TCN)
|
||||
- **Input:** 8-dimensional feature vector extracted from raw CSI
|
||||
- **Output:** 128-dimensional embedding
|
||||
- **Parameters:** ~2.5M
|
||||
- **Format:** ONNX (runs on any platform with ONNX Runtime)
|
||||
|
||||
### Task Heads
|
||||
|
||||
- **Type:** Small MLPs (multi-layer perceptrons), one per task
|
||||
- **Input:** 128-dim embedding from the encoder
|
||||
- **Output:** Task-specific predictions (presence, count, activity, vitals)
|
||||
- **Parameters:** ~100K total across all heads
|
||||
- **Format:** ONNX
|
||||
|
||||
### Feature extraction (runs on ESP32-S3)
|
||||
|
||||
The ESP32-S3 captures raw CSI frames at ~100 Hz and computes 8 summary features per window:
|
||||
|
||||
| Feature | Description |
|
||||
|---|---|
|
||||
| `amplitude_mean` | Average signal strength across subcarriers |
|
||||
| `amplitude_std` | Variation in signal strength (movement indicator) |
|
||||
| `phase_slope` | Rate of phase change across subcarriers |
|
||||
| `doppler_energy` | Energy in the Doppler spectrum (velocity indicator) |
|
||||
| `subcarrier_variance` | How much individual subcarriers differ |
|
||||
| `temporal_stability` | Consistency of signal over time (stillness indicator) |
|
||||
| `csi_ratio` | Ratio between antenna pairs (direction indicator) |
|
||||
| `spectral_entropy` | Randomness of the frequency spectrum |
|
||||
|
||||
---
|
||||
|
||||
## Training Data
|
||||
|
||||
### How it was trained
|
||||
|
||||
This model was trained using **self-supervised contrastive learning**, which means it learned entirely from unlabeled WiFi signals. No cameras, no manual annotations, and no privacy-invasive data collection were needed.
|
||||
|
||||
The training process works like this:
|
||||
|
||||
1. **Collect** raw CSI frames from ESP32-S3 nodes placed in a room
|
||||
2. **Extract** 8-dimensional feature vectors from sliding windows of CSI data
|
||||
3. **Contrast** -- the model learns that features from nearby time windows should produce similar embeddings, while features from different scenarios should produce different embeddings
|
||||
4. **Fine-tune** task heads — *planned:* weak labels from environmental sensors (PIR motion, temperature, pressure) on the Cognitum Seed companion device. **This environmental-sensor ground-truth path is not yet implemented** (no PIR/BME280 ingestion in the training pipeline today); current task-head supervision uses the proxy/camera labels described elsewhere.
|
||||
|
||||
### Data provenance
|
||||
|
||||
- **Source:** Live CSI from 2x ESP32-S3 nodes (802.11n, HT40, 114 subcarriers)
|
||||
- **Volume:** ~360,000 CSI frames (~3,600 feature vectors) per collection run
|
||||
- **Environment:** Residential room, ~4x5 meters
|
||||
- **Ground truth:** *Planned* — environmental sensors on the Cognitum Seed (PIR, BME280, light). Not yet wired into training; treat the PIR/BME280 references in this card as the intended design, not a current capability.
|
||||
- **Attestation:** Every collection run produces a cryptographic witness chain (`collection-witness.json`) that proves data provenance and integrity
|
||||
|
||||
### Witness chain
|
||||
|
||||
The `collection-witness.json` file contains a chain of SHA-256 hashes linking every step from raw CSI capture through feature extraction to model training. This allows anyone to verify that the published model was trained on data collected by specific hardware at a specific time.
|
||||
|
||||
---
|
||||
|
||||
## Hardware Requirements
|
||||
|
||||
### Minimum: single-node sensing ($9)
|
||||
|
||||
| Component | What it does | Cost | Where to get it |
|
||||
|---|---|---|---|
|
||||
| ESP32-S3 (8MB flash) | Captures WiFi CSI + runs feature extraction | ~$9 | Amazon, AliExpress, Adafruit |
|
||||
| USB-C cable | Power + data | ~$3 | Any electronics store |
|
||||
|
||||
This gets you: presence detection, motion classification, breathing rate.
|
||||
|
||||
### Recommended: dual-node sensing ($18)
|
||||
|
||||
Add a second ESP32-S3 to enable cross-node signal fusion for better accuracy and person counting.
|
||||
|
||||
### Full setup: sensing + ground truth ($27)
|
||||
|
||||
| Component | What it does | Cost |
|
||||
|---|---|---|
|
||||
| 2x ESP32-S3 (8MB) | WiFi CSI sensing nodes | ~$18 |
|
||||
| Cognitum Seed (Pi Zero 2W) | Runs inference + collects ground truth | ~$15 |
|
||||
| USB-C cables (x3) | Power + data | ~$9 |
|
||||
| **Total** | | **~$27** |
|
||||
|
||||
The Cognitum Seed runs the ONNX models on-device and orchestrates the ESP32 nodes over USB serial. (Using its onboard PIR/BME280 sensors as training ground truth is planned but not yet implemented — see "Data provenance" above.)
|
||||
|
||||
---
|
||||
|
||||
## Files in this repo
|
||||
|
||||
| File | Size | Description |
|
||||
|---|---|---|
|
||||
| `pretrained-encoder.onnx` | ~2 MB | Contrastive encoder (TCN backbone, 8-dim input, 128-dim output) |
|
||||
| `pretrained-heads.onnx` | ~100 KB | Task heads (presence, count, activity, vitals) |
|
||||
| `pretrained.rvf` | ~500 KB | RuVector format embeddings for advanced fusion pipelines |
|
||||
| `room-profiles.json` | ~10 KB | Environment calibration profiles (room geometry, baseline noise) |
|
||||
| `collection-witness.json` | ~5 KB | Cryptographic witness chain proving data provenance |
|
||||
| `config.json` | ~2 KB | Training configuration (hyperparameters, feature schema, versions) |
|
||||
| `README.md` | -- | This file |
|
||||
|
||||
### RuVector format (.rvf)
|
||||
|
||||
The `.rvf` file contains pre-computed embeddings in RuVector format, used by the RuView application for advanced multi-node fusion and cross-viewpoint pose estimation. You only need this if you are using the full RuView pipeline. For basic inference, the ONNX files are sufficient.
|
||||
|
||||
---
|
||||
|
||||
## How to use with RuView
|
||||
|
||||
[RuView](https://github.com/ruvnet/RuView) is the open-source application that ties everything together: firmware flashing, real-time sensing, and a browser-based dashboard.
|
||||
|
||||
### 1. Flash firmware to ESP32-S3
|
||||
|
||||
```bash
|
||||
git clone https://github.com/ruvnet/RuView.git
|
||||
cd RuView
|
||||
|
||||
# Flash an ESP32-S3 ($9 on Amazon/AliExpress)
|
||||
python -m esptool --chip esp32s3 --port COM9 --baud 460800 \
|
||||
write_flash 0x0 bootloader.bin 0x8000 partition-table.bin \
|
||||
0xf000 ota_data_initial.bin 0x20000 esp32-csi-node.bin
|
||||
|
||||
# Provision WiFi
|
||||
python firmware/esp32-csi-node/provision.py --port COM9 \
|
||||
--ssid "YourWiFi" --password "secret" --target-ip YOUR_IP
|
||||
|
||||
# See what WiFi reveals about your room
|
||||
node scripts/deep-scan.js --bind YOUR_IP --duration 10
|
||||
# Flash firmware (requires ESP-IDF v5.4 or use pre-built binaries from Releases)
|
||||
# See the repo README for platform-specific instructions
|
||||
```
|
||||
|
||||
## Using with the Rust sensing server (RVF conversion)
|
||||
|
||||
`model.safetensors` does not carry the `RVFS` binary-container magic that
|
||||
`wifi-densepose-sensing-server`'s `--model` loader expects natively — it needs
|
||||
converting first (issue #894). As of #1480, `--model` auto-detects and
|
||||
converts `model.safetensors` / `model.rvf.jsonl` in-memory, so the one-liner
|
||||
below is enough for most uses:
|
||||
### 2. Download models
|
||||
|
||||
```bash
|
||||
cargo run -p wifi-densepose-sensing-server -- --model model.safetensors
|
||||
pip install huggingface_hub
|
||||
huggingface-cli download ruvnet/wifi-densepose-pretrained --local-dir models/
|
||||
```
|
||||
|
||||
To pre-convert once and skip re-conversion on every startup (recommended for
|
||||
repeated runs, or to inspect the converted container), use `--convert-model`:
|
||||
### 3. Run inference
|
||||
|
||||
```bash
|
||||
cargo run -p wifi-densepose-sensing-server -- \
|
||||
--convert-model model.safetensors --convert-out model.rvf
|
||||
# Start the CSI bridge (connects ESP32 serial output to the inference pipeline)
|
||||
python scripts/seed_csi_bridge.py --port COM7 --model models/pretrained-encoder.onnx
|
||||
|
||||
cargo run -p wifi-densepose-sensing-server -- \
|
||||
--model model.rvf --load-rvf model.rvf
|
||||
# Or run the full sensing server with web dashboard
|
||||
cargo run -p wifi-densepose-sensing-server
|
||||
```
|
||||
|
||||
`--model` loads the weights for inference; `--load-rvf` separately populates
|
||||
the container metadata that `/api/v1/model/info` reports — pass both if you
|
||||
want that endpoint to reflect the loaded container.
|
||||
### 4. Adapt to your room
|
||||
|
||||
Converting `model.safetensors` wires the format/load path (magic, version,
|
||||
segments, weights all valid) but the pose-decoder *architecture* published on
|
||||
HF differs from this crate's inference head, so the converted weights are not
|
||||
claimed to reproduce pose accuracy end-to-end (tracked in #894). `model-q2/q4/q8.bin`
|
||||
(quantized HF blobs) have no reader in this build yet — convert the
|
||||
full-precision `model.safetensors` instead.
|
||||
The model works best after a brief calibration period (~60 seconds of no movement) to learn the baseline signal characteristics of your specific room. The `room-profiles.json` file contains example profiles; the system will create one for your environment automatically.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
WiFi signals → ESP32-S3 ($9) → 8-dim features @ 1 Hz → Encoder → 128-dim embedding
|
||||
↓
|
||||
┌──────────────────────────┼──────────────────┐
|
||||
↓ ↓ ↓
|
||||
Presence head Activity head Vitals head
|
||||
(v1 "100%" retracted) (still/walk/talk) (BR, HR)
|
||||
```
|
||||
|
||||
The encoder converts 8 WiFi Channel State Information (CSI) features into a 128-dimensional embedding:
|
||||
|
||||
| Dim | Feature | What it captures |
|
||||
|-----|---------|-----------------|
|
||||
| 0 | Presence | How much the WiFi signal is disturbed |
|
||||
| 1 | Motion | Rate of signal change (walking > typing > still) |
|
||||
| 2 | Breathing | Chest movement modulates subcarrier phase at 6-30 BPM |
|
||||
| 3 | Heart rate | Blood pulse creates micro-Doppler at 40-120 BPM |
|
||||
| 4 | Phase variance | Signal quality — higher = more movement |
|
||||
| 5 | Person count | Independent motion clusters via min-cut graph |
|
||||
| 6 | Fall detected | Sudden phase acceleration followed by stillness |
|
||||
| 7 | RSSI | Signal strength — indicates distance from sensor |
|
||||
|
||||
## Training Details
|
||||
|
||||
**No camera was used.** Trained using self-supervised contrastive learning:
|
||||
|
||||
- **Data**: 60,630 samples from 2 ESP32-S3 nodes over 8 hours
|
||||
- **Method**: Triplet loss + InfoNCE (nearby frames = similar, distant = different)
|
||||
- **Augmentation**: 10x via temporal interpolation, noise, cross-node blending
|
||||
- **Supervision**: PIR sensor, BME280, RSSI triangulation, subcarrier asymmetry
|
||||
- **Quantization**: TurboQuant 2/4/8-bit with <0.5% quality loss
|
||||
- **Adaptation**: LoRA rank-4 per room, EWC to prevent forgetting
|
||||
|
||||
## 17 Sensing Applications
|
||||
|
||||
Built on these embeddings ([RuView](https://github.com/ruvnet/RuView)):
|
||||
|
||||
**Core:** Presence, person counting, RF scanning, SNN learning, CNN fingerprinting
|
||||
|
||||
**Health:** Sleep monitoring, apnea screening, stress detection, gait analysis
|
||||
|
||||
**Environment:** Room fingerprinting, material detection, device fingerprinting
|
||||
|
||||
**Multi-frequency:** RF tomography, passive radar, material classification, through-wall motion
|
||||
|
||||
## Hardware
|
||||
|
||||
| Component | Cost | Purpose |
|
||||
|-----------|------|---------|
|
||||
| ESP32-S3 (8MB) | ~$9 | WiFi CSI sensing |
|
||||
| [Cognitum Seed](https://cognitum.one) (optional) | $131 | Persistent storage, kNN, witness chain, AI proxy |
|
||||
---
|
||||
|
||||
## Limitations
|
||||
|
||||
- Room-specific (use LoRA adapters for new rooms)
|
||||
- Camera-free pose: 2.5% PCK@20 (camera labels improve significantly)
|
||||
- Health features are for screening only, not medical diagnosis
|
||||
- Breathing/HR less accurate during active movement
|
||||
Be honest about what this technology can and cannot do:
|
||||
|
||||
- **Room-specific.** The model needs a short calibration period in each new environment. A model calibrated in a living room will not work as well in a warehouse without re-adaptation.
|
||||
- **Single room only.** There is no cross-room tracking. Each room needs its own sensing node(s).
|
||||
- **Person count accuracy degrades above 4.** Counting works well for 1-3 people, becomes unreliable above 4 in a single room.
|
||||
- **Vitals require stillness.** Breathing and heart rate estimation work best when the person is sitting or lying down. Accuracy drops significantly during walking or exercise.
|
||||
- **Heart rate is experimental.** The +/- 5 BPM accuracy is a best-case figure. In practice, cardiac sensing via WiFi is still a research-stage capability.
|
||||
- **Wall materials matter.** Metal walls, concrete reinforced with rebar, or foil-backed insulation will significantly attenuate the signal and reduce range.
|
||||
- **WiFi interference.** Heavy WiFi traffic from other devices can add noise. The system works best on a dedicated or lightly-used WiFi channel.
|
||||
- **Not a medical device.** Vital sign estimates are for informational and research purposes only. Do not use them for medical decisions.
|
||||
|
||||
---
|
||||
|
||||
## Use Cases
|
||||
|
||||
- **Elder care:** Non-invasive fall detection and activity monitoring without cameras
|
||||
- **Smart home:** Presence-based lighting and HVAC control
|
||||
- **Security:** Occupancy detection through walls
|
||||
- **Sleep monitoring:** Breathing rate tracking overnight
|
||||
- **Research:** Low-cost human sensing for academic experiments
|
||||
- **Disaster response:** The MAT (Mass Casualty Assessment Tool) uses this model to detect survivors through rubble via WiFi signal reflections
|
||||
|
||||
---
|
||||
|
||||
## Ethical Considerations
|
||||
|
||||
WiFi sensing is a privacy-preserving alternative to cameras, but it still detects human presence and activity. Consider these points:
|
||||
|
||||
- **Consent:** Always inform people that WiFi sensing is active in a space.
|
||||
- **No biometric identification:** This model cannot identify *who* someone is -- only that someone is present and what they are doing.
|
||||
- **Data minimization:** Raw CSI data is processed on-device and only summary features or embeddings leave the sensor. No images, audio, or video are ever captured.
|
||||
- **Dual use:** Like any sensing technology, this can be misused for surveillance. We encourage transparent deployment and clear signage.
|
||||
|
||||
---
|
||||
|
||||
## Citation
|
||||
|
||||
If you use this model in your research, please cite:
|
||||
|
||||
```bibtex
|
||||
@software{ruview2026,
|
||||
title={RuView: WiFi Sensing with Self-Supervised Contrastive Learning},
|
||||
author={rUv},
|
||||
year={2026},
|
||||
url={https://github.com/ruvnet/RuView},
|
||||
note={Models: https://huggingface.co/ruv/ruview}
|
||||
@software{wifi_densepose_2026,
|
||||
title = {WiFi-DensePose: Human Pose Estimation from WiFi Channel State Information},
|
||||
author = {ruvnet},
|
||||
year = {2026},
|
||||
url = {https://github.com/ruvnet/RuView},
|
||||
license = {MIT},
|
||||
note = {Self-supervised contrastive learning on ESP32-S3 CSI data}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
MIT License. See [LICENSE](https://github.com/ruvnet/RuView/blob/main/LICENSE) for details.
|
||||
|
||||
You are free to use, modify, and distribute this model for any purpose, including commercial applications.
|
||||
|
||||
---
|
||||
|
||||
## Links
|
||||
|
||||
- **GitHub**: https://github.com/ruvnet/RuView
|
||||
- **Cognitum Seed**: https://cognitum.one
|
||||
- **RuVector**: https://github.com/ruvnet/ruvector
|
||||
- **License**: MIT
|
||||
- **GitHub:** [github.com/ruvnet/RuView](https://github.com/ruvnet/RuView)
|
||||
- **Hardware:** [ESP32-S3 DevKit](https://www.espressif.com/en/products/devkits) | [Cognitum Seed](https://cognitum.one)
|
||||
- **ONNX Runtime:** [onnxruntime.ai](https://onnxruntime.ai)
|
||||
|
||||
@@ -1,306 +0,0 @@
|
||||
# Coherent Wideband RF Tomography: Simulating and Reconstructing with `wifi-densepose-sar`
|
||||
|
||||
A walkthrough of the `wifi-densepose-sar` crate (ADR-287): simulating
|
||||
synthetic-aperture radar (SAR) style measurements and reconstructing a 3D
|
||||
reflectivity image from them via delay-and-sum backprojection.
|
||||
|
||||
**Estimated time:** 30 minutes.
|
||||
|
||||
**What you will build:** A small Rust program that simulates a handheld
|
||||
stepped-frequency radar sweep past a couple of point targets, reconstructs
|
||||
a 3D image from the resulting complex measurements, and extracts a sparse
|
||||
point cloud from it — then verifies the reconstruction's resolution
|
||||
against closed-form theory.
|
||||
|
||||
**Who this is for:** Rust developers comfortable with basic signal
|
||||
processing terminology (frequency, bandwidth, phase) who want to
|
||||
understand what a coherent RF imaging pipeline actually computes, or who
|
||||
are evaluating whether this crate is a useful building block for their own
|
||||
radar-imaging research.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [What This Is (and Isn't)](#1-what-this-is-and-isnt)
|
||||
2. [Prerequisites](#2-prerequisites)
|
||||
3. [The Physics in Five Minutes](#3-the-physics-in-five-minutes)
|
||||
4. [Your First Reconstruction](#4-your-first-reconstruction)
|
||||
5. [Range Resolution: Why Bandwidth Matters](#5-range-resolution-why-bandwidth-matters)
|
||||
6. [Cross-Range Resolution: Why You Need to Move the Antenna](#6-cross-range-resolution-why-you-need-to-move-the-antenna)
|
||||
7. [The Antenna-Pose Coherence Budget](#7-the-antenna-pose-coherence-budget)
|
||||
8. [Extracting a Point Cloud](#8-extracting-a-point-cloud)
|
||||
9. [Benchmarking Your Own Scenario](#9-benchmarking-your-own-scenario)
|
||||
10. [Where This Could Go Next](#10-where-this-could-go-next)
|
||||
11. [Troubleshooting](#11-troubleshooting)
|
||||
|
||||
---
|
||||
|
||||
## 1. What This Is (and Isn't)
|
||||
|
||||
This crate exists because of a real question: could this repo build
|
||||
something like [Applied Electrodynamics' WaveSight](https://www.ae-dyn.com/)
|
||||
— a handheld device that images through walls using radio waves? The
|
||||
honest answer, worked out in ADR-287, is **no, not as a hardware product**
|
||||
— that needs a custom coherent RF front end, a calibrated antenna array,
|
||||
and real-time reconstruction hardware, which is an 18–36 month, high
|
||||
six-to-seven-figure hardware engineering program, not a software change.
|
||||
|
||||
What *is* useful to build, and what this crate is, is the **reconstruction
|
||||
algorithm** such a device needs: given coherent, phase-preserving,
|
||||
stepped-frequency measurements recorded from several known antenna
|
||||
positions, recover the 3D locations of the things that reflected the
|
||||
signal. That's a well-understood problem (synthetic-aperture radar,
|
||||
ground-penetrating radar imaging, and microwave tomography all solve
|
||||
versions of it) with textbook closed-form math behind it.
|
||||
|
||||
Every number in this crate comes from its own **synthetic forward
|
||||
simulator** — there is no real radio hardware anywhere in this crate, and
|
||||
none of its tests, benchmarks, or accuracy numbers say anything about how
|
||||
well a real device would perform through a real wall. That's evidence
|
||||
level **L0 (Synthetic)** in this repo's [ADR-282](../adr/ADR-282-ruview-ecosystem-positioning.md)
|
||||
evidence ladder, and it stays L0 until (if ever) real wideband RF hardware
|
||||
feeds this pipeline real measurements.
|
||||
|
||||
## 2. Prerequisites
|
||||
|
||||
- Rust 1.75+ (workspace MSRV), already set up if you can build the rest of
|
||||
this repo's `v2/` workspace.
|
||||
- No special hardware. Everything in this tutorial runs from synthetic
|
||||
data.
|
||||
|
||||
```bash
|
||||
cd v2
|
||||
cargo test -p wifi-densepose-sar --no-default-features
|
||||
```
|
||||
|
||||
If that passes (24 tests, 0 failed), you're ready.
|
||||
|
||||
## 3. The Physics in Five Minutes
|
||||
|
||||
A stepped-frequency radar sweeps `K` frequencies `f_0..f_{K-1}` across a
|
||||
band of total width `B` (the bandwidth). At each of `M` antenna positions
|
||||
`p_0..p_{M-1}` along a handheld sweep, it records one complex number per
|
||||
frequency — amplitude and phase, not just amplitude, which is what makes
|
||||
this "coherent."
|
||||
|
||||
For a point scatterer at position `x` with reflectivity `σ`, range
|
||||
`R = |p_m - x|` from antenna position `m`, the forward model this crate
|
||||
simulates is:
|
||||
|
||||
```text
|
||||
y_{m,k} = sigma / R^2 * exp(-i * 4*pi * f_k * R / c)
|
||||
```
|
||||
|
||||
`4*pi*f*R/c` is the two-way (round-trip) propagation phase; `1/R^2` is the
|
||||
two-way free-space spreading loss. With several targets, the measurement
|
||||
is just the sum of each target's contribution (superposition — this crate
|
||||
never models multipath/interaction between targets, only free-space direct
|
||||
paths).
|
||||
|
||||
**Reconstruction (backprojection)** inverts this: for every candidate
|
||||
voxel `x` in a 3D grid, it multiplies each measurement by the *complex
|
||||
conjugate* of the phase the forward model would have applied for a target
|
||||
at `x`, then sums:
|
||||
|
||||
```text
|
||||
I(x) = | (1/MK) * sum_m sum_k y_{m,k} * R_{m,x}^2 * exp(+i * 4*pi * f_k * R_{m,x} / c) |
|
||||
```
|
||||
|
||||
If `x` coincides with a real target, every term's phase correction exactly
|
||||
cancels the phase the forward model applied — the sum adds up
|
||||
constructively ("coherent gain"). At any other voxel, the phases are
|
||||
essentially uncorrelated across the `(m, k)` grid and the sum averages
|
||||
toward zero. That's the entire algorithm: matched filtering, done in 3D,
|
||||
one voxel at a time.
|
||||
|
||||
## 4. Your First Reconstruction
|
||||
|
||||
Add `wifi-densepose-sar` to a scratch binary or run this in a workspace
|
||||
example. It simulates two targets, reconstructs, and finds the brightest
|
||||
voxel:
|
||||
|
||||
```rust
|
||||
use wifi_densepose_sar::{
|
||||
backproject, linear_aperture, simulate_measurement, FrequencySweep,
|
||||
Point3, ScatteringTarget, VoxelGrid,
|
||||
};
|
||||
|
||||
fn main() {
|
||||
// A 1-meter handheld sweep, 21 antenna positions along it.
|
||||
let poses = linear_aperture(
|
||||
Point3::new(-0.5, 0.0, 0.0),
|
||||
Point3::new(0.5, 0.0, 0.0),
|
||||
21,
|
||||
);
|
||||
|
||||
// Sweep 2-6 GHz (4 GHz of bandwidth) in 32 steps.
|
||||
let sweep = FrequencySweep::new(2.0e9, 6.0e9, 32);
|
||||
|
||||
// One target, 2 meters downrange, reflectivity 1.0 (arbitrary units).
|
||||
let target = ScatteringTarget::new(Point3::new(0.0, 2.0, 0.0), 1.0);
|
||||
|
||||
// Simulate the measurement with a touch of noise (seeded -- rerunning
|
||||
// with the same seed gives byte-identical output).
|
||||
let measurement = simulate_measurement(&poses, &sweep, &[target], 0.01, 42);
|
||||
|
||||
// Reconstruct a 21x21x21 voxel grid around where we expect the target.
|
||||
let grid = VoxelGrid::new(Point3::new(-0.3, 1.7, -0.3), 0.03, 21, 21, 21);
|
||||
let image = backproject(&measurement, &poses, &sweep, &grid);
|
||||
|
||||
let (peak_location, peak_magnitude) = image.peak();
|
||||
println!("true target: {:?}", target.position);
|
||||
println!("reconstructed peak: {peak_location:?} (magnitude {peak_magnitude:.4})");
|
||||
}
|
||||
```
|
||||
|
||||
Run it and you should see the reconstructed peak within a couple of
|
||||
centimeters of the true target position — well inside the voxel spacing
|
||||
used here (3 cm). That's `tests/reconstruct.rs::single_point_target_reconstructs_at_its_true_location`
|
||||
running live.
|
||||
|
||||
## 5. Range Resolution: Why Bandwidth Matters
|
||||
|
||||
How close together can two targets be *along the same bearing* (same
|
||||
antenna, different distance) before they blur into one blob? The classic
|
||||
radar answer: `ΔR = c / (2B)` — resolution improves with more swept
|
||||
bandwidth, full stop. Carrier frequency, antenna count, and aperture
|
||||
length don't enter into it at all.
|
||||
|
||||
```rust
|
||||
use wifi_densepose_sar::resolution::range_resolution_m;
|
||||
|
||||
let dr = range_resolution_m(4.0e9); // 4 GHz swept bandwidth
|
||||
println!("range resolution: {:.1} cm", dr * 100.0);
|
||||
// -> range resolution: 3.7 cm
|
||||
```
|
||||
|
||||
`tests/physics_validation.rs::range_separated_targets_resolve_only_beyond_range_resolution`
|
||||
proves this isn't just a formula sitting in a doc comment: it forward-simulates
|
||||
two targets 4x `ΔR` apart (they resolve into two distinct peaks) and 0.25x
|
||||
`ΔR` apart (they merge into one), using the *same* `range_resolution_m`
|
||||
call to pick the separations.
|
||||
|
||||
## 6. Cross-Range Resolution: Why You Need to Move the Antenna
|
||||
|
||||
A single antenna position, no matter how much bandwidth it sweeps, cannot
|
||||
tell two targets apart if they're at the same range but different bearing
|
||||
— all it measures is round-trip distance, which is the same for both. This
|
||||
is exactly why "handheld... sweep the antenna around" matters: moving the
|
||||
antenna across a synthetic aperture of length `L` gives you angular
|
||||
information, with cross-range resolution:
|
||||
|
||||
```text
|
||||
delta_CR ~= lambda * R / (2 * L)
|
||||
```
|
||||
|
||||
— finer with a longer aperture, a shorter wavelength (higher carrier
|
||||
frequency), or a closer target.
|
||||
|
||||
```rust
|
||||
use wifi_densepose_sar::resolution::cross_range_resolution_m;
|
||||
|
||||
let short = cross_range_resolution_m(4.0e9, 0.05, 2.0); // 5cm sweep
|
||||
let long = cross_range_resolution_m(4.0e9, 1.0, 2.0); // 1m sweep
|
||||
println!("5cm aperture: {:.2} m cross-range resolution", short);
|
||||
println!("1m aperture: {:.2} m cross-range resolution", long);
|
||||
// -> a 20x longer aperture gives 20x finer cross-range resolution
|
||||
```
|
||||
|
||||
`tests/physics_validation.rs::cross_range_separated_targets_resolve_only_with_long_enough_aperture`
|
||||
demonstrates this end-to-end: the same pair of cross-range-separated
|
||||
targets resolves into two peaks with a 1m synthetic aperture and collapses
|
||||
into one with a 5cm aperture, no other change.
|
||||
|
||||
## 7. The Antenna-Pose Coherence Budget
|
||||
|
||||
Backprojection assumes you know exactly where the antenna was at each
|
||||
measurement. If your position tracking (in a real device: visual-inertial
|
||||
odometry, encoders, whatever) is off by `Δp`, the phase correction applied
|
||||
during reconstruction is wrong by an amount that grows with `Δp` and with
|
||||
frequency. The classical rule of thumb for "still well focused": keep the
|
||||
round-trip path error under a quarter wavelength, which works out to an
|
||||
antenna-position tolerance of `λ/8`:
|
||||
|
||||
```rust
|
||||
use wifi_densepose_sar::resolution::max_coherent_pose_error_m;
|
||||
|
||||
let budget = max_coherent_pose_error_m(8.0e9); // 8 GHz carrier
|
||||
println!("position tolerance at 8 GHz: {:.1} mm", budget * 1000.0);
|
||||
// -> position tolerance at 8 GHz: 4.7 mm
|
||||
```
|
||||
|
||||
`tests/physics_validation.rs::phase_error_from_pose_jitter_degrades_focus_beyond_pose_budget`
|
||||
verifies this isn't just asserted: it perturbs the *true* antenna positions
|
||||
away from the *assumed* ones used in reconstruction, and shows focus at the
|
||||
true target location degrades as that perturbation grows — the concrete
|
||||
mechanism behind why real SAR/GPR imaging systems need accurate pose
|
||||
tracking, not just a good radio.
|
||||
|
||||
## 8. Extracting a Point Cloud
|
||||
|
||||
A dense voxel grid isn't a useful end product — you want a short list of
|
||||
detected points:
|
||||
|
||||
Continuing the program from §4 (which already has `image` in scope):
|
||||
|
||||
```rust
|
||||
use wifi_densepose_sar::extract_point_cloud;
|
||||
|
||||
let points = extract_point_cloud(&image, 0.5); // 50%-of-peak threshold
|
||||
for p in &points {
|
||||
println!("{:?} magnitude={:.3}", p.position, p.magnitude);
|
||||
}
|
||||
```
|
||||
|
||||
`extract_point_cloud` does threshold + 6-connected local-maximum
|
||||
extraction — a real blob will still yield one point, not one per voxel
|
||||
inside it. There is deliberately no clustering, material classification,
|
||||
or confidence calibration here (ADR-287 §5): that needs real data to
|
||||
calibrate against, which this crate does not have.
|
||||
|
||||
## 9. Benchmarking Your Own Scenario
|
||||
|
||||
```bash
|
||||
cargo bench -p wifi-densepose-sar
|
||||
```
|
||||
|
||||
The shipped benchmark (`benches/backprojection_bench.rs`) sweeps 512 /
|
||||
4,096 / 32,768-voxel grids with 21 poses x 32 frequencies. Reconstruction
|
||||
is embarrassingly parallel over voxels (each voxel's cost is independent),
|
||||
so it's rayon-parallelized already — see the crate README for the last
|
||||
recorded MEASURED numbers on the reference machine.
|
||||
|
||||
## 10. Where This Could Go Next
|
||||
|
||||
This crate deliberately stops short of several things (ADR-287 §5):
|
||||
|
||||
- It's monostatic (one antenna, both TX and RX) — real handheld SAR/MIMO
|
||||
devices often use multiple simultaneous antenna elements.
|
||||
- The forward model is free-space only — no multipath, no per-material
|
||||
attenuation (contrast `ruview-unified`'s narrowband Fresnel material
|
||||
model, which isn't yet extended to wideband).
|
||||
- It isn't wired into `ruview-unified`'s `FmcwRadarCube` adapter or
|
||||
`GaussianMap` — ADR-278 names that as the eventual integration point,
|
||||
once (and if) a reconstruction system is ready for it.
|
||||
|
||||
If you're picking this up to extend it, start with ADR-287's "Follow-up"
|
||||
section rather than guessing at scope.
|
||||
|
||||
## 11. Troubleshooting
|
||||
|
||||
**"My reconstructed peak isn't near my target."** Check your voxel grid
|
||||
actually covers the target's true location — `backproject` happily
|
||||
reconstructs whatever region you ask for; if the target is outside the
|
||||
grid, you'll get whatever's brightest inside it instead (usually noise).
|
||||
|
||||
**"Two targets I expected to resolve didn't."** Compute
|
||||
`range_resolution_m`/`cross_range_resolution_m` for your actual bandwidth
|
||||
and aperture length and check your separation against them — resolution
|
||||
is a hard physical limit here, not a tuning parameter.
|
||||
|
||||
**"Backprojection is slow for my grid size."** Cost is
|
||||
`O(voxels x poses x freqs)` and already parallelized over voxels via
|
||||
rayon; the only way to go faster is fewer voxels, fewer poses, or fewer
|
||||
frequency steps (each is a hard tradeoff against resolution or aperture
|
||||
coverage — see §5/§6).
|
||||
+1
-6
@@ -135,7 +135,7 @@ The compiled binary is at `target/release/sensing-server`.
|
||||
|
||||
### From crates.io (Individual Crates)
|
||||
|
||||
The workspace's crates publish independently, so versions vary crate to crate (`wifi-densepose-core` is at 0.3.2, `wifi-densepose-signal` at 0.3.6, etc. as of this writing) — `cargo add` resolves each to its own latest by default, so you don't need to track exact numbers yourself. Add individual crates to your own Rust project:
|
||||
All 16 crates are published to crates.io at v0.3.0. Add individual crates to your own Rust project:
|
||||
|
||||
```bash
|
||||
# Core types and traits
|
||||
@@ -161,11 +161,6 @@ cargo add wifi-densepose-wasm
|
||||
|
||||
# WASM edge runtime (lightweight, for embedded/IoT)
|
||||
cargo add wifi-densepose-wasm-edge
|
||||
|
||||
# Coherent wideband RF tomography research crate (ADR-287) — synthetic
|
||||
# stepped-frequency backprojection reconstruction. SYNTHETIC/L0 evidence
|
||||
# only; not wired into any sensing pipeline above. See its own README.
|
||||
cargo add wifi-densepose-sar
|
||||
```
|
||||
|
||||
See the full crate list and dependency order in [CLAUDE.md](../CLAUDE.md#crate-publishing-order).
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"name": "wifi-densepose-sar-harness",
|
||||
"version": "0.1.0",
|
||||
"description": "Harness for wifi-densepose-sar",
|
||||
"author": {
|
||||
"displayName": "Generated by metaharness",
|
||||
"url": "https://www.npmjs.com/package/metaharness"
|
||||
},
|
||||
"license": "MIT",
|
||||
"categories": [
|
||||
"agent-harness",
|
||||
"metaharness-scaffold",
|
||||
"Engineering",
|
||||
"software-engineering"
|
||||
],
|
||||
"tags": [
|
||||
"metaharness",
|
||||
"agent-harness",
|
||||
"vertical:coding",
|
||||
"software-engineering"
|
||||
],
|
||||
"homepage": "https://github.com/ruvnet/agent-harness-generator"
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
---
|
||||
description: "Health-check the harness: kernel load, MCP wiring, memory backend, host adapter."
|
||||
---
|
||||
|
||||
Run a full health check and print a PASS/FAIL table.
|
||||
|
||||
1. Kernel loads and `kernelInfo().version` matches package.json.
|
||||
2. The MCP server starts and lists its tools.
|
||||
3. The memory backend is reachable.
|
||||
4. The configured host adapter is present.
|
||||
|
||||
Exit non-zero if any check fails.
|
||||
@@ -1,10 +0,0 @@
|
||||
---
|
||||
description: "Run the SYNTHETIC @metaharness/flywheel self-improvement demo and report the lift curve."
|
||||
---
|
||||
|
||||
Run the propose → evaluate → gate → promote loop and report what it found.
|
||||
|
||||
1. Run `npm run build` first if `dist/flywheel.js` doesn't exist yet.
|
||||
2. Run `wifi-densepose-sar-harness flywheel [generations]` (default 3 if omitted).
|
||||
3. Report each generation's `primary` score and `delta`, the total promotions, and whether `verifyReplayBundle` passed.
|
||||
4. State plainly: this run's `dataSource` is `SYNTHETIC` — the proposer and evaluator are deterministic stand-ins (see `src/flywheel.ts`'s honesty note), not a real model call or a real coding-task benchmark. Do not report its numbers as if they reflect real harness improvement.
|
||||
@@ -1,10 +0,0 @@
|
||||
---
|
||||
description: "Review the current working diff for correctness, security, and reuse."
|
||||
---
|
||||
|
||||
Review the current git diff.
|
||||
|
||||
1. `git diff` to read the change.
|
||||
2. Report only high-confidence findings as `file:line — issue — fix`.
|
||||
3. Separate bugs from nits.
|
||||
4. End with APPROVE or REQUEST-CHANGES and a one-line reason.
|
||||
@@ -1,10 +0,0 @@
|
||||
---
|
||||
description: "Route a 4-axis task embedding to the cost-optimal model tier via @metaharness/router."
|
||||
---
|
||||
|
||||
Route one query to the cheapest model tier predicted to clear the quality bar.
|
||||
|
||||
1. Run `npm run build` first if `dist/router.js` doesn't exist yet.
|
||||
2. Run `wifi-densepose-sar-harness route <physicsExplanation> <codeReview> <numericalDebugging> <docWriting>` — four 0..1 numbers scoring how much the query looks like each of those four shapes (see `src/router.ts` for the axis definitions).
|
||||
3. Report the picked tier (`cheap-tier` or `frontier-tier`), its predicted quality, and whether it cleared the 0.8 quality bar.
|
||||
4. Remind the user: the labelled examples behind this decision are illustrative seed data (see `src/router.ts`'s honesty note), not measured eval logs — the routing mechanism is real, the specific pick isn't backed by production data yet.
|
||||
@@ -1,40 +0,0 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(npx wifi-densepose-sar-harness*)",
|
||||
"mcp__wifi-densepose-sar-harness__*",
|
||||
"mcp__code_index__*",
|
||||
"Bash(npm test*)",
|
||||
"Bash(npm run*)",
|
||||
"Bash(git diff*)",
|
||||
"Bash(git status*)",
|
||||
"Bash(git log*)"
|
||||
],
|
||||
"deny": [
|
||||
"Read(./.env)",
|
||||
"Read(./.env.*)",
|
||||
"Bash(git push*)",
|
||||
"Bash(rm -rf*)"
|
||||
]
|
||||
},
|
||||
"mcpServers": {
|
||||
"wifi-densepose-sar-harness": {
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"-y",
|
||||
"wifi-densepose-sar-harness@latest",
|
||||
"mcp",
|
||||
"start"
|
||||
]
|
||||
},
|
||||
"code_index": {
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"-y",
|
||||
"wifi-densepose-sar-harness@latest",
|
||||
"mcp",
|
||||
"index"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
---
|
||||
name: evolve
|
||||
description: "Evolve this harness with Darwin Mode — frozen model, evolving harness (real, sandboxed, safety-gated)."
|
||||
---
|
||||
|
||||
# evolve — Darwin Mode self-improvement
|
||||
|
||||
`wifi-densepose-sar-harness` ships with **Darwin Mode** (`@metaharness/darwin`, ADR-070…146): the model
|
||||
is frozen; the *harness* evolves. Each generation mutates ONE of the 7 surface files
|
||||
(planner, contextBuilder, reviewer, retry/tool/memory/score policy), sandboxes each
|
||||
child, scores it, and keeps only variants that *measurably* improve — building an
|
||||
archive of successful descendants.
|
||||
|
||||
## Run it
|
||||
|
||||
```bash
|
||||
npm run evolve # real substrate: runs your test command per variant (deterministic mutator — no API key, no network)
|
||||
npm run evolve:dry # mock substrate: fast, fully offline, no test execution
|
||||
```
|
||||
|
||||
Or directly:
|
||||
|
||||
```bash
|
||||
npx metaharness-darwin evolve . --sandbox real --generations 3 --children 4
|
||||
```
|
||||
|
||||
## Safety (secure by default)
|
||||
|
||||
- **Deterministic mutator** is the default — **no network, no API key, air-gapped**.
|
||||
- Every mutation passes the `validateGeneratedCode` gate: no new imports, network,
|
||||
filesystem, shell, env access, or dependencies — pure refactor/tuning only.
|
||||
- Mutations run in a **sandbox**; only variants that pass your tests are archived.
|
||||
- Nothing is promoted without measured improvement (guard against Goodharting).
|
||||
|
||||
See `@metaharness/darwin` for selection strategies (`--selection`, `--crossover`,
|
||||
`--curriculum`), statistical gates (`--fdr`, `--bench`), and the real-LLM mutator (library API).
|
||||
|
||||
## What the benchmarks taught us (measured, full SWE-bench Lite 300)
|
||||
|
||||
Defaults worth carrying into how you evolve and run this harness (full evidence + CIs in
|
||||
`@metaharness/darwin`'s `LEARNINGS.md` / `bench/results/RESULTS.md`):
|
||||
|
||||
1. **Closed-loop repair is the #1 lever (~2×).** Feeding test/compiler failure back and retrying took
|
||||
resolve-rate 7.7% → 15.3% on the *same cheap model*. Iterate against ground truth, don't single-shot.
|
||||
2. **Cheap-first + cost-aware routing.** Track **$/resolve**, not just resolve-rate; a cheap model
|
||||
resolved 31× cheaper per fix than a frontier one. Reserve frontier for *measured* capability gaps.
|
||||
3. **Tier the models (Barbarian & Scholar).** Cheap sweep + frontier on *only the residual* = 33.3%
|
||||
at ~6× lower cost than running frontier everywhere.
|
||||
4. **Put the output-format contract in a system message + example**, and size prompts to the model's
|
||||
real context window — this alone took a weak local model from 0% to ~50% valid output.
|
||||
5. **Only trust batch evaluation of the final artifact** — in-loop counters drift 1.5–5×.
|
||||
6. **The harness multiplies the model; it can't rescue one below the task's reasoning floor.** Pick
|
||||
the smallest model *above* the floor, then let evolution do the rest.
|
||||
@@ -1,15 +0,0 @@
|
||||
---
|
||||
name: plan-change
|
||||
description: "Turn a feature request into a minimal, file-level implementation plan before any code."
|
||||
---
|
||||
|
||||
# plan-change
|
||||
|
||||
Produce an implementation plan for a requested change.
|
||||
|
||||
1. Restate the goal in one sentence.
|
||||
2. List the files to touch and why.
|
||||
3. Name the smallest interface that satisfies it.
|
||||
4. Flag anything that ripples beyond three files or widens a permission.
|
||||
|
||||
Hand the plan to the implementer; do not write code in this step.
|
||||
@@ -1,39 +0,0 @@
|
||||
{
|
||||
"schema": 1,
|
||||
"generator": "0.1.0",
|
||||
"template": "vertical:coding",
|
||||
"template_version": "0.0.0",
|
||||
"vars": {
|
||||
"name": "wifi-densepose-sar-harness",
|
||||
"description": "Harness for wifi-densepose-sar",
|
||||
"host": "claude-code"
|
||||
},
|
||||
"hosts": [
|
||||
"claude-code"
|
||||
],
|
||||
"files": {
|
||||
".claude/commands/doctor.md": "2f1475fa0ed34729cb2ed9d6cecf29e99781da0a56afef133361e3ff0a17bb52",
|
||||
".claude/commands/review-diff.md": "2ab52f01487bfe67335f4de5193d7a6fb0f72f646411114613d8fa5da071ef2b",
|
||||
".claude/settings.json": "ea983de8f425d313ee42848a7da58ce98e05713a06dc2f45a5119f421a311e07",
|
||||
".claude/skills/plan-change/SKILL.md": "84e1c44ca264b999ebf80cd8fa0e5ebf554275bbd8023b1530ad053a44482e30",
|
||||
".claude-plugin/plugin.json": "f716a379f3e077b47dae884b5ca0d4a077e9a1ab1f091afa8f0f031ead1008e5",
|
||||
"bin/cli.js": "78bd27074b3ce22bff63729995032c7b262a414ebfc8d75345fc82b40a4a84ee",
|
||||
"CLAUDE.md": "4e9e11558e124605fd1a5de4be116c2b8d9dc15a3bc7c8f6789a7eec23dac19d",
|
||||
"package.json": "a9f103fc452b5497a41333d2e80c834de07972e951be1701f25f98a5acaaea9d",
|
||||
"README.md": "0efc949a11c20a8179261887d44afac60fb6e28af1ee0cf13451bd665b1cfbd4",
|
||||
"src/agents/architect.ts": "b146bb8ad7729d7738f6c07d7770fecb6a13cf7683019c8b8afaec7c13806515",
|
||||
"src/agents/implementer.ts": "55898d303ae87574348821ef2c270eceeec733278bb94041d767308b3a509291",
|
||||
"src/agents/reviewer.ts": "ef5ab428ea799be7caeb05e761723122c52ab35d9ef4a18b618647ff72549ad8",
|
||||
"src/agents/test-writer.ts": "f966a4d3a97a02b98606aac104f6fcbcf294c0186c15d7a809aa7555b20c0b5f",
|
||||
"src/init.ts": "e19b1dbe6e4c3b7282a102bf068c20630e8cb0323d8abefa477d0f2dbbc68ce2",
|
||||
"tsconfig.json": "8b4e730a1aa39162ac574455d7a98e1881f5313ca80ffe503b9652dcf0c76b9d",
|
||||
"vitest.config.ts": "e9e94875611ab1cd602c1f6923902c2dab7953a75ac022962973639d1937587a",
|
||||
"__tests__/smoke.test.ts": "e38dfc1389b8b419841f2c6513854dbb291e957be0c1c325c507dc58ef3d8c5e",
|
||||
"LICENSE": "0580604803a2c38a1e4fe2c86d5e2f6d213cc3317cd5bc050691e484c523aedc",
|
||||
".claude/skills/evolve/SKILL.md": "05965b34edd9bbea83391ae20c5c22bf2653396e7b54557cfcf395fd7d11294a"
|
||||
},
|
||||
"generated_at": "2026-07-30T22:39:05.579Z",
|
||||
"meta": {
|
||||
"surface": "cli"
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
b5ac57b4ae092fda710fef597519f3f623ebd9bb40bee9c7e010d0ad42200241
|
||||
@@ -1,45 +0,0 @@
|
||||
# wifi-densepose-sar-harness
|
||||
|
||||
Harness for [wifi-densepose-sar](https://crates.io/crates/wifi-densepose-sar) (ADR-287) — the coherent wideband RF tomography research crate this harness assists development on. Both are published: the crate on crates.io, this harness itself as [`wifi-densepose-sar-harness`](https://www.npmjs.com/package/wifi-densepose-sar-harness) on npm.
|
||||
|
||||
> Advanced Coding harness · domain: `software-engineering`. Generated with [create-agent-harness](https://github.com/ruvnet/agent-harness-generator).
|
||||
|
||||
## Behavioral rules
|
||||
|
||||
- Use the harness's MCP tools (`mcp__wifi-densepose-sar-harness__*`) for orchestration
|
||||
- Memory and routing are handled by the kernel — you don't need to learn them
|
||||
- Defer destructive operations to the user
|
||||
|
||||
## Agents
|
||||
|
||||
| Agent | Tier | Role |
|
||||
|---|---|---|
|
||||
| `architect` | opus | Designs the change before code is written. |
|
||||
| `implementer` | sonnet | Writes code that matches the surrounding style. |
|
||||
| `reviewer` | opus | Hunts correctness bugs in the diff. |
|
||||
| `test-writer` | sonnet | Adds the missing tests for the change. |
|
||||
## Skills
|
||||
|
||||
- `/plan-change` — Turn a feature request into a minimal, file-level implementation plan before any code.
|
||||
- `/evolve` — Run Darwin Mode (`npm run evolve` / `evolve:dry`) to self-mutate the harness's own operating policy and keep only measurable improvements.
|
||||
|
||||
## Commands
|
||||
|
||||
Each command below has a matching `.claude/commands/<name>.md` guidance file — the MCP tool listing (`mcp__wifi-densepose-sar-harness__*`) is derived from these, so a new CLI subcommand isn't fully wired up until it has one too.
|
||||
|
||||
- `doctor` — Health-check the harness: kernel load, MCP wiring, memory backend, host adapter.
|
||||
- `review-diff` — Review the current working diff for correctness, security, and reuse.
|
||||
- `route <e0> <e1> <e2> <e3>` — cost-optimal model routing via `@metaharness/router` (needs `npm run build` first).
|
||||
- `flywheel [generations]` — run the SYNTHETIC self-improvement demo via `@metaharness/flywheel` (needs `npm run build` first).
|
||||
|
||||
## Architecture
|
||||
|
||||
This harness uses [@metaharness/kernel](https://www.npmjs.com/package/@metaharness/kernel) — a Rust-compiled WASM module with a NAPI-RS native fallback — so the same code runs identically on every platform.
|
||||
|
||||
### Darwin, router, flywheel
|
||||
|
||||
Three complementary self-improvement/cost pieces, all real npm dependencies (not aspirational):
|
||||
|
||||
- **Darwin Mode** (`@metaharness/darwin`, devDependency) — `npm run evolve` (real sandbox) / `npm run evolve:dry` (mock sandbox) mutates the harness's own config and keeps only changes that measurably improve it. Wired by the scaffold itself.
|
||||
- **Router** (`@metaharness/router`) — `src/router.ts` wires a real `Router` with a `qualityBar: 0.8` cost-optimal policy over two example model tiers. Its labelled examples are illustrative/seed data (see the file's honesty note), not measured eval-log observations — the routing *mechanism* is real and tested (`__tests__/router.test.ts`), the specific decisions it makes today are not yet backed by real data.
|
||||
- **Flywheel** (`@metaharness/flywheel`) — `src/flywheel.ts` wires the real `runFlywheelGenerations` promotion loop (propose → evaluate → gate → promote, Ed25519-signed, independently replayable) with a SYNTHETIC proposer/evaluator (`dataSource: 'SYNTHETIC'`, no model call). It proves the wiring end-to-end (`__tests__/flywheel.test.ts` checks a real lift curve and a passing `verifyReplayBundle`); a LIVE run needs a real Proposer (model call) and Evaluator (real coding-task holdout/anchor suites) supplied by the operator — see the file's comments for exactly what those seams are.
|
||||
@@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 wifi-densepose-sar-harness authors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,42 +0,0 @@
|
||||
# wifi-densepose-sar-harness
|
||||
|
||||
Harness for wifi-densepose-sar
|
||||
|
||||
> **Advanced Coding** — Architect → implement → review → test, with a code-index MCP and push-guarded git perms.
|
||||
>
|
||||
> Generated with [`create-agent-harness`](https://github.com/ruvnet/agent-harness-generator). Multi-host scaffolding with a kernel that resolves native → wasm → js (js backend in the published beta; see `harness doctor`).
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
npm install -g wifi-densepose-sar-harness
|
||||
wifi-densepose-sar-harness init
|
||||
wifi-densepose-sar-harness doctor
|
||||
```
|
||||
|
||||
## Agents
|
||||
|
||||
| Agent | Role |
|
||||
|---|---|
|
||||
| `architect` | Designs the change before code is written. |
|
||||
| `implementer` | Writes code that matches the surrounding style. |
|
||||
| `reviewer` | Hunts correctness bugs in the diff. |
|
||||
| `test-writer` | Adds the missing tests for the change. |
|
||||
|
||||
This harness ships with the **claude-code** adapter.
|
||||
|
||||
## Darwin, router, flywheel
|
||||
|
||||
- `npm run evolve` / `evolve:dry` — Darwin Mode self-mutation of the harness's own config (`@metaharness/darwin`).
|
||||
- `npm run route -- <e0> <e1> <e2> <e3>` (after `npm run build`) — cost-optimal model routing via `@metaharness/router`.
|
||||
- `npm run flywheel:dry` — the SYNTHETIC `@metaharness/flywheel` self-improvement demo (propose → evaluate → gate → promote, signed + independently replayable).
|
||||
|
||||
See `CLAUDE.md`'s "Darwin, router, flywheel" section and the comments at the top of `src/router.ts` / `src/flywheel.ts` for what's real wiring vs. illustrative/synthetic data.
|
||||
|
||||
## Known gaps
|
||||
|
||||
- `.harness/manifest.json` / `manifest.sha256` reflect the initial `metaharness analyze --scaffold` output and were not regenerated after adding `src/router.ts`, `src/flywheel.ts`, and their tests — this scaffold has no `manifest:update` script (unlike `harness/homecore/`). Treat the manifest as historical provenance for the scaffold step, not a current file-integrity check.
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
@@ -1,37 +0,0 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { runSarFlywheelDemo, verifySarFlywheelDemo, SAR_ROOT_POLICY } from '../src/flywheel.js';
|
||||
|
||||
describe('runSarFlywheelDemo — @metaharness/flywheel wiring (SYNTHETIC data)', () => {
|
||||
it('runs the configured number of generations and stamps SYNTHETIC provenance', async () => {
|
||||
const result = await runSarFlywheelDemo(3);
|
||||
expect(result.generationsRun).toBeGreaterThan(0);
|
||||
expect(result.generationsRun).toBeLessThanOrEqual(3);
|
||||
expect(result.replayBundle.data_source).toBe('SYNTHETIC');
|
||||
});
|
||||
|
||||
it('produces a non-empty lift curve rooted at the initial policy', async () => {
|
||||
const result = await runSarFlywheelDemo(3);
|
||||
expect(result.liftCurve.length).toBeGreaterThan(0);
|
||||
expect(result.liftCurve[0].generation).toBe(0);
|
||||
});
|
||||
|
||||
it('the final policy still carries every root lever', async () => {
|
||||
const result = await runSarFlywheelDemo(2);
|
||||
for (const key of Object.keys(SAR_ROOT_POLICY)) {
|
||||
expect(result.finalPolicy).toHaveProperty(key);
|
||||
}
|
||||
});
|
||||
|
||||
it('the replay bundle independently verifies (no trust in the producer)', async () => {
|
||||
const result = await runSarFlywheelDemo(3);
|
||||
const verdict = verifySarFlywheelDemo(result);
|
||||
expect(verdict.pass).toBe(true);
|
||||
});
|
||||
|
||||
it('is deterministic in structure across repeated runs (same generations requested)', async () => {
|
||||
const a = await runSarFlywheelDemo(2);
|
||||
const b = await runSarFlywheelDemo(2);
|
||||
expect(a.generationsRun).toBe(b.generationsRun);
|
||||
});
|
||||
});
|
||||
@@ -1,36 +0,0 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { sarTaskRouter, routeSarQuery, SAR_ROUTER_CANDIDATES } from '../src/router.js';
|
||||
|
||||
describe('sarTaskRouter — @metaharness/router wiring', () => {
|
||||
it('has both a cheap and a frontier candidate', () => {
|
||||
const ids = SAR_ROUTER_CANDIDATES.map((c) => c.id);
|
||||
expect(ids).toContain('cheap-tier');
|
||||
expect(ids).toContain('frontier-tier');
|
||||
});
|
||||
|
||||
it('routes a physics-explanation-shaped query to the cheap tier', () => {
|
||||
const pick = routeSarQuery([1, 0, 0, 0]);
|
||||
expect(pick.id).toBe('cheap-tier');
|
||||
expect(pick.metBar).toBe(true);
|
||||
});
|
||||
|
||||
it('routes a code-review-shaped query to the frontier tier (cheap tier misses the quality bar)', () => {
|
||||
const pick = routeSarQuery([0, 1, 0, 0]);
|
||||
expect(pick.id).toBe('frontier-tier');
|
||||
});
|
||||
|
||||
it('always returns a candidate with a nonnegative predicted quality and a positive cost', () => {
|
||||
for (const q of [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]] as const) {
|
||||
const pick = routeSarQuery(q);
|
||||
expect(pick.predictedQuality).toBeGreaterThanOrEqual(0);
|
||||
expect(pick.costPerMTok).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
|
||||
it('sarTaskRouter.route and routeSarQuery agree (same underlying router)', () => {
|
||||
const a = sarTaskRouter.route([1, 0, 0, 0]);
|
||||
const b = routeSarQuery([1, 0, 0, 0]);
|
||||
expect(a).toEqual(b);
|
||||
});
|
||||
});
|
||||
@@ -1,37 +0,0 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Generated by metaharness — a real smoke test for wifi-densepose-sar-harness.
|
||||
//
|
||||
// This is NOT a placeholder: it boots the actual kernel + host adapter the
|
||||
// harness depends on, so `npm test` fails loudly if @metaharness/kernel or
|
||||
// @metaharness/host-claude-code is missing, broken, or version-skewed. It is the
|
||||
// fastest signal that `npm install` produced a runnable harness.
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { loadKernel } from '@metaharness/kernel';
|
||||
import adapter from '@metaharness/host-claude-code';
|
||||
import { run } from '../bin/cli.js';
|
||||
|
||||
describe('wifi-densepose-sar-harness — install smoke test', () => {
|
||||
it('loads the kernel and reports a version + a known backend', async () => {
|
||||
const kernel = await loadKernel();
|
||||
const info = kernel.kernelInfo();
|
||||
expect(typeof info.version).toBe('string');
|
||||
expect(info.version.length).toBeGreaterThan(0);
|
||||
expect(['native', 'wasm', 'js']).toContain(kernel.backend);
|
||||
});
|
||||
|
||||
it('resolves the host adapter with a name', () => {
|
||||
expect(typeof adapter.name).toBe('string');
|
||||
expect(adapter.name.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('the CLI doctor command succeeds (exit 0)', async () => {
|
||||
const code = await run(['doctor']);
|
||||
expect(code).toBe(0);
|
||||
});
|
||||
|
||||
it('an unknown CLI command exits non-zero', async () => {
|
||||
const code = await run(['definitely-not-a-command']);
|
||||
expect(code).not.toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -1,156 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Generated by metaharness — the `wifi-densepose-sar-harness` CLI entry point.
|
||||
//
|
||||
// This is plain ESM JavaScript on purpose: it runs as-is via `npx wifi-densepose-sar-harness`
|
||||
// with NO build step. `npm run build` (tsc) is only needed if you extend the
|
||||
// TypeScript in src/. The published package ships this file directly (see the
|
||||
// "bin" + "files" fields in package.json), so `npx wifi-densepose-sar-harness` works the moment
|
||||
// `npm install` has resolved @metaharness/kernel + @metaharness/host-claude-code.
|
||||
|
||||
import { loadKernel } from '@metaharness/kernel';
|
||||
import adapter from '@metaharness/host-claude-code';
|
||||
|
||||
const HARNESS_NAME = 'wifi-densepose-sar-harness';
|
||||
|
||||
/** `wifi-densepose-sar-harness init` — boot the kernel + host adapter and report status. */
|
||||
async function init() {
|
||||
const kernel = await loadKernel();
|
||||
const info = kernel.kernelInfo();
|
||||
console.log(`${HARNESS_NAME} — kernel ${info.version} (${kernel.backend})`);
|
||||
console.log(`Host adapter: ${adapter.name}`);
|
||||
console.log(`Run \`${HARNESS_NAME} doctor\` to verify the install.`);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* `wifi-densepose-sar-harness route <e0> <e1> <e2> <e3>` — route a 4-axis
|
||||
* task embedding to the cost-optimal model tier via @metaharness/router.
|
||||
* Needs `npm run build` first (src/router.ts is TypeScript; this command
|
||||
* imports the compiled dist/ output so the base CLI stays build-free).
|
||||
*/
|
||||
async function route(args) {
|
||||
const embedding = args.map(Number);
|
||||
if (embedding.length !== 4 || embedding.some((n) => Number.isNaN(n))) {
|
||||
console.error('Usage: wifi-densepose-sar-harness route <physicsExplanation> <codeReview> <numericalDebugging> <docWriting> (four 0..1 numbers)');
|
||||
return 2;
|
||||
}
|
||||
let routeSarQuery;
|
||||
try {
|
||||
({ routeSarQuery } = await import('../dist/router.js'));
|
||||
} catch (err) {
|
||||
console.error(`route: dist/router.js not found — run \`npm run build\` first. (${err.message})`);
|
||||
return 1;
|
||||
}
|
||||
const pick = routeSarQuery(embedding);
|
||||
console.log(`route -> ${pick.id} (predicted quality ${pick.predictedQuality.toFixed(3)}, $${pick.costPerMTok}/MTok, met bar: ${pick.metBar})`);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* `wifi-densepose-sar-harness flywheel [generations]` — run the SYNTHETIC
|
||||
* @metaharness/flywheel demo (see src/flywheel.ts) and print the lift curve
|
||||
* + an independent replay-bundle verification. Needs `npm run build` first.
|
||||
*/
|
||||
async function flywheel(args) {
|
||||
const generations = args[0] ? Number(args[0]) : 3;
|
||||
if (Number.isNaN(generations) || generations < 1) {
|
||||
console.error('Usage: wifi-densepose-sar-harness flywheel [generations>=1]');
|
||||
return 2;
|
||||
}
|
||||
let runSarFlywheelDemo, verifySarFlywheelDemo;
|
||||
try {
|
||||
({ runSarFlywheelDemo, verifySarFlywheelDemo } = await import('../dist/flywheel.js'));
|
||||
} catch (err) {
|
||||
console.error(`flywheel: dist/flywheel.js not found — run \`npm run build\` first. (${err.message})`);
|
||||
return 1;
|
||||
}
|
||||
console.log(`Running ${generations}-generation flywheel demo (dataSource: SYNTHETIC — see src/flywheel.ts)...`);
|
||||
const result = await runSarFlywheelDemo(generations);
|
||||
for (const point of result.liftCurve) {
|
||||
console.log(` gen ${point.generation}: primary=${point.primary.toFixed(3)} delta=${point.delta.toFixed(3)} anchor=${point.anchor ?? 'n/a'}`);
|
||||
}
|
||||
const verdict = verifySarFlywheelDemo(result);
|
||||
console.log(`generations run: ${result.generationsRun} · promotions: ${result.promotions.length} · replay verified: ${verdict.pass}`);
|
||||
return verdict.pass ? 0 : 1;
|
||||
}
|
||||
|
||||
/** `wifi-densepose-sar-harness doctor` — verify the install end-to-end (kernel + host resolve). */
|
||||
async function doctor() {
|
||||
const kernel = await loadKernel();
|
||||
const info = kernel.kernelInfo();
|
||||
const checks = [
|
||||
['kernel loads', !!kernel],
|
||||
['kernel reports a version', typeof info.version === 'string' && info.version.length > 0],
|
||||
['kernel backend is native|wasm|js', ['native', 'wasm', 'js'].includes(kernel.backend)],
|
||||
['host adapter has a name', typeof adapter?.name === 'string' && adapter.name.length > 0],
|
||||
];
|
||||
let ok = true;
|
||||
for (const [label, pass] of checks) {
|
||||
console.log(`${pass ? 'PASS' : 'FAIL'} ${label}`);
|
||||
if (!pass) ok = false;
|
||||
}
|
||||
console.log(
|
||||
ok
|
||||
? `\n${HARNESS_NAME}: all checks passed (kernel ${info.version}, ${kernel.backend} backend, host ${adapter.name})`
|
||||
: `\n${HARNESS_NAME}: doctor found problems`,
|
||||
);
|
||||
return ok ? 0 : 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch one CLI invocation. Exported (not just run on import) so a test can
|
||||
* drive it without spawning a subprocess. Returns the intended exit code.
|
||||
*/
|
||||
export async function run(argv) {
|
||||
const cmd = argv[0] ?? 'init';
|
||||
switch (cmd) {
|
||||
case 'init':
|
||||
return init();
|
||||
case 'doctor':
|
||||
return doctor();
|
||||
case 'route':
|
||||
return route(argv.slice(1));
|
||||
case 'flywheel':
|
||||
return flywheel(argv.slice(1));
|
||||
case '--version':
|
||||
case '-v': {
|
||||
const kernel = await loadKernel();
|
||||
console.log(kernel.version());
|
||||
return 0;
|
||||
}
|
||||
case '--help':
|
||||
case '-h':
|
||||
console.log(`Usage: ${HARNESS_NAME} <command>\n\n init boot the kernel + host adapter (default)\n doctor verify the install end-to-end\n route route a 4-axis task embedding to a cost-optimal model tier (needs \`npm run build\`)\n flywheel run the SYNTHETIC self-improvement demo loop (needs \`npm run build\`)\n --version print the kernel version`);
|
||||
return 0;
|
||||
default:
|
||||
console.error(`Unknown command: ${cmd}. Try \`${HARNESS_NAME} --help\`.`);
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
|
||||
// CLI guard: execute only when invoked directly (not when imported by a test).
|
||||
// npm's bin shims pass a NON-normalized argv[1] (e.g. ".../.bin/../<pkg>/bin/cli.js"
|
||||
// on Windows) and may differ in case, so realpath BOTH sides before comparing —
|
||||
// a naive string === misses the npx/shim path and the CLI silently no-ops.
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { realpathSync } from 'node:fs';
|
||||
import { argv } from 'node:process';
|
||||
const invokedDirectly = (() => {
|
||||
if (!argv[1]) return false;
|
||||
try {
|
||||
const a = realpathSync(argv[1]);
|
||||
const b = realpathSync(fileURLToPath(import.meta.url));
|
||||
return process.platform === 'win32' ? a.toLowerCase() === b.toLowerCase() : a === b;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
})();
|
||||
if (invokedDirectly) {
|
||||
run(argv.slice(2))
|
||||
.then((code) => process.exit(code))
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
{
|
||||
"name": "wifi-densepose-sar-harness",
|
||||
"version": "0.1.0",
|
||||
"description": "Harness for wifi-densepose-sar",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"wifi-densepose-sar-harness": "bin/cli.js"
|
||||
},
|
||||
"files": [
|
||||
"bin/**",
|
||||
"dist/**",
|
||||
"src/**",
|
||||
"tsconfig.json",
|
||||
".claude/**",
|
||||
"CLAUDE.md",
|
||||
"README.md",
|
||||
"LICENSE"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"test": "vitest run",
|
||||
"init": "node ./bin/cli.js init",
|
||||
"doctor": "node ./bin/cli.js doctor",
|
||||
"evolve": "metaharness-darwin evolve . --sandbox real --generations 3 --children 4",
|
||||
"evolve:dry": "metaharness-darwin evolve . --sandbox mock --generations 2 --children 3",
|
||||
"route": "node ./bin/cli.js route",
|
||||
"flywheel:dry": "npm run build && node ./bin/cli.js flywheel 3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@metaharness/kernel": "^0.1.0",
|
||||
"@metaharness/host-claude-code": "^0.1.0",
|
||||
"@metaharness/router": "^0.3.2",
|
||||
"@metaharness/flywheel": "^0.1.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.0.0",
|
||||
"typescript": "^5.4.0",
|
||||
"vitest": "^3.0.0",
|
||||
"@metaharness/darwin": "^0.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Architect agent — Designs the change before code is written.
|
||||
|
||||
export const SYSTEM_PROMPT = `You are the architect. Before any code is written you produce the smallest design that satisfies the request: the files to touch, the interfaces to add, and the trade-offs. You never write the implementation — you hand a crisp plan to the implementer. Prefer reuse over new abstractions; call out any change that ripples beyond three files. You operate inside the wifi-densepose-sar-harness harness; defer destructive actions to the user.`;
|
||||
|
||||
export const NAME = 'architect';
|
||||
export const TIER = 'opus' as const;
|
||||
@@ -1,7 +0,0 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Implementer agent — Writes code that matches the surrounding style.
|
||||
|
||||
export const SYSTEM_PROMPT = `You implement the architect's plan. Match the existing code's naming, comment density, and idioms — your diff should read like the person who wrote the file kept writing. Make the minimal change; do not refactor unrelated code. Leave the tests to the test-writer unless asked. You operate inside the wifi-densepose-sar-harness harness; defer destructive actions to the user.`;
|
||||
|
||||
export const NAME = 'implementer';
|
||||
export const TIER = 'sonnet' as const;
|
||||
@@ -1,7 +0,0 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Reviewer agent — Hunts correctness bugs in the diff.
|
||||
|
||||
export const SYSTEM_PROMPT = `You review diffs for correctness, security, and reuse. Report only high-confidence findings, each with a file:line and a concrete fix. Distinguish a bug (will break) from a nit (style). Never approve a change that widens a permission, swallows an error, or ships a secret. You operate inside the wifi-densepose-sar-harness harness; defer destructive actions to the user.`;
|
||||
|
||||
export const NAME = 'reviewer';
|
||||
export const TIER = 'opus' as const;
|
||||
@@ -1,7 +0,0 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Test Writer agent — Adds the missing tests for the change.
|
||||
|
||||
export const SYSTEM_PROMPT = `You write the tests the change needs: the happy path, the boundary, and the one failure mode most likely to regress. Mirror the project's existing test style and runner. A test that cannot fail is worse than no test — assert behaviour, not implementation. You operate inside the wifi-densepose-sar-harness harness; defer destructive actions to the user.`;
|
||||
|
||||
export const NAME = 'test-writer';
|
||||
export const TIER = 'sonnet' as const;
|
||||
@@ -1,114 +0,0 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
// The wifi-densepose-sar harness's self-improvement loop, via
|
||||
// @metaharness/flywheel: run -> measure -> mutate -> verify -> promote,
|
||||
// with a frozen, conjunctive promotion gate and a signed, replayable lineage
|
||||
// (see @metaharness/darwin's `npm run evolve` for the harness-wide mutation
|
||||
// entry point this formalizes the promotion loop for).
|
||||
//
|
||||
// HONESTY NOTE (load-bearing): `runSarFlywheelDemo()` below wires the real
|
||||
// @metaharness/flywheel API end-to-end, but its Proposer and Evaluator are
|
||||
// SYNTHETIC stand-ins — a deterministic string mutation and a deterministic
|
||||
// scoring function over that string, with NO model call and NO real
|
||||
// benchmark suite. It exists to prove the wiring works (see
|
||||
// __tests__/flywheel.test.ts: a non-empty lift curve, a verifiable replay
|
||||
// bundle) and to give a `dataSource: 'SYNTHETIC'`-stamped demo, exactly as
|
||||
// the flywheel's own design requires callers to be honest about data
|
||||
// provenance. A LIVE run needs the operator to supply:
|
||||
// - a real Proposer: an actual model call that improves one policy lever
|
||||
// (e.g. the review-diff checklist, the architect's planning prompt);
|
||||
// - a real Evaluator: scores that policy against real coding-task
|
||||
// holdout/anchor suites (e.g. "did review-diff catch the seeded bug").
|
||||
// Neither exists in this repo — wiring them is a live-API-key decision for
|
||||
// whoever operates this harness, not something to fake here.
|
||||
|
||||
import {
|
||||
runFlywheelGenerations,
|
||||
meetsPromotionRule,
|
||||
makeSigner,
|
||||
verifyReplayBundle,
|
||||
type Policy,
|
||||
type PolicyGenome,
|
||||
type Proposer,
|
||||
type Evaluator,
|
||||
type Suite,
|
||||
type FlywheelResult,
|
||||
} from '@metaharness/flywheel';
|
||||
|
||||
/** The gen-0 operating policy for the SAR harness's review agents. Opaque
|
||||
* string levers — the flywheel never interprets their meaning, only the
|
||||
* Evaluator does. */
|
||||
export const SAR_ROOT_POLICY: Policy = {
|
||||
reviewDepth: 'standard-checklist',
|
||||
architectPlanning: 'single-pass',
|
||||
};
|
||||
|
||||
/**
|
||||
* SYNTHETIC proposer: deterministically lengthens/varies the target lever's
|
||||
* value rather than calling a model. Stands in for a real model call that
|
||||
* would draft an improved lever value.
|
||||
*/
|
||||
const syntheticProposer: Proposer = async (base: PolicyGenome, target: string) => {
|
||||
const current = base.policy[target] ?? '';
|
||||
return `${current}+g${base.generation + 1}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* SYNTHETIC evaluator: scores a policy purely as a function of its own
|
||||
* string content (longer, more "refined"-looking levers score marginally
|
||||
* higher on quality and lower on no-op rate, both with a floor/ceiling) —
|
||||
* a deterministic stand-in for actually running the harness's agents
|
||||
* against a real coding-task suite. `noopRate` must move (not stay
|
||||
* constant) for anything to ever promote: the default gate's clause 2
|
||||
* requires it to strictly improve generation over generation (ADR-226's
|
||||
* "the executor policy is the part that mattered" finding, encoded as a
|
||||
* hard requirement) — a constant noopRate, even a "good" one, gates every
|
||||
* candidate out forever.
|
||||
*/
|
||||
const syntheticEvaluator: Evaluator = async (policy: Policy, _suite: Suite) => {
|
||||
const totalLength = Object.values(policy).reduce((s, v) => s + v.length, 0);
|
||||
const primary = Math.min(0.5 + totalLength / 200, 0.98);
|
||||
const noopRate = Math.max(0.3 - totalLength / 300, 0.02);
|
||||
return {
|
||||
primary,
|
||||
noopRate,
|
||||
costPerWin: 1 / primary,
|
||||
regressed: false,
|
||||
};
|
||||
};
|
||||
|
||||
const SAR_HOLDOUT: Suite = {
|
||||
id: 'sar-harness-holdout-synthetic',
|
||||
items: ['seeded-review-task-1', 'seeded-review-task-2', 'seeded-review-task-3'],
|
||||
};
|
||||
|
||||
const SAR_ANCHOR: Suite = {
|
||||
id: 'sar-harness-anchor-synthetic',
|
||||
items: ['frozen-regression-task-1'],
|
||||
};
|
||||
|
||||
/**
|
||||
* Run a small, fully SYNTHETIC flywheel demo end-to-end: propose, evaluate,
|
||||
* gate, and (when it clears the gate) promote a few generations of mutated
|
||||
* policy, returning the real @metaharness/flywheel result — a genuine lift
|
||||
* curve and a signed, independently replayable bundle, just built from
|
||||
* synthetic (not live) evidence.
|
||||
*/
|
||||
export async function runSarFlywheelDemo(maxGenerations = 3): Promise<FlywheelResult> {
|
||||
return runFlywheelGenerations({
|
||||
rootPolicy: SAR_ROOT_POLICY,
|
||||
proposer: syntheticProposer,
|
||||
evaluator: syntheticEvaluator,
|
||||
promotionRule: meetsPromotionRule,
|
||||
holdout: SAR_HOLDOUT,
|
||||
anchor: SAR_ANCHOR,
|
||||
maxGenerations,
|
||||
signer: makeSigner(),
|
||||
dataSource: 'SYNTHETIC',
|
||||
});
|
||||
}
|
||||
|
||||
/** Independently verify a flywheel demo's replay bundle (no trust in the producer). */
|
||||
export function verifySarFlywheelDemo(result: FlywheelResult) {
|
||||
return verifyReplayBundle(result.replayBundle);
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Generated by create-agent-harness — your harness's `wifi-densepose-sar-harness init` entry.
|
||||
|
||||
import { loadKernel } from '@metaharness/kernel';
|
||||
import adapter from '@metaharness/host-claude-code';
|
||||
|
||||
const HARNESS_NAME = 'wifi-densepose-sar-harness';
|
||||
|
||||
async function main(): Promise<number> {
|
||||
const kernel = await loadKernel();
|
||||
const info = kernel.kernelInfo();
|
||||
console.log(`${HARNESS_NAME} — kernel ${info.version} (${kernel.backend})`);
|
||||
console.log(`Host adapter: ${adapter.name}`);
|
||||
console.log(`Run \`${HARNESS_NAME} doctor\` to verify the install.`);
|
||||
return 0;
|
||||
}
|
||||
|
||||
main().then(c => process.exit(c)).catch(err => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,77 +0,0 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
// Cost-optimal task routing for the wifi-densepose-sar harness, via
|
||||
// @metaharness/router (ADR-040's DRACO Phase-2 finding, productized): route
|
||||
// each agent query to the cheapest model predicted to clear a quality bar,
|
||||
// instead of sending every query to the frontier tier by default.
|
||||
//
|
||||
// HONESTY NOTE: the candidate `examples` below are SEED/ILLUSTRATIVE data —
|
||||
// four hand-picked (embedding, quality) points per candidate, not measured
|
||||
// eval-log observations. They exist so `sarTaskRouter` is a real, runnable
|
||||
// k-NN router out of the box (see __tests__/router.test.ts), not so its
|
||||
// routing decisions should be trusted for production cost savings. Replace
|
||||
// `SAR_ROUTER_CANDIDATES[*].examples` with real (query embedding → quality
|
||||
// achieved) rows from your own eval logs before relying on this for
|
||||
// production routing — see @metaharness/router's README ("the more
|
||||
// examples, the closer it gets to the per-query oracle").
|
||||
|
||||
import { Router, type RouterCandidate } from '@metaharness/router';
|
||||
|
||||
/**
|
||||
* A 4-axis feature embedding for a harness query, used only to pick a
|
||||
* nearby labelled example — NOT a real text embedding. Axes (each 0..1):
|
||||
* [0] physicsExplanation — "explain the range-resolution formula"-shaped
|
||||
* [1] codeReview — "review this diff for correctness"-shaped
|
||||
* [2] numericalDebugging — "why did this reconstruction test fail"-shaped
|
||||
* [3] docWriting — "write/update the tutorial"-shaped
|
||||
* A caller with a real embedding model should project onto whatever
|
||||
* dimensionality that model produces instead — the router only needs
|
||||
* consistent vectors, not these specific four axes.
|
||||
*/
|
||||
export type SarTaskEmbedding = readonly [number, number, number, number];
|
||||
|
||||
export const SAR_ROUTER_CANDIDATES: RouterCandidate[] = [
|
||||
{
|
||||
id: 'cheap-tier',
|
||||
costPerMTok: 1,
|
||||
examples: [
|
||||
{ embedding: [1, 0, 0, 0], quality: 0.9 }, // physics explanations: cheap tier does fine
|
||||
{ embedding: [0, 0, 0, 1], quality: 0.85 }, // doc writing: cheap tier does fine
|
||||
{ embedding: [0, 1, 0, 0], quality: 0.55 }, // code review: cheap tier is weak
|
||||
{ embedding: [0, 0, 1, 0], quality: 0.5 }, // numerical debugging: cheap tier is weak
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'frontier-tier',
|
||||
costPerMTok: 15,
|
||||
examples: [
|
||||
{ embedding: [1, 0, 0, 0], quality: 0.95 },
|
||||
{ embedding: [0, 0, 0, 1], quality: 0.93 },
|
||||
{ embedding: [0, 1, 0, 0], quality: 0.92 }, // code review: frontier tier needed
|
||||
{ embedding: [0, 0, 1, 0], quality: 0.9 }, // numerical debugging: frontier tier needed
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Cost-optimal router for the harness's four query shapes above. `qualityBar`
|
||||
* of 0.8 matches the router README's worked example: return the cheapest
|
||||
* candidate predicted to clear 80% quality, or the best-predicted candidate
|
||||
* if none do.
|
||||
*/
|
||||
export const sarTaskRouter = new Router({
|
||||
qualityBar: 0.8,
|
||||
candidates: SAR_ROUTER_CANDIDATES,
|
||||
// k=1: each candidate has only 4 (orthogonal, one-hot) examples covering
|
||||
// the 4 task axes. The router's default k=5 would average ALL of a
|
||||
// candidate's examples regardless of query similarity once a candidate
|
||||
// has <=5 examples, collapsing every query to the same prediction. k=1
|
||||
// makes it pick the single nearest labelled task type, which is what
|
||||
// this small illustrative dataset is shaped for.
|
||||
k: 1,
|
||||
});
|
||||
|
||||
/** Route one query embedding to the cost-optimal model tier. */
|
||||
export function routeSarQuery(queryEmbedding: SarTaskEmbedding) {
|
||||
return sarTaskRouter.route([...queryEmbedding]);
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"lib": ["ES2022"],
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["node_modules", "dist", "__tests__"]
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Generated by metaharness. Strips the `#!/usr/bin/env node` shebang from
|
||||
// importable entrypoints (e.g. bin/cli.js) before Vite parses them — Vite/esbuild
|
||||
// (used internally by Vitest) does NOT strip shebangs, so importing a shebanged
|
||||
// module throws `SyntaxError: Invalid or unexpected token`. See issue #44.
|
||||
// Has no effect on direct CLI execution or `npm run doctor` (those bypass Vite).
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
{
|
||||
name: 'strip-shebang',
|
||||
enforce: 'pre',
|
||||
transform(code: string) {
|
||||
if (code.startsWith('#!')) {
|
||||
// Replace with a blank line so source line numbers stay aligned.
|
||||
return { code: code.replace(/^#![^\n]*/, ''), map: null };
|
||||
}
|
||||
return null;
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
Generated
-13
@@ -11627,19 +11627,6 @@ dependencies = [
|
||||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wifi-densepose-sar"
|
||||
version = "0.3.1"
|
||||
dependencies = [
|
||||
"criterion",
|
||||
"num-complex",
|
||||
"rand 0.8.5",
|
||||
"rand_chacha 0.3.1",
|
||||
"rayon",
|
||||
"serde",
|
||||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wifi-densepose-sensing-server"
|
||||
version = "0.3.5"
|
||||
|
||||
@@ -88,12 +88,6 @@ members = [
|
||||
# submodule crates (rufield-core/-provenance/-privacy/-fusion); single
|
||||
# coupling point between RuView and the standalone RuField MFS spec.
|
||||
"crates/wifi-densepose-rufield",
|
||||
# ADR-287 — coherent wideband RF tomography research crate: synthetic
|
||||
# stepped-frequency multi-position measurement simulation + delay-and-sum
|
||||
# backprojection reconstruction. Standalone leaf (nvsim pattern), zero
|
||||
# hardware coupling, every number SYNTHETIC/L0 until real wideband RF
|
||||
# hardware exists.
|
||||
"crates/wifi-densepose-sar",
|
||||
]
|
||||
# ADR-040: WASM edge crate targets wasm32-unknown-unknown (no_std),
|
||||
# excluded from workspace to avoid breaking `cargo test --workspace`.
|
||||
|
||||
@@ -14,7 +14,7 @@ name = "nvsim-server"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
nvsim = { path = "../nvsim", version = "0.3.1" }
|
||||
nvsim = { path = "../nvsim" }
|
||||
axum = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
tower = { workspace = true }
|
||||
|
||||
@@ -3,9 +3,6 @@ name = "wifi-densepose-pointcloud"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "Real-time dense point cloud from camera depth + WiFi CSI tomography"
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[[bin]]
|
||||
name = "ruview-pointcloud"
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
[package]
|
||||
name = "wifi-densepose-sar"
|
||||
description = "Coherent wideband RF tomography research crate (ADR-287): synthetic stepped-frequency multi-position measurement simulation + delay-and-sum backprojection reconstruction. SYNTHETIC/L0 only -- no wideband RF hardware backs this crate."
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
documentation.workspace = true
|
||||
keywords = ["radar", "sar", "backprojection", "rf-tomography", "simulation"]
|
||||
categories = ["science", "simulation"]
|
||||
readme = "README.md"
|
||||
|
||||
# `wifi-densepose-sar` is a standalone leaf crate (the nvsim / ruview-unified
|
||||
# pattern): pure-Rust math, deterministic ChaCha20 randomness (same seed =>
|
||||
# byte-identical output on every machine), zero coupling to any hardware
|
||||
# ingestion path. It has NO internal RuView dependency -- see the crate-level
|
||||
# doc comment in `src/lib.rs` for why (ADR-287 §2): this validates the
|
||||
# reconstruction *algorithm* against synthetic ground truth before any
|
||||
# question of wiring it into `ruview-unified`'s `FmcwRadarCube` adapter or
|
||||
# `GaussianMap` is in scope.
|
||||
[dependencies]
|
||||
num-complex = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
rand = { version = "0.8", default-features = false }
|
||||
rand_chacha = { version = "0.3", default-features = false }
|
||||
rayon = "1.10"
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = { workspace = true }
|
||||
|
||||
[[bench]]
|
||||
name = "backprojection_bench"
|
||||
harness = false
|
||||
|
||||
[lints.rust]
|
||||
unsafe_code = "forbid"
|
||||
missing_docs = "warn"
|
||||
|
||||
[lints.clippy]
|
||||
all = "warn"
|
||||
@@ -1,74 +0,0 @@
|
||||
# wifi-densepose-sar
|
||||
|
||||
Coherent wideband RF tomography research crate (ADR-287): synthetic
|
||||
stepped-frequency multi-position measurement simulation + delay-and-sum
|
||||
backprojection reconstruction of a 3D reflectivity field.
|
||||
|
||||
**This is not a hardware capability.** It is the reconstruction primitive a
|
||||
handheld through-wall RF imaging device would need, validated against its
|
||||
own synthetic ground truth. Every number this crate produces is
|
||||
SYNTHETIC / evidence level L0 (ADR-282) until real wideband RF hardware
|
||||
(a VNA, SDR, or purpose-built radar front end) exists to feed it real
|
||||
measurements. See the crate-level doc comment in `src/lib.rs` for the full
|
||||
honesty boundary, and the tutorial at
|
||||
`docs/tutorials/coherent-rf-tomography-backprojection.md` for a walkthrough.
|
||||
|
||||
## Quick example
|
||||
|
||||
```rust
|
||||
use wifi_densepose_sar::{
|
||||
backproject, linear_aperture, simulate_measurement, FrequencySweep,
|
||||
Point3, ScatteringTarget, VoxelGrid,
|
||||
};
|
||||
|
||||
let poses = linear_aperture(Point3::new(-0.5, 0.0, 0.0), Point3::new(0.5, 0.0, 0.0), 21);
|
||||
let sweep = FrequencySweep::new(2.0e9, 6.0e9, 32);
|
||||
let target = ScatteringTarget::new(Point3::new(0.0, 2.0, 0.0), 1.0);
|
||||
let measurement = simulate_measurement(&poses, &sweep, &[target], 0.01, 42);
|
||||
|
||||
let grid = VoxelGrid::new(Point3::new(-0.3, 1.7, -0.3), 0.03, 21, 21, 21);
|
||||
let image = backproject(&measurement, &poses, &sweep, &grid);
|
||||
let (peak_location, peak_magnitude) = image.peak();
|
||||
println!("reconstructed target near {peak_location:?}, magnitude {peak_magnitude:.4}");
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
cargo test -p wifi-densepose-sar --no-default-features
|
||||
cargo bench -p wifi-densepose-sar
|
||||
```
|
||||
|
||||
`tests/physics_validation.rs` checks the reconstruction's actual behavior
|
||||
against the closed-form formulas in `resolution.rs` (range resolution,
|
||||
cross-range/synthetic-aperture resolution, and the antenna-pose coherence
|
||||
budget) rather than merely asserting them: 25 tests (22 unit + 3
|
||||
integration), 0 failed, clippy-clean.
|
||||
|
||||
## Performance (MEASURED)
|
||||
|
||||
`cargo bench -p wifi-densepose-sar`, 21 antenna poses × 32 frequency steps
|
||||
(672 measurement terms/voxel), rayon-parallelized over voxels, this
|
||||
machine, release profile:
|
||||
|
||||
| Voxels | Median time | Throughput |
|
||||
|-------:|------------:|-----------:|
|
||||
| 512 | 300 µs | ~1.71M voxels/s |
|
||||
| 4,096 | 1.97 ms | ~2.08M voxels/s |
|
||||
| 32,768 | 14.5 ms | ~2.26M voxels/s |
|
||||
|
||||
Scales as expected: each voxel's cost is independent (`O(poses × freqs)`
|
||||
per voxel, embarrassingly parallel), so throughput is roughly constant
|
||||
across grid sizes and total time scales linearly with voxel count.
|
||||
|
||||
**Optimization (MEASURED, criterion regression detection, p < 0.001): ~4.4-4.5x
|
||||
faster** than the first-shipped implementation, across all three grid
|
||||
sizes. Frequencies in a [`FrequencySweep`](src/measurement.rs) are evenly
|
||||
spaced by construction, so the per-(pose, frequency) phase term is an
|
||||
arithmetic progression; `focus_at_point` now evaluates the phasor once per
|
||||
pose and advances it by a fixed complex-multiply step per frequency,
|
||||
instead of one `sin`/`cos` pair (`Complex64::from_polar`) per frequency —
|
||||
K trig evaluations become 2. Proven equivalent (not just faster) to an
|
||||
independently-reimplemented direct per-frequency reference in
|
||||
`reconstruct::tests::backprojection_incremental_rotation_matches_direct_per_frequency_computation`,
|
||||
across several sweep sizes and both on-target and off-target points.
|
||||
@@ -1,38 +0,0 @@
|
||||
//! Criterion benchmark for the backprojection reconstruction kernel.
|
||||
//! `cargo bench -p wifi-densepose-sar` reports MEASURED throughput --
|
||||
//! see the crate README for the last recorded numbers.
|
||||
#![allow(missing_docs)]
|
||||
|
||||
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
|
||||
use wifi_densepose_sar::geometry::linear_aperture;
|
||||
use wifi_densepose_sar::measurement::{simulate_measurement, FrequencySweep, ScatteringTarget};
|
||||
use wifi_densepose_sar::reconstruct::{backproject, VoxelGrid};
|
||||
use wifi_densepose_sar::Point3;
|
||||
|
||||
fn backprojection_benchmark(c: &mut Criterion) {
|
||||
let poses = linear_aperture(Point3::new(-0.5, 0.0, 0.0), Point3::new(0.5, 0.0, 0.0), 21);
|
||||
let sweep = FrequencySweep::new(2.0e9, 6.0e9, 32);
|
||||
let targets = vec![
|
||||
ScatteringTarget::new(Point3::new(0.0, 2.0, 0.0), 1.0),
|
||||
ScatteringTarget::new(Point3::new(0.3, 1.8, 0.1), 0.6),
|
||||
];
|
||||
let measurement = simulate_measurement(&poses, &sweep, &targets, 0.01, 7);
|
||||
|
||||
let mut group = c.benchmark_group("backproject");
|
||||
for &voxels_per_axis in &[8usize, 16, 32] {
|
||||
let grid = VoxelGrid::new(
|
||||
Point3::new(-0.5, 1.5, -0.5),
|
||||
1.0 / voxels_per_axis as f64,
|
||||
voxels_per_axis,
|
||||
voxels_per_axis,
|
||||
voxels_per_axis,
|
||||
);
|
||||
group.bench_with_input(BenchmarkId::from_parameter(grid.len()), &grid, |b, grid| {
|
||||
b.iter(|| backproject(&measurement, &poses, &sweep, grid));
|
||||
});
|
||||
}
|
||||
group.finish();
|
||||
}
|
||||
|
||||
criterion_group!(benches, backprojection_benchmark);
|
||||
criterion_main!(benches);
|
||||
@@ -1,140 +0,0 @@
|
||||
//! Antenna positions, synthetic-aperture trajectories, and point geometry.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// A point in 3D space, meters, in an arbitrary right-handed scene frame.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Point3 {
|
||||
/// X coordinate, meters.
|
||||
pub x: f64,
|
||||
/// Y coordinate, meters.
|
||||
pub y: f64,
|
||||
/// Z coordinate, meters.
|
||||
pub z: f64,
|
||||
}
|
||||
|
||||
impl Point3 {
|
||||
/// Construct a point.
|
||||
pub fn new(x: f64, y: f64, z: f64) -> Self {
|
||||
Self { x, y, z }
|
||||
}
|
||||
|
||||
/// Euclidean distance to another point, meters.
|
||||
pub fn distance(&self, other: &Point3) -> f64 {
|
||||
let dx = self.x - other.x;
|
||||
let dy = self.y - other.y;
|
||||
let dz = self.z - other.z;
|
||||
(dx * dx + dy * dy + dz * dz).sqrt()
|
||||
}
|
||||
|
||||
/// Unit vector pointing from `self` toward `other`. Returns `None` if
|
||||
/// the two points coincide (distance below `f64::EPSILON`).
|
||||
pub fn direction_to(&self, other: &Point3) -> Option<Point3> {
|
||||
let d = self.distance(other);
|
||||
if d < f64::EPSILON {
|
||||
return None;
|
||||
}
|
||||
Some(Point3::new(
|
||||
(other.x - self.x) / d,
|
||||
(other.y - self.y) / d,
|
||||
(other.z - self.z) / d,
|
||||
))
|
||||
}
|
||||
|
||||
/// Translate this point by `dist` meters along a unit vector `dir`.
|
||||
pub fn translated(&self, dir: Point3, dist: f64) -> Point3 {
|
||||
Point3::new(
|
||||
self.x + dir.x * dist,
|
||||
self.y + dir.y * dist,
|
||||
self.z + dir.z * dist,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// A single antenna position along a synthetic-aperture trajectory.
|
||||
///
|
||||
/// Only position is modeled (an isotropic-antenna approximation, ADR-287
|
||||
/// §4) -- no antenna gain pattern / boresight direction is applied to the
|
||||
/// forward measurement model.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||
pub struct AntennaPose {
|
||||
/// Antenna phase-center position, meters.
|
||||
pub position: Point3,
|
||||
}
|
||||
|
||||
impl AntennaPose {
|
||||
/// Construct a pose at `position`.
|
||||
pub fn new(position: Point3) -> Self {
|
||||
Self { position }
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate `n` antenna poses evenly spaced along a straight line segment
|
||||
/// from `start` to `end` (inclusive), the canonical "handheld linear sweep"
|
||||
/// synthetic aperture. `n` must be >= 2 for a non-degenerate aperture.
|
||||
pub fn linear_aperture(start: Point3, end: Point3, n: usize) -> Vec<AntennaPose> {
|
||||
if n == 0 {
|
||||
return Vec::new();
|
||||
}
|
||||
if n == 1 {
|
||||
return vec![AntennaPose::new(start)];
|
||||
}
|
||||
(0..n)
|
||||
.map(|i| {
|
||||
let t = i as f64 / (n - 1) as f64;
|
||||
AntennaPose::new(Point3::new(
|
||||
start.x + (end.x - start.x) * t,
|
||||
start.y + (end.y - start.y) * t,
|
||||
start.z + (end.z - start.z) * t,
|
||||
))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The physical length of a synthetic aperture: the distance between its
|
||||
/// first and last pose. Used by [`crate::resolution::cross_range_resolution`].
|
||||
pub fn aperture_length(poses: &[AntennaPose]) -> f64 {
|
||||
match (poses.first(), poses.last()) {
|
||||
(Some(a), Some(b)) => a.position.distance(&b.position),
|
||||
_ => 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn linear_aperture_spans_endpoints() {
|
||||
let start = Point3::new(0.0, 0.0, 0.0);
|
||||
let end = Point3::new(1.0, 0.0, 0.0);
|
||||
let poses = linear_aperture(start, end, 5);
|
||||
assert_eq!(poses.len(), 5);
|
||||
assert_eq!(poses[0].position, start);
|
||||
assert_eq!(poses[4].position, end);
|
||||
// Evenly spaced: 0, 0.25, 0.5, 0.75, 1.0 along x.
|
||||
assert!((poses[2].position.x - 0.5).abs() < 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aperture_length_matches_endpoint_distance() {
|
||||
let poses = linear_aperture(Point3::new(0.0, 0.0, 0.0), Point3::new(3.0, 4.0, 0.0), 10);
|
||||
assert!((aperture_length(&poses) - 5.0).abs() < 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn direction_to_is_unit_length() {
|
||||
let a = Point3::new(0.0, 0.0, 0.0);
|
||||
let b = Point3::new(2.0, 0.0, 0.0);
|
||||
let dir = a.direction_to(&b).unwrap();
|
||||
assert!((dir.x - 1.0).abs() < 1e-12);
|
||||
let len = (dir.x * dir.x + dir.y * dir.y + dir.z * dir.z).sqrt();
|
||||
assert!((len - 1.0).abs() < 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn direction_to_coincident_points_is_none() {
|
||||
let a = Point3::new(1.0, 1.0, 1.0);
|
||||
assert!(a.direction_to(&a).is_none());
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
//! # wifi-densepose-sar — Coherent Wideband RF Tomography (ADR-287)
|
||||
//!
|
||||
//! A research crate implementing the reconstruction primitive that a
|
||||
//! handheld through-wall RF imaging device (the class of product exemplified
|
||||
//! by YC-backed Applied Electrodynamics' "WaveSight") would need: recovering
|
||||
//! a 3D reflectivity field from coherent, stepped-frequency, multi-position
|
||||
//! RF measurements via delay-and-sum backprojection.
|
||||
//!
|
||||
//! ## What this crate is
|
||||
//!
|
||||
//! - [`measurement`]: a forward simulator producing synthetic complex,
|
||||
//! stepped-frequency returns from known point scatterers observed by a
|
||||
//! known synthetic-aperture antenna trajectory.
|
||||
//! - [`reconstruct`]: the backprojection reconstruction kernel that inverts
|
||||
//! that forward model back into a 3D voxel reflectivity image.
|
||||
//! - [`pointcloud`]: sparse point extraction from the dense voxel image.
|
||||
//! - [`resolution`]: closed-form range/cross-range resolution and
|
||||
//! coherence-budget formulas (`ΔR = c/2B`, `δ_CR ≈ λR/2L`, the `λ/8`
|
||||
//! antenna-pose tolerance), checked against the reconstruction's actual
|
||||
//! behavior in `tests/physics_validation.rs` rather than merely asserted.
|
||||
//! - [`geometry`]: antenna poses and synthetic-aperture trajectories.
|
||||
//!
|
||||
//! ## What this crate is NOT (ADR-287 §1, honesty boundary)
|
||||
//!
|
||||
//! - **Not a hardware driver.** There is no VNA, SDR, or wideband RF
|
||||
//! front-end integration here, and none of `wifi-densepose-hardware`'s
|
||||
//! ESP32/CSI chipset code is touched. Every measurement in this crate's
|
||||
//! tests and benchmarks is [`measurement::simulate_measurement`] output.
|
||||
//! - **Not a reproduction of any published system.** ADR-278 already gates
|
||||
//! RISE/DiffRadar/GeRaF reproduction as a separate, much larger research
|
||||
//! program with its own acceptance gates; this crate does not attempt
|
||||
//! any of them. It is scoped one level below that: the bare
|
||||
//! measurement-model + backprojection primitive those (or any other SAR
|
||||
//! pipeline) would be built on.
|
||||
//! - **Not a claim about Applied Electrodynamics' product.** Their exact
|
||||
//! waveform, antenna count, bandwidth, and reconstruction algorithm are
|
||||
//! undisclosed; nothing here reproduces or benchmarks against their
|
||||
//! device. It is simply the same well-established SAR/GPR physics
|
||||
//! (Skolnik-style backprojection) applied to synthetic data.
|
||||
//! - **Not wired into `ruview-unified` or `GaussianMap`.** ADR-278 §2.4
|
||||
//! names that as the eventual integration point once (and if) a gated
|
||||
//! reconstruction system exists; this crate is deliberately a leaf with
|
||||
//! no RuView dependency (the nvsim pattern) so its physics can be
|
||||
//! validated in isolation first.
|
||||
//! - **Every number produced by this crate is SYNTHETIC / evidence level
|
||||
//! L0** (ADR-282's mandatory evidence ladder), generated by its own
|
||||
//! forward simulator, scored against its own ground truth. It says
|
||||
//! nothing about real-world through-wall imaging performance, which
|
||||
//! depends on multipath, wall materials, antenna gain patterns, receiver
|
||||
//! noise figures, and calibration accuracy that this crate does not
|
||||
//! model.
|
||||
//!
|
||||
//! ## Design commitments
|
||||
//!
|
||||
//! - **Deterministic**: [`measurement::simulate_measurement`] is seeded
|
||||
//! ChaCha20 (the nvsim / ruview-unified commitment) -- same seed yields
|
||||
//! byte-identical output on every machine.
|
||||
//! - **Proven, not asserted**: `tests/physics_validation.rs` checks the
|
||||
//! reconstruction's actual point-target localization error, range
|
||||
//! resolution, cross-range resolution, and pose-error sensitivity against
|
||||
//! the closed-form predictions in [`resolution`] -- the same discipline
|
||||
//! `ruview-unified` uses for its ray tracer (checked against Friis and
|
||||
//! reciprocity) and its encoder (checked against finite differences).
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
pub mod geometry;
|
||||
pub mod measurement;
|
||||
pub mod pointcloud;
|
||||
pub mod reconstruct;
|
||||
pub mod resolution;
|
||||
|
||||
pub use geometry::{linear_aperture, AntennaPose, Point3};
|
||||
pub use measurement::{simulate_measurement, FrequencySweep, Measurement, ScatteringTarget};
|
||||
pub use pointcloud::{extract_point_cloud, PointCloudPoint};
|
||||
pub use reconstruct::{backproject, focus_at_point, ReflectivityImage, VoxelGrid};
|
||||
@@ -1,227 +0,0 @@
|
||||
//! Forward measurement model: simulate the complex, stepped-frequency
|
||||
//! returns a monostatic synthetic-aperture radar would record from a set
|
||||
//! of point scatterers (ADR-287 §2).
|
||||
//!
|
||||
//! ```text
|
||||
//! y_{m,k} = sum_j sigma_j / R_{m,j}^2 * exp(-i * 4*pi * f_k * R_{m,j} / c) + noise
|
||||
//! ```
|
||||
//!
|
||||
//! where `m` indexes antenna position, `k` indexes swept frequency, `j`
|
||||
//! indexes point scatterer, `R_{m,j}` is the range from antenna position
|
||||
//! `m` to scatterer `j`, and the `4*pi*f/c` phase term is the two-way
|
||||
//! (round-trip) propagation phase for a monostatic (single antenna acting
|
||||
//! as both transmitter and receiver) system. The `1/R^2` term is the
|
||||
//! two-way free-space spreading loss (amplitude, not power).
|
||||
//!
|
||||
//! This is a deliberately simplified physical model: free-space
|
||||
//! propagation only (no multipath, no per-material attenuation, no
|
||||
//! antenna gain pattern). It exists to give the reconstruction algorithm
|
||||
//! in [`crate::reconstruct`] a known ground truth to be checked against,
|
||||
//! not to predict real hardware performance.
|
||||
|
||||
use crate::geometry::{AntennaPose, Point3};
|
||||
use num_complex::Complex64;
|
||||
use rand::{Rng, SeedableRng};
|
||||
use rand_chacha::ChaCha20Rng;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::f64::consts::PI;
|
||||
|
||||
use crate::resolution::SPEED_OF_LIGHT_M_PER_S;
|
||||
|
||||
/// A stepped-frequency sweep: `n_steps` evenly spaced frequencies from
|
||||
/// `start_hz` to `stop_hz` inclusive.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||
pub struct FrequencySweep {
|
||||
/// Lowest swept frequency, Hz.
|
||||
pub start_hz: f64,
|
||||
/// Highest swept frequency, Hz.
|
||||
pub stop_hz: f64,
|
||||
/// Number of evenly spaced frequency steps (>= 1).
|
||||
pub n_steps: usize,
|
||||
}
|
||||
|
||||
impl FrequencySweep {
|
||||
/// Construct a sweep. Panics if `n_steps == 0` or `stop_hz < start_hz`.
|
||||
pub fn new(start_hz: f64, stop_hz: f64, n_steps: usize) -> Self {
|
||||
assert!(n_steps > 0, "a frequency sweep needs at least one step");
|
||||
assert!(stop_hz >= start_hz, "stop_hz must be >= start_hz");
|
||||
Self { start_hz, stop_hz, n_steps }
|
||||
}
|
||||
|
||||
/// The `n_steps` evenly spaced frequencies, Hz, ascending.
|
||||
pub fn frequencies(&self) -> Vec<f64> {
|
||||
if self.n_steps == 1 {
|
||||
return vec![self.start_hz];
|
||||
}
|
||||
(0..self.n_steps)
|
||||
.map(|i| {
|
||||
let t = i as f64 / (self.n_steps - 1) as f64;
|
||||
self.start_hz + (self.stop_hz - self.start_hz) * t
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Total swept bandwidth, Hz.
|
||||
pub fn bandwidth_hz(&self) -> f64 {
|
||||
self.stop_hz - self.start_hz
|
||||
}
|
||||
|
||||
/// Center frequency, Hz.
|
||||
pub fn center_freq_hz(&self) -> f64 {
|
||||
(self.start_hz + self.stop_hz) / 2.0
|
||||
}
|
||||
}
|
||||
|
||||
/// A single point scatterer: a location and a scalar reflectivity
|
||||
/// (dimensionless; only relative magnitudes across scatterers matter).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||
pub struct ScatteringTarget {
|
||||
/// Scatterer location, meters.
|
||||
pub position: Point3,
|
||||
/// Scalar reflectivity (>= 0 for a physical scatterer; the forward
|
||||
/// model does not enforce this so synthetic "negative" scatterers can
|
||||
/// be used to probe reconstruction linearity in tests).
|
||||
pub reflectivity: f64,
|
||||
}
|
||||
|
||||
impl ScatteringTarget {
|
||||
/// Construct a target.
|
||||
pub fn new(position: Point3, reflectivity: f64) -> Self {
|
||||
Self { position, reflectivity }
|
||||
}
|
||||
}
|
||||
|
||||
/// The complex, stepped-frequency measurement recorded at every
|
||||
/// (antenna position, frequency) pair. Row-major: index `[m][k]` is at
|
||||
/// `samples[m * n_freqs + k]`.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Measurement {
|
||||
/// Number of antenna positions.
|
||||
pub n_poses: usize,
|
||||
/// Number of frequency steps.
|
||||
pub n_freqs: usize,
|
||||
/// Complex samples, row-major over (pose, frequency).
|
||||
pub samples: Vec<Complex64>,
|
||||
}
|
||||
|
||||
impl Measurement {
|
||||
/// The complex sample at antenna position `m`, frequency index `k`.
|
||||
pub fn get(&self, m: usize, k: usize) -> Complex64 {
|
||||
self.samples[m * self.n_freqs + k]
|
||||
}
|
||||
}
|
||||
|
||||
/// Simulate the forward measurement model for `targets` observed from
|
||||
/// `poses` across the frequencies in `sweep`, with i.i.d. complex Gaussian
|
||||
/// noise of standard deviation `noise_std` (per real/imaginary component)
|
||||
/// added to every sample. Deterministic given `seed` (ChaCha20, the
|
||||
/// nvsim/ruview-unified reproducibility commitment: same seed -> identical
|
||||
/// output on every machine).
|
||||
pub fn simulate_measurement(
|
||||
poses: &[AntennaPose],
|
||||
sweep: &FrequencySweep,
|
||||
targets: &[ScatteringTarget],
|
||||
noise_std: f64,
|
||||
seed: u64,
|
||||
) -> Measurement {
|
||||
let freqs = sweep.frequencies();
|
||||
let n_poses = poses.len();
|
||||
let n_freqs = freqs.len();
|
||||
let mut rng = ChaCha20Rng::seed_from_u64(seed);
|
||||
let mut samples = Vec::with_capacity(n_poses * n_freqs);
|
||||
|
||||
for pose in poses {
|
||||
for &f in &freqs {
|
||||
let mut y = Complex64::new(0.0, 0.0);
|
||||
for t in targets {
|
||||
let r = pose.position.distance(&t.position);
|
||||
if r < 1e-6 {
|
||||
// Degenerate: antenna co-located with target. Skip to
|
||||
// avoid a divide-by-zero singularity in 1/R^2.
|
||||
continue;
|
||||
}
|
||||
let amplitude = t.reflectivity / (r * r);
|
||||
let phase = -4.0 * PI * f * r / SPEED_OF_LIGHT_M_PER_S;
|
||||
y += Complex64::from_polar(amplitude, phase);
|
||||
}
|
||||
if noise_std > 0.0 {
|
||||
y += Complex64::new(gaussian(&mut rng, noise_std), gaussian(&mut rng, noise_std));
|
||||
}
|
||||
samples.push(y);
|
||||
}
|
||||
}
|
||||
|
||||
Measurement { n_poses, n_freqs, samples }
|
||||
}
|
||||
|
||||
/// Box-Muller transform: one N(0, std^2) sample from two uniform draws.
|
||||
fn gaussian(rng: &mut ChaCha20Rng, std: f64) -> f64 {
|
||||
let u1: f64 = rng.gen_range(f64::EPSILON..1.0);
|
||||
let u2: f64 = rng.gen_range(0.0..1.0);
|
||||
std * (-2.0 * u1.ln()).sqrt() * (2.0 * PI * u2).cos()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::geometry::linear_aperture;
|
||||
|
||||
#[test]
|
||||
fn frequency_sweep_endpoints_and_count() {
|
||||
let sweep = FrequencySweep::new(1.0e9, 4.0e9, 5);
|
||||
let freqs = sweep.frequencies();
|
||||
assert_eq!(freqs.len(), 5);
|
||||
assert!((freqs[0] - 1.0e9).abs() < 1e-6);
|
||||
assert!((freqs[4] - 4.0e9).abs() < 1e-6);
|
||||
assert!((sweep.bandwidth_hz() - 3.0e9).abs() < 1e-6);
|
||||
assert!((sweep.center_freq_hz() - 2.5e9).abs() < 1e-6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_step_sweep_is_just_start_hz() {
|
||||
let sweep = FrequencySweep::new(2.0e9, 2.0e9, 1);
|
||||
assert_eq!(sweep.frequencies(), vec![2.0e9]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simulate_measurement_is_deterministic_given_seed() {
|
||||
let poses = linear_aperture(Point3::new(0.0, 0.0, 0.0), Point3::new(0.3, 0.0, 0.0), 4);
|
||||
let sweep = FrequencySweep::new(2.0e9, 3.0e9, 8);
|
||||
let targets = vec![ScatteringTarget::new(Point3::new(0.15, 1.0, 0.0), 1.0)];
|
||||
let a = simulate_measurement(&poses, &sweep, &targets, 0.01, 42);
|
||||
let b = simulate_measurement(&poses, &sweep, &targets, 0.01, 42);
|
||||
for (sa, sb) in a.samples.iter().zip(b.samples.iter()) {
|
||||
assert_eq!(sa, sb, "same seed must give byte-identical measurements");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn different_seeds_give_different_noise() {
|
||||
let poses = linear_aperture(Point3::new(0.0, 0.0, 0.0), Point3::new(0.3, 0.0, 0.0), 4);
|
||||
let sweep = FrequencySweep::new(2.0e9, 3.0e9, 8);
|
||||
let targets = vec![ScatteringTarget::new(Point3::new(0.15, 1.0, 0.0), 1.0)];
|
||||
let a = simulate_measurement(&poses, &sweep, &targets, 0.05, 1);
|
||||
let b = simulate_measurement(&poses, &sweep, &targets, 0.05, 2);
|
||||
assert_ne!(a.samples, b.samples);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_noise_is_purely_deterministic_physics() {
|
||||
let poses = linear_aperture(Point3::new(0.0, 0.0, 0.0), Point3::new(0.3, 0.0, 0.0), 2);
|
||||
let sweep = FrequencySweep::new(2.0e9, 2.0e9, 1);
|
||||
let targets = vec![ScatteringTarget::new(Point3::new(0.0, 1.0, 0.0), 2.0)];
|
||||
let m = simulate_measurement(&poses, &sweep, &targets, 0.0, 7);
|
||||
// Antenna 0 is directly below the target at range 1.0 m.
|
||||
let r = 1.0_f64;
|
||||
let expected_amp = 2.0 / (r * r);
|
||||
assert!((m.get(0, 0).norm() - expected_amp).abs() < 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_noise_std_gt_zero_produces_nonzero_noise() {
|
||||
let poses = linear_aperture(Point3::new(0.0, 0.0, 0.0), Point3::new(0.3, 0.0, 0.0), 1);
|
||||
let sweep = FrequencySweep::new(2.0e9, 2.0e9, 1);
|
||||
let m = simulate_measurement(&poses, &sweep, &[], 1.0, 5);
|
||||
assert!(m.get(0, 0).norm() > 0.0, "no targets but noise_std>0 must still yield noise");
|
||||
}
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
//! Extract a sparse point cloud from a dense [`ReflectivityImage`].
|
||||
//!
|
||||
//! A `nx * ny * nz` voxel grid is not a useful end product on its own --
|
||||
//! real point-cloud consumers (visualization, `ruview-unified`'s
|
||||
//! `GaussianMap`, downstream fusion) want a short list of "here is
|
||||
//! something" points, not every voxel. This module does simple
|
||||
//! threshold + local-maximum extraction: no clustering, no material
|
||||
//! classification, no confidence calibration against real data (ADR-287
|
||||
//! §5 -- explicitly out of scope for this crate).
|
||||
|
||||
use crate::geometry::Point3;
|
||||
use crate::reconstruct::ReflectivityImage;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// A single detected point: a location and its reconstructed reflectivity
|
||||
/// magnitude (relative, not calibrated to any physical unit).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||
pub struct PointCloudPoint {
|
||||
/// World-space location, meters.
|
||||
pub position: Point3,
|
||||
/// Reconstructed reflectivity magnitude at this voxel.
|
||||
pub magnitude: f64,
|
||||
}
|
||||
|
||||
/// Extract detected points from `image`: every voxel whose magnitude is
|
||||
/// (a) at least `threshold_fraction` of the image's peak magnitude, and
|
||||
/// (b) a local maximum among its 6-connected neighbors (so a single broad
|
||||
/// blob yields one point, not every voxel inside it).
|
||||
///
|
||||
/// `threshold_fraction` must be in `(0.0, 1.0]`. A typical value is
|
||||
/// `0.5` (a classic radar/SAR "half-power point" style threshold).
|
||||
pub fn extract_point_cloud(image: &ReflectivityImage, threshold_fraction: f64) -> Vec<PointCloudPoint> {
|
||||
assert!(
|
||||
threshold_fraction > 0.0 && threshold_fraction <= 1.0,
|
||||
"threshold_fraction must be in (0, 1]"
|
||||
);
|
||||
let grid = &image.grid;
|
||||
let peak = image.magnitude.iter().cloned().fold(0.0_f64, f64::max);
|
||||
if peak <= 0.0 {
|
||||
return Vec::new();
|
||||
}
|
||||
let threshold = peak * threshold_fraction;
|
||||
|
||||
let mag_at = |i: i64, j: i64, k: i64| -> f64 {
|
||||
if i < 0 || j < 0 || k < 0 || i as usize >= grid.nx || j as usize >= grid.ny || k as usize >= grid.nz {
|
||||
return f64::NEG_INFINITY;
|
||||
}
|
||||
let linear = grid.linear_index(i as usize, j as usize, k as usize);
|
||||
image.magnitude[linear]
|
||||
};
|
||||
|
||||
let mut points = Vec::new();
|
||||
for k in 0..grid.nz {
|
||||
for j in 0..grid.ny {
|
||||
for i in 0..grid.nx {
|
||||
let here = mag_at(i as i64, j as i64, k as i64);
|
||||
if here < threshold {
|
||||
continue;
|
||||
}
|
||||
let neighbors = [
|
||||
mag_at(i as i64 - 1, j as i64, k as i64),
|
||||
mag_at(i as i64 + 1, j as i64, k as i64),
|
||||
mag_at(i as i64, j as i64 - 1, k as i64),
|
||||
mag_at(i as i64, j as i64 + 1, k as i64),
|
||||
mag_at(i as i64, j as i64, k as i64 - 1),
|
||||
mag_at(i as i64, j as i64, k as i64 + 1),
|
||||
];
|
||||
if neighbors.iter().all(|&n| here >= n) {
|
||||
points.push(PointCloudPoint {
|
||||
position: grid.voxel_center(i, j, k),
|
||||
magnitude: here,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
points
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::geometry::linear_aperture;
|
||||
use crate::measurement::{simulate_measurement, FrequencySweep, ScatteringTarget};
|
||||
use crate::reconstruct::{backproject, VoxelGrid};
|
||||
|
||||
#[test]
|
||||
fn single_target_yields_a_single_detected_point() {
|
||||
let poses = linear_aperture(Point3::new(-0.5, 0.0, 0.0), Point3::new(0.5, 0.0, 0.0), 21);
|
||||
let sweep = FrequencySweep::new(2.0e9, 6.0e9, 32);
|
||||
let target = ScatteringTarget::new(Point3::new(0.0, 2.0, 0.0), 1.0);
|
||||
let measurement = simulate_measurement(&poses, &sweep, &[target], 0.0, 3);
|
||||
let grid = VoxelGrid::new(Point3::new(-0.5, 1.6, -0.5), 0.05, 21, 17, 21);
|
||||
let image = backproject(&measurement, &poses, &sweep, &grid);
|
||||
|
||||
let points = extract_point_cloud(&image, 0.5);
|
||||
assert!(!points.is_empty(), "must detect at least the true target");
|
||||
let best = points.iter().max_by(|a, b| a.magnitude.partial_cmp(&b.magnitude).unwrap()).unwrap();
|
||||
assert!(best.position.distance(&target.position) < 0.1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_image_yields_no_points() {
|
||||
let grid = VoxelGrid::new(Point3::new(0.0, 0.0, 0.0), 0.1, 3, 3, 3);
|
||||
let image = ReflectivityImage { grid, magnitude: vec![0.0; grid.len()] };
|
||||
assert!(extract_point_cloud(&image, 0.5).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "threshold_fraction")]
|
||||
fn rejects_out_of_range_threshold() {
|
||||
let grid = VoxelGrid::new(Point3::new(0.0, 0.0, 0.0), 0.1, 2, 2, 2);
|
||||
let image = ReflectivityImage { grid, magnitude: vec![1.0; grid.len()] };
|
||||
let _ = extract_point_cloud(&image, 1.5);
|
||||
}
|
||||
}
|
||||
@@ -1,299 +0,0 @@
|
||||
//! Delay-and-sum backprojection reconstruction (ADR-287 §2).
|
||||
//!
|
||||
//! Given a [`crate::measurement::Measurement`] recorded from known antenna
|
||||
//! [`AntennaPose`]s across a known [`FrequencySweep`], reconstruct a 3D
|
||||
//! reflectivity image on a regular voxel grid:
|
||||
//!
|
||||
//! ```text
|
||||
//! I(x) = | (1 / (M*K)) * sum_m sum_k y_{m,k} * R_{m,x}^2 * exp(+i * 4*pi * f_k * R_{m,x} / c) |
|
||||
//! ```
|
||||
//!
|
||||
//! This is the matched-filter / frequency-domain backprojection kernel:
|
||||
//! for a *correct* hypothesis voxel `x` coinciding with a real scatterer,
|
||||
//! every (pose, frequency) term's phase-correction exactly cancels the
|
||||
//! phase the forward model applied in [`crate::measurement`], so the sum
|
||||
//! coheres constructively. For any other voxel the per-term phases are
|
||||
//! effectively uncorrelated across the (pose, frequency) grid and the sum
|
||||
//! averages toward zero. The `R_{m,x}^2` factor undoes the forward
|
||||
//! model's `1/R^2` spreading-loss term (matched-filter gain
|
||||
//! compensation), so voxel brightness reflects relative reflectivity
|
||||
//! rather than falling off with range.
|
||||
|
||||
use crate::geometry::{AntennaPose, Point3};
|
||||
use crate::measurement::{FrequencySweep, Measurement};
|
||||
use num_complex::Complex64;
|
||||
use rayon::prelude::*;
|
||||
use std::f64::consts::PI;
|
||||
|
||||
use crate::resolution::SPEED_OF_LIGHT_M_PER_S;
|
||||
|
||||
/// A regular 3D grid of voxel centers over an axis-aligned box.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct VoxelGrid {
|
||||
/// Grid origin (the center of voxel `(0,0,0)`), meters.
|
||||
pub origin: Point3,
|
||||
/// Voxel edge length along each axis, meters.
|
||||
pub spacing: f64,
|
||||
/// Number of voxels along x.
|
||||
pub nx: usize,
|
||||
/// Number of voxels along y.
|
||||
pub ny: usize,
|
||||
/// Number of voxels along z.
|
||||
pub nz: usize,
|
||||
}
|
||||
|
||||
impl VoxelGrid {
|
||||
/// Construct a grid.
|
||||
pub fn new(origin: Point3, spacing: f64, nx: usize, ny: usize, nz: usize) -> Self {
|
||||
assert!(spacing > 0.0, "voxel spacing must be positive");
|
||||
Self { origin, spacing, nx, ny, nz }
|
||||
}
|
||||
|
||||
/// Total voxel count.
|
||||
pub fn len(&self) -> usize {
|
||||
self.nx * self.ny * self.nz
|
||||
}
|
||||
|
||||
/// True if the grid has zero voxels along any axis.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.len() == 0
|
||||
}
|
||||
|
||||
/// World-space center of voxel `(i, j, k)`.
|
||||
pub fn voxel_center(&self, i: usize, j: usize, k: usize) -> Point3 {
|
||||
Point3::new(
|
||||
self.origin.x + (i as f64) * self.spacing,
|
||||
self.origin.y + (j as f64) * self.spacing,
|
||||
self.origin.z + (k as f64) * self.spacing,
|
||||
)
|
||||
}
|
||||
|
||||
/// Flatten a 3D voxel index into a linear index (row-major, x fastest).
|
||||
pub fn linear_index(&self, i: usize, j: usize, k: usize) -> usize {
|
||||
(k * self.ny + j) * self.nx + i
|
||||
}
|
||||
|
||||
/// Recover the 3D voxel index `(i, j, k)` from a linear index.
|
||||
pub fn unflatten(&self, linear: usize) -> (usize, usize, usize) {
|
||||
let i = linear % self.nx;
|
||||
let j = (linear / self.nx) % self.ny;
|
||||
let k = linear / (self.nx * self.ny);
|
||||
(i, j, k)
|
||||
}
|
||||
}
|
||||
|
||||
/// The reconstructed reflectivity image: one magnitude value per voxel,
|
||||
/// row-major (`grid.linear_index`/`unflatten` order).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ReflectivityImage {
|
||||
/// The grid this image was reconstructed on.
|
||||
pub grid: VoxelGrid,
|
||||
/// Per-voxel reflectivity magnitude, same length as `grid.len()`.
|
||||
pub magnitude: Vec<f64>,
|
||||
}
|
||||
|
||||
impl ReflectivityImage {
|
||||
/// The voxel with the largest magnitude, and its world-space center.
|
||||
pub fn peak(&self) -> (Point3, f64) {
|
||||
let (idx, &mag) = self
|
||||
.magnitude
|
||||
.iter()
|
||||
.enumerate()
|
||||
.max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
|
||||
.expect("grid must have at least one voxel");
|
||||
let (i, j, k) = self.grid.unflatten(idx);
|
||||
(self.grid.voxel_center(i, j, k), mag)
|
||||
}
|
||||
}
|
||||
|
||||
/// Reconstruct a reflectivity image from `measurement`, recorded at
|
||||
/// `poses` across `sweep`, onto `grid`. Parallelized over voxels (rayon).
|
||||
///
|
||||
/// `poses` and `sweep` must describe the *same* geometry the measurement
|
||||
/// was recorded with (or, when studying pose-error sensitivity, a
|
||||
/// deliberately perturbed version of it -- see
|
||||
/// `tests/physics_validation.rs`).
|
||||
pub fn backproject(
|
||||
measurement: &Measurement,
|
||||
poses: &[AntennaPose],
|
||||
sweep: &FrequencySweep,
|
||||
grid: &VoxelGrid,
|
||||
) -> ReflectivityImage {
|
||||
assert_eq!(poses.len(), measurement.n_poses, "pose count must match measurement");
|
||||
assert_eq!(sweep.n_steps, measurement.n_freqs, "frequency count must match measurement");
|
||||
|
||||
let n = grid.len();
|
||||
|
||||
let magnitude: Vec<f64> = (0..n)
|
||||
.into_par_iter()
|
||||
.map(|linear| {
|
||||
let (i, j, k) = grid.unflatten(linear);
|
||||
let voxel = grid.voxel_center(i, j, k);
|
||||
focus_at_point(measurement, poses, sweep, &voxel)
|
||||
})
|
||||
.collect();
|
||||
|
||||
ReflectivityImage { grid: *grid, magnitude }
|
||||
}
|
||||
|
||||
/// Evaluate the coherent backprojection sum at a single world-space
|
||||
/// `point`, without building a grid. This is the same matched-filter
|
||||
/// kernel [`backproject`] evaluates per voxel; exposed directly so callers
|
||||
/// (and tests) can measure focus quality exactly at a location of
|
||||
/// interest -- e.g. a known target position -- rather than only at
|
||||
/// whatever grid points happen to be sampled.
|
||||
///
|
||||
/// Takes `sweep` rather than a raw frequency slice specifically so the
|
||||
/// evenly-spaced-frequencies guarantee ([`FrequencySweep::frequencies`])
|
||||
/// is a type-level invariant, not a caller-observed precondition: the
|
||||
/// implementation below relies on it (see the comment inside the pose
|
||||
/// loop). Passing an arbitrary non-uniform frequency list is not possible
|
||||
/// through this signature.
|
||||
pub fn focus_at_point(measurement: &Measurement, poses: &[AntennaPose], sweep: &FrequencySweep, point: &Point3) -> f64 {
|
||||
assert_eq!(poses.len(), measurement.n_poses, "pose count must match measurement");
|
||||
assert_eq!(sweep.n_steps, measurement.n_freqs, "frequency count must match measurement");
|
||||
|
||||
let n_terms = (measurement.n_poses * measurement.n_freqs) as f64;
|
||||
let k = sweep.n_steps;
|
||||
// Frequencies are evenly spaced by construction: f_kf = start_hz + kf *
|
||||
// delta_f. That makes the per-term phase phase_kf = 4*pi*f_kf*r/c an
|
||||
// arithmetic progression in kf, so instead of K trig evaluations
|
||||
// (Complex64::from_polar per frequency step) the phasor is evaluated
|
||||
// once and advanced by a fixed per-step rotation -- one complex
|
||||
// multiply per step instead of a sin/cos pair. Proven equivalent to
|
||||
// the direct per-frequency computation (independently reimplemented,
|
||||
// not reusing this code) in
|
||||
// `backprojection_incremental_rotation_matches_direct_per_frequency_computation`.
|
||||
let delta_f = if k > 1 { (sweep.stop_hz - sweep.start_hz) / (k - 1) as f64 } else { 0.0 };
|
||||
|
||||
let mut acc = Complex64::new(0.0, 0.0);
|
||||
for (m, pose) in poses.iter().enumerate() {
|
||||
let r = pose.position.distance(point);
|
||||
if r < 1e-6 {
|
||||
continue;
|
||||
}
|
||||
let gain_compensation = r * r;
|
||||
let base_phase = 4.0 * PI * sweep.start_hz * r / SPEED_OF_LIGHT_M_PER_S;
|
||||
let step_phase = 4.0 * PI * delta_f * r / SPEED_OF_LIGHT_M_PER_S;
|
||||
let step = Complex64::from_polar(1.0, step_phase);
|
||||
let mut rot = Complex64::from_polar(1.0, base_phase);
|
||||
for kf in 0..k {
|
||||
acc += measurement.get(m, kf) * gain_compensation * rot;
|
||||
if kf + 1 < k {
|
||||
rot *= step;
|
||||
}
|
||||
}
|
||||
}
|
||||
acc.norm() / n_terms
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::geometry::linear_aperture;
|
||||
use crate::measurement::{simulate_measurement, ScatteringTarget};
|
||||
|
||||
#[test]
|
||||
fn voxel_grid_flatten_unflatten_roundtrip() {
|
||||
let grid = VoxelGrid::new(Point3::new(0.0, 0.0, 0.0), 0.1, 4, 5, 3);
|
||||
for k in 0..grid.nz {
|
||||
for j in 0..grid.ny {
|
||||
for i in 0..grid.nx {
|
||||
let lin = grid.linear_index(i, j, k);
|
||||
assert_eq!(grid.unflatten(lin), (i, j, k));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_point_target_reconstructs_at_its_true_location() {
|
||||
let poses = linear_aperture(Point3::new(-0.5, 0.0, 0.0), Point3::new(0.5, 0.0, 0.0), 21);
|
||||
let sweep = FrequencySweep::new(2.0e9, 6.0e9, 32);
|
||||
let target = ScatteringTarget::new(Point3::new(0.0, 2.0, 0.0), 1.0);
|
||||
let measurement = simulate_measurement(&poses, &sweep, &[target], 0.0, 1);
|
||||
|
||||
let grid = VoxelGrid::new(Point3::new(-0.5, 1.6, -0.5), 0.05, 21, 17, 21);
|
||||
let image = backproject(&measurement, &poses, &sweep, &grid);
|
||||
let (peak_loc, _peak_mag) = image.peak();
|
||||
|
||||
let err = peak_loc.distance(&target.position);
|
||||
assert!(err < 0.1, "reconstructed peak {:?} should be within one voxel-ish of the true target {:?}, err={err}", peak_loc, target.position);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn peak_at_target_is_far_above_background() {
|
||||
let poses = linear_aperture(Point3::new(-0.5, 0.0, 0.0), Point3::new(0.5, 0.0, 0.0), 21);
|
||||
let sweep = FrequencySweep::new(2.0e9, 6.0e9, 32);
|
||||
let target = ScatteringTarget::new(Point3::new(0.0, 2.0, 0.0), 1.0);
|
||||
let measurement = simulate_measurement(&poses, &sweep, &[target], 0.0, 2);
|
||||
|
||||
let grid = VoxelGrid::new(Point3::new(-0.5, 1.6, -0.5), 0.05, 21, 17, 21);
|
||||
let image = backproject(&measurement, &poses, &sweep, &grid);
|
||||
let (_peak_loc, peak_mag) = image.peak();
|
||||
let mean_mag: f64 = image.magnitude.iter().sum::<f64>() / image.magnitude.len() as f64;
|
||||
|
||||
assert!(peak_mag > mean_mag * 5.0, "coherent focus at the target must dominate the incoherent background: peak={peak_mag}, mean={mean_mag}");
|
||||
}
|
||||
|
||||
/// Independent reference: the direct per-frequency computation
|
||||
/// `focus_at_point` used before the incremental-phasor-rotation
|
||||
/// optimization (one `Complex64::from_polar` per (pose, frequency)
|
||||
/// term, no recurrence). Deliberately reimplemented here rather than
|
||||
/// calling any shared helper, so this test cannot pass by construction.
|
||||
fn focus_at_point_direct_reference(
|
||||
measurement: &Measurement,
|
||||
poses: &[AntennaPose],
|
||||
sweep: &FrequencySweep,
|
||||
point: &Point3,
|
||||
) -> f64 {
|
||||
let freqs = sweep.frequencies();
|
||||
let n_terms = (measurement.n_poses * measurement.n_freqs) as f64;
|
||||
let mut acc = Complex64::new(0.0, 0.0);
|
||||
for (m, pose) in poses.iter().enumerate() {
|
||||
let r = pose.position.distance(point);
|
||||
if r < 1e-6 {
|
||||
continue;
|
||||
}
|
||||
let gain_compensation = r * r;
|
||||
for (kf, &f) in freqs.iter().enumerate() {
|
||||
let phase = 4.0 * PI * f * r / SPEED_OF_LIGHT_M_PER_S;
|
||||
acc += measurement.get(m, kf) * gain_compensation * Complex64::from_polar(1.0, phase);
|
||||
}
|
||||
}
|
||||
acc.norm() / n_terms
|
||||
}
|
||||
|
||||
/// PERF PROOF: the incremental-phasor-rotation `focus_at_point` (2
|
||||
/// trig evaluations/pose instead of K) matches the direct
|
||||
/// per-frequency reference to within f64 rounding, across several
|
||||
/// sweep sizes, ranges, and off-axis points (not just the on-target
|
||||
/// case, where errors could cancel).
|
||||
#[test]
|
||||
fn backprojection_incremental_rotation_matches_direct_per_frequency_computation() {
|
||||
let poses = linear_aperture(Point3::new(-0.7, 0.0, 0.0), Point3::new(0.6, 0.1, 0.0), 17);
|
||||
let targets = vec![
|
||||
ScatteringTarget::new(Point3::new(0.1, 2.3, -0.2), 1.0),
|
||||
ScatteringTarget::new(Point3::new(-0.4, 1.9, 0.3), 0.6),
|
||||
];
|
||||
let test_points = [
|
||||
Point3::new(0.1, 2.3, -0.2), // on a target
|
||||
Point3::new(-0.4, 1.9, 0.3), // on the other target
|
||||
Point3::new(0.0, 2.0, 0.0), // off-target
|
||||
Point3::new(-0.55, 2.6, 0.4), // off-target, far corner
|
||||
];
|
||||
for &(n_steps, start_hz, stop_hz) in &[(1usize, 3.0e9, 3.0e9), (2, 2.0e9, 6.0e9), (8, 1.0e9, 9.0e9), (64, 2.4e9, 2.5e9)] {
|
||||
let sweep = FrequencySweep::new(start_hz, stop_hz, n_steps);
|
||||
let measurement = simulate_measurement(&poses, &sweep, &targets, 0.0, 42);
|
||||
for point in test_points {
|
||||
let fast = focus_at_point(&measurement, &poses, &sweep, &point);
|
||||
let reference = focus_at_point_direct_reference(&measurement, &poses, &sweep, &point);
|
||||
let scale = reference.max(1e-12);
|
||||
assert!(
|
||||
(fast - reference).abs() / scale < 1e-9,
|
||||
"incremental rotation diverged from the direct reference at n_steps={n_steps}, point={point:?}: fast={fast}, reference={reference}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
//! Closed-form resolution and coherence-budget formulas (ADR-287 §3).
|
||||
//!
|
||||
//! These are textbook radar-imaging identities (see e.g. Skolnik, *Radar
|
||||
//! Handbook*, and the standard stripmap-SAR cross-range formula). They are
|
||||
//! implemented here so the crate's own reconstruction behavior can be
|
||||
//! checked against them in [`tests/physics_validation.rs`] rather than
|
||||
//! merely asserted in documentation.
|
||||
|
||||
/// Speed of light in vacuum, m/s.
|
||||
pub const SPEED_OF_LIGHT_M_PER_S: f64 = 299_792_458.0;
|
||||
|
||||
/// Wavelength (meters) of a signal at `freq_hz`.
|
||||
pub fn wavelength_m(freq_hz: f64) -> f64 {
|
||||
SPEED_OF_LIGHT_M_PER_S / freq_hz
|
||||
}
|
||||
|
||||
/// Range resolution (meters) of a stepped-frequency / wideband radar with
|
||||
/// total swept bandwidth `bandwidth_hz`: `ΔR = c / (2B)`.
|
||||
///
|
||||
/// This is the Rayleigh-style minimum range separation at which two
|
||||
/// point targets on the same bearing become distinguishable after pulse
|
||||
/// compression / coherent range processing. It does **not** depend on
|
||||
/// carrier frequency, antenna count, or synthetic-aperture length --
|
||||
/// only on how much spectrum was actually swept.
|
||||
pub fn range_resolution_m(bandwidth_hz: f64) -> f64 {
|
||||
SPEED_OF_LIGHT_M_PER_S / (2.0 * bandwidth_hz)
|
||||
}
|
||||
|
||||
/// Cross-range (azimuth) resolution (meters) of a synthetic aperture of
|
||||
/// physical length `aperture_length_m`, imaging a target at `range_m`,
|
||||
/// at carrier frequency `center_freq_hz`: `δ_CR ≈ λ·R / (2·L)`.
|
||||
///
|
||||
/// This is the classic stripmap-SAR angular-resolution identity: doubling
|
||||
/// the aperture (or halving the wavelength) halves the achievable
|
||||
/// cross-range spot size at a fixed range. It is undefined (returns
|
||||
/// `f64::INFINITY`) for a degenerate (zero-length) aperture -- a single
|
||||
/// antenna position carries no cross-range information at all, which is
|
||||
/// exactly the point of building a synthetic aperture in the first place.
|
||||
pub fn cross_range_resolution_m(center_freq_hz: f64, aperture_length_m: f64, range_m: f64) -> f64 {
|
||||
if aperture_length_m <= 0.0 {
|
||||
return f64::INFINITY;
|
||||
}
|
||||
wavelength_m(center_freq_hz) * range_m / (2.0 * aperture_length_m)
|
||||
}
|
||||
|
||||
/// Maximum antenna-position error (meters) that keeps a coherent
|
||||
/// (phase-focused) reconstruction inside the classical quarter-wave
|
||||
/// budget, at carrier frequency `center_freq_hz`.
|
||||
///
|
||||
/// Derivation: moving an antenna's phase center by `Δp` while looking
|
||||
/// (worst case) directly along boresight at the target changes the
|
||||
/// round-trip path length by up to `2·Δp` (both the outbound and return
|
||||
/// leg shift by `Δp`). Keeping that two-way path error under the
|
||||
/// standard quarter-wavelength coherence budget (`λ/4` -- the same
|
||||
/// criterion used for reflector-antenna and optical-surface tolerancing)
|
||||
/// requires `2·Δp ≤ λ/4`, i.e. `Δp ≤ λ/8`.
|
||||
///
|
||||
/// Position error beyond this does not make reconstruction impossible --
|
||||
/// it degrades the coherent sum smoothly (see
|
||||
/// `phase_error_degrades_focus_beyond_pose_budget` in
|
||||
/// `tests/physics_validation.rs`) -- but it is the standard rule-of-thumb
|
||||
/// budget for "still well focused."
|
||||
pub fn max_coherent_pose_error_m(center_freq_hz: f64) -> f64 {
|
||||
wavelength_m(center_freq_hz) / 8.0
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn range_resolution_matches_hand_computed_value() {
|
||||
// 3 GHz of swept bandwidth: c/(2*3e9) = 4.9965...cm.
|
||||
let r = range_resolution_m(3.0e9);
|
||||
assert!((r - 0.049_965_409_666_666_66).abs() < 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eight_ghz_pose_budget_is_about_5mm() {
|
||||
// At 8 GHz, lambda = c/f ~= 37.47mm, so lambda/8 ~= 4.68mm -- close
|
||||
// to the ~5mm rule-of-thumb quoted in the motivating design note.
|
||||
let budget = max_coherent_pose_error_m(8.0e9);
|
||||
assert!((budget - wavelength_m(8.0e9) / 8.0).abs() < 1e-12);
|
||||
assert!(budget < 0.005 && budget > 0.004);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cross_range_resolution_improves_with_longer_aperture() {
|
||||
let short = cross_range_resolution_m(5.0e9, 0.1, 2.0);
|
||||
let long = cross_range_resolution_m(5.0e9, 1.0, 2.0);
|
||||
assert!(long < short, "10x longer aperture must give finer cross-range resolution");
|
||||
// Exactly linear in 1/L.
|
||||
assert!((short / long - 10.0).abs() < 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_length_aperture_has_no_cross_range_resolution() {
|
||||
assert_eq!(cross_range_resolution_m(5.0e9, 0.0, 2.0), f64::INFINITY);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wider_bandwidth_gives_finer_range_resolution() {
|
||||
assert!(range_resolution_m(4.0e9) < range_resolution_m(1.0e9));
|
||||
}
|
||||
}
|
||||
@@ -1,215 +0,0 @@
|
||||
//! Checks the reconstruction's *actual* behavior against the closed-form
|
||||
//! predictions in `wifi_densepose_sar::resolution`, rather than merely
|
||||
//! asserting the formulas in documentation (ADR-287 §3, the
|
||||
//! ruview-unified "proven, not asserted" discipline).
|
||||
//!
|
||||
//! Three physical claims are validated end-to-end (forward-simulate ->
|
||||
//! backproject -> measure the reconstruction's behavior):
|
||||
//!
|
||||
//! 1. Two point targets separated along *range* resolve into two distinct
|
||||
//! peaks only once their separation exceeds `range_resolution_m`.
|
||||
//! 2. Two point targets separated along *cross-range* (same range,
|
||||
//! different bearing) resolve only once the synthetic-aperture length
|
||||
//! is long enough per `cross_range_resolution_m` -- a single short
|
||||
//! aperture cannot resolve them no matter how much bandwidth is used.
|
||||
//! 3. Antenna-position error decoheres the reconstruction: coherent focus
|
||||
//! at the true target location trends downward as position error
|
||||
//! grows past the `max_coherent_pose_error_m` (`λ/8`) budget, and has
|
||||
//! collapsed toward the incoherent background by an order of
|
||||
//! magnitude beyond it.
|
||||
|
||||
use wifi_densepose_sar::geometry::linear_aperture;
|
||||
use wifi_densepose_sar::measurement::{simulate_measurement, FrequencySweep, ScatteringTarget};
|
||||
use wifi_densepose_sar::reconstruct::{backproject, focus_at_point, VoxelGrid};
|
||||
use wifi_densepose_sar::resolution::{
|
||||
cross_range_resolution_m, max_coherent_pose_error_m, range_resolution_m, wavelength_m,
|
||||
};
|
||||
use wifi_densepose_sar::{AntennaPose, Point3};
|
||||
|
||||
/// Count local maxima at or above `threshold_fraction` of the profile's
|
||||
/// peak, in a 1D magnitude profile. Adjacent samples above threshold count
|
||||
/// as one maximum (a plateau/peak region), not one-per-sample.
|
||||
fn count_resolved_peaks(profile: &[f64], threshold_fraction: f64) -> usize {
|
||||
let peak = profile.iter().cloned().fold(0.0_f64, f64::max);
|
||||
let threshold = peak * threshold_fraction;
|
||||
let mut count = 0;
|
||||
let mut in_peak = false;
|
||||
for &v in profile {
|
||||
if v >= threshold {
|
||||
if !in_peak {
|
||||
count += 1;
|
||||
in_peak = true;
|
||||
}
|
||||
} else {
|
||||
in_peak = false;
|
||||
}
|
||||
}
|
||||
count
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn range_separated_targets_resolve_only_beyond_range_resolution() {
|
||||
let poses = linear_aperture(Point3::new(-0.5, 0.0, 0.0), Point3::new(0.5, 0.0, 0.0), 21);
|
||||
let sweep = FrequencySweep::new(2.0e9, 6.0e9, 64); // 4 GHz bandwidth
|
||||
let dr = range_resolution_m(sweep.bandwidth_hz());
|
||||
|
||||
// A 1D range profile: fixed cross-range (x=0, z=0), fine steps in y.
|
||||
let profile_grid = |center_y: f64, half_span: f64| {
|
||||
VoxelGrid::new(Point3::new(0.0, center_y - half_span, 0.0), dr / 6.0, 1, (2.0 * half_span / (dr / 6.0)) as usize, 1)
|
||||
};
|
||||
|
||||
// Case A: well separated (4x the theoretical resolution) -> two peaks.
|
||||
let sep_resolved = 4.0 * dr;
|
||||
let targets_a = vec![
|
||||
ScatteringTarget::new(Point3::new(0.0, 2.0 - sep_resolved / 2.0, 0.0), 1.0),
|
||||
ScatteringTarget::new(Point3::new(0.0, 2.0 + sep_resolved / 2.0, 0.0), 1.0),
|
||||
];
|
||||
let meas_a = simulate_measurement(&poses, &sweep, &targets_a, 0.0, 10);
|
||||
let grid_a = profile_grid(2.0, sep_resolved * 1.5);
|
||||
let image_a = backproject(&meas_a, &poses, &sweep, &grid_a);
|
||||
let peaks_a = count_resolved_peaks(&image_a.magnitude, 0.7);
|
||||
assert_eq!(
|
||||
peaks_a, 2,
|
||||
"targets separated by 4x the range resolution ({sep_resolved:.4} m vs dr={dr:.4} m) must resolve into 2 peaks, got {peaks_a}"
|
||||
);
|
||||
|
||||
// Case B: too close (0.25x the theoretical resolution) -> one merged peak.
|
||||
let sep_unresolved = 0.25 * dr;
|
||||
let targets_b = vec![
|
||||
ScatteringTarget::new(Point3::new(0.0, 2.0 - sep_unresolved / 2.0, 0.0), 1.0),
|
||||
ScatteringTarget::new(Point3::new(0.0, 2.0 + sep_unresolved / 2.0, 0.0), 1.0),
|
||||
];
|
||||
let meas_b = simulate_measurement(&poses, &sweep, &targets_b, 0.0, 11);
|
||||
let grid_b = profile_grid(2.0, dr * 2.0);
|
||||
let image_b = backproject(&meas_b, &poses, &sweep, &grid_b);
|
||||
let peaks_b = count_resolved_peaks(&image_b.magnitude, 0.7);
|
||||
assert_eq!(
|
||||
peaks_b, 1,
|
||||
"targets separated by only 0.25x the range resolution must merge into 1 peak, got {peaks_b}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cross_range_separated_targets_resolve_only_with_long_enough_aperture() {
|
||||
let sweep = FrequencySweep::new(3.0e9, 5.0e9, 32); // center 4 GHz
|
||||
let range_m = 2.0;
|
||||
let lambda = wavelength_m(sweep.center_freq_hz());
|
||||
|
||||
let long_aperture_len = 1.0;
|
||||
let short_aperture_len = 0.05;
|
||||
let res_long = cross_range_resolution_m(sweep.center_freq_hz(), long_aperture_len, range_m);
|
||||
let res_short = cross_range_resolution_m(sweep.center_freq_hz(), short_aperture_len, range_m);
|
||||
assert!(res_long < res_short, "a longer aperture must predict finer cross-range resolution");
|
||||
|
||||
// Pick a separation that is resolvable with the long aperture (well
|
||||
// above its predicted resolution) but not with the short one (well
|
||||
// below its much coarser predicted resolution).
|
||||
let separation = 5.0 * res_long;
|
||||
assert!(separation < res_short, "test setup: separation must sit inside the short aperture's blind spot (lambda={lambda:.4})");
|
||||
|
||||
let targets = vec![
|
||||
ScatteringTarget::new(Point3::new(-separation / 2.0, range_m, 0.0), 1.0),
|
||||
ScatteringTarget::new(Point3::new(separation / 2.0, range_m, 0.0), 1.0),
|
||||
];
|
||||
|
||||
let half_span = separation * 1.5;
|
||||
let cross_range_grid = || VoxelGrid::new(Point3::new(-half_span, range_m, 0.0), separation / 20.0, (2.0 * half_span / (separation / 20.0)) as usize, 1, 1);
|
||||
|
||||
// Long aperture: must resolve into two peaks.
|
||||
let poses_long = linear_aperture(
|
||||
Point3::new(-long_aperture_len / 2.0, 0.0, 0.0),
|
||||
Point3::new(long_aperture_len / 2.0, 0.0, 0.0),
|
||||
41,
|
||||
);
|
||||
let meas_long = simulate_measurement(&poses_long, &sweep, &targets, 0.0, 20);
|
||||
let grid_long = cross_range_grid();
|
||||
let image_long = backproject(&meas_long, &poses_long, &sweep, &grid_long);
|
||||
let peaks_long = count_resolved_peaks(&image_long.magnitude, 0.7);
|
||||
assert_eq!(peaks_long, 2, "a {long_aperture_len} m synthetic aperture must resolve cross-range-separated targets {separation:.4} m apart, got {peaks_long} peak(s)");
|
||||
|
||||
// Short aperture: must NOT resolve (collapses to one blob/ridge).
|
||||
let poses_short = linear_aperture(
|
||||
Point3::new(-short_aperture_len / 2.0, 0.0, 0.0),
|
||||
Point3::new(short_aperture_len / 2.0, 0.0, 0.0),
|
||||
41,
|
||||
);
|
||||
let meas_short = simulate_measurement(&poses_short, &sweep, &targets, 0.0, 21);
|
||||
let grid_short = cross_range_grid();
|
||||
let image_short = backproject(&meas_short, &poses_short, &sweep, &grid_short);
|
||||
let peaks_short = count_resolved_peaks(&image_short.magnitude, 0.7);
|
||||
assert_eq!(peaks_short, 1, "a {short_aperture_len} m synthetic aperture (far below the required {res_short:.4} m cross-range resolution) must NOT resolve the same targets, got {peaks_short} peak(s)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn phase_error_from_pose_jitter_degrades_focus_beyond_pose_budget() {
|
||||
use rand::{Rng, SeedableRng};
|
||||
use rand_chacha::ChaCha20Rng;
|
||||
|
||||
let center_freq = 4.0e9;
|
||||
let sweep = FrequencySweep::new(3.0e9, 5.0e9, 32);
|
||||
let target = ScatteringTarget::new(Point3::new(0.0, 2.0, 0.0), 1.0);
|
||||
let nominal_poses = linear_aperture(Point3::new(-0.5, 0.0, 0.0), Point3::new(0.5, 0.0, 0.0), 21);
|
||||
let budget = max_coherent_pose_error_m(center_freq);
|
||||
let lambda = wavelength_m(center_freq);
|
||||
|
||||
// A single FIXED per-position error *direction* (random sign along
|
||||
// each antenna's boresight to the target, drawn once), then scaled by
|
||||
// a growing `epsilon`. This isolates "how does focus respond as
|
||||
// position-error magnitude grows" from "which specific random error
|
||||
// pattern did we happen to draw" -- redrawing a fresh random pattern
|
||||
// at every epsilon level (tried first) makes neighboring levels
|
||||
// statistically incomparable and the trend noisy enough to need heavy
|
||||
// Monte Carlo averaging. Levels are kept within half a wavelength
|
||||
// (4x budget = lambda/2): beyond that, per-position phase error wraps
|
||||
// past 2*pi and can partially and coincidentally realign at specific
|
||||
// epsilon values (a real grating/aliasing effect, not a test bug) --
|
||||
// an honest reason to keep this test inside the regime the lambda/8
|
||||
// budget is actually about, rather than claiming a monotonic trend
|
||||
// the underlying physics doesn't guarantee once error exceeds ~lambda.
|
||||
let mut sign_rng = ChaCha20Rng::seed_from_u64(99);
|
||||
let signs: Vec<f64> = nominal_poses.iter().map(|_| if sign_rng.gen_bool(0.5) { 1.0 } else { -1.0 }).collect();
|
||||
let jittered_poses_at = |epsilon: f64| -> Vec<AntennaPose> {
|
||||
nominal_poses
|
||||
.iter()
|
||||
.zip(&signs)
|
||||
.map(|(p, &sign)| {
|
||||
let dir = p.position.direction_to(&target.position).expect("pose must not coincide with target");
|
||||
AntennaPose::new(p.position.translated(dir, sign * epsilon))
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
|
||||
let focus_at_epsilon = |epsilon: f64| -> f64 {
|
||||
let true_poses = jittered_poses_at(epsilon);
|
||||
// The measurement is recorded at the (jittered) TRUE antenna
|
||||
// positions, but reconstruction always assumes the NOMINAL
|
||||
// (design) positions -- the real-world scenario of an
|
||||
// uncalibrated / imperfectly tracked antenna trajectory.
|
||||
// Evaluate focus exactly AT the true target location (not a
|
||||
// grid-wide peak search, which can hop to a nearby voxel that
|
||||
// happens to focus slightly better and mask the coherence loss
|
||||
// this test is measuring).
|
||||
let measurement = simulate_measurement(&true_poses, &sweep, &[target], 0.0, 1);
|
||||
focus_at_point(&measurement, &nominal_poses, &sweep, &target.position)
|
||||
};
|
||||
|
||||
let levels = [0.0, 0.5 * budget, budget, 2.0 * budget, 4.0 * budget];
|
||||
assert!(4.0 * budget < lambda / 2.0 + 1e-12, "test setup: must stay within half a wavelength to avoid phase-wrap aliasing");
|
||||
let focus: Vec<f64> = levels.iter().map(|&eps| focus_at_epsilon(eps)).collect();
|
||||
|
||||
assert!(
|
||||
focus[0] == focus.iter().cloned().fold(0.0, f64::max),
|
||||
"perfect pose knowledge (zero jitter) must give the best focus of the sweep: {focus:?} at levels {levels:?}"
|
||||
);
|
||||
assert!(
|
||||
focus[4] < 0.85 * focus[0],
|
||||
"position error at 4x the lambda/8 budget ({:.4} m, still under half a wavelength) must measurably degrade focus: {:.4} vs zero-jitter {:.4}",
|
||||
4.0 * budget,
|
||||
focus[4],
|
||||
focus[0]
|
||||
);
|
||||
assert!(
|
||||
focus[4] <= focus[1] + 1e-9,
|
||||
"focus at 4x the budget should be no better than focus at 0.5x the budget: {focus:?} at levels {levels:?}"
|
||||
);
|
||||
}
|
||||
@@ -209,18 +209,7 @@ pub fn safetensors_to_rvf(data: &[u8], model_id: &str) -> Result<Vec<u8>, ModelL
|
||||
.filter(|&e| e <= data.len())
|
||||
.ok_or_else(|| fail(format!("declared header length {header_len} exceeds file size")))?;
|
||||
|
||||
// The reference safetensors format pads the header to an 8-byte boundary
|
||||
// with NUL bytes after the closing brace; a strict single-shot parse over
|
||||
// the full declared-length slice rejects that padding. Trim trailing NUL
|
||||
// (and whitespace) before parsing — every published HF safetensors file
|
||||
// exercises this padding.
|
||||
let raw_header = &data[header_start..header_end];
|
||||
let trimmed_end = raw_header
|
||||
.iter()
|
||||
.rposition(|&b| b != 0 && !b.is_ascii_whitespace())
|
||||
.map(|i| i + 1)
|
||||
.unwrap_or(0);
|
||||
let header: serde_json::Value = serde_json::from_slice(&raw_header[..trimmed_end])
|
||||
let header: serde_json::Value = serde_json::from_slice(&data[header_start..header_end])
|
||||
.map_err(|e| fail(format!("safetensors header is not valid JSON: {e}")))?;
|
||||
let obj = header
|
||||
.as_object()
|
||||
@@ -438,43 +427,6 @@ mod tests {
|
||||
assert_eq!(detect_format(&rvfs, "model.rvf"), ModelFormat::Rvf);
|
||||
}
|
||||
|
||||
/// Build a safetensors buffer with the header padded to an 8-byte boundary
|
||||
/// with NUL bytes, matching the reference format and the published
|
||||
/// `model.safetensors` (issue #1480).
|
||||
fn make_safetensors_nul_padded(weights: &[f32]) -> Vec<u8> {
|
||||
let n = weights.len();
|
||||
let header = serde_json::json!({
|
||||
"weight": {
|
||||
"dtype": "F32",
|
||||
"shape": [n],
|
||||
"data_offsets": [0, n * 4],
|
||||
}
|
||||
});
|
||||
let mut header_bytes = serde_json::to_vec(&header).unwrap();
|
||||
let padded_len = (header_bytes.len() + 7) & !7;
|
||||
header_bytes.resize(padded_len, 0);
|
||||
let mut out = Vec::new();
|
||||
out.extend_from_slice(&(header_bytes.len() as u64).to_le_bytes());
|
||||
out.extend_from_slice(&header_bytes);
|
||||
for &w in weights {
|
||||
out.extend_from_slice(&w.to_le_bytes());
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// REGRESSION #1480: a NUL-padded header (the reference safetensors format,
|
||||
/// and what the published `model.safetensors` actually ships) must convert,
|
||||
/// not fail with "safetensors header is not valid JSON: trailing characters".
|
||||
#[test]
|
||||
fn safetensors_nul_padded_header_converts() {
|
||||
let st = make_safetensors_nul_padded(&[1.0, 2.0, 3.0, 4.0, 5.0]);
|
||||
let rvf = safetensors_to_rvf(&st, "wifi-densepose-pretrained")
|
||||
.expect("NUL-padded header must convert");
|
||||
let mut loader = ProgressiveLoader::new(&rvf).expect("converted RVF must load");
|
||||
let lc = loader.load_layer_c().expect("Layer C");
|
||||
assert_eq!(lc.all_weights, vec![1.0, 2.0, 3.0, 4.0, 5.0]);
|
||||
}
|
||||
|
||||
/// CORE #894 PROOF: the published safetensors converts to a container the
|
||||
/// ProgressiveLoader loads (Layer A succeeds, weights present) — the old
|
||||
/// path returned the opaque "invalid magic … 0x77455735" and gave up.
|
||||
|
||||
Vendored
-1
Submodule vendor/metaharness deleted from 458bb6142e
Reference in New Issue
Block a user