mirror of
https://github.com/ruvnet/RuView
synced 2026-07-22 17:23:19 +00:00
feat(adr-185): P2 MERIDIAN bindings (wifi_densepose.meridian) + parity harness
Bind the ADR-027 MERIDIAN cross-environment domain-generalization surface
into the wheel behind a gated [meridian] extra / Cargo `meridian` feature.
Inference/adaptation path only (tch-free), per ADR-185 section 3.3.
Surface (bound against the REAL code at HEAD, not the ADR wishlist):
- HardwareType / HardwareNormalizer / CanonicalCsiFrame (from
wifi-densepose-signal::hardware_norm)
- MeridianGeometryConfig / GeometryEncoder (64-dim, permutation-invariant)
- RapidAdaptation / AdaptationResult (push_frame + adapt, LoRA deltas)
- CrossDomainEvaluator + mpjpe (from wifi-densepose-train, NO tch-backend)
All compute paths GIL-released (py.allow_threads).
Honest deviations from ADR section 3.3 (documented in the module header):
- ADR's RapidAdaptation.calibrate(windows) and AdaptationResult.converged
DO NOT EXIST. Real API is push_frame + adapt(); result carries
{lora_weights, final_loss, frames_used, adaptation_epochs}. Bound as-is.
- ADR's HardwareType.detect exposed as a staticmethod delegating to the
real HardwareNormalizer::detect_hardware.
- ADR's normalize(frame: CsiFrame, hw) is really normalize(amplitude,
phase, hw) over f64 vectors returning Result; bound faithfully.
- CanonicalCsiFrame fields are singular amplitude/phase (ADR said plural).
Training-time types (DomainFactorizer, GradientReversalLayer,
VirtualDomainAugmentor) are out of P6 scope (need the libtorch tier).
Parity (section 4.1, release-blocking): committed fixture
meridian_input.json -> native Rust reference (tests/meridian_parity.rs,
calls hardware_norm + geometry + rapid_adapt directly) locks
tests/golden/meridian_output.sha256 over the concatenated f32 outputs
(esp32+intel canonical frames, 64-dim geometry vector, rapid-adapt LoRA
weights); pytest (tests/test_meridian.py) runs the same fixture through
the binding and asserts the identical SHA-256. Both pass.
Verified:
cargo test --features meridian --test meridian_parity -> 2/2 pass
maturin develop --features meridian + pytest tests/test_meridian.py
-> 13/13 pass
default cargo build clean, 0 train/signal/sensing-server refs in the
default dep graph (gate keeps the base wheel lean).
WHEEL-SIZE FINDING (ADR-185 section 9 / section 1.2): the libtorch risk
the ADR feared is AVOIDED -- wifi-densepose-train's `tch` dep is properly
optional (feature tch-backend, OFF), so no libtorch links. BUT train
still carries NON-optional deps: tokio (rt subset), the five ruvector-*
crates, and wifi-densepose-nn (which itself pulls `ort` / ONNX Runtime +
reqwest/hyper). So a [meridian] wheel exceeds the ADR-117 section 5.4
<=5 MB budget (though lighter than AETHER's axum/tokio server tree). The
clean fix is the same leaf-crate hoist: move the pure inference modules
(geometry, rapid_adapt, eval, hardware_norm) into a tch/tokio/ort-free
leaf crate. A required pre-release follow-up, not a functional blocker;
P2 binds real code and proves parity today.
This commit is contained in:
Generated
+390
-3
@@ -2,6 +2,12 @@
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "adler2"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.4"
|
||||
@@ -108,6 +114,15 @@ dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arbitrary"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
|
||||
dependencies = [
|
||||
"derive_arbitrary",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arrayref"
|
||||
version = "0.3.9"
|
||||
@@ -445,6 +460,19 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "console"
|
||||
version = "0.15.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8"
|
||||
dependencies = [
|
||||
"encode_unicode",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"unicode-width",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "const-oid"
|
||||
version = "0.9.6"
|
||||
@@ -516,6 +544,15 @@ version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853"
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam"
|
||||
version = "0.8.4"
|
||||
@@ -582,6 +619,27 @@ dependencies = [
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "csv"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938"
|
||||
dependencies = [
|
||||
"csv-core",
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "csv-core"
|
||||
version = "0.1.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "curve25519-dalek"
|
||||
version = "4.1.3"
|
||||
@@ -670,12 +728,33 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "der"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a69dedd701da44b0536442edf09c81a64b0ab97a7a4a5e3d1971f00027cbc63d"
|
||||
dependencies = [
|
||||
"pem-rfc7468",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
version = "0.5.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
|
||||
|
||||
[[package]]
|
||||
name = "derive_arbitrary"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
@@ -728,6 +807,12 @@ version = "1.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e"
|
||||
|
||||
[[package]]
|
||||
name = "encode_unicode"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
|
||||
|
||||
[[package]]
|
||||
name = "enum-as-inner"
|
||||
version = "0.6.1"
|
||||
@@ -815,6 +900,16 @@ version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
|
||||
dependencies = [
|
||||
"miniz_oxide",
|
||||
"zlib-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
version = "0.1.5"
|
||||
@@ -1034,6 +1129,12 @@ dependencies = [
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hmac-sha256"
|
||||
version = "1.1.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec9d92d097f4749b64e8cc33d924d9f40a2d4eb91402b458014b781f5733d60f"
|
||||
|
||||
[[package]]
|
||||
name = "hnsw_rs"
|
||||
version = "0.3.4"
|
||||
@@ -1331,6 +1432,19 @@ dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indicatif"
|
||||
version = "0.17.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235"
|
||||
dependencies = [
|
||||
"console",
|
||||
"number_prefix",
|
||||
"portable-atomic",
|
||||
"unicode-width",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indoc"
|
||||
version = "2.0.7"
|
||||
@@ -1546,6 +1660,12 @@ version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
|
||||
|
||||
[[package]]
|
||||
name = "lzma-rust2"
|
||||
version = "0.15.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e20f57f9918e5bd7bc58c22cdd70a6afc7375d4dd9683af5f2b34bd3d2bba619"
|
||||
|
||||
[[package]]
|
||||
name = "mach2"
|
||||
version = "0.4.3"
|
||||
@@ -1679,6 +1799,16 @@ dependencies = [
|
||||
"unicase",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
|
||||
dependencies = [
|
||||
"adler2",
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "1.2.2"
|
||||
@@ -1803,6 +1933,20 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ndarray-npy"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "58e8a348bca0075000d999d750420d74434fd0d3e0993b456554f885e7657a11"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"ndarray 0.17.2",
|
||||
"num-complex",
|
||||
"num-traits",
|
||||
"py_literal",
|
||||
"zip",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.30.1"
|
||||
@@ -1889,6 +2033,12 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "number_prefix"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3"
|
||||
|
||||
[[package]]
|
||||
name = "numpy"
|
||||
version = "0.22.1"
|
||||
@@ -1968,6 +2118,30 @@ dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ort"
|
||||
version = "2.0.0-rc.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7de3af33d24a745ffb8fab904b13478438d1cd52868e6f17735ef6e1f8bf133"
|
||||
dependencies = [
|
||||
"ndarray 0.17.2",
|
||||
"ort-sys",
|
||||
"smallvec",
|
||||
"tracing",
|
||||
"ureq 3.3.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ort-sys"
|
||||
version = "2.0.0-rc.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7b497d21a8b6fbb4b5a544f8fadb77e801a09ae0add9e411d31c6f89e3c1e90"
|
||||
dependencies = [
|
||||
"hmac-sha256",
|
||||
"lzma-rust2",
|
||||
"ureq 3.3.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.12.5"
|
||||
@@ -2007,12 +2181,63 @@ dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pem-rfc7468"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6305423e0e7738146434843d1694d621cce767262b2a86910beab705e4493d9"
|
||||
dependencies = [
|
||||
"base64ct",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
||||
|
||||
[[package]]
|
||||
name = "pest"
|
||||
version = "2.8.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47627dd7305c6a2d6c8c6bcd24c5a4c17dbbf425f4f9c5313e724b38fc9782e9"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"ucd-trie",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pest_derive"
|
||||
version = "2.8.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b4254325ecad416ab689e27ba51da03ba01a9632bc6e108f5fe7c3c4ad29d58"
|
||||
dependencies = [
|
||||
"pest",
|
||||
"pest_generator",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pest_generator"
|
||||
version = "2.8.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c4c0e91ead7a8f7acecbca6f003fc2e8282b1dbe2dd9c9d2f16aba42995e0a7"
|
||||
dependencies = [
|
||||
"pest",
|
||||
"pest_meta",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pest_meta"
|
||||
version = "2.8.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9744bc48116fee06334924bb5f2bad41eed5e89bd26e29b0b799f9a3f82c210"
|
||||
dependencies = [
|
||||
"pest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "petgraph"
|
||||
version = "0.6.5"
|
||||
@@ -2035,7 +2260,7 @@ version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
|
||||
dependencies = [
|
||||
"der",
|
||||
"der 0.7.10",
|
||||
"spki",
|
||||
]
|
||||
|
||||
@@ -2132,6 +2357,19 @@ dependencies = [
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "py_literal"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "102df7a3d46db9d3891f178dcc826dc270a6746277a9ae6436f8d29fd490a8e1"
|
||||
dependencies = [
|
||||
"num-bigint",
|
||||
"num-complex",
|
||||
"num-traits",
|
||||
"pest",
|
||||
"pest_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyo3"
|
||||
version = "0.22.6"
|
||||
@@ -3063,6 +3301,12 @@ dependencies = [
|
||||
"wide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simd-adler32"
|
||||
version = "0.3.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea"
|
||||
|
||||
[[package]]
|
||||
name = "simd_cesu8"
|
||||
version = "1.2.0"
|
||||
@@ -3116,6 +3360,17 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "socks"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0c3dbbd9ae980613c6dd8e28a9407b50509d3803b57624d5dfe8315218cd58b"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"libc",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spki"
|
||||
version = "0.7.3"
|
||||
@@ -3123,7 +3378,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
|
||||
dependencies = [
|
||||
"base64ct",
|
||||
"der",
|
||||
"der 0.7.10",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3608,6 +3863,12 @@ version = "1.20.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
|
||||
|
||||
[[package]]
|
||||
name = "ucd-trie"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
|
||||
|
||||
[[package]]
|
||||
name = "unicase"
|
||||
version = "2.9.0"
|
||||
@@ -3620,6 +3881,12 @@ version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
version = "0.2.6"
|
||||
@@ -3661,6 +3928,36 @@ dependencies = [
|
||||
"webpki-roots 0.26.11",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ureq"
|
||||
version = "3.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dea7109cdcd5864d4eeb1b58a1648dc9bf520360d7af16ec26d0a9354bafcfc0"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"der 0.8.1",
|
||||
"log",
|
||||
"native-tls",
|
||||
"percent-encoding",
|
||||
"rustls-pki-types",
|
||||
"socks",
|
||||
"ureq-proto",
|
||||
"utf8-zero",
|
||||
"webpki-root-certs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ureq-proto"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e994ba84b0bd1b1b0cf92878b7ef898a5c1760108fe7b6010327e274917a808c"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"http",
|
||||
"httparse",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.5.8"
|
||||
@@ -3679,6 +3976,12 @@ version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
|
||||
|
||||
[[package]]
|
||||
name = "utf8-zero"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e"
|
||||
|
||||
[[package]]
|
||||
name = "utf8_iter"
|
||||
version = "1.0.4"
|
||||
@@ -3991,6 +4294,23 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wifi-densepose-nn"
|
||||
version = "0.3.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"memmap2",
|
||||
"ndarray 0.17.2",
|
||||
"num-traits",
|
||||
"ort",
|
||||
"parking_lot",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wifi-densepose-py"
|
||||
version = "2.0.0-alpha.1"
|
||||
@@ -4002,6 +4322,8 @@ dependencies = [
|
||||
"wifi-densepose-bfld",
|
||||
"wifi-densepose-core",
|
||||
"wifi-densepose-sensing-server",
|
||||
"wifi-densepose-signal",
|
||||
"wifi-densepose-train",
|
||||
"wifi-densepose-vitals",
|
||||
]
|
||||
|
||||
@@ -4050,7 +4372,7 @@ dependencies = [
|
||||
"tower-http",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"ureq",
|
||||
"ureq 2.12.1",
|
||||
"wifi-densepose-bfld",
|
||||
"wifi-densepose-engine",
|
||||
"wifi-densepose-geo",
|
||||
@@ -4085,6 +4407,39 @@ dependencies = [
|
||||
"wifi-densepose-ruvector",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wifi-densepose-train"
|
||||
version = "0.3.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"clap",
|
||||
"csv",
|
||||
"indicatif",
|
||||
"memmap2",
|
||||
"ndarray 0.17.2",
|
||||
"ndarray-npy",
|
||||
"num-complex",
|
||||
"num-traits",
|
||||
"petgraph",
|
||||
"ruvector-attention",
|
||||
"ruvector-attn-mincut",
|
||||
"ruvector-mincut",
|
||||
"ruvector-solver",
|
||||
"ruvector-temporal-tensor",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"toml",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"walkdir",
|
||||
"wifi-densepose-nn",
|
||||
"wifi-densepose-signal",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wifi-densepose-vitals"
|
||||
version = "0.3.1"
|
||||
@@ -4647,8 +5002,40 @@ dependencies = [
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zip"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eb2a05c7c36fde6c09b08576c9f7fb4cda705990f73b58fe011abf7dfb24168b"
|
||||
dependencies = [
|
||||
"arbitrary",
|
||||
"crc32fast",
|
||||
"flate2",
|
||||
"indexmap",
|
||||
"memchr",
|
||||
"zopfli",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zlib-rs"
|
||||
version = "0.6.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b142a20ec14a91d5bc708c1dc21b080c550113d8aa77afa29635673a65dd02c5"
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||
|
||||
[[package]]
|
||||
name = "zopfli"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"crc32fast",
|
||||
"log",
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
@@ -31,6 +31,11 @@ default = []
|
||||
# `wifi-densepose-sensing-server` crate (see the honest wheel-size note
|
||||
# on that dep below).
|
||||
aether = ["dep:wifi-densepose-sensing-server"]
|
||||
# ADR-185 P2 — MERIDIAN domain generalization. Binds the tch-free
|
||||
# inference/adaptation path only (see the wheel-size note on the deps
|
||||
# below). `wifi-densepose-train` is depended on WITHOUT `tch-backend`,
|
||||
# so no libtorch is linked.
|
||||
meridian = ["dep:wifi-densepose-train", "dep:wifi-densepose-signal"]
|
||||
|
||||
[dependencies]
|
||||
# PyO3 with abi3-py310 — one compiled binary covers Python 3.10, 3.11,
|
||||
@@ -75,6 +80,23 @@ numpy = "0.22"
|
||||
# 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 }
|
||||
|
||||
# ADR-185 P2 — MERIDIAN backing crates (optional, `meridian`-gated).
|
||||
#
|
||||
# HONEST WHEEL-SIZE NOTE (ADR-185 §9 / §1.2): unlike AETHER, the libtorch
|
||||
# risk is AVOIDED here — `wifi-densepose-train`'s `tch` dep is properly
|
||||
# optional (feature `tch-backend`, OFF by default), so no libtorch links.
|
||||
# BUT `wifi-densepose-train` still carries NON-optional deps: `tokio` (rt
|
||||
# subset), the five `ruvector-*` crates, `wifi-densepose-nn`, petgraph,
|
||||
# memmap2, indicatif, ndarray-npy, csv, toml, clap. So a `[meridian]`
|
||||
# wheel is heavier than the ≤5 MB ADR-117 §5.4 budget (though far lighter
|
||||
# than AETHER's axum/tokio server tree). The clean fix is the same
|
||||
# leaf-crate hoist: move the pure inference modules (geometry,
|
||||
# rapid_adapt, eval, hardware_norm) into a tch/tokio-free leaf crate.
|
||||
# `wifi-densepose-signal` is depended on `default-features = false` to
|
||||
# drop the optional ndarray-linalg/BLAS chain (Windows-friendly).
|
||||
wifi-densepose-train = { version = "0.3.0", path = "../v2/crates/wifi-densepose-train", optional = true, default-features = false }
|
||||
wifi-densepose-signal = { version = "0.3.0", path = "../v2/crates/wifi-densepose-signal", optional = true, default-features = false }
|
||||
|
||||
[dev-dependencies]
|
||||
# ADR-185 §4.1 parity harness — SHA-256 the native-Rust reference
|
||||
# embedding and read the committed golden fixture.
|
||||
|
||||
@@ -54,6 +54,9 @@ client = [
|
||||
# feature axis, ADR-185 §3.1). Installing the base wheel and importing
|
||||
# `wifi_densepose.aether` raises a clear ImportError naming this extra.
|
||||
aether = []
|
||||
# ADR-185 P2 — MERIDIAN domain generalization. Same compiled-feature
|
||||
# marker pattern as `aether` (built via `maturin ... --features meridian`).
|
||||
meridian = []
|
||||
# Developer dependencies for running the test suite + lint.
|
||||
dev = [
|
||||
"pytest>=8.0",
|
||||
|
||||
@@ -0,0 +1,492 @@
|
||||
//! ADR-185 P2 — PyO3 bindings for MERIDIAN cross-environment domain
|
||||
//! generalization (ADR-027).
|
||||
//!
|
||||
//! Surfaces the **pure-sync, tch-free** inference/adaptation path into
|
||||
//! `wifi_densepose.meridian`:
|
||||
//!
|
||||
//! - `HardwareType` / `HardwareNormalizer` / `CanonicalCsiFrame`
|
||||
//! (from `wifi-densepose-signal::hardware_norm`)
|
||||
//! - `MeridianGeometryConfig` / `GeometryEncoder`
|
||||
//! - `RapidAdaptation` / `AdaptationResult`
|
||||
//! - `CrossDomainEvaluator` + `mpjpe`
|
||||
//! (from `wifi-densepose-train`, NO `tch-backend`)
|
||||
//!
|
||||
//! ## Honest scope vs ADR-185 §3.3
|
||||
//!
|
||||
//! ADR-185 §3.3 names a surface that partly diverges from the code at HEAD;
|
||||
//! this binding tracks the **real** API and documents each deviation:
|
||||
//!
|
||||
//! - `HardwareType.detect(subcarrier_count)` — the real detector is the
|
||||
//! static `HardwareNormalizer::detect_hardware`; exposed here as a
|
||||
//! `HardwareType.detect` staticmethod delegating to it (no reimpl).
|
||||
//! - `HardwareNormalizer.normalize(frame: CsiFrame, hw)` — the real method
|
||||
//! takes raw `(amplitude, phase)` f64 vectors and returns a `Result`, so
|
||||
//! it is bound as `normalize(amplitude, phase, hw)` (raises on error).
|
||||
//! - `CanonicalCsiFrame.amplitudes/.phases` — the real fields are singular
|
||||
//! `amplitude`/`phase`; bound under their real names.
|
||||
//! - `RapidAdaptation.calibrate(csi_windows) -> AdaptationResult` with a
|
||||
//! `converged` field — **does not exist**. The real engine is
|
||||
//! `push_frame` + `adapt()`, and `AdaptationResult` carries
|
||||
//! `{lora_weights, final_loss, frames_used, adaptation_epochs}` (no
|
||||
//! `converged`). Bound as-is; the `calibrate`/`converged` surface is a
|
||||
//! Rust-side gap, not fabricated here.
|
||||
//!
|
||||
//! Training-time types (`DomainFactorizer`, `GradientReversalLayer`,
|
||||
//! `VirtualDomainAugmentor`) are out of P6 scope (ADR-185 §3.3 / Open Q
|
||||
//! §11.2) — inference/adaptation only.
|
||||
//!
|
||||
//! ## GIL release (per ADR-117 §7, matching bindings/vitals.rs)
|
||||
//!
|
||||
//! `normalize`, `encode`, `adapt`, and `evaluate` are pure-sync numeric
|
||||
//! ops touching no Python objects, so they run inside `py.allow_threads`.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use pyo3::exceptions::PyValueError;
|
||||
use pyo3::prelude::*;
|
||||
|
||||
use wifi_densepose_signal::hardware_norm::{
|
||||
CanonicalCsiFrame, HardwareNormalizer, HardwareType,
|
||||
};
|
||||
use wifi_densepose_train::eval::{mpjpe as rust_mpjpe, CrossDomainEvaluator};
|
||||
use wifi_densepose_train::geometry::{GeometryEncoder, MeridianGeometryConfig};
|
||||
use wifi_densepose_train::rapid_adapt::{AdaptationLoss, AdaptationResult, RapidAdaptation};
|
||||
|
||||
// ─── HardwareType ────────────────────────────────────────────────────
|
||||
|
||||
/// WiFi chipset family, keyed by subcarrier count.
|
||||
#[pyclass(eq, eq_int, frozen, hash, name = "HardwareType")]
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum PyHardwareType {
|
||||
Esp32S3 = 0,
|
||||
Intel5300 = 1,
|
||||
Atheros = 2,
|
||||
Generic = 3,
|
||||
}
|
||||
|
||||
impl PyHardwareType {
|
||||
fn as_rust(self) -> HardwareType {
|
||||
match self {
|
||||
Self::Esp32S3 => HardwareType::Esp32S3,
|
||||
Self::Intel5300 => HardwareType::Intel5300,
|
||||
Self::Atheros => HardwareType::Atheros,
|
||||
Self::Generic => HardwareType::Generic,
|
||||
}
|
||||
}
|
||||
fn from_rust(hw: HardwareType) -> Self {
|
||||
match hw {
|
||||
HardwareType::Esp32S3 => Self::Esp32S3,
|
||||
HardwareType::Intel5300 => Self::Intel5300,
|
||||
HardwareType::Atheros => Self::Atheros,
|
||||
HardwareType::Generic => Self::Generic,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyHardwareType {
|
||||
/// Detect hardware from subcarrier count (64→Esp32S3, 30→Intel5300,
|
||||
/// 56→Atheros, else Generic). Delegates to the real
|
||||
/// `HardwareNormalizer::detect_hardware`.
|
||||
#[staticmethod]
|
||||
fn detect(subcarrier_count: usize) -> Self {
|
||||
Self::from_rust(HardwareNormalizer::detect_hardware(subcarrier_count))
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn subcarrier_count(&self) -> usize {
|
||||
self.as_rust().subcarrier_count()
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn mimo_streams(&self) -> usize {
|
||||
self.as_rust().mimo_streams()
|
||||
}
|
||||
|
||||
fn __repr__(&self) -> String {
|
||||
format!("HardwareType.{:?}", self.as_rust())
|
||||
}
|
||||
}
|
||||
|
||||
// ─── CanonicalCsiFrame ───────────────────────────────────────────────
|
||||
|
||||
/// A CSI frame canonicalized to the normalizer's subcarrier grid
|
||||
/// (default 56): z-scored amplitude + sanitized (unwrapped, detrended)
|
||||
/// phase.
|
||||
#[pyclass(frozen, name = "CanonicalCsiFrame")]
|
||||
pub struct PyCanonicalCsiFrame {
|
||||
inner: CanonicalCsiFrame,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyCanonicalCsiFrame {
|
||||
#[getter]
|
||||
fn amplitude(&self) -> Vec<f32> {
|
||||
self.inner.amplitude.clone()
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn phase(&self) -> Vec<f32> {
|
||||
self.inner.phase.clone()
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn hardware_type(&self) -> PyHardwareType {
|
||||
PyHardwareType::from_rust(self.inner.hardware_type)
|
||||
}
|
||||
|
||||
fn __repr__(&self) -> String {
|
||||
format!(
|
||||
"CanonicalCsiFrame(subcarriers={}, hardware_type={:?})",
|
||||
self.inner.amplitude.len(),
|
||||
self.inner.hardware_type,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ─── HardwareNormalizer ──────────────────────────────────────────────
|
||||
|
||||
/// Normalizes CSI frames from heterogeneous chipsets into a canonical
|
||||
/// representation (cubic resample → z-score amplitude → sanitize phase).
|
||||
#[pyclass(name = "HardwareNormalizer")]
|
||||
pub struct PyHardwareNormalizer {
|
||||
inner: HardwareNormalizer,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyHardwareNormalizer {
|
||||
/// Create a normalizer. `canonical_subcarriers` defaults to 56.
|
||||
#[new]
|
||||
#[pyo3(signature = (canonical_subcarriers=56))]
|
||||
fn new(canonical_subcarriers: usize) -> PyResult<Self> {
|
||||
HardwareNormalizer::with_canonical_subcarriers(canonical_subcarriers)
|
||||
.map(|inner| Self { inner })
|
||||
.map_err(|e| PyValueError::new_err(e.to_string()))
|
||||
}
|
||||
|
||||
/// Detect hardware from subcarrier count (static).
|
||||
#[staticmethod]
|
||||
fn detect_hardware(subcarrier_count: usize) -> PyHardwareType {
|
||||
PyHardwareType::from_rust(HardwareNormalizer::detect_hardware(subcarrier_count))
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn canonical_subcarriers(&self) -> usize {
|
||||
self.inner.canonical_subcarriers()
|
||||
}
|
||||
|
||||
/// Normalize a raw CSI frame given per-subcarrier `amplitude` and
|
||||
/// `phase` (equal length) and its `hardware` type. Raises
|
||||
/// `ValueError` on empty/mismatched input. GIL released.
|
||||
fn normalize(
|
||||
&self,
|
||||
py: Python<'_>,
|
||||
amplitude: Vec<f64>,
|
||||
phase: Vec<f64>,
|
||||
hardware: PyHardwareType,
|
||||
) -> PyResult<PyCanonicalCsiFrame> {
|
||||
let hw = hardware.as_rust();
|
||||
py.allow_threads(|| self.inner.normalize(&litude, &phase, hw))
|
||||
.map(|inner| PyCanonicalCsiFrame { inner })
|
||||
.map_err(|e| PyValueError::new_err(e.to_string()))
|
||||
}
|
||||
|
||||
fn __repr__(&self) -> String {
|
||||
format!(
|
||||
"HardwareNormalizer(canonical_subcarriers={})",
|
||||
self.inner.canonical_subcarriers()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ─── MeridianGeometryConfig ──────────────────────────────────────────
|
||||
|
||||
/// Config for the geometry encoder (Fourier bands + DeepSets output dim).
|
||||
#[pyclass(frozen, name = "MeridianGeometryConfig")]
|
||||
#[derive(Clone)]
|
||||
pub struct PyMeridianGeometryConfig {
|
||||
inner: MeridianGeometryConfig,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyMeridianGeometryConfig {
|
||||
#[new]
|
||||
#[pyo3(signature = (n_frequencies=10, scale=1.0, geometry_dim=64, seed=42))]
|
||||
fn new(n_frequencies: usize, scale: f32, geometry_dim: usize, seed: u64) -> Self {
|
||||
Self {
|
||||
inner: MeridianGeometryConfig {
|
||||
n_frequencies,
|
||||
scale,
|
||||
geometry_dim,
|
||||
seed,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn n_frequencies(&self) -> usize {
|
||||
self.inner.n_frequencies
|
||||
}
|
||||
#[getter]
|
||||
fn scale(&self) -> f32 {
|
||||
self.inner.scale
|
||||
}
|
||||
#[getter]
|
||||
fn geometry_dim(&self) -> usize {
|
||||
self.inner.geometry_dim
|
||||
}
|
||||
#[getter]
|
||||
fn seed(&self) -> u64 {
|
||||
self.inner.seed
|
||||
}
|
||||
|
||||
fn __repr__(&self) -> String {
|
||||
format!(
|
||||
"MeridianGeometryConfig(n_frequencies={}, scale={}, geometry_dim={}, seed={})",
|
||||
self.inner.n_frequencies, self.inner.scale, self.inner.geometry_dim, self.inner.seed,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ─── GeometryEncoder ─────────────────────────────────────────────────
|
||||
|
||||
/// Permutation-invariant encoder: variable-count AP positions `[x,y,z]`
|
||||
/// → a fixed `geometry_dim` (default 64) vector.
|
||||
#[pyclass(name = "GeometryEncoder")]
|
||||
pub struct PyGeometryEncoder {
|
||||
inner: GeometryEncoder,
|
||||
geometry_dim: usize,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyGeometryEncoder {
|
||||
#[new]
|
||||
#[pyo3(signature = (config=None))]
|
||||
fn new(config: Option<PyMeridianGeometryConfig>) -> Self {
|
||||
let cfg = config.map(|c| c.inner).unwrap_or_default();
|
||||
let geometry_dim = cfg.geometry_dim;
|
||||
Self {
|
||||
inner: GeometryEncoder::new(&cfg),
|
||||
geometry_dim,
|
||||
}
|
||||
}
|
||||
|
||||
/// Encode AP positions (a non-empty list of `[x, y, z]`) into a
|
||||
/// `geometry_dim`-length vector. Raises `ValueError` if the list is
|
||||
/// empty or any position is not exactly 3 coordinates. GIL released.
|
||||
fn encode(&self, py: Python<'_>, ap_positions: Vec<Vec<f32>>) -> PyResult<Vec<f32>> {
|
||||
if ap_positions.is_empty() {
|
||||
return Err(PyValueError::new_err(
|
||||
"ap_positions must contain at least one [x, y, z] position",
|
||||
));
|
||||
}
|
||||
let mut coords: Vec<[f32; 3]> = Vec::with_capacity(ap_positions.len());
|
||||
for (i, p) in ap_positions.iter().enumerate() {
|
||||
if p.len() != 3 {
|
||||
return Err(PyValueError::new_err(format!(
|
||||
"ap_positions[{i}] must have exactly 3 coordinates, got {}",
|
||||
p.len()
|
||||
)));
|
||||
}
|
||||
coords.push([p[0], p[1], p[2]]);
|
||||
}
|
||||
Ok(py.allow_threads(|| self.inner.encode(&coords)))
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn geometry_dim(&self) -> usize {
|
||||
self.geometry_dim
|
||||
}
|
||||
|
||||
fn __repr__(&self) -> String {
|
||||
format!("GeometryEncoder(geometry_dim={})", self.geometry_dim)
|
||||
}
|
||||
}
|
||||
|
||||
// ─── RapidAdaptation / AdaptationResult ──────────────────────────────
|
||||
|
||||
/// Result of `RapidAdaptation.adapt()`.
|
||||
#[pyclass(frozen, name = "AdaptationResult")]
|
||||
pub struct PyAdaptationResult {
|
||||
inner: AdaptationResult,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyAdaptationResult {
|
||||
#[getter]
|
||||
fn lora_weights(&self) -> Vec<f32> {
|
||||
self.inner.lora_weights.clone()
|
||||
}
|
||||
#[getter]
|
||||
fn final_loss(&self) -> f32 {
|
||||
self.inner.final_loss
|
||||
}
|
||||
#[getter]
|
||||
fn frames_used(&self) -> usize {
|
||||
self.inner.frames_used
|
||||
}
|
||||
#[getter]
|
||||
fn adaptation_epochs(&self) -> usize {
|
||||
self.inner.adaptation_epochs
|
||||
}
|
||||
|
||||
fn __repr__(&self) -> String {
|
||||
format!(
|
||||
"AdaptationResult(final_loss={:.6}, frames_used={}, adaptation_epochs={})",
|
||||
self.inner.final_loss, self.inner.frames_used, self.inner.adaptation_epochs,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Few-shot test-time adaptation: accumulate unlabeled CSI frames, then
|
||||
/// `adapt()` to produce LoRA weight deltas that minimize a self-supervised
|
||||
/// proxy loss.
|
||||
///
|
||||
/// Scope caveat (from the Rust module, kept honest): this minimizes a
|
||||
/// self-supervised proxy over a tiny LoRA bottleneck; it is NOT wired to
|
||||
/// the pose model and there is no measured end-to-end PCK gain from this
|
||||
/// path — do not cite a PCK improvement from `adapt()`.
|
||||
#[pyclass(name = "RapidAdaptation")]
|
||||
pub struct PyRapidAdaptation {
|
||||
inner: RapidAdaptation,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyRapidAdaptation {
|
||||
/// Build an adaptation engine. `loss_kind` is one of
|
||||
/// `"contrastive"`, `"entropy"`, `"combined"` (default). `lambda_ent`
|
||||
/// is used only by `"combined"`.
|
||||
#[new]
|
||||
#[pyo3(signature = (
|
||||
min_calibration_frames,
|
||||
lora_rank,
|
||||
loss_kind="combined",
|
||||
epochs=5,
|
||||
lr=0.001,
|
||||
lambda_ent=0.5
|
||||
))]
|
||||
fn new(
|
||||
min_calibration_frames: usize,
|
||||
lora_rank: usize,
|
||||
loss_kind: &str,
|
||||
epochs: usize,
|
||||
lr: f32,
|
||||
lambda_ent: f32,
|
||||
) -> PyResult<Self> {
|
||||
let loss = match loss_kind {
|
||||
"contrastive" => AdaptationLoss::ContrastiveTTT { epochs, lr },
|
||||
"entropy" => AdaptationLoss::EntropyMin { epochs, lr },
|
||||
"combined" => AdaptationLoss::Combined {
|
||||
epochs,
|
||||
lr,
|
||||
lambda_ent,
|
||||
},
|
||||
other => {
|
||||
return Err(PyValueError::new_err(format!(
|
||||
"unknown loss_kind '{other}'; expected 'contrastive', 'entropy', or 'combined'"
|
||||
)))
|
||||
}
|
||||
};
|
||||
Ok(Self {
|
||||
inner: RapidAdaptation::new(min_calibration_frames, lora_rank, loss),
|
||||
})
|
||||
}
|
||||
|
||||
/// Push a single unlabeled CSI frame into the calibration buffer.
|
||||
fn push_frame(&mut self, frame: Vec<f32>) {
|
||||
self.inner.push_frame(&frame);
|
||||
}
|
||||
|
||||
/// True once at least `min_calibration_frames` have been buffered.
|
||||
fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn buffer_len(&self) -> usize {
|
||||
self.inner.buffer_len()
|
||||
}
|
||||
|
||||
/// Run test-time adaptation over the buffered frames. Raises
|
||||
/// `ValueError` if the buffer is empty or `lora_rank == 0`. GIL
|
||||
/// released during the finite-difference optimization.
|
||||
fn adapt(&self, py: Python<'_>) -> PyResult<PyAdaptationResult> {
|
||||
py.allow_threads(|| self.inner.adapt())
|
||||
.map(|inner| PyAdaptationResult { inner })
|
||||
.map_err(|e| PyValueError::new_err(e.to_string()))
|
||||
}
|
||||
|
||||
fn __repr__(&self) -> String {
|
||||
format!("RapidAdaptation(buffered={})", self.inner.buffer_len())
|
||||
}
|
||||
}
|
||||
|
||||
// ─── CrossDomainEvaluator ────────────────────────────────────────────
|
||||
|
||||
/// Cross-domain pose-accuracy evaluator (MPJPE + domain-gap ratio).
|
||||
#[pyclass(name = "CrossDomainEvaluator")]
|
||||
pub struct PyCrossDomainEvaluator {
|
||||
inner: CrossDomainEvaluator,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyCrossDomainEvaluator {
|
||||
/// Create an evaluator for `n_joints` (e.g. 17 for COCO).
|
||||
#[new]
|
||||
fn new(n_joints: usize) -> Self {
|
||||
Self {
|
||||
inner: CrossDomainEvaluator::new(n_joints),
|
||||
}
|
||||
}
|
||||
|
||||
/// Evaluate `predictions` (a list of `(pred, gt)` flat `n_joints*3`
|
||||
/// vectors) grouped by `domain_labels` (0 = in-domain). Returns a
|
||||
/// dict of the six cross-domain metrics. Raises `ValueError` on a
|
||||
/// length mismatch. GIL released.
|
||||
fn evaluate(
|
||||
&self,
|
||||
py: Python<'_>,
|
||||
predictions: Vec<(Vec<f32>, Vec<f32>)>,
|
||||
domain_labels: Vec<u32>,
|
||||
) -> PyResult<HashMap<String, f32>> {
|
||||
if predictions.len() != domain_labels.len() {
|
||||
return Err(PyValueError::new_err(format!(
|
||||
"predictions ({}) and domain_labels ({}) must have equal length",
|
||||
predictions.len(),
|
||||
domain_labels.len()
|
||||
)));
|
||||
}
|
||||
let m = py.allow_threads(|| self.inner.evaluate(&predictions, &domain_labels));
|
||||
let mut out = HashMap::with_capacity(6);
|
||||
out.insert("in_domain_mpjpe".to_string(), m.in_domain_mpjpe);
|
||||
out.insert("cross_domain_mpjpe".to_string(), m.cross_domain_mpjpe);
|
||||
out.insert("few_shot_mpjpe".to_string(), m.few_shot_mpjpe);
|
||||
out.insert("cross_hardware_mpjpe".to_string(), m.cross_hardware_mpjpe);
|
||||
out.insert("domain_gap_ratio".to_string(), m.domain_gap_ratio);
|
||||
out.insert("adaptation_speedup".to_string(), m.adaptation_speedup);
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn __repr__(&self) -> String {
|
||||
"CrossDomainEvaluator()".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Mean Per Joint Position Error between flat `[n_joints*3]` pose vectors.
|
||||
#[pyfunction]
|
||||
fn mpjpe(pred: Vec<f32>, gt: Vec<f32>, n_joints: usize) -> f32 {
|
||||
rust_mpjpe(&pred, >, n_joints)
|
||||
}
|
||||
|
||||
pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.add_class::<PyHardwareType>()?;
|
||||
m.add_class::<PyCanonicalCsiFrame>()?;
|
||||
m.add_class::<PyHardwareNormalizer>()?;
|
||||
m.add_class::<PyMeridianGeometryConfig>()?;
|
||||
m.add_class::<PyGeometryEncoder>()?;
|
||||
m.add_class::<PyAdaptationResult>()?;
|
||||
m.add_class::<PyRapidAdaptation>()?;
|
||||
m.add_class::<PyCrossDomainEvaluator>()?;
|
||||
m.add_function(wrap_pyfunction!(mpjpe, m)?)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -20,6 +20,8 @@ mod bindings {
|
||||
#[cfg(feature = "aether")]
|
||||
pub mod aether;
|
||||
pub mod bfld;
|
||||
#[cfg(feature = "meridian")]
|
||||
pub mod meridian;
|
||||
pub mod keypoint;
|
||||
pub mod pose;
|
||||
pub mod privacy_gate;
|
||||
@@ -47,6 +49,8 @@ fn build_features() -> Vec<&'static str> {
|
||||
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
|
||||
#[cfg(feature = "meridian")]
|
||||
feats.push("p6-meridian-bindings"); // ADR-185 P2 — MERIDIAN domain generalization
|
||||
feats
|
||||
}
|
||||
|
||||
@@ -96,5 +100,11 @@ fn wifi_densepose_native(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
#[cfg(feature = "aether")]
|
||||
bindings::aether::register(m)?;
|
||||
|
||||
// ADR-185 P2 — MERIDIAN cross-environment domain-generalization
|
||||
// bindings (hardware normalization, geometry encoding, rapid
|
||||
// adaptation, cross-domain eval). Gated behind `meridian`; tch-free.
|
||||
#[cfg(feature = "meridian")]
|
||||
bindings::meridian::register(m)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{"esp32_amplitude": [0.51171875, 0.5390625, 0.56640625, 0.59375, 0.62109375, 0.6484375, 0.67578125, 0.703125, 0.73046875, 0.7578125, 0.78515625, 0.8125, 0.83984375, 0.8671875, 0.89453125, 0.921875, 0.94921875, 0.9765625, 1.00390625, 1.03125, 1.05859375, 1.0859375, 1.11328125, 1.140625, 1.16796875, 1.1953125, 1.22265625, 1.25, 1.27734375, 1.3046875, 1.33203125, 1.359375, 1.38671875, 1.4140625, 1.44140625, 1.46875, 1.49609375, 0.5234375, 0.55078125, 0.578125, 0.60546875, 0.6328125, 0.66015625, 0.6875, 0.71484375, 0.7421875, 0.76953125, 0.796875, 0.82421875, 0.8515625, 0.87890625, 0.90625, 0.93359375, 0.9609375, 0.98828125, 1.015625, 1.04296875, 1.0703125, 1.09765625, 1.125, 1.15234375, 1.1796875, 1.20703125, 1.234375], "esp32_phase": [-0.45703125, -0.4375, -0.41796875, -0.3984375, -0.37890625, -0.359375, -0.33984375, -0.3203125, -0.30078125, -0.28125, -0.26171875, -0.2421875, -0.22265625, -0.203125, -0.18359375, -0.1640625, -0.14453125, -0.125, -0.10546875, -0.0859375, -0.06640625, -0.046875, -0.02734375, -0.0078125, 0.01171875, 0.03125, 0.05078125, 0.0703125, 0.08984375, 0.109375, 0.12890625, 0.1484375, 0.16796875, 0.1875, 0.20703125, 0.2265625, 0.24609375, 0.265625, 0.28515625, 0.3046875, 0.32421875, 0.34375, 0.36328125, 0.3828125, 0.40234375, 0.421875, 0.44140625, 0.4609375, 0.48046875, -0.5, -0.48046875, -0.4609375, -0.44140625, -0.421875, -0.40234375, -0.3828125, -0.36328125, -0.34375, -0.32421875, -0.3046875, -0.28515625, -0.265625, -0.24609375, -0.2265625], "intel_amplitude": [0.50390625, 0.546875, 0.58984375, 0.6328125, 0.67578125, 0.71875, 0.76171875, 0.8046875, 0.84765625, 0.890625, 0.93359375, 0.9765625, 1.01953125, 1.0625, 1.10546875, 1.1484375, 1.19140625, 1.234375, 1.27734375, 1.3203125, 1.36328125, 1.40625, 1.44921875, 1.4921875, 0.53515625, 0.578125, 0.62109375, 0.6640625, 0.70703125, 0.75], "intel_phase": [-0.47265625, -0.4609375, -0.44921875, -0.4375, -0.42578125, -0.4140625, -0.40234375, -0.390625, -0.37890625, -0.3671875, -0.35546875, -0.34375, -0.33203125, -0.3203125, -0.30859375, -0.296875, -0.28515625, -0.2734375, -0.26171875, -0.25, -0.23828125, -0.2265625, -0.21484375, -0.203125, -0.19140625, -0.1796875, -0.16796875, -0.15625, -0.14453125, -0.1328125], "ap_positions": [[0.25, 0.5, 0.75], [1.0, 1.25, 1.5], [2.0, 0.0, -0.5]], "rapid_frames": [[0.0, 0.01953125, 0.0390625, 0.05859375, 0.078125, 0.09765625, 0.1171875, 0.13671875, 0.15625, 0.17578125, 0.1953125, 0.21484375, 0.234375, 0.25390625, 0.2734375, 0.29296875], [0.05078125, 0.0703125, 0.08984375, 0.109375, 0.12890625, 0.1484375, 0.16796875, 0.1875, 0.20703125, 0.2265625, 0.24609375, 0.265625, 0.28515625, 0.3046875, 0.32421875, 0.34375], [0.1015625, 0.12109375, 0.140625, 0.16015625, 0.1796875, 0.19921875, 0.21875, 0.23828125, 0.2578125, 0.27734375, 0.296875, 0.31640625, 0.3359375, 0.35546875, 0.375, 0.39453125], [0.15234375, 0.171875, 0.19140625, 0.2109375, 0.23046875, 0.25, 0.26953125, 0.2890625, 0.30859375, 0.328125, 0.34765625, 0.3671875, 0.38671875, 0.40625, 0.42578125, 0.4453125], [0.203125, 0.22265625, 0.2421875, 0.26171875, 0.28125, 0.30078125, 0.3203125, 0.33984375, 0.359375, 0.37890625, 0.3984375, 0.41796875, 0.4375, 0.45703125, 0.4765625, 0.49609375], [0.25390625, 0.2734375, 0.29296875, 0.3125, 0.33203125, 0.3515625, 0.37109375, 0.390625, 0.41015625, 0.4296875, 0.44921875, 0.46875, 0.48828125, 0.5078125, 0.52734375, 0.546875], [0.3046875, 0.32421875, 0.34375, 0.36328125, 0.3828125, 0.40234375, 0.421875, 0.44140625, 0.4609375, 0.48046875, 0.5, 0.51953125, 0.5390625, 0.55859375, 0.578125, 0.59765625], [0.35546875, 0.375, 0.39453125, 0.4140625, 0.43359375, 0.453125, 0.47265625, 0.4921875, 0.51171875, 0.53125, 0.55078125, 0.5703125, 0.58984375, 0.609375, 0.62890625, 0.6484375], [0.40625, 0.42578125, 0.4453125, 0.46484375, 0.484375, 0.50390625, 0.5234375, 0.54296875, 0.5625, 0.58203125, 0.6015625, 0.62109375, 0.640625, 0.66015625, 0.6796875, 0.69921875], [0.45703125, 0.4765625, 0.49609375, 0.515625, 0.53515625, 0.5546875, 0.57421875, 0.59375, 0.61328125, 0.6328125, 0.65234375, 0.671875, 0.69140625, 0.7109375, 0.73046875, 0.75], [0.5078125, 0.52734375, 0.546875, 0.56640625, 0.5859375, 0.60546875, 0.625, 0.64453125, 0.6640625, 0.68359375, 0.703125, 0.72265625, 0.7421875, 0.76171875, 0.78125, 0.80078125], [0.55859375, 0.578125, 0.59765625, 0.6171875, 0.63671875, 0.65625, 0.67578125, 0.6953125, 0.71484375, 0.734375, 0.75390625, 0.7734375, 0.79296875, 0.8125, 0.83203125, 0.8515625]]}
|
||||
@@ -0,0 +1 @@
|
||||
0486402d5a860f459a319cd779ca44a112d8543442ae9ce9eb7b1a01780aee4b
|
||||
@@ -0,0 +1,178 @@
|
||||
//! ADR-185 §4.1 — MERIDIAN bit-for-bit parity: native-Rust reference half.
|
||||
//!
|
||||
//! Calls the canonical `wifi-densepose-signal::hardware_norm` +
|
||||
//! `wifi-densepose-train::{geometry,rapid_adapt}` code DIRECTLY (no PyO3)
|
||||
//! on the committed `tests/golden/meridian_input.json` fixture and locks
|
||||
//! the SHA-256 of the concatenated f32 outputs into
|
||||
//! `tests/golden/meridian_output.sha256`.
|
||||
//!
|
||||
//! Concatenation order (identical in the pytest half, tests/test_meridian.py):
|
||||
//! 1. esp32 canonical amplitude (56) 2. esp32 canonical phase (56)
|
||||
//! 3. intel5300 canonical amplitude 4. intel5300 canonical phase
|
||||
//! 5. geometry.encode(ap_positions) 6. rapid_adapt lora_weights
|
||||
//!
|
||||
//! Regenerate (only on an intentional Rust change): delete the .sha256 and
|
||||
//! re-run `cargo test --features meridian --test meridian_parity`.
|
||||
#![cfg(feature = "meridian")]
|
||||
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use serde_json::Value;
|
||||
use sha2::{Digest, Sha256};
|
||||
use wifi_densepose_signal::hardware_norm::{HardwareNormalizer, HardwareType};
|
||||
use wifi_densepose_train::geometry::{GeometryEncoder, MeridianGeometryConfig};
|
||||
use wifi_densepose_train::rapid_adapt::{AdaptationLoss, RapidAdaptation};
|
||||
|
||||
fn golden_dir() -> PathBuf {
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("tests")
|
||||
.join("golden")
|
||||
}
|
||||
|
||||
fn fixture() -> Value {
|
||||
let raw = fs::read_to_string(golden_dir().join("meridian_input.json"))
|
||||
.expect("read meridian_input.json fixture");
|
||||
serde_json::from_str(&raw).expect("parse meridian_input.json")
|
||||
}
|
||||
|
||||
fn f64_vec(v: &Value, key: &str) -> Vec<f64> {
|
||||
v[key]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|x| x.as_f64().unwrap())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn f32_frames(v: &Value, key: &str) -> Vec<Vec<f32>> {
|
||||
v[key]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|row| {
|
||||
row.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|x| x.as_f64().unwrap() as f32)
|
||||
.collect()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Compute the full concatenated MERIDIAN output vector, mirroring the
|
||||
/// Python binding's default construction exactly.
|
||||
fn meridian_output(fx: &Value) -> Vec<f32> {
|
||||
let mut out: Vec<f32> = Vec::new();
|
||||
|
||||
// 1–4: hardware normalization (default normalizer, canonical 56).
|
||||
let norm = HardwareNormalizer::new();
|
||||
let esp = norm
|
||||
.normalize(
|
||||
&f64_vec(fx, "esp32_amplitude"),
|
||||
&f64_vec(fx, "esp32_phase"),
|
||||
HardwareType::Esp32S3,
|
||||
)
|
||||
.unwrap();
|
||||
out.extend_from_slice(&esp.amplitude);
|
||||
out.extend_from_slice(&esp.phase);
|
||||
let intel = norm
|
||||
.normalize(
|
||||
&f64_vec(fx, "intel_amplitude"),
|
||||
&f64_vec(fx, "intel_phase"),
|
||||
HardwareType::Intel5300,
|
||||
)
|
||||
.unwrap();
|
||||
out.extend_from_slice(&intel.amplitude);
|
||||
out.extend_from_slice(&intel.phase);
|
||||
|
||||
// 5: geometry encoding (default config → 64-dim).
|
||||
let enc = GeometryEncoder::new(&MeridianGeometryConfig::default());
|
||||
let aps: Vec<[f32; 3]> = fx["ap_positions"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|p| {
|
||||
let a = p.as_array().unwrap();
|
||||
[
|
||||
a[0].as_f64().unwrap() as f32,
|
||||
a[1].as_f64().unwrap() as f32,
|
||||
a[2].as_f64().unwrap() as f32,
|
||||
]
|
||||
})
|
||||
.collect();
|
||||
out.extend_from_slice(&enc.encode(&aps));
|
||||
|
||||
// 6: rapid adaptation lora weights (Combined, epochs 5, lr 1e-3, λ 0.5).
|
||||
let mut ra = RapidAdaptation::new(
|
||||
10,
|
||||
4,
|
||||
AdaptationLoss::Combined {
|
||||
epochs: 5,
|
||||
lr: 0.001,
|
||||
lambda_ent: 0.5,
|
||||
},
|
||||
);
|
||||
for frame in f32_frames(fx, "rapid_frames") {
|
||||
ra.push_frame(&frame);
|
||||
}
|
||||
out.extend_from_slice(&ra.adapt().unwrap().lora_weights);
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
fn sha256_le(vals: &[f32]) -> String {
|
||||
let mut hasher = Sha256::new();
|
||||
for &x in vals {
|
||||
hasher.update(x.to_le_bytes());
|
||||
}
|
||||
hasher
|
||||
.finalize()
|
||||
.iter()
|
||||
.map(|b| format!("{b:02x}"))
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn native_canonical_frames_are_56_wide() {
|
||||
let fx = fixture();
|
||||
let norm = HardwareNormalizer::new();
|
||||
let esp = norm
|
||||
.normalize(
|
||||
&f64_vec(&fx, "esp32_amplitude"),
|
||||
&f64_vec(&fx, "esp32_phase"),
|
||||
HardwareType::Esp32S3,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(esp.amplitude.len(), 56);
|
||||
assert_eq!(esp.phase.len(), 56);
|
||||
let intel = norm
|
||||
.normalize(
|
||||
&f64_vec(&fx, "intel_amplitude"),
|
||||
&f64_vec(&fx, "intel_phase"),
|
||||
HardwareType::Intel5300,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(intel.amplitude.len(), 56);
|
||||
// 64-dim geometry vector.
|
||||
let enc = GeometryEncoder::new(&MeridianGeometryConfig::default());
|
||||
assert_eq!(enc.encode(&[[0.25, 0.5, 0.75]]).len(), 64);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn native_meridian_matches_committed_golden() {
|
||||
let got = sha256_le(&meridian_output(&fixture()));
|
||||
let path = golden_dir().join("meridian_output.sha256");
|
||||
match fs::read_to_string(&path) {
|
||||
Ok(expected) => assert_eq!(
|
||||
got,
|
||||
expected.trim(),
|
||||
"native MERIDIAN 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,160 @@
|
||||
"""ADR-185 P2 — MERIDIAN binding tests, incl. the §4.1 bit-for-bit parity gate.
|
||||
|
||||
The parity test packs the binding's concatenated outputs (2× canonical
|
||||
frame, geometry vector, rapid-adapt LoRA weights) to little-endian f32
|
||||
bytes and asserts SHA-256 equality with the golden produced by the
|
||||
native-Rust reference (`tests/meridian_parity.rs`). A mismatch is a
|
||||
release blocker.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import struct
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from wifi_densepose import meridian as mer
|
||||
|
||||
GOLDEN = Path(__file__).parent / "golden"
|
||||
|
||||
|
||||
def fixture() -> dict:
|
||||
return json.loads((GOLDEN / "meridian_input.json").read_text())
|
||||
|
||||
|
||||
# ─── HardwareType / HardwareNormalizer / CanonicalCsiFrame ───────────
|
||||
|
||||
def test_hardware_type_detect() -> None:
|
||||
assert mer.HardwareType.detect(64) == mer.HardwareType.Esp32S3
|
||||
assert mer.HardwareType.detect(30) == mer.HardwareType.Intel5300
|
||||
assert mer.HardwareType.detect(56) == mer.HardwareType.Atheros
|
||||
assert mer.HardwareType.detect(128) == mer.HardwareType.Generic
|
||||
|
||||
|
||||
def test_hardware_type_properties() -> None:
|
||||
assert mer.HardwareType.Esp32S3.subcarrier_count == 64
|
||||
assert mer.HardwareType.Esp32S3.mimo_streams == 1
|
||||
assert mer.HardwareType.Intel5300.mimo_streams == 3
|
||||
|
||||
|
||||
def test_normalize_shapes_and_hardware() -> None:
|
||||
fx = fixture()
|
||||
norm = mer.HardwareNormalizer()
|
||||
assert norm.canonical_subcarriers == 56
|
||||
frame = norm.normalize(fx["esp32_amplitude"], fx["esp32_phase"], mer.HardwareType.Esp32S3)
|
||||
assert len(frame.amplitude) == 56
|
||||
assert len(frame.phase) == 56
|
||||
assert frame.hardware_type == mer.HardwareType.Esp32S3
|
||||
|
||||
|
||||
def test_normalize_rejects_mismatched_lengths() -> None:
|
||||
norm = mer.HardwareNormalizer()
|
||||
with pytest.raises(ValueError):
|
||||
norm.normalize([1.0, 2.0], [1.0], mer.HardwareType.Generic)
|
||||
|
||||
|
||||
# ─── GeometryEncoder ─────────────────────────────────────────────────
|
||||
|
||||
def test_geometry_encode_dim_and_permutation_invariance() -> None:
|
||||
enc = mer.GeometryEncoder(mer.MeridianGeometryConfig())
|
||||
aps = [[0.25, 0.5, 0.75], [1.0, 1.25, 1.5], [2.0, 0.0, -0.5]]
|
||||
v = enc.encode(aps)
|
||||
assert len(v) == 64
|
||||
# DeepSets mean-pool is permutation-invariant.
|
||||
v_perm = enc.encode([aps[2], aps[0], aps[1]])
|
||||
assert max(abs(a - b) for a, b in zip(v, v_perm)) < 1e-5
|
||||
|
||||
|
||||
def test_geometry_encode_rejects_empty_and_bad_shape() -> None:
|
||||
enc = mer.GeometryEncoder()
|
||||
with pytest.raises(ValueError):
|
||||
enc.encode([])
|
||||
with pytest.raises(ValueError):
|
||||
enc.encode([[0.0, 1.0]]) # not 3 coords
|
||||
|
||||
|
||||
# ─── RapidAdaptation ─────────────────────────────────────────────────
|
||||
|
||||
def test_rapid_adaptation_adapt() -> None:
|
||||
fx = fixture()
|
||||
ra = mer.RapidAdaptation(
|
||||
min_calibration_frames=10, lora_rank=4, loss_kind="combined",
|
||||
epochs=5, lr=0.001, lambda_ent=0.5,
|
||||
)
|
||||
for frame in fx["rapid_frames"]:
|
||||
ra.push_frame(frame)
|
||||
assert ra.is_ready()
|
||||
assert ra.buffer_len == 12
|
||||
res = ra.adapt()
|
||||
assert res.frames_used == 12
|
||||
assert res.adaptation_epochs == 5
|
||||
assert len(res.lora_weights) == 2 * 16 * 4 # 2 * fdim * rank
|
||||
|
||||
|
||||
def test_rapid_adaptation_rejects_bad_loss_kind() -> None:
|
||||
with pytest.raises(ValueError):
|
||||
mer.RapidAdaptation(10, 4, loss_kind="nonsense")
|
||||
|
||||
|
||||
def test_rapid_adaptation_empty_buffer_raises() -> None:
|
||||
ra = mer.RapidAdaptation(1, 4)
|
||||
with pytest.raises(ValueError):
|
||||
ra.adapt()
|
||||
|
||||
|
||||
# ─── CrossDomainEvaluator ────────────────────────────────────────────
|
||||
|
||||
def test_cross_domain_evaluator_gap_ratio() -> None:
|
||||
ev = mer.CrossDomainEvaluator(1)
|
||||
preds = [
|
||||
([0.0, 0.0, 0.0], [1.0, 0.0, 0.0]), # domain 0, err 1
|
||||
([0.0, 0.0, 0.0], [2.0, 0.0, 0.0]), # domain 1, err 2
|
||||
]
|
||||
m = ev.evaluate(preds, [0, 1])
|
||||
assert abs(m["in_domain_mpjpe"] - 1.0) < 1e-6
|
||||
assert abs(m["cross_domain_mpjpe"] - 2.0) < 1e-6
|
||||
assert abs(m["domain_gap_ratio"] - 2.0) < 1e-6
|
||||
|
||||
|
||||
def test_mpjpe_module_fn() -> None:
|
||||
assert abs(mer.mpjpe([0.0, 0.0, 0.0], [3.0, 4.0, 0.0], 1) - 5.0) < 1e-6
|
||||
|
||||
|
||||
# ─── §4.1 bit-for-bit parity gate (release-blocking) ─────────────────
|
||||
|
||||
def test_bit_for_bit_parity_with_native_rust() -> None:
|
||||
fx = fixture()
|
||||
out: list[float] = []
|
||||
|
||||
norm = mer.HardwareNormalizer()
|
||||
esp = norm.normalize(fx["esp32_amplitude"], fx["esp32_phase"], mer.HardwareType.Esp32S3)
|
||||
out += list(esp.amplitude) + list(esp.phase)
|
||||
intel = norm.normalize(fx["intel_amplitude"], fx["intel_phase"], mer.HardwareType.Intel5300)
|
||||
out += list(intel.amplitude) + list(intel.phase)
|
||||
|
||||
enc = mer.GeometryEncoder(mer.MeridianGeometryConfig())
|
||||
out += list(enc.encode(fx["ap_positions"]))
|
||||
|
||||
ra = mer.RapidAdaptation(
|
||||
min_calibration_frames=10, lora_rank=4, loss_kind="combined",
|
||||
epochs=5, lr=0.001, lambda_ent=0.5,
|
||||
)
|
||||
for frame in fx["rapid_frames"]:
|
||||
ra.push_frame(frame)
|
||||
out += list(ra.adapt().lora_weights)
|
||||
|
||||
packed = b"".join(struct.pack("<f", x) for x in out)
|
||||
got = hashlib.sha256(packed).hexdigest()
|
||||
expected = (GOLDEN / "meridian_output.sha256").read_text().strip()
|
||||
assert got == expected, (
|
||||
f"Python binding MERIDIAN output diverged from native-Rust golden "
|
||||
f"({got} != {expected})"
|
||||
)
|
||||
|
||||
|
||||
def test_base_wheel_import_error_message() -> None:
|
||||
src = Path(mer.__file__).read_text()
|
||||
assert "pip install wifi-densepose[meridian]" in src
|
||||
@@ -0,0 +1,60 @@
|
||||
"""MERIDIAN — cross-environment domain generalization (ADR-027, ADR-185 P2).
|
||||
|
||||
Hardware-invariant CSI normalization, geometry-conditioned deployment,
|
||||
few-shot room adaptation, and cross-domain evaluation — the tch-free
|
||||
inference/adaptation path of Project MERIDIAN, computed by the Rust core.
|
||||
|
||||
Available **only** when the wheel was built with the ``[meridian]`` extra::
|
||||
|
||||
pip install wifi-densepose[meridian]
|
||||
|
||||
Quick start::
|
||||
|
||||
from wifi_densepose.meridian import HardwareNormalizer, HardwareType
|
||||
|
||||
norm = HardwareNormalizer() # canonical 56 subcarriers
|
||||
hw = HardwareType.detect(64) # -> HardwareType.Esp32S3
|
||||
frame = norm.normalize(amplitude, phase, hw) # -> CanonicalCsiFrame
|
||||
print(len(frame.amplitude), frame.hardware_type)
|
||||
|
||||
Note (honest scope, ADR-185 §3.3): the ADR's ``RapidAdaptation.calibrate``
|
||||
/ ``AdaptationResult.converged`` do not exist in the Rust core — use
|
||||
``push_frame(...)`` then ``adapt()``; the result exposes ``final_loss``,
|
||||
``frames_used``, ``adaptation_epochs``. Training-time types
|
||||
(DomainFactorizer, GradientReversalLayer, VirtualDomainAugmentor) are
|
||||
out of scope for P6 (they need the deferred libtorch training tier).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from wifi_densepose import _native
|
||||
|
||||
# MERIDIAN symbols are compiled into `_native` only under the Rust
|
||||
# `meridian` feature; absent in a base wheel (ADR-185 §6 acceptance).
|
||||
if not hasattr(_native, "HardwareNormalizer"):
|
||||
raise ImportError(
|
||||
"wifi_densepose.meridian is not available in this wheel. "
|
||||
"It requires the 'meridian' extra: pip install wifi-densepose[meridian]"
|
||||
)
|
||||
|
||||
HardwareType = _native.HardwareType
|
||||
CanonicalCsiFrame = _native.CanonicalCsiFrame
|
||||
HardwareNormalizer = _native.HardwareNormalizer
|
||||
MeridianGeometryConfig = _native.MeridianGeometryConfig
|
||||
GeometryEncoder = _native.GeometryEncoder
|
||||
RapidAdaptation = _native.RapidAdaptation
|
||||
AdaptationResult = _native.AdaptationResult
|
||||
CrossDomainEvaluator = _native.CrossDomainEvaluator
|
||||
mpjpe = _native.mpjpe
|
||||
|
||||
__all__ = [
|
||||
"HardwareType",
|
||||
"CanonicalCsiFrame",
|
||||
"HardwareNormalizer",
|
||||
"MeridianGeometryConfig",
|
||||
"GeometryEncoder",
|
||||
"RapidAdaptation",
|
||||
"AdaptationResult",
|
||||
"CrossDomainEvaluator",
|
||||
"mpjpe",
|
||||
]
|
||||
@@ -0,0 +1,105 @@
|
||||
"""Type stubs for the MERIDIAN bindings (ADR-185 P2).
|
||||
|
||||
Present only when the wheel is built with the ``[meridian]`` extra.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import enum
|
||||
|
||||
class HardwareType(enum.Enum):
|
||||
Esp32S3 = 0
|
||||
Intel5300 = 1
|
||||
Atheros = 2
|
||||
Generic = 3
|
||||
@staticmethod
|
||||
def detect(subcarrier_count: int) -> HardwareType: ...
|
||||
@property
|
||||
def subcarrier_count(self) -> int: ...
|
||||
@property
|
||||
def mimo_streams(self) -> int: ...
|
||||
def __repr__(self) -> str: ...
|
||||
|
||||
class CanonicalCsiFrame:
|
||||
@property
|
||||
def amplitude(self) -> list[float]: ...
|
||||
@property
|
||||
def phase(self) -> list[float]: ...
|
||||
@property
|
||||
def hardware_type(self) -> HardwareType: ...
|
||||
def __repr__(self) -> str: ...
|
||||
|
||||
class HardwareNormalizer:
|
||||
def __init__(self, canonical_subcarriers: int = ...) -> None: ...
|
||||
@staticmethod
|
||||
def detect_hardware(subcarrier_count: int) -> HardwareType: ...
|
||||
@property
|
||||
def canonical_subcarriers(self) -> int: ...
|
||||
def normalize(
|
||||
self, amplitude: list[float], phase: list[float], hardware: HardwareType
|
||||
) -> CanonicalCsiFrame: ...
|
||||
def __repr__(self) -> str: ...
|
||||
|
||||
class MeridianGeometryConfig:
|
||||
def __init__(
|
||||
self,
|
||||
n_frequencies: int = ...,
|
||||
scale: float = ...,
|
||||
geometry_dim: int = ...,
|
||||
seed: int = ...,
|
||||
) -> None: ...
|
||||
@property
|
||||
def n_frequencies(self) -> int: ...
|
||||
@property
|
||||
def scale(self) -> float: ...
|
||||
@property
|
||||
def geometry_dim(self) -> int: ...
|
||||
@property
|
||||
def seed(self) -> int: ...
|
||||
def __repr__(self) -> str: ...
|
||||
|
||||
class GeometryEncoder:
|
||||
def __init__(self, config: MeridianGeometryConfig | None = ...) -> None: ...
|
||||
def encode(self, ap_positions: list[list[float]]) -> list[float]: ...
|
||||
@property
|
||||
def geometry_dim(self) -> int: ...
|
||||
def __repr__(self) -> str: ...
|
||||
|
||||
class AdaptationResult:
|
||||
@property
|
||||
def lora_weights(self) -> list[float]: ...
|
||||
@property
|
||||
def final_loss(self) -> float: ...
|
||||
@property
|
||||
def frames_used(self) -> int: ...
|
||||
@property
|
||||
def adaptation_epochs(self) -> int: ...
|
||||
def __repr__(self) -> str: ...
|
||||
|
||||
class RapidAdaptation:
|
||||
def __init__(
|
||||
self,
|
||||
min_calibration_frames: int,
|
||||
lora_rank: int,
|
||||
loss_kind: str = ...,
|
||||
epochs: int = ...,
|
||||
lr: float = ...,
|
||||
lambda_ent: float = ...,
|
||||
) -> None: ...
|
||||
def push_frame(self, frame: list[float]) -> None: ...
|
||||
def is_ready(self) -> bool: ...
|
||||
@property
|
||||
def buffer_len(self) -> int: ...
|
||||
def adapt(self) -> AdaptationResult: ...
|
||||
def __repr__(self) -> str: ...
|
||||
|
||||
class CrossDomainEvaluator:
|
||||
def __init__(self, n_joints: int) -> None: ...
|
||||
def evaluate(
|
||||
self,
|
||||
predictions: list[tuple[list[float], list[float]]],
|
||||
domain_labels: list[int],
|
||||
) -> dict[str, float]: ...
|
||||
def __repr__(self) -> str: ...
|
||||
|
||||
def mpjpe(pred: list[float], gt: list[float], n_joints: int) -> float: ...
|
||||
Reference in New Issue
Block a user