Skip to content

Commit 7fe74a2

Browse files
committed
update ecef2geodetic to be closed-form
cleanup tests test some nan pep8
1 parent 0c77a60 commit 7fe74a2

File tree

17 files changed

+233
-132
lines changed

17 files changed

+233
-132
lines changed

archive/ecef.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"""
44
from pymap3d.ecef import Ellipsoid
55
from numpy import sqrt, arctan, arctan2, hypot, degrees
6+
from typing import Tuple
67

78

89
def cbrt(x):
@@ -38,3 +39,70 @@ def ecef2geodetic(x, y, z, ell=Ellipsoid(), deg=True):
3839
return degrees(lat), degrees(lon), alt
3940
else:
4041
return lat, lon, alt # radians
42+
43+
44+
def ecef2geodetic_old(x: float, y: float, z: float,
45+
ell: Ellipsoid = None, deg: bool = True) -> Tuple[float, float, float]:
46+
"""
47+
convert ECEF (meters) to geodetic coordinates
48+
49+
input
50+
-----
51+
x,y,z [meters] target ECEF location [0,Infinity)
52+
ell reference ellipsoid
53+
deg degrees input/output (False: radians in/out)
54+
55+
output
56+
------
57+
lat,lon (degrees/radians)
58+
alt (meters)
59+
60+
Algorithm is based on
61+
http://www.astro.uni.torun.pl/~kb/Papers/geod/Geod-BG.htm
62+
This algorithm provides a converging solution to the latitude equation
63+
in terms of the parametric or reduced latitude form (v)
64+
This algorithm provides a uniform solution over all latitudes as it does
65+
not involve division by cos(phi) or sin(phi)
66+
"""
67+
if ell is None:
68+
ell = Ellipsoid()
69+
70+
ea = ell.a
71+
eb = ell.b
72+
rad = hypot(x, y)
73+
# Constant required for Latitude equation
74+
rho = arctan2(eb * z, ea * rad)
75+
# Constant required for latitude equation
76+
c = (ea**2 - eb**2) / hypot(ea * rad, eb * z)
77+
# Starter for the Newtons Iteration Method
78+
vnew = arctan2(ea * z, eb * rad)
79+
# Initializing the parametric latitude
80+
v = 0
81+
for _ in range(5):
82+
v = deepcopy(vnew)
83+
# %% Newtons Method for computing iterations
84+
vnew = v - ((2 * sin(v - rho) - c * sin(2 * v)) /
85+
(2 * (cos(v - rho) - c * cos(2 * v))))
86+
87+
if allclose(v, vnew):
88+
break
89+
# %% Computing latitude from the root of the latitude equation
90+
lat = arctan2(ea * tan(vnew), eb)
91+
# by inspection
92+
lon = arctan2(y, x)
93+
94+
alt = (((rad - ea * cos(vnew)) * cos(lat)) +
95+
((z - eb * sin(vnew)) * sin(lat)))
96+
97+
with np.errstate(invalid='ignore'):
98+
# NOTE: need np.any() to handle scalar and array cases
99+
if np.any((lat < -pi / 2) | (lat > pi / 2)):
100+
raise ValueError('-90 <= lat <= 90')
101+
102+
if np.any((lon < -pi) | (lon > 2 * pi)):
103+
raise ValueError('-180 <= lat <= 360')
104+
105+
if deg:
106+
return degrees(lat), degrees(lon), alt
107+
else:
108+
return lat, lon, alt # radians

pymap3d/aer.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
def ecef2aer(x: float, y: float, z: float,
1010
lat0: float, lon0: float, h0: float,
11-
ell=None, deg: bool=True) -> Tuple[float, float, float]:
11+
ell=None, deg: bool = True) -> Tuple[float, float, float]:
1212
"""
1313
Observer => Point
1414
@@ -32,7 +32,7 @@ def ecef2aer(x: float, y: float, z: float,
3232

3333
def geodetic2aer(lat: float, lon: float, h: float,
3434
lat0: float, lon0: float, h0: float,
35-
ell=None, deg: bool=True) -> Tuple[float, float, float]:
35+
ell=None, deg: bool = True) -> Tuple[float, float, float]:
3636
"""
3737
Observer => Point
3838
@@ -55,7 +55,7 @@ def geodetic2aer(lat: float, lon: float, h: float,
5555

5656
def aer2geodetic(az: float, el: float, srange: float,
5757
lat0: float, lon0: float, h0: float,
58-
deg: bool=True) -> Tuple[float, float, float]:
58+
deg: bool = True) -> Tuple[float, float, float]:
5959
"""
6060
Input:
6161
-----
@@ -79,7 +79,7 @@ def aer2geodetic(az: float, el: float, srange: float,
7979
def eci2aer(eci: Tuple[float, float, float],
8080
lat0: float, lon0: float, h0: float,
8181
t: datetime,
82-
useastropy: bool=True) -> Tuple[float, float, float]:
82+
useastropy: bool = True) -> Tuple[float, float, float]:
8383
"""
8484
Observer => Point
8585
@@ -103,8 +103,8 @@ def eci2aer(eci: Tuple[float, float, float],
103103

104104
def aer2eci(az: float, el: float, srange: float,
105105
lat0: float, lon0: float, h0: float, t: datetime,
106-
ell=None, deg: bool=True,
107-
useastropy: bool=True) -> np.ndarray:
106+
ell=None, deg: bool = True,
107+
useastropy: bool = True) -> np.ndarray:
108108
"""
109109
110110
input
@@ -127,7 +127,7 @@ def aer2eci(az: float, el: float, srange: float,
127127

128128
def aer2ecef(az: float, el: float, srange: float,
129129
lat0: float, lon0: float, alt0: float,
130-
ell=None, deg: bool=True) -> Tuple[float, float, float]:
130+
ell=None, deg: bool = True) -> Tuple[float, float, float]:
131131
"""
132132
convert target azimuth, elevation, range (meters) from observer at lat0,lon0,alt0 to ECEF coordinates.
133133

pymap3d/azelradec.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
def azel2radec(az_deg: float, el_deg: float,
1818
lat_deg: float, lon_deg: float,
19-
time: datetime, usevallado: bool=False) -> Tuple[float, float]:
19+
time: datetime, usevallado: bool = False) -> Tuple[float, float]:
2020
"""
2121
viewing angle (az, el) to sky coordinates (ra, dec)
2222
@@ -47,7 +47,7 @@ def azel2radec(az_deg: float, el_deg: float,
4747

4848
def radec2azel(ra_deg: float, dec_deg: float,
4949
lat_deg: float, lon_deg: float,
50-
time: datetime, usevallado: bool=False) -> Tuple[float, float]:
50+
time: datetime, usevallado: bool = False) -> Tuple[float, float]:
5151
"""
5252
sky coordinates (ra, dec) to viewing angle (az, el)
5353

pymap3d/datetime2hourangle.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818

1919
def datetime2sidereal(time: datetime,
2020
lon_radians: float,
21-
usevallado: bool=True) -> float:
21+
usevallado: bool = True) -> float:
2222
"""
2323
Convert ``datetime`` to sidereal time
2424

