rubylabs 0.9.0 → 0.9.1

Sign up to get free protection for your applications and to get access to all the features.
data/lib/tsplab.rb CHANGED
@@ -1,23 +1,18 @@
1
+ module RubyLabs
2
+
1
3
  =begin rdoc
2
4
 
3
- == Traveling Salesman Lab
5
+ == TSPLab
4
6
 
5
- =end
7
+ The TSPLab module has definitions of classes and methods used in the projects for Chapter 12
8
+ of <em>Explorations in Computing</em>. The experiments in this chapter are on the Traveling
9
+ Salesman Problem and techniques for solving it with a genetic algorithm. A class called Map
10
+ implements a symmetric matrix to represent driving distances, and a class called Tour
11
+ represents paths that connect every point on a map. In the genetic algorithm, a "population"
12
+ is simply an array of Tour objects, and methods gradually evolve a solution by throwing out
13
+ high cost tours and replacing them with new tours that are slight variations of the survivors.
6
14
 
7
- =begin
8
- todo unit tests
9
- todo keep track of how tour made, color those from crosses a different color
10
- todo ? later -- hightlight changes when drawing tour
11
- todo possible error in call to m.make_tour(:mutate,t) when t is not full size tour of m
12
- todo kinda brittle with popsize param -- interaction between histogram and popsize needs
13
- to be stabilized (see esp need to pass popsize to rebuild_population)
14
- todo fix labels -- top level methods need to erase labels from rsearch or other experiments
15
- todo to_s for maps should print : in front of symbol labels
16
- todo clean up rsearch, add :begin/:end wrapper
17
- todo catch ^C interrupt in esearch? each_permutation?
18
15
  =end
19
-
20
- module RubyLabs
21
16
 
22
17
  module TSPLab
23
18
 
@@ -27,181 +22,270 @@ module TSPLab
27
22
  MapView = Struct.new(:cities, :nodes, :links, :labels, :histogram, :options)
28
23
  ItemWithDirection = Struct.new(:value, :direction)
29
24
 
25
+ # An ItemWithDirection is a struct with two fields, a numeric value and a character that
26
+ # represents right or left, used by the Johnson-Trotter algorithm that generates permutations
27
+ # of points on a map.
28
+
30
29
  class ItemWithDirection
31
- def inspect
30
+ def inspect # :nodoc:
32
31
  (self.direction ? "<" : ">") + self.value.inspect
33
32
  end
34
33
  end
35
34
 
36
35
  =begin rdoc
37
- A Map is a 2D array of distances between pairs of cities. A new Map object
38
- will initially be empty. Call m[i,j] = x to assign the distance between i and
39
- j. Maps are symmetric (i.e. m[i,j] == m[j,i] for all i and j) so assigning a
40
- value to m[i,j] automatically assigns the same value to m[j,i]. Indices can be
41
- strings, symbols, or integers.
36
+
37
+ == Map
38
+
39
+ A Map is a 2D array of distances between pairs of cities. Use the index operator to
40
+ look up the distance between two points. For example, given a Map object +m+,
41
+ call <tt>m[i,j] to find the distance between cities +i+ and
42
+ +j+. Indices +i+ and +j+ can be symbols or integers.
43
+
42
44
  =end
43
45
 
44
46
  class Map
45
47
 
46
- # Make a new distance matrix.
48
+ # Create a new Map object. If the argument is an integer +n+, make a map with +n+
49
+ # cities at random locations (see the method make_random_map for a description of
50
+ # how the cities are chosen). If the argument is a symbol, read a file with that
51
+ # name from the TSPLab data directory. If the argument is a string, look for a file
52
+ # with that name in the current directory.
53
+ #
54
+ # Example:
55
+ # >> m = Map.new(10)
56
+ # => #<TSPLab::Map [0,1,2,3,4,5,6,7,8,9]>
57
+ # >> m = Map.new(:ireland)
58
+ # => #<TSPLab::Map [belfast,cork,dublin,galway,limerick]>
47
59
 
48
60
  def initialize(arg)
49
- @labels = Array.new
50
- @dist = Array.new
51
- @coords = Array.new
61
+ @labels = Array.new
62
+ @dist = Array.new
63
+ @coords = Array.new
52
64
  if arg.class == String || arg.class == Symbol
53
65
  read_file(arg)
54
66
  elsif arg.class == Fixnum
55
67
  make_random_map(arg)
56
- elsif arg.class == Array
57
- make_map(arg)
58
- elsif arc.class != NilClass
59
- raise "Map.new: parameter must be a file name, array of points, or an integer"
68
+ # elsif arg.class == Array
69
+ # make_map(arg)
70
+ else
71
+ raise "Map.new: parameter must be a symbol, a file name, or an integer"
60
72
  end
61
73
  end
62
74
 
75
+ # Generate a string that lists the cities in this map object.
76
+
63
77
  def inspect
64
78
  sprintf "#<TSPLab::Map [%s]>", @labels.join(",")
65
79
  end
66
80
 
67
81
  alias to_s inspect
68
82
 
69
- # print the current state of the metric; the parameter is the field width (number
70
- # of chars in each matrix entry)
71
-
72
- def display(fw = nil)
73
- if fw.nil?
83
+ # Print the complete set of driving distances in the map in the form of a
84
+ # symmetric matrix. The argument is the field width (number
85
+ # of chars in each matrix entry). If no argument is passed, the method uses the
86
+ # number of letters in the longest city name as the field width.
87
+ #
88
+ # Example:
89
+ # >> m = Map.new(:ireland)
90
+ # => #<TSPLab::Map [belfast,cork,dublin,galway,limerick]>
91
+ # >> m.display
92
+ # belfast cork dublin galway limerick
93
+ # belfast 0.00
94
+ # cork 425.00 0.00
95
+ # dublin 167.00 257.00 0.00
96
+ # galway 306.00 209.00 219.00 0.00
97
+ # limerick 323.00 105.00 198.00 105.00 0.00
98
+
99
+ def display(fw = nil)
100
+ if fw.nil?
74
101
  lw = labels.inject(0) { |max, x| n = x.to_s.length; (n > max) ? n : max }
75
102
  dw = (log10(@maxdist).ceil+4)
76
103
  fw = max(lw+1,dw)
77
- end
104
+ end
78
105
  res = " " * fw
79
106
  @labels.each { |name| res << sprintf("%#{fw-1}s ", name.to_s[0..fw-1]) }
80
107
  res += "\n"
