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
@@ -1,24 +1,56 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Evolvable
4
+ #
5
+ # @readme
6
+ # After a population's instances are evaluated, they undergo evolution.
7
+ # The default evolution object is composed of selection,
8
+ # crossover, and mutation objects and applies them as operations to
9
+ # a population's evolvables in that order.
10
+ #
4
11
  class Evolution
5
12
  extend Forwardable
6
13
 
14
+ #
15
+ # Initializes a new evolution object.
16
+ #
17
+ # Keyword arguments:
18
+ #
19
+ # #### selection
20
+ # The default is `Selection.new`
21
+ # #### crossover - deprecated
22
+ # The default is `GeneCrossover.new`
23
+ # #### mutation
24
+ # The default is `Mutation.new`
25
+ #
7
26
  def initialize(selection: Selection.new,
8
- crossover: GeneCrossover.new,
27
+ combination: GeneCombination.new,
28
+ crossover: nil, # deprecated
9
29
  mutation: Mutation.new)
10
30
  @selection = selection
11
- @crossover = crossover
31
+ @combination = crossover || combination
12
32
  @mutation = mutation
13
33
  end
14
34
 
15
- attr_accessor :selection,
16
- :crossover,
17
- :mutation
35
+ attr_reader :selection,
36
+ :combination,
37
+ :mutation
38
+
39
+ def selection=(val)
40
+ @selection = Evolvable.new_object(@selection, val, Selection)
41
+ end
42
+
43
+ def combination=(val)
44
+ @combination = Evolvable.new_object(@combination, val, GeneCombination)
45
+ end
46
+
47
+ def mutation=(val)
48
+ @mutation = Evolvable.new_object(@mutation, val, Mutation)
49
+ end
18
50
 
19
51
  def call(population)
20
52
  selection.call(population)
21
- crossover.call(population)
53
+ combination.call(population)
22
54
  mutation.call(population)
23
55
  population
24
56
  end
@@ -1,22 +1,64 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Evolvable
4
+ #
5
+ # @readme
6
+ # For evolution to be effective, an evolvable's genes must be able to influence
7
+ # its behavior. Evolvables are composed of genes that can be used to run simple
8
+ # functions or orchestrate complex interactions. The level of abstraction is up
9
+ # to you.
10
+ #
11
+ # Defining gene classes requires encapsulating some "sample space" and returning
12
+ # a sample outcome when a gene attribute is accessed. For evolution to proceed
13
+ # in a non-random way, the same sample outcome should be returned every time
14
+ # a particular gene is accessed with a particular set of parameters.
15
+ # Memoization is a useful technique for doing just this. The
16
+ # [memo_wise](https://github.com/panorama-ed/memo_wise) gem may be useful for
17
+ # more complex memoizations.
18
+ #
19
+ # @example
20
+ # # This gene generates a random hexidecimal color code for use by evolvables.
21
+ #
22
+ # require 'securerandom'
23
+ #
24
+ # class ColorGene
25
+ # include Evolvable::Gene
26
+ #
27
+ # def hex_code
28
+ # @hex_code ||= SecureRandom.hex(3)
29
+ # end
30
+ # end
31
+ #
32
+
4
33
  module Gene
5
34
  def self.included(base)
6
- def base.key=(val)
35
+ base.extend(ClassMethods)
36
+ end
37
+
38
+ module ClassMethods
39
+ def key=(val)
7
40
  @key = val
8
41
  end
9
42
 
10
- def base.key
43
+ def key
11
44
  @key
12
45
  end
13
46
 
14
- def base.crossover(gene_a, gene_b)
15
- [gene_a, gene_b].sample
47
+ def combine(gene_a, gene_b)
48
+ genes = [gene_a, gene_b]
49
+ genes.compact!
50
+ genes.sample
16
51
  end
52
+
53
+ #
54
+ # @deprecated
55
+ # Will be removed in 2.0
56
+ # Use {#combine}
57
+ #
58
+ alias crossover combine
17
59
  end
18
60
 
19
- attr_accessor :instance
61
+ attr_accessor :evolvable
20
62
 
21
63
  def key
