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.
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