This commit is contained in:
Dennis Eichhorn 2023-08-28 22:06:37 +00:00
parent c4fc99ed39
commit 74e1684ad0
46 changed files with 2365 additions and 30 deletions

View 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
{
}

View 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
{
}

View 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
{
}

View 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] = [];
}
}
}
}

View File

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

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

View File

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

View File

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

View 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
{
}

View 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
{
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 710 KiB

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

View File

View 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
}

View 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
}

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

View 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 = [];
}

View 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 = [];
}

View 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 = [];
}

View 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 = [];
}

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

View 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

View 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
}

View 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]);
}
}

View File

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

View File

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

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

View File

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

View 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

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

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

View 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();
}
}

View File

@ -0,0 +1,3 @@
# Recommendation
Additional recommendation algorithms not included in this directory are Clustering and Classifier available in this libarary.

View File

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

View File

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

View File

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

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

View File

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