use crate::common::MultiObjectiveIndividual;
pub fn constrained_dominates(f1: &[f64], v1: f64, f2: &[f64], v2: f64) -> bool {
if v1 == 0.0 && v2 > 0.0 {
return true;
}
if v1 > 0.0 && v2 == 0.0 {
return false;
}
if v1 > 0.0 && v2 > 0.0 {
return v1 < v2;
}
let mut better = false;
for i in 0..f1.len() {
if f1[i] > f2[i] {
return false;
}
if f1[i] < f2[i] {
better = true;
}
}
better
}
pub fn fast_non_dominated_sort(population: &mut [MultiObjectiveIndividual]) {
let n = population.len();
let mut dominance_counts = vec![0usize; n];
let mut dominated_sets: Vec<Vec<usize>> = vec![Vec::new(); n];
let mut fronts: Vec<Vec<usize>> = vec![Vec::new()];
for i in 0..n {
for j in 0..n {
if i == j {
continue;
}
if constrained_dominates(
&population[i].fitness,
population[i].constraint_violation,
&population[j].fitness,
population[j].constraint_violation,
) {
dominated_sets[i].push(j);
} else if constrained_dominates(
&population[j].fitness,
population[j].constraint_violation,
&population[i].fitness,
population[i].constraint_violation,
) {
dominance_counts[i] += 1;
}
}
if dominance_counts[i] == 0 {
population[i].rank = 0;
fronts[0].push(i);
}
}
let mut i = 0;
while !fronts[i].is_empty() {
let mut next_front = Vec::new();
for &p in &fronts[i] {
for &q in &dominated_sets[p] {
dominance_counts[q] -= 1;
if dominance_counts[q] == 0 {
population[q].rank = i + 1;
next_front.push(q);
}
}
}
i += 1;
fronts.push(next_front);
}
}
pub fn crowding_distance(
population: &mut [MultiObjectiveIndividual],
indices: &[usize],
) {
if indices.is_empty() {
return;
}
let num_objectives = population[indices[0]].fitness.len();
for &idx in indices {
population[idx].crowding_distance = 0.0;
}
for m in 0..num_objectives {
let mut sorted = indices.to_vec();
sorted.sort_by(|&a, &b| {
population[a].fitness[m]
.partial_cmp(&population[b].fitness[m])
.unwrap_or(std::cmp::Ordering::Equal)
});
let min_val = population[*sorted.first().unwrap()].fitness[m];
let max_val = population[*sorted.last().unwrap()].fitness[m];
let range = max_val - min_val;
population[*sorted.first().unwrap()].crowding_distance = f64::INFINITY;
population[*sorted.last().unwrap()].crowding_distance = f64::INFINITY;
if range > 1e-12 {
for k in 1..(sorted.len() - 1) {
let prev = population[sorted[k - 1]].fitness[m];
let next = population[sorted[k + 1]].fitness[m];
population[sorted[k]].crowding_distance += (next - prev) / range;
}
}
}
}
pub fn evaluate_population(population: &mut [MultiObjectiveIndividual]) {
fast_non_dominated_sort(population);
let mut rank = 0;
loop {
let indices: Vec<usize> = population
.iter()
.enumerate()
.filter(|(_, ind)| ind.rank == rank)
.map(|(i, _)| i)
.collect();
if indices.is_empty() {
break;
}
crowding_distance(population, &indices);
rank += 1;
}
}
pub struct EliteArchive {
pub capacity: usize,
pub members: Vec<MultiObjectiveIndividual>,
}
impl EliteArchive {
pub fn new(capacity: usize) -> Self {
Self {
capacity,
members: Vec::new(),
}
}
pub fn insert(&mut self, candidate: MultiObjectiveIndividual) {
self.members.push(candidate);
evaluate_population(&mut self.members);
self.members.retain(|m| m.rank == 0);
if self.members.len() > self.capacity {
self.members.sort_by(|a, b| {
b.crowding_distance
.partial_cmp(&a.crowding_distance)
.unwrap_or(std::cmp::Ordering::Equal)
});
self.members.truncate(self.capacity);
}
}
}
pub fn hypervolume_2d(front: &[Vec<f64>], reference: [f64; 2]) -> f64 {
let mut points: Vec<&Vec<f64>> = front
.iter()
.filter(|p| p[0] <= reference[0] && p[1] <= reference[1])
.collect();
if points.is_empty() {
return 0.0;
}
points.sort_by(|a, b| a[0].partial_cmp(&b[0]).unwrap_or(std::cmp::Ordering::Equal));
let mut hv = 0.0;
let mut prev_y = reference[1];
for p in points {
if p[1] < prev_y {
hv += (reference[0] - p[0]) * (prev_y - p[1]);
prev_y = p[1];
}
}
hv
}
pub fn igd(approx_front: &[Vec<f64>], reference_front: &[Vec<f64>]) -> f64 {
if reference_front.is_empty() || approx_front.is_empty() {
return f64::INFINITY;
}
let mut sum = 0.0;
for r in reference_front {
let mut d_min = f64::INFINITY;
for a in approx_front {
let mut d2 = 0.0;
for k in 0..r.len() {
let diff = r[k] - a[k];
d2 += diff * diff;
}
let d = d2.sqrt();
if d < d_min {
d_min = d;
}
}
sum += d_min;
}
sum / reference_front.len() as f64
}
#[cfg(test)]
mod tests {
use super::*;
use ndarray::array;
fn ind(fit: Vec<f64>, viol: f64) -> MultiObjectiveIndividual {
MultiObjectiveIndividual::new(array![0.0], fit, viol)
}
#[test]
fn dominance_basic() {
assert!(constrained_dominates(&[1.0, 1.0], 0.0, &[2.0, 2.0], 0.0));
assert!(!constrained_dominates(&[1.0, 2.0], 0.0, &[2.0, 1.0], 0.0));
}
#[test]
fn dominance_constraints() {
assert!(constrained_dominates(&[5.0], 0.0, &[1.0], 1.0));
assert!(constrained_dominates(&[5.0], 0.5, &[1.0], 2.0));
}
#[test]
fn fnds_assigns_ranks() {
let mut pop = vec![
ind(vec![1.0, 1.0], 0.0),
ind(vec![2.0, 2.0], 0.0),
ind(vec![3.0, 3.0], 0.0),
];
fast_non_dominated_sort(&mut pop);
assert_eq!(pop[0].rank, 0);
assert_eq!(pop[1].rank, 1);
assert_eq!(pop[2].rank, 2);
}
#[test]
fn hv_unit_square() {
let front = vec![vec![0.0, 1.0], vec![0.5, 0.5], vec![1.0, 0.0]];
let hv = hypervolume_2d(&front, [2.0, 2.0]);
assert!((hv - 3.25).abs() < 1e-9, "hv = {}", hv);
}
#[test]
fn igd_zero_when_match() {
let r = vec![vec![0.0, 1.0], vec![1.0, 0.0]];
let a = r.clone();
assert!(igd(&a, &r) < 1e-12);
}
#[test]
fn elite_archive_caps() {
let mut arch = EliteArchive::new(2);
arch.insert(ind(vec![0.0, 1.0], 0.0));
arch.insert(ind(vec![1.0, 0.0], 0.0));
arch.insert(ind(vec![0.5, 0.5], 0.0));
assert_eq!(arch.members.len(), 2);
}
}