use std::cmp::Ordering;
use ndarray::{Array1, Array2, ArrayView1, Axis};
use crate::{
genetic::{D12, Fronts},
helpers::extreme_points::{get_ideal, get_nadir},
operators::survival::moo::{FrontsAndRankingBasedSurvival, SurvivalScoringComparison},
random::RandomGenerator,
};
#[derive(Debug, Clone, Default)]
pub struct Rnsga2ReferencePointsSurvival {
reference_points: Array2<f64>,
epsilon: f64,
}
impl Rnsga2ReferencePointsSurvival {
pub fn new(reference_points: Array2<f64>, epsilon: f64) -> Self {
Self {
reference_points,
epsilon,
}
}
}
impl FrontsAndRankingBasedSurvival for Rnsga2ReferencePointsSurvival {
fn scoring_comparison(&self) -> SurvivalScoringComparison {
SurvivalScoringComparison::Minimize
}
fn set_front_survival_score<ConstrDim>(
&self,
fronts: &mut Fronts<ConstrDim>,
rng: &mut impl RandomGenerator,
) where
ConstrDim: D12,
{
let len_fronts = fronts.len();
let num_objectives = fronts[0].fitness.ncols();
let weights = Array1::from_elem(num_objectives, 1.0 / (num_objectives as f64));
for front in fronts.iter_mut().take(len_fronts.saturating_sub(1)) {
let nadir = get_nadir(&front.fitness);
let ideal = get_ideal(&front.fitness);
let survival_score = assign_crowding_distance_to_inner_front(
&front.fitness,
&self.reference_points,
&weights,
&nadir,
&ideal,
);
front.set_survival_score(survival_score);
}
if let Some(last_front) = fronts.last_mut() {
let nadir = get_nadir(&last_front.fitness);
let ideal = get_ideal(&last_front.fitness);
let survival_score = assign_crowding_distance_splitting_front(
&last_front.fitness,
&self.reference_points,
&weights,
self.epsilon,
&nadir,
&ideal,
rng,
);
last_front.set_survival_score(survival_score);
}
}
}
fn weighted_normalized_euclidean_distance(
f1: &ArrayView1<f64>,
f2: &ArrayView1<f64>,
weights: &Array1<f64>,
ideal: &Array1<f64>,
nadir: &Array1<f64>,
) -> f64 {
let diff = f1 - f2;
let ranges = nadir - ideal;
let normalized_diff = diff / &ranges;
let weighted_sum_sq: f64 = normalized_diff.mapv(|x| x * x).dot(weights);
weighted_sum_sq.sqrt()
}
fn distance_to_reference(
front_fitness: &Array2<f64>,
reference_points: &Array2<f64>,
weights: &Array1<f64>,
ideal: &Array1<f64>,
nadir: &Array1<f64>,
) -> Array1<f64> {
let num_front_individuals = front_fitness.nrows();
let mut crowding = vec![f64::INFINITY; num_front_individuals];
for rp in reference_points.axis_iter(Axis(0)) {
let mut solution_distances: Vec<(usize, f64)> = front_fitness
.axis_iter(Axis(0))
.enumerate()
.map(|(i, sol)| {
let distance =
weighted_normalized_euclidean_distance(&sol, &rp, weights, ideal, nadir);
(i, distance)
})
.collect();
solution_distances.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(Ordering::Equal));
for (rank, (i, _)) in solution_distances.into_iter().enumerate() {
let current_rank = (rank + 1) as f64;
if current_rank < crowding[i] {
crowding[i] = current_rank;
}
}
}
Array1::from_vec(crowding)
}
fn assign_crowding_distance_to_inner_front(
front_fitness: &Array2<f64>,
reference_points: &Array2<f64>,
weights: &Array1<f64>,
nadir: &Array1<f64>,
ideal: &Array1<f64>,
) -> Array1<f64> {
distance_to_reference(&front_fitness, &reference_points, &weights, &ideal, &nadir)
}
fn assign_crowding_distance_splitting_front(
front_fitness: &Array2<f64>,
reference_points: &Array2<f64>,
weights: &Array1<f64>,
epsilon: f64,
nadir: &Array1<f64>,
ideal: &Array1<f64>,
rng: &mut impl RandomGenerator,
) -> Array1<f64> {
let num_front_individuals = front_fitness.nrows();
let mut crowding = assign_crowding_distance_to_inner_front(
&front_fitness,
&reference_points,
&weights,
nadir,
ideal,
);
let mut visited = vec![false; num_front_individuals];
let mut groups: Vec<Vec<usize>> = Vec::new();
for i in 0..num_front_individuals {
if visited[i] {
continue;
}
let mut group = vec![i];
visited[i] = true;
let sol_i = front_fitness.row(i);
for j in (i + 1)..num_front_individuals {
if !visited[j] {
let sol_j = front_fitness.row(j);
let sum_diff = weighted_normalized_euclidean_distance(
&sol_i, &sol_j, &weights, &ideal, &nadir,
);
if sum_diff <= epsilon {
group.push(j);
visited[j] = true;
}
}
}
groups.push(group);
}
for group in groups {
if group.len() > 1 {
let mut group_copy = group.clone();
rng.shuffle_vec_usize(&mut group_copy);
for &idx in group_copy.iter().skip(1) {
crowding[idx] = f64::INFINITY;
}
}
}
crowding
}
#[cfg(test)]
mod tests {
use core::f64;
use super::*;
use ndarray::array;
#[test]
fn test_distance_zero() {
let f1 = array![1.0, 2.0];
let f2 = array![1.0, 2.0];
let weights = array![1.0, 1.0];
let ideal = array![0.0, 0.0];
let nadir = array![1.0, 1.0];
let distance = weighted_normalized_euclidean_distance(
&f1.view(),
&f2.view(),
&weights,
&ideal,
&nadir,
);
assert_eq!(distance, 0.0);
}
#[test]
fn test_distance_simple() {
let f1 = array![3.0, 4.0];
let f2 = array![1.0, 2.0];
let weights = array![1.0, 1.0];
let ideal = array![1.0, 2.0];
let nadir = array![5.0, 6.0];
let distance = weighted_normalized_euclidean_distance(
&f1.view(),
&f2.view(),
&weights,
&ideal,
&nadir,
);
let expected = (0.25_f64 + 0.25).sqrt();
assert!((distance - expected).abs() < 1e-6);
}
#[test]
fn test_distance_with_zero_range() {
let f1 = array![3.0, 4.0];
let f2 = array![1.0, 2.0];
let weights = array![1.0, 1.0];
let ideal = array![1.0, 2.0];
let nadir = array![1.0, 6.0];
let distance: f64 = weighted_normalized_euclidean_distance(
&f1.view(),
&f2.view(),
&weights,
&ideal,
&nadir,
);
let expected: f64 = f64::INFINITY;
assert!(expected == distance);
}
#[test]
fn test_single_solution_single_reference() {
let front_fitness = array![[0.5, 0.5]];
let reference_points = array![[0.5, 0.5]];
let weights = array![1.0, 1.0];
let ideal = array![0.0, 0.0];
let nadir = array![1.0, 1.0];
let result =
distance_to_reference(&front_fitness, &reference_points, &weights, &ideal, &nadir);
let expected = array![1.0];
assert_eq!(result, expected);
}
#[test]
fn test_two_solutions_single_reference() {
let front_fitness = array![[0.5, 0.5], [0.2, 0.8]];
let reference_points = array![[0.5, 0.5]];
let weights = array![1.0, 1.0];
let ideal = array![0.0, 0.0];
let nadir = array![1.0, 1.0];
let result =
distance_to_reference(&front_fitness, &reference_points, &weights, &ideal, &nadir);
let expected = array![1.0, 2.0];
assert_eq!(result, expected);
}
#[test]
fn test_two_solutions_two_references() {
let front_fitness = array![[0.2, 0.8], [0.8, 0.2]];
let reference_points = array![[0.0, 1.0], [1.0, 0.0]];
let weights = array![1.0, 1.0];
let ideal = array![0.0, 0.0];
let nadir = array![1.0, 1.0];
let result =
distance_to_reference(&front_fitness, &reference_points, &weights, &ideal, &nadir);
let expected = array![1.0, 1.0];
assert_eq!(result, expected);
}
#[test]
fn test_multiple_solutions_multiple_references() {
let front_fitness = array![[0.1, 0.9], [0.4, 0.6], [0.9, 0.1]];
let reference_points = array![[0.0, 1.0], [1.0, 0.0]];
let weights = array![1.0, 1.0];
let ideal = array![0.0, 0.0];
let nadir = array![1.0, 1.0];
let result =
distance_to_reference(&front_fitness, &reference_points, &weights, &ideal, &nadir);
let expected = array![1.0, 2.0, 1.0];
assert_eq!(result, expected);
}
}