1use std::collections::{HashMap, HashSet};
19use std::fmt::{self, Debug, Formatter};
20use std::sync::Mutex;
21use dashmap::DashMap;
22use sha3::{Sha3_512, Digest};
23use g_math::fixed_point::{FixedPoint, FixedVector};
24use super::hyperbolic_geometry::{PoincareDisk, HyperbolicPoint, distance_to_ratio};
25use crate::metric_tree::{hyperbolic_ratio_sq, sq_ratio_separation_exceeds};
26use crate::constants;
27
28#[derive(Clone, PartialEq, Eq, Hash)]
37pub struct GeometricSignature {
38 hash: String,
40 level: u32,
42 position_signature: Vec<i32>,
44}
45
46impl GeometricSignature {
47 pub fn new(hash: String, level: u32, position_signature: Vec<i32>) -> Self {
49 Self {
50 hash,
51 level,
52 position_signature,
53 }
54 }
55
56 pub fn hash(&self) -> &str {
58 &self.hash
59 }
60
61 pub fn level(&self) -> u32 {
63 self.level
64 }
65
66 pub fn position_signature(&self) -> &[i32] {
68 &self.position_signature
69 }
70
71 pub fn stub(unique_id: &str) -> Self {
73 Self {
74 hash: unique_id.to_string(),
75 level: 0,
76 position_signature: Vec::new(),
77 }
78 }
79
80 pub fn is_stub(&self) -> bool {
83 self.position_signature.is_empty()
84 }
85
86 pub fn unique_id(&self) -> String {
92 if self.position_signature.is_empty() {
93 return self.hash.clone();
95 }
96 use sha3::{Sha3_256, Digest as _};
97 let mut hasher = Sha3_256::new();
98 hasher.update(self.hash.as_bytes());
99 hasher.update(self.level.to_le_bytes());
100 for &v in &self.position_signature {
101 hasher.update(v.to_le_bytes());
102 }
103 hex::encode(&hasher.finalize()[..16])
104 }
105}
106
107impl Debug for GeometricSignature {
108 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
109 write!(f, "GeometricSignature(hash={}, level={})",
110 &self.hash[0..8], self.level)
111 }
112}
113
114#[derive(Clone, Debug)]
122pub struct HyperbolicRegion {
123 center: HyperbolicPoint,
125 radius: FixedPoint,
127 validation_mask: FixedVector,
129 center_norm_sq: FixedPoint,
131 radius_ratio_sq: FixedPoint,
134}
135
136impl HyperbolicRegion {
137 pub fn new(center: HyperbolicPoint, radius: FixedPoint) -> Self {
139 let dimension = center.dimension();
140
141 let validation_mask = {
143 let one = FixedPoint::from_int(1);
144 let mut mask = FixedVector::new(dimension);
145 for i in 0..dimension {
146 let x = center.coords()[i];
147 mask[i] = x * (one + x.tanh());
148 }
149 mask
150 };
151
152 let center_norm_sq = center.coords().length_squared();
153 let radius_ratio_sq = {
154 let r = distance_to_ratio(radius);
155 r * r
156 };
157 Self {
158 center,
159 radius,
160 validation_mask,
161 center_norm_sq,
162 radius_ratio_sq,
163 }
164 }
165
166 pub fn contains(&self, point: &HyperbolicPoint, _poincare_disk: &PoincareDisk) -> bool {
175 let point_norm_sq = point.coords().length_squared();
176 let s = hyperbolic_ratio_sq(&self.center, self.center_norm_sq, point, point_norm_sq);
177 s <= self.radius_ratio_sq
178 }
179
180 pub fn center(&self) -> &HyperbolicPoint {
182 &self.center
183 }
184
185 pub fn radius(&self) -> FixedPoint {
187 self.radius
188 }
189
190 pub fn validation_mask(&self) -> &FixedVector {
192 &self.validation_mask
193 }
194
195 pub fn quick_validate(&self, point: &HyperbolicPoint) -> bool {
198 let similarity = point.coords().dot(&self.validation_mask);
199 similarity > constants::epsilon()
200 }
201}
202
203#[derive(Clone, Debug)]
209pub struct BucketEntry {
210 pub unique_id: String,
212 pub point: HyperbolicPoint,
214 pub level: u32,
216 pub norm_sq: FixedPoint,
220}
221
222impl BucketEntry {
223 pub fn new(unique_id: String, point: HyperbolicPoint, level: u32) -> Self {
225 let norm_sq = point.coords().length_squared();
226 Self { unique_id, point, level, norm_sq }
227 }
228}
229
230fn cmp_fp(a: FixedPoint, b: FixedPoint) -> std::cmp::Ordering {
236 if a < b { std::cmp::Ordering::Less }
237 else if a > b { std::cmp::Ordering::Greater }
238 else { std::cmp::Ordering::Equal }
239}
240
241fn euclidean_distance_sq(a: &HyperbolicPoint, b: &HyperbolicPoint) -> FixedPoint {
244 let mut sum = FixedPoint::from_int(0);
250 let d = a.dimension().min(b.dimension());
251 for i in 0..d {
252 let diff = a.coords()[i] - b.coords()[i];
253 sum = sum + diff * diff;
254 }
255 sum
256}
257
258const VP_BUFFER_THRESHOLD: usize = 32;
260
261const VP_REBUILD_DIVISOR: usize = 16;
265
266
267const VP_DELETE_THRESHOLD: usize = 32;
269
270#[derive(Clone, Debug)]
272struct VPNode {
273 entry: BucketEntry,
275 median: FixedPoint,
277 left: Option<Box<VPNode>>,
279 right: Option<Box<VPNode>>,
281}
282
283#[derive(Clone, Debug)]
293pub struct VPTree {
294 root: Option<Box<VPNode>>,
296 buffer: Vec<BucketEntry>,
298 deleted: HashSet<String>,
300 tree_size: usize,
302}
303
304impl VPTree {
305 pub fn new() -> Self {
307 Self {
308 root: None,
309 buffer: Vec::new(),
310 deleted: HashSet::new(),
311 tree_size: 0,
312 }
313 }
314
315 pub fn insert(&mut self, entry: BucketEntry) {
317 if self.buffer.iter().any(|e| e.unique_id == entry.unique_id) {
318 return;
319 }
320 self.deleted.remove(&entry.unique_id);
322
323 self.buffer.push(entry);
324 let tree_len = self.tree_size;
331 let threshold = VP_BUFFER_THRESHOLD.max(tree_len / VP_REBUILD_DIVISOR);
332 if self.buffer.len() >= threshold {
333 self.rebuild();
334 }
335 }
336
337 pub fn remove(&mut self, unique_id: &str) {
339 let before = self.buffer.len();
341 self.buffer.retain(|e| e.unique_id != unique_id);
342 if self.buffer.len() < before {
343 return;
344 }
345
346 self.deleted.insert(unique_id.to_string());
348 if self.deleted.len() >= VP_DELETE_THRESHOLD {
349 self.rebuild();
350 }
351 }
352
353 pub fn live_count(&self) -> usize {
355 let tree_live = self.tree_size.saturating_sub(self.deleted.len());
356 tree_live + self.buffer.len()
357 }
358
359 pub fn is_empty(&self) -> bool {
361 self.live_count() == 0
362 }
363
364 pub fn find_in_radius(&self, center: &HyperbolicPoint, radius: FixedPoint) -> Vec<(String, FixedPoint)> {
366 let mut results = Vec::new();
367
368 let radius_sq = {
374 let r = distance_to_ratio(radius);
375 r * r
376 };
377 let center_norm_sq = center.coords().length_squared();
378 if let Some(ref root) = self.root {
379 Self::search_radius(root, center, center_norm_sq, radius_sq, &self.deleted, &mut results);
380 }
381
382 for entry in &self.buffer {
383 let s = hyperbolic_ratio_sq(center, center_norm_sq, &entry.point, entry.norm_sq);
384 if s <= radius_sq {
385 results.push((entry.unique_id.clone(), center.hyperbolic_distance(&entry.point)));
386 }
387 }
388
389 results
390 }
391
392 pub fn find_nearest(&self, point: &HyperbolicPoint, k: usize) -> Vec<(String, FixedPoint)> {
395 if k == 0 { return Vec::new(); }
396
397 let query_norm_sq = point.coords().length_squared();
402 let mut candidates: Vec<(FixedPoint, &BucketEntry)> = Vec::with_capacity(k + 1);
403 let mut tau = FixedPoint::from_int(1);
407
408 if let Some(ref root) = self.root {
410 Self::search_knn(root, point, query_norm_sq, k, &self.deleted, &mut candidates, &mut tau);
411 }
412
413 for entry in &self.buffer {
416 let s = hyperbolic_ratio_sq(point, query_norm_sq, &entry.point, entry.norm_sq);
417 if candidates.len() < k || s < tau {
418 candidates.push((s, entry));
419 candidates.sort_by(|a, b| cmp_fp(a.0, b.0).then_with(|| a.1.unique_id.cmp(&b.1.unique_id)));
420 if candidates.len() > k {
421 candidates.truncate(k);
422 }
423 if candidates.len() == k {
424 tau = candidates.last().unwrap().0;
425 }
426 }
427 }
428
429 candidates
432 .into_iter()
433 .map(|(_, e)| (e.unique_id.clone(), point.hyperbolic_distance(&e.point)))
434 .collect()
435 }
436
437 fn rebuild(&mut self) {
441 let mut entries = Vec::with_capacity(self.tree_size + self.buffer.len());
442
443 if let Some(root) = self.root.take() {
445 Self::collect_live(*root, &self.deleted, &mut entries);
446 }
447
448 entries.append(&mut self.buffer);
450
451 self.deleted.clear();
452 self.tree_size = entries.len();
453 self.root = Self::build_tree(entries);
454 }
455
456 fn collect_live(node: VPNode, deleted: &HashSet<String>, out: &mut Vec<BucketEntry>) {
458 if !deleted.contains(&node.entry.unique_id) {
459 out.push(node.entry);
460 }
461 if let Some(left) = node.left {
462 Self::collect_live(*left, deleted, out);
463 }
464 if let Some(right) = node.right {
465 Self::collect_live(*right, deleted, out);
466 }
467 }
468
469 pub fn farthest_from(&self, center: &HyperbolicPoint) -> Option<(String, FixedPoint)> {
477 let center_norm_sq = center.coords().length_squared();
483 let mut best: Option<(FixedPoint, String, HyperbolicPoint)> = None;
484 let mut consider = |entry: &BucketEntry| {
485 let s = hyperbolic_ratio_sq(center, center_norm_sq, &entry.point, entry.norm_sq);
486 if best.as_ref().is_none_or(|(m, _, _)| s > *m) {
487 best = Some((s, entry.unique_id.clone(), entry.point.clone()));
488 }
489 };
490 for entry in &self.buffer {
491 consider(entry);
492 }
493 if let Some(ref root) = self.root {
494 Self::visit_live(root, &self.deleted, &mut consider);
495 }
496 best.map(|(_, id, pt)| (id, center.hyperbolic_distance(&pt)))
499 }
500
501 fn visit_live<F: FnMut(&BucketEntry)>(node: &VPNode, deleted: &HashSet<String>, f: &mut F) {
503 if !deleted.contains(&node.entry.unique_id) {
504 f(&node.entry);
505 }
506 if let Some(ref left) = node.left {
507 Self::visit_live(left, deleted, f);
508 }
509 if let Some(ref right) = node.right {
510 Self::visit_live(right, deleted, f);
511 }
512 }
513
514 fn build_tree(mut entries: Vec<BucketEntry>) -> Option<Box<VPNode>> {
523 if entries.is_empty() {
524 return None;
525 }
526
527 if entries.len() == 1 {
528 return Some(Box::new(VPNode {
529 entry: entries.remove(0),
530 median: FixedPoint::from_int(0),
531 left: None,
532 right: None,
533 }));
534 }
535
536 let vp = entries.swap_remove(0);
538
539 let mut with_dists: Vec<(BucketEntry, FixedPoint)> = entries
547 .into_iter()
548 .map(|e| {
549 let s = hyperbolic_ratio_sq(&vp.point, vp.norm_sq, &e.point, e.norm_sq);
550 (e, s)
551 })
552 .collect();
553
554 with_dists.sort_by(|a, b| cmp_fp(a.1, b.1));
557
558 let median = with_dists[with_dists.len() / 2].1;
561
562 let (left_vec, right_vec): (Vec<_>, Vec<_>) = with_dists
564 .into_iter()
565 .partition(|(_, d)| *d < median);
566
567 let left = Self::build_tree(left_vec.into_iter().map(|(e, _)| e).collect());
568 let right = Self::build_tree(right_vec.into_iter().map(|(e, _)| e).collect());
569
570 Some(Box::new(VPNode {
571 entry: vp,
572 median,
573 left,
574 right,
575 }))
576 }
577
578 fn search_radius(
586 node: &VPNode,
587 center: &HyperbolicPoint,
588 center_norm_sq: FixedPoint,
589 radius_sq: FixedPoint,
590 deleted: &HashSet<String>,
591 results: &mut Vec<(String, FixedPoint)>,
592 ) {
593 let s = hyperbolic_ratio_sq(center, center_norm_sq, &node.entry.point, node.entry.norm_sq);
594
595 if s <= radius_sq && !deleted.contains(&node.entry.unique_id) {
596 results.push((
597 node.entry.unique_id.clone(),
598 center.hyperbolic_distance(&node.entry.point),
599 ));
600 }
601
602 if let Some(ref left) = node.left {
604 let prune = s > node.median && sq_ratio_separation_exceeds(s, node.median, radius_sq);
605 if !prune {
606 Self::search_radius(left, center, center_norm_sq, radius_sq, deleted, results);
607 }
608 }
609
610 if let Some(ref right) = node.right {
612 let prune = node.median > s && sq_ratio_separation_exceeds(node.median, s, radius_sq);
613 if !prune {
614 Self::search_radius(right, center, center_norm_sq, radius_sq, deleted, results);
615 }
616 }
617 }
618
619 #[allow(clippy::too_many_arguments)]
629 fn search_knn<'a>(
630 node: &'a VPNode,
631 center: &HyperbolicPoint,
632 center_norm_sq: FixedPoint,
633 k: usize,
634 deleted: &HashSet<String>,
635 candidates: &mut Vec<(FixedPoint, &'a BucketEntry)>,
636 tau: &mut FixedPoint,
637 ) {
638 let s = hyperbolic_ratio_sq(center, center_norm_sq, &node.entry.point, node.entry.norm_sq);
639
640 if !deleted.contains(&node.entry.unique_id) {
642 if candidates.len() < k || s < *tau {
643 candidates.push((s, &node.entry));
644 candidates.sort_by(|a, b| cmp_fp(a.0, b.0).then_with(|| a.1.unique_id.cmp(&b.1.unique_id)));
645 if candidates.len() > k {
646 candidates.truncate(k);
647 }
648 if candidates.len() == k {
649 *tau = candidates.last().unwrap().0;
650 }
651 }
652 }
653
654 let search_left_first = s < node.median;
656
657 let prune_left = |s: FixedPoint, tau: FixedPoint| {
658 s > node.median && sq_ratio_separation_exceeds(s, node.median, tau)
659 };
660 let prune_right = |s: FixedPoint, tau: FixedPoint| {
661 node.median > s && sq_ratio_separation_exceeds(node.median, s, tau)
662 };
663
664 if search_left_first {
665 if let Some(ref left) = node.left {
666 if !prune_left(s, *tau) {
667 Self::search_knn(left, center, center_norm_sq, k, deleted, candidates, tau);
668 }
669 }
670 if let Some(ref right) = node.right {
671 if !prune_right(s, *tau) {
672 Self::search_knn(right, center, center_norm_sq, k, deleted, candidates, tau);
673 }
674 }
675 } else {
676 if let Some(ref right) = node.right {
677 if !prune_right(s, *tau) {
678 Self::search_knn(right, center, center_norm_sq, k, deleted, candidates, tau);
679 }
680 }
681 if let Some(ref left) = node.left {
682 if !prune_left(s, *tau) {
683 Self::search_knn(left, center, center_norm_sq, k, deleted, candidates, tau);
684 }
685 }
686 }
687 }
688}
689
690#[derive(Debug)]
699pub struct HyperbolicHashBucket {
700 region: HyperbolicRegion,
702 position_signature: Vec<i32>,
704 _metrics: Vec<FixedPoint>,
706 vp_tree: Mutex<VPTree>,
708 eff: Mutex<EffRadius>,
713}
714
715#[derive(Clone, Debug)]
724struct EffRadius {
725 nominal: FixedPoint,
727 current: FixedPoint,
729 max_uid: Option<String>,
734}
735
736impl EffRadius {
737 fn new(nominal: FixedPoint) -> Self {
738 Self { nominal, current: nominal, max_uid: None }
739 }
740}
741
742impl Clone for HyperbolicHashBucket {
743 fn clone(&self) -> Self {
744 let vp_tree = self.vp_tree.lock().unwrap_or_else(|e| e.into_inner()).clone();
748 let eff = self.eff.lock().unwrap_or_else(|e| e.into_inner()).clone();
749 Self {
750 region: self.region.clone(),
751 position_signature: self.position_signature.clone(),
752 _metrics: self._metrics.clone(),
753 vp_tree: Mutex::new(vp_tree),
754 eff: Mutex::new(eff),
755 }
756 }
757}
758
759impl HyperbolicHashBucket {
760 pub fn new(region: HyperbolicRegion, position_signature: Vec<i32>) -> Self {
762 let mut metrics = Vec::new();
763
764 let center = region.center();
765 metrics.push(center.euclidean_norm());
766
767 let sum_squares = center.coords().iter().enumerate().fold(
768 FixedPoint::from_int(0),
769 |acc, (_i, &x)| acc + x * x
770 );
771 metrics.push(sum_squares);
772
773 let nominal_radius = region.radius();
774 Self {
775 region,
776 position_signature,
777 _metrics: metrics,
778 vp_tree: Mutex::new(VPTree::new()),
779 eff: Mutex::new(EffRadius::new(nominal_radius)),
780 }
781 }
782
783 pub fn effective_radius(&self) -> FixedPoint {
786 self.eff.lock().unwrap_or_else(|e| e.into_inner()).current
787 }
788
789 fn note_node_distance(&self, unique_id: &str, center_dist: FixedPoint) {
793 let mut e = self.eff.lock().unwrap_or_else(|e| e.into_inner());
794 if center_dist > e.current {
795 e.current = center_dist;
796 e.max_uid = Some(unique_id.to_string());
797 }
798 }
799
800 fn forget_node(&self, unique_id: &str) {
811 let mut e = self.eff.lock().unwrap_or_else(|e| e.into_inner());
812 if e.max_uid.as_deref() != Some(unique_id) {
813 return;
814 }
815 let tree = self.vp_tree.lock().unwrap_or_else(|e| e.into_inner());
816 match tree.farthest_from(self.region.center()) {
817 Some((uid, dist)) if dist > e.nominal => {
818 e.current = dist;
819 e.max_uid = Some(uid);
820 }
821 _ => {
822 e.current = e.nominal;
823 e.max_uid = None;
824 }
825 }
826 }
827
828 pub fn contains(&self, point: &HyperbolicPoint, poincare_disk: &PoincareDisk) -> bool {
830 self.region.contains(point, poincare_disk)
831 }
832
833 pub fn region(&self) -> &HyperbolicRegion {
835 &self.region
836 }
837
838 pub fn position_signature(&self) -> &[i32] {
840 &self.position_signature
841 }
842
843 pub fn quick_validate(&self, point: &HyperbolicPoint) -> bool {
845 self.region.quick_validate(point)
846 }
847}
848
849#[derive(Clone)]
860pub struct HyperbolicHashTable {
861 poincare_disk: PoincareDisk,
863 buckets: HashMap<String, HyperbolicHashBucket>,
865 signature_map: HashMap<Vec<i32>, String>,
867 node_to_bucket: DashMap<String, String>,
869}
870
871impl HyperbolicHashTable {
872 pub fn new(dimension: usize) -> Self {
874 let poincare_disk = PoincareDisk::new(dimension);
875
876 let mut table = Self {
877 poincare_disk,
878 buckets: HashMap::new(),
879 signature_map: HashMap::new(),
880 node_to_bucket: DashMap::new(),
881 };
882
883 table.initialize_buckets();
884 table
885 }
886
887 fn initialize_buckets(&mut self) {
889 let dimension = self.poincare_disk.dimension();
890
891 let distances = [
893 FixedPoint::from_int(0), constants::half(), FixedPoint::from_int(1), FixedPoint::from_int(3) / FixedPoint::from_int(2), FixedPoint::from_int(2), ];
899
900 let directions_per_distance = [
901 1, dimension * 2, dimension * 3, dimension * 4, dimension * 5, ];
907
908 for (dist_idx, &distance) in distances.iter().enumerate() {
909 let num_directions = directions_per_distance[dist_idx];
910
911 if dist_idx == 0 {
913 let origin = self.poincare_disk.origin();
914 let region = HyperbolicRegion::new(origin.clone(), constants::region_radius());
915
916 let position_signature = vec![0; dimension];
917
918 let bucket = HyperbolicHashBucket::new(region, position_signature.clone());
919 let signature = self.compute_geometric_signature(&origin);
920 let hash = self.compute_stable_hash(&signature);
921
922 self.buckets.insert(hash.clone(), bucket);
923 self.signature_map.insert(position_signature, hash);
924
925 continue;
926 }
927
928 for dir_idx in 0..num_directions {
929 let direction = self.generate_direction_vector(dir_idx, num_directions);
930
931 let center = self.poincare_disk.point_at_distance_from_origin(
932 &direction, distance
933 );
934
935 let one_fifth = FixedPoint::from_int(1) / FixedPoint::from_int(5);
937 let one_tenth = FixedPoint::from_int(1) / FixedPoint::from_int(10);
938 let radius = one_fifth + one_tenth * distance;
939 let region = HyperbolicRegion::new(center.clone(), radius);
940
941 let position_signature = self.generate_position_signature(¢er);
942
943 let bucket = HyperbolicHashBucket::new(region, position_signature.clone());
944 let signature = self.compute_geometric_signature(¢er);
945 let hash = self.compute_stable_hash(&signature);
946
947 self.buckets.insert(hash.clone(), bucket);
948 self.signature_map.insert(position_signature, hash);
949 }
950 }
951 }
952
953 fn generate_direction_vector(&self, index: usize, total: usize) -> FixedVector {
955 let dimension = self.poincare_disk.dimension();
956 let mut direction = FixedVector::new(dimension);
957
958 if dimension == 2 {
960 let angle = constants::two_pi()
961 * FixedPoint::from_int(index as i32)
962 / FixedPoint::from_int(total as i32);
963 let (sin_a, cos_a) = angle.sincos();
964 direction[0] = cos_a;
965 direction[1] = sin_a;
966 return direction;
967 }
968
969 let phi = constants::golden_angle();
971
972 let idx = FixedPoint::from_int((index + 1) as i32);
974 for i in 0..dimension {
975 let phase = idx * phi * FixedPoint::from_int((i + 1) as i32);
976 direction[i] = phase.sin();
977 }
978
979 let norm_sq = direction.dot(&direction);
980 if norm_sq > constants::epsilon() {
981 direction.normalize();
982 } else {
983 direction[0] = FixedPoint::from_int(1);
985 }
986
987 direction
988 }
989
990 fn generate_position_signature(&self, point: &HyperbolicPoint) -> Vec<i32> {
992 let dimension = self.poincare_disk.dimension();
993 let mut signature = Vec::with_capacity(dimension);
994
995 for i in 0..dimension {
996 signature.push(constants::quantize_position(point.coords()[i]));
997 }
998
999 signature
1000 }
1001
1002 fn compute_geometric_signature(&self, point: &HyperbolicPoint) -> Vec<i32> {
1006 let dimension = self.poincare_disk.dimension();
1007 let mut signature = Vec::with_capacity(dimension);
1008 let one = FixedPoint::from_int(1);
1009
1010 for i in 0..dimension {
1011 let x = point.coords()[i];
1012 let transformed = x * (one + x.tanh());
1013 signature.push(constants::quantize_1000(transformed));
1014 }
1015
1016 signature
1017 }
1018
1019 fn compute_stable_hash(&self, signature: &[i32]) -> String {
1021 let mut hasher = Sha3_512::new();
1022
1023 for &value in signature {
1024 hasher.update(value.to_le_bytes());
1025 }
1026
1027 let hash = hasher.finalize();
1028 hex::encode(&hash[..16])
1029 }
1030
1031 pub fn find_bucket(&self, point: &HyperbolicPoint) -> Option<String> {
1041 let position_signature = self.generate_position_signature(point);
1043 if let Some(hash) = self.signature_map.get(&position_signature) {
1044 return Some(hash.clone());
1045 }
1046
1047 let mut candidates: Vec<(&String, FixedPoint)> = self.buckets.iter()
1052 .map(|(hash, bucket)| {
1053 (hash, euclidean_distance_sq(point, bucket.region().center()))
1054 })
1055 .collect();
1056 candidates.sort_unstable_by(|a, b| cmp_fp(a.1, b.1).then_with(|| a.0.cmp(b.0)));
1059
1060 for (hash, _) in &candidates {
1061 if let Some(bucket) = self.buckets.get(*hash) {
1062 if bucket.contains(point, &self.poincare_disk) {
1063 return Some((*hash).clone());
1064 }
1065 }
1066 }
1067
1068 for (hash, _) in &candidates {
1072 if let Some(bucket) = self.buckets.get(*hash) {
1073 if bucket.quick_validate(point) {
1074 return Some((*hash).clone());
1075 }
1076 }
1077 }
1078
1079 None
1080 }
1081
1082 pub fn create_signature(&self, point: &HyperbolicPoint, level: u32) -> Option<GeometricSignature> {
1089 let position_signature = self.generate_position_signature(point);
1091
1092 let hash = if let Some(bucket_hash) = self.find_bucket(point) {
1094 bucket_hash
1095 } else {
1096 let geo_sig = self.compute_geometric_signature(point);
1098 self.compute_stable_hash(&geo_sig)
1099 };
1100
1101 Some(GeometricSignature::new(hash, level, position_signature))
1102 }
1103
1104 pub fn validate_point(&self, point: &HyperbolicPoint) -> bool {
1106 let norm = point.euclidean_norm();
1107 if norm >= FixedPoint::from_int(1) {
1108 return false;
1109 }
1110
1111 self.find_bucket(point).is_some()
1112 }
1113
1114 pub fn poincare_disk(&self) -> &PoincareDisk {
1116 &self.poincare_disk
1117 }
1118
1119 pub fn bucket_count(&self) -> usize {
1121 self.buckets.len()
1122 }
1123
1124 pub fn register_node(&self, point: &HyperbolicPoint, unique_id: &str, level: u32) -> Option<String> {
1127 self.register_node_with_hint(point, unique_id, level, None)
1128 }
1129
1130 pub fn register_node_with_hint(&self, point: &HyperbolicPoint, unique_id: &str, level: u32, bucket_hint: Option<&str>) -> Option<String> {
1136 if self.node_to_bucket.contains_key(unique_id) {
1138 return self.node_to_bucket.get(unique_id).map(|r| r.value().clone());
1139 }
1140
1141 let bucket_hash = match bucket_hint {
1143 Some(hint) if self.buckets.contains_key(hint) => hint.to_string(),
1144 _ => self.find_bucket(point)?,
1145 };
1146
1147 if let Some(bucket) = self.buckets.get(&bucket_hash) {
1148 let center_dist = self.poincare_disk.distance(point, bucket.region.center());
1152 bucket.note_node_distance(unique_id, center_dist);
1153 bucket.vp_tree.lock().unwrap_or_else(|e| e.into_inner()).insert(BucketEntry::new(unique_id.to_string(), point.clone(), level));
1154 }
1155 self.node_to_bucket.insert(unique_id.to_string(), bucket_hash.clone());
1156 Some(bucket_hash)
1157 }
1158
1159 pub fn unregister_node(&self, unique_id: &str) {
1162 if let Some((_, bucket_hash)) = self.node_to_bucket.remove(unique_id) {
1163 if let Some(bucket) = self.buckets.get(&bucket_hash) {
1164 bucket.vp_tree.lock().unwrap_or_else(|e| e.into_inner()).remove(unique_id);
1168 bucket.forget_node(unique_id);
1169 }
1170 }
1171 }
1172
1173 pub fn find_nodes_in_radius(&self, center: &HyperbolicPoint, radius: FixedPoint) -> Vec<(String, FixedPoint)> {
1178 let mut results = Vec::new();
1179
1180 for bucket in self.buckets.values() {
1181 let bucket_center_dist = self.poincare_disk.distance(
1185 center, bucket.region.center()
1186 );
1187 if bucket_center_dist > radius + bucket.effective_radius() {
1188 continue;
1189 }
1190
1191 let bucket_results = bucket.vp_tree.lock().unwrap_or_else(|e| e.into_inner()).find_in_radius(center, radius);
1193 results.extend(bucket_results);
1194 }
1195
1196 results
1197 }
1198
1199 pub fn find_nearest_nodes(&self, point: &HyperbolicPoint, k: usize) -> Vec<(String, FixedPoint)> {
1206 if k == 0 { return Vec::new(); }
1207
1208 let zero = FixedPoint::from_int(0);
1215 let mut bucket_dists: Vec<(&String, FixedPoint)> = self.buckets.iter()
1216 .map(|(hash, bucket)| {
1217 let d = self.poincare_disk.distance(point, bucket.region.center());
1218 let r = bucket.effective_radius();
1219 let min_possible = if d > r { d - r } else { zero };
1220 (hash, min_possible)
1221 })
1222 .collect();
1223 bucket_dists.sort_by(|a, b| cmp_fp(a.1, b.1).then_with(|| a.0.cmp(b.0)));
1224
1225 let mut candidates: Vec<(String, FixedPoint)> = Vec::new();
1226
1227 for (hash, min_possible) in &bucket_dists {
1228 if candidates.len() >= k {
1231 let kth_dist = candidates.last().unwrap().1;
1232 if *min_possible > kth_dist {
1233 break;
1234 }
1235 }
1236
1237 if let Some(bucket) = self.buckets.get(*hash) {
1238 let bucket_results = bucket.vp_tree.lock().unwrap_or_else(|e| e.into_inner()).find_nearest(point, k);
1240
1241 for result in bucket_results {
1243 candidates.push(result);
1244 }
1245
1246 candidates.sort_by(|a, b| cmp_fp(a.1, b.1));
1248 candidates.truncate(k);
1249 }
1250 }
1251
1252 candidates
1253 }
1254
1255 pub fn verify_integrity(&self) -> bool {
1257 if self.buckets.is_empty() {
1258 return false;
1259 }
1260
1261 for (sig, hash) in &self.signature_map {
1262 if !self.buckets.contains_key(hash) {
1263 return false;
1264 }
1265
1266 let bucket = &self.buckets[hash];
1267 if bucket.position_signature() != sig.as_slice() {
1268 return false;
1269 }
1270 }
1271
1272 true
1273 }
1274}
1275
1276impl Debug for HyperbolicHashTable {
1277 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
1278 write!(f, "HyperbolicHashTable(dim={}, buckets={}, nodes={})",
1279 self.poincare_disk.dimension(), self.buckets.len(),
1280 self.node_to_bucket.len())
1281 }
1282}
1283
1284#[cfg(test)]
1285mod tests {
1286 use super::*;
1287
1288 #[test]
1289 fn test_hash_table_creation() {
1290 let table = HyperbolicHashTable::new(2);
1291 assert_eq!(table.poincare_disk().dimension(), 2);
1292 assert!(table.bucket_count() > 0);
1293 }
1294
1295 #[test]
1296 fn test_geometric_signature() {
1297 let table = HyperbolicHashTable::new(2);
1298 let point = table.poincare_disk().point_from_f32_slice(&[0.5, 0.0]);
1299
1300 let signature = table.create_signature(&point, 0).unwrap();
1301 assert_eq!(signature.level(), 0);
1302 assert!(!signature.hash().is_empty());
1303 assert!(!signature.position_signature().is_empty());
1304 }
1305
1306 #[test]
1307 fn test_bucket_finding() {
1308 let table = HyperbolicHashTable::new(2);
1309 let origin = table.poincare_disk().origin();
1310
1311 let bucket_hash = table.find_bucket(&origin);
1312 assert!(bucket_hash.is_some());
1313 }
1314
1315 #[test]
1316 fn test_point_validation() {
1317 let table = HyperbolicHashTable::new(2);
1318
1319 let valid_point = table.poincare_disk().point_from_f32_slice(&[0.5, 0.0]);
1320 assert!(table.validate_point(&valid_point));
1321
1322 let projected_point = table.poincare_disk().point_from_f32_slice(&[1.5, 0.0]);
1323 assert!(table.validate_point(&projected_point));
1324 }
1325
1326 #[test]
1327 fn test_hyperbolic_region() {
1328 let disk = PoincareDisk::new(2);
1329 let center = disk.point_from_f32_slice(&[0.5, 0.0]);
1330 let radius = constants::half();
1331
1332 let region = HyperbolicRegion::new(center.clone(), radius);
1333
1334 assert!(region.contains(¢er, &disk));
1335 assert!(!region.contains(&disk.origin(), &disk));
1336
1337 let far_point = disk.point_from_f32_slice(&[0.8, 0.0]);
1338 assert!(!region.contains(&far_point, &disk));
1339 }
1340
1341 #[test]
1342 fn test_hash_bucket() {
1343 let disk = PoincareDisk::new(2);
1344 let center = disk.point_from_f32_slice(&[0.5, 0.0]);
1345 let radius = constants::half();
1346
1347 let region = HyperbolicRegion::new(center.clone(), radius);
1348 let position_signature = vec![500, 0];
1349
1350 let bucket = HyperbolicHashBucket::new(region, position_signature);
1351
1352 assert!(bucket.contains(¢er, &disk));
1353 assert!(bucket.quick_validate(¢er));
1354 }
1355
1356 #[test]
1357 fn test_integrity_verification() {
1358 let table = HyperbolicHashTable::new(2);
1359 assert!(table.verify_integrity());
1360 }
1361
1362 #[test]
1365 fn test_vp_tree_empty() {
1366 let vp = VPTree::new();
1367 assert!(vp.is_empty());
1368 assert_eq!(vp.live_count(), 0);
1369
1370 let origin = HyperbolicPoint::origin(2);
1371 let results = vp.find_in_radius(&origin, FixedPoint::from_int(10));
1372 assert!(results.is_empty());
1373
1374 let nearest = vp.find_nearest(&origin, 5);
1375 assert!(nearest.is_empty());
1376 }
1377
1378 #[test]
1379 fn test_vp_tree_insert_and_find() {
1380 let disk = PoincareDisk::new(2);
1381 let mut vp = VPTree::new();
1382
1383 let points: Vec<(&str, [f32; 2])> = vec![
1385 ("a", [0.1, 0.0]),
1386 ("b", [0.2, 0.0]),
1387 ("c", [0.3, 0.0]),
1388 ("d", [0.0, 0.1]),
1389 ("e", [0.0, 0.2]),
1390 ];
1391
1392 for (id, coords) in &points {
1393 vp.insert(BucketEntry::new(id.to_string(), disk.point_from_f32_slice(coords), 0));
1394 }
1395
1396 assert_eq!(vp.live_count(), 5);
1397
1398 let origin = disk.origin();
1400 let nearest = vp.find_nearest(&origin, 2);
1401 assert_eq!(nearest.len(), 2);
1402 assert!(nearest[0].1 <= nearest[1].1);
1404
1405 let all = vp.find_in_radius(&origin, FixedPoint::from_int(10));
1407 assert_eq!(all.len(), 5);
1408
1409 let tiny = vp.find_in_radius(&origin, FixedPoint::from_int(1) / FixedPoint::from_int(10000));
1411 assert!(tiny.len() <= 1);
1412 }
1413
1414 #[test]
1415 fn test_vp_tree_remove() {
1416 let disk = PoincareDisk::new(2);
1417 let mut vp = VPTree::new();
1418
1419 vp.insert(BucketEntry::new("x".to_string(), disk.point_from_f32_slice(&[0.1, 0.0]), 0));
1420 vp.insert(BucketEntry::new("y".to_string(), disk.point_from_f32_slice(&[0.2, 0.0]), 0));
1421
1422 assert_eq!(vp.live_count(), 2);
1423
1424 vp.remove("x");
1425 assert_eq!(vp.live_count(), 1);
1426
1427 let origin = disk.origin();
1429 let results = vp.find_in_radius(&origin, FixedPoint::from_int(10));
1430 assert_eq!(results.len(), 1);
1431 assert_eq!(results[0].0, "y");
1432 }
1433
1434 #[test]
1435 fn test_vp_tree_rebuild_on_buffer_threshold() {
1436 let disk = PoincareDisk::new(2);
1437 let mut vp = VPTree::new();
1438
1439 for i in 0..(VP_BUFFER_THRESHOLD + 5) {
1441 let angle = constants::two_pi()
1442 * FixedPoint::from_int(i as i32)
1443 / FixedPoint::from_int((VP_BUFFER_THRESHOLD + 5) as i32);
1444 let r = FixedPoint::from_int(3) / FixedPoint::from_int(10);
1445 let mut coords = FixedVector::new(2);
1446 let (sin_a, cos_a) = angle.sincos();
1447 coords[0] = r * cos_a;
1448 coords[1] = r * sin_a;
1449
1450 vp.insert(BucketEntry::new(format!("node_{}", i), HyperbolicPoint::new(coords), 0));
1451 }
1452
1453 assert!(vp.root.is_some());
1455 assert_eq!(vp.live_count(), VP_BUFFER_THRESHOLD + 5);
1456
1457 let origin = disk.origin();
1459 let all = vp.find_in_radius(&origin, FixedPoint::from_int(10));
1460 assert_eq!(all.len(), VP_BUFFER_THRESHOLD + 5);
1461 }
1462
1463 #[test]
1464 fn test_vp_tree_knn_ordering() {
1465 let disk = PoincareDisk::new(2);
1466 let mut vp = VPTree::new();
1467
1468 let distances = [0.05f32, 0.1, 0.2, 0.3, 0.5, 0.7];
1470 for (i, &d) in distances.iter().enumerate() {
1471 vp.insert(BucketEntry::new(format!("p{}", i), disk.point_from_f32_slice(&[d, 0.0]), 0));
1472 }
1473
1474 let origin = disk.origin();
1475 let nearest = vp.find_nearest(&origin, 3);
1476 assert_eq!(nearest.len(), 3);
1477
1478 for i in 1..nearest.len() {
1480 assert!(nearest[i].1 >= nearest[i - 1].1,
1481 "Results not sorted: {:?} >= {:?}", nearest[i].1, nearest[i - 1].1);
1482 }
1483
1484 let ids: Vec<&str> = nearest.iter().map(|(id, _)| id.as_str()).collect();
1486 assert!(ids.contains(&"p0"));
1487 assert!(ids.contains(&"p1"));
1488 assert!(ids.contains(&"p2"));
1489 }
1490
1491 #[test]
1492 fn test_register_unregister_with_vp_tree() {
1493 let table = HyperbolicHashTable::new(2);
1494 let disk_clone = table.poincare_disk().clone();
1495
1496 let p1 = disk_clone.point_from_f32_slice(&[0.1, 0.0]);
1497 let p2 = disk_clone.point_from_f32_slice(&[0.2, 0.0]);
1498 let p3 = disk_clone.point_from_f32_slice(&[0.3, 0.0]);
1499
1500 table.register_node(&p1, "node1", 0);
1501 table.register_node(&p2, "node2", 1);
1502 table.register_node(&p3, "node3", 1);
1503
1504 let origin = disk_clone.origin();
1506 let results = table.find_nodes_in_radius(&origin, FixedPoint::from_int(10));
1507 assert!(results.len() >= 3, "Expected at least 3, got {}", results.len());
1508
1509 table.unregister_node("node2");
1511
1512 let results = table.find_nodes_in_radius(&origin, FixedPoint::from_int(10));
1514 let ids: Vec<&str> = results.iter().map(|(id, _)| id.as_str()).collect();
1515 assert!(!ids.contains(&"node2"), "node2 should be unregistered");
1516 assert!(ids.contains(&"node1"));
1517 assert!(ids.contains(&"node3"));
1518 }
1519
1520 #[test]
1521 fn test_find_nearest_with_early_termination() {
1522 let table = HyperbolicHashTable::new(2);
1523 let disk_clone = table.poincare_disk().clone();
1524
1525 let positions: Vec<(&str, [f32; 2])> = vec![
1527 ("close1", [0.05, 0.0]),
1528 ("close2", [0.0, 0.05]),
1529 ("mid1", [0.3, 0.0]),
1530 ("mid2", [0.0, 0.3]),
1531 ("far1", [0.7, 0.0]),
1532 ("far2", [0.0, 0.7]),
1533 ];
1534
1535 for (id, coords) in &positions {
1536 let point = disk_clone.point_from_f32_slice(coords);
1537 table.register_node(&point, id, 0);
1538 }
1539
1540 let origin = disk_clone.origin();
1541 let nearest = table.find_nearest_nodes(&origin, 2);
1542 assert_eq!(nearest.len(), 2);
1543
1544 let ids: Vec<&str> = nearest.iter().map(|(id, _)| id.as_str()).collect();
1546 assert!(ids.contains(&"close1"));
1547 assert!(ids.contains(&"close2"));
1548
1549 assert!(nearest[0].1 <= nearest[1].1);
1551 }
1552
1553 #[test]
1554 fn test_duplicate_registration_prevented() {
1555 let table = HyperbolicHashTable::new(2);
1556 let point = table.poincare_disk().point_from_f32_slice(&[0.1, 0.0]);
1557
1558 let h1 = table.register_node(&point, "dup_node", 0);
1559 let h2 = table.register_node(&point, "dup_node", 0);
1560
1561 assert_eq!(h1, h2);
1563
1564 let origin = table.poincare_disk().origin();
1566 let results = table.find_nodes_in_radius(&origin, FixedPoint::from_int(10));
1567 let count = results.iter().filter(|(id, _)| id == "dup_node").count();
1568 assert_eq!(count, 1, "Duplicate registration should be prevented");
1569 }
1570
1571 #[test]
1572 fn effective_radius_returns_to_nominal_when_lone_outlier_removed() {
1573 let table = HyperbolicHashTable::new(2);
1578 let disk = table.poincare_disk().clone();
1579
1580 let deep = disk.point_from_f32_slice(&[0.95, 0.0]);
1581 let bucket_hash = table.register_node(&deep, "deep", 5).unwrap();
1582
1583 let nominal = table.buckets.get(&bucket_hash).unwrap().region().radius();
1584 let inflated = table.buckets.get(&bucket_hash).unwrap().effective_radius();
1585 assert!(
1586 inflated > nominal,
1587 "deep node should widen the bucket past nominal (inflated={:?}, nominal={:?})",
1588 inflated, nominal
1589 );
1590
1591 table.unregister_node("deep");
1592
1593 let after = table.buckets.get(&bucket_hash).unwrap().effective_radius();
1594 assert_eq!(
1595 after, nominal,
1596 "with the only out-of-region member gone, the bound must return to nominal"
1597 );
1598 }
1599
1600 #[test]
1601 fn effective_radius_falls_to_second_farthest_not_nominal() {
1602 let table = HyperbolicHashTable::new(2);
1607 let disk = table.poincare_disk().clone();
1608
1609 let near_deep = disk.point_from_f32_slice(&[0.85, 0.0]);
1611 let far_deep = disk.point_from_f32_slice(&[0.97, 0.0]);
1612
1613 let h_near = table.register_node(&near_deep, "near_deep", 4).unwrap();
1614 let h_far = table.register_node(&far_deep, "far_deep", 6).unwrap();
1615
1616 if h_near != h_far {
1619 return;
1620 }
1621
1622 let bucket = || table.buckets.get(&h_near).unwrap();
1623 let nominal = bucket().region().radius();
1624 let with_both = bucket().effective_radius();
1625
1626 let center = bucket().region().center().clone();
1629 let near_dist = center.hyperbolic_distance(&near_deep);
1630
1631 table.unregister_node("far_deep");
1632 let after = bucket().effective_radius();
1633
1634 assert!(after < with_both, "removing the farther node must shrink the bound");
1635 assert!(after > nominal, "the remaining out-of-region node must keep the bound above nominal");
1636 assert_eq!(after, near_dist, "the bound must equal the remaining node's center distance");
1637 }
1638}
1639