use crate::fitness::traits::FitnessValue;
use crate::genome::traits::EvolutionaryGenome;
use crate::population::population::Population;
#[derive(Clone, Debug)]
pub struct EvolutionState<'a, G, F = f64>
where
G: EvolutionaryGenome,
F: FitnessValue,
{
pub generation: usize,
pub evaluations: usize,
pub best_fitness: f64,
pub population: &'a Population<G, F>,
pub fitness_history: &'a [f64],
}
pub trait TerminationCriterion<G: EvolutionaryGenome, F: FitnessValue = f64>: Send + Sync {
fn should_terminate(&self, state: &EvolutionState<G, F>) -> bool;
fn reason(&self) -> &'static str;
}
#[derive(Clone, Debug)]
pub struct MaxGenerations(pub usize);
impl MaxGenerations {
pub fn new(max: usize) -> Self {
Self(max)
}
}
impl<G: EvolutionaryGenome, F: FitnessValue> TerminationCriterion<G, F> for MaxGenerations {
fn should_terminate(&self, state: &EvolutionState<G, F>) -> bool {
state.generation >= self.0
}
fn reason(&self) -> &'static str {
"Maximum generations reached"
}
}
#[derive(Clone, Debug)]
pub struct MaxEvaluations(pub usize);
impl MaxEvaluations {
pub fn new(max: usize) -> Self {
Self(max)
}
}
impl<G: EvolutionaryGenome, F: FitnessValue> TerminationCriterion<G, F> for MaxEvaluations {
fn should_terminate(&self, state: &EvolutionState<G, F>) -> bool {
state.evaluations >= self.0
}
fn reason(&self) -> &'static str {
"Maximum evaluations reached"
}
}
#[derive(Clone, Debug)]
pub struct FitnessStagnation {
pub window: usize,
pub epsilon: f64,
}
impl FitnessStagnation {
pub fn new(window: usize, epsilon: f64) -> Self {
Self { window, epsilon }
}
}
impl<G: EvolutionaryGenome, F: FitnessValue> TerminationCriterion<G, F> for FitnessStagnation {
fn should_terminate(&self, state: &EvolutionState<G, F>) -> bool {
if state.fitness_history.len() < self.window {
return false;
}
let start_idx = state.fitness_history.len() - self.window;
let window = &state.fitness_history[start_idx..];
if window.is_empty() {
return false;
}
let first = window[0];
let best_in_window = window.iter().copied().fold(f64::NEG_INFINITY, f64::max);
let improvement = best_in_window - first;
improvement < self.epsilon
}
fn reason(&self) -> &'static str {
"Fitness stagnation detected"
}
}
#[derive(Clone, Debug)]
pub struct TargetFitness {
pub target: f64,
pub tolerance: f64,
}
impl TargetFitness {
pub fn new(target: f64) -> Self {
Self {
target,
tolerance: 0.0,
}
}
pub fn with_tolerance(target: f64, tolerance: f64) -> Self {
Self { target, tolerance }
}
}
impl<G: EvolutionaryGenome, F: FitnessValue> TerminationCriterion<G, F> for TargetFitness {
fn should_terminate(&self, state: &EvolutionState<G, F>) -> bool {
state.best_fitness >= self.target - self.tolerance
}
fn reason(&self) -> &'static str {
"Target fitness reached"
}
}
#[derive(Clone, Debug)]
pub struct DiversityThreshold {
pub min_diversity: f64,
}
impl DiversityThreshold {
pub fn new(min_diversity: f64) -> Self {
Self { min_diversity }
}
}
impl<G: EvolutionaryGenome, F: FitnessValue> TerminationCriterion<G, F> for DiversityThreshold {
fn should_terminate(&self, state: &EvolutionState<G, F>) -> bool {
let diversity = state.population.diversity();
diversity < self.min_diversity
}
fn reason(&self) -> &'static str {
"Diversity threshold reached"
}
}
pub struct AnyOf<G: EvolutionaryGenome, F: FitnessValue = f64> {
criteria: Vec<Box<dyn TerminationCriterion<G, F>>>,
}
impl<G: EvolutionaryGenome, F: FitnessValue> AnyOf<G, F> {
pub fn new(criteria: Vec<Box<dyn TerminationCriterion<G, F>>>) -> Self {
Self { criteria }
}
}
impl<G: EvolutionaryGenome, F: FitnessValue> TerminationCriterion<G, F> for AnyOf<G, F> {
fn should_terminate(&self, state: &EvolutionState<G, F>) -> bool {
self.criteria.iter().any(|c| c.should_terminate(state))
}
fn reason(&self) -> &'static str {
"One of multiple criteria met"
}
}
pub struct AllOf<G: EvolutionaryGenome, F: FitnessValue = f64> {
criteria: Vec<Box<dyn TerminationCriterion<G, F>>>,
}
impl<G: EvolutionaryGenome, F: FitnessValue> AllOf<G, F> {
pub fn new(criteria: Vec<Box<dyn TerminationCriterion<G, F>>>) -> Self {
Self { criteria }
}
}
impl<G: EvolutionaryGenome, F: FitnessValue> TerminationCriterion<G, F> for AllOf<G, F> {
fn should_terminate(&self, state: &EvolutionState<G, F>) -> bool {
!self.criteria.is_empty() && self.criteria.iter().all(|c| c.should_terminate(state))
}
fn reason(&self) -> &'static str {
"All criteria met"
}
}
pub mod prelude {
pub use super::{
AllOf, AnyOf, DiversityThreshold, EvolutionState, FitnessStagnation, MaxEvaluations,
MaxGenerations, TargetFitness, TerminationCriterion,
};
}
#[cfg(test)]
mod tests {
use super::*;
use crate::genome::real_vector::RealVector;
use crate::population::individual::Individual;
use crate::population::population::Population;
fn create_test_state<'a>(
generation: usize,
evaluations: usize,
best_fitness: f64,
population: &'a Population<RealVector>,
fitness_history: &'a [f64],
) -> EvolutionState<'a, RealVector> {
EvolutionState {
generation,
evaluations,
best_fitness,
population,
fitness_history,
}
}
#[test]
fn test_max_generations() {
let individuals = vec![Individual::with_fitness(RealVector::new(vec![1.0]), 10.0)];
let pop = Population::from_individuals(individuals);
let history = vec![];
let criterion = MaxGenerations::new(100);
let state = create_test_state(50, 0, 10.0, &pop, &history);
assert!(!criterion.should_terminate(&state));
let state = create_test_state(100, 0, 10.0, &pop, &history);
assert!(criterion.should_terminate(&state));
let state = create_test_state(150, 0, 10.0, &pop, &history);
assert!(criterion.should_terminate(&state));
}
#[test]
fn test_max_evaluations() {
let individuals = vec![Individual::with_fitness(RealVector::new(vec![1.0]), 10.0)];
let pop = Population::from_individuals(individuals);
let history = vec![];
let criterion = MaxEvaluations::new(1000);
let state = create_test_state(0, 500, 10.0, &pop, &history);
assert!(!criterion.should_terminate(&state));
let state = create_test_state(0, 1000, 10.0, &pop, &history);
assert!(criterion.should_terminate(&state));
}
#[test]
fn test_fitness_stagnation() {
let individuals = vec![Individual::with_fitness(RealVector::new(vec![1.0]), 10.0)];
let pop = Population::from_individuals(individuals);
let criterion = FitnessStagnation::new(5, 0.01);
let history = vec![1.0, 2.0, 3.0];
let state = create_test_state(0, 0, 3.0, &pop, &history);
assert!(!criterion.should_terminate(&state));
let history = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let state = create_test_state(0, 0, 5.0, &pop, &history);
assert!(!criterion.should_terminate(&state));
let history = vec![5.0, 5.0, 5.0, 5.0, 5.0];
let state = create_test_state(0, 0, 5.0, &pop, &history);
assert!(criterion.should_terminate(&state));
}
#[test]
fn test_fitness_stagnation_nonmonotonic_window() {
let individuals = vec![Individual::with_fitness(RealVector::new(vec![1.0]), 10.0)];
let pop = Population::from_individuals(individuals);
let criterion = FitnessStagnation::new(3, 0.01);
let history = vec![10.0, 90.0, 10.0];
let state = create_test_state(0, 0, 10.0, &pop, &history);
assert!(
!criterion.should_terminate(&state),
"non-monotonic window with a mid-window peak must not read as stagnant"
);
let history = vec![10.0, 90.0, 20.0];
let state = create_test_state(0, 0, 20.0, &pop, &history);
assert!(!criterion.should_terminate(&state));
let history = vec![90.0, 50.0, 10.0];
let state = create_test_state(0, 0, 10.0, &pop, &history);
assert!(criterion.should_terminate(&state));
}
#[test]
fn test_target_fitness() {
let individuals = vec![Individual::with_fitness(RealVector::new(vec![1.0]), 10.0)];
let pop = Population::from_individuals(individuals);
let history = vec![];
let criterion = TargetFitness::new(0.0);
let state = create_test_state(0, 0, -10.0, &pop, &history);
assert!(!criterion.should_terminate(&state));
let state = create_test_state(0, 0, 0.0, &pop, &history);
assert!(criterion.should_terminate(&state));
let criterion = TargetFitness::with_tolerance(0.0, 0.1);
let state = create_test_state(0, 0, -0.05, &pop, &history);
assert!(criterion.should_terminate(&state));
}
#[test]
fn test_any_of() {
let individuals = vec![Individual::with_fitness(RealVector::new(vec![1.0]), 10.0)];
let pop = Population::from_individuals(individuals);
let history = vec![];
let criterion = AnyOf::new(vec![
Box::new(MaxGenerations::new(100)),
Box::new(TargetFitness::new(0.0)),
]);
let state = create_test_state(50, 0, -10.0, &pop, &history);
assert!(!criterion.should_terminate(&state));
let state = create_test_state(100, 0, -10.0, &pop, &history);
assert!(criterion.should_terminate(&state));
let state = create_test_state(50, 0, 0.0, &pop, &history);
assert!(criterion.should_terminate(&state));
}
#[test]
fn test_all_of() {
let individuals = vec![Individual::with_fitness(RealVector::new(vec![1.0]), 10.0)];
let pop = Population::from_individuals(individuals);
let history = vec![];
let criterion = AllOf::new(vec![
Box::new(MaxGenerations::new(100)),
Box::new(TargetFitness::new(0.0)),
]);
let state = create_test_state(50, 0, -10.0, &pop, &history);
assert!(!criterion.should_terminate(&state));
let state = create_test_state(100, 0, -10.0, &pop, &history);
assert!(!criterion.should_terminate(&state));
let state = create_test_state(50, 0, 0.0, &pop, &history);
assert!(!criterion.should_terminate(&state));
let state = create_test_state(100, 0, 0.0, &pop, &history);
assert!(criterion.should_terminate(&state));
}
}