simplex 1.0.3 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (4) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -0
  3. data/lib/simplex.rb +134 -95
  4. metadata +4 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 5431fa9728f8db469cb36abb63ac55a373034f54
4
- data.tar.gz: a742fe78724dba49f9611a6fadcabe6f0167e81c
3
+ metadata.gz: 653a4166c7a16f592147416d42e2a9e69aebc01d
4
+ data.tar.gz: a023d7fd81dd5bb510ae19c6508415fdc9ab9935
5
5
  SHA512:
6
- metadata.gz: ac3edb3eaeda1411bec67c29f2a556d67ad14dee695e76a0c5470733b0611094665d1cbc2799b25a6a7e51a0f20d4a9c7ef2562893767054fe059c325c0b2143
7
- data.tar.gz: e63deec02516c54b58bef985fafd584ade2003815d7c2b4201837459eedd82408b112e8c9e7f4b3397aeb7940ac1357cfbb508cfb549c7b5e3b52353ac1ac9e1
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?
@@ -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
- DEFAULT_MAX_ITERATIONS = 10_000
8
+ DEFAULT_MAX_PIVOTS = 10_000
15
9
 
16
- attr_accessor :max_iterations
10
+ attr_accessor :max_pivots
17
11
 
18
12
  def initialize(c, a, b)
19
- @max_iterations = DEFAULT_MAX_ITERATIONS
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 = Matrix[*a.map {|a1| a1.clone + [0]*@num_constraints}]
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
- @basic_vars = ((@num_non_slack_vars)...(@num_vars)).to_a
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 solve
40
- return if @solved
41
- i = 0
42
- while can_improve?
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
- @basic_vars << pivot_column
73
- @basic_vars.sort!
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
- row_coeff_1 = nil
83
- 0.upto(@a.row_count - 1) do |row_ix|
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[row_coeff_1]
54
+ @x[basic_var] = @b[row_with_1]
92
55
  end
93
56
  end
94
57
 
95
- def solution
96
- solve
97
- @x.to_a[0...@num_non_slack_vars]
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
- !!entering_variable_ix
102
- end
103
-
104
- def entering_variable_ix
105
- current_min_value = nil
106
- current_min_index = nil
107
- @c.each_with_index do |v, i|
108
- if v < 0
109
- if current_min_value == nil || v < current_min_value
110
- current_min_value = v
111
- current_min_index = i
112
- end
113
- end
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
- current_min_index
100
+
101
+ update_solution
116
102
  end
117
103
 
118
- def leaving_variable(pivot_row)
119
- 0.upto(@a.column_count - 1) do |column_ix|
120
- if @a[pivot_row, column_ix] == 1 and @basic_vars.include?(column_ix)
121
- return column_ix
122
- end
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 minimum_coefficient_ratio_row_ix(column_ix)
127
- current_min_value = nil
128
- current_min_index = nil
129
- 0.upto(@a.row_count - 1) do |row_ix|
130
- next if @a[row_ix, column_ix] == 0
131
- b_val = @b[row_ix]
132
- a_val = @a[row_ix, column_ix]
133
- ratio = Rational(b_val, a_val)
134
- is_negative = (@b[row_ix] < 0 || @a[row_ix, column_ix] < 0) && !(@b[row_ix] < 0 && @a[row_ix, column_ix] < 0)
135
- if !is_negative && (!current_min_value || ratio <= current_min_value)
136
- current_min_value = ratio
137
- current_min_index = row_ix
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
- return current_min_index
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.3
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-09-17 00:00:00.000000000 Z
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: '0'
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.3
42
+ rubygems_version: 2.0.14
43
43
  signing_key:
44
44
  specification_version: 4
45
45
  summary: Simplex linear programming solver