81
- @dist.each_with_index do |a,i|
82
- res += sprintf("%#{fw-1}s ", @labels[i].to_s[0..fw-1])
83
- a.each { |x| res += (x.nil? ? " -- " : sprintf("%#{fw}.2f", x)) }
84
- res += "\n"
85
- end
86
- puts res
87
- end
88
-
89
- # method to make tours
90
-
91
- def make_tour(*args)
92
- begin
93
- args << :any if args.length == 0
94
- case args[0]
95
- when :any
96
- tour = Tour.new(self, labels) # note labels returns clone of @labels array...
97
- when :random
98
- tour = Tour.new(self, permute!(labels))
99
- when :mutate
100
- raise "usage" unless args.length >= 2 && args[1].class == Tour && (args[2].nil? || args[2].class == Fixnum)
101
- child = args[1].clone
102
- dmax = args[2] ? args[2] : 1
103
- i = rand(size)
108
+ @dist.each_with_index do |a,i|
109
+ res += sprintf("%#{fw-1}s ", @labels[i].to_s[0..fw-1])
110
+ a.each { |x| res += (x.nil? ? " -- " : sprintf("%#{fw}.2f", x)) }
111
+ res += "\n"
112
+ end
113
+ puts res
114
+ end
115
+
116
+ # Creat a new Tour object that represents a path that connects cities
117
+ # in this map. If the argument is an array of city names, the tour will include
118
+ # just these cities, in the order they are given (the array does not have to include
119
+ # all the cities). If the method is called without any arguments it returns a tour
120
+ # that contains all the cities in the order in which they were defined. The third
121
+ # situation is to pass a symbol as the first argument in the call, in which case the
122
+ # symbol specifies how the new tour is created:
123
+ # <tt> m.make_tour(:random)</tt>:: make a complete tour of all cities in a random order
124
+ # <tt> m.make_tour(:mutate, t1)</tt>:: the new tour is a copy of <tt>t1</tt> with a single point mutation
125
+ # <tt> m.make_tour(:cross, t1, t2)</tt>:: the new tour is a cross between tours <tt>t1</tt> and <tt>t2</tt>.
126
+ #
127
+ # Example:
128
+ # >> m = Map.new(10)
129
+ # => #<TSPLab::Map [0,1,2,3,4,5,6,7,8,9]>
130
+ # >> m.make_tour([2,4,6])
131
+ # => #<TSPLab::Tour [2, 4, 6] (871.38)>
132
+ # >> t1 = m.make_tour
133
+ # => #<TSPLab::Tour [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] (2161.70)>
134
+ # >> t2 = m.make_tour(:random)
135
+ # => #<TSPLab::Tour [8, 6, 7, 5, 4, 0, 1, 9, 2, 3] (2294.16)>
136
+ # >> m.make_tour(:mutate, t1)
137
+ # => #<TSPLab::Tour [0, 1, 2, 3, 5, 4, 6, 7, 8, 9] (2426.67)>
138
+ # >> m.make_tour(:cross, t1, t2)
139
+ # => #<TSPLab::Tour [4, 5, 6, 7, 8, 9, 0, 1, 2, 3] (2161.70)>
140
+
141
+ def make_tour(*args)
142
+ begin
143
+ args << :nil if args.length == 0
144
+ case args[0]
145
+ when :nil
146
+ tour = Tour.new(self, labels) # note labels returns clone of @labels array...
147
+ when :random
148
+ tour = Tour.new(self, permute!(labels))
149
+ when :mutate
150
+ raise "usage" unless args.length >= 2 && args[1].class == Tour && (args[2].nil? || args[2].class == Fixnum)
151
+ child = args[1].clone
152
+ dmax = args[2] ? args[2] : 1
153
+ i = rand(size)
104
154
  d = 1 + rand(dmax)
105
155
  # puts "mutate #{i} #{d}"
106
- tour = child.mutate!(i,d)
107
- child.parent = args[1]
108
- child.op = [:mutate, i, d]
109
- when :cross
110
- raise "usage" unless args.length == 3 && args[1].class == Tour && args[2].class == Tour
111
- child = args[1].clone
112
- i = rand(size)
113
- n = 1 + rand(size-1)
156
+ tour = child.mutate!(i,d)
157
+ child.parent = args[1]
158
+ child.op = [:mutate, i, d]
159
+ when :cross
160
+ raise "usage" unless args.length == 3 && args[1].class == Tour && args[2].class == Tour
161
+ child = args[1].clone
162
+ i = rand(size)
163
+ n = 1 + rand(size-1)
114
164
  # puts "cross #{i} #{n}"
115
- tour = child.cross!(args[2], i, n)
116
- child.parent = args[1]
117
- child.op = [:cross, args[2], i, n]
118
- else
119
- raise "usage" unless args[0].class == Array
120
- a = args[0]
121
- errs = 0
122
- a.each do |x|
123
- if ! @labels.include?(x)
124
- puts "unknown city: #{x}"
125
- errs += 1
126
- end
127
- end
128
- raise "errors in list of cities" if errs > 0
129
- tour = Tour.new(self, a)
130
- end
131
- rescue Exception => e
132
- if e.message == "usage"
133
- puts "Usage:"
134
- puts " make_tour( [x,y,..] )"
135
- puts " make_tour( :random )"
136
- puts " make_tour( :mutate, t [,n] )"
137
- puts " make_tour( :cross, t1, t2 )"
138
- else
139
- puts "make_tour: #{e.message}"
140
- end
141
- return nil
142
- end
143
-
165
+ tour = child.cross!(args[2], i, n)
166
+ child.parent = args[1]
167
+ child.op = [:cross, args[2], i, n]
168
+ else
169
+ raise "usage" unless args[0].class == Array
170
+ a = args[0]
171
+ errs = 0
172
+ a.each do |x|
173
+ if ! @labels.include?(x)
174
+ puts "unknown city: #{x}"
175
+ errs += 1
176
+ end
177
+ end
178
+ raise "errors in list of cities" if errs > 0
179
+ tour = Tour.new(self, a)
180
+ end
181
+ rescue Exception => e
182
+ if e.message == "usage"
183
+ puts "Usage:"
184
+ puts " make_tour( [x,y,..] )"
185
+ puts " make_tour( :random )"
186
+ puts " make_tour( :mutate, t [,n] )"
187
+ puts " make_tour( :cross, t1, t2 )"
188
+ else
189
+ puts "make_tour: #{e.message}"
190
+ end
191
+ return nil
192
+ end
193
+
144
194
  return tour
145
195
 
146
- end
147
-
148
- # Generate all possible tours using the Johnson-Trotter algorithm. The method
149
- # works by generating all permutations of array indices, then for each permutation
150
- # generating an array of labels.
151
-
152
- def each_tour
153
- a = []
154
- n = @labels.length
155
- for i in 1...n do
156
- a << ItemWithDirection.new(i, true)
157
- end
158
- loop do
196
+ end
197
+
198
+ # Generate all possible tours of this map. Every tour starts in the first
199
+ # city (city 0 when city names are integers, or the first city read from
200
+ # the file). Successive tours are generated by the Johnson-Trotter algorithm,
201
+ # which generates permutations by exchanging just two cities from the previous
202
+ # tour. The Johnson-Trotter algorithm makes it possible to stop iterating
203
+ # after all unique tours starting from city 0 have been generated.
204
+ #
205
+ # Example:
206
+ # >> m = Map.new(5)
207
+ # => #<TSPLab::Map [0,1,2,3,4]>
208
+ # >> m.each_tour { |t| puts t }
209
+ # #<TSPLab::Tour [0, 1, 2, 3, 4] (865.11)>
210
+ # #<TSPLab::Tour [0, 1, 2, 4, 3] (1042.97)>
211
+ # #<TSPLab::Tour [0, 1, 4, 2, 3] (987.40)>
212
+ # #<TSPLab::Tour [0, 4, 1, 2, 3] (808.91)>
213
+ # #<TSPLab::Tour [0, 4, 1, 3, 2] (741.31)>
214
+ # #<TSPLab::Tour [0, 1, 4, 3, 2] (941.69)>
215
+ # #<TSPLab::Tour [0, 1, 3, 4, 2] (975.37)>
216
+ # #<TSPLab::Tour [0, 1, 3, 2, 4] (843.23)>
217
+ # #<TSPLab::Tour [0, 3, 1, 2, 4] (842.59)>
218
+ # #<TSPLab::Tour [0, 3, 1, 4, 2] (919.17)>
219
+ # #<TSPLab::Tour [0, 3, 4, 1, 2] (941.06)>
220
+ # #<TSPLab::Tour [0, 4, 3, 1, 2] (796.88)>
221
+ # => nil
222
+
223
+ def each_tour
224
+ a = []
225
+ n = @labels.length
226
+ for i in 1...n do
227
+ a << ItemWithDirection.new(i, true)
228
+ end
229
+ loop do
159
230
  # yield [0] + a
