Skip to content

Commit

Permalink
permutable point distance algorithm #12
Browse files Browse the repository at this point in the history
  • Loading branch information
bdelespierre committed May 19, 2021
1 parent 8695727 commit a4f3b16
Show file tree
Hide file tree
Showing 10 changed files with 180 additions and 24 deletions.
13 changes: 11 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,16 @@
}
},
"scripts": {
"test": "vendor/bin/phpunit",
"test-coverage": "vendor/bin/phpunit --coverage-html coverage"
"test": [
"vendor/bin/phpunit --color=always"
],
"test:coverage": [
"@putenv XDEBUG_MODE=coverage",
"vendor/bin/phpunit --color=always --coverage-clover=\"build/code-coverage/clover.xml\""
],
"test:coverage-html": [
"@putenv XDEBUG_MODE=coverage",
"vendor/bin/phpunit --color=always --coverage-html=\"build/code-coverage\""
]
}
}
21 changes: 21 additions & 0 deletions src/KMeans/Algorithms/CallbackDistance.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

namespace KMeans\Algorithms;

use KMeans\Interfaces\DistanceAlgorithmInterface;
use KMeans\Interfaces\PointInterface;

class CallbackDistance implements DistanceAlgorithmInterface
{
private $fn;

public function __construct(callable $fn)
{
$this->fn = $fn;
}

public function getDistanceBetween(PointInterface $pointA, PointInterface $pointB): float
{
return call_user_func($this->fn, $pointA, $pointB);
}
}
30 changes: 30 additions & 0 deletions src/KMeans/Algorithms/EuclidianDistance.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

namespace KMeans\Algorithms;

use KMeans\Interfaces\DistanceAlgorithmInterface;
use KMeans\Interfaces\PointInterface;

class EuclidianDistance implements DistanceAlgorithmInterface
{
public function getDistanceBetween(PointInterface $pointA, PointInterface $pointB): float
{
if ($pointA->getSpace()->getDimention() !== $pointB->getSpace()->getDimention()) {
throw new \LogicException(
"Cannot calculate euclidian distance between point of different dimentions"
);
}

$distance = 0;
$dimention = $pointA->getSpace()->getDimention();
$coordinatesA = $pointA->getCoordinates();
$coordinatesB = $pointB->getCoordinates();

for ($n = 0; $n < $dimention; $n++) {
$difference = $coordinatesA[$n] - $coordinatesB[$n];
$distance += $difference ** 2;
}

return sqrt($distance);
}
}
7 changes: 4 additions & 3 deletions src/KMeans/Cluster.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,15 +82,16 @@ public function updateCentroid(): void
return;
}

$centroid = $this->space->newPoint(array_fill(0, $this->dimention, 0));
$dimention = $this->space->getDimention();
$centroid = $this->space->newPoint(array_fill(0, $dimention, 0));

foreach ($this->points as $point) {
for ($n = 0; $n < $this->dimention; $n++) {
for ($n = 0; $n < $dimention; $n++) {
$centroid->coordinates[$n] += $point->coordinates[$n];
}
}

for ($n = 0; $n < $this->dimention; $n++) {
for ($n = 0; $n < $dimention; $n++) {
$this->coordinates[$n] = $centroid->coordinates[$n] / $count;
}
}
Expand Down
10 changes: 10 additions & 0 deletions src/KMeans/Interfaces/DistanceAlgorithmInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

namespace KMeans\Interfaces;

use KMeans\Interfaces\PointInterface;

interface DistanceAlgorithmInterface
{
public function getDistanceBetween(PointInterface $pointA, PointInterface $pointB): float;
}
15 changes: 15 additions & 0 deletions src/KMeans/Interfaces/PointInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

namespace KMeans\Interfaces;

use KMeans\Interfaces\DistanceAlgorithmInterface;
use KMeans\Interfaces\SpaceInterface;

interface PointInterface
{
public function getSpace(): SpaceInterface;

public function getCoordinates(): array;

public function setDistanceAlgorithm(DistanceAlgorithmInterface $algo): void;
}
12 changes: 12 additions & 0 deletions src/KMeans/Interfaces/SpaceInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

namespace KMeans\Interfaces;

use KMeans\Interfaces\DistanceAlgorithmInterface;

interface SpaceInterface
{
public function getDimention(): int;

public function setDistanceAlgorithm($algo): void;
}
34 changes: 17 additions & 17 deletions src/KMeans/Point.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,27 @@

namespace KMeans;

class Point implements \ArrayAccess
use KMeans\Algorithms\EuclidianDistance;
use KMeans\Interfaces\DistanceAlgorithmInterface;
use KMeans\Interfaces\PointInterface;
use KMeans\Interfaces\SpaceInterface;

class Point implements PointInterface, \ArrayAccess
{
protected $space;
protected $dimention;
protected $coordinates;
protected $distanceAlgo;

public function __construct(Space $space, array $coordinates)
public function __construct(SpaceInterface $space, array $coordinates, DistanceAlgorithmInterface $algo = null)
{
$this->space = $space;
$this->dimention = $space->getDimention();
$this->space = $space;
$this->coordinates = $coordinates;
$this->setDistanceAlgorithm($algo ?: new EuclidianDistance());
}

public function setDistanceAlgorithm(DistanceAlgorithmInterface $algo): void
{
$this->distanceAlgo = $algo;
}

public function toArray(): array
Expand All @@ -47,19 +57,9 @@ public function toArray(): array
];
}

