Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
0.00% covered (danger)
0.00%
0 / 1
53.85% covered (warning)
53.85%
7 / 13
CRAP
88.89% covered (warning)
88.89%
72 / 81
Space
0.00% covered (danger)
0.00%
0 / 1
53.85% covered (warning)
53.85%
7 / 13
45.54
88.89% covered (warning)
88.89%
72 / 81
 __construct
0.00% covered (danger)
0.00%
0 / 1
2.06
75.00% covered (warning)
75.00%
3 / 4
 toArray
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 4
 newPoint
0.00% covered (danger)
0.00%
0 / 1
2.15
66.67% covered (warning)
66.67%
2 / 3
 addPoint
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 attach
0.00% covered (danger)
0.00%
0 / 1
2.15
66.67% covered (warning)
66.67%
2 / 3
 getDimension
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 getBoundaries
0.00% covered (danger)
0.00%
0 / 1
8.09
88.89% covered (warning)
88.89%
8 / 9
 getRandomPoint
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
4 / 4
 cluster
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
3 / 3
 initializeClusters
100.00% covered (success)
100.00%
1 / 1
3
100.00% covered (success)
100.00%
8 / 8
 iterate
100.00% covered (success)
100.00%
1 / 1
9
100.00% covered (success)
100.00%
19 / 19
 initializeRandomClusters
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
5 / 5
 initializeKMPPClusters
100.00% covered (success)
100.00%
1 / 1
7
100.00% covered (success)
100.00%
17 / 17
<?php
declare (strict_types = 1);
namespace Phpml\Clustering\KMeans;
use Phpml\Clustering\KMeans;
use SplObjectStorage;
use LogicException;
use InvalidArgumentException;
class Space extends SplObjectStorage
{
    /**
     * @var int
     */
    protected $dimension;
    /**
     * @param $dimension
     */
    public function __construct($dimension)
    {
        if ($dimension < 1) {
            throw new LogicException('a space dimension cannot be null or negative');
        }
        $this->dimension = $dimension;
    }
    /**
     * @return array
     */
    public function toArray()
    {
        $points = [];
        foreach ($this as $point) {
            $points[] = $point->toArray();
        }
        return ['points' => $points];
    }
    /**
     * @param array $coordinates
     *
     * @return Point
     */
    public function newPoint(array $coordinates)
    {
        if (count($coordinates) != $this->dimension) {
            throw new LogicException('('.implode(',', $coordinates).') is not a point of this space');
        }
        return new Point($coordinates);
    }
    /**
     * @param array $coordinates
     * @param null  $data
     */
    public function addPoint(array $coordinates, $data = null)
    {
        return $this->attach($this->newPoint($coordinates), $data);
    }
    /**
     * @param object $point
     * @param null   $data
     */
    public function attach($point, $data = null)
    {
        if (!$point instanceof Point) {
            throw new InvalidArgumentException('can only attach points to spaces');
        }
        return parent::attach($point, $data);
    }
    /**
     * @return int
     */
    public function getDimension()
    {
        return $this->dimension;
    }
    /**
     * @return array|bool
     */
    public function getBoundaries()
    {
        if (!count($this)) {
            return false;
        }
        $min = $this->newPoint(array_fill(0, $this->dimension, null));
        $max = $this->newPoint(array_fill(0, $this->dimension, null));
        foreach ($this as $point) {
            for ($n = 0; $n < $this->dimension; ++$n) {
                ($min[$n] > $point[$n] || $min[$n] === null) && $min[$n] = $point[$n];
                ($max[$n] < $point[$n] || $max[$n] === null) && $max[$n] = $point[$n];
            }
        }
        return array($min, $max);
    }
    /**
     * @param Point $min
     * @param Point $max
     *
     * @return Point
     */
    public function getRandomPoint(Point $min, Point $max)
    {
        $point = $this->newPoint(array_fill(0, $this->dimension, null));
        for ($n = 0; $n < $this->dimension; ++$n) {
            $point[$n] = rand($min[$n], $max[$n]);
        }
        return $point;
    }
    /**
     * @param int $clustersNumber
     * @param int $initMethod
     *
     * @return array|Cluster[]
     */
    public function cluster(int $clustersNumber, int $initMethod = KMeans::INIT_RANDOM)
    {
        $clusters = $this->initializeClusters($clustersNumber, $initMethod);
        do {
        } while (!$this->iterate($clusters));
        return $clusters;
    }
    /**
     * @param $clustersNumber
     * @param $initMethod
     *
     * @return array|Cluster[]
     */
    protected function initializeClusters(int $clustersNumber, int $initMethod)
    {
        switch ($initMethod) {
            case KMeans::INIT_RANDOM:
                $clusters = $this->initializeRandomClusters($clustersNumber);
                break;
            case KMeans::INIT_KMEANS_PLUS_PLUS:
                $clusters = $this->initializeKMPPClusters($clustersNumber);
                break;
        }
        $clusters[0]->attachAll($this);
        return $clusters;
    }
    /**
     * @param $clusters
     *
     * @return bool
     */
    protected function iterate($clusters)
    {
        $convergence = true;
        $attach = new SplObjectStorage();
        $detach = new SplObjectStorage();
        foreach ($clusters as $cluster) {
            foreach ($cluster as $point) {
                $closest = $point->getClosest($clusters);
                if ($closest !== $cluster) {
                    isset($attach[$closest]) || $attach[$closest] = new SplObjectStorage();
                    isset($detach[$cluster]) || $detach[$cluster] = new SplObjectStorage();
                    $attach[$closest]->attach($point);
                    $detach[$cluster]->attach($point);
                    $convergence = false;
                }
            }
        }
        foreach ($attach as $cluster) {
            $cluster->attachAll($attach[$cluster]);
        }
        foreach ($detach as $cluster) {
            $cluster->detachAll($detach[$cluster]);
        }
        foreach ($clusters as $cluster) {
            $cluster->updateCentroid();
        }
        return $convergence;
    }
    /**
     * @param int $clustersNumber
     *
     * @return array
     */
    private function initializeRandomClusters(int $clustersNumber)
    {
        $clusters = [];
        list($min, $max) = $this->getBoundaries();
        for ($n = 0; $n < $clustersNumber; ++$n) {
            $clusters[] = new Cluster($this, $this->getRandomPoint($min, $max)->getCoordinates());
        }
        return $clusters;
    }
    /**
     * @param int $clustersNumber
     *
     * @return array
     */
    protected function initializeKMPPClusters(int $clustersNumber)
    {
        $clusters = [];
        $position = rand(1, count($this));
        for ($i = 1, $this->rewind(); $i < $position && $this->valid(); $i++, $this->next());
        $clusters[] = new Cluster($this, $this->current()->getCoordinates());
        $distances = new SplObjectStorage();
        for ($i = 1; $i < $clustersNumber; ++$i) {
            $sum = 0;
            foreach ($this as $point) {
                $distance = $point->getDistanceWith($point->getClosest($clusters));
                $sum += $distances[$point] = $distance;
            }
            $sum = rand(0, (int) $sum);
            foreach ($this as $point) {
                if (($sum -= $distances[$point]) > 0) {
                    continue;
                }
                $clusters[] = new Cluster($this, $point->getCoordinates());
                break;
            }
        }
        return $clusters;
    }
}