Skip to content

Commit

Permalink
cleanup examples
Browse files Browse the repository at this point in the history
  • Loading branch information
Thomascountz committed Jul 29, 2023
1 parent 86c5967 commit 1b65115
Show file tree
Hide file tree
Showing 10 changed files with 501 additions and 60 deletions.
61 changes: 3 additions & 58 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,65 +184,10 @@ If you add new code, remember to add corresponding tests and ensure all tests pa

Several example problems are configured and solved in the `/examples` directory.

### Lazy Dog
- [Lazy Dog](examples/lazy_dog/README.md) - Evolving a string to match "the quick brown fox jumped over the lazy white dog"
- [Traveling Salesperson](examples/traveling_salesperson/README.md) - Finding the shortest route that visits each city exactly once
- [Low-Poly Image Reconstruction](examples/low_poly_reconstruction/README.md) - Generating a low-poly representation of an image

The `lazy_dog_example.rb` is an example of using the Petri Dish library to solve a simple problem: Evolving a string to match "the quick brown fox jumped over the lazy white dog". This is a classic example of using a genetic algorithm to find a solution to a problem.

The genetic material in this case is the array of all lowercase letters and space. The target genes are the characters in the target string. The fitness function is defined as the cube of the sum of matches between the genes of a member and the target genes. This means that members with more matching characters will have a much higher fitness.

The parents for crossover are selected using a tournament selection function which picks the best 2 out of a random sample of 20% of the population. Crossover is performed at a random midpoint in the genes.

Mutation is implemented as a chance to replace a gene with a random gene from the genetic material. The mutation rate is set to 0.005, which means that on average, 0.5% of the genes in a member will mutate in each generation.

The end condition for the evolutionary process is when a member with genes exactly matching the target genes is found.

To run the example, simply execute the following command in your terminal:

```bash
bundle exec ruby examples/lazy_dog_example.rb
```

### Traveling Salesperson

The `salesperson_example.rb` is an example of using the Petri Dish library to solve a more complex problem: The Traveling Salesperson Problem. In this problem, a salesperson needs to visit a number of cities, each at a different location, and return to the starting city. The goal is to find the shortest possible route that visits each city exactly once.

In this example, each city is represented as a `Gene` object with `x` and `y` coordinates. The genetic material is the array of all possible `x` and `y` coordinates. The fitness function is defined as the inverse of the total distance of the route, which means that shorter routes will have higher fitness.

The parents for crossover are selected using a tournament selection function which picks the best 2 out of a random sample of 20% of the population. Crossover is performed using an ordered crossover method which maintains the relative order of the genes from both parents.

Mutation is implemented as a chance to swap two genes in a member. The mutation rate is set to 0.01, which means that on average, 1% of the genes in a member will mutate in each generation.

The evolutionary process runs for a fixed number of generations, and the highest fitness member in each generation is saved to a CSV file.

To run the example, simply execute the following command in your terminal:

```bash
bundle exec ruby examples/salesperson_example.rb
```

### Low-Poly Image Reconstruction

The `low_poly_image_reconstruction.rb` script demonstrates a use of the Petri Dish library for generating a low-poly representation of an image. It gives us a glimpse of the potential applications of genetic algorithms in creative digital tasks.

In this script, each member of the population represents a unique rendering of an image. The genetic material, or "genes", for each member are a series of `Point`-s spread across the image, each with its own `x` and `y` coordinates and `grayscale` value. These `Point`-s serve as vertices for triangles, which are created using the Delaunay triangulation algorithm, a method for efficiently generating triangles from a given set of points.

To initialize the image, evenly spaced vertices are selected (though small amount of randomness, or "jitter", is introduced to the point locations to prevent issues with the triangulation algorithm when points align). The `grayscale` value of each vertices is initialized as a random 8-bit number and the final fill value for the triangle is calculated by averaging the grayscale values of its three vertices.

