simplex 1.0.3 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +2 -0
- data/lib/simplex.rb +134 -95
- metadata +4 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 653a4166c7a16f592147416d42e2a9e69aebc01d
|
4
|
+
data.tar.gz: a023d7fd81dd5bb510ae19c6508415fdc9ab9935
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: df013865a60100ab6056ef9df8699e2f729aa8e9778650fc33e401cfddcf3100307548c72d273c606f37ea1d7b7e7b463f6d347dd279a7bc31999cabd69633de
|
7
|
+
data.tar.gz: 9e77d9f2623751bd64c8370b1477c9cce6bc54a8ee7adb2a9c506935db613f4d1fe68108ef0c227772acc78ff8455c70ba5d4786a088cd16bb16102954b0ebed
|
data/README.md
CHANGED
@@ -2,6 +2,8 @@
|
|
2
2
|
simplex
|
3
3
|
=======
|
4
4
|
|
5
|
+
[![Build Status](https://travis-ci.org/danlucraft/simplex.png)](https://travis-ci.org/danlucraft/simplex)
|
6
|
+
|
5
7
|
A naive pure-Ruby implementation of the Simplex algorithm for solving linear programming problems. Solves maximizations in standard form.
|
6
8
|
|
7
9
|
### Why?
|
data/lib/simplex.rb
CHANGED
@@ -1,143 +1,182 @@
|
|
1
1
|
require 'matrix'
|
2
2
|
|
3
|
-
class Matrix
|
4
|
-
def []=(i, j, x)
|
5
|
-
@rows[i][j] = x
|
6
|
-
end
|
7
|
-
end
|
8
|
-
|
9
3
|
class Vector
|
10
4
|
public :[]=
|
11
5
|
end
|
12
6
|
|
13
7
|
class Simplex
|
14
|
-
|
8
|
+
DEFAULT_MAX_PIVOTS = 10_000
|
15
9
|
|
16
|
-
attr_accessor :
|
10
|
+
attr_accessor :max_pivots
|
17
11
|
|
18
12
|
def initialize(c, a, b)
|
19
|
-
@
|
13
|
+
@pivot_count = 0
|
14
|
+
@max_pivots = DEFAULT_MAX_PIVOTS
|
15
|
+
|
20
16
|
# Problem dimensions
|
21
17
|
@num_non_slack_vars = a.first.length
|
22
18
|
@num_constraints = b.length
|
23
19
|
@num_vars = @num_non_slack_vars + @num_constraints
|
24
|
-
@x = Array.new(@num_vars)
|
25
20
|
|
26
21
|
# Set up initial matrix A and vectors b, c
|
27
22
|
@c = Vector[*c.map {|c1| -1*c1 } + [0]*@num_constraints]
|
28
|
-
@a =
|
23
|
+
@a = a.map {|a1| Vector[*(a1.clone + [0]*@num_constraints)]}
|
29
24
|
@b = Vector[*b.clone]
|
30
|
-
0.upto(@num_constraints - 1) {|i| @a[i, @num_non_slack_vars + i] = 1 }
|
31
25
|
|
32
|
-
@
|
26
|
+
unless @a.all? {|a| a.size == @c.size } and @b.size == @a.length
|
27
|
+
raise ArgumentError, "Input arrays have mismatched dimensions"
|
28
|
+
end
|
29
|
+
|
30
|
+
0.upto(@num_constraints - 1) {|i| @a[i][@num_non_slack_vars + i] = 1 }
|
33
31
|
|
34
32
|
# set initial solution: all non-slack variables = 0
|
33
|
+
@x = Vector[*([0]*@num_vars)]
|
34
|
+
@basic_vars = (@num_non_slack_vars...@num_vars).to_a
|
35
35
|
update_solution
|
36
|
-
@solved = false
|
37
36
|
end
|
38
37
|
|
39
|
-
def
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
i += 1
|
44
|
-
raise "Too many iterations" if i > max_iterations
|
45
|
-
|
46
|
-
pivot_column = entering_variable_ix
|
47
|
-
pivot_row = minimum_coefficient_ratio_row_ix(pivot_column)
|
48
|
-
leaving_var = leaving_variable(pivot_row)
|
49
|
-
@basic_vars.delete(leaving_var)
|
50
|
-
|
51
|
-
# update objective
|
52
|
-
c_ratio = Rational(@c[pivot_column], @a[pivot_row, pivot_column])
|
53
|
-
@c = @c - (@a.row(pivot_row)*c_ratio)
|
54
|
-
|
55
|
-
# update pivot row
|
56
|
-
ratio = Rational(1, @a[pivot_row, pivot_column])
|
57
|
-
0.upto(@a.column_count - 1) do |column_ix|
|
58
|
-
@a[pivot_row, column_ix] = ratio * @a[pivot_row, column_ix]
|
59
|
-
end
|
60
|
-
@b[pivot_row] = ratio * @b[pivot_row]
|
61
|
-
|
62
|
-
# update A and B
|
63
|
-
0.upto(@a.row_count - 1) do |row_ix|
|
64
|
-
next if row_ix == pivot_row
|
65
|
-
ratio = @a[row_ix, pivot_column]
|
66
|
-
0.upto(@a.column_count - 1) do |column_ix|
|
67
|
-
@a[row_ix, column_ix] = @a[row_ix, column_ix] - ratio*@a[pivot_row, column_ix]
|
68
|
-
end
|
69
|
-
@b[row_ix] = @b[row_ix] - ratio*@b[pivot_row]
|
70
|
-
end
|
38
|
+
def solution
|
39
|
+
solve
|
40
|
+
current_solution
|
41
|
+
end
|
71
42
|
|
72
|
-
|
73
|
-
|
74
|
-
update_solution
|
75
|
-
end
|
76
|
-
@solved = true
|
43
|
+
def current_solution
|
44
|
+
@x.to_a[0...@num_non_slack_vars]
|
77
45
|
end
|
78
46
|
|
79
47
|
def update_solution
|
80
48
|
0.upto(@num_vars - 1) {|i| @x[i] = 0 }
|
49
|
+
|
81
50
|
@basic_vars.each do |basic_var|
|
82
|
-
|
83
|
-
|
84
|
-
coeff = @a[row_ix, basic_var]
|
85
|
-
if coeff == 1
|
86
|
-
if row_coeff_1 == nil
|
87
|
-
row_coeff_1 = row_ix
|
88
|
-
end
|
89
|
-
end
|
51
|
+
row_with_1 = row_indices.detect do |row_ix|
|
52
|
+
@a[row_ix][basic_var] == 1
|
90
53
|
end
|
91
|
-
@x[basic_var] = @b[
|
54
|
+
@x[basic_var] = @b[row_with_1]
|
92
55
|
end
|
93
56
|
end
|
94
57
|
|
95
|
-
def
|
96
|
-
|
97
|
-
|
58
|
+
def solve
|
59
|
+
while can_improve?
|
60
|
+
@pivot_count += 1
|
61
|
+
raise "Too many pivots" if @pivot_count > max_pivots
|
62
|
+
pivot
|
63
|
+
end
|
98
64
|
end
|
99
65
|
|
100
66
|
def can_improve?
|
101
|
-
!!
|
102
|
-
end
|
103
|
-
|
104
|
-
def
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
67
|
+
!!entering_variable
|
68
|
+
end
|
69
|
+
|
70
|
+
def variables
|
71
|
+
(0...@c.size).to_a
|
72
|
+
end
|
73
|
+
|
74
|
+
def entering_variable
|
75
|
+
variables.select { |var| @c[var] < 0 }.
|
76
|
+
min_by { |var| @c[var] }
|
77
|
+
end
|
78
|
+
|
79
|
+
def pivot
|
80
|
+
pivot_column = entering_variable
|
81
|
+
pivot_row = pivot_row(pivot_column)
|
82
|
+
leaving_var = basic_variable_in_row(pivot_row)
|
83
|
+
replace_basic_variable(leaving_var => pivot_column)
|
84
|
+
|
85
|
+
pivot_ratio = Rational(1, @a[pivot_row][pivot_column])
|
86
|
+
|
87
|
+
# update pivot row
|
88
|
+
@a[pivot_row] *= pivot_ratio
|
89
|
+
@b[pivot_row] = pivot_ratio * @b[pivot_row]
|
90
|
+
|
91
|
+
# update objective
|
92
|
+
@c -= @c[pivot_column] * @a[pivot_row]
|
93
|
+
|
94
|
+
# update A and B
|
95
|
+
(row_indices - [pivot_row]).each do |row_ix|
|
96
|
+
r = @a[row_ix][pivot_column]
|
97
|
+
@a[row_ix] -= r * @a[pivot_row]
|
98
|
+
@b[row_ix] -= r * @b[pivot_row]
|
114
99
|
end
|
115
|
-
|
100
|
+
|
101
|
+
update_solution
|
116
102
|
end
|
117
103
|
|
118
|
-
def
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
104
|
+
def replace_basic_variable(hash)
|
105
|
+
from, to = hash.keys.first, hash.values.first
|
106
|
+
@basic_vars.delete(from)
|
107
|
+
@basic_vars << to
|
108
|
+
@basic_vars.sort!
|
109
|
+
end
|
110
|
+
|
111
|
+
def pivot_row(column_ix)
|
112
|
+
row_ix_a_and_b = row_indices.map { |row_ix|
|
113
|
+
[row_ix, @a[row_ix][column_ix], @b[row_ix]]
|
114
|
+
}.reject { |_, a, b|
|
115
|
+
a == 0
|
116
|
+
}.reject { |_, a, b|
|
117
|
+
(b < 0 or a < 0) and !(b < 0 and a < 0) # negative sign check
|
118
|
+
}
|
119
|
+
row_ix, _, _ = *last_min_by(row_ix_a_and_b) { |_, a, b|
|
120
|
+
Rational(b, a)
|
121
|
+
}
|
122
|
+
row_ix
|
123
|
+
end
|
124
|
+
|
125
|
+
def basic_variable_in_row(pivot_row)
|
126
|
+
column_indices.detect do |column_ix|
|
127
|
+
@a[pivot_row][column_ix] == 1 and @basic_vars.include?(column_ix)
|
123
128
|
end
|
124
129
|
end
|
125
130
|
|
126
|
-
def
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
131
|
+
def row_indices
|
132
|
+
(0...@a.length).to_a
|
133
|
+
end
|
134
|
+
|
135
|
+
def column_indices
|
136
|
+
(0...@a.first.size).to_a
|
137
|
+
end
|
138
|
+
|
139
|
+
def formatted_tableau
|
140
|
+
pivot_column = entering_variable
|
141
|
+
pivot_row = pivot_row(pivot_column)
|
142
|
+
num_cols = @c.size + 1
|
143
|
+
c = formatted_values(@c.to_a)
|
144
|
+
b = formatted_values(@b.to_a)
|
145
|
+
a = @a.to_a.map {|ar| formatted_values(ar.to_a) }
|
146
|
+
a[pivot_row][pivot_column] = "*" + a[pivot_row][pivot_column]
|
147
|
+
max = (c + b + a + ["1234567"]).flatten.map(&:size).max
|
148
|
+
result = []
|
149
|
+
result << c.map {|c| c.rjust(max, " ") }
|
150
|
+
a.zip(b) do |arow, brow|
|
151
|
+
result << (arow + [brow]).map {|a| a.rjust(max, " ") }
|
152
|
+
result.last.insert(arow.length, "|")
|
153
|
+
end
|
154
|
+
lines = result.map {|b| b.join(" ") }
|
155
|
+
max_line_length = lines.map(&:length).max
|
156
|
+
lines.insert(1, "-"*max_line_length)
|
157
|
+
lines.join("\n")
|
158
|
+
end
|
159
|
+
|
160
|
+
def formatted_values(array)
|
161
|
+
array.map {|c| "%2.3f" % c }
|
162
|
+
end
|
163
|
+
|
164
|
+
# like Enumerable#min_by except if multiple values are minimum
|
165
|
+
# it returns the last
|
166
|
+
def last_min_by(array)
|
167
|
+
best_element, best_value = nil, nil
|
168
|
+
array.each do |element|
|
169
|
+
value = yield element
|
170
|
+
if !best_element || value <= best_value
|
171
|
+
best_element, best_value = element, value
|
138
172
|
end
|
139
173
|
end
|
140
|
-
|
174
|
+
best_element
|
141
175
|
end
|
176
|
+
|
177
|
+
def assert(boolean)
|
178
|
+
raise unless boolean
|
179
|
+
end
|
180
|
+
|
142
181
|
end
|
143
182
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: simplex
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0
|
4
|
+
version: 1.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Daniel Lucraft
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2013-
|
11
|
+
date: 2013-12-20 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: Naive implementation of the simplex linear programming algorithm in pure
|
14
14
|
Ruby.
|
@@ -31,7 +31,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
31
31
|
requirements:
|
32
32
|
- - '>='
|
33
33
|
- !ruby/object:Gem::Version
|
34
|
-
version:
|
34
|
+
version: 2.0.0
|
35
35
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
36
36
|
requirements:
|
37
37
|
- - '>='
|
@@ -39,7 +39,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
39
39
|
version: '0'
|
40
40
|
requirements: []
|
41
41
|
rubyforge_project:
|
42
|
-
rubygems_version: 2.0.
|
42
|
+
rubygems_version: 2.0.14
|
43
43
|
signing_key:
|
44
44
|
specification_version: 4
|
45
45
|
summary: Simplex linear programming solver
|