evolvable 0.1.3 → 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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +0 -1
  3. data/.yardopts +4 -0
  4. data/CHANGELOG.md +63 -0
  5. data/Gemfile +3 -0
  6. data/Gemfile.lock +39 -39
  7. data/LICENSE +21 -0
  8. data/README.md +234 -248
  9. data/README_YARD.md +237 -0
  10. data/bin/console +20 -43
  11. data/evolvable.gemspec +2 -3
  12. data/examples/ascii_art.rb +62 -0
  13. data/examples/ascii_gene.rb +9 -0
  14. data/examples/hello_world.rb +91 -0
  15. data/examples/images/diagram.png +0 -0
  16. data/examples/stickman.rb +77 -0
  17. data/exe/hello +16 -0
  18. data/lib/evolvable/count_gene.rb +42 -0
  19. data/lib/evolvable/equalize_goal.rb +29 -0
  20. data/lib/evolvable/error/undefined_method.rb +7 -0
  21. data/lib/evolvable/evaluation.rb +74 -0
  22. data/lib/evolvable/evolution.rb +58 -0
  23. data/lib/evolvable/gene.rb +67 -0
  24. data/lib/evolvable/gene_combination.rb +69 -0
  25. data/lib/evolvable/genome.rb +86 -0
  26. data/lib/evolvable/goal.rb +52 -0
  27. data/lib/evolvable/maximize_goal.rb +30 -0
  28. data/lib/evolvable/minimize_goal.rb +29 -0
  29. data/lib/evolvable/mutation.rb +71 -42
  30. data/lib/evolvable/point_crossover.rb +71 -0
  31. data/lib/evolvable/population.rb +202 -83
  32. data/lib/evolvable/rigid_count_gene.rb +17 -0
  33. data/lib/evolvable/search_space.rb +181 -0
  34. data/lib/evolvable/selection.rb +45 -0
  35. data/lib/evolvable/serializer.rb +21 -0
  36. data/lib/evolvable/uniform_crossover.rb +42 -0
  37. data/lib/evolvable/version.rb +1 -1
  38. data/lib/evolvable.rb +203 -56
  39. metadata +46 -24
  40. data/.rubocop.yml +0 -20
  41. data/lib/evolvable/crossover.rb +0 -35
  42. data/lib/evolvable/errors/not_implemented.rb +0 -5
  43. data/lib/evolvable/helper_methods.rb +0 -45
  44. data/lib/evolvable/hooks.rb +0 -9
data/lib/evolvable.rb CHANGED
@@ -1,86 +1,233 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'forwardable'
4
- require 'logger'
5
-
6
4
  require 'evolvable/version'
7
- require 'evolvable/population'
8
- require 'evolvable/crossover'
5
+ require 'evolvable/error/undefined_method'
6
+ require 'evolvable/gene'
7
+ require 'evolvable/search_space'
8
+ require 'evolvable/genome'
9
+ require 'evolvable/goal'
10
+ require 'evolvable/equalize_goal'
11
+ require 'evolvable/maximize_goal'
12
+ require 'evolvable/minimize_goal'
13
+ require 'evolvable/evaluation'
14
+ require 'evolvable/evolution'
15
+ require 'evolvable/selection'
16
+ require 'evolvable/gene_combination'
17
+ require 'evolvable/point_crossover'
18
+ require 'evolvable/uniform_crossover'
9
19
  require 'evolvable/mutation'
10
- require 'evolvable/helper_methods'
11
- require 'evolvable/hooks'
12
- require 'evolvable/errors/not_implemented'
13
-
20
+ require 'evolvable/population'
21
+ require 'evolvable/count_gene'
22
+ require 'evolvable/rigid_count_gene'
23
+ require 'evolvable/serializer'
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
+ #
14
39
  module Evolvable
15
- extend HelperMethods
40
+ extend Forwardable
16
41
 
17
42
  def self.included(base)
18
- base.extend Hooks
19
-
20
- def base.evolvable_gene_pool
21
- raise Errors::NotImplemented, __method__
22
- end
23
-
24
- def base.evolvable_genes_count
25
- evolvable_gene_pool_size
26
- end
27
-
28
- def base.evolvable_evaluate!(_objects); end
43
+ base.extend(ClassMethods)
44
+ end
29
45
 
