Start of factor graph port. Things don't work yet, but a lot of syntax updates towards PHP

This commit is contained in:
Jeff Moser 2010-09-18 11:11:44 -04:00
parent 4a76cc34cc
commit e434696b44
25 changed files with 1637 additions and 20 deletions

46
HashMap.php Normal file

@ -0,0 +1,46 @@
<?php
class HashMap
{
private $_hashToValue = array();
private $_hashToKey = array();
public function getValue($key)
{
return $this->_hashToValue[self::getHash($key)];
}
public function setValue($key, $value)
{
$hash = self::getHash($key);
$this->_hashToKey[$hash] = $key;
$this->_hashToValue[$hash] = $value;
return $this;
}
public function getAllKeys()
{
return \array_values($this->_hashToKey);
}
public function getAllValues()
{
return \array_values($this->_hashToValue);
}
public function count()
{
return \count($this->_hashToKey);
}
private static function getHash($key)
{
if(\is_object($key))
{
return \spl_object_hash($key);
}
return $key;
}
}
?>

@ -0,0 +1,96 @@
<?php
namespace Moserware\Skills\FactorGraphs;
require_once(dirname(__FILE__) . "../Guard.php");
require_once(dirname(__FILE__) . "../HashMap.php");
use Moserware\Skills\Guard;
use Moserware\Skills\HashMap;
class Factor
{
private $_messages = array();
private $_messageToVariableBinding;
private $_name;
private $_variables = array();
protected function __construct($name)
{
$this->_name = "Factor[" . $name . "]";
$this->_messagesToVariableBinding = new HashMap();
}
/// Returns the log-normalization constant of that factor
public function getLogNormalization()
{
return 0;
}
/// Returns the number of messages that the factor has
public function getNumberOfMessages()
{
return count($this->_messages);
}
protected function getVariables()
{
return $this->_variables;
}
protected function getMessages()
{
return $this->_messages;
}
/// Update the message and marginal of the i-th variable that the factor is connected to
public function updateMessageIndex($messageIndex)
{
Guard::argumentIsValidIndex($messageIndex, count($this->_messages), "messageIndex");
return $this->updateMessageVariable($this->_messages[$messageIndex], $this->_messageToVariableBinding->getValue($messageIndex));
}
protected function updateMessageVariable($message, $variable)
{
throw new Exception();
}
/// Resets the marginal of the variables a factor is connected to
public function resetMarginals()
{
foreach ($this->_messageToVariableBindings->getAllValues() as $currentVariable)
{
$currentVariable->resetToPrior();
}
}
/// Sends the ith message to the marginal and returns the log-normalization constant
public function sendMessageIndex($messageIndex)
{
Guard::argumentIsValidIndex($messageIndex, count($_messages), "messageIndex");
$message = $this->_messages[$messageIndex];
$variable = $this->_messageToVariableBinding->getValue($message);
return $this->sendMessageVariable($message, $variable);
}
protected abstract function sendMessageVariable($message, $variable);
public abstract function createVariableToMessageBinding($variable);
protected function createVariableToMessageBinding($variable, $message)
{
$index = count($this->_messages);
$this->_messages[] = $message;
$this->_messageToVariableBinding->setValue($message) = $variable;
$this->_variables[] = $variable;
return $message;
}
public function __toString()
{
return ($this->_name != null) ? $this->_name : base::__toString();
}
}
?>

@ -0,0 +1,18 @@
<?php
namespace Moserware\Skills\FactorGraphs;
class FactorGraph
{
private $_variableFactory;
public function getVariableFactory()
{
return $this->_variableFactory;
}
public function setVariableFactory($factory)
{
$this->_variableFactory = $factory;
}
}
?>

@ -0,0 +1,67 @@
<?php
namespace Moserware\Skills\FactorGraphs;
abstract class FactorGraphLayer
{
private $_localFactors = array();
private $_outputVariablesGroups = array();
private $_inputVariablesGroups = array();
private $_parentFactorGraph;
protected function __construct($parentGraph)
{
$this->_parentFactorGraph = $parentGraph;
}
protected function getInputVariablesGroups()
{
return $this->_inputVariablesGroups;
}
// HACK
public function getParentFactorGraph()
{
return $this->_parentFactorGraph;
}
public function getOutputVariablesGroups()
{
return $this->_outputVariablesGroups;
}
public function getLocalFactors()
{
return $this->_localFactors;
}
public function setInputVariablesGroups($value)
{
$this->_inputVariablesGroups = $value;
}
protected function scheduleSequence($itemsToSequence)
{
return new ScheduleSequence($itemsToSequence);
}
protected function addLayerFactor($factor)
{
$this->_localFactors[] = $factor;
}
public abstract function buildLayer();
public function createPriorSchedule()
{
return null;
}
public function createPosteriorSchedule()
{
return null;
}
}
?>

@ -0,0 +1,57 @@
<?php
namespace Moserware\Skills\FactorGraphs;
/// <summary>
/// Helper class for computing the factor graph's normalization constant.
/// </summary>
class FactorList
{
private $_list = array();
public function getLogNormalization()
{
foreach($this->_list as $currentFactor)
{
$currentFactor->resetMarginals();
}
$sumLogZ = 0.0;
$listCount = count($this->_list);
for ($i = 0; $i < $listCount; $i++)
{
$f = $this->_list[$i];
$numberOfMessages = $f->getNumberOfMessages();
for ($j = 0; $j < $numberOfMessages; $j++)
{
$sumLogZ += $f->sendMessageIndex($j);
}
}
$sumLogS = 0;
foreach($this->_list as $currentFactor)
{
$sumLogS = $sumLogS + $currentFactor->getLogNormalization();
}
return $sumLogZ + $sumLogS;
}
public function count()
{
return count($this->_list);
}
public function addFactor(Factor $factor)
{
$this->_list[] = $factor;
return $factor;
}
}
?>

