compsci 0.1.1.1 → 0.2.0.1
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.
- 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
|