diff --git a/HashMap.php b/HashMap.php
new file mode 100644
index 0000000..1669a0a
--- /dev/null
+++ b/HashMap.php
@@ -0,0 +1,46 @@
+_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;
+ }
+}
+?>
diff --git a/PHPSkills/FactorGraphs/Factor.php b/PHPSkills/FactorGraphs/Factor.php
new file mode 100644
index 0000000..a3a911a
--- /dev/null
+++ b/PHPSkills/FactorGraphs/Factor.php
@@ -0,0 +1,96 @@
+_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();
+ }
+}
+
+?>
diff --git a/PHPSkills/FactorGraphs/FactorGraph.php b/PHPSkills/FactorGraphs/FactorGraph.php
new file mode 100644
index 0000000..813ae65
--- /dev/null
+++ b/PHPSkills/FactorGraphs/FactorGraph.php
@@ -0,0 +1,18 @@
+_variableFactory;
+ }
+
+ public function setVariableFactory($factory)
+ {
+ $this->_variableFactory = $factory;
+ }
+}
+?>
diff --git a/PHPSkills/FactorGraphs/FactorGraphLayer.php b/PHPSkills/FactorGraphs/FactorGraphLayer.php
new file mode 100644
index 0000000..7fa7f5a
--- /dev/null
+++ b/PHPSkills/FactorGraphs/FactorGraphLayer.php
@@ -0,0 +1,67 @@
+_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;
+ }
+}
+
+
+?>
diff --git a/PHPSkills/FactorGraphs/FactorList.php b/PHPSkills/FactorGraphs/FactorList.php
new file mode 100644
index 0000000..a875e5f
--- /dev/null
+++ b/PHPSkills/FactorGraphs/FactorList.php
@@ -0,0 +1,57 @@
+
+/// Helper class for computing the factor graph's normalization constant.
+///
+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;
+ }
+}
+
+?>
diff --git a/PHPSkills/FactorGraphs/Message.php b/PHPSkills/FactorGraphs/Message.php
new file mode 100644
index 0000000..f64178a
--- /dev/null
+++ b/PHPSkills/FactorGraphs/Message.php
@@ -0,0 +1,28 @@
+_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);
+ }
+}
+
+?>
diff --git a/PHPSkills/FactorGraphs/Schedule.php b/PHPSkills/FactorGraphs/Schedule.php
new file mode 100644
index 0000000..0c432ab
--- /dev/null
+++ b/PHPSkills/FactorGraphs/Schedule.php
@@ -0,0 +1,89 @@
+_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;
+ }
+}
+
+?>
diff --git a/PHPSkills/FactorGraphs/Variable.php b/PHPSkills/FactorGraphs/Variable.php
new file mode 100644
index 0000000..ccd65d3
--- /dev/null
+++ b/PHPSkills/FactorGraphs/Variable.php
@@ -0,0 +1,71 @@
+_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;
+ }
+}
+
+?>
diff --git a/PHPSkills/FactorGraphs/VariableFactory.php b/PHPSkills/FactorGraphs/VariableFactory.php
new file mode 100644
index 0000000..08c4900
--- /dev/null
+++ b/PHPSkills/FactorGraphs/VariableFactory.php
@@ -0,0 +1,28 @@
+ 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
+
+?>
diff --git a/PHPSkills/Numerics/GaussianDistribution.php b/PHPSkills/Numerics/GaussianDistribution.php
index 999c5ce..724185e 100644
--- a/PHPSkills/Numerics/GaussianDistribution.php
+++ b/PHPSkills/Numerics/GaussianDistribution.php
@@ -48,6 +48,16 @@ class GaussianDistribution
{
return $this->_standardDeviation;
}
+
+ public function getPrecision()
+ {
+ return $this->_precision;
+ }
+
+ public function getPrecisionMean()
+ {
+ return $this->_precisionMean;
+ }
public function getNormalizationConstant()
{
diff --git a/PHPSkills/RatingContainer.php b/PHPSkills/RatingContainer.php
index 382b962..6b2a117 100644
--- a/PHPSkills/RatingContainer.php
+++ b/PHPSkills/RatingContainer.php
@@ -1,46 +1,40 @@
_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());
+ }
}
?>
diff --git a/PHPSkills/TrueSkill/FactorGraphTrueSkillCalculator.php b/PHPSkills/TrueSkill/FactorGraphTrueSkillCalculator.php
new file mode 100644
index 0000000..2765c62
--- /dev/null
+++ b/PHPSkills/TrueSkill/FactorGraphTrueSkillCalculator.php
@@ -0,0 +1,168 @@
+
+/// Calculates TrueSkill using a full factor graph.
+///
+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;
+ }
+}
+
+?>
diff --git a/PHPSkills/TrueSkill/Factors/GaussianFactor.php b/PHPSkills/TrueSkill/Factors/GaussianFactor.php
new file mode 100644
index 0000000..267246c
--- /dev/null
+++ b/PHPSkills/TrueSkill/Factors/GaussianFactor.php
@@ -0,0 +1,32 @@
+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));
+ }
+}
+
+
+?>
diff --git a/PHPSkills/TrueSkill/Factors/GaussianGreaterThanFactor.php b/PHPSkills/TrueSkill/Factors/GaussianGreaterThanFactor.php
new file mode 100644
index 0000000..8f332a8
--- /dev/null
+++ b/PHPSkills/TrueSkill/Factors/GaussianGreaterThanFactor.php
@@ -0,0 +1,73 @@
+
+/// Factor representing a team difference that has exceeded the draw margin.
+///
+/// See the accompanying math paper for more details.
+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);
+ }
+}
+?>
diff --git a/PHPSkills/TrueSkill/Factors/GaussianLikelihoodFactor.php b/PHPSkills/TrueSkill/Factors/GaussianLikelihoodFactor.php
new file mode 100644
index 0000000..7927254
--- /dev/null
+++ b/PHPSkills/TrueSkill/Factors/GaussianLikelihoodFactor.php
@@ -0,0 +1,78 @@
+
+/// Connects two variables and adds uncertainty.
+///
+/// See the accompanying math paper for more details.
+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();
+ }
+ }
+}
+
+?>
diff --git a/PHPSkills/TrueSkill/Factors/GaussianPriorFactor.php b/PHPSkills/TrueSkill/Factors/GaussianPriorFactor.php
new file mode 100644
index 0000000..e4500b9
--- /dev/null
+++ b/PHPSkills/TrueSkill/Factors/GaussianPriorFactor.php
@@ -0,0 +1,40 @@
+
+/// Supplies the factor graph with prior information.
+///
+/// See the accompanying math paper for more details.
+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);
+ }
+}
+
+
+?>
diff --git a/PHPSkills/TrueSkill/Factors/GaussianWeightedSumFactor.php b/PHPSkills/TrueSkill/Factors/GaussianWeightedSumFactor.php
new file mode 100644
index 0000000..e4a3e53
--- /dev/null
+++ b/PHPSkills/TrueSkill/Factors/GaussianWeightedSumFactor.php
@@ -0,0 +1,216 @@
+
+/// Factor that sums together multiple Gaussians.
+///
+/// See the accompanying math paper for more details.
+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);
+ }
+}
+
+?>
diff --git a/PHPSkills/TrueSkill/Factors/GaussianWithinFactor.php b/PHPSkills/TrueSkill/Factors/GaussianWithinFactor.php
new file mode 100644
index 0000000..28a6822
--- /dev/null
+++ b/PHPSkills/TrueSkill/Factors/GaussianWithinFactor.php
@@ -0,0 +1,72 @@
+
+/// Factor representing a team difference that has not exceeded the draw margin.
+///
+/// See the accompanying math paper for more details.
+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);
+ }
+}
+
+?>
diff --git a/PHPSkills/TrueSkill/Layers/IteratedTeamDifferencesInnerLayer.php b/PHPSkills/TrueSkill/Layers/IteratedTeamDifferencesInnerLayer.php
new file mode 100644
index 0000000..7c3b356
--- /dev/null
+++ b/PHPSkills/TrueSkill/Layers/IteratedTeamDifferencesInnerLayer.php
@@ -0,0 +1,147 @@
+_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;
+ }
+}
+
+?>
diff --git a/PHPSkills/TrueSkill/Layers/PlayerPerformancesToTeamPerformancesLayer.php b/PHPSkills/TrueSkill/Layers/PlayerPerformancesToTeamPerformancesLayer.php
new file mode 100644
index 0000000..7b179d6
--- /dev/null
+++ b/PHPSkills/TrueSkill/Layers/PlayerPerformancesToTeamPerformancesLayer.php
@@ -0,0 +1,71 @@
+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(
+ "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);
+ }
+}
+
+?>
diff --git a/PHPSkills/TrueSkill/Layers/PlayerPriorValuesToSkillsLayer.php b/PHPSkills/TrueSkill/Layers/PlayerPriorValuesToSkillsLayer.php
new file mode 100644
index 0000000..aedf59c
--- /dev/null
+++ b/PHPSkills/TrueSkill/Layers/PlayerPriorValuesToSkillsLayer.php
@@ -0,0 +1,59 @@
+_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);
+ }
+}
+
+?>
diff --git a/PHPSkills/TrueSkill/Layers/PlayerSkillsToPerformancesLayer.php b/PHPSkills/TrueSkill/Layers/PlayerSkillsToPerformancesLayer.php
new file mode 100644
index 0000000..ed5eeea
--- /dev/null
+++ b/PHPSkills/TrueSkill/Layers/PlayerSkillsToPerformancesLayer.php
@@ -0,0 +1,64 @@
+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");
+ }
+}
+
+?>
diff --git a/PHPSkills/TrueSkill/Layers/TeamDifferencesComparisonLayer.php b/PHPSkills/TrueSkill/Layers/TeamDifferencesComparisonLayer.php
new file mode 100644
index 0000000..4468688
--- /dev/null
+++ b/PHPSkills/TrueSkill/Layers/TeamDifferencesComparisonLayer.php
@@ -0,0 +1,38 @@
+_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);
+ }
+ }
+}
+
+?>
diff --git a/PHPSkills/TrueSkill/Layers/TeamPerformancesToTeamPerformanceDifferencesLayer.php b/PHPSkills/TrueSkill/Layers/TeamPerformancesToTeamPerformanceDifferencesLayer.php
new file mode 100644
index 0000000..872052a
--- /dev/null
+++ b/PHPSkills/TrueSkill/Layers/TeamPerformancesToTeamPerformanceDifferencesLayer.php
@@ -0,0 +1,42 @@
+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");
+ }
+}
+
+?>
diff --git a/PHPSkills/TrueSkill/Layers/TrueSkillFactorGraphLayer.php b/PHPSkills/TrueSkill/Layers/TrueSkillFactorGraphLayer.php
new file mode 100644
index 0000000..df2193c
--- /dev/null
+++ b/PHPSkills/TrueSkill/Layers/TrueSkillFactorGraphLayer.php
@@ -0,0 +1,13 @@
+