Skip to content

Commit

Permalink
Merge pull request #8 from ryanjoneil/cbc-and-clp
Browse files Browse the repository at this point in the history
Adds cbc and clp
  • Loading branch information
ryanjoneil authored Jan 8, 2017
2 parents cf0fdfc + e3e87e1 commit 1ed6e44
Show file tree
Hide file tree
Showing 8 changed files with 165 additions and 24 deletions.
17 changes: 15 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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!
8 changes: 7 additions & 1 deletion lib/rams/model.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
require 'tempfile'
require_relative 'variable'
require_relative 'solvers/cbc'
require_relative 'solvers/clp'
require_relative 'solvers/glpk'

module RAMS
Expand Down Expand Up @@ -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
Expand Down
45 changes: 45 additions & 0 deletions lib/rams/solvers/cbc.rb
Original file line number Diff line number Diff line change
@@ -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
45 changes: 45 additions & 0 deletions lib/rams/solvers/clp.rb
Original file line number Diff line number Diff line change
@@ -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
15 changes: 7 additions & 8 deletions lib/rams/solvers/glpk.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 6 additions & 6 deletions lib/rams/solvers/solver.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down
2 changes: 1 addition & 1 deletion rams.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
45 changes: 39 additions & 6 deletions tests/test_model.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand All @@ -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)
Expand All @@ -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
Expand All @@ -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)
Expand All @@ -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

0 comments on commit 1ed6e44

Please sign in to comment.