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
data/lib/evolvable.rb CHANGED
@@ -4,70 +4,238 @@ 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
92
+ end
93
+
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
143
+ {}
144
+ end
145
+
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
+ []
43
155
  end
44
156
 
45
- def base.gene_space
157
+ # @deprecated
158
+ # Will be removed in version 2.0.
159
+ # Use {#search_space} instead.
160
+ def gene_space
46
161
  {}
47
162
  end
48
163
 
49
- def base.before_evaluation(population); end
50
164
 
51
- def base.before_evolution(population); end
165
+ #
166
+ # @readme
167
+ # Runs before evaluation.
168
+ #
169
+ def before_evaluation(population); end
170
+
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
52
188
 
53
- def base.after_evolution(population); end
189
+ #
190
+ # @readme
191
+ # Runs after evolution.
192
+ #
193
+ def after_evolution(population); end
54
194
  end
55
195
 
56
- def initialize_instance; end
196
+ # Runs an evolvable is initialized. Ueful for implementing custom initialization logic.
197
+ def after_initialize; end
57
198
 
58
- attr_accessor :population,
59
- :genes,
60
- :population_index
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
61
208
 
62
- def value
63
- raise Errors::UndefinedMethod, "#{self.class.name}##{__method__}"
209
+ #
210
+ # @deprecated
211
+ # Will be removed in version 2.0.
212
+ # Use {#generation_index} instead.
213
+ #
214
+ def population_index
215
+ generation_index
64
216
  end
65
217
 
66
- def find_gene(key)
67
- @genes.detect { |g| g.key == key }
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
233
+
234
+ def dump_genome(serializer: Serializer)
235
+ @genome.dump(serializer: serializer)
68
236
  end
69
237
 
70
- def find_genes(key)
71
- @genes.select { |g| g.key == key }
238
+ def load_genome(data, serializer: Serializer)
239
+ @genome = Genome.load(data, serializer: serializer)
72
240
  end
73
241
  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.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matt Ruzicka
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-09-11 00:00:00.000000000 Z
11
+ date: 2022-01-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -25,65 +25,76 @@ dependencies:
25
25
  - !ruby/object:Gem::Version
26
26
  version: '2.0'
27
27
  - !ruby/object:Gem::Dependency
28
- name: byebug
28
+ name: readme_yard
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - "~>"
31
+ - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: '11.0'
33
+ version: '0'
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - "~>"
38
+ - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: '11.0'
40
+ version: '0'
41
41
  - !ruby/object:Gem::Dependency
42
- name: rubocop
42
+ name: yard
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
- - - "~>"
45
+ - - ">="
46
46
  - !ruby/object:Gem::Version
47
- version: 0.64.0
47
+ version: '0'
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
- - - "~>"
52
+ - - ">="
53
53
  - !ruby/object:Gem::Version
54
- version: 0.64.0
55
- description:
56
- email:
57
- executables: []
54
+ version: '0'
55
+ description:
56
+ email:
57
+ executables:
58
+ - hello
58
59
  extensions: []
59
60
  extra_rdoc_files: []
60
61
  files:
61
62
  - ".gitignore"
62
- - ".rubocop.yml"
63
+ - ".yardopts"
63
64
  - CHANGELOG.md
64
65
  - Gemfile
65
66
  - Gemfile.lock
67
+ - LICENSE
66
68
  - README.md
69
+ - README_YARD.md
67
70
  - bin/console
68
71
  - bin/setup
69
72
  - evolvable.gemspec
70
- - examples/evolvable_string.rb
71
- - 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
72
79
  - lib/evolvable.rb
80
+ - lib/evolvable/count_gene.rb
81
+ - lib/evolvable/equalize_goal.rb
73
82
  - lib/evolvable/error/undefined_method.rb
74
83
  - lib/evolvable/evaluation.rb
75
84
  - lib/evolvable/evolution.rb
76
85
  - lib/evolvable/gene.rb
77
- - lib/evolvable/gene_crossover.rb
78
- - lib/evolvable/gene_space.rb
86
+ - lib/evolvable/gene_combination.rb
87
+ - lib/evolvable/genome.rb
79
88
  - lib/evolvable/goal.rb
80
- - lib/evolvable/goal/equalize.rb
81
- - lib/evolvable/goal/maximize.rb
82
- - lib/evolvable/goal/minimize.rb
89
+ - lib/evolvable/maximize_goal.rb
90
+ - lib/evolvable/minimize_goal.rb
83
91
  - lib/evolvable/mutation.rb
84
92
  - lib/evolvable/point_crossover.rb
85
93
  - lib/evolvable/population.rb
94
+ - lib/evolvable/rigid_count_gene.rb
95
+ - lib/evolvable/search_space.rb
86
96
  - lib/evolvable/selection.rb
97
+ - lib/evolvable/serializer.rb
87
98
  - lib/evolvable/uniform_crossover.rb
88
99
  - lib/evolvable/version.rb
89
100
  homepage: https://github.com/mattruzicka/evolvable
@@ -93,7 +104,7 @@ metadata:
93
104
  homepage_uri: https://github.com/mattruzicka/evolvable
94
105
  source_code_uri: https://github.com/mattruzicka/evolvable
95
106
  changelog_uri: https://github.com/mattruzicka/evolvable/blob/master/CHANGELOG.md
96
- post_install_message:
107
+ post_install_message:
97
108
  rdoc_options: []
98
109
  require_paths:
99
110
  - lib
@@ -108,8 +119,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
108
119
  - !ruby/object:Gem::Version
109
120
  version: '0'
110
121
  requirements: []
111
- rubygems_version: 3.1.2
112
- signing_key:
122
+ rubygems_version: 3.3.3
123
+ signing_key:
113
124
  specification_version: 4
114
125
  summary: Add evolutionary behavior to any Ruby object
115
126
  test_files: []
data/.rubocop.yml DELETED
@@ -1,20 +0,0 @@
1
- AllCops:
2
- TargetRubyVersion: 2.6.1
3
-
4
- Style/Documentation:
5
- Enabled: false
6
-
7
- Style/SymbolArray:
8
- Enabled: false
9
-
10
- Style/FrozenStringLiteralComment:
11
- Enabled: false
12
-
13
- Style/ClassAndModuleChildren:
14
- Enabled: false
15
-
16
- Naming/VariableNumber:
17
- EnforcedStyle: 'snake_case'
18
-
19
- Metrics/LineLength:
20
- Max: 90
@@ -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,37 +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
- gene.key = gene_key
23
- genes << gene
24
- end
25
- end
26
- genes
27
- end
28
-
29
- private
30
-
31
- def normalize_config(config)
32
- config.each do |_gene_key, gene_config|
33
- gene_config[:class] = Kernel.const_get(gene_config[:type]) if gene_config[:type]
34
- end
35
- end
36
- end
37
- 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