use super::nsga2::{fast_non_dominated_sort, mutate, sbx};
use super::nsga3::das_dennis;
use crate::problem::MultiProblem;
use crate::rng::Rng;
use crate::solution::{MultiSolution, ParetoFront};
use crate::termination::Termination;
#[derive(Debug, Clone, Copy)]
pub struct Moead {
pub divisions: usize,
pub neighbors: usize,
pub delta: f64,
pub max_replacements: usize,
pub crossover_eta: f64,
pub crossover_prob: f64,
pub mutation_eta: f64,
pub mutation_prob: Option<f64>,
pub seed: u64,
}
impl Default for Moead {
fn default() -> Self {
Moead {
divisions: 99,
neighbors: 20,
delta: 0.9,
max_replacements: 2,
crossover_eta: 20.0,
crossover_prob: 1.0,
mutation_eta: 20.0,
mutation_prob: None,
seed: 42,
}
}
}
impl Moead {
pub fn optimize(&self, problem: &dyn MultiProblem, term: &Termination) -> ParetoFront {
crate::problem::validate_multi(problem)
.unwrap_or_else(|e| panic!("Moead: invalid problem: {e}"));
let bounds = problem.bounds();
let dim = bounds.len();
let m = problem.n_objectives();
let pm = self.mutation_prob.unwrap_or(1.0 / dim as f64);
let mut rng = Rng::new(self.seed);
let weights: Vec<Vec<f64>> = das_dennis(m, self.divisions.max(1))
.into_iter()
.map(|w| w.into_iter().map(|v| v.max(1e-6)).collect())
.collect();
let n = weights.len();
let t = self.neighbors.clamp(2, n);
let neighborhoods: Vec<Vec<usize>> = (0..n)
.map(|i| {
let mut order: Vec<usize> = (0..n).collect();
order.sort_by(|&a, &b| {
let da = dist2(&weights[i], &weights[a]);
let db = dist2(&weights[i], &weights[b]);
da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal)
});
order.truncate(t);
order
})
.collect();
let eval = |x: &[f64]| -> Vec<f64> {
let mut o = problem.objectives(x);
for v in &mut o {
if !v.is_finite() {
*v = f64::INFINITY;
}
}
o
};
let mut pop: Vec<MultiSolution> = Vec::with_capacity(n);
let mut ideal = vec![f64::INFINITY; m];
let mut evaluations = 0usize;
for _ in 0..n {
if evaluations >= term.max_evaluations {
break;
}
let x: Vec<f64> = bounds
.iter()
.map(|&(lo, hi)| rng.uniform_in(lo, hi))
.collect();
let objectives = eval(&x);
evaluations += 1;
for (z, &o) in ideal.iter_mut().zip(&objectives) {
if o.is_finite() {
*z = z.min(o);
}
}
pop.push(MultiSolution { x, objectives });
}
if pop.is_empty() {
return ParetoFront {
solutions: Vec::new(),
evaluations,
};
}
let full_pop = pop.len() == n;
while full_pop && evaluations < term.max_evaluations {
#[allow(clippy::needless_range_loop)] for i in 0..n {
if evaluations >= term.max_evaluations {
break;
}
let use_nb = rng.uniform() < self.delta;
let pick = |rng: &mut Rng| -> usize {
if use_nb {
neighborhoods[i][rng.index(t)]
} else {
rng.index(n)
}
};
let a = pick(&mut rng);
let b = pick(&mut rng);
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;
for (z, &o) in ideal.iter_mut().zip(&objectives) {
if o.is_finite() {
*z = z.min(o);
}
}
let child = MultiSolution {
x: child,
objectives,
};
let pool: &[usize] = if use_nb {
&neighborhoods[i]
} else {
&[]
};
let mut replaced = 0usize;
if pool.is_empty() {
for _ in 0..t {
if replaced >= self.max_replacements {
break;
}
let j = rng.index(n);
replaced += try_replace(&mut pop[j], &child, &weights[j], &ideal) as usize;
}
} else {
let mut order: Vec<usize> = pool.to_vec();
shuffle(&mut order, &mut rng);
for &j in &order {
if replaced >= self.max_replacements {
break;
}
replaced += try_replace(&mut pop[j], &child, &weights[j], &ideal) as usize;
}
}
}
}
let fronts = fast_non_dominated_sort(&pop);
let solutions = fronts[0].iter().map(|&i| pop[i].clone()).collect();
ParetoFront {
solutions,
evaluations,
}
}
}
fn try_replace(
slot: &mut MultiSolution,
child: &MultiSolution,
weight: &[f64],
ideal: &[f64],
) -> bool {
if tchebycheff(&child.objectives, weight, ideal) < tchebycheff(&slot.objectives, weight, ideal)
{
*slot = child.clone();
true
} else {
false
}
}
fn tchebycheff(objectives: &[f64], weight: &[f64], ideal: &[f64]) -> f64 {
objectives
.iter()
.zip(weight)
.zip(ideal)
.map(|((&f, &w), &z)| w * (f - z).abs())
.fold(f64::NEG_INFINITY, f64::max)
}
fn dist2(a: &[f64], b: &[f64]) -> f64 {
a.iter().zip(b).map(|(x, y)| (x - y) * (x - y)).sum()
}
fn shuffle(v: &mut [usize], rng: &mut Rng) {
for i in (1..v.len()).rev() {
let j = rng.index(i + 1);
v.swap(i, j);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tchebycheff_is_weighted_max_distance() {
let g = tchebycheff(&[2.0, 3.0], &[0.5, 0.5], &[1.0, 1.0]);
assert!((g - 1.0).abs() < 1e-12); }
}