Skip to main content

irithyll_core/reservoir/
delay_buffer.rs

1//! Circular delay buffer for time-delay embeddings.
2//!
3//! [`DelayBuffer`] stores the last `capacity` observations (each a `Vec<f64>`)
4//! in a ring buffer, enabling efficient construction of time-delay feature vectors
5//! for reservoir computing models like NG-RC.
6//!
7//! Index 0 is the most recent observation, index 1 is the previous, and so on.
8
9use alloc::vec;
10use alloc::vec::Vec;
11
12/// Circular buffer storing the last `capacity` observation vectors.
13///
14/// Observations are pushed one at a time. Once `capacity` observations have
15/// been stored, the buffer is "ready" and older observations are overwritten
16/// in FIFO order.
17///
18/// # Examples
19///
20/// ```
21/// use irithyll_core::reservoir::DelayBuffer;
22///
23/// let mut buf = DelayBuffer::new(3);
24/// buf.push(&[1.0]);
25/// buf.push(&[2.0]);
26/// buf.push(&[3.0]);
27/// assert!(buf.is_ready());
28///
29/// assert_eq!(buf.get(0).unwrap(), &[3.0]); // most recent
30/// assert_eq!(buf.get(1).unwrap(), &[2.0]);
31/// assert_eq!(buf.get(2).unwrap(), &[1.0]); // oldest
32/// ```
33pub struct DelayBuffer {
34    /// Ring buffer storage.
35    data: Vec<Vec<f64>>,
36    /// Capacity (maximum number of stored observations).
37    capacity: usize,
38    /// Write cursor — the index where the next push will write.
39    head: usize,
40    /// Number of observations currently stored (saturates at capacity).
41    count: usize,
42}
43
44impl DelayBuffer {
45    /// Create a new delay buffer with the given capacity.
46    ///
47    /// # Panics
48    ///
49    /// Panics if `capacity` is 0.
50    pub fn new(capacity: usize) -> Self {
51        assert!(capacity > 0, "DelayBuffer capacity must be > 0");
52        Self {
53            data: vec![Vec::new(); capacity],
54            capacity,
55            head: 0,
56            count: 0,
57        }
58    }
59
60    /// Push a new observation into the buffer.
61    ///
62    /// If the buffer is full, the oldest observation is overwritten.
63    pub fn push(&mut self, obs: &[f64]) {
64        self.data[self.head] = obs.to_vec();
65        self.head = (self.head + 1) % self.capacity;
66        if self.count < self.capacity {
67            self.count += 1;
68        }
69    }
70
71    /// Get the observation at the given delay index.
72    ///
73    /// - `delay_idx = 0` returns the most recently pushed observation.
74    /// - `delay_idx = 1` returns the previous observation.
75    /// - Returns `None` if `delay_idx >= len()`.
76    pub fn get(&self, delay_idx: usize) -> Option<&[f64]> {
77        if delay_idx >= self.count {
78            return None;
79        }
80        // head points to the NEXT write slot, so most recent is head - 1.
81        // delay_idx = 0 → most recent = head - 1
82        // delay_idx = 1 → head - 2, etc.
83        let idx = (self.head + self.capacity - 1 - delay_idx) % self.capacity;
84        Some(&self.data[idx])
85    }
86
87    /// Whether the buffer has been filled to capacity at least once.
88    ///
89    /// NG-RC requires `is_ready()` before building features, because all
90    /// `k` delay slots must be populated.
91    #[inline]
92    pub fn is_ready(&self) -> bool {
93        self.count >= self.capacity
94    }
95
96    /// Number of observations currently stored (at most `capacity`).
97    #[inline]
98    pub fn len(&self) -> usize {
99        self.count
100    }
101
102    /// Whether the buffer is empty.
103    #[inline]
104    pub fn is_empty(&self) -> bool {
105        self.count == 0
106    }
107
108    /// The maximum number of observations this buffer can hold.
109    #[inline]
110    pub fn capacity(&self) -> usize {
111        self.capacity
112    }
113
114    /// Reset the buffer, discarding all stored observations.
115    pub fn reset(&mut self) {
116        for slot in &mut self.data {
117            slot.clear();
118        }
119        self.head = 0;
120        self.count = 0;
121    }
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127
128    #[test]
129    fn basic_push_and_get() {
130        let mut buf = DelayBuffer::new(3);
131        buf.push(&[1.0, 10.0]);
132        buf.push(&[2.0, 20.0]);
133        buf.push(&[3.0, 30.0]);
134
135        assert!(buf.is_ready());
136        assert_eq!(buf.len(), 3);
137
138        assert_eq!(buf.get(0).unwrap(), &[3.0, 30.0]);
139        assert_eq!(buf.get(1).unwrap(), &[2.0, 20.0]);
140        assert_eq!(buf.get(2).unwrap(), &[1.0, 10.0]);
141        assert!(buf.get(3).is_none());
142    }
143
144    #[test]
145    fn wraparound_overwrites_oldest() {
146        let mut buf = DelayBuffer::new(2);
147        buf.push(&[1.0]);
148        buf.push(&[2.0]);
149        buf.push(&[3.0]); // overwrites [1.0]
150
151        assert_eq!(buf.len(), 2);
152        assert_eq!(buf.get(0).unwrap(), &[3.0]);
153        assert_eq!(buf.get(1).unwrap(), &[2.0]);
154        assert!(buf.get(2).is_none());
155    }
156
157    #[test]
158    fn not_ready_until_full() {
159        let mut buf = DelayBuffer::new(3);
160        assert!(!buf.is_ready());
161        assert!(buf.is_empty());
162
163        buf.push(&[1.0]);
164        assert!(!buf.is_ready());
165        assert_eq!(buf.len(), 1);
166
167        buf.push(&[2.0]);
168        assert!(!buf.is_ready());
169        assert_eq!(buf.len(), 2);
170
171        buf.push(&[3.0]);
172        assert!(buf.is_ready());
173        assert_eq!(buf.len(), 3);
174    }
175
176    #[test]
177    fn get_returns_none_for_empty() {
178        let buf = DelayBuffer::new(5);
179        assert!(buf.get(0).is_none());
180    }
181
182    #[test]
183    fn get_partial_fill() {
184        let mut buf = DelayBuffer::new(5);
185        buf.push(&[10.0]);
186        buf.push(&[20.0]);
187
188        assert_eq!(buf.get(0).unwrap(), &[20.0]);
189        assert_eq!(buf.get(1).unwrap(), &[10.0]);
190        assert!(buf.get(2).is_none());
191    }
192
193    #[test]
194    fn reset_clears_all() {
195        let mut buf = DelayBuffer::new(3);
196        buf.push(&[1.0]);
197        buf.push(&[2.0]);
198        buf.push(&[3.0]);
199        assert!(buf.is_ready());
200
201        buf.reset();
202        assert!(buf.is_empty());
203        assert!(!buf.is_ready());
204        assert_eq!(buf.len(), 0);
205        assert!(buf.get(0).is_none());
206    }
207
208    #[test]
209    fn capacity_one() {
210        let mut buf = DelayBuffer::new(1);
211        buf.push(&[42.0]);
212        assert!(buf.is_ready());
213        assert_eq!(buf.get(0).unwrap(), &[42.0]);
214
215        buf.push(&[99.0]);
216        assert_eq!(buf.get(0).unwrap(), &[99.0]);
217        assert!(buf.get(1).is_none());
218    }
219
220    #[test]
221    fn many_pushes_stress_test() {
222        let cap = 5;
223        let mut buf = DelayBuffer::new(cap);
224        for i in 0..100 {
225            buf.push(&[i as f64]);
226        }
227        assert!(buf.is_ready());
228        assert_eq!(buf.len(), cap);
229
230        // Most recent should be 99, oldest should be 95.
231        for d in 0..cap {
232            let expected = (99 - d) as f64;
233            assert_eq!(
234                buf.get(d).unwrap(),
235                &[expected],
236                "delay {} should be {}",
237                d,
238                expected,
239            );
240        }
241    }
242
243    #[test]
244    #[should_panic(expected = "capacity must be > 0")]
245    fn zero_capacity_panics() {
246        let _ = DelayBuffer::new(0);
247    }
248}