opt-rb 0.1.0
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 +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
|