feat(adr-185): P1 AETHER bindings (wifi_densepose.aether) + parity harness

Bind the ADR-024 contrastive CSI embedding surface into the wheel behind
a gated [aether] extra / Cargo `aether` feature, per ADR-185 section 3.2.

Surface (bound against the REAL code at HEAD, not the ADR wishlist):
- AetherConfig       -> EmbeddingConfig {d_model,d_proj,temperature,normalize}
- CsiAugmenter       -> augment_pair(window, seed)
- EmbeddingExtractor -> embed(csi): 128-dim L2-normed, GIL-released
- info_nce_loss, cosine_similarity (module fns)

ADR-185 section 3.2 also names aether_loss/VICReg components,
alignment_metric, uniformity_metric, forward_dual, and vicreg_* config
fields. None exist in embedding.rs at HEAD, so they are intentionally NOT
bound (a Rust-side gap, not a binding gap) rather than fabricated.

Parity (section 4.1, release-blocking): committed fixture
aether_input.json -> native Rust reference (tests/aether_parity.rs, calls
sensing-server EmbeddingExtractor directly) locks
tests/golden/aether_embedding.sha256; pytest (tests/test_aether.py) runs
the same fixture through the binding and asserts the identical SHA-256 of
the LE f32 bytes. Both pass.

Verified: cargo test --features aether --test aether_parity -> 2/2 pass;
maturin develop --features aether + pytest tests/test_aether.py -> 9/9
pass; default cargo build clean with 0 sensing-server refs in the dep
graph (gate keeps the base wheel lean).

HONEST WHEEL-SIZE FINDING (ADR-185 section 9 / Open Q 11.1, unresolved):
wifi-densepose-sensing-server is an Axum/tokio crate whose tokio/axum/
worldgraph/ruvector deps are NON-optional (only mqtt/matter are
features), so default-features=false does NOT drop them. An [aether]
wheel therefore links the full server tree and BREAKS the ADR-117 section
5.4 <=5 MB budget. The fix is the ADR-sanctioned hoist of embedding.rs
(+ its graph_transformer/sona siblings) into a leaf crate so the wheel
links pure compute only -- a change INSIDE wifi-densepose-sensing-server,
owned by another agent this session, so deferred as a required
pre-release follow-up. P1 binds real code and proves parity today; the
hoist is a wheel-size optimization, not a functional blocker.

