Initial version of Moserware.Skills TrueSkill calculator to go along with my Computing Your Skill blog post

This commit is contained in:
Jeff Moser
2010-03-18 07:39:48 -04:00
commit cb46631ff8
77 changed files with 6172 additions and 0 deletions

View File

@ -0,0 +1,23 @@
using System;
using Moserware.Numerics;
namespace Moserware.Skills.TrueSkill
{
internal static class DrawMargin
{
public static double GetDrawMarginFromDrawProbability(double drawProbability, double beta)
{
// Derived from TrueSkill technical report (MSR-TR-2006-80), page 6
// draw probability = 2 * CDF(margin/(sqrt(n1+n2)*beta)) -1
// implies
//
// margin = inversecdf((draw probability + 1)/2) * sqrt(n1+n2) * beta
// n1 and n2 are the number of players on each team
double margin = GaussianDistribution.InverseCumulativeTo(.5*(drawProbability + 1), 0, 1)*Math.Sqrt(1 + 1)*
beta;
return margin;
}
}
}

View File

@ -0,0 +1,156 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Moserware.Numerics;
namespace Moserware.Skills.TrueSkill
{
/// <summary>
/// Calculates TrueSkill using a full factor graph.
/// </summary>
internal class FactorGraphTrueSkillCalculator : SkillCalculator
{
public FactorGraphTrueSkillCalculator()
: base(SupportedOptions.PartialPlay | SupportedOptions.PartialUpdate, TeamsRange.AtLeast(2), PlayersRange.AtLeast(1))
{
}
public override IDictionary<TPlayer, Rating> CalculateNewRatings<TPlayer>(GameInfo gameInfo,
IEnumerable<IDictionary<TPlayer, Rating>> teams,
params int[] teamRanks)
{
Guard.ArgumentNotNull(gameInfo, "gameInfo");
ValidateTeamCountAndPlayersCountPerTeam(teams);
RankSorter.Sort(ref teams, ref teamRanks);
var factorGraph = new TrueSkillFactorGraph<TPlayer>(gameInfo, teams, teamRanks);
factorGraph.BuildGraph();
factorGraph.RunSchedule();
double probabilityOfOutcome = factorGraph.GetProbabilityOfRanking();
return factorGraph.GetUpdatedRatings();
}
public override double CalculateMatchQuality<TPlayer>(GameInfo gameInfo,
IEnumerable<IDictionary<TPlayer, Rating>> teams)
{
// We need to create the A matrix which is the player team assigments.
List<IDictionary<TPlayer, Rating>> teamAssignmentsList = teams.ToList();
Matrix skillsMatrix = GetPlayerCovarianceMatrix(teamAssignmentsList);
Vector meanVector = GetPlayerMeansVector(teamAssignmentsList);
Matrix meanVectorTranspose = meanVector.Transpose;
Matrix playerTeamAssignmentsMatrix = CreatePlayerTeamAssignmentMatrix(teamAssignmentsList, meanVector.Rows);
Matrix playerTeamAssignmentsMatrixTranspose = playerTeamAssignmentsMatrix.Transpose;
double betaSquared = Square(gameInfo.Beta);
Matrix start = meanVectorTranspose * playerTeamAssignmentsMatrix;
Matrix aTa = (betaSquared * playerTeamAssignmentsMatrixTranspose) * playerTeamAssignmentsMatrix;
Matrix aTSA = playerTeamAssignmentsMatrixTranspose * skillsMatrix * playerTeamAssignmentsMatrix;
Matrix middle = aTa + aTSA;
Matrix middleInverse = middle.Inverse;
Matrix end = playerTeamAssignmentsMatrixTranspose * meanVector;
Matrix expPartMatrix = -0.5 * (start * middleInverse * end);
double expPart = expPartMatrix.Determinant;
double sqrtPartNumerator = aTa.Determinant;
double sqrtPartDenominator = middle.Determinant;
double sqrtPart = sqrtPartNumerator / sqrtPartDenominator;
double result = Math.Exp(expPart) * Math.Sqrt(sqrtPart);
return result;
}
private static Vector GetPlayerMeansVector<TPlayer>(
IEnumerable<IDictionary<TPlayer, Rating>> teamAssignmentsList)
{
// A simple vector of all the player means.
return new Vector(GetPlayerRatingValues(teamAssignmentsList, rating => rating.Mean));
}
private static Matrix GetPlayerCovarianceMatrix<TPlayer>(
IEnumerable<IDictionary<TPlayer, Rating>> teamAssignmentsList)
{
// This is a square matrix whose diagonal values represent the variance (square of standard deviation) of all
// players.
return
new DiagonalMatrix(GetPlayerRatingValues(teamAssignmentsList, rating => Square(rating.StandardDeviation)));
}
// Helper function that gets a list of values for all player ratings
private static IList<double> GetPlayerRatingValues<TPlayer>(
IEnumerable<IDictionary<TPlayer, Rating>> teamAssignmentsList, Func<Rating, double> playerRatingFunction)
{
var playerRatingValues = new List<double>();
foreach (var currentTeam in teamAssignmentsList)
{
foreach (Rating currentRating in currentTeam.Values)
{
playerRatingValues.Add(playerRatingFunction(currentRating));
}
}
return playerRatingValues;
}
private static Matrix CreatePlayerTeamAssignmentMatrix<TPlayer>(
IList<IDictionary<TPlayer, Rating>> teamAssignmentsList, int totalPlayers)
{
// The team assignment matrix is often referred to as the "A" matrix. It's a matrix whose rows represent the players
// and the columns represent teams. At Matrix[row, column] represents that player[row] is on team[col]
// Positive values represent an assignment and a negative value means that we subtract the value of the next
// team since we're dealing with pairs. This means that this matrix always has teams - 1 columns.
// The only other tricky thing is that values represent the play percentage.
// For example, consider a 3 team game where team1 is just player1, team 2 is player 2 and player 3, and
// team3 is just player 4. Furthermore, player 2 and player 3 on team 2 played 25% and 75% of the time
// (e.g. partial play), the A matrix would be:
// A = this 4x2 matrix:
// | 1.00 0.00 |
// | -0.25 0.25 |
// | -0.75 0.75 |
// | 0.00 -1.00 |
var playerAssignments = new List<IEnumerable<double>>();
int totalPreviousPlayers = 0;
for (int i = 0; i < teamAssignmentsList.Count - 1; i++)
{
IDictionary<TPlayer, Rating> currentTeam = teamAssignmentsList[i];
// Need to add in 0's for all the previous players, since they're not
// on this team
var currentRowValues = new List<double>(new double[totalPreviousPlayers]);
playerAssignments.Add(currentRowValues);
foreach (var currentRating in currentTeam)
{
currentRowValues.Add(PartialPlay.GetPartialPlayPercentage(currentRating.Key));
// indicates the player is on the team
totalPreviousPlayers++;
}
IDictionary<TPlayer, Rating> nextTeam = teamAssignmentsList[i + 1];
foreach (var nextTeamPlayerPair in nextTeam)
{
// Add a -1 * playing time to represent the difference
currentRowValues.Add(-1 * PartialPlay.GetPartialPlayPercentage(nextTeamPlayerPair.Key));
}
}
var playerTeamAssignmentsMatrix = new Matrix(totalPlayers, teamAssignmentsList.Count - 1, playerAssignments);
return playerTeamAssignmentsMatrix;
}
}
}

View File

@ -0,0 +1,33 @@
using Moserware.Numerics;
using Moserware.Skills.FactorGraphs;
namespace Moserware.Skills.TrueSkill.Factors
{
public abstract class GaussianFactor : Factor<GaussianDistribution>
{
protected GaussianFactor(string name)
: base(name)
{
}
/// Sends the factor-graph message with and returns the log-normalization constant
protected override double SendMessage(Message<GaussianDistribution> message,
Variable<GaussianDistribution> variable)
{
GaussianDistribution marginal = variable.Value;
GaussianDistribution messageValue = message.Value;
double logZ = GaussianDistribution.LogProductNormalization(marginal, messageValue);
variable.Value = marginal*messageValue;
return logZ;
}
public override Message<GaussianDistribution> CreateVariableToMessageBinding(
Variable<GaussianDistribution> variable)
{
return CreateVariableToMessageBinding(variable,
new Message<GaussianDistribution>(
GaussianDistribution.FromPrecisionMean(0, 0),
"message from {0} to {1}", this, variable));
}
}
}

View File

