evolvable 0.1.3 → 1.0.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/.gitignore +0 -1
- data/.rubocop.yml +1 -1
- data/CHANGELOG.md +14 -0
- data/Gemfile.lock +9 -26
- data/README.md +167 -254
- data/bin/console +5 -41
- data/evolvable.gemspec +0 -1
- data/examples/evolvable_string.rb +32 -0
- data/examples/evolvable_string/char_gene.rb +9 -0
- data/lib/evolvable.rb +45 -58
- data/lib/evolvable/error/undefined_method.rb +7 -0
- data/lib/evolvable/evaluation.rb +51 -0
- data/lib/evolvable/evolution.rb +26 -0
- data/lib/evolvable/gene.rb +13 -0
- data/lib/evolvable/gene_crossover.rb +28 -0
- data/lib/evolvable/gene_space.rb +37 -0
- data/lib/evolvable/goal.rb +19 -0
- data/lib/evolvable/goal/equalize.rb +19 -0
- data/lib/evolvable/goal/maximize.rb +19 -0
- data/lib/evolvable/goal/minimize.rb +19 -0
- data/lib/evolvable/mutation.rb +22 -44
- data/lib/evolvable/point_crossover.rb +57 -0
- data/lib/evolvable/population.rb +70 -91
- data/lib/evolvable/selection.rb +18 -0
- data/lib/evolvable/uniform_crossover.rb +22 -0
- data/lib/evolvable/version.rb +1 -1
- metadata +18 -7
- data/lib/evolvable/crossover.rb +0 -35
- data/lib/evolvable/errors/not_implemented.rb +0 -5
- data/lib/evolvable/helper_methods.rb +0 -45
- data/lib/evolvable/hooks.rb +0 -9
data/bin/console
CHANGED
@@ -3,48 +3,12 @@
|
|
3
3
|
require 'bundler/setup'
|
4
4
|
require 'evolvable'
|
5
5
|
|
6
|
-
|
7
|
-
# with your gem easier. You can also use a different console, if you like.
|
8
|
-
|
9
|
-
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
-
# require "pry"
|
11
|
-
# Pry.start
|
12
|
-
|
13
|
-
|
14
|
-
class BandMember
|
15
|
-
include Evolvable
|
16
|
-
|
17
|
-
SYNTHS = [:synth_1, :synth_2]
|
18
|
-
|
19
|
-
# SYNTH_OPTIONS = { synth_1: [[:cutoff, (1..100).to_a],
|
20
|
-
# [:reverb: (1..5).to_a]],
|
21
|
-
# synth_2: []}
|
22
|
-
|
23
|
-
SAMPLES = [:sample_1, :sample_2]
|
24
|
-
|
25
|
-
# SAMPLE_OPTIONS = { sample_1: [[:cutoff, (1..100).to_a],
|
26
|
-
# [:reverb: (1..5).to_a]],
|
27
|
-
# sample_2: []}
|
28
|
-
|
29
|
-
def self.evolvable_gene_pool
|
30
|
-
|
31
|
-
end
|
32
|
-
|
33
|
-
def base.evolvable_random_genes(count = nil)
|
34
|
-
gene_pool = evolvable_gene_pool_cache
|
35
|
-
count ||= evolvable_genes_count
|
36
|
-
gene_pool = gene_pool.sample(count) if count < gene_pool.size
|
37
|
-
genes = {}
|
38
|
-
gene_pool.each { |name, potentials| genes[name] = potentials.sample }
|
39
|
-
genes
|
40
|
-
end
|
41
|
-
|
42
|
-
def fitness
|
43
|
-
0
|
44
|
-
end
|
45
|
-
end
|
46
|
-
|
6
|
+
require './examples/evolvable_string'
|
47
7
|
|
8
|
+
puts "To get started, try this:\n\n" \
|
9
|
+
" population = EvolvableString.new_population\n" \
|
10
|
+
" population.mutation.probability = 0.8\n" \
|
11
|
+
" population.evolve(goal_value: EvolvableString::TARGET_STRING.length)\n\n"
|
48
12
|
|
49
13
|
require 'irb'
|
50
14
|
IRB.start(__FILE__)
|
data/evolvable.gemspec
CHANGED
@@ -0,0 +1,32 @@
|
|
1
|
+
require './examples/evolvable_string/char_gene'
|
2
|
+
|
3
|
+
class EvolvableString
|
4
|
+
include Evolvable
|
5
|
+
|
6
|
+
TARGET_STRING = 'supercalifragilisticexpialidocious'
|
7
|
+
|
8
|
+
def self.gene_space
|
9
|
+
{ char_genes: { type: 'CharGene', count: TARGET_STRING.length } }
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.before_evolution(population)
|
13
|
+
best_instance = population.best_instance
|
14
|
+
puts "#{best_instance} | #{best_instance.value} matches | Generation #{population.evolutions_count}"
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_s
|
18
|
+
find_genes(:char_genes).join
|
19
|
+
end
|
20
|
+
|
21
|
+
def value
|
22
|
+
@value ||= compute_value
|
23
|
+
end
|
24
|
+
|
25
|
+
def compute_value
|
26
|
+
value = 0
|
27
|
+
find_genes(:char_genes).each_with_index do |gene, index|
|
28
|
+
value += 1 if gene.to_s == TARGET_STRING[index]
|
29
|
+
end
|
30
|
+
value
|
31
|
+
end
|
32
|
+
end
|
data/lib/evolvable.rb
CHANGED
@@ -1,86 +1,73 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'forwardable'
|
4
|
-
require 'logger'
|
5
|
-
|
6
4
|
require 'evolvable/version'
|
7
|
-
require 'evolvable/
|
8
|
-
require 'evolvable/
|
5
|
+
require 'evolvable/error/undefined_method'
|
6
|
+
require 'evolvable/gene'
|
7
|
+
require 'evolvable/gene_space'
|
8
|
+
require 'evolvable/goal'
|
9
|
+
require 'evolvable/goal/equalize'
|
10
|
+
require 'evolvable/goal/maximize'
|
11
|
+
require 'evolvable/goal/minimize'
|
12
|
+
require 'evolvable/evaluation'
|
13
|
+
require 'evolvable/evolution'
|
14
|
+
require 'evolvable/selection'
|
15
|
+
require 'evolvable/gene_crossover'
|
16
|
+
require 'evolvable/point_crossover'
|
17
|
+
require 'evolvable/uniform_crossover'
|
9
18
|
require 'evolvable/mutation'
|
10
|
-
require 'evolvable/
|
11
|
-
require 'evolvable/hooks'
|
12
|
-
require 'evolvable/errors/not_implemented'
|
19
|
+
require 'evolvable/population'
|
13
20
|
|
14
21
|
module Evolvable
|
15
|
-
extend HelperMethods
|
16
|
-
|
17
22
|
def self.included(base)
|
18
|
-
base.
|
19
|
-
|
20
|
-
|
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
|
29
|
-
|
30
|
-
def base.evolvable_population_attrs
|
31
|
-
{}
|
23
|
+
def base.new_population(keyword_args = {})
|
24
|
+
keyword_args[:evolvable_class] = self
|
25
|
+
Population.new(**keyword_args)
|
32
26
|
end
|
33
27
|
|
34
|
-
def base.
|
35
|
-
|
36
|
-
args = evolvable_population_attrs.merge!(args)
|
37
|
-
args[:evolvable_class] = self
|
38
|
-
Population.new(args)
|
39
|
-
end
|
40
|
-
|
41
|
-
def base.evolvable_initialize(genes, population, _object_index)
|
42
|
-
evolvable = new
|
43
|
-
evolvable.genes = genes
|
28
|
+
def base.new_instance(population: nil, genes: [], population_index: nil)
|
29
|
+
evolvable = initialize_instance
|
44
30
|
evolvable.population = population
|
31
|
+
evolvable.genes = genes
|
32
|
+
evolvable.population_index = population_index
|
33
|
+
evolvable.initialize_instance
|
45
34
|
evolvable
|
46
35
|
end
|
47
36
|
|
48
|
-
def base.
|
49
|
-
|
37
|
+
def base.initialize_instance
|
38
|
+
new
|
50
39
|
end
|
51
40
|
|
52
|
-
def base.
|
53
|
-
|
41
|
+
def base.new_gene_space
|
42
|
+
GeneSpace.build(gene_space)
|
54
43
|
end
|
55
44
|
|
56
|
-
def base.
|
57
|
-
|
58
|
-
@evolvable_gene_pool_size = nil
|
45
|
+
def base.gene_space
|
46
|
+
{}
|
59
47
|
end
|
60
48
|
|
61
|
-
def base.
|
62
|
-
|
63
|
-
|
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
|
68
|
-
end
|
69
|
-
end
|
49
|
+
def base.before_evaluation(population); end
|
50
|
+
|
51
|
+
def base.before_evolution(population); end
|
70
52
|
|
71
|
-
|
72
|
-
@logger ||= Logger.new(STDOUT)
|
53
|
+
def base.after_evolution(population); end
|
73
54
|
end
|
74
55
|
|
75
|
-
|
76
|
-
|
56
|
+
def initialize_instance; end
|
57
|
+
|
58
|
+
attr_accessor :population,
|
59
|
+
:genes,
|
60
|
+
:population_index
|
61
|
+
|
62
|
+
def value
|
63
|
+
raise Errors::UndefinedMethod, "#{self.class.name}##{__method__}"
|
64
|
+
end
|
77
65
|
|
78
|
-
def
|
79
|
-
|
66
|
+
def find_gene(key)
|
67
|
+
@genes.detect { |g| g.key == key }
|
80
68
|
end
|
81
69
|
|
82
|
-
def
|
83
|
-
|
84
|
-
Evolvable.logger.info(info)
|
70
|
+
def find_genes(key)
|
71
|
+
@genes.select { |g| g.key == key }
|
85
72
|
end
|
86
73
|
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Evolvable
|
4
|
+
class Evaluation
|
5
|
+
GOALS = { maximize: Evolvable::Goal::Maximize.new,
|
6
|
+
minimize: Evolvable::Goal::Minimize.new,
|
7
|
+
equalize: Evolvable::Goal::Equalize.new }.freeze
|
8
|
+
|
9
|
+
def initialize(goal = :maximize)
|
10
|
+
@goal = normalize_goal(goal)
|
11
|
+
end
|
12
|
+
|
13
|
+
attr_accessor :goal
|
14
|
+
|
15
|
+
def call(population)
|
16
|
+
population.instances.sort_by! { |instance| goal.evaluate(instance) }
|
17
|
+
end
|
18
|
+
|
19
|
+
def best_instance(population)
|
20
|
+
population.instances.max_by { |instance| goal.evaluate(instance) }
|
21
|
+
end
|
22
|
+
|
23
|
+
def met_goal?(population)
|
24
|
+
goal.met?(population.instances.last)
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def normalize_goal(goal_arg)
|
30
|
+
case goal_arg
|
31
|
+
when Symbol
|
32
|
+
goal_from_symbol(goal_arg)
|
33
|
+
when Hash
|
34
|
+
goal_from_hash(goal_arg)
|
35
|
+
else
|
36
|
+
goal
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def goal_from_symbol(goal_arg)
|
41
|
+
GOALS[goal_arg]
|
42
|
+
end
|
43
|
+
|
44
|
+
def goal_from_hash(goal_arg)
|
45
|
+
goal_type, value = goal_arg.first
|
46
|
+
goal = GOALS[goal_type]
|
47
|
+
goal.value = value
|
48
|
+
goal
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Evolvable
|
4
|
+
class Evolution
|
5
|
+
extend Forwardable
|
6
|
+
|
7
|
+
def initialize(selection: Selection.new,
|
8
|
+
crossover: GeneCrossover.new,
|
9
|
+
mutation: Mutation.new)
|
10
|
+
@selection = selection
|
11
|
+
@crossover = crossover
|
12
|
+
@mutation = mutation
|
13
|
+
end
|
14
|
+
|
15
|
+
attr_accessor :selection,
|
16
|
+
:crossover,
|
17
|
+
:mutation
|
18
|
+
|
19
|
+
def call(population)
|
20
|
+
@selection.call(population)
|
21
|
+
@crossover.call(population)
|
22
|
+
@mutation.call(population)
|
23
|
+
population
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Evolvable
|
4
|
+
class GeneCrossover
|
5
|
+
def call(population)
|
6
|
+
population.instances = initialize_offspring(population)
|
7
|
+
population
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def initialize_offspring(population)
|
13
|
+
parent_genes = population.instances.map!(&:genes)
|
14
|
+
parent_gene_couples = parent_genes.combination(2).cycle
|
15
|
+
Array.new(population.size) do |index|
|
16
|
+
genes_1, genes_2 = parent_gene_couples.next
|
17
|
+
genes = crossover_genes(genes_1, genes_2)
|
18
|
+
population.new_instance(genes: genes, population_index: index)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def crossover_genes(genes_1, genes_2)
|
23
|
+
genes_1.zip(genes_2).map! do |gene_a, gene_b|
|
24
|
+
gene_a.class.crossover(gene_a, gene_b)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Evolvable
|
4
|
+
class GeneSpace
|
5
|
+
def self.build(config)
|
6
|
+
return config if config.respond_to?(:new_genes)
|
7
|
+
|
8
|
+
new(config: config)
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize(config: {})
|
12
|
+
@config = normalize_config(config)
|
13
|
+
end
|
14
|
+
|
15
|
+
attr_reader :config
|
16
|
+
|
17
|
+
def new_genes
|
18
|
+
genes = []
|
19
|
+
config.each do |gene_key, gene_config|
|
20
|
+
(gene_config[:count] || 1).times do
|
21
|
+
gene = gene_config[:class].new
|
22
|
+
gene.key = gene_key
|
23
|
+
genes << gene
|
24
|
+
end
|
25
|
+
end
|
26
|
+
genes
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def normalize_config(config)
|
32
|
+
config.each do |_gene_key, gene_config|
|
33
|
+
gene_config[:class] = Kernel.const_get(gene_config[:type]) if gene_config[:type]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Evolvable
|
4
|
+
module Goal
|
5
|
+
def initialize(value: nil)
|
6
|
+
@value = value if value
|
7
|
+
end
|
8
|
+
|
9
|
+
attr_accessor :value
|
10
|
+
|
11
|
+
def evaluate(_instance)
|
12
|
+
raise Errors::UndefinedMethod, "#{self.class.name}##{__method__}"
|
13
|
+
end
|
14
|
+
|
15
|
+
def met?(_instance)
|
16
|
+
raise Errors::UndefinedMethod, "#{self.class.name}##{__method__}"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Evolvable::Goal
|
4
|
+
class Equalize
|
5
|
+
include Evolvable::Goal
|
6
|
+
|
7
|
+
def value
|
8
|
+
@value ||= 0
|
9
|
+
end
|
10
|
+
|
11
|
+
def evaluate(instance)
|
12
|
+
-(instance.value - value).abs
|
13
|
+
end
|
14
|
+
|
15
|
+
def met?(instance)
|
16
|
+
instance.value == value
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|