rulp 0.0.45 → 0.0.50

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: 58a47250456efb480a41ea21a923355242c38c27d1e9eb04d17782493a51b083
4
- data.tar.gz: e7b05a98c43d57035049cd1b2537fd3ecb42792e08114d8dd0729229092e96ac
3
+ metadata.gz: 85bd89961eddfceba98245a2ac9882467ae8363209242a3b87bf092afc746d28
4
+ data.tar.gz: 7fdaf681f33655e4fbd461363ca4d94c581362080958b50fef21050f6562d29c
5
5
  SHA512:
6
- metadata.gz: d12655f339c1095d9bc5f3c4620829965e3375ae4025fceeaea0438d97e6f334bdc0b727e0ac975d03ef8b688da4775c6c34cc9b56c0885cfa6fe38cd757d2dc
7
- data.tar.gz: 9a3bd500cabd00c9118ff81cd79da588d629f5ee5788eae993d986161cd2ce3da715965534491f3d088b202154e53f124f7053dea671df606b028db96e24cdb4
6
+ metadata.gz: 1e676c5a658047114c59e29bab30fcb43f61a3fe2ddb8156234f034d475a1a2d50e78d232fa006bb723a84e453727857dc5963bee096c9db4f7b033119d6c96c
7
+ data.tar.gz: 58584bcfe586ac5ae4603b54ed1a1c2de520e95bb41a06cb0a553d510fb2c6c914fda5bcb820d8d23348259e9c623face707c5ec06ab75f35ee52a391928a9c8
@@ -21,15 +21,14 @@ class Expressions
21
21
  @expressions.map(&:variable)
22
22
  end
23
23
 
24
- [:==, :<, :<=, :>, :>=].each do |constraint_type|
25
- define_method(constraint_type){|value|
24
+ %i[== < <= > >=].each do |constraint_type|
25
+ define_method(constraint_type) do |value|
26
26
  Constraint.new(self, constraint_type, value)
27
- }
27
+ end
28
28
  end
29
29
 
30
30
  def -@
31
- -self.expressions[0]
32
- self
31
+ self.class.new(expressions.map(&:-@))
33
32
  end
34
33
 
35
34
  def -(other)
@@ -38,7 +37,7 @@ class Expressions
38
37
  end
39
38
 
40
39
  def +(other)
41
- Expressions.new(self.expressions + Expressions[other].expressions)
40
+ Expressions.new(expressions + Expressions[other].expressions)
42
41
  end
43
42
 
44
43
  def self.[](value)
@@ -50,7 +49,7 @@ class Expressions
50
49
  end
51
50
 
52
51
  def evaluate
53
- self.expressions.map(&:evaluate).inject(:+)
52
+ expressions.map(&:evaluate).inject(:+)
54
53
  end
55
54
  end
56
55
 
@@ -66,19 +65,19 @@ class Fragment
66
65
  end
67
66
 
68
67
  def +(other)
69
- return Expressions.new([self] + Expressions[other].expressions)
68
+ Expressions.new([self] + Expressions[other].expressions)
70
69
  end
71
70
 
72
71
  def -(other)
73
72
  self.+(-other)
74
73
  end
75
74
 
76
- def *(value)
77
- Fragment.new(@lv, @operand * value)
75
+ def *(other)
76
+ Fragment.new(@lv, @operand * other)
78
77
  end
79
78
 
80
79
  def evaluate
81
- if [TrueClass,FalseClass].include? @lv.value.class
80
+ if [TrueClass, FalseClass].include? @lv.value.class
82
81
  @operand * (@lv.value ? 1 : 0)
83
82
  else
84
83
  @operand * @lv.value
@@ -94,20 +93,18 @@ class Fragment
94
93
  @lv
95
94
  end
96
95
 
97
- [:==, :<, :<=, :>, :>=].each do |constraint_type|
98
- define_method(constraint_type){|value|
96
+ %i[== < <= > >=].each do |constraint_type|
97
+ define_method(constraint_type) do |value|
99
98
  Constraint.new(Expressions.new(self), constraint_type, value)
100
- }
99
+ end
101
100
  end
102
101
 
103
102
  def to_s
104
- @as_str ||= begin
105
- case @operand
106
- when -1 then " - #{@lv}"
107
- when 1 then " + #{@lv}"
108
- when ->(op){ op < 0} then " - #{@operand.abs} #{@lv}"
109
- else " + #{@operand} #{@lv}"
110
- end
111
- end
103
+ @as_str ||= case @operand
104
+ when -1 then " - #{@lv}"
105
+ when 1 then " + #{@lv}"
106
+ when ->(op) { op < 0 } then " - #{@operand.abs} #{@lv}"
107
+ else " + #{@operand} #{@lv}"
108
+ end
112
109
  end
113
- end
110
+ end
data/lib/rulp/lv.rb CHANGED
@@ -40,7 +40,7 @@ class LV
40
40
  end
41
41
  end
42
42
 
43
- def * (numeric)
43
+ def *(numeric)
44
44
  self.nocoerce
