use std::borrow::Cow;
use ndarray::{Array1, Array2, Axis, s};
use ndarray_stats::QuantileExt;
use crate::{
genetic::{D12, PopulationMOO},
helpers::extreme_points::get_ideal,
non_dominated_sorting::build_fronts,
operators::survival::{SurvivalOperator, moo::helpers::HyperPlaneNormalization},
random::RandomGenerator,
};
struct Nsga3HyperPlaneNormalization;
impl Nsga3HyperPlaneNormalization {
pub fn new() -> Self {
Self
}
}
impl HyperPlaneNormalization for Nsga3HyperPlaneNormalization {
fn compute_extreme_points(&self, translated_population: &Array2<f64>) -> Array2<f64> {
let num_objectives = translated_population.ncols();
let mut extreme_points = Array2::<f64>::zeros((num_objectives, num_objectives));
for j in 0..num_objectives {
let mut weight = Array1::<f64>::from_elem(num_objectives, 1e-6);
weight[j] = 1.0;
let asf_values: Vec<f64> = translated_population
.outer_iter()
.map(|solution| asf(&solution.to_owned(), &weight))
.collect();
let asf_array = Array1::from(asf_values);
let best_idx = asf_array.argmin().unwrap();
let extreme = translated_population.row(best_idx);
extreme_points.slice_mut(s![j, ..]).assign(&extreme);
}
extreme_points
}
}
#[derive(Debug, Clone, Default)]
pub struct Nsga3ReferencePointsSurvival {
reference_points: Array2<f64>, are_aspirational: bool,
}
impl Nsga3ReferencePointsSurvival {
pub fn new(reference_points: Array2<f64>, are_aspirational: bool) -> Self {
Self {
reference_points,
are_aspirational,
}
}
}
impl SurvivalOperator for Nsga3ReferencePointsSurvival {
type FDim = ndarray::Ix2;
fn operate<ConstrDim>(
&mut self,
population: PopulationMOO<ConstrDim>,
num_survive: usize,
rng: &mut impl RandomGenerator,
) -> PopulationMOO<ConstrDim>
where
ConstrDim: D12,
{
let mut fronts = build_fronts(population, num_survive);
let mut survivors: Option<PopulationMOO<ConstrDim>> = None;
let mut n_survivors = 0;
let drained = fronts.drain(..).enumerate();
for (_i, front) in drained {
let front_len = front.len();
if n_survivors + front_len <= num_survive {
survivors = Some(match survivors {
Some(acc) => PopulationMOO::merge(&acc, &front),
None => front,
});
n_survivors += front_len;
} else {
let remaining = num_survive - n_survivors;
if remaining > 0 {
let (st, n_complete) = match &survivors {
Some(acc) => (PopulationMOO::merge(&acc, &front), acc.len()),
None => (front, 0),
};
let z_min = get_ideal(&st.fitness);
let translated_population = &st.fitness - &z_min;
let normalizer = Nsga3HyperPlaneNormalization::new();
let intercepts =
normalizer.compute_hyperplane_intercepts(&translated_population);
let normalized_fitness = &translated_population / (&intercepts - &z_min);
let zr: Cow<Array2<f64>> = if self.are_aspirational {
let normalized_zr =
(&self.reference_points - &z_min) / (&intercepts - &z_min);
Cow::Owned(normalized_zr)
} else {
Cow::Borrowed(&self.reference_points)
};
let (assignments, distances) = associate(&normalized_fitness, &zr);
let survivors_assignments = &assignments[0..n_complete];
let mut niche_counts = compute_niche_counts(survivors_assignments, zr.nrows());
let mut splitting_indices: Vec<usize> = (n_complete..st.len()).collect();
let chosen_indices = niching(
remaining,
&mut niche_counts,
&assignments,
&distances,
&mut splitting_indices,
rng,
);
let selection_from_splitting_front = st.selected(&chosen_indices);
survivors = Some(match survivors {
Some(acc) => PopulationMOO::merge(&acc, &selection_from_splitting_front),
None => selection_from_splitting_front,
});
}
break;
}
}
survivors.expect("Failed to build survivors")
}
}
fn asf(x: &Array1<f64>, w: &Array1<f64>) -> f64 {
let ratios = x / w;
ratios.fold(std::f64::MIN, |acc, &val| acc.max(val))
}
fn associate(st_fitness: &Array2<f64>, zr: &Array2<f64>) -> (Vec<usize>, Vec<f64>) {
let n = st_fitness.nrows();
let norm_s_sq: Array1<f64> = st_fitness.outer_iter().map(|s| s.dot(&s)).collect();
let norm_w_sq: Array1<f64> = zr.outer_iter().map(|w| w.dot(&w)).collect();
let dot = st_fitness.dot(&zr.t());
let norm_s_sq = norm_s_sq.insert_axis(Axis(1)); let norm_w_sq = norm_w_sq.insert_axis(Axis(0));
let dot_sq = dot.mapv(|x| x * x);
let d2 = &norm_s_sq - &dot_sq / &norm_w_sq;
let mut assignments = Vec::with_capacity(n);
let mut distances = Vec::with_capacity(n);
for row in d2.outer_iter() {
let (min_idx, &min_val) = row
.indexed_iter()
.min_by(|a, b| a.1.partial_cmp(b.1).unwrap())
.unwrap();
assignments.push(min_idx);
distances.push(min_val);
}
(assignments, distances)
}
fn compute_niche_counts(assignments: &[usize], n_references: usize) -> Vec<usize> {
let mut niche_counts = vec![0; n_references];
for &assigned_ref in assignments.iter() {
niche_counts[assigned_ref] += 1;
}
niche_counts
}
fn niching(
mut n_remaining: usize,
niche_counts: &mut Vec<usize>,
assignments: &Vec<usize>,
distances: &Vec<f64>,
splitting_front: &mut Vec<usize>,
rng: &mut impl RandomGenerator,
) -> Vec<usize> {
let mut available_refs: Vec<usize> = (0..niche_counts.len()).collect();
let mut pt_next = Vec::new();
while n_remaining > 0 {
if available_refs.is_empty() {
break;
}
let min_count = available_refs
.iter()
.map(|&j| niche_counts[j])
.min()
.unwrap(); let jmin: Vec<usize> = available_refs
.iter()
.copied()
.filter(|&j| niche_counts[j] == min_count)
.collect();
let j_bar = *rng.choose_usize(&jmin).unwrap();
let i_j_bar: Vec<usize> = splitting_front
.iter()
.copied()
.filter(|&s| assignments[s] == j_bar)
.collect();
if !i_j_bar.is_empty() {
let s_chosen = if niche_counts[j_bar] == 0 {
*i_j_bar
.iter()
.min_by(|&&s1, &&s2| distances[s1].partial_cmp(&distances[s2]).unwrap())
.unwrap()
} else {
*rng.choose_usize(&i_j_bar).unwrap()
};
pt_next.push(s_chosen);
if let Some(pos) = splitting_front.iter().position(|&s| s == s_chosen) {
splitting_front.remove(pos);
}
niche_counts[j_bar] += 1;
n_remaining -= 1;
} else {
if let Some(pos) = available_refs.iter().position(|&j| j == j_bar) {
available_refs.remove(pos);
}
}
}
pt_next
}
#[cfg(test)]
mod tests {
use super::*;
use crate::random::{RandomGenerator, TestDummyRng};
use ndarray::array;
#[test]
fn test_asf_with_identity_weights() {
let x = array![0.2, 0.5, 0.3];
let w1 = array![1.0, 1e-6, 1e-6];
let asf1 = asf(&x, &w1);
assert_eq!(asf1, 500000.0);
let w2 = array![1e-6, 1.0, 1e-6];
let asf2 = asf(&x, &w2);
assert_eq!(asf2, 300000.0);
let w3 = array![1e-6, 1e-6, 1.0];
let asf3 = asf(&x, &w3);
assert_eq!(asf3, 500000.0);
}
#[test]
fn test_compute_extreme_points() {
let pop = array![[1.0, 10.0], [10.0, 1.0]];
let normalizer = Nsga3HyperPlaneNormalization::new();
let extreme = normalizer.compute_extreme_points(&pop);
let expected = array![[10.0, 1.0], [1.0, 10.0]];
assert_eq!(
extreme, expected,
"Computed extreme points do not match expected values"
);
}
#[test]
fn test_associate() {
let st_fitness = array![[1.0, 10.0], [10.0, 1.0]];
let zr = array![[1.0, 0.0], [0.0, 1.0]];
let (assignments, distances) = associate(&st_fitness, &zr);
assert_eq!(assignments, vec![1, 0]);
for (i, d) in distances.iter().enumerate() {
assert!(
((*d) - 1.0).abs() < 1e-5,
"Solution {}: expected distance 1, got {}",
i,
d
);
}
}
#[test]
fn test_compute_niche_counts() {
let assignments = vec![0, 1, 0, 1, 1];
let n_references = 2;
let niche_counts = compute_niche_counts(&assignments, n_references);
assert_eq!(niche_counts, vec![2, 3]);
}
struct FakeRandomGenerator {
dummy: TestDummyRng,
}
impl FakeRandomGenerator {
fn new() -> Self {
Self {
dummy: TestDummyRng,
}
}
}
impl RandomGenerator for FakeRandomGenerator {
type R = TestDummyRng;
fn rng(&mut self) -> &mut TestDummyRng {
&mut self.dummy
}
fn choose_usize<'a>(&mut self, vector: &'a [usize]) -> Option<&'a usize> {
vector.first()
}
}
#[test]
fn test_niching() {
let assignments = vec![0, 1, 0, 1]; let distances = vec![10.0, 20.0, 30.0, 40.0]; let mut niche_counts = vec![0, 0]; let mut splitting_front = vec![0, 1, 2, 3]; let n_remaining = 2;
let mut dummy_rng = FakeRandomGenerator::new();
let chosen = niching(
n_remaining,
&mut niche_counts,
&assignments,
&distances,
&mut splitting_front,
&mut dummy_rng,
);
assert_eq!(chosen, vec![0, 1]);
}
#[test]
fn test_operate_split_first_front_content() {
let fitness = array![[1.0, 1.0], [2.0, 2.0], [3.0, 3.0], [4.0, 4.0], [5.0, 5.0]];
let population = PopulationMOO::new_unconstrained(fitness.clone(), fitness.clone());
let mut survival_operator = Nsga3ReferencePointsSurvival::new(Array2::eye(2), false);
let mut rng = FakeRandomGenerator::new();
let survivors = survival_operator.operate(population, 3, &mut rng);
assert_eq!(survivors.len(), 3, "Final survivors count should be 3");
for survivor in survivors.fitness.outer_iter() {
let mut found = false;
for orig in fitness.outer_iter() {
if survivor == orig {
found = true;
break;
}
}
assert!(
found,
"Survivor row {:?} not found in original front",
survivor
);
}
}
#[test]
fn test_operate_split_later_front_content() {
let fitness = array![
[1.0, 1.0],
[1.1, 1.1],
[1.2, 1.2],
[2.0, 2.0],
[2.1, 2.1],
[2.2, 2.2],
[2.3, 2.3]
];
let population = PopulationMOO::new_unconstrained(fitness.clone(), fitness.clone());
let reference_points = Array2::eye(2);
let are_aspirational = false;
let mut survival_operator =
Nsga3ReferencePointsSurvival::new(reference_points, are_aspirational);
let mut rng = FakeRandomGenerator::new();
let survivors = survival_operator.operate(population, 5, &mut rng);
assert_eq!(survivors.len(), 5, "Final survivors count should be 5");
let survivors_fitness = survivors.fitness;
for i in 0..3 {
let survivor_row = survivors_fitness.slice(s![i, ..]);
let expected_row = fitness.slice(s![i, ..]);
assert!(
survivor_row.eq(&expected_row),
"Survivor row {} does not match expected front1 row: got {:?}, expected {:?}",
i,
survivor_row,
expected_row
);
}
for i in 3..5 {
let survivor_row = survivors_fitness.slice(s![i, ..]);
let mut found = false;
for orig in fitness.outer_iter() {
if survivor_row.eq(&orig) {
found = true;
break;
}
}
assert!(
found,
"Survivor row {} from splitting front not found in front2. Row: {:?}",
i, survivor_row
);
}
}
}