opr-calc 0.1.2 → 1.0.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.
@@ -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