Skip to content

Commit 9fae7d9

Browse files
committed
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
1 parent f527d43 commit 9fae7d9

File tree

1 file changed

+32
-18
lines changed

1 file changed

+32
-18
lines changed

geodesy/haversine_distance.py

Lines changed: 32 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
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
@@ -21,8 +19,10 @@ def haversine_distance(lat1: float, lon1: float, lat2: float, lon2: float) -> fl
2119
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__":

0 commit comments

Comments
 (0)