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,67 +3,166 @@
3
3
  module Evolvable
4
4
  #
5
5
  # @readme
6
- # For selection to be effective in the context of evolution, there needs to be
7
- # a way to compare evolvables. In the genetic algorithm, this is often
8
- # referred to as the "fitness function".
6
+ # Evaluation sorts evolvables based on their fitness and provides mechanisms to
7
+ # change the goal type and value (fitness goal). Goals define the success criteria
8
+ # for evolution. They allow you to specify what your population is evolving toward,
9
+ # whether it's maximizing a value, minimizing a value, or seeking a specific value.
9
10
  #
10
- # The `Evolvable::Evaluation` object expects evolvable instances to define a `#value` method that
11
- # returns some numeric value. Values are used to evaluate instances relative to each
12
- # other and with regards to some goal. Out of the box, the goal can be set
13
- # to maximize, minimize, or equalize numeric values.
11
+ # **How It Works**
14
12
  #
15
- # @example
16
- # # TODO: Show how to add/change population's evaluation object
13
+ # 1. Your evolvable class defines a `#fitness` method that returns a
14
+ # [Comparable](https://docs.ruby-lang.org/en//3.4/Comparable.html) object.
15
+ # - Preferably a numeric value like an integer or float.
17
16
  #
18
- # # The goal value can also be assigned via as argument to `Evolvable::Population#evolve`
19
- # population.evolve(goal_value: 1000)
17
+ # 2. During evolution, evolvables are sorted by your goal's fitness interpretation
18
+ # - The default goal type is `:maximize`, see goal types below for other options
19
+ #
20
+ # 3. If a goal value is specified, evolution will stop when it is met
21
+ #
22
+ # **Goal Types**
23
+ #
24
+ # - Maximize (higher is better)
25
+ #
26
+ # ```ruby
27
+ # robots = Robot.new_population(evaluation: :maximize) # Defaults to infinity
28
+ # robots.evolve_to_goal(100) # Evolve until fitness reaches 100+
29
+ #
30
+ # # Same as above
31
+ # Robot.new_population(evaluation: { maximize: 100 }).evolve_to_goal
32
+ # ```
33
+ #
34
+ # - Minimize (lower is better)
35
+ #
36
+ # ```ruby
37
+ # errors = ErrorModel.new_population(evaluation: :minimize) # Defaults to -infinity
38
+ # errors.evolve_to_goal(0.01) # Evolve until error rate reaches 0.01 or less
39
+ #
40
+ # # Same as above
41
+ # ErrorModel.new_population(evaluation: { minimize: 0.01 }).evolve_to_goal
42
+ # ```
43
+ #
44
+ # - Equalize (closer to target is better)
45
+ #
46
+ # ```ruby
47
+ # targets = TargetMatcher.new_population(evaluation: :equalize) # Defaults to 0
48
+ # targets.evolve_to_goal(42) # Evolve until we match the target value
49
+ #
50
+ # # Same as above
51
+ # TargetMatcher.new_population(evaluation: { equalize: 42 }).evolve_to_goal
52
+ # ```
53
+ #
54
+ # @see Evolvable::Population
55
+ # @see Evolvable::Selection
20
56
  #
21
57
  class Evaluation
22
- GOALS = { maximize: Evolvable::Goal::Maximize.new,
23
- minimize: Evolvable::Goal::Minimize.new,
24
- equalize: Evolvable::Goal::Equalize.new }.freeze
58
+ #
59
+ # Mapping of goal type symbols to their corresponding goal objects.
60
+ # See the readme section above for details on each goal type.
61
+ #
62
+ # @return [Hash<Symbol, Evolvable::Goal>] Available goal objects by type
63
+ #
64
+ GOALS = { maximize: Evolvable::MaximizeGoal.new,
65
+ minimize: Evolvable::MinimizeGoal.new,
66
+ equalize: Evolvable::EqualizeGoal.new }.freeze
25
67
 
