rubylabs 0.7.5 → 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
-
|