You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
I do not come in peace; I am here on a mission. I wish to vanquish the angles and trig which run amok
To my knowledge, this seems to stem from the thought:
The user provides and requests angles, therefore my formulas should input and output angles
At the very least, this is untrue in that the user provides degrees and we compute with radians
But more pervasively, I argue that the formulas are first and foremost Geometric and not Trigonometric
To make my case, I will go through common formulas both in terms of latitude, longitude ($\phi$, $\lambda$), and unit vectors of $ℝ^3$
Even if you remain unconvinced that you should prefer the use of the vectors, over angles and trigonometry,
I hope it provides a more intuitive form and derivation for why they work and where they come from
Conversions between latitude, longitude $({\phi \atop \lambda})$ and unit vectors of $ℝ^3$
We'll use $N$ to denote the North pole, defined as $\ \phi = \frac{\pi}{2} \ \ \equiv \ \hat{z}$
and $L$ where the Prime meridian meets the Equator $\ ({\phi = 0 \atop \lambda = 0}) \ \ \equiv \hat{x}$
Important that from here on out, $\text{atan2} ( \thinspace \tfrac{y}{x} \thinspace )$ denotes atan2(y, x) as two parameters without division
And, while $V$ always outputs a unit vector, the input of $𝒜$ need not be normalized
Rotations
We'll start by doing a head-count of length preserving linear transforms. Namely, reflections and rotations
Reflection through the origin is simple enough: $P \to {-P}$
Reflection through plane $\hat{n}$ is similarly familiar: $P \to P - 2 \thinspace (P \cdot \hat{n})\hat{n}$
Rotations, on the other hand, deserve a bit more attention. We'll compare matrices and quaternions, which for our purposes can be thought of as a convenient and lightweight representation of rotations around arbitrary axes
While I won't be giving an in-depth exposition, I'll briefly list the notation, descriptions, and costs
So if we need to rotate a single vector $v$ around $\hat{n}$ by $\theta$
Construct then apply matrix: $\text{1 sincos, 18 adds, 25 muls}$
Construct then apply quat: $\text{1 sincos, 12 adds, 22 muls}$
A couple things to note:
To transform more than 1 vector by the same rotation, matrix counts are increasingly cheaper
These are scalar op counts, they don't account for fma or SIMD usage, where matrices fare better
Op counts for quaternion application and conversion presume unit-quaternions.
All quaternions of this post are normalized by construction so I'll make no further note of this
While the rest of this post treats quaternions as a drop-in for rotations, it would be remiss of me not to mention, at least in passing, the much richer structure underlying the quaternions beyond just their algebraic and computational properties. While not the topic of this post, I may one day feel motivated to expand on this later
The need to ensure $\lambda$ is normalized within $\pm\pi$ will be a common pattern for angles but thankfully absent for vectors
Great Circle Distance $\theta$ from $A$ to $B$
$$ \cos\theta = \sin\phi_A\ \sin\phi_B \ + \ \cos\phi_A\ \cos\phi_B\ \cos\ \Delta \lambda $$$$ \cos\theta = A \cdot B $$
It's important to note that this computes the $\cos\theta$ rather than $\theta$ itself
The reason is simple, for most computations $\cos\theta$ and/or $\sin\theta$ are the values you actually end up using
There are, however, some computations which require $\theta$, though I claim that $\text{acos}$ and $\text{asin}$ are still a poor fit
So while I feel the need to stress the importance in avoiding $\text{acos}$ and $\text{asin}$, it doesn't provide a full picture
Numerically, $\text{acos}(v)$ is ill-conditioned in near-parallel cases, while $\text{asin}(v)$ is ill-conditioned in near-perpendicular cases
The role of $\text{atan2}(y,x)$ is to be well-conditioned for all cases without requiring clamping to [-1,1] to avoid NaNs
First is the haversine formula, where the denominator is $\cos\theta$ shown above, hinting the numerator to be $\sin\theta$
The vector expressions are refreshingly clear. The last one returning twice the bisected angle
We can similarly improve the robustness of $a \times b$ by considering
$$ (a - b) \times (a+b) = 2 \ (a \times b) $$
Which allows us to always compute the cross-product of orthogonal vectors
And use Difference of Products if even more precision is required (and fma is available)
True Course $\gamma$ from $A$ to $B$ (when $A \neq \pm N$)
The last expression is doing a quaternion product of $q_\theta$ and $q_\gamma$ which is applied to $A$
Reading right-to-left, we start at $A$, move "up" (towards north) until $\theta$, then rotating clockwise around $A$ by $\gamma$
We start by considering the $\lambda=0$ case first, then later apply a rotation by $\lambda$ to generalize
We form $L \times N$, a plane through the origin containing $\lambda=0$ then intersect it with $A \times B$, the plane of the great circle
The intersection of two distinct planes through the origin is a line through the origin, so the correct result is either $\pm Q$
We must now consider that our plane contains both $\lambda=0$ and $\lambda = \pm \tau / 2$, so we select $L$ which bisects $\lambda=0$
Now test whether $+Q$ is within a quarter-turn of $L$. If it's not, then $-Q$ must be within a quarter-turn of $L$
Alternatively, consider the plane with normal $\hat{n} = L$ whose upper half-space, determined by $\forall P\ (P \cdot \hat{n} \ge 0)$, contains $\lambda=0$
While distance to a point may initially be more intuitive, planes appear often enough that it's worthwhile to build familiarity
Now, that we've solidified our understanding we can apply some optimizations
$$ \begin{gather*}
q_\lambda[L \times N] && q_\lambda[L] \\
q_\lambda[{-\hat{y}}] && q_\lambda[\hat{x}] \\
{-T_y} && T_x \\
\end{gather*} $$$$ \text{where $T$ is the matrix of } q_\lambda $$
So instead of doing 2 quaternion applications, costing $\text{24 adds, 36 muls}$
We need only convert to a matrix, costing merely $\text{12 adds, 12 muls}$
And since the axis of rotation is $N = \hat{z}$ this cost becomes $\text{0 adds, 0 muls, 1 sincos}$
I have no issue with applying these optimizations. In fact, I enjoy and encourage doing so
But it should come only after the principles are understood
Midpoint $M$ of $A$ to $B$ on Line of constant $\lambda$
While the others had some plausible deniability, this just converts angles to vectors, calls slerp, and converts back
Ironically, the vector $\text{slerp}$ is doing something similar. Computing an angle, and converting the angle back to a vector
At a high level, we first compute $\theta_t = \text{lerp}(0, \theta, t) = \theta \cdot t$ where $\theta$ is the angle between $A$ and $B$
Then take $\{A,\ A^{\perp}\}$ as a 2D Graham-Schimdt of $\{A,\ B\}$ and return $(\cos\theta_t \cdot A \ + \ \sin\theta_t \cdot A^{\perp})$
We may be tempted to use $\text{normalize(lerp}(A,\ B,\ t))$ but this fails the requirement $\forall t \left(A \cdot \text{interpolate}(A,\ B,\ t) = \cos\theta_t \right)$
It will still be geodesic, monotonic, and symmetric, with smaller arcs yielding smaller errors; so worthwhile to consider
Furthermore, t can be refit to approximate $\text{slerp}$. (this approximates quaternion slerp, with slightly different considerations)
Exercise: Subdivide $A$, $B$ into arcs no longer than $\theta$
Find $n = \lceil \theta_{AB} / \theta \rceil$ then compute $P_i = \text{slerp}(A,\ B,\ i/n)$ for $i \in [0, n]$
This is a good enough starting point, but it can be optimized further :)
Distance $\theta$ from $P$ to Great Circle segment $AB$
$$ \begin{align*}
&{\atop \text{let }\ \hat{n} = }{\over{A \times B}} {\atop \ \text{ and }\ h = P \cdot \hat{n}}\\
&\text{let }\ Q = P - h\ \hat{n} \\
&\cos\theta =
\begin{cases}
P \cdot \overline{Q} \quad \text{if both }
{{Q \cdot (\hat{n} \times A) > 0} \atop
{Q \cdot (B \times \hat{n}) > 0}} \\
\text{max}(P \cdot A,\ P \cdot B)
\end{cases}
\end{align*} $$
When we try to implement this in code:
// cosTheta from P to arc AB using anglesletbearAB=Bearing(latA,lonA,latB,lonB);// 1*atan2 + 3*sincosletbearAP=Bearing(latA,lonA,latP,lonP);// 1*atan2 + 3*sincoslet[cosAP,sinAP]=CS_Dist(latA,lonA,latP,lonP);// 1*sqrt + 3*sincosletcosBP=C_Dist(latB,lonB,latP,lonP);// 3*sincosvardiff=abs(bearAP-bearAB);if(diff>.pi){diff=.tau-diff;}if(diff>.pi/2){returnmax(cosAP,cosBP);// min_total: 2*atan2 + 1*sqrt + 12*sincos}letcosAB=C_Dist(latA,lonA,latB,lonB);// 3*sincosletsinPQ=sinAP*sin(bearAP-bearAB);// 1*sin/cosletcosPQ=sqrt(1-sinPQ*sinPQ);// 1*sqrtletcosAQ=cosAP/cosPQ;return(cosAQ<cosAB ? cosBP : cosPQ);// max_total: 2*atan2 + 2*sqrt + 16*sincos
// cosTheta from P to arc AB using vectorsletn=normalize(cross(A,B));// 1*sqrtleth=dot(P,n);letQ=P-h*n;letinA=dot(Q,cross(n,A))>0;letinB=dot(Q,cross(B,n))>0;return(inA&&inB ? dot(P,normalize(Q))// 1*sqrt
: max(dot(P,A),dot(P,B)));
While the pure vector cost is 2 sqrt's, if we consider the cost of $V:\ (\phi, \lambda) \to ℝ^3$ there's an additional 6 sincos
Still much more efficient than the proposed method using angles, but let's try and do better
While projecting $P$ to $Q$ is helpful for our initial formulation, it turns out that this is unecessary
In order to avoid computing $Q$ and $\overline{Q}$, we'll start by observing:
$$ \begin{align*}
Q \cdot (\hat{n} \times A) &= (P - h\ \hat{n}) \cdot (\hat{n} \times A) \\
&= P \cdot (\hat{n} \times A) \ - \ (h\ \hat{n} \cdot (\hat{n} \times A)) \\
&= P \cdot (\hat{n} \times A) \qquad \text{since }\ \hat{n} \perp \hat{n} \times A \\
\\
Q \cdot (B \times \hat{n}) &= P \cdot (B \times \hat{n}) \qquad \text{likewise}
\end{align*} $$
Since the projection is orthogonal, it forms a right triangle $\triangle OQP$ allowing us to substitute:
$$ \cos\theta = P \cdot \overline{Q} = \sqrt{1-h²} $$
The use of $h^2$ instead of $h$ allows us to go even further:
While we could call it quits here, lets see if we can take one last stab at it
If we inline ${\overset{⇀} n} = A \times B$ we can apply the vector triple product and hoist common subexpressions
$$ \begin{align}
{\overset{⇀} n} \times A &= \ \ (A \times B) \times A &= \ \ B(A \cdot A) - A(A \cdot B) &= \ \ B - A(A \cdot B) \\
B \times {\overset{⇀} n} &= \ \ B \times (A \times B) &= \ \ A(B \cdot B) - B(B \cdot A) &= \ \ A - B(A \cdot B) \\
\\
&\quad &\quad \text{since }\ A \cdot A = B \cdot B = 1 &
\end{align} $$
$$ \begin{align*}
P \cdot ({\overset{⇀} n} \times A) > 0 &\equiv \quad P \cdot (B - A(A \cdot B)) > 0 &\equiv \quad P \cdot B - (A \cdot B) (P \cdot A) > 0 &\equiv \quad P \cdot B > (A \cdot B) (P \cdot A) \\
P \cdot (B \times {\overset{⇀} n}) > 0 &\equiv \quad P \cdot (A - B(A \cdot B)) > 0 &\equiv \quad P \cdot A - (A \cdot B) (P \cdot B) > 0 &\equiv \quad P \cdot A > (A \cdot B) (P \cdot B) \\
\end{align*} $$
Takes us from $\text{10 adds, 18 muls}$ down to $\text{4 adds, 5 muls}$ since we already take dot(P, A) and dot(P, B)
Breaking it down by case: interior saves $\text{2 adds, 7 muls}$ while exterior saves $\text{8 adds, 18 muls}$
Distance $\theta$ from $P$ to major Great Circle segment $AB$
Now that we know how to do minor arcs, we could extend this to major arcs as such:
Which seems innocent enough, but this is where the individual costs start adding up
While the vector code is cheaper, we're also in a better position to reason about it
Since we're detecting interior vs exterior, we can simply flip those cases and be done
Distance $\theta$ from $P$ to Small Circle segment $AB$ with pole $\hat{n}$
For Great Circles, $\hat{n} = A \times B$ is taken to mean “the minor arc of the Great Circle from $A$ to $B$ around $\hat{n}$”
It's a baked in assumption that Great Circles are used as geodesic/shortest paths, so of course we want the minor arc, and if someone wants the major arc, then they should be explicit about that
So when we're instead given a pole $\hat{n}$ equidistant from $A$ and $B$, it raises an interesting question:
Do we go from A to B “along the minor arc” or “around $\hat{n}$”? (since they're no longer one and the same)
I argue for “from $A$ to $B$ around $\hat{n}$” allows for more flexibility since if $\hat{n}$ is a minor arc, then ${-\hat{n}}$ is a major arc
We can check whether $\hat{n}$ and $A \times B$ point in the same direction, in which case we use the minor arc within the intersection of two half-spaces, but if they point in opposite directions, we use the major arc within the union of those half-spaces
$$ \begin{align*}
&s = {-\text{sgn}(\hat{n} \cdot (A \times B))}\\
&s_A = s\ P \cdot (\hat{n} \times A) \\
&s_B = s\ P \cdot (B \times \hat{n}) \\
&\text{inside}\ \equiv\ s \cdot \text{max}(s_A,\ s_B) > 0
\end{align*} $$
The inspiration here comes from SDF operations and min/max identities (also a kind of De Morgan's law)
Since SDFs use the convention $d<0$ for inside, $s = {-\text{sgn(...)}}$ flips the signs which were previously tested for $d>0$
This way, $\text{max($s_A$, $s_B$)}$ is read “the intersection of $s_A$ and $s_B$” to match the SDF convention for $\text{max}$
When the sign of $s$ flips, it turns $\text{max}(s_A,\ s_B) \to {-\text{max}}({-s_A},\ {-s_B}) \equiv \text{min}(s_A,\ s_B)$ which is now “the union of $s_A$ and $s_B$”
So with that out of the way, let's take a shot at computing some distances
$$ \begin{align*}
&\text{let }\ \delta = \hat{n} \cdot A = \hat{n} \cdot B \ \text{ and }\ O = \delta \hat{n} \\
&\text{let }\ \ell = \sqrt{1 - \delta²} = \Vert A - O \Vert = \Vert B - O \Vert \\
&\text{let }\ Q = P - \hat{n}(\hat{n} \cdot P - \delta) \\
&{\atop \text{let }\ \overline{Q} = }{\over{Q-O}} {\atop \ \ell + O} \\
&\cos\theta =
\begin{cases}
P \cdot \overline{Q} \quad \text{if inside} \\
\text{max}(P \cdot A,\ P \cdot B)
\end{cases}
\end{align*} $$
As you may have guessed, our use of $\overline{Q}$ is only as a stepping stone towards a better solution
We'll start by working backwards from $\cos\theta = P \cdot \overline{Q}$ by solving in terms of the squared chord of the triangle
$$ \begin{align*}
&\delta = \hat{n} \cdot A = \hat{n} \cdot B \ \text{ and }\ O = \delta \hat{n} \\
&\ell = \sqrt{1 - \delta²} = \Vert \overline{Q} - O \Vert = (b+c) \\
&m = \hat{n} \cdot P \\
&h = m - \delta \\
&h² = (m - \delta)² = m² + \delta² - 2 m \delta \\
\Rightarrow &h² - m² - \delta² = {-2 m \delta}
\end{align*} $$
$$ \begin{align*}
d² = \Vert P - O \Vert² &= \Vert P \Vert² + \Vert O \Vert² - 2\ P \cdot O \\
&= 1² + \delta² - 2 \delta m \\
&= 1 + \delta² + (h² - m² - \delta²) \\
&= 1 + h² - m²
\end{align*} $$
$$ \begin{align*}
\quad \alpha² \ = \ h² + b² &= h² + \ell² + c² - 2 \ell c \\
&= h² + (1-\delta²) + (1-m²) - 2 \ell c \\
&= 2 - 2 \delta m - 2 \ell c \\
\\
\Rightarrow \cos\theta = 1- \tfrac{\alpha²}{2} &= \ell c + \delta m \\
&= \sqrt{1-\delta²} \sqrt{1-m²} + \delta m \\
&= \sqrt{\smash[b]{(}1-\delta²\smash[b]{)(}1-m²\smash[b]{)}} + \delta m \\
\end{align*} $$
So boiling this all down, we get to a simpler, and notably familiar:
$$ \begin{align*}
&\text{let }\ \delta = \hat{n} \cdot A = \hat{n} \cdot B \ \text{ and }\ m = \hat{n} \cdot P \\
&\cos\theta =
\begin{cases}
\sqrt{\smash[b]{(1-\delta²)(1-m²)}} + \delta m \quad \text{if inside} \\
\text{max}(P \cdot A,\ P \cdot B)
\end{cases}
\end{align*} $$
This familiarity serves as an intruiging sanity check
Small Circles are formed by any plane which cuts the sphere, including those through the origin forming Great Circles
When planes contain the origin, $\hat{n} \cdot A = \hat{n} \cdot B = \delta = 0 \Rightarrow \hat{n} \perp \text{span}\{A,\ B\} \text{ and } h = m = P \cdot \hat{n} - 0$
This gives an end result of $\cos\theta = \sqrt{1-h²} + 0$ which coincides with our previous formula