use super::Objective;
pub fn dominates(a: &[f64], b: &[f64], objectives: &[Objective]) -> bool {
debug_assert!(a.len() == b.len() && a.len() == objectives.len());
let mut strictly_better_somewhere = false;
for ((&ai, &bi), &obj) in a.iter().zip(b.iter()).zip(objectives.iter()) {
let (xa, xb) = (obj.min_sign() * ai, obj.min_sign() * bi);
if xa > xb {
return false; }
if xa < xb {
strictly_better_somewhere = true;
}
}
strictly_better_somewhere
}
pub fn pareto_front(points: &[Vec<f64>], objectives: &[Objective]) -> Vec<usize> {
(0..points.len())
.filter(|&i| {
!(0..points.len()).any(|j| j != i && dominates(&points[j], &points[i], objectives))
})
.collect()
}
#[derive(Clone, Debug, PartialEq, serde::Serialize)]
pub struct KneePoint {
pub index: usize,
pub distance: f64,
pub anchors: (usize, usize),
}
pub fn knee_point(points: &[Vec<f64>], objectives: &[Objective]) -> Option<KneePoint> {
let front = pareto_front(points, objectives);
knee_of_front(points, objectives, &front)
}
pub fn knee_of_front(
points: &[Vec<f64>],
objectives: &[Objective],
front: &[usize],
) -> Option<KneePoint> {
if front.is_empty() {
return None;
}
if front.len() == 1 {
return Some(KneePoint {
index: front[0],
distance: 0.0,
anchors: (front[0], front[0]),
});
}
let m = objectives.len();
let mut lo = vec![f64::INFINITY; m];
let mut hi = vec![f64::NEG_INFINITY; m];
for &idx in front {
for k in 0..m {
let x = objectives[k].min_sign() * points[idx][k];
lo[k] = lo[k].min(x);
hi[k] = hi[k].max(x);
}
}
let norm = |idx: usize| -> Vec<f64> {
(0..m)
.map(|k| {
let range = hi[k] - lo[k];
if range <= 0.0 {
0.0
} else {
(objectives[k].min_sign() * points[idx][k] - lo[k]) / range
}
})
.collect()
};
let normed: Vec<Vec<f64>> = front.iter().map(|&i| norm(i)).collect();
let mut anchor_a = 0usize;
let mut anchor_b = if front.len() > 1 { 1 } else { 0 };
let mut best_d2 = -1.0;
for i in 0..front.len() {
for j in (i + 1)..front.len() {
let d2: f64 = normed[i]
.iter()
.zip(normed[j].iter())
.map(|(a, b)| (a - b) * (a - b))
.sum();
if d2 > best_d2 {
best_d2 = d2;
anchor_a = i;
anchor_b = j;
}
}
}
let p0 = &normed[anchor_a];
let p1 = &normed[anchor_b];
let dir: Vec<f64> = p1.iter().zip(p0.iter()).map(|(b, a)| b - a).collect();
let dir_norm = dir.iter().map(|x| x * x).sum::<f64>().sqrt();
let mut best_idx = anchor_a;
let mut best_dist = 0.0;
for (local, q) in normed.iter().enumerate() {
let dist = if dir_norm <= 0.0 {
0.0
} else {
let qp: Vec<f64> = q.iter().zip(p0.iter()).map(|(a, b)| a - b).collect();
let dot: f64 = qp.iter().zip(dir.iter()).map(|(a, b)| a * b).sum::<f64>() / dir_norm;
let perp2: f64 = qp
.iter()
.zip(dir.iter())
.map(|(a, b)| {
let proj = dot * (b / dir_norm);
(a - proj) * (a - proj)
})
.sum();
perp2.max(0.0).sqrt()
};
if dist > best_dist {
best_dist = dist;
best_idx = local;
}
}
Some(KneePoint {
index: front[best_idx],
distance: best_dist,
anchors: (front[anchor_a], front[anchor_b]),
})
}
#[cfg(test)]
mod tests {
use super::Objective::{Max, Min};
use super::*;
#[test]
fn dominance_known_answers_min_min() {
let o = [Min, Min];
assert!(dominates(&[2.0, 2.0], &[4.0, 4.0], &o));
assert!(!dominates(&[4.0, 4.0], &[2.0, 2.0], &o));
assert!(!dominates(&[1.0, 4.0], &[4.0, 1.0], &o)); assert!(!dominates(&[2.0, 2.0], &[2.0, 2.0], &o)); assert!(dominates(&[2.0, 1.0], &[2.0, 2.0], &o));
}
#[test]
fn dominance_respects_per_objective_direction() {
let o = [Max, Min];
assert!(dominates(&[5.0, 1.0], &[3.0, 2.0], &o)); assert!(!dominates(&[3.0, 2.0], &[5.0, 1.0], &o));
}
#[test]
fn pareto_front_selects_the_non_dominated_set() {
let pts = vec![
vec![1.0, 4.0], vec![2.0, 2.0], vec![3.0, 1.0], vec![4.0, 4.0], vec![2.5, 3.0], ];
let o = [Min, Min];
assert_eq!(pareto_front(&pts, &o), vec![0, 1, 2]);
}
#[test]
fn pareto_front_all_non_dominated_when_strictly_trading_off() {
let pts = vec![vec![0.0, 1.0], vec![0.5, 0.5], vec![1.0, 0.0]];
let o = [Min, Min];
assert_eq!(pareto_front(&pts, &o), vec![0, 1, 2]);
}
#[test]
fn knee_point_is_the_corner_of_a_convex_front() {
let pts = vec![
vec![0.0, 1.0], vec![0.1, 0.2], vec![0.4, 0.1], vec![1.0, 0.0], ];
let o = [Min, Min];
let k = knee_point(&pts, &o).unwrap();
assert_eq!(k.index, 1);
assert_eq!(k.anchors, (0, 3));
assert!(k.distance > 0.4 && k.distance < 0.6, "dist {}", k.distance);
}
#[test]
fn knee_point_is_orientation_invariant_under_maximise() {
let pts = vec![
vec![1.0, 0.0],
vec![0.9, 0.8], vec![0.6, 0.9],
vec![0.0, 1.0],
];
let o = [Max, Max];
let k = knee_point(&pts, &o).unwrap();
assert_eq!(k.index, 1);
}
#[test]
fn knee_of_degenerate_fronts() {
let o = [Min, Min];
assert!(knee_point(&[], &o).is_none());
let one = knee_point(&[vec![1.0, 2.0]], &o).unwrap();
assert_eq!(one.index, 0);
assert_eq!(one.distance, 0.0);
}
}