160
231
  yield Tour.new(self, [@labels[0]] + a.map { |x| @labels[x.value] })
161
- mover = nil
162
- for i in 0...a.length
163
- mover = i if movable(a,i) && (mover.nil? || a[i].value > a[mover].value)
232
+ mover = nil
233
+ for i in 0...a.length
234
+ mover = i if movable(a,i) && (mover.nil? || a[i].value > a[mover].value)
164
235
  end
165
- break if mover.nil?
166
- k = a[mover].value
236
+ break if mover.nil?
237
+ k = a[mover].value
167
238
  # puts "mover = #{mover} k = #{k}"
168
- break if k == 2
169
- adj = a[mover].direction ? mover-1 : mover+1
170
- a[adj], a[mover] = a[mover], a[adj]
171
- for i in 0...a.length
172
- if a[i].value > k
173
- a[i].direction ^= true
174
- end
239
+ break if k == 2
240
+ adj = a[mover].direction ? mover-1 : mover+1
241
+ a[adj], a[mover] = a[mover], a[adj]
242
+ for i in 0...a.length
243
+ if a[i].value > k
244
+ a[i].direction ^= true
245
+ end
175
246
  end
176
- end
177
- end
247
+ end
248
+ end
178
249
 
179
- # return the number of rows in the matrix
250
+ # Return the number of cities in this map.
180
251
 
181
- def size
182
- return @dist.length
183
- end
184
-
185
- # return the first pair of city labels
252
+ def size
253
+ return @labels.length
254
+ end
255
+
256
+ # Return the first pair of city labels in this map -- used only by
257
+ # read_file when loading a map from a file.
186
258
 
187
- def first
188
- return @labels[0..1]
259
+ def first # :nodoc:
260
+ return @labels[0..1]
189
261
  end
190
262
 
191
- # this iterator will generate the remaining pairs of labels
263
+ # Iterate over every other pair of city labels except the first -- used
264
+ # only by read_file when loading a map from a file.
192
265
 
193
- def rest
194
- n = @labels.length
195
- @labels.each_with_index do |x,i|
196
- @labels.last(n-i-1).each_with_index do |y,j|
197
- next if i == 0 && j == 0
198
- yield(x,y)
199
- end
200
- end
201
- end
266
+ def rest # :nodoc:
267
+ n = @labels.length
268
+ @labels.each_with_index do |x,i|
269
+ @labels.last(n-i-1).each_with_index do |y,j|
270
+ next if i == 0 && j == 0
271
+ yield(x,y)
272
+ end
273
+ end
274
+ end
202
275
 
203
- # get/set the distance between labels i and j (which can be)
204
- # given in either order, i.e. d[i,j] is the same as d[j,i]
276
+ # Return the distance between cities +i+ and +j+ (which can be
277
+ # given in either order, i.e. <tt>d[i,j]</tt> is the same as <tt>d[j,i]</tt>).
278
+ #
279
+ # Example:
280
+ # >> m = Map.new(:ireland)
281
+ # => #<TSPLab::Map [belfast,cork,dublin,galway,limerick]>
282
+ # >> m[:cork, :dublin]
283
+ # => 257.0
284
+ #
285
+ # >> m = Map.new(10)
286
+ # => #<TSPLab::Map [0,1,2,3,4,5,6,7,8,9]>
287
+ # >> m[6,2]
288
+ # => 111.07
205
289
 
206
290
  def [](i,j)
207
291
  ix = @labels.index(i) or return nil
@@ -209,6 +293,12 @@ module TSPLab
209
293
  ix, jx = jx, ix if ix < jx
210
294
  @dist[ix][jx]
211
295
  end
296
+
297
+ # Update the map by assigning a new distance between cities +i+ and +j+.
298
+ #
299
+ # Example:
300
+ # >> m[6,2] = 110
301
+ # => 110
212
302
 
213
303
  def []=(i,j,val)
214
304
  raise "Map: can't assign to diagonal" if i == j
@@ -218,9 +308,25 @@ module TSPLab
218
308
  @dist[ix][jx] = val.to_f
219
309
  end
220
310
 
311
+ # Return a list of city names for this map.
312
+ #
313
+ # Example:
314
+ # >> m = Map.new(:ireland)
315
+ # => #<TSPLab::Map [belfast,cork,dublin,galway,limerick]>
316
+ # >> m.labels
317
+ # => [:belfast, :cork, :dublin, :galway, :limerick]
318
+
221
319
  def labels
222
320
  return @labels.clone
223
321
  end
322
+
323
+ # Return a copy of the distance matrix for this map, in the form of 2D triangular array of Floats.
324
+ #
325
+ # Example:
326
+ # >> m = Map.new(:ireland)
327
+ # => #<TSPLab::Map [belfast,cork,dublin,galway,limerick]>
328
+ # >> m.dist
329
+ # => [[0.0], [425.0, 0.0], [167.0, 257.0, 0.0], [306.0, 209.0, 219.0, 0.0], [323.0, 105.0, 198.0, 105.0, 0.0]]
224
330
 
225
331
  def dist
226
332
  d = Array.new
@@ -228,18 +334,20 @@ module TSPLab
228
334
  return d
229
335
  end
230
336
 
337
+ # Return the canvas coordinates of city +x+.
338
+
231
339
  def coords(x)
232
340
  return @coords[index_of(x)]
233
341
  end
234
342
 
235
- # method left over from Metric class, probably not useful for TSP, but...
236
-
237
- def delete(i)
238
- ix = @labels.index(i) or return nil
239
- (ix...@labels.length).each { |x| @dist[x].slice!(ix) }
240
- @dist.slice!(ix)
241
- @labels.slice!(ix)
242
- end
343
+ # deprecated
344
+ #
345
+ # def delete(i)
346
+ # ix = @labels.index(i) or return nil
347
+ # (ix...@labels.length).each { |x| @dist[x].slice!(ix) }
348
+ # @dist.slice!(ix)
349
+ # @labels.slice!(ix)
350
+ # end
243
351
 
244
352
  private
245
353
 
@@ -297,17 +405,19 @@ module TSPLab
297
405
  def make_random_map(n)
298
406
  h = Hash.new
299
407
  a = Array.new
