rulp 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +15 -0
- data/bin/rulp +9 -0
- data/lib/extensions/array_extensions.rb +5 -0
- data/lib/extensions/extensions.rb +3 -0
- data/lib/extensions/kernel_extensions.rb +82 -0
- data/lib/extensions/object_extensions.rb +26 -0
- data/lib/helpers/log.rb +57 -0
- data/lib/rulp.rb +2 -0
- data/lib/rulp/constraint.rb +17 -0
- data/lib/rulp/expression.rb +112 -0
- data/lib/rulp/lv.rb +74 -0
- data/lib/rulp/rulp.rb +134 -0
- data/lib/rulp/rulp_bounds.rb +65 -0
- data/lib/rulp/rulp_initializers.rb +24 -0
- data/lib/solvers/cbc.rb +24 -0
- data/lib/solvers/glpk.rb +19 -0
- data/lib/solvers/scip.rb +30 -0
- data/lib/solvers/solvers.rb +20 -0
- data/test/test_boolean.rb +44 -0
- data/test/test_helper.rb +6 -0
- data/test/test_simple.rb +50 -0
- metadata +67 -0
checksums.yaml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
MjEyMDI0ZDM1NzVhNWE3NmJkYTk5Yzg3ODQyMjRiMmYxOWFkMDdkZQ==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
YzE0NmE1YzFhNDg1YTI4YjUzNTc3YmY2NzA4NTI3MDE1NGE2MzI2NA==
|
7
|
+
SHA512:
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
M2ZhODczODZkYmU4ZGQ0N2RmZGM4MzZiN2ZmYjc3NjhkZjAyOTE0MjE0MWNj
|
10
|
+
ZTI1MWJkYzA5YWI0NmQyMjE1NmJjNTZhMGUxYjIyNDA4NzhjZDQzZTBkNzNi
|
11
|
+
ZjE3ZjQwZjEyNDI4YWNmYTkwMDk5NzZkYjliYTIxMTczMGJlODY=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
ODhjYmYwOGY1N2E0MDc2OTlmYjBjZTRjN2VmMDk5NjVkOGIyMDM3NDA4ZTU2
|
14
|
+
YWE0MWU3ODIwY2VhZmI4NWU0OTcwMDg0ZmJiMzIzNTk2OGIwZDQ2ZTUwMDFh
|
15
|
+
ZTIwM2EwZDVmYWMyZjdkYzgwZDZiYWQyYmJmN2Y5ZjYzMjdmNWQ=
|
data/bin/rulp
ADDED
@@ -0,0 +1,82 @@
|
|
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
|
+
start = method_name[0]
|
22
|
+
if (start <= "Z" && start >= "A")
|
23
|
+
case method_name[-1]
|
24
|
+
when "b"
|
25
|
+
method_name = method_name[0..(method_name[-2] == "_" ? -3 : -2)] + args.join("_")
|
26
|
+
return BV.send(method_name)
|
27
|
+
when "i"
|
28
|
+
method_name = method_name[0..(method_name[-2] == "_" ? -3 : -2)] + args.join("_")
|
29
|
+
return IV.send(method_name)
|
30
|
+
when "f"
|
31
|
+
method_name = method_name[0..(method_name[-2] == "_" ? -3 : -2)] + args.join("_")
|
32
|
+
return LV.send(method_name)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
old_method_missing(value, *args)
|
36
|
+
end
|
37
|
+
|
38
|
+
def _profile
|
39
|
+
start = Time.now
|
40
|
+
return_value = yield
|
41
|
+
return return_value, Time.now - start
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
|
46
|
+
module Kernel
|
47
|
+
##
|
48
|
+
# Adds assertion capabilities to ruby.
|
49
|
+
# The assert function will raise an error if the inner block returns false.
|
50
|
+
# The error will contain the file, line number and source line of the failing assertion.
|
51
|
+
##
|
52
|
+
class AssertionException < Exception; end
|
53
|
+
|
54
|
+
##
|
55
|
+
# Ensure the SCRIPT_LINES global variable exists so that we can access the source of the failed assertion
|
56
|
+
##
|
57
|
+
::SCRIPT_LINES__ = {} unless defined? ::SCRIPT_LINES__
|
58
|
+
|
59
|
+
##
|
60
|
+
# If assertion returns false we return a new assertion exception with the failing file and line,
|
61
|
+
# and attempt to return the failed source if accessible.
|
62
|
+
##
|
63
|
+
def assert(truthy=false)
|
64
|
+
unless truthy || (block_given? && yield)
|
65
|
+
file, line = caller[0].split(":")
|
66
|
+
error_message = "Assertion Failed! < #{file}:#{line}:#{ SCRIPT_LINES__[file][line.to_i - 1][0..-2] rescue ''} >"
|
67
|
+
raise AssertionException.new(error_message)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
|
73
|
+
def given
|
74
|
+
@dummy ||= begin
|
75
|
+
dummy = {}
|
76
|
+
class << dummy
|
77
|
+
def [](*args)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
dummy
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,26 @@
|
|
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
|
+
def const_missing(value)
|
15
|
+
method_name = "#{value}" rescue ""
|
16
|
+
if (("A".."Z").include?(method_name[0]))
|
17
|
+
if(method_name.end_with?("b"))
|
18
|
+
BV.send(method_name[0..(method_name[-2] == "_" ? -3 : -2)])
|
19
|
+
elsif(method_name.end_with?("i"))
|
20
|
+
IV.send(method_name[0..(method_name[-2] == "_" ? -3 : -2)])
|
21
|
+
elsif(method_name.end_with?("f"))
|
22
|
+
LV.send(method_name[0..(method_name[-2] == "_" ? -3 : -2)])
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
data/lib/helpers/log.rb
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
##
|
2
|
+
# Basic logger module.
|
3
|
+
# Allows logging in the format
|
4
|
+
#
|
5
|
+
# "This is a string".log(:debug)
|
6
|
+
# "Oh no!".log(:error)
|
7
|
+
#
|
8
|
+
# Log level is set as follows.
|
9
|
+
# Rulp::Logger::level = :debug
|
10
|
+
#
|
11
|
+
##
|
12
|
+
module Rulp
|
13
|
+
module Logger
|
14
|
+
DEBUG = 5
|
15
|
+
INFO = 4
|
16
|
+
WARN = 3
|
17
|
+
ERROR = 2
|
18
|
+
OFF = 1
|
19
|
+
|
20
|
+
LEVELS = {
|
21
|
+
debug: DEBUG,
|
22
|
+
info: INFO,
|
23
|
+
warn: WARN,
|
24
|
+
error: ERROR,
|
25
|
+
off: OFF
|
26
|
+
}
|
27
|
+
|
28
|
+
def self.level=(value)
|
29
|
+
raise Exception.new("#{value} is not a valid log level") unless LEVELS[value]
|
30
|
+
@@level = value
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.level
|
34
|
+
@@level || :info
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.log(level, message)
|
38
|
+
if(LEVELS[level].to_i <= LEVELS[self.level])
|
39
|
+
puts("[#{level}] #{message}")
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
self.level = :info
|
44
|
+
|
45
|
+
class ::String
|
46
|
+
def log(level)
|
47
|
+
Logger::log(level, self)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
class ::Array
|
52
|
+
def log(level, sep="\n")
|
53
|
+
Logger::log(level, self.join("#{sep}[#{level}] "))
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
data/lib/rulp.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
##
|
2
|
+
# An LP Expression constraint. A mathematical expression of which the result
|
3
|
+
# must be constrained in some way.
|
4
|
+
##
|
5
|
+
class Constraint
|
6
|
+
def initialize(*constraint_expression)
|
7
|
+
@expressions , @constraint_op, @value = constraint_expression
|
8
|
+
end
|
9
|
+
|
10
|
+
def variables
|
11
|
+
@expressions.variables
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_s
|
15
|
+
return "#{@expressions} #{@constraint_op == :== ? "=" : @constraint_op} #{@value}"
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,112 @@
|
|
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 +(other)
|
12
|
+
return Expressions.new(@expressions + [other])
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_s
|
16
|
+
@expressions.map{|expression|
|
17
|
+
[expression.operand < 0 ? " " : " + ", expression.to_s]
|
18
|
+
}.flatten[1..-1].join("")
|
19
|
+
end
|
20
|
+
|
21
|
+
def variables
|
22
|
+
@expressions.map(&:variable)
|
23
|
+
end
|
24
|
+
|
25
|
+
[:==, :<, :<=, :>, :>=].each do |constraint_type|
|
26
|
+
define_method(constraint_type){|value|
|
27
|
+
Constraint.new(self, constraint_type, value)
|
28
|
+
}
|
29
|
+
end
|
30
|
+
|
31
|
+
def -@
|
32
|
+
-self.expressions[0]
|
33
|
+
self
|
34
|
+
end
|
35
|
+
|
36
|
+
def -(other)
|
37
|
+
other = -other
|
38
|
+
self + other
|
39
|
+
end
|
40
|
+
|
41
|
+
def +(other)
|
42
|
+
Expressions.new(self.expressions + Expressions[other].expressions)
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.[](value)
|
46
|
+
return Expressions.new([Fragment.new(value, 1)]) if value.kind_of?(LV)
|
47
|
+
return Expressions.new([value]) if value.kind_of?(Fragment)
|
48
|
+
return value if value.kind_of?(Expressions)
|
49
|
+
end
|
50
|
+
|
51
|
+
def evaluate
|
52
|
+
self.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
|
+
return Expressions.new([self] + Expressions[other].expressions)
|
69
|
+
end
|
70
|
+
|
71
|
+
def -(other)
|
72
|
+
self.+(-other)
|
73
|
+
end
|
74
|
+
|
75
|
+
def *(value)
|
76
|
+
Fragment.new(@lv, @operand * value)
|
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
|
+
[:==, :<, :<=, :>, :>=].each do |constraint_type|
|
97
|
+
define_method(constraint_type){|value|
|
98
|
+
Constraint.new(Expressions.new(self), constraint_type, value)
|
99
|
+
}
|
100
|
+
end
|
101
|
+
|
102
|
+
def to_s
|
103
|
+
case @operand
|
104
|
+
when -1
|
105
|
+
"-#{@lv}"
|
106
|
+
when 1
|
107
|
+
"#{@lv}"
|
108
|
+
else
|
109
|
+
"#{@operand} #{@lv}"
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
data/lib/rulp/lv.rb
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
##
|
2
|
+
# An LP Variable. Used as arguments in LP Expressions.
|
3
|
+
# The subtypes BV and IV represent Binary and Integer variables.
|
4
|
+
# These are constructed dynamically by using the special Capitalised variable declaration syntax.
|
5
|
+
##
|
6
|
+
class LV
|
7
|
+
attr_reader :name
|
8
|
+
attr_accessor :lt, :lte, :gt, :gte, :value
|
9
|
+
include Rulp::Bounds
|
10
|
+
include Rulp::Initializers
|
11
|
+
|
12
|
+
def to_proc
|
13
|
+
->(index){ send(self.meth(index)) }
|
14
|
+
end
|
15
|
+
|
16
|
+
def meth(index)
|
17
|
+
"#{self.name}#{index}_#{self.suffix}"
|
18
|
+
end
|
19
|
+
|
20
|
+
def suffix
|
21
|
+
"f"
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.method_missing(name, *args)
|
25
|
+
return self.definition( "#{name}#{args.join("_")}" )
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.const_missing(name)
|
29
|
+
return self.definition(name)
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.definition(name)
|
33
|
+
self.class.send(:define_method, name){
|
34
|
+
LV::names_table["#{name}#{self}"] if LV::names_table["#{name}#{self}"].class == self
|
35
|
+
}
|
36
|
+
return self.send(name) || self.new(name)
|
37
|
+
end
|
38
|
+
|
39
|
+
def * (numeric)
|
40
|
+
self.nocoerce
|
41
|
+
Expressions.new([Fragment.new(self, numeric)])
|
42
|
+
end
|
43
|
+
|
44
|
+
def -@
|
45
|
+
return self * -1
|
46
|
+
end
|
47
|
+
|
48
|
+
def -(other)
|
49
|
+
self + (-other)
|
50
|
+
end
|
51
|
+
|
52
|
+
def + (expressions)
|
53
|
+
Expressions[self] + Expressions[expressions]
|
54
|
+
end
|
55
|
+
|
56
|
+
def value
|
57
|
+
if self.class == BV
|
58
|
+
return @value.round(2) == 1
|
59
|
+
else
|
60
|
+
@value
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
class BV < LV;
|
66
|
+
def suffix
|
67
|
+
"b"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
class IV < LV;
|
71
|
+
def suffix
|
72
|
+
"i"
|
73
|
+
end
|
74
|
+
end
|
data/lib/rulp/rulp.rb
ADDED
@@ -0,0 +1,134 @@
|
|
1
|
+
require_relative "rulp_bounds"
|
2
|
+
require_relative "rulp_initializers"
|
3
|
+
require_relative "lv"
|
4
|
+
require_relative "constraint"
|
5
|
+
require_relative "expression"
|
6
|
+
|
7
|
+
require_relative "../solvers/solvers"
|
8
|
+
require_relative "../extensions/extensions"
|
9
|
+
require_relative "../helpers/log"
|
10
|
+
|
11
|
+
require 'set'
|
12
|
+
|
13
|
+
GLPK = "glpsol"
|
14
|
+
SCIP = "scip"
|
15
|
+
CBC = "cbc"
|
16
|
+
|
17
|
+
module Rulp
|
18
|
+
attr_accessor :expressions
|
19
|
+
MIN = "Minimize"
|
20
|
+
MAX = "Maximize"
|
21
|
+
|
22
|
+
GLPK = ::GLPK
|
23
|
+
SCIP = ::SCIP
|
24
|
+
CBC = ::CBC
|
25
|
+
|
26
|
+
SOLVERS = {
|
27
|
+
"glpsol" => Glpk,
|
28
|
+
"scip" => Scip,
|
29
|
+
"cbc" => Cbc
|
30
|
+
}
|
31
|
+
|
32
|
+
def self.Glpk(lp)
|
33
|
+
lp.solve_with(GLPK)
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.Cbc(lp)
|
37
|
+
lp.solve_with(CBC)
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.Scip(lp)
|
41
|
+
lp.solve_with(SCIP)
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.Max(objective_expression)
|
45
|
+
"Creating maximization problem".log :info
|
46
|
+
Problem.new(Rulp::MAX, objective_expression)
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.Min(objective_expression)
|
50
|
+
"Creating minimization problem".log :info
|
51
|
+
Problem.new(Rulp::MIN, objective_expression)
|
52
|
+
end
|
53
|
+
|
54
|
+
class Problem
|
55
|
+
def initialize(objective, objective_expression)
|
56
|
+
@objective = objective
|
57
|
+
@variables = Set.new
|
58
|
+
@objective_expression = objective_expression.kind_of?(LV) ? 1 * objective_expression : objective_expression
|
59
|
+
@variables.merge(@objective_expression.variables)
|
60
|
+
@constraints = []
|
61
|
+
end
|
62
|
+
|
63
|
+
def [](*constraints)
|
64
|
+
@constraints.concat(constraints)
|
65
|
+
@variables.merge(constraints.map(&:variables).flatten)
|
66
|
+
self
|
67
|
+
end
|
68
|
+
|
69
|
+
def constraints
|
70
|
+
@constraints.map.with_index{|constraint, i|
|
71
|
+
" c#{i}: #{constraint}"
|
72
|
+
}.join("\n")
|
73
|
+
end
|
74
|
+
|
75
|
+
def integers
|
76
|
+
ints = @variables.select{|x| x.kind_of?(IV) }.join(" ")
|
77
|
+
return "\nGeneral\n #{ints}" if(ints.length > 0)
|
78
|
+
end
|
79
|
+
|
80
|
+
def bits
|
81
|
+
bits = @variables.select{|x| x.kind_of?(BV) }.join(" ")
|
82
|
+
return "\nBinary\n #{bits}" if(bits.length > 0)
|
83
|
+
end
|
84
|
+
|
85
|
+
def bounds
|
86
|
+
@variables.map{|var|
|
87
|
+
next unless var.bounds_str
|
88
|
+
" #{var.bounds_str}"
|
89
|
+
}.compact.join("\n")
|
90
|
+
end
|
91
|
+
|
92
|
+
def get_output_filename
|
93
|
+
"/tmp/rulp-#{Random.rand(0..1000)}.lp"
|
94
|
+
end
|
95
|
+
|
96
|
+
def output(filename)
|
97
|
+
IO.write(filename, self)
|
98
|
+
end
|
99
|
+
|
100
|
+
def solve_with(type, open_definition=false, open_solution=false)
|
101
|
+
filename = get_output_filename
|
102
|
+
solver = SOLVERS[type].new(filename)
|
103
|
+
|
104
|
+
"Writing problem".log(:info)
|
105
|
+
IO.write(filename, self)
|
106
|
+
|
107
|
+
`open #{filename}` if open_definition
|
108
|
+
|
109
|
+
"Solving problem".log(:info)
|
110
|
+
_, time = _profile{ solver.solve(open_solution) }
|
111
|
+
|
112
|
+
"Solver took #{time}".log(:info)
|
113
|
+
|
114
|
+
"Parsing result".log(:info)
|
115
|
+
solver.store_results(@variables)
|
116
|
+
|
117
|
+
result = @objective_expression.evaluate
|
118
|
+
|
119
|
+
"Objective: #{result}\n#{@variables.map{|v|[v.name, "=", v.value].join(' ') if v.value}.compact.join("\n")}".log(:debug)
|
120
|
+
return result
|
121
|
+
end
|
122
|
+
|
123
|
+
def to_s
|
124
|
+
"\
|
125
|
+
#{@objective}
|
126
|
+
obj: #{@objective_expression}
|
127
|
+
Subject to
|
128
|
+
#{constraints}
|
129
|
+
Bounds
|
130
|
+
#{bounds}#{integers}#{bits}
|
131
|
+
End"
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module Rulp
|
2
|
+
module Bounds
|
3
|
+
|
4
|
+
attr_accessor :const
|
5
|
+
|
6
|
+
DIRS = {">" => {value: "gt=", equality: "gte="}, "<" => {value: "lt=", equality: "lte="}}
|
7
|
+
DIRS_REVERSED = {">" => DIRS["<"], "<" => DIRS[">"]}
|
8
|
+
|
9
|
+
def relative_constraint direction, value, equality=false
|
10
|
+
direction = coerced? ? DIRS_REVERSED[direction] : DIRS[direction]
|
11
|
+
self.const = false
|
12
|
+
self.send(direction[:value], value)
|
13
|
+
self.send(direction[:equality], equality)
|
14
|
+
return self
|
15
|
+
end
|
16
|
+
|
17
|
+
def nocoerce
|
18
|
+
@@coerced = false
|
19
|
+
end
|
20
|
+
|
21
|
+
def coerced?
|
22
|
+
was_coerced = @@coerced rescue nil
|
23
|
+
@@coerced = false
|
24
|
+
return was_coerced
|
25
|
+
end
|
26
|
+
|
27
|
+
def >(val)
|
28
|
+
relative_constraint(">", val)
|
29
|
+
end
|
30
|
+
|
31
|
+
def >=(val)
|
32
|
+
relative_constraint(">", val, true)
|
33
|
+
end
|
34
|
+
|
35
|
+
def <(val)
|
36
|
+
relative_constraint("<", val)
|
37
|
+
end
|
38
|
+
|
39
|
+
def <=(val)
|
40
|
+
relative_constraint("<", val, true)
|
41
|
+
end
|
42
|
+
|
43
|
+
def ==(val)
|
44
|
+
self.const = val
|
45
|
+
end
|
46
|
+
|
47
|
+
def coerce(something)
|
48
|
+
@@coerced = true
|
49
|
+
return self, something
|
50
|
+
end
|
51
|
+
|
52
|
+
def bounds_str
|
53
|
+
return nil if !(self.gt || self.lt || self.const)
|
54
|
+
return "#{self.name} = #{self.const}" if self.const
|
55
|
+
|
56
|
+
[
|
57
|
+
self.gt,
|
58
|
+
self.gt ? "<=" : nil,
|
59
|
+
self.name,
|
60
|
+
self.lt ? "<=" : nil,
|
61
|
+
self.lt
|
62
|
+
].compact.join(" ")
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Rulp
|
2
|
+
module Initializers
|
3
|
+
def initialize(name)
|
4
|
+
raise Exception.new("Variable with the name #{name} of a different type (#{LV::names_table[name].class}) already exists") if LV::names_table[name]
|
5
|
+
LV::names_table["#{name}#{self.class}"] = self
|
6
|
+
@name = name
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.included(base)
|
10
|
+
base.extend(ClassMethods)
|
11
|
+
end
|
12
|
+
|
13
|
+
module ClassMethods
|
14
|
+
def names_table
|
15
|
+
@@names ||= {}
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_s
|
20
|
+
"#{self.name}"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
data/lib/solvers/cbc.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
class Cbc < Solver
|
2
|
+
def solve(open_solution=false)
|
3
|
+
`#{executable} #{@filename} branch solution #{@outfile}`
|
4
|
+
`open #{@outfile}` if open_solution
|
5
|
+
end
|
6
|
+
|
7
|
+
def executable
|
8
|
+
:cbc
|
9
|
+
end
|
10
|
+
|
11
|
+
def store_results(variables)
|
12
|
+
rows = IO.read(@outfile).split("\n")
|
13
|
+
objective = rows[0].split(/\s+/)[-1].to_f
|
14
|
+
vars_by_name = {}
|
15
|
+
rows[1..-1].each do |row|
|
16
|
+
cols = row.strip.split(/\s+/)
|
17
|
+
vars_by_name[cols[1].to_s] = cols[2].to_f
|
18
|
+
end
|
19
|
+
variables.each do |var|
|
20
|
+
var.value = vars_by_name[var.name.to_s].to_f
|
21
|
+
end
|
22
|
+
return objective
|
23
|
+
end
|
24
|
+
end
|
data/lib/solvers/glpk.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
class Glpk < Solver
|
2
|
+
def solve(open_solution=false)
|
3
|
+
`#{executable} --lp #{@filename} --write #{@outfile}`
|
4
|
+
`open #{@outfile}` if open_solution
|
5
|
+
end
|
6
|
+
|
7
|
+
def executable
|
8
|
+
:glpsol
|
9
|
+
end
|
10
|
+
|
11
|
+
def store_results(variables)
|
12
|
+
rows = IO.read(@outfile).split("\n")
|
13
|
+
variables.zip(rows[-variables.count..-1]).each do |var, row|
|
14
|
+
cols = row.split(" ")
|
15
|
+
var.value = cols[[1, cols.count - 1].min].to_f
|
16
|
+
end
|
17
|
+
return rows[1].split(" ")[-1]
|
18
|
+
end
|
19
|
+
end
|
data/lib/solvers/scip.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
class Scip < Solver
|
2
|
+
def solve(open_solution=false)
|
3
|
+
`#{executable} -f #{@filename} > #{@outfile}`
|
4
|
+
`open #{@outfile}` if open_solution
|
5
|
+
end
|
6
|
+
|
7
|
+
def executable
|
8
|
+
:scip
|
9
|
+
end
|
10
|
+
|
11
|
+
def store_results(variables)
|
12
|
+
results = IO.read(@outfile)
|
13
|
+
start = results.sub(/.*?primal solution:.*?=+/m, "")
|
14
|
+
stripped = start.sub(/Statistics.+/m, "").strip
|
15
|
+
rows = stripped.split("\n")
|
16
|
+
|
17
|
+
objective = rows[0].split(/\s+/)[-1].to_f
|
18
|
+
|
19
|
+
vars_by_name = {}
|
20
|
+
rows[1..-1].each do |row|
|
21
|
+
cols = row.strip.split(/\s+/)
|
22
|
+
vars_by_name[cols[0].to_s] = cols[1].to_f
|
23
|
+
end
|
24
|
+
variables.each do |var|
|
25
|
+
var.value = vars_by_name[var.name.to_s].to_f
|
26
|
+
end
|
27
|
+
|
28
|
+
return objective
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
class Solver
|
2
|
+
def initialize(filename)
|
3
|
+
@filename = filename
|
4
|
+
@outfile = "/tmp/#{executable}-output.txt"
|
5
|
+
raise Exception.new("Couldn't find solver #{executable}!") if `which #{executable}`.length == 0
|
6
|
+
@solver_exists = true
|
7
|
+
end
|
8
|
+
|
9
|
+
def store_results(variables)
|
10
|
+
puts "Not yet implemented"
|
11
|
+
end
|
12
|
+
|
13
|
+
def solver_exists?
|
14
|
+
@solver_exists || false
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
require_relative 'cbc'
|
19
|
+
require_relative 'scip'
|
20
|
+
require_relative 'glpk'
|
@@ -0,0 +1,44 @@
|
|
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.sum
|
12
|
+
items_costs = @items.map{|item| item * Random.rand(1.0...5.0)}.sum
|
13
|
+
|
14
|
+
@problem =
|
15
|
+
Rulp::Min( items_costs ) [
|
16
|
+
items_count >= 10,
|
17
|
+
items_costs >= 15
|
18
|
+
]
|
19
|
+
end
|
20
|
+
|
21
|
+
def test_scip
|
22
|
+
solution = Rulp::Scip @problem
|
23
|
+
selected = @items.select(&:value)
|
24
|
+
assert_equal selected.length, 10
|
25
|
+
assert_operator solution.round(2), :>=, 15
|
26
|
+
assert_operator solution, :<=, 25
|
27
|
+
end
|
28
|
+
|
29
|
+
def test_cbc
|
30
|
+
solution = Rulp::Cbc @problem
|
31
|
+
selected = @items.select(&:value)
|
32
|
+
assert_equal selected.length, 10
|
33
|
+
assert_operator solution.round(2), :>=, 15
|
34
|
+
assert_operator solution, :<=, 25
|
35
|
+
end
|
36
|
+
|
37
|
+
def test_glpk
|
38
|
+
solution = Rulp::Glpk @problem
|
39
|
+
selected = @items.select(&:value)
|
40
|
+
assert_equal selected.length, 10
|
41
|
+
assert_operator solution.round(2), :>=, 15
|
42
|
+
assert_operator solution, :<=, 25
|
43
|
+
end
|
44
|
+
end
|
data/test/test_helper.rb
ADDED
data/test/test_simple.rb
ADDED
@@ -0,0 +1,50 @@
|
|
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
|
+
@problem =
|
18
|
+
Rulp::Max( 10 * X_i + 6 * Y_i + 4 * Z_i ) [
|
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_scip
|
26
|
+
solution = Rulp::Scip @problem
|
27
|
+
assert_equal X_i.value, 33
|
28
|
+
assert_equal Y_i.value, 67
|
29
|
+
assert_equal Z_i.value, 0
|
30
|
+
assert_equal solution , 732
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
def test_cbc
|
35
|
+
solution = Rulp::Cbc @problem
|
36
|
+
assert_equal X_i.value, 33
|
37
|
+
assert_equal Y_i.value, 67
|
38
|
+
assert_equal Z_i.value, 0
|
39
|
+
assert_equal solution , 732
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
def test_glpk
|
44
|
+
solution = Rulp::Glpk @problem
|
45
|
+
assert_equal X_i.value, 33
|
46
|
+
assert_equal Y_i.value, 67
|
47
|
+
assert_equal Z_i.value, 0
|
48
|
+
assert_equal solution , 732
|
49
|
+
end
|
50
|
+
end
|
metadata
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rulp
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Wouter Coppieters
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-02-18 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: A simple Ruby LP description DSL
|
14
|
+
email: wc@pico.net.nz
|
15
|
+
executables:
|
16
|
+
- rulp
|
17
|
+
extensions: []
|
18
|
+
extra_rdoc_files: []
|
19
|
+
files:
|
20
|
+
- bin/rulp
|
21
|
+
- lib/extensions/array_extensions.rb
|
22
|
+
- lib/extensions/extensions.rb
|
23
|
+
- lib/extensions/kernel_extensions.rb
|
24
|
+
- lib/extensions/object_extensions.rb
|
25
|
+
- lib/helpers/log.rb
|
26
|
+
- lib/rulp.rb
|
27
|
+
- lib/rulp/constraint.rb
|
28
|
+
- lib/rulp/expression.rb
|
29
|
+
- lib/rulp/lv.rb
|
30
|
+
- lib/rulp/rulp.rb
|
31
|
+
- lib/rulp/rulp_bounds.rb
|
32
|
+
- lib/rulp/rulp_initializers.rb
|
33
|
+
- lib/solvers/cbc.rb
|
34
|
+
- lib/solvers/glpk.rb
|
35
|
+
- lib/solvers/scip.rb
|
36
|
+
- lib/solvers/solvers.rb
|
37
|
+
- test/test_boolean.rb
|
38
|
+
- test/test_helper.rb
|
39
|
+
- test/test_simple.rb
|
40
|
+
homepage:
|
41
|
+
licenses: []
|
42
|
+
metadata: {}
|
43
|
+
post_install_message:
|
44
|
+
rdoc_options: []
|
45
|
+
require_paths:
|
46
|
+
- lib
|
47
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
48
|
+
requirements:
|
49
|
+
- - ! '>='
|
50
|
+
- !ruby/object:Gem::Version
|
51
|
+
version: '0'
|
52
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
53
|
+
requirements:
|
54
|
+
- - ! '>='
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
version: '0'
|
57
|
+
requirements: []
|
58
|
+
rubyforge_project:
|
59
|
+
rubygems_version: 2.3.0
|
60
|
+
signing_key:
|
61
|
+
specification_version: 4
|
62
|
+
summary: Ruby Linear Programming
|
63
|
+
test_files:
|
64
|
+
- test/test_boolean.rb
|
65
|
+
- test/test_helper.rb
|
66
|
+
- test/test_simple.rb
|
67
|
+
has_rdoc:
|