feat(pareto): add non_dominated_sort
Deb's fast non-dominated sort: returns Vec<Vec<usize>> 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.
This commit is contained in:
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
pub mod dominance;
|
pub mod dominance;
|
||||||
pub mod front;
|
pub mod front;
|
||||||
|
pub mod sort;
|
||||||
|
|
||||||
pub use dominance::*;
|
pub use dominance::*;
|
||||||
pub use front::*;
|
pub use front::*;
|
||||||
|
pub use sort::*;
|
||||||
|
|||||||
@@ -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<D>(
|
||||||
|
population: &[Candidate<D>],
|
||||||
|
objectives: &ObjectiveSpace,
|
||||||
|
) -> Vec<Vec<usize>> {
|
||||||
|
let n = population.len();
|
||||||
|
if n == 0 {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut dominates: Vec<Vec<usize>> = vec![Vec::new(); n];
|
||||||
|
let mut dominated_by_count: Vec<usize> = vec![0; n];
|
||||||
|
let mut fronts: Vec<Vec<usize>> = Vec::new();
|
||||||
|
let mut first_front: Vec<usize> = 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<usize> = Vec::new();
|
||||||
|
// Borrow-friendly: collect dominated indices for the current front first.
|
||||||
|
let to_visit: Vec<usize> = 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<f64>) -> 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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
+3
-1
@@ -11,4 +11,6 @@ pub use crate::core::{
|
|||||||
|
|
||||||
pub use crate::traits::{Initializer, Optimizer, Variation};
|
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,
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user