opt-rb 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +3 -0
- data/LICENSE.txt +202 -0
- data/README.md +108 -0
- data/lib/opt/binary.rb +7 -0
- data/lib/opt/comparison.rb +15 -0
- data/lib/opt/constant.rb +26 -0
- data/lib/opt/expression.rb +75 -0
- data/lib/opt/integer.rb +4 -0
- data/lib/opt/problem.rb +277 -0
- data/lib/opt/product.rb +28 -0
- data/lib/opt/solvers/abstract_solver.rb +9 -0
- data/lib/opt/solvers/cbc_solver.rb +49 -0
- data/lib/opt/solvers/clp_solver.rb +46 -0
- data/lib/opt/solvers/glop_solver.rb +56 -0
- data/lib/opt/solvers/glpk_solver.rb +72 -0
- data/lib/opt/solvers/highs_solver.rb +92 -0
- data/lib/opt/solvers/osqp_solver.rb +73 -0
- data/lib/opt/solvers/scs_solver.rb +76 -0
- data/lib/opt/variable.rb +19 -0
- data/lib/opt/version.rb +3 -0
- data/lib/opt-rb.rb +1 -0
- data/lib/opt.rb +46 -0
- metadata +64 -0
data/lib/opt/problem.rb
ADDED
@@ -0,0 +1,277 @@
|
|
1
|
+
module Opt
|
2
|
+
class Problem
|
3
|
+
attr_reader :sense, :objective, :constraints
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@constraints = []
|
7
|
+
@indexed_constraints = []
|
8
|
+
end
|
9
|
+
|
10
|
+
def add(constraint)
|
11
|
+
raise ArgumentError, "Expected Comparison" unless constraint.is_a?(Comparison)
|
12
|
+
|
13
|
+
@constraints << constraint
|
14
|
+
@indexed_constraints << index_constraint(constraint)
|
15
|
+
end
|
16
|
+
|
17
|
+
def minimize(objective)
|
18
|
+
set_objective(:minimize, objective)
|
19
|
+
end
|
20
|
+
|
21
|
+
def maximize(objective)
|
22
|
+
set_objective(:maximize, objective)
|
23
|
+
end
|
24
|
+
|
25
|
+
def solve(solver: nil, verbose: false, time_limit: nil)
|
26
|
+
@indexed_objective ||= {}
|
27
|
+
|
28
|
+
vars = self.vars
|
29
|
+
raise Error, "No variables" if vars.empty?
|
30
|
+
type = vars.any? { |v| v.is_a?(Integer) } ? :mip : :lp
|
31
|
+
quadratic = @indexed_objective.any? { |k, _| k.is_a?(Array) }
|
32
|
+
|
33
|
+
if quadratic
|
34
|
+
raise Error, "Not supported" if type == :mip
|
35
|
+
type = :qp
|
36
|
+
end
|
37
|
+
|
38
|
+
raise Error, "No solvers found" if Opt.available_solvers.empty?
|
39
|
+
|
40
|
+
solver ||= (Opt.default_solvers[type] || Opt.available_solvers.find { |s| s.supports_type?(type) })
|
41
|
+
raise Error, "No solvers found for #{type}" unless solver
|
42
|
+
|
43
|
+
# TODO better error message
|
44
|
+
solver_cls = Opt.solvers.fetch(solver)
|
45
|
+
raise Error, "Solver does not support #{type}" unless solver_cls.supports_type?(type)
|
46
|
+
|
47
|
+
col_lower = []
|
48
|
+
col_upper = []
|
49
|
+
obj = []
|
50
|
+
|
51
|
+
@sense ||= :minimize
|
52
|
+
|
53
|
+
vars.each do |var|
|
54
|
+
col_lower << (var.bounds.begin || -Float::INFINITY)
|
55
|
+
upper = var.bounds.end
|
56
|
+
if upper && var.bounds.exclude_end?
|
57
|
+
if var.is_a?(Integer)
|
58
|
+
upper -= 1
|
59
|
+
else
|
60
|
+
upper -= Float::EPSILON
|
61
|
+
end
|
62
|
+
end
|
63
|
+
col_upper << (upper || Float::INFINITY)
|
64
|
+
obj << (@indexed_objective[var] || 0)
|
65
|
+
end
|
66
|
+
|
67
|
+
row_lower = []
|
68
|
+
row_upper = []
|
69
|
+
constraints_by_var = @indexed_constraints
|
70
|
+
constraints_by_var.each do |left, op, right|
|
71
|
+
case op
|
72
|
+
when :>=
|
73
|
+
row_lower << right
|
74
|
+
row_upper << Float::INFINITY
|
75
|
+
when :<=
|
76
|
+
row_lower << -Float::INFINITY
|
77
|
+
row_upper << right
|
78
|
+
else # :==
|
79
|
+
row_lower << right
|
80
|
+
row_upper << right
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
start = []
|
85
|
+
index = []
|
86
|
+
value = []
|
87
|
+
|
88
|
+
vars.each do |var|
|
89
|
+
start << index.size
|
90
|
+
constraints_by_var.map(&:first).each_with_index do |ic, i|
|
91
|
+
if ic[var]
|
92
|
+
index << i
|
93
|
+
value << ic[var]
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
start << index.size
|
98
|
+
|
99
|
+
if type == :qp
|
100
|
+
@indexed_objective.select { |k, _| k.is_a?(Array) }.each do |k, v|
|
101
|
+
@indexed_objective[k.reverse] = v
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
res = solver_cls.new.solve(
|
106
|
+
sense: @sense, start: start, index: index, value: value,
|
107
|
+
col_lower: col_lower, col_upper: col_upper, obj: obj,
|
108
|
+
row_lower: row_lower, row_upper: row_upper,
|
109
|
+
constraints_by_var: constraints_by_var, vars: vars,
|
110
|
+
offset: @indexed_objective[nil] || 0, verbose: verbose,
|
111
|
+
type: type, time_limit: time_limit, indexed_objective: @indexed_objective
|
112
|
+
)
|
113
|
+
|
114
|
+
if res[:status] == :optimal
|
115
|
+
vars.zip(res.delete(:x)) do |a, b|
|
116
|
+
a.value =
|
117
|
+
case a
|
118
|
+
when Binary
|
119
|
+
b.round != 0
|
120
|
+
when Integer
|
121
|
+
b.round
|
122
|
+
else
|
123
|
+
b
|
124
|
+
end
|
125
|
+
end
|
126
|
+
else
|
127
|
+
res.delete(:objective)
|
128
|
+
res.delete(:x)
|
129
|
+
end
|
130
|
+
res
|
131
|
+
end
|
132
|
+
|
133
|
+
def inspect
|
134
|
+
str = String.new("")
|
135
|
+
str << "#{@sense}\n #{@objective.inspect}\n"
|
136
|
+
str << "subject to\n"
|
137
|
+
@constraints.each do |constraint|
|
138
|
+
str << " #{constraint.inspect}\n"
|
139
|
+
end
|
140
|
+
str << "vars\n"
|
141
|
+
vars.each do |var|
|
142
|
+
bounds = var.bounds
|
143
|
+
end_op = bounds.exclude_end? ? "<" : "<="
|
144
|
+
var_str =
|
145
|
+
if bounds.begin && bounds.end
|
146
|
+
"#{bounds.begin} <= #{var.name} #{end_op} #{bounds.end}"
|
147
|
+
elsif var.bounds.begin
|
148
|
+
"#{var.name} >= #{bounds.begin}"
|
149
|
+
else
|
150
|
+
"#{var.name} #{end_op} #{bounds.end}"
|
151
|
+
end
|
152
|
+
str << " #{var_str}\n"
|
153
|
+
end
|
154
|
+
str
|
155
|
+
end
|
156
|
+
|
157
|
+
private
|
158
|
+
|
159
|
+
def vars
|
160
|
+
vars = []
|
161
|
+
vars.concat(@objective.vars) if @objective
|
162
|
+
@constraints.each do |constraint|
|
163
|
+
vars.concat(constraint.left.vars)
|
164
|
+
vars.concat(constraint.right.vars)
|
165
|
+
end
|
166
|
+
vars.uniq
|
167
|
+
end
|
168
|
+
|
169
|
+
def set_objective(sense, objective)
|
170
|
+
raise Error, "Objective already set" if @sense
|
171
|
+
|
172
|
+
@sense = sense
|
173
|
+
@objective = Expression.to_expression(objective)
|
174
|
+
@indexed_objective = index_expression(@objective)
|
175
|
+
end
|
176
|
+
|
177
|
+
def index_constraint(constraint)
|
178
|
+
left = index_expression(constraint.left, check_linear: true)
|
179
|
+
right = index_expression(constraint.right, check_linear: true)
|
180
|
+
|
181
|
+
const = right.delete(nil).to_f - left.delete(nil).to_f
|
182
|
+
right.each do |k, v|
|
183
|
+
left[k] -= v
|
184
|
+
end
|
185
|
+
|
186
|
+
[left, constraint.op, const]
|
187
|
+
end
|
188
|
+
|
189
|
+
def index_expression(expression, check_linear: false)
|
190
|
+
vars = Hash.new(0)
|
191
|
+
case expression
|
192
|
+
when Numeric
|
193
|
+
vars[nil] += expression
|
194
|
+
when Constant
|
195
|
+
vars[nil] += expression.value
|
196
|
+
when Variable
|
197
|
+
vars[expression] += 1
|
198
|
+
when Product
|
199
|
+
if check_linear && expression.left.vars.any? && expression.right.vars.any?
|
200
|
+
raise ArgumentError, "Nonlinear"
|
201
|
+
end
|
202
|
+
vars = index_product(expression.left, expression.right)
|
203
|
+
else # Expression
|
204
|
+
expression.parts.each do |part|
|
205
|
+
index_expression(part, check_linear: check_linear).each do |k, v|
|
206
|
+
vars[k] += v
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
vars
|
211
|
+
end
|
212
|
+
|
213
|
+
def index_product(left, right)
|
214
|
+
# normalize
|
215
|
+
types = [Constant, Variable, Product, Expression]
|
216
|
+
if types.index { |t| left.is_a?(t) } > types.index { |t| right.is_a?(t) }
|
217
|
+
left, right = right, left
|
218
|
+
end
|
219
|
+
|
220
|
+
vars = Hash.new(0)
|
221
|
+
case left
|
222
|
+
when Constant
|
223
|
+
vars = index_expression(right)
|
224
|
+
vars.transform_values! { |v| v * left.value }
|
225
|
+
when Variable
|
226
|
+
case right
|
227
|
+
when Variable
|
228
|
+
vars[quad_key(left, right)] = 1
|
229
|
+
when Product
|
230
|
+
index_expression(right).each do |k, v|
|
231
|
+
case k
|
232
|
+
when Array
|
233
|
+
raise Error, "Non-quadratic"
|
234
|
+
when Variable
|
235
|
+
vars[quad_key(left, k)] = v
|
236
|
+
else # nil
|
237
|
+
raise "Bug?"
|
238
|
+
end
|
239
|
+
end
|
240
|
+
else
|
241
|
+
right.parts.each do |part|
|
242
|
+
index_product(left, part).each do |k, v|
|
243
|
+
vars[k] += v
|
244
|
+
end
|
245
|
+
end
|
246
|
+
end
|
247
|
+
when Product
|
248
|
+
index_expression(left).each do |lk, lv|
|
249
|
+
index_expression(right).each do |rk, rv|
|
250
|
+
if lk.is_a?(Variable) && rk.is_a?(Variable)
|
251
|
+
vars[quad_key(lk, rk)] = lv * rv
|
252
|
+
else
|
253
|
+
raise "todo"
|
254
|
+
end
|
255
|
+
end
|
256
|
+
end
|
257
|
+
else # Expression
|
258
|
+
left.parts.each do |lp|
|
259
|
+
right.parts.each do |rp|
|
260
|
+
index_product(lp, rp).each do |k, v|
|
261
|
+
vars[k] += v
|
262
|
+
end
|
263
|
+
end
|
264
|
+
end
|
265
|
+
end
|
266
|
+
vars
|
267
|
+
end
|
268
|
+
|
269
|
+
def quad_key(left, right)
|
270
|
+
if left.object_id <= right.object_id
|
271
|
+
[left, right]
|
272
|
+
else
|
273
|
+
[right, left]
|
274
|
+
end
|
275
|
+
end
|
276
|
+
end
|
277
|
+
end
|
data/lib/opt/product.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
module Opt
|
2
|
+
class Product < Expression
|
3
|
+
attr_reader :left, :right
|
4
|
+
|
5
|
+
def initialize(left, right)
|
6
|
+
@left = left
|
7
|
+
@right = right
|
8
|
+
end
|
9
|
+
|
10
|
+
def inspect
|
11
|
+
"#{inspect_part(@left)} * #{inspect_part(@right)}"
|
12
|
+
end
|
13
|
+
|
14
|
+
def vars
|
15
|
+
@vars ||= (@left.vars + @right.vars).uniq
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def inspect_part(var)
|
21
|
+
if var.instance_of?(Expression)
|
22
|
+
"(#{var.inspect})"
|
23
|
+
else
|
24
|
+
var.inspect
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Opt
|
2
|
+
module Solvers
|
3
|
+
class CbcSolver < AbstractSolver
|
4
|
+
def solve(sense:, start:, index:, value:, col_lower:, col_upper:, obj:, row_lower:, row_upper:, offset:, verbose:, time_limit:, vars:, **)
|
5
|
+
model =
|
6
|
+
Cbc.load_problem(
|
7
|
+
sense: sense,
|
8
|
+
start: start,
|
9
|
+
index: index,
|
10
|
+
value: value,
|
11
|
+
col_lower: col_lower,
|
12
|
+
col_upper: col_upper,
|
13
|
+
obj: obj,
|
14
|
+
row_lower: row_lower,
|
15
|
+
row_upper: row_upper,
|
16
|
+
col_type: vars.map { |v| v.is_a?(Integer) ? :integer : :continuous }
|
17
|
+
)
|
18
|
+
|
19
|
+
# only pass options if set to support Cbc < 2.10.0
|
20
|
+
options = {}
|
21
|
+
options[:log_level] = 1 if verbose
|
22
|
+
options[:time_limit] = time_limit if time_limit
|
23
|
+
res = model.solve(**options)
|
24
|
+
|
25
|
+
status =
|
26
|
+
case res[:status]
|
27
|
+
when :primal_infeasible
|
28
|
+
:infeasible
|
29
|
+
else
|
30
|
+
res[:status]
|
31
|
+
end
|
32
|
+
|
33
|
+
{
|
34
|
+
status: status,
|
35
|
+
objective: res[:objective] + offset,
|
36
|
+
x: res[:primal_col]
|
37
|
+
}
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.available?
|
41
|
+
defined?(Cbc)
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.supported_types
|
45
|
+
[:lp, :mip]
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Opt
|
2
|
+
module Solvers
|
3
|
+
class ClpSolver < AbstractSolver
|
4
|
+
def solve(sense:, start:, index:, value:, col_lower:, col_upper:, obj:, row_lower:, row_upper:, offset:, verbose:, time_limit:, **)
|
5
|
+
model =
|
6
|
+
Clp.load_problem(
|
7
|
+
sense: sense,
|
8
|
+
start: start,
|
9
|
+
index: index,
|
10
|
+
value: value,
|
11
|
+
col_lower: col_lower,
|
12
|
+
col_upper: col_upper,
|
13
|
+
obj: obj,
|
14
|
+
row_lower: row_lower,
|
15
|
+
row_upper: row_upper,
|
16
|
+
offset: -offset
|
17
|
+
)
|
18
|
+
res = model.solve(log_level: verbose ? 4 : nil, time_limit: time_limit)
|
19
|
+
|
20
|
+
status =
|
21
|
+
case res[:status]
|
22
|
+
when :primal_infeasible
|
23
|
+
:infeasible
|
24
|
+
when :dual_infeasible
|
25
|
+
:unbounded
|
26
|
+
else
|
27
|
+
res[:status]
|
28
|
+
end
|
29
|
+
|
30
|
+
{
|
31
|
+
status: status,
|
32
|
+
objective: res[:objective],
|
33
|
+
x: res[:primal_col]
|
34
|
+
}
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.available?
|
38
|
+
defined?(Clp)
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.supported_types
|
42
|
+
[:lp]
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module Opt
|
2
|
+
module Solvers
|
3
|
+
class GlopSolver < AbstractSolver
|
4
|
+
def solve(sense:, col_lower:, col_upper:, obj:, offset:, verbose:, time_limit:, constraints_by_var:, vars:, **)
|
5
|
+
solver = ORTools::Solver.new("GLOP")
|
6
|
+
|
7
|
+
# create vars
|
8
|
+
vars2 =
|
9
|
+
vars.map.with_index do |v, i|
|
10
|
+
solver.num_var(col_lower[i], col_upper[i], v.name)
|
11
|
+
end
|
12
|
+
|
13
|
+
var_index = vars.map.with_index.to_h
|
14
|
+
|
15
|
+
# add constraints
|
16
|
+
constraints_by_var.each do |left, op, right|
|
17
|
+
expr = left.sum { |k, v| vars2[var_index[k]] * v }
|
18
|
+
case op
|
19
|
+
when :<=
|
20
|
+
solver.add(expr <= right)
|
21
|
+
when :>=
|
22
|
+
solver.add(expr >= right)
|
23
|
+
else
|
24
|
+
solver.add(expr == right)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# add objective
|
29
|
+
objective = vars2.zip(obj).sum { |v, o| v * o }
|
30
|
+
|
31
|
+
if sense == :maximize
|
32
|
+
solver.maximize(objective)
|
33
|
+
else
|
34
|
+
solver.minimize(objective)
|
35
|
+
end
|
36
|
+
|
37
|
+
solver.time_limit = time_limit if time_limit
|
38
|
+
status = solver.solve
|
39
|
+
|
40
|
+
{
|
41
|
+
status: status,
|
42
|
+
objective: solver.objective.value + offset,
|
43
|
+
x: vars2.map(&:solution_value)
|
44
|
+
}
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.available?
|
48
|
+
defined?(ORTools)
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.supported_types
|
52
|
+
[:lp]
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module Opt
|
2
|
+
module Solvers
|
3
|
+
class GlpkSolver < AbstractSolver
|
4
|
+
def solve(sense:, start:, index:, value:, col_lower:, col_upper:, obj:, row_lower:, row_upper:, offset:, verbose:, time_limit:, vars:, **)
|
5
|
+
col_kind =
|
6
|
+
vars.map do |v|
|
7
|
+
case v
|
8
|
+
when Binary
|
9
|
+
:binary
|
10
|
+
when Integer
|
11
|
+
:integer
|
12
|
+
else
|
13
|
+
:continuous
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
mat_ia = []
|
18
|
+
mat_ja = []
|
19
|
+
mat_ar = []
|
20
|
+
start.each_with_index do |s, j|
|
21
|
+
rng = s...start[j + 1]
|
22
|
+
index[rng].zip(value[rng]) do |i, v|
|
23
|
+
mat_ia << i + 1
|
24
|
+
mat_ja << j + 1
|
25
|
+
mat_ar << v
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
model =
|
30
|
+
Glpk.load_problem(
|
31
|
+
obj_dir: sense,
|
32
|
+
obj_coef: obj,
|
33
|
+
mat_ia: mat_ia,
|
34
|
+
mat_ja: mat_ja,
|
35
|
+
mat_ar: mat_ar,
|
36
|
+
col_kind: col_kind,
|
37
|
+
col_lower: col_lower,
|
38
|
+
col_upper: col_upper,
|
39
|
+
row_lower: row_lower,
|
40
|
+
row_upper: row_upper,
|
41
|
+
)
|
42
|
+
res = model.solve(message_level: verbose ? 3 : 0, time_limit: time_limit)
|
43
|
+
|
44
|
+
status =
|
45
|
+
case res[:status]
|
46
|
+
when :no_feasible, :no_primal_feasible
|
47
|
+
:infeasible
|
48
|
+
else
|
49
|
+
res[:status]
|
50
|
+
end
|
51
|
+
|
52
|
+
if status == :optimal && !res[:obj_val].finite?
|
53
|
+
status = :unbounded
|
54
|
+
end
|
55
|
+
|
56
|
+
{
|
57
|
+
status: status,
|
58
|
+
objective: res[:obj_val] + offset,
|
59
|
+
x: res[:col_primal]
|
60
|
+
}
|
61
|
+
end
|
62
|
+
|
63
|
+
def self.available?
|
64
|
+
defined?(Glpk)
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.supported_types
|
68
|
+
[:lp, :mip]
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
module Opt
|
2
|
+
module Solvers
|
3
|
+
class HighsSolver < AbstractSolver
|
4
|
+
def solve(sense:, start:, index:, value:, col_lower:, col_upper:, obj:, row_lower:, row_upper:, vars:, offset:, verbose:, type:, time_limit:, indexed_objective:, **)
|
5
|
+
start.pop
|
6
|
+
|
7
|
+
model =
|
8
|
+
case type
|
9
|
+
when :mip
|
10
|
+
Highs.mip(
|
11
|
+
sense: sense,
|
12
|
+
col_cost: obj,
|
13
|
+
col_lower: col_lower,
|
14
|
+
col_upper: col_upper,
|
15
|
+
row_lower: row_lower,
|
16
|
+
row_upper: row_upper,
|
17
|
+
a_format: :colwise,
|
18
|
+
a_start: start,
|
19
|
+
a_index: index,
|
20
|
+
a_value: value,
|
21
|
+
offset: offset,
|
22
|
+
integrality: vars.map { |v| v.is_a?(Integer) ? 1 : 0 }
|
23
|
+
)
|
24
|
+
when :qp
|
25
|
+
q_start = []
|
26
|
+
q_index = []
|
27
|
+
q_value = []
|
28
|
+
|
29
|
+
vars.each_with_index do |v2, j|
|
30
|
+
q_start << q_index.size
|
31
|
+
vars.each_with_index do |v1, i|
|
32
|
+
v = indexed_objective[[v1, v2]]
|
33
|
+
if v != 0
|
34
|
+
q_index << i
|
35
|
+
# multiply values by 2 since minimizes 1/2
|
36
|
+
q_value << (v1.equal?(v2) ? v * 2 : v)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
Highs.qp(
|
42
|
+
sense: sense,
|
43
|
+
col_cost: obj,
|
44
|
+
col_lower: col_lower,
|
45
|
+
col_upper: col_upper,
|
46
|
+
row_lower: row_lower,
|
47
|
+
row_upper: row_upper,
|
48
|
+
a_format: :colwise,
|
49
|
+
a_start: start,
|
50
|
+
a_index: index,
|
51
|
+
a_value: value,
|
52
|
+
offset: offset,
|
53
|
+
q_format: :colwise,
|
54
|
+
q_start: q_start,
|
55
|
+
q_index: q_index,
|
56
|
+
q_value: q_value
|
57
|
+
)
|
58
|
+
else
|
59
|
+
Highs.lp(
|
60
|
+
sense: sense,
|
61
|
+
col_cost: obj,
|
62
|
+
col_lower: col_lower,
|
63
|
+
col_upper: col_upper,
|
64
|
+
row_lower: row_lower,
|
65
|
+
row_upper: row_upper,
|
66
|
+
a_format: :colwise,
|
67
|
+
a_start: start,
|
68
|
+
a_index: index,
|
69
|
+
a_value: value,
|
70
|
+
offset: offset
|
71
|
+
)
|
72
|
+
end
|
73
|
+
|
74
|
+
res = model.solve(verbose: verbose, time_limit: time_limit)
|
75
|
+
|
76
|
+
{
|
77
|
+
status: res[:status],
|
78
|
+
objective: res[:obj_value],
|
79
|
+
x: res[:col_value]
|
80
|
+
}
|
81
|
+
end
|
82
|
+
|
83
|
+
def self.available?
|
84
|
+
defined?(Highs)
|
85
|
+
end
|
86
|
+
|
87
|
+
def self.supported_types
|
88
|
+
[:lp, :qp, :mip]
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module Opt
|
2
|
+
module Solvers
|
3
|
+
class OsqpSolver < AbstractSolver
|
4
|
+
def solve(sense:, col_lower:, col_upper:, obj:, row_lower:, row_upper:, constraints_by_var:, vars:, offset:, verbose:, time_limit:, type:, indexed_objective:, **)
|
5
|
+
obj = obj.map { |v| -v } if sense == :maximize
|
6
|
+
|
7
|
+
a =
|
8
|
+
constraints_by_var.map(&:first).map do |ic|
|
9
|
+
vars.map do |var|
|
10
|
+
ic[var] || 0
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# add variable constraints
|
15
|
+
vars.each_with_index do |v, i|
|
16
|
+
row = [0] * vars.size
|
17
|
+
row[i] = 1
|
18
|
+
a << row
|
19
|
+
row_lower << col_lower[i]
|
20
|
+
row_upper << col_upper[i]
|
21
|
+
end
|
22
|
+
|
23
|
+
p = OSQP::Matrix.new(a.first.size, a.first.size)
|
24
|
+
if type == :qp
|
25
|
+
vars.map.with_index do |v1, i|
|
26
|
+
vars.map.with_index do |v2, j|
|
27
|
+
if i > j
|
28
|
+
0
|
29
|
+
else
|
30
|
+
v = indexed_objective[[v1, v2]]
|
31
|
+
v = (v1.equal?(v2) ? v * 2 : v)
|
32
|
+
v *= -1 if sense == :maximize
|
33
|
+
p[i, j] = v
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
solver = OSQP::Solver.new
|
40
|
+
res = solver.solve(p, obj, a, row_lower, row_upper, verbose: verbose, time_limit: time_limit, polish: true)
|
41
|
+
objective = res[:obj_val]
|
42
|
+
objective *= -1 if sense == :maximize
|
43
|
+
objective += offset
|
44
|
+
|
45
|
+
status =
|
46
|
+
case res[:status]
|
47
|
+
when "solved"
|
48
|
+
:optimal
|
49
|
+
when "primal infeasible"
|
50
|
+
:infeasible
|
51
|
+
when "dual infeasible"
|
52
|
+
:unbounded
|
53
|
+
else
|
54
|
+
res[:status]
|
55
|
+
end
|
56
|
+
|
57
|
+
{
|
58
|
+
status: status,
|
59
|
+
objective: objective,
|
60
|
+
x: res[:x]
|
61
|
+
}
|
62
|
+
end
|
63
|
+
|
64
|
+
def self.available?
|
65
|
+
defined?(OSQP)
|
66
|
+
end
|
67
|
+
|
68
|
+
def self.supported_types
|
69
|
+
[:lp, :qp]
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|