Passed
Pull Request — master (#34)
by Benjamin
12:13
created

Algorithm::clusterize()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 16
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 8
nc 2
nop 2
dl 0
loc 16
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Kmeans;
4
5
use Kmeans\Cluster;
6
use Kmeans\ClusterCollection;
7
use Kmeans\Interfaces\AlgorithmInterface;
8
use Kmeans\Interfaces\ClusterCollectionInterface;
9
use Kmeans\Interfaces\ClusterInterface;
10
use Kmeans\Interfaces\InitializationSchemeInterface;
11
use Kmeans\Interfaces\PointCollectionInterface;
12
use Kmeans\Interfaces\PointInterface;
13
14
class Algorithm implements AlgorithmInterface
15
{
16
    private InitializationSchemeInterface $initScheme;
17
    /** @var array<callable> */
18
    private array $iterationCallbacks = [];
19
20
21
    public function __construct(InitializationSchemeInterface $initScheme)
22
    {
23
        $this->initScheme = $initScheme;
24
    }
25
26
    public function registerIterationCallback(callable $callback): void
27
    {
28
        $this->iterationCallbacks[] = $callback;
29
    }
30
31
    public function clusterize(PointCollectionInterface $points, int $nbClusters): ClusterCollectionInterface
32
    {
33
        try {
34
            // initialize clusters
35
            $clusters = $this->initScheme->initializeClusters($points, $nbClusters);
36
        } catch (\Exception $e) {
37
            throw new \RuntimeException("Cannot initialize clusters", 0, $e);
38
        }
39
40
        // iterate until convergence is reached
41
        do {
42
            $this->invokeIterationCallbacks($clusters);
43
        } while ($this->iterate($clusters));
44
45
        // clustering is done.
46
        return $clusters;
47
    }
48
49
    protected function iterate(ClusterCollectionInterface $clusters): bool
50
    {
51
        /** @var \SplObjectStorage<ClusterInterface, null> */
52
        $changed = new \SplObjectStorage();
53
54
        // calculate proximity amongst points and clusters
55
        foreach ($clusters as $cluster) {
56
            foreach ($cluster->getPoints() as $point) {
57
                // find the closest cluster
58
                $closest = $this->getClosestCluster($clusters, $point);
59
60
                if ($closest !== $cluster) {
61
                    // move the point from its current cluster to its closest
62
                    $cluster->detach($point);
63
                    $closest->attach($point);
64
65
                    // flag both clusters as changed
66
                    $changed->attach($cluster);
67
                    $changed->attach($closest);
68
                }
69
            }
70
        }
71
72
        // update changed clusters' centroid
73
        foreach ($changed as $cluster) {
74
            $cluster->setCentroid($this->findCentroid($cluster->getPoints()));
75
        }
76
77
        // return true if something changed during this iteration
78
        return count($changed) > 0;
79
    }
80
81
    protected function getClosestCluster(ClusterCollectionInterface $clusters, PointInterface $point): ClusterInterface
82
    {
83
        $min = null;
84
        $closest = null;
85
86
        foreach ($clusters as $cluster) {
87
            $distance = $this->getDistanceBetween($point, $cluster->getCentroid());
88
89
            if (is_null($min) || $distance < $min) {
90
                $min = $distance;
91
                $closest = $cluster;
92
            }
93
        }
94
95
        assert($closest !== null);
96
        return $closest;
97
    }
98
99
    protected function getDistanceBetween(PointInterface $pointA, PointInterface $pointB): float
100
    {
101
        return euclidean_dist($pointA->getCoordinates(), $pointB->getCoordinates());
102
    }
103
104
    protected function findCentroid(PointCollectionInterface $points): PointInterface
105
    {
106
        return new Point($points->getSpace(), find_centroid(
107
            array_map(fn ($point) => $point->getCoordinates(), iterator_to_array($points))
108
        ));
109
    }
110
111
    protected function invokeIterationCallbacks(ClusterCollectionInterface $clusters): void
112
    {
113
        foreach ($this->iterationCallbacks as $callback) {
114
            $callback($this, $clusters);
115
        }
116
    }
117
}
118