charlie 0.6.0 → 0.7.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 (66) hide show
  1. data/History.txt +14 -0
  2. data/Manifest.txt +13 -22
  3. data/README.txt +3 -3
  4. data/Rakefile +1 -1
  5. data/TODO.txt +11 -21
  6. data/data/BENCHMARK +25 -23
  7. data/data/CROSSOVER +5 -1
  8. data/data/GENOTYPE +6 -6
  9. data/data/MUTATION +19 -7
  10. data/data/SELECTION +2 -1
  11. data/data/template.html +2 -1
  12. data/examples/EXAMPLES_README.txt +70 -0
  13. data/examples/bitstring.rb +72 -0
  14. data/examples/{gladiatorial_sunburn.rb → coevolution.rb} +80 -22
  15. data/examples/function_optimization.rb +113 -0
  16. data/examples/output/{royalroad1_report.html → bitstring_royalroad.html} +822 -655
  17. data/examples/output/function_optimization_sombrero.html +2289 -0
  18. data/examples/output/function_optimization_twopeak.csv +210 -0
  19. data/examples/output/function_optimization_twopeak.html +2477 -0
  20. data/examples/output/string_weasel.html +513 -0
  21. data/examples/output/tsp.html +633 -882
  22. data/examples/{money.rb → permutation.rb} +20 -8
  23. data/examples/string.rb +98 -0
  24. data/examples/tree.rb +37 -12
  25. data/examples/tsp.rb +34 -22
  26. data/lib/charlie.rb +5 -1
  27. data/lib/charlie/1.9fixes.rb +46 -0
  28. data/lib/charlie/crossover.rb +31 -14
  29. data/lib/charlie/etc/minireport.rb +5 -4
  30. data/lib/charlie/etc/monkey.rb +11 -8
  31. data/lib/charlie/gabenchmark.rb +230 -0
  32. data/lib/charlie/genotype.rb +4 -0
  33. data/lib/charlie/list/list_crossover.rb +25 -5
  34. data/lib/charlie/mutate.rb +34 -7
  35. data/lib/charlie/permutation/permutation.rb +34 -6
  36. data/lib/charlie/population.rb +12 -122
  37. data/lib/charlie/selection.rb +1 -0
  38. data/lib/charlie/tree/tree.rb +179 -17
  39. data/test/t_common.rb +1 -1
  40. data/test/test_benchmark.rb +19 -5
  41. data/test/test_cross.rb +23 -1
  42. data/test/test_evolve.rb +14 -1
  43. data/test/test_mutator.rb +28 -2
  44. data/test/test_permutation.rb +23 -1
  45. data/test/test_sel.rb +3 -1
  46. data/test/test_tree.rb +63 -1
  47. metadata +17 -25
  48. data/examples/bit.rb +0 -10
  49. data/examples/function_opt_2peak.rb +0 -24
  50. data/examples/function_opt_sombero.rb +0 -38
  51. data/examples/gladiatorial_simple.rb +0 -17
  52. data/examples/gridwalk.rb +0 -29
  53. data/examples/output/flattened_sombero.html +0 -6400
  54. data/examples/output/flattened_sombero2_.html +0 -3576
  55. data/examples/output/fopt1_dblopt.html +0 -2160
  56. data/examples/output/hill10.html +0 -5816
  57. data/examples/output/hill2.csv +0 -24
  58. data/examples/output/hill2.html +0 -384
  59. data/examples/output/royalroad2_report.html +0 -1076
  60. data/examples/output/royalroadquick_report.html +0 -504
  61. data/examples/output/weasel1_report.html +0 -1076
  62. data/examples/output/weasel2_report.html +0 -240
  63. data/examples/royalroad.rb +0 -26
  64. data/examples/royalroad2.rb +0 -18
  65. data/examples/simple_climb_hill2.rb +0 -47
  66. data/examples/weasel.rb +0 -36
@@ -6,7 +6,7 @@
6
6
  class Population
7
7
  DEFAULT_MAX_GENS = 100
8
8
 
9
- attr_reader :size, :population
9
+ attr_reader :size, :population, :genotype_class
10
10
  def initialize(genotype_class,population_size=20)
11
11
  @size = population_size
