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.
- checksums.yaml +7 -0
- data/lib/opr-calc.rb +325 -0
- metadata +45 -0
checksums.yaml
ADDED
@@ -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
|
data/lib/opr-calc.rb
ADDED
@@ -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: []
|