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