Files
wifi-densepose/examples/subpolynomial-time/src/fusion/structural_monitor.rs
ruv d803bfe2b1 Squashed 'vendor/ruvector/' content from commit b64c2172
git-subtree-dir: vendor/ruvector
git-subtree-split: b64c21726f2bb37286d9ee36a7869fef60cc6900
2026-02-28 14:39:40 -05:00

441 lines
13 KiB
Rust

//! Structural Monitor: Brittleness Detection
//!
//! Monitors the fusion graph's structural health by tracking minimum-cut
//! trends, volatility, and generating actionable triggers.
use std::collections::VecDeque;
/// Configuration for the structural monitor
#[derive(Debug, Clone)]
pub struct MonitorConfig {
/// Window size for trend analysis
pub window_size: usize,
/// Threshold for low-cut warning (λ_low)
pub lambda_low: f64,
/// Threshold for critical warning (λ_critical)
pub lambda_critical: f64,
/// Volatility threshold for instability warning
pub volatility_threshold: f64,
/// Trend slope threshold for degradation warning
pub trend_slope_threshold: f64,
}
impl Default for MonitorConfig {
fn default() -> Self {
Self {
window_size: 100,
lambda_low: 3.0,
lambda_critical: 1.0,
volatility_threshold: 0.5,
trend_slope_threshold: -0.1,
}
}
}
/// Current state of the monitor
#[derive(Debug, Clone)]
pub struct MonitorState {
/// Current minimum-cut estimate
pub lambda_est: f64,
/// Moving average slope (trend)
pub lambda_trend: f64,
/// Variance over window (volatility)
pub cut_volatility: f64,
/// Top-k edges crossing current cut
pub boundary_edges: Vec<(u64, u64)>,
/// Number of observations in window
pub observation_count: usize,
/// Last update timestamp
pub last_update_ts: u64,
}
impl Default for MonitorState {
fn default() -> Self {
Self {
lambda_est: f64::INFINITY,
lambda_trend: 0.0,
cut_volatility: 0.0,
boundary_edges: Vec::new(),
observation_count: 0,
last_update_ts: 0,
}
}
}
/// Type of trigger condition
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TriggerType {
/// Min-cut below critical threshold (islanding risk)
IslandingRisk,
/// Volatility above threshold (unstable structure)
Instability,
/// Negative trend (degrading connectivity)
Degradation,
/// Cut increased significantly (over-clustering)
OverClustering,
/// Graph became disconnected
Disconnected,
}
/// A trigger event from the monitor
#[derive(Debug, Clone)]
pub struct Trigger {
/// Type of trigger
pub trigger_type: TriggerType,
/// Current minimum-cut value
pub lambda_current: f64,
/// Threshold that was crossed
pub threshold: f64,
/// Severity (0.0 - 1.0)
pub severity: f64,
/// Recommended action
pub recommendation: String,
/// Timestamp
pub timestamp: u64,
}
impl Trigger {
/// Create a new trigger
pub fn new(trigger_type: TriggerType, lambda: f64, threshold: f64) -> Self {
let severity = match trigger_type {
TriggerType::Disconnected => 1.0,
TriggerType::IslandingRisk => 0.8,
TriggerType::Instability => 0.6,
TriggerType::Degradation => 0.5,
TriggerType::OverClustering => 0.3,
};
let recommendation = match trigger_type {
TriggerType::IslandingRisk => {
"Consider adding bridge edges or merging sparse partitions".to_string()
}
TriggerType::Instability => {
"Structure is volatile; consider stabilizing with explicit relations".to_string()
}
TriggerType::Degradation => {
"Connectivity trending down; review recent deletions".to_string()
}
TriggerType::OverClustering => {
"May have too many clusters; consider relaxing similarity threshold".to_string()
}
TriggerType::Disconnected => "Critical: graph has disconnected components".to_string(),
};
Self {
trigger_type,
lambda_current: lambda,
threshold,
severity,
recommendation,
timestamp: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs(),
}
}
}
/// Signal from the monitor indicating graph health
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BrittlenessSignal {
/// Healthy: good connectivity
Healthy,
/// Warning: connectivity getting low
Warning,
/// Critical: at risk of fragmentation
Critical,
/// Disconnected: already fragmented
Disconnected,
}
impl BrittlenessSignal {
/// Get string representation
pub fn as_str(&self) -> &'static str {
match self {
BrittlenessSignal::Healthy => "healthy",
BrittlenessSignal::Warning => "warning",
BrittlenessSignal::Critical => "critical",
BrittlenessSignal::Disconnected => "disconnected",
}
}
}
/// The structural monitor
#[derive(Debug)]
pub struct StructuralMonitor {
/// Configuration
config: MonitorConfig,
/// Current state
state: MonitorState,
/// History of lambda values for trend analysis
lambda_history: VecDeque<f64>,
/// Active triggers
active_triggers: Vec<Trigger>,
/// Total observations processed
total_observations: u64,
}
impl StructuralMonitor {
/// Create a new monitor with default config
pub fn new() -> Self {
Self::with_config(MonitorConfig::default())
}
/// Create a monitor with custom config
pub fn with_config(config: MonitorConfig) -> Self {
Self {
config,
state: MonitorState::default(),
lambda_history: VecDeque::new(),
active_triggers: Vec::new(),
total_observations: 0,
}
}
/// Get current state
pub fn state(&self) -> &MonitorState {
&self.state
}
/// Get current brittleness signal
pub fn signal(&self) -> BrittlenessSignal {
if self.state.lambda_est == 0.0 {
BrittlenessSignal::Disconnected
} else if self.state.lambda_est < self.config.lambda_critical {
BrittlenessSignal::Critical
} else if self.state.lambda_est < self.config.lambda_low {
BrittlenessSignal::Warning
} else {
BrittlenessSignal::Healthy
}
}
/// Get active triggers
pub fn triggers(&self) -> &[Trigger] {
&self.active_triggers
}
/// Update the monitor with a new minimum-cut observation
pub fn observe(&mut self, lambda: f64, boundary_edges: Vec<(u64, u64)>) -> Vec<Trigger> {
let mut new_triggers = Vec::new();
// Update history
self.lambda_history.push_back(lambda);
if self.lambda_history.len() > self.config.window_size {
self.lambda_history.pop_front();
}
// Update state
self.state.lambda_est = lambda;
self.state.boundary_edges = boundary_edges;
self.state.observation_count = self.lambda_history.len();
self.state.last_update_ts = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
self.total_observations += 1;
// Compute trend (linear regression slope)
self.state.lambda_trend = self.compute_trend();
// Compute volatility (variance)
self.state.cut_volatility = self.compute_volatility();
// Check triggers
if lambda == 0.0 || lambda.is_infinite() && lambda.is_sign_negative() {
new_triggers.push(Trigger::new(TriggerType::Disconnected, lambda, 0.0));
} else if lambda < self.config.lambda_critical {
new_triggers.push(Trigger::new(
TriggerType::IslandingRisk,
lambda,
self.config.lambda_critical,
));
}
if self.state.cut_volatility > self.config.volatility_threshold {
new_triggers.push(Trigger::new(
TriggerType::Instability,
lambda,
self.config.volatility_threshold,
));
}
if self.state.lambda_trend < self.config.trend_slope_threshold {
new_triggers.push(Trigger::new(
TriggerType::Degradation,
lambda,
self.config.trend_slope_threshold,
));
}
// Update active triggers
self.active_triggers = new_triggers.clone();
new_triggers
}
/// Check if immediate action is needed
pub fn needs_action(&self) -> bool {
!self.active_triggers.is_empty()
}
/// Get summary report
pub fn report(&self) -> String {
let signal = self.signal();
let trend_dir = if self.state.lambda_trend > 0.01 {
"↑ improving"
} else if self.state.lambda_trend < -0.01 {
"↓ degrading"
} else {
"→ stable"
};
format!(
"Signal: {} | λ={:.2} | Trend: {} ({:.3}) | Volatility: {:.3} | Boundary: {} edges",
signal.as_str(),
self.state.lambda_est,
trend_dir,
self.state.lambda_trend,
self.state.cut_volatility,
self.state.boundary_edges.len()
)
}
/// Reset the monitor
pub fn reset(&mut self) {
self.state = MonitorState::default();
self.lambda_history.clear();
self.active_triggers.clear();
}
/// Compute linear regression slope for trend
fn compute_trend(&self) -> f64 {
let n = self.lambda_history.len();
if n < 2 {
return 0.0;
}
let n_f64 = n as f64;
let sum_x: f64 = (0..n).map(|i| i as f64).sum();
let sum_y: f64 = self.lambda_history.iter().sum();
let sum_xy: f64 = self
.lambda_history
.iter()
.enumerate()
.map(|(i, &y)| i as f64 * y)
.sum();
let sum_xx: f64 = (0..n).map(|i| (i * i) as f64).sum();
let denominator = n_f64 * sum_xx - sum_x * sum_x;
if denominator.abs() < 1e-10 {
return 0.0;
}
(n_f64 * sum_xy - sum_x * sum_y) / denominator
}
/// Compute variance for volatility
fn compute_volatility(&self) -> f64 {
let n = self.lambda_history.len();
if n < 2 {
return 0.0;
}
let mean: f64 = self.lambda_history.iter().sum::<f64>() / n as f64;
let variance: f64 = self
.lambda_history
.iter()
.map(|&x| (x - mean) * (x - mean))
.sum::<f64>()
/ (n - 1) as f64;
variance.sqrt()
}
}
impl Default for StructuralMonitor {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_monitor_creation() {
let monitor = StructuralMonitor::new();
// Initial state has lambda_est = INFINITY (no observations yet)
// which counts as Healthy since it's above all thresholds
assert_eq!(monitor.signal(), BrittlenessSignal::Healthy);
}
#[test]
fn test_healthy_observation() {
let mut monitor = StructuralMonitor::new();
monitor.observe(5.0, vec![]);
assert_eq!(monitor.signal(), BrittlenessSignal::Healthy);
}
#[test]
fn test_warning_observation() {
let mut monitor = StructuralMonitor::new();
monitor.observe(2.0, vec![]);
assert_eq!(monitor.signal(), BrittlenessSignal::Warning);
}
#[test]
fn test_critical_observation() {
let mut monitor = StructuralMonitor::new();
monitor.observe(0.5, vec![]);
assert_eq!(monitor.signal(), BrittlenessSignal::Critical);
}
#[test]
fn test_trigger_generation() {
let mut monitor = StructuralMonitor::new();
let triggers = monitor.observe(0.5, vec![(1, 2)]);
assert!(!triggers.is_empty());
assert!(triggers
.iter()
.any(|t| t.trigger_type == TriggerType::IslandingRisk));
}
#[test]
fn test_trend_computation() {
let mut monitor = StructuralMonitor::new();
// Simulate decreasing trend
for i in (0..10).rev() {
monitor.observe(i as f64, vec![]);
}
assert!(monitor.state().lambda_trend < 0.0);
}
#[test]
fn test_volatility_computation() {
let mut monitor = StructuralMonitor::new();
// Simulate volatile observations
for i in 0..10 {
let value = if i % 2 == 0 { 5.0 } else { 1.0 };
monitor.observe(value, vec![]);
}
assert!(monitor.state().cut_volatility > 0.0);
}
#[test]
fn test_report() {
let mut monitor = StructuralMonitor::new();
monitor.observe(3.5, vec![(1, 2), (2, 3)]);
let report = monitor.report();
assert!(report.contains("healthy"));
assert!(report.contains("3.50"));
}
}