rams 0.1.6 → 0.1.8

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
- SHA1:
3
- metadata.gz: d4517c505465b19e574e3cc95c74f2752e2f86c6
4
- data.tar.gz: 8a89edf255ae13faecef79b088405fa4585f0bcd
2
+ SHA256:
3
+ metadata.gz: defc078f3cd03a2c628d0e11188285cb9ee9382a51aa1f9ec1f922ed07e572fb
4
+ data.tar.gz: 272fea822e2e6653b9c459c771c4791872336f052c792e08da3b0408a57058b4
5
5
  SHA512:
6
- metadata.gz: 057e75b5aed5a80b4522becfe96377c80c3f55c635db61dad3e53a7a22586b6774935e8d339c8acb81906e4de9817d6dbe555d2a207a4d57927632400cfe3a3e
7
- data.tar.gz: 543e206a28a6b83918b1dd9edb390b55986a1f25cbec295f49251abcec9c1af5f3a7d41b4b8e2a3c0059d05459bc6ba1e2f9efe25b93cfa8938ac636a0970512
6
+ metadata.gz: 8786c449623e046a876cf4d6a17fc67839d050bfe0e8cea3a3045965b54bb78230d7741ac54d92486af7fdf4e693fecfe7a7c4020870b95692d262d8cbf02638
7
+ data.tar.gz: 5594f7fb97c6b6e22127a7feeb6a2032319f5d00cc38c37fb22baa96011af6dfea6fe4994c583d8fbc221c51019bd25f6ed11e1c202986e5064b30e9351d683b
data/lib/rams/model.rb CHANGED
@@ -3,8 +3,8 @@ require_relative 'expression'
3
3
  require_relative 'formatters/lp'
4
4
  require_relative 'solvers/cbc'
5
5
  require_relative 'solvers/clp'
6
- require_relative 'solvers/cplex'
7
6
  require_relative 'solvers/glpk'
7
+ require_relative 'solvers/highs'
8
8
  require_relative 'solvers/scip'
9
9
  require_relative 'variable'
10
10
 
@@ -38,8 +38,8 @@ module RAMS
38
38
  SOLVERS = {
39
39
  cbc: RAMS::Solvers::CBC.new,
40
40
  clp: RAMS::Solvers::CLP.new,
41
- cplex: RAMS::Solvers::CPLEX.new,
42
41
  glpk: RAMS::Solvers::GLPK.new,
42
+ highs: RAMS::Solvers::HiGHS.new,
43
43
  scip: RAMS::Solvers::SCIP.new
44
44
  }.freeze
45
45
 
data/lib/rams/numeric.rb CHANGED
@@ -30,7 +30,7 @@ class Integer
30
30
  end
31
31
  end
32
32
 
33
- # Floats can be treated the same way as Fixnums.
33
+ # Floats can be treated the same way as Integers.
34
34
  class Float
35
35
  alias old_add +
36
36
  alias old_sub -
@@ -5,7 +5,7 @@ module RAMS
5
5
  # Interface to COIN-OR Branch-and-Cut
6
6
  class CBC < Solver
7
7
  def solver_command(model_path, solution_path, args)
8
- ['cbc', model_path] + args + ['printingOptions', 'all', 'solve', 'solution', solution_path]
8
+ [solver_executable('coin.cbc', 'cbc'), model_path] + args + ['printingOptions', 'all', 'solve', 'solution', solution_path]
9
9
  end
10
10
 
11
11
  private
@@ -22,8 +22,7 @@ module RAMS
22
22
 
23
23
  def parse_objective(model, lines)
24
24
  return nil if lines.count < 1
25
- objective = lines.first.split[-1].to_f
26
- model.sense == :max ? -objective : objective
25
+ lines.first.split[-1].to_f
27
26
  end
28
27
 
29
28
  def parse_primal(model, lines)
@@ -36,7 +35,7 @@ module RAMS
36
35
  def parse_dual(model, lines)
37
36
  lines[1, model.constraints.count].map do |l|
38
37
  comps = l.split
39
- dual = model.sense == :max ? -comps[3].to_f : comps[3].to_f
38
+ dual = comps[3].to_f
40
39
  [model.constraints[comps[1]], dual]
41
40
  end.to_h
42
41
  end
@@ -5,7 +5,7 @@ module RAMS
5
5
  # Interface to COIN-OR Linear Programming
6
6
  class CLP < Solver
7
7
  def solver_command(model_path, solution_path, args)
8
- ['clp', model_path] + args + ['printingOptions', 'all', 'solve', 'solution', solution_path]
8
+ [solver_executable('clp', 'clp'), model_path] + args + ['printingOptions', 'all', 'solve', 'solution', solution_path]
9
9
  end
10
10
 
11
11
  private
@@ -21,22 +21,21 @@ module RAMS
21
21
  end
22
22
 
23
23
  def parse_objective(model, lines)
24
- return nil if lines.count < 2
25
- objective = lines[1].split[-1].to_f
26
- model.sense == :max ? -objective : objective
24
+ return nil if lines.count < 1
25
+ lines[0].split[-1].to_f
27
26
  end
