opl 1.1.0 → 2.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 +239 -69
  2. metadata +7 -6
data/lib/opl.rb CHANGED
@@ -9,19 +9,17 @@ require "rglpk"
9
9
  # "x >= 0"
10
10
  # ]))
11
11
 
12
- #1.2
13
- #allow a POSITIVE: x option or NEGATIVE: x
14
-
15
- #1.3
16
- #float coefficients
17
-
18
- #2.0
19
- #data arrays
20
-
21
12
  #2.1
13
+ #ability to do arithmetic with data indices
14
+ #forall(i in (2..3) d[i-1]x[i] <= 10)
15
+
16
+ #2.2
22
17
  #a matrix representation of the solution if using
23
18
  #sub notation
24
19
 
20
+ #2.3
21
+ #parse data from a file
22
+
25
23
  #3.0
26
24
  #multiple level sub notation e.g. x[1][[3]]
27
25
 
@@ -63,6 +61,31 @@ class String
63
61
  end
64
62
  end
65
63
 
64
+ class Array
65
+ def dimension
66
+ a = self
67
+ return 0 if a.class != Array
68
+ result = 1
69
+ a.each do |sub_a|
70
+ if sub_a.class == Array
71
+ dim = sub_a.dimension
72
+ result = dim + 1 if dim + 1 > result
73
+ end
74
+ end
75
+ return result
76
+ end
77
+
78
+ def values_at_a(indices, current_array=self)
79
+ #in: self = [3,4,[6,5,[3,4]],3], indices = [2,2,0]
80
+ #out: 3
81
+ if indices.size == 1
82
+ return(current_array[indices[0]])
83
+ else
84
+ values_at_a(indices[1..-1], current_array[indices[0]])
85
+ end
86
+ end
87
+ end
88
+
66
89
  class OPL
67
90
  class Helper
68
91
  def self.mass_product(array_of_arrays, base=[])
@@ -155,7 +178,7 @@ class OPL
155
178
  {:lhs => sides[0], :rhs => sides[1]}
156
179
  end
157
180
 
158
- def self.add_ones(text)
181
+ def self.add_ones(text, lp)
159
182
  equation = text
160
183
  equation = "#"+equation
