evolvable 1.0.2 → 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 (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