@ -0,0 +1,75 @@
using System;
using Moserware.Numerics;
using Moserware.Skills.FactorGraphs;
namespace Moserware.Skills.TrueSkill.Factors
{
/// <summary>
/// Factor representing a team difference that has exceeded the draw margin.
/// </summary>
/// <remarks>See the accompanying math paper for more details.</remarks>
public class GaussianGreaterThanFactor : GaussianFactor
{
private readonly double _Epsilon;
public GaussianGreaterThanFactor(double epsilon, Variable<GaussianDistribution> variable)
: base(String.Format("{0} > {1:0.000}", variable, epsilon))
{
_Epsilon = epsilon;
CreateVariableToMessageBinding(variable);
}
public override double LogNormalization
{
get
{
GaussianDistribution marginal = Variables[0].Value;
GaussianDistribution message = Messages[0].Value;
GaussianDistribution messageFromVariable = marginal/message;
return -GaussianDistribution.LogProductNormalization(messageFromVariable, message)
+
Math.Log(
GaussianDistribution.CumulativeTo((messageFromVariable.Mean - _Epsilon)/
messageFromVariable.StandardDeviation));
}
}
protected override double UpdateMessage(Message<GaussianDistribution> message,
Variable<GaussianDistribution> variable)
{
GaussianDistribution oldMarginal = variable.Value.Clone();
GaussianDistribution oldMessage = message.Value.Clone();
GaussianDistribution messageFromVar = oldMarginal/oldMessage;
double c = messageFromVar.Precision;
double d = messageFromVar.PrecisionMean;
double sqrtC = Math.Sqrt(c);
double dOnSqrtC = d/sqrtC;
double epsilsonTimesSqrtC = _Epsilon*sqrtC;
d = messageFromVar.PrecisionMean;
double denom = 1.0 - TruncatedGaussianCorrectionFunctions.WExceedsMargin(dOnSqrtC, epsilsonTimesSqrtC);
double newPrecision = c/denom;
double newPrecisionMean = (d +
sqrtC*
TruncatedGaussianCorrectionFunctions.VExceedsMargin(dOnSqrtC, epsilsonTimesSqrtC))/
denom;
GaussianDistribution newMarginal = GaussianDistribution.FromPrecisionMean(newPrecisionMean, newPrecision);
GaussianDistribution newMessage = oldMessage*newMarginal/oldMarginal;
/// Update the message and marginal
message.Value = newMessage;
variable.Value = newMarginal;
/// Return the difference in the new marginal
return newMarginal - oldMarginal;
}
}
}

View File

@ -0,0 +1,72 @@
using System;
using Moserware.Numerics;
using Moserware.Skills.FactorGraphs;
namespace Moserware.Skills.TrueSkill.Factors
{
/// <summary>
/// Connects two variables and adds uncertainty.
/// </summary>
/// <remarks>See the accompanying math paper for more details.</remarks>
public class GaussianLikelihoodFactor : GaussianFactor
{
private readonly double _Precision;
public GaussianLikelihoodFactor(double betaSquared, Variable<GaussianDistribution> variable1,
Variable<GaussianDistribution> variable2)
: base(String.Format("Likelihood of {0} going to {1}", variable2, variable1))
{
_Precision = 1.0/betaSquared;
CreateVariableToMessageBinding(variable1);
CreateVariableToMessageBinding(variable2);
}
public override double LogNormalization
{
get { return GaussianDistribution.LogRatioNormalization(Variables[0].Value, Messages[0].Value); }
}
private double UpdateHelper(Message<GaussianDistribution> message1, Message<GaussianDistribution> message2,
Variable<GaussianDistribution> variable1, Variable<GaussianDistribution> variable2)
{
GaussianDistribution message1Value = message1.Value.Clone();
GaussianDistribution message2Value = message2.Value.Clone();
GaussianDistribution marginal1 = variable1.Value.Clone();
GaussianDistribution marginal2 = variable2.Value.Clone();
double a = _Precision/(_Precision + marginal2.Precision - message2Value.Precision);
GaussianDistribution newMessage = GaussianDistribution.FromPrecisionMean(
a*(marginal2.PrecisionMean - message2Value.PrecisionMean),
a*(marginal2.Precision - message2Value.Precision));
GaussianDistribution oldMarginalWithoutMessage = marginal1/message1Value;
GaussianDistribution newMarginal = oldMarginalWithoutMessage*newMessage;
/// Update the message and marginal
message1.Value = newMessage;
variable1.Value = newMarginal;
/// Return the difference in the new marginal
return newMarginal - marginal1;
}
public override double UpdateMessage(int messageIndex)
{
switch (messageIndex)
{
case 0:
return UpdateHelper(Messages[0], Messages[1],
Variables[0], Variables[1]);
case 1:
return UpdateHelper(Messages[1], Messages[0],
Variables[1], Variables[0]);
default:
throw new ArgumentOutOfRangeException();
}
}
}
}

View File

@ -0,0 +1,39 @@
using System;
using Moserware.Numerics;
using Moserware.Skills.FactorGraphs;
namespace Moserware.Skills.TrueSkill.Factors
{
/// <summary>
/// Supplies the factor graph with prior information.
/// </summary>
/// <remarks>See the accompanying math paper for more details.</remarks>
public class GaussianPriorFactor : GaussianFactor
{
private readonly GaussianDistribution _NewMessage;
public GaussianPriorFactor(double mean, double variance, Variable<GaussianDistribution> variable)
: base(String.Format("Prior value going to {0}", variable))
{
_NewMessage = new GaussianDistribution(mean, Math.Sqrt(variance));
CreateVariableToMessageBinding(variable,
new Message<GaussianDistribution>(
GaussianDistribution.FromPrecisionMean(0, 0), "message from {0} to {1}",
this, variable));
}
protected override double UpdateMessage(Message<GaussianDistribution> message,
Variable<GaussianDistribution> variable)
{
GaussianDistribution oldMarginal = variable.Value.Clone();
Message<GaussianDistribution> oldMessage = message;
GaussianDistribution newMarginal =
GaussianDistribution.FromPrecisionMean(
oldMarginal.PrecisionMean + _NewMessage.PrecisionMean - oldMessage.Value.PrecisionMean,
oldMarginal.Precision + _NewMessage.Precision - oldMessage.Value.Precision);
variable.Value = newMarginal;
message.Value = _NewMessage;
return oldMarginal - newMarginal;
}
}
}

View File

