test(nelder_mead,nsga2,nsga3,one_plus_one_es,particle_swarm): pin selection/geometry helpers

Phase 1 tests:
- nelder_mead: compare / better feasibility-first + direction.
- nsga2: binary_tournament prefers lower rank, then higher crowding
  distance at equal rank (statistical majority over 200 seeds).
- nsga3: solve_intercepts on axis-aligned extremes / singular / empty;
  associate picks the closest reference direction with correct
  perpendicular distance.
- one_plus_one_es: worse_than across feasibility + direction + equal.
- particle_swarm: best_index min/max/tie/single-element.
This commit is contained in:
2026-05-13 22:58:17 -06:00
parent c2319116b8
commit 6819ce4091
5 changed files with 198 additions and 0 deletions
+28
View File
@@ -551,4 +551,32 @@ mod tests {
);
let _ = opt.run(&SchafferN1);
}
// ---- 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);
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);
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_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));
}
}
+60
View File
@@ -463,4 +463,64 @@ mod tests {
);
let _ = opt.run(&SchafferN1);
}
// ---- Mutation-test pinned helpers --------------------------------------
#[test]
fn binary_tournament_prefers_lower_rank() {
use crate::core::candidate::Candidate;
use crate::core::evaluation::Evaluation;
use crate::core::rng::rng_from_seed;
// Entry 0: rank 0; entry 1: rank 1. Lower rank must win every time
// the two draws differ.
let entries = vec![
Nsga2Entry {
candidate: Candidate::new(0u32, Evaluation::new(vec![1.0, 1.0])),
rank: 0,
crowding_distance: 0.0,
},
Nsga2Entry {
candidate: Candidate::new(1u32, Evaluation::new(vec![2.0, 2.0])),
rank: 1,
crowding_distance: 100.0,
},
];
let mut wins0 = 0;
for seed in 0..200 {
let mut rng = rng_from_seed(seed);
if binary_tournament(&entries, &mut rng) == 0 {
wins0 += 1;
}
}
// Rank dominates crowding distance — index 0 wins the clear majority.
assert!(wins0 > 130, "lower-rank index won only {wins0}/200");
}
#[test]
fn binary_tournament_prefers_higher_crowding_at_equal_rank() {
use crate::core::candidate::Candidate;
use crate::core::evaluation::Evaluation;
use crate::core::rng::rng_from_seed;
// Both rank 0; entry 0 has higher crowding distance → preferred.
let entries = vec![
Nsga2Entry {
candidate: Candidate::new(0u32, Evaluation::new(vec![1.0, 1.0])),
rank: 0,
crowding_distance: 10.0,
},
Nsga2Entry {
candidate: Candidate::new(1u32, Evaluation::new(vec![1.0, 1.0])),
rank: 0,
crowding_distance: 1.0,
},
];
let mut wins0 = 0;
for seed in 0..200 {
let mut rng = rng_from_seed(seed);
if binary_tournament(&entries, &mut rng) == 0 {
wins0 += 1;
}
}
assert!(wins0 > 130, "higher-crowding index won only {wins0}/200");
}
}
+53
View File
@@ -554,6 +554,59 @@ impl<I, V> crate::traits::AlgorithmInfo for Nsga3<I, V> {
}
}
#[cfg(test)]
mod helper_tests {
use super::*;
#[test]
fn solve_intercepts_axis_aligned_extremes() {
// Extremes (2, 0) and (0, 3): the plane through them on the
// canonical simplex has intercepts (2, 3).
let oriented = vec![vec![2.0, 0.0], vec![0.0, 3.0]];
let intercepts = solve_intercepts(&oriented, &[0, 1]).expect("solvable");
assert!((intercepts[0] - 2.0).abs() < 1e-9, "got {:?}", intercepts);
assert!((intercepts[1] - 3.0).abs() < 1e-9, "got {:?}", intercepts);
}
#[test]
fn solve_intercepts_singular_matrix_returns_none() {
// Two identical extremes → singular system → None.
let oriented = vec![vec![1.0, 1.0], vec![1.0, 1.0]];
assert!(solve_intercepts(&oriented, &[0, 1]).is_none());
}
#[test]
fn solve_intercepts_empty_extremes_returns_none() {
let oriented: Vec<Vec<f64>> = Vec::new();
assert!(solve_intercepts(&oriented, &[]).is_none());
}
#[test]
fn associate_picks_closest_reference_direction() {
// Two reference directions: the x-axis and the y-axis.
let refs = vec![vec![1.0, 0.0], vec![0.0, 1.0]];
// A point near the x-axis associates with reference 0;
// a point near the y-axis associates with reference 1.
let normalized = vec![vec![1.0, 0.05], vec![0.05, 1.0]];
let (assoc, dist) = associate(&normalized, &refs, 2);
assert_eq!(assoc[0], 0);
assert_eq!(assoc[1], 1);
// Perpendicular distance from (1, 0.05) to the x-axis is 0.05.
assert!((dist[0] - 0.05).abs() < 1e-9, "dist0 = {}", dist[0]);
assert!((dist[1] - 0.05).abs() < 1e-9, "dist1 = {}", dist[1]);
}
#[test]
fn associate_point_on_reference_line_has_zero_distance() {
let refs = vec![vec![1.0, 0.0]];
// (3, 0) lies exactly on the x-axis direction → perp distance 0.
let normalized = vec![vec![3.0, 0.0]];
let (assoc, dist) = associate(&normalized, &refs, 2);
assert_eq!(assoc[0], 0);
assert!(dist[0].abs() < 1e-9, "dist = {}", dist[0]);
}
}
#[cfg(test)]
mod tests {
use super::*;
+28
View File
@@ -345,4 +345,32 @@ 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 worse_than_feasibility_and_direction() {
let feasible = Evaluation::new(vec![100.0]);
let infeasible = Evaluation::constrained(vec![0.0], 1.0);
// infeasible is worse than feasible regardless of objective.
assert!(worse_than(&infeasible, &feasible, Direction::Minimize));
assert!(!worse_than(&feasible, &infeasible, Direction::Minimize));
// two feasible, minimize: larger objective is worse.
let lo = Evaluation::new(vec![1.0]);
let hi = Evaluation::new(vec![2.0]);
assert!(worse_than(&hi, &lo, Direction::Minimize));
assert!(!worse_than(&lo, &hi, Direction::Minimize));
// maximize inverts.
assert!(worse_than(&lo, &hi, Direction::Maximize));
// equal → not worse.
let eq = Evaluation::new(vec![1.0]);
assert!(!worse_than(&lo, &eq, Direction::Minimize));
// two infeasible: larger violation is worse.
let v_lo = Evaluation::constrained(vec![0.0], 0.2);
let v_hi = Evaluation::constrained(vec![0.0], 0.8);
assert!(worse_than(&v_hi, &v_lo, Direction::Minimize));
}
}
+29
View File
@@ -418,4 +418,33 @@ mod tests {
let mut opt = make_optimizer(0);
let _ = opt.run(&SchafferN1);
}
// ---- Mutation-test pinned helpers --------------------------------------
use crate::core::objective::Direction;
#[test]
fn best_index_minimize_picks_smallest() {
let v = [3.0, 1.0, 4.0, 1.5];
assert_eq!(best_index(&v, Direction::Minimize), 1);
}
#[test]
fn best_index_maximize_picks_largest() {
let v = [3.0, 1.0, 4.0, 1.5];
assert_eq!(best_index(&v, Direction::Maximize), 2);
}
#[test]
fn best_index_keeps_first_on_tie() {
// Strict comparison → the earliest index of a tied extreme wins.
let v = [1.0, 1.0, 1.0];
assert_eq!(best_index(&v, Direction::Minimize), 0);
assert_eq!(best_index(&v, Direction::Maximize), 0);
}
#[test]
fn best_index_single_element() {
assert_eq!(best_index(&[42.0], Direction::Minimize), 0);
}
}