cvx_index/hnsw/
temporal_edges.rs

1//! Temporal edge layer — successor/predecessor links between entity time points.
2//!
3//! Stores temporal ordering edges SEPARATELY from the HNSW graph, preserving
4//! the vanilla HNSW structure while adding causal navigation capabilities.
5//!
6//! # Design
7//!
8//! For each entity's consecutive points (ordered by timestamp), this layer
9//! maintains bidirectional temporal edges:
10//! - `predecessor[node_i+1] = node_i`
11//! - `successor[node_i] = node_i+1`
12//!
13//! Memory: 8 bytes/node (Option<u32> for successor + predecessor).
14
15use serde::{Deserialize, Serialize};
16
17/// Temporal edge layer tracking successor/predecessor relationships.
18///
19/// Node IDs must be registered sequentially (0, 1, 2, ...).
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct TemporalEdgeLayer {
22    /// node_id → temporal successor (next point for same entity, or None).
23    successors: Vec<Option<u32>>,
24    /// node_id → temporal predecessor (previous point for same entity, or None).
25    predecessors: Vec<Option<u32>>,
26}
27
28impl TemporalEdgeLayer {
29    /// Create a new empty temporal edge layer.
30    pub fn new() -> Self {
31        Self {
32            successors: Vec::new(),
33            predecessors: Vec::new(),
34        }
35    }
36
37    /// Create with pre-allocated capacity.
38    pub fn with_capacity(capacity: usize) -> Self {
39        Self {
40            successors: Vec::with_capacity(capacity),
41            predecessors: Vec::with_capacity(capacity),
42        }
43    }
44
45    /// Register a new node and link it to the entity's previous latest node.
46    ///
47    /// `entity_last_node` should be the node_id of the entity's most recent
48    /// point before this insert (None if this is the entity's first point).
49    ///
50    /// # Panics
51    ///
52    /// Panics if `node_id != self.len()` (nodes must be registered sequentially).
53    pub fn register(&mut self, node_id: u32, entity_last_node: Option<u32>) {
54        assert_eq!(
55            node_id as usize,
56            self.successors.len(),
57            "nodes must be registered sequentially"
58        );
59
60        // New node has no successor yet
61        self.successors.push(None);
62        // New node's predecessor is the entity's previous latest
63        self.predecessors.push(entity_last_node);
64
65        // Link the previous node's successor to this new node
66        if let Some(prev) = entity_last_node {
67            if (prev as usize) < self.successors.len() {
68                self.successors[prev as usize] = Some(node_id);
69            }
70        }
71    }
72
73    /// Get the temporal successor of a node (next point for same entity).
74    pub fn successor(&self, node_id: u32) -> Option<u32> {
75        self.successors.get(node_id as usize).copied().flatten()
76    }
77
78    /// Get the temporal predecessor of a node (previous point for same entity).
79    pub fn predecessor(&self, node_id: u32) -> Option<u32> {
80        self.predecessors.get(node_id as usize).copied().flatten()
81    }
82
83    /// Iterate temporal neighbors (predecessor and/or successor).
84    pub fn temporal_neighbors(&self, node_id: u32) -> impl Iterator<Item = u32> + '_ {
85        let pred = self.predecessor(node_id);
86        let succ = self.successor(node_id);
87        pred.into_iter().chain(succ)
88    }
89
90    /// Walk forward in time from a node, returning up to `max_steps` successors.
91    pub fn walk_forward(&self, start: u32, max_steps: usize) -> Vec<u32> {
92        let mut path = Vec::with_capacity(max_steps);
93        let mut current = start;
94        for _ in 0..max_steps {
95            match self.successor(current) {
96                Some(next) => {
97                    path.push(next);
98                    current = next;
99                }
100                None => break,
101            }
102        }
103        path
104    }
105
106    /// Walk backward in time from a node, returning up to `max_steps` predecessors.
107    pub fn walk_backward(&self, start: u32, max_steps: usize) -> Vec<u32> {
108        let mut path = Vec::with_capacity(max_steps);
109        let mut current = start;
110        for _ in 0..max_steps {
111            match self.predecessor(current) {
112                Some(prev) => {
113                    path.push(prev);
114                    current = prev;
115                }
116                None => break,
117            }
118        }
119        path
120    }
121
122    /// Number of registered nodes.
123    pub fn len(&self) -> usize {
124        self.successors.len()
125    }
126
127    /// Whether no nodes have been registered.
128    pub fn is_empty(&self) -> bool {
129        self.successors.is_empty()
130    }
131
132    /// Approximate memory usage in bytes.
133    pub fn memory_bytes(&self) -> usize {
134        // Each Vec<Option<u32>>: len * 8 bytes (Option<u32> = 8 bytes due to alignment)
135        self.successors.len() * 8 + self.predecessors.len() * 8
136    }
137}
138
139impl Default for TemporalEdgeLayer {
140    fn default() -> Self {
141        Self::new()
142    }
143}
144
145// ─── Tests ──────────────────────────────────────────────────────────
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150
151    // ─── Basic operations ───────────────────────────────────────
152
153    #[test]
154    fn new_empty() {
155        let layer = TemporalEdgeLayer::new();
156        assert_eq!(layer.len(), 0);
157        assert!(layer.is_empty());
158    }
159
160    #[test]
161    fn register_single_node_no_predecessor() {
162        let mut layer = TemporalEdgeLayer::new();
163        layer.register(0, None);
164
165        assert_eq!(layer.len(), 1);
166        assert_eq!(layer.successor(0), None);
167        assert_eq!(layer.predecessor(0), None);
168    }
169
170    // ─── Single entity chain ────────────────────────────────────
171
172    #[test]
173    fn single_entity_chain() {
174        let mut layer = TemporalEdgeLayer::new();
175
176        // Entity A: 5 sequential points
177        layer.register(0, None); // first point
178        layer.register(1, Some(0)); // second
179        layer.register(2, Some(1)); // third
180        layer.register(3, Some(2)); // fourth
181        layer.register(4, Some(3)); // fifth
182
183        // Forward chain
184        assert_eq!(layer.successor(0), Some(1));
185        assert_eq!(layer.successor(1), Some(2));
186        assert_eq!(layer.successor(2), Some(3));
187        assert_eq!(layer.successor(3), Some(4));
188        assert_eq!(layer.successor(4), None); // end
189
190        // Backward chain
191        assert_eq!(layer.predecessor(0), None); // start
192        assert_eq!(layer.predecessor(1), Some(0));
193        assert_eq!(layer.predecessor(2), Some(1));
194        assert_eq!(layer.predecessor(3), Some(2));
195        assert_eq!(layer.predecessor(4), Some(3));
196    }
197
198    // ─── Multi-entity isolation ─────────────────────────────────
199
200    #[test]
201    fn multi_entity_edges_dont_cross() {
202        let mut layer = TemporalEdgeLayer::new();
203
204        // Interleaved inserts from 2 entities:
205        // Entity A: nodes 0, 2, 4
206        // Entity B: nodes 1, 3, 5
207        layer.register(0, None); // A first
208        layer.register(1, None); // B first
209        layer.register(2, Some(0)); // A second (links to A's last = 0)
210        layer.register(3, Some(1)); // B second (links to B's last = 1)
211        layer.register(4, Some(2)); // A third (links to A's last = 2)
212        layer.register(5, Some(3)); // B third (links to B's last = 3)
213
214        // Entity A chain: 0 → 2 → 4
215        assert_eq!(layer.successor(0), Some(2));
216        assert_eq!(layer.successor(2), Some(4));
217        assert_eq!(layer.successor(4), None);
218        assert_eq!(layer.predecessor(4), Some(2));
219        assert_eq!(layer.predecessor(2), Some(0));
220
221        // Entity B chain: 1 → 3 → 5
222        assert_eq!(layer.successor(1), Some(3));
223        assert_eq!(layer.successor(3), Some(5));
224        assert_eq!(layer.successor(5), None);
225        assert_eq!(layer.predecessor(5), Some(3));
226        assert_eq!(layer.predecessor(3), Some(1));
227
228        // No cross-entity edges
229        assert_ne!(layer.successor(0), Some(1)); // A doesn't link to B
230        assert_ne!(layer.successor(1), Some(2)); // B doesn't link to A
231    }
232
233    // ─── Walk operations ────────────────────────────────────────
234
235    #[test]
236    fn walk_forward_full_chain() {
237        let mut layer = TemporalEdgeLayer::new();
238        for i in 0..5u32 {
239            layer.register(i, if i == 0 { None } else { Some(i - 1) });
240        }
241
242        let path = layer.walk_forward(0, 10);
243        assert_eq!(path, vec![1, 2, 3, 4]);
244    }
245
246    #[test]
247    fn walk_forward_limited() {
248        let mut layer = TemporalEdgeLayer::new();
249        for i in 0..10u32 {
250            layer.register(i, if i == 0 { None } else { Some(i - 1) });
251        }
252
253        let path = layer.walk_forward(0, 3);
254        assert_eq!(path, vec![1, 2, 3]);
255    }
256
257    #[test]
258    fn walk_forward_from_end() {
259        let mut layer = TemporalEdgeLayer::new();
260        layer.register(0, None);
261        layer.register(1, Some(0));
262
263        let path = layer.walk_forward(1, 5);
264        assert!(path.is_empty());
265    }
266
267    #[test]
268    fn walk_backward_full_chain() {
269        let mut layer = TemporalEdgeLayer::new();
270        for i in 0..5u32 {
271            layer.register(i, if i == 0 { None } else { Some(i - 1) });
272        }
273
274        let path = layer.walk_backward(4, 10);
275        assert_eq!(path, vec![3, 2, 1, 0]);
276    }
277
278    #[test]
279    fn walk_backward_limited() {
280        let mut layer = TemporalEdgeLayer::new();
281        for i in 0..10u32 {
282            layer.register(i, if i == 0 { None } else { Some(i - 1) });
283        }
284
285        let path = layer.walk_backward(9, 3);
286        assert_eq!(path, vec![8, 7, 6]);
287    }
288
289    // ─── Temporal neighbors ─────────────────────────────────────
290
291    #[test]
292    fn temporal_neighbors_middle_node() {
293        let mut layer = TemporalEdgeLayer::new();
294        layer.register(0, None);
295        layer.register(1, Some(0));
296        layer.register(2, Some(1));
297
298        let neighbors: Vec<u32> = layer.temporal_neighbors(1).collect();
299        assert_eq!(neighbors.len(), 2);
300        assert!(neighbors.contains(&0)); // predecessor
301        assert!(neighbors.contains(&2)); // successor
302    }
303
304    #[test]
305    fn temporal_neighbors_first_node() {
306        let mut layer = TemporalEdgeLayer::new();
307        layer.register(0, None);
308        layer.register(1, Some(0));
309
310        let neighbors: Vec<u32> = layer.temporal_neighbors(0).collect();
311        assert_eq!(neighbors.len(), 1); // only successor
312        assert_eq!(neighbors[0], 1);
313    }
314
315    #[test]
316    fn temporal_neighbors_last_node() {
317        let mut layer = TemporalEdgeLayer::new();
318        layer.register(0, None);
319        layer.register(1, Some(0));
320
321        let neighbors: Vec<u32> = layer.temporal_neighbors(1).collect();
322        assert_eq!(neighbors.len(), 1); // only predecessor
323        assert_eq!(neighbors[0], 0);
324    }
325
326    #[test]
327    fn temporal_neighbors_isolated_node() {
328        let mut layer = TemporalEdgeLayer::new();
329        layer.register(0, None); // no predecessor, no successor
330
331        let neighbors: Vec<u32> = layer.temporal_neighbors(0).collect();
332        assert!(neighbors.is_empty());
333    }
334
335    // ─── Edge cases ─────────────────────────────────────────────
336
337    #[test]
338    fn out_of_bounds_queries() {
339        let layer = TemporalEdgeLayer::new();
340        assert_eq!(layer.successor(999), None);
341        assert_eq!(layer.predecessor(999), None);
342    }
343
344    #[test]
345    fn walk_from_nonexistent() {
346        let layer = TemporalEdgeLayer::new();
347        assert!(layer.walk_forward(999, 5).is_empty());
348        assert!(layer.walk_backward(999, 5).is_empty());
349    }
350
351    #[test]
352    #[should_panic(expected = "nodes must be registered sequentially")]
353    fn register_out_of_order_panics() {
354        let mut layer = TemporalEdgeLayer::new();
355        layer.register(5, None); // should be 0
356    }
357
358    // ─── Memory ─────────────────────────────────────────────────
359
360    #[test]
361    fn memory_scales_linearly() {
362        let mut layer = TemporalEdgeLayer::new();
363        for i in 0..1000u32 {
364            layer.register(i, if i == 0 { None } else { Some(i - 1) });
365        }
366        let bytes = layer.memory_bytes();
367        // 1000 nodes * 8 bytes * 2 (succ + pred) = 16000
368        assert_eq!(bytes, 16000);
369    }
370
371    // ─── Serialization ──────────────────────────────────────────
372
373    #[test]
374    fn serialize_deserialize_roundtrip() {
375        let mut layer = TemporalEdgeLayer::new();
376        layer.register(0, None);
377        layer.register(1, Some(0));
378        layer.register(2, Some(1));
379
380        let bytes = postcard::to_allocvec(&layer).unwrap();
381        let restored: TemporalEdgeLayer = postcard::from_bytes(&bytes).unwrap();
382
383        assert_eq!(restored.len(), 3);
384        assert_eq!(restored.successor(0), Some(1));
385        assert_eq!(restored.successor(1), Some(2));
386        assert_eq!(restored.predecessor(2), Some(1));
387    }
388}