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/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
|