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, ParamUpdate, 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    /// Parameter indices for per-attribute waveform output.
32    /// Set by `set_param_indices` after plugin registration.
33    attr_param_indices: Vec<usize>,
34    /// Parameter index for the UID waveform.
35    uid_param_index: Option<usize>,
36    /// Parameter index for the number of data points.
37    n_data_param_index: Option<usize>,
38}
39
40impl AttrPlotProcessor {
41    /// Create a new processor with the given maximum buffer size.
42    pub fn new(max_points: usize) -> Self {
43        Self {
44            attributes: Vec::new(),
45            buffers: Vec::new(),
46            uid_buffer: VecDeque::new(),
47            max_points,
48            initialized: false,
49            last_uid: -1,
50            attr_param_indices: Vec::new(),
51            uid_param_index: None,
52            n_data_param_index: None,
53        }
54    }
55
56    /// Set param indices for waveform output after plugin registration.
57    /// `attr_indices`: one Float64Array param per attribute slot.
58    /// `uid_index`: Float64Array param for UID buffer.
59    /// `n_data_index`: Int32 param for number of data points.
60    pub fn set_param_indices(
61        &mut self,
62        attr_indices: Vec<usize>,
63        uid_index: usize,
64        n_data_index: usize,
65    ) {
66        self.attr_param_indices = attr_indices;
67        self.uid_param_index = Some(uid_index);
68        self.n_data_param_index = Some(n_data_index);
69    }
70
71    /// Get the list of tracked attribute names.
72    pub fn attributes(&self) -> &[String] {
73        &self.attributes
74    }
75
76    /// Get the circular buffer for a specific attribute index.
77    pub fn buffer(&self, index: usize) -> Option<&VecDeque<f64>> {
78        self.buffers.get(index)
79    }
80
81    /// Get the unique_id buffer.
82    pub fn uid_buffer(&self) -> &VecDeque<f64> {
83        &self.uid_buffer
84    }
85
86    /// Get the number of tracked attributes.
87    pub fn num_attributes(&self) -> usize {
88        self.attributes.len()
89    }
90
91    /// Find the index of a named attribute. Returns None if not tracked.
92    pub fn find_attribute(&self, name: &str) -> Option<usize> {
93        self.attributes.iter().position(|n| n == name)
94    }
95
96    /// Reset all buffers and re-initialize on the next frame.
97    pub fn reset(&mut self) {
98        self.attributes.clear();
99        self.buffers.clear();
100        self.uid_buffer.clear();
101        self.initialized = false;
102        self.last_uid = -1;
103    }
104
105    /// Push a value to a VecDeque, enforcing max_points as a ring buffer.
106    fn push_capped(buf: &mut VecDeque<f64>, value: f64, max_points: usize) {
107        if max_points > 0 && buf.len() >= max_points {
108            buf.pop_front();
109        }
110        buf.push_back(value);
111    }
112
113    /// Initialize tracked attributes from the first frame.
114    fn initialize_from_array(&mut self, array: &NDArray) {
115        let mut names: Vec<String> = Vec::new();
116        for attr in array.attributes.iter() {
117            if attr.value.as_f64().is_some() {
118                names.push(attr.name.clone());
119            }
120        }
121        names.sort();
122
123        self.buffers = vec![VecDeque::new(); names.len()];
124        self.attributes = names;
125        self.uid_buffer.clear();
126        self.initialized = true;
127    }
128
129    /// Clear all data buffers but keep tracked attributes.
130    fn clear_buffers(&mut self) {
131        for buf in &mut self.buffers {
132            buf.clear();
133        }
134        self.uid_buffer.clear();
135    }
136}
137
138impl NDPluginProcess for AttrPlotProcessor {
139    fn process_array(&mut self, array: &NDArray, _pool: &NDArrayPool) -> ProcessResult {
140        // Detect UID decrease (re-acquisition)
141        if self.initialized && array.unique_id < self.last_uid {
142            self.clear_buffers();
143        }
144        self.last_uid = array.unique_id;
145
146        // Initialize on first frame
147        if !self.initialized {
148            self.initialize_from_array(array);
149        }
150
151        // Push unique_id
152        Self::push_capped(
153            &mut self.uid_buffer,
154            array.unique_id as f64,
155            self.max_points,
156        );
157
158        // Push each tracked attribute's value
159        for (i, name) in self.attributes.iter().enumerate() {
160            let value = array
161                .attributes
162                .get(name)
163                .and_then(|attr| attr.value.as_f64())
164                .unwrap_or(f64::NAN);
165            Self::push_capped(&mut self.buffers[i], value, self.max_points);
166        }
167
168        // Write buffers to params for waveform readback
169        let mut updates = Vec::new();
170        for (i, buf) in self.buffers.iter().enumerate() {
171            if let Some(&param) = self.attr_param_indices.get(i) {
172                updates.push(ParamUpdate::float64_array(
173                    param,
174                    buf.iter().copied().collect(),
175                ));
176            }
177        }
178        if let Some(uid_param) = self.uid_param_index {
179            updates.push(ParamUpdate::float64_array(
180                uid_param,
181                self.uid_buffer.iter().copied().collect(),
182            ));
183        }
184        if let Some(n_data_param) = self.n_data_param_index {
185            updates.push(ParamUpdate::int32(
186                n_data_param,
187                self.uid_buffer.len() as i32,
188            ));
189        }
190
191        ProcessResult::sink(updates)
192    }
193
194    fn plugin_type(&self) -> &str {
195        "NDPluginAttrPlot"
196    }
197}
198
199#[cfg(test)]
200mod tests {
201    use super::*;
202    use ad_core_rs::attributes::{NDAttrSource, NDAttrValue, NDAttribute};
203    use ad_core_rs::ndarray::{NDDataType, NDDimension};
204
205    fn make_array_with_attrs(uid: i32, attrs: &[(&str, f64)]) -> NDArray {
206        let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
207        arr.unique_id = uid;
208        for (name, value) in attrs {
209            arr.attributes.add(NDAttribute {
210                name: name.to_string(),
211                description: String::new(),
212                source: NDAttrSource::Driver,
213                value: NDAttrValue::Float64(*value),
214            });
215        }
216        arr
217    }
218
219    #[test]
220    fn test_attribute_auto_detection() {
221        let mut proc = AttrPlotProcessor::new(100);
222        let pool = NDArrayPool::new(1_000_000);
223
224        let mut arr = make_array_with_attrs(1, &[("Temp", 25.0), ("Gain", 1.5)]);
225        // Add a string attribute that should be excluded
226        arr.attributes.add(NDAttribute {
227            name: "Label".to_string(),
228            description: String::new(),
229            source: NDAttrSource::Driver,
230            value: NDAttrValue::String("test".to_string()),
231        });
232
233        proc.process_array(&arr, &pool);
234
235        // Should detect 2 numeric attributes, sorted
236        assert_eq!(proc.num_attributes(), 2);
237        assert_eq!(proc.attributes()[0], "Gain");
238        assert_eq!(proc.attributes()[1], "Temp");
239    }
240
241    #[test]
242    fn test_value_tracking() {
243        let mut proc = AttrPlotProcessor::new(100);
244        let pool = NDArrayPool::new(1_000_000);
245
246        for i in 0..5 {
247            let arr = make_array_with_attrs(i, &[("Value", i as f64 * 10.0)]);
248            proc.process_array(&arr, &pool);
249        }
250
251        let idx = proc.find_attribute("Value").unwrap();
252        let buf = proc.buffer(idx).unwrap();
253        assert_eq!(buf.len(), 5);
254        assert!((buf[0] - 0.0).abs() < 1e-10);
255        assert!((buf[4] - 40.0).abs() < 1e-10);
256    }
257
258    #[test]
259    fn test_uid_buffer() {
260        let mut proc = AttrPlotProcessor::new(100);
261        let pool = NDArrayPool::new(1_000_000);
262
263        for i in 1..=3 {
264            let arr = make_array_with_attrs(i, &[("X", 1.0)]);
265            proc.process_array(&arr, &pool);
266        }
267
268        let uid_buf = proc.uid_buffer();
269        assert_eq!(uid_buf.len(), 3);
270        assert!((uid_buf[0] - 1.0).abs() < 1e-10);
271        assert!((uid_buf[1] - 2.0).abs() < 1e-10);
272        assert!((uid_buf[2] - 3.0).abs() < 1e-10);
273    }
274
275    #[test]
276    fn test_circular_buffer_max_points() {
277        let mut proc = AttrPlotProcessor::new(3);
278        let pool = NDArrayPool::new(1_000_000);
279
280        for i in 0..5 {
281            let arr = make_array_with_attrs(i, &[("Val", i as f64)]);
282            proc.process_array(&arr, &pool);
283        }
284
285        let idx = proc.find_attribute("Val").unwrap();
286        let buf = proc.buffer(idx).unwrap();
287        // Only last 3 values should remain
288        assert_eq!(buf.len(), 3);
289        assert!((buf[0] - 2.0).abs() < 1e-10);
290        assert!((buf[1] - 3.0).abs() < 1e-10);
291        assert!((buf[2] - 4.0).abs() < 1e-10);
292
293        // UID buffer also limited
294        assert_eq!(proc.uid_buffer().len(), 3);
295    }
296
297    #[test]
298    fn test_uid_decrease_resets_buffers() {
299        let mut proc = AttrPlotProcessor::new(100);
300        let pool = NDArrayPool::new(1_000_000);
301
302        // First acquisition
303        for i in 1..=5 {
304            let arr = make_array_with_attrs(i, &[("X", i as f64)]);
305            proc.process_array(&arr, &pool);
306        }
307
308        let idx = proc.find_attribute("X").unwrap();
309        assert_eq!(proc.buffer(idx).unwrap().len(), 5);
310
311        // New acquisition: UID resets to 1
312        let arr = make_array_with_attrs(1, &[("X", 100.0)]);
313        proc.process_array(&arr, &pool);
314
315        // Buffers should be cleared and have just the new point
316        let buf = proc.buffer(idx).unwrap();
317        assert_eq!(buf.len(), 1);
318        assert!((buf[0] - 100.0).abs() < 1e-10);
319    }
320
321    #[test]
322    fn test_missing_attribute_uses_nan() {
323        let mut proc = AttrPlotProcessor::new(100);
324        let pool = NDArrayPool::new(1_000_000);
325
326        // Frame 1: has attribute
327        let arr1 = make_array_with_attrs(1, &[("Temp", 25.0)]);
328        proc.process_array(&arr1, &pool);
329
330        // Frame 2: attribute missing
331        let arr2 = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
332        let mut arr2 = arr2;
333        arr2.unique_id = 2;
334        proc.process_array(&arr2, &pool);
335
336        let idx = proc.find_attribute("Temp").unwrap();
337        let buf = proc.buffer(idx).unwrap();
338        assert_eq!(buf.len(), 2);
339        assert!((buf[0] - 25.0).abs() < 1e-10);
340        assert!(buf[1].is_nan());
341    }
342
343    #[test]
344    fn test_manual_reset() {
345        let mut proc = AttrPlotProcessor::new(100);
346        let pool = NDArrayPool::new(1_000_000);
347
348        let arr = make_array_with_attrs(1, &[("A", 1.0), ("B", 2.0)]);
349        proc.process_array(&arr, &pool);
350        assert_eq!(proc.num_attributes(), 2);
351
352        proc.reset();
353        assert_eq!(proc.num_attributes(), 0);
354        assert!(proc.uid_buffer().is_empty());
355
356        // Re-initializes from next frame
357        let arr2 = make_array_with_attrs(1, &[("C", 3.0)]);
358        proc.process_array(&arr2, &pool);
359        assert_eq!(proc.num_attributes(), 1);
360        assert_eq!(proc.attributes()[0], "C");
361    }
362
363    #[test]
364    fn test_unlimited_buffer() {
365        let mut proc = AttrPlotProcessor::new(0);
366        let pool = NDArrayPool::new(1_000_000);
367
368        for i in 0..100 {
369            let arr = make_array_with_attrs(i, &[("X", i as f64)]);
370            proc.process_array(&arr, &pool);
371        }
372
373        let idx = proc.find_attribute("X").unwrap();
374        assert_eq!(proc.buffer(idx).unwrap().len(), 100);
375    }
376
377    #[test]
378    fn test_multiple_attributes_sorted() {
379        let mut proc = AttrPlotProcessor::new(100);
380        let pool = NDArrayPool::new(1_000_000);
381
382        let arr = make_array_with_attrs(1, &[("Zebra", 1.0), ("Alpha", 2.0), ("Mid", 3.0)]);
383        proc.process_array(&arr, &pool);
384
385        assert_eq!(proc.attributes(), &["Alpha", "Mid", "Zebra"]);
386    }
387
388    #[test]
389    fn test_find_attribute() {
390        let mut proc = AttrPlotProcessor::new(100);
391        let pool = NDArrayPool::new(1_000_000);
392
393        let arr = make_array_with_attrs(1, &[("X", 1.0), ("Y", 2.0)]);
394        proc.process_array(&arr, &pool);
395
396        assert_eq!(proc.find_attribute("X"), Some(0));
397        assert_eq!(proc.find_attribute("Y"), Some(1));
398        assert_eq!(proc.find_attribute("Z"), None);
399    }
400
401    #[test]
402    fn test_plugin_type() {
403        let proc = AttrPlotProcessor::new(100);
404        assert_eq!(proc.plugin_type(), "NDPluginAttrPlot");
405    }
406}