Skip to content

Commit 2e0d972

Browse files
committed
adding GPS clustering algorithm using Haversine formula
1 parent fc10db3 commit 2e0d972

19 files changed

+613
-232
lines changed

src/Algorithm.php

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,23 +25,15 @@ public function __construct(InitializationSchemeInterface $initScheme)
2525
$this->initScheme = $initScheme;
2626
}
2727

28-
abstract protected function getDistanceBetween(PointInterface $pointA, PointInterface $pointB): float;
29-
30-
abstract protected function findCentroid(PointCollectionInterface $points): PointInterface;
31-
3228
public function registerIterationCallback(callable $callback): void
3329
{
3430
$this->iterationCallbacks[] = $callback;
3531
}
3632

3733
public function clusterize(PointCollectionInterface $points, int $nbClusters): ClusterCollectionInterface
3834
{
39-
try {
40-
// initialize clusters
41-
$clusters = $this->initScheme->initializeClusters($points, $nbClusters);
42-
} catch (\Exception $e) {
43-
throw new \RuntimeException("Cannot initialize clusters", 0, $e);
44-
}
35+
// initialize clusters
36+
$clusters = $this->initScheme->initializeClusters($points, $nbClusters);
4537

4638
// iterate until convergence is reached
4739
do {

src/ClusterCollection.php

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,17 @@ public function __construct(SpaceInterface $space, array $clusters = [])
2929
}
3030
}
3131

32+
// ------------------------------------------------------------------------
33+
// ClusterCollectionInterface
34+
3235
public function contains(ClusterInterface $cluster): bool
3336
{
3437
return $this->clusters->contains($cluster);
3538
}
3639

