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
@@ -1,5 +1,10 @@
1
+ begin
2
+ require '../lib/charlie'
3
+ rescue LoadError
4
+ require 'rubygems'
5
+ require 'charlie'
6
+ end
1
7
 
2
- require '../lib/charlie'
3
8
 
4
9
  module MoneyBase
5
10
  def words
@@ -17,19 +22,26 @@ module MoneyBase
17
22
  end
18
23
  end
19
24
 
20
- # send+more=money problem as a string genotype
25
+ puts "send+more=money problem as a permutation genotype"
26
+
27
+ class MoneyP < PermutationGenotype(10) # send+more=money problem as a permutation
28
+ include MoneyBase
29
+ end
30
+
31
+ Population.new(MoneyP).evolve_on_console
32
+
33
+
34
+ =begin
35
+ # can also be done with StringGenotype, more effectively too.
21
36
  # remember, string genotypes are just arrays with elements from some set, not necessarily chars.
37
+
22
38
  class Money < StringGenotype(8,0..9)
23
39
  include MoneyBase
24
40
  def fitness # no permutation, so need to have a penalty for reusing chars
25
41
  super - 100*(8 - genes.uniq.size)
26
42
  end
27
43
  end
28
- Population.new(Money).evolve_on_console
29
-
30
- class MoneyP < PermutationGenotype(10) # send+more=money problem as a permutation
31
- include MoneyBase
32
- end
33
44
 
34
- Population.new(MoneyP).evolve_on_console
45
+ Population.new(Money).evolve_on_console
35
46
 
47
+ =end
@@ -0,0 +1,98 @@
1
+ begin
2
+ require '../lib/charlie'
3
+ rescue LoadError
4
+ require 'rubygems'
5
+ require 'charlie'
6
+ end
7
+
8
+
9
+ if ARGV[0].nil?
10
+ puts "Several examples for string genotypes."
11
+ puts "Usage: ruby string.rb weasel|gridwalk"
12
+
13
+
14
+
15
+
16
+ elsif ARGV[0].downcase.gsub(/[^a-z]/,'') == 'weasel'
17
+
18
+
19
+
20
+
21
+ STR = 'methinks it is like a weasel'
22
+ Schars = ('a'..'z').to_a << ' '
23
+
24
+ puts "A version of the weasel program. Fitness is # of characters that match '#{STR}'."
25
+ puts "Useful as a simple test for convergence speed."
26
+
27
+
28
+ class Weasel < StringGenotype(STR.size,Schars)
29
+ def fitness
30
+ genes.zip(STR.chars).find_all{|a,b|a==b}.size
31
+ end
32
+
33
+ def to_s
34
+ genes.join.inspect
35
+ end
36
+ use BestOnlySelection, UniformCrossover, ListMutator(:single_point,:replace[*Schars])
37
+ end
38
+
39
+ Population.new(Weasel).evolve_on_console(200)
40
+
41
+
42
+ output_file = 'output/string_weasel.html'
43
+
44
+ puts "Running benchmark. Takes about a minute on Ruby 1.9. Output in #{output_file}"
45
+ puts "Press [Enter] to continue, or Ctrl-C to abort."
46
+ STDIN.gets
47
+
48
+ GABenchmark.benchmark(Weasel,output_file){
49
+ selection TruncationSelection(0.2), BestOnlySelection, Elitism(ScaledRouletteSelection)
50
+ crossover NullCrossover, SinglePointCrossover, UniformCrossover
51
+ mutator *(1..4).map{|n| ListMutator(:n_point[n],:replace[*Schars]) }
52
+ generations 100
53
+ }
54
+
55
+
56
+
57
+
58
+
59
+ elsif ARGV[0].downcase.gsub(/[^a-z]/,'') == 'gridwalk'
60
+
61
+
62
+
63
+
64
+
65
+
66
+ puts "This example tries to find a tour through a grid, in such a way that each square is visited only once."
67
+ puts "Often needs to restart multiple times, each time getting stuck in a local maximum, before finding the full solution."
68
+
69
+ DIRS = [[-1,0],[1,0],[0,-1],[0,1]]
70
+ class Walk < StringGenotype(25,DIRS) # Walk with steps in 4 directions
71
+
72
+ def fitness
73
+ grid = Array.new(5){Array.new(5)}
74
+ x,y=0,0
75
+ genes.each{|dx,dy|
76
+ grid[x][y] = :visited
77
+ nx,ny = x+dx,y+dy
78
+ x,y=nx,ny if (0..4)===nx && (0..4)===ny && grid[nx][ny].nil? # on grid and haven't been there before
79
+ }
80
+ grid[x][y] = :visited
81
+ grid.flatten.compact.size
82
+ end
83
+ use TournamentSelection(4)
84
+ end
85
+
86
+ # multiple runs of 100 generations until a solution is found. This is better for problems that get stuck in local maxima a lot
87
+ pop = gen = nil
88
+ loop{
89
+ pop, gen = Population.new(Walk,20).evolve_until(100){|b| b.fitness == 25 }
90
+ break if gen
91
+ puts "Did not converge in 100 generations, length = #{pop.max.fitness}/25"
92
+ }
93
+ puts 'Found walk of length 25:', pop[-1].genes.inspect
94
+
95
+
96
+
97
+
98
+ end
@@ -1,28 +1,44 @@
1
+ begin
2
+ require '../lib/charlie'
3
+ rescue LoadError
4
+ require 'rubygems'
5
+ require 'charlie'
6
+ end
1
7
  require 'pp'
