test(tlbo,umda,simulated_annealing,tabu_search,tpe,snes,rvea,spea2): pin comparison and geometry helpers

Phase 1 tests for eight more algorithms — the feasibility-first
comparison helpers (better / better_than / compare_so / worse_than),
plus algorithm-specific pure functions:
- tlbo: best_index min/max/tie.
- tpe: oriented_target sign-flip + penalty; split_good_bad partition
  and clamp-to-at-least-one-each.
- snes: nes_utilities sum-to-zero + descending + positive-best.
- rvea: unit_normalize (3-4-5 → 0.6/0.8), zero-vector passthrough;
  closest_reference smallest-angle; smallest_neighbor_angle = π/2 for
  orthogonal refs.
- spea2: euclidean distance basics; binary_tournament prefers lower
  fitness.
This commit is contained in:
2026-05-13 22:58:17 -06:00
parent 6819ce4091
commit c5b003a9c9
8 changed files with 261 additions and 0 deletions
+37
View File
@@ -556,4 +556,41 @@ mod tests {
);
let _ = opt.run(&SchafferN1);
}
// ---- Mutation-test pinned helpers --------------------------------------
#[test]
fn unit_normalize_produces_unit_vector() {
let v = unit_normalize(vec![3.0, 4.0]);
let norm: f64 = v.iter().map(|x| x * x).sum::<f64>().sqrt();
assert!((norm - 1.0).abs() < 1e-12);
assert!((v[0] - 0.6).abs() < 1e-12);
assert!((v[1] - 0.8).abs() < 1e-12);
}
#[test]
fn unit_normalize_zero_vector_unchanged() {
// A (near-)zero vector is left as-is (no division by ~0).
let v = unit_normalize(vec![0.0, 0.0]);
assert_eq!(v, vec![0.0, 0.0]);
}
#[test]
fn closest_reference_picks_smallest_angle() {
// References along the two axes; a point near the x-axis associates
// with reference 0 at a small angle.
let refs = vec![vec![1.0, 0.0], vec![0.0, 1.0]];
let (idx, angle) = closest_reference(&[1.0, 0.0], &refs);
assert_eq!(idx, 0);
assert!(angle.abs() < 1e-9, "angle = {angle}");
let (idx2, _) = closest_reference(&[0.1, 1.0], &refs);
assert_eq!(idx2, 1);
}
#[test]
fn smallest_neighbor_angle_of_orthogonal_refs_is_pi_over_2() {
let refs = vec![vec![1.0, 0.0], vec![0.0, 1.0]];
let a = smallest_neighbor_angle(&refs);
assert!((a - std::f64::consts::FRAC_PI_2).abs() < 1e-9, "angle = {a}");
}
}
+22
View File
@@ -407,4 +407,26 @@ mod tests {
);
let _ = opt.run(&Sphere1D);
}
// ---- Mutation-test pinned helpers --------------------------------------
use crate::core::evaluation::Evaluation;
use crate::core::objective::Direction;
#[test]
fn better_than_feasibility_first_and_direction() {
let feasible = Evaluation::new(vec![100.0]);
let infeasible = Evaluation::constrained(vec![0.0], 1.0);
assert!(better_than(&feasible, &infeasible, Direction::Minimize));
assert!(!better_than(&infeasible, &feasible, Direction::Minimize));
let lo = Evaluation::new(vec![1.0]);
let hi = Evaluation::new(vec![2.0]);
assert!(better_than(&lo, &hi, Direction::Minimize));
assert!(better_than(&hi, &lo, Direction::Maximize));
let eq = Evaluation::new(vec![1.0]);
assert!(!better_than(&lo, &eq, Direction::Minimize));
let v_lo = Evaluation::constrained(vec![0.0], 0.2);
let v_hi = Evaluation::constrained(vec![0.0], 0.8);
assert!(better_than(&v_lo, &v_hi, Direction::Minimize));
}
}
+42
View File
@@ -457,4 +457,46 @@ mod tests {
let mut opt = make_optimizer(0);
let _ = opt.run(&SchafferN1);
}
// ---- Mutation-test pinned helpers --------------------------------------
use crate::core::evaluation::Evaluation;
use crate::core::objective::Direction;
#[test]
fn nes_utilities_sum_to_zero_and_are_descending() {
// The NES utility weights are a shifted log-rank scheme; they sum
// to (approximately) zero and the first (best-ranked) is largest.
let u = nes_utilities(10);
assert_eq!(u.len(), 10);
let sum: f64 = u.iter().sum();
assert!(sum.abs() < 1e-9, "utilities sum = {sum}");
// Descending: best rank gets the most weight.
for w in u.windows(2) {
assert!(w[0] >= w[1] - 1e-12, "not descending: {:?}", u);
}
// The first utility is positive (it gets above-average weight).
assert!(u[0] > 0.0);
}
#[test]
fn compare_feasibility_first_and_direction() {
let feasible = Evaluation::new(vec![100.0]);
let infeasible = Evaluation::constrained(vec![0.0], 1.0);
assert_eq!(compare(&feasible, &infeasible, Direction::Minimize), std::cmp::Ordering::Less);
let lo = Evaluation::new(vec![1.0]);
let hi = Evaluation::new(vec![2.0]);
assert_eq!(compare(&lo, &hi, Direction::Minimize), std::cmp::Ordering::Less);
assert_eq!(compare(&lo, &hi, Direction::Maximize), std::cmp::Ordering::Greater);
}
#[test]
fn better_is_strict_less() {
let lo = Evaluation::new(vec![1.0]);
let hi = Evaluation::new(vec![2.0]);
assert!(better(&lo, &hi, Direction::Minimize));
assert!(!better(&hi, &lo, Direction::Minimize));
let eq = Evaluation::new(vec![1.0]);
assert!(!better(&lo, &eq, Direction::Minimize));
}
}
+26
View File
@@ -601,4 +601,30 @@ mod tests {
);
let _ = opt.run(&SchafferN1);
}
// ---- Mutation-test pinned helpers --------------------------------------
#[test]
fn euclidean_distance_basics() {
// (0,0) to (3,4) = 5.
assert!((euclidean(&[0.0, 0.0], &[3.0, 4.0]) - 5.0).abs() < 1e-12);
// symmetric and zero-to-self.
assert!((euclidean(&[3.0, 4.0], &[0.0, 0.0]) - 5.0).abs() < 1e-12);
assert_eq!(euclidean(&[1.0, 2.0, 3.0], &[1.0, 2.0, 3.0]), 0.0);
}
#[test]
fn binary_tournament_prefers_lower_fitness() {
// SPEA2 fitness is "lower is better" — index 1 here is the best.
use crate::core::rng::rng_from_seed;
let fitness = vec![5.0_f64, 0.5];
let mut wins1 = 0;
for seed in 0..200 {
let mut rng = rng_from_seed(seed);
if binary_tournament(&fitness, &mut rng) == 1 {
wins1 += 1;
}
}
assert!(wins1 > 130, "lower-fitness index won only {wins1}/200");
}
}
+20
View File
@@ -421,4 +421,24 @@ mod tests {
rb.best.unwrap().evaluation.objectives,
);
}
// ---- Mutation-test pinned helpers --------------------------------------
#[test]
fn better_than_feasibility_first_and_direction() {
use crate::core::objective::Direction;
let feasible = Evaluation::new(vec![100.0]);
let infeasible = Evaluation::constrained(vec![0.0], 1.0);
assert!(better_than(&feasible, &infeasible, Direction::Minimize));
assert!(!better_than(&infeasible, &feasible, Direction::Minimize));
let lo = Evaluation::new(vec![1.0]);
let hi = Evaluation::new(vec![2.0]);
assert!(better_than(&lo, &hi, Direction::Minimize));
assert!(better_than(&hi, &lo, Direction::Maximize));
let eq = Evaluation::new(vec![1.0]);
assert!(!better_than(&lo, &eq, Direction::Minimize));
let v_lo = Evaluation::constrained(vec![0.0], 0.2);
let v_hi = Evaluation::constrained(vec![0.0], 0.8);
assert!(better_than(&v_lo, &v_hi, Direction::Minimize));
}
}
+36
View File
@@ -383,4 +383,40 @@ mod tests {
let mut opt = make_optimizer(0);
let _ = opt.run(&SchafferN1);
}
// ---- Mutation-test pinned helpers --------------------------------------
use crate::core::evaluation::Evaluation;
use crate::core::objective::Direction;
#[test]
fn better_feasibility_first_and_direction() {
let feasible = Evaluation::new(vec![100.0]);
let infeasible = Evaluation::constrained(vec![0.0], 1.0);
assert!(better(&feasible, &infeasible, Direction::Minimize));
assert!(!better(&infeasible, &feasible, Direction::Minimize));
let lo = Evaluation::new(vec![1.0]);
let hi = Evaluation::new(vec![2.0]);
assert!(better(&lo, &hi, Direction::Minimize));
assert!(better(&hi, &lo, Direction::Maximize));
let eq = Evaluation::new(vec![1.0]);
assert!(!better(&lo, &eq, Direction::Minimize));
let v_lo = Evaluation::constrained(vec![0.0], 0.2);
let v_hi = Evaluation::constrained(vec![0.0], 0.8);
assert!(better(&v_lo, &v_hi, Direction::Minimize));
}
#[test]
fn best_index_finds_min_and_max() {
let evals = [
Evaluation::new(vec![3.0]),
Evaluation::new(vec![1.0]),
Evaluation::new(vec![4.0]),
];
assert_eq!(best_index(&evals, Direction::Minimize), 1);
assert_eq!(best_index(&evals, Direction::Maximize), 2);
// tie keeps the first.
let flat = [Evaluation::new(vec![1.0]), Evaluation::new(vec![1.0])];
assert_eq!(best_index(&flat, Direction::Minimize), 0);
}
}
+52
View File
@@ -552,4 +552,56 @@ mod tests {
let mut opt = make_optimizer(0);
let _ = opt.run(&SchafferN1);
}
// ---- Mutation-test pinned helpers --------------------------------------
use crate::core::evaluation::Evaluation;
use crate::core::objective::Direction;
#[test]
fn oriented_target_flips_sign_and_penalizes() {
let e = Evaluation::new(vec![3.0]);
assert!((oriented_target(&e, Direction::Minimize) - 3.0).abs() < 1e-12);
assert!((oriented_target(&e, Direction::Maximize) + 3.0).abs() < 1e-12);
let mut bad = Evaluation::new(vec![1.0]);
bad.constraint_violation = 0.5;
assert!((oriented_target(&bad, Direction::Minimize) - 500_001.0).abs() < 1e-9);
}
#[test]
fn better_feasibility_first_and_direction() {
let feasible = Evaluation::new(vec![100.0]);
let infeasible = Evaluation::constrained(vec![0.0], 1.0);
assert!(better(&feasible, &infeasible, Direction::Minimize));
let lo = Evaluation::new(vec![1.0]);
let hi = Evaluation::new(vec![2.0]);
assert!(better(&lo, &hi, Direction::Minimize));
assert!(better(&hi, &lo, Direction::Maximize));
}
#[test]
fn split_good_bad_partitions_by_target_rank() {
// targets 5, 1, 3, 9, 7 → ranked 1<3<5<7<9 → indices 1,2,0,4,3.
let targets = [5.0, 1.0, 3.0, 9.0, 7.0];
let (good, bad) = split_good_bad(&targets, 0.4);
// 40% of 5 = 2 good.
assert_eq!(good.len(), 2);
assert_eq!(bad.len(), 3);
// The two smallest targets (1.0 at idx 1, 3.0 at idx 2) are "good".
assert!(good.contains(&1));
assert!(good.contains(&2));
}
#[test]
fn split_good_bad_clamps_to_at_least_one_each() {
let targets = [5.0, 1.0, 3.0];
// good_fraction 0.0 would round to 0 — must clamp to >= 1.
let (good, bad) = split_good_bad(&targets, 0.0);
assert!(!good.is_empty());
assert!(!bad.is_empty());
// good_fraction 1.0 would take everything — must leave >= 1 bad.
let (good2, bad2) = split_good_bad(&targets, 1.0);
assert!(!good2.is_empty());
assert!(!bad2.is_empty());
}
}
+26
View File
@@ -449,4 +449,30 @@ mod tests {
});
let _ = opt.run(&DummyMo);
}
// ---- Mutation-test pinned helpers --------------------------------------
#[test]
fn compare_so_feasibility_first_and_direction() {
let feasible = Evaluation::new(vec![100.0]);
let infeasible = Evaluation::constrained(vec![0.0], 1.0);
assert_eq!(compare_so(&feasible, &infeasible, Direction::Minimize), std::cmp::Ordering::Less);
let lo = Evaluation::new(vec![1.0]);
let hi = Evaluation::new(vec![2.0]);
assert_eq!(compare_so(&lo, &hi, Direction::Minimize), std::cmp::Ordering::Less);
assert_eq!(compare_so(&lo, &hi, Direction::Maximize), std::cmp::Ordering::Greater);
let v_lo = Evaluation::constrained(vec![0.0], 0.2);
let v_hi = Evaluation::constrained(vec![0.0], 0.8);
assert_eq!(compare_so(&v_lo, &v_hi, Direction::Minimize), std::cmp::Ordering::Less);
}
#[test]
fn better_than_so_is_strict_less() {
let lo = Evaluation::new(vec![1.0]);
let hi = Evaluation::new(vec![2.0]);
assert!(better_than_so(&lo, &hi, Direction::Minimize));
assert!(!better_than_so(&hi, &lo, Direction::Minimize));
let eq = Evaluation::new(vec![1.0]);
assert!(!better_than_so(&lo, &eq, Direction::Minimize));
}
}