diff --git a/python/tests/test_aether.py b/python/tests/test_aether.py index e2244e10..e4ea74e7 100644 --- a/python/tests/test_aether.py +++ b/python/tests/test_aether.py @@ -37,23 +37,41 @@ def load_input() -> list[list[float]]: def assert_embedding_matches_golden(embedding: list[float], golden_name: str) -> None: - """Assert `embedding` matches the committed golden vector within tolerance.""" + """Assert `embedding` matches the committed golden vector. + + Two independent checks, because a per-element tolerance alone is not enough: + - **Per element**, within atol+rtol — catches a single component drifting. + - **Whole-vector cosine** ≥ 1 - 1e-6 — catches a *coherent* shift that stays + inside the per-element bound on every component yet moves the vector as a + whole (the failure mode a loose element tolerance would hide). + + NaN/inf are rejected explicitly: `abs(nan - b) > tol` is False, so a bare + tolerance check would silently PASS an all-NaN embedding. Every value must be + finite first. + """ golden = json.loads((GOLDEN / golden_name).read_text()) assert len(embedding) == len(golden), ( f"{golden_name}: length {len(embedding)} != golden {len(golden)}" ) - worst = max( - (abs(a - b), i, a, b) - for i, (a, b) in enumerate(zip(embedding, golden)) - if abs(a - b) > PARITY_ATOL + PARITY_RTOL * abs(b) - ) if any( - abs(a - b) > PARITY_ATOL + PARITY_RTOL * abs(b) - for a, b in zip(embedding, golden) - ) else None - assert worst is None, ( - f"{golden_name}: element {worst[1]} diverged from native golden beyond " - f"tolerance (got {worst[2]}, golden {worst[3]}, |Δ|={worst[0]:.3e}) — " - "a real regression, not cross-arch f32 drift." + for i, x in enumerate(embedding): + assert math.isfinite(x), f"{golden_name}: element {i} is not finite ({x})" + + for i, (a, b) in enumerate(zip(embedding, golden)): + tol = PARITY_ATOL + PARITY_RTOL * abs(b) + assert abs(a - b) <= tol, ( + f"{golden_name}: element {i} diverged from native golden beyond " + f"tolerance (got {a}, golden {b}, |Δ|={abs(a - b):.3e}) — " + "a real regression, not cross-arch f32 drift." + ) + + dot = sum(a * b for a, b in zip(embedding, golden)) + na = math.sqrt(sum(a * a for a in embedding)) + nb = math.sqrt(sum(b * b for b in golden)) + cosine = dot / (na * nb) if na > 0 and nb > 0 else 0.0 + assert cosine >= 1.0 - 1e-6, ( + f"{golden_name}: whole-vector cosine similarity to the golden is " + f"{cosine:.9f} (< 1 - 1e-6) — a coherent shift the per-element " + "tolerance did not catch." ) diff --git a/v2/crates/wifi-densepose-aether/Cargo.toml b/v2/crates/wifi-densepose-aether/Cargo.toml index 752a0be0..bd88d67c 100644 --- a/v2/crates/wifi-densepose-aether/Cargo.toml +++ b/v2/crates/wifi-densepose-aether/Cargo.toml @@ -16,6 +16,11 @@ categories.workspace = true # ruvector server tree. Keep it std-only. [dependencies] +# Test-only: parses the committed golden fixtures shared with the Python parity +# tests. Never linked into the library or the wheel. +[dev-dependencies] +serde_json = "1" + [lib] name = "wifi_densepose_aether" path = "src/lib.rs" diff --git a/v2/crates/wifi-densepose-aether/tests/golden_parity.rs b/v2/crates/wifi-densepose-aether/tests/golden_parity.rs new file mode 100644 index 00000000..5458601a --- /dev/null +++ b/v2/crates/wifi-densepose-aether/tests/golden_parity.rs @@ -0,0 +1,115 @@ +//! ADR-185 §4.1 — NATIVE half of the AETHER parity gate, and the half that runs +//! in CI. +//! +//! The committed golden vectors live under `python/tests/golden/` and are +//! shared with `python/tests/test_aether.py` (the binding half). That pytest +//! runs in python-ci; the native reference tests in `python/tests/*.rs` link +//! against the PyO3 crate and are NOT run by any workflow. This test closes that +//! gap: it recomputes the embedding through THIS std-only crate — no PyO3, no +//! marshalling — and asserts it matches the same golden within tolerance. +//! +//! Together with the pytest half: native≈golden AND binding≈golden ⇒ +//! binding≈native, portably. And because this side has no marshalling, a +//! binding-specific defect baked into the golden would surface here as a native +//! mismatch — which is the failure mode a binding-derived golden + pytest-only +//! CI would otherwise hide. +//! +//! Runs under the repo's `cargo test --workspace` (this crate is a member). + +use std::path::PathBuf; + +use wifi_densepose_aether::embedding::{EmbeddingConfig, EmbeddingExtractor}; +use wifi_densepose_aether::graph_transformer::TransformerConfig; + +// Same tolerance as the Python and .rs parity tests. f32 + transcendentals are +// not bit-reproducible across arch, so the golden (generated on one machine) is +// compared within a bound, not by hash. +const PARITY_ATOL: f32 = 1e-4; +const PARITY_RTOL: f32 = 1e-4; + +fn golden_dir() -> PathBuf { + // This crate lives at v2/crates/wifi-densepose-aether; the shared golden + // fixtures are the single source of truth under python/tests/golden. + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../../../python/tests/golden") +} + +fn read_vec(name: &str) -> Vec { + let raw = std::fs::read_to_string(golden_dir().join(name)) + .unwrap_or_else(|e| panic!("read {name}: {e}")); + serde_json::from_str(&raw).unwrap_or_else(|e| panic!("parse {name}: {e}")) +} + +fn load_input() -> Vec> { + let raw = std::fs::read_to_string(golden_dir().join("aether_input.json")) + .expect("read aether_input.json"); + let rows: Vec> = serde_json::from_str(&raw).expect("parse aether_input.json"); + rows.into_iter() + .map(|r| r.into_iter().map(|x| x as f32).collect()) + .collect() +} + +fn extractor() -> EmbeddingExtractor { + let e = EmbeddingConfig { d_model: 64, d_proj: 128, temperature: 0.07, normalize: true }; + let t = TransformerConfig { + n_subcarriers: 56, + n_keypoints: 17, + d_model: 64, + n_heads: 4, + n_gnn_layers: 2, + }; + EmbeddingExtractor::new(t, e) +} + +fn formula_weights(n: usize) -> Vec { + (0..n) + .map(|i| ((i as u64 * 1103515245 + 12345) % 65536) as f32 / 65536.0 - 0.5) + .collect() +} + +fn assert_matches_golden(embedding: &[f32], name: &str) { + let golden = read_vec(name); + assert_eq!(embedding.len(), golden.len(), "{name}: length mismatch"); + for (i, (&a, &b)) in embedding.iter().zip(&golden).enumerate() { + assert!(a.is_finite(), "{name}: element {i} is not finite ({a})"); + let tol = PARITY_ATOL + PARITY_RTOL * b.abs(); + assert!( + (a - b).abs() <= tol, + "{name}: element {i} diverged beyond tolerance \ + (got {a}, golden {b}, |Δ|={}) — real regression, not arch drift", + (a - b).abs() + ); + } +} + +#[test] +fn native_base_embedding_matches_committed_golden() { + let emb = extractor().extract(&load_input()); + assert_matches_golden(&emb, "aether_embedding.json"); +} + +#[test] +fn native_loaded_embedding_matches_committed_golden() { + let input = load_input(); + let mut ext = extractor(); + let baseline = ext.extract(&input); + + let weights = formula_weights(ext.param_count()); + let mut buf = Vec::new(); + buf.extend_from_slice(b"AETHERW1"); + buf.extend_from_slice(&(weights.len() as u32).to_le_bytes()); + for w in &weights { + buf.extend_from_slice(&w.to_le_bytes()); + } + let wpath = std::env::temp_dir().join(format!("aether_golden_parity_{}.bin", std::process::id())); + std::fs::write(&wpath, &buf).expect("write weights"); + ext.load_weights(&wpath).expect("load_weights"); + let _ = std::fs::remove_file(&wpath); + + let loaded = ext.extract(&input); + assert!( + baseline.iter().zip(&loaded).any(|(a, b)| (a - b).abs() > 1e-6), + "load_weights had no effect vs the random-init baseline" + ); + assert_matches_golden(&loaded, "aether_loaded_embedding.json"); +}