rubylabs 0.6.4 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/tsplab.rb CHANGED
@@ -12,7 +12,11 @@
12
12
  TODO plot space: get all 180K costs, order lexicographically, plot (smoothed)
13
13
  =end
14
14
 
15
- require 'set'
15
+ module RubyLabs
16
+
17
+ module TSPLab
18
+
19
+ require 'set'
16
20
 
17
21
  =begin rdoc
18
22
 
@@ -33,7 +37,7 @@ minutes.
33
37
  # distance(x,y) returns the distance between cities with keys x and y
34
38
 
35
39
 
36
- class CityList
40
+ class CityList
37
41
 
38
42
  =begin rdoc
39
43
  The lines in the input file passed to the constructor come from a
@@ -43,130 +47,101 @@ city 1, city 2, days, hours, minutes, all separated by tabs. Each
43
47
  city is assigned a one-letter key.
44
48
  =end
45
49
 
46
- def initialize(fn)
47
- raise "CityList.new: must specify city file" unless fn != nil
48
-
49
- @key = ?A
50
- @cities = Hash.new # initially a map from name string to one-letter key
51
- @matrix = Hash.new
52
-
53
- File.open(fn).each do |line|
54
- next if line.chomp.length == 0
55
- rec = line.split(/\t/)
56
- i = getKey(rec[0])
57
- j = getKey(rec[1])
58
- dist = ((24 * getValue(rec[2]) + getValue(rec[3])) * 60) + getValue(rec[4])
59
- assign(i,j,dist)
60
- assign(j,i,dist)
61
- end
50
+ def initialize(matrix)
51
+ matrixfilename = matrix.to_s + ".txt"
52
+ matrixfilename = File.join(@@dataDirectory, matrixfilename)
53
+ raise "Matrix not found: #{matrixfilename}" unless File.exists?(matrixfilename)
54
+
55
+ @key = ?A
56
+ @cities = Hash.new # initially a map from name string to one-letter key
57
+ @matrix = Hash.new
58
+
59
+ File.open(matrixfilename).each do |line|
60
+ next if line.chomp.length == 0
61
+ rec = line.split(/\t/)
62
+ i = getKey(rec[0])
63
+ j = getKey(rec[1])
64
+ dist = ((24 * getValue(rec[2]) + getValue(rec[3])) * 60) + getValue(rec[4])
65
+ assign(i,j,dist)
66
+ assign(j,i,dist)
67
+ end
62
68
 
63
- @cities = @cities.invert # from now on a map from key to name string
69
+ @cities = @cities.invert # from now on a map from key to name string
64
70
 
65
- errs = 0
66
- (?A..@key-1).each do |i|
67
- (i+1..@key-1).each do |j|
68
- unless @matrix[i][j] && @matrix[j][i]
69
- errs += 1
70
- puts "CityList.new: missing distance between #{@cities[i]} and #{@cities[j]}"
71
+ errs = 0
72
+ (?A..@key-1).each do |i|
73
+ (i+1..@key-1).each do |j|
74
+ unless @matrix[i][j] && @matrix[j][i]
75
+ errs += 1
76
+ puts "CityList.new: missing distance between #{@cities[i]} and #{@cities[j]}"
77
+ end
71
78
  end
72
79
  end
73
- end
74
- raise "CityList.new: errors in input file" if errs > 0
80
+ raise "CityList.new: errors in input file" if errs > 0
75
81
 
76
- end
82
+ end
77
83
 
78
- def inspect()
79
- n = @matrix.length
80
- return "List of #{n} cities"
81
- end
84
+ def inspect()
85
+ n = @matrix.length
86
+ return sprintf "TSPLab::CityList (%d x %d)", n, n-1
87
+ end
82
88
 
83
- def size()
84
- return @key - ?A
85
- end
89
+ def size()
90
+ return @key - ?A
91
+ end
86
92
 
87
- def keys()
88
- return @cities.keys.sort.map{|x| x.chr}
89
- end
93
+ def keys()
94
+ return @cities.keys.sort.map{|x| x.chr}
95
+ end
90
96
 
