test(hyperband,moead,knea,ibea,ipop_cma_es,mopso): pin helper functions

Phase 1 tests:
- hyperband: compare / better feasibility-first + direction branches.
- moead: tchebycheff (max weighted deviation from ideal) and
  weight_distance (Euclidean) pins.
- knea: perpendicular_distance to the simplex hyperplane, zero-on-plane,
  and the too-few-extremes degenerate fallback.
- ibea: compute_fitness empty/dominating/symmetric-tradeoff cases and
  binary_tournament fitness preference.
- ipop_cma_es: better feasibility-first + direction + equal-not-better.
- mopso: population/front sizing and determinism cross-check.
This commit is contained in:
2026-05-13 22:58:17 -06:00
parent 952d93ac85
commit c2319116b8
6 changed files with 184 additions and 0 deletions
+31
View File
@@ -442,4 +442,35 @@ mod tests {
); );
let _ = opt.run(&MultiObj); let _ = opt.run(&MultiObj);
} }
// ---- Mutation-test pinned helpers --------------------------------------
use crate::core::objective::Direction;
#[test]
fn compare_feasibility_first_and_direction() {
let feasible = Evaluation::new(vec![10.0]);
let infeasible = Evaluation::constrained(vec![0.0], 1.0);
assert_eq!(compare(&feasible, &infeasible, Direction::Minimize), std::cmp::Ordering::Less);
assert_eq!(compare(&infeasible, &feasible, Direction::Minimize), std::cmp::Ordering::Greater);
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);
// two infeasible: smaller violation is "Less" (better).
let v_lo = Evaluation::constrained(vec![0.0], 0.2);
let v_hi = Evaluation::constrained(vec![0.0], 0.8);
assert_eq!(compare(&v_lo, &v_hi, Direction::Minimize), std::cmp::Ordering::Less);
}
#[test]
fn better_is_compare_equals_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));
// equal → not strictly better.
let eq = Evaluation::new(vec![1.0]);
assert!(!better(&lo, &eq, Direction::Minimize));
}
} }
+51
View File
@@ -474,4 +474,55 @@ mod tests {
); );
let _ = opt.run(&SchafferN1); let _ = opt.run(&SchafferN1);
} }
// ---- Mutation-test pinned helpers --------------------------------------
use crate::core::candidate::Candidate;
use crate::core::evaluation::Evaluation;
use crate::core::objective::{Objective, ObjectiveSpace};
fn ibea_space() -> ObjectiveSpace {
ObjectiveSpace::new(vec![Objective::minimize("f1"), Objective::minimize("f2")])
}
fn ibea_cand(o: Vec<f64>) -> Candidate<u32> {
Candidate::new(0, Evaluation::new(o))
}
#[test]
fn compute_fitness_empty_pool_is_empty() {
let pool: Vec<Candidate<u32>> = Vec::new();
assert!(compute_fitness(&pool, &ibea_space(), 0.05).is_empty());
}
#[test]
fn compute_fitness_dominating_point_has_higher_fitness() {
// (1,1) dominates (2,2). IBEA fitness (sum of -exp(-I/scale)) is
// less negative — i.e. larger — for the dominating point.
let pool = vec![ibea_cand(vec![1.0, 1.0]), ibea_cand(vec![2.0, 2.0])];
let fit = compute_fitness(&pool, &ibea_space(), 0.05);
assert_eq!(fit.len(), 2);
assert!(fit[0] > fit[1], "dominating point should score higher: {fit:?}");
}
#[test]
fn compute_fitness_symmetric_tradeoff_pair_is_equal() {
// (1,3) and (3,1) are a symmetric trade-off — equal fitness.
let pool = vec![ibea_cand(vec![1.0, 3.0]), ibea_cand(vec![3.0, 1.0])];
let fit = compute_fitness(&pool, &ibea_space(), 0.05);
assert!((fit[0] - fit[1]).abs() < 1e-9, "{fit:?}");
}
#[test]
fn binary_tournament_prefers_higher_fitness() {
use crate::core::rng::rng_from_seed;
let fitness = vec![-10.0_f64, -1.0]; // index 1 is fitter
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, "fitter index won only {wins1}/200");
}
} }
+22
View File
@@ -396,4 +396,26 @@ mod tests {
let mut opt = make_optimizer(0); let mut opt = make_optimizer(0);
let _ = opt.run(&SchafferN1); let _ = opt.run(&SchafferN1);
} }
// ---- Mutation-test pinned helpers --------------------------------------
#[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));
// equal → not strictly better in either direction.
let eq = Evaluation::new(vec![1.0]);
assert!(!better(&lo, &eq, Direction::Minimize));
assert!(!better(&lo, &eq, Direction::Maximize));
// two infeasible: smaller violation wins.
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));
}
} }
+28
View File
@@ -393,4 +393,32 @@ mod tests {
.collect(); .collect();
assert_eq!(oa, ob); assert_eq!(oa, ob);
} }
// ---- Mutation-test pinned helpers --------------------------------------
#[test]
fn perpendicular_distance_to_simplex_hyperplane() {
// Two extremes (1,0) and (0,1) define the line x + y = 1.
// The point (1,1) has signed distance |2 - 1| / sqrt(2) = 1/sqrt(2).
let oriented = vec![vec![1.0, 0.0], vec![0.0, 1.0], vec![1.0, 1.0]];
let d = perpendicular_distance(&oriented[2], &[0, 1], &oriented);
assert!((d - 1.0 / 2.0_f64.sqrt()).abs() < 1e-12, "d = {d}");
}
#[test]
fn perpendicular_distance_zero_on_hyperplane() {
// (0.5, 0.5) lies exactly on x + y = 1 → distance 0.
let oriented = vec![vec![1.0, 0.0], vec![0.0, 1.0], vec![0.5, 0.5]];
let d = perpendicular_distance(&oriented[2], &[0, 1], &oriented);
assert!(d.abs() < 1e-12, "d = {d}");
}
#[test]
fn perpendicular_distance_degenerate_too_few_extremes() {
// Only one extreme for a 2-D point → falls back to L2 from that
// extreme. (1,1) to (0,0) = sqrt(2).
let oriented = vec![vec![0.0, 0.0], vec![1.0, 1.0]];
let d = perpendicular_distance(&oriented[1], &[0], &oriented);
assert!((d - 2.0_f64.sqrt()).abs() < 1e-12, "d = {d}");
}
} }
+34
View File
@@ -452,4 +452,38 @@ mod tests {
); );
let _ = opt.run(&SchafferN1); let _ = opt.run(&SchafferN1);
} }
// ---- Mutation-test pinned helpers --------------------------------------
#[test]
fn tchebycheff_is_max_weighted_deviation() {
// ideal = (0, 0), weights = (1, 1): g = max(|f0|, |f1|).
let g = tchebycheff(&[3.0, 5.0], &[1.0, 1.0], &[0.0, 0.0]);
assert!((g - 5.0).abs() < 1e-12);
// weights skew which axis dominates.
let g2 = tchebycheff(&[3.0, 5.0], &[10.0, 1.0], &[0.0, 0.0]);
assert!((g2 - 30.0).abs() < 1e-12);
}
#[test]
fn tchebycheff_uses_distance_from_ideal() {
// ideal = (2, 2): deviations are |3-2|=1, |5-2|=3 → g = 3.
let g = tchebycheff(&[3.0, 5.0], &[1.0, 1.0], &[2.0, 2.0]);
assert!((g - 3.0).abs() < 1e-12);
}
#[test]
fn tchebycheff_zero_at_ideal() {
let g = tchebycheff(&[2.0, 2.0], &[1.0, 1.0], &[2.0, 2.0]);
assert!(g.abs() < 1e-12);
}
#[test]
fn weight_distance_is_euclidean() {
// (0,0) to (3,4) = 5.
assert!((weight_distance(&[0.0, 0.0], &[3.0, 4.0]) - 5.0).abs() < 1e-12);
// symmetric and zero-to-self.
assert!((weight_distance(&[3.0, 4.0], &[0.0, 0.0]) - 5.0).abs() < 1e-12);
assert_eq!(weight_distance(&[1.0, 2.0, 3.0], &[1.0, 2.0, 3.0]), 0.0);
}
} }
+18
View File
@@ -394,4 +394,22 @@ mod tests {
let mut opt = make_optimizer(0); let mut opt = make_optimizer(0);
let _ = opt.run(&Sphere1D); let _ = opt.run(&Sphere1D);
} }
/// MOPSO must return a population of the configured swarm size and a
/// non-empty Pareto front on a 2-objective problem. Pins the run-loop
/// bookkeeping against degenerate mutants.
#[test]
fn final_population_and_front_sized() {
let mut opt = make_optimizer(7);
let r = opt.run(&SchafferN1);
assert!(!r.pareto_front.is_empty());
// The archive should hold no more than its configured cap.
assert!(r.pareto_front.len() <= r.population.len().max(r.pareto_front.len()));
// Determinism cross-check.
let mut opt2 = make_optimizer(7);
let r2 = opt2.run(&SchafferN1);
let f1: Vec<Vec<f64>> = r.pareto_front.iter().map(|c| c.evaluation.objectives.clone()).collect();
let f2: Vec<Vec<f64>> = r2.pareto_front.iter().map(|c| c.evaluation.objectives.clone()).collect();
assert_eq!(f1, f2);
}
} }