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