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 +7 -0
- data/lib/rams/constraint.rb +58 -0
- data/lib/rams/expression.rb +99 -0
- data/lib/rams/model.rb +92 -0
- data/lib/rams/numeric.rb +59 -0
- data/lib/rams/solution.rb +23 -0
- data/lib/rams/solvers/glpk.rb +40 -0
- data/lib/rams/solvers/solver.rb +88 -0
- data/lib/rams/variable.rb +48 -0
- data/lib/rams.rb +5 -0
- metadata +53 -0
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
|
data/lib/rams/numeric.rb
ADDED
@@ -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
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: []
|