From 70bb0c99d174e66375b3c3b5129e7c9d95c276b9 Mon Sep 17 00:00:00 2001 From: Rick Hull Date: Tue, 31 Oct 2017 05:26:38 +0000 Subject: [PATCH 01/37] add benchmark/ips: 460 ips --- Rakefile | 13 ++++++ metrics/bench | 5 +++ test/bench/simplex.rb | 98 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 116 insertions(+) create mode 100644 metrics/bench create mode 100644 test/bench/simplex.rb diff --git a/Rakefile b/Rakefile index cb70dcb..0aba59a 100644 --- a/Rakefile +++ b/Rakefile @@ -5,3 +5,16 @@ task :default => :test Rake::TestTask.new do |t| t.test_files = FileList['test/**/*_test.rb'] 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 diff --git a/metrics/bench b/metrics/bench new file mode 100644 index 0000000..09b8955 --- /dev/null +++ b/metrics/bench @@ -0,0 +1,5 @@ +/proc/loadavg 0.08 0.02 0.01 1/79 1931 +Warming up -------------------------------------- + Simplex Vector 46.000 i/100ms +Calculating ------------------------------------- + Simplex Vector 460.019 (± 0.9%) i/s - 1.380k in 3.000113s diff --git a/test/bench/simplex.rb b/test/bench/simplex.rb new file mode 100644 index 0000000..60c0695 --- /dev/null +++ b/test/bench/simplex.rb @@ -0,0 +1,98 @@ +require 'simplex' +require 'benchmark/ips' + +Benchmark.ips do |b| + b.config time: 3, warmup: 0.5 + + b.report("Simplex Vector") { + Simplex.new([1, 1], + [[2, 1], + [1, 2]], + [4, 3]).solution + + Simplex.new([3, 4], + [[1, 1], + [2, 1]], + [4, 5]).solution + + Simplex.new([2, -1], + [[1, 2], + [3, 2],], + [6, 12]).solution + + Simplex.new([60, 90, 300], + [[1, 1, 1], + [1, 3, 0], + [2, 0, 1]], + [600, 600, 900]).solution + + Simplex.new([70, 210, 140], + [[1, 1, 1], + [5, 4, 4], + [40, 20, 30]], + [100, 480, 3200]).solution + + Simplex.new([2, -1, 2], + [[2, 1, 0], + [1, 2, -2], + [0, 1, 2]], + [10, 20, 5]).solution + + Simplex.new([11, 16, 15], + [[1, 2, Rational(3, 2)], + [Rational(2, 3), Rational(2, 3), 1], + [Rational(1, 2), Rational(1, 3), Rational(1, 2)]], + [12_000, 4_600, 2_400]).solution + + Simplex.new([5, 4, 3], + [[2, 3, 1], + [4, 1, 2], + [3, 4, 2]], + [5, 11, 8]).solution + + Simplex.new([3, 2, -4], + [[1, 4, 0], + [2, 4,-2], + [1, 1,-2]], + [5, 6, 2]).solution + + Simplex.new([2, -1, 8], + [[2, -4, 6], + [-1, 3, 4], + [0, 0, 2]], + [3, 2, 1]).solution + + Simplex.new([100_000, 40_000, 18_000], + [[20, 6, 3], + [0, 1, 0], + [-1,-1, 1], + [-9, 1, 1]], + [182, 10, 0, 0]).solution + + Simplex.new([1, 2, 1, 2], + [[1, 0, 1, 0], + [0, 1, 0, 1], + [1, 1, 0, 0], + [0, 0, 1, 1]], + [1, 4, 2, 2]).solution + + Simplex.new([10, -57, -9, -24], + [[0.5, -5.5, -2.5, 9], + [0.5, -1.5, -0.5, 1], + [ 1, 0, 0, 0]], + [0, 0, 1]).solution + + Simplex.new([25, 20], + [[20, 12], + [1, 1]], + [1800, 8*15]).solution + } + +#b.report("Simplex Array") { +#} + +#b.report("Simplex Matrix") { +#} + +# b.compare! +end From e48c7ec08e25268bc14aa054f3f2d59882ad006f Mon Sep 17 00:00:00 2001 From: Rick Hull Date: Mon, 30 Oct 2017 03:26:57 +0000 Subject: [PATCH 02/37] use t.pattern and enable warnings --- Rakefile | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Rakefile b/Rakefile index 0aba59a..9014442 100644 --- a/Rakefile +++ b/Rakefile @@ -1,9 +1,8 @@ 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| From 15c85ce24917f90131661aade940a4a6a962065c Mon Sep 17 00:00:00 2001 From: Rick Hull Date: Mon, 30 Oct 2017 03:27:40 +0000 Subject: [PATCH 03/37] stop manipulating load path in code --- test/simplex_test.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/test/simplex_test.rb b/test/simplex_test.rb index 6b406e8..ed00878 100644 --- a/test/simplex_test.rb +++ b/test/simplex_test.rb @@ -1,5 +1,4 @@ require 'test/unit' -$:.push(File.expand_path("../../lib", __FILE__)) require 'simplex' class SimplexTest < Test::Unit::TestCase From d53f7d95cd90a2a581b9bd99e46c165163ec2513 Mon Sep 17 00:00:00 2001 From: Rick Hull Date: Mon, 30 Oct 2017 03:28:36 +0000 Subject: [PATCH 04/37] fix warnings about unused locals --- test/simplex_test.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/simplex_test.rb b/test/simplex_test.rb index ed00878..8001597 100644 --- a/test/simplex_test.rb +++ b/test/simplex_test.rb @@ -186,7 +186,7 @@ def test_cycle2 def test_error_mismatched_dimensions assert_raise ArgumentError do - result = Simplex.new( + Simplex.new( [10, -57, -9], [ [0.5, -5.5, -2.5, 9], @@ -198,7 +198,7 @@ def test_error_mismatched_dimensions end assert_raise ArgumentError do - result = Simplex.new( + Simplex.new( [10, -57, -9, 2], [ [0.5, -5.5, 9, 4], @@ -210,7 +210,7 @@ def test_error_mismatched_dimensions end assert_raise ArgumentError do - result = Simplex.new( + Simplex.new( [10, -57, -9, 2], [ [0.5, -5.5, 9, 4], From b88eab0c78fc1949ea917233b40752422ce4e733 Mon Sep 17 00:00:00 2001 From: Rick Hull Date: Mon, 30 Oct 2017 03:29:53 +0000 Subject: [PATCH 05/37] fix warnings about unused or shadow local vars --- lib/simplex.rb | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/lib/simplex.rb b/lib/simplex.rb index 98c893f..79ebbe1 100644 --- a/lib/simplex.rb +++ b/lib/simplex.rb @@ -26,8 +26,9 @@ def initialize(c, a, b) @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" + # unless @a.all? {|a| a.size == @c.size } and @b.size == @a.length + unless @a.all? {|a1| a1.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 } @@ -147,7 +148,7 @@ def formatted_tableau else pivot_row = nil end - num_cols = @c.size + 1 + # 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) } @@ -156,12 +157,15 @@ def formatted_tableau end max = (c + b + a + ["1234567"]).flatten.map(&:size).max result = [] - result << c.map {|c| c.rjust(max, " ") } + # result << c.map {|c| c.rjust(max, " ") } + result << c.map {|c1| c1.rjust(max, " ") } a.zip(b) do |arow, brow| - result << (arow + [brow]).map {|a| a.rjust(max, " ") } + # result << (arow + [brow]).map {|a| a.rjust(max, " ") } + result << (arow + [brow]).map {|a1| a1.rjust(max, " ") } result.last.insert(arow.length, "|") end - lines = result.map {|b| b.join(" ") } + # lines = result.map {|b| b.join(" ") } + lines = result.map {|b1| b1.join(" ") } max_line_length = lines.map(&:length).max lines.insert(1, "-"*max_line_length) lines.join("\n") From 60b2edf70402fd7522e8020c2ca5df2b651ca0e2 Mon Sep 17 00:00:00 2001 From: Rick Hull Date: Mon, 30 Oct 2017 03:30:12 +0000 Subject: [PATCH 06/37] remove trailing whitespace --- lib/simplex.rb | 5 ++--- test/simplex_test.rb | 6 +++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/lib/simplex.rb b/lib/simplex.rb index 79ebbe1..acc0093 100644 --- a/lib/simplex.rb +++ b/lib/simplex.rb @@ -62,7 +62,7 @@ def update_solution def solve while can_improve? @pivot_count += 1 - raise "Too many pivots" if @pivot_count > max_pivots + raise "Too many pivots" if @pivot_count > max_pivots pivot end end @@ -175,7 +175,7 @@ def formatted_values(array) array.map {|c| "%2.3f" % c } end - # like Enumerable#min_by except if multiple values are minimum + # 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 @@ -193,4 +193,3 @@ def assert(boolean) end end - diff --git a/test/simplex_test.rb b/test/simplex_test.rb index 8001597..f5d3964 100644 --- a/test/simplex_test.rb +++ b/test/simplex_test.rb @@ -262,16 +262,16 @@ def test_cup_factory # [1, -2] # ) # while simplex.can_improve? - # puts + # puts # puts simplex.formatted_tableau # simplex.pivot # end # p :done - # puts + # puts # puts simplex.formatted_tableau #end - + def test_unbounded simplex = Simplex.new( [1, 1, 1], From 7ee8a0d8240263d37c3cac92c523623cf018d456 Mon Sep 17 00:00:00 2001 From: Rick Hull Date: Mon, 30 Oct 2017 04:00:09 +0000 Subject: [PATCH 07/37] convert from test/unit to minitest/test --- test/simplex_test.rb | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/test/simplex_test.rb b/test/simplex_test.rb index f5d3964..125e67c 100644 --- a/test/simplex_test.rb +++ b/test/simplex_test.rb @@ -1,7 +1,7 @@ -require 'test/unit' +require 'minitest/autorun' require 'simplex' -class SimplexTest < Test::Unit::TestCase +class SimplexTest < Minitest::Test def test_2x2 result = Simplex.new( [1, 1], @@ -179,13 +179,13 @@ def test_cycle2 ], [0, 0] ) - assert_raise Simplex::UnboundedProblem do + assert_raises Simplex::UnboundedProblem do simplex.solution end end def test_error_mismatched_dimensions - assert_raise ArgumentError do + assert_raises ArgumentError do Simplex.new( [10, -57, -9], [ @@ -197,7 +197,7 @@ def test_error_mismatched_dimensions ) end - assert_raise ArgumentError do + assert_raises ArgumentError do Simplex.new( [10, -57, -9, 2], [ @@ -209,7 +209,7 @@ def test_error_mismatched_dimensions ) end - assert_raise ArgumentError do + assert_raises ArgumentError do Simplex.new( [10, -57, -9, 2], [ @@ -281,7 +281,7 @@ def test_unbounded ], [5, 7] ) - assert_raise Simplex::UnboundedProblem do + assert_raises Simplex::UnboundedProblem do simplex.solution end end From 38b20c42714142a66ddf056b56e69e7b6c2e366f Mon Sep 17 00:00:00 2001 From: Rick Hull Date: Mon, 30 Oct 2017 05:10:46 +0000 Subject: [PATCH 08/37] remove unused Simplex#assert; add comments --- lib/simplex.rb | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/simplex.rb b/lib/simplex.rb index acc0093..35629f2 100644 --- a/lib/simplex.rb +++ b/lib/simplex.rb @@ -12,6 +12,9 @@ class UnboundedProblem < StandardError attr_accessor :max_pivots + # c - coefficients of objective function + # a - inequality lhs coefficients + # b - inequality rhs constants def initialize(c, a, b) @pivot_count = 0 @max_pivots = DEFAULT_MAX_PIVOTS @@ -187,9 +190,4 @@ def last_min_by(array) end best_element end - - def assert(boolean) - raise unless boolean - end - end From 5dfc7952d67ed2f371eb57cfb43f4974f76800c8 Mon Sep 17 00:00:00 2001 From: Rick Hull Date: Mon, 30 Oct 2017 05:12:28 +0000 Subject: [PATCH 09/37] drop @pivot_count --- lib/simplex.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/simplex.rb b/lib/simplex.rb index 35629f2..2b585e3 100644 --- a/lib/simplex.rb +++ b/lib/simplex.rb @@ -16,7 +16,6 @@ class UnboundedProblem < StandardError # a - inequality lhs coefficients # b - inequality rhs constants def initialize(c, a, b) - @pivot_count = 0 @max_pivots = DEFAULT_MAX_PIVOTS # Problem dimensions @@ -63,10 +62,11 @@ def update_solution end def solve + count = 0 while can_improve? - @pivot_count += 1 - raise "Too many pivots" if @pivot_count > max_pivots - pivot + count += 1 + raise "too many pivots: #{count}" unless count < @max_pivots + self.pivot end end From 7c35aa3b2d59977063875e463d5d3e8f45347842 Mon Sep 17 00:00:00 2001 From: Rick Hull Date: Mon, 30 Oct 2017 05:49:16 +0000 Subject: [PATCH 10/37] simplify initalize; make @x an Array rather than Vector --- lib/simplex.rb | 44 ++++++++++++++++++++++++++++---------------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/lib/simplex.rb b/lib/simplex.rb index 2b585e3..540a29d 100644 --- a/lib/simplex.rb +++ b/lib/simplex.rb @@ -12,31 +12,43 @@ class UnboundedProblem < StandardError attr_accessor :max_pivots - # c - coefficients of objective function - # a - inequality lhs coefficients - # b - inequality rhs constants + def self.new_vector(ary, num_constraints) + Vector[*(ary + Array.new(num_constraints, 0))] + end + + # X = num vars + # Y = num inequalities + + # c - coefficients of objective function (size X) + # a - inequality lhs coefficients, 2dim (size Y, size X) + # b - inequality rhs constants (size Y) def initialize(c, a, b) + 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 + @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 - unless @a.all? {|a1| a1.size == @c.size } and @b.size == @a.length - raise ArgumentError, "Input arrays have mismatched dimensions" - end + @c = self.class.new_vector(c.map { |flt| -1 * flt }, @num_constraints) + @a = a.map { |ary| + if ary.size != @num_non_slack_vars + raise ArgumentError, "a is inconsistent" + end + self.class.new_vector(ary, @num_constraints) + } + @b = self.class.new_vector(b, 0) - 0.upto(@num_constraints - 1) {|i| @a[i][@num_non_slack_vars + i] = 1 } + @num_constraints.times { |i| @a[i][@num_non_slack_vars + i] = 1 } # set initial solution: all non-slack variables = 0 - @x = Vector[*([0]*@num_vars)] + @x = Array.new(@num_vars, 0) @basic_vars = (@num_non_slack_vars...@num_vars).to_a update_solution end @@ -47,7 +59,7 @@ def solution end def current_solution - @x.to_a[0...@num_non_slack_vars] + @x[0...@num_non_slack_vars] end def update_solution From e6db1c5513ac368811c742f6d32c461f3de8453d Mon Sep 17 00:00:00 2001 From: Rick Hull Date: Mon, 30 Oct 2017 06:13:17 +0000 Subject: [PATCH 11/37] cleanup --- lib/simplex.rb | 26 +++++++++----------------- 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/lib/simplex.rb b/lib/simplex.rb index 540a29d..6ca3578 100644 --- a/lib/simplex.rb +++ b/lib/simplex.rb @@ -12,16 +12,9 @@ class UnboundedProblem < StandardError attr_accessor :max_pivots - def self.new_vector(ary, num_constraints) - Vector[*(ary + Array.new(num_constraints, 0))] - end - - # X = num vars - # Y = num inequalities - - # c - coefficients of objective function (size X) - # a - inequality lhs coefficients, 2dim (size Y, size X) - # b - inequality rhs constants (size Y) + # 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) num_vars = c.size num_inequalities = b.size @@ -36,16 +29,15 @@ def initialize(c, a, b) @num_vars = @num_non_slack_vars + @num_constraints # Set up initial matrix A and vectors b, c - @c = self.class.new_vector(c.map { |flt| -1 * flt }, @num_constraints) - @a = a.map { |ary| + @c = Vector[*(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 - self.class.new_vector(ary, @num_constraints) + constraints = Array.new(@num_constraints) { |ci| ci == i ? 1 : 0 } + Vector[*(ary + constraints)] } - @b = self.class.new_vector(b, 0) - - @num_constraints.times { |i| @a[i][@num_non_slack_vars + i] = 1 } + @b = Vector.elements(b, true) # set initial solution: all non-slack variables = 0 @x = Array.new(@num_vars, 0) @@ -63,7 +55,7 @@ def current_solution end def update_solution - 0.upto(@num_vars - 1) {|i| @x[i] = 0 } + @x = Array.new(@num_vars, 0) @basic_vars.each do |basic_var| row_with_1 = row_indices.detect do |row_ix| From f5cb8ebca3e1f087e2e6971be0dae45cf49ff7a1 Mon Sep 17 00:00:00 2001 From: Rick Hull Date: Mon, 30 Oct 2017 06:43:53 +0000 Subject: [PATCH 12/37] drop Simplex#variables and #replace_basic_variable --- lib/simplex.rb | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/lib/simplex.rb b/lib/simplex.rb index 6ca3578..960829e 100644 --- a/lib/simplex.rb +++ b/lib/simplex.rb @@ -23,7 +23,7 @@ def initialize(c, a, b) @max_pivots = DEFAULT_MAX_PIVOTS - # Problem dimensions + # Problem dimensions; these never change @num_non_slack_vars = num_vars @num_constraints = num_inequalities @num_vars = @num_non_slack_vars + @num_constraints @@ -40,7 +40,6 @@ def initialize(c, a, b) @b = Vector.elements(b, true) # set initial solution: all non-slack variables = 0 - @x = Array.new(@num_vars, 0) @basic_vars = (@num_non_slack_vars...@num_vars).to_a update_solution end @@ -78,13 +77,10 @@ def can_improve? !!entering_variable end - def variables - (0...@c.size).to_a - end - def entering_variable - variables.select { |var| @c[var] < 0 }. - min_by { |var| @c[var] } + (0...@c.size).to_a.select { |var| + @c[var] < 0 + }.min_by { |var| @c[var] } end def pivot @@ -92,7 +88,9 @@ def pivot 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) + @basic_vars.delete(leaving_var) + @basic_vars.push(pivot_column) + @basic_vars.sort! pivot_ratio = Rational(1, @a[pivot_row][pivot_column]) @@ -113,13 +111,6 @@ def pivot 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! - 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]] From 2449f2284dac04f38e8003d1675f08a20317756c Mon Sep 17 00:00:00 2001 From: Rick Hull Date: Mon, 30 Oct 2017 06:57:57 +0000 Subject: [PATCH 13/37] drop Gemfile and update travis --- .travis.yml | 11 ++++++++--- Gemfile | 6 ------ Gemfile.lock | 12 ------------ 3 files changed, 8 insertions(+), 21 deletions(-) delete mode 100644 Gemfile delete mode 100644 Gemfile.lock diff --git a/.travis.yml b/.travis.yml index d250ed9..a582fbd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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 diff --git a/Gemfile b/Gemfile deleted file mode 100644 index 75db47d..0000000 --- a/Gemfile +++ /dev/null @@ -1,6 +0,0 @@ -source "https://rubygems.org" -gem "rake" - -group :test do - gem "test-unit" -end diff --git a/Gemfile.lock b/Gemfile.lock deleted file mode 100644 index 9b744b9..0000000 --- a/Gemfile.lock +++ /dev/null @@ -1,12 +0,0 @@ -GEM - remote: https://rubygems.org/ - specs: - rake (10.1.0) - test-unit (2.0.0.0) - -PLATFORMS - ruby - -DEPENDENCIES - rake - test-unit From 919d78492f5d166a0edda2fccc92180976281386 Mon Sep 17 00:00:00 2001 From: Rick Hull Date: Mon, 30 Oct 2017 07:05:58 +0000 Subject: [PATCH 14/37] rename to test/simplex.rb --- test/{simplex_test.rb => simplex.rb} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/{simplex_test.rb => simplex.rb} (100%) diff --git a/test/simplex_test.rb b/test/simplex.rb similarity index 100% rename from test/simplex_test.rb rename to test/simplex.rb From 20a948438bcd8b999cd8f09b299e0554de00ba67 Mon Sep 17 00:00:00 2001 From: Rick Hull Date: Mon, 30 Oct 2017 07:21:10 +0000 Subject: [PATCH 15/37] drop #basic_variable_in_row --- lib/simplex.rb | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/lib/simplex.rb b/lib/simplex.rb index 960829e..911ab8c 100644 --- a/lib/simplex.rb +++ b/lib/simplex.rb @@ -87,7 +87,10 @@ 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) + leaving_var = self.column_indices.detect { |idx| + @a[pivot_row][idx] == 1 and @basic_vars.include?(idx) + } + @basic_vars.delete(leaving_var) @basic_vars.push(pivot_column) @basic_vars.sort! @@ -125,12 +128,6 @@ def pivot_row(column_ix) 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 From 9f5f9a57f1948d0cee158660d9fc76f2bcb00d96 Mon Sep 17 00:00:00 2001 From: Rick Hull Date: Mon, 30 Oct 2017 07:24:24 +0000 Subject: [PATCH 16/37] use self.instance_method when calling instance methods --- lib/simplex.rb | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/simplex.rb b/lib/simplex.rb index 911ab8c..aeca588 100644 --- a/lib/simplex.rb +++ b/lib/simplex.rb @@ -41,12 +41,12 @@ def initialize(c, a, b) # set initial solution: all non-slack variables = 0 @basic_vars = (@num_non_slack_vars...@num_vars).to_a - update_solution + self.update_solution end def solution - solve - current_solution + self.solve + self.current_solution end def current_solution @@ -66,7 +66,7 @@ def update_solution def solve count = 0 - while can_improve? + while self.can_improve? count += 1 raise "too many pivots: #{count}" unless count < @max_pivots self.pivot @@ -74,7 +74,7 @@ def solve end def can_improve? - !!entering_variable + !!self.entering_variable end def entering_variable @@ -84,8 +84,8 @@ def entering_variable end def pivot - pivot_column = entering_variable - pivot_row = pivot_row(pivot_column) + pivot_column = self.entering_variable + pivot_row = self.pivot_row(pivot_column) raise UnboundedProblem unless pivot_row leaving_var = self.column_indices.detect { |idx| @a[pivot_row][idx] == 1 and @basic_vars.include?(idx) From cadb517db47e3f25ff10474f3f81ccaa6eab4471 Mon Sep 17 00:00:00 2001 From: Rick Hull Date: Mon, 30 Oct 2017 07:43:01 +0000 Subject: [PATCH 17/37] add comments --- lib/simplex.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/simplex.rb b/lib/simplex.rb index aeca588..dd9bbb9 100644 --- a/lib/simplex.rb +++ b/lib/simplex.rb @@ -53,6 +53,7 @@ def current_solution @x[0...@num_non_slack_vars] end + # does not modify vector / matrix def update_solution @x = Array.new(@num_vars, 0) @@ -77,6 +78,9 @@ def can_improve? !!self.entering_variable end + # idx of @c's minimum negative value + # nil when no improvement is possible + # def entering_variable (0...@c.size).to_a.select { |var| @c[var] < 0 From e6c420c5077cae4274c889afb339f06cc54abc0a Mon Sep 17 00:00:00 2001 From: Rick Hull Date: Mon, 30 Oct 2017 07:44:11 +0000 Subject: [PATCH 18/37] cleanup; check for non-nil self.entering_variable --- lib/simplex.rb | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/lib/simplex.rb b/lib/simplex.rb index dd9bbb9..7a6f952 100644 --- a/lib/simplex.rb +++ b/lib/simplex.rb @@ -75,22 +75,19 @@ def solve end def can_improve? - !!self.entering_variable + !self.entering_variable.nil? end # idx of @c's minimum negative value # nil when no improvement is possible # def entering_variable - (0...@c.size).to_a.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 = self.entering_variable - pivot_row = self.pivot_row(pivot_column) - raise UnboundedProblem unless pivot_row + pivot_column = self.entering_variable or return nil + pivot_row = self.pivot_row(pivot_column) or raise UnboundedProblem leaving_var = self.column_indices.detect { |idx| @a[pivot_row][idx] == 1 and @basic_vars.include?(idx) } From 3c3c2e70af4892e00d4d1e886a36bd5efd7bbe2e Mon Sep 17 00:00:00 2001 From: Rick Hull Date: Mon, 30 Oct 2017 07:51:49 +0000 Subject: [PATCH 19/37] use self.instance method and fix some block parameters --- lib/simplex.rb | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/lib/simplex.rb b/lib/simplex.rb index 7a6f952..b491b27 100644 --- a/lib/simplex.rb +++ b/lib/simplex.rb @@ -106,24 +106,24 @@ def pivot @c -= @c[pivot_column] * @a[pivot_row] # update A and B - (row_indices - [pivot_row]).each do |row_ix| + (self.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 - update_solution + self.update_solution end def pivot_row(column_ix) - row_ix_a_and_b = row_indices.map { |row_ix| + row_ix_a_and_b = self.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| + row_ix, _, _ = *self.last_min_by(row_ix_a_and_b) { |_, a, b| Rational(b, a) } row_ix @@ -138,30 +138,27 @@ def column_indices 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 = self.formatted_values(@c.to_a) + b = self.formatted_values(@b.to_a) + a = @a.to_a.map {|ar| self.formatted_values(ar.to_a) } 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 {|c1| c1.rjust(max, " ") } + result << c.map { |flt| flt.rjust(max, " ") } a.zip(b) do |arow, brow| - # result << (arow + [brow]).map {|a| a.rjust(max, " ") } - result << (arow + [brow]).map {|a1| a1.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 {|b1| b1.join(" ") } + lines = result.map {|ary| ary.join(" ") } max_line_length = lines.map(&:length).max lines.insert(1, "-"*max_line_length) lines.join("\n") From 3516aae1e99f60041b85260736829b9e54d39c64 Mon Sep 17 00:00:00 2001 From: Rick Hull Date: Mon, 30 Oct 2017 07:52:04 +0000 Subject: [PATCH 20/37] TODO: investigate conditional --- lib/simplex.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/simplex.rb b/lib/simplex.rb index b491b27..05d42ac 100644 --- a/lib/simplex.rb +++ b/lib/simplex.rb @@ -174,6 +174,7 @@ def last_min_by(array) best_element, best_value = nil, nil array.each do |element| value = yield element + # TODO: uh oh if !best_element || value <= best_value best_element, best_value = element, value end From 7950c49c8d478ecc64c55adcb6d6259234588a21 Mon Sep 17 00:00:00 2001 From: Rick Hull Date: Mon, 30 Oct 2017 08:04:06 +0000 Subject: [PATCH 21/37] drop #formatted_values --- lib/simplex.rb | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/lib/simplex.rb b/lib/simplex.rb index 05d42ac..267b66d 100644 --- a/lib/simplex.rb +++ b/lib/simplex.rb @@ -144,30 +144,25 @@ def formatted_tableau else pivot_row = nil end - # num_cols = @c.size + 1 - c = self.formatted_values(@c.to_a) - b = self.formatted_values(@b.to_a) - a = @a.to_a.map {|ar| self.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 { |flt| flt.rjust(max, " ") } + result << c.map { |str| str.rjust(max, " ") } a.zip(b) do |arow, brow| result << (arow + [brow]).map { |val| val.rjust(max, " ") } result.last.insert(arow.length, "|") end - lines = result.map {|ary| ary.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) From 19ab1aeb7e5b9368ba33b025bd0f00f81bc5de12 Mon Sep 17 00:00:00 2001 From: Rick Hull Date: Mon, 30 Oct 2017 08:14:12 +0000 Subject: [PATCH 22/37] make last_min_by a class method --- lib/simplex.rb | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/lib/simplex.rb b/lib/simplex.rb index 267b66d..0f578ae 100644 --- a/lib/simplex.rb +++ b/lib/simplex.rb @@ -10,6 +10,20 @@ class Simplex class UnboundedProblem < StandardError end + # like Enumerable#min_by except if multiple values are minimum + # it returns the last + def self.last_min_by array, &blk + best_element, best_value = nil, nil + array.each do |element| + value = yield element + # TODO: uh oh + if !best_element || value <= best_value + best_element, best_value = element, value + end + end + best_element + end + attr_accessor :max_pivots # c - coefficients of objective function; size: num_vars @@ -123,7 +137,7 @@ def pivot_row(column_ix) }.reject { |_, a, b| (b < 0) ^ (a < 0) # negative sign check } - row_ix, _, _ = *self.last_min_by(row_ix_a_and_b) { |_, a, b| + row_ix, _, _ = *self.class.last_min_by(row_ix_a_and_b) { |_, a, b| Rational(b, a) } row_ix @@ -162,18 +176,4 @@ def formatted_tableau lines.insert(1, "-"*max_line_length) lines.join("\n") 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 - # TODO: uh oh - if !best_element || value <= best_value - best_element, best_value = element, value - end - end - best_element - end end From 58b2f89047e35bddc63987f92a7cf18919724f58 Mon Sep 17 00:00:00 2001 From: Rick Hull Date: Mon, 30 Oct 2017 08:15:03 +0000 Subject: [PATCH 23/37] just call reject once --- lib/simplex.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/simplex.rb b/lib/simplex.rb index 0f578ae..d068520 100644 --- a/lib/simplex.rb +++ b/lib/simplex.rb @@ -133,9 +133,7 @@ def pivot_row(column_ix) row_ix_a_and_b = self.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 + a == 0 or (b < 0) ^ (a < 0) # negative sign check } row_ix, _, _ = *self.class.last_min_by(row_ix_a_and_b) { |_, a, b| Rational(b, a) From 59b2ae940e2523d79182bd4994c5f3f7ed11e9fd Mon Sep 17 00:00:00 2001 From: Rick Hull Date: Mon, 30 Oct 2017 08:27:13 +0000 Subject: [PATCH 24/37] drop last_min_by --- lib/simplex.rb | 30 ++++++++---------------------- 1 file changed, 8 insertions(+), 22 deletions(-) diff --git a/lib/simplex.rb b/lib/simplex.rb index d068520..4e2695e 100644 --- a/lib/simplex.rb +++ b/lib/simplex.rb @@ -10,20 +10,6 @@ class Simplex class UnboundedProblem < StandardError end - # like Enumerable#min_by except if multiple values are minimum - # it returns the last - def self.last_min_by array, &blk - best_element, best_value = nil, nil - array.each do |element| - value = yield element - # TODO: uh oh - if !best_element || value <= best_value - best_element, best_value = element, value - end - end - best_element - end - attr_accessor :max_pivots # c - coefficients of objective function; size: num_vars @@ -130,15 +116,15 @@ def pivot end def pivot_row(column_ix) - row_ix_a_and_b = self.row_indices.map { |row_ix| - [row_ix, @a[row_ix][column_ix], @b[row_ix]] - }.reject { |_, a, b| - a == 0 or (b < 0) ^ (a < 0) # negative sign check - } - row_ix, _, _ = *self.class.last_min_by(row_ix_a_and_b) { |_, a, b| - Rational(b, a) + min_ratio = nil + idx = nil + self.row_indices.each { |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 + idx end def row_indices From 11d7239c361705e341aee60fefcf628a622abaa7 Mon Sep 17 00:00:00 2001 From: Rick Hull Date: Mon, 30 Oct 2017 08:37:51 +0000 Subject: [PATCH 25/37] drop #row_indices --- lib/simplex.rb | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/lib/simplex.rb b/lib/simplex.rb index 4e2695e..4e53408 100644 --- a/lib/simplex.rb +++ b/lib/simplex.rb @@ -58,10 +58,15 @@ def update_solution @x = Array.new(@num_vars, 0) @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] + idx = nil + @a.size.times { |i| + if @a[i][basic_var] == 1 + idx = i + break + end + } + raise "no idx found for basic_var #{basic_var} in a" unless idx + @x[basic_var] = @b[idx] end end @@ -106,11 +111,12 @@ def pivot @c -= @c[pivot_column] * @a[pivot_row] # update A and B - (self.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 + @a.size.times { |i| + next if i == pivot_row + r = @a[i][pivot_column] + @a[i] -= r * @a[pivot_row] + @b[i] -= r * @b[pivot_row] + } self.update_solution end @@ -118,7 +124,7 @@ def pivot def pivot_row(column_ix) min_ratio = nil idx = nil - self.row_indices.each { |i| + @a.size.times { |i| a, b = @a[i][column_ix], @b[i] next if a == 0 or (b < 0) ^ (a < 0) ratio = Rational(b, a) @@ -127,10 +133,6 @@ def pivot_row(column_ix) idx end - def row_indices - (0...@a.length).to_a - end - def column_indices (0...@a.first.size).to_a end From a24525b194ec681a10dd320760dd68a9285636e8 Mon Sep 17 00:00:00 2001 From: Rick Hull Date: Mon, 30 Oct 2017 08:38:45 +0000 Subject: [PATCH 26/37] prefer @num_constraints to @a.size --- lib/simplex.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/simplex.rb b/lib/simplex.rb index 4e53408..f48cdbd 100644 --- a/lib/simplex.rb +++ b/lib/simplex.rb @@ -59,7 +59,7 @@ def update_solution @basic_vars.each do |basic_var| idx = nil - @a.size.times { |i| + @num_constraints.times { |i| if @a[i][basic_var] == 1 idx = i break @@ -111,7 +111,7 @@ def pivot @c -= @c[pivot_column] * @a[pivot_row] # update A and B - @a.size.times { |i| + @num_constraints.times { |i| next if i == pivot_row r = @a[i][pivot_column] @a[i] -= r * @a[pivot_row] @@ -124,7 +124,7 @@ def pivot def pivot_row(column_ix) min_ratio = nil idx = nil - @a.size.times { |i| + @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) From 6f0e0a24c169139b223704bdc103a8e146bfae22 Mon Sep 17 00:00:00 2001 From: Rick Hull Date: Mon, 30 Oct 2017 08:45:35 +0000 Subject: [PATCH 27/37] drop #column_indices --- lib/simplex.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/simplex.rb b/lib/simplex.rb index f48cdbd..2a21392 100644 --- a/lib/simplex.rb +++ b/lib/simplex.rb @@ -93,8 +93,12 @@ def entering_variable def pivot pivot_column = self.entering_variable or return nil pivot_row = self.pivot_row(pivot_column) or raise UnboundedProblem - leaving_var = self.column_indices.detect { |idx| - @a[pivot_row][idx] == 1 and @basic_vars.include?(idx) + leaving_var = nil + @a[pivot_row].each_with_index { |a, i| + if a == 1 and @basic_vars.include?(i) + leaving_var = i + break + end } @basic_vars.delete(leaving_var) @@ -133,10 +137,6 @@ def pivot_row(column_ix) idx end - def column_indices - (0...@a.first.size).to_a - end - def formatted_tableau if self.can_improve? pivot_column = self.entering_variable From 20e81aa54a422d00aa07e3daa9ccd9803ff7af6d Mon Sep 17 00:00:00 2001 From: Rick Hull Date: Mon, 30 Oct 2017 08:49:28 +0000 Subject: [PATCH 28/37] reorder some method definitions --- lib/simplex.rb | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/simplex.rb b/lib/simplex.rb index 2a21392..876f2b3 100644 --- a/lib/simplex.rb +++ b/lib/simplex.rb @@ -44,15 +44,6 @@ def initialize(c, a, b) self.update_solution end - def solution - self.solve - self.current_solution - end - - def current_solution - @x[0...@num_non_slack_vars] - end - # does not modify vector / matrix def update_solution @x = Array.new(@num_vars, 0) @@ -70,6 +61,11 @@ def update_solution end end + def solution + self.solve + self.current_solution + end + def solve count = 0 while self.can_improve? @@ -79,6 +75,10 @@ def solve end end + def current_solution + @x[0...@num_non_slack_vars] + end + def can_improve? !self.entering_variable.nil? end From 433c05c8fbd2f152540d583a073e4d74becada7b Mon Sep 17 00:00:00 2001 From: Rick Hull Date: Mon, 30 Oct 2017 08:57:56 +0000 Subject: [PATCH 29/37] Exceptions: add SanityCheck and TooManyPivots; inherit from RuntimeError --- lib/simplex.rb | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/lib/simplex.rb b/lib/simplex.rb index 876f2b3..8ab0909 100644 --- a/lib/simplex.rb +++ b/lib/simplex.rb @@ -7,8 +7,9 @@ class Vector class Simplex DEFAULT_MAX_PIVOTS = 10_000 - class UnboundedProblem < StandardError - end + class UnboundedProblem < RuntimeError; end + class SanityCheck < RuntimeError; end + class TooManyPivots < RuntimeError; end attr_accessor :max_pivots @@ -48,17 +49,17 @@ def initialize(c, a, b) def update_solution @x = Array.new(@num_vars, 0) - @basic_vars.each do |basic_var| + @basic_vars.each { |basic_var| idx = nil @num_constraints.times { |i| if @a[i][basic_var] == 1 - idx = i + idx =i break end } - raise "no idx found for basic_var #{basic_var} in a" unless idx + raise(SanityCheck, "no idx for basic_var #{basic_var} in a") unless idx @x[basic_var] = @b[idx] - end + } end def solution @@ -70,7 +71,7 @@ def solve count = 0 while self.can_improve? count += 1 - raise "too many pivots: #{count}" unless count < @max_pivots + raise(TooManyPivots, count.to_s) unless count < @max_pivots self.pivot end end @@ -100,6 +101,7 @@ def pivot break end } + raise(SanityCheck, "no leaving_var") if leaving_var.nil? @basic_vars.delete(leaving_var) @basic_vars.push(pivot_column) From e1617c8cea8cf85b5226d88339147bf03a184285 Mon Sep 17 00:00:00 2001 From: Rick Hull Date: Mon, 30 Oct 2017 09:09:13 +0000 Subject: [PATCH 30/37] introduce Simplex::Error and inherit from it --- lib/simplex.rb | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/simplex.rb b/lib/simplex.rb index 8ab0909..53ae3b8 100644 --- a/lib/simplex.rb +++ b/lib/simplex.rb @@ -7,9 +7,10 @@ class Vector class Simplex DEFAULT_MAX_PIVOTS = 10_000 - class UnboundedProblem < RuntimeError; end - class SanityCheck < RuntimeError; end - class TooManyPivots < RuntimeError; end + class Error < RuntimeError; end + class UnboundedProblem < Error; end + class SanityCheck < Error; end + class TooManyPivots < Error; end attr_accessor :max_pivots From 97ffa3d771259b9da0387470b3be4daa4d0d2db5 Mon Sep 17 00:00:00 2001 From: Rick Hull Date: Tue, 31 Oct 2017 05:34:42 +0000 Subject: [PATCH 31/37] benchmark/ips: 546 ips (was 460) --- metrics/bench | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/metrics/bench b/metrics/bench index 09b8955..e0ab388 100644 --- a/metrics/bench +++ b/metrics/bench @@ -1,5 +1,5 @@ -/proc/loadavg 0.08 0.02 0.01 1/79 1931 +/proc/loadavg 0.01 0.01 0.00 1/79 3023 Warming up -------------------------------------- - Simplex Vector 46.000 i/100ms + Simplex Vector 54.000 i/100ms Calculating ------------------------------------- - Simplex Vector 460.019 (± 0.9%) i/s - 1.380k in 3.000113s + Simplex Vector 546.614 (± 1.3%) i/s - 1.674k in 3.063034s From f58fe51eae0e0345e054c7407bec5c41140f3831 Mon Sep 17 00:00:00 2001 From: Rick Hull Date: Tue, 31 Oct 2017 04:56:57 +0000 Subject: [PATCH 32/37] s/Vector/Array/g --- lib/simplex.rb | 26 +++++++++++--------------- metrics/bench | 6 +++--- test/bench/simplex.rb | 2 +- 3 files changed, 15 insertions(+), 19 deletions(-) diff --git a/lib/simplex.rb b/lib/simplex.rb index 53ae3b8..573b678 100644 --- a/lib/simplex.rb +++ b/lib/simplex.rb @@ -1,9 +1,3 @@ -require 'matrix' - -class Vector - public :[]= -end - class Simplex DEFAULT_MAX_PIVOTS = 10_000 @@ -31,15 +25,14 @@ def initialize(c, a, b) @num_vars = @num_non_slack_vars + @num_constraints # Set up initial matrix A and vectors b, c - @c = Vector[*(c.map { |flt| -1 * flt } + Array.new(@num_constraints, 0))] + @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 - constraints = Array.new(@num_constraints) { |ci| ci == i ? 1 : 0 } - Vector[*(ary + constraints)] + ary + Array.new(@num_constraints) { |ci| ci == i ? 1 : 0 } } - @b = Vector.elements(b, true) + @b = b # set initial solution: all non-slack variables = 0 @basic_vars = (@num_non_slack_vars...@num_vars).to_a @@ -111,18 +104,21 @@ def pivot 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 @num_constraints.times { |i| next if i == pivot_row r = @a[i][pivot_column] - @a[i] -= r * @a[pivot_row] - @b[i] -= r * @b[pivot_row] + @a[i] = @a[i].map.with_index { |val, j| val - r * @a[pivot_row][j] } + @b[i] = @b[i] - r * @b[pivot_row] } self.update_solution diff --git a/metrics/bench b/metrics/bench index e0ab388..32918eb 100644 --- a/metrics/bench +++ b/metrics/bench @@ -1,5 +1,5 @@ -/proc/loadavg 0.01 0.01 0.00 1/79 3023 +/proc/loadavg 0.01 0.01 0.00 1/77 1173 Warming up -------------------------------------- - Simplex Vector 54.000 i/100ms + Simplex Array 84.000 i/100ms Calculating ------------------------------------- - Simplex Vector 546.614 (± 1.3%) i/s - 1.674k in 3.063034s + Simplex Array 849.092 (± 1.3%) i/s - 2.604k in 3.067337s diff --git a/test/bench/simplex.rb b/test/bench/simplex.rb index 60c0695..10728e0 100644 --- a/test/bench/simplex.rb +++ b/test/bench/simplex.rb @@ -4,7 +4,7 @@ Benchmark.ips do |b| b.config time: 3, warmup: 0.5 - b.report("Simplex Vector") { + b.report("Simplex Array") { Simplex.new([1, 1], [[2, 1], [1, 2]], From daf4e2554bc88e3e05e2b71092c3b1129e17d2e2 Mon Sep 17 00:00:00 2001 From: Rick Hull Date: Mon, 30 Oct 2017 03:55:59 +0000 Subject: [PATCH 33/37] add Simplex::Parse module --- lib/simplex/parse.rb | 51 ++++++++++++++++++++++++++++++++++++++++++++ test/parse.rb | 38 +++++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+) create mode 100644 lib/simplex/parse.rb create mode 100644 test/parse.rb diff --git a/lib/simplex/parse.rb b/lib/simplex/parse.rb new file mode 100644 index 0000000..1acb462 --- /dev/null +++ b/lib/simplex/parse.rb @@ -0,0 +1,51 @@ +class Simplex + module Parse + TERM_RGX = %r{ + \A # starts with + (-)? # possible negative sign + (\d+\.?\d*)? # possible float (optional) + ([a-zA-Z]) # single letter variable + \z # end str + }x + + # rules: variables are a single letter + # may have a coefficient (default: 1.0) + # only sum and difference operations allowed + # normalize to all sums with possibly negative coefficients + # valid inputs: + # 'x + y' => [1.0, 1.0], [:x, :y] + # '2x - 5y' => [2.0, -5.0], [:x, :y] + # '-2x - 3y + -4z' => [-2.0, -3.0, -4.0], [:x, :y, :z] + def self.objective(str) + terms = str.split(/\s+/) + negative = false + coefficients = [] + variables = [] + while !terms.empty? + # consume plus and minus operations + term = terms.shift + if term == '-' + negative = true + term = terms.shift + elsif term == '+' + negative = false + term = terms.shift + end + + coefficient, variable = self.term(term) + coefficient *= -1 if negative + coefficients << coefficient + variables << variable + end + return coefficients, variables + end + + def self.term(term_str) + matches = term_str.match TERM_RGX + raise "bad term: #{term_str}" unless matches + flt = (matches[2] || 1).to_f * (matches[1] ? -1 : 1) + sym = matches[3].to_sym # consider matches[3].downcase.to_sym + return flt, sym + end + end +end diff --git a/test/parse.rb b/test/parse.rb new file mode 100644 index 0000000..84111d3 --- /dev/null +++ b/test/parse.rb @@ -0,0 +1,38 @@ +require 'simplex/parse' +require 'minitest/autorun' + +describe Simplex::Parse do + describe "Parse.term" do + it "must parse valid terms" do + { "-1.2A" => [-1.2, :A], + "99x" => [99.0, :x], + "z" => [1.0, :z], + "-b" => [-1.0, :b] }.each { |valid, expected| + Simplex::Parse.term(valid).must_equal expected + } + end + + it "must reject invalid terms" do + ["3xy", "24/7x", "x17", "2*x"].each { |invalid| + proc { Simplex::Parse.term(invalid) }.must_raise RuntimeError + } + end + end + + describe "Parse.objective" do + it "must parse valid expressions" do + { "x + y" => [[1.0, 1.0], [:x, :y]], + "2x - 5y" => [[2.0, -5.0], [:x, :y]], + "-2x - 3y + -42.7z" => [[-2.0, -3.0, -42.7], [:x, :y, :z]], + }.each { |valid, expected| + Simplex::Parse.objective(valid).must_equal expected + } + end + + it "must reject invalid expressions" do + ["a2 + b2 = c2", "x + xy", "x * 2"].each { |invalid| + proc { Simplex::Parse.objective(invalid) }.must_raise RuntimeError + } + end + end +end From d59592b450d64c4b6fa780dd5526069aa43d2f71 Mon Sep 17 00:00:00 2001 From: Rick Hull Date: Mon, 30 Oct 2017 04:47:12 +0000 Subject: [PATCH 34/37] add tokenizing, expression and inequality parsing, and some regexen --- lib/simplex/parse.rb | 37 +++++++++++++++++++++++++++------ test/parse.rb | 49 ++++++++++++++++++++++++++++++++++++++------ 2 files changed, 74 insertions(+), 12 deletions(-) diff --git a/lib/simplex/parse.rb b/lib/simplex/parse.rb index 1acb462..62bc520 100644 --- a/lib/simplex/parse.rb +++ b/lib/simplex/parse.rb @@ -1,13 +1,38 @@ class Simplex module Parse + # coefficient concatenated with a single letter variable, e.g. "-1.23x" TERM_RGX = %r{ \A # starts with (-)? # possible negative sign - (\d+\.?\d*)? # possible float (optional) + (\d+(?:\.\d*)?)? # possible float (optional) ([a-zA-Z]) # single letter variable \z # end str }x + # a float or integer, possibly negative + CONSTANT_RGX = %r{ + \A # starts with + -? # possible negative sign + \d+ # integer portion + (?:\.\d*)? # possible decimal portion + \z # end str + }x + + def self.inequality(str) + lhs, rhs = str.split('<=') + lhco, lhvar = self.expression(lhs) + rht = self.tokenize(rhs) + raise "bad inequality: #{str}; bad rhs: #{rhs}" unless rht.size == 1 + raise "bad rhs: #{rhs}" if !rht.first.match CONSTANT_RGX + return lhco, lhvar, rht.first.to_f + end + + # ignore leading and trailing spaces + # ignore multiple spaces + def self.tokenize(str) + str.strip.split(/\s+/) + end + # rules: variables are a single letter # may have a coefficient (default: 1.0) # only sum and difference operations allowed @@ -16,8 +41,8 @@ module Parse # 'x + y' => [1.0, 1.0], [:x, :y] # '2x - 5y' => [2.0, -5.0], [:x, :y] # '-2x - 3y + -4z' => [-2.0, -3.0, -4.0], [:x, :y, :z] - def self.objective(str) - terms = str.split(/\s+/) + def self.expression(str) + terms = self.tokenize(str) negative = false coefficients = [] variables = [] @@ -40,9 +65,9 @@ def self.objective(str) return coefficients, variables end - def self.term(term_str) - matches = term_str.match TERM_RGX - raise "bad term: #{term_str}" unless matches + def self.term(str) + matches = str.match TERM_RGX + raise "bad term: #{str}" unless matches flt = (matches[2] || 1).to_f * (matches[1] ? -1 : 1) sym = matches[3].to_sym # consider matches[3].downcase.to_sym return flt, sym diff --git a/test/parse.rb b/test/parse.rb index 84111d3..43a0045 100644 --- a/test/parse.rb +++ b/test/parse.rb @@ -2,36 +2,73 @@ require 'minitest/autorun' describe Simplex::Parse do + P = Simplex::Parse + describe "Parse.term" do it "must parse valid terms" do { "-1.2A" => [-1.2, :A], "99x" => [99.0, :x], "z" => [1.0, :z], "-b" => [-1.0, :b] }.each { |valid, expected| - Simplex::Parse.term(valid).must_equal expected + P.term(valid).must_equal expected } end it "must reject invalid terms" do ["3xy", "24/7x", "x17", "2*x"].each { |invalid| - proc { Simplex::Parse.term(invalid) }.must_raise RuntimeError + proc { P.term(invalid) }.must_raise RuntimeError } end end - describe "Parse.objective" do + describe "Parse.expression" do it "must parse valid expressions" do { "x + y" => [[1.0, 1.0], [:x, :y]], "2x - 5y" => [[2.0, -5.0], [:x, :y]], "-2x - 3y + -42.7z" => [[-2.0, -3.0, -42.7], [:x, :y, :z]], + " -5y + -x " => [[-5.0, -1.0], [:y, :x]], + "a - -b" => [[1.0, 1.0], [:a, :b]], + "a b c" => [[1.0, 1.0, 1.0], [:a, :b, :c]], }.each { |valid, expected| - Simplex::Parse.objective(valid).must_equal expected + P.expression(valid).must_equal expected } end it "must reject invalid expressions" do - ["a2 + b2 = c2", "x + xy", "x * 2"].each { |invalid| - proc { Simplex::Parse.objective(invalid) }.must_raise RuntimeError + ["a2 + b2 = c2", + "x + xy", + "x * 2"].each { |invalid| + proc { P.expression(invalid) }.must_raise RuntimeError + } + end + end + + describe "Parse.tokenize" do + it "ignores leading or trailing whitespace" do + P.tokenize(" 5x + 2.9y ").must_equal ["5x", "+", "2.9y"] + end + + it "ignores multiple spaces" do + P.tokenize("5x + 2.9y").must_equal ["5x", "+", "2.9y"] + end + end + + describe "Parse.inequality" do + it "must parse valid inequalities" do + { "x + y <= 4" => [[1.0, 1.0], [:x, :y], 4.0], + "0.94a - 22.1b <= -14.67" => [[0.94, -22.1], [:a, :b], -14.67], + "x <= 0" => [[1.0], [:x], 0], + }.each { |valid, expected| + P.inequality(valid).must_equal expected + } + end + + it "must reject invalid inequalities" do + ["x + y >= 4", + "0.94a - 22.1b <= -14.67c", + "x < 0", + ].each { |invalid| + proc { P.inequality(invalid) }.must_raise RuntimeError } end end From 6808fe458810a2170cccec899590c73c61e06884 Mon Sep 17 00:00:00 2001 From: Rick Hull Date: Mon, 30 Oct 2017 09:07:03 +0000 Subject: [PATCH 35/37] introduce Parse::Error and use it --- lib/simplex/parse.rb | 14 ++++++++++---- test/parse.rb | 4 ++-- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/lib/simplex/parse.rb b/lib/simplex/parse.rb index 62bc520..54e605a 100644 --- a/lib/simplex/parse.rb +++ b/lib/simplex/parse.rb @@ -1,5 +1,10 @@ class Simplex module Parse + class Error < RuntimeError; end + class InvalidExpression < Error; end + class InvalidInequality < Error; end + class InvalidTerm < Error; end + # coefficient concatenated with a single letter variable, e.g. "-1.23x" TERM_RGX = %r{ \A # starts with @@ -22,9 +27,10 @@ def self.inequality(str) lhs, rhs = str.split('<=') lhco, lhvar = self.expression(lhs) rht = self.tokenize(rhs) - raise "bad inequality: #{str}; bad rhs: #{rhs}" unless rht.size == 1 - raise "bad rhs: #{rhs}" if !rht.first.match CONSTANT_RGX - return lhco, lhvar, rht.first.to_f + raise(InvalidInequality, "#{str}; bad rhs: #{rhs}") unless rht.size == 1 + c = rht.first + raise(InvalidInequality, "bad rhs: #{rhs}") if !c.match CONSTANT_RGX + return lhco, lhvar, c.to_f end # ignore leading and trailing spaces @@ -67,7 +73,7 @@ def self.expression(str) def self.term(str) matches = str.match TERM_RGX - raise "bad term: #{str}" unless matches + raise(InvalidTerm, str) unless matches flt = (matches[2] || 1).to_f * (matches[1] ? -1 : 1) sym = matches[3].to_sym # consider matches[3].downcase.to_sym return flt, sym diff --git a/test/parse.rb b/test/parse.rb index 43a0045..1bdf355 100644 --- a/test/parse.rb +++ b/test/parse.rb @@ -38,7 +38,7 @@ ["a2 + b2 = c2", "x + xy", "x * 2"].each { |invalid| - proc { P.expression(invalid) }.must_raise RuntimeError + proc { P.expression(invalid) }.must_raise P::Error } end end @@ -68,7 +68,7 @@ "0.94a - 22.1b <= -14.67c", "x < 0", ].each { |invalid| - proc { P.inequality(invalid) }.must_raise RuntimeError + proc { P.inequality(invalid) }.must_raise P::Error } end end From 35bc7d7368c362debad16ece43e35c8999dc75d7 Mon Sep 17 00:00:00 2001 From: Rick Hull Date: Mon, 30 Oct 2017 09:24:27 +0000 Subject: [PATCH 36/37] reformat for readability --- test/parse.rb | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/test/parse.rb b/test/parse.rb index 1bdf355..84e2cf3 100644 --- a/test/parse.rb +++ b/test/parse.rb @@ -7,9 +7,9 @@ describe "Parse.term" do it "must parse valid terms" do { "-1.2A" => [-1.2, :A], - "99x" => [99.0, :x], - "z" => [1.0, :z], - "-b" => [-1.0, :b] }.each { |valid, expected| + "99x" => [99.0, :x], + "z" => [1.0, :z], + "-b" => [-1.0, :b] }.each { |valid, expected| P.term(valid).must_equal expected } end @@ -23,12 +23,12 @@ describe "Parse.expression" do it "must parse valid expressions" do - { "x + y" => [[1.0, 1.0], [:x, :y]], - "2x - 5y" => [[2.0, -5.0], [:x, :y]], + { "x + y" => [[1.0, 1.0], [:x, :y]], + "2x - 5y" => [[2.0, -5.0], [:x, :y]], "-2x - 3y + -42.7z" => [[-2.0, -3.0, -42.7], [:x, :y, :z]], - " -5y + -x " => [[-5.0, -1.0], [:y, :x]], - "a - -b" => [[1.0, 1.0], [:a, :b]], - "a b c" => [[1.0, 1.0, 1.0], [:a, :b, :c]], + " -5y + -x " => [[-5.0, -1.0], [:y, :x]], + "a - -b" => [[1.0, 1.0], [:a, :b]], + "a A b" => [[1.0, 1.0, 1.0], [:a, :A, :b]], }.each { |valid, expected| P.expression(valid).must_equal expected } @@ -55,9 +55,9 @@ describe "Parse.inequality" do it "must parse valid inequalities" do - { "x + y <= 4" => [[1.0, 1.0], [:x, :y], 4.0], + { "x + y <= 4" => [[1.0, 1.0], [:x, :y], 4.0], "0.94a - 22.1b <= -14.67" => [[0.94, -22.1], [:a, :b], -14.67], - "x <= 0" => [[1.0], [:x], 0], + "x <= 0" => [[1.0], [:x], 0], }.each { |valid, expected| P.inequality(valid).must_equal expected } From f75d70b277575d840058cdcee0b504ff9b53e551 Mon Sep 17 00:00:00 2001 From: Rick Hull Date: Tue, 31 Oct 2017 07:24:38 +0000 Subject: [PATCH 37/37] add Simplex.maximize - based on Simplex.problem - which required changing Parse.expression's return type to a hash with variable keys and coefficient values --- lib/simplex/parse.rb | 57 +++++++++++++++++++++++++++++++++++++------- test/parse.rb | 37 ++++++++++++++++++++-------- 2 files changed, 76 insertions(+), 18 deletions(-) diff --git a/lib/simplex/parse.rb b/lib/simplex/parse.rb index 54e605a..00f360e 100644 --- a/lib/simplex/parse.rb +++ b/lib/simplex/parse.rb @@ -25,12 +25,14 @@ class InvalidTerm < Error; end def self.inequality(str) lhs, rhs = str.split('<=') - lhco, lhvar = self.expression(lhs) + if lhs.nil? or lhs.empty? or rhs.nil? or rhs.empty? + raise(InvalidInequality, "#{str}") + end rht = self.tokenize(rhs) raise(InvalidInequality, "#{str}; bad rhs: #{rhs}") unless rht.size == 1 c = rht.first raise(InvalidInequality, "bad rhs: #{rhs}") if !c.match CONSTANT_RGX - return lhco, lhvar, c.to_f + return self.expression(lhs), c.to_f end # ignore leading and trailing spaces @@ -50,8 +52,7 @@ def self.tokenize(str) def self.expression(str) terms = self.tokenize(str) negative = false - coefficients = [] - variables = [] + coefficients = {} while !terms.empty? # consume plus and minus operations term = terms.shift @@ -64,11 +65,10 @@ def self.expression(str) end coefficient, variable = self.term(term) - coefficient *= -1 if negative - coefficients << coefficient - variables << variable + raise("double variable: #{str}") if coefficients.key?(variable) + coefficients[variable] = negative ? coefficient * -1 : coefficient end - return coefficients, variables + coefficients end def self.term(str) @@ -79,4 +79,45 @@ def self.term(str) return flt, sym end end + + def self.problem(maximize: nil, constraints: [], **kwargs) + if maximize + obj, maximize = maximize, true + elsif kwargs[:minimize] + obj, maximize = kwargs[:minimize], false + else + raise(ArgumentError, "one of maximize/minimize expected") + end + unless obj.is_a?(String) + raise(ArgumentError, "bad expr: #{expr} (#{expr.class})") + end + obj_cof = Parse.expression(obj) + + c = [] # coefficients of objective expression + a = [] # array (per constraint) of the inequality's lhs coefficients + b = [] # rhs (constant) for the inequalities / constraints + + # this determines the order of coefficients + letter_vars = obj_cof.keys + letter_vars.each { |v| c << obj_cof[v] } + + constraints.each { |str| + unless str.is_a?(String) + raise(ArgumentError, "bad constraint: #{str} (#{str.class})") + end + cofs = [] + ineq_cofs, rhs = Parse.inequality(str) + letter_vars.each { |v| + raise("constraint #{str} is missing var #{v}") unless ineq_cofs.key?(v) + cofs << ineq_cofs[v] + } + a.push cofs + b.push rhs + } + self.new(c, a, b) + end + + def self.maximize(expression, *ineqs) + self.problem(maximize: expression, constraints: ineqs).solution + end end diff --git a/test/parse.rb b/test/parse.rb index 84e2cf3..73f816b 100644 --- a/test/parse.rb +++ b/test/parse.rb @@ -8,7 +8,7 @@ it "must parse valid terms" do { "-1.2A" => [-1.2, :A], "99x" => [99.0, :x], - "z" => [1.0, :z], + "z" => [ 1.0, :z], "-b" => [-1.0, :b] }.each { |valid, expected| P.term(valid).must_equal expected } @@ -23,12 +23,12 @@ describe "Parse.expression" do it "must parse valid expressions" do - { "x + y" => [[1.0, 1.0], [:x, :y]], - "2x - 5y" => [[2.0, -5.0], [:x, :y]], - "-2x - 3y + -42.7z" => [[-2.0, -3.0, -42.7], [:x, :y, :z]], - " -5y + -x " => [[-5.0, -1.0], [:y, :x]], - "a - -b" => [[1.0, 1.0], [:a, :b]], - "a A b" => [[1.0, 1.0, 1.0], [:a, :A, :b]], + { "x + y" => { x: 1.0, y: 1.0 }, + "2x - 5y" => { x: 2.0, y: -5.0 }, + "-2x - 3y + -42.7z" => { x: -2.0, y: -3.0, z: -42.7 }, + " -5y + -x " => { y: -5.0, x: -1.0 }, + "a - -b" => { a: 1.0, b: 1.0 }, + "a A b" => { a: 1.0, :A => 1.0, b: 1.0 }, }.each { |valid, expected| P.expression(valid).must_equal expected } @@ -55,9 +55,9 @@ describe "Parse.inequality" do it "must parse valid inequalities" do - { "x + y <= 4" => [[1.0, 1.0], [:x, :y], 4.0], - "0.94a - 22.1b <= -14.67" => [[0.94, -22.1], [:a, :b], -14.67], - "x <= 0" => [[1.0], [:x], 0], + { "x + y <= 4" => [{ x: 1.0, y: 1.0 }, 4.0], + "0.94a - 22.1b <= -14.67" => [{ a: 0.94, b: -22.1 }, -14.67], + "x <= 0" => [{ x: 1.0 }, 0], }.each { |valid, expected| P.inequality(valid).must_equal expected } @@ -73,3 +73,20 @@ end end end + +describe "Simplex.maximize" do + it "must problem stuff" do + prob = Simplex.problem(maximize: 'x + y', + constraints: ['2x + y <= 4', + 'x + 2y <= 3']) + sol = prob.solution + sol.must_equal [Rational(5, 3), Rational(2, 3)] + end + + it "must maximize stuff" do + Simplex.maximize('x + y', + '2x + y <= 4', + 'x + 2y <= 3').must_equal [Rational(5, 3), + Rational(2, 3)] + end +end