use rand::{rngs::StdRng, Rng, SeedableRng};
use std::cmp::Ordering;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Optimize {
Maximize,
Minimize,
}
#[derive(Clone, Debug)]
pub struct Evaluated<G, F> {
pub genome: G,
pub fitness: F,
}
pub trait Problem {
type Genome: Clone + Send + Sync + 'static;
type Fitness: PartialOrd + Copy + Send + Sync + 'static;
fn random_genome<R: Rng + ?Sized>(&mut self, rng: &mut R) -> Self::Genome;
fn fitness(&self, genome: &Self::Genome) -> Self::Fitness;
fn mutate<R: Rng + ?Sized>(&mut self, _genome: &mut Self::Genome, _rng: &mut R) {
}
fn crossover<R: Rng + ?Sized>(
&mut self,
a: &Self::Genome,
_b: &Self::Genome,
_rng: &mut R,
) -> Self::Genome {
a.clone()
}
fn repair(&mut self, _genome: &mut Self::Genome) {
}
}
pub trait Selection<P: Problem> {
fn select<'a, R: Rng + ?Sized>(
&self,
rng: &mut R,
population: &'a [Evaluated<P::Genome, P::Fitness>],
optimize: Optimize,
) -> (&'a P::Genome, &'a P::Genome);
}
#[derive(Clone, Copy, Debug)]
pub struct TournamentSelection {
pub k: usize,
}
impl Default for TournamentSelection {
fn default() -> Self {
Self { k: 3 }
}
}
impl<P: Problem> Selection<P> for TournamentSelection {
fn select<'a, R: Rng + ?Sized>(
&self,
rng: &mut R,
population: &'a [Evaluated<P::Genome, P::Fitness>],
optimize: Optimize,
) -> (&'a P::Genome, &'a P::Genome) {
debug_assert!(population.len() >= 2, "population must have at least 2 individuals");
let pick_one = |rng: &mut R| {
let k = self.k.max(1).min(population.len());
let mut best_idx = rng.gen_range(0..population.len());
for _ in 1..k {
let idx = rng.gen_range(0..population.len());
best_idx = better_index(population, best_idx, idx, optimize);
}
&population[best_idx].genome
};
(pick_one(rng), pick_one(rng))
}
}
#[derive(Clone, Debug)]
pub struct Config {
pub pop_size: usize,
pub optimize: Optimize,
pub crossover_rate: f64, pub mutation_rate: f64, pub elitism: usize, pub seed: Option<u64>, }
impl Default for Config {
fn default() -> Self {
Self {
pop_size: 100,
optimize: Optimize::Maximize,
crossover_rate: 0.9,
mutation_rate: 0.1,
elitism: 1,
seed: None,
}
}
}
pub struct Builder<P: Problem, S: Selection<P> = TournamentSelection> {
problem: P,
config: Config,
selection: S,
}
impl<P: Problem> Builder<P, TournamentSelection> {
pub fn new(problem: P) -> Self {
Self {
problem,
config: Config::default(),
selection: TournamentSelection::default(),
}
}
}
impl<P: Problem, S: Selection<P>> Builder<P, S> {
pub fn pop_size(mut self, n: usize) -> Self {
self.config.pop_size = n.max(2);
self
}
pub fn optimize(mut self, opt: Optimize) -> Self {
self.config.optimize = opt;
self
}
pub fn mutation_rate(mut self, p: f64) -> Self {
self.config.mutation_rate = clamp01(p);
self
}
pub fn crossover_rate(mut self, p: f64) -> Self {
self.config.crossover_rate = clamp01(p);
self
}
pub fn elitism(mut self, k: usize) -> Self {
self.config.elitism = k;
self
}
pub fn seed(mut self, seed: u64) -> Self {
self.config.seed = Some(seed);
self
}
pub fn selection<S2: Selection<P>>(self, selection: S2) -> Builder<P, S2> {
Builder {
problem: self.problem,
config: self.config,
selection,
}
}
pub fn build(mut self) -> GA<P, S> {
let mut rng = match self.config.seed {
Some(s) => StdRng::seed_from_u64(s),
None => StdRng::from_entropy(),
};
let mut pop = Vec::with_capacity(self.config.pop_size);
for _ in 0..self.config.pop_size {
let g = self.problem.random_genome(&mut rng);
let f = self.problem.fitness(&g);
pop.push(Evaluated { genome: g, fitness: f });
}
GA {
problem: self.problem,
config: self.config,
selection: self.selection,
rng,
population: pop,
generation: 0,
}
}
}
pub struct GA<P: Problem, S: Selection<P> = TournamentSelection> {
problem: P,
config: Config,
selection: S,
rng: StdRng,
population: Vec<Evaluated<P::Genome, P::Fitness>>,
generation: usize,
}
#[derive(Clone, Debug)]
pub struct RunResult<G, F> {
pub generations: usize,
pub best: Evaluated<G, F>,
pub average_fitness: f64,
}
impl<P: Problem, S: Selection<P>> GA<P, S> {
pub fn builder(problem: P) -> Builder<P, TournamentSelection> {
Builder::new(problem)
}
pub fn generation(&self) -> usize {
self.generation
}
pub fn best(&self) -> &Evaluated<P::Genome, P::Fitness> {
best_of(&self.population, self.config.optimize)
.expect("population must be non-empty")
}
pub fn average_fitness(&self) -> f64
where
P::Fitness: Into<f64>,
{
let sum: f64 = self.population.iter().map(|e| e.fitness.into()).sum();
sum / self.population.len() as f64
}
pub fn step(&mut self) {
let n = self.population.len();
let elitism = self.config.elitism.min(n);
let mut next: Vec<Evaluated<P::Genome, P::Fitness>> = if elitism > 0 {
let mut sorted = self.population.clone();
sorted.sort_by(|a, b| cmp_fit(self.config.optimize, &a.fitness, &b.fitness));
sorted.into_iter().rev().take(elitism).collect()
} else {
Vec::with_capacity(n)
};
while next.len() < n {
let (pa, pb) = self.selection.select(&mut self.rng, &self.population, self.config.optimize);
let mut child = if chance(&mut self.rng, self.config.crossover_rate) {
self.problem.crossover(pa, pb, &mut self.rng)
} else {
pa.clone()
};
if chance(&mut self.rng, self.config.mutation_rate) {
self.problem.mutate(&mut child, &mut self.rng);
}
self.problem.repair(&mut child);
let fit = self.problem.fitness(&child);
next.push(Evaluated { genome: child, fitness: fit });
}
self.population = next;
self.generation += 1;
}
pub fn run_for(&mut self, generations: usize) -> RunResult<P::Genome, P::Fitness>
where
P::Fitness: Into<f64>,
{
for _ in 0..generations {
self.step();
}
RunResult {
generations: self.generation,
best: self.best().clone(),
average_fitness: self.average_fitness(),
}
}
pub fn run_until<F>(
&mut self,
max_generations: usize,
mut stop_when: F,
) -> RunResult<P::Genome, P::Fitness>
where
F: FnMut(usize, &Evaluated<P::Genome, P::Fitness>) -> bool,
P::Fitness: Into<f64>,
{
for _ in 0..max_generations {
if stop_when(self.generation, self.best()) {
break;
}
self.step();
}
RunResult {
generations: self.generation,
best: self.best().clone(),
average_fitness: self.average_fitness(),
}
}
pub fn population(&self) -> &[Evaluated<P::Genome, P::Fitness>] {
&self.population
}
pub fn problem_mut(&mut self) -> &mut P {
&mut self.problem
}
}
fn clamp01(p: f64) -> f64 {
if p.is_nan() {
0.0
} else if p < 0.0 {
0.0
} else if p > 1.0 {
1.0
} else {
p
}
}
fn chance<R: Rng + ?Sized>(rng: &mut R, p: f64) -> bool {
rng.gen_bool(clamp01(p))
}
fn cmp_fit<F: PartialOrd>(optimize: Optimize, a: &F, b: &F) -> Ordering {
let ord = a.partial_cmp(b).unwrap_or(Ordering::Equal);
match optimize {
Optimize::Maximize => ord,
Optimize::Minimize => ord.reverse(),
}
}
fn best_of<G: Clone, F: PartialOrd>(
pop: &[Evaluated<G, F>],
optimize: Optimize,
) -> Option<&Evaluated<G, F>> {
pop.iter()
.max_by(|a, b| cmp_fit(optimize, &a.fitness, &b.fitness))
}
fn better_index<G, F: PartialOrd>(
pop: &[Evaluated<G, F>],
i: usize,
j: usize,
optimize: Optimize,
) -> usize {
match cmp_fit(optimize, &pop[i].fitness, &pop[j].fitness) {
Ordering::Less => j,
_ => i,
}
}
pub mod prelude {
pub use super::{
Builder, Config, Evaluated, GA, Optimize, Problem, Selection, TournamentSelection,
};
}