opl 0.0.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.
- 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: []
|