test(pareto,metrics,selection): pin shared-utility comparisons and arithmetic

Phase 1, tier 3 of the mutation-testing campaign — the shared Pareto /
metric / selection utilities used by every multi-objective algorithm.
A scoped cargo-mutants run found 75 survivors across these files; the
tests below target them.

- metrics/hypervolume.rs: dominates() boundary cases, non_dominated_
  projection retained-set pins, hso_recursive 1-D/2-D base cases,
  hypervolume_nd_from_evaluations empty/non-dominating skips.
- selection/tournament.rs: challenger_wins across the full feasibility
  cross-product + equal-objective tie; better_by_objective and
  better_by_feasibility branch pins; stochastic_ranking_select pf=0
  feasibility ordering and count-wraps-modulo-population.
- pareto/crowding.rs: exact interior crowding distance on symmetric
  and asymmetric fronts (pins the (next-prev)/span arithmetic).
- pareto/sort.rs: three-non-dominated-then-one-dominated and a strict
  3-chain producing three singleton fronts.
- pareto/dominance.rs: trade-off → NonDominated, better-on-one-equal-
  on-other → Dominates, identical → Equal.
- pareto/archive.rs: truncate boundary, trade-off kept alongside,
  equal candidate rejected, smaller-violation infeasible eviction.
- pareto/front.rs: best_candidate keeps the first of tied minima.
- metrics/spacing.rs: exact spacing for a varying-NN-distance front.

src/core/problem.rs's lone survivor (decision_schema default body
'replace with vec![]') is an equivalent mutant — Vec::new() and vec![]
are identical — and is left in the residue.
This commit is contained in:
2026-05-13 22:58:17 -06:00
parent 7b8b7170f4
commit 4569244a68
8 changed files with 403 additions and 0 deletions
+101
View File
@@ -269,4 +269,105 @@ mod tests {
let mut rng = rng_from_seed(0);
let _ = stochastic_ranking_select(&pop, &s, 1.5, 1, &mut rng);
}
// ---- Mutation-test pinned helpers --------------------------------------
fn constrained(d: u32, obj: f64, cv: f64) -> Candidate<u32> {
Candidate::new(d, Evaluation::constrained(vec![obj], cv))
}
#[test]
fn challenger_wins_feasibility_first() {
// Feasible challenger beats infeasible best, regardless of objective.
let feasible = cand_min(1, 100.0);
let infeasible = constrained(2, 0.0, 1.0);
assert!(challenger_wins(&feasible, &infeasible, Direction::Minimize));
assert!(!challenger_wins(&infeasible, &feasible, Direction::Minimize));
}
#[test]
fn challenger_wins_two_infeasible_compares_violation() {
let less_violating = constrained(1, 0.0, 0.5);
let more_violating = constrained(2, 0.0, 1.0);
assert!(challenger_wins(&less_violating, &more_violating, Direction::Minimize));
assert!(!challenger_wins(&more_violating, &less_violating, Direction::Minimize));
}
#[test]
fn challenger_wins_two_feasible_under_min_and_max() {
let lower = cand_min(1, 1.0);
let higher = cand_min(2, 2.0);
assert!(challenger_wins(&lower, &higher, Direction::Minimize));
assert!(!challenger_wins(&higher, &lower, Direction::Minimize));
assert!(challenger_wins(&higher, &lower, Direction::Maximize));
assert!(!challenger_wins(&lower, &higher, Direction::Maximize));
}
#[test]
fn challenger_wins_equal_objectives_does_not_win() {
// Strict comparison: equal objectives → challenger does NOT win.
let a = cand_min(1, 1.0);
let b = cand_min(2, 1.0);
assert!(!challenger_wins(&a, &b, Direction::Minimize));
assert!(!challenger_wins(&a, &b, Direction::Maximize));
}
#[test]
fn better_by_objective_min_and_max() {
let a = Evaluation::new(vec![1.0]);
let b = Evaluation::new(vec![2.0]);
assert!(better_by_objective(&a, &b, Direction::Minimize));
assert!(!better_by_objective(&b, &a, Direction::Minimize));
assert!(better_by_objective(&b, &a, Direction::Maximize));
assert!(!better_by_objective(&a, &b, Direction::Maximize));
// Equal → not strictly better.
let c = Evaluation::new(vec![1.0]);
assert!(!better_by_objective(&a, &c, Direction::Minimize));
}
#[test]
fn better_by_feasibility_all_four_branches() {
let feasible_a = Evaluation::new(vec![10.0]);
let infeasible_b = Evaluation::constrained(vec![0.0], 1.0);
// feasible vs infeasible
assert!(better_by_feasibility(&feasible_a, &infeasible_b, Direction::Minimize));
assert!(!better_by_feasibility(&infeasible_b, &feasible_a, Direction::Minimize));
// two infeasible: smaller violation wins
let low_cv = Evaluation::constrained(vec![0.0], 0.3);
let high_cv = Evaluation::constrained(vec![0.0], 0.9);
assert!(better_by_feasibility(&low_cv, &high_cv, Direction::Minimize));
assert!(!better_by_feasibility(&high_cv, &low_cv, Direction::Minimize));
// two feasible: delegates to better_by_objective
let feasible_lower = Evaluation::new(vec![1.0]);
let feasible_higher = Evaluation::new(vec![2.0]);
assert!(better_by_feasibility(&feasible_lower, &feasible_higher, Direction::Minimize));
}
#[test]
fn stochastic_ranking_select_pf_zero_is_pure_feasibility_order() {
// pf = 0 → always compare by feasibility. The feasible candidate
// must rank first regardless of objective value.
let s = ObjectiveSpace::new(vec![Objective::minimize("f")]);
let pop = [
constrained(1, 0.0, 2.0), // infeasible, great objective
cand_min(2, 100.0), // feasible, terrible objective
];
let mut rng = rng_from_seed(7);
let picks = stochastic_ranking_select(&pop, &s, 0.0, 1, &mut rng);
// With pf=0, feasibility dominates → candidate 2 ranked first.
assert_eq!(picks, vec![2]);
}
#[test]
fn stochastic_ranking_select_count_wraps_modulo_population() {
// count > population size wraps around via `order[k % n]`.
let s = ObjectiveSpace::new(vec![Objective::minimize("f")]);
let pop = [cand_min(1, 1.0), cand_min(2, 2.0)];
let mut rng = rng_from_seed(0);
let picks = stochastic_ranking_select(&pop, &s, 0.0, 5, &mut rng);
assert_eq!(picks.len(), 5);
// Best (candidate 1) is at index 0; index 2 wraps to it again.
assert_eq!(picks[0], 1);
assert_eq!(picks[2], 1);
}
}