Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'
This commit is contained in:
524
vendor/ruvector/crates/prime-radiant/src/cohomology/obstruction.rs
vendored
Normal file
524
vendor/ruvector/crates/prime-radiant/src/cohomology/obstruction.rs
vendored
Normal file
@@ -0,0 +1,524 @@
|
||||
//! Obstruction Detection
|
||||
//!
|
||||
//! Obstructions are cohomological objects that indicate global inconsistency.
|
||||
//! A non-trivial obstruction means that local data cannot be patched together
|
||||
//! into a global section.
|
||||
|
||||
use super::cocycle::{Cocycle, SheafCoboundary};
|
||||
use super::laplacian::{HarmonicRepresentative, LaplacianConfig, SheafLaplacian};
|
||||
use super::sheaf::{Sheaf, SheafSection};
|
||||
use crate::substrate::NodeId;
|
||||
use crate::substrate::SheafGraph;
|
||||
use ndarray::Array1;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Severity of an obstruction
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum ObstructionSeverity {
|
||||
/// No obstruction (fully coherent)
|
||||
None,
|
||||
/// Minor obstruction (near-coherent, easily fixable)
|
||||
Minor,
|
||||
/// Moderate obstruction (requires attention)
|
||||
Moderate,
|
||||
/// Severe obstruction (significant inconsistency)
|
||||
Severe,
|
||||
/// Critical obstruction (fundamental contradiction)
|
||||
Critical,
|
||||
}
|
||||
|
||||
impl ObstructionSeverity {
|
||||
/// Create from energy magnitude
|
||||
pub fn from_energy(energy: f64, thresholds: &[f64; 4]) -> Self {
|
||||
if energy < thresholds[0] {
|
||||
Self::None
|
||||
} else if energy < thresholds[1] {
|
||||
Self::Minor
|
||||
} else if energy < thresholds[2] {
|
||||
Self::Moderate
|
||||
} else if energy < thresholds[3] {
|
||||
Self::Severe
|
||||
} else {
|
||||
Self::Critical
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if this requires action
|
||||
pub fn requires_action(&self) -> bool {
|
||||
matches!(self, Self::Moderate | Self::Severe | Self::Critical)
|
||||
}
|
||||
|
||||
/// Check if this is critical
|
||||
pub fn is_critical(&self) -> bool {
|
||||
matches!(self, Self::Critical)
|
||||
}
|
||||
}
|
||||
|
||||
/// An obstruction representing a global inconsistency
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Obstruction {
|
||||
/// Unique identifier
|
||||
pub id: u64,
|
||||
/// Cohomology degree where obstruction lives
|
||||
pub degree: usize,
|
||||
/// Severity level
|
||||
pub severity: ObstructionSeverity,
|
||||
/// Total obstruction energy
|
||||
pub energy: f64,
|
||||
/// Edges contributing to obstruction (edge -> contribution)
|
||||
pub edge_contributions: HashMap<(NodeId, NodeId), f64>,
|
||||
/// Localization: nodes most affected
|
||||
pub hotspots: Vec<(NodeId, f64)>,
|
||||
/// Representative cocycle
|
||||
pub cocycle: Option<Cocycle>,
|
||||
/// Dimension of obstruction space
|
||||
pub multiplicity: usize,
|
||||
/// Description of the obstruction
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
impl Obstruction {
|
||||
/// Create a new obstruction
|
||||
pub fn new(id: u64, degree: usize, energy: f64, severity: ObstructionSeverity) -> Self {
|
||||
Self {
|
||||
id,
|
||||
degree,
|
||||
severity,
|
||||
energy,
|
||||
edge_contributions: HashMap::new(),
|
||||
hotspots: Vec::new(),
|
||||
cocycle: None,
|
||||
multiplicity: 1,
|
||||
description: String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Add edge contribution
|
||||
pub fn add_edge_contribution(&mut self, source: NodeId, target: NodeId, contribution: f64) {
|
||||
self.edge_contributions
|
||||
.insert((source, target), contribution);
|
||||
}
|
||||
|
||||
/// Set hotspots
|
||||
pub fn with_hotspots(mut self, hotspots: Vec<(NodeId, f64)>) -> Self {
|
||||
self.hotspots = hotspots;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set cocycle
|
||||
pub fn with_cocycle(mut self, cocycle: Cocycle) -> Self {
|
||||
self.cocycle = Some(cocycle);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set description
|
||||
pub fn with_description(mut self, description: impl Into<String>) -> Self {
|
||||
self.description = description.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Get top k contributing edges
|
||||
pub fn top_edges(&self, k: usize) -> Vec<((NodeId, NodeId), f64)> {
|
||||
let mut edges: Vec<_> = self
|
||||
.edge_contributions
|
||||
.iter()
|
||||
.map(|(&e, &c)| (e, c))
|
||||
.collect();
|
||||
edges.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
|
||||
edges.truncate(k);
|
||||
edges
|
||||
}
|
||||
}
|
||||
|
||||
/// Detailed obstruction report
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ObstructionReport {
|
||||
/// Total cohomological obstruction energy
|
||||
pub total_energy: f64,
|
||||
/// Maximum local obstruction
|
||||
pub max_local_energy: f64,
|
||||
/// Overall severity
|
||||
pub severity: ObstructionSeverity,
|
||||
/// List of detected obstructions
|
||||
pub obstructions: Vec<Obstruction>,
|
||||
/// Betti numbers (cohomology dimensions)
|
||||
pub betti_numbers: Vec<usize>,
|
||||
/// Spectral gap (if computed)
|
||||
pub spectral_gap: Option<f64>,
|
||||
/// Whether system is globally coherent
|
||||
pub is_coherent: bool,
|
||||
/// Recommendations for resolution
|
||||
pub recommendations: Vec<String>,
|
||||
}
|
||||
|
||||
impl ObstructionReport {
|
||||
/// Create an empty report
|
||||
pub fn empty() -> Self {
|
||||
Self {
|
||||
total_energy: 0.0,
|
||||
max_local_energy: 0.0,
|
||||
severity: ObstructionSeverity::None,
|
||||
obstructions: Vec::new(),
|
||||
betti_numbers: Vec::new(),
|
||||
spectral_gap: None,
|
||||
is_coherent: true,
|
||||
recommendations: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a coherent report
|
||||
pub fn coherent(spectral_gap: Option<f64>) -> Self {
|
||||
Self {
|
||||
total_energy: 0.0,
|
||||
max_local_energy: 0.0,
|
||||
severity: ObstructionSeverity::None,
|
||||
obstructions: Vec::new(),
|
||||
betti_numbers: vec![1], // Single connected component
|
||||
spectral_gap,
|
||||
is_coherent: true,
|
||||
recommendations: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Add an obstruction
|
||||
pub fn add_obstruction(&mut self, obs: Obstruction) {
|
||||
self.total_energy += obs.energy;
|
||||
self.max_local_energy = self.max_local_energy.max(obs.energy);
|
||||
|
||||
if obs.severity as u8 > self.severity as u8 {
|
||||
self.severity = obs.severity;
|
||||
}
|
||||
|
||||
if obs.severity.requires_action() {
|
||||
self.is_coherent = false;
|
||||
}
|
||||
|
||||
self.obstructions.push(obs);
|
||||
}
|
||||
|
||||
/// Add a recommendation
|
||||
pub fn add_recommendation(&mut self, rec: impl Into<String>) {
|
||||
self.recommendations.push(rec.into());
|
||||
}
|
||||
|
||||
/// Get critical obstructions
|
||||
pub fn critical_obstructions(&self) -> Vec<&Obstruction> {
|
||||
self.obstructions
|
||||
.iter()
|
||||
.filter(|o| o.severity.is_critical())
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// Detector for cohomological obstructions
|
||||
pub struct ObstructionDetector {
|
||||
/// Energy thresholds for severity classification
|
||||
thresholds: [f64; 4],
|
||||
/// Laplacian configuration
|
||||
laplacian_config: LaplacianConfig,
|
||||
/// Whether to compute detailed cocycles
|
||||
compute_cocycles: bool,
|
||||
/// Number of hotspots to track
|
||||
num_hotspots: usize,
|
||||
}
|
||||
|
||||
impl ObstructionDetector {
|
||||
/// Create a new detector with default settings
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
thresholds: [0.01, 0.1, 0.5, 1.0],
|
||||
laplacian_config: LaplacianConfig::default(),
|
||||
compute_cocycles: true,
|
||||
num_hotspots: 5,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set energy thresholds
|
||||
pub fn with_thresholds(mut self, thresholds: [f64; 4]) -> Self {
|
||||
self.thresholds = thresholds;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set whether to compute cocycles
|
||||
pub fn with_cocycles(mut self, compute: bool) -> Self {
|
||||
self.compute_cocycles = compute;
|
||||
self
|
||||
}
|
||||
|
||||
/// Detect obstructions in a SheafGraph
|
||||
pub fn detect(&self, graph: &SheafGraph) -> ObstructionReport {
|
||||
let mut report = ObstructionReport::empty();
|
||||
|
||||
// Build the sheaf Laplacian
|
||||
let laplacian = SheafLaplacian::from_graph(graph, self.laplacian_config.clone());
|
||||
|
||||
// Compute global energy from current state
|
||||
let section = self.graph_to_section(graph);
|
||||
let total_energy = laplacian.energy(graph, §ion);
|
||||
|
||||
// Compute per-edge energies
|
||||
let mut edge_energies: HashMap<(NodeId, NodeId), f64> = HashMap::new();
|
||||
for edge_id in graph.edge_ids() {
|
||||
if let Some(edge) = graph.get_edge(edge_id) {
|
||||
if let (Some(source_node), Some(target_node)) =
|
||||
(graph.get_node(edge.source), graph.get_node(edge.target))
|
||||
{
|
||||
let residual = edge.weighted_residual_energy(
|
||||
source_node.state.as_slice(),
|
||||
target_node.state.as_slice(),
|
||||
);
|
||||
edge_energies.insert((edge.source, edge.target), residual as f64);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Compute spectrum for Betti numbers
|
||||
let spectrum = laplacian.compute_spectrum(graph);
|
||||
report.betti_numbers = vec![spectrum.null_space_dim];
|
||||
report.spectral_gap = spectrum.spectral_gap;
|
||||
|
||||
// Create obstruction if energy is non-trivial
|
||||
if total_energy > self.thresholds[0] {
|
||||
let severity = ObstructionSeverity::from_energy(total_energy, &self.thresholds);
|
||||
|
||||
let mut obstruction = Obstruction::new(1, 1, total_energy, severity);
|
||||
|
||||
// Add edge contributions
|
||||
for ((source, target), energy) in &edge_energies {
|
||||
obstruction.add_edge_contribution(*source, *target, *energy);
|
||||
}
|
||||
|
||||
// Find hotspots (nodes with highest adjacent energy)
|
||||
let node_energies = self.compute_node_energies(graph, &edge_energies);
|
||||
let mut hotspots: Vec<_> = node_energies.into_iter().collect();
|
||||
hotspots.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
|
||||
hotspots.truncate(self.num_hotspots);
|
||||
obstruction = obstruction.with_hotspots(hotspots);
|
||||
|
||||
// Set description
|
||||
let desc = format!(
|
||||
"H^1 obstruction: {} edges with total energy {:.4}",
|
||||
edge_energies.len(),
|
||||
total_energy
|
||||
);
|
||||
obstruction = obstruction.with_description(desc);
|
||||
|
||||
report.add_obstruction(obstruction);
|
||||
}
|
||||
|
||||
report.total_energy = total_energy;
|
||||
report.max_local_energy = edge_energies.values().copied().fold(0.0, f64::max);
|
||||
|
||||
// Generate recommendations
|
||||
self.generate_recommendations(&mut report);
|
||||
|
||||
report
|
||||
}
|
||||
|
||||
/// Convert graph state to section
|
||||
fn graph_to_section(&self, graph: &SheafGraph) -> SheafSection {
|
||||
let mut section = SheafSection::empty();
|
||||
|
||||
for node_id in graph.node_ids() {
|
||||
if let Some(node) = graph.get_node(node_id) {
|
||||
let values: Vec<f64> = node.state.as_slice().iter().map(|&x| x as f64).collect();
|
||||
section.set(node_id, Array1::from_vec(values));
|
||||
}
|
||||
}
|
||||
|
||||
section
|
||||
}
|
||||
|
||||
/// Compute energy per node (sum of incident edge energies)
|
||||
fn compute_node_energies(
|
||||
&self,
|
||||
graph: &SheafGraph,
|
||||
edge_energies: &HashMap<(NodeId, NodeId), f64>,
|
||||
) -> HashMap<NodeId, f64> {
|
||||
let mut node_energies: HashMap<NodeId, f64> = HashMap::new();
|
||||
|
||||
for ((source, target), energy) in edge_energies {
|
||||
*node_energies.entry(*source).or_insert(0.0) += energy;
|
||||
*node_energies.entry(*target).or_insert(0.0) += energy;
|
||||
}
|
||||
|
||||
node_energies
|
||||
}
|
||||
|
||||
/// Generate recommendations based on obstructions
|
||||
fn generate_recommendations(&self, report: &mut ObstructionReport) {
|
||||
if report.is_coherent {
|
||||
report.add_recommendation("System is coherent - no action required");
|
||||
return;
|
||||
}
|
||||
|
||||
// Collect recommendations first to avoid borrow checker issues
|
||||
let mut recommendations: Vec<String> = Vec::new();
|
||||
|
||||
for obs in &report.obstructions {
|
||||
match obs.severity {
|
||||
ObstructionSeverity::Minor => {
|
||||
recommendations.push(format!(
|
||||
"Minor inconsistency detected. Consider reviewing edges: {:?}",
|
||||
obs.top_edges(2).iter().map(|(e, _)| e).collect::<Vec<_>>()
|
||||
));
|
||||
}
|
||||
ObstructionSeverity::Moderate => {
|
||||
recommendations.push(format!(
|
||||
"Moderate obstruction. Focus on hotspot nodes: {:?}",
|
||||
obs.hotspots
|
||||
.iter()
|
||||
.take(3)
|
||||
.map(|(n, _)| n)
|
||||
.collect::<Vec<_>>()
|
||||
));
|
||||
}
|
||||
ObstructionSeverity::Severe | ObstructionSeverity::Critical => {
|
||||
recommendations.push(format!(
|
||||
"Severe obstruction with energy {:.4}. Immediate review required.",
|
||||
obs.energy
|
||||
));
|
||||
recommendations
|
||||
.push("Consider isolating incoherent region using MinCut".to_string());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if report.spectral_gap.is_some_and(|g| g < 0.1) {
|
||||
recommendations.push(
|
||||
"Small spectral gap indicates near-obstruction. Monitor for drift.".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
// Now add all recommendations
|
||||
for rec in recommendations {
|
||||
report.add_recommendation(rec);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ObstructionDetector {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::substrate::edge::SheafEdgeBuilder;
|
||||
use crate::substrate::node::SheafNodeBuilder;
|
||||
use uuid::Uuid;
|
||||
|
||||
fn make_node_id() -> NodeId {
|
||||
Uuid::new_v4()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_coherent_system() {
|
||||
let graph = SheafGraph::new();
|
||||
|
||||
// Two nodes with same state
|
||||
let node1 = SheafNodeBuilder::new()
|
||||
.state_from_slice(&[1.0, 0.0])
|
||||
.build();
|
||||
let node2 = SheafNodeBuilder::new()
|
||||
.state_from_slice(&[1.0, 0.0])
|
||||
.build();
|
||||
|
||||
let id1 = graph.add_node(node1);
|
||||
let id2 = graph.add_node(node2);
|
||||
|
||||
let edge = SheafEdgeBuilder::new(id1, id2)
|
||||
.identity_restrictions(2)
|
||||
.weight(1.0)
|
||||
.build();
|
||||
graph.add_edge(edge).unwrap();
|
||||
|
||||
let detector = ObstructionDetector::new();
|
||||
let report = detector.detect(&graph);
|
||||
|
||||
assert!(report.is_coherent);
|
||||
assert!(report.total_energy < 0.01);
|
||||
assert_eq!(report.severity, ObstructionSeverity::None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_incoherent_system() {
|
||||
let graph = SheafGraph::new();
|
||||
|
||||
// Two nodes with different states
|
||||
let node1 = SheafNodeBuilder::new()
|
||||
.state_from_slice(&[1.0, 0.0])
|
||||
.build();
|
||||
let node2 = SheafNodeBuilder::new()
|
||||
.state_from_slice(&[0.0, 1.0])
|
||||
.build();
|
||||
|
||||
let id1 = graph.add_node(node1);
|
||||
let id2 = graph.add_node(node2);
|
||||
|
||||
let edge = SheafEdgeBuilder::new(id1, id2)
|
||||
.identity_restrictions(2)
|
||||
.weight(1.0)
|
||||
.build();
|
||||
graph.add_edge(edge).unwrap();
|
||||
|
||||
let detector = ObstructionDetector::new();
|
||||
let report = detector.detect(&graph);
|
||||
|
||||
assert!(!report.is_coherent || report.total_energy > 0.01);
|
||||
assert!(report.total_energy > 0.5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_severity_classification() {
|
||||
assert_eq!(
|
||||
ObstructionSeverity::from_energy(0.001, &[0.01, 0.1, 0.5, 1.0]),
|
||||
ObstructionSeverity::None
|
||||
);
|
||||
assert_eq!(
|
||||
ObstructionSeverity::from_energy(0.05, &[0.01, 0.1, 0.5, 1.0]),
|
||||
ObstructionSeverity::Minor
|
||||
);
|
||||
assert_eq!(
|
||||
ObstructionSeverity::from_energy(2.0, &[0.01, 0.1, 0.5, 1.0]),
|
||||
ObstructionSeverity::Critical
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_obstruction_hotspots() {
|
||||
let graph = SheafGraph::new();
|
||||
|
||||
let node1 = SheafNodeBuilder::new().state_from_slice(&[1.0]).build();
|
||||
let node2 = SheafNodeBuilder::new().state_from_slice(&[5.0]).build();
|
||||
let node3 = SheafNodeBuilder::new().state_from_slice(&[1.5]).build();
|
||||
|
||||
let id1 = graph.add_node(node1);
|
||||
let id2 = graph.add_node(node2);
|
||||
let id3 = graph.add_node(node3);
|
||||
|
||||
let edge1 = SheafEdgeBuilder::new(id1, id2)
|
||||
.identity_restrictions(1)
|
||||
.weight(1.0)
|
||||
.build();
|
||||
let edge2 = SheafEdgeBuilder::new(id2, id3)
|
||||
.identity_restrictions(1)
|
||||
.weight(1.0)
|
||||
.build();
|
||||
|
||||
graph.add_edge(edge1).unwrap();
|
||||
graph.add_edge(edge2).unwrap();
|
||||
|
||||
let detector = ObstructionDetector::new();
|
||||
let report = detector.detect(&graph);
|
||||
|
||||
// Node 2 should be a hotspot (connects to both high-energy edges)
|
||||
if let Some(obs) = report.obstructions.first() {
|
||||
assert!(!obs.hotspots.is_empty());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user