evolvable 1.0.0 → 1.2.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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +4 -0
  3. data/CHANGELOG.md +56 -1
  4. data/Gemfile +3 -0
  5. data/Gemfile.lock +38 -21
  6. data/LICENSE +21 -0
  7. data/README.md +234 -161
  8. data/README_YARD.md +237 -0
  9. data/bin/console +18 -5
  10. data/evolvable.gemspec +2 -2
  11. data/examples/ascii_art.rb +62 -0
  12. data/examples/ascii_gene.rb +9 -0
  13. data/examples/hello_world.rb +91 -0
  14. data/examples/images/diagram.png +0 -0
  15. data/examples/stickman.rb +77 -0
  16. data/exe/hello +16 -0
  17. data/lib/evolvable/count_gene.rb +42 -0
  18. data/lib/evolvable/equalize_goal.rb +29 -0
  19. data/lib/evolvable/evaluation.rb +29 -6
  20. data/lib/evolvable/evolution.rb +40 -8
  21. data/lib/evolvable/gene.rb +54 -2
  22. data/lib/evolvable/gene_combination.rb +73 -0
  23. data/lib/evolvable/genome.rb +86 -0
  24. data/lib/evolvable/goal.rb +36 -3
  25. data/lib/evolvable/maximize_goal.rb +30 -0
  26. data/lib/evolvable/minimize_goal.rb +29 -0
  27. data/lib/evolvable/mutation.rb +66 -15
  28. data/lib/evolvable/point_crossover.rb +33 -19
  29. data/lib/evolvable/population.rb +171 -31
  30. data/lib/evolvable/rigid_count_gene.rb +17 -0
  31. data/lib/evolvable/search_space.rb +181 -0
  32. data/lib/evolvable/selection.rb +28 -1
  33. data/lib/evolvable/serializer.rb +21 -0
  34. data/lib/evolvable/uniform_crossover.rb +28 -8
  35. data/lib/evolvable/version.rb +1 -1
  36. data/lib/evolvable.rb +197 -29
  37. metadata +38 -27
  38. data/.rubocop.yml +0 -20
  39. data/examples/evolvable_string/char_gene.rb +0 -9
  40. data/examples/evolvable_string.rb +0 -32
  41. data/lib/evolvable/gene_crossover.rb +0 -28
  42. data/lib/evolvable/gene_space.rb +0 -37
  43. data/lib/evolvable/goal/equalize.rb +0 -19
  44. data/lib/evolvable/goal/maximize.rb +0 -19
  45. 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.0'
4
+ VERSION = '1.2.0'
5
5
  end