evolvable 0.1.3 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
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