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.
729    ///
730    /// The point-location grid **proposes**; hyperbolic distance **decides**.
731    /// That split is load-bearing: the grid stores one owner per tile, so a
732    /// node whose power cell is smaller than a tile is unnameable by it, and
733    /// Sarkar placement drives cells below tile size within a few levels.
734    /// Treating a grid hit as the answer therefore returns a confidently
735    /// wrong node — including for a query sitting exactly on a node's own
736    /// position. The VP-tree is consulted on every query, not only when the
737    /// grid misses, so a grid *mistake* is recoverable and not just a grid
738    /// *miss*.
739    ///
740    /// Algorithm:
741    /// 1. Convert Poincaré → Klein coordinates: O(d)
742    /// 2. Grid lookup, O(1), yielding a cell generator and its tree
743    ///    neighbours — for high-degree nodes, ranked by power distance
744    ///    (~24ns each) and truncated to `VERIFY_K`
745    /// 3. Add the VP-tree's own candidate: O(log n)
746    /// 4. Score every candidate with `hyperbolic_ratio` (measured 23.9µs) and
747    ///    convert the winner once via `ratio_to_distance`, so the costlier
748    ///    `hyperbolic_distance` (measured 37.5µs) is never paid per-candidate
749    ///
750    /// **Complexity**: O(log n), set by the VP-tree consultation in step 3.
751    /// The grid keeps the candidate set small and constant; it does not make
752    /// the query O(1), and it never did — see the tile-resolution note above.
753    ///
754    /// **Cost**: ~2.0ms per query, of which ~1.95ms is
755    /// [`HyperbolicHashTable::find_nearest_nodes`] taking a full
756    /// `hyperbolic_distance` to every bucket centre (≈52 buckets × 37.5µs)
757    /// purely to order buckets for its pruning bound. That scan, not this
758    /// function, is the optimization target; `nearest_neighbor_point_k` has
759    /// always paid it, so exactness here costs what exactness already cost
760    /// there.
761    pub fn nearest_neighbor_point(&self, query_poincare: &HyperbolicPoint) -> Option<(String, FixedPoint)> {
762        if self.klein_points.is_empty() {
763            return None;
764        }
765
766        // Max tree neighbors to carry forward from a high-degree grid cell.
767        const VERIFY_K: usize = 5;
768
769        let query_klein = klein::poincare_to_klein(query_poincare);
770
771        // --- Propose ---
772        let mut candidates: Vec<String> = Vec::with_capacity(VERIFY_K + 2);
773
774        let grid_candidate = self.point_location.read().unwrap_or_else(|e| e.into_inner()).query(&query_klein.coords).map(|s| s.to_string());
775        if let Some(candidate_id) = grid_candidate {
776            match self.power_cells.get(&candidate_id).map(|r| r.value().clone()) {
777                Some(cell) if cell.half_planes.len() > VERIFY_K => {
778                    // High degree: rank by power distance before truncating,
779                    // so the VERIFY_K we keep are the plausible ones.
780                    let mut pd_ranked: Vec<(String, FixedPoint)> =
781                        Vec::with_capacity(cell.half_planes.len() + 1);
782                    if let Some(ck) = self.klein_points.get(&candidate_id) {
783                        pd_ranked.push((
784                            candidate_id.clone(),
785                            klein::power_distance(&query_klein.coords, ck.value()),
786                        ));
787                    }
788                    for hp in &cell.half_planes {
789                        if let Some(nk) = self.klein_points.get(&hp.neighbor_id) {
790                            pd_ranked.push((
791                                hp.neighbor_id.clone(),
792                                klein::power_distance(&query_klein.coords, nk.value()),
793                            ));
794                        }
795                    }
796                    pd_ranked.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
797                    candidates.extend(pd_ranked.into_iter().take(VERIFY_K).map(|(id, _)| id));
798                }
799                Some(cell) => {
800                    candidates.push(candidate_id);
801                    candidates.extend(cell.half_planes.iter().map(|hp| hp.neighbor_id.clone()));
802                }
803                None => candidates.push(candidate_id),
804            }
805        }
806
807        // Always, not only on a grid miss: the grid can name the wrong cell,
808        // and its generator's tree neighbours need not include the true
809        // nearest node.
810        candidates.extend(
811            self.hash_table
812                .find_nearest_nodes(query_poincare, 1)
813                .into_iter()
814                .map(|(id, _)| id),
815        );
816
817        // --- Decide ---
818        // `hyperbolic_ratio` is monotone in hyperbolic distance, so the
819        // argmin is the true nearest among the candidates. The winning ratio
820        // already determines the exact distance (d = 2·atanh(ratio)), which
821        // stays correct even if the winner is concurrently removed.
822        let mut best: Option<(String, FixedPoint)> = None;
823        for id in candidates {
824            let Some(point) = self.point_map.get(&id) else { continue };
825            let ratio = query_poincare.hyperbolic_ratio(point.value());
826            if best.as_ref().map_or(true, |(_, incumbent)| ratio < *incumbent) {
827                best = Some((id, ratio));
828            }
829        }
830        best.map(|(id, ratio)| (id, ratio_to_distance(ratio)))
831    }
832
833    /// Find the k nearest stored nodes to an arbitrary Poincaré disk point.
834    ///
835    /// Combines the Nielsen grid candidate + its neighbors with the VP-tree
836    /// fallback to produce k results sorted by ascending hyperbolic distance.
837    pub fn nearest_neighbor_point_k(&self, query_poincare: &HyperbolicPoint, k: usize) -> Vec<(String, FixedPoint)> {
838        if self.klein_points.is_empty() || k == 0 {
839            return Vec::new();
840        }
841
842        let query_klein = klein::poincare_to_klein(query_poincare);
843
844        // Collect candidates from grid cell + neighbors (ratio-scored)
845        let mut candidates: Vec<(String, FixedPoint)> = Vec::new();
846
847        let grid_candidate = self.point_location.read().unwrap_or_else(|e| e.into_inner())
848            .query(&query_klein.coords).map(|s| s.to_string());
849
850        if let Some(candidate_id) = grid_candidate.as_deref() {
851            // Add grid candidate itself
852            if let Some(point) = self.point_map.get(candidate_id) {
853                let dist = query_poincare.hyperbolic_distance(point.value());
854                candidates.push((candidate_id.to_string(), dist));
855            }
856
857            // Add all its Voronoi neighbors
858            if let Some(cell) = self.power_cells.get(candidate_id).map(|r| r.value().clone()) {
859                for hp in &cell.half_planes {
860                    if let Some(point) = self.point_map.get(&hp.neighbor_id) {
861                        let dist = query_poincare.hyperbolic_distance(point.value());
862                        candidates.push((hp.neighbor_id.clone(), dist));
863                    }
864                }
865            }
866        }
867
868        // Merge with VP-tree results to cover gaps the grid may miss
869        let vp_results = self.hash_table.find_nearest_nodes(query_poincare, k + candidates.len());
870        for (id, dist) in vp_results {
871            if !candidates.iter().any(|(cid, _)| cid == &id) {
872                candidates.push((id, dist));
873            }
874        }
875
876        candidates.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
877        candidates.truncate(k);
878        candidates
879    }
880
881    /// Get the Klein point for a node by unique_id (cloned).
882    pub fn get_klein_point(&self, unique_id: &str) -> Option<KleinPoint> {
883        self.klein_points.get(unique_id).map(|r| r.value().clone())
884    }
885
886    /// Get the power cell for a node by unique_id (cloned).
887    pub fn get_power_cell(&self, unique_id: &str) -> Option<PowerCell> {
888        self.power_cells.get(unique_id).map(|r| r.value().clone())
889    }
890
891    /// Get the number of assigned tiles in the point location grid.
892    pub fn grid_assigned_tile_count(&self) -> usize {
893        self.point_location.read().unwrap_or_else(|e| e.into_inner()).assigned_tile_count()
894    }
895
896    // -----------------------------------------------------------------------
897    // Semantic dimensional distance queries
898    // -----------------------------------------------------------------------
899
900    /// Compute Euclidean distance between two semantic coordinate vectors
901    /// across a dimensional slice (specified dimension range).
902    ///
903    /// Each dimension is 16 bytes (i128 LE, Q64.64 fixed-point).
904    /// Dimensions outside the vectors are treated as zero.
905    ///
906    /// Uses gMath's fused kernel: differences, squares, and the accumulator
907    /// all live at the compute tier, so the sum cannot wrap the way a
908    /// storage-tier Q64.64 accumulator would for large coordinates or many
909    /// dimensions.
910    pub fn semantic_distance(
911        coords_a: &[u8],
912        coords_b: &[u8],
913        dim_range: &Range<usize>,
914    ) -> FixedPoint {
915        let a = Self::decode_semantic_slice(coords_a, dim_range);
916        let b = Self::decode_semantic_slice(coords_b, dim_range);
917        g_math::fixed_point::imperative::fused::euclidean_distance(&a, &b)
918    }
919
920    /// Decode a dimension slice of a raw Q64.64 coordinate vector.
921    ///
922    /// Dimensions beyond the end of `coords` decode as zero — short vectors
923    /// are zero-extended, matching [`Self::semantic_distance`] semantics.
924    pub fn decode_semantic_slice(coords: &[u8], dim_range: &Range<usize>) -> Vec<FixedPoint> {
925        dim_range
926            .clone()
927            .map(|dim| {
928                let start = dim * 16;
929                let end = start + 16;
930                if coords.len() >= end {
931                    FixedPoint::from_raw(i128::from_le_bytes(
932                        coords[start..end].try_into().unwrap(),
933                    ))
934                } else {
935                    FixedPoint::from_int(0)
936                }
937            })
938            .collect()
939    }
940
941    /// Find the k nearest nodes by Euclidean distance in semantic dimension space.
942    ///
943    /// `query_coords`: raw Q64.64 byte vector representing the query point.
944    /// `k`: number of nearest neighbors to return.
945    /// `dim_range`: which semantic dimensions to compare (the "dimensional slice").
946    ///
947    /// Returns `Vec<(key, distance)>` sorted ascending by `(distance, key)` —
948    /// ties break deterministically by the user-visible node key, both for
949    /// ordering and for which ties survive the k-boundary.
950    ///
951    /// Routing (`docs/SEMANTIC_INDEX.md`): stores below
952    /// [`constants::SEMANTIC_INDEX_MIN_NODES`] use the brute-force scan;
953    /// larger stores query a lazily built per-`dim_range` VP-tree, rebuilt
954    /// when the semantic epoch has advanced (any coord write, insert, or
955    /// delete). Warm-index queries are O(log n) expected on low-dimensional
956    /// slices; the first query for a slice after a mutation pays the
957    /// O(n log n) build. Results are identical to the scan path.
958    pub fn nearest_semantic(
959        &self,
960        query_coords: &[u8],
961        k: usize,
962        dim_range: &Range<usize>,
963    ) -> Vec<(String, FixedPoint)> {
964        if k == 0 {
965            return Vec::new();
966        }
967
968        if self.nodes.len() < constants::SEMANTIC_INDEX_MIN_NODES {
969            return self.nearest_semantic_scan(query_coords, k, dim_range);
970        }
971
972        let query = Self::decode_semantic_slice(query_coords, dim_range);
973        let index = self.semantic_index.get_or_build(dim_range, || {
974            self.nodes
975                .iter()
976                .filter(|entry| !entry.value().semantic_coords().is_empty())
977                .map(|entry| {
978                    (
979                        entry.value().metadata().key.clone(),
980                        Self::decode_semantic_slice(entry.value().semantic_coords(), dim_range),
981                    )
982                })
983                .collect()
984        });
985        index.tree.knn(&query, k, &EuclideanMetric)
986    }
987
988    /// Reference brute-force path for [`Self::nearest_semantic`]:
989    /// O(n × d) scan over every node with semantic coordinates.
990    ///
991    /// Same ordering contract as the indexed path — ascending
992    /// `(distance, key)`. Public so tests and benchmarks can compare
993    /// the two paths directly; prefer `nearest_semantic`, which picks.
994    pub fn nearest_semantic_scan(
995        &self,
996        query_coords: &[u8],
997        k: usize,
998        dim_range: &Range<usize>,
999    ) -> Vec<(String, FixedPoint)> {
1000        if k == 0 {
1001            return Vec::new();
1002        }
1003
1004        // Max-heap of size k on (distance, key): the peek is the current
1005        // worst candidate under the same total order the index uses, so
1006        // ties at the k-boundary break by key on both paths.
1007        let mut heap: BinaryHeap<(FixedPoint, String)> = BinaryHeap::new();
1008
1009        for entry in self.nodes.iter() {
1010            let coords = entry.value().semantic_coords();
1011
1012            // Skip nodes with no semantic coordinates
1013            if coords.is_empty() {
1014                continue;
1015            }
1016
1017            let dist = Self::semantic_distance(query_coords, coords, dim_range);
1018            let key = entry.value().metadata().key.as_str();
1019
1020            if heap.len() < k {
1021                heap.push((dist, key.to_string()));
1022            } else if let Some(worst) = heap.peek() {
1023                if (dist, key) < (worst.0, worst.1.as_str()) {
1024                    heap.pop();
1025                    heap.push((dist, key.to_string()));
1026                }
1027            }
1028        }
1029
1030        // Extract and sort ascending by (distance, uid)
1031        let mut results: Vec<(String, FixedPoint)> = heap
1032            .into_iter()
1033            .map(|(dist, uid)| (uid, dist))
1034            .collect();
1035        results.sort_by(|a, b| a.1.cmp(&b.1).then_with(|| a.0.cmp(&b.0)));
1036        results
1037    }
1038
1039    /// Check if the network has a valid structure.
1040    ///
1041    /// Verifies structural invariants across all internal data structures:
1042    /// nodes, point_map, klein_points, power_cells, and the hash table.
1043    pub fn validate_network(&self) -> bool {
1044        if self.nodes.is_empty() {
1045            return false;
1046        }
1047
1048        let root_sig = self.root_signature.lock().unwrap_or_else(|e| e.into_inner());
1049        if root_sig.is_none() {
1050            return false;
1051        }
1052
1053        let root_id = root_sig.as_ref().unwrap().unique_id();
1054        drop(root_sig);
1055        if !self.nodes.contains_key(&root_id) {
1056            return false;
1057        }
1058
1059        // Child signatures reference existing nodes
1060        for entry in self.nodes.iter() {
1061            let node = entry.value();
1062            for child_sig in node.children() {
1063                if !self.nodes.contains_key(&child_sig.unique_id()) {
1064                    return false;
1065                }
1066            }
1067        }
1068
1069        // Every node has a point_map entry
1070        for entry in self.nodes.iter() {
1071            if !self.point_map.contains_key(entry.key()) {
1072                return false;
1073            }
1074        }
1075
1076        // point_map keys are a subset of nodes
1077        for entry in self.point_map.iter() {
1078            if !self.nodes.contains_key(entry.key()) {
1079                return false;
1080            }
1081        }
1082
1083        // klein_points ↔ nodes sync
1084        for entry in self.klein_points.iter() {
1085            if !self.nodes.contains_key(entry.key()) {
1086                return false;
1087            }
1088        }
1089
1090        // power_cells ↔ nodes sync
1091        for entry in self.power_cells.iter() {
1092            if !self.nodes.contains_key(entry.key()) {
1093                return false;
1094            }
1095        }
1096
1097        // power_cell half-plane neighbors reference existing nodes
1098        for entry in self.power_cells.iter() {
1099            for hp in &entry.value().half_planes {
1100                if !self.nodes.contains_key(&hp.neighbor_id) {
1101                    return false;
1102                }
1103            }
1104        }
1105
1106        // child_counts keys are a subset of nodes (no orphan entries)
1107        for entry in self.child_counts.iter() {
1108            if !self.nodes.contains_key(entry.key()) {
1109                return false;
1110            }
1111        }
1112
1113        true
1114    }
1115}
1116
1117impl Debug for HyperbolicTensorNetwork {
1118    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
1119        write!(f, "HyperbolicTensorNetwork(nodes={}, dimension={})",
1120               self.nodes.len(),
1121               self.hash_table.poincare_disk().dimension())
1122    }
1123}
1124
1125#[cfg(test)]
1126mod tests {
1127    use super::*;
1128
1129    #[test]
1130    fn test_compressed_node() {
1131        let metadata = NodeMetadata::new("test".to_string(), None);
1132        let value = b"Node data".to_vec();
1133
1134        let node = CompressedNode::new(metadata, value.clone());
1135
1136        assert_eq!(node.metadata().key, "test");
1137        assert_eq!(node.value(), &value[..]);
1138        assert!(!node.has_children());
1139        assert_eq!(node.child_count(), 0);
1140    }
1141
1142    #[test]
1143    fn test_tensor_network_creation() {
1144        let network = HyperbolicTensorNetwork::new(2, constants::default_tau());
1145
1146        assert_eq!(network.node_count(), 0);
1147        assert!(network.root_node().is_none());
1148    }
1149
1150    #[test]
1151    fn test_adding_nodes() {
1152        let network = HyperbolicTensorNetwork::new(2, constants::default_tau());
1153
1154        let root_meta = NodeMetadata::new("/".to_string(), None);
1155        let root_sig = network.add_node(
1156            root_meta,
1157            b"Root node data".to_vec(),
1158            None,
1159            0
1160        ).unwrap();
1161
1162        assert_eq!(network.node_count(), 1);
1163        assert!(network.root_node().is_some());
1164
1165        let child_meta = NodeMetadata::new("/child".to_string(), None);
1166        let child_sig = network.add_node(
1167            child_meta,
1168            b"Child node data".to_vec(),
1169            Some(&root_sig),
1170            1
1171        ).unwrap();
1172
1173        assert_eq!(network.node_count(), 2);
1174
1175        let root_node = network.get_node_by_signature(&root_sig).unwrap();
1176        assert_eq!(root_node.child_count(), 1);
1177        assert_eq!(root_node.children()[0].unique_id(), child_sig.unique_id());
1178    }
1179
1180    #[test]
1181    fn test_network_validation() {
1182        let network = HyperbolicTensorNetwork::new(2, constants::default_tau());
1183
1184        assert!(!network.validate_network());
1185
1186        let root_sig = network.add_node(
1187            NodeMetadata::new("/".to_string(), None),
1188            b"Root data".to_vec(),
1189            None,
1190            0
1191        ).unwrap();
1192
1193        assert!(network.validate_network());
1194
1195        network.add_node(
1196            NodeMetadata::new("/child1".to_string(), None),
1197            b"Child 1 data".to_vec(),
1198            Some(&root_sig),
1199            1
1200        ).unwrap();
1201
1202        network.add_node(
1203            NodeMetadata::new("/child2".to_string(), None),
1204            b"Child 2 data".to_vec(),
1205            Some(&root_sig),
1206            1
1207        ).unwrap();
1208
1209        assert!(network.validate_network());
1210    }
1211
1212    #[test]
1213    fn test_point_map() {
1214        let network = HyperbolicTensorNetwork::new(2, constants::default_tau());
1215
1216        let root_sig = network.add_node(
1217            NodeMetadata::new("/".to_string(), None),
1218            b"root".to_vec(),
1219            None,
1220            0
1221        ).unwrap();
1222
1223        // Root should be at origin
1224        let root_point = network.get_point(&root_sig.unique_id()).unwrap();
1225        assert!(root_point.euclidean_norm() < constants::epsilon());
1226
1227        let child_sig = network.add_node(
1228            NodeMetadata::new("/child".to_string(), None),
1229            b"child".to_vec(),
1230            Some(&root_sig),
1231            1
1232        ).unwrap();
1233
1234        // Child should be away from origin
1235        let child_point = network.get_point(&child_sig.unique_id()).unwrap();
1236        assert!(child_point.euclidean_norm() > constants::epsilon());
1237    }
1238
1239    #[test]
1240    fn test_spatial_descendants() {
1241        let network = HyperbolicTensorNetwork::new(2, constants::default_tau());
1242
1243        let root_sig = network.add_node(
1244            NodeMetadata::new("/".to_string(), None),
1245            b"root".to_vec(),
1246            None,
1247            0
1248        ).unwrap();
1249
1250        let child_sig = network.add_node(
1251            NodeMetadata::new("/child".to_string(), None),
1252            b"child".to_vec(),
1253            Some(&root_sig),
1254            1
1255        ).unwrap();
1256
1257        let _grandchild_sig = network.add_node(
1258            NodeMetadata::new("/child/grandchild".to_string(), None),
1259            b"grandchild".to_vec(),
1260            Some(&child_sig),
1261            2
1262        ).unwrap();
1263
1264        // Root's spatial descendants should include child and grandchild
1265        let descendants = network.find_descendants_spatial(&root_sig);
1266        assert!(descendants.len() >= 2,
1267            "Expected at least 2 descendants, got {}", descendants.len());
1268    }
1269
1270    #[test]
1271    fn test_sarkar_child_distance() {
1272        // Children should be at exactly hyperbolic distance τ from parent
1273        let tau = constants::default_tau();
1274        let network = HyperbolicTensorNetwork::new(2, tau);
1275
1276        let root_sig = network.add_node(
1277            NodeMetadata::new("/".to_string(), None),
1278            b"root".to_vec(),
1279            None,
1280            0
1281        ).unwrap();
1282
1283        let child_sig = network.add_node(
1284            NodeMetadata::new("/child".to_string(), None),
1285            b"child".to_vec(),
1286            Some(&root_sig),
1287            1
1288        ).unwrap();
1289
1290        let root_point = network.get_point(&root_sig.unique_id()).unwrap();
1291        let child_point = network.get_point(&child_sig.unique_id()).unwrap();
1292
1293        let dist = root_point.hyperbolic_distance(&child_point);
1294        let tolerance = FixedPoint::from_int(1) / FixedPoint::from_int(100);
1295        assert!((dist - tau).abs() < tolerance,
1296            "Child should be at distance τ={} from parent, got {}", tau, dist);
1297    }
1298
1299    #[test]
1300    fn test_sarkar_sibling_separation() {
1301        // Multiple children of the same parent should be at distinct positions
1302        let network = HyperbolicTensorNetwork::new(2, constants::default_tau());
1303
1304        let root_sig = network.add_node(
1305            NodeMetadata::new("/".to_string(), None),
1306            b"root".to_vec(),
1307            None,
1308            0
1309        ).unwrap();
1310
1311        let mut child_sigs = Vec::new();
1312        for i in 0..5 {
1313            let sig = network.add_node(
1314                NodeMetadata::new(format!("/child{}", i), None),
1315                format!("child{}", i).into_bytes(),
1316                Some(&root_sig),
1317                1
1318            ).unwrap();
1319            child_sigs.push(sig);
1320        }
1321
1322        // All children should be at the same distance from root
1323        let tau = constants::default_tau();
1324        let tolerance = FixedPoint::from_int(1) / FixedPoint::from_int(100);
1325        let root_point = network.get_point(&root_sig.unique_id()).unwrap();
1326
1327        for sig in &child_sigs {
1328            let child_point = network.get_point(&sig.unique_id()).unwrap();
1329            let dist = root_point.hyperbolic_distance(&child_point);
1330            assert!((dist - tau).abs() < tolerance,
1331                "All children should be at distance τ from parent");
1332        }
1333
1334        // All siblings should be pairwise distinct (non-zero distance)
1335        for i in 0..child_sigs.len() {
1336            for j in (i+1)..child_sigs.len() {
1337                let pi = network.get_point(&child_sigs[i].unique_id()).unwrap();
1338                let pj = network.get_point(&child_sigs[j].unique_id()).unwrap();
1339                let dist = pi.hyperbolic_distance(&pj);
1340                assert!(dist > constants::epsilon(),
1341                    "Siblings {} and {} should be at distinct positions", i, j);
1342            }
1343        }
1344    }
1345
1346    #[test]
1347    fn test_klein_points_created() {
1348        // Verify Klein points are created alongside Poincaré points
1349        let network = HyperbolicTensorNetwork::new(2, constants::default_tau());
1350
1351        let root_sig = network.add_node(
1352            NodeMetadata::new("/".to_string(), None),
1353            b"root".to_vec(),
1354            None,
1355            0
1356        ).unwrap();
1357
1358        let root_klein = network.get_klein_point(&root_sig.unique_id());
1359        assert!(root_klein.is_some(), "Root should have a Klein point");
1360
1361        let rk = root_klein.unwrap();
1362        // Root at origin → Klein origin, weight = 1
1363        assert!(rk.coords[0].abs() < constants::epsilon());
1364        assert!(rk.weight > constants::half());
1365
1366        let child_sig = network.add_node(
1367            NodeMetadata::new("/child".to_string(), None),
1368            b"child".to_vec(),
1369            Some(&root_sig),
1370            1
1371        ).unwrap();
1372
1373        let child_klein = network.get_klein_point(&child_sig.unique_id());
1374        assert!(child_klein.is_some(), "Child should have a Klein point");
1375        assert!(child_klein.unwrap().coords.length() > constants::epsilon(),
1376            "Child Klein point should be away from origin");
1377    }
1378
1379    #[test]
1380    fn test_power_cells_created() {
1381        let network = HyperbolicTensorNetwork::new(2, constants::default_tau());
1382
1383        let root_sig = network.add_node(
1384            NodeMetadata::new("/".to_string(), None),
1385            b"root".to_vec(),
1386            None,
1387            0
1388        ).unwrap();
1389
1390        // Root cell should have no half-planes (covers entire disk)
1391        let root_cell = network.get_power_cell(&root_sig.unique_id()).unwrap();
1392        assert!(root_cell.half_planes.is_empty(), "Root cell should have no constraints initially");
1393
1394        let child_sig = network.add_node(
1395            NodeMetadata::new("/child".to_string(), None),
1396            b"child".to_vec(),
1397            Some(&root_sig),
1398            1
1399        ).unwrap();
1400
1401        // After adding child: root should have 1 half-plane, child should have 1
1402        let root_cell = network.get_power_cell(&root_sig.unique_id()).unwrap();
1403        assert_eq!(root_cell.half_planes.len(), 1, "Root should have 1 half-plane after adding child");
1404
1405        let child_cell = network.get_power_cell(&child_sig.unique_id()).unwrap();
1406        assert_eq!(child_cell.half_planes.len(), 1, "Child (leaf) should have 1 half-plane");
1407    }
1408
1409    #[test]
1410    fn test_nearest_neighbor_point_finds_self() {
1411        // Query at a node's own position should return that node
1412        let network = HyperbolicTensorNetwork::new(2, constants::default_tau());
1413
1414        let root_sig = network.add_node(
1415            NodeMetadata::new("/".to_string(), None),
1416            b"root".to_vec(),
1417            None,
1418            0
1419        ).unwrap();
1420
1421        let child_sig = network.add_node(
1422            NodeMetadata::new("/child".to_string(), None),
1423            b"child".to_vec(),
1424            Some(&root_sig),
1425            1
1426        ).unwrap();
1427
1428        // Query at child's position should find child
1429        let child_point = network.get_point(&child_sig.unique_id()).unwrap();
1430        let (nn_id, nn_dist) = network.nearest_neighbor_point(&child_point).unwrap();
1431
1432        assert_eq!(nn_id, child_sig.unique_id(),
1433            "Nearest neighbor at child's position should be child itself");
1434        let tolerance = FixedPoint::from_int(1) / FixedPoint::from_int(100);
1435        assert!(nn_dist < tolerance,
1436            "Distance to self should be ~0, got {}", nn_dist);
1437    }
1438
1439    #[test]
1440    fn test_grid_reflects_insertions() {
1441        let network = HyperbolicTensorNetwork::new(2, constants::default_tau());
1442
1443        let root_sig = network.add_node(
1444            NodeMetadata::new("/".to_string(), None),
1445            b"root".to_vec(),
1446            None,
1447            0
1448        ).unwrap();
1449
1450        // Grid should have assigned tiles after root
1451        assert!(network.grid_assigned_tile_count() > 0, "Grid should have tiles after root insert");
1452
1453        let _child_sig = network.add_node(
1454            NodeMetadata::new("/child".to_string(), None),
1455            b"child".to_vec(),
1456            Some(&root_sig),
1457            1
1458        ).unwrap();
1459
1460        // After child insert, grid should still have assigned tiles
1461        assert!(network.grid_assigned_tile_count() > 0, "Grid should still have tiles after child insert");
1462    }
1463
1464    #[test]
1465    fn test_delete_cleans_up_klein_state() {
1466        let network = HyperbolicTensorNetwork::new(2, constants::default_tau());
1467
1468        let root_sig = network.add_node(
1469            NodeMetadata::new("/".to_string(), None),
1470            b"root".to_vec(),
1471            None,
1472            0
1473        ).unwrap();
1474
1475        let child_sig = network.add_node(
1476            NodeMetadata::new("/child".to_string(), None),
1477            b"child".to_vec(),
1478            Some(&root_sig),
1479            1
1480        ).unwrap();
1481
1482        let child_id = child_sig.unique_id();
1483
1484        // Verify Klein state exists
1485        assert!(network.get_klein_point(&child_id).is_some());
1486        assert!(network.get_power_cell(&child_id).is_some());
1487
1488        // Delete child
1489        network.unregister_node(&child_id);
1490
1491        // Klein state should be cleaned up
1492        assert!(network.get_klein_point(&child_id).is_none());
1493        assert!(network.get_power_cell(&child_id).is_none());
1494
1495        // Parent's cell should no longer have the child's half-plane
1496        let root_cell = network.get_power_cell(&root_sig.unique_id()).unwrap();
1497        assert!(root_cell.half_planes.is_empty(),
1498            "Root cell should have no half-planes after child deletion");
1499    }
1500
1501    #[test]
1502    fn test_semantic_distance_identical() {
1503        // Identical coordinates → distance = 0
1504        let coords = {
1505            let mut v = vec![0u8; 3 * 16]; // 3 dims
1506            let val = FixedPoint::from_f64(0.5).raw().to_le_bytes();
1507            v[0..16].copy_from_slice(&val);
1508            v[16..32].copy_from_slice(&val);
1509            v[32..48].copy_from_slice(&val);
1510            v
1511        };
1512        let dist = HyperbolicTensorNetwork::semantic_distance(&coords, &coords, &(0..3));
1513        assert!(dist < constants::epsilon(), "Distance to self should be ~0, got {}", dist);
1514    }
1515
1516    #[test]
1517    fn test_semantic_distance_known_value() {
1518        // dim0: (1.0, 0.0), dim1: (0.0, 0.0) → distance = 1.0
1519        let mut a = vec![0u8; 2 * 16];
1520        let one = FixedPoint::from_f64(1.0).raw().to_le_bytes();
1521        a[0..16].copy_from_slice(&one);
1522        // dim1 stays zero
1523
1524        let b = vec![0u8; 2 * 16]; // all zero
1525
1526        let dist = HyperbolicTensorNetwork::semantic_distance(&a, &b, &(0..2));
1527        let tolerance = FixedPoint::from_int(1) / FixedPoint::from_int(100);
1528        assert!((dist - FixedPoint::from_int(1)).abs() < tolerance,
1529            "Distance should be 1.0, got {}", dist);
1530    }
1531
1532    #[test]
1533    fn test_semantic_distance_dimensional_slice() {
1534        // Only compare dim 1, ignore dim 0
1535        let mut a = vec![0u8; 2 * 16];
1536        let one = FixedPoint::from_f64(1.0).raw().to_le_bytes();
1537        a[0..16].copy_from_slice(&one); // dim 0 = 1.0
1538
1539        let b = vec![0u8; 2 * 16]; // all zero
1540
1541        // Slice dim 1 only → both are 0.0 at dim 1 → distance = 0
1542        let dist = HyperbolicTensorNetwork::semantic_distance(&a, &b, &(1..2));
1543        assert!(dist < constants::epsilon(),
1544            "Slicing only dim 1 should give distance ~0, got {}", dist);
1545
1546        // Slice dim 0 only → (1.0 vs 0.0) → distance = 1.0
1547        let dist_full = HyperbolicTensorNetwork::semantic_distance(&a, &b, &(0..1));
1548        let tolerance = FixedPoint::from_int(1) / FixedPoint::from_int(100);
1549        assert!((dist_full - FixedPoint::from_int(1)).abs() < tolerance,
1550            "Slicing dim 0 should give distance 1.0, got {}", dist_full);
1551    }
1552
1553    #[test]
1554    fn test_nearest_semantic_basic() {
1555        let network = HyperbolicTensorNetwork::new(2, constants::default_tau());
1556
1557        // Add 3 nodes with semantic coords in 2 dims
1558        let root_sig = network.add_node(
1559            NodeMetadata::new("/".to_string(), None),
1560            b"root".to_vec(), None, 0,
1561        ).unwrap();
1562
1563        let a_sig = network.add_node(
1564            NodeMetadata::new("/a".to_string(), None),
1565            b"a".to_vec(), Some(&root_sig), 1,
1566        ).unwrap();
1567
1568        let b_sig = network.add_node(
1569            NodeMetadata::new("/b".to_string(), None),
1570            b"b".to_vec(), Some(&root_sig), 1,
1571        ).unwrap();
1572
1573        let c_sig = network.add_node(
1574            NodeMetadata::new("/c".to_string(), None),
1575            b"c".to_vec(), Some(&root_sig), 1,
1576        ).unwrap();
1577
1578        // Set semantic coords: /a at (0.8, 0.1), /b at (0.7, 0.2), /c at (0.1, 0.9)
1579        let make_coords = |d0: f64, d1: f64| -> Vec<u8> {
1580            let mut v = vec![0u8; 2 * 16];
1581            v[0..16].copy_from_slice(&FixedPoint::from_f64(d0).raw().to_le_bytes());
1582            v[16..32].copy_from_slice(&FixedPoint::from_f64(d1).raw().to_le_bytes());
1583            v
1584        };
1585
1586        network.set_node_semantic(&a_sig.unique_id(), make_coords(0.8, 0.1));
1587        network.set_node_semantic(&b_sig.unique_id(), make_coords(0.7, 0.2));
1588        network.set_node_semantic(&c_sig.unique_id(), make_coords(0.1, 0.9));
1589
1590        // Query near /a's position → /b should be closest, /c farthest
1591        let query = make_coords(0.8, 0.1);
1592        let results = network.nearest_semantic(&query, 3, &(0..2));
1593
1594        assert!(!results.is_empty());
1595
1596        // First result should be /a (distance ~0)
1597        let first_dist = results[0].1;
1598        assert!(first_dist < FixedPoint::from_f64(0.01),
1599            "Nearest to (0.8,0.1) should be /a at ~0 distance, got {}", first_dist);
1600
1601        // /c should be much farther than /b
1602        if results.len() >= 3 {
1603            assert!(results[2].1 > results[1].1,
1604                "Third result should be farther than second");
1605        }
1606    }
1607}