use crate::fitness::FitnessValue;
use crate::genome::{BehaviorDescriptor, Genome};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Individual<G: Genome> {
pub genome: G,
pub fitness: Option<FitnessValue>,
pub behavior: Option<BehaviorDescriptor>,
pub birth_generation: usize,
}
impl<G: Genome> Individual<G> {
pub fn new(genome: G) -> Self {
Self {
genome,
fitness: None,
behavior: None,
birth_generation: 0,
}
}
pub fn fitness_value(&self) -> f64 {
self.fitness.as_ref().map(|f| f.primary()).unwrap_or(0.0)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PopulationConfig {
pub size: usize,
pub elitism: usize,
}
impl Default for PopulationConfig {
fn default() -> Self {
Self {
size: 100,
elitism: 5,
}
}
}
#[derive(Debug, Clone)]
pub struct Population<G: Genome> {
pub individuals: Vec<Individual<G>>,
pub generation: usize,
pub config: PopulationConfig,
}
impl<G: Genome> Population<G> {
pub fn random<R: rand::Rng>(config: PopulationConfig, rng: &mut R) -> Self {
let individuals = (0..config.size)
.map(|_| Individual::new(G::random(rng)))
.collect();
Self {
individuals,
generation: 0,
config,
}
}
pub fn best(&self) -> Option<&Individual<G>> {
self.individuals
.iter()
.filter(|i| i.fitness.is_some())
.max_by(|a, b| {
a.fitness_value()
.partial_cmp(&b.fitness_value())
.unwrap_or(std::cmp::Ordering::Equal)
})
}
pub fn top_n(&self, n: usize) -> Vec<&Individual<G>> {
let mut sorted: Vec<_> = self.individuals.iter().collect();
sorted.sort_by(|a, b| {
b.fitness_value()
.partial_cmp(&a.fitness_value())
.unwrap_or(std::cmp::Ordering::Equal)
});
sorted.into_iter().take(n).collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use rand::SeedableRng;
use rand_chacha::ChaCha8Rng;
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
struct TestGenome {
value: f64,
}
impl Genome for TestGenome {
type Phenotype = f64;
fn random<R: rand::Rng>(rng: &mut R) -> Self {
Self {
value: rng.gen_range(0.0..1.0),
}
}
fn mutate<R: rand::Rng>(&mut self, rng: &mut R, _rate: f64) {
self.value = rng.gen_range(0.0..1.0);
}
fn crossover<R: rand::Rng>(&self, other: &Self, _rng: &mut R) -> Self {
Self {
value: (self.value + other.value) / 2.0,
}
}
fn to_phenotype(&self) -> f64 {
self.value
}
}
#[test]
fn test_individual_new() {
let genome = TestGenome { value: 0.5 };
let ind = Individual::new(genome);
assert!(ind.fitness.is_none());
assert!(ind.behavior.is_none());
assert_eq!(ind.birth_generation, 0);
}
#[test]
fn test_individual_fitness_value_none() {
let genome = TestGenome { value: 0.5 };
let ind = Individual::new(genome);
assert!((ind.fitness_value() - 0.0).abs() < 1e-10);
}
#[test]
fn test_individual_fitness_value_some() {
let genome = TestGenome { value: 0.5 };
let mut ind = Individual::new(genome);
ind.fitness = Some(FitnessValue::Single(0.75));
assert!((ind.fitness_value() - 0.75).abs() < 1e-10);
}
#[test]
fn test_population_config_default() {
let config = PopulationConfig::default();
assert_eq!(config.size, 100);
assert_eq!(config.elitism, 5);
}
#[test]
fn test_population_random_size() {
let mut rng = ChaCha8Rng::seed_from_u64(42);
let config = PopulationConfig {
size: 20,
elitism: 2,
};
let pop: Population<TestGenome> = Population::random(config, &mut rng);
assert_eq!(pop.individuals.len(), 20);
assert_eq!(pop.generation, 0);
}
#[test]
fn test_population_best_none_evaluated() {
let mut rng = ChaCha8Rng::seed_from_u64(42);
let config = PopulationConfig {
size: 10,
elitism: 1,
};
let pop: Population<TestGenome> = Population::random(config, &mut rng);
assert!(pop.best().is_none());
}
#[test]
fn test_population_best_with_fitness() {
let mut rng = ChaCha8Rng::seed_from_u64(42);
let config = PopulationConfig {
size: 10,
elitism: 1,
};
let mut pop: Population<TestGenome> = Population::random(config, &mut rng);
for (i, ind) in pop.individuals.iter_mut().enumerate() {
ind.fitness = Some(FitnessValue::Single(i as f64));
}
let best = pop.best().unwrap();
assert!((best.fitness_value() - 9.0).abs() < 1e-10);
}
#[test]
fn test_population_top_n() {
let mut rng = ChaCha8Rng::seed_from_u64(42);
let config = PopulationConfig {
size: 10,
elitism: 1,
};
let mut pop: Population<TestGenome> = Population::random(config, &mut rng);
for (i, ind) in pop.individuals.iter_mut().enumerate() {
ind.fitness = Some(FitnessValue::Single(i as f64));
}
let top3 = pop.top_n(3);
assert_eq!(top3.len(), 3);
assert!((top3[0].fitness_value() - 9.0).abs() < 1e-10);
assert!((top3[1].fitness_value() - 8.0).abs() < 1e-10);
assert!((top3[2].fitness_value() - 7.0).abs() < 1e-10);
}
#[test]
fn test_population_top_n_larger_than_population() {
let mut rng = ChaCha8Rng::seed_from_u64(42);
let config = PopulationConfig {
size: 5,
elitism: 1,
};
let pop: Population<TestGenome> = Population::random(config, &mut rng);
let top10 = pop.top_n(10);
assert_eq!(top10.len(), 5); }
}