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//! - O(1) spatial lookups via the hash table's bucket structure
7//! - Exact storage of node data (metadata + raw value bytes)
8//! - Spatial index for range and nearest-neighbor queries
9
10use std::collections::HashMap;
11use std::collections::BinaryHeap;
12use std::fmt::{self, Debug, Formatter};
13use std::ops::Range;
14use std::sync::{Mutex, RwLock};
15use dashmap::DashMap;
16use g_math::fixed_point::{FixedPoint, FixedVector};
17use super::hyperbolic_geometry::{HyperbolicPoint, ratio_to_distance};
18use super::hash_table::{GeometricSignature, HyperbolicHashTable};
19use super::klein::{self, KleinPoint, PowerCell, PointLocationGrid};
20use crate::constants;
21use crate::metric_tree::EuclideanMetric;
22use crate::semantic_index::SemanticIndexCache;
23
24/// Exact metadata stored per node. Never lossy-compressed.
25#[derive(Clone, Debug)]
26pub struct NodeMetadata {
27    /// Node key (path)
28    pub key: String,
29    /// Content type
30    pub content_type: Option<String>,
31    /// User-defined metadata
32    pub metadata: HashMap<String, String>,
33    /// Creation timestamp (seconds since epoch)
34    pub created_at: u64,
35    /// Last update timestamp
36    pub updated_at: u64,
37}
38
39impl NodeMetadata {
40    /// Create new metadata with current timestamp.
41    pub fn new(key: String, content_type: Option<String>) -> Self {
42        let now = std::time::SystemTime::now()
43            .duration_since(std::time::UNIX_EPOCH)
44            .unwrap_or_default()
45            .as_secs();
46
47        Self {
48            key,
49            content_type,
50            metadata: HashMap::new(),
51            created_at: now,
52            updated_at: now,
53        }
54    }
55
56    /// Touch the updated_at timestamp.
57    pub fn touch(&mut self) {
58        self.updated_at = std::time::SystemTime::now()
59            .duration_since(std::time::UNIX_EPOCH)
60            .unwrap_or_default()
61            .as_secs();
62    }
63}
64
65/// Siblings per rainbow band: children beyond this cascade to the next
66/// concentric ring. Derived from signature quantization capacity (see
67/// `create_child_point`); 256 keeps a wide margin below the ~700-sibling
68/// single-ring collision threshold.
69pub const RAINBOW_BAND_CAPACITY: u32 = 256;
70/// Each band steps outward by τ / RAINBOW_BAND_STEP_DIV.
71const RAINBOW_BAND_STEP_DIV: i32 = 64;
72/// Warn when fan-out reaches this many bands (spacing degradation).
73const RAINBOW_BAND_WARN: u32 = 32;
74
75/// A node in the hyperbolic tensor network.
76///
77/// Stores exact metadata and raw value bytes. Despite the historical name,
78/// no compression is performed — the name is retained for API stability and
79/// will be revisited before a 1.0 release.
80#[derive(Clone, Debug)]
81pub struct CompressedNode {
82    /// Exact metadata (key, content_type, user metadata, timestamps)
83    node_metadata: NodeMetadata,
84    /// Raw value bytes (exact, no compression)
85    value: Vec<u8>,
86    /// Child node references by their geometric signatures
87    children: Vec<GeometricSignature>,
88    /// Semantic coordinates — raw Q64.64 bytes (16 bytes per dimension).
89    /// Empty if no semantic dimensions are set.
90    semantic_coords: Vec<u8>,
91}
92
93impl CompressedNode {
94    /// Create a new node.
95    pub fn new(metadata: NodeMetadata, value: Vec<u8>) -> Self {
96        Self {
97            node_metadata: metadata,
98            value,
99            children: Vec::new(),
100            semantic_coords: Vec::new(),
101        }
102    }
103
104    /// Get the node metadata.
105    pub fn metadata(&self) -> &NodeMetadata {
106        &self.node_metadata
107    }
108
109    /// Get a mutable reference to the metadata.
110    pub fn metadata_mut(&mut self) -> &mut NodeMetadata {
111        &mut self.node_metadata
112    }
113
114    /// Get the raw value bytes.
115    pub fn value(&self) -> &[u8] {
116        &self.value
117    }
118
119    /// Update the value bytes.
120    pub fn update_value(&mut self, value: Vec<u8>) {
121        self.value = value;
122        self.node_metadata.touch();
123    }
124
125    /// Add a child node reference.
126    pub fn add_child(&mut self, signature: GeometricSignature) {
127        self.children.push(signature);
128    }
129
130    /// Remove a child reference by unique id (used when the child is deleted).
131    pub fn remove_child(&mut self, unique_id: &str) {
132        self.children.retain(|sig| sig.unique_id() != unique_id);
133    }
134
135    /// Get the child node references.
136    pub fn children(&self) -> &[GeometricSignature] {
137        &self.children
138    }
139
140    /// Check if this node has any children.
141    pub fn has_children(&self) -> bool {
142        !self.children.is_empty()
143    }
144
145    /// Get the number of children.
146    pub fn child_count(&self) -> usize {
147        self.children.len()
148    }
149
150    /// Get the semantic coordinates (raw Q64.64 bytes, 16 bytes per dimension).
151    pub fn semantic_coords(&self) -> &[u8] {
152        &self.semantic_coords
153    }
154
155    /// Set the semantic coordinates (raw Q64.64 bytes).
156    pub fn set_semantic_coords(&mut self, coords: Vec<u8>) {
157        self.semantic_coords = coords;
158    }
159}
160
161/// Hyperbolic Tensor Network for tree data representation.
162///
163/// Embeds hierarchical data into the Poincaré disk model using Sarkar's
164/// cone-based construction. Each node occupies a point in hyperbolic space,
165/// with children placed at hyperbolic distance τ from their parent using
166/// Möbius reflections to preserve the tree structure as a Delaunay graph.
167///
168/// Internal maps use DashMap for lock-free concurrent reads, preparing
169/// for multi-threaded access in later phases.
170pub struct HyperbolicTensorNetwork {
171    /// Hyperbolic hash table for O(1) spatial lookups
172    hash_table: HyperbolicHashTable,
173    /// Nodes mapped by their unique_id
174    nodes: DashMap<String, CompressedNode>,
175    /// Points in the Poincaré disk for each node (keyed by unique_id)
176    point_map: DashMap<String, HyperbolicPoint>,
177    /// Root node signature (Mutex: set once during root insert)
178    root_signature: Mutex<Option<GeometricSignature>>,
179    /// Sarkar embedding scale factor: parent-child hyperbolic distance
180    tau: FixedPoint,
181    /// Per-parent child count for Sarkar cone angular placement
182    child_counts: DashMap<String, u32>,
183    /// Klein model points: unique_id → KleinPoint (Nielsen power diagram)
184    klein_points: DashMap<String, KleinPoint>,
185    /// Power cells: unique_id → PowerCell (Voronoi regions in Klein model)
186    power_cells: DashMap<String, PowerCell>,
187    /// Point location grid for O(1) nearest-neighbor queries (RwLock: brief write hold on insert/delete)
188    point_location: RwLock<PointLocationGrid>,
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    /// Default grid resolution for the point location grid.
196    const DEFAULT_GRID_RESOLUTION: usize = 64;
197
198    /// Create a new hyperbolic tensor network with the given Sarkar scale factor τ.
199    pub fn new(dimension: usize, tau: FixedPoint) -> Self {
200        Self::with_grid_resolution(dimension, tau, Self::DEFAULT_GRID_RESOLUTION)
201    }
202
203    /// Create a new network with a specific grid resolution.
204    pub fn with_grid_resolution(dimension: usize, tau: FixedPoint, grid_resolution: usize) -> Self {
205        // Semantic coordinates and persisted geometry assume a 16-byte Q64.64
206        // FixedPoint. GMATH_PROFILE is a build-time env var, so a rebuild under
207        // a different profile would silently reinterpret those bytes — fail
208        // loudly instead.
209        assert_eq!(
210            FixedPoint::raw_byte_len(), 16,
211            "horon-engine requires the 16-byte Q64.64 g_math profile (GMATH_PROFILE=embedded); \
212             rebuild with the correct profile"
213        );
214
215        let hash_table = HyperbolicHashTable::new(dimension);
216
217        Self {
218            hash_table,
219            nodes: DashMap::new(),
220            point_map: DashMap::new(),
221            root_signature: Mutex::new(None),
222            tau,
223            child_counts: DashMap::new(),
224            klein_points: DashMap::new(),
225            power_cells: DashMap::new(),
226            point_location: RwLock::new(PointLocationGrid::with_dimension(grid_resolution, dimension)),
227            semantic_index: SemanticIndexCache::new(),
228        }
229    }
230
231    /// Add a node to the DashMap without computing geometric embedding.
232    ///
233    /// Creates a key-derived unique_id for path_map/id_to_path lookups.
234    /// Semantic queries (nearest_semantic, neighbors_semantic, get/set)
235    /// work normally. Spatial queries (nearest, neighbors) will not
236    /// find this node until it is embedded.
237    pub fn add_node_data_only(&self, metadata: NodeMetadata, value: Vec<u8>, _level: u32) -> String {
238        use sha3::{Sha3_256, Digest as _};
239        let mut hasher = Sha3_256::new();
240        hasher.update(b"data_only:");
241        hasher.update(metadata.key.as_bytes());
242        let unique_id = hex::encode(&hasher.finalize()[..16]);
243
244        let node = CompressedNode::new(metadata, value);
245        self.nodes.insert(unique_id.clone(), node);
246        // Fresh nodes carry no semantic coords, but bump anyway: free
247        // insurance against future insert-with-coords paths.
248        self.semantic_index.bump();
249        unique_id
250    }
251
252    /// Add a node to the tensor network.
253    ///
254    /// Computes a position in the Poincaré disk using Sarkar's cone construction:
255    /// children are placed at hyperbolic distance τ from their parent, at
256    /// golden-angle-spaced angles in the parent's reflected frame.
257    pub fn add_node(&self,
258                    metadata: NodeMetadata,
259                    value: Vec<u8>,
260                    parent_signature: Option<&GeometricSignature>,
261                    level: u32) -> Option<GeometricSignature> {
262        self.add_node_inner(metadata, value, parent_signature, level, None)
263    }
264
265    /// Add a node with an explicit child_index for deterministic Sarkar reconstruction.
266    ///
267    /// Used during snapshot replay: the stored child_index ensures the node gets
268    /// the same geometric position regardless of replay order.
269    pub fn add_node_positioned(&self,
270                    metadata: NodeMetadata,
271                    value: Vec<u8>,
272                    parent_signature: Option<&GeometricSignature>,
273                    level: u32,
274                    child_index: u32) -> Option<GeometricSignature> {
275        self.add_node_inner(metadata, value, parent_signature, level, Some(child_index))
276    }
277
278    fn add_node_inner(&self,
279                    metadata: NodeMetadata,
280                    value: Vec<u8>,
281                    parent_signature: Option<&GeometricSignature>,
282                    level: u32,
283                    child_index_hint: Option<u32>) -> Option<GeometricSignature> {
284        // Q64.64 precision supports depth ≈ 44/τ before sibling separation
285        // degrades near the disk boundary. Warn as inserts approach the
286        // budget rather than silently losing angular precision.
287        if self.tau > FixedPoint::from_int(0) {
288            let depth_budget = (FixedPoint::from_int(44) / self.tau).to_int() as u32;
289            if level.saturating_mul(10) >= depth_budget.saturating_mul(9) {
290                log::warn!(
291                    "insert '{}' at depth {} approaches the Q64.64 precision budget (~{} levels at tau={}); sibling positions may lose separation",
292                    metadata.key, level, depth_budget, self.tau.to_f64()
293                );
294            }
295        }
296        // Resolve the child index and geometric point WITHOUT yet advancing
297        // the parent's sibling counter — the counter is committed only after
298        // the insert is known to succeed (below), so a refused collision or
299        // any other early return can never leave a gap in the index sequence
300        // (which would make placement depend on transient failed inserts).
301        //
302        // The requested slot may already be occupied by a DIFFERENT key, and
303        // that is not always a precision failure. Not every insert path feeds
304        // the sibling counter: data-only nodes (`add_node_data_only`) and
305        // ancestors auto-created during replay take positions without
306        // reserving an index, so a counter-assigned index and an index
307        // recorded in `_child_index` can name the same point. Whichever node
308        // is replayed second then lands on an occupied slot.
309        //
310        // Refusing outright made the file unopenable — the failure surfaced as
311        // `Failed to add node to tensor network` on reopen, after a
312        // `put_data_only` + `compact()`, with no way to recover the data. So
313        // probe forward for the next free slot instead. A file whose slots do
314        // not collide is unaffected: the first probe succeeds, placement is
315        // unchanged, and on-disk geometry stays bit-identical.
316        const MAX_PROBE: u32 = 1024;
317        let mut probe = child_index_hint;
318        let mut resolved = None;
319
320        for _ in 0..MAX_PROBE {
321            let (point, child_index) = match parent_signature {
322                Some(parent_sig) => self.compute_child_placement(parent_sig, probe),
323                None => (self.hash_table.poincare_disk().origin(), 0),
324            };
325
326            let signature = self.hash_table.create_signature(&point, level)?;
327            let unique_id = signature.unique_id();
328
329            // Re-inserting the same key is an update, not a collision.
330            let taken_by_other = self
331                .nodes
332                .get(&unique_id)
333                .map(|existing| existing.metadata().key != metadata.key)
334                .unwrap_or(false);
335
336            if !taken_by_other {
337                resolved = Some((point, child_index, signature, unique_id));
338                break;
339            }
340
341            // The root has exactly one slot (the origin); there is nothing to
342            // probe, and a genuine clash there is a real error.
343            if parent_signature.is_none() {
344                break;
345            }
346            probe = Some(child_index.saturating_add(1));
347        }
348
349        let Some((point, child_index, signature, unique_id)) = resolved else {
350            log::error!(
351                "could not place '{}': no free sibling slot within {} probes — \
352                 precision budget exceeded (depth/fan-out); insert refused",
353                metadata.key, MAX_PROBE
354            );
355            return None;
356        };
357
358        // Commit the sibling-counter advance now that the insert will succeed.
359        // Callers hold the parent stripe lock, so peek-then-commit is atomic
360        // with respect to other inserts under the same parent.
361        if let Some(parent_sig) = parent_signature {
362            // Commit against the slot actually taken, not the one requested —
363            // they differ when the probe above had to step past an occupied
364            // position. `commit_child_index` takes the max, so for a
365            // first-probe hit this is identical to the previous behaviour.
366            self.commit_child_index(&parent_sig.unique_id(), Some(child_index), child_index);
367        }
368
369        let node = CompressedNode::new(metadata, value);
370        self.nodes.insert(unique_id.clone(), node);
371        // Fresh nodes carry no semantic coords, but bump anyway: free
372        // insurance against future insert-with-coords paths.
373        self.semantic_index.bump();
374
375        // Store child_index in metadata for deterministic snapshot reconstruction
376        if let Some(mut node_ref) = self.nodes.get_mut(&unique_id) {
377            node_ref.metadata_mut().metadata.insert(
378                "_child_index".to_string(),
379                child_index.to_string(),
380            );
381        }
382        self.point_map.insert(unique_id.clone(), point.clone());
383
384        // Register in spatial index (VP-tree), reusing the bucket hash from
385        // create_signature to skip a second find_bucket call
386        self.hash_table.register_node_with_hint(&point, &unique_id, level, Some(signature.hash()));
387
388        // --- Nielsen power diagram maintenance ---
389        let klein_pt = klein::poincare_to_klein(&point);
390        self.klein_points.insert(unique_id.clone(), klein_pt.clone());
391
392        if let Some(parent_sig) = parent_signature {
393            let parent_id = parent_sig.unique_id();
394
395            // Compute bisectors between new leaf and parent
396            if let Some(parent_klein) = self.klein_points.get(&parent_id).map(|r| r.value().clone()) {
397                // Half-plane for leaf's cell: toward parent
398                let hp_leaf = klein::compute_bisector(&klein_pt, &parent_klein, &parent_id);
399                // Half-plane for parent's cell: toward leaf (shrinks parent)
400                let hp_parent = klein::compute_bisector(&parent_klein, &klein_pt, &unique_id);
401
402                // Create leaf's power cell (1 neighbor: parent)
403                self.power_cells.insert(unique_id.clone(), PowerCell {
404                    node_id: unique_id.clone(),
405                    site: klein_pt.clone(),
406                    half_planes: vec![hp_leaf],
407                });
408
409                // Add half-plane to parent's cell (shrinks it)
410                if let Some(mut parent_cell) = self.power_cells.get_mut(&parent_id) {
411                    parent_cell.half_planes.push(hp_parent);
412                }
413
414                // Update point location grid: carve new leaf's region from parent
415                self.point_location.write().unwrap_or_else(|e| e.into_inner()).update_insert(&parent_id, &unique_id, &klein_pt, &parent_klein);
416            }
417        } else {
418            // Root node: cell covers entire disk (no half-planes)
419            self.power_cells.insert(unique_id.clone(), PowerCell {
420                node_id: unique_id.clone(),
421                site: klein_pt.clone(),
422                half_planes: Vec::new(),
423            });
424
425            // Build initial grid with just the root
426            let sites: Vec<(String, KleinPoint)> = vec![
427                (unique_id.clone(), klein_pt.clone()),
428            ];
429            self.point_location.write().unwrap_or_else(|e| e.into_inner()).build(&sites);
430        }
431        // --- End power diagram maintenance ---
432
433        if parent_signature.is_none() {
434            let mut root = self.root_signature.lock().unwrap_or_else(|e| e.into_inner());
435            if root.is_none() {
436                *root = Some(signature.clone());
437            }
438        }
439
440        if let Some(parent_sig) = parent_signature {
441            if let Some(mut parent_node) = self.nodes.get_mut(&parent_sig.unique_id()) {
442                parent_node.add_child(signature.clone());
443            }
444        }
445
446        Some(signature)
447    }
448
449    /// Place a child node using Sarkar's cone construction.
450    ///
451    /// 1. Look up parent's position in the Poincaré disk
452    /// 2. Compute child angle: child_count × golden_angle (irrational spacing)
453    /// 3. Create child at origin frame: (r·cos θ, r·sin θ, 0, …) where r = tanh(τ/2)
454    /// 4. Möbius-reflect from origin to parent's position
455    ///
456    /// This produces embeddings where the tree IS its own Delaunay triangulation
457    /// (Sarkar 2011), with (1+ε) distance distortion for any tree.
458    /// Resolve the child index and Poincaré-disk point for a new child of
459    /// `parent_signature`, **without** mutating the parent's sibling counter.
460    ///
461    /// The returned index is what the child *would* receive; the caller
462    /// commits the counter advance via [`Self::commit_child_index`] once the
463    /// insert is certain to succeed. Splitting peek from commit keeps the
464    /// sibling-index sequence gap-free across refused/failed inserts, which
465    /// is what makes placement independent of transient failures.
466    fn compute_child_placement(&self, parent_signature: &GeometricSignature, child_index_hint: Option<u32>) -> (HyperbolicPoint, u32) {
467        let parent_id = parent_signature.unique_id();
468        let dimension = self.hash_table.poincare_disk().dimension();
469
470        // Get parent position (root is at origin)
471        let parent_point = self.point_map.get(&parent_id)
472            .map(|r| r.value().clone())
473            .unwrap_or_else(|| HyperbolicPoint::origin(dimension));
474
475        // Peek the child index: explicit hint (snapshot replay) or the current
476        // auto-increment counter. No mutation here.
477        let child_index = child_index_hint
478            .unwrap_or_else(|| self.child_counts.get(&parent_id).map(|r| *r.value()).unwrap_or(0));
479
480        // Rainbow bands: siblings fill concentric rings instead of exhausting
481        // one circle. Band 0 sits at the classic Sarkar distance τ —
482        // bit-identical to the historical placement, so existing trees keep
483        // their exact geometry. Each full band cascades outward by τ/64:
484        // angular quantization capacity is renewed per ring, so fan-out is
485        // collision-free by construction rather than guarded by warnings.
486        // The angle sequence runs continuously across bands (a discretized
487        // Vogel/phyllotaxis spiral).
488        //
489        // Capacity math: signatures quantize positions to 1e-3 cells; at
490        // ring radius tanh(τ/2) the golden-angle minimum chord falls below a
491        // cell around ~700 siblings (earlier for deep parents, which Möbius
492        // reflection compresses). 256 leaves a wide margin; the τ/64 radial
493        // step keeps adjacent bands ~6 cells apart.
494        let band = child_index / RAINBOW_BAND_CAPACITY;
495        if band >= RAINBOW_BAND_WARN {
496            log::warn!(
497                "parent of child '{}' reached rainbow band {} ({}+ siblings): placement \
498                 remains collision-free but subtree spacing is degrading — consider restructuring",
499                child_index, band, child_index
500            );
501        }
502        let effective_tau = self.tau
503            + self.tau * FixedPoint::from_int(band as i32)
504                / FixedPoint::from_int(RAINBOW_BAND_STEP_DIV);
505        let half_tau = effective_tau / FixedPoint::from_int(2);
506        let r = half_tau.tanh();
507
508        // Child angle: golden angle spacing ensures no clustering regardless of child count
509        let angle = FixedPoint::from_int(child_index as i32) * constants::golden_angle();
510
511        // Build child position in the origin frame
512        let mut child_at_origin = FixedVector::new(dimension);
513        if dimension >= 2 {
514            let (sin_a, cos_a) = angle.sincos();
515            child_at_origin[0] = r * cos_a;
516            child_at_origin[1] = r * sin_a;
517            // Higher dimensions stay at zero — children lie in a 2D geodesic submanifold
518        } else {
519            // 1D: alternate left/right
520            child_at_origin[0] = if child_index % 2 == 0 { r } else { -r };
521        }
522        let child_point = HyperbolicPoint::new(child_at_origin);
523
524        // Möbius-reflect from origin to parent's position
525        (child_point.reflect_from_origin(&parent_point), child_index)
526    }
527
528    /// Advance the parent's sibling counter after a successful insert.
529    ///
530    /// For an auto-increment insert this bumps the counter past `child_index`;
531    /// for a hinted (snapshot-replay) insert it tracks the running maximum so
532    /// later auto-increment inserts never collide with a replayed index.
533    fn commit_child_index(&self, parent_id: &str, child_index_hint: Option<u32>, child_index: u32) {
534        let next = match child_index_hint {
535            Some(hint) => {
536                let current = self.child_counts.get(parent_id).map(|r| *r.value()).unwrap_or(0);
537                current.max(hint + 1)
538            }
539            None => child_index + 1,
540        };
541        self.child_counts.insert(parent_id.to_string(), next);
542    }
543
544    /// Get a node by its signature (returns cloned value).
545    pub fn get_node_by_signature(&self, signature: &GeometricSignature) -> Option<CompressedNode> {
546        self.nodes.get(&signature.unique_id()).map(|r| r.value().clone())
547    }
548
549    /// Update the value of a node by its unique_id.
550    pub fn update_node_value(&self, unique_id: &str, value: Vec<u8>) -> bool {
551        if let Some(mut node) = self.nodes.get_mut(unique_id) {
552            node.update_value(value);
553            true
554        } else {
555            false
556        }
557    }
558
559    /// Set a metadata key-value pair on a node by its unique_id.
560    pub fn set_node_metadata_entry(&self, unique_id: &str, key: &str, val: &str) -> bool {
561        if let Some(mut node) = self.nodes.get_mut(unique_id) {
562            node.metadata_mut().metadata.insert(key.to_string(), val.to_string());
563            true
564        } else {
565            false
566        }
567    }
568
569    /// Set semantic coordinates on a node by its unique_id.
570    pub fn set_node_semantic(&self, unique_id: &str, coords: Vec<u8>) -> bool {
571        if let Some(mut node) = self.nodes.get_mut(unique_id) {
572            node.set_semantic_coords(coords);
573            drop(node); // release the shard before the epoch bump
574            // Mutation first, then bump: a builder that pre-read the old
575            // epoch tags its tree stale (see semantic_index.rs).
576            self.semantic_index.bump();
577            true
578        } else {
579            false
580        }
581    }
582
583    /// Get semantic coordinates for a node by its unique_id.
584    pub fn get_node_semantic(&self, unique_id: &str) -> Option<Vec<u8>> {
585        self.nodes.get(unique_id).map(|node| node.semantic_coords().to_vec())
586    }
587
588    /// Get the root node (cloned).
589    pub fn root_node(&self) -> Option<CompressedNode> {
590        let root = self.root_signature.lock().unwrap_or_else(|e| e.into_inner());
591        root.as_ref().and_then(|sig| {
592            self.get_node_by_signature(sig)
593        })
594    }
595
596    /// Get the root node signature (cloned).
597    pub fn root_signature(&self) -> Option<GeometricSignature> {
598        self.root_signature.lock().unwrap_or_else(|e| e.into_inner()).clone()
599    }
600
601    /// Get the children of a node by its signature (cloned).
602    pub fn children_of(&self, signature: &GeometricSignature) -> Vec<CompressedNode> {
603        let node = match self.nodes.get(&signature.unique_id()) {
604            Some(r) => r.value().clone(),
605            None => return Vec::new(),
606        };
607
608        let mut children = Vec::new();
609        for child_sig in node.children() {
610            if let Some(child) = self.nodes.get(&child_sig.unique_id()) {
611                children.push(child.value().clone());
612            }
613        }
614
615        children
616    }
617
618    /// Get the hyperbolic point for a node (cloned).
619    pub fn get_point(&self, unique_id: &str) -> Option<HyperbolicPoint> {
620        self.point_map.get(unique_id).map(|r| r.value().clone())
621    }
622
623    /// Monotone counter of semantic-relevant mutations (coordinate writes,
624    /// inserts, deletes). External caches — like the semantic disk's
625    /// derived-position index — use it exactly as the internal per-slice
626    /// cache does: tag on build, rebuild when it has advanced.
627    pub fn semantic_epoch(&self) -> u64 {
628        self.semantic_index.epoch()
629    }
630
631    /// Get the hyperbolic hash table.
632    pub fn hash_table(&self) -> &HyperbolicHashTable {
633        &self.hash_table
634    }
635
636    /// Get the number of nodes in the network.
637    pub fn node_count(&self) -> usize {
638        self.nodes.len()
639    }
640
641    /// Remove a node-map entry that has NO geometric registration — the
642    /// data-only entry retired by `embed_existing` after its embedded
643    /// replacement went live under a new signature-derived id. Not for
644    /// embedded nodes: those need [`Self::unregister_node_with_parent`].
645    pub fn remove_detached_node(&self, unique_id: &str) {
646        self.nodes.remove(unique_id);
647        // The candidate set changed shape (old id gone): invalidate the semantic index.
648        self.semantic_index.bump();
649    }
650
651    /// Unregister a node from the spatial index (for deletion).
652    ///
653    /// Prefer [`Self::unregister_node_with_parent`] when the parent is known:
654    /// data-only nodes have no power cell, so the parent cannot always be
655    /// derived here, and the parent's child list must drop the deleted node.
656    pub fn unregister_node(&self, unique_id: &str) {
657        self.unregister_node_with_parent(unique_id, None)
658    }
659
660    /// Unregister a node, removing it from the node map and from its parent's
661    /// child list. `parent_uid` is used when the parent cannot be derived from
662    /// the power diagram (e.g. data-only nodes).
663    pub fn unregister_node_with_parent(&self, unique_id: &str, parent_uid: Option<&str>) {
664        // --- Klein/power diagram cleanup ---
665        // Find the parent by checking which cell has this node as a neighbor
666        let parent_id: Option<String> = self.power_cells.get(unique_id)
667            .and_then(|cell| cell.half_planes.first().map(|hp| hp.neighbor_id.clone()));
668
669        if let Some(ref pid) = parent_id {
670            // Remove the half-plane from parent's cell that points to this node
671            if let Some(mut parent_cell) = self.power_cells.get_mut(pid) {
672                parent_cell.half_planes.retain(|hp| hp.neighbor_id != unique_id);
673            }
674
675            // Reassign grid tiles from deleted node to parent
676            self.point_location.write().unwrap_or_else(|e| e.into_inner()).update_delete(unique_id, pid);
677        }
678
679        self.klein_points.remove(unique_id);
680        self.power_cells.remove(unique_id);
681        self.child_counts.remove(unique_id);
682        // --- End Klein cleanup ---
683
684        self.hash_table.unregister_node(unique_id);
685        self.point_map.remove(unique_id);
686
687        // Remove the node itself — a ghost entry would keep serving stale
688        // semantic coordinates to nearest_semantic and permanently fail
689        // validate()'s nodes↔point_map invariant.
690        self.nodes.remove(unique_id);
691        // Deletion changes the semantic candidate set: invalidate the semantic index.
692        self.semantic_index.bump();
693
694        // Drop the deleted node from its parent's child list.
695        if let Some(pid) = parent_uid.map(str::to_string).or(parent_id) {
696            if let Some(mut parent_node) = self.nodes.get_mut(&pid) {
697                parent_node.remove_child(unique_id);
698            }
699        }
700
701        // If the root itself was deleted, clear the root signature.
702        let mut root = self.root_signature.lock().unwrap_or_else(|e| e.into_inner());
703        if root.as_ref().map(|s| s.unique_id()).as_deref() == Some(unique_id) {
704            *root = None;
705        }
706    }
707
708    /// Find all descendants of a node using the spatial index.
709    ///
710    /// Uses the parent's stored point + a τ-based radius to find all nodes
711    /// within the Sarkar cone. Radius = 3·τ covers ~3 levels of descendants.
712    pub fn find_descendants_spatial(&self, signature: &GeometricSignature) -> Vec<(String, FixedPoint)> {
713        let unique_id = signature.unique_id();
714        let point = match self.point_map.get(&unique_id) {
715            Some(r) => r.value().clone(),
716            None => return Vec::new(),
717        };
718
719        // Subtree radius: 3·τ — covers descendants within 3 levels of the Sarkar cone
720        let subtree_radius = FixedPoint::from_int(3) * self.tau;
721
722        self.hash_table.find_nodes_in_radius(&point, subtree_radius)
723            .into_iter()
724            .filter(|(uid, _)| *uid != unique_id)
725            .collect()
726    }
727
728    /// Find the nearest node to an arbitrary point using the power diagram grid.
729    ///
730    /// Algorithm:
731    /// 1. Convert Poincaré → Klein coordinates: O(d)
732    /// 2. Grid lookup for candidate: O(1)
733    /// 3. Power-distance pre-filter: rank all tree neighbors by pd (~24ns each)
734    /// 4. Hyperbolic distance verification: only top-K candidates (~59µs each)
735    /// 5. Return true nearest
736    ///
737    /// Uses hyperbolic_ratio (~200ns) for all comparisons instead of
738    /// hyperbolic_distance (~62µs), computing the full distance only once
739    /// for the final winner. For high-degree nodes, power distance pre-filter
740    /// ranks neighbors at ~24ns each, then ratio-verifies top-K.
741    ///
742    /// Falls back to VP-tree KNN if grid misses.
743    pub fn nearest_neighbor_point(&self, query_poincare: &HyperbolicPoint) -> Option<(String, FixedPoint)> {
744        if self.klein_points.is_empty() {
745            return None;
746        }
747
748        // Max neighbors to verify with hyperbolic_ratio after power-distance pre-filter
749        const VERIFY_K: usize = 5;
750
751        let query_klein = klein::poincare_to_klein(query_poincare);
752
753        // O(1) grid lookup (read lock — brief, shared with other readers)
754        let grid_candidate = self.point_location.read().unwrap_or_else(|e| e.into_inner()).query(&query_klein.coords).map(|s| s.to_string());
755        if let Some(candidate_id) = grid_candidate.as_deref() {
756            // Clone the power cell data we need to avoid holding DashMap guards
757            let cell_data = self.power_cells.get(candidate_id).map(|r| r.value().clone());
758
759            if let Some(cell) = cell_data {
760                let n_neighbors = cell.half_planes.len();
761
762                if n_neighbors <= VERIFY_K {
763                    // Few neighbors: verify all with cheap hyperbolic_ratio
764                    let mut best_id = candidate_id.to_string();
765                    let mut best_ratio = self.point_map.get(candidate_id)
766                        .map(|p| query_poincare.hyperbolic_ratio(p.value()))
767                        .unwrap_or(FixedPoint::from_int(1));
768
769                    for hp in &cell.half_planes {
770                        if let Some(neighbor_point) = self.point_map.get(&hp.neighbor_id) {
771                            let r = query_poincare.hyperbolic_ratio(neighbor_point.value());
772                            if r < best_ratio {
773                                best_ratio = r;
774                                best_id = hp.neighbor_id.clone();
775                            }
776                        }
777                    }
778
779                    // The winning ratio already determines the exact distance
780                    // (d = 2·atanh(ratio)); no second lookup or sentinel needed,
781                    // and this stays correct even if the winner's point is
782                    // concurrently removed after selection.
783                    return Some((best_id, ratio_to_distance(best_ratio)));
784                }
785
786                // Many neighbors: power-distance pre-filter (O(d_max × 24ns))
787                let candidate_klein = self.klein_points.get(candidate_id).map(|r| r.value().clone());
788                let mut pd_ranked: Vec<(String, FixedPoint)> = Vec::with_capacity(n_neighbors + 1);
789
790                if let Some(ref ck) = candidate_klein {
791                    pd_ranked.push((candidate_id.to_string(), klein::power_distance(&query_klein.coords, ck)));
792                }
793
794                for hp in &cell.half_planes {
795                    if let Some(nk) = self.klein_points.get(&hp.neighbor_id) {
796                        pd_ranked.push((hp.neighbor_id.clone(), klein::power_distance(&query_klein.coords, nk.value())));
797                    }
798                }
799
800                pd_ranked.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
801
802                // Verify top-K with cheap hyperbolic_ratio
803                let mut best_id = String::new();
804                let mut best_ratio = FixedPoint::from_int(1);
805
806                for (id, _pd) in pd_ranked.iter().take(VERIFY_K) {
807                    if let Some(point) = self.point_map.get(id) {
808                        let r = query_poincare.hyperbolic_ratio(point.value());
809                        if r < best_ratio {
810                            best_ratio = r;
811                            best_id = id.clone();
812                        }
813                    }
814                }
815
816                if !best_id.is_empty() {
817                    // Exact distance from the winning ratio (see above).
818                    return Some((best_id, ratio_to_distance(best_ratio)));
819                }
820            } else {
821                if let Some(candidate_point) = self.point_map.get(candidate_id) {
822                    let dist = query_poincare.hyperbolic_distance(candidate_point.value());
823                    return Some((candidate_id.to_string(), dist));
824                }
825            }
826        }
827
828        // Fallback: use VP-tree KNN (O(log n) per bucket)
829        let results = self.hash_table.find_nearest_nodes(query_poincare, 1);
830        if let Some((id, dist)) = results.into_iter().next() {
831            return Some((id, dist));
832        }
833
834        None
835    }
836
837    /// Find the k nearest stored nodes to an arbitrary Poincaré disk point.
838    ///
839    /// Combines the Nielsen grid candidate + its neighbors with the VP-tree
840    /// fallback to produce k results sorted by ascending hyperbolic distance.
841    pub fn nearest_neighbor_point_k(&self, query_poincare: &HyperbolicPoint, k: usize) -> Vec<(String, FixedPoint)> {
842        if self.klein_points.is_empty() || k == 0 {
843            return Vec::new();
844        }
845
846        let query_klein = klein::poincare_to_klein(query_poincare);
847
848        // Collect candidates from grid cell + neighbors (ratio-scored)
849        let mut candidates: Vec<(String, FixedPoint)> = Vec::new();
850
851        let grid_candidate = self.point_location.read().unwrap_or_else(|e| e.into_inner())
852            .query(&query_klein.coords).map(|s| s.to_string());
853
854        if let Some(candidate_id) = grid_candidate.as_deref() {
855            // Add grid candidate itself
856            if let Some(point) = self.point_map.get(candidate_id) {
857                let dist = query_poincare.hyperbolic_distance(point.value());
858                candidates.push((candidate_id.to_string(), dist));
859            }
860
861            // Add all its Voronoi neighbors
862            if let Some(cell) = self.power_cells.get(candidate_id).map(|r| r.value().clone()) {
863                for hp in &cell.half_planes {
864                    if let Some(point) = self.point_map.get(&hp.neighbor_id) {
865                        let dist = query_poincare.hyperbolic_distance(point.value());
866                        candidates.push((hp.neighbor_id.clone(), dist));
867                    }
868                }
869            }
870        }
871
872        // Merge with VP-tree results to cover gaps the grid may miss
873        let vp_results = self.hash_table.find_nearest_nodes(query_poincare, k + candidates.len());
874        for (id, dist) in vp_results {
875            if !candidates.iter().any(|(cid, _)| cid == &id) {
876                candidates.push((id, dist));
877            }
878        }
879
880        candidates.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
881        candidates.truncate(k);
882        candidates
883    }
884
885    /// Get the Klein point for a node by unique_id (cloned).
886    pub fn get_klein_point(&self, unique_id: &str) -> Option<KleinPoint> {
887        self.klein_points.get(unique_id).map(|r| r.value().clone())
888    }
889
890    /// Get the power cell for a node by unique_id (cloned).
891    pub fn get_power_cell(&self, unique_id: &str) -> Option<PowerCell> {
892        self.power_cells.get(unique_id).map(|r| r.value().clone())
893    }
894
895    /// Get the number of assigned tiles in the point location grid.
896    pub fn grid_assigned_tile_count(&self) -> usize {
897        self.point_location.read().unwrap_or_else(|e| e.into_inner()).assigned_tile_count()
898    }
899
900    // -----------------------------------------------------------------------
901    // Semantic dimensional distance queries
902    // -----------------------------------------------------------------------
903
904    /// Compute Euclidean distance between two semantic coordinate vectors
905    /// across a dimensional slice (specified dimension range).
906    ///
907    /// Each dimension is 16 bytes (i128 LE, Q64.64 fixed-point).
908    /// Dimensions outside the vectors are treated as zero.
909    ///
910    /// Uses gMath's fused kernel: differences, squares, and the accumulator
911    /// all live at the compute tier, so the sum cannot wrap the way a
912    /// storage-tier Q64.64 accumulator would for large coordinates or many
913    /// dimensions.
914    pub fn semantic_distance(
915        coords_a: &[u8],
916        coords_b: &[u8],
917        dim_range: &Range<usize>,
918    ) -> FixedPoint {
919        let a = Self::decode_semantic_slice(coords_a, dim_range);
920        let b = Self::decode_semantic_slice(coords_b, dim_range);
921        g_math::fixed_point::imperative::fused::euclidean_distance(&a, &b)
922    }
923
924    /// Decode a dimension slice of a raw Q64.64 coordinate vector.
925    ///
926    /// Dimensions beyond the end of `coords` decode as zero — short vectors
927    /// are zero-extended, matching [`Self::semantic_distance`] semantics.
928    pub fn decode_semantic_slice(coords: &[u8], dim_range: &Range<usize>) -> Vec<FixedPoint> {
929        dim_range
930            .clone()
931            .map(|dim| {
932                let start = dim * 16;
933                let end = start + 16;
934                if coords.len() >= end {
935                    FixedPoint::from_raw(i128::from_le_bytes(
936                        coords[start..end].try_into().unwrap(),
937                    ))
938                } else {
939                    FixedPoint::from_int(0)
940                }
941            })
942            .collect()
943    }
944
945    /// Find the k nearest nodes by Euclidean distance in semantic dimension space.
946    ///
947    /// `query_coords`: raw Q64.64 byte vector representing the query point.
948    /// `k`: number of nearest neighbors to return.
949    /// `dim_range`: which semantic dimensions to compare (the "dimensional slice").
950    ///
951    /// Returns `Vec<(key, distance)>` sorted ascending by `(distance, key)` —
952    /// ties break deterministically by the user-visible node key, both for
953    /// ordering and for which ties survive the k-boundary.
954    ///
955    /// Routing (`docs/SEMANTIC_INDEX.md`): stores below
956    /// [`constants::SEMANTIC_INDEX_MIN_NODES`] use the brute-force scan;
957    /// larger stores query a lazily built per-`dim_range` VP-tree, rebuilt
958    /// when the semantic epoch has advanced (any coord write, insert, or
959    /// delete). Warm-index queries are O(log n) expected on low-dimensional
960    /// slices; the first query for a slice after a mutation pays the
961    /// O(n log n) build. Results are identical to the scan path.
962    pub fn nearest_semantic(
963        &self,
964        query_coords: &[u8],
965        k: usize,
966        dim_range: &Range<usize>,
967    ) -> Vec<(String, FixedPoint)> {
968        if k == 0 {
969            return Vec::new();
970        }
971
972        if self.nodes.len() < constants::SEMANTIC_INDEX_MIN_NODES {
973            return self.nearest_semantic_scan(query_coords, k, dim_range);
974        }
975
976        let query = Self::decode_semantic_slice(query_coords, dim_range);
977        let index = self.semantic_index.get_or_build(dim_range, || {
978            self.nodes
979                .iter()
980                .filter(|entry| !entry.value().semantic_coords().is_empty())
981                .map(|entry| {
982                    (
983                        entry.value().metadata().key.clone(),
984                        Self::decode_semantic_slice(entry.value().semantic_coords(), dim_range),
985                    )
986                })
987                .collect()
988        });
989        index.tree.knn(&query, k, &EuclideanMetric)
990    }
991
992    /// Reference brute-force path for [`Self::nearest_semantic`]:
993    /// O(n × d) scan over every node with semantic coordinates.
994    ///
995    /// Same ordering contract as the indexed path — ascending
996    /// `(distance, key)`. Public so tests and benchmarks can compare
997    /// the two paths directly; prefer `nearest_semantic`, which picks.
998    pub fn nearest_semantic_scan(
999        &self,
1000        query_coords: &[u8],
1001        k: usize,
1002        dim_range: &Range<usize>,
1003    ) -> Vec<(String, FixedPoint)> {
1004        if k == 0 {
1005            return Vec::new();
1006        }
1007
1008        // Max-heap of size k on (distance, key): the peek is the current
1009        // worst candidate under the same total order the index uses, so
1010        // ties at the k-boundary break by key on both paths.
1011        let mut heap: BinaryHeap<(FixedPoint, String)> = BinaryHeap::new();
1012
1013        for entry in self.nodes.iter() {
1014            let coords = entry.value().semantic_coords();
1015
1016            // Skip nodes with no semantic coordinates
1017            if coords.is_empty() {
1018                continue;
1019            }
1020
1021            let dist = Self::semantic_distance(query_coords, coords, dim_range);
1022            let key = entry.value().metadata().key.as_str();
1023
1024            if heap.len() < k {
1025                heap.push((dist, key.to_string()));
1026            } else if let Some(worst) = heap.peek() {
1027                if (dist, key) < (worst.0, worst.1.as_str()) {
1028                    heap.pop();
1029                    heap.push((dist, key.to_string()));
1030                }
1031            }
1032        }
1033
1034        // Extract and sort ascending by (distance, uid)
1035        let mut results: Vec<(String, FixedPoint)> = heap
1036            .into_iter()
1037            .map(|(dist, uid)| (uid, dist))
1038            .collect();
1039        results.sort_by(|a, b| a.1.cmp(&b.1).then_with(|| a.0.cmp(&b.0)));
1040        results
1041    }
1042
1043    /// Check if the network has a valid structure.
1044    ///
1045    /// Verifies structural invariants across all internal data structures:
1046    /// nodes, point_map, klein_points, power_cells, and the hash table.
1047    pub fn validate_network(&self) -> bool {
1048        if self.nodes.is_empty() {
1049            return false;
1050        }
1051
1052        let root_sig = self.root_signature.lock().unwrap_or_else(|e| e.into_inner());
1053        if root_sig.is_none() {
1054            return false;
1055        }
1056
1057        let root_id = root_sig.as_ref().unwrap().unique_id();
1058        drop(root_sig);
1059        if !self.nodes.contains_key(&root_id) {
1060            return false;
1061        }
1062
1063        // Child signatures reference existing nodes
1064        for entry in self.nodes.iter() {
1065            let node = entry.value();
1066            for child_sig in node.children() {
1067                if !self.nodes.contains_key(&child_sig.unique_id()) {
1068                    return false;
1069                }
1070            }
1071        }
1072
1073        // Every node has a point_map entry
1074        for entry in self.nodes.iter() {
1075            if !self.point_map.contains_key(entry.key()) {
1076                return false;
1077            }
1078        }
1079
1080        // point_map keys are a subset of nodes
1081        for entry in self.point_map.iter() {
1082            if !self.nodes.contains_key(entry.key()) {
1083                return false;
1084            }
1085        }
1086
1087        // klein_points ↔ nodes sync
1088        for entry in self.klein_points.iter() {
1089            if !self.nodes.contains_key(entry.key()) {
1090                return false;
1091            }
1092        }
1093
1094        // power_cells ↔ nodes sync
1095        for entry in self.power_cells.iter() {
1096            if !self.nodes.contains_key(entry.key()) {
1097                return false;
1098            }
1099        }
1100
1101        // power_cell half-plane neighbors reference existing nodes
1102        for entry in self.power_cells.iter() {
1103            for hp in &entry.value().half_planes {
1104                if !self.nodes.contains_key(&hp.neighbor_id) {
1105                    return false;
1106                }
1107            }
1108        }
1109
1110        // child_counts keys are a subset of nodes (no orphan entries)
1111        for entry in self.child_counts.iter() {
1112            if !self.nodes.contains_key(entry.key()) {
1113                return false;
1114            }
1115        }
1116
1117        true
1118    }
1119}
1120
1121impl Debug for HyperbolicTensorNetwork {
1122    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
1123        write!(f, "HyperbolicTensorNetwork(nodes={}, dimension={})",
1124               self.nodes.len(),
1125               self.hash_table.poincare_disk().dimension())
1126    }
1127}
1128
1129#[cfg(test)]
1130mod tests {
1131    use super::*;
1132
1133    #[test]
1134    fn test_compressed_node() {
1135        let metadata = NodeMetadata::new("test".to_string(), None);
1136        let value = b"Node data".to_vec();
1137
1138        let node = CompressedNode::new(metadata, value.clone());
1139
1140        assert_eq!(node.metadata().key, "test");
1141        assert_eq!(node.value(), &value[..]);
1142        assert!(!node.has_children());
1143        assert_eq!(node.child_count(), 0);
1144    }
1145
1146    #[test]
1147    fn test_tensor_network_creation() {
1148        let network = HyperbolicTensorNetwork::new(2, constants::default_tau());
1149
1150        assert_eq!(network.node_count(), 0);
1151        assert!(network.root_node().is_none());
1152    }
1153
1154    #[test]
1155    fn test_adding_nodes() {
1156        let network = HyperbolicTensorNetwork::new(2, constants::default_tau());
1157
1158        let root_meta = NodeMetadata::new("/".to_string(), None);
1159        let root_sig = network.add_node(
1160            root_meta,
1161            b"Root node data".to_vec(),
1162            None,
1163            0
1164        ).unwrap();
1165
1166        assert_eq!(network.node_count(), 1);
1167        assert!(network.root_node().is_some());
1168
1169        let child_meta = NodeMetadata::new("/child".to_string(), None);
1170        let child_sig = network.add_node(
1171            child_meta,
1172            b"Child node data".to_vec(),
1173            Some(&root_sig),
1174            1
1175        ).unwrap();
1176
1177        assert_eq!(network.node_count(), 2);
1178
1179        let root_node = network.get_node_by_signature(&root_sig).unwrap();
1180        assert_eq!(root_node.child_count(), 1);
1181        assert_eq!(root_node.children()[0].unique_id(), child_sig.unique_id());
1182    }
1183
1184    #[test]
1185    fn test_network_validation() {
1186        let network = HyperbolicTensorNetwork::new(2, constants::default_tau());
1187
1188        assert!(!network.validate_network());
1189
1190        let root_sig = network.add_node(
1191            NodeMetadata::new("/".to_string(), None),
1192            b"Root data".to_vec(),
1193            None,
1194            0
1195        ).unwrap();
1196
1197        assert!(network.validate_network());
1198
1199        network.add_node(
1200            NodeMetadata::new("/child1".to_string(), None),
1201            b"Child 1 data".to_vec(),
1202            Some(&root_sig),
1203            1
1204        ).unwrap();
1205
1206        network.add_node(
1207            NodeMetadata::new("/child2".to_string(), None),
1208            b"Child 2 data".to_vec(),
1209            Some(&root_sig),
1210            1
1211        ).unwrap();
1212
1213        assert!(network.validate_network());
1214    }
1215
1216    #[test]
1217    fn test_point_map() {
1218        let network = HyperbolicTensorNetwork::new(2, constants::default_tau());
1219
1220        let root_sig = network.add_node(
1221            NodeMetadata::new("/".to_string(), None),
1222            b"root".to_vec(),
1223            None,
1224            0
1225        ).unwrap();
1226
1227        // Root should be at origin
1228        let root_point = network.get_point(&root_sig.unique_id()).unwrap();
1229        assert!(root_point.euclidean_norm() < constants::epsilon());
1230
1231        let child_sig = network.add_node(
1232            NodeMetadata::new("/child".to_string(), None),
1233            b"child".to_vec(),
1234            Some(&root_sig),
1235            1
1236        ).unwrap();
1237
1238        // Child should be away from origin
1239        let child_point = network.get_point(&child_sig.unique_id()).unwrap();
1240        assert!(child_point.euclidean_norm() > constants::epsilon());
1241    }
1242
1243    #[test]
1244    fn test_spatial_descendants() {
1245        let network = HyperbolicTensorNetwork::new(2, constants::default_tau());
1246
1247        let root_sig = network.add_node(
1248            NodeMetadata::new("/".to_string(), None),
1249            b"root".to_vec(),
1250            None,
1251            0
1252        ).unwrap();
1253
1254        let child_sig = network.add_node(
1255            NodeMetadata::new("/child".to_string(), None),
1256            b"child".to_vec(),
1257            Some(&root_sig),
1258            1
1259        ).unwrap();
1260
1261        let _grandchild_sig = network.add_node(
1262            NodeMetadata::new("/child/grandchild".to_string(), None),
1263            b"grandchild".to_vec(),
1264            Some(&child_sig),
1265            2
1266        ).unwrap();
1267
1268        // Root's spatial descendants should include child and grandchild
1269        let descendants = network.find_descendants_spatial(&root_sig);
1270        assert!(descendants.len() >= 2,
1271            "Expected at least 2 descendants, got {}", descendants.len());
1272    }
1273
1274    #[test]
1275    fn test_sarkar_child_distance() {
1276        // Children should be at exactly hyperbolic distance τ from parent
1277        let tau = constants::default_tau();
1278        let network = HyperbolicTensorNetwork::new(2, tau);
1279
1280        let root_sig = network.add_node(
1281            NodeMetadata::new("/".to_string(), None),
1282            b"root".to_vec(),
1283            None,
1284            0
1285        ).unwrap();
1286
1287        let child_sig = network.add_node(
1288            NodeMetadata::new("/child".to_string(), None),
1289            b"child".to_vec(),
1290            Some(&root_sig),
1291            1
1292        ).unwrap();
1293
1294        let root_point = network.get_point(&root_sig.unique_id()).unwrap();
1295        let child_point = network.get_point(&child_sig.unique_id()).unwrap();
1296
1297        let dist = root_point.hyperbolic_distance(&child_point);
1298        let tolerance = FixedPoint::from_int(1) / FixedPoint::from_int(100);
1299        assert!((dist - tau).abs() < tolerance,
1300            "Child should be at distance τ={} from parent, got {}", tau, dist);
1301    }
1302
1303    #[test]
1304    fn test_sarkar_sibling_separation() {
1305        // Multiple children of the same parent should be at distinct positions
1306        let network = HyperbolicTensorNetwork::new(2, constants::default_tau());
1307
1308        let root_sig = network.add_node(
1309            NodeMetadata::new("/".to_string(), None),
1310            b"root".to_vec(),
1311            None,
1312            0
1313        ).unwrap();
1314
1315        let mut child_sigs = Vec::new();
1316        for i in 0..5 {
1317            let sig = network.add_node(
1318                NodeMetadata::new(format!("/child{}", i), None),
1319                format!("child{}", i).into_bytes(),
1320                Some(&root_sig),
1321                1
1322            ).unwrap();
1323            child_sigs.push(sig);
1324        }
1325
1326        // All children should be at the same distance from root
1327        let tau = constants::default_tau();
1328        let tolerance = FixedPoint::from_int(1) / FixedPoint::from_int(100);
1329        let root_point = network.get_point(&root_sig.unique_id()).unwrap();
1330
1331        for sig in &child_sigs {
1332            let child_point = network.get_point(&sig.unique_id()).unwrap();
1333            let dist = root_point.hyperbolic_distance(&child_point);
1334            assert!((dist - tau).abs() < tolerance,
1335                "All children should be at distance τ from parent");
1336        }
1337
1338        // All siblings should be pairwise distinct (non-zero distance)
1339        for i in 0..child_sigs.len() {
1340            for j in (i+1)..child_sigs.len() {
1341                let pi = network.get_point(&child_sigs[i].unique_id()).unwrap();
1342                let pj = network.get_point(&child_sigs[j].unique_id()).unwrap();
1343                let dist = pi.hyperbolic_distance(&pj);
1344                assert!(dist > constants::epsilon(),
1345                    "Siblings {} and {} should be at distinct positions", i, j);
1346            }
1347        }
1348    }
1349
1350    #[test]
1351    fn test_klein_points_created() {
1352        // Verify Klein points are created alongside Poincaré points
1353        let network = HyperbolicTensorNetwork::new(2, constants::default_tau());
1354
1355        let root_sig = network.add_node(
1356            NodeMetadata::new("/".to_string(), None),
1357            b"root".to_vec(),
1358            None,
1359            0
1360        ).unwrap();
1361
1362        let root_klein = network.get_klein_point(&root_sig.unique_id());
1363        assert!(root_klein.is_some(), "Root should have a Klein point");
1364
1365        let rk = root_klein.unwrap();
1366        // Root at origin → Klein origin, weight = 1
1367        assert!(rk.coords[0].abs() < constants::epsilon());
1368        assert!(rk.weight > constants::half());
1369
1370        let child_sig = network.add_node(
1371            NodeMetadata::new("/child".to_string(), None),
1372            b"child".to_vec(),
1373            Some(&root_sig),
1374            1
1375        ).unwrap();
1376
1377        let child_klein = network.get_klein_point(&child_sig.unique_id());
1378        assert!(child_klein.is_some(), "Child should have a Klein point");
1379        assert!(child_klein.unwrap().coords.length() > constants::epsilon(),
1380            "Child Klein point should be away from origin");
1381    }
1382
1383    #[test]
1384    fn test_power_cells_created() {
1385        let network = HyperbolicTensorNetwork::new(2, constants::default_tau());
1386
1387        let root_sig = network.add_node(
1388            NodeMetadata::new("/".to_string(), None),
1389            b"root".to_vec(),
1390            None,
1391            0
1392        ).unwrap();
1393
1394        // Root cell should have no half-planes (covers entire disk)
1395        let root_cell = network.get_power_cell(&root_sig.unique_id()).unwrap();
1396        assert!(root_cell.half_planes.is_empty(), "Root cell should have no constraints initially");
1397
1398        let child_sig = network.add_node(
1399            NodeMetadata::new("/child".to_string(), None),
1400            b"child".to_vec(),
1401            Some(&root_sig),
1402            1
1403        ).unwrap();
1404
1405        // After adding child: root should have 1 half-plane, child should have 1
1406        let root_cell = network.get_power_cell(&root_sig.unique_id()).unwrap();
1407        assert_eq!(root_cell.half_planes.len(), 1, "Root should have 1 half-plane after adding child");
1408
1409        let child_cell = network.get_power_cell(&child_sig.unique_id()).unwrap();
1410        assert_eq!(child_cell.half_planes.len(), 1, "Child (leaf) should have 1 half-plane");
1411    }
1412
1413    #[test]
1414    fn test_nearest_neighbor_point_finds_self() {
1415        // Query at a node's own position should return that node
1416        let network = HyperbolicTensorNetwork::new(2, constants::default_tau());
1417
1418        let root_sig = network.add_node(
1419            NodeMetadata::new("/".to_string(), None),
1420            b"root".to_vec(),
1421            None,
1422            0
1423        ).unwrap();
1424
1425        let child_sig = network.add_node(
1426            NodeMetadata::new("/child".to_string(), None),
1427            b"child".to_vec(),
1428            Some(&root_sig),
1429            1
1430        ).unwrap();
1431
1432        // Query at child's position should find child
1433        let child_point = network.get_point(&child_sig.unique_id()).unwrap();
1434        let (nn_id, nn_dist) = network.nearest_neighbor_point(&child_point).unwrap();
1435
1436        assert_eq!(nn_id, child_sig.unique_id(),
1437            "Nearest neighbor at child's position should be child itself");
1438        let tolerance = FixedPoint::from_int(1) / FixedPoint::from_int(100);
1439        assert!(nn_dist < tolerance,
1440            "Distance to self should be ~0, got {}", nn_dist);
1441    }
1442
1443    #[test]
1444    fn test_grid_reflects_insertions() {
1445        let network = HyperbolicTensorNetwork::new(2, constants::default_tau());
1446
1447        let root_sig = network.add_node(
1448            NodeMetadata::new("/".to_string(), None),
1449            b"root".to_vec(),
1450            None,
1451            0
1452        ).unwrap();
1453
1454        // Grid should have assigned tiles after root
1455        assert!(network.grid_assigned_tile_count() > 0, "Grid should have tiles after root insert");
1456
1457        let _child_sig = network.add_node(
1458            NodeMetadata::new("/child".to_string(), None),
1459            b"child".to_vec(),
1460            Some(&root_sig),
1461            1
1462        ).unwrap();
1463
1464        // After child insert, grid should still have assigned tiles
1465        assert!(network.grid_assigned_tile_count() > 0, "Grid should still have tiles after child insert");
1466    }
1467
1468    #[test]
1469    fn test_delete_cleans_up_klein_state() {
1470        let network = HyperbolicTensorNetwork::new(2, constants::default_tau());
1471
1472        let root_sig = network.add_node(
1473            NodeMetadata::new("/".to_string(), None),
1474            b"root".to_vec(),
1475            None,
1476            0
1477        ).unwrap();
1478
1479        let child_sig = network.add_node(
1480            NodeMetadata::new("/child".to_string(), None),
1481            b"child".to_vec(),
1482            Some(&root_sig),
1483            1
1484        ).unwrap();
1485
1486        let child_id = child_sig.unique_id();
1487
1488        // Verify Klein state exists
1489        assert!(network.get_klein_point(&child_id).is_some());
1490        assert!(network.get_power_cell(&child_id).is_some());
1491
1492        // Delete child
1493        network.unregister_node(&child_id);
1494
1495        // Klein state should be cleaned up
1496        assert!(network.get_klein_point(&child_id).is_none());
1497        assert!(network.get_power_cell(&child_id).is_none());
1498
1499        // Parent's cell should no longer have the child's half-plane
1500        let root_cell = network.get_power_cell(&root_sig.unique_id()).unwrap();
1501        assert!(root_cell.half_planes.is_empty(),
1502            "Root cell should have no half-planes after child deletion");
1503    }
1504
1505    #[test]
1506    fn test_semantic_distance_identical() {
1507        // Identical coordinates → distance = 0
1508        let coords = {
1509            let mut v = vec![0u8; 3 * 16]; // 3 dims
1510            let val = FixedPoint::from_f64(0.5).raw().to_le_bytes();
1511            v[0..16].copy_from_slice(&val);
1512            v[16..32].copy_from_slice(&val);
1513            v[32..48].copy_from_slice(&val);
1514            v
1515        };
1516        let dist = HyperbolicTensorNetwork::semantic_distance(&coords, &coords, &(0..3));
1517        assert!(dist < constants::epsilon(), "Distance to self should be ~0, got {}", dist);
1518    }
1519
1520    #[test]
1521    fn test_semantic_distance_known_value() {
1522        // dim0: (1.0, 0.0), dim1: (0.0, 0.0) → distance = 1.0
1523        let mut a = vec![0u8; 2 * 16];
1524        let one = FixedPoint::from_f64(1.0).raw().to_le_bytes();
1525        a[0..16].copy_from_slice(&one);
1526        // dim1 stays zero
1527
1528        let b = vec![0u8; 2 * 16]; // all zero
1529
1530        let dist = HyperbolicTensorNetwork::semantic_distance(&a, &b, &(0..2));
1531        let tolerance = FixedPoint::from_int(1) / FixedPoint::from_int(100);
1532        assert!((dist - FixedPoint::from_int(1)).abs() < tolerance,
1533            "Distance should be 1.0, got {}", dist);
1534    }
1535
1536    #[test]
1537    fn test_semantic_distance_dimensional_slice() {
1538        // Only compare dim 1, ignore dim 0
1539        let mut a = vec![0u8; 2 * 16];
1540        let one = FixedPoint::from_f64(1.0).raw().to_le_bytes();
1541        a[0..16].copy_from_slice(&one); // dim 0 = 1.0
1542
1543        let b = vec![0u8; 2 * 16]; // all zero
1544
1545        // Slice dim 1 only → both are 0.0 at dim 1 → distance = 0
1546        let dist = HyperbolicTensorNetwork::semantic_distance(&a, &b, &(1..2));
1547        assert!(dist < constants::epsilon(),
1548            "Slicing only dim 1 should give distance ~0, got {}", dist);
1549
1550        // Slice dim 0 only → (1.0 vs 0.0) → distance = 1.0
1551        let dist_full = HyperbolicTensorNetwork::semantic_distance(&a, &b, &(0..1));
1552        let tolerance = FixedPoint::from_int(1) / FixedPoint::from_int(100);
1553        assert!((dist_full - FixedPoint::from_int(1)).abs() < tolerance,
1554            "Slicing dim 0 should give distance 1.0, got {}", dist_full);
1555    }
1556
1557    #[test]
1558    fn test_nearest_semantic_basic() {
1559        let network = HyperbolicTensorNetwork::new(2, constants::default_tau());
1560
1561        // Add 3 nodes with semantic coords in 2 dims
1562        let root_sig = network.add_node(
1563            NodeMetadata::new("/".to_string(), None),
1564            b"root".to_vec(), None, 0,
1565        ).unwrap();
1566
1567        let a_sig = network.add_node(
1568            NodeMetadata::new("/a".to_string(), None),
1569            b"a".to_vec(), Some(&root_sig), 1,
1570        ).unwrap();
1571
1572        let b_sig = network.add_node(
1573            NodeMetadata::new("/b".to_string(), None),
1574            b"b".to_vec(), Some(&root_sig), 1,
1575        ).unwrap();
1576
1577        let c_sig = network.add_node(
1578            NodeMetadata::new("/c".to_string(), None),
1579            b"c".to_vec(), Some(&root_sig), 1,
1580        ).unwrap();
1581
1582        // Set semantic coords: /a at (0.8, 0.1), /b at (0.7, 0.2), /c at (0.1, 0.9)
1583        let make_coords = |d0: f64, d1: f64| -> Vec<u8> {
1584            let mut v = vec![0u8; 2 * 16];
1585            v[0..16].copy_from_slice(&FixedPoint::from_f64(d0).raw().to_le_bytes());
1586            v[16..32].copy_from_slice(&FixedPoint::from_f64(d1).raw().to_le_bytes());
1587            v
1588        };
1589
1590        network.set_node_semantic(&a_sig.unique_id(), make_coords(0.8, 0.1));
1591        network.set_node_semantic(&b_sig.unique_id(), make_coords(0.7, 0.2));
1592        network.set_node_semantic(&c_sig.unique_id(), make_coords(0.1, 0.9));
1593
1594        // Query near /a's position → /b should be closest, /c farthest
1595        let query = make_coords(0.8, 0.1);
1596        let results = network.nearest_semantic(&query, 3, &(0..2));
1597
1598        assert!(!results.is_empty());
1599
1600        // First result should be /a (distance ~0)
1601        let first_dist = results[0].1;
1602        assert!(first_dist < FixedPoint::from_f64(0.01),
1603            "Nearest to (0.8,0.1) should be /a at ~0 distance, got {}", first_dist);
1604
1605        // /c should be much farther than /b
1606        if results.len() >= 3 {
1607            assert!(results[2].1 > results[1].1,
1608                "Third result should be farther than second");
1609        }
1610    }
1611}