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.
@@ -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 => 7
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
  #
@@ -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]
@@ -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
- @children = []
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
- super(node)
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
- super(node)
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