rag_plusplus_core/trajectory/
ring.rs

1//! Ring Topology for Trajectory Memory
2//!
3//! Provides a circular structure for organizing episodes in a trajectory.
4//! Inspired by RCP (Ring Contextual Propagation) which uses ring topology
5//! for multi-scale context organization.
6//!
7//! # Key Properties
8//!
9//! - **Circular ordering**: Enables wraparound attention
10//! - **O(1) neighbor access**: Adjacent elements in constant time
11//! - **Ring distance**: Shortest path in either direction
12//! - **Cache-friendly**: Contiguous storage in Vec
13//!
14//! # Usage
15//!
16//! ```
17//! use rag_plusplus_core::trajectory::Ring;
18//!
19//! let ring = Ring::new(vec![1, 2, 3, 4, 5]);
20//!
21//! // Get neighbors
22//! let neighbors: Vec<_> = ring.neighbors(0, 2).collect();
23//!
24//! // Ring distance (shortest path)
25//! let dist = ring.ring_distance(0, 3); // min(3, 2) = 2
26//! ```
27
28use std::ops::Index;
29
30/// A ring topology over a collection of elements.
31///
32/// Provides circular access patterns and ring-based distance metrics.
33/// Elements are stored contiguously for cache efficiency.
34#[derive(Debug, Clone)]
35pub struct Ring<T> {
36    nodes: Vec<T>,
37}
38
39impl<T> Ring<T> {
40    /// Create a new ring from a vector of nodes.
41    ///
42    /// # Panics
43    ///
44    /// Panics if nodes is empty.
45    pub fn new(nodes: Vec<T>) -> Self {
46        assert!(!nodes.is_empty(), "Ring cannot be empty");
47        Self { nodes }
48    }
49
50    /// Create a ring with capacity for n nodes.
51    pub fn with_capacity(capacity: usize) -> Self {
52        Self {
53            nodes: Vec::with_capacity(capacity),
54        }
55    }
56
57    /// Number of nodes in the ring.
58    #[inline]
59    pub fn len(&self) -> usize {
60        self.nodes.len()
61    }
62
63    /// Check if the ring is empty.
64    #[inline]
65    pub fn is_empty(&self) -> bool {
66        self.nodes.is_empty()
67    }
68
69    /// Get a reference to the node at the given index (wrapping).
70    #[inline]
71    pub fn get(&self, idx: usize) -> &T {
72        &self.nodes[idx % self.len()]
73    }
74
75    /// Get a mutable reference to the node at the given index (wrapping).
76    #[inline]
77    pub fn get_mut(&mut self, idx: usize) -> &mut T {
78        let len = self.len();
79        &mut self.nodes[idx % len]
80    }
81
82    /// Get the next index in the ring (wrapping).
83    #[inline]
84    pub fn next(&self, idx: usize) -> usize {
85        (idx + 1) % self.len()
86    }
87
88    /// Get the previous index in the ring (wrapping).
89    #[inline]
90    pub fn prev(&self, idx: usize) -> usize {
91        if idx == 0 {
92            self.len() - 1
93        } else {
94            idx - 1
95        }
96    }
97
98    /// Compute the ring distance between two indices.
99    ///
100    /// The ring distance is the shortest path between two nodes,
101    /// going either forward or backward around the ring.
102    ///
103    /// # Example
104    ///
105    /// For a ring of size 5:
106    /// - `ring_distance(0, 1)` = 1 (one step forward)
107    /// - `ring_distance(0, 4)` = 1 (one step backward)
108    /// - `ring_distance(0, 2)` = 2 (two steps either way)
109    #[inline]
110    pub fn ring_distance(&self, a: usize, b: usize) -> usize {
111        let n = self.len();
112        let a = a % n;
113        let b = b % n;
114        let forward = (b + n - a) % n;
115        let backward = (a + n - b) % n;
116        forward.min(backward)
117    }
118
119    /// Iterate over neighbors within a given radius.
120    ///
121    /// Returns an iterator yielding references to nodes within `radius`
122    /// steps in both directions.
123    ///
124    /// # Example
125    ///
126    /// For a ring [0, 1, 2, 3, 4], neighbors(0, 1) yields nodes at indices 4, 1
127    /// (previous and next).
128    pub fn neighbors(&self, idx: usize, radius: usize) -> impl Iterator<Item = &T> {
129        let n = self.len();
130        let idx = idx % n;
131
132        (1..=radius).flat_map(move |d| {
133            let prev_idx = (idx + n - d) % n;
134            let next_idx = (idx + d) % n;
135
136            // Avoid duplicate if prev_idx == next_idx (happens in small rings)
137            if prev_idx == next_idx {
138                vec![&self.nodes[prev_idx]].into_iter()
139            } else {
140                vec![&self.nodes[prev_idx], &self.nodes[next_idx]].into_iter()
141            }
142        })
143    }
144
145    /// Get neighbor indices within a given radius.
146    ///
147    /// Returns indices (not values) of neighbors.
148    pub fn neighbor_indices(&self, idx: usize, radius: usize) -> Vec<usize> {
149        let n = self.len();
150        let idx = idx % n;
151
152        let mut indices = Vec::with_capacity(radius * 2);
153
154        for d in 1..=radius {
155            let prev_idx = (idx + n - d) % n;
156            let next_idx = (idx + d) % n;
157
158            if prev_idx == next_idx {
159                indices.push(prev_idx);
160            } else {
161                indices.push(prev_idx);
162                indices.push(next_idx);
163            }
164        }
165
166        indices
167    }
168
169    /// Iterate over all nodes in order.
170    pub fn iter(&self) -> impl Iterator<Item = &T> {
171        self.nodes.iter()
172    }
173
174    /// Iterate over all nodes mutably in order.
175    pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut T> {
176        self.nodes.iter_mut()
177    }
178
179    /// Iterate starting from a given index, wrapping around.
180    pub fn iter_from(&self, start: usize) -> impl Iterator<Item = &T> {
181        let n = self.len();
182        let start = start % n;
183        (0..n).map(move |i| &self.nodes[(start + i) % n])
184    }
185
186    /// Push a new node to the ring.
187    pub fn push(&mut self, node: T) {
188        self.nodes.push(node);
189    }
190
191    /// Get the underlying nodes as a slice.
192    #[inline]
193    pub fn as_slice(&self) -> &[T] {
194        &self.nodes
195    }
196
197    /// Get the underlying nodes as a mutable slice.
198    #[inline]
199    pub fn as_mut_slice(&mut self) -> &mut [T] {
200        &mut self.nodes
201    }
202
203    /// Into the underlying vector.
204    pub fn into_inner(self) -> Vec<T> {
205        self.nodes
206    }
207}
208
209impl<T> Index<usize> for Ring<T> {
210    type Output = T;
211
212    #[inline]
213    fn index(&self, idx: usize) -> &Self::Output {
214        self.get(idx)
215    }
216}
217
218impl<T> FromIterator<T> for Ring<T> {
219    fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
220        Self::new(iter.into_iter().collect())
221    }
222}
223
224/// A node in a ring with additional metadata.
225#[derive(Debug, Clone)]
226pub struct RingNode<T> {
227    /// The value stored in this node
228    pub value: T,
229    /// Weight for attention/importance
230    pub weight: f32,
231    /// Optional connection strength to next node
232    pub forward_weight: f32,
233    /// Optional connection strength to previous node
234    pub backward_weight: f32,
235}
236
237impl<T> RingNode<T> {
238    /// Create a new ring node with default weights.
239    pub fn new(value: T) -> Self {
240        Self {
241            value,
242            weight: 1.0,
243            forward_weight: 1.0,
244            backward_weight: 1.0,
245        }
246    }
247
248    /// Create a ring node with custom weight.
249    pub fn with_weight(value: T, weight: f32) -> Self {
250        Self {
251            value,
252            weight,
253            forward_weight: 1.0,
254            backward_weight: 1.0,
255        }
256    }
257}
258
259/// Ring of episodes for multi-scale trajectory organization.
260pub type EpisodeRing<E> = Ring<RingNode<E>>;
261
262/// Build a ring from a sequence of episodes with computed weights.
263///
264/// # Arguments
265///
266/// * `episodes` - Episode values
267/// * `weights` - Weight for each episode (e.g., salience scores)
268pub fn build_weighted_ring<T>(episodes: Vec<T>, weights: &[f32]) -> Ring<RingNode<T>> {
269    assert_eq!(episodes.len(), weights.len(), "Episode and weight counts must match");
270
271    let nodes: Vec<RingNode<T>> = episodes
272        .into_iter()
273        .zip(weights.iter())
274        .map(|(e, &w)| RingNode::with_weight(e, w))
275        .collect();
276
277    Ring::new(nodes)
278}
279
280/// Dual Ring Structure for IRCP/RCP
281///
282/// Represents the same episodes in two different orderings:
283/// - **Temporal Ring (RCP)**: Ordered by causal/temporal flow
284/// - **Influence Ring (IRCP)**: Ordered by influence/attention weight
285///
286/// This enables efficient traversal in both directions:
287/// - Forward (RCP): "What context led to this response?"
288/// - Inverse (IRCP): "What did this response influence?"
289///
290/// # Example
291///
292/// ```
293/// use rag_plusplus_core::trajectory::{DualRing, DualRingNode};
294///
295/// // Create nodes with temporal order and influence weights
296/// let nodes = vec![
297///     DualRingNode::new(0, "episode_a", 0.8),  // temporal_idx=0, influence=0.8
298///     DualRingNode::new(1, "episode_b", 0.3),  // temporal_idx=1, influence=0.3
299///     DualRingNode::new(2, "episode_c", 0.9),  // temporal_idx=2, influence=0.9
300///     DualRingNode::new(3, "episode_d", 0.5),  // temporal_idx=3, influence=0.5
301/// ];
302///
303/// let dual = DualRing::new(nodes);
304///
305/// // Traverse temporally (RCP direction)
306/// let temporal_order: Vec<_> = dual.iter_temporal().map(|n| n.value).collect();
307/// // ["episode_a", "episode_b", "episode_c", "episode_d"]
308///
309/// // Traverse by influence (IRCP direction)
310/// let influence_order: Vec<_> = dual.iter_by_influence().map(|n| n.value).collect();
311/// // ["episode_c", "episode_a", "episode_d", "episode_b"] (by descending influence)
312/// ```
313#[derive(Debug, Clone)]
314pub struct DualRing<T> {
315    /// Nodes stored in temporal order
316    nodes: Vec<DualRingNode<T>>,
317    /// Indices sorted by influence (descending)
318    influence_order: Vec<usize>,
319}
320
321/// A node in a dual ring with both temporal position and influence weight.
322#[derive(Debug, Clone)]
323pub struct DualRingNode<T> {
324    /// The value stored in this node
325    pub value: T,
326    /// Temporal index (position in time)
327    pub temporal_idx: usize,
328    /// Influence weight (how much this node influenced others)
329    pub influence: f32,
330    /// Attention received (how much attention this node got from others)
331    pub attention_received: f32,
332    /// Attention given (how much attention this node gave to others)
333    pub attention_given: f32,
334}
335
336impl<T> DualRingNode<T> {
337    /// Create a new dual ring node.
338    pub fn new(temporal_idx: usize, value: T, influence: f32) -> Self {
339        Self {
340            value,
341            temporal_idx,
342            influence,
343            attention_received: 0.0,
344            attention_given: 0.0,
345        }
346    }
347
348    /// Create with full attention data.
349    pub fn with_attention(
350        temporal_idx: usize,
351        value: T,
352        influence: f32,
353        attention_received: f32,
354        attention_given: f32,
355    ) -> Self {
356        Self {
357            value,
358            temporal_idx,
359            influence,
360            attention_received,
361            attention_given,
362        }
363    }
364}
365
366impl<T> DualRing<T> {
367    /// Create a dual ring from nodes.
368    ///
369    /// Nodes should be provided in temporal order.
370    pub fn new(nodes: Vec<DualRingNode<T>>) -> Self {
371        assert!(!nodes.is_empty(), "DualRing cannot be empty");
372
373        // Build influence order (indices sorted by descending influence)
374        let mut influence_order: Vec<usize> = (0..nodes.len()).collect();
375        influence_order.sort_by(|&a, &b| {
376            nodes[b].influence.partial_cmp(&nodes[a].influence)
377                .unwrap_or(std::cmp::Ordering::Equal)
378        });
379
380        Self { nodes, influence_order }
381    }
382
383    /// Number of nodes.
384    #[inline]
385    pub fn len(&self) -> usize {
386        self.nodes.len()
387    }
388
389    /// Check if empty.
390    #[inline]
391    pub fn is_empty(&self) -> bool {
392        self.nodes.is_empty()
393    }
394
395    // ========== RCP (Temporal) Operations ==========
396
397    /// Get node by temporal index (wrapping).
398    #[inline]
399    pub fn get_temporal(&self, idx: usize) -> &DualRingNode<T> {
400        &self.nodes[idx % self.len()]
401    }
402
403    /// Next in temporal order.
404    #[inline]
405    pub fn temporal_next(&self, idx: usize) -> usize {
406        (idx + 1) % self.len()
407    }
408
409    /// Previous in temporal order.
410    #[inline]
411    pub fn temporal_prev(&self, idx: usize) -> usize {
412        if idx == 0 { self.len() - 1 } else { idx - 1 }
413    }
414
415    /// Temporal ring distance.
416    #[inline]
417    pub fn temporal_distance(&self, a: usize, b: usize) -> usize {
418        let n = self.len();
419        let a = a % n;
420        let b = b % n;
421        let forward = (b + n - a) % n;
422        let backward = (a + n - b) % n;
423        forward.min(backward)
424    }
425
426    /// Iterate in temporal order (RCP direction).
427    pub fn iter_temporal(&self) -> impl Iterator<Item = &DualRingNode<T>> {
428        self.nodes.iter()
429    }
430
431    /// Iterate temporally from a starting point.
432    pub fn iter_temporal_from(&self, start: usize) -> impl Iterator<Item = &DualRingNode<T>> {
433        let n = self.len();
434        let start = start % n;
435        (0..n).map(move |i| &self.nodes[(start + i) % n])
436    }
437
438    /// Get temporal neighbors within radius.
439    pub fn temporal_neighbors(&self, idx: usize, radius: usize) -> impl Iterator<Item = &DualRingNode<T>> {
440        let n = self.len();
441        let idx = idx % n;
442
443        (1..=radius).flat_map(move |d| {
444            let prev_idx = (idx + n - d) % n;
445            let next_idx = (idx + d) % n;
446
447            if prev_idx == next_idx {
448                vec![&self.nodes[prev_idx]].into_iter()
449            } else {
450                vec![&self.nodes[prev_idx], &self.nodes[next_idx]].into_iter()
451            }
452        })
453    }
454
455    // ========== IRCP (Influence) Operations ==========
456
457    /// Get node by influence rank (0 = highest influence).
458    #[inline]
459    pub fn get_by_influence_rank(&self, rank: usize) -> &DualRingNode<T> {
460        let idx = self.influence_order[rank % self.len()];
461        &self.nodes[idx]
462    }
463
464    /// Get influence rank of a temporal index.
465    pub fn influence_rank_of(&self, temporal_idx: usize) -> usize {
466        self.influence_order.iter()
467            .position(|&idx| idx == temporal_idx)
468            .unwrap_or(self.len())
469    }
470
471    /// Iterate by influence (IRCP direction, highest first).
472    pub fn iter_by_influence(&self) -> impl Iterator<Item = &DualRingNode<T>> {
473        self.influence_order.iter().map(move |&idx| &self.nodes[idx])
474    }
475
476    /// Get top-k most influential nodes.
477    pub fn top_influential(&self, k: usize) -> impl Iterator<Item = &DualRingNode<T>> {
478        self.influence_order.iter()
479            .take(k)
480            .map(move |&idx| &self.nodes[idx])
481    }
482
483    /// Influence ring distance (distance in influence-sorted order).
484    pub fn influence_distance(&self, a_temporal: usize, b_temporal: usize) -> usize {
485        let a_rank = self.influence_rank_of(a_temporal);
486        let b_rank = self.influence_rank_of(b_temporal);
487
488        let n = self.len();
489        let forward = (b_rank + n - a_rank) % n;
490        let backward = (a_rank + n - b_rank) % n;
491        forward.min(backward)
492    }
493
494    /// Get influence neighbors (nodes with similar influence).
495    pub fn influence_neighbors(&self, temporal_idx: usize, radius: usize) -> impl Iterator<Item = &DualRingNode<T>> {
496        let rank = self.influence_rank_of(temporal_idx);
497        let n = self.len();
498
499        (1..=radius).flat_map(move |d| {
500            let prev_rank = if rank >= d { rank - d } else { n - (d - rank) };
501            let next_rank = (rank + d) % n;
502
503            let prev_idx = self.influence_order[prev_rank];
504            let next_idx = self.influence_order[next_rank];
505
506            if prev_idx == next_idx {
507                vec![&self.nodes[prev_idx]].into_iter()
508            } else {
509                vec![&self.nodes[prev_idx], &self.nodes[next_idx]].into_iter()
510            }
511        })
512    }
513
514    // ========== Cross-Ring Operations ==========
515
516    /// Find the temporal distance from high-influence to low-influence nodes.
517    ///
518    /// This measures how "spread out" influence is temporally.
519    /// Low values mean influence clusters in time; high values mean it's distributed.
520    pub fn influence_temporal_spread(&self) -> f32 {
521        if self.len() < 2 {
522            return 0.0;
523        }
524
525        let mut total_distance = 0.0;
526        let count = self.len().min(5); // Top 5 influential
527
528        for i in 0..count {
529            for j in (i + 1)..count {
530                let idx_i = self.influence_order[i];
531                let idx_j = self.influence_order[j];
532                total_distance += self.temporal_distance(idx_i, idx_j) as f32;
533            }
534        }
535
536        let pairs = (count * (count - 1)) / 2;
537        if pairs > 0 {
538            total_distance / pairs as f32
539        } else {
540            0.0
541        }
542    }
543
544    /// Compute attention flow from earlier to later nodes (RCP forward flow).
545    ///
546    /// Returns total influence flowing from past to future.
547    pub fn forward_attention_flow(&self) -> f32 {
548        self.nodes.iter()
549            .map(|n| n.attention_given)
550            .sum()
551    }
552
553    /// Compute attention flow from later to earlier nodes (IRCP inverse flow).
554    ///
555    /// Returns total influence flowing from future to past (attribution).
556    pub fn inverse_attention_flow(&self) -> f32 {
557        self.nodes.iter()
558            .map(|n| n.attention_received)
559            .sum()
560    }
561
562    /// Update influence weight for a node.
563    pub fn update_influence(&mut self, temporal_idx: usize, new_influence: f32) {
564        let idx = temporal_idx % self.len();
565        self.nodes[idx].influence = new_influence;
566
567        // Re-sort influence order
568        self.influence_order.sort_by(|&a, &b| {
569            self.nodes[b].influence.partial_cmp(&self.nodes[a].influence)
570                .unwrap_or(std::cmp::Ordering::Equal)
571        });
572    }
573
574    /// Get underlying nodes.
575    pub fn as_slice(&self) -> &[DualRingNode<T>] {
576        &self.nodes
577    }
578}
579
580/// Build a dual ring from episodes with temporal positions and influence scores.
581pub fn build_dual_ring<T>(
582    episodes: Vec<T>,
583    influences: &[f32],
584) -> DualRing<T> {
585    assert_eq!(episodes.len(), influences.len(), "Episode and influence counts must match");
586
587    let nodes: Vec<DualRingNode<T>> = episodes
588        .into_iter()
589        .enumerate()
590        .zip(influences.iter())
591        .map(|((idx, value), &influence)| DualRingNode::new(idx, value, influence))
592        .collect();
593
594    DualRing::new(nodes)
595}
596
597/// Build a dual ring with full attention data.
598pub fn build_dual_ring_with_attention<T>(
599    episodes: Vec<T>,
600    influences: &[f32],
601    attention_received: &[f32],
602    attention_given: &[f32],
603) -> DualRing<T> {
604    assert_eq!(episodes.len(), influences.len());
605    assert_eq!(episodes.len(), attention_received.len());
606    assert_eq!(episodes.len(), attention_given.len());
607
608    let nodes: Vec<DualRingNode<T>> = episodes
609        .into_iter()
610        .enumerate()
611        .zip(influences.iter())
612        .zip(attention_received.iter())
613        .zip(attention_given.iter())
614        .map(|((((idx, value), &inf), &recv), &given)| {
615            DualRingNode::with_attention(idx, value, inf, recv, given)
616        })
617        .collect();
618
619    DualRing::new(nodes)
620}
621
622#[cfg(test)]
623mod tests {
624    use super::*;
625
626    #[test]
627    fn test_new_ring() {
628        let ring = Ring::new(vec![1, 2, 3, 4, 5]);
629        assert_eq!(ring.len(), 5);
630        assert!(!ring.is_empty());
631    }
632
633    #[test]
634    #[should_panic(expected = "Ring cannot be empty")]
635    fn test_empty_ring_panics() {
636        let _ring: Ring<i32> = Ring::new(vec![]);
637    }
638
639    #[test]
640    fn test_get_wrapping() {
641        let ring = Ring::new(vec![0, 1, 2, 3, 4]);
642
643        assert_eq!(*ring.get(0), 0);
644        assert_eq!(*ring.get(4), 4);
645        assert_eq!(*ring.get(5), 0); // Wraps
646        assert_eq!(*ring.get(7), 2); // Wraps
647    }
648
649    #[test]
650    fn test_next_prev() {
651        let ring = Ring::new(vec![0, 1, 2, 3, 4]);
652
653        assert_eq!(ring.next(0), 1);
654        assert_eq!(ring.next(4), 0); // Wraps
655        assert_eq!(ring.prev(0), 4); // Wraps
656        assert_eq!(ring.prev(3), 2);
657    }
658
659    #[test]
660    fn test_ring_distance() {
661        let ring = Ring::new(vec![0, 1, 2, 3, 4]);
662
663        // Same node
664        assert_eq!(ring.ring_distance(0, 0), 0);
665
666        // Adjacent
667        assert_eq!(ring.ring_distance(0, 1), 1);
668        assert_eq!(ring.ring_distance(0, 4), 1); // Backward is shorter
669
670        // Two steps
671        assert_eq!(ring.ring_distance(0, 2), 2);
672        assert_eq!(ring.ring_distance(0, 3), 2); // Either direction
673
674        // Wrapping
675        assert_eq!(ring.ring_distance(1, 4), 2);
676    }
677
678    #[test]
679    fn test_neighbors() {
680        let ring = Ring::new(vec![0, 1, 2, 3, 4]);
681
682        // Neighbors of 0 with radius 1: prev=4, next=1
683        let neighbors: Vec<_> = ring.neighbors(0, 1).copied().collect();
684        assert_eq!(neighbors.len(), 2);
685        assert!(neighbors.contains(&4));
686        assert!(neighbors.contains(&1));
687
688        // Neighbors of 0 with radius 2: prev=3,4 and next=1,2
689        let neighbors: Vec<_> = ring.neighbors(0, 2).copied().collect();
690        assert_eq!(neighbors.len(), 4);
691        assert!(neighbors.contains(&3));
692        assert!(neighbors.contains(&4));
693        assert!(neighbors.contains(&1));
694        assert!(neighbors.contains(&2));
695    }
696
697    #[test]
698    fn test_neighbor_indices() {
699        let ring = Ring::new(vec![0, 1, 2, 3, 4]);
700
701        let indices = ring.neighbor_indices(0, 1);
702        assert_eq!(indices.len(), 2);
703        assert!(indices.contains(&4));
704        assert!(indices.contains(&1));
705    }
706
707    #[test]
708    fn test_iter_from() {
709        let ring = Ring::new(vec![0, 1, 2, 3, 4]);
710
711        let from_2: Vec<_> = ring.iter_from(2).copied().collect();
712        assert_eq!(from_2, vec![2, 3, 4, 0, 1]);
713
714        let from_0: Vec<_> = ring.iter_from(0).copied().collect();
715        assert_eq!(from_0, vec![0, 1, 2, 3, 4]);
716    }
717
718    #[test]
719    fn test_index_operator() {
720        let ring = Ring::new(vec![10, 20, 30]);
721
722        assert_eq!(ring[0], 10);
723        assert_eq!(ring[2], 30);
724        assert_eq!(ring[3], 10); // Wraps
725    }
726
727    #[test]
728    fn test_from_iterator() {
729        let ring: Ring<i32> = (0..5).collect();
730        assert_eq!(ring.len(), 5);
731        assert_eq!(*ring.get(0), 0);
732        assert_eq!(*ring.get(4), 4);
733    }
734
735    #[test]
736    fn test_ring_node() {
737        let node = RingNode::new(42);
738        assert_eq!(node.value, 42);
739        assert!((node.weight - 1.0).abs() < 1e-6);
740
741        let weighted = RingNode::with_weight("hello", 0.5);
742        assert_eq!(weighted.value, "hello");
743        assert!((weighted.weight - 0.5).abs() < 1e-6);
744    }
745
746    #[test]
747    fn test_build_weighted_ring() {
748        let episodes = vec!["a", "b", "c"];
749        let weights = vec![0.1, 0.5, 0.9];
750
751        let ring = build_weighted_ring(episodes, &weights);
752
753        assert_eq!(ring.len(), 3);
754        assert_eq!(ring.get(0).value, "a");
755        assert!((ring.get(0).weight - 0.1).abs() < 1e-6);
756        assert!((ring.get(2).weight - 0.9).abs() < 1e-6);
757    }
758
759    // ========== Dual Ring Tests ==========
760
761    #[test]
762    fn test_dual_ring_creation() {
763        let nodes = vec![
764            DualRingNode::new(0, "a", 0.3),
765            DualRingNode::new(1, "b", 0.9),
766            DualRingNode::new(2, "c", 0.5),
767            DualRingNode::new(3, "d", 0.1),
768        ];
769
770        let dual = DualRing::new(nodes);
771        assert_eq!(dual.len(), 4);
772    }
773
774    #[test]
775    fn test_dual_ring_temporal_order() {
776        let nodes = vec![
777            DualRingNode::new(0, "first", 0.3),
778            DualRingNode::new(1, "second", 0.9),
779            DualRingNode::new(2, "third", 0.5),
780        ];
781
782        let dual = DualRing::new(nodes);
783
784        // Temporal iteration should preserve order
785        let temporal: Vec<_> = dual.iter_temporal().map(|n| n.value).collect();
786        assert_eq!(temporal, vec!["first", "second", "third"]);
787    }
788
789    #[test]
790    fn test_dual_ring_influence_order() {
791        let nodes = vec![
792            DualRingNode::new(0, "low", 0.1),
793            DualRingNode::new(1, "high", 0.9),
794            DualRingNode::new(2, "medium", 0.5),
795        ];
796
797        let dual = DualRing::new(nodes);
798
799        // Influence iteration should be sorted by influence (descending)
800        let influence: Vec<_> = dual.iter_by_influence().map(|n| n.value).collect();
801        assert_eq!(influence, vec!["high", "medium", "low"]);
802    }
803
804    #[test]
805    fn test_dual_ring_top_influential() {
806        let nodes = vec![
807            DualRingNode::new(0, "a", 0.2),
808            DualRingNode::new(1, "b", 0.8),
809            DualRingNode::new(2, "c", 0.5),
810            DualRingNode::new(3, "d", 0.9),
811            DualRingNode::new(4, "e", 0.1),
812        ];
813
814        let dual = DualRing::new(nodes);
815
816        let top2: Vec<_> = dual.top_influential(2).map(|n| n.value).collect();
817        assert_eq!(top2, vec!["d", "b"]); // 0.9, 0.8
818    }
819
820    #[test]
821    fn test_dual_ring_influence_rank() {
822        let nodes = vec![
823            DualRingNode::new(0, "a", 0.1),
824            DualRingNode::new(1, "b", 0.9),
825            DualRingNode::new(2, "c", 0.5),
826        ];
827
828        let dual = DualRing::new(nodes);
829
830        // Temporal idx 1 has highest influence (0.9) → rank 0
831        assert_eq!(dual.influence_rank_of(1), 0);
832        // Temporal idx 2 has medium influence (0.5) → rank 1
833        assert_eq!(dual.influence_rank_of(2), 1);
834        // Temporal idx 0 has lowest influence (0.1) → rank 2
835        assert_eq!(dual.influence_rank_of(0), 2);
836    }
837
838    #[test]
839    fn test_dual_ring_temporal_distance() {
840        let nodes: Vec<DualRingNode<i32>> = (0..5)
841            .map(|i| DualRingNode::new(i, i as i32, 0.5))
842            .collect();
843
844        let dual = DualRing::new(nodes);
845
846        assert_eq!(dual.temporal_distance(0, 1), 1);
847        assert_eq!(dual.temporal_distance(0, 4), 1); // Wrap around
848        assert_eq!(dual.temporal_distance(0, 2), 2);
849    }
850
851    #[test]
852    fn test_dual_ring_influence_distance() {
853        let nodes = vec![
854            DualRingNode::new(0, "a", 0.1), // Rank 2
855            DualRingNode::new(1, "b", 0.9), // Rank 0
856            DualRingNode::new(2, "c", 0.5), // Rank 1
857        ];
858
859        let dual = DualRing::new(nodes);
860
861        // Distance between rank 0 (temporal 1) and rank 1 (temporal 2)
862        assert_eq!(dual.influence_distance(1, 2), 1);
863        // Distance between rank 0 (temporal 1) and rank 2 (temporal 0)
864        assert_eq!(dual.influence_distance(1, 0), 1); // Wrap: 2→0 or 0→2
865    }
866
867    #[test]
868    fn test_dual_ring_temporal_neighbors() {
869        let nodes: Vec<DualRingNode<i32>> = (0..5)
870            .map(|i| DualRingNode::new(i, i as i32, 0.5))
871            .collect();
872
873        let dual = DualRing::new(nodes);
874
875        // Neighbors of 0 with radius 1: prev=4, next=1
876        let neighbors: Vec<_> = dual.temporal_neighbors(0, 1).map(|n| n.value).collect();
877        assert_eq!(neighbors.len(), 2);
878        assert!(neighbors.contains(&4));
879        assert!(neighbors.contains(&1));
880    }
881
882    #[test]
883    fn test_dual_ring_update_influence() {
884        let nodes = vec![
885            DualRingNode::new(0, "a", 0.1),
886            DualRingNode::new(1, "b", 0.9),
887            DualRingNode::new(2, "c", 0.5),
888        ];
889
890        let mut dual = DualRing::new(nodes);
891
892        // Initially, b (idx 1) is most influential
893        assert_eq!(dual.get_by_influence_rank(0).value, "b");
894
895        // Update a's influence to be highest
896        dual.update_influence(0, 0.99);
897
898        // Now a (idx 0) should be most influential
899        assert_eq!(dual.get_by_influence_rank(0).value, "a");
900    }
901
902    #[test]
903    fn test_dual_ring_influence_temporal_spread() {
904        // Use 6+ nodes so top 5 influential selection matters
905        // Clustered: high influence nodes are adjacent (positions 0, 1, 2)
906        let clustered = vec![
907            DualRingNode::new(0, "a", 0.95), // high - position 0
908            DualRingNode::new(1, "b", 0.90), // high - position 1
909            DualRingNode::new(2, "c", 0.85), // high - position 2
910            DualRingNode::new(3, "d", 0.80), // high - position 3
911            DualRingNode::new(4, "e", 0.75), // top 5 cutoff
912            DualRingNode::new(5, "f", 0.10), // excluded from top 5
913            DualRingNode::new(6, "g", 0.10), // excluded from top 5
914        ];
915        let dual_clustered = DualRing::new(clustered);
916        // Top 5: positions 0,1,2,3,4 - all adjacent, avg distance ~1.6
917
918        // Spread: high influence nodes are spread apart
919        let spread = vec![
920            DualRingNode::new(0, "a", 0.95), // high - position 0
921            DualRingNode::new(1, "b", 0.10), // low
922            DualRingNode::new(2, "c", 0.90), // high - position 2
923            DualRingNode::new(3, "d", 0.10), // low
924            DualRingNode::new(4, "e", 0.85), // high - position 4
925            DualRingNode::new(5, "f", 0.10), // low
926            DualRingNode::new(6, "g", 0.80), // high - position 6
927        ];
928        let dual_spread = DualRing::new(spread);
929        // Top 5: positions 0,2,4,6, and one of {1,3,5} - more spread, avg distance ~2.0
930
931        // Spread should have higher value than clustered (more temporal distance between influential nodes)
932        assert!(
933            dual_spread.influence_temporal_spread() >= dual_clustered.influence_temporal_spread(),
934            "spread={}, clustered={}",
935            dual_spread.influence_temporal_spread(),
936            dual_clustered.influence_temporal_spread()
937        );
938    }
939
940    #[test]
941    fn test_build_dual_ring() {
942        let episodes = vec!["a", "b", "c"];
943        let influences = vec![0.3, 0.9, 0.5];
944
945        let dual = build_dual_ring(episodes, &influences);
946
947        assert_eq!(dual.len(), 3);
948        assert_eq!(dual.get_temporal(0).value, "a");
949        assert!((dual.get_temporal(1).influence - 0.9).abs() < 1e-6);
950    }
951
952    #[test]
953    fn test_build_dual_ring_with_attention() {
954        let episodes = vec!["a", "b"];
955        let influences = vec![0.5, 0.8];
956        let received = vec![0.3, 0.7];
957        let given = vec![0.4, 0.6];
958
959        let dual = build_dual_ring_with_attention(episodes, &influences, &received, &given);
960
961        assert_eq!(dual.len(), 2);
962        assert!((dual.get_temporal(0).attention_received - 0.3).abs() < 1e-6);
963        assert!((dual.get_temporal(1).attention_given - 0.6).abs() < 1e-6);
964    }
965}