91
- def values()
92
- return @cities.keys.sort.collect{|x| @cities[x]}
93
- end
97
+ def values()
98
+ return @cities.keys.sort.collect{|x| @cities[x]}
99
+ end
94
100
 
95
- def [](x)
96
- x = x[0] if x.class == String
97
- raise "unknown key: #{x}" unless x >= ?A && x < @key
98
- return @cities[x]
99
- end
101
+ def [](x)
102
+ x = x[0] if x.class == String
103
+ return nil unless x >= ?A && x < @key
104
+ return @cities[x]
105
+ end
100
106
 
101
- def distance(x,y)
102
- x = x[0] if x.class == String
103
- y = y[0] if y.class == String
104
- raise "unknowns key: #{x}" unless x >= ?A && x < @key
105
- raise "unknown key: #{y}" unless y >= ?A && y < @key
106
- return @matrix[x][y]
107
- end
107
+ def distance(x,y)
108
+ x = x[0] if x.class == String
109
+ y = y[0] if y.class == String
110
+ return nil unless x >= ?A && x < @key
111
+ return nil unless y >= ?A && y < @key
112
+ return @matrix[x][y]
113
+ end
108
114
 
109
115
  # private methods -- return the key for a city (make a new one if necessary),
110
116
  # get a time value from an input line, save a distance
111
117
 
112
- private
113
-
114
- def getKey(s)
115
- if @cities[s] == nil
116
- @cities[s] = @key
117
- @key += 1
118
- end
119
- return @cities[s]
120
- end
121
-
122
- def getValue(s)
123
- a = s.scan(/(\d+)/)
124
- if a.length == 1
125
- return a[0][0].to_i
126
- else
127
- return 0
128
- end
129
- end
118
+ private
130
119
 
131
- def assign(i,j,v)
132
- if @matrix[i] == nil
133
- @matrix[i] = Hash.new
120
+ def getKey(s)
121
+ if @cities[s] == nil
122
+ @cities[s] = @key
123
+ @key += 1
124
+ end
125
+ return @cities[s]
134
126
  end
135
- @matrix[i][j] = v
136
- end
137
-
138
- end
139
127
 
140
- class String
141
-
142
- =begin rdoc
143
- Call <tt>s.permute</tt> to scramble the characters in string +s+.
144
- =end
145
-
146
- def permute
147
- for i in 0..length-2
148
- r = rand(length-i) + i # i <= r < length
149
- self[i],self[r] = self[r],self[i]
128
+ def getValue(s)
129
+ a = s.scan(/(\d+)/)
130
+ if a.length == 1
131
+ return a[0][0].to_i
132
+ else
133
+ return 0
134
+ end
150
135
  end
151
- self
152
- end
153
-
154
-
155
- =begin rdoc
156
- Call <tt>s.rotate</tt> to "rotate" the characters in string +s+, i.e. detach
157
- the last character in +s+ and reinsert it at the front.
158
- =end
159
136
 
160
- def rotate(n)
161
- if n > 0
162
- tail = self[n..-1]
163
- self[n..-1] = ""
164
- self.insert(0,tail)
137
+ def assign(i,j,v)
138
+ if @matrix[i] == nil
139
+ @matrix[i] = Hash.new
140
+ end
141
+ @matrix[i][j] = v
165
142
  end
166
- self
167
- end
168
143
 
169
- end
144
+ end # class CityList
170
145
 
171
146
  =begin rdoc
172
147
 
@@ -181,129 +156,129 @@ inherited from a parent, and incremented whenever a mutation or crossover is app
181
156
  =end
182
157
 
183
158
 
184
- class Tour
185
- attr_reader :id, :path, :cost, :matrix, :nm, :nx
159
+ class Tour
160
+ attr_reader :id, :path, :cost, :matrix, :nm, :nx
186
161
 
187
- @@id = 0
162
+ @@id = 0
188
163
 
