diff --git a/Algorithm/Clustering/AffinityPropagation.php b/Algorithm/Clustering/AffinityPropagation.php index 52d5fc5be..b5ca7250d 100644 --- a/Algorithm/Clustering/AffinityPropagation.php +++ b/Algorithm/Clustering/AffinityPropagation.php @@ -20,8 +20,8 @@ namespace phpOMS\Algorithm\Clustering; * @package phpOMS\Algorithm\Clustering * @license OMS License 2.0 * @link https://jingga.app - * @since 1.0.0 * @see ./clustering_overview.png + * @since 1.0.0 * * @todo Implement */ diff --git a/Algorithm/Clustering/AgglomerativeClustering.php b/Algorithm/Clustering/AgglomerativeClustering.php index a699a54a6..3f0ee601e 100644 --- a/Algorithm/Clustering/AgglomerativeClustering.php +++ b/Algorithm/Clustering/AgglomerativeClustering.php @@ -20,8 +20,8 @@ namespace phpOMS\Algorithm\Clustering; * @package phpOMS\Algorithm\Clustering * @license OMS License 2.0 * @link https://jingga.app - * @since 1.0.0 * @see ./clustering_overview.png + * @since 1.0.0 * * @todo Implement */ diff --git a/Algorithm/Clustering/Birch.php b/Algorithm/Clustering/Birch.php index cded72653..57ba4522e 100644 --- a/Algorithm/Clustering/Birch.php +++ b/Algorithm/Clustering/Birch.php @@ -20,8 +20,8 @@ namespace phpOMS\Algorithm\Clustering; * @package phpOMS\Algorithm\Clustering * @license OMS License 2.0 * @link https://jingga.app - * @since 1.0.0 * @see ./clustering_overview.png + * @since 1.0.0 * * @todo Implement */ diff --git a/Algorithm/Clustering/DBSCAN.php b/Algorithm/Clustering/DBSCAN.php index 5481565d7..99102d72a 100644 --- a/Algorithm/Clustering/DBSCAN.php +++ b/Algorithm/Clustering/DBSCAN.php @@ -24,8 +24,8 @@ use phpOMS\Math\Topology\MetricsND; * @package phpOMS\Algorithm\Clustering * @license OMS License 2.0 * @link https://jingga.app - * @since 1.0.0 * @see ./clustering_overview.png + * @since 1.0.0 * * @todo Expand to n dimensions */ @@ -66,13 +66,19 @@ final class DBSCAN /** * Clusters * - * Array of clusters containing point ids + * Array of points assigned to a cluster * - * @var array + * @var array * @since 1.0.0 */ private array $clusters = []; + /** + * Convex hull of all clusters + * + * @var array + * @since 1.0.0 + */ private array $convexHulls = []; /** @@ -85,12 +91,20 @@ final class DBSCAN */ private array $clusteredPoints = []; + /** + * Distance matrix + * + * Distances between points + * + * @var array + * @since 1.0.0 + */ private array $distanceMatrix = []; /** * Constructor * - * @param null|\Closure $metric metric to use for the distance between two points + * @param null|\Closure $metric metric to use for the distance between two points * * @since 1.0.0 */ @@ -104,50 +118,88 @@ final class DBSCAN }; } - private function expandCluster(PointInterface $point, array $neighbors, int $c, float $epsilon, int $minPoints) : void - { - $this->clusters[$c][] = $point; - $this->clusteredPoints[] = $point; - $nPoint = reset($neighbors); + /** + * Expand cluster with additional point and potential neighbors. + * + * @param PointInterface $point Point to add to a cluster + * @param array $neighbors Neighbors of point + * @param int $c Cluster id + * @param float $epsilon Max distance + * @param int $minPoints Min amount of points required for a cluster + * + * @return void + * + * @since 1.0.0 + */ + private function expandCluster( + PointInterface $point, + array $neighbors, + int $c, + float $epsilon, + int $minPoints + ) : void + { + $this->clusters[$c][] = $point; + $this->clusteredPoints[] = $point; + $nPoint = reset($neighbors); - while ($nPoint) { - $neighbors2 = $this->findNeighbors($nPoint, $epsilon); + while ($nPoint) { + $neighbors2 = $this->findNeighbors($nPoint, $epsilon); - if (\count($neighbors2) >= $minPoints) { - foreach ($neighbors2 as $nPoint2) { - if (!isset($neighbors[$nPoint2->name])) { - $neighbors[$nPoint2->name] = $nPoint2; - } - } - } + if (\count($neighbors2) >= $minPoints) { + foreach ($neighbors2 as $nPoint2) { + if (!isset($neighbors[$nPoint2->name])) { + $neighbors[$nPoint2->name] = $nPoint2; + } + } + } - if (!\in_array($nPoint->name, $this->clusteredPoints)) { - $this->clusters[$c][] = $nPoint; - $this->clusteredPoints[] = $nPoint; - } + if (!\in_array($nPoint->name, $this->clusteredPoints)) { + $this->clusters[$c][] = $nPoint; + $this->clusteredPoints[] = $nPoint; + } - $nPoint = next($neighbors); - } - } + $nPoint = next($neighbors); + } + } - private function findNeighbors(PointInterface $point, float $epsilon) : array - { - $neighbors = []; - foreach ($this->points as $point2) { - if ($point->isEquals($point2)) { + /** + * Find neighbors of a point + * + * @param PointInterface $point Base point for potential neighbors + * @param float $epsion Max distance to neighbor + * + * @return array + * + * @since 1.0.0 + */ + private function findNeighbors(PointInterface $point, float $epsilon) : array + { + $neighbors = []; + foreach ($this->points as $point2) { + if ($point->isEquals($point2)) { $distance = isset($this->distanceMatrix[$point->name]) ? $this->distanceMatrix[$point->name][$point2->name] : $this->distanceMatrix[$point2->name][$point->name]; - if ($distance < $epsilon) { - $neighbors[$point2->name] = $point2; - } - } - } + if ($distance < $epsilon) { + $neighbors[$point2->name] = $point2; + } + } + } - return $neighbors; - } + return $neighbors; + } + /** + * Generate distances between points + * + * @param array $points Array of all points + * + * @return float[] + * + * @since 1.0.0 + */ private function generateDistanceMatrix(array $points) : array { $distances = []; @@ -161,6 +213,15 @@ final class DBSCAN return $distances; } + /** + * Find the cluster for a point + * + * @param PointInterface $point Point to find the cluster for + * + * @return int Cluster id + * + * @since 1.0.0 + */ public function cluster(PointInterface $point) : int { if ($this->convexHulls === []) { @@ -193,28 +254,40 @@ final class DBSCAN return -1; } - public function generateClusters(array $points, float $epsilon, int $minPoints) : void - { - $this->noisePoints = []; - $this->clusters = []; - $this->clusteredPoints = []; - $this->points = $points; - $this->convexHulls = []; + /** + * Generate the clusters of the points + * + * @param PointInterface[] $points Points to cluster + * @param float $epsilon Max distance + * @param int $minPoints Min amount of points required for a cluster + * + * @return void + * + * @since 1.0.0 + */ + public function generateClusters(array $points, float $epsilon, int $minPoints) : void + { + $this->noisePoints = []; + $this->clusters = []; + $this->clusteredPoints = []; + $this->points = $points; + $this->convexHulls = []; $this->distanceMatrix = $this->generateDistanceMatrix($points); - $c = 0; - $this->clusters[$c] = []; - foreach ($this->points as $point) { - $neighbors = $this->findNeighbors($point, $epsilon); + $c = 0; + $this->clusters[$c] = []; - if (\count($neighbors) < $minPoints) { - $this->noisePoints[] = $point->name; - } elseif (!\in_array($point->name, $this->clusteredPoints)) { - $this->expandCluster($point->name, $neighbors, $c, $epsilon, $minPoints); - ++$c; - $this->clusters[$c] = []; - } - } - } + foreach ($this->points as $point) { + $neighbors = $this->findNeighbors($point, $epsilon); + + if (\count($neighbors) < $minPoints) { + $this->noisePoints[] = $point->name; + } elseif (!\in_array($point->name, $this->clusteredPoints)) { + $this->expandCluster($point, $neighbors, $c, $epsilon, $minPoints); + ++$c; + $this->clusters[$c] = []; + } + } + } } diff --git a/Algorithm/Clustering/Kmeans.php b/Algorithm/Clustering/Kmeans.php index 0b465e1b6..e11bc7e55 100755 --- a/Algorithm/Clustering/Kmeans.php +++ b/Algorithm/Clustering/Kmeans.php @@ -22,8 +22,8 @@ use phpOMS\Math\Topology\MetricsND; * @package phpOMS\Algorithm\Clustering * @license OMS License 2.0 * @link https://jingga.app - * @since 1.0.0 * @see ./clustering_overview.png + * @since 1.0.0 */ final class Kmeans { @@ -54,7 +54,7 @@ final class Kmeans /** * Constructor * - * @param null|\Closure $metric metric to use for the distance between two points + * @param null|\Closure $metric metric to use for the distance between two points * * @since 1.0.0 */ diff --git a/Algorithm/Clustering/MeanShift.php b/Algorithm/Clustering/MeanShift.php index 25550b317..a9f1a30f3 100644 --- a/Algorithm/Clustering/MeanShift.php +++ b/Algorithm/Clustering/MeanShift.php @@ -23,13 +23,35 @@ use phpOMS\Math\Topology\MetricsND; * @package phpOMS\Algorithm\Clustering * @license OMS License 2.0 * @link https://jingga.app - * @since 1.0.0 * @see ./clustering_overview.png + * @since 1.0.0 */ final class MeanShift { + /** + * Min distance for clustering + * + * As long as a point is further away as the min distance the shifting is performed + * + * @var float + * @since 1.0.0 + */ + public const MIN_DISTANCE = 0.001; + + /** + * Kernel function + * + * @var \Closure + * @since 1.0.0 + */ private \Closure $kernel; + /** + * Metric function + * + * @var \Closure + * @since 1.0.0 + */ private \Closure $metric; private array $points; @@ -60,13 +82,21 @@ final class MeanShift */ private $clusterCenters = []; - public const MIN_DISTANCE = 0.000001; - public const GROUP_DISTANCE_TOLERANCE = .1; + /** + * Max distance to cluster to be still considered part of cluster + * + * @var float + * @since 1.0.0 + */ + public float $groupDistanceTolerance = 0.1; /** * Constructor * - * @param null|\Closure $metric metric to use for the distance between two points + * Both the metric and kernel function need to be of the same dimension. + * + * @param null|\Closure $metric Metric to use for the distance between two points + * @param null|\Closure $kernel Kernel * * @since 1.0.0 */ @@ -84,10 +114,20 @@ final class MeanShift }; } + /** + * Generate the clusters of the points + * + * @param PointInterface[] $points Points to cluster + * @param array $bandwidth Bandwidth(s) + * + * @return void + * + * @since 1.0.0 + */ public function generateClusters(array $points, array $bandwidth) : void { $shiftPoints = $points; - $maxMinDist = 1; + $maxMinDist = 1; $stillShifting = \array_fill(0, \count($points), true); @@ -101,10 +141,10 @@ final class MeanShift continue; } - $pNew = $shiftPoints[$i]; + $pNew = $shiftPoints[$i]; $pNewStart = $pNew; - $pNew = $this->shiftPoint($pNew, $points, $bandwidth); - $dist = ($this->metric)($pNew, $pNewStart); + $pNew = $this->shiftPoint($pNew, $points, $bandwidth); + $dist = ($this->metric)($pNew, $pNewStart); if ($dist > $maxMinDist) { $maxMinDist = $dist; @@ -124,6 +164,17 @@ final class MeanShift $this->clusterCenters = $shiftPoints; } + /** + * Perform shift on a point + * + * @param PointInterface $point Point to shift + * @param PointInterface $points Array of all points + * @param array $bandwidth Bandwidth(s) + * + * @return PointInterface + * + * @since 1.0.0 + */ private function shiftPoint(PointInterface $point, array $points, array $bandwidth) : PointInterface { $scaleFactor = 0.0; @@ -131,7 +182,7 @@ final class MeanShift $shifted = clone $point; foreach ($points as $pTemp) { - $dist = ($this->metric)($point, $pTemp); + $dist = ($this->metric)($point, $pTemp); $weight = ($this->kernel)($dist, $bandwidth); foreach ($point->coordinates as $idx => $_) { @@ -152,6 +203,15 @@ final class MeanShift return $shifted; } + /** + * Group points together into clusters + * + * @param PointInterface[] $points Array of points to assign to groups + * + * @return array + * + * @since 1.0.0 + */ private function groupPoints(array $points) : array { $groupAssignment = []; @@ -163,11 +223,12 @@ final class MeanShift if ($nearestGroupIndex === -1) { // create new group - $groups[] = [$point]; + $groups[] = [$point]; $groupAssignment[] = $groupIndex; + ++$groupIndex; } else { - $groupAssignment[] = $nearestGroupIndex; + $groupAssignment[] = $nearestGroupIndex; $groups[$nearestGroupIndex][] = $point; } } @@ -175,16 +236,27 @@ final class MeanShift return $groupAssignment; } + /** + * Find the closest cluster/group of a point + * + * @param PointInterface $point Point to find the cluster for + * @param PointInterface[] $group Clusters + * + * @return int + * + * @since 1.0.0 + */ private function findNearestGroup(PointInterface $point, array $groups) : int { $nearestGroupIndex = -1; - $index = 0; + $index = 0; foreach ($groups as $group) { $distanceToGroup = $this->distanceToGroup($point, $group); - if ($distanceToGroup < self::GROUP_DISTANCE_TOLERANCE) { + if ($distanceToGroup < $this->groupDistanceTolerance) { $nearestGroupIndex = $index; + break; } @@ -194,6 +266,16 @@ final class MeanShift return $nearestGroupIndex; } + /** + * Find distance of point to best cluster/group + * + * @param PointInterface $point Point to find the cluster for + * @param PointInterface[] $group Clusters + * + * @return float Distance + * + * @since 1.0.0 + */ private function distanceToGroup(PointInterface $point, array $group) : float { $minDistance = \PHP_FLOAT_MAX; diff --git a/Algorithm/Clustering/Point.php b/Algorithm/Clustering/Point.php index f11ee27db..0157da8a6 100755 --- a/Algorithm/Clustering/Point.php +++ b/Algorithm/Clustering/Point.php @@ -86,6 +86,9 @@ class Point implements PointInterface $this->coordinates[$index] = $value; } + /** + * {@inheritdoc} + */ public function isEquals(PointInterface $point) : bool { return $this->name === $point->name && $this->coordinates === $point->coordinates; diff --git a/Algorithm/Clustering/PointInterface.php b/Algorithm/Clustering/PointInterface.php index d59123e1a..83d20e1d2 100755 --- a/Algorithm/Clustering/PointInterface.php +++ b/Algorithm/Clustering/PointInterface.php @@ -60,5 +60,14 @@ interface PointInterface */ public function setCoordinate(int $index, int | float $value) : void; + /** + * Check if two points are equal + * + * @param self $point Point to compare with + * + * @return bool + * + * @since 1.0.0 + */ public function isEquals(self $point) : bool; } diff --git a/Algorithm/Clustering/SpectralClustering.php b/Algorithm/Clustering/SpectralClustering.php index 46b863cc0..8ad8b6b2d 100644 --- a/Algorithm/Clustering/SpectralClustering.php +++ b/Algorithm/Clustering/SpectralClustering.php @@ -20,8 +20,8 @@ namespace phpOMS\Algorithm\Clustering; * @package phpOMS\Algorithm\Clustering * @license OMS License 2.0 * @link https://jingga.app - * @since 1.0.0 * @see ./clustering_overview.png + * @since 1.0.0 * * @todo Implement */ diff --git a/Algorithm/Clustering/Ward.php b/Algorithm/Clustering/Ward.php index 6babdd946..6791356a2 100644 --- a/Algorithm/Clustering/Ward.php +++ b/Algorithm/Clustering/Ward.php @@ -20,8 +20,8 @@ namespace phpOMS\Algorithm\Clustering; * @package phpOMS\Algorithm\Clustering * @license OMS License 2.0 * @link https://jingga.app - * @since 1.0.0 * @see ./clustering_overview.png + * @since 1.0.0 * * @todo Implement */ diff --git a/Algorithm/Frequency/Apriori.php b/Algorithm/Frequency/Apriori.php index c85da583d..3a524ca2b 100644 --- a/Algorithm/Frequency/Apriori.php +++ b/Algorithm/Frequency/Apriori.php @@ -15,7 +15,9 @@ declare(strict_types=1); namespace phpOMS\Algorithm\CoinMatching; /** - * Matching a value with a set of coins + * Apriori algorithm. + * + * The algorithm cheks how often a set exists in a given set of sets. * * @package phpOMS\Algorithm\CoinMatching * @license OMS License 2.0 @@ -24,7 +26,27 @@ namespace phpOMS\Algorithm\CoinMatching; */ final class Apriori { - private static function generateSubsets(array $arr) { + /** + * Constructor + * + * @since 1.0.0 + * @codeCoverageIgnore + */ + private function __construct() + { + } + + /** + * Generate all possible subsets + * + * @param array $arr Array of eleements + * + * @return array + * + * @since 1.0.0 + */ + private static function generateSubsets(array $arr) : array + { $subsets = [[]]; foreach ($arr as $element) { @@ -44,13 +66,15 @@ final class Apriori } /** - * $transactions = array( - array('milk', 'bread', 'eggs'), - array('milk', 'bread'), - array('milk', 'eggs'), - array('bread', 'eggs'), - array('milk') - ); + * Performs the apriori algorithm. + * + * The algorithm cheks how often a set exists in a given set of sets. + * + * @param array $sets Sets of a set (e.g. [[1,2,3,4], [1,2], [1]]) + * + * @return array + * + * @since 1.0.0 */ public static function apriori(array $sets) : array { diff --git a/Algorithm/Optimization/AntColonyOptimization.php b/Algorithm/Optimization/AntColonyOptimization.php index e69de29bb..e288c7dc2 100644 --- a/Algorithm/Optimization/AntColonyOptimization.php +++ b/Algorithm/Optimization/AntColonyOptimization.php @@ -0,0 +1,28 @@ + $population List of all elements with ther parameters (i.e. list of "objects" as arrays). + * The constraints are defined as array values. + * @param \Closure $fitness Fitness function calculates score/feasability of solution + * @param \Closure $mutate Mutation function to change the parameters of an "object" + * @param \Closure $crossover Crossover function to exchange parameter values between "objects". + * Sometimes single parameters can be exchanged but sometimes interdependencies exist between parameters which is why this function is required. + * @param int $generations Number of generations to create + * @param float $mutationRate Rate at which parameters are changed. + * How this is used depends on the mutate function. + * + * @return array{solutions:array, fitnesses:float[]} + * + * @since 1.0.0 + */ public static function optimize( array $population, \Closure $fitness, diff --git a/Algorithm/Optimization/HarmonySearch.php b/Algorithm/Optimization/HarmonySearch.php index e69de29bb..04380666c 100644 --- a/Algorithm/Optimization/HarmonySearch.php +++ b/Algorithm/Optimization/HarmonySearch.php @@ -0,0 +1,28 @@ + $currentGeneration, - 'costs' => $currentCost + 'costs' => $currentCost ]; } } diff --git a/Algorithm/Optimization/TabuSearch.php b/Algorithm/Optimization/TabuSearch.php index bfb3f799c..abd08481f 100644 --- a/Algorithm/Optimization/TabuSearch.php +++ b/Algorithm/Optimization/TabuSearch.php @@ -25,31 +25,65 @@ namespace phpOMS\Algorithm\Optimization; */ class TabuSearch { + /** + * Constructor + * + * @since 1.0.0 + * @codeCoverageIgnore + */ + private function __construct() + { + } + + /* // Define your fitness function here public static function fitness($solution) { // Calculate and return the fitness of the solution // This function should be tailored to your specific problem - return /* ... */; + return $solution; } // Define your neighborhood generation function here public static function generateNeighbor($currentSolution) { // Generate a neighboring solution based on the current solution // This function should be tailored to your specific problem - return /* ... */; + return $currentSolution; } + */ - // Define the Tabu Search algorithm - public static function optimize($initialSolution, \Closure $fitness, \Closure $neighbor, $tabuListSize, $maxIterations) { + /** + * Perform optimization + * + * @example See unit test for example use case + * + * @param array $initialSolution List of all elements with ther parameters (i.e. list of "objects" as arrays). + * The constraints are defined as array values. + * @param \Closure $fitness Fitness function calculates score/feasability of solution + * @param \Closure $neighbor Neighbor function to find a new solution/neighbor + * @param int $tabuListSize ???? + * @param int $iterations Number of iterations + * + * @return array + * + * @since 1.0.0 + */ + public static function optimize( + array $initialSolution, + \Closure $fitness, + \Closure $neighbor, + int $tabuListSize, + int $iterations + ) : array + { $currentSolution = $initialSolution; - $bestSolution = $currentSolution; - $bestFitness = \PHP_FLOAT_MIN; - $tabuList = []; + $bestSolution = $currentSolution; + $bestFitness = \PHP_FLOAT_MIN; + $tabuList = []; - for ($iteration = 0; $iteration < $maxIterations; ++$iteration) { + for ($i = 0; $i < $iterations; ++$i) { $neighbors = []; for ($i = 0; $i < $tabuListSize; ++$i) { - $neighbor = ($neighbor)($currentSolution); + $neighbor = ($neighbor)($currentSolution); $neighbors[] = $neighbor; } @@ -77,7 +111,7 @@ class TabuSearch if (($score = ($fitness)($bestNeighbor)) > $bestFitness) { $bestSolution = $bestNeighbor; - $bestFitness = $score; + $bestFitness = $score; } } diff --git a/Algorithm/Rating/BradleyTerry.php b/Algorithm/Rating/BradleyTerry.php index dcc995113..ed3ab4db1 100644 --- a/Algorithm/Rating/BradleyTerry.php +++ b/Algorithm/Rating/BradleyTerry.php @@ -25,14 +25,10 @@ namespace phpOMS\Algorithm\Rating; */ 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) /** + * Rate the strongest to the weakest team based on historic performances (wins/losses) + * + * The following example contains match results (matrix) of teams A-D facing each other (each point is a victory). * @example rating( * [ * 'A' => ['A' => 0, 'B' => 2, 'C' => 0, 'D' => 1], @@ -40,8 +36,15 @@ final class BradleyTerry * '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] + * 10 + * ) // [0.640, 1.043, 0.660, 2.270] -> D is strongest + * + * @param array[] $history Historic results + * @param int $iterations Iterations for estimation + * + * @return float[] Array of "strength" scores (highest = strongest) + * + * @since 1.0.0 */ public function rating(array $history, int $iterations = 20) : array { diff --git a/Algorithm/Rating/Elo.php b/Algorithm/Rating/Elo.php index c1d596a02..9c2a3bb3e 100644 --- a/Algorithm/Rating/Elo.php +++ b/Algorithm/Rating/Elo.php @@ -20,23 +20,52 @@ namespace phpOMS\Algorithm\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 + * @since 1.0.0 */ final class Elo { + /** + * ELO change rate + * + * @var int + * @since 1.0.0 + */ public int $K = 32; + /** + * Default elo to use for new players + * + * @var int + * @since 1.0.0 + */ public int $DEFAULT_ELO = 1500; + /** + * Lowest elo allowed + * + * @var int + * @since 1.0.0 + */ public int $MIN_ELO = 100; + /** + * Calculate the elo rating + * + * @param int $elo Current player elo + * @param int[] $oElo Current elo of all opponents + * @param int[] $s Match results against the opponents (1 = victor, 0 = loss, 0.5 = draw) + * + * @return array{elo:int} + * + * @since 1.0.0 + */ public function rating(int $elo, array $oElo, array $s) : array { $eloNew = $elo; foreach ($oElo as $idx => $o) { $expected = 1 / (1 + 10 ** (($o - $elo) / 400)); - $r = $this->K * ($s[$idx] - $expected); + $r = $this->K * ($s[$idx] - $expected); $eloNew += $r; } diff --git a/Algorithm/Rating/Glicko1.php b/Algorithm/Rating/Glicko1.php index 51e933a96..72368ff40 100644 --- a/Algorithm/Rating/Glicko1.php +++ b/Algorithm/Rating/Glicko1.php @@ -20,41 +20,104 @@ namespace phpOMS\Algorithm\Rating; * @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 + * @since 1.0.0 */ final class Glicko1 { - public const Q = 0.00575646273; // ln(10) / 400 + /** + * Helper constant + * + * @latex Q = ln(10) / 400 + * + * @var int + * @since 1.0.0 + */ + private const Q = 0.00575646273; + /** + * Default elo to use for new players + * + * @var int + * @since 1.0.0 + */ public int $DEFAULT_ELO = 1500; + /** + * Default rd to use for new players + * + * @var int + * @since 1.0.0 + */ public int $DEFAULT_RD = 350; + /** + * C (constant) for RD caclulation + * + * This is used to adjust the RD value based on the time from the last time a player played a match + * + * @latex RD = min\left(\sqrt{RD_0^2 + c^2t}, 350\right) + * + * @see calculateC(); + * + * @var int + * @since 1.0.0 + */ public float $DEFAULT_C = 34.6; + /** + * Lowest elo allowed + * + * @var int + * @since 1.0.0 + */ public int $MIN_ELO = 100; + /** + * Lowest rd allowed + * + * @example 50 means that the player rating is probably between -100 / +100 of the current rating + * + * @var int + * @since 1.0.0 + */ public int $MIN_RD = 50; + /** + * Calculate the C value. + * + * This is only necessary if you change the DEFAULT_RD, want a different rating period or have significantly different average RD values. + * + * @param int $ratingPeriods Time without matches until the RD returns to the default RD + * @param int $avgRD Average RD + * + * @return void + * + * @since 1.0.0 + */ + public function calculateC(int $ratingPeriods = 100, int $avgRD = 50) : void + { + $this->DEFAULT_C = \sqrt(($this->DEFAULT_RD ** 2 - $avgRD ** 2) / $ratingPeriods); + } + /** * 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 + * @param int $elo Current player "elo" + * @param int $rdOld Current player deviation (RD) + * @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 (RD) * * @return array{elo:int, rd:int} * * @since 1.0.0 */ public function rating( - int $eloOld = 1500, + int $elo = 1500, int $rdOld = 50, int $lastMatchDate = 0, int $matchDate = 0, @@ -64,8 +127,8 @@ final class Glicko1 ) : array { // Step 1: - $s = []; - $E = []; + $s = []; + $E = []; $gRD = []; $RD = \min( @@ -83,7 +146,7 @@ final class Glicko1 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)); + $E[] = 1 / (1 + \pow(10, $gRD_t * ($elo - $e) / -400)); } $d = 0; @@ -96,7 +159,7 @@ final class Glicko1 foreach ($E as $id => $_) { $r += $gRD[$id] * ($s[$id] - $E[$id]); } - $r = $eloOld + self::Q / (1 / ($RD * $RD) + 1 / $d2) * $r; + $r = $elo + self::Q / (1 / ($RD * $RD) + 1 / $d2) * $r; // Step 3: $RD_ = \sqrt(1 / (1 / ($RD * $RD) + 1 / $d2)); diff --git a/Algorithm/Rating/Glicko2.php b/Algorithm/Rating/Glicko2.php index ecc7bb2ea..30bab1952 100644 --- a/Algorithm/Rating/Glicko2.php +++ b/Algorithm/Rating/Glicko2.php @@ -22,31 +22,84 @@ use phpOMS\Math\Solver\Root\Bisection; * @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 + * @since 1.0.0 * * @todo: implement */ final class Glicko2 { + /** + * Constraint for the volatility over time (smaller = stronger constraint) + * + * @var float + * @since 1.0.0 + */ public float $tau = 0.5; + /** + * Default elo to use for new players + * + * @var int + * @since 1.0.0 + */ public int $DEFAULT_ELO = 1500; + /** + * Default rd to use for new players + * + * @var int + * @since 1.0.0 + */ public int $DEFAULT_RD = 350; + /** + * Valatility (sigma) + * + * Expected flactuation = how erratic is the player's performance + * + * @var float + * @since 1.0.0 + */ public float $DEFAULT_VOLATILITY = 0.06; + /** + * Lowest elo allowed + * + * @var int + * @since 1.0.0 + */ public int $MIN_ELO = 100; + /** + * Lowest rd allowed + * + * @example 50 means that the player rating is probably between -100 / +100 of the current rating + * + * @var int + * @since 1.0.0 + */ public int $MIN_RD = 50; /** + * Calcualte the glicko-2 elo + * * @example $glicko->elo(1500, 200, 0.06, [1,0,0], [1400,1550,1700], [30,100,300]) // 1464, 151, 0.059 + * + * @param int $elo Current player "elo" + * @param int $rdOld Current player deviation (RD) + * @param float $volOld Last match date used to calculate the time difference (can be days, months, ... depending on your match interval) + * @param float[] $s Match results (1 = victor, 0 = loss, 0.5 = draw) + * @param int[] $oElo Opponent "elo" + * @param int[] $oRd Opponent deviation (RD) + * + * @return array{elo:int, rd:int, vol:float} + * + * @since 1.0.0 */ public function rating( - int $eloOld = 1500, + int $elo = 1500, int $rdOld = 50, float $volOld = 0.06, array $s = [], @@ -58,7 +111,7 @@ final class Glicko2 // Step 0: $rdOld = $rdOld / 173.7178; - $eloOld = ($eloOld - $this->DEFAULT_ELO) / 173.7178; + $elo = ($elo - $this->DEFAULT_ELO) / 173.7178; foreach ($oElo as $idx => $value) { $oElo[$idx] = ($value - $this->DEFAULT_ELO) / 173.7178; @@ -76,7 +129,7 @@ final class Glicko2 $E = []; foreach ($oElo as $idx => $elo) { - $E[] = 1 / (1 + \exp(-$g[$idx] * ($eloOld - $elo))); + $E[] = 1 / (1 + \exp(-$g[$idx] * ($elo - $elo))); } $v = 0; @@ -98,12 +151,12 @@ final class Glicko2 - ($x - \log($volOld ** 2)) / ($tau ** 2); }; - $root = Bisection::bisection($fn, -100, 100, 1000); + $root = Bisection::root($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; + $r = $elo + $RD ** 2 * $tDelta; // Undo step 0: $RD = 173.7178 * $RD; @@ -111,7 +164,7 @@ final class Glicko2 return [ 'elo' => (int) \max($r, $this->MIN_ELO), - 'rd' => (int) \max($RD, $this->MIN_RD), + 'rd' => (int) \max($RD, $this->MIN_RD), 'vol' => $vol, ]; } diff --git a/Business/Recommendation/MemoryCF.php b/Business/Recommendation/MemoryCF.php index 72f0c719e..d34f22a27 100644 --- a/Business/Recommendation/MemoryCF.php +++ b/Business/Recommendation/MemoryCF.php @@ -28,18 +28,42 @@ use phpOMS\Math\Topology\MetricsND; * @package phpOMS\Business\Recommendation * @license OMS License 2.0 * @link https://jingga.app - * @since 1.0.0 * @see https://realpython.com/build-recommendation-engine-collaborative-filtering/ + * @since 1.0.0 */ final class MemoryCF { + /** + * All rankings + * + * @var array + * @since 1.0.0 + */ private array $rankings = []; - public function __construc(array $rankings) + /** + * Constructor. + * + * @param array $rankings Array of item ratings by users (or reverse to find users) + * + * @since 1.0.0 + */ + public function __construct(array $rankings) { $this->rankings = $this->normalizeRanking($rankings); } + /** + * Normalize all ratings. + * + * This is necessary because some users my give lower or higher ratings on average (bias). + * + * @param array $rankings Item ratings/rankings + * + * @return array + * + * @since 1.0.0 + */ private function normalizeRanking(array $rankings) : array { foreach ($rankings as $idx => $items) { @@ -53,7 +77,16 @@ final class MemoryCF return $rankings; } - // Used to find similar users + /** + * Euclidean distance between users + * + * @param array $ranking Rating to find the distance for + * @param array $rankings All ratings to find the distance to + * + * @return float[] + * + * @since 1.0.0 + */ public function euclideanDistance(array $ranking, array $rankings) : array { $distances = []; @@ -64,7 +97,16 @@ final class MemoryCF return $distances; } - // Used to find similar users + /** + * Cosine distance between users + * + * @param array $ranking Rating to find the distance for + * @param array $rankings All ratings to find the distance to + * + * @return float[] + * + * @since 1.0.0 + */ public function cosineDistance(array $ranking, array $rankings) : array { $distances = []; @@ -75,16 +117,28 @@ final class MemoryCF return $distances; } + /** + * Assign a item rank/rating based on the distance to other items + * + * @param string $itemId Id of the item to rank + * @param array $distances Distance to other users + * @param array $users All user ratings + * @param int $size Only consider the top n distances (best matches with other users) + * + * @return float Estimated item rank/rating based on similarity to other users + * + * @since 1.0.0 + */ private function weightedItemRank(string $itemId, array $distances, array $users, int $size) : float { - $rank = 0.0; + $rank = 0.0; $count = 0; foreach ($distances as $uId => $_) { if ($count >= $size) { break; } - if (!isset($user[$itemId])) { + if (!isset($users[$itemId])) { continue; } @@ -92,30 +146,43 @@ final class MemoryCF $rank += $users[$uId][$itemId]; } - return $rank / $count; + return $count === 0 ? 0.0 : $rank / $count; } - // This can be used to find items for a specific user (aka might be interested in) or to find users who might be interested in this item - // option 1 - find items - // ranking[itemId] = itemRank (how much does specific user like item) - // rankings[userId][itemId] = itemRank - // - // option 2 - find user - // ranking[userId] = itemRank (how much does user like specific item) - // rankings[itemId][userId] = itemRank - // option 1 searches for items, option 2 searches for users + /** + * Find potential items/users which are a good match for a user/item. + * + * The algorithm uses the ratings of a a user and tries to find other users who have similar rating behavior and then searches for high rated items that the user doesn't have yet. + * + * This can be used to find items for a specific user (aka might be interested in) or to find users who might be interested in this item + * + * option 1 - find items + * ranking[itemId] = itemRank (how much does specific user like item) + * rankings[userId][itemId] = itemRank + * + * option 2 - find user + * ranking[userId] = itemRank (how much does user like specific item) + * rankings[itemId][userId] = itemRank + * option 1 searches for items, option 2 searches for users + * + * @param array $ranking Array of item ratings (e.g. products, movies, ...) + * + * @return array + * + * @since 1.0.0 + */ public function bestMatch(array $ranking, int $size = 10) : array { - $ranking = $this->normalizeRanking([$ranking]); - $ranking = $ranking[0]; + $ranking = $this->normalizeRanking([$ranking]); + $ranking = $ranking[0]; $euclidean = $this->euclideanDistance($ranking, $this->rankings); - $cosine = $this->cosineDistance($ranking, $this->rankings); + $cosine = $this->cosineDistance($ranking, $this->rankings); \asort($euclidean); \asort($cosine); - $size = \min($size, \count($this->rankings)); + $size = \min($size, \count($this->rankings)); $matches = []; $distancePointer = \array_keys($euclidean); @@ -125,8 +192,9 @@ final class MemoryCF for ($i = 1; $i <= $size; ++$i) { $index = (int) ($i / 2) - 1; - $uId = $i % 2 === 1 ? $distancePointer[$index] : $anglePointer[$index]; + $uId = $i % 2 === 1 ? $distancePointer[$index] : $anglePointer[$index]; $distances = $i % 2 === 1 ? $euclidean : $cosine; + foreach ($this->rankings[$uId] as $iId => $_) { // Item is not already in dataset and not in historic dataset (we are only interested in new) if (isset($matches[$iId]) || isset($ranking[$iId])) { diff --git a/Business/Recommendation/ModelCF.php b/Business/Recommendation/ModelCF.php index a0f189bba..9f9cda6f2 100644 --- a/Business/Recommendation/ModelCF.php +++ b/Business/Recommendation/ModelCF.php @@ -22,8 +22,8 @@ use phpOMS\Math\Matrix\Matrix; * @package phpOMS\Business\Recommendation * @license OMS License 2.0 * @link https://jingga.app - * @since 1.0.0 * @see https://realpython.com/build-recommendation-engine-collaborative-filtering/ + * @since 1.0.0 */ final class ModelCF { @@ -37,12 +37,25 @@ final class ModelCF { } - // $user and $item can also be Vectors resulting in a individual evaluation - // e.g. the user matrix contains a user in every row, every column represents a score for a certain attribute - // the item matrix contains in every row a score for how much it belongs to a certain attribute. Each column represents an item. - // example: users columns define how much a user likes a certain movie genre and the item rows define how much this movie belongs to a certain genre. - // the multiplication gives a score of how much the user may like that movie. - // A segnificant amount of attributes are required to calculate a good match + /** + * Calculate the score of a user <-> item match. + * + * This function calculates how much a user likes a certain item (product, movie etc.) + * + * $user and $item can also be Vectors resulting in a individual evaluation + * e.g. the user matrix contains a user in every row, every column represents a score for a certain attribute + * the item matrix contains in every row a score for how much it belongs to a certain attribute. Each column represents an item. + * example: users columns define how much a user likes a certain movie genre and the item rows define how much this movie belongs to a certain genre. + * the multiplication gives a score of how much the user may like that movie. + * A segnificant amount of attributes are required to calculate a good match + * + * @param Matrix $users A mxa matrix where each "m" defines how much the user likes a certain attribute type and "a" defines different users + * @param Matrix $items A bxm matrix where each "b" defines a item and "m" defines how much it belongs to a certain attribute type + * + * @return array + * + * @since 1.0.0 + */ public static function score(Matrix $users, Matrix $items) : array { return $users->mult($items)->getMatrix(); diff --git a/DataStorage/Session/HttpSession.php b/DataStorage/Session/HttpSession.php index 0b14a84c3..15fa1b015 100755 --- a/DataStorage/Session/HttpSession.php +++ b/DataStorage/Session/HttpSession.php @@ -141,6 +141,18 @@ final class HttpSession implements SessionInterface UriFactory::setQuery('$CSRF', $csrf); /* @phpstan-ignore-line */ } + /** + * Populate the session from the request. + * + * This is only used when the session data is stored in the request itself (e.g. JWT) + * + * @param string $secret Secret to validate the request + * @param RequestAbstract $request Request + * + * @return void + * + * @since 1.0.0 + */ public function populateFromRequest(string $secret, RequestAbstract $request) : void { $authentication = $request->header->get('Authorization'); diff --git a/DataStorage/Session/JWT.php b/DataStorage/Session/JWT.php index 741206d8c..a11987303 100644 --- a/DataStorage/Session/JWT.php +++ b/DataStorage/Session/JWT.php @@ -35,9 +35,9 @@ final class JWT /** * Create JWT signature part * - * @param string $secret Secret (at least 256 bit) - * @var array{alg:string, typ:string} $header Header - * @var array{sub:string, ?uid:string, ?name:string, iat:string} $payload Payload + * @param string $secret Secret (at least 256 bit) + * @param array{alg:string, typ:string} $header Header + * @param array{sub:string, ?uid:string, ?name:string, iat:string} $payload Payload * * @return string hmac(Header64 . Payload64, secret) * @@ -60,11 +60,13 @@ final class JWT /** * Create JWT token * - * @param string $secret Secret (at least 256 bit) - * @var array{alg:string, typ:string} $header Header - * @var array{sub:string, ?uid:string, ?name:string, iat:string} $payload Payload + * @param string $secret Secret (at least 256 bit) + * @param array{alg:string, typ:string} $header Header + * @param array{sub:string, ?uid:string, ?name:string, iat:string} $payload Payload * * @return string Header64 . Payload64 . hmac(Header64 . Payload64, secret) + * + * @since 1.0.0 */ public static function createJWT(string $secret, array $header = [], array $payload = []) : string { @@ -75,6 +77,15 @@ final class JWT return $header64 . $payload64 . Base64Url::encode($signature); } + /** + * Get the header from the jwt string + * + * @param string $jwt JWT string + * + * @return array + * + * @since 1.0.0 + */ public static function getHeader(string $jwt) : array { $explode = \explode('.', $jwt); @@ -90,6 +101,15 @@ final class JWT } } + /** + * Get the payload from the jwt string + * + * @param string $jwt JWT string + * + * @return array + * + * @since 1.0.0 + */ public static function getPayload(string $jwt) : array { $explode = \explode('.', $jwt); @@ -112,6 +132,8 @@ final class JWT * @param string $jwt JWT token [Header64 . Payload64 . hmac(Header64 . Payload64, secret)] * * @return bool + * + * @since 1.0.0 */ public static function validateJWT(string $secret, string $jwt) : bool { diff --git a/Localization/ISO3166Trait.php b/Localization/ISO3166Trait.php index 6d8719c4a..d47f889f8 100644 --- a/Localization/ISO3166Trait.php +++ b/Localization/ISO3166Trait.php @@ -24,7 +24,16 @@ namespace phpOMS\Localization; */ trait ISO3166Trait { - public static function getBy2Code(string $code) + /** + * Get value by 2 code + * + * @param string $code 2-code + * + * @return mixed + * + * @since 1.0.0 + */ + public static function getBy2Code(string $code) : mixed { $code3 = ISO3166TwoEnum::getName($code); diff --git a/Localization/ISO639Trait.php b/Localization/ISO639Trait.php index 1c1860634..60fef73ec 100644 --- a/Localization/ISO639Trait.php +++ b/Localization/ISO639Trait.php @@ -24,7 +24,16 @@ namespace phpOMS\Localization; */ trait ISO639Trait { - public static function getBy2Code(string $code) + /** + * Get value by 2 code + * + * @param string $code 2-code + * + * @return mixed + * + * @since 1.0.0 + */ + public static function getBy2Code(string $code) : mixed { return self::getByName('_' . \strtoupper($code)); } diff --git a/Math/Functions/Functions.php b/Math/Functions/Functions.php index 88c4855a3..10f116510 100755 --- a/Math/Functions/Functions.php +++ b/Math/Functions/Functions.php @@ -243,6 +243,12 @@ final class Functions return \abs(self::mod($value - $start, $length)); } + /** + * Error function coefficients for approximation + * + * @var float[] + * @since 1.0.0 + */ private const ERF_COF = [ -1.3026537197817094, 6.4196979235649026e-1, 1.9476473204185836e-2,-9.561514786808631e-3,-9.46595344482036e-4, @@ -253,172 +259,99 @@ final class Functions -1.12708e-13,3.81e-16,7.106e-15,-1.523e-15,-9.4e-17,1.21e-16,-2.8e-17 ]; + /** + * Error function + * + * @param float $x X-Value + * + * @return float + * + * @since 1.0.0 + */ public static function getErf(float $x) : float { return $x > 0.0 ? 1.0 - self::erfccheb($x) : self::erfccheb(-$x) - 1.0; - } + } + /** + * Complementary error function + * + * @param float $x X-Value + * + * @return float + * + * @since 1.0.0 + */ public static function getErfc(float $x) : float { return $x > 0.0 ? self::erfccheb($x) : 2.0 - self::erfccheb(-$x); - } + } + /** + * Error function helper function + * + * @param float $z Z-Value + * + * @return float + * + * @throws \InvalidArgumentException + * + * @since 1.0.0 + */ private static function erfccheb(float $z) : float { - $d = 0.; + $d = 0.; $dd = 0.; $ncof = \count(self::ERF_COF); - if ($z < 0.) { + if ($z < 0.) { throw new \InvalidArgumentException("erfccheb requires nonnegative argument"); } - $t = 2. / (2. + $z); - $ty = 4. * $t - 2.; + $t = 2. / (2. + $z); + $ty = 4. * $t - 2.; - for ($j = $ncof - 1; $j > 0; --$j) { - $tmp = $d; - $d = $ty * $d - $dd + self::ERF_COF[$j]; - $dd = $tmp; - } + for ($j = $ncof - 1; $j > 0; --$j) { + $tmp = $d; + $d = $ty * $d - $dd + self::ERF_COF[$j]; + $dd = $tmp; + } - return $t * \exp(-$z * $z + 0.5*(self::ERF_COF[0] + $ty * $d) - $dd); - } + return $t * \exp(-$z * $z + 0.5*(self::ERF_COF[0] + $ty * $d) - $dd); + } + /** + * Inverse complementary error function + * + * @param float $p P-Value + * + * @return float + * + * @since 1.0.0 + */ public static function getInvErfc(float $p) : float { - if ($p >= 2.0) { + if ($p >= 2.0) { return -100.; } elseif ($p <= 0.0) { return 100.; } - $pp = ($p < 1.0) ? $p : 2. - $p; - $t = sqrt(-2. * \log($pp / 2.)); - $x = -0.70711 * ((2.30753 + $t * 0.27061)/(1. + $t * (0.99229 + $t * 0.04481)) - $t); + $pp = ($p < 1.0) ? $p : 2. - $p; + $t = sqrt(-2. * \log($pp / 2.)); + $x = -0.70711 * ((2.30753 + $t * 0.27061)/(1. + $t * (0.99229 + $t * 0.04481)) - $t); - for ($j = 0; $j < 2; ++$j) { - $err = self::getErfc($x) - $pp; - $x += $err / (1.12837916709551257 * \exp(-($x * $x)) - $x * $err); - } - - return ($p < 1.0? $x : -$x); - } - - /** - * Calculate the value of the error function (gauss error function) - * - * @param float $value Value - * - * @return float - * - * @see Sylvain Chevillard; HAL Id: ensl-00356709 - * @see https://hal-ens-lyon.archives-ouvertes.fr/ensl-00356709v3 - * - * @since 1.0.0 - */ - /* - public static function getErf(float $value) : float - { - if (\abs($value) > 2.2) { - return 1 - self::getErfc($value); + for ($j = 0; $j < 2; ++$j) { + $err = self::getErfc($x) - $pp; + $x += $err / (1.12837916709551257 * \exp(-($x * $x)) - $x * $err); } - $valueSquared = $value * $value; - $sum = $value; - $term = $value; - $i = 1; - - do { - $term *= $valueSquared / $i; - $sum -= $term / (2 * $i + 1); - - ++$i; - - $term *= $valueSquared / $i; - $sum += $term / (2 * $i + 1); - - ++$i; - } while ($sum !== 0.0 && \abs($term / $sum) > self::EPSILON); - - return 2 / \sqrt(\M_PI) * $sum; - } - */ - - /** - * Calculate the value of the complementary error fanction - * - * @param float $value Value - * - * @return float - * - * @see Sylvain Chevillard; HAL Id: ensl-00356709 - * @see https://hal-ens-lyon.archives-ouvertes.fr/ensl-00356709v3 - * - * @since 1.0.0 - */ - /* - public static function getErfc(float $value) : float - { - if (\abs($value) <= 2.2) { - return 1 - self::getErf($value); - } - - if ($value < 0.0) { - return 2 - self::getErfc(-$value); - } - - $a = $n = 1; - $b = $c = $value; - $d = ($value * $value) + 0.5; - $q1 = $q2 = $b / $d; - $t = 0; - - do { - $t = $a * $n + $b * $value; - $a = $b; - $b = $t; - $t = $c * $n + $d * $value; - $c = $d; - $d = $t; - $n += 0.5; - $q1 = $q2; - $q2 = $b / $d; - } while (\abs($q1 - $q2) / $q2 > self::EPSILON); - - return 1 / \sqrt(\M_PI) * \exp(-$value * $value) * $q2; - } - */ - - public static function getErfcInv(float $value) : float - { - if ($value >= 2) { - return \PHP_FLOAT_MIN; - } elseif ($value <= 0) { - return \PHP_FLOAT_MAX; - } elseif ($value === 1.0) { - return 0.0; - } - - if ($ge = ($value >= 1)) { - $value = 2 - $value; - } - - $t = \sqrt(-2 * \log($value / 2.0)); - $x = -0.70711 * ((2.30753 + $t * 0.27061) / (1. + $t * (0.99229 + $t * 0.04481)) - $t); - - $err = self::getErfc($x) - $value; - $x = $err / (1.12837916709551257 * \exp(-($x ** 2)) - $x * $err); - - $err = self::getErfc($x) - $value; - $x = $err / (1.12837916709551257 * \exp(-($x ** 2)) - $x * $err); - - return $ge ? -$x : $x; + return ($p < 1.0? $x : -$x); } /** diff --git a/Math/Matrix/Matrix.php b/Math/Matrix/Matrix.php index 12f21cb63..b0360a4e7 100755 --- a/Math/Matrix/Matrix.php +++ b/Math/Matrix/Matrix.php @@ -688,9 +688,19 @@ class Matrix implements \ArrayAccess, \Iterator public function det() : float { $L = new LUDecomposition($this); + return $L->det(); } + /** + * Dot product + * + * @param self $B Matrix + * + * @return self + * + * @since 1.0.0 + */ public function dot(self $B) : self { $value1 = $this->matrix; @@ -751,7 +761,16 @@ class Matrix implements \ArrayAccess, \Iterator return self::fromArray($result); } - public function sum(int $axis = -1) + /** + * Sum the elements in the matrix + * + * @param int $axis Axis (-1 -> all dimensions, 0 -> columns, 1 -> rows) + * + * @return int|float|self Returns int or float for axis -1 + * + * @since 1.0.0 + */ + public function sum(int $axis = -1) : int|float|self { if ($axis === -1) { $sum = 0; @@ -786,6 +805,13 @@ class Matrix implements \ArrayAccess, \Iterator return new self(); } + /** + * Is matrix a diagonal matrix + * + * @return bool + * + * @since 1.0.0 + */ public function isDiagonal() : bool { if ($this->m !== $this->n) { @@ -803,6 +829,17 @@ class Matrix implements \ArrayAccess, \Iterator return true; } + /** + * Calculate the power of a matrix + * + * @param int|float $exponent Exponent + * + * @return self + * + * @throws InvalidDimensionException + * + * @since 1.0.0 + */ public function pow(int | float $exponent) : self { if ($this->isDiagonal()) { @@ -839,6 +876,15 @@ class Matrix implements \ArrayAccess, \Iterator } } + /** + * Calculate e^M + * + * @param int $iterations Iterations for approximation + * + * @return self + * + * @since 1.0.0 + */ public function exp(int $iterations = 10) : self { if ($this->m !== $this->n) { diff --git a/Math/Matrix/Vector.php b/Math/Matrix/Vector.php index 25ecc014c..afae98a93 100755 --- a/Math/Matrix/Vector.php +++ b/Math/Matrix/Vector.php @@ -88,10 +88,19 @@ final class Vector extends Matrix return $this; } + /** + * Angle between two vectors + * + * @param self $v Vector + * + * @return float + * + * @since 1.0.0 + */ public function cosine(self $v) : float { $dotProduct = 0; - for ($i = 0; $i < \count($this->matrix); $i++) { + for ($i = 0; $i < $this->m; ++$i) { $dotProduct += $this->matrix[$i][0] * $v[$i]; } diff --git a/Math/Solver/Root/Bisection.php b/Math/Solver/Root/Bisection.php index cc57970b4..3746a15f7 100644 --- a/Math/Solver/Root/Bisection.php +++ b/Math/Solver/Root/Bisection.php @@ -15,20 +15,41 @@ declare(strict_types=1); namespace phpOMS\Math\Solver\Root; /** - * Basic math function evaluation. + * Find the root of a function. * * @package phpOMS\Math\Solver\Root * @license OMS License 2.0 * @link https://jingga.app * @since 1.0.0 - * - * @todo: implement */ final class Bisection { + /** + * Epsilon for float comparison. + * + * @var float + * @since 1.0.0 + */ public const EPSILON = 1e-6; - public static function bisection(Callable $func, float $a, float $b, $maxIterations = 100) { + /** + * Perform bisection to find the root of a function + * + * Iteratively searches for root between two points on the x-axis + * + * @param Callable $func Function defintion + * @param float $a Start value + * @param float $b End value + * @param int $maxIterations Maximum amount of iterations + * + * @throws \Exception + * + * @return float + * + * @since 1.0.0 + */ + public static function root(Callable $func, float $a, float $b, int $maxIterations = 100) : float + { if ($func($a) * $func($b) >= 0) { throw new \Exception("Function values at endpoints must have opposite signs."); } diff --git a/Math/Solver/Root/Illinois.php b/Math/Solver/Root/Illinois.php index fac0eeff5..c14caa650 100644 --- a/Math/Solver/Root/Illinois.php +++ b/Math/Solver/Root/Illinois.php @@ -15,27 +15,48 @@ declare(strict_types=1); namespace phpOMS\Math\Solver\Root; /** - * Basic math function evaluation. + * Find the root of a function. * * @package phpOMS\Math\Solver\Root * @license OMS License 2.0 * @link https://jingga.app * @since 1.0.0 - * - * @todo: implement */ final class Illinois { + /** + * Epsilon for float comparison. + * + * @var float + * @since 1.0.0 + */ public const EPSILON = 1e-6; - public static function bisection(Callable $func, float $a, float $b, $maxIterations = 100) { + /** + * Perform bisection to find the root of a function + * + * Iteratively searches for root between two points on the x-axis + * + * @param Callable $func Function defintion + * @param float $a Start value + * @param float $b End value + * @param int $maxIterations Maximum amount of iterations + * + * @throws \Exception + * + * @return float + * + * @since 1.0.0 + */ + public static function root(Callable $func, float $a, float $b, int $maxIterations = 100) : float + { if ($func($a) * $func($b) >= 0) { throw new \Exception("Function values at endpoints must have opposite signs."); } - $c = $b; + $c = $b; $iteration = 0; - $sign = 1; + $sign = 1; while (($y = \abs($func($c))) > self::EPSILON && $iteration < $maxIterations) { $fa = $func($a); diff --git a/Math/Solver/Root/RegulaFalsi.php b/Math/Solver/Root/RegulaFalsi.php index b987ceb6c..180646018 100644 --- a/Math/Solver/Root/RegulaFalsi.php +++ b/Math/Solver/Root/RegulaFalsi.php @@ -15,25 +15,46 @@ declare(strict_types=1); namespace phpOMS\Math\Solver\Root; /** - * Basic math function evaluation. + * Find the root of a function. * * @package phpOMS\Math\Solver\Root * @license OMS License 2.0 * @link https://jingga.app * @since 1.0.0 - * - * @todo: implement */ final class RegulaFalsi { + /** + * Epsilon for float comparison. + * + * @var float + * @since 1.0.0 + */ public const EPSILON = 1e-6; - public static function bisection(Callable $func, float $a, float $b, $maxIterations = 100) { + /** + * Perform bisection to find the root of a function + * + * Iteratively searches for root between two points on the x-axis + * + * @param Callable $func Function defintion + * @param float $a Start value + * @param float $b End value + * @param int $maxIterations Maximum amount of iterations + * + * @throws \Exception + * + * @return float + * + * @since 1.0.0 + */ + public static function root(Callable $func, float $a, float $b, int $maxIterations = 100) : float + { if ($func($a) * $func($b) >= 0) { throw new \Exception("Function values at endpoints must have opposite signs."); } - $c = $b; + $c = $b; $iteration = 0; while (($y = \abs($func($c))) > self::EPSILON && $iteration < $maxIterations) { diff --git a/Math/Stochastic/Distribution/NormalDistribution.php b/Math/Stochastic/Distribution/NormalDistribution.php index 7cfabb853..03ab4db70 100755 --- a/Math/Stochastic/Distribution/NormalDistribution.php +++ b/Math/Stochastic/Distribution/NormalDistribution.php @@ -104,7 +104,17 @@ final class NormalDistribution return 0.5 * Functions::getErf(-($x - $mu) / ($sig * \sqrt(2))); } - // AKA Quantile function and sometimes PPF + /** + * Inverse cumulative distribution function / AKA Quantile function / PPF + * + * @param float $x Value x + * @param float $mu Mean + * @param float $sig Sigma + * + * @return float + * + * @since 1.0.0 + */ public static function getICdf(float $x, float $mu, float $sig) : float { return $mu - $sig * \sqrt(2) * Functions::getInvErfc(2 * $x); diff --git a/Math/Topology/Kernel2D.php b/Math/Topology/Kernel2D.php index 58af4c02b..e790ff056 100644 --- a/Math/Topology/Kernel2D.php +++ b/Math/Topology/Kernel2D.php @@ -16,7 +16,7 @@ declare(strict_types=1); namespace phpOMS\Math\Topology; /** - * Metrics. + * Kernels. * * @package phpOMS\Math\Topology * @license OMS License 2.0 @@ -35,6 +35,16 @@ final class Kernels2D { } + /** + * Uniform kernel. + * + * @param float $distance Distance + * @param float $bandwidth Bandwidth + * + * @return float + * + * @since 1.0.0 + */ public static function uniformKernel(float $distance, float $bandwidth) : float { return \abs($distance) <= $bandwidth / 2 @@ -42,6 +52,16 @@ final class Kernels2D : 0.0; } + /** + * Triangular kernel. + * + * @param float $distance Distance + * @param float $bandwidth Bandwidth + * + * @return float + * + * @since 1.0.0 + */ public static function triangularKernel(float $distance, float $bandwidth) : float { return \abs($distance) <= $bandwidth / 2 @@ -49,6 +69,16 @@ final class Kernels2D : 0.0; } + /** + * Epanechnikov kernel. + * + * @param float $distance Distance + * @param float $bandwidth Bandwidth + * + * @return float + * + * @since 1.0.0 + */ public static function epanechnikovKernel(float $distance, float $bandwidth) : float { if (\abs($distance) <= $bandwidth) { @@ -60,6 +90,16 @@ final class Kernels2D } } + /** + * Quartic kernel. + * + * @param float $distance Distance + * @param float $bandwidth Bandwidth + * + * @return float + * + * @since 1.0.0 + */ public static function quarticKernel(float $distance, float $bandwidth) : float { if (\abs($distance) <= $bandwidth) { @@ -71,6 +111,16 @@ final class Kernels2D } } + /** + * Triweight kernel. + * + * @param float $distance Distance + * @param float $bandwidth Bandwidth + * + * @return float + * + * @since 1.0.0 + */ public static function triweightKernel(float $distance, float $bandwidth) : float { if (\abs($distance) <= $bandwidth) { @@ -82,6 +132,16 @@ final class Kernels2D } } + /** + * Tricube kernel. + * + * @param float $distance Distance + * @param float $bandwidth Bandwidth + * + * @return float + * + * @since 1.0.0 + */ public static function tricubeKernel(float $distance, float $bandwidth) : float { if (\abs($distance) <= $bandwidth) { @@ -93,11 +153,31 @@ final class Kernels2D } } + /** + * Gaussian kernel. + * + * @param float $distance Distance + * @param float $bandwidth Bandwidth + * + * @return float + * + * @since 1.0.0 + */ public static function gaussianKernel(float $distance, float $bandwidth) : float { return \exp(-($distance * $distance) / (2 * $bandwidth * $bandwidth)) / ($bandwidth * \sqrt(2 * \M_PI)); } + /** + * Cosine kernel. + * + * @param float $distance Distance + * @param float $bandwidth Bandwidth + * + * @return float + * + * @since 1.0.0 + */ public static function cosineKernel(float $distance, float $bandwidth) : float { return \abs($distance) <= $bandwidth @@ -105,6 +185,16 @@ final class Kernels2D : 0.0; } + /** + * Logistic kernel. + * + * @param float $distance Distance + * @param float $bandwidth Bandwidth + * + * @return float + * + * @since 1.0.0 + */ public static function logisticKernel(float $distance, float $bandwidth) : float { return 1 / (\exp($distance / $bandwidth) + 2 + \exp(-$distance / $bandwidth)); diff --git a/Math/Topology/KernelsND.php b/Math/Topology/KernelsND.php index 6d4b70312..7bacf6d20 100644 --- a/Math/Topology/KernelsND.php +++ b/Math/Topology/KernelsND.php @@ -19,7 +19,7 @@ use phpOMS\Math\Matrix\IdentityMatrix; use phpOMS\Math\Matrix\Matrix; /** - * Metrics. + * Kernels. * * @package phpOMS\Math\Topology * @license OMS License 2.0 @@ -38,6 +38,16 @@ final class KernelsND { } + /** + * Gaussian kernel + * + * @param array $distances Distances + * @param array $bandwidths Bandwidths + * + * @return array + * + * @since 1.0.0 + */ public static function gaussianKernel(array $distances, array $bandwidths) : array { $dim = \count($bandwidths); diff --git a/Math/Topology/MetricsND.php b/Math/Topology/MetricsND.php index 8081ecd09..32e90bbe2 100755 --- a/Math/Topology/MetricsND.php +++ b/Math/Topology/MetricsND.php @@ -93,8 +93,24 @@ final class MetricsND return \sqrt($dist); } + /** + * Cosine metric. + * + * @param array $a n-D array + * @param array $b n-D array + * + * @return float + * + * @throws InvalidDimensionException + * + * @since 1.0.0 + */ public static function cosine(array $a, array $b) : float { + if (\count($a) !== \count($b)) { + throw new InvalidDimensionException(\count($a) . 'x' . \count($b)); + } + $dotProduct = 0; for ($i = 0; $i < \count($a); $i++) { $dotProduct += $a[$i] * $b[$i]; diff --git a/System/File/FileUtils.php b/System/File/FileUtils.php index d4038cc18..e2993b861 100755 --- a/System/File/FileUtils.php +++ b/System/File/FileUtils.php @@ -281,6 +281,15 @@ final class FileUtils return !\preg_match('#^[a-z][a-z\d+.-]*://#i', $path); } + /** + * Turn a string into a safe file name (sanitize a string) + * + * @param string $name String to sanitize for file name usage + * + * @return string + * + * @since 1.0.0 + */ public static function makeSafeFileName(string $name) : string { $name = \preg_replace("/[^A-Za-z0-9\-_.]/", '_', $name); diff --git a/System/MimeType.php b/System/MimeType.php index 348bdb1cf..27b9a49e6 100755 --- a/System/MimeType.php +++ b/System/MimeType.php @@ -2028,6 +2028,16 @@ abstract class MimeType extends Enum } } + /** + * Get the file extension from a mime + * + * @param string $mime Mime + * + * @return null|string + * + * @since 1.0.0 + * @todo continue implementation + */ public static function mimeToExtension(string $mime) : ?string { switch($mime) { diff --git a/Utils/Formatter/HtmlFormatter.php b/Utils/Formatter/HtmlFormatter.php index 473cf42e8..199fa91e0 100644 --- a/Utils/Formatter/HtmlFormatter.php +++ b/Utils/Formatter/HtmlFormatter.php @@ -15,7 +15,7 @@ declare(strict_types=1); namespace phpOMS\Utils\Formatter; /** - * Gray encoding class + * Html code formatter * * @package phpOMS\Utils\Formatter * @license OMS License 2.0 @@ -24,6 +24,15 @@ namespace phpOMS\Utils\Formatter; */ class HtmlFormatter { + /** + * Format html code + * + * @param string $text Html code + * + * @return string + * + * @since 1.0.0 + */ public static function format(string $text) : string { $dom = new \DOMDocument();