Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'
This commit is contained in:
538
vendor/ruvector/crates/cognitum-gate-tilezero/src/decision.rs
vendored
Normal file
538
vendor/ruvector/crates/cognitum-gate-tilezero/src/decision.rs
vendored
Normal file
@@ -0,0 +1,538 @@
|
||||
//! 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user