300
- while h.size < n
301
- h[rand(400)] = 1
302
- end
303
- h.keys.each do |k|
304
- x = k / 20
305
- y = k % 20
306
- a << [x*20 + rand(5) + 50, y*20 + rand(5) + 50]
307
- end
408
+ while h.size < n
409
+ h[rand(400)] = 1
410
+ end
411
+ h.keys.each do |k|
412
+ x = k / 20
413
+ y = k % 20
414
+ a << [x*20 + rand(5) + 50, y*20 + rand(5) + 50]
415
+ end
308
416
  make_map(a)
309
417
  end
310
-
418
+
419
+ # helper method -- compute and save distances between cities named in array +a+
420
+
311
421
  def make_map(a)
312
422
  @maxdist = 0.0
313
423
  for i in 0...a.length
@@ -336,28 +446,42 @@ module TSPLab
336
446
 
337
447
 
338
448
  =begin rdoc
339
- A Tour object is an array of city names, corresponding to the cities visited, in order,
340
- by the salesman. Attributes are the path, its cost, a unique tour ID, and a reference to
341
- the matrix used to define the distance between pairs of cities.
449
+
450
+ == Tour
451
+
452
+ A Tour object is an array of city names, corresponding to the cities visited, in order,
453
+ by the salesman. Attributes are the path, its cost, a unique tour ID, and a reference to
454
+ the matrix used to define the distance between pairs of cities.
342
455
 
343
- Class methods access the number of tours created, or reset the tour counter to 0.
456
+ Class methods access the number of tours created, or reset the tour counter to 0. There
457
+ is a constructor, but users should call the <tt>make_tour</tt> method of the Map class to
458
+ create a new tour instead of calling <tt>Tour.new</tt>.
459
+ #--
460
+ TODO don't give access to path (unless there's a way to make it read-only -- freeze it?)
344
461
  =end
345
462
 
346
- # TODO don't give access to path (unless there's a way to make it read-only -- freeze it?)
347
-
348
463
  class Tour
349
464
  attr_reader :id, :path, :cost, :matrix
350
465
  attr_accessor :parent, :op
351
466
 
352
467
  @@id = 0
353
468
 
469
+ # Set the tour counter back to 0.
470
+
354
471
  def Tour.reset
355
472
  @@id = 0
356
473
  end
357
474
 
475
+ # Return the number of Tour objects created.
476
+
358
477
  def Tour.count
359
478
  return @@id
360
479
  end
480
+
481
+ # Create a new Tour object for Map +m+, visiting cities in the array +a+.
482
+ #
483
+ # NOTE: this method should not be called directly; make a new Tour by calling
484
+ # Map#make_tour instead.
361
485
 
362
486
  def initialize(m, s)
363
487
  @matrix = m
@@ -366,6 +490,8 @@ module TSPLab
366
490
  @id = @@id
367
491
  @@id += 1
368
492
  end
493
+
494
+ # Create a string that lists the cities in this object, in order, and the tour cost.
369
495
 
370
496
  def inspect
371
497
  sprintf "#<TSPLab::Tour %s (%.2f)>", @path.inspect, @cost
@@ -373,13 +499,29 @@ module TSPLab
373
499
 
374
500
  alias to_s inspect
375
501
 
376
- # A copy of a tour needs its own new id and copy of the path and pedigree
502
+ # Make a "deep copy" of this tour object, giving it a copy of the list of cities.
377
503
 
378
504
  def clone
379
505
  # return Tour.new(@matrix, @path, @nm, @nx)
380
506
  return Tour.new(@matrix, @path)
381
507
  end
382
508
 
509
+ # Change this tour object by applying a "point mutation" that swaps the city
510
+ # at location +i+ in the tour with the city +d+ locations away. +d+ is set to 1 by
511
+ # default, i.e. if no value is supplied for +d+ the city at location +i+ is
512
+ # exchanged with the one at location +i++1.
513
+ #
514
+ # Example:
515
+ # >> t = m.make_tour
516
+ # => #<TSPLab::Tour [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] (2281.08)>
517
+ # >> t.mutate!(3)
518
+ # => #<TSPLab::Tour [0, 1, 2, 4, 3, 5, 6, 7, 8, 9] (1752.78)>
519
+ #
520
+ # >> t = m.make_tour
521
+ # => #<TSPLab::Tour [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] (2281.08)>
522
+ # >> t.mutate!(3, 5)
523
+ # => #<TSPLab::Tour [0, 1, 2, 8, 4, 5, 6, 7, 3, 9] (1919.38)>
524
+ #--
383
525
  # Exchange mutation (called 'EM' by Larranaga et al). Swaps node i with one
384
526
  # d links away (d = 1 means neighbor). An optimization (has a big impact when
385
527
  # tours are 20+ cities) computes new cost by subtracting and adding single
@@ -427,6 +569,19 @@ module TSPLab
427
569
  self
428
570
  end
429
571
 
572
+ # Mutate the current tour by applying a "cross-over" mutation with tour <tt>t2</tt>.
573
+ # The new path for this object will contain all the cities from locations +i+ through
574
+ # +j+ in the current path, then all the remaining cities in the order in which they are
575
+ # found in tour <tt>t2</tt>.
576
+ #
577
+ # Example:
578
+ # >> t = m.make_tour
579
+ # => #<TSPLab::Tour [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] (2281.08)>
580
+ # >> t2 = m.make_tour(:random)
581
+ # => #<TSPLab::Tour [8, 1, 3, 2, 7, 6, 4, 0, 9, 5] (2039.49)>
582
+ # >> t.cross!(t2, 2, 5)
583
+ # => #<TSPLab::Tour [2, 3, 4, 5, 6, 8, 1, 7, 0, 9] (2492.92)>
584
+ #--
430
585
  # Order cross-over (called 'OX1' by Larranaga et al). Save a chunk of the
431
586
  # current tour, then copy the remaining cities in the order they occur in
432
587
  # tour t. i is the index of the place to start copying, n is the number to
@@ -448,8 +603,11 @@ module TSPLab
448
603
  self
449
604
  end
450
605
 
451
- # Not used normally, but is use in unit tests to make sure mutate! is computing
452
- # the right costs with its optimized strategy
606
+ # Compute the cost of this tour by summing the distances between cities in the
607
+ # order shown in the current path. In general users do not need to call this
608
+ # method, since the path is computed when the object is created, and is updated
609
+ # automatically by calls to <tt>mutate!</tt> and <tt>cross!</tt>, but this method
610
+ # is used in unit tests to make sure the cost is updated properly by the mutation methods.
453
611
 
454
612
  def pathcost
455
613
  sum = @matrix[ @path[0], @path[-1] ]
@@ -460,9 +618,15 @@ module TSPLab
460
618
  end
461
619
 
462
620
  end # class Tour
463
-
464
- # Combinatorics... tempting to write factorial with inject, but this is for the
465
- # students to look at....
621
+
622
+ # Return the value of +n+!, i.e. compute +n+ * +n+-1 * +n+-2 ... 1.
623
+ #
624
+ # Example:
625
+ # >> factorial(10)
626
+ # => 3628800
627
+ #--
628
+ # It's tempting to write factorial with inject, but this is for the
629
+ # students to look at....
466
630
 
467
631
  def factorial(n)
468
632
  f = 1
@@ -472,11 +636,30 @@ module TSPLab
472
636
  return f
473
637
  end
474
638
 
