use std::collections::HashSet;
use crate::{
genetic::{D12, Fronts},
helpers::{
extreme_points::get_ideal,
linalg::{cross_p_distances, lp_norm_arrayview},
},
operators::survival::moo::{FrontsAndRankingBasedSurvival, helpers::HyperPlaneNormalization},
random::RandomGenerator,
};
use ndarray::{Array1, Array2, ArrayView1, Axis, stack};
use ndarray_stats::QuantileExt;
struct AgeMoeaHyperPlaneNormalization;
impl AgeMoeaHyperPlaneNormalization {
pub fn new() -> Self {
Self
}
}
impl HyperPlaneNormalization for AgeMoeaHyperPlaneNormalization {
fn compute_extreme_points(&self, population_fitness: &Array2<f64>) -> Array2<f64> {
let extreme_indices: Vec<usize> = population_fitness
.axis_iter(Axis(1))
.map(|col| col.argmax().unwrap())
.collect();
let extreme_rows: Vec<_> = extreme_indices
.iter()
.map(|&i| population_fitness.row(i).to_owned())
.collect();
stack(
Axis(0),
&extreme_rows
.iter()
.map(|row| row.view())
.collect::<Vec<_>>(),
)
.unwrap()
}
}
#[derive(Debug, Clone)]
pub struct AgeMoeaSurvival;
impl AgeMoeaSurvival {
pub fn new() -> Self {
Self
}
}
impl FrontsAndRankingBasedSurvival for AgeMoeaSurvival {
fn set_front_survival_score<ConstrDim>(
&self,
fronts: &mut Fronts<ConstrDim>,
_rng: &mut impl RandomGenerator,
) where
ConstrDim: D12,
{
if let Some((first_front, other_fronts)) = fronts.split_first_mut() {
let z_min = get_ideal(&first_front.fitness);
let translated = &first_front.fitness - &z_min;
let normalizer = AgeMoeaHyperPlaneNormalization::new();
let intercepts = normalizer.compute_hyperplane_intercepts(&translated);
let normalized_first_front = translated / &intercepts;
let central_point = get_central_point_normalized(&normalized_first_front);
if central_point.iter().all(|&x| x == 0.0) {
first_front
.set_survival_score(Array1::from_elem(first_front.len(), std::f64::INFINITY));
for front in other_fronts.iter_mut() {
front.set_survival_score(Array1::from_elem(
first_front.len(),
std::f64::INFINITY,
))
}
} else {
let p = compute_exponent_p(¢ral_point);
let score_first_front =
assign_survival_scores_first_front(&normalized_first_front, p);
first_front.set_survival_score(score_first_front);
for front in other_fronts.iter_mut() {
let translated = &front.fitness - &z_min;
let normalized_front = translated / &intercepts;
let score = assign_survival_scores_higher_front(&normalized_front, p);
front.set_survival_score(score)
}
}
}
}
}
fn get_central_point_normalized(normalized_fitness: &Array2<f64>) -> Array1<f64> {
let num_objectives = normalized_fitness.shape()[1];
let beta = Array1::<f64>::ones(num_objectives);
let beta_norm = beta.dot(&beta).sqrt(); let beta_hat = beta.mapv(|x| x / beta_norm);
let dot_products = normalized_fitness.dot(&beta_hat);
let projections = dot_products.insert_axis(Axis(1)) * &beta_hat;
let diff = normalized_fitness - &projections;
let squared_norms = diff.map_axis(Axis(1), |row| row.dot(&row));
let min_index = squared_norms
.argmin()
.expect("There should be at least one solution in the front");
normalized_fitness.row(min_index).to_owned()
}
fn compute_exponent_p(central: &Array1<f64>) -> f64 {
let m = central.len() as f64;
for &value in central.iter() {
assert!(
value > 0.0,
"All components of the central point must be > 0"
);
}
let product: f64 = central.iter().product();
let ln_m = m.ln();
let ln_product = product.ln();
ln_m / (ln_m - ln_product)
}
fn proximity(normalized_individual_fitness: &ArrayView1<f64>, p: f64) -> f64 {
lp_norm_arrayview(normalized_individual_fitness, p)
}
fn assign_survival_scores_first_front(
normalized_front_fitness: &Array2<f64>,
p: f64,
) -> Array1<f64> {
let num_solutions = normalized_front_fitness.nrows();
let num_objectives = normalized_front_fitness.ncols();
let mut scores = vec![0.0; num_solutions];
let mut extreme_indices = HashSet::new();
for j in 0..num_objectives {
let col = normalized_front_fitness.column(j);
let idx = col
.argmax()
.expect("Each column must have at least one element");
extreme_indices.insert(idx);
}
for &idx in &extreme_indices {
scores[idx] = f64::INFINITY;
}
let mut remaining: Vec<usize> = (0..num_solutions)
.filter(|&i| !extreme_indices.contains(&i))
.collect();
let mut considered: Vec<usize> = extreme_indices.iter().copied().collect();
let mut proximities = vec![0.0; num_solutions];
for i in 0..num_solutions {
proximities[i] = proximity(&normalized_front_fitness.row(i), p);
}
let distance_matrix = cross_p_distances(normalized_front_fitness, normalized_front_fitness, p);
while !remaining.is_empty() {
let mut candidate_values: Vec<(usize, f64)> = Vec::new();
for &s in &remaining {
let mut dists: Vec<f64> = considered
.iter()
.map(|&t| distance_matrix[[s, t]])
.collect();
dists.sort_by(|a, b| a.partial_cmp(b).unwrap());
let diversity = if dists.len() >= 2 {
dists[0] + dists[1]
} else {
dists[0]
};
let value = if proximities[s] != 0.0 {
diversity / proximities[s]
} else {
0.0
};
candidate_values.push((s, value));
}
if let Some(&(s_star, max_value)) = candidate_values
.iter()
.max_by(|a, b| a.1.partial_cmp(&b.1).unwrap())
{
scores[s_star] = max_value;
considered.push(s_star);
remaining.retain(|&x| x != s_star);
} else {
break;
}
}
Array1::from(scores)
}
pub fn assign_survival_scores_higher_front(
normalized_front_fitness: &Array2<f64>,
p: f64,
) -> Array1<f64> {
let scores: Vec<f64> = normalized_front_fitness
.axis_iter(Axis(0))
.map(|row| 1.0 / proximity(&row, p))
.collect();
Array1::from(scores)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::genetic::PopulationMOO;
use crate::operators::survival::moo::helpers::HyperPlaneNormalization;
use crate::random::NoopRandomGenerator;
use ndarray::{Array1, Array2, array};
#[test]
fn test_solve_intercepts() {
let front = array![[1.0, 2.0], [3.0, 1.0]];
let z_min = crate::helpers::extreme_points::get_ideal(&front);
let translated = &front - &z_min;
let normalizer = AgeMoeaHyperPlaneNormalization::new();
let intercepts = normalizer.compute_hyperplane_intercepts(&translated);
let expected = array![2.0, 1.0];
assert_eq!(&intercepts, &expected);
}
#[test]
fn test_solve_intercepts_no_solution() {
let front = array![[1.0, 2.0], [1.0, 3.0]];
let z_min = get_ideal(&front);
let translated = front - z_min;
let normalizer = AgeMoeaHyperPlaneNormalization::new();
let intercepts = normalizer.compute_hyperplane_intercepts(&translated);
let expected = array![0.0, 1.0];
assert_eq!(&intercepts, &expected);
}
#[test]
fn test_get_central_point_normalized_2d() {
let normalized = array![[0.1, 0.9], [0.9, 0.1], [0.5, 0.5]];
let central = get_central_point_normalized(&normalized);
let expected: Array1<f64> = array![0.5, 0.5];
assert_eq!(¢ral, &expected);
}
#[test]
fn test_c_paper_example() {
let normalized = array![[1.0, 0.0], [0.0, 1.0], [0.5, 0.5]];
let central = get_central_point_normalized(&normalized);
let expected: Array1<f64> = array![0.5, 0.5];
assert_eq!(¢ral, &expected);
let p = 2.0;
let prox = proximity(¢ral.view(), p);
let expected_prox = (0.5_f64.powi(2) + 0.5_f64.powi(2)).sqrt();
assert!((prox - expected_prox).abs() < 1e-6);
}
#[test]
fn test_proximity_2d() {
let solution: Array1<f64> = array![0.5, 0.5];
let p = 2.0;
let prox = proximity(&solution.view(), p);
let expected = (0.5_f64.powi(2) + 0.5_f64.powi(2)).sqrt();
assert!((prox - expected).abs() < 1e-6);
}
#[test]
fn test_assign_survival_scores_p2() {
let front = array![[1.0, 0.0], [0.5, 0.5], [0.0, 1.0]];
let p = 2.0;
let scores = assign_survival_scores_first_front(&front, p);
assert!(scores[0].is_infinite(), "Index 0 should be extreme (∞)");
assert!(scores[2].is_infinite(), "Index 2 should be extreme (∞)");
let candidate_score = scores[1];
let expected_score = 1.0 / 0.70710678; assert!(
(candidate_score - expected_score).abs() < 1e-6,
"Score for index 1 should be ~1.41421356"
);
}
#[test]
fn test_assign_survival_scores_p1() {
let front = array![[1.0, 0.0], [0.5, 0.5], [0.0, 1.0]];
let p = 1.0;
let scores = assign_survival_scores_first_front(&front, p);
assert!(scores[0].is_infinite(), "Index 0 should be extreme (∞)");
assert!(scores[2].is_infinite(), "Index 2 should be extreme (∞)");
let candidate_score = scores[1];
assert!(
(candidate_score - 2.0).abs() < 1e-6,
"Score for index 1 should be ~2.0"
);
}
#[test]
fn test_assign_survival_scores_multiple() {
let front = array![[1.0, 0.0], [0.8, 0.2], [0.2, 0.8], [0.0, 1.0]];
let p = 2.0;
let scores = assign_survival_scores_first_front(&front, p);
assert!(scores[0].is_infinite(), "Index 0 should be extreme (∞)");
assert!(scores[3].is_infinite(), "Index 3 should be extreme (∞)");
assert!(
scores[1].is_finite() && scores[1] > 0.0,
"Index 1 should have a positive finite score"
);
assert!(
scores[2].is_finite() && scores[2] > 0.0,
"Index 2 should have a positive finite score"
);
}
#[test]
fn test_operate_age_moea_survival() {
let fitness: Array2<f64> =
array![[1.0, 0.0], [0.5, 0.5], [0.0, 1.0], [0.8, 0.2], [0.2, 0.8]];
let genes = fitness.clone();
let population = PopulationMOO::new_unconstrained(genes, fitness);
let num_survive = 4;
let mut operator = AgeMoeaSurvival;
let mut rng = NoopRandomGenerator::new();
let survivors = operator.operate(population, num_survive, &mut rng);
assert_eq!(
survivors.len(),
num_survive,
"Expected {} survivors, got {}",
num_survive,
survivors.len()
)
}
}