189
- def initialize(m, s = nil, nm = 0, nx = 0)
190
- if s == nil
191
- @path = m.keys.to_s.permute
192
- else
193
- skeys = Set.new(m.keys)
194
- used = Set.new
195
- s.each_byte do |x|
196
- raise "Tour.new: invalid character in tour: #{x}" unless skeys.member?(x.chr)
197
- raise "Tour.new: duplicate character in tour: #{x.chr}" if used.member?(x)
198
- used.add(x)
164
+ def initialize(m, s = nil, nm = 0, nx = 0)
165
+ if s == nil
166
+ @path = m.keys.to_s.permute!
167
+ else
168
+ skeys = Set.new(m.keys)
169
+ used = Set.new
170
+ s.each_byte do |x|
171
+ raise "Tour.new: invalid character in tour: #{x}" unless skeys.member?(x.chr)
172
+ raise "Tour.new: duplicate character in tour: #{x.chr}" if used.member?(x)
173
+ used.add(x)
174
+ end
175
+ @path = s.clone
199
176
  end
200
- @path = s.clone
177
+ @cost = pathcost(m,@path)
178
+ @matrix = m # need matrix to update cost after mutation...
179
+ @id = @@id
180
+ @nm = nm
181
+ @nx = nx
182
+ @@id += 1
201
183
  end
202
- @cost = pathcost(m,@path)
203
- @matrix = m # need matrix to update cost after mutation...
204
- @id = @@id
205
- @nm = nm
206
- @nx = nx
207
- @@id += 1
208
- end
209
184
 
210
- # An alterntaive constructor -- either copy a single parent and add
211
- # a point mutation, or cross two parents
185
+ # An alterntaive constructor -- either copy a single parent and add
186
+ # a point mutation, or cross two parents
212
187
 
213
- def Tour.reproduce(x, y = nil, trace = nil)
214
- if y.nil? || y == :trace
215
- i = rand(x.path.length)
216
- j = (i+1) % x.path.length
217
- trace = :trace if y == :trace # handle calls like reproduce(x, :trace)
218
- x.clone.mutate(i, j, trace)
219
- else
220
- i = rand(x.path.length)
221
- loop { j = rand(x.path.length); break if j != i }
222
- i, j = j, i if i > j
223
- x.clone.cross(y, i, j, trace)
224
- end
225
- end
226
-
227
- def to_s()
228
- return "\##{@id}: #{@path} / #{@cost}"
229
- end
188
+ def Tour.reproduce(x, y = nil, trace = nil)
189
+ if y.nil? || y == :trace
190
+ i = rand(x.path.length)
191
+ j = (i+1) % x.path.length
192
+ trace = :trace if y == :trace # handle calls like reproduce(x, :trace)
193
+ x.clone.mutate(i, j, trace)
194
+ else
195
+ i = rand(x.path.length)
196
+ loop { j = rand(x.path.length); break if j != i }
197
+ i, j = j, i if i > j
198
+ x.clone.cross(y, i, j, trace)
199
+ end
200
+ end
201
+
202
+ def to_s()
203
+ return "\##{@id}: #{@path} / #{@cost}"
204
+ end
230
205
 
231
- def inspect
232
- return to_s
233
- end
206
+ def inspect
207
+ return to_s
208
+ end
234
209
 
235
- # Reset the id to 0 (e.g. to count number of objects)
210
+ # Reset the id to 0 (e.g. to count number of objects)
236
211
 
237
- def Tour.reset
238
- @@id = 0
239
- end
212
+ def Tour.reset
213
+ @@id = 0
214
+ end
240
215
 
241
- def Tour.count
242
- @@id
243
- end
216
+ def Tour.count
217
+ @@id
218
+ end
244
219
 
245
- # A copy of a tour needs its own new id and copy of the path and pedigree
220
+ # A copy of a tour needs its own new id and copy of the path and pedigree
246
221
 
247
- def clone
248
- return Tour.new(@matrix, @path, @nm, @nx)
249
- end
222
+ def clone
223
+ return Tour.new(@matrix, @path, @nm, @nx)
224
+ end
250
225
 
251
- # Exchange mutation (called EM by Larranaga et al). Simply swaps two
252
- # cities at the specified locations.
226
+ # Exchange mutation (called EM by Larranaga et al). Simply swaps two
227
+ # cities at the specified locations.
253
228
 
