lib/opr-calc.rb in opr-calc-0.1.0 vs lib/opr-calc.rb in opr-calc-0.1.1
- old
+ new
@@ -1,325 +1,20 @@
-#!/usr/bin/env ruby
-
=begin
- opr-calc is a tool for calculating OPR and other scouting stats for FRC teams.
- Copyright (C) 2014 Kristofer Rye and Christopher Cooper
+ opr-calc is a tool for calculating OPR and other scouting stats for FRC teams.
+ Copyright (C) 2014 Kristofer Rye and Christopher Cooper
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
- You should have received a copy of the GNU General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
=end
-require 'matrix'
-
-# Props to http://rosettacode.org/wiki/Cholesky_decomposition#Ruby :L
-class Matrix
- # Returns whether or not the Matrix is symmetric (http://en.wikipedia.org/wiki/Symmetric_matrix)
- def symmetric?
-
- # Matrices can't be symmetric if they're not square.
- return false if not square?
-
- (0...row_size).each do |i|
- (0..i).each do |j|
- return false if self[i,j] != self[j,i]
- end
- end
-
- true
- end
-
- def cholesky_factor
- # We need a symmetric matrix for Cholesky.
- raise ArgumentError, "You must provide symmetric matrix" unless symmetric?
-
- # Make a new matrix to return
- l = Array.new(row_size) { Array.new(row_size, 0) }
-
- (0...row_size).each do |k|
- (0...row_size).each do |i|
- if i == k
- sum = (0..k-1).inject(0.0) {|sum, j| sum + l[k][j] ** 2}
- val = Math.sqrt(self[k,k] - sum)
- l[k][k] = val
- elsif i > k
- sum = (0..k-1).inject(0.0) {|sum, j| sum + l[i][j] * l[k][j]}
- val = (self[k,i] - sum) / l[k][k]
- l[i][k] = val
- end
- end
- end
-
- Matrix[*l]
- end
-
- # A helpful debug function for Matrices
- def output
- (0..self.row_size - 1).each do |row_number|
- (0..self.column_size - 1).each do |column_number|
- printf("%8.4f ", self[row_number, column_number])
- end
- printf("\n")
- end
- self
- end
-end
-
-
-
-class ScoreSet
- attr_reader :ared, :ablue, :scorered, :scoreblue
-
- def ared=(value)
- @ared = value
- @opr_recalc = @dpr_recalc = @ccwm_recalc = true
- end
- def ablue=(value)
- @ablue = value
- @opr_recalc = @dpr_recalc = @ccwm_recalc = true
- end
- def scorered=(value)
- @scorered = value
- @opr_recalc = @dpr_recalc = @ccwm_recalc = true
- end
- def scoreblue=(value)
- @scoreblue = value
- @opr_recalc = @dpr_recalc = @ccwm_recalc = true
- end
-
- def initialize(ared, ablue, scorered, scoreblue)
- @ared = ared
- @ablue = ablue
- @scorered = scorered
- @scoreblue = scoreblue
- end
-
- # A generic function for smooshing two matrices (one red, one blue).
- # Each should have the same dimensions.
- def alliance_smooshey(redmatrix, bluematrix)
- throw ArgumentError "Matrices must have same dimensions" unless (redmatrix.row_size == bluematrix.row_size) && (redmatrix.column_size == bluematrix.column_size)
-
- # Then we just pull the column and row size from the red matrix because we can.
- column_count = redmatrix.column_size
- row_count = redmatrix.row_size
-
- # Use a block function to generate the new matrix
- matrix = Matrix.build(row_count * 2, column_count) do
- |row, column|
-
- # note: no need to alternate, instead put all red, then all blue
- if row < row_count # first half = red
- redmatrix[row, column]
- else # second half = blue
- bluematrix[row - row_count, column]
- end
- end
- # This will end up looking like as follows:
-
- # [[red[0]],
- # [red[1]],
- # [red[2]],
- # ...
- # [red[n]],
- # [blue[0]],
- # [blue[1]],
- # [blue[2]],
- # ...
- # [blue[n]]]
- return matrix
- end
-
- private :alliance_smooshey
-
- # Solve equation of form [l][x] = [s] for [x]
- # l must be a lower triangular matrix.
- # Based off of algorithm given at http://en.wikipedia.org/wiki/Triangular_matrix#Forward_and_back_substitution
- def forward_substitute(l, s)
- raise "l must be a lower triangular matrix" unless l.lower_triangular?
-
- x = Array.new s.row_size
-
- x.size.times do |i|
- x[i] = s[i, 0]
- i.times do |j|
- x[i] -= l[i, j] * x[j]
- end
- x[i] /= l[i, i]
- end
-
- Matrix.column_vector x
- end
-
- # Solve equation of form [u][x] = [s] for [x]
- # u must be a upper triangular matrix.
- def back_substitute(u, s)
- raise "u must be an upper triangular matrix" unless u.upper_triangular?
-
- x = Array.new s.row_size
-
- (x.size - 1).downto 0 do |i|
- x[i] = s[i, 0]
- (i + 1).upto(x.size - 1) do |j|
- x[i] -= u[i, j] * x[j]
- end
- x[i] /= u[i, i]
- end
-
- Matrix.column_vector x
- end
-
- private :forward_substitute, :back_substitute
-
-=begin
- base matrix equation: [A][OPR] = [SCORE]
- define [A]^t to be [A] transpose
- define [P] to be [A]^t[A]
- define [S] to be [A]^t[SCORE]
- equation is now [P][OPR] = [S]
- refactor [P] as [L][L]^t using cholesky
- [L] is a lower triangular matrix and [L]^t an upper
- Therefore [L][L]^t[OPR] = [S]
- define [Y] = [L]^t[OPR]
- equation is now [L][Y] = [S]
- find [Y] through forward substitution
- find [OPR] through back substitution
-=end
-
- def opr_calculate(a, score)
- p = a.t * a
- s = a.t * score
-
- l = p.cholesky_factor
-
- # l.output
- # l.t.output
-
- y = forward_substitute l, s
- back_substitute l.t, y
- end
-
- # Offensive power rating: the average amount of points that a team contributes to their alliance's score
- # This is high for a good team
- def opr(recalc = false)
- if !@opr || recalc || @opr_recalc
- a = alliance_smooshey @ared, @ablue
- score = alliance_smooshey @scorered, @scoreblue
-
- @opr = opr_calculate a, score
- @opr_recalc = false
- end
- @opr
- end
-
- # Defensive power rating: the average amount of points that a team lets the other alliance score
- # This is low for a good team
- def dpr(recalc = false)
- if !@dpr || recalc || @dpr_recalc
- a = alliance_smooshey @ared, @ablue
- score = alliance_smooshey @scoreblue, @scorered # intentionally swapped, that's how dpr works
-
- @dpr = opr_calculate a, score
- @dpr_recalc = false
- end
- @dpr
- end
-
- # Calculated contribution to winning margin: the average amount of points that a team contributes to their alliance's winning margin
- # This is high for a good team
- def ccwm(recalc = false)
- if !@ccwm || recalc || @ccwm_recalc
- a = alliance_smooshey @ared, @ablue
-
- red_wm = Matrix.build(@scorered.row_size, @scorered.column_size) do |row, column|
- @scorered[row, column] - @scoreblue[row, column]
- end
- blue_wm = Matrix.build(@scoreblue.row_size, @scoreblue.column_size) do |row, column|
- @scoreblue[row, column] - @scorered[row, column]
- end
-
- score = alliance_smooshey red_wm, blue_wm
-
- @ccwm = opr_calculate a, score
- @ccwm_recalc = false
- end
- @ccwm
- end
-
-end
-
-
-def test_stuff
- # Team 0 opr: 0
- # Team 1 opr: 1
- # Team 2 opr: 2
- # ...
-
- # I don't think any team is every playing on both blue and red at the same time, but I might be wrong
-
- # 0 1 2 3 4 5 6 7 8 9
- test_ared = Matrix[[1, 0, 1, 0, 1, 0, 0, 0, 0, 0],
- [0, 1, 0, 1, 0, 1, 0, 0, 0, 0],
- [0, 0, 0, 0, 1, 1, 1, 0, 0, 0],
- [0, 0, 0, 0, 0, 0, 0, 1, 1, 1],
- [0, 1, 0, 0, 0, 1, 1, 0, 0, 0],
- [1, 0, 0, 1, 0, 0, 1, 0, 0, 0]]
-
- # 0 1 2 3 4 5 6 7 8 9
- test_ablue = Matrix[[0, 0, 1, 0, 0, 0, 0, 1, 0, 1],
- [1, 0, 0, 0, 1, 0, 0, 0, 1, 0],
- [1, 1, 1, 0, 0, 0, 0, 0, 0, 0],
- [0, 0, 1, 0, 0, 1, 1, 0, 0, 0],
- [0, 0, 0, 1, 0, 0, 0, 0, 1, 1],
- [0, 1, 0, 0, 1, 0, 0, 1, 0, 0]]
-
-
- test_scorered = Matrix[[6],
- [9],
- [15],
- [24],
- [12],
- [9]]
-
- test_scoreblue = Matrix[[18],
- [12],
- [3],
- [13],
- [20],
- [12]]
-
- test_expectedopr = Matrix[[0],
- [1],
- [2],
- [3],
- [4],
- [5],
- [6],
- [7],
- [8],
- [9]]
-
- test = ScoreSet.new test_ared, test_ablue, test_scorered, test_scoreblue
-
- puts "Expected OPR:"
- test_expectedopr.output
- puts "Actual OPR:"
- test.opr.output
-
- puts "DPR:"
- test.dpr.output
-
- puts "CCWM:"
- test.ccwm.output
- puts "CCWM by OPR - DPR:"
- (test.opr - test.dpr).output
-
- return true
-end
+require "opr-calc/matrix.rb"
+require "opr-calc/score_set.rb"