Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
70bb0c9
add benchmark/ips: 460 ips
rickhull Oct 31, 2017
e48c7ec
use t.pattern and enable warnings
rickhull Oct 30, 2017
15c85ce
stop manipulating load path in code
rickhull Oct 30, 2017
d53f7d9
fix warnings about unused locals
rickhull Oct 30, 2017
b88eab0
fix warnings about unused or shadow local vars
rickhull Oct 30, 2017
60b2edf
remove trailing whitespace
rickhull Oct 30, 2017
7ee8a0d
convert from test/unit to minitest/test
rickhull Oct 30, 2017
38b20c4
remove unused Simplex#assert; add comments
rickhull Oct 30, 2017
5dfc795
drop @pivot_count
rickhull Oct 30, 2017
7c35aa3
simplify initalize; make @x an Array rather than Vector
rickhull Oct 30, 2017
e6db1c5
cleanup
rickhull Oct 30, 2017
f5cb8eb
drop Simplex#variables and #replace_basic_variable
rickhull Oct 30, 2017
2449f22
drop Gemfile and update travis
rickhull Oct 30, 2017
919d784
rename to test/simplex.rb
rickhull Oct 30, 2017
20a9484
drop #basic_variable_in_row
rickhull Oct 30, 2017
9f5f9a5
use self.instance_method when calling instance methods
rickhull Oct 30, 2017
cadb517
add comments
rickhull Oct 30, 2017
e6c420c
cleanup; check for non-nil self.entering_variable
rickhull Oct 30, 2017
3c3c2e7
use self.instance method and fix some block parameters
rickhull Oct 30, 2017
3516aae
TODO: investigate conditional
rickhull Oct 30, 2017
7950c49
drop #formatted_values
rickhull Oct 30, 2017
19ab1ae
make last_min_by a class method
rickhull Oct 30, 2017
58b2f89
just call reject once
rickhull Oct 30, 2017
59b2ae9
drop last_min_by
rickhull Oct 30, 2017
11d7239
drop #row_indices
rickhull Oct 30, 2017
a24525b
prefer @num_constraints to @a.size
rickhull Oct 30, 2017
6f0e0a2
drop #column_indices
rickhull Oct 30, 2017
20e81aa
reorder some method definitions
rickhull Oct 30, 2017
433c05c
Exceptions: add SanityCheck and TooManyPivots; inherit from RuntimeError
rickhull Oct 30, 2017
e1617c8
introduce Simplex::Error and inherit from it
rickhull Oct 30, 2017
97ffa3d
benchmark/ips: 546 ips (was 460)
rickhull Oct 31, 2017
f58fe51
s/Vector/Array/g
rickhull Oct 31, 2017
daf4e25
add Simplex::Parse module
rickhull Oct 30, 2017
d59592b
add tokenizing, expression and inequality parsing, and some regexen
rickhull Oct 30, 2017
6808fe4
introduce Parse::Error and use it
rickhull Oct 30, 2017
35bc7d7
reformat for readability
rickhull Oct 30, 2017
f75d70b
add Simplex.maximize
rickhull Oct 31, 2017
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
language: ruby
rvm:
- 1.9.3
- 2.0.0
- jruby
- 2.2
- 2.3
- 2.4
- ruby
- ruby-head
- jruby-9.1.9
install: gem install minitest
script: rake
6 changes: 0 additions & 6 deletions Gemfile

This file was deleted.

12 changes: 0 additions & 12 deletions Gemfile.lock

This file was deleted.

18 changes: 15 additions & 3 deletions Rakefile
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
require 'rake/testtask'

task :default => :test

Rake::TestTask.new do |t|
t.test_files = FileList['test/**/*_test.rb']
t.pattern = ['test/*.rb']
t.warning = true
end

Rake::TestTask.new(bench: :loadavg) do |t|
t.pattern = ['test/bench/*.rb']
t.warning = true
t.description = "Run benchmarks"
end

desc "Show current system load"
task :loadavg do
puts "/proc/loadavg %s" % (File.read("/proc/loadavg") rescue "Unavailable")
end

task :default => :test
226 changes: 99 additions & 127 deletions lib/simplex.rb
Original file line number Diff line number Diff line change
@@ -1,192 +1,164 @@
require 'matrix'

class Vector
public :[]=
end

