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/lib/evolvable/population.rb
CHANGED
@@ -3,55 +3,110 @@
|
|
3
3
|
module Evolvable
|
4
4
|
#
|
5
5
|
# @readme
|
6
|
-
#
|
7
|
-
# They orchestrate all the other Evolvable objects to do so.
|
6
|
+
# Populations orchestrate the evolutionary process through four key components:
|
8
7
|
#
|
9
|
-
#
|
10
|
-
#
|
8
|
+
# 1. **Evaluation**: Sorts evolvable instances by fitness
|
9
|
+
# 2. **Selection**: Chooses parents for combination
|
10
|
+
# 3. **Combination**: Creates new evolvables from selected parents
|
11
|
+
# 4. **Mutation**: Introduces variation to maintain genetic diversity
|
12
|
+
#
|
13
|
+
# **Features**:
|
14
|
+
#
|
15
|
+
# Initialize a population with default or custom parameters:
|
16
|
+
#
|
17
|
+
# ```ruby
|
18
|
+
# population = YourEvolvable.new_population(
|
19
|
+
# size: 50,
|
20
|
+
# evaluation: { equalize: 0 },
|
21
|
+
# selection: { size: 10 },
|
22
|
+
# mutation: { probability: 0.2, rate: 0.02 }
|
23
|
+
# )
|
24
|
+
# ```
|
25
|
+
#
|
26
|
+
# Or inject fully customized strategy objects:
|
27
|
+
#
|
28
|
+
# ```ruby
|
29
|
+
# population = YourEvolvable.new_population(
|
30
|
+
# evaluation: Your::Evaluation.new,
|
31
|
+
# evolution: Your::Evolution.new,
|
32
|
+
# selection: Your::Selection.new,
|
33
|
+
# combination: Your::Combination.new,
|
34
|
+
# mutation: Your::Mutation.new
|
35
|
+
# )
|
36
|
+
# ```
|
37
|
+
#
|
38
|
+
# Evolve your population:
|
39
|
+
#
|
40
|
+
# ```ruby
|
41
|
+
# population.evolve(count: 20) # Run for 20 generations
|
42
|
+
# population.evolve_to_goal # Run until the current goal is met
|
43
|
+
# population.evolve_to_goal(0.0) # Run until a specific goal is met
|
44
|
+
# population.evolve_forever # Run indefinitely, ignoring any goal
|
45
|
+
# population.evolve_selected([...]) # Use a custom subset of evolvables
|
46
|
+
# ```
|
47
|
+
#
|
48
|
+
# Create new evolvables:
|
49
|
+
#
|
50
|
+
# ```ruby
|
51
|
+
# new = population.new_evolvable
|
52
|
+
# many = population.new_evolvables(count: 10)
|
53
|
+
# with_genome = population.new_evolvable(genome: another.genome)
|
54
|
+
# ```
|
55
|
+
#
|
56
|
+
# Customize the evolution lifecycle by implementing hooks:
|
57
|
+
#
|
58
|
+
# ```ruby
|
59
|
+
# def self.before_evaluation(pop); end
|
60
|
+
# def self.before_evolution(pop); end
|
61
|
+
# def self.after_evolution(pop); end
|
62
|
+
# ```
|
63
|
+
#
|
64
|
+
# Evaluate progress:
|
65
|
+
#
|
66
|
+
# ```ruby
|
67
|
+
# best = population.best_evolvable if population.met_goal?
|
68
|
+
# ```
|
11
69
|
#
|
12
|
-
# @example
|
13
|
-
# # TODO: initialize a population with all supported parameters
|
14
70
|
class Population
|
15
71
|
extend Forwardable
|
16
72
|
|
73
|
+
#
|
74
|
+
# Loads a population from serialized data.
|
75
|
+
#
|
76
|
+
# @param data [String] The serialized population data
|
77
|
+
# @return [Evolvable::Population] The loaded population
|
78
|
+
#
|
17
79
|
def self.load(data)
|
18
80
|
dump_attrs = Serializer.load(data)
|
19
81
|
new(**dump_attrs)
|
20
82
|
end
|
21
83
|
|
84
|
+
#
|
22
85
|
# Initializes an Evolvable::Population.
|
23
|
-
#
|
24
|
-
#
|
25
|
-
#
|
26
|
-
#
|
27
|
-
#
|
28
|
-
#
|
29
|
-
#
|
30
|
-
#
|
31
|
-
#
|
32
|
-
#
|
33
|
-
#
|
34
|
-
#
|
35
|
-
# [
|
36
|
-
#
|
37
|
-
#
|
38
|
-
#
|
39
|
-
|
40
|
-
# towards Float::INFINITY. See [evaluation](#evaluation-1)
|
41
|
-
# #### instances
|
42
|
-
# Defaults to initializing a `size` number of `evolvable_class`
|
43
|
-
# instances using the `search_space` object. Any given instances
|
44
|
-
# are assigned, but if given less than `size`, more will be initialized.
|
45
|
-
#
|
46
|
-
def initialize(evolvable_type: nil,
|
47
|
-
evolvable_class: nil, # Deprecated
|
86
|
+
#
|
87
|
+
# @param evolvable_type [Class] Required. The class of evolvables to create
|
88
|
+
# @param id [String, nil] Optional identifier, not used by Evolvable internally
|
89
|
+
# @param name [String, nil] Optional name, not used by Evolvable internally
|
90
|
+
# @param size [Integer] The number of instances in the population (default: 0)
|
91
|
+
# @param evolutions_count [Integer] The number of evolutions completed (default: 0)
|
92
|
+
# @param gene_space [Evolvable::GeneSpace, nil] The gene space for initializing evolvables
|
93
|
+
# @param parent_evolvables [Array<Evolvable>] Parent evolvables for breeding the next generation
|
94
|
+
# @param selected_evolvables [Array<Evolvable>] Evolvables selected for combinations
|
95
|
+
# @param evaluation [Evolvable::Evaluation, Hash] The evaluation strategy
|
96
|
+
# @param evolution [Evolvable::Evolution, Hash] The evolution strategy
|
97
|
+
# @param selection [Evolvable::Selection, Hash, nil] Optional selection strategy
|
98
|
+
# @param combination [Evolvable::Combination, Hash, nil] Optional combination strategy
|
99
|
+
# @param mutation [Evolvable::Mutation, Hash, nil] Optional mutation strategy
|
100
|
+
# @param evolvables [Array<Evolvable>] Initial evolvables (will be supplemented if fewer than size)
|
101
|
+
#
|
102
|
+
def initialize(evolvable_type:,
|
48
103
|
id: nil,
|
49
104
|
name: nil,
|
50
|
-
size:
|
105
|
+
size: 0,
|
51
106
|
evolutions_count: 0,
|
52
|
-
gene_space: nil,
|
53
|
-
search_space: nil,
|
107
|
+
gene_space: nil,
|
54
108
|
parent_evolvables: [],
|
109
|
+
selected_evolvables: [],
|
55
110
|
evaluation: Evaluation.new,
|
56
111
|
evolution: Evolution.new,
|
57
112
|
selection: nil,
|
@@ -59,35 +114,47 @@ module Evolvable
|
|
59
114
|
mutation: nil,
|
60
115
|
evolvables: [])
|
61
116
|
@id = id
|
62
|
-
@evolvable_type = evolvable_type
|
117
|
+
@evolvable_type = evolvable_type.is_a?(Class) ? evolvable_type : Object.const_get(evolvable_type)
|
63
118
|
@name = name
|
64
119
|
@size = size
|
65
120
|
@evolutions_count = evolutions_count
|
66
|
-
@
|
121
|
+
@gene_space = initialize_gene_space(gene_space)
|
67
122
|
@parent_evolvables = parent_evolvables
|
123
|
+
@selected_evolvables = selected_evolvables
|
68
124
|
self.evaluation = evaluation
|
69
125
|
@evolution = evolution
|
70
126
|
self.selection = selection if selection
|
71
127
|
self.combination = combination if combination
|
72
128
|
self.mutation = mutation if mutation
|
73
|
-
|
129
|
+
self.evolvables = evolvables || []
|
130
|
+
new_evolvables(count: @size - evolvables.count)
|
74
131
|
end
|
75
132
|
|
133
|
+
#
|
134
|
+
# Population properties
|
135
|
+
#
|
76
136
|
attr_accessor :id,
|
77
137
|
:evolvable_type,
|
78
138
|
:name,
|
79
139
|
:size,
|
80
140
|
:evolutions_count,
|
81
|
-
:
|
141
|
+
:gene_space,
|
82
142
|
:evolution,
|
83
143
|
:parent_evolvables,
|
144
|
+
:selected_evolvables,
|
84
145
|
:evolvables
|
85
146
|
|
86
|
-
|
147
|
+
#
|
148
|
+
# Delegate lifecycle hook methods to the evolvable type
|
149
|
+
#
|
150
|
+
def_delegators :evolvable_type,
|
87
151
|
:before_evaluation,
|
88
152
|
:before_evolution,
|
89
153
|
:after_evolution
|
90
154
|
|
155
|
+
#
|
156
|
+
# Delegate evolution component accessors to the evolution object
|
157
|
+
#
|
91
158
|
def_delegators :evolution,
|
92
159
|
:selection,
|
93
160
|
:selection=,
|
@@ -96,55 +163,51 @@ module Evolvable
|
|
96
163
|
:mutation,
|
97
164
|
:mutation=
|
98
165
|
|
166
|
+
#
|
167
|
+
# Convenience delegators for selection settings
|
168
|
+
#
|
99
169
|
def_delegator :selection, :size, :selection_size
|
100
170
|
def_delegator :selection, :size=, :selection_size=
|
101
171
|
|
172
|
+
#
|
173
|
+
# Convenience delegators for mutation settings
|
174
|
+
#
|
102
175
|
def_delegator :mutation, :rate, :mutation_rate
|
103
176
|
def_delegator :mutation, :rate=, :mutation_rate=
|
104
177
|
def_delegator :mutation, :probability, :mutation_probability
|
105
178
|
def_delegator :mutation, :probability=, :mutation_probability=
|
106
179
|
|
180
|
+
#
|
181
|
+
# The evaluation strategy used to assess evolvables
|
182
|
+
# @return [Evolvable::Evaluation] The current evaluation object
|
183
|
+
#
|
107
184
|
attr_reader :evaluation
|
108
185
|
|
186
|
+
#
|
187
|
+
# Sets the evaluation strategy.
|
188
|
+
#
|
189
|
+
# @param val [Evolvable::Evaluation, Hash] The new evaluation strategy or configuration hash
|
190
|
+
# @return [Evolvable::Evaluation] The updated evaluation object
|
191
|
+
#
|
109
192
|
def evaluation=(val)
|
110
193
|
@evaluation = Evolvable.new_object(@evaluation, val, Evaluation)
|
111
194
|
end
|
112
195
|
|
196
|
+
#
|
197
|
+
# Delegate goal accessors to the evaluation object
|
198
|
+
#
|
113
199
|
def_delegators :evaluation,
|
114
200
|
:goal,
|
115
201
|
:goal=
|
116
202
|
|
117
203
|
#
|
118
|
-
#
|
119
|
-
#
|
120
|
-
# #### count
|
121
|
-
# The number of evolutions to run. Expects a positive integer
|
122
|
-
# and Defaults to Float::INFINITY and will therefore run indefinitely
|
123
|
-
# unless a `goal_value` is specified.
|
124
|
-
# #### goal_value
|
125
|
-
# Assigns the goal object's value. Will continue running until any
|
126
|
-
# instance's value reaches it. See [evaluation](#evaluation-1)
|
127
|
-
#
|
128
|
-
# ### Evolvable::Population#best_instance
|
129
|
-
# Returns an instance with the value that is nearest to the goal value.
|
204
|
+
# Evolves the population for a specified number of generations or until the goal is achieved.
|
130
205
|
#
|
131
|
-
#
|
132
|
-
#
|
206
|
+
# @param count [Integer, Float] Number of generations to evolve. Use `Float::INFINITY` for indefinite evolution. Defaults to `1`.
|
207
|
+
# @param goal_value [Numeric, nil] Optional target value for the goal. If provided, evolution halts when this value is met.
|
208
|
+
# @return [Evolvable::Population] The evolved population.
|
133
209
|
#
|
134
|
-
|
135
|
-
# Initializes an instance for the population. Note that this method does not
|
136
|
-
# add the new instance to its array of instances.
|
137
|
-
#
|
138
|
-
# Keyword arguments:
|
139
|
-
#
|
140
|
-
# #### genes
|
141
|
-
# An array of initialized gene objects. Defaults to `[]`
|
142
|
-
# #### population_index
|
143
|
-
# Defaults to `nil` and expects an integer.
|
144
|
-
#
|
145
|
-
# See (EvolvableClass#population_index)[#evolvableclasspopulation_index-population_index]
|
146
|
-
#
|
147
|
-
def evolve(count: Float::INFINITY, goal_value: nil)
|
210
|
+
def evolve(count: 1, goal_value: nil)
|
148
211
|
goal.value = goal_value if goal_value
|
149
212
|
1.upto(count) do
|
150
213
|
before_evaluation(self)
|
@@ -158,61 +221,160 @@ module Evolvable
|
|
158
221
|
end
|
159
222
|
end
|
160
223
|
|
224
|
+
#
|
225
|
+
# Evolves the population until the goal is met.
|
226
|
+
#
|
227
|
+
# If no goal value is provided, it uses the currently defined `goal.value`.
|
228
|
+
#
|
229
|
+
# @param goal_value [Numeric, nil] Optional target value. Overrides the current goal if provided.
|
230
|
+
# @return [Evolvable::Population] The evolved population.
|
231
|
+
#
|
232
|
+
def evolve_to_goal(goal_value = nil)
|
233
|
+
goal_value ||= goal.value
|
234
|
+
evolve(count: Float::INFINITY, goal_value: goal_value)
|
235
|
+
end
|
236
|
+
|
237
|
+
#
|
238
|
+
# Evolves the population indefinitely, ignoring any goal.
|
239
|
+
#
|
240
|
+
# Clears any previously set `goal.value` to ensure evolution continues indefinitely.
|
241
|
+
#
|
242
|
+
# @return [Evolvable::Population] The evolved population.
|
243
|
+
#
|
244
|
+
def evolve_forever
|
245
|
+
goal.value = nil
|
246
|
+
evolve(count: Float::INFINITY)
|
247
|
+
end
|
248
|
+
|
249
|
+
#
|
250
|
+
# Evolves the population using a pre-selected set of evolvables.
|
251
|
+
# This allows for custom selection beyond the built-in selection strategies.
|
252
|
+
#
|
253
|
+
# @param selected_evolvables [Array<Evolvable>] The evolvables selected for combinations
|
254
|
+
# @return [Evolvable::Population] The evolved population
|
255
|
+
#
|
256
|
+
def evolve_selected(selected_evolvables)
|
257
|
+
self.selected_evolvables = selected_evolvables
|
258
|
+
before_evolution(self)
|
259
|
+
evolution.call(self)
|
260
|
+
self.evolutions_count += 1
|
261
|
+
after_evolution(self)
|
262
|
+
end
|
263
|
+
|
264
|
+
#
|
265
|
+
# Returns the best evolvable in the population according to the evaluation goal.
|
266
|
+
#
|
267
|
+
# @return [Evolvable] The best evolvable based on the current goal
|
268
|
+
#
|
161
269
|
def best_evolvable
|
162
270
|
evaluation.best_evolvable(self)
|
163
271
|
end
|
164
272
|
|
273
|
+
#
|
274
|
+
# Checks if the goal has been met by any evolvable in the population.
|
275
|
+
#
|
276
|
+
# @return [Boolean] True if the goal has been met, false otherwise
|
277
|
+
#
|
165
278
|
def met_goal?
|
166
279
|
evaluation.met_goal?(self)
|
167
280
|
end
|
168
281
|
|
282
|
+
#
|
283
|
+
# Creates a new evolvable instance with an optional genome.
|
284
|
+
# If no genome is provided and there are parent evolvables,
|
285
|
+
# a genome will be generated through combination.
|
286
|
+
#
|
287
|
+
# @param genome [Evolvable::Genome, nil] Optional genome for the new evolvable
|
288
|
+
# @return [Evolvable] A new evolvable instance
|
289
|
+
#
|
169
290
|
def new_evolvable(genome: nil)
|
170
291
|
return generate_evolvables(1).first unless genome || parent_evolvables.empty?
|
171
292
|
|
172
|
-
evolvable =
|
173
|
-
|
174
|
-
|
293
|
+
evolvable = evolvable_type.new_evolvable(population: self,
|
294
|
+
genome: genome || new_genome,
|
295
|
+
generation_index: @evolvables.count)
|
175
296
|
@evolvables << evolvable
|
176
297
|
evolvable
|
177
298
|
end
|
178
299
|
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
300
|
+
#
|
301
|
+
# Creates multiple new evolvable instances.
|
302
|
+
#
|
303
|
+
# @param count [Integer] The number of evolvables to create
|
304
|
+
# @return [Array<Evolvable>] The newly created evolvables
|
305
|
+
#
|
306
|
+
def new_evolvables(count:)
|
183
307
|
if parent_evolvables.empty?
|
184
|
-
Array.new(count) { new_evolvable(genome:
|
308
|
+
Array.new(count) { new_evolvable(genome: new_genome) }
|
185
309
|
else
|
186
|
-
|
310
|
+
evolvables = generate_evolvables(count)
|
311
|
+
@evolvables ||= []
|
312
|
+
@evolvables.concat evolvables
|
313
|
+
evolvables
|
187
314
|
end
|
188
315
|
end
|
189
316
|
|
317
|
+
#
|
318
|
+
# Creates a new genome from the gene space.
|
319
|
+
#
|
320
|
+
# @return [Evolvable::Genome] A new genome
|
321
|
+
#
|
322
|
+
def new_genome
|
323
|
+
gene_space.new_genome
|
324
|
+
end
|
325
|
+
|
326
|
+
#
|
327
|
+
# Resets the population by clearing all evolvables and creating new ones.
|
328
|
+
#
|
329
|
+
# @return [Array<Evolvable>] The new collection of evolvables
|
330
|
+
#
|
190
331
|
def reset_evolvables
|
191
332
|
self.evolvables = []
|
192
333
|
new_evolvables(count: size)
|
193
334
|
end
|
194
335
|
|
336
|
+
#
|
337
|
+
# Creates a cycle of parent genome pairs for combination.
|
338
|
+
# Shuffles parent genomes and creates combinations of two.
|
339
|
+
#
|
340
|
+
# @return [Enumerator] A cycle of parent genome pairs
|
341
|
+
#
|
195
342
|
def new_parent_genome_cycle
|
196
343
|
parent_evolvables.map(&:genome).shuffle!.combination(2).cycle
|
197
344
|
end
|
198
345
|
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
346
|
+
#
|
347
|
+
# Serializes the population to a string.
|
348
|
+
#
|
349
|
+
# @param only [Array<Symbol>, nil] Optional list of attributes to include
|
350
|
+
# @param except [Array<Symbol>, nil] Optional list of attributes to exclude
|
351
|
+
# @return [String] The serialized population
|
352
|
+
#
|
203
353
|
def dump(only: nil, except: nil)
|
204
354
|
Serializer.dump(dump_attrs(only: only, except: except))
|
205
355
|
end
|
206
356
|
|
357
|
+
#
|
358
|
+
# List of attributes that can be dumped during serialization.
|
359
|
+
#
|
360
|
+
# @return [Array<Symbol>] The dumpable attributes
|
361
|
+
#
|
207
362
|
DUMP_METHODS = %i[evolvable_type
|
208
363
|
id
|
209
364
|
name
|
210
365
|
size
|
211
366
|
evolutions_count
|
212
|
-
|
367
|
+
gene_space
|
213
368
|
evolution
|
214
369
|
evaluation].freeze
|
215
370
|
|
371
|
+
#
|
372
|
+
# Returns a hash of attributes for serialization.
|
373
|
+
#
|
374
|
+
# @param only [Array<Symbol>, nil] Optional list of attributes to include
|
375
|
+
# @param except [Array<Symbol>, nil] Optional list of attributes to exclude
|
376
|
+
# @return [Hash] The attributes hash for serialization
|
377
|
+
#
|
216
378
|
def dump_attrs(only: nil, except: nil)
|
217
379
|
attrs = {}
|
218
380
|
dump_methods = only || DUMP_METHODS
|
@@ -223,12 +385,24 @@ module Evolvable
|
|
223
385
|
|
224
386
|
private
|
225
387
|
|
226
|
-
|
227
|
-
|
388
|
+
#
|
389
|
+
# Initializes the gene space for the population.
|
390
|
+
#
|
391
|
+
# @param gene_space [Evolvable::GeneSpace, nil] Optional gene space configuration
|
392
|
+
# @return [Evolvable::GeneSpace] The initialized gene space
|
393
|
+
#
|
394
|
+
def initialize_gene_space(gene_space)
|
395
|
+
return GeneSpace.build(gene_space, evolvable_type) if gene_space
|
228
396
|
|
229
|
-
|
397
|
+
evolvable_type.new_gene_space
|
230
398
|
end
|
231
399
|
|
400
|
+
#
|
401
|
+
# Generates multiple evolvables through combination and mutation.
|
402
|
+
#
|
403
|
+
# @param count [Integer] The number of evolvables to generate
|
404
|
+
# @return [Array<Evolvable>] The generated evolvables
|
405
|
+
#
|
232
406
|
def generate_evolvables(count)
|
233
407
|
evolvables = combination.new_evolvables(self, count)
|
234
408
|
mutation.mutate_evolvables(evolvables)
|
@@ -1,17 +1,53 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Evolvable
|
4
|
+
#
|
5
|
+
# This class manages fixed gene counts in evolvable instances.
|
6
|
+
# Unlike the CountGene, the RigidCountGene maintains a constant number of genes
|
7
|
+
# that doesn't change during evolution. This is used when a gene is defined
|
8
|
+
# with a fixed integer for `count:` (e.g., `count: 5`).
|
9
|
+
#
|
10
|
+
# @example Define a chord with exactly 4 notes
|
11
|
+
# class Chord
|
12
|
+
# include Evolvable
|
13
|
+
#
|
14
|
+
# gene :notes, type: NoteGene, count: 4
|
15
|
+
#
|
16
|
+
# # The number of notes will always be 4, never changing during evolution
|
17
|
+
# def play
|
18
|
+
# puts "Playing chord with #{notes.count} notes"
|
19
|
+
# end
|
20
|
+
# end
|
21
|
+
#
|
4
22
|
class RigidCountGene
|
5
23
|
include Gene
|
6
24
|
|
25
|
+
#
|
26
|
+
# Combines two rigid count genes by always returning the first one.
|
27
|
+
# This ensures the count remains constant during evolution.
|
28
|
+
#
|
29
|
+
# @param gene_a [RigidCountGene] The first rigid count gene
|
30
|
+
# @param _gene_b [RigidCountGene] The second rigid count gene (unused)
|
31
|
+
# @return [RigidCountGene] The first count gene unchanged
|
32
|
+
#
|
7
33
|
def self.combine(gene_a, _gene_b)
|
8
34
|
gene_a
|
9
35
|
end
|
10
36
|
|
37
|
+
#
|
38
|
+
# Initializes a new RigidCountGene with a fixed count.
|
39
|
+
#
|
40
|
+
# @param count [Integer] The fixed number of genes to create
|
41
|
+
#
|
11
42
|
def initialize(count)
|
12
43
|
@count = count
|
13
44
|
end
|
14
45
|
|
46
|
+
#
|
47
|
+
# The fixed number of genes to create.
|
48
|
+
#
|
49
|
+
# @return [Integer] The gene count
|
50
|
+
#
|
15
51
|
attr_reader :count
|
16
52
|
end
|
17
53
|
end
|
data/lib/evolvable/selection.rb
CHANGED
@@ -3,15 +3,37 @@
|
|
3
3
|
module Evolvable
|
4
4
|
#
|
5
5
|
# @readme
|
6
|
-
#
|
7
|
-
#
|
8
|
-
# undergo combination and thereby produce the next generation of evolvables.
|
6
|
+
# Selection determines which evolvables will serve as parents for the next
|
7
|
+
# generation. You can control the selection process in several ways:
|
9
8
|
#
|
10
|
-
#
|
11
|
-
# The selection size is configurable.
|
9
|
+
# Set the selection size during population initialization:
|
12
10
|
#
|
13
|
-
#
|
14
|
-
#
|
11
|
+
# ```ruby
|
12
|
+
# population = MyEvolvable.new_population(
|
13
|
+
# selection: { size: 3 }
|
14
|
+
# )
|
15
|
+
# ```
|
16
|
+
#
|
17
|
+
# Adjust the selection size after initialization:
|
18
|
+
#
|
19
|
+
# ```ruby
|
20
|
+
# population.selection_size = 4
|
21
|
+
# ```
|
22
|
+
#
|
23
|
+
# Manually assign the selected evolvables:
|
24
|
+
#
|
25
|
+
# ```ruby
|
26
|
+
# population.selected_evolvables = [evolvable1, evolvable2]
|
27
|
+
# ```
|
28
|
+
#
|
29
|
+
# Or evolve a custom selection directly:
|
30
|
+
#
|
31
|
+
# ```ruby
|
32
|
+
# population.evolve_selected([evolvable1, evolvable2])
|
33
|
+
# ```
|
34
|
+
#
|
35
|
+
# This flexibility lets you implement custom selection strategies,
|
36
|
+
# overriding or augmenting the built-in behavior.
|
15
37
|
#
|
16
38
|
class Selection
|
17
39
|
extend Forwardable
|
@@ -19,25 +41,57 @@ module Evolvable
|
|
19
41
|
#
|
20
42
|
# Initializes a new selection object.
|
21
43
|
#
|
22
|
-
#
|
23
|
-
#
|
24
|
-
#
|
25
|
-
# The number of instances to select from each generation from which to
|
26
|
-
# perform crossover and generate or "breed" the next generation. The
|
27
|
-
# number of parents The default is 2.
|
44
|
+
# @param size [Integer] The number of instances to select from each generation
|
45
|
+
# to perform crossover and generate or "breed" the next generation.
|
46
|
+
# The default is 2.
|
28
47
|
#
|
29
48
|
def initialize(size: 2)
|
30
49
|
@size = size
|
31
50
|
end
|
32
51
|
|
52
|
+
#
|
53
|
+
# The number of evolvables to select as parents for the next generation.
|
54
|
+
# @return [Integer] The selection size
|
55
|
+
#
|
33
56
|
attr_accessor :size
|
34
57
|
|
58
|
+
#
|
59
|
+
# Performs selection on the population.
|
60
|
+
# Takes the evaluated and sorted evolvables and selects a subset to
|
61
|
+
# become parents for the next generation.
|
62
|
+
#
|
63
|
+
# @param population [Evolvable::Population] The population to perform selection on
|
64
|
+
# @return [Evolvable::Population] The population with selected parent evolvables
|
65
|
+
#
|
35
66
|
def call(population)
|
36
|
-
population.parent_evolvables = select_evolvables(population.evolvables)
|
67
|
+
population.parent_evolvables = population.selected_evolvables.empty? ? select_evolvables(population.evolvables) : population.selected_evolvables
|
68
|
+
population.selected_evolvables = []
|
37
69
|
population.evolvables = []
|
38
70
|
population
|
39
71
|
end
|
40
72
|
|
73
|
+
#
|
74
|
+
# Selects the best evolvables from the given collection.
|
75
|
+
# By default, selects the last N evolvables, where N is the selection size.
|
76
|
+
# This assumes evolvables are already sorted in the evaluation step, with the best at the end.
|
77
|
+
#
|
78
|
+
# Override this method in a subclass to implement different selection strategies
|
79
|
+
# such as tournament selection or roulette wheel selection.
|
80
|
+
#
|
81
|
+
# @example
|
82
|
+
# # A custom selection strategy using tournament selection
|
83
|
+
# class TournamentSelection < Evolvable::Selection
|
84
|
+
# def select_evolvables(evolvables)
|
85
|
+
# Array.new(size) do
|
86
|
+
# # Randomly select 3 individuals and pick the best one
|
87
|
+
# evolvables.sample(3).max_by(&:fitness)
|
88
|
+
# end
|
89
|
+
# end
|
90
|
+
# end
|
91
|
+
#
|
92
|
+
# @param evolvables [Array<Evolvable>] The evolvables to select from
|
93
|
+
# @return [Array<Evolvable>] The selected evolvables to use as parents
|
94
|
+
#
|
41
95
|
def select_evolvables(evolvables)
|
42
96
|
evolvables.last(@size)
|
43
97
|
end
|