68
+ #
69
+ # The default goal type used if none is specified.
70
+ # @return [Symbol] The default goal type (maximize)
71
+ #
26
72
  DEFAULT_GOAL_TYPE = :maximize
27
73
 
74
+ #
75
+ # Initializes a new evaluation object.
76
+ #
77
+ # @param goal [Symbol, Hash, Evolvable::Goal] The goal type (:maximize, :minimize, :equalize),
78
+ # a hash specifying goal type and value, or a custom goal object
79
+ #
28
80
  def initialize(goal = DEFAULT_GOAL_TYPE)
29
81
  @goal = normalize_goal(goal)
30
82
  end
31
83
 
84
+ #
85
+ # The goal object used for evaluation.
86
+ # @return [Evolvable::Goal] The current goal object
87
+ #
32
88
  attr_accessor :goal
33
89
 
90
+ #
91
+ # Evaluates and sorts all evolvables in the population according to the goal.
92
+ #
93
+ # @param population [Evolvable::Population] The population to evaluate
94
+ # @return [Array<Evolvable>] The sorted evolvables
95
+ #
34
96
  def call(population)
35
97
  population.evolvables.sort_by! { |evolvable| goal.evaluate(evolvable) }
36
98
  end
37
99
 
100
+ #
101
+ # Returns the best evolvable in the population according to the goal.
102
+ #
103
+ # @param population [Evolvable::Population] The population to evaluate
104
+ # @return [Evolvable] The best evolvable based on the current goal
105
+ #
38
106
  def best_evolvable(population)
39
107
  population.evolvables.max_by { |evolvable| goal.evaluate(evolvable) }
40
108
  end
41
109
 
110
+ #
111
+ # Checks if the goal has been met by any evolvable in the population.
112
+ #
113
+ # @param population [Evolvable::Population] The population to check
114
+ # @return [Boolean] True if the goal has been met, false otherwise
115
+ #
42
116
  def met_goal?(population)
43
117
  goal.met?(population.evolvables.last)
44
118
  end
45
119
 
46
120
  private
47
121
 
122
+ #
123
+ # Normalizes the goal parameter into a proper goal object.
124
+ #
125
+ # @param goal_arg [Symbol, Hash, Evolvable::Goal] The goal parameter
126
+ # @return [Evolvable::Goal] A normalized goal object
127
+ #
48
128
  def normalize_goal(goal_arg)
49
129
  case goal_arg
50
130
  when Symbol
51
131
  goal_from_symbol(goal_arg)
52
132
  when Hash
53
133
  goal_from_hash(goal_arg)
134
+ when String
135
+ goal_from_symbol(goal_arg.to_sym)
54
136
  else
55
137
  goal_arg || default_goal
56
138
  end
57
139
  end
58
140
 
141
+ #
142
+ # Returns the default goal object.
143
+ #
144
+ # @return [Evolvable::Goal] The default goal object
145
+ #
59
146
  def default_goal
60
147
  GOALS[DEFAULT_GOAL_TYPE]
61
148
  end
62
149
 
150
+ #
151
+ # Creates a goal object from a symbol.
152
+ #
153
+ # @param goal_arg [Symbol] The goal type symbol
154
+ # @return [Evolvable::Goal] The corresponding goal object
155
+ #
63
156
  def goal_from_symbol(goal_arg)
64
157
  GOALS[goal_arg]
65
158
  end
66
159
 
160
+ #
161
+ # Creates a goal object from a hash specifying the goal type and value.
162
+ #
163
+ # @param goal_arg [Hash] A hash with a single key-value pair
164
+ # @return [Evolvable::Goal] The corresponding goal object with the specified value
165
+ #
67
166
  def goal_from_hash(goal_arg)
68
167
  goal_type, value = goal_arg.first
69
168
  goal = GOALS[goal_type]
@@ -3,32 +3,30 @@
3
3
  module Evolvable
4
4
  #