pymap3d/ecef.py

Lines changed: 41 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
from numpy import radians, sin, cos, tan, allclose, hypot, degrees, arctan2, sqrt, pi
1+
from numpy import radians, sin, cos, tan, arctan, hypot, degrees, arctan2, sqrt, pi
22
import numpy as np
3-
from copy import deepcopy
43
from typing import Tuple
54
from datetime import datetime
65

@@ -14,7 +13,7 @@ class Ellipsoid:
1413
https://en.wikibooks.org/wiki/PROJ.4#Spheroid
1514
"""
1615

17-
def __init__(self, model: str='wgs84') -> None:
16+
def __init__(self, model: str = 'wgs84') -> None:
1817
if model == 'wgs84':
1918
"""https://en.wikipedia.org/wiki/World_Geodetic_System#WGS84"""
2019
self.a = 6378137. # semi-major axis [m]
@@ -45,7 +44,7 @@ def __init__(self, model: str='wgs84') -> None:
4544
raise NotImplementedError('{} model not implemented, let us know and we will add it (or make a pull request)'.format(model))
4645

4746

48-
def get_radius_normal(lat_radians: float, ell: Ellipsoid=None) -> float:
47+
def get_radius_normal(lat_radians: float, ell: Ellipsoid = None) -> float:
4948
""" Compute normal radius of planetary body"""
5049
if ell is None:
5150
ell = Ellipsoid()
@@ -57,7 +56,7 @@ def get_radius_normal(lat_radians: float, ell: Ellipsoid=None) -> float:
5756