2
- require '../lib/charlie'
3
8
 
4
9
  # several examples of genetic programming
5
10
 
6
11
  if ARGV[0]=='cos'
7
12
 
13
+ $size_fac = 0.0
8
14
  # approximate cos(x) by a polynomial.on [0,3]. usually results in some kind of linear approximation.like 1.3-0.8x
9
- class Cos < TreeGenotype([proc{3*rand-1.5},:x], [:+,:*,:-], [:-@])
15
+ class Cos < TreeGenotype([proc{3*rand-1.5},:x], [:-@], [:+,:*,:-])
10
16
  def fitness
11
- -(0..10).map{|x| (eval_genes(:x=>0.3*x) - Math.cos(0.3 * x) ).abs }.sum - 0.1*size # last term used to counter bloat
17
+ #-(0..10).map{|x| (eval_genes(:x=>0.3*x) - Math.cos(0.3 * x) ).abs }.sum - 0.1*size # last term used to counter bloat
12
18
  # also possible, use infinity norm instead of L1 norm
13
19
  #-(0..10).map{|x| (eval_genes(:x=>0.3*x) - Math.cos(0.3 * x) ).abs }.max
14
20
 
15
21
  # smaller range [0,1.5] and inf norm give higher order approximations
16
- #-(0..10).map{|x| (eval_genes(:x=>0.1*x) - Math.cos(0.1 * x) ).abs }.max
22
+ -(0..10).map{|x| (eval_genes(:x=>0.1*x) - Math.cos(0.1 * x) ).abs }.max
17
23
  end
24
+ use PMutate(0.5,TreeReplaceMutator.dup,TreeNumTerminalMutator.dup)
18
25
  end
19
26
 
20
- pop = Population.new(Cos).evolve_on_console(500)
21
- pp pop.max.genes
27
+ pop = Population.new(Cos)
28
+
29
+ loop{
30
+ pop.evolve_on_console(500)
31
+ pp pop.best.genes
32
+ puts "q to quit, enter to continue"
33
+ break if $stdin.gets =~ /q/
34
+ }
35
+
36
+
22
37
 
23
38
  elsif ARGV[0]=='pors'
24
39
 
25
- # plus one recall store, generate the number ARGV[1] || 32 using +, 1 and recall/store operations (as few ops as possible).
40
+
41
+ # plus one recall store, generate the number ARGV[1] || 31 using +, 1 and recall/store operations (as few ops as possible).
26
42
 
27
43
  class Fixnum
28
44
  def sto
@@ -30,7 +46,7 @@ class Fixnum
30
46
  end
31
47
  end
32
48
 
33
- class PORS < TreeGenotype([1,:rec], [:+], [:sto])
49
+ class PORS < TreeGenotype([1,:rec], [:sto], [:+])
34
50
  N = (ARGV[1] || 32).to_i
35
51
  def fitness
36
52
  $pors_store = 0 # reset store
@@ -41,8 +57,13 @@ end
41
57
  pop = Population.new(PORS).evolve_on_console(500)
42
58
  pp pop.max.genes
43
59
 