class Simplex
DEFAULT_MAX_PIVOTS = 10_000

class UnboundedProblem < StandardError
end
class Error < RuntimeError; end
class UnboundedProblem < Error; end
class SanityCheck < Error; end
class TooManyPivots < Error; end

attr_accessor :max_pivots

# c - coefficients of objective function; size: num_vars
# a - inequality lhs coefficients; 2dim size: num_inequalities, num_vars
# b - inequality rhs constants size: num_inequalities
def initialize(c, a, b)
@pivot_count = 0
num_vars = c.size
num_inequalities = b.size
raise(ArgumentError, "a doesn't match b") unless a.size == num_inequalities
raise(ArgumentError, "a doesn't match c") unless a.first.size == num_vars

@max_pivots = DEFAULT_MAX_PIVOTS

# Problem dimensions
@num_non_slack_vars = a.first.length
@num_constraints = b.length
# Problem dimensions; these never change
@num_non_slack_vars = num_vars
@num_constraints = num_inequalities
@num_vars = @num_non_slack_vars + @num_constraints

# Set up initial matrix A and vectors b, c
@c = Vector[*c.map {|c1| -1*c1 } + [0]*@num_constraints]
@a = a.map {|a1| Vector[*(a1.clone + [0]*@num_constraints)]}
@b = Vector[*b.clone]

unless @a.all? {|a| a.size == @c.size } and @b.size == @a.length
raise ArgumentError, "Input arrays have mismatched dimensions"
end

0.upto(@num_constraints - 1) {|i| @a[i][@num_non_slack_vars + i] = 1 }
@c = c.map { |flt| -1 * flt } + Array.new(@num_constraints, 0)
@a = a.map.with_index { |ary, i|
if ary.size != @num_non_slack_vars
raise ArgumentError, "a is inconsistent"
end
ary + Array.new(@num_constraints) { |ci| ci == i ? 1 : 0 }
}
@b = b

# set initial solution: all non-slack variables = 0
@x = Vector[*([0]*@num_vars)]
@basic_vars = (@num_non_slack_vars...@num_vars).to_a
update_solution
end

def solution
solve
current_solution
end

def current_solution
@x.to_a[0...@num_non_slack_vars]
self.update_solution
end

# does not modify vector / matrix
def update_solution
0.upto(@num_vars - 1) {|i| @x[i] = 0 }
@x = Array.new(@num_vars, 0)

@basic_vars.each { |basic_var|
idx = nil
@num_constraints.times { |i|
if @a[i][basic_var] == 1
idx =i
break
end
}
raise(SanityCheck, "no idx for basic_var #{basic_var} in a") unless idx
@x[basic_var] = @b[idx]
}
end

@basic_vars.each do |basic_var|
row_with_1 = row_indices.detect do |row_ix|
@a[row_ix][basic_var] == 1
end
@x[basic_var] = @b[row_with_1]
end
def solution
self.solve
self.current_solution
end

def solve
while can_improve?
@pivot_count += 1
raise "Too many pivots" if @pivot_count > max_pivots
pivot
count = 0
while self.can_improve?
count += 1
raise(TooManyPivots, count.to_s) unless count < @max_pivots
self.pivot
end
end

def can_improve?
!!entering_variable
def current_solution
@x[0...@num_non_slack_vars]
end

def variables
(0...@c.size).to_a
def can_improve?
!self.entering_variable.nil?
end

# idx of @c's minimum negative value
# nil when no improvement is possible
#
def entering_variable
variables.select { |var| @c[var] < 0 }.
min_by { |var| @c[var] }
(0...@c.size).select { |i| @c[i] < 0 }.min_by { |i| @c[i] }
end

def pivot
pivot_column = entering_variable
pivot_row = pivot_row(pivot_column)
raise UnboundedProblem unless pivot_row
leaving_var = basic_variable_in_row(pivot_row)
replace_basic_variable(leaving_var => pivot_column)
pivot_column = self.entering_variable or return nil
pivot_row = self.pivot_row(pivot_column) or raise UnboundedProblem
leaving_var = nil
@a[pivot_row].each_with_index { |a, i|
if a == 1 and @basic_vars.include?(i)
leaving_var = i
break
end
}
raise(SanityCheck, "no leaving_var") if leaving_var.nil?

