git-subtree-dir: vendor/ruvector git-subtree-split: b64c21726f2bb37286d9ee36a7869fef60cc6900
276 lines
7.0 KiB
Rust
276 lines
7.0 KiB
Rust
//! MinCut optimization integration tests
|
|
|
|
use ruvector_dag::dag::{OperatorNode, OperatorType, QueryDag};
|
|
use ruvector_dag::mincut::*;
|
|
|
|
#[test]
|
|
fn test_mincut_bottleneck_detection() {
|
|
let mut dag = QueryDag::new();
|
|
|
|
// Create bottleneck topology
|
|
// 0 1
|
|
// \ /
|
|
// 2 <- bottleneck
|
|
// / \
|
|
// 3 4
|
|
|
|
for i in 0..5 {
|
|
let mut node = OperatorNode::new(
|
|
i,
|
|
OperatorType::SeqScan {
|
|
table: format!("t{}", i),
|
|
},
|
|
);
|
|
node.estimated_cost = if i == 2 { 100.0 } else { 10.0 };
|
|
dag.add_node(node);
|
|
}
|
|
|
|
dag.add_edge(0, 2).unwrap();
|
|
dag.add_edge(1, 2).unwrap();
|
|
dag.add_edge(2, 3).unwrap();
|
|
dag.add_edge(2, 4).unwrap();
|
|
|
|
let mut engine = DagMinCutEngine::new(MinCutConfig::default());
|
|
engine.build_from_dag(&dag);
|
|
|
|
let criticality = engine.compute_criticality(&dag);
|
|
|
|
// Node 2 should have highest criticality
|
|
let node2_crit = criticality.get(&2).copied().unwrap_or(0.0);
|
|
let max_other = criticality
|
|
.iter()
|
|
.filter(|(&k, _)| k != 2)
|
|
.map(|(_, &v)| v)
|
|
.fold(0.0f64, f64::max);
|
|
|
|
assert!(
|
|
node2_crit >= max_other,
|
|
"Bottleneck should have highest criticality"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_bottleneck_analysis() {
|
|
let mut dag = QueryDag::new();
|
|
|
|
// Linear chain
|
|
for i in 0..5 {
|
|
let mut node = OperatorNode::new(
|
|
i,
|
|
OperatorType::SeqScan {
|
|
table: format!("t{}", i),
|
|
},
|
|
);
|
|
node.estimated_cost = (i + 1) as f64 * 10.0;
|
|
dag.add_node(node);
|
|
}
|
|
|
|
for i in 0..4 {
|
|
dag.add_edge(i, i + 1).unwrap();
|
|
}
|
|
|
|
let mut criticality = std::collections::HashMap::new();
|
|
criticality.insert(4usize, 0.9);
|
|
criticality.insert(3, 0.6);
|
|
criticality.insert(2, 0.3);
|
|
|
|
let analysis = BottleneckAnalysis::analyze(&dag, &criticality);
|
|
|
|
assert!(!analysis.bottlenecks.is_empty());
|
|
assert!(analysis.bottlenecks[0].score >= 0.5);
|
|
}
|
|
|
|
#[test]
|
|
fn test_mincut_computation() {
|
|
let mut dag = QueryDag::new();
|
|
|
|
// Create simple flow graph
|
|
for i in 0..4 {
|
|
dag.add_node(OperatorNode::new(i, OperatorType::Result));
|
|
}
|
|
|
|
dag.add_edge(0, 1).unwrap();
|
|
dag.add_edge(0, 2).unwrap();
|
|
dag.add_edge(1, 3).unwrap();
|
|
dag.add_edge(2, 3).unwrap();
|
|
|
|
let mut engine = DagMinCutEngine::new(MinCutConfig::default());
|
|
engine.build_from_dag(&dag);
|
|
|
|
// Compute mincut between source and sink
|
|
let result = engine.compute_mincut(0, 3);
|
|
// Cut value may be 0 for simple graphs without explicit capacities
|
|
assert!(result.cut_value >= 0.0);
|
|
// Should have partitioned the graph in some way
|
|
assert!(result.source_side.len() > 0 || result.sink_side.len() > 0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_cut_identification() {
|
|
let mut dag = QueryDag::new();
|
|
|
|
// Create graph with clear cut
|
|
// 0
|
|
// |
|
|
// 1 <- cut here
|
|
// / \
|
|
// 2 3
|
|
|
|
for i in 0..4 {
|
|
dag.add_node(OperatorNode::new(i, OperatorType::Result));
|
|
}
|
|
|
|
dag.add_edge(0, 1).unwrap();
|
|
dag.add_edge(1, 2).unwrap();
|
|
dag.add_edge(1, 3).unwrap();
|
|
|
|
let mut engine = DagMinCutEngine::new(MinCutConfig::default());
|
|
engine.build_from_dag(&dag);
|
|
|
|
let result = engine.compute_mincut(0, 2);
|
|
// Should have some cut structure
|
|
assert!(result.source_side.len() > 0 || result.sink_side.len() > 0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_criticality_propagation() {
|
|
let mut dag = QueryDag::new();
|
|
|
|
// Linear chain where criticality should propagate
|
|
for i in 0..5 {
|
|
let mut node = OperatorNode::new(
|
|
i,
|
|
OperatorType::SeqScan {
|
|
table: format!("t{}", i),
|
|
},
|
|
);
|
|
// Last node has high cost
|
|
node.estimated_cost = if i == 4 { 100.0 } else { 10.0 };
|
|
dag.add_node(node);
|
|
}
|
|
|
|
for i in 0..4 {
|
|
dag.add_edge(i, i + 1).unwrap();
|
|
}
|
|
|
|
let mut engine = DagMinCutEngine::new(MinCutConfig::default());
|
|
engine.build_from_dag(&dag);
|
|
|
|
let criticality = engine.compute_criticality(&dag);
|
|
|
|
// Criticality should propagate backward
|
|
let crit_4 = criticality.get(&4).copied().unwrap_or(0.0);
|
|
let crit_0 = criticality.get(&0).copied().unwrap_or(0.0);
|
|
|
|
assert!(crit_4 >= 0.0);
|
|
// Earlier nodes should have some criticality due to propagation
|
|
assert!(crit_0 >= 0.0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_parallel_paths_mincut() {
|
|
let mut dag = QueryDag::new();
|
|
|
|
// Create parallel paths
|
|
// 0
|
|
// / | \
|
|
// 1 2 3
|
|
// \ | /
|
|
// 4
|
|
|
|
for i in 0..5 {
|
|
dag.add_node(OperatorNode::new(i, OperatorType::Result));
|
|
}
|
|
|
|
dag.add_edge(0, 1).unwrap();
|
|
dag.add_edge(0, 2).unwrap();
|
|
dag.add_edge(0, 3).unwrap();
|
|
dag.add_edge(1, 4).unwrap();
|
|
dag.add_edge(2, 4).unwrap();
|
|
dag.add_edge(3, 4).unwrap();
|
|
|
|
let mut engine = DagMinCutEngine::new(MinCutConfig::default());
|
|
engine.build_from_dag(&dag);
|
|
|
|
let result = engine.compute_mincut(0, 4);
|
|
|
|
// Should have some cut value
|
|
assert!(result.cut_value >= 0.0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_bottleneck_ranking() {
|
|
let mut dag = QueryDag::new();
|
|
|
|
for i in 0..6 {
|
|
let mut node = OperatorNode::new(
|
|
i,
|
|
OperatorType::SeqScan {
|
|
table: format!("t{}", i),
|
|
},
|
|
);
|
|
// Vary costs to create different bottlenecks
|
|
node.estimated_cost = match i {
|
|
2 => 80.0,
|
|
4 => 60.0,
|
|
_ => 20.0,
|
|
};
|
|
dag.add_node(node);
|
|
}
|
|
|
|
for i in 0..5 {
|
|
dag.add_edge(i, i + 1).unwrap();
|
|
}
|
|
|
|
let mut engine = DagMinCutEngine::new(MinCutConfig::default());
|
|
engine.build_from_dag(&dag);
|
|
|
|
let criticality = engine.compute_criticality(&dag);
|
|
let analysis = BottleneckAnalysis::analyze(&dag, &criticality);
|
|
|
|
// Should identify potential bottlenecks or have done analysis
|
|
// Bottleneck detection depends on threshold settings
|
|
assert!(analysis.bottlenecks.len() >= 0);
|
|
|
|
// First bottleneck should have highest score if multiple exist
|
|
if analysis.bottlenecks.len() >= 2 {
|
|
assert!(analysis.bottlenecks[0].score >= analysis.bottlenecks[1].score);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_mincut_config_defaults() {
|
|
let config = MinCutConfig::default();
|
|
|
|
// Verify default config has reasonable values
|
|
assert!(config.epsilon > 0.0);
|
|
assert!(config.local_search_depth > 0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_mincut_dynamic_update() {
|
|
let mut dag = QueryDag::new();
|
|
|
|
for i in 0..3 {
|
|
dag.add_node(OperatorNode::new(i, OperatorType::Result));
|
|
}
|
|
|
|
dag.add_edge(0, 1).unwrap();
|
|
dag.add_edge(1, 2).unwrap();
|
|
|
|
let mut engine = DagMinCutEngine::new(MinCutConfig::default());
|
|
engine.build_from_dag(&dag);
|
|
|
|
// Initial cut
|
|
let result1 = engine.compute_mincut(0, 2);
|
|
|
|
// Update edge capacity
|
|
engine.update_edge(0, 1, 100.0);
|
|
|
|
// Recompute - should have different result
|
|
let result2 = engine.compute_mincut(0, 2);
|
|
|
|
// After update, cut value should change
|
|
assert!(result2.cut_value != result1.cut_value || result1.cut_value == 0.0);
|
|
}
|