Skip to content

Commit 7e9dd2a

Browse files
committed
ecef2geodetic 100x speedup from not using vectorize
1 parent e7da166 commit 7e9dd2a

File tree

6 files changed

+89
-87
lines changed

6 files changed

+89
-87
lines changed

scripts/benchmark_ecef2geo.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
#!/usr/bin/env python3
2+
"""
3+
benchmark ecef2geodetic
4+
"""
5+
import time
6+
from pymap3d.ecef import ecef2geodetic
7+
import numpy as np
8+
import argparse
9+
10+
ll0 = (42.0, 82.0)
11+
12+
13+
def bench(N: int) -> float:
14+
15+
x = np.random.random(N)
16+
y = np.random.random(N)
17+
z = np.random.random(N)
18+
19+
tic = time.monotonic()
20+
lat, lon, alt = ecef2geodetic(x, y, z)
21+
22+
return time.monotonic() - tic
23+
24+
25+
if __name__ == "__main__":
26+
p = argparse.ArgumentParser()
27+
p.add_argument("N", type=int)
28+
p = p.parse_args()
29+
N = p.N
30+
31+
print(f"ecef2geodetic: {bench(N):.3f} seconds")

src/pymap3d/aer.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,15 @@
2020

2121

2222
def ecef2aer(
23-
x: float | ndarray,
24-
y: float | ndarray,
25-
z: float | ndarray,
23+
x: ndarray,
24+
y: ndarray,
25+
z: ndarray,
2626
lat0: float,
2727
lon0: float,
2828
h0: float,
2929
ell: Ellipsoid = None,
3030
deg: bool = True,
31-
) -> tuple[float, float, float]:
31+
) -> tuple[ndarray, ndarray, ndarray]:
3232
"""
3333
compute azimuth, elevation and slant range from an Observer to a Point with ECEF coordinates.
3434
@@ -69,15 +69,15 @@ def ecef2aer(
6969

7070

7171
def geodetic2aer(
72-
lat: float,
73-
lon: float,
74-
h: float,
72+
lat: ndarray,
73+
lon: ndarray,
74+
h: ndarray,
7575
lat0: float,
7676
lon0: float,
7777
h0: float,
7878
ell: Ellipsoid = None,
7979
deg: bool = True,
80-
) -> tuple[float, float, float]:
80+
) -> tuple[ndarray, ndarray, ndarray]:
8181
"""
8282
gives azimuth, elevation and slant range from an Observer to a Point with geodetic coordinates.
8383
@@ -177,7 +177,7 @@ def eci2aer(
177177
*,
178178
deg: bool = True,
179179
use_astropy: bool = True
180-
) -> tuple[float, float, float]:
180+
) -> tuple[ndarray, ndarray, ndarray]:
181181
"""
182182
takes Earth Centered Inertial x,y,z ECI coordinates of point and gives az, el, slant range from Observer
183183

src/pymap3d/ecef.py

Lines changed: 14 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,10 @@
1515
arctan2 as atan2,
1616
sqrt,
1717
pi,
18-
vectorize,
1918
)
2019
except ImportError:
2120
from math import radians, sin, cos, tan, atan, hypot, degrees, atan2, sqrt, pi # type: ignore
2221

23-
vectorize = None
2422
ndarray = typing.Any # type: ignore
2523

2624
from datetime import datetime
@@ -110,21 +108,6 @@ def ecef2geodetic(
110108
z: float | ndarray,
111109
ell: Ellipsoid = None,
112110
deg: bool = True,
113-
) -> tuple[float | ndarray, float | ndarray, float | ndarray]:
114-
if vectorize is not None:
115-
fun = vectorize(ecef2geodetic_point)
116-
lat, lon, alt = fun(x, y, z, ell, deg)
117-
return lat[()], lon[()], alt[()]
118-
else:
119-
return ecef2geodetic_point(x, y, z, ell, deg)
120-
121-
122-
def ecef2geodetic_point(
123-
x: float | ndarray,
124-
y: float | ndarray,
125-
z: float | ndarray,
126-
ell: Ellipsoid = None,
127-
deg: bool = True,
128111
) -> tuple[float | ndarray, float | ndarray, float | ndarray]:
129112
"""
130113
convert ECEF (meters) to geodetic coordinates
@@ -155,6 +138,7 @@ def ecef2geodetic_point(
155138
You, Rey-Jer. (2000). Transformation of Cartesian to Geodetic Coordinates without Iterations.
156139
Journal of Surveying Engineering. doi: 10.1061/(ASCE)0733-9453
157140
"""
141+
158142
if ell is None:
159143
ell = Ellipsoid()
160144

@@ -199,8 +183,14 @@ def ecef2geodetic_point(
199183
+ z ** 2 / ell.semiminor_axis ** 2
200184
< 1
201185
)
202-
if inside:
203-
alt = -alt
186+
187+
try:
188+
if inside.any(): # type: ignore
189+
# avoid all false assignment bug
190+
alt[inside] = -alt
191+
except (TypeError, AttributeError):
192+
if inside:
193+
alt = -alt
204194

205195
if deg:
206196
lat = degrees(lat)
@@ -255,15 +245,15 @@ def ecef2enuv(
255245

256246

257247
def ecef2enu(
258-
x: float | ndarray,
259-
y: float | ndarray,
260-
z: float | ndarray,
248+
x: ndarray,
249+
y: ndarray,
250+
z: ndarray,
261251
lat0: float,
262252
lon0: float,
263253
h0: float,
264254
ell: Ellipsoid = None,
265255
deg: bool = True,
266-
) -> tuple[float | ndarray, float | ndarray, float | ndarray]:
256+
) -> tuple[ndarray, ndarray, ndarray]:
267257
"""
268258
from observer to target, ECEF => ENU
269259
@@ -343,7 +333,7 @@ def enu2uvw(
343333

344334
def uvw2enu(
345335
u: float, v: float, w: float, lat0: float, lon0: float, deg: bool = True
346-
) -> tuple[float, float, float]:
336+
) -> tuple[ndarray, ndarray, ndarray]:
347337
"""
348338
Parameters
349339
----------

src/pymap3d/enu.py

Lines changed: 24 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,10 @@
33
import typing
44

55
try:
6-
from numpy import radians, sin, cos, hypot, arctan2 as atan2, degrees, pi, vectorize, ndarray
6+
from numpy import asarray, radians, sin, cos, hypot, arctan2 as atan2, degrees, pi, ndarray
77
except ImportError:
88
from math import radians, sin, cos, hypot, atan2, degrees, pi # type: ignore
99

10-
vectorize = None
1110
ndarray = typing.Any # type: ignore
1211

1312
from .ecef import geodetic2ecef, ecef2geodetic, enu2ecef, uvw2enu
@@ -20,8 +19,8 @@
2019

2120

2221
def enu2aer(
23-
e: float | ndarray, n: float | ndarray, u: float | ndarray, deg: bool = True
24-
) -> tuple[float, float, float]:
22+
e: ndarray, n: ndarray, u: ndarray, deg: bool = True
23+
) -> tuple[ndarray, ndarray, ndarray]:
2524
"""
2625
ENU to Azimuth, Elevation, Range
2726
@@ -48,26 +47,19 @@ def enu2aer(
4847
slant range [meters]
4948
"""
5049

51-
if vectorize is not None:
52-
fun = vectorize(enu2aer_point)
53-
az, el, rng = fun(e, n, u, deg)
54-
return az[()], el[()], rng[()]
55-
else:
56-
return enu2aer_point(e, n, u, deg)
57-
58-
59-
def enu2aer_point(
60-
e: float | ndarray, n: float | ndarray, u: float | ndarray, deg: bool = True
61-
) -> tuple[float, float, float]:
62-
6350
# 1 millimeter precision for singularity
6451

65-
if abs(e) < 1e-3:
66-
e = 0.0
67-
if abs(n) < 1e-3:
68-
n = 0.0
69-
if abs(u) < 1e-3:
70-
u = 0.0
52+
try:
53+
e[abs(e) < 1e-3] = 0.0
54+
n[abs(n) < 1e-3] = 0.0
55+
u[abs(u) < 1e-3] = 0.0
56+
except TypeError:
57+
if abs(e) < 1e-3:
58+
e = 0.0 # type: ignore
59+
if abs(n) < 1e-3:
60+
n = 0.0 # type: ignore
61+
if abs(u) < 1e-3:
62+
u = 0.0 # type: ignore
7163

7264
r = hypot(e, n)
7365
slantRange = hypot(r, u)
@@ -82,17 +74,6 @@ def enu2aer_point(
8274

8375

8476
def aer2enu(az: float, el: float, srange: float, deg: bool = True) -> tuple[float, float, float]:
85-
if vectorize is not None:
86-
fun = vectorize(aer2enu_point)
87-
e, n, u = fun(az, el, srange, deg)
88-
return e[()], n[()], u[()]
89-
else:
90-
return aer2enu_point(az, el, srange, deg)
91-
92-
93-
def aer2enu_point(
94-
az: float, el: float, srange: float, deg: bool = True
95-
) -> tuple[float, float, float]:
9677
"""
9778
Azimuth, Elevation, Slant range to target to East, North, Up
9879
@@ -120,8 +101,12 @@ def aer2enu_point(
120101
el = radians(el)
121102
az = radians(az)
122103

123-
if srange < 0:
124-
raise ValueError("Slant range [0, Infinity)")
104+
try:
105+
if (asarray(srange) < 0).any():
106+
raise ValueError("Slant range [0, Infinity)")
107+
except UnboundLocalError:
108+
if srange < 0:
109+
raise ValueError("Slant range [0, Infinity)")
125110

126111
r = srange * cos(el)
127112

@@ -177,15 +162,15 @@ def enu2geodetic(
177162

178163

179164
def geodetic2enu(
180-
lat: float,
181-
lon: float,
182-
h: float,
165+
lat: ndarray,
166+
lon: ndarray,
167+
h: ndarray,
183168
lat0: float,
184169
lon0: float,
185170
h0: float,
186171
ell: Ellipsoid = None,
187172
deg: bool = True,
188-
) -> tuple[float, float, float]:
173+
) -> tuple[ndarray, ndarray, ndarray]:
189174
"""
190175
Parameters
191176
----------

src/pymap3d/ned.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,9 @@ def aer2ned(
4545
return n, e, -u
4646

4747

48-
def ned2aer(n: float, e: float, d: float, deg: bool = True) -> tuple[float, float, float]:
48+
def ned2aer(
49+
n: ndarray, e: ndarray, d: ndarray, deg: bool = True
50+
) -> tuple[ndarray, ndarray, ndarray]:
4951
"""
5052
converts North, East, Down to azimuth, elevation, range
5153
@@ -170,9 +172,9 @@ def ned2ecef(
170172

171173

172174
def ecef2ned(
173-
x: float,
174-
y: float,
175-
z: float,
175+
x: ndarray,
176+
y: ndarray,
177+
z: ndarray,
176178
lat0: float,
177179
lon0: float,
178180
h0: float,
@@ -219,9 +221,9 @@ def ecef2ned(
219221

220222

221223
def geodetic2ned(
222-
lat: float,
223-
lon: float,
224-
h: float,
224+
lat: ndarray,
225+
lon: ndarray,
226+
h: ndarray,
225227
lat0: float,
226228
lon0: float,
227229
h0: float,

src/pymap3d/tests/test_geodetic.py

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,17 +37,11 @@ def test_3d_geodetic2ecef():
3737
assert (x0, y0, z0) == approx(xyz0)
3838

3939

40-
@pytest.mark.parametrize(
41-
"xyz", [(xyz0[0], xyz0[1], xyz0[2]), ([xyz0[0]], [xyz0[1]], [xyz0[2]])], ids=("scalar", "list")
42-
)
43-
def test_scalar_ecef2geodetic(xyz):
40+
def test_scalar_ecef2geodetic():
4441
"""
4542
verify we can handle the wide variety of input data type users might use
4643
"""
47-
if isinstance(xyz[0], list):
48-
pytest.importorskip("numpy")
49-
50-
lat, lon, alt = pm.ecef2geodetic(*xyz)
44+
lat, lon, alt = pm.ecef2geodetic(xyz0[0], xyz0[1], xyz0[2])
5145

5246
assert [lat, lon, alt] == approx(lla0, rel=1e-4)
5347

0 commit comments

Comments
 (0)