Skip to content
Draft
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
3 changes: 3 additions & 0 deletions lib/ruby-cbc.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ module Cbc
ilp/term_array
ilp/var
utils/compressed_row_storage
utils/c_string
utils/problem_unwrap
utils/mps
]

files.each do |file|
Expand Down
17 changes: 17 additions & 0 deletions lib/ruby-cbc/utils/c_string.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# frozen_string_literal: true

module Cbc
module Utils
class CString
NULL_CHAR = "\x00"

# @param null_terminated_string <String>
def self.from_c(null_terminated_string)
null_index = null_terminated_string.index(NULL_CHAR)
return null_terminated_string if null_index.nil?

null_terminated_string[0, null_index]
end
end
end
end
18 changes: 18 additions & 0 deletions lib/ruby-cbc/utils/mps.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
module Cbc
module Utils
class Mps
def initialize(mps_file)
@mps_file = mps_file
end

def to_model
cbc_model = Cbc_wrapper.Cbc_newModel
Cbc_wrapper.Cbc_readMps(cbc_model, @mps_file)

model = Utils::ProblemUnwrap.new(cbc_model).to_model
Cbc_wrapper.Cbc_deleteModel(cbc_model)
model
end
end
end
end
146 changes: 146 additions & 0 deletions lib/ruby-cbc/utils/problem_unwrap.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
# frozen_string_literal: true

module Cbc
module Utils
class ProblemUnwrap
attr_reader :cbc_model

def initialize(cbc_model)
@cbc_model = cbc_model
end

def to_model
m = Model.new(name: problem_name)

variables(m)
constraints(m)
objective(m)
m
end

private

def variables(model)
lower_bounds = double_array(Cbc_wrapper.Cbc_getColLower(cbc_model))
upper_bounds = double_array(Cbc_wrapper.Cbc_getColUpper(cbc_model))

nb_vars.times do |i|
range = float_value(lower_bounds[i])..float_value(upper_bounds[i])
if Cbc_wrapper.Cbc_isInteger(cbc_model, i) == 1
model.int_var(range, name: var_name(i))
else
model.cont_var(range, name: var_name(i))
end
end
end

def constraints(model)
lower_bounds = double_array(Cbc_wrapper.Cbc_getRowLower(cbc_model))
upper_bounds = double_array(Cbc_wrapper.Cbc_getRowUpper(cbc_model))

nb_elements = Cbc_wrapper.Cbc_getNumElements(cbc_model)
starts = int_array(Cbc_wrapper.Cbc_getVectorStarts(cbc_model))
indices = int_array(Cbc_wrapper.Cbc_getIndices(cbc_model))
elements = double_array(Cbc_wrapper.Cbc_getElements(cbc_model))

cons_terms = Array.new(nb_cons) { [] }

var_idx = 0
while var_idx < nb_vars
from = starts[var_idx]
to = var_idx == nb_vars - 1 ? nb_elements : starts[var_idx + 1]

var = model.vars[var_idx]
(from...to).each do |i|
cons_idx = indices[i]
mult = elements[i]
cons_terms[cons_idx] << var * mult
end

var_idx += 1
end

cons_terms.each_with_index do |terms, cons_idx|
low = float_value(lower_bounds[cons_idx])
up = float_value(upper_bounds[cons_idx])

name = cons_name(cons_idx)
terms = Ilp::TermArray.new(terms)

if low == up
model.enforce(name => terms == low)
else
model.enforce(name => terms >= low) if low != -Cbc::INF
model.enforce(name => terms <= up) if up != Cbc::INF
end
end
end

OBJ_IGNORE = 0
OBJ_MIN = 1
OBJ_MAX = -1

def objective(model)
obj_sense = Cbc_wrapper.Cbc_getObjSense(cbc_model)
return if obj_sense == OBJ_IGNORE

coeffs = double_array(Cbc_wrapper.Cbc_getObjCoefficients(cbc_model))
terms = (0...nb_vars).map { |i| model.vars[i] * coeffs[i] }

if obj_sense == OBJ_MIN
model.minimize(Cbc.add_all(terms))
else
model.maximize(Cbc.add_all(terms))
end
end

def problem_name
name = " " * 40
Cbc_wrapper.Cbc_problemName(cbc_model, 40, name)
CString.from_c(name)
end

def float_value(val)
return Cbc::INF if val == Float::MAX
return -Cbc::INF if val == -Float::MAX

val
end

def nb_vars
Cbc_wrapper.Cbc_getNumCols(cbc_model)
end

def nb_cons
Cbc_wrapper.Cbc_getNumRows(cbc_model)
end

def var_name(var_idx)
Cbc_wrapper.Cbc_getColName(cbc_model, var_idx, name_container, max_name_size)
CString.from_c(name_container)
end

def cons_name(cons_idx)
Cbc_wrapper.Cbc_getRowName(cbc_model, cons_idx, name_container, max_name_size)
CString.from_c(name_container)
end

def name_container
@name_container ||= " " * max_name_size
end

def max_name_size
# Need to leave space for null char
@max_name_size ||= Cbc_wrapper.Cbc_maxNameLength(cbc_model) + 1
end

def int_array(ptr)
Cbc_wrapper::IntArray.frompointer(ptr)
end

def double_array(ptr)
Cbc_wrapper::DoubleArray.frompointer(ptr)
end
end
end
end
30 changes: 30 additions & 0 deletions spec/utils/c_string_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
require "spec_helper"

