Skip to main content

ad_plugins_rs/
attr_plot.rs

1//! NDPluginAttrPlot: tracks numeric attribute values over time in circular buffers.
2//!
3//! On the first frame, the plugin scans the array's attribute list and auto-detects
4//! all numeric attributes (those where `as_f64()` returns `Some`). The attribute names
5//! are sorted alphabetically for deterministic ordering. On subsequent frames, each
6//! tracked attribute's value is pushed into a per-attribute circular buffer (VecDeque).
7//!
8//! If the array's `unique_id` decreases relative to the previous frame, all buffers
9//! are reset (indicating a new acquisition).
10
11use std::collections::VecDeque;
12
13use ad_core_rs::ndarray::NDArray;
14use ad_core_rs::ndarray_pool::NDArrayPool;
15use ad_core_rs::plugin::runtime::{NDPluginProcess, ProcessResult};
16
17/// Processor that tracks attribute values over time in circular buffers.
18pub struct AttrPlotProcessor {
19    /// Tracked attribute names (sorted alphabetically).
20    attributes: Vec<String>,
21    /// Per-attribute circular buffer of values.
22    buffers: Vec<VecDeque<f64>>,
23    /// Circular buffer of unique_id values.
24    uid_buffer: VecDeque<f64>,
25    /// Maximum number of points per buffer. 0 = unlimited.
26    max_points: usize,
27    /// Whether we have initialized from the first frame.
28    initialized: bool,
29    /// The unique_id from the last processed frame.
30    last_uid: i32,
31}
32
33impl AttrPlotProcessor {
34    /// Create a new processor with the given maximum buffer size.
35    pub fn new(max_points: usize) -> Self {
36        Self {
37            attributes: Vec::new(),
38            buffers: Vec::new(),
39            uid_buffer: VecDeque::new(),
40            max_points,
41            initialized: false,
42            last_uid: -1,
43        }
44    }
45
46    /// Get the list of tracked attribute names.
47    pub fn attributes(&self) -> &[String] {
48        &self.attributes
49    }
50
51    /// Get the circular buffer for a specific attribute index.
52    pub fn buffer(&self, index: usize) -> Option<&VecDeque<f64>> {
53        self.buffers.get(index)
54    }
55
56    /// Get the unique_id buffer.
57    pub fn uid_buffer(&self) -> &VecDeque<f64> {
58        &self.uid_buffer
59    }
60
61    /// Get the number of tracked attributes.
62    pub fn num_attributes(&self) -> usize {
63        self.attributes.len()
64    }
65
66    /// Find the index of a named attribute. Returns None if not tracked.
67    pub fn find_attribute(&self, name: &str) -> Option<usize> {
68        self.attributes.iter().position(|n| n == name)
69    }
70
71    /// Reset all buffers and re-initialize on the next frame.
72    pub fn reset(&mut self) {
73        self.attributes.clear();
74        self.buffers.clear();
75        self.uid_buffer.clear();
76        self.initialized = false;
77        self.last_uid = -1;
78    }
79
80    /// Push a value to a VecDeque, enforcing max_points as a ring buffer.
81    fn push_capped(buf: &mut VecDeque<f64>, value: f64, max_points: usize) {
82        if max_points > 0 && buf.len() >= max_points {
83            buf.pop_front();
84        }
85        buf.push_back(value);
86    }
87
88    /// Initialize tracked attributes from the first frame.
89    fn initialize_from_array(&mut self, array: &NDArray) {
90        let mut names: Vec<String> = Vec::new();
91        for attr in array.attributes.iter() {
92            if attr.value.as_f64().is_some() {
93                names.push(attr.name.clone());
94            }
95        }
96        names.sort();
97
98        self.buffers = vec![VecDeque::new(); names.len()];
99        self.attributes = names;
100        self.uid_buffer.clear();
101        self.initialized = true;
102    }
103
104    /// Clear all data buffers but keep tracked attributes.
105    fn clear_buffers(&mut self) {
106        for buf in &mut self.buffers {
107            buf.clear();
108        }
109        self.uid_buffer.clear();
110    }
111}
112
113impl NDPluginProcess for AttrPlotProcessor {
114    fn process_array(&mut self, array: &NDArray, _pool: &NDArrayPool) -> ProcessResult {
115        // Detect UID decrease (re-acquisition)
116        if self.initialized && array.unique_id < self.last_uid {
117            self.clear_buffers();
118        }
119        self.last_uid = array.unique_id;
120
121        // Initialize on first frame
122        if !self.initialized {
123            self.initialize_from_array(array);
124        }
125
126        // Push unique_id
127        Self::push_capped(
128            &mut self.uid_buffer,
129            array.unique_id as f64,
130            self.max_points,
131        );
132
133        // Push each tracked attribute's value
134        for (i, name) in self.attributes.iter().enumerate() {
135            let value = array
136                .attributes
137                .get(name)
138                .and_then(|attr| attr.value.as_f64())
139                .unwrap_or(0.0);
140            Self::push_capped(&mut self.buffers[i], value, self.max_points);
141        }
142
143        ProcessResult::sink(vec![])
144    }
145
146    fn plugin_type(&self) -> &str {
147        "NDPluginAttrPlot"
148    }
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154    use ad_core_rs::attributes::{NDAttrSource, NDAttrValue, NDAttribute};
155    use ad_core_rs::ndarray::{NDDataType, NDDimension};
156
157    fn make_array_with_attrs(uid: i32, attrs: &[(&str, f64)]) -> NDArray {
158        let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
159        arr.unique_id = uid;
160        for (name, value) in attrs {
161            arr.attributes.add(NDAttribute {
162                name: name.to_string(),
163                description: String::new(),
164                source: NDAttrSource::Driver,
165                value: NDAttrValue::Float64(*value),
166            });
167        }
168        arr
169    }
170
171    #[test]
172    fn test_attribute_auto_detection() {
173        let mut proc = AttrPlotProcessor::new(100);
174        let pool = NDArrayPool::new(1_000_000);
175
176        let mut arr = make_array_with_attrs(1, &[("Temp", 25.0), ("Gain", 1.5)]);
177        // Add a string attribute that should be excluded
178        arr.attributes.add(NDAttribute {
179            name: "Label".to_string(),
180            description: String::new(),
181            source: NDAttrSource::Driver,
182            value: NDAttrValue::String("test".to_string()),
183        });
184
185        proc.process_array(&arr, &pool);
186
187        // Should detect 2 numeric attributes, sorted
188        assert_eq!(proc.num_attributes(), 2);
189        assert_eq!(proc.attributes()[0], "Gain");
190        assert_eq!(proc.attributes()[1], "Temp");
191    }
192
193    #[test]
194    fn test_value_tracking() {
195        let mut proc = AttrPlotProcessor::new(100);
196        let pool = NDArrayPool::new(1_000_000);
197
198        for i in 0..5 {
199            let arr = make_array_with_attrs(i, &[("Value", i as f64 * 10.0)]);
200            proc.process_array(&arr, &pool);
201        }
202
203        let idx = proc.find_attribute("Value").unwrap();
204        let buf = proc.buffer(idx).unwrap();
205        assert_eq!(buf.len(), 5);
206        assert!((buf[0] - 0.0).abs() < 1e-10);
207        assert!((buf[4] - 40.0).abs() < 1e-10);
208    }
209
210    #[test]
211    fn test_uid_buffer() {
212        let mut proc = AttrPlotProcessor::new(100);
213        let pool = NDArrayPool::new(1_000_000);
214
215        for i in 1..=3 {
216            let arr = make_array_with_attrs(i, &[("X", 1.0)]);
217            proc.process_array(&arr, &pool);
218        }
219
220        let uid_buf = proc.uid_buffer();
221        assert_eq!(uid_buf.len(), 3);
222        assert!((uid_buf[0] - 1.0).abs() < 1e-10);
223        assert!((uid_buf[1] - 2.0).abs() < 1e-10);
224        assert!((uid_buf[2] - 3.0).abs() < 1e-10);
225    }
226
227    #[test]
228    fn test_circular_buffer_max_points() {
229        let mut proc = AttrPlotProcessor::new(3);
230        let pool = NDArrayPool::new(1_000_000);
231
232        for i in 0..5 {
233            let arr = make_array_with_attrs(i, &[("Val", i as f64)]);
234            proc.process_array(&arr, &pool);
235        }
236
237        let idx = proc.find_attribute("Val").unwrap();
238        let buf = proc.buffer(idx).unwrap();
239        // Only last 3 values should remain
240        assert_eq!(buf.len(), 3);
241        assert!((buf[0] - 2.0).abs() < 1e-10);
242        assert!((buf[1] - 3.0).abs() < 1e-10);
243        assert!((buf[2] - 4.0).abs() < 1e-10);
244
245        // UID buffer also limited
246        assert_eq!(proc.uid_buffer().len(), 3);
247    }
248
249    #[test]
250    fn test_uid_decrease_resets_buffers() {
251        let mut proc = AttrPlotProcessor::new(100);
252        let pool = NDArrayPool::new(1_000_000);
253
254        // First acquisition
255        for i in 1..=5 {
256            let arr = make_array_with_attrs(i, &[("X", i as f64)]);
257            proc.process_array(&arr, &pool);
258        }
259
260        let idx = proc.find_attribute("X").unwrap();
261        assert_eq!(proc.buffer(idx).unwrap().len(), 5);
262
263        // New acquisition: UID resets to 1
264        let arr = make_array_with_attrs(1, &[("X", 100.0)]);
265        proc.process_array(&arr, &pool);
266
267        // Buffers should be cleared and have just the new point
268        let buf = proc.buffer(idx).unwrap();
269        assert_eq!(buf.len(), 1);
270        assert!((buf[0] - 100.0).abs() < 1e-10);
271    }
272
273    #[test]
274    fn test_missing_attribute_uses_zero() {
275        let mut proc = AttrPlotProcessor::new(100);
276        let pool = NDArrayPool::new(1_000_000);
277
278        // Frame 1: has attribute
279        let arr1 = make_array_with_attrs(1, &[("Temp", 25.0)]);
280        proc.process_array(&arr1, &pool);
281
282        // Frame 2: attribute missing
283        let arr2 = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
284        let mut arr2 = arr2;
285        arr2.unique_id = 2;
286        proc.process_array(&arr2, &pool);
287
288        let idx = proc.find_attribute("Temp").unwrap();
289        let buf = proc.buffer(idx).unwrap();
290        assert_eq!(buf.len(), 2);
291        assert!((buf[0] - 25.0).abs() < 1e-10);
292        assert!((buf[1] - 0.0).abs() < 1e-10);
293    }
294
295    #[test]
296    fn test_manual_reset() {
297        let mut proc = AttrPlotProcessor::new(100);
298        let pool = NDArrayPool::new(1_000_000);
299
300        let arr = make_array_with_attrs(1, &[("A", 1.0), ("B", 2.0)]);
301        proc.process_array(&arr, &pool);
302        assert_eq!(proc.num_attributes(), 2);
303
304        proc.reset();
305        assert_eq!(proc.num_attributes(), 0);
306        assert!(proc.uid_buffer().is_empty());
307
308        // Re-initializes from next frame
309        let arr2 = make_array_with_attrs(1, &[("C", 3.0)]);
310        proc.process_array(&arr2, &pool);
311        assert_eq!(proc.num_attributes(), 1);
312        assert_eq!(proc.attributes()[0], "C");
313    }
314
315    #[test]
316    fn test_unlimited_buffer() {
317        let mut proc = AttrPlotProcessor::new(0);
318        let pool = NDArrayPool::new(1_000_000);
319
320        for i in 0..100 {
321            let arr = make_array_with_attrs(i, &[("X", i as f64)]);
322            proc.process_array(&arr, &pool);
323        }
324
325        let idx = proc.find_attribute("X").unwrap();
326        assert_eq!(proc.buffer(idx).unwrap().len(), 100);
327    }
328
329    #[test]
330    fn test_multiple_attributes_sorted() {
331        let mut proc = AttrPlotProcessor::new(100);
332        let pool = NDArrayPool::new(1_000_000);
333
334        let arr = make_array_with_attrs(1, &[("Zebra", 1.0), ("Alpha", 2.0), ("Mid", 3.0)]);
335        proc.process_array(&arr, &pool);
336
337        assert_eq!(proc.attributes(), &["Alpha", "Mid", "Zebra"]);
338    }
339
340    #[test]
341    fn test_find_attribute() {
342        let mut proc = AttrPlotProcessor::new(100);
343        let pool = NDArrayPool::new(1_000_000);
344
345        let arr = make_array_with_attrs(1, &[("X", 1.0), ("Y", 2.0)]);
346        proc.process_array(&arr, &pool);
347
348        assert_eq!(proc.find_attribute("X"), Some(0));
349        assert_eq!(proc.find_attribute("Y"), Some(1));
350        assert_eq!(proc.find_attribute("Z"), None);
351    }
352
353    #[test]
354    fn test_plugin_type() {
355        let proc = AttrPlotProcessor::new(100);
356        assert_eq!(proc.plugin_type(), "NDPluginAttrPlot");
357    }
358}