public function getDistanceWith(self $point, bool $precise = true): float
public function getDistanceWith(self $point): float
{
if ($point->space !== $this->space) {
throw new \LogicException("can only calculate distances from points in the same space");
}

$distance = 0;
for ($n = 0; $n < $this->dimention; $n++) {
$difference = $this->coordinates[$n] - $point->coordinates[$n];
$distance += $difference * $difference;
}

return $precise ? sqrt($distance) : $distance;
return $this->distanceAlgo->getDistanceBetween($this, $point);
}

public function getClosest(iterable $points): Point
Expand Down
30 changes: 28 additions & 2 deletions src/KMeans/Space.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,15 @@

namespace KMeans;

class Space extends \SplObjectStorage
use KMeans\Algorithms\CallbackDistance;
use KMeans\Algorithms\EuclidianDistance;
use KMeans\Interfaces\DistanceAlgorithmInterface;
use KMeans\Interfaces\SpaceInterface;

class Space extends \SplObjectStorage implements SpaceInterface
{
protected $dimention;
protected $distanceAlgo;

protected static $rng = 'mt_rand';

Expand All @@ -39,13 +45,33 @@ public function __construct($dimention)
}

$this->dimention = $dimention;
$this->setDistanceAlgorithm(new EuclidianDistance());
}

public static function setRng(callable $fn): void
{
static::$rng = $fn;
}

public function setDistanceAlgorithm($algo): void
{
if (is_callable($algo) && ! $algo instanceof DistanceAlgorithmInterface) {
$algo = new CallbackDistance($algo);
}

if (! $algo instanceof DistanceAlgorithmInterface) {
throw new \RuntimeException(
"distance algorithm must implement KMeans\Interfaces\DistanceAlgorithmInterface"
);
}

$this->distanceAlgo = $algo;

foreach ($this as $point) {
$point->setDistanceAlgorithm($algo);
}
}

public function toArray(): array
{
$points = [];
Expand All @@ -62,7 +88,7 @@ public function newPoint(array $coordinates): Point
throw new \LogicException("(" . implode(',', $coordinates) . ") is not a point of this space");
}

return new Point($this, $coordinates);
return new Point($this, $coordinates, $this->distanceAlgo);
}

public function addPoint(array $coordinates, $data = null): Point
Expand Down
32 changes: 32 additions & 0 deletions tests/Kmeans/SpaceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Tests\Kmeans;

use KMeans\Algorithms\CallbackDistance;
use KMeans\Point;
use KMeans\Space;
use PHPUnit\Framework\TestCase;
Expand All @@ -25,6 +26,37 @@ public function testConstructException()
new Space(-1);
}

public function testSetDistanceAlgorithm()
{
$space = new Space(2);

$a = $space->addPoint([1,1]);
$b = $space->addPoint([2,2]);

// bogus dist. alg. for testing
$space->setDistanceAlgorithm(function ($a, $b) {
return -1;
});

$this->assertEquals(
-1,
$a->getDistanceWith($b)
);
}

public function testSetDistanceAlgorithmFails()
{
$this->expectException(\RuntimeException::class);

$space = new Space(2);

$a = $space->addPoint([1,1]);
$b = $space->addPoint([2,2]);

// bogus dist. alg. for testing
$space->setDistanceAlgorithm("hello!");
}

public function testToArray()
{
$space = new Space(2);
Expand Down

0 comments on commit a4f3b16

Please sign in to comment.