petri_dish_lab 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.DS_Store +0 -0
- data/.rspec +3 -0
- data/.standard.yml +2 -0
- data/CHANGELOG.md +19 -0
- data/CODE_OF_CONDUCT.md +27 -0
- data/LICENSE.txt +21 -0
- data/README.md +233 -0
- data/Rakefile +10 -0
- data/examples/lazy_dog_example.rb +62 -0
- data/examples/salesperson_example.rb +135 -0
- data/lib/petri_dish/configuration.rb +125 -0
- data/lib/petri_dish/member.rb +18 -0
- data/lib/petri_dish/metadata.rb +40 -0
- data/lib/petri_dish/version.rb +5 -0
- data/lib/petri_dish/world.rb +66 -0
- data/lib/petri_dish.rb +12 -0
- data/sig/petri_dish.rbs +4 -0
- metadata +121 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 2b6825c1a801fb7bf1b0055bf7ad08eeafb11b07ba28738e119649e78da901e5
|
4
|
+
data.tar.gz: 8b4ec7f7f3aecf5dfe436b17e4fe1fc47cdbce4414cbfd0762a91f229787f7d6
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: f606acc337b9e3854a4d5f5fd97f322ab92819352d42ae5ad96904600451a31de000649cfc3db816fb111fa0146a3a0eeec58ebd8cc22e796450f048fbf9f08c
|
7
|
+
data.tar.gz: ce04833309b18e813f8ded79dc9d6d73af440754dc802045e4a4ed892d0c2c83843952491cf6474690f4a389c51998b6abab4a9705acd9fd71839c296e300824
|
data/.DS_Store
ADDED
Binary file
|
data/.rspec
ADDED
data/.standard.yml
ADDED
data/CHANGELOG.md
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
## [Unreleased]
|
2
|
+
### Added
|
3
|
+
### Changed
|
4
|
+
### Fixed
|
5
|
+
### Removed
|
6
|
+
|
7
|
+
## [0.1.1] - 2023-07-26
|
8
|
+
|
9
|
+
### Added
|
10
|
+
|
11
|
+
- Added `Configuration` class for customizing the parameters of the evolutionary algorithm.
|
12
|
+
- Added `Member` class to represent an individual in the population.
|
13
|
+
- Added `Metadata` class to keep track of the evolution process.
|
14
|
+
- Added `World` class to run the evolutionary algorithm.
|
15
|
+
- Initial implementation of evolutionary algorithm operations, including selection, crossover, and mutation.
|
16
|
+
- Added fitness function support for evaluating the quality of individuals in the population.
|
17
|
+
- Added callback functions for various events in the evolution process, including when a new highest fitness is found, when the maximum number of generations is reached, and when the end condition is met.
|
18
|
+
- Included an example of using the library to solve a simple genetic algorithm problem (lazy dog example).
|
19
|
+
- Included an example of using the library to solve the Traveling Salesperson Problem (salesperson example).
|
data/CODE_OF_CONDUCT.md
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# Code of Conduct
|
2
|
+
|
3
|
+
Hey there, it's Thomas! I'm really keen on keeping this project fun and inclusive. Everyone's welcome, no matter who you are or where you come from.
|
4
|
+
|
5
|
+
## Let's Have Fun
|
6
|
+
|
7
|
+
* Be kind and empathetic.
|
8
|
+
* Respect other's opinions and viewpoints.
|
9
|
+
* Learn from your mistakes, and most importantly, from each other.
|
10
|
+
* Focus on what's best for everyone.
|
11
|
+
|
12
|
+
## Not Fun
|
13
|
+
|
14
|
+
* No inappropriate language or imagery.
|
15
|
+
* No personal attacks or offensive behavior.
|
16
|
+
* No harassment, public or private.
|
17
|
+
* No publishing others' private info without permission.
|
18
|
+
|
19
|
+
## If Things Are Not Fun
|
20
|
+
|
21
|
+
If you see or experience something that's not fun, drop me an email at thomascountz@gmail.com. I'll take it seriously, and handle it respectfully and fairly.
|
22
|
+
|
23
|
+
Depending on what happened, I might send a warning, or for repeated or serious incidents, issue a temporary or even permanent ban from the project.
|
24
|
+
|
25
|
+
Let's keep it fun, respectful, and awesome!
|
26
|
+
|
27
|
+
This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org/version/2/0/code_of_conduct.html), version 2.0, and inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2023 Thomas Countz
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,233 @@
|
|
1
|
+
# Petri Dish - A Ruby library for Evolutionary Algorithms
|
2
|
+
|
3
|
+
> **Note**
|
4
|
+
> `gem install petri_dish_lab` to install the gem, _not_ `petri_dish`.
|
5
|
+
|
6
|
+
## Introduction
|
7
|
+
|
8
|
+
Welcome to Petri Dish, a Ruby library designed to provide an easy-to-use interface for implementing evolutionary algorithms. Petri Dish is a flexible library that allows you to configure and run your own evolutionary algorithms by simply providing your own genetic material, fitness function, and other parameters. This library is perfect for both beginners who are just starting to learn about evolutionary algorithms, and experts who want to experiment with different configurations and parameters.
|
9
|
+
|
10
|
+
## Overview of Evolutionary Algorithms
|
11
|
+
|
12
|
+
Evolutionary algorithms are a class of optimization algorithms that are inspired by the process of natural evolution. They work by maintaining a population of candidate solutions for the problem at hand and iteratively improving that population by applying operations that mimic natural evolution, such as mutation, crossover (or recombination), and selection.
|
13
|
+
|
14
|
+
The basic steps of an evolutionary algorithm are as follows:
|
15
|
+
|
16
|
+
1. **Initialization**: Begin with a population of randomly generated individuals.
|
17
|
+
2. **Evaluation**: Compute the fitness of each individual in the population.
|
18
|
+
3. **Selection**: Select individuals for reproduction based on their fitness.
|
19
|
+
4. **Crossover**: Generate offspring by combining the traits of selected individuals.
|
20
|
+
5. **Mutation**: Randomly alter some traits of the offspring.
|
21
|
+
6. **Replacement**: Replace the current population with the offspring.
|
22
|
+
7. **Termination**: If a termination condition is met (e.g., a solution of sufficient quality is found, or a maximum number of generations is reached), stop and return the best solution found. Otherwise, go back to step 2.
|
23
|
+
|
24
|
+
## Key Concepts of this Library
|
25
|
+
|
26
|
+
Petri Dish is built around a few key classes: `Configuration`, `Member`, `Metadata`, and `World`.
|
27
|
+
|
28
|
+
- `Configuration`: This class provides a way to configure the behavior of the evolutionary algorithm. It exposes various parameters like `population_size`, `mutation_rate`, `genetic_material`, and several callback functions that can be used to customize the evolution process.
|
29
|
+
|
30
|
+
- `Member`: This class represents an individual in the population. It has a set of genes and a fitness value, which is computed by a fitness function provided in the configuration.
|
31
|
+
|
32
|
+
- `Metadata`: This class keeps track of the evolution process, like the number of generations that have passed and the highest fitness value found so far.
|
33
|
+
|
34
|
+
- `World`: This class is responsible for running the evolutionary algorithm. It takes a configuration and a population of members as input, and runs the evolution process until a termination condition is met.
|
35
|
+
|
36
|
+
## Configuration
|
37
|
+
|
38
|
+
The `Configuration` class in Petri Dish allows you to customize various aspects of the evolutionary algorithm. Here are the parameters you can set:
|
39
|
+
|
40
|
+
Here is the reformatted list as a markdown table:
|
41
|
+
|
42
|
+
| Parameter | Description | Type |
|
43
|
+
|---|---|---|
|
44
|
+
| `logger` | An object that responds to `:info` for logging purposes | `Logger` |
|
45
|
+
| `population_size` | The number of individuals in the population | `Integer` |
|
46
|
+
| `mutation_rate` | The chance that a gene will change during mutation (between 0 and 1, inclusive) | `Float` |
|
47
|
+
| `genetic_material` | An array of possible gene values | `Array[untyped]` |
|
48
|
+
| `elitism_rate` | The proportion of the population preserved through elitism (between 0 and 1, inclusive) | `Float` |
|
49
|
+
| `target_genes` | The ideal set of genes for the problem at hand | `Array[untyped]` |
|
50
|
+
| `max_generations` | The maximum number of generations to run the evolution for | `Integer` |
|
51
|
+
| `parents_selection_function` | A function used to select parents for crossover | `Proc[Array[Member], Array[Member]]` |
|
52
|
+
| `crossover_function` | A function used to perform crossover between two parents | `Proc[Array[Member], Member]` |
|
53
|
+
| `mutation_function` | A function used to mutate the genes of an individual | `Proc[Member, Member]` |
|
54
|
+
| `fitness_function` | A function used to calculate the fitness of an individual | `Proc[Member, Numeric]` |
|
55
|
+
| `highest_fitness_callback` | A callback function invoked when a new highest fitness is found | `Proc[Member, void]` |
|
56
|
+
| `max_generation_reached_callback` | A callback function invoked when the maximum number of generations is reached | `Proc[void, void]` |
|
57
|
+
| `end_condition_function` | A function that determines whether the evolution process should stop premature of `max_generations` | `Proc[Member, bool]` |
|
58
|
+
| `next_generation_callback` | A callback function invoked at the start of each new generation | `Proc[void, void]` |
|
59
|
+
| `end_condition_reached_callback` | A callback function invoked when the end condition is met. It is called with the `Member` which triggered the `end_condition_function` | `Proc[Member, void]` |
|
60
|
+
|
61
|
+
You can create a new `Configuration` object by calling `Configuration.configure` and providing a block:
|
62
|
+
|
63
|
+
```ruby
|
64
|
+
configuration = PetriDish::Configuration.configure do |config|
|
65
|
+
# set your configuration parameters here
|
66
|
+
end
|
67
|
+
```
|
68
|
+
|
69
|
+
In the block, you can set the parameters of the configuration to customize the behavior of your evolutionary algorithm.
|
70
|
+
|
71
|
+
## Member
|
72
|
+
|
73
|
+
The `Member` class in Petri Dish represents an individual in the population. Each member has a set of genes and a fitness value, which is calculated by a fitness function provided in the configuration. Here are the parameters and methods you can interact with:
|
74
|
+
|
75
|
+
- `new(genes:, fitness_function:)`: This method is used to create a new member. It takes an array of genes and a fitness function as arguments.
|
76
|
+
|
77
|
+
- `genes` (`Array[untyped]`): The genetic material of the individual, represented as an array .
|
78
|
+
|
79
|
+
- `fitness_function` (`Proc[Member, Float]`): The function used to calculate the fitness of the individual. It is provided during the initialization of the member.
|
80
|
+
|
81
|
+
- `fitness`: This method calls the provided fitness function. The resulting fitness value is cached after the first calculation and reused in subsequent calls.
|
82
|
+
|
83
|
+
Here's an example of how to create a new member:
|
84
|
+
|
85
|
+
```ruby
|
86
|
+
member = PetriDish::Member.new(
|
87
|
+
genes: ["gene1", "gene2", "gene3"],
|
88
|
+
fitness_function: ->(member) { # calculate fitness }
|
89
|
+
)
|
90
|
+
```
|
91
|
+
|
92
|
+
In this example, `["gene1", "gene2", "gene3"]` is the genetic material for the member, and the lambda function is used to calculate the fitness of the member. You should replace `# calculate fitness` with the actual logic for calculating fitness based on the problem you're trying to solve.
|
93
|
+
|
94
|
+
## Fitness Function
|
95
|
+
|
96
|
+
A fitness function is crucial as it provides a way to evaluate how good or "fit" an individual member of the population is in solving the problem at hand. The fitness function is a measure of quality or performance, and it guides the evolutionary algorithm in the search for optimal solutions.
|
97
|
+
|
98
|
+
Here are the necessary technical properties required when defining a fitness function for the Petri Dish framework:
|
99
|
+
|
100
|
+
1. Callable: The fitness function should be a callable object (for example, a lambda or a `Proc`). It should respond to `#call`.
|
101
|
+
|
102
|
+
2. Input: The fitness function should take a single argument, which is an instance of the `Member` class. This represents an individual member of the population whose fitness is to be evaluated.
|
103
|
+
|
104
|
+
3. Output: The fitness function should return a numerical value that represents the fitness of the given member. This could be an `Integer` or a `Float`, depending on the precision required. **Higher values should signify better fitness**.
|
105
|
+
|
106
|
+
4. Deterministic: Given the same `Member`, the fitness function should always return the same fitness score. This is because the fitness of a member may be evaluated multiple times during the evolutionary process, and inconsistent results could lead to unpredictable behavior.
|
107
|
+
|
108
|
+
5. Non-negative: The fitness function should ideally return non-negative values. This isn't a strict requirement, but having non-negative fitness values can make the algorithm easier to understand and debug.
|
109
|
+
|
110
|
+
6. Discriminative: The fitness function should be able to discriminate between different members of the population. That is, members with different genes should have different fitness scores. If many members have the same fitness score, the evolutionary algorithm will have a harder time deciding which members are better.
|
111
|
+
|
112
|
+
## Install and Setup
|
113
|
+
|
114
|
+
> **Warning**
|
115
|
+
> The name of the _repo_ is `petri_dish`.
|
116
|
+
> The name of the _module_ is `PetriDish`.
|
117
|
+
> The name of the _gem_ is `petri_dish_lab`.
|
118
|
+
|
119
|
+
You can install `petri_dish_lab` as a gem in your application. Add this line to your application's Gemfile:
|
120
|
+
|
121
|
+
```ruby
|
122
|
+
gem 'petri_dish_lab'
|
123
|
+
```
|
124
|
+
|
125
|
+
And then execute:
|
126
|
+
|
127
|
+
```bash
|
128
|
+
bundle install
|
129
|
+
```
|
130
|
+
|
131
|
+
Or install it yourself as:
|
132
|
+
|
133
|
+
```bash
|
134
|
+
gem install petri_dish_lab
|
135
|
+
```
|
136
|
+
|
137
|
+
At the top of your Ruby file, require the `petri_dish` _module_ name:
|
138
|
+
|
139
|
+
```ruby
|
140
|
+
require "petri_dish"
|
141
|
+
```
|
142
|
+
|
143
|
+
### Setup for Development
|
144
|
+
|
145
|
+
If you want to set up the `petri_dish_lab` gem for development, follow these steps:
|
146
|
+
|
147
|
+
1. Clone the repository:
|
148
|
+
|
149
|
+
```bash
|
150
|
+
git clone https://github.com/thomascountz/petri_dish.git
|
151
|
+
```
|
152
|
+
|
153
|
+
2. Change into the `petri_dish` directory:
|
154
|
+
|
155
|
+
```bash
|
156
|
+
cd petri_dish
|
157
|
+
```
|
158
|
+
|
159
|
+
3. Run the setup script:
|
160
|
+
|
161
|
+
```bash
|
162
|
+
bin/setup
|
163
|
+
```
|
164
|
+
|
165
|
+
This will install the necessary dependencies for development and testing.
|
166
|
+
|
167
|
+
### Using Console for Development
|
168
|
+
|
169
|
+
After setting up, you can use the development console to experiment with the `petri_dish` library:
|
170
|
+
|
171
|
+
```bash
|
172
|
+
bin/console
|
173
|
+
```
|
174
|
+
|
175
|
+
This will start an interactive Ruby session (IRB) with `PetriDish` pre-loaded. You can use this console to experiment with `PetriDish`, create `PetriDish::Member` instances, run evolutionary algorithms, etc.
|
176
|
+
|
177
|
+
Remember to run your tests frequently during development to ensure everything is working as expected:
|
178
|
+
|
179
|
+
```bash
|
180
|
+
bundle exec rspec
|
181
|
+
```
|
182
|
+
|
183
|
+
If you add new code, remember to add corresponding tests and ensure all tests pass before committing your changes.
|
184
|
+
|
185
|
+
## Examples
|
186
|
+
|
187
|
+
### Lazy Dog Example
|
188
|
+
|
189
|
+
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.
|
190
|
+
|
191
|
+
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.
|
192
|
+
|
193
|
+
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.
|
194
|
+
|
195
|
+
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.
|
196
|
+
|
197
|
+
The end condition for the evolutionary process is when a member with genes exactly matching the target genes is found.
|
198
|
+
|
199
|
+
To run the example, simply execute the following command in your terminal:
|
200
|
+
|
201
|
+
```bash
|
202
|
+
bundle exec ruby examples/lazy_dog_example.rb
|
203
|
+
```
|
204
|
+
|
205
|
+
### Traveling Salesperson Example
|
206
|
+
|
207
|
+
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.
|
208
|
+
|
209
|
+
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.
|
210
|
+
|
211
|
+
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.
|
212
|
+
|
213
|
+
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.
|
214
|
+
|
215
|
+
The evolutionary process runs for a fixed number of generations, and the highest fitness member in each generation is saved to a CSV file.
|
216
|
+
|
217
|
+
To run the example, simply execute the following command in your terminal:
|
218
|
+
|
219
|
+
```bash
|
220
|
+
bundle exec ruby examples/salesperson_example.rb
|
221
|
+
```
|
222
|
+
|
223
|
+
You can then visualize the best route using the provided `uplot` command.
|
224
|
+
|
225
|
+
## Resources
|
226
|
+
- [Genetic Algorithms Explained By Example - Youtube](https://www.youtube.com/watch?v=uQj5UNhCPuo)
|
227
|
+
- [Genetic Algorithms for Autonomous Robot Navigation - Paper](http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.208.9941&rep=rep1&type=pdf)
|
228
|
+
- [Nature of Code, Chapter 9 - The Evolution of Code - Book](https://natureofcode.com/book/chapter-9-the-evolution-of-code/)
|
229
|
+
- [Weighted Random Sampling in Ruby - Gist](https://gist.github.com/O-I/3e0654509dd8057b539a)
|
230
|
+
- [Tail Call Optimization in Ruby - Blog](https://nithinbekal.com/posts/ruby-tco/)
|
231
|
+
- [Neural network and genetic algorithm based global path planning in a static environment - Paper](http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.583.3340&rep=rep1&type=pdf)
|
232
|
+
- [Traveling Salesman Problem using Genetic Algorithm - Blog](https://www.geeksforgeeks.org/traveling-salesman-problem-using-genetic-algorithm/)
|
233
|
+
- [A KNOWLEDGE-BASED GENETIC ALGORITHM FOR PATH PLANNING OF MOBILE ROBOTS - Thesis](https://atrium.lib.uoguelph.ca/xmlui/bitstream/handle/10214/22039/Hu_Yanrong_MSc.pdf?sequence=2)
|
data/Rakefile
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
# require "petri_dish" # Uncomment this line and comment/remove the line below if you're using Petri Dish as a gem
|
2
|
+
require_relative "../lib/petri_dish"
|
3
|
+
|
4
|
+
target_genes = "the quick brown fox jumped over the lazy white dog".chars
|
5
|
+
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", " "]
|
6
|
+
|
7
|
+
def genes_match_target_end_condition_function(configuration)
|
8
|
+
->(member) do
|
9
|
+
member.genes == configuration.target_genes
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def twenty_percent_tournament_function(configuration)
|
14
|
+
->(members) do
|
15
|
+
members.sample(configuration.population_size * 0.2).max_by(2) { |member| member.fitness }
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def exponential_fitness_function(configuration)
|
20
|
+
->(member) do
|
21
|
+
member.genes.zip(configuration.target_genes).map do |target_gene, member_gene|
|
22
|
+
(target_gene == member_gene) ? 1 : 0
|
23
|
+
end.sum**3
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def random_midpoint_crossover_function(configuration)
|
28
|
+
->(parents) do
|
29
|
+
midpoint = rand(parents[0].genes.length)
|
30
|
+
PetriDish::Member.new(fitness_function: configuration.fitness_function, genes: parents[0].genes[0...midpoint] + parents[1].genes[midpoint..])
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def random_mutation_function(configuration)
|
35
|
+
->(member) do
|
36
|
+
mutated_genes = member.genes.map do |gene|
|
37
|
+
if rand < configuration.mutation_rate
|
38
|
+
configuration.genetic_material.sample
|
39
|
+
else
|
40
|
+
gene
|
41
|
+
end
|
42
|
+
end
|
43
|
+
PetriDish::Member.new(fitness_function: configuration.fitness_function, genes: mutated_genes)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
configuration = PetriDish::Configuration.configure do |config|
|
48
|
+
config.max_generations = 5000
|
49
|
+
config.population_size = 250
|
50
|
+
config.mutation_rate = 0.005
|
51
|
+
config.genetic_material = genetic_material
|
52
|
+
config.target_genes = target_genes
|
53
|
+
config.parents_selection_function = twenty_percent_tournament_function(config)
|
54
|
+
config.fitness_function = exponential_fitness_function(config)
|
55
|
+
config.crossover_function = random_midpoint_crossover_function(config)
|
56
|
+
config.mutation_function = random_mutation_function(config)
|
57
|
+
config.end_condition_function = genes_match_target_end_condition_function(config)
|
58
|
+
config.highest_fitness_callback = ->(member) { puts "Highest fitness: #{member.fitness} (#{member})" }
|
59
|
+
end
|
60
|
+
|
61
|
+
init_members = Array.new(configuration.population_size) { PetriDish::Member.new(fitness_function: configuration.fitness_function, genes: Array.new(target_genes.size) { genetic_material.sample }) }
|
62
|
+
PetriDish::World.run(configuration: configuration, members: init_members)
|
@@ -0,0 +1,135 @@
|
|
1
|
+
# require "petri_dish" # Uncomment this line and comment/remove the line below if you're using Petri Dish as a gem
|
2
|
+
require_relative "../lib/petri_dish"
|
3
|
+
require "csv"
|
4
|
+
|
5
|
+
XLIMIT = 10
|
6
|
+
YLIMIT = XLIMIT
|
7
|
+
NUM_OF_CITIES = 10
|
8
|
+
GENETIC_MATERIAL = (0..XLIMIT - 1).to_a
|
9
|
+
|
10
|
+
def random_uniq_city_gene_generation
|
11
|
+
result = []
|
12
|
+
until result.size == NUM_OF_CITIES
|
13
|
+
result << Gene.new(
|
14
|
+
x: GENETIC_MATERIAL.sample,
|
15
|
+
y: GENETIC_MATERIAL.sample
|
16
|
+
)
|
17
|
+
result.uniq!
|
18
|
+
end
|
19
|
+
result.shuffle
|
20
|
+
end
|
21
|
+
|
22
|
+
def fitness_function
|
23
|
+
->(member) do
|
24
|
+
city_pairs = []
|
25
|
+
member.genes.each_cons(2) { |cities| city_pairs << cities }
|
26
|
+
# Return to the starting city
|
27
|
+
city_pairs << [member.genes.last, member.genes.first]
|
28
|
+
1.0 / city_pairs.sum { |a, b| a.distance_to(b) }
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def swap_mutation_function(configuration)
|
33
|
+
->(member) do
|
34
|
+
mutated_genes = member.genes.dup
|
35
|
+
if configuration.mutation_rate > rand
|
36
|
+
gene_one_index = rand(mutated_genes.size)
|
37
|
+
gene_two_index = rand(mutated_genes.size)
|
38
|
+
mutated_genes[gene_one_index], mutated_genes[gene_two_index] = mutated_genes[gene_two_index], mutated_genes[gene_one_index]
|
39
|
+
end
|
40
|
+
PetriDish::Member.new(fitness_function: configuration.fitness_function, genes: mutated_genes)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def twenty_percent_tournament(configuration)
|
45
|
+
->(members) do
|
46
|
+
members.sample(configuration.population_size * 0.2).max_by(2) { |member| member.fitness }
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# In ordered crossover, we randomly select a subset of the first
|
51
|
+
# parent string and then fill the remainder of the route with the
|
52
|
+
# genes from the second parent in the order in which they appear,
|
53
|
+
# without duplicating any genes in the selected subset from the
|
54
|
+
# first parent
|
55
|
+
def random_ordered_crossover_function(configuration)
|
56
|
+
->(members) do
|
57
|
+
start_slice_index, end_slice_index = rand(members[0].genes.size), rand(members[0].genes.size)
|
58
|
+
parent1_slice = members[0].genes[start_slice_index...end_slice_index]
|
59
|
+
parent2_contribution = members[1].genes - parent1_slice
|
60
|
+
child_genes = Array.new(members[0].genes.size)
|
61
|
+
child_genes[start_slice_index...end_slice_index] = parent1_slice
|
62
|
+
child_genes.map! { |gene| gene.nil? ? parent2_contribution.shift : gene }
|
63
|
+
PetriDish::Member.new(fitness_function: configuration.fitness_function, genes: child_genes)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def append_best_member_to_file
|
68
|
+
->(member) do
|
69
|
+
File.open("best_member.txt", "a") do |file|
|
70
|
+
file.puts member.genes.join
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def write_best_member_to_csv
|
76
|
+
->(member) do
|
77
|
+
CSV.open("best_member.csv", "wb") do |csv|
|
78
|
+
csv << ["x", "y"]
|
79
|
+
member.genes.each do |gene|
|
80
|
+
csv << [gene.x, gene.y]
|
81
|
+
end
|
82
|
+
csv << [member.genes.first.x, member.genes.first.y]
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
class Gene
|
88
|
+
attr_reader :x, :y
|
89
|
+
def initialize(x: nil, y: nil)
|
90
|
+
@x = x
|
91
|
+
@y = y
|
92
|
+
end
|
93
|
+
|
94
|
+
def distance_to(other)
|
95
|
+
Math.sqrt((x - other.x)**2 + (y - other.y)**2)
|
96
|
+
end
|
97
|
+
|
98
|
+
def to_s
|
99
|
+
"(#{x}, #{y})"
|
100
|
+
end
|
101
|
+
|
102
|
+
# Override equality methods
|
103
|
+
def ==(other)
|
104
|
+
x == other.x && y == other.y
|
105
|
+
end
|
106
|
+
|
107
|
+
def eql?(other)
|
108
|
+
self == other
|
109
|
+
end
|
110
|
+
|
111
|
+
def hash
|
112
|
+
[x, y].hash
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
configuration = PetriDish::Configuration.configure do |config|
|
117
|
+
config.max_generations = 100
|
118
|
+
config.population_size = 100
|
119
|
+
config.mutation_rate = 0.01
|
120
|
+
config.genetic_material = GENETIC_MATERIAL
|
121
|
+
config.target_genes = random_uniq_city_gene_generation
|
122
|
+
config.mutation_function = swap_mutation_function(config)
|
123
|
+
config.fitness_function = fitness_function
|
124
|
+
config.parents_selection_function = twenty_percent_tournament(config)
|
125
|
+
config.crossover_function = random_ordered_crossover_function(config)
|
126
|
+
config.highest_fitness_callback = write_best_member_to_csv
|
127
|
+
# Rely on number of generations for end condition
|
128
|
+
config.end_condition_function = ->(_member) { false }
|
129
|
+
end
|
130
|
+
|
131
|
+
init_members = Array.new(configuration.population_size) { PetriDish::Member.new(fitness_function: configuration.fitness_function, genes: random_uniq_city_gene_generation) }
|
132
|
+
PetriDish::World.run(configuration: configuration, members: init_members)
|
133
|
+
|
134
|
+
# View CSV with YouPlot (https://github.com/red-data-tools/YouPlot):
|
135
|
+
# ruby examples/salesperson_example.rb && uplot line best_member.csv --canvas dot -h 45 -w 150 -H -d ',' && rm best_member.csv
|
@@ -0,0 +1,125 @@
|
|
1
|
+
module PetriDish
|
2
|
+
class Configuration
|
3
|
+
attr_accessor :logger,
|
4
|
+
:population_size,
|
5
|
+
:mutation_rate,
|
6
|
+
:genetic_material,
|
7
|
+
:elitism_rate,
|
8
|
+
:target_genes,
|
9
|
+
:max_generations,
|
10
|
+
:parents_selection_function,
|
11
|
+
:crossover_function,
|
12
|
+
:mutation_function,
|
13
|
+
:fitness_function,
|
14
|
+
:highest_fitness_callback,
|
15
|
+
:max_generation_reached_callback,
|
16
|
+
:end_condition_function,
|
17
|
+
:next_generation_callback,
|
18
|
+
:end_condition_reached_callback
|
19
|
+
|
20
|
+
def self.configure
|
21
|
+
yield(configuration = new)
|
22
|
+
configuration.validate!
|
23
|
+
configuration
|
24
|
+
end
|
25
|
+
|
26
|
+
def initialize
|
27
|
+
@logger = default_logger
|
28
|
+
@max_generations = default_max_generations
|
29
|
+
@population_size = default_population_size
|
30
|
+
@mutation_rate = default_mutation_rate
|
31
|
+
@elitism_rate = default_elitism_rate
|
32
|
+
@genetic_material = default_genetic_material
|
33
|
+
@target_genes = default_target_genes
|
34
|
+
@fitness_function = default_fitness_function
|
35
|
+
@parents_selection_function = default_parents_selection_function
|
36
|
+
@crossover_function = default_crossover_function
|
37
|
+
@mutation_function = default_mutation_function
|
38
|
+
@highest_fitness_callback = default_highest_fitness_callback
|
39
|
+
@end_condition_function = default_end_condition_function
|
40
|
+
@max_generation_reached_callback = default_max_generation_reached_callback
|
41
|
+
@next_generation_callback = default_next_generation_callback
|
42
|
+
@end_condition_reached_callback = default_end_condition_reached_callback
|
43
|
+
end
|
44
|
+
|
45
|
+
def validate!
|
46
|
+
raise ArgumentError, "logger must respond to :info" unless logger.respond_to?(:info)
|
47
|
+
raise ArgumentError, "max_generations must be greater than 0" unless max_generations > 0
|
48
|
+
raise ArgumentError, "population_size must be greater than 0" unless population_size > 0
|
49
|
+
raise ArgumentError, "mutation_rate must be between 0 and 1" unless mutation_rate >= 0 && mutation_rate <= 1
|
50
|
+
raise ArgumentError, "elitism_rate must be between 0 and 1" unless elitism_rate >= 0 && elitism_rate <= 1
|
51
|
+
raise ArgumentError, "genetic_material must be an Array" unless genetic_material.is_a?(Array)
|
52
|
+
raise ArgumentError, "target_genes must be an Array" unless target_genes.is_a?(Array)
|
53
|
+
raise ArgumentError, "fitness_function must respond to :call" unless fitness_function.respond_to?(:call)
|
54
|
+
raise ArgumentError, "parents_selection_function must respond to :call" unless parents_selection_function.respond_to?(:call)
|
55
|
+
raise ArgumentError, "crossover_function must respond to :call" unless crossover_function.respond_to?(:call)
|
56
|
+
raise ArgumentError, "mutation_function must respond to :call" unless mutation_function.respond_to?(:call)
|
57
|
+
raise ArgumentError, "end_condition_function must respond to :call" unless end_condition_function.respond_to?(:call)
|
58
|
+
raise ArgumentError, "highest_fitness_callback must respond to :call" unless highest_fitness_callback.respond_to?(:call)
|
59
|
+
raise ArgumentError, "max_generation_reached_callback must respond to :call" unless max_generation_reached_callback.respond_to?(:call)
|
60
|
+
raise ArgumentError, "next_generation_callback must respond to :call" unless next_generation_callback.respond_to?(:call)
|
61
|
+
raise ArgumentError, "end_condition_reached_callback must respond to :call" unless end_condition_reached_callback.respond_to?(:call)
|
62
|
+
end
|
63
|
+
|
64
|
+
def reset!
|
65
|
+
@logger = default_logger
|
66
|
+
@max_generations = default_max_generations
|
67
|
+
@population_size = default_population_size
|
68
|
+
@mutation_rate = default_mutation_rate
|
69
|
+
@elitism_rate = default_elitism_rate
|
70
|
+
@genetic_material = default_genetic_material
|
71
|
+
@target_genes = default_target_genes
|
72
|
+
@fitness_function = default_fitness_function
|
73
|
+
@parents_selection_function = default_parents_selection_function
|
74
|
+
@crossover_function = default_crossover_function
|
75
|
+
@mutation_function = default_mutation_function
|
76
|
+
@end_condition_function = default_end_condition_function
|
77
|
+
@highest_fitness_callback = default_highest_fitness_callback
|
78
|
+
@max_generation_reached_callback = default_max_generation_reached_callback
|
79
|
+
@next_generation_callback = default_next_generation_callback
|
80
|
+
@end_condition_reached_callback = default_end_condition_reached_callback
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
|
85
|
+
def default_logger
|
86
|
+
@logger = Logger.new($stdout).tap do |logger|
|
87
|
+
logger.level = Logger::INFO
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def default_max_generations = 1
|
92
|
+
|
93
|
+
def default_population_size = 100
|
94
|
+
|
95
|
+
def default_mutation_rate = 0.005
|
96
|
+
|
97
|
+
def default_elitism_rate = 0.00
|
98
|
+
|
99
|
+
def default_genetic_material = []
|
100
|
+
|
101
|
+
def default_target_genes = nil
|
102
|
+
|
103
|
+
def default_fitness_function = ->(_member) { raise ArgumentError, "fitness_function must be set" }
|
104
|
+
|
105
|
+
def default_parents_selection_function = ->(_members) { raise ArgumentError, "parents_selection_function must be set" }
|
106
|
+
|
107
|
+
def default_crossover_function = ->(_members) { raise ArgumentError, "crossover_function must be set" }
|
108
|
+
|
109
|
+
def default_mutation_function = ->(_member) { raise ArgumentError, "mutation_function must be set" }
|
110
|
+
|
111
|
+
def default_end_condition_function = ->(_member) { false }
|
112
|
+
|
113
|
+
def default_highest_fitness_callback = ->(_member) { :noop }
|
114
|
+
|
115
|
+
# TODO: We might want to consider whether we really want to use `exit` as a
|
116
|
+
# default callback. This will stop the entire Ruby process, which could be
|
117
|
+
# surprising behavior if the user of the library doesn't override these
|
118
|
+
# callbacks.
|
119
|
+
def default_max_generation_reached_callback = -> { exit }
|
120
|
+
|
121
|
+
def default_next_generation_callback = ->(_generation) { :noop }
|
122
|
+
|
123
|
+
def default_end_condition_reached_callback = ->(_member) { exit }
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module PetriDish
|
2
|
+
class Member
|
3
|
+
attr_reader :genes, :fitness_function
|
4
|
+
|
5
|
+
def initialize(genes:, fitness_function:)
|
6
|
+
@fitness_function = fitness_function
|
7
|
+
@genes = genes
|
8
|
+
end
|
9
|
+
|
10
|
+
def fitness
|
11
|
+
@fitness ||= fitness_function.call(self)
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_s
|
15
|
+
genes.join("")
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module PetriDish
|
2
|
+
class Metadata
|
3
|
+
attr_reader :generation_count, :id, :start_time, :highest_fitness, :last_fitness_increase
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@id = SecureRandom.uuid
|
7
|
+
@generation_count = 0
|
8
|
+
@highest_fitness = 0
|
9
|
+
@last_fitness_increase = 0
|
10
|
+
@start_time = nil
|
11
|
+
end
|
12
|
+
|
13
|
+
def start
|
14
|
+
@start_time = Time.now
|
15
|
+
end
|
16
|
+
|
17
|
+
def increment_generation
|
18
|
+
@generation_count += 1
|
19
|
+
end
|
20
|
+
|
21
|
+
def set_highest_fitness(fitness)
|
22
|
+
@highest_fitness = fitness
|
23
|
+
@last_fitness_increase = generation_count
|
24
|
+
end
|
25
|
+
|
26
|
+
def to_h
|
27
|
+
{
|
28
|
+
id: id,
|
29
|
+
generation_count: generation_count,
|
30
|
+
highest_fitness: highest_fitness,
|
31
|
+
elapsed_time: (Time.now - start_time).round(2),
|
32
|
+
last_fitness_increase: last_fitness_increase
|
33
|
+
}
|
34
|
+
end
|
35
|
+
|
36
|
+
def to_json
|
37
|
+
to_h.to_json
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
RubyVM::InstructionSequence.compile_option = {
|
2
|
+
tailcall_optimization: true,
|
3
|
+
trace_instruction: false
|
4
|
+
}
|
5
|
+
|
6
|
+
require "json"
|
7
|
+
require "logger"
|
8
|
+
require "securerandom"
|
9
|
+
|
10
|
+
require_relative "../petri_dish"
|
11
|
+
|
12
|
+
module PetriDish
|
13
|
+
class World
|
14
|
+
class << self
|
15
|
+
attr_accessor :metadata
|
16
|
+
attr_reader :configuration, :end_condition_reached
|
17
|
+
|
18
|
+
def run(
|
19
|
+
members:,
|
20
|
+
configuration: Configuration.new,
|
21
|
+
metadata: Metadata.new
|
22
|
+
)
|
23
|
+
configuration.next_generation_callback.call(metadata.generation_count)
|
24
|
+
|
25
|
+
end_condition_reached = false
|
26
|
+
max_generation_reached = false
|
27
|
+
|
28
|
+
if metadata.generation_count.zero?
|
29
|
+
configuration.logger.info "Run started."
|
30
|
+
metadata.start
|
31
|
+
end
|
32
|
+
|
33
|
+
configuration.logger.info(metadata.to_json)
|
34
|
+
|
35
|
+
if metadata.generation_count >= configuration.max_generations
|
36
|
+
configuration.max_generation_reached_callback.call
|
37
|
+
max_generation_reached = true
|
38
|
+
end
|
39
|
+
|
40
|
+
elitism_count = (configuration.population_size * configuration.elitism_rate).round
|
41
|
+
elite_members = members.sort_by(&:fitness).last(elitism_count)
|
42
|
+
|
43
|
+
new_members = (configuration.population_size - elitism_count).times.map do
|
44
|
+
child_member = configuration.crossover_function.call(configuration.parents_selection_function.call(members))
|
45
|
+
|
46
|
+
configuration.mutation_function.call(child_member).tap do |mutated_child|
|
47
|
+
if metadata.highest_fitness < mutated_child.fitness
|
48
|
+
metadata.set_highest_fitness(mutated_child.fitness)
|
49
|
+
configuration.highest_fitness_callback.call(mutated_child)
|
50
|
+
|
51
|
+
configuration.logger.info(metadata.to_json)
|
52
|
+
end
|
53
|
+
|
54
|
+
if configuration.end_condition_function.call(mutated_child)
|
55
|
+
configuration.end_condition_reached_callback.call(mutated_child)
|
56
|
+
end_condition_reached = true
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
metadata.increment_generation
|
62
|
+
run(members: (new_members + elite_members), configuration: configuration, metadata: metadata) unless end_condition_reached || max_generation_reached
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
data/lib/petri_dish.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "petri_dish/version"
|
4
|
+
require_relative "petri_dish/configuration"
|
5
|
+
require_relative "petri_dish/metadata"
|
6
|
+
require_relative "petri_dish/world"
|
7
|
+
require_relative "petri_dish/member"
|
8
|
+
|
9
|
+
module PetriDish
|
10
|
+
class Error < StandardError; end
|
11
|
+
# Your code goes here...
|
12
|
+
end
|
data/sig/petri_dish.rbs
ADDED
metadata
ADDED
@@ -0,0 +1,121 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: petri_dish_lab
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Thomas Countz
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2023-07-26 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rspec
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: ruby-lsp
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: standardrb
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rake
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
description: Petri Dish allows for various configuration options to let users to customize
|
70
|
+
and experiment with different parameters and functions of the evolutionary algorithm.
|
71
|
+
email:
|
72
|
+
- thomascountz@gmail.com
|
73
|
+
executables: []
|
74
|
+
extensions: []
|
75
|
+
extra_rdoc_files: []
|
76
|
+
files:
|
77
|
+
- ".DS_Store"
|
78
|
+
- ".rspec"
|
79
|
+
- ".standard.yml"
|
80
|
+
- CHANGELOG.md
|
81
|
+
- CODE_OF_CONDUCT.md
|
82
|
+
- LICENSE.txt
|
83
|
+
- README.md
|
84
|
+
- Rakefile
|
85
|
+
- examples/lazy_dog_example.rb
|
86
|
+
- examples/salesperson_example.rb
|
87
|
+
- lib/petri_dish.rb
|
88
|
+
- lib/petri_dish/configuration.rb
|
89
|
+
- lib/petri_dish/member.rb
|
90
|
+
- lib/petri_dish/metadata.rb
|
91
|
+
- lib/petri_dish/version.rb
|
92
|
+
- lib/petri_dish/world.rb
|
93
|
+
- sig/petri_dish.rbs
|
94
|
+
homepage: https://github.com/thomascountz/petri_dish
|
95
|
+
licenses:
|
96
|
+
- MIT
|
97
|
+
metadata:
|
98
|
+
allowed_push_host: https://rubygems.org
|
99
|
+
homepage_uri: https://github.com/thomascountz/petri_dish
|
100
|
+
source_code_uri: https://github.com/thomascountz/petri_dish
|
101
|
+
changelog_uri: https://github.com/thomascountz/petri_dish/CHANGELOG.md
|
102
|
+
post_install_message:
|
103
|
+
rdoc_options: []
|
104
|
+
require_paths:
|
105
|
+
- lib
|
106
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: 2.6.0
|
111
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
112
|
+
requirements:
|
113
|
+
- - ">="
|
114
|
+
- !ruby/object:Gem::Version
|
115
|
+
version: '0'
|
116
|
+
requirements: []
|
117
|
+
rubygems_version: 3.4.17
|
118
|
+
signing_key:
|
119
|
+
specification_version: 4
|
120
|
+
summary: A Ruby library for implementing and experimenting with evolutionary algorithms.
|
121
|
+
test_files: []
|