charlie 0.5.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 (54) hide show
  1. data/History.txt +3 -0
  2. data/Manifest.txt +53 -0
  3. data/README.txt +90 -0
  4. data/Rakefile +43 -0
  5. data/TODO.txt +28 -0
  6. data/data/BENCHMARK +53 -0
  7. data/data/CROSSOVER +49 -0
  8. data/data/GENOTYPE +49 -0
  9. data/data/MUTATION +43 -0
  10. data/data/SELECTION +48 -0
  11. data/data/template.html +34 -0
  12. data/examples/bit.rb +10 -0
  13. data/examples/function_opt_2peak.rb +24 -0
  14. data/examples/function_opt_sombero.rb +38 -0
  15. data/examples/gladiatorial_simple.rb +17 -0
  16. data/examples/gladiatorial_sunburn.rb +89 -0
  17. data/examples/gridwalk.rb +29 -0
  18. data/examples/output/flattened_sombero.html +6400 -0
  19. data/examples/output/flattened_sombero2_.html +3576 -0
  20. data/examples/output/fopt1_dblopt.html +2160 -0
  21. data/examples/output/hill10.html +5816 -0
  22. data/examples/output/hill2.csv +24 -0
  23. data/examples/output/hill2.html +384 -0
  24. data/examples/output/royalroad1_report.html +1076 -0
  25. data/examples/output/royalroad2_report.html +1076 -0
  26. data/examples/output/royalroadquick_report.html +504 -0
  27. data/examples/output/tsp.html +632 -0
  28. data/examples/output/weasel1_report.html +1076 -0
  29. data/examples/output/weasel2_report.html +240 -0
  30. data/examples/royalroad.rb +26 -0
  31. data/examples/royalroad2.rb +18 -0
  32. data/examples/simple_climb_hill2.rb +47 -0
  33. data/examples/tsp.rb +35 -0
  34. data/examples/weasel.rb +36 -0
  35. data/lib/charlie.rb +35 -0
  36. data/lib/charlie/crossover.rb +49 -0
  37. data/lib/charlie/etc/minireport.rb +45 -0
  38. data/lib/charlie/etc/monkey.rb +136 -0
  39. data/lib/charlie/genotype.rb +45 -0
  40. data/lib/charlie/list/list_crossover.rb +30 -0
  41. data/lib/charlie/list/list_genotype.rb +53 -0
  42. data/lib/charlie/list/list_mutate.rb +75 -0
  43. data/lib/charlie/mutate.rb +25 -0
  44. data/lib/charlie/permutation/permutation.rb +47 -0
  45. data/lib/charlie/population.rb +156 -0
  46. data/lib/charlie/selection.rb +162 -0
  47. data/test/t_common.rb +32 -0
  48. data/test/test_basic.rb +32 -0
  49. data/test/test_benchmark.rb +56 -0
  50. data/test/test_cross.rb +28 -0
  51. data/test/test_mutator.rb +44 -0
  52. data/test/test_permutation.rb +23 -0
  53. data/test/test_sel.rb +39 -0
  54. metadata +115 -0