12
12
  @genotype_class = genotype_class
@@ -46,12 +46,12 @@ class Population
46
46
  end
47
47
  alias :evolve :evolve_on_console
48
48
 
49
- # breaks if the block (which is passed the population each generation) returns true.
49
+ # breaks if the block (which is passed the population each "check_every" generations) returns true.
50
50
  # returns an array [population, generations needed]. generations needed==nil for no convergence.
51
- def evolve_until_population(generations=DEFAULT_MAX_GENS)
51
+ def evolve_until_population(generations=DEFAULT_MAX_GENS,check_every=10)
52
52
  tot_gens = nil
53
53
  evolve_block(generations) {|p,g|
54
- if yield(p)
54
+ if (g % check_every).zero? && yield(p)
55
55
  tot_gens = g
56
56
  break
57
57
  end
@@ -59,12 +59,12 @@ class Population
59
59
  [@population, tot_gens]
60
60
  end
61
61
 
62
- # breaks if the block (which is passed the best individual each generation) returns true.
62
+ # breaks if the block (which is passed the best individual each "check_every" generations) returns true.
63
63
  # returns an array [population, generations needed]. generations needed==nil for no convergence.
64
- def evolve_until_best(generations=DEFAULT_MAX_GENS)
64
+ def evolve_until_best(generations=DEFAULT_MAX_GENS,check_every=10)
65
65
  tot_gens = nil
66
66
  evolve_block(generations) {|p,g|
67
- if yield(p.max)
67
+ if (g % check_every).zero? && yield(p.max)
68
68
  tot_gens = g
69
69
  break
70
70
  end
@@ -74,121 +74,11 @@ class Population
74
74
  alias :evolve_until :evolve_until_best
75
75
 
76
76
 
77
- class << self
77
+ # accessors for population
78
+ [:[],:max,:min].each{|m|
79
+ define_method(m){|*args| population.send(m,*args) }
80
+ }
81
+ alias :best :max
78
82
 
79
- # 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.
80
- def benchmark(genotype_class, html_outfile='report.html', csv_outfile=nil, &b)
81
- start = Time.now
82
-
83
- d = StrategiesDSL.new; d.instance_eval(&b)
84
- tries = d.get_tests
85
- gens = d.generations
86
- pop_size = d.population_size
87
- rep = d.repeat
88
- puts "#{tries.size} Total tests:"; done=0
89
-
90
- data = tries.map{|s,c,m|
91
- print "\nRunning test #{done+=1}/#{tries.size} : #{s} / #{c} / #{m}\t"
92
- gclass = Class.new(genotype_class) { use s,c,m }
93
- start_try = Time.now
94
- bestf = (0...rep).map{
95
- print '.'; $stdout.flush
96
- Population.new(gclass,pop_size).evolve_silent(gens)[-1].fitness # todo: generations
97
- }
98
- [s,c,m, (Time.now-start_try) / rep, bestf]
99
- }
100
-
101
- # Generate HTML
102
- html_tables = <<INFO
103
- <h1>Information</h1>\n
104
- <table>
105
- <tr><td>Genotype class</td><td>#{genotype_class}</td></tr>
106
- <tr><td>Population size</td><td>#{pop_size}</td></tr>
107
- <tr><td># of generations per run</td><td>#{gens}</td></tr>
108
- <tr><td>Number of tests </td><td>#{tries.size}</td></tr>
109
- <tr><td>Tests repeated </td><td>#{rep} times</td></tr>
110
- <tr><td>Number of runs </td><td>#{tries.size*rep}</td></tr>
111
- <tr><td>Total number of generations </td><td>#{tries.size*rep*gens}</td></tr>
112
- <tr><td>Total time</td><td>#{'%.2f' % (Time.now-start)} seconds</td></tr>
113
- </table
114
- INFO
115
-
116
- # - combined stats
117
- joined = data.map(&:last).flatten
118
- avg_time = data.map{|r| r[-2] }.average
119
-
120
- html_tables << "<h1>Stats for all</h1>"
121
- html_tables << [["All"] + (joined.stats << avg_time).map{|f| '%.5f' % f } ].to_table(%w[. min max avg stddev avg-time]).to_html
122
- # - stats grouped by selection, crossover, mutation methods
123
- ["selection","crossover","mutation"].each_with_index{|title,i|
124
- data_grp = data.group_by{|x|x[i]}.map{|k,a|
125
- joined = a.map(&:last).flatten
126
- avg_time = a.map{|r| r[-2] }.average
127
- [k] + (joined.stats << avg_time).map{|f| '%.5f' % f }
128
- }.sort_by{|row| -row[-3].to_f } # sort by average
129
- html_tables << "<h1>Stats for #{title}</h1>"
130
- html_tables << data_grp.to_table([title]+%w[min max avg stddev avg-time]).to_html
131
- }
132
- # - detailed stats
133
-
134
- 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])
135
- html_tables << '<h1>Raw Stats</h1>' << tbl.to_html
136
- # write HTML stats
137
- File.open(html_outfile,'w'){|f| f << File.read(File.dirname(__FILE__)+"/../../data/template.html").sub('{{CONTENT}}',html_tables) } if html_outfile
138
-
139
- # write csv file, contains raw stats
140
- File.open(csv_outfile,'w'){|f| f << data.map{|r|r[0..2] << r[-1].join(', ') }.to_table.to_csv } if csv_outfile
141
-
142
- puts '',tbl.to_s
143
- data
144
- end
145
- end
146
-
147
- end
148
-
149
-
150
- # Used in the Population#benchmark function.
151
- class StrategiesDSL
152
- # Number of generations run in each test.
153
- attr_accessor :generations
154
- # Population size used.
155
- attr_accessor :population_size
156
- # Number of times all tests are run. Default=10. Increase for more accuracy on the benchmark.
157
- attr_accessor:repeat
158
-
159
- def initialize
160
- @repeat = 10
161
- @population_size = 20
162
- @generations = 50
163
- end
164
-
165
- # Pass several modules to this to test these selection methods.
166
- def selection(*s); @s=s; end
167
- # Pass several modules to this to test these crossover methods.
168
- def crossover(*c); @c=c; end
169
- # Pass several modules to this to test these mutation methods.
170
- def mutator(*m) ; @m=m; end
171
- alias :mutation :mutator
172
- # Get all the tests. Basically a cartesian product of all selection, crossover and mutation methods.
173
- def get_tests
174
- t = []
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
184
- @s.each{|s|
185
- @c.each{|c|
186
- @m.each{|m|
187
- t << [s,c,m]
188
- }
189
- }
190
- }
191
- t
192
- end
193
83
  end
