rams 0.1.7 → 0.2.0
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/lib/rams/model.rb +4 -4
- data/lib/rams/solvers/cbc.rb +1 -1
- data/lib/rams/solvers/clp.rb +1 -1
- data/lib/rams/solvers/glpk.rb +1 -1
- data/lib/rams/solvers/highs.rb +94 -0
- data/lib/rams/solvers/scip.rb +1 -1
- data/lib/rams/solvers/solver.rb +5 -0
- metadata +3 -3
- data/lib/rams/solvers/cplex.rb +0 -55
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 51df1f0581f9e3537db77282d196e48f2f0c36a89dabf596117894bbc071fd27
|
4
|
+
data.tar.gz: 2c05958f5ce73828097854aa0b3084af2e208b6112ffa97eac01e29daf3d7289
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5d3fe18d6745528aa1296bdc02dcab81e0cab9536856ba14110a1baa94fddec213db9d69d0915d9fc1c7ceac2c770057891f1f309f3e62243aef45c9e23d1455
|
7
|
+
data.tar.gz: 9607509c99f7476ba6853a56a965ed3ffd8fa6cd024f9c9bc7bcfae9e5b030f961afbdfe8959481cbd8df07a805333018227f486b9567e8f2000fe4c8657340a
|
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
|
|
@@ -26,7 +26,7 @@ module RAMS
|
|
26
26
|
#
|
27
27
|
# m.sense = :max
|
28
28
|
# m.objective = (x1 + (2 * x2))
|
29
|
-
# m.solver = :
|
29
|
+
# m.solver = :highs
|
30
30
|
# m.verbose = true
|
31
31
|
# m.solve
|
32
32
|
#
|
@@ -38,13 +38,13 @@ 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
|
|
46
46
|
def initialize
|
47
|
-
@solver = :
|
47
|
+
@solver = :highs
|
48
48
|
@sense = :max
|
49
49
|
@objective = nil
|
50
50
|
@verbose = false
|
data/lib/rams/solvers/cbc.rb
CHANGED
@@ -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
|
-
['coin.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
|
data/lib/rams/solvers/clp.rb
CHANGED
@@ -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
|
data/lib/rams/solvers/glpk.rb
CHANGED
@@ -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
|
data/lib/rams/solvers/scip.rb
CHANGED
@@ -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
|
|
data/lib/rams/solvers/solver.rb
CHANGED
@@ -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,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rams
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ryan J. O'Neil
|
@@ -11,7 +11,7 @@ date: 2025-06-24 00:00:00.000000000 Z
|
|
11
11
|
dependencies: []
|
12
12
|
description: A library for solving MILPs in Ruby.
|
13
13
|
email:
|
14
|
-
-
|
14
|
+
- adventures-in-opt.1ba50@passmail.net
|
15
15
|
executables: []
|
16
16
|
extensions: []
|
17
17
|
extra_rdoc_files: []
|
@@ -25,8 +25,8 @@ files:
|
|
25
25
|
- lib/rams/solution.rb
|
26
26
|
- lib/rams/solvers/cbc.rb
|
27
27
|
- lib/rams/solvers/clp.rb
|
28
|
-
- lib/rams/solvers/cplex.rb
|
29
28
|
- lib/rams/solvers/glpk.rb
|
29
|
+
- lib/rams/solvers/highs.rb
|
30
30
|
- lib/rams/solvers/scip.rb
|
31
31
|
- lib/rams/solvers/solver.rb
|
32
32
|
- lib/rams/variable.rb
|
data/lib/rams/solvers/cplex.rb
DELETED
@@ -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
|