The fitness of each member is then calculated by comparing its generated image with the target image. The closer the resemblance, the higher the fitness. Specifically, a fitness function based on the mean error per pixel is used: the lower the mean error (i.e., the closer the member's image to the target), the higher the fitness score.

To create a "generation" of the population of images, parents are selected using the roulette wheel method, also known as stochastic acceptance. This method works by calculating a "wheel of fortune" where each member of the population gets a slice proportional to their fitness. Then, a random number is generated to select two members from the wheel. This method provides a balance, giving members with higher fitness a better chance of being selected, while still allowing less fit members a shot, helping to allow diversity in the population.

To further maintain diversity in the population, the script uses a high mutation rate of `0.1`. Given the relatively small population size of `50`, and an elitism rate of `0.1` (meaning that 5 highest fitness members are carried over to the next generation unmutated), a high mutation rate helps ensure that there is enough diversity in the gene pool to allow for continual exploration of the search space.

The script is designed to run for a fixed number of generations (2500 in this case), and during crossover, if a new high fitness score has been achieved, the corresponding image is saved to an output directory via the `highest_fitness_callback`. This way, we can track the progress of the algorithm and observe how the images evolve over time.

To set off on this journey yourself, update the `LOW_POLY_RECONSTRUCTION_PATH` and `INPUT_IMAGE_PATH` to point to the working directory and image you want to reconstruct, respectively. Then, run the following command in your terminal:

```bash
bundle exec ruby <PATH_TO_SCRIPT>/low_poly_image_reconstruction.rb
```

Remember, this script requires the RMagick and Delaunator libraries for image processing and triangulation. These will be installed automatically via an inline Gemfile. It reads an input image and saves the progressively evolved images to a specified output directory.

## Resources
- [Genetic Algorithms Explained By Example - Youtube](https://www.youtube.com/watch?v=uQj5UNhCPuo)
Expand Down
64 changes: 64 additions & 0 deletions examples/lazy_dog/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
### Lazy Dog

```
000125 " noq hvqcyuliwenkoocgjjbhzdbkegxtopnukrvbyasmyqdks"
000343 "r rawfdrfmjkzwarduuossipzzioeqhxihezwwqmvewiffvdzi"
000729 "tcqoorqcwwwdmstufymelutnkvyovga mwopl zjfflahrmiey"
001000 "tcqoorqcwwwdmstufymelutngkaovga mwopl zjfflaheovqr"
001331 "tcqoorqcwwwdmstufacljcxzgkaovga mwopl zjfflatwpddv"
001728 "tcqoorqcwwwdmstufymelutnkvyovga mww wzzscdnitwpddv"
002197 "tcqoorqcwwwdmstufymelutnkvyovga mww l zjgf i epdzi"
002744 "tcqoouqcwwwdmstufacljcxzgkaovga mww l zjffnitwpddv"
003375 "tcqoouqcwwwdmwtufymelutnkvyovga mww l zjfflitwpddv"
004096 "tcqoouqcwwwdmstufacljutnkvyovga mww l zscdnitwpddg"
004913 "tcqoouqcwwwdmwtufacljutnkvyovga mww l zjfflitwpddg"
005832 "tcqoouqcwwwdmstufyxelutnedyovga mww l zscdnitwpddg"
006859 "tcqoouqcwwwdmstufyxelutnedyovga mww l zycdnitwpddg"
008000 "tcqoouqcwwwdmwnufoxelutneuyovga mww l zscdnitwpddg"
009261 "tcqoouqcwwwdmwtufoxelutnedyovga mww l zycdnitwpddg"
010648 "tcqoouqcwwwdmwnufyxejutnedyovga mww l zycdnitwpddg"
012167 "tcqoouqcwwwdmwnufoxejutnedyovga mww l zycdnitwpddg"
013824 "tcqoouqcwwwdmwn foxejutnedyovga mww l zycdnitwpddg"
015625 "tcqoouqcwwwdmwn foxejutneddovga mww l zycwnitwpddg"
017576 "tcqoouqcwwwdmwn foxejutneddovga mww l zycwnitw ddg"
019683 "thqoouqcwwwdmwn foxejutneddovga mww l zycwnitw ddg"
021952 "thqoouqcwwwrmwn foxejutnedyovga mww l zycwnitw ddg"
024389 "thqoquqcwwwrmwn foxejutnedyovga mww l zycwnitw ddg"
027000 "thqoquqcwwwrmwn foxejutnedyovga tww l zycwnitw ddg"
029791 "thqoquqcwwarmwn fox jutneddovga tww l zycwnitw ddg"
032768 "thqoquqcwwwrmwn fox jumneddovga tww l zycwnitw ddg"
035937 "thqoquicwwwrmwn fox jumneddovga tww l zycwnitw ddg"
039304 "thqoquicwwwrown fox jumneddovga tww l zycwnitw ddg"
042875 "thqoquicwwwrown fox jumneddovga tww l zycwnitw dog"
046656 "thqoquicwwbrown fox jumneddovga tww l zycwnitw dog"
050653 "thqoquicwwbrown fox jumneddovga tww l zy wnitw dog"
054872 "thqoquicwwbrown fox jumned ovga tww l zy wnitw dog"
059319 "thq quicwwbrown fox jumned ovga tww l zy wnitw dog"
064000 "thq quicwwbrown fox jumned ovea tww l zy wnitw dog"
068921 "thq quicwwbrown fox jumped ovea tww l zy wnitw dog"
074088 "thq quicwwbrown fox jumped over tww l zy wnitw dog"
079507 "thq quicwwbrown fox jumped over tww l zy wnite dog"
085184 "thq quicwwbrown fox jumped over tww lazy wnite dog"
091125 "thq quicwwbrown fox jumped over twe lazy wnite dog"
097336 "the quicwwbrown fox jumped over twe lazy wnite dog"
103823 "the quici brown fox jumped over twe lazy wnite dog"
110592 "the quick brown fox jumped over twe lazy wnite dog"
117649 "the quick brown fox jumped over the lazy wnite dog"
125000 "the quick brown fox jumped over the lazy white dog"
```

The `lazy_dog_example.rb` is an example of using the Petri Dish library to solve a simple problem: Evolving a string to match "the quick brown fox jumped over the lazy white dog". This is a classic example of using a genetic algorithm to find a solution to a problem.

The genetic material in this case is the array of all lowercase letters and space. The target genes are the characters in the target string. The fitness function is defined as the cube of the sum of matches between the genes of a member and the target genes. This means that members with more matching characters will have a much higher fitness.

The parents for crossover are selected using a tournament selection function which picks the best 2 out of a random sample of 20% of the population. Crossover is performed at a random midpoint in the genes.

Mutation is implemented as a chance to replace a gene with a random gene from the genetic material. The mutation rate is set to 0.005, which means that on average, 0.5% of the genes in a member will mutate in each generation.

The end condition for the evolutionary process is when a member with genes exactly matching the target genes is found.

To run the example, simply execute the following command in your terminal:

```bash
bundle exec ruby examples/lazy_dog_example.rb
```
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# require "petri_dish" # Uncomment this line and comment/remove the line below if you're using Petri Dish as a gem
require_relative "../lib/petri_dish"
require_relative "../../lib/petri_dish"

target_genes = "the quick brown fox jumped over the lazy white dog".chars
genetic_material = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", " "]
Expand Down Expand Up @@ -45,6 +45,7 @@ def random_mutation_function(configuration)
end

configuration = PetriDish::Configuration.configure do |config|
config.logger = Logger.new("/dev/null")
config.max_generations = 5000
config.population_size = 250
config.mutation_rate = 0.005
Expand Down
Loading

0 comments on commit 1b65115

Please sign in to comment.