use super::*;
use crate::canonical::FixedWeight;
use crate::canonical::source_anchored::{
canonical_mincut, SourceAnchoredConfig,
};
use crate::graph::DynamicGraph;
fn make_dynamic(
edges: &[(u64, u64, f64)],
) -> DynamicMinCut {
let edge_vec: Vec<(u64, u64, f64)> = edges.to_vec();
DynamicMinCut::with_edges(edge_vec, DynamicMinCutConfig::default()).unwrap()
}
fn make_dynamic_with_threshold(
edges: &[(u64, u64, f64)],
threshold: u64,
) -> DynamicMinCut {
let edge_vec: Vec<(u64, u64, f64)> = edges.to_vec();
let config = DynamicMinCutConfig {
canonical_config: SourceAnchoredConfig::default(),
staleness_threshold: threshold,
};
DynamicMinCut::with_edges(edge_vec, config).unwrap()
}
fn make_graph(edges: &[(u64, u64, f64)]) -> DynamicGraph {
let g = DynamicGraph::new();
for &(u, v, w) in edges {
g.insert_edge(u, v, w).unwrap();
}
g
}
#[test]
fn test_dynamic_basic_computation() {
let mut dmc = make_dynamic(&[
(0, 1, 1.0), (1, 2, 1.0), (2, 0, 1.0),
]);
let cut = dmc.canonical_cut().unwrap();
assert_eq!(cut.lambda, FixedWeight::from_f64(2.0));
assert_eq!(cut.source_vertex, 0);
}
#[test]
fn test_dynamic_empty_graph() {
let mut dmc = DynamicMinCut::new();
assert!(dmc.canonical_cut().is_none());
}
#[test]
fn test_dynamic_default() {
let dmc = DynamicMinCut::default();
assert_eq!(dmc.epoch(), 0);
assert_eq!(dmc.last_full_epoch(), 0);
assert_eq!(dmc.incremental_count(), 0);
}
#[test]
fn test_add_edge_same_side_no_recompute() {
let mut dmc = make_dynamic(&[
(0, 1, 1.0), (1, 2, 1.0), (2, 0, 1.0),
]);
let cut1 = dmc.canonical_cut().unwrap();
let lambda1 = cut1.lambda;
let hash1 = cut1.cut_hash;
let mut dmc2 = make_dynamic(&[
(0, 1, 1.0), (1, 2, 1.0), (2, 3, 1.0),
]);
let cut2 = dmc2.canonical_cut().unwrap();
assert_eq!(cut2.lambda, FixedWeight::from_f64(1.0));
}
#[test]
fn test_add_edge_crosses_cut_triggers_recompute() {
let mut dmc = make_dynamic(&[
(0, 1, 5.0), (1, 2, 5.0), (2, 0, 5.0),
(3, 4, 5.0), (4, 5, 5.0), (5, 3, 5.0),
(2, 3, 1.0),
]);
let cut1 = dmc.canonical_cut().unwrap();
assert_eq!(cut1.lambda, FixedWeight::from_f64(1.0));
dmc.add_edge(0, 5, 3.0).unwrap();
assert!(dmc.is_stale());
let cut2 = dmc.canonical_cut().unwrap();
assert!(cut2.lambda.to_f64() > 1.0);
}
#[test]
fn test_remove_edge_not_in_cut() {
let mut dmc = make_dynamic(&[
(0, 1, 5.0), (1, 2, 5.0), (2, 0, 5.0),
(3, 4, 5.0), (4, 5, 5.0), (5, 3, 5.0),
(2, 3, 1.0),
]);
let cut1 = dmc.canonical_cut().unwrap();
assert_eq!(cut1.lambda, FixedWeight::from_f64(1.0));
dmc.remove_edge(0, 1).unwrap();
assert!(!dmc.is_stale());
}
#[test]
fn test_remove_edge_in_cut_triggers_recompute() {
let mut dmc = make_dynamic(&[
(0, 1, 5.0), (1, 2, 5.0), (2, 0, 5.0),
(3, 4, 5.0), (4, 5, 5.0), (5, 3, 5.0),
(2, 3, 1.0),
]);
let cut1 = dmc.canonical_cut().unwrap();
assert_eq!(cut1.lambda, FixedWeight::from_f64(1.0));
dmc.remove_edge(2, 3).unwrap();
assert!(dmc.is_stale());
let cut2 = dmc.canonical_cut();
assert!(cut2.is_none()); }
#[test]
fn test_batch_updates_match_sequential() {
let base_edges = vec![
(0, 1, 1.0), (1, 2, 1.0), (2, 0, 1.0),
];
let mut dmc_seq = make_dynamic(&base_edges);
dmc_seq.canonical_cut(); dmc_seq.add_edge(0, 3, 2.0).unwrap();
dmc_seq.add_edge(1, 3, 3.0).unwrap();
let cut_seq = dmc_seq.canonical_cut();
let mut dmc_batch = make_dynamic(&base_edges);
dmc_batch.canonical_cut(); dmc_batch.apply_batch(&[
EdgeMutation::Add(0, 3, 2.0),
EdgeMutation::Add(1, 3, 3.0),
]).unwrap();
let cut_batch = dmc_batch.canonical_cut();
match (cut_seq, cut_batch) {
(Some(s), Some(b)) => {
assert_eq!(s.lambda, b.lambda);
assert_eq!(s.cut_hash, b.cut_hash);
}
(None, None) => {}
_ => panic!("Sequential and batch should agree"),
}
}
#[test]
fn test_batch_add_and_remove() {
let mut dmc = make_dynamic(&[
(0, 1, 1.0), (1, 2, 1.0), (2, 0, 1.0),
(2, 3, 1.0), (3, 4, 1.0), (4, 2, 1.0),
]);
dmc.canonical_cut();
dmc.apply_batch(&[
EdgeMutation::Add(0, 4, 2.0),
EdgeMutation::Remove(2, 3),
]).unwrap();
let cut = dmc.canonical_cut();
assert!(cut.is_some());
}
#[test]
fn test_epoch_increments() {
let mut dmc = make_dynamic(&[
(0, 1, 1.0), (1, 2, 1.0), (2, 0, 1.0),
]);
assert_eq!(dmc.epoch(), 0);
dmc.add_edge(3, 0, 1.0).unwrap();
assert_eq!(dmc.epoch(), 1);
dmc.add_edge(3, 1, 1.0).unwrap();
assert_eq!(dmc.epoch(), 2);
dmc.remove_edge(3, 0).unwrap();
assert_eq!(dmc.epoch(), 3);
}
#[test]
fn test_last_full_epoch_updates_on_recompute() {
let mut dmc = make_dynamic(&[
(0, 1, 1.0), (1, 2, 1.0), (2, 0, 1.0),
]);
assert_eq!(dmc.last_full_epoch(), 0);
dmc.canonical_cut(); assert_eq!(dmc.last_full_epoch(), 0);
dmc.add_edge(3, 0, 1.0).unwrap(); dmc.add_edge(3, 1, 1.0).unwrap();
dmc.force_recompute();
assert_eq!(dmc.last_full_epoch(), 2);
}
#[test]
fn test_staleness_triggers_recompute() {
let mut dmc = make_dynamic_with_threshold(
&[(0, 1, 1.0), (1, 2, 1.0), (2, 0, 1.0)],
3,
);
dmc.canonical_cut(); assert_eq!(dmc.incremental_count(), 0);
dmc.add_edge(3, 4, 1.0).unwrap();
let mut dmc2 = make_dynamic_with_threshold(
&[
(0, 1, 5.0), (1, 2, 5.0), (2, 0, 5.0),
(3, 4, 5.0), (4, 5, 5.0), (5, 3, 5.0),
(2, 3, 1.0),
],
3,
);
dmc2.canonical_cut();
dmc2.remove_edge(0, 1).unwrap(); assert_eq!(dmc2.incremental_count(), 1);
dmc2.remove_edge(3, 4).unwrap(); assert_eq!(dmc2.incremental_count(), 2);
dmc2.remove_edge(4, 5).unwrap();
let cut = dmc2.canonical_cut();
assert_eq!(dmc2.incremental_count(), 0);
assert_eq!(dmc2.last_full_epoch(), dmc2.epoch());
}
#[test]
fn test_staleness_disabled_when_zero() {
let mut dmc = make_dynamic_with_threshold(
&[(0, 1, 1.0), (1, 2, 1.0), (2, 0, 1.0)],
0, );
dmc.canonical_cut();
for i in 3..20 {
let _ = dmc.add_edge(i, 0, 1.0);
}
let cut = dmc.canonical_cut();
assert!(cut.is_some());
}
#[test]
fn test_dynamic_determinism_100_runs() {
let edges = vec![
(0, 1, 3.0), (1, 2, 2.0), (2, 3, 4.0),
(3, 0, 1.0), (0, 2, 5.0), (1, 3, 2.0),
];
let mut first_hash = None;
for _ in 0..100 {
let mut dmc = make_dynamic(&edges);
let cut = dmc.canonical_cut().unwrap();
match first_hash {
None => first_hash = Some(cut.cut_hash),
Some(h) => assert_eq!(h, cut.cut_hash, "Dynamic must be deterministic"),
}
}
}
#[test]
fn test_dynamic_single_edge() {
let mut dmc = make_dynamic(&[(0, 1, 1.0)]);
let cut = dmc.canonical_cut().unwrap();
assert_eq!(cut.lambda, FixedWeight::from_f64(1.0));
}
#[test]
fn test_dynamic_add_then_remove_restores_cut_value() {
let mut dmc = make_dynamic(&[
(0, 1, 1.0), (1, 2, 1.0), (2, 3, 1.0), (3, 0, 1.0),
]);
let cut1 = dmc.canonical_cut().unwrap();
let lambda1 = cut1.lambda;
dmc.add_edge(0, 2, 1.0).unwrap();
dmc.remove_edge(0, 2).unwrap();
let cut2 = dmc.canonical_cut().unwrap();
assert_eq!(lambda1, cut2.lambda);
assert_eq!(cut1.cut_hash, cut2.cut_hash);
}
#[test]
fn test_dynamic_receipt() {
let mut dmc = make_dynamic(&[
(0, 1, 1.0), (1, 2, 1.0), (2, 0, 1.0),
]);
let receipt = dmc.receipt().unwrap();
assert_eq!(receipt.epoch, 0);
assert_eq!(receipt.lambda, FixedWeight::from_f64(2.0));
}
#[test]
fn test_dynamic_matches_full_recompute_after_additions() {
let base_edges = vec![
(0, 1, 2.0), (1, 2, 3.0), (2, 3, 1.0),
(3, 0, 4.0), (0, 2, 1.0),
];
let mut dmc = make_dynamic(&base_edges);
dmc.canonical_cut();
dmc.add_edge(10, 0, 2.0).unwrap();
dmc.add_edge(11, 1, 3.0).unwrap();
dmc.force_recompute();
let dynamic_cut = dmc.canonical_cut();
let mut all_edges = base_edges.clone();
all_edges.push((10, 0, 2.0));
all_edges.push((11, 1, 3.0));
let g = make_graph(&all_edges);
let fresh_cut = canonical_mincut(&g, &SourceAnchoredConfig::default());
match (dynamic_cut, fresh_cut) {
(Some(d), Some(f)) => {
assert_eq!(d.lambda, f.lambda);
assert_eq!(d.cut_hash, f.cut_hash);
}
(None, None) => {}
_ => panic!("Dynamic and fresh should agree"),
}
}
#[test]
fn test_dynamic_matches_full_recompute_after_deletions() {
let base_edges = vec![
(0, 1, 2.0), (1, 2, 3.0), (2, 3, 1.0),
(3, 0, 4.0), (0, 2, 1.0), (1, 3, 2.0),
];
let mut dmc = make_dynamic(&base_edges);
dmc.canonical_cut();
dmc.remove_edge(0, 2).unwrap();
dmc.remove_edge(1, 3).unwrap();
dmc.force_recompute();
let dynamic_cut = dmc.cached_cut.clone();
let remaining_edges = vec![
(0, 1, 2.0), (1, 2, 3.0), (2, 3, 1.0), (3, 0, 4.0),
];
let g = make_graph(&remaining_edges);
let fresh_cut = canonical_mincut(&g, &SourceAnchoredConfig::default());
match (dynamic_cut, fresh_cut) {
(Some(d), Some(f)) => {
assert_eq!(d.lambda, f.lambda);
assert_eq!(d.cut_hash, f.cut_hash);
}
(None, None) => {}
_ => panic!("Dynamic and fresh should agree"),
}
}