1 unstable release

0.1.0 Apr 29, 2020

#2431 in Algorithms

Custom license

11KB
57 lines

Ecosystem

A small genetic algorithms library, written in Rust.

To use this library in your project, just add the following line to its Cargo.toml dependencies:

ecosystem = "0.1"

Note that the following usage instructions assume a basic understanding of genetic algorithms. If you don't know anything about the topic, but wish to learn more, I can recommend the beginner-friendly video series by The Coding Train, which you can find here.

Fundamentals

This library is based around two key components: organisms and ecosystems.

Organisms

Implementing the Organism trait on any type will allow it to be a part of an Ecosystem, thus enabling genetic functionality. It requires you to fill in three methods:

  1. fitness: this should return an indicator of how well the organism is performing the task that it has been set
  2. breed: this should return a child organism with a mix of both parents' attributes ('genetic material')
  3. mutate: this should randomly modify the organism, by an amount determined by the rate parameter

An example usage of this trait is covered in the walkthrough example.

Ecosystems

An Ecosystem is simply a group of Organisms that contains functionality for the breeding of new generations.

You can create one like so:

use ecosystem::Ecosystem;

// `your_organisms` must be a vector of items which implement
// the `Organism` trait
let mut ecosystem = Ecosystem::new(your_organisms);

You can breed a new generation of organisms by calling the breed_next_generation method. This will overwrite the existing organisms, meaning that the population count will always stay the same.

// The only argument passed to the method is the mutation rate,
// a floating-point value that describes the extent to which the
// new organisms should be mutated (randomly modified)
ecosystem.breed_next_generation(0.1);

As it's often beneficial to know which organism in an Ecosystem has the highest fitness, they also include the helper method fittest:

let the_best = ecosystem.fittest();

The next section walks through a simple example scenario in which these fundamentals are put to practical use.

Walkthrough example

This section will cover how to build an ecosystem of organisms that attempt to approximate the value of π.

Building the foundations

There are two things we need to set up before we start adding genetic functionality.

Firstly, we need some structure to represent a 'π approximator', which will simply hold the value of its guess:

struct PiApproximator {
    value: f64,
}

And secondly, we need to create a vector holding a number of these approximators, which we can do using a simple iterator:

use rand::Rng;

const POPULATION_COUNT: u32 = 10;
const MAX_INITIAL_VALUE: f64 = 10.0;

fn main() {
    let mut rng = rand::thread_rng();
    let approximators: Vec<PiApproximator> = (0..POPULATION_COUNT)
        .map(|_| PiApproximator {
            value: rng.gen_range(-MAX_INITIAL_VALUE, MAX_INITIAL_VALUE),
        })
        .collect();
}

Adding genetic functionality

The first thing we must do in order to add genetic functionality is to implement the Organism trait on the PiApproximator structure, which requires us to fill in the fitness, breed, and mutate methods.

use ecosystem::Organism;

impl Organism for PiApproximator {
    fn fitness(&self) -> f64 { ... }

    fn breed(&self, other: &Self) -> Self { ... }

    fn mutate(&mut self, rate: f64) { ... }
}

Let's go through each of these in order.

One way of calculating the fitness of an approximator is to take the reciprocal of the difference between its guess and the real value of π. This means that the closer an approximator's guess is, the higher its resulting fitness will be.

fn fitness(&self) -> f64 {
    let diff = (std::f64::consts::PI - self.value).abs();
    1.0 / diff
}

Breeding approximators is even simpler: we can just take the average of the two parents' values:

fn breed(&self, other: &Self) -> Self {
    Self {
        value: (self.value + other.value) / 2.0,
    }
}

Finally, we can mutate an approximator by randomly shifting its value up or down. The higher the rate, the greater the potential change.

fn mutate(&mut self, rate: f64) {
    let change = rand::thread_rng().gen_range(-rate, rate);
    self.value += change;
}

To put it all together, let's build an Ecosystem out of the approximators we created earlier:

use ecosystem::Ecosystem;

fn main() {
    ...

    let mut ecosystem = Ecosystem::new(approximators);
}

Finally, we can set up a loop that will run for a certain number of generations, printing the guess of the best approximator in each:

const GENERATIONS: u32 = 50;
const MUTATION_RATE: f64 = 0.1;

fn main() {
    ...

    for _ in 0..GENERATIONS {
        ecosystem.breed_next_generation(MUTATION_RATE);
        println!("{}", ecosystem.fittest().value);
    }
}

That's it! Try playing around with the constants to see what different results you can get.

The full code for the walkthrough is in the examples folder of this repository.

Dependencies

~2MB
~35K SLoC