254
- def mutate(i, j, trace = nil)
255
- puts "mutate: #{i} <-> #{j}" if ! trace.nil?
256
- @path[i], @path[j] = @path[j], @path[i]
257
- @cost = pathcost(@matrix,@path)
258
- @nm += 1
259
- self
260
- end
229
+ def mutate(i, j, trace = nil)
230
+ puts "mutate: #{i} <-> #{j}" if ! trace.nil?
231
+ @path[i], @path[j] = @path[j], @path[i]
232
+ @cost = pathcost(@matrix,@path)
233
+ @nm += 1
234
+ self
235
+ end
261
236
 
262
- # Order cross-over (called OX1 by Larranaga et al). Save a chunk of the
263
- # current tour, then copy the remaining cities in the order they occur in
264
- # tour t.
237
+ # Order cross-over (called OX1 by Larranaga et al). Save a chunk of the
238
+ # current tour, then copy the remaining cities in the order they occur in
239
+ # tour t.
265
240
 
266
- def cross(t, i, j, trace = nil)
267
- puts "cross: copy #{i}..#{j}" if ! trace.nil?
268
- @path = @path[i..j]
269
- t.path.each_byte do |c|
270
- @path << c unless @path.include?(c)
241
+ def cross(t, i, j, trace = nil)
242
+ puts "cross: copy #{i}..#{j}" if ! trace.nil?
243
+ @path = @path[i..j]
244
+ t.path.each_byte do |c|
245
+ @path << c unless @path.include?(c)
246
+ end
247
+ @cost = pathcost(@matrix,@path)
248
+ @nx += 1 + t.nx
249
+ @nm += t.nm
250
+ self
271
251
  end
272
- @cost = pathcost(@matrix,@path)
273
- @nx += 1 + t.nx
274
- @nm += t.nm
275
- self
276
- end
277
252
 
278
- # private methods -- compute the cost of a path, given a matrix; apply
279
- # mutations (point mutations, cross-overs)
253
+ # private methods -- compute the cost of a path, given a matrix; apply
254
+ # mutations (point mutations, cross-overs)
280
255
 
281
- private
256
+ private
282
257
 
283
- def pathcost(m,s)
284
- sum = m.distance(s[0],s[-1]) # link from end to start
285
- for i in 0..s.length-2
286
- sum += m.distance(s[i],s[i+1])
258
+ def pathcost(m,s)
259
+ sum = m.distance(s[0],s[-1]) # link from end to start
260
+ for i in 0..s.length-2
261
+ sum += m.distance(s[i],s[i+1])
262
+ end
263
+ return sum
287
264
  end
288
- return sum
289
- end
290
265
 
291
- end
266
+ end # class Tour
292
267
 
293
268
  # Make a population of size n -- n instances of tours made from a set
294
269
  # of cities
295
270
 
296
- def initPopulation(n,m)
297
- a = Array.new
271
+ def initPopulation(n,m)
272
+ a = Array.new
298
273
 
299
- n.times do
300
- a.push(Tour.new(m))
301
- end
274
+ n.times do
275
+ a.push(Tour.new(m))
276
+ end
302
277
 
303
- a.sort! { |x,y| x.cost <=> y.cost }
278
+ a.sort! { |x,y| x.cost <=> y.cost }
304
279
 
305
- return a
306
- end
280
+ return a
281
+ end
307
282
 
308
283
  # Evolve a population. Sort by fitness, then remove individuals with
309
284
  # a probability based on their ranking (the ith individual survives
@@ -312,105 +287,162 @@ end
312
287
  # argument is a collection of parameters that can override various
313
288
  # options (e.g. the probability of survival).
314
289
 
