simplex 1.0.3 → 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.
- 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
|
+
[](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
|