1use g_math::fixed_point::{FixedPoint, FixedVector};
15use crate::constants;
16use crate::hyperbolic_geometry::HyperbolicPoint;
17
18#[derive(Clone, Debug)]
27pub struct KleinPoint {
28 pub coords: FixedVector,
30 pub weight: FixedPoint,
32}
33
34impl KleinPoint {
35 pub fn new(coords: FixedVector) -> Self {
37 let weight = FixedPoint::from_int(1) - coords.length_squared();
38 Self { coords, weight }
39 }
40
41 pub fn dimension(&self) -> usize {
43 self.coords.len()
44 }
45}
46
47pub fn poincare_to_klein(p: &HyperbolicPoint) -> KleinPoint {
56 let dim = p.dimension();
57 let norm_sq = p.coords().length_squared();
58 let one = FixedPoint::from_int(1);
59 let two = FixedPoint::from_int(2);
60
61 let denom = one + norm_sq; let scale = two / denom; let mut klein_coords = FixedVector::new(dim);
65 for i in 0..dim {
66 klein_coords[i] = p.coords()[i] * scale;
67 }
68
69 KleinPoint::new(klein_coords)
70}
71
72pub fn klein_to_poincare(k: &KleinPoint) -> HyperbolicPoint {
76 let dim = k.dimension();
77 let one = FixedPoint::from_int(1);
78 let norm_sq = k.coords.length_squared();
79
80 if norm_sq < constants::small_epsilon() {
82 return HyperbolicPoint::origin(dim);
83 }
84
85 let sqrt_term = (one - norm_sq).sqrt(); let denom = one + sqrt_term;
87 let inv_denom = one / denom;
88
89 let mut poincare_coords = FixedVector::new(dim);
90 for i in 0..dim {
91 poincare_coords[i] = k.coords[i] * inv_denom;
92 }
93
94 HyperbolicPoint::new(poincare_coords)
95}
96
97pub fn weighted_barycenter(sites: &[(KleinPoint, FixedPoint)]) -> Option<KleinPoint> {
117 let zero = FixedPoint::from_int(0);
118 let one = FixedPoint::from_int(1);
119
120 let mut dim = 0;
121 let mut denom = zero;
122 let mut numer: Option<FixedVector> = None;
123
124 for (site, w) in sites {
125 if *w <= zero {
126 continue;
127 }
128 let radicand = if site.weight > constants::small_epsilon() {
132 site.weight
133 } else {
134 constants::small_epsilon()
135 };
136 let gamma = one / radicand.sqrt();
137 let coeff = *w * gamma;
138
139 if numer.is_none() {
140 dim = site.dimension();
141 numer = Some(FixedVector::new(dim));
142 }
143 let acc = numer.as_mut().unwrap();
144 for i in 0..dim {
145 acc[i] += site.coords[i] * coeff;
146 }
147 denom += coeff;
148 }
149
150 let numer = numer?;
151 if denom <= zero {
152 return None;
153 }
154 let inv = one / denom;
155 let mut coords = FixedVector::new(dim);
156 for i in 0..dim {
157 coords[i] = numer[i] * inv;
158 }
159 Some(KleinPoint::new(coords))
160}
161
162pub fn power_distance(query: &FixedVector, site: &KleinPoint) -> FixedPoint {
172 let dim = query.len();
173 assert_eq!(dim, site.dimension(), "Dimension mismatch");
174
175 let mut dist_sq = FixedPoint::from_int(0);
181 for i in 0..dim {
182 let d = query[i] - site.coords[i];
183 dist_sq = dist_sq + d * d;
184 }
185
186 dist_sq - site.weight
187}
188
189pub fn nearest_by_power_distance(query: &FixedVector, sites: &[KleinPoint]) -> Option<(usize, FixedPoint)> {
193 if sites.is_empty() {
194 return None;
195 }
196
197 let mut best_idx = 0;
198 let mut best_pd = power_distance(query, &sites[0]);
199
200 for (i, site) in sites.iter().enumerate().skip(1) {
201 let pd = power_distance(query, site);
202 if pd < best_pd {
203 best_pd = pd;
204 best_idx = i;
205 }
206 }
207
208 Some((best_idx, best_pd))
209}
210
211#[derive(Clone, Debug)]
220pub struct HalfPlane {
221 pub normal: FixedVector,
223 pub offset: FixedPoint,
225 pub neighbor_id: String,
227}
228
229#[derive(Clone, Debug)]
234pub struct PowerCell {
235 pub node_id: String,
237 pub site: KleinPoint,
239 pub half_planes: Vec<HalfPlane>,
241}
242
243pub fn compute_bisector(site_i: &KleinPoint, site_j: &KleinPoint, neighbor_id: &str) -> HalfPlane {
251 let dim = site_i.dimension();
252 assert_eq!(dim, site_j.dimension(), "Dimension mismatch");
253
254 let mut normal = FixedVector::new(dim);
256 for i in 0..dim {
257 normal[i] = site_j.coords[i] - site_i.coords[i];
258 }
259
260 let offset = site_j.coords.length_squared() - site_i.coords.length_squared();
262
263 HalfPlane {
264 normal,
265 offset,
266 neighbor_id: neighbor_id.to_string(),
267 }
268}
269
270pub fn point_in_cell(query: &FixedVector, cell: &PowerCell) -> bool {
274 for hp in &cell.half_planes {
275 let dot = query.dot(&hp.normal);
276 if dot > hp.offset {
277 return false;
278 }
279 }
280 true
281}
282
283pub struct PointLocationGrid {
293 pub resolution: usize,
295 dimension: usize,
297 cell_size: FixedPoint,
299 inv_cell_size: FixedPoint,
301 grid: Vec<Option<String>>,
303 tile_owners: std::collections::HashMap<String, Vec<usize>>,
306}
307
308impl PointLocationGrid {
309 pub fn new(resolution: usize) -> Self {
312 Self::with_dimension(resolution, 2)
313 }
314
315 pub fn with_dimension(resolution: usize, dimension: usize) -> Self {
322 let resolution = resolution.max(1);
323 let res_fp = FixedPoint::from_int(resolution as i32);
324 let two = FixedPoint::from_int(2);
325 let cell_size = two / res_fp;
326 let inv_cell_size = res_fp / two;
327
328 Self {
329 resolution,
330 dimension,
331 cell_size,
332 inv_cell_size,
333 grid: vec![None; resolution * resolution],
334 tile_owners: std::collections::HashMap::new(),
335 }
336 }
337
338 pub fn build(&mut self, sites: &[(String, KleinPoint)]) {
343 if sites.is_empty() {
344 return;
345 }
346
347 let one = FixedPoint::from_int(1);
348
349 for row in 0..self.resolution {
350 for col in 0..self.resolution {
351 let center = self.tile_center(row, col);
352
353 if center.length_squared() >= one {
355 self.grid[row * self.resolution + col] = None;
356 continue;
357 }
358
359 let mut best_id: Option<&str> = None;
361 let mut best_pd = FixedPoint::from_int(0);
362 let mut first = true;
363
364 for (id, site) in sites {
365 let pd = power_distance(¢er, site);
366 if first || pd < best_pd {
367 best_pd = pd;
368 best_id = Some(id.as_str());
369 first = false;
370 }
371 }
372
373 self.grid[row * self.resolution + col] = best_id.map(|s| s.to_string());
374 }
375 }
376
377 self.tile_owners.clear();
379 for (idx, cell) in self.grid.iter().enumerate() {
380 if let Some(ref id) = cell {
381 self.tile_owners.entry(id.clone()).or_default().push(idx);
382 }
383 }
384 }
385
386 pub fn query(&self, query_klein: &FixedVector) -> Option<&str> {
390 let (row, col) = self.coords_to_tile(query_klein);
392
393 if row >= self.resolution || col >= self.resolution {
394 return None;
395 }
396
397 self.grid[row * self.resolution + col].as_deref()
398 }
399
400 pub fn update_insert(&mut self, parent_id: &str, new_id: &str, new_site: &KleinPoint, parent_site: &KleinPoint) {
406 let one = FixedPoint::from_int(1);
407
408 let parent_tiles = match self.tile_owners.get(parent_id) {
410 Some(tiles) => tiles.clone(),
411 None => return,
412 };
413
414 let mut tiles_to_reassign = Vec::new();
415
416 for &idx in &parent_tiles {
417 let row = idx / self.resolution;
418 let col = idx % self.resolution;
419 let center = self.tile_center(row, col);
420
421 if center.length_squared() >= one {
422 continue;
423 }
424
425 let pd_parent = power_distance(¢er, parent_site);
426 let pd_new = power_distance(¢er, new_site);
427
428 if pd_new < pd_parent {
429 tiles_to_reassign.push(idx);
430 }
431 }
432
433 for &idx in &tiles_to_reassign {
435 self.grid[idx] = Some(new_id.to_string());
436 }
437
438 if !tiles_to_reassign.is_empty() {
440 if let Some(parent_list) = self.tile_owners.get_mut(parent_id) {
441 parent_list.retain(|idx| !tiles_to_reassign.contains(idx));
442 }
443 self.tile_owners.entry(new_id.to_string())
444 .or_default()
445 .extend(&tiles_to_reassign);
446 }
447 }
448
449 pub fn update_delete(&mut self, deleted_id: &str, parent_id: &str) {
453 let deleted_tiles = match self.tile_owners.remove(deleted_id) {
455 Some(tiles) => tiles,
456 None => return,
457 };
458
459 for &idx in &deleted_tiles {
461 self.grid[idx] = Some(parent_id.to_string());
462 }
463
464 self.tile_owners.entry(parent_id.to_string())
466 .or_default()
467 .extend(deleted_tiles);
468 }
469
470 fn tile_center(&self, row: usize, col: usize) -> FixedVector {
473 let half = constants::half();
474 let one = FixedPoint::from_int(1);
475
476 let col_fp = FixedPoint::from_int(col as i32);
480 let row_fp = FixedPoint::from_int(row as i32);
481
482 let x = -one + (col_fp + half) * self.cell_size;
483 let y = -one + (row_fp + half) * self.cell_size;
484
485 let mut v = FixedVector::new(self.dimension);
486 v[0] = x;
487 if self.dimension >= 2 {
488 v[1] = y;
489 }
490 v
492 }
493
494 fn coords_to_tile(&self, coords: &FixedVector) -> (usize, usize) {
496 let one = FixedPoint::from_int(1);
497
498 let x = coords[0];
500 let y = if coords.len() >= 2 { coords[1] } else { FixedPoint::from_int(0) };
501 let col_fp = (x + one) * self.inv_cell_size;
502 let row_fp = (y + one) * self.inv_cell_size;
503
504 let col = col_fp.to_int().max(0) as usize;
505 let row = row_fp.to_int().max(0) as usize;
506
507 (row.min(self.resolution - 1), col.min(self.resolution - 1))
508 }
509
510 pub fn assigned_tile_count(&self) -> usize {
512 self.grid.iter().filter(|t| t.is_some()).count()
513 }
514
515 pub fn resolution(&self) -> usize {
517 self.resolution
518 }
519}
520
521impl std::fmt::Debug for PointLocationGrid {
522 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
523 write!(f, "PointLocationGrid(resolution={}, assigned={})",
524 self.resolution, self.assigned_tile_count())
525 }
526}
527
528#[cfg(test)]
533mod tests {
534 use super::*;
535 use crate::constants;
536
537 fn fp(v: i32) -> FixedPoint {
538 FixedPoint::from_int(v)
539 }
540
541 fn fp_approx_eq(a: FixedPoint, b: FixedPoint, tol: FixedPoint) -> bool {
542 (a - b).abs() < tol
543 }
544
545 fn klein_at(x: f32, y: f32) -> KleinPoint {
548 poincare_to_klein(&HyperbolicPoint::from_f32_slice(&[x, y]))
549 }
550
551 #[test]
552 fn barycenter_single_site_is_identity() {
553 let site = klein_at(0.4, -0.2);
554 let m = weighted_barycenter(&[(site.clone(), fp(3))]).unwrap();
555 assert!(fp_approx_eq(m.coords[0], site.coords[0], constants::epsilon()));
556 assert!(fp_approx_eq(m.coords[1], site.coords[1], constants::epsilon()));
557 }
558
559 #[test]
560 fn barycenter_equal_weights_matches_verified_midpoint() {
561 let pa = HyperbolicPoint::from_f32_slice(&[0.5, 0.1]);
564 let pb = HyperbolicPoint::from_f32_slice(&[-0.2, 0.4]);
565 let expected = pa.hyperbolic_midpoint(&pb);
566
567 let m = weighted_barycenter(&[
568 (poincare_to_klein(&pa), fp(1)),
569 (poincare_to_klein(&pb), fp(1)),
570 ])
571 .unwrap();
572 let got = klein_to_poincare(&m);
573
574 let tol = FixedPoint::from_int(1) / FixedPoint::from_int(1000);
575 assert!(
576 fp_approx_eq(got.coords()[0], expected.coords()[0], tol)
577 && fp_approx_eq(got.coords()[1], expected.coords()[1], tol),
578 "einstein midpoint {:?} != gyro midpoint {:?}",
579 got, expected
580 );
581 }
582
583 #[test]
584 fn barycenter_is_weight_scale_invariant() {
585 let sites = [klein_at(0.3, 0.3), klein_at(-0.4, 0.1), klein_at(0.0, -0.5)];
586 let a = weighted_barycenter(&[
587 (sites[0].clone(), fp(1)),
588 (sites[1].clone(), fp(2)),
589 (sites[2].clone(), fp(3)),
590 ])
591 .unwrap();
592 let b = weighted_barycenter(&[
593 (sites[0].clone(), fp(7)),
594 (sites[1].clone(), fp(14)),
595 (sites[2].clone(), fp(21)),
596 ])
597 .unwrap();
598 let tol = FixedPoint::from_int(1) / FixedPoint::from_int(100000);
599 assert!(fp_approx_eq(a.coords[0], b.coords[0], tol));
600 assert!(fp_approx_eq(a.coords[1], b.coords[1], tol));
601 }
602
603 #[test]
604 fn barycenter_stays_inside_disk_and_handles_zero_weights() {
605 let m = weighted_barycenter(&[
607 (klein_at(0.9, 0.0), fp(100)),
608 (klein_at(-0.9, 0.0), fp(1)),
609 ])
610 .unwrap();
611 assert!(m.coords.length_squared() < FixedPoint::from_int(1));
612
613 assert!(weighted_barycenter(&[(klein_at(0.5, 0.0), fp(0))]).is_none());
615 assert!(weighted_barycenter(&[]).is_none());
616 let only_positive = weighted_barycenter(&[
617 (klein_at(0.5, 0.0), fp(0)),
618 (klein_at(0.2, 0.2), fp(1)),
619 (klein_at(0.7, 0.0), fp(-2)),
620 ])
621 .unwrap();
622 let expected = klein_at(0.2, 0.2);
623 assert!(fp_approx_eq(only_positive.coords[0], expected.coords[0], constants::epsilon()));
624 assert!(fp_approx_eq(only_positive.coords[1], expected.coords[1], constants::epsilon()));
625 }
626
627 #[test]
630 fn test_klein_origin_maps_to_origin() {
631 let origin = HyperbolicPoint::origin(2);
632 let k = poincare_to_klein(&origin);
633
634 assert!(k.coords[0].abs() < constants::epsilon());
635 assert!(k.coords[1].abs() < constants::epsilon());
636 assert!(fp_approx_eq(k.weight, fp(1), constants::epsilon()));
638 }
639
640 #[test]
641 fn test_klein_roundtrip() {
642 let p = HyperbolicPoint::from_f32_slice(&[0.5, 0.0]);
644 let k = poincare_to_klein(&p);
645 let p2 = klein_to_poincare(&k);
646
647 let tol = constants::epsilon();
648 assert!(fp_approx_eq(p.coords()[0], p2.coords()[0], tol),
649 "x roundtrip: {} vs {}", p.coords()[0], p2.coords()[0]);
650 assert!(fp_approx_eq(p.coords()[1], p2.coords()[1], tol),
651 "y roundtrip: {} vs {}", p.coords()[1], p2.coords()[1]);
652 }
653
654 #[test]
655 fn test_klein_roundtrip_multiple() {
656 let test_points: Vec<[f32; 2]> = vec![
658 [0.3, 0.2],
659 [-0.4, 0.1],
660 [0.0, 0.7],
661 [0.1, -0.5],
662 [0.8, 0.0],
663 ];
664
665 let tol = constants::epsilon();
666 for coords in &test_points {
667 let p = HyperbolicPoint::from_f32_slice(coords);
668 let k = poincare_to_klein(&p);
669 let p2 = klein_to_poincare(&k);
670
671 assert!(fp_approx_eq(p.coords()[0], p2.coords()[0], tol),
672 "Roundtrip failed for ({}, {})", coords[0], coords[1]);
673 assert!(fp_approx_eq(p.coords()[1], p2.coords()[1], tol),
674 "Roundtrip failed for ({}, {})", coords[0], coords[1]);
675 }
676 }
677
678 #[test]
679 fn test_klein_known_example() {
680 let p = HyperbolicPoint::from_f32_slice(&[0.5, 0.0]);
684 let k = poincare_to_klein(&p);
685
686 let tol = FixedPoint::from_int(1) / FixedPoint::from_int(100);
687 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),
691 "Klein x: expected 0.8, got {}", k.coords[0]);
692 assert!(k.coords[1].abs() < tol,
693 "Klein y: expected 0, got {}", k.coords[1]);
694 assert!(fp_approx_eq(k.weight, expected_w, tol),
695 "Klein weight: expected 0.36, got {}", k.weight);
696 }
697
698 #[test]
699 fn test_klein_boundary_behavior() {
700 let near_boundary = HyperbolicPoint::from_f32_slice(&[0.95, 0.0]);
702 let k = poincare_to_klein(&near_boundary);
703
704 let k_norm = k.coords.length();
706 assert!(k_norm > FixedPoint::from_int(9) / FixedPoint::from_int(10),
707 "Klein norm should be near 1 for boundary point, got {}", k_norm);
708 assert!(k_norm < FixedPoint::from_int(1),
709 "Klein norm should be < 1, got {}", k_norm);
710 }
711
712 #[test]
713 fn test_power_distance_at_site_center() {
714 let p = HyperbolicPoint::from_f32_slice(&[0.5, 0.0]);
716 let k = poincare_to_klein(&p);
717
718 let pd = power_distance(&k.coords, &k);
719 let expected = -k.weight; let tol = constants::epsilon();
722 assert!(fp_approx_eq(pd, expected, tol),
723 "Power distance at site center should be -weight: {} vs {}", pd, expected);
724 assert!(pd < FixedPoint::from_int(0),
725 "Power distance at own site should be negative");
726 }
727
728 #[test]
729 fn test_power_distance_ordering_matches_hyperbolic() {
730 let query_p = HyperbolicPoint::from_f32_slice(&[0.1, 0.1]);
733 let site1_p = HyperbolicPoint::from_f32_slice(&[0.2, 0.0]);
734 let site2_p = HyperbolicPoint::from_f32_slice(&[0.6, 0.3]);
735
736 let query_k = poincare_to_klein(&query_p);
737 let site1_k = poincare_to_klein(&site1_p);
738 let site2_k = poincare_to_klein(&site2_p);
739
740 let pd1 = power_distance(&query_k.coords, &site1_k);
741 let pd2 = power_distance(&query_k.coords, &site2_k);
742
743 let hd1 = query_p.hyperbolic_distance(&site1_p);
744 let hd2 = query_p.hyperbolic_distance(&site2_p);
745
746 if hd1 < hd2 {
748 assert!(pd1 < pd2,
749 "Power distance ordering should match hyperbolic: pd1={} pd2={}, hd1={} hd2={}",
750 pd1, pd2, hd1, hd2);
751 } else {
752 assert!(pd2 <= pd1,
753 "Power distance ordering should match hyperbolic: pd1={} pd2={}, hd1={} hd2={}",
754 pd1, pd2, hd1, hd2);
755 }
756 }
757
758 #[test]
759 fn test_nearest_by_power_distance() {
760 let sites = vec![
761 KleinPoint::new(FixedVector::from_f32_slice(&[0.2, 0.0])),
762 KleinPoint::new(FixedVector::from_f32_slice(&[0.8, 0.0])),
763 KleinPoint::new(FixedVector::from_f32_slice(&[0.0, 0.5])),
764 ];
765
766 let query = FixedVector::from_f32_slice(&[0.1, 0.0]);
767
768 let (idx, _pd) = nearest_by_power_distance(&query, &sites).unwrap();
769
770 assert_eq!(idx, 0, "Nearest should be site 0");
772 }
773
774 #[test]
777 fn test_compute_bisector_symmetry() {
778 let site_i = KleinPoint::new(FixedVector::from_f32_slice(&[0.2, 0.0]));
779 let site_j = KleinPoint::new(FixedVector::from_f32_slice(&[0.6, 0.0]));
780
781 let hp_ij = compute_bisector(&site_i, &site_j, "j");
782 let hp_ji = compute_bisector(&site_j, &site_i, "i");
783
784 let tol = constants::epsilon();
786 assert!(fp_approx_eq(hp_ij.normal[0], -hp_ji.normal[0], tol));
787 assert!(fp_approx_eq(hp_ij.offset, -hp_ji.offset, tol));
788 }
789
790 #[test]
791 fn test_point_in_cell_at_site_center() {
792 let site_i = KleinPoint::new(FixedVector::from_f32_slice(&[0.2, 0.0]));
794 let site_j = KleinPoint::new(FixedVector::from_f32_slice(&[0.6, 0.0]));
795
796 let hp = compute_bisector(&site_i, &site_j, "j");
797 let cell = PowerCell {
798 node_id: "i".to_string(),
799 site: site_i.clone(),
800 half_planes: vec![hp],
801 };
802
803 assert!(point_in_cell(&site_i.coords, &cell),
804 "Site center should be inside its own cell");
805 }
806
807 #[test]
808 fn test_bisector_midpoint_on_boundary() {
809 let site_i = KleinPoint::new(FixedVector::from_f32_slice(&[0.2, 0.0]));
811 let site_j = KleinPoint::new(FixedVector::from_f32_slice(&[0.6, 0.0]));
812
813 let hp = compute_bisector(&site_i, &site_j, "j");
814
815 let mut midpoint = FixedVector::new(2);
817 midpoint[0] = (site_i.coords[0] + site_j.coords[0]) * constants::half();
818 midpoint[1] = (site_i.coords[1] + site_j.coords[1]) * constants::half();
819
820 let _pd_i = power_distance(&midpoint, &site_i);
824 let _pd_j = power_distance(&midpoint, &site_j);
825
826 let bisector_x = hp.offset / hp.normal[0];
834 let mut bisector_pt = FixedVector::new(2);
835 bisector_pt[0] = bisector_x;
836
837 let pd_i_bpt = power_distance(&bisector_pt, &site_i);
838 let pd_j_bpt = power_distance(&bisector_pt, &site_j);
839
840 let tol = constants::epsilon();
841 assert!(fp_approx_eq(pd_i_bpt, pd_j_bpt, tol),
842 "Bisector point should have equal power distances: {} vs {}", pd_i_bpt, pd_j_bpt);
843 }
844
845 #[test]
846 fn test_cell_membership_consistency() {
847 let site_a = KleinPoint::new(FixedVector::from_f32_slice(&[0.3, 0.0]));
849 let site_b = KleinPoint::new(FixedVector::from_f32_slice(&[-0.3, 0.0]));
850
851 let hp_ab = compute_bisector(&site_a, &site_b, "b");
852 let hp_ba = compute_bisector(&site_b, &site_a, "a");
853
854 let cell_a = PowerCell {
855 node_id: "a".to_string(),
856 site: site_a.clone(),
857 half_planes: vec![hp_ab],
858 };
859 let cell_b = PowerCell {
860 node_id: "b".to_string(),
861 site: site_b.clone(),
862 half_planes: vec![hp_ba],
863 };
864
865 let test_xs: Vec<f32> = vec![-0.8, -0.5, -0.2, 0.0, 0.2, 0.5, 0.8];
867 for &x in &test_xs {
868 let q = FixedVector::from_f32_slice(&[x, 0.0]);
869 let in_a = point_in_cell(&q, &cell_a);
870 let in_b = point_in_cell(&q, &cell_b);
871
872 assert!(in_a || in_b,
874 "Point ({}, 0) should be in at least one cell", x);
875 }
876 }
877
878 #[test]
881 fn test_grid_single_site() {
882 let sites = vec![
884 ("root".to_string(), KleinPoint::new(FixedVector::from_f32_slice(&[0.0, 0.0]))),
885 ];
886
887 let mut grid = PointLocationGrid::new(16);
888 grid.build(&sites);
889
890 let test_points: Vec<[f32; 2]> = vec![[0.0, 0.0], [0.5, 0.0], [0.0, -0.5], [0.3, 0.3]];
892 for coords in &test_points {
893 let q = FixedVector::from_f32_slice(coords);
894 let result = grid.query(&q);
895 assert_eq!(result, Some("root"), "Single site should own all tiles");
896 }
897 }
898
899 #[test]
900 fn test_grid_query_matches_brute_force() {
901 let sites = vec![
903 ("a".to_string(), KleinPoint::new(FixedVector::from_f32_slice(&[0.3, 0.0]))),
904 ("b".to_string(), KleinPoint::new(FixedVector::from_f32_slice(&[-0.3, 0.0]))),
905 ("c".to_string(), KleinPoint::new(FixedVector::from_f32_slice(&[0.0, 0.4]))),
906 ];
907
908 let mut grid = PointLocationGrid::new(32);
909 grid.build(&sites);
910
911 let test_points: Vec<[f32; 2]> = vec![
913 [0.2, 0.0], [-0.2, 0.0], [0.0, 0.3],
914 [0.5, 0.1], [-0.4, -0.2], [0.1, 0.5],
915 ];
916
917 let klein_sites: Vec<KleinPoint> = sites.iter().map(|(_, s)| s.clone()).collect();
918 let ids: Vec<&str> = sites.iter().map(|(id, _)| id.as_str()).collect();
919
920 for coords in &test_points {
921 let q = FixedVector::from_f32_slice(coords);
922 if q.length_squared() >= FixedPoint::from_int(1) {
923 continue;
924 }
925
926 let grid_result = grid.query(&q);
927 let (brute_idx, _) = nearest_by_power_distance(&q, &klein_sites).unwrap();
928 let brute_result = ids[brute_idx];
929
930 assert_eq!(grid_result, Some(brute_result),
931 "Grid mismatch at ({}, {}): grid={:?} brute={}",
932 coords[0], coords[1], grid_result, brute_result);
933 }
934 }
935
936 #[test]
937 fn test_grid_insert_update() {
938 let parent_site = KleinPoint::new(FixedVector::from_f32_slice(&[0.0, 0.0]));
940 let sites = vec![
941 ("parent".to_string(), parent_site.clone()),
942 ];
943
944 let mut grid = PointLocationGrid::new(16);
945 grid.build(&sites);
946
947 let q = FixedVector::from_f32_slice(&[0.5, 0.0]);
949 assert_eq!(grid.query(&q), Some("parent"));
950
951 let child_site = KleinPoint::new(FixedVector::from_f32_slice(&[0.5, 0.0]));
953 grid.update_insert("parent", "child", &child_site, &parent_site);
954
955 let q_near_child = FixedVector::from_f32_slice(&[0.6, 0.0]);
957 let result = grid.query(&q_near_child);
958 assert_eq!(result, Some("child"),
959 "After insert, tile near child should be owned by child");
960
961 let q_origin = FixedVector::from_f32_slice(&[0.0, 0.0]);
963 assert_eq!(grid.query(&q_origin), Some("parent"),
964 "Origin tile should still be parent");
965 }
966
967 #[test]
968 fn test_grid_delete_update() {
969 let parent_site = KleinPoint::new(FixedVector::from_f32_slice(&[0.0, 0.0]));
970 let child_site = KleinPoint::new(FixedVector::from_f32_slice(&[0.5, 0.0]));
971
972 let sites = vec![
973 ("parent".to_string(), parent_site.clone()),
974 ("child".to_string(), child_site.clone()),
975 ];
976
977 let mut grid = PointLocationGrid::new(16);
978 grid.build(&sites);
979
980 let q = FixedVector::from_f32_slice(&[0.6, 0.0]);
982 assert_eq!(grid.query(&q), Some("child"));
983
984 grid.update_delete("child", "parent");
986
987 assert_eq!(grid.query(&q), Some("parent"),
988 "After delete, child's tiles should revert to parent");
989 }
990
991 #[test]
992 fn test_grid_tile_count() {
993 let resolution = 64;
995 let sites = vec![
996 ("root".to_string(), KleinPoint::new(FixedVector::from_f32_slice(&[0.0, 0.0]))),
997 ];
998
999 let mut grid = PointLocationGrid::new(resolution);
1000 grid.build(&sites);
1001
1002 let assigned = grid.assigned_tile_count();
1003 let expected_approx = (std::f64::consts::PI / 4.0 * (resolution as f64).powi(2)) as usize;
1004
1005 let lower = expected_approx * 9 / 10;
1007 let upper = expected_approx * 11 / 10;
1008 assert!(assigned >= lower && assigned <= upper,
1009 "Assigned tiles {} should be near π/4·{}² ≈ {} (range [{}, {}])",
1010 assigned, resolution, expected_approx, lower, upper);
1011 }
1012
1013 #[test]
1014 fn test_klein_roundtrip_4d() {
1015 let p = HyperbolicPoint::from_f32_slice(&[0.3, 0.2, -0.1, 0.15]);
1017 let k = poincare_to_klein(&p);
1018 let p2 = klein_to_poincare(&k);
1019
1020 let tol = constants::epsilon();
1021 for i in 0..4 {
1022 assert!(fp_approx_eq(p.coords()[i], p2.coords()[i], tol),
1023 "4D roundtrip failed at dim {}: {} vs {}", i, p.coords()[i], p2.coords()[i]);
1024 }
1025 }
1026
1027 #[test]
1030 fn test_power_distance_ordering_equidistant_sites() {
1031 let tau = constants::default_tau();
1035 let half_tau = tau * constants::half();
1036 let r = half_tau.tanh(); let angles: Vec<FixedPoint> = vec![
1040 FixedPoint::from_int(0),
1041 FixedPoint::from_int(3) / FixedPoint::from_int(2),
1042 FixedPoint::from_int(3),
1043 FixedPoint::from_int(9) / FixedPoint::from_int(2),
1044 ];
1045 let sites_p: Vec<HyperbolicPoint> = angles.iter().map(|a| {
1046 let mut v = FixedVector::new(2);
1047 let (sin_a, cos_a) = a.sincos();
1048 v[0] = r * cos_a;
1049 v[1] = r * sin_a;
1050 HyperbolicPoint::new(v)
1051 }).collect();
1052
1053 let sites_k: Vec<KleinPoint> = sites_p.iter().map(|p| poincare_to_klein(p)).collect();
1054
1055 for (qi, site) in sites_p.iter().enumerate() {
1057 let mut q_coords = site.coords().clone();
1059 q_coords[0] = q_coords[0] + constants::epsilon();
1060 let q_p = HyperbolicPoint::new(q_coords.clone());
1061 let q_k = poincare_to_klein(&q_p);
1062
1063 let (pd_nn, _) = nearest_by_power_distance(&q_k.coords, &sites_k).unwrap();
1064
1065 let mut hyp_dists: Vec<(usize, FixedPoint)> = sites_p.iter().enumerate()
1066 .map(|(i, s)| (i, q_p.hyperbolic_distance(s)))
1067 .collect();
1068 hyp_dists.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
1069
1070 assert_eq!(pd_nn, hyp_dists[0].0,
1071 "Power NN should match hyperbolic NN near site {}", qi);
1072 }
1073 }
1074
1075 #[test]
1076 fn test_grid_vs_brute_force_stress() {
1077 let site_coords: Vec<[f32; 2]> = vec![
1080 [0.0, 0.0], [0.3, 0.0], [-0.3, 0.0], [0.0, 0.3], [0.0, -0.3],
1081 [0.2, 0.2], [-0.2, 0.2], [0.2, -0.2], [-0.2, -0.2],
1082 [0.5, 0.1], [-0.4, 0.3], [0.1, 0.6], [-0.1, -0.5],
1083 ];
1084
1085 let sites: Vec<(String, KleinPoint)> = site_coords.iter().enumerate()
1086 .map(|(i, c)| {
1087 let p = HyperbolicPoint::from_f32_slice(c);
1088 let k = poincare_to_klein(&p);
1089 (format!("node_{}", i), k)
1090 })
1091 .collect();
1092
1093 let mut grid = PointLocationGrid::new(64);
1094 grid.build(&sites);
1095
1096 let klein_only: Vec<KleinPoint> = sites.iter().map(|(_, k)| k.clone()).collect();
1097 let ids: Vec<&str> = sites.iter().map(|(id, _)| id.as_str()).collect();
1098
1099 let mut mismatches = 0;
1101 let mut total = 0;
1102 for xi in -9..=9 {
1103 for yi in -9..=9 {
1104 let x = xi as f32 / 10.0;
1105 let y = yi as f32 / 10.0;
1106 if x * x + y * y >= 0.99 {
1107 continue;
1108 }
1109
1110 let q = FixedVector::from_f32_slice(&[x, y]);
1111 total += 1;
1112
1113 let grid_result = grid.query(&q);
1114 let (brute_idx, _) = nearest_by_power_distance(&q, &klein_only).unwrap();
1115 let brute_result = ids[brute_idx];
1116
1117 if grid_result != Some(brute_result) {
1118 mismatches += 1;
1119 }
1120 }
1121 }
1122
1123 let mismatch_rate = mismatches as f64 / total as f64;
1125 assert!(mismatch_rate < 0.05,
1126 "Grid vs brute-force mismatch rate {} ({}/{}) exceeds 5%",
1127 mismatch_rate, mismatches, total);
1128 }
1129
1130 #[test]
1131 fn test_insert_preserves_grid_correctness() {
1132 let mut sites: Vec<(String, KleinPoint)> = Vec::new();
1134 let mut grid = PointLocationGrid::new(32);
1135
1136 let root_k = KleinPoint::new(FixedVector::from_f32_slice(&[0.0, 0.0]));
1138 sites.push(("root".to_string(), root_k.clone()));
1139 grid.build(&sites);
1140
1141 let child_coords: Vec<[f32; 2]> = vec![
1143 [0.3, 0.0], [-0.3, 0.0], [0.0, 0.3], [0.0, -0.3],
1144 [0.5, 0.1], [-0.4, 0.3], [0.1, 0.6], [-0.1, -0.5],
1145 [0.2, 0.2], [-0.2, 0.2], [0.2, -0.2], [-0.2, -0.2],
1146 [0.7, 0.0], [0.0, 0.7], [-0.6, 0.1], [0.3, -0.4],
1147 [0.4, 0.4], [-0.3, -0.3], [0.15, 0.15], [-0.15, 0.45],
1148 ];
1149
1150 for (i, coords) in child_coords.iter().enumerate() {
1151 let p = HyperbolicPoint::from_f32_slice(coords);
1152 let k = poincare_to_klein(&p);
1153 let new_id = format!("node_{}", i);
1154
1155 grid.update_insert("root", &new_id, &k, &root_k);
1157 sites.push((new_id, k));
1158
1159 if (i + 1) % 5 == 0 {
1161 let klein_only: Vec<KleinPoint> = sites.iter().map(|(_, k)| k.clone()).collect();
1162 let test_queries: Vec<[f32; 2]> = vec![[0.0, 0.0], [0.2, 0.1], [-0.3, 0.2]];
1164 for q_coords in &test_queries {
1165 let q = FixedVector::from_f32_slice(q_coords);
1166 if q.length_squared() >= fp(1) { continue; }
1167
1168 let grid_r = grid.query(&q);
1169 let (_brute_idx, _) = nearest_by_power_distance(&q, &klein_only).unwrap();
1170 assert!(grid_r.is_some(),
1172 "Grid should return a result for query inside disk");
1173 }
1174 }
1175 }
1176 }
1177
1178 #[test]
1179 fn test_empty_tree_grid() {
1180 let grid = PointLocationGrid::new(16);
1182 let q = FixedVector::from_f32_slice(&[0.0, 0.0]);
1183 assert_eq!(grid.query(&q), None, "Empty grid should return None");
1184 }
1185
1186 #[test]
1187 fn test_query_outside_disk_clamped() {
1188 let sites = vec![
1190 ("root".to_string(), KleinPoint::new(FixedVector::from_f32_slice(&[0.0, 0.0]))),
1191 ];
1192 let mut grid = PointLocationGrid::new(16);
1193 grid.build(&sites);
1194
1195 let q = FixedVector::from_f32_slice(&[1.5, 0.0]);
1196 let _result = grid.query(&q);
1198 }
1199
1200 #[test]
1201 fn test_inverted_index_consistency_after_build() {
1202 let sites = vec![
1203 ("a".to_string(), KleinPoint::new(FixedVector::from_f32_slice(&[0.3, 0.0]))),
1204 ("b".to_string(), KleinPoint::new(FixedVector::from_f32_slice(&[-0.3, 0.0]))),
1205 ("c".to_string(), KleinPoint::new(FixedVector::from_f32_slice(&[0.0, 0.4]))),
1206 ];
1207
1208 let mut grid = PointLocationGrid::new(32);
1209 grid.build(&sites);
1210
1211 for (id, tiles) in &grid.tile_owners {
1213 for &idx in tiles {
1214 assert_eq!(grid.grid[idx].as_deref(), Some(id.as_str()),
1215 "tile_owners[{}] contains idx {} but grid[{}] = {:?}",
1216 id, idx, idx, grid.grid[idx]);
1217 }
1218 }
1219
1220 for (idx, cell) in grid.grid.iter().enumerate() {
1222 if let Some(ref id) = cell {
1223 let tiles = grid.tile_owners.get(id).expect("grid has id not in tile_owners");
1224 assert!(tiles.contains(&idx),
1225 "grid[{}] = {} but tile_owners[{}] doesn't contain {}", idx, id, id, idx);
1226 }
1227 }
1228 }
1229
1230 #[test]
1231 fn test_inverted_index_after_insert() {
1232 let parent_site = KleinPoint::new(FixedVector::from_f32_slice(&[0.0, 0.0]));
1233 let sites = vec![("parent".to_string(), parent_site.clone())];
1234
1235 let mut grid = PointLocationGrid::new(16);
1236 grid.build(&sites);
1237
1238 let parent_tiles_before = grid.tile_owners.get("parent").map(|v| v.len()).unwrap_or(0);
1239 assert!(parent_tiles_before > 0, "Parent should own tiles after build");
1240
1241 let child_site = KleinPoint::new(FixedVector::from_f32_slice(&[0.5, 0.0]));
1243 grid.update_insert("parent", "child", &child_site, &parent_site);
1244
1245 let parent_tiles_after = grid.tile_owners.get("parent").map(|v| v.len()).unwrap_or(0);
1246 let child_tiles = grid.tile_owners.get("child").map(|v| v.len()).unwrap_or(0);
1247
1248 assert!(child_tiles > 0, "Child should own some tiles");
1249 assert_eq!(parent_tiles_before, parent_tiles_after + child_tiles,
1250 "Total tiles should be conserved: {} != {} + {}",
1251 parent_tiles_before, parent_tiles_after, child_tiles);
1252
1253 for (id, tiles) in &grid.tile_owners {
1255 for &idx in tiles {
1256 assert_eq!(grid.grid[idx].as_deref(), Some(id.as_str()));
1257 }
1258 }
1259 }
1260
1261 #[test]
1262 fn test_inverted_index_after_delete() {
1263 let parent_site = KleinPoint::new(FixedVector::from_f32_slice(&[0.0, 0.0]));
1264 let child_site = KleinPoint::new(FixedVector::from_f32_slice(&[0.5, 0.0]));
1265
1266 let sites = vec![
1267 ("parent".to_string(), parent_site.clone()),
1268 ("child".to_string(), child_site.clone()),
1269 ];
1270
1271 let mut grid = PointLocationGrid::new(16);
1272 grid.build(&sites);
1273
1274 let total_before: usize = grid.tile_owners.values().map(|v| v.len()).sum();
1275 let child_tiles_before = grid.tile_owners.get("child").map(|v| v.len()).unwrap_or(0);
1276 assert!(child_tiles_before > 0, "Child should own tiles");
1277
1278 grid.update_delete("child", "parent");
1280
1281 assert!(grid.tile_owners.get("child").is_none(),
1282 "Deleted node should be removed from tile_owners");
1283
1284 let parent_tiles_after = grid.tile_owners.get("parent").map(|v| v.len()).unwrap_or(0);
1285 assert_eq!(parent_tiles_after, total_before,
1286 "Parent should absorb all tiles: {} vs {}", parent_tiles_after, total_before);
1287
1288 for (id, tiles) in &grid.tile_owners {
1290 for &idx in tiles {
1291 assert_eq!(grid.grid[idx].as_deref(), Some(id.as_str()));
1292 }
1293 }
1294 }
1295}