Files
wifi-densepose/crates/ruvector-mincut/tests/wrapper_tests.rs
ruv d803bfe2b1 Squashed 'vendor/ruvector/' content from commit b64c2172
git-subtree-dir: vendor/ruvector
git-subtree-split: b64c21726f2bb37286d9ee36a7869fef60cc6900
2026-02-28 14:39:40 -05:00

749 lines
21 KiB
Rust

//! 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"
);
}