Skip to content

Commit f155a78

Browse files
committed
Fix LAEA xmf/ymf swap, re-enable in dispatch (#1045)
The oblique-mode LAEA had xmf and ymf swapped (rq/dd vs rq*dd). Research agent found the bug by comparing against PROJ's laea.cpp source. Forward accuracy is now sub-millimeter vs pyproj.
1 parent 5ccc5f6 commit f155a78

File tree

2 files changed

+29
-12
lines changed

2 files changed

+29
-12
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,7 @@ Built-in Numba JIT and CUDA projection kernels bypass pyproj for common CRS pair
226226
| Albers Equal Area | 5070 | ✅️ | ✅️ |
227227
| Cylindrical Equal Area | 6933 | ✅️ | ✅️ |
228228
| Sinusoidal | MODIS grids | ✅️ | |
229+
| Lambert Azimuthal Equal Area | 3035, 6931, 6932 | ✅️ | |
229230
| Polar Stereographic | 3031, 3413, 3996 | ✅️ | |
230231

231232
Other CRS pairs fall back to pyproj automatically.

xrspatial/reproject/_projections.py

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -621,16 +621,16 @@ def _laea_params(crs):
621621
cosb1 = math.sqrt(1.0 - sinb1 * sinb1)
622622
m1 = math.cos(lat_0) / math.sqrt(1.0 - e2 * sinphi0 * sinphi0)
623623
dd = m1 / (rq * cosb1)
624-
# PROJ applies 'a' outside the projection function
625-
xmf = rq / dd
626-
ymf = rq * dd
624+
# PROJ: xmf = rq * dd, ymf = rq / dd
625+
xmf = rq * dd
626+
ymf = rq / dd
627627
elif mode == 1: # EQUIT
628628
sinb1 = 0.0
629629
cosb1 = 1.0
630630
m1 = math.cos(lat_0) / math.sqrt(1.0 - e2 * math.sin(lat_0)**2)
631631
dd = m1 / rq
632-
xmf = rq / dd
633-
ymf = rq * dd
632+
xmf = rq * dd
633+
ymf = rq / dd
634634
else: # POLAR
635635
sinb1 = 1.0 if mode == 2 else -1.0
636636
cosb1 = 0.0
@@ -705,8 +705,9 @@ def _laea_inv_point(x, y, lon0, sinb1, cosb1,
705705
else:
706706
lam = math.atan2(x_a, -y_a)
707707
else: # OBLIQ or EQUIT
708-
xn = x / (a * xmf)
709-
yn = y / (a * ymf)
708+
# PROJ: x /= dd, y *= dd (undo the xmf/ymf scaling)
709+
xn = x / (a * xmf) # = x / (a * rq * dd)
710+
yn = y / (a * ymf) # = y / (a * rq / dd) = y * dd / (a * rq)
710711
rho = math.hypot(xn, yn)
711712
if rho < 1e-30:
712713
return math.degrees(lon0), math.degrees(math.asin(sinb1))
@@ -1367,11 +1368,26 @@ def try_numba_transform(src_crs, tgt_crs, chunk_bounds, chunk_shape):
13671368
return (src_y_flat.reshape(height, width),
13681369
src_x_flat.reshape(height, width))
13691370

1370-
# LAEA -- disabled pending investigation of ~940m oblique-mode error
1371-
# if _is_geographic_wgs84_or_nad83(src_epsg):
1372-
# params = _laea_params(tgt_crs)
1373-
# ...
1374-
# Falls through to pyproj for now.
1371+
# LAEA
1372+
if _is_geographic_wgs84_or_nad83(src_epsg):
1373+
params = _laea_params(tgt_crs)
1374+
if params is not None:
1375+
lon0, lat0, sinb1, cosb1, dd, xmf, ymf, rq, qp, fe, fn, mode = params
1376+
laea_inverse(out_x_flat, out_y_flat, src_x_flat, src_y_flat,
1377+
lon0, sinb1, cosb1, xmf, ymf, rq, qp,
1378+
fe, fn, _WGS84_E, _WGS84_A, _WGS84_E2, mode, _APA)
1379+
return (src_y_flat.reshape(height, width),
1380+
src_x_flat.reshape(height, width))
1381+
1382+
if _is_geographic_wgs84_or_nad83(tgt_epsg):
1383+
params = _laea_params(src_crs)
1384+
if params is not None:
1385+
lon0, lat0, sinb1, cosb1, dd, xmf, ymf, rq, qp, fe, fn, mode = params
1386+
laea_forward(out_x_flat, out_y_flat, src_x_flat, src_y_flat,
1387+
lon0, sinb1, cosb1, xmf, ymf, rq, qp,
1388+
fe, fn, _WGS84_E, _WGS84_A, _WGS84_E2, mode)
1389+
return (src_y_flat.reshape(height, width),
1390+
src_x_flat.reshape(height, width))
13751391

13761392
# Polar Stereographic
13771393
if _is_geographic_wgs84_or_nad83(src_epsg):

0 commit comments

Comments
 (0)