@basic_vars.delete(leaving_var)
@basic_vars.push(pivot_column)
@basic_vars.sort!

pivot_ratio = Rational(1, @a[pivot_row][pivot_column])

# update pivot row
@a[pivot_row] *= pivot_ratio
@b[pivot_row] = pivot_ratio * @b[pivot_row]
@a[pivot_row] = @a[pivot_row].map { |val| val * pivot_ratio }
@b[pivot_row] = @b[pivot_row] * pivot_ratio

# update objective
@c -= @c[pivot_column] * @a[pivot_row]
# @c -= @c[pivot_column] * @a[pivot_row]
@c = @c.map.with_index { |val, i|
val - @c[pivot_column] * @a[pivot_row][i]
}

# update A and B
(row_indices - [pivot_row]).each do |row_ix|
r = @a[row_ix][pivot_column]
@a[row_ix] -= r * @a[pivot_row]
@b[row_ix] -= r * @b[pivot_row]
end
@num_constraints.times { |i|
next if i == pivot_row
r = @a[i][pivot_column]
@a[i] = @a[i].map.with_index { |val, j| val - r * @a[pivot_row][j] }
@b[i] = @b[i] - r * @b[pivot_row]
}

update_solution
end

def replace_basic_variable(hash)
from, to = hash.keys.first, hash.values.first
@basic_vars.delete(from)
@basic_vars << to
@basic_vars.sort!
self.update_solution
end

def pivot_row(column_ix)
row_ix_a_and_b = row_indices.map { |row_ix|
[row_ix, @a[row_ix][column_ix], @b[row_ix]]
}.reject { |_, a, b|
a == 0
}.reject { |_, a, b|
(b < 0) ^ (a < 0) # negative sign check
}
row_ix, _, _ = *last_min_by(row_ix_a_and_b) { |_, a, b|
Rational(b, a)
min_ratio = nil
idx = nil
@num_constraints.times { |i|
a, b = @a[i][column_ix], @b[i]
next if a == 0 or (b < 0) ^ (a < 0)
ratio = Rational(b, a)
idx, min_ratio = i, ratio if min_ratio.nil? or ratio <= min_ratio
}
row_ix
end

def basic_variable_in_row(pivot_row)
column_indices.detect do |column_ix|
@a[pivot_row][column_ix] == 1 and @basic_vars.include?(column_ix)
end
end

def row_indices
(0...@a.length).to_a
end

def column_indices
(0...@a.first.size).to_a
idx
end

def formatted_tableau
if can_improve?
pivot_column = entering_variable
pivot_row = pivot_row(pivot_column)
if self.can_improve?
pivot_column = self.entering_variable
pivot_row = self.pivot_row(pivot_column)
else
pivot_row = nil
end
num_cols = @c.size + 1
c = formatted_values(@c.to_a)
b = formatted_values(@b.to_a)
a = @a.to_a.map {|ar| formatted_values(ar.to_a) }
c = @c.to_a.map { |flt| "%2.3f" % flt }
b = @b.to_a.map { |flt| "%2.3f" % flt }
a = @a.to_a.map { |vec| vec.to_a.map { |flt| "%2.3f" % flt } }
if pivot_row
a[pivot_row][pivot_column] = "*" + a[pivot_row][pivot_column]
end
max = (c + b + a + ["1234567"]).flatten.map(&:size).max
result = []
result << c.map {|c| c.rjust(max, " ") }
result << c.map { |str| str.rjust(max, " ") }
a.zip(b) do |arow, brow|
result << (arow + [brow]).map {|a| a.rjust(max, " ") }
result << (arow + [brow]).map { |val| val.rjust(max, " ") }
result.last.insert(arow.length, "|")
end
lines = result.map {|b| b.join(" ") }
lines = result.map { |ary| ary.join(" ") }
max_line_length = lines.map(&:length).max
lines.insert(1, "-"*max_line_length)
lines.join("\n")
end

def formatted_values(array)
array.map {|c| "%2.3f" % c }
end

# like Enumerable#min_by except if multiple values are minimum
# it returns the last
def last_min_by(array)
best_element, best_value = nil, nil
array.each do |element|
value = yield element
if !best_element || value <= best_value
best_element, best_value = element, value
end
end
best_element
end

def assert(boolean)
raise unless boolean
end

end

Loading