45
45
  Expressions.new([Fragment.new(self, numeric)])
46
46
  end
@@ -53,7 +53,7 @@ class LV
53
53
  self + (-other)
54
54
  end
55
55
 
56
- def + (expressions)
56
+ def +(expressions)
57
57
  Expressions[self] + Expressions[expressions]
58
58
  end
59
59
 
data/lib/rulp/rulp.rb CHANGED
@@ -18,6 +18,7 @@ GLPK = "glpsol"
18
18
  SCIP = "scip"
19
19
  CBC = "cbc"
20
20
  GUROBI = "gurobi_cl"
21
+ HIGHS = "highs"
21
22
 
22
23
  module Rulp
23
24
  attr_accessor :expressions
@@ -31,12 +32,14 @@ module Rulp
31
32
  GUROBI = ::GUROBI
32
33
  SCIP = ::SCIP
33
34
  CBC = ::CBC
35
+ HIGHS = ::HIGHS
34
36
 
35
37
  SOLVERS = {
36
38
  GLPK => Glpk,
37
39
  SCIP => Scip,
38
40
  CBC => Cbc,
39
41
  GUROBI => Gurobi,
42
+ HIGHS => Highs
40
43
  }
41
44
 
42
45
 
@@ -56,6 +59,10 @@ module Rulp
56
59
  lp.solve_with(GUROBI, opts)
57
60
  end
58
61
 
62
+ def self.Highs(lp, opts={})
63
+ lp.solve_with(HIGHS, opts)
64
+ end
65
+
59
66
  def self.Max(objective_expression)
60
67
  Rulp.log(Logger::INFO, "Creating maximization problem")
61
68
  Problem.new(Rulp::MAX, objective_expression)
@@ -185,6 +192,8 @@ module Rulp
185
192
  solver.store_results(@variables)
186
193
 
187
194
  if solver.unsuccessful
195
+ raise "Solve failed: #{solver.model_status}" if solver.model_status
196
+
188
197
  outfile_contents = IO.read(solver.outfile)
189
198
  raise "Solve failed: solution infeasible" if outfile_contents.downcase.include?("infeasible") || outfile_contents.strip.length.zero?
190
199
  raise "Solve failed: all units undefined"
data/lib/rulp/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Rulp
2
- VERSION = '0.0.45'
3
- end
2
+ VERSION = '0.0.50'
3
+ end
data/lib/solvers/glpk.rb CHANGED
@@ -4,7 +4,7 @@ class Glpk < Solver
4
4
  command %= [
5
5
  options[:gap] ? "--mipgap #{options[:gap]}" : "",
6
6
  options[:time_limit] ? "--tmlim #{options[:time_limit]}" : ""
7
- ]
7
+ ].join(" ")
8
8
  exec(command)
9
9
  end
10
10
 
@@ -30,4 +30,4 @@ class Glpk < Solver
30
30
  self.unsuccessful = rows[-3].downcase.include?('infeasible')
31
31
  return objective_str.to_f
32
32
  end
