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
data/lib/evolvable.rb CHANGED
@@ -4,70 +4,230 @@ require 'forwardable'
4
4
  require 'evolvable/version'
5
5
  require 'evolvable/error/undefined_method'
6
6
  require 'evolvable/gene'
7
- require 'evolvable/gene_space'
7
+ require 'evolvable/search_space'
8
+ require 'evolvable/genome'
8
9
  require 'evolvable/goal'
9
- require 'evolvable/goal/equalize'
10
- require 'evolvable/goal/maximize'
11
- require 'evolvable/goal/minimize'
10
+ require 'evolvable/equalize_goal'
11
+ require 'evolvable/maximize_goal'
12
+ require 'evolvable/minimize_goal'
12
13
  require 'evolvable/evaluation'
13
14
  require 'evolvable/evolution'
14
15
  require 'evolvable/selection'
15
- require 'evolvable/gene_crossover'
16
+ require 'evolvable/gene_combination'
16
17
  require 'evolvable/point_crossover'
17
18
  require 'evolvable/uniform_crossover'
18
19
  require 'evolvable/mutation'
19
20
  require 'evolvable/population'
21
+ require 'evolvable/count_gene'
22
+ require 'evolvable/rigid_count_gene'
23
+ require 'evolvable/serializer'
20
24
 
25
+ #
26
+ # @readme
27
+ # The `Evolvable` module makes it possible to implement evolutionary behaviors for
28
+ # any class by defining a `.search_space` class method and `#value` instance method.
29
+ # Then to evolve instances, initialize a population with `.new_population` and invoke
30
+ # the `#evolve` method on the resulting population object.
31
+ #
32
+ # ### Implementation Steps
33
+ #
34
+ # 1. [Include the `Evolvable` module in the class you want to evolve.](https://rubydoc.info/github/mattruzicka/evolvable/Evolvable)
35
+ # 2. [Define `.search_space` and any gene classes that you reference.](https://rubydoc.info/github/mattruzicka/evolvable/Evolvable/SearchSpace)
36
+ # 3. [Define `#value`.](https://rubydoc.info/github/mattruzicka/evolvable/Evolvable/Evaluation)
37
+ # 4. [Initialize a population with `.new_population` and use `#evolve`.](https://rubydoc.info/github/mattruzicka/evolvable/Evolvable/Population)
38
+ #
21
39
  module Evolvable
40
+ extend Forwardable
41
+
22
42
  def self.included(base)
23
- def base.new_population(keyword_args = {})
24
- keyword_args[:evolvable_class] = self
43
+ base.extend(ClassMethods)
44
+ end
45
+
46
+ def self.new_object(old_val, new_val, default_class)
47
+ new_val.is_a?(Hash) ? (old_val&.class || default_class).new(**new_val) : new_val
48
+ end
49
+
50
+ module ClassMethods
51
+ #
52
+ # @readme
53
+ # Initializes a population using configurable defaults that can be configured and optimized.
54
+ # Accepts the same named parameters as
55
+ # [Population#initialize](https://rubydoc.info/github/mattruzicka/evolvable/Evolvable/Population#initialize).
56
+ #
57
+ def new_population(keyword_args = {})
58
+ keyword_args[:evolvable_type] = self
25
59
  Population.new(**keyword_args)
26
60
  end
27
61
 
28
- def base.new_instance(population: nil, genes: [], population_index: nil)
29
- evolvable = initialize_instance
62
+ #
63
+ # Initializes a new instance. Accepts a population object, an array of gene objects,
64
+ # and the instance's population index. This method is useful for re-initializing
65
+ # instances and populations that have been saved.
66
+ #
67
+ # _It is not recommended that you override this method_ as it is used by
68
+ # Evolvable internals. If you need to customize how your instances are
69
+ # initialized you can override either of the following two "initialize_instance"
70
+ # methods.
71
+ #
72
+ def new_evolvable(population: nil,
73
+ genome: Genome.new,
74
+ generation_index: nil)
75
+ evolvable = initialize_evolvable
30
76
  evolvable.population = population
