Skip to content

Commit 9660b08

Browse files
committed
lox: tolerate array / scalar together better
1 parent dbdb697 commit 9660b08

File tree

4 files changed

+89
-24
lines changed

4 files changed

+89
-24
lines changed

src/pymap3d/latitude.py

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,24 @@
44
import typing
55

66
from .ellipsoid import Ellipsoid
7-
from .utils import sanitize
7+
from .utils import sanitize, sign
88
from . import rcurve
99

1010
try:
11-
from numpy import radians, degrees, tan, sin, exp, pi, sqrt, inf
11+
from numpy import radians, degrees, tan, sin, cos, exp, pi, sqrt, inf
1212
from numpy import arctan as atan, arcsinh as asinh, arctanh as atanh # noqa: A001
1313

1414
use_numpy = True
1515
except ImportError:
16-
from math import atan, radians, degrees, tan, sin, asinh, atanh, exp, pi, sqrt, inf # type: ignore
16+
from math import atan, radians, degrees, tan, sin, cos, asinh, atanh, exp, pi, sqrt, inf # type: ignore
1717

1818
use_numpy = False
1919

2020
if typing.TYPE_CHECKING:
2121
from numpy import ndarray
2222

23+
COS_EPS = 1e-9 # tolerance for angles near abs([90, 270])
24+
2325
__all__ = [
2426
"geodetic2isometric",
2527
"isometric2geodetic",
@@ -208,16 +210,22 @@ def geodetic2isometric(
208210
# isometric_lat = log(tan(a2) * (y ** (e / 2)))
209211
# isometric_lat = log(tan(a2)) + e/2 * log((1-e*sin(geodetic_lat)) / (1+e*sin(geodetic_lat)))
210212

213+
coslat = cos(geodetic_lat)
214+
i = abs(coslat) <= COS_EPS
215+
211216
try:
212-
isometric_lat[abs(geodetic_lat - pi / 2) <= 1e-9] = inf # type: ignore
213-
isometric_lat[abs(-geodetic_lat - pi / 2) <= 1e-9] = -inf # type: ignore
217+
isometric_lat[i] = sign(geodetic_lat[i]) * inf # type: ignore
214218
except TypeError:
215-
if abs(geodetic_lat - pi / 2) <= 1e-9: # type: ignore
216-
isometric_lat = inf
217-
elif abs(-geodetic_lat - pi / 2) <= 1e-9: # type: ignore
218-
isometric_lat = -inf
219+
if i:
220+
isometric_lat = sign(geodetic_lat) * inf
219221

220-
return degrees(isometric_lat) if deg else isometric_lat
222+
if deg:
223+
isometric_lat = degrees(isometric_lat)
224+
225+
try:
226+
return isometric_lat.squeeze()[()]
227+
except AttributeError:
228+
return isometric_lat
221229

222230

223231
def isometric2geodetic(isometric_lat: ndarray, ell: Ellipsoid = None, deg: bool = True) -> ndarray:

src/pymap3d/lox.py

Lines changed: 53 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,17 @@
44
import typing
55

66
try:
7-
from numpy import radians, degrees, cos, arctan2 as atan2, tan, pi, array, atleast_1d
7+
from numpy import (
8+
radians,
9+
degrees,
10+
cos,
11+
arctan2 as atan2,
12+
tan,
13+
pi,
14+
array,
15+
atleast_1d,
16+
broadcast_to,
17+
)
818
except ImportError:
919
from math import radians, degrees, cos, atan2, tan, pi # type: ignore
1020

@@ -97,8 +107,6 @@ def loxodrome_inverse(
97107
98108
like Matlab distance('rh',...) and azimuth('rh',...)
99109
100-
If any of inputs lat1,lon1,lat2,lon2 are arrays, all must be arrays of same shape
101-
102110
Parameters
103111
----------
104112
@@ -141,6 +149,33 @@ def loxodrome_inverse(
141149
if deg:
142150
lat1, lon1, lat2, lon2 = radians(lat1), radians(lon1), radians(lat2), radians(lon2)
143151

152+
try:
153+
lat1 = atleast_1d(lat1)
154+
lon1 = atleast_1d(lon1)
155+
lat2 = atleast_1d(lat2)
156+
lon2 = atleast_1d(lon2)
157+
158+
if lat1.shape != lon1.shape:
159+
if lat1.size == 1:
160+
lat1 = broadcast_to(lat1, lon1.shape)
161+
elif lon1.size == 1:
162+
lon1 = broadcast_to(lon1, lat1.shape)
163+
else:
164+
raise ValueError("lat1, lon1 must have same shape")
165+
if lat2.shape != lon2.shape:
166+
if lat2.size == 1:
167+
lat2 = broadcast_to(lat2, lon2.shape)
168+
elif lon2.size == 1:
169+
lon2 = broadcast_to(lon2, lat2.shape)
170+
else:
171+
raise ValueError("lat2, lon2 must have same shape")
172+
if lat2.shape != lat1.shape:
173+
lat2 = broadcast_to(lat2, lat1.shape)
174+
lon2 = broadcast_to(lon2, lat1.shape)
175+
176+
except NameError:
177+
pass
178+
144179
# compute changes in isometric latitude and longitude between points
145180
disolat = geodetic2isometric(lat2, deg=False, ell=ell) - geodetic2isometric(
146181
lat1, deg=False, ell=ell
@@ -165,7 +200,10 @@ def loxodrome_inverse(
165200
if deg:
166201
az12 = degrees(az12) % 360.0
167202

168-
return dist, az12
203+
try:
204+
return dist.squeeze()[()], az12.squeeze()[()]
205+
except AttributeError:
206+
return dist, az12
169207

170208

171209
def loxodrome_direct(
@@ -182,8 +220,6 @@ def loxodrome_direct(
182220
like Matlab reckon('rh', ...)
183221
except that "rng" in meters instead of "arclen" degrees of arc
184222
185-
If any of inputs lat,lon1,rng are arrays, all must be arrays of same shape
186-
187223
Parameters
188224
----------
189225
lat1 : float
@@ -213,12 +249,20 @@ def loxodrome_direct(
213249
try:
214250
lat1 = atleast_1d(lat1)
215251
rng = atleast_1d(rng)
216-
if (abs(lat1) > pi / 2).any():
252+
253+
if lat1.shape != rng.shape:
254+
if rng.size == 1:
255+
rng = broadcast_to(rng, lat1.shape)
256+
elif lat1.size == 1:
257+
lat1 = broadcast_to(lat1, rng.shape)
258+
else:
259+
raise ValueError("lat1 and rng must each be scalars or same shape")
260+
if (abs(lat1) > pi / 2).any(): # type: ignore
217261
raise ValueError("-90 <= latitude <= 90")
218-
if (rng < 0).any():
262+
if (rng < 0).any(): # type: ignore
219263
raise ValueError("ground distance must be >= 0")
220264
except NameError:
221-
if abs(lat1) > pi / 2:
265+
if abs(lat1) > pi / 2: # type: ignore
222266
raise ValueError("-90 <= latitude <= 90")
223267
if rng < 0:
224268
raise ValueError("ground distance must be >= 0")

src/pymap3d/tests/test_latitude.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,14 @@ def test_numpy_geodetic_geocentric():
4949

5050
@pytest.mark.parametrize(
5151
"geodetic_lat, isometric_lat",
52-
[(0, 0), (90, inf), (-90, -inf), (45, 50.227466), (-45, -50.227466), (89, 271.275)],
52+
[
53+
(0, 0),
54+
(90, inf),
55+
(-90, -inf),
56+
(45, 50.227466),
57+
(-45, -50.227466),
58+
(89, 271.275),
59+
],
5360
)
5461
def test_geodetic_isometric(geodetic_lat, isometric_lat):
5562
isolat = latitude.geodetic2isometric(geodetic_lat)

src/pymap3d/tests/test_rhumb.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,10 +69,12 @@ def test_loxodrome_inverse(lat1, lon1, lat2, lon2, arclen, az):
6969
def test_numpy_loxodrome_inverse():
7070
pytest.importorskip("numpy")
7171
d, a = lox.loxodrome_inverse([40, 40], [-80, -80], 65, -148)
72-
7372
assert d == approx(5248666.209)
7473
assert a == approx(302.00567)
7574

75+
d, a = lox.loxodrome_inverse([40, 40], [-80, -80], [65, 65], -148)
76+
d, a = lox.loxodrome_inverse([40, 40], [-80, -80], 65, [-148, -148])
77+
7678

7779
@pytest.mark.parametrize(
7880
"lat0,lon0,rng,az,lat1,lon1",
@@ -98,9 +100,13 @@ def test_loxodrome_direct(lat0, lon0, rng, az, lat1, lon1):
98100

99101
def test_numpy_loxodrome_direct():
100102
pytest.importorskip("numpy")
101-
lat, lon = lox.loxodrome_direct([40, 40], [-80, -80], [10000, 10000], [30, 30])
102-
assert lat == approx(40.077995)
103-
assert lon == approx(-79.941414)
103+
lat, lon = lox.loxodrome_direct([40, 40], [-80, -80], [10, 10], [30, 30])
104+
assert lat == approx(40.000078)
105+
assert lon == approx(-79.99994145)
106+
107+
lat, lon = lox.loxodrome_direct([40, 40], [-80, -80], 10, 30)
108+
lat, lon = lox.loxodrome_direct([40, 40], [-80, -80], [10, 10], 30)
109+
lat, lon = lox.loxodrome_direct([40, 40], [-80, -80], 10, [30, 30])
104110

105111

106112
@pytest.mark.parametrize("lat,lon", [([0, 45, 90], [0, 45, 90])])

0 commit comments

Comments
 (0)