@ -0,0 +1,252 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using Moserware.Numerics;
using Moserware.Skills.FactorGraphs;
namespace Moserware.Skills.TrueSkill.Factors
{
/// <summary>
/// Factor that sums together multiple Gaussians.
/// </summary>
/// <remarks>See the accompanying math paper for more details.</remarks>
public class GaussianWeightedSumFactor : GaussianFactor
{
private readonly List<int[]> _VariableIndexOrdersForWeights = new List<int[]>();
// This following is used for convenience, for example, the first entry is [0, 1, 2]
// corresponding to v[0] = a1*v[1] + a2*v[2]
private readonly double[][] _Weights;
private readonly double[][] _WeightsSquared;
public GaussianWeightedSumFactor(Variable<GaussianDistribution> sumVariable,
Variable<GaussianDistribution>[] variablesToSum)
: this(sumVariable,
variablesToSum,
variablesToSum.Select(v => 1.0).ToArray()) // By default, set the weight to 1.0
{
}
public GaussianWeightedSumFactor(Variable<GaussianDistribution> sumVariable,
Variable<GaussianDistribution>[] variablesToSum, double[] variableWeights)
: base(CreateName(sumVariable, variablesToSum, variableWeights))
{
_Weights = new double[variableWeights.Length + 1][];
_WeightsSquared = new double[_Weights.Length][];
// The first weights are a straightforward copy
// v_0 = a_1*v_1 + a_2*v_2 + ... + a_n * v_n
_Weights[0] = new double[variableWeights.Length];
Array.Copy(variableWeights, _Weights[0], variableWeights.Length);
_WeightsSquared[0] = _Weights[0].Select(w => w*w).ToArray();
// 0..n-1
_VariableIndexOrdersForWeights.Add(Enumerable.Range(0, 1 + variablesToSum.Length).ToArray());
// The rest move the variables around and divide out the constant.
// For example:
// v_1 = (-a_2 / a_1) * v_2 + (-a3/a1) * v_3 + ... + (1.0 / a_1) * v_0
// By convention, we'll put the v_0 term at the end
for (int weightsIndex = 1; weightsIndex < _Weights.Length; weightsIndex++)
{
var currentWeights = new double[variableWeights.Length];
_Weights[weightsIndex] = currentWeights;
var variableIndices = new int[variableWeights.Length + 1];
variableIndices[0] = weightsIndex;
var currentWeightsSquared = new double[variableWeights.Length];
_WeightsSquared[weightsIndex] = currentWeightsSquared;
// keep a single variable to keep track of where we are in the array.
// This is helpful since we skip over one of the spots
int currentDestinationWeightIndex = 0;
for (int currentWeightSourceIndex = 0;
currentWeightSourceIndex < variableWeights.Length;
currentWeightSourceIndex++)
{
// TODO: get this test to be right
if (currentWeightSourceIndex == (weightsIndex - 1))
{
continue;
}
double currentWeight = (-variableWeights[currentWeightSourceIndex]/variableWeights[weightsIndex - 1]);
if (variableWeights[weightsIndex - 1] == 0)
{
// HACK: Getting around division by zero
currentWeight = 0;
}
currentWeights[currentDestinationWeightIndex] = currentWeight;
currentWeightsSquared[currentDestinationWeightIndex] = currentWeight*currentWeight;
variableIndices[currentDestinationWeightIndex + 1] = currentWeightSourceIndex + 1;
currentDestinationWeightIndex++;
}
// And the final one
double finalWeight = 1.0/variableWeights[weightsIndex - 1];
if (variableWeights[weightsIndex - 1] == 0)
{
// HACK: Getting around division by zero
finalWeight = 0;
}
currentWeights[currentDestinationWeightIndex] = finalWeight;
currentWeightsSquared[currentDestinationWeightIndex] = finalWeight*finalWeight;
variableIndices[variableIndices.Length - 1] = 0;
_VariableIndexOrdersForWeights.Add(variableIndices);
}
CreateVariableToMessageBinding(sumVariable);
foreach (var currentVariable in variablesToSum)
{
CreateVariableToMessageBinding(currentVariable);
}
}
public override double LogNormalization
{
get
{
ReadOnlyCollection<Variable<GaussianDistribution>> vars = Variables;
ReadOnlyCollection<Message<GaussianDistribution>> messages = Messages;
double result = 0.0;
// We start at 1 since offset 0 has the sum
for (int i = 1; i < vars.Count; i++)
{
result += GaussianDistribution.LogRatioNormalization(vars[i].Value, messages[i].Value);
}
return result;
}
}
private double UpdateHelper(double[] weights, double[] weightsSquared,
IList<Message<GaussianDistribution>> messages,
IList<Variable<GaussianDistribution>> variables)
{
// Potentially look at http://mathworld.wolfram.com/NormalSumDistribution.html for clues as
// to what it's doing
GaussianDistribution message0 = messages[0].Value.Clone();
GaussianDistribution marginal0 = variables[0].Value.Clone();
// The math works out so that 1/newPrecision = sum of a_i^2 /marginalsWithoutMessages[i]
double inverseOfNewPrecisionSum = 0.0;
double anotherInverseOfNewPrecisionSum = 0.0;
double weightedMeanSum = 0.0;
double anotherWeightedMeanSum = 0.0;
for (int i = 0; i < weightsSquared.Length; i++)
{
// These flow directly from the paper
inverseOfNewPrecisionSum += weightsSquared[i]/
(variables[i + 1].Value.Precision - messages[i + 1].Value.Precision);
GaussianDistribution diff = (variables[i + 1].Value/messages[i + 1].Value);
anotherInverseOfNewPrecisionSum += weightsSquared[i]/diff.Precision;
weightedMeanSum += weights[i]
*
(variables[i + 1].Value.PrecisionMean - messages[i + 1].Value.PrecisionMean)
/
(variables[i + 1].Value.Precision - messages[i + 1].Value.Precision);
anotherWeightedMeanSum += weights[i]*diff.PrecisionMean/diff.Precision;
}
double newPrecision = 1.0/inverseOfNewPrecisionSum;
double anotherNewPrecision = 1.0/anotherInverseOfNewPrecisionSum;
double newPrecisionMean = newPrecision*weightedMeanSum;
double anotherNewPrecisionMean = anotherNewPrecision*anotherWeightedMeanSum;
GaussianDistribution newMessage = GaussianDistribution.FromPrecisionMean(newPrecisionMean, newPrecision);
GaussianDistribution oldMarginalWithoutMessage = marginal0/message0;
GaussianDistribution newMarginal = oldMarginalWithoutMessage*newMessage;
/// Update the message and marginal
messages[0].Value = newMessage;
variables[0].Value = newMarginal;
/// Return the difference in the new marginal
return newMarginal - marginal0;
}
public override double UpdateMessage(int messageIndex)
{
ReadOnlyCollection<Message<GaussianDistribution>> allMessages = Messages;
ReadOnlyCollection<Variable<GaussianDistribution>> allVariables = Variables;
Guard.ArgumentIsValidIndex(messageIndex, allMessages.Count, "messageIndex");
var updatedMessages = new List<Message<GaussianDistribution>>();
var updatedVariables = new List<Variable<GaussianDistribution>>();
int[] indicesToUse = _VariableIndexOrdersForWeights[messageIndex];
// The tricky part here is that we have to put the messages and variables in the same
// order as the weights. Thankfully, the weights and messages share the same index numbers,
// so we just need to make sure they're consistent
for (int i = 0; i < allMessages.Count; i++)
{
updatedMessages.Add(allMessages[indicesToUse[i]]);
updatedVariables.Add(allVariables[indicesToUse[i]]);
}
return UpdateHelper(_Weights[messageIndex], _WeightsSquared[messageIndex], updatedMessages, updatedVariables);
}
private static string CreateName(Variable<GaussianDistribution> sumVariable,
IList<Variable<GaussianDistribution>> variablesToSum, double[] weights)
{
var sb = new StringBuilder();
sb.Append(sumVariable.ToString());
sb.Append(" = ");
for (int i = 0; i < variablesToSum.Count; i++)
{
bool isFirst = (i == 0);
if (isFirst && (weights[i] < 0))
{
sb.Append("-");
}
sb.Append(Math.Abs(weights[i]).ToString("0.00"));
sb.Append("*[");
sb.Append(variablesToSum[i]);
sb.Append("]");
bool isLast = (i == variablesToSum.Count - 1);
if (!isLast)
{
if (weights[i + 1] >= 0)
{
sb.Append(" + ");
}
else
{
sb.Append(" - ");
}
}
}
return sb.ToString();
}
}
}

View File

@ -0,0 +1,73 @@
using System;
using Moserware.Numerics;
using Moserware.Skills.FactorGraphs;
namespace Moserware.Skills.TrueSkill.Factors
{
/// <summary>
/// Factor representing a team difference that has not exceeded the draw margin.
/// </summary>
/// <remarks>See the accompanying math paper for more details.</remarks>
public class GaussianWithinFactor : GaussianFactor
{
private readonly double _Epsilon;
public GaussianWithinFactor(double epsilon, Variable<GaussianDistribution> variable)
: base(String.Format("{0} <= {1:0.000}", variable, epsilon))
{
_Epsilon = epsilon;
CreateVariableToMessageBinding(variable);
}
public override double LogNormalization
{
get
{
GaussianDistribution marginal = Variables[0].Value;
GaussianDistribution message = Messages[0].Value;
GaussianDistribution messageFromVariable = marginal/message;
double mean = messageFromVariable.Mean;
double std = messageFromVariable.StandardDeviation;
double z = GaussianDistribution.CumulativeTo((_Epsilon - mean)/std)
-
GaussianDistribution.CumulativeTo((-_Epsilon - mean)/std);
return -GaussianDistribution.LogProductNormalization(messageFromVariable, message) + Math.Log(z);
}
}
protected override double UpdateMessage(Message<GaussianDistribution> message,
Variable<GaussianDistribution> variable)
{
GaussianDistribution oldMarginal = variable.Value.Clone();
GaussianDistribution oldMessage = message.Value.Clone();
GaussianDistribution messageFromVariable = oldMarginal/oldMessage;
double c = messageFromVariable.Precision;
double d = messageFromVariable.PrecisionMean;
double sqrtC = Math.Sqrt(c);
double dOnSqrtC = d/sqrtC;
double epsilonTimesSqrtC = _Epsilon*sqrtC;
d = messageFromVariable.PrecisionMean;
double denominator = 1.0 - TruncatedGaussianCorrectionFunctions.WWithinMargin(dOnSqrtC, epsilonTimesSqrtC);
double newPrecision = c/denominator;
double newPrecisionMean = (d +
sqrtC*
TruncatedGaussianCorrectionFunctions.VWithinMargin(dOnSqrtC, epsilonTimesSqrtC))/
denominator;
GaussianDistribution newMarginal = GaussianDistribution.FromPrecisionMean(newPrecisionMean, newPrecision);
GaussianDistribution newMessage = oldMessage*newMarginal/oldMarginal;
/// Update the message and marginal
message.Value = newMessage;
variable.Value = newMarginal;
/// Return the difference in the new marginal
return newMarginal - oldMarginal;
}
}
}

