mirror of
https://github.com/ruvnet/RuView
synced 2026-07-20 17:03:24 +00:00
0bffe27288
* docs(adr-117): seed branch — ADR-117 pip-modernization spec + soul-signature research bundle
Two artifacts landing together on this new branch as the prerequisite
documentation for the v2.0.0 Python wheel modernization work:
1. **docs/adr/ADR-117-pip-wifi-densepose-modernization.md** (644 lines)
— Plan to bring the 2025-published `wifi-densepose` PyPI package
(last release v1.1.0, 2025-06-07, 11.5 months out of sync) up to
the current Rust v2/ workspace SOTA. Recommends PyO3 + maturin
with abi3-py310 (one binary covers Python 3.10–3.13 per OS/arch),
first-wheel scope = core + vitals + signal crates (~5 MB), v1.99.0
tombstone + 90-day un-yank window for v1.1.0, v2.0.0 hard break.
Open questions catalogued; phases P1–P6+ laid out with concrete
acceptance criteria.
2. **docs/research/soul/** (5 files, ~1,450 lines) — Soul Signature
research spec: 7-channel electromagnetic biometric fingerprint
(AETHER 128-dim + cardiac HR/HRV + cardiac waveform morphology +
respiratory pattern + gait timing + skeletal proportions +
subcarrier reflection profile), fused into one RVF graph file.
Includes 60s scanning protocol, 5-layer security model,
threat-model + mitigations, references to existing ADRs (014,
021, 024, 027, 030, 039, 079, 106, 108, 109, 110, 115). Marked
"Research Specification (Pre-Implementation)". Explicit "what
this is NOT" disclaimers preempt pseudoscience drift; every
discriminative-power claim either cites a measurement or is
marked "open research; baseline TBD".
Branch off main at HEAD; ready for /loop 10m implementation
iterations.
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(adr-117/p1): scaffold python/ workspace — PyO3 + maturin + smoke tests (refs #785)
ADR-117 P1 — the python/ directory is now a working maturin-buildable
crate that produces the v2.x replacement for the legacy pure-Python
wifi-densepose==1.1.0 PyPI wheel.
## What lands
- `python/Cargo.toml` — PyO3 0.22 with `extension-module` + `abi3-py310`
(one binary covers Python 3.10–3.13 per OS/arch — keeps the
cibuildwheel matrix to 5 wheels per release, not 20). Depends on
`wifi-densepose-core` from the existing v2/ workspace via relative
path.
- `python/pyproject.toml` — maturin>=1.7 build backend with
`python-source = "python"` and `module-name = "wifi_densepose._native"`
so the compiled module loads as an internal underscore-private
submodule of the user-facing `wifi_densepose` package. PEP 621
metadata + classifiers + project URLs. Optional-deps:
`wifi-densepose[client]` for the P4 WS/MQTT pure-Python layer,
`wifi-densepose[dev]` for the test toolchain (pytest, ruff, mypy).
- `python/src/lib.rs` — minimal `#[pymodule] wifi_densepose_native`
exporting `__rust_version__`, `__rust_build_tag__`,
`__build_features__`, and a `hello()` smoke function. P2 will land
the core type bindings here.
- `python/wifi_densepose/__init__.py` — pure-Python facade re-exporting
the compiled module's symbols under their stable user-facing names.
Docstring teaches the v1→v2 migration story up-front.
- `python/wifi_densepose/py.typed` — PEP 561 marker so `mypy --strict`
in user code treats the wheel as fully typed (real stubs land in P2).
- `python/tests/test_smoke.py` — 6 P1 acceptance tests:
1. package imports without error
2. version string is PEP 440-compliant
3. `__rust_version__` is reachable from Python (the diagnostic
surface ADR-117 §5.2 promised)
4. `__build_features__` lists `p1-scaffold` marker
5. `wifi_densepose.hello()` returns "ok" (FFI round-trip)
6. `wifi_densepose._native` is reachable but the leading underscore
conveys "private; users should import the parent package"
- `python/README.md` — phase ledger, local build instructions
(`maturin develop`), layout diagram.
## What's deferred to P2+
- Core type bindings (`CsiFrame`, `Keypoint`, `PoseEstimate`) — P2
- Vitals + signal DSP bindings + witness v2 — P3
- Pure-Python WS/MQTT client layer (`wifi_densepose[client]`) — P4
- cibuildwheel + PyPI publish — P5
- v1.99.0 tombstone — concurrent with P5
The new `python/` crate is intentionally OUTSIDE the v2/ Cargo
workspace — it has its own Cargo.toml with `[package]` not
`[workspace.package]` inheritance — to keep maturin's `python-source`
+ `module-name` config self-contained and to avoid forcing every
`cargo test --workspace` invocation in v2/ to compile pyo3.
Refs ADR-117 §5 (Detailed design) and §6 (Phased migration).
Refs #785 (tracking issue).
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(adr-117/p1): standalone Cargo.toml + python-source=. + #[pyo3(name=_native)] (P1 GREEN)
Three fixes to make maturin develop actually work locally:
1. `python/Cargo.toml` removed `*.workspace = true` inheritance —
the python/ crate is intentionally outside the v2/ workspace
(ADR-117 §5.2) so it needs every `[package]` field local.
2. `python/pyproject.toml` `python-source = "python"` was wrong
because pyproject.toml lives at python/ — maturin was looking for
python/python/. Changed to `python-source = "."` so the
`wifi_densepose/` package directory sibling-to-pyproject is found.
3. `python/src/lib.rs` `#[pymodule] fn wifi_densepose_native` →
`#[pymodule] #[pyo3(name = "_native")] fn wifi_densepose_native`.
PyO3 generates `PyInit__native` from the pyo3-name attribute, which
must match the `module-name` in pyproject.toml's [tool.maturin]
block ("wifi_densepose._native"). Without this attribute the wheel
builds but `import wifi_densepose._native` fails with
ModuleNotFoundError.
## Local validation (P1 acceptance gate)
```
$ python -m venv .venv && .venv/Scripts/python -m pip install maturin pytest
$ VIRTUAL_ENV=… maturin develop --release
…
Finished `release` profile [optimized] target(s)
📦 Built wheel for abi3 Python ≥ 3.10
🛠 Installed wifi-densepose-2.0.0a1
$ .venv/Scripts/python -c 'import wifi_densepose; print(wifi_densepose.__version__, wifi_densepose.__rust_version__, wifi_densepose.hello())'
2.0.0a1 2.0.0-alpha.1 ok
$ .venv/Scripts/python -m pytest tests/ -v
tests/test_smoke.py::test_package_imports PASSED
tests/test_smoke.py::test_version_string_well_formed PASSED
tests/test_smoke.py::test_rust_version_surfaced PASSED
tests/test_smoke.py::test_build_features_listed PASSED
tests/test_smoke.py::test_hello_returns_ok PASSED
tests/test_smoke.py::test_native_module_private PASSED
======================== 6 passed in 0.05s =========================
```
P1 closed. Moving to P2 (core type bindings).
Refs #785, ADR-117 §6.
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(adr-117/p2): Keypoint + KeypointType bindings — 23 new tests (29/29 GREEN)
Lands the first chunk of P2: PyO3 bindings for `Keypoint` and
`KeypointType` from `wifi_densepose_core`. Bound types surface to
Python as `wifi_densepose.Keypoint` / `wifi_densepose.KeypointType`.
## Design choices that affect the API surface
1. **`Confidence` is NOT bound as a separate class.** Users hate
wrapping a float in a constructor. Python-side, confidence is just
a `float in [0.0, 1.0]`; the binding validates on construction
(`ValueError` for out-of-range, matching the Rust core error).
2. **`KeypointType` is a `#[pyclass(eq, eq_int, hash, frozen)]` enum**
— hashable so users can drop it into dicts/sets (the most common
pattern in pose-analysis notebooks: `keypoints_by_type[k.type] = k`).
3. **`Keypoint.__init__` keyword-only `z`** so 2D users don't have to
write `None` and 3D users get a clear named arg:
`Keypoint(KeypointType.LeftWrist, 0.2, 0.4, 0.8, z=0.1)`.
4. **`Keypoint` is `#[pyclass(frozen)]`** — no in-place mutation. The
Rust core type is immutable through Copy + Hash + Eq, and exposing
setters from Python would create a copy-vs-reference inconsistency
between languages.
## Files
- `python/src/bindings/keypoint.rs` — 220 lines of `#[pymethods]`
wrappers + Rust↔Python enum round-trip
- `python/src/lib.rs` — `mod bindings { pub mod keypoint; }` +
`bindings::keypoint::register(m)?` call from `#[pymodule]`
- `python/wifi_densepose/__init__.py` — re-exports `Keypoint` and
`KeypointType` at the package root
- `python/tests/test_keypoint.py` — 23 tests covering:
- 17-element COCO ordering of `KeypointType.all()`
- index→type mapping for every variant
- snake_name matches COCO spec
- `is_face()` / `is_upper_body()` predicates
- hashability (the bug I caught when I added the set-based face
test — fixed by adding `hash` to the `#[pyclass]` attribute)
- 2D + 3D constructor variants
- position_2d / position_3d tuples
- is_visible threshold
- confidence validation (Err on out-of-range)
- distance_to (2D Euclidean, 3D Euclidean, fallback when one is 2D
and the other is 3D)
- __repr__ + __eq__
- the new `p2-keypoint-bindings` feature marker landed
## Local validation
\`\`\`
$ cd python && .venv/Scripts/python -m pytest tests/ -v
tests/test_smoke.py::test_package_imports PASSED
tests/test_smoke.py::test_version_string_well_formed PASSED
tests/test_smoke.py::test_rust_version_surfaced PASSED
tests/test_smoke.py::test_build_features_listed PASSED
tests/test_smoke.py::test_hello_returns_ok PASSED
tests/test_smoke.py::test_native_module_private PASSED
tests/test_keypoint.py::test_keypoint_type_all_returns_17 PASSED
…
======================== 29 passed in 0.06s =========================
\`\`\`
Wheel size after both bindings: still well under the 5 MB ADR §5.4
budget (release build with --strip on Windows: ~340 KB).
Also adds `python/.gitignore` to prevent the `.venv/` + `target/` +
`_native.abi3.pyd` artifacts from getting committed.
## What's left in P2
CsiFrame + PoseEstimate bindings land in the next iteration. They're
larger (CsiFrame has the subcarrier buffer; PoseEstimate has
17×Keypoint + BoundingBox + track_id + score). Pattern is now proven
so they go faster.
Refs #785, ADR-117 §6.
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(adr-117/p2): BoundingBox + PersonPose + PoseEstimate — P2 COMPLETE (57/57 tests GREEN)
Lands the second + third chunks of P2: PyO3 bindings for `BoundingBox`,
`PersonPose`, `PoseEstimate` from `wifi_densepose_core`. Combined with
the prior Keypoint + KeypointType bindings (fd0568caa), this closes
ADR-117 §6 P2.
## Coverage
| Type | Bound | Tests | Mutability |
|---|---|---|---|
| Confidence | exposed as `float` with validation | (covered in keypoint tests) | n/a |
| KeypointType | `#[pyclass(eq, eq_int, hash, frozen)]` | 7 tests | immutable |
| Keypoint | `#[pyclass(frozen)]` | 16 tests | immutable |
| BoundingBox | `#[pyclass(frozen)]` | 8 tests | immutable |
| PersonPose | `#[pyclass]` (mutable, builder-style) | 12 tests | mutable |
| PoseEstimate | `#[pyclass(frozen)]` | 8 tests | immutable |
Smoke (P1) + new tests: **57/57 PASS** locally on Windows.
## What's deferred to P3
CsiFrame intentionally NOT bound in P2 because it uses
`Array2<Complex64>` (ndarray) — the natural Python surface is via the
`numpy` pyo3 bridge, which lands in P3 alongside the vitals + signal
DSP bindings. Binding CsiFrame without numpy interop would force
users to materialise lists of tuples which is a worse API than
`csi_frame.amplitude_array()` returning an ndarray.
## Design choices that affect the API surface
1. **PersonPose.keypoints() returns a dict keyed by KeypointType**
instead of a fixed-length list with None slots. Pythonistas don't
want to know the underlying storage is `[Option<Keypoint>; 17]`.
2. **PoseEstimate.id and .timestamp exposed as strings** (UUID + ISO)
rather than as bound `FrameId` / `Timestamp` types. Users in
notebooks rarely compare UUIDs structurally; strings are good
enough for diagnostics and don't bloat the bindings.
3. **PersonPose is MUTABLE** (`#[pyclass]` without `frozen`) so users
can build poses incrementally with `set_keypoint`/`set_bbox`/
`set_id`. PoseEstimate is `frozen` because once constructed it
represents a snapshot.
## Three PyO3 0.22 gotchas surfaced this iteration
1. `#[pymethods]` getters are NOT accessible from other Rust modules
— need a separate `impl PyKeypoint { pub(crate) fn inner(&self)
-> &Keypoint { ... } }` block for cross-module use.
2. `PyDict::new(py)` was removed in PyO3 0.21 → 0.22 in favour of
`PyDict::new_bound(py)`. (Confusing because `Bound<'py, PyDict>`
is the return type either way.)
3. `dict.set_item(K, V)` requires both K and V to impl
`ToPyObject`. `#[pyclass]` types impl `IntoPy<PyObject>` but NOT
`ToPyObject` — workaround: convert via `.into_py(py)` first, then
`set_item(py_object_k, py_object_v)`.
Saved as PyO3 0.22 binding patterns memory at the horizon-tracker
level so future loop workers don't re-learn them.
## Local validation
\`\`\`
$ cd python && .venv/Scripts/python -m pytest tests/ -v
…
======================== 57 passed in 0.24s =========================
\`\`\`
Wheel size: still ~340 KB on Windows release build.
Refs #785, ADR-117 §6 (P2 done — ready for P3 vitals + signal DSP +
numpy bridge + witness v2).
Co-Authored-By: claude-flow <ruv@ruv.net>
* docs(adr-117): add BFLD support (§5.7a + P3.5 phase + §11.11/12 open questions)
Per maintainer feedback during P3 implementation, expand ADR-117 to
include Beamforming Feedback Loop Data (BFLD) as a first-class binding
target alongside CSI. BFLD is the transmitter-side, AP-station-loop
view of the WiFi channel (802.11ac/ax/be compressed beamforming feedback
frames) — complementary to receiver-side CSI, with three properties
that make it strategically important for the pip wheel:
1. **Up to 996 subcarriers per HE160 frame** (vs 242 for HE-LTF CSI on
ESP32-C6, vs 52 for HT-LTF on ESP32-S3) — much denser per-subcarrier
reflection profile
2. **Works on stock 802.11ac+ hardware** — no Nexmon patch, no ESP32
monitor mode, no firmware drift. Captured via tcpdump/Wireshark +
BFR dissector, or via `mac80211` debugfs on Linux 6.10+
3. **Direct input for the soul-signature spec** (`docs/research/soul/`)
— the seven-channel biometric needs dense subcarrier reflection;
BFLD provides it without specialized hardware
## Three additions to ADR-117
### §5.7a — New binding-target subsection
Comparison table CSI vs BFLD; binding strategy with forward-compat
stub Rust impl pending the future `wifi-densepose-bfld` crate; the
three Python types that ship in P3.5:
- `BfldFrame` (frozen) — one compressed feedback matrix snapshot
- `BfldReport` (frozen) — aggregator over a 60-s scan window
- `BfldKind` enum — `CompressedHE20/40/80/160`, `UncompressedHT20/40`
### §6 P3.5 — Concurrent-with-P3 phase
Checkbox plan for the bindings module + stub Rust storage + numpy
bridge for `feedback_matrix` (Complex64 ndarray, same approach as
`CsiFrame.amplitude` from P3). Lands in the same wheel as P3, no
schedule cushion needed.
### §11.11/12 — Two new open questions
- **§11.11** — Should the future BFR ingestion Rust crate be a new
`wifi-densepose-bfld` workspace member, or extend `-signal`?
*Tentative: new dedicated crate. Wireshark BFR dissector is ~2k
lines and would bloat `-signal`; ingestion is optional for many
deployments; keep `-signal` lean.*
- **§11.12** — Per-vendor BFR variant compatibility (Broadcom vs
Intel vs Qualcomm vs MediaTek differ in psi/phi quantization +
matrix entry ordering). How much normalisation in the Python
binding vs. the future Rust crate? *Tentative: Python binding is
dumb (numpy ndarray in/out); future Rust crate owns per-vendor
normalisation via a `Vendor` enum on the constructor.*
### §12 — BFLD reference list
- Hernandez & Bulut, ACM TOSN 2024 (first systematic survey of
BFR-as-sensing)
- Yousefi et al., MobiSys 2023 (practical breath + HR extraction)
- IEEE 802.11ax-2021 §27.3.10 (frame format)
- Wireshark `packet-ieee80211.c` dissector
- AX210 Linux mac80211 debugfs path (kernel 6.10+)
ADR line count: 644 → 807 (+163). Refs #785 (tracking issue).
The implementation work for P3.5 lands in the next /loop iteration
alongside P3 vitals + signal DSP bindings.
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(adr-117/p3+p3.5): vitals + BFLD bindings
P3 — Vital sign extraction bindings (wifi-densepose-vitals):
- VitalStatus enum (eq, eq_int, hash, frozen) — Valid/Degraded/Unreliable/Unavailable
- VitalEstimate (frozen) — value_bpm + confidence + status
- VitalReading (frozen) — HR + BR + signal quality composite
- BreathingExtractor — 0.1–0.5 Hz bandpass + zero-crossing
- HeartRateExtractor — 0.8–2.0 Hz bandpass + autocorrelation
- py.allow_threads on extract() hot loops (Q5 audit confirmed
core/vitals/signal are pure-sync — zero tokio deps, safe to release
GIL with no embedded runtime needed)
- 17 tests covering construction, getters, frozen immutability,
esp32_default + explicit ctors, synthetic-signal end-to-end
P3.5 — BFLD bindings (forward-compat surface, stub Rust):
- BfldKind enum — CompressedHE20/40/80/160 + UncompressedHT20/40
with n_subcarriers, bandwidth_mhz, is_he metadata getters
- BfldFrame (frozen) — from_compressed_feedback() accepts numpy
Complex64 ndarray [Nr x Nc x Nsc], validates dims against kind,
feedback_matrix() returns lossless roundtrip ndarray
- BfldReport — aggregates frames, rejects mismatched kinds,
computes inverse-CV coherence score
- 19 tests covering all 6 PHY variants + numpy roundtrip +
dim-mismatch error + aggregation
- Real Rust ingestion (wifi-densepose-bfld crate) lands post-v2.0
per ADR-117 §11.11/12 — Python API will not change
Total Python test count: 93 (was 57, +36 P3+P3.5). All passing.
Refs: docs/adr/ADR-117-pip-wifi-densepose-modernization.md
Refs: #785
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(adr-117/p4): pure-Python WS/MQTT client layer
New sub-package `wifi_densepose.client` (no PyO3, no Rust deps):
- ws.SensingClient — asyncio websockets>=12 wrapper for the Rust
sensing-server /ws/sensing endpoint. Yields typed dataclasses
(ConnectionEstablishedMessage, EdgeVitalsMessage, PoseDataMessage)
with raw-payload fallback for forward-compat with unknown types.
Malformed frames log+drop without breaking the stream.
- mqtt.RuViewMqttClient — paho-mqtt v2 wrapper using the explicit
CallbackAPIVersion.VERSION2 API. Per-instance unique client_id by
default (rumqttc memory lesson). MQTT v5-spec-correct topic
wildcard matcher: + as whole-level wildcard, # matches the prefix
itself plus all sub-levels. Auto-resubscribes on reconnect.
Handler exceptions are caught and logged so a misbehaving callback
can't crash the network loop.
- primitives.SemanticPrimitiveListener — typed router for the 10
HA-MIND fused inference outputs from ADR-115 §3.12
(SomeoneSleeping, PossibleDistress, RoomActive, ElderlyInactivity-
Anomaly, MeetingInProgress, BathroomOccupied, FallRiskElevated,
BedExit, NoMovementSafety, MultiRoomTransition). Decodes both
JSON payloads with confidence+explanation AND plain HA state
strings ("ON"/"OFF"/numeric). Pluggable into RuViewMqttClient.
- ha.HABlueprintHelper — read-only parser for the
homeassistant/<kind>/wifi_densepose_<node>/<id>/config payload
family. Aggregator queries: entities_for_node, by_device_class,
nodes. Useful for blueprint authors + dashboard introspection.
Test coverage (63 new tests, 156 total in Python suite):
- test_client_ha — 18 tests (topic+payload parsing, aggregator)
- test_client_primitives — 13 tests (enum coverage, listener routing)
- test_client_mqtt — 17 tests (matcher parametrize, dispatch path,
on_connect, exception isolation) — no broker needed
- test_client_ws — 6 tests including end-to-end against an in-process
websockets.serve() fixture exercising all 4 message types plus a
malformed-frame survival check
Post-bridge wheel size: 238 KB (well under ADR §5.4 5 MB budget).
Refs: docs/adr/ADR-117-pip-wifi-densepose-modernization.md §5.6
Refs: docs/adr/ADR-115-home-assistant-integration.md §3.12
Refs: #785
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(adr-117/p5+p-tomb): pip-release workflow + v1.99.0 tombstone wheel
P5 — `.github/workflows/pip-release.yml`:
- cibuildwheel matrix per ADR §5.4: manylinux x86_64 + aarch64,
macos x86_64 + arm64, win amd64 (5 wheels via abi3-py310 stable
ABI — one binary per OS/arch covers Python 3.10–3.13)
- Linux aarch64 cross-builds via QEMU; rustup 1.82 pinned in
CIBW_BEFORE_ALL_LINUX for reproducibility
- Per-wheel smoke test: import wifi_densepose, assert hello()=="ok"
- sdist via `maturin sdist`
- Trigger: workflow_dispatch + push to `v*-pip` tags ONLY (never
on regular commits — won't accidentally publish)
- TestPyPI dry-run gate via `repository-url: https://test.pypi.org/legacy/`
- Production PyPI publish via Trusted Publisher OIDC (no API tokens
in GH secrets per ADR §9). Requires one-time PyPI Trusted Publisher
registration before the first publish can fire.
- Q3 (witness hash v2 — ADR-117 §11.3) flagged in workflow comments
as a hard gate before the first tag.
P-tomb — `python/tombstone/`:
- Separate `wifi-densepose==1.99.0` sdist+wheel using setuptools
backend (NOT maturin — tombstone is pure Python, no Rust).
- `src/wifi_densepose/__init__.py` raises ImportError with the
migration URL on import. Verified locally: 2.7 KB wheel,
`pip install` then `import wifi_densepose` raises ImportError
with `pip install wifi-densepose==2.0.0` hint + repo URL.
- 5 unit tests (`tests/test_tombstone.py`) lock the file content
down: must `raise ImportError`, must contain v2 install hint
and migration URL, must NOT contain any `def`/`class`/`import`
beyond the bare `raise` — so a well-intentioned refactor can't
accidentally bloat the tombstone into a real module that loads
partway before failing.
Both wheels are published by the same pip-release.yml workflow:
- `v1.99.0-pip` tag → publishes tombstone (or via workflow_dispatch
with `target: v1-99-tombstone`)
- `v2.X.Y-pip` tag → publishes the v2 wheel matrix
Per ADR-117 §7.3: tag and publish 1.99.0-pip FIRST so the tombstone
claims the "current" slot in pip's resolver, THEN publish 2.0.0-pip.
Test count unchanged in main python/ suite (156/156). Tombstone
sub-suite: 5 passing.
Refs: docs/adr/ADR-117-pip-wifi-densepose-modernization.md §5.4, §7
Refs: #785
Co-Authored-By: claude-flow <ruv@ruv.net>
* hardening(adr-117): benchmarks + security/robustness test suite
Benchmarks (`python/bench/`, pytest-benchmark — opt-in via --benchmark-only):
| Hot path | Mean | Ops/sec | % of 100 Hz budget |
|---|---|---|---|
| BfldFrame HT20 1×1×52 | 800 ns | 1.25 Mops | 0.008% |
| BfldFrame HE20 2×1×242 | 1.3 μs | 750 kops | 0.013% |
| BfldFrame HE80 2×1×996 | 4.2 μs | 236 kops | 0.042% |
| BfldFrame HE160 2×2×1992 | 14 μs | 71 kops | 0.14% |
| BfldFrame.feedback_matrix() | 2.8 μs | 352 kops | — |
| WS edge_vitals decode | 7.4 μs | 134 kops | 0.074% |
| WS pose_data decode (3 persons) | 23 μs | 42 kops | 0.24% |
| BreathingExtractor.extract() 56sc | 28 μs | 35 kops | 0.28% |
| BreathingExtractor.extract() 114sc | 44 μs | 23 kops | 0.44% |
| BreathingExtractor.extract() 242sc | 79 μs | 13 kops | 0.79% |
| HeartRateExtractor.extract() 56sc | 105 μs | 9.5 kops | 1.05% |
All hot paths well under the 100 Hz ESP32 frame budget (10 ms).
Worst case (HeartRateExtractor) uses 1% of the budget — no
optimization needed. Scaling on n_subcarriers is sub-quadratic
(56→242 = 4.3× input, 2.8× time) — catches future O(n²)
regressions.
Security & robustness tests (`tests/test_security.py`, +27 tests):
- WS decoder: rejects non-object roots cleanly, survives 1 MB string
values, handles non-ASCII node IDs, survives deeply-nested JSON
(Python's json.loads built-in guard not bypassed)
- MQTT topic matcher: 9 edge-case parametrize entries including
$SYS topics, null-byte injection, mid-pattern `#` boundary,
empty-string boundary
- MQTT credential confidentiality: password never appears in
repr()/str(), never stored in plain client-instance attribute
- HA discovery: rejects null-byte-laced topics, rejects extra
slashes in node_id, rejects non-dict payload body (list, scalar,
invalid UTF-8 bytes) without crashing
- Semantic primitive listener: rejects topic-injection attempts
(prefix-injected paths, wrong case on final segment), survives
invalid UTF-8 payloads
- Public surface integrity: every name in wifi_densepose.__all__
AND wifi_densepose.client.__all__ resolves — catches accidental
re-export breakage between phases
- Multi-handler MQTT exception isolation: a crashing handler in
the middle of the registered list doesn't stop later handlers
from firing
Test count: 156 → 183 (+27). All passing.
Bench results steady-state confirm no Rust-binding-layer
optimization is needed before the v2.0.0 publish.
Refs: docs/adr/ADR-117-pip-wifi-densepose-modernization.md
Refs: #785
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(adr-117/p5): switch publish workflow to PYPI_API_TOKEN + user-facing README
- Workflow rewired from OIDC Trusted Publisher to token-based publish
via the `PYPI_API_TOKEN` GitHub Actions secret. Both publish jobs
(v2 wheels + tombstone) pass `password: ${{ secrets.PYPI_API_TOKEN }}`
to `pypa/gh-action-pypi-publish@release/v1`. Workflow comments now
document the GCP → GH secret-refresh command.
- Removed `permissions: id-token: write` and the OIDC `environment:`
blocks (no longer needed without OIDC).
- Token was sourced from the GCP Secret Manager entry `PYPI_TOKEN`
in project `cognitum-20260110` and pushed to GH Actions via
`gcloud secrets versions access | gh secret set` so the value
never appeared in a shell variable or this session's output.
- Rewrote `python/README.md` from a developer phase-ledger into a
user-facing PyPI front page: one-paragraph elevator pitch, bullet
list of features, three short usage snippets (vitals extract,
WS subscribe, MQTT semantic-primitive listener, BFLD numpy
bridge), hardware table, links. The README is the FIRST thing
pip users see at https://pypi.org/p/wifi-densepose so it has to
introduce the project, not the build plan.
Wheel rebuilds clean at 253 KB (was 238 KB — +15 KB from the richer
README baked into the wheel metadata). Test suite unchanged at 183/183.
Refs: docs/adr/ADR-117-pip-wifi-densepose-modernization.md
Refs: #785
Co-Authored-By: claude-flow <ruv@ruv.net>
* docs(adr-117): point root README + user-guide at the v2 pip wheel
- Root README — add Option 4 alongside the existing Docker / ESP32 /
Cognitum Seed installs: `pip install "wifi-densepose[client]"` with
a two-line import preview.
- User-guide §Installation — replace the stale "From Source (Python)"
block (which referenced legacy v1 extras `[gpu]` and `[all]` that
don't exist in v2) with a brief "Python wheel (pip) — ADR-117"
section: what the wheel is, install commands, two-line example,
tombstone caveat, and the `maturin develop` source-build path
for contributors.
Refs: docs/adr/ADR-117-pip-wifi-densepose-modernization.md
Refs: #785
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(adr-117/p5): pin Python 3.12 + isolated venv for tombstone smoke-test
First v1.99.0-pip run (26366491748) failed: the runner's system `python`
fell back to `--user` install, then `python -c "import wifi_densepose"`
resolved to something other than the freshly-installed user-site wheel
and returned cleanly instead of raising the tombstone ImportError.
Fixes:
- `actions/setup-python@v5` with explicit 3.12 — owns its own site-
packages so pip won't fall back to --user.
- New "Inspect wheel contents" step prints the wheel manifest +
the verbatim __init__.py inside it. If a future regression ships
an empty __init__.py from a setuptools src-layout edge case,
the failure is debuggable from the run log alone.
- Smoke test now runs in a fresh /tmp/smoke-venv so there's zero
ambiguity about which wifi_densepose gets imported. Also uses
importlib.util.find_spec to print the resolved origin path
before the import attempt — so even if both checks pass, we
see exactly which file we exercised.
No code changes to the tombstone source itself.
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(adr-117/p5): smoke-test must cd out of repo root before importing
Root cause from run 26366579422 diagnostics: the wheel built correctly
(872 bytes, valid ImportError) but `import wifi_densepose` resolved to
the legacy `./wifi_densepose/__init__.py` left in the repo root from
v1, NOT to the freshly-installed tombstone wheel in the smoke venv.
Python places the cwd at sys.path[0] for `python -c "..."`, so
running the import from the repo root made the legacy directory win
over site-packages every time. The "isolated venv" was not the
problem — the cwd was.
Fix: copy the wheel to /tmp, cd /tmp before the import. Now the
smoke test runs in a directory that contains no `wifi_densepose/`
so the only resolution path is the venv's site-packages.
The repo-root `./wifi_densepose/__init__.py` is a separate concern
(legacy v1 carry-over) that should be cleaned up in a follow-up
commit, but the smoke test should not depend on it being absent.
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(adr-117): publish wifi-densepose 2.0.0a1 + ruview 2.0.0a1 to PyPI
Three PyPI artifacts now live (published from .env-sourced PYPI_TOKEN
via twine from the maintainer box — direct upload bypassed the GH
Actions workflow auth churn):
1. wifi-densepose==1.99.0 — tombstone (raises ImportError with migration URL)
https://pypi.org/project/wifi-densepose/1.99.0/
2. wifi-densepose==2.0.0a1 — PyO3 wheel (win_amd64 cp310-abi3) + sdist
https://pypi.org/project/wifi-densepose/2.0.0a1/
3. ruview==2.0.0a1 — meta-package re-exporting wifi_densepose
https://pypi.org/project/ruview/2.0.0a1/
New `python/ruview-meta/` subdirectory:
- pyproject.toml — name="ruview", version="2.0.0a1", setuptools backend,
dependencies = ["wifi-densepose==2.0.0a1"]
- src/ruview/__init__.py — re-exports every name from
`wifi_densepose.__all__` so `from ruview import BreathingExtractor`
is equivalent to `from wifi_densepose import BreathingExtractor`.
Also re-exports `__version__`, `__rust_version__`,
`__rust_build_tag__`, `__build_features__`. Aliases the `client`
sub-package transparently when wifi-densepose[client] extras are
installed.
- README.md — explains why two PyPI names ship the same code (brand
vs technical name) and shows install commands for both.
End-to-end verified: fresh venv, `pip install ruview`,
`import ruview` + `import wifi_densepose` both succeed,
`ruview.BreathingExtractor is wifi_densepose.BreathingExtractor` → True.
Multi-platform wheels (manylinux x86_64+aarch64, macos x86_64+arm64)
still pending — the cibuildwheel workflow path remains for that.
Linux/macOS users today install via the sdist (requires rustup +
maturin locally).
Refs: docs/adr/ADR-117-pip-wifi-densepose-modernization.md
Refs: #785
Co-Authored-By: claude-flow <ruv@ruv.net>
* ci(adr-117): kics-compatible workflow comments + fix-marker guards
- KICS error fix (.github/workflows/pip-release.yml:20): the inline
`gcloud secrets versions access --secret=PYPI_TOKEN ...` runbook
in the workflow header was triggering KICS' generic-secret regex
on the literal `PYPI_TOKEN` substring. Moved the refresh runbook
to docs/integrations/pypi-release.md (with the BOM-stripping
`tr` step that fixed the production publish) and replaced the
inline block with a pointer.
- Three new fix-marker guards in scripts/fix-markers.json so the
next person to touch this code can't silently regress what
PR #786 just shipped:
* RuView#786-tombstone-import — the tombstone __init__.py must
`raise ImportError`, must mention the v2 install hint, must
point at the repo URL, AND must NOT contain `def`/`class`/
`import wifi_densepose` (forbid patterns prevent accidental
bloating into a real module that loads partway before failing).
* RuView#786-tombstone-smoke-cwd — pip-release.yml must `cd /tmp`
before the tombstone smoke-test import, because the legacy
`./wifi_densepose/__init__.py` at repo root would otherwise
shadow the venv install. This was the root cause of run
26366648768; locking it in.
* RuView#786-pypi-token-auth — the workflow must use
`password: ${{ secrets.PYPI_API_TOKEN }}` and must NOT carry
`id-token: write`. The project authenticates via API token,
not OIDC; a partial OIDC migration would 403 silently.
Local check: all 25 markers pass.
Refs: docs/adr/ADR-117-pip-wifi-densepose-modernization.md
Refs: #786
Co-Authored-By: claude-flow <ruv@ruv.net>
526 lines
23 KiB
Markdown
526 lines
23 KiB
Markdown
# Soul Signature — Technical Specification
|
||
|
||
**Status:** Research Specification (Pre-Implementation)
|
||
**Date:** 2026-05-24
|
||
**Author:** ruv
|
||
|
||
---
|
||
|
||
## 1. Overview
|
||
|
||
A Soul Signature is a typed, content-addressed RVF graph encoding seven
|
||
electromagnetic observables extracted from a person in a WiFi-DensePose sensing
|
||
zone. The graph is stored as a single `.rvf` binary blob using the existing RVF
|
||
container format (`v2/crates/wifi-densepose-sensing-server/src/rvf_container.rs`)
|
||
extended with two new segment types defined below. A human-readable JSON sidecar
|
||
accompanies the blob for inspection and provenance.
|
||
|
||
The signature is probabilistic, not deterministic. Matching computes a weighted
|
||
cosine similarity across graph dimensions, producing a score in [0, 1] with a
|
||
calibrated false-accept rate (FAR). The FAR at a given threshold is an open
|
||
research question; the AETHER person re-identification baseline (ADR-024 §2.8:
|
||
>80% mAP at 5 subjects) is the lower bound for the primary embedding channel.
|
||
|
||
---
|
||
|
||
## 2. Design Principles
|
||
|
||
### 2.1 Per-Individual
|
||
|
||
The signature encodes features that are structurally unique to one person at the
|
||
sensing resolution of commodity WiFi hardware. Discriminative dimensions include:
|
||
cardiac timing (R-R interval structure), respiratory mechanics (tidal depth,
|
||
inspiration-to-expiration ratio), skeletal proportions (limb ratios from 17-keypoint
|
||
pose, ADR-079), gait cadence variability, and the RF backscatter profile shaped by
|
||
body mass distribution and geometry.
|
||
|
||
### 2.2 Passive at Enrollment Time
|
||
|
||
No explicit action from the subject is required at recognition time after
|
||
enrollment. Recognition fires whenever an enrolled person is detected in a sensing
|
||
zone. Enrollment itself requires a 60-second structured protocol (see
|
||
`scanning-process.md`). This is a deliberate asymmetry: passive recognition +
|
||
active enrollment — which is the same model used by FaceID (passive unlock after
|
||
initial face setup).
|
||
|
||
The passivity of post-enrollment recognition is a privacy concern addressed in full
|
||
in `security.md` §4.
|
||
|
||
### 2.3 Multi-Modal
|
||
|
||
Seven orthogonal channels contribute. Orthogonality matters: if one channel
|
||
degrades (e.g., cardiac is masked by motion), the remaining six carry the match.
|
||
No single channel is necessary for a positive identification above threshold;
|
||
the fused score is a weighted aggregate.
|
||
|
||
### 2.4 Persistent Across Time
|
||
|
||
The stored signature is valid over weeks to months for adults with stable anatomy
|
||
and health. Re-scan cadence is prescribed in `scanning-process.md`. The
|
||
`longitudinal.rs` module (ADR-030 Tier 4) provides the drift detection that
|
||
flags when a re-scan is necessary.
|
||
|
||
### 2.5 Defensible False-Accept Rate
|
||
|
||
The security model is not "unbreakable." It is "attacker cost exceeds value of
|
||
attack for the threat model in §security." See `security.md` §3.
|
||
|
||
---
|
||
|
||
## 3. Signature as a Typed RVF Graph
|
||
|
||
### 3.1 Container Format
|
||
|
||
The soul signature reuses the RVF binary container defined in
|
||
`v2/crates/wifi-densepose-sensing-server/src/rvf_container.rs` (lines 1–660).
|
||
Existing segment types used:
|
||
|
||
| Segment type | Const | Purpose in soul signature |
|
||
|---|---|---|
|
||
| `SEG_MANIFEST` | `0x05` | Graph metadata: schema version, enroll timestamp, device ID, person_id (opaque u64) |
|
||
| `SEG_VEC` | `0x01` | AETHER 128-dim embedding weights (backbone + projection head) |
|
||
| `SEG_META` | `0x07` | JSON overlay: all non-vector node attributes |
|
||
| `SEG_WITNESS` | `0x0A` | Ed25519 signature over `(content_hash_sha256 || timestamp_ns || enrolled_by_device_id)` |
|
||
| `SEG_EMBED` | `0x0C` | AETHER embedding config + projection head weights (ADR-024 Phase 7) |
|
||
| `SEG_LORA` | `0x0D` | Per-environment LoRA deltas for environment-adapted query |
|
||
|
||
Two new segment types are proposed for the soul signature extension:
|
||
|
||
| Segment type | Const | Purpose |
|
||
|---|---|---|
|
||
| `SEG_SOUL_GRAPH` | `0x10` | JSON-serialized graph: node list + edge list + attribute schemas |
|
||
| `SEG_SOUL_INDEX` | `0x11` | Per-node HNSW index serialization for fast graph-level query |
|
||
|
||
The `SegmentHeader` structure is unchanged. Each segment is 64-byte aligned
|
||
(field `alignment_pad` at offset `0x3C`). CRC32 content hash at offset `0x28`
|
||
covers the payload, providing tamper detection per the existing implementation
|
||
at `rvf_container.rs:line 70`.
|
||
|
||
### 3.2 Node Types
|
||
|
||
Each node is a typed struct. Serialized into SEG_META as a JSON object with a
|
||
`node_type` discriminator string. Vector fields (f32 arrays) are co-located in
|
||
a SEG_VEC segment indexed by the node's `vec_segment_id` field.
|
||
|
||
#### Node: AETHER_Embedding
|
||
|
||
Primary identity anchor. The contrastive CSI embedding from ADR-024.
|
||
|
||
```rust
|
||
pub struct AetherEmbeddingNode {
|
||
pub node_type: &'static str, // "AETHER_Embedding"
|
||
pub vec_segment_id: u64, // references SEG_VEC containing 128 f32s
|
||
pub embedding_dim: usize, // 128
|
||
pub backbone: String, // "csi-to-pose-transformer"
|
||
pub pretrain_method: String, // "simclr+vicreg"
|
||
pub alignment_score: f32, // Lowman alignment metric at enrollment time
|
||
pub uniformity_score: f32, // Hypersphere uniformity at enrollment time
|
||
pub enrollment_frames: u32, // Number of CSI windows averaged into this node
|
||
pub environment_id: String, // SHA-256 of field model eigenstate at enrollment
|
||
pub confidence: f32, // HNSW search confidence against person_track index
|
||
}
|
||
```
|
||
|
||
Stored size: 128 × 4 = 512 bytes in SEG_VEC; JSON metadata ~200 bytes in SEG_META.
|
||
Per ADR-024 §2.8, the person re-identification target is >80% mAP at 5 subjects.
|
||
At 10+ subjects the accuracy is open research; baseline TBD.
|
||
|
||
#### Node: Cardiac_HR_Profile
|
||
|
||
Extracted from the ADR-039 vitals pipeline (magic `0xC511_0002`, fields offset 6-11:
|
||
breathing_rate at `u16 LE` BPM×100, heart_rate at `u32 LE` BPM×10000).
|
||
For the soul signature, cardiac extraction uses the ADR-021 bandpass pipeline
|
||
(0.8–2.0 Hz) over a minimum 30-second rest window.
|
||
|
||
```rust
|
||
pub struct CardiacHRProfileNode {
|
||
pub node_type: &'static str, // "Cardiac_HR_Profile"
|
||
pub baseline_bpm: f32, // mean HR over enrollment window (40–180 BPM range)
|
||
pub hrv_sdnn_ms: f32, // SDNN: std dev of R-R intervals (ms)
|
||
pub hrv_rmssd_ms: f32, // RMSSD: root mean square successive differences
|
||
pub hrv_lf_power: f32, // LF band power (0.04–0.15 Hz), normalized
|
||
pub hrv_hf_power: f32, // HF band power (0.15–0.4 Hz), normalized
|
||
pub hrv_lf_hf_ratio: f32, // LF/HF ratio (autonomic balance marker)
|
||
pub sinus_rhythm_class: u8, // 0=regular, 1=irregular, 2=indeterminate
|
||
pub confidence: f32, // from ADR-021 VitalCoherenceGate PERMIT fraction
|
||
pub window_seconds: u32, // duration of the measurement window
|
||
}
|
||
```
|
||
|
||
WiFi CSI-based HRV extraction is an active research area. The SDNN and RMSSD values
|
||
are discriminative at group level (Zhao et al. 2017, Widar 3.0 2019) but per-person
|
||
uniqueness has not been independently validated at scale. Status: open research.
|
||
|
||
#### Node: Cardiac_Waveform_Morphology
|
||
|
||
Wavelet decomposition of the bandpass-filtered cardiac phase signal. Captures the
|
||
shape of the cardiac waveform, not just its rate. More discriminative than HR alone
|
||
but requires higher SNR and longer measurement window.
|
||
|
||
```rust
|
||
pub struct CardiacWaveformMorphologyNode {
|
||
pub node_type: &'static str, // "Cardiac_Waveform_Morphology"
|
||
pub vec_segment_id: u64, // references SEG_VEC: 64 f32 wavelet coefficients
|
||
pub wavelet_family: String, // "db4" (Daubechies 4, standard for cardiac)
|
||
pub decomposition_levels: u8, // 4 levels
|
||
pub snr_db: f32, // measured SNR at enrollment; low-SNR nodes down-weighted
|
||
pub confidence: f32,
|
||
}
|
||
```
|
||
|
||
Wavelet coefficient dimension: 64 floats = 256 bytes in SEG_VEC. Waveform
|
||
morphology from CSI is highly environment-dependent; the ADR-030 field model
|
||
subtraction must run before this measurement is taken to isolate body perturbation
|
||
from room standing-wave artifacts.
|
||
|
||
#### Node: Respiratory_Pattern
|
||
|
||
Extracted by the ADR-021 BreathingExtractor (0.1–0.5 Hz bandpass) plus the
|
||
ADR-030 persistence layer that accumulates statistics over the enrollment window.
|
||
|
||
```rust
|
||
pub struct RespiratoryPatternNode {
|
||
pub node_type: &'static str, // "Respiratory_Pattern"
|
||
pub baseline_bpm: f32, // mean RR (normal adult: 12–20 BPM)
|
||
pub depth_amplitude_normalized: f32, // tidal depth proxy from CSI variance
|
||
pub inspiration_expiration_ratio: f32, // I:E ratio (1:1.5 to 1:3 typical)
|
||
pub hrv_rsa_power: f32, // respiratory sinus arrhythmia spectral power
|
||
pub apnea_index: f32, // events per hour of significant pauses
|
||
pub waveform_regularity: f32, // coefficient of variation of breath intervals
|
||
pub confidence: f32,
|
||
pub window_seconds: u32,
|
||
}
|
||
```
|
||
|
||
Note: the `apnea_index` field is a biophysical proxy signal (pause events in
|
||
the signal), not a clinical AHI score. It is provided for signature
|
||
discriminability, not diagnostic use.
|
||
|
||
#### Node: Gait_Timing
|
||
|
||
Extracted from the 17-keypoint Kalman pose tracker (`pose_tracker.rs`, ADR-029
|
||
Sect 2.7) during the gait phase of the enrollment protocol. The tracker uses
|
||
ruvector-mincut for person separation and AETHER re-ID for identity continuity.
|
||
|
||
```rust
|
||
pub struct GaitTimingNode {
|
||
pub node_type: &'static str, // "Gait_Timing"
|
||
pub cadence_steps_per_min: f32, // steps per minute
|
||
pub stride_period_variance: f32, // coefficient of variation of stride period
|
||
pub double_support_pct: f32, // fraction of gait cycle in double support
|
||
pub asymmetry_index: f32, // |left_stride - right_stride| / mean_stride
|
||
pub step_width_m: f32, // lateral distance between foot strikes (proxy)
|
||
pub velocity_variance: f32, // gait speed variability
|
||
pub confidence: f32,
|
||
pub stride_count: u32, // number of strides captured during enrollment
|
||
}
|
||
```
|
||
|
||
Gait biometrics from WiFi CSI are documented in WiGait (Adib et al., SIGCOMM
|
||
2015) and WiDraw (Wang et al., MobiCom 2014). Discrimination across 10+ subjects
|
||
in the same household is an open research question for the WiFi-only modality.
|
||
|
||
#### Node: Skeletal_Proportions
|
||
|
||
Derived from the ADR-079 camera + CSI paired keypoint pipeline when available,
|
||
or from CSI-only pose estimation (ADR-023 CsiToPoseTransformer) in camera-free
|
||
deployments. Encodes body geometry as ratios (not absolute values) for scale
|
||
invariance.
|
||
|
||
```rust
|
||
pub struct SkeletalProportionsNode {
|
||
pub node_type: &'static str, // "Skeletal_Proportions"
|
||
pub torso_to_leg_ratio: f32, // torso height / leg length
|
||
pub shoulder_to_hip_ratio: f32, // shoulder width / hip width
|
||
pub upper_to_lower_arm_ratio: f32, // upper arm / forearm
|
||
pub upper_to_lower_leg_ratio: f32, // thigh / shin
|
||
pub head_to_torso_ratio: f32, // head height / torso height
|
||
pub arm_span_to_height_ratio: f32, // Vitruvian ratio (close to 1.0 for most adults)
|
||
pub confidence: f32,
|
||
pub keypoint_source: String, // "camera_paired" | "csi_only" | "fused"
|
||
}
|
||
```
|
||
|
||
CSI-only skeletal proportion estimation has ~15–25% error on individual ratio
|
||
values (open research; baseline from ADR-023 MPJPE ~91.7 mm at best, per
|
||
Person-in-WiFi 3D, CVPR 2024). Camera-paired values (ADR-079) are substantially
|
||
more accurate. The node degrades gracefully when only CSI is available.
|
||
|
||
#### Node: Subcarrier_Reflection_Profile
|
||
|
||
The per-subcarrier amplitude attenuation and phase shift profile measured when
|
||
the subject stands still at three orientations (0°, 90°, 180° rotation). This
|
||
encodes the body's RF backscatter cross-section shape, which is determined by
|
||
body mass distribution, limb geometry, and clothing/material factors.
|
||
|
||
```rust
|
||
pub struct SubcarrierReflectionProfileNode {
|
||
pub node_type: &'static str, // "Subcarrier_Reflection_Profile"
|
||
pub vec_segment_id: u64, // SEG_VEC: 56 × 3 × 2 = 336 f32s
|
||
// (56 subcarriers × 3 orientations ×
|
||
// [amplitude_attenuation, phase_shift])
|
||
pub n_subcarriers: u8, // 56 (HT-LTF) or up to 242 (HE-LTF, ADR-110 C6)
|
||
pub n_orientations: u8, // 3
|
||
pub frequency_mhz: u32, // center frequency at measurement time
|
||
pub environment_id: String, // references field model used for subtraction
|
||
pub confidence: f32,
|
||
}
|
||
```
|
||
|
||
This node directly exploits the ADR-030 field model: the empty-room baseline
|
||
eigenstate is subtracted before computing the reflection profile, isolating the
|
||
person's contribution. Without ADR-030 field subtraction, the profile is too
|
||
environment-coupled to be transferable across rooms. With MERIDIAN (ADR-027),
|
||
the hardware-normalizer layer maps ESP32-S3 (52 subcarriers HT-LTF) and
|
||
ESP32-C6 (242 subcarriers HE-LTF per ADR-110) into a canonical 56-subcarrier
|
||
representation before this measurement.
|
||
|
||
Stored: 336 × 4 = 1,344 bytes in SEG_VEC.
|
||
|
||
#### Node: Body_Field_Coupling
|
||
|
||
The AETHER attention map cells weighted by the ADR-030 room eigenmode structure.
|
||
Encodes how strongly the person's body couples to each dominant electromagnetic
|
||
mode of the room. This is the most physics-grounded node: it captures the
|
||
person's interaction with the actual electromagnetic geometry of the space.
|
||
|
||
```rust
|
||
pub struct BodyFieldCouplingNode {
|
||
pub node_type: &'static str, // "Body_Field_Coupling"
|
||
pub vec_segment_id: u64, // SEG_VEC: n_eigenmodes × n_keypoints f32s
|
||
pub n_eigenmodes: u8, // top-K SVD modes from field_model.rs (default K=8)
|
||
pub n_keypoints: u8, // 17 (COCO)
|
||
pub eigenmode_energy_fractions: Vec<f32>, // fraction of total variance per mode
|
||
pub environment_id: String, // must match SubcarrierReflectionProfile env
|
||
pub confidence: f32,
|
||
}
|
||
```
|
||
|
||
This node is only valid when the same room's field model is available. For
|
||
cross-room recognition, MERIDIAN's environment-disentangled embedding (ADR-027)
|
||
is used instead. The BodyFieldCoupling node provides additional discriminative
|
||
power in single-room deployments and degrades to optional in multi-room contexts.
|
||
|
||
---
|
||
|
||
### 3.3 Edge Types
|
||
|
||
Edges are stored in the SEG_SOUL_GRAPH JSON array. Each edge has a typed
|
||
relationship that constrains how the nodes may be used in matching.
|
||
|
||
| Edge type | Source node(s) | Target node(s) | Semantics |
|
||
|---|---|---|---|
|
||
| `derived_from` | FieldModel_Residual (implicit) | AetherEmbedding | The embedding was computed after field model subtraction |
|
||
| `correlates_with` | Cardiac_HR_Profile | Respiratory_Pattern | Cardiorespiratory coupling at measurement time; correlation coefficient stored as edge weight |
|
||
| `temporally_colocated` | Any pair | Any pair | Both nodes were measured in the same time window; ensures consistency |
|
||
| `temporally_after` | Post-gait node | Pre-gait node | Nodes acquired sequentially during enrollment protocol |
|
||
| `requires_field_model` | SubcarrierReflectionProfile | BodyFieldCoupling | Matching this node requires the same room's ADR-030 field model |
|
||
| `fuses` | AetherEmbedding | SubcarrierReflectionProfile | MERIDIAN-normalized fusion: both mapped to environment-invariant space |
|
||
| `attested_by` | Any leaf node | WitnessChain | Ed25519 witness covers this node's content hash |
|
||
| `derived_by_keypoint_tracker` | GaitTiming | SkeletalProportions | Both extracted from the same pose_tracker.rs output |
|
||
| `environment_normalized` | Any node with `environment_id` | MERIDIAN manifest | MERIDIAN (ADR-027) was applied; signature is cross-room capable |
|
||
|
||
---
|
||
|
||
### 3.4 The Aggregator vs. the Stored Profile
|
||
|
||
Two distinct graph instances exist in the runtime:
|
||
|
||
**Online Aggregator** — a mutable, in-memory graph that accumulates measurements
|
||
across multiple sensing windows. Nodes are incrementally updated with Welford
|
||
online statistics (`field_model.rs::WelfordStats`). Confidence fields grow toward
|
||
1.0 as more frames accumulate. The aggregator never writes to disk during
|
||
normal operation.
|
||
|
||
**Stored Profile** — an immutable, content-addressed `.rvf` file on disk. It is
|
||
generated from the aggregator at the end of the enrollment protocol, when all node
|
||
confidence fields exceed their minimum thresholds. The stored profile is the
|
||
canonical soul signature.
|
||
|
||
```
|
||
Online Aggregator (RAM) Stored Profile (disk / secure enclave)
|
||
+----------------------+ +---------------------------+
|
||
| AETHER_Embedding | enrollment | signature-<sha256>.rvf |
|
||
| accumulated over | completion | SEG_MANIFEST |
|
||
| 60-second protocol +-------------> | SEG_VEC (embedding + refl)|
|
||
| Confidence: 0.0→1.0 | when all | SEG_META (all node attrs) |
|
||
| | gates pass | SEG_EMBED (AETHER config) |
|
||
| Cardiac_HR_Profile | | SEG_WITNESS (Ed25519) |
|
||
| accumulated 30s rest | | SEG_SOUL_GRAPH (graph) |
|
||
+----------------------+ +---------------------------+
|
||
```
|
||
|
||
The aggregator pattern ensures that a partial scan (e.g., subject leaves after
|
||
20 seconds) never produces a stored profile — the quality gates prevent premature
|
||
commitment (see `scanning-process.md §5`).
|
||
|
||
---
|
||
|
||
### 3.5 Serialization
|
||
|
||
**Binary container:** RVF blob, per `rvf_container.rs`. All numeric data is
|
||
little-endian, f32 IEEE 754. Segment alignment: 64 bytes. CRC32 (IEEE 802.3
|
||
polynomial) over each segment payload.
|
||
|
||
**Content addressing:** The file name is:
|
||
```
|
||
signature-<sha256-hex-of-rvf-bytes>.rvf
|
||
```
|
||
SHA-256 is computed over the complete concatenated RVF byte stream after
|
||
`RvfBuilder::build()`. This is a different hash from the per-segment CRC32;
|
||
the CRC32 provides corruption detection within segments, the SHA-256 provides
|
||
content-based addressing and enables deduplication.
|
||
|
||
**JSON-LD sidecar:** An optional `signature-<sha256>.json` file with the same
|
||
base name. Structure:
|
||
|
||
```json
|
||
{
|
||
"@context": "https://ruv.net/soul-signature/v1",
|
||
"schema_version": "0.1.0",
|
||
"person_id": "<opaque_u64_hex>",
|
||
"enrolled_at": "2026-05-24T00:00:00Z",
|
||
"enrolled_by_device_id": "<mac_or_device_fingerprint>",
|
||
"rvf_sha256": "<content_hash>",
|
||
"nodes": [
|
||
{ "node_type": "AETHER_Embedding", "confidence": 0.92, ... },
|
||
{ "node_type": "Cardiac_HR_Profile", "confidence": 0.85, ... },
|
||
...
|
||
],
|
||
"edges": [...],
|
||
"witness": {
|
||
"algorithm": "Ed25519",
|
||
"public_key": "<hex>",
|
||
"signature": "<hex>",
|
||
"signed_fields": ["rvf_sha256", "enrolled_at", "enrolled_by_device_id"]
|
||
}
|
||
}
|
||
```
|
||
|
||
The JSON-LD sidecar is human-readable and intended for audit and provenance.
|
||
It does not contain raw biometric vectors; those stay in the RVF blob.
|
||
|
||
**ISO/IEC 19794-4 alignment:** The soul signature's graph-based vector template
|
||
is conceptually analogous to the ISO/IEC 19794-4 finger image data format
|
||
and ISO/IEC 19794-2 minutiae data. The node/edge schema is not binary-compatible
|
||
with ISO 19794, but the design intent (typed attribute records, quality scores,
|
||
creator provenance) follows the same standard's principles. Future work may
|
||
include a conformance layer if regulatory certification is sought.
|
||
|
||
---
|
||
|
||
### 3.6 Matching Algorithm
|
||
|
||
Given a stored profile `P` and a query embedding `Q` derived from a live sensing
|
||
window, the match score is computed as a weighted sum of per-channel cosine
|
||
similarities:
|
||
|
||
```
|
||
match_score = sum_i ( w_i * cosine_sim(P.channel_i, Q.channel_i) )
|
||
/ sum_i ( w_i * availability(P.channel_i, Q.channel_i) )
|
||
```
|
||
|
||
Where `availability` is 1.0 if both nodes are present and 0.0 if either is absent
|
||
(graceful degradation when a channel cannot be measured in the query window).
|
||
|
||
Default weights (open research; these are design intent, not validated):
|
||
|
||
| Channel | Weight | Rationale |
|
||
|---|---|---|
|
||
| AETHER_Embedding | 0.35 | Primary identity anchor; best-studied channel |
|
||
| Subcarrier_Reflection_Profile | 0.20 | Body geometry; angle-stable |
|
||
| Cardiac_HR_Profile | 0.15 | Physiologically stable in healthy adults |
|
||
| Gait_Timing | 0.15 | Well-studied biometric; discriminative |
|
||
| Respiratory_Pattern | 0.10 | More variable than cardiac |
|
||
| Skeletal_Proportions | 0.05 | Proxy for body shape; CSI-only is noisy |
|
||
| Body_Field_Coupling | 0.00 (single-room) / 0.10 (cross-room disabled) | Valid only when room field model available |
|
||
| Cardiac_Waveform_Morphology | 0.05 (supplementary) | High SNR requirement |
|
||
|
||
The threshold for a positive match is a deployment-specific parameter with a
|
||
documented FAR/FRR trade-off. The AETHER channel alone achieves >80% mAP at 5
|
||
subjects (ADR-024 §2.8 target). The fused multi-channel score is expected to
|
||
exceed this; the exact improvement is open research, baseline TBD.
|
||
|
||
---
|
||
|
||
### 3.7 Rust Type Sketch
|
||
|
||
The following sketch shows how the soul signature types would integrate with
|
||
the existing codebase. This is a design sketch, not implemented code.
|
||
|
||
```rust
|
||
// In a future: v2/crates/wifi-densepose-sensing-server/src/soul_signature.rs
|
||
|
||
pub const SEG_SOUL_GRAPH: u8 = 0x10;
|
||
pub const SEG_SOUL_INDEX: u8 = 0x11;
|
||
|
||
/// Complete soul signature as a graph container.
|
||
pub struct SoulSignature {
|
||
/// Content-addressed identifier: SHA-256 of the RVF blob bytes.
|
||
pub content_hash: [u8; 32],
|
||
/// Opaque person identifier (never PII directly).
|
||
pub person_id: u64,
|
||
/// Unix timestamp of enrollment completion (nanoseconds).
|
||
pub enrolled_at_ns: u64,
|
||
/// Device that performed enrollment.
|
||
pub enrolled_by_device_id: String,
|
||
/// All graph nodes, typed.
|
||
pub nodes: SoulNodes,
|
||
/// All graph edges.
|
||
pub edges: Vec<SoulEdge>,
|
||
/// Ed25519 witness chain (per ADR-110).
|
||
pub witness: WitnessChain,
|
||
}
|
||
|
||
pub struct SoulNodes {
|
||
pub aether_embedding: Option<AetherEmbeddingNode>,
|
||
pub cardiac_hr: Option<CardiacHRProfileNode>,
|
||
pub cardiac_waveform: Option<CardiacWaveformMorphologyNode>,
|
||
pub respiratory: Option<RespiratoryPatternNode>,
|
||
pub gait_timing: Option<GaitTimingNode>,
|
||
pub skeletal_proportions: Option<SkeletalProportionsNode>,
|
||
pub subcarrier_reflection: Option<SubcarrierReflectionProfileNode>,
|
||
pub body_field_coupling: Option<BodyFieldCouplingNode>,
|
||
}
|
||
|
||
pub struct SoulEdge {
|
||
pub edge_type: SoulEdgeType,
|
||
pub source_node_type: String,
|
||
pub target_node_type: String,
|
||
pub weight: f32, // edge attribute (e.g., correlation coefficient)
|
||
}
|
||
|
||
pub enum SoulEdgeType {
|
||
DerivedFrom,
|
||
CorrelatesWith,
|
||
TemporallyColocated,
|
||
TemporallyAfter,
|
||
RequiresFieldModel,
|
||
Fuses,
|
||
AttestedBy,
|
||
DerivedByKeypointTracker,
|
||
EnvironmentNormalized,
|
||
}
|
||
|
||
impl SoulSignature {
|
||
/// Serialize to an RVF binary blob.
|
||
pub fn to_rvf(&self) -> Vec<u8>;
|
||
/// Deserialize from an RVF binary blob.
|
||
pub fn from_rvf(data: &[u8]) -> Result<Self, SoulError>;
|
||
/// Compute the weighted match score against a query.
|
||
pub fn match_score(&self, query: &SoulQuery, weights: &MatchWeights) -> f32;
|
||
/// Check whether all required nodes meet minimum confidence thresholds.
|
||
pub fn is_complete(&self, policy: &CompletenessPolicy) -> bool;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 3.8 What the Signature Is NOT
|
||
|
||
- Not a fingerprint of the room (that is the ADR-030 field model, a separate object).
|
||
- Not a waveform recording (the enrolled vectors are statistics and embeddings, not raw CSI).
|
||
- Not invertible to the original CSI stream (the AETHER projection head's information bottleneck prevents reconstruction; see ADR-024 §4 Negative consequences).
|
||
- Not a single scalar. Reducing to one number for threshold comparison is a deployment decision; the underlying object is a 7-channel graph.
|
||
- Not equal to a stored pose. The AETHER embedding captures body dynamics over many windows, not a single body pose at one instant.
|