use ndarray::{Array1, Array2, ArrayView1, Axis};
use crate::genetic::{D12, PopulationMOO};
use crate::helpers::extreme_points::normalize_fitness;
use crate::non_dominated_sorting::dominates_weak;
use crate::operators::SurvivalOperator;
use crate::random::RandomGenerator;
pub trait Indicator {
fn kappa(&self) -> f64;
fn indicator(&self, f1: ArrayView1<'_, f64>, f2: ArrayView1<'_, f64>) -> f64;
fn indicator_matrix(&self, fitness: &Array2<f64>) -> Array2<f64> {
let n = fitness.nrows();
let mut out = Array2::<f64>::zeros((n, n));
for i in 0..n {
let ai = fitness.row(i);
for j in 0..n {
out[[i, j]] = if i == j {
0.0
} else {
let bj = fitness.row(j);
self.indicator(ai, bj)
};
}
}
out
}
fn exponential_indicator_matrix(&self, fitness: &Array2<f64>) -> Array2<f64> {
let m = self.indicator_matrix(fitness);
let c = m
.iter()
.max_by(|a, b| a.abs().partial_cmp(&b.abs()).unwrap())
.unwrap();
let kappa = self.kappa() * c;
let mut exp_matrix = m.map(|a| -((-a / kappa).clamp(-50.0, 50.0).exp()));
let n = exp_matrix.nrows();
for i in 0..n {
exp_matrix[[i, i]] = 0.0;
}
exp_matrix
}
}
#[derive(Debug, Default)]
pub struct HyperVolumeIndicator {
reference: Array1<f64>,
kappa: f64,
}
impl HyperVolumeIndicator {
fn hypervolume_singleton(&self, point: ArrayView1<'_, f64>) -> f64 {
self.reference
.iter()
.zip(point.iter())
.map(|(r, x)| (r - x).max(0.0))
.product()
}
}
impl Indicator for HyperVolumeIndicator {
fn kappa(&self) -> f64 {
self.kappa
}
fn indicator(&self, f1: ArrayView1<'_, f64>, f2: ArrayView1<'_, f64>) -> f64 {
let hv_f1 = self.hypervolume_singleton(f1);
let hv_f2 = self.hypervolume_singleton(f2);
if dominates_weak(&f1, &f2) {
return hv_f2 - hv_f1;
}
let inter: f64 = self
.reference
.iter()
.zip(f1.iter().zip(f2.iter()))
.map(|(r, (a, b))| (r - a.max(*b)).max(0.0))
.product();
hv_f2 - inter
}
}
#[derive(Debug, Clone, Default)]
pub struct IbeaSurvivalOperator<I: Indicator> {
indicator: I,
}
impl<I: Indicator> SurvivalOperator for IbeaSurvivalOperator<I> {
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 to_drop = population.len() - num_survive;
let mut indices_to_drop: Vec<usize> = Vec::with_capacity(to_drop);
let normalized_fitness = normalize_fitness(&population.fitness);
let m = self
.indicator
.exponential_indicator_matrix(&normalized_fitness);
let mut f = m.sum_axis(Axis(0));
while to_drop > 0 {
let k = f
.iter()
.enumerate()
.min_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap()) .unwrap()
.0;
indices_to_drop.push(k);
f -= &m.row(k);
f[k] = f64::INFINITY;
to_drop -= 1;
}
let n = population.len();
let mut dropped = vec![false; n];
for i in indices_to_drop {
dropped[i] = true;
}
let keep: Vec<usize> = (0..n).filter(|&i| !dropped[i]).collect();
let survival_score = f.select(Axis(0), &keep);
let mut survivors = population.selected(&keep);
survivors.set_survival_score(survival_score);
survivors
}
}
pub type IbeaHyperVolumeSurvivalOperator = IbeaSurvivalOperator<HyperVolumeIndicator>;
impl IbeaHyperVolumeSurvivalOperator {
pub fn new(reference: Array1<f64>, kappa: f64) -> Self {
Self {
indicator: HyperVolumeIndicator {
reference: reference,
kappa: kappa,
},
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::genetic::PopulationMOO;
use crate::random::NoopRandomGenerator;
use ndarray::{Array1, Array2, array};
fn approx_eq(a: f64, b: f64, eps: f64) -> bool {
(a - b).abs() <= eps
}
#[test]
fn indicator_hv_basics() {
let r: Array1<f64> = array![3.0, 3.0];
let ind = HyperVolumeIndicator {
reference: r,
kappa: 1.0,
};
let a = array![1.0, 1.0];
let b = array![2.0, 2.0];
let i_ab = ind.indicator(a.view(), b.view());
let i_ba = ind.indicator(b.view(), a.view());
assert!(approx_eq(i_ab, -3.0, 1e-12));
assert!(approx_eq(i_ba, 3.0, 1e-12));
}
#[test]
fn operate_no_drop_returns_same_population() {
let r: Array1<f64> = array![3.0, 3.0];
let mut op = IbeaHyperVolumeSurvivalOperator::new(r, 1.0);
let genes: Array2<f64> = array![[0.0, 0.0], [1.0, 1.0], [2.0, 2.0]];
let fitness: Array2<f64> = array![[1.0, 2.0], [2.0, 1.0], [2.0, 2.0]];
let pop = PopulationMOO::new_unconstrained(genes.clone(), fitness.clone());
let mut rng = NoopRandomGenerator::new();
let out = op.operate(pop, 3, &mut rng);
assert_eq!(out.len(), 3);
assert_eq!(out.genes, genes);
assert_eq!(out.fitness, fitness);
let score = out.survival_score.as_ref().expect("survival score set");
assert_eq!(score.len(), 3);
assert!(score.iter().all(|v| v.is_finite()));
}
#[test]
fn operate_drops_one_keeps_two_expected_indices() {
let r: Array1<f64> = array![3.0, 3.0];
let mut op = IbeaHyperVolumeSurvivalOperator::new(r, 1.0);
let genes: Array2<f64> = array![[10.0, 10.0], [20.0, 20.0], [25.0, 25.0]];
let fitness: Array2<f64> = array![[1.0, 1.0], [2.0, 2.0], [2.5, 2.5]];
let pop = PopulationMOO::new_unconstrained(genes.clone(), fitness.clone());
let mut rng = NoopRandomGenerator::new();
let out = op.operate(pop, 2, &mut rng);
assert_eq!(out.len(), 2);
assert_eq!(out.genes, array![[10.0, 10.0], [20.0, 20.0]]);
assert_eq!(out.fitness, array![[1.0, 1.0], [2.0, 2.0]]);
let score = out.survival_score.as_ref().expect("survival score set");
assert_eq!(score.len(), 2);
assert!(score.iter().all(|v| v.is_finite()));
}
#[test]
fn pairwise_matrix_shape_and_diagonal() {
let r: Array1<f64> = array![3.0, 3.0, 3.0];
let ind = HyperVolumeIndicator {
reference: r,
kappa: 0.5,
};
let fitness: Array2<f64> = array![[1.0, 1.0, 1.0], [2.0, 2.0, 2.0], [2.5, 1.5, 2.0]];
let m = ind.indicator_matrix(&fitness);
assert_eq!(m.nrows(), 3);
assert_eq!(m.ncols(), 3);
for i in 0..3 {
assert!(approx_eq(m[[i, i]], 0.0, 1e-12));
for j in 0..3 {
if i != j {
assert!(m[[i, j]].is_finite());
}
}
}
}
}