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
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Evolvable
4
+ #
5
+ # @readme
6
+ # For selection to be effective in the context of evolution, there needs to be
7
+ # a way to compare evolvables. In the genetic algorithm, this is often
8
+ # referred to as the "fitness function".
9
+ #
10
+ # The `Evolvable::Evaluation` object expects evolvable instances to define a `#value` method that
11
+ # returns some numeric value. Values are used to evaluate instances relative to each
12
+ # other and with regards to some goal. Out of the box, the goal can be set
13
+ # to maximize, minimize, or equalize numeric values.
14
+ #
15
+ # @example
16
+ # # TODO: Show how to add/change population's evaluation object
17
+ #
18
+ # # The goal value can also be assigned via as argument to `Evolvable::Population#evolve`
19
+ # population.evolve(goal_value: 1000)
20
+ #
21
+ class Evaluation
22
+ GOALS = { maximize: Evolvable::Goal::Maximize.new,
23
+ minimize: Evolvable::Goal::Minimize.new,
24
+ equalize: Evolvable::Goal::Equalize.new }.freeze
25
+
26
+ DEFAULT_GOAL_TYPE = :maximize
27
+
28
+ def initialize(goal = DEFAULT_GOAL_TYPE)
29
+ @goal = normalize_goal(goal)
30
+ end
31
+
32
+ attr_accessor :goal
33
+
34
+ def call(population)
35
+ population.evolvables.sort_by! { |evolvable| goal.evaluate(evolvable) }
36
+ end
37
+
38
+ def best_evolvable(population)
39
+ population.evolvables.max_by { |evolvable| goal.evaluate(evolvable) }
40
+ end
41
+
42
+ def met_goal?(population)
43
+ goal.met?(population.evolvables.last)
44
+ end
45
+
46
+ private
47
+
48
+ def normalize_goal(goal_arg)
49
+ case goal_arg
50
+ when Symbol
51
+ goal_from_symbol(goal_arg)
52
+ when Hash
53
+ goal_from_hash(goal_arg)
54
+ else
55
+ goal_arg || default_goal
56
+ end
57
+ end
58
+
59
+ def default_goal
60
+ GOALS[DEFAULT_GOAL_TYPE]
61
+ end
62
+
63
+ def goal_from_symbol(goal_arg)
64
+ GOALS[goal_arg]
65
+ end
66
+
67
+ def goal_from_hash(goal_arg)
68
+ goal_type, value = goal_arg.first
69
+ goal = GOALS[goal_type]
70
+ goal.value = value
71
+ goal
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
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
+ #
11
+ class Evolution
12
+ extend Forwardable
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
+ #
26
+ def initialize(selection: Selection.new,
27
+ combination: GeneCombination.new,
28
+ crossover: nil, # deprecated
29
+ mutation: Mutation.new)
30
+ @selection = selection
31
+ @combination = crossover || combination
32
+ @mutation = mutation
33
+ end
34
+
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
50
+
51
+ def call(population)
52
+ selection.call(population)
53
+ combination.call(population)
54
+ mutation.call(population)
55
+ population
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
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
+
33
+ module Gene
34
+ def self.included(base)
35
+ base.extend(ClassMethods)
36
+ end
37
+
38
+ module ClassMethods
39
+ def key=(val)
40
+ @key = val
41
+ end
42
+
43
+ def key
44
+ @key
45
+ end
46
+
47
+ def combine(gene_a, gene_b)
48
+ genes = [gene_a, gene_b]
49
+ genes.compact!
50
+ genes.sample
51
+ end
52
+
53
+ #
54
+ # @deprecated
55
+ # Will be removed in 2.0
56
+ # Use {#combine}
57
+ #
58
+ alias crossover combine
59
+ end
60
+
61
+ attr_accessor :evolvable
62
+
63
+ def key
64
+ self.class.key
65
+ end
66
+ end
67
+ end
@@ -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
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Evolvable
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
38
+ def initialize(value: nil)
39
+ @value = value if value
40
+ end
41
+
42
+ attr_accessor :value
43
+
44
+ def evaluate(_evolvable)
45
+ raise Errors::UndefinedMethod, "#{self.class.name}##{__method__}"
46
+ end
47
+
48
+ def met?(_evolvable)
49
+ raise Errors::UndefinedMethod, "#{self.class.name}##{__method__}"
50
+ end
51
+ end
52
+ 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,64 +1,93 @@
1
+ # frozen_string_literal: true
2
+
1
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
+ #
2
17
  class Mutation
