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
@@ -1,6 +1,52 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Evolvable
4
+ #
5
+ # @readme
6
+ # Evolvable supports saving and restoring the state of both populations
7
+ # and individual evolvable instances through a built-in `Serializer`.
8
+ # By default, it uses Ruby's `Marshal` class for fast, portable binary serialization.
9
+ #
10
+ # Serialization is useful for:
11
+ # - Saving progress during long-running evolution
12
+ # - Storing champion solutions for later reuse
13
+ # - Transferring evolved populations between systems
14
+ # - Creating checkpoints you can revert to
15
+ #
16
+ # Both `Population` and individual evolvables expose `dump` and `load` methods
17
+ # that use the `Serializer` internally.
18
+ #
19
+ # Save a population to a file:
20
+ #
21
+ # ```ruby
22
+ # population = YourEvolvable.new_population
23
+ # population.evolve(count: 100)
24
+ # File.write("population.marshal", population.dump)
25
+ # ```
26
+ #
27
+ # Restore and continue evolution:
28
+ #
29
+ # ```ruby
30
+ # data = File.read("population.marshal")
31
+ # restored = Evolvable::Population.load(data)
32
+ # restored.evolve(count: 100)
33
+ # ```
34
+ #
35
+ # Save an individual evolvable's genome:
36
+ #
37
+ # ```ruby
38
+ # best = restored.best_evolvable
39
+ # File.write("champion.marshal", best.dump_genome)
40
+ # ```
41
+ #
42
+ # Restore genome into a new evolvable:
43
+ #
44
+ # ```ruby
45
+ # raw = File.read("champion.marshal")
46
+ # champion = YourEvolvable.new_evolvable
47
+ # champion.load_genome(raw)
48
+ # ```
49
+ #
4
50
  class Serializer
5
51
  class << self
6
52
  def dump(data)
@@ -2,7 +2,31 @@
2
2
 
3
3
  module Evolvable
4
4
  #
5
- # Randomly chooses a gene from one of the parents for each gene position.
5
+ # @readme
6
+ # Chooses genes independently at each position, selecting randomly from either
7
+ # parent with equal probability. No segments are preserved—each gene is treated
8
+ # in isolation.
9
+ #
10
+ # Best for:
11
+ # - Problems where gene order doesn't matter
12
+ # - High genetic diversity and exploration
13
+ # - Complex interdependencies across traits
14
+ #
15
+ # Uniform crossover is especially effective when good traits are scattered across the genome.
16
+ #
17
+ # Set your population to use this strategy during initialization with:
18
+ #
19
+ # ```ruby
20
+ # population = MyEvolvable.new_population(
21
+ # combination: Evolvable::UniformCrossover.new
22
+ # )
23
+ # ```
24
+ #
25
+ # Or update an existing population:
26
+ #
27
+ # ```ruby
28
+ # population.combination = Evolvable::UniformCrossover.new
29
+ # ```
6
30
  #
7
31
  class UniformCrossover
8
32
  def call(population)
@@ -22,10 +46,11 @@ module Evolvable
22
46
 
23
47
  def build_genome(genome_pair)
24
48
  new_config = {}
25
- genome_1, genome_2 = genome_pair.shuffle!
49
+ genome_1, genome_2 = genome_pair
26
50
  genome_1.each do |gene_key, gene_config_1|
27
- count_gene = gene_config_1[:count_gene]
28
- genes = crossover_genes(count_gene.count, gene_config_1, genome_2.config[gene_key])
51
+ gene_config_2 = genome_2.config[gene_key]
52
+ count_gene = [gene_config_1, gene_config_2].sample[:count_gene]
53
+ genes = crossover_genes(count_gene.count, gene_config_1, gene_config_2)
29
54
  new_config[gene_key] = { count_gene: count_gene, genes: genes }
30
55
  end
31
56
  Genome.new(config: new_config)
@@ -34,8 +59,7 @@ module Evolvable
34
59
  def crossover_genes(count, gene_config_1, gene_config_2)
35
60
  gene_arrays = [gene_config_1[:genes], gene_config_2[:genes]]
36
61
  Array.new(count) do |index|
37
- genes = gene_arrays.sample
38
- genes[index] || gene_arrays.detect { |a| a != genes }[index]
62
+ gene_arrays.shuffle!.detect { |genes| genes[index] }
39
63
  end
40
64
  end
41
65
  end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Evolvable
4
- VERSION = '1.2.0'
4
+ VERSION = '2.0.0'
5
+ DOC_URL = "https://mattruzicka.github.io/evolvable"
5
6
  end
data/lib/evolvable.rb CHANGED
@@ -2,9 +2,9 @@
2
2
 
