opl 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (2) hide show
  1. data/lib/opl.rb +405 -0
  2. metadata +48 -0
@@ -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: []