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.
- checksums.yaml +4 -4
- data/.yardopts +2 -0
- data/CHANGELOG.md +24 -0
- data/Gemfile +2 -2
- data/Gemfile.lock +44 -25
- data/README.md +498 -190
- data/README_YARD.md +85 -166
- data/bin/console +10 -19
- data/docs/Evolvable/ClassMethods.html +1233 -0
- data/docs/Evolvable/Community/ClassMethods.html +708 -0
- data/docs/Evolvable/Community.html +1342 -0
- data/docs/Evolvable/CountGene.html +886 -0
- data/docs/Evolvable/EqualizeGoal.html +347 -0
- data/docs/Evolvable/Error.html +134 -0
- data/docs/Evolvable/Evaluation.html +773 -0
- data/docs/Evolvable/Evolution.html +616 -0
- data/docs/Evolvable/Gene/ClassMethods.html +413 -0
- data/docs/Evolvable/Gene.html +522 -0
- data/docs/Evolvable/GeneCluster/ClassMethods.html +431 -0
- data/docs/Evolvable/GeneCluster.html +280 -0
- data/docs/Evolvable/GeneCombination.html +515 -0
- data/docs/Evolvable/GeneSpace.html +619 -0
- data/docs/Evolvable/Genome.html +1070 -0
- data/docs/Evolvable/Goal.html +500 -0
- data/docs/Evolvable/MaximizeGoal.html +348 -0
- data/docs/Evolvable/MinimizeGoal.html +348 -0
- data/docs/Evolvable/Mutation.html +729 -0
- data/docs/Evolvable/PointCrossover.html +444 -0
- data/docs/Evolvable/Population.html +2826 -0
- data/docs/Evolvable/RigidCountGene.html +501 -0
- data/docs/Evolvable/Selection.html +594 -0
- data/docs/Evolvable/Serializer.html +293 -0
- data/docs/Evolvable/UniformCrossover.html +286 -0
- data/docs/Evolvable.html +1619 -0
- data/docs/_index.html +341 -0
- data/docs/class_list.html +54 -0
- data/docs/css/common.css +1 -0
- data/docs/css/full_list.css +58 -0
- data/docs/css/style.css +503 -0
- data/docs/file.README.html +750 -0
- data/docs/file_list.html +59 -0
- data/docs/frames.html +22 -0
- data/docs/index.html +750 -0
- data/docs/js/app.js +344 -0
- data/docs/js/full_list.js +242 -0
- data/docs/js/jquery.js +4 -0
- data/docs/method_list.html +1302 -0
- data/docs/top-level-namespace.html +110 -0
- data/evolvable.gemspec +6 -6
- data/examples/ascii_art.rb +5 -9
- data/examples/stickman.rb +25 -33
- data/{examples/hello_world.rb → exe/hello_evolvable_world} +46 -30
- data/lib/evolvable/community.rb +190 -0
- data/lib/evolvable/count_gene.rb +65 -0
- data/lib/evolvable/equalize_goal.rb +2 -9
- data/lib/evolvable/evaluation.rb +113 -14
- data/lib/evolvable/evolution.rb +38 -15
- data/lib/evolvable/gene.rb +124 -25
- data/lib/evolvable/gene_cluster.rb +106 -0
- data/lib/evolvable/gene_combination.rb +57 -16
- data/lib/evolvable/gene_space.rb +111 -0
- data/lib/evolvable/genome.rb +32 -4
- data/lib/evolvable/goal.rb +19 -24
- data/lib/evolvable/maximize_goal.rb +2 -9
- data/lib/evolvable/minimize_goal.rb +3 -9
- data/lib/evolvable/mutation.rb +87 -41
- data/lib/evolvable/point_crossover.rb +24 -4
- data/lib/evolvable/population.rb +258 -84
- data/lib/evolvable/rigid_count_gene.rb +36 -0
- data/lib/evolvable/selection.rb +68 -14
- data/lib/evolvable/serializer.rb +46 -0
- data/lib/evolvable/uniform_crossover.rb +30 -6
- data/lib/evolvable/version.rb +2 -1
- data/lib/evolvable.rb +268 -107
- metadata +57 -36
- data/examples/images/diagram.png +0 -0
- data/exe/hello +0 -16
- data/lib/evolvable/error/undefined_method.rb +0 -7
- data/lib/evolvable/search_space.rb +0 -181
data/README.md
CHANGED
@@ -1,11 +1,36 @@
|
|
1
|
-
# Evolvable
|
1
|
+
# Evolvable 🧬
|
2
2
|
|
3
|
-
[](https://badge.fury.io/rb/evolvable)
|
3
|
+
[](https://badge.fury.io/rb/evolvable)
|
4
4
|
|
5
|
-
|
5
|
+
**Code Version: 2.0.0**
|
6
6
|
|
7
|
-
|
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
|
-
* [
|
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
|
-
|
31
|
-
|
32
|
-
|
33
|
-
the `#
|
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
|
-
|
68
|
+
```ruby
|
69
|
+
# Step 1
|
70
|
+
class Shirt
|
71
|
+
include Evolvable
|
36
72
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
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
|
-
|
82
|
+
# Step 4
|
83
|
+
population = Shirt.new_population(size: 10)
|
84
|
+
population.evolvables.each { |shirt| shirt.fitness = style_rating }
|
85
|
+
```
|
44
86
|
|
45
|
-
|
87
|
+
You are free to tailor the genes to your needs and find a style that suits you.
|
46
88
|
|
47
|
-
|
89
|
+
The `ColorGene` could be as simple as this:
|
48
90
|
|
49
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
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
|
142
|
+
class Robot
|
86
143
|
include Evolvable
|
87
144
|
|
88
|
-
|
89
|
-
|
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
|
-
|
154
|
+
**Gene Count**
|
95
155
|
|
96
|
-
|
156
|
+
You can control how many copies of a gene are created using the `count:` parameter:
|
97
157
|
|
98
|
-
|
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
|
-
|
162
|
+
Evolves melody length:
|
101
163
|
|
102
164
|
```ruby
|
103
|
-
|
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.
|
107
|
-
|
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
|
-
|
111
|
-
|
183
|
+
attr_writer :value
|
184
|
+
|
185
|
+
def value
|
186
|
+
@value ||= rand(1..100)
|
112
187
|
end
|
113
188
|
end
|
114
189
|
```
|
115
190
|
|
116
|
-
|
191
|
+
**Design Patterns**
|
192
|
+
|
193
|
+
Effective gene design typically follows these principles:
|
117
194
|
|
118
|
-
|
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
|
-
|
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
|
-
|
206
|
+
[Gene Documentation](https://mattruzicka.github.io/evolvable/Evolvable/Gene)
|
125
207
|
|
126
|
-
|
208
|
+
## Populations
|
127
209
|
|
128
|
-
|
210
|
+
Populations orchestrate the evolutionary process through four key components:
|
129
211
|
|
130
|
-
|
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
|
-
|
217
|
+
**Features**:
|
218
|
+
|
219
|
+
Initialize a population with default or custom parameters:
|
133
220
|
|
134
221
|
```ruby
|
135
|
-
population =
|
136
|
-
|
137
|
-
|
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
|
-
|
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
|
-
|
242
|
+
Evolve your population:
|
144
243
|
|
145
|
-
|
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
|
-
|
252
|
+
Create new evolvables:
|
148
253
|
|
149
|
-
|
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
|
-
|
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
|
-
|
268
|
+
Evaluate progress:
|
155
269
|
|
156
270
|
```ruby
|
157
|
-
|
158
|
-
|
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
|
-
|
169
|
-
@to_s ||= genes.join
|
170
|
-
end
|
277
|
+
## Evaluation
|
171
278
|
|
172
|
-
|
173
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
307
|
+
- Minimize (lower is better)
|
183
308
|
|
184
|
-
|
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
|
-
|
313
|
+
# Same as above
|
314
|
+
ErrorModel.new_population(evaluation: { minimize: 0.01 }).evolve_to_goal
|
315
|
+
```
|
187
316
|
|
188
|
-
|
317
|
+
- Equalize (closer to target is better)
|
189
318
|
|
190
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
338
|
+
class YourRangeGoal < Evolvable::Goal
|
339
|
+
def value
|
340
|
+
@value ||= 0..100
|
341
|
+
end
|
211
342
|
|
212
|
-
|
343
|
+
def evaluate(evolvable)
|
344
|
+
return 1 if value.include?(evolvable.fitness)
|
213
345
|
|
214
|
-
|
215
|
-
|
346
|
+
min, max = value.minmax
|
347
|
+
-[(min - evolvable.fitness).abs, (max - evolvable.fitness).abs].min
|
348
|
+
end
|
216
349
|
|
217
|
-
def
|
218
|
-
|
350
|
+
def met?(evolvable)
|
351
|
+
value.include?(evolvable.fitness)
|
219
352
|
end
|
220
353
|
end
|
221
354
|
```
|
222
355
|
|
223
356
|
|
224
|
-
[Documentation](https://
|
357
|
+
[Evaluation Documentation](https://mattruzicka.github.io/evolvable/Evolvable/Evaluation)
|
225
358
|
|
226
|
-
|
227
|
-
|
228
|
-
|
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
|
-
|
383
|
+
population = MyEvolvable.new_population(
|
384
|
+
selection: { size: 3 }
|
385
|
+
)
|
236
386
|
```
|
237
387
|
|
388
|
+
Adjust the selection size after initialization:
|
238
389
|
|
239
|
-
|
390
|
+
```ruby
|
391
|
+
population.selection_size = 4
|
392
|
+
```
|
240
393
|
|
241
|
-
|
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
|
-
|
247
|
-
|
248
|
-
|
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
|
-
|
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
|
-
|
256
|
-
|
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://
|
429
|
+
[Combination Documentation](https://mattruzicka.github.io/evolvable/Evolvable/Combination)
|
261
430
|
|
262
|
-
|
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
|
-
|
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
|
-
|
272
|
-
|
273
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
460
|
+
**Uniform Crossover**
|
287
461
|
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
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
|
-
|
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
|
-
|
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
|
306
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
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
|
-
|
325
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
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
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
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://
|
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.
|