opt-rb 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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,9 @@
1
+ module Opt
2
+ module Solvers
3
+ class AbstractSolver
4
+ def self.supports_type?(type)
5
+ supported_types.include?(type)
6
+ end
7
+ end
8
+ end
9
+ 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