5
5
  # @readme
6
- # After a population's instances are evaluated, they undergo evolution.
7
- # The default evolution object is composed of selection,
8
- # crossover, and mutation objects and applies them as operations to
9
- # a population's evolvables in that order.
6
+ # **Evolution** moves a population from one generation to the next.
7
+ # It runs in three steps: selection, combination, and mutation.
8
+ # You can swap out any step with your own strategy.
9
+ #
10
+ # Default pipeline:
11
+ # 1. **Selection** – keep the most fit evolvables
12
+ # 2. **Combination** – create offspring by recombining genes
13
+ # 3. **Mutation** – add random variation to preserve diversity
10
14
  #
11
15
  class Evolution
12
16
  extend Forwardable
13
17
 
14
18
  #
15
- # Initializes a new evolution object.
16
- #
17
- # Keyword arguments:
19
+ # Initializes a new evolution object.
18
20
  #
19
- # #### selection
20
- # The default is `Selection.new`
21
- # #### crossover - deprecated
22
- # The default is `GeneCrossover.new`
23
- # #### mutation
24
- # The default is `Mutation.new`
21
+ # @param selection [Evolvable::Selection, Hash] The selection strategy
22
+ # @param combination [Evolvable::Combination, Hash] The combination strategy
23
+ # @param mutation [Evolvable::Mutation, Hash] The mutation strategy
25
24
  #
26
25
  def initialize(selection: Selection.new,
27
26
  combination: GeneCombination.new,
28
- crossover: nil, # deprecated
29
27
  mutation: Mutation.new)
30
28
  @selection = selection
31
- @combination = crossover || combination
29
+ @combination = combination
32
30
  @mutation = mutation
33
31
  end
34
32
 
@@ -36,18 +34,43 @@ module Evolvable
36
34
  :combination,
37
35
  :mutation
38
36
 
37
+ #
38
+ # Sets the selection strategy.
39
+ #
40
+ # @param val [Evolvable::Selection, Hash] The new selection strategy or configuration hash
41
+ # @return [Evolvable::Selection] The updated selection object
42
+ #
39
43
  def selection=(val)
40
44
  @selection = Evolvable.new_object(@selection, val, Selection)
41
45
  end
42
46
 
47
+ #
48
+ # Sets the combination strategy.
49
+ #
50
+ # @param val [Evolvable::Combination, Hash] The new combination strategy or configuration hash
51
+ # @return [Evolvable::Combination] The updated combination object
52
+ #
43
53
  def combination=(val)
44
54
  @combination = Evolvable.new_object(@combination, val, GeneCombination)
45
55
  end
46
56
 
57
+ #
58
+ # Sets the mutation strategy.
59
+ #
60
+ # @param val [Evolvable::Mutation, Hash] The new mutation strategy or configuration hash
61
+ # @return [Evolvable::Mutation] The updated mutation object
62
+ #
47
63
  def mutation=(val)
48
64
  @mutation = Evolvable.new_object(@mutation, val, Mutation)
49
65
  end
50
66
 
67
+ #
68
+ # Executes the evolution process on the population.
69
+ # Applies selection, combination, and mutation in sequence.
70
+ #
71
+ # @param population [Evolvable::Population] The population to evolve
72
+ # @return [Evolvable::Population] The evolved population
73
+ #
51
74
  def call(population)
52
75
  selection.call(population)
53
76
  combination.call(population)
@@ -3,61 +3,160 @@
3
3
  module Evolvable
4
4
  #
5
5
  # @readme
6
- # For evolution to be effective, an evolvable's genes must be able to influence
7
- # its behavior. Evolvables are composed of genes that can be used to run simple
8
- # functions or orchestrate complex interactions. The level of abstraction is up
9
- # to you.
6
+ # Genes are the building blocks of evolvable objects, encapsulating individual characteristics
7
+ # that can be combined and mutated during evolution. Each gene represents a trait or behavior
8
+ # that can influence an evolvable's performance.
10
9
  #