5857

5958
def geodetic2ecef(lat: float, lon: float, alt: float,
60-
ell: Ellipsoid=None, deg: bool=True) -> Tuple[float, float, float]:
59+
ell: Ellipsoid = None, deg: bool = True) -> Tuple[float, float, float]:
6160
"""
6261
Point
6362
@@ -78,14 +77,15 @@ def geodetic2ecef(lat: float, lon: float, alt: float,
7877
lon = radians(lon)
7978

8079
with np.errstate(invalid='ignore'):
81-
if ((lat < -pi / 2) | (lat > pi / 2)).any():
80+
# need np.any() to handle scalar and array cases
81+
if np.any((lat < -pi / 2) | (lat > pi / 2)):
8282
raise ValueError('-90 <= lat <= 90')
8383

84-
if ((lon < -pi) | (lon > 2 * pi)).any():
84+
if np.any((lon < -pi) | (lon > 2 * pi)):
8585
raise ValueError('-180 <= lat <= 360')
8686

87-
if (np.asarray(alt) < 0).any():
88-
raise ValueError('altitude \in [0, Infinity)')
87+
if np.any(np.asarray(alt) < 0):
88+
raise ValueError('altitude [0, Infinity)')
8989
# radius of curvature of the prime vertical section
9090
N = get_radius_normal(lat, ell)
9191
# Compute cartesian (geocentric) coordinates given (curvilinear) geodetic
@@ -98,7 +98,7 @@ def geodetic2ecef(lat: float, lon: float, alt: float,
9898

9999

100100
def ecef2geodetic(x: float, y: float, z: float,
101-
ell: Ellipsoid=None, deg: bool=True) -> Tuple[float, float, float]:
101+
ell: Ellipsoid = None, deg: bool = True) -> Tuple[float, float, float]:
102102
"""
103103
convert ECEF (meters) to geodetic coordinates
104104
@@ -113,49 +113,38 @@ def ecef2geodetic(x: float, y: float, z: float,
113113
lat,lon (degrees/radians)
114114
alt (meters)
115115
116-
Algorithm is based on
117-
http://www.astro.uni.torun.pl/~kb/Papers/geod/Geod-BG.htm
118-
This algorithm provides a converging solution to the latitude equation
119-
in terms of the parametric or reduced latitude form (v)
120-
This algorithm provides a uniform solution over all latitudes as it does
121-
not involve division by cos(phi) or sin(phi)
116+
based on:
117+
You, Rey-Jer. (2000). Transformation of Cartesian to Geodetic Coordinates without Iterations.
118+
Journal of Surveying Engineering. doi: 10.1061/(ASCE)0733-9453
122119
"""
123120
if ell is None:
124121
ell = Ellipsoid()
125122

126-
ea = ell.a
127-
eb = ell.b
128-
rad = hypot(x, y)
129-
# Constant required for Latitude equation
130-
rho = arctan2(eb * z, ea * rad)
131-
# Constant required for latitude equation
132-
c = (ea**2 - eb**2) / hypot(ea * rad, eb * z)
133-
# Starter for the Newtons Iteration Method
134-
vnew = arctan2(ea * z, eb * rad)
135-
# Initializing the parametric latitude
136-
v = 0
137-
for _ in range(5):
138-
v = deepcopy(vnew)
139-
# %% Newtons Method for computing iterations
140-
vnew = v - ((2 * sin(v - rho) - c * sin(2 * v)) /
141-
(2 * (cos(v - rho) - c * cos(2 * v))))
142-
143-
if allclose(v, vnew):
144-
break
145-
# %% Computing latitude from the root of the latitude equation
146-
lat = arctan2(ea * tan(vnew), eb)
147-
# by inspection
148-
lon = arctan2(y, x)
123+
r = sqrt(x**2 + y**2 + z**2)
149124

150-
alt = (((rad - ea * cos(vnew)) * cos(lat)) +
151-
((z - eb * sin(vnew)) * sin(lat)))
125+
E = sqrt(ell.a**2 - ell.b**2)
152126

153-
with np.errstate(invalid='ignore'):
154-
if ((lat < -pi / 2) | (lat > pi / 2)).any():
155-
raise ValueError('-90 <= lat <= 90')
127+
# eqn. 4a
128+
u = sqrt(0.5 * (r**2 - E**2) + 0.5 * sqrt((r**2 - E**2)**2 + 4 * E**2 * z**2))
156129

157-
if ((lon < -pi) | (lon > 2 * pi)).any():
158-
raise ValueError('-180 <= lat <= 360')
130+
Q = hypot(x, y)
131+
132+
huE = hypot(u, E)
133+
134+
# eqn. 4b
135+
Beta = arctan(huE / u * z / hypot(x, y))
136+
137+
# eqn. 13
138+
eps = ((ell.b * u - ell.a * huE + E**2) * sin(Beta)) / (ell.a * huE * 1 / cos(Beta) - E**2 * cos(Beta))
139+
140+
Beta += eps
141+
# %% final output
142+
lat = arctan(ell.a / ell.b * tan(Beta))
143+
144+
lon = arctan2(y, x)
145+
146+
# eqn. 7
147+
alt = sqrt((z - ell.b * sin(Beta))**2 + (Q - ell.a * cos(Beta))**2)
159148

160149
if deg:
161150
return degrees(lat), degrees(lon), alt
@@ -164,7 +153,7 @@ def ecef2geodetic(x: float, y: float, z: float,
164153

165154

166155
def ecef2enuv(u: float, v: float, w: float,
167-
lat0: float, lon0: float, deg: bool=True) -> Tuple[float, float, float]:
156+
lat0: float, lon0: float, deg: bool = True) -> Tuple[float, float, float]:
168157
"""
169158
for VECTOR i.e. between two points
170159
@@ -187,7 +176,7 @@ def ecef2enuv(u: float, v: float, w: float,
187176

188177
def ecef2enu(x: float, y: float, z: float,
189178
lat0: float, lon0: float, h0: float,
190-
ell: Ellipsoid=None, deg: bool=True) -> Tuple[float, float, float]:
179+
ell: Ellipsoid = None, deg: bool = True) -> Tuple[float, float, float]:
191180
"""
192181
193182
input
@@ -208,7 +197,7 @@ def ecef2enu(x: float, y: float, z: float,
208197

209198

210199
def enu2uvw(east: float, north: float, up: float,
211-
lat0: float, lon0: float, deg: bool=True) -> Tuple[float, float, float]:
200+
lat0: float, lon0: float, deg: bool = True) -> Tuple[float, float, float]:
212201
if deg:
213202
lat0 = radians(lat0)
214203
lon0 = radians(lon0)
@@ -223,7 +212,7 @@ def enu2uvw(east: float, north: float, up: float,
223212

224213

225214
def uvw2enu(u: float, v: float, w: float,
226-
lat0: float, lon0: float, deg: bool=True) -> Tuple[float, float, float]:
215+
lat0: float, lon0: float, deg: bool = True) -> Tuple[float, float, float]:
227216
if deg:
228217
lat0 = radians(lat0)
229218
lon0 = radians(lon0)
@@ -237,7 +226,7 @@ def uvw2enu(u: float, v: float, w: float,
237226

238227

239228
def eci2geodetic(eci: np.ndarray, t: datetime,
240-
useastropy: bool=True) -> Tuple[float, float, float]:
229+
useastropy: bool = True) -> Tuple[float, float, float]:
241230
"""
242231
convert ECI to geodetic coordinates
243232
@@ -265,7 +254,7 @@ def eci2geodetic(eci: np.ndarray, t: datetime,
265254

266255
def enu2ecef(e1: float, n1: float, u1: float,
267256
lat0: float, lon0: float, h0: float,
268-
ell: Ellipsoid=None, deg: bool=True) -> Tuple[float, float, float]:
257+
ell: Ellipsoid = None, deg: bool = True) -> Tuple[float, float, float]:
269258
"""
270259
ENU to ECEF
271260

pymap3d/eci.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
def eci2ecef(eci: np.ndarray,
1111
time: datetime,
12-
useastropy: bool=True) -> np.ndarray:
12+
useastropy: bool = True) -> np.ndarray:
1313
"""
1414
Observer => Point
1515
@@ -49,7 +49,7 @@ def eci2ecef(eci: np.ndarray,
4949

5050
def ecef2eci(ecef: np.ndarray,
5151
time: datetime,
52-
useastropy: bool=True) -> np.ndarray:
52+
useastropy: bool = True) -> np.ndarray:
5353
"""
5454
Point => Point
5555

0 commit comments

Comments
 (0)