31
- evolvable.genes = genes
32
- evolvable.population_index = population_index
33
- evolvable.initialize_instance
77
+ evolvable.genome = genome
78
+ evolvable.generation_index = generation_index
79
+ evolvable.after_initialize
34
80
  evolvable
35
81
  end
36
82
 
37
- def base.initialize_instance
83
+ def initialize_evolvable
38
84
  new
39
85
  end
40
86
 
41
- def base.new_gene_space
42
- GeneSpace.build(gene_space)
87
+ def new_search_space
88
+ space_config = search_space.empty? ? gene_space : search_space
89
+ search_space = SearchSpace.build(space_config, self)
90
+ search_spaces.each { |space| search_space.merge_search_space!(space) }
91
+ search_space
43
92
  end
44
93
 
45
- def base.gene_space
94
+ #
95
+ # @abstract
96
+ #
97
+ # This method is responsible for configuring the available gene types
98
+ # of evolvable instances. In effect, it provides the
99
+ # blueprint for constructing a hyperdimensional genetic space that's capable
100
+ # of being used and searched by evolvable objects.
101
+ #
102
+ # Override this method with a search space config for initializing
103
+ # SearchSpace objects. The config can be a hash, array of arrays,
104
+ # or single array when there's only one type of gene.
105
+ #
106
+ # The below example definitions could conceivably be used to generate evolvable music.
107
+ #
108
+ # @todo
109
+ # Define gene config attributes - name, type, count
110
+ #
111
+ # @example Hash config
112
+ # def search_space
113
+ # { instrument: { type: InstrumentGene, count: 1..4 },
114
+ # notes: { type: NoteGene, count: 16 } }
115
+ # end
116
+ # @example Array of arrays config
117
+ # # With explicit gene names
118
+ # def search_space
119
+ # [[:instrument, InstrumentGene, 1..4],
120
+ # [:notes, NoteGene, 16]]
121
+ # end
122
+ #
123
+ # # Without explicit gene names
124
+ # def search_space
125
+ # [[SynthGene, 0..4], [RhythmGene, 0..8]]
126
+ # end
127
+ # @example Array config
128
+ # # Available when when just one type of gene
129
+ # def search_space
130
+ # [NoteGene, 1..100]
131
+ # end
132
+ #
133
+ # # With explicit gene type name.
134
+ # def search_space
135
+ # ['notes', 'NoteGene', 1..100]
136
+ # end
137
+ #
138
+ # @return [Hash, Array]
139
+ #
140
+ # @see https://github.com/mattruzicka/evolvable#search_space
141
+ #
142
+ def search_space
46
143
  {}
47
144
  end
48
145
 
49
- def base.before_evaluation(population); end
146
+ #
147
+ # @abstract Override this method to define multiple search spaces
148
+ #
149
+ # @return [Array]
150
+ #
151
+ # @see https://github.com/mattruzicka/evolvable#search_space
152
+ #
153
+ def search_spaces
154
+ []
155
+ end
50
156
 
51
- def base.before_evolution(population); end
157
+ # @deprecated
158
+ # Will be removed in version 2.0.
159
+ # Use {#search_space} instead.
160
+ def gene_space
161
+ {}
162
+ end
52
163
 
53
- def base.after_evolution(population); end
54
- end
55
164
 
56
- def initialize_instance; end
165
+ #
166
+ # @readme
167
+ # Runs before evaluation.
168
+ #
169
+ def before_evaluation(population); end
57
170
 
58
- attr_accessor :population,
59
- :genes,
60
- :population_index
171
+ #
172
+ # @readme
173
+ # Runs after evaluation and before evolution.
174
+ #
175
+ # @example
176
+ # class Melody
177
+ # include Evolvable
178
+ #
179
+ # # Play the best melody from each generation
180
+ # def self.before_evolution(population)
181
+ # population.best_evolvable.play
182
+ # end
183
+ #
184
+ # # ...
185
+ # end
186
+ #
187
+ def before_evolution(population); end
61
188
 
