opr-calc 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: []