11
- # Defining gene classes requires encapsulating some "sample space" and returning
12
- # a sample outcome when a gene attribute is accessed. For evolution to proceed
13
- # in a non-random way, the same sample outcome should be returned every time
14
- # a particular gene is accessed with a particular set of parameters.
15
- # Memoization is a useful technique for doing just this. The
16
- # [memo_wise](https://github.com/panorama-ed/memo_wise) gem may be useful for
17
- # more complex memoizations.
10
+ # **To define a gene class:**
11
+ # 1. Include the `Evolvable::Gene` module
12
+ # 2. Define how the gene's value is determined
18
13
  #
19
- # @example
20
- # # This gene generates a random hexidecimal color code for use by evolvables.
14
+ # ```ruby
15
+ # class BehaviorGene
16
+ # include Evolvable::Gene
17
+ #
18
+ # def value
19
+ # @value ||= %w[explore gather attack defend build].sample
20
+ # end
21
+ # end
22
+ # ```
23
+ #
24
+ # Then use it in an evolvable class:
25
+ #
26
+ # ```ruby
27
+ # class Robot
28
+ # include Evolvable
29
+ #
30
+ # gene :behaviors, type: BehaviorGene, count: 3..5
31
+ # gene :speed, type: SpeedGene, count: 1
32
+ #
33
+ # def fitness
34
+ # run_simulation(behaviors: behaviors.map(&:value), speed: speed.value)
35
+ # end
36
+ # end
37
+ # ```
21
38
  #
22
- # require 'securerandom'
39
+ # **Gene Count**
23
40
  #
24
- # class ColorGene
41
+ # You can control how many copies of a gene are created using the `count:` parameter:
42
+ #
43
+ # - `count: 1` (default) creates a single instance.
44
+ # - A numeric value (e.g. `count: 5`) creates a fixed number of genes using `RigidCountGene`.
45
+ # - A range (e.g. `count: 2..8`) creates a variable number of genes using `CountGene`, allowing the count to evolve over time.
46
+ #
47
+ # Evolves melody length:
48
+ #
49
+ # ```ruby
50
+ # gene :notes, type: NoteGene, count: 4..12
51
+ # ```
52
+ #
53
+ # **Custom Combination**
54
+ #
55
+ # By default, the `combine` method randomly picks one of the two parent genes.
56
+ # A gene class can implement custom behavior by overriding `.combine`.
57
+ #
58
+ # ```ruby
59
+ # class SpeedGene
25
60
  # include Evolvable::Gene
26
61
  #
27
- # def hex_code
28
- # @hex_code ||= SecureRandom.hex(3)
62
+ # def self.combine(gene_a, gene_b)
63
+ # new_gene = new
64
+ # new_gene.value = (gene_a.value + gene_b.value) / 2
65
+ # new_gene
66
+ # end
67
+ #
68
+ # attr_writer :value
69
+ #
70
+ # def value
71
+ # @value ||= rand(1..100)
29
72
  # end
30
73
  # end
74
+ # ```
75
+ #
76
+ # **Design Patterns**
77
+ #
78
+ # Effective gene design typically follows these principles:
79
+ #
80
+ # - **Immutability**: Cache values after initial sampling (e.g., `@value ||= ...`)
81
+ # - **Self-Contained**: Genes should encapsulate their logic and state
82
+ # - **Composable**: You can build complex structures using multiple genes or clusters
83
+ # - **Domain-Specific**: Genes should map directly to your problem’s traits or features
84
+ #
85
+ # Genes come in various types, each representing different aspects of a solution.
86
+ # Common examples include numeric genes for quantities, selection genes for choices
87
+ # from sets, boolean genes for binary decisions, structural genes for architecture,
88
+ # and parameter genes for configuration settings.
89
+ #
90
+ # @see Evolvable::GeneSpace
91
+ # @see Evolvable::GeneCluster
92
+ # @see Evolvable::GeneCombination
93
+ # @see Evolvable::CountGene
94
+ # @see Evolvable::RigidCountGene
31
95
  #
