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};
25use crate::constants;
26
27#[derive(Clone, PartialEq, Eq, Hash)]
36pub struct GeometricSignature {
37 hash: String,
39 level: u32,
41 position_signature: Vec<i32>,
43}
44
45impl GeometricSignature {
46 pub fn new(hash: String, level: u32, position_signature: Vec<i32>) -> Self {
48 Self {
49 hash,
50 level,
51 position_signature,
52 }
53 }
54
55 pub fn hash(&self) -> &str {
57 &self.hash
58 }
59
60 pub fn level(&self) -> u32 {
62 self.level
63 }
64
65 pub fn position_signature(&self) -> &[i32] {
67 &self.position_signature
68 }
69
70 pub fn stub(unique_id: &str) -> Self {
72 Self {
73 hash: unique_id.to_string(),
74 level: 0,
75 position_signature: Vec::new(),
76 }
77 }
78
79 pub fn is_stub(&self) -> bool {
82 self.position_signature.is_empty()
83 }
84
85 pub fn unique_id(&self) -> String {
91 if self.position_signature.is_empty() {
92 return self.hash.clone();
94 }
95 use sha3::{Sha3_256, Digest as _};
96 let mut hasher = Sha3_256::new();
97 hasher.update(self.hash.as_bytes());
98 hasher.update(self.level.to_le_bytes());
99 for &v in &self.position_signature {
100 hasher.update(v.to_le_bytes());
101 }
102 hex::encode(&hasher.finalize()[..16])
103 }
104}
105
106impl Debug for GeometricSignature {
107 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
108 write!(f, "GeometricSignature(hash={}, level={})",
109 &self.hash[0..8], self.level)
110 }
111}
112
113#[derive(Clone, Debug)]
121pub struct HyperbolicRegion {
122 center: HyperbolicPoint,
124 radius: FixedPoint,
126 validation_mask: FixedVector,
128}
129
130impl HyperbolicRegion {
131 pub fn new(center: HyperbolicPoint, radius: FixedPoint) -> Self {
133 let dimension = center.dimension();
134
135 let validation_mask = {
137 let one = FixedPoint::from_int(1);
138 let mut mask = FixedVector::new(dimension);
139 for i in 0..dimension {
140 let x = center.coords()[i];
141 mask[i] = x * (one + x.tanh());
142 }
143 mask
144 };
145
146 Self {
147 center,
148 radius,
149 validation_mask,
150 }
151 }
152
153 pub fn contains(&self, point: &HyperbolicPoint, poincare_disk: &PoincareDisk) -> bool {
155 let distance = poincare_disk.distance(&self.center, point);
156 distance <= self.radius
157 }
158
159 pub fn center(&self) -> &HyperbolicPoint {
161 &self.center
162 }
163
164 pub fn radius(&self) -> FixedPoint {
166 self.radius
167 }
168
169 pub fn validation_mask(&self) -> &FixedVector {
171 &self.validation_mask
172 }
173
174 pub fn quick_validate(&self, point: &HyperbolicPoint) -> bool {
177 let similarity = point.coords().dot(&self.validation_mask);
178 similarity > constants::epsilon()
179 }
180}
181
182#[derive(Clone, Debug)]
188pub struct BucketEntry {
189 pub unique_id: String,
191 pub point: HyperbolicPoint,
193 pub level: u32,
195}
196
197fn cmp_fp(a: FixedPoint, b: FixedPoint) -> std::cmp::Ordering {
203 if a < b { std::cmp::Ordering::Less }
204 else if a > b { std::cmp::Ordering::Greater }
205 else { std::cmp::Ordering::Equal }
206}
207
208fn euclidean_distance_sq(a: &HyperbolicPoint, b: &HyperbolicPoint) -> FixedPoint {
211 let mut sum = FixedPoint::from_int(0);
217 let d = a.dimension().min(b.dimension());
218 for i in 0..d {
219 let diff = a.coords()[i] - b.coords()[i];
220 sum = sum + diff * diff;
221 }
222 sum
223}
224
225const VP_BUFFER_THRESHOLD: usize = 32;
227
228const VP_DELETE_THRESHOLD: usize = 32;
230
231#[derive(Clone, Debug)]
233struct VPNode {
234 entry: BucketEntry,
236 median: FixedPoint,
238 left: Option<Box<VPNode>>,
240 right: Option<Box<VPNode>>,
242}
243
244#[derive(Clone, Debug)]
254pub struct VPTree {
255 root: Option<Box<VPNode>>,
257 buffer: Vec<BucketEntry>,
259 deleted: HashSet<String>,
261 tree_size: usize,
263}
264
265impl VPTree {
266 pub fn new() -> Self {
268 Self {
269 root: None,
270 buffer: Vec::new(),
271 deleted: HashSet::new(),
272 tree_size: 0,
273 }
274 }
275
276 pub fn insert(&mut self, entry: BucketEntry) {
278 if self.buffer.iter().any(|e| e.unique_id == entry.unique_id) {
279 return;
280 }
281 self.deleted.remove(&entry.unique_id);
283
284 self.buffer.push(entry);
285 if self.buffer.len() >= VP_BUFFER_THRESHOLD {
286 self.rebuild();
287 }
288 }
289
290 pub fn remove(&mut self, unique_id: &str) {
292 let before = self.buffer.len();
294 self.buffer.retain(|e| e.unique_id != unique_id);
295 if self.buffer.len() < before {
296 return;
297 }
298
299 self.deleted.insert(unique_id.to_string());
301 if self.deleted.len() >= VP_DELETE_THRESHOLD {
302 self.rebuild();
303 }
304 }
305
306 pub fn live_count(&self) -> usize {
308 let tree_live = self.tree_size.saturating_sub(self.deleted.len());
309 tree_live + self.buffer.len()
310 }
311
312 pub fn is_empty(&self) -> bool {
314 self.live_count() == 0
315 }
316
317 pub fn find_in_radius(&self, center: &HyperbolicPoint, radius: FixedPoint) -> Vec<(String, FixedPoint)> {
319 let mut results = Vec::new();
320
321 if let Some(ref root) = self.root {
323 Self::search_radius(root, center, radius, &self.deleted, &mut results);
324 }
325
326 for entry in &self.buffer {
328 let dist = center.hyperbolic_distance(&entry.point);
329 if dist <= radius {
330 results.push((entry.unique_id.clone(), dist));
331 }
332 }
333
334 results
335 }
336
337 pub fn find_nearest(&self, point: &HyperbolicPoint, k: usize) -> Vec<(String, FixedPoint)> {
340 if k == 0 { return Vec::new(); }
341
342 let mut candidates: Vec<(String, FixedPoint)> = Vec::with_capacity(k + 1);
343 let mut tau = FixedPoint::from_int(200);
349
350 if let Some(ref root) = self.root {
352 Self::search_knn(root, point, k, &self.deleted, &mut candidates, &mut tau);
353 }
354
355 for entry in &self.buffer {
357 let dist = point.hyperbolic_distance(&entry.point);
358 if candidates.len() < k || dist < tau {
359 candidates.push((entry.unique_id.clone(), dist));
360 candidates.sort_by(|a, b| cmp_fp(a.1, b.1));
361 if candidates.len() > k {
362 candidates.truncate(k);
363 }
364 if candidates.len() == k {
365 tau = candidates.last().unwrap().1;
366 }
367 }
368 }
369
370 candidates
371 }
372
373 fn rebuild(&mut self) {
377 let mut entries = Vec::with_capacity(self.tree_size + self.buffer.len());
378
379 if let Some(root) = self.root.take() {
381 Self::collect_live(*root, &self.deleted, &mut entries);
382 }
383
384 entries.append(&mut self.buffer);
386
387 self.deleted.clear();
388 self.tree_size = entries.len();
389 self.root = Self::build_tree(entries);
390 }
391
392 fn collect_live(node: VPNode, deleted: &HashSet<String>, out: &mut Vec<BucketEntry>) {
394 if !deleted.contains(&node.entry.unique_id) {
395 out.push(node.entry);
396 }
397 if let Some(left) = node.left {
398 Self::collect_live(*left, deleted, out);
399 }
400 if let Some(right) = node.right {
401 Self::collect_live(*right, deleted, out);
402 }
403 }
404
405 pub fn farthest_from(&self, center: &HyperbolicPoint) -> Option<(String, FixedPoint)> {
413 let mut best: Option<(String, FixedPoint)> = None;
414 let mut consider = |entry: &BucketEntry| {
415 let dist = center.hyperbolic_distance(&entry.point);
416 if best.as_ref().is_none_or(|(_, m)| dist > *m) {
417 best = Some((entry.unique_id.clone(), dist));
418 }
419 };
420 for entry in &self.buffer {
421 consider(entry);
422 }
423 if let Some(ref root) = self.root {
424 Self::visit_live(root, &self.deleted, &mut consider);
425 }
426 best
427 }
428
429 fn visit_live<F: FnMut(&BucketEntry)>(node: &VPNode, deleted: &HashSet<String>, f: &mut F) {
431 if !deleted.contains(&node.entry.unique_id) {
432 f(&node.entry);
433 }
434 if let Some(ref left) = node.left {
435 Self::visit_live(left, deleted, f);
436 }
437 if let Some(ref right) = node.right {
438 Self::visit_live(right, deleted, f);
439 }
440 }
441
442 fn build_tree(mut entries: Vec<BucketEntry>) -> Option<Box<VPNode>> {
451 if entries.is_empty() {
452 return None;
453 }
454
455 if entries.len() == 1 {
456 return Some(Box::new(VPNode {
457 entry: entries.remove(0),
458 median: FixedPoint::from_int(0),
459 left: None,
460 right: None,
461 }));
462 }
463
464 let vp = entries.swap_remove(0);
466
467 let mut with_dists: Vec<(BucketEntry, FixedPoint)> = entries
469 .into_iter()
470 .map(|e| {
471 let d = vp.point.hyperbolic_distance(&e.point);
472 (e, d)
473 })
474 .collect();
475
476 with_dists.sort_by(|a, b| cmp_fp(a.1, b.1));
478
479 let median = with_dists[with_dists.len() / 2].1;
480
481 let (left_vec, right_vec): (Vec<_>, Vec<_>) = with_dists
483 .into_iter()
484 .partition(|(_, d)| *d < median);
485
486 let left = Self::build_tree(left_vec.into_iter().map(|(e, _)| e).collect());
487 let right = Self::build_tree(right_vec.into_iter().map(|(e, _)| e).collect());
488
489 Some(Box::new(VPNode {
490 entry: vp,
491 median,
492 left,
493 right,
494 }))
495 }
496
497 fn search_radius(
504 node: &VPNode,
505 center: &HyperbolicPoint,
506 radius: FixedPoint,
507 deleted: &HashSet<String>,
508 results: &mut Vec<(String, FixedPoint)>,
509 ) {
510 let d = center.hyperbolic_distance(&node.entry.point);
511
512 if d <= radius && !deleted.contains(&node.entry.unique_id) {
513 results.push((node.entry.unique_id.clone(), d));
514 }
515
516 if let Some(ref left) = node.left {
521 if d - radius <= node.median {
522 Self::search_radius(left, center, radius, deleted, results);
523 }
524 }
525
526 if let Some(ref right) = node.right {
530 if d + radius >= node.median {
531 Self::search_radius(right, center, radius, deleted, results);
532 }
533 }
534 }
535
536 fn search_knn(
541 node: &VPNode,
542 center: &HyperbolicPoint,
543 k: usize,
544 deleted: &HashSet<String>,
545 candidates: &mut Vec<(String, FixedPoint)>,
546 tau: &mut FixedPoint,
547 ) {
548 let d = center.hyperbolic_distance(&node.entry.point);
549
550 if !deleted.contains(&node.entry.unique_id) {
552 if candidates.len() < k || d < *tau {
553 candidates.push((node.entry.unique_id.clone(), d));
554 candidates.sort_by(|a, b| cmp_fp(a.1, b.1));
555 if candidates.len() > k {
556 candidates.truncate(k);
557 }
558 if candidates.len() == k {
559 *tau = candidates.last().unwrap().1;
560 }
561 }
562 }
563
564 let search_left_first = d < node.median;
566
567 if search_left_first {
568 if let Some(ref left) = node.left {
569 if d - *tau <= node.median {
570 Self::search_knn(left, center, k, deleted, candidates, tau);
571 }
572 }
573 if let Some(ref right) = node.right {
574 if d + *tau >= node.median {
575 Self::search_knn(right, center, k, deleted, candidates, tau);
576 }
577 }
578 } else {
579 if let Some(ref right) = node.right {
580 if d + *tau >= node.median {
581 Self::search_knn(right, center, k, deleted, candidates, tau);
582 }
583 }
584 if let Some(ref left) = node.left {
585 if d - *tau <= node.median {
586 Self::search_knn(left, center, k, deleted, candidates, tau);
587 }
588 }
589 }
590 }
591}
592
593#[derive(Debug)]
602pub struct HyperbolicHashBucket {
603 region: HyperbolicRegion,
605 position_signature: Vec<i32>,
607 _metrics: Vec<FixedPoint>,
609 vp_tree: Mutex<VPTree>,
611 eff: Mutex<EffRadius>,
616}
617
618#[derive(Clone, Debug)]
627struct EffRadius {
628 nominal: FixedPoint,
630 current: FixedPoint,
632 max_uid: Option<String>,
637}
638
639impl EffRadius {
640 fn new(nominal: FixedPoint) -> Self {
641 Self { nominal, current: nominal, max_uid: None }
642 }
643}
644
645impl Clone for HyperbolicHashBucket {
646 fn clone(&self) -> Self {
647 let vp_tree = self.vp_tree.lock().unwrap_or_else(|e| e.into_inner()).clone();
651 let eff = self.eff.lock().unwrap_or_else(|e| e.into_inner()).clone();
652 Self {
653 region: self.region.clone(),
654 position_signature: self.position_signature.clone(),
655 _metrics: self._metrics.clone(),
656 vp_tree: Mutex::new(vp_tree),
657 eff: Mutex::new(eff),
658 }
659 }
660}
661
662impl HyperbolicHashBucket {
663 pub fn new(region: HyperbolicRegion, position_signature: Vec<i32>) -> Self {
665 let mut metrics = Vec::new();
666
667 let center = region.center();
668 metrics.push(center.euclidean_norm());
669
670 let sum_squares = center.coords().iter().enumerate().fold(
671 FixedPoint::from_int(0),
672 |acc, (_i, &x)| acc + x * x
673 );
674 metrics.push(sum_squares);
675
676 let nominal_radius = region.radius();
677 Self {
678 region,
679 position_signature,
680 _metrics: metrics,
681 vp_tree: Mutex::new(VPTree::new()),
682 eff: Mutex::new(EffRadius::new(nominal_radius)),
683 }
684 }
685
686 pub fn effective_radius(&self) -> FixedPoint {
689 self.eff.lock().unwrap_or_else(|e| e.into_inner()).current
690 }
691
692 fn note_node_distance(&self, unique_id: &str, center_dist: FixedPoint) {
696 let mut e = self.eff.lock().unwrap_or_else(|e| e.into_inner());
697 if center_dist > e.current {
698 e.current = center_dist;
699 e.max_uid = Some(unique_id.to_string());
700 }
701 }
702
703 fn forget_node(&self, unique_id: &str) {
714 let mut e = self.eff.lock().unwrap_or_else(|e| e.into_inner());
715 if e.max_uid.as_deref() != Some(unique_id) {
716 return;
717 }
718 let tree = self.vp_tree.lock().unwrap_or_else(|e| e.into_inner());
719 match tree.farthest_from(self.region.center()) {
720 Some((uid, dist)) if dist > e.nominal => {
721 e.current = dist;
722 e.max_uid = Some(uid);
723 }
724 _ => {
725 e.current = e.nominal;
726 e.max_uid = None;
727 }
728 }
729 }
730
731 pub fn contains(&self, point: &HyperbolicPoint, poincare_disk: &PoincareDisk) -> bool {
733 self.region.contains(point, poincare_disk)
734 }
735
736 pub fn region(&self) -> &HyperbolicRegion {
738 &self.region
739 }
740
741 pub fn position_signature(&self) -> &[i32] {
743 &self.position_signature
744 }
745
746 pub fn quick_validate(&self, point: &HyperbolicPoint) -> bool {
748 self.region.quick_validate(point)
749 }
750}
751
752#[derive(Clone)]
763pub struct HyperbolicHashTable {
764 poincare_disk: PoincareDisk,
766 buckets: HashMap<String, HyperbolicHashBucket>,
768 signature_map: HashMap<Vec<i32>, String>,
770 node_to_bucket: DashMap<String, String>,
772}
773
774impl HyperbolicHashTable {
775 pub fn new(dimension: usize) -> Self {
777 let poincare_disk = PoincareDisk::new(dimension);
778
779 let mut table = Self {
780 poincare_disk,
781 buckets: HashMap::new(),
782 signature_map: HashMap::new(),
783 node_to_bucket: DashMap::new(),
784 };
785
786 table.initialize_buckets();
787 table
788 }
789
790 fn initialize_buckets(&mut self) {
792 let dimension = self.poincare_disk.dimension();
793
794 let distances = [
796 FixedPoint::from_int(0), constants::half(), FixedPoint::from_int(1), FixedPoint::from_int(3) / FixedPoint::from_int(2), FixedPoint::from_int(2), ];
802
803 let directions_per_distance = [
804 1, dimension * 2, dimension * 3, dimension * 4, dimension * 5, ];
810
811 for (dist_idx, &distance) in distances.iter().enumerate() {
812 let num_directions = directions_per_distance[dist_idx];
813
814 if dist_idx == 0 {
816 let origin = self.poincare_disk.origin();
817 let region = HyperbolicRegion::new(origin.clone(), constants::region_radius());
818
819 let position_signature = vec![0; dimension];
820
821 let bucket = HyperbolicHashBucket::new(region, position_signature.clone());
822 let signature = self.compute_geometric_signature(&origin);
823 let hash = self.compute_stable_hash(&signature);
824
825 self.buckets.insert(hash.clone(), bucket);
826 self.signature_map.insert(position_signature, hash);
827
828 continue;
829 }
830
831 for dir_idx in 0..num_directions {
832 let direction = self.generate_direction_vector(dir_idx, num_directions);
833
834 let center = self.poincare_disk.point_at_distance_from_origin(
835 &direction, distance
836 );
837
838 let one_fifth = FixedPoint::from_int(1) / FixedPoint::from_int(5);
840 let one_tenth = FixedPoint::from_int(1) / FixedPoint::from_int(10);
841 let radius = one_fifth + one_tenth * distance;
842 let region = HyperbolicRegion::new(center.clone(), radius);
843
844 let position_signature = self.generate_position_signature(¢er);
845
846 let bucket = HyperbolicHashBucket::new(region, position_signature.clone());
847 let signature = self.compute_geometric_signature(¢er);
848 let hash = self.compute_stable_hash(&signature);
849
850 self.buckets.insert(hash.clone(), bucket);
851 self.signature_map.insert(position_signature, hash);
852 }
853 }
854 }
855
856 fn generate_direction_vector(&self, index: usize, total: usize) -> FixedVector {
858 let dimension = self.poincare_disk.dimension();
859 let mut direction = FixedVector::new(dimension);
860
861 if dimension == 2 {
863 let angle = constants::two_pi()
864 * FixedPoint::from_int(index as i32)
865 / FixedPoint::from_int(total as i32);
866 let (sin_a, cos_a) = angle.sincos();
867 direction[0] = cos_a;
868 direction[1] = sin_a;
869 return direction;
870 }
871
872 let phi = constants::golden_angle();
874
875 let idx = FixedPoint::from_int((index + 1) as i32);
877 for i in 0..dimension {
878 let phase = idx * phi * FixedPoint::from_int((i + 1) as i32);
879 direction[i] = phase.sin();
880 }
881
882 let norm_sq = direction.dot(&direction);
883 if norm_sq > constants::epsilon() {
884 direction.normalize();
885 } else {
886 direction[0] = FixedPoint::from_int(1);
888 }
889
890 direction
891 }
892
893 fn generate_position_signature(&self, point: &HyperbolicPoint) -> Vec<i32> {
895 let dimension = self.poincare_disk.dimension();
896 let mut signature = Vec::with_capacity(dimension);
897
898 for i in 0..dimension {
899 signature.push(constants::quantize_position(point.coords()[i]));
900 }
901
902 signature
903 }
904
905 fn compute_geometric_signature(&self, point: &HyperbolicPoint) -> Vec<i32> {
909 let dimension = self.poincare_disk.dimension();
910 let mut signature = Vec::with_capacity(dimension);
911 let one = FixedPoint::from_int(1);
912
913 for i in 0..dimension {
914 let x = point.coords()[i];
915 let transformed = x * (one + x.tanh());
916 signature.push(constants::quantize_1000(transformed));
917 }
918
919 signature
920 }
921
922 fn compute_stable_hash(&self, signature: &[i32]) -> String {
924 let mut hasher = Sha3_512::new();
925
926 for &value in signature {
927 hasher.update(value.to_le_bytes());
928 }
929
930 let hash = hasher.finalize();
931 hex::encode(&hash[..16])
932 }
933
934 pub fn find_bucket(&self, point: &HyperbolicPoint) -> Option<String> {
944 let position_signature = self.generate_position_signature(point);
946 if let Some(hash) = self.signature_map.get(&position_signature) {
947 return Some(hash.clone());
948 }
949
950 let mut candidates: Vec<(&String, FixedPoint)> = self.buckets.iter()
955 .map(|(hash, bucket)| {
956 (hash, euclidean_distance_sq(point, bucket.region().center()))
957 })
958 .collect();
959 candidates.sort_unstable_by(|a, b| cmp_fp(a.1, b.1).then_with(|| a.0.cmp(b.0)));
962
963 for (hash, _) in &candidates {
964 if let Some(bucket) = self.buckets.get(*hash) {
965 if bucket.contains(point, &self.poincare_disk) {
966 return Some((*hash).clone());
967 }
968 }
969 }
970
971 for (hash, _) in &candidates {
975 if let Some(bucket) = self.buckets.get(*hash) {
976 if bucket.quick_validate(point) {
977 return Some((*hash).clone());
978 }
979 }
980 }
981
982 None
983 }
984
985 pub fn create_signature(&self, point: &HyperbolicPoint, level: u32) -> Option<GeometricSignature> {
992 let position_signature = self.generate_position_signature(point);
994
995 let hash = if let Some(bucket_hash) = self.find_bucket(point) {
997 bucket_hash
998 } else {
999 let geo_sig = self.compute_geometric_signature(point);
1001 self.compute_stable_hash(&geo_sig)
1002 };
1003
1004 Some(GeometricSignature::new(hash, level, position_signature))
1005 }
1006
1007 pub fn validate_point(&self, point: &HyperbolicPoint) -> bool {
1009 let norm = point.euclidean_norm();
1010 if norm >= FixedPoint::from_int(1) {
1011 return false;
1012 }
1013
1014 self.find_bucket(point).is_some()
1015 }
1016
1017 pub fn poincare_disk(&self) -> &PoincareDisk {
1019 &self.poincare_disk
1020 }
1021
1022 pub fn bucket_count(&self) -> usize {
1024 self.buckets.len()
1025 }
1026
1027 pub fn register_node(&self, point: &HyperbolicPoint, unique_id: &str, level: u32) -> Option<String> {
1030 self.register_node_with_hint(point, unique_id, level, None)
1031 }
1032
1033 pub fn register_node_with_hint(&self, point: &HyperbolicPoint, unique_id: &str, level: u32, bucket_hint: Option<&str>) -> Option<String> {
1039 if self.node_to_bucket.contains_key(unique_id) {
1041 return self.node_to_bucket.get(unique_id).map(|r| r.value().clone());
1042 }
1043
1044 let bucket_hash = match bucket_hint {
1046 Some(hint) if self.buckets.contains_key(hint) => hint.to_string(),
1047 _ => self.find_bucket(point)?,
1048 };
1049
1050 if let Some(bucket) = self.buckets.get(&bucket_hash) {
1051 let center_dist = self.poincare_disk.distance(point, bucket.region.center());
1055 bucket.note_node_distance(unique_id, center_dist);
1056 bucket.vp_tree.lock().unwrap_or_else(|e| e.into_inner()).insert(BucketEntry {
1057 unique_id: unique_id.to_string(),
1058 point: point.clone(),
1059 level,
1060 });
1061 }
1062 self.node_to_bucket.insert(unique_id.to_string(), bucket_hash.clone());
1063 Some(bucket_hash)
1064 }
1065
1066 pub fn unregister_node(&self, unique_id: &str) {
1069 if let Some((_, bucket_hash)) = self.node_to_bucket.remove(unique_id) {
1070 if let Some(bucket) = self.buckets.get(&bucket_hash) {
1071 bucket.vp_tree.lock().unwrap_or_else(|e| e.into_inner()).remove(unique_id);
1075 bucket.forget_node(unique_id);
1076 }
1077 }
1078 }
1079
1080 pub fn find_nodes_in_radius(&self, center: &HyperbolicPoint, radius: FixedPoint) -> Vec<(String, FixedPoint)> {
1085 let mut results = Vec::new();
1086
1087 for bucket in self.buckets.values() {
1088 let bucket_center_dist = self.poincare_disk.distance(
1092 center, bucket.region.center()
1093 );
1094 if bucket_center_dist > radius + bucket.effective_radius() {
1095 continue;
1096 }
1097
1098 let bucket_results = bucket.vp_tree.lock().unwrap_or_else(|e| e.into_inner()).find_in_radius(center, radius);
1100 results.extend(bucket_results);
1101 }
1102
1103 results
1104 }
1105
1106 pub fn find_nearest_nodes(&self, point: &HyperbolicPoint, k: usize) -> Vec<(String, FixedPoint)> {
1113 if k == 0 { return Vec::new(); }
1114
1115 let zero = FixedPoint::from_int(0);
1122 let mut bucket_dists: Vec<(&String, FixedPoint)> = self.buckets.iter()
1123 .map(|(hash, bucket)| {
1124 let d = self.poincare_disk.distance(point, bucket.region.center());
1125 let r = bucket.effective_radius();
1126 let min_possible = if d > r { d - r } else { zero };
1127 (hash, min_possible)
1128 })
1129 .collect();
1130 bucket_dists.sort_by(|a, b| cmp_fp(a.1, b.1).then_with(|| a.0.cmp(b.0)));
1131
1132 let mut candidates: Vec<(String, FixedPoint)> = Vec::new();
1133
1134 for (hash, min_possible) in &bucket_dists {
1135 if candidates.len() >= k {
1138 let kth_dist = candidates.last().unwrap().1;
1139 if *min_possible > kth_dist {
1140 break;
1141 }
1142 }
1143
1144 if let Some(bucket) = self.buckets.get(*hash) {
1145 let bucket_results = bucket.vp_tree.lock().unwrap_or_else(|e| e.into_inner()).find_nearest(point, k);
1147
1148 for result in bucket_results {
1150 candidates.push(result);
1151 }
1152
1153 candidates.sort_by(|a, b| cmp_fp(a.1, b.1));
1155 candidates.truncate(k);
1156 }
1157 }
1158
1159 candidates
1160 }
1161
1162 pub fn verify_integrity(&self) -> bool {
1164 if self.buckets.is_empty() {
1165 return false;
1166 }
1167
1168 for (sig, hash) in &self.signature_map {
1169 if !self.buckets.contains_key(hash) {
1170 return false;
1171 }
1172
1173 let bucket = &self.buckets[hash];
1174 if bucket.position_signature() != sig.as_slice() {
1175 return false;
1176 }
1177 }
1178
1179 true
1180 }
1181}
1182
1183impl Debug for HyperbolicHashTable {
1184 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
1185 write!(f, "HyperbolicHashTable(dim={}, buckets={}, nodes={})",
1186 self.poincare_disk.dimension(), self.buckets.len(),
1187 self.node_to_bucket.len())
1188 }
1189}
1190
1191#[cfg(test)]
1192mod tests {
1193 use super::*;
1194
1195 #[test]
1196 fn test_hash_table_creation() {
1197 let table = HyperbolicHashTable::new(2);
1198 assert_eq!(table.poincare_disk().dimension(), 2);
1199 assert!(table.bucket_count() > 0);
1200 }
1201
1202 #[test]
1203 fn test_geometric_signature() {
1204 let table = HyperbolicHashTable::new(2);
1205 let point = table.poincare_disk().point_from_f32_slice(&[0.5, 0.0]);
1206
1207 let signature = table.create_signature(&point, 0).unwrap();
1208 assert_eq!(signature.level(), 0);
1209 assert!(!signature.hash().is_empty());
1210 assert!(!signature.position_signature().is_empty());
1211 }
1212
1213 #[test]
1214 fn test_bucket_finding() {
1215 let table = HyperbolicHashTable::new(2);
1216 let origin = table.poincare_disk().origin();
1217
1218 let bucket_hash = table.find_bucket(&origin);
1219 assert!(bucket_hash.is_some());
1220 }
1221
1222 #[test]
1223 fn test_point_validation() {
1224 let table = HyperbolicHashTable::new(2);
1225
1226 let valid_point = table.poincare_disk().point_from_f32_slice(&[0.5, 0.0]);
1227 assert!(table.validate_point(&valid_point));
1228
1229 let projected_point = table.poincare_disk().point_from_f32_slice(&[1.5, 0.0]);
1230 assert!(table.validate_point(&projected_point));
1231 }
1232
1233 #[test]
1234 fn test_hyperbolic_region() {
1235 let disk = PoincareDisk::new(2);
1236 let center = disk.point_from_f32_slice(&[0.5, 0.0]);
1237 let radius = constants::half();
1238
1239 let region = HyperbolicRegion::new(center.clone(), radius);
1240
1241 assert!(region.contains(¢er, &disk));
1242 assert!(!region.contains(&disk.origin(), &disk));
1243
1244 let far_point = disk.point_from_f32_slice(&[0.8, 0.0]);
1245 assert!(!region.contains(&far_point, &disk));
1246 }
1247
1248 #[test]
1249 fn test_hash_bucket() {
1250 let disk = PoincareDisk::new(2);
1251 let center = disk.point_from_f32_slice(&[0.5, 0.0]);
1252 let radius = constants::half();
1253
1254 let region = HyperbolicRegion::new(center.clone(), radius);
1255 let position_signature = vec![500, 0];
1256
1257 let bucket = HyperbolicHashBucket::new(region, position_signature);
1258
1259 assert!(bucket.contains(¢er, &disk));
1260 assert!(bucket.quick_validate(¢er));
1261 }
1262
1263 #[test]
1264 fn test_integrity_verification() {
1265 let table = HyperbolicHashTable::new(2);
1266 assert!(table.verify_integrity());
1267 }
1268
1269 #[test]
1272 fn test_vp_tree_empty() {
1273 let vp = VPTree::new();
1274 assert!(vp.is_empty());
1275 assert_eq!(vp.live_count(), 0);
1276
1277 let origin = HyperbolicPoint::origin(2);
1278 let results = vp.find_in_radius(&origin, FixedPoint::from_int(10));
1279 assert!(results.is_empty());
1280
1281 let nearest = vp.find_nearest(&origin, 5);
1282 assert!(nearest.is_empty());
1283 }
1284
1285 #[test]
1286 fn test_vp_tree_insert_and_find() {
1287 let disk = PoincareDisk::new(2);
1288 let mut vp = VPTree::new();
1289
1290 let points: Vec<(&str, [f32; 2])> = vec![
1292 ("a", [0.1, 0.0]),
1293 ("b", [0.2, 0.0]),
1294 ("c", [0.3, 0.0]),
1295 ("d", [0.0, 0.1]),
1296 ("e", [0.0, 0.2]),
1297 ];
1298
1299 for (id, coords) in &points {
1300 vp.insert(BucketEntry {
1301 unique_id: id.to_string(),
1302 point: disk.point_from_f32_slice(coords),
1303 level: 0,
1304 });
1305 }
1306
1307 assert_eq!(vp.live_count(), 5);
1308
1309 let origin = disk.origin();
1311 let nearest = vp.find_nearest(&origin, 2);
1312 assert_eq!(nearest.len(), 2);
1313 assert!(nearest[0].1 <= nearest[1].1);
1315
1316 let all = vp.find_in_radius(&origin, FixedPoint::from_int(10));
1318 assert_eq!(all.len(), 5);
1319
1320 let tiny = vp.find_in_radius(&origin, FixedPoint::from_int(1) / FixedPoint::from_int(10000));
1322 assert!(tiny.len() <= 1);
1323 }
1324
1325 #[test]
1326 fn test_vp_tree_remove() {
1327 let disk = PoincareDisk::new(2);
1328 let mut vp = VPTree::new();
1329
1330 vp.insert(BucketEntry {
1331 unique_id: "x".to_string(),
1332 point: disk.point_from_f32_slice(&[0.1, 0.0]),
1333 level: 0,
1334 });
1335 vp.insert(BucketEntry {
1336 unique_id: "y".to_string(),
1337 point: disk.point_from_f32_slice(&[0.2, 0.0]),
1338 level: 0,
1339 });
1340
1341 assert_eq!(vp.live_count(), 2);
1342
1343 vp.remove("x");
1344 assert_eq!(vp.live_count(), 1);
1345
1346 let origin = disk.origin();
1348 let results = vp.find_in_radius(&origin, FixedPoint::from_int(10));
1349 assert_eq!(results.len(), 1);
1350 assert_eq!(results[0].0, "y");
1351 }
1352
1353 #[test]
1354 fn test_vp_tree_rebuild_on_buffer_threshold() {
1355 let disk = PoincareDisk::new(2);
1356 let mut vp = VPTree::new();
1357
1358 for i in 0..(VP_BUFFER_THRESHOLD + 5) {
1360 let angle = constants::two_pi()
1361 * FixedPoint::from_int(i as i32)
1362 / FixedPoint::from_int((VP_BUFFER_THRESHOLD + 5) as i32);
1363 let r = FixedPoint::from_int(3) / FixedPoint::from_int(10);
1364 let mut coords = FixedVector::new(2);
1365 let (sin_a, cos_a) = angle.sincos();
1366 coords[0] = r * cos_a;
1367 coords[1] = r * sin_a;
1368
1369 vp.insert(BucketEntry {
1370 unique_id: format!("node_{}", i),
1371 point: HyperbolicPoint::new(coords),
1372 level: 0,
1373 });
1374 }
1375
1376 assert!(vp.root.is_some());
1378 assert_eq!(vp.live_count(), VP_BUFFER_THRESHOLD + 5);
1379
1380 let origin = disk.origin();
1382 let all = vp.find_in_radius(&origin, FixedPoint::from_int(10));
1383 assert_eq!(all.len(), VP_BUFFER_THRESHOLD + 5);
1384 }
1385
1386 #[test]
1387 fn test_vp_tree_knn_ordering() {
1388 let disk = PoincareDisk::new(2);
1389 let mut vp = VPTree::new();
1390
1391 let distances = [0.05f32, 0.1, 0.2, 0.3, 0.5, 0.7];
1393 for (i, &d) in distances.iter().enumerate() {
1394 vp.insert(BucketEntry {
1395 unique_id: format!("p{}", i),
1396 point: disk.point_from_f32_slice(&[d, 0.0]),
1397 level: 0,
1398 });
1399 }
1400
1401 let origin = disk.origin();
1402 let nearest = vp.find_nearest(&origin, 3);
1403 assert_eq!(nearest.len(), 3);
1404
1405 for i in 1..nearest.len() {
1407 assert!(nearest[i].1 >= nearest[i - 1].1,
1408 "Results not sorted: {:?} >= {:?}", nearest[i].1, nearest[i - 1].1);
1409 }
1410
1411 let ids: Vec<&str> = nearest.iter().map(|(id, _)| id.as_str()).collect();
1413 assert!(ids.contains(&"p0"));
1414 assert!(ids.contains(&"p1"));
1415 assert!(ids.contains(&"p2"));
1416 }
1417
1418 #[test]
1419 fn test_register_unregister_with_vp_tree() {
1420 let table = HyperbolicHashTable::new(2);
1421 let disk_clone = table.poincare_disk().clone();
1422
1423 let p1 = disk_clone.point_from_f32_slice(&[0.1, 0.0]);
1424 let p2 = disk_clone.point_from_f32_slice(&[0.2, 0.0]);
1425 let p3 = disk_clone.point_from_f32_slice(&[0.3, 0.0]);
1426
1427 table.register_node(&p1, "node1", 0);
1428 table.register_node(&p2, "node2", 1);
1429 table.register_node(&p3, "node3", 1);
1430
1431 let origin = disk_clone.origin();
1433 let results = table.find_nodes_in_radius(&origin, FixedPoint::from_int(10));
1434 assert!(results.len() >= 3, "Expected at least 3, got {}", results.len());
1435
1436 table.unregister_node("node2");
1438
1439 let results = table.find_nodes_in_radius(&origin, FixedPoint::from_int(10));
1441 let ids: Vec<&str> = results.iter().map(|(id, _)| id.as_str()).collect();
1442 assert!(!ids.contains(&"node2"), "node2 should be unregistered");
1443 assert!(ids.contains(&"node1"));
1444 assert!(ids.contains(&"node3"));
1445 }
1446
1447 #[test]
1448 fn test_find_nearest_with_early_termination() {
1449 let table = HyperbolicHashTable::new(2);
1450 let disk_clone = table.poincare_disk().clone();
1451
1452 let positions: Vec<(&str, [f32; 2])> = vec![
1454 ("close1", [0.05, 0.0]),
1455 ("close2", [0.0, 0.05]),
1456 ("mid1", [0.3, 0.0]),
1457 ("mid2", [0.0, 0.3]),
1458 ("far1", [0.7, 0.0]),
1459 ("far2", [0.0, 0.7]),
1460 ];
1461
1462 for (id, coords) in &positions {
1463 let point = disk_clone.point_from_f32_slice(coords);
1464 table.register_node(&point, id, 0);
1465 }
1466
1467 let origin = disk_clone.origin();
1468 let nearest = table.find_nearest_nodes(&origin, 2);
1469 assert_eq!(nearest.len(), 2);
1470
1471 let ids: Vec<&str> = nearest.iter().map(|(id, _)| id.as_str()).collect();
1473 assert!(ids.contains(&"close1"));
1474 assert!(ids.contains(&"close2"));
1475
1476 assert!(nearest[0].1 <= nearest[1].1);
1478 }
1479
1480 #[test]
1481 fn test_duplicate_registration_prevented() {
1482 let table = HyperbolicHashTable::new(2);
1483 let point = table.poincare_disk().point_from_f32_slice(&[0.1, 0.0]);
1484
1485 let h1 = table.register_node(&point, "dup_node", 0);
1486 let h2 = table.register_node(&point, "dup_node", 0);
1487
1488 assert_eq!(h1, h2);
1490
1491 let origin = table.poincare_disk().origin();
1493 let results = table.find_nodes_in_radius(&origin, FixedPoint::from_int(10));
1494 let count = results.iter().filter(|(id, _)| id == "dup_node").count();
1495 assert_eq!(count, 1, "Duplicate registration should be prevented");
1496 }
1497
1498 #[test]
1499 fn effective_radius_returns_to_nominal_when_lone_outlier_removed() {
1500 let table = HyperbolicHashTable::new(2);
1505 let disk = table.poincare_disk().clone();
1506
1507 let deep = disk.point_from_f32_slice(&[0.95, 0.0]);
1508 let bucket_hash = table.register_node(&deep, "deep", 5).unwrap();
1509
1510 let nominal = table.buckets.get(&bucket_hash).unwrap().region().radius();
1511 let inflated = table.buckets.get(&bucket_hash).unwrap().effective_radius();
1512 assert!(
1513 inflated > nominal,
1514 "deep node should widen the bucket past nominal (inflated={:?}, nominal={:?})",
1515 inflated, nominal
1516 );
1517
1518 table.unregister_node("deep");
1519
1520 let after = table.buckets.get(&bucket_hash).unwrap().effective_radius();
1521 assert_eq!(
1522 after, nominal,
1523 "with the only out-of-region member gone, the bound must return to nominal"
1524 );
1525 }
1526
1527 #[test]
1528 fn effective_radius_falls_to_second_farthest_not_nominal() {
1529 let table = HyperbolicHashTable::new(2);
1534 let disk = table.poincare_disk().clone();
1535
1536 let near_deep = disk.point_from_f32_slice(&[0.85, 0.0]);
1538 let far_deep = disk.point_from_f32_slice(&[0.97, 0.0]);
1539
1540 let h_near = table.register_node(&near_deep, "near_deep", 4).unwrap();
1541 let h_far = table.register_node(&far_deep, "far_deep", 6).unwrap();
1542
1543 if h_near != h_far {
1546 return;
1547 }
1548
1549 let bucket = || table.buckets.get(&h_near).unwrap();
1550 let nominal = bucket().region().radius();
1551 let with_both = bucket().effective_radius();
1552
1553 let center = bucket().region().center().clone();
1556 let near_dist = center.hyperbolic_distance(&near_deep);
1557
1558 table.unregister_node("far_deep");
1559 let after = bucket().effective_radius();
1560
1561 assert!(after < with_both, "removing the farther node must shrink the bound");
1562 assert!(after > nominal, "the remaining out-of-region node must keep the bound above nominal");
1563 assert_eq!(after, near_dist, "the bound must equal the remaining node's center distance");
1564 }
1565}