use converge_optimization::assignment::{AssignmentProblem, hungarian};
use converge_optimization::graph::dijkstra;
use converge_optimization::graph::flow::{
FlowNetwork, MinCostFlowProblem, max_flow, min_cost_flow,
};
use converge_optimization::graph::matching::bipartite_matching;
use converge_optimization::knapsack::{self, KnapsackProblem};
use converge_optimization::scheduling::{Interval, SchedulingProblem, list_schedule};
use converge_optimization::setcover::{self, SetCoverProblem};
#[test]
fn hungarian_3x3_textbook() {
let problem = AssignmentProblem::from_costs(vec![vec![9, 2, 7], vec![6, 4, 3], vec![5, 8, 1]]);
let solution = hungarian::solve(&problem).unwrap();
assert_eq!(solution.total_cost, 9, "optimal cost for classic 3×3 is 9");
}
#[test]
fn hungarian_4x4_wikipedia() {
let problem = AssignmentProblem::from_costs(vec![
vec![82, 83, 69, 92],
vec![77, 37, 49, 92],
vec![11, 69, 5, 86],
vec![8, 9, 98, 23],
]);
let solution = hungarian::solve(&problem).unwrap();
assert_eq!(
solution.total_cost, 140,
"Wikipedia Hungarian example optimal = 140"
);
}
#[test]
fn hopcroft_karp_complete_bipartite_k3_3() {
let edges: Vec<(usize, usize)> = (0..3).flat_map(|l| (0..3).map(move |r| (l, r))).collect();
let matching = bipartite_matching(3, 3, &edges).unwrap();
assert_eq!(matching.size, 3, "K₃,₃ has perfect matching of size 3");
}
#[test]
fn hopcroft_karp_augmenting_path_required() {
let edges = vec![(0, 0), (0, 1), (1, 0)];
let matching = bipartite_matching(2, 2, &edges).unwrap();
assert_eq!(
matching.size, 2,
"augmenting path yields maximum matching of 2"
);
}
#[test]
fn hopcroft_karp_konigs_theorem_verification() {
let edges = vec![(0, 0), (0, 1), (0, 2), (0, 3), (1, 4)];
let matching = bipartite_matching(2, 5, &edges).unwrap();
assert_eq!(
matching.size, 2,
"star graph: max matching = min vertex cover = 2"
);
}
#[test]
fn knapsack_textbook_5_items() {
let problem = KnapsackProblem::new(vec![2, 3, 4, 5, 9], vec![3, 4, 5, 8, 10], 20).unwrap();
let solution = knapsack::solve(&problem).unwrap();
assert_eq!(
solution.total_value, 26,
"CLRS-variant 5-item knapsack optimal = 26"
);
assert!(solution.total_weight <= 20, "must fit in capacity");
}
#[test]
fn knapsack_rosetta_code_example() {
let problem = KnapsackProblem::new(
vec![9, 13, 153, 50, 15, 68, 27, 39, 23, 52, 11, 32],
vec![150, 35, 200, 160, 60, 45, 60, 40, 30, 10, 70, 30],
400,
)
.unwrap();
let solution = knapsack::solve(&problem).unwrap();
assert_eq!(
solution.total_value, 835,
"12-item knapsack optimal = 835 (take all except tin and beer)"
);
assert!(solution.total_weight <= 400);
}
#[test]
fn dijkstra_textbook_graph() {
use petgraph::graph::DiGraph;
let mut g = DiGraph::<(), i64>::new();
let n0 = g.add_node(());
let n1 = g.add_node(());
let n2 = g.add_node(());
let n3 = g.add_node(());
g.add_edge(n0, n1, 7);
g.add_edge(n0, n2, 9);
g.add_edge(n1, n3, 10);
g.add_edge(n2, n1, 1);
g.add_edge(n2, n3, 2);
let path = dijkstra::shortest_path(&g, n0, n3, |e| *e)
.unwrap()
.unwrap();
assert_eq!(path.cost, 11, "shortest 0→3 = 11 via 0→2→3");
assert_eq!(path.nodes, vec![n0, n2, n3], "path must traverse 0→2→3");
let distances = dijkstra::dijkstra(&g, n0, |e| *e).unwrap();
assert_eq!(distances[&n1], 7, "0→1 = 7 (direct)");
assert_eq!(distances[&n2], 9, "0→2 = 9 (direct)");
assert_eq!(distances[&n3], 11, "0→3 = 11 (via 2)");
}
#[test]
fn max_flow_textbook_network() {
let mut net = FlowNetwork::new(4);
net.add_edge(0, 1, 10, 0);
net.add_edge(0, 2, 10, 0);
net.add_edge(1, 3, 10, 0);
net.add_edge(2, 3, 10, 0);
net.add_edge(1, 2, 1, 0);
let result = max_flow(&net, 0, 3).unwrap();
assert_eq!(result.max_flow, 20, "parallel paths: max flow = 20");
}
#[test]
fn min_cost_flow_simple_network() {
let mut net = FlowNetwork::new(4);
net.add_edge(0, 1, 3, 1);
net.add_edge(1, 3, 3, 1);
net.add_edge(0, 2, 3, 5);
net.add_edge(2, 3, 3, 5);
let problem = MinCostFlowProblem::source_sink(net, 0, 3, 4).unwrap();
let result = min_cost_flow(&problem).unwrap();
assert_eq!(result.flow, 4, "total flow = demand = 4");
assert_eq!(result.cost, 16, "min cost for 4 units = 16");
}
#[test]
fn set_cover_textbook_instance() {
let problem = SetCoverProblem::new(
5,
vec![
(3, vec![0, 1, 2]),
(2, vec![2, 3]),
(2, vec![3, 4]),
(6, vec![0, 1, 2, 3, 4]),
],
)
.unwrap();
let solution = setcover::solve(&problem).unwrap();
assert!(
solution.total_cost <= 13,
"greedy cost {} must be within O(ln n) of optimal 5",
solution.total_cost
);
assert_eq!(
solution.total_cost, 5,
"greedy finds optimal for this instance"
);
}
#[test]
fn disjunctive_scheduling_hand_verifiable() {
let intervals = vec![
Interval::new(0, 0, 10, 3),
Interval::new(1, 0, 10, 2),
Interval::new(2, 0, 10, 4),
];
let problem = SchedulingProblem::disjunctive(intervals);
let solution = list_schedule(&problem).unwrap();
assert_eq!(
solution.makespan, 9,
"3 sequential tasks: makespan = sum of durations = 9"
);
}
#[test]
fn hungarian_8x8_planted_optimal() {
#[rustfmt::skip]
let costs = vec![
vec![15, 12, 9, 8, 3, 14, 10, 11], vec![11, 7, 13, 12, 8, 6, 15, 2], vec![14, 4, 12, 16, 9, 11, 7, 13], vec![10, 15, 11, 8, 13, 1, 9, 14], vec![ 5, 11, 16, 14, 7, 10, 12, 9], vec![12, 9, 7, 2, 11, 15, 13, 8], vec![ 8, 13, 10, 11, 14, 9, 3, 16], vec![13, 10, 4, 15, 12, 8, 11, 6], ];
let problem = AssignmentProblem::from_costs(costs);
let solution = hungarian::solve(&problem).unwrap();
assert_eq!(solution.total_cost, 24, "8×8 planted optimal = 24");
}
#[test]
fn hopcroft_karp_6x6_sparse() {
let edges = vec![
(0, 0),
(0, 1),
(1, 1),
(1, 2),
(2, 2),
(2, 3),
(3, 3),
(3, 4),
(4, 4),
(4, 5),
(5, 5),
(5, 0),
];
let matching = bipartite_matching(6, 6, &edges).unwrap();
assert_eq!(matching.size, 6, "6×6 cycle structure has perfect matching");
}
#[test]
fn knapsack_15_items_greedy_suboptimal() {
let problem = KnapsackProblem::new(
vec![10, 5, 15, 8, 12, 3, 20, 7, 2, 11, 18, 6, 14, 9, 4],
vec![60, 28, 75, 36, 50, 12, 70, 22, 6, 30, 45, 14, 30, 18, 7],
50,
)
.unwrap();
let solution = knapsack::solve(&problem).unwrap();
assert_eq!(
solution.total_value, 249,
"15-item knapsack: DP beats greedy (249 > 239)"
);
assert!(solution.total_weight <= 50);
}
#[test]
fn dijkstra_10_node_multi_hop() {
use petgraph::graph::DiGraph;
let mut g = DiGraph::<(), i64>::new();
let nodes: Vec<_> = (0..10).map(|_| g.add_node(())).collect();
let edges = [
(0, 1, 4),
(0, 2, 2),
(0, 4, 7),
(2, 5, 1),
(2, 3, 3),
(3, 6, 2),
(3, 7, 1),
(4, 7, 1),
(5, 8, 3),
(6, 8, 1),
(6, 9, 4),
(7, 9, 1),
(8, 9, 2),
];
for (from, to, weight) in edges {
g.add_edge(nodes[from], nodes[to], weight);
}
let distances = dijkstra::dijkstra(&g, nodes[0], |e| *e).unwrap();
assert_eq!(distances[&nodes[1]], 4);
assert_eq!(distances[&nodes[2]], 2);
assert_eq!(distances[&nodes[3]], 5, "0→2→3 = 2+3 = 5");
assert_eq!(distances[&nodes[4]], 7);
assert_eq!(distances[&nodes[5]], 3, "0→2→5 = 2+1 = 3");
assert_eq!(distances[&nodes[6]], 7, "0→2→3→6 = 2+3+2 = 7");
assert_eq!(distances[&nodes[7]], 6, "0→2→3→7 = 2+3+1 = 6");
assert_eq!(distances[&nodes[8]], 6, "0→2→5→8 = 2+1+3 = 6");
assert_eq!(distances[&nodes[9]], 7, "0→2→3→7→9 = 2+3+1+1 = 7");
}
#[test]
fn max_flow_bottleneck_network() {
let mut net = FlowNetwork::new(6);
net.add_edge(0, 1, 10, 0); net.add_edge(0, 2, 10, 0); net.add_edge(1, 3, 4, 0); net.add_edge(1, 4, 7, 0); net.add_edge(2, 3, 5, 0); net.add_edge(2, 4, 3, 0); net.add_edge(3, 5, 6, 0); net.add_edge(4, 5, 8, 0);
let result = max_flow(&net, 0, 5).unwrap();
assert_eq!(
result.max_flow, 14,
"bottleneck at sink layer: max flow = 14"
);
}
#[test]
fn min_cost_flow_three_paths() {
let mut net = FlowNetwork::new(5);
net.add_edge(0, 1, 4, 1); net.add_edge(1, 4, 4, 1); net.add_edge(0, 2, 4, 3); net.add_edge(2, 4, 4, 3); net.add_edge(0, 3, 4, 2); net.add_edge(3, 4, 4, 2);
let problem = MinCostFlowProblem::source_sink(net, 0, 4, 10).unwrap();
let result = min_cost_flow(&problem).unwrap();
assert_eq!(result.flow, 10);
assert_eq!(result.cost, 36, "3-path min cost: 4×2 + 4×4 + 2×6 = 36");
}
#[test]
fn set_cover_10_elements() {
let problem = SetCoverProblem::new(
10,
vec![
(4, vec![0, 1, 2, 3]),
(4, vec![2, 3, 4, 5]),
(4, vec![4, 5, 6, 7]),
(4, vec![6, 7, 8, 9]),
(7, vec![0, 1, 2, 3, 4, 5]),
(12, vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9]),
],
)
.unwrap();
let solution = setcover::solve(&problem).unwrap();
assert!(solution.total_cost <= 12, "greedy should find cost ≤ 12");
}
#[test]
fn scheduling_6_tasks_varying_windows() {
let intervals = vec![
Interval::new(0, 0, 30, 5),
Interval::new(1, 0, 30, 3),
Interval::new(2, 0, 30, 7),
Interval::new(3, 0, 30, 2),
Interval::new(4, 0, 30, 4),
Interval::new(5, 0, 30, 3),
];
let problem = SchedulingProblem::disjunctive(intervals);
let solution = list_schedule(&problem).unwrap();
assert_eq!(
solution.makespan, 24,
"6 tasks: makespan = sum of durations = 24"
);
}