315
- def evolve(p, param={})
316
- debug = param[:trace] ? true : false
317
- pcross = param[:pcross] ? param[:pcross] : 0.25
318
- plotci = param[:plotci]
319
-
320
- p.sort! { |x,y| x.cost <=> y.cost }
321
- i = 0
322
- n = p.length
323
-
324
- if plotci
325
- m = 0.0
326
- p.each { |t| m += t.cost }
327
- m = m / p.length
328
- printf "%.2f %.2f %.2f\n", m, p[0].cost, p[-1].cost
329
- end
330
-
331
- # phase 1 -- delete random tours
332
-
333
- while i < p.length
334
- pk = (n-i).to_f / n
335
- if rand < pk
336
- puts "keep #{p[i]}" if debug
337
- i += 1 # keep this tour, skip to next one
338
- else
339
- puts "zap #{p[i]}" if debug
340
- p.delete_at(i) # zap this one; keep i at same value
290
+ def evolve(p, param={})
291
+ debug = param[:trace] ? true : false
292
+ pcross = param[:pcross] ? param[:pcross] : 0.25
293
+ plotci = param[:plotci]
294
+
295
+ p.sort! { |x,y| x.cost <=> y.cost }
296
+ i = 0
297
+ n = p.length
298
+
299
+ if plotci
300
+ m = 0.0
301
+ p.each { |t| m += t.cost }
302
+ m = m / p.length
303
+ printf "%.2f %.2f %.2f\n", m, p[0].cost, p[-1].cost
304
+ end
305
+
306
+ # phase 1 -- delete random tours
307
+
308
+ while i < p.length
309
+ pk = (n-i).to_f / n
310
+ if rand < pk
311
+ puts "keep #{p[i]}" if debug
312
+ i += 1 # keep this tour, skip to next one
313
+ else
314
+ puts "zap #{p[i]}" if debug
315
+ p.delete_at(i) # zap this one; keep i at same value
316
+ end
341
317
  end
342
- end
343
318
 
344
- # phase 2 -- build back up to n tours; prev is the index of the last
345
- # tour from the previous generation (candidates for cloning/crossing)
346
-
347
- best = p[0]
348
- prev = p.length
349
-
350
- while p.length < n
351
- mom = p[rand(prev)]
352
- if rand < pcross
353
- dad = p[rand(prev)]
354
- kid = Tour.reproduce(mom,dad)
355
- puts "#{mom} x #{dad} => #{kid}" if debug
356
- else
357
- kid = Tour.reproduce(mom)
358
- puts "#{mom} => #{kid}" if debug
319
+ # phase 2 -- build back up to n tours; prev is the index of the last
320
+ # tour from the previous generation (candidates for cloning/crossing)
321
+
322
+ best = p[0]
323
+ prev = p.length
324
+
325
+ while p.length < n
326
+ mom = p[rand(prev)]
327
+ if rand < pcross
328
+ dad = p[rand(prev)]
329
+ kid = Tour.reproduce(mom,dad)
330
+ puts "#{mom} x #{dad} => #{kid}" if debug
331
+ else
332
+ kid = Tour.reproduce(mom)
333
+ puts "#{mom} => #{kid}" if debug
334
+ end
335
+ p.push(kid)
336
+ best = kid if kid.cost < best.cost
359
337
  end
360
- p.push(kid)
361
- best = kid if kid.cost < best.cost
362
- end
363
338
 
364
- return best
339
+ return best
365
340
 
366
- end
341
+ end
367
342
 
368
343
  # High level interface -- make a population for a set of cities, then
369
344
  # call evolve() until the best tour doesn't change after some number of
370
345
  # iterations (also passed as a parameter). Print the tour when done.
371
346
 
372
- def bestTour(matrix, param={})
373
- popsize = param[:popsize] ? param[:popsize] : 10
374
- maxgen = param[:maxgen] ? param[:maxgen] : 25
375
- maxstatic = param[:maxstatic] ? param[:maxstatic] : maxgen/2
347
+ def bestTour(matrix, param={})
348
+ popsize = param[:popsize] ? param[:popsize] : 10
349
+ maxgen = param[:maxgen] ? param[:maxgen] : 25
350
+ maxstatic = param[:maxstatic] ? param[:maxstatic] : maxgen/2
351
+
352
+ p = initPopulation(popsize,matrix)
353
+ best = p[0]
354
+ nstatic = 0
355
+ ngen = 0
376
356
 
