Class: LpSolver::NativeModel

Inherits:
Object
  • Object
show all
Defined in:
lib/lpsolver/native_model.rb

Overview

Note:

This is a prototype. The native extension must be compiled separately using ‘rake compile` or `ruby ext/lpsolver/extconf.rb && make`.

A native (C extension) backed model solver for HiGHS.

This class uses the native C extension to call HiGHS directly, bypassing the LP file serialization overhead of the CLI approach. It requires the native extension to be compiled and linked against the HiGHS library.

Examples:

Basic usage

require 'lpsolver/native_model'

model = LpSolver::NativeModel.new
x = model.add_variable(:x, lb: 0)
y = model.add_variable(:y, lb: 0)

model.add_constraint(:c1, (x + y) >= 4)
solution = model.minimize!(x * 3 + y * 5)

puts solution[:x]  # => 4.0

With MIP

model = LpSolver::NativeModel.new
x = model.add_variable(:x, lb: 0, integer: true)
y = model.add_variable(:y, lb: 0, integer: true)

model.add_constraint(:c1, (x + y) == 10)
solution = model.minimize!(x * 2 + y * 3)

puts solution[:x]  # => 10.0

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(name = nil) ⇒ NativeModel

Creates a new empty model.

Parameters:

  • name (String) (defaults to: nil)

    An optional name for this model.



57
58
59
60
61
62
63
64
65
66
67
# File 'lib/lpsolver/native_model.rb', line 57

def initialize(name = nil)
  @name = name || 'untitled'
  @variables = {}
  @constraints = []
  @var_counter = 0
  @sense = :minimize
  @objective = {}
  @quadratic_terms = []
  @var_types = {}
  @var_bounds = {}
end

Instance Attribute Details

#constraintsArray<Hash> (readonly)

Returns Constraint data for each constraint.

Returns:

  • (Array<Hash>)

    Constraint data for each constraint.



43
44
45
# File 'lib/lpsolver/native_model.rb', line 43

def constraints
  @constraints
end

#nameString (readonly)

Returns A descriptive name for this model.

Returns:

  • (String)

    A descriptive name for this model.



37
38
39
# File 'lib/lpsolver/native_model.rb', line 37

def name
  @name
end

#objectiveHash{Integer => Float} (readonly)

Returns Linear objective coefficients.

Returns:

  • (Hash{Integer => Float})

    Linear objective coefficients.



49
50
51
# File 'lib/lpsolver/native_model.rb', line 49

def objective
  @objective
end

#quadratic_termsArray<[Integer, Integer, Float]> (readonly)

Returns Quadratic terms (for QP).

Returns:

  • (Array<[Integer, Integer, Float]>)

    Quadratic terms (for QP).



52
53
54
# File 'lib/lpsolver/native_model.rb', line 52

def quadratic_terms
  @quadratic_terms
end

#senseSymbol (readonly)

Returns Optimization sense (:minimize or :maximize).

Returns:

  • (Symbol)

    Optimization sense (:minimize or :maximize).



46
47
48
# File 'lib/lpsolver/native_model.rb', line 46

def sense
  @sense
end

#variablesHash{Symbol => Variable} (readonly)

Returns All variables in the model.

Returns:

  • (Hash{Symbol => Variable})

    All variables in the model.



40
41
42
# File 'lib/lpsolver/native_model.rb', line 40

def variables
  @variables
end

Instance Method Details

#add_constraint(name, expr) ⇒ Symbol

Adds a constraint to the model.

Parameters:

  • name (Symbol, String)

    The constraint name.

  • expr (ConstraintSpec)

    The constraint specification.

Returns:

  • (Symbol)

    The constraint name.



92
93
94
95
96
97
98
99
100
101
102
103
104
# File 'lib/lpsolver/native_model.rb', line 92

def add_constraint(name, expr)
  name = name.to_sym
  lb_val, ub_val = expr.bounds
  data_expr = expr.expr

  @constraints << {
    name: name,
    lb: lb_val,
    ub: ub_val,
    expr: data_expr
  }
  name
end

#add_variable(name, lb: 0.0, ub: Float::INFINITY, integer: false) ⇒ Variable

Adds a variable to the model.

Parameters:

  • name (Symbol, String)

    The variable name.

  • lb (Float) (defaults to: 0.0)

    Lower bound (default: 0.0).

  • ub (Float) (defaults to: Float::INFINITY)

    Upper bound (default: Float::INFINITY).

  • integer (Boolean) (defaults to: false)

    Whether the variable must be integer (default: false).

Returns:



76
77
78
79
80
81
82
83
84
85
# File 'lib/lpsolver/native_model.rb', line 76

def add_variable(name, lb: 0.0, ub: Float::INFINITY, integer: false)
  name = name.to_sym
  idx = @var_counter
  var = Variable.new(idx, name)
  @variables[name] = var
  @var_types[name] = integer ? 1 : 0
  @var_bounds[name] = [lb, ub]
  @var_counter += 1
  var
end

#maximizevoid

This method returns an undefined value.

Sets the optimization sense to maximization.



116
117
118
# File 'lib/lpsolver/native_model.rb', line 116

def maximize
  @sense = :maximize
end

#maximize!(objective) ⇒ Solution

Sets the optimization sense, objective, and solves in one call.

Parameters:

Returns:



255
256
257
258
259
# File 'lib/lpsolver/native_model.rb', line 255

def maximize!(objective)
  @sense = :maximize
  set_objective(objective)
  solve
end

#minimizevoid

This method returns an undefined value.

Sets the optimization sense to minimization.



109
110
111
# File 'lib/lpsolver/native_model.rb', line 109

def minimize
  @sense = :minimize
end

#minimize!(objective) ⇒ Solution

Sets the optimization sense, objective, and solves in one call.

Parameters:

Returns:



245
246
247
248
249
# File 'lib/lpsolver/native_model.rb', line 245

def minimize!(objective)
  @sense = :minimize
  set_objective(objective)
  solve
end

#set_objective(objective) ⇒ void

This method returns an undefined value.

Sets the objective function.

Parameters:



124
125
126
127
128
129
130
131
132
# File 'lib/lpsolver/native_model.rb', line 124

def set_objective(objective)
  if objective.is_a?(QuadraticExpression)
    @objective = objective.linear_terms.transform_values(&:to_f)
    @quadratic_terms = objective.hessian_entries
  elsif objective.is_a?(LinearExpression)
    @objective = objective.terms.transform_values(&:to_f)
    @quadratic_terms = []
  end
end

#solveSolution

Solves the model using the native extension.

Returns:

Raises:

  • (SolverError)

    If the solver encounters an error.

  • (LoadError)

    If the native extension is not available.



139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
# File 'lib/lpsolver/native_model.rb', line 139

def solve
  unless defined?(LpSolver::Native)
    raise LoadError, 'Native extension not available. Compile with: rake compile'
  end

  num_col = @var_counter
  num_row = @constraints.size

  # Build column arrays
  col_cost = Array.new(num_col, 0.0)
  col_lower = Array.new(num_col)
  col_upper = Array.new(num_col)
  col_integrality = Array.new(num_col, 0)

  @var_bounds.each do |name, (lb, ub)|
    idx = @variables[name].index
    col_lower[idx] = lb
    col_upper[idx] = ub
    col_integrality[idx] = @var_types[name] || 0
  end

  @objective.each do |idx, coeff|
    col_cost[idx] = coeff
  end

  # Build constraint arrays (sparse matrix in CSC format)
  row_lower = Array.new(num_row)
  row_upper = Array.new(num_row)
  astart = Array.new(num_row + 1, 0)
  aindex = []
  avalues = []
  nz_count = 0

  @constraints.each_with_index do |constr, row_idx|
    row_lower[row_idx] = constr[:lb]
    row_upper[row_idx] = constr[:ub]

    constr[:expr].each do |col_idx, coeff|
      aindex << col_idx
      avalues << coeff
      astart[row_idx + 1] += 1
      nz_count += 1
    end
  end

  # Adjust astart to be cumulative
  cumulative = 0
  astart.each_with_index do |val, i|
    old = astart[i]
    astart[i] = cumulative
    cumulative += val
  end

  # Determine sense
  sense = @sense == :maximize ? :maximize : :minimize

  # Build quadratic arrays (for QP)
  q_start = [0]
  q_index = []
  q_values = []

  @quadratic_terms.each do |i1, i2, coeff|
    q_index << i2
    q_values << coeff
    q_start << q_index.size
  end

  # Call native solver
  if @quadratic_terms.any?
    result = LpSolver::Native.solve_qp(
      num_col, num_row,
      col_cost, col_lower, col_upper,
      row_lower, row_upper,
      astart, aindex, avalues,
      q_start, q_index, q_values,
      sense
    )
  else
    result = LpSolver::Native.solve_lp(
      num_col, num_row,
      col_cost, col_lower, col_upper, col_integrality,
      row_lower, row_upper,
      astart, aindex, avalues,
      sense
    )
  end

  # Parse result
  variables = {}
  result[:col_value].each_with_index do |val, idx|
    var_name = @variables.find { |_, v| v.index == idx }&.first
    variables[var_name.to_s] = val if var_name
  end

  Solution.new(
    variables: variables,
    objective_value: result[:objective],
    model_status: result[:status].to_s,
    iterations: 0
  )
end