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
@@ -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