compsci 0.1.1.1 → 0.2.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +74 -5
- data/Rakefile +20 -9
- data/VERSION +1 -1
- data/compsci.gemspec +1 -0
- data/examples/binary_search_tree.rb +16 -0
- data/examples/heap.rb +0 -42
- data/examples/heap_push.rb +46 -0
- data/examples/tree.rb +2 -1
- data/examples/{binary_tree.rb → tree_push.rb} +3 -2
- data/lib/compsci/binary_search_tree.rb +86 -0
- data/lib/compsci/fibonacci.rb +1 -9
- data/lib/compsci/fit.rb +34 -14
- data/lib/compsci/names.rb +3 -4
- data/lib/compsci/node.rb +66 -19
- data/lib/compsci/simplex.rb +173 -0
- data/lib/compsci/simplex/parse.rb +125 -0
- data/lib/compsci/tree.rb +14 -1
- data/test/bench/complete_tree.rb +59 -0
- data/test/bench/fibonacci.rb +0 -4
- data/test/bench/simplex.rb +141 -0
- data/test/bench/tree.rb +20 -15
- data/test/binary_search_tree.rb +106 -0
- data/test/fit.rb +5 -11
- data/test/node.rb +55 -4
- data/test/simplex.rb +291 -0
- data/test/simplex_parse.rb +94 -0
- data/test/tree.rb +33 -9
- metadata +27 -3
data/lib/compsci/fit.rb
CHANGED
@@ -1,5 +1,38 @@
|
|
1
1
|
module CompSci
|
2
2
|
module Fit
|
3
|
+
##
|
4
|
+
# Fits the functional form: a (+ 0x)
|
5
|
+
#
|
6
|
+
# Takes x and y values and returns [a, variance]
|
7
|
+
#
|
8
|
+
|
9
|
+
def self.constant xs, ys
|
10
|
+
y_bar = sigma(ys) / ys.size.to_f
|
11
|
+
variance = sigma(ys) { |y| (y - y_bar) ** 2 }
|
12
|
+
[y_bar, variance]
|
13
|
+
end
|
14
|
+
|
15
|
+
##
|
16
|
+
# Run logarithmic, linear, exponential, and power fits
|
17
|
+
# Return the stats for the best fit (highest r^2)
|
18
|
+
#
|
19
|
+
# Takes x and y values and returns [a, b, r2, fn]
|
20
|
+
#
|
21
|
+
|
22
|
+
def self.best xs, ys
|
23
|
+
vals = []
|
24
|
+
max_r2 = 0
|
25
|
+
[:logarithmic, :linear, :exponential, :power].each { |fn|
|
26
|
+
a, b, r2 = Fit.send(fn, xs, ys)
|
27
|
+
# p [a, b, r2, fn]
|
28
|
+
if r2 > max_r2
|
29
|
+
vals = [a, b, r2, fn]
|
30
|
+
max_r2 = r2
|
31
|
+
end
|
32
|
+
}
|
33
|
+
vals
|
34
|
+
end
|
35
|
+
|
3
36
|
#
|
4
37
|
# functions below originally from https://github.com/seattlrb/minitest
|
5
38
|
#
|
@@ -8,7 +41,7 @@ module CompSci
|
|
8
41
|
# Enumerates over +enum+ mapping +block+ if given, returning the
|
9
42
|
# sum of the result. Eg:
|
10
43
|
#
|
11
|
-
# sigma([1, 2, 3]) # => 1 + 2 + 3 =>
|
44
|
+
# sigma([1, 2, 3]) # => 1 + 2 + 3 => 6
|
12
45
|
# sigma([1, 2, 3]) { |n| n ** 2 } # => 1 + 4 + 9 => 14
|
13
46
|
|
14
47
|
def self.sigma enum, &block
|
@@ -34,19 +67,6 @@ module CompSci
|
|
34
67
|
1 - (ss_res / ss_tot)
|
35
68
|
end
|
36
69
|
|
37
|
-
##
|
38
|
-
# Fits the functional form: a (+ 0x)
|
39
|
-
#
|
40
|
-
# Takes x and y values and returns [a, variance]
|
41
|
-
#
|
42
|
-
|
43
|
-
def self.constant xs, ys
|
44
|
-
# written by Rick
|
45
|
-
y_bar = sigma(ys) / ys.size.to_f
|
46
|
-
variance = sigma(ys) { |y| (y - y_bar) ** 2 }
|
47
|
-
[y_bar, variance]
|
48
|
-
end
|
49
|
-
|
50
70
|
##
|
51
71
|
# To fit a functional form: y = a + b*ln(x).
|
52
72
|
#
|
data/lib/compsci/names.rb
CHANGED
@@ -1,5 +1,8 @@
|
|
1
1
|
module CompSci
|
2
2
|
module Names
|
3
|
+
ENGLISH_UPPER = [*'A'..'Z']
|
4
|
+
ENGLISH_LOWER = [*'a'..'z']
|
5
|
+
|
3
6
|
WW1 = [:apples, :butter, :charlie, :duff, :edward, :freddy, :george,
|
4
7
|
:harry, :ink, :johnnie, :king, :london, :monkey, :nuts, :orange,
|
5
8
|
:pudding, :queenie, :robert, :sugar, :tommy, :uncle, :vinegar,
|
@@ -14,12 +17,8 @@ module CompSci
|
|
14
17
|
CRYPTO = [:alice, :bob, :charlie, :david, :eve, :frank, :grace, :heidi,
|
15
18
|
:judy, :mallory, :olivia, :peggy, :sybil, :trudy, :victor,
|
16
19
|
:wendy]
|
17
|
-
ENGLISH_UPPER = [*'A'..'Z']
|
18
|
-
ENGLISH_LOWER = [*'a'..'z']
|
19
|
-
|
20
20
|
PLANETS = [:mercury, :venus, :earth, :mars, :jupiter, :saturn, :uranus,
|
21
21
|
:neptune, :pluto]
|
22
|
-
|
23
22
|
SOLAR = [:mercury, :venus, :earth, :mars, :asteroid_belt, :jupiter,
|
24
23
|
:saturn, :uranus, :neptune, :kuiper_belt, :scattered_disk,
|
25
24
|
:heliosphere]
|
data/lib/compsci/node.rb
CHANGED
@@ -1,31 +1,29 @@
|
|
1
1
|
module CompSci
|
2
|
-
# has a value and an array of children
|
2
|
+
# has a value and an array of children; allows child gaps
|
3
3
|
class Node
|
4
4
|
attr_accessor :value
|
5
5
|
attr_reader :children
|
6
6
|
|
7
|
-
def initialize(value)
|
7
|
+
def initialize(value, children: [])
|
8
8
|
@value = value
|
9
|
-
|
9
|
+
if children.is_a?(Integer)
|
10
|
+
@children = Array.new(children)
|
11
|
+
else
|
12
|
+
@children = children
|
13
|
+
end
|
10
14
|
# @metadata = {}
|
11
15
|
end
|
12
16
|
|
13
|
-
def add_child(node)
|
14
|
-
@children << node
|
15
|
-
end
|
16
|
-
|
17
|
-
def new_child(value)
|
18
|
-
self.add_child self.class.new(value)
|
19
|
-
end
|
20
|
-
|
21
|
-
def add_parent(node)
|
22
|
-
node.add_child(self)
|
23
|
-
end
|
24
|
-
|
25
17
|
def to_s
|
26
18
|
@value.to_s
|
27
19
|
end
|
28
20
|
|
21
|
+
# This could be done directly with self.children, but #set_child is part
|
22
|
+
# of the Node API.
|
23
|
+
def set_child(idx, node)
|
24
|
+
@children[idx] = node
|
25
|
+
end
|
26
|
+
|
29
27
|
def inspect
|
30
28
|
"#<%s:0x%0xi @value=%s @children=[%s]>" %
|
31
29
|
[self.class,
|
@@ -35,13 +33,44 @@ module CompSci
|
|
35
33
|
end
|
36
34
|
end
|
37
35
|
|
36
|
+
# adds a key to Node; often the key is used to place the node in the
|
37
|
+
# tree, independent of the value; e.g. key=priority, value=process_id
|
38
|
+
class KeyNode < Node
|
39
|
+
attr_accessor :key
|
40
|
+
|
41
|
+
def initialize(val, key: nil, children: [])
|
42
|
+
@key = key
|
43
|
+
super(val, children: children)
|
44
|
+
end
|
45
|
+
|
46
|
+
def to_s
|
47
|
+
[key, value].join(':')
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# accumulate children; no child gaps
|
52
|
+
class FlexNode < Node
|
53
|
+
def add_child(node)
|
54
|
+
@children << node
|
55
|
+
end
|
56
|
+
|
57
|
+
# TODO: are we passing everything needed to self.class.new ?
|
58
|
+
def new_child(value)
|
59
|
+
self.add_child self.class.new(value)
|
60
|
+
end
|
61
|
+
|
62
|
+
def add_parent(node)
|
63
|
+
node.add_child(self)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
38
67
|
# like Node but with a reference to its parent
|
39
68
|
class ChildNode < Node
|
40
69
|
attr_accessor :parent
|
41
70
|
|
42
|
-
def initialize(value)
|
71
|
+
def initialize(value, children: [])
|
43
72
|
@parent = nil
|
44
|
-
super(value)
|
73
|
+
super(value, children: children)
|
45
74
|
end
|
46
75
|
|
47
76
|
# O(log n) recursive
|
@@ -53,15 +82,33 @@ module CompSci
|
|
53
82
|
@parent ? @parent.children : []
|
54
83
|
end
|
55
84
|
|
85
|
+
def set_child(idx, node)
|
86
|
+
node.parent ||= self
|
87
|
+
raise "node has a parent: #{node.parent}" if node.parent != self
|
88
|
+
@children[idx] = node
|
89
|
+
end
|
90
|
+
|
91
|
+
def set_parent(idx, node)
|
92
|
+
@parent = node
|
93
|
+
@parent.set_child(idx, self)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# ChildNode which accumulates children with no gaps
|
98
|
+
class ChildFlexNode < ChildNode
|
56
99
|
def add_child(node)
|
57
100
|
node.parent ||= self
|
58
101
|
raise "node has a parent: #{node.parent}" if node.parent != self
|
59
|
-
|
102
|
+
@children << node
|
103
|
+
end
|
104
|
+
|
105
|
+
def new_child(value)
|
106
|
+
self.add_child self.class.new(value)
|
60
107
|
end
|
61
108
|
|
62
109
|
def add_parent(node)
|
63
110
|
@parent = node
|
64
|
-
|
111
|
+
@parent.add_child(self)
|
65
112
|
end
|
66
113
|
end
|
67
114
|
end
|
@@ -0,0 +1,173 @@
|
|
1
|
+
require 'compsci'
|
2
|
+
|
3
|
+
# note, this work is based on https://github.com/rickhull/simplex
|
4
|
+
# which was forked in 2017 from https://github.com/danlucraft/simplex
|
5
|
+
# which had its last commit in 2013
|
6
|
+
|
7
|
+
class CompSci::Simplex
|
8
|
+
DEFAULT_MAX_PIVOTS = 10_000
|
9
|
+
|
10
|
+
class Error < RuntimeError; end
|
11
|
+
class UnboundedProblem < Error; end
|
12
|
+
class SanityCheck < Error; end
|
13
|
+
class TooManyPivots < Error; end
|
14
|
+
|
15
|
+
attr_accessor :max_pivots
|
16
|
+
|
17
|
+
# c - coefficients of objective function; size: num_vars
|
18
|
+
# a - inequality lhs coefficients; 2dim size: num_inequalities, num_vars
|
19
|
+
# b - inequality rhs constants size: num_inequalities
|
20
|
+
def initialize(c, a, b)
|
21
|
+
num_vars = c.size
|
22
|
+
num_inequalities = b.size
|
23
|
+
raise(ArgumentError, "a doesn't match b") unless a.size == num_inequalities
|
24
|
+
raise(ArgumentError, "a doesn't match c") unless a.first.size == num_vars
|
25
|
+
|
26
|
+
@max_pivots = DEFAULT_MAX_PIVOTS
|
27
|
+
|
28
|
+
# Problem dimensions; these never change
|
29
|
+
@num_non_slack_vars = num_vars
|
30
|
+
@num_constraints = num_inequalities
|
31
|
+
@num_vars = @num_non_slack_vars + @num_constraints
|
32
|
+
|
33
|
+
# Set up initial matrix A and vectors b, c
|
34
|
+
@c = c.map { |flt| -1 * flt } + Array.new(@num_constraints, 0)
|
35
|
+
@a = a.map.with_index { |ary, i|
|
36
|
+
if ary.size != @num_non_slack_vars
|
37
|
+
raise ArgumentError, "a is inconsistent"
|
38
|
+
end
|
39
|
+
# set diagonal to 1 (identity matrix?)
|
40
|
+
ary + Array.new(@num_constraints) { |ci| ci == i ? 1 : 0 }
|
41
|
+
}
|
42
|
+
@b = b
|
43
|
+
|
44
|
+
# set initial solution: all non-slack variables = 0
|
45
|
+
@basic_vars = (@num_non_slack_vars...@num_vars).to_a
|
46
|
+
self.update_solution
|
47
|
+
end
|
48
|
+
|
49
|
+
# does not modify vector / matrix
|
50
|
+
def update_solution
|
51
|
+
@x = Array.new(@num_vars, 0)
|
52
|
+
|
53
|
+
@basic_vars.each { |basic_var|
|
54
|
+
idx = nil
|
55
|
+
@num_constraints.times { |i|
|
56
|
+
if @a[i][basic_var] == 1
|
57
|
+
idx =i
|
58
|
+
break
|
59
|
+
end
|
60
|
+
}
|
61
|
+
raise(SanityCheck, "no idx for basic_var #{basic_var} in a") unless idx
|
62
|
+
@x[basic_var] = @b[idx]
|
63
|
+
}
|
64
|
+
end
|
65
|
+
|
66
|
+
def solution
|
67
|
+
self.solve
|
68
|
+
self.current_solution
|
69
|
+
end
|
70
|
+
|
71
|
+
def solve
|
72
|
+
count = 0
|
73
|
+
while self.can_improve?
|
74
|
+
count += 1
|
75
|
+
raise(TooManyPivots, count.to_s) unless count < @max_pivots
|
76
|
+
self.pivot
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def current_solution
|
81
|
+
@x[0...@num_non_slack_vars]
|
82
|
+
end
|
83
|
+
|
84
|
+
def can_improve?
|
85
|
+
!self.entering_variable.nil?
|
86
|
+
end
|
87
|
+
|
88
|
+
# idx of @c's minimum negative value
|
89
|
+
# nil when no improvement is possible
|
90
|
+
#
|
91
|
+
def entering_variable
|
92
|
+
(0...@c.size).select { |i| @c[i] < 0 }.min_by { |i| @c[i] }
|
93
|
+
end
|
94
|
+
|
95
|
+
def pivot
|
96
|
+
pivot_column = self.entering_variable or return nil
|
97
|
+
pivot_row = self.pivot_row(pivot_column) or raise UnboundedProblem
|
98
|
+
leaving_var = nil
|
99
|
+
@a[pivot_row].each_with_index { |a, i|
|
100
|
+
if a == 1 and @basic_vars.include?(i)
|
101
|
+
leaving_var = i
|
102
|
+
break
|
103
|
+
end
|
104
|
+
}
|
105
|
+
raise(SanityCheck, "no leaving_var") if leaving_var.nil?
|
106
|
+
|
107
|
+
@basic_vars.delete(leaving_var)
|
108
|
+
@basic_vars.push(pivot_column)
|
109
|
+
@basic_vars.sort!
|
110
|
+
|
111
|
+
pivot_ratio = Rational(1, @a[pivot_row][pivot_column])
|
112
|
+
|
113
|
+
# update pivot row
|
114
|
+
@a[pivot_row] = @a[pivot_row].map { |val| val * pivot_ratio }
|
115
|
+
@b[pivot_row] = @b[pivot_row] * pivot_ratio
|
116
|
+
|
117
|
+
# update objective
|
118
|
+
# @c -= @c[pivot_column] * @a[pivot_row]
|
119
|
+
@c = @c.map.with_index { |val, i|
|
120
|
+
val - @c[pivot_column] * @a[pivot_row][i]
|
121
|
+
}
|
122
|
+
|
123
|
+
# update A and B
|
124
|
+
@num_constraints.times { |i|
|
125
|
+
next if i == pivot_row
|
126
|
+
r = @a[i][pivot_column]
|
127
|
+
@a[i] = @a[i].map.with_index { |val, j| val - r * @a[pivot_row][j] }
|
128
|
+
@b[i] = @b[i] - r * @b[pivot_row]
|
129
|
+
}
|
130
|
+
|
131
|
+
self.update_solution
|
132
|
+
end
|
133
|
+
|
134
|
+
def pivot_row(column_ix)
|
135
|
+
min_ratio = nil
|
136
|
+
idx = nil
|
137
|
+
@num_constraints.times { |i|
|
138
|
+
a, b = @a[i][column_ix], @b[i]
|
139
|
+
next if a == 0 or (b < 0) ^ (a < 0)
|
140
|
+
ratio = Rational(b, a)
|
141
|
+
idx, min_ratio = i, ratio if min_ratio.nil? or ratio <= min_ratio
|
142
|
+
}
|
143
|
+
idx
|
144
|
+
end
|
145
|
+
|
146
|
+
def formatted_tableau
|
147
|
+
if self.can_improve?
|
148
|
+
pivot_column = self.entering_variable
|
149
|
+
pivot_row = self.pivot_row(pivot_column)
|
150
|
+
else
|
151
|
+
pivot_row = nil
|
152
|
+
end
|
153
|
+
c = @c.to_a.map { |flt| "%2.3f" % flt }
|
154
|
+
b = @b.to_a.map { |flt| "%2.3f" % flt }
|
155
|
+
a = @a.to_a.map { |vec| vec.to_a.map { |flt| "%2.3f" % flt } }
|
156
|
+
if pivot_row
|
157
|
+
a[pivot_row][pivot_column] = "*" + a[pivot_row][pivot_column]
|
158
|
+
end
|
159
|
+
max = (c + b + a + ["1234567"]).flatten.map(&:size).max
|
160
|
+
result = []
|
161
|
+
result << c.map { |str| str.rjust(max, " ") }
|
162
|
+
a.zip(b) do |arow, brow|
|
163
|
+
result << (arow + [brow]).map { |val| val.rjust(max, " ") }
|
164
|
+
result.last.insert(arow.length, "|")
|
165
|
+
end
|
166
|
+
lines = result.map { |ary| ary.join(" ") }
|
167
|
+
max_line_length = lines.map(&:length).max
|
168
|
+
lines.insert(1, "-"*max_line_length)
|
169
|
+
lines.join("\n")
|
170
|
+
end
|
171
|
+
|
172
|
+
|
173
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
require 'compsci/simplex'
|
2
|
+
|
3
|
+
class CompSci::Simplex
|
4
|
+
module Parse
|
5
|
+
class Error < RuntimeError; end
|
6
|
+
class InvalidExpression < Error; end
|
7
|
+
class InvalidInequality < Error; end
|
8
|
+
class InvalidTerm < Error; end
|
9
|
+
|
10
|
+
# coefficient concatenated with a single letter variable, e.g. "-1.23x"
|
11
|
+
TERM_RGX = %r{
|
12
|
+
\A # starts with
|
13
|
+
(-)? # possible negative sign
|
14
|
+
(\d+(?:\.\d*)?)? # possible float (optional)
|
15
|
+
([a-zA-Z]) # single letter variable
|
16
|
+
\z # end str
|
17
|
+
}x
|
18
|
+
|
19
|
+
# a float or integer, possibly negative
|
20
|
+
CONSTANT_RGX = %r{
|
21
|
+
\A # starts with
|
22
|
+
-? # possible negative sign
|
23
|
+
\d+ # integer portion
|
24
|
+
(?:\.\d*)? # possible decimal portion
|
25
|
+
\z # end str
|
26
|
+
}x
|
27
|
+
|
28
|
+
def self.inequality(str)
|
29
|
+
lhs, rhs = str.split('<=')
|
30
|
+
if lhs.nil? or lhs.empty? or rhs.nil? or rhs.empty?
|
31
|
+
raise(InvalidInequality, "#{str}")
|
32
|
+
end
|
33
|
+
rht = self.tokenize(rhs)
|
34
|
+
raise(InvalidInequality, "#{str}; bad rhs: #{rhs}") unless rht.size == 1
|
35
|
+
c = rht.first
|
36
|
+
raise(InvalidInequality, "bad rhs: #{rhs}") if !c.match CONSTANT_RGX
|
37
|
+
return self.expression(lhs), c.to_f
|
38
|
+
end
|
39
|
+
|
40
|
+
# ignore leading and trailing spaces
|
41
|
+
# ignore multiple spaces
|
42
|
+
def self.tokenize(str)
|
43
|
+
str.strip.split(/\s+/)
|
44
|
+
end
|
45
|
+
|
46
|
+
# rules: variables are a single letter
|
47
|
+
# may have a coefficient (default: 1.0)
|
48
|
+
# only sum and difference operations allowed
|
49
|
+
# normalize to all sums with possibly negative coefficients
|
50
|
+
# valid inputs:
|
51
|
+
# 'x + y' => [1.0, 1.0], [:x, :y]
|
52
|
+
# '2x - 5y' => [2.0, -5.0], [:x, :y]
|
53
|
+
# '-2x - 3y + -4z' => [-2.0, -3.0, -4.0], [:x, :y, :z]
|
54
|
+
def self.expression(str)
|
55
|
+
terms = self.tokenize(str)
|
56
|
+
negative = false
|
57
|
+
coefficients = {}
|
58
|
+
while !terms.empty?
|
59
|
+
# consume plus and minus operations
|
60
|
+
term = terms.shift
|
61
|
+
if term == '-'
|
62
|
+
negative = true
|
63
|
+
term = terms.shift
|
64
|
+
elsif term == '+'
|
65
|
+
negative = false
|
66
|
+
term = terms.shift
|
67
|
+
end
|
68
|
+
|
69
|
+
coefficient, variable = self.term(term)
|
70
|
+
raise("double variable: #{str}") if coefficients.key?(variable)
|
71
|
+
coefficients[variable] = negative ? coefficient * -1 : coefficient
|
72
|
+
end
|
73
|
+
coefficients
|
74
|
+
end
|
75
|
+
|
76
|
+
def self.term(str)
|
77
|
+
matches = str.match TERM_RGX
|
78
|
+
raise(InvalidTerm, str) unless matches
|
79
|
+
flt = (matches[2] || 1).to_f * (matches[1] ? -1 : 1)
|
80
|
+
sym = matches[3].to_sym # consider matches[3].downcase.to_sym
|
81
|
+
return flt, sym
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def self.problem(maximize: nil, constraints: [], **kwargs)
|
86
|
+
if maximize
|
87
|
+
obj, maximize = maximize, true
|
88
|
+
elsif kwargs[:minimize]
|
89
|
+
obj, maximize = kwargs[:minimize], false
|
90
|
+
else
|
91
|
+
raise(ArgumentError, "one of maximize/minimize expected")
|
92
|
+
end
|
93
|
+
unless obj.is_a?(String)
|
94
|
+
raise(ArgumentError, "bad expr: #{expr} (#{expr.class})")
|
95
|
+
end
|
96
|
+
obj_cof = Parse.expression(obj)
|
97
|
+
|
98
|
+
c = [] # coefficients of objective expression
|
99
|
+
a = [] # array (per constraint) of the inequality's lhs coefficients
|
100
|
+
b = [] # rhs (constant) for the inequalities / constraints
|
101
|
+
|
102
|
+
# this determines the order of coefficients
|
103
|
+
letter_vars = obj_cof.keys
|
104
|
+
letter_vars.each { |v| c << obj_cof[v] }
|
105
|
+
|
106
|
+
constraints.each { |str|
|
107
|
+
unless str.is_a?(String)
|
108
|
+
raise(ArgumentError, "bad constraint: #{str} (#{str.class})")
|
109
|
+
end
|
110
|
+
cofs = []
|
111
|
+
ineq_cofs, rhs = Parse.inequality(str)
|
112
|
+
letter_vars.each { |v|
|
113
|
+
raise("constraint #{str} is missing var #{v}") unless ineq_cofs.key?(v)
|
114
|
+
cofs << ineq_cofs[v]
|
115
|
+
}
|
116
|
+
a.push cofs
|
117
|
+
b.push rhs
|
118
|
+
}
|
119
|
+
self.new(c, a, b)
|
120
|
+
end
|
121
|
+
|
122
|
+
def self.maximize(expression, *ineqs)
|
123
|
+
self.problem(maximize: expression, constraints: ineqs).solution
|
124
|
+
end
|
125
|
+
end
|