feat(engine): integrate ADR-135..141 into an end-to-end trust pipeline
- signal/calibration.rs: BaselineCalibration gains calibration_id()/ calibration_uuid()/apply() — the ADR-135->136 link that stamps FrameMeta.calibration_id (deterministic id, no serialization change). +1 test. - NEW crate wifi-densepose-engine: StreamingEngine::process_cycle() composes fuse_scored (137) -> calibration provenance (135/136) -> privacy demotion on contradiction (141) -> WorldGraph SemanticState with mandatory provenance + DerivedFrom edge (139). Returns TrustedOutput (the trust chain made concrete). - Validates the throughline: every output names evidence + model + calibration + privacy decision; calibration_id flows input->QualityScore->provenance; contradiction demotes class; deterministic; privacy mode attested. - 4 integration tests; workspace 0 errors; signal 410 lib tests pass. Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
parent
f2e9e2f2bd
commit
2eada40e3b
|
|
@ -10652,6 +10652,17 @@ dependencies = [
|
||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wifi-densepose-engine"
|
||||||
|
version = "0.3.0"
|
||||||
|
dependencies = [
|
||||||
|
"wifi-densepose-bfld",
|
||||||
|
"wifi-densepose-core",
|
||||||
|
"wifi-densepose-geo",
|
||||||
|
"wifi-densepose-signal",
|
||||||
|
"wifi-densepose-worldgraph",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wifi-densepose-geo"
|
name = "wifi-densepose-geo"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
@ -10826,6 +10837,7 @@ dependencies = [
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sha2",
|
"sha2",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
|
"uuid",
|
||||||
"wifi-densepose-core",
|
"wifi-densepose-core",
|
||||||
"wifi-densepose-ruvector",
|
"wifi-densepose-ruvector",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ members = [
|
||||||
"crates/wifi-densepose-pointcloud",
|
"crates/wifi-densepose-pointcloud",
|
||||||
"crates/wifi-densepose-geo",
|
"crates/wifi-densepose-geo",
|
||||||
"crates/wifi-densepose-worldgraph", # ADR-139 — WorldGraph environmental digital twin
|
"crates/wifi-densepose-worldgraph", # ADR-139 — WorldGraph environmental digital twin
|
||||||
|
"crates/wifi-densepose-engine", # ADR-135..146 integration/composition layer
|
||||||
"crates/nvsim",
|
"crates/nvsim",
|
||||||
"crates/nvsim-server",
|
"crates/nvsim-server",
|
||||||
"crates/homecore", # ADR-127 — HOMECORE state machine
|
"crates/homecore", # ADR-127 — HOMECORE state machine
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
[package]
|
||||||
|
name = "wifi-densepose-engine"
|
||||||
|
description = "RuView streaming-engine integration layer — composes the ADR-135..146 building blocks into one trust-traceable pipeline cycle"
|
||||||
|
version = "0.3.0"
|
||||||
|
edition.workspace = true
|
||||||
|
authors.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
repository.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
# Composed building blocks (ADR-135..146).
|
||||||
|
wifi-densepose-core = { version = "0.3.0", path = "../wifi-densepose-core" }
|
||||||
|
wifi-densepose-signal = { version = "0.3.1", path = "../wifi-densepose-signal", default-features = false }
|
||||||
|
# bfld is no_std by default; the privacy CONTROL PLANE (PrivacyModeRegistry) is
|
||||||
|
# std-gated, so request std explicitly even under a workspace --no-default-features build.
|
||||||
|
wifi-densepose-bfld = { version = "0.3.0", path = "../wifi-densepose-bfld", features = ["std"] }
|
||||||
|
wifi-densepose-worldgraph = { version = "0.3.0", path = "../wifi-densepose-worldgraph" }
|
||||||
|
wifi-densepose-geo = { path = "../wifi-densepose-geo" }
|
||||||
|
|
||||||
|
[lints.rust]
|
||||||
|
unsafe_code = "forbid"
|
||||||
|
missing_docs = "warn"
|
||||||
|
|
@ -0,0 +1,316 @@
|
||||||
|
//! # RuView Streaming Engine — integration layer
|
||||||
|
//!
|
||||||
|
//! This crate is the **composition root** that wires the ADR-135..146 building
|
||||||
|
//! blocks into one end-to-end *trust-traceable* pipeline cycle. Each block was
|
||||||
|
//! built and unit-tested independently; this crate proves they compose and that
|
||||||
|
//! the **trust throughline** holds end-to-end:
|
||||||
|
//!
|
||||||
|
//! > *Why believe the system when it says a person is present?* — every
|
||||||
|
//! > [`TrustedOutput`] names its **signal evidence** (ADR-137 `EvidenceRef`),
|
||||||
|
//! > its **model version** (ADR-136), its **calibration version** (ADR-135
|
||||||
|
//! > baseline id, ADR-136 `calibration_id`), and the **privacy decision**
|
||||||
|
//! > (ADR-141 mode → class) it was emitted under — and is anchored as a
|
||||||
|
//! > provenance-bearing node in the ADR-139 WorldGraph.
|
||||||
|
//!
|
||||||
|
//! One [`StreamingEngine::process_cycle`] performs, in order:
|
||||||
|
//! 1. **Fuse + score** the node frames (ADR-137 `fuse_scored`) → `QualityScore`
|
||||||
|
//! with per-node weights, evidence, and tolerated contradiction flags.
|
||||||
|
//! 2. **Stamp calibration provenance** (ADR-135/136): the `CalibrationId` the
|
||||||
|
//! calibration stage applied is recorded on the `QualityScore`.
|
||||||
|
//! 3. **Privacy control plane** (ADR-141): if the fusion recorded a tolerated
|
||||||
|
//! contradiction, the active privacy class is **demoted one step** before
|
||||||
|
//! emission (monotonic — information only ever removed).
|
||||||
|
//! 4. **Semantic state** (ADR-139/140): a `SemanticState` node is appended to
|
||||||
|
//! the WorldGraph with mandatory provenance and a `DerivedFrom` edge to the
|
||||||
|
//! room it was observed in.
|
||||||
|
//!
|
||||||
|
//! What is intentionally *not* here: the live 20 Hz I/O loop (sensing-server),
|
||||||
|
//! UWB hardware (ADR-144), and model training (ADR-146). This is the
|
||||||
|
//! composition + validation layer those will plug into.
|
||||||
|
|
||||||
|
#![forbid(unsafe_code)]
|
||||||
|
|
||||||
|
use wifi_densepose_bfld::{PrivacyClass, PrivacyMode, PrivacyModeRegistry};
|
||||||
|
use wifi_densepose_geo::types::GeoRegistration;
|
||||||
|
use wifi_densepose_signal::ruvsense::fusion_quality::CalibrationId;
|
||||||
|
use wifi_densepose_signal::ruvsense::multistatic::{MultistaticConfig, MultistaticFuser};
|
||||||
|
use wifi_densepose_signal::ruvsense::{MultiBandCsiFrame, QualityScore};
|
||||||
|
use wifi_densepose_worldgraph::{
|
||||||
|
EnuPoint, SemanticProvenance, WorldEdge, WorldGraph, WorldId, WorldNode, ZoneBoundsEnu,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Errors from an engine cycle.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum EngineError {
|
||||||
|
/// Multistatic fusion failed (no frames, timestamp spread, dimension mismatch).
|
||||||
|
Fusion(wifi_densepose_signal::ruvsense::multistatic::MultistaticError),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl core::fmt::Display for EngineError {
|
||||||
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
|
match self {
|
||||||
|
EngineError::Fusion(e) => write!(f, "fusion error: {e}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl std::error::Error for EngineError {}
|
||||||
|
impl From<wifi_densepose_signal::ruvsense::multistatic::MultistaticError> for EngineError {
|
||||||
|
fn from(e: wifi_densepose_signal::ruvsense::multistatic::MultistaticError) -> Self {
|
||||||
|
EngineError::Fusion(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The auditable result of one engine cycle — the trust chain made concrete.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct TrustedOutput {
|
||||||
|
/// The `SemanticState` node id created in the WorldGraph.
|
||||||
|
pub semantic_id: WorldId,
|
||||||
|
/// The fusion quality record (evidence + contradictions + calibration).
|
||||||
|
pub quality: QualityScore,
|
||||||
|
/// The privacy class the output was emitted under (after any demotion).
|
||||||
|
pub effective_class: PrivacyClass,
|
||||||
|
/// Whether a tolerated contradiction forced a privacy demotion this cycle.
|
||||||
|
pub demoted: bool,
|
||||||
|
/// The mandatory provenance attached to the semantic node.
|
||||||
|
pub provenance: SemanticProvenance,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Composition root for the RuView streaming engine.
|
||||||
|
pub struct StreamingEngine {
|
||||||
|
fuser: MultistaticFuser,
|
||||||
|
coherence_accept: f32,
|
||||||
|
privacy: PrivacyModeRegistry,
|
||||||
|
world: WorldGraph,
|
||||||
|
model_version: u16,
|
||||||
|
cycle: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StreamingEngine {
|
||||||
|
/// Build an engine with a starting privacy mode and model version. The
|
||||||
|
/// WorldGraph is registered to the installation origin.
|
||||||
|
#[must_use]
|
||||||
|
pub fn new(mode: PrivacyMode, model_version: u16, registration: GeoRegistration) -> Self {
|
||||||
|
Self {
|
||||||
|
fuser: MultistaticFuser::with_config(MultistaticConfig::default()),
|
||||||
|
coherence_accept: 0.85,
|
||||||
|
privacy: PrivacyModeRegistry::new(mode),
|
||||||
|
world: WorldGraph::new(registration),
|
||||||
|
model_version,
|
||||||
|
cycle: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Register a room and return its WorldGraph id (the observation scope).
|
||||||
|
pub fn add_room(&mut self, area_id: &str, name: &str) -> WorldId {
|
||||||
|
self.world.upsert_node(WorldNode::Room {
|
||||||
|
id: WorldId::UNASSIGNED,
|
||||||
|
area_id: Some(area_id.to_string()),
|
||||||
|
name: name.to_string(),
|
||||||
|
bounds_enu: ZoneBoundsEnu::Rectangle { min_e: 0.0, min_n: 0.0, max_e: 5.0, max_n: 4.0 },
|
||||||
|
floor: 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Register a sensor node and an `observes` edge to a room.
|
||||||
|
pub fn add_sensor(&mut self, device_id: &str, room: WorldId) -> WorldId {
|
||||||
|
let id = self.world.upsert_node(WorldNode::Sensor {
|
||||||
|
id: WorldId::UNASSIGNED,
|
||||||
|
device_id: device_id.to_string(),
|
||||||
|
position: EnuPoint { east_m: 0.0, north_m: 0.0, up_m: 0.0 },
|
||||||
|
modality: wifi_densepose_worldgraph::SensorModality::WifiCsi,
|
||||||
|
});
|
||||||
|
let _ = self.world.add_edge(
|
||||||
|
id,
|
||||||
|
room,
|
||||||
|
WorldEdge::Observes { quality: 1.0, last_seen_unix_ms: 0 },
|
||||||
|
);
|
||||||
|
id
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Switch the active privacy mode (records a hash-chained attestation).
|
||||||
|
pub fn set_privacy_mode(&mut self, mode: PrivacyMode) {
|
||||||
|
self.privacy.set_mode(mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Borrow the WorldGraph (for queries / persistence).
|
||||||
|
#[must_use]
|
||||||
|
pub fn world(&self) -> &WorldGraph {
|
||||||
|
&self.world
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Borrow the privacy registry (for attestation audit).
|
||||||
|
#[must_use]
|
||||||
|
pub fn privacy(&self) -> &PrivacyModeRegistry {
|
||||||
|
&self.privacy
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cycles processed so far.
|
||||||
|
#[must_use]
|
||||||
|
pub fn cycle_count(&self) -> u64 {
|
||||||
|
self.cycle
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run one full trust-traceable cycle (see crate docs for the steps).
|
||||||
|
///
|
||||||
|
/// `calibration` is the [`CalibrationId`] the calibration stage applied to
|
||||||
|
/// these frames (ADR-135 `BaselineCalibration::calibration_id()`); `room` is
|
||||||
|
/// the observation scope (an existing WorldGraph Room id).
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// [`EngineError::Fusion`] if multistatic fusion rejects the input.
|
||||||
|
pub fn process_cycle(
|
||||||
|
&mut self,
|
||||||
|
node_frames: &[MultiBandCsiFrame],
|
||||||
|
calibration: CalibrationId,
|
||||||
|
room: WorldId,
|
||||||
|
now_ms: i64,
|
||||||
|
) -> Result<TrustedOutput, EngineError> {
|
||||||
|
// 1. Fuse + score (ADR-137).
|
||||||
|
let (fused, mut quality) = self.fuser.fuse_scored(node_frames, self.coherence_accept)?;
|
||||||
|
|
||||||
|
// 2. Stamp calibration provenance (ADR-135 → ADR-136 → ADR-137).
|
||||||
|
quality.calibration_id = Some(calibration);
|
||||||
|
|
||||||
|
// 3. Privacy control plane (ADR-141): demote on contradiction.
|
||||||
|
let base_class = self.privacy.active_class();
|
||||||
|
let demoted = quality.forces_privacy_demotion();
|
||||||
|
let effective_class = if demoted { demote_one(base_class) } else { base_class };
|
||||||
|
|
||||||
|
// 4. Semantic state with mandatory provenance (ADR-139/140).
|
||||||
|
let provenance = SemanticProvenance {
|
||||||
|
evidence: quality.evidence_refs.iter().map(|e| format!("{e:?}")).collect(),
|
||||||
|
model_version: format!("rfenc-v{}", self.model_version),
|
||||||
|
calibration_version: format!("cal:{:016x}", calibration.0),
|
||||||
|
privacy_decision: format!("{:?}/{:?}", self.privacy.active_mode(), effective_class),
|
||||||
|
};
|
||||||
|
let statement = format!(
|
||||||
|
"occupancy coherence={:.2} nodes={} demoted={}",
|
||||||
|
quality.base_coherence, fused.active_nodes, demoted
|
||||||
|
);
|
||||||
|
let semantic_id = self.world.add_semantic_state(
|
||||||
|
statement,
|
||||||
|
quality.penalized_coherence(),
|
||||||
|
now_ms,
|
||||||
|
provenance.clone(),
|
||||||
|
&[room],
|
||||||
|
);
|
||||||
|
|
||||||
|
self.cycle += 1;
|
||||||
|
Ok(TrustedOutput { semantic_id, quality, effective_class, demoted, provenance })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Demote a privacy class by one step (more restrictive), clamped at `Restricted`.
|
||||||
|
/// Monotonic: information is only ever removed (ADR-120/141).
|
||||||
|
fn demote_one(c: PrivacyClass) -> PrivacyClass {
|
||||||
|
let next = (c.as_u8() + 1).min(PrivacyClass::Restricted.as_u8());
|
||||||
|
PrivacyClass::try_from(next).unwrap_or(PrivacyClass::Restricted)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use wifi_densepose_signal::hardware_norm::{CanonicalCsiFrame, HardwareType};
|
||||||
|
|
||||||
|
fn node_frame(node_id: u8, ts_us: u64, n_sub: usize) -> MultiBandCsiFrame {
|
||||||
|
MultiBandCsiFrame {
|
||||||
|
node_id,
|
||||||
|
timestamp_us: ts_us,
|
||||||
|
channel_frames: vec![CanonicalCsiFrame {
|
||||||
|
amplitude: (0..n_sub).map(|i| 1.0 + 0.1 * i as f32).collect(),
|
||||||
|
phase: (0..n_sub).map(|i| i as f32 * 0.05).collect(),
|
||||||
|
hardware_type: HardwareType::Esp32S3,
|
||||||
|
}],
|
||||||
|
frequencies_mhz: vec![2412],
|
||||||
|
coherence: 0.9,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn engine() -> (StreamingEngine, WorldId) {
|
||||||
|
let mut e = StreamingEngine::new(PrivacyMode::PrivateHome, 1, GeoRegistration::default());
|
||||||
|
let room = e.add_room("living_room", "Living Room");
|
||||||
|
e.add_sensor("esp32-com9", room);
|
||||||
|
(e, room)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// End-to-end trust invariant: a clean cycle produces a SemanticState whose
|
||||||
|
/// provenance names evidence + model + calibration + privacy decision, and
|
||||||
|
/// the calibration id flows from input → QualityScore → provenance.
|
||||||
|
#[test]
|
||||||
|
fn cycle_carries_full_provenance() {
|
||||||
|
let (mut e, room) = engine();
|
||||||
|
let cal = CalibrationId(0xABCD_1234);
|
||||||
|
let frames = [node_frame(0, 1000, 56), node_frame(1, 1001, 56)];
|
||||||
|
let out = e.process_cycle(&frames, cal, room, 10_000).unwrap();
|
||||||
|
|
||||||
|
// Calibration flows all the way through.
|
||||||
|
assert_eq!(out.quality.calibration_id, Some(cal));
|
||||||
|
assert_eq!(out.provenance.calibration_version, "cal:00000000abcd1234");
|
||||||
|
// Model + privacy provenance present.
|
||||||
|
assert_eq!(out.provenance.model_version, "rfenc-v1");
|
||||||
|
assert!(out.provenance.privacy_decision.starts_with("PrivateHome/"));
|
||||||
|
// Evidence refs recorded.
|
||||||
|
assert!(!out.provenance.evidence.is_empty());
|
||||||
|
// Clean cycle (tight timestamps) → no demotion, stays Anonymous (PrivateHome).
|
||||||
|
assert!(!out.demoted);
|
||||||
|
assert_eq!(out.effective_class, PrivacyClass::Anonymous);
|
||||||
|
|
||||||
|
// The SemanticState is in the graph with a DerivedFrom edge to the room.
|
||||||
|
assert!(e.world().node(out.semantic_id).is_some());
|
||||||
|
assert!(e
|
||||||
|
.world()
|
||||||
|
.neighbors(out.semantic_id)
|
||||||
|
.iter()
|
||||||
|
.any(|(to, edge)| *to == room && matches!(edge, WorldEdge::DerivedFrom { .. })));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A tolerated contradiction (loose timestamp spread, within the hard guard)
|
||||||
|
/// demotes the privacy class one step — proving ADR-137 → ADR-141 wiring.
|
||||||
|
#[test]
|
||||||
|
fn contradiction_demotes_privacy() {
|
||||||
|
let (mut e, room) = engine();
|
||||||
|
let cal = CalibrationId(7);
|
||||||
|
// 2 ms spread: within the 5 ms hard guard but above the 1 ms soft guard.
|
||||||
|
let frames = [node_frame(0, 1000, 56), node_frame(1, 3000, 56)];
|
||||||
|
let out = e.process_cycle(&frames, cal, room, 20_000).unwrap();
|
||||||
|
|
||||||
|
assert!(out.demoted, "loose alignment must demote");
|
||||||
|
// PrivateHome base = Anonymous(2) → demoted to Restricted(3).
|
||||||
|
assert_eq!(out.effective_class, PrivacyClass::Restricted);
|
||||||
|
assert!(out.provenance.privacy_decision.contains("Restricted"));
|
||||||
|
// Penalized coherence is below the base coherence.
|
||||||
|
assert!(out.quality.penalized_coherence() <= out.quality.base_coherence);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Determinism: identical input twice → identical provenance + class
|
||||||
|
/// (the ADR-136 witness-replay spirit, end-to-end through the engine).
|
||||||
|
#[test]
|
||||||
|
fn cycle_is_deterministic() {
|
||||||
|
let cal = CalibrationId(42);
|
||||||
|
let frames = [node_frame(0, 1000, 56), node_frame(1, 1001, 56)];
|
||||||
|
|
||||||
|
let (mut e1, r1) = engine();
|
||||||
|
let o1 = e1.process_cycle(&frames, cal, r1, 5_000).unwrap();
|
||||||
|
let (mut e2, r2) = engine();
|
||||||
|
let o2 = e2.process_cycle(&frames, cal, r2, 5_000).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(o1.provenance.calibration_version, o2.provenance.calibration_version);
|
||||||
|
assert_eq!(o1.provenance.evidence, o2.provenance.evidence);
|
||||||
|
assert_eq!(o1.effective_class, o2.effective_class);
|
||||||
|
assert_eq!(o1.quality.per_node_weights, o2.quality.per_node_weights);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The privacy mode switch is recorded in a verifiable attestation chain
|
||||||
|
/// (ADR-141), and a stricter mode raises the emitted class.
|
||||||
|
#[test]
|
||||||
|
fn privacy_mode_switch_is_attested_and_effective() {
|
||||||
|
let (mut e, room) = engine();
|
||||||
|
e.set_privacy_mode(PrivacyMode::StrictNoIdentity);
|
||||||
|
assert!(e.privacy().verify_chain());
|
||||||
|
let out = e
|
||||||
|
.process_cycle(&[node_frame(0, 1000, 56), node_frame(1, 1001, 56)], CalibrationId(1), room, 1)
|
||||||
|
.unwrap();
|
||||||
|
// StrictNoIdentity base = Restricted, even with no contradiction.
|
||||||
|
assert_eq!(out.effective_class, PrivacyClass::Restricted);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -46,6 +46,9 @@ ruvector-solver = { workspace = true }
|
||||||
midstreamer-temporal-compare = { workspace = true }
|
midstreamer-temporal-compare = { workspace = true }
|
||||||
midstreamer-attractor = { workspace = true }
|
midstreamer-attractor = { workspace = true }
|
||||||
|
|
||||||
|
# ADR-136: deterministic calibration-id → FrameMeta.calibration_id (ADR-135 link)
|
||||||
|
uuid = { version = "1.6", features = ["v4"] }
|
||||||
|
|
||||||
# Internal
|
# Internal
|
||||||
wifi-densepose-core = { version = "0.3.0", path = "../wifi-densepose-core" }
|
wifi-densepose-core = { version = "0.3.0", path = "../wifi-densepose-core" }
|
||||||
# ADR-084 Pass 2: sketch-prefilter for the EmbeddingHistory search loop.
|
# ADR-084 Pass 2: sketch-prefilter for the EmbeddingHistory search loop.
|
||||||
|
|
|
||||||
|
|
@ -260,6 +260,44 @@ impl BaselineCalibration {
|
||||||
Ok(CalibrationDeviationScore { amplitude_z_median, amplitude_z_max, phase_drift_median, motion_flagged })
|
Ok(CalibrationDeviationScore { amplitude_z_median, amplitude_z_max, phase_drift_median, motion_flagged })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Deterministic calibration epoch id (ADR-137 `CalibrationId`), derived
|
||||||
|
/// from the immutable baseline fields — stable across reboots, changes only
|
||||||
|
/// on recalibration. Deterministic (no RNG) so the ADR-136 witness replay
|
||||||
|
/// stays reproducible.
|
||||||
|
#[must_use]
|
||||||
|
pub fn calibration_id(&self) -> super::fusion_quality::CalibrationId {
|
||||||
|
// splitmix64 over (captured_at, frame_count, subcarrier_count, tier).
|
||||||
|
let mut h = (self.captured_at_unix_s as u64)
|
||||||
|
.wrapping_mul(0x9E37_79B9_7F4A_7C15)
|
||||||
|
.wrapping_add(self.frame_count.wrapping_mul(0xBF58_476D_1CE4_E5B9))
|
||||||
|
.wrapping_add((self.subcarriers.len() as u64).wrapping_mul(0x94D0_49BB_1331_11EB))
|
||||||
|
.wrapping_add(self.tier as u64);
|
||||||
|
h ^= h >> 30;
|
||||||
|
h = h.wrapping_mul(0xBF58_476D_1CE4_E5B9);
|
||||||
|
h ^= h >> 27;
|
||||||
|
super::fusion_quality::CalibrationId(h)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The ADR-136 `FrameMeta.calibration_id` value (a UUID derived
|
||||||
|
/// deterministically from [`Self::calibration_id`]).
|
||||||
|
#[must_use]
|
||||||
|
pub fn calibration_uuid(&self) -> uuid::Uuid {
|
||||||
|
uuid::Uuid::from_u128(self.calibration_id().0 as u128)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ADR-136 §2.4 calibration **Stage**: subtract the baseline AND stamp the
|
||||||
|
/// frame's `calibration_id` provenance field. This is the only place that
|
||||||
|
/// sets `calibration_id` (the append-only boundary rule).
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// [`CalibrationError::SubcarrierMismatch`] if the frame's subcarrier count
|
||||||
|
/// does not match this baseline.
|
||||||
|
pub fn apply(&self, frame: &mut CsiFrame) -> Result<(), CalibrationError> {
|
||||||
|
self.subtract_in_place(frame)?;
|
||||||
|
frame.metadata.set_calibration(self.calibration_uuid());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Subtract the amplitude baseline from `frame.data` in-place.
|
/// Subtract the amplitude baseline from `frame.data` in-place.
|
||||||
/// Only amplitude mean is subtracted; phase is left untouched.
|
/// Only amplitude mean is subtracted; phase is left untouched.
|
||||||
pub fn subtract_in_place(&self, frame: &mut CsiFrame) -> Result<(), CalibrationError> {
|
pub fn subtract_in_place(&self, frame: &mut CsiFrame) -> Result<(), CalibrationError> {
|
||||||
|
|
@ -597,6 +635,27 @@ mod tests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ADR-136: calibration Stage stamps calibration_id deterministically.
|
||||||
|
#[test]
|
||||||
|
fn apply_stamps_calibration_id_deterministically() {
|
||||||
|
let mut cfg = CalibrationConfig::ht20();
|
||||||
|
cfg.min_frames = 2;
|
||||||
|
let mut rec = CalibrationRecorder::new(cfg);
|
||||||
|
rec.record(&constant_frame(52, 0.8, 0.5)).unwrap();
|
||||||
|
rec.record(&constant_frame(52, 0.9, 0.6)).unwrap();
|
||||||
|
let baseline = rec.finalize().unwrap();
|
||||||
|
|
||||||
|
// id is stable across calls (no RNG).
|
||||||
|
assert_eq!(baseline.calibration_id(), baseline.calibration_id());
|
||||||
|
assert_eq!(baseline.calibration_uuid(), baseline.calibration_uuid());
|
||||||
|
|
||||||
|
// apply() subtracts AND stamps the frame's provenance field.
|
||||||
|
let mut frame = constant_frame(52, 1.0, 0.5);
|
||||||
|
assert_eq!(frame.metadata.calibration_id, None);
|
||||||
|
baseline.apply(&mut frame).unwrap();
|
||||||
|
assert_eq!(frame.metadata.calibration_id, Some(baseline.calibration_uuid()));
|
||||||
|
}
|
||||||
|
|
||||||
// (d) Tier dispatch: each config constructor produces the correct counts.
|
// (d) Tier dispatch: each config constructor produces the correct counts.
|
||||||
#[test]
|
#[test]
|
||||||
fn tier_dispatch_correct_counts() {
|
fn tier_dispatch_correct_counts() {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue