use super::nsga2::{crowding_distance, dominates};
use crate::problem::MultiProblem;
use crate::rng::Rng;
use crate::solution::{MultiSolution, ParetoFront};
use crate::termination::Termination;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum PaddsSelection {
Random,
Crowding,
Hypervolume,
}
#[derive(Debug, Clone, Copy)]
pub struct Padds {
pub r: f64,
pub selection: PaddsSelection,
pub seed: u64,
}
impl Default for Padds {
fn default() -> Self {
Padds {
r: 0.2,
selection: PaddsSelection::Hypervolume,
seed: 42,
}
}
}
const INFEASIBLE: f64 = 1e30;
impl Padds {
pub fn optimize(&self, problem: &dyn MultiProblem, term: &Termination) -> ParetoFront {
crate::problem::validate_multi(problem)
.unwrap_or_else(|e| panic!("Padds: invalid problem: {e}"));
let bounds = problem.bounds();
let dim = bounds.len();
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 = INFEASIBLE;
}
}
o
};
let mut evaluations = 0usize;
if term.max_evaluations == 0 {
return ParetoFront {
solutions: Vec::new(),
evaluations,
};
}
let x0: Vec<f64> = bounds
.iter()
.map(|&(lo, hi)| rng.uniform_in(lo, hi))
.collect();
let o0 = eval(&x0);
evaluations += 1;
let mut archive: Vec<MultiSolution> = vec![MultiSolution {
x: x0,
objectives: o0,
}];
let m = (term.max_evaluations.saturating_sub(1)).max(2) as f64;
let mut i = 1usize;
let mut candidate = vec![0.0; dim];
while evaluations < term.max_evaluations {
let cur = self.select(&archive, &mut rng);
let base = &archive[cur].x;
let p = 1.0 - (i as f64).ln() / m.ln();
candidate.copy_from_slice(base);
let mut perturbed = 0;
for (j, &(lo, hi)) in bounds.iter().enumerate() {
if rng.uniform() < p {
candidate[j] = perturb(base[j], lo, hi, self.r, &mut rng);
perturbed += 1;
}
}
if perturbed == 0 {
let j = rng.index(dim);
let (lo, hi) = bounds[j];
candidate[j] = perturb(base[j], lo, hi, self.r, &mut rng);
}
let objs = eval(&candidate);
evaluations += 1;
i += 1;
let dominated_or_dup = archive
.iter()
.any(|s| dominates(&s.objectives, &objs) || s.objectives == objs);
if !dominated_or_dup {
archive.retain(|s| !dominates(&objs, &s.objectives));
archive.push(MultiSolution {
x: candidate.clone(),
objectives: objs,
});
}
}
ParetoFront {
solutions: archive,
evaluations,
}
}
fn select(&self, archive: &[MultiSolution], rng: &mut Rng) -> usize {
let n = archive.len();
if n == 1 {
return 0;
}
let weights: Vec<f64> = match self.selection {
PaddsSelection::Random => return rng.index(n),
PaddsSelection::Crowding => {
let all: Vec<usize> = (0..n).collect();
let mut c = crowding_distance(&all, archive);
let max_finite = c
.iter()
.copied()
.filter(|v| v.is_finite())
.fold(0.0, f64::max);
let cap = if max_finite > 0.0 {
2.0 * max_finite
} else {
1.0
};
for v in &mut c {
if !v.is_finite() {
*v = cap;
}
}
c
}
PaddsSelection::Hypervolume => {
let mobj = archive[0].objectives.len();
let mut lo = vec![f64::INFINITY; mobj];
let mut hi = vec![f64::NEG_INFINITY; mobj];
for s in archive {
for (j, &v) in s.objectives.iter().enumerate() {
lo[j] = lo[j].min(v);
hi[j] = hi[j].max(v);
}
}
let normalized: Vec<Vec<f64>> = archive
.iter()
.map(|s| {
(0..mobj)
.map(|j| {
let span = hi[j] - lo[j];
if span > 0.0 && span.is_finite() {
(s.objectives[j] - lo[j]) / span
} else {
0.0
}
})
.collect()
})
.collect();
let r = vec![1.1; mobj];
crate::indicators::hv_contributions(&normalized, &r)
}
};
let total: f64 = weights.iter().sum();
if !(total.is_finite() && total > 0.0) {
return rng.index(n);
}
let u = rng.uniform() * total;
let mut acc = 0.0;
for (idx, w) in weights.iter().enumerate() {
acc += w;
if u < acc {
return idx;
}
}
n - 1
}
}
fn perturb(x: f64, lo: f64, hi: f64, r: f64, rng: &mut Rng) -> f64 {
let range = hi - lo;
let mut xn = x + range * r * rng.normal();
if xn < lo {
xn = lo + (lo - xn);
if xn > hi {
xn = lo;
}
} else if xn > hi {
xn = hi - (xn - hi);
if xn < lo {
xn = hi;
}
}
xn.clamp(lo, hi)
}