View File

@ -0,0 +1,192 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Moserware.Numerics;
using Moserware.Skills.FactorGraphs;
using Moserware.Skills.TrueSkill.Factors;
namespace Moserware.Skills.TrueSkill.Layers
{
// The whole purpose of this is to do a loop on the bottom
internal class IteratedTeamDifferencesInnerLayer<TPlayer> :
TrueSkillFactorGraphLayer
<TPlayer, Variable<GaussianDistribution>, GaussianWeightedSumFactor, Variable<GaussianDistribution>>
{
private readonly TeamDifferencesComparisonLayer<TPlayer> _TeamDifferencesComparisonLayer;
private readonly TeamPerformancesToTeamPerformanceDifferencesLayer<TPlayer>
_TeamPerformancesToTeamPerformanceDifferencesLayer;
public IteratedTeamDifferencesInnerLayer(TrueSkillFactorGraph<TPlayer> parentGraph,
TeamPerformancesToTeamPerformanceDifferencesLayer<TPlayer>
teamPerformancesToPerformanceDifferences,
TeamDifferencesComparisonLayer<TPlayer> teamDifferencesComparisonLayer)
: base(parentGraph)
{
_TeamPerformancesToTeamPerformanceDifferencesLayer = teamPerformancesToPerformanceDifferences;
_TeamDifferencesComparisonLayer = teamDifferencesComparisonLayer;
}
public override IEnumerable<Factor<GaussianDistribution>> UntypedFactors
{
get
{
return
_TeamPerformancesToTeamPerformanceDifferencesLayer.UntypedFactors.Concat(
_TeamDifferencesComparisonLayer.UntypedFactors);
}
}
public override void BuildLayer()
{
_TeamPerformancesToTeamPerformanceDifferencesLayer.SetRawInputVariablesGroups(InputVariablesGroups);
_TeamPerformancesToTeamPerformanceDifferencesLayer.BuildLayer();
_TeamDifferencesComparisonLayer.SetRawInputVariablesGroups(
_TeamPerformancesToTeamPerformanceDifferencesLayer.GetRawOutputVariablesGroups());
_TeamDifferencesComparisonLayer.BuildLayer();
}
public override Schedule<GaussianDistribution> CreatePriorSchedule()
{
Schedule<GaussianDistribution> loop = null;
switch (InputVariablesGroups.Count)
{
case 0:
case 1:
throw new InvalidOperationException();
case 2:
loop = CreateTwoTeamInnerPriorLoopSchedule();
break;
default:
loop = CreateMultipleTeamInnerPriorLoopSchedule();
break;
}
// When dealing with differences, there are always (n-1) differences, so add in the 1
int totalTeamDifferences = _TeamPerformancesToTeamPerformanceDifferencesLayer.LocalFactors.Count;
int totalTeams = totalTeamDifferences + 1;
var innerSchedule = new ScheduleSequence<GaussianDistribution>(
"inner schedule",
new[]
{
loop,
new ScheduleStep<GaussianDistribution>(
"teamPerformanceToPerformanceDifferenceFactors[0] @ 1",
_TeamPerformancesToTeamPerformanceDifferencesLayer.LocalFactors[0], 1),
new ScheduleStep<GaussianDistribution>(
String.Format("teamPerformanceToPerformanceDifferenceFactors[teamTeamDifferences = {0} - 1] @ 2",
totalTeamDifferences),
_TeamPerformancesToTeamPerformanceDifferencesLayer.LocalFactors[totalTeamDifferences - 1], 2)
}
);
return innerSchedule;
}
private Schedule<GaussianDistribution> CreateTwoTeamInnerPriorLoopSchedule()
{
return ScheduleSequence(
new[]
{
new ScheduleStep<GaussianDistribution>(
"send team perf to perf differences",
_TeamPerformancesToTeamPerformanceDifferencesLayer.LocalFactors[0],
0),
new ScheduleStep<GaussianDistribution>(
"send to greater than or within factor",
_TeamDifferencesComparisonLayer.LocalFactors[0],
0)
},
"loop of just two teams inner sequence");
}
private Schedule<GaussianDistribution> CreateMultipleTeamInnerPriorLoopSchedule()
{
int totalTeamDifferences = _TeamPerformancesToTeamPerformanceDifferencesLayer.LocalFactors.Count;
var forwardScheduleList = new List<Schedule<GaussianDistribution>>();
for (int i = 0; i < totalTeamDifferences - 1; i++)
{
Schedule<GaussianDistribution> currentForwardSchedulePiece =
ScheduleSequence(
new Schedule<GaussianDistribution>[]
{
new ScheduleStep<GaussianDistribution>(
String.Format("team perf to perf diff {0}",
i),
_TeamPerformancesToTeamPerformanceDifferencesLayer.LocalFactors[i], 0),
new ScheduleStep<GaussianDistribution>(
String.Format("greater than or within result factor {0}",
i),
_TeamDifferencesComparisonLayer.LocalFactors[i],
0),
new ScheduleStep<GaussianDistribution>(
String.Format("team perf to perf diff factors [{0}], 2",
i),
_TeamPerformancesToTeamPerformanceDifferencesLayer.LocalFactors[i], 2)
}, "current forward schedule piece {0}", i);
forwardScheduleList.Add(currentForwardSchedulePiece);
}
var forwardSchedule =
new ScheduleSequence<GaussianDistribution>(
"forward schedule",
forwardScheduleList);
var backwardScheduleList = new List<Schedule<GaussianDistribution>>();
for (int i = 0; i < totalTeamDifferences - 1; i++)
{
var currentBackwardSchedulePiece = new ScheduleSequence<GaussianDistribution>(
"current backward schedule piece",
new Schedule<GaussianDistribution>[]
{
new ScheduleStep<GaussianDistribution>(
String.Format("teamPerformanceToPerformanceDifferenceFactors[totalTeamDifferences - 1 - {0}] @ 0",
i),
_TeamPerformancesToTeamPerformanceDifferencesLayer.LocalFactors[
totalTeamDifferences - 1 - i], 0),
new ScheduleStep<GaussianDistribution>(
String.Format("greaterThanOrWithinResultFactors[totalTeamDifferences - 1 - {0}] @ 0",
i),
_TeamDifferencesComparisonLayer.LocalFactors[totalTeamDifferences - 1 - i], 0),
new ScheduleStep<GaussianDistribution>(
String.Format("teamPerformanceToPerformanceDifferenceFactors[totalTeamDifferences - 1 - {0}] @ 1",
i),
_TeamPerformancesToTeamPerformanceDifferencesLayer.LocalFactors[
totalTeamDifferences - 1 - i], 1)
}
);
backwardScheduleList.Add(currentBackwardSchedulePiece);
}
var backwardSchedule =
new ScheduleSequence<GaussianDistribution>(
"backward schedule",
backwardScheduleList);
var forwardBackwardScheduleToLoop =
new ScheduleSequence<GaussianDistribution>(
"forward Backward Schedule To Loop",
new Schedule<GaussianDistribution>[]
{
forwardSchedule, backwardSchedule
});
const double initialMaxDelta = 0.0001;
var loop = new ScheduleLoop<GaussianDistribution>(
String.Format("loop with max delta of {0}",
initialMaxDelta),
forwardBackwardScheduleToLoop,
initialMaxDelta);
return loop;
}
}
}

View File

