Files
wifi-densepose/vendor/ruvector/crates/cognitum-gate-tilezero/src/decision.rs

539 lines
17 KiB
Rust

//! Gate decision types, thresholds, and three-filter decision logic
//!
//! This module implements the three-filter decision process:
//! 1. Structural filter - based on min-cut analysis
//! 2. Shift filter - drift detection from expected patterns
//! 3. Evidence filter - confidence score threshold
//!
//! ## Performance Optimizations
//!
//! - VecDeque for O(1) history rotation (instead of Vec::remove(0))
//! - Inline score calculation functions
//! - Pre-computed threshold reciprocals for division optimization
//! - Early-exit evaluation order (most likely failures first)
use std::collections::VecDeque;
use serde::{Deserialize, Serialize};
use crate::supergraph::ReducedGraph;
/// Gate decision: Permit, Defer, or Deny
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum GateDecision {
/// Action is permitted - stable enough to proceed
Permit,
/// Action is deferred - uncertain, escalate to human/stronger model
Defer,
/// Action is denied - unstable or policy-violating
Deny,
}
impl std::fmt::Display for GateDecision {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
GateDecision::Permit => write!(f, "permit"),
GateDecision::Defer => write!(f, "defer"),
GateDecision::Deny => write!(f, "deny"),
}
}
}
/// Evidence filter decision
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EvidenceDecision {
/// Sufficient evidence of coherence
Accept,
/// Insufficient evidence either way
Continue,
/// Strong evidence of incoherence
Reject,
}
/// Filter type in the decision process
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum DecisionFilter {
/// Min-cut based structural analysis
Structural,
/// Drift detection from patterns
Shift,
/// Confidence/evidence threshold
Evidence,
}
impl std::fmt::Display for DecisionFilter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DecisionFilter::Structural => write!(f, "Structural"),
DecisionFilter::Shift => write!(f, "Shift"),
DecisionFilter::Evidence => write!(f, "Evidence"),
}
}
}
/// Outcome of the three-filter decision process
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DecisionOutcome {
/// The gate decision
pub decision: GateDecision,
/// Overall confidence score (0.0 - 1.0)
pub confidence: f64,
/// Which filter rejected (if any)
pub rejected_by: Option<DecisionFilter>,
/// Reason for rejection (if rejected)
pub rejection_reason: Option<String>,
/// Structural filter score
pub structural_score: f64,
/// Shift filter score
pub shift_score: f64,
/// Evidence filter score
pub evidence_score: f64,
/// Min-cut value from structural analysis
pub mincut_value: f64,
}
impl DecisionOutcome {
/// Create a permit outcome
#[inline]
pub fn permit(
confidence: f64,
structural: f64,
shift: f64,
evidence: f64,
mincut: f64,
) -> Self {
Self {
decision: GateDecision::Permit,
confidence,
rejected_by: None,
rejection_reason: None,
structural_score: structural,
shift_score: shift,
evidence_score: evidence,
mincut_value: mincut,
}
}
/// Create a deferred outcome
#[inline]
pub fn defer(
filter: DecisionFilter,
reason: String,
structural: f64,
shift: f64,
evidence: f64,
mincut: f64,
) -> Self {
// OPTIMIZATION: Multiply by reciprocal instead of divide
let confidence = (structural + shift + evidence) * (1.0 / 3.0);
Self {
decision: GateDecision::Defer,
confidence,
rejected_by: Some(filter),
rejection_reason: Some(reason),
structural_score: structural,
shift_score: shift,
evidence_score: evidence,
mincut_value: mincut,
}
}
/// Create a denied outcome
#[inline]
pub fn deny(
filter: DecisionFilter,
reason: String,
structural: f64,
shift: f64,
evidence: f64,
mincut: f64,
) -> Self {
// OPTIMIZATION: Multiply by reciprocal instead of divide
let confidence = (structural + shift + evidence) * (1.0 / 3.0);
Self {
decision: GateDecision::Deny,
confidence,
rejected_by: Some(filter),
rejection_reason: Some(reason),
structural_score: structural,
shift_score: shift,
evidence_score: evidence,
mincut_value: mincut,
}
}
}
/// Threshold configuration for the gate
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GateThresholds {
/// E-process level indicating incoherence (default: 0.01)
pub tau_deny: f64,
/// E-process level indicating coherence (default: 100.0)
pub tau_permit: f64,
/// Minimum cut value for structural stability
pub min_cut: f64,
/// Maximum shift pressure before deferral
pub max_shift: f64,
/// Permit token TTL in nanoseconds
pub permit_ttl_ns: u64,
/// Conformal set size requiring deferral
pub theta_uncertainty: f64,
/// Conformal set size for confident permit
pub theta_confidence: f64,
}
impl Default for GateThresholds {
fn default() -> Self {
Self {
tau_deny: 0.01,
tau_permit: 100.0,
min_cut: 5.0,
max_shift: 0.5,
permit_ttl_ns: 60_000_000_000, // 60 seconds
theta_uncertainty: 20.0,
theta_confidence: 5.0,
}
}
}
/// Three-filter decision evaluator
///
/// Implements the core decision logic for the coherence gate:
/// 1. Structural filter - checks min-cut stability
/// 2. Shift filter - detects drift from baseline
/// 3. Evidence filter - validates confidence threshold
///
/// OPTIMIZATION: Uses VecDeque for O(1) history rotation instead of Vec::remove(0)
pub struct ThreeFilterDecision {
/// Gate thresholds
thresholds: GateThresholds,
/// Pre-computed reciprocals for fast division
/// OPTIMIZATION: Avoid division in hot path
inv_min_cut: f64,
inv_max_shift: f64,
inv_tau_range: f64,
/// Historical baseline for shift detection
baseline_mincut: Option<f64>,
/// Window of recent mincut values for drift detection
/// OPTIMIZATION: VecDeque for O(1) push_back and pop_front
mincut_history: VecDeque<f64>,
/// Maximum history size
history_size: usize,
}
impl ThreeFilterDecision {
/// Create a new three-filter decision evaluator
pub fn new(thresholds: GateThresholds) -> Self {
// OPTIMIZATION: Pre-compute reciprocals for fast division
let inv_min_cut = 1.0 / thresholds.min_cut;
let inv_max_shift = 1.0 / thresholds.max_shift;
let inv_tau_range = 1.0 / (thresholds.tau_permit - thresholds.tau_deny);
Self {
thresholds,
inv_min_cut,
inv_max_shift,
inv_tau_range,
baseline_mincut: None,
// OPTIMIZATION: Use VecDeque for O(1) rotation
mincut_history: VecDeque::with_capacity(100),
history_size: 100,
}
}
/// Set baseline min-cut for shift detection
#[inline]
pub fn set_baseline(&mut self, baseline: f64) {
self.baseline_mincut = Some(baseline);
}
/// Update history with a new min-cut observation
///
/// OPTIMIZATION: Uses VecDeque for O(1) push/pop instead of Vec::remove(0) which is O(n)
#[inline]
pub fn observe_mincut(&mut self, mincut: f64) {
// OPTIMIZATION: VecDeque::push_back + pop_front is O(1)
if self.mincut_history.len() >= self.history_size {
self.mincut_history.pop_front();
}
self.mincut_history.push_back(mincut);
// Update baseline if not set
if self.baseline_mincut.is_none() && !self.mincut_history.is_empty() {
self.baseline_mincut = Some(self.compute_baseline());
}
}
/// Compute baseline from history
///
/// OPTIMIZATION: Uses iterator sum for cache-friendly access
#[inline]
fn compute_baseline(&self) -> f64 {
let len = self.mincut_history.len();
if len == 0 {
return 0.0;
}
let sum: f64 = self.mincut_history.iter().sum();
sum / len as f64
}
/// Evaluate a request against the three filters
///
/// OPTIMIZATION: Uses pre-computed reciprocals for division,
/// inline score calculations, early-exit on failures
#[inline]
pub fn evaluate(&self, graph: &ReducedGraph) -> DecisionOutcome {
let mincut_value = graph.global_cut();
let shift_pressure = graph.aggregate_shift_pressure();
let e_value = graph.aggregate_evidence();
// 1. Structural Filter - Min-cut analysis
// OPTIMIZATION: Use pre-computed reciprocal
let structural_score = self.compute_structural_score(mincut_value);
if mincut_value < self.thresholds.min_cut {
return DecisionOutcome::deny(
DecisionFilter::Structural,
format!(
"Min-cut {:.3} below threshold {:.3}",
mincut_value, self.thresholds.min_cut
),
structural_score,
0.0,
0.0,
mincut_value,
);
}
// 2. Shift Filter - Drift detection
// OPTIMIZATION: Use pre-computed reciprocal
let shift_score = self.compute_shift_score(shift_pressure);
if shift_pressure >= self.thresholds.max_shift {
return DecisionOutcome::defer(
DecisionFilter::Shift,
format!(
"Shift pressure {:.3} exceeds threshold {:.3}",
shift_pressure, self.thresholds.max_shift
),
structural_score,
shift_score,
0.0,
mincut_value,
);
}
// 3. Evidence Filter - E-value threshold
// OPTIMIZATION: Use pre-computed reciprocal
let evidence_score = self.compute_evidence_score(e_value);
if e_value < self.thresholds.tau_deny {
return DecisionOutcome::deny(
DecisionFilter::Evidence,
format!(
"E-value {:.3} below denial threshold {:.3}",
e_value, self.thresholds.tau_deny
),
structural_score,
shift_score,
evidence_score,
mincut_value,
);
}
if e_value < self.thresholds.tau_permit {
return DecisionOutcome::defer(
DecisionFilter::Evidence,
format!(
"E-value {:.3} below permit threshold {:.3}",
e_value, self.thresholds.tau_permit
),
structural_score,
shift_score,
evidence_score,
mincut_value,
);
}
// All filters passed
// OPTIMIZATION: Multiply by reciprocal
let confidence = (structural_score + shift_score + evidence_score) * (1.0 / 3.0);
DecisionOutcome::permit(
confidence,
structural_score,
shift_score,
evidence_score,
mincut_value,
)
}
/// Compute structural score from min-cut value
///
/// OPTIMIZATION: Uses pre-computed reciprocal, marked inline(always)
#[inline(always)]
fn compute_structural_score(&self, mincut_value: f64) -> f64 {
if mincut_value >= self.thresholds.min_cut {
1.0
} else {
// OPTIMIZATION: Multiply by reciprocal instead of divide
mincut_value * self.inv_min_cut
}
}
/// Compute shift score from shift pressure
///
/// OPTIMIZATION: Uses pre-computed reciprocal, marked inline(always)
#[inline(always)]
fn compute_shift_score(&self, shift_pressure: f64) -> f64 {
// OPTIMIZATION: Multiply by reciprocal, use f64::min for branchless
1.0 - (shift_pressure * self.inv_max_shift).min(1.0)
}
/// Compute evidence score from e-value
///
/// OPTIMIZATION: Uses pre-computed reciprocal, marked inline(always)
#[inline(always)]
fn compute_evidence_score(&self, e_value: f64) -> f64 {
if e_value >= self.thresholds.tau_permit {
1.0
} else if e_value <= self.thresholds.tau_deny {
0.0
} else {
// OPTIMIZATION: Multiply by reciprocal
(e_value - self.thresholds.tau_deny) * self.inv_tau_range
}
}
/// Get current thresholds
#[inline]
pub fn thresholds(&self) -> &GateThresholds {
&self.thresholds
}
/// Get history size
#[inline(always)]
pub fn history_len(&self) -> usize {
self.mincut_history.len()
}
/// Get current baseline
#[inline(always)]
pub fn baseline(&self) -> Option<f64> {
self.baseline_mincut
}
/// Update thresholds and recompute reciprocals
///
/// OPTIMIZATION: Recomputes cached reciprocals when thresholds change
pub fn update_thresholds(&mut self, thresholds: GateThresholds) {
self.inv_min_cut = 1.0 / thresholds.min_cut;
self.inv_max_shift = 1.0 / thresholds.max_shift;
self.inv_tau_range = 1.0 / (thresholds.tau_permit - thresholds.tau_deny);
self.thresholds = thresholds;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_gate_decision_display() {
assert_eq!(GateDecision::Permit.to_string(), "permit");
assert_eq!(GateDecision::Defer.to_string(), "defer");
assert_eq!(GateDecision::Deny.to_string(), "deny");
}
#[test]
fn test_default_thresholds() {
let thresholds = GateThresholds::default();
assert_eq!(thresholds.tau_deny, 0.01);
assert_eq!(thresholds.tau_permit, 100.0);
assert_eq!(thresholds.min_cut, 5.0);
}
#[test]
fn test_three_filter_decision() {
let thresholds = GateThresholds::default();
let decision = ThreeFilterDecision::new(thresholds);
// Default graph should permit
let graph = ReducedGraph::new();
let outcome = decision.evaluate(&graph);
// Default graph has high coherence, should permit
assert_eq!(outcome.decision, GateDecision::Permit);
}
#[test]
fn test_structural_denial() {
let thresholds = GateThresholds::default();
let decision = ThreeFilterDecision::new(thresholds);
let mut graph = ReducedGraph::new();
graph.set_global_cut(1.0); // Below min_cut of 5.0
let outcome = decision.evaluate(&graph);
assert_eq!(outcome.decision, GateDecision::Deny);
assert_eq!(outcome.rejected_by, Some(DecisionFilter::Structural));
}
#[test]
fn test_shift_deferral() {
let thresholds = GateThresholds::default();
let decision = ThreeFilterDecision::new(thresholds);
let mut graph = ReducedGraph::new();
graph.set_shift_pressure(0.8); // Above max_shift of 0.5
let outcome = decision.evaluate(&graph);
assert_eq!(outcome.decision, GateDecision::Defer);
assert_eq!(outcome.rejected_by, Some(DecisionFilter::Shift));
}
#[test]
fn test_evidence_deferral() {
let thresholds = GateThresholds::default();
let decision = ThreeFilterDecision::new(thresholds);
let mut graph = ReducedGraph::new();
graph.set_evidence(50.0); // Between tau_deny (0.01) and tau_permit (100.0)
let outcome = decision.evaluate(&graph);
assert_eq!(outcome.decision, GateDecision::Defer);
assert_eq!(outcome.rejected_by, Some(DecisionFilter::Evidence));
}
#[test]
fn test_decision_outcome_creation() {
let outcome = DecisionOutcome::permit(0.95, 1.0, 0.9, 0.95, 10.0);
assert_eq!(outcome.decision, GateDecision::Permit);
assert!(outcome.confidence > 0.9);
assert!(outcome.rejected_by.is_none());
}
#[test]
fn test_decision_filter_display() {
assert_eq!(DecisionFilter::Structural.to_string(), "Structural");
assert_eq!(DecisionFilter::Shift.to_string(), "Shift");
assert_eq!(DecisionFilter::Evidence.to_string(), "Evidence");
}
#[test]
fn test_baseline_observation() {
let thresholds = GateThresholds::default();
let mut decision = ThreeFilterDecision::new(thresholds);
assert!(decision.baseline().is_none());
decision.observe_mincut(10.0);
decision.observe_mincut(12.0);
decision.observe_mincut(8.0);
assert!(decision.baseline().is_some());
assert_eq!(decision.history_len(), 3);
}
}