62
- def value
63
- raise Errors::UndefinedMethod, "#{self.class.name}##{__method__}"
189
+ #
190
+ # @readme
191
+ # Runs after evolution.
192
+ #
193
+ def after_evolution(population); end
64
194
  end
65
195
 
66
- def find_gene(key)
67
- @genes.detect { |g| g.key == key }
68
- end
196
+ # Runs an evolvable is initialized. Ueful for implementing custom initialization logic.
197
+ def after_initialize; end
198
+
199
+ #
200
+ # @!method value
201
+ # Implementing this method is required for evaluation and selection.
202
+ #
203
+ attr_accessor :id,
204
+ :population,
205
+ :genome,
206
+ :generation_index,
207
+ :value
69
208
 
70
- def find_genes(key)
71
- @genes.select { |g| g.key == key }
209
+ #
210
+ # @deprecated
211
+ # Will be removed in version 2.0.
212
+ # Use {#generation_index} instead.
213
+ #
214
+ def population_index
215
+ generation_index
72
216
  end
217
+
218
+ #
219
+ # @!method find_gene
220
+ # @see Genome#find_gene
221
+ # @!method find_genes
222
+ # @see Genome#find_genes
223
+ # @!method find_genes_count
224
+ # @see Genome#find_genes_count
225
+ # @!method genes
226
+ # @see Genome#genes
227
+ #
228
+ def_delegators :genome,
229
+ :find_gene,
230
+ :find_genes,
231
+ :find_genes_count,
232
+ :genes
73
233
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: evolvable
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.2
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matt Ruzicka
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-04-04 00:00:00.000000000 Z
11
+ date: 2021-12-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -24,38 +24,77 @@ dependencies:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: '2.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: readme_yard
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: yard
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
27
55
  description:
28
56
  email:
29
- executables: []
57
+ executables:
58
+ - hello
30
59
  extensions: []
31
60
  extra_rdoc_files: []
32
61
  files:
33
62
  - ".gitignore"
63
+ - ".yardopts"
34
64
  - CHANGELOG.md
35
65
  - Gemfile
36
66
  - Gemfile.lock
37
67
  - LICENSE
38
68
  - README.md
69
+ - README_YARD.md
39
70
  - bin/console
40
71
  - bin/setup
41
72
  - evolvable.gemspec
42
- - examples/evolvable_string.rb
43
- - examples/evolvable_string/char_gene.rb
73
+ - examples/ascii_art.rb
74
+ - examples/ascii_gene.rb
75
+ - examples/hello_world.rb
76
+ - examples/images/diagram.png
77
+ - examples/stickman.rb
78
+ - exe/hello
44
79
  - lib/evolvable.rb
80
+ - lib/evolvable/count_gene.rb
81
+ - lib/evolvable/equalize_goal.rb
45
82
  - lib/evolvable/error/undefined_method.rb
46
83
  - lib/evolvable/evaluation.rb
47
84
  - lib/evolvable/evolution.rb
48
85
  - lib/evolvable/gene.rb
49
- - lib/evolvable/gene_crossover.rb
50
- - lib/evolvable/gene_space.rb
86
+ - lib/evolvable/gene_combination.rb
87
+ - lib/evolvable/genome.rb
51
88
  - lib/evolvable/goal.rb
52
- - lib/evolvable/goal/equalize.rb
53
- - lib/evolvable/goal/maximize.rb
54
- - lib/evolvable/goal/minimize.rb
89
+ - lib/evolvable/maximize_goal.rb
90
+ - lib/evolvable/minimize_goal.rb
55
91
  - lib/evolvable/mutation.rb
56
92
  - lib/evolvable/point_crossover.rb
57
93
  - lib/evolvable/population.rb
94
+ - lib/evolvable/rigid_count_gene.rb
95
+ - lib/evolvable/search_space.rb
58
96
  - lib/evolvable/selection.rb
97
+ - lib/evolvable/serializer.rb
59
98
  - lib/evolvable/uniform_crossover.rb
