test(ant_colony_tsp): pin better_than_so branches and build_tour invariants

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.
This commit is contained in:
2026-05-13 22:58:16 -06:00
parent e5e979f02a
commit 3456db6cd8
+112
View File
@@ -565,4 +565,116 @@ mod tests {
); );
let _ = opt.run(&DummyMo); 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<usize> = (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]);
}
} }