639
+ # Compute the number of possible tours for a map with +n+ cities, taking
640
+ # into account the fact that a tour can start at any of the +n+ cities and
641
+ # that direction doesn't matter.
642
+ #
643
+ # Example:
644
+ # >> ntours(10)
645
+ # => 181440
646
+
475
647
  def ntours(n)
476
648
  return factorial(n-1) / 2
477
649
  end
478
650
 
479
- # Exhaustive search
651
+ # Do an exhaustive search of all possible tours of cities in map +m+,
652
+ # returning the tour object that has the lowest cost path.
653
+ #
654
+ # Example:
655
+ # >> m = Map.new(10)
656
+ # => #<TSPLab::Map [0,1,2,3,4,5,6,7,8,9]>
657
+ # >> ntours(10)
658
+ # => 181440
659
+ # >> xsearch(m)
660
+ # => #<TSPLab::Tour [0, 7, 1, 8, 2, 3, 4, 6, 9, 5] (1027.68)>
661
+ # >> time { xsearch(m) }
662
+ # => 41.668501
480
663
 
481
664
  def xsearch(m)
482
665
  best = m.make_tour
@@ -484,15 +667,25 @@ module TSPLab
484
667
  return best
485
668
  end
486
669
 
487
- # Random search
488
-
489
-
490
- def rsearch( matrix, nsamples, userOptions = {} )
670
+ # Do a random search of the possible tours of cities in map +m+.
671
+ # Creates +nsamples+ random tour objects and returns the one that
672
+ # has the lowest cost. Optional arguments:
673
+ # :pause => 0.01 the amount of time (in seconds) to pause between samples
674
+ #
675
+ # Example:
676
+ # >> m = Map.new(10)
677
+ # => #<TSPLab::Map [0,1,2,3,4,5,6,7,8,9]>
678
+ # >> m.make_tour(:random)
679
+ # => #<TSPLab::Tour [1, 4, 5, 6, 7, 2, 8, 9, 3, 0] (2323.46)>
680
+ # >> rsearch(m, 100)
681
+ # => #<TSPLab::Tour [8, 7, 1, 2, 9, 4, 6, 3, 5, 0] (1356.22)>
682
+
683
+ def rsearch( m, nsamples, userOptions = {} )
491
684
  options = @@rsearchOptions.merge(userOptions)
492
685
 
493
686
  Tour.reset
494
687
 
495
- best = matrix.make_tour(:random)
688
+ best = m.make_tour(:random)
496
689
  if @@drawing
497
690
  make_histogram([]) # clear histogram display
498
691
  view_tour(best)
@@ -500,7 +693,7 @@ module TSPLab
500
693
  end
501
694
 
502
695
  (nsamples-1).times do |i|
503
- t = matrix.make_tour(:random)
696
+ t = m.make_tour(:random)
504
697
  if t.cost < best.cost
505
698
  best = t
506
699
  if @@drawing
@@ -517,11 +710,19 @@ module TSPLab
517
710
  return best
518
711
  end
519
712
 
520
- =begin rdoc
521
- Make a set of +n+ random tours from map +m+
522
- =end
523
-
524
- # :begin :init_population
713
+ # Create an array with +n+ random tours of the cities in map +m+. The tours
714
+ # will be sorted in order of increasing path cost. If the canvas is open,
715
+ # a histogram of tour costs is displayed.
716
+ #
717
+ # Example:
718
+ # >> p = init_population(m, 10)
719
+ # => [ #<TSPLab::Tour [1, 4, 3, 5, 0, 8, 6, 9, 2, 7] (1823.65)>,
720
+ # #<TSPLab::Tour [5, 1, 7, 8, 9, 4, 3, 2, 0, 6] (1835.27)>,
721
+ # ...
722
+ # #<TSPLab::Tour [1, 5, 6, 3, 8, 4, 2, 0, 7, 9] (2446.24)>,
723
+ # #<TSPLab::Tour [3, 2, 9, 8, 5, 4, 1, 0, 6, 7] (2765.43)> ]
724
+ #--
725
+ # :begin :init_population
525
726
  def init_population( m, n )
526
727
  a = Array.new
527
728
 
@@ -537,48 +738,90 @@ module TSPLab
537
738
 
538
739
  return a
539
740
  end
540
- # :end :init_population
541
-
542
-
543
- =begin rdoc
544
- +select_survivors(p, options)+ Apply "natural selection" to population +p+. Sort by
545
- fitness, then remove individual +i+ with probability +i/n+ where +n+ is the population
546
- size. Note the first item in the array is always kept since +0/n = 0+.
547
- =end
548
-
549
- # p.sort! { |x,y| (x.cost == y.cost) ? (x.id <=> y.id) : (x.cost <=> y.cost) }
550
-
551
- # :begin :select_survivors
741
+ # :end :init_population
742
+
743
+ # Apply "natural selection" to +population+, which should be an array of tour objects. Sort
744
+ # the array by
745
+ # fitness, then remove individual +i+ with probability +i+/+n+ where +n+ is the population
746
+ # size. Note the first item in the array is always kept since 0/+n+ = 0.
747
+ #
748
+ # Example:
749
+ # >> p = init_population(m, 10)
750
+ # => [#<TSPLab::Tour [6, 14, 8, 11, 9, 7, 12, 0, 5, 4, 3, 2, 10, 13, 1] (2926.67)>,
751
+ # #<TSPLab::Tour [13, 6, 3, 2, 12, 7, 4, 10, 5, 1, 8, 11, 9, 14, 0] (3069.48)>,
752
+ # ...
753
+ # #<TSPLab::Tour [5, 9, 1, 14, 10, 8, 0, 4, 2, 13, 3, 6, 7, 11, 12] (3455.32)>,
754
+ # #<TSPLab::Tour [0, 4, 6, 14, 2, 5, 7, 8, 11, 9, 10, 12, 13, 3, 1] (3511.14)>]
755
+ # >> p.length
756
+ # => 10
757
+ # >> select_survivors(p)
758
+ # => [#<TSPLab::Tour [6, 14, 8, 11, 9, 7, 12, 0, 5, 4, 3, 2, 10, 13, 1] (2926.67)>,
759
+ # #<TSPLab::Tour [10, 14, 6, 12, 9, 7, 11, 13, 3, 2, 4, 8, 0, 5, 1] (3119.66)>,
760
+ # #<TSPLab::Tour [6, 10, 7, 12, 13, 4, 1, 2, 11, 14, 0, 3, 9, 8, 5] (3156.77)>,
761
+ # #<TSPLab::Tour [11, 10, 1, 0, 5, 12, 8, 13, 2, 14, 6, 4, 7, 9, 3] (3206.06)>]
762
+ # >> p.length
763
+ # => 4
764
+ #--
765
+ # This version of the call to sort! keeps the first tour created if there is a tie:
766
+ # p.sort! { |x,y| (x.cost == y.cost) ? (x.id <=> y.id) : (x.cost <=> y.cost) }
767
+ #
768
+ # This version of the method uses two phases when the map is shown on the screen, in
769
+ # order to display deleted populations as gray bars in the histogram.
770
+ # :begin :select_survivors
552
771
  def select_survivors( population, toplevel = true )
553
772
  population.sort! { |x,y| x.cost <=> y.cost }
554
773
  n = population.size
555
774
 