3
18
  extend Forwardable
4
19
 
5
- def initialize(rate: 0.03)
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
+ #
57
+ def initialize(probability: nil, rate: nil)
58
+ @probability = probability || (rate ? 1 : DEFAULT_PROBABILITY)
6
59
  @rate = rate
7
60
  end
8
61
 
9
- attr_accessor :rate
10
-
11
- def_delegators :@evolvable_class,
12
- :evolvable_genes_count,
13
- :evolvable_gene_pool_size,
14
- :evolvable_random_genes
15
-
16
- def call!(objects)
17
- @evolvable_class = objects.first.class
18
- mutations_count = find_mutations_count(objects)
19
- return if mutations_count.zero?
20
-
21
- mutant_genes = generate_mutant_genes(mutations_count)
22
- object_mutations_count = mutations_count / objects.count
23
- object_mutations_count = 1 if object_mutations_count.zero?
62
+ attr_accessor :probability,
63
+ :rate
24
64
 
25
- mutant_genes.each_slice(object_mutations_count).with_index do |m_genes, index|
26
- object = objects[index] || objects.sample
27
- genes = object.genes
28
- genes.merge!(m_genes.to_h)
29
- rm_genes_count = genes.count - evolvable_genes_count
30
- genes.keys.sample(rm_genes_count).each { |key| genes.delete(key) }
31
- end
65
+ def call(population)
66
+ mutate_evolvables(population.evolvables) unless probability.zero?
67
+ population
32
68
  end
33
69
 
34
- def inspect
35
- "#<#{self.class.name} #{as_json.map { |a| a.join(': ') }.join(', ')} >"
36
- end
70
+ def mutate_evolvables(evolvables)
71
+ evolvables.each do |evolvable|
72
+ next unless rand <= probability
37
73
 
38
- def as_json
39
- { type: self.class.name,
40
- rate: @rate }
74
+ evolvable.genome.each { |_key, config| mutate_genes(config[:genes]) }
75
+ end
41
76
  end
42
77
 
43
78
  private
44
79
 
45
- def find_mutations_count(objects)
46
- return 0 if @rate.zero?
80
+ def mutate_genes(genes)
81
+ genes_count = genes.count
82
+ return if genes_count.zero?
47
83
 
48
- count = (objects.count * evolvable_genes_count * @rate)
49
- return count.to_i if count >= 1
84
+ return mutate_gene_by_index(genes, rand(genes_count)) unless rate
50
85
 
51
- rand <= count ? 1 : 0
86
+ genes_count.times { |index| mutate_gene_by_index(genes, index) if rand <= rate }
52
87
  end
53
88
 
54
- def generate_mutant_genes(mutations_count)
55
- gene_pool_size = evolvable_gene_pool_size
56
- mutant_genes = []
57
- while mutant_genes.count < mutations_count
58
- genes_count = [gene_pool_size, mutations_count - mutant_genes.count].min
59
- mutant_genes.concat evolvable_random_genes(genes_count).to_a
60
- end
61
- mutant_genes
89
+ def mutate_gene_by_index(genes, gene_index)
90
+ genes[gene_index] = genes[gene_index].class.new
62
91
  end
63
92
  end
64
93
  end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
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
+ #
10
+ class PointCrossover
11
+ def initialize(points_count: 1)
12
+ @points_count = points_count
13
+ end
14
+
15
+ attr_accessor :points_count
16
+
17
+ def call(population)
18
+ population.evolvables = new_evolvables(population, population.size)
19
+ population
20
+ end
21
+
22
+ def new_evolvables(population, count)
23
+ parent_genome_cycle = population.new_parent_genome_cycle
24
+ evolvables = []
25
+ loop do
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
31
+ end
32
+ end
33
+ end
34
+
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)
50
+ generate_ranges(genes_1.length).each do |range|
51
+ genes_2_range_values = genes_2[range]
52
+ genes_2[range] = genes_1[range]
53
+ genes_1[range] = genes_2_range_values
54
+ end
55
+ end
56
+
57
+ def generate_ranges(genes_count)
58
+ current_point = rand(genes_count)
59
+ range_slices = [0...current_point]
60
+ (points_count - 1).times do
61
+ new_point = rand(current_point...genes_count)
62
+ break if new_point.nil?
63
+
64
+ range_slices << (current_point...new_point)
65
+ current_point = new_point
66
+ end
67
+ range_slices << (current_point..-1)
68
+ range_slices
69
+ end
70
+ end
71
+ end