30
- def base.evolvable_population_attrs
31
- {}
32
- end
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
33
49
 
34
- def base.evolvable_population(args = {})
35
- clear_evolvable_gene_pool_caches
36
- args = evolvable_population_attrs.merge!(args)
37
- args[:evolvable_class] = self
38
- Population.new(args)
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
59
+ Population.new(**keyword_args)
39
60
  end
40
61
 
41
- def base.evolvable_initialize(genes, population, _object_index)
42
- evolvable = new
43
- evolvable.genes = genes
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
44
76
  evolvable.population = population
77
+ evolvable.genome = genome
78
+ evolvable.generation_index = generation_index
79
+ evolvable.after_initialize
45
80
  evolvable
46
81
  end
47
82
 
48
- def base.evolvable_gene_pool_cache
49
- @evolvable_gene_pool_cache ||= evolvable_gene_pool
83
+ def initialize_evolvable
84
+ new
50
85
  end
51
86
 
52
- def base.evolvable_gene_pool_size
53
- @evolvable_gene_pool_size ||= evolvable_gene_pool_cache.size
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
54
92
  end
55
93
 
56
- def base.clear_evolvable_gene_pool_caches
57
- @evolvable_gene_pool_cache = nil
58
- @evolvable_gene_pool_size = nil
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
+ {}
59
144
  end
60
145
 
61
- def base.evolvable_random_genes(count = nil)
62
- gene_pool = evolvable_gene_pool_cache
63
- count ||= evolvable_genes_count
64
- gene_pool = gene_pool.sample(count) if count < gene_pool.size
65
- genes = {}
66
- gene_pool.each { |name, potentials| genes[name] = potentials.sample }
67
- genes
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
+ []
68
155
  end
69
- end
70
156
 
71
- def self.logger
72
- @logger ||= Logger.new(STDOUT)
73
- end
157
+ # @deprecated
158
+ # Will be removed in version 2.0.
159
+ # Use {#search_space} instead.
160
+ def gene_space
161
+ {}
162
+ end
74
163
 
75
- attr_accessor :genes,
76
- :population
77
164
 
78
- def fitness
79
- raise Errors::NotImplemented, __method__
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
188
+
189
+ #
190
+ # @readme
191
+ # Runs after evolution.
192
+ #
193
+ def after_evolution(population); end
80
194
  end
81
195
 
82
- def evolvable_progress(info = nil)
83
- info ||= "Generation: #{population.generation_count} | Fitness: #{fitness}"
84
- Evolvable.logger.info(info)
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
208
+
209
+ #
210
+ # @deprecated
211
+ # Will be removed in version 2.0.
212
+ # Use {#generation_index} instead.
213
+ #
214
+ def population_index
215
+ generation_index
85
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
86
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: 0.1.3
4
+ version: 1.1.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: 2019-03-11 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
@@ -25,55 +25,77 @@ 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
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
70
79
  - lib/evolvable.rb
71
- - lib/evolvable/crossover.rb
72
- - lib/evolvable/errors/not_implemented.rb
73
- - lib/evolvable/helper_methods.rb
74
- - lib/evolvable/hooks.rb
80
+ - lib/evolvable/count_gene.rb
81
+ - lib/evolvable/equalize_goal.rb
82
+ - lib/evolvable/error/undefined_method.rb
83
+ - lib/evolvable/evaluation.rb
84
+ - lib/evolvable/evolution.rb
85
+ - lib/evolvable/gene.rb
86
+ - lib/evolvable/gene_combination.rb
87
+ - lib/evolvable/genome.rb
88
+ - lib/evolvable/goal.rb
89
+ - lib/evolvable/maximize_goal.rb
90
+ - lib/evolvable/minimize_goal.rb
75
91
  - lib/evolvable/mutation.rb
92
+ - lib/evolvable/point_crossover.rb
76
93
  - lib/evolvable/population.rb
94
+ - lib/evolvable/rigid_count_gene.rb
95
+ - lib/evolvable/search_space.rb
96
+ - lib/evolvable/selection.rb
97
+ - lib/evolvable/serializer.rb
98
+ - lib/evolvable/uniform_crossover.rb
77
99
  - lib/evolvable/version.rb
78
100
  homepage: https://github.com/mattruzicka/evolvable
79
101
  licenses:
@@ -82,7 +104,7 @@ metadata:
82
104
  homepage_uri: https://github.com/mattruzicka/evolvable
