mirror of
https://github.com/ruvnet/RuView
synced 2026-07-23 17:33:20 +00:00
189ac9dfb0
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.
179 lines
5.4 KiB
Rust
179 lines
5.4 KiB
Rust
//! 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.");
|
||
}
|
||
}
|
||
}
|