Squashed 'vendor/ruvector/' content from commit b64c2172
git-subtree-dir: vendor/ruvector git-subtree-split: b64c21726f2bb37286d9ee36a7869fef60cc6900
This commit is contained in:
748
crates/ruvector-mincut/tests/wrapper_tests.rs
Normal file
748
crates/ruvector-mincut/tests/wrapper_tests.rs
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