@ -0,0 +1,68 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Moserware.Numerics;
using Moserware.Skills.FactorGraphs;
using Moserware.Skills.TrueSkill.Factors;
namespace Moserware.Skills.TrueSkill.Layers
{
internal class PlayerPerformancesToTeamPerformancesLayer<TPlayer> :
TrueSkillFactorGraphLayer
<TPlayer, KeyedVariable<TPlayer, GaussianDistribution>, GaussianWeightedSumFactor,
Variable<GaussianDistribution>>
{
public PlayerPerformancesToTeamPerformancesLayer(TrueSkillFactorGraph<TPlayer> parentGraph)
: base(parentGraph)
{
}
public override void BuildLayer()
{
foreach (var currentTeam in InputVariablesGroups)
{
Variable<GaussianDistribution> teamPerformance = CreateOutputVariable(currentTeam);
AddLayerFactor(CreatePlayerToTeamSumFactor(currentTeam, teamPerformance));
// REVIEW: Does it make sense to have groups of one?
OutputVariablesGroups.Add(new[] {teamPerformance});
}
}
public override Schedule<GaussianDistribution> CreatePriorSchedule()
{
return ScheduleSequence(
from weightedSumFactor in LocalFactors
select new ScheduleStep<GaussianDistribution>("Perf to Team Perf Step", weightedSumFactor, 0),
"all player perf to team perf schedule");
}
protected GaussianWeightedSumFactor CreatePlayerToTeamSumFactor(
IList<KeyedVariable<TPlayer, GaussianDistribution>> teamMembers, Variable<GaussianDistribution> sumVariable)
{
return new GaussianWeightedSumFactor(sumVariable, teamMembers.ToArray(),
teamMembers.Select(v => PartialPlay.GetPartialPlayPercentage(v.Key)).
ToArray());
}
public override Schedule<GaussianDistribution> CreatePosteriorSchedule()
{
return ScheduleSequence(from currentFactor in LocalFactors
from currentIteration in
Enumerable.Range(1, currentFactor.NumberOfMessages - 1)
select new ScheduleStep<GaussianDistribution>(
"team sum perf @" + currentIteration,
currentFactor,
currentIteration),
"all of the team's sum iterations");
}
private Variable<GaussianDistribution> CreateOutputVariable(
IList<KeyedVariable<TPlayer, GaussianDistribution>> team)
{
string teamMemberNames = String.Join(", ", team.Select(teamMember => teamMember.Key.ToString()).ToArray());
return ParentFactorGraph.VariableFactory.CreateBasicVariable("Team[{0}]'s performance", teamMemberNames);
}
}
}

View File

@ -0,0 +1,63 @@
using System.Collections.Generic;
using System.Linq;
using Moserware.Numerics;
using Moserware.Skills.FactorGraphs;
using Moserware.Skills.TrueSkill.Factors;
namespace Moserware.Skills.TrueSkill.Layers
{
// We intentionally have no Posterior schedule since the only purpose here is to
internal class PlayerPriorValuesToSkillsLayer<TPlayer> :
TrueSkillFactorGraphLayer
<TPlayer, DefaultVariable<GaussianDistribution>, GaussianPriorFactor,
KeyedVariable<TPlayer, GaussianDistribution>>
{
private readonly IEnumerable<IDictionary<TPlayer, Rating>> _Teams;
public PlayerPriorValuesToSkillsLayer(TrueSkillFactorGraph<TPlayer> parentGraph,
IEnumerable<IDictionary<TPlayer, Rating>> teams)
: base(parentGraph)
{
_Teams = teams;
}
public override void BuildLayer()
{
foreach (var currentTeam in _Teams)
{
var currentTeamSkills = new List<KeyedVariable<TPlayer, GaussianDistribution>>();
foreach (var currentTeamPlayer in currentTeam)
{
KeyedVariable<TPlayer, GaussianDistribution> playerSkill =
CreateSkillOutputVariable(currentTeamPlayer.Key);
AddLayerFactor(CreatePriorFactor(currentTeamPlayer.Key, currentTeamPlayer.Value, playerSkill));
currentTeamSkills.Add(playerSkill);
}
OutputVariablesGroups.Add(currentTeamSkills);
}
}
public override Schedule<GaussianDistribution> CreatePriorSchedule()
{
return ScheduleSequence(
from prior in LocalFactors
select new ScheduleStep<GaussianDistribution>("Prior to Skill Step", prior, 0),
"All priors");
}
private GaussianPriorFactor CreatePriorFactor(TPlayer player, Rating priorRating,
Variable<GaussianDistribution> skillsVariable)
{
return new GaussianPriorFactor(priorRating.Mean,
Square(priorRating.StandardDeviation) +
Square(ParentFactorGraph.GameInfo.DynamicsFactor), skillsVariable);
}
private KeyedVariable<TPlayer, GaussianDistribution> CreateSkillOutputVariable(TPlayer key)
{
return ParentFactorGraph.VariableFactory.CreateKeyedVariable(key, "{0}'s skill", key);
}
}
}

View File

@ -0,0 +1,64 @@
using System.Collections.Generic;
using System.Linq;
using Moserware.Numerics;
using Moserware.Skills.FactorGraphs;
using Moserware.Skills.TrueSkill.Factors;
namespace Moserware.Skills.TrueSkill.Layers
{
internal class PlayerSkillsToPerformancesLayer<TPlayer> :
TrueSkillFactorGraphLayer
<TPlayer, KeyedVariable<TPlayer, GaussianDistribution>, GaussianLikelihoodFactor,
KeyedVariable<TPlayer, GaussianDistribution>>
{
public PlayerSkillsToPerformancesLayer(TrueSkillFactorGraph<TPlayer> parentGraph)
: base(parentGraph)
{
}
public override void BuildLayer()
{
foreach (var currentTeam in InputVariablesGroups)
{
var currentTeamPlayerPerformances = new List<KeyedVariable<TPlayer, GaussianDistribution>>();
foreach (var playerSkillVariable in currentTeam)
{
KeyedVariable<TPlayer, GaussianDistribution> playerPerformance =
CreateOutputVariable(playerSkillVariable.Key);
AddLayerFactor(CreateLikelihood(playerSkillVariable, playerPerformance));
currentTeamPlayerPerformances.Add(playerPerformance);
}
OutputVariablesGroups.Add(currentTeamPlayerPerformances);
}
}
private GaussianLikelihoodFactor CreateLikelihood(KeyedVariable<TPlayer, GaussianDistribution> playerSkill,
KeyedVariable<TPlayer, GaussianDistribution> playerPerformance)
{
return new GaussianLikelihoodFactor(Square(ParentFactorGraph.GameInfo.Beta), playerPerformance, playerSkill);
}
private KeyedVariable<TPlayer, GaussianDistribution> CreateOutputVariable(TPlayer key)
{
return ParentFactorGraph.VariableFactory.CreateKeyedVariable(key, "{0}'s performance", key);
}
public override Schedule<GaussianDistribution> CreatePriorSchedule()
{
return ScheduleSequence(
from likelihood in LocalFactors
select new ScheduleStep<GaussianDistribution>("Skill to Perf step", likelihood, 0),
"All skill to performance sending");
}
public override Schedule<GaussianDistribution> CreatePosteriorSchedule()
{
return ScheduleSequence(
from likelihood in LocalFactors
select new ScheduleStep<GaussianDistribution>("name", likelihood, 1),
"All skill to performance sending");
}
}
}

View File

@ -0,0 +1,38 @@
using Moserware.Numerics;
using Moserware.Skills.FactorGraphs;
using Moserware.Skills.TrueSkill.Factors;
namespace Moserware.Skills.TrueSkill.Layers
{
internal class TeamDifferencesComparisonLayer<TPlayer> :
TrueSkillFactorGraphLayer
<TPlayer, Variable<GaussianDistribution>, GaussianFactor, DefaultVariable<GaussianDistribution>>
{
private readonly double _Epsilon;
private readonly int[] _TeamRanks;
public TeamDifferencesComparisonLayer(TrueSkillFactorGraph<TPlayer> parentGraph, int[] teamRanks)
: base(parentGraph)
{
_TeamRanks = teamRanks;
GameInfo gameInfo = ParentFactorGraph.GameInfo;
_Epsilon = DrawMargin.GetDrawMarginFromDrawProbability(gameInfo.DrawProbability, gameInfo.Beta);
}
public override void BuildLayer()
{
for (int i = 0; i < InputVariablesGroups.Count; i++)
{
bool isDraw = (_TeamRanks[i] == _TeamRanks[i + 1]);
Variable<GaussianDistribution> teamDifference = InputVariablesGroups[i][0];
GaussianFactor factor =
isDraw
? (GaussianFactor) new GaussianWithinFactor(_Epsilon, teamDifference)
: new GaussianGreaterThanFactor(_Epsilon, teamDifference);
AddLayerFactor(factor);
}
}
}
}

View File