3
3
  require 'forwardable'
4
4
  require 'evolvable/version'
5
- require 'evolvable/error/undefined_method'
6
5
  require 'evolvable/gene'
7
- require 'evolvable/search_space'
6
+ require 'evolvable/gene_cluster'
7
+ require 'evolvable/gene_space'
8
8
  require 'evolvable/genome'
9
9
  require 'evolvable/goal'
10
10
  require 'evolvable/equalize_goal'
@@ -21,28 +21,87 @@ require 'evolvable/population'
21
21
  require 'evolvable/count_gene'
22
22
  require 'evolvable/rigid_count_gene'
23
23
  require 'evolvable/serializer'
24
+ require 'evolvable/community'
24
25
 
25
26
  #
26
27
  # @readme
27
- # The `Evolvable` module makes it possible to implement evolutionary behaviors for
28
- # any class by defining a `.search_space` class method and `#value` instance method.
29
- # Then to evolve instances, initialize a population with `.new_population` and invoke
30
- # the `#evolve` method on the resulting population object.
28
+ # **Quick start**:
29
+ # 1. Include `Evolvable` in your Ruby class
30
+ # 2. Define genes with the macro-style `gene` method
31
+ # 3. Have the `#fitness` method return a numeric value
32
+ # 4. Initialize a population and evolve it
31
33
  #
32
- # ### Implementation Steps
34
+ # Example population of "shirts" with various colors, buttons, and collars.
33
35
  #
34
- # 1. [Include the `Evolvable` module in the class you want to evolve.](https://rubydoc.info/github/mattruzicka/evolvable/Evolvable)
35
- # 2. [Define `.search_space` and any gene classes that you reference.](https://rubydoc.info/github/mattruzicka/evolvable/Evolvable/SearchSpace)
36
- # 3. [Define `#value`.](https://rubydoc.info/github/mattruzicka/evolvable/Evolvable/Evaluation)
37
- # 4. [Initialize a population with `.new_population` and use `#evolve`.](https://rubydoc.info/github/mattruzicka/evolvable/Evolvable/Population)
36
+ # ```ruby
37
+ # # Step 1
38
+ # class Shirt
39
+ # include Evolvable
40
+ #
41
+ # # Step 2
42
+ # gene :color, type: ColorGene # count: 1 default
43
+ # gene :buttons, type: ButtonGene, count: 0..10 # Builds an array of genes that can vary in size
44
+ # gene :collar, type: CollarGene, count: 0..1 # Collar optional
45
+ #
46
+ # # Step 3
47
+ # attr_accessor :fitness
48
+ # end
49
+ #
50
+ # # Step 4
51
+ # population = Shirt.new_population(size: 10)
52
+ # population.evolvables.each { |shirt| shirt.fitness = style_rating }
53
+ # ```
54
+ #
55
+ # You are free to tailor the genes to your needs and find a style that suits you.
56
+ #
57
+ # The `ColorGene` could be as simple as this:
58
+ #
59
+ # ```ruby
60
+ # class ColorGene
61
+ # include Evolvable::Gene
62
+ #
63
+ # def to_s
64
+ # @to_s ||= %w[red green blue].sample
65
+ # end
66
+ # end
67
+ # ```
68
+ #
69
+ # Shirts aren't your style?
70
+ #
71
+ # Here's a [Hello World](https://github.com/mattruzicka/evolvable/blob/main/exe/hello_evolvable_world)
72
+ # command line demo.
38
73
  #
39
74
  module Evolvable
40
75
  extend Forwardable
41
76
 
77
+ #
78
+ # Error class for Evolvable-specific exceptions.
79
+ #
80
+ class Error < StandardError; end
81
+
82
+ #
83
+ # Called when the Evolvable module is included in a class.
84
+ # Sets up the necessary instance variables and extends the class with ClassMethods.
85
+ #
86
+ # @param base [Class] The class that is including the Evolvable module
87
+ # @return [void]
88
+ #
42
89
  def self.included(base)
90
+ base.instance_variable_set(:@gene_config, {})
91
+ base.instance_variable_set(:@cluster_config, {})
43
92
  base.extend(ClassMethods)
44
93
  end
45
94
 
95
+ #
96
+ # Helper method for creating or updating objects with hash parameters.
97
+ # This is used internally for creating evaluation, selection, mutation, etc. objects
98
+ # from configuration hashes.
99
+ #
100
+ # @param old_val [Object, nil] The existing object to update, if any
101
+ # @param new_val [Object, Hash] The new object or configuration hash
102
+ # @param default_class [Class] The default class to instantiate if new_val is a Hash
103
+ # @return [Object] The new or updated object
104
+ #
46
105
  def self.new_object(old_val, new_val, default_class)
