From 2965955874e3b11dcbefcef104cdbdd18f13c78b Mon Sep 17 00:00:00 2001 From: Stephen Waits Date: Mon, 4 May 2026 19:54:16 -0600 Subject: [PATCH] feat(pareto): add Das-Dennis reference-point generator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The standard structured weight/reference vector generator for many-objective MOEAs (NSGA-III, MOEA/D). Generates (H+M-1 choose M-1) points uniformly distributed on the unit simplex by enumerating all integer compositions of `divisions` into `num_objectives` parts and dividing each by `divisions`. Lives in src/pareto/reference_points.rs. Re-exported from the prelude as `das_dennis`. Tests cover: M=2/H=4 → 5 points along the diagonal; M=3/H=12 → 91 points (the canonical NSGA-III 3-objective ref set); each generated point has exactly M components summing to 1 within float tolerance. --- src/pareto/mod.rs | 2 + src/pareto/reference_points.rs | 99 ++++++++++++++++++++++++++++++++++ src/prelude.rs | 4 +- 3 files changed, 103 insertions(+), 2 deletions(-) create mode 100644 src/pareto/reference_points.rs diff --git a/src/pareto/mod.rs b/src/pareto/mod.rs index 41dfa51..c78db97 100644 --- a/src/pareto/mod.rs +++ b/src/pareto/mod.rs @@ -4,10 +4,12 @@ pub mod archive; pub mod crowding; pub mod dominance; pub mod front; +pub mod reference_points; pub mod sort; pub use archive::*; pub use crowding::*; pub use dominance::*; pub use front::*; +pub use reference_points::*; pub use sort::*; diff --git a/src/pareto/reference_points.rs b/src/pareto/reference_points.rs new file mode 100644 index 0000000..dc74189 --- /dev/null +++ b/src/pareto/reference_points.rs @@ -0,0 +1,99 @@ +//! Structured reference-point generators on the unit simplex. + +/// Das–Dennis reference points: all compositions of `divisions` into +/// `num_objectives` non-negative integer parts, divided by `divisions`. +/// +/// Returns `binomial(divisions + num_objectives - 1, num_objectives - 1)` +/// points, each a `Vec` of length `num_objectives` summing to `1.0`. +/// +/// This is the canonical NSGA-III / MOEA/D weight-vector generator. +/// +/// # Panics +/// If `num_objectives == 0`. +pub fn das_dennis(num_objectives: usize, divisions: usize) -> Vec> { + assert!(num_objectives > 0, "das_dennis requires num_objectives >= 1"); + let mut out = Vec::new(); + let mut current = Vec::with_capacity(num_objectives); + recurse(num_objectives, divisions, divisions, &mut current, &mut out); + out +} + +fn recurse( + remaining_axes: usize, + remaining_units: usize, + total: usize, + current: &mut Vec, + out: &mut Vec>, +) { + if remaining_axes == 1 { + current.push(remaining_units); + let scale = (total as f64).max(1.0); + out.push(current.iter().map(|&v| v as f64 / scale).collect()); + current.pop(); + return; + } + for take in 0..=remaining_units { + current.push(take); + recurse(remaining_axes - 1, remaining_units - take, total, current, out); + current.pop(); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn approx_eq(a: f64, b: f64) -> bool { + (a - b).abs() < 1e-12 + } + + #[test] + fn two_objective_four_divisions() { + let pts = das_dennis(2, 4); + // Expected: (0,4),(1,3),(2,2),(3,1),(4,0) → /4 → 5 points. + assert_eq!(pts.len(), 5); + for p in &pts { + assert_eq!(p.len(), 2); + assert!(approx_eq(p[0] + p[1], 1.0)); + } + let first = &pts[0]; + let last = &pts[pts.len() - 1]; + assert!(approx_eq(first[0], 0.0) && approx_eq(first[1], 1.0)); + assert!(approx_eq(last[0], 1.0) && approx_eq(last[1], 0.0)); + } + + #[test] + fn three_objective_twelve_divisions_has_91_points() { + // C(12+3-1, 3-1) = C(14, 2) = 91 — the canonical NSGA-III 3-obj set. + let pts = das_dennis(3, 12); + assert_eq!(pts.len(), 91); + for p in &pts { + assert_eq!(p.len(), 3); + assert!(approx_eq(p.iter().sum::(), 1.0)); + } + } + + #[test] + fn five_objective_six_divisions_has_210_points() { + // C(6+5-1, 5-1) = C(10, 4) = 210. + let pts = das_dennis(5, 6); + assert_eq!(pts.len(), 210); + for p in &pts { + assert!(approx_eq(p.iter().sum::(), 1.0)); + } + } + + #[test] + fn zero_divisions_yields_one_zero_point() { + // With 0 divisions every axis must take 0 → a single all-zero point. + let pts = das_dennis(3, 0); + assert_eq!(pts.len(), 1); + assert_eq!(pts[0], vec![0.0, 0.0, 0.0]); + } + + #[test] + #[should_panic(expected = "num_objectives >= 1")] + fn zero_objectives_panics() { + let _ = das_dennis(0, 4); + } +} diff --git a/src/prelude.rs b/src/prelude.rs index a4e9fe5..8535c1a 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -12,8 +12,8 @@ pub use crate::core::{ pub use crate::traits::{Initializer, Optimizer, Variation}; pub use crate::pareto::{ - Dominance, ParetoArchive, best_candidate, crowding_distance, non_dominated_sort, - pareto_compare, pareto_front, + Dominance, ParetoArchive, best_candidate, crowding_distance, das_dennis, + non_dominated_sort, pareto_compare, pareto_front, }; pub use crate::operators::{