Skip to content

Commit

Permalink
Merge pull request #7 from HyperCodec/dev
Browse files Browse the repository at this point in the history
Update to 0.2.0
  • Loading branch information
HyperCodec committed Jan 26, 2024
2 parents 22f907d + 1e238ed commit 80f9414
Show file tree
Hide file tree
Showing 7 changed files with 164 additions and 78 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/ci-cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
test:
runs-on: ubuntu-latest
environment: testing
name: Run Unit Tests
steps:
- uses: actions/checkout@v4
- name: Run cargo test
run: cargo test --verbose
run: cargo test --verbose --features rayon, crossover
9 changes: 1 addition & 8 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 4 additions & 7 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "genetic-rs"
description = "A small crate for quickstarting genetic algorithm projects"
version = "0.1.1"
version = "0.2.0"
edition = "2021"
authors = ["HyperCodec"]
license = "MIT"
Expand All @@ -14,16 +14,13 @@ categories = ["algorithms", "science", "simulation"]
[features]
default = ["builtin", "genrand"]
builtin = []
crossover = ["dep:rand"]
genrand = ["dep:rand"]
crossover = []
genrand = []
rayon = ["dep:rayon"]

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
replace_with = "0.1.7"
rand = { version = "0.8.5", optional = true }
rand = "0.8.5"
rayon = { version = "1.8.0", optional = true }

[dev-dependencies]
fastrand = "2.0.1"
11 changes: 7 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
# genetic-rs
![Crates.io Total Downloads](https://img.shields.io/crates/d/genetic-rs)
![GitHub deployments](https://img.shields.io/github/deployments/HyperCodec/genetic-rs/testing)

A small crate for quickstarting genetic algorithm projects

### How to Use
Expand All @@ -14,16 +17,16 @@ struct MyEntity {

// required in all of the builtin functions as requirements of `DivsionReproduction` and `CrossoverReproduction`
impl RandomlyMutable for MyEntity {
fn mutate(&mut self, rate: f32) {
self.field1 += fastrand::f32() * rate;
fn mutate(&mut self, rate: f32, rng: &mut impl rand::Rng) {
self.field1 += rng.gen::<f32>() * rate;
}
}

// required for `asexual_pruning_nextgen`.
impl DivsionReproduction for MyEntity {
fn spawn_child(&self) -> Self {
fn spawn_child(&self, rng: &mut impl rand::Rng) -> Self {
let mut child = self.clone();
child.mutate(0.25); // use a constant mutation rate when spawning children in pruning algorithms.
child.mutate(0.25, rng); // use a constant mutation rate when spawning children in pruning algorithms.
child
}
}
Expand Down
28 changes: 24 additions & 4 deletions examples/readme_ex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,16 @@ struct MyEntity {

// required in all of the builtin functions as requirements of `DivisionReproduction` and `CrossoverReproduction`.
impl RandomlyMutable for MyEntity {
fn mutate(&mut self, rate: f32) {
self.field1 += fastrand::f32() * rate;
fn mutate(&mut self, rate: f32, rng: &mut impl rand::Rng) {
self.field1 += rng.gen::<f32>() * rate;
}
}

// required for `division_pruning_nextgen`.
impl DivisionReproduction for MyEntity {
fn spawn_child(&self) -> Self {
fn spawn_child(&self, rng: &mut impl rand::Rng) -> Self {
let mut child = self.clone();
child.mutate(0.25); // use a constant mutation rate when spawning children in pruning algorithms.
child.mutate(0.25, rng); // use a constant mutation rate when spawning children in pruning algorithms.
child
}
}
Expand All @@ -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);
}
63 changes: 35 additions & 28 deletions src/builtin.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
/// Used in all of the builtin [next_gen]s to randomly mutate entities a given amount
pub trait RandomlyMutable {
/// Mutate the entity with a given mutation rate (0..1)
fn mutate(&mut self, rate: f32);
fn mutate(&mut self, rate: f32, rng: &mut impl rand::Rng);
}

/// Used in dividually-reproducing [next_gen]s
pub trait DivisionReproduction: RandomlyMutable {
/// Create a new child with mutation. Similar to [RandomlyMutable::mutate], but returns a new instance instead of modifying the original.
/// If it is simply returning a cloned and mutated version, consider using a constant mutation rate.
fn spawn_child(&self) -> Self;
fn spawn_child(&self, rng: &mut impl rand::Rng) -> Self;
}

/// Used in crossover-reproducing [next_gen]s
#[cfg(feature = "crossover")]
pub trait CrossoverReproduction: RandomlyMutable {
/// Use crossover reproduction to create a new entity.
fn spawn_child(&self, other: &Self) -> Self;
fn spawn_child(&self, other: &Self, rng: &mut impl Rng) -> Self;
}

/// Used in pruning [next_gen]s
Expand All @@ -29,49 +29,57 @@ pub trait Prunable: Sized {
pub mod next_gen {
use super::*;

#[cfg(feature = "crossover")] use rand::prelude::*;
#[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 = StdRng::from_rng(rand::thread_rng()).unwrap();

rewards
.into_iter()
.enumerate()
.map(|(i, (mut e, _))| {
e.mutate(i as f32 / len);
e.mutate(i as f32 / len, &mut rng);
e
})
.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 = StdRng::from_rng(rand::thread_rng()).unwrap();

rewards
.into_par_iter()
.enumerate()
.map(|(i, (mut e, _))| {
e.mutate(i as f32 / len);
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 = 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
.into_iter()
Expand All @@ -80,7 +88,7 @@ pub mod next_gen {
while next_gen.len() < population_size {
let e = og_champions.next().unwrap();

next_gen.push(e.spawn_child());
next_gen.push(e.spawn_child(&mut rng));
}

next_gen
Expand All @@ -89,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 All @@ -105,13 +112,13 @@ pub mod next_gen {

while next_gen.len() < population_size {
let e1 = og_champs_cycle.next().unwrap();
let e2 = og_champions[rand::gen::<usize>(0..og_champions.len()-1)];
let e2 = og_champions[rng.gen::<usize>(0..og_champions.len()-1)];

if !S && e1 == e2 {
continue;
}

next_gen.push(e1.spawn_child(&e2));
next_gen.push(e1.spawn_child(&e2, &mut rng));
}

next_gen
Expand All @@ -137,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

0 comments on commit 80f9414

Please sign in to comment.