feat(pareto): add Das-Dennis reference-point generator

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.
This commit is contained in:
2026-05-04 19:54:37 -06:00
parent 18d778a00b
commit 2965955874
3 changed files with 103 additions and 2 deletions
+2
View File
@@ -4,10 +4,12 @@ pub mod archive;
pub mod crowding; pub mod crowding;
pub mod dominance; pub mod dominance;
pub mod front; pub mod front;
pub mod reference_points;
pub mod sort; pub mod sort;
pub use archive::*; pub use archive::*;
pub use crowding::*; pub use crowding::*;
pub use dominance::*; pub use dominance::*;
pub use front::*; pub use front::*;
pub use reference_points::*;
pub use sort::*; pub use sort::*;
+99
View File
@@ -0,0 +1,99 @@
//! Structured reference-point generators on the unit simplex.
/// DasDennis 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<f64>` 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<Vec<f64>> {
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<usize>,
out: &mut Vec<Vec<f64>>,
) {
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::<f64>(), 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::<f64>(), 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);
}
}
+2 -2
View File
@@ -12,8 +12,8 @@ pub use crate::core::{
pub use crate::traits::{Initializer, Optimizer, Variation}; pub use crate::traits::{Initializer, Optimizer, Variation};
pub use crate::pareto::{ pub use crate::pareto::{
Dominance, ParetoArchive, best_candidate, crowding_distance, non_dominated_sort, Dominance, ParetoArchive, best_candidate, crowding_distance, das_dennis,
pareto_compare, pareto_front, non_dominated_sort, pareto_compare, pareto_front,
}; };
pub use crate::operators::{ pub use crate::operators::{