diff options
author | Peter John Acklam <pjacklam@gmail.com> | 2022-09-01 20:42:35 +0200 |
---|---|---|
committer | James E Keenan <jkeenan@cpan.org> | 2023-01-01 01:18:24 +0000 |
commit | 2e35afaa9f9ce8fc52aa15b30305e1343cc386af (patch) | |
tree | 17d62decb3fd8701346f9a9cdb3faca2ce0aa6ff /dist | |
parent | 7eba531be05d7470ade04ba4a80179f53cd4d871 (diff) | |
download | perl-2e35afaa9f9ce8fc52aa15b30305e1343cc386af.tar.gz |
Math-Complex: improve numerical accuracy
- Compute the great circle distance with a formula that has better
numerical properties. The formula used now is accurate for all
distances.
- Add a few tests to verify.
- This fixes CPAN RT #78938
Improve documentation for great_circle_distance()
For: https://github.com/Perl/perl5/pull/20212
Diffstat (limited to 'dist')
-rw-r--r-- | dist/Math-Complex/lib/Math/Trig.pm | 46 | ||||
-rw-r--r-- | dist/Math-Complex/t/Trig.t | 12 |
2 files changed, 33 insertions, 25 deletions
diff --git a/dist/Math-Complex/lib/Math/Trig.pm b/dist/Math-Complex/lib/Math/Trig.pm index 4bd90fbd67..bb96b5e69d 100644 --- a/dist/Math-Complex/lib/Math/Trig.pm +++ b/dist/Math-Complex/lib/Math/Trig.pm @@ -154,12 +154,19 @@ sub great_circle_distance { $rho = 1 unless defined $rho; # Default to the unit sphere. - my $lat0 = pip2 - $phi0; - my $lat1 = pip2 - $phi1; - - return $rho * - acos_real( cos( $lat0 ) * cos( $lat1 ) * cos( $theta0 - $theta1 ) + - sin( $lat0 ) * sin( $lat1 ) ); + my $dphi = $phi1 - $phi0; + my $dtheta = $theta1 - $theta0; + + # A formula that is accurate for all distances is the following special + # case of the Vincenty formula for an ellipsoid with equal major and minor + # axes. See + # https://en.wikipedia.org/wiki/Great-circle_distance#Computational_formulas + + my $c1 = sin($phi1) * sin($dtheta); + my $c2 = sin($phi1) * cos($dtheta); + my $c3 = sin($phi0) * cos($phi1) - cos($phi0) * $c2; + my $c4 = cos($phi0) * cos($phi1) + sin($phi0) * $c2; + return $rho * atan2(sqrt($c1 * $c1 + $c3 * $c3), $c4); } sub great_circle_direction { @@ -538,26 +545,19 @@ points. =head2 great_circle_distance -You can compute spherical distances, called B<great circle distances>, -by importing the great_circle_distance() function: - - use Math::Trig 'great_circle_distance'; +Returns the great circle distance between two points on a sphere. - $distance = great_circle_distance($theta0, $phi0, $theta1, $phi1, [, $rho]); + $distance = great_circle_distance($theta0, $phi0, $theta1, $phi1, [, $rho]); -The I<great circle distance> is the shortest distance between two -points on a sphere. The distance is in C<$rho> units. The C<$rho> is -optional, it defaults to 1 (the unit sphere), therefore the distance -defaults to radians. +Where ($theta0, $phi0) and ($theta1, $phi1) are the spherical coordinates of +the two points, respectively. The distance is in C<$rho> units. The C<$rho> +is optional. It defaults to 1 (the unit sphere). -If you think geographically the I<theta> are longitudes: zero at the -Greenwhich meridian, eastward positive, westward negative -- and the -I<phi> are latitudes: zero at the North Pole, northward positive, -southward negative. B<NOTE>: this formula thinks in mathematics, not -geographically: the I<phi> zero is at the North Pole, not at the -Equator on the west coast of Africa (Bay of Guinea). You need to -subtract your geographical coordinates from I<pi/2> (also known as 90 -degrees). +If you are using geographic coordinates, latitude and longitude, you need to +adjust for the fact that latitude is zero at the equator increasing towards +the north and decreasing towards the south. Assuming ($lat0, $lon0) and +($lat1, $lon1) are the geographic coordinates in radians of the two points, +the distance can be computed with $distance = great_circle_distance($lon0, pi/2 - $lat0, $lon1, pi/2 - $lat1, $rho); diff --git a/dist/Math-Complex/t/Trig.t b/dist/Math-Complex/t/Trig.t index a9a12556b6..8e2e5d1645 100644 --- a/dist/Math-Complex/t/Trig.t +++ b/dist/Math-Complex/t/Trig.t @@ -10,7 +10,7 @@ use strict; use warnings; -use Test::More tests => 153; +use Test::More tests => 157; use Math::Trig 1.18; use Math::Trig 1.18 qw(:pi Inf); @@ -363,7 +363,15 @@ print "# great_circle_distance with small angles\n"; for my $e (qw(1e-2 1e-3 1e-4 1e-5)) { # Can't assume == 0 because of floating point fuzz, # but let's hope for at least < $e. - cmp_ok(great_circle_distance(0, $e, 0, $e), '<', $e); + cmp_ok(great_circle_distance(0, $e, 0, $e), '<', $e, + "great_circle_distance(0, $e, 0, $e) < $e"); +} + +for my $e (qw(1e-5 1e-6 1e-7 1e-8)) { + # Verify that the distance is positive for points close together. A poor + # algorithm is likely to give a distance of zero in some of these cases. + cmp_ok(great_circle_distance(2, 2, 2, 2+$e), '>', 0, + "great_circle_distance(2, 2, 2, " . (2+$e) . ") > 0"); } print "# asin_real, acos_real\n"; |