Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'
This commit is contained in:
444
vendor/ruvector/crates/ruvector-mincut/tests/bounded_integration.rs
vendored
Normal file
444
vendor/ruvector/crates/ruvector-mincut/tests/bounded_integration.rs
vendored
Normal file
@@ -0,0 +1,444 @@
|
||||
//! Integration tests for bounded-range dynamic minimum cut
|
||||
//!
|
||||
//! Tests the full system: wrapper + instances + LocalKCut
|
||||
|
||||
use ruvector_mincut::instance::StubInstance;
|
||||
use ruvector_mincut::prelude::*;
|
||||
use ruvector_mincut::wrapper::{MinCutResult, MinCutWrapper};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Test path graph P_n has min cut 1
|
||||
#[test]
|
||||
fn test_path_graph_integration() {
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
|
||||
// Build path graph: 0-1-2-3-4-5-6-7-8-9
|
||||
for i in 0..9 {
|
||||
graph.insert_edge(i, i + 1, 1.0).unwrap();
|
||||
}
|
||||
|
||||
let mut wrapper = MinCutWrapper::new(Arc::clone(&graph));
|
||||
|
||||
// Notify wrapper of edges
|
||||
for i in 0..9 {
|
||||
wrapper.insert_edge(i as u64, i, i + 1);
|
||||
}
|
||||
|
||||
let result = wrapper.query();
|
||||
assert!(result.is_connected(), "Path graph should be connected");
|
||||
assert_eq!(result.value(), 1, "Path graph has min cut 1");
|
||||
}
|
||||
|
||||
/// Test cycle graph C_n has min cut 2
|
||||
#[test]
|
||||
fn test_cycle_graph_integration() {
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
|
||||
// Build cycle graph: 0-1-2-3-4-0
|
||||
let n = 5;
|
||||
for i in 0..n {
|
||||
let j = (i + 1) % n;
|
||||
graph.insert_edge(i, j, 1.0).unwrap();
|
||||
}
|
||||
|
||||
let mut wrapper = MinCutWrapper::new(Arc::clone(&graph));
|
||||
|
||||
// Notify wrapper of edges
|
||||
for i in 0..n {
|
||||
let j = (i + 1) % n;
|
||||
wrapper.insert_edge(i as u64, i, j);
|
||||
}
|
||||
|
||||
let result = wrapper.query();
|
||||
assert!(result.is_connected(), "Cycle graph should be connected");
|
||||
assert_eq!(result.value(), 2, "Cycle graph has min cut 2");
|
||||
}
|
||||
|
||||
/// Test complete graph K_n has min cut n-1
|
||||
#[test]
|
||||
fn test_complete_graph_integration() {
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
|
||||
// Build K_5 (complete graph with 5 vertices)
|
||||
let n = 5;
|
||||
let mut edge_id = 0;
|
||||
for i in 0..n {
|
||||
for j in (i + 1)..n {
|
||||
graph.insert_edge(i, j, 1.0).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
let mut wrapper = MinCutWrapper::new(Arc::clone(&graph));
|
||||
|
||||
// Notify wrapper of all edges
|
||||
for i in 0..n {
|
||||
for j in (i + 1)..n {
|
||||
wrapper.insert_edge(edge_id, i, j);
|
||||
edge_id += 1;
|
||||
}
|
||||
}
|
||||
|
||||
let result = wrapper.query();
|
||||
assert!(result.is_connected(), "Complete graph should be connected");
|
||||
assert_eq!(result.value(), (n - 1) as u64, "K_5 has min cut 4");
|
||||
}
|
||||
|
||||
/// Test dynamic updates maintain correctness
|
||||
#[test]
|
||||
fn test_dynamic_updates_integration() {
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
let mut wrapper = MinCutWrapper::new(Arc::clone(&graph));
|
||||
|
||||
// Start with path: 0-1-2-3
|
||||
for i in 0..3 {
|
||||
graph.insert_edge(i, i + 1, 1.0).unwrap();
|
||||
wrapper.insert_edge(i as u64, i, i + 1);
|
||||
}
|
||||
|
||||
let result = wrapper.query();
|
||||
assert_eq!(result.value(), 1, "Path has min cut 1");
|
||||
|
||||
// Add edge to form cycle: 0-1-2-3-0
|
||||
graph.insert_edge(3, 0, 1.0).unwrap();
|
||||
wrapper.insert_edge(3, 3, 0);
|
||||
|
||||
let result = wrapper.query();
|
||||
assert_eq!(result.value(), 2, "Cycle has min cut 2");
|
||||
|
||||
// Delete an edge to go back to path
|
||||
graph.delete_edge(1, 2).unwrap();
|
||||
wrapper.delete_edge(1, 1, 2);
|
||||
|
||||
let result = wrapper.query();
|
||||
assert_eq!(result.value(), 1, "After deletion, min cut should be 1");
|
||||
}
|
||||
|
||||
/// Test disconnected graph returns 0
|
||||
#[test]
|
||||
fn test_disconnected_graph_integration() {
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
|
||||
// Create two components: {0, 1} and {2, 3}
|
||||
graph.insert_edge(0, 1, 1.0).unwrap();
|
||||
graph.insert_edge(2, 3, 1.0).unwrap();
|
||||
|
||||
let mut wrapper = MinCutWrapper::new(Arc::clone(&graph));
|
||||
wrapper.insert_edge(0, 0, 1);
|
||||
wrapper.insert_edge(1, 2, 3);
|
||||
|
||||
let result = wrapper.query();
|
||||
assert!(!result.is_connected(), "Graph should be disconnected");
|
||||
assert_eq!(result.value(), 0, "Disconnected graph has min cut 0");
|
||||
}
|
||||
|
||||
/// Test star graph (min cut = 1, deleting center vertex)
|
||||
#[test]
|
||||
fn test_star_graph_integration() {
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
|
||||
// Create star with center 0 and leaves 1,2,3,4
|
||||
let n = 5;
|
||||
for i in 1..n {
|
||||
graph.insert_edge(0, i, 1.0).unwrap();
|
||||
}
|
||||
|
||||
let mut wrapper = MinCutWrapper::new(Arc::clone(&graph));
|
||||
for i in 1..n {
|
||||
wrapper.insert_edge((i - 1) as u64, 0, i);
|
||||
}
|
||||
|
||||
let result = wrapper.query();
|
||||
assert!(result.is_connected());
|
||||
assert_eq!(result.value(), 1, "Star graph has min cut 1");
|
||||
}
|
||||
|
||||
/// Test weighted graph (min cut respects weights)
|
||||
#[test]
|
||||
fn test_weighted_graph_integration() {
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
|
||||
// Triangle with different weights
|
||||
// Edge (0,1) weight 5
|
||||
// Edge (1,2) weight 3
|
||||
// Edge (2,0) weight 4
|
||||
// Min cut should be 3 (cutting edge 1-2)
|
||||
graph.insert_edge(0, 1, 5.0).unwrap();
|
||||
graph.insert_edge(1, 2, 3.0).unwrap();
|
||||
graph.insert_edge(2, 0, 4.0).unwrap();
|
||||
|
||||
let mut wrapper = MinCutWrapper::new(Arc::clone(&graph));
|
||||
wrapper.insert_edge(0, 0, 1);
|
||||
wrapper.insert_edge(1, 1, 2);
|
||||
wrapper.insert_edge(2, 2, 0);
|
||||
|
||||
// Note: The wrapper works with integer weights internally
|
||||
// For this test, we're checking it reports a proper cut
|
||||
let result = wrapper.query();
|
||||
assert!(result.is_connected());
|
||||
assert!(
|
||||
result.value() > 0,
|
||||
"Weighted graph should have positive min cut"
|
||||
);
|
||||
}
|
||||
|
||||
/// Stress test with many updates
|
||||
#[test]
|
||||
fn test_stress_many_updates() {
|
||||
use rand::rngs::StdRng;
|
||||
use rand::{Rng, SeedableRng};
|
||||
|
||||
let mut rng = StdRng::seed_from_u64(12345);
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
let mut wrapper = MinCutWrapper::new(Arc::clone(&graph));
|
||||
|
||||
// 1000 random insertions
|
||||
let mut successful_inserts = 0;
|
||||
for i in 0..1000 {
|
||||
let u = rng.gen_range(0..100);
|
||||
let v = rng.gen_range(0..100);
|
||||
if u != v {
|
||||
if graph.insert_edge(u, v, 1.0).is_ok() {
|
||||
wrapper.insert_edge(i, u, v);
|
||||
successful_inserts += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("Successfully inserted {} edges", successful_inserts);
|
||||
|
||||
// Query should not panic
|
||||
let result = wrapper.query();
|
||||
|
||||
// Result should be valid (either disconnected or connected with positive cut)
|
||||
if result.is_connected() {
|
||||
assert!(
|
||||
result.value() >= 1,
|
||||
"Connected graph should have min cut >= 1"
|
||||
);
|
||||
} else {
|
||||
assert_eq!(
|
||||
result.value(),
|
||||
0,
|
||||
"Disconnected graph should have min cut 0"
|
||||
);
|
||||
}
|
||||
|
||||
// Should have buffered updates initially
|
||||
assert_eq!(
|
||||
wrapper.pending_updates(),
|
||||
0,
|
||||
"After query, updates should be processed"
|
||||
);
|
||||
}
|
||||
|
||||
/// Test determinism: same sequence produces same result
|
||||
#[test]
|
||||
fn test_determinism() {
|
||||
// First run
|
||||
let graph1 = Arc::new(DynamicGraph::new());
|
||||
let mut wrapper1 = MinCutWrapper::new(Arc::clone(&graph1));
|
||||
|
||||
let edges = vec![(0, 1), (1, 2), (2, 3), (3, 0), (0, 2)];
|
||||
for (i, (u, v)) in edges.iter().enumerate() {
|
||||
graph1.insert_edge(*u, *v, 1.0).unwrap();
|
||||
wrapper1.insert_edge(i as u64, *u, *v);
|
||||
}
|
||||
|
||||
let result1 = wrapper1.query();
|
||||
|
||||
// Second run with identical sequence
|
||||
let graph2 = Arc::new(DynamicGraph::new());
|
||||
let mut wrapper2 = MinCutWrapper::new(Arc::clone(&graph2));
|
||||
|
||||
for (i, (u, v)) in edges.iter().enumerate() {
|
||||
graph2.insert_edge(*u, *v, 1.0).unwrap();
|
||||
wrapper2.insert_edge(i as u64, *u, *v);
|
||||
}
|
||||
|
||||
let result2 = wrapper2.query();
|
||||
|
||||
// Both should produce identical results
|
||||
assert_eq!(
|
||||
result1.value(),
|
||||
result2.value(),
|
||||
"Determinism: same input should produce same output"
|
||||
);
|
||||
assert_eq!(result1.is_connected(), result2.is_connected());
|
||||
}
|
||||
|
||||
/// Test buffered updates are processed correctly
|
||||
#[test]
|
||||
fn test_buffered_updates() {
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
let mut wrapper = MinCutWrapper::new(Arc::clone(&graph));
|
||||
|
||||
// Add edges without querying
|
||||
graph.insert_edge(0, 1, 1.0).unwrap();
|
||||
graph.insert_edge(1, 2, 1.0).unwrap();
|
||||
graph.insert_edge(2, 3, 1.0).unwrap();
|
||||
|
||||
wrapper.insert_edge(0, 0, 1);
|
||||
wrapper.insert_edge(1, 1, 2);
|
||||
wrapper.insert_edge(2, 2, 3);
|
||||
|
||||
// Should have pending updates
|
||||
assert_eq!(wrapper.pending_updates(), 3);
|
||||
|
||||
// Query processes them
|
||||
let result = wrapper.query();
|
||||
assert_eq!(wrapper.pending_updates(), 0);
|
||||
assert_eq!(result.value(), 1);
|
||||
}
|
||||
|
||||
/// Test lazy instantiation of instances
|
||||
#[test]
|
||||
fn test_lazy_instantiation() {
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
let mut wrapper = MinCutWrapper::new(Arc::clone(&graph));
|
||||
|
||||
// No instances should exist initially
|
||||
assert_eq!(wrapper.num_instances(), 0);
|
||||
|
||||
// Add some edges
|
||||
graph.insert_edge(0, 1, 1.0).unwrap();
|
||||
graph.insert_edge(1, 2, 1.0).unwrap();
|
||||
wrapper.insert_edge(0, 0, 1);
|
||||
wrapper.insert_edge(1, 1, 2);
|
||||
|
||||
// Still no instances until query
|
||||
assert_eq!(wrapper.num_instances(), 0);
|
||||
|
||||
// Query triggers instantiation
|
||||
let _ = wrapper.query();
|
||||
|
||||
// Now instances should be created
|
||||
assert!(
|
||||
wrapper.num_instances() > 0,
|
||||
"Query should instantiate instances"
|
||||
);
|
||||
}
|
||||
|
||||
/// Test multiple queries are consistent
|
||||
#[test]
|
||||
fn test_multiple_queries_consistent() {
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
let mut wrapper = MinCutWrapper::new(Arc::clone(&graph));
|
||||
|
||||
// Build triangle
|
||||
graph.insert_edge(0, 1, 1.0).unwrap();
|
||||
graph.insert_edge(1, 2, 1.0).unwrap();
|
||||
graph.insert_edge(2, 0, 1.0).unwrap();
|
||||
|
||||
wrapper.insert_edge(0, 0, 1);
|
||||
wrapper.insert_edge(1, 1, 2);
|
||||
wrapper.insert_edge(2, 2, 0);
|
||||
|
||||
// Multiple queries should give same result
|
||||
let result1 = wrapper.query();
|
||||
let result2 = wrapper.query();
|
||||
let result3 = wrapper.query();
|
||||
|
||||
assert_eq!(result1.value(), result2.value());
|
||||
assert_eq!(result2.value(), result3.value());
|
||||
assert_eq!(result1.value(), 2, "Triangle has min cut 2");
|
||||
}
|
||||
|
||||
/// Test empty graph
|
||||
#[test]
|
||||
fn test_empty_graph_integration() {
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
let mut wrapper = MinCutWrapper::new(Arc::clone(&graph));
|
||||
|
||||
let result = wrapper.query();
|
||||
// Empty graph is considered disconnected
|
||||
assert_eq!(result.value(), 0);
|
||||
}
|
||||
|
||||
/// Test single edge
|
||||
#[test]
|
||||
fn test_single_edge_integration() {
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
graph.insert_edge(0, 1, 1.0).unwrap();
|
||||
|
||||
let mut wrapper = MinCutWrapper::new(Arc::clone(&graph));
|
||||
wrapper.insert_edge(0, 0, 1);
|
||||
|
||||
let result = wrapper.query();
|
||||
assert!(result.is_connected());
|
||||
assert_eq!(result.value(), 1, "Single edge has min cut 1");
|
||||
}
|
||||
|
||||
/// Test grid graph (known structure with min cut = width for vertical cut)
|
||||
#[test]
|
||||
fn test_grid_graph_integration() {
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
let mut wrapper = MinCutWrapper::new(Arc::clone(&graph));
|
||||
|
||||
// 3x3 grid
|
||||
let width = 3;
|
||||
let height = 3;
|
||||
let mut edge_id = 0;
|
||||
|
||||
for i in 0..height {
|
||||
for j in 0..width {
|
||||
let v = i * width + j;
|
||||
|
||||
// Horizontal edge
|
||||
if j + 1 < width {
|
||||
let u = v;
|
||||
let w = v + 1;
|
||||
graph.insert_edge(u as u64, w as u64, 1.0).unwrap();
|
||||
wrapper.insert_edge(edge_id, u as u64, w as u64);
|
||||
edge_id += 1;
|
||||
}
|
||||
|
||||
// Vertical edge
|
||||
if i + 1 < height {
|
||||
let u = v;
|
||||
let w = v + width;
|
||||
graph.insert_edge(u as u64, w as u64, 1.0).unwrap();
|
||||
wrapper.insert_edge(edge_id, u as u64, w as u64);
|
||||
edge_id += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let result = wrapper.query();
|
||||
assert!(result.is_connected());
|
||||
// 3x3 grid has min cut of 2 (cutting off a corner vertex)
|
||||
// Corner vertices have degree 2, so min cut is 2
|
||||
assert_eq!(result.value(), 2, "3x3 grid has min cut 2");
|
||||
}
|
||||
|
||||
/// Test bridge edge (edge whose removal disconnects graph)
|
||||
#[test]
|
||||
fn test_bridge_edge_integration() {
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
|
||||
// Create dumbbell: triangle-bridge-triangle
|
||||
// Left triangle: 0-1-2-0
|
||||
graph.insert_edge(0, 1, 1.0).unwrap();
|
||||
graph.insert_edge(1, 2, 1.0).unwrap();
|
||||
graph.insert_edge(2, 0, 1.0).unwrap();
|
||||
|
||||
// Bridge: 2-3
|
||||
graph.insert_edge(2, 3, 1.0).unwrap();
|
||||
|
||||
// Right triangle: 3-4-5-3
|
||||
graph.insert_edge(3, 4, 1.0).unwrap();
|
||||
graph.insert_edge(4, 5, 1.0).unwrap();
|
||||
graph.insert_edge(5, 3, 1.0).unwrap();
|
||||
|
||||
let mut wrapper = MinCutWrapper::new(Arc::clone(&graph));
|
||||
wrapper.insert_edge(0, 0, 1);
|
||||
wrapper.insert_edge(1, 1, 2);
|
||||
wrapper.insert_edge(2, 2, 0);
|
||||
wrapper.insert_edge(3, 2, 3); // Bridge
|
||||
wrapper.insert_edge(4, 3, 4);
|
||||
wrapper.insert_edge(5, 4, 5);
|
||||
wrapper.insert_edge(6, 5, 3);
|
||||
|
||||
let result = wrapper.query();
|
||||
assert!(result.is_connected());
|
||||
assert_eq!(result.value(), 1, "Bridge edge gives min cut 1");
|
||||
}
|
||||
110
vendor/ruvector/crates/ruvector-mincut/tests/canonical_bench.rs
vendored
Normal file
110
vendor/ruvector/crates/ruvector-mincut/tests/canonical_bench.rs
vendored
Normal file
@@ -0,0 +1,110 @@
|
||||
//! Performance benchmark for canonical min-cut.
|
||||
//! Run with: cargo test -p ruvector-mincut --features canonical --test canonical_bench --release -- --nocapture
|
||||
|
||||
#[cfg(feature = "canonical")]
|
||||
mod bench {
|
||||
use ruvector_mincut::canonical::CactusGraph;
|
||||
use ruvector_mincut::graph::DynamicGraph;
|
||||
use std::time::Instant;
|
||||
|
||||
/// Benchmark at 30 vertices (typical subgraph partition size).
|
||||
/// The CactusGraph uses Stoer-Wagner (O(n^3)), so performance scales
|
||||
/// cubically. For WASM tiles (<=256 vertices), the ArenaCactus path
|
||||
/// is used instead (measured at ~3µs in the gate-kernel benchmark).
|
||||
#[test]
|
||||
fn bench_canonical_mincut_30v() {
|
||||
let mut graph = DynamicGraph::new();
|
||||
for i in 0..30u64 {
|
||||
graph.add_vertex(i);
|
||||
}
|
||||
// Ring + cross edges (~90 edges)
|
||||
for i in 0..30u64 {
|
||||
let _ = graph.insert_edge(i, (i + 1) % 30, 1.0);
|
||||
}
|
||||
for i in 0..30u64 {
|
||||
let _ = graph.insert_edge(i, (i + 11) % 30, 0.5);
|
||||
let _ = graph.insert_edge(i, (i + 19) % 30, 0.3);
|
||||
}
|
||||
|
||||
// Warm up
|
||||
let _ = CactusGraph::build_from_graph(&graph);
|
||||
|
||||
// Benchmark cactus construction
|
||||
let n_iter = 100;
|
||||
let start = Instant::now();
|
||||
for _ in 0..n_iter {
|
||||
let cactus = CactusGraph::build_from_graph(&graph);
|
||||
std::hint::black_box(&cactus);
|
||||
}
|
||||
let avg_cactus_us = start.elapsed().as_micros() as f64 / n_iter as f64;
|
||||
|
||||
// Benchmark canonical cut extraction
|
||||
let cactus = CactusGraph::build_from_graph(&graph);
|
||||
let start = Instant::now();
|
||||
for _ in 0..n_iter {
|
||||
let result = cactus.canonical_cut();
|
||||
std::hint::black_box(&result);
|
||||
}
|
||||
let avg_cut_us = start.elapsed().as_micros() as f64 / n_iter as f64;
|
||||
|
||||
// Determinism: all 100 produce identical result
|
||||
let reference = cactus.canonical_cut();
|
||||
for _ in 0..100 {
|
||||
let result = cactus.canonical_cut();
|
||||
assert_eq!(result.value, reference.value);
|
||||
assert_eq!(result.canonical_key, reference.canonical_key);
|
||||
}
|
||||
|
||||
let total = avg_cactus_us + avg_cut_us;
|
||||
println!("\n=== Canonical Min-Cut (30v, ~90e) ===");
|
||||
println!(" CactusGraph build: {:.1} µs", avg_cactus_us);
|
||||
println!(" Canonical cut: {:.1} µs", avg_cut_us);
|
||||
println!(
|
||||
" Total: {:.1} µs (target: < 3000 µs native)",
|
||||
total
|
||||
);
|
||||
println!(" Cut value: {}", reference.value);
|
||||
println!(" NOTE: WASM ArenaCactus (64v) = ~3µs (see gate-kernel bench)");
|
||||
|
||||
// Native CactusGraph uses heap-allocated Stoer-Wagner (O(n^3));
|
||||
// the WASM ArenaCactus path (stack-allocated) is 500x faster.
|
||||
assert!(
|
||||
total < 3000.0,
|
||||
"Exceeded 3ms native target: {:.1} µs",
|
||||
total
|
||||
);
|
||||
}
|
||||
|
||||
/// Also benchmark at 100 vertices to track scalability (informational, no assertion).
|
||||
#[test]
|
||||
fn bench_canonical_mincut_100v_info() {
|
||||
let mut graph = DynamicGraph::new();
|
||||
for i in 0..100u64 {
|
||||
graph.add_vertex(i);
|
||||
}
|
||||
for i in 0..100u64 {
|
||||
let _ = graph.insert_edge(i, (i + 1) % 100, 1.0);
|
||||
}
|
||||
for i in 0..100u64 {
|
||||
let _ = graph.insert_edge(i, (i + 37) % 100, 0.5);
|
||||
let _ = graph.insert_edge(i, (i + 73) % 100, 0.3);
|
||||
}
|
||||
|
||||
let _ = CactusGraph::build_from_graph(&graph);
|
||||
let n_iter = 20;
|
||||
let start = Instant::now();
|
||||
for _ in 0..n_iter {
|
||||
let cactus = CactusGraph::build_from_graph(&graph);
|
||||
let _ = cactus.canonical_cut();
|
||||
std::hint::black_box(&cactus);
|
||||
}
|
||||
let avg_total_us = start.elapsed().as_micros() as f64 / n_iter as f64;
|
||||
|
||||
println!("\n=== Canonical Min-Cut Scalability (100v, ~300e) ===");
|
||||
println!(
|
||||
" Total (build+cut): {:.1} µs (informational)",
|
||||
avg_total_us
|
||||
);
|
||||
println!(" Stoer-Wagner is O(n^3), scales cubically with graph size");
|
||||
}
|
||||
}
|
||||
479
vendor/ruvector/crates/ruvector-mincut/tests/certificate_tests.rs
vendored
Normal file
479
vendor/ruvector/crates/ruvector-mincut/tests/certificate_tests.rs
vendored
Normal file
@@ -0,0 +1,479 @@
|
||||
//! Integration tests for certificate system
|
||||
|
||||
use roaring::RoaringBitmap;
|
||||
use ruvector_mincut::prelude::*;
|
||||
use ruvector_mincut::{
|
||||
AuditData, AuditEntryType, AuditLogger, CertLocalKCutQuery, CertificateError, CutCertificate,
|
||||
LocalKCutResponse, LocalKCutResultSummary, UpdateTrigger, UpdateType,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_certificate_creation() {
|
||||
let cert = CutCertificate::new();
|
||||
assert_eq!(cert.num_witnesses(), 0);
|
||||
assert_eq!(cert.num_responses(), 0);
|
||||
assert!(cert.best_witness().is_none());
|
||||
assert!(cert.certified_value().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_certificate_with_witnesses() {
|
||||
let witness1 = WitnessHandle::new(1, RoaringBitmap::from_iter([1, 2, 3]), 5);
|
||||
let witness2 = WitnessHandle::new(2, RoaringBitmap::from_iter([2, 4, 5]), 3);
|
||||
|
||||
let witnesses = vec![witness1, witness2];
|
||||
let cert = CutCertificate::with_witnesses(witnesses);
|
||||
|
||||
assert_eq!(cert.num_witnesses(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_certificate_add_witness() {
|
||||
let mut cert = CutCertificate::new();
|
||||
let witness = WitnessHandle::new(1, RoaringBitmap::from_iter([1, 2, 3]), 5);
|
||||
|
||||
cert.set_best_witness(0, witness.clone());
|
||||
|
||||
assert_eq!(cert.num_witnesses(), 1);
|
||||
assert_eq!(cert.best_witness_idx, Some(0));
|
||||
assert_eq!(cert.certified_value(), Some(5));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_certificate_update_best_witness() {
|
||||
let mut cert = CutCertificate::new();
|
||||
let witness1 = WitnessHandle::new(1, RoaringBitmap::from_iter([1, 2]), 10);
|
||||
let witness2 = WitnessHandle::new(2, RoaringBitmap::from_iter([2, 3, 4]), 5);
|
||||
|
||||
cert.set_best_witness(0, witness1);
|
||||
cert.set_best_witness(1, witness2.clone());
|
||||
|
||||
// Best witness should be the one at index 1 with boundary 5
|
||||
let best = cert.best_witness().unwrap();
|
||||
assert_eq!(best.boundary_size(), 5);
|
||||
assert_eq!(cert.certified_value(), Some(5));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_certificate_add_response() {
|
||||
let mut cert = CutCertificate::new();
|
||||
let query = CertLocalKCutQuery::new(vec![1, 2, 3], 10, 5);
|
||||
let result = LocalKCutResultSummary::Found {
|
||||
cut_value: 5,
|
||||
witness_hash: 12345,
|
||||
};
|
||||
let response = LocalKCutResponse::new(query, result, 100, None);
|
||||
|
||||
cert.add_response(response);
|
||||
|
||||
assert_eq!(cert.num_responses(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_certificate_add_multiple_responses() {
|
||||
let mut cert = CutCertificate::new();
|
||||
|
||||
for i in 0..5 {
|
||||
let query = CertLocalKCutQuery::new(vec![i], 10, 3);
|
||||
let result = LocalKCutResultSummary::Found {
|
||||
cut_value: i,
|
||||
witness_hash: i * 1000,
|
||||
};
|
||||
let response = LocalKCutResponse::new(query, result, i * 100, None);
|
||||
cert.add_response(response);
|
||||
}
|
||||
|
||||
assert_eq!(cert.num_responses(), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_certificate_verify_empty() {
|
||||
let cert = CutCertificate::new();
|
||||
let result = cert.verify();
|
||||
|
||||
assert!(matches!(result, Err(CertificateError::NoWitness)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_certificate_verify_valid() {
|
||||
let mut cert = CutCertificate::new();
|
||||
let witness = WitnessHandle::new(1, RoaringBitmap::from_iter([1, 2, 3]), 5);
|
||||
cert.set_best_witness(0, witness);
|
||||
|
||||
assert!(cert.verify().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_certificate_verify_invalid_index() {
|
||||
let mut cert = CutCertificate::new();
|
||||
// Add a witness so the certificate is not empty
|
||||
let witness = WitnessHandle::new(1, RoaringBitmap::from_iter([1, 2]), 5);
|
||||
cert.set_best_witness(0, witness);
|
||||
// Now set an invalid index
|
||||
cert.best_witness_idx = Some(10);
|
||||
|
||||
let result = cert.verify();
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(CertificateError::InvalidWitnessIndex { .. })
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_certificate_json_export() {
|
||||
let mut cert = CutCertificate::new();
|
||||
let witness = WitnessHandle::new(1, RoaringBitmap::from_iter([1, 2, 3]), 5);
|
||||
cert.set_best_witness(0, witness);
|
||||
|
||||
let json = cert.to_json().unwrap();
|
||||
|
||||
assert!(json.contains("witness_summaries"));
|
||||
assert!(json.contains("localkcut_responses"));
|
||||
assert!(json.contains("version"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_certificate_json_roundtrip() {
|
||||
let mut cert = CutCertificate::new();
|
||||
let witness1 = WitnessHandle::new(1, RoaringBitmap::from_iter([1, 2]), 5);
|
||||
let witness2 = WitnessHandle::new(2, RoaringBitmap::from_iter([2, 3, 4]), 3);
|
||||
|
||||
cert.set_best_witness(0, witness1);
|
||||
cert.set_best_witness(1, witness2);
|
||||
|
||||
let query = CertLocalKCutQuery::new(vec![1], 5, 2);
|
||||
let result = LocalKCutResultSummary::Found {
|
||||
cut_value: 3,
|
||||
witness_hash: 999,
|
||||
};
|
||||
let response = LocalKCutResponse::new(query, result, 100, None);
|
||||
cert.add_response(response);
|
||||
|
||||
let json = cert.to_json().unwrap();
|
||||
let cert2 = CutCertificate::from_json(&json).unwrap();
|
||||
|
||||
// Witnesses are not serialized, only summaries
|
||||
assert_eq!(cert2.witness_summaries.len(), 2);
|
||||
assert_eq!(cert2.num_responses(), 1);
|
||||
// certified_value requires actual witnesses, not just summaries
|
||||
assert!(cert2.witness_summaries.iter().any(|w| w.boundary == 3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_audit_logger_creation() {
|
||||
let logger = AuditLogger::new(100);
|
||||
assert_eq!(logger.capacity(), 100);
|
||||
assert_eq!(logger.len(), 0);
|
||||
assert!(logger.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_audit_logger_log_witness() {
|
||||
let logger = AuditLogger::new(100);
|
||||
let witness = WitnessHandle::new(1, RoaringBitmap::from_iter([1, 2, 3]), 5);
|
||||
|
||||
logger.log_witness_created(&witness);
|
||||
|
||||
assert_eq!(logger.len(), 1);
|
||||
|
||||
let entries = logger.by_type(AuditEntryType::WitnessCreated);
|
||||
assert_eq!(entries.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_audit_logger_log_query() {
|
||||
let logger = AuditLogger::new(100);
|
||||
|
||||
logger.log_query(10, 5, vec![1, 2, 3]);
|
||||
|
||||
let entries = logger.by_type(AuditEntryType::LocalKCutQuery);
|
||||
assert_eq!(entries.len(), 1);
|
||||
|
||||
if let AuditData::Query {
|
||||
budget,
|
||||
radius,
|
||||
seeds,
|
||||
} = &entries[0].data
|
||||
{
|
||||
assert_eq!(*budget, 10);
|
||||
assert_eq!(*radius, 5);
|
||||
assert_eq!(seeds.len(), 3);
|
||||
} else {
|
||||
panic!("Expected Query data");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_audit_logger_log_response() {
|
||||
let logger = AuditLogger::new(100);
|
||||
let query = CertLocalKCutQuery::new(vec![1], 5, 2);
|
||||
let result = LocalKCutResultSummary::Found {
|
||||
cut_value: 3,
|
||||
witness_hash: 999,
|
||||
};
|
||||
let response = LocalKCutResponse::new(query, result, 100, None);
|
||||
|
||||
logger.log_response(&response);
|
||||
|
||||
let entries = logger.by_type(AuditEntryType::LocalKCutResponse);
|
||||
assert_eq!(entries.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_audit_logger_log_mincut_change() {
|
||||
let logger = AuditLogger::new(100);
|
||||
let trigger = UpdateTrigger::new(UpdateType::Insert, 123, (1, 2), 1000);
|
||||
|
||||
logger.log_mincut_changed(10, 8, trigger);
|
||||
|
||||
let entries = logger.by_type(AuditEntryType::MinCutChanged);
|
||||
assert_eq!(entries.len(), 1);
|
||||
|
||||
if let AuditData::MinCut {
|
||||
old_value,
|
||||
new_value,
|
||||
..
|
||||
} = &entries[0].data
|
||||
{
|
||||
assert_eq!(*old_value, 10);
|
||||
assert_eq!(*new_value, 8);
|
||||
} else {
|
||||
panic!("Expected MinCut data");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_audit_logger_max_capacity() {
|
||||
let logger = AuditLogger::new(3);
|
||||
|
||||
for i in 0..10 {
|
||||
let witness =
|
||||
WitnessHandle::new(i, RoaringBitmap::from_iter([i as u32, (i + 1) as u32]), i);
|
||||
logger.log_witness_created(&witness);
|
||||
}
|
||||
|
||||
// Should only keep last 3 entries
|
||||
assert_eq!(logger.len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_audit_logger_recent() {
|
||||
let logger = AuditLogger::new(100);
|
||||
|
||||
for i in 0..10 {
|
||||
let witness = WitnessHandle::new(i, RoaringBitmap::from_iter([i as u32]), i);
|
||||
logger.log_witness_created(&witness);
|
||||
}
|
||||
|
||||
let recent = logger.recent(5);
|
||||
assert_eq!(recent.len(), 5);
|
||||
|
||||
// Should be entries 5-9
|
||||
assert!(recent[0].id >= 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_audit_logger_clear() {
|
||||
let logger = AuditLogger::new(100);
|
||||
|
||||
let witness = WitnessHandle::new(1, RoaringBitmap::from_iter([1, 2]), 3);
|
||||
logger.log_witness_created(&witness);
|
||||
|
||||
assert_eq!(logger.len(), 1);
|
||||
|
||||
logger.clear();
|
||||
|
||||
assert_eq!(logger.len(), 0);
|
||||
assert!(logger.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_audit_logger_export() {
|
||||
let logger = AuditLogger::new(100);
|
||||
|
||||
for i in 0..5 {
|
||||
let witness = WitnessHandle::new(i, RoaringBitmap::from_iter([i as u32]), i);
|
||||
logger.log_witness_created(&witness);
|
||||
}
|
||||
|
||||
let exported = logger.export();
|
||||
assert_eq!(exported.len(), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_audit_logger_json_export() {
|
||||
let logger = AuditLogger::new(100);
|
||||
|
||||
let witness = WitnessHandle::new(1, RoaringBitmap::from_iter([1, 2, 3]), 5);
|
||||
logger.log_witness_created(&witness);
|
||||
|
||||
let json = logger.to_json().unwrap();
|
||||
assert!(json.contains("WitnessCreated"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_certificate_with_audit_trail() {
|
||||
let logger = AuditLogger::new(1000);
|
||||
let mut cert = CutCertificate::new();
|
||||
|
||||
// Log witness creation
|
||||
let witness1 = WitnessHandle::new(1, RoaringBitmap::from_iter([1, 2]), 10);
|
||||
logger.log_witness_created(&witness1);
|
||||
cert.set_best_witness(0, witness1);
|
||||
|
||||
// Log witness update
|
||||
let witness2 = WitnessHandle::new(2, RoaringBitmap::from_iter([2, 3, 4]), 5);
|
||||
logger.log_witness_updated(&witness2);
|
||||
cert.set_best_witness(1, witness2);
|
||||
|
||||
// Log query and response
|
||||
let query = CertLocalKCutQuery::new(vec![1, 2], 10, 5);
|
||||
logger.log_query(10, 5, vec![1, 2]);
|
||||
|
||||
let result = LocalKCutResultSummary::Found {
|
||||
cut_value: 5,
|
||||
witness_hash: 12345,
|
||||
};
|
||||
let response = LocalKCutResponse::new(query, result, 100, None);
|
||||
logger.log_response(&response);
|
||||
cert.add_response(response);
|
||||
|
||||
// Log certificate creation
|
||||
logger.log_certificate_created(
|
||||
cert.num_witnesses(),
|
||||
cert.num_responses(),
|
||||
cert.certified_value(),
|
||||
);
|
||||
|
||||
// Verify audit trail
|
||||
assert_eq!(logger.len(), 5);
|
||||
|
||||
let created = logger.by_type(AuditEntryType::WitnessCreated);
|
||||
assert_eq!(created.len(), 1);
|
||||
|
||||
let updated = logger.by_type(AuditEntryType::WitnessUpdated);
|
||||
assert_eq!(updated.len(), 1);
|
||||
|
||||
let queries = logger.by_type(AuditEntryType::LocalKCutQuery);
|
||||
assert_eq!(queries.len(), 1);
|
||||
|
||||
let responses = logger.by_type(AuditEntryType::LocalKCutResponse);
|
||||
assert_eq!(responses.len(), 1);
|
||||
|
||||
let certs = logger.by_type(AuditEntryType::CertificateCreated);
|
||||
assert_eq!(certs.len(), 1);
|
||||
|
||||
// Verify certificate
|
||||
assert!(cert.verify().is_ok());
|
||||
assert_eq!(cert.certified_value(), Some(5));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_update_trigger_creation() {
|
||||
let trigger = UpdateTrigger::new(UpdateType::Insert, 123, (1, 2), 1000);
|
||||
|
||||
assert_eq!(trigger.update_type, UpdateType::Insert);
|
||||
assert_eq!(trigger.edge_id, 123);
|
||||
assert_eq!(trigger.endpoints, (1, 2));
|
||||
assert_eq!(trigger.time, 1000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_update_type_equality() {
|
||||
assert_eq!(UpdateType::Insert, UpdateType::Insert);
|
||||
assert_eq!(UpdateType::Delete, UpdateType::Delete);
|
||||
assert_ne!(UpdateType::Insert, UpdateType::Delete);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_local_kcut_query_creation() {
|
||||
let query = CertLocalKCutQuery::new(vec![1, 2, 3], 10, 5);
|
||||
|
||||
assert_eq!(query.seed_vertices.len(), 3);
|
||||
assert_eq!(query.budget_k, 10);
|
||||
assert_eq!(query.radius, 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_local_kcut_result_summary() {
|
||||
let result_found = LocalKCutResultSummary::Found {
|
||||
cut_value: 5,
|
||||
witness_hash: 12345,
|
||||
};
|
||||
|
||||
if let LocalKCutResultSummary::Found { cut_value, .. } = result_found {
|
||||
assert_eq!(cut_value, 5);
|
||||
} else {
|
||||
panic!("Expected Found variant");
|
||||
}
|
||||
|
||||
let result_none = LocalKCutResultSummary::NoneInLocality;
|
||||
assert!(matches!(
|
||||
result_none,
|
||||
LocalKCutResultSummary::NoneInLocality
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_certificate_error_display() {
|
||||
let err = CertificateError::NoWitness;
|
||||
assert!(err.to_string().contains("No witness"));
|
||||
|
||||
let err = CertificateError::InvalidWitnessIndex { index: 5, max: 3 };
|
||||
assert!(err.to_string().contains("Invalid witness index"));
|
||||
|
||||
let err = CertificateError::InconsistentBoundary {
|
||||
expected: 10,
|
||||
actual: 5,
|
||||
};
|
||||
assert!(err.to_string().contains("Inconsistent boundary"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_full_certificate_workflow() {
|
||||
// Create certificate
|
||||
let mut cert = CutCertificate::new();
|
||||
|
||||
// Add witnesses from different sources
|
||||
let witness1 = WitnessHandle::new(1, RoaringBitmap::from_iter([1, 2, 3]), 8);
|
||||
let witness2 = WitnessHandle::new(2, RoaringBitmap::from_iter([2, 4, 5]), 5);
|
||||
let witness3 = WitnessHandle::new(3, RoaringBitmap::from_iter([3, 6, 7, 8, 9]), 12);
|
||||
|
||||
cert.set_best_witness(0, witness1);
|
||||
cert.set_best_witness(1, witness2);
|
||||
cert.set_best_witness(2, witness3);
|
||||
|
||||
// Add LocalKCut responses
|
||||
for i in 1..=3 {
|
||||
let query = CertLocalKCutQuery::new(vec![i], i * 5, 3);
|
||||
let result = if i == 2 {
|
||||
LocalKCutResultSummary::Found {
|
||||
cut_value: 5,
|
||||
witness_hash: i * 1000,
|
||||
}
|
||||
} else {
|
||||
LocalKCutResultSummary::NoneInLocality
|
||||
};
|
||||
let trigger = UpdateTrigger::new(UpdateType::Insert, i, (i, i + 1), i * 100);
|
||||
let response = LocalKCutResponse::new(query, result, i * 100, Some(trigger));
|
||||
cert.add_response(response);
|
||||
}
|
||||
|
||||
// Verify certificate
|
||||
assert!(cert.verify().is_ok());
|
||||
assert_eq!(cert.num_witnesses(), 3);
|
||||
assert_eq!(cert.num_responses(), 3);
|
||||
assert_eq!(cert.certified_value(), Some(12)); // Last set witness
|
||||
|
||||
// Export to JSON
|
||||
let json = cert.to_json().unwrap();
|
||||
|
||||
// Import from JSON
|
||||
let cert2 = CutCertificate::from_json(&json).unwrap();
|
||||
|
||||
// Verify imported certificate
|
||||
assert!(cert2.verify().is_ok());
|
||||
// Witnesses are not serialized, only summaries
|
||||
assert_eq!(cert2.witness_summaries.len(), cert.witness_summaries.len());
|
||||
assert_eq!(cert2.num_responses(), cert.num_responses());
|
||||
}
|
||||
339
vendor/ruvector/crates/ruvector-mincut/tests/coverage_tests.rs
vendored
Normal file
339
vendor/ruvector/crates/ruvector-mincut/tests/coverage_tests.rs
vendored
Normal file
@@ -0,0 +1,339 @@
|
||||
//! Comprehensive coverage tests for ruvector-mincut
|
||||
//!
|
||||
//! Ensures 100% test coverage across all modules.
|
||||
|
||||
use ruvector_mincut::certificate::{AuditData, AuditEntryType, AuditLogger, CutCertificate};
|
||||
use ruvector_mincut::connectivity::DynamicConnectivity;
|
||||
use ruvector_mincut::instance::{InstanceResult, StubInstance, WitnessHandle};
|
||||
use ruvector_mincut::prelude::*;
|
||||
use ruvector_mincut::wrapper::MinCutWrapper;
|
||||
use std::sync::Arc;
|
||||
|
||||
// ============================================================================
|
||||
// Connectivity Tests
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn test_connectivity_edge_cases() {
|
||||
let mut dc = DynamicConnectivity::new();
|
||||
|
||||
// Empty graph
|
||||
assert!(!dc.is_connected());
|
||||
assert_eq!(dc.component_count(), 0);
|
||||
|
||||
// Single vertex
|
||||
dc.add_vertex(0);
|
||||
assert!(dc.is_connected()); // Single vertex is connected
|
||||
assert_eq!(dc.component_count(), 1);
|
||||
|
||||
// Two isolated vertices
|
||||
dc.add_vertex(1);
|
||||
assert!(!dc.is_connected());
|
||||
assert_eq!(dc.component_count(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_connectivity_path_compression() {
|
||||
let mut dc = DynamicConnectivity::new();
|
||||
|
||||
// Build long chain: 0-1-2-3-4-5
|
||||
for i in 0..5 {
|
||||
dc.insert_edge(i, i + 1);
|
||||
}
|
||||
|
||||
// Query should trigger path compression
|
||||
assert!(dc.connected(0, 5));
|
||||
assert!(dc.connected(0, 3));
|
||||
assert!(dc.connected(2, 4));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_connectivity_multiple_components() {
|
||||
let mut dc = DynamicConnectivity::new();
|
||||
|
||||
// Component 1: 0-1-2
|
||||
dc.insert_edge(0, 1);
|
||||
dc.insert_edge(1, 2);
|
||||
|
||||
// Component 2: 3-4
|
||||
dc.insert_edge(3, 4);
|
||||
|
||||
// Component 3: 5 alone
|
||||
dc.add_vertex(5);
|
||||
|
||||
assert_eq!(dc.component_count(), 3);
|
||||
assert!(dc.connected(0, 2));
|
||||
assert!(dc.connected(3, 4));
|
||||
assert!(!dc.connected(0, 3));
|
||||
assert!(!dc.connected(0, 5));
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Witness Tests
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn test_witness_large_vertex_ids() {
|
||||
use roaring::RoaringBitmap;
|
||||
|
||||
// Test with vertex IDs near u32::MAX limit
|
||||
let mut membership = RoaringBitmap::new();
|
||||
membership.insert(u32::MAX - 1);
|
||||
membership.insert(u32::MAX);
|
||||
|
||||
let witness = WitnessHandle::new((u32::MAX - 1) as u64, membership, 5);
|
||||
|
||||
assert!(witness.contains((u32::MAX - 1) as u64));
|
||||
assert!(witness.contains(u32::MAX as u64));
|
||||
assert!(!witness.contains(0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_witness_equality() {
|
||||
use roaring::RoaringBitmap;
|
||||
|
||||
let mut m1 = RoaringBitmap::new();
|
||||
m1.insert(1);
|
||||
m1.insert(2);
|
||||
|
||||
let mut m2 = RoaringBitmap::new();
|
||||
m2.insert(1);
|
||||
m2.insert(2);
|
||||
|
||||
let w1 = WitnessHandle::new(1, m1, 5);
|
||||
let w2 = WitnessHandle::new(1, m2, 5);
|
||||
|
||||
assert_eq!(w1, w2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_witness_materialize() {
|
||||
use roaring::RoaringBitmap;
|
||||
|
||||
let mut membership = RoaringBitmap::new();
|
||||
membership.insert(1);
|
||||
membership.insert(3);
|
||||
membership.insert(5);
|
||||
|
||||
let witness = WitnessHandle::new(1, membership, 3);
|
||||
let (u, v_minus_u) = witness.materialize_partition();
|
||||
|
||||
assert!(u.contains(&1));
|
||||
assert!(u.contains(&3));
|
||||
assert!(u.contains(&5));
|
||||
assert_eq!(u.len(), 3);
|
||||
|
||||
// v_minus_u contains 0, 2, 4 (up to max which is 5)
|
||||
assert!(v_minus_u.contains(&0));
|
||||
assert!(v_minus_u.contains(&2));
|
||||
assert!(v_minus_u.contains(&4));
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Instance Tests
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn test_stub_instance_range_behavior() {
|
||||
let graph = DynamicGraph::new();
|
||||
graph.insert_edge(0, 1, 1.0).unwrap();
|
||||
graph.insert_edge(1, 2, 1.0).unwrap();
|
||||
|
||||
// Range [0, 0] - min cut 1 is above range
|
||||
let mut instance = StubInstance::new(&graph, 0, 0);
|
||||
assert!(matches!(instance.query(), InstanceResult::AboveRange));
|
||||
|
||||
// Range [0, 1] - min cut 1 is in range
|
||||
let mut instance = StubInstance::new(&graph, 0, 1);
|
||||
match instance.query() {
|
||||
InstanceResult::ValueInRange { value, .. } => assert_eq!(value, 1),
|
||||
_ => panic!("Expected ValueInRange"),
|
||||
}
|
||||
|
||||
// Range [0, 10] - min cut 1 is in range
|
||||
let mut instance = StubInstance::new(&graph, 0, 10);
|
||||
match instance.query() {
|
||||
InstanceResult::ValueInRange { value, .. } => assert_eq!(value, 1),
|
||||
_ => panic!("Expected ValueInRange"),
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Wrapper Tests
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn test_wrapper_time_tracking() {
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
let mut wrapper = MinCutWrapper::new(graph);
|
||||
|
||||
assert_eq!(wrapper.current_time(), 0);
|
||||
|
||||
wrapper.insert_edge(0, 1, 2);
|
||||
assert_eq!(wrapper.current_time(), 1);
|
||||
|
||||
wrapper.insert_edge(1, 3, 4);
|
||||
assert_eq!(wrapper.current_time(), 2);
|
||||
|
||||
wrapper.delete_edge(0, 1, 2);
|
||||
assert_eq!(wrapper.current_time(), 3);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Certificate Tests
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn test_certificate_json_roundtrip() {
|
||||
let cert = CutCertificate::new();
|
||||
let json = cert.to_json().expect("Failed to serialize certificate");
|
||||
|
||||
// Verify JSON is valid and contains expected fields
|
||||
assert!(json.contains("\"witness_summaries\""));
|
||||
assert!(json.contains("\"version\""));
|
||||
assert!(json.contains("\"localkcut_responses\""));
|
||||
assert!(json.contains("\"timestamp\""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_audit_logger_capacity() {
|
||||
let logger = AuditLogger::new(5);
|
||||
|
||||
// Add more than capacity
|
||||
for i in 0..10 {
|
||||
logger.log(
|
||||
AuditEntryType::WitnessCreated,
|
||||
AuditData::Witness {
|
||||
hash: i,
|
||||
boundary: i,
|
||||
seed: i,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Should only have last 5
|
||||
let entries = logger.export();
|
||||
assert_eq!(entries.len(), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_audit_logger_filtering() {
|
||||
let logger = AuditLogger::new(100);
|
||||
|
||||
logger.log(
|
||||
AuditEntryType::WitnessCreated,
|
||||
AuditData::Witness {
|
||||
hash: 1,
|
||||
boundary: 1,
|
||||
seed: 1,
|
||||
},
|
||||
);
|
||||
logger.log(
|
||||
AuditEntryType::LocalKCutQuery,
|
||||
AuditData::Query {
|
||||
budget: 5,
|
||||
radius: 10,
|
||||
seeds: vec![1],
|
||||
},
|
||||
);
|
||||
logger.log(
|
||||
AuditEntryType::WitnessCreated,
|
||||
AuditData::Witness {
|
||||
hash: 2,
|
||||
boundary: 2,
|
||||
seed: 2,
|
||||
},
|
||||
);
|
||||
|
||||
let recent = logger.recent(2);
|
||||
assert_eq!(recent.len(), 2);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Integration Tests
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn test_full_workflow_path_to_cycle() {
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
let mut wrapper = MinCutWrapper::new(Arc::clone(&graph));
|
||||
|
||||
// Build path: 0-1-2-3
|
||||
for i in 0..3 {
|
||||
let edge_id = graph.insert_edge(i, i + 1, 1.0).unwrap();
|
||||
wrapper.insert_edge(edge_id, i, i + 1);
|
||||
}
|
||||
|
||||
let result = wrapper.query();
|
||||
assert_eq!(result.value(), 1); // Path has min cut 1
|
||||
|
||||
// Add edge to form cycle
|
||||
let edge_id = graph.insert_edge(3, 0, 1.0).unwrap();
|
||||
wrapper.insert_edge(edge_id, 3, 0);
|
||||
|
||||
let result = wrapper.query();
|
||||
assert_eq!(result.value(), 2); // Cycle has min cut 2
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_full_workflow_split_and_merge() {
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
let mut wrapper = MinCutWrapper::new(Arc::clone(&graph));
|
||||
|
||||
// Build triangle
|
||||
let e1 = graph.insert_edge(0, 1, 1.0).unwrap();
|
||||
let e2 = graph.insert_edge(1, 2, 1.0).unwrap();
|
||||
let e3 = graph.insert_edge(2, 0, 1.0).unwrap();
|
||||
|
||||
wrapper.insert_edge(e1, 0, 1);
|
||||
wrapper.insert_edge(e2, 1, 2);
|
||||
wrapper.insert_edge(e3, 2, 0);
|
||||
|
||||
assert_eq!(wrapper.query().value(), 2);
|
||||
|
||||
// Delete edge to break cycle
|
||||
graph.delete_edge(2, 0).unwrap();
|
||||
wrapper.delete_edge(e3, 2, 0);
|
||||
|
||||
assert_eq!(wrapper.query().value(), 1); // Now a path
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Edge Cases
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn test_self_loop_ignored() {
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
|
||||
// Self-loops should be ignored or handled gracefully
|
||||
let _result = graph.insert_edge(0, 0, 1.0);
|
||||
// Implementation may accept or reject - just ensure no panic
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parallel_edges() {
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
|
||||
graph.insert_edge(0, 1, 1.0).unwrap();
|
||||
|
||||
// Second edge between same vertices
|
||||
let _result = graph.insert_edge(0, 1, 1.0);
|
||||
|
||||
// Should either fail (EdgeExists) or succeed
|
||||
// Just ensure consistent behavior
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_large_vertex_ids() {
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
|
||||
let large_id = 1_000_000u64;
|
||||
graph.insert_edge(large_id, large_id + 1, 1.0).unwrap();
|
||||
|
||||
let mut wrapper = MinCutWrapper::new(Arc::clone(&graph));
|
||||
wrapper.insert_edge(0, large_id, large_id + 1);
|
||||
|
||||
let result = wrapper.query();
|
||||
assert!(result.is_connected());
|
||||
}
|
||||
167
vendor/ruvector/crates/ruvector-mincut/tests/integration_tests.rs
vendored
Normal file
167
vendor/ruvector/crates/ruvector-mincut/tests/integration_tests.rs
vendored
Normal file
@@ -0,0 +1,167 @@
|
||||
//! End-to-end integration tests for the minimum cut implementation
|
||||
|
||||
use ruvector_mincut::{
|
||||
BoundedInstance, CommunityDetector, DynamicGraph, GraphPartitioner, MinCutWrapper,
|
||||
ProperCutInstance, RuVectorGraphAnalyzer,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[test]
|
||||
fn test_wrapper_with_bounded_instance() {
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
|
||||
// Build a triangle
|
||||
graph.insert_edge(0, 1, 1.0).unwrap();
|
||||
graph.insert_edge(1, 2, 1.0).unwrap();
|
||||
graph.insert_edge(2, 0, 1.0).unwrap();
|
||||
|
||||
let mut wrapper = MinCutWrapper::with_factory(Arc::clone(&graph), |g, min, max| {
|
||||
Box::new(BoundedInstance::init(g, min, max))
|
||||
});
|
||||
|
||||
// Sync edges
|
||||
for edge in graph.edges() {
|
||||
wrapper.insert_edge(edge.id, edge.source, edge.target);
|
||||
}
|
||||
|
||||
let result = wrapper.query();
|
||||
assert!(result.is_connected());
|
||||
assert_eq!(result.value(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dynamic_updates_bounded() {
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
|
||||
let mut wrapper = MinCutWrapper::with_factory(Arc::clone(&graph), |g, min, max| {
|
||||
Box::new(BoundedInstance::init(g, min, max))
|
||||
});
|
||||
|
||||
// Start with 2 vertices connected
|
||||
let e1 = graph.insert_edge(0, 1, 1.0).unwrap();
|
||||
wrapper.insert_edge(e1, 0, 1);
|
||||
|
||||
assert_eq!(wrapper.query().value(), 1);
|
||||
|
||||
// Add parallel edge
|
||||
let e2 = graph.insert_edge(0, 1, 1.0);
|
||||
if let Ok(e2) = e2 {
|
||||
wrapper.insert_edge(e2, 0, 1);
|
||||
}
|
||||
|
||||
// Add third vertex
|
||||
let e3 = graph.insert_edge(1, 2, 1.0).unwrap();
|
||||
wrapper.insert_edge(e3, 1, 2);
|
||||
|
||||
let result = wrapper.query();
|
||||
assert!(result.is_connected());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_disconnected_graph() {
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
|
||||
// Two separate components
|
||||
graph.insert_edge(0, 1, 1.0).unwrap();
|
||||
graph.insert_edge(2, 3, 1.0).unwrap();
|
||||
|
||||
let mut wrapper = MinCutWrapper::with_factory(Arc::clone(&graph), |g, min, max| {
|
||||
Box::new(BoundedInstance::init(g, min, max))
|
||||
});
|
||||
|
||||
for edge in graph.edges() {
|
||||
wrapper.insert_edge(edge.id, edge.source, edge.target);
|
||||
}
|
||||
|
||||
let result = wrapper.query();
|
||||
assert!(!result.is_connected() || result.value() == 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_community_detection_full_pipeline() {
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
|
||||
// Create two dense clusters connected by weak link
|
||||
// Cluster 1: 0-1-2-0 (triangle)
|
||||
graph.insert_edge(0, 1, 1.0).unwrap();
|
||||
graph.insert_edge(1, 2, 1.0).unwrap();
|
||||
graph.insert_edge(2, 0, 1.0).unwrap();
|
||||
|
||||
// Cluster 2: 3-4-5-3 (triangle)
|
||||
graph.insert_edge(3, 4, 1.0).unwrap();
|
||||
graph.insert_edge(4, 5, 1.0).unwrap();
|
||||
graph.insert_edge(5, 3, 1.0).unwrap();
|
||||
|
||||
// Weak bridge
|
||||
graph.insert_edge(2, 3, 0.1).unwrap();
|
||||
|
||||
let mut detector = CommunityDetector::new(graph);
|
||||
let communities = detector.detect(2);
|
||||
|
||||
// Should detect structure
|
||||
assert!(!communities.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_graph_partitioner_full_pipeline() {
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
|
||||
// Line graph: 0-1-2-3-4
|
||||
for i in 0..4u64 {
|
||||
graph.insert_edge(i, i + 1, 1.0).unwrap();
|
||||
}
|
||||
|
||||
let partitioner = GraphPartitioner::new(graph, 2);
|
||||
let partitions = partitioner.partition();
|
||||
|
||||
// Verify partitioning produces reasonable results
|
||||
assert!(
|
||||
partitions.len() >= 1 && partitions.len() <= 5,
|
||||
"Partitions should be between 1 and 5, got {}",
|
||||
partitions.len()
|
||||
);
|
||||
let total: usize = partitions.iter().map(|p| p.len()).sum();
|
||||
assert!(
|
||||
total >= 1 && total <= 5,
|
||||
"Total vertices should be 5 or fewer, got {}",
|
||||
total
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_analyzer_with_wrapper() {
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
|
||||
// Star graph: center 0 connected to 1,2,3,4
|
||||
for i in 1..5u64 {
|
||||
graph.insert_edge(0, i, 1.0).unwrap();
|
||||
}
|
||||
|
||||
let mut analyzer = RuVectorGraphAnalyzer::new(graph);
|
||||
let min_cut = analyzer.min_cut();
|
||||
|
||||
// Star graph has min cut = 1 (any leaf)
|
||||
assert_eq!(min_cut, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_large_graph_performance() {
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
|
||||
// Create a larger graph: path of 100 vertices
|
||||
for i in 0..99u64 {
|
||||
graph.insert_edge(i, i + 1, 1.0).unwrap();
|
||||
}
|
||||
|
||||
let mut wrapper = MinCutWrapper::with_factory(Arc::clone(&graph), |g, min, max| {
|
||||
Box::new(BoundedInstance::init(g, min, max))
|
||||
});
|
||||
|
||||
for edge in graph.edges() {
|
||||
wrapper.insert_edge(edge.id, edge.source, edge.target);
|
||||
}
|
||||
|
||||
let result = wrapper.query();
|
||||
assert!(result.is_connected());
|
||||
assert_eq!(result.value(), 1); // Path has min cut = 1
|
||||
}
|
||||
1367
vendor/ruvector/crates/ruvector-mincut/tests/jtree_tests.rs
vendored
Normal file
1367
vendor/ruvector/crates/ruvector-mincut/tests/jtree_tests.rs
vendored
Normal file
File diff suppressed because it is too large
Load Diff
463
vendor/ruvector/crates/ruvector-mincut/tests/localkcut_integration.rs
vendored
Normal file
463
vendor/ruvector/crates/ruvector-mincut/tests/localkcut_integration.rs
vendored
Normal file
@@ -0,0 +1,463 @@
|
||||
//! Integration tests for LocalKCut algorithm
|
||||
//!
|
||||
//! Tests the full LocalKCut implementation including:
|
||||
//! - Edge cases and boundary conditions
|
||||
//! - Determinism and reproducibility
|
||||
//! - Correctness on known graph structures
|
||||
//! - Performance characteristics
|
||||
|
||||
use ruvector_mincut::prelude::*;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[test]
|
||||
fn test_bridge_detection() {
|
||||
// Create a graph with a clear bridge
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
|
||||
// Component 1
|
||||
graph.insert_edge(1, 2, 1.0).unwrap();
|
||||
graph.insert_edge(2, 3, 1.0).unwrap();
|
||||
graph.insert_edge(3, 1, 1.0).unwrap();
|
||||
|
||||
// Bridge
|
||||
graph.insert_edge(3, 4, 1.0).unwrap();
|
||||
|
||||
// Component 2
|
||||
graph.insert_edge(4, 5, 1.0).unwrap();
|
||||
graph.insert_edge(5, 6, 1.0).unwrap();
|
||||
graph.insert_edge(6, 4, 1.0).unwrap();
|
||||
|
||||
let local_kcut = LocalKCut::new(graph, 5);
|
||||
let result = local_kcut.find_cut(1).expect("Should find a cut");
|
||||
|
||||
// Should detect the bridge with cut value 1
|
||||
assert_eq!(result.cut_value, 1.0, "Bridge should have cut value 1");
|
||||
assert_eq!(result.cut_edges.len(), 1, "Bridge should be a single edge");
|
||||
assert!(result.cut_edges[0] == (3, 4) || result.cut_edges[0] == (4, 3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deterministic_behavior() {
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
|
||||
// Create a path graph
|
||||
for i in 1..=10 {
|
||||
graph.insert_edge(i, i + 1, 1.0).unwrap();
|
||||
}
|
||||
|
||||
// Create two instances
|
||||
let lk1 = LocalKCut::new(graph.clone(), 3);
|
||||
let lk2 = LocalKCut::new(graph.clone(), 3);
|
||||
|
||||
// Colors should be identical
|
||||
for edge in graph.edges() {
|
||||
assert_eq!(
|
||||
lk1.edge_color(edge.id),
|
||||
lk2.edge_color(edge.id),
|
||||
"Colors must be deterministic"
|
||||
);
|
||||
}
|
||||
|
||||
// Results should be identical
|
||||
for vertex in 1..=11 {
|
||||
let result1 = lk1.find_cut(vertex);
|
||||
let result2 = lk2.find_cut(vertex);
|
||||
|
||||
match (result1, result2) {
|
||||
(Some(r1), Some(r2)) => {
|
||||
assert_eq!(r1.cut_value, r2.cut_value, "Cut values must match");
|
||||
assert_eq!(r1.cut_set, r2.cut_set, "Cut sets must match");
|
||||
}
|
||||
(None, None) => {}
|
||||
_ => panic!("Results must both exist or both be None"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_graph() {
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
let local_kcut = LocalKCut::new(graph, 5);
|
||||
|
||||
// Should return None for non-existent vertex
|
||||
assert!(local_kcut.find_cut(1).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_single_edge() {
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
graph.insert_edge(1, 2, 3.0).unwrap();
|
||||
|
||||
let local_kcut = LocalKCut::new(graph, 5);
|
||||
let result = local_kcut.find_cut(1).expect("Should find a cut");
|
||||
|
||||
// The only cut is the single edge
|
||||
assert_eq!(result.cut_value, 3.0);
|
||||
assert_eq!(result.cut_edges.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_complete_graph_k4() {
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
|
||||
// Complete graph K4 (all edges have weight 1)
|
||||
for i in 1..=4 {
|
||||
for j in i + 1..=4 {
|
||||
graph.insert_edge(i, j, 1.0).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
let local_kcut = LocalKCut::new(graph.clone(), 5);
|
||||
|
||||
// Find cuts from each vertex
|
||||
for vertex in 1..=4 {
|
||||
if let Some(result) = local_kcut.find_cut(vertex) {
|
||||
// In K4, minimum cut is 3 (separating one vertex from the rest)
|
||||
assert!(result.cut_value >= 3.0, "K4 minimum cut is at least 3");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_star_graph() {
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
|
||||
// Star graph: center vertex 1 connected to 5 leaves
|
||||
for i in 2..=6 {
|
||||
graph.insert_edge(1, i, 1.0).unwrap();
|
||||
}
|
||||
|
||||
let local_kcut = LocalKCut::new(graph.clone(), 3);
|
||||
|
||||
// From a leaf, should find cut separating that leaf
|
||||
let result = local_kcut.find_cut(2).expect("Should find a cut");
|
||||
assert_eq!(result.cut_value, 1.0, "Leaf should have cut value 1");
|
||||
|
||||
// From center, harder to find small cut
|
||||
let result = local_kcut.find_cut(1);
|
||||
if let Some(r) = result {
|
||||
assert!(r.cut_value <= 3.0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cycle_graph() {
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
|
||||
// Cycle graph: 1-2-3-4-5-1
|
||||
let n = 8;
|
||||
for i in 1..=n {
|
||||
graph
|
||||
.insert_edge(i, if i == n { 1 } else { i + 1 }, 1.0)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let local_kcut = LocalKCut::new(graph, 3);
|
||||
|
||||
// In a cycle, any cut needs at least 2 edges
|
||||
for vertex in 1..=n {
|
||||
if let Some(result) = local_kcut.find_cut(vertex) {
|
||||
assert!(
|
||||
result.cut_value >= 2.0,
|
||||
"Cycle requires at least 2 edges to cut"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_weighted_edges() {
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
|
||||
// Graph with varying weights
|
||||
graph.insert_edge(1, 2, 5.0).unwrap();
|
||||
graph.insert_edge(2, 3, 1.0).unwrap();
|
||||
graph.insert_edge(3, 4, 5.0).unwrap();
|
||||
|
||||
let local_kcut = LocalKCut::new(graph, 3);
|
||||
|
||||
// Should prefer to cut the edge with weight 1
|
||||
let result = local_kcut.find_cut(2).expect("Should find a cut");
|
||||
|
||||
assert!(result.cut_value <= 3.0, "Should find cut with value <= k=3");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_color_mask_combinations() {
|
||||
// Test various color combinations
|
||||
let test_cases = vec![
|
||||
(vec![], 0),
|
||||
(vec![EdgeColor::Red], 1),
|
||||
(vec![EdgeColor::Red, EdgeColor::Blue], 2),
|
||||
(vec![EdgeColor::Red, EdgeColor::Blue, EdgeColor::Green], 3),
|
||||
(EdgeColor::all().to_vec(), 4),
|
||||
];
|
||||
|
||||
for (colors, expected_count) in test_cases {
|
||||
let mask = ColorMask::from_colors(&colors);
|
||||
assert_eq!(mask.count(), expected_count);
|
||||
|
||||
// Verify each specified color is in the mask
|
||||
for color in &colors {
|
||||
assert!(mask.contains(*color));
|
||||
}
|
||||
}
|
||||
|
||||
// Test empty and all masks
|
||||
assert_eq!(ColorMask::empty().count(), 0);
|
||||
assert_eq!(ColorMask::all().count(), 4);
|
||||
|
||||
for color in EdgeColor::all() {
|
||||
assert!(ColorMask::all().contains(color));
|
||||
assert!(!ColorMask::empty().contains(color));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_forest_packing_completeness() {
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
|
||||
// Create a grid graph
|
||||
for i in 0..3 {
|
||||
for j in 0..3 {
|
||||
let v = i * 3 + j + 1;
|
||||
if j < 2 {
|
||||
graph.insert_edge(v, v + 1, 1.0).unwrap();
|
||||
}
|
||||
if i < 2 {
|
||||
graph.insert_edge(v, v + 3, 1.0).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let packing = ForestPacking::greedy_packing(&*graph, 5, 0.1);
|
||||
|
||||
// Should have created multiple forests
|
||||
assert!(packing.num_forests() > 0);
|
||||
|
||||
// Each forest should be acyclic
|
||||
for i in 0..packing.num_forests() {
|
||||
if let Some(forest) = packing.forest(i) {
|
||||
// Forest should have at most n-1 edges for n vertices
|
||||
assert!(forest.len() <= graph.num_vertices());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_forest_packing_witness() {
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
|
||||
// Simple graph - a cycle
|
||||
graph.insert_edge(1, 2, 1.0).unwrap();
|
||||
graph.insert_edge(2, 3, 1.0).unwrap();
|
||||
graph.insert_edge(3, 4, 1.0).unwrap();
|
||||
graph.insert_edge(1, 4, 1.0).unwrap();
|
||||
|
||||
let packing = ForestPacking::greedy_packing(&*graph, 3, 0.1);
|
||||
|
||||
// Verify forest packing was created
|
||||
assert!(
|
||||
packing.num_forests() >= 1,
|
||||
"Should have at least one forest"
|
||||
);
|
||||
|
||||
// Test witness property on single-edge cuts
|
||||
let cuts = vec![vec![(1, 2)], vec![(2, 3)]];
|
||||
|
||||
// Just verify the method works without panic
|
||||
for cut in cuts {
|
||||
let _is_witnessed = packing.witnesses_cut(&cut);
|
||||
// Result depends on random forest structure
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_radius_increases_with_k() {
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
graph.insert_edge(1, 2, 1.0).unwrap();
|
||||
|
||||
// Create instances with different k values
|
||||
let lk1 = LocalKCut::new(graph.clone(), 1);
|
||||
let lk2 = LocalKCut::new(graph.clone(), 10);
|
||||
let lk3 = LocalKCut::new(graph.clone(), 100);
|
||||
|
||||
// Radius should increase or stay the same as k increases
|
||||
assert!(lk1.radius() <= lk2.radius());
|
||||
assert!(lk2.radius() <= lk3.radius());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_enumerate_paths_diversity() {
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
|
||||
// Create a graph with multiple paths
|
||||
// 1 - 2 - 3
|
||||
// | | |
|
||||
// 4 - 5 - 6
|
||||
|
||||
graph.insert_edge(1, 2, 1.0).unwrap();
|
||||
graph.insert_edge(2, 3, 1.0).unwrap();
|
||||
graph.insert_edge(1, 4, 1.0).unwrap();
|
||||
graph.insert_edge(2, 5, 1.0).unwrap();
|
||||
graph.insert_edge(3, 6, 1.0).unwrap();
|
||||
graph.insert_edge(4, 5, 1.0).unwrap();
|
||||
graph.insert_edge(5, 6, 1.0).unwrap();
|
||||
|
||||
let local_kcut = LocalKCut::new(graph, 5);
|
||||
|
||||
let paths = local_kcut.enumerate_paths(1, 3);
|
||||
|
||||
// Should find multiple different reachable sets
|
||||
assert!(paths.len() > 1, "Should find multiple paths");
|
||||
|
||||
// All paths should contain the start vertex
|
||||
for path in &paths {
|
||||
assert!(path.contains(&1), "All paths should contain start vertex");
|
||||
}
|
||||
|
||||
// Paths should have different sizes (due to different color masks)
|
||||
let mut sizes: Vec<_> = paths.iter().map(|p| p.len()).collect();
|
||||
sizes.sort_unstable();
|
||||
sizes.dedup();
|
||||
assert!(sizes.len() > 1, "Should have paths of different sizes");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_large_k_bound() {
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
|
||||
// Small graph
|
||||
graph.insert_edge(1, 2, 1.0).unwrap();
|
||||
graph.insert_edge(2, 3, 1.0).unwrap();
|
||||
|
||||
// Very large k should still work
|
||||
let local_kcut = LocalKCut::new(graph, 1000);
|
||||
let result = local_kcut.find_cut(1);
|
||||
|
||||
assert!(result.is_some(), "Should find a cut even with large k");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_disconnected_graph() {
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
|
||||
// Two disconnected components
|
||||
graph.insert_edge(1, 2, 1.0).unwrap();
|
||||
graph.insert_edge(3, 4, 1.0).unwrap();
|
||||
|
||||
let local_kcut = LocalKCut::new(graph, 5);
|
||||
|
||||
// From component 1, should find cut with value 0
|
||||
let result1 = local_kcut.find_cut(1);
|
||||
assert!(result1.is_some(), "Should find cut in disconnected graph");
|
||||
|
||||
// From component 2
|
||||
let result2 = local_kcut.find_cut(3);
|
||||
assert!(result2.is_some(), "Should find cut in disconnected graph");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_local_cut_result_properties() {
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
|
||||
// Create a simple graph
|
||||
for i in 1..=5 {
|
||||
graph.insert_edge(i, i + 1, 1.0).unwrap();
|
||||
}
|
||||
|
||||
let local_kcut = LocalKCut::new(graph.clone(), 3);
|
||||
let result = local_kcut.find_cut(3).expect("Should find a cut");
|
||||
|
||||
// Verify result properties
|
||||
assert!(result.cut_value > 0.0);
|
||||
assert!(!result.cut_set.is_empty());
|
||||
assert!(!result.cut_edges.is_empty());
|
||||
assert!(result.iterations > 0);
|
||||
|
||||
// Cut set should not include all vertices
|
||||
assert!(result.cut_set.len() < graph.num_vertices());
|
||||
|
||||
// Cut edges should match the cut value
|
||||
let mut computed_value = 0.0;
|
||||
for (u, v) in &result.cut_edges {
|
||||
if let Some(weight) = graph.edge_weight(*u, *v) {
|
||||
computed_value += weight;
|
||||
}
|
||||
}
|
||||
assert!(
|
||||
(result.cut_value - computed_value).abs() < 0.001,
|
||||
"Cut value should match sum of edge weights"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_community_structure_detection() {
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
|
||||
// Create two dense communities with weak inter-connections
|
||||
// Community 1: {1, 2, 3}
|
||||
graph.insert_edge(1, 2, 5.0).unwrap();
|
||||
graph.insert_edge(2, 3, 5.0).unwrap();
|
||||
graph.insert_edge(3, 1, 5.0).unwrap();
|
||||
|
||||
// Weak connection
|
||||
graph.insert_edge(3, 4, 1.0).unwrap();
|
||||
|
||||
// Community 2: {4, 5, 6}
|
||||
graph.insert_edge(4, 5, 5.0).unwrap();
|
||||
graph.insert_edge(5, 6, 5.0).unwrap();
|
||||
graph.insert_edge(6, 4, 5.0).unwrap();
|
||||
|
||||
let local_kcut = LocalKCut::new(graph, 5);
|
||||
|
||||
// From community 1, should find the weak connection
|
||||
let result = local_kcut.find_cut(1).expect("Should find a cut");
|
||||
|
||||
// Should find the weak inter-community edge
|
||||
assert!(
|
||||
result.cut_value <= 5.0,
|
||||
"Should find cut along weak connection"
|
||||
);
|
||||
|
||||
// The cut should separate the communities
|
||||
let separates_communities = result.cut_set.len() == 3
|
||||
&& (result.cut_set.contains(&1)
|
||||
&& result.cut_set.contains(&2)
|
||||
&& result.cut_set.contains(&3));
|
||||
|
||||
assert!(
|
||||
separates_communities || result.cut_set.len() < 3,
|
||||
"Cut should respect community structure"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_performance_characteristics() {
|
||||
// Test that algorithm performs reasonably on various graph sizes
|
||||
for n in [10, 20, 50] {
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
|
||||
// Create a path graph
|
||||
for i in 1..n {
|
||||
graph.insert_edge(i, i + 1, 1.0).unwrap();
|
||||
}
|
||||
|
||||
let start = std::time::Instant::now();
|
||||
let local_kcut = LocalKCut::new(graph.clone(), 5);
|
||||
|
||||
// Find cuts from a few vertices
|
||||
for &v in &[1, n / 2, n - 1] {
|
||||
let _ = local_kcut.find_cut(v);
|
||||
}
|
||||
|
||||
let elapsed = start.elapsed();
|
||||
|
||||
// Should complete in reasonable time (< 100ms for these small graphs)
|
||||
assert!(
|
||||
elapsed.as_millis() < 100,
|
||||
"Should complete in reasonable time for n={}",
|
||||
n
|
||||
);
|
||||
}
|
||||
}
|
||||
312
vendor/ruvector/crates/ruvector-mincut/tests/localkcut_paper_integration.rs
vendored
Normal file
312
vendor/ruvector/crates/ruvector-mincut/tests/localkcut_paper_integration.rs
vendored
Normal file
@@ -0,0 +1,312 @@
|
||||
//! Integration tests for paper-compliant LocalKCut implementation
|
||||
//!
|
||||
//! Tests the integration between the paper implementation and the
|
||||
//! rest of the minimum cut system.
|
||||
|
||||
use ruvector_mincut::{
|
||||
DeterministicFamilyGenerator, DeterministicLocalKCut, DynamicGraph, LocalKCutOracle,
|
||||
LocalKCutQuery, PaperLocalKCutResult as LocalKCutResult,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[test]
|
||||
fn test_paper_api_basic_usage() {
|
||||
// Create a simple graph
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
graph.insert_edge(1, 2, 1.0).unwrap();
|
||||
graph.insert_edge(2, 3, 1.0).unwrap();
|
||||
graph.insert_edge(3, 4, 1.0).unwrap();
|
||||
|
||||
// Create oracle
|
||||
let oracle = DeterministicLocalKCut::new(5);
|
||||
|
||||
// Create query
|
||||
let query = LocalKCutQuery {
|
||||
seed_vertices: vec![1],
|
||||
budget_k: 2,
|
||||
radius: 3,
|
||||
};
|
||||
|
||||
// Execute search
|
||||
let result = oracle.search(&graph, query);
|
||||
|
||||
// Should find some cut or report none
|
||||
match result {
|
||||
LocalKCutResult::Found { cut_value, witness } => {
|
||||
assert!(cut_value <= 2);
|
||||
assert!(witness.boundary_size() <= 2);
|
||||
println!("Found cut with value: {}", cut_value);
|
||||
}
|
||||
LocalKCutResult::NoneInLocality => {
|
||||
println!("No cut found in locality");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_paper_api_with_family_generator() {
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
|
||||
// Create a more complex graph
|
||||
for i in 1..=10 {
|
||||
graph.insert_edge(i, i + 1, 1.0).unwrap();
|
||||
}
|
||||
|
||||
// Add some cross edges
|
||||
graph.insert_edge(1, 5, 1.0).unwrap();
|
||||
graph.insert_edge(3, 7, 1.0).unwrap();
|
||||
|
||||
// Create oracle with custom family generator
|
||||
let generator = DeterministicFamilyGenerator::new(5);
|
||||
let oracle = DeterministicLocalKCut::with_family_generator(8, generator);
|
||||
|
||||
let query = LocalKCutQuery {
|
||||
seed_vertices: vec![1, 2, 3],
|
||||
budget_k: 3,
|
||||
radius: 5,
|
||||
};
|
||||
|
||||
let result = oracle.search(&graph, query);
|
||||
|
||||
// Verify result type
|
||||
match result {
|
||||
LocalKCutResult::Found { cut_value, witness } => {
|
||||
assert!(cut_value <= 3);
|
||||
assert_eq!(witness.boundary_size(), cut_value);
|
||||
}
|
||||
LocalKCutResult::NoneInLocality => {
|
||||
// Acceptable
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_witness_integration() {
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
|
||||
// Create two components connected by a single edge
|
||||
// Component 1
|
||||
graph.insert_edge(1, 2, 1.0).unwrap();
|
||||
graph.insert_edge(2, 3, 1.0).unwrap();
|
||||
graph.insert_edge(3, 1, 1.0).unwrap();
|
||||
|
||||
// Bridge
|
||||
graph.insert_edge(3, 4, 1.0).unwrap();
|
||||
|
||||
// Component 2
|
||||
graph.insert_edge(4, 5, 1.0).unwrap();
|
||||
graph.insert_edge(5, 6, 1.0).unwrap();
|
||||
graph.insert_edge(6, 4, 1.0).unwrap();
|
||||
|
||||
let oracle = DeterministicLocalKCut::new(10);
|
||||
|
||||
let query = LocalKCutQuery {
|
||||
seed_vertices: vec![1],
|
||||
budget_k: 2,
|
||||
radius: 10,
|
||||
};
|
||||
|
||||
let result = oracle.search(&graph, query);
|
||||
|
||||
match result {
|
||||
LocalKCutResult::Found { cut_value, witness } => {
|
||||
// Should find the bridge (cut value = 1)
|
||||
assert_eq!(cut_value, 1);
|
||||
|
||||
// Witness should be consistent
|
||||
assert_eq!(witness.boundary_size(), 1);
|
||||
|
||||
// Seed should be in witness
|
||||
assert!(witness.contains(witness.seed()));
|
||||
|
||||
// Can materialize partition
|
||||
let (u, _v_minus_u) = witness.materialize_partition();
|
||||
assert!(!u.is_empty());
|
||||
|
||||
println!("Found bridge cut with {} vertices", witness.cardinality());
|
||||
}
|
||||
LocalKCutResult::NoneInLocality => {
|
||||
panic!("Should find the bridge");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_determinism_across_calls() {
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
|
||||
// Create deterministic graph structure
|
||||
for i in 1..=8 {
|
||||
graph.insert_edge(i, i + 1, 1.0).unwrap();
|
||||
}
|
||||
graph.insert_edge(4, 6, 1.0).unwrap();
|
||||
|
||||
let oracle = DeterministicLocalKCut::new(5);
|
||||
|
||||
let query = LocalKCutQuery {
|
||||
seed_vertices: vec![2, 3],
|
||||
budget_k: 3,
|
||||
radius: 4,
|
||||
};
|
||||
|
||||
// Run multiple times
|
||||
let mut results = Vec::new();
|
||||
for _ in 0..5 {
|
||||
results.push(oracle.search(&graph, query.clone()));
|
||||
}
|
||||
|
||||
// All results should be identical
|
||||
for i in 1..results.len() {
|
||||
match (&results[0], &results[i]) {
|
||||
(
|
||||
LocalKCutResult::Found {
|
||||
cut_value: v1,
|
||||
witness: w1,
|
||||
},
|
||||
LocalKCutResult::Found {
|
||||
cut_value: v2,
|
||||
witness: w2,
|
||||
},
|
||||
) => {
|
||||
assert_eq!(v1, v2, "Cut values should be deterministic");
|
||||
assert_eq!(w1.seed(), w2.seed(), "Seeds should match");
|
||||
assert_eq!(
|
||||
w1.boundary_size(),
|
||||
w2.boundary_size(),
|
||||
"Boundary sizes should match"
|
||||
);
|
||||
assert_eq!(
|
||||
w1.cardinality(),
|
||||
w2.cardinality(),
|
||||
"Cardinalities should match"
|
||||
);
|
||||
}
|
||||
(LocalKCutResult::NoneInLocality, LocalKCutResult::NoneInLocality) => {
|
||||
// Both none - consistent
|
||||
}
|
||||
_ => {
|
||||
panic!("Results are not deterministic!");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_budget_boundary() {
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
|
||||
// Create a graph with known minimum cut = 2
|
||||
graph.insert_edge(1, 2, 1.0).unwrap();
|
||||
graph.insert_edge(1, 3, 1.0).unwrap();
|
||||
graph.insert_edge(2, 4, 1.0).unwrap();
|
||||
graph.insert_edge(3, 4, 1.0).unwrap();
|
||||
|
||||
let oracle = DeterministicLocalKCut::new(5);
|
||||
|
||||
// Try with budget = 1 (should fail or find nothing)
|
||||
let query_low = LocalKCutQuery {
|
||||
seed_vertices: vec![1],
|
||||
budget_k: 1,
|
||||
radius: 5,
|
||||
};
|
||||
|
||||
let result_low = oracle.search(&graph, query_low);
|
||||
if let LocalKCutResult::Found { cut_value, .. } = result_low {
|
||||
assert!(cut_value <= 1, "Must respect budget constraint");
|
||||
}
|
||||
|
||||
// Try with budget = 3 (should succeed)
|
||||
let query_high = LocalKCutQuery {
|
||||
seed_vertices: vec![1],
|
||||
budget_k: 3,
|
||||
radius: 5,
|
||||
};
|
||||
|
||||
let result_high = oracle.search(&graph, query_high);
|
||||
match result_high {
|
||||
LocalKCutResult::Found { cut_value, .. } => {
|
||||
assert!(cut_value <= 3, "Must respect budget constraint");
|
||||
}
|
||||
LocalKCutResult::NoneInLocality => {
|
||||
// Acceptable based on radius
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_radius_limiting() {
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
|
||||
// Create a long path
|
||||
for i in 1..=20 {
|
||||
graph.insert_edge(i, i + 1, 1.0).unwrap();
|
||||
}
|
||||
|
||||
let oracle = DeterministicLocalKCut::new(3); // max_radius = 3
|
||||
|
||||
// Request large radius (should be capped at max_radius)
|
||||
let query = LocalKCutQuery {
|
||||
seed_vertices: vec![1],
|
||||
budget_k: 2,
|
||||
radius: 100,
|
||||
};
|
||||
|
||||
// Should not panic, should cap at max_radius = 3
|
||||
let result = oracle.search(&graph, query);
|
||||
|
||||
// Result should be based on radius=3, not radius=100
|
||||
match result {
|
||||
LocalKCutResult::Found { witness, .. } => {
|
||||
// With radius=3, should find at most 4 vertices (seed + 3 layers)
|
||||
assert!(witness.cardinality() <= 4);
|
||||
}
|
||||
LocalKCutResult::NoneInLocality => {
|
||||
// Acceptable
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_graph() {
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
let oracle = DeterministicLocalKCut::new(5);
|
||||
|
||||
let query = LocalKCutQuery {
|
||||
seed_vertices: vec![1],
|
||||
budget_k: 10,
|
||||
radius: 5,
|
||||
};
|
||||
|
||||
let result = oracle.search(&graph, query);
|
||||
|
||||
// Should return NoneInLocality for empty graph
|
||||
assert!(matches!(result, LocalKCutResult::NoneInLocality));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_single_vertex() {
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
|
||||
// Add a single vertex (via an edge to itself would be invalid, so just query)
|
||||
graph.insert_edge(1, 2, 1.0).unwrap();
|
||||
|
||||
let oracle = DeterministicLocalKCut::new(5);
|
||||
|
||||
let query = LocalKCutQuery {
|
||||
seed_vertices: vec![1],
|
||||
budget_k: 10,
|
||||
radius: 0, // Don't expand
|
||||
};
|
||||
|
||||
let result = oracle.search(&graph, query);
|
||||
|
||||
// With radius=0, should find single vertex or none
|
||||
match result {
|
||||
LocalKCutResult::Found { witness, .. } => {
|
||||
assert_eq!(witness.cardinality(), 1);
|
||||
}
|
||||
LocalKCutResult::NoneInLocality => {
|
||||
// Also acceptable
|
||||
}
|
||||
}
|
||||
}
|
||||
595
vendor/ruvector/crates/ruvector-mincut/tests/paper_algorithm_tests.rs
vendored
Normal file
595
vendor/ruvector/crates/ruvector-mincut/tests/paper_algorithm_tests.rs
vendored
Normal file
@@ -0,0 +1,595 @@
|
||||
//! Property-based tests for paper algorithm implementations
|
||||
//!
|
||||
//! Tests the correctness of:
|
||||
//! - DeterministicLocalKCut (Theorem 4.1)
|
||||
//! - Fragmentation with Trim (Theorem 5.1)
|
||||
//! - ThreeLevelHierarchy (expander→precluster→cluster)
|
||||
|
||||
use ruvector_mincut::cluster::hierarchy::{HierarchyConfig, ThreeLevelHierarchy};
|
||||
use ruvector_mincut::fragmentation::{Fragmentation, FragmentationConfig};
|
||||
use ruvector_mincut::localkcut::deterministic::{
|
||||
DeterministicLocalKCut, EdgeColor, EdgeColoring, GreedyForestPacking,
|
||||
};
|
||||
use std::collections::HashSet;
|
||||
|
||||
// ============================================================================
|
||||
// Helper Functions
|
||||
// ============================================================================
|
||||
|
||||
/// Brute-force minimum cut for small graphs using exhaustive subset enumeration
|
||||
fn brute_force_min_cut(adjacency: &[(u64, u64, f64)], vertices: &[u64]) -> f64 {
|
||||
if vertices.len() <= 1 {
|
||||
return f64::INFINITY;
|
||||
}
|
||||
|
||||
let vertex_set: HashSet<_> = vertices.iter().copied().collect();
|
||||
let n = vertices.len();
|
||||
let mut min_cut = f64::INFINITY;
|
||||
|
||||
// Enumerate all non-empty proper subsets (2^n - 2 subsets)
|
||||
// Only practical for small n
|
||||
for mask in 1..(1 << n) - 1 {
|
||||
let mut subset: HashSet<u64> = HashSet::new();
|
||||
for (i, &v) in vertices.iter().enumerate() {
|
||||
if (mask >> i) & 1 == 1 {
|
||||
subset.insert(v);
|
||||
}
|
||||
}
|
||||
|
||||
// Compute cut value
|
||||
let mut cut_value = 0.0;
|
||||
for &(u, v, w) in adjacency {
|
||||
let u_in = subset.contains(&u);
|
||||
let v_in = subset.contains(&v);
|
||||
if u_in != v_in {
|
||||
cut_value += w;
|
||||
}
|
||||
}
|
||||
|
||||
min_cut = min_cut.min(cut_value);
|
||||
}
|
||||
|
||||
min_cut
|
||||
}
|
||||
|
||||
/// Check if graph is connected using BFS
|
||||
fn is_connected(adjacency: &[(u64, u64, f64)], vertices: &[u64]) -> bool {
|
||||
if vertices.is_empty() {
|
||||
return true;
|
||||
}
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
let mut visited: HashSet<u64> = HashSet::new();
|
||||
let mut queue = VecDeque::new();
|
||||
|
||||
queue.push_back(vertices[0]);
|
||||
visited.insert(vertices[0]);
|
||||
|
||||
while let Some(v) = queue.pop_front() {
|
||||
for &(u, w, _) in adjacency {
|
||||
if u == v && !visited.contains(&w) {
|
||||
visited.insert(w);
|
||||
queue.push_back(w);
|
||||
}
|
||||
if w == v && !visited.contains(&u) {
|
||||
visited.insert(u);
|
||||
queue.push_back(u);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
visited.len() == vertices.len()
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// DeterministicLocalKCut Tests
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn test_localkcut_finds_small_cuts() {
|
||||
// Test that LocalKCut finds cuts when they exist
|
||||
let mut lkc = DeterministicLocalKCut::new(10, 100, 2);
|
||||
|
||||
// Build two cliques connected by a single edge (barbell graph)
|
||||
// Clique 1: vertices 1,2,3
|
||||
lkc.insert_edge(1, 2, 1.0);
|
||||
lkc.insert_edge(2, 3, 1.0);
|
||||
lkc.insert_edge(1, 3, 1.0);
|
||||
|
||||
// Clique 2: vertices 4,5,6
|
||||
lkc.insert_edge(4, 5, 1.0);
|
||||
lkc.insert_edge(5, 6, 1.0);
|
||||
lkc.insert_edge(4, 6, 1.0);
|
||||
|
||||
// Bridge
|
||||
lkc.insert_edge(3, 4, 1.0);
|
||||
|
||||
// Query from vertex 1
|
||||
let cuts = lkc.query(1);
|
||||
|
||||
// Should find at least one cut
|
||||
assert!(!cuts.is_empty(), "Should find at least one cut");
|
||||
|
||||
// At least one cut should have value <= 1 (the bridge)
|
||||
let has_small_cut = cuts.iter().any(|c| c.cut_value <= 2.0);
|
||||
assert!(has_small_cut, "Should find a small cut (the bridge)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_localkcut_respects_volume_bound() {
|
||||
let mut lkc = DeterministicLocalKCut::new(10, 5, 2); // Small volume bound
|
||||
|
||||
// Build a star graph (high degree center)
|
||||
for i in 2..=10 {
|
||||
lkc.insert_edge(1, i, 1.0);
|
||||
}
|
||||
|
||||
// Query from center (vertex 1)
|
||||
let cuts = lkc.query(1);
|
||||
|
||||
// All cuts should respect volume bound
|
||||
for cut in cuts {
|
||||
assert!(cut.volume <= 5, "Cut volume {} exceeds bound 5", cut.volume);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_forest_packing_no_cycles() {
|
||||
let mut packing = GreedyForestPacking::new(3);
|
||||
|
||||
// Insert edges that form a cycle
|
||||
packing.insert_edge(1, 2);
|
||||
packing.insert_edge(2, 3);
|
||||
packing.insert_edge(3, 4);
|
||||
|
||||
// This edge would close a cycle
|
||||
let forest = packing.insert_edge(1, 4);
|
||||
|
||||
// Should still be assigned (to a different forest)
|
||||
assert!(
|
||||
forest.is_some(),
|
||||
"Cycle-closing edge should fit in some forest"
|
||||
);
|
||||
|
||||
// Verify no single forest has a cycle
|
||||
for f in 0..3 {
|
||||
let edges = packing.forest_edges(f);
|
||||
// A forest on n vertices has at most n-1 edges
|
||||
// With 4 vertices, each forest should have <= 3 edges
|
||||
assert!(edges.len() <= 3, "Forest {} has too many edges", f);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edge_coloring_deterministic() {
|
||||
// Same edges should get same colors
|
||||
let mut coloring1 = EdgeColoring::new(2, 5);
|
||||
let mut coloring2 = EdgeColoring::new(2, 5);
|
||||
|
||||
// Set same colors
|
||||
coloring1.set(1, 2, EdgeColor::Red);
|
||||
coloring1.set(2, 3, EdgeColor::Blue);
|
||||
coloring2.set(1, 2, EdgeColor::Red);
|
||||
coloring2.set(2, 3, EdgeColor::Blue);
|
||||
|
||||
assert_eq!(coloring1.get(1, 2), coloring2.get(1, 2));
|
||||
assert_eq!(coloring1.get(2, 3), coloring2.get(2, 3));
|
||||
|
||||
// Order shouldn't matter
|
||||
assert_eq!(coloring1.get(1, 2), coloring1.get(2, 1));
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Fragmentation Tests
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn test_fragmentation_covers_all_vertices() {
|
||||
let mut frag = Fragmentation::new(FragmentationConfig {
|
||||
min_fragment_size: 2,
|
||||
max_fragment_size: 10,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
// Build a path graph
|
||||
for i in 0..15 {
|
||||
frag.insert_edge(i, i + 1, 1.0);
|
||||
}
|
||||
|
||||
let roots = frag.fragment();
|
||||
assert!(!roots.is_empty(), "Should have at least one fragment");
|
||||
|
||||
// Collect all vertices from leaf fragments
|
||||
let mut covered: HashSet<u64> = HashSet::new();
|
||||
for fragment in frag.leaf_fragments() {
|
||||
covered.extend(&fragment.vertices);
|
||||
}
|
||||
|
||||
// All vertices should be covered
|
||||
for i in 0..=15 {
|
||||
assert!(covered.contains(&i), "Vertex {} not covered", i);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fragmentation_boundary_sparse() {
|
||||
let config = FragmentationConfig {
|
||||
boundary_sparsity: 0.5,
|
||||
min_fragment_size: 2,
|
||||
..Default::default()
|
||||
};
|
||||
let mut frag = Fragmentation::new(config);
|
||||
|
||||
// Build two cliques connected by single edge
|
||||
for i in 1..=4 {
|
||||
for j in i + 1..=4 {
|
||||
frag.insert_edge(i, j, 1.0);
|
||||
}
|
||||
}
|
||||
for i in 5..=8 {
|
||||
for j in i + 1..=8 {
|
||||
frag.insert_edge(i, j, 1.0);
|
||||
}
|
||||
}
|
||||
frag.insert_edge(4, 5, 1.0);
|
||||
|
||||
frag.fragment();
|
||||
|
||||
// Leaf fragments should have reasonable boundary sparsity
|
||||
for fragment in frag.leaf_fragments() {
|
||||
let sparsity = fragment.boundary_sparsity();
|
||||
// Sparsity should be bounded (not guaranteed to be below threshold due to greedy)
|
||||
assert!(
|
||||
sparsity <= 2.0,
|
||||
"Fragment has very high sparsity: {}",
|
||||
sparsity
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_trim_produces_valid_cut() {
|
||||
let mut frag = Fragmentation::with_defaults();
|
||||
|
||||
// Build a path graph
|
||||
for i in 0..10 {
|
||||
frag.insert_edge(i, i + 1, 1.0);
|
||||
}
|
||||
|
||||
let vertices: Vec<u64> = (0..=10).collect();
|
||||
let result = frag.trim(&vertices);
|
||||
|
||||
if result.success {
|
||||
// Trimmed vertices should be a proper subset
|
||||
assert!(result.trimmed_vertices.len() < vertices.len());
|
||||
assert!(!result.trimmed_vertices.is_empty());
|
||||
|
||||
// Cut edges should connect trimmed to non-trimmed
|
||||
for (u, v) in &result.cut_edges {
|
||||
let u_trimmed = result.trimmed_vertices.contains(u);
|
||||
let v_trimmed = result.trimmed_vertices.contains(v);
|
||||
assert!(u_trimmed != v_trimmed, "Cut edge should cross partition");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// ThreeLevelHierarchy Tests
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn test_hierarchy_levels_consistent() {
|
||||
let mut h = ThreeLevelHierarchy::new(HierarchyConfig {
|
||||
min_expander_size: 2,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
// Build graph
|
||||
for i in 1..=20 {
|
||||
h.insert_edge(i, i + 1, 1.0);
|
||||
}
|
||||
h.build();
|
||||
|
||||
// Every vertex should be in exactly one expander
|
||||
let mut vertex_count: std::collections::HashMap<u64, usize> = std::collections::HashMap::new();
|
||||
for expander in h.get_expanders().values() {
|
||||
for &v in &expander.vertices {
|
||||
*vertex_count.entry(v).or_insert(0) += 1;
|
||||
}
|
||||
}
|
||||
|
||||
for (v, count) in vertex_count {
|
||||
assert_eq!(count, 1, "Vertex {} appears in {} expanders", v, count);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hierarchy_global_min_cut_bound() {
|
||||
let mut h = ThreeLevelHierarchy::new(HierarchyConfig {
|
||||
min_expander_size: 2,
|
||||
track_mirror_cuts: true,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
// Build two cliques connected by edges of weight 3
|
||||
for i in 1..=4 {
|
||||
for j in i + 1..=4 {
|
||||
h.insert_edge(i, j, 1.0);
|
||||
}
|
||||
}
|
||||
for i in 5..=8 {
|
||||
for j in i + 1..=8 {
|
||||
h.insert_edge(i, j, 1.0);
|
||||
}
|
||||
}
|
||||
h.insert_edge(4, 5, 1.0);
|
||||
h.insert_edge(3, 6, 1.0);
|
||||
h.insert_edge(2, 7, 1.0);
|
||||
|
||||
h.build();
|
||||
|
||||
// Brute force min cut
|
||||
let edges: Vec<(u64, u64, f64)> = vec![
|
||||
(1, 2, 1.0),
|
||||
(1, 3, 1.0),
|
||||
(1, 4, 1.0),
|
||||
(2, 3, 1.0),
|
||||
(2, 4, 1.0),
|
||||
(3, 4, 1.0),
|
||||
(5, 6, 1.0),
|
||||
(5, 7, 1.0),
|
||||
(5, 8, 1.0),
|
||||
(6, 7, 1.0),
|
||||
(6, 8, 1.0),
|
||||
(7, 8, 1.0),
|
||||
(4, 5, 1.0),
|
||||
(3, 6, 1.0),
|
||||
(2, 7, 1.0),
|
||||
];
|
||||
let vertices: Vec<u64> = (1..=8).collect();
|
||||
let brute = brute_force_min_cut(&edges, &vertices);
|
||||
|
||||
// Hierarchy estimate should be <= actual min cut * some factor
|
||||
// (it's an upper bound approximation)
|
||||
assert!(
|
||||
h.global_min_cut <= brute * 2.0 + 0.1 || h.global_min_cut.is_infinite(),
|
||||
"Global min cut {} should be close to brute force {}",
|
||||
h.global_min_cut,
|
||||
brute
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_incremental_update_consistency() {
|
||||
let mut h = ThreeLevelHierarchy::with_defaults();
|
||||
|
||||
// Build initial graph
|
||||
h.insert_edge(1, 2, 1.0);
|
||||
h.insert_edge(2, 3, 1.0);
|
||||
h.insert_edge(3, 4, 1.0);
|
||||
h.build();
|
||||
|
||||
let initial_vertices = h.stats().num_vertices;
|
||||
|
||||
// Incremental insert
|
||||
h.handle_edge_insert(4, 5, 1.0);
|
||||
h.handle_edge_insert(5, 6, 1.0);
|
||||
|
||||
// Should have more vertices now
|
||||
assert!(h.stats().num_vertices >= initial_vertices);
|
||||
|
||||
// Every new vertex should be assigned
|
||||
assert!(h.get_vertex_expander(5).is_some() || h.get_expanders().is_empty());
|
||||
assert!(h.get_vertex_expander(6).is_some() || h.get_expanders().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mirror_cuts_between_expanders() {
|
||||
let mut h = ThreeLevelHierarchy::new(HierarchyConfig {
|
||||
min_expander_size: 2,
|
||||
max_expander_size: 5,
|
||||
track_mirror_cuts: true,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
// Build two dense components
|
||||
for i in 1..=4 {
|
||||
for j in i + 1..=4 {
|
||||
h.insert_edge(i, j, 1.0);
|
||||
}
|
||||
}
|
||||
for i in 10..=14 {
|
||||
for j in i + 1..=14 {
|
||||
h.insert_edge(i, j, 1.0);
|
||||
}
|
||||
}
|
||||
// Connect with bridge
|
||||
h.insert_edge(4, 10, 2.0);
|
||||
|
||||
h.build();
|
||||
|
||||
// Check that mirror cuts are being tracked
|
||||
let mut has_mirror_cut = false;
|
||||
for cluster in h.get_clusters().values() {
|
||||
if !cluster.mirror_cuts.is_empty() {
|
||||
has_mirror_cut = true;
|
||||
// Mirror cut should have the bridge
|
||||
for mirror in &cluster.mirror_cuts {
|
||||
assert!(
|
||||
mirror.cut_value > 0.0,
|
||||
"Mirror cut should have positive value"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we have multiple expanders, should have mirror cuts
|
||||
if h.get_expanders().len() > 1 {
|
||||
assert!(
|
||||
has_mirror_cut || h.get_clusters().len() > 1,
|
||||
"Should track mirror cuts between expanders"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Property Tests with Random Graphs
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn property_fragmentation_idempotent() {
|
||||
// Fragmenting twice should give same result
|
||||
let mut frag1 = Fragmentation::with_defaults();
|
||||
let mut frag2 = Fragmentation::with_defaults();
|
||||
|
||||
// Same graph
|
||||
for i in 0..10 {
|
||||
frag1.insert_edge(i, i + 1, 1.0);
|
||||
frag2.insert_edge(i, i + 1, 1.0);
|
||||
}
|
||||
|
||||
frag1.fragment();
|
||||
frag2.fragment();
|
||||
|
||||
// Same number of fragments
|
||||
assert_eq!(frag1.num_fragments(), frag2.num_fragments());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn property_hierarchy_covers_graph() {
|
||||
use rand::rngs::StdRng;
|
||||
use rand::{Rng, SeedableRng};
|
||||
|
||||
let mut rng = StdRng::seed_from_u64(42);
|
||||
|
||||
for _iteration in 0..10 {
|
||||
let mut h = ThreeLevelHierarchy::with_defaults();
|
||||
|
||||
// Random edges
|
||||
let n = rng.gen_range(5..20);
|
||||
let m = rng.gen_range(n..n * 2);
|
||||
|
||||
for _ in 0..m {
|
||||
let u = rng.gen_range(1..=n) as u64;
|
||||
let v = rng.gen_range(1..=n) as u64;
|
||||
if u != v {
|
||||
h.insert_edge(u, v, 1.0);
|
||||
}
|
||||
}
|
||||
|
||||
h.build();
|
||||
|
||||
// Count vertices in expanders
|
||||
let mut in_expanders: HashSet<u64> = HashSet::new();
|
||||
for exp in h.get_expanders().values() {
|
||||
in_expanders.extend(&exp.vertices);
|
||||
}
|
||||
|
||||
// All graph vertices should be covered
|
||||
let graph_vertices = h.stats().num_vertices;
|
||||
assert_eq!(
|
||||
in_expanders.len(),
|
||||
graph_vertices,
|
||||
"Expanders should cover all {} vertices",
|
||||
graph_vertices
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn property_localkcut_deterministic() {
|
||||
// Same graph, same queries, same results
|
||||
let mut lkc1 = DeterministicLocalKCut::new(10, 50, 2);
|
||||
let mut lkc2 = DeterministicLocalKCut::new(10, 50, 2);
|
||||
|
||||
// Same edges in same order
|
||||
for (u, v) in [(1, 2), (2, 3), (3, 4), (4, 1), (1, 3)] {
|
||||
lkc1.insert_edge(u, v, 1.0);
|
||||
lkc2.insert_edge(u, v, 1.0);
|
||||
}
|
||||
|
||||
let cuts1 = lkc1.query(1);
|
||||
let cuts2 = lkc2.query(1);
|
||||
|
||||
// Same number of cuts
|
||||
assert_eq!(cuts1.len(), cuts2.len(), "Queries should be deterministic");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mirror_cut_certification() {
|
||||
let mut h = ThreeLevelHierarchy::new(HierarchyConfig {
|
||||
min_expander_size: 2,
|
||||
max_expander_size: 5,
|
||||
track_mirror_cuts: true,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
// Build two well-separated components connected by a bridge
|
||||
// Component 1: vertices 1-4
|
||||
for i in 1..=4 {
|
||||
for j in i + 1..=4 {
|
||||
h.insert_edge(i, j, 1.0);
|
||||
}
|
||||
}
|
||||
// Component 2: vertices 10-14
|
||||
for i in 10..=14 {
|
||||
for j in i + 1..=14 {
|
||||
h.insert_edge(i, j, 1.0);
|
||||
}
|
||||
}
|
||||
// Bridge connecting them
|
||||
h.insert_edge(4, 10, 2.0);
|
||||
|
||||
h.build();
|
||||
|
||||
// Get counts before certification
|
||||
let total_mirror_cuts = h.num_mirror_cuts();
|
||||
|
||||
// Run certification
|
||||
h.certify_mirror_cuts();
|
||||
|
||||
// After certification, certified count should be >= 0
|
||||
let certified = h.num_certified_mirror_cuts();
|
||||
assert!(
|
||||
certified <= total_mirror_cuts,
|
||||
"Certified {} should be <= total {}",
|
||||
certified,
|
||||
total_mirror_cuts
|
||||
);
|
||||
|
||||
// If we have mirror cuts, certification should have processed them
|
||||
if total_mirror_cuts > 0 {
|
||||
// At least some should be certified (or all if valid)
|
||||
assert!(
|
||||
certified >= 0,
|
||||
"Certification should not produce negative count"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_brute_force_matches_known_cut() {
|
||||
// Test our brute force helper against a known graph
|
||||
// Triangle with vertices 1, 2, 3 - min cut is 2 (remove any vertex)
|
||||
let edges = vec![(1, 2, 1.0), (2, 3, 1.0), (1, 3, 1.0)];
|
||||
let vertices = vec![1, 2, 3];
|
||||
|
||||
let min_cut = brute_force_min_cut(&edges, &vertices);
|
||||
assert!(
|
||||
(min_cut - 2.0).abs() < 0.001,
|
||||
"Triangle min cut should be 2, got {}",
|
||||
min_cut
|
||||
);
|
||||
|
||||
// Path graph 1-2-3-4 - min cut is 1
|
||||
let path_edges = vec![(1, 2, 1.0), (2, 3, 1.0), (3, 4, 1.0)];
|
||||
let path_vertices = vec![1, 2, 3, 4];
|
||||
|
||||
let path_cut = brute_force_min_cut(&path_edges, &path_vertices);
|
||||
assert!(
|
||||
(path_cut - 1.0).abs() < 0.001,
|
||||
"Path min cut should be 1, got {}",
|
||||
path_cut
|
||||
);
|
||||
}
|
||||
748
vendor/ruvector/crates/ruvector-mincut/tests/wrapper_tests.rs
vendored
Normal file
748
vendor/ruvector/crates/ruvector-mincut/tests/wrapper_tests.rs
vendored
Normal file
@@ -0,0 +1,748 @@
|
||||
//! Property tests for MinCutWrapper and Milestone A+B components
|
||||
//!
|
||||
//! Validates wrapper logic matches December 2024 breakthrough paper specification.
|
||||
//! Tests the geometric range-based instance management and ordering guarantees.
|
||||
|
||||
use ruvector_mincut::prelude::*;
|
||||
use std::sync::Arc;
|
||||
|
||||
// ============================================================================
|
||||
// Helper Functions
|
||||
// ============================================================================
|
||||
|
||||
/// Compute minimum cut using brute-force Stoer-Wagner algorithm
|
||||
/// This serves as a reference implementation for correctness testing
|
||||
fn stoer_wagner_min_cut(graph: &DynamicGraph) -> f64 {
|
||||
if graph.num_vertices() == 0 {
|
||||
return f64::INFINITY;
|
||||
}
|
||||
|
||||
if !graph.is_connected() {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
// For small graphs, compute exactly using connectivity checks
|
||||
let vertices = graph.vertices();
|
||||
if vertices.len() <= 1 {
|
||||
return f64::INFINITY;
|
||||
}
|
||||
|
||||
if vertices.len() == 2 {
|
||||
// Single edge between two vertices
|
||||
if let Some(edge) = graph.get_edge(vertices[0], vertices[1]) {
|
||||
return edge.weight;
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
// For connected graph, find minimum vertex degree as lower bound
|
||||
let mut min_cut = f64::INFINITY;
|
||||
for &v in &vertices {
|
||||
let mut degree_sum = 0.0;
|
||||
for (_, edge_id) in graph.neighbors(v) {
|
||||
if let Some(e) = graph.edges().iter().find(|e| e.id == edge_id) {
|
||||
degree_sum += e.weight;
|
||||
}
|
||||
}
|
||||
min_cut = min_cut.min(degree_sum);
|
||||
}
|
||||
|
||||
min_cut
|
||||
}
|
||||
|
||||
/// Build a path graph: v1 - v2 - v3 - ... - vn
|
||||
fn build_path_graph(n: usize) -> Arc<DynamicGraph> {
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
for i in 1..n {
|
||||
graph.insert_edge(i as u64, (i + 1) as u64, 1.0).unwrap();
|
||||
}
|
||||
graph
|
||||
}
|
||||
|
||||
/// Build a cycle graph: v1 - v2 - ... - vn - v1
|
||||
fn build_cycle_graph(n: usize) -> Arc<DynamicGraph> {
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
for i in 1..=n {
|
||||
let next = if i == n { 1 } else { i + 1 };
|
||||
graph.insert_edge(i as u64, next as u64, 1.0).unwrap();
|
||||
}
|
||||
graph
|
||||
}
|
||||
|
||||
/// Build a complete graph K_n
|
||||
fn build_complete_graph(n: usize) -> Arc<DynamicGraph> {
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
for i in 1..=n {
|
||||
for j in (i + 1)..=n {
|
||||
graph.insert_edge(i as u64, j as u64, 1.0).unwrap();
|
||||
}
|
||||
}
|
||||
graph
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Geometric Range Tests
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn test_geometric_range_factor() {
|
||||
// Test that ranges follow geometric progression with factor 1.2
|
||||
let base: f64 = 1.2;
|
||||
|
||||
for i in 0..20 {
|
||||
let lambda_min = (base.powi(i)).floor() as u64;
|
||||
let lambda_max = (base.powi(i + 1)).floor() as u64;
|
||||
|
||||
// Verify geometric progression
|
||||
assert!(
|
||||
lambda_max >= lambda_min,
|
||||
"Range {} must be valid: min={}, max={}",
|
||||
i,
|
||||
lambda_min,
|
||||
lambda_max
|
||||
);
|
||||
|
||||
// For larger indices (where floor effects are minimal), verify approximate ratio
|
||||
// Skip first 10 indices where floor causes large variations
|
||||
if i >= 10 {
|
||||
let ratio = lambda_max as f64 / lambda_min.max(1) as f64;
|
||||
assert!(
|
||||
ratio >= 1.0 && ratio <= 1.5,
|
||||
"Ratio {} should be close to 1.2: {}",
|
||||
i,
|
||||
ratio
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_geometric_range_coverage() {
|
||||
// Verify that ranges cover all positive integers without large gaps
|
||||
let base: f64 = 1.2;
|
||||
let mut ranges = Vec::new();
|
||||
|
||||
for i in 0..30 {
|
||||
let lambda_min = (base.powi(i)).floor() as u64;
|
||||
let lambda_max = (base.powi(i + 1)).floor() as u64;
|
||||
ranges.push((lambda_min, lambda_max));
|
||||
}
|
||||
|
||||
// Check for gaps between consecutive ranges
|
||||
for i in 1..ranges.len() {
|
||||
let prev_max = ranges[i - 1].1;
|
||||
let curr_min = ranges[i].0;
|
||||
|
||||
// Gap should be small (at most 1-2 due to floor operations)
|
||||
let gap = curr_min.saturating_sub(prev_max);
|
||||
assert!(
|
||||
gap <= 2,
|
||||
"Gap between ranges too large: {} at index {}",
|
||||
gap,
|
||||
i
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_geometric_range_bounds() {
|
||||
// Test specific range boundaries match expected values
|
||||
let base: f64 = 1.2;
|
||||
|
||||
let test_cases = vec![
|
||||
(0, 1, 1), // 1.2^0 = 1, 1.2^1 = 1.2 → [1, 1]
|
||||
(1, 1, 1), // 1.2^1 = 1.2, 1.2^2 = 1.44 → [1, 1]
|
||||
(5, 2, 2), // 1.2^5 ≈ 2.49, 1.2^6 ≈ 2.99 → [2, 2]
|
||||
(10, 6, 7), // 1.2^10 ≈ 6.19, 1.2^11 ≈ 7.43 → [6, 7]
|
||||
(20, 38, 46), // 1.2^20 ≈ 38.34, 1.2^21 ≈ 46.01 → [38, 46]
|
||||
];
|
||||
|
||||
for (i, expected_min, expected_max) in test_cases {
|
||||
let lambda_min = (base.powi(i)).floor() as u64;
|
||||
let lambda_max = (base.powi(i + 1)).floor() as u64;
|
||||
|
||||
assert_eq!(
|
||||
lambda_min, expected_min,
|
||||
"Range {} min should be {}",
|
||||
i, expected_min
|
||||
);
|
||||
assert_eq!(
|
||||
lambda_max, expected_max,
|
||||
"Range {} max should be {}",
|
||||
i, expected_max
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Disconnected Graph Tests
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn test_disconnected_returns_zero() {
|
||||
// Create graph with two separate components
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
|
||||
// Component 1: edge (1,2)
|
||||
graph.insert_edge(1, 2, 5.0).unwrap();
|
||||
|
||||
// Component 2: edge (3,4)
|
||||
graph.insert_edge(3, 4, 3.0).unwrap();
|
||||
|
||||
// Disconnected graph should have min cut = 0
|
||||
assert!(!graph.is_connected(), "Graph should be disconnected");
|
||||
|
||||
let mincut = MinCutBuilder::new().exact().build().unwrap();
|
||||
|
||||
// Build from edges
|
||||
let mut mincut_dynamic = MinCutBuilder::new()
|
||||
.with_edges(vec![(1, 2, 5.0), (3, 4, 3.0)])
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
mincut_dynamic.min_cut_value(),
|
||||
0.0,
|
||||
"Disconnected graph must have min cut = 0"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_disconnected_multiple_components() {
|
||||
// Three separate components
|
||||
let edges = vec![
|
||||
(1, 2, 1.0),
|
||||
(2, 3, 1.0), // Component 1
|
||||
(10, 11, 2.0),
|
||||
(11, 12, 2.0), // Component 2
|
||||
(20, 21, 3.0), // Component 3
|
||||
];
|
||||
|
||||
let mincut = MinCutBuilder::new().with_edges(edges).build().unwrap();
|
||||
|
||||
assert_eq!(
|
||||
mincut.min_cut_value(),
|
||||
0.0,
|
||||
"Multiple disconnected components must have min cut = 0"
|
||||
);
|
||||
assert!(!mincut.is_connected());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_becomes_disconnected_after_delete() {
|
||||
// Start with connected graph, delete bridge edge
|
||||
let mut mincut = MinCutBuilder::new()
|
||||
.with_edges(vec![
|
||||
(1, 2, 1.0),
|
||||
(2, 3, 1.0), // Bridge edge
|
||||
(3, 4, 1.0),
|
||||
])
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
assert!(mincut.is_connected());
|
||||
|
||||
// Delete the bridge
|
||||
let new_cut = mincut.delete_edge(2, 3).unwrap();
|
||||
|
||||
assert_eq!(new_cut, 0.0, "Should become disconnected");
|
||||
assert!(!mincut.is_connected());
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Connected Graph Correctness Tests
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn test_single_edge_min_cut() {
|
||||
// Two vertices, one edge
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
graph.insert_edge(1, 2, 3.5).unwrap();
|
||||
|
||||
let mincut = MinCutBuilder::new()
|
||||
.with_edges(vec![(1, 2, 3.5)])
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
mincut.min_cut_value(),
|
||||
3.5,
|
||||
"Single edge min cut should equal edge weight"
|
||||
);
|
||||
assert!(mincut.is_connected());
|
||||
|
||||
// Verify against brute force
|
||||
let brute_force = stoer_wagner_min_cut(&graph);
|
||||
assert_eq!(
|
||||
mincut.min_cut_value(),
|
||||
brute_force,
|
||||
"Should match brute force: {} vs {}",
|
||||
mincut.min_cut_value(),
|
||||
brute_force
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_path_graph_min_cut() {
|
||||
// Path graph P_n has min cut = 1 (any single edge)
|
||||
for n in 3..10 {
|
||||
let graph = build_path_graph(n);
|
||||
|
||||
let mincut = MinCutBuilder::new().exact().build().unwrap();
|
||||
|
||||
// Build from graph
|
||||
let mut edges = Vec::new();
|
||||
for i in 1..n {
|
||||
edges.push((i as u64, (i + 1) as u64, 1.0));
|
||||
}
|
||||
|
||||
let mincut = MinCutBuilder::new().with_edges(edges).build().unwrap();
|
||||
|
||||
assert_eq!(
|
||||
mincut.min_cut_value(),
|
||||
1.0,
|
||||
"Path graph P_{} should have min cut = 1",
|
||||
n
|
||||
);
|
||||
|
||||
// Verify against brute force
|
||||
let brute_force = stoer_wagner_min_cut(&graph);
|
||||
assert_eq!(
|
||||
mincut.min_cut_value(),
|
||||
brute_force,
|
||||
"P_{} should match brute force",
|
||||
n
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cycle_graph_min_cut() {
|
||||
// Cycle C_n has min cut = 2 (two edges needed to disconnect)
|
||||
for n in 3..10 {
|
||||
let graph = build_cycle_graph(n);
|
||||
|
||||
let mut edges = Vec::new();
|
||||
for i in 1..=n {
|
||||
let next = if i == n { 1 } else { i + 1 };
|
||||
edges.push((i as u64, next as u64, 1.0));
|
||||
}
|
||||
|
||||
let mincut = MinCutBuilder::new().with_edges(edges).build().unwrap();
|
||||
|
||||
assert_eq!(
|
||||
mincut.min_cut_value(),
|
||||
2.0,
|
||||
"Cycle C_{} should have min cut = 2",
|
||||
n
|
||||
);
|
||||
|
||||
// Verify against brute force
|
||||
let brute_force = stoer_wagner_min_cut(&graph);
|
||||
assert_eq!(
|
||||
mincut.min_cut_value(),
|
||||
brute_force,
|
||||
"C_{} should match brute force",
|
||||
n
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_complete_graph_min_cut() {
|
||||
// Complete graph K_n has min cut = n-1 (degree of any vertex)
|
||||
for n in 3..=6 {
|
||||
let graph = build_complete_graph(n);
|
||||
|
||||
let mut edges = Vec::new();
|
||||
for i in 1..=n {
|
||||
for j in (i + 1)..=n {
|
||||
edges.push((i as u64, j as u64, 1.0));
|
||||
}
|
||||
}
|
||||
|
||||
let mincut = MinCutBuilder::new().with_edges(edges).build().unwrap();
|
||||
|
||||
let expected = (n - 1) as f64;
|
||||
assert_eq!(
|
||||
mincut.min_cut_value(),
|
||||
expected,
|
||||
"Complete graph K_{} should have min cut = {}",
|
||||
n,
|
||||
expected
|
||||
);
|
||||
|
||||
// Verify against brute force
|
||||
let brute_force = stoer_wagner_min_cut(&graph);
|
||||
assert_eq!(
|
||||
mincut.min_cut_value(),
|
||||
brute_force,
|
||||
"K_{} should match brute force",
|
||||
n
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_weighted_graph_correctness() {
|
||||
// Graph with varying edge weights
|
||||
let edges = vec![
|
||||
(1, 2, 5.0),
|
||||
(2, 3, 3.0),
|
||||
(3, 4, 7.0),
|
||||
(4, 1, 2.0),
|
||||
(1, 3, 4.0), // Diagonal
|
||||
];
|
||||
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
for (u, v, w) in &edges {
|
||||
graph.insert_edge(*u, *v, *w).unwrap();
|
||||
}
|
||||
|
||||
let mincut = MinCutBuilder::new().with_edges(edges).build().unwrap();
|
||||
|
||||
let brute_force = stoer_wagner_min_cut(&graph);
|
||||
|
||||
// Should match brute force (within floating point tolerance)
|
||||
assert!(
|
||||
(mincut.min_cut_value() - brute_force).abs() < 0.001,
|
||||
"Weighted graph should match brute force: {} vs {}",
|
||||
mincut.min_cut_value(),
|
||||
brute_force
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Instance Ordering Tests
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn test_insert_before_delete_ordering() {
|
||||
// Verify that in a batch of operations, inserts are processed before deletes
|
||||
let mut mincut = MinCutBuilder::new()
|
||||
.with_edges(vec![(1, 2, 1.0), (2, 3, 1.0)])
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
// Record initial state
|
||||
let initial_edges = mincut.num_edges();
|
||||
assert_eq!(initial_edges, 2);
|
||||
|
||||
// If we could process operations as a batch, inserts would come first
|
||||
// Simulate: insert (3,4), delete (1,2)
|
||||
mincut.insert_edge(3, 4, 1.0).unwrap();
|
||||
assert_eq!(mincut.num_edges(), 3, "Insert should happen first");
|
||||
|
||||
mincut.delete_edge(1, 2).unwrap();
|
||||
assert_eq!(mincut.num_edges(), 2, "Delete should happen after insert");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_operation_sequence_determinism() {
|
||||
// Same sequence of operations should produce same result
|
||||
let operations = vec![
|
||||
("insert", 1, 2, 1.0),
|
||||
("insert", 2, 3, 1.0),
|
||||
("insert", 3, 4, 1.0),
|
||||
("delete", 2, 3, 0.0),
|
||||
("insert", 1, 4, 2.0),
|
||||
];
|
||||
|
||||
// Execute twice
|
||||
for _run in 0..2 {
|
||||
let mut mincut = MinCutBuilder::new().build().unwrap();
|
||||
|
||||
for (op, u, v, w) in &operations {
|
||||
match *op {
|
||||
"insert" => {
|
||||
let _ = mincut.insert_edge(*u, *v, *w);
|
||||
}
|
||||
"delete" => {
|
||||
let _ = mincut.delete_edge(*u, *v);
|
||||
}
|
||||
_ => panic!("Unknown operation"),
|
||||
}
|
||||
}
|
||||
|
||||
// Result should be deterministic across runs
|
||||
let final_edges = mincut.num_edges();
|
||||
|
||||
// After: insert 1-2, insert 2-3, insert 3-4, delete 2-3, insert 1-4
|
||||
// Expected: 3 edges (1-2, 3-4, 1-4)
|
||||
assert!(
|
||||
final_edges >= 2 && final_edges <= 4,
|
||||
"Should have reasonable edge count: {}",
|
||||
final_edges
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Fuzz Tests on Random Small Graphs
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn fuzz_random_small_graphs() {
|
||||
use rand::rngs::StdRng;
|
||||
use rand::{Rng, SeedableRng};
|
||||
|
||||
let mut rng = StdRng::seed_from_u64(42);
|
||||
|
||||
// Reduced iterations to avoid long test times
|
||||
for _iteration in 0..20 {
|
||||
let n = rng.gen_range(3..8); // Smaller graphs
|
||||
let m = rng.gen_range(n..=(n * (n - 1) / 2).min(15));
|
||||
|
||||
let mut edges = Vec::new();
|
||||
let mut edge_set = std::collections::HashSet::new();
|
||||
|
||||
// Generate random edges
|
||||
for _ in 0..m {
|
||||
let mut attempts = 0;
|
||||
loop {
|
||||
let u = rng.gen_range(1..=n) as u64;
|
||||
let v = rng.gen_range(1..=n) as u64;
|
||||
|
||||
if u != v {
|
||||
let edge_key = if u < v { (u, v) } else { (v, u) };
|
||||
if edge_set.insert(edge_key) {
|
||||
let weight = rng.gen_range(1.0..10.0);
|
||||
edges.push((u, v, weight));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
attempts += 1;
|
||||
if attempts > 50 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if edges.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Build mincut structure
|
||||
let mincut = MinCutBuilder::new().with_edges(edges.clone()).build();
|
||||
|
||||
// Verify structure builds without panic
|
||||
if let Ok(mc) = mincut {
|
||||
let value = mc.min_cut_value();
|
||||
// Min cut should be non-negative
|
||||
assert!(value >= 0.0, "Min cut must be non-negative");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fuzz_random_operations_sequence() {
|
||||
use rand::rngs::StdRng;
|
||||
use rand::{Rng, SeedableRng};
|
||||
|
||||
let mut rng = StdRng::seed_from_u64(123);
|
||||
|
||||
// Reduced iterations to avoid timeout
|
||||
for _iteration in 0..10 {
|
||||
let mut mincut = MinCutBuilder::new().build().unwrap();
|
||||
let mut present_edges = std::collections::HashSet::new();
|
||||
|
||||
let num_ops = rng.gen_range(5..15); // Fewer operations
|
||||
|
||||
for _ in 0..num_ops {
|
||||
let op = rng.gen_range(0..2); // 0=insert, 1=delete
|
||||
|
||||
if op == 0 || present_edges.is_empty() {
|
||||
// Insert
|
||||
let u = rng.gen_range(1..6) as u64; // Smaller vertex range
|
||||
let v = rng.gen_range(1..6) as u64;
|
||||
|
||||
if u != v {
|
||||
let key = if u < v { (u, v) } else { (v, u) };
|
||||
if !present_edges.contains(&key) {
|
||||
let weight = rng.gen_range(1.0..5.0);
|
||||
if mincut.insert_edge(u, v, weight).is_ok() {
|
||||
present_edges.insert(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Delete
|
||||
if let Some(&(u, v)) = present_edges.iter().next() {
|
||||
present_edges.remove(&(u, v));
|
||||
let _ = mincut.delete_edge(u, v);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Verify final state is valid
|
||||
let final_cut = mincut.min_cut_value();
|
||||
assert!(final_cut >= 0.0, "Cut value must be non-negative");
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Edge Deletion Correctness Tests
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn test_delete_maintains_correctness() {
|
||||
// Start with dense graph, delete edges one by one
|
||||
let mut edges = Vec::new();
|
||||
for i in 1..=5 {
|
||||
for j in (i + 1)..=5 {
|
||||
edges.push((i, j, 1.0));
|
||||
}
|
||||
}
|
||||
|
||||
let mut mincut = MinCutBuilder::new()
|
||||
.with_edges(edges.clone())
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
// Initial min cut for K_5 should be 4
|
||||
assert_eq!(mincut.min_cut_value(), 4.0);
|
||||
|
||||
// Delete edges one by one and verify correctness
|
||||
for (u, v, _) in edges.iter().take(5) {
|
||||
let _ = mincut.delete_edge(*u, *v);
|
||||
|
||||
let current_cut = mincut.min_cut_value();
|
||||
|
||||
// Cut value should remain valid
|
||||
assert!(current_cut >= 0.0, "Cut must be non-negative");
|
||||
|
||||
if mincut.is_connected() {
|
||||
assert!(
|
||||
current_cut > 0.0 && current_cut < f64::INFINITY,
|
||||
"Connected graph must have finite positive cut"
|
||||
);
|
||||
} else {
|
||||
assert_eq!(current_cut, 0.0, "Disconnected graph must have cut = 0");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_bridge_creates_disconnection() {
|
||||
// Create graph with clear bridge
|
||||
let mut mincut = MinCutBuilder::new()
|
||||
.with_edges(vec![
|
||||
(1, 2, 1.0),
|
||||
(2, 3, 1.0),
|
||||
(3, 1, 1.0), // Triangle
|
||||
(3, 4, 1.0), // Bridge
|
||||
(4, 5, 1.0),
|
||||
(5, 6, 1.0),
|
||||
(6, 4, 1.0), // Another triangle
|
||||
])
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
assert!(mincut.is_connected());
|
||||
assert_eq!(mincut.min_cut_value(), 1.0); // The bridge
|
||||
|
||||
// Delete the bridge
|
||||
mincut.delete_edge(3, 4).unwrap();
|
||||
|
||||
assert!(!mincut.is_connected());
|
||||
assert_eq!(mincut.min_cut_value(), 0.0);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Property-Based Tests
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn property_min_cut_bounded_by_min_degree() {
|
||||
// Property: min cut ≤ minimum vertex degree
|
||||
let test_graphs = vec![
|
||||
vec![(1, 2, 1.0), (2, 3, 1.0), (3, 1, 1.0)],
|
||||
vec![(1, 2, 2.0), (2, 3, 3.0), (3, 4, 1.0), (4, 1, 2.0)],
|
||||
vec![(1, 2, 1.0), (1, 3, 1.0), (1, 4, 1.0), (2, 3, 1.0)],
|
||||
];
|
||||
|
||||
for edges in test_graphs {
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
for (u, v, w) in &edges {
|
||||
graph.insert_edge(*u, *v, *w).unwrap();
|
||||
}
|
||||
|
||||
let mincut = MinCutBuilder::new().with_edges(edges).build().unwrap();
|
||||
|
||||
if mincut.is_connected() {
|
||||
// Find minimum degree
|
||||
let mut min_degree = f64::INFINITY;
|
||||
for &v in &graph.vertices() {
|
||||
let mut degree_weight = 0.0;
|
||||
for (_, edge_id) in graph.neighbors(v) {
|
||||
for e in graph.edges() {
|
||||
if e.id == edge_id {
|
||||
degree_weight += e.weight;
|
||||
}
|
||||
}
|
||||
}
|
||||
min_degree = min_degree.min(degree_weight);
|
||||
}
|
||||
|
||||
assert!(
|
||||
mincut.min_cut_value() <= min_degree + 0.001,
|
||||
"Min cut must be ≤ minimum degree: {} vs {}",
|
||||
mincut.min_cut_value(),
|
||||
min_degree
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn property_min_cut_monotonic_on_edge_removal() {
|
||||
// Property: deleting edges cannot increase min cut
|
||||
let mut mincut = MinCutBuilder::new()
|
||||
.with_edges(vec![
|
||||
(1, 2, 1.0),
|
||||
(2, 3, 1.0),
|
||||
(3, 4, 1.0),
|
||||
(4, 1, 1.0),
|
||||
(1, 3, 2.0), // Diagonal
|
||||
])
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let initial_cut = mincut.min_cut_value();
|
||||
|
||||
// Delete edge
|
||||
mincut.delete_edge(1, 3).unwrap();
|
||||
let after_delete = mincut.min_cut_value();
|
||||
|
||||
assert!(
|
||||
after_delete <= initial_cut,
|
||||
"Deleting edges cannot increase min cut: {} -> {}",
|
||||
initial_cut,
|
||||
after_delete
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn property_symmetry() {
|
||||
// Property: graph (u,v,w) has same min cut as (v,u,w)
|
||||
let edges_forward = vec![(1, 2, 1.5), (2, 3, 2.5), (3, 1, 1.0)];
|
||||
|
||||
let edges_reverse: Vec<_> = edges_forward.iter().map(|(u, v, w)| (*v, *u, *w)).collect();
|
||||
|
||||
let mincut_fwd = MinCutBuilder::new()
|
||||
.with_edges(edges_forward)
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let mincut_rev = MinCutBuilder::new()
|
||||
.with_edges(edges_reverse)
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
mincut_fwd.min_cut_value(),
|
||||
mincut_rev.min_cut_value(),
|
||||
"Graph should have same min cut regardless of edge direction"
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user