377
- p = initPopulation(popsize,matrix)
378
- best = p[0]
379
- nstatic = 0
380
- ngen = 0
357
+ if param[:verbose]
358
+ puts "popsize: #{popsize}"
359
+ puts "maxgen: #{maxgen}"
360
+ puts "maxstatic: #{maxstatic}"
361
+ puts "pcross: #{param[:pcross]}"
362
+ end
363
+
364
+ while ngen < maxgen && nstatic < maxstatic
365
+ puts "best tour: #{p[0]}" if param[:verbose]
366
+ evolve(p, param)
367
+ if p[0].cost < best.cost
368
+ best = p[0]
369
+ nstatic = 0
370
+ else
371
+ nstatic += 1
372
+ end
373
+ ngen += 1
374
+ end
375
+
376
+ # puts "#{ngen} generations (#{nstatic} at best value)"
377
+ puts "#{ngen} generations"
381
378
 
382
- if param[:verbose]
383
- puts "popsize: #{popsize}"
384
- puts "maxgen: #{maxgen}"
385
- puts "maxstatic: #{maxstatic}"
386
- puts "pcross: #{param[:pcross]}"
379
+ if param[:verbose]
380
+ puts "best cost: #{best.cost}"
381
+ puts "pedigree: #{best.nx} cross #{best.nm} mutate"
382
+ puts "best tour:"
383
+ best.path.each_byte do |x|
384
+ puts " " + matrix[x]
385
+ end
386
+ end
387
+
388
+ return best
387
389
  end
388
390
 
389
- while ngen < maxgen && nstatic < maxstatic
390
- puts "best tour: #{p[0]}" if param[:verbose]
391
- evolve(p, param)
392
- if p[0].cost < best.cost
393
- best = p[0]
394
- nstatic = 0
395
- else
396
- nstatic += 1
391
+ def checkout(matrix, filename = nil)
392
+ matrixfilename = matrix.to_s + ".txt"
393
+ matrixfilename = File.join(@@dataDirectory, matrixfilename)
394
+ if !File.exists?(matrixfilename)
395
+ puts "Matrix not found: #{matrixfilename}"
396
+ return nil
397
397
  end
398
- ngen += 1
398
+ outfilename = filename.nil? ? (matrix.to_s + ".txt") : filename
399
+ dest = File.open(outfilename, "w")
400
+ File.open(matrixfilename).each do |line|
401
+ dest.puts line.chomp
402
+ end
403
+ dest.close
404
+ puts "Copy of #{matrix} saved in #{outfilename}"
399
405
  end
406
+
407
+ # Values accessible to all the methods in the module
408
+
409
+ @@dataDirectory = File.join(File.dirname(__FILE__), '..', 'data', 'tsp')
410
+
411
+ end # module TSPLab
412
+
413
+ end # module RubyLabs
414
+
415
+ # Additions to the String class
400
416
 
401
- # puts "#{ngen} generations (#{nstatic} at best value)"
402
- puts "#{ngen} generations"
417
+ class String
403
418
 
404
- if param[:verbose]
405
- puts "best cost: #{best.cost}"
406
- puts "pedigree: #{best.nx} cross #{best.nm} mutate"
407
- puts "best tour:"
408
- best.path.each_byte do |x|
409
- puts " " + matrix[x]
419
+ =begin rdoc
420
+ Call <tt>s.permute</tt> to scramble the characters in string +s+.
421
+ =end
422
+
423
+ def permute!
424
+ for i in 0..length-2
425
+ r = rand(length-i) + i # i <= r < length
426
+ self[i],self[r] = self[r],self[i]
410
427
  end
428
+ self
429
+ end
430
+
431
+
432
+ =begin rdoc
433
+ Call <tt>s.rotate</tt> to "rotate" the characters in string +s+, i.e. detach
434
+ the last character in +s+ and reinsert it at the front.
435
+ =end
436
+
437
+ def rotate!(n)
438
+ if n > 0
439
+ tail = self[n..-1]
440
+ self[n..-1] = ""
441
+ self.insert(0,tail)
442
+ end
443
+ self
411
444
  end
412
445
 
413
- return best
414
446
  end
415
447
 
416
448