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:
@@ -442,4 +442,35 @@ mod tests {
|
||||
);
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -474,4 +474,55 @@ mod tests {
|
||||
);
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -396,4 +396,26 @@ mod tests {
|
||||
let mut opt = make_optimizer(0);
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -393,4 +393,32 @@ mod tests {
|
||||
.collect();
|
||||
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}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -452,4 +452,38 @@ mod tests {
|
||||
);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -394,4 +394,22 @@ mod tests {
|
||||
let mut opt = make_optimizer(0);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user