60
99
  - lib/evolvable/version.rb
61
100
  homepage: https://github.com/mattruzicka/evolvable
@@ -1,9 +0,0 @@
1
- class CharGene
2
- include Evolvable::Gene
3
-
4
- CHARS = ('a'..'z').to_a
5
-
6
- def to_s
7
- @to_s ||= CHARS.sample
8
- end
9
- end
@@ -1,32 +0,0 @@
1
- require './examples/evolvable_string/char_gene'
2
-
3
- class EvolvableString
4
- include Evolvable
5
-
6
- TARGET_STRING = 'supercalifragilisticexpialidocious'
7
-
8
- def self.gene_space
9
- { char_genes: { type: 'CharGene', count: TARGET_STRING.length } }
10
- end
11
-
12
- def self.before_evolution(population)
13
- best_instance = population.best_instance
14
- puts "#{best_instance} | #{best_instance.value} matches | Generation #{population.evolutions_count}"
15
- end
16
-
17
- def to_s
18
- find_genes(:char_genes).join
19
- end
20
-
21
- def value
22
- @value ||= compute_value
23
- end
24
-
25
- def compute_value
26
- value = 0
27
- find_genes(:char_genes).each_with_index do |gene, index|
28
- value += 1 if gene.to_s == TARGET_STRING[index]
29
- end
30
- value
31
- end
32
- end
@@ -1,28 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Evolvable
4
- class GeneCrossover
5
- def call(population)
6
- population.instances = initialize_offspring(population)
7
- population
8
- end
9
-
10
- private
11
-
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 = crossover_genes(genes_1, genes_2)
18
- population.new_instance(genes: genes, population_index: index)
19
- end
20
- end
21
-
22
- def crossover_genes(genes_1, genes_2)
23
- genes_1.zip(genes_2).map! do |gene_a, gene_b|
24
- gene_a.class.crossover(gene_a, gene_b)
25
- end
26
- end
27
- end
28
- end
@@ -1,40 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Evolvable
4
- class GeneSpace
5
- def self.build(config)
6
- return config if config.respond_to?(:new_genes)
7
-
8
- new(config: config)
9
- end
10
-
11
- def initialize(config: {})
12
- @config = normalize_config(config)
13
- end
14
-
15
- attr_reader :config
16
-
17
- def new_genes
18
- genes = []
19
- config.each do |_gene_key, gene_config|
20
- (gene_config[:count] || 1).times do
21
- gene = gene_config[:class].new
22
- genes << gene
23
- end
24
- end
25
- genes
26
- end
27
-
28
- private
29
-
30
- def normalize_config(config)
31
- config.each do |gene_key, gene_config|
32
- next unless gene_config[:type]
33
-
34
- gene_class = Kernel.const_get(gene_config[:type])
35
- gene_class.key = gene_key
36
- gene_config[:class] = gene_class
37
- end
38
- end
39
- end
40
- end
@@ -1,19 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Evolvable::Goal
4
- class Equalize
5
- include Evolvable::Goal
6
-
7
- def value
8
- @value ||= 0
9
- end
10
-
11
- def evaluate(instance)
12
- -(instance.value - value).abs
13
- end
14
-
15
- def met?(instance)
16
- instance.value == value
17
- end
18
- end
19
- end
@@ -1,19 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Evolvable::Goal
4
- class Maximize
5
- include Evolvable::Goal
6
-
7
- def value
8
- @value ||= Float::INFINITY
9
- end
10
-
11
- def evaluate(instance)
12
- instance.value
13
- end
14
-
15
- def met?(instance)
16
- instance.value >= value
17
- end
18
- end
19
- end
@@ -1,19 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Evolvable::Goal
4
- class Minimize
5
- include Evolvable::Goal
6
-
7
- def value
8
- @value ||= -Float::INFINITY
9
- end
10
-
11
- def evaluate(instance)
12
- -instance.value
13
- end
14
-
15
- def met?(instance)
16
- instance.value <= value
17
- end
18
- end
19
- end