use forge_core::problem::multi_func;
use forge_core::testfn::{Dtlz2, Zdt1};
use forge_core::{NsgaII, NsgaIII, SmsEmoa, Termination};
fn mean_distance_to_zdt1_front(front: &forge_core::ParetoFront) -> f64 {
let mut total = 0.0;
for o in front.objective_vectors() {
let (f1, f2) = (o[0], o[1]);
total += (f2 - Zdt1::front_f2(f1)).abs();
}
total / front.len() as f64
}
#[test]
fn nsga2_converges_to_zdt1_front() {
let problem = Zdt1::new(10);
let nsga = NsgaII {
pop_size: 100,
..NsgaII::default()
};
let front = nsga.optimize(&problem, &Termination::budget(20_000));
assert!(front.len() >= 50, "front too small: {}", front.len());
let gd = mean_distance_to_zdt1_front(&front);
assert!(gd < 0.02, "mean distance to ZDT1 front too high: {gd}");
let f1s: Vec<f64> = front.objective_vectors().map(|o| o[0]).collect();
let (min, max) = f1s
.iter()
.fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), &v| {
(lo.min(v), hi.max(v))
});
assert!(min < 0.1 && max > 0.9, "poor spread: f1 in [{min}, {max}]");
}
#[test]
fn nsga2_front_is_internally_non_dominated() {
let problem = Zdt1::new(8);
let front = NsgaII::default().optimize(&problem, &Termination::budget(8000));
let objs: Vec<&[f64]> = front.objective_vectors().collect();
for (i, a) in objs.iter().enumerate() {
for (j, b) in objs.iter().enumerate() {
if i == j {
continue;
}
let dominates = a[0] <= b[0] && a[1] <= b[1] && (a[0] < b[0] || a[1] < b[1]);
assert!(!dominates, "solution {i} dominates {j} on the front");
}
}
}
#[test]
fn nsga2_is_deterministic() {
let problem = Zdt1::new(6);
let t = Termination::budget(5000);
let a = NsgaII::default().optimize(&problem, &t);
let b = NsgaII::default().optimize(&problem, &t);
assert_eq!(a.solutions, b.solutions);
assert_eq!(a.evaluations, b.evaluations);
}
#[test]
fn nsga2_solves_schaffer_n1() {
let sch = multi_func(vec![(-5.0, 5.0)], 2, |x| {
vec![x[0] * x[0], (x[0] - 2.0).powi(2)]
});
let front = NsgaII {
pop_size: 60,
..NsgaII::default()
}
.optimize(&sch, &Termination::budget(6000));
for s in &front.solutions {
assert!(
(-0.05..=2.05).contains(&s.x[0]),
"x outside Pareto set: {}",
s.x[0]
);
}
}
fn mean_distance_to_unit_sphere(front: &forge_core::ParetoFront) -> f64 {
let mut total = 0.0;
for o in front.objective_vectors() {
let norm = o.iter().map(|v| v * v).sum::<f64>().sqrt();
total += (norm - 1.0).abs();
}
total / front.len() as f64
}
#[test]
fn nsga3_converges_on_dtlz2_three_objectives() {
let problem = Dtlz2::new(3, 12);
let nsga = NsgaIII {
pop_size: 92,
divisions: 12,
..NsgaIII::default()
};
let front = nsga.optimize_within(&problem, &Termination::budget(30_000));
assert!(front.len() >= 50, "front too small: {}", front.len());
let gd = mean_distance_to_unit_sphere(&front);
assert!(gd < 0.03, "mean distance to DTLZ2 sphere too high: {gd}");
for k in 0..3 {
let reaches_axis = front
.objective_vectors()
.any(|o| o[k] > 0.9 && (0..3).filter(|&j| j != k).all(|j| o[j] < 0.2));
assert!(reaches_axis, "front does not reach axis {k}");
}
}
#[test]
fn nsga3_front_is_internally_non_dominated() {
let problem = Dtlz2::new(3, 10);
let front = NsgaIII::default().optimize_within(&problem, &Termination::budget(12_000));
let objs: Vec<&[f64]> = front.objective_vectors().collect();
for (i, a) in objs.iter().enumerate() {
for (j, b) in objs.iter().enumerate() {
if i == j {
continue;
}
let dominates =
a.iter().zip(*b).all(|(x, y)| x <= y) && a.iter().zip(*b).any(|(x, y)| x < y);
assert!(
!dominates,
"solution {i} dominates {j} on the NSGA-III front"
);
}
}
}
#[test]
fn nsga3_is_deterministic() {
let problem = Dtlz2::new(3, 9);
let t = Termination::budget(8000);
let a = NsgaIII::default().optimize_within(&problem, &t);
let b = NsgaIII::default().optimize_within(&problem, &t);
assert_eq!(a.solutions, b.solutions);
assert_eq!(a.evaluations, b.evaluations);
}
#[test]
fn sms_emoa_converges_to_zdt1_front() {
let problem = Zdt1::new(10);
let front = SmsEmoa::default().optimize(&problem, &Termination::budget(20_000));
assert!(front.len() >= 50, "front too small: {}", front.len());
let gd = mean_distance_to_zdt1_front(&front);
assert!(gd < 0.02, "mean distance to ZDT1 front too high: {gd}");
let f1s: Vec<f64> = front.objective_vectors().map(|o| o[0]).collect();
let (min, max) = f1s
.iter()
.fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), &v| {
(lo.min(v), hi.max(v))
});
assert!(min < 0.1 && max > 0.9, "poor spread: f1 in [{min}, {max}]");
}
#[test]
fn sms_emoa_is_deterministic() {
let problem = Zdt1::new(6);
let t = Termination::budget(4000);
let a = SmsEmoa::default().optimize(&problem, &t);
let b = SmsEmoa::default().optimize(&problem, &t);
assert_eq!(a.solutions, b.solutions);
assert_eq!(a.evaluations, b.evaluations);
}
#[test]
fn nsga2_respects_budget_roughly() {
let front = NsgaII::default().optimize(&Zdt1::new(5), &Termination::budget(3000));
assert!(
front.evaluations < 3000 + 100,
"spent {}",
front.evaluations
);
}
#[test]
fn moead_converges_to_zdt1_front() {
use forge_core::Moead;
let problem = Zdt1::new(10);
let front = Moead::default().optimize(&problem, &Termination::budget(20_000));
assert!(front.len() >= 50, "front too small: {}", front.len());
let gd = mean_distance_to_zdt1_front(&front);
assert!(gd < 0.02, "mean distance to ZDT1 front too high: {gd}");
let f1s: Vec<f64> = front.objective_vectors().map(|o| o[0]).collect();
let (min, max) = f1s
.iter()
.fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), &v| {
(lo.min(v), hi.max(v))
});
assert!(min < 0.1 && max > 0.9, "poor spread: f1 in [{min}, {max}]");
}
#[test]
fn moead_is_deterministic() {
use forge_core::Moead;
let problem = Zdt1::new(6);
let t = Termination::budget(5000);
let a = Moead::default().optimize(&problem, &t);
let b = Moead::default().optimize(&problem, &t);
assert_eq!(a.solutions, b.solutions);
assert_eq!(a.evaluations, b.evaluations);
}
#[test]
fn padds_converges_on_zdt1() {
use forge_core::Padds;
let problem = Zdt1::new(10);
let front = Padds::default().optimize(&problem, &Termination::budget(20_000));
assert!(front.len() >= 20, "archive too small: {}", front.len());
let gd = mean_distance_to_zdt1_front(&front);
assert!(gd < 0.05, "mean distance to ZDT1 front too high: {gd}");
let f1s: Vec<f64> = front.objective_vectors().map(|o| o[0]).collect();
let (min, max) = f1s
.iter()
.fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), &v| {
(lo.min(v), hi.max(v))
});
assert!(min < 0.2 && max > 0.8, "poor spread: f1 in [{min}, {max}]");
}
#[test]
fn padds_front_is_internally_non_dominated_and_deterministic() {
use forge_core::Padds;
let problem = Zdt1::new(8);
let t = Termination::budget(6000);
let a = Padds::default().optimize(&problem, &t);
let b = Padds::default().optimize(&problem, &t);
assert_eq!(a.solutions, b.solutions);
let objs: Vec<&[f64]> = a.objective_vectors().collect();
for (i, x) in objs.iter().enumerate() {
for (j, y) in objs.iter().enumerate() {
if i == j {
continue;
}
let dominates = x[0] <= y[0] && x[1] <= y[1] && (x[0] < y[0] || x[1] < y[1]);
assert!(!dominates, "archive member {i} dominates {j}");
}
}
}