From 9d64a0f186c2c8717ecb7e8bafa0a844bc112d73 Mon Sep 17 00:00:00 2001 From: Stephen Waits Date: Mon, 4 May 2026 19:19:19 -0600 Subject: [PATCH] feat(pareto): add non_dominated_sort MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Deb's fast non-dominated sort: returns Vec> of front indices into the input population, with fronts[0] being the non-dominated set. O(N²·M) is acceptable for v1 (spec §9.5). Tests cover: small known population produces expected fronts; equal candidates land on the same front; an empty population yields no fronts. --- src/pareto/mod.rs | 2 + src/pareto/sort.rs | 123 +++++++++++++++++++++++++++++++++++++++++++++ src/prelude.rs | 4 +- 3 files changed, 128 insertions(+), 1 deletion(-) create mode 100644 src/pareto/sort.rs diff --git a/src/pareto/mod.rs b/src/pareto/mod.rs index 6966ada..3d24d37 100644 --- a/src/pareto/mod.rs +++ b/src/pareto/mod.rs @@ -2,6 +2,8 @@ pub mod dominance; pub mod front; +pub mod sort; pub use dominance::*; pub use front::*; +pub use sort::*; diff --git a/src/pareto/sort.rs b/src/pareto/sort.rs new file mode 100644 index 0000000..cc774f7 --- /dev/null +++ b/src/pareto/sort.rs @@ -0,0 +1,123 @@ +//! Fast non-dominated sorting (Deb et al., NSGA-II). + +use crate::core::candidate::Candidate; +use crate::core::objective::ObjectiveSpace; +use crate::pareto::dominance::{Dominance, pareto_compare}; + +/// Partition the population into Pareto fronts by dominance rank. +/// +/// `fronts[0]` is the non-dominated set, `fronts[1]` is what becomes +/// non-dominated after removing `fronts[0]`, and so on. Each entry is an index +/// into the input population. Equal-objective candidates land on the same +/// front. O(N²·M) is acceptable for v1 (spec §9.5). +pub fn non_dominated_sort( + population: &[Candidate], + objectives: &ObjectiveSpace, +) -> Vec> { + let n = population.len(); + if n == 0 { + return Vec::new(); + } + + let mut dominates: Vec> = vec![Vec::new(); n]; + let mut dominated_by_count: Vec = vec![0; n]; + let mut fronts: Vec> = Vec::new(); + let mut first_front: Vec = Vec::new(); + + for i in 0..n { + for j in 0..n { + if i == j { + continue; + } + match pareto_compare( + &population[i].evaluation, + &population[j].evaluation, + objectives, + ) { + Dominance::Dominates => dominates[i].push(j), + Dominance::DominatedBy => dominated_by_count[i] += 1, + _ => {} + } + } + if dominated_by_count[i] == 0 { + first_front.push(i); + } + } + + fronts.push(first_front); + let mut k = 0; + while k < fronts.len() && !fronts[k].is_empty() { + let mut next: Vec = Vec::new(); + // Borrow-friendly: collect dominated indices for the current front first. + let to_visit: Vec = fronts[k].clone(); + for i in to_visit { + for &j in &dominates[i] { + dominated_by_count[j] -= 1; + if dominated_by_count[j] == 0 { + next.push(j); + } + } + } + if next.is_empty() { + break; + } + fronts.push(next); + k += 1; + } + + fronts +} + +#[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_population_no_fronts() { + let s = space_min2(); + let fronts = non_dominated_sort::<()>(&[], &s); + assert!(fronts.is_empty()); + } + + #[test] + fn known_population_yields_expected_fronts() { + let s = space_min2(); + // Indices 0..4 deliberately mix layers: + // 0: (1, 5) ← front 0 + // 1: (2, 3) ← front 0 + // 2: (4, 1) ← front 0 + // 3: (3, 4) ← front 1 (dominated by 1) + // 4: (5, 6) ← front 2 (dominated by 1, 2, 3) + let pop = [ + cand(vec![1.0, 5.0]), + cand(vec![2.0, 3.0]), + cand(vec![4.0, 1.0]), + cand(vec![3.0, 4.0]), + cand(vec![5.0, 6.0]), + ]; + let fronts = non_dominated_sort(&pop, &s); + assert_eq!(fronts.len(), 3); + let mut f0 = fronts[0].clone(); + let mut f1 = fronts[1].clone(); + let mut f2 = fronts[2].clone(); + f0.sort(); + f1.sort(); + f2.sort(); + assert_eq!(f0, vec![0, 1, 2]); + assert_eq!(f1, vec![3]); + assert_eq!(f2, vec![4]); + } +} diff --git a/src/prelude.rs b/src/prelude.rs index 4e64132..16ce109 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -11,4 +11,6 @@ pub use crate::core::{ pub use crate::traits::{Initializer, Optimizer, Variation}; -pub use crate::pareto::{Dominance, best_candidate, pareto_compare, pareto_front}; +pub use crate::pareto::{ + Dominance, best_candidate, non_dominated_sort, pareto_compare, pareto_front, +};