mipper 0.0.5 → 0.0.6
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 +4 -4
- data/lib/mipper.rb +1 -0
- data/lib/mipper/cbc/model.rb +118 -54
- data/lib/mipper/constraint.rb +4 -0
- data/lib/mipper/glpk/ext/structs.rb +22 -21
- data/lib/mipper/glpk/model.rb +30 -28
- data/lib/mipper/gurobi/model.rb +81 -75
- data/lib/mipper/lp_solve/model.rb +66 -57
- data/lib/mipper/model.rb +51 -23
- data/lib/mipper/solution.rb +11 -0
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 99f7bd81a866c8127ba5527dd8eb2107fc3c3ee1
|
4
|
+
data.tar.gz: 6b94eefa7fb9ab14b6400963f7975fca4b40b538
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f7c81f9e6743229544bd55ba20796c39a80aa35ba63cea211722ab2d56056dd9cc86314d685cd0ef746daca4ebe03da8b9b92da3925bc76e5dc21ee1f30f00b6
|
7
|
+
data.tar.gz: 1c43605bf6efdb20bd227f569b385c89bc25567416e5511d28125d81ac081884c93d754fe4e6667631b6be31440c836384b808e72503203980628dff1140e700
|
data/lib/mipper.rb
CHANGED
data/lib/mipper/cbc/model.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
require 'zlib'
|
2
2
|
|
3
3
|
module MIPPeR
|
4
|
+
# A linear programming model using the COIN-OR solver
|
4
5
|
class CbcModel < Model
|
5
6
|
attr_reader :ptr
|
6
7
|
|
@@ -11,18 +12,31 @@ module MIPPeR
|
|
11
12
|
@constr_count = 0
|
12
13
|
|
13
14
|
# Construct a new model
|
14
|
-
@ptr =
|
15
|
-
Cbc.method(:Cbc_deleteModel)
|
16
|
-
Cbc.Cbc_setParameter @ptr, 'logLevel', '0'
|
15
|
+
@ptr = new_model
|
17
16
|
end
|
18
17
|
|
19
18
|
# Write the model to a file in MPS format
|
20
19
|
def write_mps(filename)
|
21
|
-
|
22
|
-
|
20
|
+
# Make a new model and ensure everything is added
|
21
|
+
old_ptr = @ptr
|
22
|
+
@ptr = new_model
|
23
|
+
parent_update
|
24
|
+
|
25
|
+
Cbc.Cbc_writeMps @ptr, filename.chomp('.mps')
|
23
26
|
contents = Zlib::GzipReader.open(filename + '.gz').read
|
24
27
|
File.delete(filename + '.gz')
|
25
28
|
File.open(filename, 'w').write contents
|
29
|
+
|
30
|
+
# Reset to the original model
|
31
|
+
@ptr = old_ptr
|
32
|
+
reset_model
|
33
|
+
end
|
34
|
+
|
35
|
+
alias_method :parent_update, :update
|
36
|
+
|
37
|
+
# Avoid doing anything here. Updating multiple times will
|
38
|
+
# break the model so we defer to #solve.
|
39
|
+
def update
|
26
40
|
end
|
27
41
|
|
28
42
|
# Set the sense of the model
|
@@ -35,40 +49,24 @@ module MIPPeR
|
|
35
49
|
# Optimize the model
|
36
50
|
def optimize
|
37
51
|
# Ensure pending variables and constraints are added
|
38
|
-
|
52
|
+
parent_update
|
39
53
|
|
40
54
|
# Run the solver and save the status for later
|
41
55
|
Cbc.Cbc_solve @ptr
|
42
56
|
fail if Cbc.Cbc_status(@ptr) != 0
|
43
57
|
|
44
|
-
|
45
|
-
if Cbc.Cbc_isProvenOptimal(@ptr) == 1
|
46
|
-
@status = :optimized
|
47
|
-
elsif Cbc.Cbc_isProvenInfeasible(@ptr) == 1 or
|
48
|
-
Cbc.Cbc_isContinuousUnbounded(@ptr) == 1
|
49
|
-
@status = :invalid
|
50
|
-
else
|
51
|
-
@status = :unknown
|
52
|
-
end
|
53
|
-
end
|
54
|
-
|
55
|
-
# Get the status of the model
|
56
|
-
def status
|
57
|
-
@status
|
58
|
-
end
|
58
|
+
save_solution
|
59
59
|
|
60
|
-
|
61
|
-
|
62
|
-
Cbc.Cbc_getObjValue @ptr
|
60
|
+
@ptr = new_model
|
61
|
+
reset_model
|
63
62
|
end
|
64
63
|
|
65
|
-
#
|
66
|
-
def
|
67
|
-
|
68
|
-
|
69
|
-
|
64
|
+
# Set the bounds of a variable in the model
|
65
|
+
def set_variable_bounds(var_index, lb, ub, force = false)
|
66
|
+
# This is a bit of a hack so that we don't try to set
|
67
|
+
# the variable bounds before they get added to the model
|
68
|
+
return unless force
|
70
69
|
|
71
|
-
def set_variable_bounds(var_index, lb, ub)
|
72
70
|
Cbc.Cbc_setColLower @ptr, var_index, lb
|
73
71
|
Cbc.Cbc_setColUpper @ptr, var_index, ub
|
74
72
|
end
|
@@ -87,11 +85,40 @@ module MIPPeR
|
|
87
85
|
|
88
86
|
# Add multiple constraints at once
|
89
87
|
def add_constraints(constrs)
|
90
|
-
|
88
|
+
start, index, value = build_constraint_matrix constrs
|
89
|
+
start_buffer = build_pointer_array start, :int
|
90
|
+
index_buffer = build_pointer_array index, :int
|
91
|
+
value_buffer = build_pointer_array value, :double
|
92
|
+
|
93
|
+
Cbc.Cbc_loadProblem @ptr, @variables.length, constrs.length,
|
94
|
+
start_buffer, index_buffer, value_buffer,
|
95
|
+
nil, nil, nil, nil, nil
|
96
|
+
|
97
|
+
store_model constrs, @variables
|
98
|
+
end
|
99
|
+
|
100
|
+
private
|
101
|
+
|
102
|
+
# Construct a new model object
|
103
|
+
def new_model
|
104
|
+
ptr = FFI::AutoPointer.new Cbc.Cbc_newModel,
|
105
|
+
Cbc.method(:Cbc_deleteModel)
|
106
|
+
Cbc.Cbc_setParameter ptr, 'logLevel', '0'
|
107
|
+
|
108
|
+
ptr
|
109
|
+
end
|
110
|
+
|
111
|
+
# Store the index which will be used for each constraint
|
112
|
+
def store_constraint_indexes(constrs)
|
91
113
|
constrs.each do |constr|
|
92
114
|
constr.instance_variable_set :@index, @constr_count
|
93
115
|
@constr_count += 1
|
94
116
|
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# Build a constraint matrix for the currently existing variables
|
120
|
+
def build_constraint_matrix(constrs)
|
121
|
+
store_constraint_indexes constrs
|
95
122
|
|
96
123
|
# Construct a matrix of non-zero values in CSC format
|
97
124
|
start = []
|
@@ -110,30 +137,60 @@ module MIPPeR
|
|
110
137
|
end
|
111
138
|
start << col_start
|
112
139
|
|
113
|
-
|
114
|
-
|
115
|
-
index_buffer = FFI::MemoryPointer.new :int, index.length
|
116
|
-
index_buffer.write_array_of_int index
|
117
|
-
value_buffer = FFI::MemoryPointer.new :double, value.length
|
118
|
-
value_buffer.write_array_of_double value
|
119
|
-
|
120
|
-
Cbc.Cbc_loadProblem @ptr, @variables.length, constrs.length,
|
121
|
-
start_buffer, index_buffer, value_buffer,
|
122
|
-
nil, nil, nil, nil, nil
|
140
|
+
[start, index, value]
|
141
|
+
end
|
123
142
|
|
143
|
+
# Store all data for the model
|
144
|
+
def store_model(constrs, vars)
|
145
|
+
# Store all constraints
|
124
146
|
constrs.each do |constr|
|
125
147
|
store_constraint constr
|
126
148
|
@constraints << constr
|
127
149
|
end
|
128
150
|
|
129
151
|
# We store variables now since they didn't exist earlier
|
130
|
-
|
152
|
+
vars.each_with_index do |var, i|
|
131
153
|
var.instance_variable_set(:@index, i)
|
132
154
|
store_variable var
|
133
155
|
end
|
134
156
|
end
|
135
157
|
|
136
|
-
|
158
|
+
# Save the solution to the model for access later
|
159
|
+
def save_solution
|
160
|
+
# Check and store the model status
|
161
|
+
if Cbc.Cbc_isProvenOptimal(@ptr) == 1
|
162
|
+
status = :optimized
|
163
|
+
elsif Cbc.Cbc_isProvenInfeasible(@ptr) == 1 ||
|
164
|
+
Cbc.Cbc_isContinuousUnbounded(@ptr) == 1
|
165
|
+
status = :invalid
|
166
|
+
else
|
167
|
+
status = :unknown
|
168
|
+
end
|
169
|
+
|
170
|
+
if status == :optimized
|
171
|
+
objective_value = Cbc.Cbc_getObjValue @ptr
|
172
|
+
dblptr = Cbc.Cbc_getColSolution @ptr
|
173
|
+
values = dblptr.read_array_of_double(@variables.length)
|
174
|
+
variable_values = Hash[@variables.map do |var|
|
175
|
+
[var.name, values[var.index]]
|
176
|
+
end]
|
177
|
+
else
|
178
|
+
objective_value = nil
|
179
|
+
variable_values = {}
|
180
|
+
end
|
181
|
+
|
182
|
+
@solution = Solution.new status, objective_value, variable_values
|
183
|
+
end
|
184
|
+
|
185
|
+
# Reset the internal state of the model so we can reuse it
|
186
|
+
def reset_model
|
187
|
+
@var_count = 0
|
188
|
+
@constr_count = 0
|
189
|
+
@pending_variables = @variables
|
190
|
+
@pending_constraints = @constraints
|
191
|
+
@variables = []
|
192
|
+
@constraints = []
|
193
|
+
end
|
137
194
|
|
138
195
|
# Save the constraint to the model and update the constraint pointers
|
139
196
|
def store_constraint(constr)
|
@@ -143,17 +200,22 @@ module MIPPeR
|
|
143
200
|
|
144
201
|
# Set constraint properties
|
145
202
|
Cbc.Cbc_setRowName(@ptr, index, constr.name) unless constr.name.nil?
|
203
|
+
store_constraint_bounds index, constr.sense, constr.rhs
|
204
|
+
end
|
146
205
|
|
147
|
-
|
206
|
+
# Store the bounds for a given constraint
|
207
|
+
def store_constraint_bounds(index, sense, rhs)
|
208
|
+
case sense
|
148
209
|
when :==
|
149
|
-
lb = ub =
|
210
|
+
lb = ub = rhs
|
150
211
|
when :>=
|
151
|
-
lb =
|
212
|
+
lb = rhs
|
152
213
|
ub = Float::INFINITY
|
153
214
|
when :<=
|
154
215
|
lb = -Float::INFINITY
|
155
|
-
ub =
|
216
|
+
ub = rhs
|
156
217
|
end
|
218
|
+
|
157
219
|
Cbc.Cbc_setRowLower @ptr, index, lb
|
158
220
|
Cbc.Cbc_setRowUpper @ptr, index, ub
|
159
221
|
end
|
@@ -165,21 +227,23 @@ module MIPPeR
|
|
165
227
|
var.instance_variable_set(:@lower_bound, 0)
|
166
228
|
var.instance_variable_set(:@upper_bound, 1)
|
167
229
|
end
|
230
|
+
set_variable_bounds var.index, var.lower_bound, var.upper_bound, true
|
168
231
|
|
169
|
-
Cbc.Cbc_setColLower @ptr, var.index, var.lower_bound
|
170
|
-
Cbc.Cbc_setColUpper @ptr, var.index, var.upper_bound
|
171
232
|
Cbc.Cbc_setObjCoeff @ptr, var.index, var.coefficient
|
233
|
+
Cbc.Cbc_setColName(@ptr, var.index, var.name) unless var.name.nil?
|
234
|
+
set_variable_type var.index, var.type
|
235
|
+
end
|
172
236
|
|
173
|
-
|
237
|
+
# Set the type of a variable
|
238
|
+
def set_variable_type(index, type)
|
239
|
+
case type
|
174
240
|
when :continuous
|
175
|
-
Cbc.Cbc_setContinuous @ptr,
|
241
|
+
Cbc.Cbc_setContinuous @ptr, index
|
176
242
|
when :integer, :binary
|
177
|
-
Cbc.Cbc_setInteger @ptr,
|
243
|
+
Cbc.Cbc_setInteger @ptr, index
|
178
244
|
else
|
179
245
|
fail :type
|
180
246
|
end
|
181
|
-
|
182
|
-
Cbc.Cbc_setColName(@ptr, var.index, var.name) unless var.name.nil?
|
183
247
|
end
|
184
248
|
end
|
185
249
|
end
|
data/lib/mipper/constraint.rb
CHANGED
@@ -1,28 +1,29 @@
|
|
1
1
|
module MIPPeR
|
2
2
|
module GLPK
|
3
|
+
# Integer optimization control parameters
|
3
4
|
class IOCP < FFI::Struct
|
4
5
|
layout :msg_lev, :int,
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
6
|
+
:br_tech, :int,
|
7
|
+
:bt_tech, :int,
|
8
|
+
:tol_int, :double,
|
9
|
+
:tol_obj, :double,
|
10
|
+
:tm_lim, :int,
|
11
|
+
:out_frq, :int,
|
12
|
+
:out_dly, :int,
|
13
|
+
:cb_func, callback([:pointer, :pointer], :void),
|
14
|
+
:cb_info, :pointer,
|
15
|
+
:cb_size, :int,
|
16
|
+
:pp_tech, :int,
|
17
|
+
:mip_gap, :double,
|
18
|
+
:mir_cuts, :int,
|
19
|
+
:gmi_cuts, :int,
|
20
|
+
:cov_cuts, :int,
|
21
|
+
:clq_cuts, :int,
|
22
|
+
:presolve, :int,
|
23
|
+
:binarize, :int,
|
24
|
+
:fp_heur, :int,
|
25
|
+
:alien, :int,
|
26
|
+
:foo_bar, [:double, 29]
|
26
27
|
end
|
27
28
|
end
|
28
29
|
end
|
data/lib/mipper/glpk/model.rb
CHANGED
@@ -49,29 +49,9 @@ module MIPPeR
|
|
49
49
|
iocp = GLPK::IOCP.new
|
50
50
|
GLPK.glp_init_iocp iocp
|
51
51
|
iocp[:presolve] = GLPK::GLP_ON
|
52
|
-
|
53
|
-
end
|
54
|
-
|
55
|
-
# Get the status of the model
|
56
|
-
def status
|
57
|
-
case @status
|
58
|
-
when 0, GLPK::GLP_EMIPGAP
|
59
|
-
:optimized
|
60
|
-
when GLPK::GLP_EBOUND, GLPK::GLP_EROOT, GLPK::GLP_ENOPFS
|
61
|
-
:invalid
|
62
|
-
else
|
63
|
-
:unknown
|
64
|
-
end
|
65
|
-
end
|
66
|
-
|
67
|
-
# The value of the objective function
|
68
|
-
def objective_value
|
69
|
-
GLPK.glp_mip_obj_val @ptr
|
70
|
-
end
|
52
|
+
status = GLPK.glp_intopt(@ptr, iocp)
|
71
53
|
|
72
|
-
|
73
|
-
def variable_value(var)
|
74
|
-
GLPK.glp_mip_col_val(@ptr, var.index)
|
54
|
+
save_solution status
|
75
55
|
end
|
76
56
|
|
77
57
|
def set_variable_bounds(var_index, lb, ub)
|
@@ -116,12 +96,9 @@ module MIPPeR
|
|
116
96
|
end
|
117
97
|
end
|
118
98
|
|
119
|
-
ia_buffer =
|
120
|
-
|
121
|
-
|
122
|
-
ja_buffer.write_array_of_int @ja
|
123
|
-
ar_buffer = FFI::MemoryPointer.new :pointer, @ar.length
|
124
|
-
ar_buffer.write_array_of_double @ar
|
99
|
+
ia_buffer = build_pointer_array @ia, :int
|
100
|
+
ja_buffer = build_pointer_array @ja, :int
|
101
|
+
ar_buffer = build_pointer_array @ar, :double
|
125
102
|
|
126
103
|
GLPK.glp_load_matrix(@ptr, @ar.length - 1, ia_buffer, ja_buffer,
|
127
104
|
ar_buffer)
|
@@ -131,6 +108,31 @@ module MIPPeR
|
|
131
108
|
|
132
109
|
private
|
133
110
|
|
111
|
+
# Save the solution to the model for access later
|
112
|
+
def save_solution(status)
|
113
|
+
status = case status
|
114
|
+
when 0, GLPK::GLP_EMIPGAP
|
115
|
+
:optimized
|
116
|
+
when GLPK::GLP_EBOUND, GLPK::GLP_EROOT, GLPK::GLP_ENOPFS
|
117
|
+
:invalid
|
118
|
+
else
|
119
|
+
:unknown
|
120
|
+
end
|
121
|
+
|
122
|
+
if status == :optimized
|
123
|
+
objective_value = GLPK.glp_mip_obj_val @ptr
|
124
|
+
variable_values = Hash[@variables.map do |var|
|
125
|
+
value = GLPK.glp_mip_col_val(@ptr, var.index)
|
126
|
+
[var.name, value]
|
127
|
+
end]
|
128
|
+
else
|
129
|
+
objective_value = nil
|
130
|
+
variable_values = {}
|
131
|
+
end
|
132
|
+
|
133
|
+
@solution = Solution.new status, objective_value, variable_values
|
134
|
+
end
|
135
|
+
|
134
136
|
# Save the constraint to the model and update the constraint pointers
|
135
137
|
def store_constraint(constr)
|
136
138
|
# Update the constraint to track the index in the model
|
data/lib/mipper/gurobi/model.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
module MIPPeR
|
2
|
+
# A linear programming model using the Gurobi solver
|
2
3
|
class GurobiModel < Model
|
3
4
|
attr_reader :ptr, :environment
|
4
5
|
|
@@ -51,10 +52,25 @@ module MIPPeR
|
|
51
52
|
|
52
53
|
ret = Gurobi.GRBoptimize @ptr
|
53
54
|
fail if ret != 0
|
55
|
+
|
56
|
+
save_solution
|
54
57
|
end
|
55
58
|
|
59
|
+
# Compute an irreducible inconsistent subsytem for the model
|
60
|
+
def compute_iis
|
61
|
+
ret = Gurobi.GRBcomputeIIS @ptr
|
62
|
+
fail if ret != 0
|
63
|
+
end
|
64
|
+
|
65
|
+
def set_variable_bounds(var_index, lb, ub)
|
66
|
+
set_double_attribute Gurobi::GRB_DBL_ATTR_LB, var_index, lb
|
67
|
+
set_double_attribute Gurobi::GRB_DBL_ATTR_UB, var_index, ub
|
68
|
+
end
|
69
|
+
|
70
|
+
protected
|
71
|
+
|
56
72
|
# Get the status of the model
|
57
|
-
def
|
73
|
+
def gurobi_status
|
58
74
|
intptr = FFI::MemoryPointer.new :pointer
|
59
75
|
ret = Gurobi.GRBgetintattr @ptr, Gurobi::GRB_INT_ATTR_STATUS, intptr
|
60
76
|
fail if ret != 0
|
@@ -63,59 +79,31 @@ module MIPPeR
|
|
63
79
|
when Gurobi::GRB_OPTIMAL
|
64
80
|
:optimized
|
65
81
|
when Gurobi::GRB_INFEASIBLE, Gurobi::GRB_INF_OR_UNBD,
|
66
|
-
|
82
|
+
Gurobi::GRB_UNBOUNDED
|
67
83
|
:invalid
|
68
84
|
else
|
69
85
|
:unknown
|
70
86
|
end
|
71
87
|
end
|
72
88
|
|
73
|
-
# Compute an irreducible inconsistent subsytem for the model
|
74
|
-
def compute_IIS
|
75
|
-
ret = Gurobi.GRBcomputeIIS @ptr
|
76
|
-
fail if ret != 0
|
77
|
-
end
|
78
|
-
|
79
89
|
# The value of the objective function
|
80
|
-
def
|
90
|
+
def gurobi_objective
|
81
91
|
dblptr = FFI::MemoryPointer.new :pointer
|
82
92
|
ret = Gurobi.GRBgetdblattr @ptr, Gurobi::GRB_DBL_ATTR_OBJVAL, dblptr
|
83
93
|
fail if ret != 0
|
84
94
|
dblptr.read_double
|
85
95
|
end
|
86
96
|
|
87
|
-
def set_variable_bounds(var_index, lb, ub)
|
88
|
-
set_double_attribute Gurobi::GRB_DBL_ATTR_LB, var_index, lb
|
89
|
-
set_double_attribute Gurobi::GRB_DBL_ATTR_UB, var_index, ub
|
90
|
-
end
|
91
|
-
|
92
|
-
# Get the value of a variable from the model
|
93
|
-
def variable_value(var)
|
94
|
-
dblptr = FFI::MemoryPointer.new :pointer
|
95
|
-
Gurobi.GRBgetdblattrarray @ptr, Gurobi::GRB_DBL_ATTR_X,
|
96
|
-
var.index, 1, dblptr
|
97
|
-
dblptr.read_array_of_double(1)[0]
|
98
|
-
end
|
99
|
-
|
100
|
-
protected
|
101
|
-
|
102
97
|
# Add multiple variables to the model simultaneously
|
103
98
|
def add_variables(vars)
|
104
|
-
objective_buffer =
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
type_buffer = FFI::MemoryPointer.new :char, vars.length
|
114
|
-
type_buffer.write_array_of_char vars.map { |var| gurobi_type(var.type) }
|
115
|
-
|
116
|
-
names = array_to_pointers_to_names vars
|
117
|
-
names_buffer = FFI::MemoryPointer.new :pointer, vars.length
|
118
|
-
names_buffer.write_array_of_pointer names
|
99
|
+
objective_buffer = build_pointer_array vars.map(&:coefficient), :double
|
100
|
+
lb_buffer = build_pointer_array vars.map(&:lower_bound), :double
|
101
|
+
ub_buffer = build_pointer_array vars.map(&:upper_bound), :double
|
102
|
+
type_buffer = build_pointer_array(vars.map do |var|
|
103
|
+
gurobi_type(var.type)
|
104
|
+
end, :char)
|
105
|
+
names_buffer = build_pointer_array array_to_pointers_to_names(vars),
|
106
|
+
:pointer
|
119
107
|
|
120
108
|
ret = Gurobi.GRBaddvars @ptr, vars.length, 0, nil, nil, nil,
|
121
109
|
objective_buffer, lb_buffer, ub_buffer,
|
@@ -125,6 +113,10 @@ module MIPPeR
|
|
125
113
|
|
126
114
|
# Store all the variables in the model
|
127
115
|
vars.each { |var| store_variable var }
|
116
|
+
|
117
|
+
# Update the model with variables so constraint adds succeed
|
118
|
+
ret = Gurobi.GRBupdatemodel @ptr
|
119
|
+
fail if ret != 0
|
128
120
|
end
|
129
121
|
|
130
122
|
# Add a new variable to the model
|
@@ -139,37 +131,18 @@ module MIPPeR
|
|
139
131
|
|
140
132
|
# Add multiple constraints at once
|
141
133
|
def add_constraints(constrs)
|
142
|
-
cbeg =
|
143
|
-
cind = []
|
144
|
-
cval = []
|
145
|
-
constrs.each_with_index.map do |constr, i|
|
146
|
-
cbeg << cind.length
|
147
|
-
constr.expression.terms.each do |var, coeff|
|
148
|
-
cind << var.instance_variable_get(:@index)
|
149
|
-
cval << coeff
|
150
|
-
end
|
151
|
-
end
|
152
|
-
|
153
|
-
cbeg_buffer = FFI::MemoryPointer.new :pointer, cbeg.length
|
154
|
-
cbeg_buffer.write_array_of_int cbeg
|
155
|
-
|
156
|
-
cind_buffer = FFI::MemoryPointer.new :pointer, cind.length
|
157
|
-
cind_buffer.write_array_of_int cind
|
134
|
+
cbeg, cind, cval = build_constraint_matrix constrs
|
158
135
|
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
sense_buffer =
|
163
|
-
sense_buffer.write_array_of_char(constrs.map do |c|
|
136
|
+
cbeg_buffer = build_pointer_array cbeg, :int
|
137
|
+
cind_buffer = build_pointer_array cind, :int
|
138
|
+
cval_buffer = build_pointer_array cval, :double
|
139
|
+
sense_buffer = build_pointer_array(constrs.map do |c|
|
164
140
|
gurobi_sense(c.sense)
|
165
|
-
end)
|
166
|
-
|
167
|
-
rhs_buffer = FFI::MemoryPointer.new :pointer, constrs.length
|
168
|
-
rhs_buffer.write_array_of_double constrs.map(&:rhs)
|
141
|
+
end, :char)
|
142
|
+
rhs_buffer = build_pointer_array constrs.map(&:rhs), :double
|
169
143
|
|
170
|
-
|
171
|
-
|
172
|
-
names_buffer.write_array_of_pointer names
|
144
|
+
names_buffer = build_pointer_array array_to_pointers_to_names(constrs),
|
145
|
+
:pointer
|
173
146
|
|
174
147
|
ret = Gurobi.GRBaddconstrs @ptr, constrs.length, cind.length,
|
175
148
|
cbeg_buffer, cind_buffer, cval_buffer,
|
@@ -182,13 +155,10 @@ module MIPPeR
|
|
182
155
|
# Add a new constraint to the model
|
183
156
|
def add_constraint(constr)
|
184
157
|
terms = constr.expression.terms
|
185
|
-
indexes_buffer =
|
186
|
-
indexes_buffer.write_array_of_int(terms.each_key.map do |var|
|
158
|
+
indexes_buffer = build_pointer_array(terms.each_key.map do |var|
|
187
159
|
var.instance_variable_get(:@index)
|
188
|
-
end)
|
189
|
-
|
190
|
-
values_buffer = FFI::MemoryPointer.new :double, terms.length
|
191
|
-
values_buffer.write_array_of_double terms.values
|
160
|
+
end, :int)
|
161
|
+
values_buffer = build_pointer_array terms.values, :double
|
192
162
|
|
193
163
|
ret = Gurobi.GRBaddconstr @ptr, terms.length,
|
194
164
|
indexes_buffer, values_buffer,
|
@@ -206,6 +176,43 @@ module MIPPeR
|
|
206
176
|
proc { Gurobi.GRBfreemodel ptr }
|
207
177
|
end
|
208
178
|
|
179
|
+
# Construct a matrix of values for the given list of constraints
|
180
|
+
def build_constraint_matrix(constrs)
|
181
|
+
cbeg = []
|
182
|
+
cind = []
|
183
|
+
cval = []
|
184
|
+
constrs.each.map do |constr|
|
185
|
+
cbeg << cind.length
|
186
|
+
constr.expression.terms.each do |var, coeff|
|
187
|
+
cind << var.instance_variable_get(:@index)
|
188
|
+
cval << coeff
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
[cbeg, cind, cval]
|
193
|
+
end
|
194
|
+
|
195
|
+
# Save the solution to the model for access later
|
196
|
+
def save_solution
|
197
|
+
status = gurobi_status
|
198
|
+
|
199
|
+
if status == :optimized
|
200
|
+
objective_value = gurobi_objective
|
201
|
+
variable_values = Hash[@variables.map do |var|
|
202
|
+
dblptr = FFI::MemoryPointer.new :pointer
|
203
|
+
Gurobi.GRBgetdblattrarray @ptr, Gurobi::GRB_DBL_ATTR_X,
|
204
|
+
var.index, 1, dblptr
|
205
|
+
value = dblptr.read_array_of_double(1)[0]
|
206
|
+
[var.name, value]
|
207
|
+
end]
|
208
|
+
else
|
209
|
+
objective_value = nil
|
210
|
+
variable_values = {}
|
211
|
+
end
|
212
|
+
|
213
|
+
@solution = Solution.new status, objective_value, variable_values
|
214
|
+
end
|
215
|
+
|
209
216
|
# Save the variable to the model and update the variable pointers
|
210
217
|
def store_variable(var)
|
211
218
|
# Update the variable to track the index in the model
|
@@ -217,8 +224,7 @@ module MIPPeR
|
|
217
224
|
end
|
218
225
|
|
219
226
|
def set_double_attribute(name, var_index, value)
|
220
|
-
buffer =
|
221
|
-
buffer.write_array_of_double [value]
|
227
|
+
buffer = build_pointer_array [value], :double
|
222
228
|
ret = Gurobi.GRBsetdblattrarray @ptr, name, var_index, 1, buffer
|
223
229
|
fail if ret != 0
|
224
230
|
end
|
@@ -1,4 +1,5 @@
|
|
1
1
|
module MIPPeR
|
2
|
+
# A linear programming model using the lp_solve solver
|
2
3
|
class LPSolveModel < Model
|
3
4
|
attr_reader :ptr
|
4
5
|
|
@@ -30,35 +31,8 @@ module MIPPeR
|
|
30
31
|
update
|
31
32
|
|
32
33
|
# Run the solver and save the status for later
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
# Get the status of the model
|
37
|
-
def status
|
38
|
-
case @status
|
39
|
-
when LPSolve::OPTIMAL
|
40
|
-
:optimized
|
41
|
-
when LPSolve::INFEASIBLE, LPSolve::UNBOUNDED, LPSolve::DEGENERATE
|
42
|
-
:invalid
|
43
|
-
else
|
44
|
-
:unknown
|
45
|
-
end
|
46
|
-
end
|
47
|
-
|
48
|
-
# The value of the objective function
|
49
|
-
def objective_value
|
50
|
-
LPSolve.get_objective @ptr
|
51
|
-
end
|
52
|
-
|
53
|
-
# Get the value of a variable from the model
|
54
|
-
def variable_value(var)
|
55
|
-
# To access the value of a result variable we need an index into the
|
56
|
-
# solution which setarts with the objective function, followed by all
|
57
|
-
# of the constraints, and finally the variables
|
58
|
-
# We explicitly ask for the number of rows since extra constraints
|
59
|
-
# may have been added
|
60
|
-
rows = LPSolve.get_Nrows(@ptr)
|
61
|
-
LPSolve.get_var_primalresult @ptr, rows + var.index
|
34
|
+
status = LPSolve.solve(@ptr)
|
35
|
+
save_solution status
|
62
36
|
end
|
63
37
|
|
64
38
|
def set_variable_bounds(var_index, lb, ub)
|
@@ -90,6 +64,52 @@ module MIPPeR
|
|
90
64
|
|
91
65
|
private
|
92
66
|
|
67
|
+
# Build the constraint matrix and add it to the model
|
68
|
+
def store_constraint_matrix(constr, type)
|
69
|
+
# Initialize arrays used to hold the coefficients for each variable
|
70
|
+
row = []
|
71
|
+
colno = []
|
72
|
+
constr.expression.terms.each do |var, coeff|
|
73
|
+
row << coeff * 1.0
|
74
|
+
colno << var.instance_variable_get(:@index)
|
75
|
+
end
|
76
|
+
|
77
|
+
row_buffer = build_pointer_array row, :double
|
78
|
+
colno_buffer = build_pointer_array colno, :int
|
79
|
+
|
80
|
+
ret = LPSolve.add_constraintex(@ptr, constr.expression.terms.length,
|
81
|
+
row_buffer, colno_buffer,
|
82
|
+
type, constr.rhs)
|
83
|
+
fail if ret != 1
|
84
|
+
end
|
85
|
+
|
86
|
+
# Save the solution to the model for access later
|
87
|
+
def save_solution(status)
|
88
|
+
status = case status
|
89
|
+
when LPSolve::OPTIMAL
|
90
|
+
:optimized
|
91
|
+
when LPSolve::INFEASIBLE, LPSolve::UNBOUNDED,
|
92
|
+
LPSolve::DEGENERATE
|
93
|
+
:invalid
|
94
|
+
else
|
95
|
+
:unknown
|
96
|
+
end
|
97
|
+
|
98
|
+
if status == :optimized
|
99
|
+
objective_value = LPSolve.get_objective @ptr
|
100
|
+
rows = LPSolve.get_Nrows(@ptr)
|
101
|
+
variable_values = Hash[@variables.map do |var|
|
102
|
+
value = LPSolve.get_var_primalresult @ptr, rows + var.index
|
103
|
+
[var.name, value]
|
104
|
+
end]
|
105
|
+
else
|
106
|
+
objective_value = nil
|
107
|
+
variable_values = {}
|
108
|
+
end
|
109
|
+
|
110
|
+
@solution = Solution.new status, objective_value, variable_values
|
111
|
+
end
|
112
|
+
|
93
113
|
# Save the constraint to the model and update the constraint pointers
|
94
114
|
def store_constraint(constr)
|
95
115
|
# Update the constraint to track the index in the model
|
@@ -114,24 +134,7 @@ module MIPPeR
|
|
114
134
|
type = LPSolve::LE
|
115
135
|
end
|
116
136
|
|
117
|
-
|
118
|
-
row = []
|
119
|
-
colno = []
|
120
|
-
constr.expression.terms.each do |var, coeff|
|
121
|
-
row << coeff * 1.0
|
122
|
-
colno << var.instance_variable_get(:@index)
|
123
|
-
end
|
124
|
-
|
125
|
-
row_buffer = FFI::MemoryPointer.new :pointer, row.length
|
126
|
-
row_buffer.write_array_of_double row
|
127
|
-
colno_buffer = FFI::MemoryPointer.new :pointer, colno.length
|
128
|
-
colno_buffer.write_array_of_int colno
|
129
|
-
|
130
|
-
ret = LPSolve.add_constraintex(@ptr, constr.expression.terms.length,
|
131
|
-
row_buffer, colno_buffer,
|
132
|
-
type, constr.rhs)
|
133
|
-
fail if ret != 1
|
134
|
-
|
137
|
+
store_constraint_matrix constr, type
|
135
138
|
@constraints << constr
|
136
139
|
end
|
137
140
|
|
@@ -147,7 +150,22 @@ module MIPPeR
|
|
147
150
|
fail if ret != 1
|
148
151
|
|
149
152
|
# Set variable properties
|
150
|
-
|
153
|
+
set_variable_type index, var.type
|
154
|
+
|
155
|
+
ret = LPSolve.set_col_name(@ptr, index, var.name)
|
156
|
+
fail if ret != 1
|
157
|
+
|
158
|
+
ret = LPSolve.set_obj(@ptr, index, var.coefficient)
|
159
|
+
fail if ret != 1
|
160
|
+
|
161
|
+
set_variable_bounds index, var.lower_bound, var.upper_bound
|
162
|
+
|
163
|
+
@variables << var
|
164
|
+
end
|
165
|
+
|
166
|
+
# Set the type of a variable
|
167
|
+
def set_variable_type(index, type)
|
168
|
+
case type
|
151
169
|
when :integer
|
152
170
|
ret = LPSolve.set_int @ptr, index, 1
|
153
171
|
when :binary
|
@@ -157,17 +175,8 @@ module MIPPeR
|
|
157
175
|
else
|
158
176
|
fail type
|
159
177
|
end
|
160
|
-
fail if ret != 1
|
161
178
|
|
162
|
-
ret = LPSolve.set_col_name(@ptr, index, var.name)
|
163
179
|
fail if ret != 1
|
164
|
-
|
165
|
-
ret = LPSolve.set_obj(@ptr, index, var.coefficient)
|
166
|
-
fail if ret != 1
|
167
|
-
|
168
|
-
set_variable_bounds index, var.lower_bound, var.upper_bound
|
169
|
-
|
170
|
-
@variables << var
|
171
180
|
end
|
172
181
|
end
|
173
182
|
end
|
data/lib/mipper/model.rb
CHANGED
@@ -1,8 +1,10 @@
|
|
1
1
|
module MIPPeR
|
2
|
+
# Stores a linear programming model for a particular solver
|
2
3
|
class Model
|
3
4
|
attr_reader :variables, :constraints
|
4
5
|
|
5
6
|
def initialize
|
7
|
+
@solution = nil
|
6
8
|
@sense = :min
|
7
9
|
|
8
10
|
@variables = []
|
@@ -26,33 +28,22 @@ module MIPPeR
|
|
26
28
|
|
27
29
|
# Update the model
|
28
30
|
def update
|
29
|
-
|
30
|
-
|
31
|
-
elsif @pending_variables.length > 0
|
32
|
-
add_variables @pending_variables
|
33
|
-
end
|
34
|
-
@pending_variables = []
|
35
|
-
|
36
|
-
if @pending_constraints.length == 1
|
37
|
-
add_constraint @pending_constraints.first
|
38
|
-
elsif @pending_constraints.length > 0
|
39
|
-
add_constraints @pending_constraints
|
40
|
-
end
|
41
|
-
@pending_constraints = []
|
31
|
+
update_variables
|
32
|
+
update_constraints
|
42
33
|
end
|
43
34
|
|
44
35
|
# Write the model to a file in CPLEX LP format
|
45
|
-
def write_lp(
|
36
|
+
def write_lp(_filename)
|
46
37
|
fail NotImplementedError
|
47
38
|
end
|
48
39
|
|
49
40
|
# Write the model to a file in MPS format
|
50
|
-
def write_mps(
|
41
|
+
def write_mps(_filename)
|
51
42
|
fail NotImplementedError
|
52
43
|
end
|
53
44
|
|
54
45
|
# Set the sense of the model
|
55
|
-
def sense=(
|
46
|
+
def sense=(_sense)
|
56
47
|
fail NotImplementedError
|
57
48
|
end
|
58
49
|
|
@@ -63,35 +54,72 @@ module MIPPeR
|
|
63
54
|
|
64
55
|
# Get the status of the model
|
65
56
|
def status
|
66
|
-
|
57
|
+
if @solution.nil?
|
58
|
+
:unknown
|
59
|
+
else
|
60
|
+
@solution.status
|
61
|
+
end
|
67
62
|
end
|
68
63
|
|
69
64
|
# Compute an irreducible inconsistent subsytem for the model
|
70
|
-
def
|
65
|
+
def compute_iis
|
71
66
|
fail NotImplementedError
|
72
67
|
end
|
73
68
|
|
74
|
-
#
|
69
|
+
# Get the value of the objective function from a previous solution
|
75
70
|
def objective_value
|
76
|
-
|
71
|
+
@solution.objective_value unless @solution.nil?
|
72
|
+
end
|
73
|
+
|
74
|
+
# Get the value of a variable from a previous solution
|
75
|
+
def variable_value(var)
|
76
|
+
@solution.variable_values[var.name] unless @solution.nil?
|
77
77
|
end
|
78
78
|
|
79
79
|
protected
|
80
80
|
|
81
|
+
# Shortcut to build a C array from a Ruby array
|
82
|
+
def build_pointer_array(array, type)
|
83
|
+
buffer = FFI::MemoryPointer.new type, array.length
|
84
|
+
buffer.send("write_array_of_#{type}".to_sym, array)
|
85
|
+
|
86
|
+
buffer
|
87
|
+
end
|
88
|
+
|
89
|
+
# Just add the variable as an array
|
81
90
|
def add_variable(var)
|
82
91
|
add_variables([var])
|
83
92
|
end
|
84
93
|
|
94
|
+
# Just add the constraint as an array
|
85
95
|
def add_constraint(constr)
|
86
96
|
add_constraints([constr])
|
87
97
|
end
|
88
98
|
|
89
|
-
def set_variable_bounds(
|
99
|
+
def set_variable_bounds(_var_index, _ub, _lb)
|
90
100
|
fail NotImplementedError
|
91
101
|
end
|
92
102
|
|
93
|
-
|
94
|
-
|
103
|
+
private
|
104
|
+
|
105
|
+
# Add any pending variables to the model
|
106
|
+
def update_variables
|
107
|
+
if @pending_variables.length == 1
|
108
|
+
add_variable @pending_variables.first
|
109
|
+
elsif @pending_variables.length > 0
|
110
|
+
add_variables @pending_variables
|
111
|
+
end
|
112
|
+
@pending_variables = []
|
113
|
+
end
|
114
|
+
|
115
|
+
# Add any pending constraints to the model
|
116
|
+
def update_constraints
|
117
|
+
if @pending_constraints.length == 1
|
118
|
+
add_constraint @pending_constraints.first
|
119
|
+
elsif @pending_constraints.length > 0
|
120
|
+
add_constraints @pending_constraints
|
121
|
+
end
|
122
|
+
@pending_constraints = []
|
95
123
|
end
|
96
124
|
end
|
97
125
|
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module MIPPeR
|
2
|
+
class Solution
|
3
|
+
attr_accessor :status, :objective_value, :variable_values
|
4
|
+
|
5
|
+
def initialize(status, objective_value, variable_values)
|
6
|
+
@status = status
|
7
|
+
@objective_value = objective_value
|
8
|
+
@variable_values = variable_values
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: mipper
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.6
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Michael Mior
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-01-
|
11
|
+
date: 2016-01-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: ffi
|
@@ -51,6 +51,7 @@ files:
|
|
51
51
|
- lib/mipper/lp_solve/ext/constants.rb
|
52
52
|
- lib/mipper/lp_solve/model.rb
|
53
53
|
- lib/mipper/model.rb
|
54
|
+
- lib/mipper/solution.rb
|
54
55
|
- lib/mipper/util.rb
|
55
56
|
- lib/mipper/variable.rb
|
56
57
|
homepage: https://github.com/michaelmior/mipper
|