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.
@@ -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