lpsolver 0.2.0 → 0.2.1
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 +34 -0
- data/ext/lpsolver/Makefile +8 -12
- data/ext/lpsolver/native.so +0 -0
- data/lib/lpsolver/model.rb +7 -3
- data/lib/lpsolver/native.so +0 -0
- data/lib/lpsolver/solution.rb +21 -1
- data/lib/lpsolver/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 92ca955854e8330a4c7bc4000076d0a4e1b6c2828507b1bbaae1cdc4168a80b1
|
|
4
|
+
data.tar.gz: 20993564cfc5e75be002b3a558b6a3cab49136a96f73e693cdfb7e1791f51c71
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c61c459e3a47afc69fe0090aca6979ff4079cf73a4b17216594888babafba706511e115c59c4d23b7f14ff511cde0778f7e7557c716b31246fafc4b8d17431a2
|
|
7
|
+
data.tar.gz: bcc475ef900fab92a6c21696d2de870cc78e2890df0c35529385a68d3d80e0b7a1c908828e5806a80fbddc786277f4bb5db6502c530f3e210a48aaea3b4b00f9
|
data/README.md
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
# LpSolver
|
|
2
2
|
|
|
3
|
+
[](https://github.com/davidsiaw/lpsolver/actions/workflows/test.yml)
|
|
4
|
+
|
|
3
5
|
A Ruby gem for solving optimization problems using the [HiGHS](https://github.com/ERGO-Code/HiGHS) solver.
|
|
4
6
|
|
|
7
|
+
Reference documentation can be found at https://davidsiaw.github.io/lpsolver/
|
|
8
|
+
|
|
5
9
|
## What is this for?
|
|
6
10
|
|
|
7
11
|
Imagine you want to **maximize profit** or **minimize cost** while following certain rules (like a budget limit or a minimum requirement). This gem helps you find the best answer.
|
|
@@ -190,6 +194,32 @@ solution = model.maximize!(x * 3 + y * 5)
|
|
|
190
194
|
puts solution.objective_value # => 50.0
|
|
191
195
|
```
|
|
192
196
|
|
|
197
|
+
### Infeasible and Unbounded Solutions
|
|
198
|
+
|
|
199
|
+
Not all problems have a valid solution. Always check the status before accessing values:
|
|
200
|
+
|
|
201
|
+
```ruby
|
|
202
|
+
model = LpSolver::Model.new
|
|
203
|
+
x = model.add_variable(:x, lb: 0)
|
|
204
|
+
y = model.add_variable(:y, lb: 0)
|
|
205
|
+
|
|
206
|
+
# Contradictory constraints — no solution exists
|
|
207
|
+
model.add_constraint(:c1, (x + y) <= 2)
|
|
208
|
+
model.add_constraint(:c2, (x + y) >= 5)
|
|
209
|
+
|
|
210
|
+
solution = model.minimize!(x + y)
|
|
211
|
+
|
|
212
|
+
if solution.infeasible?
|
|
213
|
+
puts "No feasible solution exists"
|
|
214
|
+
elsif solution.unbounded?
|
|
215
|
+
puts "The objective can grow without limit"
|
|
216
|
+
else
|
|
217
|
+
puts "Optimal value: #{solution.objective_value}"
|
|
218
|
+
end
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
> **Important:** When a solution is infeasible, `objective_value` returns `0.0` and `variables` is empty. Always call `solution.infeasible?` or `solution.unbounded?` first before reading `objective_value` or variable values.
|
|
222
|
+
|
|
193
223
|
### Complex Expressions
|
|
194
224
|
|
|
195
225
|
You can chain operators with constants and unary minus:
|
|
@@ -387,3 +417,7 @@ True if the objective can improve without limit.
|
|
|
387
417
|
## Code of Conduct
|
|
388
418
|
|
|
389
419
|
See [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md)
|
|
420
|
+
|
|
421
|
+
# Disclaimer
|
|
422
|
+
|
|
423
|
+
Note that this is vibe-coded in nearly its entirety as part of a quick experiment to improve tokens per sec on my local machine and I do not recommend it for use in any sensitive environments. Note that I do not accept any responsibility for any damages or losses caused by the use of this gem, and I still expect contributed PRs to be provided in sensible and human-reviewable doses.
|
data/ext/lpsolver/Makefile
CHANGED
|
@@ -13,12 +13,12 @@ NULLCMD = :
|
|
|
13
13
|
#### Start of system configuration section. ####
|
|
14
14
|
|
|
15
15
|
srcdir = .
|
|
16
|
-
topdir = /
|
|
16
|
+
topdir = /home/node/.rvm/rubies/ruby-3.3.7/include/ruby-3.3.0
|
|
17
17
|
hdrdir = $(topdir)
|
|
18
|
-
arch_hdrdir = /
|
|
18
|
+
arch_hdrdir = /home/node/.rvm/rubies/ruby-3.3.7/include/ruby-3.3.0/x86_64-linux
|
|
19
19
|
PATH_SEPARATOR = :
|
|
20
20
|
VPATH = $(srcdir):$(arch_hdrdir)/ruby:$(hdrdir)/ruby
|
|
21
|
-
prefix = $(DESTDIR)/
|
|
21
|
+
prefix = $(DESTDIR)/home/node/.rvm/rubies/ruby-3.3.7
|
|
22
22
|
rubysitearchprefix = $(rubylibprefix)/$(sitearch)
|
|
23
23
|
rubyarchprefix = $(rubylibprefix)/$(arch)
|
|
24
24
|
rubylibprefix = $(libdir)/$(RUBY_BASE_NAME)
|
|
@@ -42,7 +42,6 @@ archincludedir = $(includedir)/$(arch)
|
|
|
42
42
|
sitearchlibdir = $(libdir)/$(sitearch)
|
|
43
43
|
archlibdir = $(libdir)/$(arch)
|
|
44
44
|
ridir = $(datarootdir)/$(RI_BASE_NAME)
|
|
45
|
-
modular_gc_dir = $(DESTDIR)
|
|
46
45
|
mandir = $(datarootdir)/man
|
|
47
46
|
localedir = $(datarootdir)/locale
|
|
48
47
|
libdir = $(exec_prefix)/lib
|
|
@@ -79,7 +78,7 @@ COUTFLAG = -o $(empty)
|
|
|
79
78
|
CSRCFLAG = $(empty)
|
|
80
79
|
|
|
81
80
|
RUBY_EXTCONF_H =
|
|
82
|
-
cflags = $(
|
|
81
|
+
cflags = $(optflags) $(debugflags) $(warnflags)
|
|
83
82
|
cxxflags =
|
|
84
83
|
optflags = -O3 -fno-fast-math
|
|
85
84
|
debugflags = -ggdb3
|
|
@@ -87,19 +86,17 @@ warnflags = -Wall -Wextra -Wdeprecated-declarations -Wdiv-by-zero -Wduplicated-c
|
|
|
87
86
|
cppflags =
|
|
88
87
|
CCDLFLAGS = -fPIC
|
|
89
88
|
CFLAGS = $(CCDLFLAGS) $(cflags) -fPIC $(ARCH_FLAG)
|
|
90
|
-
INCFLAGS = -I. -I$(arch_hdrdir) -I$(hdrdir)/ruby/backward -I$(hdrdir) -I$(srcdir) -I/
|
|
89
|
+
INCFLAGS = -I. -I$(arch_hdrdir) -I$(hdrdir)/ruby/backward -I$(hdrdir) -I$(srcdir) -I/workspace/lpsolver/ext/lpsolver-highs/highs -I/workspace/lpsolver/ext/lpsolver-highs/build/include/highs
|
|
91
90
|
DEFS =
|
|
92
91
|
CPPFLAGS = $(DEFS) $(cppflags)
|
|
93
92
|
CXXFLAGS = $(CCDLFLAGS) $(ARCH_FLAG)
|
|
94
|
-
ldflags = -L. -fstack-protector-strong -rdynamic -Wl,-export-dynamic -Wl,--no-as-needed -L/
|
|
93
|
+
ldflags = -L. -fstack-protector-strong -rdynamic -Wl,-export-dynamic -Wl,--no-as-needed -L/workspace/lpsolver/ext/lpsolver-highs/build/lib -Wl,-rpath,/workspace/lpsolver/ext/lpsolver-highs/build/lib
|
|
95
94
|
dldflags = -Wl,--compress-debug-sections=zlib
|
|
96
95
|
ARCH_FLAG =
|
|
97
96
|
DLDFLAGS = $(ldflags) $(dldflags) $(ARCH_FLAG)
|
|
98
97
|
LDSHARED = $(CC) -shared
|
|
99
98
|
LDSHAREDXX = $(CXX) -shared
|
|
100
|
-
POSTLINK = :
|
|
101
99
|
AR = gcc-ar
|
|
102
|
-
LD = ld
|
|
103
100
|
EXEEXT =
|
|
104
101
|
|
|
105
102
|
RUBY_INSTALL_NAME = $(RUBY_BASE_NAME)
|
|
@@ -111,7 +108,7 @@ RUBY_BASE_NAME = ruby
|
|
|
111
108
|
|
|
112
109
|
arch = x86_64-linux
|
|
113
110
|
sitearch = $(arch)
|
|
114
|
-
ruby_version = 3.
|
|
111
|
+
ruby_version = 3.3.0
|
|
115
112
|
ruby = $(bindir)/$(RUBY_BASE_NAME)
|
|
116
113
|
RUBY = $(ruby)
|
|
117
114
|
BUILTRUBY = $(bindir)/$(RUBY_BASE_NAME)
|
|
@@ -131,7 +128,7 @@ TOUCH = exit >
|
|
|
131
128
|
|
|
132
129
|
preload =
|
|
133
130
|
libpath = . $(libdir)
|
|
134
|
-
LIBPATH =
|
|
131
|
+
LIBPATH = -L. -L$(libdir) -Wl,-rpath,$(libdir)
|
|
135
132
|
DEFFILE =
|
|
136
133
|
|
|
137
134
|
CLEANFILES = mkmf.log
|
|
@@ -266,7 +263,6 @@ $(TARGET_SO): $(OBJS) Makefile
|
|
|
266
263
|
$(ECHO) linking shared-object lpsolver/$(DLLIB)
|
|
267
264
|
-$(Q)$(RM) $(@)
|
|
268
265
|
$(Q) $(LDSHARED) -o $@ $(OBJS) $(LIBPATH) $(DLDFLAGS) $(LOCAL_LIBS) $(LIBS)
|
|
269
|
-
$(Q) $(POSTLINK)
|
|
270
266
|
|
|
271
267
|
|
|
272
268
|
|
data/ext/lpsolver/native.so
CHANGED
|
Binary file
|
data/lib/lpsolver/model.rb
CHANGED
|
@@ -264,12 +264,16 @@ module LpSolver
|
|
|
264
264
|
"--solution_file #{solution_file.path}"
|
|
265
265
|
|
|
266
266
|
output = `#{cmd} 2>&1`
|
|
267
|
-
status = $?.success?
|
|
268
|
-
|
|
269
267
|
lp_file.unlink
|
|
270
268
|
opts_file.unlink
|
|
271
269
|
|
|
272
|
-
|
|
270
|
+
# HiGHS returns non-zero exit code for infeasible/unbounded problems,
|
|
271
|
+
# but still writes a valid solution file. Check for valid status instead.
|
|
272
|
+
solution_content = File.read(solution_file.path)
|
|
273
|
+
status_match = solution_content.match(/Model status\s*\n\s*(\S+)/i)
|
|
274
|
+
unless status_match
|
|
275
|
+
raise SolverError, "HiGHS solver failed:\n#{output}" unless $?.success?
|
|
276
|
+
end
|
|
273
277
|
|
|
274
278
|
@solution = parse_solution_file(solution_file.path)
|
|
275
279
|
solution_file.unlink
|
data/lib/lpsolver/native.so
CHANGED
|
Binary file
|
data/lib/lpsolver/solution.rb
CHANGED
|
@@ -21,11 +21,15 @@ module LpSolver
|
|
|
21
21
|
# @return [Hash{String => Float}] Maps variable names to their optimal values.
|
|
22
22
|
# The keys are the variable names as strings (as produced by HiGHS),
|
|
23
23
|
# and the values are the optimal decision variable values.
|
|
24
|
+
# When the solution is infeasible, this hash is empty.
|
|
25
|
+
# Always check `infeasible?` or `unbounded?` before reading variable values.
|
|
24
26
|
attr_reader :variables
|
|
25
27
|
|
|
26
28
|
# @return [Float] The optimal objective function value.
|
|
27
29
|
# For minimization problems, this is the minimum value.
|
|
28
30
|
# For maximization problems, this is the maximum value.
|
|
31
|
+
# When the solution is infeasible, this returns `0.0`.
|
|
32
|
+
# Always check `infeasible?` or `unbounded?` before reading this value.
|
|
29
33
|
attr_reader :objective_value
|
|
30
34
|
|
|
31
35
|
# @return [String] The status of the model as reported by HiGHS.
|
|
@@ -40,6 +44,19 @@ module LpSolver
|
|
|
40
44
|
# This is a diagnostic metric; may be 0 for some solver types.
|
|
41
45
|
attr_reader :iterations
|
|
42
46
|
|
|
47
|
+
# Returns the model status as a Symbol.
|
|
48
|
+
#
|
|
49
|
+
# @return [Symbol] The solver status as a Ruby symbol:
|
|
50
|
+
# - :optimal — An optimal solution was found.
|
|
51
|
+
# - :infeasible — No feasible solution exists.
|
|
52
|
+
# - :unbounded — The objective can be improved without bound.
|
|
53
|
+
# - :unknown — The solver could not determine the status.
|
|
54
|
+
# @example
|
|
55
|
+
# solution.status # => :optimal
|
|
56
|
+
def status
|
|
57
|
+
@model_status.to_sym
|
|
58
|
+
end
|
|
59
|
+
|
|
43
60
|
# Creates a new Solution object.
|
|
44
61
|
#
|
|
45
62
|
# @param variables [Hash{String => Float}] Maps variable names to values.
|
|
@@ -57,12 +74,15 @@ module LpSolver
|
|
|
57
74
|
#
|
|
58
75
|
# @param name [Symbol, String, Variable] The variable name (Symbol, String,
|
|
59
76
|
# or Variable object).
|
|
60
|
-
# @return [Float] The optimal value of the variable
|
|
77
|
+
# @return [Float] The optimal value of the variable, or `nil` if the
|
|
78
|
+
# solution is infeasible or unbounded.
|
|
61
79
|
# @raise [KeyError] If the variable name is not found in the solution.
|
|
62
80
|
# @example
|
|
63
81
|
# solution[:x] # => 4.0 (by symbol)
|
|
64
82
|
# solution['x'] # => 4.0 (by string)
|
|
65
83
|
# solution[x] # => 4.0 (by Variable object)
|
|
84
|
+
# @note When the solution is infeasible, all variables are empty.
|
|
85
|
+
# Check `infeasible?` first before accessing variable values.
|
|
66
86
|
def [](name)
|
|
67
87
|
key = if name.is_a?(Variable)
|
|
68
88
|
name.name.to_s
|
data/lib/lpsolver/version.rb
CHANGED