60
+ # n=31 1 + sto(sto(sto(1+1) + rec + 1) + rec + rec) + rec , size 18
61
+
62
+
44
63
  elsif ARGV[0]=='porsx'
45
64
 
65
+
66
+
46
67
  # variant on pors, multiply T by some N, with only one access to T allowed
47
68
 
48
69
  class Fixnum
@@ -51,7 +72,7 @@ class Fixnum
51
72
  end
52
73
  end
53
74
 
54
- class PORS < TreeGenotype([:rec,:T], [:+], [:sto])
75
+ class PORS < TreeGenotype([:rec,:T], [:sto], [:+])
55
76
  N = (ARGV[1] || 31).to_i # quite complicated optimal tree for 31
56
77
  def fitness
57
78
  -(0..5).map{|t|
@@ -73,8 +94,11 @@ pp pop.max.genes
73
94
 
74
95
 
75
96
  elsif ARGV[0]=='bloat'
97
+
98
+
99
+
76
100
  # just generates huge trees.
77
- class Bloat < TreeGenotype([1], [:+], [:+@])
101
+ class Bloat < TreeGenotype([1], [:+@], [:+])
78
102
  def fitness
79
103
  size
80
104
  end
@@ -86,5 +110,6 @@ end
86
110
  pop = Population.new(Bloat).evolve_on_console(200)
87
111
 
88
112
  else
89
- puts "choose a test : cos, pors, porsx, bloat"
90
- end
113
+ puts "Several examples for tree genotypes."
114
+ puts "Usage: ruby tree.rb cos|pors|porsx|bloat"
115
+ end
@@ -1,15 +1,14 @@
1
- require '../lib/charlie'
2
- # Travelling salesperson problem
3
-
4
- N=5
5
-
6
- # N cities in a circle
7
- #CITIES = (0...N).map{|i| th = i * 2 * Math::PI / N; [Math.cos(th),Math.sin(th)] }
1
+ begin
2
+ require '../lib/charlie'
3
+ rescue LoadError
4
+ require 'rubygems'
5
+ require 'charlie'
6
+ end
8
7
 
9
- # .. or on a line
10
- #CITIES = (0...N).map{|i| [0,i] }
8
+ puts "Travelling salesperson problem."
11
9
 
12
- # .. or NxN cities on a grid
10
+ N=7
11
+ # NxN cities on a grid
13
12
  CITIES = (0...N).map{|i| (0...N).map{|j| [i,j] } }.inject{|a,b|a+b}
14
13
 
15
14
  class TSP < PermutationGenotype(CITIES.size)
@@ -21,22 +20,35 @@ class TSP < PermutationGenotype(CITIES.size)
21
20
  }
22
21
  -d # higher (less negative) fitness is better. This breaks RouletteSelection (use 1.0/d instead), but most other methods can handle this
23
22
  end
23
+ use TournamentSelection(4), PCross(0.01,EdgeRecombinationCrossover), PMutateN(InversionMutator=>0.4,InsertionMutator=>0.4)
24
+
24
25
  cache_fitness # benchmark ~ 20% faster, tournament selection 25+% faster
25
26
  end
26
27
 
28
+ puts "Running GA until the optimal solution has been found."
29
+ gen = nil
30
+ pop, gen = Population.new(TSP,30).evolve_until(1000,100){|b| puts b.fitness; b.fitness > -(N*N+1) } while gen.nil?
31
+ puts "Fitness: #{pop.max.fitness}"
32
+ puts "Generations needed: #{gen.inspect}"
33
+ p pop.max.genes.map{|a| CITIES[a]}
34
+
35
+
36
+ puts "Running benchmark. Output in output/tsp.html. Takes about 50 minutes on Ruby 1.9."
37
+ puts "Press [Enter] to continue, or Ctrl-C to abort."
38
+ STDIN.gets
27
39
 
