mipper 0.0.5 → 0.0.6

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ad701722ba6d2ee732f074d86dba521d8f725d46
4
- data.tar.gz: e00ab9b7c632eb8f8eba0164c0c8c0ed68d5c5bb
3
+ metadata.gz: 99f7bd81a866c8127ba5527dd8eb2107fc3c3ee1
4
+ data.tar.gz: 6b94eefa7fb9ab14b6400963f7975fca4b40b538
5
5
  SHA512:
6
- metadata.gz: 09bb2783869873c9d04a20592b1e3d69856dd09fc032546d85bc8405f8931b953eaba2152e3e2c726fe387727bcc33428055c64399f187851a0aaa6964ed7b30
7
- data.tar.gz: a7ecfb338d2bc14bf44586217d0cd059c212ed1ee0950b1c17d40cb4dbda6d7d1cf4756087094e99ec167aed8984a02e259de5d1f3b6016029b18913a8e3c671
6
+ metadata.gz: f7c81f9e6743229544bd55ba20796c39a80aa35ba63cea211722ab2d56056dd9cc86314d685cd0ef746daca4ebe03da8b9b92da3925bc76e5dc21ee1f30f00b6
7
+ data.tar.gz: 1c43605bf6efdb20bd227f569b385c89bc25567416e5511d28125d81ac081884c93d754fe4e6667631b6be31440c836384b808e72503203980628dff1140e700
data/lib/mipper.rb CHANGED
@@ -3,6 +3,7 @@ require_relative 'mipper/util'
3
3
  require_relative 'mipper/constraint'
4
4
  require_relative 'mipper/expression'
5
5
  require_relative 'mipper/model'
6
+ require_relative 'mipper/solution'
6
7
  require_relative 'mipper/variable'
7
8
 
8
9
  require_relative 'mipper/cbc'
@@ -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 = FFI::AutoPointer.new Cbc.Cbc_newModel,
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
- Cbc.Cbc_writeMps @ptr, File.join(File.dirname(filename),
22
- File.basename(filename, '.mps'))
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
- update
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
- # Check and store the model status
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
- # The value of the objective function
61
- def objective_value
62
- Cbc.Cbc_getObjValue @ptr
60
+ @ptr = new_model
61
+ reset_model
63
62
  end
64
63
 
65
- # Get the value of a variable from the model
66
- def variable_value(var)
67
- dblptr = Cbc.Cbc_getColSolution @ptr
68
- dblptr.read_array_of_double(@variables.length)[var.index]
69
- end
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
- # Store the index which will be used for each constraint
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
- start_buffer = FFI::MemoryPointer.new :int, start.length
114
- start_buffer.write_array_of_int start
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
- @variables.each_with_index do |var, i|
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
- private
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
- case constr.sense
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 = constr.rhs
210
+ lb = ub = rhs
150
211
  when :>=
151
- lb = constr.rhs
212
+ lb = rhs
152
213
  ub = Float::INFINITY
153
214
  when :<=
154
215
  lb = -Float::INFINITY
155
- ub = constr.rhs
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
- case var.type
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, var.index
241
+ Cbc.Cbc_setContinuous @ptr, index
176
242
  when :integer, :binary
177
- Cbc.Cbc_setInteger @ptr, var.index
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
@@ -13,5 +13,9 @@ module MIPPeR
13
13
  var.constraints << self
14
14
  end
15
15
  end
16
+
17
+ def inspect
18
+ "#{@expression.inspect} #{sense.to_s} #{rhs}"
19
+ end
16
20
  end
17
21
  end
@@ -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
- :br_tech, :int,
6
- :bt_tech, :int,
7
- :tol_int, :double,
8
- :tol_obj, :double,
9
- :tm_lim, :int,
10
- :out_frq, :int,
11
- :out_dly, :int,
12
- :cb_func, callback([:pointer, :pointer], :void),
13
- :cb_info, :pointer,
14
- :cb_size, :int,
15
- :pp_tech, :int,
16
- :mip_gap, :double,
17
- :mir_cuts, :int,
18
- :gmi_cuts, :int,
19
- :cov_cuts, :int,
20
- :clq_cuts, :int,
21
- :presolve, :int,
22
- :binarize, :int,
23
- :fp_heur, :int,
24
- :alien, :int,
25
- :foo_bar, [:double, 29]
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
@@ -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
- @status = GLPK.glp_intopt @ptr, iocp
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
- # Get the value of a variable from the model
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 = FFI::MemoryPointer.new :pointer, @ia.length
120
- ia_buffer.write_array_of_int @ia
121
- ja_buffer = FFI::MemoryPointer.new :pointer, @ja.length
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
@@ -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 status
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
- Gurobi::GRB_UNBOUNDED
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 objective_value
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 = FFI::MemoryPointer.new :double, vars.length
105
- objective_buffer.write_array_of_double vars.map(&:coefficient)
106
-
107
- lb_buffer = FFI::MemoryPointer.new :double, vars.length
108
- lb_buffer.write_array_of_double vars.map(&:lower_bound)
109
-
110
- ub_buffer = FFI::MemoryPointer.new :double, vars.length
111
- ub_buffer.write_array_of_double vars.map(&:upper_bound)
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
- cval_buffer = FFI::MemoryPointer.new :pointer, cval.length
160
- cval_buffer.write_array_of_double cval
161
-
162
- sense_buffer = FFI::MemoryPointer.new :pointer, constrs.length
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
- names = array_to_pointers_to_names constrs
171
- names_buffer = FFI::MemoryPointer.new :pointer, constrs.length
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 = FFI::MemoryPointer.new :int, terms.length
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 = FFI::MemoryPointer.new :double, 1
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
- @status = LPSolve.solve @ptr
34
- end
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
- # Initialize arrays used to hold the coefficients for each variable
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
- case var.type
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
- if @pending_variables.length == 1
30
- add_variable @pending_variables.first
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(filename)
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(filename)
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=(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
- :unknown
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 compute_IIS
65
+ def compute_iis
71
66
  fail NotImplementedError
72
67
  end
73
68
 
74
- # The value of the objective function
69
+ # Get the value of the objective function from a previous solution
75
70
  def objective_value
76
- fail NotImplementedError
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(var_index, ub, lb)
99
+ def set_variable_bounds(_var_index, _ub, _lb)
90
100
  fail NotImplementedError
91
101
  end
92
102
 
93
- def variable_value(var)
94
- fail NotImplementedError
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.5
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-15 00:00:00.000000000 Z
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