47
106
  new_val.is_a?(Hash) ? (old_val&.class || default_class).new(**new_val) : new_val
48
107
  end
@@ -50,9 +109,8 @@ module Evolvable
50
109
  module ClassMethods
51
110
  #
52
111
  # @readme
53
- # Initializes a population using configurable defaults that can be configured and optimized.
54
- # Accepts the same named parameters as
55
- # [Population#initialize](https://rubydoc.info/github/mattruzicka/evolvable/Evolvable/Population#initialize).
112
+ # Initializes a population with defaults that can be changed using the same named parameters as
113
+ # [Population#initialize](https://mattruzicka.github.io/evolvable/Evolvable/Population#initialize).
56
114
  #
57
115
  def new_population(keyword_args = {})
58
116
  keyword_args[:evolvable_type] = self
@@ -69,102 +127,148 @@ module Evolvable
69
127
  # initialized you can override either of the following two "initialize_instance"
70
128
  # methods.
71
129
  #
72
- def new_evolvable(population: nil,
73
- genome: Genome.new,
74
- generation_index: nil)
130
+ def new_evolvable(population: nil, genome: nil, generation_index: nil)
75
131
  evolvable = initialize_evolvable
76
- evolvable.population = population
77
- evolvable.genome = genome
78
- evolvable.generation_index = generation_index
79
- evolvable.after_initialize
132
+ evolvable.make_evolvable(population: population, genome: genome, generation_index: generation_index)
133
+ evolvable.after_initialize_evolvable
80
134
  evolvable
81
135
  end
82
136
 
137
+ #
138
+ # Override this method to customize how your evolvable instances are initialized.
139
+ # By default, simply calls new with no arguments.
140
+ #
141
+ # @return [Evolvable] A new evolvable instance
142
+ #
83
143
  def initialize_evolvable
84
144
  new
85
145
  end
86
146
 
87
- def new_search_space
88
- space_config = search_space.empty? ? gene_space : search_space
89
- search_space = SearchSpace.build(space_config, self)
90
- search_spaces.each { |space| search_space.merge_search_space!(space) }
91
- search_space
92
- end
93
-
94
- #
95
- # @abstract
96
147
  #
97
- # This method is responsible for configuring the available gene types
98
- # of evolvable instances. In effect, it provides the
99
- # blueprint for constructing a hyperdimensional genetic space that's capable
100
- # of being used and searched by evolvable objects.
148
+ # @readme
149
+ # The `.gene` macro defines traits that can mutate and evolve over time.
150
+ # Syntactically similar to ActiveRecord-style macros, it sets up the genetic structure of your model.
101
151
  #
102
- # Override this method with a search space config for initializing
103
- # SearchSpace objects. The config can be a hash, array of arrays,
104
- # or single array when there's only one type of gene.
152
+ # Key features:
153
+ # - Fixed or variable gene counts
154
+ # - Automatic accessor methods
155
+ # - Optional clustering for related genes
105
156
  #
106
- # The below example definitions could conceivably be used to generate evolvable music.
157
+ # @example Basic gene definition
158
+ # class Melody
159
+ # include Evolvable
107
160
  #
108
- # @todo
109
- # Define gene config attributes - name, type, count
161
+ # gene :notes, type: NoteGene, count: 4..16 # Can have 4-16 notes
162
+ # gene :instrument, type: InstrumentGene, count: 1
110
163
  #
111
- # @example Hash config
112
- # def search_space
113
- # { instrument: { type: InstrumentGene, count: 1..4 },
114
- # notes: { type: NoteGene, count: 16 } }
115
- # end
116
- # @example Array of arrays config
117
- # # With explicit gene names
118
- # def search_space
119
- # [[:instrument, InstrumentGene, 1..4],
120
- # [:notes, NoteGene, 16]]
164
+ # def play
165
+ # instrument.play(notes)
166
+ # end
121
167
  # end
122
168
  #
123
- # # Without explicit gene names
124
- # def search_space
125
- # [[SynthGene, 0..4], [RhythmGene, 0..8]]
126
- # end
127
- # @example Array config
128
- # # Available when when just one type of gene
129
- # def search_space
130
- # [NoteGene, 1..100]
131
- # end
169
+ # @param name [Symbol] The name of the gene
170
+ # @param type [String, Class] The gene type or class name
171
+ # @param count [Integer, Range] Number or range of gene instances
172
+ # @param cluster [Symbol, nil] Optional cluster name for grouping related genes
132
173
  #
