use derive_builder::Builder;
use ndarray::{Array2, ArrayBase, Axis, Dimension, OwnedRepr};
use thiserror::Error;
use crate::genetic::{D01, D12, Population};
pub trait ConstraintsFn
where
Self::Dim: D12,
<Self::Dim as Dimension>::Smaller: D01,
{
type Dim: D12;
fn call(&self, genes: &Array2<f64>) -> ArrayBase<OwnedRepr<f64>, Self::Dim>;
fn lower_bound(&self) -> Option<f64> {
None
}
fn upper_bound(&self) -> Option<f64> {
None
}
}
impl<G, Dim> ConstraintsFn for G
where
G: Fn(&Array2<f64>) -> ArrayBase<OwnedRepr<f64>, Dim>,
Dim: D12,
<Dim as Dimension>::Smaller: D01,
{
type Dim = Dim;
fn call(&self, genes: &Array2<f64>) -> ArrayBase<OwnedRepr<f64>, Dim> {
(self)(genes)
}
}
#[derive(Debug)]
pub struct NoConstraints;
impl ConstraintsFn for NoConstraints {
type Dim = ndarray::Ix2;
fn call(&self, genes: &Array2<f64>) -> ArrayBase<OwnedRepr<f64>, Self::Dim> {
let n = genes.nrows();
Array2::zeros((n, 0))
}
}
pub trait FitnessFn
where
<Self::Dim as Dimension>::Smaller: D01,
{
type Dim: D12;
fn call(&self, genes: &Array2<f64>) -> ArrayBase<OwnedRepr<f64>, Self::Dim>;
}
impl<F, Dim> FitnessFn for F
where
F: Fn(&Array2<f64>) -> ArrayBase<OwnedRepr<f64>, Dim>,
Dim: D12,
<Dim as Dimension>::Smaller: D01,
{
type Dim = Dim;
fn call(&self, genes: &Array2<f64>) -> ArrayBase<OwnedRepr<f64>, Dim> {
(self)(genes)
}
}
#[derive(Debug, Error)]
pub enum EvaluatorError {
#[error("No feasible individuals found in the population.")]
NoFeasibleIndividuals,
}
#[derive(Debug, Builder)]
#[builder(pattern = "owned")]
pub struct Evaluator<F, G>
where
F: FitnessFn,
G: ConstraintsFn,
{
fitness: F,
constraints: G,
#[builder(default = "true")]
keep_infeasible: bool,
}
impl<F, G> Evaluator<F, G>
where
F: FitnessFn,
G: ConstraintsFn,
{
pub fn evaluate(
&self,
genes: Array2<f64>,
) -> Result<Population<F::Dim, G::Dim>, EvaluatorError> {
let fitness = self.fitness.call(&genes);
let constraints = self.constraints.call(&genes);
let mut evaluated_population = Population::new(genes, fitness, constraints);
if !self.keep_infeasible {
let n = evaluated_population.genes.nrows();
let mut feasible_indices: Vec<usize> = (0..n).collect();
if evaluated_population.constraints.len() > 0 {
feasible_indices.retain(|&i| {
evaluated_population
.constraints
.index_axis(Axis(0), i)
.iter()
.all(|&val| val <= 0.0)
});
};
if feasible_indices.is_empty() {
return Err(EvaluatorError::NoFeasibleIndividuals);
}
evaluated_population = evaluated_population.selected(&feasible_indices);
}
Ok(evaluated_population)
}
}
#[cfg(test)]
mod tests {
use super::*;
use ndarray::{Array1, Array2, Axis, array, concatenate};
use crate::NoConstraints;
fn fitness_2d_single(genes: &Array2<f64>) -> Array2<f64> {
genes .map_axis(Axis(1), |ind| ind.iter().map(|&x| x * x).sum::<f64>())
.insert_axis(Axis(1)) }
fn fitness_1d(genes: &Array2<f64>) -> Array1<f64> {
genes.map_axis(Axis(1), |ind| ind.iter().map(|&x| x * x).sum::<f64>())
}
fn fitness_2d_two_obj(genes: &Array2<f64>) -> Array2<f64> {
let f0 = genes
.map_axis(Axis(1), |ind| ind.iter().map(|&x| x * x).sum::<f64>())
.insert_axis(Axis(1));
let f1 = genes
.map_axis(Axis(1), |ind| ind.iter().map(|&x| x.abs()).sum::<f64>())
.insert_axis(Axis(1));
concatenate![Axis(1), f0, f1] }
fn constraints_multi(genes: &Array2<f64>) -> Array2<f64> {
let c0 = genes
.sum_axis(Axis(1))
.mapv(|s| s - 10.0)
.insert_axis(Axis(1));
let non_neg = genes.mapv(|x| -x);
concatenate![Axis(1), c0, non_neg] }
fn constraints_single(genes: &Array2<f64>) -> Array1<f64> {
genes.sum_axis(Axis(1)).mapv(|s| s - 10.0)
}
#[test]
fn two_d_fitness_without_constraints_keeps_every_row() {
let eval = EvaluatorBuilder::default()
.fitness(fitness_2d_single)
.constraints(NoConstraints)
.keep_infeasible(true)
.build()
.expect("Builder failed");
let genes = array![[1.0, 2.0], [3.0, 4.0], [0.0, 0.0]];
let fit = eval.evaluate(genes).unwrap().fitness;
let expected = array![[5.0], [25.0], [0.0]];
assert_eq!(fit, expected);
}
#[test]
fn multi_constraints_are_computed_correctly() {
let eval = EvaluatorBuilder::default()
.fitness(fitness_2d_single)
.constraints(constraints_multi)
.build()
.expect("Builder failed");
let genes = array![
[1.0, 2.0], [3.0, 4.0],
[5.0, 6.0], ];
let c = eval.evaluate(genes).unwrap().constraints;
let expected = array![[-7.0, -1.0, -2.0], [-3.0, -3.0, -4.0], [1.0, -5.0, -6.0],];
assert_eq!(c, expected);
}
#[test]
fn keep_infeasible_true_retains_every_row() {
let eval = EvaluatorBuilder::default()
.fitness(fitness_2d_single)
.constraints(constraints_multi)
.keep_infeasible(true)
.build()
.expect("Builder failed");
let genes = array![[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]];
let pop = eval.evaluate(genes).unwrap();
assert_eq!(
pop.genes.nrows(),
3,
"Nothing filtered when keep_infeasible = true"
);
}
#[test]
fn one_d_fitness_multi_constraints_filters_by_constraint_only() {
let eval = EvaluatorBuilder::default()
.fitness(fitness_1d)
.constraints(constraints_multi)
.keep_infeasible(false)
.build()
.expect("Builder failed");
let genes = array![
[1.0, 2.0], [6.0, 5.0], ];
let pop = eval.evaluate(genes).unwrap();
assert_eq!(pop.genes.nrows(), 1, "Second row violates Σx - 10 ≤ 0");
assert_eq!(pop.fitness, array![5.0]);
}
#[test]
fn one_d_fitness_single_constraint_filters_correctly() {
let eval = EvaluatorBuilder::default()
.fitness(fitness_1d)
.constraints(constraints_single)
.keep_infeasible(false)
.build()
.expect("Builder failed");
let genes = array![
[2.0, 3.0], [5.0, 6.0], ];
let pop = eval.evaluate(genes).unwrap();
assert_eq!(pop.genes.nrows(), 1);
assert_eq!(pop.fitness, array![13.0]); assert_eq!(
pop.constraints,
array![-5.0], "Only constraint column for the surviving row"
);
}
#[test]
fn all_rows_removed_returns_no_feasible_error() {
let eval = EvaluatorBuilder::default()
.fitness(fitness_1d)
.constraints(constraints_multi)
.keep_infeasible(false)
.build()
.expect("Builder failed");
let genes = array![[6.0, 6.0], [5.5, 6.0], [6.0, 100.0]];
let err = eval.evaluate(genes).unwrap_err();
assert!(
matches!(err, EvaluatorError::NoFeasibleIndividuals),
"When no rows survive, Evaluator must return the dedicated error"
);
}
#[test]
fn two_objective_fitness_is_computed_for_each_row() {
let eval = EvaluatorBuilder::default()
.fitness(fitness_2d_two_obj)
.constraints(NoConstraints)
.build()
.expect("Builder failed");
let genes = array![[1.0, 2.0], [3.0, 4.0]];
let fit = eval.evaluate(genes).unwrap().fitness;
let expected = array![[5.0, 3.0], [25.0, 7.0]];
assert_eq!(fit, expected);
}
}