evolvable 1.0.2 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +4 -0
  3. data/CHANGELOG.md +37 -0
  4. data/Gemfile +3 -0
  5. data/Gemfile.lock +38 -2
  6. data/README.md +227 -290
  7. data/README_YARD.md +237 -0
  8. data/bin/console +18 -5
  9. data/evolvable.gemspec +2 -0
  10. data/examples/ascii_art.rb +62 -0
  11. data/examples/ascii_gene.rb +9 -0
  12. data/examples/hello_world.rb +91 -0
  13. data/examples/images/diagram.png +0 -0
  14. data/examples/stickman.rb +77 -0
  15. data/exe/hello +16 -0
  16. data/lib/evolvable/count_gene.rb +42 -0
  17. data/lib/evolvable/equalize_goal.rb +29 -0
  18. data/lib/evolvable/evaluation.rb +29 -6
  19. data/lib/evolvable/evolution.rb +38 -6
  20. data/lib/evolvable/gene.rb +47 -5
  21. data/lib/evolvable/gene_combination.rb +69 -0
  22. data/lib/evolvable/genome.rb +86 -0
  23. data/lib/evolvable/goal.rb +36 -3
  24. data/lib/evolvable/maximize_goal.rb +30 -0
  25. data/lib/evolvable/minimize_goal.rb +29 -0
  26. data/lib/evolvable/mutation.rb +66 -14
  27. data/lib/evolvable/point_crossover.rb +33 -19
  28. data/lib/evolvable/population.rb +171 -31
  29. data/lib/evolvable/rigid_count_gene.rb +17 -0
  30. data/lib/evolvable/search_space.rb +181 -0
  31. data/lib/evolvable/selection.rb +28 -1
  32. data/lib/evolvable/serializer.rb +21 -0
  33. data/lib/evolvable/uniform_crossover.rb +28 -8
  34. data/lib/evolvable/version.rb +1 -1
  35. data/lib/evolvable.rb +191 -31
  36. metadata +49 -10
  37. data/examples/evolvable_string/char_gene.rb +0 -9
  38. data/examples/evolvable_string.rb +0 -32
  39. data/lib/evolvable/gene_crossover.rb +0 -28
  40. data/lib/evolvable/gene_space.rb +0 -40
  41. data/lib/evolvable/goal/equalize.rb +0 -19
  42. data/lib/evolvable/goal/maximize.rb +0 -19
  43. data/lib/evolvable/goal/minimize.rb +0 -19
@@ -1,38 +1,87 @@
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(id: nil,
8
- 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,
9
49
  name: nil,
10
50
  size: 40,
11
51
  evolutions_count: 0,
12
- gene_space: nil,
13
- evolution: Evolution.new,
52
+ gene_space: nil, # Deprecated
53
+ search_space: nil,
54
+ parent_evolvables: [],
14
55
  evaluation: Evaluation.new,
15
- instances: [])
56
+ evolution: Evolution.new,
57
+ selection: nil,
58
+ combination: nil,
59
+ mutation: nil,
60
+ evolvables: [])
16
61
  @id = id
17
- @evolvable_class = evolvable_class
62
+ @evolvable_type = evolvable_type || evolvable_class
18
63
  @name = name
19
64
  @size = size
20
65
  @evolutions_count = evolutions_count
21
- @gene_space = initialize_gene_space(gene_space)
66
+ @search_space = initialize_search_space(search_space || gene_space)
67
+ @parent_evolvables = parent_evolvables
68
+ self.evaluation = evaluation
22
69
  @evolution = evolution
23
- @evaluation = evaluation || Evaluation.new
24
- initialize_instances(instances)
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)
25
74
  end
26
75
 
27
76
  attr_accessor :id,
28
- :evolvable_class,
77
+ :evolvable_type,
29
78
  :name,
30
79
  :size,
31
80
  :evolutions_count,
32
- :gene_space,
81
+ :search_space,
33
82
  :evolution,
34
- :evaluation,
35
- :instances
83
+ :parent_evolvables,
84
+ :evolvables
36
85
 
37
86
  def_delegators :evolvable_class,
38
87
  :before_evaluation,
@@ -42,18 +91,62 @@ module Evolvable
42
91
  def_delegators :evolution,
43
92
  :selection,
44
93
  :selection=,
45
- :crossover,
46
- :crossover=,
94
+ :combination,
95
+ :combination=,
47
96
  :mutation,
