rams 0.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 67e00b03b834709c1fe99786d347e930d4446738
4
+ data.tar.gz: 92e7882abc5840a724ec73a9966180150bedddd4
5
+ SHA512:
6
+ metadata.gz: a8d314f2631447ae0a8d500b5b358f55fe319cb6a60a44c21ac439a032c9a7b0504654ac14bbe1b81bc9c52687ec04e68f8d6b3d617e305f143912d8cff748ad
7
+ data.tar.gz: d9944355d64f23fd6c43ddc841e668ae9f6d272eeec906b1df9809a01df987a59514e43600528619eec3a34db7df0dafb0e6f0557542438452b3b2a5b68e3b73
@@ -0,0 +1,58 @@
1
+ module RAMS
2
+ # A RAMS::Constraint must take the form:
3
+ #
4
+ # lhs ==|<=|>= rhs
5
+ #
6
+ # lhs is a hash of variables to coefficients and rhs is a constant.
7
+ # The sense is the sense of the inequality and must be closed.
8
+ #
9
+ class Constraint
10
+ attr_reader :id, :lhs, :sense, :rhs
11
+
12
+ def initialize(lhs, sense, rhs)
13
+ @id = Variable.next_id
14
+
15
+ @lhs = lhs.dup
16
+ @sense = sense
17
+ @rhs = rhs
18
+
19
+ validate
20
+ end
21
+
22
+ def name
23
+ "c#{id}"
24
+ end
25
+
26
+ def to_s
27
+ lhs_s = lhs.map { |v, c| "#{c >= 0 ? '+' : '-'} #{c} #{v.name} " }.join
28
+ sense_s = sense == :== ? '=' : sense.to_s
29
+ "#{name}: #{lhs_s}#{sense_s} #{rhs}"
30
+ end
31
+
32
+ @next_id = 0
33
+
34
+ def self.next_id
35
+ @next_id += 1
36
+ end
37
+
38
+ private
39
+
40
+ def validate
41
+ validate_lhs
42
+ validate_sense
43
+ validate_rhs
44
+ end
45
+
46
+ def validate_lhs
47
+ raise(ArgumentError, 'invalid lhs') if lhs.empty?
48
+ end
49
+
50
+ def validate_sense
51
+ raise(ArgumentError, 'invalid sense') unless [:<=, :==, :>=].index(sense)
52
+ end
53
+
54
+ def validate_rhs
55
+ raise(ArgumentError, 'invalid rhs') unless rhs.is_a? Numeric
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,99 @@
1
+ require_relative 'constraint'
2
+
3
+ module RAMS
4
+ # A RAMS::Expression is a dot product of variables, cofficients, and a
5
+ # constant offset:
6
+ #
7
+ # 3 * x1 + 1.5 * x3 - 4
8
+ #
9
+ # Expressions must be linear. They can be added and subtracted.
10
+ #
11
+ class Expression
12
+ attr_reader :coefficients, :constant
13
+
14
+ def initialize(coefficients = {}, constant = 0.0)
15
+ @coefficients = coefficients.dup
16
+ @coefficients.default = 0.0
17
+ @constant = constant.to_f
18
+ end
19
+
20
+ def -@
21
+ Expression.new coefficients.map { |v, c| [v, -c] }.to_h, -constant
22
+ end
23
+
24
+ def +(other)
25
+ if other.is_a? Numeric
26
+ return Expression.new({}, 0.0) unless other
27
+ return Expression.new(coefficients, constant + other)
28
+ end
29
+ Expression.new add_coefficients(other), constant + other.constant
30
+ end
31
+
32
+ def -(other)
33
+ if other.is_a? Numeric
34
+ return Expression.new({}, 0.0) unless other
35
+ return Expression.new(coefficients, constant - other)
36
+ end
37
+ Expression.new add_coefficients(other, -1), constant - other.constant
38
+ end
39
+
40
+ def *(other)
41
+ if other.is_a? Numeric
42
+ return Expression.new(coefficients.map do |v, c|
43
+ [v, c * other]
44
+ end.to_h, constant * other)
45
+ end
46
+ raise NotImplementedError
47
+ end
48
+
49
+ def /(other)
50
+ if other.is_a? Numeric
51
+ return Expression.new(coefficients.map do |v, c|
52
+ [v, c / other]
53
+ end.to_h, constant / other)
54
+ end
55
+ raise NotImplementedError
56
+ end
57
+
58
+ def <=(other)
59
+ RAMS::Constraint.new(lhs(other), :<=, rhs(other))
60
+ end
61
+
62
+ def ==(other)
63
+ RAMS::Constraint.new(lhs(other), :==, rhs(other))
64
+ end
65
+
66
+ def >=(other)
67
+ RAMS::Constraint.new(lhs(other), :>=, rhs(other))
68
+ end
69
+
70
+ def to_s
71
+ vars_s = coefficients.map { |v, c| "#{c >= 0 ? '+' : '-'} #{c} #{v.name} " }.join
72
+ sign_s = constant >= 0 ? '+' : '-'
73
+ const_s = constant == 0 ? '' : "#{sign_s} #{constant}"
74
+ vars_s + const_s
75
+ end
76
+
77
+ private
78
+
79
+ def merge_variables(other)
80
+ (coefficients.keys + other.coefficients.keys).uniq
81
+ end
82
+
83
+ def add_coefficients(other, sign = +1)
84
+ vars = merge_variables(other)
85
+ vars.map do |v|
86
+ [v, coefficients[v] + (sign * other.coefficients[v])]
87
+ end.to_h
88
+ end
89
+
90
+ def lhs(other)
91
+ (self - other).coefficients
92
+ end
93
+
94
+ def rhs(other)
95
+ return other - constant if other.is_a? Numeric
96
+ other.constant - constant
97
+ end
98
+ end
99
+ end
data/lib/rams/model.rb ADDED
@@ -0,0 +1,92 @@
1
+ require 'tempfile'
2
+ require_relative 'variable'
3
+ require_relative 'solvers/glpk'
4
+
5
+ module RAMS
6
+ # A Model is a collection of:
7
+ #
8
+ # * Variables
9
+ # * Constraints
10
+ # * An objective function and sense
11
+ #
12
+ # An example of a simple model:
13
+ #
14
+ # m = RAMS::Model.new
15
+ # x1 = m.variable(type: :binary)
16
+ # x2 = m.variable
17
+ # m.constrain(x1 + x2 <= 1)
18
+ #
19
+ # Models can be maximized or minimized by different solvers.
20
+ #
21
+ # m.sense = :max
22
+ # m.objective = (x1 + (2 * x2))
23
+ # m.solver = :glpk
24
+ # m.verbose = true
25
+ # m.solve
26
+ #
27
+ class Model
28
+ attr_accessor :objective, :args, :verbose
29
+ attr_reader :solver, :sense, :variables, :constraints
30
+
31
+ SOLVERS = { glpk: RAMS::Solvers::GLPK.new }.freeze
32
+
33
+ def initialize
34
+ @solver = :glpk
35
+ @sense = :max
36
+ @objective = nil
37
+ @verbose = false
38
+ @args = []
39
+ @variables = {}
40
+ @constraints = {}
41
+ end
42
+
43
+ def solver=(solver)
44
+ raise(ArgumentError, "valid solvers: #{SOLVERS.keys.join(' ')}") if SOLVERS[solver].nil?
45
+ @solver = solver
46
+ end
47
+
48
+ def sense=(sense)
49
+ raise(ArgumentError, 'sense must be :min or :max') unless sense == :min || sense == :max
50
+ @sense = sense
51
+ end
52
+
53
+ def variable(low: 0.0, high: nil, type: :continuous)
54
+ v = Variable.new low: low, high: high, type: type
55
+ variables[v.name] = v
56
+ end
57
+
58
+ def constrain(constraint)
59
+ constraints[constraint.name] = constraint
60
+ end
61
+
62
+ def solve
63
+ raise(ArgumentError, 'model has no variables') if variables.empty?
64
+ raise(ArgumentError, 'model has no constraints') if constraints.empty?
65
+ SOLVERS[solver].solve self
66
+ end
67
+
68
+ # rubocop:disable AbcSize
69
+ def to_s
70
+ <<-LP
71
+ #{sense}
72
+ obj: #{feasible_objective.is_a?(Variable) ? feasible_objective.name : feasible_objective}
73
+ st
74
+ #{constraints.values.map(&:to_s).join("\n ")}
75
+ bounds
76
+ #{variables.values.map(&:to_s).join("\n ")}
77
+ general
78
+ #{variables.values.select { |v| v.type == :integer }.map(&:name).join("\n ")}
79
+ binary
80
+ #{variables.values.select { |v| v.type == :binary }.map(&:name).join("\n ")}
81
+ end
82
+ LP
83
+ end
84
+ # rubocop:enable AbcSize
85
+
86
+ private
87
+
88
+ def feasible_objective
89
+ objective || variables.values.first
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,59 @@
1
+ # Fixnums can be added to expressions or variables or multiplied
2
+ # by them to create expressions:
3
+ #
4
+ # 4 - (3 * x)
5
+ # y / 2
6
+ class Fixnum
7
+ alias old_add +
8
+ alias old_sub -
9
+ alias old_multiply *
10
+ alias old_divide /
11
+
12
+ def +(other)
13
+ return other + self if other.is_a? RAMS::Expression
14
+ old_add other
15
+ end
16
+
17
+ def -(other)
18
+ return -other + self if other.is_a? RAMS::Expression
19
+ old_sub other
20
+ end
21
+
22
+ def *(other)
23
+ return other * self if other.is_a? RAMS::Expression
24
+ old_multiply other
25
+ end
26
+
27
+ def /(other)
28
+ return other * (1.0 / self) if other.is_a? RAMS::Expression
29
+ old_divide other
30
+ end
31
+ end
32
+
33
+ # Floats can be treated the same way as Fixnums.
34
+ class Float
35
+ alias old_add +
36
+ alias old_sub -
37
+ alias old_multiply *
38
+ alias old_divide /
39
+
40
+ def +(other)
41
+ return other + self if other.is_a? RAMS::Expression
42
+ old_add other
43
+ end
44
+
45
+ def -(other)
46
+ return -other + self if other.is_a? RAMS::Expression
47
+ old_sub other
48
+ end
49
+
50
+ def *(other)
51
+ return other * self if other.is_a? RAMS::Expression
52
+ old_multiply other
53
+ end
54
+
55
+ def /(other)
56
+ return other * (1.0 / self) if other.is_a? RAMS::Expression
57
+ old_divide other
58
+ end
59
+ end
@@ -0,0 +1,23 @@
1
+ module RAMS
2
+ # A Solution contains the output running a model through a solver:
3
+ #
4
+ # * Solution status
5
+ # * Objective value
6
+ # * Primal variable values
7
+ # * Dual prices
8
+ #
9
+ class Solution
10
+ attr_reader :status, :objective, :primal, :dual
11
+
12
+ def initialize(status, objective, primal, dual)
13
+ @status = status
14
+ @objective = objective
15
+ @primal = primal
16
+ @dual = dual
17
+ end
18
+
19
+ def [](variable)
20
+ primal[variable]
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,40 @@
1
+ require_relative 'solver'
2
+
3
+ module RAMS
4
+ module Solvers
5
+ # Interface to the GNU Linear Programming Kit
6
+ class GLPK < Solver
7
+ def solver_command(model_file, solution_file)
8
+ ['glpsol', '--lp', model_file.path, '--output', solution_file.path]
9
+ end
10
+
11
+ private
12
+
13
+ def parse_status(lines)
14
+ status = lines.select { |l| l =~ /^Status/ }.first
15
+ return :optimal if status =~ /OPTIMAL/
16
+ return :feasible if status =~ /FEASIBLE/
17
+ return :infeasible if status =~ /EMPTY/
18
+ :undefined
19
+ end
20
+
21
+ def parse_objective(lines)
22
+ lines.select { |l| l =~ /^Objective/ }.first.split[3].to_f
23
+ end
24
+
25
+ def parse_primal(model, lines)
26
+ primal = model.variables.values.map { |v| [v, 0.0] }.to_h
27
+ start_idx = lines.index { |l| l =~ /Column name/ } + 2
28
+ length = lines[start_idx, lines.length].index { |l| l == '' }
29
+ primal.update(lines[start_idx, length].map { |l| [model.variables[l[7, 12].strip], l[23, 13].to_f] }.to_h)
30
+ end
31
+
32
+ def parse_dual(model, lines)
33
+ duals = model.constraints.values.map { |c| [c, 0.0] }.to_h
34
+ start_idx = lines.index { |l| l =~ /Row name/ } + 2
35
+ length = lines[start_idx, lines.length].index { |l| l == '' }
36
+ duals.update(lines[start_idx, length].map { |l| [model.constraints[l[7, 12].strip], l[-13, 13].to_f] }.to_h)
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,88 @@
1
+ require 'open3'
2
+ require_relative '../solution'
3
+
4
+ module RAMS
5
+ module Solvers
6
+ # Generic solver interface
7
+ class Solver
8
+ def solve(model)
9
+ model_file = write_model_file model
10
+ begin
11
+ get_solution model, model_file
12
+ ensure
13
+ model_file.unlink
14
+ end
15
+ end
16
+
17
+ private
18
+
19
+ def write_model_file(model)
20
+ model_file = Tempfile.new ['', '.lp']
21
+ model_file.write model.to_s
22
+ model_file.close
23
+ model_file
24
+ end
25
+
26
+ def get_solution(model, model_file)
27
+ solution_file = Tempfile.new ['', '.sol']
28
+ begin
29
+ solve_and_parse model, model_file, solution_file
30
+ ensure
31
+ solution_file.unlink
32
+ end
33
+ end
34
+
35
+ def solve_and_parse(model, model_file, solution_file)
36
+ call_solver model, model_file, solution_file
37
+ parse_solution model, File.read(solution_file)
38
+ end
39
+
40
+ # rubocop:disable MethodLength
41
+ def call_solver(model, model_file, solution_file)
42
+ command = solver_command(model_file, solution_file) + model.args
43
+ _, stdout, stderr, exit_code = Open3.popen3(*command)
44
+
45
+ begin
46
+ output = stdout.gets(nil) || ''
47
+ error = output + (stderr.gets(nil) || '')
48
+ puts output if model.verbose && output != ''
49
+ raise error unless exit_code.value == 0
50
+ ensure
51
+ stdout.close
52
+ stderr.close
53
+ end
54
+ end
55
+ # rubocop:enable MethodLength
56
+
57
+ def solver_command(_model_file, _solution_file)
58
+ raise NotImplementedError
59
+ end
60
+
61
+ def parse_solution(model, solution_text)
62
+ lines = solution_text.split "\n"
63
+ RAMS::Solution.new(
64
+ parse_status(lines),
65
+ parse_objective(lines),
66
+ parse_primal(model, lines),
67
+ parse_dual(model, lines)
68
+ )
69
+ end
70
+
71
+ def parse_status(_lines)
72
+ raise NotImplementedError
73
+ end
74
+
75
+ def parse_objective(_lines)
76
+ raise NotImplementedError
77
+ end
78
+
79
+ def parse_primal(_model, _lines)
80
+ raise NotImplementedError
81
+ end
82
+
83
+ def parse_dual(_model, _lines)
84
+ raise NotImplementedError
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,48 @@
1
+ require_relative 'expression'
2
+
3
+ module RAMS
4
+ # A Variable has bounds and a type. Names are created automatically.
5
+ #
6
+ # RAMS::Variable.new # continuous, >= 0
7
+ # RAMS::Variable.new low: nil # continuous, unbounded
8
+ # RAMS::Variable.new low: 1, high: 2.5 # continuous, >= 1, <= 2.5
9
+ # RAMS::Variable.new type: :binary # binary variable
10
+ # RAMS::Variable.new type: :integer # integer variable, >= 0
11
+ #
12
+ class Variable < Expression
13
+ attr_reader :id, :low, :high, :type
14
+
15
+ def initialize(low: 0.0, high: nil, type: :continuous)
16
+ @id = Variable.next_id
17
+
18
+ @low = low
19
+ @high = high
20
+ @type = type
21
+
22
+ super({ self => 1.0 })
23
+ end
24
+
25
+ def name
26
+ "v#{id}"
27
+ end
28
+
29
+ def to_s
30
+ return to_s_binary if type == :binary
31
+ "#{low.nil? ? '-inf' : low} <= #{name} <= #{high.nil? ? '+inf' : high}"
32
+ end
33
+
34
+ @next_id = 0
35
+
36
+ def self.next_id
37
+ @next_id += 1
38
+ end
39
+
40
+ private
41
+
42
+ def to_s_binary
43
+ low_s = low.nil? ? 0.0 : [0.0, low].max
44
+ high_s = high.nil? ? 1.0 : [1.0, high].min
45
+ "#{low_s} <= #{name} <= #{high_s}"
46
+ end
47
+ end
48
+ end
data/lib/rams.rb ADDED
@@ -0,0 +1,5 @@
1
+ require_relative 'rams/constraint'
2
+ require_relative 'rams/expression'
3
+ require_relative 'rams/model'
4
+ require_relative 'rams/numeric'
5
+ require_relative 'rams/variable'
metadata ADDED
@@ -0,0 +1,53 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rams
3
+ version: !ruby/object:Gem::Version
4
+ version: '0.1'
5
+ platform: ruby
6
+ authors:
7
+ - Ryan J. O'Neil
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-01-07 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: A library for solving MILPs in Ruby.
14
+ email:
15
+ - ryanjoneil@gmail.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - lib/rams.rb
21
+ - lib/rams/constraint.rb
22
+ - lib/rams/expression.rb
23
+ - lib/rams/model.rb
24
+ - lib/rams/numeric.rb
25
+ - lib/rams/solution.rb
26
+ - lib/rams/solvers/glpk.rb
27
+ - lib/rams/solvers/solver.rb
28
+ - lib/rams/variable.rb
29
+ homepage: https://github.com/ryanjoneil/rams
30
+ licenses:
31
+ - MIT
32
+ metadata: {}
33
+ post_install_message:
34
+ rdoc_options: []
35
+ require_paths:
36
+ - lib
37
+ required_ruby_version: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: '0'
42
+ required_rubygems_version: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ requirements: []
48
+ rubyforge_project:
49
+ rubygems_version: 2.4.5.1
50
+ signing_key:
51
+ specification_version: 4
52
+ summary: Ruby Algebraic Modeling System
53
+ test_files: []