@@ -0,0 +1,156 @@
1
+ # Contains the Population class
2
+
3
+
4
+ # The population class represents an array of genotypes.
5
+ # Create an instance of this, and call one of the evolve functions to run the genetic algorithm.
6
+ class Population
7
+ attr_reader :size, :population
8
+ def initialize(genotype_class,population_size=20)
9
+ @size = population_size
10
+ @genotype_class = genotype_class
11
+ @population = Array.new(population_size){ genotype_class.new }
12
+ end
13
+
14
+ # Runs the genetic algorithm with some stats on the console. Returns the population sorted by fitness.
15
+ def evolve_on_console(generations=100)
16
+ puts "Generation\tBest Fitness\t\tBest Individual"
17
+ generations.times{|g|
18
+ best = @population.max
19
+ puts "#{g}\t\t#{best.fitness}\t\t#{best.to_s}"
20
+ @population = @genotype_class.next_generation(@population){|*parents|
21
+ ch = [*@genotype_class.cross(*parents)]
22
+ ch.each{|c| c.mutate! }
23
+ ch
24
+ }
25
+ }
26
+ @population = @population.sort_by{|x|x.fitness}
27
+ puts "Finished: Best fitness = #{@population[-1].fitness}"
28
+ @population
29
+ end
30
+
31
+ # Runs the genetic algorithm without any output. Returns the population sorted by fitness (unsorted for co-evolution).
32
+ def evolve_silent(generations=100)
33
+ generations.times{|g|
34
+ @population = @genotype_class.next_generation(@population){|*parents|
35
+ ch = [*@genotype_class.cross(*parents)]
36
+ ch.each{|c| c.mutate! }
37
+ ch
38
+ }
39
+ }
40
+ @population.sort_by{|x|x.fitness} rescue @population
41
+ end
42
+
43
+ alias :evolve :evolve_on_console
44
+
45
+ class << self
46
+
47
+ # This method generates reports comparing several selection/crossover/mutation methods. Check the examples directory for several examples. See the BENCHMARK documentation file for more information.
48
+ def benchmark(genotype_class, html_outfile='report.html', csv_outfile=nil, &b)
49
+ start = Time.now
50
+
51
+ d = StrategiesDSL.new; d.instance_eval(&b)
52
+ tries = d.get_tests
53
+ gens = d.generations
54
+ pop_size = d.population_size
55
+ rep = d.repeat
56
+ puts "#{tries.size} Total tests:"; done=0
57
+
58
+ data = tries.map{|s,c,m|
59
+ print "\nRunning test #{done+=1}/#{tries.size} : #{s} / #{c} / #{m}\t"
60
+ gclass = Class.new(genotype_class) { use s,c,m }
61
+ start_try = Time.now
62
+ bestf = (0...rep).map{
63
+ print '.'; $stdout.flush
64
+ Population.new(gclass,pop_size).evolve_silent(gens)[-1].fitness # todo: generations
65
+ }
66
+ [s,c,m, (Time.now-start_try) / rep, bestf]
67
+ }
68
+
69
+ # Generate HTML
70
+ html_tables = <<INFO
71
+ <h1>Information</h1>\n
72
+ <table>
73
+ <tr><td>Genotype class</td><td>#{genotype_class}</td></tr>
74
+ <tr><td>Population size</td><td>#{pop_size}</td></tr>
75
+ <tr><td># of generations per run</td><td>#{gens}</td></tr>
76
+ <tr><td>Number of tests </td><td>#{tries.size}</td></tr>
77
+ <tr><td>Tests repeated </td><td>#{rep} times</td></tr>
78
+ <tr><td>Number of runs </td><td>#{tries.size*rep}</td></tr>
79
+ <tr><td>Total number of generations </td><td>#{tries.size*rep*gens}</td></tr>
80
+ <tr><td>Total time</td><td>#{'%.2f' % (Time.now-start)} seconds</td></tr>
81
+ </table
82
+ INFO
83
+
84
+ # - combined stats
85
+ joined = data.map(&:last).flatten
86
+ avg_time = data.map{|r| r[-2] }.average
87
+
88
+ html_tables << "<h1>Stats for all</h1>"
89
+ html_tables << [["All"] + (joined.stats << avg_time).map{|f| '%.5f' % f } ].to_table(%w[. min max avg stddev avg-time]).to_html
90
+ # - stats grouped by selection, crossover, mutation methods
91
+ ["selection","crossover","mutation"].each_with_index{|title,i|
92
+ data_grp = data.group_by{|x|x[i]}.map{|k,a|
93
+ joined = a.map(&:last).flatten
94
+ avg_time = a.map{|r| r[-2] }.average
95
+ [k] + (joined.stats << avg_time).map{|f| '%.5f' % f }
96
+ }.sort_by{|row| -row[-3].to_f } # sort by average
97
+ html_tables << "<h1>Stats for #{title}</h1>"
98
+ html_tables << data_grp.to_table([title]+%w[min max avg stddev avg-time]).to_html
99
+ }
100
+ # - detailed stats
101
+
102
+ tbl = data.map{|s,c,m,t,a| [s,c,m] + (a.stats << t).map{|f| '%.5f' % f } }.sort_by{|row| -row[-3].to_f }.to_table(%w[selection crossover mutation min max avg stddev avg-time])
103
+ html_tables << '<h1>Raw Stats</h1>' << tbl.to_html
104
+ # write HTML stats
105
+ File.open(html_outfile,'w'){|f| f << File.read(File.dirname(__FILE__)+"/../../data/template.html").sub('{{CONTENT}}',html_tables) } if html_outfile
106
+
107
+ # write csv file, contains raw stats
108
+ File.open(csv_outfile,'w'){|f| f << data.map{|r|r[0..-2] << r[-1].join(', ') }.to_table.to_csv } if csv_outfile
109
+
110
+ puts '',tbl.to_s
111
+ data
112
+ end
113
+ end
114
+
115
+ end
116
+
117
+
118
+ # Used in the Population#benchmark function.
119
+ class StrategiesDSL
120
+ # Number of generations run in each test.
121
+ attr_accessor :generations
122
+ # Population size used.
123
+ attr_accessor :population_size
124
+ # Number of times all tests are run. Default=10. Increase for more accuracy on the benchmark.
125
+ attr_accessor:repeat
126
+
127
+ def initialize
128
+ @repeat = 10
129
+ @population_size = 20
130
+ @generations = 50
131
+ end
132
+
133
+ # Pass several modules to this to test these selection methods.
134
+ def selection(*s); @s=s; end
135
+ # Pass several modules to this to test these crossover methods.
136
+ def crossover(*c); @c=c; end
137
+ # Pass several modules to this to test these mutation methods.
138
+ def mutator(*m) ; @m=m; end
139
+ alias :mutation :mutator
140
+ # Get all the tests. Basically a cartesian product of all selection, crossover and mutation methods.
141
+ def get_tests
142
+ t = []
143
+ raise 'No selection modules defined' unless @s
144
+ raise 'No crossover modules defined' unless @c
145
+ raise 'No mutation modules defined' unless @m
146
+ @s.each{|s|
147
+ @c.each{|c|
148
+ @m.each{|m|
149
+ t << [s,c,m]
150
+ }
151
+ }
152
+ }
153
+ t
154
+ end
155
+ end
156
+
@@ -0,0 +1,162 @@
1
+ # Contains the selection methods.
2
+
3
+
4
+ # Random selection. May be useful in benchmarks, but otherwise useless.
5
+ module RandomSelection
6
+ def next_generation(population)
7
+ new_pop = []
8
+ new_pop += yield(population.at_rand,population.at_rand) while new_pop.size < population.size
9
+ new_pop.pop until new_pop.size == population.size
10
+ new_pop
11
+ end
12
+ end
13
+
14
+ # Truncation selection takes the +best+ invididuals with the highest fitness
15
+ # and crosses them randomly to replace all the others.
16
+ # +best+ can be an float < 1 (fraction of population size), or a number >=1 (number of individuals)
17
+ def TruncationSelection(best=0.3)
18
+ Module::new{
19
+ @@best = best
20
+ def next_generation(population)
21
+ k = if @@best >= 1 # best is an integer >= 1 -> select this much
22
+ @@best.round
23
+ else # best is a float < 1 -> select this percentage
24
+ [1,(@@best * population.size).round].max
25
+ end
26
+ n_new = population.size - k
27
+
28
+ population = population.sort_by(&:fitness)
29
+ best = population[-k..-1]
30
+ new_pop = []
31
+ new_pop += yield(best.at_rand,best.at_rand) while new_pop.size < n_new
32
+ new_pop.pop until new_pop.size == n_new
33
+
34
+ new_pop + best
35
+ end
36
+ self.name = "TruncationSelection(#{best})"
37
+ }
38
+ end
39
+
40
+ # This selection algorithm is basically randomized hill climbing.
41
+ BestOnlySelection = TruncationSelection(1)
42
+
43
+ # Roulette selection without replacement. Probability of individual i being selected is fitness(i) / sum fitness(1..population size)
44
+ module RouletteSelection
45
+ def next_generation(population)
46
+ partial_sum = []
47
+ sum = population.inject(0){|a,b| cs = a + b.fitness; partial_sum << cs; cs }
48
+
49
+ new_pop = []
50
+ while new_pop.size < population.size
51
+ i1,i2 = [0,0].map{
52
+ r = rand * sum
53
+ partial_sum.index partial_sum.find{|x| x > r }
54
+ } until i1!=i2 # no replacement, except when this fails
55
+ new_pop += yield(population[i1],population[i2])
56
+ end
57
+ new_pop.pop until new_pop.size == population.size
58
+ new_pop
59
+ end
60
+
61
+ end
62
+
63
+ # Scaled Roulette selection without replacement.
64
+ # Sorts by fitness, scales fitness by the values the given block yields for its index, then applies Roulette selection to the resulting fitness values.
65
+ # Default is Rank selection: {|x| x+1 }
66
+ def ScaledRouletteSelection(&block)
67
+ Module.new{
68
+ block = proc{|x|x+1} if block.nil?
69
+ @@block = block
70
+ @@index = nil
71
+ @@index_size = 0 # population size used to generate index
72
+
73
+ def next_generation(population)
74
+
75
+ if @@index_size != population.size # build index, cache for constant population size
76
+ @@index_size = population.size
77
+ @@index = []
78
+ (0...population.size).map(&@@block).each_with_index{|e,i| @@index += Array.new(e.round,i) }
79
+ end
80
+
81
+ population = population.sort_by(&:fitness)
82
+ new_pop = []
83
+ while new_pop.size < population.size
84
+ i1,i2 = @@index.at_rand, @@index.at_rand until i1!=i2 # no replacement
85
+ new_pop += yield(population[i1],population[i2])
86
+ end
87
+ new_pop.pop until new_pop.size == population.size
88
+ new_pop
89
+ end
90
+
91
+ self.name= "ScaledRouletteSelection[#{(0..3).map(&block).map(&:to_s).join(',')},...]"
92
+ }
93
+ end
94
+
95
+ ScaledRouletteSelection = ScaledRouletteSelection()
96
+
97
+ # Generates a selection module with elitism from a normal selection module.
98
+ # Elitism is saving the best +elite_n+ individuals each generation, to ensure the best solutions are never lost.
99
+ def Elitism(sel_module,elite_n=1)
100
+ Module.new{
101
+ include sel_module
102
+ @@elite_n = elite_n
103
+ def next_generation(population)
104
+ population = population.sort_by(&:fitness)
105
+ best = population[-@@elite_n..-1]
106
+ population = super
107
+ # reset old best elite_n, but don't overwrite better ones
108
+ population[-@@elite_n..-1] = best.zip_with(population[-@@elite_n..-1]){|old,new| [old,new].max }
109
+ population
110
+ end
111
+ self.name= "Elitism(#{sel_module.to_s},#{elite_n})"
112
+ }
113
+ end
114
+
115
+
116
+
117
+ # Tournament selection.
118
+ # Default: select the 2 individuals with the highest fitness out of a random population with size group_size
119
+ # and replaces the others with offspring of these 2.
120
+ # Does this n_times. n_times==nil takes population size / (group_size-2) , i.e. about the same number of new individuals as roulette selection etc.
121
+ def TournamentSelection(group_size=4,n_times=nil) # TODO: maybe expand for probabilistic selection(?) Or in a new module
122
+ Module::new{
123
+ @@group_size = group_size
124
+ @@n_times = n_times
125
+ def next_generation(population)
126
+ @@n_times ||= population.size / (@@group_size-2)
127
+ @@n_times.times{
128
+ ix=[]
129
+ begin
130
+ ix = (0...@@group_size).map{ population.rand_index }
131
+ end while ix.uniq.size != @@group_size
132
+ ix=ix.sort_by{|i| population[i].fitness }
133
+ p1,p2 = population[ix[-1]],population[ix[-2]]
134
+ nw = [];
135
+ nw += yield(p1,p2) while nw.size < @@group_size-2
136
+ (@@group_size-2).times{|i| population[ix[i]] = nw[i] }
137
+ }
138
+ population
139
+ end
140
+ self.name= "TournamentSelection(#{group_size},#{n_times.inspect})"
141
+ }
142
+ end
143
+ TournamentSelection = TournamentSelection()
144
+
145
+
146
+
147
+ # Direct competition (gladiatorial) selection
148
+ module GladiatorialSelection
149
+ def next_generation(population)
150
+ (population.size/2).times{
151
+ ix=[0,0,0,0]
152
+ ix=(0..3).map{population.rand_index} while ix.uniq.size < 4
153
+ ix[0],ix[1] = ix[1],ix[0] unless population[ix[0]].fight(population[ix[1]]) # 0 and 2 hold winners
154
+ ix[2],ix[3] = ix[3],ix[2] unless population[ix[2]].fight(population[ix[3]])
155
+ nw = [];
156
+ nw += yield(population[ix[0]],population[ix[2]]) while nw.size < 2
157
+ population[ix[1]],population[ix[3]] = *nw
158
+ }
159
+ population
160
+ end
161
+ end
162
+
data/test/t_common.rb ADDED
@@ -0,0 +1,32 @@
1
+
2
+
3
+
4
+
5
+ require File.dirname(__FILE__) + '/../lib/charlie' unless Object.const_defined?('Charlie')
6
+ require 'test/unit'
7
+
8
+ $crs_mth = [NullCrossover, SinglePointCrossover, UniformCrossover]
9
+
10
+
11
+ class TestProblem < FloatListGenotype(2,0..1)
12
+ def fitness
13
+ genes.map{|x| [x,10].min }.sum
14
+ end
15
+ end
16
+
17
+ def TestClass(mod,klass=TestProblem)
18
+ Class.new(klass){use mod}
19
+ end
20
+
21
+ class RoyalRoad < BitStringGenotype(64)
22
+ def fitness
23
+ genes.enum_slice(8).find_all{|e| e.all?{|x|x==1} }.size
24
+ end
25
+ end
26
+
27
+ class StringA < StringGenotype(20,'a'..'d')
28
+ def fitness
29
+ genes.find_all{|c| c=='a'}.size
30
+ end
31
+ use UniformCrossover
32
+ end
@@ -0,0 +1,32 @@
1
+ require 't_common'
2
+
3
+ class BasicTest < 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
17
+ p = Population.new(TestProblem,10)
18
+ best3 = p.evolve_silent(3)[-1].fitness
19
+ best33 = p.evolve_silent(30)[-1].fitness
20
+ # Not true for all problems, but in this case the probability of failure is negligible
21
+ assert( best33 > best3, "test_continue failed. Please rerun test to make sure this isn't just extremely bad luck.")
22
+ end
23
+
24
+ def test_testclass
25
+ assert_nothing_raised{
26
+ Population.new(TestClass(Module::new),10).evolve_silent(10)
27
+ Population.new(TestClass(UniformCrossover),10).evolve_silent(10)
28
+ Population.new(TestClass(ListMutator(:single_point,:gaussian) ),10).evolve_silent(10)
29
+ Population.new(TestClass(TournamentSelection(4)),10).evolve_silent(10)
30
+ }
31
+ end
32
+ end
@@ -0,0 +1,56 @@
1
+ require 't_common'
2
+
3
+ CDIR = File.dirname(__FILE__)
4
+
5
+ class BMTest < Test::Unit::TestCase
6
+ def setup
7
+ Dir[CDIR+'/output/*'].each{|f| File.unlink f}
8
+ end
9
+
10
+ def test_basic_bm
11
+ assert_nothing_raised {
12
+ Population.benchmark(RoyalRoad,CDIR+'/output/test_benchmark.html'){
13
+ selection TruncationSelection(0.2), Elitism(ScaledRouletteSelection(),1)
14
+ crossover NullCrossover, UniformCrossover
15
+ mutator ListMutator(:single_point,:flip), ListMutator(:expected_n[4],:flip)
16
+ self.repeat = 5
17
+ self.generations = 40
18
+ self.population_size = 10
19
+ }
20
+ }
21
+ assert( File.read(CDIR+'/output/test_benchmark.html').size > 1000, 'Output file not generated.')
22
+ end
23
+
24
+
25
+ def test_csv
26
+ assert_nothing_raised {
27
+ Population.benchmark(StringA,nil,CDIR+'/output/test_benchmark.csv'){
28
+ selection TruncationSelection(0.2)
29
+ crossover NullCrossover, Module.new{self.name='default'}
30
+ mutation ListMutator(:expected_n[1],:replace[*'a'..'d']), NullMutator
31
+ }
32
+ }
33
+ assert( File.readlines(CDIR+'/output/test_benchmark.csv').size == 4, 'Output file not generated.')
34
+ end
35
+
36
+ def test_raise
37
+ assert_raises(RuntimeError) {
38
+ Population.benchmark(StringA,nil,nil){
39
+ selection TruncationSelection(0.2)
40
+ # no crossover, etc specified.
41
+ }
42
+ }
43
+ assert_raises(RuntimeError) {
44
+ Population.benchmark(StringA,nil,nil){
45
+ crossover NullCrossover
46
+ }
47
+ }
48
+ assert_raises(RuntimeError) {
49
+ Population.benchmark(StringA,nil,nil){
50
+ mutation Module.new
51
+ selection Module.new
52
+ }
53
+ }
54
+ end
55
+
56
+ end
@@ -0,0 +1,28 @@
1
+ require 't_common'
2
+
3
+ class TestCrossover < Test::Unit::TestCase
4
+ def test_cross
5
+ $crs_mth.each{|s|
6
+ klass = TestClass(s)
7
+ assert_nothing_raised{ Population.new(klass,10).evolve_silent(10) }
8
+ }
9
+ end
10
+
11
+ def test_singlechild
12
+ $crs_mth.map{|c| SingleChild(c) }.each{|s|
13
+ klass = TestClass(s)
14
+ assert_nothing_raised{ Population.new(klass,10).evolve_silent(10) }
15
+ }
16
+ end
17
+
18
+ def test_pcross
19
+ $crs_mth.map{|c| PCross(0.5,c) }.each{|s|
20
+ klass = TestClass(s)
21
+ assert_nothing_raised{ Population.new(klass,10).evolve_silent(10) }
22
+ }
23
+ $crs_mth.map{|c| PCross(0.5,c,$crs_mth[-2]) }.each{|s|
24
+ klass = TestClass(s)
25
+ assert_nothing_raised{ Population.new(klass,10).evolve_silent(10) }
26
+ }
27
+ end
28
+ end