evolvable 0.1.3 → 1.1.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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +0 -1
  3. data/.yardopts +4 -0
  4. data/CHANGELOG.md +63 -0
  5. data/Gemfile +3 -0
  6. data/Gemfile.lock +39 -39
  7. data/LICENSE +21 -0
  8. data/README.md +234 -248
  9. data/README_YARD.md +237 -0
  10. data/bin/console +20 -43
  11. data/evolvable.gemspec +2 -3
  12. data/examples/ascii_art.rb +62 -0
  13. data/examples/ascii_gene.rb +9 -0
  14. data/examples/hello_world.rb +91 -0
  15. data/examples/images/diagram.png +0 -0
  16. data/examples/stickman.rb +77 -0
  17. data/exe/hello +16 -0
  18. data/lib/evolvable/count_gene.rb +42 -0
  19. data/lib/evolvable/equalize_goal.rb +29 -0
  20. data/lib/evolvable/error/undefined_method.rb +7 -0
  21. data/lib/evolvable/evaluation.rb +74 -0
  22. data/lib/evolvable/evolution.rb +58 -0
  23. data/lib/evolvable/gene.rb +67 -0
  24. data/lib/evolvable/gene_combination.rb +69 -0
  25. data/lib/evolvable/genome.rb +86 -0
  26. data/lib/evolvable/goal.rb +52 -0
  27. data/lib/evolvable/maximize_goal.rb +30 -0
  28. data/lib/evolvable/minimize_goal.rb +29 -0
  29. data/lib/evolvable/mutation.rb +71 -42
  30. data/lib/evolvable/point_crossover.rb +71 -0
  31. data/lib/evolvable/population.rb +202 -83
  32. data/lib/evolvable/rigid_count_gene.rb +17 -0
  33. data/lib/evolvable/search_space.rb +181 -0
  34. data/lib/evolvable/selection.rb +45 -0
  35. data/lib/evolvable/serializer.rb +21 -0
  36. data/lib/evolvable/uniform_crossover.rb +42 -0
  37. data/lib/evolvable/version.rb +1 -1
  38. data/lib/evolvable.rb +203 -56
  39. metadata +46 -24
  40. data/.rubocop.yml +0 -20
  41. data/lib/evolvable/crossover.rb +0 -35
  42. data/lib/evolvable/errors/not_implemented.rb +0 -5
  43. data/lib/evolvable/helper_methods.rb +0 -45
  44. data/lib/evolvable/hooks.rb +0 -9
@@ -1,119 +1,238 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Evolvable
4
+ #
5
+ # @readme
6
+ # Population objects are responsible for generating and evolving instances.
7
+ # They orchestrate all the other Evolvable objects to do so.
8
+ #
9
+ # Populations can be initialized and re-initialized with a number of useful
10
+ # parameters.
11
+ #
12
+ # @example
13
+ # # TODO: initialize a population with all supported parameters
4
14
  class Population
5
15
  extend Forwardable
6
16
 
7
- def initialize(evolvable_class:,
8
- size: 20,
9
- selection_count: 2,
10
- crossover: Crossover.new,
11
- mutation: Mutation.new,
12
- generation_count: 0,
13
- log_progress: false,
14
- objects: [])
15
- @evolvable_class = evolvable_class
17
+ def self.load(data)
18
+ dump_attrs = Serializer.load(data)
19
+ new(**dump_attrs)
20
+ end
21
+
22
+ # 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
48
+ id: nil,
49
+ name: nil,
50
+ size: 40,
51
+ evolutions_count: 0,
52
+ gene_space: nil, # Deprecated
53
+ search_space: nil,
54
+ parent_evolvables: [],
55
+ evaluation: Evaluation.new,
56
+ evolution: Evolution.new,
57
+ selection: nil,
58
+ combination: nil,
59
+ mutation: nil,
60
+ evolvables: [])
61
+ @id = id
62
+ @evolvable_type = evolvable_type || evolvable_class
63
+ @name = name
16
64
  @size = size
