opr-calc 0.1.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 (3) hide show
  1. checksums.yaml +7 -0
  2. data/lib/opr-calc.rb +325 -0
  3. metadata +45 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 434b7eba4789fd454564da8db982a45cbc31c5ce
4
+ data.tar.gz: 71bbf74562b830a636202be0c0e100785c125b6a
5
+ SHA512:
6
+ metadata.gz: bc786e92a7d323d21ab2cacd40b7a05379453215b238754d45d2a490f17bb9473c1ea9067ad0d9140432c4c4e2737d600c55e3f96b78380ba7abe0a6939f6ab1
7
+ data.tar.gz: 58a190ddaf8aa3b405b5e80127f66a28f1682f3df4cc907d7976bb9f05c9b1252e50b752538f61dab2a00e6a54a66fab70d676358de3a6c5a6e0da8c6b0ed133
@@ -0,0 +1,325 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ =begin
4
+ opr-calc is a tool for calculating OPR and other scouting stats for FRC teams.
5
+ Copyright (C) 2014 Kristofer Rye and Christopher Cooper
6
+
7
+ This program is free software: you can redistribute it and/or modify
8
+ it under the terms of the GNU General Public License as published by
9
+ the Free Software Foundation, either version 3 of the License, or
10
+ (at your option) any later version.
11
+
12
+ This program is distributed in the hope that it will be useful,
13
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ GNU General Public License for more details.
16
+
17
+ You should have received a copy of the GNU General Public License
18
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
19
+ =end
20
+
21
+ require 'matrix'
22
+
23
+ # Props to http://rosettacode.org/wiki/Cholesky_decomposition#Ruby :L
24
+ class Matrix
25
+ # Returns whether or not the Matrix is symmetric (http://en.wikipedia.org/wiki/Symmetric_matrix)
26
+ def symmetric?
27
+
28
+ # Matrices can't be symmetric if they're not square.
29
+ return false if not square?
30
+
31
+ (0...row_size).each do |i|
32
+ (0..i).each do |j|
33
+ return false if self[i,j] != self[j,i]
34
+ end
35
+ end
36
+
37
+ true
38
+ end
39
+
40
+ def cholesky_factor
41
+ # We need a symmetric matrix for Cholesky.
42
+ raise ArgumentError, "You must provide symmetric matrix" unless symmetric?
43
+
44
+ # Make a new matrix to return
45
+ l = Array.new(row_size) { Array.new(row_size, 0) }
46
+
47
+ (0...row_size).each do |k|
48
+ (0...row_size).each do |i|
49
+ if i == k
50
+ sum = (0..k-1).inject(0.0) {|sum, j| sum + l[k][j] ** 2}
51
+ val = Math.sqrt(self[k,k] - sum)
52
+ l[k][k] = val
53
+ elsif i > k
54
+ sum = (0..k-1).inject(0.0) {|sum, j| sum + l[i][j] * l[k][j]}
55
+ val = (self[k,i] - sum) / l[k][k]
56
+ l[i][k] = val
57
+ end
58
+ end
59
+ end
60
+
61
+ Matrix[*l]
62
+ end
63
+
64
+ # A helpful debug function for Matrices
65
+ def output
66
+ (0..self.row_size - 1).each do |row_number|
67
+ (0..self.column_size - 1).each do |column_number|
68
+ printf("%8.4f ", self[row_number, column_number])
69
+ end
70
+ printf("\n")
71
+ end
72
+ self
73
+ end
74
+ end
75
+
76
+
77
+
78
+ class ScoreSet
79
+ attr_reader :ared, :ablue, :scorered, :scoreblue
80
+
81
+ def ared=(value)
82
+ @ared = value
83
+ @opr_recalc = @dpr_recalc = @ccwm_recalc = true
84
+ end
85
+ def ablue=(value)
86
+ @ablue = value
87
+ @opr_recalc = @dpr_recalc = @ccwm_recalc = true
88
+ end
89
+ def scorered=(value)
90
+ @scorered = value
91
+ @opr_recalc = @dpr_recalc = @ccwm_recalc = true
92
+ end
93
+ def scoreblue=(value)
94
+ @scoreblue = value
95
+ @opr_recalc = @dpr_recalc = @ccwm_recalc = true
96
+ end
97
+
98
+ def initialize(ared, ablue, scorered, scoreblue)
99
+ @ared = ared
100
+ @ablue = ablue
101
+ @scorered = scorered
102
+ @scoreblue = scoreblue
103
+ end
104
+
105
+ # A generic function for smooshing two matrices (one red, one blue).
106
+ # Each should have the same dimensions.
107
+ def alliance_smooshey(redmatrix, bluematrix)
108
+ throw ArgumentError "Matrices must have same dimensions" unless (redmatrix.row_size == bluematrix.row_size) && (redmatrix.column_size == bluematrix.column_size)
109
+
110
+ # Then we just pull the column and row size from the red matrix because we can.
111
+ column_count = redmatrix.column_size
112
+ row_count = redmatrix.row_size
113
+
114
+ # Use a block function to generate the new matrix
115
+ matrix = Matrix.build(row_count * 2, column_count) do
116
+ |row, column|
117
+
118
+ # note: no need to alternate, instead put all red, then all blue
119
+ if row < row_count # first half = red
120
+ redmatrix[row, column]
121
+ else # second half = blue
122
+ bluematrix[row - row_count, column]
123
+ end
124
+ end
125
+ # This will end up looking like as follows:
126
+
127
+ # [[red[0]],
128
+ # [red[1]],
129
+ # [red[2]],
130
+ # ...
131
+ # [red[n]],
132
+ # [blue[0]],
133
+ # [blue[1]],
134
+ # [blue[2]],
135
+ # ...
136
+ # [blue[n]]]
137
+ return matrix
138
+ end
139
+
140
+ private :alliance_smooshey
141
+
142
+ # Solve equation of form [l][x] = [s] for [x]
143
+ # l must be a lower triangular matrix.
144
+ # Based off of algorithm given at http://en.wikipedia.org/wiki/Triangular_matrix#Forward_and_back_substitution
145
+ def forward_substitute(l, s)
146
+ raise "l must be a lower triangular matrix" unless l.lower_triangular?
147
+
148
+ x = Array.new s.row_size
149
+
150
+ x.size.times do |i|
151
+ x[i] = s[i, 0]
152
+ i.times do |j|
153
+ x[i] -= l[i, j] * x[j]
154
+ end
155
+ x[i] /= l[i, i]
156
+ end
157
+
158
+ Matrix.column_vector x
159
+ end
160
+
161
+ # Solve equation of form [u][x] = [s] for [x]
162
+ # u must be a upper triangular matrix.
163
+ def back_substitute(u, s)
164
+ raise "u must be an upper triangular matrix" unless u.upper_triangular?
165
+
166
+ x = Array.new s.row_size
167
+
168
+ (x.size - 1).downto 0 do |i|
169
+ x[i] = s[i, 0]
170
+ (i + 1).upto(x.size - 1) do |j|
171
+ x[i] -= u[i, j] * x[j]
172
+ end
173
+ x[i] /= u[i, i]
174
+ end
175
+
176
+ Matrix.column_vector x
177
+ end
178
+
179
+ private :forward_substitute, :back_substitute
180
+
181
+ =begin
182
+ base matrix equation: [A][OPR] = [SCORE]
183
+ define [A]^t to be [A] transpose
184
+ define [P] to be [A]^t[A]
185
+ define [S] to be [A]^t[SCORE]
186
+ equation is now [P][OPR] = [S]
187
+ refactor [P] as [L][L]^t using cholesky
188
+ [L] is a lower triangular matrix and [L]^t an upper
189
+ Therefore [L][L]^t[OPR] = [S]
190
+ define [Y] = [L]^t[OPR]
191
+ equation is now [L][Y] = [S]
192
+ find [Y] through forward substitution
193
+ find [OPR] through back substitution
194
+ =end
195
+
196
+ def opr_calculate(a, score)
197
+ p = a.t * a
198
+ s = a.t * score
199
+
200
+ l = p.cholesky_factor
201
+
202
+ # l.output
203
+ # l.t.output
204
+
205
+ y = forward_substitute l, s
206
+ back_substitute l.t, y
207
+ end
208
+
209
+ # Offensive power rating: the average amount of points that a team contributes to their alliance's score
210
+ # This is high for a good team
211
+ def opr(recalc = false)
212
+ if !@opr || recalc || @opr_recalc
213
+ a = alliance_smooshey @ared, @ablue
214
+ score = alliance_smooshey @scorered, @scoreblue
215
+
216
+ @opr = opr_calculate a, score
217
+ @opr_recalc = false
218
+ end
219
+ @opr
220
+ end
221
+
222
+ # Defensive power rating: the average amount of points that a team lets the other alliance score
223
+ # This is low for a good team
224
+ def dpr(recalc = false)
225
+ if !@dpr || recalc || @dpr_recalc
226
+ a = alliance_smooshey @ared, @ablue
227
+ score = alliance_smooshey @scoreblue, @scorered # intentionally swapped, that's how dpr works
228
+
229
+ @dpr = opr_calculate a, score
230
+ @dpr_recalc = false
231
+ end
232
+ @dpr
233
+ end
234
+
235
+ # Calculated contribution to winning margin: the average amount of points that a team contributes to their alliance's winning margin
236
+ # This is high for a good team
237
+ def ccwm(recalc = false)
238
+ if !@ccwm || recalc || @ccwm_recalc
239
+ a = alliance_smooshey @ared, @ablue
240
+
241
+ red_wm = Matrix.build(@scorered.row_size, @scorered.column_size) do |row, column|
242
+ @scorered[row, column] - @scoreblue[row, column]
243
+ end
244
+ blue_wm = Matrix.build(@scoreblue.row_size, @scoreblue.column_size) do |row, column|
245
+ @scoreblue[row, column] - @scorered[row, column]
246
+ end
247
+
248
+ score = alliance_smooshey red_wm, blue_wm
249
+
250
+ @ccwm = opr_calculate a, score
251
+ @ccwm_recalc = false
252
+ end
253
+ @ccwm
254
+ end
255
+
256
+ end
257
+
258
+
259
+ def test_stuff
260
+ # Team 0 opr: 0
261
+ # Team 1 opr: 1
262
+ # Team 2 opr: 2
263
+ # ...
264
+
265
+ # I don't think any team is every playing on both blue and red at the same time, but I might be wrong
266
+
267
+ # 0 1 2 3 4 5 6 7 8 9
268
+ test_ared = Matrix[[1, 0, 1, 0, 1, 0, 0, 0, 0, 0],
269
+ [0, 1, 0, 1, 0, 1, 0, 0, 0, 0],
270
+ [0, 0, 0, 0, 1, 1, 1, 0, 0, 0],
271
+ [0, 0, 0, 0, 0, 0, 0, 1, 1, 1],
272
+ [0, 1, 0, 0, 0, 1, 1, 0, 0, 0],
273
+ [1, 0, 0, 1, 0, 0, 1, 0, 0, 0]]
274
+
275
+ # 0 1 2 3 4 5 6 7 8 9
276
+ test_ablue = Matrix[[0, 0, 1, 0, 0, 0, 0, 1, 0, 1],
277
+ [1, 0, 0, 0, 1, 0, 0, 0, 1, 0],
278
+ [1, 1, 1, 0, 0, 0, 0, 0, 0, 0],
279
+ [0, 0, 1, 0, 0, 1, 1, 0, 0, 0],
280
+ [0, 0, 0, 1, 0, 0, 0, 0, 1, 1],
281
+ [0, 1, 0, 0, 1, 0, 0, 1, 0, 0]]
282
+
283
+
284
+ test_scorered = Matrix[[6],
285
+ [9],
286
+ [15],
287
+ [24],
288
+ [12],
289
+ [9]]
290
+
291
+ test_scoreblue = Matrix[[18],
292
+ [12],
293
+ [3],
294
+ [13],
295
+ [20],
296
+ [12]]
297
+
298
+ test_expectedopr = Matrix[[0],
299
+ [1],
300
+ [2],
301
+ [3],
302
+ [4],
303
+ [5],
304
+ [6],
305
+ [7],
306
+ [8],
307
+ [9]]
308
+
309
+ test = ScoreSet.new test_ared, test_ablue, test_scorered, test_scoreblue
310
+
311
+ puts "Expected OPR:"
312
+ test_expectedopr.output
313
+ puts "Actual OPR:"
314
+ test.opr.output
315
+
316
+ puts "DPR:"
317
+ test.dpr.output
318
+
319
+ puts "CCWM:"
320
+ test.ccwm.output
321
+ puts "CCWM by OPR - DPR:"
322
+ (test.opr - test.dpr).output
323
+
324
+ return true
325
+ end
metadata ADDED
@@ -0,0 +1,45 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: opr-calc
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Kristofer Rye
8
+ - Christopher Cooper
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2014-01-25 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description:
15
+ email:
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - lib/opr-calc.rb
21
+ homepage: https://github.com/team461WBI/opr-calc
22
+ licenses:
23
+ - GPL-3.0
24
+ metadata: {}
25
+ post_install_message:
26
+ rdoc_options: []
27
+ require_paths:
28
+ - lib
29
+ required_ruby_version: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: 1.9.3
34
+ required_rubygems_version: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - '>='
37
+ - !ruby/object:Gem::Version
38
+ version: '0'
39
+ requirements: []
40
+ rubyforge_project:
41
+ rubygems_version: 2.0.14
42
+ signing_key:
43
+ specification_version: 4
44
+ summary: A tool for calculating OPR and other scouting stats for FRC teams.
45
+ test_files: []