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::{