556
775
  (n-1).downto(1) do |i|
557
776
  if ( rand < (i.to_f / n) )
558
- # population.delete_at(i)
559
- population[i].op = :zap
777
+ if @@drawing && toplevel
778
+ population[i].op = :zap
779
+ else
780
+ population.delete_at(i)
781
+ end
560
782
  end
561
783
  end
562
784
 
563
785
  if @@drawing && toplevel
564
- show_survivors( population )
786
+ show_survivors( population )
787
+ (n-1).downto(1) do |i|
788
+ population.delete_at(i) if population[i].op == :zap
789
+ end
565
790
  end
566
791
 
567
792
  return population
568
793
  end
569
- # :end :select_survivors
570
-
571
- =begin rdoc
572
- +rebuild_population(p, n, options)+ Add new Tour objects to population +p+ until the size
573
- of the population reaches +n+. Each new Tour is a mutation of one or two Tours currently
574
- in +p+. The hash +options+ has mutation parameters, e.g. the probability of doing a crossover
575
- vs. point mutation.
576
- =end
577
-
578
- # :begin :rebuild_population
794
+ # :end :select_survivors
795
+
796
+ # Add new tours to +population+, which should be an array of Tour objects, until the size
797
+ # of the array reaches +n+. Each new tour is a mutation of one or two tours currently
798
+ # in +p+. The optional arguments is an expression of the form <tt>:distribution => :x</tt> where +x+
799
+ # is an array of three floating point numbers corresponding to the probability of a small point mutation,
800
+ # the probability of a large point mutation, and the probability of a crossover. The three numbers
801
+ # must sum to 1.0. For convenience, instead of passing an array, the argument +x+ can be the name
802
+ # of one of the predefined distributions:
803
+ # :mixed [0.5, 0.25, 0.25]
804
+ # :all_small [1.0, 0.0, 0.0]
805
+ # :all_large [0.0, 1.0, 0.0]
806
+ # :all_cross [0.0, 0.0, 1.0]
807
+ # The distribution name can also be <tt>:random</tt>, in which case new tours are not mutations of
808
+ # existing tours but rather random tours.
809
+ # If no distribution is specified the default is <tt>:all_small</tt>
810
+ #
811
+ # Example:
812
+ # >> rebuild_population(pop, 10)
813
+ # => [#<TSPLab::Tour [2, 7, 4, 11, 12, 6, 5, 14, 3, 0, 13, 9, 10, 1, 8] (2224.71)>,
814
+ # ...
815
+ # #<TSPLab::Tour [2, 7, 4, 11, 12, 6, 5, 14, 3, 0, 13, 9, 8, 10, 1] (2485.64)>]
816
+ #
817
+ # >> rebuild_population(pop, 20, :distribution => :mixed)
818
+ # => [#<TSPLab::Tour [2, 7, 4, 11, 12, 6, 5, 14, 3, 0, 13, 9, 10, 1, 8] (2224.71)>,
819
+ # ...
820
+ # #<TSPLab::Tour [14, 3, 0, 13, 9, 10, 1, 2, 7, 4, 11, 12, 6, 5, 8] (2329.32)>]
821
+ #--
822
+ # :begin :rebuild_population
579
823
  def rebuild_population( population, popsize, userOptions = {} )
580
824
  options = @@esearchOptions.merge(userOptions)
581
- tracing = (options[:trace] == :on)
582
825
 
583
826
  map = population[0].matrix # assume they're all from the same map...
584
827
 
@@ -586,10 +829,6 @@ module TSPLab
586
829
  psmall, plarge, pcross = options[:profiles][dist]
587
830
  sdmax = options[:sdmax] || ((map.size > 10) ? (map.size / 10) : 1)
588
831
  ldmax = options[:ldmax] || ((map.size > 10) ? (map.size / 4) : 1)
589
-
590
- (popsize-1).downto(1) do |i|
591
- population.delete_at(i) if population[i].op == :zap
592
- end
593
832
 
594
833
  prev = population.length # items before this index are from previous generation
595
834
 
@@ -599,14 +838,14 @@ module TSPLab
599
838
  kid = map.make_tour( :random )
600
839
  elsif r < 1.0 - (plarge + pcross)
601
840
  mom = population[ rand(prev) ]
602
- kid = map.make_tour( :mutate, mom, sdmax )
603
- elsif r < 1.0 - pcross
841
+ kid = map.make_tour( :mutate, mom, sdmax )
842
+ elsif r < 1.0 - pcross
604
843
  mom = population[ rand(prev) ]
605
- kid = map.make_tour( :mutate, mom, ldmax )
844
+ kid = map.make_tour( :mutate, mom, ldmax )
606
845
  else
607
846
  mom = population[ rand(prev) ]
608
847
  dad = population[ rand(prev) ]
609
- kid = map.make_tour( :cross, mom, dad )
848
+ kid = map.make_tour( :cross, mom, dad )
610
849
  end
611
850
  population << kid
612
851
  end
@@ -617,9 +856,26 @@ module TSPLab
617
856
 
618
857
  return population
619
858
  end
620
- # :end :rebuild_population
621
-
622
- # :begin :evolve
859
+ # :end :rebuild_population
860
+
861
+ # Main loop of the genetic algorithm to find the optimal tour of a set of cities.
862
+ # +population+ is an array of tour objects (see init_population). +maxgen+ is the
863
+ # number of rounds of selection and rebuilding to execute. The third argument is
864
+ # usually omitted, but might be set when this method is called from esearch. It is
865
+ # used to continue a previous search. For example, if an earlier search ran for 100
866
+ # generations, to continue for another 100 generations, esearch will call this method
867
+ # with +maxgen+ = 200 and +ngen+ = 100. Any options passed after +ngen+ will be passed
868
+ # on to rebuild_population, to specify which mutation parameters to use.
869
+ #
870
+ # Example -- create a population and evolve it over 100 generations:
871
+ # >> p = init_population(m, 10)
872
+ # => [#<TSPLab::Tour [0, 5, 9, 13, 12, 10, 11, 4, 8, 14, 2, 7, 1, 6, 3] (2853.56)>,
873
+ # ...
874
+ # #<TSPLab::Tour [9, 14, 13, 3, 11, 6, 5, 0, 8, 10, 1, 4, 7, 2, 12] (3527.98)>]
875
+ # >> evolve(p, 100)
876
+ # => #<TSPLab::Tour [5, 0, 13, 12, 9, 10, 11, 8, 4, 14, 7, 1, 2, 6, 3] (2376.64)>
877
+ #--
878
+ # :begin :evolve
623
879
  def evolve( population, maxgen, ngen = 0, options = {} )
624
880
  popsize = population.length
625
881
  best = population[0]
@@ -629,7 +885,6 @@ module TSPLab
629
885
  if (population[0].cost - best.cost).abs > 1e-10
630
886
  best = population[0]
631
887
  updated = true
632
- # @@bestGeneration = ngen
633
888
  else
634
889
  updated = false
635
890
  end
@@ -638,18 +893,33 @@ module TSPLab
638
893
  end
639
894
  return best
640
895
  end