22
64
  self.class.key
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Evolvable
4
+ #
5
+ # @readme
6
+ # Combination generates new evolvable instances by combining the genes of selected instances.
7
+ # You can think of it as a mixing of parent genes from one generation to
8
+ # produce the next generation.
9
+ #
10
+ # You may choose from a selection of combination objects or implement your own.
11
+ # The default combination object is `Evolvable::GeneCombination`.
12
+ #
13
+ # Custom crossover objects must implement the `#call` method which accepts
14
+ # the population as the first object.
15
+ # Enables gene types to define combination behaviors.
16
+ #
17
+ # Each gene class can implement a unique behavior for
18
+ # combination by overriding the following default implementation
19
+ # which mirrors the behavior of `Evolvable::UniformCrossover`
20
+ #
21
+ class GeneCombination
22
+ def call(population)
23
+ new_evolvables(population, population.size)
24
+ population
25
+ end
26
+
27
+ def new_evolvables(population, count)
28
+ parent_genome_cycle = population.new_parent_genome_cycle
29
+ Array.new(count) do
30
+ genome = build_genome(parent_genome_cycle.next)
31
+ population.new_evolvable(genome: genome)
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ def build_genome(genome_pair)
38
+ new_config = {}
39
+ genome_1, genome_2 = genome_pair.shuffle!
40
+ genome_1.each do |gene_key, gene_config_1|
41
+ gene_config_2 = genome_2.config[gene_key]
42
+ count_gene = combine_count_genes(gene_config_1, gene_config_2)
43
+ genes = combine_genes(count_gene.count, gene_config_1, gene_config_2)
44
+ new_config[gene_key] = { count_gene: count_gene, genes: genes }
45
+ end
46
+ Genome.new(config: new_config)
47
+ end
48
+
49
+ def combine_count_genes(gene_config_1, gene_config_2)
50
+ count_gene_1 = gene_config_1[:count_gene]
51
+ count_gene_2 = gene_config_2[:count_gene]
52
+ count_gene_1.class.combine(count_gene_1, count_gene_2)
53
+ end
54
+
55
+ def combine_genes(count, gene_config_1, gene_config_2)
56
+ genes_1 = gene_config_1[:genes]
57
+ genes_2 = gene_config_2[:genes]
58
+ first_gene = genes_1.first || genes_2.first
59
+ return [] unless first_gene
60
+
61
+ gene_class = first_gene.class
62
+ Array.new(count) do |index|
63
+ gene_a = genes_1[index]
64
+ gene_b = genes_2[index]
65
+ gene_class.combine(gene_a, gene_b) || gene_class.new
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Evolvable
4
+ #
5
+ # @readme
6
+ # TODO...
7
+ #
8
+ class Genome
9
+ extend Forwardable
10
+
11
+ def self.load(data)
12
+ new(config: Serializer.load(data))
13
+ end
14
+
15
+ def initialize(config: {})
16
+ @config = config
17
+ end
18
+
19
+ attr_reader :config
20
+
21
+ #
22
+ # Returns the first gene with the given key. In the Melody example above, the instrument gene has the key `:instrument` so we might write something like: `instrument_gene = melody.find_gene(instrument)`
23
+ #
24
+ # @param [<Type>] key <description>
25
+ #
26
+ # @return [<Type>] <description>
27
+ #
28
+ def find_gene(key)
29
+ @config.dig(key, :genes, 0)
30
+ end
31
+
32
+ #
33
+ # Returns an array of genes that have the given key. Gene keys are defined in the [EvolvableClass.search_space](#evolvableclasssearch_space) method. In the Melody example above, the key for the note genes would be `:notes`. The following would return an array of them: `note_genes = melody.find_genes(:notes)`
34
+ #
35
+ # @param [<Type>] *keys <description>
36
+ #
37
+ # @return [<Type>] <description>
38
+ #
39
+ def find_genes(*keys)
40
+ keys.flatten!
41
+ return @config.dig(keys.first, :genes) if keys.count <= 1
42
+
43
+ @config.values_at(*keys).flat_map { _1&.fetch(:genes, []) || [] }
44
+ end
45
+
46
+ #
47
+ # <Description>
48
+ #
49
+ # @param [<Type>] key <description>
50
+ #
51
+ # @return [<Type>] <description>
52
+ #
53
+ def find_genes_count(key)
54
+ find_count_gene(key).count
55
+ end
56
+
57
+ #
58
+ # <Description>
59
+ #
60
+ # @param [<Type>] key <description>
61
+ #
62
+ # @return [<Type>] <description>
63
+ #
64
+ def find_count_gene(key)
65
+ @config.dig(key, :count_gene)
66
+ end
67
+
68
+ def_delegators :config, :each
69
+
70
+ def gene_keys
71
+ @config.keys
72
+ end
73
+
74
+ def genes
75
+ @config.flat_map { |_gene_key, gene_config| gene_config[:genes] }
76
+ end
77
+
78
+ def inspect
79
+ self.class.name
80
+ end
81
+
82
+ def dump
83
+ Serializer.dump @config
84
+ end
85
+ end
86
+ end
@@ -1,18 +1,51 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Evolvable
4
- module Goal
4
+
5
+ #
6
+ # The goal for a population can be specified via assignment - `population.goal = Evolvable::Goal::Equalize.new` - or by passing an evaluation object when [initializing a population](#evolvablepopulationnew).
7
+ #
8
+ # You can intialize the `Evolvable::Evaluation` object with any goal object like this:
9
+ #
10
+ # You can implement custom goal object like so:
11
+ #
12
+ # @example
13
+ # goal_object = SomeGoal.new(value: 100)
14
+ # Evolvable::Evaluation.new(goal_object)
15
+ #
16
+ # or more succinctly like this:
17
+ #
18
+ # @example
19
+ # Evolvable::Evaluation.new(:maximize) # Uses default goal value of Float::INFINITY
20
+ # Evolvable::Evaluation.new(maximize: 50) # Sets goal value to 50
21
+ # Evolvable::Evaluation.new(:minimize) # Uses default goal value of -Float::INFINITY
22
+ # Evolvable::Evaluation.new(minimize: 100) # Sets goal value to 100
23
+ # Evolvable::Evaluation.new(:equalize) # Uses default goal value of 0
24
+ # Evolvable::Evaluation.new(equalize: 1000) # Sets goal value to 1000
25
+ #
26
+ # @example
27
+ # class CustomGoal < Evolvable::Goal
28
+ # def evaluate(instance)
29
+ # # Required by Evolvable::Evaluation in order to sort instances in preparation for selection.
30
+ # end
31
+ #
32
+ # def met?(instance)
33
+ # # Used by Evolvable::Population#evolve to stop evolving when the goal value has been reached.
34
+ # end
35
+ # end
36
+ #
37
+ class Goal
5
38
  def initialize(value: nil)
