diff --git a/lib/ruby-cbc.rb b/lib/ruby-cbc.rb index b917e86..94b6ba8 100644 --- a/lib/ruby-cbc.rb +++ b/lib/ruby-cbc.rb @@ -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| diff --git a/lib/ruby-cbc/utils/c_string.rb b/lib/ruby-cbc/utils/c_string.rb new file mode 100644 index 0000000..8a1189b --- /dev/null +++ b/lib/ruby-cbc/utils/c_string.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Cbc + module Utils + class CString + NULL_CHAR = "\x00" + + # @param null_terminated_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 diff --git a/lib/ruby-cbc/utils/mps.rb b/lib/ruby-cbc/utils/mps.rb new file mode 100644 index 0000000..57fe7b4 --- /dev/null +++ b/lib/ruby-cbc/utils/mps.rb @@ -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 diff --git a/lib/ruby-cbc/utils/problem_unwrap.rb b/lib/ruby-cbc/utils/problem_unwrap.rb new file mode 100644 index 0000000..e7cad46 --- /dev/null +++ b/lib/ruby-cbc/utils/problem_unwrap.rb @@ -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 diff --git a/spec/utils/c_string_spec.rb b/spec/utils/c_string_spec.rb new file mode 100644 index 0000000..87e3d62 --- /dev/null +++ b/spec/utils/c_string_spec.rb @@ -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 diff --git a/spec/utils/prob.mps b/spec/utils/prob.mps new file mode 100644 index 0000000..6a13e6b --- /dev/null +++ b/spec/utils/prob.mps @@ -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 diff --git a/spec/utils/problem_unwrap_spec.rb b/spec/utils/problem_unwrap_spec.rb new file mode 100644 index 0000000..f130627 --- /dev/null +++ b/spec/utils/problem_unwrap_spec.rb @@ -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