@ -0,0 +1,28 @@
<?php
namespace Moserware\Skills\FactorGraphs;
class Message
{
private $_nameFormat;
private $_nameFormatArgs;
private $_value;
public function __construct($value = null, $nameFormat = null, $args = null)
{
$this->_nameFormat = $nameFormat;
$this->_nameFormatArgs = $args;
$this->_value = $value;
}
public function getValue()
{
return $this->_value;
}
public function __toString()
{
return $this->_nameFormat; //return (_NameFormat == null) ? base.ToString() : String.Format(_NameFormat, _NameFormatArgs);
}
}
?>

@ -0,0 +1,89 @@
<?php
namespace Moserware\Skills\FactorGraphs;
abstract class Schedule
{
private $_name;
protected function __construct($name)
{
$this->_name = $name;
}
public abstract function visit($depth = -1, $maxDepth = 0);
public function __toString()
{
return $this->_name;
}
}
class ScheduleStep extends Schedule
{
private $_factor;
private $_index;
public function __construct($name, $factor, $index)
{
parent::__construct($name);
$this->_factor = $factor;
$this->_index = $index;
}
public function visit($depth, $maxDepth)
{
$delta = $this->_factor->updateMessageIndex($this->_index);
return $delta;
}
}
class ScheduleSequence extends Schedule
{
private $_schedules;
public function __construct($name, $schedules)
{
parent::__construct($name);
$this->_schedules = $schedules;
}
public function visit($depth, $maxDepth)
{
$maxDelta = 0;
foreach ($this->_schedules as $currentSchedule)
{
$maxDelta = max($currentSchedule->visit($depth + 1, $maxDepth), $maxDelta);
}
return $maxDelta;
}
}
class ScheduleLoop extends Schedule
{
private $_maxDelta;
private $_scheduleToLoop;
public function __construct($name, Schedule $scheduleToLoop, $maxDelta)
{
parent::__construct($name);
$this->_scheduleToLoop = $scheduleToLoop;
$this->_maxDelta = $maxDelta;
}
public function visit($depth, $maxDepth)
{
$totalIterations = 1;
$delta = $this->_scheduleToLoop->visit($depth + 1, $maxDepth);
while ($delta > $this->_maxDelta)
{
$delta = $this->_scheduleToLoop->visit($depth + 1, $maxDepth);
$totalIterations++;
}
return $delta;
}
}
?>

@ -0,0 +1,71 @@
<?php
namespace Moserware\Skills\FactorGraphs;
class Variable
{
private $_name;
private $_prior;
private $_value;
public function __construct($name, $prior)
{
$this->_name = "Variable[" . $name . "]";
$this->_prior = $prior;
$this->resetToPrior();
}
public function getValue()
{
return $this->_value;
}
public function setValue($value)
{
$this->_value = $value;
}
public function resetToPrior()
{
$this->_value = $this->_prior;
}
public function __toString()
{
return $this->_name;
}
}
class DefaultVariable extends Variable
{
public function __construct()
{
parent::__construct("Default", null);
}
public function getValue()
{
return null;
}
public function setValue($value)
{
throw new Exception();
}
}
class KeyedVariable extends Variable
{
private $_key;
public function __construct($key, $name, $prior)
{
parent::__construct($name, $prior);
$this->_key = $key;
}
public function getKey()
{
return $this->_key;
}
}
?>

@ -0,0 +1,28 @@
<?php
namespace Moserware\Skills\FactorGraphs;
class VariableFactory
{
// using a Func<TValue> to encourage fresh copies in case it's overwritten
private $_variablePriorInitializer;
public function __construct($variablePriorInitializer)
{
$this->_variablePriorInitializer = $variablePriorInitializer;
}
public function createBasicVariable()
{
$newVar = new Variable($this->_variablePriorInitializer());
return $newVar;
}
public function createKeyedVariable($key)
{
$newVar = new KeyedVariable($key, $this->_variablePriorInitializer());
return $newVar;
}
}s
?>

@ -48,6 +48,16 @@ class GaussianDistribution
{
return $this->_standardDeviation;
}
public function getPrecision()
{
return $this->_precision;
}
public function getPrecisionMean()
{
return $this->_precisionMean;
}
public function getNormalizationConstant()
{

@ -1,46 +1,40 @@
<?php
namespace Moserware\Skills;
require_once(dirname(__FILE__) . "/HashMap.php");
class RatingContainer
{
private $_playerHashToRating = array();
private $_playerHashToPlayer = array();
private $_playerToRating;
public function __construct()
{
$this->_playerToRating = new \HashMap();
}
public function getRating($player)
{
return $this->_playerHashToRating[self::getHash($player)];
return $this->_playerToRating->getValue($player);
}
public function setRating($player, $rating)
{
$hash = self::getHash($player);
$this->_playerHashToPlayer[$hash] = $player;
$this->_playerHashToRating[$hash] = $rating;
return $this;
return $this->_playerToRating->setValue($player, $rating);
}
public function getAllPlayers()
{
return \array_values($this->_playerHashToPlayer);
return $this->_playerToRating->getAllKeys();
}
public function getAllRatings()
{
return \array_values($this->_playerHashToRating);
return $this->_playerToRating->getAllValues();
}
public function count()
{
return \count($this->_playerHashToPlayer);
}
private static function getHash($player)
{
if(\is_object($player))
{
return \spl_object_hash($player);
}
return $player;
}
return \count($this->_playerToRating->count());
}
}
?>