641
- # :end :evolve
642
-
643
- =begin rdoc
644
- +esearch( map, n, options)+ Top level method for the genetic algorithm. +m+ is a Map
645
- containing pairwise distances between a set of cities. The hash named
646
- +options+ contains tour parameters, e.g. population size and mutation probabilities.
647
- Make an initial population of random tours, then iterate +n+ times, calling +select_survivors+
648
- and +rebuild_population+ to evolve an optimal tour. The return value is the lowest cost
649
- tour after the last round of evolution.
650
- =end
651
-
652
- def esearch( matrix, maxgen, userOptions = {} )
896
+ # :end :evolve
897
+
898
+ # Use an evolutionary algorithm to search for the optimal tour of the cities on a map +m+.
899
+ # The +maxgen+ argument is the number of cycles of selection and rebuilding to perform.
900
+ # The return value is the tour object with the lowest cost in the final population.
901
+ #
902
+ # The optional arguments following +maxgen+ specify parameters of the evolutionary algorithm. Possible options
903
+ # and their defaults are:
904
+ # :popsize => 10 population size
905
+ # :distribution => :all_small (see note below)
906
+ # :pause => 0.02 time (in seconds) to pause between each generation
907
+ # The distribution option can be an array of Floats or one of the symbols <tt>:mixed</tt>, <tt>:random</tt>,
908
+ # <tt>:all_small</tt>,
909
+ # <tt>:all_large</tt>, or <tt>:all_cross</tt> passed to the rebuild_population method (see the
910
+ # documentation of that method for an explanation).
911
+ #
912
+ # Example:
913
+ # >> m = Map.new(15)
914
+ # => #<TSPLab::Map [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14]>
915
+ # >> esearch(m, 100)
916
+ # => #<TSPLab::Tour [7, 14, 6, 9, 3, 10, 2, 1, 5, 0, 13, 8, 12, 4, 11] (1823.57)>
917
+ # >> esearch(m, 100, :distribution => :mixed)
918
+ # => #<TSPLab::Tour [6, 9, 3, 12, 8, 4, 2, 10, 11, 13, 0, 5, 1, 7, 14] (1472.27)>
919
+ # >> esearch(m, 100, :distribution => [0.5, 0.0, 0.5])
920
+ # => #<TSPLab::Tour [3, 4, 1, 6, 7, 9, 14, 11, 5, 8, 2, 13, 10, 12, 0] (1581.29)>
921
+
922
+ def esearch( m, maxgen, userOptions = {} )
653
923
  options = @@esearchOptions.merge(userOptions)
654
924
 
655
925
  if options[:continue]
@@ -663,7 +933,7 @@ module TSPLab
663
933
  end
664
934
  else
665
935
  Tour.reset
666
- population = init_population( matrix, options[:popsize] )
936
+ population = init_population( m, options[:popsize] )
667
937
  ngen = 0
668
938
  if @@drawing
669
939
  init_labels(["generations:", "tours:", "cost:"])
@@ -687,60 +957,45 @@ module TSPLab
687
957
  return population[0]
688
958
  end
689
959
 
690
- def update_tour_display(population, ngen, new_tour, dt)
691
- if new_tour
692
- view_tour(population[0])
693
- @@drawing.labels[2].update(sprintf( "cost: %.2f", population[0].cost ))
694
- end
695
- update_histogram(population)
696
- @@drawing.labels[0].update(sprintf( "generations: %d", ngen ))
697
- @@drawing.labels[1].update(sprintf( "#tours: %d", Tour.count ))
698
- sleep dt
699
- end
700
-
701
- =begin rdoc
702
- Make sure the mutation parameters are valid
703
- =end
704
-
705
- def check_mutation_parameters(options)
706
- dist = options[:distribution]
707
- profiles = options[:profiles]
708
- raise "specify mutation probabilities with :distribution option" unless dist
709
- floaterr = "distribution must be an array of three numbers between 0.0 and 1.0"
710
- symbolerr = "distribution must be one of #{profiles.keys.inspect}"
711
- if dist.class == Symbol
712
- raise symbolerr unless profiles[dist]
713
- elsif dist.class == Array
714
- raise floaterr unless dist.length == 3
715
- sum = 0.0
716
- dist.each { |x| raise floaterr if (x < 0.0 || x > 1.0); sum += x}
717
- raise "sum of probabilities must be 1.0" unless (sum - 1).abs < Float::EPSILON
718
- profiles[:user] = dist
719
- options[:distribution] = :user
720
- else
721
- raise "#{floaterr} or #{symbolerr}"
722
- end
723
- end
724
-
725
- def checkout(matrix, filename = nil)
726
- matrixfilename = matrix.to_s + ".txt"
960
+ # Save a copy of a map from the TSPLab data directory in the current working directory.
961
+ # If the +filename+ argument is not specified the new file will be the name of the map
962
+ # with ".txt" appended.
963
+ #
964
+ # Example:
965
+ # >> checkout(:ireland, "my_ireland_map.txt")
966
+ # Copy of ireland saved in my_ireland_map.txt
967
+ # => nil
968
+
969
+ def checkout(m, filename = nil)
970
+ matrixfilename = m.to_s + ".txt"
727
971
  matrixfilename = File.join(@@tspDirectory, matrixfilename)
728
972
  if !File.exists?(matrixfilename)
729
- puts "Matrix not found: #{matrixfilename}"
973
+ puts "Map not found: #{matrixfilename}"
730
974
  return nil
731
975
  end
732
- outfilename = filename.nil? ? (matrix.to_s + ".txt") : filename
976
+ outfilename = filename.nil? ? (m.to_s + ".txt") : filename
733
977
  dest = File.open(outfilename, "w")
734
- File.open(matrixfilename).each do |line|
735
- dest.puts line.chomp
736
- end
978
+ File.open(matrixfilename).each do |line|
979
+ dest.puts line.chomp
980
+ end
737
981
  dest.close
738
- puts "Copy of #{matrix} saved in #{outfilename}"
982
+ puts "Copy of #{m} saved in #{outfilename}"
739
983
  end
740
984
 
741
- =begin rdoc
742
- Methods for displaying a map and a tour
743
- =end
985
+ # Initialize the RubyLabs Canvas and draw the cities in map +m+. The locations
986
+ # of the cities are defined when the map is created. For maps distributed in
987
+ # the TSPLab data area (e.g. <tt>:ireland</tt>) the +x+ and +y+ coordinates of
988
+ # each city are part of the data file. For random maps, cities are placed randomly
989
+ # on the map.
990
+ #
991
+ # Options for controlling the color and size of a circle representing a city can
992
+ # be passed following +m+. The options, and their defaults, are:
993
+ # :dotColor => '#cccccc'
994
+ # :dotRadius => 5.0
995
+ #
996
+ # Example -- display the cities in map +m+ with large green circles:
997
+ # >> view_map(m, :dotRadius => 10, :dotColor => 'darkgreen')
998
+ # => true
744
999
 
745
1000
  def view_map(m, userOptions = {} )
746
1001
  options = @@mapOptions.merge(userOptions)
@@ -757,6 +1012,11 @@ module TSPLab
757
1012
  return true
758
1013
  end
759
1014
 
1015
+ # Update the drawing on the RubyLabs canvas to show the paths in tour object +t+.
1016
+ # Raises an error if the user has not yet called view_map to draw the locations of
1017
+ # the cities first. A call to view_tour will erase an existing tour and then draw
1018
+ # a new set of lines showing the path defined by +t+.
1019
+
760
1020
  def view_tour(t, userOptions = {} )
