lpsolver 0.2.1 → 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 +38 -13
- data/ext/lpsolver/Makefile +2 -2
- 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 -410
- data/lib/lpsolver/version.rb +1 -1
- data/lib/lpsolver.rb +10 -4
- metadata +6 -2
- data/lib/lpsolver/native_model.rb +0 -261
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d76ac229fc048afc42cb81ae328ac716095d43767fb8329ae6cdc8d8a69f8f14
|
|
4
|
+
data.tar.gz: 76b6e5e26102cc8ad5cd8fbc9da236a6b5bdd7496c5099083d0ea750d27e9000
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c969d6289498dfa06bb0ed44633fa0ea0f9100aa50ec9ec3be9a1a9e11c2f2c27ad2978c7d7f802e3834692edc9fe9802742035e3847249c8bb726112a25b769
|
|
7
|
+
data.tar.gz: 45f270ac494d18748344cbc446ebf591f28af625341f424649e7f2452c880d8478bb8bde1ce7dc0ea2805cbb909e5d796dbbe5df60506e9d25e9b04e96505085
|
data/README.md
CHANGED
|
@@ -115,6 +115,32 @@ ruby -Ilib examples/coin_purse.rb
|
|
|
115
115
|
|
|
116
116
|
> **HiGHS is bundled automatically.** The `rake compile` task (run during `bundle install` or `rake`) downloads the HiGHS v1.14.0 precompiled static library from GitHub and links it into the gem. No system-level HiGHS installation is required — it ships with the gem.
|
|
117
117
|
|
|
118
|
+
## Solvers
|
|
119
|
+
|
|
120
|
+
The Model class uses a pluggable driver architecture. By default it uses the **CLI driver** (HiGHS subprocess), but you can switch to the **native driver** (C extension) for direct in-process solving.
|
|
121
|
+
|
|
122
|
+
### CLI Driver (default)
|
|
123
|
+
|
|
124
|
+
The CLI driver serializes the model to HiGHS LP format and invokes the HiGHS binary as a subprocess. This is the default and most reliable approach.
|
|
125
|
+
|
|
126
|
+
```ruby
|
|
127
|
+
model = LpSolver::Model.new
|
|
128
|
+
# Uses CliDriver by default
|
|
129
|
+
solution = model.minimize!(x * 3 + y * 5)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Native Driver
|
|
133
|
+
|
|
134
|
+
The native driver calls the HiGHS C extension directly, bypassing file serialization. It requires the native extension to be compiled (`rake compile`).
|
|
135
|
+
|
|
136
|
+
```ruby
|
|
137
|
+
model = LpSolver::Model.new
|
|
138
|
+
model.driver = LpSolver::NativeDriver.new
|
|
139
|
+
solution = model.minimize!(x * 3 + y * 5)
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
> **Note:** The native driver is currently experimental. The CLI driver is recommended for production use.
|
|
143
|
+
|
|
118
144
|
## Usage
|
|
119
145
|
|
|
120
146
|
### Simple Example (Operator DSL)
|
|
@@ -248,7 +274,7 @@ x = model.add_variable(:x, lb: 0)
|
|
|
248
274
|
y = model.add_variable(:y, lb: 0)
|
|
249
275
|
|
|
250
276
|
model.add_constraint(:c1, (x * 2 + y) <= 10)
|
|
251
|
-
model.
|
|
277
|
+
model.minimize!(x + y)
|
|
252
278
|
|
|
253
279
|
# Print the LP file format
|
|
254
280
|
puts model.to_lp
|
|
@@ -313,24 +339,23 @@ Set the objective to minimize and solve in one call.
|
|
|
313
339
|
### `Model#maximize!(objective)`
|
|
314
340
|
Set the objective to maximize and solve in one call.
|
|
315
341
|
|
|
316
|
-
### `Model#minimize`
|
|
317
|
-
Set the optimization sense to minimization (legacy, use `minimize!` instead).
|
|
318
|
-
|
|
319
|
-
### `Model#maximize`
|
|
320
|
-
Set the optimization sense to maximization (legacy, use `maximize!` instead).
|
|
321
|
-
|
|
322
|
-
### `Model#set_objective(objective)`
|
|
323
|
-
Set the objective function without solving (legacy, use `minimize!`/`maximize!` instead).
|
|
324
|
-
- `objective` can be a `LinearExpression`, `QuadraticExpression`, or Hash.
|
|
325
|
-
|
|
326
342
|
### `Model#to_lp`
|
|
327
343
|
Returns the model as a HiGHS LP format string.
|
|
328
344
|
|
|
329
345
|
### `Model#write_lp(filename)`
|
|
330
346
|
Writes the model to an LP file.
|
|
331
347
|
|
|
332
|
-
### `Model#
|
|
333
|
-
|
|
348
|
+
### `Model#driver`
|
|
349
|
+
Returns the current solver driver.
|
|
350
|
+
|
|
351
|
+
### `Model#driver=(driver)`
|
|
352
|
+
Sets the solver driver. Accepts `LpSolver::CliDriver` (default) or `LpSolver::NativeDriver`.
|
|
353
|
+
|
|
354
|
+
### `LpSolver::CliDriver.new(highs_path: nil)`
|
|
355
|
+
Creates a CLI driver. Uses `Model::HIGHS_PATH` by default, or the provided path.
|
|
356
|
+
|
|
357
|
+
### `LpSolver::NativeDriver.new`
|
|
358
|
+
Creates a native driver. Requires the native extension to be compiled.
|
|
334
359
|
|
|
335
360
|
### `Solution#[]`
|
|
336
361
|
Get a variable's value. Accepts Symbol, String, or Variable object.
|
data/ext/lpsolver/Makefile
CHANGED
|
@@ -86,11 +86,11 @@ warnflags = -Wall -Wextra -Wdeprecated-declarations -Wdiv-by-zero -Wduplicated-c
|
|
|
86
86
|
cppflags =
|
|
87
87
|
CCDLFLAGS = -fPIC
|
|
88
88
|
CFLAGS = $(CCDLFLAGS) $(cflags) -fPIC $(ARCH_FLAG)
|
|
89
|
-
INCFLAGS = -I. -I$(arch_hdrdir) -I$(hdrdir)/ruby/backward -I$(hdrdir) -I$(srcdir) -I/workspace/lpsolver/ext/lpsolver-highs/
|
|
89
|
+
INCFLAGS = -I. -I$(arch_hdrdir) -I$(hdrdir)/ruby/backward -I$(hdrdir) -I$(srcdir) -I/workspace/lpsolver/ext/lpsolver-highs/build/include/highs
|
|
90
90
|
DEFS =
|
|
91
91
|
CPPFLAGS = $(DEFS) $(cppflags)
|
|
92
92
|
CXXFLAGS = $(CCDLFLAGS) $(ARCH_FLAG)
|
|
93
|
-
ldflags = -L. -fstack-protector-strong -rdynamic -Wl,-export-dynamic -Wl,--no-as-needed -L/workspace/lpsolver/ext/lpsolver-highs/build/lib
|
|
93
|
+
ldflags = -L. -fstack-protector-strong -rdynamic -Wl,-export-dynamic -Wl,--no-as-needed -L/workspace/lpsolver/ext/lpsolver-highs/build/lib
|
|
94
94
|
dldflags = -Wl,--compress-debug-sections=zlib
|
|
95
95
|
ARCH_FLAG =
|
|
96
96
|
DLDFLAGS = $(ldflags) $(dldflags) $(ARCH_FLAG)
|
data/ext/lpsolver/ext.o
CHANGED
|
Binary file
|
data/ext/lpsolver/extconf.rb
CHANGED
|
@@ -11,6 +11,8 @@ highs_build = File.join(highs_src, 'build')
|
|
|
11
11
|
|
|
12
12
|
highs_found = false
|
|
13
13
|
|
|
14
|
+
|
|
15
|
+
|
|
14
16
|
# Priority: 1. HIGHS_DIR env var, 2. submodule build, 3. system paths
|
|
15
17
|
highs_dir = ENV.fetch('HIGHS_DIR', nil)
|
|
16
18
|
|
|
@@ -32,23 +34,24 @@ unless highs_found
|
|
|
32
34
|
# Check bundled submodule (CMake build or precompiled tarball)
|
|
33
35
|
if File.exist?(File.join(highs_build, 'lib', 'libhighs.so')) ||
|
|
34
36
|
File.exist?(File.join(highs_build, 'lib', 'libhighs.a'))
|
|
35
|
-
highs_include = File.join(highs_src, 'highs')
|
|
36
37
|
highs_lib = File.join(highs_build, 'lib')
|
|
37
38
|
|
|
38
|
-
# CMake build: HConfig.h is in build dir
|
|
39
|
-
# Precompiled tarball: HConfig.h is in build/include/highs/
|
|
39
|
+
# CMake build: HConfig.h is in build dir, headers in highs/ subdir
|
|
40
|
+
# Precompiled tarball: HConfig.h is in build/include/highs/, headers in include/highs/
|
|
40
41
|
highs_config = File.join(highs_build, 'HConfig.h')
|
|
41
42
|
if File.exist?(highs_config)
|
|
43
|
+
# CMake build layout
|
|
44
|
+
highs_include = File.join(highs_src, 'highs')
|
|
42
45
|
$INCFLAGS += " -I#{highs_include} -I#{highs_build}"
|
|
43
46
|
else
|
|
44
|
-
# Precompiled tarball layout
|
|
47
|
+
# Precompiled tarball layout: headers under include/highs/
|
|
45
48
|
highs_config_inc = File.join(highs_build, 'include', 'highs')
|
|
46
49
|
if File.exist?(File.join(highs_config_inc, 'HConfig.h'))
|
|
47
|
-
$INCFLAGS += " -I#{
|
|
50
|
+
$INCFLAGS += " -I#{highs_config_inc}"
|
|
48
51
|
end
|
|
49
52
|
end
|
|
50
53
|
|
|
51
|
-
$LDFLAGS += " -L#{highs_lib}
|
|
54
|
+
$LDFLAGS += " -L#{highs_lib}"
|
|
52
55
|
$LIBS += " -lhighs -lstdc++"
|
|
53
56
|
$LOAD_PATH << highs_lib
|
|
54
57
|
highs_found = true
|
|
@@ -70,10 +73,10 @@ abort <<~ERROR unless highs_found
|
|
|
70
73
|
Options:
|
|
71
74
|
1. Build from submodule: rake build_highs && rake compile
|
|
72
75
|
2. Set HIGHS_DIR=/path/to/highs
|
|
73
|
-
3. Install system-wide: sudo apt install highs
|
|
76
|
+
3. Install system-wide: brew install highs (macOS) or sudo apt install highs (Linux)
|
|
74
77
|
|
|
75
78
|
Submodule location: #{highs_src}
|
|
76
|
-
Build output: #{highs_build}/lib/libhighs.
|
|
79
|
+
Build output: #{highs_build}/lib/libhighs.a
|
|
77
80
|
ERROR
|
|
78
81
|
|
|
79
82
|
create_makefile('lpsolver/native')
|
data/ext/lpsolver/native.so
CHANGED
|
Binary file
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# LpSolver Drivers
|
|
2
|
+
|
|
3
|
+
Pluggable solving backends for `LpSolver::Model`.
|
|
4
|
+
|
|
5
|
+
## API Contract
|
|
6
|
+
|
|
7
|
+
Every driver must implement the following interface:
|
|
8
|
+
|
|
9
|
+
### `#initialize(opts = {})`
|
|
10
|
+
|
|
11
|
+
Optional constructor. Drivers may accept configuration options.
|
|
12
|
+
|
|
13
|
+
| Driver | Options |
|
|
14
|
+
|--------|---------|
|
|
15
|
+
| `CliDriver` | `highs_path: String` — path to HiGHS binary (uses `HIGHS_PATH` resolution if omitted) |
|
|
16
|
+
| `NativeDriver` | None |
|
|
17
|
+
|
|
18
|
+
### `#solve(model) → Solution`
|
|
19
|
+
|
|
20
|
+
Solves the given model and returns a `Solution` object.
|
|
21
|
+
|
|
22
|
+
**Parameters:**
|
|
23
|
+
- `model` (`LpSolver::Model`) — the model to solve
|
|
24
|
+
|
|
25
|
+
**Returns:**
|
|
26
|
+
- `LpSolver::Solution` — contains variable values, objective value, and model status
|
|
27
|
+
|
|
28
|
+
**Raises:**
|
|
29
|
+
- `LpSolver::SolverError` — if the solver encounters an error (e.g., HiGHS binary not found)
|
|
30
|
+
- `LoadError` — if required dependencies are missing (e.g., native extension not compiled)
|
|
31
|
+
|
|
32
|
+
## Model Data Access
|
|
33
|
+
|
|
34
|
+
Drivers read model state via the following `Model` accessors:
|
|
35
|
+
|
|
36
|
+
| Accessor | Type | Description |
|
|
37
|
+
|----------|------|-------------|
|
|
38
|
+
| `model.var_counter` | `Integer` | Number of variables |
|
|
39
|
+
| `model.heading` | `Symbol` | `:minimize` or `:maximize` |
|
|
40
|
+
| `model.constraints` | `Array<Hash>` | Constraint data: `{ name:, lb:, ub:, expr: [[col, coeff], ...] }` |
|
|
41
|
+
| `model.constraints_data` | `Hash` | Named constraint map |
|
|
42
|
+
| `model.var_types` | `Hash{Symbol => Symbol}` | Variable types: `:continuous` or `:integer` |
|
|
43
|
+
| `model.objective` | `Hash{Integer => Float}` | Objective coefficients by variable index |
|
|
44
|
+
| `model.quadratic_terms` | `Array<[Integer, Integer, Float]>` | Quadratic terms: `[col1, col2, coeff]` |
|
|
45
|
+
| `model.var_bounds` | `Hash{Symbol => [Float, Float]}` | Variable bounds: `[lb, ub]` |
|
|
46
|
+
| `model.variables` | `Hash{Symbol => Variable}` | Variable map (name → Variable object) |
|
|
47
|
+
|
|
48
|
+
## Writing a Custom Driver
|
|
49
|
+
|
|
50
|
+
To create a new driver:
|
|
51
|
+
|
|
52
|
+
```ruby
|
|
53
|
+
require 'lpsolver/drivers'
|
|
54
|
+
|
|
55
|
+
module LpSolver
|
|
56
|
+
module Drivers
|
|
57
|
+
class MyCustomDriver
|
|
58
|
+
def initialize(**opts)
|
|
59
|
+
# driver-specific setup
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def solve(model)
|
|
63
|
+
# 1. Read model state via model.var_counter, model.constraints, etc.
|
|
64
|
+
# 2. Call your solver
|
|
65
|
+
# 3. Return a LpSolver::Solution
|
|
66
|
+
LpSolver::Solution.new(
|
|
67
|
+
variables: { 'x' => 1.0, 'y' => 2.0 },
|
|
68
|
+
objective_value: 12.0,
|
|
69
|
+
model_status: 'optimal',
|
|
70
|
+
iterations: 0
|
|
71
|
+
)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Then use it:
|
|
79
|
+
|
|
80
|
+
```ruby
|
|
81
|
+
model = LpSolver::Model.new
|
|
82
|
+
model.driver = LpSolver::Drivers::MyCustomDriver.new
|
|
83
|
+
solution = model.solve
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Available Drivers
|
|
87
|
+
|
|
88
|
+
| Driver | Description | Dependencies |
|
|
89
|
+
|--------|-------------|-------------|
|
|
90
|
+
| `CliDriver` | HiGHS subprocess via LP file | `highs` binary |
|
|
91
|
+
| `NativeDriver` | HiGHS C extension | Compiled native extension (`rake compile`) |
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'tempfile'
|
|
4
|
+
|
|
5
|
+
module LpSolver
|
|
6
|
+
module Drivers
|
|
7
|
+
# Solves a model using the HiGHS command-line interface.
|
|
8
|
+
#
|
|
9
|
+
# This driver serializes the model to HiGHS LP format, invokes the
|
|
10
|
+
# HiGHS executable as a subprocess, and parses the solution file.
|
|
11
|
+
#
|
|
12
|
+
class CliDriver
|
|
13
|
+
# The path to the HiGHS binary.
|
|
14
|
+
#
|
|
15
|
+
# Resolution order:
|
|
16
|
+
# 1. Path passed to initialize
|
|
17
|
+
# 2. HIGHS_PATH environment variable
|
|
18
|
+
# 3. Bundled binary at lib/lpsolver/highs (from rake compile)
|
|
19
|
+
# 4. 'highs' on system PATH
|
|
20
|
+
#
|
|
21
|
+
# @return [String] The path to the HiGHS executable.
|
|
22
|
+
HIGHS_PATH = begin
|
|
23
|
+
env_path = ENV.fetch('HIGHS_PATH', nil)
|
|
24
|
+
if env_path
|
|
25
|
+
env_path
|
|
26
|
+
else
|
|
27
|
+
bundled = File.expand_path('../../../lib/lpsolver/highs', __dir__)
|
|
28
|
+
if File.exist?(bundled)
|
|
29
|
+
bundled
|
|
30
|
+
else
|
|
31
|
+
'highs'
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Creates a new CLI driver.
|
|
37
|
+
#
|
|
38
|
+
# @param highs_path [String, nil] Path to the HiGHS binary.
|
|
39
|
+
# If nil, uses the default resolution order.
|
|
40
|
+
def initialize(highs_path: nil)
|
|
41
|
+
@highs_path = highs_path || HIGHS_PATH
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Solves the model using the HiGHS CLI.
|
|
45
|
+
#
|
|
46
|
+
# @param model [Model] The model to solve.
|
|
47
|
+
# @return [Solution] The solution object.
|
|
48
|
+
# @raise [SolverError] If the HiGHS solver encounters an error.
|
|
49
|
+
def solve(model)
|
|
50
|
+
lp_content = model.to_lp
|
|
51
|
+
lp_file = Tempfile.new(['model', '.lp'])
|
|
52
|
+
lp_file.write(lp_content)
|
|
53
|
+
lp_file.close
|
|
54
|
+
|
|
55
|
+
solution_file = Tempfile.new(['solution', '.sol'])
|
|
56
|
+
opts_file = Tempfile.new(['highs_opts', '.txt'])
|
|
57
|
+
opts_file.write("log_to_console = false\noutput_flag = false\n")
|
|
58
|
+
opts_file.close
|
|
59
|
+
|
|
60
|
+
cmd = "#{@highs_path} " \
|
|
61
|
+
"--model_file #{lp_file.path} " \
|
|
62
|
+
"--options_file #{opts_file.path} " \
|
|
63
|
+
"--solution_file #{solution_file.path}"
|
|
64
|
+
|
|
65
|
+
output = `#{cmd} 2>&1`
|
|
66
|
+
lp_file.unlink
|
|
67
|
+
opts_file.unlink
|
|
68
|
+
|
|
69
|
+
# HiGHS returns non-zero exit code for infeasible/unbounded problems,
|
|
70
|
+
# but still writes a valid solution file. Check for valid status instead.
|
|
71
|
+
solution_content = File.read(solution_file.path)
|
|
72
|
+
status_match = solution_content.match(/Model status\s*\n\s*(\S+)/i)
|
|
73
|
+
unless status_match
|
|
74
|
+
raise SolverError, "HiGHS solver failed:\n#{output}" unless $?.success?
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
parse_solution(solution_file.path)
|
|
78
|
+
ensure
|
|
79
|
+
solution_file&.unlink
|
|
80
|
+
opts_file&.unlink
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
# Parses the HiGHS solution file.
|
|
86
|
+
#
|
|
87
|
+
# @param path [String] The path to the HiGHS solution file.
|
|
88
|
+
# @return [Solution] The parsed solution object.
|
|
89
|
+
def parse_solution(path)
|
|
90
|
+
content = File.read(path)
|
|
91
|
+
variables = {}
|
|
92
|
+
objective_value = 0.0
|
|
93
|
+
model_status = 'unknown'
|
|
94
|
+
|
|
95
|
+
status_match = content.match(/Model status\s*\n\s*(\S+)/i)
|
|
96
|
+
model_status = status_match[1].downcase.gsub('_', ' ') if status_match
|
|
97
|
+
|
|
98
|
+
obj_match = content.match(/Objective\s+(\S+)/i)
|
|
99
|
+
objective_value = obj_match[1].to_f if obj_match
|
|
100
|
+
|
|
101
|
+
in_columns = false
|
|
102
|
+
content.each_line do |line|
|
|
103
|
+
if line =~ /# Columns/i
|
|
104
|
+
in_columns = true
|
|
105
|
+
next
|
|
106
|
+
end
|
|
107
|
+
next unless in_columns
|
|
108
|
+
break if line.strip.empty? || line.start_with?('#')
|
|
109
|
+
|
|
110
|
+
parts = line.strip.split(/\s+/, 2)
|
|
111
|
+
variables[parts[0]] = parts[1].to_f if parts.length == 2
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
Solution.new(
|
|
115
|
+
variables: variables,
|
|
116
|
+
objective_value: objective_value,
|
|
117
|
+
model_status: model_status,
|
|
118
|
+
iterations: 0
|
|
119
|
+
)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LpSolver
|
|
4
|
+
module Drivers
|
|
5
|
+
# Solves a model using the native C extension.
|
|
6
|
+
#
|
|
7
|
+
# This driver builds HiGHS data structures directly from the model
|
|
8
|
+
# and calls the native C API, bypassing LP file serialization.
|
|
9
|
+
# It requires the native extension to be compiled and loaded.
|
|
10
|
+
#
|
|
11
|
+
class NativeDriver
|
|
12
|
+
# Solves the model using the native C extension.
|
|
13
|
+
#
|
|
14
|
+
# @param model [Model] The model to solve.
|
|
15
|
+
# @return [Solution] The solution object.
|
|
16
|
+
# @raise [LoadError] If the native extension is not available.
|
|
17
|
+
# @raise [SolverError] If the native solver encounters an error.
|
|
18
|
+
def solve(model)
|
|
19
|
+
unless defined?(LpSolver::HiGhSSolver)
|
|
20
|
+
raise LoadError, 'Native extension not available. Compile with: rake compile'
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
num_col = model.var_counter
|
|
24
|
+
num_row = model.constraints.size
|
|
25
|
+
|
|
26
|
+
# Determine heading — native API always minimizes, so negate for maximize
|
|
27
|
+
heading = model.heading == :maximize ? :maximize : :minimize
|
|
28
|
+
|
|
29
|
+
# Build column arrays
|
|
30
|
+
col_cost = Array.new(num_col, 0.0)
|
|
31
|
+
col_lower = Array.new(num_col)
|
|
32
|
+
col_upper = Array.new(num_col)
|
|
33
|
+
col_integrality = Array.new(num_col, 0)
|
|
34
|
+
|
|
35
|
+
model.var_bounds.each do |name, (lb, ub)|
|
|
36
|
+
idx = model.variables[name].index
|
|
37
|
+
col_lower[idx] = lb
|
|
38
|
+
col_upper[idx] = ub
|
|
39
|
+
col_integrality[idx] = model.var_types[name] == :integer ? 1 : 0
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
model.objective.each do |idx, coeff|
|
|
43
|
+
col_cost[idx] = heading == :maximize ? -coeff.to_f : coeff.to_f
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Build constraint arrays
|
|
47
|
+
row_lower = Array.new(num_row)
|
|
48
|
+
row_upper = Array.new(num_row)
|
|
49
|
+
|
|
50
|
+
model.constraints.each_with_index do |constr, row_idx|
|
|
51
|
+
row_lower[row_idx] = constr[:lb]
|
|
52
|
+
row_upper[row_idx] = constr[:ub]
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Build matrix in CSC (column-wise) format
|
|
56
|
+
# a_start[col] = index into a_index/a_value where column `col` starts
|
|
57
|
+
# a_index[nz] = row index of non-zero element
|
|
58
|
+
# a_value[nz] = value of non-zero element
|
|
59
|
+
nz_per_col = Array.new(num_col, 0)
|
|
60
|
+
nz_entries = []
|
|
61
|
+
|
|
62
|
+
model.constraints.each_with_index do |constr, row_idx|
|
|
63
|
+
constr[:expr].each do |col_idx, coeff|
|
|
64
|
+
nz_entries << [col_idx, row_idx, coeff.to_f]
|
|
65
|
+
nz_per_col[col_idx] += 1
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
total_nz = nz_entries.size
|
|
70
|
+
|
|
71
|
+
# Build a_start (cumulative column starts)
|
|
72
|
+
a_start = [0]
|
|
73
|
+
nz_per_col.each { |count| a_start << a_start.last + count }
|
|
74
|
+
|
|
75
|
+
# Sort entries by column index for CSC format
|
|
76
|
+
nz_entries.sort_by! { |col, row, _| col }
|
|
77
|
+
|
|
78
|
+
aindex = nz_entries.map { |_, row, _| row }
|
|
79
|
+
avalues = nz_entries.map { |_, _, val| val }
|
|
80
|
+
|
|
81
|
+
# Call native solver (skip if no columns/rows)
|
|
82
|
+
if num_col > 0 && num_row > 0
|
|
83
|
+
result = call_native(
|
|
84
|
+
num_col, num_row, total_nz,
|
|
85
|
+
col_cost, col_lower, col_upper, col_integrality,
|
|
86
|
+
row_lower, row_upper,
|
|
87
|
+
a_start, aindex, avalues
|
|
88
|
+
)
|
|
89
|
+
else
|
|
90
|
+
result = { status: :unbounded, objective: 0.0, col_value: [] }
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Parse result — variables are empty for infeasible/unbounded
|
|
94
|
+
variables = {}
|
|
95
|
+
unless [:infeasible, :unbounded, :unbounded_or_infeasible].include?(result[:status])
|
|
96
|
+
result[:col_value].each_with_index do |val, idx|
|
|
97
|
+
var_name = model.variables.find { |_, v| v.index == idx }&.first
|
|
98
|
+
variables[var_name.to_s] = val if var_name
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
Solution.new(
|
|
103
|
+
variables: variables,
|
|
104
|
+
objective_value: heading == :maximize ? -result[:objective] : result[:objective],
|
|
105
|
+
model_status: result[:status].to_s,
|
|
106
|
+
iterations: 0
|
|
107
|
+
)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
private
|
|
111
|
+
|
|
112
|
+
# Calls the native HiGHS solver.
|
|
113
|
+
#
|
|
114
|
+
# @param num_col [Integer] Number of columns.
|
|
115
|
+
# @param num_row [Integer] Number of rows.
|
|
116
|
+
# @param num_nz [Integer] Number of non-zero elements.
|
|
117
|
+
# @param col_cost [Array<Float>] Column cost coefficients.
|
|
118
|
+
# @param col_lower [Array<Float>] Column lower bounds.
|
|
119
|
+
# @param col_upper [Array<Float>] Column upper bounds.
|
|
120
|
+
# @param col_integrality [Array<Integer>] Column integrality flags.
|
|
121
|
+
# @param row_lower [Array<Float>] Row lower bounds.
|
|
122
|
+
# @param row_upper [Array<Float>] Row upper bounds.
|
|
123
|
+
# @param a_start [Array<Integer>] Matrix column start indices (CSC format).
|
|
124
|
+
# @param aindex [Array<Integer>] Matrix row indices.
|
|
125
|
+
# @param avalues [Array<Float>] Matrix values.
|
|
126
|
+
# @return [Hash] Solver result with :status, :objective, :col_value, etc.
|
|
127
|
+
def call_native(num_col, num_row, num_nz, col_cost, col_lower, col_upper, col_integrality,
|
|
128
|
+
row_lower, row_upper, a_start, aindex, avalues)
|
|
129
|
+
solver = LpSolver::HiGhSSolver.new
|
|
130
|
+
solver.num_col = num_col
|
|
131
|
+
solver.num_row = num_row
|
|
132
|
+
solver.num_nz = num_nz
|
|
133
|
+
|
|
134
|
+
solver.col_cost = col_cost
|
|
135
|
+
solver.col_lower = col_lower
|
|
136
|
+
solver.col_upper = col_upper
|
|
137
|
+
solver.col_integrality = col_integrality
|
|
138
|
+
|
|
139
|
+
solver.row_lower = row_lower
|
|
140
|
+
solver.row_upper = row_upper
|
|
141
|
+
|
|
142
|
+
solver.a_start = a_start
|
|
143
|
+
solver.a_index = aindex
|
|
144
|
+
solver.a_value = avalues
|
|
145
|
+
|
|
146
|
+
solver.solve
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Driver classes — pluggable solving backends.
|
|
4
|
+
#
|
|
5
|
+
# Each driver implements the same interface so Model can swap them
|
|
6
|
+
# at runtime. See drivers/README.md for the API contract.
|
|
7
|
+
#
|
|
8
|
+
require_relative 'drivers/cli_driver'
|
|
9
|
+
require_relative 'drivers/native_driver'
|