@ -0,0 +1,43 @@
using Moserware.Numerics;
using Moserware.Skills.FactorGraphs;
using Moserware.Skills.TrueSkill.Factors;
namespace Moserware.Skills.TrueSkill.Layers
{
internal class TeamPerformancesToTeamPerformanceDifferencesLayer<TPlayer> :
TrueSkillFactorGraphLayer
<TPlayer, Variable<GaussianDistribution>, GaussianWeightedSumFactor, Variable<GaussianDistribution>>
{
public TeamPerformancesToTeamPerformanceDifferencesLayer(TrueSkillFactorGraph<TPlayer> parentGraph)
: base(parentGraph)
{
}
public override void BuildLayer()
{
for (int i = 0; i < InputVariablesGroups.Count - 1; i++)
{
Variable<GaussianDistribution> strongerTeam = InputVariablesGroups[i][0];
Variable<GaussianDistribution> weakerTeam = InputVariablesGroups[i + 1][0];
Variable<GaussianDistribution> currentDifference = CreateOutputVariable();
AddLayerFactor(CreateTeamPerformanceToDifferenceFactor(strongerTeam, weakerTeam, currentDifference));
// REVIEW: Does it make sense to have groups of one?
OutputVariablesGroups.Add(new[] {currentDifference});
}
}
private GaussianWeightedSumFactor CreateTeamPerformanceToDifferenceFactor(
Variable<GaussianDistribution> strongerTeam, Variable<GaussianDistribution> weakerTeam,
Variable<GaussianDistribution> output)
{
return new GaussianWeightedSumFactor(output, new[] {strongerTeam, weakerTeam}, new[] {1.0, -1.0});
}
private Variable<GaussianDistribution> CreateOutputVariable()
{
return ParentFactorGraph.VariableFactory.CreateBasicVariable("Team performance difference");
}
}
}

View File

@ -0,0 +1,20 @@
using Moserware.Numerics;
using Moserware.Skills.FactorGraphs;
namespace Moserware.Skills.TrueSkill.Layers
{
internal abstract class TrueSkillFactorGraphLayer<TPlayer, TInputVariable, TFactor, TOutputVariable>
:
FactorGraphLayer
<TrueSkillFactorGraph<TPlayer>, GaussianDistribution, Variable<GaussianDistribution>, TInputVariable,
TFactor, TOutputVariable>
where TInputVariable : Variable<GaussianDistribution>
where TFactor : Factor<GaussianDistribution>
where TOutputVariable : Variable<GaussianDistribution>
{
public TrueSkillFactorGraphLayer(TrueSkillFactorGraph<TPlayer> parentGraph)
: base(parentGraph)
{
}
}
}

View File

@ -0,0 +1,119 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Moserware.Numerics;
using Moserware.Skills.FactorGraphs;
using Moserware.Skills.TrueSkill.Layers;
namespace Moserware.Skills.TrueSkill
{
public class TrueSkillFactorGraph<TPlayer> :
FactorGraph<TrueSkillFactorGraph<TPlayer>, GaussianDistribution, Variable<GaussianDistribution>>
{
private readonly List<FactorGraphLayerBase<GaussianDistribution>> _Layers;
private readonly PlayerPriorValuesToSkillsLayer<TPlayer> _PriorLayer;
public TrueSkillFactorGraph(GameInfo gameInfo, IEnumerable<IDictionary<TPlayer, Rating>> teams, int[] teamRanks)
{
_PriorLayer = new PlayerPriorValuesToSkillsLayer<TPlayer>(this, teams);
GameInfo = gameInfo;
VariableFactory =
new VariableFactory<GaussianDistribution>(() => GaussianDistribution.FromPrecisionMean(0, 0));
_Layers = new List<FactorGraphLayerBase<GaussianDistribution>>
{
_PriorLayer,
new PlayerSkillsToPerformancesLayer<TPlayer>(this),
new PlayerPerformancesToTeamPerformancesLayer<TPlayer>(this),
new IteratedTeamDifferencesInnerLayer<TPlayer>(
this,
new TeamPerformancesToTeamPerformanceDifferencesLayer<TPlayer>(this),
new TeamDifferencesComparisonLayer<TPlayer>(this, teamRanks))
};
}
public GameInfo GameInfo { get; private set; }
public void BuildGraph()
{
object lastOutput = null;
foreach (var currentLayer in _Layers)
{
if (lastOutput != null)
{
currentLayer.SetRawInputVariablesGroups(lastOutput);
}
currentLayer.BuildLayer();
lastOutput = currentLayer.GetRawOutputVariablesGroups();
}
}
public void RunSchedule()
{
Schedule<GaussianDistribution> fullSchedule = CreateFullSchedule();
double fullScheduleDelta = fullSchedule.Visit();
}
public double GetProbabilityOfRanking()
{
var factorList = new FactorList<GaussianDistribution>();
foreach (var currentLayer in _Layers)
{
foreach (var currentFactor in currentLayer.UntypedFactors)
{
factorList.AddFactor(currentFactor);
}
}
double logZ = factorList.LogNormalization;
return Math.Exp(logZ);
}
private Schedule<GaussianDistribution> CreateFullSchedule()
{
var fullSchedule = new List<Schedule<GaussianDistribution>>();
foreach (var currentLayer in _Layers)
{
Schedule<GaussianDistribution> currentPriorSchedule = currentLayer.CreatePriorSchedule();
if (currentPriorSchedule != null)
{
fullSchedule.Add(currentPriorSchedule);
}
}
// Casting to IEnumerable to get the LINQ Reverse()
IEnumerable<FactorGraphLayerBase<GaussianDistribution>> allLayers = _Layers;
foreach (var currentLayer in allLayers.Reverse())
{
Schedule<GaussianDistribution> currentPosteriorSchedule = currentLayer.CreatePosteriorSchedule();
if (currentPosteriorSchedule != null)
{
fullSchedule.Add(currentPosteriorSchedule);
}
}
return new ScheduleSequence<GaussianDistribution>("Full schedule", fullSchedule);
}
public IDictionary<TPlayer, Rating> GetUpdatedRatings()
{
var result = new Dictionary<TPlayer, Rating>();
foreach (var currentTeam in _PriorLayer.OutputVariablesGroups)
{
foreach (var currentPlayer in currentTeam)
{
result[currentPlayer.Key] = new Rating(currentPlayer.Value.Mean,
currentPlayer.Value.StandardDeviation);
}
}
return result;
}
}
}

View File