32
-
33
96
  module Gene
97
+ #
98
+ # When included in a class, extends the class with ClassMethods.
99
+ # Gene classes should include this module to participate in the evolutionary process.
100
+ #
101
+ # @param base [Class] The class that includes the Gene module
102
+ #
34
103
  def self.included(base)
35
104
  base.extend(ClassMethods)
36
105
  end
37
106
 
107
+ #
108
+ # Class methods added to classes that include Evolvable::Gene.
109
+ # These methods enable gene-level behaviors like combination during evolution.
110
+ #
38
111
  module ClassMethods
112
+ #
113
+ # Sets the unique key for this gene type.
114
+ # The key is typically set automatically when using the `gene` macro.
115
+ #
116
+ # @param val [Symbol] The key to identify this gene type
117
+ #
39
118
  def key=(val)
40
119
  @key = val
41
120
  end
42
121
 
122
+ #
123
+ # Returns the unique key for this gene type.
124
+ #
125
+ # @return [Symbol] The key that identifies this gene type
126
+ #
43
127
  def key
44
128
  @key
45
129
  end
46
130
 
131
+ #
132
+ # Combines two genes to produce a new gene during the combination phase.
133
+ # By default, randomly picks one of the two parent genes.
134
+ #
135
+ # Override this method in your gene class to implement custom combination behavior.
136
+ #
137
+ # @param gene_a [Evolvable::Gene] The first gene to combine
138
+ # @param gene_b [Evolvable::Gene] The second gene to combine
139
+ # @return [Evolvable::Gene] A new gene resulting from the combination
140
+ #
47
141
  def combine(gene_a, gene_b)
48
142
  [gene_a, gene_b].sample
49
143
  end
50
-
51
- #
52
- # @deprecated
53
- # Will be removed in 2.0
54
- # Use {#combine}
55
- #
56
- alias crossover combine
57
144
  end
58
145
 
146
+ #
147
+ # The evolvable instance that this gene belongs to.
148
+ # Used for accessing other genes or evolvable properties.
149
+ #
150
+ # @return [Evolvable] The evolvable instance this gene is part of
151
+ #
59
152
  attr_accessor :evolvable
60
153
 
154
+ #
155
+ # Returns the unique key for this gene instance.
156
+ # Delegates to the class-level key.
157
+ #
158
+ # @return [Symbol] The key that identifies this gene type
159
+ #
61
160
  def key
62
161
  self.class.key
63
162
  end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Evolvable
