From 3456db6cd8a56ded5ceddfefbc9d4f801c99698f Mon Sep 17 00:00:00 2001 From: Stephen Waits Date: Wed, 13 May 2026 22:36:47 -0600 Subject: [PATCH] test(ant_colony_tsp): pin better_than_so branches and build_tour invariants MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 1 for src/algorithms/ant_colony_tsp.rs. Targets the ~30 remaining mutants after Phase 0 sweeps — mostly arithmetic flips in build_tour (pheromone × heuristic weighting) and the feasibility-comparison logic in better_than_so. Added: - Four branch tests for better_than_so covering the full feasibility cross-product: feasible-vs-infeasible (both orders), two-infeasible (smaller violation wins), and two-feasible under both directions. Plus an equal-objectives test pinning the strict-less-than semantics. - build_tour-is-a-permutation invariant across 20 seeds × 6 start cities. - A strong-heuristic test: with eta favoring the next-city by 1000x and beta=5, build_tour walks the preferred path. Pins the .powf(beta) arithmetic. - Zero-alpha/zero-beta degenerate-case test: uniform random fallback still returns a permutation. --- src/algorithms/ant_colony_tsp.rs | 112 +++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) diff --git a/src/algorithms/ant_colony_tsp.rs b/src/algorithms/ant_colony_tsp.rs index 96cd556..cb747a6 100644 --- a/src/algorithms/ant_colony_tsp.rs +++ b/src/algorithms/ant_colony_tsp.rs @@ -565,4 +565,116 @@ mod tests { ); let _ = opt.run(&DummyMo); } + + // ---- Mutation-test pinned helpers -------------------------------------- + + use crate::core::objective::Direction; + use crate::core::rng::rng_from_seed; + + /// `better_than_so` follows the feasibility-first / objective-second + /// tournament rule. Pin each of the four feasibility-cross-product + /// branches so the `<` and `>` comparisons cannot flip silently. + #[test] + fn better_than_so_feasible_beats_infeasible() { + let mut a = Evaluation::new(vec![10.0]); + a.constraint_violation = 0.0; // feasible + let mut b = Evaluation::new(vec![1.0]); + b.constraint_violation = 1.0; // infeasible + assert!(better_than_so(&a, &b, Direction::Minimize)); + assert!(!better_than_so(&b, &a, Direction::Minimize)); + } + + #[test] + fn better_than_so_two_infeasible_compares_violation() { + let mut a = Evaluation::new(vec![0.0]); + a.constraint_violation = 0.5; + let mut b = Evaluation::new(vec![0.0]); + b.constraint_violation = 1.0; + // a has smaller constraint_violation → "better". + assert!(better_than_so(&a, &b, Direction::Minimize)); + assert!(!better_than_so(&b, &a, Direction::Minimize)); + } + + #[test] + fn better_than_so_two_feasible_compares_objective_under_min() { + let a = Evaluation::new(vec![1.0]); // feasible (default cv=0) + let b = Evaluation::new(vec![2.0]); // feasible + assert!(better_than_so(&a, &b, Direction::Minimize)); + assert!(!better_than_so(&b, &a, Direction::Minimize)); + } + + #[test] + fn better_than_so_two_feasible_compares_objective_under_max() { + let a = Evaluation::new(vec![2.0]); + let b = Evaluation::new(vec![1.0]); + assert!(better_than_so(&a, &b, Direction::Maximize)); + assert!(!better_than_so(&b, &a, Direction::Maximize)); + } + + #[test] + fn better_than_so_equal_objectives_neither_strictly_better() { + let a = Evaluation::new(vec![1.0]); + let b = Evaluation::new(vec![1.0]); + // Equal objectives → strict `<` is false both directions. + assert!(!better_than_so(&a, &b, Direction::Minimize)); + assert!(!better_than_so(&b, &a, Direction::Minimize)); + } + + /// `build_tour` must produce a permutation of `[0..n)` starting at the + /// given start city. Pin both invariants across many seeds. + #[test] + fn build_tour_is_permutation_starting_at_start() { + let n = 6; + let pher = vec![vec![1.0; n]; n]; + let eta = vec![vec![1.0; n]; n]; + for seed in 0..20 { + for start in 0..n { + let mut rng = rng_from_seed(seed); + let tour = build_tour(n, start, &pher, &eta, 1.0, 2.0, &mut rng); + assert_eq!(tour.len(), n); + assert_eq!(tour[0], start, "tour must start at the given city"); + let mut sorted = tour.clone(); + sorted.sort(); + let expected: Vec = (0..n).collect(); + assert_eq!(sorted, expected, "tour must visit every city exactly once"); + } + } + } + + /// With a high `beta` and a heuristic that strongly prefers the next + /// city, `build_tour` chooses that next city with near-certainty. + /// Pins the heuristic-weighting arithmetic. + #[test] + fn build_tour_follows_strong_heuristic() { + let n = 4; + let pher = vec![vec![1.0; n]; n]; + // Heuristic strongly favors city (i+1) % n: 1000x preferred. + let mut eta = vec![vec![1.0; n]; n]; + for i in 0..n { + eta[i][(i + 1) % n] = 1000.0; + } + let mut rng = rng_from_seed(0); + let tour = build_tour(n, 0, &pher, &eta, 1.0, 5.0, &mut rng); + // With beta=5 and 1000× heuristic, the path 0→1→2→3 has overwhelming + // probability. + assert_eq!(tour, vec![0, 1, 2, 3]); + } + + /// `build_tour` with zero alpha + zero beta degenerates to uniform + /// random over unvisited cities; the result is still a permutation. + #[test] + fn build_tour_zero_weights_still_produces_permutation() { + let n = 5; + let pher = vec![vec![1.0; n]; n]; + let eta = vec![vec![1.0; n]; n]; + let mut rng = rng_from_seed(42); + let tour = build_tour(n, 2, &pher, &eta, 0.0, 0.0, &mut rng); + // With alpha=beta=0, every term is 1.0 so the result is uniform but + // still a permutation. + assert_eq!(tour.len(), n); + assert_eq!(tour[0], 2); + let mut sorted = tour.clone(); + sorted.sort(); + assert_eq!(sorted, vec![0, 1, 2, 3, 4]); + } }