replace \Closure with Callable

This commit is contained in:
Dennis Eichhorn 2023-08-14 16:38:39 +00:00
parent bc1cd635fe
commit 11b6f5f471
10 changed files with 611 additions and 17 deletions

View File

@ -35,10 +35,10 @@ final class Kmeans
/**
* Metric to calculate the distance between two points
*
* @var \Closure
* @var Callable
* @since 1.0.0
*/
private \Closure $metric;
private Callable $metric;
/**
* Points of the cluster centers
@ -53,11 +53,11 @@ final class Kmeans
*
* @param PointInterface[] $points Points to cluster
* @param int<0, max> $clusters Amount of clusters
* @param null|\Closure $metric metric to use for the distance between two points
* @param null|Callable $metric metric to use for the distance between two points
*
* @since 1.0.0
*/
public function __construct(array $points, int $clusters, \Closure $metric = null)
public function __construct(array $points, int $clusters, Callable $metric = null)
{
$this->metric = $metric ?? function (PointInterface $a, PointInterface $b) {
$aCoordinates = $a->getCoordinates();

View File

@ -0,0 +1,82 @@
<?php
/**
* Jingga
*
* PHP Version 8.1
*
* @package phpOMS\Algorithm\Rating
* @copyright Dennis Eichhorn
* @license OMS License 2.0
* @version 1.0.0
* @link https://jingga.app
*/
declare(strict_types=1);
namespace phpOMS\Algorithm\Rating;
/**
* Calculate rating strength using the Bradley Terry model
*
* @package phpOMS\Algorithm\Rating
* @license OMS License 2.0
* @link https://jingga.app
* @since 1.0.0
* @see https://en.wikipedia.org/wiki/Bradley%E2%80%93Terry_model
*/
final class BradleyTerry
{
public int $K = 32;
public int $DEFAULT_ELO = 1500;
public int $MIN_ELO = 100;
// history = matrix of past victories/performance against other teams (diagonal is empty)
/**
* @example rating(
* [
* 'A' => ['A' => 0, 'B' => 2, 'C' => 0, 'D' => 1],
* 'B' => ['A' => 3, 'B' => 0, 'C' => 5, 'D' => 0],
* 'C' => ['A' => 0, 'B' => 3, 'C' => 0, 'D' => 1],
* 'D' => ['A' => 4, 'B' => 0, 'C' => 3, 'D' => 0],
* ],
* 20
* ) // [0.139, 0.226, 0.143, 0.492]
*/
public function rating(array $history, int $iterations = 20) : array
{
$keys = \array_keys($history);
$pOld = [];
foreach ($keys as $key) {
$pOld[$key] = 1;
}
$p = $pOld;
for ($i = 0; $i < $iterations; ++$i) {
foreach ($history as $idx => $row) {
$W = \array_sum($row);
$d = 0;
foreach ($history as $idx2 => $_) {
if ($idx === $idx2) {
continue;
}
$d += ($history[$idx][$idx2] + $history[$idx2][$idx])
/ ($pOld[$idx] + $pOld[$idx2]);
}
$p[$idx] = $W / $d;
}
$norm = \array_sum($p);
foreach ($p as $idx => $_) {
$p[$idx] /= $norm;
}
$pOld = $p;
}
return $p;
}
}

48
Algorithm/Rating/Elo.php Normal file
View File

@ -0,0 +1,48 @@
<?php
/**
* Jingga
*
* PHP Version 8.1
*
* @package phpOMS\Algorithm\Rating
* @copyright Dennis Eichhorn
* @license OMS License 2.0
* @version 1.0.0
* @link https://jingga.app
*/
declare(strict_types=1);
namespace phpOMS\Algorithm\Rating;
/**
* Elo rating calculation using Elo rating
*
* @package phpOMS\Algorithm\Rating
* @license OMS License 2.0
* @link https://jingga.app
* @since 1.0.0
* @see https://en.wikipedia.org/wiki/Elo_rating_system
*/
final class Elo
{
public int $K = 32;
public int $DEFAULT_ELO = 1500;
public int $MIN_ELO = 100;
public function rating(int $elo, array $oElo, array $s)
{
$eloNew = $elo;
foreach ($oElo as $idx => $o) {
$expected = 1 / (1 + 10 ** (($o - $elo) / 400));
$r = $this->K * ($s[$idx] - $expected);
$eloNew += $r;
}
return [
'elo' => (int) \max((int) $eloNew, $this->MIN_ELO),
];
}
}

View File

@ -0,0 +1,109 @@
<?php
/**
* Jingga
*
* PHP Version 8.1
*
* @package phpOMS\Algorithm\Rating
* @copyright Dennis Eichhorn
* @license OMS License 2.0
* @version 1.0.0
* @link https://jingga.app
*/
declare(strict_types=1);
namespace phpOMS\Algorithm\Rating;
/**
* Elo rating calculation using Glicko-1
*
* @package phpOMS\Algorithm\Rating
* @license OMS License 2.0
* @link https://jingga.app
* @since 1.0.0
* @see https://en.wikipedia.org/wiki/Glicko_rating_system
* @see http://www.glicko.net/glicko/glicko.pdf
*/
final class Glicko1
{
public const Q = 0.00575646273; // ln(10) / 400
public int $DEFAULT_ELO = 1500;
public int $DEFAULT_RD = 350;
public float $DEFAULT_C = 34.6;
public int $MIN_ELO = 100;
public int $MIN_RD = 50;
/**
* Calcualte the glicko-1 elo
*
* @param int $eloOld Old elo
* @param int $rdOld Old deviation (50 === +/-100 elo points)
* @param int $lastMatchDate Last match date used to calculate the time difference (can be days, months, ... depending on your match interval)
* @param int $matchDate Match date (usually day)
* @param float[] $s Match results (1 = victor, 0 = loss, 0.5 = draw)
* @param int[] $oElo Opponent elo
* @param int[] $oRd Opponent deviation
*
* @return array{elo:int, rd:int}
*
* @since 1.0.0
*/
public function rating(
int $eloOld = 1500,
int $rdOld = 50,
int $lastMatchDate = 0,
int $matchDate = 0,
array $s = [],
array $oElo = [],
array $oRd = []
) : array
{
// Step 1:
$s = [];
$E = [];
$gRD = [];
$RD = \min(
350,
\max(
\sqrt(
$rdOld * $rdOld
+ $this->DEFAULT_C * $this->DEFAULT_C * \max(0, $matchDate - $lastMatchDate)
),
$this->MIN_RD
)
);
// Step 2:
foreach ($oElo as $id => $e) {
$gRD_t = 1 / (\sqrt(1 + 3 * self::Q * self::Q * $oRd[$id] * $oRd[$id] / (\M_PI * \M_PI)));
$gRD[] = $gRD_t;
$E[] = 1 / (1 + \pow(10, $gRD_t * ($eloOld - $e) / -400));
}
$d = 0;
foreach ($E as $id => $_) {
$d += $gRD[$id] * $gRD[$id] * $E[$id] * (1 - $E[$id]);
}
$d2 = 1 / (self::Q * self::Q * $d);
$r = 0;
foreach ($E as $id => $_) {
$r += $gRD[$id] * ($s[$id] - $E[$id]);
}
$r = $eloOld + self::Q / (1 / ($RD * $RD) + 1 / $d2) * $r;
// Step 3:
$RD_ = \sqrt(1 / (1 / ($RD * $RD) + 1 / $d2));
return [
'elo' => (int) \max((int) $r, $this->MIN_ELO),
'rd' => (int) \max($RD_, $this->MIN_RD)
];
}
}

View File

@ -0,0 +1,118 @@
<?php
/**
* Jingga
*
* PHP Version 8.1
*
* @package phpOMS\Algorithm\Rating
* @copyright Dennis Eichhorn
* @license OMS License 2.0
* @version 1.0.0
* @link https://jingga.app
*/
declare(strict_types=1);
namespace phpOMS\Algorithm\Rating;
use phpOMS\Math\Solver\Root\Bisection;
/**
* Elo rating calculation using Glicko-2
*
* @package phpOMS\Algorithm\Rating
* @license OMS License 2.0
* @link https://jingga.app
* @since 1.0.0
* @see https://en.wikipedia.org/wiki/Glicko_rating_system
* @see http://www.glicko.net/glicko/glicko2.pdf
*
* @todo: implement
*/
final class Glicko2
{
public float $tau = 0.5;
public int $DEFAULT_ELO = 1500;
public int $DEFAULT_RD = 350;
public float $DEFAULT_VOLATILITY = 0.06;
public int $MIN_ELO = 100;
public int $MIN_RD = 50;
/**
* @example $glicko->elo(1500, 200, 0.06, [1,0,0], [1400,1550,1700], [30,100,300]) // 1464, 151, 0.059
*/
public function rating(
int $eloOld = 1500,
int $rdOld = 50,
float $volOld = 0.06,
array $s = [],
array $oElo = [],
array $oRd = []
) : array
{
$tau = $this->tau;
// Step 0:
$rdOld = $rdOld / 173.7178;
$eloOld = ($eloOld - $this->DEFAULT_ELO) / 173.7178;
foreach ($oElo as $idx => $value) {
$oElo[$idx] = ($value - $this->DEFAULT_ELO) / 173.7178;
}
foreach ($oRd as $idx => $value) {
$oRd[$idx] = $value / 173.7178;
}
// Step 1:
$g = [];
foreach ($oRd as $rd) {
$g[] = 1 / \sqrt(1 + 3 * $rd * $rd / (\M_PI * \M_PI));
}
$E = [];
foreach ($oElo as $idx => $elo) {
$E[] = 1 / (1 + \exp(-$g[$idx] * ($eloOld - $elo)));
}
$v = 0;
foreach ($g as $idx => $t) {
$v += $t * $t * $E[$idx] * (1 - $E[$idx]);
}
$v = 1 / $v;
$tDelta = 0;
foreach ($g as $idx => $t) {
$tDelta += $t * ($s[$idx] - $E[$idx]);
}
$Delta = $v * $tDelta;
// Step 2:
$fn = function($x) use ($Delta, $rdOld, $v, $tau, $volOld)
{
return 0.5 * (\exp($x) * ($Delta ** 2 - $rdOld ** 2 - $v - \exp($x))) / (($rdOld ** 2 + $v + \exp($x)) ** 2)
- ($x - \log($volOld ** 2)) / ($tau ** 2);
};
$root = Bisection::bisection($fn, -100, 100, 1000);
$vol = \exp($root / 2);
// Step 3:
$RD = 1 / \sqrt(1 / ($rdOld ** 2 + $vol ** 2) + 1 / $v);
$r = $eloOld + $RD ** 2 * $tDelta;
// Undo step 0:
$RD = 173.7178 * $RD;
$r = 173.7178 * $r + $this->DEFAULT_ELO;
return [
'elo' => (int) \max($r, $this->MIN_ELO),
'rd' => (int) \max($RD, $this->MIN_RD),
'vol' => $vol,
];
}
}

View File

@ -0,0 +1,234 @@
<?php
/**
* Jingga
*
* PHP Version 8.1
*
* @package phpOMS\Algorithm\Rating
* @copyright Microsoft
* @license This algorithm may be patented by Microsoft, verify and acquire a license if necessary
* @version 1.0.0
* @link https://jingga.app
*/
declare(strict_types=1);
namespace phpOMS\Algorithm\Rating;
use phpOMS\Math\Stochastic\Distribution\NormalDistribution;
/**
* Elo rating calculation using Elo rating
*
* @package phpOMS\Algorithm\Rating
* @license OMS License 2.0
* @link https://jingga.app
* @since 1.0.0
* @see https://www.moserware.com/assets/computing-your-skill/The%20Math%20Behind%20TrueSkill.pdf
*
* @todo implement https://github.com/sublee/trueskill/blob/master/trueskill/__init__.py
*/
class TrueSkill
{
public int $DEFAULT_MU = 25;
public float $DEFAULT_SIGMA = 25 / 3;
public float $DEFAULT_BETA = 25 / 3 / 2;
public float $DEFAULT_TAU = 25 / 3 / 100;
public float $DEFAULT_DRAW_PROBABILITY = 0.1;
public function __construct()
{
}
// Draw margin = epsilon
/**
* P_{draw} = 2\Phi\left(\dfrac{\epsilon}{\sqrt{n_1 + n_2} * \beta}\right) - 1
*/
public function drawProbability(float $drawMargin, int $n1, int $n2, float $beta)
{
return 2 * NormalDistribution::getCdf($drawMargin / (\sqrt($n1 + $n2) * $beta), 0.0, 1.0) - 1;
}
/**
* \epsilon = \Phi^{-1}\left(\dfrac{P_{draw} + 1}{2}\right) * \sqrt{n_1 + n_2} * \beta
*/
public function drawMargin(float $drawProbability, int $n1, int $n2, float $beta)
{
return NormalDistribution::getIcdf(($drawProbability + 1) / 2.0, 0.0, 1.0) * \sqrt($n1 + $n2) * $beta;
}
/**
* Mean additive truncated gaussion function "v" for wins
*
* @latex c = \sqrt{2 * \beta^2 + \sigma_{winner}^2 + \sigma_{loser}^2}
* @latex \mu_{winner} = \mu_{winner} + \dfrac{\sigma_{winner}^2}{c} * \nu \left(\dfrac{\mu_{winner} - \mu_{loser}}{c}, \dfrac{\epsilon}{c}\right)
* @latex \mu_{loser} = \mu_{loser} + \dfrac{\sigma_{loser}^2}{c} * \nu \left(\dfrac{\mu_{winner} - \mu_{loser}}{c}, \dfrac{\epsilon}{c}\right)
* @latex t = \dfrac{\mu_{winner} - \mu_{loser}}{c}
*
* @latex \nu = \dfrac{\mathcal{N}(t - \epsilon)}{\Phi(t - \epsilon)}
*
* @param float $t Difference winner and loser mu
* @param float $epsilon Draw margin
*
* @return float
*
* @since 1.0.0
*/
private function vWin(float $t, float $epsilon) : float
{
return NormalDistribution::getPdf($t - $epsilon, 0, 1.0) / NormalDistribution::getCdf($t - $epsilon, 0.0, 1.0);
}
/**
* Mean additive truncated gaussion function "v" for draws
*
* @latex c = \sqrt{2 * \beta^2 + \sigma_{winner}^2 + \sigma_{loser}^2}
* @latex \mu_{winner} = \mu_{winner} + \dfrac{\sigma_{winner}^2}{c} * \nu \left(\dfrac{\mu_{winner} - \mu_{loser}}{c}, \dfrac{\epsilon}{c}\right)
* @latex \mu_{loser} = \mu_{loser} + \dfrac{\sigma_{loser}^2}{c} * \nu \left(\dfrac{\mu_{winner} - \mu_{loser}}{c}, \dfrac{\epsilon}{c}\right)
* @latex t = \dfrac{\mu_{winner} - \mu_{loser}}{c}
* @latex \dfrac{\mathcal{N}(t - \epsilon)}{\Phi(t - \epsilon)}
*
* @latex \nu = \dfrac{\mathcal{N}(-\epsilon - t) - \mathcal{N}(\epsilon - t)}{\Phi(\epsilon - t) - \Phi(-\epsilon - t)}
*
* @param float $t Difference winner and loser mu
* @param float $epsilon Draw margin
*
* @return float
*
* @since 1.0.0
*/
private function vDraw(float $t, float $epsilon) : float
{
$tAbs = \abs($t);
$a = $epsilon - $tAbs;
$b = -$epsilon - $tAbs;
$aPdf = NormalDistribution::getPdf($a, 0.0, 1.0);
$bPdf = NormalDistribution::getPdf($b, 0.0, 1.0);
$numer = $bPdf - $aPdf;
$aCdf = NormalDistribution::getCdf($a, 0.0, 1.0);
$bCdf = NormalDistribution::getCdf($b, 0.0, 1.0);
$denom = $aCdf - $bCdf;
return $numer / $denom;
}
/**
* Variance multiplicative function "w" for draws
*
* @latex w = \nu * (\nu + t - \epsilon)
*
* @param float $t Difference winner and loser mu
* @param float $epsilon Draw margin
*
* @return float
*
* @since 1.0.0
*/
private function wWin(float $t, float $epsilon) : float
{
$v = $this->vWin($t, $epsilon);
return $v * ($v + $t - $epsilon);
}
/**
* Variance multiplicative function "w" for draws
*
* @latex w = \nu^2 + \dfrac{(\epsilon - t) * \mathcal{N}(\epsilon - t) + (\epsilon + t) * \mathcal{N}(\epsilon + t)}{\Phi(\epsilon - t) - \Phi(-\epsilon - t)}
*
* @param float $t Difference winner and loser mu
* @param float $epsilon Draw margin
*
* @return float
*
* @since 1.0.0
*/
private function wDraw(float $t, float $epsilon) : float
{
$tAbs = \abs($t);
$v = $this->vDraw($t, $epsilon);
return $v * $v
+ (($epsilon - $t) * NormalDistribution::getPdf($epsilon - $tAbs, 0.0, 1.0) + ($epsilon + $tAbs) * NormalDistribution::getPdf($epsilon + $tAbs, 0.0, 1.0))
/ (NormalDistribution::getCdf($epsilon - $tAbs, 0.0, 1.0) - NormalDistribution::getCdf(-$epsilon - $tAbs, 0.0, 1.0));
}
private function buildRatingLayer()
{
}
private function buildPerformanceLayer()
{
}
private function buildTeamPerformanceLayer()
{
}
private function buildTruncLayer()
{
}
private function factorGraphBuilders()
{
// Rating layer
// Performance layer
// Team Performance layer
// Trunc layer
return [
'rating_layer' => $ratingLayer,
'performance_layer' => $ratingLayer,
'team_performance_layer' => $ratingLayer,
'trunc_layer' => $ratingLayer,
];
}
public function rating()
{
// Start values
$mu = 25;
$sigma = $mu / 3;
$beta = $sigma / 2;
$tau = $sigma / 100;
$Pdraw = 0.1;
$alpha = 0.25;
// Partial update
$sigmaPartial = $sigmaOld * $sigmaNew / \sqrt($alpha * $sigmaOld * $sigmaOld - ($alpha - 1) * $sigmaNew * $sigmaNew);
$muPartial = $muOld * ($alpha - 1) * $sigmaNew * $sigmaNew - $muNew * $alpha * $sigmaOld * $sigmaOld
/ (($alpha - 1) * $sigmaNew * $sigmaNew - $alpha * $sigmaOld * $sigmaOld);
// New
$tau = $pi * $mu;
$P = NormalDistribution::getCdf(($s1 - $s2) / (\sqrt(2) * $beta));
$Delta = $alpha * $beta * \sqrt($pi) * (($y + 1) / 2 - $P);
$K = NormalDistribution::getCdf();
$pi = 1 / ($sigma * $sigma);
}
}

View File

@ -137,12 +137,12 @@ final class UpdateMapper extends DataMapperAbstract
$id = \is_object($tValue) ? $this->updateOwnsOne($propertyName, $tValue) : $tValue;
$value = $this->parseValue($column['type'], $id);
$query->set([$this->mapper::TABLE . '.' . $column['name'] => $value]);
$query->set([$column['name'] => $value]);
} elseif (isset($this->mapper::BELONGS_TO[$propertyName])) {
$id = \is_object($tValue) ? $this->updateBelongsTo($propertyName, $tValue) : $tValue;
$value = $this->parseValue($column['type'], $id);
$query->set([$this->mapper::TABLE . '.' . $column['name'] => $value]);
$query->set([$column['name'] => $value]);
} elseif ($column['name'] !== $this->mapper::PRIMARYFIELD) {
if (\stripos($column['internal'], '/') !== false) {
$path = \substr($column['internal'], \stripos($column['internal'], '/') + 1);
@ -151,11 +151,14 @@ final class UpdateMapper extends DataMapperAbstract
$value = $this->parseValue($column['type'], $tValue);
$query->set([$this->mapper::TABLE . '.' . $column['name'] => $value]);
$query->set([$column['name'] => $value]);
}
}
$sth = $this->db->con->prepare($query->toSql());
// @todo:
// @bug: Sqlite doesn't allow table_name.column_name in set queries for whatver reason.
$sth = $this->db->con->prepare($a = $query->toSql());
if ($sth !== false) {
$sth->execute();
}

View File

@ -61,7 +61,7 @@ final class Dispatcher implements DispatcherInterface
/**
* {@inheritdoc}
*/
public function dispatch(array | string | \Closure $controller, mixed ...$data) : array
public function dispatch(array | string | Callable $controller, mixed ...$data) : array
{
$views = [];
$data = \array_values($data);
@ -165,14 +165,14 @@ final class Dispatcher implements DispatcherInterface
/**
* Dispatch closure.
*
* @param \Closure $controller Controller string
* @param Callable $controller Controller string
* @param null|array $data Data
*
* @return mixed
*
* @since 1.0.0
*/
private function dispatchClosure(\Closure $controller, array $data = null) : mixed
private function dispatchClosure(Callable $controller, array $data = null) : mixed
{
return $data === null ? $controller($this->app) : $controller($this->app, ...$data);
}

View File

@ -27,7 +27,7 @@ interface DispatcherInterface
/**
* Dispatch controller.
*
* @param array|\Closure|string $controller Controller
* @param array|Callable|string $controller Controller
* @param mixed ...$data Data
*
* @return array Returns array of all dispatched results
@ -36,5 +36,5 @@ interface DispatcherInterface
*
* @since 1.0.0
*/
public function dispatch(array | string | \Closure $controller, mixed ...$data) : array;
public function dispatch(array | string | Callable $controller, mixed ...$data) : array;
}

View File

@ -70,9 +70,9 @@ final class EventManager implements \Countable
/**
* {@inheritdoc}
*/
public function dispatch(array | string | \Closure $func, mixed ...$data) : array
public function dispatch(array | string | Callable $func, mixed ...$data) : array
{
if (!($func instanceof \Closure)) {
if (!\is_callable($func)) {
return [];
}
@ -136,7 +136,7 @@ final class EventManager implements \Countable
* Attach new event
*
* @param string $group Name of the event (unique)
* @param string|\Closure $callback Callback or route for the event
* @param string|Callable $callback Callback or route for the event
* @param bool $remove Remove event after triggering it?
* @param bool $reset Reset event after triggering it? Remove must be false!
*
@ -144,7 +144,7 @@ final class EventManager implements \Countable
*
* @since 1.0.0
*/
public function attach(string $group, string | \Closure $callback, bool $remove = false, bool $reset = false) : bool
public function attach(string $group, string | Callable $callback, bool $remove = false, bool $reset = false) : bool
{
if (!isset($this->callbacks[$group])) {
$this->callbacks[$group] = ['remove' => $remove, 'reset' => $reset, 'callbacks' => []];