format fixes

This commit is contained in:
Dennis Eichhorn 2023-08-30 11:44:08 +00:00
parent 74e1684ad0
commit 447efca623
42 changed files with 1235 additions and 326 deletions

View File

@ -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
*/

View File

@ -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
*/

View File

@ -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
*/

View File

@ -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<int, array{x:float, y:float}>
* @since 1.0.0
*/
private array $clusters = [];
/**
* Convex hull of all clusters
*
* @var array<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<float[]>
* @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] = [];
}
}
}
}

View File

@ -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
*/

View File

@ -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<int|float> $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<int|float> $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;

View File

@ -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;

View File

@ -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;
}

View File

@ -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
*/

View File

@ -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
*/

View File

@ -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<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<array> $sets Sets of a set (e.g. [[1,2,3,4], [1,2], [1]])
*
* @return array<array>
*
* @since 1.0.0
*/
public static function apriori(array $sets) : array
{

View File

@ -0,0 +1,28 @@
<?php
/**
* Jingga
*
* PHP Version 8.1
*
* @package phpOMS\Algorithm\Optimization
* @copyright Dennis Eichhorn
* @license OMS License 2.0
* @version 1.0.0
* @link https://jingga.app
*
*/
declare(strict_types=1);
namespace phpOMS\Algorithm\Optimization;
/**
* Perform ant colony algorithm.
*
* @package phpOMS\Algorithm\Optimization
* @license OMS License 2.0
* @link https://jingga.app
* @since 1.0.0
*/
class AntColonyOptimization
{
}

View File

@ -0,0 +1,28 @@
<?php
/**
* Jingga
*
* PHP Version 8.1
*
* @package phpOMS\Algorithm\Optimization
* @copyright Dennis Eichhorn
* @license OMS License 2.0
* @version 1.0.0
* @link https://jingga.app
*
*/
declare(strict_types=1);
namespace phpOMS\Algorithm\Optimization;
/**
* Perform bees algorithm.
*
* @package phpOMS\Algorithm\Optimization
* @license OMS License 2.0
* @link https://jingga.app
* @since 1.0.0
*/
class BeesAlgorithm
{
}

View File

@ -0,0 +1,28 @@
<?php
/**
* Jingga
*
* PHP Version 8.1
*
* @package phpOMS\Algorithm\Optimization
* @copyright Dennis Eichhorn
* @license OMS License 2.0
* @version 1.0.0
* @link https://jingga.app
*
*/
declare(strict_types=1);
namespace phpOMS\Algorithm\Optimization;
/**
* Perform firefly algorithm.
*
* @package phpOMS\Algorithm\Optimization
* @license OMS License 2.0
* @link https://jingga.app
* @since 1.0.0
*/
class FireflyAlgorithm
{
}

View File

@ -25,6 +25,17 @@ namespace phpOMS\Algorithm\Optimization;
*/
class GeneticOptimization
{
/**
* Constructor
*
* @since 1.0.0
* @codeCoverageIgnore
*/
private function __construct()
{
}
/*
// Fitness function (may require to pass solution space as \Closure variable)
// E.g.
// highest value of some sorts (e.g. profit)
@ -36,7 +47,6 @@ class GeneticOptimization
return $x;
}
// Mutation
public static function mutate($parameters, $mutationRate)
{
for ($i = 0; $i < \count($parameters); $i++) {
@ -64,9 +74,27 @@ class GeneticOptimization
return [$child1, $child2];
}
*/
// the fitness and mutation functions may need to be able to create the object to optimize and/or validate
// @todo maybe a crossover function must be provided to find and create the crossover (sometimes there are dependencies between parameters)
/**
* Perform optimization
*
* @example See unit test for example use case
*
* @param array<array> $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,

View File

@ -0,0 +1,28 @@
<?php
/**
* Jingga
*
* PHP Version 8.1
*
* @package phpOMS\Algorithm\Optimization
* @copyright Dennis Eichhorn
* @license OMS License 2.0
* @version 1.0.0
* @link https://jingga.app
*
*/
declare(strict_types=1);
namespace phpOMS\Algorithm\Optimization;
/**
* Perform harmony search algorithm.
*
* @package phpOMS\Algorithm\Optimization
* @license OMS License 2.0
* @link https://jingga.app
* @since 1.0.0
*/
class HarmonySearch
{
}

View File

@ -0,0 +1,28 @@
<?php
/**
* Jingga
*
* PHP Version 8.1
*
* @package phpOMS\Algorithm\Optimization
* @copyright Dennis Eichhorn
* @license OMS License 2.0
* @version 1.0.0
* @link https://jingga.app
*
*/
declare(strict_types=1);
namespace phpOMS\Algorithm\Optimization;
/**
* Perform intelligent water drops algorithm.
*
* @package phpOMS\Algorithm\Optimization
* @license OMS License 2.0
* @link https://jingga.app
* @since 1.0.0
*/
class IntelligentWaterDrops
{
}

View File

@ -25,6 +25,17 @@ namespace phpOMS\Algorithm\Optimization;
*/
class SimulatedAnnealing
{
/**
* Constructor
*
* @since 1.0.0
* @codeCoverageIgnore
*/
private function __construct()
{
}
/*
public static function costFunction($x)
{
return $x;
@ -44,24 +55,43 @@ class SimulatedAnnealing
return $newGeneration;
}
*/
// Simulated Annealing algorithm
// @todo allow to create a solution space (currently all soluctions need to be in space)
// @todo: currently only replacing generations, not altering them
/**
* Perform optimization
*
* @example See unit test for example use case
*
* @param array $space List of all elements with ther parameters (i.e. list of "objects" as arrays).
* The constraints are defined as array values.
* @param int $initialTemperature Starting temperature
* @param \Closure $costFunction Fitness function calculates score/feasability of solution
* @param \Closure $neighbor Neighbor function to find a new solution/neighbor
* @param float $coolingRate Rate at which cooling takes place
* @param int $iterations Number of iterations
*
* @return array{solutions:array, costs:float[]}
*
* @since 1.0.0
*/
function optimize(
array $space,
int $initialTemperature,
\Closure $costFunction,
\Closure $neighbor,
$coolingRate = 0.98,
$numIterations = 1000
) {
$parameterCount = \count($space);
float $coolingRate = 0.98,
int $iterations = 1000
) : array
{
$parameterCount = \count($space);
$currentGeneration = \reset($space);
$currentCost = ($costFunction)($currentGeneration);
for ($i = 0; $i < $numIterations; $i++) {
for ($i = 0; $i < $iterations; ++$i) {
$newGeneration = ($neighbor)($currentGeneration, $parameterCount);
$newCost = ($costFunction)($newGeneration);
@ -78,7 +108,7 @@ class SimulatedAnnealing
return [
'solutions' => $currentGeneration,
'costs' => $currentCost
'costs' => $currentCost
];
}
}

View File

@ -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;
}
}

View File

@ -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
{

View File

@ -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;
}

View File

@ -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));

View File

@ -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,
];
}

View File

@ -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<array>
* @since 1.0.0
*/
private array $rankings = [];
public function __construc(array $rankings)
/**
* Constructor.
*
* @param array<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<array> $rankings Item ratings/rankings
*
* @return array<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<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<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<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])) {

View File

@ -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();

View File

@ -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');

View File

@ -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
{

View File

@ -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);

View File

@ -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));
}

View File

@ -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);
}
/**

View File

@ -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) {

View File

@ -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];
}

View File

@ -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.");
}

View File

@ -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);

View File

@ -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) {

View File

@ -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);

View File

@ -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));

View File

@ -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<float|int> $distances Distances
* @param array<float|int> $bandwidths Bandwidths
*
* @return array
*
* @since 1.0.0
*/
public static function gaussianKernel(array $distances, array $bandwidths) : array
{
$dim = \count($bandwidths);

View File

@ -93,8 +93,24 @@ final class MetricsND
return \sqrt($dist);
}
/**
* Cosine metric.
*
* @param array<int|string, int|float> $a n-D array
* @param array<int|string, int|float> $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];

View File

@ -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);

View File

@ -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) {

View File

@ -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();