evolvable 1.0.2 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +4 -0
  3. data/CHANGELOG.md +37 -0
  4. data/Gemfile +3 -0
  5. data/Gemfile.lock +38 -2
  6. data/README.md +227 -290
  7. data/README_YARD.md +237 -0
  8. data/bin/console +18 -5
  9. data/evolvable.gemspec +2 -0
  10. data/examples/ascii_art.rb +62 -0
  11. data/examples/ascii_gene.rb +9 -0
  12. data/examples/hello_world.rb +91 -0
  13. data/examples/images/diagram.png +0 -0
  14. data/examples/stickman.rb +77 -0
  15. data/exe/hello +16 -0
  16. data/lib/evolvable/count_gene.rb +42 -0
  17. data/lib/evolvable/equalize_goal.rb +29 -0
  18. data/lib/evolvable/evaluation.rb +29 -6
  19. data/lib/evolvable/evolution.rb +38 -6
  20. data/lib/evolvable/gene.rb +47 -5
  21. data/lib/evolvable/gene_combination.rb +69 -0
  22. data/lib/evolvable/genome.rb +86 -0
  23. data/lib/evolvable/goal.rb +36 -3
  24. data/lib/evolvable/maximize_goal.rb +30 -0
  25. data/lib/evolvable/minimize_goal.rb +29 -0
  26. data/lib/evolvable/mutation.rb +66 -14
  27. data/lib/evolvable/point_crossover.rb +33 -19
  28. data/lib/evolvable/population.rb +171 -31
  29. data/lib/evolvable/rigid_count_gene.rb +17 -0
  30. data/lib/evolvable/search_space.rb +181 -0
  31. data/lib/evolvable/selection.rb +28 -1
  32. data/lib/evolvable/serializer.rb +21 -0
  33. data/lib/evolvable/uniform_crossover.rb +28 -8
  34. data/lib/evolvable/version.rb +1 -1
  35. data/lib/evolvable.rb +191 -31
  36. metadata +49 -10
  37. data/examples/evolvable_string/char_gene.rb +0 -9
  38. data/examples/evolvable_string.rb +0 -32
  39. data/lib/evolvable/gene_crossover.rb +0 -28
  40. data/lib/evolvable/gene_space.rb +0 -40
  41. data/lib/evolvable/goal/equalize.rb +0 -19
  42. data/lib/evolvable/goal/maximize.rb +0 -19
  43. 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
- require './examples/evolvable_string'
7
+ Dir['./examples/*.rb'].each { |f| require f }
7
8
 
8
- puts "To get started, try this:\n\n" \
9
- " population = EvolvableString.new_population\n" \
10
- " population.mutation.probability = 0.8\n" \
11
- " population.evolve(goal_value: EvolvableString::TARGET_STRING.length)\n\n"
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,4 +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 'readme_yard'
30
+ spec.add_development_dependency 'yard'
29
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,9 @@
1
+ class AsciiGene
2
+ include Evolvable::Gene
3
+
4
+ ASCII_RANGE = 32..126
5
+
6
+ def to_s
7
+ @to_s ||= rand(ASCII_RANGE).chr
8
+ end
9
+ 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
@@ -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
- def initialize(goal = :maximize)
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.instances.sort_by! { |instance| goal.evaluate(instance) }
35
+ population.evolvables.sort_by! { |evolvable| goal.evaluate(evolvable) }
17
36
  end
18
37
 
19
- def best_instance(population)
20
- population.instances.max_by { |instance| goal.evaluate(instance) }
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.instances.last)
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
- goal_arg
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