33
- end
33
+ end
@@ -0,0 +1,70 @@
1
+ class Highs < Solver
2
+ HIGHS_SUPPORTED_OPTIONS = {
3
+ gap: :mip_rel_gap
4
+ }.freeze
5
+
6
+ def solve
7
+ options_file = create_highs_options_file
8
+
9
+ command = "#{executable} --model_file #{@filename} --solution_file #{@outfile} %s %s"
10
+ command %= [
11
+ options[:time_limit] ? "--time_limit #{options[:time_limit]}" : '',
12
+ options_file.length.positive? ? "--options_file #{options_file}" : ''
13
+ ]
14
+
15
+ exec(command)
16
+
17
+ # Remove options file as HiGHS requires additional params in file format instead of command line arguments
18
+ FileUtils.rm(options_file) unless options_file.empty?
19
+ end
20
+
21
+ def self.executable
22
+ :highs
23
+ end
24
+
25
+ def store_results(variables)
26
+ rows = IO.read(@outfile).split("\n")
27
+ self.model_status = rows[1]
28
+
29
+ if model_status.downcase.include?('infeasible') ||
30
+ model_status.downcase.include?('time limit reached')
31
+ self.unsuccessful = true
32
+ return
33
+ end
34
+
35
+ columns_idx = rows.index { |r| r.match?(/# Columns/) }
36
+ rows_idx = rows.index { |r| r.match?(/# Rows/) }
37
+
38
+ vars_by_name = {}
39
+
40
+ rows[(columns_idx + 1)...rows_idx].each do |row|
41
+ var_name, var_value = row.strip.split(/\s+/)
42
+ vars_by_name[var_name] = var_value
43
+ end
44
+
45
+ variables.each do |var|
46
+ var.value = vars_by_name[var.to_s].to_f
47
+ end
48
+
49
+ rows.find { |r| r.match?(/Objective/) }.to_s.split(/\s+/).last.to_f
50
+ end
51
+
52
+ private
53
+
54
+ def create_highs_options_file
55
+ options_str = ''
56
+
57
+ HIGHS_SUPPORTED_OPTIONS.each do |rulp_key, highs_key|
58
+ next unless options[rulp_key]
59
+
60
+ options_str += "#{highs_key} = #{options[rulp_key]}\n"
61
+ end
62
+
63
+ return '' if options_str.empty?
64
+
65
+ options_file = "/tmp/highs-#{Random.rand(0..1_000_000)}.opt"
66
+ IO.write(options_file, options_str)
67
+
68
+ options_file
69
+ end
70
+ end
@@ -1,6 +1,8 @@
1
+ require 'fileutils'
2
+
1
3
  class Solver
2
4
  attr_reader :options, :outfile, :filename
3
- attr_accessor :unsuccessful
5
+ attr_accessor :unsuccessful, :model_status
4
6
 
5
7
  def initialize(filename, options)
6
8
  @options = options
@@ -61,3 +63,4 @@ require_relative 'cbc'
61
63
  require_relative 'scip'
62
64
  require_relative 'glpk'
63
65
  require_relative 'gurobi'
66
+ require_relative 'highs'
@@ -55,4 +55,4 @@ class BasicSuite < Minitest::Test
55
55
  assert_in_delta X_f.value, 345.4321, 0.001
56
56
  end
57
57
  end
58
- end
58
+ end
data/test/test_helper.rb CHANGED
@@ -8,7 +8,7 @@ Rulp::log_level = Logger::UNKNOWN
8
8
  Rulp::print_solver_outputs = false
9
9
 
10
10
  def each_solver
11
- [:scip, :cbc, :glpk, :gurobi].each do |solver|
11
+ [:scip, :cbc, :glpk, :gurobi, :highs].each do |solver|
12
12
  LV::clear
13
13
  if Rulp::solver_exists?(solver)
14
14
  yield(solver)
@@ -0,0 +1,30 @@
1
+ require_relative 'test_helper'
2
+
3
+ class NegateExpressionTest < Minitest::Test
4
+
5
+ def test_negate_expression
6
+ @problem = Rulp::Max(- (X1_i + 2 * X2_i))
7
+ @problem[
8
+ X1_i + X2_i == 10,
9
+ X1_i >= 0,
10
+ X2_i >= 0
11
+ ]
12
+
13
+ @problem.solve
14
+ assert X1_i.value == 10
15
+ assert X2_i.value == 0
16
+ end
17
+
18
+ def test_double_negate_expression
19
+ @problem = Rulp::Max(-(-(X1_i + 2 * X2_i)))
20
+ @problem[
21
+ X1_i + X2_i == 10,
22
+ X1_i >= 0,
23
+ X2_i >= 0
24
+ ]
25
+
26
+ @problem.solve
27
+ assert X1_i.value == 0
28
+ assert X2_i.value == 10
29
+ end
30
+ end
@@ -0,0 +1,19 @@
1
+ require_relative 'test_helper'
2
+
3
+ class NegateTerm < Minitest::Test
4
+ def setup
5
+ @problem = Rulp::Max(-X1_i + 2 * X2_i)
6
+ @problem[
7
+ X1_i + X2_i == 10,
8
+ X1_i >= 0,
9
+ X2_i >= 0
10
+ ]
11
+
12
+ @problem.solve
13
+ end
14
+
15
+ def test_negate_term
16
+ assert X1_i.value == 0
17
+ assert X2_i.value == 10
18
+ end
19
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rulp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.45
4
+ version: 0.0.50
5
5
  platform: ruby
6
6
  authors:
7
7
  - Wouter Coppieters
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-03-03 00:00:00.000000000 Z
11
+ date: 2024-05-26 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: A simple Ruby LP description DSL
14
14
  email: wc@pico.net.nz
@@ -36,12 +36,15 @@ files:
36
36
  - lib/solvers/cbc.rb
37
37
  - lib/solvers/glpk.rb
38
38
  - lib/solvers/gurobi.rb
39
+ - lib/solvers/highs.rb
39
40
  - lib/solvers/scip.rb
40
41
  - lib/solvers/solver.rb
41
42
  - test/test_basic_suite.rb
42
43
  - test/test_boolean.rb
43
44
  - test/test_helper.rb
44
45
  - test/test_infeasible.rb
46
+ - test/test_negate_expression.rb
47
+ - test/test_negate_term.rb
45
48
  - test/test_save_to_file.rb
46
49
  - test/test_simple.rb
47
50
  homepage:
@@ -62,7 +65,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
62
65
  - !ruby/object:Gem::Version
63
66
  version: '0'
64
67
  requirements: []
65
- rubygems_version: 3.5.6
68
+ rubygems_version: 3.4.19
66
69
  signing_key:
67
70
  specification_version: 4
68
71
  summary: Ruby Linear Programming
@@ -71,5 +74,7 @@ test_files:
71
74
  - test/test_boolean.rb
72
75
  - test/test_helper.rb
73
76
  - test/test_infeasible.rb
77
+ - test/test_negate_expression.rb
78
+ - test/test_negate_term.rb
74
79
  - test/test_save_to_file.rb
75
80
  - test/test_simple.rb