feat(adr-185): P3 MAT bindings (wifi_densepose.mat) + parity harness

Bind the ADR-024 MAT (Mass Casualty Assessment Tool) disaster-survivor
detection + START triage surface into the wheel behind a gated [mat]
extra / Cargo `mat` feature, mirroring the upstream disaster/ML gating.
Also adds the [sota] superset extra (aether+meridian+mat).

Surface (bound against the REAL code at HEAD, not the ADR wishlist):
- DisasterType (9 variants) / TriageStatus (5, START) enums
- DisasterConfig (builder-backed, continuous_monitoring forced off)
- DisasterResponse: initialize_event / add_zone / push_csi_data /
  scan_once / survivors / survivors_by_triage
- Survivor (id, triage_status, confidence, location, latest_vitals)
- VitalSignsReading (breathing/heartbeat rate, movement, confidence)
- ScanZone.rectangle / ScanZone.circle
push_csi_data + scan_once are GIL-released.

Honest deviations from ADR section 3.4 (documented in module header):
- ADR proposed adding a Rust-side sync scan_once() (section 11.3). That was
  UNNECESSARY: the public async start_scanning() runs exactly one
  scan_cycle and returns when continuous_monitoring == false. The binding
  forces that flag off and drives one cycle on a private current-thread
  tokio runtime -- NO change to wifi-densepose-mat.
- scan_cycle requires an active event + Active zone, which the ADR surface
  omitted; initialize_event + add_zone are bound as required additions.
- Survivor.vital_signs is a *history* in the real code; bound as
  Survivor.latest_vitals -> Optional[VitalSignsReading].
- DisasterType has 9 variants at HEAD (adds Landslide/MineCollapse/
  Industrial/TunnelCollapse); all bound.

Parity (section 4.1, release-blocking): committed fixture mat_input.json
(synthetic breathing-modulated CSI stream) -> native Rust reference
(tests/mat_parity.rs, drives DisasterResponse directly) locks
tests/golden/mat_result.sha256 over a canonical
`count=<K>;triage_priorities=<sorted>` string (survivor UUIDs/timestamps
excluded as non-deterministic); pytest (tests/test_mat.py) runs the same
stream through the binding and asserts the identical hash. Both detect
exactly 1 survivor, triage Delayed. Honest: synthetic fixture proves
binding==native path equality, NOT live detection accuracy.

Verified:
  cargo test --features mat --test mat_parity -> 2/2 pass
  maturin develop --features mat + pytest tests/test_mat.py -> 7/7 pass
  default cargo build clean, 0 mat/tokio refs in the default dep graph.

