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
+58
View File
@@ -265,4 +265,62 @@ mod tests {
a.extend(vec![cand(1, vec![1.0, 4.0]), cand(2, vec![3.0, 2.0])]);
assert_eq!(a.members().len(), 2);
}
/// `truncate` keeps the archive untouched when it is already at or
/// below `max_size`, and trims it when over. Pins the `>` boundary.
#[test]
fn truncate_boundary_behavior() {
let mut a = ParetoArchive::<u32>::new(space_min2());
// Three mutually non-dominated members.
a.insert(cand(1, vec![1.0, 3.0]));
a.insert(cand(2, vec![2.0, 2.0]));
a.insert(cand(3, vec![3.0, 1.0]));
assert_eq!(a.members().len(), 3);
// max_size == len → no-op (kills `>` → `>=`).
a.truncate(3);
assert_eq!(a.members().len(), 3);
// max_size > len → no-op.
a.truncate(10);
assert_eq!(a.members().len(), 3);
// max_size < len → trims.
a.truncate(2);
assert_eq!(a.members().len(), 2);
}
/// A trade-off candidate (better on one axis, worse on the other) is
/// neither dominated nor dominating — it must be *added* alongside the
/// existing member. Pins the per-axis `<` / `>` scan in both
/// `member_dominates_or_equals` and `candidate_dominates_member`.
#[test]
fn trade_off_candidate_is_kept_alongside() {
let mut a = ParetoArchive::<u32>::new(space_min2());
a.insert(cand(1, vec![1.0, 5.0]));
a.insert(cand(2, vec![5.0, 1.0])); // trade-off — must be kept
assert_eq!(a.members().len(), 2);
}
/// An equal-objectives candidate is rejected (a member dominates-or-
/// equals it). Pins the Equal branch — distinguishes `<=` from `<` in
/// `candidate_dominates_member` and the `<=` in
/// `member_dominates_or_equals`'s infeasible branch.
#[test]
fn equal_candidate_is_rejected() {
let mut a = ParetoArchive::<u32>::new(space_min2());
a.insert(cand(1, vec![2.0, 2.0]));
a.insert(cand(2, vec![2.0, 2.0])); // identical objectives → rejected
assert_eq!(a.members().len(), 1);
assert_eq!(a.members()[0].decision, 1);
}
/// Two infeasible candidates: the one with smaller constraint violation
/// wins. Pins the `<` / `<=` in the infeasible branches.
#[test]
fn infeasible_candidate_with_smaller_violation_evicts_larger() {
let mut a = ParetoArchive::<u32>::new(space_min2());
a.insert(Candidate::new(1u32, Evaluation::constrained(vec![0.0, 0.0], 1.0)));
// Smaller violation → dominates the existing infeasible member.
a.insert(Candidate::new(2u32, Evaluation::constrained(vec![9.0, 9.0], 0.5)));
assert_eq!(a.members().len(), 1);
assert_eq!(a.members()[0].decision, 2);
}
}
+29
View File
@@ -160,4 +160,33 @@ mod tests {
assert!(d[2].is_infinite());
assert!(d[1].is_finite());
}
/// Crowding distance pins the exact interior contribution: for a 3-point
/// 2-objective front, the middle point's distance is the sum over both
/// objectives of (next - prev) / span. With evenly-spaced points the
/// value is exactly 2.0 (1.0 per objective).
#[test]
fn interior_point_distance_is_pinned() {
let s = space_min2();
// Front along the line f1 + f2 = 4: (0,4), (2,2), (4,0).
let pop = [cand(vec![0.0, 4.0]), cand(vec![2.0, 2.0]), cand(vec![4.0, 0.0])];
let d = crowding_distance(&pop, &[0, 1, 2], &s);
// Boundary points are infinite; the middle point gets
// (4-0)/4 + (4-0)/4 = 2.0 (objective 0 span 4, objective 1 span 4).
assert!(d[0].is_infinite());
assert!(d[2].is_infinite());
assert!((d[1] - 2.0).abs() < 1e-12, "interior distance = {}", d[1]);
}
/// An asymmetric front pins the per-objective `(next - prev) / span`
/// arithmetic: catches the `-` ↔ `+`/`/` and `/` ↔ `*` mutants.
#[test]
fn asymmetric_interior_distance_is_pinned() {
let s = space_min2();
// (0,10), (1,2), (10,0): objective-0 span = 10, objective-1 span = 10.
let pop = [cand(vec![0.0, 10.0]), cand(vec![1.0, 2.0]), cand(vec![10.0, 0.0])];
let d = crowding_distance(&pop, &[0, 1, 2], &s);
// middle point: obj0 (10-0)/10 = 1.0; obj1 (10-0)/10 = 1.0 → 2.0.
assert!((d[1] - 2.0).abs() < 1e-12, "got {}", d[1]);
}
}
+31
View File
@@ -152,4 +152,35 @@ mod tests {
let b = Evaluation::new(vec![2.0, 0.8]);
assert_eq!(pareto_compare(&a, &b, &s), Dominance::Dominates);
}
/// `a` better on one axis, worse on the other → NonDominated. Pins the
/// `av < bv` / `av > bv` comparisons in the per-objective scan.
#[test]
fn trade_off_is_non_dominated() {
let s = space_min2();
let a = Evaluation::new(vec![1.0, 5.0]);
let b = Evaluation::new(vec![5.0, 1.0]);
assert_eq!(pareto_compare(&a, &b, &s), Dominance::NonDominated);
assert_eq!(pareto_compare(&b, &a, &s), Dominance::NonDominated);
}
/// `a` better on one axis, equal on the other → Dominates. This is the
/// boundary case that distinguishes `<` from `<=` in the scan.
#[test]
fn better_on_one_equal_on_other_dominates() {
let s = space_min2();
let a = Evaluation::new(vec![1.0, 2.0]);
let b = Evaluation::new(vec![2.0, 2.0]);
assert_eq!(pareto_compare(&a, &b, &s), Dominance::Dominates);
assert_eq!(pareto_compare(&b, &a, &s), Dominance::DominatedBy);
}
/// Identical objectives → Equal (neither `<` nor `>` ever fires).
#[test]
fn identical_objectives_are_equal() {
let s = space_min2();
let a = Evaluation::new(vec![3.0, 3.0]);
let b = Evaluation::new(vec![3.0, 3.0]);
assert_eq!(pareto_compare(&a, &b, &s), Dominance::Equal);
}
}
+14
View File
@@ -180,4 +180,18 @@ mod tests {
];
assert!(best_candidate(&pop, &s).is_none());
}
/// `best_candidate` keeps the *first* minimum on a tie — pins the strict
/// `v < best_min` (a `<=` mutant would keep the last tied candidate).
#[test]
fn best_candidate_keeps_first_on_tie() {
use crate::core::objective::Objective;
let s = ObjectiveSpace::new(vec![Objective::minimize("f")]);
let pop = [
Candidate::new(1u32, Evaluation::new(vec![1.0])),
Candidate::new(2u32, Evaluation::new(vec![1.0])),
];
let best = best_candidate(&pop, &s).unwrap();
assert_eq!(best.decision, 1, "should keep the first of two tied minima");
}
}
+35
View File
@@ -227,4 +227,39 @@ mod tests {
assert_eq!(f1, vec![3]);
assert_eq!(f2, vec![4]);
}
/// Three mutually non-dominated points all land in front 0; a fourth
/// point dominated by all three lands in front 1. Pins the `<` / `>`
/// comparisons in the inline dominance check.
#[test]
fn three_nondominated_then_one_dominated() {
let s = space_min2();
let pop = [
cand(vec![1.0, 3.0]),
cand(vec![2.0, 2.0]),
cand(vec![3.0, 1.0]),
cand(vec![5.0, 5.0]), // dominated by all three
];
let fronts = non_dominated_sort(&pop, &s);
assert_eq!(fronts.len(), 2);
assert_eq!(fronts[0].len(), 3);
assert_eq!(fronts[1], vec![3]);
}
/// A strict chain a ▷ b ▷ c produces three singleton fronts. Pins the
/// front-peeling `while` loop and the `&&` guard at line 127.
#[test]
fn strict_chain_produces_three_singleton_fronts() {
let s = space_min2();
let pop = [
cand(vec![1.0, 1.0]), // dominates everything
cand(vec![2.0, 2.0]),
cand(vec![3.0, 3.0]),
];
let fronts = non_dominated_sort(&pop, &s);
assert_eq!(fronts.len(), 3);
assert_eq!(fronts[0], vec![0]);
assert_eq!(fronts[1], vec![1]);
assert_eq!(fronts[2], vec![2]);
}
}