pub fn hypervolume(points: &[Vec<f64>], r: &[f64]) -> f64 {
if points.is_empty() {
return 0.0;
}
if r.len() == 2 {
return hypervolume_2d(points, r);
}
wfg(points, r)
}
pub fn hv_contributions(points: &[Vec<f64>], r: &[f64]) -> Vec<f64> {
if points.is_empty() {
return Vec::new();
}
if r.len() == 2 {
return contributions_2d(points, r);
}
let total = wfg(points, r);
(0..points.len())
.map(|i| {
let without: Vec<Vec<f64>> = points
.iter()
.enumerate()
.filter(|&(j, _)| j != i)
.map(|(_, p)| p.clone())
.collect();
(total - wfg(&without, r)).max(0.0)
})
.collect()
}
pub fn gd(front: &[Vec<f64>], reference: &[Vec<f64>]) -> f64 {
mean_min_distance(front, reference, euclidean)
}
pub fn igd(front: &[Vec<f64>], reference: &[Vec<f64>]) -> f64 {
mean_min_distance(reference, front, euclidean)
}
pub fn gd_plus(front: &[Vec<f64>], reference: &[Vec<f64>]) -> f64 {
mean_min_distance(front, reference, d_plus)
}
pub fn igd_plus(front: &[Vec<f64>], reference: &[Vec<f64>]) -> f64 {
mean_min_distance(reference, front, |z, a| d_plus(a, z))
}
fn mean_min_distance(
from: &[Vec<f64>],
to: &[Vec<f64>],
dist: impl Fn(&[f64], &[f64]) -> f64,
) -> f64 {
if from.is_empty() || to.is_empty() {
return f64::NAN;
}
let total: f64 = from
.iter()
.map(|p| to.iter().map(|q| dist(p, q)).fold(f64::INFINITY, f64::min))
.sum();
total / from.len() as f64
}
fn euclidean(a: &[f64], b: &[f64]) -> f64 {
a.iter()
.zip(b)
.map(|(x, y)| (x - y) * (x - y))
.sum::<f64>()
.sqrt()
}
fn d_plus(a: &[f64], z: &[f64]) -> f64 {
a.iter()
.zip(z)
.map(|(ai, zi)| (ai - zi).max(0.0).powi(2))
.sum::<f64>()
.sqrt()
}
fn hypervolume_2d(points: &[Vec<f64>], r: &[f64]) -> f64 {
let mut sorted: Vec<&Vec<f64>> = points.iter().collect();
sorted.sort_by(|a, b| {
a[0].partial_cmp(&b[0])
.unwrap_or(std::cmp::Ordering::Equal)
.then(a[1].partial_cmp(&b[1]).unwrap_or(std::cmp::Ordering::Equal))
});
let mut vol = 0.0;
let mut best_f2 = r[1];
for p in sorted {
if p[1] < best_f2 && p[0] < r[0] {
vol += (r[0] - p[0]) * (best_f2 - p[1]);
best_f2 = p[1];
}
}
vol
}
fn contributions_2d(points: &[Vec<f64>], r: &[f64]) -> Vec<f64> {
let k = points.len();
let mut order: Vec<usize> = (0..k).collect();
order.sort_by(|&a, &b| {
points[a][0]
.partial_cmp(&points[b][0])
.unwrap_or(std::cmp::Ordering::Equal)
.then(
points[a][1]
.partial_cmp(&points[b][1])
.unwrap_or(std::cmp::Ordering::Equal),
)
});
let mut contr = vec![0.0; k];
let mut stair: Vec<usize> = Vec::with_capacity(k);
let mut best_f2 = f64::INFINITY;
for &i in &order {
if points[i][1] < best_f2 {
stair.push(i);
best_f2 = points[i][1];
}
}
for (pos, &i) in stair.iter().enumerate() {
let x_next = if pos + 1 < stair.len() {
points[stair[pos + 1]][0]
} else {
r[0]
};
let y_prev = if pos > 0 {
points[stair[pos - 1]][1]
} else {
r[1]
};
contr[i] = ((x_next - points[i][0]) * (y_prev - points[i][1])).max(0.0);
}
contr
}
fn wfg(points: &[Vec<f64>], r: &[f64]) -> f64 {
if points.is_empty() {
return 0.0;
}
let mut vol = 0.0;
for i in 0..points.len() {
let incl: f64 = r
.iter()
.zip(&points[i])
.map(|(rj, pj)| (rj - pj).max(0.0))
.product();
let limited: Vec<Vec<f64>> = points[i + 1..]
.iter()
.map(|q| (0..r.len()).map(|j| points[i][j].max(q[j])).collect())
.collect();
let nd = non_dominated(&limited);
vol += incl - wfg(&nd, r);
}
vol
}
fn non_dominated(set: &[Vec<f64>]) -> Vec<Vec<f64>> {
let mut out = Vec::new();
for (i, p) in set.iter().enumerate() {
let dominated = set.iter().enumerate().any(|(j, q)| {
i != j && q.iter().zip(p).all(|(a, b)| a <= b) && q.iter().zip(p).any(|(a, b)| a < b)
});
if !dominated {
out.push(p.clone());
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hypervolume_known_2d_and_3d() {
let pts = vec![vec![1.0, 3.0], vec![2.0, 2.0]];
assert!((hypervolume(&pts, &[3.0, 4.0]) - 3.0).abs() < 1e-9);
let with_dominated = vec![vec![1.0, 3.0], vec![2.0, 2.0], vec![2.5, 3.5]];
assert!((hypervolume(&with_dominated, &[3.0, 4.0]) - 3.0).abs() < 1e-9);
let one = vec![vec![1.0, 1.0, 1.0]];
assert!((hypervolume(&one, &[2.0, 3.0, 4.0]) - 6.0).abs() < 1e-9);
}
#[test]
fn wfg_and_2d_sweep_agree() {
let pts = vec![
vec![0.1, 0.9],
vec![0.4, 0.5],
vec![0.4, 0.5], vec![0.8, 0.2],
vec![0.9, 0.8], ];
let r = [1.1, 1.1];
let sweep = hypervolume_2d(&pts, &r);
let recursive = wfg(&pts, &r);
assert!(
(sweep - recursive).abs() < 1e-12,
"sweep {sweep} vs wfg {recursive}"
);
}
#[test]
fn contributions_match_leave_one_out() {
let pts = vec![vec![0.1, 0.9], vec![0.4, 0.5], vec![0.8, 0.2]];
let r = [1.0, 1.0];
let fast = hv_contributions(&pts, &r);
let total = hypervolume(&pts, &r);
for (i, f) in fast.iter().enumerate() {
let without: Vec<Vec<f64>> = pts
.iter()
.enumerate()
.filter(|&(j, _)| j != i)
.map(|(_, p)| p.clone())
.collect();
let slow = total - hypervolume(&without, &r);
assert!(
(f - slow).abs() < 1e-12,
"point {i}: fast {f} vs slow {slow}"
);
}
let with_extra = vec![
vec![0.1, 0.9],
vec![0.1, 0.9],
vec![0.5, 0.95], ];
let c = hv_contributions(&with_extra, &r);
assert_eq!(c[1], 0.0);
assert_eq!(c[2], 0.0);
}
#[test]
fn igd_plus_ignores_dominating_deviation() {
let reference = vec![vec![0.5, 0.5]];
let dominating_front = vec![vec![0.4, 0.4]];
assert!(igd(&dominating_front, &reference) > 0.0);
assert_eq!(igd_plus(&dominating_front, &reference), 0.0);
let dominated_front = vec![vec![0.6, 0.6]];
let d_igd = igd(&dominated_front, &reference);
let d_plusv = igd_plus(&dominated_front, &reference);
assert!((d_igd - d_plusv).abs() < 1e-12);
}
#[test]
fn gd_igd_basics() {
let front = vec![vec![0.0, 1.0], vec![1.0, 0.0]];
assert_eq!(gd(&front, &front), 0.0);
assert_eq!(igd(&front, &front), 0.0);
assert_eq!(gd_plus(&front, &front), 0.0);
assert_eq!(igd_plus(&front, &front), 0.0);
assert!(gd(&[], &front).is_nan());
assert!(igd(&front, &[]).is_nan());
}
}