1use 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
17pub struct AttrPlotProcessor {
19 attributes: Vec<String>,
21 buffers: Vec<VecDeque<f64>>,
23 uid_buffer: VecDeque<f64>,
25 max_points: usize,
27 initialized: bool,
29 last_uid: i32,
31}
32
33impl AttrPlotProcessor {
34 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 pub fn attributes(&self) -> &[String] {
48 &self.attributes
49 }
50
51 pub fn buffer(&self, index: usize) -> Option<&VecDeque<f64>> {
53 self.buffers.get(index)
54 }
55
56 pub fn uid_buffer(&self) -> &VecDeque<f64> {
58 &self.uid_buffer
59 }
60
61 pub fn num_attributes(&self) -> usize {
63 self.attributes.len()
64 }
65
66 pub fn find_attribute(&self, name: &str) -> Option<usize> {
68 self.attributes.iter().position(|n| n == name)
69 }
70
71 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 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 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 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 if self.initialized && array.unique_id < self.last_uid {
117 self.clear_buffers();
118 }
119 self.last_uid = array.unique_id;
120
121 if !self.initialized {
123 self.initialize_from_array(array);
124 }
125
126 Self::push_capped(
128 &mut self.uid_buffer,
129 array.unique_id as f64,
130 self.max_points,
131 );
132
133 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 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 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 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 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 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 let arr = make_array_with_attrs(1, &[("X", 100.0)]);
265 proc.process_array(&arr, &pool);
266
267 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 let arr1 = make_array_with_attrs(1, &[("Temp", 25.0)]);
280 proc.process_array(&arr1, &pool);
281
282 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 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}