Files
wifi-densepose/vendor/ruvector/crates/prime-radiant/benches/mincut_bench.rs

631 lines
19 KiB
Rust

//! 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<u64, HashMap<u64, f64>>,
/// Total edge count
edge_count: usize,
/// Vertex count
vertex_count: usize,
/// Cached connected components
components: Option<Vec<HashSet<u64>>>,
/// 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<u64> {
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<HashSet<u64>> {
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<Edge> {
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<f64>,
/// 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<u64>, HashSet<u64>) {
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);