194
84
 
@@ -94,6 +94,7 @@ end
94
94
 
95
95
  ScaledRouletteSelection = ScaledRouletteSelection()
96
96
 
97
+
97
98
  # Generates a selection module with elitism from a normal selection module.
98
99
  # Elitism is saving the best +elite_n+ individuals each generation, to ensure the best solutions are never lost.
99
100
  def Elitism(sel_module,elite_n=1)
@@ -18,7 +18,15 @@ module GPTreeHelper
18
18
  end
19
19
  end
20
20
 
21
- def all_subtrees(t)
21
+ def tree_depth(t)
22
+ if t.first==:term
23
+ 0
24
+ else
25
+ 1 + t[1..-1].map{|st| tree_depth(st) }.max
26
+ end
27
+ end
28
+
29
+ def all_subtrees(t=@genes)
22
30
  if t.first==:term
23
31
  [t]
24
32
  else
@@ -30,6 +38,18 @@ module GPTreeHelper
30
38
  all_subtrees(t).at_rand
31
39
  end
32
40
 
41
+ def all_terminals(t=@genes)
42
+ if t.first==:term
43
+ [t]
44
+ else
45
+ t[1..-1].map{|st| all_terminals(st) }.inject{|a,b|a+b}
46
+ end
47
+ end
48
+
49
+ def random_terminal(t=@genes)
50
+ all_terminals(t).at_rand
51
+ end
52
+
33
53
  def eval_tree(tree,values_hash)
34
54
  if tree.first == :term
35
55
  termval = tree[1]
@@ -47,17 +67,21 @@ module GPTreeHelper
47
67
  end
48
68
 
49
69
  # Tree genotype, for genetic programming etc.
