use crate::error::{OptimizeError, OptimizeResult};
pub fn hypervolume_2d(front: &[(f64, f64)], reference: (f64, f64)) -> OptimizeResult<f64> {
if front.is_empty() {
return Ok(0.0);
}
let mut pts: Vec<(f64, f64)> = front
.iter()
.filter(|&&(f1, f2)| f1 < reference.0 && f2 < reference.1)
.copied()
.collect();
if pts.is_empty() {
return Ok(0.0);
}
pts.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
let mut non_dom: Vec<(f64, f64)> = Vec::with_capacity(pts.len());
let mut min_f2 = f64::INFINITY;
for (f1, f2) in pts {
if f2 < min_f2 {
non_dom.push((f1, f2));
min_f2 = f2;
}
}
let mut hv = 0.0_f64;
let n = non_dom.len();
for i in 0..n {
let f1_next = if i + 1 < n { non_dom[i + 1].0 } else { reference.0 };
let width = f1_next - non_dom[i].0;
let height = reference.1 - non_dom[i].1;
if width > 0.0 && height > 0.0 {
hv += width * height;
}
}
Ok(hv)
}
pub fn hypervolume_3d(front: &[[f64; 3]], reference: &[f64; 3]) -> OptimizeResult<f64> {
if front.is_empty() {
return Ok(0.0);
}
let mut pts: Vec<[f64; 3]> = front
.iter()
.filter(|p| p[0] < reference[0] && p[1] < reference[1] && p[2] < reference[2])
.copied()
.collect();
if pts.is_empty() {
return Ok(0.0);
}
pts.sort_by(|a, b| a[2].partial_cmp(&b[2]).unwrap_or(std::cmp::Ordering::Equal));
let mut z_vals: Vec<f64> = pts.iter().map(|p| p[2]).collect();
z_vals.dedup_by(|a, b| (*a - *b).abs() < 1e-15);
let mut hv = 0.0_f64;
let mut prev_z = z_vals[0];
for &z_cut in &z_vals {
let active: Vec<(f64, f64)> = pts
.iter()
.filter(|p| p[2] <= z_cut)
.map(|p| (p[0], p[1]))
.collect();
if !active.is_empty() {
let hv_2d = hypervolume_2d(&active, (reference[0], reference[1]))?;
let dz = z_cut - prev_z;
if dz > 0.0 {
hv += hv_2d * dz;
}
}
prev_z = z_cut;
}
let all_2d: Vec<(f64, f64)> = pts.iter().map(|p| (p[0], p[1])).collect();
if !all_2d.is_empty() {
let hv_2d = hypervolume_2d(&all_2d, (reference[0], reference[1]))?;
let dz = reference[2] - prev_z;
if dz > 0.0 {
hv += hv_2d * dz;
}
}
Ok(hv)
}
pub fn hypervolume_wfg(front: &[Vec<f64>], reference: &[f64]) -> OptimizeResult<f64> {
if front.is_empty() {
return Ok(0.0);
}
let n_obj = reference.len();
for (i, pt) in front.iter().enumerate() {
if pt.len() != n_obj {
return Err(OptimizeError::InvalidInput(format!(
"front[{i}] has length {} but reference has length {n_obj}",
pt.len()
)));
}
}
let mut pts: Vec<Vec<f64>> = front
.iter()
.filter(|pt| pt.iter().zip(reference.iter()).all(|(f, r)| f < r))
.cloned()
.collect();
if pts.is_empty() {
return Ok(0.0);
}
Ok(wfg_recurse(&mut pts, reference))
}
fn wfg_recurse(pts: &mut Vec<Vec<f64>>, reference: &[f64]) -> f64 {
let n = pts.len();
if n == 0 {
return 0.0;
}
let d = reference.len();
if d == 1 {
let min_f: f64 = pts.iter().map(|p| p[0]).fold(f64::INFINITY, f64::min);
return (reference[0] - min_f).max(0.0);
}
if n == 1 {
let vol: f64 = pts[0]
.iter()
.zip(reference.iter())
.map(|(f, r)| (r - f).max(0.0))
.product();
return vol;
}
pts.sort_by(|a, b| {
b[d - 1]
.partial_cmp(&a[d - 1])
.unwrap_or(std::cmp::Ordering::Equal)
});
let non_dom = filter_nd_non_dominated(pts);
let n_nd = non_dom.len();
let mut total = 0.0_f64;
for i in 0..n_nd {
let ref_last = if i + 1 < n_nd {
non_dom[i + 1][d - 1]
} else {
reference[d - 1]
};
let dz = ref_last - non_dom[i][d - 1];
if dz <= 0.0 {
continue;
}
let sub_ref: Vec<f64> = reference[..d - 1].to_vec();
let mut sub_pts: Vec<Vec<f64>> = non_dom[..=i]
.iter()
.map(|p| p[..d - 1].to_vec())
.collect();
let hv_sub = wfg_recurse(&mut sub_pts, &sub_ref);
total += hv_sub * dz;
}
total
}
fn filter_nd_non_dominated(pts: &[Vec<f64>]) -> Vec<Vec<f64>> {
let mut result: Vec<Vec<f64>> = Vec::with_capacity(pts.len());
'outer: for pt in pts {
for existing in &result {
if nd_dominates(existing, pt) {
continue 'outer;
}
}
result.retain(|existing| !nd_dominates(pt, existing));
result.push(pt.clone());
}
result
}
fn nd_dominates(a: &[f64], b: &[f64]) -> bool {
let mut any_strict = false;
for (ai, bi) in a.iter().zip(b.iter()) {
if *ai > *bi {
return false;
}
if *ai < *bi {
any_strict = true;
}
}
any_strict
}
pub fn hypervolume_contribution_wfg(
front: &[Vec<f64>],
reference: &[f64],
idx: usize,
) -> OptimizeResult<f64> {
if idx >= front.len() {
return Err(OptimizeError::InvalidInput(format!(
"idx={idx} out of range for front of length {}",
front.len()
)));
}
let total_hv = hypervolume_wfg(front, reference)?;
let reduced: Vec<Vec<f64>> = front
.iter()
.enumerate()
.filter(|(i, _)| *i != idx)
.map(|(_, pt)| pt.clone())
.collect();
let reduced_hv = hypervolume_wfg(&reduced, reference)?;
Ok((total_hv - reduced_hv).max(0.0))
}
pub fn exclusive_hypervolume(front: &[Vec<f64>], reference: &[f64]) -> OptimizeResult<Vec<f64>> {
if front.is_empty() {
return Ok(vec![]);
}
let n = front.len();
let n_obj = reference.len();
for (i, pt) in front.iter().enumerate() {
if pt.len() != n_obj {
return Err(OptimizeError::InvalidInput(format!(
"front[{i}] has length {} but reference has length {n_obj}",
pt.len()
)));
}
}
let total_hv = hypervolume_wfg(front, reference)?;
let mut contributions = Vec::with_capacity(n);
for idx in 0..n {
let reduced: Vec<Vec<f64>> = front
.iter()
.enumerate()
.filter(|(i, _)| *i != idx)
.map(|(_, pt)| pt.clone())
.collect();
let reduced_hv = hypervolume_wfg(&reduced, reference)?;
contributions.push((total_hv - reduced_hv).max(0.0));
}
Ok(contributions)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hv2d_empty_front() {
let hv = hypervolume_2d(&[], (2.0, 2.0)).expect("failed to create hv");
assert_eq!(hv, 0.0);
}
#[test]
fn test_hv2d_single_point() {
let hv = hypervolume_2d(&[(0.0, 0.0)], (2.0, 2.0)).expect("failed to create hv");
assert!((hv - 4.0).abs() < 1e-10, "expected 4.0, got {hv}");
}
#[test]
fn test_hv2d_three_points_on_front() {
let front = &[(0.0f64, 1.0f64), (0.5, 0.5), (1.0, 0.0)];
let hv = hypervolume_2d(front, (2.0, 2.0)).expect("failed to create hv");
assert!((hv - 3.25).abs() < 1e-10, "expected 3.25, got {hv}");
}
#[test]
fn test_hv2d_unsorted_input() {
let front = &[(1.0f64, 0.0f64), (0.0, 1.0), (0.5, 0.5)];
let hv = hypervolume_2d(front, (2.0, 2.0)).expect("failed to create hv");
assert!((hv - 3.25).abs() < 1e-10, "expected 3.25, got {hv}");
}
#[test]
fn test_hv2d_with_dominated_points() {
let front = &[(0.0, 1.0), (0.5, 0.5), (1.0, 0.0), (0.5, 1.0)];
let hv = hypervolume_2d(front, (2.0, 2.0)).expect("failed to create hv");
assert!((hv - 3.25).abs() < 1e-10, "expected 3.25, got {hv}");
}
#[test]
fn test_hv2d_point_outside_reference() {
let front = &[(3.0, 0.5)]; let hv = hypervolume_2d(front, (2.0, 2.0)).expect("failed to create hv");
assert_eq!(hv, 0.0);
}
#[test]
fn test_hv3d_empty_front() {
let hv = hypervolume_3d(&[], &[2.0, 2.0, 2.0]).expect("failed to create hv");
assert_eq!(hv, 0.0);
}
#[test]
fn test_hv3d_single_point_unit_cube() {
let hv = hypervolume_3d(&[[0.0, 0.0, 0.0]], &[1.0, 1.0, 1.0]).expect("failed to create hv");
assert!((hv - 1.0).abs() < 1e-10, "expected 1.0, got {hv}");
}
#[test]
fn test_hv3d_two_non_dominated_points() {
let front = [[0.0, 0.0, 1.0], [0.0, 1.0, 0.0]];
let hv = hypervolume_3d(&front, &[2.0, 2.0, 2.0]).expect("failed to create hv");
assert!(hv > 0.0, "hypervolume should be positive, got {hv}");
}
#[test]
fn test_hv3d_matches_wfg() {
let front = [[0.2, 0.5, 0.8], [0.5, 0.3, 0.6], [0.8, 0.2, 0.4]];
let ref3 = [1.5, 1.5, 1.5];
let hv3d = hypervolume_3d(&front, &ref3).expect("failed to create hv3d");
let front_vecs: Vec<Vec<f64>> = front.iter().map(|p| p.to_vec()).collect();
let hv_wfg = hypervolume_wfg(&front_vecs, &ref3).expect("failed to create hv_wfg");
assert!(
(hv3d - hv_wfg).abs() < 1e-6,
"3D ({hv3d}) and WFG ({hv_wfg}) should agree"
);
}
#[test]
fn test_wfg_empty_front() {
let hv = hypervolume_wfg(&[], &[2.0, 2.0]).expect("failed to create hv");
assert_eq!(hv, 0.0);
}
#[test]
fn test_wfg_2d_matches_hv2d() {
let front = vec![vec![0.0, 1.0], vec![0.5, 0.5], vec![1.0, 0.0]];
let hv_wfg = hypervolume_wfg(&front, &[2.0, 2.0]).expect("failed to create hv_wfg");
let front_2d: Vec<(f64, f64)> = front.iter().map(|p| (p[0], p[1])).collect();
let hv_2d = hypervolume_2d(&front_2d, (2.0, 2.0)).expect("failed to create hv_2d");
assert!((hv_wfg - hv_2d).abs() < 1e-10, "WFG={hv_wfg}, 2D={hv_2d}");
}
#[test]
fn test_wfg_single_point() {
let front = vec![vec![1.0, 1.0, 1.0]];
let hv = hypervolume_wfg(&front, &[3.0, 3.0, 3.0]).expect("failed to create hv");
assert!((hv - 8.0).abs() < 1e-10, "expected 8.0, got {hv}");
}
#[test]
fn test_wfg_dimension_mismatch_error() {
let front = vec![vec![1.0, 2.0, 3.0]];
let result = hypervolume_wfg(&front, &[4.0, 4.0]); assert!(result.is_err());
}
#[test]
fn test_hvc_sum_leq_total() {
let front = vec![vec![0.0, 1.0], vec![0.5, 0.5], vec![1.0, 0.0]];
let total = hypervolume_wfg(&front, &[2.0, 2.0]).expect("failed to create total");
let hvc0 = hypervolume_contribution_wfg(&front, &[2.0, 2.0], 0).expect("failed to create hvc0");
let hvc1 = hypervolume_contribution_wfg(&front, &[2.0, 2.0], 1).expect("failed to create hvc1");
let hvc2 = hypervolume_contribution_wfg(&front, &[2.0, 2.0], 2).expect("failed to create hvc2");
assert!(hvc0 >= 0.0 && hvc1 >= 0.0 && hvc2 >= 0.0);
assert!(hvc0 <= total + 1e-10);
assert!(hvc1 <= total + 1e-10);
assert!(hvc2 <= total + 1e-10);
}
#[test]
fn test_hvc_out_of_bounds_error() {
let front = vec![vec![0.0, 1.0]];
let result = hypervolume_contribution_wfg(&front, &[2.0, 2.0], 5);
assert!(result.is_err());
}
#[test]
fn test_exclusive_hv_correct_length() {
let front = vec![vec![0.0, 1.0], vec![0.5, 0.5], vec![1.0, 0.0]];
let hvc = exclusive_hypervolume(&front, &[2.0, 2.0]).expect("failed to create hvc");
assert_eq!(hvc.len(), 3);
}
#[test]
fn test_exclusive_hv_non_negative() {
let front = vec![
vec![0.1, 0.9],
vec![0.3, 0.7],
vec![0.5, 0.5],
vec![0.7, 0.3],
vec![0.9, 0.1],
];
let hvc = exclusive_hypervolume(&front, &[1.5, 1.5]).expect("failed to create hvc");
for (i, &c) in hvc.iter().enumerate() {
assert!(c >= 0.0, "contribution[{i}] = {c} should be >= 0");
}
}
#[test]
fn test_exclusive_hv_empty_front() {
let hvc = exclusive_hypervolume(&[], &[2.0, 2.0]).expect("failed to create hvc");
assert!(hvc.is_empty());
}
#[test]
fn test_exclusive_hv_dominated_gets_zero() {
let front = vec![vec![0.5, 0.5], vec![1.0, 1.0]];
let hvc = exclusive_hypervolume(&front, &[2.0, 2.0]).expect("failed to create hvc");
assert!(hvc[1] < 1e-10, "dominated point contribution = {} should ~0", hvc[1]);
}
#[test]
fn test_exclusive_hv_extreme_points_larger_contribution() {
let front = vec![vec![0.0, 1.0], vec![0.5, 0.5], vec![1.0, 0.0]];
let hvc = exclusive_hypervolume(&front, &[2.0, 2.0]).expect("failed to create hvc");
assert!(hvc[1] >= 0.0, "interior contribution should be >= 0");
}
}