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 committed 2026-07-21 16:49:49 -07:00
1 parent 189ac9dfb0
commit 1c9727f9cf
11 files changed
+1043

No files matched your search

+1
View File
@@ -0,0 +1 @@
{"stream": [{"amplitude": [10.0, 10.05, 10.1, 10.15, 10.2, 10.25, 10.3, 10.35, 10.4, 10.45, 10.5, 10.55, 10.6, 10.65, 10.7, 10.75, 10.8, 10.85, 10.9, 10.95, 11.0, 11.05, 11.1, 11.15, 11.2, 11.25, 11.3, 11.35, 11.4, 11.45, 11.5, 11.55, 11.6, 11.65, 11.7, 11.75, 11.8, 11.85, 11.9, 11.95, 12.0, 12.05, 12.1, 12.15, 12.2, 12.25, 12.3, 12.35, 12.4, 12.45, 12.5, 12.55, 12.6, 12.65, 12.7, 12.75], "phase": [0.0, 0.01, 0.02, 0.03, 0.04, 0.05, 0.06, 0.07, 0.08, 0.09, 0.1, 0.11, 0.12, 0.13, 0.14, 0.15, 0.16, 0.17, 0.18, 0.19, 0.2, 0.21, 0.22, 0.23, 0.24, 0.25, 0.26, 0.27, 0.28, 0.29, 0.3, 0.31, 0.32, 0.33, 0.34, 0.35000000000000003, 0.36, 0.37, 0.38, 0.39, 0.4, 0.41000000000000003, 0.42, 0.43, 0.44, 0.45, 0.46, 0.47000000000000003, 0.48, 0.49, 0.5, 0.51, 0.52, 0.53, 0.54, 0.55]}, {"amplitude": [10.298653992442432, 10.348653992442433, 10.398653992442432, 10.448653992442432, 10.498653992442431, 10.548653992442432, 10.598653992442433, 10.648653992442432, 10.698653992442432, 10.748653992442431, 10.798653992442432, 10.848653992442433, 10.898653992442432, 10.948653992442432, 10.998653992442431, 11.048653992442432, 11.098653992442433, 11.148653992442432, 11.198653992442432, 11.248653992442431, 11.298653992442432, 11.348653992442433, 11.398653992442432, 11.448653992442432, 11.498653992442431, 11.548653992442432, 11.598653992442433, 11.648653992442432, 11.698653992442432, 11.748653992442431, 11.798653992442432, 11.848653992442433, 11.898653992442432, 11.948653992442432, 11.998653992442431, 12.048653992442432, 12.098653992442433, 12.148653992442432, 12.198653992442432, 12.248653992442431, 12.298653992442432, 12.348653992442433, 12.398653992442432, 12.448653992442432, 12.498653992442431, 12.548653992442432, 12.598653992442433, 12.648653992442432, 12.698653992442432, 12.748653992442431, 12.798653992442432, 12.848653992442433, 12.898653992442432, 12.948653992442432, 12.998653992442431, 13.048653992442432], "phase": [0.009410831331851433, 0.01941083133185143, 0.029410831331851434, 0.03941083133185143, 0.04941083133185144, 0.05941083133185143, 0.06941083133185143, 0.07941083133185144, 0.08941083133185143, 0.09941083133185143, 0.10941083133185144, 0.11941083133185143, 0.12941083133185144, 0.13941083133185145, 0.14941083133185146, 0.15941083133185144, 0.16941083133185145, 0.17941083133185146, 0.18941083133185144, 0.19941083133185145, 0.20941083133185145, 0.21941083133185144, 0.22941083133185144, 0.23941083133185145, 0.24941083133185143, 0.25941083133185144, 0.26941083133185145, 0.27941083133185146, 0.28941083133185147, 0.2994108313318514, 0.30941083133185143, 0.31941083133185144, 0.32941083133185145, 0.33941083133185146, 0.34941083133185147, 0.3594108313318515, 0.36941083133185143, 0.37941083133185144, 0.38941083133185145, 0.39941083133185146, 0.40941083133185147, 0.4194108313318515, 0.42941083133185143, 0.43941083133185144, 0.44941083133185145, 0.45941083133185145, 0.46941083133185146, 0.4794108313318515, 0.4894108313318514, 0.49941083133185143, 0.5094108313318514, 0.5194108313318514, 0.5294108313318514, 0.5394108313318514, 0.5494108313318514, 0.5594108313318514]}, {"amplitude": [10.580126760950057, 10.630126760950057, 10.680126760950056, 10.730126760950057, 10.780126760950056, 10.830126760950057, 10.880126760950057, 10.930126760950056, 10.980126760950057, 11.030126760950056, 11.080126760950057, 11.130126760950057, 11.180126760950056, 11.230126760950057, 11.280126760950056, 11.330126760950057, 11.380126760950057, 11.430126760950056, 11.480126760950057, 11.530126760950056, 11.580126760950057, 11.630126760950057, 11.680126760950056, 11.730126760950057, 11.780126760950056, 11.830126760950057, 11.880126760950057, 11.930126760950056, 11.980126760950057, 12.030126760950056, 12.080126760950057, 12.130126760950057, 12.180126760950056, 12.230126760950057, 12.280126760950056, 12.330126760950057, 12.380126760950057, 12.430126760950056, 12.480126760950057, 12.530126760950056, 12.580126760950057, 12.630126760950057, 12.680126760950056, 12.730126760950057, 12.780126760950056, 12.830126760950057, 12.880126760950057, 12.930126760950056, 12.980126760950057, 13.030126760950056, 13.080126760950057, 13.130126760950057, 13.180126760950056, 13.230126760950057, 13.280126760950056, 13.330126760950057], "phase": [0.018738131458572463, 0.028738131458572465, 0.03873813145857247, 0.04873813145857246, 0.058738131458572464, 0.06873813145857247, 0.07873813145857246, 0.08873813145857247, 0.09873813145857246, 0.10873813145857246, 0.11873813145857247, 0.12873813145857246, 0.13873813145857244, 0.14873813145857245, 0.15873813145857246, 0.16873813145857247, 0.17873813145857248, 0.1887381314585725, 0.19873813145857244, 0.20873813145857245, 0.21873813145857246, 0.22873813145857247, 0.23873813145857248, 0.2487381314585725, 0.25873813145857244, 0.26873813145857245, 0.27873813145857246, 0.28873813145857247, 0.2987381314585725, 0.30873813145857243, 0.31873813145857244, 0.32873813145857245, 0.33873813145857246, 0.34873813145857246, 0.3587381314585725, 0.3687381314585725, 0.378Line truncated
+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