4
+ #
5
+ # @readme
6
+ # Gene clusters group related genes into reusable components that can be applied
7
+ # to multiple evolvable classes. This promotes clean organization, eliminates
8
+ # naming conflicts, and simplifies gene access.
9
+ #
10
+ # **Benefits:**
11
+ # - Reuse gene groups across multiple evolvables
12
+ # - Prevent name collisions via automatic namespacing
13
+ # - Treat clusters as structured subcomponents of a genome
14
+ # - Access all genes in a cluster with a single method call
15
+ #
16
+ # The `ColorPaletteCluster` below defines a group of genes commonly used for styling themes:
17
+ #
18
+ # ```ruby
19
+ # class ColorPaletteCluster
20
+ # include Evolvable::GeneCluster
21
+ #
22
+ # gene :primary, type: 'ColorGene', count: 1
23
+ # gene :secondary, type: 'ColorGene', count: 1
24
+ # gene :accent, type: 'ColorGene', count: 1
25
+ # gene :neutral, type: 'ColorGene', count: 1
26
+ # end
27
+ # ```
28
+ #
29
+ # Use the `cluster` macro to apply the cluster to your evolvable class:
30
+ #
31
+ # ```ruby
32
+ # class Theme
33
+ # include Evolvable
34
+ #
35
+ # cluster :colors, type: ColorPaletteCluster
36
+ #
37
+ # def inspect_colors
38
+ # colors.join(", ")
39
+ # end
40
+ # end
41
+ # ```
42
+ #
43
+ # When a cluster is applied, its genes are automatically namespaced with the cluster name:
44
+ # - Access the full group: `theme.colors` → returns all genes in the colors cluster
45
+ # - Access individual genes: `theme.find_gene("colors-primary")`
46
+ #
47
+ # @see Evolvable::Gene
48
+ # @see Evolvable::GeneSpace
49
+ #
50
+ module GeneCluster
51
+ #
52
+ # When included in a class, extends the class with ClassMethods and initializes
53
+ # the cluster configuration. This is called automatically when you include
54
+ # the GeneCluster module in your class.
55
+ #
56
+ # @param base [Class] The class that includes the GeneCluster module
57
+ # @return [void]
58
+ #
59
+ def self.included(base)
60
+ base.extend(ClassMethods)
61
+ base.instance_variable_set(:@cluster_config, [])
62
+ end
63
+
64
+ #
65
+ # Class methods added to classes that include Evolvable::GeneCluster
66
+ #
67
+ module ClassMethods
68
+ #
69
+ # Defines a gene in the cluster.
70
+ # This is used internally by the cluster to define its component genes.
71
+ #
72
+ # @param name [Symbol] The name of the gene within the cluster
73
+ # @param opts [Hash] Options for the gene (type, count, etc.)
74
+ #
75
+ def gene(name, **opts)
76
+ @cluster_config << [name, opts]
77
+ end
78
+
79
+ #
80
+ # Applies all genes in this cluster to the given evolvable class.
81
+ # This is called automatically when using the `cluster` macro.
82
+ #
83
+ # @param evolvable_class [Class] The evolvable class to apply the cluster to
84
+ # @param cluster_name [Symbol] The name to use for the cluster in the evolvable class
85
+ # @param _ [Hash] Additional options (for future expansion)
86
+ # @return [void]
87
+ #
88
+ def apply_cluster(evolvable_class, cluster_name, **_)
89
+ @cluster_config.each do |name, kw|
90
+ evolvable_class.gene("#{cluster_name}-#{name}", **kw, cluster: cluster_name)
91
+ end
92
+ end
93
+
94
+ #
95
+ # Ensures that subclasses inherit the cluster configuration.
96
+ #
97
+ # @param subclass [Class] The subclass that is inheriting from this class
98
+ # @return [void]
99
+ #
100
+ def inherited(subclass)
101
+ super
102
+ subclass.instance_variable_set(:@cluster_config, @cluster_config.dup)
103
+ end
104
+ end
105
+ end
106
+ end
@@ -3,40 +3,60 @@
3
3
  module Evolvable
4
4
  #
5
5
  # @readme
6
- # Combination generates new evolvable instances by combining the genes of selected instances.
7
- # You can think of it as a mixing of parent genes from one generation to
8
- # produce the next generation.
6
+ # Combination is the process of creating new evolvables by mixing the genes
7
+ # of selected parents. This step drives the creation of the next generation
8
+ # by recombining traits in novel ways.
9
9
  #
10
- # You may choose from a selection of combination objects or implement your own.
11
- # The default combination object is `Evolvable::GeneCombination`.
10
+ # You can choose from several built-in combination strategies or implement your own.
11
+ # By default, Evolvable uses `Evolvable::GeneCombination`, which delegates
12
+ # gene-level behavior to individual gene classes.
12
13
  #
13
- # Custom crossover objects must implement the `#call` method which accepts
14
- # the population as the first object.
15
- # Enables gene types to define combination behaviors.
14
+ # To define custom combination logic for a gene type, implement:
16
15
  #
