rubylabs 0.9.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/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