rubylabs 0.7.5 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/LICENSE +1 -1
- data/VERSION +1 -1
- data/data/huffman/testcodes.txt +30 -0
- data/data/huffman/testwords.txt +30 -0
- data/data/mars/dwarf.txt +1 -1
- data/data/spheres/fdemo.txt +6 -0
- data/data/spheres/fdemo2.txt +6 -0
- data/data/spheres/{urey.txt → melon.txt} +1 -1
- data/data/tsp/ireland.txt +23 -0
- data/data/tsp/test.txt +8 -8
- data/data/tsp/test10.txt +80 -0
- data/data/tsp/test7.txt +42 -0
- data/lib/bitlab.rb +381 -316
- data/lib/demos.rb +141 -0
- data/lib/elizalab.rb +7 -3
- data/lib/hashlab.rb +173 -115
- data/lib/introlab.rb +43 -4
- data/lib/iterationlab.rb +5 -33
- data/lib/marslab.rb +5 -5
- data/lib/permute.rb +30 -0
- data/lib/randomlab.rb +10 -29
- data/lib/recursionlab.rb +33 -25
- data/lib/rubylabs.rb +98 -76
- data/lib/sievelab.rb +6 -26
- data/lib/spherelab.rb +49 -23
- data/lib/tsplab.rb +717 -425
- data/test/bit_test.rb +5 -6
- data/test/eliza_test.rb +29 -24
- data/test/hash_test.rb +96 -0
- data/test/iteration_test.rb +20 -1
- data/test/random_test.rb +1 -1
- data/test/rubylabs_test.rb +93 -0
- data/test/sieve_test.rb +0 -1
- data/test/tsp_test.rb +140 -0
- metadata +14 -4
- data/data/huffman/hacodes.txt +0 -35
data/lib/tsplab.rb
CHANGED
@@ -6,24 +6,15 @@
|
|
6
6
|
|
7
7
|
=begin
|
8
8
|
todo unit tests
|
9
|
-
todo
|
10
|
-
todo
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
TODO (idea is that genes are fixed when critter made, don't change after that)
|
19
|
-
=end
|
20
|
-
|
21
|
-
=begin
|
22
|
-
TODO clean up interface
|
23
|
-
todo two constructors (mutate, cross) instead of one
|
24
|
-
todo distances are floats, not ints
|
25
|
-
todo wider histogram, up to 100 bins 3 pixels wide
|
26
|
-
todo line color = blue if bin width 4 or less
|
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?
|
27
18
|
=end
|
28
19
|
|
29
20
|
module RubyLabs
|
@@ -31,170 +22,276 @@ module RubyLabs
|
|
31
22
|
module TSPLab
|
32
23
|
|
33
24
|
require 'set'
|
25
|
+
require "permute.rb"
|
26
|
+
|
27
|
+
MapView = Struct.new(:cities, :nodes, :links, :labels, :histogram, :options)
|
28
|
+
ItemWithDirection = Struct.new(:value, :direction)
|
29
|
+
|
30
|
+
class ItemWithDirection
|
31
|
+
def inspect
|
32
|
+
(self.direction ? "<" : ">") + self.value.inspect
|
33
|
+
end
|
34
|
+
end
|
34
35
|
|
35
|
-
MapView = Struct.new(:cities, :nodes, :links, :histogram, :options)
|
36
|
-
|
37
36
|
=begin rdoc
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
minutes.
|
44
|
-
|
37
|
+
A Map is a 2D array of distances between pairs of cities. A new Map object
|
38
|
+
will initially be empty. Call m[i,j] = x to assign the distance between i and
|
39
|
+
j. Maps are symmetric (i.e. m[i,j] == m[j,i] for all i and j) so assigning a
|
40
|
+
value to m[i,j] automatically assigns the same value to m[j,i]. Indices can be
|
41
|
+
strings, symbols, or integers.
|
45
42
|
=end
|
43
|
+
|
44
|
+
class Map
|
46
45
|
|
47
|
-
#
|
48
|
-
# new(f) initializes an object using names and distances in file f
|
49
|
-
# size() returns number of cities
|
50
|
-
# keys() returns an array of keys
|
51
|
-
# values() returns an array of city names
|
52
|
-
# [x] returns the name of the city with key x
|
53
|
-
# distance(x,y) returns the distance between cities with keys x and y
|
54
|
-
# coords(x) returns x, y coordinates of city
|
46
|
+
# Make a new distance matrix.
|
55
47
|
|
56
|
-
class CityList
|
57
|
-
|
58
|
-
attr_reader :cities, :matrix
|
59
|
-
|
60
48
|
def initialize(arg)
|
49
|
+
@labels = Array.new
|
50
|
+
@dist = Array.new
|
51
|
+
@coords = Array.new
|
61
52
|
if arg.class == String || arg.class == Symbol
|
62
53
|
read_file(arg)
|
63
|
-
elsif arg.class == Array
|
64
|
-
make_map(arg)
|
65
54
|
elsif arg.class == Fixnum
|
66
55
|
make_random_map(arg)
|
67
|
-
|
68
|
-
|
56
|
+
elsif arg.class == Array
|
57
|
+
make_map(arg)
|
58
|
+
elsif arc.class != NilClass
|
59
|
+
raise "Map.new: parameter must be a file name, array of points, or an integer"
|
69
60
|
end
|
70
61
|
end
|
71
62
|
|
72
|
-
def inspect
|
73
|
-
|
74
|
-
return sprintf "TSPLab::CityList (%d x %d)", n, n-1
|
63
|
+
def inspect
|
64
|
+
sprintf "#<TSPLab::Map [%s]>", @labels.join(",")
|
75
65
|
end
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
66
|
+
|
67
|
+
alias to_s inspect
|
68
|
+
|
69
|
+
# print the current state of the metric; the parameter is the field width (number
|
70
|
+
# of chars in each matrix entry)
|
71
|
+
|
72
|
+
def display(fw = nil)
|
73
|
+
if fw.nil?
|
74
|
+
lw = labels.inject(0) { |max, x| n = x.to_s.length; (n > max) ? n : max }
|
75
|
+
dw = (log10(@maxdist).ceil+4)
|
76
|
+
fw = max(lw+1,dw)
|
77
|
+
end
|
78
|
+
res = " " * fw
|
79
|
+
@labels.each { |name| res << sprintf("%#{fw-1}s ", name.to_s[0..fw-1]) }
|
80
|
+
res += "\n"
|
81
|
+
@dist.each_with_index do |a,i|
|
82
|
+
res += sprintf("%#{fw-1}s ", @labels[i].to_s[0..fw-1])
|
83
|
+
a.each { |x| res += (x.nil? ? " -- " : sprintf("%#{fw}.2f", x)) }
|
84
|
+
res += "\n"
|
85
|
+
end
|
86
|
+
puts res
|
87
|
+
end
|
88
|
+
|
89
|
+
# method to make tours
|
90
|
+
|
91
|
+
def make_tour(*args)
|
92
|
+
begin
|
93
|
+
raise "usage" if args.length == 0
|
94
|
+
case args[0]
|
95
|
+
when :random
|
96
|
+
tour = Tour.new(self, permute!(labels)) # note labels returns clone of @labels array...
|
97
|
+
when :mutate
|
98
|
+
raise "usage" unless args.length >= 2 && args[1].class == Tour && (args[2].nil? || args[2].class == Fixnum)
|
99
|
+
child = args[1].clone
|
100
|
+
dmax = args[2] ? args[2] : 1
|
101
|
+
i = rand(size)
|
102
|
+
d = 1 + rand(dmax)
|
103
|
+
# puts "mutate #{i} #{d}"
|
104
|
+
tour = child.mutate!(i,d)
|
105
|
+
child.parent = args[1]
|
106
|
+
child.op = [:mutate, i, d]
|
107
|
+
when :cross
|
108
|
+
raise "usage" unless args.length == 3 && args[1].class == Tour && args[2].class == Tour
|
109
|
+
child = args[1].clone
|
110
|
+
i = rand(size)
|
111
|
+
n = 1 + rand(size-1)
|
112
|
+
# puts "cross #{i} #{n}"
|
113
|
+
tour = child.cross!(args[2], i, n)
|
114
|
+
child.parent = args[1]
|
115
|
+
child.op = [:cross, args[2], i, n]
|
116
|
+
else
|
117
|
+
raise "usage" unless args[0].class == Array
|
118
|
+
a = args[0]
|
119
|
+
errs = 0
|
120
|
+
a.each do |x|
|
121
|
+
if ! @labels.include?(x)
|
122
|
+
puts "unknown city: #{x}"
|
123
|
+
errs += 1
|
124
|
+
end
|
125
|
+
end
|
126
|
+
raise "errors in list of cities" if errs > 0
|
127
|
+
tour = Tour.new(self, a)
|
128
|
+
end
|
129
|
+
rescue Exception => e
|
130
|
+
if e.message == "usage"
|
131
|
+
puts "Usage:"
|
132
|
+
puts " make_tour( [x,y,..] )"
|
133
|
+
puts " make_tour( :random )"
|
134
|
+
puts " make_tour( :mutate, t [,n] )"
|
135
|
+
puts " make_tour( :cross, t1, t2 )"
|
136
|
+
else
|
137
|
+
puts "make_tour: #{e.message}"
|
138
|
+
end
|
139
|
+
return nil
|
140
|
+
end
|
141
|
+
|
142
|
+
return tour
|
143
|
+
|
144
|
+
end
|
145
|
+
|
146
|
+
# Generate all possible tours using the Johnson-Trotter algorithm. The method
|
147
|
+
# works by generating all permutations of array indices, then for each permutation
|
148
|
+
# generating an array of labels.
|
149
|
+
|
150
|
+
def each_tour
|
151
|
+
a = []
|
152
|
+
n = @labels.length
|
153
|
+
for i in 1...n do
|
154
|
+
a << ItemWithDirection.new(i, true)
|
155
|
+
end
|
156
|
+
loop do
|
157
|
+
# yield [0] + a
|
158
|
+
yield Tour.new(self, [@labels[0]] + a.map { |x| @labels[x.value] })
|
159
|
+
mover = nil
|
160
|
+
for i in 0...a.length
|
161
|
+
mover = i if movable(a,i) && (mover.nil? || a[i].value > a[mover].value)
|
81
162
|
end
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
(i+1..@key-1).each do |j|
|
93
|
-
s += sprintf " %3d", @matrix[i][j]
|
163
|
+
break if mover.nil?
|
164
|
+
k = a[mover].value
|
165
|
+
# puts "mover = #{mover} k = #{k}"
|
166
|
+
break if k == 2
|
167
|
+
adj = a[mover].direction ? mover-1 : mover+1
|
168
|
+
a[adj], a[mover] = a[mover], a[adj]
|
169
|
+
for i in 0...a.length
|
170
|
+
if a[i].value > k
|
171
|
+
a[i].direction ^= true
|
172
|
+
end
|
94
173
|
end
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
# return the number of rows in the matrix
|
178
|
+
|
179
|
+
def size
|
180
|
+
return @dist.length
|
181
|
+
end
|
182
|
+
|
183
|
+
# return the first pair of city labels
|
184
|
+
|
185
|
+
def first
|
186
|
+
return @labels[0..1]
|
102
187
|
end
|
103
188
|
|
104
|
-
|
105
|
-
|
189
|
+
# this iterator will generate the remaining pairs of labels
|
190
|
+
|
191
|
+
def rest
|
192
|
+
n = @labels.length
|
193
|
+
@labels.each_with_index do |x,i|
|
194
|
+
@labels.last(n-i-1).each_with_index do |y,j|
|
195
|
+
next if i == 0 && j == 0
|
196
|
+
yield(x,y)
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
# get/set the distance between labels i and j (which can be)
|
202
|
+
# given in either order, i.e. d[i,j] is the same as d[j,i]
|
203
|
+
|
204
|
+
def [](i,j)
|
205
|
+
ix = @labels.index(i) or return nil
|
206
|
+
jx = @labels.index(j) or return nil
|
207
|
+
ix, jx = jx, ix if ix < jx
|
208
|
+
@dist[ix][jx]
|
106
209
|
end
|
107
210
|
|
108
|
-
def
|
109
|
-
|
211
|
+
def []=(i,j,val)
|
212
|
+
raise "Map: can't assign to diagonal" if i == j
|
213
|
+
ix = index_of(i)
|
214
|
+
jx = index_of(j)
|
215
|
+
ix, jx = jx, ix if ix < jx
|
216
|
+
@dist[ix][jx] = val.to_f
|
110
217
|
end
|
111
218
|
|
112
|
-
def
|
113
|
-
|
114
|
-
return nil unless x >= ?A && x < @key
|
115
|
-
return @cities[x]
|
219
|
+
def labels
|
220
|
+
return @labels.clone
|
116
221
|
end
|
117
222
|
|
118
|
-
def
|
119
|
-
|
120
|
-
|
121
|
-
return
|
122
|
-
return nil unless y >= ?A && y < @key
|
123
|
-
return @matrix[x][y]
|
223
|
+
def dist
|
224
|
+
d = Array.new
|
225
|
+
@dist.each { |row| d << row.clone }
|
226
|
+
return d
|
124
227
|
end
|
125
228
|
|
126
|
-
def coords(
|
127
|
-
return @coords[
|
229
|
+
def coords(x)
|
230
|
+
return @coords[index_of(x)]
|
231
|
+
end
|
232
|
+
|
233
|
+
# method left over from Metric class, probably not useful for TSP, but...
|
234
|
+
|
235
|
+
def delete(i)
|
236
|
+
ix = @labels.index(i) or return nil
|
237
|
+
(ix...@labels.length).each { |x| @dist[x].slice!(ix) }
|
238
|
+
@dist.slice!(ix)
|
239
|
+
@labels.slice!(ix)
|
128
240
|
end
|
129
|
-
|
130
|
-
# private methods -- return the key for a city (make a new one if necessary),
|
131
|
-
# get a time value from an input line, save a distance
|
132
241
|
|
133
242
|
private
|
134
243
|
|
244
|
+
def index_of(i)
|
245
|
+
if (ix = @labels.index(i)) == nil
|
246
|
+
ix = @labels.length
|
247
|
+
@labels << i
|
248
|
+
@dist[ix] = Array.new
|
249
|
+
@dist[ix][ix] = 0.0
|
250
|
+
end
|
251
|
+
return ix
|
252
|
+
end
|
253
|
+
|
135
254
|
def read_file(fn)
|
136
255
|
matrixfilename = fn.to_s + ".txt"
|
137
|
-
matrixfilename = File.join(@@
|
256
|
+
matrixfilename = File.join(@@tspDirectory, matrixfilename)
|
138
257
|
raise "Matrix not found: #{matrixfilename}" unless File.exists?(matrixfilename)
|
139
258
|
|
140
|
-
@key = ?A
|
141
|
-
@cities = Hash.new # initially a map from name string to one-letter key
|
142
|
-
@matrix = Hash.new
|
143
|
-
@coords = Hash.new
|
144
259
|
readingLocations = true
|
260
|
+
@maxdist = 0.0
|
145
261
|
|
146
262
|
File.open(matrixfilename).each do |line|
|
147
263
|
line.chomp!
|
148
264
|
next if line.length == 0
|
265
|
+
next if line[0] == ?#
|
149
266
|
rec = line.split
|
150
267
|
if rec[0][0] == ?:
|
151
268
|
readingLocations = false if rec[0] == ":matrix"
|
152
269
|
# tbd -- deal with other directives
|
153
270
|
elsif readingLocations
|
154
|
-
|
271
|
+
x = rec[2].to_sym
|
272
|
+
# printf "loc #{x} at #{rec[0]}, #{rec[1]}\n"
|
273
|
+
@coords[index_of(x)] = [rec[0].to_i, rec[1].to_i]
|
274
|
+
# p index_of(x)
|
275
|
+
# p @coords[index_of(x)]
|
155
276
|
else
|
156
|
-
i =
|
157
|
-
j =
|
158
|
-
|
159
|
-
|
160
|
-
|
277
|
+
i = rec[0].to_sym
|
278
|
+
j = rec[1].to_sym
|
279
|
+
d = rec[2].to_f
|
280
|
+
# printf "dist #{rec[0]} to #{rec[1]} = #{rec[2]}\n"
|
281
|
+
self[i,j] = d
|
282
|
+
@maxdist = d if d > @maxdist
|
161
283
|
end
|
162
284
|
end
|
163
285
|
|
164
|
-
|
165
|
-
|
166
|
-
errs
|
167
|
-
|
168
|
-
|
169
|
-
unless @matrix[i][j] && @matrix[j][i]
|
170
|
-
errs += 1
|
171
|
-
puts "CityList.new: missing distance between #{@cities[i]} and #{@cities[j]}"
|
172
|
-
end
|
173
|
-
end
|
286
|
+
errs = []
|
287
|
+
i, j = first
|
288
|
+
errs << [i,j] if j.nil?
|
289
|
+
self.rest do |i,j|
|
290
|
+
errs << [i,j] unless self[i,j] != nil
|
174
291
|
end
|
175
|
-
raise "
|
292
|
+
raise "Map.new: missing distances #{errs.inspect}" if errs.length > 0
|
176
293
|
end
|
177
|
-
|
178
|
-
def make_map(a)
|
179
|
-
@key = ?A + a.length
|
180
|
-
@cities = Hash.new
|
181
|
-
@matrix = Hash.new
|
182
|
-
@coords = Hash.new
|
183
|
-
for i in 0...a.length
|
184
|
-
chi = ?A + i
|
185
|
-
@cities[chi] = chi.chr
|
186
|
-
for j in (i+1)...a.length
|
187
|
-
x = a[i][0] - a[j][0]
|
188
|
-
y = a[i][1] - a[j][1]
|
189
|
-
d = Math.sqrt(x**2 + y**2).round
|
190
|
-
chj = ?A + j
|
191
|
-
assign(chi,chj,d)
|
192
|
-
assign(chj,chi,d)
|
193
|
-
end
|
194
|
-
@coords[chi.chr] = a[i]
|
195
|
-
end
|
196
|
-
end
|
197
|
-
|
294
|
+
|
198
295
|
def make_random_map(n)
|
199
296
|
h = Hash.new
|
200
297
|
a = Array.new
|
@@ -209,326 +306,413 @@ minutes.
|
|
209
306
|
make_map(a)
|
210
307
|
end
|
211
308
|
|
212
|
-
def
|
213
|
-
|
214
|
-
|
215
|
-
|
309
|
+
def make_map(a)
|
310
|
+
@maxdist = 0.0
|
311
|
+
for i in 0...a.length
|
312
|
+
for j in (i+1)...a.length
|
313
|
+
x = a[i][0] - a[j][0]
|
314
|
+
y = a[i][1] - a[j][1]
|
315
|
+
d = sqrt(x**2 + y**2)
|
316
|
+
self[i,j] = d
|
317
|
+
@maxdist = d if d > @maxdist
|
318
|
+
end
|
319
|
+
@coords[i] = a[i]
|
216
320
|
end
|
217
|
-
return @cities[s]
|
218
321
|
end
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
322
|
+
|
323
|
+
# helper method for each_tour iterator, used by Johnson-Trotter algorithm
|
324
|
+
|
325
|
+
def movable(a, i)
|
326
|
+
if a[i].direction
|
327
|
+
return i > 0 && a[i].value > a[i-1].value
|
224
328
|
else
|
225
|
-
return
|
329
|
+
return i < a.length-1 && a[i].value > a[i+1].value
|
226
330
|
end
|
227
331
|
end
|
228
332
|
|
229
|
-
|
230
|
-
|
231
|
-
@matrix[i] = Hash.new
|
232
|
-
end
|
233
|
-
@matrix[i][j] = v
|
234
|
-
end
|
235
|
-
|
236
|
-
end # class CityList
|
333
|
+
end # class Map
|
334
|
+
|
237
335
|
|
238
336
|
=begin rdoc
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
The +nm+ and +nx+ attributes keep track of a tour's "pedigree." The counters are
|
246
|
-
inherited from a parent, and incremented whenever a mutation or crossover is applied.
|
247
|
-
|
337
|
+
A Tour object is an array of city names, corresponding to the cities visited, in order,
|
338
|
+
by the salesman. Attributes are the path, its cost, a unique tour ID, and a reference to
|
339
|
+
the matrix used to define the distance between pairs of cities.
|
340
|
+
|
341
|
+
Class methods access the number of tours created, or reset the tour counter to 0.
|
248
342
|
=end
|
249
343
|
|
250
|
-
|
251
|
-
TODO don't give access to path (unless there's a way to make it read-only -- freeze it?)
|
252
|
-
=end
|
344
|
+
# TODO don't give access to path (unless there's a way to make it read-only -- freeze it?)
|
253
345
|
|
254
346
|
class Tour
|
255
|
-
attr_reader :id, :path, :cost, :matrix
|
347
|
+
attr_reader :id, :path, :cost, :matrix
|
348
|
+
attr_accessor :parent, :op
|
256
349
|
|
257
350
|
@@id = 0
|
258
|
-
|
259
|
-
def
|
260
|
-
|
261
|
-
@path = m.keys.to_s.permute!
|
262
|
-
else
|
263
|
-
skeys = Set.new(m.keys)
|
264
|
-
used = Set.new
|
265
|
-
s.each_byte do |x|
|
266
|
-
raise "Tour.new: invalid character in tour: #{x}" unless skeys.member?(x.chr)
|
267
|
-
raise "Tour.new: duplicate character in tour: #{x.chr}" if used.member?(x)
|
268
|
-
used.add(x)
|
269
|
-
end
|
270
|
-
@path = s.clone
|
271
|
-
end
|
272
|
-
@cost = pathcost(m,@path)
|
273
|
-
@matrix = m # need matrix to update cost after mutation...
|
274
|
-
@id = @@id
|
275
|
-
@nm = nm
|
276
|
-
@nx = nx
|
277
|
-
@@id += 1
|
351
|
+
|
352
|
+
def Tour.reset
|
353
|
+
@@id = 0
|
278
354
|
end
|
279
|
-
|
280
|
-
# An alterntaive constructor -- either copy a single parent and add
|
281
|
-
# a point mutation, or cross two parents
|
282
|
-
|
283
|
-
def Tour.reproduce(x, y = nil, trace = nil)
|
284
|
-
if y.nil? || y == :trace
|
285
|
-
i = rand(x.path.length)
|
286
|
-
j = (i+1) % x.path.length
|
287
|
-
# j = rand(x.path.length)
|
288
|
-
trace = :trace if y == :trace # handle calls like reproduce(x, :trace)
|
289
|
-
x.clone.mutate(i, j, trace)
|
290
|
-
else
|
291
|
-
i = rand(x.path.length)
|
292
|
-
loop { j = rand(x.path.length); break if j != i }
|
293
|
-
i, j = j, i if i > j
|
294
|
-
x.clone.cross(y, i, j, trace)
|
295
|
-
end
|
296
|
-
end
|
297
|
-
|
298
|
-
# Another constructor -- return the next tour lexicographically
|
299
|
-
# following tour t
|
300
355
|
|
301
|
-
def Tour.
|
302
|
-
|
303
|
-
n = p.length
|
304
|
-
# find largest j s.t. path[j] < path[j+1]
|
305
|
-
j = n-2
|
306
|
-
while j >= 0
|
307
|
-
break if p[j] < p[j+1]
|
308
|
-
j -= 1
|
309
|
-
end
|
310
|
-
return nil if j < 0
|
311
|
-
# find largest i s.t. path[j] < path[i]
|
312
|
-
i = n-1
|
313
|
-
loop do
|
314
|
-
break if p[j] < p[i]
|
315
|
-
i -= 1
|
316
|
-
end
|
317
|
-
# exchange path[j], path[i]
|
318
|
-
p[j], p[i] = p[i], p[j]
|
319
|
-
# reverse path from j+1 to end
|
320
|
-
tmp = p.slice!(j+1, n-1)
|
321
|
-
p += tmp.reverse
|
322
|
-
return Tour.new(t.matrix, p)
|
356
|
+
def Tour.count
|
357
|
+
return @@id
|
323
358
|
end
|
324
359
|
|
325
|
-
def
|
326
|
-
|
360
|
+
def initialize(m, s)
|
361
|
+
@matrix = m
|
362
|
+
@path = s.clone
|
363
|
+
@cost = pathcost
|
364
|
+
@id = @@id
|
365
|
+
@@id += 1
|
327
366
|
end
|
328
367
|
|
329
368
|
def inspect
|
330
|
-
|
369
|
+
sprintf "#<TSPLab::Tour %s (%.2f)>", @path.inspect, @cost
|
331
370
|
end
|
332
371
|
|
333
|
-
|
334
|
-
|
335
|
-
def Tour.reset
|
336
|
-
@@id = 0
|
337
|
-
end
|
338
|
-
|
339
|
-
def Tour.count
|
340
|
-
@@id
|
341
|
-
end
|
372
|
+
alias to_s inspect
|
342
373
|
|
343
374
|
# A copy of a tour needs its own new id and copy of the path and pedigree
|
344
375
|
|
345
376
|
def clone
|
346
|
-
return Tour.new(@matrix, @path, @nm, @nx)
|
377
|
+
# return Tour.new(@matrix, @path, @nm, @nx)
|
378
|
+
return Tour.new(@matrix, @path)
|
347
379
|
end
|
348
380
|
|
349
|
-
# Exchange mutation (called EM by Larranaga et al).
|
350
|
-
#
|
381
|
+
# Exchange mutation (called 'EM' by Larranaga et al). Swaps node i with one
|
382
|
+
# d links away (d = 1 means neighbor). An optimization (has a big impact when
|
383
|
+
# tours are 20+ cities) computes new cost by subtracting and adding single
|
384
|
+
# link costs instead of recomputing full path length. Notation: path
|
385
|
+
# through node i goes xi - i - yi, and path through j is xj - j - yj.
|
351
386
|
|
352
|
-
def mutate(i,
|
353
|
-
|
387
|
+
def mutate!(i, d = 1)
|
388
|
+
raise "mutate!: index #{i} out of range 0..#{@path.length}" unless (i >=0 && i < @path.length)
|
389
|
+
return if d == 0 # these two special cases won't occur when
|
390
|
+
if d == @path.length-1 # mutate! called from evolve, but....
|
391
|
+
i = (i-1) % @path.length
|
392
|
+
d = 1
|
393
|
+
end
|
394
|
+
|
395
|
+
j = (i+d) % @path.length # location of swap
|
396
|
+
|
397
|
+
xi = (i-1) % @path.length # locations before, after i
|
398
|
+
yi = (i+1) % @path.length
|
399
|
+
xj = (j-1) % @path.length # locations before, after j
|
400
|
+
yj = (j+1) % @path.length
|
401
|
+
|
402
|
+
if d == 1
|
403
|
+
@cost -= @matrix[ @path[xi], @path[i] ]
|
404
|
+
@cost -= @matrix[ @path[j], @path[yj] ]
|
405
|
+
@cost += @matrix[ @path[xi], @path[j] ]
|
406
|
+
@cost += @matrix[ @path[i], @path[yj] ]
|
407
|
+
else
|
408
|
+
@cost -= @matrix[ @path[xi], @path[i] ]
|
409
|
+
@cost -= @matrix[ @path[i], @path[yi] ]
|
410
|
+
@cost -= @matrix[ @path[xj], @path[j] ]
|
411
|
+
@cost -= @matrix[ @path[j], @path[yj] ]
|
412
|
+
@cost += @matrix[ @path[xi], @path[j] ]
|
413
|
+
@cost += @matrix[ @path[j], @path[yi] ]
|
414
|
+
@cost += @matrix[ @path[xj], @path[i] ]
|
415
|
+
@cost += @matrix[ @path[i], @path[yj] ]
|
416
|
+
end
|
417
|
+
|
354
418
|
@path[i], @path[j] = @path[j], @path[i]
|
355
|
-
|
356
|
-
|
419
|
+
|
420
|
+
# uncomment to verify path cost logic
|
421
|
+
# if (@cost - pathcost).abs > 0.001
|
422
|
+
# puts "#{i} #{j}" + self.to_s + " / " + @cost.to_s
|
423
|
+
# end
|
424
|
+
|
357
425
|
self
|
358
426
|
end
|
359
427
|
|
360
|
-
# Order cross-over (called OX1 by Larranaga et al). Save a chunk of the
|
428
|
+
# Order cross-over (called 'OX1' by Larranaga et al). Save a chunk of the
|
361
429
|
# current tour, then copy the remaining cities in the order they occur in
|
362
|
-
# tour t.
|
430
|
+
# tour t. i is the index of the place to start copying, n is the number to
|
431
|
+
# copy.
|
363
432
|
|
364
|
-
def cross(t, i,
|
365
|
-
|
366
|
-
|
367
|
-
|
433
|
+
def cross!(t, i, n)
|
434
|
+
j = (i + n - 1) % @path.length
|
435
|
+
if i <= j
|
436
|
+
p = @path[i..j]
|
437
|
+
else
|
438
|
+
p = @path[i..-1]
|
439
|
+
p += @path[0..j]
|
440
|
+
end
|
441
|
+
@path = p
|
442
|
+
t.path.each do |c|
|
368
443
|
@path << c unless @path.include?(c)
|
369
444
|
end
|
370
|
-
@cost = pathcost
|
371
|
-
@nx += 1 + t.nx
|
372
|
-
@nm += t.nm
|
445
|
+
@cost = pathcost
|
373
446
|
self
|
374
447
|
end
|
375
448
|
|
376
|
-
#
|
377
|
-
#
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
for i in 0..s.length-2
|
384
|
-
sum += m.distance(s[i],s[i+1])
|
449
|
+
# Not used normally, but is use in unit tests to make sure mutate! is computing
|
450
|
+
# the right costs with its optimized strategy
|
451
|
+
|
452
|
+
def pathcost
|
453
|
+
sum = @matrix[ @path[0], @path[-1] ]
|
454
|
+
for i in 0..@path.length-2
|
455
|
+
sum += @matrix[ @path[i], @path[i+1] ]
|
385
456
|
end
|
386
457
|
return sum
|
387
458
|
end
|
388
459
|
|
389
460
|
end # class Tour
|
461
|
+
|
462
|
+
# Combinatorics... tempting to write factorial with inject, but this is for the
|
463
|
+
# students to look at....
|
390
464
|
|
391
|
-
|
392
|
-
|
465
|
+
def factorial(n)
|
466
|
+
f = 1
|
467
|
+
for i in 2..n
|
468
|
+
f *= i
|
469
|
+
end
|
470
|
+
return f
|
471
|
+
end
|
472
|
+
|
473
|
+
def ntours(n)
|
474
|
+
return factorial(n-1) / 2
|
475
|
+
end
|
476
|
+
|
477
|
+
# Random search
|
478
|
+
|
479
|
+
def rsearch( matrix, nsamples )
|
480
|
+
Tour.reset
|
481
|
+
best = matrix.make_tour(:random)
|
482
|
+
if @@drawing
|
483
|
+
make_histogram([]) # clear histogram display
|
484
|
+
view_tour(best)
|
485
|
+
init_labels(["tours:", "cost:"])
|
486
|
+
end
|
487
|
+
(nsamples-1).times do |i|
|
488
|
+
t = matrix.make_tour(:random)
|
489
|
+
update = t.cost < best.cost
|
490
|
+
best = t if update
|
491
|
+
if @@drawing
|
492
|
+
view_tour(t) if update
|
493
|
+
labels = []
|
494
|
+
labels << sprintf( "#tours: %d", Tour.count )
|
495
|
+
labels << sprintf( "cost: %.2f", best.cost )
|
496
|
+
update_labels( labels )
|
497
|
+
end
|
498
|
+
end
|
499
|
+
Canvas.sync
|
500
|
+
return best
|
501
|
+
end
|
502
|
+
|
503
|
+
=begin rdoc
|
504
|
+
Make a set of +n+ random tours from map +m+
|
505
|
+
=end
|
393
506
|
|
394
|
-
|
507
|
+
# :begin :init_population
|
508
|
+
def init_population( m, n )
|
395
509
|
a = Array.new
|
396
510
|
|
397
511
|
n.times do
|
398
|
-
a.
|
512
|
+
a << m.make_tour(:random)
|
399
513
|
end
|
400
514
|
|
401
515
|
a.sort! { |x,y| x.cost <=> y.cost }
|
402
|
-
|
516
|
+
|
517
|
+
if @@drawing
|
518
|
+
make_histogram(a)
|
519
|
+
Canvas.sync
|
520
|
+
end
|
521
|
+
|
403
522
|
return a
|
404
523
|
end
|
524
|
+
# :end :init_population
|
525
|
+
|
526
|
+
|
527
|
+
=begin rdoc
|
528
|
+
+select_survivors(p, options)+ Apply "natural selection" to population +p+. Sort by
|
529
|
+
fitness, then remove individual +i+ with probability +i/n+ where +n+ is the population
|
530
|
+
size. Note the first item in the array is always kept since +0/n = 0+.
|
531
|
+
=end
|
405
532
|
|
406
|
-
#
|
407
|
-
# a probability based on their ranking (the ith individual survives
|
408
|
-
# with p = (n-i)/n). Then Build back up to the original population
|
409
|
-
# size via copies with mutations or crossovers. An optional second
|
410
|
-
# argument is a collection of parameters that can override various
|
411
|
-
# options (e.g. the probability of survival).
|
412
|
-
|
413
|
-
def evolve(p, param={})
|
414
|
-
debug = param[:trace] ? true : false
|
415
|
-
pcross = param[:pcross] ? param[:pcross] : 0.25
|
416
|
-
plotci = param[:plotci]
|
417
|
-
|
418
|
-
p.sort! { |x,y| x.cost <=> y.cost }
|
419
|
-
i = 0
|
420
|
-
n = p.length
|
421
|
-
|
422
|
-
if plotci
|
423
|
-
m = 0.0
|
424
|
-
p.each { |t| m += t.cost }
|
425
|
-
m = m / p.length
|
426
|
-
printf "%.2f %.2f %.2f\n", m, p[0].cost, p[-1].cost
|
427
|
-
end
|
428
|
-
|
429
|
-
# phase 1 -- delete random tours
|
533
|
+
# p.sort! { |x,y| (x.cost == y.cost) ? (x.id <=> y.id) : (x.cost <=> y.cost) }
|
430
534
|
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
535
|
+
# :begin :select_survivors
|
536
|
+
def select_survivors( population, toplevel = true )
|
537
|
+
population.sort! { |x,y| x.cost <=> y.cost }
|
538
|
+
n = population.size
|
539
|
+
|
540
|
+
(n-1).downto(1) do |i|
|
541
|
+
if ( rand < (i.to_f / n) )
|
542
|
+
# population.delete_at(i)
|
543
|
+
population[i].op = :zap
|
439
544
|
end
|
440
545
|
end
|
546
|
+
|
547
|
+
if @@drawing && toplevel
|
548
|
+
show_survivors( population )
|
549
|
+
Canvas.sync
|
550
|
+
end
|
551
|
+
|
552
|
+
return population
|
553
|
+
end
|
554
|
+
# :end :select_survivors
|
441
555
|
|
442
|
-
|
443
|
-
|
556
|
+
=begin rdoc
|
557
|
+
+rebuild_population(p, n, options)+ Add new Tour objects to population +p+ until the size
|
558
|
+
of the population reaches +n+. Each new Tour is a mutation of one or two Tours currently
|
559
|
+
in +p+. The hash +options+ has mutation parameters, e.g. the probability of doing a crossover
|
560
|
+
vs. point mutation.
|
561
|
+
=end
|
444
562
|
|
445
|
-
|
446
|
-
|
563
|
+
# :begin :rebuild_population
|
564
|
+
def rebuild_population( population, popsize, userOptions = {} )
|
565
|
+
options = @@tourOptions.merge(userOptions)
|
566
|
+
tracing = (options[:trace] == :on)
|
567
|
+
|
568
|
+
map = population[0].matrix # assume they're all from the same map...
|
569
|
+
|
570
|
+
dist = options[:distribution]
|
571
|
+
psmall, plarge, pcross = options[:profiles][dist]
|
572
|
+
sdmax = options[:sdmax] || ((map.size > 10) ? (map.size / 10) : 1)
|
573
|
+
ldmax = options[:ldmax] || ((map.size > 10) ? (map.size / 4) : 1)
|
447
574
|
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
575
|
+
(popsize-1).downto(1) do |i|
|
576
|
+
population.delete_at(i) if population[i].op == :zap
|
577
|
+
end
|
578
|
+
|
579
|
+
prev = population.length # items before this index are from previous generation
|
580
|
+
|
581
|
+
while population.length < popsize
|
582
|
+
r = rand
|
583
|
+
if r < 1.0 - (psmall + plarge + pcross)
|
584
|
+
kid = map.make_tour( :random )
|
585
|
+
elsif r < 1.0 - (plarge + pcross)
|
586
|
+
mom = population[ rand(prev) ]
|
587
|
+
kid = map.make_tour( :mutate, mom, sdmax )
|
588
|
+
elsif r < 1.0 - pcross
|
589
|
+
mom = population[ rand(prev) ]
|
590
|
+
kid = map.make_tour( :mutate, mom, ldmax )
|
454
591
|
else
|
455
|
-
|
456
|
-
|
592
|
+
mom = population[ rand(prev) ]
|
593
|
+
dad = population[ rand(prev) ]
|
594
|
+
kid = map.make_tour( :cross, mom, dad )
|
457
595
|
end
|
458
|
-
|
459
|
-
best = kid if kid.cost < best.cost
|
460
|
-
end
|
461
|
-
|
462
|
-
return best
|
463
|
-
|
464
|
-
end
|
465
|
-
|
466
|
-
# High level interface -- make a population for a set of cities, then
|
467
|
-
# call evolve() until the best tour doesn't change after some number of
|
468
|
-
# iterations (also passed as a parameter). Print the tour when done.
|
469
|
-
|
470
|
-
def bestTour(matrix, param={})
|
471
|
-
popsize = param[:popsize] ? param[:popsize] : 10
|
472
|
-
maxgen = param[:maxgen] ? param[:maxgen] : 25
|
473
|
-
maxstatic = param[:maxstatic] ? param[:maxstatic] : maxgen/2
|
474
|
-
|
475
|
-
p = initPopulation(popsize,matrix)
|
476
|
-
best = p[0]
|
477
|
-
nstatic = 0
|
478
|
-
ngen = 0
|
479
|
-
|
480
|
-
if param[:verbose]
|
481
|
-
puts "popsize: #{popsize}"
|
482
|
-
puts "maxgen: #{maxgen}"
|
483
|
-
puts "maxstatic: #{maxstatic}"
|
484
|
-
puts "pcross: #{param[:pcross]}"
|
596
|
+
population << kid
|
485
597
|
end
|
486
598
|
|
487
599
|
if @@drawing
|
488
|
-
|
489
|
-
|
600
|
+
update_histogram(population)
|
601
|
+
Canvas.sync
|
490
602
|
end
|
491
603
|
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
604
|
+
return population
|
605
|
+
end
|
606
|
+
# :end :rebuild_population
|
607
|
+
|
608
|
+
# :begin :evolve
|
609
|
+
def evolve( population, maxgen, ngen = 0, options = {} )
|
610
|
+
popsize = population.length
|
611
|
+
best = population[0]
|
612
|
+
while ngen < maxgen
|
613
|
+
select_survivors( population, false )
|
614
|
+
rebuild_population( population, popsize, options)
|
615
|
+
if (population[0].cost - best.cost).abs > 1e-10
|
616
|
+
best = population[0]
|
617
|
+
updated = true
|
618
|
+
# @@bestGeneration = ngen
|
499
619
|
else
|
500
|
-
|
620
|
+
updated = false
|
501
621
|
end
|
502
|
-
update_histogram(p) if @@drawing
|
503
622
|
ngen += 1
|
504
|
-
|
623
|
+
update_tour_display( population, ngen, updated ) if @@drawing
|
624
|
+
end
|
625
|
+
Canvas.sync if @@drawing
|
626
|
+
return best
|
627
|
+
end
|
628
|
+
# :end :evolve
|
505
629
|
|
506
|
-
|
507
|
-
|
508
|
-
|
509
|
-
|
510
|
-
|
511
|
-
|
512
|
-
|
513
|
-
|
514
|
-
|
630
|
+
=begin rdoc
|
631
|
+
+esearch( map, n, options)+ Top level method for the genetic algorithm. +m+ is a Map
|
632
|
+
containing pairwise distances between a set of cities. The hash named
|
633
|
+
+options+ contains tour parameters, e.g. population size and mutation probabilities.
|
634
|
+
Make an initial population of random tours, then iterate +n+ times, calling +select_survivors+
|
635
|
+
and +rebuild_population+ to evolve an optimal tour. The return value is the lowest cost
|
636
|
+
tour after the last round of evolution.
|
637
|
+
=end
|
638
|
+
|
639
|
+
def esearch( matrix, maxgen, userOptions = {} )
|
640
|
+
options = @@tourOptions.merge(userOptions)
|
641
|
+
|
642
|
+
if options[:continue]
|
643
|
+
if @@previousPopulation
|
644
|
+
population = @@previousPopulation
|
645
|
+
options = @@previousOptions
|
646
|
+
ngen = @@previousNGen
|
647
|
+
else
|
648
|
+
puts "no saved population"
|
649
|
+
return nil
|
650
|
+
end
|
651
|
+
else
|
652
|
+
Tour.reset
|
653
|
+
population = init_population( matrix, options[:popsize] )
|
654
|
+
ngen = 0
|
655
|
+
if @@drawing
|
656
|
+
init_labels(["generations:", "tours:", "cost:"])
|
657
|
+
view_tour( population[0] )
|
515
658
|
end
|
516
659
|
end
|
660
|
+
|
661
|
+
begin
|
662
|
+
check_mutation_parameters(options)
|
663
|
+
rescue Exception => e
|
664
|
+
puts "esearch: " + e
|
665
|
+
return false
|
666
|
+
end
|
667
|
+
|
668
|
+
evolve( population, maxgen, ngen, options )
|
669
|
+
|
670
|
+
@@previousPopulation = population
|
671
|
+
@@previousOptions = options
|
672
|
+
@@previousNGen = maxgen
|
673
|
+
|
674
|
+
Canvas.sync if @@drawing
|
517
675
|
|
518
|
-
return
|
676
|
+
return population[0]
|
519
677
|
end
|
520
678
|
|
521
|
-
|
679
|
+
def update_tour_display(population, ngen, draw_tour)
|
680
|
+
view_tour(population[0]) if draw_tour
|
681
|
+
update_histogram(population)
|
682
|
+
labels = []
|
683
|
+
labels << sprintf( "generations: %d", ngen )
|
684
|
+
labels << sprintf( "#tours: %d", Tour.count )
|
685
|
+
labels << sprintf( "cost: %.2f", population[0].cost )
|
686
|
+
update_labels(labels)
|
687
|
+
end
|
522
688
|
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
|
689
|
+
=begin rdoc
|
690
|
+
Make sure the mutation parameters are valid
|
691
|
+
=end
|
692
|
+
|
693
|
+
def check_mutation_parameters(options)
|
694
|
+
dist = options[:distribution]
|
695
|
+
profiles = options[:profiles]
|
696
|
+
raise "specify mutation probabilities with :distribution option" unless dist
|
697
|
+
floaterr = "distribution must be an array of three numbers between 0.0 and 1.0"
|
698
|
+
symbolerr = "distribution must be one of #{profiles.keys.inspect}"
|
699
|
+
if dist.class == Symbol
|
700
|
+
raise symbolerr unless profiles[dist]
|
701
|
+
elsif dist.class == Array
|
702
|
+
raise floaterr unless dist.length == 3
|
703
|
+
sum = 0.0
|
704
|
+
dist.each { |x| raise floaterr if (x < 0.0 || x > 1.0); sum += x}
|
705
|
+
raise "sum of probabilities must be 1.0" unless (sum - 1).abs < Float::EPSILON
|
706
|
+
profiles[:user] = dist
|
707
|
+
options[:distribution] = :user
|
708
|
+
else
|
709
|
+
raise "#{floaterr} or #{symbolerr}"
|
710
|
+
end
|
527
711
|
end
|
528
712
|
|
529
713
|
def checkout(matrix, filename = nil)
|
530
714
|
matrixfilename = matrix.to_s + ".txt"
|
531
|
-
matrixfilename = File.join(@@
|
715
|
+
matrixfilename = File.join(@@tspDirectory, matrixfilename)
|
532
716
|
if !File.exists?(matrixfilename)
|
533
717
|
puts "Matrix not found: #{matrixfilename}"
|
534
718
|
return nil
|
@@ -548,14 +732,16 @@ inherited from a parent, and incremented whenever a mutation or crossover is app
|
|
548
732
|
|
549
733
|
def view_map(m, userOptions = {} )
|
550
734
|
options = @@mapOptions.merge(userOptions)
|
551
|
-
Canvas.init(
|
735
|
+
Canvas.init(800, 500, "TSPLab")
|
552
736
|
links = Array.new
|
553
737
|
nodes = Array.new
|
554
|
-
|
738
|
+
r = options[:dotRadius]
|
739
|
+
m.labels.each do |loc|
|
555
740
|
x, y = m.coords(loc)
|
556
|
-
nodes << Canvas.circle( x, y,
|
741
|
+
nodes << Canvas.circle( x, y, r, :fill => options[:dotColor] )
|
742
|
+
Canvas.text(loc.to_s, x+r, y+r)
|
557
743
|
end
|
558
|
-
@@drawing = MapView.new(m, nodes, links,
|
744
|
+
@@drawing = MapView.new(m, nodes, links, [], [], options)
|
559
745
|
Canvas.sync
|
560
746
|
return true
|
561
747
|
end
|
@@ -565,105 +751,211 @@ inherited from a parent, and incremented whenever a mutation or crossover is app
|
|
565
751
|
puts "call view_map to initialize the canvas"
|
566
752
|
return nil
|
567
753
|
end
|
754
|
+
options = @@tourOptions.merge(userOptions)
|
568
755
|
map = @@drawing.cities
|
569
756
|
@@drawing.links.each { |x| x.delete }
|
570
757
|
@@drawing.links.clear
|
571
|
-
x0, y0 = map.coords(t.path[-1]
|
758
|
+
x0, y0 = map.coords(t.path[-1])
|
572
759
|
for i in 0...t.path.length
|
573
|
-
x1, y1 = map.coords(t.path[i]
|
760
|
+
x1, y1 = map.coords(t.path[i])
|
574
761
|
@@drawing.links << Canvas.line(x0, y0, x1, y1)
|
575
762
|
x0, y0 = x1, y1
|
576
763
|
end
|
577
764
|
@@drawing.nodes.each { |x| x.raise }
|
578
765
|
Canvas.sync
|
766
|
+
return true
|
767
|
+
end
|
768
|
+
|
769
|
+
def init_labels(a)
|
770
|
+
labelx = 525
|
771
|
+
labely = 350
|
772
|
+
dy = 20
|
773
|
+
labels = @@drawing.labels
|
774
|
+
if labels.length > 0
|
775
|
+
labels.each { |x| x.text = "" }
|
776
|
+
end
|
777
|
+
labels.clear
|
778
|
+
for i in 0...a.length
|
779
|
+
if i == labels.length
|
780
|
+
labels << Canvas.text("", labelx, labely)
|
781
|
+
labely += dy
|
782
|
+
end
|
783
|
+
labels[i].text = a[i]
|
784
|
+
end
|
785
|
+
end
|
786
|
+
|
787
|
+
def update_labels(a)
|
788
|
+
labels = @@drawing.labels
|
789
|
+
for i in 0...labels.length
|
790
|
+
labels[i].text = a[i]
|
791
|
+
end
|
579
792
|
end
|
580
793
|
|
581
|
-
# todo
|
582
|
-
|
583
|
-
#
|
584
|
-
# todo -- scale w to fit number of tours, min size = 1
|
585
|
-
# todo -- max bins 100 -- if pop size over that just show first 100 tours
|
586
|
-
# todo! color bin red if new tour comes from crossover
|
794
|
+
# todo! color bin red if new tour comes from crossover? but what if > 1 tour per bin?
|
795
|
+
|
796
|
+
# note: calling make_histogram with an empty list erases the previous histogram
|
587
797
|
|
588
798
|
def make_histogram(pop)
|
589
799
|
if @@drawing.histogram.length > 0
|
590
|
-
|
591
|
-
|
592
|
-
|
593
|
-
|
594
|
-
|
595
|
-
|
596
|
-
|
597
|
-
|
598
|
-
|
599
|
-
|
600
|
-
|
601
|
-
|
602
|
-
|
603
|
-
|
800
|
+
@@drawing.histogram.each { |box| box.delete }
|
801
|
+
@@drawing.histogram.clear
|
802
|
+
end
|
803
|
+
return if pop.length == 0
|
804
|
+
x = @@histogramOptions[:x]
|
805
|
+
y = @@histogramOptions[:y]
|
806
|
+
ymax = @@histogramOptions[:ymax]
|
807
|
+
w = @@histogramOptions[:binwidth]
|
808
|
+
scale = ymax / pop[-1].cost
|
809
|
+
npb = (pop.length / @@histogramOptions[:maxbins]).ceil
|
810
|
+
nbins = pop.length / npb
|
811
|
+
(0..pop.length-1).step(npb) do |i|
|
812
|
+
h = pop[i..(i+npb-1)].inject(0.0) { |sum, tour| sum + tour.cost }
|
813
|
+
h = (h/npb)*scale
|
814
|
+
@@drawing.histogram << Canvas.rectangle(x, y+ymax-h, x+w, y+ymax, :fill => 'darkblue' )
|
815
|
+
x += w
|
816
|
+
end
|
817
|
+
@@drawing.options[:scale] = scale
|
818
|
+
@@histogramOptions[:npb] = npb
|
819
|
+
@@histogramOptions[:nbins] = nbins
|
820
|
+
@@recolor = false
|
604
821
|
end
|
605
822
|
|
606
823
|
def update_histogram(pop)
|
607
|
-
y =
|
608
|
-
ymax =
|
824
|
+
y = @@histogramOptions[:y]
|
825
|
+
ymax = @@histogramOptions[:ymax]
|
826
|
+
npb = @@histogramOptions[:npb]
|
609
827
|
scale = @@drawing.options[:scale]
|
610
828
|
@@drawing.histogram.each_with_index do |box, i|
|
611
|
-
|
829
|
+
j = i*npb
|
830
|
+
if j > pop.length
|
831
|
+
h = 0
|
832
|
+
else
|
833
|
+
h = pop[j..(j+npb-1)].inject(0.0) { |sum, tour| sum + tour.cost }
|
834
|
+
h = (h/npb)*scale
|
835
|
+
end
|
612
836
|
x0, y0, x1, y1 = box.coords
|
613
837
|
box.coords = x0, y+ymax-h, x1, y1
|
838
|
+
box.fill = 'darkblue' if @@recolor
|
839
|
+
end
|
840
|
+
end
|
841
|
+
|
842
|
+
def show_survivors(pop)
|
843
|
+
hist = @@drawing.histogram
|
844
|
+
return unless pop.length == hist.length
|
845
|
+
hist.each_with_index do |box, i|
|
846
|
+
box.fill = 'gray' if pop[i].op == :zap
|
614
847
|
end
|
848
|
+
@@recolor = true
|
615
849
|
end
|
616
850
|
|
851
|
+
def show_lineage(tour, userOptions = {} )
|
852
|
+
options = @@lineageOptions.merge(userOptions)
|
853
|
+
dt = options[:dt]
|
854
|
+
a = []
|
855
|
+
while tour.parent
|
856
|
+
a << tour
|
857
|
+
tour = tour.parent
|
858
|
+
end
|
859
|
+
init_labels(["id:", "cost:", "op:"]) if @@drawing
|
860
|
+
a.reverse.each do |x|
|
861
|
+
puts x.inspect + ": " + x.op.inspect
|
862
|
+
if @@drawing
|
863
|
+
view_tour(x)
|
864
|
+
labels = []
|
865
|
+
labels << sprintf( "id: %d", x.id )
|
866
|
+
labels << sprintf( "cost: %.2f", x.cost )
|
867
|
+
labels << sprintf( "op: %s", x.op.inspect )
|
868
|
+
update_labels(labels)
|
869
|
+
sleep(dt)
|
870
|
+
end
|
871
|
+
end
|
872
|
+
return true
|
873
|
+
end
|
874
|
+
|
617
875
|
# Values accessible to all the methods in the module
|
618
876
|
|
619
|
-
@@
|
877
|
+
@@tspDirectory = File.join(File.dirname(__FILE__), '..', 'data', 'tsp')
|
620
878
|
|
621
879
|
@@mapOptions = {
|
622
880
|
:dotColor => '#cccccc',
|
623
881
|
:dotRadius => 5.0,
|
624
882
|
}
|
625
883
|
|
884
|
+
# @@rsearchOptions = {
|
885
|
+
# :pause => 0.0,
|
886
|
+
# }
|
887
|
+
|
626
888
|
@@tourOptions = {
|
627
|
-
|
889
|
+
:popsize => 10,
|
890
|
+
:maxgen => 25,
|
891
|
+
:profiles => {
|
892
|
+
:mixed => [0.5, 0.25, 0.25],
|
893
|
+
:random => [0.0, 0.0, 0.0],
|
894
|
+
:all_small => [1.0, 0.0, 0.0],
|
895
|
+
:all_large => [0.0, 1.0, 0.0],
|
896
|
+
:all_cross => [0.0, 0.0, 1.0],
|
897
|
+
},
|
898
|
+
:distribution => :all_small,
|
899
|
+
:pause => 0.0,
|
900
|
+
}
|
901
|
+
|
902
|
+
@@histogramOptions = {
|
903
|
+
:x => 520.0,
|
904
|
+
:y => 100.0,
|
905
|
+
:ymax => 200.0,
|
906
|
+
:maxbins => 50.0,
|
907
|
+
:binwidth => 5,
|
908
|
+
}
|
909
|
+
|
910
|
+
@@lineageOptions = {
|
911
|
+
:dt => 2.0,
|
912
|
+
:first => 0,
|
913
|
+
:last => -1,
|
628
914
|
}
|
629
915
|
|
630
916
|
@@drawing = nil
|
631
|
-
|
917
|
+
@@previousPopulation = nil
|
918
|
+
|
632
919
|
end # module TSPLab
|
633
920
|
|
634
921
|
end # module RubyLabs
|
635
922
|
|
636
|
-
# Additions to the String class
|
637
923
|
|
638
|
-
|
924
|
+
module Enumerable
|
639
925
|
|
640
926
|
=begin rdoc
|
641
|
-
|
927
|
+
<tt>each_permtation</tt> is an iterator that makes all permutations of
|
928
|
+
a container in lexicographic order.
|
642
929
|
=end
|
643
930
|
|
644
|
-
def
|
645
|
-
|
646
|
-
|
647
|
-
|
648
|
-
|
649
|
-
|
650
|
-
|
651
|
-
|
652
|
-
|
653
|
-
|
654
|
-
|
655
|
-
|
656
|
-
|
657
|
-
|
658
|
-
|
659
|
-
|
660
|
-
|
661
|
-
|
662
|
-
|
931
|
+
def each_permutation
|
932
|
+
p = self.clone
|
933
|
+
n = p.length
|
934
|
+
res = []
|
935
|
+
loop do
|
936
|
+
yield p if block_given?
|
937
|
+
res << p.clone
|
938
|
+
# find largest j s.t. path[j] < path[j+1]
|
939
|
+
j = n-2
|
940
|
+
while j >= 0
|
941
|
+
break if p[j] < p[j+1]
|
942
|
+
j -= 1
|
943
|
+
end
|
944
|
+
break if j < 0
|
945
|
+
# find largest i s.t. path[j] < path[i]
|
946
|
+
i = n-1
|
947
|
+
loop do
|
948
|
+
break if p[j] < p[i]
|
949
|
+
i -= 1
|
950
|
+
end
|
951
|
+
# exchange path[j], path[i]
|
952
|
+
p[j], p[i] = p[i], p[j]
|
953
|
+
# reverse path from j+1 to end
|
954
|
+
tmp = p.slice!(j+1, n-1)
|
955
|
+
p += tmp.reverse
|
663
956
|
end
|
664
|
-
|
957
|
+
return block_given? ? nil : res
|
665
958
|
end
|
666
|
-
|
959
|
+
|
667
960
|
end
|
668
961
|
|
669
|
-
|