6
39
  @value = value if value
7
40
  end
8
41
 
9
42
  attr_accessor :value
10
43
 
11
- def evaluate(_instance)
44
+ def evaluate(_evolvable)
12
45
  raise Errors::UndefinedMethod, "#{self.class.name}##{__method__}"
13
46
  end
14
47
 
15
- def met?(_instance)
48
+ def met?(_evolvable)
16
49
  raise Errors::UndefinedMethod, "#{self.class.name}##{__method__}"
17
50
  end
18
51
  end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Evolvable
4
+ #
5
+ # Prioritizes instances with greater values. This is the default.
6
+ #
7
+ # The default goal value is `Float::INFINITY`, but it can be reassigned
8
+ # to any numeric value.
9
+ #
10
+ class MaximizeGoal < Goal
11
+ def value
12
+ @value ||= Float::INFINITY
13
+ end
14
+
15
+ def evaluate(evolvable)
16
+ evolvable.value
17
+ end
18
+
19
+ def met?(evolvable)
20
+ evolvable.value >= value
21
+ end
22
+ end
23
+
24
+ #
25
+ # @deprecated
26
+ # Will be removed in 2.0.
27
+ # Use {MaximizeGoal} instead
28
+ #
29
+ class Goal::Maximize < MaximizeGoal; end
30
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Evolvable
4
+ # Prioritizes instances with lesser values.
5
+ #
6
+ # The default goal value is `-Float::INFINITY`, but it can be reassigned
7
+ # to any numeric value
8
+ #
9
+ class MinimizeGoal < Goal
10
+ def value
11
+ @value ||= -Float::INFINITY
12
+ end
13
+
14
+ def evaluate(evolvable)
15
+ -evolvable.value
16
+ end
17
+
18
+ def met?(evolvable)
19
+ evolvable.value <= value
20
+ end
21
+ end
22
+
23
+ #
24
+ # @deprecated
25
+ # Will be removed in 2.0.
26
+ # Use {MinimizeGoal} instead
27
+ #
28
+ class Goal::Minimize < MinimizeGoal; end
29
+ end
@@ -1,41 +1,93 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Evolvable
4
+ #
5
+ # @readme
6
+ # Mutation serves the role of increasing genetic variation. When an evolvable
7
+ # undergoes a mutation, one or more of its genes are replaced by newly
8
+ # initialized ones. In effect, a gene mutation invokes a new random outcome
9
+ # from the genetic search space.
10
+ #
11
+ # Mutation frequency is configurable using the `probability` and `rate`
12
+ # parameters.
13
+ #
14
+ # @example
15
+ # # Show how to initialize/assign population with a specific mutation object
16
+ #
4
17
  class Mutation
