1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
//! Half-angle geometry of a geodesic separation on `S²`, carried without ever
//! forming `cos γ`.
//!
//! # Why not `cos γ`
//!
//! Every zonal Wahba kernel in [`super::sphere_kernels`] is a function of the
//! half-angle pair
//!
//! ```text
//! u = sin²(γ/2) = (1 − cos γ)/2 (vanishes at coincidence)
//! v = cos²(γ/2) = (1 + cos γ)/2 (vanishes at the antipode)
//! ```
//!
//! and never of `cos γ` itself — the closed forms are `−ln u`, `Li₂(v)`,
//! `Li₃(u)`, `√u`, and their derivatives are `1/u`, `ln u / v`. Carrying the
//! separation as `cos γ` and recovering `u = (1 − cos γ)/2` at the point of use
//! destroys the answer for nearby points, because `cos γ = 1 − O(γ²)`: the
//! subtraction is exact (Sterbenz), but the information is already gone. Since
//! the spacing below `1.0` is `2⁻⁵³`, a `u` reconstructed from a dot product is
//! *quantized* to multiples of `ε/4 ≈ 5.6e-17`, so it can only be `0`, `ε/4`,
//! `ε/2`, … — measured against a 50-digit reference (#2489), that is `27 %`
//! relative error at `1e-6` degrees of latitude separation, `72×` at `1e-7`,
//! and *exactly zero* below `1e-8`, i.e. two distinct points reported
//! coincident. On Earth-scale coordinates `1e-5` degrees is about a metre.
//!
//! It also breaks rotation invariance of the Gram matrix. A zonal kernel's
//! diagonal is `K(0)` for every point, but whether `sin²φ + cos²φ·(cos²ψ +
//! sin²ψ)` rounds to exactly `1.0` depends on the particular `φ, ψ`: over 24
//! farthest-point centers, 17 landed on `1.0`, 5 one ulp below and 2 two ulps
//! below, giving **three different diagonal entries in one Gram matrix**. A
//! rotation relabels the coordinates and reshuffles which centers round which
//! way, so the shipped matrix was not a function of geodesic distance alone.
//!
//! # The chord form
//!
//! Both halves have a haversine expression that is a sum of non-negative terms,
//! so nothing cancels:
//!
//! ```text
//! u = sin²(Δφ/2) + cos φ · cos φ_c · sin²(Δψ/2)
//! v = sin²((φ + φ_c)/2) + cos φ · cos φ_c · cos²(Δψ/2)
//! ```
//!
//! The `v` form is the `u` form evaluated against the *antipode* of the second
//! point (`φ_c → −φ_c`, `ψ_c → ψ_c + π`), which is the statement
//! `cos²(γ/2) = sin²((π − γ)/2)`. Their sum is `1` analytically:
//! `sin²((φ−φ_c)/2) + sin²((φ+φ_c)/2) = 1 − cos φ cos φ_c`.
//!
//! Each of the four half-angle squares is then taken in **chord form** rather
//! than from a half-angle trig call, because the callers have already
//! precomputed `(sin, cos)` of each latitude and longitude once per point and
//! reuse them across the whole `N × K` grid:
//!
//! ```text
//! 4 sin²(Δθ/2) = (sin θ₁ − sin θ₂)² + (cos θ₁ − cos θ₂)² (chord²)
//! 4 cos²(Δθ/2) = (sin θ₁ + sin θ₂)² + (cos θ₁ + cos θ₂)² (chord² to the antipode)
//! ```
//!
//! Both are `|w₁ ∓ w₂|²` for the unit plane vectors `wᵢ = (cos θᵢ, sin θᵢ)`,
//! and `|w₁ − w₂| = 2|sin(Δθ/2)|` is the chord subtending `Δθ`. This buys the
//! two properties the dot product lacks, for about a dozen extra flops per pair
//! and no transcendental calls:
//!
//! 1. **`u = 0` exactly iff the two coordinates are bitwise equal.** The same
//! `sin_cos` on the same bits gives the same bits, so each difference is
//! exactly `0`. Self-distance becomes a theorem instead of a rounding
//! accident, and the Gram diagonal is one number for every center.
//! 2. **No catastrophic cancellation.** The error in `u` is now set by the
//! absolute error of the stored `sin`/`cos` values (`≈ ε`) against a chord
//! of length `≈ γ`, i.e. `O(ε/γ)` relative — instead of the dot product's
//! `O(ε/γ²)`. At `1e-6` degrees that is `1e-8` instead of `27 %`, and it
//! degrades linearly rather than falling off a cliff.
//!
//! # Genericity
//!
//! [`half_angle_separation`] is written once over any type closed under
//! `+ − ×` that can be built from an `f64` literal, so the scalar path, the
//! `wide::f64x4` SIMD path and the jet path share one derivation rather than
//! three transcriptions of it.
use ;
/// A scalar or SIMD lane type the half-angle algebra can run in.
///
/// Satisfied by `f64` (via the reflexive `From` impl) and by `wide::f64x4`.
pub
/// The precomputed trigonometry of one lat/lon point, in radians.
///
/// Callers build this once per point and reuse it across every pair, which is
/// what makes the chord form cheaper than a per-pair half-angle `sin`.
pub
/// The half-angle pair of a geodesic separation `γ`: `u = sin²(γ/2)` and
/// `v = cos²(γ/2)`, with `u + v = 1` analytically.
///
/// `u` resolves the coincident end to full relative precision and `v` the
/// antipodal end; keeping both is what lets the Sobolev closed forms be
/// evaluated accurately at *either* singular end without reconstructing one
/// from the other (see [`super::sphere_kernels`], which needs `−ln u` near
/// `γ = 0` and `ln(1 − v)` near `γ = π`).
pub
/// `u = sin²(γ/2)` and `v = cos²(γ/2)` from the precomputed trigonometry of
/// two lat/lon points, in chord form.
///
/// Returns `(u, v)` in the lane type, so the SIMD caller gets four pairs at
/// once. See the module docs for the derivation; in brief, with
/// `cc = cos φ · cos φ_c`,
///
/// ```text
/// u = ¼[(sinφ − sinφ_c)² + (cosφ − cosφ_c)²] + cc · ¼[(sinψ − sinψ_c)² + (cosψ − cosψ_c)²]
/// v = ¼[(sinφ + sinφ_c)² + (cosφ − cosφ_c)²] + cc · ¼[(sinψ + sinψ_c)² + (cosψ + cosψ_c)²]
/// ```
///
/// Note that `v`'s latitude term reuses the *difference* of cosines: reflecting
/// `φ_c → −φ_c` flips the sign of `sin φ_c` and leaves `cos φ_c` alone.
pub
/// Scalar [`half_angle_separation`] packaged as a [`HalfAngleSeparation`], with
/// both halves clamped into `[0, 1]`.
///
/// The clamp is a range assertion, not a regularization: every summand is a
/// square or a product of non-negative cosines, so the exact values already lie
/// in `[0, 1]` and only the final roundings of `u + v = 1` can push a hair
/// outside.
pub
/// `u = sin²(γ/2)` and `v = cos²(γ/2)` for two AMBIENT unit vectors on
/// `S^{dim−1}`, where `cos γ = t · c`.
///
/// The chord form is even more direct here than in lat/lon coordinates, and
/// is an identity for unit vectors rather than a trigonometric rearrangement:
///
/// ```text
/// |t − c|² = |t|² + |c|² − 2 t·c = 2(1 − cos γ) = 4 sin²(γ/2)
/// |t + c|² = |t|² + |c|² + 2 t·c = 2(1 + cos γ) = 4 cos²(γ/2)
/// ```
///
/// so `u = |t − c|²/4` and `v = |t + c|²/4`. As in the lat/lon case, each is a
/// sum of squares — nothing cancels, `u` is exactly `0` iff the two vectors are
/// bitwise equal, and `v` is exactly `0` iff they are exact antipodes — whereas
/// `1 − t·c` throws away every bit below `2⁻⁵³` of the separation.
///
/// The two halves are normalized against `(|t|² + |c|²)/2` rather than against
/// the nominal `1`, which is what makes `u + v = 1` hold to a rounding even
/// when the inputs are unit vectors only to within their own storage error.
pub
/// `∂u/∂φ` and `∂u/∂ψ` at the first point, in radian space.
///
/// The jet of a zonal kernel is `dK/du · ∂u/∂(φ, ψ)`, and taking it in `u`
/// rather than in `cos γ` is what makes the coincident limit computable at all.
/// With `cos γ = 1 − 2u`,
///
/// ```text
/// dK/d(cos γ) · ∂(cos γ)/∂φ = (dK/du · (−½)) · (−2 · ∂u/∂φ) = dK/du · ∂u/∂φ
/// ```
///
/// so the two divergent factors of the `cos γ` chain — `dK/d(cos γ) ~ 1/γ`
/// against `∂(cos γ)/∂φ ~ γ`, whose product is the finite `|γ|` cusp gradient —
/// never appear separately. The old form recovered that finite limit as a
/// numerical `∞ · 0`, and lost it: measured against the constant true value
/// `−0.002778` per degree for the pseudo `m = 1` cusp, it was `+17 %` off at
/// `1e-6°`, `−99 %` at `1e-10°`, and `−100 %` (exactly zero) *at* a center —
/// which is reached in ordinary use, since farthest-point selection picks
/// centers from the data rows themselves.
///
/// The partials are
///
/// ```text
/// ∂u/∂φ = ½ sin(Δφ) − sin φ · cos φ_c · sin²(Δψ/2)
/// ∂u/∂ψ = ½ cos φ · cos φ_c · sin(Δψ)
/// ```
///
/// with `sin(Δθ) = sin θ · cos θ_c − cos θ · sin θ_c` from the precomputed
/// values, which is exactly `0` when the two angles are bitwise equal. So a row
/// sitting on a center yields `∂u/∂φ = ∂u/∂ψ = 0` exactly, and the caller
/// resolves the cusp there rather than multiplying zero by an infinity.
///
/// Consistency check against the `cos γ` form these replace:
/// `−2 ∂u/∂φ = cos φ sin φ_c − sin φ cos φ_c cos Δψ = ∂(cos γ)/∂φ` and
/// `−2 ∂u/∂ψ = −cos φ cos φ_c sin Δψ = ∂(cos γ)/∂ψ`.
pub