├── lib ├── rulp │ ├── version.rb │ ├── constraint.rb │ ├── rulp_initializers.rb │ ├── rulp_bounds.rb │ ├── lv.rb │ ├── expression.rb │ └── rulp.rb ├── .DS_Store ├── rulp.rb ├── helpers │ ├── file_helpers.rb │ └── log.rb ├── extensions │ ├── extensions.rb │ ├── file_extensions.rb │ ├── os_extensions.rb │ ├── object_extensions.rb │ └── kernel_extensions.rb └── solvers │ ├── glpk.rb │ ├── cbc.rb │ ├── gurobi.rb │ ├── solver.rb │ ├── scip.rb │ └── highs.rb ├── Rakefile ├── Gemfile ├── bin └── rulp ├── test ├── test_negate_term.rb ├── test_helper.rb ├── test_negate_expression.rb ├── test_infeasible.rb ├── test_boolean.rb ├── test_save_to_file.rb ├── test_simple.rb └── test_basic_suite.rb ├── Gemfile.lock ├── rulp.gemspec ├── examples ├── boolean_example.rb ├── simple_example.rb └── whiskas_model2.rb └── README.md /lib/rulp/version.rb: -------------------------------------------------------------------------------- 1 | module Rulp 2 | VERSION = '0.0.50' 3 | end 4 | -------------------------------------------------------------------------------- /lib/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wouterken/rulp/HEAD/lib/.DS_Store -------------------------------------------------------------------------------- /lib/rulp.rb: -------------------------------------------------------------------------------- 1 | STDOUT.sync = true 2 | require_relative 'rulp/version' 3 | require_relative 'rulp/rulp' 4 | -------------------------------------------------------------------------------- /lib/helpers/file_helpers.rb: -------------------------------------------------------------------------------- 1 | module FileHelpers 2 | def get_unique_filename(label: nil) 3 | unique 4 | while 5 | end 6 | end -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake/testtask' 2 | 3 | Rake::TestTask.new do |t| 4 | t.libs << 'test' 5 | end 6 | 7 | desc "Run tests" 8 | task :default => :test -------------------------------------------------------------------------------- /lib/extensions/extensions.rb: -------------------------------------------------------------------------------- 1 | require_relative './kernel_extensions' 2 | require_relative './object_extensions' 3 | require_relative './file_extensions' 4 | require_relative './os_extensions' 5 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # A sample Gemfile 2 | source "https://rubygems.org" 3 | 4 | gem "rake" 5 | 6 | group :development, :test do 7 | gem "minitest", "5.5.1" 8 | gem 'pry' 9 | gem 'pry-byebug' 10 | end 11 | -------------------------------------------------------------------------------- /bin/rulp: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative '../lib/rulp' 3 | begin 4 | require 'pry' 5 | load Gem.bin_path('pry', 'pry', ">= 0.9") 6 | rescue 7 | require 'irb' 8 | ARGV.clear # otherwise all script parameters get passed to IRB 9 | IRB.start 10 | end 11 | -------------------------------------------------------------------------------- /lib/extensions/file_extensions.rb: -------------------------------------------------------------------------------- 1 | def choose_file 2 | case os 3 | when :macosx 4 | command = "osascript -e 'set the_file to choose file name with prompt \"Select an output file\"' -e 'set the result to POSIX path of the_file'" 5 | File.absolute_path(`#{command}`.strip) 6 | else 7 | File.absolute_path(gets) 8 | end 9 | end -------------------------------------------------------------------------------- /test/test_negate_term.rb: -------------------------------------------------------------------------------- 1 | require_relative 'test_helper' 2 | 3 | class NegateTerm < Minitest::Test 4 | def setup 5 | @problem = Rulp::Max(-X1_i + 2 * X2_i) 6 | @problem[ 7 | X1_i + X2_i == 10, 8 | X1_i >= 0, 9 | X2_i >= 0 10 | ] 11 | 12 | @problem.solve 13 | end 14 | 15 | def test_negate_term 16 | assert X1_i.value == 0 17 | assert X2_i.value == 10 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require_relative "../lib/rulp" 2 | require 'logger' 3 | 4 | gem "minitest" 5 | require "minitest/autorun" 6 | 7 | Rulp::log_level = Logger::UNKNOWN 8 | Rulp::print_solver_outputs = false 9 | 10 | def each_solver 11 | [:scip, :cbc, :glpk, :gurobi, :highs].each do |solver| 12 | LV::clear 13 | if Rulp::solver_exists?(solver) 14 | yield(solver) 15 | else 16 | Rulp::log(Logger::INFO, "Couldn't find solver #{solver}") 17 | end 18 | end 19 | end -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | byebug (11.1.3) 5 | coderay (1.1.3) 6 | method_source (1.1.0) 7 | minitest (5.5.1) 8 | pry (0.14.2) 9 | coderay (~> 1.1) 10 | method_source (~> 1.0) 11 | pry-byebug (3.10.1) 12 | byebug (~> 11.0) 13 | pry (>= 0.13, < 0.15) 14 | rake (12.3.3) 15 | 16 | PLATFORMS 17 | ruby 18 | 19 | DEPENDENCIES 20 | minitest (= 5.5.1) 21 | pry 22 | pry-byebug 23 | rake 24 | 25 | BUNDLED WITH 26 | 2.5.3 27 | -------------------------------------------------------------------------------- /lib/extensions/os_extensions.rb: -------------------------------------------------------------------------------- 1 | def os 2 | @os ||= ( 3 | require "rbconfig" 4 | host_os = RbConfig::CONFIG['host_os'].downcase 5 | 6 | case host_os 7 | when /linux/ 8 | :linux 9 | when /darwin|mac os/ 10 | :macosx 11 | when /mswin|msys|mingw32/ 12 | :windows 13 | when /cygwin/ 14 | :cygwin 15 | when /solaris|sunos/ 16 | :solaris 17 | when /bsd/ 18 | :bsd 19 | when /aix/ 20 | :aix 21 | else 22 | raise Error, "unknown os: #{host_os.inspect}" 23 | end 24 | ) 25 | end -------------------------------------------------------------------------------- /rulp.gemspec: -------------------------------------------------------------------------------- 1 | lib = File.expand_path('../lib', __FILE__) 2 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 3 | require 'rulp/version' 4 | require 'date' 5 | 6 | Gem::Specification.new do |s| 7 | s.name = 'rulp' 8 | s.version = Rulp::VERSION 9 | s.date = Date.today 10 | s.summary = "Ruby Linear Programming" 11 | s.description = "A simple Ruby LP description DSL" 12 | s.authors = ["Wouter Coppieters"] 13 | s.email = 'wc@pico.net.nz' 14 | s.files = Dir["lib/**/*"] 15 | s.test_files = Dir["test/**/*"] 16 | s.executables << 'rulp' 17 | end 18 | -------------------------------------------------------------------------------- /test/test_negate_expression.rb: -------------------------------------------------------------------------------- 1 | require_relative 'test_helper' 2 | 3 | class NegateExpressionTest < Minitest::Test 4 | 5 | def test_negate_expression 6 | @problem = Rulp::Max(- (X1_i + 2 * X2_i)) 7 | @problem[ 8 | X1_i + X2_i == 10, 9 | X1_i >= 0, 10 | X2_i >= 0 11 | ] 12 | 13 | @problem.solve 14 | assert X1_i.value == 10 15 | assert X2_i.value == 0 16 | end 17 | 18 | def test_double_negate_expression 19 | @problem = Rulp::Max(-(-(X1_i + 2 * X2_i))) 20 | @problem[ 21 | X1_i + X2_i == 10, 22 | X1_i >= 0, 23 | X2_i >= 0 24 | ] 25 | 26 | @problem.solve 27 | assert X1_i.value == 0 28 | assert X2_i.value == 10 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/rulp/constraint.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | ## 4 | # An LP Expression constraint. A mathematical expression of which the result 5 | # must be constrained in some way. 6 | ## 7 | class Constraint 8 | def initialize(*constraint_expression) 9 | @expressions , @constraint_op, @value = constraint_expression 10 | end 11 | 12 | def variables 13 | @expressions.variables 14 | end 15 | 16 | def to_s 17 | return "#{@expressions} #{constraint_op} #{@value}" 18 | end 19 | 20 | def constraint_op 21 | case "#{@constraint_op}" 22 | when "==" 23 | "=" 24 | when "<" 25 | "<=" 26 | when ">" 27 | ">=" 28 | else 29 | @constraint_op 30 | end 31 | end 32 | end -------------------------------------------------------------------------------- /lib/helpers/log.rb: -------------------------------------------------------------------------------- 1 | require 'logger' 2 | 3 | module Rulp 4 | module Log 5 | 6 | def print_solver_outputs=(print) 7 | @@print_solver_outputs = print 8 | end 9 | 10 | def print_solver_outputs 11 | @@print_solver_outputs 12 | end 13 | 14 | def log_level=(level) 15 | @@log_level = level 16 | end 17 | 18 | def log_level 19 | @@log_level || Logger::DEBUG 20 | end 21 | 22 | def log(level, message) 23 | if level >= self.log_level 24 | self.logger.add(level, message) 25 | end 26 | end 27 | 28 | def logger=(logger) 29 | @@logger = logger 30 | end 31 | 32 | def logger 33 | @@logger ||= Logger.new(STDOUT) 34 | end 35 | end 36 | end -------------------------------------------------------------------------------- /examples/boolean_example.rb: -------------------------------------------------------------------------------- 1 | require_relative "../lib/rulp" 2 | 3 | Rulp::log_level = Logger::INFO 4 | 5 | ## 6 | # 7 | # Given 50 items of varying prices 8 | # Get the minimal sum of 10 items that equals at least $15 dollars 9 | # 10 | ## 11 | 12 | items = 50.times.map(&Shop_Item_b) 13 | items_count = items.inject(:+) 14 | items_costs = items.map{|item| item * Random.rand(1.0...5.0)}.inject(:+) 15 | 16 | Rulp::Min( items_costs ) [ 17 | items_count >= 10, 18 | items_costs >= 15 19 | ].solve 20 | 21 | 22 | cost = items_costs.evaluate 23 | puts items.map(&:inspect) 24 | ## 25 | # 'cost' is the result of the objective function. 26 | # You can retrieve allocations by querying the variables. 27 | # E.g 28 | # Shop_Item_b(4).value 29 | ## 30 | -------------------------------------------------------------------------------- /test/test_infeasible.rb: -------------------------------------------------------------------------------- 1 | require_relative 'test_helper' 2 | ## 3 | # 4 | # Given 50 items of varying prices 5 | # Get the minimal sum of 10 items that equals at least $15 dollars 6 | # 7 | ## 8 | class InfeasibleTest < Minitest::Test 9 | def setup 10 | @items = 30.times.map(&Shop_Item_b) 11 | items_count = @items.inject(:+) 12 | @items_costs = @items.map{|item| item * Random.rand(1.0...5.0)}.inject(:+) 13 | 14 | @problem = 15 | Rulp::Min( @items_costs ) [ 16 | items_count >= 10, 17 | @items_costs >= 150_000 18 | ] 19 | end 20 | 21 | def test_simple 22 | each_solver do |solver| 23 | assert_raises RuntimeError do 24 | @problem.send(solver) 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/rulp/rulp_initializers.rb: -------------------------------------------------------------------------------- 1 | module Rulp 2 | module Initializers 3 | def initialize(name, args) 4 | @name = name 5 | @args = args 6 | @value = nil 7 | @identifier = "#{self.name}#{self.args.join("_")}" 8 | raise StandardError.new("Variable with the name #{self} of a different type (#{LV::names_table[self.to_s].class}) already exists") if LV::names_table[self.to_s] 9 | LV::names_table[self.to_s] = self 10 | end 11 | 12 | def self.included(base) 13 | base.extend(ClassMethods) 14 | end 15 | 16 | module ClassMethods 17 | def names_table 18 | @@names ||= {} 19 | end 20 | 21 | def clear 22 | @@names = {} 23 | end 24 | end 25 | 26 | def to_s 27 | @identifier 28 | end 29 | end 30 | end 31 | 32 | -------------------------------------------------------------------------------- /test/test_boolean.rb: -------------------------------------------------------------------------------- 1 | require_relative 'test_helper' 2 | ## 3 | # 4 | # Given 50 items of varying prices 5 | # Get the minimal sum of 10 items that equals at least $15 dollars 6 | # 7 | ## 8 | class BooleanTest < Minitest::Test 9 | def setup 10 | @items = 30.times.map(&Shop_Item_b) 11 | items_count = @items.inject(:+) 12 | @items_costs = @items.map{|item| item * Random.rand(1.0...5.0)}.inject(:+) 13 | 14 | @problem = 15 | Rulp::Min( @items_costs ) [ 16 | items_count >= 10, 17 | @items_costs >= 15 18 | ] 19 | end 20 | 21 | def test_simple 22 | each_solver do |solver| 23 | setup 24 | @problem.send(solver) 25 | selected = @items.select(&:value) 26 | assert_equal selected.length, 10 27 | assert_operator @items_costs.evaluate.round(2), :>=, 15 28 | assert_operator @items_costs.evaluate, :<=, 25 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/solvers/glpk.rb: -------------------------------------------------------------------------------- 1 | class Glpk < Solver 2 | def solve 3 | command = "#{executable} --lp #{@filename} %s --cuts --output #{@outfile}" 4 | command %= [ 5 | options[:gap] ? "--mipgap #{options[:gap]}" : "", 6 | options[:time_limit] ? "--tmlim #{options[:time_limit]}" : "" 7 | ].join(" ") 8 | exec(command) 9 | end 10 | 11 | def self.executable 12 | :glpsol 13 | end 14 | 15 | def store_results(variables) 16 | rows = IO.read(@outfile).split("\n") 17 | objective_str = rows[5].split(/\s+/)[-2] 18 | vars_by_name = {} 19 | cols = [] 20 | rows[1..-1].each do |row| 21 | cols.concat(row.strip.split(/\s+/)) 22 | if cols.length > 4 23 | vars_by_name[cols[1].to_s] = cols[3].to_f 24 | cols = [] 25 | end 26 | end 27 | variables.each do |var| 28 | var.value = vars_by_name[var.to_s].to_f 29 | end 30 | self.unsuccessful = rows[-3].downcase.include?('infeasible') 31 | return objective_str.to_f 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /examples/simple_example.rb: -------------------------------------------------------------------------------- 1 | require_relative "../lib/rulp" 2 | 3 | 4 | Rulp::log_level = Logger::DEBUG 5 | 6 | # maximize 7 | # objective = 10 * x + 6 * y + 4 * z 8 | # 9 | # subject to 10 | # p: x + y + z <= 100 11 | # q: 10 * x + 4 * y + 5 * z <= 600 12 | # r: 2 * x + 2 * y + 6 * z <= 300 13 | # 14 | # where all variables are non-negative integers 15 | # x >= 0, y >= 0, z >= 0 16 | # 17 | 18 | 19 | given[ 20 | 21 | X_i >= 0, 22 | Y_i >= 0, 23 | Z_i >= 0 24 | 25 | ] 26 | 27 | Rulp::Max( objective = 10 * X_i + 6 * Y_i + 4 * Z_i ) [ 28 | X_i + Y_i + Z_i <= 100, 29 | 10 * X_i + 4 * Y_i + 5 * Z_i <= 600, 30 | 2 * X_i + 2 * Y_i + 6 * Z_i <= 300 31 | ].cbc 32 | 33 | result = objective.evaluate 34 | 35 | ## 36 | # 'result' is the result of the objective function. 37 | # You can retrieve the values of variables by using the 'value' method 38 | # E.g 39 | # X_i.value == 32 40 | # Y_i.value == 67 41 | # Z_i.value == 0 42 | ## -------------------------------------------------------------------------------- /test/test_save_to_file.rb: -------------------------------------------------------------------------------- 1 | require_relative 'test_helper' 2 | # maximize 3 | # z = 10 * x + 6 * y + 4 * z 4 | # 5 | # subject to 6 | # p: x + y + z <= 100 7 | # q: 10 * x + 4 * y + 5 * z <= 600 8 | # r: 2 * x + 2 * y + 6 * z <= 300 9 | # 10 | # where all variables are non-negative integers 11 | # x >= 0, y >= 0, z >= 0 12 | # 13 | 14 | class SaveToFile < Minitest::Test 15 | def setup 16 | LV::clear 17 | given[ X_i >= 0, Y_i >= 0, Z_i >= 0 ] 18 | @objective = 10 * X_i + 6 * Y_i + 4 * Z_i 19 | @problem = Rulp::Max( @objective ) [ 20 | X_i + Y_i + Z_i <= 100, 21 | 10 * X_i + 4 * Y_i + 5 * Z_i <= 600, 22 | 2 * X_i + 2 * Y_i + 6 * Z_i <= 300 23 | ] 24 | end 25 | 26 | def test_save 27 | sample_output_filename = @problem.get_output_filename 28 | @problem.save(sample_output_filename) 29 | assert_equal(IO.read(sample_output_filename), "#{@problem}") 30 | assert_operator "#{@problem}".length, :>=, 100 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /test/test_simple.rb: -------------------------------------------------------------------------------- 1 | require_relative 'test_helper' 2 | # maximize 3 | # z = 10 * x + 6 * y + 4 * z 4 | # 5 | # subject to 6 | # p: x + y + z <= 100 7 | # q: 10 * x + 4 * y + 5 * z <= 600 8 | # r: 2 * x + 2 * y + 6 * z <= 300 9 | # 10 | # where all variables are non-negative integers 11 | # x >= 0, y >= 0, z >= 0 12 | # 13 | 14 | class SimpleTest < Minitest::Test 15 | def setup 16 | given[ X_i >= 0, Y_i >= 0, Z_i >= 0 ] 17 | @objective = 10 * X_i + 6 * Y_i + 4 * Z_i 18 | @problem = Rulp::Max( @objective ) [ 19 | X_i + Y_i + Z_i <= 100, 20 | 10 * X_i + 4 * Y_i + 5 * Z_i <= 600, 21 | 2 * X_i + 2 * Y_i + 6 * Z_i <= 300 22 | ] 23 | end 24 | 25 | def test_simple 26 | each_solver do |solver| 27 | setup 28 | @problem.send(solver) 29 | assert_equal X_i.value, 33 30 | assert_equal Y_i.value, 67 31 | assert_equal Z_i.value, 0 32 | assert_equal @objective.evaluate , 732 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/extensions/object_extensions.rb: -------------------------------------------------------------------------------- 1 | ## 2 | # Object extension to allow numbered LP variables to be initialised dynamically using the following 3 | # syntax. 4 | # 5 | # [Capitalized_varname][lp var type suffix] 6 | # 7 | # Where lp var type suffix is either _b for binary, _i for integer, or _f for float. 8 | # I.e 9 | # 10 | # Rating_i is the equivalent of Rating (type integer) 11 | # Is_happy_b is the equivalent of Is_happy (type binary/boolean) 12 | ## 13 | class << Object 14 | alias_method :old_const_missing, :const_missing 15 | def const_missing(value) 16 | method_name = "#{value}".split("::")[-1] rescue "" 17 | if ("A".."Z").cover?(method_name[0]) 18 | if method_name.end_with?(BV.suffix) 19 | return BV.definition(method_name.chomp(BV.suffix).chomp("_")) 20 | elsif method_name.end_with?(IV.suffix) 21 | return IV.definition(method_name.chomp(IV.suffix).chomp("_")) 22 | elsif method_name.end_with?(LV.suffix) 23 | return LV.definition(method_name.chomp(LV.suffix).chomp("_")) 24 | end 25 | end 26 | old_const_missing(value) 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/solvers/cbc.rb: -------------------------------------------------------------------------------- 1 | class Cbc < Solver 2 | def solve 3 | if options[:parallel] 4 | command = "#{executable} #{@filename} %s %s threads 8 branch solution #{@outfile}" 5 | else 6 | command = "#{executable} #{@filename} %s %s branch solution #{@outfile}" 7 | end 8 | command %= [ 9 | options[:gap] ? "ratio #{options[:gap]}":"", 10 | options[:node_limit] ? "maxN #{options[:node_limit]}":"", 11 | options[:time_limit] ? "seconds #{options[:time_limit]}":"" 12 | ] 13 | 14 | exec(command) 15 | end 16 | 17 | def self.executable 18 | :cbc 19 | end 20 | 21 | def store_results(variables) 22 | rows = IO.read(@outfile).split("\n") 23 | objective_str = rows[0].split(/\s+/)[-1] 24 | vars_by_name = {} 25 | rows[1..-1].each do |row| 26 | cols = row.strip.split(/\s+/) 27 | vars_by_name[cols[1].to_s] = cols[2].to_f 28 | end 29 | variables.each do |var| 30 | var.value = vars_by_name[var.to_s].to_f 31 | end 32 | self.unsuccessful = rows[0].downcase.include?('infeasible') 33 | return objective_str.to_f 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/solvers/gurobi.rb: -------------------------------------------------------------------------------- 1 | class Gurobi < Solver 2 | def solve 3 | command = "#{executable} ResultFile=#{@outfile} %s %s %s #{@filename}" 4 | command %= [ 5 | options[:gap] ? "MipGap=#{options[:gap]}":"", 6 | options[:node_limit] ? "NodeLimit=#{options[:node_limit]}":"", 7 | options[:time_limit] ? "TimeLimit=#{options[:time_limit]}":"", 8 | ENV['RULP_GUROBI_CMD_ARGS'] ? ENV['RULP_GUROBI_CMD_ARGS'] : '' 9 | ] 10 | exec(command) 11 | end 12 | 13 | def self.executable 14 | :gurobi_cl 15 | end 16 | 17 | def store_results(variables) 18 | text = IO.read(@outfile) 19 | self.unsuccessful = text.strip.length.zero? || text.downcase.include?('infeasible') 20 | unless self.unsuccessful 21 | rows = text.split("\n") 22 | objective_str = rows[0].split(/\s+/)[-1] 23 | vars_by_name = {} 24 | rows[1..-1].each do |row| 25 | cols = row.strip.split(/\s+/) 26 | vars_by_name[cols[0].to_s] = cols[1].to_f 27 | end 28 | variables.each do |var| 29 | var.value = vars_by_name[var.to_s].to_f 30 | end 31 | end 32 | return objective_str.to_f 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/solvers/solver.rb: -------------------------------------------------------------------------------- 1 | require 'fileutils' 2 | 3 | class Solver 4 | attr_reader :options, :outfile, :filename 5 | attr_accessor :unsuccessful, :model_status 6 | 7 | def initialize(filename, options) 8 | @options = options 9 | @filename = filename 10 | @outfile = get_output_filename 11 | raise StandardError.new("Couldn't find solver #{executable}!") if `which #{executable}`.length == 0 12 | end 13 | 14 | def get_output_filename 15 | "/tmp/rulp-#{Random.rand(0..1000)}.sol" 16 | end 17 | 18 | def store_results(variables) 19 | puts "Not yet implemented" 20 | end 21 | 22 | def executable 23 | self.class.executable 24 | end 25 | 26 | def exec(command) 27 | Rulp.exec(command) 28 | end 29 | 30 | def remove_lp_file 31 | FileUtils.rm(@filename) 32 | end 33 | 34 | def remove_sol_file 35 | FileUtils.rm(@outfile) 36 | end 37 | 38 | def self.exists? 39 | return `which #{self.executable}`.length != 0 40 | end 41 | 42 | def next_pipe 43 | filename = "./tmp/_rulp_pipe" 44 | file_index = 1 45 | file_index += 1 while File.exists?("#{filename}_#{file_index}") 46 | pipe = "#{filename}_#{file_index}" 47 | `mkfifo #{pipe}` 48 | pipe 49 | end 50 | 51 | def with_pipe(pipe) 52 | output = open(pipe, 'w+') 53 | thread = Thread.new{ 54 | yield output 55 | output.flush 56 | } 57 | return thread, output 58 | end 59 | 60 | end 61 | 62 | require_relative 'cbc' 63 | require_relative 'scip' 64 | require_relative 'glpk' 65 | require_relative 'gurobi' 66 | require_relative 'highs' 67 | -------------------------------------------------------------------------------- /lib/rulp/rulp_bounds.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Rulp 4 | module Bounds 5 | 6 | attr_accessor :const 7 | 8 | DIRS = {">" => {value: "gt=", equality: "gte="}, "<" => {value: "lt=", equality: "lte="}} 9 | DIRS_REVERSED = {">" => DIRS["<"], "<" => DIRS[">"]} 10 | 11 | def relative_constraint direction, value, equality=false 12 | direction = coerced? ? DIRS_REVERSED[direction] : DIRS[direction] 13 | self.const = false 14 | self.send(direction[:value], value) 15 | self.send(direction[:equality], equality) 16 | return self 17 | end 18 | 19 | def nocoerce 20 | @@coerced = false 21 | end 22 | 23 | def coerced? 24 | was_coerced = @@coerced rescue nil 25 | @@coerced = false 26 | return was_coerced 27 | end 28 | 29 | def >(val) 30 | relative_constraint(">", val) 31 | end 32 | 33 | def >=(val) 34 | relative_constraint(">", val, true) 35 | end 36 | 37 | def <(val) 38 | relative_constraint("<", val) 39 | end 40 | 41 | def <=(val) 42 | relative_constraint("<", val, true) 43 | end 44 | 45 | def ==(val) 46 | self.const = val 47 | end 48 | 49 | def coerce(something) 50 | @@coerced = true 51 | return self, something 52 | end 53 | 54 | def bounds 55 | return nil if !(self.gt || self.lt || self.const) 56 | return "#{self.name} = #{self.const}" if self.const 57 | 58 | [ 59 | self.gt, 60 | self.gt ? "<=" : nil, 61 | self.to_s, 62 | self.lt ? "<=" : nil, 63 | self.lt 64 | ].compact.join(" ") 65 | end 66 | end 67 | end -------------------------------------------------------------------------------- /examples/whiskas_model2.rb: -------------------------------------------------------------------------------- 1 | # Direct translation of the Whiskas Model 2 example from Pulp to Rulp 2 | # https://github.com/coin-or/pulp/blob/master/examples/WhiskasModel2.py 3 | # 4 | # Usage: SOLVER=Cbc ruby whiskas_model2.rb 5 | # 6 | require_relative "../lib/rulp" 7 | 8 | ingredients = [Chicken_i, Beef_i, Mutton_i, Rice_i, Wheat_i, Gel_i] 9 | costs = {Chicken: 0.013, Beef: 0.008, Mutton: 0.010, Rice: 0.002, Wheat: 0.005, Gel: 0.001} 10 | protein_percent = {Chicken: 0.100, Beef: 0.200, Mutton: 0.150, Rice: 0.000, Wheat: 0.040, Gel: 0.000} 11 | fat_percent = {Chicken: 0.080, Beef: 0.100, Mutton: 0.110, Rice: 0.010, Wheat: 0.010, Gel: 0.000} 12 | fibre_percent = {Chicken: 0.001, Beef: 0.005, Mutton: 0.003, Rice: 0.100, Wheat: 0.150, Gel: 0.000} 13 | salt_percent = {Chicken: 0.002, Beef: 0.005, Mutton: 0.007, Rice: 0.002, Wheat: 0.008, Gel: 0.000} 14 | 15 | objective = ingredients.map{|i| costs[i.name.to_sym] * i}.inject(:+) 16 | problem = Rulp::Min(objective) 17 | problem[ 18 | ingredients.inject(:+) == 100, 19 | ingredients.map{|i| protein_percent[i.name.to_sym] * i}.inject(:+) >= 8.0, 20 | ingredients.map{|i| fat_percent[i.name.to_sym] * i}.inject(:+) >= 6.0, 21 | ingredients.map{|i| fibre_percent[i.name.to_sym] * i}.inject(:+) <= 2.0, 22 | ingredients.map{|i| salt_percent[i.name.to_sym] * i}.inject(:+) <= 0.4, 23 | ] 24 | 25 | result = problem.solve 26 | 27 | puts "Total Cost Per Can: #{result}" 28 | puts 29 | puts "Chicken: #{Chicken_i.value}" 30 | puts "Beef: #{Beef_i.value}" 31 | puts "Mutton: #{Mutton_i.value}" 32 | puts "Rice: #{Rice_i.value}" 33 | puts "Wheat: #{Wheat_i.value}" 34 | puts "Gel: #{Gel_i.value}" 35 | -------------------------------------------------------------------------------- /test/test_basic_suite.rb: -------------------------------------------------------------------------------- 1 | require_relative 'test_helper' 2 | 3 | class BasicSuite < Minitest::Test 4 | 5 | def test_single_binary_var 6 | each_solver do |solver| 7 | assert_equal X_b.value, nil 8 | 9 | # The minimal value for a single binary variable is 0 10 | Rulp::Min(X_b).(solver) 11 | assert_equal X_b.value, false 12 | 13 | # The maximal value for a single binary variable is 1 14 | Rulp::Max(X_b).(solver) 15 | assert_equal X_b.value, true 16 | 17 | # If we set an upper bound this is respected by the solver 18 | Rulp::Max(X_b)[1 * X_b <= 0].(solver) 19 | assert_equal X_b.value, false 20 | 21 | # If we set a lower bound this is respected by the solver 22 | Rulp::Min(X_b)[1 * X_b >= 1].(solver) 23 | assert_equal X_b.value, true 24 | end 25 | end 26 | 27 | def test_single_integer_var 28 | each_solver do |solver| 29 | assert_equal X_i.value, nil 30 | 31 | given[ -35 <= X_i <= 35 ] 32 | 33 | # Integer variables respect integer bounds 34 | Rulp::Min(X_i).(solver) 35 | assert_equal X_i.value, -35 36 | 37 | # Integer variables respect integer bounds 38 | Rulp::Max(X_i).(solver) 39 | assert_equal X_i.value, 35 40 | end 41 | end 42 | 43 | def test_single_general_var 44 | each_solver do |solver| 45 | assert_equal X_f.value, nil 46 | 47 | given[ -345.4321 <= X_f <= 345.4321 ] 48 | 49 | # Integer variables respect integer bounds 50 | Rulp::Min(X_f).(solver) 51 | assert_in_delta X_f.value, -345.4321, 0.001 52 | 53 | # Integer variables respect integer bounds 54 | Rulp::Max(X_f).(solver) 55 | assert_in_delta X_f.value, 345.4321, 0.001 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/solvers/scip.rb: -------------------------------------------------------------------------------- 1 | class Scip < Solver 2 | def self.executable 3 | :scip 4 | end 5 | 6 | def solve 7 | settings = settings_file 8 | if options[:parallel] 9 | exec("touch /tmp/fscip_params") 10 | exec("rm #{@outfile}") 11 | command = "fscip /tmp/fscip_params #{@filename} -fsol #{@outfile} -s #{settings}" 12 | else 13 | exec("rm #{@outfile}") 14 | command = "#{executable} -f #{@filename} -l #{@outfile} -s #{settings}" 15 | end 16 | exec(command) 17 | end 18 | 19 | def settings_file 20 | existing_settings = if File.exist?("./scip.set") 21 | IO.read("./scip.set") 22 | else 23 | "" 24 | end 25 | if options[:node_limit] 26 | existing_settings += "\nlimits/nodes = #{options[:node_limit]}" 27 | end 28 | if options[:gap] 29 | existing_settings += "\nlimits/gap = #{options[:gap]}" 30 | end 31 | if options[:time_limit] 32 | existing_settings += "\nlimits/time = #{options[:time_limit]}" 33 | end 34 | 35 | settings_file = get_settings_filename 36 | IO.write(settings_file, existing_settings) 37 | return settings_file 38 | end 39 | 40 | def get_settings_filename 41 | "/tmp/rulp-#{Random.rand(0..1000)}.set" 42 | end 43 | 44 | def store_results(variables) 45 | results = IO.read(@outfile) 46 | start = results.sub(/.*?primal solution.*?=+/m, "") 47 | stripped = start.sub(/Statistics.+/m, "").strip 48 | rows = stripped.split("\n") 49 | 50 | objective_str = rows[options[:parallel] ? 1 : 0].split(/\s+/)[-1] 51 | 52 | vars_by_name = {} 53 | rows[1..-1].each do |row| 54 | cols = row.strip.split(/\s+/) 55 | vars_by_name[cols[0].to_s] = cols[1].to_f 56 | end 57 | variables.each do |var| 58 | var.value = vars_by_name[var.to_s].to_f 59 | end 60 | self.unsuccessful = !(Float(objective_str) rescue false) 61 | return objective_str.to_f 62 | end 63 | end -------------------------------------------------------------------------------- /lib/solvers/highs.rb: -------------------------------------------------------------------------------- 1 | class Highs < Solver 2 | HIGHS_SUPPORTED_OPTIONS = { 3 | gap: :mip_rel_gap 4 | }.freeze 5 | 6 | def solve 7 | options_file = create_highs_options_file 8 | 9 | command = "#{executable} --model_file #{@filename} --solution_file #{@outfile} %s %s" 10 | command %= [ 11 | options[:time_limit] ? "--time_limit #{options[:time_limit]}" : '', 12 | options_file.length.positive? ? "--options_file #{options_file}" : '' 13 | ] 14 | 15 | exec(command) 16 | 17 | # Remove options file as HiGHS requires additional params in file format instead of command line arguments 18 | FileUtils.rm(options_file) unless options_file.empty? 19 | end 20 | 21 | def self.executable 22 | :highs 23 | end 24 | 25 | def store_results(variables) 26 | rows = IO.read(@outfile).split("\n") 27 | self.model_status = rows[1] 28 | 29 | if model_status.downcase.include?('infeasible') || 30 | model_status.downcase.include?('time limit reached') 31 | self.unsuccessful = true 32 | return 33 | end 34 | 35 | columns_idx = rows.index { |r| r.match?(/# Columns/) } 36 | rows_idx = rows.index { |r| r.match?(/# Rows/) } 37 | 38 | vars_by_name = {} 39 | 40 | rows[(columns_idx + 1)...rows_idx].each do |row| 41 | var_name, var_value = row.strip.split(/\s+/) 42 | vars_by_name[var_name] = var_value 43 | end 44 | 45 | variables.each do |var| 46 | var.value = vars_by_name[var.to_s].to_f 47 | end 48 | 49 | rows.find { |r| r.match?(/Objective/) }.to_s.split(/\s+/).last.to_f 50 | end 51 | 52 | private 53 | 54 | def create_highs_options_file 55 | options_str = '' 56 | 57 | HIGHS_SUPPORTED_OPTIONS.each do |rulp_key, highs_key| 58 | next unless options[rulp_key] 59 | 60 | options_str += "#{highs_key} = #{options[rulp_key]}\n" 61 | end 62 | 63 | return '' if options_str.empty? 64 | 65 | options_file = "/tmp/highs-#{Random.rand(0..1_000_000)}.opt" 66 | IO.write(options_file, options_str) 67 | 68 | options_file 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /lib/rulp/lv.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | ## 4 | # An LP Variable. Used as arguments in LP Expressions. 5 | # The subtypes BV and IV represent Binary and Integer variables. 6 | # These are constructed dynamically by using the special Capitalised variable declaration syntax. 7 | ## 8 | class LV 9 | attr_reader :name, :args 10 | attr_writer :value 11 | attr_accessor :lt, :lte, :gt, :gte 12 | 13 | include Rulp::Bounds 14 | include Rulp::Initializers 15 | 16 | def to_proc 17 | ->(index){ send(self.meth, index) } 18 | end 19 | 20 | def meth 21 | "#{self.name}_#{self.suffix}" 22 | end 23 | 24 | def self.suffix 25 | ENV['RULP_LV_SUFFIX'] || "f" 26 | end 27 | 28 | def suffix 29 | self.class.suffix 30 | end 31 | 32 | def self.definition(name, *args) 33 | identifier = "#{name}#{args.join("_")}" 34 | defined = LV::names_table["#{identifier}"] 35 | case defined 36 | when self then defined 37 | when nil then self.new(name, args) 38 | else raise StandardError.new("ERROR:\n#{name} was already defined as a variable of type #{defined.class}."+ 39 | "You are trying to redefine it as a variable of type #{self}") 40 | end 41 | end 42 | 43 | def *(numeric) 44 | self.nocoerce 45 | Expressions.new([Fragment.new(self, numeric)]) 46 | end 47 | 48 | def -@ 49 | return self * -1 50 | end 51 | 52 | def -(other) 53 | self + (-other) 54 | end 55 | 56 | def +(expressions) 57 | Expressions[self] + Expressions[expressions] 58 | end 59 | 60 | def value 61 | return nil unless @value 62 | case self 63 | when BV then @value.round(2) == 1 64 | when IV then @value 65 | else @value 66 | end 67 | end 68 | 69 | def value? 70 | value ? value : false 71 | end 72 | 73 | def inspect 74 | "#{name}#{args.join("-")}(#{suffix})[#{value.nil? ? 'undefined' : value }]" 75 | end 76 | 77 | alias_method :selected?, :value? 78 | end 79 | 80 | class BV < LV; 81 | def self.suffix 82 | ENV['RULP_BV_SUFFIX'] || "b" 83 | end 84 | end 85 | 86 | class IV < LV; 87 | def self.suffix 88 | ENV['RULP_IV_SUFFIX'] || "i" 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /lib/extensions/kernel_extensions.rb: -------------------------------------------------------------------------------- 1 | ## 2 | # Kernel extension to allow numbered LP variables to be initialised dynamically using the following 3 | # syntax. 4 | # 5 | # [Capitalized_varname][lp var type suffix]( integer ) 6 | # 7 | # This is similar to the syntax defined in the object extensions but allows for numbered 8 | # suffixes to quickly generate ranges of similar variables. 9 | # 10 | # Where lp var type suffix is either _b for binary, _i for integer, or _f for float. 11 | # I.e 12 | # 13 | # Rating_i(5) is the equivalent of Rating_5 (type integer) 14 | # Is_happy_b(2) is the equivalent of Is_happy_2 (type binary/boolean) 15 | # ... 16 | ## 17 | module Kernel 18 | alias_method :old_method_missing, :method_missing 19 | def method_missing(value, *args) 20 | method_name = "#{value}" rescue "" 21 | if ("A".."Z").cover?(method_name[0]) 22 | if method_name.end_with?(BV.suffix) 23 | method_name = method_name.chomp(BV.suffix).chomp("_") 24 | return BV.definition(method_name, args) 25 | elsif method_name.end_with?(IV.suffix) 26 | method_name = method_name.chomp(IV.suffix).chomp("_") 27 | return IV.definition(method_name, args) 28 | elsif method_name.end_with?(LV.suffix) 29 | method_name = method_name.chomp(LV.suffix).chomp("_") 30 | return LV.definition(method_name, args) 31 | end 32 | end 33 | old_method_missing(value, *args) 34 | end 35 | 36 | def _profile 37 | start = Time.now 38 | return_value = yield 39 | return return_value, Time.now - start 40 | end 41 | end 42 | 43 | 44 | module Kernel 45 | ## 46 | # Adds assertion capabilities to ruby. 47 | # The assert function will raise an error if the inner block returns false. 48 | # The error will contain the file, line number and source line of the failing assertion. 49 | ## 50 | class AssertionException < Exception; end 51 | 52 | ## 53 | # Ensure the SCRIPT_LINES global variable exists so that we can access the source of the failed assertion 54 | ## 55 | ::SCRIPT_LINES__ = {} unless defined? ::SCRIPT_LINES__ 56 | 57 | ## 58 | # If assertion returns false we return a new assertion exception with the failing file and line, 59 | # and attempt to return the failed source if accessible. 60 | ## 61 | def assert(truthy=false) 62 | unless truthy || (block_given? && yield) 63 | file, line = caller[0].split(":") 64 | error_message = "Assertion Failed! < #{file}:#{line}:#{ SCRIPT_LINES__[file][line.to_i - 1][0..-2] rescue ''} >" 65 | raise AssertionException.new(error_message) 66 | end 67 | end 68 | end 69 | 70 | 71 | def given 72 | @dummy ||= begin 73 | dummy = {} 74 | class << dummy 75 | def [](*args) 76 | end 77 | end 78 | dummy 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /lib/rulp/expression.rb: -------------------------------------------------------------------------------- 1 | ## 2 | # An LP Expression. A mathematical expression. 3 | # Can be a constraint or can be the objective function of a LP or MIP problem. 4 | ## 5 | class Expressions 6 | attr_accessor :expressions 7 | def initialize(expressions) 8 | @expressions = expressions 9 | end 10 | 11 | def to_s 12 | as_str = @expressions[0].to_s 13 | as_str = as_str[1] == '+' ? as_str[3..-1] : as_str.dup 14 | (@expressions.length - 1).times do |i| 15 | as_str << @expressions[i + 1].to_s 16 | end 17 | as_str 18 | end 19 | 20 | def variables 21 | @expressions.map(&:variable) 22 | end 23 | 24 | %i[== < <= > >=].each do |constraint_type| 25 | define_method(constraint_type) do |value| 26 | Constraint.new(self, constraint_type, value) 27 | end 28 | end 29 | 30 | def -@ 31 | self.class.new(expressions.map(&:-@)) 32 | end 33 | 34 | def -(other) 35 | other = -other 36 | self + other 37 | end 38 | 39 | def +(other) 40 | Expressions.new(expressions + Expressions[other].expressions) 41 | end 42 | 43 | def self.[](value) 44 | case value 45 | when LV then Expressions.new([Fragment.new(value, 1)]) 46 | when Fragment then Expressions.new([value]) 47 | when Expressions then value 48 | end 49 | end 50 | 51 | def evaluate 52 | expressions.map(&:evaluate).inject(:+) 53 | end 54 | end 55 | 56 | ## 57 | # An expression fragment. An expression can consist of many fragments. 58 | ## 59 | class Fragment 60 | attr_accessor :lv, :operand 61 | 62 | def initialize(lv, operand) 63 | @lv = lv 64 | @operand = operand 65 | end 66 | 67 | def +(other) 68 | Expressions.new([self] + Expressions[other].expressions) 69 | end 70 | 71 | def -(other) 72 | self.+(-other) 73 | end 74 | 75 | def *(other) 76 | Fragment.new(@lv, @operand * other) 77 | end 78 | 79 | def evaluate 80 | if [TrueClass, FalseClass].include? @lv.value.class 81 | @operand * (@lv.value ? 1 : 0) 82 | else 83 | @operand * @lv.value 84 | end 85 | end 86 | 87 | def -@ 88 | @operand = -@operand 89 | self 90 | end 91 | 92 | def variable 93 | @lv 94 | end 95 | 96 | %i[== < <= > >=].each do |constraint_type| 97 | define_method(constraint_type) do |value| 98 | Constraint.new(Expressions.new(self), constraint_type, value) 99 | end 100 | end 101 | 102 | def to_s 103 | @as_str ||= case @operand 104 | when -1 then " - #{@lv}" 105 | when 1 then " + #{@lv}" 106 | when ->(op) { op < 0 } then " - #{@operand.abs} #{@lv}" 107 | else " + #{@operand} #{@lv}" 108 | end 109 | end 110 | end 111 | -------------------------------------------------------------------------------- /lib/rulp/rulp.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "rulp_bounds" 4 | require_relative "rulp_initializers" 5 | require_relative "lv" 6 | require_relative "constraint" 7 | require_relative "expression" 8 | 9 | require_relative "../solvers/solver" 10 | require_relative "../extensions/extensions" 11 | require_relative "../helpers/log" 12 | 13 | require 'set' 14 | require 'open3' 15 | require 'logger' 16 | 17 | GLPK = "glpsol" 18 | SCIP = "scip" 19 | CBC = "cbc" 20 | GUROBI = "gurobi_cl" 21 | HIGHS = "highs" 22 | 23 | module Rulp 24 | attr_accessor :expressions 25 | extend Rulp::Log 26 | self.print_solver_outputs = true 27 | self.log_level = Logger::DEBUG 28 | MIN = "Minimize" 29 | MAX = "Maximize" 30 | 31 | GLPK = ::GLPK 32 | GUROBI = ::GUROBI 33 | SCIP = ::SCIP 34 | CBC = ::CBC 35 | HIGHS = ::HIGHS 36 | 37 | SOLVERS = { 38 | GLPK => Glpk, 39 | SCIP => Scip, 40 | CBC => Cbc, 41 | GUROBI => Gurobi, 42 | HIGHS => Highs 43 | } 44 | 45 | 46 | def self.Glpk(lp, opts={}) 47 | lp.solve_with(GLPK, opts) 48 | end 49 | 50 | def self.Cbc(lp, opts={}) 51 | lp.solve_with(CBC, opts) 52 | end 53 | 54 | def self.Scip(lp, opts={}) 55 | lp.solve_with(SCIP, opts) 56 | end 57 | 58 | def self.Gurobi(lp, opts={}) 59 | lp.solve_with(GUROBI, opts) 60 | end 61 | 62 | def self.Highs(lp, opts={}) 63 | lp.solve_with(HIGHS, opts) 64 | end 65 | 66 | def self.Max(objective_expression) 67 | Rulp.log(Logger::INFO, "Creating maximization problem") 68 | Problem.new(Rulp::MAX, objective_expression) 69 | end 70 | 71 | def self.Min(objective_expression) 72 | Rulp.log(Logger::INFO, "Creating minimization problem") 73 | Problem.new(Rulp::MIN, objective_expression) 74 | end 75 | 76 | def self.solver_exists?(solver_name) 77 | solver = solver_name[0].upcase + solver_name[1..-1].downcase 78 | solver_class = ObjectSpace.const_defined?(solver) && ObjectSpace.const_get(solver) 79 | solver_class.exists? if(solver_class) 80 | end 81 | 82 | def self.exec(command) 83 | Open3.popen2e("#{command} #{Rulp.print_solver_outputs ? "" : "> /dev/null 2>&1"}") do | inp, out, thr| 84 | out.each.map do |line| 85 | Rulp.log(Logger::DEBUG, line) 86 | line 87 | end 88 | end.join 89 | end 90 | 91 | class Problem 92 | 93 | attr_accessor :result, :trace, :lp_file 94 | 95 | def initialize(objective, objective_expression) 96 | @variables = Set.new 97 | @objective = objective 98 | @lp_file = nil 99 | @constraints = [] 100 | self.objective = objective_expression 101 | end 102 | 103 | def objective=(objective_expression) 104 | @objective_expression = objective_expression.kind_of?(LV) ? 1 * objective_expression : objective_expression 105 | @variables.merge(@objective_expression.variables) 106 | end 107 | 108 | def [](*constraints) 109 | Rulp.log(Logger::INFO, "Got constraints") 110 | constraints = constraints.flatten.select{|x| x.kind_of?(Constraint)} 111 | Rulp.log(Logger::INFO, "Flattened constraints") 112 | @constraints.concat(constraints) 113 | Rulp.log(Logger::INFO, "Joint constraints") 114 | @variables.merge(constraints.flat_map(&:variables).uniq) 115 | Rulp.log(Logger::INFO, "Extracted variables") 116 | self 117 | end 118 | 119 | def solve(opts={}) 120 | Rulp.send(self.solver, self, opts) 121 | end 122 | 123 | def method_missing(method_name, *args) 124 | self.call(method_name, *args) 125 | end 126 | 127 | def solver(solver=nil) 128 | solver = solver || ENV["SOLVER"] || "Scip" 129 | solver = solver[0].upcase + solver[1..-1].downcase 130 | end 131 | 132 | def call(using=nil, options={}) 133 | Rulp.send(self.solver(using), self, options) 134 | end 135 | 136 | def constraints 137 | return "0 #{@variables.first} = 0" if @constraints.length == 0 138 | @constraints.each.with_index.map{|constraint, i| 139 | " c#{i}: #{constraint}\n" 140 | }.join 141 | end 142 | 143 | def integers 144 | ints = @variables.select{|x| x.kind_of?(IV) }.join(" ") 145 | return "\nGeneral\n #{ints}" if(ints.length > 0) 146 | end 147 | 148 | def bits 149 | bits = @variables.select{|x| x.kind_of?(BV) }.join(" ") 150 | return "\nBinary\n #{bits}" if(bits.length > 0) 151 | end 152 | 153 | def bounds 154 | @variables.map{|var| 155 | next unless var.bounds 156 | " #{var.bounds}" 157 | }.compact.join("\n") 158 | end 159 | 160 | def get_output_filename 161 | "/tmp/rulp-#{Random.rand(0..1000)}.lp" 162 | end 163 | 164 | def output(filename=choose_file) 165 | IO.write(filename, self) 166 | end 167 | 168 | def solve_with(type, options={}) 169 | 170 | filename = get_output_filename 171 | solver = SOLVERS[type].new(filename, options) 172 | 173 | Rulp.log(Logger::INFO, "Writing problem") 174 | IO.write(filename, self) 175 | 176 | Rulp.exec("open #{filename}") if options[:open_definition] 177 | 178 | Rulp.log(Logger::INFO, "Solving problem") 179 | self.trace, time = _profile{ solver.solve } 180 | 181 | Rulp.exec("open #{solver.outfile}") if options[:open_solution] 182 | 183 | Rulp.log(Logger::DEBUG, "Solver took #{time}") 184 | 185 | Rulp.log(Logger::INFO, "Parsing result") 186 | 187 | unless solver.outfile 188 | raise "No output file detected. Solver failed" 189 | return 190 | end 191 | 192 | solver.store_results(@variables) 193 | 194 | if solver.unsuccessful 195 | raise "Solve failed: #{solver.model_status}" if solver.model_status 196 | 197 | outfile_contents = IO.read(solver.outfile) 198 | raise "Solve failed: solution infeasible" if outfile_contents.downcase.include?("infeasible") || outfile_contents.strip.length.zero? 199 | raise "Solve failed: all units undefined" 200 | end 201 | 202 | if options[:remove_lp_file] 203 | solver.remove_lp_file 204 | else 205 | self.lp_file = solver.filename 206 | end 207 | solver.remove_sol_file if options[:remove_sol_file] 208 | 209 | self.result = @objective_expression.evaluate 210 | 211 | Rulp.log(Logger::DEBUG, "Objective: #{result}\n#{@variables.map{|v|[v.name, "=", v.value].join(' ') if v.value}.compact.join("\n")}") 212 | return self.result 213 | end 214 | 215 | def inspect 216 | to_s 217 | end 218 | 219 | def write(output) 220 | output.puts "#{@objective}" 221 | output.puts " obj: #{@objective_expression}" 222 | output.puts "Subject to" 223 | output.puts "#{constraints}" 224 | output.puts "Bounds" 225 | output.puts "#{bounds}#{integers}#{bits}" 226 | output.puts "End" 227 | end 228 | 229 | def to_s 230 | %Q( 231 | #{' '*0}#{@objective} 232 | #{' '*0} obj: #{@objective_expression} 233 | #{' '*0}Subject to 234 | #{' '*0}#{constraints} 235 | #{' '*0}Bounds 236 | #{' '*0}#{bounds}#{integers}#{bits} 237 | #{' '*0}End 238 | ) 239 | end 240 | 241 | alias_method :save, :output 242 | end 243 | end 244 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Rulp](https://img.shields.io/badge/Rulp-Ruby%20Linear%20Programming-blue.svg)](Rulp) 2 | [![Gem Version](https://badge.fury.io/rb/rulp.svg)](http://badge.fury.io/rb/rulp) 3 | [![Downloads](https://img.shields.io/gem/dt/rulp/stable.svg)](https://img.shields.io/gem/dt/rulp) 4 | [![Inline docs](http://inch-ci.org/github/wouterken/rulp.svg?branch=master)](http://inch-ci.org/github/wouterken/rulp) 5 | [![Codeship Status for wouterken/rulp](https://codeship.com/projects/f97c2f00-a4d2-0132-7283-026d769eacf9/status?branch=master)](https://codeship.com/projects/66508) 6 | 7 | 8 | 9 | 10 | **Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* 11 | 12 | - [Rulp](#rulp) 13 | - [Sample Code](#sample-code) 14 | - [Installation](#installation) 15 | - [Scip:](#scip) 16 | - [UG (Scip parallel)](#ug-scip-parallel) 17 | - [Coin Cbc:](#coin-cbc) 18 | - [GLPK:](#glpk) 19 | - [HiGHS:](#highs) 20 | - [Usage](#usage) 21 | - [Variables](#variables) 22 | - [Variable Constraints](#variable-constraints) 23 | - [Problem constraints](#problem-constraints) 24 | - [Solving or saving 'lp' files](#solving-or-saving-lp-files) 25 | - [Additional Options](#additional-options) 26 | - [Mip tolerance.](#mip-tolerance) 27 | - [Node Limit.](#node-limit) 28 | - [Saving LP files.](#saving-lp-files) 29 | - [Examples.](#examples) 30 | - [Rulp Executable](#rulp-executable) 31 | - [A larger example](#a-larger-example) 32 | - [MIP Minimization example](#mip-minimization-example) 33 | 34 | 35 | 36 | 37 | # Rulp 38 | Rulp is an easy to use Ruby DSL for generating linear programming and mixed integer programming problem descriptions in the LP file format. 39 | 40 | The *[.lp]* file format can be read and executed by most LP solvers including coin-Cbc, Scip, GLPK, CPLEX and Gurobi. 41 | 42 | Rulp will execute and parse the results generated by **Cbc**, **Scip**, **fscip(ug)** and **GLPK**. 43 | 44 | Rulp is inspired by the ruby wrapper for the GLPK toolkit and the python LP library "Pulp". 45 | 46 | ## Sample Code 47 | 48 | ```ruby 49 | # maximize 50 | # z = 10 * x + 6 * y + 4 * z 51 | # 52 | # subject to 53 | # p: x + y + z <= 100 54 | # q: 10 * x + 4 * y + 5 * z <= 600 55 | # r: 2 * x + 2 * y + 6 * z <= 300 56 | # 57 | # where all variables are non-negative integers 58 | # x >= 0, y >= 0, z >= 0 59 | # 60 | 61 | 62 | given[ 63 | 64 | X_i >= 0, 65 | Y_i >= 0, 66 | Z_i >= 0 67 | 68 | ] 69 | 70 | result = Rulp::Max( 10 * X_i + 6 * Y_i + 4 * Z_i ) [ 71 | X_i + Y_i + Z_i <= 100, 72 | 10 * X_i + 4 * Y_i + 5 * Z_i <= 600, 73 | 2 * X_i + 2 * Y_i + 6 * Z_i <= 300 74 | ].solve 75 | 76 | ## 77 | # 'result' is the result of the objective function. 78 | # You can retrieve the values of variables by using the 'value' method 79 | # E.g 80 | # X_i.value == 32 81 | # Y_i.value == 67 82 | # Z_i.value == 0 83 | ## 84 | ``` 85 | 86 | 87 | 88 | ## Installation 89 | To use Rulp as a complete solver toolkit you will need one of either Glpsol(GLPK), Scip, Coin-Cbc or HiGHS installed. Here are some sample instructions of how you install each of these solvers. 90 | Be sure to read the license terms for each of these solvers before using them. 91 | 92 | ### Scip: 93 | 94 | Go to install directory 95 | 96 | cd /usr/local 97 | 98 | Download Scip source (Be sure to visit the SCIP website and check the license terms first.) 99 | 100 | curl -O http://scip.zib.de/download/release/scipoptsuite-3.1.1.tgz 101 | 102 | Extract Scip source 103 | 104 | gunzip -c scipoptsuite-3.1.1.tgz | tar xvf - 105 | 106 | Scip relies on a number of libraries including ZLIB, GMP and READLINE. 107 | Please read the INSTALL directory in the application directory for help 108 | on getting these installed. 109 | 110 | Build Scip 111 | 112 | cd scipoptsuite-3.1.1 113 | make 114 | 115 | Add scip bin directory to your path 116 | E.g 117 | 118 | echo '\nexport PATH="$PATH:/usr/local/scipoptsuite-3.1.1/scip-3.1.1/bin"' >> ~/.bash_profile 119 | 120 | Or 121 | 122 | echo '\nexport PATH="$PATH:/usr/local/scipoptsuite-3.1.1/scip-3.1.1/bin"' >> ~/.zshrc 123 | 124 | You should now have scip installed. 125 | 126 | ### UG (Scip parallel) 127 | 128 | To run scip using multiple cores you will need to use the UG library for scip. 129 | This is bundled in the scipoptsuite directory. 130 | To install this simply run 131 | 132 | make ug 133 | 134 | From the base directory and then add the fscip binary to your path. 135 | E.g 136 | 137 | echo '\nexport PATH="$PATH:/usr/local/scipoptsuite-3.1.1/ug-0.7.5/bin"' >> ~/.bash_profile 138 | 139 | Or 140 | 141 | echo '\nexport PATH="$PATH:/usr/local/scipoptsuite-3.1.1/ug-0.7.5/bin"' >> ~/.zshrc 142 | 143 | ### Coin Cbc: 144 | 145 | Navigate to install location 146 | 147 | cd /usr/local 148 | 149 | Follow CBC installation instructions 150 | 151 | svn co https://projects.coin-or.org/svn/Cbc/stable/2.8 coin-Cbc 152 | cd coin-Cbc 153 | ./configure -C #Optionally add --enable-cbc-parallel here to enable multiple threads for cbc 154 | make 155 | make install 156 | 157 | Add Coin-cbc to path 158 | 159 | E.g 160 | 161 | echo '\nexport PATH="$PATH:/usr/local/coin-Cbc/bin"' >> ~/.bash_profile 162 | 163 | Or 164 | 165 | echo '\nexport PATH="$PATH:/usr/local/coin-Cbc/bin"' >> ~/.zshrc 166 | 167 | You should now have coin-Cbc installed. 168 | 169 | ### GLPK: 170 | Download the latest version of GLPK from http://www.gnu.org/software/glpk/#downloading 171 | 172 | From the download directory 173 | 174 | tar -xzf glpk-4.55.tar.gz 175 | cd glpk-4.55 176 | ./configure --prefix=/usr/local 177 | make 178 | sudo make install 179 | 180 | At this point, you should have GLPK installed. Verify it: 181 | 182 | which glpsol 183 | => /usr/local/bin/glpsol 184 | 185 | ### HiGHS: 186 | Install HiGHS following the installation instructions at https://ergo-code.github.io/HiGHS/dev/interfaces/cpp/ 187 | 188 | ## Usage 189 | 190 | #### Variables 191 | 192 | ```ruby 193 | # Rulp variables are initialized as soon as they are needed so there is no 194 | # need to initialize them. 195 | # They follow a naming convention that defines their type. 196 | # A variable is declared as a constant with one of three different suffixes. 197 | # 'f' or '_f' indicates a general variable (No constraints) 198 | # 'i' or '_i' indicates a integer variable 199 | # 'b' or '_b' indicates a binary/boolean variable 200 | 201 | 202 | An_Integer_i 203 | => # 204 | 205 | Generalf 206 | => # 207 | 208 | Bool_Val_b 209 | => # 210 | 211 | # In some cases it is implausible to generate a unique name for every possible variable 212 | # as an LP problem description may contain many hundreds of variables. 213 | # To handle these scenarios variable definitions can 214 | # accept index parameters to create large ranges of unique variables. 215 | # Examples of how indexed variables can be declared are as follows: 216 | 217 | Item_i(4,5) 218 | # 219 | 220 | Item_i("store_3", "table_2") 221 | # 222 | 223 | [*0..10].map(&Unit_f) 224 | => [#, 225 | #, 226 | #, 227 | #, 228 | #, 229 | #, 230 | #, 231 | #, 232 | #, 233 | #, 234 | #] 235 | ``` 236 | 237 | #### Variable Constraints 238 | 239 | Add variable constraints to a variable using the <,>,<=,>=,== operators. 240 | Be careful to use '==' and not '=' when expressing equality. 241 | Constraints on a variable can only use numeric literals and not other variables. 242 | Inter-variable constraints should be expressed as problem constrants. (Explained below.) 243 | 244 | ```ruby 245 | X_i < 5 246 | X_i.bounds 247 | => "X <= 5" 248 | 249 | 3 <= X_i < 15 250 | X_i.bounds 251 | => "3 <= X <= 15" 252 | 253 | Y_f == 10 254 | Y_f.bounds 255 | => "y = 10" 256 | ``` 257 | 258 | #### Problem constraints 259 | 260 | Constraints are added to a problem using the :[] syntax. 261 | 262 | ```ruby 263 | problem = Rulp::Max( 10 * X_i + 6 * Y_i + 4 * Z_i ) 264 | 265 | problem[ 266 | X_i + Y_i + Z_i <= 100 267 | ] 268 | 269 | problem[ 270 | 10 * X_i + 4 * Y_i + 5 * Z_i <= 600 271 | ] 272 | ... 273 | problem.solve 274 | ``` 275 | 276 | You can add multiple constraints at once by comma separating them as seen in the earlier examples: 277 | 278 | ```ruby 279 | Rulp::Max( 10 * X_i + 6 * Y_i + 4 * Z_i ) [ 280 | X_i + Y_i + Z_i <= 100, 281 | 10 * X_i + 4 * Y_i + 5 * Z_i <= 600, 282 | 2 * X_i + 2 * Y_i + 6 * Z_i <= 300 283 | ] 284 | ``` 285 | 286 | #### Solving or saving 'lp' files 287 | 288 | There are multiple ways to solve the problem or output the problem to an 'lp' file. 289 | Currently the Rulp library supports calling installed executables for Scip, Cbc and Glpk. 290 | For each of these solvers it requires the solver command line binaries to be installed 291 | such that the command `which [exec_name]` returns a path. (I.e they must be on your PATH. See [Installation](#installation)). 292 | 293 | Given a problem there are multiple ways to initiate a solver. 294 | 295 | ```ruby 296 | @problem = Rulp::Max( 10 * X_i + 6 * Y_i + 4 * Z_i ) [ 297 | X_i + Y_i + Z_i <= 100, 298 | 10 * X_i + 4 * Y_i + 5 * Z_i <= 600, 299 | 2 * X_i + 2 * Y_i + 6 * Z_i <= 300 300 | ] 301 | ``` 302 | 303 | Default solver: 304 | 305 | ```ruby 306 | @problem.solve 307 | # this will use the solver specified in the environment variable 'SOLVER' by default. 308 | # This can be 'scip', 'cbc', 'glpk' or 'gurobi'. 309 | # You can also try and run scip or cbc in parallel using by passing an options argument containing { parallel: true } 310 | # If no variable is given it uses 'scip' as a default. 311 | ``` 312 | 313 | If you had a linear equation in a file named 'problem.rb' from the command line you could specify an alternate solver by executing: 314 | 315 | ```ruby 316 | SOLVER=cbc ruby problem.rb 317 | ``` 318 | 319 | Explicit solver: 320 | 321 | ```ruby 322 | @problem.scip 323 | # Or 324 | @problem.cbc 325 | # Or 326 | @problem.glpk 327 | # Or 328 | @problem.cbc(parallel: true) 329 | # Or 330 | @problem.scip(parallel: true) 331 | ``` 332 | 333 | Or 334 | 335 | ```ruby 336 | Rulp::Scip(@problem) 337 | Rulp::Glpk(@problem) 338 | Rulp::Cbc(@problem) 339 | Rulp::scip(@problem, parallel: true) 340 | Rulp::cbc(@problem, parallel: true) 341 | ``` 342 | 343 | For debugging purposes you may wish to see the input and output files generated and consumed by Rulp. 344 | To do this you can pass ```open_definition``` and ```open_solution``` as options with the following extended syntax: 345 | 346 | ```ruby 347 | Rulp::Cbc(@problem, open_definition: true, open_solution: true) 348 | ``` 349 | 350 | The optional booleans will optionally call the 'open' utility to open the problem definition or the solution. (This utility is installed by default on a mac and will not work if the utility is not on your PATH) 351 | 352 | 353 | #### Additional Options 354 | 355 | ##### Mip tolerance. 356 | You can provide a MIP tolerance to any of the solvers to allow it 357 | to return a sub-optimal result within this tolerance from the dual bound. 358 | 359 | ```ruby 360 | # Various ways to invoke this option 361 | @problem.cbc(gap: 0.05) 362 | Rulp::Cbc(@problem, gap: 0.05) 363 | @problem.solve(gap: 0.05) 364 | ``` 365 | 366 | ##### Node Limit. 367 | You can limit the number of nodes the solver is able to explore before it must return 368 | it's current most optimal solution. If it has not discovered a solution by the time it hits 369 | the node limit it will return nil. Glpk does not accept a node limit option. 370 | 371 | ```ruby 372 | # Various ways to invoke this option 373 | @problem.cbc(node_limit: 10_000) 374 | Rulp::Cbc(@problem, node_limit: 10_000) 375 | @problem.solve(node_limit: 10_000) 376 | ``` 377 | 378 | For Scip you can also specify these limits in a settings file called scip.set in the current 379 | directory. Options passed to Rulp will overwrite these values. 380 | 381 | ``` 382 | # E.g inside ./scip.set 383 | limits/gap = 0.15 # optimality within 0.1%. 384 | limits/nodes = 200000 # No more than 200000 nodes explored 385 | ``` 386 | 387 | #### Saving LP files. 388 | 389 | You may not wish to use one of the RULP compatible solvers but instead another solver that is able to read .lp files. (E.g CPLEX) but still want to use Rulp to generate your LP file. In this case you should use Rulp to output your lp problem description to a file of your choice. To do this simply use the following call 390 | 391 | ```ruby 392 | @problem.save("/Users/johndoe/Desktop/myproblem.lp") 393 | ``` 394 | 395 | OR 396 | 397 | ```ruby 398 | @problem.output("/Users/johndoe/Desktop/myproblem.lp") 399 | ``` 400 | 401 | You should also be able to call 402 | 403 | ```ruby 404 | @problem.save 405 | ``` 406 | Without parameters to be prompted for a save location. 407 | 408 | ### Examples. 409 | Take a look at some basic examples in the ./examples directory in the source code. 410 | 411 | ### Rulp Executable 412 | Rulp comes bundled with a 'rulp' executable which by default loads the rulp environment and either the Pry or Irb REPL. 413 | (Preference for Pry). Once installed you should be able to simply execute 'rulp' to launch this rulp enabled REPL. 414 | You can then play with and attempt LP and MIP problems straight from the command line. 415 | 416 | ```ruby 417 | 418 | [1] pry(main)> 13 <= X_i <= 45 # Declare integer variable 419 | => X(i)[undefined] 420 | 421 | [2] pry(main)> -15 <= Y_f <= 15 # Declare float variable 422 | => Y(f)[undefined] 423 | 424 | [3] pry(main)> @problem = Rulp::Min(X_i - Y_f) # Create min problem 425 | [info] Creating minimization problem 426 | => 427 | Minimize 428 | obj: X -Y 429 | Subject to 430 | 0 X = 0 431 | Bounds 432 | 13 <= X <= 45 433 | -15 <= Y <= 15 434 | General 435 | X 436 | End 437 | 438 | [4] @problem[ X_i - 2 * Y_f < 40] #Adding a problem constraint 439 | => 440 | Minimize 441 | obj: X -Y 442 | Subject to 443 | c0: X -2 Y <= 40 444 | Bounds 445 | 13 <= X <= 45 446 | -15 <= Y <= 15 447 | General 448 | X 449 | End 450 | 451 | 452 | [5] pry(main)> @problem.solve # Solve 453 | ... 454 | [info] Solver took 0.12337 455 | [info] Parsing result 456 | => -2.0 #(Minimal result) 457 | 458 | [6] pry(main)> Y_f # See value calculated for Y now that solver has run 459 | => Y(f)[15.0] 460 | 461 | [8] pry(main)> X_i 462 | => X(i)[13.0] 463 | 464 | # The result of the objective function (-2.0) was returned by the call to .solve 465 | # Now the solver has run and calculated values for our variables we can also test the 466 | # objective function (or any function) by calling evaluate on it. 467 | # E.g 468 | 469 | [9] (X_i - Y_f).evaluate 470 | => -2.0 471 | 472 | [10] pry(main)> (2 * X_i + 15 * Y_f).evaluate 473 | => 251.0 474 | ``` 475 | 476 | ### A larger example 477 | Here is a basic example of how Rulp can help you model problems with a large number of variables. 478 | Suppose we are playing an IOS app which contains in-app purchases. Each of these in-app purchases costs 479 | a variable amount and gives us a certain number of in-game points. Suppose our mother gave us $55 to spend. 480 | We want to find the maximal number of in-game points we can buy using this money. Here is a simple example 481 | of how we could use Rulp to formulate this problem. 482 | 483 | We decide to model each of these possible purchases as a binary variable (as we either purchase them or 484 | we don't. We can't partially purchase one.) 485 | 486 | ```ruby 487 | # Generate the data randomly for this example. 488 | costs, points = [*0..1000].map do |i| 489 | [Purchase_b(i) * Random.rand(1.0..3.0), Purchase_b(i) * Random.rand(5.0..10.0)] 490 | end.transpose.map(&:sum) #We sum the array of points and array of costs to create a Rulp expression 491 | 492 | # And this is where the magic happens!. We ask rulp to maximise the number of points given 493 | # the constraint that costs must be less than $55 494 | 495 | Rulp::Max(points)[ 496 | costs < 55 497 | ].solve 498 | => 538.2125623353652 (# You will get a different value as data was generated randomly) 499 | 500 | # Now how do we check which purchases were selected? 501 | selected_purchases = [*0..1000].map(&Purchase_b).select(&:selected?) 502 | 503 | => [Purchase27(b)[true], 504 | Purchase86(b)[true], 505 | Purchase120(b)[true], 506 | Purchase141(b)[true], 507 | Purchase154(b)[true], 508 | ... 509 | ``` 510 | 511 | ### MIP Minimization Example 512 | 513 | Here is another example of using array style variables. Let's use the example of making a cup of coffee. Personally, I like my coffee with 2 creams and 1 sugar. There are many types of products that can add cream, or sugar to taste. Each has a different associated cost. As a price sensitive consumer what combination of products will allow me to flavor my coffee as desired with the lowest total cost? 514 | 515 | 516 | ```ruby 517 | desired = [ 518 | { ingredient: :cream, amount: 2 }, 519 | { ingredient: :sugar, amount: 1 }, 520 | ] 521 | 522 | products = [ 523 | { name: 'Milk', cost: 0.50, supplies: { cream: 1 } }, 524 | { name: 'Baileys_Irish_Cream', cost: 2.99, supplies: { cream: 1, sugar: 1 } }, 525 | { name: 'Non_Dairy_Creamer', cost: 0.10, supplies: { cream: 1 } }, 526 | { name: 'Sugar', cost: 0.10, supplies: { sugar: 1 } }, 527 | ] 528 | 529 | variables = products.map do |product| 530 | Products_i(product[:name]) 531 | end 532 | # => [ ProductMilk_i, ProductBaileys_Irish_Cream_i, ProductNon_Dairy_Creamer_i, ProductSugar_i ] 533 | 534 | given[ 535 | variables.map { |i| i >= 0 } 536 | ] 537 | # given [ 538 | # ProductMilk_i >= 0, 539 | # ProductBaileys_Irish_Cream_i >= 0, 540 | # ProductNon_Dairy_Creamer_i >= 0, 541 | # ProductSugar_i >= 0, 542 | # ] 543 | 544 | constraints = desired.map do |minimum| 545 | ingredient = minimum[:ingredient] 546 | desired_amount = minimum[:amount] 547 | 548 | products.map.with_index do |p, i| 549 | coefficient = p[:supplies][ingredient] || 0 550 | coefficient * variables[i] 551 | end.inject(:+) >= desired_amount 552 | end 553 | # => [ 554 | # 1 * ProductMilk_i + 1 * ProductBaileys_Irish_Cream_i + 1 * ProductNon_Dairy_Creamer_i + 0 * ProductSugar_i >= 2 555 | # 0 * ProductMilk_i + 1 * ProductBaileys_Irish_Cream_i + 0 * ProductNon_Dairy_Creamer_i + 1 * ProductSugar_i >= 1 556 | # ] 557 | 558 | objective = products.map.with_index { |p, i| p[:cost] * variables[i] }.inject(:+) 559 | # => 0.50 * ProductMilk_i + 2.99 * ProductBaileys_Irish_Cream_i + 0.10 * ProductNon_Dairy_Creamer_i + 0.10 * ProductSugar_i 560 | 561 | problem = Rulp::Min(objective) 562 | problem[constraints] 563 | 564 | Rulp::Glpk(problem) 565 | # DEBUG -- : Objective: 0.30000000000000004 566 | # Products = 0.0 567 | # Products = 0.0 568 | # Products = 2.0 569 | # Products = 1.0 570 | 571 | variables.map(&:value) 572 | # => [ 0, 0, 2, 1 ] 573 | ``` 574 | --------------------------------------------------------------------------------