use crate::core::candidate::Candidate;
use crate::core::evaluation::Evaluation;
use crate::core::objective::ObjectiveSpace;
pub fn hypervolume_2d<D>(
front: &[Candidate<D>],
objectives: &ObjectiveSpace,
reference_point: [f64; 2],
) -> f64 {
assert_eq!(
objectives.len(),
2,
"hypervolume_2d requires exactly 2 objectives",
);
if front.is_empty() {
return 0.0;
}
let mut points: Vec<[f64; 2]> = front
.iter()
.filter_map(|c| {
let m = objectives.as_minimization(&c.evaluation.objectives);
let p = [m[0], m[1]];
if p[0] < reference_point[0] && p[1] < reference_point[1] {
Some(p)
} else {
None
}
})
.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 area = 0.0;
let mut last_y = reference_point[1];
for p in &points {
if p[1] >= last_y {
continue;
}
let width = reference_point[0] - p[0];
let height = last_y - p[1];
area += width * height;
last_y = p[1];
}
area
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::evaluation::Evaluation;
use crate::core::objective::Objective;
fn cand(obj: Vec<f64>) -> Candidate<()> {
Candidate::new((), Evaluation::new(obj))
}
fn space_min2() -> ObjectiveSpace {
ObjectiveSpace::new(vec![Objective::minimize("f1"), Objective::minimize("f2")])
}
#[test]
fn known_three_point_front_area() {
let s = space_min2();
let front = [
cand(vec![1.0, 3.0]),
cand(vec![2.0, 2.0]),
cand(vec![3.0, 1.0]),
];
let hv = hypervolume_2d(&front, &s, [4.0, 4.0]);
assert!((hv - 6.0).abs() < 1e-12, "expected 6.0, got {hv}");
}
#[test]
fn empty_front_is_zero() {
let s = space_min2();
let front: [Candidate<()>; 0] = [];
assert_eq!(hypervolume_2d(&front, &s, [10.0, 10.0]), 0.0);
}
#[test]
fn point_not_dominating_reference_skipped() {
let s = space_min2();
let front = [cand(vec![2.0, 0.5])];
assert_eq!(hypervolume_2d(&front, &s, [1.0, 1.0]), 0.0);
}
#[test]
fn maximize_axis_handled_via_orientation() {
let s = ObjectiveSpace::new(vec![
Objective::minimize("cost"),
Objective::maximize("score"),
]);
let front = [cand(vec![1.0, 0.9])];
let hv = hypervolume_2d(&front, &s, [2.0, 0.0]);
assert!((hv - 0.9).abs() < 1e-12);
}
#[test]
#[should_panic(expected = "exactly 2 objectives")]
fn panics_on_non_2d() {
let s = ObjectiveSpace::new(vec![Objective::minimize("only")]);
let front = [cand(vec![1.0])];
let _ = hypervolume_2d(&front, &s, [10.0, 10.0]);
}
}
pub fn hypervolume_nd<D>(
front: &[Candidate<D>],
objectives: &ObjectiveSpace,
reference_point: &[f64],
) -> f64 {
assert_eq!(
objectives.len(),
reference_point.len(),
"hypervolume_nd: ObjectiveSpace and reference_point must agree on dimension",
);
assert!(
!reference_point.is_empty(),
"hypervolume_nd: dimension must be >= 1"
);
if front.is_empty() {
return 0.0;
}
let oriented: Vec<Vec<f64>> = front
.iter()
.filter_map(|c| {
let m = objectives.as_minimization(&c.evaluation.objectives);
if m.iter().zip(reference_point.iter()).all(|(p, r)| p < r) {
Some(m)
} else {
None
}
})
.collect();
if oriented.is_empty() {
return 0.0;
}
hso_recursive(&oriented, reference_point)
}
fn hso_recursive(points: &[Vec<f64>], reference: &[f64]) -> f64 {
let m = reference.len();
if m == 1 {
let best = points.iter().map(|p| p[0]).fold(f64::INFINITY, f64::min);
return (reference[0] - best).max(0.0);
}
if m == 2 {
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));
let mut area = 0.0;
let mut last_y = reference[1];
for p in sorted {
if p[1] >= last_y {
continue;
}
let width = reference[0] - p[0];
let height = last_y - p[1];
area += width * height;
last_y = p[1];
}
return area;
}
let last = m - 1;
let mut order: Vec<usize> = (0..points.len()).collect();
order.sort_by(|&i, &j| {
points[i][last]
.partial_cmp(&points[j][last])
.unwrap_or(std::cmp::Ordering::Equal)
});
let projected: Vec<Vec<f64>> = order.iter().map(|&i| points[i][..last].to_vec()).collect();
let sub_reference: &[f64] = &reference[..last];
let mut total = 0.0;
let mut prev = reference[last];
if sub_reference.len() == 2 {
let r0 = sub_reference[0];
let r1 = sub_reference[1];
let mut x_order: Vec<usize> = (0..projected.len()).collect();
x_order.sort_by(|&a, &b| {
projected[a][0]
.partial_cmp(&projected[b][0])
.unwrap_or(std::cmp::Ordering::Equal)
});
for k in (0..order.len()).rev() {
let p_last = points[order[k]][last];
let depth = prev - p_last;
if depth > 0.0 {
let mut area = 0.0;
let mut last_y = r1;
for &pi in &x_order {
if pi > k {
continue;
}
let p = &projected[pi];
if p[1] >= last_y {
continue;
}
area += (r0 - p[0]) * (last_y - p[1]);
last_y = p[1];
}
total += depth * area;
}
prev = p_last;
}
} else {
for k in (0..order.len()).rev() {
let p_last = points[order[k]][last];
let depth = prev - p_last;
if depth > 0.0 {
let active = &projected[..=k];
let nd = non_dominated_projection(active);
total += depth * hso_recursive(&nd, sub_reference);
}
prev = p_last;
}
}
total
}
fn non_dominated_projection(points: &[Vec<f64>]) -> Vec<Vec<f64>> {
let m = if let Some(first) = points.first() {
first.len()
} else {
return Vec::new();
};
let mut out: Vec<Vec<f64>> = Vec::new();
'outer: for p in points {
for q in &out {
if dominates(q, p, m) {
continue 'outer;
}
}
out.retain(|q| !dominates(p, q, m));
out.push(p.clone());
}
out
}
fn dominates(a: &[f64], b: &[f64], m: usize) -> bool {
let mut strictly_better = false;
for i in 0..m {
if a[i] > b[i] {
return false;
}
if a[i] < b[i] {
strictly_better = true;
}
}
strictly_better
}
pub(crate) fn hypervolume_nd_from_evaluations(
evaluations: &[&Evaluation],
objectives: &ObjectiveSpace,
reference_point: &[f64],
) -> f64 {
if evaluations.is_empty() {
return 0.0;
}
let oriented: Vec<Vec<f64>> = evaluations
.iter()
.filter_map(|e| {
let m = objectives.as_minimization(&e.objectives);
if m.iter().zip(reference_point.iter()).all(|(p, r)| p < r) {
Some(m)
} else {
None
}
})
.collect();
if oriented.is_empty() {
return 0.0;
}
hso_recursive(&oriented, reference_point)
}
#[cfg(test)]
mod nd_tests {
use super::*;
use crate::core::evaluation::Evaluation;
use crate::core::objective::Objective;
fn cand_n(obj: Vec<f64>) -> Candidate<()> {
Candidate::new((), Evaluation::new(obj))
}
#[test]
fn nd_matches_2d_on_known_case() {
let s = ObjectiveSpace::new(vec![Objective::minimize("f1"), Objective::minimize("f2")]);
let front = [
cand_n(vec![1.0, 3.0]),
cand_n(vec![2.0, 2.0]),
cand_n(vec![3.0, 1.0]),
];
let hv2 = hypervolume_2d(&front, &s, [4.0, 4.0]);
let hvn = hypervolume_nd(&front, &s, &[4.0, 4.0]);
assert!((hv2 - hvn).abs() < 1e-12, "{hv2} vs {hvn}");
assert!((hvn - 6.0).abs() < 1e-12);
}
#[test]
fn nd_three_d_single_point_at_origin() {
let s = ObjectiveSpace::new(vec![
Objective::minimize("f1"),
Objective::minimize("f2"),
Objective::minimize("f3"),
]);
let front = [cand_n(vec![0.0, 0.0, 0.0])];
let hv = hypervolume_nd(&front, &s, &[1.0, 1.0, 1.0]);
assert!((hv - 1.0).abs() < 1e-12);
}
#[test]
fn nd_three_d_two_points_no_overlap() {
let s = ObjectiveSpace::new(vec![
Objective::minimize("f1"),
Objective::minimize("f2"),
Objective::minimize("f3"),
]);
let front_one = [cand_n(vec![0.0, 1.0, 1.0])];
let front_two = [cand_n(vec![0.0, 1.0, 1.0]), cand_n(vec![1.0, 0.0, 1.0])];
let front_three = [
cand_n(vec![0.0, 1.0, 1.0]),
cand_n(vec![1.0, 0.0, 1.0]),
cand_n(vec![1.0, 1.0, 0.0]),
];
let hv1 = hypervolume_nd(&front_one, &s, &[2.0, 2.0, 2.0]);
let hv2 = hypervolume_nd(&front_two, &s, &[2.0, 2.0, 2.0]);
let hv3 = hypervolume_nd(&front_three, &s, &[2.0, 2.0, 2.0]);
assert!(hv1 < hv2, "{hv1} should be < {hv2}");
assert!(hv2 < hv3, "{hv2} should be < {hv3}");
assert!(hv3 < 8.0);
}
#[test]
fn nd_empty_is_zero() {
let s = ObjectiveSpace::new(vec![
Objective::minimize("f1"),
Objective::minimize("f2"),
Objective::minimize("f3"),
]);
let front: [Candidate<()>; 0] = [];
assert_eq!(hypervolume_nd(&front, &s, &[1.0, 1.0, 1.0]), 0.0);
}
#[test]
fn nd_skips_points_not_dominating_reference() {
let s = ObjectiveSpace::new(vec![
Objective::minimize("f1"),
Objective::minimize("f2"),
Objective::minimize("f3"),
]);
let front = [cand_n(vec![3.0, 0.0, 0.0])];
assert_eq!(hypervolume_nd(&front, &s, &[1.0, 1.0, 1.0]), 0.0);
}
#[test]
#[should_panic(expected = "must agree on dimension")]
fn nd_panics_on_dim_mismatch() {
let s = ObjectiveSpace::new(vec![Objective::minimize("f1"), Objective::minimize("f2")]);
let front = [cand_n(vec![1.0, 1.0])];
let _ = hypervolume_nd(&front, &s, &[1.0, 1.0, 1.0]);
}
#[test]
fn nd_dominated_points_dont_increase_hv() {
let s = ObjectiveSpace::new(vec![
Objective::minimize("f1"),
Objective::minimize("f2"),
Objective::minimize("f3"),
]);
let base = vec![cand_n(vec![0.0, 1.0, 1.0]), cand_n(vec![1.0, 0.0, 1.0])];
let mut with_dominated = base.clone();
with_dominated.push(cand_n(vec![1.5, 1.5, 1.5]));
let hv_base = hypervolume_nd(&base, &s, &[2.0, 2.0, 2.0]);
let hv_with = hypervolume_nd(&with_dominated, &s, &[2.0, 2.0, 2.0]);
assert!((hv_base - hv_with).abs() < 1e-12, "{hv_base} vs {hv_with}");
}
#[test]
fn dominates_strict_and_boundary_cases() {
assert!(dominates(&[1.0, 1.0], &[2.0, 2.0], 2));
assert!(!dominates(&[2.0, 2.0], &[1.0, 1.0], 2));
assert!(!dominates(&[1.0, 1.0], &[1.0, 1.0], 2));
assert!(dominates(&[1.0, 2.0], &[2.0, 2.0], 2));
assert!(!dominates(&[1.0, 3.0], &[2.0, 2.0], 2));
}
#[test]
fn non_dominated_projection_drops_dominated() {
let pts = vec![
vec![1.0, 3.0], vec![3.0, 1.0], vec![2.0, 2.0], vec![4.0, 4.0], ];
let nd = non_dominated_projection(&pts);
assert_eq!(nd.len(), 3);
assert!(!nd.contains(&vec![4.0, 4.0]));
assert!(nd.contains(&vec![1.0, 3.0]));
assert!(nd.contains(&vec![3.0, 1.0]));
assert!(nd.contains(&vec![2.0, 2.0]));
}
#[test]
fn non_dominated_projection_empty_input_is_empty() {
let pts: Vec<Vec<f64>> = Vec::new();
assert!(non_dominated_projection(&pts).is_empty());
}
#[test]
fn non_dominated_projection_all_nondominated_keeps_all() {
let pts = vec![vec![1.0, 3.0], vec![2.0, 2.0], vec![3.0, 1.0]];
let nd = non_dominated_projection(&pts);
assert_eq!(nd.len(), 3);
}
#[test]
fn hso_recursive_1d_base_case() {
let pts = vec![vec![0.5], vec![1.5], vec![0.2]];
assert!((hso_recursive(&pts, &[2.0]) - 1.8).abs() < 1e-12);
let pts2 = vec![vec![3.0]];
assert_eq!(hso_recursive(&pts2, &[2.0]), 0.0);
}
#[test]
fn hso_recursive_2d_staircase() {
let pts = vec![vec![1.0, 3.0], vec![2.0, 2.0], vec![3.0, 1.0]];
let hv = hso_recursive(&pts, &[4.0, 4.0]);
assert!((hv - 6.0).abs() < 1e-12, "hv = {hv}");
}
#[test]
fn hypervolume_nd_from_evaluations_empty_and_nonempty() {
let s = ObjectiveSpace::new(vec![Objective::minimize("f1"), Objective::minimize("f2")]);
let empty: Vec<&Evaluation> = Vec::new();
assert_eq!(
hypervolume_nd_from_evaluations(&empty, &s, &[2.0, 2.0]),
0.0
);
let e = Evaluation::new(vec![1.0, 1.0]);
let evals = vec![&e];
let hv = hypervolume_nd_from_evaluations(&evals, &s, &[2.0, 2.0]);
assert!((hv - 1.0).abs() < 1e-12, "hv = {hv}");
}
#[test]
fn hypervolume_nd_from_evaluations_skips_non_dominating() {
let s = ObjectiveSpace::new(vec![Objective::minimize("f1"), Objective::minimize("f2")]);
let e = Evaluation::new(vec![2.0, 1.0]);
let evals = vec![&e];
assert_eq!(
hypervolume_nd_from_evaluations(&evals, &s, &[2.0, 2.0]),
0.0
);
}
}