48
97
  :mutation=
49
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
+
50
113
  def_delegators :evaluation,
51
114
  :goal,
52
115
  :goal=
53
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
+ #
54
147
  def evolve(count: Float::INFINITY, goal_value: nil)
55
148
  goal.value = goal_value if goal_value
56
- (1..count).each do
149
+ 1.upto(count) do
57
150
  before_evaluation(self)
58
151
  evaluation.call(self)
59
152
  before_evolution(self)
@@ -65,34 +158,81 @@ module Evolvable
65
158
  end
66
159
  end
67
160
 
68
- def best_instance
69
- evaluation.best_instance(self)
161
+ def best_evolvable
162
+ evaluation.best_evolvable(self)
70
163
  end
71
164
 
72
165
  def met_goal?
73
166
  evaluation.met_goal?(self)
74
167
  end
75
168
 
76
- def new_instance(genes: [], population_index: nil)
77
- evolvable_class.new_instance(population: self,
78
- genes: genes,
79
- population_index: population_index)
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
177
+ end
178
+
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
188
+ end
189
+
190
+ def reset_evolvables
191
+ self.evolvables = []
192
+ new_evolvables(count: size)
193
+ end
194
+
195
+ def new_parent_genome_cycle
196
+ parent_evolvables.map(&:genome).shuffle!.combination(2).cycle
197
+ end
198
+
199
+ def evolvable_class
200
+ @evolvable_class ||= evolvable_type.is_a?(Class) ? evolvable_type : Object.const_get(evolvable_type)
201
+ end
202
+
203
+ def dump(only: nil, except: nil)
204
+ Serializer.dump(dump_attrs(only: only, except: except))
205
+ end
206
+
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
80
222
  end
81
223
 
82
224
  private
83
225
 
84
- def initialize_gene_space(gene_space)
85
- return GeneSpace.build(gene_space) if gene_space
226
+ def initialize_search_space(search_space)
227
+ return SearchSpace.build(search_space, evolvable_class) if search_space
86
228
 
87
- evolvable_class.new_gene_space
229
+ evolvable_class.new_search_space
88
230
  end
89
231
 
90
- def initialize_instances(instances)
91
- @instances = instances || []
92
- (@size - instances.count).times do |n|
93
- genes = gene_space.new_genes
94
- @instances << new_instance(genes: genes, population_index: n)
95
- end
232
+ def generate_evolvables(count)
233
+ evolvables = combination.new_evolvables(self, count)
234
+ mutation.mutate_evolvables(evolvables)
235
+ evolvables
96
236
  end
97
237
  end
98
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
@@ -1,9 +1,31 @@
1
1
  # frozen_string_literal: true
2
2
 
3
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
+ #
4
16
  class Selection
5
17
  extend Forwardable
6
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
+ #
7
29
  def initialize(size: 2)
8
30
  @size = size
9
31
  end
@@ -11,8 +33,13 @@ module Evolvable
11
33
  attr_accessor :size
12
34
 
13
35
  def call(population)
14
- population.instances.slice!(0..-1 - @size)
36
+ population.parent_evolvables = select_evolvables(population.evolvables)
37
+ population.evolvables = []
15
38
  population
16
39
  end
40
+
41
+ def select_evolvables(evolvables)
42
+ evolvables.last(@size)
43
+ end
17
44
  end
18
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
@@ -1,21 +1,41 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Evolvable
4
+ #
5
+ # Randomly chooses a gene from one of the parents for each gene position.
6
+ #
4
7
  class UniformCrossover
5
8
  def call(population)
6
- population.instances = initialize_offspring(population)
9
+ population.evolvables = new_evolvables(population, population.size)
7
10
  population
8
11
  end
9
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
+
10
21
  private
11
22
 
12
- def initialize_offspring(population)
13
- parent_genes = population.instances.map!(&:genes)
14
- parent_gene_couples = parent_genes.combination(2).cycle
15
- Array.new(population.size) do |index|
16
- genes_1, genes_2 = parent_gene_couples.next
17
- genes = genes_1.zip(genes_2).map!(&:sample)
18
- population.new_instance(genes: genes, population_index: index)
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]
19
39
  end
20
40
  end
21
41
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Evolvable
4
- VERSION = '1.0.2'
4
+ VERSION = '1.1.0'
5
5
  end