Space::attach()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 8
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 2
1
<?php
2
3
/**
4
 * This file is part of PHP K-Means
5
 *
6
 * Copyright (c) 2014 Benjamin Delespierre
7
 *
8
 * Permission is hereby granted, free of charge, to any person obtaining a copy
9
 * of this software and associated documentation files (the "Software"), to deal
10
 * in the Software without restriction, including without limitation the rights
11
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
 * copies of the Software, and to permit persons to whom the Software is furnished
13
 * to do so, subject to the following conditions:
14
 *
15
 * The above copyright notice and this permission notice shall be included in all
16
 * copies or substantial portions of the Software.
17
 *
18
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24
 * THE SOFTWARE.
25
 */
26
27
namespace KMeans;
28
29
class Space extends \SplObjectStorage
30
{
31
    protected $dimention;
32
33
    protected static $rng = 'mt_rand';
34
35
    public function __construct($dimention)
36
    {
37
        if ($dimention < 1) {
38
            throw new \LogicException("a space dimention cannot be null or negative");
39
        }
40
41
        $this->dimention = $dimention;
42
    }
43
44
    public static function setRng(callable $fn): void
45
    {
46
        static::$rng = $fn;
0 ignored issues
show
Documentation Bug introduced by
It seems like $fn of type callable is incompatible with the declared type string of property $rng.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
47
    }
48
49
    public function toArray(): array
50
    {
51
        $points = [];
52
        foreach ($this as $point) {
53
            $points[] = $point->toArray();
54
        }
55
56
        return ['points' => $points];
57
    }
58
59
    public function newPoint(array $coordinates): Point
60
    {
61
        if (count($coordinates) != $this->dimention) {
62
            throw new \LogicException("(" . implode(',', $coordinates) . ") is not a point of this space");
63
        }
64
65
        return new Point($this, $coordinates);
66
    }
67
68
    public function addPoint(array $coordinates, $data = null): Point
69
    {
70
        $this->attach($point = $this->newPoint($coordinates), $data);
71
72
        return $point;
73
    }
74
75
    public function attach($point, $data = null): void
76
    {
77
        if (!$point instanceof Point) {
78
            throw new \InvalidArgumentException("can only attach points to spaces");
79
        }
80
81
        parent::attach($point, $data);
82
    }
83
84
    public function getDimention(): int
85
    {
86
        return $this->dimention;
87
    }
88
89
    public function getBoundaries(): array
90
    {
91
        if (!count($this)) {
92
            return [];
93
        }
94
95
        $min = $this->newPoint(array_fill(0, $this->dimention, null));
96
        $max = $this->newPoint(array_fill(0, $this->dimention, null));
97
98
        foreach ($this as $point) {
99
            for ($n = 0; $n < $this->dimention; $n++) {
100 View Code Duplication
                if ($min[$n] === null || $min[$n] > $point[$n]) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
101
                    $min[$n] = $point[$n];
102
                }
103
104 View Code Duplication
                if ($max[$n] === null || $max[$n] < $point[$n]) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
105
                    $max[$n] = $point[$n];
106
                }
107
            }
108
        }
109
110
        return [$min, $max];
111
    }
112
113
    public function getRandomPoint(Point $min, Point $max): Point
114
    {
115
        $point = $this->newPoint(array_fill(0, $this->dimention, null));
116
        $rng = static::$rng;
117
118
        for ($n = 0; $n < $this->dimention; $n++) {
119
            $point[$n] = $rng($min[$n], $max[$n]);
120
        }
121
122
        return $point;
123
    }
124
125
    public function solve(int $nbClusters, callable $iterationCallback = null): array
126
    {
127
        // initialize K clusters
128
        $clusters = $this->initializeClusters($nbClusters);
129
130
        // there's only one cluster, clusterization has no meaning
131
        if (count($clusters) == 1) {
132
            return $clusters;
133
        }
134
135
        // until convergence is reached
136
        do {
137
            if ($iterationCallback) {
138
                $iterationCallback($this, $clusters);
139
            }
140
        } while ($this->iterate($clusters));
141
142
        // clustering is done.
143
        return $clusters;
144
    }
145
146
    protected function initializeClusters(int $nbClusters): array
147
    {
148
        if ($nbClusters <= 0) {
149
            throw new \InvalidArgumentException("invalid clusters number");
150
        }
151
152
        $clusters = [];
153
154
        // get the space boundaries to avoid placing clusters centroid too far from points
155
        list($min, $max) = $this->getBoundaries();
156
157
        // initialize N clusters with a random point within space boundaries
158
        for ($n = 0; $n < $nbClusters; $n++) {
159
            $clusters[] = new Cluster($this, $this->getRandomPoint($min, $max)->getCoordinates());
160
        }
161
162
        // assign all points to the first cluster
163
        $clusters[0]->attachAll($this);
164
165
        return $clusters;
166
    }
167
168
    protected function iterate(array $clusters): bool
169
    {
170
        $continue = false;
171
172
        // migration storages
173
        $attach = new \SplObjectStorage();
174
        $detach = new \SplObjectStorage();
175
176
        // calculate proximity amongst points and clusters
177
        foreach ($clusters as $cluster) {
178
            foreach ($cluster as $point) {
179
                // find the closest cluster
180
                $closest = $point->getClosest($clusters);
181
182
                // move the point from its old cluster to its closest
183
                if ($closest !== $cluster) {
184
                    if (! isset($attach[$closest])) {
185
                        $attach[$closest] = new \SplObjectStorage();
186
                    }
187
188
                    if (! isset($detach[$cluster])) {
189
                        $detach[$cluster] = new \SplObjectStorage();
190
                    }
191
192
                    $attach[$closest]->attach($point);
193
                    $detach[$cluster]->attach($point);
194
195
                    $continue = true;
196
                }
197
            }
198
        }
199
200
        // perform points migrations
201
        foreach ($attach as $cluster) {
202
            $cluster->attachAll($attach[$cluster]);
203
        }
204
205
        foreach ($detach as $cluster) {
206
            $cluster->detachAll($detach[$cluster]);
207
        }
208
209
        // update all cluster's centroids
210
        foreach ($clusters as $cluster) {
211
            $cluster->updateCentroid();
212
        }
213
214
        return $continue;
215
    }
216
}
217