use rand::RngCore;
use std::{fmt, marker::PhantomData};
pub trait Genome: Clone + Send + Sync + Sized {
fn random(rng: &mut dyn RngCore) -> Self;
fn mutate(&mut self, rng: &mut dyn RngCore);
fn crossover(&self, other: &Self, rng: &mut dyn RngCore) -> Self;
}
#[derive(Debug, Clone)]
pub struct OptimizationOutcome<M> {
pub fitness: f64,
pub metrics: M,
}
#[derive(thiserror::Error, Debug)]
pub enum OptimizationError {
#[error("population size must be greater than zero")]
EmptyPopulation,
#[error("elitism must be smaller than the population size")]
InvalidElitism,
#[error("tournament size must be greater than zero")]
InvalidTournamentSize,
#[error("candidate evaluation failed: {0}")]
EvaluationFailed(String),
}
#[derive(Debug, Clone)]
pub struct OptimizationResult<G, M>
where
G: Genome,
M: Clone + Send + Sync,
{
pub best_candidate: G,
pub best_metrics: M,
pub best_fitness: f64,
pub generations: Vec<GenerationSummary<M>>,
}
#[derive(Debug, Clone)]
pub struct GenerationSummary<M>
where
M: Clone + Send + Sync,
{
pub index: usize,
pub best_fitness: f64,
pub average_fitness: f64,
pub best_metrics: M,
}
#[derive(Debug, Clone, Copy)]
pub struct GeneticOptimizerConfig {
pub population_size: usize,
pub elitism: usize,
pub generations: usize,
pub tournament_size: usize,
}
impl Default for GeneticOptimizerConfig {
fn default() -> Self {
Self {
population_size: 32,
elitism: 2,
generations: 20,
tournament_size: 3,
}
}
}
pub trait FitnessEvaluator<G>: Send + Sync
where
G: Genome,
{
type Metrics: Clone + Send + Sync;
fn evaluate(
&self,
candidate: &G,
) -> Result<OptimizationOutcome<Self::Metrics>, Box<dyn std::error::Error + Send + Sync>>;
}
impl<G, M, F, E> FitnessEvaluator<G> for F
where
G: Genome,
M: Clone + Send + Sync + 'static,
F: Fn(&G) -> Result<OptimizationOutcome<M>, E> + Send + Sync,
E: std::error::Error + Send + Sync + 'static,
{
type Metrics = M;
fn evaluate(
&self,
candidate: &G,
) -> Result<OptimizationOutcome<M>, Box<dyn std::error::Error + Send + Sync>> {
self(candidate).map_err(|err| Box::new(err) as _)
}
}
#[derive(Clone)]
struct Individual<G, M>
where
G: Genome,
M: Clone + Send + Sync,
{
genome: G,
metrics: Option<M>,
fitness: f64,
}
impl<G, M> Individual<G, M>
where
G: Genome,
M: Clone + Send + Sync,
{
fn unevaluated(genome: G) -> Self {
Self {
genome,
metrics: None,
fitness: f64::NEG_INFINITY,
}
}
}
pub struct GeneticOptimizer<G, E>
where
G: Genome,
E: FitnessEvaluator<G>,
{
config: GeneticOptimizerConfig,
evaluator: E,
phantom: PhantomData<G>,
}
impl<G, E> GeneticOptimizer<G, E>
where
G: Genome,
E: FitnessEvaluator<G>,
{
pub fn new(config: GeneticOptimizerConfig, evaluator: E) -> Self {
Self {
config,
evaluator,
phantom: PhantomData,
}
}
pub fn run<R>(
&self,
rng: &mut R,
) -> Result<OptimizationResult<G, E::Metrics>, OptimizationError>
where
R: RngCore,
{
if self.config.population_size == 0 {
return Err(OptimizationError::EmptyPopulation);
}
if self.config.elitism >= self.config.population_size {
return Err(OptimizationError::InvalidElitism);
}
if self.config.tournament_size == 0 {
return Err(OptimizationError::InvalidTournamentSize);
}
let mut population: Vec<Individual<G, E::Metrics>> = (0..self.config.population_size)
.map(|_| Individual::unevaluated(G::random(rng)))
.collect();
let mut generation_summaries = Vec::with_capacity(self.config.generations + 1);
self.evaluate_population(&mut population)?;
population.sort_by(|a, b| b.fitness.total_cmp(&a.fitness));
generation_summaries.push(Self::summarize_generation(0, &population));
for generation in 1..=self.config.generations {
let mut next_population: Vec<Individual<G, E::Metrics>> =
Vec::with_capacity(self.config.population_size);
next_population.extend(population.iter().take(self.config.elitism).cloned());
while next_population.len() < self.config.population_size {
let parent_a =
Self::tournament_select(&population, self.config.tournament_size, rng);
let parent_b =
Self::tournament_select(&population, self.config.tournament_size, rng);
let mut child_genome = parent_a.genome.crossover(&parent_b.genome, rng);
child_genome.mutate(rng);
next_population.push(Individual::unevaluated(child_genome));
}
population = next_population;
self.evaluate_population(&mut population)?;
population.sort_by(|a, b| b.fitness.total_cmp(&a.fitness));
generation_summaries.push(Self::summarize_generation(generation, &population));
}
let best = population
.first()
.expect("population cannot be empty after initialization");
Ok(OptimizationResult {
best_candidate: best.genome.clone(),
best_metrics: best
.metrics
.clone()
.expect("metrics must be present after evaluation"),
best_fitness: best.fitness,
generations: generation_summaries,
})
}
fn evaluate_population(
&self,
population: &mut [Individual<G, E::Metrics>],
) -> Result<(), OptimizationError> {
for individual in population.iter_mut() {
if individual.metrics.is_some() {
continue;
}
let outcome = self
.evaluator
.evaluate(&individual.genome)
.map_err(|err| OptimizationError::EvaluationFailed(err.to_string()))?;
individual.fitness = if outcome.fitness.is_finite() {
outcome.fitness
} else {
f64::NEG_INFINITY
};
individual.metrics = Some(outcome.metrics);
}
Ok(())
}
fn tournament_select<'a, R>(
population: &'a [Individual<G, E::Metrics>],
tournament_size: usize,
rng: &mut R,
) -> &'a Individual<G, E::Metrics>
where
R: RngCore,
{
let mut best_index = rng.next_u32() as usize % population.len();
let mut best_fitness = population[best_index].fitness;
for _ in 1..tournament_size {
let idx = rng.next_u32() as usize % population.len();
let fitness = population[idx].fitness;
if fitness > best_fitness {
best_index = idx;
best_fitness = fitness;
}
}
&population[best_index]
}
fn summarize_generation(
index: usize,
population: &[Individual<G, E::Metrics>],
) -> GenerationSummary<E::Metrics> {
let mut total = 0.0;
let mut count = 0usize;
let mut best: Option<&Individual<G, E::Metrics>> = None;
for individual in population {
if best
.as_ref()
.map(|current| individual.fitness > current.fitness)
.unwrap_or(true)
{
best = Some(individual);
}
if individual.fitness.is_finite() {
total += individual.fitness;
count += 1;
}
}
let average = if count > 0 {
total / count as f64
} else {
f64::NEG_INFINITY
};
let best = best.expect("population must contain at least one individual");
GenerationSummary {
index,
best_fitness: best.fitness,
average_fitness: average,
best_metrics: best
.metrics
.clone()
.expect("metrics must exist after evaluation"),
}
}
}
impl<G, E> fmt::Debug for GeneticOptimizer<G, E>
where
G: Genome,
E: FitnessEvaluator<G>,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("GeneticOptimizer")
.field("config", &self.config)
.finish()
}
}