Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix rayon feature issues #11

Merged
merged 2 commits into from
Jan 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci-cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Run cargo test
run: cargo test --verbose
run: cargo test --verbose --features rayon, crossover
20 changes: 20 additions & 0 deletions examples/readme_ex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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);
}
50 changes: 27 additions & 23 deletions src/builtin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<E: RandomlyMutable>(mut rewards: Vec<(E, f32)>) -> Vec<E> {
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()
Expand All @@ -50,30 +50,35 @@ pub mod next_gen {
.collect()
}

#[cfg(feature = "rayon")]
pub fn scrambling_nextgen<E: RandomlyMutable>(mut rewards: Vec<(E, f32)>) -> Vec<E> {
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<E: DivisionReproduction + Prunable + Clone>(rewards: Vec<(E, f32)>) -> Vec<E> {
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<E: DivisionReproduction + Prunable + Clone>(rewards: Vec<(E, f32)>) -> Vec<E> {
#[cfg(feature = "rayon")]
pub fn division_pruning_nextgen<E: DivisionReproduction + Prunable + Clone + Send>(rewards: Vec<(E, f32)>) -> Vec<E> {
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
Expand All @@ -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<E: CrossoverReproduction + Prunable + Clone, const S: bool = false>(rewards: Vec<(E, f32)>) -> Vec<E> {
pub fn crossover_pruning_nextgen<E: CrossoverReproduction + Prunable + Clone + Send, const S: bool = false>(rewards: Vec<(E, f32)>) -> Vec<E> {
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();
Expand Down Expand Up @@ -140,7 +144,7 @@ pub mod next_gen {
}

#[cfg(feature = "rayon")]
fn pruning_helper<E: Prunable + Clone>(mut rewards: Vec<(E, f32)>) -> Vec<E> {
fn pruning_helper<E: Prunable + Send>(mut rewards: Vec<(E, f32)>) -> Vec<E> {
rewards.sort_by(|(_, r1), (_, r2)| r1.partial_cmp(r2).unwrap());

let median = rewards[rewards.len() / 2].1;
Expand Down
69 changes: 62 additions & 7 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ pub mod prelude;
/// dbg!(sim.entities);
/// }
/// ```
#[cfg(not(feature = "rayon"))]
pub struct GeneticSim<E>
where
E: Sized,
Expand All @@ -161,6 +162,18 @@ where
next_gen: Box<dyn Fn(Vec<(E, f32)>) -> Vec<E> + Send + Sync + 'static>,
}

#[cfg(feature = "rayon")]
pub struct GeneticSim<E>
where
E: Sized + Send,
{
/// The current population of entities
pub entities: Vec<E>,
fitness: Box<dyn Fn(&E) -> f32 + Send + Sync + 'static>,
next_gen: Box<dyn Fn(Vec<(E, f32)>) -> Vec<E> + Send + Sync + 'static>,
}

#[cfg(not(feature = "rayon"))]
impl<E> GeneticSim<E>
where
E: Sized,
Expand All @@ -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| {
Expand All @@ -195,8 +207,25 @@ where
(self.next_gen)(rewards)
});
}
}

#[cfg(feature = "rayon")]
impl<E> GeneticSim<E>
where
E: Sized + Send,
{
pub fn new(
starting_entities: Vec<E>,
fitness: impl Fn(&E) -> f32 + Send + Sync + 'static,
next_gen: impl Fn(Vec<(E, f32) >) -> Vec<E> + 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
Expand All @@ -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<T>
where
T: GenerateRandom,
Expand All @@ -231,6 +260,14 @@ where
fn gen_random(rng: &mut impl Rng, amount: usize) -> Self;
}

#[cfg(all(feature = "genrand", feature = "rayon"))]
pub trait GenerateRandomCollection<T>
where
T: GenerateRandom + Send,
{
fn gen_random(amount: usize) -> Self;
}

#[cfg(not(feature = "rayon"))]
impl<C, T> GenerateRandomCollection<T> for C
where
Expand All @@ -248,13 +285,13 @@ where
#[cfg(feature = "rayon")]
impl<C, T> GenerateRandomCollection<T> for C
where
C: FromIterator<T>,
T: GenerateRandom,
C: FromParallelIterator<T>,
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()
}
}
Expand Down Expand Up @@ -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();
Expand All @@ -314,6 +352,7 @@ mod tests {
dbg!(sim.entities);
}

#[cfg(not(feature = "rayon"))]
#[test]
fn d_prune() {
let mut rng = rand::thread_rng();
Expand All @@ -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);
}
}