diff --git a/src/algorithms/rvea.rs b/src/algorithms/rvea.rs index 5e4e7ae..87806d1 100644 --- a/src/algorithms/rvea.rs +++ b/src/algorithms/rvea.rs @@ -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::().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}"); + } } diff --git a/src/algorithms/simulated_annealing.rs b/src/algorithms/simulated_annealing.rs index f021651..dc5f7d9 100644 --- a/src/algorithms/simulated_annealing.rs +++ b/src/algorithms/simulated_annealing.rs @@ -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)); + } } diff --git a/src/algorithms/snes.rs b/src/algorithms/snes.rs index a1e1916..7182673 100644 --- a/src/algorithms/snes.rs +++ b/src/algorithms/snes.rs @@ -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)); + } } diff --git a/src/algorithms/spea2.rs b/src/algorithms/spea2.rs index b9fcb32..118ae4b 100644 --- a/src/algorithms/spea2.rs +++ b/src/algorithms/spea2.rs @@ -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"); + } } diff --git a/src/algorithms/tabu_search.rs b/src/algorithms/tabu_search.rs index bf53533..128cdac 100644 --- a/src/algorithms/tabu_search.rs +++ b/src/algorithms/tabu_search.rs @@ -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)); + } } diff --git a/src/algorithms/tlbo.rs b/src/algorithms/tlbo.rs index 04fdea8..daeef4b 100644 --- a/src/algorithms/tlbo.rs +++ b/src/algorithms/tlbo.rs @@ -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); + } } diff --git a/src/algorithms/tpe.rs b/src/algorithms/tpe.rs index 9a7a805..b02646a 100644 --- a/src/algorithms/tpe.rs +++ b/src/algorithms/tpe.rs @@ -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()); + } } diff --git a/src/algorithms/umda.rs b/src/algorithms/umda.rs index 23f5e1f..520da3e 100644 --- a/src/algorithms/umda.rs +++ b/src/algorithms/umda.rs @@ -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)); + } }