rubylabs 0.6.4 → 0.7.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/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