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
@@ -3,55 +3,110 @@
3
3
  module Evolvable
4
4
  #
5
5
  # @readme
6
- # Population objects are responsible for generating and evolving instances.
7
- # They orchestrate all the other Evolvable objects to do so.
6
+ # Populations orchestrate the evolutionary process through four key components:
8
7
  #
9
- # Populations can be initialized and re-initialized with a number of useful
10
- # parameters.
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
- # Keyword arguments:
24
- # #### evolvable_class
25
- # Required. Implicitly specified when using EvolvableClass.new_population.
26
- # #### id, name
27
- # Both default to `nil`. Not used by Evolvable, but convenient when working
28
- # with multiple populations.
29
- # #### size
30
- # Defaults to `40`. Specifies the number of instances in the population.
31
- # #### evolutions_count
32
- # Defaults to `0`. Useful when re-initializing a saved population with instances.
33
- # #### search_space
34
- # Defaults to `evolvable_class.new_search_space` which uses the
35
- # [EvolvableClass.search_space](#evolvableclasssearch_space) method
36
- # #### evolution
37
- # Defaults to `Evolvable::Evolution.new`. See [evolution](#evolution-1)
38
- # #### evaluation
39
- # Defaults to `Evolvable::Evaluation.new`, with a goal of maximizing
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: 40,
105
+ size: 0,
51
106
  evolutions_count: 0,
52
- gene_space: nil, # Deprecated
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 || evolvable_class
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
- @search_space = initialize_search_space(search_space || gene_space)
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
- @evolvables = new_evolvables(count: @size - evolvables.count, evolvables: evolvables)
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
- :search_space,
141
+ :gene_space,
82
142
  :evolution,
83
143
  :parent_evolvables,
144
+ :selected_evolvables,
84
145
  :evolvables
85
146
 
86
- def_delegators :evolvable_class,
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
- # Keyword arguments:
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
- # ### Evolvable::Population#met_goal?
132
- # Returns true if any instance's value matches the goal value, otherwise false.
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
- # ### Evolvable::Population#new_instance
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 = evolvable_class.new_evolvable(population: self,
173
- genome: genome || search_space.new_genome,
174
- generation_index: @evolvables.count)
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
- def new_evolvables(count:, evolvables: nil)
180
- evolvables ||= @evolvables || []
181
- @evolvables = evolvables
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: search_space.new_genome) }
308
+ Array.new(count) { new_evolvable(genome: new_genome) }
185
309
  else
186
- @evolvables = generate_evolvables(count)
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
- def evolvable_class
200
- @evolvable_class ||= evolvable_type.is_a?(Class) ? evolvable_type : Object.const_get(evolvable_type)
201
- end
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
- search_space
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
- def initialize_search_space(search_space)
227
- return SearchSpace.build(search_space, evolvable_class) if search_space
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
- evolvable_class.new_search_space
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
@@ -3,15 +3,37 @@
3
3
  module Evolvable
4
4
  #
5
5
  # @readme
6
- # The selection object assumes that a population's evolvables have already
7
- # been sorted by the evaluation object. It selects "parent" evolvables to
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
- # Only two evolvables are selected as parents for each generation by default.
11
- # The selection size is configurable.
9
+ # Set the selection size during population initialization:
12
10
  #
13
- # @example
14
- # # TODO: Show how to add/change population's selection object
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
- # Keyword arguments:
23
- #
24
- # #### size
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