83
105
  source_code_uri: https://github.com/mattruzicka/evolvable
84
106
  changelog_uri: https://github.com/mattruzicka/evolvable/blob/master/CHANGELOG.md
85
- post_install_message:
107
+ post_install_message:
86
108
  rdoc_options: []
87
109
  require_paths:
88
110
  - lib
@@ -97,8 +119,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
97
119
  - !ruby/object:Gem::Version
98
120
  version: '0'
99
121
  requirements: []
100
- rubygems_version: 3.0.3
101
- signing_key:
122
+ rubygems_version: 3.2.3
123
+ signing_key:
102
124
  specification_version: 4
103
125
  summary: Add evolutionary behavior to any Ruby object
104
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,35 +0,0 @@
1
- class Crossover
2
- def call(parent_genes, offspring_count)
3
- parent_gene_couples = parent_genes.combination(2).cycle
4
- Array.new(offspring_count) do
5
- p1_genes, p2_genes = parent_gene_couples.next
6
- p1_genes.merge(p2_genes) { |_k, p1_val, p2_val| [p1_val, p2_val].sample }
7
- offspring_genes = merge_parent_genes(p1_genes, p2_genes)
8
- trim_offspring_genes!(offspring_genes, p1_genes.count)
9
- offspring_genes
10
- end
11
- end
12
-
13
- def inspect
14
- "#<#{self.class.name} #{as_json.map { |a| a.join(': ') }.join(', ')} >"
15
- end
16
-
17
- def as_json
18
- { type: self.class.name }
19
- end
20
-
21
- private
22
-
23
- def merge_parent_genes(p1_genes, p2_genes)
24
- p1_genes.merge(p2_genes) { |_k, p1_val, p2_val| [p1_val, p2_val].sample }
25
- end
26
-
27
- def trim_offspring_genes!(offspring_genes, parent_genes_count)
28
- trim_genes_count = offspring_genes.count - parent_genes_count
29
- return if trim_genes_count.zero?
30
-
31
- offspring_genes.keys.sample(trim_genes_count).each do |name|
32
- offspring_genes.delete(name)
33
- end
34
- end
35
- end
@@ -1,5 +0,0 @@
1
- module Evolvable
2
- module Errors
3
- class NotImplemented < StandardError; end
4
- end
5
- end
@@ -1,45 +0,0 @@
1
- module Evolvable
2
- module HelperMethods
3
- def combine_dimensions(dimensions)
4
- dimension_lengths = dimensions.map(&:length)
5
- genes_count = dimension_lengths.inject(1) { |product, c| product * c }
6
-
7
- genes = Array.new(genes_count) { [] }
8
- element_repeat_counts = compute_element_repeat_counts(dimension_lengths)
9
- sequence_counts = compute_sequence_counts(dimension_lengths)
10
-
11
- dimensions.each_with_index do |dimension, dim_index|
12
- element_count = element_repeat_counts[dim_index]
13
- sequence = dimension.flat_map { |e| [e] * element_count }
14
- dimension_sequence = sequence * sequence_counts[dim_index]
15
- dimension_sequence.each_with_index do |element, gene_index|
16
- genes[gene_index][dim_index] = element
17
- end
18
- end
19
-
20
- genes
21
- end
22
-
23
- private
24
-
25
- def compute_element_repeat_counts(dimension_lengths)
26
- repeat_counts = Array.new(dimension_lengths.count - 1) do |n|
27
- right_side_counts = dimension_lengths[(n + 1)..-1]
28
- right_side_counts.inject(1) { |product, a| product * a }
29
- end
30
- repeat_counts << 1
31
- repeat_counts
32
- end
33
-
34
- def compute_sequence_counts(dimension_lengths)
35
- Array.new(dimension_lengths.count) do |n|
36
- if n.zero?
37
- 1
38
- else
39
- right_side_counts = dimension_lengths[0...n]
40
- right_side_counts.inject(1) { |product, a| product * a }
41
- end
42
- end
43
- end
44
- end
45
- end
@@ -1,9 +0,0 @@
1
- module Evolvable
2
- module Hooks
3
- def evolvable_before_evolution(population); end
4
-
5
- def evolvable_after_select(population); end
6
-
7
- def evolvable_after_evolution(population); end
8
- end
9
- end