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 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