evolvable 1.0.0 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.yardopts +4 -0
- data/CHANGELOG.md +56 -1
- data/Gemfile +3 -0
- data/Gemfile.lock +38 -21
- data/LICENSE +21 -0
- data/README.md +234 -161
- data/README_YARD.md +237 -0
- data/bin/console +18 -5
- data/evolvable.gemspec +2 -2
- data/examples/ascii_art.rb +62 -0
- data/examples/ascii_gene.rb +9 -0
- data/examples/hello_world.rb +91 -0
- data/examples/images/diagram.png +0 -0
- data/examples/stickman.rb +77 -0
- data/exe/hello +16 -0
- data/lib/evolvable/count_gene.rb +42 -0
- data/lib/evolvable/equalize_goal.rb +29 -0
- data/lib/evolvable/evaluation.rb +29 -6
- data/lib/evolvable/evolution.rb +40 -8
- data/lib/evolvable/gene.rb +54 -2
- data/lib/evolvable/gene_combination.rb +73 -0
- data/lib/evolvable/genome.rb +86 -0
- data/lib/evolvable/goal.rb +36 -3
- data/lib/evolvable/maximize_goal.rb +30 -0
- data/lib/evolvable/minimize_goal.rb +29 -0
- data/lib/evolvable/mutation.rb +66 -15
- data/lib/evolvable/point_crossover.rb +33 -19
- data/lib/evolvable/population.rb +171 -31
- data/lib/evolvable/rigid_count_gene.rb +17 -0
- data/lib/evolvable/search_space.rb +181 -0
- data/lib/evolvable/selection.rb +28 -1
- data/lib/evolvable/serializer.rb +21 -0
- data/lib/evolvable/uniform_crossover.rb +28 -8
- data/lib/evolvable/version.rb +1 -1
- data/lib/evolvable.rb +197 -29
- metadata +38 -27
- data/.rubocop.yml +0 -20
- data/examples/evolvable_string/char_gene.rb +0 -9
- data/examples/evolvable_string.rb +0 -32
- data/lib/evolvable/gene_crossover.rb +0 -28
- data/lib/evolvable/gene_space.rb +0 -37
- data/lib/evolvable/goal/equalize.rb +0 -19
- data/lib/evolvable/goal/maximize.rb +0 -19
- data/lib/evolvable/goal/minimize.rb +0 -19
data/README_YARD.md
ADDED
@@ -0,0 +1,237 @@
|
|
1
|
+
# Evolvable 🦎
|
2
|
+
|
3
|
+
[![Gem Version](https://badge.fury.io/rb/evolvable.svg)](https://badge.fury.io/rb/evolvable) [![Maintainability](https://api.codeclimate.com/v1/badges/7faf84a6d467718b33c0/maintainability)](https://codeclimate.com/github/mattruzicka/evolvable/maintainability)
|
4
|
+
|
5
|
+
An evolutionary framework for writing programs that use operations such as selection, crossover, and mutation. Explore ideas generatively in any domain, discover novel solutions to complex problems, and build intuitions about intelligence, complexity, and the natural world.
|
6
|
+
|
7
|
+
Subscribe to the [Evolvable Newsletter](https://www.evolvable.site/newsletter) to slowly learn more, or keep reading this contextualization of the [full documentation](https://rubydoc.info/github/mattruzicka/evolvable).
|
8
|
+
|
9
|
+
|
10
|
+
## Table of Contents
|
11
|
+
* [Installation](#installation)
|
12
|
+
* [Getting Started](#getting-started)
|
13
|
+
* [Concepts](#concepts)
|
14
|
+
* [Genes](#genes)
|
15
|
+
* [Populations](#populations)
|
16
|
+
* [Evaluation](#evaluation)
|
17
|
+
* [Evolution](#evolution)
|
18
|
+
* [Selection](#selection)
|
19
|
+
* [Combination](#combination)
|
20
|
+
* [Mutation](#mutation)
|
21
|
+
* [Search Space](#search-space)
|
22
|
+
|
23
|
+
|
24
|
+
## Installation
|
25
|
+
|
26
|
+
Add [gem "evolvable"](https://rubygems.org/gems/evolvable) to your Gemfile and run `bundle install` or install it yourself with: `gem install evolvable`
|
27
|
+
|
28
|
+
## Getting Started
|
29
|
+
|
30
|
+
{@readme Evolvable}
|
31
|
+
|
32
|
+
To demonstrate these steps, we'll look at the [Hello World](#) example program.
|
33
|
+
|
34
|
+
### Hello World
|
35
|
+
|
36
|
+
Let's build the evolvable hello world program using the above steps. It'll evolve a population of arbitrary strings to be more like a given target string. After installing this gem, run `evolvable hello` at the command line to see it in action.
|
37
|
+
|
38
|
+
Below is example output from evolving a population of randomly initialized string objects to match "Hello World!", then "Hello Evolvable World".
|
39
|
+
|
40
|
+
```
|
41
|
+
❯ Enter a string to evolve: Hello World!
|
42
|
+
|
43
|
+
pp`W^jXG'_N`% Generation 0
|
44
|
+
H-OQXZ\a~{H* Generation 1 ...
|
45
|
+
HRv9X WorlNi Generation 50 ...
|
46
|
+
HRl6W World# Generation 100 ...
|
47
|
+
Hello World! Generation 165
|
48
|
+
|
49
|
+
❯ Enter a string to evolve: Hello Evolvable World
|
50
|
+
|
51
|
+
Helgo World!b+=1}3 Generation 165
|
52
|
+
Helgo Worlv!}:c(SoV Generation 166
|
53
|
+
Helgo WorlvsC`X(Joqs Generation 167
|
54
|
+
Helgo WorlvsC`X(So1RE Generation 168 ...
|
55
|
+
Hello Evolv#"l{ Wor*5 Generation 300 ...
|
56
|
+
Hello Evolvable World Generation 388
|
57
|
+
```
|
58
|
+
|
59
|
+
### Step 1
|
60
|
+
|
61
|
+
Let's begin by defining a `HelloWorld` class and have it **include the `Evolvable` module**.
|
62
|
+
|
63
|
+
```ruby
|
64
|
+
class HelloWorld
|
65
|
+
include Evolvable
|
66
|
+
end
|
67
|
+
```
|
68
|
+
|
69
|
+
### Step 2
|
70
|
+
|
71
|
+
Now we can **define the `.search_space`** class method with the types of [genes](#genes) that we want our our evolvable "hello world" instances to be able to have. We'll use `CharGene` instances to represent single characters within strings. So an instance with the string value of "Hello" would be composed of five `CharGene` instances.
|
72
|
+
|
73
|
+
```ruby
|
74
|
+
class HelloWorld
|
75
|
+
include Evolvable
|
76
|
+
|
77
|
+
def self.search_space
|
78
|
+
["CharGene", 1..40]
|
79
|
+
end
|
80
|
+
end
|
81
|
+
```
|
82
|
+
|
83
|
+
The [Search Space](#search-space) can be defined in a variety of ways. The above is shorthand that's useful for when there's only one type of gene. This method can also return an array of arrays or hash.
|
84
|
+
|
85
|
+
The `1..40` specifies the range of possible genes for a particular HelloWorld instance. Evolvable translates this range or integer value into a `Evolvable::CountGene` object.
|
86
|
+
|
87
|
+
By specifying a range, an `Evolvable::CountGene` instance can change the number of genes that are present in an evovlable instance. Count genes undergo evolutionary operations like any other gene. Their effects can be seen in the letter changes from Generation 165 to 168 in the above example output.
|
88
|
+
|
89
|
+
To finish step 2, we'll **define the gene class** that we referenced in the above `.search_space` method. Gene classes should include the `Evolvable::Gene` module.
|
90
|
+
|
91
|
+
```ruby
|
92
|
+
class CharGene
|
93
|
+
include Evolvable::Gene
|
94
|
+
|
95
|
+
def self.chars
|
96
|
+
@chars ||= 32.upto(126).map(&:chr)
|
97
|
+
end
|
98
|
+
|
99
|
+
def to_s
|
100
|
+
@to_s ||= self.class.chars.sample
|
101
|
+
end
|
102
|
+
end
|
103
|
+
```
|
104
|
+
|
105
|
+
It's important that, once accessed, the data for a particular gene never change. When the `#to_s` method first runs, Ruby's `||=` operator memoizes the result of randomly picking a char, enabling this method to sample a char only once per gene.
|
106
|
+
|
107
|
+
After defining the search space, we can now initialize `HelloWorld` instances with random genes, but to actually evolve them, we need to **define the `#value` instance method**. It provides the basis for comparing different evolvable instances.
|
108
|
+
|
109
|
+
### Step 3
|
110
|
+
|
111
|
+
In the next step, we'll set the goal value to 0, so that evolution favors evolvable HelloWorld instances with `#value` methods that return numbers closer to 0. That means we want instances that more closely match their targets to return scores nearer to 0. As an example, if our target is "hello world", an instance that returns "jello world" would have a value of 1 and "hello world" would have a value of 0.
|
112
|
+
|
113
|
+
For a working implementation, see the `#value` method in [examples/hello_world.rb](https://github.com/mattruzicka/evolvable/blob/main/examples/hello_world.rb)
|
114
|
+
|
115
|
+
### Step 4
|
116
|
+
|
117
|
+
Now it's time to **initialize a population with `.new_population`**. By default, evolvable populations seek to maximize numeric values. In this program, we always know the best possible value, so setting the goal to a concrete number makes sense. This is done by passing the evaluation params with equalize set to 0.
|
118
|
+
|
119
|
+
We'll also specify the number of instances in a population using the population's `size` parameter and change the mutation porbability from 0.03 (3%) to 0.6 (60%).
|
120
|
+
|
121
|
+
Experimentation has suggested that a large mutation probability tends to decrease the time it takes to evolve matches with short strings and has the opposite effect for long strings. This is demonstrated in the example output above by how many generations it took to go from "Hello World!" to "Hello Evolvable World". As an optimization, we could dynamically change the mutation probability using a population hook detailed below, but doing so will be left as an exercise for the reader. [Pull requests are welcome.](#contributing)
|
122
|
+
|
123
|
+
```ruby
|
124
|
+
population = HelloWorld.new_population(size: 100,
|
125
|
+
evaluation: { equalize: 0 },
|
126
|
+
mutation: { probability: 0.6 }
|
127
|
+
```
|
128
|
+
|
129
|
+
|
130
|
+
At this point, everything should work when we run `population.evolve`, but it'll look like nothing is happening. The next section will allow us to gain instight by hooking into the evolutionary process.
|
131
|
+
|
132
|
+
### Evolvable Population Hooks
|
133
|
+
|
134
|
+
The following class methods can be implemented on your Evolvable class, e.g. HelloWorld, to hook into the Population#evolve lifecycle. This is useful for logging evolutionary progress, optimizing parameters, and creating interactions with and between evolvable instances.
|
135
|
+
|
136
|
+
1. `.before_evaluation(population)` - {@readme Evolvable::ClassMethods#before_evaluation}
|
137
|
+
2. `.before_evolution(population)`- {@readme Evolvable::ClassMethods#before_evolution}
|
138
|
+
3. `.after_evolution(population)` - {@readme Evolvable::ClassMethods#after_evolution}
|
139
|
+
|
140
|
+
Let's define `.before_evolution` to print the best value for each generation. We'll also define `HelloWorld#to_s`, which implicitly delegates to `CharGene#to_s` during the string interpolation that happens.
|
141
|
+
|
142
|
+
```ruby
|
143
|
+
class HelloWorld
|
144
|
+
include Evolvable
|
145
|
+
|
146
|
+
def self.before_evolution(population)
|
147
|
+
best_evolvable = population.best_evolvable
|
148
|
+
evolutions_count = population.evolutions_count
|
149
|
+
puts "#{best_evolvable} - Generation #{evolutions_count}"
|
150
|
+
end
|
151
|
+
|
152
|
+
# ...
|
153
|
+
|
154
|
+
def to_s
|
155
|
+
@to_s ||= genes.join
|
156
|
+
end
|
157
|
+
|
158
|
+
# ...
|
159
|
+
end
|
160
|
+
```
|
161
|
+
|
162
|
+
Finally we can **evolve the population with the `Evolvable::Population#evolve` instance method**.
|
163
|
+
|
164
|
+
```ruby
|
165
|
+
population.evolve
|
166
|
+
```
|
167
|
+
|
168
|
+
**You now know the fundamental steps to building evolvable programs of endless complexity in any domain!** 🐸 The exact implementation for the command line demo can be found in [exe/hello](https://github.com/mattruzicka/evolvable/blob/main/exe/hello) and [examples/hello_world.rb](https://github.com/mattruzicka/evolvable/blob/main/examples/hello_world.rb).
|
169
|
+
|
170
|
+
## Concepts
|
171
|
+
|
172
|
+
[Populations](#populations) are composed of evolvables which are composed of genes. Evolvables orchestrate behaviors by delegating to gene objects. Collections of genes are organized into genomes and constitute the [search space](#search-space). [Evaluation](#evaluation) and [evolution](#evolution) objects are used to evolve populations. By default, evolution is composed of [selection](#selection), [combination](#combination), and [mutation](#mutation).
|
173
|
+
|
174
|
+
The following concept map depicts how genes flow through populations.
|
175
|
+
|
176
|
+
![Concept Map](https://github.com/mattruzicka/evolvable/raw/main/examples/images/diagram.png)
|
177
|
+
|
178
|
+
Evolvable is designed with extensibility in mind. Evolvable objects such as [evaluation](#evaluation), [evolution](#evolution), [selection](#selection), [combination](#combination), and [mutation](#mutation) can be extended and swapped, potentially in ways that alter the above graph.
|
179
|
+
|
180
|
+
## Genes
|
181
|
+
{@readme Evolvable::Gene}
|
182
|
+
|
183
|
+
{@example Evolvable::Gene}
|
184
|
+
|
185
|
+
[Documentation](https://rubydoc.info/github/mattruzicka/evolvable/Evolvable/Gene)
|
186
|
+
|
187
|
+
## Populations
|
188
|
+
{@readme Evolvable::Population}
|
189
|
+
|
190
|
+
{@example Evolvable::Population}
|
191
|
+
|
192
|
+
[Documentation](https://rubydoc.info/github/mattruzicka/evolvable/Evolvable/Population)
|
193
|
+
|
194
|
+
## Evaluation
|
195
|
+
{@readme Evolvable::Evaluation}
|
196
|
+
|
197
|
+
{@example Evolvable::Evaluation}
|
198
|
+
|
199
|
+
[Documentation](https://rubydoc.info/github/mattruzicka/evolvable/Evolvable/Evaluation)
|
200
|
+
|
201
|
+
## Evolution
|
202
|
+
{@readme Evolvable::Evolution}
|
203
|
+
|
204
|
+
[Documentation](https://rubydoc.info/github/mattruzicka/evolvable/Evolvable/Evolution)
|
205
|
+
|
206
|
+
## Selection
|
207
|
+
{@readme Evolvable::Selection}
|
208
|
+
|
209
|
+
{@example Evolvable::Selection}
|
210
|
+
|
211
|
+
|
212
|
+
[Documentation](https://rubydoc.info/github/mattruzicka/evolvable/Evolvable/Selection)
|
213
|
+
|
214
|
+
## Combination
|
215
|
+
{@readme Evolvable::GeneCombination}
|
216
|
+
|
217
|
+
[Documentation](https://rubydoc.info/github/mattruzicka/evolvable/Evolvable/Combination)
|
218
|
+
|
219
|
+
## Mutation
|
220
|
+
{@readme Evolvable::Mutation}
|
221
|
+
|
222
|
+
{@example Evolvable::Mutation}
|
223
|
+
|
224
|
+
[Documentation](https://rubydoc.info/github/mattruzicka/evolvable/Evolvable/Mutation)
|
225
|
+
|
226
|
+
## Search Space
|
227
|
+
{@readme Evolvable::SearchSpace}
|
228
|
+
|
229
|
+
{@example Evolvable::SearchSpace}
|
230
|
+
|
231
|
+
[Documentation](https://rubydoc.info/github/mattruzicka/evolvable/Evolvable/SearchSpace)
|
232
|
+
|
233
|
+
## Contributing
|
234
|
+
|
235
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/mattruzicka/evolvable.
|
236
|
+
|
237
|
+
If you're interested in contributing, but don't know where to get started, message me on twitter at [@mattruzicka](https://twitter.com/mattruzicka).
|
data/bin/console
CHANGED
@@ -2,13 +2,26 @@
|
|
2
2
|
|
3
3
|
require 'bundler/setup'
|
4
4
|
require 'evolvable'
|
5
|
+
require 'byebug'
|
5
6
|
|
6
|
-
|
7
|
+
Dir['./examples/*.rb'].each { |f| require f }
|
7
8
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
9
|
+
## HelloWorld
|
10
|
+
# population = HelloWorld.new_population(size: 100,
|
11
|
+
# evaluation: { equalize: 0 },
|
12
|
+
# mutation: { probability: 0.6 })
|
13
|
+
# population.evolve
|
14
|
+
# HelloWorld.start_loop(population)
|
15
|
+
|
16
|
+
## Stickman
|
17
|
+
# population = Stickman.new_population(size: 5,
|
18
|
+
# mutation: { probability: 0.3 })
|
19
|
+
# population.evolve
|
20
|
+
|
21
|
+
## AsciiArt
|
22
|
+
# ascii_art = AsciiArt.new_population(size: 8,
|
23
|
+
# mutation: { probability: 0.3, rate: 0.02 })
|
24
|
+
# ascii_art.evolve
|
12
25
|
|
13
26
|
require 'irb'
|
14
27
|
IRB.start(__FILE__)
|
data/evolvable.gemspec
CHANGED
@@ -26,6 +26,6 @@ Gem::Specification.new do |spec|
|
|
26
26
|
spec.require_paths = ['lib']
|
27
27
|
|
28
28
|
spec.add_development_dependency 'bundler', '~> 2.0'
|
29
|
-
spec.add_development_dependency '
|
30
|
-
spec.add_development_dependency '
|
29
|
+
spec.add_development_dependency 'readme_yard'
|
30
|
+
spec.add_development_dependency 'yard'
|
31
31
|
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require './examples/ascii_gene'
|
2
|
+
|
3
|
+
class AsciiArt
|
4
|
+
include Evolvable
|
5
|
+
|
6
|
+
class << self
|
7
|
+
def search_space
|
8
|
+
{ chars: { type: 'AsciiGene', count: 136 } }
|
9
|
+
end
|
10
|
+
|
11
|
+
def before_evaluation(population)
|
12
|
+
population.best_evolvable.to_terminal
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
CLEAR_SEQUENCE = ("\e[1A\r\033[2K" * 14).freeze
|
17
|
+
|
18
|
+
def to_terminal
|
19
|
+
print(CLEAR_SEQUENCE) unless population.evolutions_count.zero?
|
20
|
+
lines = genes.each_slice(17).flat_map(&:join)
|
21
|
+
lines[0] = "\n #{lines[0]} #{green_text('Minimalism Score:')} #{value}"
|
22
|
+
lines[1] << " #{green_text('Generation:')} #{population.evolutions_count}"
|
23
|
+
print "\n\n#{lines.join("\n ")}\n\n\n\n #{green_text('Use Ctrl-C to stop')} "
|
24
|
+
end
|
25
|
+
|
26
|
+
def minimalism_score
|
27
|
+
@minimalism_score ||= essence_score + spacial_score - clutter_score
|
28
|
+
end
|
29
|
+
|
30
|
+
alias value minimalism_score
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def chars
|
35
|
+
@chars ||= genes.map(&:to_s)
|
36
|
+
end
|
37
|
+
|
38
|
+
def essence_score
|
39
|
+
chars.each_slice(16).each_cons(3).sum do |top, middle, bottom|
|
40
|
+
top_cons = top.each_cons(3).to_a
|
41
|
+
bottom_cons = bottom.each_cons(3).to_a
|
42
|
+
middle.each_cons(3).with_index.sum do |middle_chars, index|
|
43
|
+
mid_center_char = middle_chars.delete_at(1)
|
44
|
+
count = middle_chars.count(mid_center_char)
|
45
|
+
count += top_cons[index].count(mid_center_char)
|
46
|
+
count + (bottom_cons[index]&.count(mid_center_char) || 0)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def spacial_score
|
52
|
+
chars.count { |c| c == ' ' }
|
53
|
+
end
|
54
|
+
|
55
|
+
def clutter_score
|
56
|
+
chars.uniq.count
|
57
|
+
end
|
58
|
+
|
59
|
+
def green_text(text)
|
60
|
+
"\e[32m#{text}\e[0m"
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
class HelloWorld
|
2
|
+
include Evolvable
|
3
|
+
|
4
|
+
class CharGene
|
5
|
+
include Evolvable::Gene
|
6
|
+
|
7
|
+
def self.chars
|
8
|
+
@chars ||= 32.upto(126).map(&:chr)
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.ensure_chars(string)
|
12
|
+
@chars.concat(string.chars - chars)
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_s
|
16
|
+
@to_s ||= self.class.chars.sample
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
MAX_STRING_LENGTH = 40
|
21
|
+
|
22
|
+
def self.search_space
|
23
|
+
{ char_genes: { type: 'CharGene', count: 1..MAX_STRING_LENGTH } }
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.start_loop(population)
|
27
|
+
loop do
|
28
|
+
HelloWorld.seek_target
|
29
|
+
prepare_to_exit_loop && break if exit_loop?
|
30
|
+
|
31
|
+
population.reset_evolvables
|
32
|
+
population.evolve
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.exit_loop?
|
37
|
+
['"exit"', 'exit'].include?(target)
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.prepare_to_exit_loop
|
41
|
+
print "\n\n\n\n\n #{green_text('Goodbye!')}\n\n\n"
|
42
|
+
true
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.seek_target
|
46
|
+
print "\n\n\n\n\n #{green_text('Use "exit" to stop')} \e[1A\e[1A\e[1A\r" \
|
47
|
+
" #{green_text('Enter a string to evolve: ')}"
|
48
|
+
self.target = gets.strip!
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.target=(val)
|
52
|
+
@target = if val.empty?
|
53
|
+
'I chose this string :)'
|
54
|
+
else
|
55
|
+
CharGene.ensure_chars(val)
|
56
|
+
val.slice(0...MAX_STRING_LENGTH)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.target
|
61
|
+
@target ||= 'Hello World!'
|
62
|
+
end
|
63
|
+
|
64
|
+
def self.before_evolution(population)
|
65
|
+
best_evolvable = population.best_evolvable
|
66
|
+
spacing = ' ' * (2 + MAX_STRING_LENGTH - best_evolvable.to_s.length)
|
67
|
+
puts " #{best_evolvable}#{spacing}#{green_text("Generation #{population.evolutions_count}")}"
|
68
|
+
end
|
69
|
+
|
70
|
+
def self.green_text(text)
|
71
|
+
"\e[32m#{text}\e[0m"
|
72
|
+
end
|
73
|
+
|
74
|
+
def to_s
|
75
|
+
@to_s ||= genes.join
|
76
|
+
end
|
77
|
+
|
78
|
+
def value
|
79
|
+
@value ||= compute_value
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
def compute_value
|
85
|
+
string = to_s
|
86
|
+
target = self.class.target
|
87
|
+
target_length = target.length
|
88
|
+
char_matches = target.each_char.with_index.count { |chr, i| chr == string[i] }
|
89
|
+
(target_length - char_matches) + (target_length - string.length).abs
|
90
|
+
end
|
91
|
+
end
|
Binary file
|
@@ -0,0 +1,77 @@
|
|
1
|
+
require './examples/ascii_gene'
|
2
|
+
|
3
|
+
class Stickman
|
4
|
+
include Evolvable
|
5
|
+
|
6
|
+
class HeadGene
|
7
|
+
include Evolvable::Gene
|
8
|
+
|
9
|
+
def to_s
|
10
|
+
@to_s ||= %w[0 4 9 A C D O P Q R b c d g o p q].sample
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class AppendageGene
|
15
|
+
include Evolvable::Gene
|
16
|
+
|
17
|
+
OPTIONS = %w[` ^ - _ = \ | /] << ' '
|
18
|
+
|
19
|
+
def to_s
|
20
|
+
@to_s ||= OPTIONS.sample
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class << self
|
25
|
+
def search_space
|
26
|
+
{ head: { type: 'HeadGene', count: 1 },
|
27
|
+
body: { type: 'AsciiGene', count: 1 },
|
28
|
+
appendages: { type: 'AppendageGene', count: 4 } }
|
29
|
+
end
|
30
|
+
|
31
|
+
CLEAR_SEQUENCE = ("\e[1A\r\033[2K" * 8).freeze
|
32
|
+
|
33
|
+
def before_evaluation(population)
|
34
|
+
population.evolvables.each do |evolvable|
|
35
|
+
puts "\n\n#{evolvable.draw}\n\n"
|
36
|
+
print green_text(" Rate Stickman #{population.evolutions_count}.#{evolvable.generation_index + 1}: ")
|
37
|
+
evolvable.value = gets.to_i
|
38
|
+
print CLEAR_SEQUENCE
|
39
|
+
end
|
40
|
+
|
41
|
+
(@best_evolvables ||= []) << population.best_evolvable
|
42
|
+
animate_best_evolvables if @best_evolvables.count > 1
|
43
|
+
print "\n\n\n\n\n\n\n #{green_text('Evolve next generation?');} Yes!" \
|
44
|
+
" #{green_text('...Use Ctrl-C to stop')}#{"\b" * 23}"
|
45
|
+
gets
|
46
|
+
print CLEAR_SEQUENCE
|
47
|
+
end
|
48
|
+
|
49
|
+
def animate_best_evolvables
|
50
|
+
@best_evolvables.each_with_index do |evolvable, index|
|
51
|
+
puts "\n\n#{green_text(evolvable.draw)}\n\n"
|
52
|
+
print green_text(" Generation: #{index}\r\n ")
|
53
|
+
sleep 0.12
|
54
|
+
print "#{CLEAR_SEQUENCE}"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def green_text(text)
|
59
|
+
"\e[32m#{text}\e[0m"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def draw
|
64
|
+
canvas.sub!('o', find_gene(:head).to_s)
|
65
|
+
canvas.sub!('0', find_gene(:body).to_s)
|
66
|
+
find_genes(:appendages).each { |a| canvas.sub!('-', a.to_s) }
|
67
|
+
canvas
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def canvas
|
73
|
+
@canvas ||= " o \n" \
|
74
|
+
" -0- \n" \
|
75
|
+
" - - \n" \
|
76
|
+
end
|
77
|
+
end
|
data/exe/hello
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'evolvable'
|
5
|
+
require 'byebug'
|
6
|
+
|
7
|
+
Dir['./examples/*.rb'].each { |f| require f }
|
8
|
+
|
9
|
+
population = HelloWorld.new_population(size: 100,
|
10
|
+
evaluation: { equalize: 0 },
|
11
|
+
mutation: { probability: 0.6 })
|
12
|
+
population.evolve
|
13
|
+
HelloWorld.start_loop(population)
|
14
|
+
|
15
|
+
require 'irb'
|
16
|
+
IRB.start(__FILE__)
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Evolvable
|
4
|
+
class CountGene
|
5
|
+
include Gene
|
6
|
+
|
7
|
+
class << self
|
8
|
+
def combine(gene_a, gene_b)
|
9
|
+
min = gene_a.min_count
|
10
|
+
max = gene_a.max_count
|
11
|
+
count = combination.call(gene_a, gene_b).clamp(min, max)
|
12
|
+
new(range: gene_a.range, count: count)
|
13
|
+
end
|
14
|
+
|
15
|
+
LAMBDAS = [->(a, b) { [a, b].sample.count + rand(-1..1) },
|
16
|
+
->(a, b) { a.count + b.count / 2 }].freeze
|
17
|
+
|
18
|
+
def combination
|
19
|
+
LAMBDAS.sample
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def initialize(range:, count: nil)
|
24
|
+
@range = range
|
25
|
+
@count = count
|
26
|
+
end
|
27
|
+
|
28
|
+
attr_reader :range
|
29
|
+
|
30
|
+
def count
|
31
|
+
@count ||= rand(@range)
|
32
|
+
end
|
33
|
+
|
34
|
+
def min_count
|
35
|
+
@range.min
|
36
|
+
end
|
37
|
+
|
38
|
+
def max_count
|
39
|
+
@range.max
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Evolvable
|
4
|
+
#
|
5
|
+
# Prioritizes instances that equal the goal value.
|
6
|
+
#
|
7
|
+
# The default goal value is `0`, but it can be reassigned to any numeric value.
|
8
|
+
#
|
9
|
+
class EqualizeGoal < Goal
|
10
|
+
def value
|
11
|
+
@value ||= 0
|
12
|
+
end
|
13
|
+
|
14
|
+
def evaluate(evolvable)
|
15
|
+
-(evolvable.value - value).abs
|
16
|
+
end
|
17
|
+
|
18
|
+
def met?(evolvable)
|
19
|
+
evolvable.value == value
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
#
|
24
|
+
# @deprecated
|
25
|
+
# Will be removed in 2.0.
|
26
|
+
# Use {EqualizeGoal} instead
|
27
|
+
#
|
28
|
+
class Goal::Equalize < EqualizeGoal; end
|
29
|
+
end
|
data/lib/evolvable/evaluation.rb
CHANGED
@@ -1,27 +1,46 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Evolvable
|
4
|
+
#
|
5
|
+
# @readme
|
6
|
+
# For selection to be effective in the context of evolution, there needs to be
|
7
|
+
# a way to compare evolvables. In the genetic algorithm, this is often
|
8
|
+
# referred to as the "fitness function".
|
9
|
+
#
|
10
|
+
# The `Evolvable::Evaluation` object expects evolvable instances to define a `#value` method that
|
11
|
+
# returns some numeric value. Values are used to evaluate instances relative to each
|
12
|
+
# other and with regards to some goal. Out of the box, the goal can be set
|
13
|
+
# to maximize, minimize, or equalize numeric values.
|
14
|
+
#
|
15
|
+
# @example
|
16
|
+
# # TODO: Show how to add/change population's evaluation object
|
17
|
+
#
|
18
|
+
# # The goal value can also be assigned via as argument to `Evolvable::Population#evolve`
|
19
|
+
# population.evolve(goal_value: 1000)
|
20
|
+
#
|
4
21
|
class Evaluation
|
5
22
|
GOALS = { maximize: Evolvable::Goal::Maximize.new,
|
6
23
|
minimize: Evolvable::Goal::Minimize.new,
|
7
24
|
equalize: Evolvable::Goal::Equalize.new }.freeze
|
8
25
|
|
9
|
-
|
26
|
+
DEFAULT_GOAL_TYPE = :maximize
|
27
|
+
|
28
|
+
def initialize(goal = DEFAULT_GOAL_TYPE)
|
10
29
|
@goal = normalize_goal(goal)
|
11
30
|
end
|
12
31
|
|
13
32
|
attr_accessor :goal
|
14
33
|
|
15
34
|
def call(population)
|
16
|
-
population.
|
35
|
+
population.evolvables.sort_by! { |evolvable| goal.evaluate(evolvable) }
|
17
36
|
end
|
18
37
|
|
19
|
-
def
|
20
|
-
population.
|
38
|
+
def best_evolvable(population)
|
39
|
+
population.evolvables.max_by { |evolvable| goal.evaluate(evolvable) }
|
21
40
|
end
|
22
41
|
|
23
42
|
def met_goal?(population)
|
24
|
-
goal.met?(population.
|
43
|
+
goal.met?(population.evolvables.last)
|
25
44
|
end
|
26
45
|
|
27
46
|
private
|
@@ -33,10 +52,14 @@ module Evolvable
|
|
33
52
|
when Hash
|
34
53
|
goal_from_hash(goal_arg)
|
35
54
|
else
|
36
|
-
|
55
|
+
goal_arg || default_goal
|
37
56
|
end
|
38
57
|
end
|
39
58
|
|
59
|
+
def default_goal
|
60
|
+
GOALS[DEFAULT_GOAL_TYPE]
|
61
|
+
end
|
62
|
+
|
40
63
|
def goal_from_symbol(goal_arg)
|
41
64
|
GOALS[goal_arg]
|
42
65
|
end
|