diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 4f62fd7..28e4e0f 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -12,4 +12,4 @@ jobs: steps: - uses: actions/checkout@v4 - name: Run cargo test - run: cargo test --verbose \ No newline at end of file + run: cargo test --verbose --features rayon, crossover \ No newline at end of file diff --git a/examples/readme_ex.rs b/examples/readme_ex.rs index 1745139..176f3a4 100644 --- a/examples/readme_ex.rs +++ b/examples/readme_ex.rs @@ -45,6 +45,7 @@ fn my_fitness_fn(ent: &MyEntity) -> f32 { ent.field1 } +#[cfg(not(feature = "rayon"))] fn main() { let mut rng = rand::thread_rng(); let mut sim = GeneticSim::new( @@ -61,5 +62,24 @@ fn main() { sim.next_generation(); // in a genetic algorithm with state, such as a physics simulation, you'd want to do things with `sim.entities` in between these calls } + dbg!(sim.entities); +} + +#[cfg(feature = "rayon")] +fn main() { + let mut sim = GeneticSim::new( + // you must provide a random starting population. + // size will be preserved in builtin nextgen fns, but it is not required to keep a constant size if you were to build your own nextgen function. + // in this case, you do not need to specify a type for `Vec::gen_random` because of the input of `my_fitness_fn`. + Vec::gen_random(100), + my_fitness_fn, + division_pruning_nextgen, + ); + + // perform evolution (100 gens) + for _ in 0..100 { + sim.next_generation(); // in a genetic algorithm with state, such as a physics simulation, you'd want to do things with `sim.entities` in between these calls + } + dbg!(sim.entities); } \ No newline at end of file diff --git a/src/builtin.rs b/src/builtin.rs index ba9113c..c935ed9 100644 --- a/src/builtin.rs +++ b/src/builtin.rs @@ -30,15 +30,15 @@ pub mod next_gen { use super::*; #[cfg(feature = "rayon")] use rayon::prelude::*; + use rand::{rngs::StdRng, SeedableRng}; /// When making a new generation, it mutates each entity a certain amount depending on their reward. /// This nextgen is very situational and should not be your first choice. - #[cfg(not(feature = "rayon"))] pub fn scrambling_nextgen(mut rewards: Vec<(E, f32)>) -> Vec { rewards.sort_by(|(_, r1), (_, r2)| r1.partial_cmp(r2).unwrap()); let len = rewards.len() as f32; - let mut rng = rand::thread_rng(); + let mut rng = StdRng::from_rng(rand::thread_rng()).unwrap(); rewards .into_iter() @@ -50,30 +50,35 @@ pub mod next_gen { .collect() } - #[cfg(feature = "rayon")] - pub fn scrambling_nextgen(mut rewards: Vec<(E, f32)>) -> Vec { - rewards.sort_by(|(_, r1), (_, r2)| r1.partial_cmp(r2).unwrap()); + /// When making a new generation, it despawns half of the entities and then spawns children from the remaining to reproduce. + /// WIP: const generic for mutation rate, will allow for [DivisionReproduction::spawn_child] to accept a custom mutation rate. Delayed due to current Rust limitations + #[cfg(not(feature = "rayon"))] + pub fn division_pruning_nextgen(rewards: Vec<(E, f32)>) -> Vec { + let population_size = rewards.len(); + let mut next_gen = pruning_helper(rewards); - let len = rewards.len() as f32; - let mut rng = rand::thread_rng(); + let mut rng = StdRng::from_rng(rand::thread_rng()).unwrap(); - rewards - .into_par_iter() - .enumerate() - .map(|(i, (mut e, _))| { - e.mutate(i as f32 / len, &mut rng); - e - }) - .collect() + let mut og_champions = next_gen + .clone() // TODO remove if possible. currently doing so because `next_gen` is borrowed as mutable later + .into_iter() + .cycle(); + + while next_gen.len() < population_size { + let e = og_champions.next().unwrap(); + + next_gen.push(e.spawn_child(&mut rng)); + } + + next_gen } - /// When making a new generation, it despawns half of the entities and then spawns children from the remaining to reproduce. - /// WIP: const generic for mutation rate, will allow for [DivisionReproduction::spawn_child] to accept a custom mutation rate. Delayed due to current Rust limitations - pub fn division_pruning_nextgen(rewards: Vec<(E, f32)>) -> Vec { + #[cfg(feature = "rayon")] + pub fn division_pruning_nextgen(rewards: Vec<(E, f32)>) -> Vec { let population_size = rewards.len(); let mut next_gen = pruning_helper(rewards); - let mut rng = rand::thread_rng(); + let mut rng = StdRng::from_rng(rand::thread_rng()).unwrap(); let mut og_champions = next_gen .clone() // TODO remove if possible. currently doing so because `next_gen` is borrowed as mutable later @@ -92,12 +97,11 @@ pub mod next_gen { /// Prunes half of the entities and randomly breeds the remaining ones. /// S: allow selfbreeding - false by default. #[cfg(feature = "crossover")] - pub fn crossover_pruning_nextgen(rewards: Vec<(E, f32)>) -> Vec { + pub fn crossover_pruning_nextgen(rewards: Vec<(E, f32)>) -> Vec { let population_size = rewards.len(); let mut next_gen = pruning_helper(rewards); - // TODO better/more customizable rng - let mut rng = rand::thread_rng(); + let mut rng = StdRng::from_rng(rand::thread_rng()).unwrap(); // TODO remove clone smh let og_champions = next_gen.clone(); @@ -140,7 +144,7 @@ pub mod next_gen { } #[cfg(feature = "rayon")] - fn pruning_helper(mut rewards: Vec<(E, f32)>) -> Vec { + fn pruning_helper(mut rewards: Vec<(E, f32)>) -> Vec { rewards.sort_by(|(_, r1), (_, r2)| r1.partial_cmp(r2).unwrap()); let median = rewards[rewards.len() / 2].1; diff --git a/src/lib.rs b/src/lib.rs index e3af018..4cca3a3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -151,6 +151,7 @@ pub mod prelude; /// dbg!(sim.entities); /// } /// ``` +#[cfg(not(feature = "rayon"))] pub struct GeneticSim where E: Sized, @@ -161,6 +162,18 @@ where next_gen: Box) -> Vec + Send + Sync + 'static>, } +#[cfg(feature = "rayon")] +pub struct GeneticSim +where + E: Sized + Send, +{ + /// The current population of entities + pub entities: Vec, + fitness: Box f32 + Send + Sync + 'static>, + next_gen: Box) -> Vec + Send + Sync + 'static>, +} + +#[cfg(not(feature = "rayon"))] impl GeneticSim where E: Sized, @@ -180,7 +193,6 @@ where } /// Uses the `next_gen` provided in [GeneticSim::new] to create the next generation of entities. - #[cfg(not(feature = "rayon"))] pub fn next_generation(&mut self) { // TODO maybe remove unneccessary dependency, can prob use std::mem::replace replace_with_or_abort(&mut self.entities, |entities| { @@ -195,8 +207,25 @@ where (self.next_gen)(rewards) }); } +} + +#[cfg(feature = "rayon")] +impl GeneticSim +where + E: Sized + Send, +{ + pub fn new( + starting_entities: Vec, + fitness: impl Fn(&E) -> f32 + Send + Sync + 'static, + next_gen: impl Fn(Vec<(E, f32) >) -> Vec + Send + Sync + 'static + ) -> Self { + Self { + entities: starting_entities, + fitness: Box::new(fitness), + next_gen: Box::new(next_gen), + } + } - #[cfg(feature = "rayon")] pub fn next_generation(&mut self) { replace_with_or_abort(&mut self.entities, |entities| { let rewards = entities @@ -222,7 +251,7 @@ pub trait GenerateRandom { } /// Blanket trait used on collections that contain objects implementing GenerateRandom -#[cfg(feature = "genrand")] +#[cfg(all(feature = "genrand", not(feature = "rayon")))] pub trait GenerateRandomCollection where T: GenerateRandom, @@ -231,6 +260,14 @@ where fn gen_random(rng: &mut impl Rng, amount: usize) -> Self; } +#[cfg(all(feature = "genrand", feature = "rayon"))] +pub trait GenerateRandomCollection +where + T: GenerateRandom + Send, +{ + fn gen_random(amount: usize) -> Self; +} + #[cfg(not(feature = "rayon"))] impl GenerateRandomCollection for C where @@ -248,13 +285,13 @@ where #[cfg(feature = "rayon")] impl GenerateRandomCollection for C where - C: FromIterator, - T: GenerateRandom, + C: FromParallelIterator, + T: GenerateRandom + Send, { - fn gen_random(rng: &mut impl Rng, amount: usize) -> Self { + fn gen_random(amount: usize) -> Self { (0..amount) .into_par_iter() - .map(|_| T::gen_random(rng)) + .map(|_| T::gen_random(&mut rand::thread_rng())) .collect() } } @@ -298,6 +335,7 @@ mod tests { (MAGIC_NUMBER - ent.0).abs() * -1. } + #[cfg(not(feature = "rayon"))] #[test] fn scramble() { let mut rng = rand::thread_rng(); @@ -314,6 +352,7 @@ mod tests { dbg!(sim.entities); } + #[cfg(not(feature = "rayon"))] #[test] fn d_prune() { let mut rng = rand::thread_rng(); @@ -340,4 +379,20 @@ mod tests { h.join().unwrap(); } + + #[cfg(feature = "rayon")] + #[test] + fn rayon_test() { + let mut sim = GeneticSim::new( + Vec::gen_random(100), + my_fitness_fn, + division_pruning_nextgen, + ); + + for _ in 0..100 { + sim.next_generation(); + } + + dbg!(sim.entities); + } } \ No newline at end of file