@ -0,0 +1,168 @@
<?php
namespace Moserware\Skills\TrueSkill;
/// <summary>
/// Calculates TrueSkill using a full factor graph.
/// </summary>
class FactorGraphTrueSkillCalculator extends SkillCalculator
{
public function __construct()
{
parent::__construct(SkillCalculatorSupportedOptions::PARTIAL_PLAY | SkillCalculatorSupportedOptions::PARTIAL_UPDATE, TeamsRange::atLeast(2), PlayersRange::atLeast(1));
}
public function CalculateNewRatings(GameInfo $gameInfo,
array $teams,
array $teamRanks)
{
Guard::argumentNotNull($gameInfo, "gameInfo");
$this->validateTeamCountAndPlayersCountPerTeam($teams);
RankSorter::sort($teams, $teamRanks);
$factorGraph = new TrueSkillFactorGraph($gameInfo, $teams, $teamRanks);
$factorGraph->buildGraph();
$factorGraph->runSchedule();
$probabilityOfOutcome = $factorGraph->getProbabilityOfRanking();
return $factorGraph->getUpdatedRatings();
}
public function calculateMatchQuality(GameInfo $gameInfo,
array $teams)
{
// We need to create the A matrix which is the player team assigments.
$teamAssignmentsList = $teams.ToList();
$skillsMatrix = $this->getPlayerCovarianceMatrix($teamAssignmentsList);
$meanVector = $this->getPlayerMeansVector($teamAssignmentsList);
$meanVectorTranspose = $meanVector->getTranspose();
$playerTeamAssignmentsMatrix = $this->createPlayerTeamAssignmentMatrix($teamAssignmentsList, $meanVector->getRowCount());
$playerTeamAssignmentsMatrixTranspose = $playerTeamAssignmentsMatrix->getTranspose();
$betaSquared = square($gameInfo->getBeta());
$start = Matrix::multiply($meanVectorTranspose, $playerTeamAssignmentsMatrix);
$aTa = Matrix::multiply(
Matrix::scalarMultiply($betaSquared,
$playerTeamAssignmentsMatrixTranspose),
$playerTeamAssignmentsMatrix);
$aTSA = Matrix::multiply(
Matrix::multiply($playerTeamAssignmentsMatrixTranspose, $skillsMatrix),
$playerTeamAssignmentsMatrix);
$middle = Matrix::add($aTa, $aTSA);
$middleInverse = $middle->getInverse();
$end = Matrix::multiply($playerTeamAssignmentsMatrixTranspose, $meanVector);
$expPartMatrix = Matrix::scalarMultiply(-0.5, (Matrix::multiply(Matrix::multiply($start, $middleInverse), $end)));
$expPart = $expPartMatrix->getDeterminant();
$sqrtPartNumerator = $aTa->getDeterminant();
$sqrtPartDenominator = $middle->getDeterminant();
$sqrtPart = $sqrtPartNumerator / $sqrtPartDenominator;
$result = exp($expPart) * sqrt($sqrtPart);
return $result;
}
private static function getPlayerMeansVector(array $teamAssignmentsList)
{
// A simple vector of all the player means.
return new Vector($this->getPlayerRatingValues($teamAssignmentsList,
function($rating)
{
return $rating->getMean();
}));
}
private static function getPlayerCovarianceMatrix(array $teamAssignmentsList)
{
// This is a square matrix whose diagonal values represent the variance (square of standard deviation) of all
// players.
return new DiagonalMatrix(
$this->getPlayerRatingValues($teamAssignmentsList,
function($rating)
{
return square($rating->getStandardDeviation());
}));
}
// Helper function that gets a list of values for all player ratings
private static function getPlayerRatingValues(array $teamAssignmentsList,
$playerRatingFunction)
{
$playerRatingValues = array();
foreach ($teamAssignmentsList as $currentTeam)
{
foreach (currentTeam.Values as $currentRating)
{
$playerRatingValues[] = $playerRatingFunction($currentRating);
}
}
return $playerRatingValues;
}
private static function createPlayerTeamAssignmentMatrix($teamAssignmentsList, $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 |
$playerAssignments = array();
$totalPreviousPlayers = 0;
$teamAssignmentsListCount = count($teamAssignmentsList);
for ($i = 0; $i < $teamAssignmentsListCount - 1; $i++)
{
$currentTeam = $teamAssignmentsList[$i];
// Need to add in 0's for all the previous players, since they're not
// on this team
$currentRowValues = array();
$playerAssignments[] = &$currentRowValues;
foreach ($currentTeam as $currentRating)
{
$currentRowValues[] = PartialPlay::getPartialPlayPercentage($currentRating->getKey());
// indicates the player is on the team
$totalPreviousPlayers++;
}
$nextTeam = $teamAssignmentsList[$i + 1];
foreach ($nextTeam as $nextTeamPlayerPair)
{
// Add a -1 * playing time to represent the difference
$currentRowValues[] = -1 * PartialPlay::getPartialPlayPercentage($nextTeamPlayerPair->getKey());
}
}
$playerTeamAssignmentsMatrix = new Matrix($totalPlayers, $teamAssignmentsListCount - 1, $playerAssignments);
return $playerTeamAssignmentsMatrix;
}
}
?>

@ -0,0 +1,32 @@
<?php
namespace Moserware\Skills\TrueSkill\Factors;
abstract class GaussianFactor extends Factor
{
protected function __construct($name)
{
parent::__construct($name);
}
/// Sends the factor-graph message with and returns the log-normalization constant
protected function sendMessageVariable(Message $message, Variable $variable)
{
$marginal = $variable->getValue();
$messageValue = $message->getValue();
$logZ = GaussianDistribution::logProductNormalization($marginal, $messageValue);
$variable->setValue($marginal*$messageValue);
return $logZ;
}
public function createVariableToMessageBinding(Variable $variable)
{
return parent::createVariableToMessageBinding($variable,
new Message(
GaussianDistribution::fromPrecisionMean(0, 0),
"message from {0} to {1}", $this));
}
}
?>