module Cbc
module Utils
describe CString do
it "returns the ruby string when trailing null chars exist" do
bytes = [78, 65, 77, 69, 0, 0, 0, 0, 0, 0]
c_string = bytes.map(&:chr).join
expect(CString.from_c(c_string)).to eq "NAME"
end

it "returns the ruby string when no trailing space exist" do
bytes = [78, 65, 77, 69]
c_string = bytes.map(&:chr).join
expect(CString.from_c(c_string)).to eq "NAME"
end

it "returns the empty string when the c string is only null chars" do
bytes = [0]
c_string = bytes.map(&:chr).join
expect(CString.from_c(c_string)).to be_empty
end

it "returns the empty string when the c string empty" do
c_string = ""
expect(CString.from_c(c_string)).to be_empty
end
end
end
end
70 changes: 70 additions & 0 deletions spec/utils/prob.mps
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
************************************************************************
*
* The data in this file represents the following problem:
*
* Minimize or maximize Z = x1 + 2x5 - x8
*
* Subject to:
*
* 2.5 <= 3x1 + x2 - 2x4 - x5 - x8
* 2x2 + 1.1x3 <= 2.1
* x3 + x6 = 4.0
* 1.8 <= 2.8x4 -1.2x7 <= 5.0
* 3.0 <= 5.6x1 + x5 + 1.9x8 <= 15.0
*
* where:
*
* 2.5 <= x1
* 0 <= x2 <= 4.1
* 0 <= x3
* 0 <= x4
* 0.5 <= x5 <= 4.0
* 0 <= x6
* 0 <= x7
* 0 <= x8 <= 4.3
*
* x3, x4 are 0,1 variables.
*
************************************************************************
NAME EXAMPLE
ROWS
N OBJ
G ROW01
L ROW02
E ROW03
G ROW04
L ROW05
COLUMNS
COL01 OBJ 1.0
COL01 ROW01 3.0 ROW05 5.6
COL02 ROW01 1.0 ROW02 2.0
*
* Mark COL03 and COL04 as integer variables.
*
INT1 'MARKER' 'INTORG'
COL03 ROW02 1.1 ROW03 1.0
COL04 ROW01 -2.0 ROW04 2.8
INT1END 'MARKER' 'INTEND'
*
COL05 OBJ 2.0
COL05 ROW01 -1.0 ROW05 1.0
COL06 ROW03 1.0
COL07 ROW04 -1.2
COL08 OBJ -1.0
COL08 ROW01 -1.0 ROW05 1.9
RHS
RHS1 ROW01 2.5
RHS1 ROW02 2.1
RHS1 ROW03 4.0
RHS1 ROW04 1.8
RHS1 ROW05 15.0
RANGES
RNG1 ROW04 3.2
RNG1 ROW05 12.0
BOUNDS
LO BND1 COL01 2.5
UP BND1 COL02 4.1
LO BND1 COL05 0.5
UP BND1 COL05 4.0
UP BND1 COL08 4.3
ENDATA
65 changes: 65 additions & 0 deletions spec/utils/problem_unwrap_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# frozen_string_literal: true

require "spec_helper"

module Cbc
module Utils
describe ProblemUnwrap do
def problem_unwrap
model = Cbc_wrapper.Cbc_newModel
Cbc_wrapper.Cbc_readMps(model, "./spec/utils/prob.mps")
ProblemUnwrap.new model
end

it "returns the right name" do
p = problem_unwrap
expect(p.to_model.name).to eq "EXAMPLE"
end

it "gets the variables right" do
p = problem_unwrap
vars = p.to_model.vars
expect(vars.size).to eq 8

col01 = vars.find { |var| var.name == "COL01" }
expect(col01).to_not be_nil
expect(col01).to have_attributes(lower_bound: 2.5, upper_bound: Cbc::INF, kind: :continuous)

col02 = vars.find { |var| var.name == "COL02" }
expect(col02).to_not be_nil
expect(col02).to have_attributes(lower_bound: 0, upper_bound: 4.1, kind: :continuous)

col03 = vars.find { |var| var.name == "COL03" }
expect(col03).to_not be_nil
expect(col03).to have_attributes(lower_bound: 0, upper_bound: 1.0, kind: :integer)

col05 = vars.find { |var| var.name == "COL05" }
expect(col05).to_not be_nil
expect(col05).to have_attributes(lower_bound: 0.5, upper_bound: 4.0, kind: :continuous)
end

it "gets the constraints right" do
p = problem_unwrap
constraints = p.to_model.constraints
expect(constraints.size).to eq 7

row01 = constraints.find { |cons| cons.function_name == "ROW01" }
expect(row01).to_not be_nil
expect(row01.to_s)
.to eq "+ 3.0 COL01 + COL02 - 2.0 COL04 - 1.0 COL05 - 1.0 COL08 >= 2.5"

row02 = constraints.find { |cons| cons.function_name == "ROW02" }
expect(row02).to_not be_nil
expect(row02.to_s)
.to eq "+ 2.0 COL02 + 1.1 COL03 <= 2.1"
end

it "gets the objective right" do
p = problem_unwrap
obj = p.to_model.objective

expect(obj.to_s).to eq "Minimize\n + COL01 + 2.0 COL05 - 1.0 COL08"
end
end
end
end