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/evaluation.rb
CHANGED
@@ -3,67 +3,166 @@
|
|
3
3
|
module Evolvable
|
4
4
|
#
|
5
5
|
# @readme
|
6
|
-
#
|
7
|
-
#
|
8
|
-
#
|
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
|
-
#
|
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
|
-
#
|
16
|
-
#
|
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
|
-
#
|
19
|
-
#
|
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
|
-
|
23
|
-
|
24
|
-
|
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]
|
data/lib/evolvable/evolution.rb
CHANGED
@@ -3,32 +3,30 @@
|
|
3
3
|
module Evolvable
|
4
4
|
#
|
5
5
|
# @readme
|
6
|
-
#
|
7
|
-
#
|
8
|
-
#
|
9
|
-
#
|
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
|
-
#
|
16
|
-
#
|
17
|
-
# Keyword arguments:
|
19
|
+
# Initializes a new evolution object.
|
18
20
|
#
|
19
|
-
#
|
20
|
-
# The
|
21
|
-
#
|
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 =
|
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)
|
data/lib/evolvable/gene.rb
CHANGED
@@ -3,61 +3,160 @@
|
|
3
3
|
module Evolvable
|
4
4
|
#
|
5
5
|
# @readme
|
6
|
-
#
|
7
|
-
#
|
8
|
-
#
|
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
|
-
#
|
12
|
-
#
|
13
|
-
#
|
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
|
-
#
|
20
|
-
#
|
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
|
-
#
|
39
|
+
# **Gene Count**
|
23
40
|
#
|
24
|
-
#
|
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
|
28
|
-
#
|
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
|
7
|
-
#
|
8
|
-
#
|
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
|
11
|
-
#
|
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
|
-
#
|
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
|
-
#
|
18
|
-
#
|
19
|
-
#
|
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
|
-
|
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
|
-
|
36
|
-
|
37
|
-
|
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]
|