28
- pop = Population.new(TSP,20).evolve_on_console(50)
29
- #p pop[-1].genes.map{|a| CITIES[a]}
30
- # 49 -6.18033988749895 [2, 3, 4, 5, 6, 7, 8, 9, 0, 1] for the circle
40
+ GABenchmark.benchmark(TSP,'output/tsp.html') {
41
+ selection TruncationSelection(1), Elitism(ScaledRouletteSelection), TournamentSelection(4)
42
+ crossover EdgeRecombinationCrossover, PermutationCrossover,
43
+ PCross(0.5,EdgeRecombinationCrossover,PermutationCrossover),
44
+ PCross(0.01,EdgeRecombinationCrossover), NullCrossover
31
45
 
46
+ mutator TranspositionMutator, InversionMutator, InsertionMutator,
47
+ PMutateN(InversionMutator=>0.4,InsertionMutator=>0.4),
48
+ PMutateN(InversionMutator=>0.4,InsertionMutator=>0.4,RotationMutator=>0.2)
32
49
 
33
- Population.benchmark(TSP,'output/tsp.html') {
34
- selection TruncationSelection(1), TruncationSelection(0.3), Elitism(ScaledRouletteSelection,1), TournamentSelection(4), TournamentSelection(3)
35
- crossover EdgeRecombinationCrossover, PermutationCrossover, PCross(0.5,PermutationCrossover), NullCrossover, PCross(0.5,EdgeRecombinationCrossover,PermutationCrossover)
36
- mutator PermutationMutator, PMutate(0.5,PermutationMutator),
37
- InversionMutator, PMutate(0.5,InversionMutator,PermutationMutator)
38
- self.generations = 30
39
- self.repeat = 25
40
- self.population_size = 20
41
- } if ARGV[0]=='bm' # takes about 7 minutes on ruby 1.9. lower the repeat number for a quicker run
50
+ generations 400
51
+ repeat 8
52
+ population_size 20
53
+ }
42
54
 
@@ -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.6.0'
7
+ VERSION = '0.7.0'
8
8
  end
9
9
 
10
10
  require 'charlie/etc/monkey'
@@ -28,5 +28,9 @@ require 'charlie/permutation/permutation'
28
28
 
29
29
  require 'charlie/tree/tree'
30
30
 
31
+ require 'charlie/gabenchmark'
31
32
 
33
+ if RUBY_VERSION >= '1.9'
34
+ require 'charlie/1.9fixes'
35
+ end
32
36
 
@@ -0,0 +1,46 @@
1
+ #1.9 versions of some functions, to avoid bug #16493.
2
+ #TODO: remove on bugfix/1.9.1
3
+
4
+
5
+
6
+ def Elitism(sel_module,elite_n=1) # :nodoc:
7
+ ng_name = sel_module.to_s.gsub(/[^A-Za-z0-9]/,'_')
8
+ Module.new{
9
+ include sel_module
10
+ alias_method ng_name, :next_generation
11
+ @@elite_n = elite_n
12
+ define_method :next_generation, ->(population,&block){
13
+ population = population.sort_by(&:fitness)
14
+ best = population[-@@elite_n..-1]
15
+ population = send(ng_name,population,&block)
16
+ # reset old best elite_n, but don't overwrite better ones
17
+ population[-@@elite_n..-1] = best.zip_with(population[-@@elite_n..-1]){|old,new| [old,new].max }
18
+ population
19
+ }
20
+ self.name= "Elitism(#{sel_module.to_s},#{elite_n})"
21
+ }
22
+ end
23
+
24
+
25
+ def SingleChild(crossover_module) # :nodoc:
26
+ crs_name = '_cross_' + crossover_module.to_s.gsub(/[^A-Za-z0-9]/,'')
27
+ Module.new{
28
+ include crossover_module
29
+ alias_method crs_name, :cross
30
+
31
+ define_method(:cross){|*args| send(crs_name,*args).at_rand }
32
+ self.name= "SingleChild(#{crossover_module})"
33
+ }
34
+ end
35
+
36
+ module Enumerable
37
+ undef_method :sum
38
+ def sum # faster than both r=0; each; r and {|a,b|a+b}
39
+ inject(0,:+)
40
+ end
41
+
42
+ undef_method :zip_with
43
+ def zip_with(a2) # avoid Enumerable#zip in 1.9
44
+ r=[]; each_with_index{|e,i| r << yield(e,a2[i]) }; r
45
+ end
46
+ end
@@ -23,27 +23,44 @@ end
23
23
 
24
24
  # Takes crossover c1 with probability p, and crossover c2 with probability 1-p
25
25
  def PCross(p,c1,c2=NullCrossover)