17
- # Each gene class can implement a unique behavior for
18
- # combination by overriding the following default implementation
19
- # which mirrors the behavior of `Evolvable::UniformCrossover`
16
+ # ```ruby
17
+ # YourGeneClass.combine(parent_1_gene, parent_2_gene)
18
+ # ```
20
19
  #
21
20
  class GeneCombination
21
+ #
22
+ # Performs the combination operation on the population.
23
+ # Creates new evolvables by combining parent genomes.
24
+ #
25
+ # @param population [Evolvable::Population] The population to perform combination on
26
+ # @return [Evolvable::Population] The population with new evolvables
27
+ #
22
28
  def call(population)
23
29
  new_evolvables(population, population.size)
24
30
  population
25
31
  end
26
32
 
33
+ #
34
+ # Creates new evolvable instances by combining parent genomes.
35
+ # For each new evolvable, selects parent genomes and combines them.
36
+ #
37
+ # @param population [Evolvable::Population] The population containing parent evolvables
38
+ # @param count [Integer] The number of new evolvables to create
39
+ # @return [Array<Evolvable>] The newly created evolvables
40
+ #
27
41
  def new_evolvables(population, count)
28
42
  parent_genome_cycle = population.new_parent_genome_cycle
29
43
  Array.new(count) do
30
- genome = build_genome(parent_genome_cycle.next)
44
+ genome_1, genome_2 = parent_genome_cycle.next
45
+ genome = combine_genomes(genome_1, genome_2)
31
46
  population.new_evolvable(genome: genome)
32
47
  end
33
48
  end
34
49
 
35
- private
36
-
37
- def build_genome(genome_pair)
50
+ #
51
+ # Combines two parent genomes to create a new genome.
52
+ # For each gene key, combines the count genes and individual genes.
53
+ #
54
+ # @param genome_1 [Evolvable::Genome] The first parent genome
55
+ # @param genome_2 [Evolvable::Genome] The second parent genome
56
+ # @return [Evolvable::Genome] A new genome resulting from the combination
57
+ #
58
+ def combine_genomes(genome_1, genome_2)
38
59
  new_config = {}
39
- genome_1, genome_2 = genome_pair.shuffle!
40
60
  genome_1.each do |gene_key, gene_config_1|
41
61
  gene_config_2 = genome_2.config[gene_key]
42
62
  count_gene = combine_count_genes(gene_config_1, gene_config_2)
@@ -46,12 +66,33 @@ module Evolvable
46
66
  Genome.new(config: new_config)
47
67
  end
48
68
 
69
+ private
70
+
71
+ #
72
+ # Combines two count genes to create a new count gene.
73
+ # Delegates to the count gene class's combine method.
74
+ #
75
+ # @param gene_config_1 [Hash] Configuration for the first gene group
76
+ # @param gene_config_2 [Hash] Configuration for the second gene group
77
+ # @return [Evolvable::CountGene, Evolvable::RigidCountGene] A new count gene
78
+ #
49
79
  def combine_count_genes(gene_config_1, gene_config_2)
50
80
  count_gene_1 = gene_config_1[:count_gene]
51
81
  count_gene_2 = gene_config_2[:count_gene]
52
82
  count_gene_1.class.combine(count_gene_1, count_gene_2)
53
83
  end
54
84
 
85
+ #
86
+ # Combines genes from two parent gene configurations.
87
+ # For each gene position, if both genes exist, uses the gene class's
88
+ # combine method to create a new gene. Otherwise, uses the existing
89
+ # gene or creates a new one.
90
+ #
91
+ # @param count [Integer] The number of genes to create
92
+ # @param gene_config_1 [Hash] Configuration for the first gene group
93
+ # @param gene_config_2 [Hash] Configuration for the second gene group
94
+ # @return [Array<Evolvable::Gene>] An array of combined genes
95
+ #
55
96
  def combine_genes(count, gene_config_1, gene_config_2)
56
97
  genes_1 = gene_config_1[:genes]
57
98
  genes_2 = gene_config_2[:genes]