Note: building required initializing the worldgraph/rufield/ruvector
submodules (absent in the fresh worktree).
This commit is contained in:
ruv
2026-07-21 16:28:14 -07:00
parent 5426fe830c
commit d060998e3b
11 changed files with 4276 additions and 26 deletions
+3683 -24
View File
File diff suppressed because it is too large Load Diff
+29 -2
View File
@@ -23,6 +23,15 @@ name = "wifi_densepose_native"
crate-type = ["cdylib", "rlib"]
path = "src/lib.rs"
# ADR-185 §3.1 — optional pip extras map to Cargo features so the
# default wheel links none of the SOTA subsystems. P1 wires `aether`.
[features]
default = []
# ADR-185 P1 — AETHER contrastive CSI embeddings. Pulls the backing
# `wifi-densepose-sensing-server` crate (see the honest wheel-size note
# on that dep below).
aether = ["dep:wifi-densepose-sensing-server"]
[dependencies]
# PyO3 with abi3-py310 — one compiled binary covers Python 3.10, 3.11,
# 3.12, 3.13, and any future 3.x that keeps the stable ABI (ADR-117 §5.4).
@@ -50,6 +59,24 @@ wifi-densepose-bfld = { version = "0.3.0", path = "../v2/crates/wifi-densepose-b
# the future P3 CsiFrame numpy round-trip.
numpy = "0.22"
# ADR-185 P1 — AETHER backing crate (contrastive `embedding` module,
# ADR-024). Optional + gated behind the `aether` feature.
#
# HONEST WHEEL-SIZE NOTE (ADR-185 §9 / Open Q §11.1): this crate is an
# Axum/tokio server crate. `default-features = false` does NOT drop
# tokio/axum here — they are non-optional deps (only `mqtt`/`matter`
# are features). So an `[aether]` wheel currently links the full
# sensing-server dependency tree, breaking the ADR-117 §5.4 ≤5 MB
# budget. The ADR-sanctioned fix is to hoist `embedding.rs` (+ its
# `graph_transformer`/`sona` siblings) into a leaf crate so the wheel
# links only pure compute — a Rust-side refactor inside
# `wifi-densepose-sensing-server`, owned by another agent this session.
# P1 binds against the real code and proves parity; the leaf-crate
# hoist is the required pre-release follow-up.
wifi-densepose-sensing-server = { version = "0.3.0", path = "../v2/crates/wifi-densepose-sensing-server", optional = true, default-features = false }
[dev-dependencies]
# Doc-test infrastructure for the Python-facing examples in the bound
# Rust functions. Lands properly in P2 once #[pyfunction]s exist to test.
# ADR-185 §4.1 parity harness — SHA-256 the native-Rust reference
# embedding and read the committed golden fixture.
sha2 = "0.10"
serde_json = "1"
+6
View File
@@ -48,6 +48,12 @@ client = [
"websockets>=12.0",
"paho-mqtt>=2.1",
]
# ADR-185 P1 — AETHER contrastive embeddings. Unlike `client`, this
# extra carries no pure-Python deps: it is a marker for a *compiled*
# feature build (`maturin ... --features aether` / a cibuildwheel
# feature axis, ADR-185 §3.1). Installing the base wheel and importing
# `wifi_densepose.aether` raises a clear ImportError naming this extra.
aether = []
# Developer dependencies for running the test suite + lint.
dev = [
"pytest>=8.0",
+242
View File
@@ -0,0 +1,242 @@
//! ADR-185 P1 — PyO3 bindings for AETHER contrastive CSI embeddings.
//!
//! Surfaces the **pure-sync** contrastive-embedding compute from
//! `wifi-densepose-sensing-server::embedding` (ADR-024) into
//! `wifi_densepose.aether`:
//!
//! - `AetherConfig` — wraps `EmbeddingConfig` (d_model / d_proj /
//! temperature / normalize)
//! - `CsiAugmenter` — SimCLR-style augmentation pair generator
//! - `EmbeddingExtractor`— backbone + projection → 128-dim L2-normed embedding
//! - `info_nce_loss` — NT-Xent contrastive loss (module function)
//! - `cosine_similarity` — re-ID similarity helper (module function)
//!
//! ## Honest scope vs ADR-185 §3.2
//!
//! ADR-185 §3.2 names an aspirational surface (`aether_loss` returning
//! VICReg components, `alignment_metric`, `uniformity_metric`,
//! `forward_dual`, an `AetherConfig` with `vicreg_*` fields). Those do
//! **not** exist in the backing crate at HEAD — `embedding.rs` exposes
//! `EmbeddingConfig { d_model, d_proj, temperature, normalize }`,
//! `info_nce_loss` (plain `f32`), `CsiAugmenter::augment_pair`, and
//! `EmbeddingExtractor::extract`. This binding surfaces **what actually
//! exists** rather than fabricating the ADR's wished-for API. The
//! VICReg loss / metric surface is a Rust-side gap, not a binding gap.
//!
//! ## GIL release strategy (per ADR-117 §7, matching bindings/vitals.rs)
//!
//! `extract`, `augment_pair`, and `info_nce_loss` are pure-sync matrix
//! ops touching no Python objects, so they run inside
//! `py.allow_threads(|| ...)`.
use pyo3::prelude::*;
use wifi_densepose_sensing_server::embedding::{
info_nce_loss as rust_info_nce_loss, CsiAugmenter, EmbeddingConfig, EmbeddingExtractor,
};
use wifi_densepose_sensing_server::graph_transformer::TransformerConfig;
// ─── AetherConfig ────────────────────────────────────────────────────
/// Configuration for the contrastive embedding model.
///
/// Python:
/// ```python
/// from wifi_densepose.aether import AetherConfig
/// cfg = AetherConfig(d_model=64, d_proj=128, temperature=0.07, normalize=True)
/// ```
#[pyclass(frozen, name = "AetherConfig")]
#[derive(Clone)]
pub struct PyAetherConfig {
inner: EmbeddingConfig,
}
#[pymethods]
impl PyAetherConfig {
#[new]
#[pyo3(signature = (d_model=64, d_proj=128, temperature=0.07, normalize=true))]
fn new(d_model: usize, d_proj: usize, temperature: f32, normalize: bool) -> Self {
Self {
inner: EmbeddingConfig {
d_model,
d_proj,
temperature,
normalize,
},
}
}
#[getter]
fn d_model(&self) -> usize {
self.inner.d_model
}
#[getter]
fn d_proj(&self) -> usize {
self.inner.d_proj
}
#[getter]
fn temperature(&self) -> f32 {
self.inner.temperature
}
#[getter]
fn normalize(&self) -> bool {
self.inner.normalize
}
fn __repr__(&self) -> String {
format!(
"AetherConfig(d_model={}, d_proj={}, temperature={}, normalize={})",
self.inner.d_model, self.inner.d_proj, self.inner.temperature, self.inner.normalize,
)
}
}
// ─── CsiAugmenter ────────────────────────────────────────────────────
/// SimCLR-style CSI augmentation. `augment_pair` returns two distinct
/// augmented views of the same CSI window for contrastive pretraining.
///
/// Python:
/// ```python
/// from wifi_densepose.aether import CsiAugmenter
/// aug = CsiAugmenter()
/// view_a, view_b = aug.augment_pair(window, seed=42)
/// ```
#[pyclass(name = "CsiAugmenter")]
pub struct PyCsiAugmenter {
inner: CsiAugmenter,
}
#[pymethods]
impl PyCsiAugmenter {
#[new]
fn new() -> Self {
Self {
inner: CsiAugmenter::new(),
}
}
/// Produce two augmented views `(view_a, view_b)` of `window`
/// (frames × subcarriers) using the deterministic `seed`. GIL is
/// released during augmentation.
fn augment_pair(
&self,
py: Python<'_>,
window: Vec<Vec<f32>>,
seed: u64,
) -> (Vec<Vec<f32>>, Vec<Vec<f32>>) {
py.allow_threads(|| self.inner.augment_pair(&window, seed))
}
fn __repr__(&self) -> String {
"CsiAugmenter(SimCLR-style CSI augmentation)".to_string()
}
}
// ─── EmbeddingExtractor ──────────────────────────────────────────────
/// Full AETHER embedding extractor: CSI→pose transformer backbone +
/// projection head → a `d_proj`-dim (default 128) L2-normalized
/// embedding. Weights are deterministically seeded, so `embed` is a
/// pure function of its input for a fixed config.
///
/// Python:
/// ```python
/// from wifi_densepose.aether import AetherConfig, EmbeddingExtractor
/// ext = EmbeddingExtractor(n_subcarriers=56, config=AetherConfig())
/// emb = ext.embed(window) # list[float], len == config.d_proj
/// ```
#[pyclass(name = "EmbeddingExtractor")]
pub struct PyEmbeddingExtractor {
inner: EmbeddingExtractor,
embedding_dim: usize,
}
#[pymethods]
impl PyEmbeddingExtractor {
/// Construct an extractor. The transformer backbone is sized from
/// `n_subcarriers` and `config.d_model`; `config.d_proj` sets the
/// embedding dimension.
#[new]
#[pyo3(signature = (n_subcarriers, config, n_keypoints=17, n_heads=4, n_gnn_layers=2))]
fn new(
n_subcarriers: usize,
config: PyAetherConfig,
n_keypoints: usize,
n_heads: usize,
n_gnn_layers: usize,
) -> Self {
let e_config = config.inner.clone();
let t_config = TransformerConfig {
n_subcarriers,
n_keypoints,
d_model: e_config.d_model,
n_heads,
n_gnn_layers,
};
let embedding_dim = e_config.d_proj;
Self {
inner: EmbeddingExtractor::new(t_config, e_config),
embedding_dim,
}
}
/// Extract an embedding from a CSI window (frames × subcarriers).
/// Returns a `d_proj`-length vector (L2-normed when the config's
/// `normalize` is set). GIL released during the forward pass.
fn embed(&mut self, py: Python<'_>, csi_features: Vec<Vec<f32>>) -> Vec<f32> {
py.allow_threads(|| self.inner.extract(&csi_features))
}
#[getter]
fn embedding_dim(&self) -> usize {
self.embedding_dim
}
fn __repr__(&self) -> String {
format!("EmbeddingExtractor(embedding_dim={})", self.embedding_dim)
}
}
// ─── Module functions ────────────────────────────────────────────────
/// InfoNCE (NT-Xent) contrastive loss between two batches of embeddings.
/// Delegates to the identical Rust implementation. GIL released.
#[pyfunction]
#[pyo3(signature = (embeddings_a, embeddings_b, temperature=0.07))]
fn info_nce_loss(
py: Python<'_>,
embeddings_a: Vec<Vec<f32>>,
embeddings_b: Vec<Vec<f32>>,
temperature: f32,
) -> f32 {
py.allow_threads(|| rust_info_nce_loss(&embeddings_a, &embeddings_b, temperature))
}
/// Cosine similarity between two embeddings — the re-ID scoring
/// primitive. Byte-identical to the private `cosine_similarity` in the
/// backing crate (same dot-product / norm formula, `f32`).
#[pyfunction]
fn cosine_similarity(a: Vec<f32>, b: Vec<f32>) -> f32 {
let n = a.len().min(b.len());
let dot: f32 = (0..n).map(|i| a[i] * b[i]).sum();
let na = (0..n).map(|i| a[i] * a[i]).sum::<f32>().sqrt();
let nb = (0..n).map(|i| b[i] * b[i]).sum::<f32>().sqrt();
if na > 1e-10 && nb > 1e-10 {
dot / (na * nb)
} else {
0.0
}
}
pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<PyAetherConfig>()?;
m.add_class::<PyCsiAugmenter>()?;
m.add_class::<PyEmbeddingExtractor>()?;
m.add_function(wrap_pyfunction!(info_nce_loss, m)?)?;
m.add_function(wrap_pyfunction!(cosine_similarity, m)?)?;
Ok(())
}
+11
View File
@@ -17,6 +17,8 @@
use pyo3::prelude::*;
mod bindings {
#[cfg(feature = "aether")]
pub mod aether;
pub mod bfld;
pub mod keypoint;
pub mod pose;
@@ -43,6 +45,8 @@ fn build_features() -> Vec<&'static str> {
feats.push("p2-pose-bindings"); // BoundingBox + PersonPose + PoseEstimate
feats.push("p3-vitals-bindings"); // BreathingExtractor + HeartRateExtractor + VitalEstimate
feats.push("p3.5-bfld-bindings"); // BfldFrame + BfldReport + BfldKind (stub Rust)
#[cfg(feature = "aether")]
feats.push("p6-aether-bindings"); // ADR-185 P1 — AETHER contrastive embeddings
feats
}
@@ -85,5 +89,12 @@ fn wifi_densepose_native(m: &Bound<'_, PyModule>) -> PyResult<()> {
// the published `wifi-densepose-bfld 0.3.0` crate, not the Python port).
// Closes ADR-125 §2.1.d at the binding boundary.
bindings::privacy_gate::register(m)?;
// ADR-185 P1 — AETHER contrastive CSI embedding bindings, compiled
// and registered only under the `aether` feature so the default
// wheel links none of the sensing-server dependency tree.
#[cfg(feature = "aether")]
bindings::aether::register(m)?;
Ok(())
}
+99
View File
@@ -0,0 +1,99 @@
//! ADR-185 §4.1 — AETHER bit-for-bit parity: native-Rust reference half.
//!
//! Produces the golden 128-dim embedding by calling the canonical
//! `wifi-densepose-sensing-server::embedding` code DIRECTLY (no PyO3),
//! for the committed `tests/golden/aether_input.json` fixture, and locks
//! its SHA-256 into `tests/golden/aether_embedding.sha256`.
//!
//! The pytest half (`tests/test_aether.py`) independently runs the same
//! fixture through the Python binding and asserts the identical hash —
//! together they prove the binding is byte-identical to native Rust.
//!
//! Regeneration (only when the Rust subsystem intentionally changes):
//! delete `tests/golden/aether_embedding.sha256` and re-run
//! `cargo test --features aether`.
#![cfg(feature = "aether")]
use std::fs;
use std::path::PathBuf;
use sha2::{Digest, Sha256};
use wifi_densepose_sensing_server::embedding::{EmbeddingConfig, EmbeddingExtractor};
use wifi_densepose_sensing_server::graph_transformer::TransformerConfig;
fn golden_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("golden")
}
fn load_input() -> Vec<Vec<f32>> {
let raw = fs::read_to_string(golden_dir().join("aether_input.json"))
.expect("read aether_input.json fixture");
let rows: Vec<Vec<f64>> = serde_json::from_str(&raw).expect("parse aether_input.json");
rows.into_iter()
.map(|row| row.into_iter().map(|x| x as f32).collect())
.collect()
}
/// Build the extractor identically to the Python binding's default
/// construction: `AetherConfig()` + `EmbeddingExtractor(n_subcarriers=56, cfg)`.
fn embed_native(input: &[Vec<f32>]) -> Vec<f32> {
let e_config = EmbeddingConfig {
d_model: 64,
d_proj: 128,
temperature: 0.07,
normalize: true,
};
let t_config = TransformerConfig {
n_subcarriers: 56,
n_keypoints: 17,
d_model: 64,
n_heads: 4,
n_gnn_layers: 2,
};
let mut ext = EmbeddingExtractor::new(t_config, e_config);
ext.extract(input)
}
fn sha256_le(embedding: &[f32]) -> String {
let mut hasher = Sha256::new();
for &x in embedding {
hasher.update(x.to_le_bytes());
}
hasher
.finalize()
.iter()
.map(|b| format!("{b:02x}"))
.collect()
}
#[test]
fn native_embedding_is_128_dim_unit_norm() {
let emb = embed_native(&load_input());
assert_eq!(emb.len(), 128, "AETHER embedding must be 128-dim");
let norm: f32 = emb.iter().map(|x| x * x).sum::<f32>().sqrt();
assert!(
(norm - 1.0).abs() < 1e-4,
"embedding must be L2-normalized, got norm={norm}"
);
}
#[test]
fn native_embedding_matches_committed_golden() {
let emb = embed_native(&load_input());
let got = sha256_le(&emb);
let path = golden_dir().join("aether_embedding.sha256");
match fs::read_to_string(&path) {
Ok(expected) => assert_eq!(
got,
expected.trim(),
"native AETHER embedding hash drifted from committed golden \
(intentional? delete the .sha256 and regenerate)"
),
Err(_) => {
fs::write(&path, &got).expect("write golden sha256");
panic!("no committed golden found; wrote {got}. Re-run to verify parity.");
}
}
}
@@ -0,0 +1 @@
b315bd0c19f44409f9dec5677c30dec657327e46df81c249ddbd509e0ba541c2
+1
View File
@@ -0,0 +1 @@
[[0.0, 0.00390625, 0.0078125, 0.01171875, 0.015625, 0.01953125, 0.0234375, 0.02734375, 0.03125, 0.03515625, 0.0390625, 0.04296875, 0.046875, 0.05078125, 0.0546875, 0.05859375, 0.0625, 0.06640625, 0.0703125, 0.07421875, 0.078125, 0.08203125, 0.0859375, 0.08984375, 0.09375, 0.09765625, 0.1015625, 0.10546875, 0.109375, 0.11328125, 0.1171875, 0.12109375, 0.125, 0.12890625, 0.1328125, 0.13671875, 0.140625, 0.14453125, 0.1484375, 0.15234375, 0.15625, 0.16015625, 0.1640625, 0.16796875, 0.171875, 0.17578125, 0.1796875, 0.18359375, 0.1875, 0.19140625, 0.1953125, 0.19921875, 0.203125, 0.20703125, 0.2109375, 0.21484375], [0.21875, 0.22265625, 0.2265625, 0.23046875, 0.234375, 0.23828125, 0.2421875, 0.24609375, 0.25, 0.25390625, 0.2578125, 0.26171875, 0.265625, 0.26953125, 0.2734375, 0.27734375, 0.28125, 0.28515625, 0.2890625, 0.29296875, 0.296875, 0.30078125, 0.3046875, 0.30859375, 0.3125, 0.31640625, 0.3203125, 0.32421875, 0.328125, 0.33203125, 0.3359375, 0.33984375, 0.34375, 0.34765625, 0.3515625, 0.35546875, 0.359375, 0.36328125, 0.3671875, 0.37109375, 0.375, 0.37890625, 0.3828125, 0.38671875, 0.390625, 0.39453125, 0.3984375, 0.40234375, 0.40625, 0.41015625, 0.4140625, 0.41796875, 0.421875, 0.42578125, 0.4296875, 0.43359375], [0.4375, 0.44140625, 0.4453125, 0.44921875, 0.453125, 0.45703125, 0.4609375, 0.46484375, 0.46875, 0.47265625, 0.4765625, 0.48046875, 0.484375, 0.48828125, 0.4921875, 0.49609375, 0.5, 0.50390625, 0.5078125, 0.51171875, 0.515625, 0.51953125, 0.5234375, 0.52734375, 0.53125, 0.53515625, 0.5390625, 0.54296875, 0.546875, 0.55078125, 0.5546875, 0.55859375, 0.5625, 0.56640625, 0.5703125, 0.57421875, 0.578125, 0.58203125, 0.5859375, 0.58984375, 0.59375, 0.59765625, 0.6015625, 0.60546875, 0.609375, 0.61328125, 0.6171875, 0.62109375, 0.625, 0.62890625, 0.6328125, 0.63671875, 0.640625, 0.64453125, 0.6484375, 0.65234375], [0.65625, 0.66015625, 0.6640625, 0.66796875, 0.671875, 0.67578125, 0.6796875, 0.68359375, 0.6875, 0.69140625, 0.6953125, 0.69921875, 0.703125, 0.70703125, 0.7109375, 0.71484375, 0.71875, 0.72265625, 0.7265625, 0.73046875, 0.734375, 0.73828125, 0.7421875, 0.74609375, 0.75, 0.75390625, 0.7578125, 0.76171875, 0.765625, 0.76953125, 0.7734375, 0.77734375, 0.78125, 0.78515625, 0.7890625, 0.79296875, 0.796875, 0.80078125, 0.8046875, 0.80859375, 0.8125, 0.81640625, 0.8203125, 0.82421875, 0.828125, 0.83203125, 0.8359375, 0.83984375, 0.84375, 0.84765625, 0.8515625, 0.85546875, 0.859375, 0.86328125, 0.8671875, 0.87109375], [0.875, 0.87890625, 0.8828125, 0.88671875, 0.890625, 0.89453125, 0.8984375, 0.90234375, 0.90625, 0.91015625, 0.9140625, 0.91796875, 0.921875, 0.92578125, 0.9296875, 0.93359375, 0.9375, 0.94140625, 0.9453125, 0.94921875, 0.953125, 0.95703125, 0.9609375, 0.96484375, 0.96875, 0.97265625, 0.9765625, 0.98046875, 0.984375, 0.98828125, 0.9921875, 0.99609375, 0.0, 0.00390625, 0.0078125, 0.01171875, 0.015625, 0.01953125, 0.0234375, 0.02734375, 0.03125, 0.03515625, 0.0390625, 0.04296875, 0.046875, 0.05078125, 0.0546875, 0.05859375, 0.0625, 0.06640625, 0.0703125, 0.07421875, 0.078125, 0.08203125, 0.0859375, 0.08984375], [0.09375, 0.09765625, 0.1015625, 0.10546875, 0.109375, 0.11328125, 0.1171875, 0.12109375, 0.125, 0.12890625, 0.1328125, 0.13671875, 0.140625, 0.14453125, 0.1484375, 0.15234375, 0.15625, 0.16015625, 0.1640625, 0.16796875, 0.171875, 0.17578125, 0.1796875, 0.18359375, 0.1875, 0.19140625, 0.1953125, 0.19921875, 0.203125, 0.20703125, 0.2109375, 0.21484375, 0.21875, 0.22265625, 0.2265625, 0.23046875, 0.234375, 0.23828125, 0.2421875, 0.24609375, 0.25, 0.25390625, 0.2578125, 0.26171875, 0.265625, 0.26953125, 0.2734375, 0.27734375, 0.28125, 0.28515625, 0.2890625, 0.29296875, 0.296875, 0.30078125, 0.3046875, 0.30859375], [0.3125, 0.31640625, 0.3203125, 0.32421875, 0.328125, 0.33203125, 0.3359375, 0.33984375, 0.34375, 0.34765625, 0.3515625, 0.35546875, 0.359375, 0.36328125, 0.3671875, 0.37109375, 0.375, 0.37890625, 0.3828125, 0.38671875, 0.390625, 0.39453125, 0.3984375, 0.40234375, 0.40625, 0.41015625, 0.4140625, 0.41796875, 0.421875, 0.42578125, 0.4296875, 0.43359375, 0.4375, 0.44140625, 0.4453125, 0.44921875, 0.453125, 0.45703125, 0.4609375, 0.46484375, 0.46875, 0.47265625, 0.4765625, 0.48046875, 0.484375, 0.48828125, 0.4921875, 0.49609375, 0.5, 0.50390625, 0.5078125, 0.51171875, 0.515625, 0.51953125, 0.5234375, 0.52734375], [0.53125, 0.53515625, 0.5390625, 0.54296875, 0.546875, 0.55078125, 0.5546875, 0.55859375, 0.5625, 0.56640625, 0.5703125, 0.57421875, 0.578125, 0.58203125, 0.5859375, 0.58984375, 0.59375, 0.59765625, 0.6015625, 0.60546875, 0.609375, 0.61328125, 0.6171875, 0.62109375, 0.625, 0.62890625, 0.6328125, 0.63671875, 0.640625, 0.64453125, 0.6484375, 0.65234375, 0.65625, 0.66015625, 0.6640625, 0.66796875, 0.671875, 0.67578125, 0.6796875, 0.68359375, 0.6875, 0.69140625, 0.6953125, 0.69921875, 0.703125, 0.70703125, 0.7109375, 0.71484375, 0.71875, 0.72265625, 0.7265625, 0.73046875, 0.734375, 0.73828125, 0.7421875, 0.74609375]]
+103
View File
@@ -0,0 +1,103 @@
"""ADR-185 P1 — AETHER binding tests, incl. the §4.1 bit-for-bit parity gate.
The parity test packs the binding's embedding to little-endian f32 bytes
and asserts its SHA-256 equals the committed golden produced by the
native-Rust reference (`tests/aether_parity.rs`). A mismatch is a
release blocker, not a warning.
"""
from __future__ import annotations
import hashlib
import json
import math
import struct
from pathlib import Path
import pytest
from wifi_densepose import aether
GOLDEN = Path(__file__).parent / "golden"
def load_input() -> list[list[float]]:
return json.loads((GOLDEN / "aether_input.json").read_text())
def build_extractor() -> aether.EmbeddingExtractor:
# Must match the native-Rust reference construction exactly.
cfg = aether.AetherConfig(d_model=64, d_proj=128, temperature=0.07, normalize=True)
return aether.EmbeddingExtractor(n_subcarriers=56, config=cfg)
def test_config_roundtrips_fields() -> None:
cfg = aether.AetherConfig(d_model=64, d_proj=128, temperature=0.07, normalize=True)
assert cfg.d_model == 64
assert cfg.d_proj == 128
assert abs(cfg.temperature - 0.07) < 1e-6
assert cfg.normalize is True
def test_embedding_shape_and_unit_norm() -> None:
emb = build_extractor().embed(load_input())
assert len(emb) == 128
norm = math.sqrt(sum(x * x for x in emb))
assert abs(norm - 1.0) < 1e-4, f"expected unit-norm embedding, got {norm}"
def test_bit_for_bit_parity_with_native_rust() -> None:
"""The release-blocking §4.1 gate: binding output == native Rust, byte-for-byte."""
emb = build_extractor().embed(load_input())
packed = b"".join(struct.pack("<f", x) for x in emb)
got = hashlib.sha256(packed).hexdigest()
expected = (GOLDEN / "aether_embedding.sha256").read_text().strip()
assert got == expected, (
"Python binding embedding diverged from the native-Rust golden "
f"({got} != {expected}) — PyO3 marshalling is not byte-identical."
)
def test_embedding_is_deterministic() -> None:
ext = build_extractor()
inp = load_input()
assert ext.embed(inp) == ext.embed(inp)
def test_cosine_similarity_self_is_one() -> None:
v = [0.1 * i - 0.5 for i in range(32)]
assert abs(aether.cosine_similarity(v, v) - 1.0) < 1e-5
def test_cosine_similarity_orthogonal_is_zero() -> None:
a = [1.0, 0.0, 0.0, 0.0]
b = [0.0, 1.0, 0.0, 0.0]
assert abs(aether.cosine_similarity(a, b)) < 1e-6
def test_info_nce_loss_identical_batch_is_log_n() -> None:
# Identical embeddings → all similarities equal → loss == ln(N).
emb = [[1.0, 0.0, 0.0]] * 4
loss = aether.info_nce_loss(emb, emb, 0.07)
assert abs(loss - math.log(4)) < 0.1
def test_augment_pair_preserves_shape_and_differs() -> None:
window = load_input()
view_a, view_b = aether.CsiAugmenter().augment_pair(window, seed=42)
assert len(view_a) == len(window)
assert len(view_b) == len(window)
assert len(view_a[0]) == len(window[0])
differs = any(
abs(x - y) > 1e-6
for ra, rb in zip(view_a, view_b)
for x, y in zip(ra, rb)
)
assert differs, "augment_pair should return two distinct views"
def test_base_wheel_import_error_message() -> None:
# This wheel HAS the extra, so the import succeeds; assert the guard
# message is present in source so the base-wheel path stays honest.
src = (Path(aether.__file__)).read_text()
assert "pip install wifi-densepose[aether]" in src
+47
View File
@@ -0,0 +1,47 @@
"""AETHER — contrastive CSI embeddings & re-identification (ADR-024, ADR-185 P1).
Self-supervised 128-dim L2-normalized embeddings for WiFi CSI: room
fingerprinting, person re-identification, and anomaly scoring, computed
entirely offline by the Rust core (no server, no network).
Available **only** when the wheel was built with the ``[aether]`` extra::
pip install wifi-densepose[aether]
Quick start::
from wifi_densepose.aether import AetherConfig, EmbeddingExtractor, cosine_similarity
ext = EmbeddingExtractor(n_subcarriers=56, config=AetherConfig())
a = ext.embed(window_a) # list[float], length == config.d_proj (128)
b = ext.embed(window_b)
score = cosine_similarity(a, b) # re-ID similarity in [-1, 1]
"""
from __future__ import annotations
from wifi_densepose import _native
# The AETHER symbols are compiled into `_native` only under the Rust
# `aether` feature. In a base (`pip install wifi-densepose`) wheel they
# are absent — surface a clear, actionable error naming the extra
# (ADR-185 §6 acceptance criterion).
if not hasattr(_native, "AetherConfig"):
raise ImportError(
"wifi_densepose.aether is not available in this wheel. "
"It requires the 'aether' extra: pip install wifi-densepose[aether]"
)
AetherConfig = _native.AetherConfig
CsiAugmenter = _native.CsiAugmenter
EmbeddingExtractor = _native.EmbeddingExtractor
info_nce_loss = _native.info_nce_loss
cosine_similarity = _native.cosine_similarity
__all__ = [
"AetherConfig",
"CsiAugmenter",
"EmbeddingExtractor",
"info_nce_loss",
"cosine_similarity",
]
+54
View File
@@ -0,0 +1,54 @@
"""Type stubs for the AETHER bindings (ADR-185 P1).
Present only when the wheel is built with the ``[aether]`` extra. The
top-level ``wifi_densepose`` package does not re-export these names, so
``mypy --strict`` sees them only via ``from wifi_densepose.aether import ...``.
"""
from __future__ import annotations
class AetherConfig:
def __init__(
self,
d_model: int = ...,
d_proj: int = ...,
temperature: float = ...,
normalize: bool = ...,
) -> None: ...
@property
def d_model(self) -> int: ...
@property
def d_proj(self) -> int: ...
@property
def temperature(self) -> float: ...
@property
def normalize(self) -> bool: ...
def __repr__(self) -> str: ...
class CsiAugmenter:
def __init__(self) -> None: ...
def augment_pair(
self, window: list[list[float]], seed: int
) -> tuple[list[list[float]], list[list[float]]]: ...
def __repr__(self) -> str: ...
class EmbeddingExtractor:
def __init__(
self,
n_subcarriers: int,
config: AetherConfig,
n_keypoints: int = ...,
n_heads: int = ...,
n_gnn_layers: int = ...,
) -> None: ...
def embed(self, csi_features: list[list[float]]) -> list[float]: ...
@property
def embedding_dim(self) -> int: ...
def __repr__(self) -> str: ...
def info_nce_loss(
embeddings_a: list[list[float]],
embeddings_b: list[list[float]],
temperature: float = ...,
) -> float: ...
def cosine_similarity(a: list[float], b: list[float]) -> float: ...