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, ParamUpdate, 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 attr_param_indices: Vec<usize>,
34 uid_param_index: Option<usize>,
36 n_data_param_index: Option<usize>,
38}
39
40impl AttrPlotProcessor {
41 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 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 pub fn attributes(&self) -> &[String] {
73 &self.attributes
74 }
75
76 pub fn buffer(&self, index: usize) -> Option<&VecDeque<f64>> {
78 self.buffers.get(index)
79 }
80
81 pub fn uid_buffer(&self) -> &VecDeque<f64> {
83 &self.uid_buffer
84 }
85
86 pub fn num_attributes(&self) -> usize {
88 self.attributes.len()
89 }
90
91 pub fn find_attribute(&self, name: &str) -> Option<usize> {
93 self.attributes.iter().position(|n| n == name)
94 }
95
96 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 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 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 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 if self.initialized && array.unique_id < self.last_uid {
142 self.clear_buffers();
143 }
144 self.last_uid = array.unique_id;
145
146 if !self.initialized {
148 self.initialize_from_array(array);
149 }
150
151 Self::push_capped(
153 &mut self.uid_buffer,
154 array.unique_id as f64,
155 self.max_points,
156 );
157
158 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 let mut updates = Vec::new();
170 for (i, buf) in self.buffers.iter().enumerate() {
171 if let Some(¶m) = 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 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 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 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 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 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 let arr = make_array_with_attrs(1, &[("X", 100.0)]);
313 proc.process_array(&arr, &pool);
314
315 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 let arr1 = make_array_with_attrs(1, &[("Temp", 25.0)]);
328 proc.process_array(&arr1, &pool);
329
330 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 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}