use genetic_rs::prelude::*;
use genetic_rs::speciation::SpeciatedPopulation;
#[derive(Clone, Debug, PartialEq)]
struct Genome {
class: i32,
val: f32,
}
impl Speciated for Genome {
type Context = ();
fn divergence(&self, other: &Self, _: &()) -> f32 {
if self.class == other.class {
0.0
} else {
1.0
}
}
}
impl GenerateRandom for Genome {
fn gen_random(rng: &mut impl rand::Rng) -> Self {
Self {
class: rng.random_range(0..3),
val: rng.random(),
}
}
}
impl RandomlyMutable for Genome {
type Context = ();
fn mutate(&mut self, _: &(), rate: f32, rng: &mut impl rand::Rng) {
self.val += rng.random::<f32>() * rate;
}
}
impl Crossover for Genome {
type Context = ();
fn crossover(&self, other: &Self, _: &(), _rate: f32, _rng: &mut impl rand::Rng) -> Self {
Self {
class: self.class,
val: (self.val + other.val) / 2.0,
}
}
}
fn fitness(g: &Genome) -> f32 {
g.val
}
#[test]
fn identical_genomes_in_same_species() {
let genomes: Vec<Genome> = (0..5).map(|_| Genome { class: 0, val: 0.0 }).collect();
let pop = SpeciatedPopulation::from_genomes(&genomes, 0.5, &());
assert_eq!(pop.species().len(), 1, "expected a single species");
assert_eq!(
pop.species()[0].len(),
5,
"all genomes must belong to the single species"
);
}
#[test]
fn different_class_genomes_in_different_species() {
let genomes: Vec<Genome> = (0..4).map(|i| Genome { class: i, val: 0.0 }).collect();
let pop = SpeciatedPopulation::from_genomes(&genomes, 0.5, &());
assert_eq!(pop.species().len(), 4);
}
#[test]
fn high_threshold_groups_all_genomes() {
let genomes: Vec<Genome> = (0..6)
.map(|i| Genome {
class: i % 3,
val: 0.0,
})
.collect();
let pop = SpeciatedPopulation::from_genomes(&genomes, 1.5, &());
assert_eq!(pop.species().len(), 1, "all genomes must be in one species");
}
#[test]
fn every_genome_index_appears_exactly_once() {
let n = 9usize;
let genomes: Vec<Genome> = (0..n)
.map(|i| Genome {
class: (i % 3) as i32,
val: i as f32,
})
.collect();
let pop = SpeciatedPopulation::from_genomes(&genomes, 0.5, &());
let mut seen = vec![false; n];
for species in pop.species() {
for &idx in species {
assert!(
!seen[idx],
"genome index {idx} appears in more than one species"
);
seen[idx] = true;
}
}
assert!(
seen.iter().all(|&v| v),
"not every genome index appeared in a species"
);
}
#[test]
fn insert_genome_creates_new_species_for_novel_genome() {
let genomes = vec![
Genome { class: 0, val: 0.0 },
Genome { class: 1, val: 0.0 }, ];
let mut pop = SpeciatedPopulation::new(0.5);
pop.insert_genome(0, &genomes, &());
let created_new = pop.insert_genome(1, &genomes, &());
assert!(created_new, "expected a new species to be created");
assert_eq!(pop.species().len(), 2);
}
#[test]
fn insert_genome_joins_existing_species_for_similar_genome() {
let genomes = vec![
Genome { class: 0, val: 0.0 },
Genome { class: 0, val: 1.0 }, ];
let mut pop = SpeciatedPopulation::new(0.5);
pop.insert_genome(0, &genomes, &());
let created_new = pop.insert_genome(1, &genomes, &());
assert!(
!created_new,
"must not create a new species for a similar genome"
);
assert_eq!(pop.species().len(), 1);
assert_eq!(pop.species()[0].len(), 2);
}
#[test]
fn round_robin_covers_all_genomes() {
let genomes: Vec<Genome> = (0..6)
.map(|i| Genome {
class: (i % 3) as i32,
val: i as f32,
})
.collect();
let pop = SpeciatedPopulation::from_genomes(&genomes, 0.5, &());
let mut seen = vec![false; 6];
for idx in pop.round_robin().take(6) {
seen[idx] = true;
}
assert!(
seen.iter().all(|&v| v),
"round_robin must cover all genome indices in one cycle"
);
}
#[test]
fn round_robin_enumerate_species_index_is_valid() {
let genomes: Vec<Genome> = (0..6)
.map(|i| Genome {
class: (i % 3) as i32,
val: i as f32,
})
.collect();
let pop = SpeciatedPopulation::from_genomes(&genomes, 0.5, &());
for (species_i, genome_i) in pop.round_robin_enumerate().take(12) {
assert!(
pop.species()[species_i].contains(&genome_i),
"genome index {genome_i} is not in species {species_i}"
);
}
}
#[test]
fn speciated_sim_population_size_preserved() {
let mut rng = rand::rng();
let initial_size = 30;
let eliminator = SpeciatedFitnessEliminator::new(fitness, 0.5, 0.5, (), ());
let repopulator = SpeciatedCrossoverRepopulator::new(
0.1,
0.5,
ActionIfIsolated::CrossoverSimilarSpecies,
(),
(),
);
let mut sim = GeneticSim::new(
Vec::<Genome>::gen_random(&mut rng, initial_size),
eliminator,
repopulator,
);
sim.perform_generations(20);
assert_eq!(sim.genomes.len(), initial_size);
}
#[test]
fn speciation_protects_rare_species() {
let mut class0_genomes: Vec<Genome> = (0..4).map(|_| Genome { class: 0, val: 1.0 }).collect();
let rare = Genome { class: 1, val: 0.5 };
class0_genomes.push(rare.clone());
let mut eliminator = SpeciatedFitnessEliminator::new(fitness, 0.5, 0.5, (), ());
let survivors = eliminator.eliminate(class0_genomes);
assert!(
survivors.iter().any(|g| g == &rare),
"the rare species genome must survive despite lower raw fitness"
);
}
#[test]
fn observer_receives_fitness_sorted_by_raw_descending() {
use std::sync::{Arc, Mutex};
let observed: Arc<Mutex<Vec<f32>>> = Arc::new(Mutex::new(Vec::new()));
let observed_clone = Arc::clone(&observed);
let observer = move |fitnesses: &[(Genome, f32)]| {
let mut v = observed_clone.lock().unwrap();
v.extend(fitnesses.iter().map(|(_, f)| *f));
};
let mut genomes = vec![Genome { class: 1, val: 0.5 }];
genomes.extend((0..4).map(|_| Genome { class: 0, val: 1.0 }));
let mut eliminator = SpeciatedFitnessEliminator::new(fitness, 0.5, 0.5, observer, ());
eliminator.eliminate(genomes);
let scores = observed.lock().unwrap();
assert_eq!(scores.len(), 5, "observer must receive all genomes");
for window in scores.windows(2) {
assert!(
window[0] >= window[1],
"observer inputs must be sorted descending by fitness, but got: {:?}",
*scores
);
}
assert!(
(scores[0] - 1.0_f32).abs() < 1e-6,
"first fitness must be the highest raw fitness (1.0), but got: {:?}",
*scores
);
assert!(
(scores[4] - 0.5_f32).abs() < 1e-6,
"last fitness must be the lowest raw fitness (0.5), but got: {:?}",
*scores
);
}
#[test]
fn observer_receives_pre_division_fitness() {
use std::sync::{Arc, Mutex};
let observed: Arc<Mutex<Vec<f32>>> = Arc::new(Mutex::new(Vec::new()));
let observed_clone = Arc::clone(&observed);
let observer = move |fitnesses: &[(Genome, f32)]| {
let mut v = observed_clone.lock().unwrap();
v.extend(fitnesses.iter().map(|(_, f)| *f));
};
let mut class0_genomes: Vec<Genome> = (0..4).map(|_| Genome { class: 0, val: 1.0 }).collect();
class0_genomes.push(Genome { class: 1, val: 0.5 });
let mut eliminator = SpeciatedFitnessEliminator::new(fitness, 0.5, 0.5, observer, ());
eliminator.eliminate(class0_genomes);
let scores = observed.lock().unwrap();
assert!(
scores.iter().any(|&f| (f - 1.0_f32).abs() < 1e-6),
"observer must see the raw fitness 1.0, but got: {:?}",
*scores,
);
assert!(
scores.iter().any(|&f| (f - 0.5_f32).abs() < 1e-6),
"observer must see the raw fitness 0.5, but got: {:?}",
*scores,
);
assert!(
!scores.iter().any(|&f| (f - 0.25_f32).abs() < 1e-6),
"observer must NOT see the divided fitness 0.25 (pre-division values expected), but got: {:?}",
*scores,
);
}