From 69e5dd1249db7c5b5e09285e4866aba632f6c8cd Mon Sep 17 00:00:00 2001 From: Stephen Waits Date: Mon, 4 May 2026 19:25:37 -0600 Subject: [PATCH] feat(metrics): add Schott spacing metric for Pareto fronts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Standard Schott spacing: for each front point compute the Manhattan distance to its nearest neighbor on minimization-oriented objective values; the spacing metric is the population standard deviation of those nearest-neighbor distances. Returns 0.0 for empty or single-point fronts (spec §14.1). --- src/lib.rs | 1 + src/metrics/mod.rs | 5 ++ src/metrics/spacing.rs | 106 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 112 insertions(+) create mode 100644 src/metrics/mod.rs create mode 100644 src/metrics/spacing.rs diff --git a/src/lib.rs b/src/lib.rs index ce754aa..0d211cc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,6 +4,7 @@ pub mod algorithms; pub mod core; +pub mod metrics; pub mod operators; pub mod pareto; pub mod prelude; diff --git a/src/metrics/mod.rs b/src/metrics/mod.rs new file mode 100644 index 0000000..3458c5f --- /dev/null +++ b/src/metrics/mod.rs @@ -0,0 +1,5 @@ +//! Quality metrics for Pareto fronts. + +pub mod spacing; + +pub use spacing::*; diff --git a/src/metrics/spacing.rs b/src/metrics/spacing.rs new file mode 100644 index 0000000..8f25a5e --- /dev/null +++ b/src/metrics/spacing.rs @@ -0,0 +1,106 @@ +//! 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). +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); + } +}