use std::collections::VecDeque;
use ad_core_rs::ndarray::NDArray;
use ad_core_rs::ndarray_pool::NDArrayPool;
use ad_core_rs::plugin::runtime::{NDPluginProcess, ParamUpdate, ProcessResult};
pub struct AttrPlotProcessor {
attributes: Vec<String>,
buffers: Vec<VecDeque<f64>>,
uid_buffer: VecDeque<f64>,
max_points: usize,
initialized: bool,
last_uid: i32,
attr_param_indices: Vec<usize>,
uid_param_index: Option<usize>,
n_data_param_index: Option<usize>,
}
impl AttrPlotProcessor {
pub fn new(max_points: usize) -> Self {
Self {
attributes: Vec::new(),
buffers: Vec::new(),
uid_buffer: VecDeque::new(),
max_points,
initialized: false,
last_uid: -1,
attr_param_indices: Vec::new(),
uid_param_index: None,
n_data_param_index: None,
}
}
pub fn set_param_indices(
&mut self,
attr_indices: Vec<usize>,
uid_index: usize,
n_data_index: usize,
) {
self.attr_param_indices = attr_indices;
self.uid_param_index = Some(uid_index);
self.n_data_param_index = Some(n_data_index);
}
pub fn attributes(&self) -> &[String] {
&self.attributes
}
pub fn buffer(&self, index: usize) -> Option<&VecDeque<f64>> {
self.buffers.get(index)
}
pub fn uid_buffer(&self) -> &VecDeque<f64> {
&self.uid_buffer
}
pub fn num_attributes(&self) -> usize {
self.attributes.len()
}
pub fn find_attribute(&self, name: &str) -> Option<usize> {
self.attributes.iter().position(|n| n == name)
}
pub fn reset(&mut self) {
self.attributes.clear();
self.buffers.clear();
self.uid_buffer.clear();
self.initialized = false;
self.last_uid = -1;
}
fn push_capped(buf: &mut VecDeque<f64>, value: f64, max_points: usize) {
if max_points > 0 && buf.len() >= max_points {
buf.pop_front();
}
buf.push_back(value);
}
fn initialize_from_array(&mut self, array: &NDArray) {
let mut names: Vec<String> = Vec::new();
for attr in array.attributes.iter() {
if attr.value.as_f64().is_some() {
names.push(attr.name.clone());
}
}
names.sort();
self.buffers = vec![VecDeque::new(); names.len()];
self.attributes = names;
self.uid_buffer.clear();
self.initialized = true;
}
fn clear_buffers(&mut self) {
for buf in &mut self.buffers {
buf.clear();
}
self.uid_buffer.clear();
}
}
impl NDPluginProcess for AttrPlotProcessor {
fn process_array(&mut self, array: &NDArray, _pool: &NDArrayPool) -> ProcessResult {
if self.initialized && array.unique_id < self.last_uid {
self.clear_buffers();
}
self.last_uid = array.unique_id;
if !self.initialized {
self.initialize_from_array(array);
}
Self::push_capped(
&mut self.uid_buffer,
array.unique_id as f64,
self.max_points,
);
for (i, name) in self.attributes.iter().enumerate() {
let value = array
.attributes
.get(name)
.and_then(|attr| attr.value.as_f64())
.unwrap_or(f64::NAN);
Self::push_capped(&mut self.buffers[i], value, self.max_points);
}
let mut updates = Vec::new();
for (i, buf) in self.buffers.iter().enumerate() {
if let Some(¶m) = self.attr_param_indices.get(i) {
updates.push(ParamUpdate::float64_array(
param,
buf.iter().copied().collect(),
));
}
}
if let Some(uid_param) = self.uid_param_index {
updates.push(ParamUpdate::float64_array(
uid_param,
self.uid_buffer.iter().copied().collect(),
));
}
if let Some(n_data_param) = self.n_data_param_index {
updates.push(ParamUpdate::int32(
n_data_param,
self.uid_buffer.len() as i32,
));
}
ProcessResult::sink(updates)
}
fn plugin_type(&self) -> &str {
"NDPluginAttrPlot"
}
}
#[cfg(test)]
mod tests {
use super::*;
use ad_core_rs::attributes::{NDAttrSource, NDAttrValue, NDAttribute};
use ad_core_rs::ndarray::{NDDataType, NDDimension};
fn make_array_with_attrs(uid: i32, attrs: &[(&str, f64)]) -> NDArray {
let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
arr.unique_id = uid;
for (name, value) in attrs {
arr.attributes.add(NDAttribute {
name: name.to_string(),
description: String::new(),
source: NDAttrSource::Driver,
value: NDAttrValue::Float64(*value),
});
}
arr
}
#[test]
fn test_attribute_auto_detection() {
let mut proc = AttrPlotProcessor::new(100);
let pool = NDArrayPool::new(1_000_000);
let mut arr = make_array_with_attrs(1, &[("Temp", 25.0), ("Gain", 1.5)]);
arr.attributes.add(NDAttribute {
name: "Label".to_string(),
description: String::new(),
source: NDAttrSource::Driver,
value: NDAttrValue::String("test".to_string()),
});
proc.process_array(&arr, &pool);
assert_eq!(proc.num_attributes(), 2);
assert_eq!(proc.attributes()[0], "Gain");
assert_eq!(proc.attributes()[1], "Temp");
}
#[test]
fn test_value_tracking() {
let mut proc = AttrPlotProcessor::new(100);
let pool = NDArrayPool::new(1_000_000);
for i in 0..5 {
let arr = make_array_with_attrs(i, &[("Value", i as f64 * 10.0)]);
proc.process_array(&arr, &pool);
}
let idx = proc.find_attribute("Value").unwrap();
let buf = proc.buffer(idx).unwrap();
assert_eq!(buf.len(), 5);
assert!((buf[0] - 0.0).abs() < 1e-10);
assert!((buf[4] - 40.0).abs() < 1e-10);
}
#[test]
fn test_uid_buffer() {
let mut proc = AttrPlotProcessor::new(100);
let pool = NDArrayPool::new(1_000_000);
for i in 1..=3 {
let arr = make_array_with_attrs(i, &[("X", 1.0)]);
proc.process_array(&arr, &pool);
}
let uid_buf = proc.uid_buffer();
assert_eq!(uid_buf.len(), 3);
assert!((uid_buf[0] - 1.0).abs() < 1e-10);
assert!((uid_buf[1] - 2.0).abs() < 1e-10);
assert!((uid_buf[2] - 3.0).abs() < 1e-10);
}
#[test]
fn test_circular_buffer_max_points() {
let mut proc = AttrPlotProcessor::new(3);
let pool = NDArrayPool::new(1_000_000);
for i in 0..5 {
let arr = make_array_with_attrs(i, &[("Val", i as f64)]);
proc.process_array(&arr, &pool);
}
let idx = proc.find_attribute("Val").unwrap();
let buf = proc.buffer(idx).unwrap();
assert_eq!(buf.len(), 3);
assert!((buf[0] - 2.0).abs() < 1e-10);
assert!((buf[1] - 3.0).abs() < 1e-10);
assert!((buf[2] - 4.0).abs() < 1e-10);
assert_eq!(proc.uid_buffer().len(), 3);
}
#[test]
fn test_uid_decrease_resets_buffers() {
let mut proc = AttrPlotProcessor::new(100);
let pool = NDArrayPool::new(1_000_000);
for i in 1..=5 {
let arr = make_array_with_attrs(i, &[("X", i as f64)]);
proc.process_array(&arr, &pool);
}
let idx = proc.find_attribute("X").unwrap();
assert_eq!(proc.buffer(idx).unwrap().len(), 5);
let arr = make_array_with_attrs(1, &[("X", 100.0)]);
proc.process_array(&arr, &pool);
let buf = proc.buffer(idx).unwrap();
assert_eq!(buf.len(), 1);
assert!((buf[0] - 100.0).abs() < 1e-10);
}
#[test]
fn test_missing_attribute_uses_nan() {
let mut proc = AttrPlotProcessor::new(100);
let pool = NDArrayPool::new(1_000_000);
let arr1 = make_array_with_attrs(1, &[("Temp", 25.0)]);
proc.process_array(&arr1, &pool);
let arr2 = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
let mut arr2 = arr2;
arr2.unique_id = 2;
proc.process_array(&arr2, &pool);
let idx = proc.find_attribute("Temp").unwrap();
let buf = proc.buffer(idx).unwrap();
assert_eq!(buf.len(), 2);
assert!((buf[0] - 25.0).abs() < 1e-10);
assert!(buf[1].is_nan());
}
#[test]
fn test_manual_reset() {
let mut proc = AttrPlotProcessor::new(100);
let pool = NDArrayPool::new(1_000_000);
let arr = make_array_with_attrs(1, &[("A", 1.0), ("B", 2.0)]);
proc.process_array(&arr, &pool);
assert_eq!(proc.num_attributes(), 2);
proc.reset();
assert_eq!(proc.num_attributes(), 0);
assert!(proc.uid_buffer().is_empty());
let arr2 = make_array_with_attrs(1, &[("C", 3.0)]);
proc.process_array(&arr2, &pool);
assert_eq!(proc.num_attributes(), 1);
assert_eq!(proc.attributes()[0], "C");
}
#[test]
fn test_unlimited_buffer() {
let mut proc = AttrPlotProcessor::new(0);
let pool = NDArrayPool::new(1_000_000);
for i in 0..100 {
let arr = make_array_with_attrs(i, &[("X", i as f64)]);
proc.process_array(&arr, &pool);
}
let idx = proc.find_attribute("X").unwrap();
assert_eq!(proc.buffer(idx).unwrap().len(), 100);
}
#[test]
fn test_multiple_attributes_sorted() {
let mut proc = AttrPlotProcessor::new(100);
let pool = NDArrayPool::new(1_000_000);
let arr = make_array_with_attrs(1, &[("Zebra", 1.0), ("Alpha", 2.0), ("Mid", 3.0)]);
proc.process_array(&arr, &pool);
assert_eq!(proc.attributes(), &["Alpha", "Mid", "Zebra"]);
}
#[test]
fn test_find_attribute() {
let mut proc = AttrPlotProcessor::new(100);
let pool = NDArrayPool::new(1_000_000);
let arr = make_array_with_attrs(1, &[("X", 1.0), ("Y", 2.0)]);
proc.process_array(&arr, &pool);
assert_eq!(proc.find_attribute("X"), Some(0));
assert_eq!(proc.find_attribute("Y"), Some(1));
assert_eq!(proc.find_attribute("Z"), None);
}
#[test]
fn test_plugin_type() {
let proc = AttrPlotProcessor::new(100);
assert_eq!(proc.plugin_type(), "NDPluginAttrPlot");
}
}