//! Benchmarks for dynamic mincut updates //! //! ADR-014 Performance Target: n^o(1) amortized time per update //! //! The mincut algorithm isolates incoherent subgraphs using //! subpolynomial dynamic updates. use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion, Throughput}; use std::collections::{HashMap, HashSet, VecDeque}; // ============================================================================ // Dynamic MinCut Types (Simulated for benchmarking) // ============================================================================ /// Edge in dynamic graph #[derive(Clone, Copy)] pub struct Edge { pub source: u64, pub target: u64, pub weight: f64, } /// Dynamic graph with mincut tracking pub struct DynamicGraph { /// Adjacency lists adjacency: HashMap>, /// Total edge count edge_count: usize, /// Vertex count vertex_count: usize, /// Cached connected components components: Option>>, /// Modification counter for cache invalidation mod_count: u64, } impl DynamicGraph { pub fn new() -> Self { Self { adjacency: HashMap::new(), edge_count: 0, vertex_count: 0, components: None, mod_count: 0, } } pub fn with_capacity(vertices: usize, _edges: usize) -> Self { Self { adjacency: HashMap::with_capacity(vertices), edge_count: 0, vertex_count: 0, components: None, mod_count: 0, } } /// Insert edge pub fn insert_edge(&mut self, source: u64, target: u64, weight: f64) -> bool { self.components = None; self.mod_count += 1; let adj = self.adjacency.entry(source).or_insert_with(HashMap::new); if adj.contains_key(&target) { return false; } adj.insert(target, weight); let adj = self.adjacency.entry(target).or_insert_with(HashMap::new); adj.insert(source, weight); self.edge_count += 1; self.vertex_count = self.adjacency.len(); true } /// Delete edge pub fn delete_edge(&mut self, source: u64, target: u64) -> bool { self.components = None; self.mod_count += 1; let removed = if let Some(adj) = self.adjacency.get_mut(&source) { adj.remove(&target).is_some() } else { false }; if removed { if let Some(adj) = self.adjacency.get_mut(&target) { adj.remove(&source); } self.edge_count -= 1; } removed } /// Check if edge exists pub fn has_edge(&self, source: u64, target: u64) -> bool { self.adjacency .get(&source) .map(|adj| adj.contains_key(&target)) .unwrap_or(false) } /// Get vertex degree pub fn degree(&self, vertex: u64) -> usize { self.adjacency .get(&vertex) .map(|adj| adj.len()) .unwrap_or(0) } /// Get neighbors pub fn neighbors(&self, vertex: u64) -> Vec { self.adjacency .get(&vertex) .map(|adj| adj.keys().copied().collect()) .unwrap_or_default() } /// Compute connected components using BFS pub fn connected_components(&mut self) -> &Vec> { if self.components.is_some() { return self.components.as_ref().unwrap(); } let mut visited = HashSet::new(); let mut components = Vec::new(); for &vertex in self.adjacency.keys() { if visited.contains(&vertex) { continue; } let mut component = HashSet::new(); let mut queue = VecDeque::new(); queue.push_back(vertex); while let Some(v) = queue.pop_front() { if visited.insert(v) { component.insert(v); if let Some(neighbors) = self.adjacency.get(&v) { for &neighbor in neighbors.keys() { if !visited.contains(&neighbor) { queue.push_back(neighbor); } } } } } components.push(component); } self.components = Some(components); self.components.as_ref().unwrap() } /// Check if graph is connected pub fn is_connected(&mut self) -> bool { let components = self.connected_components(); components.len() <= 1 } /// Get edges as list pub fn edges(&self) -> Vec { let mut edges = Vec::with_capacity(self.edge_count); let mut seen = HashSet::new(); for (&source, neighbors) in &self.adjacency { for (&target, &weight) in neighbors { let key = if source < target { (source, target) } else { (target, source) }; if seen.insert(key) { edges.push(Edge { source, target, weight, }); } } } edges } /// Get graph statistics pub fn stats(&self) -> GraphStats { GraphStats { vertices: self.vertex_count, edges: self.edge_count, max_degree: self .adjacency .values() .map(|adj| adj.len()) .max() .unwrap_or(0), avg_degree: if self.vertex_count > 0 { (self.edge_count * 2) as f64 / self.vertex_count as f64 } else { 0.0 }, } } } pub struct GraphStats { pub vertices: usize, pub edges: usize, pub max_degree: usize, pub avg_degree: f64, } /// Subpolynomial MinCut (simplified simulation) /// Real implementation would use randomized contraction or tree packing pub struct SubpolynomialMinCut { graph: DynamicGraph, /// Cached mincut value cached_mincut: Option, /// Update count since last computation updates_since_compute: usize, /// Threshold for recomputation recompute_threshold: usize, } impl SubpolynomialMinCut { pub fn new() -> Self { Self { graph: DynamicGraph::new(), cached_mincut: None, updates_since_compute: 0, recompute_threshold: 10, } } pub fn with_capacity(vertices: usize, edges: usize) -> Self { Self { graph: DynamicGraph::with_capacity(vertices, edges), cached_mincut: None, updates_since_compute: 0, recompute_threshold: ((vertices as f64).sqrt() as usize).max(10), } } /// Insert edge with lazy mincut update pub fn insert_edge(&mut self, source: u64, target: u64, weight: f64) -> bool { let result = self.graph.insert_edge(source, target, weight); if result { self.updates_since_compute += 1; // Mincut can only decrease or stay same on edge insertion // So we can keep cached value as upper bound } result } /// Delete edge with lazy mincut update pub fn delete_edge(&mut self, source: u64, target: u64) -> bool { let result = self.graph.delete_edge(source, target); if result { self.updates_since_compute += 1; // Mincut might have decreased, invalidate cache self.cached_mincut = None; } result } /// Compute mincut (lazy - uses cache if available) pub fn min_cut(&mut self) -> f64 { if let Some(cached) = self.cached_mincut { if self.updates_since_compute < self.recompute_threshold { return cached; } } // Simplified: use min degree as lower bound approximation // Real implementation: Karger's algorithm or tree packing let mincut = self.compute_mincut_approximation(); self.cached_mincut = Some(mincut); self.updates_since_compute = 0; mincut } /// Approximate mincut using min degree heuristic fn compute_mincut_approximation(&self) -> f64 { // Min cut <= min weighted degree let mut min_cut = f64::MAX; for (_vertex, neighbors) in &self.graph.adjacency { let weighted_degree: f64 = neighbors.values().sum(); if weighted_degree < min_cut { min_cut = weighted_degree; } } if min_cut == f64::MAX { 0.0 } else { min_cut } } /// Get partition (simplified: just split by component) pub fn partition(&mut self) -> (HashSet, HashSet) { let components = self.graph.connected_components(); if components.is_empty() { return (HashSet::new(), HashSet::new()); } if components.len() == 1 { // Single component - split roughly in half let vertices: Vec<_> = components[0].iter().copied().collect(); let mid = vertices.len() / 2; let left: HashSet<_> = vertices[..mid].iter().copied().collect(); let right: HashSet<_> = vertices[mid..].iter().copied().collect(); (left, right) } else { // Multiple components - use first vs rest let left = components[0].clone(); let right: HashSet<_> = components[1..] .iter() .flat_map(|c| c.iter()) .copied() .collect(); (left, right) } } } // ============================================================================ // Test Data Generation // ============================================================================ fn generate_random_graph(n: usize, m: usize, seed: u64) -> Vec<(u64, u64, f64)> { use std::collections::hash_map::DefaultHasher; use std::hash::{Hash, Hasher}; let mut edges = Vec::with_capacity(m); let mut edge_set = HashSet::new(); for i in 0..m * 2 { if edges.len() >= m { break; } let mut hasher = DefaultHasher::new(); (seed, i, "source").hash(&mut hasher); let u = hasher.finish() % n as u64; let mut hasher = DefaultHasher::new(); (seed, i, "target").hash(&mut hasher); let v = hasher.finish() % n as u64; if u != v { let key = if u < v { (u, v) } else { (v, u) }; if edge_set.insert(key) { edges.push((u, v, 1.0)); } } } edges } // ============================================================================ // Benchmarks // ============================================================================ /// Benchmark edge insertion fn bench_insert_edge(c: &mut Criterion) { let mut group = c.benchmark_group("mincut_insert"); group.throughput(Throughput::Elements(1)); for size in [100, 1000, 10000] { let edges = generate_random_graph(size, size * 2, 42); let mut mincut = SubpolynomialMinCut::with_capacity(size, size * 3); // Pre-populate for (u, v, w) in &edges[..edges.len() / 2] { mincut.insert_edge(*u, *v, *w); } group.bench_with_input(BenchmarkId::new("insert_single", size), &size, |b, &n| { let mut i = edges.len() / 2; b.iter(|| { let (u, v, w) = edges[i % edges.len()]; black_box(mincut.insert_edge(u + n as u64, v + n as u64, w)); i += 1; }) }); } group.finish(); } /// Benchmark edge deletion fn bench_delete_edge(c: &mut Criterion) { let mut group = c.benchmark_group("mincut_delete"); group.throughput(Throughput::Elements(1)); for size in [100, 1000, 10000] { let edges = generate_random_graph(size, size * 2, 42); group.bench_with_input(BenchmarkId::new("delete_single", size), &size, |b, _| { b.iter_batched( || { let mut mincut = SubpolynomialMinCut::with_capacity(size, size * 3); for (u, v, w) in &edges { mincut.insert_edge(*u, *v, *w); } (mincut, edges.clone()) }, |(mut mincut, edges)| { let (u, v, _) = edges[edges.len() / 2]; black_box(mincut.delete_edge(u, v)) }, criterion::BatchSize::SmallInput, ) }); } group.finish(); } /// Benchmark mincut query fn bench_mincut_query(c: &mut Criterion) { let mut group = c.benchmark_group("mincut_query"); group.throughput(Throughput::Elements(1)); for size in [100, 1000, 10000] { let edges = generate_random_graph(size, size * 2, 42); let mut mincut = SubpolynomialMinCut::with_capacity(size, size * 3); for (u, v, w) in &edges { mincut.insert_edge(*u, *v, *w); } // Cold query (no cache) group.bench_with_input(BenchmarkId::new("cold_query", size), &size, |b, _| { b.iter_batched( || { let mc = mincut.graph.adjacency.clone(); SubpolynomialMinCut { graph: DynamicGraph { adjacency: mc, edge_count: mincut.graph.edge_count, vertex_count: mincut.graph.vertex_count, components: None, mod_count: 0, }, cached_mincut: None, updates_since_compute: 0, recompute_threshold: 10, } }, |mut mc| black_box(mc.min_cut()), criterion::BatchSize::SmallInput, ) }); // Warm query (cached) mincut.min_cut(); // Prime cache group.bench_with_input(BenchmarkId::new("warm_query", size), &size, |b, _| { b.iter(|| black_box(mincut.min_cut())) }); } group.finish(); } /// Benchmark scaling behavior (verify subpolynomial) fn bench_scaling(c: &mut Criterion) { let mut group = c.benchmark_group("mincut_scaling"); group.sample_size(20); // Sizes chosen for subpolynomial verification // n^(2/3) scaling should show sub-linear growth let sizes = vec![100, 316, 1000, 3162, 10000]; for size in sizes { let edges = generate_random_graph(size, size * 2, 42); // Measure insert amortized time group.throughput(Throughput::Elements(1)); group.bench_with_input( BenchmarkId::new("insert_amortized", size), &size, |b, &n| { b.iter_batched( || { let mut mincut = SubpolynomialMinCut::with_capacity(n, n * 3); for (u, v, w) in &edges[..edges.len() / 2] { mincut.insert_edge(*u, *v, *w); } (mincut, n) }, |(mut mincut, n)| { for i in 0..10 { let u = (i * 37) as u64 % n as u64; let v = (i * 73 + 1) as u64 % n as u64; if u != v { mincut.insert_edge(u + n as u64, v + n as u64, 1.0); } } black_box(mincut.min_cut()) }, criterion::BatchSize::SmallInput, ) }, ); } group.finish(); } /// Benchmark mixed workload fn bench_mixed_workload(c: &mut Criterion) { let mut group = c.benchmark_group("mincut_mixed"); group.throughput(Throughput::Elements(1)); for size in [100, 1000, 10000] { let edges = generate_random_graph(size, size * 2, 42); group.bench_with_input(BenchmarkId::new("mixed_ops", size), &size, |b, &n| { b.iter_batched( || { let mut mincut = SubpolynomialMinCut::with_capacity(n, n * 3); for (u, v, w) in &edges { mincut.insert_edge(*u, *v, *w); } (mincut, 0usize) }, |(mut mincut, mut op_idx)| { // 50% insert, 30% delete, 20% query match op_idx % 10 { 0..=4 => { let u = (op_idx * 37) as u64 % n as u64; let v = (op_idx * 73 + 1) as u64 % n as u64; if u != v { mincut.insert_edge(u + n as u64, v + n as u64, 1.0); } } 5..=7 => { if !edges.is_empty() { let (u, v, _) = edges[op_idx % edges.len()]; mincut.delete_edge(u, v); } } _ => { let _ = mincut.min_cut(); } } op_idx += 1; black_box(op_idx) }, criterion::BatchSize::SmallInput, ) }); } group.finish(); } /// Benchmark partition computation fn bench_partition(c: &mut Criterion) { let mut group = c.benchmark_group("mincut_partition"); for size in [100, 1000, 10000] { let edges = generate_random_graph(size, size * 2, 42); let mut mincut = SubpolynomialMinCut::with_capacity(size, size * 3); for (u, v, w) in &edges { mincut.insert_edge(*u, *v, *w); } group.bench_with_input(BenchmarkId::new("partition", size), &size, |b, _| { b.iter(|| black_box(mincut.partition())) }); } group.finish(); } /// Benchmark connected components fn bench_components(c: &mut Criterion) { let mut group = c.benchmark_group("mincut_components"); for size in [100, 1000, 10000] { // Create graph with multiple components let mut mincut = SubpolynomialMinCut::with_capacity(size, size * 2); let component_size = size / 5; for comp in 0..5 { let offset = comp * component_size; for i in 0..component_size - 1 { let u = (offset + i) as u64; let v = (offset + i + 1) as u64; mincut.insert_edge(u, v, 1.0); } } group.bench_with_input(BenchmarkId::new("multi_component", size), &size, |b, _| { b.iter(|| { // Force recomputation mincut.graph.components = None; let components = mincut.graph.connected_components(); black_box(components.len()) }) }); } group.finish(); } criterion_group!( benches, bench_insert_edge, bench_delete_edge, bench_mincut_query, bench_scaling, bench_mixed_workload, bench_partition, bench_components, ); criterion_main!(benches);