50
- # * Pass arrays of terminals/binary operators and unary operators to this function to generate a class.
70
+ # * Pass arrays of terminals/unary operators/binary operators to this function to generate a class.
51
71
  # * 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
72
  # * 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)
73
+ def TreeGenotype(terminals, unary_ops, binary_ops, init_depth = 3, init_type = :half)
74
+ unary_ops = nil if unary_ops.empty?
54
75
  Class.new(Genotype) {
55
- @@terms = terminals
56
- @@binary_ops = binary_ops
57
- @@unary_ops = unary_ops
76
+
77
+ define_method(:unary_ops) {unary_ops }
78
+ define_method(:binary_ops) {binary_ops}
79
+ define_method(:terminals) {terminals }
80
+ define_method(:init_depth) {init_depth}
81
+ define_method(:init_type) {init_type }
58
82
 
59
83
  def initialize
60
- self.genes = random_tree(3)
84
+ self.genes = generate_random_tree(init_depth,init_type)
61
85
  end
62
86
 
63
87
  def genes=(g)
@@ -73,15 +97,48 @@ def TreeGenotype(terminals = [proc{rand},:x], binary_ops = [:+,:*,:-,:/], unary_
73
97
  tree_size(@genes)
74
98
  end
75
99
 
76
- def random_tree(d)
77
- if d == 0 || rand < 1.0 / 3
78
- e = @@terms.at_rand
100
+ def depth
101
+ tree_depth(@genes)
102
+ end
103
+
104
+
105
+ # Generates a random tree.
106
+ # * type = grow uses generate_random_tree_grow
107
+ # * type = full uses generate_random_tree_full
108
+ # * type = half uses one of them, with 50% probability each.
109
+ def generate_random_tree(depth, type=:half)
110
+ if type==:full || ( type == :half && rand < 0.5 )
111
+ generate_random_tree_full(depth)
112
+ else
113
+ generate_random_tree_grow(depth,true)
114
+ end
115
+ end
116
+
117
+ # Generates a random tree of a certain maximum depth.
118
+ # * <tt>no_term=true</tt> makes sure the root node is a function.
119
+ def generate_random_tree_grow(depth,no_term=nil)
120
+ if depth.zero? || (rand(3).zero? && !no_term)
121
+ e = terminals.at_rand
79
122
  [:term, e.is_a?(Proc) ? e.call : e]
80
123
  else
81
- if @@unary_ops.nil? || rand < 0.5
82
- [@@binary_ops.at_rand,random_tree(d-1),random_tree(d-1)]
124
+ if unary_ops.nil? || rand(2).zero?
125
+ [binary_ops.at_rand,generate_random_tree_grow(depth-1),generate_random_tree_grow(depth-1)]
83
126
  else
84
- [@@unary_ops.at_rand,random_tree(d-1)]
127
+ [unary_ops.at_rand,generate_random_tree_grow(depth-1)]
128
+ end
129
+ end
130
+ end
131
+
132
+ # Generates a random tree, with all terminals at 'depth'.
133
+ def generate_random_tree_full(depth)
134
+ if depth.zero?
135
+ e = terminals.at_rand
136
+ [:term, e.is_a?(Proc) ? e.call : e]
137
+ else
138
+ if unary_ops.nil? || rand(2).zero?
139
+ [binary_ops.at_rand,generate_random_tree_full(depth-1),generate_random_tree_full(depth-1)]
140
+ else
141
+ [unary_ops.at_rand,generate_random_tree_full(depth-1)]
85
142
  end
86
143
  end
87
144
  end
@@ -94,7 +151,7 @@ def TreeGenotype(terminals = [proc{rand},:x], binary_ops = [:+,:*,:-,:/], unary_
94
151
  @genes.inspect
95
152
  end
96
153
 
97
- use PCross(0.7,TreeCrossover), PMutate(0.5,TreeMutator) # TODO: test what probabilities are best
154
+ use PCross(0.7,TreeCrossover), PMutate(0.5,TreeReplaceMutator) # TODO: test what options are best -- benchmark show that these are ok for simple settings.
98
155
 
99
156
  # make helper functions available at both class and instance level
100
157
  include GPTreeHelper
@@ -118,11 +175,116 @@ module TreeCrossover
118
175
  end
119
176
  end
120
177
 
121
- # TreeMutator replaces a randomly chosen subtree with a new, randomly generated, subtree of depth <= 2.
122
- module TreeMutator
178
+ # TreeReplaceMutator replaces a randomly chosen subtree with a new, randomly generated, subtree.
179
+ # * depth and type are arguments for TreeGenotype#generate_random_tree
180
+ # * depth == [1,2,3,..] or depth==(1..3) uses one of the elements in the range for the depth.
181
+ # * depth == :same to use the depth of the replaced subtree.
182
+ # * depth == :same[min,max] for depth of the replaced subtree plus a random offset between min and max.
183
+ def TreeReplaceMutator(depth=2,type=:half)
184
+ Module.new{
185
+ self.name = "TreeReplaceMutator(#{depth.inspect},#{type.inspect})"
186
+ if depth.is_a? Numeric
187
+ define_method(:mutate!) {
188
+ random_subtree.replace generate_random_tree(depth,type)
189
+ self
190
+ }
191
+ elsif depth==:same || (depth.is_a?(Array) && depth[0]==:same)
192
+ s, dd_min, dd_max = *depth
193
+ possible_deltas = (dd_min||0..dd_max||0).to_a
194
+ define_method(:mutate!) {
195
+ st = random_subtree
196
+ st.replace generate_random_tree([tree_depth(st) + possible_deltas.at_rand,0].max, type)
197
+ self
198
+ }
199
+ elsif depth.respond_to?(:to_a)
200
+ possible_depths = depth.to_a
201
+ define_method(:mutate!) {
202
+ random_subtree.replace generate_random_tree(possible_depths.at_rand,type)
203
+ self
204
+ }
205
+ else
206
+ raise ArgumentError, "invalid option for depth"
207
+ end
208
+ }
209
+ end
210
+
211
+ TreeReplaceMutator = TreeReplaceMutator()
212
+ TreePruneMutator = TreeReplaceMutator(0)
213
+
214
+ # replace root by one of its children, i.e. TreeRemoveNodeMutator with the root instead of a random node.
215
+ module TreeChopMutator
216
+ def mutate!
217
+ return self if genes.first==:term
218
+ genes.replace genes[1..-1].at_rand # replace root with child
219
+ self
220
+ end
221
+ end
222
+
223
+ # replaces a random node by one of its children. does nothing if the randomly chosen node is a terminal.
224
+ module TreeRemoveNodeMutator
123
225
  def mutate!
124
- random_subtree.replace random_tree(2)
226
+ st = random_subtree
227
+ return self if st.first==:term
228
+ st.replace st[1..-1].at_rand
125
229
  self
126
230
  end
127
231
  end
128
232
 
233
+ # replaces a random node x by a new operator node having x as one of its children. When inserting a binary operator the other node will be a terminal.
234
+ module TreeInsertNodeMutator
235
+ def mutate!
236
+ st = random_subtree
237
+ if rand < 0.5 || unary_ops.nil?
238
+ st.replace [binary_ops.at_rand, st.dup, generate_random_tree_full(0)]
239
+ else
240
+ st.replace [unary_ops.at_rand, st.dup]
241
+ end
242
+ self
243
+ end
244
+ end
245
+
246
+ # replaces a random terminal by a new one.
247
+ module TreeTerminalMutator
248
+ def mutate!
249
+ random_terminal.replace generate_random_tree(0)
250
+ self
251
+ end
252
+ end
253
+
254
+ # mutates a random numeric terminal using a point mutator (cf. ListMutator) or a block (e.g. {|x| x-rand+0.5}
255
+ def TreeNumTerminalMutator(mutate=:uniform[0.1], &b)
256
+ if block_given?
257
+ mutate_proc = b
258
+ else
259
+ mut_name, *mut_arg = mutate
260
+ mut_fn = PointMutators[mut_name]
261
+ mutate_proc = proc{|x| mut_fn.call(x,*mut_arg) }
262
+ end
263
+
264
+ Module.new{
265
+ self.name = "TreeNumTerminalMutator(#{mutate.inspect})"
266
+ define_method(:mutate!) {
267
+ numterms = all_terminals.select{|x| x[1].is_a? Numeric }
268
+ unless numterms.empty?
269
+ random_term = numterms.at_rand
270
+ random_term[1] = mutate_proc.call(random_term[1])
271
+ end
272
+ self
273
+ }
274
+ }
275
+ end
276
+ TreeNumTerminalMutator = TreeNumTerminalMutator()
277
+
278
+ # Replaces a random subtree by the result of its evaluation. value_hash is passed to eval_tree.
279
+ def TreeEvalMutator(value_hash=Hash.new{0})
280
+ Module.new{
281
+ define_method(:mutate!) {
282
+ st = random_subtree
283
+ st.replace [:term,eval_tree(st,value_hash)]
284
+ self
285
+ }
286
+ }
287
+ end
288
+ TreeEvalMutator = TreeEvalMutator()
289
+
290
+
@@ -3,7 +3,7 @@
3
3
  require File.dirname(__FILE__) + '/../lib/charlie' unless Object.const_defined?('Charlie')
4
4
  require 'test/unit'
5
5
 
6
- $crs_mth = [NullCrossover, SinglePointCrossover, UniformCrossover]
6
+ $crs_mth = [NullCrossover, SinglePointCrossover, UniformCrossover, NPointCrossover(1), NPointCrossover(2), NPointCrossover(24)]
7
7
 
8
8
 
9
9
  class TestProblem < FloatListGenotype(2,0..1)
@@ -10,7 +10,7 @@ class BMTest < Test::Unit::TestCase
10
10
 
11
11
  def test_basic_bm
12
12
  assert_nothing_raised {
13
- Population.benchmark(RoyalRoad,CDIR+'/output/test_benchmark.html'){
13
+ GABenchmark.benchmark(RoyalRoad,CDIR+'/output/test_benchmark.html'){
14
14
  selection TruncationSelection(0.2), Elitism(ScaledRouletteSelection(),1)
15
15
  crossover NullCrossover, UniformCrossover
16
16
  mutator ListMutator(:single_point,:flip), ListMutator(:expected_n[4],:flip)
@@ -25,7 +25,7 @@ class BMTest < Test::Unit::TestCase
25
25
 
26
26
  def test_csv
27
27
  assert_nothing_raised {
28
- Population.benchmark(StringA,nil,CDIR+'/output/test_benchmark.csv'){
28
+ GABenchmark.benchmark(StringA,nil,CDIR+'/output/test_benchmark.csv'){
29
29
  selection TruncationSelection(0.2)
30
30
  crossover NullCrossover, Module.new{self.name='default'}
31
31
  mutation ListMutator(:expected_n[1],:replace[*'a'..'d']), NullMutator
@@ -36,23 +36,37 @@ class BMTest < Test::Unit::TestCase
36
36
 
37
37
  def test_defaults
38
38
  assert_nothing_raised {
39
- r = Population.benchmark(StringA,nil,nil){
39
+ r = GABenchmark.benchmark(StringA,nil,nil){
40
40
  selection TruncationSelection(0.2), TournamentSelection(3)
41
41
  # no crossover, etc specified.
42
42
  }
43
43
  assert_equal 2,r.size
44
44
  }
45
45
  assert_nothing_raised {
46
- r=Population.benchmark(StringA,nil,nil){
46
+ r=GABenchmark.benchmark(StringA,nil,nil){
47
47
  crossover NullCrossover
48
48
  }
49
49
  assert_equal 1,r.size
50
50
  }
51
51
  assert_nothing_raised {
52
- r=Population.benchmark(StringA,nil,nil){
52
+ r=GABenchmark.benchmark(StringA,nil,nil){
53
53
  }
54
54
  assert_equal 1,r.size
55
55
  }
56
56
  end
57
57
 
58
+
59
+ def test_multiple_stats
60
+ d = nil
61
+ assert_nothing_raised {
62
+ d = GABenchmark.benchmark(StringA,nil,nil){
63
+ track_stats{|b| [0,1] }
64
+ repeat 11
65
+ generations 17
66
+ }
67
+ }
68
+ assert_equal 1,d.size
69
+ assert_equal 11,d[0][-1].size
70
+ assert d.all?{|r| r[-1].all?{|e| e.size==2 } }
71
+ end
58
72
  end