Skip to content

Commit 12d0648

Browse files
fix: use geodetic latitudes in haversine distance formula (#14351)
* fix: use geodetic latitudes in haversine distance formula The implementation was incorrectly using reduced latitudes (via a flattening factor from WGS84 ellipsoid constants) instead of raw geodetic latitudes. Reduced latitudes are appropriate for ellipsoidal models like Lambert's formula, but the Haversine formula operates on a sphere and should use geodetic latitudes directly. Changes: - Use radians(lat) directly instead of computing reduced latitudes with atan((1 - flattening) * tan(radians(lat))) - Replace equatorial radius (6378137m) with mean Earth radius (6371000m) for better spherical approximation - Remove unused WGS84 ellipsoid constants (AXIS_A, AXIS_B) - Remove unused imports (atan, tan) - Add edge case and cross-continental doctests Fixes #11308 * fix: update Lambert's to use corrected haversine radius for central angle Lambert's ellipsoidal distance computes the central angle sigma by dividing the haversine distance by a radius. Previously both functions used the same equatorial radius (6378137m), so the values cancelled out. After correcting haversine to use the mean Earth radius (6371000m), Lambert's must divide by the same radius to recover the correct angle. Also update the expected doctest values to match the corrected haversine output. Fixes #11308 * Fix typos Updated the docstring for the haversine_distance function to improve clarity and fix minor grammatical issues. * Fix typos in docstring and variable names * Clarify note on using haversine_distance.py Updated the note to clarify the use of haversine_distance.py. --------- Co-authored-by: Christian Clauss <cclauss@me.com>
1 parent d502013 commit 12d0648

2 files changed

Lines changed: 48 additions & 35 deletions

File tree

geodesy/haversine_distance.py

Lines changed: 35 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,28 @@
1-
from math import asin, atan, cos, radians, sin, sqrt, tan
1+
from math import asin, cos, radians, sin, sqrt
22

3-
AXIS_A = 6378137.0
4-
AXIS_B = 6356752.314245
5-
RADIUS = 6378137
3+
EARTH_RADIUS = 6371000
64

75

86
def haversine_distance(lat1: float, lon1: float, lat2: float, lon2: float) -> float:
97
"""
10-
Calculate great circle distance between two points in a sphere,
8+
Calculate great-circle distance between two points on a sphere,
119
given longitudes and latitudes https://en.wikipedia.org/wiki/Haversine_formula
1210
1311
We know that the globe is "sort of" spherical, so a path between two points
1412
isn't exactly a straight line. We need to account for the Earth's curvature
1513
when calculating distance from point A to B. This effect is negligible for
1614
small distances but adds up as distance increases. The Haversine method treats
17-
the earth as a sphere which allows us to "project" the two points A and B
15+
the Earth as a sphere, which allows us to "project" the two points A and B
1816
onto the surface of that sphere and approximate the spherical distance between
1917
them. Since the Earth is not a perfect sphere, other methods which model the
20-
Earth's ellipsoidal nature are more accurate but a quick and modifiable
21-
computation like Haversine can be handy for shorter range distances.
18+
Earth's ellipsoidal nature are more accurate, but a quick and modifiable
19+
computation like Haversine can be handy for shorter-range distances.
2220
2321
Args:
24-
* `lat1`, `lon1`: latitude and longitude of coordinate 1
25-
* `lat2`, `lon2`: latitude and longitude of coordinate 2
22+
lat1: latitude of coordinate 1 in degrees
23+
lon1: longitude of coordinate 1 in degrees
24+
lat2: latitude of coordinate 2 in degrees
25+
lon2: longitude of coordinate 2 in degrees
2626
Returns:
2727
geographical distance between two points in metres
2828
@@ -31,25 +31,39 @@ def haversine_distance(lat1: float, lon1: float, lat2: float, lon2: float) -> fl
3131
>>> SAN_FRANCISCO = point_2d(37.774856, -122.424227)
3232
>>> YOSEMITE = point_2d(37.864742, -119.537521)
3333
>>> f"{haversine_distance(*SAN_FRANCISCO, *YOSEMITE):0,.0f} meters"
34-
'254,352 meters'
34+
'253,748 meters'
35+
>>> NEW_YORK = point_2d(40.712776, -74.005974)
36+
>>> LOS_ANGELES = point_2d(34.052235, -118.243683)
37+
>>> f"{haversine_distance(*NEW_YORK, *LOS_ANGELES):0,.0f} meters"
38+
'3,935,746 meters'
39+
>>> LONDON = point_2d(51.507351, -0.127758)
40+
>>> PARIS = point_2d(48.856614, 2.352222)
41+
>>> f"{haversine_distance(*LONDON, *PARIS):0,.0f} meters"
42+
'343,549 meters'
43+
>>> haversine_distance(0, 0, 0, 0)
44+
0.0
45+
>>> from math import isclose
46+
>>> quarter_equator = haversine_distance(0, 0, 0, 90)
47+
>>> isclose(quarter_equator, 10_007_543, rel_tol=1e-3)
48+
True
3549
"""
36-
# CONSTANTS per WGS84 https://en.wikipedia.org/wiki/World_Geodetic_System
37-
# Distance in metres(m)
38-
# Equation parameters
39-
# Equation https://en.wikipedia.org/wiki/Haversine_formula#Formulation
40-
flattening = (AXIS_A - AXIS_B) / AXIS_A
41-
phi_1 = atan((1 - flattening) * tan(radians(lat1)))
42-
phi_2 = atan((1 - flattening) * tan(radians(lat2)))
50+
# Convert geodetic coordinates from degrees to radians.
51+
# The Haversine formula operates on a sphere, so we use the raw geodetic
52+
# latitudes directly rather than reduced latitudes (which apply to
53+
# ellipsoidal models like Lambert's formula).
54+
# Reference: https://en.wikipedia.org/wiki/Haversine_formula#Formulation
55+
phi_1 = radians(lat1)
56+
phi_2 = radians(lat2)
4357
lambda_1 = radians(lon1)
4458
lambda_2 = radians(lon2)
45-
# Equation
59+
60+
# Haversine equation
4661
sin_sq_phi = sin((phi_2 - phi_1) / 2)
4762
sin_sq_lambda = sin((lambda_2 - lambda_1) / 2)
48-
# Square both values
4963
sin_sq_phi *= sin_sq_phi
5064
sin_sq_lambda *= sin_sq_lambda
5165
h_value = sqrt(sin_sq_phi + (cos(phi_1) * cos(phi_2) * sin_sq_lambda))
52-
return 2 * RADIUS * asin(h_value)
66+
return 2 * EARTH_RADIUS * asin(h_value)
5367

5468

5569
if __name__ == "__main__":

geodesy/lamberts_ellipsoidal_distance.py

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from math import atan, cos, radians, sin, tan
22

3-
from .haversine_distance import haversine_distance
3+
from .haversine_distance import EARTH_RADIUS, haversine_distance
44

55
AXIS_A = 6378137.0
66
AXIS_B = 6356752.314245
@@ -12,18 +12,17 @@ def lamberts_ellipsoidal_distance(
1212
) -> float:
1313
"""
1414
Calculate the shortest distance along the surface of an ellipsoid between
15-
two points on the surface of earth given longitudes and latitudes
15+
two points on the surface of Earth given longitudes and latitudes
1616
https://en.wikipedia.org/wiki/Geographical_distance#Lambert's_formula_for_long_lines
1717
18-
NOTE: This algorithm uses geodesy/haversine_distance.py to compute central angle,
19-
sigma
18+
NOTE: Uses geodesy/haversine_distance.py to compute the central angle, sigma.
2019
21-
Representing the earth as an ellipsoid allows us to approximate distances between
20+
Representing the Earth as an ellipsoid allows us to approximate distances between
2221
points on the surface much better than a sphere. Ellipsoidal formulas treat the
23-
Earth as an oblate ellipsoid which means accounting for the flattening that happens
22+
Earth as an oblate ellipsoid, which means accounting for the flattening that happens
2423
at the North and South poles. Lambert's formulae provide accuracy on the order of
25-
10 meteres over thousands of kilometeres. Other methods can provide
26-
millimeter-level accuracy but this is a simpler method to calculate long range
24+
10 meters over thousands of kilometers. Other methods can provide
25+
millimeter-level accuracy, but this is a simpler method to calculate long-range
2726
distances without increasing computational intensity.
2827
2928
Args:
@@ -59,11 +58,11 @@ def lamberts_ellipsoidal_distance(
5958
>>> NEW_YORK = point_2d(40.713019, -74.012647)
6059
>>> VENICE = point_2d(45.443012, 12.313071)
6160
>>> f"{lamberts_ellipsoidal_distance(*SAN_FRANCISCO, *YOSEMITE):0,.0f} meters"
62-
'254,351 meters'
61+
'254,032 meters'
6362
>>> f"{lamberts_ellipsoidal_distance(*SAN_FRANCISCO, *NEW_YORK):0,.0f} meters"
64-
'4,138,992 meters'
63+
'4,133,295 meters'
6564
>>> f"{lamberts_ellipsoidal_distance(*SAN_FRANCISCO, *VENICE):0,.0f} meters"
66-
'9,737,326 meters'
65+
'9,719,525 meters'
6766
"""
6867

6968
# Validate latitude values
@@ -86,7 +85,7 @@ def lamberts_ellipsoidal_distance(
8685

8786
# Compute central angle between two points
8887
# using haversine theta. sigma = haversine_distance / equatorial radius
89-
sigma = haversine_distance(lat1, lon1, lat2, lon2) / EQUATORIAL_RADIUS
88+
sigma = haversine_distance(lat1, lon1, lat2, lon2) / EARTH_RADIUS
9089

9190
# Intermediate P and Q values
9291
p_value = (b_lat1 + b_lat2) / 2
@@ -95,8 +94,8 @@ def lamberts_ellipsoidal_distance(
9594
# Intermediate X value
9695
# X = (sigma - sin(sigma)) * sin^2Pcos^2Q / cos^2(sigma/2)
9796
x_numerator = (sin(p_value) ** 2) * (cos(q_value) ** 2)
98-
x_demonimator = cos(sigma / 2) ** 2
99-
x_value = (sigma - sin(sigma)) * (x_numerator / x_demonimator)
97+
x_denominator = cos(sigma / 2) ** 2
98+
x_value = (sigma - sin(sigma)) * (x_numerator / x_denominator)
10099

101100
# Intermediate Y value
102101
# Y = (sigma + sin(sigma)) * cos^2Psin^2Q / sin^2(sigma/2)

0 commit comments

Comments
 (0)