charlie 0.5.0 → 0.6.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.
- data/History.txt +11 -0
- data/Manifest.txt +5 -1
- data/README.txt +33 -43
- data/Rakefile +30 -5
- data/TODO.txt +11 -9
- data/data/CROSSOVER +6 -1
- data/data/GENOTYPE +5 -2
- data/data/MUTATION +5 -0
- data/data/SELECTION +2 -3
- data/examples/money.rb +35 -0
- data/examples/output/flattened_sombero.html +3849 -3849
- data/examples/output/flattened_sombero2_.html +2321 -2321
- data/examples/output/fopt1_dblopt.html +1276 -1276
- data/examples/output/hill10.html +3906 -3906
- data/examples/output/hill2.csv +24 -24
- data/examples/output/hill2.html +177 -177
- data/examples/output/royalroad1_report.html +531 -531
- data/examples/output/royalroad2_report.html +592 -592
- data/examples/output/royalroadquick_report.html +243 -243
- data/examples/output/tsp.html +947 -403
- data/examples/output/weasel1_report.html +616 -616
- data/examples/output/weasel2_report.html +115 -115
- data/examples/tree.rb +90 -0
- data/examples/tsp.rb +19 -12
- data/lib/charlie/etc/monkey.rb +28 -44
- data/lib/charlie/genotype.rb +30 -1
- data/lib/charlie/permutation/permutation.rb +52 -1
- data/lib/charlie/population.rb +58 -20
- data/lib/charlie/selection.rb +1 -1
- data/lib/charlie/tree/tree.rb +128 -0
- data/lib/charlie.rb +2 -5
- data/test/t_common.rb +18 -2
- data/test/test_benchmark.rb +12 -10
- data/test/test_evolve.rb +87 -0
- data/test/test_permutation.rb +37 -3
- data/test/test_tree.rb +57 -0
- metadata +69 -50
- data/test/test_basic.rb +0 -32
@@ -22,7 +22,6 @@ end
|
|
22
22
|
|
23
23
|
# Transposition mutator for PermutationGenotype
|
24
24
|
module PermutationMutator
|
25
|
-
|
26
25
|
# Transposes two elements
|
27
26
|
def mutate!
|
28
27
|
i1, i2 = @genes.rand_index,@genes.rand_index
|
@@ -31,6 +30,24 @@ module PermutationMutator
|
|
31
30
|
end
|
32
31
|
end
|
33
32
|
|
33
|
+
# Inversion mutator for PermutationGenotype. May work on other array/string-based genotypes as well, but this is untested.
|
34
|
+
# Takes two random indices, and reverses the elements in between (includes possible wrapping if index2 < index1)
|
35
|
+
module InversionMutator
|
36
|
+
# Inverts parts of the genes
|
37
|
+
def mutate!
|
38
|
+
i1, i2 = @genes.rand_index,@genes.rand_index
|
39
|
+
if i2 >= i1
|
40
|
+
@genes[i1..i2] = @genes[i1..i2].reverse unless i1==i2
|
41
|
+
else
|
42
|
+
reversed = (@genes[i1..-1] + @genes[0..i2]).reverse
|
43
|
+
@genes[i1..-1] = reversed.slice!(0,@genes.size-i1)
|
44
|
+
@genes[0..i2] = reversed
|
45
|
+
end
|
46
|
+
self
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
|
34
51
|
# One point partial preservation crossover for PermutationGenotype
|
35
52
|
# Child 1 is identical to parent 1 up to the cross point, and contains the remaining elements in the same order as parent 2.
|
36
53
|
module PermutationCrossover
|
@@ -44,4 +61,38 @@ module PermutationCrossover
|
|
44
61
|
end
|
45
62
|
|
46
63
|
|
64
|
+
# Edge recombination operator (http://en.wikipedia.org/wiki/Edge_recombination_operator)
|
65
|
+
# * Useful in permutations representing a path, e.g. travelling salesperson.
|
66
|
+
# * Returns a single child.
|
67
|
+
# * Permutations of 0...n only, i.e. default second parameter to PermutationGenotype
|
68
|
+
# * Rather slow.
|
69
|
+
module EdgeRecombinationCrossover
|
70
|
+
def cross(parent1,parent2)
|
71
|
+
p1, p2 = parent1.genes, parent2.genes
|
72
|
+
|
73
|
+
nb = Array.new(parent1.size){[]}
|
74
|
+
(p1 + p1[0..1]).each_cons(3){|l,m,r| nb[m] += [l,r] } # build neighbour lists
|
75
|
+
(p2 + p2[0..1]).each_cons(3){|l,m,r| nb[m] += [l,r] } # build neighbour lists
|
76
|
+
nb.map(&:uniq!)
|
77
|
+
|
78
|
+
n = (rand < 0.5 ? p1.first : p2.first)
|
79
|
+
child = [n]
|
80
|
+
(nb.size-1).times{
|
81
|
+
nb.each{|l| l.delete n unless l==:done } # remove n from the lists
|
82
|
+
|
83
|
+
if nb[n].empty? # nb[n] empty, pick random next
|
84
|
+
nb[n] = :done
|
85
|
+
n = (0...nb.size).find_all{|x| nb[x] != :done }.at_rand # no neighbors left, pick random
|
86
|
+
else # pick neighbour with minimal degree, random tie-breaking
|
87
|
+
min_deg = nb[n].map{|x| nb[x].size }.min
|
88
|
+
next_n = nb[n].find_all{|x| nb[x].size == min_deg }.at_rand # pick random neighbor with min. degree
|
89
|
+
nb[n] = :done
|
90
|
+
n = next_n
|
91
|
+
end
|
92
|
+
child << n # add new n to path
|
93
|
+
}
|
94
|
+
|
95
|
+
return from_genes(child)
|
96
|
+
end
|
97
|
+
end
|
47
98
|
|
data/lib/charlie/population.rb
CHANGED
@@ -4,6 +4,8 @@
|
|
4
4
|
# The population class represents an array of genotypes.
|
5
5
|
# Create an instance of this, and call one of the evolve functions to run the genetic algorithm.
|
6
6
|
class Population
|
7
|
+
DEFAULT_MAX_GENS = 100
|
8
|
+
|
7
9
|
attr_reader :size, :population
|
8
10
|
def initialize(genotype_class,population_size=20)
|
9
11
|
@size = population_size
|
@@ -11,36 +13,66 @@ class Population
|
|
11
13
|
@population = Array.new(population_size){ genotype_class.new }
|
12
14
|
end
|
13
15
|
|
14
|
-
#
|
15
|
-
def
|
16
|
-
|
17
|
-
|
18
|
-
best = @population.max
|
19
|
-
puts "#{g}\t\t#{best.fitness}\t\t#{best.to_s}"
|
16
|
+
# yields population and generation number to block each generation, for a maximum of max_generations.
|
17
|
+
def evolve_block(max_generations=DEFAULT_MAX_GENS)
|
18
|
+
yield @population, 0
|
19
|
+
(max_generations || DEFAULT_MAX_GENS).times {|generation|
|
20
20
|
@population = @genotype_class.next_generation(@population){|*parents|
|
21
21
|
ch = [*@genotype_class.cross(*parents)]
|
22
22
|
ch.each{|c| c.mutate! }
|
23
23
|
ch
|
24
24
|
}
|
25
|
+
yield @population, generation+1
|
26
|
+
}
|
27
|
+
@population
|
28
|
+
end
|
29
|
+
|
30
|
+
# Runs the genetic algorithm without any output. Returns the population sorted by fitness (unsorted for co-evolution).
|
31
|
+
def evolve_silent(generations=DEFAULT_MAX_GENS)
|
32
|
+
evolve_block(generations){}
|
33
|
+
@population.sort_by{|x|x.fitness} rescue @population
|
34
|
+
end
|
35
|
+
|
36
|
+
# Runs the genetic algorithm with some stats on the console. Returns the population sorted by fitness.
|
37
|
+
def evolve_on_console(generations=DEFAULT_MAX_GENS)
|
38
|
+
puts "Generation\tBest Fitness\t\tBest Individual"
|
39
|
+
evolve_block(generations) {|p,g|
|
40
|
+
best = p.max
|
41
|
+
puts "#{g}\t\t#{best.fitness}\t\t#{best.to_s}"
|
25
42
|
}
|
26
43
|
@population = @population.sort_by{|x|x.fitness}
|
27
44
|
puts "Finished: Best fitness = #{@population[-1].fitness}"
|
28
45
|
@population
|
29
46
|
end
|
47
|
+
alias :evolve :evolve_on_console
|
30
48
|
|
31
|
-
#
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
49
|
+
# breaks if the block (which is passed the population each generation) returns true.
|
50
|
+
# returns an array [population, generations needed]. generations needed==nil for no convergence.
|
51
|
+
def evolve_until_population(generations=DEFAULT_MAX_GENS)
|
52
|
+
tot_gens = nil
|
53
|
+
evolve_block(generations) {|p,g|
|
54
|
+
if yield(p)
|
55
|
+
tot_gens = g
|
56
|
+
break
|
57
|
+
end
|
39
58
|
}
|
40
|
-
@population
|
59
|
+
[@population, tot_gens]
|
41
60
|
end
|
42
61
|
|
43
|
-
|
62
|
+
# breaks if the block (which is passed the best individual each generation) returns true.
|
63
|
+
# returns an array [population, generations needed]. generations needed==nil for no convergence.
|
64
|
+
def evolve_until_best(generations=DEFAULT_MAX_GENS)
|
65
|
+
tot_gens = nil
|
66
|
+
evolve_block(generations) {|p,g|
|
67
|
+
if yield(p.max)
|
68
|
+
tot_gens = g
|
69
|
+
break
|
70
|
+
end
|
71
|
+
}
|
72
|
+
[@population, tot_gens]
|
73
|
+
end
|
74
|
+
alias :evolve_until :evolve_until_best
|
75
|
+
|
44
76
|
|
45
77
|
class << self
|
46
78
|
|
@@ -105,7 +137,7 @@ INFO
|
|
105
137
|
File.open(html_outfile,'w'){|f| f << File.read(File.dirname(__FILE__)+"/../../data/template.html").sub('{{CONTENT}}',html_tables) } if html_outfile
|
106
138
|
|
107
139
|
# write csv file, contains raw stats
|
108
|
-
File.open(csv_outfile,'w'){|f| f << data.map{|r|r[0
|
140
|
+
File.open(csv_outfile,'w'){|f| f << data.map{|r|r[0..2] << r[-1].join(', ') }.to_table.to_csv } if csv_outfile
|
109
141
|
|
110
142
|
puts '',tbl.to_s
|
111
143
|
data
|
@@ -140,9 +172,15 @@ class StrategiesDSL
|
|
140
172
|
# Get all the tests. Basically a cartesian product of all selection, crossover and mutation methods.
|
141
173
|
def get_tests
|
142
174
|
t = []
|
143
|
-
|
144
|
-
|
145
|
-
|
175
|
+
|
176
|
+
defmod = Module.new{self.name='default'}
|
177
|
+
@s ||= [defmod]
|
178
|
+
@c ||= [defmod]
|
179
|
+
@m ||= [defmod]
|
180
|
+
|
181
|
+
#raise 'No selection modules defined' unless @s
|
182
|
+
#raise 'No crossover modules defined' unless @c
|
183
|
+
#raise 'No mutation modules defined' unless @m
|
146
184
|
@s.each{|s|
|
147
185
|
@c.each{|c|
|
148
186
|
@m.each{|m|
|
data/lib/charlie/selection.rb
CHANGED
@@ -15,7 +15,7 @@ end
|
|
15
15
|
# and crosses them randomly to replace all the others.
|
16
16
|
# +best+ can be an float < 1 (fraction of population size), or a number >=1 (number of individuals)
|
17
17
|
def TruncationSelection(best=0.3)
|
18
|
-
Module
|
18
|
+
Module.new{
|
19
19
|
@@best = best
|
20
20
|
def next_generation(population)
|
21
21
|
k = if @@best >= 1 # best is an integer >= 1 -> select this much
|
@@ -0,0 +1,128 @@
|
|
1
|
+
#Tree genotype, crossover, mutation etc.
|
2
|
+
|
3
|
+
# some general helper functions, which are independant of the operator arrays
|
4
|
+
module GPTreeHelper
|
5
|
+
def dup_tree(t)
|
6
|
+
if t.first==:term
|
7
|
+
t.clone # avoid inf recursion here
|
8
|
+
else
|
9
|
+
t.map{|st| st.is_a?(Symbol) ? st : dup_tree(st) }
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def tree_size(t)
|
14
|
+
if t.first==:term
|
15
|
+
1
|
16
|
+
else
|
17
|
+
t[1..-1].inject(1){|sum,st| sum + tree_size(st) }
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def all_subtrees(t)
|
22
|
+
if t.first==:term
|
23
|
+
[t]
|
24
|
+
else
|
25
|
+
t[1..-1].map{|st| all_subtrees(st) }.inject{|a,b|a+b} << t
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def random_subtree(t=@genes)
|
30
|
+
all_subtrees(t).at_rand
|
31
|
+
end
|
32
|
+
|
33
|
+
def eval_tree(tree,values_hash)
|
34
|
+
if tree.first == :term
|
35
|
+
termval = tree[1]
|
36
|
+
if termval.is_a?(Symbol) # look up symbols in the hash
|
37
|
+
termval = values_hash[termval]
|
38
|
+
termval = termval.call if termval.is_a?(Proc) # and if hash value is a proc, evaluate it
|
39
|
+
end
|
40
|
+
termval
|
41
|
+
else # tree.first is an operator
|
42
|
+
eval_tree(tree[1],values_hash).send(tree.first, *tree[2..-1].map{|t| eval_tree(t,values_hash) } )
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
extend self
|
47
|
+
end
|
48
|
+
|
49
|
+
# Tree genotype, for genetic programming etc.
|
50
|
+
# * Pass arrays of terminals/binary operators and unary operators to this function to generate a class.
|
51
|
+
# * terminals can be procs (eval'd on initialization), symbols (replaced by values in calls to eval_genes) or anything else (not changed, so make sure all operators are defined for these)
|
52
|
+
# * This needs more options. Depth of initial trees, etc. Also needs a better mutator.
|
53
|
+
def TreeGenotype(terminals = [proc{rand},:x], binary_ops = [:+,:*,:-,:/], unary_ops = nil)
|
54
|
+
Class.new(Genotype) {
|
55
|
+
@@terms = terminals
|
56
|
+
@@binary_ops = binary_ops
|
57
|
+
@@unary_ops = unary_ops
|
58
|
+
|
59
|
+
def initialize
|
60
|
+
self.genes = random_tree(3)
|
61
|
+
end
|
62
|
+
|
63
|
+
def genes=(g)
|
64
|
+
class << g # ensures a genes.dup call is a deep copy
|
65
|
+
def dup
|
66
|
+
GPTreeHelper.dup_tree(self)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
@genes = g
|
70
|
+
end
|
71
|
+
|
72
|
+
def size
|
73
|
+
tree_size(@genes)
|
74
|
+
end
|
75
|
+
|
76
|
+
def random_tree(d)
|
77
|
+
if d == 0 || rand < 1.0 / 3
|
78
|
+
e = @@terms.at_rand
|
79
|
+
[:term, e.is_a?(Proc) ? e.call : e]
|
80
|
+
else
|
81
|
+
if @@unary_ops.nil? || rand < 0.5
|
82
|
+
[@@binary_ops.at_rand,random_tree(d-1),random_tree(d-1)]
|
83
|
+
else
|
84
|
+
[@@unary_ops.at_rand,random_tree(d-1)]
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def eval_genes(terminals_value_hash = {})
|
90
|
+
eval_tree(@genes,terminals_value_hash)
|
91
|
+
end
|
92
|
+
|
93
|
+
def to_s
|
94
|
+
@genes.inspect
|
95
|
+
end
|
96
|
+
|
97
|
+
use PCross(0.7,TreeCrossover), PMutate(0.5,TreeMutator) # TODO: test what probabilities are best
|
98
|
+
|
99
|
+
# make helper functions available at both class and instance level
|
100
|
+
include GPTreeHelper
|
101
|
+
class << self
|
102
|
+
include GPTreeHelper
|
103
|
+
end
|
104
|
+
}
|
105
|
+
end
|
106
|
+
|
107
|
+
# TreeCrossover does a standard subtree swapping crossover for trees.
|
108
|
+
module TreeCrossover
|
109
|
+
def cross(parent1,parent2)
|
110
|
+
child1 = parent1.dup
|
111
|
+
child2 = parent2.dup
|
112
|
+
c1_st = child1.random_subtree
|
113
|
+
c2_st = child1.random_subtree
|
114
|
+
c1_copy = dup_tree(c1_st)
|
115
|
+
c1_st.replace dup_tree(c2_st)
|
116
|
+
c2_st.replace c1_copy
|
117
|
+
[child1,child2]
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
# TreeMutator replaces a randomly chosen subtree with a new, randomly generated, subtree of depth <= 2.
|
122
|
+
module TreeMutator
|
123
|
+
def mutate!
|
124
|
+
random_subtree.replace random_tree(2)
|
125
|
+
self
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
data/lib/charlie.rb
CHANGED
@@ -4,7 +4,7 @@ $:.unshift File.dirname(__FILE__)
|
|
4
4
|
|
5
5
|
# This is just a dummy module to avoid making the VERSION constant a global.
|
6
6
|
module Charlie
|
7
|
-
VERSION = '0.
|
7
|
+
VERSION = '0.6.0'
|
8
8
|
end
|
9
9
|
|
10
10
|
require 'charlie/etc/monkey'
|
@@ -26,10 +26,7 @@ require 'charlie/list/list_genotype'
|
|
26
26
|
|
27
27
|
require 'charlie/permutation/permutation'
|
28
28
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
29
|
+
require 'charlie/tree/tree'
|
33
30
|
|
34
31
|
|
35
32
|
|
data/test/t_common.rb
CHANGED
@@ -1,7 +1,5 @@
|
|
1
1
|
|
2
2
|
|
3
|
-
|
4
|
-
|
5
3
|
require File.dirname(__FILE__) + '/../lib/charlie' unless Object.const_defined?('Charlie')
|
6
4
|
require 'test/unit'
|
7
5
|
|
@@ -30,3 +28,21 @@ class StringA < StringGenotype(20,'a'..'d')
|
|
30
28
|
end
|
31
29
|
use UniformCrossover
|
32
30
|
end
|
31
|
+
|
32
|
+
class RRTest < BitStringGenotype(8) # easier convergence, fitness 0 at max. for testing evolve_until, etc.
|
33
|
+
def fitness
|
34
|
+
genes.enum_slice(2).find_all{|e| e.all?{|x|x==1} }.size - 4
|
35
|
+
end
|
36
|
+
use ListMutator(:expected_n[1],:flip), TruncationSelection(0.3), UniformCrossover
|
37
|
+
end
|
38
|
+
|
39
|
+
class TestTest < Test::Unit::TestCase
|
40
|
+
def test_testclass
|
41
|
+
assert_nothing_raised{
|
42
|
+
Population.new(TestClass(Module::new),10).evolve_silent(2)
|
43
|
+
Population.new(TestClass(UniformCrossover),10).evolve_silent(2)
|
44
|
+
Population.new(TestClass(ListMutator(:single_point,:gaussian) ),10).evolve_silent(2)
|
45
|
+
Population.new(TestClass(TournamentSelection(4)),10).evolve_silent(2)
|
46
|
+
}
|
47
|
+
end
|
48
|
+
end
|
data/test/test_benchmark.rb
CHANGED
@@ -4,6 +4,7 @@ CDIR = File.dirname(__FILE__)
|
|
4
4
|
|
5
5
|
class BMTest < Test::Unit::TestCase
|
6
6
|
def setup
|
7
|
+
Dir.mkdir(CDIR+'/output/') rescue nil
|
7
8
|
Dir[CDIR+'/output/*'].each{|f| File.unlink f}
|
8
9
|
end
|
9
10
|
|
@@ -33,23 +34,24 @@ class BMTest < Test::Unit::TestCase
|
|
33
34
|
assert( File.readlines(CDIR+'/output/test_benchmark.csv').size == 4, 'Output file not generated.')
|
34
35
|
end
|
35
36
|
|
36
|
-
def
|
37
|
-
|
38
|
-
Population.benchmark(StringA,nil,nil){
|
39
|
-
selection TruncationSelection(0.2)
|
37
|
+
def test_defaults
|
38
|
+
assert_nothing_raised {
|
39
|
+
r = Population.benchmark(StringA,nil,nil){
|
40
|
+
selection TruncationSelection(0.2), TournamentSelection(3)
|
40
41
|
# no crossover, etc specified.
|
41
42
|
}
|
43
|
+
assert_equal 2,r.size
|
42
44
|
}
|
43
|
-
|
44
|
-
Population.benchmark(StringA,nil,nil){
|
45
|
+
assert_nothing_raised {
|
46
|
+
r=Population.benchmark(StringA,nil,nil){
|
45
47
|
crossover NullCrossover
|
46
48
|
}
|
49
|
+
assert_equal 1,r.size
|
47
50
|
}
|
48
|
-
|
49
|
-
Population.benchmark(StringA,nil,nil){
|
50
|
-
mutation Module.new
|
51
|
-
selection Module.new
|
51
|
+
assert_nothing_raised {
|
52
|
+
r=Population.benchmark(StringA,nil,nil){
|
52
53
|
}
|
54
|
+
assert_equal 1,r.size
|
53
55
|
}
|
54
56
|
end
|
55
57
|
|
data/test/test_evolve.rb
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
require 't_common'
|
2
|
+
|
3
|
+
class EvolveTest < Test::Unit::TestCase
|
4
|
+
def test_console
|
5
|
+
r = Population.new(TestProblem,10).evolve_on_console(10)
|
6
|
+
assert_respond_to r, :[]
|
7
|
+
assert_respond_to r[-1], :fitness
|
8
|
+
end
|
9
|
+
|
10
|
+
def test_silent
|
11
|
+
r = Population.new(TestProblem,10).evolve_silent(10)
|
12
|
+
assert_respond_to r, :[]
|
13
|
+
assert_respond_to r[-1], :fitness
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_continue # test if calling evolve twice does not reset the population
|
17
|
+
5.times{
|
18
|
+
p = Population.new(TestProblem,10)
|
19
|
+
best3 = p.evolve_silent(3)[-1].fitness
|
20
|
+
best6 = p.evolve_silent(3)[-1].fitness
|
21
|
+
assert( best6 >= best3 )
|
22
|
+
best36 = p.evolve_silent(30)[-1].fitness
|
23
|
+
# Not true for all problems, but in this case the probability of failure is negligible
|
24
|
+
assert( best36 > best6, "test_continue failed. Please rerun test to make sure this isn't just extremely bad luck.")
|
25
|
+
}
|
26
|
+
end
|
27
|
+
|
28
|
+
def test_until_best # test evolve_until_best
|
29
|
+
start = Time.now
|
30
|
+
r,g = Population.new(RRTest,10).evolve_until_best(1000_000_000){|b| b.fitness == 0}
|
31
|
+
assert_not_nil g
|
32
|
+
assert g >= 0 && g < 1000, "test_until_pop failed to converge within 1000 generations"
|
33
|
+
assert( (Time.now - start < 1), "test_until failed to converge within 1 second.")
|
34
|
+
assert_equal r.max.fitness, 0 # actually converged
|
35
|
+
assert_respond_to r, :[]
|
36
|
+
assert_respond_to r[-1], :fitness
|
37
|
+
# test on failure to converge
|
38
|
+
r,g = Population.new(RRTest,10).evolve_until_best(13){|b| false }
|
39
|
+
assert_nil g # did not converge
|
40
|
+
assert_respond_to r, :[]
|
41
|
+
assert_respond_to r[-1], :fitness
|
42
|
+
# alias
|
43
|
+
Population.new(RRTest,10).evolve_until{|b| b.fitness == 0}
|
44
|
+
end
|
45
|
+
|
46
|
+
|
47
|
+
def test_until_pop # test evolve_until_population
|
48
|
+
start = Time.now
|
49
|
+
r,g = Population.new(RRTest,10).evolve_until_population(1000_000_000){|p| p.count{|x| x.fitness == 0 } > 3}
|
50
|
+
assert_not_nil g
|
51
|
+
assert g >= 0 && g < 1000, "test_until_pop failed to converge within 1000 generations"
|
52
|
+
assert( (Time.now - start < 1), "test_until_pop failed to converge within 1 second.")
|
53
|
+
assert r.count{|x| x.fitness == 0 } > 3 # actually converged to > 3 clones of best solution
|
54
|
+
assert_respond_to r, :[]
|
55
|
+
assert_respond_to r[-1], :fitness
|
56
|
+
# test on failure to converge
|
57
|
+
r,g = Population.new(RRTest,10).evolve_until_population(13){|b| false }
|
58
|
+
assert_nil g # did not converge
|
59
|
+
assert_respond_to r, :[]
|
60
|
+
assert_respond_to r[-1], :fitness
|
61
|
+
end
|
62
|
+
|
63
|
+
# doesn't really belong here, and should be moved...eventually
|
64
|
+
def test_cache
|
65
|
+
klass_r = Class.new(TestProblem){ def fitness; rand; end }
|
66
|
+
|
67
|
+
# test cache in random fitness
|
68
|
+
e = klass_r.new;
|
69
|
+
assert_not_equal e.fitness, e.fitness
|
70
|
+
klass_r.class_eval{ cache_fitness }
|
71
|
+
assert_equal e.fitness, e.fitness
|
72
|
+
# test performance gain of cache. using tournamentselection(3) and sleep for maximum effect
|
73
|
+
klass_nc = Class.new(TestProblem){ def fitness; sleep(0.01); rand; end; use TournamentSelection(3) }
|
74
|
+
klass_c = Class.new(klass_nc){ cache_fitness }
|
75
|
+
3.times { # consistently
|
76
|
+
s = Time.now
|
77
|
+
Population.new(klass_nc,5).evolve_silent(2)
|
78
|
+
t_nc = Time.now - s
|
79
|
+
|
80
|
+
s = Time.now
|
81
|
+
Population.new(klass_c,5).evolve_silent(2)
|
82
|
+
t_c = Time.now - s
|
83
|
+
assert t_c < t_nc # 3x as fast, so fairly safe test
|
84
|
+
}
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
data/test/test_permutation.rb
CHANGED
@@ -2,15 +2,21 @@ require 't_common'
|
|
2
2
|
|
3
3
|
N=10
|
4
4
|
|
5
|
-
#CITIES = (0...N).map{|i| th = i * 2 * Math::PI / N; [Math.cos(th),Math.sin(th)] }
|
6
|
-
#p CITIES
|
7
|
-
|
8
5
|
class PermutationTest < PermutationGenotype(N)
|
9
6
|
def fitness
|
10
7
|
(0...N).zip_with(genes){|a,b| a==b ? 1 : 0}.sum
|
11
8
|
end
|
12
9
|
end
|
13
10
|
|
11
|
+
class PermutationTestERO < PermutationTest
|
12
|
+
use EdgeRecombinationCrossover
|
13
|
+
use PMutate(0.5,PermutationMutator)
|
14
|
+
end
|
15
|
+
|
16
|
+
class PermutationTestInvert < PermutationTest
|
17
|
+
use InversionMutator, RandomSelection
|
18
|
+
end
|
19
|
+
|
14
20
|
|
15
21
|
class PermTests < Test::Unit::TestCase
|
16
22
|
def test_evolve
|
@@ -20,4 +26,32 @@ class PermTests < Test::Unit::TestCase
|
|
20
26
|
}
|
21
27
|
p.each{|s| assert_equal s.genes.sort, (0...N).to_a }
|
22
28
|
end
|
29
|
+
|
30
|
+
|
31
|
+
def test_edge_recombination
|
32
|
+
p=nil
|
33
|
+
assert_nothing_raised{
|
34
|
+
p=Population.new(PermutationTestERO,20).evolve_silent(20)
|
35
|
+
}
|
36
|
+
p.each{|s| assert_equal s.genes.sort, (0...N).to_a }
|
37
|
+
end
|
38
|
+
|
39
|
+
def test_edge_recombination_rand # test if permutation doesn't just stay preserved because of fitness
|
40
|
+
p=nil
|
41
|
+
assert_nothing_raised{
|
42
|
+
klass = Class.new(PermutationTestERO){ use RandomSelection }
|
43
|
+
p=Population.new(klass,20).evolve_silent(20)
|
44
|
+
}
|
45
|
+
p.each{|s| assert_equal s.genes.sort, (0...N).to_a }
|
46
|
+
end
|
47
|
+
|
48
|
+
def test_inversion_mutator # test if inversion mutator works
|
49
|
+
p=nil
|
50
|
+
assert_nothing_raised{
|
51
|
+
p=Population.new(PermutationTestInvert,20).evolve_silent(20)
|
52
|
+
}
|
53
|
+
p.each{|s| assert_equal s.genes.sort, (0...N).to_a }
|
54
|
+
end
|
55
|
+
|
56
|
+
|
23
57
|
end
|
data/test/test_tree.rb
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
require 't_common'
|
2
|
+
|
3
|
+
class TreeTest < TreeGenotype([1,:x,proc{rand}], [:+], [:+@])
|
4
|
+
def fitness
|
5
|
+
- (eval_genes(:x=>1) - 5).abs
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
class BloatTest < TreeTest
|
10
|
+
def fitness
|
11
|
+
size
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
# hard to test, also see examples/tree.rb
|
16
|
+
class TreeTests < Test::Unit::TestCase
|
17
|
+
def test_evolve
|
18
|
+
p=nil
|
19
|
+
assert_nothing_raised{
|
20
|
+
p=Population.new(TreeTest,20).evolve_silent(20)
|
21
|
+
}
|
22
|
+
p.each{|s| assert s.genes.is_a?(Array) }
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_eval
|
26
|
+
p=TreeTest.new
|
27
|
+
p.genes = [:term,5]
|
28
|
+
assert_equal 0, p.fitness
|
29
|
+
p.genes = [:+, [:term,:x], [:term,:x]]
|
30
|
+
assert_equal 2, p.eval_genes(:x=>1)
|
31
|
+
assert_equal 6, p.eval_genes(:x=>3)
|
32
|
+
assert_equal( -3, p.fitness)
|
33
|
+
p.genes = [:term,:x]
|
34
|
+
assert_not_equal p.eval_genes(:x=>proc{rand}), p.eval_genes(:x=>proc{rand})
|
35
|
+
assert p.eval_genes(:x=>proc{rand}).is_a?(Float)
|
36
|
+
end
|
37
|
+
|
38
|
+
def test_size
|
39
|
+
p=TreeTest.new
|
40
|
+
p.genes = [:term,5]
|
41
|
+
assert_equal 1, p.size
|
42
|
+
p.genes = [:+, [:term,:x], [:term,:x]]
|
43
|
+
assert_equal 3, p.size
|
44
|
+
end
|
45
|
+
|
46
|
+
def test_bloat
|
47
|
+
p=nil
|
48
|
+
assert_nothing_raised{
|
49
|
+
p=Population.new(BloatTest,20).evolve_silent(50)
|
50
|
+
}
|
51
|
+
p.each{|s| assert s.genes.is_a?(Array) }
|
52
|
+
assert p.max.size > 20, "tree bloat test failed, please rerun to check if this isn't just extremely bad luck"
|
53
|
+
end
|
54
|
+
|
55
|
+
|
56
|
+
|
57
|
+
end
|