@ -0,0 +1,188 @@
using System;
using Moserware.Numerics;
namespace Moserware.Skills.TrueSkill
{
internal static class TruncatedGaussianCorrectionFunctions
{
// These functions from the bottom of page 4 of the TrueSkill paper.
/// <summary>
/// The "V" function where the team performance difference is greater than the draw margin.
/// </summary>
/// <remarks>In the reference F# implementation, this is referred to as "the additive
/// correction of a single-sided truncated Gaussian with unit variance."</remarks>
/// <param name="teamPerformanceDifference"></param>
/// <param name="drawMargin">In the paper, it's referred to as just "ε".</param>
/// <returns></returns>
public static double VExceedsMargin(double teamPerformanceDifference, double drawMargin, double c)
{
return VExceedsMargin(teamPerformanceDifference/c, drawMargin/c);
//return GaussianDistribution.At((teamPerformanceDifference - drawMargin) / c) / GaussianDistribution.CumulativeTo((teamPerformanceDifference - drawMargin) / c);
}
public static double VExceedsMargin(double teamPerformanceDifference, double drawMargin)
{
double denominator = GaussianDistribution.CumulativeTo(teamPerformanceDifference - drawMargin);
if (denominator < 2.222758749e-162)
{
return -teamPerformanceDifference + drawMargin;
}
return GaussianDistribution.At(teamPerformanceDifference - drawMargin)/denominator;
}
/// <summary>
/// The "W" function where the team performance difference is greater than the draw margin.
/// </summary>
/// <remarks>In the reference F# implementation, this is referred to as "the multiplicative
/// correction of a single-sided truncated Gaussian with unit variance."</remarks>
/// <param name="teamPerformanceDifference"></param>
/// <param name="drawMargin"></param>
/// <param name="c"></param>
/// <returns></returns>
public static double WExceedsMargin(double teamPerformanceDifference, double drawMargin, double c)
{
return WExceedsMargin(teamPerformanceDifference/c, drawMargin/c);
//var vWin = VExceedsMargin(teamPerformanceDifference, drawMargin, c);
//return vWin * (vWin + (teamPerformanceDifference - drawMargin) / c);
}
public static double WExceedsMargin(double teamPerformanceDifference, double drawMargin)
{
double denominator = GaussianDistribution.CumulativeTo(teamPerformanceDifference - drawMargin);
if (denominator < 2.222758749e-162)
{
if (teamPerformanceDifference < 0.0)
{
return 1.0;
}
return 0.0;
}
double vWin = VExceedsMargin(teamPerformanceDifference, drawMargin);
return vWin*(vWin + teamPerformanceDifference - drawMargin);
}
// the additive correction of a double-sided truncated Gaussian with unit variance
public static double VWithinMargin(double teamPerformanceDifference, double drawMargin, double c)
{
return VWithinMargin(teamPerformanceDifference/c, drawMargin/c);
//var teamPerformanceDifferenceAbsoluteValue = Math.Abs(teamPerformanceDifference);
//return (GaussianDistribution.At((-drawMargin - teamPerformanceDifferenceAbsoluteValue) / c) - GaussianDistribution.At((drawMargin - teamPerformanceDifferenceAbsoluteValue) / c))
// /
// (GaussianDistribution.CumulativeTo((drawMargin - teamPerformanceDifferenceAbsoluteValue) / c) - GaussianDistribution.CumulativeTo((-drawMargin - teamPerformanceDifferenceAbsoluteValue) / c));
}
// My original:
//public static double VWithinMargin(double teamPerformanceDifference, double drawMargin)
//{
// var teamPerformanceDifferenceAbsoluteValue = Math.Abs(teamPerformanceDifference);
// return (GaussianDistribution.At(-drawMargin - teamPerformanceDifferenceAbsoluteValue) - GaussianDistribution.At(drawMargin - teamPerformanceDifferenceAbsoluteValue))
// /
// (GaussianDistribution.CumulativeTo(drawMargin - teamPerformanceDifferenceAbsoluteValue) - GaussianDistribution.CumulativeTo(-drawMargin - teamPerformanceDifferenceAbsoluteValue));
//}
// from F#:
public static double VWithinMargin(double teamPerformanceDifference, double drawMargin)
{
double teamPerformanceDifferenceAbsoluteValue = Math.Abs(teamPerformanceDifference);
double denominator =
GaussianDistribution.CumulativeTo(drawMargin - teamPerformanceDifferenceAbsoluteValue) -
GaussianDistribution.CumulativeTo(-drawMargin - teamPerformanceDifferenceAbsoluteValue);
if (denominator < 2.222758749e-162)
{
if (teamPerformanceDifference < 0.0)
{
return -teamPerformanceDifference - drawMargin;
}
return -teamPerformanceDifference + drawMargin;
}
double numerator = GaussianDistribution.At(-drawMargin - teamPerformanceDifferenceAbsoluteValue) -
GaussianDistribution.At(drawMargin - teamPerformanceDifferenceAbsoluteValue);
if (teamPerformanceDifference < 0.0)
{
return -numerator/denominator;
}
return numerator/denominator;
}
// the multiplicative correction of a double-sided truncated Gaussian with unit variance
public static double WWithinMargin(double teamPerformanceDifference, double drawMargin, double c)
{
return WWithinMargin(teamPerformanceDifference/c, drawMargin/c);
//var teamPerformanceDifferenceAbsoluteValue = Math.Abs(teamPerformanceDifference);
//var vDraw = VWithinMargin(teamPerformanceDifferenceAbsoluteValue, drawMargin, c);
//return (vDraw * vDraw)
// +
// (
// (
// (
// ((drawMargin - teamPerformanceDifferenceAbsoluteValue) / c)
// *
// GaussianDistribution.At((drawMargin - teamPerformanceDifferenceAbsoluteValue) / c)
// )
// +
// (
// ((drawMargin + teamPerformanceDifferenceAbsoluteValue) / c)
// *
// GaussianDistribution.At((drawMargin + teamPerformanceDifferenceAbsoluteValue) / c)
// )
// )
// /
// (
// GaussianDistribution.CumulativeTo((drawMargin - teamPerformanceDifferenceAbsoluteValue) / c)
// -
// GaussianDistribution.CumulativeTo((-drawMargin - teamPerformanceDifferenceAbsoluteValue) / c)
// )
// );
}
// My original:
//public static double WWithinMargin(double teamPerformanceDifference, double drawMargin)
//{
// var teamPerformanceDifferenceAbsoluteValue = Math.Abs(teamPerformanceDifference);
// var vDraw = VWithinMargin(teamPerformanceDifferenceAbsoluteValue, drawMargin);
// return (vDraw * vDraw)
// +
// (
// ((drawMargin - teamPerformanceDifferenceAbsoluteValue) * GaussianDistribution.At(drawMargin - teamPerformanceDifferenceAbsoluteValue) + (drawMargin + teamPerformanceDifferenceAbsoluteValue) * GaussianDistribution.At(drawMargin + teamPerformanceDifferenceAbsoluteValue))
// /
// (GaussianDistribution.CumulativeTo(drawMargin - teamPerformanceDifferenceAbsoluteValue) - GaussianDistribution.CumulativeTo(-drawMargin - teamPerformanceDifferenceAbsoluteValue))
// );
//}
// From F#:
public static double WWithinMargin(double teamPerformanceDifference, double drawMargin)
{
double teamPerformanceDifferenceAbsoluteValue = Math.Abs(teamPerformanceDifference);
double denominator = GaussianDistribution.CumulativeTo(drawMargin - teamPerformanceDifferenceAbsoluteValue)
-
GaussianDistribution.CumulativeTo(-drawMargin - teamPerformanceDifferenceAbsoluteValue);
if (denominator < 2.222758749e-162)
{
return 1.0;
}
double vt = VWithinMargin(teamPerformanceDifferenceAbsoluteValue, drawMargin);
return vt*vt +
(
(drawMargin - teamPerformanceDifferenceAbsoluteValue)
*
GaussianDistribution.At(
drawMargin - teamPerformanceDifferenceAbsoluteValue)
- (-drawMargin - teamPerformanceDifferenceAbsoluteValue)
*
GaussianDistribution.At(-drawMargin - teamPerformanceDifferenceAbsoluteValue))/denominator;
}
}
}

View File

@ -0,0 +1,150 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Moserware.Skills.Numerics;
namespace Moserware.Skills.TrueSkill
{
/// <summary>
/// Calculates the new ratings for only two players.
/// </summary>
/// <remarks>
/// When you only have two players, a lot of the math simplifies. The main purpose of this class
/// is to show the bare minimum of what a TrueSkill implementation should have.
/// </remarks>
public class TwoPlayerTrueSkillCalculator : SkillCalculator
{
public TwoPlayerTrueSkillCalculator()
: base(SupportedOptions.None, Range<TeamsRange>.Exactly(2), Range<PlayersRange>.Exactly(1))
{
}
/// <inheritdoc/>
public override IDictionary<TPlayer, Rating> CalculateNewRatings<TPlayer>(GameInfo gameInfo,
IEnumerable
<IDictionary<TPlayer, Rating>>
teams, params int[] teamRanks)
{
// Basic argument checking
Guard.ArgumentNotNull(gameInfo, "gameInfo");
ValidateTeamCountAndPlayersCountPerTeam(teams);
// Make sure things are in order
RankSorter.Sort(ref teams, ref teamRanks);
// Get the teams as a list to make it easier to index
List<IDictionary<TPlayer, Rating>> teamList = teams.ToList();
// Since we verified that each team has one player, we know the player is the first one
IDictionary<TPlayer, Rating> winningTeam = teamList[0];
TPlayer winner = winningTeam.Keys.First();
Rating winnerPreviousRating = winningTeam[winner];
IDictionary<TPlayer, Rating> losingTeam = teamList[1];
TPlayer loser = losingTeam.Keys.First();
Rating loserPreviousRating = losingTeam[loser];
bool wasDraw = (teamRanks[0] == teamRanks[1]);
var results = new Dictionary<TPlayer, Rating>();
results[winner] = CalculateNewRating(gameInfo, winnerPreviousRating, loserPreviousRating,
wasDraw ? PairwiseComparison.Draw : PairwiseComparison.Win);
results[loser] = CalculateNewRating(gameInfo, loserPreviousRating, winnerPreviousRating,
wasDraw ? PairwiseComparison.Draw : PairwiseComparison.Lose);
// And we're done!
return results;
}
private static Rating CalculateNewRating(GameInfo gameInfo, Rating selfRating, Rating opponentRating,
PairwiseComparison comparison)
{
double drawMargin = DrawMargin.GetDrawMarginFromDrawProbability(gameInfo.DrawProbability, gameInfo.Beta);
double c =
Math.Sqrt(
Square(selfRating.StandardDeviation)
+
Square(opponentRating.StandardDeviation)
+
2*Square(gameInfo.Beta));
double winningMean = selfRating.Mean;
double losingMean = opponentRating.Mean;
switch (comparison)
{
case PairwiseComparison.Win:
case PairwiseComparison.Draw:
// NOP
break;
case PairwiseComparison.Lose:
winningMean = opponentRating.Mean;
losingMean = selfRating.Mean;
break;
}
double meanDelta = winningMean - losingMean;
double v;
double w;
double rankMultiplier;
if (comparison != PairwiseComparison.Draw)
{
// non-draw case
v = TruncatedGaussianCorrectionFunctions.VExceedsMargin(meanDelta, drawMargin, c);
w = TruncatedGaussianCorrectionFunctions.WExceedsMargin(meanDelta, drawMargin, c);
rankMultiplier = (int) comparison;
}
else
{
v = TruncatedGaussianCorrectionFunctions.VWithinMargin(meanDelta, drawMargin, c);
w = TruncatedGaussianCorrectionFunctions.WWithinMargin(meanDelta, drawMargin, c);
rankMultiplier = 1;
}
double meanMultiplier = (Square(selfRating.StandardDeviation) + Square(gameInfo.DynamicsFactor))/c;
double varianceWithDynamics = Square(selfRating.StandardDeviation) + Square(gameInfo.DynamicsFactor);
double stdDevMultiplier = varianceWithDynamics/Square(c);
double newMean = selfRating.Mean + (rankMultiplier*meanMultiplier*v);
double newStdDev = Math.Sqrt(varianceWithDynamics*(1 - w*stdDevMultiplier));
return new Rating(newMean, newStdDev);
}
/// <inheritdoc/>
public override double CalculateMatchQuality<TPlayer>(GameInfo gameInfo,
IEnumerable<IDictionary<TPlayer, Rating>> teams)
{
Guard.ArgumentNotNull(gameInfo, "gameInfo");
ValidateTeamCountAndPlayersCountPerTeam(teams);
Rating player1Rating = teams.First().Values.First();
Rating player2Rating = teams.Last().Values.First();
// We just use equation 4.1 found on page 8 of the TrueSkill 2006 paper:
double betaSquared = Square(gameInfo.Beta);
double player1SigmaSquared = Square(player1Rating.StandardDeviation);
double player2SigmaSquared = Square(player2Rating.StandardDeviation);
// This is the square root part of the equation:
double sqrtPart =
Math.Sqrt(
(2*betaSquared)
/
(2*betaSquared + player1SigmaSquared + player2SigmaSquared));
// This is the exponent part of the equation:
double expPart =
Math.Exp(
(-1*Square(player1Rating.Mean - player2Rating.Mean))
/
(2*(2*betaSquared + player1SigmaSquared + player2SigmaSquared)));
return sqrtPart*expPart;
}
}
}

