evolvable 1.2.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +2 -0
  3. data/CHANGELOG.md +24 -0
  4. data/Gemfile +2 -2
  5. data/Gemfile.lock +44 -25
  6. data/README.md +498 -190
  7. data/README_YARD.md +85 -166
  8. data/bin/console +10 -19
  9. data/docs/Evolvable/ClassMethods.html +1233 -0
  10. data/docs/Evolvable/Community/ClassMethods.html +708 -0
  11. data/docs/Evolvable/Community.html +1342 -0
  12. data/docs/Evolvable/CountGene.html +886 -0
  13. data/docs/Evolvable/EqualizeGoal.html +347 -0
  14. data/docs/Evolvable/Error.html +134 -0
  15. data/docs/Evolvable/Evaluation.html +773 -0
  16. data/docs/Evolvable/Evolution.html +616 -0
  17. data/docs/Evolvable/Gene/ClassMethods.html +413 -0
  18. data/docs/Evolvable/Gene.html +522 -0
  19. data/docs/Evolvable/GeneCluster/ClassMethods.html +431 -0
  20. data/docs/Evolvable/GeneCluster.html +280 -0
  21. data/docs/Evolvable/GeneCombination.html +515 -0
  22. data/docs/Evolvable/GeneSpace.html +619 -0
  23. data/docs/Evolvable/Genome.html +1070 -0
  24. data/docs/Evolvable/Goal.html +500 -0
  25. data/docs/Evolvable/MaximizeGoal.html +348 -0
  26. data/docs/Evolvable/MinimizeGoal.html +348 -0
  27. data/docs/Evolvable/Mutation.html +729 -0
  28. data/docs/Evolvable/PointCrossover.html +444 -0
  29. data/docs/Evolvable/Population.html +2826 -0
  30. data/docs/Evolvable/RigidCountGene.html +501 -0
  31. data/docs/Evolvable/Selection.html +594 -0
  32. data/docs/Evolvable/Serializer.html +293 -0
  33. data/docs/Evolvable/UniformCrossover.html +286 -0
  34. data/docs/Evolvable.html +1619 -0
  35. data/docs/_index.html +341 -0
  36. data/docs/class_list.html +54 -0
  37. data/docs/css/common.css +1 -0
  38. data/docs/css/full_list.css +58 -0
  39. data/docs/css/style.css +503 -0
  40. data/docs/file.README.html +750 -0
  41. data/docs/file_list.html +59 -0
  42. data/docs/frames.html +22 -0
  43. data/docs/index.html +750 -0
  44. data/docs/js/app.js +344 -0
  45. data/docs/js/full_list.js +242 -0
  46. data/docs/js/jquery.js +4 -0
  47. data/docs/method_list.html +1302 -0
  48. data/docs/top-level-namespace.html +110 -0
  49. data/evolvable.gemspec +6 -6
  50. data/examples/ascii_art.rb +5 -9
  51. data/examples/stickman.rb +25 -33
  52. data/{examples/hello_world.rb → exe/hello_evolvable_world} +46 -30
  53. data/lib/evolvable/community.rb +190 -0
  54. data/lib/evolvable/count_gene.rb +65 -0
  55. data/lib/evolvable/equalize_goal.rb +2 -9
  56. data/lib/evolvable/evaluation.rb +113 -14
  57. data/lib/evolvable/evolution.rb +38 -15
  58. data/lib/evolvable/gene.rb +124 -25
  59. data/lib/evolvable/gene_cluster.rb +106 -0
  60. data/lib/evolvable/gene_combination.rb +57 -16
  61. data/lib/evolvable/gene_space.rb +111 -0
  62. data/lib/evolvable/genome.rb +32 -4
  63. data/lib/evolvable/goal.rb +19 -24
  64. data/lib/evolvable/maximize_goal.rb +2 -9
  65. data/lib/evolvable/minimize_goal.rb +3 -9
  66. data/lib/evolvable/mutation.rb +87 -41
  67. data/lib/evolvable/point_crossover.rb +24 -4
  68. data/lib/evolvable/population.rb +258 -84
  69. data/lib/evolvable/rigid_count_gene.rb +36 -0
  70. data/lib/evolvable/selection.rb +68 -14
  71. data/lib/evolvable/serializer.rb +46 -0
  72. data/lib/evolvable/uniform_crossover.rb +30 -6
  73. data/lib/evolvable/version.rb +2 -1
  74. data/lib/evolvable.rb +268 -107
  75. metadata +57 -36
  76. data/examples/images/diagram.png +0 -0
  77. data/exe/hello +0 -16
  78. data/lib/evolvable/error/undefined_method.rb +0 -7
  79. data/lib/evolvable/search_space.rb +0 -181