5
18
  extend Forwardable
6
19
 
20
+ DEFAULT_PROBABILITY = 0.03
21
+
22
+ #
23
+ # Initializes a new mutation object.
24
+ #
25
+ # Keyword arguments:
26
+ #
27
+ # #### probability
28
+ # The probability that a particular instance undergoes a mutation.
29
+ # By default, the probability is 0.03 which translates to 3%.
30
+ # If initialized with a `rate`, the probability will be 1 which
31
+ # means all genes _can_ undergo mutation, but actual gene mutations
32
+ # will be subject to the given mutation rate.
33
+ # #### rate
34
+ # the rate at which individual genes mutate. The default rate is 0 which,
35
+ # when combined with a non-zero `probability` (the default), means that
36
+ # one gene for each instance that undergoes mutation will change.
37
+ # If a rate is given, but no `probability` is given, then the `probability`
38
+ # will bet set to 1 which always defers to the mutation rate.
39
+ #
40
+ # To summarize, the `probability` represents the chance of mutation on
41
+ # the instance level and the `rate` represents the chance on the gene level.
42
+ # The `probability` and `rate` can be any number from 0 to 1. When the
43
+ # `probability` is 0, no mutation will ever happen. When the `probability`
44
+ # is not 0 but the rate is 0, then any instance that undergoes mutation
45
+ # will only receive one mutant gene. If the rate is not 0, then if an
46
+ # instance has been chosen to undergo mutation, each of its genes will
47
+ # mutate with a probability as defined by the `rate`.
48
+ #
49
+ # @example Example Initializations:
50
+ # Evolvable::Mutation.new # Approximately #{DEFAULT_PROBABILITY} of instances will receive one mutant gene
51
+ # Evolvable::Mutation.new(probability: 0.5) # Approximately 50% of instances will receive one mutant gene
52
+ # Evolvable::Mutation.new(rate: 0.03) # Approximately 3% of all genes in the population will mutate.
53
+ # Evolvable::Mutation.new(probability: 0.3, rate: 0.03) # Approximately 30% of instances will have approximately 3% of their genes mutated.
54
+ #
55
+ # Custom mutation objects must implement the `#call` method which accepts the population as the first object.
56
+ #
7
57
  def initialize(probability: nil, rate: nil)
8
- @probability = probability || (rate ? 1 : 0.03)
9
- @rate = rate || 0
58
+ @probability = probability || (rate ? 1 : DEFAULT_PROBABILITY)
59
+ @rate = rate
10
60
  end
11
61
 
12
62
  attr_accessor :probability,
13
63
  :rate
14
64
 
15
65
  def call(population)
16
- return population if probability.zero?
66
+ mutate_evolvables(population.evolvables) unless probability.zero?
67
+ population
68
+ end
17
69
 
18
- population.instances.each do |instance|
19
- mutate_instance(instance) if rand <= probability
70
+ def mutate_evolvables(evolvables)
71
+ evolvables.each do |evolvable|
72
+ next unless rand <= probability
73
+
74
+ evolvable.genome.each { |_key, config| mutate_genes(config[:genes]) }
20
75
  end