View File

@ -0,0 +1,173 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Moserware.Skills.Numerics;
namespace Moserware.Skills.TrueSkill
{
/// <summary>
/// Calculates new ratings for only two teams where each team has 1 or more players.
/// </summary>
/// <remarks>
/// When you only have two teams, the math is still simple: no factor graphs are used yet.
/// </remarks>
public class TwoTeamTrueSkillCalculator : SkillCalculator
{
public TwoTeamTrueSkillCalculator()
: base(SupportedOptions.None, Range<TeamsRange>.Exactly(2), Range<PlayersRange>.AtLeast(1))
{
}
/// <inheritdoc/>
public override IDictionary<TPlayer, Rating> CalculateNewRatings<TPlayer>(GameInfo gameInfo,
IEnumerable
<IDictionary<TPlayer, Rating>>
teams, params int[] teamRanks)
{
Guard.ArgumentNotNull(gameInfo, "gameInfo");
ValidateTeamCountAndPlayersCountPerTeam(teams);
RankSorter.Sort(ref teams, ref teamRanks);
IDictionary<TPlayer, Rating> team1 = teams.First();
IDictionary<TPlayer, Rating> team2 = teams.Last();
bool wasDraw = (teamRanks[0] == teamRanks[1]);
var results = new Dictionary<TPlayer, Rating>();
UpdatePlayerRatings(gameInfo,
results,
team1,
team2,
wasDraw ? PairwiseComparison.Draw : PairwiseComparison.Win);
UpdatePlayerRatings(gameInfo,
results,
team2,
team1,
wasDraw ? PairwiseComparison.Draw : PairwiseComparison.Lose);
return results;
}
private static void UpdatePlayerRatings<TPlayer>(GameInfo gameInfo,
IDictionary<TPlayer, Rating> newPlayerRatings,
IDictionary<TPlayer, Rating> selfTeam,
IDictionary<TPlayer, Rating> otherTeam,
PairwiseComparison selfToOtherTeamComparison)
{
double drawMargin = DrawMargin.GetDrawMarginFromDrawProbability(gameInfo.DrawProbability, gameInfo.Beta);
double betaSquared = Square(gameInfo.Beta);
double tauSquared = Square(gameInfo.DynamicsFactor);
int totalPlayers = selfTeam.Count() + otherTeam.Count();
double selfMeanSum = selfTeam.Values.Sum(r => r.Mean);
double otherTeamMeanSum = otherTeam.Values.Sum(r => r.Mean);
double c = Math.Sqrt(selfTeam.Values.Sum(r => Square(r.StandardDeviation))
+
otherTeam.Values.Sum(r => Square(r.StandardDeviation))
+
totalPlayers*betaSquared);
double winningMean = selfMeanSum;
double losingMean = otherTeamMeanSum;
switch (selfToOtherTeamComparison)
{
case PairwiseComparison.Win:
case PairwiseComparison.Draw:
// NOP
break;
case PairwiseComparison.Lose:
winningMean = otherTeamMeanSum;
losingMean = selfMeanSum;
break;
}
double meanDelta = winningMean - losingMean;
double v;
double w;
double rankMultiplier;
if (selfToOtherTeamComparison != PairwiseComparison.Draw)
{
// non-draw case
v = TruncatedGaussianCorrectionFunctions.VExceedsMargin(meanDelta, drawMargin, c);
w = TruncatedGaussianCorrectionFunctions.WExceedsMargin(meanDelta, drawMargin, c);
rankMultiplier = (int) selfToOtherTeamComparison;
}
else
{
// assume draw
v = TruncatedGaussianCorrectionFunctions.VWithinMargin(meanDelta, drawMargin, c);
w = TruncatedGaussianCorrectionFunctions.WWithinMargin(meanDelta, drawMargin, c);
rankMultiplier = 1;
}
foreach (var teamPlayerRatingPair in selfTeam)
{
Rating previousPlayerRating = teamPlayerRatingPair.Value;
double meanMultiplier = (Square(previousPlayerRating.StandardDeviation) + tauSquared)/c;
double stdDevMultiplier = (Square(previousPlayerRating.StandardDeviation) + tauSquared)/Square(c);
double playerMeanDelta = (rankMultiplier*meanMultiplier*v);
double newMean = previousPlayerRating.Mean + playerMeanDelta;
double newStdDev =
Math.Sqrt((Square(previousPlayerRating.StandardDeviation) + tauSquared)*(1 - w*stdDevMultiplier));
newPlayerRatings[teamPlayerRatingPair.Key] = new Rating(newMean, newStdDev);
}
}
/// <inheritdoc/>
public override double CalculateMatchQuality<TPlayer>(GameInfo gameInfo,
IEnumerable<IDictionary<TPlayer, Rating>> teams)
{
Guard.ArgumentNotNull(gameInfo, "gameInfo");
ValidateTeamCountAndPlayersCountPerTeam(teams);
// We've verified that there's just two teams
ICollection<Rating> team1 = teams.First().Values;
int team1Count = team1.Count();
ICollection<Rating> team2 = teams.Last().Values;
int team2Count = team2.Count();
int totalPlayers = team1Count + team2Count;
double betaSquared = Square(gameInfo.Beta);
double team1MeanSum = team1.Sum(r => r.Mean);
double team1StdDevSquared = team1.Sum(r => Square(r.StandardDeviation));
double team2MeanSum = team2.Sum(r => r.Mean);
double team2SigmaSquared = team2.Sum(r => Square(r.StandardDeviation));
// This comes from equation 4.1 in the TrueSkill paper on page 8
// The equation was broken up into the part under the square root sign and
// the exponential part to make the code easier to read.
double sqrtPart
= Math.Sqrt(
(totalPlayers*betaSquared)
/
(totalPlayers*betaSquared + team1StdDevSquared + team2SigmaSquared)
);
double expPart
= Math.Exp(
(-1*Square(team1MeanSum - team2MeanSum))
/
(2*(totalPlayers*betaSquared + team1StdDevSquared + team2SigmaSquared))
);
return expPart*sqrtPart;
}
}
}