opl 0.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/opl.rb +405 -0
- metadata +48 -0
data/lib/opl.rb
ADDED
@@ -0,0 +1,405 @@
|
|
1
|
+
require "rglpk"
|
2
|
+
|
3
|
+
#TODO
|
4
|
+
#my next goal should be handling all basic constraints
|
5
|
+
#forget foralls and sums for a second
|
6
|
+
#just allow the user to write a linear
|
7
|
+
#model in a pleasant syntax
|
8
|
+
#make sure extreme cases of foralls and sums
|
9
|
+
#are handled
|
10
|
+
#need to be able to handle arithmetic operations
|
11
|
+
#within a constraint or index
|
12
|
+
#e.g. sum(i in (1..3), x[i-1])
|
13
|
+
#a matrix representation of the solution if using
|
14
|
+
#sub notation
|
15
|
+
#multiple level sub notation e.g. x[1][[3]]
|
16
|
+
#all relationships (<, >, =, <=, >=)
|
17
|
+
#constants in constraints and objectives
|
18
|
+
#float coefficients and constants
|
19
|
+
#write as module
|
20
|
+
|
21
|
+
def sides(equation)
|
22
|
+
if equation.include?("<")
|
23
|
+
char = "<="
|
24
|
+
elsif equation.include?(">")
|
25
|
+
char = ">="
|
26
|
+
elsif equation.include?("<=")
|
27
|
+
char = "<"
|
28
|
+
elsif equation.include?("<=")
|
29
|
+
char = ">"
|
30
|
+
elsif equation.include?("=")
|
31
|
+
char = "="
|
32
|
+
end
|
33
|
+
sides = equation.split(char)
|
34
|
+
{:lhs => sides[0], :rhs => sides[1]}
|
35
|
+
end
|
36
|
+
|
37
|
+
def add_ones(equation)
|
38
|
+
equation = "#"+equation
|
39
|
+
equation.scan(/[#+-][a-z]/).each do |p|
|
40
|
+
if p.include?("+")
|
41
|
+
q = p.gsub("+", "+1*")
|
42
|
+
elsif p.include?("-")
|
43
|
+
q = p.gsub("-","-1*")
|
44
|
+
elsif p.include?("#")
|
45
|
+
q = p.gsub("#","#1*")
|
46
|
+
end
|
47
|
+
equation = equation.gsub(p,q)
|
48
|
+
end
|
49
|
+
equation.gsub("#","")
|
50
|
+
end
|
51
|
+
|
52
|
+
def paren_to_array(text)
|
53
|
+
#in: "(2..5)"
|
54
|
+
#out: "[2,3,4,5]"
|
55
|
+
start = text[1].to_i
|
56
|
+
stop = text[-2].to_i
|
57
|
+
(start..stop).map{|i|i}.to_s
|
58
|
+
end
|
59
|
+
|
60
|
+
def sub_paren_with_array(text)
|
61
|
+
targets = text.scan(/\([\d]+\.\.[\d]+\)/)
|
62
|
+
targets.each do |target|
|
63
|
+
text = text.gsub(target, paren_to_array(target))
|
64
|
+
end
|
65
|
+
return(text)
|
66
|
+
end
|
67
|
+
|
68
|
+
def mass_product(array_of_arrays, base=[])
|
69
|
+
return(base) if array_of_arrays.empty?
|
70
|
+
array = array_of_arrays[0]
|
71
|
+
new_array_of_arrays = array_of_arrays[1..-1]
|
72
|
+
if base==[]
|
73
|
+
mass_product(new_array_of_arrays, array)
|
74
|
+
else
|
75
|
+
mass_product(new_array_of_arrays, base.product(array).map{|e|e.flatten})
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def forall(text)
|
80
|
+
#need to be able to handle sums inside here
|
81
|
+
#in: "i in (0..2), x[i] <= 5"
|
82
|
+
#out: ["x[0] <= 5", "x[1] <= 5", "x[2] <= 5"]
|
83
|
+
text = sub_paren_with_array(text)
|
84
|
+
final_constraints = []
|
85
|
+
indices = text.scan(/[a-z] in/).map{|sc|sc[0]}
|
86
|
+
values = text.scan(/\s\[[\-\s\d+,]+\]/).map{|e|e.gsub(" ", "").scan(/[\-\d]+/)}
|
87
|
+
index_value_pairs = indices.zip(values)
|
88
|
+
variable = text.scan(/[a-z]\[/)[0].gsub("[","")
|
89
|
+
#will need to make this multiple variables??
|
90
|
+
#or is this even used at all????
|
91
|
+
value_combinations = mass_product(values)
|
92
|
+
value_combinations.each_index do |vc_index|
|
93
|
+
value_combination = value_combinations[vc_index]
|
94
|
+
value_combination = [value_combination] unless value_combination.is_a?(Array)
|
95
|
+
if text.include?("sum")
|
96
|
+
constraint = "sum"+text.split("sum")[1..-1].join("sum")
|
97
|
+
else
|
98
|
+
constraint = text.split(",")[-1].gsub(" ","")
|
99
|
+
end
|
100
|
+
e = constraint
|
101
|
+
value_combination.each_index do |i|
|
102
|
+
index = indices[i]
|
103
|
+
value = value_combination[i]
|
104
|
+
e = e.gsub("("+index, "("+value)
|
105
|
+
e = e.gsub(index+")", value+")")
|
106
|
+
e = e.gsub("["+index, "["+value)
|
107
|
+
e = e.gsub(index+"]", value+"]")
|
108
|
+
e = e.gsub("=>"+index, "=>"+value)
|
109
|
+
e = e.gsub("<="+index, "<="+value)
|
110
|
+
e = e.gsub(">"+index, ">"+value)
|
111
|
+
e = e.gsub("<"+index, "<"+value)
|
112
|
+
e = e.gsub("="+index, "="+value)
|
113
|
+
e = e.gsub("=> "+index, "=> "+value)
|
114
|
+
e = e.gsub("<= "+index, "<= "+value)
|
115
|
+
e = e.gsub("> "+index, "> "+value)
|
116
|
+
e = e.gsub("< "+index, "< "+value)
|
117
|
+
e = e.gsub("= "+index, "= "+value)
|
118
|
+
end
|
119
|
+
final_constraints += [e]
|
120
|
+
end
|
121
|
+
final_constraints
|
122
|
+
end
|
123
|
+
|
124
|
+
def sub_forall(equation, indexvalues={:indices => [], :values => []})
|
125
|
+
#in: "forall(i in (0..2), x[i] <= 5)"
|
126
|
+
#out: ["x[0] <= 5", "x[1] <= 5", "x[2] <= 5"]
|
127
|
+
return equation unless equation.include?("forall")
|
128
|
+
foralls = (equation+"#").split("forall(").map{|ee|ee.split(")")[0..-2].join(")")}.find_all{|eee|eee!=""}
|
129
|
+
constraints = []
|
130
|
+
if foralls.empty?
|
131
|
+
return(equation)
|
132
|
+
else
|
133
|
+
foralls.each do |text|
|
134
|
+
constraints << forall(text)
|
135
|
+
end
|
136
|
+
return(constraints.flatten)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def sum(text, indexvalues={:indices => [], :values => []})
|
141
|
+
#in: "i in [0,1], j in [4,-5], 3x[i][j]"
|
142
|
+
#out: "3x[0][4] + 3x[0][-5] + 3x[1][4] + 3x[1][-5]"
|
143
|
+
text = sub_paren_with_array(text)
|
144
|
+
final_text = ""
|
145
|
+
element = text.split(",")[-1].gsub(" ","")
|
146
|
+
indices = text.scan(/[a-z] in/).map{|sc|sc[0]}
|
147
|
+
input_indices = indexvalues[:indices] - indices
|
148
|
+
if not input_indices.empty?
|
149
|
+
input_values = input_indices.map{|ii|indexvalues[:values][indexvalues[:indices].index(ii)]}
|
150
|
+
else
|
151
|
+
input_values = []
|
152
|
+
end
|
153
|
+
values = text.scan(/\s\[[\-\s\d+,]+\]/).map{|e|e.gsub(" ", "").scan(/[\-\d]+/)}
|
154
|
+
indices += input_indices
|
155
|
+
values += input_values
|
156
|
+
index_value_pairs = indices.zip(values)
|
157
|
+
variable = text.scan(/[a-z]\[/)[0].gsub("[","")
|
158
|
+
coefficient_a = text.split(",")[-1].split("[")[0].scan(/\-?[\d\*]+[a-z]/)
|
159
|
+
if coefficient_a.empty?
|
160
|
+
if text.split(",")[-1].split("[")[0].include?("-")
|
161
|
+
coefficient = "-1"
|
162
|
+
else
|
163
|
+
coefficient = "1"
|
164
|
+
end
|
165
|
+
else
|
166
|
+
coefficient = coefficient_a[0].scan(/[\d\-]+/)
|
167
|
+
end
|
168
|
+
value_combinations = mass_product(values)
|
169
|
+
value_combinations.each_index do |vc_index|
|
170
|
+
value_combination = value_combinations[vc_index]
|
171
|
+
e = element
|
172
|
+
value_combination = [value_combination] unless value_combination.is_a?(Array)
|
173
|
+
value_combination.each_index do |i|
|
174
|
+
index = indices[i]
|
175
|
+
value = value_combination[i]
|
176
|
+
e = e.gsub("("+index, "("+value)
|
177
|
+
e = e.gsub(index+")", value+")")
|
178
|
+
e = e.gsub("["+index, "["+value)
|
179
|
+
e = e.gsub(index+"]", value+"]")
|
180
|
+
e = e.gsub("=>"+index, "=>"+value)
|
181
|
+
e = e.gsub("<="+index, "<="+value)
|
182
|
+
e = e.gsub(">"+index, ">"+value)
|
183
|
+
e = e.gsub("<"+index, "<"+value)
|
184
|
+
e = e.gsub("="+index, "="+value)
|
185
|
+
e = e.gsub("=> "+index, "=> "+value)
|
186
|
+
e = e.gsub("<= "+index, "<= "+value)
|
187
|
+
e = e.gsub("> "+index, "> "+value)
|
188
|
+
e = e.gsub("< "+index, "< "+value)
|
189
|
+
e = e.gsub("= "+index, "= "+value)
|
190
|
+
end
|
191
|
+
e = "+"+e unless (coefficient.include?("-") || vc_index==0)
|
192
|
+
final_text += e
|
193
|
+
end
|
194
|
+
final_text
|
195
|
+
end
|
196
|
+
|
197
|
+
def sub_sum(equation, indexvalues={:indices => [], :values => []})
|
198
|
+
#in: "sum(i in (0..3), x[i]) <= 100"
|
199
|
+
#out: "x[0]+x[1]+x[2]+x[3] <= 100"
|
200
|
+
sums = (equation+"#").split("sum(").map{|ee|ee.split(")")[0..-2].join(")")}.find_all{|eee|eee!=""}.find_all{|eeee|!eeee.include?("forall")}
|
201
|
+
sums.each do |text|
|
202
|
+
e = text
|
203
|
+
unless indexvalues[:indices].empty?
|
204
|
+
indexvalues[:indices].each_index do |i|
|
205
|
+
index = indexvalues[:indices][i]
|
206
|
+
value = indexvalues[:values][i].to_s
|
207
|
+
e = e.gsub("("+index, "("+value)
|
208
|
+
e = e.gsub(index+")", value+")")
|
209
|
+
e = e.gsub("["+index, "["+value)
|
210
|
+
e = e.gsub(index+"]", value+"]")
|
211
|
+
e = e.gsub("=>"+index, "=>"+value)
|
212
|
+
e = e.gsub("<="+index, "<="+value)
|
213
|
+
e = e.gsub(">"+index, ">"+value)
|
214
|
+
e = e.gsub("<"+index, "<"+value)
|
215
|
+
e = e.gsub("="+index, "="+value)
|
216
|
+
e = e.gsub("=> "+index, "=> "+value)
|
217
|
+
e = e.gsub("<= "+index, "<= "+value)
|
218
|
+
e = e.gsub("> "+index, "> "+value)
|
219
|
+
e = e.gsub("< "+index, "< "+value)
|
220
|
+
e = e.gsub("= "+index, "= "+value)
|
221
|
+
end
|
222
|
+
end
|
223
|
+
equation = equation.gsub(text, e)
|
224
|
+
result = sum(text)
|
225
|
+
equation = equation.gsub("sum("+text+")", result)
|
226
|
+
end
|
227
|
+
return(equation)
|
228
|
+
end
|
229
|
+
|
230
|
+
def coefficients(equation)#parameter is one side of the equation
|
231
|
+
equation = add_ones(equation)
|
232
|
+
if equation[0]=="-"
|
233
|
+
equation.scan(/[+-]\d+/)
|
234
|
+
else
|
235
|
+
("#"+equation).scan(/[#+-]\d+/).map{|e|e.gsub("#","+")}
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
def variables(equation)#parameter is one side of the equation
|
240
|
+
equation = add_ones(equation)
|
241
|
+
equation.scan(/[a-z]+[\[\]\d]*/)
|
242
|
+
end
|
243
|
+
|
244
|
+
class LinearProgram
|
245
|
+
attr_accessor :objective
|
246
|
+
attr_accessor :constraints
|
247
|
+
attr_accessor :rows
|
248
|
+
attr_accessor :solution
|
249
|
+
|
250
|
+
def initialize(objective, constraints)
|
251
|
+
@objective = objective
|
252
|
+
@constraints = constraints
|
253
|
+
@rows = []
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
class Objective
|
258
|
+
attr_accessor :function
|
259
|
+
attr_accessor :optimization#minimize, maximize, equals
|
260
|
+
attr_accessor :variable_coefficient_pairs
|
261
|
+
|
262
|
+
def initialize(function, optimization)
|
263
|
+
@function = function
|
264
|
+
@optimization = optimization
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
class Row
|
269
|
+
attr_accessor :name
|
270
|
+
attr_accessor :constraint
|
271
|
+
attr_accessor :lower_bound
|
272
|
+
attr_accessor :upper_bound
|
273
|
+
attr_accessor :variable_coefficient_pairs
|
274
|
+
|
275
|
+
def initialize(name, lower_bound, upper_bound)
|
276
|
+
@name = name
|
277
|
+
@lower_bound = lower_bound
|
278
|
+
@upper_bound = upper_bound
|
279
|
+
@variable_coefficient_pairs = []
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
class VariableCoefficientPair
|
284
|
+
attr_accessor :variable
|
285
|
+
attr_accessor :coefficient
|
286
|
+
|
287
|
+
def initialize(variable, coefficient)
|
288
|
+
@variable = variable
|
289
|
+
@coefficient = coefficient
|
290
|
+
end
|
291
|
+
end
|
292
|
+
|
293
|
+
def get_all_vars(constraints)
|
294
|
+
all_vars = []
|
295
|
+
constraints.each do |constraint|
|
296
|
+
constraint = constraint.gsub(" ", "")
|
297
|
+
value = constraint.split(":")[1] || constraint
|
298
|
+
all_vars << variables(value)
|
299
|
+
end
|
300
|
+
all_vars.flatten.uniq
|
301
|
+
end
|
302
|
+
|
303
|
+
def subject_to(constraints)
|
304
|
+
constraints = constraints.flatten
|
305
|
+
constraints = constraints.map do |constraint|
|
306
|
+
sub_forall(constraint)
|
307
|
+
end.flatten
|
308
|
+
constraints = constraints.map do |constraint|
|
309
|
+
sub_sum(constraint)
|
310
|
+
end
|
311
|
+
all_vars = get_all_vars(constraints)
|
312
|
+
rows = []
|
313
|
+
constraints.each do |constraint|
|
314
|
+
negate = false
|
315
|
+
constraint = constraint.gsub(" ", "")
|
316
|
+
name = constraint.split(":")[0]
|
317
|
+
value = constraint.split(":")[1] || constraint
|
318
|
+
if value.include?("<=")
|
319
|
+
upper_bound = value.split("<=")[1]
|
320
|
+
elsif value.include?(">=")
|
321
|
+
negate = true
|
322
|
+
bound = value.split(">=")[1].to_i
|
323
|
+
upper_bound = (bound*-1).to_s
|
324
|
+
end
|
325
|
+
coefs = coefficients(sides(value)[:lhs])
|
326
|
+
if negate
|
327
|
+
coefs = coefs.map do |coef|
|
328
|
+
if coef.include?("+")
|
329
|
+
coef.gsub("+", "-")
|
330
|
+
elsif coef.include?("-")
|
331
|
+
coef.gsub("-", "+")
|
332
|
+
end
|
333
|
+
end
|
334
|
+
end
|
335
|
+
vars = variables(sides(value)[:lhs])
|
336
|
+
zero_coef_vars = all_vars - vars
|
337
|
+
row = Row.new(name, nil, upper_bound)
|
338
|
+
row.constraint = constraint
|
339
|
+
coefs = coefs + zero_coef_vars.map{|z|0}
|
340
|
+
vars = vars + zero_coef_vars
|
341
|
+
zipped = vars.zip(coefs)
|
342
|
+
pairs = []
|
343
|
+
all_vars.each do |var|
|
344
|
+
coef = coefs[vars.index(var)]
|
345
|
+
pairs << VariableCoefficientPair.new(var, coef)
|
346
|
+
end
|
347
|
+
row.variable_coefficient_pairs = pairs
|
348
|
+
rows << row
|
349
|
+
end
|
350
|
+
rows
|
351
|
+
end
|
352
|
+
|
353
|
+
def maximize(objective, rows_c)#objective function has no = in it
|
354
|
+
optimize("maximize", objective, rows_c)
|
355
|
+
end
|
356
|
+
|
357
|
+
def minimize(objective, rows_c)#objective function has no = in it
|
358
|
+
optimize("minimize", objective, rows_c)
|
359
|
+
end
|
360
|
+
|
361
|
+
def optimize(optimization, objective, rows_c)
|
362
|
+
lp = LinearProgram.new(objective, rows_c.map{|row|row.constraint})
|
363
|
+
objective = sub_sum(objective)
|
364
|
+
lp.rows = rows_c
|
365
|
+
p = Rglpk::Problem.new
|
366
|
+
p.name = "sample"
|
367
|
+
if optimization == "maximize"
|
368
|
+
p.obj.dir = Rglpk::GLP_MAX
|
369
|
+
elsif optimization == "minimize"
|
370
|
+
p.obj.dir = Rglpk::GLP_MIN
|
371
|
+
end
|
372
|
+
rows = p.add_rows(rows_c.size)
|
373
|
+
rows_c.each_index do |i|
|
374
|
+
row = rows_c[i]
|
375
|
+
rows[i].name = row.name
|
376
|
+
rows[i].set_bounds(Rglpk::GLP_UP, 0.0, row.upper_bound) unless row.upper_bound.nil?
|
377
|
+
rows[i].set_bounds(Rglpk::GLP_LO, 0.0, row.lower_bound) unless row.lower_bound.nil?
|
378
|
+
end
|
379
|
+
vars = rows_c.first.variable_coefficient_pairs.map{|vcp|vcp.variable}
|
380
|
+
cols = p.add_cols(vars.size)
|
381
|
+
vars.each_index do |i|
|
382
|
+
column_name = vars[i]
|
383
|
+
cols[i].name = column_name
|
384
|
+
cols[i].set_bounds(Rglpk::GLP_LO, 0.0, 0.0)
|
385
|
+
end
|
386
|
+
all_vars = rows_c.first.variable_coefficient_pairs.map{|vcp|vcp.variable}
|
387
|
+
obj_coefficients = coefficients(objective.gsub(" ","")).map{|c|c.to_i}
|
388
|
+
obj_vars = variables(objective.gsub(" ",""))
|
389
|
+
all_obj_coefficients = []
|
390
|
+
all_vars.each do |var|
|
391
|
+
i = obj_vars.index(var)
|
392
|
+
coef = i.nil? ? 0 : obj_coefficients[i]
|
393
|
+
all_obj_coefficients << coef
|
394
|
+
end
|
395
|
+
p.obj.coefs = all_obj_coefficients
|
396
|
+
p.set_matrix(rows_c.map{|row|row.variable_coefficient_pairs.map{|vcp|vcp.coefficient.to_i}}.flatten)
|
397
|
+
p.simplex
|
398
|
+
z = p.obj.get
|
399
|
+
answer = Hash.new()
|
400
|
+
cols.each do |c|
|
401
|
+
answer[c.name] = c.get_prim.to_s
|
402
|
+
end
|
403
|
+
lp.solution = answer
|
404
|
+
lp
|
405
|
+
end
|
metadata
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: opl
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Benjamin Godlove
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-08-18 00:00:00.000000000 Z
|
13
|
+
dependencies: []
|
14
|
+
description: Built on top of the glpk gem for linear programming. The syntax is copied
|
15
|
+
from OPL Studio, which remains my favorite linear programming software, but the
|
16
|
+
license is quite expensive.
|
17
|
+
email: bgodlove88@gmail.com
|
18
|
+
executables: []
|
19
|
+
extensions: []
|
20
|
+
extra_rdoc_files: []
|
21
|
+
files:
|
22
|
+
- lib/opl.rb
|
23
|
+
homepage: http://rubygems.org/gems/opl
|
24
|
+
licenses:
|
25
|
+
- GNU
|
26
|
+
post_install_message:
|
27
|
+
rdoc_options: []
|
28
|
+
require_paths:
|
29
|
+
- lib
|
30
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
31
|
+
none: false
|
32
|
+
requirements:
|
33
|
+
- - ! '>='
|
34
|
+
- !ruby/object:Gem::Version
|
35
|
+
version: '0'
|
36
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
37
|
+
none: false
|
38
|
+
requirements:
|
39
|
+
- - ! '>='
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: '0'
|
42
|
+
requirements: []
|
43
|
+
rubyforge_project:
|
44
|
+
rubygems_version: 1.8.23
|
45
|
+
signing_key:
|
46
|
+
specification_version: 3
|
47
|
+
summary: Linear Program Solver
|
48
|
+
test_files: []
|