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.md CHANGED
@@ -1,422 +1,359 @@
1
- # Evolvable
1
+ # Evolvable 🦎
2
+
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)
3
4
 
4
- A framework for building evolutionary behaviors in Ruby.
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).
5
8
 
6
- [Evolutionary algorithms](https://en.wikipedia.org/wiki/Evolutionary_algorithm) build upon ideas such as natural selection, crossover, and mutation to construct relatively simple solutions to complex problems. This gem has been used to implement evolutionary behaviors for [visual, textual, and auditory experiences](https://projectpag.es/evolvable) as well as a variety of AI agents.
7
9
 
8
- With a straightforward and extensible API, Evolvable aims to make building simple as well as complex evolutionary algorithms fun and relatively easy.
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)
9
22
 
10
- ### The Evolvable Abstraction
11
- Population objects are composed of instances that include the `Evolvable` module. Instances are composed of gene objects that include the `Evolvable::Gene` module. Evaluation and evolution objects are used by population objects to evolve your instances. An evaluation object has one goal object and the evolution object is composed of selection, crossover, and mutation objects by default. All classes exposed by Evolvable are prefixed with `Evolvable::` and can be configured, inherited, removed, and extended.
12
23
 
13
24
  ## Installation
14
25
 
15
- Add `gem 'evolvable'` to your application's Gemfile and run `bundle install` or install it yourself with `gem install evolvable`
26
+ Add [gem "evolvable"](https://rubygems.org/gems/evolvable) to your Gemfile and run `bundle install` or install it yourself with: `gem install evolvable`
16
27
 
17
28
  ## Getting Started
18
29
 
19
- After installing and requiring the "evolvable" Ruby gem:
30
+ The `Evolvable` module makes it possible to implement evolutionary behaviors for
31
+ any class by defining a `.search_space` class method and `#value` instance method.
32
+ Then to evolve instances, initialize a population with `.new_population` and invoke
33
+ the `#evolve` method on the resulting population object.
20
34
 
21
- 1. Include the `Evolvable` module in the class for the instances you want to evolve. (See [Configuration](#Configuration)).
22
- 2. Implement `.gene_space`, define any gene classes referenced by it, and include the `Evolvable::Gene` module for each. (See [Genes](#Genes)).
23
- 3. Implement `#value`. (See [Evaluation](#evaluation-1)).
24
- 4. Initialize a population and start evolving. (See [Populations](#Populations)).
35
+ ### Implementation Steps
25
36
 
26
- Visit the [Evolving Strings](https://github.com/mattruzicka/evolvable/wiki/Evolving-Strings) tutorial to see these steps in action. It walks through a simplified implementation of the [evolve string](https://github.com/mattruzicka/evolve_string) command-line program. Here's the [example source code](https://github.com/mattruzicka/evolvable/blob/master/examples/evolvable_string.rb) for the tutorial.
37
+ 1. [Include the `Evolvable` module in the class you want to evolve.](https://rubydoc.info/github/mattruzicka/evolvable/Evolvable)
38
+ 2. [Define `.search_space` and any gene classes that you reference.](https://rubydoc.info/github/mattruzicka/evolvable/Evolvable/SearchSpace)
39
+ 3. [Define `#value`.](https://rubydoc.info/github/mattruzicka/evolvable/Evolvable/Evaluation)
40
+ 4. [Initialize a population with `.new_population` and use `#evolve`.](https://rubydoc.info/github/mattruzicka/evolvable/Evolvable/Population)
27
41
 
28
- If you’d like to quickly play around with an evolvable string Population object, you can do so by cloning this repo and running the command `bin/console` in this project's directory.
29
42
 
30
- ## Usage
31
- - [Configuration](#Configuration)
32
- - [Genes](#Genes)
33
- - [Populations](#Populations)
34
- - [Evaluation](#evaluation-1)
35
- - [Evolution](#evolution-1)
36
- - [Selection](#selection-1)
37
- - [Crossover](#Crossover)
38
- - [Mutation](#mutation-1)
43
+ To demonstrate these steps, we'll look at the [Hello World](#) example program.
39
44
 
40
- ## Configuration
45
+ ### Hello World
41
46
 
42
- You'll need to define a class for the instances you want to evolve and include the `Evolvable` module. Let's say you want to evolve a melody. You might do something like this:
47
+ 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.
43
48
 
44
- ```ruby
45
- class Melody
46
- include Evolvable
49
+ Below is example output from evolving a population of randomly initialized string objects to match "Hello World!", then "Hello Evolvable World".
47
50
 
48
- def self.gene_space
49
- { instrument: { type: 'InstrumentGene', count: 1 },
50
- notes: { type: 'NoteGene', count: 16 } }
51
- end
52
-
53
- def value
54
- average_rating # ...
55
- end
56
- end
51
+ ```
52
+ Enter a string to evolve: Hello World!
53
+
54
+ pp`W^jXG'_N`% Generation 0
55
+ H-OQXZ\a~{H* Generation 1 ...
56
+ HRv9X WorlNi Generation 50 ...
57
+ HRl6W World# Generation 100 ...
58
+ Hello World! Generation 165
59
+
60
+ ❯ Enter a string to evolve: Hello Evolvable World
61
+
62
+ Helgo World!b+=1}3 Generation 165
63
+ Helgo Worlv!}:c(SoV Generation 166
64
+ Helgo WorlvsC`X(Joqs Generation 167
65
+ Helgo WorlvsC`X(So1RE Generation 168 ...
66
+ Hello Evolv#"l{ Wor*5 Generation 300 ...
67
+ Hello Evolvable World Generation 388
57
68
  ```
58
69
 
59
- The `Evolvable` module expects the [".gene_space" class method](#evolvableclassgene_space) and requires the ["#value" instance method](#evolvableclassvalue) to be defined as documented below. Other methods exposed by `Evolvable` have also been documented below.
60
-
61
- ### EvolvableClass.gene_space
62
-
63
- You're expected to override this and return a gene space configuration hash or GeneSpace object. It defines the mapping for a hyperdimensional "gene space" so to speak. The above sample definition for the melody class configures each instance to have 16 note genes and 1 instrument gene.
64
-
65
- See the section on [Genes](#genes) for more details.
66
-
67
- ### EvolvableClass.new_population(keyword_args = {})
68
-
69
- Initializes a new population. Example: `population = Melody.new_population(size: 100)`
70
-
71
- Accepts the same arguments as [Population.new](#evolvablepopulationnew)
72
-
73
- ### EvolvableClass.new_instance(population: nil, genes: [], population_index: nil)
74
-
75
- Initializes a new instance. Accepts a population object, an array of gene objects, and the instance's population index. This method is useful for re-initializing instances and populations that have been saved.
76
-
77
- _It is not recommended that you override this method_ as it is used by Evolvable internals. If you need to customize how your instances are initialized you can override either of the following two "initialize_instance" methods.
78
-
79
- ### EvolvableClass.initialize_instance
80
-
81
- The default implementation simply delegates to `.new` and is useful for instances with custom initialize methods.
82
-
83
- ### EvolvableClass#initialize_instance
84
-
85
- Runs after Evolvable finishes building your instance. It's useful for stuff like implementing custom gene initialization logic. For example, the Evolvable Strings web demo (coming soon) uses it to read from a "length gene" and add or remove "char genes" accordingly.
86
-
87
- ### EvolvableClass#population, #population=
88
-
89
- The population object being used to evolve this instance.
90
-
91
- ### EvolvableClass#genes, #genes=
92
-
93
- An array of all an instance's genes. You can find specific types of genes with the following two methods.
94
-
95
- ### EvolvableClass#find_genes(key)
96
-
97
- Returns an array of genes that have the given key. Gene keys are defined in the [EvolvableClass.gene_space](#evolvableclassgene_space) method. In the Melody example above, the key for the note genes would be `:notes`. The following would return an array of them: `note_genes = melody.find_genes(:notes)`
98
-
99
- ### EvolvableClass#find_gene(key)
100
-
101
- Returns the first gene with the given key. In the Melody example above, the instrument gene has the key `:instrument` so we might write something like: `instrument_gene = melody.find_gene(instrument)`
102
-
103
- ### EvolvableClass#population_index, #population_index=
104
-
105
- Returns an instance's population index - an integer representing the order in which it was initialized in a population. It's the most basic way to distinguish instances in a population.
106
-
107
- ### EvolvableClass#value
108
-
109
- You must implement this method. It is used when evaluating instances before undergoing evolution. The above melody example imagines that the melodies have ratings and uses them as the basis for evaluation and selection.
110
-
111
- Technically, this method can return any object that implements Ruby's [Comparable](https://ruby-doc.org/core-2.7.1/Comparable.html). See the section on [Evaluation](#evaluation-1) for details.
112
-
113
- ### Evolvable Hooks
114
-
115
- The following class method hooks can be overridden. The hooks run for each evolution in the following order:
70
+ ### Step 1
116
71
 
117
- **.before_evaluation(population)**
72
+ Let's begin by defining a `HelloWorld` class and have it **include the `Evolvable` module**.
118
73
 
119
- **.before_evolution(population)**
74
+ ```ruby
75
+ class HelloWorld
76
+ include Evolvable
77
+ end
78
+ ```
120
79
 
121
- **.after_evolution(population)**
80
+ ### Step 2
122
81
 
123
- To use our Melody example from above, you could override the `.before_evolution` method to play the best melody from each generation with something like this:
82
+ 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.
124
83
 
125
84
  ```ruby
126
- class Melody
127
- def self.before_evolution(population)
128
- best_melody = population.best_instance
129
- best_melody.play
130
- end
85
+ class HelloWorld
86
+ include Evolvable
131
87
 
132
- def play
133
- note_genes = melody.find_genes(:notes)
134
- note_values = note_genes.map(&:value)
135
- find_gene(:instrument).play(note_values)
88
+ def self.search_space
89
+ ["CharGene", 1..40]
136
90
  end
137
91
  end
138
92
  ```
139
93
 
140
- ## Genes
141
-
142
- Instances rely on gene objects to compose behaviors. In other words, a gene can be thought of as an object that in some way affects the behavior of an instance. They are used to encapsulate a "sample space" and return a sample outcome when accessed.
94
+ 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.
143
95
 
144
- ### The Evolvable::Gene module
96
+ 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.
145
97
 
146
- Gene objects must include the `Evolvable::Gene` module which enables them to undergo evolutionary operations such as crossover and mutation.
98
+ 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.
147
99
 
148
- To continue with the melody example, we might encode a NoteGene like so:
100
+ 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.
149
101
 
150
102
  ```ruby
151
- class NoteGene
103
+ class CharGene
152
104
  include Evolvable::Gene
153
105
 
154
- NOTES = ['C', 'C♯', 'D', 'D♯', 'E', 'F', 'F♯', 'G', 'G♯', 'A', 'A♯', 'B']
106
+ def self.chars
107
+ @chars ||= 32.upto(126).map(&:chr)
108
+ end
155
109
 
156
- def value
157
- @value ||= NOTES.sample
110
+ def to_s
111
+ @to_s ||= self.class.chars.sample
158
112
  end
159
113
  end
160
114
  ```
161
- Here, the "sample space" for the NoteGene class has twelve notes, but each object will have only one note which is randomly chosen when the "value" method is invoked for the first time. _It is important that the data for a particular gene never change._ Ruby's or-equals operator `||=` is super useful for memoizing gene attributes. It is used above to randomly pick a note only once and return the same note for the lifetime of the object.
162
115
 
163
- A melody instance with multiple note genes might use the `NoteGene#value` method to compose the notes of its melody like so: `melody.find_genes(:note).map(&:value)`. Let's keep humming with the melody example and implement the `InstrumentGene` too:
116
+ 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.
164
117
 
165
- ```ruby
166
- class InstrumentGene
167
- include Evolvable::Gene
118
+ 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.
168
119
 
169
- def instrument_class
170
- @instrument_class ||= [Guitar, Synth, Trumpet].sample
171
- end
120
+ ### Step 3
172
121
 
173
- def volume
174
- @volume ||= rand(1..100)
175
- end
122
+ 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.
176
123
 
177
- def play(notes)
178
- instrument_class.play(notes: notes, volume: volume)
179
- end
180
- end
181
- ```
124
+ For a working implementation, see the `#value` method in [examples/hello_world.rb](https://github.com/mattruzicka/evolvable/blob/main/examples/hello_world.rb)
182
125
 
183
- You can model your sample space however you like. Ruby's [Array](https://ruby-doc.org/core-2.7.1/Array.html), [Hash](https://ruby-doc.org/core-2.7.1/Hash.html), [Range](https://ruby-doc.org/core-2.7.1/Range.html), and [Random](https://ruby-doc.org/core-2.7.1/Random.html) classes may be useful. This InstrumentGene implementation has 300 possible outcomes (3 instruments * 100 volumes) and uses Ruby's Array, Range, and Random classes.
126
+ ### Step 4
184
127
 
185
- Now that its genes are implemented, a melody instance can use them:
128
+ 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.
186
129
 
187
- ```ruby
188
- class Melody
189
- include Evolvable
130
+ 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%).
190
131
 
191
- # ...
132
+ 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)
192
133
 
193
- def play
194
- note_genes = melody.find_genes(:notes)
195
- note_values = note_genes.map(&:value)
196
- find_gene(:instrument).play(note_values)
197
- end
198
- end
134
+ ```ruby
135
+ population = HelloWorld.new_population(size: 100,
136
+ evaluation: { equalize: 0 },
137
+ mutation: { probability: 0.6 }
199
138
  ```
200
139
 
201
- In this way, instances can express behaviors via genes and even orchestrate interactions between them. Genes can also interact with each other during an instance's initialization process via the [EvolvableClass#initialize_instance](#evolvableclassinitialize_instance-1) method
202
-
203
- ### The Evolvable::GeneSpace object
204
-
205
- The `Evolvable::GeneSpace` object is responsible for initializing the full set of genes for a particular instance according to the configuration returned by the [EvolvableClass.gene_space](#evolvableclassgene_space) method. It is used by the `Evolvable::Population` to initialize new instances.
206
140
 
207
- Technically, any object that responds to a `new_genes` method which returns an array of genes for a particular instance can function as a GeneSpace object. Custom implementations will be used if returned by the `.gene_space` method.
141
+ 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.
208
142
 
143
+ ### Evolvable Population Hooks
209
144
 
210
- ## Populations
211
-
212
- The `Evolvable::Population` object is responsible for generating and evolving instances. It orchestrates all the other Evolvable objects to do so.
145
+ 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.
213
146
 
214
- ### Evolvable::Population.new
147
+ 1. `.before_evaluation(population)` - Runs before evaluation.
215
148
 
216
- Initializes an Evolvable::Population.
149
+ 2. `.before_evolution(population)`- Runs after evaluation and before evolution.
217
150
 
218
- Keyword arguments:
151
+ 3. `.after_evolution(population)` - Runs after evolution.
219
152
 
220
- #### evolvable_class
221
- Required. Implicitly specified when using EvolvableClass.new_population.
222
- #### id, name
223
- Both default to `nil`. Not used by Evolvable, but convenient when working with multiple populations.
224
- #### size
225
- Defaults to `40`. Specifies the number of instances in the population.
226
- #### evolutions_count
227
- Defaults to `0`. Useful when re-initializing a saved population with instances.
228
- #### gene_space
229
- Defaults to `evolvable_class.new_gene_space` which uses the [EvolvableClass.gene_space](#evolvableclassgene_space) method
230
- #### evolution
231
- Defaults to `Evolvable::Evolution.new`. See [evolution](#evolution-1)
232
- #### evaluation
233
- Defaults to `Evolvable::Evaluation.new`, with a goal of maximizing towards Float::INFINITY. See [evaluation](#evaluation-1)
234
- #### instances
235
- Defaults to initializing a `size` number of `evolvable_class` instances using the `gene_space` object. Any given instances are assigned, but if given less than `size`, more will be initialized.
236
153
 
237
- ### Evolvable::Population#evolve
154
+ 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.
238
155
 
239
- Keyword arguments:
240
-
241
- #### count
242
- The number of evolutions to run. Expects a positive integer and Defaults to Float::INFINITY and will therefore run indefinitely unless a `goal_value` is specified.
243
- #### goal_value
244
- Assigns the goal object's value. Will continue running until any instance's value reaches it. See [evaluation](#evaluation-1)
156
+ ```ruby
157
+ class HelloWorld
158
+ include Evolvable
245
159
 
246
- ### Evolvable::Population#best_instance
247
- Returns an instance with the value that is nearest to the goal value.
160
+ def self.before_evolution(population)
161
+ best_evolvable = population.best_evolvable
162
+ evolutions_count = population.evolutions_count
163
+ puts "#{best_evolvable} - Generation #{evolutions_count}"
164
+ end
248
165
 
249
- ### Evolvable::Population#met_goal?
250
- Returns true if any instance's value matches the goal value, otherwise false.
166
+ # ...
251
167
 
252
- ### Evolvable::Population#new_instance
253
- Initializes an instance for the population. Note that this method does not add the new instance to its array of instances.
168
+ def to_s
169
+ @to_s ||= genes.join
170
+ end
254
171
 
255
- Keyword arguments:
172
+ # ...
173
+ end
174
+ ```
256
175
 
257
- #### genes
258
- An array of initialized gene objects. Defaults to `[]`
259
- #### population_index
260
- Defaults to `nil` and expects an integer. See (EvolvableClass#population_index)[#evolvableclasspopulation_index-population_index]
176
+ Finally we can **evolve the population with the `Evolvable::Population#evolve` instance method**.
261
177
 
178
+ ```ruby
179
+ population.evolve
180
+ ```
262
181
 
263
- ### Population#selection, #selection=
264
- The [selection](#selection-1) object.
182
+ **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).
265
183
 
266
- ### Population#crossover, #crossover=
267
- The [crossover](#crossover) object.
184
+ ## Concepts
268
185
 
269
- ### Population#mutation, #mutation=
270
- The [mutation](#mutation-1) object.
186
+ [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).
271
187
 
272
- ### Population#goal, #goal=
273
- The [evaluation](#evaluation-1)'s goal object.
188
+ The following concept map depicts how genes flow through populations.
274
189
 
275
- ## Evaluation
190
+ ![Concept Map](https://github.com/mattruzicka/evolvable/raw/main/examples/images/diagram.png)
276
191
 
277
- For selection to be effective in the context of progressive evolution, there needs to be some way of comparing various instances with each other. In traditional genetic algorithms, this is referred to as the "fitness function". The `Evolvable::Evaluation` object expects instances to define a [EvolvableClass#value](#evolvableclassvalue) method that it uses to evaluate them relative to each other and against a definable goal.
192
+ 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.
278
193
 
279
- A goal object has a value that can be most easily assigned via an argument to `Evolvable::Population#evolve` like this: `population.evolve(goal_value: 1000)`. Evolvable provides the following goal object implementations and goal value defaults.
194
+ ## Genes
195
+ For evolution to be effective, an evolvable's genes must be able to influence
196
+ its behavior. Evolvables are composed of genes that can be used to run simple
197
+ functions or orchestrate complex interactions. The level of abstraction is up
198
+ to you.
280
199
 
281
- ### The Evolvable::Goal::Maximize object
200
+ Defining gene classes requires encapsulating some "sample space" and returning
201
+ a sample outcome when a gene attribute is accessed. For evolution to proceed
202
+ in a non-random way, the same sample outcome should be returned every time
203
+ a particular gene is accessed with a particular set of parameters.
204
+ Memoization is a useful technique for doing just this. The
205
+ [memo_wise](https://github.com/panorama-ed/memo_wise) gem may be useful for
206
+ more complex memoizations.
282
207
 
283
- Prioritizes instances with greater values. This is the default.
284
208
 
285
- The default goal value is `Float::INFINITY`, but it can be reassigned as anything that implements the Ruby [Comparable](https://ruby-doc.org/core-2.7.1/Comparable.html) module.
209
+ ```ruby
210
+ # This gene generates a random hexidecimal color code for use by evolvables.
286
211
 
287
- ### The Evolvable::Goal::Minimize object
212
+ require 'securerandom'
288
213
 
289
- Prioritizes instances with lesser values.
214
+ class ColorGene
215
+ include Evolvable::Gene
290
216
 
291
- The default goal value is `-Float::INFINITY`, but it can be reassigned as anything that implements the Ruby [Comparable](https://ruby-doc.org/core-2.7.1/Comparable.html) module.
217
+ def hex_code
218
+ @hex_code ||= SecureRandom.hex(3)
219
+ end
220
+ end
221
+ ```
292
222
 
293
- ### The Evolvable::Goal::Equalize object
294
223
 
295
- Prioritizes instances that equal the goal value.
224
+ [Documentation](https://rubydoc.info/github/mattruzicka/evolvable/Evolvable/Gene)
296
225
 
297
- The default goal value is `0`, but it can be reassigned as anything that implements the Ruby [Comparable](https://ruby-doc.org/core-2.7.1/Comparable.html) module.
226
+ ## Populations
227
+ Population objects are responsible for generating and evolving instances.
228
+ They orchestrate all the other Evolvable objects to do so.
298
229
 
299
- ### Custom Goal Objects
230
+ Populations can be initialized and re-initialized with a number of useful
231
+ parameters.
300
232
 
301
- You can implement custom goal object like so:
302
233
 
303
234
  ```ruby
304
- class CustomGoal
305
- include Evolvable::Goal
306
-
307
- def evaluate(instance)
308
- # Required by Evolvable::Evaluation in order to sort instances in preparation for selection.
309
- end
310
-
311
- def met?(instance)
312
- # Used by Evolvable::Population#evolve to stop evolving when the goal value has been reached.
313
- end
314
- end
235
+ # TODO: initialize a population with all supported parameters
315
236
  ```
316
237
 
317
- The goal for a population can be specified via assignment - `population.goal = Evolvable::Goal::Equalize.new` - or by passing an evaluation object when [initializing a population](#evolvablepopulationnew).
318
238
 
319
- You can intialize the `Evolvable::Evaluation` object with any goal object like this:
239
+ [Documentation](https://rubydoc.info/github/mattruzicka/evolvable/Evolvable/Population)
320
240
 
321
- ```ruby
322
- goal_object = SomeGoal.new(value: 100)
323
- Evolvable::Evaluation.new(goal_object)
324
- ```
241
+ ## Evaluation
242
+ For selection to be effective in the context of evolution, there needs to be
243
+ a way to compare evolvables. In the genetic algorithm, this is often
244
+ referred to as the "fitness function".
245
+
246
+ The `Evolvable::Evaluation` object expects evolvable instances to define a `#value` method that
247
+ returns some numeric value. Values are used to evaluate instances relative to each
248
+ other and with regards to some goal. Out of the box, the goal can be set
249
+ to maximize, minimize, or equalize numeric values.
325
250
 
326
- or more succinctly like this:
327
251
 
328
252
  ```ruby
329
- Evolvable::Evaluation.new(:maximize) # Uses default goal value of Float::INFINITY
330
- Evolvable::Evaluation.new(maximize: 50) # Sets goal value to 50
331
- Evolvable::Evaluation.new(:minimize) # Uses default goal value of -Float::INFINITY
332
- Evolvable::Evaluation.new(minimize: 100) # Sets goal value to 100
333
- Evolvable::Evaluation.new(:equalize) # Uses default goal value of 0
334
- Evolvable::Evaluation.new(equalize: 1000) # Sets goal value to 1000
253
+ # TODO: Show how to add/change population's evaluation object
335
254
 
255
+ # The goal value can also be assigned via as argument to `Evolvable::Population#evolve`
256
+ population.evolve(goal_value: 1000)
336
257
  ```
337
258
 
338
- ## Evolution
339
259
 
340
- After a population's instances are evaluated, they undergo evolution. The default `Evolvable::Evolution` object is composed of selection, crossover, and mutation objects and applies them as operations to the population in that order.
260
+ [Documentation](https://rubydoc.info/github/mattruzicka/evolvable/Evolvable/Evaluation)
341
261
 
342
- Populations can be assigned with custom evolution objects. The only necessary dependency for evolution objects is that they implement the `#call` method which accepts a population as the first argument. Population objects also expect evolution objects to define a getter and setter for selection, crossover, and mutation, but these methods are simply for ease-of-use and not necessary.
262
+ ## Evolution
263
+ After a population's instances are evaluated, they undergo evolution.
264
+ The default evolution object is composed of selection,
265
+ crossover, and mutation objects and applies them as operations to
266
+ a population's evolvables in that order.
343
267
 
344
- ### Evolvable::Evolution.new
345
268
 
346
- Initializes a new evolution object.
269
+ [Documentation](https://rubydoc.info/github/mattruzicka/evolvable/Evolvable/Evolution)
347
270
 
348
- Keyword arguments:
271
+ ## Selection
272
+ The selection object assumes that a population's evolvables have already
273
+ been sorted by the evaluation object. It selects "parent" evolvables to
274
+ undergo combination and thereby produce the next generation of evolvables.
349
275
 
350
- #### selection
351
- The default is `Selection.new`
352
- #### crossover
353
- The default is `GeneCrossover.new`
354
- #### mutation
355
- The default is `Mutation.new`
276
+ Only two evolvables are selected as parents for each generation by default.
277
+ The selection size is configurable.
356
278
 
357
- ## Selection
358
279
 
359
- The selection process assumes that the population's instances have already been sorted by the `Evaluation` object. It leaves only a select number of instances in a given population's instances array.
280
+ ```ruby
281
+ # TODO: Show how to add/change population's selection object
282
+ ```
360
283
 
361
- Custom selection objects must implement the `#call` method which accepts the population as the first object.
362
284
 
363
- ### Evolvable::Selection.new
364
285
 
365
- Initializes a new selection object.
286
+ [Documentation](https://rubydoc.info/github/mattruzicka/evolvable/Evolvable/Selection)
366
287
 
367
- Keyword arguments:
288
+ ## Combination
289
+ Combination generates new evolvable instances by combining the genes of selected instances.
290
+ You can think of it as a mixing of parent genes from one generation to
291
+ produce the next generation.
368
292
 
369
- #### size
370
- The number of instances to select from each generation from which to perform crossover and generate or "breed" the next generation. The number of parents The default is 2.
293
+ You may choose from a selection of combination objects or implement your own.
294
+ The default combination object is `Evolvable::GeneCombination`.
371
295
 
372
- ## Crossover
373
296
 
374
- Generates new instances by combining the genes of selected instances. You can think of it as a mixing of parent genes from one generation to produce a next generation.
297
+ [Documentation](https://rubydoc.info/github/mattruzicka/evolvable/Evolvable/Combination)
375
298
 
376
- Custom crossover objects must implement the `#call` method which accepts the population as the first object.
299
+ ## Mutation
300
+ Mutation serves the role of increasing genetic variation. When an evolvable
301
+ undergoes a mutation, one or more of its genes are replaced by newly
302
+ initialized ones. In effect, a gene mutation invokes a new random outcome
303
+ from the genetic search space.
377
304
 
378
- ### The Evolvable::GeneCrossover object
305
+ Mutation frequency is configurable using the `probability` and `rate`
306
+ parameters.
379
307
 
380
- Enables gene types to define crossover behaviors. Each gene class can implement a unique behavior for crossover by overriding the following default implementation which mirrors the behavior of `Evolvable::UniformCrossover`
381
308
 
382
309
  ```ruby
383
- def self.crossover(gene_a, gene_b)
384
- [gene_a, gene_b].sample
385
- end
310
+ # Show how to initialize/assign population with a specific mutation object
386
311
  ```
387
312
 
388
- ### The Evolvable::UniformCrossover object
389
-
390
- Randomly chooses a gene from one of the parents for each gene position.
391
313
 
392
- ### The Evolvable::PointCrossover object
314
+ [Documentation](https://rubydoc.info/github/mattruzicka/evolvable/Evolvable/Mutation)
393
315
 
394
- Supports single and multi-point crossover. The default is single-point crossover via a `points_count` of 1 which can be changed on an existing population (`population.crossover.points_count = 5`) or during initialization (`Evolvable::PointCrossover.new(5)`)
316
+ ## Search Space
317
+ The search space encapsulates the range of possible genes
318
+ for a particular evolvable. You can think of it as the boundaries of
319
+ genetic variation. It is configured via the
320
+ [.search_space](#evolvableclasssearch_space) method that you define
321
+ on your evolvable class. It's used by populations to initialize
322
+ new evolvables.
395
323
 
396
- ## Mutation
324
+ Evolvable provides flexibility in how you define your search space.
325
+ The below example implementations for `.search_space` produce the
326
+ exact same search space for the
327
+ [Hello World](https://github.com/mattruzicka/evolvable#hello-world)
328
+ demo program. The different styles arguably vary in suitability for
329
+ different contexts, perhaps depending on how programs are loaded and
330
+ the number of different gene types.
397
331
 
398
- Mutation serves the role of increasing genetic variation, especially when a population's instances are small in number and mostly homogeneous. When an instance undergoes a mutation, it means that one of its existing genes is replaced with a newly initialized gene. Using the language from the [section on genes](genes), a gene mutation invokes a new outcome from the gene's sample space.
399
332
 
400
- ### Evolvable::Mutation.new
401
-
402
- Initializes a new mutation object.
403
-
404
- Keyword arguments:
333
+ ```ruby
334
+ # All 9 of these example definitions are equivalent
335
+
336
+ # Hash syntax
337
+ { chars: { type: 'CharGene', max_count: 100 } }
338
+ { chars: { type: 'CharGene', min_count: 1, max_count: 100 } }
339
+ { chars: { type: 'CharGene', count: 1..100 } }
340
+
341
+ # Array of arrays syntax
342
+ [[:chars, 'CharGene', 1..100]]
343
+ [['chars', 'CharGene', 1..100]]
344
+ [['CharGene', 1..100]]
345
+
346
+ # A single array works when there's only one type of gene
347
+ ['CharGene', 1..100]
348
+ [:chars, 'CharGene', 1..100]
349
+ ['chars', 'CharGene', 1..100]
350
+ ```
405
351
 
406
- #### probability
407
- The probability that a particular instance undergoes a mutation. By default, the probability is 0.03 which translates to 3%. If initialized with a `rate`, the probability will be 1 which means all genes _can_ undergo mutation, but actual gene mutations will be subject to the given mutation rate.
408
- #### rate
409
- the rate at which individual genes mutate. The default rate is 0 which, when combined with a non-zero `probability` (the default), means that one gene for each instance that undergoes mutation will change. If a rate is given, but no `probability` is given, then the `probability` will bet set to 1 which always defers to the mutation rate.
410
352
 
411
- To summarize, the `probability` represents the chance of mutation on the instance level and the `rate` represents the chance on the gene level. The `probability` and `rate` can be any number from 0 to 1. When the `probability` is 0, no mutation will ever happen. When the `probability` is not 0 but the rate is 0, then any instance that undergoes mutation will only receive one mutant gene. If the rate is not 0, then if an instance has been chosen to undergo mutation, each of its genes will mutate with a probability as defined by the `rate`.
353
+ [Documentation](https://rubydoc.info/github/mattruzicka/evolvable/Evolvable/SearchSpace)
412
354
 
413
- Example Initializations:
355
+ ## Contributing
414
356
 
415
- ```ruby
416
- Evolvable::Mutation.new # Approximately 3% of instances will receive one mutant gene
417
- Evolvable::Mutation.new(probability: 0.5) # Approximately 50% of instances will receive one mutant gene
418
- Evolvable::Mutation.new(rate: 0.03) # Approximately 3% of all genes in the population will mutate.
419
- Evolvable::Mutation.new(probability: 0.3, rate: 0.03) # Approximately 30% of instances will have approximately 3% of their genes mutated.
420
- ```
357
+ Bug reports and pull requests are welcome on GitHub at https://github.com/mattruzicka/evolvable.
421
358
 
422
- Custom mutation objects must implement the `#call` method which accepts the population as the first object.
359
+ If you're interested in contributing, but don't know where to get started, message me on twitter at [@mattruzicka](https://twitter.com/mattruzicka).