Passed
Pull Request — master (#34)
by Benjamin
26:50 queued 11:50
created

Algorithm   A

Complexity

Total Complexity 15

Size/Duplication

Total Lines 87
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 33
c 0
b 0
f 0
dl 0
loc 87
rs 10
wmc 15

6 Methods

Rating   Name   Duplication   Size   Complexity  
A getClosestCluster() 0 16 4
A iterate() 0 30 5
A clusterize() 0 12 2
A __construct() 0 3 1
A registerIterationCallback() 0 3 1
A invokeIterationCallbacks() 0 4 2
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
abstract class Algorithm implements AlgorithmInterface
15
{
16
    private InitializationSchemeInterface $initScheme;
17
18
    /**
19
     * @var array<callable>
20
     */
21
    private array $iterationCallbacks = [];
22
23
    public function __construct(InitializationSchemeInterface $initScheme)
24
    {
25
        $this->initScheme = $initScheme;
26
    }
27
28
    public function registerIterationCallback(callable $callback): void
29
    {
30
        $this->iterationCallbacks[] = $callback;
31
    }
32
33
    public function clusterize(PointCollectionInterface $points, int $nbClusters): ClusterCollectionInterface
34
    {
35
        // initialize clusters
36
        $clusters = $this->initScheme->initializeClusters($points, $nbClusters);
37
38
        // iterate until convergence is reached
39
        do {
40
            $this->invokeIterationCallbacks($clusters);
41
        } while ($this->iterate($clusters));
42
43
        // clustering is done.
44
        return $clusters;
45
    }
46
47
    private function iterate(ClusterCollectionInterface $clusters): bool
48
    {
49
        /** @var \SplObjectStorage<ClusterInterface, null> */
50
        $changed = new \SplObjectStorage();
51
52
        // calculate proximity amongst points and clusters
53
        foreach ($clusters as $cluster) {
54
            foreach ($cluster->getPoints() as $point) {
55
                // find the closest cluster
56
                $closest = $this->getClosestCluster($clusters, $point);
57
58
                if ($closest !== $cluster) {
59
                    // move the point from its current cluster to its closest
60
                    $cluster->detach($point);
61
                    $closest->attach($point);
62
63
                    // flag both clusters as changed
64
                    $changed->attach($cluster);
65
                    $changed->attach($closest);
66
                }
67
            }
68
        }
69
70
        // update changed clusters' centroid
71
        foreach ($changed as $cluster) {
72
            $cluster->setCentroid($this->findCentroid($cluster->getPoints()));
73
        }
74
75
        // return true if something changed during this iteration
76
        return count($changed) > 0;
77
    }
78
79
    private function getClosestCluster(ClusterCollectionInterface $clusters, PointInterface $point): ClusterInterface
80
    {
81
        $min = null;
82
        $closest = null;
83
84
        foreach ($clusters as $cluster) {
85
            $distance = $this->getDistanceBetween($point, $cluster->getCentroid());
86
87
            if (is_null($min) || $distance < $min) {
88
                $min = $distance;
89
                $closest = $cluster;
90
            }
91
        }
92
93
        assert($closest !== null);
94
        return $closest;
95
    }
96
97
    private function invokeIterationCallbacks(ClusterCollectionInterface $clusters): void
98
    {
99
        foreach ($this->iterationCallbacks as $callback) {
100
            $callback($this, $clusters);
101
        }
102
    }
103
}
104