use crate::types::Direction;
#[allow(clippy::module_name_repetitions)]
pub(crate) fn dominates(a: &[f64], b: &[f64], directions: &[Direction]) -> bool {
debug_assert_eq!(a.len(), b.len());
debug_assert_eq!(a.len(), directions.len());
let mut strictly_better = false;
for ((&av, &bv), dir) in a.iter().zip(b.iter()).zip(directions.iter()) {
let better = match dir {
Direction::Minimize => av < bv,
Direction::Maximize => av > bv,
};
let worse = match dir {
Direction::Minimize => av > bv,
Direction::Maximize => av < bv,
};
if worse {
return false;
}
if better {
strictly_better = true;
}
}
strictly_better
}
pub(crate) fn constrained_dominates(
a_values: &[f64],
b_values: &[f64],
a_constraints: &[f64],
b_constraints: &[f64],
directions: &[Direction],
) -> bool {
let a_feasible = a_constraints.iter().all(|&c| c <= 0.0);
let b_feasible = b_constraints.iter().all(|&c| c <= 0.0);
match (a_feasible, b_feasible) {
(true, false) => true,
(false, true) => false,
(false, false) => {
let a_violation: f64 = a_constraints.iter().map(|c| c.max(0.0)).sum();
let b_violation: f64 = b_constraints.iter().map(|c| c.max(0.0)).sum();
a_violation < b_violation
}
(true, true) => dominates(a_values, b_values, directions),
}
}
#[allow(clippy::cast_possible_truncation)]
pub(crate) fn fast_non_dominated_sort(
values: &[Vec<f64>],
directions: &[Direction],
) -> Vec<Vec<usize>> {
fast_non_dominated_sort_constrained(values, directions, &[])
}
#[allow(clippy::cast_possible_truncation)]
pub(crate) fn fast_non_dominated_sort_constrained(
values: &[Vec<f64>],
directions: &[Direction],
constraints: &[Vec<f64>],
) -> Vec<Vec<usize>> {
let n = values.len();
if n == 0 {
return Vec::new();
}
let has_constraints = !constraints.is_empty();
let empty_constraints: Vec<f64> = Vec::new();
let mut dominated_by: Vec<Vec<usize>> = vec![Vec::new(); n];
let mut domination_count: Vec<usize> = vec![0; n];
for i in 0..n {
for j in (i + 1)..n {
let (a_c, b_c) = if has_constraints {
(&constraints[i], &constraints[j])
} else {
(&empty_constraints, &empty_constraints)
};
let i_dom_j = if has_constraints {
constrained_dominates(&values[i], &values[j], a_c, b_c, directions)
} else {
dominates(&values[i], &values[j], directions)
};
let j_dom_i = if has_constraints {
constrained_dominates(&values[j], &values[i], b_c, a_c, directions)
} else {
dominates(&values[j], &values[i], directions)
};
if i_dom_j {
dominated_by[i].push(j);
domination_count[j] += 1;
} else if j_dom_i {
dominated_by[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 &p in ¤t_front {
for &q in &dominated_by[p] {
domination_count[q] -= 1;
if domination_count[q] == 0 {
next_front.push(q);
}
}
}
fronts.push(current_front);
current_front = next_front;
}
fronts
}
#[allow(clippy::cast_precision_loss)]
pub(crate) fn crowding_distance_indexed(front_indices: &[usize], values: &[Vec<f64>]) -> Vec<f64> {
let n = front_indices.len();
if n <= 2 {
return vec![f64::INFINITY; n];
}
let m = values[front_indices[0]].len(); let mut distances = vec![0.0_f64; n];
let val = |front_pos: usize, obj: usize| -> f64 { values[front_indices[front_pos]][obj] };
for obj in 0..m {
let mut sorted: Vec<usize> = (0..n).collect();
sorted.sort_by(|&a, &b| {
val(a, obj)
.partial_cmp(&val(b, obj))
.unwrap_or(core::cmp::Ordering::Equal)
});
distances[sorted[0]] = f64::INFINITY;
distances[sorted[n - 1]] = f64::INFINITY;
let range = val(sorted[n - 1], obj) - val(sorted[0], obj);
if range > 0.0 {
for i in 1..(n - 1) {
distances[sorted[i]] += (val(sorted[i + 1], obj) - val(sorted[i - 1], obj)) / range;
}
}
}
distances
}
#[must_use]
#[allow(clippy::cast_precision_loss)]
pub fn hypervolume(front: &[Vec<f64>], reference_point: &[f64], directions: &[Direction]) -> f64 {
if front.is_empty() {
return 0.0;
}
let d = reference_point.len();
debug_assert!(front.iter().all(|p| p.len() == d));
debug_assert_eq!(d, directions.len());
let normalized: Vec<Vec<f64>> = front
.iter()
.map(|p| {
p.iter()
.zip(directions)
.map(|(&v, dir)| match dir {
Direction::Minimize => v,
Direction::Maximize => -v,
})
.collect()
})
.collect();
let ref_norm: Vec<f64> = reference_point
.iter()
.zip(directions)
.map(|(&v, dir)| match dir {
Direction::Minimize => v,
Direction::Maximize => -v,
})
.collect();
let filtered: Vec<Vec<f64>> = normalized
.into_iter()
.filter(|p| p.iter().zip(&ref_norm).all(|(&pv, &rv)| pv < rv))
.collect();
if filtered.is_empty() {
return 0.0;
}
hv_recursive(&filtered, &ref_norm)
}
#[allow(clippy::cast_precision_loss)]
fn hv_recursive(points: &[Vec<f64>], reference: &[f64]) -> f64 {
let d = reference.len();
if d == 1 {
let min_val = points.iter().map(|p| p[0]).fold(f64::INFINITY, f64::min);
return (reference[0] - min_val).max(0.0);
}
if points.len() == 1 {
return points[0]
.iter()
.zip(reference)
.map(|(&p, &r)| (r - p).max(0.0))
.product();
}
let mut sorted: Vec<&Vec<f64>> = points.iter().collect();
sorted.sort_by(|a, b| {
a[d - 1]
.partial_cmp(&b[d - 1])
.unwrap_or(core::cmp::Ordering::Equal)
});
let sub_ref: Vec<f64> = reference[..d - 1].to_vec();
let mut result = 0.0;
for i in 0..sorted.len() {
let height = if i + 1 < sorted.len() {
sorted[i + 1][d - 1] - sorted[i][d - 1]
} else {
reference[d - 1] - sorted[i][d - 1]
};
if height <= 0.0 {
continue;
}
let projected: Vec<Vec<f64>> = sorted[..=i].iter().map(|p| p[..d - 1].to_vec()).collect();
let non_dom = non_dominated_minimize(&projected);
if !non_dom.is_empty() {
result += height * hv_recursive(&non_dom, &sub_ref);
}
}
result
}
fn non_dominated_minimize(points: &[Vec<f64>]) -> Vec<Vec<f64>> {
let mut result = Vec::new();
'outer: for (i, p) in points.iter().enumerate() {
for (j, q) in points.iter().enumerate() {
if i == j {
continue;
}
let mut all_leq = true;
let mut any_lt = false;
for (&qv, &pv) in q.iter().zip(p.iter()) {
if qv > pv {
all_leq = false;
break;
}
if qv < pv {
any_lt = true;
}
}
if all_leq && any_lt {
continue 'outer;
}
}
result.push(p.clone());
}
result
}
#[must_use]
pub fn non_dominated_sort(solutions: &[Vec<f64>], directions: &[Direction]) -> Vec<Vec<usize>> {
fast_non_dominated_sort(solutions, directions)
}
#[must_use]
pub fn pareto_front_indices(solutions: &[Vec<f64>], directions: &[Direction]) -> Vec<usize> {
let fronts = fast_non_dominated_sort(solutions, directions);
fronts.into_iter().next().unwrap_or_default()
}
#[must_use]
#[allow(clippy::cast_precision_loss, clippy::needless_range_loop)]
pub fn crowding_distance(front: &[Vec<f64>], _directions: &[Direction]) -> Vec<f64> {
let n = front.len();
if n <= 2 {
return vec![f64::INFINITY; n];
}
let m = front[0].len();
let mut distances = vec![0.0_f64; n];
for obj in 0..m {
let mut sorted: Vec<usize> = (0..n).collect();
sorted.sort_by(|&a, &b| {
front[a][obj]
.partial_cmp(&front[b][obj])
.unwrap_or(core::cmp::Ordering::Equal)
});
distances[sorted[0]] = f64::INFINITY;
distances[sorted[n - 1]] = f64::INFINITY;
let range = front[sorted[n - 1]][obj] - front[sorted[0]][obj];
if range > 0.0 {
for i in 1..(n - 1) {
distances[sorted[i]] +=
(front[sorted[i + 1]][obj] - front[sorted[i - 1]][obj]) / range;
}
}
}
distances
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_dominates_basic() {
let dirs = [Direction::Minimize, Direction::Minimize];
assert!(dominates(&[1.0, 1.0], &[2.0, 2.0], &dirs));
assert!(!dominates(&[2.0, 2.0], &[1.0, 1.0], &dirs));
assert!(!dominates(&[1.0, 1.0], &[1.0, 1.0], &dirs));
}
#[test]
fn test_dominates_incomparable() {
let dirs = [Direction::Minimize, Direction::Minimize];
assert!(!dominates(&[1.0, 3.0], &[3.0, 1.0], &dirs));
assert!(!dominates(&[3.0, 1.0], &[1.0, 3.0], &dirs));
}
#[test]
fn test_dominates_maximize() {
let dirs = [Direction::Maximize, Direction::Minimize];
assert!(dominates(&[5.0, 1.0], &[3.0, 2.0], &dirs));
assert!(!dominates(&[3.0, 2.0], &[5.0, 1.0], &dirs));
}
#[test]
fn test_nds_known() {
let values = vec![
vec![1.0, 5.0], vec![5.0, 1.0], vec![3.0, 3.0], vec![4.0, 4.0], vec![6.0, 6.0], ];
let dirs = [Direction::Minimize, Direction::Minimize];
let fronts = fast_non_dominated_sort(&values, &dirs);
assert_eq!(fronts.len(), 3);
let mut f0 = fronts[0].clone();
f0.sort_unstable();
assert_eq!(f0, vec![0, 1, 2]);
assert_eq!(fronts[1], vec![3]);
assert_eq!(fronts[2], vec![4]);
}
#[test]
fn test_crowding_indexed_boundaries() {
let values = vec![vec![1.0, 5.0], vec![3.0, 3.0], vec![5.0, 1.0]];
let front = vec![0, 1, 2];
let cd = crowding_distance_indexed(&front, &values);
assert!(cd[0].is_infinite());
assert!(cd[2].is_infinite());
assert!(cd[1].is_finite());
assert!(cd[1] > 0.0);
}
#[test]
fn test_hypervolume_2d_minimize() {
let front = vec![vec![1.0, 3.0], vec![2.0, 2.0], vec![3.0, 1.0]];
let dirs = [Direction::Minimize, Direction::Minimize];
let hv = hypervolume(&front, &[4.0, 4.0], &dirs);
assert!((hv - 6.0).abs() < 1e-10);
}
#[test]
fn test_hypervolume_2d_maximize() {
let front = vec![vec![3.0, 1.0], vec![2.0, 2.0], vec![1.0, 3.0]];
let dirs = [Direction::Maximize, Direction::Maximize];
let hv = hypervolume(&front, &[0.0, 0.0], &dirs);
assert!((hv - 6.0).abs() < 1e-10);
}
#[test]
fn test_hypervolume_single_point() {
let front = vec![vec![1.0, 1.0]];
let dirs = [Direction::Minimize, Direction::Minimize];
let hv = hypervolume(&front, &[3.0, 3.0], &dirs);
assert!((hv - 4.0).abs() < 1e-10);
}
#[test]
fn test_hypervolume_empty_front() {
let front: Vec<Vec<f64>> = vec![];
let dirs = [Direction::Minimize];
assert!(hypervolume(&front, &[1.0], &dirs).abs() < f64::EPSILON);
}
#[test]
fn test_hypervolume_point_at_ref() {
let front = vec![vec![5.0, 5.0]];
let dirs = [Direction::Minimize, Direction::Minimize];
let hv = hypervolume(&front, &[5.0, 5.0], &dirs);
assert!(hv.abs() < f64::EPSILON);
}
#[test]
fn test_hypervolume_3d() {
let front = vec![vec![1.0, 1.0, 1.0]];
let dirs = [
Direction::Minimize,
Direction::Minimize,
Direction::Minimize,
];
let hv = hypervolume(&front, &[2.0, 2.0, 2.0], &dirs);
assert!((hv - 1.0).abs() < 1e-10);
}
#[test]
fn test_non_dominated_sort_public() {
let values = vec![
vec![1.0, 5.0],
vec![5.0, 1.0],
vec![3.0, 3.0],
vec![4.0, 4.0],
];
let dirs = [Direction::Minimize, Direction::Minimize];
let fronts = non_dominated_sort(&values, &dirs);
assert_eq!(fronts.len(), 2);
let mut f0 = fronts[0].clone();
f0.sort_unstable();
assert_eq!(f0, vec![0, 1, 2]);
assert_eq!(fronts[1], vec![3]);
}
#[test]
fn test_pareto_front_indices_basic() {
let values = vec![
vec![1.0, 5.0],
vec![5.0, 1.0],
vec![3.0, 3.0],
vec![4.0, 4.0],
];
let dirs = [Direction::Minimize, Direction::Minimize];
let mut idx = pareto_front_indices(&values, &dirs);
idx.sort_unstable();
assert_eq!(idx, vec![0, 1, 2]);
}
#[test]
fn test_pareto_front_indices_empty() {
let values: Vec<Vec<f64>> = vec![];
let dirs = [Direction::Minimize];
assert!(pareto_front_indices(&values, &dirs).is_empty());
}
#[test]
fn test_crowding_distance_public() {
let front = vec![vec![1.0, 5.0], vec![3.0, 3.0], vec![5.0, 1.0]];
let dirs = [Direction::Minimize, Direction::Minimize];
let cd = crowding_distance(&front, &dirs);
assert!(cd[0].is_infinite());
assert!(cd[2].is_infinite());
assert!(cd[1].is_finite());
assert!(cd[1] > 0.0);
}
#[test]
fn test_crowding_distance_single_point() {
let front = vec![vec![2.0, 3.0]];
let dirs = [Direction::Minimize, Direction::Minimize];
let cd = crowding_distance(&front, &dirs);
assert_eq!(cd.len(), 1);
assert!(cd[0].is_infinite());
}
}