use std::cmp::Ordering;
use ndarray::{Array1, Array2, Axis};
use crate::{
genetic::{D12, PopulationMOO},
helpers::linalg::cross_euclidean_distances_as_array,
non_dominated_sorting::fast_non_dominated_sorting,
operators::survival::SurvivalOperator,
random::RandomGenerator,
};
#[derive(Debug, Clone, Default)]
pub struct Spea2KnnSurvival;
impl Spea2KnnSurvival {
pub fn new() -> Self {
Self {}
}
}
impl SurvivalOperator for Spea2KnnSurvival {
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 k = population.len().isqrt();
let distance_matrix =
cross_euclidean_distances_as_array(&population.fitness, &population.fitness);
let density = compute_density(&distance_matrix, k);
let domination_indices = compute_domination_indices(&population.fitness);
let raw_fitness: Array1<f64> = &domination_indices + &density;
let mut s: Vec<usize> = raw_fitness
.iter()
.enumerate()
.filter_map(|(i, &f)| if f < 1.0 { Some(i) } else { None })
.collect();
match s.len().cmp(&num_survive) {
Ordering::Equal => {
}
Ordering::Less => {
let needed = num_survive - s.len();
let dominated_indices = select_dominated(&raw_fitness, needed);
s.extend(dominated_indices);
}
Ordering::Greater => {
s = select_by_nearest_neighbor(&distance_matrix, num_survive);
}
}
let mut survivors = population.selected(&s);
let selected_scores: Array1<f64> = raw_fitness.select(Axis(0), &s);
_ = survivors.set_survival_score(selected_scores);
survivors
}
}
pub fn compute_density(distance_matrix: &Array2<f64>, k: usize) -> Array1<f64> {
let n = distance_matrix.nrows();
let mut densities = Array1::<f64>::zeros(n);
for i in 0..n {
let mut dists: Vec<f64> = distance_matrix.row(i).iter().cloned().collect();
dists.sort_by(|a, b| a.partial_cmp(b).unwrap());
let sigma_k = dists[k];
densities[i] = 1.0 / (sigma_k + 2.0);
}
densities
}
pub fn compute_domination_indices(population_fitness: &Array2<f64>) -> Array1<f64> {
let n = population_fitness.nrows();
let ranks = fast_non_dominated_sorting(population_fitness, n);
let mut domination_indices = Array1::<f64>::zeros(n);
for (rank, group) in ranks.into_iter().enumerate() {
for &i in &group {
domination_indices[i] = rank as f64;
}
}
domination_indices
}
pub fn select_dominated(raw_fitness: &Array1<f64>, r: usize) -> Vec<usize> {
let mut dominated: Vec<(usize, f64)> = raw_fitness
.iter()
.enumerate()
.filter_map(|(i, &f)| if f >= 1.0 { Some((i, f)) } else { None })
.collect();
dominated.sort_unstable_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(Ordering::Equal));
dominated.into_iter().take(r).map(|(idx, _)| idx).collect()
}
pub fn select_by_nearest_neighbor(distance_matrix: &Array2<f64>, r: usize) -> Vec<usize> {
let n = distance_matrix.nrows();
let mut nearest: Vec<(usize, f64)> = Vec::with_capacity(n);
for i in 0..n {
let min_dist = distance_matrix
.row(i)
.iter()
.enumerate()
.filter_map(|(j, &d)| if j != i { Some(d) } else { None })
.fold(f64::INFINITY, f64::min);
nearest.push((i, min_dist));
}
nearest.sort_unstable_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(Ordering::Equal));
nearest.into_iter().take(r).map(|(idx, _)| idx).collect()
}
#[cfg(test)]
mod tests {
use super::*;
use ndarray::array;
use crate::random::NoopRandomGenerator;
#[test]
fn test_compute_density() {
let dm = array![
[0.0, 5.0, 2.0, 8.0],
[5.0, 0.0, 9.0, 1.0],
[2.0, 9.0, 0.0, 4.0],
[8.0, 1.0, 4.0, 0.0],
];
let densities = compute_density(&dm, 2);
let expected = [1.0 / 7.0, 1.0 / 7.0, 1.0 / 6.0, 1.0 / 6.0];
for i in 0..4 {
assert!(
(densities[i] - expected[i]).abs() < 1e-12,
"density[{}] = {}, but expected {}",
i,
densities[i],
expected[i]
);
}
}
#[test]
fn test_select_dominated_mixed() {
let raw: Array1<f64> = array![0.5, 1.0, 2.0, 3.0, 1.5];
let picked = select_dominated(&raw, 3);
assert_eq!(picked, vec![1, 4, 2]);
}
#[test]
fn test_select_dominated_all() {
let raw: Array1<f64> = array![1.1, 2.2, 3.3];
let picked = select_dominated(&raw, 2);
assert_eq!(picked, vec![0, 1]);
}
#[test]
fn test_chain_dominance() {
let fitness = array![[1.0, 1.0], [2.0, 2.0], [3.0, 3.0],];
let indices = compute_domination_indices(&fitness);
assert_eq!(indices.len(), 3);
assert_eq!(indices, array![0.0, 1.0, 2.0]);
}
#[test]
fn test_no_dominance_all_zero() {
let fitness = array![[1.0, 4.0], [2.0, 3.0], [3.0, 2.0], [4.0, 1.0],];
let indices = compute_domination_indices(&fitness);
assert_eq!(indices.len(), 4);
assert_eq!(indices, array![0.0, 0.0, 0.0, 0.0]);
}
fn make_population(fitness: Array2<f64>) -> PopulationMOO<ndarray::Ix2> {
let n = fitness.nrows();
let genes = Array2::<f64>::zeros((n, 1));
PopulationMOO::new_unconstrained(genes, fitness)
}
#[test]
fn test_fills_when_underflow() {
let fit = array![[0.5], [1.2], [1.5]];
let pop = make_population(fit.clone());
let expected_raw = [0.4016064, 1.4784689, 2.4784689];
let mut rng = NoopRandomGenerator::new();
let survivors = Spea2KnnSurvival::new().operate(pop, 2, &mut rng);
let scores: Vec<f64> = survivors
.survival_score
.as_ref()
.expect("survival_score must be set")
.to_vec();
assert_eq!(scores.len(), 2);
assert!((scores[0] - expected_raw[0]).abs() < 1e-6,);
assert!((scores[1] - expected_raw[1]).abs() < 1e-6);
}
#[test]
fn test_overflow_keeps_first_two_when_all_tie() {
let fit = array![[0.0, 3.0], [1.0, 2.0], [2.0, 1.0], [3.0, 0.0],];
let pop = make_population(fit.clone());
let mut rng = NoopRandomGenerator::new();
let survivors = Spea2KnnSurvival::new().operate(pop, 2, &mut rng);
assert_eq!(survivors.len(), 2);
assert_eq!(survivors.get(0).fitness.to_vec(), vec![0.0, 3.0]);
assert_eq!(survivors.get(1).fitness.to_vec(), vec![1.0, 2.0]);
let expected = [0.1, 0.25];
let scores = survivors
.survival_score
.as_ref()
.expect("survival_score must be set");
assert_eq!(scores.len(), 2);
assert!((scores[0] - expected[0]).abs() < 1e-6);
assert!((scores[1] - expected[1]).abs() < 1e-6);
}
#[test]
fn all_survive_when_capacity_equals_population() {
let fit = array![
[0.0, 3.0], [1.0, 2.0], [2.0, 1.0], [3.0, 0.0], ];
let pop = make_population(fit.clone());
let mut rng = NoopRandomGenerator::new();
let survivors = Spea2KnnSurvival::new().operate(pop, 4, &mut rng);
assert_eq!(survivors.len(), 4);
}
}