3740
public function attach(ClusterInterface $cluster): void
3841
{
39-
if ($cluster->getCentroid()->getSpace() !== $this->getSpace()) {
42+
if (! $this->getSpace()->isEqualTo($cluster->getSpace())) {
4043
throw new \InvalidArgumentException(
4144
"Cannot add cluster to collection: cluster space is not same as collection space"
4245
);
@@ -50,6 +53,9 @@ public function detach(ClusterInterface $cluster): void
5053
$this->clusters->detach($cluster);
5154
}
5255

56+
// ------------------------------------------------------------------------
57+
// Iterator
58+
5359
public function current()
5460
{
5561
return $this->clusters->current();
@@ -75,6 +81,9 @@ public function valid(): bool
7581
return $this->clusters->valid();
7682
}
7783

84+
// ------------------------------------------------------------------------
85+
// Countable
86+
7887
public function count(): int
7988
{
8089
return count($this->clusters);

src/Concerns/HasDataTrait.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
namespace Kmeans\Concerns;
4+
5+
trait HasDataTrait
6+
{
7+
/**
8+
* @var mixed
9+
*/
10+
private $data;
11+
12+
/**
13+
* @return mixed
14+
*/
15+
public function getData()
16+
{
17+
return $this->data;
18+
}
19+
20+
/**
21+
* @param mixed $data
22+
*/
23+
public function setData($data): void
24+
{
25+
$this->data = $data;
26+
}
27+
}

src/Euclidean/Algorithm.php

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,26 @@
99

1010
class Algorithm extends BaseAlgorithm
1111
{
12-
protected function getDistanceBetween(PointInterface $pointA, PointInterface $pointB): float
12+
public function getDistanceBetween(PointInterface $pointA, PointInterface $pointB): float
1313
{
14+
if (! $pointA instanceof Point || ! $pointB instanceof Point) {
15+
throw new \InvalidArgumentException(
16+
"Euclidean Algorithm can only calculate distance between euclidean points"
17+
);
18+
}
19+
1420
return Math::euclideanDist($pointA->getCoordinates(), $pointB->getCoordinates());
1521
}
1622

17-
protected function findCentroid(PointCollectionInterface $points): PointInterface
23+
public function findCentroid(PointCollectionInterface $points): PointInterface
1824
{
19-
return new Point($points->getSpace(), Math::centroid(
25+
if (! $points->getSpace() instanceof Space) {
26+
throw new \InvalidArgumentException(
27+
"Point collection should consist of Euclidean points"
28+
);
29+
}
30+
31+
return $points->getSpace()->makePoint(Math::centroid(
2032
array_map(fn (PointInterface $point) => $point->getCoordinates(), iterator_to_array($points))
2133
));
2234
}

src/Euclidean/Point.php

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,32 +2,29 @@
22

33
namespace Kmeans\Euclidean;
44

5+
use Kmeans\Concerns\HasDataTrait;
56
use Kmeans\Concerns\HasSpaceTrait;
67
use Kmeans\Interfaces\PointInterface;
78
use Kmeans\Interfaces\SpaceInterface;
89

910
class Point implements PointInterface
1011
{
1112
use HasSpaceTrait;
13+
use HasDataTrait;
1214

1315
/**
1416
* @var array<float>
1517
*/
1618
private array $coordinates;
1719

18-
/**
19-
* @var mixed
20-
*/
21-
private $data;
22-
2320
/**
2421
* @param array<int, float> $coordinates
2522
*/
2623
public function __construct(SpaceInterface $space, array $coordinates)
2724
{
2825
if (! $space instanceof Space) {
2926
throw new \LogicException(
30-
"An euclidean point must belong to an euclidean space."
27+
"An euclidean point must belong to an euclidean space"
3128
);
3229
}
3330

@@ -40,16 +37,6 @@ public function getCoordinates(): array
4037
return $this->coordinates;
4138
}
4239

43-
public function getData()
44-
{
45-
return $this->data;
46-
}
47-
48-
public function setData($data): void
49-
{
50-
$this->data = $data;
51-
}
52-
5340
/**
5441
* @param array<float> $coordinates
5542
* @return array<float>

src/Euclidean/Space.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace Kmeans\Euclidean;
44

5+
use Kmeans\Interfaces\PointInterface;
56
use Kmeans\Interfaces\SpaceInterface;
67

78
class Space implements SpaceInterface
@@ -32,4 +33,12 @@ public function isEqualTo(SpaceInterface $space): bool
3233
return $space instanceof self
3334
&& $this->dimensions == $space->dimensions;
3435
}
36+
37+
/**
38+
* @param array<float> $coordinates
39+
*/
40+
public function makePoint(array $coordinates): PointInterface
41+
{
42+
return new Point($this, $coordinates);
43+
}
3544
}

src/Gps/Algorithm.php

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,30 +9,30 @@
99

1010
class Algorithm extends BaseAlgorithm
1111
{
12-
protected function getDistanceBetween(PointInterface $pointA, PointInterface $pointB): float
12+
public function getDistanceBetween(PointInterface $pointA, PointInterface $pointB): float
1313
{
1414
if (! $pointA instanceof Point || ! $pointB instanceof Point) {
1515
throw new \InvalidArgumentException(
16-
"Expecting \\Kmeans\\GPS\\Point"
16+
"GPS algorithm can only calculate distance from GPS locations"
1717
);
1818
}
1919

2020
return Math::haversine($pointA->getCoordinates(), $pointB->getCoordinates());
2121
}
2222

23-
protected function findCentroid(PointCollectionInterface $points): PointInterface
23+
public function findCentroid(PointCollectionInterface $points): PointInterface
2424
{
2525
if (! $points->getSpace() instanceof Space) {
2626
throw new \InvalidArgumentException(
2727
"Point collection should consist of GPS coordinates"
2828
);
2929
}
3030

31-
/** @var array<Point> $points */
32-
$points = iterator_to_array($points);
31+
/** @var array<Point> $pointsArray */
32+
$pointsArray = iterator_to_array($points);
3333

34-
return new Point(Math::gpsCentroid(
35-
array_map(fn (Point $point) => $point->getCoordinates(), $points)
34+
return $points->getSpace()->makePoint(Math::gpsCentroid(
35+
array_map(fn (Point $point) => $point->getCoordinates(), $pointsArray)
3636
));
3737
}
3838
}

src/Gps/Point.php

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,36 +2,40 @@
22

33
namespace Kmeans\Gps;
44

5-
use Kmeans\Euclidean\Point as BasePoint;
5+
use Kmeans\Concerns\HasDataTrait;
6+
use Kmeans\Concerns\HasSpaceTrait;
7+
use Kmeans\Interfaces\PointInterface;
68

79
/**
810
* @method array{0: float, 1: float} getCoordinates()
911
*/
10-
class Point extends BasePoint
12+
class Point implements PointInterface
1113
{
12-
/**
13-
* @param array<float> $coordinates
14-
*/
15-
public function __construct(array $coordinates)
16-
{
17-
$this->validateCoordinates($coordinates);
14+
use HasDataTrait;
15+
use HasSpaceTrait;
16+
17+
private float $lat;
1818

19-
parent::__construct(new Space(), $coordinates);
19+
private float $long;
20+
21+
public function __construct(float $lat, float $long)
22+
{
23+
$this->validateCoordinates($lat, $long);
24+
$this->setSpace(Space::singleton());
25+
$this->lat = $lat;
26+
$this->long = $long;
2027
}
2128

2229
/**
23-
* @param array<float> $coordinates
30+
* @return array{0: float, 1: float}
2431
*/
25-
private function validateCoordinates(array $coordinates): void
32+
public function getCoordinates(): array
2633
{
27-
if (count($coordinates) != 2) {
28-
throw new \InvalidArgumentException(
29-
"Invalid GPS coordinates"
30-
);
31-
}
32-
33-
list($lat, $long) = $coordinates;
34+
return [$this->lat, $this->long];
35+
}
3436

37+
private function validateCoordinates(float $lat, float $long): void
38+
{
3539
if ($lat < -90 || $lat > 90 || $long < -180 || $long > 180) {
3640
throw new \InvalidArgumentException(
3741
"Invalid GPS coordinates"

src/Gps/Space.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,28 @@
22

33
namespace Kmeans\Gps;
44

5+
use Kmeans\Interfaces\PointInterface;
56
use Kmeans\Interfaces\SpaceInterface;
67

78
class Space implements SpaceInterface
89
{
10+
public static function singleton(): self
11+
{
12+
static $space = new self();
13+
14+
return $space;
15+
}
16+
917
public function isEqualTo(SpaceInterface $other): bool
1018
{
1119
return $other instanceof self;
1220
}
21+
22+
/**
23+
* @param array{0: float, 1: float} $coordinates
24+
*/
25+
public function makePoint(array $coordinates): PointInterface
26+
{
27+
return new Point(...$coordinates);
28+
}
1329
}

src/Interfaces/AlgorithmInterface.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,8 @@
55
interface AlgorithmInterface
66
{
77
public function clusterize(PointCollectionInterface $points, int $nbClusters): ClusterCollectionInterface;
8+
9+
public function getDistanceBetween(PointInterface $pointA, PointInterface $pointB): float;
10+
11+
public function findCentroid(PointCollectionInterface $points): PointInterface;
812
}

0 commit comments

Comments
 (0)