28
27
 
29
28
  def parse_primal(model, lines)
30
- lines[model.constraints.count + 2, model.variables.count].map do |l|
29
+ lines[model.constraints.count + 1, model.variables.count].map do |l|
31
30
  comps = l.split
32
31
  [model.variables[comps[1]], comps[2].to_f]
33
32
  end.to_h
34
33
  end
35
34
 
36
35
  def parse_dual(model, lines)
37
- lines[2, model.constraints.count].map do |l|
36
+ lines[1, model.constraints.count].map do |l|
38
37
  comps = l.split
39
- dual = model.sense == :max ? -comps[3].to_f : comps[3].to_f
38
+ dual = comps[3].to_f
40
39
  [model.constraints[comps[1]], dual]
41
40
  end.to_h
42
41
  end
@@ -5,7 +5,7 @@ module RAMS
5
5
  # Interface to the GNU Linear Programming Kit
6
6
  class GLPK < Solver
7
7
  def solver_command(model_path, solution_path, args)
8
- ['glpsol', '--lp', model_path, '--output', solution_path] + args
8
+ [solver_executable('glpsol', 'glpk'), '--lp', model_path, '--output', solution_path] + args
9
9
  end
10
10
 
11
11
  private
@@ -0,0 +1,94 @@
1
+ require_relative 'solver'
2
+
3
+ module RAMS
4
+ module Solvers
5
+ # Interface to HiGHS solver
6
+ class HiGHS < Solver
7
+ def solver_command(model_path, solution_path, args)
8
+ [solver_executable('highs', 'highs'), model_path, '--solution_file', solution_path] + args
9
+ end
10
+
11
+ private
12
+
13
+ def parse_status(_model, lines)
14
+ status_idx = lines.index { |l| l =~ /^Model status/ }
15
+ return :undefined unless status_idx
16
+
17
+ # Status is on the next line after "Model status"
18
+ status = lines[status_idx + 1]
19
+ return :undefined unless status
20
+
21
+ return :optimal if status =~ /optimal/i
22
+ return :feasible if status =~ /^feasible/i
23
+ return :infeasible if status =~ /infeasible/i
24
+ return :unbounded if status =~ /unbounded/i
25
+ :undefined
26
+ end
27
+
28
+ def parse_objective(_model, lines)
29
+ objective_line = lines.find { |l| l =~ /^Objective/ }
30
+ return nil unless objective_line
31
+ objective_line.split.last.to_f
32
+ end
33
+
34
+ def parse_primal(model, lines)
35
+ # Find the primal section
36
+ start_idx = lines.index { |l| l =~ /^# Columns/ }
37
+ return {} unless start_idx
38
+
39
+ num_columns = lines[start_idx].split.last.to_i
40
+ primal_values = {}
41
+
42
+ # Parse variable values
43
+ (1..num_columns).each do |i|
44
+ line = lines[start_idx + i]
45
+ next unless line
46
+
47
+ parts = line.split
48
+ next unless parts.length >= 2
49
+
50
+ var_name = parts[0]
51
+ var_value = parts[1].to_f
52
+ variable = model.variables[var_name]
53
+ primal_values[variable] = var_value if variable
54
+ end
55
+
56
+ primal_values
57
+ end
58
+
59
+ def parse_dual(model, lines)
60
+ # Find the dual section
61
+ dual_start_idx = lines.index { |l| l =~ /^# Dual solution values/ }
62
+ return {} unless dual_start_idx
63
+
64
+
65
+ # Check if dual solution is available
66
+ lines = lines[dual_start_idx..-1]
67
+ return {} if lines[dual_start_idx + 1] =~ /^None/
68
+
69
+ # Find the rows section within dual values
70
+ rows_idx = lines.index { |l| l =~ /^# Rows/ }
71
+ return {} unless rows_idx
72
+
73
+ num_rows = lines[rows_idx].split.last.to_i
74
+ dual_values = {}
75
+
76
+ # Parse constraint dual values
77
+ (1..num_rows).each do |i|
78
+ line = lines[rows_idx + i]
79
+ next unless line
80
+
81
+ parts = line.split
82
+ next unless parts.length >= 2
83
+
84
+ constraint_name = parts[0]
85
+ dual_value = parts[1].to_f
86
+ constraint = model.constraints[constraint_name]
87
+ dual_values[constraint] = dual_value if constraint
88
+ end
89
+
90
+ dual_values
91
+ end
92
+ end
93
+ end
94
+ end
@@ -11,7 +11,7 @@ module RAMS
11
11
  end
12
12
 
13
13
  def solver_command(model_path, _solution_path, args)
14
- ['scip', '-c', "read #{model_path}"] + args +
14
+ [solver_executable('scip', 'scip'), '-c', "read #{model_path}"] + args +
15
15
  ['-c', 'optimize', '-c', 'display solution', '-c', 'display dualsolution', '-c', 'quit']
16
16
  end
17
17
 
@@ -28,9 +28,9 @@ module RAMS
28
28
  end
29
29
 
30
30
  def parse_objective(_model, lines)
31
- status = (lines.select { |l| l =~ /^objective value:/ }.first || '').split
32
- return nil if status.size < 3
33
- status[2].to_f
31
+ objective = (lines.select { |l| l =~ /^objective value:/ }.first || '').split
32
+ return nil if objective.size < 3
33
+ objective[2].to_f
34
34
  end
35
35
 
36
36
  def parse_primal(model, lines)
@@ -60,6 +60,11 @@ module RAMS
60
60
  raise NotImplementedError
61
61
  end
62
62
 
63
+ # Get solver executable name, checking environment variable first
64
+ def solver_executable(default_name, solver_name)
65
+ ENV["RAMS_SOLVER_PATH_#{solver_name.upcase}"] || default_name
66
+ end
67
+
63
68
  def parse_solution(model, solution_text)
64
69
  lines = solution_text.split "\n"
65
70
  RAMS::Solution.new(
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rams
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.6
4
+ version: 0.1.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ryan J. O'Neil
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2017-02-05 00:00:00.000000000 Z
10
+ date: 2025-06-24 00:00:00.000000000 Z
12
11
  dependencies: []
13
12
  description: A library for solving MILPs in Ruby.
14
13
  email:
@@ -26,8 +25,8 @@ files:
26
25
  - lib/rams/solution.rb
27
26
  - lib/rams/solvers/cbc.rb
28
27
  - lib/rams/solvers/clp.rb
29
- - lib/rams/solvers/cplex.rb
30
28
  - lib/rams/solvers/glpk.rb
29
+ - lib/rams/solvers/highs.rb
31
30
  - lib/rams/solvers/scip.rb
32
31
  - lib/rams/solvers/solver.rb
33
32
  - lib/rams/variable.rb
@@ -35,7 +34,6 @@ homepage: https://github.com/ryanjoneil/rams
35
34
  licenses:
36
35
  - MIT
37
36
  metadata: {}
38
- post_install_message:
39
37
  rdoc_options: []
40
38
  require_paths:
41
39
  - lib
@@ -43,16 +41,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
43
41
  requirements:
44
42
  - - ">="
45
43
  - !ruby/object:Gem::Version
46
- version: 2.4.0
44
+ version: 3.1.0
47
45
  required_rubygems_version: !ruby/object:Gem::Requirement
48
46
  requirements:
49
47
  - - ">="
50
48
  - !ruby/object:Gem::Version
51
49
  version: '0'
52
50
  requirements: []
53
- rubyforge_project:
54
- rubygems_version: 2.6.8
55
- signing_key:
51
+ rubygems_version: 3.6.2
56
52
  specification_version: 4
57
53
  summary: Ruby Algebraic Modeling System
58
54
  test_files: []
@@ -1,55 +0,0 @@
1
- require 'nokogiri'
2
- require_relative 'solver'
3
-
4
- module RAMS
5
- module Solvers
6
- # Interface to CPLEX
7
- class CPLEX < Solver
8
- def solve_and_parse(model, model_path, solution_path)
9
- call_solver model, model_path, solution_path
10
- return RAMS::Solution.new(:infeasible, nil, {}, {}) unless File.exist? solution_path
11
- parse_solution model, File.read(solution_path)
12
- end
13
-
14
- def solver_command(model_path, solution_path, args)
15
- ['cplex', '-c', "read #{model_path}"] + args + ['optimize', "write #{solution_path}"]
16
- end
17
-
18
- private
19
-
20
- def parse_solution(model, solution_text)
21
- xml_doc = Nokogiri::XML solution_text
22
- RAMS::Solution.new(
23
- parse_status(model, xml_doc),
24
- parse_objective(model, xml_doc),
25
- parse_primal(model, xml_doc),
26
- parse_dual(model, xml_doc)
27
- )
28
- end
29
-
30
- def parse_status(_model, xml_doc)
31
- status = xml_doc.css('CPLEXSolution').css('header').first['solutionStatusString']
32
- return :optimal if status =~ /optimal/i
33
- return :feasible if status =~ /feasible/i
34
- return :unbounded if status =~ /unbounded/i
35
- :unknown
36
- end
37
-
38
- def parse_objective(_model, xml_doc)
39
- xml_doc.css('CPLEXSolution').css('header').first['objectiveValue'].to_f
40
- end
41
-
42
- def parse_primal(model, xml_doc)
43
- xml_doc.css('CPLEXSolution').css('variables').css('variable').map do |v|
44
- [model.variables[v['name']], v['value'].to_f]
45
- end.to_h
46
- end
47
-
48
- def parse_dual(model, xml_doc)
49
- xml_doc.css('CPLEXSolution').css('linearConstraints').css('constraint').map do |c|
50
- [model.constraints[c['name']], c['dual'].to_f]
51
- end.to_h
52
- end
53
- end
54
- end
55
- end