opr-calc 0.1.2 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rspec +2 -0
- data/Gemfile +3 -0
- data/Guardfile +14 -0
- data/LICENSE +630 -0
- data/README.md +25 -0
- data/Rakefile +4 -0
- data/lib/opr-calc.rb +2 -2
- data/lib/opr-calc/matrix.rb +13 -4
- data/lib/opr-calc/score_set.rb +43 -44
- data/opr-calc.gemspec +26 -0
- data/spec/lib/opr-calc/matrix_spec.rb +61 -0
- data/spec/lib/opr-calc/score_set_spec.rb +102 -0
- data/spec/lib/opr-calc_spec.rb +7 -0
- data/spec/spec_helper.rb +1 -0
- metadata +89 -14
data/README.md
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# opr-calc
|
2
|
+
|
3
|
+
[![Gem Version](https://badge.fury.io/rb/opr-calc.png)](http://badge.fury.io/rb/opr-calc)
|
4
|
+
|
5
|
+
A rubygem for calculating the OPR, DPR, and CCWM of FRC teams.
|
6
|
+
|
7
|
+
## Usage
|
8
|
+
|
9
|
+
Define a new ScoreSet with `OPRCalc::ScoreSet.new`. This takes four matrices as arguments: `ared`, `ablue`, `scorered`, and `scoreblue`.
|
10
|
+
The `a` matrices define what teams are in what matches.
|
11
|
+
The `score` matrices define what the final match scores are.
|
12
|
+
For example, consider the following match.
|
13
|
+
The first, third, and fifth team are on the red alliance, which scores 26.
|
14
|
+
The zeroth, second, and fourth team are on the blue alliance, which scores 56.
|
15
|
+
This would result in the following rows the various matrices.
|
16
|
+
|
17
|
+
`ared`: `[0, 1, 0, 1, 0, 1]`
|
18
|
+
|
19
|
+
`scorered`: `[26]`
|
20
|
+
|
21
|
+
`ablue`: `[1, 0, 1, 0, 1, 0]`
|
22
|
+
|
23
|
+
`scoreblue`: `[56]`
|
24
|
+
|
25
|
+
For other examples, read the tests, which are in the `test/` directory.
|
data/Rakefile
ADDED
data/lib/opr-calc.rb
CHANGED
data/lib/opr-calc/matrix.rb
CHANGED
@@ -16,7 +16,7 @@
|
|
16
16
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
17
17
|
=end
|
18
18
|
|
19
|
-
require
|
19
|
+
require 'matrix'
|
20
20
|
|
21
21
|
# Props to http://rosettacode.org/wiki/Cholesky_decomposition#Ruby :L
|
22
22
|
class Matrix
|
@@ -27,7 +27,9 @@ class Matrix
|
|
27
27
|
|
28
28
|
(0...row_size).each do |i|
|
29
29
|
(0..i).each do |j|
|
30
|
+
|
30
31
|
return false if self[i,j] != self[j,i]
|
32
|
+
|
31
33
|
end
|
32
34
|
end
|
33
35
|
|
@@ -38,21 +40,26 @@ class Matrix
|
|
38
40
|
# To obtain [L]*, just transpose the matrix, it should work.
|
39
41
|
def cholesky_factor
|
40
42
|
# We need a symmetric matrix for Cholesky decomposition.
|
41
|
-
raise ArgumentError,
|
43
|
+
raise ArgumentError, 'must provide a symmetric matrix' unless symmetric?
|
42
44
|
|
43
45
|
# Make a new matrix to return.
|
44
46
|
l = Array.new(row_size) { Array.new(row_size, 0) }
|
45
47
|
|
46
48
|
(0...row_size).each do |k|
|
47
49
|
(0...row_size).each do |i|
|
50
|
+
|
48
51
|
if i == k
|
52
|
+
|
49
53
|
sum = (0..k-1).inject(0.0) { |sum, j| sum + l[k][j] ** 2 }
|
50
54
|
val = Math.sqrt(self[k,k] - sum)
|
51
55
|
l[k][k] = val
|
56
|
+
|
52
57
|
elsif i > k
|
58
|
+
|
53
59
|
sum = (0..k-1).inject(0.0) { |sum, j| sum + l[i][j] * l[k][j] }
|
54
60
|
val = (self[k,i] - sum) / l[k][k]
|
55
61
|
l[i][k] = val
|
62
|
+
|
56
63
|
end
|
57
64
|
end
|
58
65
|
end
|
@@ -64,10 +71,12 @@ class Matrix
|
|
64
71
|
def output
|
65
72
|
(0..self.row_size - 1).each do |row_number|
|
66
73
|
(0..self.column_size - 1).each do |column_number|
|
67
|
-
|
74
|
+
|
75
|
+
printf('%8.4f ', self[row_number, column_number])
|
76
|
+
|
68
77
|
end
|
69
78
|
|
70
|
-
printf(
|
79
|
+
printf('\n')
|
71
80
|
end
|
72
81
|
|
73
82
|
self
|
data/lib/opr-calc/score_set.rb
CHANGED
@@ -24,28 +24,28 @@ module OPRCalc
|
|
24
24
|
@ared = value
|
25
25
|
@opr_recalc = @dpr_recalc = @ccwm_recalc = true
|
26
26
|
end
|
27
|
-
|
27
|
+
|
28
28
|
def ablue=(value)
|
29
29
|
@ablue = value
|
30
30
|
@opr_recalc = @dpr_recalc = @ccwm_recalc = true
|
31
31
|
end
|
32
|
-
|
32
|
+
|
33
33
|
def scorered=(value)
|
34
34
|
@scorered = value
|
35
35
|
@opr_recalc = @dpr_recalc = @ccwm_recalc = true
|
36
36
|
end
|
37
|
-
|
37
|
+
|
38
38
|
def scoreblue=(value)
|
39
39
|
@scoreblue = value
|
40
40
|
@opr_recalc = @dpr_recalc = @ccwm_recalc = true
|
41
41
|
end
|
42
42
|
|
43
43
|
def initialize(ared, ablue, scorered, scoreblue)
|
44
|
-
raise TypeError,
|
45
|
-
raise TypeError,
|
46
|
-
raise TypeError,
|
47
|
-
raise TypeError,
|
48
|
-
|
44
|
+
raise TypeError, 'ared must be a Matrix' unless ared.is_a? Matrix
|
45
|
+
raise TypeError, 'ablue must be a Matrix' unless ablue.is_a? Matrix
|
46
|
+
raise TypeError, 'scorered must be a Matrix' unless scorered.is_a? Matrix
|
47
|
+
raise TypeError, 'scoreblue must be a Matrix' unless scoreblue.is_a? Matrix
|
48
|
+
|
49
49
|
@ared = ared
|
50
50
|
@ablue = ablue
|
51
51
|
@scorered = scorered
|
@@ -55,9 +55,12 @@ module OPRCalc
|
|
55
55
|
# A generic function for smooshing two matrices (one red, one blue).
|
56
56
|
# Each should have the same dimensions.
|
57
57
|
def alliance_smooshey(redmatrix, bluematrix)
|
58
|
-
throw ArgumentError
|
58
|
+
throw ArgumentError 'Matrices must have same dimensions' unless
|
59
|
+
(redmatrix.row_size == bluematrix.row_size) &&
|
60
|
+
(redmatrix.column_size == bluematrix.column_size)
|
59
61
|
|
60
|
-
# Then we just pull the column and row size
|
62
|
+
# Then we just pull the column and row size
|
63
|
+
# from the red matrix because we can.
|
61
64
|
column_count = redmatrix.column_size
|
62
65
|
row_count = redmatrix.row_size
|
63
66
|
|
@@ -72,9 +75,8 @@ module OPRCalc
|
|
72
75
|
bluematrix[row - row_count, column]
|
73
76
|
end
|
74
77
|
end
|
75
|
-
|
76
|
-
# This will end up looking like as follows:
|
77
78
|
|
79
|
+
# This will end up looking like as follows:
|
78
80
|
# [[red[0]],
|
79
81
|
# [red[1]],
|
80
82
|
# [red[2]],
|
@@ -92,19 +94,20 @@ module OPRCalc
|
|
92
94
|
|
93
95
|
# Solve equation of form [l][x] = [s] for [x]
|
94
96
|
# l must be a lower triangular matrix.
|
95
|
-
# Based off of algorithm given at
|
97
|
+
# Based off of algorithm given at
|
98
|
+
# `http://en.wikipedia.org/wiki/Triangular_matrix#Forward_and_back_substitution`.
|
96
99
|
def forward_substitute(l, s)
|
97
|
-
raise
|
100
|
+
raise 'l must be a lower triangular matrix' unless l.lower_triangular?
|
98
101
|
|
99
102
|
x = Array.new s.row_size
|
100
103
|
|
101
104
|
x.size.times do |i|
|
102
105
|
x[i] = s[i, 0]
|
103
|
-
|
106
|
+
|
104
107
|
i.times do |j|
|
105
108
|
x[i] -= l[i, j] * x[j]
|
106
109
|
end
|
107
|
-
|
110
|
+
|
108
111
|
x[i] /= l[i, i]
|
109
112
|
end
|
110
113
|
|
@@ -114,17 +117,17 @@ module OPRCalc
|
|
114
117
|
# Solve equation of form [u][x] = [s] for [x]
|
115
118
|
# u must be a upper triangular matrix.
|
116
119
|
def back_substitute(u, s)
|
117
|
-
raise
|
120
|
+
raise 'u must be an upper triangular matrix' unless u.upper_triangular?
|
118
121
|
|
119
122
|
x = Array.new s.row_size
|
120
123
|
|
121
124
|
(x.size - 1).downto 0 do |i|
|
122
125
|
x[i] = s[i, 0]
|
123
|
-
|
126
|
+
|
124
127
|
(i + 1).upto(x.size - 1) do |j|
|
125
128
|
x[i] -= u[i, j] * x[j]
|
126
129
|
end
|
127
|
-
|
130
|
+
|
128
131
|
x[i] /= u[i, i]
|
129
132
|
end
|
130
133
|
|
@@ -132,19 +135,6 @@ module OPRCalc
|
|
132
135
|
end
|
133
136
|
|
134
137
|
private :forward_substitute, :back_substitute
|
135
|
-
|
136
|
-
# base matrix equation: [A][OPR] = [SCORE]
|
137
|
-
# define [A]^t to be [A] transpose
|
138
|
-
# define [P] to be [A]^t[A]
|
139
|
-
# define [S] to be [A]^t[SCORE]
|
140
|
-
# equation is now [P][OPR] = [S]
|
141
|
-
# refactor [P] as [L][L]^t using cholesky
|
142
|
-
# [L] is a lower triangular matrix and [L]^t an upper
|
143
|
-
# Therefore [L][L]^t[OPR] = [S]
|
144
|
-
# define [Y] = [L]^t[OPR]
|
145
|
-
# equation is now [L][Y] = [S]
|
146
|
-
# find [Y] through forward substitution
|
147
|
-
# find [OPR] through back substitution
|
148
138
|
|
149
139
|
def opr_calculate(a, score)
|
150
140
|
p = a.t * a
|
@@ -159,44 +149,53 @@ module OPRCalc
|
|
159
149
|
back_substitute l.t, y
|
160
150
|
end
|
161
151
|
|
162
|
-
# Offensive power rating: the average amount of points that a team
|
152
|
+
# Offensive power rating: the average amount of points that a team
|
153
|
+
# contributes to their alliance's score.
|
154
|
+
#
|
163
155
|
# This is high for a good team.
|
164
156
|
def opr(recalc = false)
|
165
157
|
if !@opr || recalc || @opr_recalc
|
166
158
|
a = alliance_smooshey @ared, @ablue
|
167
159
|
score = alliance_smooshey @scorered, @scoreblue
|
168
|
-
|
160
|
+
|
169
161
|
@opr = opr_calculate a, score
|
170
162
|
@opr_recalc = false
|
171
163
|
end
|
172
|
-
|
164
|
+
|
173
165
|
@opr
|
174
166
|
end
|
175
167
|
|
176
|
-
# Defensive power rating: the average amount of points that a team lets
|
168
|
+
# Defensive power rating: the average amount of points that a team lets
|
169
|
+
# the other alliance score.
|
170
|
+
#
|
177
171
|
# This is low for a good team.
|
178
172
|
def dpr(recalc = false)
|
179
173
|
if !@dpr || recalc || @dpr_recalc
|
180
174
|
a = alliance_smooshey @ared, @ablue
|
181
|
-
|
182
|
-
|
175
|
+
|
176
|
+
# scoreblue and scorered are intentionally
|
177
|
+
# swapped; that's how dpr works.
|
178
|
+
score = alliance_smooshey @scoreblue, @scorered
|
179
|
+
|
183
180
|
@dpr = opr_calculate a, score
|
184
181
|
@dpr_recalc = false
|
185
182
|
end
|
186
|
-
|
183
|
+
|
187
184
|
@dpr
|
188
185
|
end
|
189
186
|
|
190
|
-
# Calculated contribution to winning margin: the average amount of
|
191
|
-
#
|
187
|
+
# Calculated contribution to winning margin: the average amount of
|
188
|
+
# points that a team contributes to their alliance's winning margin.
|
189
|
+
#
|
190
|
+
# This value is high for a good team.
|
192
191
|
def ccwm(recalc = false)
|
193
192
|
if !@ccwm || recalc || @ccwm_recalc
|
194
193
|
a = alliance_smooshey @ared, @ablue
|
195
|
-
|
194
|
+
|
196
195
|
red_wm = Matrix.build(@scorered.row_size, @scorered.column_size) do |row, column|
|
197
196
|
@scorered[row, column] - @scoreblue[row, column]
|
198
197
|
end
|
199
|
-
|
198
|
+
|
200
199
|
blue_wm = Matrix.build(@scoreblue.row_size, @scoreblue.column_size) do |row, column|
|
201
200
|
@scoreblue[row, column] - @scorered[row, column]
|
202
201
|
end
|
@@ -206,7 +205,7 @@ module OPRCalc
|
|
206
205
|
@ccwm = opr_calculate a, score
|
207
206
|
@ccwm_recalc = false
|
208
207
|
end
|
209
|
-
|
208
|
+
|
210
209
|
@ccwm
|
211
210
|
end
|
212
211
|
end
|
data/opr-calc.gemspec
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = 'opr-calc'
|
3
|
+
s.email = 'kristofer.rye@gmail.com'
|
4
|
+
s.version = '1.0.0'
|
5
|
+
|
6
|
+
s.summary = 'A tool for calculating OPR and other scouting stats for FRC teams.'
|
7
|
+
s.description = "A tool for calculating the OPR, DPR, and CCWM of FRC teams, allowing for more detailed analysis of a team's projected performance. Uses the internal Ruby Matrix library for basic Matrix operations."
|
8
|
+
|
9
|
+
s.authors = ['Kristofer Rye',
|
10
|
+
'Christopher Cooper',
|
11
|
+
'Sam Craig']
|
12
|
+
|
13
|
+
s.homepage = 'https://github.com/frc461/opr-calc'
|
14
|
+
s.files = Dir['lib/**/*.rb'] + %W[Gemfile opr-calc.gemspec LICENSE README.md Rakefile]
|
15
|
+
s.test_files = Dir['spec/**/*.rb'] + %W[.rspec Guardfile]
|
16
|
+
s.required_ruby_version = '>= 1.9.3'
|
17
|
+
s.license = 'GPL-3.0'
|
18
|
+
|
19
|
+
s.add_development_dependency 'rake', '~> 11'
|
20
|
+
s.add_development_dependency 'rspec', '~> 3.4'
|
21
|
+
s.add_development_dependency 'guard', '~> 2.14'
|
22
|
+
s.add_development_dependency 'guard-rspec', '~> 4.7'
|
23
|
+
|
24
|
+
s.add_development_dependency 'pry', '~> 0'
|
25
|
+
s.add_development_dependency 'pry-doc', '~> 0.9'
|
26
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'opr-calc/matrix' do
|
4
|
+
it 'can be required without error' do
|
5
|
+
expect { require subject }.not_to raise_error
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
require 'opr-calc/matrix'
|
10
|
+
|
11
|
+
describe Matrix do
|
12
|
+
context 'when symmetric' do
|
13
|
+
subject :symmetric_matrix do
|
14
|
+
Matrix[[ 4, 12, -16 ],
|
15
|
+
[ 12, 37, -43 ],
|
16
|
+
[ -16, -43, 98 ]]
|
17
|
+
end
|
18
|
+
|
19
|
+
describe '#symmetric?' do
|
20
|
+
it 'returns true' do
|
21
|
+
expect(symmetric_matrix.symmetric?).to be(true)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
describe '#cholesky_factor' do
|
26
|
+
let :expected_cholesky_factorization do
|
27
|
+
Matrix[[ 2, 0, 0],
|
28
|
+
[ 6, 1, 0],
|
29
|
+
[-8, 5, 3]]
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'returns a Matrix' do
|
33
|
+
expect(symmetric_matrix.cholesky_factor).to be_a(Matrix)
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'properly calculates the Cholesky factorization' do
|
37
|
+
expect(symmetric_matrix.cholesky_factor).to eq(expected_cholesky_factorization)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
context 'when asymmetric' do
|
43
|
+
subject :asymmetric_matrix do
|
44
|
+
Matrix[[3, 5, 3],
|
45
|
+
[2, 4, 2],
|
46
|
+
[5, 2, 1]]
|
47
|
+
end
|
48
|
+
|
49
|
+
describe '#symmetric?' do
|
50
|
+
it 'returns false' do
|
51
|
+
expect(asymmetric_matrix.symmetric?).to be(false)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
describe '#cholesky_factor' do
|
56
|
+
it 'raises an error' do
|
57
|
+
expect { asymmetric_matrix.cholesky_factor }.to raise_error(ArgumentError, 'must provide a symmetric matrix')
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'opr-calc/score_set' do
|
4
|
+
it 'can be required without error' do
|
5
|
+
expect { require subject }.not_to raise_error
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
require 'opr-calc/score_set'
|
10
|
+
require 'opr-calc/matrix'
|
11
|
+
|
12
|
+
describe OPRCalc::ScoreSet do
|
13
|
+
context 'taking 4 valid arguments' do
|
14
|
+
let :ared do
|
15
|
+
Matrix[[1, 0, 1, 0, 1, 0, 0, 0, 0, 0],
|
16
|
+
[0, 1, 0, 1, 0, 1, 0, 0, 0, 0],
|
17
|
+
[0, 0, 0, 0, 1, 1, 1, 0, 0, 0],
|
18
|
+
[0, 0, 0, 0, 0, 0, 0, 1, 1, 1],
|
19
|
+
[0, 1, 0, 0, 0, 1, 1, 0, 0, 0],
|
20
|
+
[1, 0, 0, 1, 0, 0, 1, 0, 0, 0]]
|
21
|
+
end
|
22
|
+
|
23
|
+
let :ablue do
|
24
|
+
Matrix[[0, 0, 1, 0, 0, 0, 0, 1, 0, 1],
|
25
|
+
[1, 0, 0, 0, 1, 0, 0, 0, 1, 0],
|
26
|
+
[1, 1, 1, 0, 0, 0, 0, 0, 0, 0],
|
27
|
+
[0, 0, 1, 0, 0, 1, 1, 0, 0, 0],
|
28
|
+
[0, 0, 0, 1, 0, 0, 0, 0, 1, 1],
|
29
|
+
[0, 1, 0, 0, 1, 0, 0, 1, 0, 0]]
|
30
|
+
end
|
31
|
+
|
32
|
+
let :scorered do
|
33
|
+
Matrix[[6],
|
34
|
+
[9],
|
35
|
+
[15],
|
36
|
+
[24],
|
37
|
+
[12],
|
38
|
+
[9]]
|
39
|
+
end
|
40
|
+
|
41
|
+
let :scoreblue do
|
42
|
+
Matrix[[18],
|
43
|
+
[12],
|
44
|
+
[3],
|
45
|
+
[13],
|
46
|
+
[20],
|
47
|
+
[12]]
|
48
|
+
end
|
49
|
+
|
50
|
+
subject do
|
51
|
+
OPRCalc::ScoreSet.new ared, ablue, scorered, scoreblue
|
52
|
+
end
|
53
|
+
|
54
|
+
describe '#opr' do
|
55
|
+
let :expected do
|
56
|
+
Matrix[[0],
|
57
|
+
[1],
|
58
|
+
[2],
|
59
|
+
[3],
|
60
|
+
[4],
|
61
|
+
[5],
|
62
|
+
[6],
|
63
|
+
[7],
|
64
|
+
[8],
|
65
|
+
[9]]
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'produces a fully Numeric OPR vector that is finite' do
|
69
|
+
subject.opr.each do |cell|
|
70
|
+
expect(cell).to be_a(Numeric)
|
71
|
+
expect(cell).to be_finite
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
it 'produces the expected OPR' do
|
76
|
+
expect(subject.opr.round).to eq(expected)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
describe '#dpr' do
|
81
|
+
it 'produces a fully Numeric DPR vector that is finite' do
|
82
|
+
subject.dpr.each do |cell|
|
83
|
+
expect(cell).to be_a(Numeric)
|
84
|
+
expect(cell).to be_finite
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
describe '#ccwm' do
|
90
|
+
it 'produces a fully Numeric CCWM vector that is finite' do
|
91
|
+
subject.ccwm.each do |cell|
|
92
|
+
expect(cell).to be_a(Numeric)
|
93
|
+
expect(cell).to be_finite
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
it 'produces the correct OPR-DPR values' do
|
98
|
+
expect(subject.ccwm.round(2)).to eq((subject.opr - subject.dpr).round(2))
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|