Skip to content

Commit 2844c3b

Browse files
committed
adding tests for Math GPS formulas
1 parent 3622d9f commit 2844c3b

File tree

6 files changed

+142
-138
lines changed

6 files changed

+142
-138
lines changed

src/Math.php

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,11 @@ public static function gaussianNoise(float $mu, float $sigma): array
6666
return [$z0, $z1];
6767
}
6868

69-
public static int $earthRadius = 6371000;
69+
public static int $earthRadius = 6371009; // meters
7070

7171
/**
72-
* Calculates the great-circle distance between two points, with
73-
* the Haversine formula.
72+
* Calculates the great-circle distance (in meters) between two points,
73+
* with the Haversine formula.
7474
*
7575
* @see https://stackoverflow.com/a/14751773/17403258
7676
*
@@ -88,7 +88,7 @@ public static function haversine($from, $to): float
8888
}
8989

9090
/**
91-
* Calculates the centroid of GPS coordinates
91+
* Calculates the centroid of GPS coordinates.
9292
*
9393
* @see https://stackoverflow.com/questions/6671183
9494
*
@@ -105,7 +105,7 @@ public static function gpsCentroid(array $points): array
105105

106106
foreach ($points as $point) {
107107
$lat = deg2rad($point[0]);
108-
$long = deg2rad($point[0]);
108+
$long = deg2rad($point[1]);
109109

110110
$x += cos($lat) * cos($long);
111111
$y += cos($lat) * sin($long);

tests/Data/boundaries_2d.csv

Lines changed: 0 additions & 100 deletions
This file was deleted.

tests/Data/gps_centroid.csv

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
"Paris, Lyon, Marseille",45.9784058082879,4.226770011911983,48.85889,2.32004,45.75781,4.83201,43.29617,5.36995
2+
"Single point",48.85889,2.32004,48.85889,2.32004
3+
"5 close points",43.29619000000861,5.369947999981911,43.29617,5.36995,43.29616,5.36987,43.29625,5.36998,43.29621,5.37000,43.29616,5.36994

tests/Data/gps_centroid.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import numpy as np
2+
import numpy.linalg as lin
3+
4+
E = np.array([[0, 0, 1],
5+
[0, 1, 0],
6+
[-1, 0, 0]])
7+
8+
def lat_long2n_E(latitude,longitude):
9+
res = [np.sin(np.deg2rad(latitude)),
10+
np.sin(np.deg2rad(longitude)) * np.cos(np.deg2rad(latitude)),
11+
-np.cos(np.deg2rad(longitude)) * np.cos(np.deg2rad(latitude))]
12+
return np.dot(E.T,np.array(res))
13+
14+
def n_E2lat_long(n_E):
15+
n_E = np.dot(E, n_E)
16+
longitude=np.arctan2(n_E[1],-n_E[2]);
17+
equatorial_component = np.sqrt(n_E[1]**2 + n_E[2]**2 );
18+
latitude=np.arctan2(n_E[0],equatorial_component);
19+
return np.rad2deg(latitude), np.rad2deg(longitude)
20+
21+
def average(coords):
22+
res = []
23+
for lat,lon in coords:
24+
res.append(lat_long2n_E(lat,lon))
25+
res = np.array(res)
26+
m = np.mean(res,axis=0)
27+
m = m / lin.norm(m)
28+
return n_E2lat_long(m)
29+
30+
31+
#paris = [48.85889,2.32004]
32+
#lyon = [45.75781,4.83201]
33+
#marseille = [43.29617,5.36995]
34+
#
35+
## 45.9784058082879, 4.226770011911983
36+
#print (average([paris, lyon, marseille]))
37+
38+
print(average([
39+
[43.29617,5.36995],
40+
[43.29616,5.36987],
41+
[43.29625,5.36998],
42+
[43.29621,5.37000],
43+
[43.29616,5.36994]
44+
]))

tests/Data/haversine_distances.csv

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
"Paris - New York",48.864716,2.349014,40.7128,74.0060,5514741.115351569
2+
"Paris - Neuilly",48.864716,2.349014,48.8848,2.2685,6297.56948974873
3+
"Paris - Paris",48.864716,2.349014,48.864716,2.349014,0.0
4+
"North Pole - South Pole",90.0,0.0,-90.0,0.0,20015114.442035925
5+
"Two very close points",48.85323,2.34903,48.85321,2.34902,2.3411651390339396

tests/Unit/MathTest.php

Lines changed: 85 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111
*/
1212
class MathTest extends TestCase
1313
{
14+
// ------------------------------------------------------------------------
15+
// Euclidean Distance
16+
1417
/**
1518
* @covers ::euclideanDist
1619
* @dataProvider euclidianDistanceDataProvider
@@ -29,25 +32,28 @@ public function testEuclideanDist(array $a, array $b, float $dist): void
2932
public function euclidianDistanceDataProvider(): \Generator
3033
{
3134
/** @var array<string> $row */
32-
foreach ($this->openCsv('euclidean_distances_2d.csv') as $row) {
35+
foreach ($this->readCsv('euclidean_distances_2d') as $row) {
3336
list($x1, $y1, $x2, $y2, $dist) = array_map('floatval', $row);
3437
yield [[$x1, $y1], [$x2, $y2], $dist];
3538
}
3639

3740
/** @var array<string> $row */
38-
foreach ($this->openCsv('euclidean_distances_3d.csv') as $row) {
41+
foreach ($this->readCsv('euclidean_distances_3d') as $row) {
3942
list($x1, $y1, $z1, $x2, $y2, $z2, $dist) = array_map('floatval', $row);
4043
yield [[$x1, $y1, $z1], [$x2, $y2, $z2], $dist];
4144
}
4245
}
4346

47+
// ------------------------------------------------------------------------
48+
// Centroid
49+
4450
/**
4551
* @covers ::centroid
4652
* @dataProvider centroidDataProvider
4753
* @param array<float> $centroid
4854
* @param array<float> ...$points
4955
*/
50-
public function testFindCentroid(array $centroid, array ...$points): void
56+
public function testCentroid(array $centroid, array ...$points): void
5157
{
5258
$this->assertEquals($centroid, Math::centroid($points));
5359
}
@@ -58,43 +64,20 @@ public function testFindCentroid(array $centroid, array ...$points): void
5864
public function centroidDataProvider(): \Generator
5965
{
6066
/** @var array<string> $row */
61-
foreach ($this->openCsv('centroids_2d.csv') as $row) {
67+
foreach ($this->readCsv('centroids_2d') as $row) {
6268
list($x1, $y1, $x2, $y2, $x3, $y3, $x4, $y4, $cx, $cy) = array_map('floatval', $row);
6369
yield [[$cx, $cy], [$x1, $y1], [$x2, $y2], [$x3, $y3], [$x4, $y4]];
6470
}
6571
}
6672

67-
/**
68-
* @return \Generator<array<array<float>>>
69-
*/
70-
public function boundariesDataProvider(): \Generator
71-
{
72-
/** @var array<string> $row */
73-
foreach ($this->openCsv('boundaries_2d.csv') as $row) {
74-
list($x1, $y1, $x2, $y2, $x3, $y3, $x4, $y4, $x5, $y5, $ax, $ay, $bx, $by) = array_map('floatval', $row);
75-
yield [[$ax, $ay], [$bx, $by], [$x1, $y1], [$x2, $y2], [$x3, $y3], [$x4, $y4], [$x5, $y5]];
76-
}
77-
}
78-
79-
/**
80-
* @return array<mixed>
81-
*/
82-
public function frandDataProvider(): array
83-
{
84-
return [
85-
['min' => 0, 'max' => 1],
86-
['min' => 10, 'max' => 20],
87-
['min' => 0, 'max' => 100],
88-
['min' => -100, 'max' => 100],
89-
['min' => -1e6, 'max' => 1e6],
90-
];
91-
}
73+
// ------------------------------------------------------------------------
74+
// Gaussian Noise
9275

9376
/**
9477
* @covers ::gaussianNoise
9578
* @dataProvider gaussianNoiseDataProvider
9679
*/
97-
public function testGenerateGaussianNoise(float $mu, float $sigma = 1, float $nb = 1e3): void
80+
public function testGaussianNoise(float $mu, float $sigma = 1, float $nb = 1e3): void
9881
{
9982
// let's generate $nb numbers and sum them
10083
for ($sum = 0, $i = 0; $i < $nb; $i++) {
@@ -125,10 +108,79 @@ public function gaussianNoiseDataProvider(): array
125108
];
126109
}
127110

128-
private static function openCsv(string $path): \SplFileObject
111+
// ------------------------------------------------------------------------
112+
// Haversine
113+
114+
/**
115+
* @covers ::haversine
116+
* @dataProvider haversineDataProvider
117+
* @param array{0: float, 1: float} $from
118+
* @param array{0: float, 1: float} $to
119+
*/
120+
public function testHaversine(string $label, array $from, array $to, float $expected): void
121+
{
122+
$obtained = Math::haversine($from, $to);
123+
124+
$this->assertLessThan(
125+
1, // meter
126+
$obtained - $expected,
127+
"Haversine distance for $label should be around $expected meters",
128+
);
129+
}
130+
131+
public function haversineDataProvider(): \Generator
132+
{
133+
/** @var array<string> $row */
134+
foreach ($this->readCsv('haversine_distances') as $row) {
135+
$label = array_shift($row);
136+
$row = array_map('floatval', $row);
137+
yield [$label, [$row[0], $row[1]], [$row[2], $row[3]], $row[4]];
138+
}
139+
}
140+
141+
// ------------------------------------------------------------------------
142+
// GPS Centroid
143+
144+
/**
145+
* @covers ::gpsCentroid
146+
* @uses \Kmeans\Math::haversine
147+
* @dataProvider gpsCentroidDataProvider
148+
* @param array{0: float, 1: float} $expected
149+
* @param array<array{0: float, 1: float}> $points
150+
*/
151+
public function testGpsCentroid(string $label, array $expected, array $points): void
129152
{
130-
$csv = new \SplFileObject(__DIR__ . '/../Data/' . $path);
131-
$csv->setFlags(\SplFileObject::READ_CSV | \SplFileObject::SKIP_EMPTY | \SplFileObject::READ_AHEAD);
153+
$obtained = Math::gpsCentroid($points);
154+
155+
$this->assertLessThan(
156+
1,
157+
Math::haversine($expected, $obtained),
158+
"Centroid of $label should be near " . implode(', ', $expected),
159+
);
160+
}
161+
162+
public function gpsCentroidDataProvider(): \Generator
163+
{
164+
/** @var array<string> $row */
165+
foreach ($this->readCsv('gps_centroid') as $row) {
166+
$label = array_shift($row);
167+
$points = array_chunk(array_map('floatval', $row), 2);
168+
yield [$label, array_shift($points), $points];
169+
}
170+
}
171+
172+
// ------------------------------------------------------------------------
173+
// Helpers
174+
175+
private static function readCsv(string $path): \SplFileObject
176+
{
177+
$csv = new \SplFileObject(__DIR__ . "/../Data/{$path}.csv");
178+
179+
$csv->setFlags(
180+
\SplFileObject::READ_CSV |
181+
\SplFileObject::SKIP_EMPTY |
182+
\SplFileObject::READ_AHEAD
183+
);
132184

133185
return $csv;
134186
}

0 commit comments

Comments
 (0)