lpsolver 0.2.0 → 0.3.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 +4 -4
- data/README.md +72 -13
- data/ext/lpsolver/Makefile +8 -12
- data/ext/lpsolver/ext.o +0 -0
- data/ext/lpsolver/extconf.rb +11 -8
- data/ext/lpsolver/native.so +0 -0
- data/lib/lpsolver/drivers/README.md +91 -0
- data/lib/lpsolver/drivers/cli_driver.rb +123 -0
- data/lib/lpsolver/drivers/native_driver.rb +150 -0
- data/lib/lpsolver/drivers.rb +9 -0
- data/lib/lpsolver/lp_generator.rb +200 -0
- data/lib/lpsolver/model.rb +105 -406
- data/lib/lpsolver/native.so +0 -0
- data/lib/lpsolver/solution.rb +21 -1
- data/lib/lpsolver/version.rb +1 -1
- data/lib/lpsolver.rb +10 -4
- metadata +6 -2
- data/lib/lpsolver/native_model.rb +0 -261
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lpsolver
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- David Siaw
|
|
@@ -1194,12 +1194,16 @@ files:
|
|
|
1194
1194
|
- ext/lpsolver/native.so
|
|
1195
1195
|
- lib/lpsolver.rb
|
|
1196
1196
|
- lib/lpsolver/constraint_spec.rb
|
|
1197
|
+
- lib/lpsolver/drivers.rb
|
|
1198
|
+
- lib/lpsolver/drivers/README.md
|
|
1199
|
+
- lib/lpsolver/drivers/cli_driver.rb
|
|
1200
|
+
- lib/lpsolver/drivers/native_driver.rb
|
|
1197
1201
|
- lib/lpsolver/exception.rb
|
|
1198
1202
|
- lib/lpsolver/highs
|
|
1199
1203
|
- lib/lpsolver/linear_expression.rb
|
|
1204
|
+
- lib/lpsolver/lp_generator.rb
|
|
1200
1205
|
- lib/lpsolver/model.rb
|
|
1201
1206
|
- lib/lpsolver/native.so
|
|
1202
|
-
- lib/lpsolver/native_model.rb
|
|
1203
1207
|
- lib/lpsolver/quadratic_expression.rb
|
|
1204
1208
|
- lib/lpsolver/solution.rb
|
|
1205
1209
|
- lib/lpsolver/variable.rb
|
|
@@ -1,261 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module LpSolver
|
|
4
|
-
# A native (C extension) backed model solver for HiGHS.
|
|
5
|
-
#
|
|
6
|
-
# This class uses the native C extension to call HiGHS directly,
|
|
7
|
-
# bypassing the LP file serialization overhead of the CLI approach.
|
|
8
|
-
# It requires the native extension to be compiled and linked against
|
|
9
|
-
# the HiGHS library.
|
|
10
|
-
#
|
|
11
|
-
# @note This is a prototype. The native extension must be compiled
|
|
12
|
-
# separately using `rake compile` or `ruby ext/lpsolver/extconf.rb && make`.
|
|
13
|
-
#
|
|
14
|
-
# @example Basic usage
|
|
15
|
-
# require 'lpsolver/native_model'
|
|
16
|
-
#
|
|
17
|
-
# model = LpSolver::NativeModel.new
|
|
18
|
-
# x = model.add_variable(:x, lb: 0)
|
|
19
|
-
# y = model.add_variable(:y, lb: 0)
|
|
20
|
-
#
|
|
21
|
-
# model.add_constraint(:c1, (x + y) >= 4)
|
|
22
|
-
# solution = model.minimize!(x * 3 + y * 5)
|
|
23
|
-
#
|
|
24
|
-
# puts solution[:x] # => 4.0
|
|
25
|
-
#
|
|
26
|
-
# @example With MIP
|
|
27
|
-
# model = LpSolver::NativeModel.new
|
|
28
|
-
# x = model.add_variable(:x, lb: 0, integer: true)
|
|
29
|
-
# y = model.add_variable(:y, lb: 0, integer: true)
|
|
30
|
-
#
|
|
31
|
-
# model.add_constraint(:c1, (x + y) == 10)
|
|
32
|
-
# solution = model.minimize!(x * 2 + y * 3)
|
|
33
|
-
#
|
|
34
|
-
# puts solution[:x] # => 10.0
|
|
35
|
-
class NativeModel
|
|
36
|
-
# @return [String] A descriptive name for this model.
|
|
37
|
-
attr_reader :name
|
|
38
|
-
|
|
39
|
-
# @return [Hash{Symbol => Variable}] All variables in the model.
|
|
40
|
-
attr_reader :variables
|
|
41
|
-
|
|
42
|
-
# @return [Array<Hash>] Constraint data for each constraint.
|
|
43
|
-
attr_reader :constraints
|
|
44
|
-
|
|
45
|
-
# @return [Symbol] Optimization sense (:minimize or :maximize).
|
|
46
|
-
attr_reader :sense
|
|
47
|
-
|
|
48
|
-
# @return [Hash{Integer => Float}] Linear objective coefficients.
|
|
49
|
-
attr_reader :objective
|
|
50
|
-
|
|
51
|
-
# @return [Array<[Integer, Integer, Float]>] Quadratic terms (for QP).
|
|
52
|
-
attr_reader :quadratic_terms
|
|
53
|
-
|
|
54
|
-
# Creates a new empty model.
|
|
55
|
-
#
|
|
56
|
-
# @param name [String] An optional name for this model.
|
|
57
|
-
def initialize(name = nil)
|
|
58
|
-
@name = name || 'untitled'
|
|
59
|
-
@variables = {}
|
|
60
|
-
@constraints = []
|
|
61
|
-
@var_counter = 0
|
|
62
|
-
@sense = :minimize
|
|
63
|
-
@objective = {}
|
|
64
|
-
@quadratic_terms = []
|
|
65
|
-
@var_types = {}
|
|
66
|
-
@var_bounds = {}
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
# Adds a variable to the model.
|
|
70
|
-
#
|
|
71
|
-
# @param name [Symbol, String] The variable name.
|
|
72
|
-
# @param lb [Float] Lower bound (default: 0.0).
|
|
73
|
-
# @param ub [Float] Upper bound (default: Float::INFINITY).
|
|
74
|
-
# @param integer [Boolean] Whether the variable must be integer (default: false).
|
|
75
|
-
# @return [Variable] The variable object.
|
|
76
|
-
def add_variable(name, lb: 0.0, ub: Float::INFINITY, integer: false)
|
|
77
|
-
name = name.to_sym
|
|
78
|
-
idx = @var_counter
|
|
79
|
-
var = Variable.new(idx, name)
|
|
80
|
-
@variables[name] = var
|
|
81
|
-
@var_types[name] = integer ? 1 : 0
|
|
82
|
-
@var_bounds[name] = [lb, ub]
|
|
83
|
-
@var_counter += 1
|
|
84
|
-
var
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
# Adds a constraint to the model.
|
|
88
|
-
#
|
|
89
|
-
# @param name [Symbol, String] The constraint name.
|
|
90
|
-
# @param expr [ConstraintSpec] The constraint specification.
|
|
91
|
-
# @return [Symbol] The constraint name.
|
|
92
|
-
def add_constraint(name, expr)
|
|
93
|
-
name = name.to_sym
|
|
94
|
-
lb_val, ub_val = expr.bounds
|
|
95
|
-
data_expr = expr.expr
|
|
96
|
-
|
|
97
|
-
@constraints << {
|
|
98
|
-
name: name,
|
|
99
|
-
lb: lb_val,
|
|
100
|
-
ub: ub_val,
|
|
101
|
-
expr: data_expr
|
|
102
|
-
}
|
|
103
|
-
name
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
# Sets the optimization sense to minimization.
|
|
107
|
-
#
|
|
108
|
-
# @return [void]
|
|
109
|
-
def minimize
|
|
110
|
-
@sense = :minimize
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
# Sets the optimization sense to maximization.
|
|
114
|
-
#
|
|
115
|
-
# @return [void]
|
|
116
|
-
def maximize
|
|
117
|
-
@sense = :maximize
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
# Sets the objective function.
|
|
121
|
-
#
|
|
122
|
-
# @param objective [LinearExpression, QuadraticExpression] The objective.
|
|
123
|
-
# @return [void]
|
|
124
|
-
def set_objective(objective)
|
|
125
|
-
if objective.is_a?(QuadraticExpression)
|
|
126
|
-
@objective = objective.linear_terms.transform_values(&:to_f)
|
|
127
|
-
@quadratic_terms = objective.hessian_entries
|
|
128
|
-
elsif objective.is_a?(LinearExpression)
|
|
129
|
-
@objective = objective.terms.transform_values(&:to_f)
|
|
130
|
-
@quadratic_terms = []
|
|
131
|
-
end
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
# Solves the model using the native extension.
|
|
135
|
-
#
|
|
136
|
-
# @return [Solution] The solution object.
|
|
137
|
-
# @raise [SolverError] If the solver encounters an error.
|
|
138
|
-
# @raise [LoadError] If the native extension is not available.
|
|
139
|
-
def solve
|
|
140
|
-
unless defined?(LpSolver::Native)
|
|
141
|
-
raise LoadError, 'Native extension not available. Compile with: rake compile'
|
|
142
|
-
end
|
|
143
|
-
|
|
144
|
-
num_col = @var_counter
|
|
145
|
-
num_row = @constraints.size
|
|
146
|
-
|
|
147
|
-
# Build column arrays
|
|
148
|
-
col_cost = Array.new(num_col, 0.0)
|
|
149
|
-
col_lower = Array.new(num_col)
|
|
150
|
-
col_upper = Array.new(num_col)
|
|
151
|
-
col_integrality = Array.new(num_col, 0)
|
|
152
|
-
|
|
153
|
-
@var_bounds.each do |name, (lb, ub)|
|
|
154
|
-
idx = @variables[name].index
|
|
155
|
-
col_lower[idx] = lb
|
|
156
|
-
col_upper[idx] = ub
|
|
157
|
-
col_integrality[idx] = @var_types[name] || 0
|
|
158
|
-
end
|
|
159
|
-
|
|
160
|
-
@objective.each do |idx, coeff|
|
|
161
|
-
col_cost[idx] = coeff
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
# Build constraint arrays (sparse matrix in CSC format)
|
|
165
|
-
row_lower = Array.new(num_row)
|
|
166
|
-
row_upper = Array.new(num_row)
|
|
167
|
-
astart = Array.new(num_row + 1, 0)
|
|
168
|
-
aindex = []
|
|
169
|
-
avalues = []
|
|
170
|
-
nz_count = 0
|
|
171
|
-
|
|
172
|
-
@constraints.each_with_index do |constr, row_idx|
|
|
173
|
-
row_lower[row_idx] = constr[:lb]
|
|
174
|
-
row_upper[row_idx] = constr[:ub]
|
|
175
|
-
|
|
176
|
-
constr[:expr].each do |col_idx, coeff|
|
|
177
|
-
aindex << col_idx
|
|
178
|
-
avalues << coeff
|
|
179
|
-
astart[row_idx + 1] += 1
|
|
180
|
-
nz_count += 1
|
|
181
|
-
end
|
|
182
|
-
end
|
|
183
|
-
|
|
184
|
-
# Adjust astart to be cumulative
|
|
185
|
-
cumulative = 0
|
|
186
|
-
astart.each_with_index do |val, i|
|
|
187
|
-
old = astart[i]
|
|
188
|
-
astart[i] = cumulative
|
|
189
|
-
cumulative += val
|
|
190
|
-
end
|
|
191
|
-
|
|
192
|
-
# Determine sense
|
|
193
|
-
sense = @sense == :maximize ? :maximize : :minimize
|
|
194
|
-
|
|
195
|
-
# Build quadratic arrays (for QP)
|
|
196
|
-
q_start = [0]
|
|
197
|
-
q_index = []
|
|
198
|
-
q_values = []
|
|
199
|
-
|
|
200
|
-
@quadratic_terms.each do |i1, i2, coeff|
|
|
201
|
-
q_index << i2
|
|
202
|
-
q_values << coeff
|
|
203
|
-
q_start << q_index.size
|
|
204
|
-
end
|
|
205
|
-
|
|
206
|
-
# Call native solver
|
|
207
|
-
if @quadratic_terms.any?
|
|
208
|
-
result = LpSolver::Native.solve_qp(
|
|
209
|
-
num_col, num_row,
|
|
210
|
-
col_cost, col_lower, col_upper,
|
|
211
|
-
row_lower, row_upper,
|
|
212
|
-
astart, aindex, avalues,
|
|
213
|
-
q_start, q_index, q_values,
|
|
214
|
-
sense
|
|
215
|
-
)
|
|
216
|
-
else
|
|
217
|
-
result = LpSolver::Native.solve_lp(
|
|
218
|
-
num_col, num_row,
|
|
219
|
-
col_cost, col_lower, col_upper, col_integrality,
|
|
220
|
-
row_lower, row_upper,
|
|
221
|
-
astart, aindex, avalues,
|
|
222
|
-
sense
|
|
223
|
-
)
|
|
224
|
-
end
|
|
225
|
-
|
|
226
|
-
# Parse result
|
|
227
|
-
variables = {}
|
|
228
|
-
result[:col_value].each_with_index do |val, idx|
|
|
229
|
-
var_name = @variables.find { |_, v| v.index == idx }&.first
|
|
230
|
-
variables[var_name.to_s] = val if var_name
|
|
231
|
-
end
|
|
232
|
-
|
|
233
|
-
Solution.new(
|
|
234
|
-
variables: variables,
|
|
235
|
-
objective_value: result[:objective],
|
|
236
|
-
model_status: result[:status].to_s,
|
|
237
|
-
iterations: 0
|
|
238
|
-
)
|
|
239
|
-
end
|
|
240
|
-
|
|
241
|
-
# Sets the optimization sense, objective, and solves in one call.
|
|
242
|
-
#
|
|
243
|
-
# @param objective [LinearExpression, QuadraticExpression] The objective.
|
|
244
|
-
# @return [Solution] The solution object.
|
|
245
|
-
def minimize!(objective)
|
|
246
|
-
@sense = :minimize
|
|
247
|
-
set_objective(objective)
|
|
248
|
-
solve
|
|
249
|
-
end
|
|
250
|
-
|
|
251
|
-
# Sets the optimization sense, objective, and solves in one call.
|
|
252
|
-
#
|
|
253
|
-
# @param objective [LinearExpression, QuadraticExpression] The objective.
|
|
254
|
-
# @return [Solution] The solution object.
|
|
255
|
-
def maximize!(objective)
|
|
256
|
-
@sense = :maximize
|
|
257
|
-
set_objective(objective)
|
|
258
|
-
solve
|
|
259
|
-
end
|
|
260
|
-
end
|
|
261
|
-
end
|