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