use super::nsga2::{fast_non_dominated_sort, mutate, sbx};
use crate::problem::MultiProblem;
use crate::rng::Rng;
use crate::solution::{MultiSolution, ParetoFront};
use crate::termination::Termination;
#[derive(Debug, Clone, Copy)]
pub struct SmsEmoa {
pub pop_size: usize,
pub crossover_eta: f64,
pub crossover_prob: f64,
pub mutation_eta: f64,
pub mutation_prob: Option<f64>,
pub seed: u64,
}
impl Default for SmsEmoa {
fn default() -> Self {
SmsEmoa {
pop_size: 100,
crossover_eta: 15.0,
crossover_prob: 0.9,
mutation_eta: 20.0,
mutation_prob: None,
seed: 42,
}
}
}
impl SmsEmoa {
pub fn optimize(&self, problem: &dyn MultiProblem, term: &Termination) -> ParetoFront {
let bounds = problem.bounds();
let dim = bounds.len();
let n = self.pop_size.max(2);
let pm = self.mutation_prob.unwrap_or(1.0 / dim as f64);
let mut rng = Rng::new(self.seed);
let eval = |x: &[f64]| -> Vec<f64> {
let mut o = problem.objectives(x);
for v in &mut o {
if !v.is_finite() {
*v = 1e30; }
}
o
};
let mut pop: Vec<MultiSolution> = Vec::with_capacity(n + 1);
let mut evaluations = 0;
for _ in 0..n {
let x: Vec<f64> = bounds
.iter()
.map(|&(lo, hi)| rng.uniform_in(lo, hi))
.collect();
let objectives = eval(&x);
evaluations += 1;
pop.push(MultiSolution { x, objectives });
}
while evaluations < term.max_evaluations {
let a = rng.index(n);
let b = rng.index(n);
let (mut child, _) = sbx(
&pop[a].x,
&pop[b].x,
bounds,
self.crossover_eta,
self.crossover_prob,
&mut rng,
);
mutate(&mut child, bounds, self.mutation_eta, pm, &mut rng);
let objectives = eval(&child);
evaluations += 1;
pop.push(MultiSolution {
x: child,
objectives,
});
let fronts = fast_non_dominated_sort(&pop);
let last = fronts.last().expect("at least one front");
let drop = if last.len() == 1 {
last[0]
} else {
let objs: Vec<&[f64]> =
last.iter().map(|&i| pop[i].objectives.as_slice()).collect();
last[least_contributor(&objs)]
};
pop.swap_remove(drop);
}
let fronts = fast_non_dominated_sort(&pop);
let solutions = fronts[0].iter().map(|&i| pop[i].clone()).collect();
ParetoFront {
solutions,
evaluations,
}
}
}
fn least_contributor(objs: &[&[f64]]) -> usize {
let m = objs[0].len();
let r = reference_point(objs);
if m == 2 {
least_contributor_2d(objs, &r)
} else {
least_contributor_general(objs, &r)
}
}
fn reference_point(objs: &[&[f64]]) -> Vec<f64> {
let m = objs[0].len();
(0..m)
.map(|j| {
let mut lo = f64::INFINITY;
let mut hi = f64::NEG_INFINITY;
for o in objs {
lo = lo.min(o[j]);
hi = hi.max(o[j]);
}
hi + (0.1 * (hi - lo)).max(1e-6)
})
.collect()
}
fn least_contributor_2d(objs: &[&[f64]], r: &[f64]) -> usize {
let k = objs.len();
let mut order: Vec<usize> = (0..k).collect();
order.sort_by(|&a, &b| {
objs[a][0]
.partial_cmp(&objs[b][0])
.unwrap_or(std::cmp::Ordering::Equal)
.then(
objs[a][1]
.partial_cmp(&objs[b][1])
.unwrap_or(std::cmp::Ordering::Equal),
)
});
let mut best = 0usize;
let mut best_contr = f64::INFINITY;
for pos in 0..k {
let i = order[pos];
let xi = objs[i][0];
let yi = objs[i][1];
let x_next = if pos + 1 < k {
objs[order[pos + 1]][0]
} else {
r[0]
};
let y_prev = if pos > 0 {
objs[order[pos - 1]][1]
} else {
r[1]
};
let contr = (x_next - xi) * (y_prev - yi);
if contr < best_contr {
best_contr = contr;
best = i;
}
}
best
}
fn least_contributor_general(objs: &[&[f64]], r: &[f64]) -> usize {
let full: Vec<Vec<f64>> = objs.iter().map(|o| o.to_vec()).collect();
let total = hypervolume(&full, r);
let mut best = 0usize;
let mut best_contr = f64::INFINITY;
for i in 0..objs.len() {
let without: Vec<Vec<f64>> = (0..objs.len())
.filter(|&j| j != i)
.map(|j| objs[j].to_vec())
.collect();
let contr = total - hypervolume(&without, r);
if contr < best_contr {
best_contr = contr;
best = i;
}
}
best
}
fn hypervolume(points: &[Vec<f64>], r: &[f64]) -> f64 {
if points.is_empty() {
return 0.0;
}
let mut vol = 0.0;
for i in 0..points.len() {
let incl: f64 = r
.iter()
.zip(&points[i])
.map(|(rj, pj)| (rj - pj).max(0.0))
.product();
let limited: Vec<Vec<f64>> = points[i + 1..]
.iter()
.map(|q| (0..r.len()).map(|j| points[i][j].max(q[j])).collect())
.collect();
let nd = non_dominated(&limited);
vol += incl - hypervolume(&nd, r);
}
vol
}
fn non_dominated(set: &[Vec<f64>]) -> Vec<Vec<f64>> {
let mut out = Vec::new();
for (i, p) in set.iter().enumerate() {
let dominated = set.iter().enumerate().any(|(j, q)| {
i != j && q.iter().zip(p).all(|(a, b)| a <= b) && q.iter().zip(p).any(|(a, b)| a < b)
});
if !dominated {
out.push(p.clone());
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hypervolume_known_2d_and_3d() {
let pts = vec![vec![1.0, 3.0], vec![2.0, 2.0]];
let hv = hypervolume(&pts, &[3.0, 4.0]);
assert!((hv - 3.0).abs() < 1e-9, "2D HV = {hv}");
let one = vec![vec![1.0, 1.0, 1.0]];
let hv3 = hypervolume(&one, &[2.0, 3.0, 4.0]);
assert!((hv3 - (1.0 * 2.0 * 3.0)).abs() < 1e-9, "3D HV = {hv3}");
}
#[test]
fn least_contributor_drops_the_redundant_point() {
let a = [0.0, 2.0];
let b = [1.0, 1.99];
let c = [2.0, 0.0];
let objs: Vec<&[f64]> = vec![&a, &b, &c];
assert_eq!(least_contributor(&objs), 1);
}
}