evolvable 0.1.3 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
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