mirror of
https://github.com/Karaka-Management/phpOMS.git
synced 2026-01-11 17:58:41 +00:00
update
This commit is contained in:
parent
c4fc99ed39
commit
74e1684ad0
30
Algorithm/Clustering/AffinityPropagation.php
Normal file
30
Algorithm/Clustering/AffinityPropagation.php
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.1
|
||||
*
|
||||
* @package phpOMS\Algorithm\Clustering
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Algorithm\Clustering;
|
||||
|
||||
/**
|
||||
* Clustering points
|
||||
*
|
||||
* @package phpOMS\Algorithm\Clustering
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
* @see ./clustering_overview.png
|
||||
*
|
||||
* @todo Implement
|
||||
*/
|
||||
final class AffinityPropagation
|
||||
{
|
||||
}
|
||||
30
Algorithm/Clustering/AgglomerativeClustering.php
Normal file
30
Algorithm/Clustering/AgglomerativeClustering.php
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.1
|
||||
*
|
||||
* @package phpOMS\Algorithm\Clustering
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Algorithm\Clustering;
|
||||
|
||||
/**
|
||||
* Clustering points
|
||||
*
|
||||
* @package phpOMS\Algorithm\Clustering
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
* @see ./clustering_overview.png
|
||||
*
|
||||
* @todo Implement
|
||||
*/
|
||||
final class AgglomerativeClustering
|
||||
{
|
||||
}
|
||||
30
Algorithm/Clustering/Birch.php
Normal file
30
Algorithm/Clustering/Birch.php
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.1
|
||||
*
|
||||
* @package phpOMS\Algorithm\Clustering
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Algorithm\Clustering;
|
||||
|
||||
/**
|
||||
* Clustering points
|
||||
*
|
||||
* @package phpOMS\Algorithm\Clustering
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
* @see ./clustering_overview.png
|
||||
*
|
||||
* @todo Implement
|
||||
*/
|
||||
final class Birch
|
||||
{
|
||||
}
|
||||
220
Algorithm/Clustering/DBSCAN.php
Normal file
220
Algorithm/Clustering/DBSCAN.php
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.1
|
||||
*
|
||||
* @package phpOMS\Algorithm\Clustering
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Algorithm\Clustering;
|
||||
|
||||
use phpOMS\Math\Geometry\ConvexHull\MonotoneChain;
|
||||
use phpOMS\Math\Geometry\Shape\D2\Polygon;
|
||||
use phpOMS\Math\Topology\MetricsND;
|
||||
|
||||
/**
|
||||
* Clustering points
|
||||
*
|
||||
* @package phpOMS\Algorithm\Clustering
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
* @see ./clustering_overview.png
|
||||
*
|
||||
* @todo Expand to n dimensions
|
||||
*/
|
||||
final class DBSCAN
|
||||
{
|
||||
/**
|
||||
* Epsilon for float comparison.
|
||||
*
|
||||
* @var float
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public const EPSILON = 4.88e-04;
|
||||
|
||||
/**
|
||||
* Metric to calculate the distance between two points
|
||||
*
|
||||
* @var \Closure
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private \Closure $metric;
|
||||
|
||||
/**
|
||||
* Points outside of any cluster
|
||||
*
|
||||
* @var PointInterface[]
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private array $noisePoints = [];
|
||||
|
||||
/**
|
||||
* All points
|
||||
*
|
||||
* @var PointInterface[]
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private array $points = [];
|
||||
|
||||
/**
|
||||
* Clusters
|
||||
*
|
||||
* Array of clusters containing point ids
|
||||
*
|
||||
* @var array
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private array $clusters = [];
|
||||
|
||||
private array $convexHulls = [];
|
||||
|
||||
/**
|
||||
* Cluster points
|
||||
*
|
||||
* Points in clusters (helper to avoid looping the cluster array)
|
||||
*
|
||||
* @var array
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private array $clusteredPoints = [];
|
||||
|
||||
private array $distanceMatrix = [];
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param null|\Closure $metric metric to use for the distance between two points
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function __construct(\Closure $metric = null)
|
||||
{
|
||||
$this->metric = $metric ?? function (PointInterface $a, PointInterface $b) {
|
||||
$aCoordinates = $a->coordinates;
|
||||
$bCoordinates = $b->coordinates;
|
||||
|
||||
return MetricsND::euclidean($aCoordinates, $bCoordinates);
|
||||
};
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
$nPoint = next($neighbors);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $neighbors;
|
||||
}
|
||||
|
||||
private function generateDistanceMatrix(array $points) : array
|
||||
{
|
||||
$distances = [];
|
||||
foreach ($points as $point) {
|
||||
$distances[$point->name] = [];
|
||||
foreach ($points as $point2) {
|
||||
$distances[$point->name][$point2->name] = ($this->metric)($point, $point2);
|
||||
}
|
||||
}
|
||||
|
||||
return $distances;
|
||||
}
|
||||
|
||||
public function cluster(PointInterface $point) : int
|
||||
{
|
||||
if ($this->convexHulls === []) {
|
||||
foreach ($this->clusters as $c => $cluster) {
|
||||
$points = [];
|
||||
foreach ($cluster as $p) {
|
||||
$points[] = [
|
||||
'x' => \reset($p->coordinates),
|
||||
'y' => \end($p->coordinates),
|
||||
];
|
||||
}
|
||||
|
||||
$this->convexHulls[$c] = MonotoneChain::createConvexHull($points);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($this->convexHulls as $c => $hull) {
|
||||
if (Polygon::isPointInPolygon(
|
||||
[
|
||||
'x' => \reset($point->coordinates),
|
||||
'y' => \end($point->coordinates)
|
||||
],
|
||||
$hull
|
||||
) <= 0
|
||||
) {
|
||||
return $c;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
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] = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -14,6 +14,8 @@ declare(strict_types=1);
|
|||
|
||||
namespace phpOMS\Algorithm\Clustering;
|
||||
|
||||
use phpOMS\Math\Topology\MetricsND;
|
||||
|
||||
/**
|
||||
* Clustering points
|
||||
*
|
||||
|
|
@ -21,6 +23,7 @@ namespace phpOMS\Algorithm\Clustering;
|
|||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
* @see ./clustering_overview.png
|
||||
*/
|
||||
final class Kmeans
|
||||
{
|
||||
|
|
@ -35,10 +38,10 @@ final class Kmeans
|
|||
/**
|
||||
* Metric to calculate the distance between two points
|
||||
*
|
||||
* @var Callable
|
||||
* @var \Closure
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private Callable $metric;
|
||||
private \Closure $metric;
|
||||
|
||||
/**
|
||||
* Points of the cluster centers
|
||||
|
|
@ -51,29 +54,20 @@ final class Kmeans
|
|||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param PointInterface[] $points Points to cluster
|
||||
* @param int<0, max> $clusters Amount of clusters
|
||||
* @param null|Callable $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
|
||||
*/
|
||||
public function __construct(array $points, int $clusters, Callable $metric = null)
|
||||
public function __construct(\Closure $metric = null)
|
||||
{
|
||||
$this->metric = $metric ?? function (PointInterface $a, PointInterface $b) {
|
||||
$aCoordinates = $a->getCoordinates();
|
||||
$bCoordinates = $b->getCoordinates();
|
||||
$aCoordinates = $a->coordinates;
|
||||
$bCoordinates = $b->coordinates;
|
||||
|
||||
$n = \count($aCoordinates);
|
||||
$sum = 0;
|
||||
|
||||
for ($i = 0; $i < $n; ++$i) {
|
||||
$sum = ($aCoordinates[$i] - $bCoordinates[$i]) * ($aCoordinates[$i] - $bCoordinates[$i]);
|
||||
}
|
||||
|
||||
return $sum;
|
||||
return MetricsND::euclidean($aCoordinates, $bCoordinates);
|
||||
};
|
||||
|
||||
$this->generateClusters($points, $clusters);
|
||||
//$this->generateClusters($points, $clusters);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -81,7 +75,7 @@ final class Kmeans
|
|||
*
|
||||
* @param PointInterface $point Point to find the cluster for
|
||||
*
|
||||
* @return null|PointInterface
|
||||
* @return null|PointInterface Cluster center point
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
|
@ -122,11 +116,11 @@ final class Kmeans
|
|||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private function generateClusters(array $points, int $clusters) : void
|
||||
public function generateClusters(array $points, int $clusters) : void
|
||||
{
|
||||
$n = \count($points);
|
||||
$clusterCenters = $this->kpp($points, $clusters);
|
||||
$coordinates = \count($points[0]->getCoordinates());
|
||||
$coordinates = \count($points[0]->coordinates);
|
||||
|
||||
while (true) {
|
||||
foreach ($clusterCenters as $center) {
|
||||
|
|
|
|||
227
Algorithm/Clustering/MeanShift.php
Normal file
227
Algorithm/Clustering/MeanShift.php
Normal file
|
|
@ -0,0 +1,227 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.1
|
||||
*
|
||||
* @package phpOMS\Algorithm\Clustering
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Algorithm\Clustering;
|
||||
|
||||
use phpOMS\Math\Topology\KernelsND;
|
||||
use phpOMS\Math\Topology\MetricsND;
|
||||
|
||||
/**
|
||||
* Clustering points
|
||||
*
|
||||
* @package phpOMS\Algorithm\Clustering
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
* @see ./clustering_overview.png
|
||||
*/
|
||||
final class MeanShift
|
||||
{
|
||||
private \Closure $kernel;
|
||||
|
||||
private \Closure $metric;
|
||||
|
||||
private array $points;
|
||||
|
||||
/**
|
||||
* Points outside of any cluster
|
||||
*
|
||||
* @var PointInterface[]
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private array $noisePoints = [];
|
||||
|
||||
/**
|
||||
* Cluster points
|
||||
*
|
||||
* Points in clusters (helper to avoid looping the cluster array)
|
||||
*
|
||||
* @var array
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private array $clusters = [];
|
||||
|
||||
/**
|
||||
* Points of the cluster centers
|
||||
*
|
||||
* @var PointInterface[]
|
||||
* @since 1.0.0
|
||||
*/
|
||||
private $clusterCenters = [];
|
||||
|
||||
public const MIN_DISTANCE = 0.000001;
|
||||
public const GROUP_DISTANCE_TOLERANCE = .1;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param null|\Closure $metric metric to use for the distance between two points
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function __construct(\Closure $metric = null, \Closure $kernel = null)
|
||||
{
|
||||
$this->metric = $metric ?? function (PointInterface $a, PointInterface $b) {
|
||||
$aCoordinates = $a->coordinates;
|
||||
$bCoordinates = $b->coordinates;
|
||||
|
||||
return MetricsND::euclidean($aCoordinates, $bCoordinates);
|
||||
};
|
||||
|
||||
$this->kernel = $kernel ?? function (array $distances, array $bandwidths) {
|
||||
return KernelsND::gaussianKernel($distances, $bandwidths);
|
||||
};
|
||||
}
|
||||
|
||||
public function generateClusters(array $points, array $bandwidth) : void
|
||||
{
|
||||
$shiftPoints = $points;
|
||||
$maxMinDist = 1;
|
||||
|
||||
$stillShifting = \array_fill(0, \count($points), true);
|
||||
|
||||
$pointLength = \count($shiftPoints);
|
||||
|
||||
while ($maxMinDist > self::MIN_DISTANCE) {
|
||||
$maxMinDist = 0;
|
||||
|
||||
for ($i = 0; $i < $pointLength; ++$i) {
|
||||
if (!$stillShifting[$i]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$pNew = $shiftPoints[$i];
|
||||
$pNewStart = $pNew;
|
||||
$pNew = $this->shiftPoint($pNew, $points, $bandwidth);
|
||||
$dist = ($this->metric)($pNew, $pNewStart);
|
||||
|
||||
if ($dist > $maxMinDist) {
|
||||
$maxMinDist = $dist;
|
||||
}
|
||||
|
||||
if ($dist < self::MIN_DISTANCE) {
|
||||
$stillShifting[$i] = false;
|
||||
}
|
||||
|
||||
$shiftPoints[$i] = $pNew;
|
||||
}
|
||||
}
|
||||
|
||||
// @todo create an array of noisePoints like in the DBSCAN. That array can be empty or not depending on the bandwidth defined
|
||||
|
||||
$this->clusters = $this->groupPoints($shiftPoints);
|
||||
$this->clusterCenters = $shiftPoints;
|
||||
}
|
||||
|
||||
private function shiftPoint(PointInterface $point, array $points, array $bandwidth) : PointInterface
|
||||
{
|
||||
$scaleFactor = 0.0;
|
||||
|
||||
$shifted = clone $point;
|
||||
|
||||
foreach ($points as $pTemp) {
|
||||
$dist = ($this->metric)($point, $pTemp);
|
||||
$weight = ($this->kernel)($dist, $bandwidth);
|
||||
|
||||
foreach ($point->coordinates as $idx => $_) {
|
||||
if (!isset($shifted->coordinates[$idx])) {
|
||||
$shifted->coordinates[$idx] = 0;
|
||||
}
|
||||
|
||||
$shifted->coordinates[$idx] += $pTemp->coordinates[$idx] * $weight;
|
||||
}
|
||||
|
||||
$scaleFactor += $weight;
|
||||
}
|
||||
|
||||
foreach ($shifted->coordinates as $idx => $_) {
|
||||
$shifted->coordinates[$idx] /= $scaleFactor;
|
||||
}
|
||||
|
||||
return $shifted;
|
||||
}
|
||||
|
||||
private function groupPoints(array $points) : array
|
||||
{
|
||||
$groupAssignment = [];
|
||||
$groups = [];
|
||||
$groupIndex = 0;
|
||||
|
||||
foreach ($points as $point) {
|
||||
$nearestGroupIndex = $this->findNearestGroup($point, $groups);
|
||||
|
||||
if ($nearestGroupIndex === -1) {
|
||||
// create new group
|
||||
$groups[] = [$point];
|
||||
$groupAssignment[] = $groupIndex;
|
||||
++$groupIndex;
|
||||
} else {
|
||||
$groupAssignment[] = $nearestGroupIndex;
|
||||
$groups[$nearestGroupIndex][] = $point;
|
||||
}
|
||||
}
|
||||
|
||||
return $groupAssignment;
|
||||
}
|
||||
|
||||
private function findNearestGroup(PointInterface $point, array $groups) : int
|
||||
{
|
||||
$nearestGroupIndex = -1;
|
||||
$index = 0;
|
||||
|
||||
foreach ($groups as $group) {
|
||||
$distanceToGroup = $this->distanceToGroup($point, $group);
|
||||
|
||||
if ($distanceToGroup < self::GROUP_DISTANCE_TOLERANCE) {
|
||||
$nearestGroupIndex = $index;
|
||||
break;
|
||||
}
|
||||
|
||||
++$index;
|
||||
}
|
||||
|
||||
return $nearestGroupIndex;
|
||||
}
|
||||
|
||||
private function distanceToGroup(PointInterface $point, array $group) : float
|
||||
{
|
||||
$minDistance = \PHP_FLOAT_MAX;
|
||||
|
||||
foreach ($group as $pt) {
|
||||
$dist = ($this->metric)($point, $pt);
|
||||
|
||||
if ($dist < $minDistance) {
|
||||
$minDistance = $dist;
|
||||
}
|
||||
}
|
||||
|
||||
return $minDistance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the cluster for a point
|
||||
*
|
||||
* @param PointInterface $point Point to find the cluster for
|
||||
*
|
||||
* @return null|PointInterface Cluster center point
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public function cluster(PointInterface $point) : ?PointInterface
|
||||
{
|
||||
$clusterId = $this->findNearestGroup($point, $this->clusters);
|
||||
|
||||
return $this->clusterCenters[$clusterId] ?? null;
|
||||
}
|
||||
}
|
||||
|
|
@ -30,7 +30,7 @@ class Point implements PointInterface
|
|||
* @var array<int, int|float>
|
||||
* @sicne 1.0.0
|
||||
*/
|
||||
private array $coordinates = [];
|
||||
public array $coordinates = [];
|
||||
|
||||
/**
|
||||
* Group or cluster this point belongs to
|
||||
|
|
@ -85,4 +85,9 @@ class Point implements PointInterface
|
|||
{
|
||||
$this->coordinates[$index] = $value;
|
||||
}
|
||||
|
||||
public function isEquals(PointInterface $point) : bool
|
||||
{
|
||||
return $this->name === $point->name && $this->coordinates === $point->coordinates;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,4 +59,6 @@ interface PointInterface
|
|||
* @since 1.0.0
|
||||
*/
|
||||
public function setCoordinate(int $index, int | float $value) : void;
|
||||
|
||||
public function isEquals(self $point) : bool;
|
||||
}
|
||||
|
|
|
|||
30
Algorithm/Clustering/SpectralClustering.php
Normal file
30
Algorithm/Clustering/SpectralClustering.php
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.1
|
||||
*
|
||||
* @package phpOMS\Algorithm\Clustering
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Algorithm\Clustering;
|
||||
|
||||
/**
|
||||
* Clustering points
|
||||
*
|
||||
* @package phpOMS\Algorithm\Clustering
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
* @see ./clustering_overview.png
|
||||
*
|
||||
* @todo Implement
|
||||
*/
|
||||
final class SpectralClustering
|
||||
{
|
||||
}
|
||||
30
Algorithm/Clustering/Ward.php
Normal file
30
Algorithm/Clustering/Ward.php
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.1
|
||||
*
|
||||
* @package phpOMS\Algorithm\Clustering
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Algorithm\Clustering;
|
||||
|
||||
/**
|
||||
* Clustering points
|
||||
*
|
||||
* @package phpOMS\Algorithm\Clustering
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
* @see ./clustering_overview.png
|
||||
*
|
||||
* @todo Implement
|
||||
*/
|
||||
final class Ward
|
||||
{
|
||||
}
|
||||
BIN
Algorithm/Clustering/clustering_overview.png
Normal file
BIN
Algorithm/Clustering/clustering_overview.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 710 KiB |
94
Algorithm/Frequency/Apriori.php
Normal file
94
Algorithm/Frequency/Apriori.php
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.1
|
||||
*
|
||||
* @package phpOMS\Algorithm\CoinMatching
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Algorithm\CoinMatching;
|
||||
|
||||
/**
|
||||
* Matching a value with a set of coins
|
||||
*
|
||||
* @package phpOMS\Algorithm\CoinMatching
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
final class Apriori
|
||||
{
|
||||
private static function generateSubsets(array $arr) {
|
||||
$subsets = [[]];
|
||||
|
||||
foreach ($arr as $element) {
|
||||
$newSubsets = [];
|
||||
|
||||
foreach ($subsets as $subset) {
|
||||
$newSubsets[] = $subset;
|
||||
$newSubsets[] = \array_merge($subset, [$element]);
|
||||
}
|
||||
|
||||
$subsets = $newSubsets;
|
||||
}
|
||||
|
||||
unset($subsets[0]);
|
||||
|
||||
return $subsets;
|
||||
}
|
||||
|
||||
/**
|
||||
* $transactions = array(
|
||||
array('milk', 'bread', 'eggs'),
|
||||
array('milk', 'bread'),
|
||||
array('milk', 'eggs'),
|
||||
array('bread', 'eggs'),
|
||||
array('milk')
|
||||
);
|
||||
*/
|
||||
public static function apriori(array $sets) : array
|
||||
{
|
||||
// Unique single items
|
||||
$totalSet = [];
|
||||
foreach ($sets as &$s) {
|
||||
\sort($s);
|
||||
|
||||
foreach ($s as $item) {
|
||||
$totalSet[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
$totalSet = \array_unique($totalSet);
|
||||
\sort($totalSet);
|
||||
|
||||
// Combinations of items
|
||||
$combinations = self::generateSubsets($totalSet);
|
||||
|
||||
// Table
|
||||
$table = [];
|
||||
foreach ($combinations as &$c) {
|
||||
\sort($c);
|
||||
$table[\implode(':', $c)] = 0;
|
||||
}
|
||||
|
||||
foreach ($combinations as $combination) {
|
||||
foreach ($sets as $set) {
|
||||
foreach ($combination as $item) {
|
||||
if (!\in_array($item, $set)) {
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
|
||||
++$table[\implode(':', $combination)];
|
||||
}
|
||||
}
|
||||
|
||||
return $table;
|
||||
}
|
||||
}
|
||||
0
Algorithm/Graph/MarkovChain.php
Normal file
0
Algorithm/Graph/MarkovChain.php
Normal file
34
Algorithm/JobScheduling/v2/Dependency/IdleIntervalType.php
Normal file
34
Algorithm/JobScheduling/v2/Dependency/IdleIntervalType.php
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.1
|
||||
*
|
||||
* @package phpOMS\Scheduling\Dependency
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Scheduling\Dependency;
|
||||
|
||||
/**
|
||||
* Machine type.
|
||||
*
|
||||
* @package phpOMS\Scheduling\Dependency
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class IdleIntervalType
|
||||
{
|
||||
public const ACTIVE_TIME = 1; // every x hours of activity
|
||||
|
||||
public const JOB_TIME = 2; // every x jobs
|
||||
|
||||
public const FIXED_TIME = 3; // datetime
|
||||
|
||||
public const GENERAL_TIME = 4; // every x hours
|
||||
}
|
||||
38
Algorithm/JobScheduling/v2/Dependency/IdleTime.php
Normal file
38
Algorithm/JobScheduling/v2/Dependency/IdleTime.php
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.1
|
||||
*
|
||||
* @package phpOMS\Scheduling\Dependency
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Scheduling\Dependency;
|
||||
|
||||
/**
|
||||
* Idle time.
|
||||
*
|
||||
* @package phpOMS\Scheduling\Dependency
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class IdleTime
|
||||
{
|
||||
public int $id = 0;
|
||||
|
||||
public int $type = 0; // setup, shutdown, cleaning, maintenance, general, ...
|
||||
|
||||
public int $intervalType = IdleIntervalType::ACTIVE_TIME;
|
||||
|
||||
public int $interval = 0;
|
||||
|
||||
public int $duration = 0; // in seconds
|
||||
|
||||
|
||||
}
|
||||
73
Algorithm/JobScheduling/v2/Dependency/JobStep.php
Normal file
73
Algorithm/JobScheduling/v2/Dependency/JobStep.php
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.1
|
||||
*
|
||||
* @package phpOMS\Scheduling\Dependency
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Scheduling\Dependency;
|
||||
|
||||
/**
|
||||
* Job step.
|
||||
*
|
||||
* @package phpOMS\Scheduling\Dependency
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class JobStep
|
||||
{
|
||||
public int $id = 0;
|
||||
|
||||
public int $order = 0;
|
||||
|
||||
public int $l11n = 0;
|
||||
|
||||
public int $machineType = 0;
|
||||
|
||||
public bool $machineParallelization = false;
|
||||
|
||||
public array $machines = [];
|
||||
|
||||
public int $workerType = 0;
|
||||
|
||||
public array $workerQualifications = []; // qualifications needed
|
||||
|
||||
public bool $workerParallelization = false;
|
||||
|
||||
public array $workers = [];
|
||||
|
||||
public int $material = 0;
|
||||
|
||||
public int $materialQuantity = 0;
|
||||
|
||||
public int $duration = 0; // in seconds
|
||||
|
||||
public int $maxHoldTime = -1; // minutes it can be halted if necessary (-1 infinite, 0 not at all)
|
||||
|
||||
public int $maxHoldAfterCompletion = -1; // minutes the next processing step can be postponed (-1 infinite, 0 not at all)
|
||||
|
||||
private int $realDuration = 0;
|
||||
|
||||
// depending on job completions
|
||||
private array $jobDependencies = [];
|
||||
|
||||
public bool $shouldBeParallel = false;
|
||||
|
||||
/**
|
||||
* Duration
|
||||
* + machine type/machine specific times (e.g. setup time etc.)
|
||||
* + machine-job specific times (e.g. setup time for this job which could be different from the general machine setup time)
|
||||
*/
|
||||
public function calculateDuration() : void
|
||||
{
|
||||
$this->realDuration = $this->duration;
|
||||
}
|
||||
}
|
||||
34
Algorithm/JobScheduling/v2/Dependency/Machine.php
Normal file
34
Algorithm/JobScheduling/v2/Dependency/Machine.php
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.1
|
||||
*
|
||||
* @package phpOMS\Scheduling\Dependency
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Scheduling\Dependency;
|
||||
|
||||
/**
|
||||
* Machine.
|
||||
*
|
||||
* @package phpOMS\Scheduling\Dependency
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class Machine
|
||||
{
|
||||
public int $id = 0;
|
||||
|
||||
public MachineType $type;
|
||||
|
||||
public array $idle = [];
|
||||
|
||||
|
||||
}
|
||||
35
Algorithm/JobScheduling/v2/Dependency/MachineType.php
Normal file
35
Algorithm/JobScheduling/v2/Dependency/MachineType.php
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.1
|
||||
*
|
||||
* @package phpOMS\Scheduling\Dependency
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Scheduling\Dependency;
|
||||
|
||||
/**
|
||||
* Machine type.
|
||||
*
|
||||
* @package phpOMS\Scheduling\Dependency
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class MachineType
|
||||
{
|
||||
public int $id = 0;
|
||||
|
||||
public array $idle = [];
|
||||
|
||||
public array $qualifications = []; // qualifications needed
|
||||
|
||||
// array of arrays, where each operator type requires certain qualifications
|
||||
public array $workerTypes = [];
|
||||
}
|
||||
0
Algorithm/JobScheduling/v2/Dependency/Material.php
Normal file
0
Algorithm/JobScheduling/v2/Dependency/Material.php
Normal file
34
Algorithm/JobScheduling/v2/Dependency/Worker.php
Normal file
34
Algorithm/JobScheduling/v2/Dependency/Worker.php
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.1
|
||||
*
|
||||
* @package phpOMS\Scheduling\Dependency
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Scheduling\Dependency;
|
||||
|
||||
/**
|
||||
* Worker.
|
||||
*
|
||||
* @package phpOMS\Scheduling\Dependency
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class Worker
|
||||
{
|
||||
public int $id = 0;
|
||||
|
||||
public int $type = 0;
|
||||
|
||||
public array $idle = [];
|
||||
|
||||
public array $qualifications = [];
|
||||
}
|
||||
32
Algorithm/JobScheduling/v2/Dependency/WorkerType.php
Normal file
32
Algorithm/JobScheduling/v2/Dependency/WorkerType.php
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.1
|
||||
*
|
||||
* @package phpOMS\Scheduling\Dependency
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Scheduling\Dependency;
|
||||
|
||||
/**
|
||||
* Worker.
|
||||
*
|
||||
* @package phpOMS\Scheduling\Dependency
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class WorkerType
|
||||
{
|
||||
public int $id = 0;
|
||||
|
||||
public array $idle = [];
|
||||
|
||||
public array $qualifications = [];
|
||||
}
|
||||
56
Algorithm/JobScheduling/v2/Job.php
Normal file
56
Algorithm/JobScheduling/v2/Job.php
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.1
|
||||
*
|
||||
* @package phpOMS\Scheduling
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Scheduling;
|
||||
|
||||
/**
|
||||
* Job.
|
||||
*
|
||||
* @package phpOMS\Scheduling
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class Job
|
||||
{
|
||||
public int $id = 0;
|
||||
|
||||
public int $executionTime = 0;
|
||||
|
||||
public float $priority = 0.0;
|
||||
|
||||
public float $value = 0.0;
|
||||
|
||||
public float $cost = 0.0;
|
||||
|
||||
/** How many iterations has this job been on hold in the queue */
|
||||
public int $onhold = 0;
|
||||
|
||||
/** How many iterations has this job been in process in the queue */
|
||||
public int $inprocessing = 0;
|
||||
|
||||
public \DateTime $deadline;
|
||||
|
||||
public array $steps = [];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->deadline = new \DateTime('now');
|
||||
}
|
||||
|
||||
public function getProfit()
|
||||
{
|
||||
return $this->value - $this->cost;
|
||||
}
|
||||
}
|
||||
128
Algorithm/JobScheduling/v2/Notes.md
Normal file
128
Algorithm/JobScheduling/v2/Notes.md
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
# Notes
|
||||
|
||||
## Job / Item
|
||||
|
||||
1. output item
|
||||
2. output quantity
|
||||
3. output scale factor
|
||||
4. instruction manuals []
|
||||
5. steps []
|
||||
|
||||
### Data
|
||||
|
||||
For single item, for this specific job (quantity plays a role) and for the current state
|
||||
|
||||
1. Work time planned/actual
|
||||
1.1. Per worker type
|
||||
1.2. Total
|
||||
2. Machine time planned/actual
|
||||
2.1. Per machine type
|
||||
2.2. Total
|
||||
3. Total duration planned/actual (is NOT work time + machine time)
|
||||
4. Machines types required incl. quantity
|
||||
5. Worker types required incl. quantity
|
||||
6. Material costs
|
||||
7. Worker costs
|
||||
7.1. Per worker type
|
||||
7.2. Total
|
||||
8. Machine costs
|
||||
8.1. Per machine type
|
||||
8.2. Total
|
||||
9. Progress status in %
|
||||
10. Progress type (time based, step based, manual)
|
||||
11. Value planned/actual
|
||||
11. Costs planned/actual
|
||||
12. Current step
|
||||
|
||||
## Steps
|
||||
|
||||
1. Setup machine
|
||||
1.1. worker types required []
|
||||
1.1.1. qualifications required by worker type []
|
||||
1.1.2. defined after algorithm: workers []
|
||||
1.1.2.1. worker specific qualifications available []
|
||||
1.2. amount of workers per type required
|
||||
1.3. worker scale factor (0 = no scaling, 1 = 100% scaling)
|
||||
1.4. machine types required []
|
||||
1.4.1. qualifications required by machine type []
|
||||
1.4.2. min capacity
|
||||
1.4.3. max capacity
|
||||
1.4.4. defined after algorithm: machines []
|
||||
1.4.4.1. machine specific qualifications required by machine type []
|
||||
1.4.4.2. machine specific min capacity
|
||||
1.4.4.3. machine specific max capacity
|
||||
1.5. amount of machines per type required
|
||||
1.6. machine scale factor (0 = no scaling, 1 = 100% scaling)
|
||||
1.7. worker / machine correlation (1 = equal scaling required, > 1 = more workers required per machine scale, < 1 = less workers required per machine scale (e.g. 1.5 -> 150% additional worker required if machines are scaled by 100%, 0.8 -> 80% additional worker required if machines are scaled by 100%))
|
||||
1.8. worker duration
|
||||
1.8.1. planned
|
||||
1.8.1. current/actual
|
||||
1.9. machine duration
|
||||
1.9.1. planned
|
||||
1.9.1. current/actual
|
||||
1.10. total duration
|
||||
1.10.1. planned
|
||||
1.10.1. current/actual
|
||||
1.11. duration scale factor (1 = duration equally scaled as machine/worker scaling, > 1 = longer duration with scaling, < 1 = shorter duration with scaling (e.g. 1.1 -> 110% additional duration if scaled by 100%, 0.9 -> 90 % additional duration if scaled by 100%)). The scale factor is max(worker scale, machine scale);
|
||||
1.12. depends on steps []
|
||||
1.13. try to parallelize? (planned/actual)
|
||||
1.14. material required []
|
||||
1.14.1. material id
|
||||
1.14.2. planned quantity
|
||||
1.14.2. actual quantity
|
||||
1.15. instruction checklist []
|
||||
1.16. hold time during
|
||||
1.16. hold time until next stip
|
||||
|
||||
2. Insert material 1
|
||||
3. Insert material 2
|
||||
4. Mix material
|
||||
5. Quality control
|
||||
6. Average correction
|
||||
7. Insert material 3
|
||||
8. Insert material 4
|
||||
9. Mix material
|
||||
10. Quality control
|
||||
11. Average correction
|
||||
12. Fill into large bindings
|
||||
13. Fill into smaller bindings
|
||||
14. Quality control
|
||||
15. Packaging
|
||||
|
||||
## Algorithm
|
||||
|
||||
1. Try to manufacture in one go (no large breaks in between)
|
||||
2. Try to parallelize (minimize time needed for production)
|
||||
3. Match deadline (if no deadline available go to "find earliest possible deadline")
|
||||
3.1. Priorize close or early to deadline finish (settings dependant)
|
||||
3.2. If not possilbe re-adjust pending production
|
||||
3.2.1. Focus on (value, cost, ...) (settings dependant)
|
||||
3.2.2. If not possible re-adjust ongoing production
|
||||
3.2.2.1. Focus on (value, cost, ...) (settings dependant)
|
||||
3.2.2.2. If not possible find earliest possible deadline
|
||||
|
||||
Constraints / To consider
|
||||
|
||||
1. Deadline (maybe not defined)
|
||||
2. Machines
|
||||
2.1. Available
|
||||
2.2.1. Other jobs
|
||||
2.2.2. General maintenance cleaning
|
||||
2.2.3. Unforseable maintenance
|
||||
2.2. Scalability by a factor
|
||||
3. Worker
|
||||
2.2. Available
|
||||
2.2.1. Other jobs
|
||||
2.2.2. General maintenance cleaning
|
||||
2.2.3. Vacation/sick
|
||||
2.2. Qualification
|
||||
2.3. Scalability by a factor
|
||||
4. Job variance (multiple corrections required)
|
||||
5. Material
|
||||
4.1. Available
|
||||
4.2. Delivery time
|
||||
6. Parallelizability
|
||||
7. Stock space
|
||||
8. Putting job steps on hold
|
||||
9. max/min capacities
|
||||
10. Scaling factors
|
||||
46
Algorithm/JobScheduling/v2/PriorityMode.php
Normal file
46
Algorithm/JobScheduling/v2/PriorityMode.php
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.1
|
||||
*
|
||||
* @package phpOMS\Scheduling
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Scheduling;
|
||||
|
||||
use phpOMS\Stdlib\Base\Enum;
|
||||
|
||||
/**
|
||||
* Priority type enum.
|
||||
*
|
||||
* Defines the different priorities in which elements from the queue can be extracted.
|
||||
*
|
||||
* @package phpOMS\Scheduling
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
abstract class PriorityMode extends Enum
|
||||
{
|
||||
public const FIFO = 1; // First in first out
|
||||
|
||||
public const LIFO = 2; // Last in first out
|
||||
|
||||
public const PRIORITY = 4;
|
||||
|
||||
public const VALUE = 8;
|
||||
|
||||
public const COST = 16;
|
||||
|
||||
public const PROFIT = 32;
|
||||
|
||||
public const HOLD = 64; // Longest on hold
|
||||
|
||||
public const EARLIEST_DEADLINE = 128; // EDF
|
||||
}
|
||||
149
Algorithm/JobScheduling/v2/ScheduleQueue.php
Normal file
149
Algorithm/JobScheduling/v2/ScheduleQueue.php
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.1
|
||||
*
|
||||
* @package phpOMS\Scheduling
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Scheduling;
|
||||
|
||||
/**
|
||||
* Scheduler.
|
||||
*
|
||||
* @package phpOMS\Scheduling
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
final class ScheduleQueue
|
||||
{
|
||||
public array $queue = [];
|
||||
|
||||
public function get(int $size = 1, int $type = PriorityMode::FIFO) : array
|
||||
{
|
||||
$jobs = [];
|
||||
$keys = \array_keys($this->queue);
|
||||
|
||||
switch ($type) {
|
||||
case PriorityMode::FIFO:
|
||||
for ($i = 0; $i < $size; ++$i) {
|
||||
$jobs[$i] = $this->queue[$keys[$i]];
|
||||
}
|
||||
|
||||
break;
|
||||
case PriorityMode::LIFO:
|
||||
for ($i = \count($this->queue) - $size - 1; $i < $size; ++$i) {
|
||||
$jobs[$i] = $this->queue[$keys[$i]];
|
||||
}
|
||||
|
||||
break;
|
||||
case PriorityMode::PRIORITY:
|
||||
$queue = $this->queue;
|
||||
\uasort($queue, function (Job $a, Job $b) {
|
||||
return $a->priority <=> $b->priority;
|
||||
});
|
||||
|
||||
$jobs = \array_slice($queue, 0, $size, true);
|
||||
|
||||
break;
|
||||
case PriorityMode::VALUE:
|
||||
$queue = $this->queue;
|
||||
\uasort($queue, function (Job $a, Job $b) {
|
||||
return $b->value <=> $a->value;
|
||||
});
|
||||
|
||||
$jobs = \array_slice($queue, 0, $size, true);
|
||||
|
||||
break;
|
||||
case PriorityMode::COST:
|
||||
$queue = $this->queue;
|
||||
\uasort($queue, function (Job $a, Job $b) {
|
||||
return $a->cost <=> $b->cost;
|
||||
});
|
||||
|
||||
$jobs = \array_slice($queue, 0, $size, true);
|
||||
|
||||
break;
|
||||
case PriorityMode::PROFIT:
|
||||
$queue = $this->queue;
|
||||
\uasort($queue, function (Job $a, Job $b) {
|
||||
return $b->getProfit() <=> $a->getProfit();
|
||||
});
|
||||
|
||||
$jobs = \array_slice($queue, 0, $size, true);
|
||||
|
||||
break;
|
||||
case PriorityMode::HOLD:
|
||||
$queue = $this->queue;
|
||||
\uasort($queue, function (Job $a, Job $b) {
|
||||
return $b->onhold <=> $a->onhold;
|
||||
});
|
||||
|
||||
$jobs = \array_slice($queue, 0, $size, true);
|
||||
|
||||
break;
|
||||
case PriorityMode::EARLIEST_DEADLINE:
|
||||
$queue = $this->queue;
|
||||
\uasort($queue, function (Job $a, Job $b) {
|
||||
return $a->deadline->getTimestamp() <=> $b->deadline->getTimestamp();
|
||||
});
|
||||
|
||||
$jobs = \array_slice($queue, 0, $size, true);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return $jobs;
|
||||
}
|
||||
|
||||
public function insert(int $id, Job $job) : void
|
||||
{
|
||||
$this->queue[$id] = $job;
|
||||
}
|
||||
|
||||
public function pop(int $size = 1, int $type = PriorityMode::FIFO) : array
|
||||
{
|
||||
$jobs = $this->get($size, $type);
|
||||
foreach ($jobs as $id => $_) {
|
||||
unset($this->queue[$id]);
|
||||
}
|
||||
|
||||
return $jobs;
|
||||
}
|
||||
|
||||
public function bumpHold(int $id = 0) : void
|
||||
{
|
||||
if ($id === 0) {
|
||||
foreach ($this->queue as $job) {
|
||||
++$job->onhold;
|
||||
}
|
||||
} else {
|
||||
++$this->queue[$id]->onhold;
|
||||
}
|
||||
}
|
||||
|
||||
public function adjustPriority(int $id = 0, float $priority = 0.1) : void
|
||||
{
|
||||
if ($id === 0) {
|
||||
foreach ($this->queue as $job) {
|
||||
$job->priority += $priority;
|
||||
}
|
||||
} else {
|
||||
$this->queue[$id]->priority += $priority;
|
||||
}
|
||||
}
|
||||
|
||||
public function remove(string $id) : void
|
||||
{
|
||||
unset($this->queue[$id]);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
0
Algorithm/Optimization/AntColonyOptimization.php
Normal file
0
Algorithm/Optimization/AntColonyOptimization.php
Normal file
0
Algorithm/Optimization/BeesAlgorithm.php
Normal file
0
Algorithm/Optimization/BeesAlgorithm.php
Normal file
0
Algorithm/Optimization/FireflyAlgorithm.php
Normal file
0
Algorithm/Optimization/FireflyAlgorithm.php
Normal file
130
Algorithm/Optimization/GeneticOptimization.php
Normal file
130
Algorithm/Optimization/GeneticOptimization.php
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
<?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 genetic algorithm (GA).
|
||||
*
|
||||
* @package phpOMS\Algorithm\Optimization
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class GeneticOptimization
|
||||
{
|
||||
// Fitness function (may require to pass solution space as \Closure variable)
|
||||
// E.g.
|
||||
// highest value of some sorts (e.g. profit)
|
||||
// most elements (e.g. jobs)
|
||||
// lowest costs
|
||||
// combination of criteria = points (where some criteria are mandatory/optional)
|
||||
public static function fitness($x)
|
||||
{
|
||||
return $x;
|
||||
}
|
||||
|
||||
// Mutation
|
||||
public static function mutate($parameters, $mutationRate)
|
||||
{
|
||||
for ($i = 0; $i < \count($parameters); $i++) {
|
||||
if (\mt_rand(0, 1000) / 1000 < $mutationRate) {
|
||||
$parameters[$i] = 1 - $parameters[$i];
|
||||
}
|
||||
}
|
||||
|
||||
return $parameters;
|
||||
}
|
||||
|
||||
public static function crossover($parent1, $parent2, $parameterCount)
|
||||
{
|
||||
$crossoverPoint = \mt_rand(1, $parameterCount - 1);
|
||||
|
||||
$child1 = \array_merge(
|
||||
\array_slice($parent1, 0, $crossoverPoint),
|
||||
\array_slice($parent2, $crossoverPoint)
|
||||
);
|
||||
|
||||
$child2 = \array_merge(
|
||||
\array_slice($parent2, 0, $crossoverPoint),
|
||||
\array_slice($parent1, $crossoverPoint)
|
||||
);
|
||||
|
||||
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)
|
||||
public static function optimize(
|
||||
array $population,
|
||||
\Closure $fitness,
|
||||
\Closure $mutate,
|
||||
\Closure $crossover,
|
||||
int $generations = 500,
|
||||
float $mutationRate = 0.1
|
||||
) : array
|
||||
{
|
||||
$populationSize = \count($population);
|
||||
$parameterCount = \count(\reset($population));
|
||||
|
||||
// Genetic Algorithm Loop
|
||||
for ($generation = 0; $generation < $generations; $generation++) {
|
||||
$fitnessScores = [];
|
||||
foreach ($population as $parameters) {
|
||||
$fitnessScores[] = ($fitness)($parameters);
|
||||
}
|
||||
|
||||
// Select parents for crossover based on fitness scores
|
||||
$parents = [];
|
||||
for ($i = 0; $i < $populationSize; $i++) {
|
||||
do {
|
||||
$parentIndex1 = \array_rand($population);
|
||||
$parentIndex2 = \array_rand($population);
|
||||
} while ($parentIndex1 === $parentIndex2);
|
||||
|
||||
$parents[] = $fitnessScores[$parentIndex1] > $fitnessScores[$parentIndex2]
|
||||
? $population[$parentIndex1]
|
||||
: $population[$parentIndex2];
|
||||
}
|
||||
|
||||
// Crossover and mutation to create next generation
|
||||
$newPopulation = [];
|
||||
for ($i = 0; $i < $populationSize; $i += 2) {
|
||||
$crossover = ($crossover)($parents[$i], $parents[$i + 1], $parameterCount);
|
||||
|
||||
$child1 = ($mutate)($crossover[0], $mutationRate);
|
||||
$child2 = ($mutate)($crossover[1], $mutationRate);
|
||||
|
||||
$newPopulation[] = $child1;
|
||||
$newPopulation[] = $child2;
|
||||
}
|
||||
|
||||
$population = $newPopulation;
|
||||
}
|
||||
|
||||
$fitnesses = [];
|
||||
|
||||
foreach ($population as $parameters) {
|
||||
$fitnesses[$population] = ($fitness)($parameters);
|
||||
}
|
||||
|
||||
\asort($fitnesses);
|
||||
|
||||
return [
|
||||
'solutions' => $population,
|
||||
'fitnesses' => $fitnesses,
|
||||
];
|
||||
}
|
||||
}
|
||||
0
Algorithm/Optimization/HarmonySearch.php
Normal file
0
Algorithm/Optimization/HarmonySearch.php
Normal file
0
Algorithm/Optimization/IntelligentWaterDrops.php
Normal file
0
Algorithm/Optimization/IntelligentWaterDrops.php
Normal file
84
Algorithm/Optimization/SimulatedAnnealing.php
Normal file
84
Algorithm/Optimization/SimulatedAnnealing.php
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
<?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 simulated annealing (SA).
|
||||
*
|
||||
* @package phpOMS\Algorithm\Optimization
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class SimulatedAnnealing
|
||||
{
|
||||
public static function costFunction($x)
|
||||
{
|
||||
return $x;
|
||||
}
|
||||
|
||||
// can be many things, e.g. swapping parameters, increasing/decrising, random generation
|
||||
public static function neighbor(array $generation, $parameterCount)
|
||||
{
|
||||
$newGeneration = $generation;
|
||||
$randomIndex1 = \mt_rand(0, $parameterCount - 1);
|
||||
$randomIndex2 = \mt_rand(0, $parameterCount - 1);
|
||||
|
||||
// Swap two cities in the route
|
||||
$temp = $newGeneration[$randomIndex1];
|
||||
$newGeneration[$randomIndex1] = $newGeneration[$randomIndex2];
|
||||
$newGeneration[$randomIndex2] = $temp;
|
||||
|
||||
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
|
||||
function optimize(
|
||||
array $space,
|
||||
int $initialTemperature,
|
||||
\Closure $costFunction,
|
||||
\Closure $neighbor,
|
||||
$coolingRate = 0.98,
|
||||
$numIterations = 1000
|
||||
) {
|
||||
$parameterCount = \count($space);
|
||||
$currentGeneration = \reset($space);
|
||||
|
||||
$currentCost = ($costFunction)($currentGeneration);
|
||||
|
||||
for ($i = 0; $i < $numIterations; $i++) {
|
||||
$newGeneration = ($neighbor)($currentGeneration, $parameterCount);
|
||||
|
||||
$newCost = ($costFunction)($newGeneration);
|
||||
|
||||
$temperature = $initialTemperature * pow($coolingRate, $i);
|
||||
|
||||
if ($newCost < $currentCost
|
||||
|| \mt_rand() / \mt_getrandmax() < \exp(($currentCost - $newCost) / $temperature)
|
||||
) {
|
||||
$currentGeneration = $newGeneration;
|
||||
$currentCost = $newCost;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'solutions' => $currentGeneration,
|
||||
'costs' => $currentCost
|
||||
];
|
||||
}
|
||||
}
|
||||
86
Algorithm/Optimization/TabuSearch.php
Normal file
86
Algorithm/Optimization/TabuSearch.php
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
<?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 tabu search.
|
||||
*
|
||||
* @package phpOMS\Algorithm\Optimization
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class TabuSearch
|
||||
{
|
||||
// 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 /* ... */;
|
||||
}
|
||||
|
||||
// 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 /* ... */;
|
||||
}
|
||||
|
||||
// Define the Tabu Search algorithm
|
||||
public static function optimize($initialSolution, \Closure $fitness, \Closure $neighbor, $tabuListSize, $maxIterations) {
|
||||
$currentSolution = $initialSolution;
|
||||
$bestSolution = $currentSolution;
|
||||
$bestFitness = \PHP_FLOAT_MIN;
|
||||
$tabuList = [];
|
||||
|
||||
for ($iteration = 0; $iteration < $maxIterations; ++$iteration) {
|
||||
$neighbors = [];
|
||||
for ($i = 0; $i < $tabuListSize; ++$i) {
|
||||
$neighbor = ($neighbor)($currentSolution);
|
||||
$neighbors[] = $neighbor;
|
||||
}
|
||||
|
||||
$bestNeighbor = null;
|
||||
foreach ($neighbors as $neighbor) {
|
||||
if (!\in_array($neighbor, $tabuList) &&
|
||||
($bestNeighbor === null
|
||||
|| ($fitness)($neighbor) > ($fitness)($bestNeighbor))
|
||||
) {
|
||||
$bestNeighbor = $neighbor;
|
||||
}
|
||||
}
|
||||
|
||||
if (\is_null($bestNeighbor)) {
|
||||
break;
|
||||
}
|
||||
|
||||
$tabuList[] = $bestNeighbor;
|
||||
if (\count($tabuList) > $tabuListSize) {
|
||||
\array_shift($tabuList);
|
||||
}
|
||||
|
||||
$currentSolution = $bestNeighbor;
|
||||
|
||||
|
||||
if (($score = ($fitness)($bestNeighbor)) > $bestFitness) {
|
||||
$bestSolution = $bestNeighbor;
|
||||
$bestFitness = $score;
|
||||
}
|
||||
}
|
||||
|
||||
return $bestSolution;
|
||||
}
|
||||
}
|
||||
|
|
@ -31,7 +31,7 @@ final class Elo
|
|||
|
||||
public int $MIN_ELO = 100;
|
||||
|
||||
public function rating(int $elo, array $oElo, array $s)
|
||||
public function rating(int $elo, array $oElo, array $s) : array
|
||||
{
|
||||
$eloNew = $elo;
|
||||
foreach ($oElo as $idx => $o) {
|
||||
|
|
|
|||
10
Business/Marketing/ArticleCorrelationAffinity.php → Business/Recommendation/ArticleCorrelationAffinity.php
Executable file → Normal file
10
Business/Marketing/ArticleCorrelationAffinity.php → Business/Recommendation/ArticleCorrelationAffinity.php
Executable file → Normal file
|
|
@ -4,7 +4,7 @@
|
|||
*
|
||||
* PHP Version 8.1
|
||||
*
|
||||
* @package phpOMS\Business\Marketing
|
||||
* @package phpOMS\Business\Recommendation
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
|
|
@ -12,16 +12,16 @@
|
|||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Business\Marketing;
|
||||
namespace phpOMS\Business\Recommendation;
|
||||
|
||||
use phpOMS\Math\Statistic\Correlation;
|
||||
|
||||
/**
|
||||
* Marketing ArticleAffinity
|
||||
* Article Affinity
|
||||
*
|
||||
* This class provided basic marketing metric calculations
|
||||
* You can consider this as a "purchased with" or "customers also purchased" algorithm
|
||||
*
|
||||
* @package phpOMS\Business\Marketing
|
||||
* @package phpOMS\Business\Recommendation
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
94
Business/Recommendation/BayesianPersonalizedRanking.php
Normal file
94
Business/Recommendation/BayesianPersonalizedRanking.php
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.1
|
||||
*
|
||||
* @package phpOMS\Business\Recommendation
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Business\Recommendation;
|
||||
|
||||
/**
|
||||
* Bayesian Personalized Ranking (BPR)
|
||||
*
|
||||
* @package phpOMS\Business\Recommendation
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
* @see https://arxiv.org/ftp/arxiv/papers/1205/1205.2618.pdf
|
||||
*
|
||||
* @todo Implement, current implementation probably wrong
|
||||
*/
|
||||
final class BayesianPersonalizedRanking
|
||||
{
|
||||
private int $numFactors;
|
||||
private float $learningRate;
|
||||
private float $regularization;
|
||||
|
||||
private array $userFactors = [];
|
||||
private array $itemFactors = [];
|
||||
|
||||
// num_factors determines the dimensionality of the latent factor space.
|
||||
// learning_rate controls the step size for updating the latent factors during optimization.
|
||||
// regularization prevents overfitting by adding a penalty for large parameter values.
|
||||
public function __construct(int $numFactors, float $learningRate, float $regularization) {
|
||||
$this->numFactors = $numFactors;
|
||||
$this->learningRate = $learningRate;
|
||||
$this->regularization = $regularization;
|
||||
}
|
||||
|
||||
private function generateRandomFactors() {
|
||||
$factors = [];
|
||||
for ($i = 0; $i < $this->numFactors; ++$i) {
|
||||
$factors[$i] = \mt_rand() / \mt_getrandmax();
|
||||
}
|
||||
|
||||
return $factors;
|
||||
}
|
||||
|
||||
public function predict($userId, $itemId) {
|
||||
$userFactor = $this->userFactors[$userId];
|
||||
$itemFactor = $this->itemFactors[$itemId];
|
||||
$score = 0;
|
||||
|
||||
for ($i = 0; $i < $this->numFactors; ++$i) {
|
||||
$score += $userFactor[$i] * $itemFactor[$i];
|
||||
}
|
||||
|
||||
return $score;
|
||||
}
|
||||
|
||||
public function updateFactors($userId, $posItemId, $negItemId) {
|
||||
if (!isset($this->userFactors[$userId])) {
|
||||
$this->userFactors[$userId] = $this->generateRandomFactors();
|
||||
}
|
||||
|
||||
if (!isset($this->itemFactors[$posItemId])) {
|
||||
$this->itemFactors[$posItemId] = $this->generateRandomFactors();
|
||||
}
|
||||
|
||||
if (!isset($this->itemFactors[$negItemId])) {
|
||||
$this->itemFactors[$negItemId] = $this->generateRandomFactors();
|
||||
}
|
||||
|
||||
$userFactor = $this->userFactors[$userId];
|
||||
$posItemFactor = $this->itemFactors[$posItemId];
|
||||
$negItemFactor = $this->itemFactors[$negItemId];
|
||||
|
||||
for ($i = 0; $i < $this->numFactors; ++$i) {
|
||||
$userFactor[$i] += $this->learningRate * ($posItemFactor[$i] - $negItemFactor[$i]) - $this->regularization * $userFactor[$i];
|
||||
$posItemFactor[$i] += $this->learningRate * $userFactor[$i] - $this->regularization * $posItemFactor[$i];
|
||||
$negItemFactor[$i] += $this->learningRate * (-$userFactor[$i]) - $this->regularization * $negItemFactor[$i];
|
||||
}
|
||||
|
||||
$this->userFactors[$userId] = $userFactor;
|
||||
$this->itemFactors[$posItemId] = $posItemFactor;
|
||||
$this->itemFactors[$negItemId] = $negItemFactor;
|
||||
}
|
||||
}
|
||||
146
Business/Recommendation/MemoryCF.php
Normal file
146
Business/Recommendation/MemoryCF.php
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.1
|
||||
*
|
||||
* @package phpOMS\Business\Recommendation
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Business\Recommendation;
|
||||
|
||||
use phpOMS\Math\Topology\MetricsND;
|
||||
|
||||
/**
|
||||
* Memory based collaborative filtering
|
||||
*
|
||||
* Items or potential customers are found based on how much they like certain items.
|
||||
*
|
||||
* This requires a item/product rating of some sort in the backend.
|
||||
* Such a rating could be either manual user ratings or a rating based on how often it is purchased or how long it is used.
|
||||
* Most likely a combination is required.
|
||||
*
|
||||
* @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/
|
||||
*/
|
||||
final class MemoryCF
|
||||
{
|
||||
private array $rankings = [];
|
||||
|
||||
public function __construc(array $rankings)
|
||||
{
|
||||
$this->rankings = $this->normalizeRanking($rankings);
|
||||
}
|
||||
|
||||
private function normalizeRanking(array $rankings) : array
|
||||
{
|
||||
foreach ($rankings as $idx => $items) {
|
||||
$avg = \array_sum($items) / \count($items);
|
||||
|
||||
foreach ($items as $idx2 => $_) {
|
||||
$rankings[$idx][$idx2] -= $avg;
|
||||
}
|
||||
}
|
||||
|
||||
return $rankings;
|
||||
}
|
||||
|
||||
// Used to find similar users
|
||||
public function euclideanDistance(array $ranking, array $rankings) : array
|
||||
{
|
||||
$distances = [];
|
||||
foreach ($rankings as $idx => $r) {
|
||||
$distances[$idx] = \abs(MetricsND::euclidean($ranking, $r));
|
||||
}
|
||||
|
||||
return $distances;
|
||||
}
|
||||
|
||||
// Used to find similar users
|
||||
public function cosineDistance(array $ranking, array $rankings) : array
|
||||
{
|
||||
$distances = [];
|
||||
foreach ($rankings as $idx => $r) {
|
||||
$distances[$idx] = \abs(MetricsND::cosine($ranking, $r));
|
||||
}
|
||||
|
||||
return $distances;
|
||||
}
|
||||
|
||||
private function weightedItemRank(string $itemId, array $distances, array $users, int $size) : float
|
||||
{
|
||||
$rank = 0.0;
|
||||
$count = 0;
|
||||
foreach ($distances as $uId => $_) {
|
||||
if ($count >= $size) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (!isset($user[$itemId])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
++$count;
|
||||
$rank += $users[$uId][$itemId];
|
||||
}
|
||||
|
||||
return $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
|
||||
public function bestMatch(array $ranking, int $size = 10) : array
|
||||
{
|
||||
$ranking = $this->normalizeRanking([$ranking]);
|
||||
$ranking = $ranking[0];
|
||||
|
||||
$euclidean = $this->euclideanDistance($ranking, $this->rankings);
|
||||
$cosine = $this->cosineDistance($ranking, $this->rankings);
|
||||
|
||||
\asort($euclidean);
|
||||
\asort($cosine);
|
||||
|
||||
$size = \min($size, \count($this->rankings));
|
||||
$matches = [];
|
||||
|
||||
$distancePointer = \array_keys($euclidean);
|
||||
$anglePointer = \array_keys($cosine);
|
||||
|
||||
// Inspect items of the top n comparable users
|
||||
for ($i = 1; $i <= $size; ++$i) {
|
||||
$index = (int) ($i / 2) - 1;
|
||||
|
||||
$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])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Calculate the expected rating the user would give based on what the best comparable users did
|
||||
$matches[$iId] = $this->weightedItemRank($iId, $distances, $this->rankings, $size);
|
||||
}
|
||||
}
|
||||
|
||||
\asort($matches);
|
||||
$matches = \array_reverse($matches, true);
|
||||
|
||||
return $matches;
|
||||
}
|
||||
}
|
||||
50
Business/Recommendation/ModelCF.php
Normal file
50
Business/Recommendation/ModelCF.php
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.1
|
||||
*
|
||||
* @package phpOMS\Business\Recommendation
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Business\Recommendation;
|
||||
|
||||
use phpOMS\Math\Matrix\Matrix;
|
||||
|
||||
/**
|
||||
* Model based collaborative filtering
|
||||
*
|
||||
* @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/
|
||||
*/
|
||||
final class ModelCF
|
||||
{
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @since 1.0.0
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
// $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
|
||||
public static function score(Matrix $users, Matrix $items) : array
|
||||
{
|
||||
return $users->mult($items)->getMatrix();
|
||||
}
|
||||
}
|
||||
3
Business/Recommendation/README.md
Normal file
3
Business/Recommendation/README.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# Recommendation
|
||||
|
||||
Additional recommendation algorithms not included in this directory are Clustering and Classifier available in this libarary.
|
||||
|
|
@ -61,7 +61,7 @@ final class Polygon implements D2ShapeInterface
|
|||
*
|
||||
* @param array{x:int|float, y:int|float} $point Point location
|
||||
*
|
||||
* @return int
|
||||
* @return int -1 inside polygon 0 on vertice 1 outside
|
||||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ class Matrix implements \ArrayAccess, \Iterator
|
|||
* @var array<int, array<int, int|float>>
|
||||
* @since 1.0.0
|
||||
*/
|
||||
protected array $matrix = [];
|
||||
public array $matrix = [];
|
||||
|
||||
/**
|
||||
* Columns.
|
||||
|
|
@ -436,7 +436,7 @@ class Matrix implements \ArrayAccess, \Iterator
|
|||
$newMatrixArr = $this->matrix;
|
||||
|
||||
foreach ($newMatrixArr as $i => $vector) {
|
||||
foreach ($vector as $j => $value) {
|
||||
foreach ($vector as $j => $_) {
|
||||
$newMatrixArr[$i][$j] += $matrixArr[$i][$j];
|
||||
}
|
||||
}
|
||||
|
|
@ -691,6 +691,178 @@ class Matrix implements \ArrayAccess, \Iterator
|
|||
return $L->det();
|
||||
}
|
||||
|
||||
public function dot(self $B) : self
|
||||
{
|
||||
$value1 = $this->matrix;
|
||||
$value2 = $B->getMatrix();
|
||||
|
||||
$m1 = \count($value1);
|
||||
$n1 = ($isMatrix1 = \is_array($value1[0])) ? \count($value1[0]) : 1;
|
||||
|
||||
$m2 = \count($value2);
|
||||
$n2 = ($isMatrix2 = \is_array($value2[0])) ? \count($value2[0]) : 1;
|
||||
|
||||
$result = null;
|
||||
|
||||
if ($isMatrix1 && $isMatrix2) {
|
||||
if ($m2 !== $n1) {
|
||||
throw new InvalidDimensionException($m2 . 'x' . $n2 . ' not compatible with ' . $m1 . 'x' . $n1);
|
||||
}
|
||||
|
||||
$result = [[]];
|
||||
for ($i = 0; $i < $m1; ++$i) { // Row of 1
|
||||
for ($c = 0; $c < $n2; ++$c) { // Column of 2
|
||||
$temp = 0;
|
||||
|
||||
for ($j = 0; $j < $m2; ++$j) { // Row of 2
|
||||
$temp += $value1[$i][$j] * $value2[$j][$c];
|
||||
}
|
||||
|
||||
$result[$i][$c] = $temp;
|
||||
}
|
||||
}
|
||||
} elseif (!$isMatrix1 && !$isMatrix2) {
|
||||
if ($m1 !== $m2) {
|
||||
throw new InvalidDimensionException($m1 . 'x' . $m2);
|
||||
}
|
||||
|
||||
$result = 0;
|
||||
for ($i = 0; $i < $m1; ++$i) {
|
||||
/** @var array $value1 */
|
||||
/** @var array $value2 */
|
||||
$result += $value1[$i] * $value2[$i];
|
||||
}
|
||||
} elseif ($isMatrix1 && !$isMatrix2) {
|
||||
$result = [];
|
||||
for ($i = 0; $i < $m1; ++$i) { // Row of 1
|
||||
$temp = 0;
|
||||
|
||||
for ($c = 0; $c < $m2; ++$c) { // Row of 2
|
||||
/** @var array $value2 */
|
||||
$temp += $value1[$i][$c] * $value2[$c];
|
||||
}
|
||||
|
||||
$result[$i] = $temp;
|
||||
}
|
||||
} else {
|
||||
throw new \InvalidArgumentException();
|
||||
}
|
||||
|
||||
return self::fromArray($result);
|
||||
}
|
||||
|
||||
public function sum(int $axis = -1)
|
||||
{
|
||||
if ($axis === -1) {
|
||||
$sum = 0;
|
||||
|
||||
foreach ($this->matrix as $row) {
|
||||
$sum += \array_sum($row);
|
||||
}
|
||||
|
||||
return $sum;
|
||||
} elseif ($axis === 0) {
|
||||
$sum = [];
|
||||
foreach ($this->matrix as $row) {
|
||||
foreach ($row as $idx2 => $value) {
|
||||
if (!isset($sum[$idx2])) {
|
||||
$sum[$idx2] = 0;
|
||||
}
|
||||
|
||||
$sum[$idx2] += $value;
|
||||
}
|
||||
}
|
||||
|
||||
return self::fromArray($sum);
|
||||
} elseif ($axis === 1) {
|
||||
$sum = [];
|
||||
foreach ($this->matrix as $idx => $row) {
|
||||
$sum[$idx] = \array_sum($row);
|
||||
}
|
||||
|
||||
return self::fromArray($sum);
|
||||
}
|
||||
|
||||
return new self();
|
||||
}
|
||||
|
||||
public function isDiagonal() : bool
|
||||
{
|
||||
if ($this->m !== $this->n) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for ($i = 0; $i < $this->m; ++$i) {
|
||||
for ($j = 0; $j < $this->n; ++$j) {
|
||||
if ($i !== $j && \abs($this->matrix[$i][$j]) > self::EPSILON) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function pow(int | float $exponent) : self
|
||||
{
|
||||
if ($this->isDiagonal()) {
|
||||
$matrix = [];
|
||||
|
||||
for ($i = 0; $i < $this->m; $i++) {
|
||||
$row = [];
|
||||
for ($j = 0; $j < $this->m; $j++) {
|
||||
if ($i === $j) {
|
||||
$row[] = \pow($this->matrix[$i][$j], $exponent);
|
||||
} else {
|
||||
$row[] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
$matrix[] = $row;
|
||||
}
|
||||
|
||||
return self::fromArray($matrix);
|
||||
} elseif (\is_int($exponent)) {
|
||||
if ($this->m !== $this->n) {
|
||||
throw new InvalidDimensionException($this->m . 'x' . $this->n);
|
||||
}
|
||||
|
||||
$matrix = new IdentityMatrix($this->m);
|
||||
for ($i = 0; $i < $exponent; ++$i) {
|
||||
$matrix = $matrix->mult($this);
|
||||
}
|
||||
|
||||
return $matrix;
|
||||
} else {
|
||||
// @todo: implement
|
||||
throw new \Exception('Not yet implemented');
|
||||
}
|
||||
}
|
||||
|
||||
public function exp(int $iterations = 10) : self
|
||||
{
|
||||
if ($this->m !== $this->n) {
|
||||
throw new InvalidDimensionException($this->m . 'x' . $this->n);
|
||||
}
|
||||
|
||||
$identity = new IdentityMatrix($this->m);
|
||||
$matrix = $identity;
|
||||
|
||||
$factorial = 1;
|
||||
$pow = $matrix;
|
||||
|
||||
for ($i = 1; $i <= $iterations; ++$i) {
|
||||
$factorial *= $i;
|
||||
$coeff = 1 / $factorial;
|
||||
|
||||
$term = $pow->mult($coeff);
|
||||
$matrix = $matrix->add($term);
|
||||
$pow = $pow->mult($matrix); // @todo: maybe wrong order?
|
||||
}
|
||||
|
||||
return $matrix;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -88,6 +88,32 @@ final class Vector extends Matrix
|
|||
return $this;
|
||||
}
|
||||
|
||||
public function cosine(self $v) : float
|
||||
{
|
||||
$dotProduct = 0;
|
||||
for ($i = 0; $i < \count($this->matrix); $i++) {
|
||||
$dotProduct += $this->matrix[$i][0] * $v[$i];
|
||||
}
|
||||
|
||||
$sumOfSquares = 0;
|
||||
foreach ($this->matrix as $value) {
|
||||
$sumOfSquares += $value[0] * $value[0];
|
||||
}
|
||||
$magnitude1 = \sqrt($sumOfSquares);
|
||||
|
||||
$sumOfSquares = 0;
|
||||
foreach ($v->matrix as $value) {
|
||||
$sumOfSquares += $value[0] * $value[0];
|
||||
}
|
||||
$magnitude2 = \sqrt($sumOfSquares);
|
||||
|
||||
if ($magnitude1 === 0 || $magnitude2 === 0) {
|
||||
return \PHP_FLOAT_MAX;
|
||||
}
|
||||
|
||||
return $dotProduct / ($magnitude1 * $magnitude2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the cross product
|
||||
*
|
||||
|
|
|
|||
112
Math/Topology/Kernel2D.php
Normal file
112
Math/Topology/Kernel2D.php
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.1
|
||||
*
|
||||
* @package phpOMS\Math\Topology
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Math\Topology;
|
||||
|
||||
/**
|
||||
* Metrics.
|
||||
*
|
||||
* @package phpOMS\Math\Topology
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
final class Kernels2D
|
||||
{
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @since 1.0.0
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
public static function uniformKernel(float $distance, float $bandwidth) : float
|
||||
{
|
||||
return \abs($distance) <= $bandwidth / 2
|
||||
? 1 / $bandwidth
|
||||
: 0.0;
|
||||
}
|
||||
|
||||
public static function triangularKernel(float $distance, float $bandwidth) : float
|
||||
{
|
||||
return \abs($distance) <= $bandwidth / 2
|
||||
? 1 - abs($distance) / ($bandwidth / 2)
|
||||
: 0.0;
|
||||
}
|
||||
|
||||
public static function epanechnikovKernel(float $distance, float $bandwidth) : float
|
||||
{
|
||||
if (\abs($distance) <= $bandwidth) {
|
||||
$u = \abs($distance) / $bandwidth;
|
||||
|
||||
return 0.75 * (1 - $u * $u) / $bandwidth;
|
||||
} else {
|
||||
return 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
public static function quarticKernel(float $distance, float $bandwidth) : float
|
||||
{
|
||||
if (\abs($distance) <= $bandwidth) {
|
||||
$u = \abs($distance) / $bandwidth;
|
||||
|
||||
return (15 / 16) * (1 - $u * $u) * (1 - $u * $u) / $bandwidth;
|
||||
} else {
|
||||
return 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
public static function triweightKernel(float $distance, float $bandwidth) : float
|
||||
{
|
||||
if (\abs($distance) <= $bandwidth) {
|
||||
$u = \abs($distance) / $bandwidth;
|
||||
|
||||
return (35 / 32) * (1 - $u * $u) * (1 - $u * $u) * (1 - $u * $u) / $bandwidth;
|
||||
} else {
|
||||
return 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
public static function tricubeKernel(float $distance, float $bandwidth) : float
|
||||
{
|
||||
if (\abs($distance) <= $bandwidth) {
|
||||
$u = \abs($distance) / $bandwidth;
|
||||
|
||||
return (70 / 81) * (1 - $u * $u * $u) * (1 - $u * $u * $u) * (1 - $u * $u * $u) / $bandwidth;
|
||||
} else {
|
||||
return 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
public static function gaussianKernel(float $distance, float $bandwidth) : float
|
||||
{
|
||||
return \exp(-($distance * $distance) / (2 * $bandwidth * $bandwidth)) / ($bandwidth * \sqrt(2 * \M_PI));
|
||||
}
|
||||
|
||||
public static function cosineKernel(float $distance, float $bandwidth) : float
|
||||
{
|
||||
return \abs($distance) <= $bandwidth
|
||||
? (\M_PI / 4) * \cos(\M_PI * $distance / (2 * $bandwidth)) / $bandwidth
|
||||
: 0.0;
|
||||
}
|
||||
|
||||
public static function logisticKernel(float $distance, float $bandwidth) : float
|
||||
{
|
||||
return 1 / (\exp($distance / $bandwidth) + 2 + \exp(-$distance / $bandwidth));
|
||||
}
|
||||
}
|
||||
55
Math/Topology/KernelsND.php
Normal file
55
Math/Topology/KernelsND.php
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
<?php
|
||||
/**
|
||||
* Jingga
|
||||
*
|
||||
* PHP Version 8.1
|
||||
*
|
||||
* @package phpOMS\Math\Topology
|
||||
* @copyright Dennis Eichhorn
|
||||
* @license OMS License 2.0
|
||||
* @version 1.0.0
|
||||
* @link https://jingga.app
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace phpOMS\Math\Topology;
|
||||
|
||||
use phpOMS\Math\Matrix\IdentityMatrix;
|
||||
use phpOMS\Math\Matrix\Matrix;
|
||||
|
||||
/**
|
||||
* Metrics.
|
||||
*
|
||||
* @package phpOMS\Math\Topology
|
||||
* @license OMS License 2.0
|
||||
* @link https://jingga.app
|
||||
* @since 1.0.0
|
||||
*/
|
||||
final class KernelsND
|
||||
{
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @since 1.0.0
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
public static function gaussianKernel(array $distances, array $bandwidths) : array
|
||||
{
|
||||
$dim = \count($bandwidths);
|
||||
|
||||
$bandwithMatrix = Matrix::fromArray($bandwidths);
|
||||
$distnaceMatrix = Matrix::fromArray($distances);
|
||||
$identityMatrix = new IdentityMatrix($dim);
|
||||
|
||||
$cov = $bandwithMatrix->mult($identityMatrix);
|
||||
|
||||
$exponent = $distnaceMatrix->dot($cov->inverse())->mult($distnaceMatrix)->sum(1)->mult(-0.5);
|
||||
|
||||
return $exponent->exp()->mult((1 / \pow(2 * \M_PI, $dim / 2)) * \pow($cov->det(), 0.5))->matrix;
|
||||
}
|
||||
}
|
||||
|
|
@ -93,6 +93,32 @@ final class MetricsND
|
|||
return \sqrt($dist);
|
||||
}
|
||||
|
||||
public static function cosine(array $a, array $b) : float
|
||||
{
|
||||
$dotProduct = 0;
|
||||
for ($i = 0; $i < \count($a); $i++) {
|
||||
$dotProduct += $a[$i] * $b[$i];
|
||||
}
|
||||
|
||||
$sumOfSquares = 0;
|
||||
foreach ($a as $value) {
|
||||
$sumOfSquares += $value * $value;
|
||||
}
|
||||
$magnitude1 = \sqrt($sumOfSquares);
|
||||
|
||||
$sumOfSquares = 0;
|
||||
foreach ($b as $value) {
|
||||
$sumOfSquares += $value * $value;
|
||||
}
|
||||
$magnitude2 = \sqrt($sumOfSquares);
|
||||
|
||||
if ($magnitude1 === 0 || $magnitude2 === 0) {
|
||||
return \PHP_FLOAT_MAX;
|
||||
}
|
||||
|
||||
return $dotProduct / ($magnitude1 * $magnitude2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Chebyshev metric.
|
||||
*
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user