17
- @selection_count = selection_count
18
- @crossover = crossover
19
- @mutation = mutation
20
- @generation_count = generation_count
21
- @log_progress = log_progress
22
- assign_objects(objects)
65
+ @evolutions_count = evolutions_count
66
+ @search_space = initialize_search_space(search_space || gene_space)
67
+ @parent_evolvables = parent_evolvables
68
+ self.evaluation = evaluation
69
+ @evolution = evolution
70
+ self.selection = selection if selection
71
+ self.combination = combination if combination
72
+ self.mutation = mutation if mutation
73
+ @evolvables = new_evolvables(count: @size - evolvables.count, evolvables: evolvables)
23
74
  end
24
75
 
25
- attr_accessor :evolvable_class,
76
+ attr_accessor :id,
77
+ :evolvable_type,
78
+ :name,
26
79
  :size,
27
- :selection_count,
28
- :crossover,
29
- :mutation,
30
- :generation_count,
31
- :log_progress,
32
- :objects
33
-
34
- def_delegators :@evolvable_class,
35
- :evolvable_evaluate!,
36
- :evolvable_initialize,
37
- :evolvable_random_genes,
38
- :evolvable_before_evolution,
39
- :evolvable_after_select,
40
- :evolvable_after_evolution
41
-
42
- def evolve!(generations_count: 1, fitness_goal: nil)
43
- @fitness_goal = fitness_goal
44
- generations_count.times do
45
- @generation_count += 1
46
- evolvable_before_evolution(self)
47
- evaluate_objects!
48
- log_evolvable_progress if log_progress
49
- break if fitness_goal_met?
50
-
51
- select_objects!
52
- evolvable_after_select(self)
53
- crossover_objects!
54
- mutate_objects!
55
- evolvable_after_evolution(self)
80
+ :evolutions_count,
81
+ :search_space,
82
+ :evolution,
83
+ :parent_evolvables,
84
+ :evolvables
85
+
86
+ def_delegators :evolvable_class,
87
+ :before_evaluation,
88
+ :before_evolution,
89
+ :after_evolution
90
+
91
+ def_delegators :evolution,
92
+ :selection,
93
+ :selection=,
94
+ :combination,
95
+ :combination=,
96
+ :mutation,
97
+ :mutation=
98
+
99
+ def_delegator :selection, :size, :selection_size
100
+ def_delegator :selection, :size=, :selection_size=
101
+
102
+ def_delegator :mutation, :rate, :mutation_rate
103
+ def_delegator :mutation, :rate=, :mutation_rate=
104
+ def_delegator :mutation, :probability, :mutation_probability
105
+ def_delegator :mutation, :probability=, :mutation_probability=
106
+
107
+ attr_reader :evaluation
108
+
109
+ def evaluation=(val)
110
+ @evaluation = Evolvable.new_object(@evaluation, val, Evaluation)
111
+ end
112
+
113
+ def_delegators :evaluation,
114
+ :goal,
115
+ :goal=
116
+
117
+ #
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.
130
+ #
131
+ # ### Evolvable::Population#met_goal?
132
+ # Returns true if any instance's value matches the goal value, otherwise false.
133
+ #
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)
148
+ goal.value = goal_value if goal_value
149
+ 1.upto(count) do
150
+ before_evaluation(self)
151
+ evaluation.call(self)
152
+ before_evolution(self)
153
+ break if met_goal?
154
+
155
+ evolution.call(self)
156
+ self.evolutions_count += 1
157
+ after_evolution(self)
56
158
  end
57
159
  end
58
160
 
59
- def strongest_object
60
- objects.max_by(&:fitness)
161
+ def best_evolvable
162
+ evaluation.best_evolvable(self)
61
163
  end
62
164
 
63
- def evaluate_objects!
64
- evolvable_evaluate!(@objects)
65
- if @fitness_goal
66
- @objects.sort_by! { |i| -(i.fitness - @fitness_goal).abs }
67
- else
68
- @objects.sort_by!(&:fitness)
69
- end
165
+ def met_goal?
166
+ evaluation.met_goal?(self)
70
167
  end
71
168
 
