diff --git a/Math/Topology/Metrics2D.php b/Math/Topology/Metrics2D.php index 709ccefb3..d047f3653 100644 --- a/Math/Topology/Metrics2D.php +++ b/Math/Topology/Metrics2D.php @@ -75,7 +75,7 @@ final class Metrics2D } /** - * Euclidean metric. + * Octile metric. * * @latex d(p, q) = \begin{cases}(\sqrt{2} - 1) \times |p_i - q_i| + |p_{i+1} - q_{i+1}|,& \text{if } |p_i - q_i| < |p_{i+1} - q_{i+1}|\\(\sqrt{2} - 1) \times |p_{i+1} - q_{i+1}| + |p_i - q_i|,&\text{if } |p_i - q_i| \geq |p_{i+1} - q_{i+1}|\end{cases} * @@ -188,7 +188,7 @@ final class Metrics2D */ public static function angularSeparation(array $a, array $b) : float { - return ($a['x'] * $b['x'] + $a['y'] * $b['y']) / pow(($a['x'] ** 2 + $a['y'] ** 2) * ($b['x'] ** 2 + $b['y'] ** 2), 1 / 2); + return ($a['x'] * $b['x'] + $a['y'] * $b['y']) / \pow(($a['x'] ** 2 + $a['y'] ** 2) * ($b['x'] ** 2 + $b['y'] ** 2), 1 / 2); } /** diff --git a/Math/Topology/MetricsND.php b/Math/Topology/MetricsND.php new file mode 100644 index 000000000..7f03af3f4 --- /dev/null +++ b/Math/Topology/MetricsND.php @@ -0,0 +1,256 @@ + $e) { + $dist += \abs($a[$key] - $b[$key]); + } + + return $dist; + } + + /** + * Euclidean metric. + * + * @latex d(p, q) = \sqrt{\sum_{n=1}^N{(p_i - q_i)^2}} + * + * @param array $a n-D array + * @param array $b n-D array + * + * @return float + * + * @since 1.0.0 + */ + public static function euclidean(array $a, array $b) : float + { + if (\count($a) !== \count($b)) { + throw new InvalidDimensionException(\count($a) . 'x' . \count($b)); + } + + $dist = 0.0; + foreach ($a as $key => $e) { + $dist += \abs($a[$key] - $b[$key]) ** 2; + } + + return \sqrt($dist); + } + + /** + * Chebyshev metric. + * + * @latex d(p, q) = \max_i{(|p_i - q_i|)} + * + * @param array $a n-D array + * @param array $b n-D array + * + * @return float + * + * @since 1.0.0 + */ + public static function chebyshev(array $a, array $b) : float + { + if (\count($a) !== \count($b)) { + throw new InvalidDimensionException(\count($a) . 'x' . \count($b)); + } + + $dist = []; + foreach ($a as $key => $e) { + $dist[] = \abs($a[$key] - $b[$key]); + } + + return \max($dist); + } + + /** + * Minkowski metric. + * + * @latex d(p, q) = \sqrt[\lambda]{\sum_{n=1}^N{|p_i - q_i|^\lambda}} + * + * @param array $a n-D array + * @param array $b n-D array + * @param int $lambda Lambda + * + * @return float + * + * @since 1.0.0 + */ + public static function minkowski(array $a, array $b, int $lambda) : float + { + if (\count($a) !== \count($b)) { + throw new InvalidDimensionException(\count($a) . 'x' . \count($b)); + } + + $dist = 0.0; + foreach ($a as $key => $e) { + $dist += \pow(\abs($a[$key] - $b[$key]), $lambda); + } + + return \pow($dist, 1 / $lambda); + } + + /** + * Canberra metric. + * + * @latex d(p, q) = \sum_{n=1}^N{\frac{|p_i - q_i|}{|p_i| + |q_i|} + * + * @param array $a n-D array + * @param array $b n-D array + * + * @return float + * + * @since 1.0.0 + */ + public static function canberra(array $a, array $b) : float + { + if (\count($a) !== \count($b)) { + throw new InvalidDimensionException(\count($a) . 'x' . \count($b)); + } + + $dist = 0.0; + foreach ($a as $key => $e) { + $dist += \abs($a[$key] - $b[$key]) / (\abs($a[$key]) + \abs($b[$key])); + } + + return $dist; + } + + /** + * Bray Curtis metric. + * + * @latex d(p, q) = \frac{\sum_{n=1}^N{|p_i - q_i|}}{\sum_{n=1}^N{(p_i + q_i)}} + * + * @param array $a n-D array + * @param array $b n-D array + * + * @return float + * + * @since 1.0.0 + */ + public static function brayCurtis(array $a, array $b) : float + { + if (\count($a) !== \count($b)) { + throw new InvalidDimensionException(\count($a) . 'x' . \count($b)); + } + + $distTop = 0.0; + $distBottom = 0.0; + foreach ($a as $key => $e) { + $distTop += \abs($a[$key] - $b[$key]); + $distBottom += $a[$key] + $b[$key]; + } + + return $distTop / $distBottom; + } + + /** + * Angular separation metric. + * + * @latex d(p, q) = \frac{\sum_{n=1}^N{p_i * q_i}}{\left(\sum_{n=1}^N{p_i^2} * \sum_{n=1}^N{q_i^2}\right)^\frac{1}{2}} + * + * @param array $a n-D array + * @param array $b n-D array + * + * @return float + * + * @since 1.0.0 + */ + public static function angularSeparation(array $a, array $b) : float + { + if (\count($a) !== \count($b)) { + throw new InvalidDimensionException(\count($a) . 'x' . \count($b)); + } + + $distTop = 0.0; + $distBottomA = 0.0; + $distBottomB = 0.0; + foreach ($a as $key => $e) { + $distTop += $a[$key] * $b[$key]; + $distBottomA += $a[$key] ** 2; + $distBottomB += $b[$key] ** 2; + } + + return $distTop / \pow($distBottomA * $distBottomB, 1 / 2); + } + + /** + * Hamming metric. + * + * @latex d(p, q) = \sum_{n=1}^N{|p_i - q_i|} + * + * @param array $a n-D array + * @param array $b n-D array + * + * @return int + * + * @since 1.0.0 + */ + public static function hamming(array $a, array $b) : int + { + if (($size = \count($a)) !== \count($b)) { + throw new InvalidDimensionException(\count($a) . 'x' . \count($b)); + } + + $dist = 0; + for ($i = 0; $i < $size; ++$i) { + if ($a[$i] !== $b[$i]) { + ++$dist; + } + } + + return $dist; + } +} diff --git a/tests/Math/Topology/MetricsNDTest.php b/tests/Math/Topology/MetricsNDTest.php new file mode 100644 index 000000000..4eaf779d0 --- /dev/null +++ b/tests/Math/Topology/MetricsNDTest.php @@ -0,0 +1,147 @@ + 0, 'y' => 3], ['x' => 7, 'y' => 6]), + MetricsND::manhattan(['x' => 0, 'y' => 3], ['x' => 7, 'y' => 6]) + ); + } + + /** + * @testdox The euclidean distance can be calculated + * @covers phpOMS\Math\Topology\MetricsND + * @group framework + */ + public function testEuclidean() : void + { + self::assertEqualsWithDelta( + Metrics2D::euclidean(['x' => 0, 'y' => 3], ['x' => 7, 'y' => 6]), + MetricsND::euclidean(['x' => 0, 'y' => 3], ['x' => 7, 'y' => 6]), + 0.1 + ); + } + + /** + * @testdox The chebyshev distance can be calculated + * @covers phpOMS\Math\Topology\MetricsND + * @group framework + */ + public function testChebyshev() : void + { + self::assertEquals( + MetricsND::chebyshev(['x' => 0, 'y' => 3], ['x' => 7, 'y' => 6]), + MetricsND::chebyshev(['x' => 0, 'y' => 3], ['x' => 7, 'y' => 6]) + ); + } + + /** + * @testdox The minkowski distance can be calculated + * @covers phpOMS\Math\Topology\MetricsND + * @group framework + */ + public function testMinkowski() : void + { + self::assertEqualsWithDelta( + Metrics2D::minkowski(['x' => 0, 'y' => 3], ['x' => 7, 'y' => 6], 3), + MetricsND::minkowski(['x' => 0, 'y' => 3], ['x' => 7, 'y' => 6], 3), + 0.1 + ); + } + + /** + * @testdox The canberra distance can be calculated + * @covers phpOMS\Math\Topology\MetricsND + * @group framework + */ + public function testCanberra() : void + { + self::assertEqualsWithDelta( + Metrics2D::canberra(['x' => 0, 'y' => 3], ['x' => 7, 'y' => 6]), + MetricsND::canberra(['x' => 0, 'y' => 3], ['x' => 7, 'y' => 6]), + 0.1 + ); + } + + /** + * @testdox The bray-curtis distance can be calculated + * @covers phpOMS\Math\Topology\MetricsND + * @group framework + */ + public function testBrayCurtis() : void + { + self::assertEqualsWithDelta( + Metrics2D::brayCurtis(['x' => 0, 'y' => 3], ['x' => 7, 'y' => 6]), + MetricsND::brayCurtis(['x' => 0, 'y' => 3], ['x' => 7, 'y' => 6]), + 0.1 + ); + } + + /** + * @testdox The angular distance can be calculated + * @covers phpOMS\Math\Topology\MetricsND + * @group framework + */ + public function testAngularSeparation() : void + { + self::assertEqualsWithDelta( + Metrics2D::angularSeparation(['x' => 0, 'y' => 3], ['x' => 7, 'y' => 6]), + MetricsND::angularSeparation(['x' => 0, 'y' => 3], ['x' => 7, 'y' => 6]), + 0.1 + ); + } + + /** + * @testdox The hamming distance can be calculated + * @covers phpOMS\Math\Topology\MetricsND + * @group framework + */ + public function testHammingDistance() : void + { + self::assertEquals( + MetricsND::hamming([1, 1, 1, 1], [0, 1, 0, 0]), + MetricsND::hamming([1, 1, 1, 1], [0, 1, 0, 0]), + ); + } + + /** + * @testdox Different dimension sizes for the coordinates in the hamming metric throw a InvalidDimensionException + * @covers phpOMS\Math\Topology\MetricsND + * @group framework + */ + public function testInvalidHammingDimension() : void + { + self::expectException(\phpOMS\Math\Matrix\Exception\InvalidDimensionException::class); + + Metrics2D::ulam([3, 6, 4], [4, 6, 8, 3]); + } +}