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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a24185adf2e045f36f5249e311b5d357ca16134ab259c4f350fddbe07e141d9c
4
- data.tar.gz: 9fb91dbea34b972e32876b97b4719ef771b30b8975d9ec5bf7494a4d7d49b2d7
3
+ metadata.gz: 92ca955854e8330a4c7bc4000076d0a4e1b6c2828507b1bbaae1cdc4168a80b1
4
+ data.tar.gz: 20993564cfc5e75be002b3a558b6a3cab49136a96f73e693cdfb7e1791f51c71
5
5
  SHA512:
6
- metadata.gz: b64322ff1240d58b92345ce8e9fff552dceeada28d57066fc56a94c02a89f66debaeeb1ccc3e37fbb434fe6619b828f91f536a5f8abc323d2792c8f8da37e4c9
7
- data.tar.gz: fe8f3fba95166b9608837886d84b81adfa4d9b1416864c3607a5f194d9aaa679d36af36389ec1e6db4dc239e7d64417bd9b985a3a1a300c3c35f75597a950988
6
+ metadata.gz: c61c459e3a47afc69fe0090aca6979ff4079cf73a4b17216594888babafba706511e115c59c4d23b7f14ff511cde0778f7e7557c716b31246fafc4b8d17431a2
7
+ data.tar.gz: bcc475ef900fab92a6c21696d2de870cc78e2890df0c35529385a68d3d80e0b7a1c908828e5806a80fbddc786277f4bb5db6502c530f3e210a48aaea3b4b00f9
data/README.md CHANGED
@@ -1,7 +1,11 @@
1
1
  # LpSolver
2
2
 
3
+ [![test](https://github.com/davidsiaw/lpsolver/actions/workflows/test.yml/badge.svg)](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.
@@ -13,12 +13,12 @@ NULLCMD = :
13
13
  #### Start of system configuration section. ####
14
14
 
15
15
  srcdir = .
16
- topdir = /usr/local/include/ruby-3.4.0
16
+ topdir = /home/node/.rvm/rubies/ruby-3.3.7/include/ruby-3.3.0
17
17
  hdrdir = $(topdir)
18
- arch_hdrdir = /usr/local/include/ruby-3.4.0/x86_64-linux
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)/usr/local
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 = $(hardenflags) $(optflags) $(debugflags) $(warnflags)
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/home/david/programs/ai/pi/workspace/lpsolver/ext/lpsolver-highs/highs -I/home/david/programs/ai/pi/workspace/lpsolver/ext/lpsolver-highs/build/include/highs
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/home/david/programs/ai/pi/workspace/lpsolver/ext/lpsolver-highs/build/lib -Wl,-rpath,/home/david/programs/ai/pi/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 -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.4.0
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 = -L. -L$(libdir) -Wl,-rpath,$(libdir)
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
 
Binary file
@@ -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
- raise SolverError, "HiGHS solver failed:\n#{output}" unless status
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
Binary file
@@ -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
@@ -4,5 +4,5 @@ module LpSolver
4
4
  # The current version of the LpSolver gem.
5
5
  #
6
6
  # @return [String] The version string (e.g., "0.1.0").
7
- VERSION = '0.2.0'
7
+ VERSION = '0.2.1'
8
8
  end
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.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Siaw