pub fn dominates(a: &[f64], b: &[f64]) -> bool {
if a.len() != b.len() {
return false;
}
let mut any_strictly_better = false;
for (ai, bi) in a.iter().zip(b.iter()) {
if ai > bi {
return false;
}
if ai < bi {
any_strictly_better = true;
}
}
any_strictly_better
}
pub fn non_dominated_sort(population: &[Vec<f64>]) -> Vec<Vec<usize>> {
let n = population.len();
if n == 0 {
return vec![];
}
let mut domination_count = vec![0usize; n]; let mut dominated_set: Vec<Vec<usize>> = vec![vec![]; n];
for i in 0..n {
for j in (i + 1)..n {
if dominates(&population[i], &population[j]) {
dominated_set[i].push(j);
domination_count[j] += 1;
} else if dominates(&population[j], &population[i]) {
dominated_set[j].push(i);
domination_count[i] += 1;
}
}
}
let mut fronts: Vec<Vec<usize>> = Vec::new();
let mut current_front: Vec<usize> = (0..n)
.filter(|&i| domination_count[i] == 0)
.collect();
while !current_front.is_empty() {
let mut next_front: Vec<usize> = Vec::new();
for &i in ¤t_front {
for &j in &dominated_set[i] {
domination_count[j] -= 1;
if domination_count[j] == 0 {
next_front.push(j);
}
}
}
fronts.push(current_front);
current_front = next_front;
}
fronts
}
pub fn crowding_distance(front: &[Vec<f64>]) -> Vec<f64> {
let n = front.len();
if n == 0 {
return vec![];
}
if n == 1 {
return vec![f64::INFINITY];
}
if n == 2 {
return vec![f64::INFINITY; 2];
}
let n_obj = front[0].len();
let mut distances = vec![0.0_f64; n];
for m in 0..n_obj {
let mut sorted_idx: Vec<usize> = (0..n).collect();
sorted_idx.sort_by(|&a, &b| {
front[a][m]
.partial_cmp(&front[b][m])
.unwrap_or(std::cmp::Ordering::Equal)
});
distances[sorted_idx[0]] = f64::INFINITY;
distances[sorted_idx[n - 1]] = f64::INFINITY;
let obj_min = front[sorted_idx[0]][m];
let obj_max = front[sorted_idx[n - 1]][m];
let range = obj_max - obj_min;
if range < f64::EPSILON {
continue;
}
for k in 1..(n - 1) {
let prev_val = front[sorted_idx[k - 1]][m];
let next_val = front[sorted_idx[k + 1]][m];
if distances[sorted_idx[k]].is_finite() {
distances[sorted_idx[k]] += (next_val - prev_val) / range;
}
}
}
distances
}
pub fn hypervolume_2d(front: &[Vec<f64>], reference: &[f64]) -> f64 {
if front.is_empty() || reference.len() < 2 {
return 0.0;
}
let ref_x = reference[0];
let ref_y = reference[1];
let mut pts: Vec<(f64, f64)> = front
.iter()
.filter(|p| p.len() >= 2 && p[0] < ref_x && p[1] < ref_y)
.map(|p| (p[0], p[1]))
.collect();
if pts.is_empty() {
return 0.0;
}
pts.sort_by(|a, b| {
a.0.partial_cmp(&b.0)
.unwrap_or(std::cmp::Ordering::Equal)
});
let mut pareto_2d: Vec<(f64, f64)> = Vec::new();
let mut min_f2 = f64::INFINITY;
for (f1, f2) in &pts {
if *f2 < min_f2 {
min_f2 = *f2;
pareto_2d.push((*f1, *f2));
}
}
let np = pareto_2d.len();
let mut volume = 0.0;
for i in (0..np).rev() {
let x_start = pareto_2d[i].0;
let x_end = if i + 1 < np { pareto_2d[i + 1].0 } else { ref_x };
let y = pareto_2d[i].1;
volume += (x_end - x_start) * (ref_y - y);
}
volume
}
pub fn hypervolume_indicator(front: &[Vec<f64>], reference: &[f64]) -> f64 {
if front.is_empty() || reference.is_empty() {
return 0.0;
}
let n_obj = reference.len();
let mut pts: Vec<Vec<f64>> = front
.iter()
.filter(|p| {
p.len() == n_obj && p.iter().zip(reference.iter()).all(|(o, r)| o < r)
})
.cloned()
.collect();
if pts.is_empty() {
return 0.0;
}
wfg_hypervolume(&mut pts, reference)
}
fn wfg_hypervolume(pts: &mut Vec<Vec<f64>>, reference: &[f64]) -> f64 {
let n_dim = reference.len();
if pts.is_empty() {
return 0.0;
}
if n_dim == 1 {
let min_obj = pts.iter().map(|p| p[0]).fold(f64::INFINITY, f64::min);
return reference[0] - min_obj;
}
if n_dim == 2 {
let owned: Vec<Vec<f64>> = pts.clone();
return hypervolume_2d(&owned, reference);
}
pts.sort_by(|a, b| {
a[n_dim - 1]
.partial_cmp(&b[n_dim - 1])
.unwrap_or(std::cmp::Ordering::Equal)
});
let n = pts.len();
let mut volume = 0.0;
let sub_ref: Vec<f64> = reference[..n_dim - 1].to_vec();
for i in 0..n {
let slice_start = pts[i][n_dim - 1];
let slice_end = if i + 1 < n {
pts[i + 1][n_dim - 1]
} else {
reference[n_dim - 1]
};
let slice_height = slice_end - slice_start;
if slice_height <= 0.0 {
continue;
}
let mut sub_pts: Vec<Vec<f64>> = pts[..=i]
.iter()
.map(|p| p[..n_dim - 1].to_vec())
.collect();
sub_pts = filter_non_dominated_internal(&sub_pts);
let sub_hv = wfg_hypervolume(&mut sub_pts, &sub_ref);
volume += slice_height * sub_hv;
}
volume
}
fn filter_non_dominated_internal(pts: &[Vec<f64>]) -> Vec<Vec<f64>> {
if pts.is_empty() {
return vec![];
}
let n = pts.len();
let mut dominated = vec![false; n];
for i in 0..n {
if dominated[i] {
continue;
}
for j in 0..n {
if i == j || dominated[j] {
continue;
}
if dominates(&pts[i], &pts[j]) {
dominated[j] = true;
}
}
}
pts.iter()
.enumerate()
.filter(|(i, _)| !dominated[*i])
.map(|(_, p)| p.clone())
.collect()
}
pub fn epsilon_indicator(approx: &[Vec<f64>], reference: &[Vec<f64>]) -> f64 {
if approx.is_empty() || reference.is_empty() {
return f64::INFINITY;
}
reference
.iter()
.map(|p| {
approx
.iter()
.map(|q| {
q.iter()
.zip(p.iter())
.map(|(qi, pi)| qi - pi)
.fold(f64::NEG_INFINITY, f64::max)
})
.fold(f64::INFINITY, f64::min)
})
.fold(f64::NEG_INFINITY, f64::max)
}
pub fn generational_distance(approx: &[Vec<f64>], reference: &[Vec<f64>]) -> f64 {
if approx.is_empty() || reference.is_empty() {
return f64::INFINITY;
}
let sum: f64 = approx
.iter()
.map(|q| {
reference
.iter()
.map(|p| euclidean_distance(q, p))
.fold(f64::INFINITY, f64::min)
})
.sum();
sum / approx.len() as f64
}
pub fn spread_metric(front: &[Vec<f64>]) -> f64 {
let n = front.len();
if n < 2 {
return 0.0;
}
let nn_dists: Vec<f64> = (0..n)
.map(|i| {
(0..n)
.filter(|&j| j != i)
.map(|j| euclidean_distance(&front[i], &front[j]))
.fold(f64::INFINITY, f64::min)
})
.collect();
let mean = nn_dists.iter().sum::<f64>() / n as f64;
if mean < f64::EPSILON {
return 0.0;
}
let variance = nn_dists.iter().map(|d| (d - mean).powi(2)).sum::<f64>() / n as f64;
variance.sqrt() / mean
}
#[inline]
fn euclidean_distance(a: &[f64], b: &[f64]) -> f64 {
a.iter()
.zip(b.iter())
.map(|(x, y)| (x - y).powi(2))
.sum::<f64>()
.sqrt()
}
pub fn pareto_rank(population: &[Vec<f64>]) -> Vec<usize> {
if population.is_empty() {
return vec![];
}
let fronts = non_dominated_sort(population);
let mut ranks = vec![0usize; population.len()];
for (front_idx, front) in fronts.iter().enumerate() {
for &i in front {
ranks[i] = front_idx;
}
}
ranks
}
pub fn pareto_front(population: &[Vec<f64>]) -> Vec<usize> {
if population.is_empty() {
return vec![];
}
let fronts = non_dominated_sort(population);
fronts.into_iter().next().unwrap_or_default()
}
pub fn pareto_front_2d(points: &[(f64, f64)]) -> Vec<usize> {
if points.is_empty() {
return vec![];
}
let mut sorted_idx: Vec<usize> = (0..points.len()).collect();
sorted_idx.sort_by(|&a, &b| {
points[a]
.0
.partial_cmp(&points[b].0)
.unwrap_or(std::cmp::Ordering::Equal)
.then_with(|| {
points[a]
.1
.partial_cmp(&points[b].1)
.unwrap_or(std::cmp::Ordering::Equal)
})
});
let mut front: Vec<usize> = Vec::new();
let mut min_f2 = f64::INFINITY;
for idx in sorted_idx {
let (_, f2) = points[idx];
if f2 < min_f2 {
front.push(idx);
min_f2 = f2;
}
}
front
}
pub fn igd(approx: &[Vec<f64>], true_front: &[Vec<f64>]) -> f64 {
if true_front.is_empty() || approx.is_empty() {
return f64::INFINITY;
}
let sum: f64 = true_front
.iter()
.map(|tp| {
approx
.iter()
.map(|ap| euclidean_distance(tp, ap))
.fold(f64::INFINITY, f64::min)
})
.sum();
sum / true_front.len() as f64
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_dominates_basic_2d() {
assert!(dominates(&[1.0, 1.0], &[2.0, 2.0]));
assert!(dominates(&[1.0, 2.0], &[2.0, 2.0]));
assert!(!dominates(&[1.0, 3.0], &[2.0, 2.0]));
assert!(!dominates(&[2.0, 2.0], &[2.0, 2.0])); }
#[test]
fn test_dominates_three_objectives() {
assert!(dominates(&[1.0, 1.0, 1.0], &[2.0, 2.0, 2.0]));
assert!(!dominates(&[1.0, 2.0, 1.0], &[1.0, 1.0, 2.0]));
}
#[test]
fn test_dominates_different_lengths() {
assert!(!dominates(&[1.0, 2.0], &[3.0]));
}
#[test]
fn test_non_dominated_sort_trivial() {
let pts = vec![vec![1.0, 2.0], vec![2.0, 1.0], vec![3.0, 3.0]];
let fronts = non_dominated_sort(&pts);
assert_eq!(fronts.len(), 2);
assert_eq!(fronts[0].len(), 2);
assert_eq!(fronts[1].len(), 1);
assert!(fronts[1].contains(&2));
}
#[test]
fn test_non_dominated_sort_all_optimal() {
let pts = vec![vec![1.0, 3.0], vec![2.0, 2.0], vec![3.0, 1.0]];
let fronts = non_dominated_sort(&pts);
assert_eq!(fronts.len(), 1);
assert_eq!(fronts[0].len(), 3);
}
#[test]
fn test_non_dominated_sort_chain() {
let pts = vec![vec![1.0, 1.0], vec![2.0, 2.0], vec![3.0, 3.0]];
let fronts = non_dominated_sort(&pts);
assert_eq!(fronts.len(), 3);
assert_eq!(fronts[0], vec![0]);
assert_eq!(fronts[1], vec![1]);
assert_eq!(fronts[2], vec![2]);
}
#[test]
fn test_non_dominated_sort_empty() {
assert!(non_dominated_sort(&[]).is_empty());
}
#[test]
fn test_crowding_distance_three_points() {
let front = vec![vec![0.0, 1.0], vec![0.5, 0.5], vec![1.0, 0.0]];
let cd = crowding_distance(&front);
assert_eq!(cd.len(), 3);
let inf_count = cd.iter().filter(|d| d.is_infinite()).count();
assert_eq!(inf_count, 2, "Expected 2 boundary points with inf cd, got {:?}", cd);
assert!(cd[1].is_finite(), "Middle point should have finite cd");
}
#[test]
fn test_crowding_distance_two_points() {
let front = vec![vec![0.0, 1.0], vec![1.0, 0.0]];
let cd = crowding_distance(&front);
assert!(cd.iter().all(|d| d.is_infinite()));
}
#[test]
fn test_crowding_distance_single_point() {
let cd = crowding_distance(&[vec![0.5, 0.5]]);
assert_eq!(cd, vec![f64::INFINITY]);
}
#[test]
fn test_crowding_distance_empty() {
let cd = crowding_distance(&[]);
assert!(cd.is_empty());
}
#[test]
fn test_crowding_distance_identical_objectives() {
let front = vec![vec![0.0, 1.0], vec![0.0, 0.5], vec![0.0, 0.0]];
let cd = crowding_distance(&front);
assert_eq!(cd.len(), 3);
}
#[test]
fn test_hypervolume_2d_empty() {
assert_eq!(hypervolume_2d(&[], &[2.0, 2.0]), 0.0);
}
#[test]
fn test_hypervolume_2d_single_point() {
let front = vec![vec![1.0, 1.0]];
let hv = hypervolume_2d(&front, &[2.0, 2.0]);
assert!((hv - 1.0).abs() < 1e-10, "Expected 1.0, got {hv}");
}
#[test]
fn test_hypervolume_2d_two_points() {
let front = vec![vec![0.0, 2.0], vec![2.0, 0.0]];
let hv = hypervolume_2d(&front, &[3.0, 3.0]);
assert!((hv - 5.0).abs() < 1e-10, "Expected 5.0, got {hv}");
}
#[test]
fn test_hypervolume_2d_outside_reference() {
let front = vec![vec![5.0, 5.0]];
let hv = hypervolume_2d(&front, &[2.0, 2.0]);
assert_eq!(hv, 0.0);
}
#[test]
fn test_hypervolume_2d_with_dominated_point() {
let front = vec![vec![1.0, 1.0], vec![2.0, 2.0]];
let hv = hypervolume_2d(&front, &[3.0, 3.0]);
assert!((hv - 4.0).abs() < 1e-10, "Expected 4.0, got {hv}");
}
#[test]
fn test_hypervolume_indicator_2d() {
let front = vec![vec![0.0, 2.0], vec![2.0, 0.0]];
let hv = hypervolume_indicator(&front, &[3.0, 3.0]);
assert!((hv - 5.0).abs() < 1e-10, "Expected 5.0, got {hv}");
}
#[test]
fn test_hypervolume_indicator_3d_unit_cube() {
let front = vec![vec![1.0, 1.0, 1.0]];
let hv = hypervolume_indicator(&front, &[2.0, 2.0, 2.0]);
assert!((hv - 1.0).abs() < 1e-10, "Expected 1.0, got {hv}");
}
#[test]
fn test_hypervolume_indicator_empty() {
assert_eq!(hypervolume_indicator(&[], &[2.0, 2.0]), 0.0);
}
#[test]
fn test_hypervolume_indicator_outside_reference() {
let front = vec![vec![5.0, 5.0, 5.0]];
let hv = hypervolume_indicator(&front, &[2.0, 2.0, 2.0]);
assert_eq!(hv, 0.0);
}
#[test]
fn test_epsilon_indicator_identical() {
let reference = vec![vec![0.0, 1.0], vec![1.0, 0.0]];
let approx = reference.clone();
let eps = epsilon_indicator(&approx, &reference);
assert!(eps.abs() < 1e-10, "eps={eps}");
}
#[test]
fn test_epsilon_indicator_worse() {
let reference = vec![vec![0.0, 0.0]];
let approx = vec![vec![1.0, 1.0]];
let eps = epsilon_indicator(&approx, &reference);
assert!((eps - 1.0).abs() < 1e-10, "Expected eps=1.0, got {eps}");
}
#[test]
fn test_epsilon_indicator_better() {
let reference = vec![vec![1.0, 1.0]];
let approx = vec![vec![0.5, 1.0]];
let eps = epsilon_indicator(&approx, &reference);
assert!(eps <= 0.0, "approx is better, eps should be <=0, got {eps}");
}
#[test]
fn test_epsilon_indicator_empty() {
assert_eq!(
epsilon_indicator(&[], &[vec![1.0, 1.0]]),
f64::INFINITY
);
assert_eq!(
epsilon_indicator(&[vec![1.0, 1.0]], &[]),
f64::INFINITY
);
}
#[test]
fn test_generational_distance_identical() {
let reference = vec![vec![0.0, 1.0], vec![1.0, 0.0]];
let approx = reference.clone();
let gd = generational_distance(&approx, &reference);
assert!(gd < 1e-10, "gd={gd}");
}
#[test]
fn test_generational_distance_offset() {
let reference = vec![vec![0.0, 0.0]];
let approx = vec![vec![0.1, 0.1]];
let gd = generational_distance(&approx, &reference);
let expected = (0.1f64.powi(2) + 0.1f64.powi(2)).sqrt();
assert!((gd - expected).abs() < 1e-10);
}
#[test]
fn test_generational_distance_empty() {
assert_eq!(
generational_distance(&[], &[vec![1.0, 1.0]]),
f64::INFINITY
);
assert_eq!(
generational_distance(&[vec![1.0, 1.0]], &[]),
f64::INFINITY
);
}
#[test]
fn test_spread_metric_uniform() {
let front: Vec<Vec<f64>> = (0..5)
.map(|i| {
let f1 = i as f64 * 0.25;
vec![f1, 1.0 - f1]
})
.collect();
let sp = spread_metric(&front);
assert!(sp < 0.05, "Uniform front should have near-zero spread: {sp}");
}
#[test]
fn test_spread_metric_single_point() {
assert_eq!(spread_metric(&[vec![0.5, 0.5]]), 0.0);
}
#[test]
fn test_spread_metric_two_points() {
let front = vec![vec![0.0, 1.0], vec![1.0, 0.0]];
let sp = spread_metric(&front);
assert!(sp < 1e-10, "Two-point front spread should be 0, got {sp}");
}
#[test]
fn test_spread_metric_clustered() {
let front = vec![
vec![0.0, 1.0],
vec![0.1, 0.9],
vec![9.9, 0.1],
vec![10.0, 0.0],
];
let sp_clustered = spread_metric(&front);
let uniform_front: Vec<Vec<f64>> = (0..4)
.map(|i| vec![i as f64 * 10.0 / 3.0, 10.0 - i as f64 * 10.0 / 3.0])
.collect();
let sp_uniform = spread_metric(&uniform_front);
assert!(
sp_clustered > sp_uniform,
"Clustered ({sp_clustered}) should have higher spread than uniform ({sp_uniform})"
);
}
#[test]
fn test_nsga2_style_ranking() {
let population = vec![
vec![1.0, 3.0],
vec![2.0, 2.0],
vec![3.0, 1.0], vec![2.0, 3.0],
vec![3.0, 2.0], vec![4.0, 4.0], ];
let fronts = non_dominated_sort(&population);
assert!(fronts.len() >= 2, "Expected multiple fronts");
let front0_pts: Vec<Vec<f64>> = fronts[0]
.iter()
.map(|&i| population[i].clone())
.collect();
let cd = crowding_distance(&front0_pts);
assert_eq!(cd.len(), front0_pts.len());
}
#[test]
fn test_pareto_rank_chain() {
let pop = vec![vec![1.0, 1.0], vec![2.0, 2.0], vec![3.0, 3.0]];
let ranks = pareto_rank(&pop);
assert_eq!(ranks, vec![0, 1, 2]);
}
#[test]
fn test_pareto_rank_all_equal_rank() {
let pop = vec![vec![1.0, 3.0], vec![2.0, 2.0], vec![3.0, 1.0]];
let ranks = pareto_rank(&pop);
assert!(ranks.iter().all(|&r| r == 0), "all should be rank 0: {ranks:?}");
}
#[test]
fn test_pareto_rank_empty() {
let ranks = pareto_rank(&[]);
assert!(ranks.is_empty());
}
#[test]
fn test_pareto_rank_mixed() {
let pop = vec![
vec![1.0, 2.0], vec![2.0, 1.0], vec![2.0, 2.0], vec![3.0, 3.0], ];
let ranks = pareto_rank(&pop);
assert_eq!(ranks[0], 0);
assert_eq!(ranks[1], 0);
assert!(ranks[2] >= 1, "rank[2] should be >=1, got {}", ranks[2]);
assert!(ranks[3] >= ranks[2], "rank[3] should be >= rank[2]");
}
#[test]
fn test_pareto_front_basic() {
let pop = vec![
vec![1.0, 2.0],
vec![2.0, 1.0],
vec![3.0, 3.0],
];
let front = pareto_front(&pop);
assert_eq!(front.len(), 2);
assert!(!front.contains(&2), "dominated point should not be in front");
}
#[test]
fn test_pareto_front_empty() {
let front = pareto_front(&[]);
assert!(front.is_empty());
}
#[test]
fn test_pareto_front_all_non_dominated() {
let pop = vec![vec![0.0, 1.0], vec![0.5, 0.5], vec![1.0, 0.0]];
let front = pareto_front(&pop);
assert_eq!(front.len(), 3);
}
#[test]
fn test_igd_pareto_perfect() {
let tf = vec![vec![0.0, 1.0], vec![0.5, 0.5], vec![1.0, 0.0]];
let af = tf.clone();
let val = igd(&af, &tf);
assert!(val < 1e-10, "IGD of identical fronts: {val}");
}
#[test]
fn test_igd_pareto_empty() {
assert_eq!(igd(&[], &[vec![1.0]]), f64::INFINITY);
assert_eq!(igd(&[vec![1.0]], &[]), f64::INFINITY);
}
#[test]
fn test_igd_pareto_offset() {
let tf = vec![vec![0.0, 0.0]];
let af = vec![vec![0.3, 0.4]]; let val = igd(&af, &tf);
let expected = (0.3f64.powi(2) + 0.4f64.powi(2)).sqrt();
assert!((val - expected).abs() < 1e-10, "Expected {expected}, got {val}");
}
}