72
- def log_evolvable_progress
73
- @objects.last.evolvable_progress
169
+ def new_evolvable(genome: nil)
170
+ return generate_evolvables(1).first unless genome || parent_evolvables.empty?
171
+
172
+ evolvable = evolvable_class.new_evolvable(population: self,
173
+ genome: genome || search_space.new_genome,
174
+ generation_index: @evolvables.count)
175
+ @evolvables << evolvable
176
+ evolvable
74
177
  end
75
178
 
76
- def fitness_goal_met?
77
- @fitness_goal && @objects.last.fitness >= @fitness_goal
179
+ def new_evolvables(count:, evolvables: nil)
180
+ evolvables ||= @evolvables || []
181
+ @evolvables = evolvables
182
+
183
+ if parent_evolvables.empty?
184
+ Array.new(count) { new_evolvable(genome: search_space.new_genome) }
185
+ else
186
+ @evolvables = generate_evolvables(count)
187
+ end
78
188
  end
79
189
 
80
- def select_objects!
81
- @objects.slice!(0..-1 - @selection_count)
190
+ def reset_evolvables
191
+ self.evolvables = []
192
+ new_evolvables(count: size)
82
193
  end
83
194
 
84
- def crossover_objects!
85
- parent_genes = @objects.map(&:genes)
86
- offspring_genes = @crossover.call(parent_genes, @size)
87
- @objects = offspring_genes.map.with_index do |genes, i|
88
- evolvable_initialize(genes, self, i)
89
- end
195
+ def new_parent_genome_cycle
196
+ parent_evolvables.map(&:genome).shuffle!.combination(2).cycle
90
197
  end
91
198
 
92
- def mutate_objects!
93
- @mutation.call!(@objects)
199
+ def evolvable_class
200
+ @evolvable_class ||= evolvable_type.is_a?(Class) ? evolvable_type : Object.const_get(evolvable_type)
94
201
  end
95
202
 
96
- def inspect
97
- "#<#{self.class.name} #{as_json} >"
203
+ def dump(only: nil, except: nil)
204
+ Serializer.dump(dump_attrs(only: only, except: except))
98
205
  end
99
206
 
100
- def as_json
101
- { evolvable_class: @evolvable_class.name,
102
- size: @size,
103
- selection_count: @selection_count,
104
- crossover: @crossover.as_json,
105
- mutation: @mutation.as_json,
106
- generation_count: @generation_count }
207
+ DUMP_METHODS = %i[evolvable_type
208
+ id
209
+ name
210
+ size
211
+ evolutions_count
212
+ search_space
213
+ evolution
214
+ evaluation].freeze
215
+
216
+ def dump_attrs(only: nil, except: nil)
217
+ attrs = {}
218
+ dump_methods = only || DUMP_METHODS
219
+ dump_methods -= except if except
220
+ dump_methods.each { |m| attrs[m] = send(m) }
221
+ attrs
107
222
  end
108
223
 
109
224
  private
110
225
 
111
- def assign_objects(objects)
112
- @objects = objects || []
113
- (@size - objects.count).times do |n|
114
- genes = evolvable_random_genes
115
- @objects << evolvable_initialize(genes, self, n)
116
- end
226
+ def initialize_search_space(search_space)
227
+ return SearchSpace.build(search_space, evolvable_class) if search_space
228
+
229
+ evolvable_class.new_search_space
230
+ end
231
+
232
+ def generate_evolvables(count)
233
+ evolvables = combination.new_evolvables(self, count)
234
+ mutation.mutate_evolvables(evolvables)
235
+ evolvables
117
236
  end
118
237
  end
119
238
  end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Evolvable
