mirror of
https://github.com/ruvnet/RuView
synced 2026-07-23 17:33:20 +00:00
1c9727f9cf
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).
127 lines
4.3 KiB
Rust
127 lines
4.3 KiB
Rust
//! 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(&, &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.");
|
|
}
|
|
}
|
|
}
|