1use g_math::fixed_point::{FixedPoint, FixedVector};
24use crate::constants;
25use crate::hyperbolic_geometry::HyperbolicPoint;
26
27#[derive(Clone, Debug)]
36pub struct KleinPoint {
37 pub coords: FixedVector,
39 pub weight: FixedPoint,
41}
42
43impl KleinPoint {
44 pub fn new(coords: FixedVector) -> Self {
46 let weight = FixedPoint::from_int(1) - coords.length_squared();
47 Self { coords, weight }
48 }
49
50 pub fn dimension(&self) -> usize {
52 self.coords.len()
53 }
54}
55
56pub fn poincare_to_klein(p: &HyperbolicPoint) -> KleinPoint {
65 let dim = p.dimension();
66 let norm_sq = p.coords().length_squared();
67 let one = FixedPoint::from_int(1);
68 let two = FixedPoint::from_int(2);
69
70 let denom = one + norm_sq; let scale = two / denom; let mut klein_coords = FixedVector::new(dim);
74 for i in 0..dim {
75 klein_coords[i] = p.coords()[i] * scale;
76 }
77
78 KleinPoint::new(klein_coords)
79}
80
81pub fn klein_to_poincare(k: &KleinPoint) -> HyperbolicPoint {
85 let dim = k.dimension();
86 let one = FixedPoint::from_int(1);
87 let norm_sq = k.coords.length_squared();
88
89 if norm_sq < constants::small_epsilon() {
91 return HyperbolicPoint::origin(dim);
92 }
93
94 let sqrt_term = (one - norm_sq).sqrt(); let denom = one + sqrt_term;
96 let inv_denom = one / denom;
97
98 let mut poincare_coords = FixedVector::new(dim);
99 for i in 0..dim {
100 poincare_coords[i] = k.coords[i] * inv_denom;
101 }
102
103 HyperbolicPoint::new(poincare_coords)
104}
105
106pub fn weighted_barycenter(sites: &[(KleinPoint, FixedPoint)]) -> Option<KleinPoint> {
126 let zero = FixedPoint::from_int(0);
127 let one = FixedPoint::from_int(1);
128
129 let mut dim = 0;
130 let mut denom = zero;
131 let mut numer: Option<FixedVector> = None;
132
133 for (site, w) in sites {
134 if *w <= zero {
135 continue;
136 }
137 let radicand = if site.weight > constants::small_epsilon() {
141 site.weight
142 } else {
143 constants::small_epsilon()
144 };
145 let gamma = one / radicand.sqrt();
146 let coeff = *w * gamma;
147
148 if numer.is_none() {
149 dim = site.dimension();
150 numer = Some(FixedVector::new(dim));
151 }
152 let acc = numer.as_mut().unwrap();
153 for i in 0..dim {
154 acc[i] += site.coords[i] * coeff;
155 }
156 denom += coeff;
157 }
158
159 let numer = numer?;
160 if denom <= zero {
161 return None;
162 }
163 let inv = one / denom;
164 let mut coords = FixedVector::new(dim);
165 for i in 0..dim {
166 coords[i] = numer[i] * inv;
167 }
168 Some(KleinPoint::new(coords))
169}
170
171pub fn power_distance(query: &FixedVector, site: &KleinPoint) -> FixedPoint {
181 let dim = query.len();
182 assert_eq!(dim, site.dimension(), "Dimension mismatch");
183
184 let mut dist_sq = FixedPoint::from_int(0);
190 for i in 0..dim {
191 let d = query[i] - site.coords[i];
192 dist_sq = dist_sq + d * d;
193 }
194
195 dist_sq - site.weight
196}
197
198pub fn nearest_by_power_distance(query: &FixedVector, sites: &[KleinPoint]) -> Option<(usize, FixedPoint)> {
202 if sites.is_empty() {
203 return None;
204 }
205
206 let mut best_idx = 0;
207 let mut best_pd = power_distance(query, &sites[0]);
208
209 for (i, site) in sites.iter().enumerate().skip(1) {
210 let pd = power_distance(query, site);
211 if pd < best_pd {
212 best_pd = pd;
213 best_idx = i;
214 }
215 }
216
217 Some((best_idx, best_pd))
218}
219
220#[derive(Clone, Debug)]
229pub struct HalfPlane {
230 pub normal: FixedVector,
232 pub offset: FixedPoint,
234 pub neighbor_id: String,
236}
237
238#[derive(Clone, Debug)]
243pub struct PowerCell {
244 pub node_id: String,
246 pub site: KleinPoint,
248 pub half_planes: Vec<HalfPlane>,
250}
251
252pub fn compute_bisector(site_i: &KleinPoint, site_j: &KleinPoint, neighbor_id: &str) -> HalfPlane {
260 let dim = site_i.dimension();
261 assert_eq!(dim, site_j.dimension(), "Dimension mismatch");
262
263 let mut normal = FixedVector::new(dim);
265 for i in 0..dim {
266 normal[i] = site_j.coords[i] - site_i.coords[i];
267 }
268
269 let offset = site_j.coords.length_squared() - site_i.coords.length_squared();
271
272 HalfPlane {
273 normal,
274 offset,
275 neighbor_id: neighbor_id.to_string(),
276 }
277}
278
279pub fn point_in_cell(query: &FixedVector, cell: &PowerCell) -> bool {
283 for hp in &cell.half_planes {
284 let dot = query.dot(&hp.normal);
285 if dot > hp.offset {
286 return false;
287 }
288 }
289 true
290}
291
292pub struct PointLocationGrid {
302 pub resolution: usize,
304 dimension: usize,
306 cell_size: FixedPoint,
308 inv_cell_size: FixedPoint,
310 grid: Vec<Option<String>>,
312 tile_owners: std::collections::HashMap<String, Vec<usize>>,
315}
316
317impl PointLocationGrid {
318 pub fn new(resolution: usize) -> Self {
321 Self::with_dimension(resolution, 2)
322 }
323
324 pub fn with_dimension(resolution: usize, dimension: usize) -> Self {
331 let resolution = resolution.max(1);
332 let res_fp = FixedPoint::from_int(resolution as i32);
333 let two = FixedPoint::from_int(2);
334 let cell_size = two / res_fp;
335 let inv_cell_size = res_fp / two;
336
337 Self {
338 resolution,
339 dimension,
340 cell_size,
341 inv_cell_size,
342 grid: vec![None; resolution * resolution],
343 tile_owners: std::collections::HashMap::new(),
344 }
345 }
346
347 pub fn build(&mut self, sites: &[(String, KleinPoint)]) {
352 if sites.is_empty() {
353 return;
354 }
355
356 let one = FixedPoint::from_int(1);
357
358 for row in 0..self.resolution {
359 for col in 0..self.resolution {
360 let center = self.tile_center(row, col);
361
362 if center.length_squared() >= one {
364 self.grid[row * self.resolution + col] = None;
365 continue;
366 }
367
368 let mut best_id: Option<&str> = None;
370 let mut best_pd = FixedPoint::from_int(0);
371 let mut first = true;
372
373 for (id, site) in sites {
374 let pd = power_distance(¢er, site);
375 if first || pd < best_pd {
376 best_pd = pd;
377 best_id = Some(id.as_str());
378 first = false;
379 }
380 }
381
382 self.grid[row * self.resolution + col] = best_id.map(|s| s.to_string());
383 }
384 }
385
386 self.tile_owners.clear();
388 for (idx, cell) in self.grid.iter().enumerate() {
389 if let Some(ref id) = cell {
390 self.tile_owners.entry(id.clone()).or_default().push(idx);
391 }
392 }
393 }
394
395 pub fn query(&self, query_klein: &FixedVector) -> Option<&str> {
399 let (row, col) = self.coords_to_tile(query_klein);
401
402 if row >= self.resolution || col >= self.resolution {
403 return None;
404 }
405
406 self.grid[row * self.resolution + col].as_deref()
407 }
408
409 pub fn update_insert(&mut self, parent_id: &str, new_id: &str, new_site: &KleinPoint, parent_site: &KleinPoint) {
415 let one = FixedPoint::from_int(1);
416
417 let parent_tiles = match self.tile_owners.get(parent_id) {
419 Some(tiles) => tiles.clone(),
420 None => return,
421 };
422
423 let mut tiles_to_reassign = Vec::new();
424
425 for &idx in &parent_tiles {
426 let row = idx / self.resolution;
427 let col = idx % self.resolution;
428 let center = self.tile_center(row, col);
429
430 if center.length_squared() >= one {
431 continue;
432 }
433
434 let pd_parent = power_distance(¢er, parent_site);
435 let pd_new = power_distance(¢er, new_site);
436
437 if pd_new < pd_parent {
438 tiles_to_reassign.push(idx);
439 }
440 }
441
442 for &idx in &tiles_to_reassign {
444 self.grid[idx] = Some(new_id.to_string());
445 }
446
447 if !tiles_to_reassign.is_empty() {
449 if let Some(parent_list) = self.tile_owners.get_mut(parent_id) {
450 parent_list.retain(|idx| !tiles_to_reassign.contains(idx));
451 }
452 self.tile_owners.entry(new_id.to_string())
453 .or_default()
454 .extend(&tiles_to_reassign);
455 }
456 }
457
458 pub fn update_delete(&mut self, deleted_id: &str, parent_id: &str) {
462 let deleted_tiles = match self.tile_owners.remove(deleted_id) {
464 Some(tiles) => tiles,
465 None => return,
466 };
467
468 for &idx in &deleted_tiles {
470 self.grid[idx] = Some(parent_id.to_string());
471 }
472
473 self.tile_owners.entry(parent_id.to_string())
475 .or_default()
476 .extend(deleted_tiles);
477 }
478
479 fn tile_center(&self, row: usize, col: usize) -> FixedVector {
482 let half = constants::half();
483 let one = FixedPoint::from_int(1);
484
485 let col_fp = FixedPoint::from_int(col as i32);
489 let row_fp = FixedPoint::from_int(row as i32);
490
491 let x = -one + (col_fp + half) * self.cell_size;
492 let y = -one + (row_fp + half) * self.cell_size;
493
494 let mut v = FixedVector::new(self.dimension);
495 v[0] = x;
496 if self.dimension >= 2 {
497 v[1] = y;
498 }
499 v
501 }
502
503 fn coords_to_tile(&self, coords: &FixedVector) -> (usize, usize) {
505 let one = FixedPoint::from_int(1);
506
507 let x = coords[0];
509 let y = if coords.len() >= 2 { coords[1] } else { FixedPoint::from_int(0) };
510 let col_fp = (x + one) * self.inv_cell_size;
511 let row_fp = (y + one) * self.inv_cell_size;
512
513 let col = col_fp.to_int().max(0) as usize;
514 let row = row_fp.to_int().max(0) as usize;
515
516 (row.min(self.resolution - 1), col.min(self.resolution - 1))
517 }
518
519 pub fn assigned_tile_count(&self) -> usize {
521 self.grid.iter().filter(|t| t.is_some()).count()
522 }
523
524 pub fn resolution(&self) -> usize {
526 self.resolution
527 }
528}
529
530impl std::fmt::Debug for PointLocationGrid {
531 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
532 write!(f, "PointLocationGrid(resolution={}, assigned={})",
533 self.resolution, self.assigned_tile_count())
534 }
535}
536
537#[cfg(test)]
542mod tests {
543 use super::*;
544 use crate::constants;
545
546 fn fp(v: i32) -> FixedPoint {
547 FixedPoint::from_int(v)
548 }
549
550 fn fp_approx_eq(a: FixedPoint, b: FixedPoint, tol: FixedPoint) -> bool {
551 (a - b).abs() < tol
552 }
553
554 fn klein_at(x: f32, y: f32) -> KleinPoint {
557 poincare_to_klein(&HyperbolicPoint::from_f32_slice(&[x, y]))
558 }
559
560 #[test]
561 fn barycenter_single_site_is_identity() {
562 let site = klein_at(0.4, -0.2);
563 let m = weighted_barycenter(&[(site.clone(), fp(3))]).unwrap();
564 assert!(fp_approx_eq(m.coords[0], site.coords[0], constants::epsilon()));
565 assert!(fp_approx_eq(m.coords[1], site.coords[1], constants::epsilon()));
566 }
567
568 #[test]
569 fn barycenter_equal_weights_matches_verified_midpoint() {
570 let pa = HyperbolicPoint::from_f32_slice(&[0.5, 0.1]);
573 let pb = HyperbolicPoint::from_f32_slice(&[-0.2, 0.4]);
574 let expected = pa.hyperbolic_midpoint(&pb);
575
576 let m = weighted_barycenter(&[
577 (poincare_to_klein(&pa), fp(1)),
578 (poincare_to_klein(&pb), fp(1)),
579 ])
580 .unwrap();
581 let got = klein_to_poincare(&m);
582
583 let tol = FixedPoint::from_int(1) / FixedPoint::from_int(1000);
584 assert!(
585 fp_approx_eq(got.coords()[0], expected.coords()[0], tol)
586 && fp_approx_eq(got.coords()[1], expected.coords()[1], tol),
587 "einstein midpoint {:?} != gyro midpoint {:?}",
588 got, expected
589 );
590 }
591
592 #[test]
593 fn barycenter_is_weight_scale_invariant() {
594 let sites = [klein_at(0.3, 0.3), klein_at(-0.4, 0.1), klein_at(0.0, -0.5)];
595 let a = weighted_barycenter(&[
596 (sites[0].clone(), fp(1)),
597 (sites[1].clone(), fp(2)),
598 (sites[2].clone(), fp(3)),
599 ])
600 .unwrap();
601 let b = weighted_barycenter(&[
602 (sites[0].clone(), fp(7)),
603 (sites[1].clone(), fp(14)),
604 (sites[2].clone(), fp(21)),
605 ])
606 .unwrap();
607 let tol = FixedPoint::from_int(1) / FixedPoint::from_int(100000);
608 assert!(fp_approx_eq(a.coords[0], b.coords[0], tol));
609 assert!(fp_approx_eq(a.coords[1], b.coords[1], tol));
610 }
611
612 #[test]
613 fn barycenter_stays_inside_disk_and_handles_zero_weights() {
614 let m = weighted_barycenter(&[
616 (klein_at(0.9, 0.0), fp(100)),
617 (klein_at(-0.9, 0.0), fp(1)),
618 ])
619 .unwrap();
620 assert!(m.coords.length_squared() < FixedPoint::from_int(1));
621
622 assert!(weighted_barycenter(&[(klein_at(0.5, 0.0), fp(0))]).is_none());
624 assert!(weighted_barycenter(&[]).is_none());
625 let only_positive = weighted_barycenter(&[
626 (klein_at(0.5, 0.0), fp(0)),
627 (klein_at(0.2, 0.2), fp(1)),
628 (klein_at(0.7, 0.0), fp(-2)),
629 ])
630 .unwrap();
631 let expected = klein_at(0.2, 0.2);
632 assert!(fp_approx_eq(only_positive.coords[0], expected.coords[0], constants::epsilon()));
633 assert!(fp_approx_eq(only_positive.coords[1], expected.coords[1], constants::epsilon()));
634 }
635
636 #[test]
639 fn test_klein_origin_maps_to_origin() {
640 let origin = HyperbolicPoint::origin(2);
641 let k = poincare_to_klein(&origin);
642
643 assert!(k.coords[0].abs() < constants::epsilon());
644 assert!(k.coords[1].abs() < constants::epsilon());
645 assert!(fp_approx_eq(k.weight, fp(1), constants::epsilon()));
647 }
648
649 #[test]
650 fn test_klein_roundtrip() {
651 let p = HyperbolicPoint::from_f32_slice(&[0.5, 0.0]);
653 let k = poincare_to_klein(&p);
654 let p2 = klein_to_poincare(&k);
655
656 let tol = constants::epsilon();
657 assert!(fp_approx_eq(p.coords()[0], p2.coords()[0], tol),
658 "x roundtrip: {} vs {}", p.coords()[0], p2.coords()[0]);
659 assert!(fp_approx_eq(p.coords()[1], p2.coords()[1], tol),
660 "y roundtrip: {} vs {}", p.coords()[1], p2.coords()[1]);
661 }
662
663 #[test]
664 fn test_klein_roundtrip_multiple() {
665 let test_points: Vec<[f32; 2]> = vec![
667 [0.3, 0.2],
668 [-0.4, 0.1],
669 [0.0, 0.7],
670 [0.1, -0.5],
671 [0.8, 0.0],
672 ];
673
674 let tol = constants::epsilon();
675 for coords in &test_points {
676 let p = HyperbolicPoint::from_f32_slice(coords);
677 let k = poincare_to_klein(&p);
678 let p2 = klein_to_poincare(&k);
679
680 assert!(fp_approx_eq(p.coords()[0], p2.coords()[0], tol),
681 "Roundtrip failed for ({}, {})", coords[0], coords[1]);
682 assert!(fp_approx_eq(p.coords()[1], p2.coords()[1], tol),
683 "Roundtrip failed for ({}, {})", coords[0], coords[1]);
684 }
685 }
686
687 #[test]
688 fn test_klein_known_example() {
689 let p = HyperbolicPoint::from_f32_slice(&[0.5, 0.0]);
693 let k = poincare_to_klein(&p);
694
695 let tol = FixedPoint::from_int(1) / FixedPoint::from_int(100);
696 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),
700 "Klein x: expected 0.8, got {}", k.coords[0]);
701 assert!(k.coords[1].abs() < tol,
702 "Klein y: expected 0, got {}", k.coords[1]);
703 assert!(fp_approx_eq(k.weight, expected_w, tol),
704 "Klein weight: expected 0.36, got {}", k.weight);
705 }
706
707 #[test]
708 fn test_klein_boundary_behavior() {
709 let near_boundary = HyperbolicPoint::from_f32_slice(&[0.95, 0.0]);
711 let k = poincare_to_klein(&near_boundary);
712
713 let k_norm = k.coords.length();
715 assert!(k_norm > FixedPoint::from_int(9) / FixedPoint::from_int(10),
716 "Klein norm should be near 1 for boundary point, got {}", k_norm);
717 assert!(k_norm < FixedPoint::from_int(1),
718 "Klein norm should be < 1, got {}", k_norm);
719 }
720
721 #[test]
722 fn test_power_distance_at_site_center() {
723 let p = HyperbolicPoint::from_f32_slice(&[0.5, 0.0]);
725 let k = poincare_to_klein(&p);
726
727 let pd = power_distance(&k.coords, &k);
728 let expected = -k.weight; let tol = constants::epsilon();
731 assert!(fp_approx_eq(pd, expected, tol),
732 "Power distance at site center should be -weight: {} vs {}", pd, expected);
733 assert!(pd < FixedPoint::from_int(0),
734 "Power distance at own site should be negative");
735 }
736
737 #[test]
738 fn test_power_distance_ordering_matches_hyperbolic() {
739 let query_p = HyperbolicPoint::from_f32_slice(&[0.1, 0.1]);
742 let site1_p = HyperbolicPoint::from_f32_slice(&[0.2, 0.0]);
743 let site2_p = HyperbolicPoint::from_f32_slice(&[0.6, 0.3]);
744
745 let query_k = poincare_to_klein(&query_p);
746 let site1_k = poincare_to_klein(&site1_p);
747 let site2_k = poincare_to_klein(&site2_p);
748
749 let pd1 = power_distance(&query_k.coords, &site1_k);
750 let pd2 = power_distance(&query_k.coords, &site2_k);
751
752 let hd1 = query_p.hyperbolic_distance(&site1_p);
753 let hd2 = query_p.hyperbolic_distance(&site2_p);
754
755 if hd1 < hd2 {
757 assert!(pd1 < pd2,
758 "Power distance ordering should match hyperbolic: pd1={} pd2={}, hd1={} hd2={}",
759 pd1, pd2, hd1, hd2);
760 } else {
761 assert!(pd2 <= pd1,
762 "Power distance ordering should match hyperbolic: pd1={} pd2={}, hd1={} hd2={}",
763 pd1, pd2, hd1, hd2);
764 }
765 }
766
767 #[test]
768 fn test_nearest_by_power_distance() {
769 let sites = vec![
770 KleinPoint::new(FixedVector::from_f32_slice(&[0.2, 0.0])),
771 KleinPoint::new(FixedVector::from_f32_slice(&[0.8, 0.0])),
772 KleinPoint::new(FixedVector::from_f32_slice(&[0.0, 0.5])),
773 ];
774
775 let query = FixedVector::from_f32_slice(&[0.1, 0.0]);
776
777 let (idx, _pd) = nearest_by_power_distance(&query, &sites).unwrap();
778
779 assert_eq!(idx, 0, "Nearest should be site 0");
781 }
782
783 #[test]
786 fn test_compute_bisector_symmetry() {
787 let site_i = KleinPoint::new(FixedVector::from_f32_slice(&[0.2, 0.0]));
788 let site_j = KleinPoint::new(FixedVector::from_f32_slice(&[0.6, 0.0]));
789
790 let hp_ij = compute_bisector(&site_i, &site_j, "j");
791 let hp_ji = compute_bisector(&site_j, &site_i, "i");
792
793 let tol = constants::epsilon();
795 assert!(fp_approx_eq(hp_ij.normal[0], -hp_ji.normal[0], tol));
796 assert!(fp_approx_eq(hp_ij.offset, -hp_ji.offset, tol));
797 }
798
799 #[test]
800 fn test_point_in_cell_at_site_center() {
801 let site_i = KleinPoint::new(FixedVector::from_f32_slice(&[0.2, 0.0]));
803 let site_j = KleinPoint::new(FixedVector::from_f32_slice(&[0.6, 0.0]));
804
805 let hp = compute_bisector(&site_i, &site_j, "j");
806 let cell = PowerCell {
807 node_id: "i".to_string(),
808 site: site_i.clone(),
809 half_planes: vec![hp],
810 };
811
812 assert!(point_in_cell(&site_i.coords, &cell),
813 "Site center should be inside its own cell");
814 }
815
816 #[test]
817 fn test_bisector_midpoint_on_boundary() {
818 let site_i = KleinPoint::new(FixedVector::from_f32_slice(&[0.2, 0.0]));
820 let site_j = KleinPoint::new(FixedVector::from_f32_slice(&[0.6, 0.0]));
821
822 let hp = compute_bisector(&site_i, &site_j, "j");
823
824 let mut midpoint = FixedVector::new(2);
826 midpoint[0] = (site_i.coords[0] + site_j.coords[0]) * constants::half();
827 midpoint[1] = (site_i.coords[1] + site_j.coords[1]) * constants::half();
828
829 let _pd_i = power_distance(&midpoint, &site_i);
833 let _pd_j = power_distance(&midpoint, &site_j);
834
835 let bisector_x = hp.offset / hp.normal[0];
843 let mut bisector_pt = FixedVector::new(2);
844 bisector_pt[0] = bisector_x;
845
846 let pd_i_bpt = power_distance(&bisector_pt, &site_i);
847 let pd_j_bpt = power_distance(&bisector_pt, &site_j);
848
849 let tol = constants::epsilon();
850 assert!(fp_approx_eq(pd_i_bpt, pd_j_bpt, tol),
851 "Bisector point should have equal power distances: {} vs {}", pd_i_bpt, pd_j_bpt);
852 }
853
854 #[test]
855 fn test_cell_membership_consistency() {
856 let site_a = KleinPoint::new(FixedVector::from_f32_slice(&[0.3, 0.0]));
858 let site_b = KleinPoint::new(FixedVector::from_f32_slice(&[-0.3, 0.0]));
859
860 let hp_ab = compute_bisector(&site_a, &site_b, "b");
861 let hp_ba = compute_bisector(&site_b, &site_a, "a");
862
863 let cell_a = PowerCell {
864 node_id: "a".to_string(),
865 site: site_a.clone(),
866 half_planes: vec![hp_ab],
867 };
868 let cell_b = PowerCell {
869 node_id: "b".to_string(),
870 site: site_b.clone(),
871 half_planes: vec![hp_ba],
872 };
873
874 let test_xs: Vec<f32> = vec![-0.8, -0.5, -0.2, 0.0, 0.2, 0.5, 0.8];
876 for &x in &test_xs {
877 let q = FixedVector::from_f32_slice(&[x, 0.0]);
878 let in_a = point_in_cell(&q, &cell_a);
879 let in_b = point_in_cell(&q, &cell_b);
880
881 assert!(in_a || in_b,
883 "Point ({}, 0) should be in at least one cell", x);
884 }
885 }
886
887 #[test]
890 fn test_grid_single_site() {
891 let sites = vec![
893 ("root".to_string(), KleinPoint::new(FixedVector::from_f32_slice(&[0.0, 0.0]))),
894 ];
895
896 let mut grid = PointLocationGrid::new(16);
897 grid.build(&sites);
898
899 let test_points: Vec<[f32; 2]> = vec![[0.0, 0.0], [0.5, 0.0], [0.0, -0.5], [0.3, 0.3]];
901 for coords in &test_points {
902 let q = FixedVector::from_f32_slice(coords);
903 let result = grid.query(&q);
904 assert_eq!(result, Some("root"), "Single site should own all tiles");
905 }
906 }
907
908 #[test]
909 fn test_grid_query_matches_brute_force() {
910 let sites = vec![
912 ("a".to_string(), KleinPoint::new(FixedVector::from_f32_slice(&[0.3, 0.0]))),
913 ("b".to_string(), KleinPoint::new(FixedVector::from_f32_slice(&[-0.3, 0.0]))),
914 ("c".to_string(), KleinPoint::new(FixedVector::from_f32_slice(&[0.0, 0.4]))),
915 ];
916
917 let mut grid = PointLocationGrid::new(32);
918 grid.build(&sites);
919
920 let test_points: Vec<[f32; 2]> = vec![
922 [0.2, 0.0], [-0.2, 0.0], [0.0, 0.3],
923 [0.5, 0.1], [-0.4, -0.2], [0.1, 0.5],
924 ];
925
926 let klein_sites: Vec<KleinPoint> = sites.iter().map(|(_, s)| s.clone()).collect();
927 let ids: Vec<&str> = sites.iter().map(|(id, _)| id.as_str()).collect();
928
929 for coords in &test_points {
930 let q = FixedVector::from_f32_slice(coords);
931 if q.length_squared() >= FixedPoint::from_int(1) {
932 continue;
933 }
934
935 let grid_result = grid.query(&q);
936 let (brute_idx, _) = nearest_by_power_distance(&q, &klein_sites).unwrap();
937 let brute_result = ids[brute_idx];
938
939 assert_eq!(grid_result, Some(brute_result),
940 "Grid mismatch at ({}, {}): grid={:?} brute={}",
941 coords[0], coords[1], grid_result, brute_result);
942 }
943 }
944
945 #[test]
946 fn test_grid_insert_update() {
947 let parent_site = KleinPoint::new(FixedVector::from_f32_slice(&[0.0, 0.0]));
949 let sites = vec![
950 ("parent".to_string(), parent_site.clone()),
951 ];
952
953 let mut grid = PointLocationGrid::new(16);
954 grid.build(&sites);
955
956 let q = FixedVector::from_f32_slice(&[0.5, 0.0]);
958 assert_eq!(grid.query(&q), Some("parent"));
959
960 let child_site = KleinPoint::new(FixedVector::from_f32_slice(&[0.5, 0.0]));
962 grid.update_insert("parent", "child", &child_site, &parent_site);
963
964 let q_near_child = FixedVector::from_f32_slice(&[0.6, 0.0]);
966 let result = grid.query(&q_near_child);
967 assert_eq!(result, Some("child"),
968 "After insert, tile near child should be owned by child");
969
970 let q_origin = FixedVector::from_f32_slice(&[0.0, 0.0]);
972 assert_eq!(grid.query(&q_origin), Some("parent"),
973 "Origin tile should still be parent");
974 }
975
976 #[test]
977 fn test_grid_delete_update() {
978 let parent_site = KleinPoint::new(FixedVector::from_f32_slice(&[0.0, 0.0]));
979 let child_site = KleinPoint::new(FixedVector::from_f32_slice(&[0.5, 0.0]));
980
981 let sites = vec![
982 ("parent".to_string(), parent_site.clone()),
983 ("child".to_string(), child_site.clone()),
984 ];
985
986 let mut grid = PointLocationGrid::new(16);
987 grid.build(&sites);
988
989 let q = FixedVector::from_f32_slice(&[0.6, 0.0]);
991 assert_eq!(grid.query(&q), Some("child"));
992
993 grid.update_delete("child", "parent");
995
996 assert_eq!(grid.query(&q), Some("parent"),
997 "After delete, child's tiles should revert to parent");
998 }
999
1000 #[test]
1001 fn test_grid_tile_count() {
1002 let resolution = 64;
1004 let sites = vec![
1005 ("root".to_string(), KleinPoint::new(FixedVector::from_f32_slice(&[0.0, 0.0]))),
1006 ];
1007
1008 let mut grid = PointLocationGrid::new(resolution);
1009 grid.build(&sites);
1010
1011 let assigned = grid.assigned_tile_count();
1012 let expected_approx = (std::f64::consts::PI / 4.0 * (resolution as f64).powi(2)) as usize;
1013
1014 let lower = expected_approx * 9 / 10;
1016 let upper = expected_approx * 11 / 10;
1017 assert!(assigned >= lower && assigned <= upper,
1018 "Assigned tiles {} should be near π/4·{}² ≈ {} (range [{}, {}])",
1019 assigned, resolution, expected_approx, lower, upper);
1020 }
1021
1022 #[test]
1023 fn test_klein_roundtrip_4d() {
1024 let p = HyperbolicPoint::from_f32_slice(&[0.3, 0.2, -0.1, 0.15]);
1026 let k = poincare_to_klein(&p);
1027 let p2 = klein_to_poincare(&k);
1028
1029 let tol = constants::epsilon();
1030 for i in 0..4 {
1031 assert!(fp_approx_eq(p.coords()[i], p2.coords()[i], tol),
1032 "4D roundtrip failed at dim {}: {} vs {}", i, p.coords()[i], p2.coords()[i]);
1033 }
1034 }
1035
1036 #[test]
1039 fn test_power_distance_ordering_equidistant_sites() {
1040 let tau = constants::default_tau();
1044 let half_tau = tau * constants::half();
1045 let r = half_tau.tanh(); let angles: Vec<FixedPoint> = vec![
1049 FixedPoint::from_int(0),
1050 FixedPoint::from_int(3) / FixedPoint::from_int(2),
1051 FixedPoint::from_int(3),
1052 FixedPoint::from_int(9) / FixedPoint::from_int(2),
1053 ];
1054 let sites_p: Vec<HyperbolicPoint> = angles.iter().map(|a| {
1055 let mut v = FixedVector::new(2);
1056 let (sin_a, cos_a) = a.sincos();
1057 v[0] = r * cos_a;
1058 v[1] = r * sin_a;
1059 HyperbolicPoint::new(v)
1060 }).collect();
1061
1062 let sites_k: Vec<KleinPoint> = sites_p.iter().map(|p| poincare_to_klein(p)).collect();
1063
1064 for (qi, site) in sites_p.iter().enumerate() {
1066 let mut q_coords = site.coords().clone();
1068 q_coords[0] = q_coords[0] + constants::epsilon();
1069 let q_p = HyperbolicPoint::new(q_coords.clone());
1070 let q_k = poincare_to_klein(&q_p);
1071
1072 let (pd_nn, _) = nearest_by_power_distance(&q_k.coords, &sites_k).unwrap();
1073
1074 let mut hyp_dists: Vec<(usize, FixedPoint)> = sites_p.iter().enumerate()
1075 .map(|(i, s)| (i, q_p.hyperbolic_distance(s)))
1076 .collect();
1077 hyp_dists.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
1078
1079 assert_eq!(pd_nn, hyp_dists[0].0,
1080 "Power NN should match hyperbolic NN near site {}", qi);
1081 }
1082 }
1083
1084 #[test]
1085 fn test_grid_vs_brute_force_stress() {
1086 let site_coords: Vec<[f32; 2]> = vec![
1089 [0.0, 0.0], [0.3, 0.0], [-0.3, 0.0], [0.0, 0.3], [0.0, -0.3],
1090 [0.2, 0.2], [-0.2, 0.2], [0.2, -0.2], [-0.2, -0.2],
1091 [0.5, 0.1], [-0.4, 0.3], [0.1, 0.6], [-0.1, -0.5],
1092 ];
1093
1094 let sites: Vec<(String, KleinPoint)> = site_coords.iter().enumerate()
1095 .map(|(i, c)| {
1096 let p = HyperbolicPoint::from_f32_slice(c);
1097 let k = poincare_to_klein(&p);
1098 (format!("node_{}", i), k)
1099 })
1100 .collect();
1101
1102 let mut grid = PointLocationGrid::new(64);
1103 grid.build(&sites);
1104
1105 let klein_only: Vec<KleinPoint> = sites.iter().map(|(_, k)| k.clone()).collect();
1106 let ids: Vec<&str> = sites.iter().map(|(id, _)| id.as_str()).collect();
1107
1108 let mut mismatches = 0;
1110 let mut total = 0;
1111 for xi in -9..=9 {
1112 for yi in -9..=9 {
1113 let x = xi as f32 / 10.0;
1114 let y = yi as f32 / 10.0;
1115 if x * x + y * y >= 0.99 {
1116 continue;
1117 }
1118
1119 let q = FixedVector::from_f32_slice(&[x, y]);
1120 total += 1;
1121
1122 let grid_result = grid.query(&q);
1123 let (brute_idx, _) = nearest_by_power_distance(&q, &klein_only).unwrap();
1124 let brute_result = ids[brute_idx];
1125
1126 if grid_result != Some(brute_result) {
1127 mismatches += 1;
1128 }
1129 }
1130 }
1131
1132 let mismatch_rate = mismatches as f64 / total as f64;
1134 assert!(mismatch_rate < 0.05,
1135 "Grid vs brute-force mismatch rate {} ({}/{}) exceeds 5%",
1136 mismatch_rate, mismatches, total);
1137 }
1138
1139 #[test]
1140 fn test_insert_preserves_grid_correctness() {
1141 let mut sites: Vec<(String, KleinPoint)> = Vec::new();
1143 let mut grid = PointLocationGrid::new(32);
1144
1145 let root_k = KleinPoint::new(FixedVector::from_f32_slice(&[0.0, 0.0]));
1147 sites.push(("root".to_string(), root_k.clone()));
1148 grid.build(&sites);
1149
1150 let child_coords: Vec<[f32; 2]> = vec![
1152 [0.3, 0.0], [-0.3, 0.0], [0.0, 0.3], [0.0, -0.3],
1153 [0.5, 0.1], [-0.4, 0.3], [0.1, 0.6], [-0.1, -0.5],
1154 [0.2, 0.2], [-0.2, 0.2], [0.2, -0.2], [-0.2, -0.2],
1155 [0.7, 0.0], [0.0, 0.7], [-0.6, 0.1], [0.3, -0.4],
1156 [0.4, 0.4], [-0.3, -0.3], [0.15, 0.15], [-0.15, 0.45],
1157 ];
1158
1159 for (i, coords) in child_coords.iter().enumerate() {
1160 let p = HyperbolicPoint::from_f32_slice(coords);
1161 let k = poincare_to_klein(&p);
1162 let new_id = format!("node_{}", i);
1163
1164 grid.update_insert("root", &new_id, &k, &root_k);
1166 sites.push((new_id, k));
1167
1168 if (i + 1) % 5 == 0 {
1170 let klein_only: Vec<KleinPoint> = sites.iter().map(|(_, k)| k.clone()).collect();
1171 let test_queries: Vec<[f32; 2]> = vec![[0.0, 0.0], [0.2, 0.1], [-0.3, 0.2]];
1173 for q_coords in &test_queries {
1174 let q = FixedVector::from_f32_slice(q_coords);
1175 if q.length_squared() >= fp(1) { continue; }
1176
1177 let grid_r = grid.query(&q);
1178 let (_brute_idx, _) = nearest_by_power_distance(&q, &klein_only).unwrap();
1179 assert!(grid_r.is_some(),
1181 "Grid should return a result for query inside disk");
1182 }
1183 }
1184 }
1185 }
1186
1187 #[test]
1188 fn test_empty_tree_grid() {
1189 let grid = PointLocationGrid::new(16);
1191 let q = FixedVector::from_f32_slice(&[0.0, 0.0]);
1192 assert_eq!(grid.query(&q), None, "Empty grid should return None");
1193 }
1194
1195 #[test]
1196 fn test_query_outside_disk_clamped() {
1197 let sites = vec![
1199 ("root".to_string(), KleinPoint::new(FixedVector::from_f32_slice(&[0.0, 0.0]))),
1200 ];
1201 let mut grid = PointLocationGrid::new(16);
1202 grid.build(&sites);
1203
1204 let q = FixedVector::from_f32_slice(&[1.5, 0.0]);
1205 let _result = grid.query(&q);
1207 }
1208
1209 #[test]
1210 fn test_inverted_index_consistency_after_build() {
1211 let sites = vec![
1212 ("a".to_string(), KleinPoint::new(FixedVector::from_f32_slice(&[0.3, 0.0]))),
1213 ("b".to_string(), KleinPoint::new(FixedVector::from_f32_slice(&[-0.3, 0.0]))),
1214 ("c".to_string(), KleinPoint::new(FixedVector::from_f32_slice(&[0.0, 0.4]))),
1215 ];
1216
1217 let mut grid = PointLocationGrid::new(32);
1218 grid.build(&sites);
1219
1220 for (id, tiles) in &grid.tile_owners {
1222 for &idx in tiles {
1223 assert_eq!(grid.grid[idx].as_deref(), Some(id.as_str()),
1224 "tile_owners[{}] contains idx {} but grid[{}] = {:?}",
1225 id, idx, idx, grid.grid[idx]);
1226 }
1227 }
1228
1229 for (idx, cell) in grid.grid.iter().enumerate() {
1231 if let Some(ref id) = cell {
1232 let tiles = grid.tile_owners.get(id).expect("grid has id not in tile_owners");
1233 assert!(tiles.contains(&idx),
1234 "grid[{}] = {} but tile_owners[{}] doesn't contain {}", idx, id, id, idx);
1235 }
1236 }
1237 }
1238
1239 #[test]
1240 fn test_inverted_index_after_insert() {
1241 let parent_site = KleinPoint::new(FixedVector::from_f32_slice(&[0.0, 0.0]));
1242 let sites = vec![("parent".to_string(), parent_site.clone())];
1243
1244 let mut grid = PointLocationGrid::new(16);
1245 grid.build(&sites);
1246
1247 let parent_tiles_before = grid.tile_owners.get("parent").map(|v| v.len()).unwrap_or(0);
1248 assert!(parent_tiles_before > 0, "Parent should own tiles after build");
1249
1250 let child_site = KleinPoint::new(FixedVector::from_f32_slice(&[0.5, 0.0]));
1252 grid.update_insert("parent", "child", &child_site, &parent_site);
1253
1254 let parent_tiles_after = grid.tile_owners.get("parent").map(|v| v.len()).unwrap_or(0);
1255 let child_tiles = grid.tile_owners.get("child").map(|v| v.len()).unwrap_or(0);
1256
1257 assert!(child_tiles > 0, "Child should own some tiles");
1258 assert_eq!(parent_tiles_before, parent_tiles_after + child_tiles,
1259 "Total tiles should be conserved: {} != {} + {}",
1260 parent_tiles_before, parent_tiles_after, child_tiles);
1261
1262 for (id, tiles) in &grid.tile_owners {
1264 for &idx in tiles {
1265 assert_eq!(grid.grid[idx].as_deref(), Some(id.as_str()));
1266 }
1267 }
1268 }
1269
1270 #[test]
1271 fn test_inverted_index_after_delete() {
1272 let parent_site = KleinPoint::new(FixedVector::from_f32_slice(&[0.0, 0.0]));
1273 let child_site = KleinPoint::new(FixedVector::from_f32_slice(&[0.5, 0.0]));
1274
1275 let sites = vec![
1276 ("parent".to_string(), parent_site.clone()),
1277 ("child".to_string(), child_site.clone()),
1278 ];
1279
1280 let mut grid = PointLocationGrid::new(16);
1281 grid.build(&sites);
1282
1283 let total_before: usize = grid.tile_owners.values().map(|v| v.len()).sum();
1284 let child_tiles_before = grid.tile_owners.get("child").map(|v| v.len()).unwrap_or(0);
1285 assert!(child_tiles_before > 0, "Child should own tiles");
1286
1287 grid.update_delete("child", "parent");
1289
1290 assert!(grid.tile_owners.get("child").is_none(),
1291 "Deleted node should be removed from tile_owners");
1292
1293 let parent_tiles_after = grid.tile_owners.get("parent").map(|v| v.len()).unwrap_or(0);
1294 assert_eq!(parent_tiles_after, total_before,
1295 "Parent should absorb all tiles: {} vs {}", parent_tiles_after, total_before);
1296
1297 for (id, tiles) in &grid.tile_owners {
1299 for &idx in tiles {
1300 assert_eq!(grid.grid[idx].as_deref(), Some(id.as_str()));
1301 }
1302 }
1303 }
1304}