data/README.md CHANGED
@@ -1,11 +1,36 @@
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
+ [![Gem Version](https://badge.fury.io/rb/evolvable.svg)](https://badge.fury.io/rb/evolvable)
4
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.
5
+ **Code Version: 2.0.0**
6
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).
7
+ Evolvable is a Ruby gem that brings genetic algorithms to Ruby objects through simple, flexible APIs. Define genes, implement fitness criteria, and let evolution discover optimal solutions through selection, combination, and mutation.
8
8
 
9
+ Perfect for optimization problems, creative content generation, machine learning, and simulating complex systems.
10
+
11
+ ## Why Evolvable?
12
+
13
+ Evolvable is ideal when the solution space is too large or complex for brute-force methods. Instead of hardcoding solutions, you define constraints and let evolution discover optimal configurations over time.
14
+
15
+ **The Evolvable Approach:**
16
+ - Explore vast solution spaces efficiently without examining every possibility
17
+ - Discover novel solutions that might not be obvious to human designers
18
+ - Adapt to changing conditions through continuous evolution
19
+ - Balance diverse objectives with communities of different populations
20
+ - Integrate evolutionary concepts directly into your Ruby object model
21
+ - Generate creative content like music, art, and text, not just numerical optimization
22
+
23
+ Whether you're optimizing parameters, generating creative content, or simulating complex systems, Evolvable provides a natural, object-oriented approach to evolutionary algorithms.
24
+
25
+ **Creative Applications**
26
+
27
+ Evolvable treats creative, object-oriented representations as first-class citizens. The same API that optimizes numeric parameters can evolve music compositions, UI layouts, or game content with equal fluency. Examples include:
28
+
29
+ - **Generative art**: Evolve visual compositions based on aesthetic criteria
30
+ - **Music composition**: Create melodies, chord progressions, and rhythms
31
+ - **Game design**: Generate levels, characters, or game mechanics
32
+ - **Natural language**: Evolve text with specific tones, styles, or constraints
33
+ - **UI/UX design**: Discover intuitive layouts and color schemes
9
34
 
10
35
  ## Table of Contents
11
36
  * [Installation](#installation)
@@ -18,342 +43,625 @@ Subscribe to the [Evolvable Newsletter](https://www.evolvable.site/newsletter) t
18
43
  * [Selection](#selection)
19
44
  * [Combination](#combination)
20
45
  * [Mutation](#mutation)
21
- * [Search Space](#search-space)
46
+ * [Gene Clusters](#gene-clusters)
47
+ * [Community](#community)
48
+ * [Serialization](#serialization)
49
+ * [Documentation](https://mattruzicka.github.io/evolvable)
22
50
 
23
51
 
24
52
  ## Installation
25
53
 
26
54
  Add [gem "evolvable"](https://rubygems.org/gems/evolvable) to your Gemfile and run `bundle install` or install it yourself with: `gem install evolvable`
27
55
 
56
+ **Ruby Compatibility:** Evolvable officially supports Ruby 3.0 and higher.
57
+
28
58
  ## Getting Started
29
59
 
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.
60
+ **Quick start**:
61
+ 1. Include `Evolvable` in your Ruby class
62
+ 2. Define genes with the macro-style `gene` method
63
+ 3. Have the `#fitness` method return a numeric value
64
+ 4. Initialize a population and evolve it
65
+
66
+ Example population of "shirts" with various colors, buttons, and collars.
34
67
 
35
- ### Implementation Steps
68
+ ```ruby
69
+ # Step 1
70
+ class Shirt
71
+ include Evolvable
36
72
 
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)
73
+ # Step 2
74
+ gene :color, type: ColorGene # count: 1 default
75
+ gene :buttons, type: ButtonGene, count: 0..10 # Builds an array of genes that can vary in size
76
+ gene :collar, type: CollarGene, count: 0..1 # Collar optional
41
77
 
78
+ # Step 3
79
+ attr_accessor :fitness
80
+ end
42
81
 
43
- To demonstrate these steps, we'll look at the [Hello World](#) example program.
82
+ # Step 4
83
+ population = Shirt.new_population(size: 10)
84
+ population.evolvables.each { |shirt| shirt.fitness = style_rating }
85
+ ```
44
86
 
45
- ### Hello World
87
+ You are free to tailor the genes to your needs and find a style that suits you.
46
88
 
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.
89
+ The `ColorGene` could be as simple as this:
48
90
 
49
- Below is example output from evolving a population of randomly initialized string objects to match "Hello World!", then "Hello Evolvable World".
91
+ ```ruby
92
+ class ColorGene
93
+ include Evolvable::Gene
50
94
 
95
+ def to_s
96
+ @to_s ||= %w[red green blue].sample
97
+ end
98
+ end
51
99
  ```
52
- ❯ Enter a string to evolve: Hello World!
53
100
 
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
101
+ Shirts aren't your style?
59
102
 
60
- Enter a string to evolve: Hello Evolvable World
103
+ Here's a [Hello World](https://github.com/mattruzicka/evolvable/blob/main/exe/hello_evolvable_world)
104
+ command line demo.
61
105
 
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
68
- ```
69
106
 
70
- ### Step 1
107
+ ## Concepts
108
+
109
+ Evolvable is built on these core concepts:
110
+ - **Genes**: Ruby objects that represent traits or behaviors and are passed down during evolution.
111
+ - **Evolvables**: Your Ruby classes that include "Evolvable" and delegate to genes
112
+ - **Populations**: Groups of evolvables instances that evolve together
113
+ - **Evaluation**: Sorts evolvables by fitness
114
+ - **Evolution**: Selection → Combination → Mutation to generate new evolvables
115
+ - **Communities**: Encapsulate evolvable populations
71
116
 
72
- Let's begin by defining a `HelloWorld` class and have it **include the `Evolvable` module**.
117
+ The framework offers built-in implementations while allowing domain-specific customization through its extensible and swapable components.
118
+
119
+ ## Genes
120
+
121
+ Genes are the building blocks of evolvable objects, encapsulating individual characteristics
122
+ that can be combined and mutated during evolution. Each gene represents a trait or behavior
123
+ that can influence an evolvable's performance.
124
+
125
+ **To define a gene class:**
126
+ 1. Include the `Evolvable::Gene` module
127
+ 2. Define how the gene's value is determined
73
128
 
74
129
  ```ruby
75
- class HelloWorld
76
- include Evolvable
130
+ class BehaviorGene
131
+ include Evolvable::Gene
132
+
133
+ def value
134
+ @value ||= %w[explore gather attack defend build].sample
135
+ end
77
136
  end
78
137
  ```
79
138
 
80
- ### Step 2
81
-
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.
139
+ Then use it in an evolvable class:
83
140
 
84
141
  ```ruby
85
- class HelloWorld
142
+ class Robot
86
143
  include Evolvable
87
144
 
88
- def self.search_space
89
- ["CharGene", 1..40]
145
+ gene :behaviors, type: BehaviorGene, count: 3..5
146
+ gene :speed, type: SpeedGene, count: 1
147
+
148
+ def fitness
149
+ run_simulation(behaviors: behaviors.map(&:value), speed: speed.value)
90
150
  end
91
151
  end
92
152
  ```
93
153
 
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.
154
+ **Gene Count**
95
155
 
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.
156
+ You can control how many copies of a gene are created using the `count:` parameter:
97
157
 
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.
158
+ - `count: 1` (default) creates a single instance.
159
+ - A numeric value (e.g. `count: 5`) creates a fixed number of genes using `RigidCountGene`.
160
+ - A range (e.g. `count: 2..8`) creates a variable number of genes using `CountGene`, allowing the count to evolve over time.
99
161
 
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.
162
+ Evolves melody length:
101
163
 
102
164
  ```ruby
103
- class CharGene
165
+ gene :notes, type: NoteGene, count: 4..12
166
+ ```
167
+
168
+ **Custom Combination**
169
+
170
+ By default, the `combine` method randomly picks one of the two parent genes.
171
+ A gene class can implement custom behavior by overriding `.combine`.
172
+
173
+ ```ruby
174
+ class SpeedGene
104
175
  include Evolvable::Gene
105
176
 
106
- def self.chars
107
- @chars ||= 32.upto(126).map(&:chr)
177
+ def self.combine(gene_a, gene_b)
178
+ new_gene = new
179
+ new_gene.value = (gene_a.value + gene_b.value) / 2
180
+ new_gene
108
181
  end
109
182
 
110
- def to_s
111
- @to_s ||= self.class.chars.sample
183
+ attr_writer :value
184
+
185
+ def value
186
+ @value ||= rand(1..100)
112
187
  end
113
188
  end
114
189
  ```
115
190
 
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.
191
+ **Design Patterns**
192
+
193
+ Effective gene design typically follows these principles:
117
194
 
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.
195
+ - **Immutability**: Cache values after initial sampling (e.g., `@value ||= ...`)
196
+ - **Self-Contained**: Genes should encapsulate their logic and state
197
+ - **Composable**: You can build complex structures using multiple genes or clusters
198
+ - **Domain-Specific**: Genes should map directly to your problem’s traits or features
119
199
 
120
- ### Step 3
200
+ Genes come in various types, each representing different aspects of a solution.
201
+ Common examples include numeric genes for quantities, selection genes for choices
202
+ from sets, boolean genes for binary decisions, structural genes for architecture,
203
+ and parameter genes for configuration settings.
121
204
 
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.
123
205
 
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)
206
+ [Gene Documentation](https://mattruzicka.github.io/evolvable/Evolvable/Gene)
125
207
 
126
- ### Step 4
208
+ ## Populations
127
209
 
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.
210
+ Populations orchestrate the evolutionary process through four key components:
129
211
 
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%).
212
+ 1. **Evaluation**: Sorts evolvable instances by fitness
213
+ 2. **Selection**: Chooses parents for combination
214
+ 3. **Combination**: Creates new evolvables from selected parents
215
+ 4. **Mutation**: Introduces variation to maintain genetic diversity
131
216
 
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)
217
+ **Features**:
218
+
219
+ Initialize a population with default or custom parameters:
133
220
 
134
221
  ```ruby
135
- population = HelloWorld.new_population(size: 100,
136
- evaluation: { equalize: 0 },
137
- mutation: { probability: 0.6 }
222
+ population = YourEvolvable.new_population(
223
+ size: 50,
224
+ evaluation: { equalize: 0 },
225
+ selection: { size: 10 },
226
+ mutation: { probability: 0.2, rate: 0.02 }
227
+ )
138
228
  ```
139
229
 
230
+ Or inject fully customized strategy objects:
140
231
 
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.
232
+ ```ruby
233
+ population = YourEvolvable.new_population(
234
+ evaluation: Your::Evaluation.new,
235
+ evolution: Your::Evolution.new,
236
+ selection: Your::Selection.new,
237
+ combination: Your::Combination.new,
238
+ mutation: Your::Mutation.new
239
+ )
240
+ ```
142
241
 
143
- ### Evolvable Population Hooks
242
+ Evolve your population:
144
243
 
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.
244
+ ```ruby
245
+ population.evolve(count: 20) # Run for 20 generations
246
+ population.evolve_to_goal # Run until the current goal is met
247
+ population.evolve_to_goal(0.0) # Run until a specific goal is met
248
+ population.evolve_forever # Run indefinitely, ignoring any goal
249
+ population.evolve_selected([...]) # Use a custom subset of evolvables
250
+ ```
146
251
 
147
- 1. `.before_evaluation(population)` - Runs before evaluation.
252
+ Create new evolvables:
148
253
 
149
- 2. `.before_evolution(population)`- Runs after evaluation and before evolution.
254
+ ```ruby
255
+ new = population.new_evolvable
256
+ many = population.new_evolvables(count: 10)
257
+ with_genome = population.new_evolvable(genome: another.genome)
258
+ ```
150
259
 
151
- 3. `.after_evolution(population)` - Runs after evolution.
260
+ Customize the evolution lifecycle by implementing hooks:
152
261
 
262
+ ```ruby
263
+ def self.before_evaluation(pop); end
264
+ def self.before_evolution(pop); end
265
+ def self.after_evolution(pop); end
266
+ ```
153
267
 
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.
268
+ Evaluate progress:
155
269
 
156
270
  ```ruby
157
- class HelloWorld
158
- include Evolvable
271
+ best = population.best_evolvable if population.met_goal?
272
+ ```
159
273
 
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
165
274
 
166
- # ...
275
+ [Population Documentation](https://mattruzicka.github.io/evolvable/Evolvable/Population)
167
276
 
168
- def to_s
169
- @to_s ||= genes.join
170
- end
277
+ ## Evaluation
171
278
 
172
- # ...
173
- end
174
- ```
279
+ Evaluation sorts evolvables based on their fitness and provides mechanisms to
280
+ change the goal type and value (fitness goal). Goals define the success criteria
281
+ for evolution. They allow you to specify what your population is evolving toward,
282
+ whether it's maximizing a value, minimizing a value, or seeking a specific value.
283
+
284
+ **How It Works**
175
285
 
176
- Finally we can **evolve the population with the `Evolvable::Population#evolve` instance method**.
286
+ 1. Your evolvable class defines a `#fitness` method that returns a
287
+ [Comparable](https://docs.ruby-lang.org/en//3.4/Comparable.html) object.
288
+ - Preferably a numeric value like an integer or float.
289
+
290
+ 2. During evolution, evolvables are sorted by your goal's fitness interpretation
291
+ - The default goal type is `:maximize`, see goal types below for other options
292
+
293
+ 3. If a goal value is specified, evolution will stop when it is met
294
+
295
+ **Goal Types**
296
+
297
+ - Maximize (higher is better)
177
298
 
178
299
  ```ruby
179
- population.evolve
300
+ robots = Robot.new_population(evaluation: :maximize) # Defaults to infinity
301
+ robots.evolve_to_goal(100) # Evolve until fitness reaches 100+
302
+
303
+ # Same as above
304
+ Robot.new_population(evaluation: { maximize: 100 }).evolve_to_goal
180
305
  ```
181
306
 
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).
307
+ - Minimize (lower is better)
183
308
 
184
- ## Concepts
309
+ ```ruby
310
+ errors = ErrorModel.new_population(evaluation: :minimize) # Defaults to -infinity
311
+ errors.evolve_to_goal(0.01) # Evolve until error rate reaches 0.01 or less
185
312
 
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).
313
+ # Same as above
314
+ ErrorModel.new_population(evaluation: { minimize: 0.01 }).evolve_to_goal
315
+ ```
187
316
 
188
- The following concept map depicts how genes flow through populations.
317
+ - Equalize (closer to target is better)
189
318
 
190
- ![Concept Map](https://github.com/mattruzicka/evolvable/raw/main/examples/images/diagram.png)
319
+ ```ruby
320
+ targets = TargetMatcher.new_population(evaluation: :equalize) # Defaults to 0
321
+ targets.evolve_to_goal(42) # Evolve until we match the target value
191
322
 
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.
323
+ # Same as above
324
+ TargetMatcher.new_population(evaluation: { equalize: 42 }).evolve_to_goal
325
+ ```
193
326
 
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.
199
327
 
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.
328
+ **Custom Goals**
207
329
 
330
+ You can create custom goals by subclassing `Evolvable::Goal` and implementing:
331
+ - `evaluate(evolvable)`: Return a value that for sorting evolvables
332
+ - `met?(evolvable)`: Returns true when the goal value is reached
333
+
334
+
335
+ Example goal implementation that prioritizes evolvables with fitness values within a specific range:
208
336
 
209
337
  ```ruby
210
- # This gene generates a random hexidecimal color code for use by evolvables.
338
+ class YourRangeGoal < Evolvable::Goal
339
+ def value
340
+ @value ||= 0..100
341
+ end
211
342
 
212
- require 'securerandom'
343
+ def evaluate(evolvable)
344
+ return 1 if value.include?(evolvable.fitness)
213
345
 
214
- class ColorGene
215
- include Evolvable::Gene
346
+ min, max = value.minmax
347
+ -[(min - evolvable.fitness).abs, (max - evolvable.fitness).abs].min
348
+ end
216
349
 
217
- def hex_code
218
- @hex_code ||= SecureRandom.hex(3)
350
+ def met?(evolvable)
351
+ value.include?(evolvable.fitness)
219
352
  end
220
353
  end
221
354
  ```
222
355
 
223
356
 
224
- [Documentation](https://rubydoc.info/github/mattruzicka/evolvable/Evolvable/Gene)
357
+ [Evaluation Documentation](https://mattruzicka.github.io/evolvable/Evolvable/Evaluation)
225
358
 
226
- ## Populations
227
- Population objects are responsible for generating and evolving instances.
228
- They orchestrate all the other Evolvable objects to do so.
359
+ [Goal Documentation](https://mattruzicka.github.io/evolvable/Evolvable/Goal)
360
+
361
+ ## Evolution
362
+
363
+ **Evolution** moves a population from one generation to the next.
364
+ It runs in three steps: selection, combination, and mutation.
365
+ You can swap out any step with your own strategy.
366
+
367
+ Default pipeline:
368
+ 1. **Selection** – keep the most fit evolvables
369
+ 2. **Combination** – create offspring by recombining genes
370
+ 3. **Mutation** – add random variation to preserve diversity
229
371
 
230
- Populations can be initialized and re-initialized with a number of useful
231
- parameters.
232
372
 
373
+ [Evolution Documentation](https://mattruzicka.github.io/evolvable/Evolvable/Evolution)
374
+
375
+ ## Selection
376
+
377
+ Selection determines which evolvables will serve as parents for the next
378
+ generation. You can control the selection process in several ways:
379
+
380
+ Set the selection size during population initialization:
233
381
 
234
382
  ```ruby
235
- # TODO: initialize a population with all supported parameters
383
+ population = MyEvolvable.new_population(
384
+ selection: { size: 3 }
385
+ )
236
386
  ```
237
387
 
388
+ Adjust the selection size after initialization:
238
389
 
239
- [Documentation](https://rubydoc.info/github/mattruzicka/evolvable/Evolvable/Population)
390
+ ```ruby
391
+ population.selection_size = 4
392
+ ```
240
393
 
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".
394
+ Manually assign the selected evolvables:
245
395
 
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.
396
+ ```ruby
397
+ population.selected_evolvables = [evolvable1, evolvable2]
398
+ ```
250
399
 
400
+ Or evolve a custom selection directly:
251
401
 
252
402
  ```ruby
253
- # TODO: Show how to add/change population's evaluation object
403
+ population.evolve_selected([evolvable1, evolvable2])
404
+ ```
405
+
406
+ This flexibility lets you implement custom selection strategies,
407
+ overriding or augmenting the built-in behavior.
408
+
409
+
410
+ [Selection Documentation](https://mattruzicka.github.io/evolvable/Evolvable/Selection)
411
+
412
+ ## Combination
413
+
414
+ Combination is the process of creating new evolvables by mixing the genes
415
+ of selected parents. This step drives the creation of the next generation
416
+ by recombining traits in novel ways.
254
417
 
255
- # The goal value can also be assigned via as argument to `Evolvable::Population#evolve`
256
- population.evolve(goal_value: 1000)
418
+ You can choose from several built-in combination strategies or implement your own.
419
+ By default, Evolvable uses `Evolvable::GeneCombination`, which delegates
420
+ gene-level behavior to individual gene classes.
421
+
422
+ To define custom combination logic for a gene type, implement:
423
+
424
+ ```ruby
425
+ YourGeneClass.combine(parent_1_gene, parent_2_gene)
257
426
  ```
258
427
 
259
428
 
260
- [Documentation](https://rubydoc.info/github/mattruzicka/evolvable/Evolvable/Evaluation)
429
+ [Combination Documentation](https://mattruzicka.github.io/evolvable/Evolvable/Combination)
261
430
 
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.
431
+ **Point Crossover**
267
432
 
433
+ A classic genetic algorithm strategy that performs single or multi-point crossover
434
+ by selecting random positions in the genome and swapping gene segments between parents.
268
435
 
269
- [Documentation](https://rubydoc.info/github/mattruzicka/evolvable/Evolvable/Evolution)
436
+ - **Single-point crossover (default):** Swaps all genes after a randomly chosen position.
437
+ - **Multi-point crossover:** Alternates segments between multiple randomly chosen points.
270
438
 
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.
439
+ Best for:
440
+ - Preserving beneficial gene blocks
441
+ - Problems where related traits are located near each other
275
442
 
276
- Only two evolvables are selected as parents for each generation by default.
277
- The selection size is configurable.
443
+ Set your population to use this strategy during initialization with:
278
444
 
445
+ ```ruby
446
+ population = MyEvolvable.new_population(
447
+ combination: Evolvable::PointCrossover.new(points_count: 2)
448
+ )
449
+ ```
450
+
451
+ Or update an existing population:
279
452
 
280
453
  ```ruby
281
- # TODO: Show how to add/change population's selection object
454
+ population.combination = Evolvable::PointCrossover.new(points_count: 3)
282
455
  ```
283
456
 
284
457
 
458
+ [PointCrossover Documentation](https://mattruzicka.github.io/evolvable/Evolvable/PointCrossover)
285
459
 
286
- [Documentation](https://rubydoc.info/github/mattruzicka/evolvable/Evolvable/Selection)
460
+ **Uniform Crossover**
287
461
 
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.
462
+ Chooses genes independently at each position, selecting randomly from either
463
+ parent with equal probability. No segments are preserved—each gene is treated
464
+ in isolation.
465
+
466
+ Best for:
467
+ - Problems where gene order doesn't matter
468
+ - High genetic diversity and exploration
469
+ - Complex interdependencies across traits
292
470
 
293
- You may choose from a selection of combination objects or implement your own.
294
- The default combination object is `Evolvable::GeneCombination`.
471
+ Uniform crossover is especially effective when good traits are scattered across the genome.
295
472
 
473
+ Set your population to use this strategy during initialization with:
296
474
 
297
- [Documentation](https://rubydoc.info/github/mattruzicka/evolvable/Evolvable/Combination)
475
+ ```ruby
476
+ population = MyEvolvable.new_population(
477
+ combination: Evolvable::UniformCrossover.new
478
+ )
479
+ ```
480
+
481
+ Or update an existing population:
482
+
483
+ ```ruby
484
+ population.combination = Evolvable::UniformCrossover.new
485
+ ```
486
+
487
+
488
+ [UniformCrossover Documentation](https://mattruzicka.github.io/evolvable/Evolvable/UniformCrossover)
298
489
 
299
490
  ## 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.
304
491
 
305
- Mutation frequency is configurable using the `probability` and `rate`
306
- parameters.
492
+ Mutation introduces genetic variation by randomly replacing genes with new
493
+ ones. This helps the population explore new areas of the solution space
494
+ and prevents premature convergence on suboptimal solutions.
495
+
496
+ Mutation is controlled by two key parameters:
497
+ - **probability**: Likelihood that an individual will undergo mutation (range: 0.0–1.0)
498
+ - **rate**: Fraction of genes to mutate within those individuals (range: 0.0–1.0)
499
+
500
+ A typical strategy is to start with higher mutation to encourage exploration:
501
+
502
+ ```ruby
503
+ population = MyEvolvable.new_population(
504
+ mutation: { probability: 0.4, rate: 0.2 }
505
+ )
506
+ ```
507
+
508
+ Then later reduce the mutation rate to focus on refinement and convergence:
509
+
510
+ ```ruby
511
+ population.mutation_probability = 0.1
512
+ population.mutation_rate = 0.05
513
+ ```
514
+
515
+
516
+ [Mutation Documentation](https://mattruzicka.github.io/evolvable/Evolvable/Mutation)
517
+
518
+ ## Gene Clusters
519
+
520
+ Gene clusters group related genes into reusable components that can be applied
521
+ to multiple evolvable classes. This promotes clean organization, eliminates
522
+ naming conflicts, and simplifies gene access.
307
523
 
524
+ **Benefits:**
525
+ - Reuse gene groups across multiple evolvables
526
+ - Prevent name collisions via automatic namespacing
527
+ - Treat clusters as structured subcomponents of a genome
528
+ - Access all genes in a cluster with a single method call
529
+
530
+ The `ColorPaletteCluster` below defines a group of genes commonly used for styling themes:
531
+
532
+ ```ruby
533
+ class ColorPaletteCluster
534
+ include Evolvable::GeneCluster
535
+
536
+ gene :primary, type: 'ColorGene', count: 1
537
+ gene :secondary, type: 'ColorGene', count: 1
538
+ gene :accent, type: 'ColorGene', count: 1
539
+ gene :neutral, type: 'ColorGene', count: 1
540
+ end
541
+ ```
542
+
543
+ Use the `cluster` macro to apply the cluster to your evolvable class:
544
+
545
+ ```ruby
546
+ class Theme
547
+ include Evolvable
548
+
549
+ cluster :colors, type: ColorPaletteCluster
550
+
551
+ def inspect_colors
552
+ colors.join(", ")
553
+ end
554
+ end
555
+ ```
556
+
557
+ When a cluster is applied, its genes are automatically namespaced with the cluster name:
558
+ - Access the full group: `theme.colors` → returns all genes in the colors cluster
559
+ - Access individual genes: `theme.find_gene("colors-primary")`
560
+
561
+
562
+ [GeneCluster Documentation](https://mattruzicka.github.io/evolvable/Evolvable/GeneCluster)
563
+
564
+ ## Community
565
+
566
+ The `Community` module provides a framework for coordinating multiple evolvable populations
567
+ under a unified interface. Each population represents a distinct type of evolvable, and
568
+ each key returns a single evolvable instance drawn from its corresponding population.
569
+
570
+ Communities are ideal for simulations or systems where different components evolve
571
+ in parallel but interact as part of a larger whole - such as ecosystems, design systems,
572
+ or modular agents. Evolvables from different populations can co-evolve, influencing each other's fitness.
573
+
574
+ Use the `evolvable_community` macro to declare the set of named populations in the community.
575
+ Each population will have a corresponding method (e.g., `fish_1`, `plant`, `shrimp`) that
576
+ returns a single evolvable instance. You can evolve all populations together using the
577
+ `evolve` method, or per population.
578
+
579
+ **Key Features**
580
+ - Define a community composed of named populations
581
+ - Automatically generate accessors for each evolvable instance
582
+ - Coordinate evolution across populations through a shared interface
583
+ - Evolve all populations in a single call with `evolve(...)`
584
+
585
+ This `FishTank` example sets up a community with four named populations:
308
586
 
309
587
  ```ruby
310
- # Show how to initialize/assign population with a specific mutation object
588
+ class FishTank
589
+ include Evolvable::Community
590
+
591
+ evolvable_community fish_1: Fish,
592
+ fish_2: Fish,
593
+ plant: AquariumPlant,
594
+ shrimp: CleanerShrimp
595
+
596
+ def describe_tank
597
+ puts "🐟 Fish 1: #{fish_1.name} (#{fish_1.color})"
598
+ puts "🐟 Fish 2: #{fish_2.name} (#{fish_2.color})"
599
+ puts "🌿 Plant: #{plant.name} (#{plant.color})"
600
+ puts "🦐 Shrimp: #{shrimp.name} (#{shrimp.color})"
601
+ end
602
+ end
311
603
  ```
312
604
 
605
+ Initialize the community, describe the tank, and evolve each population:
606
+
607
+ ```ruby
608
+ tank = FishTank.new_community
609
+ tank.describe_tank
610
+ tank.evolve
611
+ ```
612
+
613
+
614
+ [Community Documentation](https://mattruzicka.github.io/evolvable/Evolvable/Community)
615
+
616
+ ## Serialization
313
617
 
314
- [Documentation](https://rubydoc.info/github/mattruzicka/evolvable/Evolvable/Mutation)
618
+ Evolvable supports saving and restoring the state of both populations
619
+ and individual evolvable instances through a built-in `Serializer`.
620
+ By default, it uses Ruby's `Marshal` class for fast, portable binary serialization.
315
621
 
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.
622
+ Serialization is useful for:
623
+ - Saving progress during long-running evolution
624
+ - Storing champion solutions for later reuse
625
+ - Transferring evolved populations between systems
626
+ - Creating checkpoints you can revert to
323
627
 
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.
628
+ Both `Population` and individual evolvables expose `dump` and `load` methods
629
+ that use the `Serializer` internally.
331
630
 
631
+ Save a population to a file:
332
632
 
333
633
  ```ruby
334
- # All 9 of these example definitions are equivalent
634
+ population = YourEvolvable.new_population
635
+ population.evolve(count: 100)
636
+ File.write("population.marshal", population.dump)
637
+ ```
638
+
639
+ Restore and continue evolution:
640
+
641
+ ```ruby
642
+ data = File.read("population.marshal")
643
+ restored = Evolvable::Population.load(data)
644
+ restored.evolve(count: 100)
645
+ ```
335
646
 
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 } }
647
+ Save an individual evolvable's genome:
340
648
 
341
- # Array of arrays syntax
342
- [[:chars, 'CharGene', 1..100]]
343
- [['chars', 'CharGene', 1..100]]
344
- [['CharGene', 1..100]]
649
+ ```ruby
650
+ best = restored.best_evolvable
651
+ File.write("champion.marshal", best.dump_genome)
652
+ ```
653
+
654
+ Restore genome into a new evolvable:
345
655
 
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]
656
+ ```ruby
657
+ raw = File.read("champion.marshal")
658
+ champion = YourEvolvable.new_evolvable
659
+ champion.load_genome(raw)
350
660
  ```
351
661
 
352
662
 
353
- [Documentation](https://rubydoc.info/github/mattruzicka/evolvable/Evolvable/SearchSpace)
663
+ [Serializer Documentation](https://mattruzicka.github.io/evolvable/Evolvable/Serializer)
354
664
 
355
665
  ## Contributing
356
666
 
357
- Bug reports and pull requests are welcome on GitHub at https://github.com/mattruzicka/evolvable.
358
-
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).
667
+ Bug reports and pull requests are welcome on GitHub at https://github.com/mattruzicka/evolvable.