relax4 1.0.5 → 1.1.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/README.rdoc +38 -5
- data/lib/relax4.rb +133 -0
- data/lib/relax4/version.rb +1 -1
- data/test/test_relax4.rb +149 -0
- metadata +5 -5
data/README.rdoc
CHANGED
@@ -6,7 +6,8 @@ http://github.com/jdleesmiller/relax4_ruby
|
|
6
6
|
|
7
7
|
== DESCRIPTION
|
8
8
|
|
9
|
-
Ruby interface for the RELAX IV code for
|
9
|
+
Ruby interface for the RELAX IV code for minimum cost network flow problems,
|
10
|
+
which include transportation problems and assignment problems.
|
10
11
|
In the words of the code's authors:
|
11
12
|
|
12
13
|
PURPOSE - THIS ROUTINE IMPLEMENTS THE RELAXATION METHOD
|
@@ -34,7 +35,7 @@ In the words of the code's authors:
|
|
34
35
|
FOR THE MINIMUM COST FLOW PROBLEM", LIDS REPORT P-2146, MIT, NOV. 1992.
|
35
36
|
|
36
37
|
The original source can be downloaded from:
|
37
|
-
* http://elib.zib.de/pub/Packages/mathprog/mincost/relax-4
|
38
|
+
* http://elib.zib.de/pub/Packages/mathprog/mincost/relax-4
|
38
39
|
|
39
40
|
See also:
|
40
41
|
* http://www-neos.mcs.anl.gov/neos/solvers/lno:RELAX4/RELAX4.html
|
@@ -44,7 +45,10 @@ See also:
|
|
44
45
|
|
45
46
|
require 'rubygems'
|
46
47
|
require 'relax4'
|
47
|
-
|
48
|
+
|
49
|
+
A general network flow problem:
|
50
|
+
|
51
|
+
#
|
48
52
|
# From Figure 29.3 of Cormen, Leiserson, Rivest and Stein (2001).
|
49
53
|
# Introduction to Algorithms, 2nd Edition.
|
50
54
|
#
|
@@ -65,6 +69,32 @@ See also:
|
|
65
69
|
|
66
70
|
A lower-level interface is also available; see the docs for the Relax4 module.
|
67
71
|
|
72
|
+
A transportation problem:
|
73
|
+
|
74
|
+
#
|
75
|
+
# From http://www.me.utexas.edu/~jensen/models/network/net8.html
|
76
|
+
# A nil cost means that the flow is not allowed.
|
77
|
+
#
|
78
|
+
Relax4.solve_transportation_problem(
|
79
|
+
:costs => [[ 3, 1, nil],
|
80
|
+
[ 4, 2, 4],
|
81
|
+
[ nil, 3, 3]],
|
82
|
+
:demands => [ 7, 3, 5],
|
83
|
+
:supplies => [ 5, 7, 3]) #=> [[2, 3, 0], [5, 0, 2], [0, 0, 3]]
|
84
|
+
|
85
|
+
An assignment problem:
|
86
|
+
|
87
|
+
#
|
88
|
+
# From http://www.me.utexas.edu/~jensen/models/network/net9.html
|
89
|
+
# A nil cost means that the assignment is not allowed.
|
90
|
+
#
|
91
|
+
Relax4.solve_assignment_problem(:costs =>
|
92
|
+
[[ nil, 8, 6, 12, 1],
|
93
|
+
[ 15, 12, 7, nil, 10],
|
94
|
+
[ 10, nil, 5, 14, nil],
|
95
|
+
[ 12, nil, 12, 16, 15],
|
96
|
+
[ 18, 17, 14, nil, 13]]) #=> [4, 2, 3, 0, 1]
|
97
|
+
|
68
98
|
== REQUIREMENTS
|
69
99
|
|
70
100
|
Has been tested on:
|
@@ -98,10 +128,13 @@ and run
|
|
98
128
|
|
99
129
|
* there are some hard-coded parameters that could be exposed
|
100
130
|
* problem state is global (cf. FORTRAN), so can have only one instance at a time
|
101
|
-
* add some wrappers for transportation and assignment problems
|
102
131
|
|
103
132
|
== HISTORY
|
104
133
|
|
134
|
+
<em>1.1.0</em>
|
135
|
+
* added wrappers for transportation and assignment problems
|
136
|
+
* updated docs
|
137
|
+
|
105
138
|
<em>1.0.5</em>
|
106
139
|
* fix in previous version didn't quite solve the problem; new fix applied
|
107
140
|
|
@@ -138,7 +171,7 @@ following user guidelines:
|
|
138
171
|
AND THE RELAXATION METHOD. THEY SHOULD ALSO REGISTER WITH THE
|
139
172
|
AUTHORS TO RECEIVE UPDATES AND SUBSEQUENT RELEASES.
|
140
173
|
|
141
|
-
The author of the RELAX IV code is:
|
174
|
+
The main author of the RELAX IV code is:
|
142
175
|
|
143
176
|
DIMITRI P. BERTSEKAS
|
144
177
|
LABORATORY FOR INFORMATION AND DECISION SYSTEMS
|
data/lib/relax4.rb
CHANGED
@@ -158,6 +158,119 @@ module Relax4
|
|
158
158
|
end
|
159
159
|
end
|
160
160
|
|
161
|
+
#
|
162
|
+
# Solve a transportation problem.
|
163
|
+
#
|
164
|
+
# There are +m+ suppliers and +n+ consumers of some commodity. Each supplier
|
165
|
+
# has a given _supply_ which must be used, and each consumer has a given
|
166
|
+
# _demand_ which must be satisfied; it is assumed that the total supply equals
|
167
|
+
# the total demand. The cost of transporting one unit of the commodity from
|
168
|
+
# each supplier to each consumer is given, and the objective is to perform the
|
169
|
+
# transport with minimum cost. The output is a +m+ by +n+ matrix that gives
|
170
|
+
# the flow from each supplier to each consumer.
|
171
|
+
#
|
172
|
+
# @param [Hash] args named arguments
|
173
|
+
#
|
174
|
+
# @option args [Array<Array<Integer>>] :costs matrix with +m+ rows (one for
|
175
|
+
# each supplier) and +n+ columns (one for each consumer); a cost of +nil+
|
176
|
+
# means that there is no corresponding edge (transport is impossible);
|
177
|
+
# negative costs are allowed; may be non-square but must be rectangular (all
|
178
|
+
# rows must have the same size).
|
179
|
+
#
|
180
|
+
# @option args [Array<Integer>] :supplies for each supplier; size +m+; total
|
181
|
+
# demand should equal total supply, or else the problem will be infeasible
|
182
|
+
# (but you can add a dummy node to fix this).
|
183
|
+
#
|
184
|
+
# @option args [Array<Integer>] :demands for each consumer; size +n+; total
|
185
|
+
# demand should equal total supply, or else the problem will be infeasible
|
186
|
+
# (but you can add a dummy node to fix this).
|
187
|
+
#
|
188
|
+
# @option args [Boolean] :auction_init see {solve}
|
189
|
+
#
|
190
|
+
# @option args [Integer] :large see {solve}
|
191
|
+
#
|
192
|
+
# @option args [Integer] :max_cost see {solve}
|
193
|
+
#
|
194
|
+
# @return [Array<Array<Integer>>] optimal flows from each supplier to each
|
195
|
+
# consumer; size +m+ by +n+; +nil+ entries in the cost matrix will have flow 0
|
196
|
+
# here.
|
197
|
+
#
|
198
|
+
# @raise [ArgumentError] if problem inputs are invalid
|
199
|
+
#
|
200
|
+
# @raise [InfeasibleError] if problem is infeasible
|
201
|
+
#
|
202
|
+
def self.solve_transportation_problem args
|
203
|
+
args = args.dup
|
204
|
+
costs = get_arg(args, :costs)
|
205
|
+
supplies = get_arg(args, :supplies)
|
206
|
+
demands = get_arg(args, :demands)
|
207
|
+
|
208
|
+
m = supplies.size
|
209
|
+
n = demands.size
|
210
|
+
|
211
|
+
arcs = set_args_from_cost_matrix(args, costs, m, n)
|
212
|
+
args[:demands] = supplies.map{|x| -x} + demands
|
213
|
+
|
214
|
+
arc_flows = Relax4.solve(args)
|
215
|
+
|
216
|
+
# Extract output into matrix with same dims as input cost matrix.
|
217
|
+
flows = Array.new(m) { Array.new(n, 0) }
|
218
|
+
arcs.zip(arc_flows).each do |(i,j),fij|
|
219
|
+
flows[i][j] = fij
|
220
|
+
end
|
221
|
+
flows
|
222
|
+
end
|
223
|
+
|
224
|
+
#
|
225
|
+
# Solve a square assignment problem.
|
226
|
+
#
|
227
|
+
# There are +n+ agents and +n+ tasks, and each agent must be assigned to
|
228
|
+
# exactly one task. The cost of each assignment is given, and the objective is
|
229
|
+
# to find the assignment with the lowest cost.
|
230
|
+
#
|
231
|
+
# Some assignments can be made illegal by passing +nil+ in the relevant entry
|
232
|
+
# in the cost matrix. If you have more agents than tasks (or vice versa), you
|
233
|
+
# can add some columns (or rows) of zeros to the cost matrix.
|
234
|
+
#
|
235
|
+
# @param [Hash] args named arguments
|
236
|
+
#
|
237
|
+
# @option args [Array<Array<Integer>>] :costs square matrix with +n+ rows (one
|
238
|
+
# for each agent) and +n+ columns (one for each task); a cost of +nil+ means
|
239
|
+
# that the corresponding assignment is not allowed; negative costs are
|
240
|
+
# allowed.
|
241
|
+
#
|
242
|
+
# @option args [Boolean] :auction_init see {solve}
|
243
|
+
#
|
244
|
+
# @option args [Integer] :large see {solve}
|
245
|
+
#
|
246
|
+
# @option args [Integer] :max_cost see {solve}
|
247
|
+
#
|
248
|
+
# @return [Array<Integer>] zero-based index of task assigned to each agent.
|
249
|
+
#
|
250
|
+
# @raise [ArgumentError] if problem inputs are invalid
|
251
|
+
#
|
252
|
+
# @raise [InfeasibleError] if problem is infeasible
|
253
|
+
#
|
254
|
+
#
|
255
|
+
def self.solve_assignment_problem args
|
256
|
+
args = args.dup
|
257
|
+
costs = get_arg(args, :costs)
|
258
|
+
|
259
|
+
n = costs.size
|
260
|
+
|
261
|
+
arcs = set_args_from_cost_matrix(args, costs, n, n)
|
262
|
+
args[:demands] = [-1]*n + [1]*n
|
263
|
+
|
264
|
+
arc_flows = Relax4.solve(args)
|
265
|
+
|
266
|
+
# Find indexes.
|
267
|
+
assignment = Array.new(n)
|
268
|
+
arcs.zip(arc_flows).each do |(i,j),fij|
|
269
|
+
assignment[i] = j if fij > 0
|
270
|
+
end
|
271
|
+
assignment
|
272
|
+
end
|
273
|
+
|
161
274
|
#
|
162
275
|
# Raised to indicate that a problem is infeasible.
|
163
276
|
#
|
@@ -220,5 +333,25 @@ module Relax4
|
|
220
333
|
private
|
221
334
|
|
222
335
|
def self.get_arg(args, key); args[key] or raise "#{key} is required" end
|
336
|
+
|
337
|
+
#
|
338
|
+
# Set start_nodes, end_nodes and costs based on cost matrix; nil entries in
|
339
|
+
# the cost matrix are not mapped to arcs.
|
340
|
+
#
|
341
|
+
# @return Array<Array<Integer>> array of pairs of (zero-based) indices into
|
342
|
+
# costs that correspond to the created arcs; size is <tt>m*n</tt> minus the
|
343
|
+
# number of nil entries in the costs matrix
|
344
|
+
#
|
345
|
+
def self.set_args_from_cost_matrix(args, costs, m, n)
|
346
|
+
# Filter out any nil costs.
|
347
|
+
arcs = (0...m).to_a.product((0...n).to_a).delete_if {|i,j| !costs[i][j]}
|
348
|
+
|
349
|
+
# Make problem inputs.
|
350
|
+
args[:start_nodes] = arcs.map{|i,j| i+1}
|
351
|
+
args[:end_nodes] = arcs.map{|i,j| m+j+1}
|
352
|
+
args[:costs] = arcs.map{|i,j| costs[i][j]}
|
353
|
+
|
354
|
+
arcs
|
355
|
+
end
|
223
356
|
end
|
224
357
|
|
data/lib/relax4/version.rb
CHANGED
data/test/test_relax4.rb
CHANGED
@@ -56,6 +56,20 @@ class TestRelax4< Test::Unit::TestCase
|
|
56
56
|
flows.zip(prob[:costs]).map{|f,c|f*c}.inject(:+)
|
57
57
|
end
|
58
58
|
|
59
|
+
# Compute cost of flows when costs and flows are represented as matrices, and
|
60
|
+
# some of the costs may be nil.
|
61
|
+
def matrix_problem_cost prob, flows
|
62
|
+
flows = flows.flatten
|
63
|
+
costs = prob[:costs].flatten
|
64
|
+
flows.zip(costs).map{|f,c|f*c if c}.compact.inject(:+)
|
65
|
+
end
|
66
|
+
|
67
|
+
# Compute cost of given assignment.
|
68
|
+
def assignment_problem_cost prob, assignment
|
69
|
+
n = prob[:costs].size # assumed square
|
70
|
+
n.times.map{|i| prob[:costs][i][assignment[i]]}.inject(:+)
|
71
|
+
end
|
72
|
+
|
59
73
|
def test_solve_3
|
60
74
|
prob = problem_from_relax4_inp 'test/RELAX4.INP'
|
61
75
|
|
@@ -292,4 +306,139 @@ ARCS
|
|
292
306
|
Relax4.solve(prob.merge(:auction_init => true))
|
293
307
|
end
|
294
308
|
end
|
309
|
+
|
310
|
+
def test_transportation_problem_1
|
311
|
+
# From http://www.me.utexas.edu/~jensen/models/network/net8.html
|
312
|
+
prob = {
|
313
|
+
:costs => [[ 3, 1, nil],
|
314
|
+
[ 4, 2, 4],
|
315
|
+
[ nil, 3, 3]],
|
316
|
+
:demands => [ 7, 3, 5],
|
317
|
+
:supplies => [ 5, 7, 3]}
|
318
|
+
flows = Relax4.solve_transportation_problem(prob)
|
319
|
+
assert_equal 46, matrix_problem_cost(prob, flows)
|
320
|
+
assert_equal [[2, 3, 0],
|
321
|
+
[5, 0, 2],
|
322
|
+
[0, 0, 3]], flows
|
323
|
+
end
|
324
|
+
|
325
|
+
def test_transportation_problem_2
|
326
|
+
# From http://www.nios.ac.in/srsec311/opt-lp6.pdf (terminal questions)
|
327
|
+
prob = {
|
328
|
+
:costs => [[ 1, 2, 1, 4],
|
329
|
+
[ 3, 3, 2, 1],
|
330
|
+
[ 4, 2, 5, 9]],
|
331
|
+
:supplies => [ 30, 50, 20],
|
332
|
+
:demands => [ 20, 40, 30, 10]}
|
333
|
+
flows = Relax4.solve_transportation_problem(prob)
|
334
|
+
assert_equal 180, matrix_problem_cost(prob, flows)
|
335
|
+
|
336
|
+
prob = {
|
337
|
+
:costs => [[ 21, 16, 25, 13],
|
338
|
+
[ 17, 18, 14, 23],
|
339
|
+
[ 32, 17, 18, 41]],
|
340
|
+
:supplies => [ 11, 13, 19],
|
341
|
+
:demands => [ 6, 10, 12, 15]}
|
342
|
+
flows = Relax4.solve_transportation_problem(prob)
|
343
|
+
assert_equal 711, matrix_problem_cost(prob, flows)
|
344
|
+
end
|
345
|
+
|
346
|
+
def test_assignment_problem_1
|
347
|
+
# From http://www.me.utexas.edu/~jensen/models/network/net9.html
|
348
|
+
prob = {
|
349
|
+
:costs => [[ nil, 8, 6, 12, 1],
|
350
|
+
[ 15, 12, 7, nil, 10],
|
351
|
+
[ 10, nil, 5, 14, nil],
|
352
|
+
[ 12, nil, 12, 16, 15],
|
353
|
+
[ 18, 17, 14, nil, 13]]}
|
354
|
+
assignment = Relax4.solve_assignment_problem(prob)
|
355
|
+
assert_equal [4, 2, 3, 0, 1], assignment
|
356
|
+
end
|
357
|
+
|
358
|
+
def test_assignment_problem_2
|
359
|
+
# Test with no nils.
|
360
|
+
assert_equal [0, 1, 2],
|
361
|
+
Relax4.solve_assignment_problem(:costs => [[1, 2, 3],
|
362
|
+
[4, 5, 6],
|
363
|
+
[7, 8, 9]])
|
364
|
+
|
365
|
+
# Test with a row of nils; problem is infeasible.
|
366
|
+
assert_raise Relax4::InfeasibleError do
|
367
|
+
Relax4.solve_assignment_problem(:costs => [[nil, nil, nil],
|
368
|
+
[ 4, 5, 6],
|
369
|
+
[ 7, 8, 9]])
|
370
|
+
end
|
371
|
+
|
372
|
+
# Test with a column of nils; problem is infeasible.
|
373
|
+
assert_raise Relax4::InfeasibleError do
|
374
|
+
Relax4.solve_assignment_problem(:costs => [[nil, 2, 3],
|
375
|
+
[nil, 5, 6],
|
376
|
+
[nil, 8, 9]])
|
377
|
+
end
|
378
|
+
end
|
379
|
+
|
380
|
+
def test_assignment_problem_3
|
381
|
+
# From http://www.utdallas.edu/~scniu/OPRE-6201/documents/TP5-Assignment.pdf
|
382
|
+
prob = {
|
383
|
+
:costs => [[ 13, 4, 7, 6],
|
384
|
+
[ 1, 11, 5, 4],
|
385
|
+
[ 6, 7, 2, 8],
|
386
|
+
[ 1, 3, 5, 9]]}
|
387
|
+
assignment = Relax4.solve_assignment_problem(prob)
|
388
|
+
assert_equal [1, 3, 2, 0], assignment
|
389
|
+
assert_equal 11, assignment_problem_cost(prob, assignment)
|
390
|
+
end
|
391
|
+
|
392
|
+
def string_array_to_matrix xs, n
|
393
|
+
xs = xs.map{|x| x.to_i}
|
394
|
+
n.times.map{|i| xs[i*n,n]}
|
395
|
+
end
|
396
|
+
|
397
|
+
def test_assignment_problem_4
|
398
|
+
# From http://www.nios.ac.in/srsec311/opt-lp5.pdf
|
399
|
+
prob = {:costs => string_array_to_matrix(%w(
|
400
|
+
140 110 155 170 180
|
401
|
+
115 100 110 140 155
|
402
|
+
120 90 135 150 165
|
403
|
+
30 30 60 60 90
|
404
|
+
35 15 50 60 85), 5)}
|
405
|
+
assignment = Relax4.solve_assignment_problem(prob)
|
406
|
+
assert_equal [4,2,1,0,3], assignment
|
407
|
+
assert_equal 470, assignment_problem_cost(prob, assignment)
|
408
|
+
|
409
|
+
prob = {:costs => string_array_to_matrix(%w(
|
410
|
+
15 17 24 16
|
411
|
+
10 15 12 13
|
412
|
+
17 19 18 16
|
413
|
+
13 20 16 14), 4)}
|
414
|
+
assignment = Relax4.solve_assignment_problem(prob)
|
415
|
+
assert_equal [1, 2, 3, 0], assignment
|
416
|
+
assert_equal 58, assignment_problem_cost(prob, assignment)
|
417
|
+
|
418
|
+
prob = {:costs => string_array_to_matrix(%w(
|
419
|
+
8 26 17 11
|
420
|
+
14 29 5 27
|
421
|
+
40 21 20 17
|
422
|
+
19 26 24 10), 4)}
|
423
|
+
assignment = Relax4.solve_assignment_problem(prob)
|
424
|
+
assert_equal [0, 2, 1, 3], assignment
|
425
|
+
|
426
|
+
prob = {:costs => string_array_to_matrix(%w(
|
427
|
+
10 6 4 8 3
|
428
|
+
2 11 7 7 6
|
429
|
+
5 10 11 4 8
|
430
|
+
6 5 3 2 5
|
431
|
+
11 7 10 11 7), 5)}
|
432
|
+
assignment = Relax4.solve_assignment_problem(prob)
|
433
|
+
assert_equal [4, 0, 3, 2, 1], assignment
|
434
|
+
|
435
|
+
prob = {:costs => string_array_to_matrix(%w(
|
436
|
+
8 9 10 11
|
437
|
+
10 11 12 13
|
438
|
+
13 14 15 13
|
439
|
+
9 11 14 10), 4)}
|
440
|
+
assignment = Relax4.solve_assignment_problem(prob)
|
441
|
+
assert_equal 43, assignment_problem_cost(prob, assignment)
|
442
|
+
assert_equal 43, assignment_problem_cost(prob, [2, 1, 3, 0])
|
443
|
+
end
|
295
444
|
end
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: relax4
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 19
|
5
5
|
prerelease: false
|
6
6
|
segments:
|
7
7
|
- 1
|
8
|
+
- 1
|
8
9
|
- 0
|
9
|
-
|
10
|
-
version: 1.0.5
|
10
|
+
version: 1.1.0
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- John Lees-Miller
|
@@ -15,7 +15,7 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2010-
|
18
|
+
date: 2010-10-30 00:00:00 +01:00
|
19
19
|
default_executable:
|
20
20
|
dependencies: []
|
21
21
|
|
@@ -52,7 +52,7 @@ rdoc_options:
|
|
52
52
|
- --main
|
53
53
|
- README.rdoc
|
54
54
|
- --title
|
55
|
-
- relax4-1.0
|
55
|
+
- relax4-1.1.0
|
56
56
|
- --exclude
|
57
57
|
- ext
|
58
58
|
require_paths:
|