161
184
  equation.scan(/[#+-][a-z]/).each do |p|
@@ -262,8 +285,8 @@ class OPL
262
285
  return(equation)
263
286
  end
264
287
 
265
- def self.coefficients(text)#parameter is one side of the equation
266
- equation = self.add_ones(text)
288
+ def self.coefficients(text, lp)#text is one side of the equation
289
+ equation = self.add_ones(text, lp)
267
290
  if equation[0]=="-"
268
291
  equation.scan(/[+-][\d\.]+/)
269
292
  else
@@ -271,17 +294,21 @@ class OPL
271
294
  end
272
295
  end
273
296
 
274
- def self.variables(text)#parameter is one side of the equation
275
- equation = self.add_ones(text)
297
+ def self.data_coefficients
298
+
299
+ end
300
+
301
+ def self.variables(text, lp)#parameter is one side of the equation
302
+ equation = self.add_ones(text, lp)
276
303
  equation.scan(/[a-z]+[\[\]\d]*/)
277
304
  end
278
305
 
279
- def self.get_all_vars(constraints)
306
+ def self.get_all_vars(constraints, lp)
280
307
  all_vars = []
281
308
  constraints.each do |constraint|
282
309
  constraint = constraint.gsub(" ", "")
283
310
  value = constraint.split(":")[1] || constraint
284
- all_vars << self.variables(value)
311
+ all_vars << self.variables(value, lp)
285
312
  end
286
313
  all_vars.flatten.uniq
287
314
  end
@@ -462,14 +489,15 @@ class OPL
462
489
  variable_type_hash
463
490
  end
464
491
 
465
- def self.sum_variables(formatted_constraint)
492
+ def self.sum_variables(formatted_constraint, lp)
466
493
  #in: x + y - z + x[3] + z + y - z + x - y <= 0
467
494
  #out: 2*x + y - z + x[3] <= 0
468
495
  helper = self
469
496
  lhs = helper.sides(formatted_constraint)[:lhs]
470
- formatted_lhs = helper.add_ones(lhs)
471
- vars = helper.variables(formatted_lhs)
472
- coefs = helper.coefficients(formatted_lhs)
497
+ formatted_lhs = helper.add_ones(lhs, lp)
498
+ vars = helper.variables(formatted_lhs, lp)
499
+ #data coefficients should already be substituted
500
+ coefs = helper.coefficients(formatted_lhs, lp)
473
501
  var_coef_hash = {}
474
502
  vars.each_index do |i|
475
503
  var = vars[i]
@@ -492,6 +520,81 @@ class OPL
492
520
  end
493
521
  formatted_constraint.gsub(lhs, new_lhs)
494
522
  end
523
+
524
+ def self.get_column_bounds(bound_info, all_variables)
525
+ #in: ["NONNEGATIVE: x1"]
526
+ #out: {:x1 => {:lower => 0}}
527
+ column_bounds = {}
528
+ bound_info.each do |info|
529
+ type = info.gsub(" ","").split(":")[0]
530
+ if type.downcase == "nonnegative"
531
+ bounds = {:lower => 0}
532
+ end
533
+ variables = info.split(":")[1].gsub(" ","").split(",")
534
+ variables.each do |root_var|
535
+ all_variables_with_root = all_variables.find_all{|var|var.include?("[") && var.split("[")[0]==root_var}+[root_var]
536
+ all_variables_with_root.each do |var|
537
+ column_bounds[var.to_sym] = bounds
538
+ end
539
+ end
540
+ end
541
+ column_bounds
542
+ end
543
+
544
+ def self.parse_data(data_info)
545
+ #in: "DATA: {d => [1, 0.3, 1.5], o => [10.3, 4.0005, -1]}"
546
+ #out: [data_object_1, data_object_2]
547
+ data_hash_string = data_info.gsub(" ","").split(":")[1]
548
+ data_string = data_hash_string.gsub("{",",").gsub("}",",")
549
+ names = data_string.scan(/,[a-z]/).map{|comma_name|comma_name.gsub(",","")}
550
+ string_values = data_string.scan(/\=\>[\[\d\.\]\,\-]+,/).map{|scanned_value|scanned_value[2..-2]}
551
+ values = []
552
+ string_values.each do |string_value|
553
+ if string_value.include?("[")
554
+ values << string_value.gsub("[","").gsub("]","").split(",")
555
+ else
556
+ values << string_value.to_f
557
+ end
558
+ end
559
+ data_hash = {}
560
+ names.each_index do |i|
561
+ name = names[i]
562
+ value = values[i]
563
+ data_hash[name] = value
564
+ end
565
+ return(data_hash)
566
+ end
567
+
568
+ def self.substitute_data(text, lp)
569
+ helper = self
570
+ potential_things_to_substitute = helper.variables(text, lp)
571
+ data_names = lp.data.map{|d|d.name}
572
+ things_to_substitute = {}
573
+ data_values = {}
574
+ lp.data.each do |data|
575
+ dname = data.name
576
+ dvalue = data.value
577
+ targets = potential_things_to_substitute.find_all do |ptts|
578
+ dname == ptts[0]
579
+ end
580
+ things_to_substitute[dname] = targets
581
+ targets.each do |target|
582
+ indices = target.scan(/\d+/).map{|ind|ind.to_i}
583
+ value = dvalue.values_at_a(indices)
584
+ data_values[dname+indices.to_s] = value
585
+ end
586
+ end
587
+ data_values.keys.each do |key|
588
+ name = key
589
+ value = data_values[key]
590
+ text = text.gsub(name, value)
591
+ end
592
+ plus_minus = text.scan(/\+[\ ]+-/)
593
+ plus_minus.each do |pm|
594
+ text = text.gsub(pm,"-")
595
+ end
596
+ return(text)
597
+ end
495
598
  end
496
599
 
497
600
  class LinearProgram
@@ -505,11 +608,20 @@ class OPL
505
608
  attr_accessor :matrix
506
609
  attr_accessor :simplex_message
507
610
  attr_accessor :mip_message
611
+ attr_accessor :data
612
+ attr_accessor :data_hash
613
+ attr_accessor :variable_types
614
+ attr_accessor :column_bounds
615
+ attr_accessor :epsilon
616
+
617
+ def keys
618
+ [:objective, :constraints, :rows, :solution, :formatted_constraints, :rglpk_object, :solver, :matrix, :simplex_message, :mip_message, :data]
619
+ end
508
620
 
509
- def initialize(objective, constraints)
510
- @objective = objective
511
- @constraints = constraints
621
+ def initialize
512
622
  @rows = []
623
+ @data = []
624
+ @epsilon = $default_epsilon
513
625
  end
514
626
  end
515
627
 
@@ -546,6 +658,8 @@ class OPL
546
658
  attr_accessor :variable
547
659
  attr_accessor :coefficient
548
660
  attr_accessor :variable_type
661
+ attr_accessor :lower_bound
662
+ attr_accessor :upper_bound
549
663
 
550
664
  def initialize(variable, coefficient, variable_type=1)
551
665
  @variable = variable
@@ -553,39 +667,75 @@ class OPL
553
667
  @variable_type = variable_type
554
668
  end
555
669
  end
670
+
671
+ class Data
672
+ attr_accessor :name
673
+ attr_accessor :value
674
+ attr_accessor :value_type#number, array
675
+ attr_accessor :array_dimension
676
+
677
+ def initialize(name, value)
678
+ @name = name
679
+ @value = value
680
+ if value.is_a?(Array)
681
+ @value_type = Array
682
+ @array_dimension = value.dimension
683
+ else
684
+ @value_type = Integer
685
+ end
686
+ end
687
+ end
556
688
  end
557
689
 
558
690
  def subject_to(constraints, options=[])
559
- variable_types = options.find_all{|option|option.downcase.include?("boolean") || option.downcase.include?("integer")} || []
560
- epsilon = options.find_all{|option|option.downcase.include?("epsilon")}.first.gsub(" ","").split(":")[1].to_f rescue $default_epsilon
561
- constraints = constraints.flatten
562
- constraints = OPL::Helper.split_equals_a(constraints)
563
- constraints = constraints.map do |constraint|
564
- OPL::Helper.sub_forall(constraint)
565
- end.flatten
566
- constraints = constraints.map do |constraint|
567
- OPL::Helper.sum_indices(constraint)
568
- end
569
- constraints = constraints.map do |constraint|
570
- OPL::Helper.sub_sum(constraint)
571
- end
572
- constraints = constraints.map do |constraint|
573
- OPL::Helper.sum_indices(constraint)
574
- end
575
- constraints = constraints.map do |constraint|
576
- OPL::Helper.put_constants_on_rhs(constraint)
577
- end
578
- constraints = constraints.map do |constraint|
579
- OPL::Helper.put_variables_on_lhs(constraint)
580
- end
581
- constraints = constraints.map do |constraint|
582
- OPL::Helper.sub_rhs_with_summed_constants(constraint)
583
- end
584
- constraints = constraints.map do |constraint|
585
- OPL::Helper.sum_variables(constraint)
586
- end
587
- all_vars = OPL::Helper.get_all_vars(constraints)
691
+ lp = OPL::LinearProgram.new
692
+ variable_types = options.find_all{|option|option.downcase.include?("boolean") || option.downcase.include?("integer")} || []
693
+ epsilon = options.find_all{|option|option.downcase.include?("epsilon")}.first.gsub(" ","").split(":")[1].to_f rescue $default_epsilon
694
+ bounded_columns = options.find_all{|option|option.downcase.include?("negative") || option.downcase.include?("positive") || option.downcase.include?("nonnegative")}
695
+ data = options.find_all{|option|option.gsub(" ","").downcase.include?("data:")}[0]
696
+ if data
697
+ parsed_data = OPL::Helper.parse_data(data)
698
+ parsed_data.keys.each do |data_key|
699
+ data_value = parsed_data[data_key]
700
+ lp.data << OPL::Data.new(data_key, data_value)
701
+ end
702
+ end
703
+ lp.epsilon = epsilon
704
+ constraints = constraints.flatten
705
+ constraints = OPL::Helper.split_equals_a(constraints)
706
+ data_names = lp.data.map{|d|d.name}
707
+ constraints = constraints.map do |constraint|
708
+ OPL::Helper.sub_forall(constraint)
709
+ end.flatten
710
+ constraints = constraints.map do |constraint|
711
+ OPL::Helper.sum_indices(constraint)
712
+ end
713
+ constraints = constraints.map do |constraint|
714
+ OPL::Helper.sub_sum(constraint)
715
+ end
716
+ constraints = constraints.map do |constraint|
717
+ OPL::Helper.sum_indices(constraint)
718
+ end
719
+ constraints = constraints.map do |constraint|
720
+ OPL::Helper.put_constants_on_rhs(constraint)
721
+ end
722
+ constraints = constraints.map do |constraint|
723
+ OPL::Helper.put_variables_on_lhs(constraint)
724
+ end
725
+ constraints = constraints.map do |constraint|
726
+ OPL::Helper.sub_rhs_with_summed_constants(constraint)
727
+ end
728
+ constraints = constraints.map do |constraint|
729
+ OPL::Helper.substitute_data(constraint, lp)
730
+ end
731
+ constraints = constraints.map do |constraint|
732
+ OPL::Helper.sum_variables(constraint, lp)
733
+ end
734
+ all_vars = OPL::Helper.get_all_vars(constraints, lp)
588
735
  variable_type_hash = OPL::Helper.produce_variable_type_hash(variable_types, all_vars)
736
+ column_bounds = OPL::Helper.get_column_bounds(bounded_columns, all_vars)
737
+ lp.variable_types = variable_type_hash
738
+ lp.column_bounds = column_bounds
589
739
  rows = []
590
740
  constraints.each do |constraint|
591
741
  negate = false
@@ -607,7 +757,7 @@ def subject_to(constraints, options=[])
607
757
  upper_bound = (bound*-1).to_s
608
758
  end
609
759
  lhs = OPL::Helper.sides(constraint)[:lhs]
610
- coefs = OPL::Helper.coefficients(lhs)
760
+ coefs = OPL::Helper.coefficients(lhs, lp)
611
761
  if negate
612
762
  coefs = coefs.map do |coef|
613
763
  if coef.include?("+")
@@ -617,7 +767,7 @@ def subject_to(constraints, options=[])
617
767
  end
618
768
  end
619
769
  end
620
- vars = OPL::Helper.variables(lhs)
770
+ vars = OPL::Helper.variables(lhs, lp)
621
771
  zero_coef_vars = all_vars - vars
622
772
  row = OPL::Row.new(name, lower_bound, upper_bound, epsilon)
623
773
  row.constraint = constraint
@@ -628,25 +778,30 @@ def subject_to(constraints, options=[])
628
778
  all_vars.each do |var|
629
779
  coef = coefs[vars.index(var)]
630
780
  variable_type = variable_type_hash[var.to_sym] || 1
631
- pairs << OPL::VariableCoefficientPair.new(var, coef, variable_type)
781
+ vcp = OPL::VariableCoefficientPair.new(var, coef, variable_type)
782
+ vcp.lower_bound = column_bounds[var.to_sym][:lower] rescue nil
783
+ vcp.upper_bound = column_bounds[var.to_sym][:upper] rescue nil
784
+ pairs << vcp
632
785
  end
633
786
  row.variable_coefficient_pairs = pairs
634
787
  rows << row
635
788
  end
636
- rows
789
+ lp.rows = rows
790
+ lp
637
791
  end
638
792
 
639
- def maximize(objective, rows_c)#objective function has no = in it
640
- optimize("maximize", objective, rows_c)
793
+ def maximize(objective, lp)
794
+ optimize("maximize", objective, lp)
641
795
  end
642
796
 
643
- def minimize(objective, rows_c)#objective function has no = in it
644
- optimize("minimize", objective, rows_c)
797
+ def minimize(objective, lp)
798
+ optimize("minimize", objective, lp)
645
799
  end
646
800
 
647
- def optimize(optimization, objective, rows_c)
801
+ def optimize(optimization, objective, lp)
802
+ objective = OPL::Helper.substitute_data(objective, lp)
648
803
  o = OPL::Objective.new(objective, optimization)
649
- lp = OPL::LinearProgram.new(o, rows_c.map{|row|row.constraint})
804
+ lp.objective = o
650
805
  objective = OPL::Helper.sub_sum(objective)
651
806
  objective_constants = OPL::Helper.get_constants(objective)
652
807
  if objective_constants[:formatted].empty?
@@ -654,7 +809,7 @@ def optimize(optimization, objective, rows_c)
654
809
  else
655
810
  objective_addition = OPL::Helper.sum_constants(objective_constants[:formatted].inject("+"))
656
811
  end
657
- lp.rows = rows_c
812
+ rows_c = lp.rows
658
813
  p = Rglpk::Problem.new
659
814
  p.name = "sample"
660
815
  if optimization == "maximize"
@@ -666,9 +821,15 @@ def optimize(optimization, objective, rows_c)
666
821
  rows_c.each_index do |i|
667
822
  row = rows_c[i]
668
823
  rows[i].name = row.name
669
- rows[i].set_bounds(Rglpk::GLP_UP, nil, row.upper_bound) unless row.upper_bound.nil?
670
- rows[i].set_bounds(Rglpk::GLP_LO, nil, row.lower_bound) unless row.lower_bound.nil?
671
- #rows[i].set_bounds(Rglpk::GLP_FR, row.lower_bound, row.upper_bound)
824
+ if row.lower_bound.nil? && row.upper_bound.nil?
825
+ rows[i].set_bounds(Rglpk::GLP_FR, nil, nil)
826
+ elsif row.lower_bound.nil?
827
+ rows[i].set_bounds(Rglpk::GLP_UP, nil, row.upper_bound)
828
+ elsif row.upper_bound.nil?
829
+ rows[i].set_bounds(Rglpk::GLP_LO, row.lower_bound, nil)
830
+ else
831
+ rows[i].set_bounds(Rglpk::GLP_DB, row.lower_bound, row.upper_bound)
832
+ end
672
833
  end
673
834
  vars = rows_c.first.variable_coefficient_pairs
674
835
  cols = p.add_cols(vars.size)
@@ -678,7 +839,15 @@ def optimize(optimization, objective, rows_c)
678
839
  cols[i].name = column_name
679
840
  cols[i].kind = vars[i].variable_type#boolean, integer, etc.
680
841
  if [1,2].include? cols[i].kind
681
- cols[i].set_bounds(Rglpk::GLP_FR, nil, nil)
842
+ if vars[i].lower_bound.nil? && vars[i].upper_bound.nil?
843
+ cols[i].set_bounds(Rglpk::GLP_FR, nil, nil)
844
+ elsif vars[i].lower_bound.nil?
845
+ cols[i].set_bounds(Rglpk::GLP_UP, nil, vars[i].upper_bound)
846
+ elsif vars[i].upper_bound.nil?
847
+ cols[i].set_bounds(Rglpk::GLP_LO, vars[i].lower_bound, nil)
848
+ else
849
+ cols[i].set_bounds(Rglpk::GLP_DB, vars[i].lower_bound, vars[i].upper_bound)
850
+ end
682
851
  end
683
852
  if vars[i].variable_type != 1
684
853
  solver = "mip"
@@ -686,8 +855,9 @@ def optimize(optimization, objective, rows_c)
686
855
  end
687
856
  lp.solver = solver
688
857
  all_vars = rows_c.first.variable_coefficient_pairs.map{|vcp|vcp.variable}
689
- obj_coefficients = OPL::Helper.coefficients(objective.gsub(" ","")).map{|c|c.to_f}
690
- obj_vars = OPL::Helper.variables(objective.gsub(" ",""))
858
+ #not sure here
859
+ obj_coefficients = OPL::Helper.coefficients(objective.gsub(" ",""), lp).map{|c|c.to_f}
860
+ obj_vars = OPL::Helper.variables(objective.gsub(" ",""), lp)
691
861
  all_obj_coefficients = []
692
862
  all_vars.each do |var|
693
863
  i = obj_vars.index(var)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: opl
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 2.0.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,12 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-09-25 00:00:00.000000000 Z
12
+ date: 2013-09-26 00:00:00.000000000 Z
13
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.
14
+ description: Built on top of the rglpk gem for linear programming, this gem is meant
15
+ to give you a beautifully simple way to formulate your linear or mixed integer program.
16
+ The syntax is inspired by OPL Studio, which remains my favorite linear programming
17
+ software, but the license is quite expensive.
17
18
  email: bgodlove88@gmail.com
18
19
  executables: []
19
20
  extensions: []
@@ -44,5 +45,5 @@ rubyforge_project:
44
45
  rubygems_version: 1.8.23
45
46
  signing_key:
46
47
  specification_version: 3
47
- summary: Linear Program Solver
48
+ summary: Linear Or Mixed Integer Program Solver
48
49
  test_files: []