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 max_degree: u32,
189 semantic_index: SemanticIndexCache,
192}
193
194impl HyperbolicTensorNetwork {
195 pub fn new(dimension: usize, tau: FixedPoint) -> Self {
197 assert_eq!(
202 FixedPoint::raw_byte_len(), 16,
203 "horon-engine requires the 16-byte Q64.64 g_math profile (GMATH_PROFILE=embedded); \
204 rebuild with the correct profile"
205 );
206
207 Self {
208 poincare_disk: PoincareDisk::new(dimension),
209 cell_index: CellIndex::default(),
210 nodes: DashMap::new(),
211 point_map: DashMap::new(),
212 root_signature: Mutex::new(None),
213 tau,
214 child_counts: DashMap::new(),
215 max_degree: constants::max_degree_for_tau(tau),
216 semantic_index: SemanticIndexCache::new(),
217 }
218 }
219
220 pub fn add_node_data_only(&self, metadata: NodeMetadata, value: Vec<u8>, _level: u32) -> String {
227 use sha3::{Sha3_256, Digest as _};
228 let mut hasher = Sha3_256::new();
229 hasher.update(b"data_only:");
230 hasher.update(metadata.key.as_bytes());
231 let unique_id = hex::encode(&hasher.finalize()[..16]);
232
233 let node = CompressedNode::new(metadata, value);
234 self.nodes.insert(unique_id.clone(), node);
235 self.semantic_index.bump();
238 unique_id
239 }
240
241 pub fn add_node(&self,
247 metadata: NodeMetadata,
248 value: Vec<u8>,
249 parent_signature: Option<&GeometricSignature>,
250 level: u32) -> Option<GeometricSignature> {
251 self.add_node_inner(metadata, value, parent_signature, level, None)
252 }
253
254 pub fn add_node_positioned(&self,
259 metadata: NodeMetadata,
260 value: Vec<u8>,
261 parent_signature: Option<&GeometricSignature>,
262 level: u32,
263 child_index: u32) -> Option<GeometricSignature> {
264 self.add_node_inner(metadata, value, parent_signature, level, Some(child_index))
265 }
266
267 fn add_node_inner(&self,
268 metadata: NodeMetadata,
269 value: Vec<u8>,
270 parent_signature: Option<&GeometricSignature>,
271 level: u32,
272 child_index_hint: Option<u32>) -> Option<GeometricSignature> {
273 if self.tau > FixedPoint::from_int(0) {
277 let depth_budget = (FixedPoint::from_int(44) / self.tau).to_int() as u32;
278 if level.saturating_mul(10) >= depth_budget.saturating_mul(9) {
279 log::warn!(
280 "insert '{}' at depth {} approaches the Q64.64 precision budget (~{} levels at tau={}); sibling positions may lose separation",
281 metadata.key, level, depth_budget, self.tau.to_f64()
282 );
283 }
284 }
285 const MAX_PROBE: u32 = 1024;
306 let mut probe = child_index_hint;
307 let mut resolved = None;
308
309 for _ in 0..MAX_PROBE {
310 let (point, child_index) = match parent_signature {
311 Some(parent_sig) => self.compute_child_placement(parent_sig, probe),
312 None => (self.poincare_disk.origin(), 0),
313 };
314
315 if crate::constants::min_safe_disk_gap()
321 > FixedPoint::from_int(1) - point.coords().length_squared()
322 {
323 log::error!(
324 "refusing to place '{}' at level {}: hyperbolic radius exceeds {} \
325 (max_safe_radius), where the Q64.64 distance kernel saturates. \
326 Depth limit is floor(max_safe_radius / tau) = {} at tau = {}.",
327 metadata.key,
328 level,
329 crate::constants::max_safe_radius().to_f64(),
330 (crate::constants::max_safe_radius() / self.tau).to_int(),
331 self.tau.to_f64(),
332 );
333 return None;
334 }
335
336 let signature =
337 GeometricSignature::embedded(&point, self.poincare_disk.dimension(), level);
338 let unique_id = signature.unique_id();
339
340 let taken_by_other = self
342 .nodes
343 .get(&unique_id)
344 .map(|existing| existing.metadata().key != metadata.key)
345 .unwrap_or(false);
346
347 if !taken_by_other {
348 resolved = Some((point, child_index, signature, unique_id));
349 break;
350 }
351
352 if parent_signature.is_none() {
355 break;
356 }
357 probe = Some(child_index.saturating_add(1));
358 }
359
360 let Some((point, child_index, signature, unique_id)) = resolved else {
361 log::error!(
362 "could not place '{}': no free sibling slot within {} probes — \
363 precision budget exceeded (depth/fan-out); insert refused",
364 metadata.key, MAX_PROBE
365 );
366 return None;
367 };
368
369 if let Some(parent_sig) = parent_signature {
373 self.commit_child_index(&parent_sig.unique_id(), Some(child_index), child_index);
378 }
379
380 let node = CompressedNode::new(metadata, value);
381 self.nodes.insert(unique_id.clone(), node);
382 self.semantic_index.bump();
385
386 if let Some(mut node_ref) = self.nodes.get_mut(&unique_id) {
388 node_ref.metadata_mut().metadata.insert(
389 "_child_index".to_string(),
390 child_index.to_string(),
391 );
392 }
393 self.point_map.insert(unique_id.clone(), point.clone());
394
395 self.cell_index.insert(&unique_id, &point);
396
397 if parent_signature.is_none() {
398 let mut root = self.root_signature.lock().unwrap_or_else(|e| e.into_inner());
399 if root.is_none() {
400 *root = Some(signature.clone());
401 }
402 }
403
404 if let Some(parent_sig) = parent_signature {
405 if let Some(mut parent_node) = self.nodes.get_mut(&parent_sig.unique_id()) {
406 parent_node.add_child(signature.clone());
407 }
408 }
409
410 Some(signature)
411 }
412
413 fn compute_child_placement(&self, parent_signature: &GeometricSignature, child_index_hint: Option<u32>) -> (HyperbolicPoint, u32) {
431 let parent_id = parent_signature.unique_id();
432 let dimension = self.poincare_disk.dimension();
433
434 let parent_point = self.point_map.get(&parent_id)
436 .map(|r| r.value().clone())
437 .unwrap_or_else(|| HyperbolicPoint::origin(dimension));
438
439 let child_index = child_index_hint
442 .unwrap_or_else(|| self.child_counts.get(&parent_id).map(|r| *r.value()).unwrap_or(0));
443
444 let band = child_index / RAINBOW_BAND_CAPACITY;
459 if band >= RAINBOW_BAND_WARN {
460 log::warn!(
461 "parent of child '{}' reached rainbow band {} ({}+ siblings): placement \
462 remains collision-free but subtree spacing is degrading — consider restructuring",
463 child_index, band, child_index
464 );
465 }
466 let effective_tau = self.tau
467 + self.tau * FixedPoint::from_int(band as i32)
468 / FixedPoint::from_int(RAINBOW_BAND_STEP_DIV);
469 let half_tau = effective_tau / FixedPoint::from_int(2);
470 let r = half_tau.tanh();
471
472 let angle = FixedPoint::from_int(child_index as i32) * constants::golden_angle();
474
475 let mut child_at_origin = FixedVector::new(dimension);
477 if dimension >= 2 {
478 let (sin_a, cos_a) = angle.sincos();
479 child_at_origin[0] = r * cos_a;
480 child_at_origin[1] = r * sin_a;
481 } else {
483 child_at_origin[0] = if child_index % 2 == 0 { r } else { -r };
485 }
486 let child_point = HyperbolicPoint::new(child_at_origin);
487
488 (child_point.reflect_from_origin(&parent_point), child_index)
490 }
491
492 fn commit_child_index(&self, parent_id: &str, child_index_hint: Option<u32>, child_index: u32) {
498 let (prev, next) = match child_index_hint {
499 Some(hint) => {
500 let current = self.child_counts.get(parent_id).map(|r| *r.value()).unwrap_or(0);
501 (current, current.max(hint + 1))
502 }
503 None => (child_index, child_index + 1),
504 };
505 self.child_counts.insert(parent_id.to_string(), next);
506
507 if prev <= self.max_degree && next > self.max_degree {
513 log::warn!(
514 "node '{}' now has {} children, past the {} that tau = {} supports \
515 (PROOF.md requires tau >= -log(tan(pi / (2 * d_max)))). Placement and \
516 query results stay correct; siblings crowd, so cells hold more nodes \
517 and scans lengthen. Raise tau via StoreConfig::tau() for wide trees.",
518 parent_id,
519 next,
520 self.max_degree,
521 self.tau.to_f64(),
522 );
523 }
524 }
525
526 pub fn max_degree(&self) -> u32 {
530 self.max_degree
531 }
532
533 pub fn get_node_by_signature(&self, signature: &GeometricSignature) -> Option<CompressedNode> {
535 self.nodes.get(&signature.unique_id()).map(|r| r.value().clone())
536 }
537
538 pub fn update_node_value(&self, unique_id: &str, value: Vec<u8>) -> bool {
540 if let Some(mut node) = self.nodes.get_mut(unique_id) {
541 node.update_value(value);
542 true
543 } else {
544 false
545 }
546 }
547
548 pub fn set_node_metadata_entry(&self, unique_id: &str, key: &str, val: &str) -> bool {
550 if let Some(mut node) = self.nodes.get_mut(unique_id) {
551 node.metadata_mut().metadata.insert(key.to_string(), val.to_string());
552 true
553 } else {
554 false
555 }
556 }
557
558 pub fn set_node_semantic(&self, unique_id: &str, coords: Vec<u8>) -> bool {
560 if let Some(mut node) = self.nodes.get_mut(unique_id) {
561 node.set_semantic_coords(coords);
562 drop(node); self.semantic_index.bump();
566 true
567 } else {
568 false
569 }
570 }
571
572 pub fn get_node_semantic(&self, unique_id: &str) -> Option<Vec<u8>> {
574 self.nodes.get(unique_id).map(|node| node.semantic_coords().to_vec())
575 }
576
577 pub fn root_node(&self) -> Option<CompressedNode> {
579 let root = self.root_signature.lock().unwrap_or_else(|e| e.into_inner());
580 root.as_ref().and_then(|sig| {
581 self.get_node_by_signature(sig)
582 })
583 }
584
585 pub fn root_signature(&self) -> Option<GeometricSignature> {
587 self.root_signature.lock().unwrap_or_else(|e| e.into_inner()).clone()
588 }
589
590 pub fn children_of(&self, signature: &GeometricSignature) -> Vec<CompressedNode> {
592 let node = match self.nodes.get(&signature.unique_id()) {
593 Some(r) => r.value().clone(),
594 None => return Vec::new(),
595 };
596
597 let mut children = Vec::new();
598 for child_sig in node.children() {
599 if let Some(child) = self.nodes.get(&child_sig.unique_id()) {
600 children.push(child.value().clone());
601 }
602 }
603
604 children
605 }
606
607 pub fn get_point(&self, unique_id: &str) -> Option<HyperbolicPoint> {
609 self.point_map.get(unique_id).map(|r| r.value().clone())
610 }
611
612 pub fn semantic_epoch(&self) -> u64 {
617 self.semantic_index.epoch()
618 }
619
620 pub fn tau(&self) -> FixedPoint {
623 self.tau
624 }
625
626 pub fn cell_index(&self) -> &CellIndex {
628 &self.cell_index
629 }
630
631 pub fn node_count(&self) -> usize {
633 self.nodes.len()
634 }
635
636 pub fn remove_detached_node(&self, unique_id: &str) {
641 self.nodes.remove(unique_id);
642 self.semantic_index.bump();
644 }
645
646 pub fn unregister_node(&self, unique_id: &str) {
651 self.unregister_node_with_parent(unique_id, None)
652 }
653
654 pub fn unregister_node_with_parent(&self, unique_id: &str, parent_uid: Option<&str>) {
658 self.child_counts.remove(unique_id);
659
660 self.cell_index.remove(unique_id);
661 self.point_map.remove(unique_id);
662
663 self.nodes.remove(unique_id);
667 self.semantic_index.bump();
669
670 if let Some(pid) = parent_uid {
672 if let Some(mut parent_node) = self.nodes.get_mut(pid) {
673 parent_node.remove_child(unique_id);
674 }
675 }
676
677 let mut root = self.root_signature.lock().unwrap_or_else(|e| e.into_inner());
679 if root.as_ref().map(|s| s.unique_id()).as_deref() == Some(unique_id) {
680 *root = None;
681 }
682 }
683
684 pub fn find_descendants_spatial(&self, signature: &GeometricSignature) -> Vec<(String, FixedPoint)> {
689 let unique_id = signature.unique_id();
690 let point = match self.point_map.get(&unique_id) {
691 Some(r) => r.value().clone(),
692 None => return Vec::new(),
693 };
694
695 let subtree_radius = FixedPoint::from_int(3) * self.tau;
697
698 self.cell_index.within_radius(&point, subtree_radius)
699 .into_iter()
700 .filter(|(uid, _)| *uid != unique_id)
701 .collect()
702 }
703
704 pub fn nearest_neighbor_point(&self, query_poincare: &HyperbolicPoint) -> Option<(String, FixedPoint)> {
715 self.cell_index.knn(query_poincare, 1).into_iter().next()
716 }
717
718 pub fn nearest_neighbor_point_k(&self, query_poincare: &HyperbolicPoint, k: usize) -> Vec<(String, FixedPoint)> {
721 self.cell_index.knn(query_poincare, k)
722 }
723
724 pub fn nodes_in_radius(&self, centre: &HyperbolicPoint, radius: FixedPoint) -> Vec<(String, FixedPoint)> {
728 self.cell_index.within_radius(centre, radius)
729 }
730
731
732 pub fn semantic_distance(
747 coords_a: &[u8],
748 coords_b: &[u8],
749 dim_range: &Range<usize>,
750 ) -> FixedPoint {
751 let a = Self::decode_semantic_slice(coords_a, dim_range);
752 let b = Self::decode_semantic_slice(coords_b, dim_range);
753 g_math::fixed_point::imperative::fused::euclidean_distance(&a, &b)
754 }
755
756 pub fn decode_semantic_slice(coords: &[u8], dim_range: &Range<usize>) -> Vec<FixedPoint> {
761 dim_range
762 .clone()
763 .map(|dim| {
764 let start = dim * 16;
765 let end = start + 16;
766 if coords.len() >= end {
767 FixedPoint::from_raw(i128::from_le_bytes(
768 coords[start..end].try_into().unwrap(),
769 ))
770 } else {
771 FixedPoint::from_int(0)
772 }
773 })
774 .collect()
775 }
776
777 pub fn nearest_semantic(
795 &self,
796 query_coords: &[u8],
797 k: usize,
798 dim_range: &Range<usize>,
799 ) -> Vec<(String, FixedPoint)> {
800 if k == 0 {
801 return Vec::new();
802 }
803
804 if self.nodes.len() < constants::SEMANTIC_INDEX_MIN_NODES {
805 return self.nearest_semantic_scan(query_coords, k, dim_range);
806 }
807
808 let query = Self::decode_semantic_slice(query_coords, dim_range);
809 let index = self.semantic_index.get_or_build(dim_range, || {
810 self.nodes
811 .iter()
812 .filter(|entry| !entry.value().semantic_coords().is_empty())
813 .map(|entry| {
814 (
815 entry.value().metadata().key.clone(),
816 Self::decode_semantic_slice(entry.value().semantic_coords(), dim_range),
817 )
818 })
819 .collect()
820 });
821 index.tree.knn(&query, k, &EuclideanMetric)
822 }
823
824 pub fn nearest_semantic_scan(
831 &self,
832 query_coords: &[u8],
833 k: usize,
834 dim_range: &Range<usize>,
835 ) -> Vec<(String, FixedPoint)> {
836 if k == 0 {
837 return Vec::new();
838 }
839
840 let mut heap: BinaryHeap<(FixedPoint, String)> = BinaryHeap::new();
844
845 for entry in self.nodes.iter() {
846 let coords = entry.value().semantic_coords();
847
848 if coords.is_empty() {
850 continue;
851 }
852
853 let dist = Self::semantic_distance(query_coords, coords, dim_range);
854 let key = entry.value().metadata().key.as_str();
855
856 if heap.len() < k {
857 heap.push((dist, key.to_string()));
858 } else if let Some(worst) = heap.peek() {
859 if (dist, key) < (worst.0, worst.1.as_str()) {
860 heap.pop();
861 heap.push((dist, key.to_string()));
862 }
863 }
864 }
865
866 let mut results: Vec<(String, FixedPoint)> = heap
868 .into_iter()
869 .map(|(dist, uid)| (uid, dist))
870 .collect();
871 results.sort_by(|a, b| a.1.cmp(&b.1).then_with(|| a.0.cmp(&b.0)));
872 results
873 }
874
875 pub fn validate_network(&self) -> bool {
880 if self.nodes.is_empty() {
881 return false;
882 }
883
884 let root_sig = self.root_signature.lock().unwrap_or_else(|e| e.into_inner());
885 if root_sig.is_none() {
886 return false;
887 }
888
889 let root_id = root_sig.as_ref().unwrap().unique_id();
890 drop(root_sig);
891 if !self.nodes.contains_key(&root_id) {
892 return false;
893 }
894
895 for entry in self.nodes.iter() {
897 let node = entry.value();
898 for child_sig in node.children() {
899 if !self.nodes.contains_key(&child_sig.unique_id()) {
900 return false;
901 }
902 }
903 }
904
905 for entry in self.nodes.iter() {
907 if !self.point_map.contains_key(entry.key()) {
908 return false;
909 }
910 }
911
912 for entry in self.point_map.iter() {
914 if !self.nodes.contains_key(entry.key()) {
915 return false;
916 }
917 }
918
919 for entry in self.child_counts.iter() {
921 if !self.nodes.contains_key(entry.key()) {
922 return false;
923 }
924 }
925
926 self.verify_index_locates_all_nodes()
927 }
928
929 pub fn verify_index_locates_all_nodes(&self) -> bool {
950 for entry in self.point_map.iter() {
951 let found = self.cell_index.knn(entry.value(), 1);
952 match found.first() {
953 Some((_, distance)) if *distance == FixedPoint::from_int(0) => {}
956 _ => {
957 log::error!(
958 "spatial index cannot locate node {} at its own stored position",
959 entry.key()
960 );
961 return false;
962 }
963 }
964 }
965 true
966 }
967}
968
969impl Debug for HyperbolicTensorNetwork {
970 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
971 write!(f, "HyperbolicTensorNetwork(nodes={}, dimension={})",
972 self.nodes.len(),
973 self.poincare_disk.dimension())
974 }
975}
976
977#[cfg(test)]
978mod tests {
979 use super::*;
980
981 #[test]
982 fn test_compressed_node() {
983 let metadata = NodeMetadata::new("test".to_string(), None);
984 let value = b"Node data".to_vec();
985
986 let node = CompressedNode::new(metadata, value.clone());
987
988 assert_eq!(node.metadata().key, "test");
989 assert_eq!(node.value(), &value[..]);
990 assert!(!node.has_children());
991 assert_eq!(node.child_count(), 0);
992 }
993
994 #[test]
995 fn test_tensor_network_creation() {
996 let network = HyperbolicTensorNetwork::new(2, constants::default_tau());
997
998 assert_eq!(network.node_count(), 0);
999 assert!(network.root_node().is_none());
1000 }
1001
1002 #[test]
1003 fn test_adding_nodes() {
1004 let network = HyperbolicTensorNetwork::new(2, constants::default_tau());
1005
1006 let root_meta = NodeMetadata::new("/".to_string(), None);
1007 let root_sig = network.add_node(
1008 root_meta,
1009 b"Root node data".to_vec(),
1010 None,
1011 0
1012 ).unwrap();
1013
1014 assert_eq!(network.node_count(), 1);
1015 assert!(network.root_node().is_some());
1016
1017 let child_meta = NodeMetadata::new("/child".to_string(), None);
1018 let child_sig = network.add_node(
1019 child_meta,
1020 b"Child node data".to_vec(),
1021 Some(&root_sig),
1022 1
1023 ).unwrap();
1024
1025 assert_eq!(network.node_count(), 2);
1026
1027 let root_node = network.get_node_by_signature(&root_sig).unwrap();
1028 assert_eq!(root_node.child_count(), 1);
1029 assert_eq!(root_node.children()[0].unique_id(), child_sig.unique_id());
1030 }
1031
1032 #[test]
1033 fn test_network_validation() {
1034 let network = HyperbolicTensorNetwork::new(2, constants::default_tau());
1035
1036 assert!(!network.validate_network());
1037
1038 let root_sig = network.add_node(
1039 NodeMetadata::new("/".to_string(), None),
1040 b"Root data".to_vec(),
1041 None,
1042 0
1043 ).unwrap();
1044
1045 assert!(network.validate_network());
1046
1047 network.add_node(
1048 NodeMetadata::new("/child1".to_string(), None),
1049 b"Child 1 data".to_vec(),
1050 Some(&root_sig),
1051 1
1052 ).unwrap();
1053
1054 network.add_node(
1055 NodeMetadata::new("/child2".to_string(), None),
1056 b"Child 2 data".to_vec(),
1057 Some(&root_sig),
1058 1
1059 ).unwrap();
1060
1061 assert!(network.validate_network());
1062 }
1063
1064 #[test]
1065 fn test_point_map() {
1066 let network = HyperbolicTensorNetwork::new(2, constants::default_tau());
1067
1068 let root_sig = network.add_node(
1069 NodeMetadata::new("/".to_string(), None),
1070 b"root".to_vec(),
1071 None,
1072 0
1073 ).unwrap();
1074
1075 let root_point = network.get_point(&root_sig.unique_id()).unwrap();
1077 assert!(root_point.euclidean_norm() < constants::epsilon());
1078
1079 let child_sig = network.add_node(
1080 NodeMetadata::new("/child".to_string(), None),
1081 b"child".to_vec(),
1082 Some(&root_sig),
1083 1
1084 ).unwrap();
1085
1086 let child_point = network.get_point(&child_sig.unique_id()).unwrap();
1088 assert!(child_point.euclidean_norm() > constants::epsilon());
1089 }
1090
1091 #[test]
1092 fn test_spatial_descendants() {
1093 let network = HyperbolicTensorNetwork::new(2, constants::default_tau());
1094
1095 let root_sig = network.add_node(
1096 NodeMetadata::new("/".to_string(), None),
1097 b"root".to_vec(),
1098 None,
1099 0
1100 ).unwrap();
1101
1102 let child_sig = network.add_node(
1103 NodeMetadata::new("/child".to_string(), None),
1104 b"child".to_vec(),
1105 Some(&root_sig),
1106 1
1107 ).unwrap();
1108
1109 let _grandchild_sig = network.add_node(
1110 NodeMetadata::new("/child/grandchild".to_string(), None),
1111 b"grandchild".to_vec(),
1112 Some(&child_sig),
1113 2
1114 ).unwrap();
1115
1116 let descendants = network.find_descendants_spatial(&root_sig);
1118 assert!(descendants.len() >= 2,
1119 "Expected at least 2 descendants, got {}", descendants.len());
1120 }
1121
1122 #[test]
1123 fn test_sarkar_child_distance() {
1124 let tau = constants::default_tau();
1126 let network = HyperbolicTensorNetwork::new(2, tau);
1127
1128 let root_sig = network.add_node(
1129 NodeMetadata::new("/".to_string(), None),
1130 b"root".to_vec(),
1131 None,
1132 0
1133 ).unwrap();
1134
1135 let child_sig = network.add_node(
1136 NodeMetadata::new("/child".to_string(), None),
1137 b"child".to_vec(),
1138 Some(&root_sig),
1139 1
1140 ).unwrap();
1141
1142 let root_point = network.get_point(&root_sig.unique_id()).unwrap();
1143 let child_point = network.get_point(&child_sig.unique_id()).unwrap();
1144
1145 let dist = root_point.hyperbolic_distance(&child_point);
1146 let tolerance = FixedPoint::from_int(1) / FixedPoint::from_int(100);
1147 assert!((dist - tau).abs() < tolerance,
1148 "Child should be at distance τ={} from parent, got {}", tau, dist);
1149 }
1150
1151 #[test]
1152 fn test_sarkar_sibling_separation() {
1153 let network = HyperbolicTensorNetwork::new(2, constants::default_tau());
1155
1156 let root_sig = network.add_node(
1157 NodeMetadata::new("/".to_string(), None),
1158 b"root".to_vec(),
1159 None,
1160 0
1161 ).unwrap();
1162
1163 let mut child_sigs = Vec::new();
1164 for i in 0..5 {
1165 let sig = network.add_node(
1166 NodeMetadata::new(format!("/child{}", i), None),
1167 format!("child{}", i).into_bytes(),
1168 Some(&root_sig),
1169 1
1170 ).unwrap();
1171 child_sigs.push(sig);
1172 }
1173
1174 let tau = constants::default_tau();
1176 let tolerance = FixedPoint::from_int(1) / FixedPoint::from_int(100);
1177 let root_point = network.get_point(&root_sig.unique_id()).unwrap();
1178
1179 for sig in &child_sigs {
1180 let child_point = network.get_point(&sig.unique_id()).unwrap();
1181 let dist = root_point.hyperbolic_distance(&child_point);
1182 assert!((dist - tau).abs() < tolerance,
1183 "All children should be at distance τ from parent");
1184 }
1185
1186 for i in 0..child_sigs.len() {
1188 for j in (i+1)..child_sigs.len() {
1189 let pi = network.get_point(&child_sigs[i].unique_id()).unwrap();
1190 let pj = network.get_point(&child_sigs[j].unique_id()).unwrap();
1191 let dist = pi.hyperbolic_distance(&pj);
1192 assert!(dist > constants::epsilon(),
1193 "Siblings {} and {} should be at distinct positions", i, j);
1194 }
1195 }
1196 }
1197
1198 #[test]
1199 fn test_nearest_neighbor_point_finds_self() {
1200 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_point = network.get_point(&child_sig.unique_id()).unwrap();
1219 let (nn_id, nn_dist) = network.nearest_neighbor_point(&child_point).unwrap();
1220
1221 assert_eq!(nn_id, child_sig.unique_id(),
1222 "Nearest neighbor at child's position should be child itself");
1223 let tolerance = FixedPoint::from_int(1) / FixedPoint::from_int(100);
1224 assert!(nn_dist < tolerance,
1225 "Distance to self should be ~0, got {}", nn_dist);
1226 }
1227
1228 #[test]
1229 fn test_delete_removes_node_from_index() {
1230 let network = HyperbolicTensorNetwork::new(2, constants::default_tau());
1231
1232 let root_sig = network.add_node(
1233 NodeMetadata::new("/".to_string(), None),
1234 b"root".to_vec(),
1235 None,
1236 0
1237 ).unwrap();
1238
1239 let child_sig = network.add_node(
1240 NodeMetadata::new("/child".to_string(), None),
1241 b"child".to_vec(),
1242 Some(&root_sig),
1243 1
1244 ).unwrap();
1245
1246 let child_id = child_sig.unique_id();
1247 let child_point = network.get_point(&child_id).unwrap();
1248
1249 network.unregister_node_with_parent(&child_id, Some(&root_sig.unique_id()));
1250
1251 assert!(network.get_point(&child_id).is_none(),
1252 "deleted node should leave point_map");
1253
1254 let (nn_id, _) = network.nearest_neighbor_point(&child_point).unwrap();
1257 assert_ne!(nn_id, child_id,
1258 "spatial index still returns a deleted node");
1259 }
1260
1261 #[test]
1262 fn test_semantic_distance_identical() {
1263 let coords = {
1265 let mut v = vec![0u8; 3 * 16]; let val = FixedPoint::from_f64(0.5).raw().to_le_bytes();
1267 v[0..16].copy_from_slice(&val);
1268 v[16..32].copy_from_slice(&val);
1269 v[32..48].copy_from_slice(&val);
1270 v
1271 };
1272 let dist = HyperbolicTensorNetwork::semantic_distance(&coords, &coords, &(0..3));
1273 assert!(dist < constants::epsilon(), "Distance to self should be ~0, got {}", dist);
1274 }
1275
1276 #[test]
1277 fn test_semantic_distance_known_value() {
1278 let mut a = vec![0u8; 2 * 16];
1280 let one = FixedPoint::from_f64(1.0).raw().to_le_bytes();
1281 a[0..16].copy_from_slice(&one);
1282 let b = vec![0u8; 2 * 16]; let dist = HyperbolicTensorNetwork::semantic_distance(&a, &b, &(0..2));
1287 let tolerance = FixedPoint::from_int(1) / FixedPoint::from_int(100);
1288 assert!((dist - FixedPoint::from_int(1)).abs() < tolerance,
1289 "Distance should be 1.0, got {}", dist);
1290 }
1291
1292 #[test]
1293 fn test_semantic_distance_dimensional_slice() {
1294 let mut a = vec![0u8; 2 * 16];
1296 let one = FixedPoint::from_f64(1.0).raw().to_le_bytes();
1297 a[0..16].copy_from_slice(&one); let b = vec![0u8; 2 * 16]; let dist = HyperbolicTensorNetwork::semantic_distance(&a, &b, &(1..2));
1303 assert!(dist < constants::epsilon(),
1304 "Slicing only dim 1 should give distance ~0, got {}", dist);
1305
1306 let dist_full = HyperbolicTensorNetwork::semantic_distance(&a, &b, &(0..1));
1308 let tolerance = FixedPoint::from_int(1) / FixedPoint::from_int(100);
1309 assert!((dist_full - FixedPoint::from_int(1)).abs() < tolerance,
1310 "Slicing dim 0 should give distance 1.0, got {}", dist_full);
1311 }
1312
1313 #[test]
1314 fn test_nearest_semantic_basic() {
1315 let network = HyperbolicTensorNetwork::new(2, constants::default_tau());
1316
1317 let root_sig = network.add_node(
1319 NodeMetadata::new("/".to_string(), None),
1320 b"root".to_vec(), None, 0,
1321 ).unwrap();
1322
1323 let a_sig = network.add_node(
1324 NodeMetadata::new("/a".to_string(), None),
1325 b"a".to_vec(), Some(&root_sig), 1,
1326 ).unwrap();
1327
1328 let b_sig = network.add_node(
1329 NodeMetadata::new("/b".to_string(), None),
1330 b"b".to_vec(), Some(&root_sig), 1,
1331 ).unwrap();
1332
1333 let c_sig = network.add_node(
1334 NodeMetadata::new("/c".to_string(), None),
1335 b"c".to_vec(), Some(&root_sig), 1,
1336 ).unwrap();
1337
1338 let make_coords = |d0: f64, d1: f64| -> Vec<u8> {
1340 let mut v = vec![0u8; 2 * 16];
1341 v[0..16].copy_from_slice(&FixedPoint::from_f64(d0).raw().to_le_bytes());
1342 v[16..32].copy_from_slice(&FixedPoint::from_f64(d1).raw().to_le_bytes());
1343 v
1344 };
1345
1346 network.set_node_semantic(&a_sig.unique_id(), make_coords(0.8, 0.1));
1347 network.set_node_semantic(&b_sig.unique_id(), make_coords(0.7, 0.2));
1348 network.set_node_semantic(&c_sig.unique_id(), make_coords(0.1, 0.9));
1349
1350 let query = make_coords(0.8, 0.1);
1352 let results = network.nearest_semantic(&query, 3, &(0..2));
1353
1354 assert!(!results.is_empty());
1355
1356 let first_dist = results[0].1;
1358 assert!(first_dist < FixedPoint::from_f64(0.01),
1359 "Nearest to (0.8,0.1) should be /a at ~0 distance, got {}", first_dist);
1360
1361 if results.len() >= 3 {
1363 assert!(results[2].1 > results[1].1,
1364 "Third result should be farther than second");
1365 }
1366 }
1367}