Skip to content

reproject: LAEA inverse off by up to 2.6 km, AEA/CEA inverse off by ~5 m vs PROJ #3274

Description

@brendancol

Describe the bug

Two numerical bugs in the inverse projection kernels in xrspatial/reproject/_projections.py (and their CUDA twins in _projections_cuda.py) make reprojections into LAEA, AEA, and CEA targets sample the wrong source location.

  1. _laea_inv_point (oblique/equatorial modes) normalizes the input by x / (a * xmf) and y / (a * ymf), which strips the rq factor that PROJ keeps in rho. The angular distance is then computed as sce = 2 * asin(0.5 * rho / rq), dividing by rq a second time. The result is a radial error of about 0.11% of the distance from the projection origin. For EPSG:3035 over Europe that is up to 2.6 km versus pyproj. The CUDA kernel _d_laea_inv has the same formula.

  2. _authalic_apa claims sub-centimetre accuracy from a 6-term inverse authalic latitude series, but the coefficients do not invert _authalic_q. Measured against exact numerical inversion of q(phi), the series has a max error of 4.79 m (7.5e-7 rad). PROJ's standard 3-term series (517/5040, 23/360 + 251/3780, 761/45360) lands at 1.6 mm. The wrong coefficient is visible at leading order in term 2: e417/360 vs PROJ's e423/360. This series feeds the AEA and CEA inverses (_aea_inv_point, _cea_inv_point, plus the inline 3-term copy in _laea_inv_point and the CUDA _d_authalic_inv call sites).

Measured parity vs pyproj (random points in each CRS's region of use):

pair direction max error
EPSG:3035 -> 4326 laea inverse 2643 m
EPSG:5070 -> 4326 aea inverse 4.86 m
EPSG:6933 -> 4326 cea inverse 4.81 m
4326 -> each of the above forward <= 1.1e-5 m

The inverse direction is what reproject() uses to build the sampling grid when the target CRS is LAEA/AEA/CEA, so reproject(raster_4326, 'EPSG:3035') georeferences the output wrong by the amounts above. With the LAEA normalization fixed and PROJ's series coefficients in place, the 3035 inverse parity drops to ~1 mm.

Expected behavior

Inverse fast-path kernels should match pyproj within the same tolerance the forward kernels already achieve (sub-mm to ~1e-5 m).

Reproduction

import numpy as np, pyproj
from xrspatial.reproject._projections import transform_points

tr = pyproj.Transformer.from_crs(4326, 3035, always_xy=True)
lons = np.random.uniform(-10, 30, 500); lats = np.random.uniform(35, 70, 500)
px, py = tr.transform(lons, lats)
tx, ty = transform_points(pyproj.CRS(3035), pyproj.CRS(4326), np.asarray(px), np.asarray(py))
inv = pyproj.Transformer.from_crs(3035, 4326, always_xy=True)
rx, ry = inv.transform(px, py)
err = np.hypot((tx - rx) * np.cos(np.radians(ry)), ty - ry) * 111320
print(err.max())   # ~2600 m

Additional context

Found by /sweep-accuracy against the reproject module. End-to-end check: reproject(..., 'EPSG:3035') with the default fast path vs transform_precision=0 (exact pyproj) differs accordingly; numpy and cupy backends agree with each other because both kernels carry the same bugs.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workinggpuCuPy / CUDA GPU supportsweep-accuracyFound by /sweep-accuracy

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions