evolvable 1.0.0 → 1.2.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.
- checksums.yaml +4 -4
- data/.yardopts +4 -0
- data/CHANGELOG.md +56 -1
- data/Gemfile +3 -0
- data/Gemfile.lock +38 -21
- data/LICENSE +21 -0
- data/README.md +234 -161
- data/README_YARD.md +237 -0
- data/bin/console +18 -5
- data/evolvable.gemspec +2 -2
- data/examples/ascii_art.rb +62 -0
- data/examples/ascii_gene.rb +9 -0
- data/examples/hello_world.rb +91 -0
- data/examples/images/diagram.png +0 -0
- data/examples/stickman.rb +77 -0
- data/exe/hello +16 -0
- data/lib/evolvable/count_gene.rb +42 -0
- data/lib/evolvable/equalize_goal.rb +29 -0
- data/lib/evolvable/evaluation.rb +29 -6
- data/lib/evolvable/evolution.rb +40 -8
- data/lib/evolvable/gene.rb +54 -2
- data/lib/evolvable/gene_combination.rb +73 -0
- data/lib/evolvable/genome.rb +86 -0
- data/lib/evolvable/goal.rb +36 -3
- data/lib/evolvable/maximize_goal.rb +30 -0
- data/lib/evolvable/minimize_goal.rb +29 -0
- data/lib/evolvable/mutation.rb +66 -15
- data/lib/evolvable/point_crossover.rb +33 -19
- data/lib/evolvable/population.rb +171 -31
- data/lib/evolvable/rigid_count_gene.rb +17 -0
- data/lib/evolvable/search_space.rb +181 -0
- data/lib/evolvable/selection.rb +28 -1
- data/lib/evolvable/serializer.rb +21 -0
- data/lib/evolvable/uniform_crossover.rb +28 -8
- data/lib/evolvable/version.rb +1 -1
- data/lib/evolvable.rb +197 -29
- metadata +38 -27
- data/.rubocop.yml +0 -20
- data/examples/evolvable_string/char_gene.rb +0 -9
- data/examples/evolvable_string.rb +0 -32
- data/lib/evolvable/gene_crossover.rb +0 -28
- data/lib/evolvable/gene_space.rb +0 -37
- data/lib/evolvable/goal/equalize.rb +0 -19
- data/lib/evolvable/goal/maximize.rb +0 -19
- data/lib/evolvable/goal/minimize.rb +0 -19
data/lib/evolvable/evolution.rb
CHANGED
@@ -1,25 +1,57 @@
|
|
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
|
-
|
27
|
+
combination: GeneCombination.new,
|
28
|
+
crossover: nil, # deprecated
|
9
29
|
mutation: Mutation.new)
|
10
30
|
@selection = selection
|
11
|
-
@
|
31
|
+
@combination = crossover || combination
|
12
32
|
@mutation = mutation
|
13
33
|
end
|
14
34
|
|
15
|
-
|
16
|
-
|
17
|
-
|
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
|
-
|
21
|
-
|
22
|
-
|
52
|
+
selection.call(population)
|
53
|
+
combination.call(population)
|
54
|
+
mutation.call(population)
|
23
55
|
population
|
24
56
|
end
|
25
57
|
end
|
data/lib/evolvable/gene.rb
CHANGED
@@ -1,13 +1,65 @@
|
|
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
|
-
|
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)
|
7
48
|
[gene_a, gene_b].sample
|
8
49
|
end
|
50
|
+
|
51
|
+
#
|
52
|
+
# @deprecated
|
53
|
+
# Will be removed in 2.0
|
54
|
+
# Use {#combine}
|
55
|
+
#
|
56
|
+
alias crossover combine
|
9
57
|
end
|
10
58
|
|
11
|
-
attr_accessor :
|
59
|
+
attr_accessor :evolvable
|
60
|
+
|
61
|
+
def key
|
62
|
+
self.class.key
|
63
|
+
end
|
12
64
|
end
|
13
65
|
end
|
@@ -0,0 +1,73 @@
|
|
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
|
+
if gene_a && gene_b
|
66
|
+
gene_class.combine(gene_a, gene_b)
|
67
|
+
else
|
68
|
+
gene_a || gene_b || gene_class.new
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
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, serializer: Serializer)
|
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(serializer: Serializer)
|
83
|
+
serializer.dump @config
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
data/lib/evolvable/goal.rb
CHANGED
@@ -1,18 +1,51 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Evolvable
|
4
|
-
|
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(
|
44
|
+
def evaluate(_evolvable)
|
12
45
|
raise Errors::UndefinedMethod, "#{self.class.name}##{__method__}"
|
13
46
|
end
|
14
47
|
|
15
|
-
def met?(
|
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
|
data/lib/evolvable/mutation.rb
CHANGED
@@ -1,42 +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 :
|
9
|
-
@rate = rate
|
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
|
-
|
66
|
+
mutate_evolvables(population.evolvables) unless probability.zero?
|
67
|
+
population
|
68
|
+
end
|
17
69
|
|
18
|
-
|
19
|
-
|
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
|
27
|
-
genes_count =
|
80
|
+
def mutate_genes(genes)
|
81
|
+
genes_count = genes.count
|
28
82
|
return if genes_count.zero?
|
29
83
|
|
30
|
-
return
|
84
|
+
return mutate_gene_by_index(genes, rand(genes_count)) unless rate
|
31
85
|
|
32
|
-
genes_count.times { |index|
|
86
|
+
genes_count.times { |index| mutate_gene_by_index(genes, index) if rand <= rate }
|
33
87
|
end
|
34
88
|
|
35
|
-
def
|
36
|
-
|
37
|
-
mutant_gene = gene.class.new
|
38
|
-
mutant_gene.key = gene.key
|
39
|
-
instance.genes[gene_index] = mutant_gene
|
89
|
+
def mutate_gene_by_index(genes, gene_index)
|
90
|
+
genes[gene_index] = genes[gene_index].class.new
|
40
91
|
end
|
41
92
|
end
|
42
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.
|
18
|
+
population.evolvables = new_evolvables(population, population.size)
|
13
19
|
population
|
14
20
|
end
|
15
21
|
|
16
|
-
|
17
|
-
|
18
|
-
|
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
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
return
|
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
|
-
|
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)
|
35
50
|
generate_ranges(genes_1.length).each do |range|
|
36
|
-
|
37
|
-
|
38
|
-
|
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)
|