Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
bf112e4
implement SpeiatedFitnessEliminator
HyperCodec Mar 9, 2026
b33448e
cargo fmt
HyperCodec Mar 9, 2026
1233e02
create new speciatedcrossoverrepopulator
HyperCodec Mar 9, 2026
a32cd23
doc changes
HyperCodec Mar 9, 2026
c6876ae
write speciation example
HyperCodec Mar 9, 2026
f1a0fe7
cargo fmt
HyperCodec Mar 9, 2026
b3b504f
fix clippy warnigns
HyperCodec Mar 9, 2026
b610ff5
fix rayon feature compile error
HyperCodec Mar 9, 2026
64c5314
cargo fmt
HyperCodec Mar 9, 2026
e135cf1
implement comment fix
HyperCodec Mar 10, 2026
424c477
fix potential edge case + shitty doc comment (AI tab complete fucked …
HyperCodec Mar 10, 2026
eb97a16
fix found robin iterator
HyperCodec Mar 10, 2026
f6ae0f3
fix enumerated round robin
HyperCodec Mar 10, 2026
362de19
Initial plan
Copilot Mar 10, 2026
853ab6c
fix CrossoverRandom affecting new genomes
HyperCodec Mar 10, 2026
820b77f
Fix ActionIfIsolated::DoNothing hang when all species are isolated
Copilot Mar 10, 2026
12340c1
fix doc comment
HyperCodec Mar 10, 2026
2f79778
Merge pull request #145 from HyperCodec/copilot/sub-pr-144
HyperCodec Mar 10, 2026
5bdd52d
Merge pull request #144 from HyperCodec/speciation-rewrite
HyperCodec Mar 10, 2026
d5133cc
create Vec::from_parent qol function
HyperCodec Mar 10, 2026
26a8d69
update doc comment
HyperCodec Mar 10, 2026
a7b8d8f
Initial plan
Copilot Mar 10, 2026
29238ba
Initial plan
Copilot Mar 10, 2026
15a3dd0
Add examples to CI workflow and register missing example metadata in …
Copilot Mar 10, 2026
b1190ff
Add Default, Clone, Debug, PartialEq, Eq trait impls to LayeredObserver
Copilot Mar 10, 2026
a7bfefe
cargo fmt
HyperCodec Mar 10, 2026
abcb120
Merge pull request #147 from HyperCodec/copilot/add-extra-traits-to-l…
HyperCodec Mar 10, 2026
2641263
Add comprehensive integration test suite covering all major invariants
Copilot Mar 10, 2026
4af20c5
cargo fmt
HyperCodec Mar 10, 2026
2fc5ab1
Merge pull request #146 from HyperCodec/copilot/rewrite-tests-code
HyperCodec Mar 10, 2026
14aa772
add a tip for genetic-rs-extras in README
HyperCodec Mar 10, 2026
38de4f1
rewrite crate promotion as ecosystem section
HyperCodec Mar 10, 2026
c464186
fix comment about example
HyperCodec Mar 10, 2026
739bd39
Initial plan
Copilot Mar 10, 2026
be23d93
Initial plan
Copilot Mar 10, 2026
4cff2ff
add an explicit panic for stupid counts in from_parent
HyperCodec Mar 10, 2026
c39fdc0
small change to README
HyperCodec Mar 10, 2026
8e3e085
Change FitnessObserver blanket impl bound from Fn to FnMut
Copilot Mar 10, 2026
5ffff28
Merge pull request #150 from HyperCodec/copilot/sub-pr-148-again
HyperCodec Mar 10, 2026
2cc8185
Restrict SpeciatedPopulation.species to private with accessor to enfo…
Copilot Mar 10, 2026
aeeee01
Merge pull request #149 from HyperCodec/copilot/sub-pr-148
HyperCodec Mar 10, 2026
09dcefe
fix edge case in SpeciatedCrossoverRepopulator
HyperCodec Mar 10, 2026
2bd9439
cargo fmt
HyperCodec Mar 10, 2026
cb71d17
bump version number
HyperCodec Mar 10, 2026
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
18 changes: 18 additions & 0 deletions .github/workflows/ci-cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,24 @@ jobs:
- name: Run Clippy
run: cargo clippy --all-features -- -D warnings

examples:
runs-on: ubuntu-latest
name: Run Examples
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- name: Run readme_ex example
run: cargo run --example readme_ex --features builtin,genrand
- name: Run crossover example
run: cargo run --example crossover --features crossover,genrand
- name: Run knockout example
run: cargo run --example knockout --features knockout,crossover,genrand
- name: Run derive example
run: cargo run --example derive --features derive
- name: Run speciation example
run: cargo run --example speciation --features speciation,genrand

fmt:
runs-on: ubuntu-latest
name: Formatting Checks
Expand Down
6 changes: 3 additions & 3 deletions Cargo.lock

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

6 changes: 5 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@ members = ["genetic-rs", "genetic-rs-common", "genetic-rs-macros"]
resolver = "2"

[workspace.package]
version = "1.3.0"
version = "1.4.0"
authors = ["HyperCodec"]
homepage = "https://github.com/hypercodec/genetic-rs"
repository = "https://github.com/hypercodec/genetic-rs"
license = "MIT"
edition = "2021"

[workspace.dependencies]
genetic-rs-common = { path = "genetic-rs-common", version = "1.4.0", default-features = false }
genetic-rs-macros = { path = "genetic-rs-macros", version = "1.4.0" }
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,14 @@ A small framework for managing genetic algorithms.
### Features
First off, this crate comes with the `builtin`, `genrand`, `crossover`, `knockout`, and `speciation` features by default. If you want the simulation to be parallelized (which is most usecases), add the `rayon` feature. There are also some convenient macros with the `derive` feature.

### How to Use
> [!NOTE]
> If you are interested in implementing NEAT with this, or just want a more complex example, check out the [neat](https://crates.io/crates/neat) crate
### Ecosystem
This framework was created with a high degree of modularity in mind, allowing other crates to contribute to the ecosystem. Here's a list of some good crates:
- [neat](https://crates.io/crates/neat) - Handles complex reproduction and prediction logic for the NEAT algorithm, allowing you to create AI simulations with ease. It also functions as a pretty good example for the more complex usecases of the crate's traits.
- [genetic-rs-extras](https://crates.io/crates/genetic-rs-extras) - A companion crate with quality-of-life improvements and utility features.

If you have a `genetic-rs`-based crate and you'd like it to be featured here, submit a PR or discussion post and I'll consider it.

### How to Use
Here's a simple genetic algorithm:

```rust
Expand Down Expand Up @@ -65,7 +69,7 @@ fn main() {
}
```

That is the minimal code for a working genetic algorithm on default features. You can [read the docs](https://docs.rs/genetic-rs) or [check the examples](/genetic-rs/examples/) for more complicated systems. I highly recommend looking into crossover reproduction, as it tends to produce better results than mitosis.
That is the minimal code for a working genetic algorithm with just the `derive` feature (+ defaults). You can [read the docs](https://docs.rs/genetic-rs) or [check the examples](/genetic-rs/examples/) for more complicated systems. I highly recommend looking into crossover reproduction, as it tends to produce better results than mitosis.

### License
This project falls under the `MIT` license.
211 changes: 208 additions & 3 deletions genetic-rs-common/src/builtin/eliminator.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
use std::ops::Not;

use crate::Eliminator;
use crate::FeatureBoundedGenome;

Expand Down Expand Up @@ -52,6 +50,16 @@ impl<G> FitnessObserver<G> for () {
fn observe(&mut self, _fitnesses: &[(G, f32)]) {}
}

impl<F, G> FitnessObserver<G> for F
where
F: FnMut(&[(G, f32)]),
G: FeatureBoundedGenome,
{
fn observe(&mut self, fitnesses: &[(G, f32)]) {
(self)(fitnesses);
}
}

/// An observer that calls two observers in sequence.
/// Created by [`FitnessObserver::layer`].
pub struct LayeredObserver<G, A: FitnessObserver<G>, B: FitnessObserver<G>>(
Expand All @@ -71,6 +79,56 @@ where
}
}

impl<G, A, B> Default for LayeredObserver<G, A, B>
where
A: Default + FitnessObserver<G>,
B: Default + FitnessObserver<G>,
{
fn default() -> Self {
Self(A::default(), B::default(), std::marker::PhantomData)
}
}

impl<G, A, B> Clone for LayeredObserver<G, A, B>
where
A: Clone + FitnessObserver<G>,
B: Clone + FitnessObserver<G>,
{
fn clone(&self) -> Self {
Self(self.0.clone(), self.1.clone(), std::marker::PhantomData)
}
}

impl<G, A, B> std::fmt::Debug for LayeredObserver<G, A, B>
where
A: std::fmt::Debug + FitnessObserver<G>,
B: std::fmt::Debug + FitnessObserver<G>,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("LayeredObserver")
.field(&self.0)
.field(&self.1)
.finish()
}
}

impl<G, A, B> PartialEq for LayeredObserver<G, A, B>
where
A: PartialEq + FitnessObserver<G>,
B: PartialEq + FitnessObserver<G>,
{
fn eq(&self, other: &Self) -> bool {
self.0 == other.0 && self.1 == other.1
}
}

impl<G, A, B> Eq for LayeredObserver<G, A, B>
where
A: Eq + FitnessObserver<G>,
B: Eq + FitnessObserver<G>,
{
}

#[cfg(not(feature = "rayon"))]
#[doc(hidden)]
pub trait FeatureBoundedFitnessObserver<G: FeatureBoundedGenome>: FitnessObserver<G> {}
Expand Down Expand Up @@ -330,7 +388,7 @@ mod knockout {
}
}

impl Not for KnockoutWinner {
impl std::ops::Not for KnockoutWinner {
type Output = Self;

fn not(self) -> Self::Output {
Expand Down Expand Up @@ -523,3 +581,150 @@ mod knockout {

#[cfg(feature = "knockout")]
pub use knockout::*;

#[cfg(feature = "speciation")]
mod speciation {
use crate::{prelude::*, speciation::SpeciatedPopulation};

/// An eliminator that attempts to preserve new experimental structures by dividing a genome's
/// fitness by the number of genomes in its species.
pub struct SpeciatedFitnessEliminator<
F: FeatureBoundedFitnessFn<G>,
G: Speciated + FeatureBoundedGenome,
O: FeatureBoundedFitnessObserver<G> = (),
> {
/// The divergence threshold used to determine whether a genome belongs in a species.
/// See [`SpeciatedPopulation::threshold`] for more info.
pub speciation_threshold: f32,

/// The inner fitness eliminator used to hold settings and such.
pub inner: FitnessEliminator<F, G, O>,

/// The context used to determine species membership.
/// This is necessary since the eliminator needs to calculate species membership to divide fitness by species size, and some genome types may require context to calculate divergence.
pub ctx: <G as Speciated>::Context,

_marker: std::marker::PhantomData<G>,
}

impl<F, G, O> SpeciatedFitnessEliminator<F, G, O>
where
F: FeatureBoundedFitnessFn<G>,
G: Speciated + FeatureBoundedGenome,
O: FeatureBoundedFitnessObserver<G>,
{
/// Creates a new [`SpeciatedFitnessEliminator`] with a given fitness function and thresholds.
pub fn new(
fitness_fn: F,
speciation_threshold: f32,
keep_threshold: f32,
observer: O,
ctx: <G as Speciated>::Context,
) -> Self {
Self {
speciation_threshold,
inner: FitnessEliminator::new(fitness_fn, keep_threshold, observer),
ctx,
_marker: std::marker::PhantomData,
}
}

/// Creates a new [`SpeciatedFitnessEliminator`] from a regular [`FitnessEliminator`] and a speciation threshold.
/// Useful since you can use the builder for [`FitnessEliminator`] to construct the fitness function and observer, then convert it into a [`SpeciatedFitnessEliminator`] with this method.
pub fn from_fitness_eliminator(
fitness_eliminator: FitnessEliminator<F, G, O>,
speciation_threshold: f32,
ctx: <G as Speciated>::Context,
) -> Self {
Self {
speciation_threshold,
inner: fitness_eliminator,
ctx,
_marker: std::marker::PhantomData,
}
}

/// Calculates the fitness of each genome, dividing by the number of genomes in its species, and sorts them by fitness.
/// Returns a vector of tuples containing the genome and its fitness score.
#[cfg(not(feature = "rayon"))]
pub fn calculate_and_sort(&self, genomes: Vec<G>) -> Vec<(G, f32)> {
let population =
SpeciatedPopulation::from_genomes(&genomes, self.speciation_threshold, &self.ctx);
let mut fitnesses = vec![0.0; genomes.len()];

for species in population.species() {
let len = species.len() as f32;
debug_assert!(len != 0.0);
for &index in species {
let genome = &genomes[index];
let fitness = self.inner.fitness_fn.fitness(genome);
if fitness < 0.0 {
fitnesses[index] = fitness * len;
} else {
fitnesses[index] = fitness / len;
}
}
}

let mut fitnesses: Vec<(G, f32)> = genomes.into_iter().zip(fitnesses).collect();
fitnesses.sort_by(|(_a, afit), (_b, bfit)| bfit.partial_cmp(afit).unwrap());
fitnesses
}

/// Calculates the fitness of each genome, dividing by the number of genomes in its species, and sorts them by fitness.
/// Returns a vector of tuples containing the genome and its fitness score.
#[cfg(feature = "rayon")]
pub fn calculate_and_sort(&self, genomes: Vec<G>) -> Vec<(G, f32)> {
let population =
SpeciatedPopulation::from_genomes(&genomes, self.speciation_threshold, &self.ctx);

let mut fitnesses = vec![0.0; genomes.len()];

for species in population.species() {
let len = species.len() as f32;
debug_assert!(len != 0.0);
for &index in species {
let genome = &genomes[index];
let fitness = self.inner.fitness_fn.fitness(genome);
if fitness < 0.0 {
fitnesses[index] = fitness * len;
} else {
fitnesses[index] = fitness / len;
}
}
}

let mut result: Vec<(G, f32)> = genomes.into_iter().zip(fitnesses).collect();
result.sort_by(|(_a, afit), (_b, bfit)| bfit.partial_cmp(afit).unwrap());
result
}
}

impl<F, G, O> Eliminator<G> for SpeciatedFitnessEliminator<F, G, O>
where
F: FeatureBoundedFitnessFn<G>,
G: Speciated + FeatureBoundedGenome,
O: FeatureBoundedFitnessObserver<G>,
{
#[cfg(not(feature = "rayon"))]
fn eliminate(&mut self, genomes: Vec<G>) -> Vec<G> {
let mut fitnesses = self.calculate_and_sort(genomes);
self.inner.observer.observe(&fitnesses);
let median_index = (fitnesses.len() as f32) * self.inner.threshold;
fitnesses.truncate(median_index as usize + 1);
fitnesses.into_iter().map(|(g, _)| g).collect()
}

#[cfg(feature = "rayon")]
fn eliminate(&mut self, genomes: Vec<G>) -> Vec<G> {
let mut fitnesses = self.calculate_and_sort(genomes);
self.inner.observer.observe(&fitnesses);
let median_index = (fitnesses.len() as f32) * self.inner.threshold;
fitnesses.truncate(median_index as usize + 1);
fitnesses.into_par_iter().map(|(g, _)| g).collect()
}
}
}

#[cfg(feature = "speciation")]
pub use speciation::*;
Loading