use std::collections::HashMap;
use rand::{Rng, RngCore, SeedableRng};
use rand_chacha::ChaCha8Rng;
use crate::traits::{SearchSpace, UpdateContext, Velocity};
#[derive(Debug, Clone)]
pub struct MopsoParams {
pub n_particles: usize,
pub max_iterations: usize,
pub archive_size: usize,
pub seed: Option<u64>,
pub mutation_rate: f64,
pub grid_divisions: Option<usize>,
}
impl Default for MopsoParams {
fn default() -> Self {
Self {
n_particles: 100,
max_iterations: 100,
archive_size: 100,
seed: None,
mutation_rate: 0.1,
grid_divisions: None,
}
}
}
#[derive(Debug, Clone)]
pub struct MoSolution<T> {
pub position: Vec<T>,
pub objectives: Vec<f64>,
}
#[derive(Debug, Clone)]
pub struct MopsoResult<T> {
pub front: Vec<MoSolution<T>>,
}
impl<T> MopsoResult<T> {
pub fn hypervolume(&self, reference: Option<&[f64]>) -> f64 {
let objs: Vec<Vec<f64>> = self.front.iter().map(|s| s.objectives.clone()).collect();
match reference {
Some(r) => hypervolume(&objs, r),
None => hypervolume(&objs, &nadir_reference(&objs)),
}
}
}
pub fn nadir_reference(objs: &[Vec<f64>]) -> Vec<f64> {
if objs.is_empty() {
return Vec::new();
}
let m = objs[0].len();
(0..m)
.map(|k| {
let mut lo = f64::INFINITY;
let mut hi = f64::NEG_INFINITY;
for o in objs {
lo = lo.min(o[k]);
hi = hi.max(o[k]);
}
let span = hi - lo;
hi + if span > 0.0 { 0.1 * span } else { 1.0 }
})
.collect()
}
pub fn dominates(a: &[f64], b: &[f64]) -> bool {
let mut strictly_better = false;
for (x, y) in a.iter().zip(b) {
if x > y {
return false;
}
if x < y {
strictly_better = true;
}
}
strictly_better
}
fn non_dominated(pts: &[Vec<f64>]) -> Vec<Vec<f64>> {
let mut out: Vec<Vec<f64>> = Vec::new();
for (i, p) in pts.iter().enumerate() {
let dominated = pts
.iter()
.enumerate()
.any(|(j, q)| j != i && dominates(q, p));
if !dominated && !out.iter().any(|o| o == p) {
out.push(p.clone());
}
}
out
}
pub fn hypervolume(front: &[Vec<f64>], reference: &[f64]) -> f64 {
let pts: Vec<Vec<f64>> = front
.iter()
.filter(|p| p.len() == reference.len() && p.iter().zip(reference).all(|(x, r)| x < r))
.cloned()
.collect();
wfg(&non_dominated(&pts), reference)
}
fn wfg(pl: &[Vec<f64>], reference: &[f64]) -> f64 {
(0..pl.len()).map(|k| exclhv(pl, k, reference)).sum()
}
fn exclhv(pl: &[Vec<f64>], k: usize, reference: &[f64]) -> f64 {
let incl: f64 = pl[k]
.iter()
.zip(reference)
.map(|(x, r)| (r - x).max(0.0))
.product();
incl - wfg(&non_dominated(&limit_set(pl, k)), reference)
}
fn limit_set(pl: &[Vec<f64>], k: usize) -> Vec<Vec<f64>> {
pl[k + 1..]
.iter()
.map(|q| pl[k].iter().zip(q).map(|(a, b)| a.max(*b)).collect())
.collect()
}
fn grid_cells(objs: &[Vec<f64>], divisions: usize) -> Vec<Vec<usize>> {
if objs.is_empty() {
return Vec::new();
}
let m = objs[0].len();
let div = divisions.max(1);
let mut lo = vec![f64::INFINITY; m];
let mut hi = vec![f64::NEG_INFINITY; m];
for o in objs {
for k in 0..m {
lo[k] = lo[k].min(o[k]);
hi[k] = hi[k].max(o[k]);
}
}
objs.iter()
.map(|o| {
(0..m)
.map(|k| {
let span = hi[k] - lo[k];
if span <= 0.0 {
0
} else {
let idx = ((o[k] - lo[k]) / span * div as f64) as usize;
idx.min(div - 1)
}
})
.collect()
})
.collect()
}
fn cell_populations(cells: &[Vec<usize>]) -> Vec<usize> {
let mut counts: HashMap<&Vec<usize>, usize> = HashMap::new();
for c in cells {
*counts.entry(c).or_insert(0) += 1;
}
cells.iter().map(|c| counts[c]).collect()
}
fn crowding_distance(objs: &[Vec<f64>]) -> Vec<f64> {
let n = objs.len();
let mut dist = vec![0.0_f64; n];
if n <= 2 {
return vec![f64::INFINITY; n];
}
let m = objs[0].len();
#[allow(clippy::needless_range_loop)]
for k in 0..m {
let mut idx: Vec<usize> = (0..n).collect();
idx.sort_by(|&a, &b| {
objs[a][k]
.partial_cmp(&objs[b][k])
.unwrap_or(std::cmp::Ordering::Equal)
});
dist[idx[0]] = f64::INFINITY;
dist[idx[n - 1]] = f64::INFINITY;
let range = objs[idx[n - 1]][k] - objs[idx[0]][k];
if range > 0.0 {
for j in 1..n - 1 {
dist[idx[j]] += (objs[idx[j + 1]][k] - objs[idx[j - 1]][k]) / range;
}
}
}
dist
}
struct Archive {
members: Vec<(Vec<f64>, Vec<f64>)>, max_size: usize,
grid_divisions: Option<usize>,
}
impl Archive {
fn new(max_size: usize, grid_divisions: Option<usize>) -> Self {
Self {
members: Vec::new(),
max_size,
grid_divisions,
}
}
fn insert(&mut self, pos: &[f64], obj: &[f64]) {
if self.members.iter().any(|(_, o)| dominates(o, obj)) {
return;
}
self.members.retain(|(_, o)| !dominates(obj, o));
if self.members.iter().any(|(_, o)| o == obj) {
return;
}
self.members.push((pos.to_vec(), obj.to_vec()));
}
fn prune_and_scores(&mut self) -> Vec<f64> {
match self.grid_divisions {
None => self.prune_and_crowding(),
Some(div) => self.prune_and_grid(div),
}
}
fn prune_and_crowding(&mut self) -> Vec<f64> {
let objs: Vec<Vec<f64>> = self.members.iter().map(|(_, o)| o.clone()).collect();
let cd = crowding_distance(&objs);
if self.members.len() <= self.max_size {
return cd;
}
let mut order: Vec<usize> = (0..self.members.len()).collect();
order.sort_by(|&a, &b| {
cd[b]
.partial_cmp(&cd[a])
.unwrap_or(std::cmp::Ordering::Equal)
});
order.truncate(self.max_size);
order.sort_unstable();
self.members = order.iter().map(|&i| self.members[i].clone()).collect();
let objs: Vec<Vec<f64>> = self.members.iter().map(|(_, o)| o.clone()).collect();
crowding_distance(&objs)
}
fn prune_and_grid(&mut self, div: usize) -> Vec<f64> {
while self.members.len() > self.max_size {
let objs: Vec<Vec<f64>> = self.members.iter().map(|(_, o)| o.clone()).collect();
let pops = cell_populations(&grid_cells(&objs, div));
let victim = (0..pops.len()).max_by_key(|&i| pops[i]).unwrap();
self.members.remove(victim);
}
let objs: Vec<Vec<f64>> = self.members.iter().map(|(_, o)| o.clone()).collect();
cell_populations(&grid_cells(&objs, div))
.into_iter()
.map(|p| 1.0 / p as f64)
.collect()
}
}
pub struct Mopso<S, V>
where
S: SearchSpace,
V: Velocity,
{
space: S,
velocity: V,
params: MopsoParams,
}
impl<S, V> Mopso<S, V>
where
S: SearchSpace,
V: Velocity,
{
pub fn new(space: S, velocity: V, params: MopsoParams) -> Self {
Self {
space,
velocity,
params,
}
}
pub fn minimize<F>(&self, mut objectives: F) -> MopsoResult<S::Scalar>
where
F: FnMut(&[S::Scalar]) -> Vec<f64>,
{
let mut rng: Box<dyn RngCore> = match self.params.seed {
Some(s) => Box::new(ChaCha8Rng::seed_from_u64(s)),
None => Box::new(ChaCha8Rng::from_entropy()),
};
let mut positions = Vec::with_capacity(self.params.n_particles);
let mut velocities = Vec::with_capacity(self.params.n_particles);
let mut objs = Vec::with_capacity(self.params.n_particles);
for _ in 0..self.params.n_particles {
let pos = self.space.sample(rng.as_mut());
let vel = self.space.sample_velocity(rng.as_mut());
let o = objectives(&self.space.decode(&pos));
positions.push(pos);
velocities.push(vel);
objs.push(o);
}
let mut pbest_pos = positions.clone();
let mut pbest_obj = objs.clone();
let mut archive = Archive::new(self.params.archive_size, self.params.grid_divisions);
for (p, o) in positions.iter().zip(&objs) {
archive.insert(p, o);
}
let span = self.space.span();
let max_iter = self.params.max_iterations.max(1) as f64;
for iter in 0..self.params.max_iterations {
let scores = archive.prune_and_scores();
let leaders: Vec<Vec<f64>> = archive.members.iter().map(|(p, _)| p.clone()).collect();
for i in 0..self.params.n_particles {
let leader_pos = {
let a = rng.gen_range(0..leaders.len());
let b = rng.gen_range(0..leaders.len());
if scores[a] >= scores[b] {
leaders[a].clone()
} else {
leaders[b].clone()
}
};
let new_vel = {
let ctx = UpdateContext {
position: &positions[i],
velocity: &velocities[i],
personal_best: &pbest_pos[i],
neighbor_best: &leader_pos,
neighbor_bests: &[],
iteration: iter,
max_iterations: self.params.max_iterations,
};
self.velocity.update(&ctx, rng.as_mut())
};
for (x, dv) in positions[i].iter_mut().zip(&new_vel) {
*x += dv;
}
velocities[i] = new_vel;
self.space.clamp(&mut positions[i]);
if self.params.mutation_rate > 0.0 && !span.is_empty() {
let decay = 1.0 - iter as f64 / max_iter;
if rng.gen::<f64>() < self.params.mutation_rate * decay {
let d = rng.gen_range(0..span.len());
let (lo, hi) = span[d];
let half = (hi - lo) * decay * 0.5;
if half > 0.0 {
let nlo = (positions[i][d] - half).max(lo);
let nhi = (positions[i][d] + half).min(hi);
if nhi > nlo {
positions[i][d] = rng.gen_range(nlo..=nhi);
}
}
}
}
let new_obj = objectives(&self.space.decode(&positions[i]));
if dominates(&new_obj, &pbest_obj[i]) {
pbest_pos[i] = positions[i].clone();
pbest_obj[i] = new_obj.clone();
} else if !dominates(&pbest_obj[i], &new_obj) && rng.gen::<bool>() {
pbest_pos[i] = positions[i].clone();
pbest_obj[i] = new_obj.clone();
}
archive.insert(&positions[i], &new_obj);
}
}
archive.prune_and_scores();
let front = archive
.members
.into_iter()
.map(|(pos, objectives)| MoSolution {
position: self.space.decode(&pos),
objectives,
})
.collect();
MopsoResult { front }
}
}