rulp 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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:
|