135 lines
4.7 KiB
Rust
135 lines
4.7 KiB
Rust
//! `BfldPipeline::current_gate_action()` diagnostic surface. Operators
|
|
//! reading the pipeline state for monitoring need a stable, documented way
|
|
//! to observe gate transitions without touching the lower-level
|
|
//! `CoherenceGate` directly. ADR-121 §2.4 + ADR-118 §2.1.
|
|
//!
|
|
//! Iter 11 covered the gate state machine in isolation; this iter pins the
|
|
//! same transitions through the public `BfldPipeline` facade so the
|
|
//! operator-facing diagnostic surface stays correct as the pipeline evolves.
|
|
|
|
#![cfg(feature = "std")]
|
|
|
|
use wifi_densepose_bfld::coherence_gate::DEBOUNCE_NS;
|
|
use wifi_densepose_bfld::{
|
|
BfldConfig, BfldPipeline, GateAction, IdentityEmbedding, SensingInputs, EMBEDDING_DIM,
|
|
};
|
|
|
|
const NS_PER_SEC: u64 = 1_000_000_000;
|
|
|
|
fn inputs(timestamp_ns: u64, risk: [f32; 4]) -> SensingInputs {
|
|
let [sep, stab, consist, risk_conf] = risk;
|
|
SensingInputs {
|
|
timestamp_ns,
|
|
presence: true,
|
|
motion: 0.4,
|
|
person_count: 1,
|
|
sensing_confidence: 0.9,
|
|
sep,
|
|
stab,
|
|
consist,
|
|
risk_conf,
|
|
rf_signature_hash: None,
|
|
}
|
|
}
|
|
|
|
fn embedding() -> IdentityEmbedding {
|
|
IdentityEmbedding::from_raw([0.05; EMBEDDING_DIM])
|
|
}
|
|
|
|
#[test]
|
|
fn fresh_pipeline_starts_in_accept() {
|
|
let p = BfldPipeline::new(BfldConfig::new("seed-obs"));
|
|
assert_eq!(p.current_gate_action(), GateAction::Accept);
|
|
}
|
|
|
|
#[test]
|
|
fn low_risk_processing_stays_in_accept() {
|
|
let mut p = BfldPipeline::new(BfldConfig::new("seed-obs"));
|
|
for i in 0..3 {
|
|
let _ = p.process(
|
|
inputs(i * NS_PER_SEC, [0.1, 0.1, 0.1, 0.1]),
|
|
Some(embedding()),
|
|
);
|
|
}
|
|
assert_eq!(p.current_gate_action(), GateAction::Accept);
|
|
}
|
|
|
|
#[test]
|
|
fn first_high_risk_input_does_not_immediately_promote_gate() {
|
|
// High-risk score causes the gate to register a PENDING transition but
|
|
// not yet promote `current()` away from Accept — debounce hasn't elapsed.
|
|
let mut p = BfldPipeline::new(BfldConfig::new("seed-obs"));
|
|
let _ = p.process(inputs(0, [1.0, 1.0, 1.0, 0.8]), Some(embedding()));
|
|
assert_eq!(
|
|
p.current_gate_action(),
|
|
GateAction::Accept,
|
|
"single high-risk input must not promote past debounce",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn sustained_high_risk_promotes_gate_to_reject_after_debounce() {
|
|
let mut p = BfldPipeline::new(BfldConfig::new("seed-obs"));
|
|
let _ = p.process(inputs(0, [1.0, 1.0, 1.0, 0.8]), Some(embedding()));
|
|
// Second high-risk input at debounce + 1 ns — gate must promote to Reject.
|
|
let _ = p.process(
|
|
inputs(DEBOUNCE_NS + 1, [1.0, 1.0, 1.0, 0.8]),
|
|
Some(embedding()),
|
|
);
|
|
assert_eq!(p.current_gate_action(), GateAction::Reject);
|
|
}
|
|
|
|
#[test]
|
|
fn sustained_recalibrate_grade_score_reaches_recalibrate() {
|
|
let mut p = BfldPipeline::new(BfldConfig::new("seed-obs"));
|
|
let _ = p.process(inputs(0, [1.0, 1.0, 1.0, 1.0]), Some(embedding()));
|
|
let _ = p.process(
|
|
inputs(DEBOUNCE_NS + 1, [1.0, 1.0, 1.0, 1.0]),
|
|
Some(embedding()),
|
|
);
|
|
assert_eq!(p.current_gate_action(), GateAction::Recalibrate);
|
|
}
|
|
|
|
#[test]
|
|
fn returning_to_low_risk_restores_accept_via_hysteresis() {
|
|
// First push into PredictOnly state via 0.55-grade score (Accept→PredictOnly
|
|
// boundary at 0.5 + hysteresis 0.05 = 0.55).
|
|
let mut p = BfldPipeline::new(BfldConfig::new("seed-obs"));
|
|
// Score = 0.6^4 = 0.13 → still Accept. Need a different factor mix.
|
|
// For PredictOnly we need score in [0.5, 0.7). Using (0.9, 0.9, 0.9, 0.85)
|
|
// → 0.62 → PredictOnly band.
|
|
let _ = p.process(inputs(0, [0.9, 0.9, 0.9, 0.85]), Some(embedding()));
|
|
let _ = p.process(
|
|
inputs(DEBOUNCE_NS + 1, [0.9, 0.9, 0.9, 0.85]),
|
|
Some(embedding()),
|
|
);
|
|
assert_eq!(p.current_gate_action(), GateAction::PredictOnly);
|
|
|
|
// Drop to low risk — gate should fall back to Accept after debounce.
|
|
let _ = p.process(
|
|
inputs(2 * DEBOUNCE_NS, [0.1, 0.1, 0.1, 0.1]),
|
|
Some(embedding()),
|
|
);
|
|
let _ = p.process(
|
|
inputs(3 * DEBOUNCE_NS + 1, [0.1, 0.1, 0.1, 0.1]),
|
|
Some(embedding()),
|
|
);
|
|
assert_eq!(p.current_gate_action(), GateAction::Accept);
|
|
}
|
|
|
|
#[test]
|
|
fn current_gate_action_is_read_only_does_not_advance_state() {
|
|
// Operators should be able to poll current_gate_action() as often as
|
|
// they like without affecting pipeline state. Multiple reads between
|
|
// processes must return the same value AND the next process must see
|
|
// the same gate state.
|
|
let mut p = BfldPipeline::new(BfldConfig::new("seed-obs"));
|
|
let _ = p.process(inputs(0, [1.0, 1.0, 1.0, 0.8]), Some(embedding()));
|
|
let a = p.current_gate_action();
|
|
let b = p.current_gate_action();
|
|
let c = p.current_gate_action();
|
|
assert_eq!(a, b);
|
|
assert_eq!(b, c);
|
|
assert_eq!(a, GateAction::Accept, "still pending at t=0, not promoted");
|
|
}
|