133
- # # With explicit gene type name.
134
- # def search_space
135
- # ['notes', 'NoteGene', 1..100]
174
+ def gene(name, type:, count: 1, cluster: nil)
175
+ raise Error, "Gene name '#{name}' is already defined" if @gene_config.key?(name)
176
+
177
+ @gene_config[name] = { type: type, count: count }
178
+
179
+ if (count.is_a?(Range) ? count.last : count) > 1
180
+ define_method(name) { find_genes(name) }
181
+ else
182
+ define_method(name) { find_gene(name) }
183
+ end
184
+
185
+ if cluster
186
+ raise Error, "Cluster name '#{cluster}' conflicts with an existing gene name" if @gene_config.key?(cluster)
187
+
188
+ if @cluster_config[cluster]
189
+ @cluster_config[cluster] << name
190
+ else
191
+ @cluster_config[cluster] = [name]
192
+ define_method(cluster) { find_gene_cluster(cluster) }
193
+ end
194
+ end
195
+ end
196
+
197
+ #
198
+ # @readme
199
+ # The `.cluster` macro applies a pre-defined group of related genes.
200
+ #
201
+ # Clusters promote code organization through:
202
+ # - Modularity: Define related genes once, reuse them
203
+ # - Organization: Group genes by function
204
+ # - Maintenance: Update in one place
205
+ # - Accessibility: Access via a single accessor
206
+ #
207
+ # @example UI Component with Styling Cluster
208
+ # # Define a gene cluster for UI styling properties
209
+ # class ColorSchemeCluster
210
+ # include Evolvable::GeneCluster
211
+ #
212
+ # gene :background_color, type: 'ColorGene', count: 1
213
+ # gene :text_color, type: 'ColorGene', count: 1
214
+ # gene :accent_color, type: 'ColorGene', count: 0..1
136
215
  # end
137
216
  #
138
- # @return [Hash, Array]
217
+ # # Use the cluster in an evolvable UI component
218
+ # class Button
219
+ # include Evolvable
139
220
  #
140
- # @see https://github.com/mattruzicka/evolvable#search_space
221
+ # cluster :colors, type: ColorSchemeCluster
222
+ # gene :padding, type: PaddingGene, count: 1
141
223
  #
142
- def search_space
143
- {}
224
+ # def render
225
+ # puts "Button with #{colors.count} colors"
226
+ # puts "Background: #{colors.background_color.hex_code}"
227
+ # end
228
+ # end
229
+ #
230
+ # @param cluster_name [Symbol] The name for accessing the cluster
231
+ # @param type [Class, String] The class that defines the cluster
232
+ # @param opts [Hash] Optional arguments passed to the cluster initializer
233
+ #
234
+ def cluster(cluster_name, type:, **opts)
235
+ recipe = type.is_a?(String) ? Object.const_get(type) : type
236
+ unless recipe.respond_to?(:apply_cluster)
237
+ raise ArgumentError, "#{recipe} cannot apply a gene cluster"
238
+ end
239
+
240
+ recipe.apply_cluster(self, cluster_name, **opts)
241
+
242
+ define_method(cluster_name) { find_gene_cluster(cluster_name) }
144
243
  end
145
244
 
245
+ attr_reader :gene_config, :cluster_config
246
+
146
247
  #
147
- # @abstract Override this method to define multiple search spaces
148
- #
149
- # @return [Array]
248
+ # Creates a new gene space for this evolvable class.
249
+ # Used internally when initializing populations.
150
250
  #
151
- # @see https://github.com/mattruzicka/evolvable#search_space
251
+ # @return [Evolvable::GeneSpace] A new gene space
152
252
  #
153
- def search_spaces
154
- []
253
+ def new_gene_space
254
+ GeneSpace.build(@gene_config, self)
155
255
  end
156
256
 
157
- # @deprecated
158
- # Will be removed in version 2.0.
159
- # Use {#search_space} instead.
160
- def gene_space
161
- {}
257
+ #
258
+ # Ensures that subclasses inherit the gene and cluster configuration.
259
+ #
260
+ # @param subclass [Class] The subclass that is inheriting from this class
261
+ # @return [void]
262
+ #
263
+ def inherited(subclass)
264
+ super
265
+ subclass.instance_variable_set(:@gene_config, @gene_config.dup)
266
+ subclass.instance_variable_set(:@cluster_config, @cluster_config.dup)
162
267
  end
163
268
 
164
-
165
269
  #
166
270
  # @readme
167
- # Runs before evaluation.
271
+ # Called before evaluation.
168
272
  #
169
273
  def before_evaluation(population); end
170
274
 
@@ -193,49 +297,106 @@ module Evolvable
193
297
  def after_evolution(population); end