@ -0,0 +1,73 @@
<?php
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>
class GaussianGreaterThanFactor extends GaussianFactor
{
private $_epsilon;
public function __construct($epsilon, Variable $variable)
{
parent::_construct("{0} > {1:0.000}");
$this->_epsilon = $epsilon;
$this->createVariableToMessageBinding($variable);
}
public function getLogNormalization()
{
$vars = $this->getVariables();
$marginal = $vars[0]->getValue();
$messages = $this->getMessages();
$message = $messages[0]->getValue();
$messageFromVariable = GaussianDistribution::divide($marginal, $message);
return -GaussianDistribution::logProductNormalization($messageFromVariable, $message)
+
log(
GaussianDistribution::cumulativeTo(($messageFromVariable->getMean() - $this->_epsilon)/
$messageFromVariable->getStandardDeviation()));
}
protected function updateMessageVariable(Message $message, Variable $variable)
{
$oldMarginal = clone $variable->getValue();
$oldMessage = clone $message->getValue();
$messageFromVar = GaussianDistribution::divide($oldMarginal, $oldMessage);
$c = $messageFromVar->getPrecision();
$d = $messageFromVar->getPrecisionMean();
$sqrtC = sqrt($c);
$dOnSqrtC = $d/$sqrtC;
$epsilsonTimesSqrtC = $this->_epsilon*$sqrtC;
$d = $messageFromVar->getPrecisionMean();
$denom = 1.0 - TruncatedGaussianCorrectionFunctions::vExceedsMargin($dOnSqrtC, $epsilsonTimesSqrtC);
$newPrecision = $c/$denom;
$newPrecisionMean = ($d +
$sqrtC*
TruncatedGaussianCorrectionFunctions::vExceedsMargin($dOnSqrtC, $epsilsonTimesSqrtC))/
$denom;
$newMarginal = GaussianDistribution::fromPrecisionMean($newPrecisionMean, $newPrecision);
$newMessage = GaussianDistribution::divide(
GaussianDistribution::multiply($oldMessage, $newMarginal),
$oldMarginal);
/// Update the message and marginal
$message->setValue($newMessage);
$variable->setValue($newMarginal);
/// Return the difference in the new marginal
return GaussianDistribution::subtract($newMarginal, $oldMarginal);
}
}
?>

@ -0,0 +1,78 @@
<?php
namespace Moserware\Skills\TrueSkill\Factors;
/// <summary>
/// Connects two variables and adds uncertainty.
/// </summary>
/// <remarks>See the accompanying math paper for more details.</remarks>
class GaussianLikelihoodFactor extends GaussianFactor
{
private $_precision;
public function __construct($betaSquared, Variable $variable1, Variable $variable2)
{
parent::__construct("Likelihood of {0} going to {1}");
$this->_precision = 1.0/$betaSquared;
$this->createVariableToMessageBinding($variable1);
$this->createVariableToMessageBinding($variable2);
}
public function getLogNormalization()
{
$vars = $this->getVariables();
$messages = $this->getMessages();
return GaussianDistribution::logRatioNormalization(
$vars[0]->getValue(),
$messages[0]->getValue());
}
private function updateHelper(Message $message1, Message $message2,
Variable $variable1, Variable $variable2)
{
$message1Value = clone $message1->getValue();
$message2Value = clone $message2->getValue();
$marginal1 = clone $variable1->getValue();
$marginal2 = clone $variable2->getValue();
$a = $this->_precision/($this->_precision + $marginal2->getPrecision() - $message2Value->getPrecision());
$newMessage = GaussianDistribution::fromPrecisionMean(
$a*($marginal2->getPrecisionMean() - $message2Value->getPrecisionMean()),
$a*($marginal2->getPrecision() - $message2Value->getPrecision()));
$oldMarginalWithoutMessage = GaussianDistribution::divide($marginal1, $message1Value);
$newMarginal = GaussianDistribution::multiply($oldMarginalWithoutMessage, $newMessage);
/// Update the message and marginal
$message1->setValue($newMessage);
$variable1->setValue($newMarginal);
/// Return the difference in the new marginal
return GaussianDistribution::subtract($newMarginal, $marginal1);
}
public function updateMessageIndex($messageIndex)
{
$messages = $this->getMessages();
$vars = $this->getVariables();
switch ($messageIndex)
{
case 0:
return $this->updateHelper($messages[0], $messages[1],
$vars[0], $vars[1]);
case 1:
return $this->updateHelper($messages[1], $messages[0],
$vars[1], $vars[0]);
default:
throw new Exception();
}
}
}
?>

@ -0,0 +1,40 @@
<?php
namespace Moserware\Skills\TrueSkill\Factors;
/// <summary>
/// Supplies the factor graph with prior information.
/// </summary>
/// <remarks>See the accompanying math paper for more details.</remarks>
class GaussianPriorFactor extends GaussianFactor
{
private $_newMessage;
public function __construct($mean, $variance, Variable $variable)
{
parent::__construct("Prior value going to {0}");
$this->_newMessage = new GaussianDistribution($mean, sqrt($variance));
$this->createVariableToMessageBinding($variable,
new Message(
GaussianDistribution::fromPrecisionMean(0, 0),
"message from {0} to {1}",
this, variable));
}
protected function updateMessageVariable(Message $message, Variable $variable)
{
$oldMarginal = clone $variable->getValue();
$oldMessage = $message;
$newMarginal =
GaussianDistribution::fromPrecisionMean(
$oldMarginal->getPrecisionMean() + $this->_newMessage->getPrecisionMean() - $oldMessage->getValue()->getPrecisionMean(),
$oldMarginal->getPrecision() + $this->_newMessage->getPrecision() - $oldMessage->getValue()->getPrecision());
$variable->setValue($newMarginal);
$message->setValue($this->_newMessage);
return GaussianDistribution::subtract($oldMarginal, $newMarginal);
}
}
?>

