rulp 0.0.45 → 0.0.50

Sign up to get free protection for your applications and to get access to all the features.
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