194
298
  end
195
299
 
196
- # Runs an evolvable is initialized. Ueful for implementing custom initialization logic.
197
- def after_initialize; end
300
+ def after_initialize_evolvable; end
301
+
302
+ attr_accessor :fitness
303
+
304
+ def find_gene(...)
305
+ @genome&.find_gene(...)
306
+ end
307
+
308
+ def find_genes(...)
309
+ @genome&.find_genes(...) || []
310
+ end
311
+
312
+ attr_reader :population,
313
+ :genome,
314
+ :generation_index
198
315
 
316
+ def genes
317
+ @genome&.genes || []
318
+ end
319
+
320
+ #
321
+ # Makes this object evolvable by setting up its population, genome, and generation index.
322
+ # This is called internally by the class method `new_evolvable`.
199
323
  #
200
- # @!method value
201
- # Implementing this method is required for evaluation and selection.
324
+ # @param population [Evolvable::Population, nil] The population this evolvable belongs to
325
+ # @param genome [Evolvable::Genome, nil] The genome to initialize with
326
+ # @param generation_index [Integer, nil] The generation index
327
+ # @return [self] This evolvable object
202
328
  #
203
- attr_accessor :id,
204
- :population,
205
- :genome,
206
- :generation_index,
207
- :value
329
+ def make_evolvable(population: nil, genome: nil, generation_index: nil)
330
+ self.population = population
331
+ @genome = genome
332
+ self.generation_index = generation_index
333
+ self
334
+ end
335
+
336
+ def population=(val)
337
+ @population = val
338
+ genes.each { |gene| gene.evolvable = self if gene.respond_to?(:evolvable=) }
339
+ end
208
340
 
341
+ def generation_index=(val)
342
+ @generation_index = val
343
+ end
344
+
345
+ #
346
+ # Finds an array of genes that belong to the specified cluster.
347
+ # This is used internally when accessing gene clusters.
209
348
  #
210
- # @deprecated
211
- # Will be removed in version 2.0.
212
- # Use {#generation_index} instead.
349
+ # @param cluster [Symbol] The cluster name
350
+ # @return [Array<Evolvable::Gene>] The genes belonging to the cluster
213
351
  #
214
- def population_index
215
- generation_index
352
+ def find_gene_cluster(cluster)
353
+ find_genes(*self.class.cluster_config[cluster])
216
354
  end
217
355
 
218
356
  #
219
- # @!method find_gene
220
- # @see Genome#find_gene
221
- # @!method find_genes
222
- # @see Genome#find_genes
223
- # @!method find_genes_count
224
- # @see Genome#find_genes_count
225
- # @!method genes
226
- # @see Genome#genes
357
+ # Dumps the genome to a serialized format.
358
+ # Useful for saving the state of an evolvable.
359
+ #
360
+ # @param serializer [Evolvable::Serializer] The serializer to use
361
+ # @return [String] The serialized genome
227
362
  #
228
- def_delegators :genome,
229
- :find_gene,
230
- :find_genes,
231
- :find_genes_count,
232
- :genes
233
-
234
363
  def dump_genome(serializer: Serializer)
235
364
  @genome.dump(serializer: serializer)
236
365
  end
237
366
 
367
+ #
368
+ # Loads a genome from serialized data.
369
+ # Useful for restoring the state of an evolvable.
370
+ #
371
+ # @param data [String] The serialized genome data
372
+ # @param serializer [Evolvable::Serializer] The serializer to use
373
+ # @return [Evolvable::Genome] The loaded genome
374
+ #
238
375
  def load_genome(data, serializer: Serializer)
239
376
  @genome = Genome.load(data, serializer: serializer)
240
377
  end
378
+
379
+ #
380
+ # Loads a genome from serialized data and merges it with the current genome.
381
+ # Useful for combining genomes from different sources.
382
+ #
383
+ # @param data [String] The serialized genome data
384
+ # @param serializer [Evolvable::Serializer] The serializer to use
385
+ # @return [Evolvable::Genome] The merged genome
386
+ #
387
+ def load_and_merge_genome!(data, serializer: Serializer)
388
+ genome = Genome.load(data, serializer: serializer)
389
+ merge_genome!(genome)
390
+ end
391
+
392
+ #
393
+ # Merges another genome into this evolvable's genome.
394
+ # Useful for combining genetic traits from different evolvables.
395
+ #
396
+ # @param other_genome [Evolvable::Genome] The genome to merge
397
+ # @return [Evolvable::Genome] The merged genome
398
+ #
399
+ def merge_genome!(other_genome)
400
+ @genome.merge!(other_genome)
401
+ end
241
402
  end