diff --git a/README.md b/README.md index 9fac6c2..74acb56 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # Ruby Algebraic Modeling System -RAMS is a library for formulating and solving [Mixed Integer Linear Programs](https://en.wikipedia.org/wiki/Integer_programming) in Ruby. Currently, it only supports the [GNU Linear Programming Kit](https://www.gnu.org/software/glpk/), but more solvers are on the way. +RAMS is a library for formulating and solving [Mixed Integer Linear Programs](https://en.wikipedia.org/wiki/Integer_programming) in Ruby. Currently it supports [CLP](https://www.coin-or.org/Clp/), [CBC](https://www.coin-or.org/Cbc/), and [GNU Linear Programming Kit](https://www.gnu.org/software/glpk/), and more solvers are on the way. ## Quick Start -Make sure you have `glpsol` available on your system. On OSX you can do that with `brew`: +GLPK is the default solver, so make sure you at least have `glpsol` available on your system. On OSX you can do that with `brew`: ``` brew install glpk @@ -51,4 +51,17 @@ x2 = 0.0 x3 = 1.0 ``` +If you want to switch to a different solver, simply install that solver onto your system, and change the `solver` attribute on the model. + +```ruby +m.solver = :cbc # or... +m.solver = :clp +``` + +Additional solver arguments can be passed as though they are command line flags. + +```ruby +m.args = ['--dfs', '--bib'] +``` + More examples are available [here](https://github.com/ryanjoneil/rams/tree/master/examples). Happy modeling! diff --git a/lib/rams/model.rb b/lib/rams/model.rb index dde588e..f98a1f8 100644 --- a/lib/rams/model.rb +++ b/lib/rams/model.rb @@ -1,5 +1,7 @@ require 'tempfile' require_relative 'variable' +require_relative 'solvers/cbc' +require_relative 'solvers/clp' require_relative 'solvers/glpk' module RAMS @@ -28,7 +30,11 @@ class Model attr_accessor :objective, :args, :verbose attr_reader :solver, :sense, :variables, :constraints - SOLVERS = { glpk: RAMS::Solvers::GLPK.new }.freeze + SOLVERS = { + cbc: RAMS::Solvers::CBC.new, + clp: RAMS::Solvers::CLP.new, + glpk: RAMS::Solvers::GLPK.new + }.freeze def initialize @solver = :glpk diff --git a/lib/rams/solvers/cbc.rb b/lib/rams/solvers/cbc.rb new file mode 100644 index 0000000..a2f164e --- /dev/null +++ b/lib/rams/solvers/cbc.rb @@ -0,0 +1,45 @@ +require_relative 'solver' + +module RAMS + module Solvers + # Interface to COIN-OR Branch-and-Cut + class CBC < Solver + def solver_command(model_file, solution_file, args) + ['cbc', model_file.path] + args + ['printingOptions', 'all', 'solve', 'solution', solution_file.path] + end + + private + + def parse_status(model, lines) + return :undefined if lines.count < 1 + status = lines.first + return :optimal if status =~ /Optimal/ + return :feasible if status =~ /Stopped/ + return :infeasible if status =~ /Infeasible/ + return :unbounded if status =~ /Unbounded/ + :undefined + end + + def parse_objective(model, lines) + return nil if lines.count < 1 + objective = lines.first.split[-1].to_f + model.sense == :max ? -objective : objective + end + + def parse_primal(model, lines) + lines[model.constraints.count + 1, model.variables.count].map do |l| + comps = l.split + [model.variables[comps[1]], comps[2].to_f] + end.to_h + end + + def parse_dual(model, lines) + lines[1, model.constraints.count].map do |l| + comps = l.split + dual = model.sense == :max ? -comps[3].to_f : comps[3].to_f + [model.constraints[comps[1]], dual] + end.to_h + end + end + end +end diff --git a/lib/rams/solvers/clp.rb b/lib/rams/solvers/clp.rb new file mode 100644 index 0000000..b28dad2 --- /dev/null +++ b/lib/rams/solvers/clp.rb @@ -0,0 +1,45 @@ +require_relative 'solver' + +module RAMS + module Solvers + # Interface to COIN-OR Linear Programming + class CLP < Solver + def solver_command(model_file, solution_file, args) + ['clp', model_file.path] + args + ['printingOptions', 'all', 'solve', 'solution', solution_file.path] + end + + private + + def parse_status(model, lines) + return :undefined if lines.count < 1 + status = lines.first + return :optimal if status =~ /optimal/ + return :feasible if status =~ /stopped/ + return :infeasible if status =~ /infeasible/ + return :unbounded if status =~ /unbounded/ + :undefined + end + + def parse_objective(model, lines) + return nil if lines.count < 2 + objective = lines[1].split[-1].to_f + model.sense == :max ? -objective : objective + end + + def parse_primal(model, lines) + lines[model.constraints.count + 2, model.variables.count].map do |l| + comps = l.split + [model.variables[comps[1]], comps[2].to_f] + end.to_h + end + + def parse_dual(model, lines) + lines[2, model.constraints.count].map do |l| + comps = l.split + dual = model.sense == :max ? -comps[3].to_f : comps[3].to_f + [model.constraints[comps[1]], dual] + end.to_h + end + end + end +end diff --git a/lib/rams/solvers/glpk.rb b/lib/rams/solvers/glpk.rb index 378a596..03daba7 100644 --- a/lib/rams/solvers/glpk.rb +++ b/lib/rams/solvers/glpk.rb @@ -4,36 +4,35 @@ module RAMS module Solvers # Interface to the GNU Linear Programming Kit class GLPK < Solver - def solver_command(model_file, solution_file) - ['glpsol', '--lp', model_file.path, '--output', solution_file.path] + def solver_command(model_file, solution_file, args) + ['glpsol', '--lp', model_file.path, '--output', solution_file.path] + args end private - def parse_status(lines) + def parse_status(_model, lines) status = lines.select { |l| l =~ /^Status/ }.first return :optimal if status =~ /OPTIMAL/ return :feasible if status =~ /FEASIBLE/ return :infeasible if status =~ /EMPTY/ + return :unbounded if status =~ /UNBOUNDED/ :undefined end - def parse_objective(lines) + def parse_objective(_model, lines) lines.select { |l| l =~ /^Objective/ }.first.split[3].to_f end def parse_primal(model, lines) - primal = model.variables.values.map { |v| [v, 0.0] }.to_h start_idx = lines.index { |l| l =~ /Column name/ } + 2 length = lines[start_idx, lines.length].index { |l| l == '' } - primal.update(lines[start_idx, length].map { |l| [model.variables[l[7, 12].strip], l[23, 13].to_f] }.to_h) + lines[start_idx, length].map { |l| [model.variables[l[7, 12].strip], l[23, 13].to_f] }.to_h end def parse_dual(model, lines) - duals = model.constraints.values.map { |c| [c, 0.0] }.to_h start_idx = lines.index { |l| l =~ /Row name/ } + 2 length = lines[start_idx, lines.length].index { |l| l == '' } - duals.update(lines[start_idx, length].map { |l| [model.constraints[l[7, 12].strip], l[-13, 13].to_f] }.to_h) + lines[start_idx, length].map { |l| [model.constraints[l[7, 12].strip], l[-13, 13].to_f] }.to_h end end end diff --git a/lib/rams/solvers/solver.rb b/lib/rams/solvers/solver.rb index 7ff4290..adc9c35 100644 --- a/lib/rams/solvers/solver.rb +++ b/lib/rams/solvers/solver.rb @@ -39,7 +39,7 @@ def solve_and_parse(model, model_file, solution_file) # rubocop:disable MethodLength def call_solver(model, model_file, solution_file) - command = solver_command(model_file, solution_file) + model.args + command = solver_command(model_file, solution_file, model.args) _, stdout, stderr, exit_code = Open3.popen3(*command) begin @@ -54,25 +54,25 @@ def call_solver(model, model_file, solution_file) end # rubocop:enable MethodLength - def solver_command(_model_file, _solution_file) + def solver_command(_model_file, _solution_file, _args) raise NotImplementedError end def parse_solution(model, solution_text) lines = solution_text.split "\n" RAMS::Solution.new( - parse_status(lines), - parse_objective(lines), + parse_status(model, lines), + parse_objective(model, lines), parse_primal(model, lines), parse_dual(model, lines) ) end - def parse_status(_lines) + def parse_status(_model, _lines) raise NotImplementedError end - def parse_objective(_lines) + def parse_objective(_model, _lines) raise NotImplementedError end diff --git a/rams.gemspec b/rams.gemspec index 20d457e..6c909b5 100644 --- a/rams.gemspec +++ b/rams.gemspec @@ -4,7 +4,7 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) Gem::Specification.new do |spec| spec.name = 'rams' - spec.version = '0.1' + spec.version = '0.1.1' spec.authors = ["Ryan J. O'Neil"] spec.email = ['ryanjoneil@gmail.com'] spec.summary = 'Ruby Algebraic Modeling System' diff --git a/tests/test_model.rb b/tests/test_model.rb index e9ae701..3699e13 100644 --- a/tests/test_model.rb +++ b/tests/test_model.rb @@ -3,9 +3,38 @@ # RAMS::Model tests class TestModel < Test::Unit::TestCase - # rubocop:disable AbcSize, MethodLength def test_simple + run_test_simple :cbc + run_test_simple :clp + run_test_simple :glpk + end + + def test_binary + run_test_binary :cbc + run_test_binary :glpk + end + + def test_integer + run_test_integer :cbc + run_test_integer :glpk + end + + def test_infeasible + run_test_infeasible :cbc + run_test_infeasible :clp + run_test_infeasible :glpk + end + + def test_unbounded + run_test_unbounded :cbc + run_test_unbounded :clp + run_test_unbounded :glpk + end + + # rubocop:disable AbcSize, MethodLength + def run_test_simple(solver) m = RAMS::Model.new + m.solver = solver x1 = m.variable x2 = m.variable @@ -26,8 +55,9 @@ def test_simple # rubocop:enable AbcSize, MethodLength # rubocop:disable AbcSize, MethodLength - def test_binary + def run_test_binary(solver) m = RAMS::Model.new + m.solver = solver x1 = m.variable type: :binary x2 = m.variable type: :binary @@ -52,8 +82,9 @@ def test_binary end # rubocop:enable AbcSize, MethodLength - def test_integer + def run_test_integer(solver) m = RAMS::Model.new + m.solver = solver x = m.variable type: :integer m.constrain(x <= 1.5) @@ -68,8 +99,9 @@ def test_integer end # rubocop:disable MethodLength - def test_infeasible + def run_test_infeasible(solver) m = RAMS::Model.new + m.solver = solver x1 = m.variable type: :binary, high: 0 x2 = m.variable type: :binary, high: 0 @@ -86,8 +118,9 @@ def test_infeasible end # rubocop:enable MethodLength - def test_unbounded + def run_test_unbounded(solver) m = RAMS::Model.new + m.solver = solver x = m.variable type: :integer m.constrain(x >= 1) @@ -96,6 +129,6 @@ def test_unbounded m.objective = x solution = m.solve - assert_equal :undefined, solution.status + assert_includes [:unbounded, :undefined], solution.status end end