@ -0,0 +1,216 @@
<?php
namespace Moserware\Skills\TrueSkill\Factors;
/// <summary>
/// Factor that sums together multiple Gaussians.
/// </summary>
/// <remarks>See the accompanying math paper for more details.</remarks>
class GaussianWeightedSumFactor extends GaussianFactor
{
private $_variableIndexOrdersForWeights = array();
// 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 $_weights;
private $_weightsSquared;
public function __construct(Variable $sumVariable, array $variablesToSum, array $variableWeights = null)
{
parent::__construct($this->createName($sumVariable, $variablesToSum, $variableWeights));
$this->_weights = array();
$this->_weightsSquared = array();
// The first weights are a straightforward copy
// v_0 = a_1*v_1 + a_2*v_2 + ... + a_n * v_n
$this->_weights[0] = array();
$variableWeightsLength = count($variableWeights);
for($i = 0; $i < $variableWeightsLength; $i++)
{
$weight = $variableWeights[$i];
$this->_weights[0][$i] = $weight;
$this->_weightsSquared[0][i] = $weight * $weight;
}
$variablesToSumLength = count($variablesToSum);
// 0..n-1
for($i = 0; $i < ($variablesToSumLength + 1); $i++)
{
$this->_variableIndexOrdersForWeights[] = $i;
}
// 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
$weightsLength = $variableWeightsLength + 1;
for ($weightsIndex = 1; $weightsIndex < $weightsLength; $weightsIndex++)
{
$this->_weights[$weightsIndex] = array();
$variableIndices = array();
$variableIndices[] = $weightsIndex;
$currentWeightsSquared = array();
$this->_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
$currentDestinationWeightIndex = 0;
for ($currentWeightSourceIndex = 0;
$currentWeightSourceIndex < $variableWeights.Length;
$currentWeightSourceIndex++)
{
if ($currentWeightSourceIndex == ($weightsIndex - 1))
{
continue;
}
$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
$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;
$this->_variableIndexOrdersForWeights[] = $variableIndices;
}
$this->createVariableToMessageBinding($sumVariable);
foreach ($variablesToSum as $currentVariable)
{
$this->createVariableToMessageBinding($currentVariable);
}
}
public function getLogNormalization()
{
$vars = $this->getVariables();
$messages = $this->getMessages();
$result = 0.0;
// We start at 1 since offset 0 has the sum
$varCount = count($vars);
for ($i = 1; $i < $varCount; $i++)
{
$result += GaussianDistribution::logRatioNormalization($vars[i]->getValue(), $messages[i]->getValue());
}
return $result;
}
private function updateHelper(array $weights, array $weightsSquared,
array $messages,
array $variables)
{
// Potentially look at http://mathworld.wolfram.com/NormalSumDistribution.html for clues as
// to what it's doing
$messages = $this->getMessages();
$message0 = clone $messages[0]->getValue();
$marginal0 = clone $variables[0]->getValue();
// The math works out so that 1/newPrecision = sum of a_i^2 /marginalsWithoutMessages[i]
$inverseOfNewPrecisionSum = 0.0;
$anotherInverseOfNewPrecisionSum = 0.0;
$weightedMeanSum = 0.0;
$anotherWeightedMeanSum = 0.0;
$weightsSquaredLength = count($weightsSquared);
for ($i = 0; $i < $weightsSquaredLength; $i++)
{
// These flow directly from the paper
$inverseOfNewPrecisionSum += $weightsSquared[i]/
($variables[$i + 1]->getValue()->getPrecision() - $messages[$i + 1]->getValue()->getPrecision());
$diff = GaussianDistribution::divide($variables[$i + 1]->getValue(), $messages[$i + 1]->getValue());
$anotherInverseOfNewPrecisionSum += $weightsSquared[i]/$diff->getPrecision();
$weightedMeanSum += $weights[i]
*
($variables[$i + 1]->getValue()->getPrecisionMean() - $messages[$i + 1]->getValue()->getPrecisionMean())
/
($variables[$i + 1]->getValue()->getPrecision() - $messages[$i + 1]->getValue()->getPrecision());
$anotherWeightedMeanSum += $weights[$i]*$diff->getPrecisionMean()/$diff->getPrecision();
}
$newPrecision = 1.0/$inverseOfNewPrecisionSum;
$anotherNewPrecision = 1.0/$anotherInverseOfNewPrecisionSum;
$newPrecisionMean = $newPrecision*$weightedMeanSum;
$anotherNewPrecisionMean = $anotherNewPrecision*$anotherWeightedMeanSum;
$newMessage = GaussianDistribution::fromPrecisionMean($newPrecisionMean, $newPrecision);
$oldMarginalWithoutMessage = GaussianDistribution::divide($marginal0, $message0);
$newMarginal = GaussianDistribution::multiply($oldMarginalWithoutMessage, $newMessage);
/// Update the message and marginal
$messages[0]->setValue($newMessage);
$variables[0]->setValue($newMarginal);
/// Return the difference in the new marginal
$finalDiff = GaussianDistribution::subtract($newMarginal, $marginal0);
return $finalDiff;
}
public function updateMessageIndex($messageIndex)
{
$allMessages = $this->getMessages();
$allVariables = $this->getVariables();
Guard::argumentIsValidIndex($messageIndex, count($allMessages),"messageIndex");
$updatedMessages = array();
$updatedVariables = array();
$indicesToUse = $this->_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
$allMessagesCount = count($allMessages);
for ($i = 0; i < $allMessagesCount; $i++)
{
$updatedMessages[] =$allMessages[$indicesToUse[$i]];
$updatedVariables[] = $allVariables[$indicesToUse[$i]];
}
return updateHelper($this->_weights[$messageIndex],
$this->_weightsSquared[$messageIndex],
$updatedMessages,
$updatedVariables);
}
}
?>

@ -0,0 +1,72 @@
<?php
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>
class GaussianWithinFactor extends GaussianFactor
{
private $_epsilon;
public function __construct($epsilon, Variable $variable)
{
$this->_epsilon = $epsilon;
$this->createVariableToMessageBinding($variable);
}
public function getLogNormalization()
{
$variables = $this->getVariables();
$marginal = $variables[0]->getValue();
$messages = $this->getMessages();
$message = $messages[0]->getValue();
$messageFromVariable = GaussianDistribution::divide($marginal, $message);
$mean = $messageFromVariable->getMean();
$std = $messageFromVariable->getStandardDeviation();
$z = GaussianDistribution::cumulativeTo(($this->_epsilon - $mean)/$std)
-
GaussianDistribution::cumulativeTo((-$this->_epsilon - $mean)/$std);
return -GaussianDistribution::logProductNormalization($messageFromVariable, $message) + log($z);
}
protected function updateMessage(Message $message, Variable $variable)
{
$oldMarginal = clone $variable->getValue();
$oldMessage = clone $message->getValue();
$messageFromVariable = GaussianDistribution::divide($oldMarginal, $oldMessage);
$c = $messageFromVariable->getPrecision();
$d = $messageFromVariable->getPrecisionMean();
$sqrtC = sqrt($c);
$dOnSqrtC = $d/$sqrtC;
$epsilonTimesSqrtC = $this->_epsilon*$sqrtC;
$d = $messageFromVariable->getPrecisionMean();
$denominator = 1.0 - TruncatedGaussianCorrectionFunctions::wWithinMargin($dOnSqrtC, $epsilonTimesSqrtC);
$newPrecision = $c/$denominator;
$newPrecisionMean = ($d +
$sqrtC*
TruncatedGaussianCorrectionFunctions::vWithinMargin($dOnSqrtC, $epsilonTimesSqrtC))/
$denominator;
$newMarginal = GaussianDistribution::fromPrecisionMean($newPrecisionMean, $newPrecision);
$newMessage = GaussianDistribution::divide(
GaussianDistribution::multiply($oldMessage, $newMarginal),
$oldMarginal);
/// Update the message and marginal
$message->setValue($newMessage);
$variable->setValue($newMarginal);
/// Return the difference in the new marginal
return GaussianDistribution::subtract($newMarginal, $oldMarginal);
}
}
?>

@ -0,0 +1,147 @@
<?php
namespace Moserware\Skills\TrueSkill\Layers;
// The whole purpose of this is to do a loop on the bottom
class IteratedTeamDifferencesInnerLayer extends TrueSkillFactorGraphLayer
{
private $_TeamDifferencesComparisonLayer;
private $_TeamPerformancesToTeamPerformanceDifferencesLayer;
public function __construct(TrueSkillFactorGraph $parentGraph,
TeamPerformancesToTeamPerformanceDifferencesLayer $teamPerformancesToPerformanceDifferences,
TeamDifferencesComparisonLayer $teamDifferencesComparisonLayer)
{
parent::__construct($parentGraph);
$this->_TeamPerformancesToTeamPerformanceDifferencesLayer = $teamPerformancesToPerformanceDifferences;
$this->_TeamDifferencesComparisonLayer = $teamDifferencesComparisonLayer;
}
public function buildLayer()
{
$this->_TeamPerformancesToTeamPerformanceDifferencesLayer->setRawInputVariablesGroups($this->getInputVariablesGroups());
$this->_TeamPerformancesToTeamPerformanceDifferencesLayer->buildLayer();
$this->_TeamDifferencesComparisonLayer->setRawInputVariablesGroups(
$this->_TeamPerformancesToTeamPerformanceDifferencesLayer->getRawOutputVariablesGroups());
$this->_TeamDifferencesComparisonLayer->buildLayer();
}
public function createPriorSchedule()
{
// BLOG about $loop
switch (count($this->getInputVariablesGroups()))
{
case 0:
case 1:
throw new InvalidOperationException();
case 2:
$loop = $this->createTwoTeamInnerPriorLoopSchedule();
break;
default:
$loop = $this->createMultipleTeamInnerPriorLoopSchedule();
break;
}
// When dealing with differences, there are always (n-1) differences, so add in the 1
$totalTeamDifferences = count($this->_TeamPerformancesToTeamPerformanceDifferencesLayer->getLocalFactors());
$totalTeams = $totalTeamDifferences + 1;
$innerSchedule = new ScheduleSequence(
"inner schedule",
array(
$loop,
new ScheduleStep(
"teamPerformanceToPerformanceDifferenceFactors[0] @ 1",
($this->_TeamPerformancesToTeamPerformanceDifferencesLayer->getLocalFactors())[0], 1),
new ScheduleStep(
"teamPerformanceToPerformanceDifferenceFactors[teamTeamDifferences = {0} - 1] @ 2",
($this->_TeamPerformancesToTeamPerformanceDifferencesLayer->getLocalFactors())[$totalTeamDifferences - 1], 2)
)
);
return innerSchedule;
}
private function createTwoTeamInnerPriorLoopSchedule()
{
return $this->scheduleSequence(
array(
new ScheduleStep(
"send team perf to perf differences",
($this->_TeamPerformancesToTeamPerformanceDifferencesLayer->getLocalFactors())[0],
0),
new ScheduleStep(
"send to greater than or within factor",
($this->_TeamDifferencesComparisonLayer->getLocalFactors())[0],
0)
),
"loop of just two teams inner sequence");
}
private function createMultipleTeamInnerPriorLoopSchedule()
{
$totalTeamDifferences = count($this->_TeamPerformancesToTeamPerformanceDifferencesLayer->getLocalFactors());
$forwardScheduleList = array();
for ($i = 0; $i < $totalTeamDifferences - 1; $i++)
{
$currentForwardSchedulePiece =
$this->scheduleSequence(
array(
new ScheduleStep(
sprintf("team perf to perf diff %d", $i),
($this->_TeamPerformancesToTeamPerformanceDifferencesLayer->getLocalFactors())[$i], 0),
new ScheduleStep(
sprintf("greater than or within result factor %d", $i),
($this->_TeamDifferencesComparisonLayer->getLocalFactors())[$i], 0),
new ScheduleStep(
sprintf("team perf to perf diff factors [%d], 2", $i),
($this->_TeamPerformancesToTeamPerformanceDifferencesLayer->getLocalFactors())[$i], 2)
), sprintf("current forward schedule piece %d", $i);
$forwardScheduleList[] = $currentForwardSchedulePiece;
}
$forwardSchedule = new ScheduleSequence("forward schedule", $forwardScheduleList);
$backwardScheduleList = array();
for ($i = 0; $i < $totalTeamDifferences - 1; $i++)
{
$currentBackwardSchedulePiece = new ScheduleSequence(
"current backward schedule piece",
array(
new ScheduleStep(
sprintf("teamPerformanceToPerformanceDifferenceFactors[totalTeamDifferences - 1 - %d] @ 0", $i),
($this->_TeamPerformancesToTeamPerformanceDifferencesLayer->getLocalFactors())[$totalTeamDifferences - 1 - $i], 0),
new ScheduleStep(
sprintf("greaterThanOrWithinResultFactors[totalTeamDifferences - 1 - %d] @ 0", $i),
($this->_TeamDifferencesComparisonLayer->getLocalFactors())[$totalTeamDifferences - 1 - $i], 0),
new ScheduleStep(
sprintf("teamPerformanceToPerformanceDifferenceFactors[totalTeamDifferences - 1 - %d] @ 1", $i),
($this->_TeamPerformancesToTeamPerformanceDifferencesLayer->getLocalFactors())[$totalTeamDifferences - 1 - $i], 1)
);
$backwardScheduleList[] = $currentBackwardSchedulePiece;
}
$backwardSchedule = new ScheduleSequence("backward schedule", $backwardScheduleList);
$forwardBackwardScheduleToLoop =
new ScheduleSequence(
"forward Backward Schedule To Loop",
array($forwardSchedule, $backwardSchedule));
$initialMaxDelta = 0.0001;
$loop = new ScheduleLoop(
sprintf("loop with max delta of %f", $initialMaxDelta),
$forwardBackwardScheduleToLoop,
$initialMaxDelta);
return $loop;
}
}
?>

@ -0,0 +1,71 @@
<?php
namespace Moserware\Skills\TrueSkill\Layers;
class PlayerPerformancesToTeamPerformancesLayer extends TrueSkillFactorGraphLayer
{
public function __construct(TrueSkillFactorGraph $parentGraph)
{
parent::__construct($parentGraph);
}
public function buildLayer()
{
foreach ($this->getInputVariablesGroups() as $currentTeam)
{
$teamPerformance = $this->createOutputVariable($currentTeam);
$this->addLayerFactor($this->createPlayerToTeamSumFactor($currentTeam, $teamPerformance));
// REVIEW: Does it make sense to have groups of one?
$this->getOutputVariablesGroups() = $teamPerformance;
}
}
public function createPriorSchedule()
{
return $this->scheduleSequence(
array_map(
function($weightedSumFactor)
{
return new ScheduleStep("Perf to Team Perf Step", $weightedSumFactor, 0);
},
$this->getLocalFactors()),
"all player perf to team perf schedule");
}
protected function createPlayerToTeamSumFactor($teamMembers, $sumVariable)
{
return new GaussianWeightedSumFactor(
$sumVariable,
$teamMembers,
array_map(
function($v)
{
return PartialPlay::getPartialPlayPercentage($v->getKey());
},
$teamMembers));
}
public function createPosteriorSchedule()
{
// BLOG
return $this->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 function createOutputVariable($team)
{
$teamMemberNames = String.Join(", ", team.Select(teamMember => teamMember.Key.ToString()).ToArray());
return ParentFactorGraph.VariableFactory.CreateBasicVariable("Team[{0}]'s performance", teamMemberNames);
}
}
?>

@ -0,0 +1,59 @@
<?php
namespace Moserware\Skills\TrueSkill\Layers;
// We intentionally have no Posterior schedule since the only purpose here is to
class PlayerPriorValuesToSkillsLayer extends TrueSkillFactorGraphLayer
{
private $_teams;
public function __construct(TrueSkillFactorGraph $parentGraph, $teams)
{
parent::__construct($parentGraph);
$this->_teams = $teams;
}
public function buildLayer()
{
foreach ($this->_teams as $currentTeam)
{
$currentTeamSkills = array();
foreach ($currentTeam as $currentTeamPlayer)
{
$playerSkill = $this->createSkillOutputVariable($currentTeamPlayer.Key);
$this->addLayerFactor($this->createPriorFactor($currentTeamPlayer.Key, $currentTeamPlayer.Value, $playerSkill));
$currentTeamSkills[] = $playerSkill;
}
OutputVariablesGroups.Add(currentTeamSkills);
}
}
public function createPriorSchedule()
{
return $this->scheduleSequence(
array_map(
function($prior)
{
return new ScheduleStep("Prior to Skill Step", $prior, 0);
},
$this->getLocalFactors()),
"All priors");
}
private function createPriorFactor($player, $priorRating, $skillsVariable)
{
return new GaussianPriorFactor($priorRating->getMean(),
square($priorRating->getStandardDeviation()) +
square($this->getParentFactorGraph()->getGameInfo()->getDynamicsFactor()),
$skillsVariable);
}
private function createSkillOutputVariable($key)
{
return $this->getParentFactorGraph()->getVariableFactory()->createKeyedVariable($key, "{0}'s skill", $key);
}
}
?>

@ -0,0 +1,64 @@
<?php
namespace Moserware\Skills\TrueSkill\Layers;
class PlayerSkillsToPerformancesLayer extends TrueSkillFactorGraphLayer
{
public function __construct(TrueSkillFactorGraph $parentGraph)
{
parent::__construct($parentGraph);
}
public function buildLayer()
{
foreach ($this->getInputVariablesGroups() as $currentTeam)
{
$currentTeamPlayerPerformances = array();
foreach ($currentTeam as $playerSkillVariable)
{
$playerPerformance = $this->createOutputVariable($playerSkillVariable->getKey());
$this->addLayerFactor($this->createLikelihood($playerSkillVariable, $playerPerformance));
$currentTeamPlayerPerformances[] = $playerPerformance;
}
$this->getOutputVariablesGroups()[] = $currentTeamPlayerPerformances;
}
}
private function createLikelihood($playerSkill, $playerPerformance)
{
return new GaussianLikelihoodFactor(square($this->getParentFactorGraph()->getGameInfo()->getBeta()), $playerPerformance, $playerSkill);
}
private function createOutputVariable($key)
{
return $this->getParentFactorGraph()->getVariableFactory()->createKeyedVariable($key, "{0}'s performance", $key);
}
public function createPriorSchedule()
{
return $this->scheduleSequence(
array_map(
function($likelihood)
{
return $this->scheduleStep("Skill to Perf step", $likelihood, 0);
},
$this->getLocalFactors()),
"All skill to performance sending");
}
public function createPosteriorSchedule()
{
return $this->scheduleSequence(
array_map(
function($likelihood)
{
return new ScheduleStep("name", $likelihood, 1);
},
$this->getLocalFactors()),
"All skill to performance sending");
}
}
?>

@ -0,0 +1,38 @@
<?php
namespace Moserware\Skills\TrueSkill\Layers;
class TeamDifferencesComparisonLayer extends TrueSkillFactorGraphLayer
{
private $_epsilon;
private $_teamRanks;
public function __construct(TrueSkillFactorGraph $parentGraph, array $teamRanks)
{
parent::__construct($parentGraph);
$this->_teamRanks = $teamRanks;
$gameInfo = $this->getParentFactorGraph()->getGameInfo();
$this->_epsilon = DrawMargin::getDrawMarginFromDrawProbability($gameInfo->getDrawProbability(), $gameInfo->getBeta());
}
public function buildLayer()
{
$inputVarGroups = $this->getInputVariablesGroups();
$inputVarGroupsCount = count($inputVarGroups);
for ($i = 0; $i < $inputVarGroupsCount; $i++)
{
$isDraw = ($this->_teamRanks[$i] == $this->_teamRanks[$i + 1]);
$teamDifference = $inputVarGroups[$i][0];
$factor =
$isDraw
? new GaussianWithinFactor($this->_epsilon, $teamDifference)
: new GaussianGreaterThanFactor($this->_epsilon, $teamDifference);
$this->addLayerFactor($factor);
}
}
}
?>

@ -0,0 +1,42 @@
<?php
namespace Moserware\Skills\TrueSkill\Layers;
class TeamPerformancesToTeamPerformanceDifferencesLayer extends TrueSkillFactorGraphLayer
{
public function __construct(TrueSkillFactorGraph $parentGraph)
{
parent::__construct($parentGraph);
}
public function buildLayer()
{
$inputVariablesGroup = $this->getInputVariablesGroups();
$inputVariablesGroupCount = count($inputVariablesGroup);
for ($i = 0; $i < $inputVariablesGroupCount - 1; $i++)
{
$strongerTeam = $inputVariablesGroups[$i][0];
$weakerTeam = $inputVariablesGroups[$i + 1][0];
$currentDifference = $this->createOutputVariable();
$this->addLayerFactor($this->createTeamPerformanceToDifferenceFactor($strongerTeam, $weakerTeam, currentDifference));
// REVIEW: Does it make sense to have groups of one?
$this->getOutputVariablesGroups()[] = $currentDifference;
}
}
private function createTeamPerformanceToDifferenceFactor(
Variable $strongerTeam, Variable $weakerTeam, Variable $output)
{
return new GaussianWeightedSumFactor($output, array($strongerTeam, $weakerTeam), array(1.0, -1.0));
}
private function createOutputVariable()
{
return $this->getParentFactorGraph()->getVariableFactory()->createBasicVariable("Team performance difference");
}
}
?>

@ -0,0 +1,13 @@
<?php
namespace Moserware\Skills\TrueSkill\Layers;
abstract class TrueSkillFactorGraphLayer extends FactorGraphLayer
{
public function __construct(TrueSkillFactorGraph $parentGraph)
{
parent::__construct($parentGraph);
}
}
?>