Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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