26
+ return c1 if c1==c2
27
+ c1_name, c2_name = [c1,c2].map{|c| '_cross_' + c.to_s.gsub(/[^A-Za-z0-9]/,'') }
26
28
  Module.new{
27
- @@p = p
28
- include c2
29
- alias :cross2 :cross
30
- include c1
31
- def cross(*args)
32
- rand < @@p ? super(*args) : cross2(*args)
33
- end
29
+ include c1.dup # dup to avoid bugs on use PCross(..,c1) .. use c1
30
+ alias_method c1_name, :cross
31
+ include c2.dup
32
+ alias_method c2_name, :cross
33
+
34
+ define_method(:cross) {|*args|
35
+ rand < p ? send(c1_name,*args) : send(c2_name,*args)
36
+ }
34
37
  self.name= "PCross(#{p},#{c1},#{c2})"
35
38
  }
36
39
  end
37
40
 
38
- =begin
39
- #a bit nicer, but needs parent1.class.from_genes(x) everywhere instead of just from_genes(x)
40
- def PCross(p,c1,c2=NullCrossover)
41
- [c1,c2].each{|m| m.module_eval{extend self} }
41
+ # Variant of PCross for more than 2 crossovers.
42
+ # * Pass a hash of Module=>probability pairs. If sum(probability) < 1, NullCrossover will be used for the remaining probability.
43
+ # * example: PCrossN(SinglePointCrossover=>0.33,UniformCrossover=>0.33) for NullCrossover/SinglePointCrossover/UniformCrossover all with probability 1/3
44
+ def PCrossN(hash)
45
+ tot_p = hash.inject(0){|s,(m,p)| s+p }
46
+ if (tot_p - 1.0).abs > 0.01 # close to 1?
47
+ raise ArgumentError, "PCrossN: sum of probabilities > 1.0" if tot_p > 1.0
48
+ hash[NullCrossover] = (hash[NullCrossover] || 0.0) + (1.0 - tot_p)
49
+ end
50
+ partial_sums = hash.sort_by{|m,p| -p } # max probability first
51
+ s = 0.0
52
+ partial_sums.map!{|m,p| ['_cross_' + m.to_s.gsub(/[^A-Za-z0-9]/,'') , s+=p, m] }
53
+
42
54
  Module.new{
55
+ partial_sums.each{|name,p,mod|
56
+ include mod.dup
57
+ alias_method name, :cross
58
+ }
43
59
  define_method(:cross) {|*args|
44
- (rand < p ? c1 : c2).cross(*args)
60
+ r = rand
61
+ c_name = partial_sums.find{|name,p,mod| p >= r }.first
62
+ send(c_name,*args)
45
63
  }
46
- self.name= "PCross(#{p},#{c1},#{c2})"
64
+ self.name= "PCrossN(#{hash.inspect})"
47
65
  }
48
66
  end
49
- =end
@@ -16,7 +16,7 @@ class Table < Array
16
16
  def to_html
17
17
  "<table>\n" +
18
18
  "<tr>" + @colnames.map{|t| "<th>#{t}</th>" }.join + "</tr>\n" +
19
- map{|r| "<tr>\n" + r.map{|e| "\t<td>#{e.gsub('<','&lt;')}</td>\n" }.join + "</tr>\n" }.join +
19
+ map{|r| "<tr>\n" + r.map{|e| "\t<td>#{e.gsub('<','&lt;').gsub("\r\n",'<br>')}</td>\n" }.join + "</tr>\n" }.join +
20
20
  "</table>"
21
21
  end
22
22
 
@@ -25,12 +25,13 @@ class Table < Array
25
25
  end
26
26
 
27
27
  def to_s(csz=nil)
28
- rsz = at(0).size
29
- csz ||= [(80-rsz) / rsz]*rsz
28
+ console_size = 80
29
+ rsz = at(0).size
30
+ csz ||= [(console_size-rsz) / rsz]*rsz
30
31
  pad = ' ' * csz.max
31
32
  sep = csz.map{|x|'-'*x}.join('+')
32
33
  ([sep,@colnames.zip_with(csz){|str,sz| (str+pad)[0...sz] }.join('|'),sep]+
33
- map{|r| r.zip_with(csz){|str,sz| (str+pad)[0...sz] }.join('|') } << sep).join("\n")
34
+ map{|r| r.zip_with(csz){|str,sz| (str.gsub("\r\n","\\")+pad)[0...sz] }.join('|') } << sep).join("\n")
34
35
  end
35
36
 
36
37
  end