use ndarray::{Array1, Array2, Axis, array, stack};
use moors::{
IbeaBuilder,
duplicates::CloseDuplicatesCleaner,
genetic::PopulationMOO,
impl_constraints_fn,
operators::{GaussianMutation, RandomSamplingFloat, SimulatedBinaryCrossover},
};
fn fitness_expo2(pop: &Array2<f64>) -> Array2<f64> {
let n = pop.ncols();
assert!(n >= 2, "EXPO2 requires at least 2 decision variables");
let f1 = pop.column(0).to_owned();
let mut tail_sum = Array1::<f64>::zeros(pop.nrows());
if n > 1 {
for j in 1..n {
let col = pop.column(j);
for i in 0..pop.nrows() {
tail_sum[i] += col[i];
}
}
}
let mut g = tail_sum.clone();
let coef = if n > 1 { 9.0 / ((n as f64) - 1.0) } else { 0.0 };
for i in 0..g.len() {
g[i] = 1.0 + coef * tail_sum[i];
}
let mut f2 = Array1::<f64>::zeros(pop.nrows());
for i in 0..pop.nrows() {
f2[i] = g[i] * (-5.0 * f1[i] / g[i]).exp();
}
stack(Axis(1), &[f1.view(), f2.view()]).expect("stack f1,f2 failed")
}
fn expo2_true_front(num_points: usize) -> Array2<f64> {
assert!(num_points >= 2);
let mut f1 = Array1::<f64>::zeros(num_points);
let denom = (num_points as f64) - 1.0;
for i in 0..num_points {
f1[i] = (i as f64) / denom;
}
let mut f2 = Array1::<f64>::zeros(num_points);
for i in 0..num_points {
f2[i] = (-5.0 * f1[i]).exp();
}
stack(Axis(1), &[f1.view(), f2.view()]).expect("stack true front failed")
}
fn gd_rms_to_front(solutions: &Array2<f64>, ref_front: &Array2<f64>) -> f64 {
assert_eq!(solutions.ncols(), ref_front.ncols());
let mut acc = 0.0;
for i in 0..solutions.nrows() {
let sx = solutions[[i, 0]];
let sy = solutions[[i, 1]];
let mut best = f64::INFINITY;
for j in 0..ref_front.nrows() {
let rx = ref_front[[j, 0]];
let ry = ref_front[[j, 1]];
let dx = sx - rx;
let dy = sy - ry;
let d = (dx * dx + dy * dy).sqrt();
if d < best {
best = d;
}
}
acc += best * best;
}
(acc / (solutions.nrows() as f64)).sqrt()
}
fn best_front_to_array2(pop: &PopulationMOO) -> Array2<f64> {
let front = pop.best();
let m = pop.fitness.ncols();
let mut out = Array2::<f64>::zeros((front.len(), m));
for i in 0..front.len() {
let ind = front.get(i);
out[[i, 0]] = ind.fitness[0];
out[[i, 1]] = ind.fitness[1];
}
out
}
#[test]
fn test_ibea_expo2() {
let hv_reference = array![6.0, 6.0];
let kappa = 0.05;
impl_constraints_fn!(MyConstr, lower_bound = 0.0, upper_bound = 1.0);
let mut algorithm = IbeaBuilder::default()
.sampler(RandomSamplingFloat::new(0.0, 1.0))
.crossover(SimulatedBinaryCrossover::new(15.0))
.mutation(GaussianMutation::new(0.05, 0.10))
.reference(hv_reference.clone())
.kappa(kappa)
.duplicates_cleaner(CloseDuplicatesCleaner::new(1e-6))
.fitness_fn(fitness_expo2)
.constraints_fn(MyConstr)
.num_vars(30)
.population_size(200)
.num_offsprings(200)
.num_iterations(500)
.mutation_rate(0.1)
.crossover_rate(0.9)
.keep_infeasible(false)
.verbose(false)
.seed(1729)
.build()
.expect("failed to build IBEA-H (EXPO2)");
algorithm.run().expect("IBEA run failed");
let population = algorithm.population.expect("population must exist");
let obtained_front = best_front_to_array2(&population);
let true_front = expo2_true_front(2000);
let gd = gd_rms_to_front(&obtained_front, &true_front);
assert!(
gd < 0.03,
"EXPO2 IBEA-H GD too high: {:.6} (expected < 0.03)",
gd
);
let mut avg_vert_dev = 0.0;
for i in 0..obtained_front.nrows() {
let f1 = obtained_front[[i, 0]];
let f2 = obtained_front[[i, 1]];
let f2_true = (-5.0 * f1).exp();
avg_vert_dev += (f2 - f2_true).abs();
assert!(f1 >= 0.0 && f1 <= hv_reference[0] + 1e-9);
assert!(f2 >= 0.0 && f2 <= hv_reference[1] + 1e-9);
}
avg_vert_dev /= obtained_front.nrows().max(1) as f64;
assert!(
avg_vert_dev < 0.05,
"Average vertical deviation too large: {:.6} (expected < 0.05)",
avg_vert_dev
);
}