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/README.rdoc +15 -6
- data/Rakefile +3 -0
- data/VERSION +1 -1
- data/lib/bitlab.rb +593 -328
- data/lib/demos.rb +20 -9
- data/lib/elizalab.rb +660 -507
- data/lib/hashlab.rb +289 -192
- data/lib/introlab.rb +33 -38
- data/lib/iterationlab.rb +117 -61
- data/lib/marslab.rb +608 -475
- data/lib/randomlab.rb +227 -121
- data/lib/recursionlab.rb +197 -140
- data/lib/rubylabs.rb +936 -390
- data/lib/sievelab.rb +32 -24
- data/lib/spherelab.rb +308 -220
- data/lib/tsplab.rb +634 -312
- data/test/bit_test.rb +4 -4
- data/test/tsp_test.rb +18 -0
- metadata +2 -2
data/lib/tsplab.rb
CHANGED
@@ -1,23 +1,18 @@
|
|
1
|
+
module RubyLabs
|
2
|
+
|
1
3
|
=begin rdoc
|
2
4
|
|
3
|
-
==
|
5
|
+
== TSPLab
|
4
6
|
|
5
|
-
|
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
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
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
|
-
|
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
|
-
|
50
|
-
|
51
|
-
|
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
|
-
|
58
|
-
|
59
|
-
raise "Map.new: parameter must be a
|
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
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
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
|
-
|
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
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
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
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
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
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
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
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
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
|
-
|
162
|
-
|
163
|
-
|
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
|
-
|
166
|
-
|
236
|
+
break if mover.nil?
|
237
|
+
k = a[mover].value
|
167
238
|
# puts "mover = #{mover} k = #{k}"
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
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
|
-
|
177
|
-
|
247
|
+
end
|
248
|
+
end
|
178
249
|
|
179
|
-
|
250
|
+
# Return the number of cities in this map.
|
180
251
|
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
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
|
-
|
188
|
-
|
259
|
+
def first # :nodoc:
|
260
|
+
return @labels[0..1]
|
189
261
|
end
|
190
262
|
|
191
|
-
|
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
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
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
|
-
|
204
|
-
|
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
|
-
#
|
236
|
-
|
237
|
-
def delete(i)
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
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
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
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
|
-
|
340
|
-
|
341
|
-
|
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
|
-
|
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
|
-
#
|
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
|
-
#
|
452
|
-
# the
|
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
|
-
#
|
465
|
-
#
|
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
|
-
#
|
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
|
-
#
|
488
|
-
|
489
|
-
|
490
|
-
|
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 =
|
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 =
|
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
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
# :
|
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
|
-
|
544
|
-
|
545
|
-
|
546
|
-
|
547
|
-
|
548
|
-
|
549
|
-
#
|
550
|
-
|
551
|
-
#
|
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
|
-
|
559
|
-
|
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
|
-
|
572
|
-
|
573
|
-
|
574
|
-
|
575
|
-
|
576
|
-
|
577
|
-
|
578
|
-
#
|
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
|
-
|
603
|
-
|
841
|
+
kid = map.make_tour( :mutate, mom, sdmax )
|
842
|
+
elsif r < 1.0 - pcross
|
604
843
|
mom = population[ rand(prev) ]
|
605
|
-
|
844
|
+
kid = map.make_tour( :mutate, mom, ldmax )
|
606
845
|
else
|
607
846
|
mom = population[ rand(prev) ]
|
608
847
|
dad = population[ rand(prev) ]
|
609
|
-
|
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
|
-
#
|
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
|
-
|
644
|
-
+
|
645
|
-
|
646
|
-
|
647
|
-
|
648
|
-
and
|
649
|
-
|
650
|
-
|
651
|
-
|
652
|
-
|
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(
|
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
|
-
|
691
|
-
|
692
|
-
|
693
|
-
|
694
|
-
|
695
|
-
|
696
|
-
|
697
|
-
|
698
|
-
|
699
|
-
|
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 "
|
973
|
+
puts "Map not found: #{matrixfilename}"
|
730
974
|
return nil
|
731
975
|
end
|
732
|
-
outfilename = filename.nil? ? (
|
976
|
+
outfilename = filename.nil? ? (m.to_s + ".txt") : filename
|
733
977
|
dest = File.open(outfilename, "w")
|
734
|
-
|
735
|
-
|
736
|
-
|
978
|
+
File.open(matrixfilename).each do |line|
|
979
|
+
dest.puts line.chomp
|
980
|
+
end
|
737
981
|
dest.close
|
738
|
-
puts "Copy of #{
|
982
|
+
puts "Copy of #{m} saved in #{outfilename}"
|
739
983
|
end
|
740
984
|
|
741
|
-
|
742
|
-
|
743
|
-
|
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
|
-
|
781
|
-
|
782
|
-
|
783
|
-
|
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
|
-
#
|
801
|
-
#
|
802
|
-
#
|
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
|
-
|
856
|
-
|
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
|
-
|
952
|
-
|
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
|