1pub const BINS: f64 = 128.0;
22
23pub const PROBE_EPS: f64 = 0.0025;
28
29const TIE_EPS: f64 = 0.015;
32
33const PAIRS: [(usize, usize); 6] = [(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)];
35
36#[must_use]
42pub fn canonical(p: &[(f64, f64); 4]) -> Option<([usize; 4], [f64; 4], f64)> {
43 let mut totals = [0.0f64; 4];
44 let mut max_edge = 0.0f64;
45 for &(i, j) in &PAIRS {
46 let d = (p[i].0 - p[j].0).hypot(p[i].1 - p[j].1);
47 totals[i] += d;
48 totals[j] += d;
49 max_edge = max_edge.max(d);
50 }
51 if max_edge <= 0.0 || !max_edge.is_finite() {
52 return None;
53 }
54 let mut order = [0usize, 1, 2, 3];
55 order.sort_by(|&a, &b| totals[a].total_cmp(&totals[b]).then(a.cmp(&b)));
56 let sorted = order.map(|i| totals[i]);
57 Some((order, sorted, max_edge))
58}
59
60#[must_use]
64pub fn orderings(order: [usize; 4], totals: [f64; 4]) -> Vec<[usize; 4]> {
65 let mut out = vec![order];
66 for k in 0..3 {
67 let tied = (totals[k + 1] - totals[k]) <= TIE_EPS * totals[k + 1];
68 if tied {
69 let n = out.len();
70 for i in 0..n {
71 let mut o = out[i];
72 o.swap(k, k + 1);
73 if !out.contains(&o) {
74 out.push(o);
75 }
76 }
77 }
78 }
79 out
80}
81
82#[must_use]
84pub fn descriptor(p: &[(f64, f64); 4]) -> [f64; 5] {
85 let mut e = [0.0f64; 6];
86 for (k, &(i, j)) in PAIRS.iter().enumerate() {
87 e[k] = (p[i].0 - p[j].0).hypot(p[i].1 - p[j].1);
88 }
89 e.sort_by(f64::total_cmp);
90 let m = e[5].max(1e-300);
91 [e[0] / m, e[1] / m, e[2] / m, e[3] / m, e[4] / m]
92}
93
94#[inline]
95fn bin(v: f64) -> i64 {
96 ((v * BINS) as i64).clamp(0, BINS as i64 - 1)
97}
98
99#[must_use]
102pub fn key(d: &[f64; 5]) -> u64 {
103 d.iter().fold(0u64, |k, &v| (k << 8) | bin(v) as u64)
104}
105
106pub fn probe_keys(d: &[f64; 5], out: &mut Vec<u64>) {
110 out.clear();
111 let mut opts = [[0i64; 2]; 5];
112 let mut n_opts = [1usize; 5];
113 for (dim, &v) in d.iter().enumerate() {
114 let b = bin(v);
115 opts[dim][0] = b;
116 let frac = v * BINS - b as f64;
117 if frac < PROBE_EPS * BINS && b > 0 {
118 opts[dim][1] = b - 1;
119 n_opts[dim] = 2;
120 } else if frac > 1.0 - PROBE_EPS * BINS && b < BINS as i64 - 1 {
121 opts[dim][1] = b + 1;
122 n_opts[dim] = 2;
123 }
124 }
125 let total: usize = n_opts.iter().product();
126 for combo in 0..total {
127 let mut rest = combo;
128 let mut k = 0u64;
129 for dim in 0..5 {
130 let pick = rest % n_opts[dim];
131 rest /= n_opts[dim];
132 k = (k << 8) | opts[dim][pick] as u64;
133 }
134 out.push(k);
135 }
136}
137
138#[inline]
142#[must_use]
143pub fn unit(ra: f64, dec: f64) -> [f64; 3] {
144 let (sr, cr) = ra.sin_cos();
145 let (sd, cd) = dec.sin_cos();
146 [cd * cr, cd * sr, sd]
147}
148
149#[inline]
150fn dot(a: &[f64; 3], b: &[f64; 3]) -> f64 {
151 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
152}
153
154#[derive(Debug, Clone, Copy)]
157pub struct Tangent {
158 pub c: [f64; 3],
160 e: [f64; 3],
161 n: [f64; 3],
162}
163
164impl Tangent {
165 #[must_use]
168 pub fn at(c: [f64; 3]) -> Option<Self> {
169 let norm = dot(&c, &c).sqrt();
170 if norm <= 0.0 {
171 return None;
172 }
173 let c = [c[0] / norm, c[1] / norm, c[2] / norm];
174 let h = c[0].hypot(c[1]);
175 if h < 1e-12 {
176 return None;
177 }
178 let e = [-c[1] / h, c[0] / h, 0.0];
179 let n = [-c[2] * e[1], c[2] * e[0], h];
180 Some(Self { c, e, n })
181 }
182
183 #[must_use]
185 pub fn at_radec(ra: f64, dec: f64) -> Option<Self> {
186 Self::at(unit(ra, dec))
187 }
188
189 #[inline]
191 #[must_use]
192 pub fn project(&self, u: &[f64; 3]) -> Option<(f64, f64)> {
193 let depth = dot(u, &self.c);
194 if depth <= 1e-9 {
195 return None;
196 }
197 Some((dot(u, &self.e) / depth, dot(u, &self.n) / depth))
198 }
199
200 #[must_use]
202 pub fn deproject(&self, xi: f64, eta: f64) -> (f64, f64) {
203 let v = [
204 self.c[0] + xi * self.e[0] + eta * self.n[0],
205 self.c[1] + xi * self.e[1] + eta * self.n[1],
206 self.c[2] + xi * self.e[2] + eta * self.n[2],
207 ];
208 let norm = dot(&v, &v).sqrt();
209 let ra = v[1].atan2(v[0]).rem_euclid(2.0 * core::f64::consts::PI);
210 (ra, (v[2] / norm).clamp(-1.0, 1.0).asin())
211 }
212}
213
214#[must_use]
218pub fn fit_affine(p: &[(f64, f64)], q: &[(f64, f64)]) -> Option<[f64; 6]> {
219 let n = p.len().min(q.len());
220 if n < 3 {
221 return None;
222 }
223 let (mut mx, mut my, mut nx, mut ny) = (0.0, 0.0, 0.0, 0.0);
225 for i in 0..n {
226 mx += p[i].0;
227 my += p[i].1;
228 nx += q[i].0;
229 ny += q[i].1;
230 }
231 let k = n as f64;
232 let (mx, my, nx, ny) = (mx / k, my / k, nx / k, ny / k);
233 let (mut sxx, mut sxy, mut syy) = (0.0, 0.0, 0.0);
234 let (mut ux, mut uy, mut vx, mut vy) = (0.0, 0.0, 0.0, 0.0);
235 for i in 0..n {
236 let (x, y) = (p[i].0 - mx, p[i].1 - my);
237 let (u, v) = (q[i].0 - nx, q[i].1 - ny);
238 sxx += x * x;
239 sxy += x * y;
240 syy += y * y;
241 ux += u * x;
242 uy += u * y;
243 vx += v * x;
244 vy += v * y;
245 }
246 let det = sxx * syy - sxy * sxy;
247 if det.abs() <= 1e-12 * (sxx * syy).max(1e-300) {
248 return None;
249 }
250 let a = (ux * syy - uy * sxy) / det;
251 let b = (uy * sxx - ux * sxy) / det;
252 let d = (vx * syy - vy * sxy) / det;
253 let e = (vy * sxx - vx * sxy) / det;
254 Some([a, b, nx - a * mx - b * my, d, e, ny - d * mx - e * my])
255}
256
257#[must_use]
261pub fn shape_error(m: &[f64; 6]) -> f64 {
262 let c1 = m[0].hypot(m[3]);
263 let c2 = m[1].hypot(m[4]);
264 if c1 <= 0.0 || c2 <= 0.0 {
265 return f64::INFINITY;
266 }
267 let ortho = (m[0] * m[1] + m[3] * m[4]).abs() / (c1 * c2);
268 ortho.max((c1 / c2 - 1.0).abs())
269}
270
271#[must_use]
273pub fn invert_affine(m: &[f64; 6]) -> Option<[f64; 6]> {
274 let det = m[0] * m[4] - m[1] * m[3];
275 if det.abs() < 1e-300 {
276 return None;
277 }
278 let (a, b, d, e) = (m[4] / det, -m[1] / det, -m[3] / det, m[0] / det);
279 Some([a, b, -(a * m[2] + b * m[5]), d, e, -(d * m[2] + e * m[5])])
280}
281
282#[cfg(test)]
283mod tests {
284 use super::*;
285
286 fn quad() -> [(f64, f64); 4] {
287 [(0.0, 0.0), (10.0, 1.0), (3.0, 7.0), (8.0, 9.5)]
288 }
289
290 fn transform(p: &[(f64, f64); 4], s: f64, th: f64, flip: bool) -> [(f64, f64); 4] {
291 let (st, ct) = th.sin_cos();
292 p.map(|(x, y)| {
293 let x = if flip { -x } else { x };
294 (s * (ct * x - st * y) + 100.0, s * (st * x + ct * y) - 40.0)
295 })
296 }
297
298 #[test]
299 fn descriptor_and_key_are_similarity_invariant() {
300 let q = quad();
301 let k = key(&descriptor(&q));
302 for (s, th, flip) in [(2.0, 0.3, false), (0.01, 2.0, true), (37.0, -1.0, true)] {
303 let t = transform(&q, s, th, flip);
304 let d = descriptor(&t);
305 let mut keys = Vec::new();
306 probe_keys(&d, &mut keys);
307 assert!(keys.contains(&k), "{s} {th} {flip}");
308 }
309 }
310
311 #[test]
312 fn the_canonical_order_gives_the_correspondence() {
313 let q = quad();
314 let t = transform(&q, 3.0, 1.1, true);
315 let (o1, _, _) = canonical(&q).unwrap();
316 let (o2, _, _) = canonical(&t).unwrap();
317 assert_eq!(o1, o2);
319 let p: Vec<_> = o1.iter().map(|&i| q[i]).collect();
320 let r: Vec<_> = o2.iter().map(|&i| t[i]).collect();
321 let m = fit_affine(&p, &r).unwrap();
322 assert!(shape_error(&m) < 1e-9);
323 }
324
325 #[test]
326 fn near_ties_offer_both_orders() {
327 let sq = [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)];
329 let (o, t, _) = canonical(&sq).unwrap();
330 assert!(orderings(o, t).len() >= 4);
331 let (o, t, _) = canonical(&quad()).unwrap();
332 assert!(!orderings(o, t).is_empty());
333 }
334
335 #[test]
336 fn probing_covers_a_value_just_across_an_edge() {
337 let mut d = [25.5 / BINS, 51.5 / BINS, 0.0, 76.5 / BINS, 115.5 / BINS];
339 d[2] = 64.0 / BINS + 0.001; let mut keys = Vec::new();
341 probe_keys(&d, &mut keys);
342 let mut below = d;
343 below[2] = 64.0 / BINS - 0.0005;
344 assert!(keys.contains(&key(&below)));
345 assert_eq!(keys.len(), 2);
346 }
347
348 #[test]
349 fn tangent_round_trip() {
350 let t = Tangent::at_radec(1.0, 0.5).unwrap();
351 let (ra, dec) = (1.01, 0.49);
352 let (x, y) = t.project(&unit(ra, dec)).unwrap();
353 let (r2, d2) = t.deproject(x, y);
354 assert!((r2 - ra).abs() < 1e-12 && (d2 - dec).abs() < 1e-12);
355 let (x, _) = t.project(&unit(1.001, 0.5)).unwrap();
357 assert!(x > 0.0);
358 let (_, y) = t.project(&unit(1.0, 0.501)).unwrap();
359 assert!(y > 0.0);
360 }
361
362 #[test]
363 fn affine_inverse_round_trips() {
364 let m = [2.0, 0.5, 3.0, -0.4, 1.5, -7.0];
365 let i = invert_affine(&m).unwrap();
366 let (x, y) = (3.3, -1.2);
367 let (u, v) = (m[0] * x + m[1] * y + m[2], m[3] * x + m[4] * y + m[5]);
368 let (x2, y2) = (i[0] * u + i[1] * v + i[2], i[3] * u + i[4] * v + i[5]);
369 assert!((x - x2).abs() < 1e-12 && (y - y2).abs() < 1e-12);
370 }
371}