Optimization: replace iterative Aitoff inverse with closed-form inverse - #4796
Optimization: replace iterative Aitoff inverse with closed-form inverse#4796robertkleffner wants to merge 5 commits into
Conversation
double rho = hypot(x/2, y), // prefer hypot(x, y) to sqrt(x^2 + y^2)
srho = sin(rho), crho = cos(rho);
if (rho > M_PI || srho < 0)
// Need to test srho because with long doubles, we have sin(M_PI) < 0.
return false;
// Arg to asin is guaranteed in [-1, 1], so don't need aasin
phi = rho == 0 ? y : asin((y / rho) * srho);
lam = 2 * atan2(x/2 * srho, rho * crho);
double clam2=cos(lam/2), slam2=sin(lam/2),
cphi=cos(phi), sphi=sin(phi),
crho = cphi * clam2,
srho = hypot(cphi * slam2, sphi),
A = srho == 0 ? 1 : atan2(srho, crho) / srho;
x = 2 * cphi * slam2 * A;
y = sphi * A;
In summary, this is a worthwhile addition (the use of an iterative solution |
Cite Snyder instead, harden numerical accuracy in both forward and inverse, broaden accepted inverse domain
|
@cffk Thanks, this is exactly the sort of review I was looking for.
I will amend the blog post to properly cite Snyder as well. |
shouldn't we then merge aitoff.cpp implementation in aeqd.cpp with 2 new modes in the pj_aeqd_ns::Mode enumeration ? That way we could even get almost for free an ellipsoidal formulation for Aitoff. |
I'm wrong about the errors in the original code for the forward |
In principle, this is simple enough to do. However, I see some downsides:
So I recommend holding off on combining |
I think I was able to address this as well and have a commit on my local branch for it, touching 2 additional files. However, I'm open to keeping it out of this PR so it can focus on the Aitoff portion, and putting it in a follow-on PR if that order of operations is preferred. |
It's fine to include it here, just put it in a separate commit. |
|
Done, added the |
docs/source/*.rstfor new APIJustification
In a side project I've been working on, I uncovered the (already-known) existence of an exact closed-form inverse for the Aitoff projection (in part thanks to an AI agent I was having help build the project). It's fairly straightforward to derive: squaring and adding the forward equations eliminates φ and λ from the right-hand side with some trig identities, leaving the identity (x/2)² + y² = α². Then α in terms of x and y, and thus derivations of φ and λ, follow directly:
This replaces the nested Newton–Raphson inverse in use since 2015. The closed form matches the iterative result to machine precision, is noticeably faster, needs no special case at the poles, and returns longitudes in [−π, π] by construction.
Winkel Tripel shares this file and unfortunately has no closed-form inverse I'm aware of, despite my attempts. I also attempted seeding the Winkel Tripel iteration with the closed-form inverse, but this ended up hampering performance a bit since the iteration converges so quickly in most cases. So, its Newton–Raphson solver and verification loop are retained, and its results are unchanged.
Correctness
Performance
Measured on an M2 MacBook Air with
test/benchmark/bench_proj_trans, inverting via a pipeline:~1.8 → ~11 M coordinates/s = ~6× speedup end-to-end through proj_trans.
References
USGS Professional Paper 1395, pp. 195–197. https://doi.org/10.3133/pp1395
azimuthal equidistant inverse): Justin Kunimune, Map-Projections,
https://github.com/jkunimune/Map-Projections