1use g_math::fixed_point::{FixedPoint, FixedVector};
27use crate::constants;
28use crate::hyperbolic_geometry::HyperbolicPoint;
29
30#[derive(Clone, Debug)]
39pub struct KleinPoint {
40 pub coords: FixedVector,
42 pub weight: FixedPoint,
44}
45
46impl KleinPoint {
47 pub fn new(coords: FixedVector) -> Self {
49 let weight = FixedPoint::from_int(1) - coords.length_squared();
50 Self { coords, weight }
51 }
52
53 pub fn dimension(&self) -> usize {
55 self.coords.len()
56 }
57}
58
59pub fn poincare_to_klein(p: &HyperbolicPoint) -> KleinPoint {
68 let dim = p.dimension();
69 let norm_sq = p.coords().length_squared();
70 let one = FixedPoint::from_int(1);
71 let two = FixedPoint::from_int(2);
72
73 let denom = one + norm_sq; let scale = two / denom; let mut klein_coords = FixedVector::new(dim);
77 for i in 0..dim {
78 klein_coords[i] = p.coords()[i] * scale;
79 }
80
81 KleinPoint::new(klein_coords)
82}
83
84pub fn klein_to_poincare(k: &KleinPoint) -> HyperbolicPoint {
88 let dim = k.dimension();
89 let one = FixedPoint::from_int(1);
90 let norm_sq = k.coords.length_squared();
91
92 if norm_sq < constants::small_epsilon() {
94 return HyperbolicPoint::origin(dim);
95 }
96
97 let sqrt_term = (one - norm_sq).sqrt(); let denom = one + sqrt_term;
99 let inv_denom = one / denom;
100
101 let mut poincare_coords = FixedVector::new(dim);
102 for i in 0..dim {
103 poincare_coords[i] = k.coords[i] * inv_denom;
104 }
105
106 HyperbolicPoint::new(poincare_coords)
107}
108
109pub fn weighted_barycenter(sites: &[(KleinPoint, FixedPoint)]) -> Option<KleinPoint> {
129 let zero = FixedPoint::from_int(0);
130 let one = FixedPoint::from_int(1);
131
132 let mut dim = 0;
133 let mut denom = zero;
134 let mut numer: Option<FixedVector> = None;
135
136 for (site, w) in sites {
137 if *w <= zero {
138 continue;
139 }
140 let radicand = if site.weight > constants::small_epsilon() {
144 site.weight
145 } else {
146 constants::small_epsilon()
147 };
148 let gamma = one / radicand.sqrt();
149 let coeff = *w * gamma;
150
151 if numer.is_none() {
152 dim = site.dimension();
153 numer = Some(FixedVector::new(dim));
154 }
155 let acc = numer.as_mut().unwrap();
156 for i in 0..dim {
157 acc[i] += site.coords[i] * coeff;
158 }
159 denom += coeff;
160 }
161
162 let numer = numer?;
163 if denom <= zero {
164 return None;
165 }
166 let inv = one / denom;
167 let mut coords = FixedVector::new(dim);
168 for i in 0..dim {
169 coords[i] = numer[i] * inv;
170 }
171 Some(KleinPoint::new(coords))
172}
173
174pub fn power_distance(query: &FixedVector, site: &KleinPoint) -> FixedPoint {
184 let dim = query.len();
185 assert_eq!(dim, site.dimension(), "Dimension mismatch");
186
187 let mut dist_sq = FixedPoint::from_int(0);
193 for i in 0..dim {
194 let d = query[i] - site.coords[i];
195 dist_sq = dist_sq + d * d;
196 }
197
198 dist_sq - site.weight
199}
200
201pub fn nearest_by_power_distance(query: &FixedVector, sites: &[KleinPoint]) -> Option<(usize, FixedPoint)> {
205 if sites.is_empty() {
206 return None;
207 }
208
209 let mut best_idx = 0;
210 let mut best_pd = power_distance(query, &sites[0]);
211
212 for (i, site) in sites.iter().enumerate().skip(1) {
213 let pd = power_distance(query, site);
214 if pd < best_pd {
215 best_pd = pd;
216 best_idx = i;
217 }
218 }
219
220 Some((best_idx, best_pd))
221}
222
223#[cfg(test)]
228mod tests {
229 use super::*;
230 use crate::constants;
231
232 fn fp(v: i32) -> FixedPoint {
233 FixedPoint::from_int(v)
234 }
235
236 fn fp_approx_eq(a: FixedPoint, b: FixedPoint, tol: FixedPoint) -> bool {
237 (a - b).abs() < tol
238 }
239
240 fn klein_at(x: f32, y: f32) -> KleinPoint {
243 poincare_to_klein(&HyperbolicPoint::from_f32_slice(&[x, y]))
244 }
245
246 #[test]
247 fn barycenter_single_site_is_identity() {
248 let site = klein_at(0.4, -0.2);
249 let m = weighted_barycenter(&[(site.clone(), fp(3))]).unwrap();
250 assert!(fp_approx_eq(m.coords[0], site.coords[0], constants::epsilon()));
251 assert!(fp_approx_eq(m.coords[1], site.coords[1], constants::epsilon()));
252 }
253
254 #[test]
255 fn barycenter_equal_weights_matches_verified_midpoint() {
256 let pa = HyperbolicPoint::from_f32_slice(&[0.5, 0.1]);
259 let pb = HyperbolicPoint::from_f32_slice(&[-0.2, 0.4]);
260 let expected = pa.hyperbolic_midpoint(&pb);
261
262 let m = weighted_barycenter(&[
263 (poincare_to_klein(&pa), fp(1)),
264 (poincare_to_klein(&pb), fp(1)),
265 ])
266 .unwrap();
267 let got = klein_to_poincare(&m);
268
269 let tol = FixedPoint::from_int(1) / FixedPoint::from_int(1000);
270 assert!(
271 fp_approx_eq(got.coords()[0], expected.coords()[0], tol)
272 && fp_approx_eq(got.coords()[1], expected.coords()[1], tol),
273 "einstein midpoint {:?} != gyro midpoint {:?}",
274 got, expected
275 );
276 }
277
278 #[test]
279 fn barycenter_is_weight_scale_invariant() {
280 let sites = [klein_at(0.3, 0.3), klein_at(-0.4, 0.1), klein_at(0.0, -0.5)];
281 let a = weighted_barycenter(&[
282 (sites[0].clone(), fp(1)),
283 (sites[1].clone(), fp(2)),
284 (sites[2].clone(), fp(3)),
285 ])
286 .unwrap();
287 let b = weighted_barycenter(&[
288 (sites[0].clone(), fp(7)),
289 (sites[1].clone(), fp(14)),
290 (sites[2].clone(), fp(21)),
291 ])
292 .unwrap();
293 let tol = FixedPoint::from_int(1) / FixedPoint::from_int(100000);
294 assert!(fp_approx_eq(a.coords[0], b.coords[0], tol));
295 assert!(fp_approx_eq(a.coords[1], b.coords[1], tol));
296 }
297
298 #[test]
299 fn barycenter_stays_inside_disk_and_handles_zero_weights() {
300 let m = weighted_barycenter(&[
302 (klein_at(0.9, 0.0), fp(100)),
303 (klein_at(-0.9, 0.0), fp(1)),
304 ])
305 .unwrap();
306 assert!(m.coords.length_squared() < FixedPoint::from_int(1));
307
308 assert!(weighted_barycenter(&[(klein_at(0.5, 0.0), fp(0))]).is_none());
310 assert!(weighted_barycenter(&[]).is_none());
311 let only_positive = weighted_barycenter(&[
312 (klein_at(0.5, 0.0), fp(0)),
313 (klein_at(0.2, 0.2), fp(1)),
314 (klein_at(0.7, 0.0), fp(-2)),
315 ])
316 .unwrap();
317 let expected = klein_at(0.2, 0.2);
318 assert!(fp_approx_eq(only_positive.coords[0], expected.coords[0], constants::epsilon()));
319 assert!(fp_approx_eq(only_positive.coords[1], expected.coords[1], constants::epsilon()));
320 }
321
322 #[test]
325 fn test_klein_origin_maps_to_origin() {
326 let origin = HyperbolicPoint::origin(2);
327 let k = poincare_to_klein(&origin);
328
329 assert!(k.coords[0].abs() < constants::epsilon());
330 assert!(k.coords[1].abs() < constants::epsilon());
331 assert!(fp_approx_eq(k.weight, fp(1), constants::epsilon()));
333 }
334
335 #[test]
336 fn test_klein_roundtrip() {
337 let p = HyperbolicPoint::from_f32_slice(&[0.5, 0.0]);
339 let k = poincare_to_klein(&p);
340 let p2 = klein_to_poincare(&k);
341
342 let tol = constants::epsilon();
343 assert!(fp_approx_eq(p.coords()[0], p2.coords()[0], tol),
344 "x roundtrip: {} vs {}", p.coords()[0], p2.coords()[0]);
345 assert!(fp_approx_eq(p.coords()[1], p2.coords()[1], tol),
346 "y roundtrip: {} vs {}", p.coords()[1], p2.coords()[1]);
347 }
348
349 #[test]
350 fn test_klein_roundtrip_multiple() {
351 let test_points: Vec<[f32; 2]> = vec![
353 [0.3, 0.2],
354 [-0.4, 0.1],
355 [0.0, 0.7],
356 [0.1, -0.5],
357 [0.8, 0.0],
358 ];
359
360 let tol = constants::epsilon();
361 for coords in &test_points {
362 let p = HyperbolicPoint::from_f32_slice(coords);
363 let k = poincare_to_klein(&p);
364 let p2 = klein_to_poincare(&k);
365
366 assert!(fp_approx_eq(p.coords()[0], p2.coords()[0], tol),
367 "Roundtrip failed for ({}, {})", coords[0], coords[1]);
368 assert!(fp_approx_eq(p.coords()[1], p2.coords()[1], tol),
369 "Roundtrip failed for ({}, {})", coords[0], coords[1]);
370 }
371 }
372
373 #[test]
374 fn test_klein_known_example() {
375 let p = HyperbolicPoint::from_f32_slice(&[0.5, 0.0]);
379 let k = poincare_to_klein(&p);
380
381 let tol = FixedPoint::from_int(1) / FixedPoint::from_int(100);
382 let expected_x = FixedPoint::from_int(4) / FixedPoint::from_int(5); let expected_w = FixedPoint::from_int(36) / FixedPoint::from_int(100); assert!(fp_approx_eq(k.coords[0], expected_x, tol),
386 "Klein x: expected 0.8, got {}", k.coords[0]);
387 assert!(k.coords[1].abs() < tol,
388 "Klein y: expected 0, got {}", k.coords[1]);
389 assert!(fp_approx_eq(k.weight, expected_w, tol),
390 "Klein weight: expected 0.36, got {}", k.weight);
391 }
392
393 #[test]
394 fn test_klein_boundary_behavior() {
395 let near_boundary = HyperbolicPoint::from_f32_slice(&[0.95, 0.0]);
397 let k = poincare_to_klein(&near_boundary);
398
399 let k_norm = k.coords.length();
401 assert!(k_norm > FixedPoint::from_int(9) / FixedPoint::from_int(10),
402 "Klein norm should be near 1 for boundary point, got {}", k_norm);
403 assert!(k_norm < FixedPoint::from_int(1),
404 "Klein norm should be < 1, got {}", k_norm);
405 }
406
407 #[test]
408 fn test_power_distance_at_site_center() {
409 let p = HyperbolicPoint::from_f32_slice(&[0.5, 0.0]);
411 let k = poincare_to_klein(&p);
412
413 let pd = power_distance(&k.coords, &k);
414 let expected = -k.weight; let tol = constants::epsilon();
417 assert!(fp_approx_eq(pd, expected, tol),
418 "Power distance at site center should be -weight: {} vs {}", pd, expected);
419 assert!(pd < FixedPoint::from_int(0),
420 "Power distance at own site should be negative");
421 }
422
423 #[test]
424 fn test_power_distance_ordering_matches_hyperbolic() {
425 let query_p = HyperbolicPoint::from_f32_slice(&[0.1, 0.1]);
428 let site1_p = HyperbolicPoint::from_f32_slice(&[0.2, 0.0]);
429 let site2_p = HyperbolicPoint::from_f32_slice(&[0.6, 0.3]);
430
431 let query_k = poincare_to_klein(&query_p);
432 let site1_k = poincare_to_klein(&site1_p);
433 let site2_k = poincare_to_klein(&site2_p);
434
435 let pd1 = power_distance(&query_k.coords, &site1_k);
436 let pd2 = power_distance(&query_k.coords, &site2_k);
437
438 let hd1 = query_p.hyperbolic_distance(&site1_p);
439 let hd2 = query_p.hyperbolic_distance(&site2_p);
440
441 if hd1 < hd2 {
443 assert!(pd1 < pd2,
444 "Power distance ordering should match hyperbolic: pd1={} pd2={}, hd1={} hd2={}",
445 pd1, pd2, hd1, hd2);
446 } else {
447 assert!(pd2 <= pd1,
448 "Power distance ordering should match hyperbolic: pd1={} pd2={}, hd1={} hd2={}",
449 pd1, pd2, hd1, hd2);
450 }
451 }
452
453 #[test]
454 fn test_nearest_by_power_distance() {
455 let sites = vec![
456 KleinPoint::new(FixedVector::from_f32_slice(&[0.2, 0.0])),
457 KleinPoint::new(FixedVector::from_f32_slice(&[0.8, 0.0])),
458 KleinPoint::new(FixedVector::from_f32_slice(&[0.0, 0.5])),
459 ];
460
461 let query = FixedVector::from_f32_slice(&[0.1, 0.0]);
462
463 let (idx, _pd) = nearest_by_power_distance(&query, &sites).unwrap();
464
465 assert_eq!(idx, 0, "Nearest should be site 0");
467 }
468
469 #[test]
470 fn test_klein_roundtrip_4d() {
471 let p = HyperbolicPoint::from_f32_slice(&[0.3, 0.2, -0.1, 0.15]);
473 let k = poincare_to_klein(&p);
474 let p2 = klein_to_poincare(&k);
475
476 let tol = constants::epsilon();
477 for i in 0..4 {
478 assert!(fp_approx_eq(p.coords()[i], p2.coords()[i], tol),
479 "4D roundtrip failed at dim {}: {} vs {}", i, p.coords()[i], p2.coords()[i]);
480 }
481 }
482
483 #[test]
486 fn test_power_distance_ordering_equidistant_sites() {
487 let tau = constants::default_tau();
491 let half_tau = tau * constants::half();
492 let r = half_tau.tanh(); let angles: Vec<FixedPoint> = vec![
496 FixedPoint::from_int(0),
497 FixedPoint::from_int(3) / FixedPoint::from_int(2),
498 FixedPoint::from_int(3),
499 FixedPoint::from_int(9) / FixedPoint::from_int(2),
500 ];
501 let sites_p: Vec<HyperbolicPoint> = angles.iter().map(|a| {
502 let mut v = FixedVector::new(2);
503 let (sin_a, cos_a) = a.sincos();
504 v[0] = r * cos_a;
505 v[1] = r * sin_a;
506 HyperbolicPoint::new(v)
507 }).collect();
508
509 let sites_k: Vec<KleinPoint> = sites_p.iter().map(|p| poincare_to_klein(p)).collect();
510
511 for (qi, site) in sites_p.iter().enumerate() {
513 let mut q_coords = site.coords().clone();
515 q_coords[0] = q_coords[0] + constants::epsilon();
516 let q_p = HyperbolicPoint::new(q_coords.clone());
517 let q_k = poincare_to_klein(&q_p);
518
519 let (pd_nn, _) = nearest_by_power_distance(&q_k.coords, &sites_k).unwrap();
520
521 let mut hyp_dists: Vec<(usize, FixedPoint)> = sites_p.iter().enumerate()
522 .map(|(i, s)| (i, q_p.hyperbolic_distance(s)))
523 .collect();
524 hyp_dists.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
525
526 assert_eq!(pd_nn, hyp_dists[0].0,
527 "Power NN should match hyperbolic NN near site {}", qi);
528 }
529 }
530}