21
- population
22
76
  end
23
77
 
24
78
  private
25
79
 
26
- def mutate_instance(instance)
27
- genes_count = instance.genes.count
80
+ def mutate_genes(genes)
81
+ genes_count = genes.count
28
82
  return if genes_count.zero?
29
83
 
30
- return mutate_gene(instance, rand(genes_count)) if rate.zero?
84
+ return mutate_gene_by_index(genes, rand(genes_count)) unless rate
31
85
 
32
- genes_count.times { |index| mutate_gene(instance, index) if rand <= rate }
86
+ genes_count.times { |index| mutate_gene_by_index(genes, index) if rand <= rate }
33
87
  end
34
88
 
35
- def mutate_gene(instance, gene_index)
36
- gene = instance.genes[gene_index]
37
- mutant_gene = gene.class.new
38
- instance.genes[gene_index] = mutant_gene
89
+ def mutate_gene_by_index(genes, gene_index)
90
+ genes[gene_index] = genes[gene_index].class.new
39
91
  end
40
92
  end
41
93
  end
@@ -1,6 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Evolvable
4
+ #
5
+ # Supports single and multi-point crossover. The default is single-point
6
+ # crossover via a `points_count` of 1 which can be changed on an existing population
7
+ # (`population.crossover.points_count = 5`) or during initialization
8
+ # (`Evolvable::PointCrossover.new(5)`)
9
+ #
4
10
  class PointCrossover
5
11
  def initialize(points_count: 1)
6
12
  @points_count = points_count
@@ -9,35 +15,43 @@ module Evolvable
9
15
  attr_accessor :points_count
10
16
 
11
17
  def call(population)
12
- population.instances = initialize_offspring(population)
18
+ population.evolvables = new_evolvables(population, population.size)
13
19
  population
14
20
  end
15
21
 
16
- private
17
-
18
- def initialize_offspring(population)
19
- parent_genes = population.instances.map!(&:genes)
20
- parent_gene_couples = parent_genes.combination(2).cycle
21
- offspring = []
22
- population_index = 0
22
+ def new_evolvables(population, count)
23
+ parent_genome_cycle = population.new_parent_genome_cycle
24
+ evolvables = []
23
25
  loop do
24
- genes_1, genes_2 = parent_gene_couples.next
25
- crossover_genes(genes_1, genes_2).each do |genes|
26
- offspring << population.new_instance(genes: genes, population_index: population_index)
27
- population_index += 1
28
- return offspring if population_index == population.size
26
+ genome_1, genome_2 = parent_genome_cycle.next
27
+ crossover_genomes(genome_1, genome_2).each do |genome|
28
+ evolvable = population.new_evolvable(genome: genome)
29
+ evolvables << evolvable
30
+ return evolvables if evolvable.generation_index == count
29
31
  end
30
32
  end
31
33
  end
32
34
 
33
- def crossover_genes(genes_1, genes_2)
34
- offspring_genes = [[], []]
35
+ private
36
+
37
+ def crossover_genomes(genome_1, genome_2)
38
+ genome_1 = genome_1.dup
39
+ genome_2 = genome_2.dup
40
+ genome_1.each do |gene_key, gene_config_1|
41
+ gene_config_2 = genome_2.config[gene_key]
42
+ genes_1 = gene_config_1[:genes]
43
+ genes_2 = gene_config_2[:genes]
44
+ crossover_genes!(genes_1, genes_2)
45
+ end
46
+ [genome_1, genome_2]
47
+ end
48
+
49
+ def crossover_genes!(genes_1, genes_2)
35
50
  generate_ranges(genes_1.length).each do |range|
36
- offspring_genes.reverse!
37
- offspring_genes[0][range] = genes_1[range]
38
- offspring_genes[1][range] = genes_2[range]
51
+ genes_2_range_values = genes_2[range]
52
+ genes_2[range] = genes_1[range]
53
+ genes_1[range] = genes_2_range_values
39
54
  end
40
- offspring_genes
41
55
  end
42
56
 
43
57
  def generate_ranges(genes_count)