WHEEL-SIZE FINDING (ADR-185 section 9): default-features=false drops MAT's
`api` (axum) and `ruvector` features, but MAT still carries NON-optional
tokio (rt/sync/time), wifi-densepose-nn (ort/ONNX + reqwest/hyper),
rustfft, geo, ndarray. So a [mat] wheel exceeds the ADR-117 section 5.4
<=5 MB budget -- same leaf-crate-hoist follow-up as AETHER/MERIDIAN. The
default wheel is untouched (feature-gated).
This commit is contained in:
ruv
2026-07-21 16:49:49 -07:00
parent 189ac9dfb0
commit 1c9727f9cf
11 changed files with 1043 additions and 0 deletions
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
ae46351e28c01161c3c20f1a3134d8e32fbbef0cda2fdb9c3a27984da0ce026d
+126
View File
@@ -0,0 +1,126 @@
//! ADR-185 §4.1 — MAT bit-for-bit parity: native-Rust reference half.
//!
//! Drives the canonical `wifi-densepose-mat` `DisasterResponse` pipeline
//! DIRECTLY (no PyO3) over the committed `tests/golden/mat_input.json` CSI
//! stream and locks the SHA-256 of a canonical result string
//! (`count=<K>;triage_priorities=<sorted>`) into
//! `tests/golden/mat_result.sha256`.
//!
//! Only the survivor **count** and **triage classes** are hashed — survivor
//! UUIDs and event timestamps are non-deterministic and deliberately
//! excluded, so the hash captures exactly the "identical triage +
//! survivor count for a fixed CSI stream" invariant of ADR-185 §4.1.
//!
//! Honesty note: the fixture is a synthetic breathing-modulated stream, so
//! this proves the Python binding drives the real pipeline byte-identically
//! to native Rust — it is NOT a detection-accuracy claim on real rubble.
//!
//! Regenerate (only on an intentional Rust change): delete the .sha256 and
//! re-run `cargo test --features mat --test mat_parity`.
#![cfg(feature = "mat")]
use std::fs;
use std::path::PathBuf;
use serde_json::Value;
use sha2::{Digest, Sha256};
use wifi_densepose_mat::{DisasterConfig, DisasterResponse, DisasterType, ScanZone, ZoneBounds};
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("mat_input.json"))
.expect("read mat_input.json fixture");
serde_json::from_str(&raw).expect("parse mat_input.json")
}
/// Build the response identically to the Python binding's default
/// construction and run one scan over the fixture CSI stream. Returns the
/// canonical `count=<K>;triage_priorities=<sorted>` string.
fn mat_canonical_result(fx: &Value) -> String {
let config = DisasterConfig::builder()
.disaster_type(DisasterType::Earthquake)
.sensitivity(0.9)
.confidence_threshold(0.1)
.max_depth(5.0)
.continuous_monitoring(false)
.build();
let mut resp = DisasterResponse::new(config);
resp.initialize_event(geo::Point::new(0.0, 0.0), "parity-fixture")
.expect("initialize_event");
resp.add_zone(ScanZone::new(
"Zone A",
ZoneBounds::rectangle(0.0, 0.0, 50.0, 30.0),
))
.expect("add_zone");
for frame in fx["stream"].as_array().unwrap() {
let amp: Vec<f64> = frame["amplitude"]
.as_array()
.unwrap()
.iter()
.map(|x| x.as_f64().unwrap())
.collect();
let ph: Vec<f64> = frame["phase"]
.as_array()
.unwrap()
.iter()
.map(|x| x.as_f64().unwrap())
.collect();
resp.push_csi_data(&amp, &ph).expect("push_csi_data");
}
let rt = tokio::runtime::Builder::new_current_thread()
.enable_time()
.build()
.unwrap();
rt.block_on(resp.start_scanning()).expect("scan");
let survivors = resp.survivors();
let mut priorities: Vec<u8> = survivors
.iter()
.map(|s| s.triage_status().priority())
.collect();
priorities.sort_unstable();
format!("count={};triage_priorities={:?}", survivors.len(), priorities)
}
fn sha256_hex(s: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(s.as_bytes());
hasher
.finalize()
.iter()
.map(|b| format!("{b:02x}"))
.collect()
}
#[test]
fn native_mat_result_is_deterministic() {
let fx = fixture();
// Two independent runs must agree (survivor count + triage classes are
// deterministic; UUIDs/timestamps are excluded from the canonical form).
assert_eq!(mat_canonical_result(&fx), mat_canonical_result(&fx));
}
#[test]
fn native_mat_matches_committed_golden() {
let canon = mat_canonical_result(&fixture());
let got = sha256_hex(&canon);
let path = golden_dir().join("mat_result.sha256");
match fs::read_to_string(&path) {
Ok(expected) => assert_eq!(
got,
expected.trim(),
"native MAT result drifted from committed golden (canonical form: {canon})"
),
Err(_) => {
fs::write(&path, &got).expect("write golden sha256");
panic!("no committed golden found; wrote {got} for [{canon}]. Re-run to verify.");
}
}
}
+108
View File
@@ -0,0 +1,108 @@
"""ADR-185 P3 — MAT binding tests, incl. the §4.1 bit-for-bit parity gate.
The parity test drives the same committed CSI stream through the binding's
DisasterResponse pipeline and asserts the survivor count + triage classes
(as a SHA-256 of a canonical string) match the native-Rust golden. A
mismatch is a release blocker.
"""
from __future__ import annotations
import hashlib
import json
from pathlib import Path
import pytest
from wifi_densepose import mat
GOLDEN = Path(__file__).parent / "golden"
def fixture() -> dict:
return json.loads((GOLDEN / "mat_input.json").read_text())
def build_response() -> mat.DisasterResponse:
cfg = mat.DisasterConfig(
mat.DisasterType.Earthquake,
sensitivity=0.9,
confidence_threshold=0.1,
max_depth=5.0,
)
resp = mat.DisasterResponse(cfg)
resp.initialize_event(0.0, 0.0, "parity-fixture")
resp.add_zone(mat.ScanZone.rectangle("Zone A", 0.0, 0.0, 50.0, 30.0))
return resp
def run_scan(resp: mat.DisasterResponse) -> None:
for frame in fixture()["stream"]:
resp.push_csi_data(frame["amplitude"], frame["phase"])
resp.scan_once()
# ─── enums / config ──────────────────────────────────────────────────
def test_triage_priority_order() -> None:
assert mat.TriageStatus.Immediate.priority == 1
assert mat.TriageStatus.Delayed.priority == 2
assert mat.TriageStatus.Unknown.priority == 5
def test_disaster_config_fields() -> None:
cfg = mat.DisasterConfig(mat.DisasterType.Flood, sensitivity=1.5, confidence_threshold=0.3)
assert cfg.sensitivity == 1.0 # clamped to [0, 1]
assert abs(cfg.confidence_threshold - 0.3) < 1e-9
# ─── pipeline behaviour ──────────────────────────────────────────────
def test_scan_requires_event() -> None:
resp = mat.DisasterResponse(mat.DisasterConfig(mat.DisasterType.Unknown))
# No initialize_event / add_zone -> scan_cycle errors "No active event".
with pytest.raises(ValueError):
resp.scan_once()
def test_push_csi_rejects_mismatched_lengths() -> None:
resp = build_response()
with pytest.raises(ValueError):
resp.push_csi_data([1.0, 2.0], [1.0])
def test_scan_detects_survivor_from_breathing_stream() -> None:
resp = build_response()
run_scan(resp)
survivors = resp.survivors()
# The synthetic breathing-modulated stream trips one detection (matches
# the native-Rust reference).
assert len(survivors) == 1
s = survivors[0]
assert isinstance(s.id, str) and len(s.id) > 0
assert s.triage_status == mat.TriageStatus.Delayed
assert 0.0 <= s.confidence <= 1.0
# survivors_by_triage is consistent with the survivor's own class.
assert len(resp.survivors_by_triage(mat.TriageStatus.Delayed)) == 1
assert len(resp.survivors_by_triage(mat.TriageStatus.Immediate)) == 0
# ─── §4.1 bit-for-bit parity gate (release-blocking) ─────────────────
def test_bit_for_bit_parity_with_native_rust() -> None:
resp = build_response()
run_scan(resp)
survivors = resp.survivors()
priorities = sorted(s.triage_status.priority for s in survivors)
canon = f"count={len(survivors)};triage_priorities={priorities}"
got = hashlib.sha256(canon.encode()).hexdigest()
expected = (GOLDEN / "mat_result.sha256").read_text().strip()
assert got == expected, (
f"Python MAT result diverged from native-Rust golden "
f"(canonical form: {canon}; {got} != {expected})"
)
def test_base_wheel_import_error_message() -> None:
src = Path(mat.__file__).read_text()
assert "pip install wifi-densepose[mat]" in src