1use std::collections::HashMap;
10use std::collections::BinaryHeap;
11use std::fmt::{self, Debug, Formatter};
12use std::ops::Range;
13use std::sync::Mutex;
14use dashmap::DashMap;
15use g_math::fixed_point::{FixedPoint, FixedVector};
16use super::hyperbolic_geometry::{PoincareDisk, HyperbolicPoint};
17use super::hash_table::GeometricSignature;
18use crate::constants;
19use crate::cell_index::CellIndex;
20use crate::metric_tree::EuclideanMetric;
21use crate::semantic_index::SemanticIndexCache;
22
23#[derive(Clone, Debug)]
25pub struct NodeMetadata {
26 pub key: String,
28 pub content_type: Option<String>,
30 pub metadata: HashMap<String, String>,
32 pub created_at: u64,
34 pub updated_at: u64,
36}
37
38impl NodeMetadata {
39 pub fn new(key: String, content_type: Option<String>) -> Self {
41 let now = std::time::SystemTime::now()
42 .duration_since(std::time::UNIX_EPOCH)
43 .unwrap_or_default()
44 .as_secs();
45
46 Self {
47 key,
48 content_type,
49 metadata: HashMap::new(),
50 created_at: now,
51 updated_at: now,
52 }
53 }
54
55 pub fn touch(&mut self) {
57 self.updated_at = std::time::SystemTime::now()
58 .duration_since(std::time::UNIX_EPOCH)
59 .unwrap_or_default()
60 .as_secs();
61 }
62}
63
64pub const RAINBOW_BAND_CAPACITY: u32 = 256;
69const RAINBOW_BAND_STEP_DIV: i32 = 64;
71const RAINBOW_BAND_WARN: u32 = 32;
73
74#[derive(Clone, Debug)]
80pub struct CompressedNode {
81 node_metadata: NodeMetadata,
83 value: Vec<u8>,
85 children: Vec<GeometricSignature>,
87 semantic_coords: Vec<u8>,
90}
91
92impl CompressedNode {
93 pub fn new(metadata: NodeMetadata, value: Vec<u8>) -> Self {
95 Self {
96 node_metadata: metadata,
97 value,
98 children: Vec::new(),
99 semantic_coords: Vec::new(),
100 }
101 }
102
103 pub fn metadata(&self) -> &NodeMetadata {
105 &self.node_metadata
106 }
107
108 pub fn metadata_mut(&mut self) -> &mut NodeMetadata {
110 &mut self.node_metadata
111 }
112
113 pub fn value(&self) -> &[u8] {
115 &self.value
116 }
117
118 pub fn update_value(&mut self, value: Vec<u8>) {
120 self.value = value;
121 self.node_metadata.touch();
122 }
123
124 pub fn add_child(&mut self, signature: GeometricSignature) {
126 self.children.push(signature);
127 }
128
129 pub fn remove_child(&mut self, unique_id: &str) {
131 self.children.retain(|sig| sig.unique_id() != unique_id);
132 }
133
134 pub fn children(&self) -> &[GeometricSignature] {
136 &self.children
137 }
138
139 pub fn has_children(&self) -> bool {
141 !self.children.is_empty()
142 }
143
144 pub fn child_count(&self) -> usize {
146 self.children.len()
147 }
148
149 pub fn semantic_coords(&self) -> &[u8] {
151 &self.semantic_coords
152 }
153
154 pub fn set_semantic_coords(&mut self, coords: Vec<u8>) {
156 self.semantic_coords = coords;
157 }
158}
159
160pub struct HyperbolicTensorNetwork {
170 poincare_disk: PoincareDisk,
173 cell_index: CellIndex,
176 nodes: DashMap<String, CompressedNode>,
178 point_map: DashMap<String, HyperbolicPoint>,
180 root_signature: Mutex<Option<GeometricSignature>>,
182 tau: FixedPoint,
184 child_counts: DashMap<String, u32>,
186 semantic_index: SemanticIndexCache,
189}
190
191impl HyperbolicTensorNetwork {
192 pub fn new(dimension: usize, tau: FixedPoint) -> Self {
194 assert_eq!(
199 FixedPoint::raw_byte_len(), 16,
200 "horon-engine requires the 16-byte Q64.64 g_math profile (GMATH_PROFILE=embedded); \
201 rebuild with the correct profile"
202 );
203
204 Self {
205 poincare_disk: PoincareDisk::new(dimension),
206 cell_index: CellIndex::default(),
207 nodes: DashMap::new(),
208 point_map: DashMap::new(),
209 root_signature: Mutex::new(None),
210 tau,
211 child_counts: DashMap::new(),
212 semantic_index: SemanticIndexCache::new(),
213 }
214 }
215
216 pub fn add_node_data_only(&self, metadata: NodeMetadata, value: Vec<u8>, _level: u32) -> String {
223 use sha3::{Sha3_256, Digest as _};
224 let mut hasher = Sha3_256::new();
225 hasher.update(b"data_only:");
226 hasher.update(metadata.key.as_bytes());
227 let unique_id = hex::encode(&hasher.finalize()[..16]);
228
229 let node = CompressedNode::new(metadata, value);
230 self.nodes.insert(unique_id.clone(), node);
231 self.semantic_index.bump();
234 unique_id
235 }
236
237 pub fn add_node(&self,
243 metadata: NodeMetadata,
244 value: Vec<u8>,
245 parent_signature: Option<&GeometricSignature>,
246 level: u32) -> Option<GeometricSignature> {
247 self.add_node_inner(metadata, value, parent_signature, level, None)
248 }
249
250 pub fn add_node_positioned(&self,
255 metadata: NodeMetadata,
256 value: Vec<u8>,
257 parent_signature: Option<&GeometricSignature>,
258 level: u32,
259 child_index: u32) -> Option<GeometricSignature> {
260 self.add_node_inner(metadata, value, parent_signature, level, Some(child_index))
261 }
262
263 fn add_node_inner(&self,
264 metadata: NodeMetadata,
265 value: Vec<u8>,
266 parent_signature: Option<&GeometricSignature>,
267 level: u32,
268 child_index_hint: Option<u32>) -> Option<GeometricSignature> {
269 if self.tau > FixedPoint::from_int(0) {
273 let depth_budget = (FixedPoint::from_int(44) / self.tau).to_int() as u32;
274 if level.saturating_mul(10) >= depth_budget.saturating_mul(9) {
275 log::warn!(
276 "insert '{}' at depth {} approaches the Q64.64 precision budget (~{} levels at tau={}); sibling positions may lose separation",
277 metadata.key, level, depth_budget, self.tau.to_f64()
278 );
279 }
280 }
281 const MAX_PROBE: u32 = 1024;
302 let mut probe = child_index_hint;
303 let mut resolved = None;
304
305 for _ in 0..MAX_PROBE {
306 let (point, child_index) = match parent_signature {
307 Some(parent_sig) => self.compute_child_placement(parent_sig, probe),
308 None => (self.poincare_disk.origin(), 0),
309 };
310
311 if crate::constants::min_safe_disk_gap()
317 > FixedPoint::from_int(1) - point.coords().length_squared()
318 {
319 log::error!(
320 "refusing to place '{}' at level {}: hyperbolic radius exceeds {} \
321 (max_safe_radius), where the Q64.64 distance kernel saturates. \
322 Depth limit is floor(max_safe_radius / tau) = {} at tau = {}.",
323 metadata.key,
324 level,
325 crate::constants::max_safe_radius().to_f64(),
326 (crate::constants::max_safe_radius() / self.tau).to_int(),
327 self.tau.to_f64(),
328 );
329 return None;
330 }
331
332 let signature =
333 GeometricSignature::embedded(&point, self.poincare_disk.dimension(), level);
334 let unique_id = signature.unique_id();
335
336 let taken_by_other = self
338 .nodes
339 .get(&unique_id)
340 .map(|existing| existing.metadata().key != metadata.key)
341 .unwrap_or(false);
342
343 if !taken_by_other {
344 resolved = Some((point, child_index, signature, unique_id));
345 break;
346 }
347
348 if parent_signature.is_none() {
351 break;
352 }
353 probe = Some(child_index.saturating_add(1));
354 }
355
356 let Some((point, child_index, signature, unique_id)) = resolved else {
357 log::error!(
358 "could not place '{}': no free sibling slot within {} probes — \
359 precision budget exceeded (depth/fan-out); insert refused",
360 metadata.key, MAX_PROBE
361 );
362 return None;
363 };
364
365 if let Some(parent_sig) = parent_signature {
369 self.commit_child_index(&parent_sig.unique_id(), Some(child_index), child_index);
374 }
375
376 let node = CompressedNode::new(metadata, value);
377 self.nodes.insert(unique_id.clone(), node);
378 self.semantic_index.bump();
381
382 if let Some(mut node_ref) = self.nodes.get_mut(&unique_id) {
384 node_ref.metadata_mut().metadata.insert(
385 "_child_index".to_string(),
386 child_index.to_string(),
387 );
388 }
389 self.point_map.insert(unique_id.clone(), point.clone());
390
391 self.cell_index.insert(&unique_id, &point);
392
393 if parent_signature.is_none() {
394 let mut root = self.root_signature.lock().unwrap_or_else(|e| e.into_inner());
395 if root.is_none() {
396 *root = Some(signature.clone());
397 }
398 }
399
400 if let Some(parent_sig) = parent_signature {
401 if let Some(mut parent_node) = self.nodes.get_mut(&parent_sig.unique_id()) {
402 parent_node.add_child(signature.clone());
403 }
404 }
405
406 Some(signature)
407 }
408
409 fn compute_child_placement(&self, parent_signature: &GeometricSignature, child_index_hint: Option<u32>) -> (HyperbolicPoint, u32) {
427 let parent_id = parent_signature.unique_id();
428 let dimension = self.poincare_disk.dimension();
429
430 let parent_point = self.point_map.get(&parent_id)
432 .map(|r| r.value().clone())
433 .unwrap_or_else(|| HyperbolicPoint::origin(dimension));
434
435 let child_index = child_index_hint
438 .unwrap_or_else(|| self.child_counts.get(&parent_id).map(|r| *r.value()).unwrap_or(0));
439
440 let band = child_index / RAINBOW_BAND_CAPACITY;
455 if band >= RAINBOW_BAND_WARN {
456 log::warn!(
457 "parent of child '{}' reached rainbow band {} ({}+ siblings): placement \
458 remains collision-free but subtree spacing is degrading — consider restructuring",
459 child_index, band, child_index
460 );
461 }
462 let effective_tau = self.tau
463 + self.tau * FixedPoint::from_int(band as i32)
464 / FixedPoint::from_int(RAINBOW_BAND_STEP_DIV);
465 let half_tau = effective_tau / FixedPoint::from_int(2);
466 let r = half_tau.tanh();
467
468 let angle = FixedPoint::from_int(child_index as i32) * constants::golden_angle();
470
471 let mut child_at_origin = FixedVector::new(dimension);
473 if dimension >= 2 {
474 let (sin_a, cos_a) = angle.sincos();
475 child_at_origin[0] = r * cos_a;
476 child_at_origin[1] = r * sin_a;
477 } else {
479 child_at_origin[0] = if child_index % 2 == 0 { r } else { -r };
481 }
482 let child_point = HyperbolicPoint::new(child_at_origin);
483
484 (child_point.reflect_from_origin(&parent_point), child_index)
486 }
487
488 fn commit_child_index(&self, parent_id: &str, child_index_hint: Option<u32>, child_index: u32) {
494 let next = match child_index_hint {
495 Some(hint) => {
496 let current = self.child_counts.get(parent_id).map(|r| *r.value()).unwrap_or(0);
497 current.max(hint + 1)
498 }
499 None => child_index + 1,
500 };
501 self.child_counts.insert(parent_id.to_string(), next);
502 }
503
504 pub fn get_node_by_signature(&self, signature: &GeometricSignature) -> Option<CompressedNode> {
506 self.nodes.get(&signature.unique_id()).map(|r| r.value().clone())
507 }
508
509 pub fn update_node_value(&self, unique_id: &str, value: Vec<u8>) -> bool {
511 if let Some(mut node) = self.nodes.get_mut(unique_id) {
512 node.update_value(value);
513 true
514 } else {
515 false
516 }
517 }
518
519 pub fn set_node_metadata_entry(&self, unique_id: &str, key: &str, val: &str) -> bool {
521 if let Some(mut node) = self.nodes.get_mut(unique_id) {
522 node.metadata_mut().metadata.insert(key.to_string(), val.to_string());
523 true
524 } else {
525 false
526 }
527 }
528
529 pub fn set_node_semantic(&self, unique_id: &str, coords: Vec<u8>) -> bool {
531 if let Some(mut node) = self.nodes.get_mut(unique_id) {
532 node.set_semantic_coords(coords);
533 drop(node); self.semantic_index.bump();
537 true
538 } else {
539 false
540 }
541 }
542
543 pub fn get_node_semantic(&self, unique_id: &str) -> Option<Vec<u8>> {
545 self.nodes.get(unique_id).map(|node| node.semantic_coords().to_vec())
546 }
547
548 pub fn root_node(&self) -> Option<CompressedNode> {
550 let root = self.root_signature.lock().unwrap_or_else(|e| e.into_inner());
551 root.as_ref().and_then(|sig| {
552 self.get_node_by_signature(sig)
553 })
554 }
555
556 pub fn root_signature(&self) -> Option<GeometricSignature> {
558 self.root_signature.lock().unwrap_or_else(|e| e.into_inner()).clone()
559 }
560
561 pub fn children_of(&self, signature: &GeometricSignature) -> Vec<CompressedNode> {
563 let node = match self.nodes.get(&signature.unique_id()) {
564 Some(r) => r.value().clone(),
565 None => return Vec::new(),
566 };
567
568 let mut children = Vec::new();
569 for child_sig in node.children() {
570 if let Some(child) = self.nodes.get(&child_sig.unique_id()) {
571 children.push(child.value().clone());
572 }
573 }
574
575 children
576 }
577
578 pub fn get_point(&self, unique_id: &str) -> Option<HyperbolicPoint> {
580 self.point_map.get(unique_id).map(|r| r.value().clone())
581 }
582
583 pub fn semantic_epoch(&self) -> u64 {
588 self.semantic_index.epoch()
589 }
590
591 pub fn tau(&self) -> FixedPoint {
594 self.tau
595 }
596
597 pub fn cell_index(&self) -> &CellIndex {
599 &self.cell_index
600 }
601
602 pub fn node_count(&self) -> usize {
604 self.nodes.len()
605 }
606
607 pub fn remove_detached_node(&self, unique_id: &str) {
612 self.nodes.remove(unique_id);
613 self.semantic_index.bump();
615 }
616
617 pub fn unregister_node(&self, unique_id: &str) {
622 self.unregister_node_with_parent(unique_id, None)
623 }
624
625 pub fn unregister_node_with_parent(&self, unique_id: &str, parent_uid: Option<&str>) {
629 self.child_counts.remove(unique_id);
630
631 self.cell_index.remove(unique_id);
632 self.point_map.remove(unique_id);
633
634 self.nodes.remove(unique_id);
638 self.semantic_index.bump();
640
641 if let Some(pid) = parent_uid {
643 if let Some(mut parent_node) = self.nodes.get_mut(pid) {
644 parent_node.remove_child(unique_id);
645 }
646 }
647
648 let mut root = self.root_signature.lock().unwrap_or_else(|e| e.into_inner());
650 if root.as_ref().map(|s| s.unique_id()).as_deref() == Some(unique_id) {
651 *root = None;
652 }
653 }
654
655 pub fn find_descendants_spatial(&self, signature: &GeometricSignature) -> Vec<(String, FixedPoint)> {
660 let unique_id = signature.unique_id();
661 let point = match self.point_map.get(&unique_id) {
662 Some(r) => r.value().clone(),
663 None => return Vec::new(),
664 };
665
666 let subtree_radius = FixedPoint::from_int(3) * self.tau;
668
669 self.cell_index.within_radius(&point, subtree_radius)
670 .into_iter()
671 .filter(|(uid, _)| *uid != unique_id)
672 .collect()
673 }
674
675 pub fn nearest_neighbor_point(&self, query_poincare: &HyperbolicPoint) -> Option<(String, FixedPoint)> {
686 self.cell_index.knn(query_poincare, 1).into_iter().next()
687 }
688
689 pub fn nearest_neighbor_point_k(&self, query_poincare: &HyperbolicPoint, k: usize) -> Vec<(String, FixedPoint)> {
692 self.cell_index.knn(query_poincare, k)
693 }
694
695 pub fn nodes_in_radius(&self, centre: &HyperbolicPoint, radius: FixedPoint) -> Vec<(String, FixedPoint)> {
699 self.cell_index.within_radius(centre, radius)
700 }
701
702
703 pub fn semantic_distance(
718 coords_a: &[u8],
719 coords_b: &[u8],
720 dim_range: &Range<usize>,
721 ) -> FixedPoint {
722 let a = Self::decode_semantic_slice(coords_a, dim_range);
723 let b = Self::decode_semantic_slice(coords_b, dim_range);
724 g_math::fixed_point::imperative::fused::euclidean_distance(&a, &b)
725 }
726
727 pub fn decode_semantic_slice(coords: &[u8], dim_range: &Range<usize>) -> Vec<FixedPoint> {
732 dim_range
733 .clone()
734 .map(|dim| {
735 let start = dim * 16;
736 let end = start + 16;
737 if coords.len() >= end {
738 FixedPoint::from_raw(i128::from_le_bytes(
739 coords[start..end].try_into().unwrap(),
740 ))
741 } else {
742 FixedPoint::from_int(0)
743 }
744 })
745 .collect()
746 }
747
748 pub fn nearest_semantic(
766 &self,
767 query_coords: &[u8],
768 k: usize,
769 dim_range: &Range<usize>,
770 ) -> Vec<(String, FixedPoint)> {
771 if k == 0 {
772 return Vec::new();
773 }
774
775 if self.nodes.len() < constants::SEMANTIC_INDEX_MIN_NODES {
776 return self.nearest_semantic_scan(query_coords, k, dim_range);
777 }
778
779 let query = Self::decode_semantic_slice(query_coords, dim_range);
780 let index = self.semantic_index.get_or_build(dim_range, || {
781 self.nodes
782 .iter()
783 .filter(|entry| !entry.value().semantic_coords().is_empty())
784 .map(|entry| {
785 (
786 entry.value().metadata().key.clone(),
787 Self::decode_semantic_slice(entry.value().semantic_coords(), dim_range),
788 )
789 })
790 .collect()
791 });
792 index.tree.knn(&query, k, &EuclideanMetric)
793 }
794
795 pub fn nearest_semantic_scan(
802 &self,
803 query_coords: &[u8],
804 k: usize,
805 dim_range: &Range<usize>,
806 ) -> Vec<(String, FixedPoint)> {
807 if k == 0 {
808 return Vec::new();
809 }
810
811 let mut heap: BinaryHeap<(FixedPoint, String)> = BinaryHeap::new();
815
816 for entry in self.nodes.iter() {
817 let coords = entry.value().semantic_coords();
818
819 if coords.is_empty() {
821 continue;
822 }
823
824 let dist = Self::semantic_distance(query_coords, coords, dim_range);
825 let key = entry.value().metadata().key.as_str();
826
827 if heap.len() < k {
828 heap.push((dist, key.to_string()));
829 } else if let Some(worst) = heap.peek() {
830 if (dist, key) < (worst.0, worst.1.as_str()) {
831 heap.pop();
832 heap.push((dist, key.to_string()));
833 }
834 }
835 }
836
837 let mut results: Vec<(String, FixedPoint)> = heap
839 .into_iter()
840 .map(|(dist, uid)| (uid, dist))
841 .collect();
842 results.sort_by(|a, b| a.1.cmp(&b.1).then_with(|| a.0.cmp(&b.0)));
843 results
844 }
845
846 pub fn validate_network(&self) -> bool {
851 if self.nodes.is_empty() {
852 return false;
853 }
854
855 let root_sig = self.root_signature.lock().unwrap_or_else(|e| e.into_inner());
856 if root_sig.is_none() {
857 return false;
858 }
859
860 let root_id = root_sig.as_ref().unwrap().unique_id();
861 drop(root_sig);
862 if !self.nodes.contains_key(&root_id) {
863 return false;
864 }
865
866 for entry in self.nodes.iter() {
868 let node = entry.value();
869 for child_sig in node.children() {
870 if !self.nodes.contains_key(&child_sig.unique_id()) {
871 return false;
872 }
873 }
874 }
875
876 for entry in self.nodes.iter() {
878 if !self.point_map.contains_key(entry.key()) {
879 return false;
880 }
881 }
882
883 for entry in self.point_map.iter() {
885 if !self.nodes.contains_key(entry.key()) {
886 return false;
887 }
888 }
889
890 for entry in self.child_counts.iter() {
892 if !self.nodes.contains_key(entry.key()) {
893 return false;
894 }
895 }
896
897 self.verify_index_locates_all_nodes()
898 }
899
900 pub fn verify_index_locates_all_nodes(&self) -> bool {
921 for entry in self.point_map.iter() {
922 let found = self.cell_index.knn(entry.value(), 1);
923 match found.first() {
924 Some((_, distance)) if *distance == FixedPoint::from_int(0) => {}
927 _ => {
928 log::error!(
929 "spatial index cannot locate node {} at its own stored position",
930 entry.key()
931 );
932 return false;
933 }
934 }
935 }
936 true
937 }
938}
939
940impl Debug for HyperbolicTensorNetwork {
941 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
942 write!(f, "HyperbolicTensorNetwork(nodes={}, dimension={})",
943 self.nodes.len(),
944 self.poincare_disk.dimension())
945 }
946}
947
948#[cfg(test)]
949mod tests {
950 use super::*;
951
952 #[test]
953 fn test_compressed_node() {
954 let metadata = NodeMetadata::new("test".to_string(), None);
955 let value = b"Node data".to_vec();
956
957 let node = CompressedNode::new(metadata, value.clone());
958
959 assert_eq!(node.metadata().key, "test");
960 assert_eq!(node.value(), &value[..]);
961 assert!(!node.has_children());
962 assert_eq!(node.child_count(), 0);
963 }
964
965 #[test]
966 fn test_tensor_network_creation() {
967 let network = HyperbolicTensorNetwork::new(2, constants::default_tau());
968
969 assert_eq!(network.node_count(), 0);
970 assert!(network.root_node().is_none());
971 }
972
973 #[test]
974 fn test_adding_nodes() {
975 let network = HyperbolicTensorNetwork::new(2, constants::default_tau());
976
977 let root_meta = NodeMetadata::new("/".to_string(), None);
978 let root_sig = network.add_node(
979 root_meta,
980 b"Root node data".to_vec(),
981 None,
982 0
983 ).unwrap();
984
985 assert_eq!(network.node_count(), 1);
986 assert!(network.root_node().is_some());
987
988 let child_meta = NodeMetadata::new("/child".to_string(), None);
989 let child_sig = network.add_node(
990 child_meta,
991 b"Child node data".to_vec(),
992 Some(&root_sig),
993 1
994 ).unwrap();
995
996 assert_eq!(network.node_count(), 2);
997
998 let root_node = network.get_node_by_signature(&root_sig).unwrap();
999 assert_eq!(root_node.child_count(), 1);
1000 assert_eq!(root_node.children()[0].unique_id(), child_sig.unique_id());
1001 }
1002
1003 #[test]
1004 fn test_network_validation() {
1005 let network = HyperbolicTensorNetwork::new(2, constants::default_tau());
1006
1007 assert!(!network.validate_network());
1008
1009 let root_sig = network.add_node(
1010 NodeMetadata::new("/".to_string(), None),
1011 b"Root data".to_vec(),
1012 None,
1013 0
1014 ).unwrap();
1015
1016 assert!(network.validate_network());
1017
1018 network.add_node(
1019 NodeMetadata::new("/child1".to_string(), None),
1020 b"Child 1 data".to_vec(),
1021 Some(&root_sig),
1022 1
1023 ).unwrap();
1024
1025 network.add_node(
1026 NodeMetadata::new("/child2".to_string(), None),
1027 b"Child 2 data".to_vec(),
1028 Some(&root_sig),
1029 1
1030 ).unwrap();
1031
1032 assert!(network.validate_network());
1033 }
1034
1035 #[test]
1036 fn test_point_map() {
1037 let network = HyperbolicTensorNetwork::new(2, constants::default_tau());
1038
1039 let root_sig = network.add_node(
1040 NodeMetadata::new("/".to_string(), None),
1041 b"root".to_vec(),
1042 None,
1043 0
1044 ).unwrap();
1045
1046 let root_point = network.get_point(&root_sig.unique_id()).unwrap();
1048 assert!(root_point.euclidean_norm() < constants::epsilon());
1049
1050 let child_sig = network.add_node(
1051 NodeMetadata::new("/child".to_string(), None),
1052 b"child".to_vec(),
1053 Some(&root_sig),
1054 1
1055 ).unwrap();
1056
1057 let child_point = network.get_point(&child_sig.unique_id()).unwrap();
1059 assert!(child_point.euclidean_norm() > constants::epsilon());
1060 }
1061
1062 #[test]
1063 fn test_spatial_descendants() {
1064 let network = HyperbolicTensorNetwork::new(2, constants::default_tau());
1065
1066 let root_sig = network.add_node(
1067 NodeMetadata::new("/".to_string(), None),
1068 b"root".to_vec(),
1069 None,
1070 0
1071 ).unwrap();
1072
1073 let child_sig = network.add_node(
1074 NodeMetadata::new("/child".to_string(), None),
1075 b"child".to_vec(),
1076 Some(&root_sig),
1077 1
1078 ).unwrap();
1079
1080 let _grandchild_sig = network.add_node(
1081 NodeMetadata::new("/child/grandchild".to_string(), None),
1082 b"grandchild".to_vec(),
1083 Some(&child_sig),
1084 2
1085 ).unwrap();
1086
1087 let descendants = network.find_descendants_spatial(&root_sig);
1089 assert!(descendants.len() >= 2,
1090 "Expected at least 2 descendants, got {}", descendants.len());
1091 }
1092
1093 #[test]
1094 fn test_sarkar_child_distance() {
1095 let tau = constants::default_tau();
1097 let network = HyperbolicTensorNetwork::new(2, tau);
1098
1099 let root_sig = network.add_node(
1100 NodeMetadata::new("/".to_string(), None),
1101 b"root".to_vec(),
1102 None,
1103 0
1104 ).unwrap();
1105
1106 let child_sig = network.add_node(
1107 NodeMetadata::new("/child".to_string(), None),
1108 b"child".to_vec(),
1109 Some(&root_sig),
1110 1
1111 ).unwrap();
1112
1113 let root_point = network.get_point(&root_sig.unique_id()).unwrap();
1114 let child_point = network.get_point(&child_sig.unique_id()).unwrap();
1115
1116 let dist = root_point.hyperbolic_distance(&child_point);
1117 let tolerance = FixedPoint::from_int(1) / FixedPoint::from_int(100);
1118 assert!((dist - tau).abs() < tolerance,
1119 "Child should be at distance τ={} from parent, got {}", tau, dist);
1120 }
1121
1122 #[test]
1123 fn test_sarkar_sibling_separation() {
1124 let network = HyperbolicTensorNetwork::new(2, constants::default_tau());
1126
1127 let root_sig = network.add_node(
1128 NodeMetadata::new("/".to_string(), None),
1129 b"root".to_vec(),
1130 None,
1131 0
1132 ).unwrap();
1133
1134 let mut child_sigs = Vec::new();
1135 for i in 0..5 {
1136 let sig = network.add_node(
1137 NodeMetadata::new(format!("/child{}", i), None),
1138 format!("child{}", i).into_bytes(),
1139 Some(&root_sig),
1140 1
1141 ).unwrap();
1142 child_sigs.push(sig);
1143 }
1144
1145 let tau = constants::default_tau();
1147 let tolerance = FixedPoint::from_int(1) / FixedPoint::from_int(100);
1148 let root_point = network.get_point(&root_sig.unique_id()).unwrap();
1149
1150 for sig in &child_sigs {
1151 let child_point = network.get_point(&sig.unique_id()).unwrap();
1152 let dist = root_point.hyperbolic_distance(&child_point);
1153 assert!((dist - tau).abs() < tolerance,
1154 "All children should be at distance τ from parent");
1155 }
1156
1157 for i in 0..child_sigs.len() {
1159 for j in (i+1)..child_sigs.len() {
1160 let pi = network.get_point(&child_sigs[i].unique_id()).unwrap();
1161 let pj = network.get_point(&child_sigs[j].unique_id()).unwrap();
1162 let dist = pi.hyperbolic_distance(&pj);
1163 assert!(dist > constants::epsilon(),
1164 "Siblings {} and {} should be at distinct positions", i, j);
1165 }
1166 }
1167 }
1168
1169 #[test]
1170 fn test_nearest_neighbor_point_finds_self() {
1171 let network = HyperbolicTensorNetwork::new(2, constants::default_tau());
1173
1174 let root_sig = network.add_node(
1175 NodeMetadata::new("/".to_string(), None),
1176 b"root".to_vec(),
1177 None,
1178 0
1179 ).unwrap();
1180
1181 let child_sig = network.add_node(
1182 NodeMetadata::new("/child".to_string(), None),
1183 b"child".to_vec(),
1184 Some(&root_sig),
1185 1
1186 ).unwrap();
1187
1188 let child_point = network.get_point(&child_sig.unique_id()).unwrap();
1190 let (nn_id, nn_dist) = network.nearest_neighbor_point(&child_point).unwrap();
1191
1192 assert_eq!(nn_id, child_sig.unique_id(),
1193 "Nearest neighbor at child's position should be child itself");
1194 let tolerance = FixedPoint::from_int(1) / FixedPoint::from_int(100);
1195 assert!(nn_dist < tolerance,
1196 "Distance to self should be ~0, got {}", nn_dist);
1197 }
1198
1199 #[test]
1200 fn test_delete_removes_node_from_index() {
1201 let network = HyperbolicTensorNetwork::new(2, constants::default_tau());
1202
1203 let root_sig = network.add_node(
1204 NodeMetadata::new("/".to_string(), None),
1205 b"root".to_vec(),
1206 None,
1207 0
1208 ).unwrap();
1209
1210 let child_sig = network.add_node(
1211 NodeMetadata::new("/child".to_string(), None),
1212 b"child".to_vec(),
1213 Some(&root_sig),
1214 1
1215 ).unwrap();
1216
1217 let child_id = child_sig.unique_id();
1218 let child_point = network.get_point(&child_id).unwrap();
1219
1220 network.unregister_node_with_parent(&child_id, Some(&root_sig.unique_id()));
1221
1222 assert!(network.get_point(&child_id).is_none(),
1223 "deleted node should leave point_map");
1224
1225 let (nn_id, _) = network.nearest_neighbor_point(&child_point).unwrap();
1228 assert_ne!(nn_id, child_id,
1229 "spatial index still returns a deleted node");
1230 }
1231
1232 #[test]
1233 fn test_semantic_distance_identical() {
1234 let coords = {
1236 let mut v = vec![0u8; 3 * 16]; let val = FixedPoint::from_f64(0.5).raw().to_le_bytes();
1238 v[0..16].copy_from_slice(&val);
1239 v[16..32].copy_from_slice(&val);
1240 v[32..48].copy_from_slice(&val);
1241 v
1242 };
1243 let dist = HyperbolicTensorNetwork::semantic_distance(&coords, &coords, &(0..3));
1244 assert!(dist < constants::epsilon(), "Distance to self should be ~0, got {}", dist);
1245 }
1246
1247 #[test]
1248 fn test_semantic_distance_known_value() {
1249 let mut a = vec![0u8; 2 * 16];
1251 let one = FixedPoint::from_f64(1.0).raw().to_le_bytes();
1252 a[0..16].copy_from_slice(&one);
1253 let b = vec![0u8; 2 * 16]; let dist = HyperbolicTensorNetwork::semantic_distance(&a, &b, &(0..2));
1258 let tolerance = FixedPoint::from_int(1) / FixedPoint::from_int(100);
1259 assert!((dist - FixedPoint::from_int(1)).abs() < tolerance,
1260 "Distance should be 1.0, got {}", dist);
1261 }
1262
1263 #[test]
1264 fn test_semantic_distance_dimensional_slice() {
1265 let mut a = vec![0u8; 2 * 16];
1267 let one = FixedPoint::from_f64(1.0).raw().to_le_bytes();
1268 a[0..16].copy_from_slice(&one); let b = vec![0u8; 2 * 16]; let dist = HyperbolicTensorNetwork::semantic_distance(&a, &b, &(1..2));
1274 assert!(dist < constants::epsilon(),
1275 "Slicing only dim 1 should give distance ~0, got {}", dist);
1276
1277 let dist_full = HyperbolicTensorNetwork::semantic_distance(&a, &b, &(0..1));
1279 let tolerance = FixedPoint::from_int(1) / FixedPoint::from_int(100);
1280 assert!((dist_full - FixedPoint::from_int(1)).abs() < tolerance,
1281 "Slicing dim 0 should give distance 1.0, got {}", dist_full);
1282 }
1283
1284 #[test]
1285 fn test_nearest_semantic_basic() {
1286 let network = HyperbolicTensorNetwork::new(2, constants::default_tau());
1287
1288 let root_sig = network.add_node(
1290 NodeMetadata::new("/".to_string(), None),
1291 b"root".to_vec(), None, 0,
1292 ).unwrap();
1293
1294 let a_sig = network.add_node(
1295 NodeMetadata::new("/a".to_string(), None),
1296 b"a".to_vec(), Some(&root_sig), 1,
1297 ).unwrap();
1298
1299 let b_sig = network.add_node(
1300 NodeMetadata::new("/b".to_string(), None),
1301 b"b".to_vec(), Some(&root_sig), 1,
1302 ).unwrap();
1303
1304 let c_sig = network.add_node(
1305 NodeMetadata::new("/c".to_string(), None),
1306 b"c".to_vec(), Some(&root_sig), 1,
1307 ).unwrap();
1308
1309 let make_coords = |d0: f64, d1: f64| -> Vec<u8> {
1311 let mut v = vec![0u8; 2 * 16];
1312 v[0..16].copy_from_slice(&FixedPoint::from_f64(d0).raw().to_le_bytes());
1313 v[16..32].copy_from_slice(&FixedPoint::from_f64(d1).raw().to_le_bytes());
1314 v
1315 };
1316
1317 network.set_node_semantic(&a_sig.unique_id(), make_coords(0.8, 0.1));
1318 network.set_node_semantic(&b_sig.unique_id(), make_coords(0.7, 0.2));
1319 network.set_node_semantic(&c_sig.unique_id(), make_coords(0.1, 0.9));
1320
1321 let query = make_coords(0.8, 0.1);
1323 let results = network.nearest_semantic(&query, 3, &(0..2));
1324
1325 assert!(!results.is_empty());
1326
1327 let first_dist = results[0].1;
1329 assert!(first_dist < FixedPoint::from_f64(0.01),
1330 "Nearest to (0.8,0.1) should be /a at ~0 distance, got {}", first_dist);
1331
1332 if results.len() >= 3 {
1334 assert!(results[2].1 > results[1].1,
1335 "Third result should be farther than second");
1336 }
1337 }
1338}