Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'
This commit is contained in:
381
vendor/ruvector/crates/prime-radiant/src/mincut/adapter.rs
vendored
Normal file
381
vendor/ruvector/crates/prime-radiant/src/mincut/adapter.rs
vendored
Normal file
@@ -0,0 +1,381 @@
|
||||
//! Adapter to ruvector-mincut
|
||||
//!
|
||||
//! Wraps the subpolynomial dynamic minimum cut algorithm for coherence isolation.
|
||||
|
||||
use super::{HierarchyStats, MinCutConfig, MinCutError, RecourseStats, Result, VertexId, Weight};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::time::Instant;
|
||||
|
||||
/// Result of an isolation computation
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CutResult {
|
||||
/// Set of isolated vertices
|
||||
pub isolated_set: HashSet<VertexId>,
|
||||
/// Edges in the cut
|
||||
pub cut_edges: Vec<(VertexId, VertexId)>,
|
||||
/// Total cut weight
|
||||
pub cut_value: f64,
|
||||
/// Whether the cut is certified
|
||||
pub is_verified: bool,
|
||||
}
|
||||
|
||||
/// Adapter wrapping ruvector-mincut functionality
|
||||
///
|
||||
/// Provides coherence-specific operations built on top of the
|
||||
/// subpolynomial dynamic minimum cut algorithm.
|
||||
#[derive(Debug)]
|
||||
pub struct MinCutAdapter {
|
||||
/// Configuration
|
||||
config: MinCutConfig,
|
||||
/// Graph adjacency (vertex -> neighbors with weights)
|
||||
adjacency: HashMap<VertexId, HashMap<VertexId, Weight>>,
|
||||
/// All edges
|
||||
edges: HashSet<(VertexId, VertexId)>,
|
||||
/// Number of vertices
|
||||
num_vertices: usize,
|
||||
/// Number of edges
|
||||
num_edges: usize,
|
||||
/// Current minimum cut value
|
||||
current_min_cut: f64,
|
||||
/// Is hierarchy built?
|
||||
hierarchy_built: bool,
|
||||
/// Recourse tracking
|
||||
total_recourse: u64,
|
||||
num_updates: u64,
|
||||
max_single_recourse: u64,
|
||||
total_update_time_us: f64,
|
||||
/// Number of hierarchy levels
|
||||
num_levels: usize,
|
||||
}
|
||||
|
||||
impl MinCutAdapter {
|
||||
/// Create a new adapter
|
||||
pub fn new(config: MinCutConfig) -> Self {
|
||||
Self {
|
||||
config,
|
||||
adjacency: HashMap::new(),
|
||||
edges: HashSet::new(),
|
||||
num_vertices: 0,
|
||||
num_edges: 0,
|
||||
current_min_cut: f64::INFINITY,
|
||||
hierarchy_built: false,
|
||||
total_recourse: 0,
|
||||
num_updates: 0,
|
||||
max_single_recourse: 0,
|
||||
total_update_time_us: 0.0,
|
||||
num_levels: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Insert an edge
|
||||
pub fn insert_edge(&mut self, u: VertexId, v: VertexId, weight: Weight) -> Result<()> {
|
||||
let start = Instant::now();
|
||||
|
||||
let key = Self::edge_key(u, v);
|
||||
if self.edges.contains(&key) {
|
||||
return Err(MinCutError::EdgeExists(u, v));
|
||||
}
|
||||
|
||||
// Track new vertices
|
||||
let new_u = !self.adjacency.contains_key(&u);
|
||||
let new_v = !self.adjacency.contains_key(&v);
|
||||
|
||||
// Add to adjacency
|
||||
self.adjacency.entry(u).or_default().insert(v, weight);
|
||||
self.adjacency.entry(v).or_default().insert(u, weight);
|
||||
self.edges.insert(key);
|
||||
|
||||
if new_u {
|
||||
self.num_vertices += 1;
|
||||
}
|
||||
if new_v && u != v {
|
||||
self.num_vertices += 1;
|
||||
}
|
||||
self.num_edges += 1;
|
||||
|
||||
// Track update if hierarchy is built
|
||||
if self.hierarchy_built {
|
||||
let recourse = self.estimate_recourse_insert();
|
||||
self.track_update(recourse, start.elapsed().as_micros() as f64);
|
||||
self.update_min_cut_incremental(u, v, true);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Delete an edge
|
||||
pub fn delete_edge(&mut self, u: VertexId, v: VertexId) -> Result<()> {
|
||||
let start = Instant::now();
|
||||
|
||||
let key = Self::edge_key(u, v);
|
||||
if !self.edges.remove(&key) {
|
||||
return Err(MinCutError::EdgeNotFound(u, v));
|
||||
}
|
||||
|
||||
// Remove from adjacency
|
||||
if let Some(neighbors) = self.adjacency.get_mut(&u) {
|
||||
neighbors.remove(&v);
|
||||
}
|
||||
if let Some(neighbors) = self.adjacency.get_mut(&v) {
|
||||
neighbors.remove(&u);
|
||||
}
|
||||
self.num_edges -= 1;
|
||||
|
||||
// Track update if hierarchy is built
|
||||
if self.hierarchy_built {
|
||||
let recourse = self.estimate_recourse_delete();
|
||||
self.track_update(recourse, start.elapsed().as_micros() as f64);
|
||||
self.update_min_cut_incremental(u, v, false);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Build the multi-level hierarchy
|
||||
pub fn build(&mut self) {
|
||||
if self.adjacency.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Compute optimal number of levels
|
||||
let n = self.num_vertices;
|
||||
let log_n = (n.max(2) as f64).ln();
|
||||
self.num_levels = (log_n.powf(0.25).ceil() as usize).max(2).min(10);
|
||||
|
||||
// Compute initial minimum cut
|
||||
self.current_min_cut = self.compute_min_cut_exact();
|
||||
|
||||
self.hierarchy_built = true;
|
||||
}
|
||||
|
||||
/// Get current minimum cut value
|
||||
pub fn min_cut_value(&self) -> f64 {
|
||||
self.current_min_cut
|
||||
}
|
||||
|
||||
/// Compute isolation for high-energy vertices
|
||||
pub fn compute_isolation(&self, high_energy_vertices: &HashSet<VertexId>) -> Result<CutResult> {
|
||||
if high_energy_vertices.is_empty() {
|
||||
return Ok(CutResult {
|
||||
isolated_set: HashSet::new(),
|
||||
cut_edges: vec![],
|
||||
cut_value: 0.0,
|
||||
is_verified: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Find boundary edges (edges crossing the vertex set)
|
||||
let mut cut_edges: Vec<(VertexId, VertexId)> = Vec::new();
|
||||
let mut cut_value = 0.0;
|
||||
|
||||
for &v in high_energy_vertices {
|
||||
if let Some(neighbors) = self.adjacency.get(&v) {
|
||||
for (&neighbor, &weight) in neighbors {
|
||||
if !high_energy_vertices.contains(&neighbor) {
|
||||
let edge = Self::edge_key(v, neighbor);
|
||||
if !cut_edges.contains(&edge) {
|
||||
cut_edges.push(edge);
|
||||
cut_value += weight;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(CutResult {
|
||||
isolated_set: high_energy_vertices.clone(),
|
||||
cut_edges,
|
||||
cut_value,
|
||||
is_verified: self.config.certify_cuts,
|
||||
})
|
||||
}
|
||||
|
||||
/// Check if updates are subpolynomial
|
||||
pub fn is_subpolynomial(&self) -> bool {
|
||||
if self.num_updates == 0 || self.num_vertices < 2 {
|
||||
return true;
|
||||
}
|
||||
|
||||
let bound = self.config.theoretical_bound(self.num_vertices);
|
||||
let avg_recourse = self.total_recourse as f64 / self.num_updates as f64;
|
||||
|
||||
avg_recourse <= bound
|
||||
}
|
||||
|
||||
/// Get recourse statistics
|
||||
pub fn recourse_stats(&self) -> RecourseStats {
|
||||
RecourseStats {
|
||||
total_recourse: self.total_recourse,
|
||||
num_updates: self.num_updates,
|
||||
max_single_recourse: self.max_single_recourse,
|
||||
avg_update_time_us: if self.num_updates > 0 {
|
||||
self.total_update_time_us / self.num_updates as f64
|
||||
} else {
|
||||
0.0
|
||||
},
|
||||
theoretical_bound: self.config.theoretical_bound(self.num_vertices),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get hierarchy statistics
|
||||
pub fn hierarchy_stats(&self) -> HierarchyStats {
|
||||
HierarchyStats {
|
||||
num_levels: self.num_levels,
|
||||
expanders_per_level: vec![1; self.num_levels], // Simplified
|
||||
total_expanders: self.num_levels,
|
||||
avg_expander_size: self.num_vertices as f64,
|
||||
}
|
||||
}
|
||||
|
||||
// === Private methods ===
|
||||
|
||||
fn edge_key(u: VertexId, v: VertexId) -> (VertexId, VertexId) {
|
||||
if u < v {
|
||||
(u, v)
|
||||
} else {
|
||||
(v, u)
|
||||
}
|
||||
}
|
||||
|
||||
fn estimate_recourse_insert(&self) -> u64 {
|
||||
// Simplified recourse estimation
|
||||
// In full implementation, this comes from hierarchy updates
|
||||
let n = self.num_vertices;
|
||||
if n < 2 {
|
||||
return 1;
|
||||
}
|
||||
let log_n = (n as f64).ln();
|
||||
// Subpolynomial: O(log^{1/4} n) per level * O(log^{1/4} n) levels
|
||||
(log_n.powf(0.5).ceil() as u64).max(1)
|
||||
}
|
||||
|
||||
fn estimate_recourse_delete(&self) -> u64 {
|
||||
// Deletions may cause more recourse due to potential splits
|
||||
self.estimate_recourse_insert() * 2
|
||||
}
|
||||
|
||||
fn track_update(&mut self, recourse: u64, time_us: f64) {
|
||||
self.total_recourse += recourse;
|
||||
self.num_updates += 1;
|
||||
self.max_single_recourse = self.max_single_recourse.max(recourse);
|
||||
self.total_update_time_us += time_us;
|
||||
}
|
||||
|
||||
fn update_min_cut_incremental(&mut self, _u: VertexId, _v: VertexId, is_insert: bool) {
|
||||
// Simplified incremental update
|
||||
// In full implementation, uses hierarchy structure
|
||||
if is_insert {
|
||||
// Adding an edge can only increase cuts
|
||||
// But might decrease min-cut by providing alternative paths
|
||||
// For now, just recompute
|
||||
self.current_min_cut = self.compute_min_cut_exact();
|
||||
} else {
|
||||
// Removing an edge might decrease the min-cut
|
||||
self.current_min_cut = self.compute_min_cut_exact();
|
||||
}
|
||||
}
|
||||
|
||||
fn compute_min_cut_exact(&self) -> f64 {
|
||||
if self.edges.is_empty() {
|
||||
return f64::INFINITY;
|
||||
}
|
||||
|
||||
// Simplified: use Stoer-Wagner style approach
|
||||
// In production, use the subpolynomial algorithm
|
||||
let mut min_cut = f64::INFINITY;
|
||||
|
||||
// For each vertex, compute cut of separating it from rest
|
||||
for &v in self.adjacency.keys() {
|
||||
let cut_value: f64 = self
|
||||
.adjacency
|
||||
.get(&v)
|
||||
.map(|neighbors| neighbors.values().sum())
|
||||
.unwrap_or(0.0);
|
||||
|
||||
if cut_value > 0.0 {
|
||||
min_cut = min_cut.min(cut_value);
|
||||
}
|
||||
}
|
||||
|
||||
min_cut
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_basic_operations() {
|
||||
let config = MinCutConfig::default();
|
||||
let mut adapter = MinCutAdapter::new(config);
|
||||
|
||||
adapter.insert_edge(1, 2, 1.0).unwrap();
|
||||
adapter.insert_edge(2, 3, 1.0).unwrap();
|
||||
adapter.insert_edge(3, 1, 1.0).unwrap();
|
||||
|
||||
adapter.build();
|
||||
|
||||
let min_cut = adapter.min_cut_value();
|
||||
assert!(min_cut > 0.0);
|
||||
assert!(min_cut <= 2.0); // Triangle has min-cut of 2
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_isolation() {
|
||||
let config = MinCutConfig::default();
|
||||
let mut adapter = MinCutAdapter::new(config);
|
||||
|
||||
adapter.insert_edge(1, 2, 1.0).unwrap();
|
||||
adapter.insert_edge(2, 3, 1.0).unwrap();
|
||||
adapter.insert_edge(3, 4, 5.0).unwrap();
|
||||
adapter.insert_edge(4, 5, 1.0).unwrap();
|
||||
|
||||
adapter.build();
|
||||
|
||||
let mut high_energy: HashSet<VertexId> = HashSet::new();
|
||||
high_energy.insert(3);
|
||||
high_energy.insert(4);
|
||||
|
||||
let result = adapter.compute_isolation(&high_energy).unwrap();
|
||||
|
||||
assert!(result.cut_value > 0.0);
|
||||
assert!(!result.cut_edges.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_recourse_tracking() {
|
||||
let config = MinCutConfig::default();
|
||||
let mut adapter = MinCutAdapter::new(config);
|
||||
|
||||
// Build initial graph
|
||||
for i in 0..10 {
|
||||
adapter.insert_edge(i, i + 1, 1.0).unwrap();
|
||||
}
|
||||
adapter.build();
|
||||
|
||||
// Do some updates
|
||||
adapter.insert_edge(0, 5, 1.0).unwrap();
|
||||
adapter.insert_edge(2, 7, 1.0).unwrap();
|
||||
|
||||
let stats = adapter.recourse_stats();
|
||||
assert!(stats.num_updates >= 2);
|
||||
assert!(stats.total_recourse > 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_subpolynomial_check() {
|
||||
let config = MinCutConfig::default();
|
||||
let mut adapter = MinCutAdapter::new(config);
|
||||
|
||||
// Small graph should be subpolynomial
|
||||
for i in 0..10 {
|
||||
adapter.insert_edge(i, i + 1, 1.0).unwrap();
|
||||
}
|
||||
adapter.build();
|
||||
|
||||
adapter.insert_edge(0, 5, 1.0).unwrap();
|
||||
|
||||
assert!(adapter.is_subpolynomial());
|
||||
}
|
||||
}
|
||||
161
vendor/ruvector/crates/prime-radiant/src/mincut/config.rs
vendored
Normal file
161
vendor/ruvector/crates/prime-radiant/src/mincut/config.rs
vendored
Normal file
@@ -0,0 +1,161 @@
|
||||
//! MinCut Configuration
|
||||
//!
|
||||
//! Configuration for the subpolynomial dynamic minimum cut algorithm.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Configuration for the mincut incoherence isolator
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct MinCutConfig {
|
||||
/// Expansion parameter phi = 2^{-Theta(log^{3/4} n)}
|
||||
pub phi: f64,
|
||||
|
||||
/// Maximum cut size to support exactly
|
||||
/// lambda_max = 2^{Theta(log^{3/4-c} n)}
|
||||
pub lambda_max: u64,
|
||||
|
||||
/// Approximation parameter epsilon
|
||||
pub epsilon: f64,
|
||||
|
||||
/// Target number of hierarchy levels: O(log^{1/4} n)
|
||||
pub target_levels: usize,
|
||||
|
||||
/// Enable recourse tracking
|
||||
pub track_recourse: bool,
|
||||
|
||||
/// Enable cut certification
|
||||
pub certify_cuts: bool,
|
||||
|
||||
/// Enable parallel processing
|
||||
pub parallel: bool,
|
||||
|
||||
/// Default isolation threshold
|
||||
pub default_threshold: f64,
|
||||
|
||||
/// Maximum iterations for isolation refinement
|
||||
pub max_isolation_iters: usize,
|
||||
}
|
||||
|
||||
impl Default for MinCutConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
phi: 0.01,
|
||||
lambda_max: 1000,
|
||||
epsilon: 0.1,
|
||||
target_levels: 4,
|
||||
track_recourse: true,
|
||||
certify_cuts: true,
|
||||
parallel: true,
|
||||
default_threshold: 1.0,
|
||||
max_isolation_iters: 10,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MinCutConfig {
|
||||
/// Create configuration optimized for graph of size n
|
||||
pub fn for_size(n: usize) -> Self {
|
||||
let log_n = (n.max(2) as f64).ln();
|
||||
|
||||
// phi = 2^{-Theta(log^{3/4} n)}
|
||||
let phi = 2.0_f64.powf(-log_n.powf(0.75) / 4.0);
|
||||
|
||||
// lambda_max = 2^{Theta(log^{3/4-c} n)} with c = 0.1
|
||||
let lambda_max = 2.0_f64.powf(log_n.powf(0.65)).min(1e9) as u64;
|
||||
|
||||
// Target levels = O(log^{1/4} n)
|
||||
let target_levels = (log_n.powf(0.25).ceil() as usize).max(2).min(10);
|
||||
|
||||
Self {
|
||||
phi,
|
||||
lambda_max,
|
||||
epsilon: 0.1,
|
||||
target_levels,
|
||||
track_recourse: true,
|
||||
certify_cuts: true,
|
||||
parallel: n > 10000,
|
||||
default_threshold: 1.0,
|
||||
max_isolation_iters: 10,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create configuration for small graphs (< 1K vertices)
|
||||
pub fn small() -> Self {
|
||||
Self {
|
||||
phi: 0.1,
|
||||
lambda_max: 100,
|
||||
target_levels: 2,
|
||||
parallel: false,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Create configuration for large graphs (> 100K vertices)
|
||||
pub fn large() -> Self {
|
||||
Self::for_size(100_000)
|
||||
}
|
||||
|
||||
/// Validate configuration
|
||||
pub fn validate(&self) -> Result<(), String> {
|
||||
if self.phi <= 0.0 || self.phi >= 1.0 {
|
||||
return Err(format!("phi must be in (0, 1), got {}", self.phi));
|
||||
}
|
||||
if self.lambda_max == 0 {
|
||||
return Err("lambda_max must be positive".to_string());
|
||||
}
|
||||
if self.epsilon <= 0.0 || self.epsilon >= 1.0 {
|
||||
return Err(format!("epsilon must be in (0, 1), got {}", self.epsilon));
|
||||
}
|
||||
if self.target_levels == 0 {
|
||||
return Err("target_levels must be positive".to_string());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Compute theoretical subpolynomial bound for graph of size n
|
||||
pub fn theoretical_bound(&self, n: usize) -> f64 {
|
||||
if n < 2 {
|
||||
return f64::INFINITY;
|
||||
}
|
||||
let log_n = (n as f64).ln();
|
||||
// 2^{O(log^{1-c} n)} with c = 0.1
|
||||
2.0_f64.powf(log_n.powf(0.9))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_default_config() {
|
||||
let config = MinCutConfig::default();
|
||||
assert!(config.validate().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_for_size() {
|
||||
let small_config = MinCutConfig::for_size(100);
|
||||
let large_config = MinCutConfig::for_size(1_000_000);
|
||||
|
||||
// Larger graphs should have smaller phi
|
||||
assert!(large_config.phi < small_config.phi);
|
||||
|
||||
// Larger graphs should have more levels
|
||||
assert!(large_config.target_levels >= small_config.target_levels);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_theoretical_bound() {
|
||||
let config = MinCutConfig::default();
|
||||
|
||||
let bound_100 = config.theoretical_bound(100);
|
||||
let bound_1m = config.theoretical_bound(1_000_000);
|
||||
|
||||
// Bound should increase with n, but subpolynomially
|
||||
assert!(bound_1m > bound_100);
|
||||
|
||||
// Should be much smaller than n
|
||||
assert!(bound_1m < 1_000_000.0);
|
||||
}
|
||||
}
|
||||
354
vendor/ruvector/crates/prime-radiant/src/mincut/isolation.rs
vendored
Normal file
354
vendor/ruvector/crates/prime-radiant/src/mincut/isolation.rs
vendored
Normal file
@@ -0,0 +1,354 @@
|
||||
//! Isolation Structures
|
||||
//!
|
||||
//! Data structures representing isolated regions and results.
|
||||
|
||||
use super::{EdgeId, VertexId, Weight};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashSet;
|
||||
|
||||
/// Result of an isolation operation
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct IsolationResult {
|
||||
/// Vertices in the isolated region
|
||||
pub isolated_vertices: HashSet<VertexId>,
|
||||
/// Edges in the cut boundary
|
||||
pub cut_edges: Vec<EdgeId>,
|
||||
/// Total weight of the cut (boundary)
|
||||
pub cut_value: f64,
|
||||
/// Number of high-energy edges that triggered isolation
|
||||
pub num_high_energy_edges: usize,
|
||||
/// Threshold used for high-energy classification
|
||||
pub threshold: Weight,
|
||||
/// Whether the cut was verified by witness tree
|
||||
pub is_verified: bool,
|
||||
}
|
||||
|
||||
impl IsolationResult {
|
||||
/// Create a result indicating no isolation needed
|
||||
pub fn no_isolation() -> Self {
|
||||
Self {
|
||||
isolated_vertices: HashSet::new(),
|
||||
cut_edges: vec![],
|
||||
cut_value: 0.0,
|
||||
num_high_energy_edges: 0,
|
||||
threshold: 0.0,
|
||||
is_verified: true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if any vertices were isolated
|
||||
pub fn has_isolation(&self) -> bool {
|
||||
!self.isolated_vertices.is_empty()
|
||||
}
|
||||
|
||||
/// Get number of isolated vertices
|
||||
pub fn num_isolated(&self) -> usize {
|
||||
self.isolated_vertices.len()
|
||||
}
|
||||
|
||||
/// Get number of cut edges
|
||||
pub fn num_cut_edges(&self) -> usize {
|
||||
self.cut_edges.len()
|
||||
}
|
||||
|
||||
/// Calculate isolation efficiency (cut value per isolated vertex)
|
||||
pub fn efficiency(&self) -> f64 {
|
||||
if self.isolated_vertices.is_empty() {
|
||||
return 0.0;
|
||||
}
|
||||
self.cut_value / self.isolated_vertices.len() as f64
|
||||
}
|
||||
|
||||
/// Check if a vertex is in the isolated set
|
||||
pub fn is_isolated(&self, vertex: VertexId) -> bool {
|
||||
self.isolated_vertices.contains(&vertex)
|
||||
}
|
||||
|
||||
/// Get boundary vertices (endpoints of cut edges in isolated set)
|
||||
pub fn boundary_vertices(&self) -> HashSet<VertexId> {
|
||||
let mut boundary = HashSet::new();
|
||||
for (u, v) in &self.cut_edges {
|
||||
if self.isolated_vertices.contains(u) {
|
||||
boundary.insert(*u);
|
||||
}
|
||||
if self.isolated_vertices.contains(v) {
|
||||
boundary.insert(*v);
|
||||
}
|
||||
}
|
||||
boundary
|
||||
}
|
||||
}
|
||||
|
||||
/// A connected region of high-energy edges
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct IsolationRegion {
|
||||
/// Vertices in this region
|
||||
pub vertices: HashSet<VertexId>,
|
||||
/// Internal edges (both endpoints in region)
|
||||
pub internal_edges: Vec<EdgeId>,
|
||||
/// Boundary edges (one endpoint outside region)
|
||||
pub boundary_edges: Vec<EdgeId>,
|
||||
/// Total energy of internal edges
|
||||
pub total_energy: Weight,
|
||||
/// Total weight of boundary edges
|
||||
pub boundary_weight: Weight,
|
||||
/// Unique region identifier
|
||||
pub region_id: usize,
|
||||
}
|
||||
|
||||
impl IsolationRegion {
|
||||
/// Get number of vertices in region
|
||||
pub fn num_vertices(&self) -> usize {
|
||||
self.vertices.len()
|
||||
}
|
||||
|
||||
/// Get number of internal edges
|
||||
pub fn num_internal_edges(&self) -> usize {
|
||||
self.internal_edges.len()
|
||||
}
|
||||
|
||||
/// Get number of boundary edges
|
||||
pub fn num_boundary_edges(&self) -> usize {
|
||||
self.boundary_edges.len()
|
||||
}
|
||||
|
||||
/// Calculate region density (edges per vertex)
|
||||
pub fn density(&self) -> f64 {
|
||||
if self.vertices.is_empty() {
|
||||
return 0.0;
|
||||
}
|
||||
self.internal_edges.len() as f64 / self.vertices.len() as f64
|
||||
}
|
||||
|
||||
/// Calculate boundary ratio (boundary / internal edges)
|
||||
pub fn boundary_ratio(&self) -> f64 {
|
||||
if self.internal_edges.is_empty() {
|
||||
return f64::INFINITY;
|
||||
}
|
||||
self.boundary_edges.len() as f64 / self.internal_edges.len() as f64
|
||||
}
|
||||
|
||||
/// Calculate average energy per edge
|
||||
pub fn avg_energy(&self) -> Weight {
|
||||
if self.internal_edges.is_empty() {
|
||||
return 0.0;
|
||||
}
|
||||
self.total_energy / self.internal_edges.len() as f64
|
||||
}
|
||||
|
||||
/// Check if vertex is in this region
|
||||
pub fn contains(&self, vertex: VertexId) -> bool {
|
||||
self.vertices.contains(&vertex)
|
||||
}
|
||||
|
||||
/// Check if edge is internal to this region
|
||||
pub fn is_internal_edge(&self, edge: &EdgeId) -> bool {
|
||||
self.internal_edges.contains(edge)
|
||||
}
|
||||
|
||||
/// Check if edge is on the boundary
|
||||
pub fn is_boundary_edge(&self, edge: &EdgeId) -> bool {
|
||||
self.boundary_edges.contains(edge)
|
||||
}
|
||||
|
||||
/// Get vertices on the boundary (adjacent to outside)
|
||||
pub fn boundary_vertices(&self) -> HashSet<VertexId> {
|
||||
let mut boundary = HashSet::new();
|
||||
for (u, v) in &self.boundary_edges {
|
||||
if self.vertices.contains(u) {
|
||||
boundary.insert(*u);
|
||||
}
|
||||
if self.vertices.contains(v) {
|
||||
boundary.insert(*v);
|
||||
}
|
||||
}
|
||||
boundary
|
||||
}
|
||||
|
||||
/// Get interior vertices (not on boundary)
|
||||
pub fn interior_vertices(&self) -> HashSet<VertexId> {
|
||||
let boundary = self.boundary_vertices();
|
||||
self.vertices
|
||||
.iter()
|
||||
.filter(|v| !boundary.contains(v))
|
||||
.copied()
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// Comparison result between two isolation results
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct IsolationComparison {
|
||||
/// Vertices isolated in both results
|
||||
pub common_isolated: HashSet<VertexId>,
|
||||
/// Vertices only isolated in first result
|
||||
pub only_first: HashSet<VertexId>,
|
||||
/// Vertices only isolated in second result
|
||||
pub only_second: HashSet<VertexId>,
|
||||
/// Jaccard similarity of isolated sets
|
||||
pub jaccard_similarity: f64,
|
||||
}
|
||||
|
||||
impl IsolationComparison {
|
||||
/// Compare two isolation results
|
||||
pub fn compare(first: &IsolationResult, second: &IsolationResult) -> Self {
|
||||
let common: HashSet<_> = first
|
||||
.isolated_vertices
|
||||
.intersection(&second.isolated_vertices)
|
||||
.copied()
|
||||
.collect();
|
||||
|
||||
let only_first: HashSet<_> = first
|
||||
.isolated_vertices
|
||||
.difference(&second.isolated_vertices)
|
||||
.copied()
|
||||
.collect();
|
||||
|
||||
let only_second: HashSet<_> = second
|
||||
.isolated_vertices
|
||||
.difference(&first.isolated_vertices)
|
||||
.copied()
|
||||
.collect();
|
||||
|
||||
let union_size =
|
||||
first.isolated_vertices.len() + second.isolated_vertices.len() - common.len();
|
||||
let jaccard = if union_size > 0 {
|
||||
common.len() as f64 / union_size as f64
|
||||
} else {
|
||||
1.0 // Both empty = identical
|
||||
};
|
||||
|
||||
Self {
|
||||
common_isolated: common,
|
||||
only_first,
|
||||
only_second,
|
||||
jaccard_similarity: jaccard,
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if results are identical
|
||||
pub fn is_identical(&self) -> bool {
|
||||
self.only_first.is_empty() && self.only_second.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_no_isolation() {
|
||||
let result = IsolationResult::no_isolation();
|
||||
assert!(!result.has_isolation());
|
||||
assert_eq!(result.num_isolated(), 0);
|
||||
assert!(result.is_verified);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_isolation_result() {
|
||||
let mut isolated = HashSet::new();
|
||||
isolated.insert(1);
|
||||
isolated.insert(2);
|
||||
isolated.insert(3);
|
||||
|
||||
let result = IsolationResult {
|
||||
isolated_vertices: isolated,
|
||||
cut_edges: vec![(3, 4), (3, 5)],
|
||||
cut_value: 2.5,
|
||||
num_high_energy_edges: 2,
|
||||
threshold: 1.0,
|
||||
is_verified: true,
|
||||
};
|
||||
|
||||
assert!(result.has_isolation());
|
||||
assert_eq!(result.num_isolated(), 3);
|
||||
assert_eq!(result.num_cut_edges(), 2);
|
||||
assert!(result.is_isolated(1));
|
||||
assert!(!result.is_isolated(4));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_boundary_vertices() {
|
||||
let mut isolated = HashSet::new();
|
||||
isolated.insert(1);
|
||||
isolated.insert(2);
|
||||
isolated.insert(3);
|
||||
|
||||
let result = IsolationResult {
|
||||
isolated_vertices: isolated,
|
||||
cut_edges: vec![(3, 4), (2, 5)],
|
||||
cut_value: 2.0,
|
||||
num_high_energy_edges: 1,
|
||||
threshold: 1.0,
|
||||
is_verified: true,
|
||||
};
|
||||
|
||||
let boundary = result.boundary_vertices();
|
||||
assert!(boundary.contains(&3));
|
||||
assert!(boundary.contains(&2));
|
||||
assert!(!boundary.contains(&1)); // Not on boundary
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_region() {
|
||||
let mut vertices = HashSet::new();
|
||||
vertices.insert(1);
|
||||
vertices.insert(2);
|
||||
vertices.insert(3);
|
||||
|
||||
let region = IsolationRegion {
|
||||
vertices,
|
||||
internal_edges: vec![(1, 2), (2, 3)],
|
||||
boundary_edges: vec![(3, 4)],
|
||||
total_energy: 5.0,
|
||||
boundary_weight: 1.0,
|
||||
region_id: 0,
|
||||
};
|
||||
|
||||
assert_eq!(region.num_vertices(), 3);
|
||||
assert_eq!(region.num_internal_edges(), 2);
|
||||
assert_eq!(region.num_boundary_edges(), 1);
|
||||
assert!((region.avg_energy() - 2.5).abs() < 0.01);
|
||||
assert!(region.contains(1));
|
||||
assert!(!region.contains(4));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_comparison() {
|
||||
let mut isolated1 = HashSet::new();
|
||||
isolated1.insert(1);
|
||||
isolated1.insert(2);
|
||||
isolated1.insert(3);
|
||||
|
||||
let result1 = IsolationResult {
|
||||
isolated_vertices: isolated1,
|
||||
cut_edges: vec![],
|
||||
cut_value: 0.0,
|
||||
num_high_energy_edges: 0,
|
||||
threshold: 1.0,
|
||||
is_verified: true,
|
||||
};
|
||||
|
||||
let mut isolated2 = HashSet::new();
|
||||
isolated2.insert(2);
|
||||
isolated2.insert(3);
|
||||
isolated2.insert(4);
|
||||
|
||||
let result2 = IsolationResult {
|
||||
isolated_vertices: isolated2,
|
||||
cut_edges: vec![],
|
||||
cut_value: 0.0,
|
||||
num_high_energy_edges: 0,
|
||||
threshold: 1.0,
|
||||
is_verified: true,
|
||||
};
|
||||
|
||||
let comparison = IsolationComparison::compare(&result1, &result2);
|
||||
|
||||
assert_eq!(comparison.common_isolated.len(), 2); // {2, 3}
|
||||
assert_eq!(comparison.only_first.len(), 1); // {1}
|
||||
assert_eq!(comparison.only_second.len(), 1); // {4}
|
||||
assert!(!comparison.is_identical());
|
||||
assert!(comparison.jaccard_similarity > 0.0 && comparison.jaccard_similarity < 1.0);
|
||||
}
|
||||
}
|
||||
300
vendor/ruvector/crates/prime-radiant/src/mincut/metrics.rs
vendored
Normal file
300
vendor/ruvector/crates/prime-radiant/src/mincut/metrics.rs
vendored
Normal file
@@ -0,0 +1,300 @@
|
||||
//! Isolation Metrics
|
||||
//!
|
||||
//! Tracking and analysis of isolation operations.
|
||||
|
||||
use super::IsolationResult;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
/// Metrics for tracking isolation operations
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct IsolationMetrics {
|
||||
/// Total number of isolation queries
|
||||
pub total_queries: u64,
|
||||
/// Number of queries that found isolation
|
||||
pub queries_with_isolation: u64,
|
||||
/// Total vertices isolated across all queries
|
||||
pub total_vertices_isolated: u64,
|
||||
/// Total cut edges across all queries
|
||||
pub total_cut_edges: u64,
|
||||
/// Total cut value across all queries
|
||||
pub total_cut_value: f64,
|
||||
/// Average vertices isolated per query (that had isolation)
|
||||
pub avg_vertices_isolated: f64,
|
||||
/// Average cut value per query (that had isolation)
|
||||
pub avg_cut_value: f64,
|
||||
/// Number of build operations
|
||||
pub num_builds: u64,
|
||||
/// Number of incremental updates
|
||||
pub num_updates: u64,
|
||||
/// Maximum single isolation size
|
||||
pub max_isolation_size: usize,
|
||||
/// Minimum non-zero cut value
|
||||
pub min_cut_value: f64,
|
||||
/// Start time for tracking
|
||||
#[serde(skip)]
|
||||
start_time: Option<Instant>,
|
||||
/// Total time spent in isolation queries (microseconds)
|
||||
pub total_query_time_us: u64,
|
||||
}
|
||||
|
||||
impl IsolationMetrics {
|
||||
/// Create new metrics tracker
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
total_queries: 0,
|
||||
queries_with_isolation: 0,
|
||||
total_vertices_isolated: 0,
|
||||
total_cut_edges: 0,
|
||||
total_cut_value: 0.0,
|
||||
avg_vertices_isolated: 0.0,
|
||||
avg_cut_value: 0.0,
|
||||
num_builds: 0,
|
||||
num_updates: 0,
|
||||
max_isolation_size: 0,
|
||||
min_cut_value: f64::INFINITY,
|
||||
start_time: None,
|
||||
total_query_time_us: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Record an isolation query result
|
||||
pub fn record_isolation(&mut self, result: &IsolationResult) {
|
||||
self.total_queries += 1;
|
||||
|
||||
if result.has_isolation() {
|
||||
self.queries_with_isolation += 1;
|
||||
self.total_vertices_isolated += result.num_isolated() as u64;
|
||||
self.total_cut_edges += result.num_cut_edges() as u64;
|
||||
self.total_cut_value += result.cut_value;
|
||||
|
||||
self.max_isolation_size = self.max_isolation_size.max(result.num_isolated());
|
||||
|
||||
if result.cut_value > 0.0 {
|
||||
self.min_cut_value = self.min_cut_value.min(result.cut_value);
|
||||
}
|
||||
|
||||
// Update averages
|
||||
self.avg_vertices_isolated =
|
||||
self.total_vertices_isolated as f64 / self.queries_with_isolation as f64;
|
||||
self.avg_cut_value = self.total_cut_value / self.queries_with_isolation as f64;
|
||||
}
|
||||
}
|
||||
|
||||
/// Record a build operation
|
||||
pub fn record_build(&mut self) {
|
||||
self.num_builds += 1;
|
||||
}
|
||||
|
||||
/// Record an incremental update
|
||||
pub fn record_update(&mut self) {
|
||||
self.num_updates += 1;
|
||||
}
|
||||
|
||||
/// Start timing a query
|
||||
pub fn start_query(&mut self) {
|
||||
self.start_time = Some(Instant::now());
|
||||
}
|
||||
|
||||
/// End timing a query
|
||||
pub fn end_query(&mut self) {
|
||||
if let Some(start) = self.start_time.take() {
|
||||
self.total_query_time_us += start.elapsed().as_micros() as u64;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get isolation rate (queries with isolation / total queries)
|
||||
pub fn isolation_rate(&self) -> f64 {
|
||||
if self.total_queries == 0 {
|
||||
return 0.0;
|
||||
}
|
||||
self.queries_with_isolation as f64 / self.total_queries as f64
|
||||
}
|
||||
|
||||
/// Get average query time in microseconds
|
||||
pub fn avg_query_time_us(&self) -> f64 {
|
||||
if self.total_queries == 0 {
|
||||
return 0.0;
|
||||
}
|
||||
self.total_query_time_us as f64 / self.total_queries as f64
|
||||
}
|
||||
|
||||
/// Get updates per build ratio
|
||||
pub fn updates_per_build(&self) -> f64 {
|
||||
if self.num_builds == 0 {
|
||||
return self.num_updates as f64;
|
||||
}
|
||||
self.num_updates as f64 / self.num_builds as f64
|
||||
}
|
||||
|
||||
/// Get efficiency (vertices isolated per cut value)
|
||||
pub fn isolation_efficiency(&self) -> f64 {
|
||||
if self.total_cut_value < 1e-10 {
|
||||
return 0.0;
|
||||
}
|
||||
self.total_vertices_isolated as f64 / self.total_cut_value
|
||||
}
|
||||
|
||||
/// Reset all metrics
|
||||
pub fn reset(&mut self) {
|
||||
*self = Self::new();
|
||||
}
|
||||
|
||||
/// Create a summary report
|
||||
pub fn summary(&self) -> MetricsSummary {
|
||||
MetricsSummary {
|
||||
total_queries: self.total_queries,
|
||||
isolation_rate: self.isolation_rate(),
|
||||
avg_vertices_isolated: self.avg_vertices_isolated,
|
||||
avg_cut_value: self.avg_cut_value,
|
||||
avg_query_time_us: self.avg_query_time_us(),
|
||||
max_isolation_size: self.max_isolation_size,
|
||||
updates_per_build: self.updates_per_build(),
|
||||
isolation_efficiency: self.isolation_efficiency(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for IsolationMetrics {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Summary of isolation metrics
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct MetricsSummary {
|
||||
/// Total isolation queries
|
||||
pub total_queries: u64,
|
||||
/// Rate of queries that found isolation
|
||||
pub isolation_rate: f64,
|
||||
/// Average vertices isolated per successful query
|
||||
pub avg_vertices_isolated: f64,
|
||||
/// Average cut value per successful query
|
||||
pub avg_cut_value: f64,
|
||||
/// Average query time in microseconds
|
||||
pub avg_query_time_us: f64,
|
||||
/// Maximum single isolation size
|
||||
pub max_isolation_size: usize,
|
||||
/// Updates per build operation
|
||||
pub updates_per_build: f64,
|
||||
/// Vertices isolated per unit cut value
|
||||
pub isolation_efficiency: f64,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for MetricsSummary {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
writeln!(f, "Isolation Metrics Summary:")?;
|
||||
writeln!(f, " Total queries: {}", self.total_queries)?;
|
||||
writeln!(f, " Isolation rate: {:.2}%", self.isolation_rate * 100.0)?;
|
||||
writeln!(
|
||||
f,
|
||||
" Avg vertices isolated: {:.2}",
|
||||
self.avg_vertices_isolated
|
||||
)?;
|
||||
writeln!(f, " Avg cut value: {:.4}", self.avg_cut_value)?;
|
||||
writeln!(f, " Avg query time: {:.2} us", self.avg_query_time_us)?;
|
||||
writeln!(f, " Max isolation size: {}", self.max_isolation_size)?;
|
||||
writeln!(f, " Updates per build: {:.2}", self.updates_per_build)?;
|
||||
writeln!(
|
||||
f,
|
||||
" Isolation efficiency: {:.4}",
|
||||
self.isolation_efficiency
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::collections::HashSet;
|
||||
|
||||
fn make_result(num_isolated: usize, cut_value: f64) -> IsolationResult {
|
||||
let mut isolated = HashSet::new();
|
||||
for i in 0..num_isolated {
|
||||
isolated.insert(i as u64);
|
||||
}
|
||||
|
||||
IsolationResult {
|
||||
isolated_vertices: isolated,
|
||||
cut_edges: vec![(0, 100)], // dummy
|
||||
cut_value,
|
||||
num_high_energy_edges: 1,
|
||||
threshold: 1.0,
|
||||
is_verified: true,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_new_metrics() {
|
||||
let metrics = IsolationMetrics::new();
|
||||
assert_eq!(metrics.total_queries, 0);
|
||||
assert_eq!(metrics.isolation_rate(), 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_record_isolation() {
|
||||
let mut metrics = IsolationMetrics::new();
|
||||
|
||||
let result = make_result(5, 2.5);
|
||||
metrics.record_isolation(&result);
|
||||
|
||||
assert_eq!(metrics.total_queries, 1);
|
||||
assert_eq!(metrics.queries_with_isolation, 1);
|
||||
assert_eq!(metrics.total_vertices_isolated, 5);
|
||||
assert!((metrics.avg_cut_value - 2.5).abs() < 0.01);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_no_isolation() {
|
||||
let mut metrics = IsolationMetrics::new();
|
||||
|
||||
let result = IsolationResult::no_isolation();
|
||||
metrics.record_isolation(&result);
|
||||
|
||||
assert_eq!(metrics.total_queries, 1);
|
||||
assert_eq!(metrics.queries_with_isolation, 0);
|
||||
assert_eq!(metrics.isolation_rate(), 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_queries() {
|
||||
let mut metrics = IsolationMetrics::new();
|
||||
|
||||
metrics.record_isolation(&make_result(5, 2.0));
|
||||
metrics.record_isolation(&make_result(10, 3.0));
|
||||
metrics.record_isolation(&IsolationResult::no_isolation());
|
||||
|
||||
assert_eq!(metrics.total_queries, 3);
|
||||
assert_eq!(metrics.queries_with_isolation, 2);
|
||||
assert!((metrics.isolation_rate() - 2.0 / 3.0).abs() < 0.01);
|
||||
assert_eq!(metrics.max_isolation_size, 10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_and_update() {
|
||||
let mut metrics = IsolationMetrics::new();
|
||||
|
||||
metrics.record_build();
|
||||
metrics.record_update();
|
||||
metrics.record_update();
|
||||
metrics.record_update();
|
||||
|
||||
assert_eq!(metrics.num_builds, 1);
|
||||
assert_eq!(metrics.num_updates, 3);
|
||||
assert!((metrics.updates_per_build() - 3.0).abs() < 0.01);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_summary() {
|
||||
let mut metrics = IsolationMetrics::new();
|
||||
|
||||
metrics.record_isolation(&make_result(5, 2.0));
|
||||
metrics.record_isolation(&make_result(10, 3.0));
|
||||
|
||||
let summary = metrics.summary();
|
||||
assert_eq!(summary.total_queries, 2);
|
||||
assert!((summary.isolation_rate - 1.0).abs() < 0.01);
|
||||
assert!((summary.avg_vertices_isolated - 7.5).abs() < 0.01);
|
||||
}
|
||||
}
|
||||
528
vendor/ruvector/crates/prime-radiant/src/mincut/mod.rs
vendored
Normal file
528
vendor/ruvector/crates/prime-radiant/src/mincut/mod.rs
vendored
Normal file
@@ -0,0 +1,528 @@
|
||||
//! MinCut Incoherence Isolation Module
|
||||
//!
|
||||
//! Isolates incoherent subgraphs using subpolynomial n^o(1) dynamic minimum cut.
|
||||
//! Leverages `ruvector-mincut` for the December 2024 breakthrough algorithm.
|
||||
//!
|
||||
//! # Features
|
||||
//!
|
||||
//! - Subpolynomial O(n^o(1)) update time for dynamic graphs
|
||||
//! - Incoherent region isolation with minimum boundary
|
||||
//! - Certificate-based cut verification with witness trees
|
||||
//! - SNN-based cognitive optimization
|
||||
//!
|
||||
//! # Use Cases
|
||||
//!
|
||||
//! - Isolate high-energy (incoherent) subgraphs for focused repair
|
||||
//! - Find minimum cuts to quarantine problematic regions
|
||||
//! - Dynamic graph updates with fast recomputation
|
||||
|
||||
mod adapter;
|
||||
mod config;
|
||||
mod isolation;
|
||||
mod metrics;
|
||||
|
||||
pub use adapter::MinCutAdapter;
|
||||
pub use config::MinCutConfig;
|
||||
pub use isolation::{IsolationRegion, IsolationResult};
|
||||
pub use metrics::IsolationMetrics;
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
/// Vertex identifier type
|
||||
pub type VertexId = u64;
|
||||
|
||||
/// Edge identifier type
|
||||
pub type EdgeId = (VertexId, VertexId);
|
||||
|
||||
/// Weight type for edges
|
||||
pub type Weight = f64;
|
||||
|
||||
/// Result type for mincut operations
|
||||
pub type Result<T> = std::result::Result<T, MinCutError>;
|
||||
|
||||
/// Errors that can occur in mincut operations
|
||||
#[derive(Debug, Clone, thiserror::Error)]
|
||||
pub enum MinCutError {
|
||||
/// Edge already exists
|
||||
#[error("Edge already exists: ({0}, {1})")]
|
||||
EdgeExists(VertexId, VertexId),
|
||||
|
||||
/// Edge not found
|
||||
#[error("Edge not found: ({0}, {1})")]
|
||||
EdgeNotFound(VertexId, VertexId),
|
||||
|
||||
/// Vertex not found
|
||||
#[error("Vertex not found: {0}")]
|
||||
VertexNotFound(VertexId),
|
||||
|
||||
/// Graph is empty
|
||||
#[error("Graph is empty")]
|
||||
EmptyGraph,
|
||||
|
||||
/// Invalid threshold
|
||||
#[error("Invalid threshold: {0}")]
|
||||
InvalidThreshold(f64),
|
||||
|
||||
/// Cut computation failed
|
||||
#[error("Cut computation failed: {0}")]
|
||||
ComputationFailed(String),
|
||||
|
||||
/// Hierarchy not built
|
||||
#[error("Hierarchy not built - call build() first")]
|
||||
HierarchyNotBuilt,
|
||||
}
|
||||
|
||||
/// Main incoherence isolator using subpolynomial mincut
|
||||
///
|
||||
/// This module identifies and isolates regions of the coherence graph
|
||||
/// where energy is above threshold, using minimum cut to find the
|
||||
/// boundary with smallest total weight.
|
||||
#[derive(Debug)]
|
||||
pub struct IncoherenceIsolator {
|
||||
/// Configuration
|
||||
config: MinCutConfig,
|
||||
/// Adapter to underlying mincut algorithm
|
||||
adapter: MinCutAdapter,
|
||||
/// Edge weights (typically residual energy)
|
||||
edge_weights: HashMap<EdgeId, Weight>,
|
||||
/// Vertex set
|
||||
vertices: HashSet<VertexId>,
|
||||
/// Is hierarchy built?
|
||||
hierarchy_built: bool,
|
||||
/// Isolation metrics
|
||||
metrics: IsolationMetrics,
|
||||
}
|
||||
|
||||
impl IncoherenceIsolator {
|
||||
/// Create a new incoherence isolator
|
||||
pub fn new(config: MinCutConfig) -> Self {
|
||||
let adapter = MinCutAdapter::new(config.clone());
|
||||
|
||||
Self {
|
||||
config,
|
||||
adapter,
|
||||
edge_weights: HashMap::new(),
|
||||
vertices: HashSet::new(),
|
||||
hierarchy_built: false,
|
||||
metrics: IsolationMetrics::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create with default configuration
|
||||
pub fn default_config() -> Self {
|
||||
Self::new(MinCutConfig::default())
|
||||
}
|
||||
|
||||
/// Create optimized for expected graph size
|
||||
pub fn for_size(expected_vertices: usize) -> Self {
|
||||
Self::new(MinCutConfig::for_size(expected_vertices))
|
||||
}
|
||||
|
||||
/// Insert an edge with weight
|
||||
pub fn insert_edge(&mut self, u: VertexId, v: VertexId, weight: Weight) -> Result<()> {
|
||||
let key = Self::edge_key(u, v);
|
||||
|
||||
if self.edge_weights.contains_key(&key) {
|
||||
return Err(MinCutError::EdgeExists(u, v));
|
||||
}
|
||||
|
||||
self.edge_weights.insert(key, weight);
|
||||
self.vertices.insert(u);
|
||||
self.vertices.insert(v);
|
||||
|
||||
// Update adapter
|
||||
self.adapter.insert_edge(u, v, weight)?;
|
||||
|
||||
// If hierarchy was built, track this as an incremental update
|
||||
if self.hierarchy_built {
|
||||
self.metrics.record_update();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Delete an edge
|
||||
pub fn delete_edge(&mut self, u: VertexId, v: VertexId) -> Result<()> {
|
||||
let key = Self::edge_key(u, v);
|
||||
|
||||
if !self.edge_weights.contains_key(&key) {
|
||||
return Err(MinCutError::EdgeNotFound(u, v));
|
||||
}
|
||||
|
||||
self.edge_weights.remove(&key);
|
||||
self.adapter.delete_edge(u, v)?;
|
||||
|
||||
if self.hierarchy_built {
|
||||
self.metrics.record_update();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Update edge weight
|
||||
pub fn update_weight(&mut self, u: VertexId, v: VertexId, weight: Weight) -> Result<()> {
|
||||
let key = Self::edge_key(u, v);
|
||||
|
||||
if !self.edge_weights.contains_key(&key) {
|
||||
return Err(MinCutError::EdgeNotFound(u, v));
|
||||
}
|
||||
|
||||
// Delete and re-insert with new weight
|
||||
self.adapter.delete_edge(u, v)?;
|
||||
self.adapter.insert_edge(u, v, weight)?;
|
||||
self.edge_weights.insert(key, weight);
|
||||
|
||||
if self.hierarchy_built {
|
||||
self.metrics.record_update();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Build the multi-level hierarchy for subpolynomial updates
|
||||
///
|
||||
/// This creates O(log^{1/4} n) levels of expander decomposition.
|
||||
pub fn build(&mut self) {
|
||||
if self.edge_weights.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
self.adapter.build();
|
||||
self.hierarchy_built = true;
|
||||
self.metrics.record_build();
|
||||
}
|
||||
|
||||
/// Get global minimum cut value
|
||||
pub fn min_cut_value(&self) -> Result<f64> {
|
||||
if !self.hierarchy_built {
|
||||
return Err(MinCutError::HierarchyNotBuilt);
|
||||
}
|
||||
Ok(self.adapter.min_cut_value())
|
||||
}
|
||||
|
||||
/// Find minimum cut to isolate high-energy region
|
||||
///
|
||||
/// Returns the cut that separates vertices with edges above `threshold`
|
||||
/// from the rest of the graph.
|
||||
pub fn isolate_high_energy(&mut self, threshold: Weight) -> Result<IsolationResult> {
|
||||
if !self.hierarchy_built {
|
||||
return Err(MinCutError::HierarchyNotBuilt);
|
||||
}
|
||||
|
||||
if threshold <= 0.0 {
|
||||
return Err(MinCutError::InvalidThreshold(threshold));
|
||||
}
|
||||
|
||||
// Identify high-energy edges
|
||||
let high_energy_edges: Vec<EdgeId> = self
|
||||
.edge_weights
|
||||
.iter()
|
||||
.filter(|(_, &w)| w > threshold)
|
||||
.map(|(&k, _)| k)
|
||||
.collect();
|
||||
|
||||
if high_energy_edges.is_empty() {
|
||||
return Ok(IsolationResult::no_isolation());
|
||||
}
|
||||
|
||||
// Get vertices incident to high-energy edges
|
||||
let mut high_energy_vertices: HashSet<VertexId> = HashSet::new();
|
||||
for (u, v) in &high_energy_edges {
|
||||
high_energy_vertices.insert(*u);
|
||||
high_energy_vertices.insert(*v);
|
||||
}
|
||||
|
||||
// Compute isolation using adapter
|
||||
let cut_result = self.adapter.compute_isolation(&high_energy_vertices)?;
|
||||
|
||||
let result = IsolationResult {
|
||||
isolated_vertices: cut_result.isolated_set,
|
||||
cut_edges: cut_result.cut_edges,
|
||||
cut_value: cut_result.cut_value,
|
||||
num_high_energy_edges: high_energy_edges.len(),
|
||||
threshold,
|
||||
is_verified: cut_result.is_verified,
|
||||
};
|
||||
|
||||
self.metrics.record_isolation(&result);
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Find multiple isolated regions using iterative mincut
|
||||
pub fn find_isolated_regions(&mut self, threshold: Weight) -> Result<Vec<IsolationRegion>> {
|
||||
if !self.hierarchy_built {
|
||||
return Err(MinCutError::HierarchyNotBuilt);
|
||||
}
|
||||
|
||||
// Get high-energy edges
|
||||
let high_energy_edges: Vec<(EdgeId, Weight)> = self
|
||||
.edge_weights
|
||||
.iter()
|
||||
.filter(|(_, &w)| w > threshold)
|
||||
.map(|(&k, &w)| (k, w))
|
||||
.collect();
|
||||
|
||||
if high_energy_edges.is_empty() {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
|
||||
// Group connected components of high-energy edges
|
||||
let mut regions: Vec<IsolationRegion> = Vec::new();
|
||||
let mut visited: HashSet<VertexId> = HashSet::new();
|
||||
|
||||
for ((u, v), weight) in &high_energy_edges {
|
||||
if visited.contains(u) && visited.contains(v) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// BFS to find connected component
|
||||
let mut component_vertices: HashSet<VertexId> = HashSet::new();
|
||||
let mut component_edges: Vec<EdgeId> = Vec::new();
|
||||
let mut queue: Vec<VertexId> = vec![*u, *v];
|
||||
let mut component_energy = 0.0;
|
||||
|
||||
while let Some(vertex) = queue.pop() {
|
||||
if visited.contains(&vertex) {
|
||||
continue;
|
||||
}
|
||||
visited.insert(vertex);
|
||||
component_vertices.insert(vertex);
|
||||
|
||||
// Find adjacent high-energy edges
|
||||
for ((eu, ev), ew) in &high_energy_edges {
|
||||
if *eu == vertex || *ev == vertex {
|
||||
if !component_edges.contains(&(*eu, *ev)) {
|
||||
component_edges.push((*eu, *ev));
|
||||
component_energy += ew;
|
||||
}
|
||||
if !visited.contains(eu) {
|
||||
queue.push(*eu);
|
||||
}
|
||||
if !visited.contains(ev) {
|
||||
queue.push(*ev);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Compute boundary
|
||||
let boundary_edges: Vec<EdgeId> = self
|
||||
.edge_weights
|
||||
.keys()
|
||||
.filter(|(a, b)| {
|
||||
(component_vertices.contains(a) && !component_vertices.contains(b))
|
||||
|| (component_vertices.contains(b) && !component_vertices.contains(a))
|
||||
})
|
||||
.copied()
|
||||
.collect();
|
||||
|
||||
let boundary_weight: Weight = boundary_edges
|
||||
.iter()
|
||||
.filter_map(|e| self.edge_weights.get(e))
|
||||
.sum();
|
||||
|
||||
regions.push(IsolationRegion {
|
||||
vertices: component_vertices,
|
||||
internal_edges: component_edges,
|
||||
boundary_edges,
|
||||
total_energy: component_energy,
|
||||
boundary_weight,
|
||||
region_id: regions.len(),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(regions)
|
||||
}
|
||||
|
||||
/// Check if updates maintain subpolynomial complexity
|
||||
pub fn is_subpolynomial(&self) -> bool {
|
||||
self.adapter.is_subpolynomial()
|
||||
}
|
||||
|
||||
/// Get recourse statistics
|
||||
pub fn recourse_stats(&self) -> RecourseStats {
|
||||
self.adapter.recourse_stats()
|
||||
}
|
||||
|
||||
/// Get hierarchy statistics
|
||||
pub fn hierarchy_stats(&self) -> HierarchyStats {
|
||||
self.adapter.hierarchy_stats()
|
||||
}
|
||||
|
||||
/// Get isolation metrics
|
||||
pub fn metrics(&self) -> &IsolationMetrics {
|
||||
&self.metrics
|
||||
}
|
||||
|
||||
/// Get number of vertices
|
||||
pub fn num_vertices(&self) -> usize {
|
||||
self.vertices.len()
|
||||
}
|
||||
|
||||
/// Get number of edges
|
||||
pub fn num_edges(&self) -> usize {
|
||||
self.edge_weights.len()
|
||||
}
|
||||
|
||||
/// Get configuration
|
||||
pub fn config(&self) -> &MinCutConfig {
|
||||
&self.config
|
||||
}
|
||||
|
||||
/// Canonical edge key (smaller vertex first)
|
||||
fn edge_key(u: VertexId, v: VertexId) -> EdgeId {
|
||||
if u < v {
|
||||
(u, v)
|
||||
} else {
|
||||
(v, u)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Recourse statistics from the subpolynomial algorithm
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct RecourseStats {
|
||||
/// Total recourse across all updates
|
||||
pub total_recourse: u64,
|
||||
/// Number of updates
|
||||
pub num_updates: u64,
|
||||
/// Maximum single update recourse
|
||||
pub max_single_recourse: u64,
|
||||
/// Average update time in microseconds
|
||||
pub avg_update_time_us: f64,
|
||||
/// Theoretical subpolynomial bound
|
||||
pub theoretical_bound: f64,
|
||||
}
|
||||
|
||||
impl RecourseStats {
|
||||
/// Get amortized recourse per update
|
||||
pub fn amortized_recourse(&self) -> f64 {
|
||||
if self.num_updates == 0 {
|
||||
0.0
|
||||
} else {
|
||||
self.total_recourse as f64 / self.num_updates as f64
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if within theoretical bounds
|
||||
pub fn within_bounds(&self) -> bool {
|
||||
self.amortized_recourse() <= self.theoretical_bound
|
||||
}
|
||||
}
|
||||
|
||||
/// Hierarchy statistics
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct HierarchyStats {
|
||||
/// Number of levels
|
||||
pub num_levels: usize,
|
||||
/// Expanders per level
|
||||
pub expanders_per_level: Vec<usize>,
|
||||
/// Total expanders
|
||||
pub total_expanders: usize,
|
||||
/// Average expander size
|
||||
pub avg_expander_size: f64,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_basic_operations() {
|
||||
let mut isolator = IncoherenceIsolator::default_config();
|
||||
|
||||
// Build a simple graph
|
||||
isolator.insert_edge(1, 2, 0.5).unwrap();
|
||||
isolator.insert_edge(2, 3, 0.5).unwrap();
|
||||
isolator.insert_edge(3, 4, 2.0).unwrap(); // High energy
|
||||
isolator.insert_edge(4, 5, 0.5).unwrap();
|
||||
isolator.insert_edge(5, 6, 0.5).unwrap();
|
||||
|
||||
assert_eq!(isolator.num_vertices(), 6);
|
||||
assert_eq!(isolator.num_edges(), 5);
|
||||
|
||||
isolator.build();
|
||||
|
||||
// Get min cut value
|
||||
let cut = isolator.min_cut_value().unwrap();
|
||||
assert!(cut > 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_isolation() {
|
||||
let mut isolator = IncoherenceIsolator::default_config();
|
||||
|
||||
// Two clusters connected by high-energy edge
|
||||
isolator.insert_edge(1, 2, 0.1).unwrap();
|
||||
isolator.insert_edge(2, 3, 0.1).unwrap();
|
||||
isolator.insert_edge(3, 1, 0.1).unwrap();
|
||||
|
||||
isolator.insert_edge(3, 4, 5.0).unwrap(); // High energy bridge
|
||||
|
||||
isolator.insert_edge(4, 5, 0.1).unwrap();
|
||||
isolator.insert_edge(5, 6, 0.1).unwrap();
|
||||
isolator.insert_edge(6, 4, 0.1).unwrap();
|
||||
|
||||
isolator.build();
|
||||
|
||||
let result = isolator.isolate_high_energy(1.0).unwrap();
|
||||
|
||||
assert_eq!(result.num_high_energy_edges, 1);
|
||||
assert!(result.cut_value >= 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_regions() {
|
||||
let mut isolator = IncoherenceIsolator::default_config();
|
||||
|
||||
// Create two separate high-energy regions
|
||||
isolator.insert_edge(1, 2, 5.0).unwrap();
|
||||
isolator.insert_edge(2, 3, 0.1).unwrap();
|
||||
|
||||
isolator.insert_edge(10, 11, 5.0).unwrap();
|
||||
isolator.insert_edge(11, 12, 5.0).unwrap();
|
||||
|
||||
// Connect them with low-energy edge
|
||||
isolator.insert_edge(3, 10, 0.1).unwrap();
|
||||
|
||||
isolator.build();
|
||||
|
||||
let regions = isolator.find_isolated_regions(1.0).unwrap();
|
||||
|
||||
// Should find 2 high-energy regions
|
||||
assert!(regions.len() >= 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_update_weight() {
|
||||
let mut isolator = IncoherenceIsolator::default_config();
|
||||
|
||||
isolator.insert_edge(1, 2, 0.5).unwrap();
|
||||
isolator.insert_edge(2, 3, 0.5).unwrap();
|
||||
|
||||
isolator.build();
|
||||
|
||||
// Update weight
|
||||
isolator.update_weight(1, 2, 2.0).unwrap();
|
||||
|
||||
// Rebuild and check
|
||||
isolator.build();
|
||||
assert!(isolator.min_cut_value().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_edge() {
|
||||
let mut isolator = IncoherenceIsolator::default_config();
|
||||
|
||||
isolator.insert_edge(1, 2, 0.5).unwrap();
|
||||
isolator.insert_edge(2, 3, 0.5).unwrap();
|
||||
isolator.insert_edge(3, 1, 0.5).unwrap();
|
||||
|
||||
assert_eq!(isolator.num_edges(), 3);
|
||||
|
||||
isolator.delete_edge(1, 2).unwrap();
|
||||
|
||||
assert_eq!(isolator.num_edges(), 2);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user