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.
@@ -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.
@@ -0,0 +1,4 @@
1
+ require 'rspec/core/rake_task'
2
+ RSpec::Core::RakeTask.new :spec
3
+
4
+ task default: :spec
@@ -16,5 +16,5 @@
16
16
  along with this program. If not, see <http://www.gnu.org/licenses/>.
17
17
  =end
18
18
 
19
- require "opr-calc/matrix.rb"
20
- require "opr-calc/score_set.rb"
19
+ require 'opr-calc/matrix.rb'
20
+ require 'opr-calc/score_set.rb'
@@ -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 "matrix"
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, "must provide a symmetric matrix" unless symmetric?
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
- printf("%8.4f ", self[row_number, column_number])
74
+
75
+ printf('%8.4f ', self[row_number, column_number])
76
+
68
77
  end
69
78
 
70
- printf("\n")
79
+ printf('\n')
71
80
  end
72
81
 
73
82
  self
@@ -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, "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
-
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 "Matrices must have same dimensions" unless (redmatrix.row_size == bluematrix.row_size) && (redmatrix.column_size == bluematrix.column_size)
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 from the red matrix because we can.
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 `http://en.wikipedia.org/wiki/Triangular_matrix#Forward_and_back_substitution`.
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 "l must be a lower triangular matrix" unless l.lower_triangular?
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 "u must be an upper triangular matrix" unless u.upper_triangular?
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 contributes to their alliance's score.
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 the other alliance score.
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
- score = alliance_smooshey @scoreblue, @scorered # intentionally swapped, that's how dpr works.
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 points that a team contributes to their alliance's winning margin.
191
- # This is high for a good team.
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
@@ -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