761
1021
  if @@drawing.nil?
762
1022
  puts "call view_map to initialize the canvas"
@@ -776,11 +1036,58 @@ module TSPLab
776
1036
  @@drawing.nodes.each { |x| x.raise }
777
1037
  return true
778
1038
  end
1039
+
1040
+ private
779
1041
 
780
- =begin rdoc
781
- Initialize a set of text label objects. Erase any old labels saved
782
- for this drawing, replace them with new ones.
783
- =end
1042
+ # Helper method, called from evolve to update the best tour and the displayed text
1043
+
1044
+ def update_tour_display(population, ngen, new_tour, dt)
1045
+ if new_tour
1046
+ view_tour(population[0])
1047
+ @@drawing.labels[2].update(sprintf( "cost: %.2f", population[0].cost ))
1048
+ end
1049
+ update_histogram(population)
1050
+ @@drawing.labels[0].update(sprintf( "generations: %d", ngen ))
1051
+ @@drawing.labels[1].update(sprintf( "#tours: %d", Tour.count ))
1052
+ sleep dt
1053
+ end
1054
+
1055
+ # Helper method, called from select_survivors to update the histogram to show which
1056
+ # tours have been deleted.
1057
+
1058
+ def show_survivors(pop)
1059
+ hist = @@drawing.histogram
1060
+ return unless pop.length == hist.length
1061
+ hist.each_with_index do |box, i|
1062
+ box.fill = 'gray' if pop[i].op == :zap
1063
+ end
1064
+ @@recolor = true
1065
+ end
1066
+
1067
+ # Helper method, called from esearch to validate items passed in the options hash
1068
+
1069
+ def check_mutation_parameters(options)
1070
+ dist = options[:distribution]
1071
+ profiles = options[:profiles]
1072
+ raise "specify mutation probabilities with :distribution option" unless dist
1073
+ floaterr = "distribution must be an array of three numbers between 0.0 and 1.0"
1074
+ symbolerr = "distribution must be one of #{profiles.keys.inspect}"
1075
+ if dist.class == Symbol
1076
+ raise symbolerr unless profiles[dist]
1077
+ elsif dist.class == Array
1078
+ raise floaterr unless dist.length == 3
1079
+ sum = 0.0
1080
+ dist.each { |x| raise floaterr if (x < 0.0 || x > 1.0); sum += x}
1081
+ raise "sum of probabilities must be 1.0" unless (sum - 1).abs < Float::EPSILON
1082
+ profiles[:user] = dist
1083
+ options[:distribution] = :user
1084
+ else
1085
+ raise "#{floaterr} or #{symbolerr}"
1086
+ end
1087
+ end
1088
+
1089
+ # Helper method called by rsearch and esearch to create text strings on the display
1090
+ # (for showing the cost of the best tour, etc.)
784
1091
 
785
1092
  def init_labels(a)
786
1093
  labelx = 525
@@ -796,17 +1103,10 @@ module TSPLab
796
1103
  end
797
1104
  return @@drawing.labels
798
1105
  end
799
-
800
- # def update_labels(a)
801
- # labels = @@drawing.labels
802
- # for i in 0...labels.length
803
- # labels[i].update(a[i])
804
- # end
805
- # end
806
-
807
- # todo! color bin red if new tour comes from crossover? but what if > 1 tour per bin?
808
-
809
- # note: calling make_histogram with an empty list erases the previous histogram
1106
+
1107
+ # Helper method to draw a histogram of tour costs, called at the start of esearch.
1108
+ # Note: rsearch calls make_histogram with an empty list to erase any previous histogram
1109
+ # left by a call to esearch
810
1110
 
811
1111
  def make_histogram(pop)
812
1112
  if @@drawing.histogram.length > 0
@@ -833,6 +1133,9 @@ module TSPLab
833
1133
  @@recolor = false
834
1134
  end
835
1135
 
1136
+ # After rebuild_population fills in the array again, it calls this method to
1137
+ # resize the boxes to show fitness of tours and refills the grayed out boxes.
1138
+
836
1139
  def update_histogram(pop)
837
1140
  y = @@histogramOptions[:y]
838
1141
  ymax = @@histogramOptions[:ymax]
@@ -852,17 +1155,8 @@ module TSPLab
852
1155
  end
853
1156
  end
854
1157
 
855
- def show_survivors(pop)
856
- hist = @@drawing.histogram
857
- return unless pop.length == hist.length
858
- hist.each_with_index do |box, i|
859
- box.fill = 'gray' if pop[i].op == :zap
860
- end
861
- @@recolor = true
862
- end
863
-
864
- # Outdated attempt to show sequence of ancestors -- if revived, needs to
865
- # be updated for new Tk interface
1158
+ # Outdated attempt to show sequence of ancestors -- if revived, needs to
1159
+ # be updated for new Tk interface
866
1160
 
867
1161
  # def show_lineage(tour, userOptions = {} )
868
1162
  # options = @@lineageOptions.merge(userOptions)
@@ -887,6 +1181,13 @@ module TSPLab
887
1181
  # end
888
1182
  # return true
889
1183
  # end
1184
+
1185
+ # def update_labels(a)
1186
+ # labels = @@drawing.labels
1187
+ # for i in 0...labels.length
1188
+ # labels[i].update(a[i])
1189
+ # end
1190
+ # end
890
1191
 
891
1192
  # def test_setup
892
1193
  # m = Map.new(15)
@@ -911,7 +1212,6 @@ module TSPLab
911
1212
 
912
1213
  @@esearchOptions = {
913
1214
  :popsize => 10,
914
- :maxgen => 25,
915
1215
  :profiles => {
916
1216
  :mixed => [0.5, 0.25, 0.25],
917
1217
  :random => [0.0, 0.0, 0.0],
@@ -944,14 +1244,36 @@ end # module TSPLab
944
1244
 
945
1245
  end # module RubyLabs
946
1246
 
947
-
948
- module Enumerable
949
-
950
1247
  =begin rdoc
951
- <tt>each_permtation</tt> is an iterator that makes all permutations of
952
- a container in lexicographic order.
1248
+
1249
+ == Enumerable
1250
+
1251
+ The code for the Traveling Salesman lab (tsplab.rb) extends the Enumerable module
1252
+ with a method that generates all permutations of a string, array, or any other object
1253
+ from a class the includes the Enumerable interface.
1254
+
953
1255
  =end
954
1256
 
1257
+ module Enumerable
1258
+
1259
+ # Iterate over all possible permutations of the objects in
1260
+ # this container. The permutations are generated in lexicographic order.
1261
+ #
1262
+ # Example:
1263
+ # >> s = "ABCD"
1264
+ # => "ABCD"
1265
+ # >> s.each_permutation { |x| p x }
1266
+ # "ABCD"
1267
+ # "ABDC"
1268
+ # "ACBD"
1269
+ # "ACDB"
1270
+ # "ADBC"
1271
+ # ...
1272
+ # "DBCA"
1273
+ # "DCAB"
1274
+ # "DCBA"
1275
+ # => nil
1276
+
955
1277
  def each_permutation
956
1278
  p = self.clone
957
1279
  n = p.length