git-subtree-dir: vendor/ruvector git-subtree-split: b64c21726f2bb37286d9ee36a7869fef60cc6900
313 lines
8.5 KiB
Rust
313 lines
8.5 KiB
Rust
//! 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
|
|
}
|
|
}
|
|
}
|