4
+ class RigidCountGene
5
+ include Gene
6
+
7
+ def self.combine(gene_a, _gene_b)
8
+ gene_a
9
+ end
10
+
11
+ def initialize(count)
12
+ @count = count
13
+ end
14
+
15
+ attr_reader :count
16
+ end
17
+ end
@@ -0,0 +1,181 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Evolvable
4
+ #
5
+ # @readme
6
+ # The search space encapsulates the range of possible genes
7
+ # for a particular evolvable. You can think of it as the boundaries of
8
+ # genetic variation. It is configured via the
9
+ # [.search_space](#evolvableclasssearch_space) method that you define
10
+ # on your evolvable class. It's used by populations to initialize
11
+ # new evolvables.
12
+ #
13
+ # Evolvable provides flexibility in how you define your search space.
14
+ # The below example implementations for `.search_space` produce the
15
+ # exact same search space for the
16
+ # [Hello World](https://github.com/mattruzicka/evolvable#hello-world)
17
+ # demo program. The different styles arguably vary in suitability for
18
+ # different contexts, perhaps depending on how programs are loaded and
19
+ # the number of different gene types.
20
+ #
21
+ # @example
22
+ # # All 9 of these example definitions are equivalent
23
+ #
24
+ # # Hash syntax
25
+ # { chars: { type: 'CharGene', max_count: 100 } }
26
+ # { chars: { type: 'CharGene', min_count: 1, max_count: 100 } }
27
+ # { chars: { type: 'CharGene', count: 1..100 } }
28
+ #
29
+ # # Array of arrays syntax
30
+ # [[:chars, 'CharGene', 1..100]]
31
+ # [['chars', 'CharGene', 1..100]]
32
+ # [['CharGene', 1..100]]
33
+ #
34
+ # # A single array works when there's only one type of gene
35
+ # ['CharGene', 1..100]
36
+ # [:chars, 'CharGene', 1..100]
37
+ # ['chars', 'CharGene', 1..100]
38
+ #
39
+ #
40
+
41
+ class SearchSpace
42
+ class << self
43
+ def build(config, evolvable_class = nil)
44
+ if config.is_a?(SearchSpace)
45
+ config.evolvable_class = evolvable_class if evolvable_class
46
+ config
47
+ else
48
+ new(config: config, evolvable_class: evolvable_class)
49
+ end
50
+ end
51
+ end
52
+
53
+ def initialize(config: {}, evolvable_class: nil)
54
+ @evolvable_class = evolvable_class
55
+ @config = normalize_config(config)
56
+ end
57
+
58
+ attr_accessor :evolvable_class, :config
59
+
60
+ def new_genome
61
+ Genome.new(config: new_genome_config)
62
+ end
63
+
64
+ def merge_search_space(val)
65
+ val = val.config if val.is_a?(self.class)
66
+ @config.merge normalize_config(val)
67
+ end
68
+
69
+ def merge_search_space!(val)
70
+ val = val.config if val.is_a?(self.class)
71
+ @config.merge! normalize_config(val)
72
+ end
73
+
74
+ private
75
+
76
+ def normalize_config(config)
77
+ case config
78
+ when Hash
79
+ normalize_hash_config(config)
80
+ when Array
81
+ if config.first.is_a?(Array)
82
+ build_config_from_2d_array(config)
83
+ else
84
+ merge_config_with_array({}, config)
85
+ end
86
+ end
87
+ end
88
+
89
+ def normalize_hash_config(config)
90
+ config.each do |gene_key, gene_config|
91
+ next unless gene_config[:type]
92
+
93
+ gene_class = lookup_gene_class(gene_config[:type])
94
+ gene_class.key = gene_key
95
+ gene_config[:class] = gene_class
96
+ end
97
+ end
98
+
99
+ def build_config_from_2d_array(array_config)
100
+ config = {}
101
+ array_config.each { |array| merge_config_with_array(config, array) }
102
+ config
103
+ end
104
+
105
+ def merge_config_with_array(config, gene_array)
106
+ gene_key, gene_class, count = extract_array_configs(gene_array)
107
+ gene_class.key = gene_key
108
+ config[gene_key] = { class: gene_class, count: count }
109
+ config
110
+ end
111
+
112
+ def extract_array_configs(gene_array)
113
+ first_item = gene_array.first
114
+ return extract_array_with_key_configs(gene_array) if first_item.is_a?(Symbol)
115
+
116
+ gene_class = lookup_gene_class(first_item)
117
+ _type, count = gene_array
118
+ [gene_class, gene_class, count]
119
+ rescue NameError
120
+ extract_array_with_key_configs(gene_array)
121
+ end
122
+
123
+ def extract_array_with_key_configs(gene_array)
124
+ gene_key, type, count = gene_array
125
+ gene_class = lookup_gene_class(type)
126
+ [gene_key, gene_class, count]
127
+ end
128
+
129
+ def lookup_gene_class(class_name)
130
+ return class_name if class_name.is_a?(Class)
131
+
132
+ (@evolvable_class || Object).const_get(class_name)
133
+ end
134
+
135
+ def new_genome_config
136
+ genome_config = {}
137
+ config.each do |gene_key, gene_config|
138
+ genome_config[gene_key] = new_gene_config(gene_config)
139
+ end
140
+ genome_config
141
+ end
142
+
143
+ def new_gene_config(gene_config)
144
+ count_gene = init_count_gene(gene_config)
145
+ gene_class = gene_config[:class]
146
+ genes = Array.new(count_gene.count) { gene_class.new }
147
+ { count_gene: count_gene, genes: genes }
148
+ end
149
+
150
+ def init_count_gene(gene_config)
151
+ min = gene_config[:min_count]
152
+ max = gene_config[:max_count]
153
+ return init_min_max_count_gene(min, max) if min || max
154
+
155
+ count = gene_config[:count] || 1
156
+ case count
157
+ when Numeric
158
+ RigidCountGene.new(count)
159
+ when Range
160
+ CountGene.new(range: count)
161
+ when String
162
+ Kernel.const_get(gene_config[:count]).new
163
+ when Class
164
+ gene_config[:count].new
165
+ end
166
+ end
167
+
168
+ def init_min_max_count_gene(min, max)
169
+ min ||= 1
170
+ max ||= min * 10
171
+ CountGene.new(range: min..max)
172
+ end
173
+ end
174
+
175
+ #
176
+ # @deprecated
177
+ # Will be removed in 2.0
178
+ # use {SearchSpace} instead
179
+ #
180
+ class GeneSpace < SearchSpace; end
181
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Evolvable
4
+ #
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.
9
+ #
10
+ # Only two evolvables are selected as parents for each generation by default.
11
+ # The selection size is configurable.
12
+ #
13
+ # @example
14
+ # # TODO: Show how to add/change population's selection object
15
+ #
16
+ class Selection
17
+ extend Forwardable
18
+
19
+ #
20
+ # Initializes a new selection object.
21
+ #
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.
28
+ #
29
+ def initialize(size: 2)
30
+ @size = size
31
+ end
32
+
33
+ attr_accessor :size
34
+
35
+ def call(population)
36
+ population.parent_evolvables = select_evolvables(population.evolvables)
37
+ population.evolvables = []
38
+ population
39
+ end
40
+
41
+ def select_evolvables(evolvables)
42
+ evolvables.last(@size)
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Evolvable
4
+ class Serializer
5
+ class << self
6
+ def dump(data)
7
+ klass.dump(data)
8
+ end
9
+
10
+ def load(data)
11
+ klass.load(data)
12
+ end
13
+
14
+ private
15
+
16
+ def klass
17
+ @klass ||= Marshal
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Evolvable
4
+ #
5
+ # Randomly chooses a gene from one of the parents for each gene position.
6
+ #
7
+ class UniformCrossover
8
+ def call(population)
9
+ population.evolvables = new_evolvables(population, population.size)
10
+ population
11
+ end
12
+
13
+ def new_evolvables(population, count)
14
+ parent_genome_cycle = population.new_parent_genome_cycle
15
+ Array.new(count) do
16
+ genome = build_genome(parent_genome_cycle.next)
17
+ population.new_evolvable(genome: genome)
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def build_genome(genome_pair)
24
+ new_config = {}
25
+ genome_1, genome_2 = genome_pair.shuffle!
26
+ 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])
29
+ new_config[gene_key] = { count_gene: count_gene, genes: genes }
30
+ end
31
+ Genome.new(config: new_config)
32
+ end
33
+
34
+ def crossover_genes(count, gene_config_1, gene_config_2)
35
+ gene_arrays = [gene_config_1[:genes], gene_config_2[:genes]]
36
+ Array.new(count) do |index|
37
+ genes = gene_arrays.sample
38
+ genes[index] || gene_arrays.detect { |a| a != genes }[index]
39
+ end
40
+ end
41
+ end
42
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Evolvable
4
- VERSION = '0.1.3'
4
+ VERSION = '1.1.0'
5
5
  end