From e434696b443230e1677d55646c3a307055fefe8e Mon Sep 17 00:00:00 2001 From: Jeff Moser Date: Sat, 18 Sep 2010 11:11:44 -0400 Subject: [PATCH] Start of factor graph port. Things don't work yet, but a lot of syntax updates towards PHP --- HashMap.php | 46 ++++ PHPSkills/FactorGraphs/Factor.php | 96 ++++++++ PHPSkills/FactorGraphs/FactorGraph.php | 18 ++ PHPSkills/FactorGraphs/FactorGraphLayer.php | 67 ++++++ PHPSkills/FactorGraphs/FactorList.php | 57 +++++ PHPSkills/FactorGraphs/Message.php | 28 +++ PHPSkills/FactorGraphs/Schedule.php | 89 ++++++++ PHPSkills/FactorGraphs/Variable.php | 71 ++++++ PHPSkills/FactorGraphs/VariableFactory.php | 28 +++ PHPSkills/Numerics/GaussianDistribution.php | 10 + PHPSkills/RatingContainer.php | 34 ++- .../FactorGraphTrueSkillCalculator.php | 168 ++++++++++++++ .../TrueSkill/Factors/GaussianFactor.php | 32 +++ .../Factors/GaussianGreaterThanFactor.php | 73 ++++++ .../Factors/GaussianLikelihoodFactor.php | 78 +++++++ .../TrueSkill/Factors/GaussianPriorFactor.php | 40 ++++ .../Factors/GaussianWeightedSumFactor.php | 216 ++++++++++++++++++ .../Factors/GaussianWithinFactor.php | 72 ++++++ .../IteratedTeamDifferencesInnerLayer.php | 147 ++++++++++++ ...yerPerformancesToTeamPerformancesLayer.php | 71 ++++++ .../Layers/PlayerPriorValuesToSkillsLayer.php | 59 +++++ .../PlayerSkillsToPerformancesLayer.php | 64 ++++++ .../Layers/TeamDifferencesComparisonLayer.php | 38 +++ ...ancesToTeamPerformanceDifferencesLayer.php | 42 ++++ .../Layers/TrueSkillFactorGraphLayer.php | 13 ++ 25 files changed, 1637 insertions(+), 20 deletions(-) create mode 100644 HashMap.php create mode 100644 PHPSkills/FactorGraphs/Factor.php create mode 100644 PHPSkills/FactorGraphs/FactorGraph.php create mode 100644 PHPSkills/FactorGraphs/FactorGraphLayer.php create mode 100644 PHPSkills/FactorGraphs/FactorList.php create mode 100644 PHPSkills/FactorGraphs/Message.php create mode 100644 PHPSkills/FactorGraphs/Schedule.php create mode 100644 PHPSkills/FactorGraphs/Variable.php create mode 100644 PHPSkills/FactorGraphs/VariableFactory.php create mode 100644 PHPSkills/TrueSkill/FactorGraphTrueSkillCalculator.php create mode 100644 PHPSkills/TrueSkill/Factors/GaussianFactor.php create mode 100644 PHPSkills/TrueSkill/Factors/GaussianGreaterThanFactor.php create mode 100644 PHPSkills/TrueSkill/Factors/GaussianLikelihoodFactor.php create mode 100644 PHPSkills/TrueSkill/Factors/GaussianPriorFactor.php create mode 100644 PHPSkills/TrueSkill/Factors/GaussianWeightedSumFactor.php create mode 100644 PHPSkills/TrueSkill/Factors/GaussianWithinFactor.php create mode 100644 PHPSkills/TrueSkill/Layers/IteratedTeamDifferencesInnerLayer.php create mode 100644 PHPSkills/TrueSkill/Layers/PlayerPerformancesToTeamPerformancesLayer.php create mode 100644 PHPSkills/TrueSkill/Layers/PlayerPriorValuesToSkillsLayer.php create mode 100644 PHPSkills/TrueSkill/Layers/PlayerSkillsToPerformancesLayer.php create mode 100644 PHPSkills/TrueSkill/Layers/TeamDifferencesComparisonLayer.php create mode 100644 PHPSkills/TrueSkill/Layers/TeamPerformancesToTeamPerformanceDifferencesLayer.php create mode 100644 PHPSkills/TrueSkill/Layers/TrueSkillFactorGraphLayer.php 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 @@ +