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.
161 lines
5.6 KiB
Python
161 lines
5.6 KiB
Python
"""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
|