Skip to main content

horon_engine/
tensor_network.rs

1//! tensor_network.rs - Hyperbolic Tensor Network for Hierarchical Data
2//!
3//! This module implements the core data network over hyperbolic space:
4//!
5//! - Nodes embedded in the Poincaré disk with parent-child geometric relationships
6//! - Exact storage of node data (metadata + raw value bytes)
7//! - Spatial queries (nearest, k-nearest, range) delegated to `cell_index`
8
9use 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/// Exact metadata stored per node. Never lossy-compressed.
24#[derive(Clone, Debug)]
25pub struct NodeMetadata {
26    /// Node key (path)
27    pub key: String,
28    /// Content type
29    pub content_type: Option<String>,
30    /// User-defined metadata
31    pub metadata: HashMap<String, String>,
32    /// Creation timestamp (seconds since epoch)
33    pub created_at: u64,
34    /// Last update timestamp
35    pub updated_at: u64,
36}
37
38impl NodeMetadata {
39    /// Create new metadata with current timestamp.
40    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    /// Touch the updated_at timestamp.
56    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
64/// Siblings per rainbow band: children beyond this cascade to the next
65/// concentric ring. Derived from signature quantization capacity (see
66/// `create_child_point`); 256 keeps a wide margin below the ~700-sibling
67/// single-ring collision threshold.
68pub const RAINBOW_BAND_CAPACITY: u32 = 256;
69/// Each band steps outward by τ / RAINBOW_BAND_STEP_DIV.
70const RAINBOW_BAND_STEP_DIV: i32 = 64;
71/// Warn when fan-out reaches this many bands (spacing degradation).
72const RAINBOW_BAND_WARN: u32 = 32;
73
74/// A node in the hyperbolic tensor network.
75///
76/// Stores exact metadata and raw value bytes. Despite the historical name,
77/// no compression is performed — the name is retained for API stability and
78/// will be revisited before a 1.0 release.
79#[derive(Clone, Debug)]
80pub struct CompressedNode {
81    /// Exact metadata (key, content_type, user metadata, timestamps)
82    node_metadata: NodeMetadata,
83    /// Raw value bytes (exact, no compression)
84    value: Vec<u8>,
85    /// Child node references by their geometric signatures
86    children: Vec<GeometricSignature>,
87    /// Semantic coordinates — raw Q64.64 bytes (16 bytes per dimension).
88    /// Empty if no semantic dimensions are set.
89    semantic_coords: Vec<u8>,
90}
91
92impl CompressedNode {
93    /// Create a new node.
94    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    /// Get the node metadata.
104    pub fn metadata(&self) -> &NodeMetadata {
105        &self.node_metadata
106    }
107
108    /// Get a mutable reference to the metadata.
109    pub fn metadata_mut(&mut self) -> &mut NodeMetadata {
110        &mut self.node_metadata
111    }
112
113    /// Get the raw value bytes.
114    pub fn value(&self) -> &[u8] {
115        &self.value
116    }
117
118    /// Update the value bytes.
119    pub fn update_value(&mut self, value: Vec<u8>) {
120        self.value = value;
121        self.node_metadata.touch();
122    }
123
124    /// Add a child node reference.
125    pub fn add_child(&mut self, signature: GeometricSignature) {
126        self.children.push(signature);
127    }
128
129    /// Remove a child reference by unique id (used when the child is deleted).
130    pub fn remove_child(&mut self, unique_id: &str) {
131        self.children.retain(|sig| sig.unique_id() != unique_id);
132    }
133
134    /// Get the child node references.
135    pub fn children(&self) -> &[GeometricSignature] {
136        &self.children
137    }
138
139    /// Check if this node has any children.
140    pub fn has_children(&self) -> bool {
141        !self.children.is_empty()
142    }
143
144    /// Get the number of children.
145    pub fn child_count(&self) -> usize {
146        self.children.len()
147    }
148
149    /// Get the semantic coordinates (raw Q64.64 bytes, 16 bytes per dimension).
150    pub fn semantic_coords(&self) -> &[u8] {
151        &self.semantic_coords
152    }
153
154    /// Set the semantic coordinates (raw Q64.64 bytes).
155    pub fn set_semantic_coords(&mut self, coords: Vec<u8>) {
156        self.semantic_coords = coords;
157    }
158}
159
160/// Hyperbolic Tensor Network for tree data representation.
161///
162/// Embeds hierarchical data into the Poincaré disk model using Sarkar's
163/// cone-based construction. Each node occupies a point in hyperbolic space,
164/// with children placed at hyperbolic distance τ from their parent using
165/// Möbius reflections to preserve the tree structure as a Delaunay graph.
166///
167/// Internal maps use DashMap for lock-free concurrent reads, preparing
168/// for multi-threaded access in later phases.
169pub struct HyperbolicTensorNetwork {
170    /// The disk these points live in: dimension and the origin. Geometry
171    /// only — it holds no nodes.
172    poincare_disk: PoincareDisk,
173    /// The spatial index. Every spatial read is answered here, exactly, by
174    /// expanding rings until a proven lower bound rules out the rest.
175    cell_index: CellIndex,
176    /// Nodes mapped by their unique_id
177    nodes: DashMap<String, CompressedNode>,
178    /// Points in the Poincaré disk for each node (keyed by unique_id)
179    point_map: DashMap<String, HyperbolicPoint>,
180    /// Root node signature (Mutex: set once during root insert)
181    root_signature: Mutex<Option<GeometricSignature>>,
182    /// Sarkar embedding scale factor: parent-child hyperbolic distance
183    tau: FixedPoint,
184    /// Per-parent child count for Sarkar cone angular placement
185    child_counts: DashMap<String, u32>,
186    /// Largest degree for which PROOF.md's Delaunay hypothesis holds at this
187    /// tau. Precomputed: deriving it costs an `exp` and an `atan`.
188    max_degree: u32,
189    /// Lazy per-slice VP-tree cache for semantic KNN, invalidated by a
190    /// semantic-epoch counter (see `docs/SEMANTIC_INDEX.md`)
191    semantic_index: SemanticIndexCache,
192}
193
194impl HyperbolicTensorNetwork {
195    /// Create a new hyperbolic tensor network with the given Sarkar scale factor τ.
196    pub fn new(dimension: usize, tau: FixedPoint) -> Self {
197        // Semantic coordinates and persisted geometry assume a 16-byte Q64.64
198        // FixedPoint. GMATH_PROFILE is a build-time env var, so a rebuild under
199        // a different profile would silently reinterpret those bytes — fail
200        // loudly instead.
201        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    /// Add a node to the DashMap without computing geometric embedding.
221    ///
222    /// Creates a key-derived unique_id for path_map/id_to_path lookups.
223    /// Semantic queries (nearest_semantic, neighbors_semantic, get/set)
224    /// work normally. Spatial queries (nearest, neighbors) will not
225    /// find this node until it is embedded.
226    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        // Fresh nodes carry no semantic coords, but bump anyway: free
236        // insurance against future insert-with-coords paths.
237        self.semantic_index.bump();
238        unique_id
239    }
240
241    /// Add a node to the tensor network.
242    ///
243    /// Computes a position in the Poincaré disk using Sarkar's cone construction:
244    /// children are placed at hyperbolic distance τ from their parent, at
245    /// golden-angle-spaced angles in the parent's reflected frame.
246    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    /// Add a node with an explicit child_index for deterministic Sarkar reconstruction.
255    ///
256    /// Used during snapshot replay: the stored child_index ensures the node gets
257    /// the same geometric position regardless of replay order.
258    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        // Q64.64 precision supports depth ≈ 44/τ before sibling separation
274        // degrades near the disk boundary. Warn as inserts approach the
275        // budget rather than silently losing angular precision.
276        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        // Resolve the child index and geometric point WITHOUT yet advancing
286        // the parent's sibling counter — the counter is committed only after
287        // the insert is known to succeed (below), so a refused collision or
288        // any other early return can never leave a gap in the index sequence
289        // (which would make placement depend on transient failed inserts).
290        //
291        // The requested slot may already be occupied by a DIFFERENT key, and
292        // that is not always a precision failure. Not every insert path feeds
293        // the sibling counter: data-only nodes (`add_node_data_only`) and
294        // ancestors auto-created during replay take positions without
295        // reserving an index, so a counter-assigned index and an index
296        // recorded in `_child_index` can name the same point. Whichever node
297        // is replayed second then lands on an occupied slot.
298        //
299        // Refusing outright made the file unopenable — the failure surfaced as
300        // `Failed to add node to tensor network` on reopen, after a
301        // `put_data_only` + `compact()`, with no way to recover the data. So
302        // probe forward for the next free slot instead. A file whose slots do
303        // not collide is unaffected: the first probe succeeds, placement is
304        // unchanged, and on-disk geometry stays bit-identical.
305        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            // Refuse placement past the radius where the distance kernel
316            // stops being faithful. Beyond it queries do not get slower, they
317            // get wrong — every node saturates to the same distance and
318            // ranking becomes arbitrary — so this is an error, not a warning.
319            // One subtraction and a compare; see `constants::max_safe_radius`.
320            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            // Re-inserting the same key is an update, not a collision.
341            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            // The root has exactly one slot (the origin); there is nothing to
353            // probe, and a genuine clash there is a real error.
354            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        // Commit the sibling-counter advance now that the insert will succeed.
370        // Callers hold the parent stripe lock, so peek-then-commit is atomic
371        // with respect to other inserts under the same parent.
372        if let Some(parent_sig) = parent_signature {
373            // Commit against the slot actually taken, not the one requested —
374            // they differ when the probe above had to step past an occupied
375            // position. `commit_child_index` takes the max, so for a
376            // first-probe hit this is identical to the previous behaviour.
377            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        // Fresh nodes carry no semantic coords, but bump anyway: free
383        // insurance against future insert-with-coords paths.
384        self.semantic_index.bump();
385
386        // Store child_index in metadata for deterministic snapshot reconstruction
387        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    /// Place a child node using Sarkar's cone construction.
414    ///
415    /// 1. Look up parent's position in the Poincaré disk
416    /// 2. Compute child angle: child_count × golden_angle (irrational spacing)
417    /// 3. Create child at origin frame: (r·cos θ, r·sin θ, 0, …) where r = tanh(τ/2)
418    /// 4. Möbius-reflect from origin to parent's position
419    ///
420    /// This produces embeddings where the tree IS its own Delaunay triangulation
421    /// (Sarkar 2011), with (1+ε) distance distortion for any tree.
422    /// Resolve the child index and Poincaré-disk point for a new child of
423    /// `parent_signature`, **without** mutating the parent's sibling counter.
424    ///
425    /// The returned index is what the child *would* receive; the caller
426    /// commits the counter advance via [`Self::commit_child_index`] once the
427    /// insert is certain to succeed. Splitting peek from commit keeps the
428    /// sibling-index sequence gap-free across refused/failed inserts, which
429    /// is what makes placement independent of transient failures.
430    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        // Get parent position (root is at origin)
435        let parent_point = self.point_map.get(&parent_id)
436            .map(|r| r.value().clone())
437            .unwrap_or_else(|| HyperbolicPoint::origin(dimension));
438
439        // Peek the child index: explicit hint (snapshot replay) or the current
440        // auto-increment counter. No mutation here.
441        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        // Rainbow bands: siblings fill concentric rings instead of exhausting
445        // one circle. Band 0 sits at the classic Sarkar distance τ —
446        // bit-identical to the historical placement, so existing trees keep
447        // their exact geometry. Each full band cascades outward by τ/64:
448        // angular quantization capacity is renewed per ring, so fan-out is
449        // collision-free by construction rather than guarded by warnings.
450        // The angle sequence runs continuously across bands (a discretized
451        // Vogel/phyllotaxis spiral).
452        //
453        // Capacity math: signatures quantize positions to 1e-3 cells; at
454        // ring radius tanh(τ/2) the golden-angle minimum chord falls below a
455        // cell around ~700 siblings (earlier for deep parents, which Möbius
456        // reflection compresses). 256 leaves a wide margin; the τ/64 radial
457        // step keeps adjacent bands ~6 cells apart.
458        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        // Child angle: golden angle spacing ensures no clustering regardless of child count
473        let angle = FixedPoint::from_int(child_index as i32) * constants::golden_angle();
474
475        // Build child position in the origin frame
476        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            // Higher dimensions stay at zero — children lie in a 2D geodesic submanifold
482        } else {
483            // 1D: alternate left/right
484            child_at_origin[0] = if child_index % 2 == 0 { r } else { -r };
485        }
486        let child_point = HyperbolicPoint::new(child_at_origin);
487
488        // Möbius-reflect from origin to parent's position
489        (child_point.reflect_from_origin(&parent_point), child_index)
490    }
491
492    /// Advance the parent's sibling counter after a successful insert.
493    ///
494    /// For an auto-increment insert this bumps the counter past `child_index`;
495    /// for a hinted (snapshot-replay) insert it tracks the running maximum so
496    /// later auto-increment inserts never collide with a replayed index.
497    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        // `CONTRACT.scn.md` declares `tau >= -log(tan(pi / (2 * d_max)))` and
508        // nothing used to check it. Since 0.6.0 a violation costs spacing
509        // quality — crowded siblings, more nodes per cell, longer scans — and
510        // never correctness, so this is a warning rather than an error. Fired
511        // once per parent, on the crossing, so a wide tree does not spam.
512        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    /// The largest node degree for which `PROOF.md`'s Delaunay hypothesis holds
527    /// at this network's tau. Exceeding it is a spacing-quality matter, not a
528    /// correctness one — see `docs/GEOMETRY_TRACK.md`.
529    pub fn max_degree(&self) -> u32 {
530        self.max_degree
531    }
532
533    /// Get a node by its signature (returns cloned value).
534    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    /// Update the value of a node by its unique_id.
539    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    /// Set a metadata key-value pair on a node by its unique_id.
549    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    /// Set semantic coordinates on a node by its unique_id.
559    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); // release the shard before the epoch bump
563            // Mutation first, then bump: a builder that pre-read the old
564            // epoch tags its tree stale (see semantic_index.rs).
565            self.semantic_index.bump();
566            true
567        } else {
568            false
569        }
570    }
571
572    /// Get semantic coordinates for a node by its unique_id.
573    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    /// Get the root node (cloned).
578    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    /// Get the root node signature (cloned).
586    pub fn root_signature(&self) -> Option<GeometricSignature> {
587        self.root_signature.lock().unwrap_or_else(|e| e.into_inner()).clone()
588    }
589
590    /// Get the children of a node by its signature (cloned).
591    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    /// Get the hyperbolic point for a node (cloned).
608    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    /// Monotone counter of semantic-relevant mutations (coordinate writes,
613    /// inserts, deletes). External caches — like the semantic disk's
614    /// derived-position index — use it exactly as the internal per-slice
615    /// cache does: tag on build, rebuild when it has advanced.
616    pub fn semantic_epoch(&self) -> u64 {
617        self.semantic_index.epoch()
618    }
619
620    /// The Sarkar scale factor: the hyperbolic distance from any node to each
621    /// of its children.
622    pub fn tau(&self) -> FixedPoint {
623        self.tau
624    }
625
626    /// The spatial index, for tests that compare it against brute force.
627    pub fn cell_index(&self) -> &CellIndex {
628        &self.cell_index
629    }
630
631    /// Get the number of nodes in the network.
632    pub fn node_count(&self) -> usize {
633        self.nodes.len()
634    }
635
636    /// Remove a node-map entry that has NO geometric registration — the
637    /// data-only entry retired by `embed_existing` after its embedded
638    /// replacement went live under a new signature-derived id. Not for
639    /// embedded nodes: those need [`Self::unregister_node_with_parent`].
640    pub fn remove_detached_node(&self, unique_id: &str) {
641        self.nodes.remove(unique_id);
642        // The candidate set changed shape (old id gone): invalidate the semantic index.
643        self.semantic_index.bump();
644    }
645
646    /// Unregister a node from the spatial index (for deletion).
647    ///
648    /// Prefer [`Self::unregister_node_with_parent`]: the parent is not derivable
649    /// here, so the parent's child list keeps a dangling entry.
650    pub fn unregister_node(&self, unique_id: &str) {
651        self.unregister_node_with_parent(unique_id, None)
652    }
653
654    /// Unregister a node, removing it from the node map and from its parent's
655    /// child list. The caller supplies `parent_uid`; it is resolved from the
656    /// path map, which is authoritative for parentage.
657    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        // Remove the node itself — a ghost entry would keep serving stale
664        // semantic coordinates to nearest_semantic and permanently fail
665        // validate()'s nodes↔point_map invariant.
666        self.nodes.remove(unique_id);
667        // Deletion changes the semantic candidate set: invalidate the semantic index.
668        self.semantic_index.bump();
669
670        // Drop the deleted node from its parent's child list.
671        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        // If the root itself was deleted, clear the root signature.
678        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    /// Find all descendants of a node using the spatial index.
685    ///
686    /// Uses the parent's stored point + a τ-based radius to find all nodes
687    /// within the Sarkar cone. Radius = 3·τ covers ~3 levels of descendants.
688    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        // Subtree radius: 3·τ — covers descendants within 3 levels of the Sarkar cone
696        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    /// The nearest stored node to an arbitrary point, exactly.
705    ///
706    /// Delegates to the cell index: the cell is computed from the query's
707    /// coordinates, and the ring expands until a proven lower bound says
708    /// nothing closer remains. No candidate cap, no window, no count-based
709    /// stopping rule.
710    ///
711    /// **Complexity**: O(1) cell lookup plus a bounded ring. Measured on
712    /// 5 461 nodes: 1.7 cells and ~27 points scanned per k=1 query, against
713    /// 7 563 µs for the bucket layer this replaced.
714    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    /// The k nearest stored nodes to an arbitrary point, ascending by
719    /// `(distance, unique_id)`.
720    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    /// Every stored node within `radius` of `centre`, ascending by
725    /// `(distance, unique_id)`. Same expansion as `nearest_neighbor_point_k`
726    /// with a fixed threshold instead of a moving k-th distance.
727    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    // -----------------------------------------------------------------------
733    // Semantic dimensional distance queries
734    // -----------------------------------------------------------------------
735
736    /// Compute Euclidean distance between two semantic coordinate vectors
737    /// across a dimensional slice (specified dimension range).
738    ///
739    /// Each dimension is 16 bytes (i128 LE, Q64.64 fixed-point).
740    /// Dimensions outside the vectors are treated as zero.
741    ///
742    /// Uses gMath's fused kernel: differences, squares, and the accumulator
743    /// all live at the compute tier, so the sum cannot wrap the way a
744    /// storage-tier Q64.64 accumulator would for large coordinates or many
745    /// dimensions.
746    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    /// Decode a dimension slice of a raw Q64.64 coordinate vector.
757    ///
758    /// Dimensions beyond the end of `coords` decode as zero — short vectors
759    /// are zero-extended, matching [`Self::semantic_distance`] semantics.
760    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    /// Find the k nearest nodes by Euclidean distance in semantic dimension space.
778    ///
779    /// `query_coords`: raw Q64.64 byte vector representing the query point.
780    /// `k`: number of nearest neighbors to return.
781    /// `dim_range`: which semantic dimensions to compare (the "dimensional slice").
782    ///
783    /// Returns `Vec<(key, distance)>` sorted ascending by `(distance, key)` —
784    /// ties break deterministically by the user-visible node key, both for
785    /// ordering and for which ties survive the k-boundary.
786    ///
787    /// Routing (`docs/SEMANTIC_INDEX.md`): stores below
788    /// [`constants::SEMANTIC_INDEX_MIN_NODES`] use the brute-force scan;
789    /// larger stores query a lazily built per-`dim_range` VP-tree, rebuilt
790    /// when the semantic epoch has advanced (any coord write, insert, or
791    /// delete). Warm-index queries are O(log n) expected on low-dimensional
792    /// slices; the first query for a slice after a mutation pays the
793    /// O(n log n) build. Results are identical to the scan path.
794    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    /// Reference brute-force path for [`Self::nearest_semantic`]:
825    /// O(n × d) scan over every node with semantic coordinates.
826    ///
827    /// Same ordering contract as the indexed path — ascending
828    /// `(distance, key)`. Public so tests and benchmarks can compare
829    /// the two paths directly; prefer `nearest_semantic`, which picks.
830    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        // Max-heap of size k on (distance, key): the peek is the current
841        // worst candidate under the same total order the index uses, so
842        // ties at the k-boundary break by key on both paths.
843        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            // Skip nodes with no semantic coordinates
849            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        // Extract and sort ascending by (distance, uid)
867        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    /// Check if the network has a valid structure.
876    ///
877    /// Verifies structural invariants across all internal data structures:
878    /// nodes, point_map, child_counts, and the spatial index.
879    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        // Child signatures reference existing nodes
896        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        // Every node has a point_map entry
906        for entry in self.nodes.iter() {
907            if !self.point_map.contains_key(entry.key()) {
908                return false;
909            }
910        }
911
912        // point_map keys are a subset of nodes
913        for entry in self.point_map.iter() {
914            if !self.nodes.contains_key(entry.key()) {
915                return false;
916            }
917        }
918
919        // child_counts keys are a subset of nodes (no orphan entries)
920        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    /// **Functional** integrity: can the spatial index actually answer a query
930    /// about what it holds?
931    ///
932    /// Every other check in this file is
933    /// *referential* — it asks whether these maps point at things that exist.
934    /// A structure can pass all of them and still be unable to find anything,
935    /// which is exactly what happened: the bucket layer was referentially
936    /// perfect while `nearest` returned the wrong node for 25 of 42 nodes in a
937    /// deep tree. No check asked it to locate a node it had itself indexed.
938    ///
939    /// This one does. `point_map` is the authority on where a node is; the
940    /// index is derived from it. Querying at a node's own stored position must
941    /// return that node, because distance 0 is the global minimum of a metric
942    /// — an expected answer known without any oracle.
943    ///
944    /// Ties are respected: several nodes may share a position, so the check is
945    /// that *something* at distance zero comes back, not that a particular id
946    /// does.
947    ///
948    /// O(n) queries, so it is a diagnostic rather than a hot path.
949    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                // Distance is zero for the node itself, or for anything
954                // embedded at the same position.
955                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        // Root should be at origin
1076        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        // Child should be away from origin
1087        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        // Root's spatial descendants should include child and grandchild
1117        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        // Children should be at exactly hyperbolic distance τ from parent
1125        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        // Multiple children of the same parent should be at distinct positions
1154        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        // All children should be at the same distance from root
1175        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        // All siblings should be pairwise distinct (non-zero distance)
1187        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        // Query at a node's own position should return that node
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        // Query at child's position should find child
1218        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        // And the spatial index must stop returning it: querying at the
1255        // deleted node's own position may only find the root now.
1256        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        // Identical coordinates → distance = 0
1264        let coords = {
1265            let mut v = vec![0u8; 3 * 16]; // 3 dims
1266            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        // dim0: (1.0, 0.0), dim1: (0.0, 0.0) → distance = 1.0
1279        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        // dim1 stays zero
1283
1284        let b = vec![0u8; 2 * 16]; // all zero
1285
1286        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        // Only compare dim 1, ignore dim 0
1295        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); // dim 0 = 1.0
1298
1299        let b = vec![0u8; 2 * 16]; // all zero
1300
1301        // Slice dim 1 only → both are 0.0 at dim 1 → distance = 0
1302        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        // Slice dim 0 only → (1.0 vs 0.0) → distance = 1.0
1307        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        // Add 3 nodes with semantic coords in 2 dims
1318        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        // Set semantic coords: /a at (0.8, 0.1), /b at (0.7, 0.2), /c at (0.1, 0.9)
1339        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        // Query near /a's position → /b should be closest, /c farthest
1351        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        // First result should be /a (distance ~0)
1357        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        // /c should be much farther than /b
1362        if results.len() >= 3 {
1363            assert!(results[2].1 > results[1].1,
1364                "Third result should be farther than second");
1365        }
1366    }
1367}