//! Schott's spacing metric for Pareto fronts. use crate::core::candidate::Candidate; use crate::core::objective::ObjectiveSpace; /// Schott's spacing metric. /// /// For each point on the front, compute the Manhattan distance to its nearest /// neighbor (in minimization-oriented objective space). The metric is the /// (population) standard deviation of those per-point distances. A perfectly /// uniform front has spacing 0. /// /// Returns `0.0` for empty or single-point fronts (spec §14.1). /// /// # Example /// /// ``` /// use heuropt::prelude::*; /// use heuropt::metrics::spacing; /// /// let space = ObjectiveSpace::new(vec![ /// Objective::minimize("f1"), /// Objective::minimize("f2"), /// ]); /// // Five points evenly spaced on a line — spacing should be 0. /// let front: Vec> = (0..5) /// .map(|i| { /// let t = i as f64; /// Candidate::new((), Evaluation::new(vec![t, 4.0 - t])) /// }) /// .collect(); /// assert!(spacing(&front, &space) < 1e-12); /// ``` pub fn spacing(front: &[Candidate], objectives: &ObjectiveSpace) -> f64 { let n = front.len(); if n < 2 { return 0.0; } let oriented: Vec> = front .iter() .map(|c| objectives.as_minimization(&c.evaluation.objectives)) .collect(); let mut nearest = vec![f64::INFINITY; n]; for i in 0..n { for j in 0..n { if i == j { continue; } let d: f64 = oriented[i] .iter() .zip(oriented[j].iter()) .map(|(a, b)| (a - b).abs()) .sum(); if d < nearest[i] { nearest[i] = d; } } } let mean = nearest.iter().sum::() / n as f64; let variance = nearest.iter().map(|d| (d - mean).powi(2)).sum::() / n as f64; variance.sqrt() } #[cfg(test)] mod tests { use super::*; use crate::core::evaluation::Evaluation; use crate::core::objective::Objective; fn cand(obj: Vec) -> Candidate<()> { Candidate::new((), Evaluation::new(obj)) } fn space_min2() -> ObjectiveSpace { ObjectiveSpace::new(vec![Objective::minimize("f1"), Objective::minimize("f2")]) } #[test] fn empty_front_is_zero() { let s = space_min2(); let pts: [Candidate<()>; 0] = []; assert_eq!(spacing(&pts, &s), 0.0); } #[test] fn single_point_is_zero() { let s = space_min2(); assert_eq!(spacing(&[cand(vec![1.0, 1.0])], &s), 0.0); } #[test] fn uniform_front_has_zero_spacing() { let s = space_min2(); // Points evenly spaced along a line: each interior point's nearest // neighbor is at the same distance as its boundary neighbors', // and the boundary points share that distance too. let pts = [ cand(vec![0.0, 4.0]), cand(vec![1.0, 3.0]), cand(vec![2.0, 2.0]), cand(vec![3.0, 1.0]), cand(vec![4.0, 0.0]), ]; let s_val = spacing(&pts, &s); assert!(s_val.abs() < 1e-12); } #[test] fn non_uniform_front_has_positive_spacing() { let s = space_min2(); // Clustered + isolated points → uneven nearest-neighbor distances. let pts = [ cand(vec![0.0, 0.0]), cand(vec![0.1, 0.1]), cand(vec![5.0, 5.0]), ]; let s_val = spacing(&pts, &s); assert!(s_val > 0.0); } }