1use std::time::Instant;
34
35use ad_core_rs::ndarray::{NDArray, NDDataBuffer, NDDataType};
36use ad_core_rs::ndarray_pool::NDArrayPool;
37use ad_core_rs::plugin::runtime::{
38 NDPluginProcess, ParamChangeResult, ParamUpdate, PluginParamSnapshot, ProcessResult,
39};
40use asyn_rs::param::ParamType;
41use asyn_rs::port::PortDriverBase;
42
43const DEFAULT_NUM_TSPOINTS: usize = 2048;
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48enum AcquireMode {
49 Fixed,
51 Circular,
53}
54
55struct Params {
57 ts_acquire: usize,
58 ts_read: usize,
59 ts_num_points: usize,
60 ts_current_point: usize,
61 ts_time_per_point: usize,
62 ts_averaging_time: usize,
63 ts_num_average: usize,
64 ts_elapsed_time: usize,
65 ts_acquire_mode: usize,
66 ts_time_axis: usize,
67 ts_timestamp: usize,
68 ts_time_series: usize,
69}
70
71impl Params {
72 const fn sentinel() -> Self {
75 Self {
76 ts_acquire: usize::MAX,
77 ts_read: usize::MAX,
78 ts_num_points: usize::MAX,
79 ts_current_point: usize::MAX,
80 ts_time_per_point: usize::MAX,
81 ts_averaging_time: usize::MAX,
82 ts_num_average: usize::MAX,
83 ts_elapsed_time: usize::MAX,
84 ts_acquire_mode: usize::MAX,
85 ts_time_axis: usize::MAX,
86 ts_timestamp: usize::MAX,
87 ts_time_series: usize::MAX,
88 }
89 }
90}
91
92#[inline]
95fn sample_f64(data: &NDDataBuffer, idx: usize) -> f64 {
96 match data {
97 NDDataBuffer::I8(v) => v[idx] as f64,
98 NDDataBuffer::U8(v) => v[idx] as f64,
99 NDDataBuffer::I16(v) => v[idx] as f64,
100 NDDataBuffer::U16(v) => v[idx] as f64,
101 NDDataBuffer::I32(v) => v[idx] as f64,
102 NDDataBuffer::U32(v) => v[idx] as f64,
103 NDDataBuffer::I64(v) => v[idx] as f64,
104 NDDataBuffer::U64(v) => v[idx] as f64,
105 NDDataBuffer::F32(v) => v[idx] as f64,
106 NDDataBuffer::F64(v) => v[idx],
107 }
108}
109
110fn averaged_value(sum: f64, num_averaged: usize, dt: NDDataType) -> f64 {
137 let mean = sum / num_averaged.max(1) as f64;
138 match dt {
139 NDDataType::Float32 => mean as f32 as f64,
140 NDDataType::Float64 => mean,
141 _ => mean.trunc(),
143 }
144}
145
146fn coalesce_updates(updates: Vec<ParamUpdate>) -> Vec<ParamUpdate> {
156 fn key(u: &ParamUpdate) -> (usize, i32) {
157 match u {
158 ParamUpdate::Int32 { reason, addr, .. }
159 | ParamUpdate::Float64 { reason, addr, .. }
160 | ParamUpdate::Octet { reason, addr, .. }
161 | ParamUpdate::Float64Array { reason, addr, .. } => (*reason, *addr),
162 }
163 }
164 let mut seen = std::collections::HashSet::new();
165 let mut out = Vec::with_capacity(updates.len());
166 for u in updates.into_iter().rev() {
168 if seen.insert(key(&u)) {
169 out.push(u);
170 }
171 }
172 out.reverse();
173 out
174}
175
176pub struct TimeSeriesProcessor {
178 max_signals: usize,
180 num_signals_in: i64,
183 num_signals: usize,
185 data_type: NDDataType,
187 num_time_points: usize,
189 current_time_point: usize,
191 num_average: usize,
193 num_averaged: usize,
195 average_store: Vec<f64>,
197 time_per_point: f64,
199 averaging_time_requested: f64,
201 averaging_time_actual: f64,
203 acquire_mode: AcquireMode,
205 acquiring: bool,
207 circular: Vec<f64>,
210 time_stamp: Vec<f64>,
212 start_time: Instant,
214 p: Params,
215}
216
217impl TimeSeriesProcessor {
218 pub fn new(max_signals: usize) -> Self {
221 let max_signals = max_signals.max(1);
222 let num_time_points = DEFAULT_NUM_TSPOINTS;
223 Self {
224 max_signals,
225 num_signals_in: -1,
226 num_signals: max_signals,
227 data_type: NDDataType::Float64,
228 num_time_points,
229 current_time_point: 0,
230 num_average: 1,
231 num_averaged: 0,
232 average_store: vec![0.0; max_signals],
233 time_per_point: 0.0,
234 averaging_time_requested: 1.0,
235 averaging_time_actual: 1.0,
238 acquire_mode: AcquireMode::Fixed,
239 acquiring: false,
240 circular: vec![0.0; max_signals * num_time_points],
241 time_stamp: vec![0.0; num_time_points],
242 start_time: Instant::now(),
243 p: Params::sentinel(),
244 }
245 }
246
247 fn create_axis_array(&self) -> Vec<ParamUpdate> {
252 let axis: Vec<f64> = (0..self.num_time_points)
253 .map(|i| match self.acquire_mode {
254 AcquireMode::Fixed => i as f64 * self.averaging_time_actual,
255 AcquireMode::Circular => {
256 -(((self.num_time_points - 1) - i) as f64) * self.averaging_time_actual
257 }
258 })
259 .collect();
260 vec![ParamUpdate::float64_array(self.p.ts_time_axis, axis)]
261 }
262
263 fn acquire_reset(&mut self) -> Vec<ParamUpdate> {
267 self.circular.iter_mut().for_each(|v| *v = 0.0);
268 self.time_stamp.iter_mut().for_each(|v| *v = 0.0);
269 self.current_time_point = 0;
270 self.start_time = Instant::now();
271 vec![ParamUpdate::int32(self.p.ts_current_point, 0)]
272 }
273
274 fn allocate_arrays(&mut self) -> Vec<ParamUpdate> {
278 self.circular = vec![0.0; self.num_signals * self.num_time_points];
279 self.time_stamp = vec![0.0; self.num_time_points];
280 let mut updates = self.create_axis_array();
281 updates.extend(self.acquire_reset());
282 updates
283 }
284
285 fn compute_num_average(&mut self) -> Vec<ParamUpdate> {
289 if self.time_per_point == 0.0 {
290 self.num_average = 1;
291 self.averaging_time_actual = self.averaging_time_requested;
292 } else {
293 let n = (self.averaging_time_requested / self.time_per_point + 0.5) as i64;
295 self.num_average = if n < 1 { 1 } else { n as usize };
296 self.averaging_time_actual = self.time_per_point * self.num_average as f64;
297 }
298 self.num_averaged = 0;
299 let mut updates = vec![
300 ParamUpdate::float64(self.p.ts_averaging_time, self.averaging_time_actual),
301 ParamUpdate::int32(self.p.ts_num_average, self.num_average as i32),
302 ];
303 updates.extend(self.create_axis_array());
304 updates
305 }
306
307 fn do_time_series_callbacks(&self) -> Vec<ParamUpdate> {
312 let ntp = self.num_time_points;
313 let mut updates = Vec::with_capacity(self.num_signals);
314 match self.acquire_mode {
315 AcquireMode::Fixed => {
316 for signal in 0..self.num_signals {
317 let start = signal * ntp;
318 let series = self.circular[start..start + self.current_time_point].to_vec();
319 updates.push(ParamUpdate::float64_array_addr(
320 self.p.ts_time_series,
321 signal as i32,
322 series,
323 ));
324 }
325 }
326 AcquireMode::Circular => {
327 for signal in 0..self.num_signals {
328 let base = signal * ntp;
329 let mut series = Vec::with_capacity(ntp);
330 let mut time_in = self.current_time_point;
331 for _ in 0..ntp {
332 series.push(self.circular[base + time_in]);
333 time_in += 1;
334 if time_in >= ntp {
335 time_in = 0;
336 }
337 }
338 updates.push(ParamUpdate::float64_array_addr(
339 self.p.ts_time_series,
340 signal as i32,
341 series,
342 ));
343 }
344 }
345 }
346 updates
347 }
348
349 fn add_to_time_series(&mut self, array: &NDArray) -> Vec<ParamUpdate> {
355 let mut updates = Vec::new();
356 let num_signals_in = self.num_signals_in.max(0) as usize;
357 if num_signals_in == 0 {
358 return updates;
359 }
360 let data = &array.data;
361 let mut num_times = if array.dims.len() == 2 {
363 array.dims[1].size
364 } else {
365 1
366 };
367 let max_times = data.len() / num_signals_in;
370 if num_times > max_times {
371 num_times = max_times;
372 }
373
374 let ntp = self.num_time_points;
375 for i in 0..num_times {
376 let base = i * num_signals_in;
377 for s in 0..self.num_signals {
378 self.average_store[s] += sample_f64(data, base + s);
379 }
380 self.num_averaged += 1;
381 if self.num_averaged < self.num_average {
382 continue;
383 }
384 for s in 0..self.num_signals {
386 let avg = averaged_value(self.average_store[s], self.num_averaged, self.data_type);
387 self.circular[s * ntp + self.current_time_point] = avg;
388 self.average_store[s] = 0.0;
389 }
390 self.num_averaged = 0;
391 self.time_stamp[self.current_time_point] = array.time_stamp;
392 self.current_time_point += 1;
393 if self.current_time_point >= ntp {
394 match self.acquire_mode {
395 AcquireMode::Fixed => {
396 self.acquiring = false;
398 updates.push(ParamUpdate::int32(self.p.ts_acquire, 0));
399 updates.extend(self.do_time_series_callbacks());
400 break;
401 }
402 AcquireMode::Circular => {
403 self.current_time_point = 0;
404 }
405 }
406 }
407 }
408
409 updates.push(ParamUpdate::int32(
410 self.p.ts_current_point,
411 self.current_time_point as i32,
412 ));
413 let elapsed = self.start_time.elapsed().as_secs_f64();
414 updates.push(ParamUpdate::float64(self.p.ts_elapsed_time, elapsed));
415 updates
416 }
417}
418
419impl NDPluginProcess for TimeSeriesProcessor {
420 fn process_array(&mut self, array: &NDArray, _pool: &NDArrayPool) -> ProcessResult {
421 let ndims = array.dims.len();
423 if !(1..=2).contains(&ndims) {
424 return ProcessResult::empty();
425 }
426
427 let mut updates: Vec<ParamUpdate> = Vec::new();
428
429 let num_signals_in = array.dims[0].size;
431 let dtype = array.data.data_type();
432 if dtype != self.data_type || (num_signals_in as i64) != self.num_signals_in {
433 self.data_type = dtype;
434 self.num_signals_in = num_signals_in as i64;
435 self.num_signals = num_signals_in.min(self.max_signals);
436 updates.extend(self.allocate_arrays());
437 }
438
439 if self.acquiring {
441 updates.extend(self.add_to_time_series(array));
442 }
443
444 ProcessResult::sink(coalesce_updates(updates))
445 }
446
447 fn plugin_type(&self) -> &str {
448 "NDPluginTimeSeries"
449 }
450
451 fn register_params(&mut self, base: &mut PortDriverBase) -> asyn_rs::error::AsynResult<()> {
452 self.p.ts_acquire = base.create_param("TS_ACQUIRE", ParamType::Int32)?;
454 self.p.ts_read = base.create_param("TS_READ", ParamType::Int32)?;
455 self.p.ts_num_points = base.create_param("TS_NUM_POINTS", ParamType::Int32)?;
456 self.p.ts_current_point = base.create_param("TS_CURRENT_POINT", ParamType::Int32)?;
457 self.p.ts_time_per_point = base.create_param("TS_TIME_PER_POINT", ParamType::Float64)?;
458 self.p.ts_averaging_time = base.create_param("TS_AVERAGING_TIME", ParamType::Float64)?;
459 self.p.ts_num_average = base.create_param("TS_NUM_AVERAGE", ParamType::Int32)?;
460 self.p.ts_elapsed_time = base.create_param("TS_ELAPSED_TIME", ParamType::Float64)?;
461 self.p.ts_acquire_mode = base.create_param("TS_ACQUIRE_MODE", ParamType::Int32)?;
462 self.p.ts_time_axis = base.create_param("TS_TIME_AXIS", ParamType::Float64Array)?;
463 self.p.ts_timestamp = base.create_param("TS_TIMESTAMP", ParamType::Float64Array)?;
464 self.p.ts_time_series = base.create_param("TS_TIME_SERIES", ParamType::Float64Array)?;
466
467 base.set_int32_param(self.p.ts_num_points, 0, self.num_time_points as i32)?;
470 base.set_int32_param(self.p.ts_num_average, 0, self.num_average as i32)?;
471 base.set_int32_param(self.p.ts_acquire, 0, 0)?;
472 base.set_int32_param(self.p.ts_acquire_mode, 0, 0)?;
473 base.set_int32_param(self.p.ts_current_point, 0, 0)?;
474 base.set_float64_param(self.p.ts_averaging_time, 0, self.averaging_time_actual)?;
475 base.set_float64_param(self.p.ts_time_per_point, 0, self.time_per_point)?;
476
477 let axis: Vec<f64> = (0..self.num_time_points)
479 .map(|i| i as f64 * self.averaging_time_actual)
480 .collect();
481 base.params
482 .set_float64_array(self.p.ts_time_axis, 0, axis)?;
483 Ok(())
484 }
485
486 fn on_param_change(
487 &mut self,
488 reason: usize,
489 params: &PluginParamSnapshot,
490 ) -> ParamChangeResult {
491 let mut updates = Vec::new();
492 if reason == self.p.ts_num_points {
493 self.num_time_points = params.value.as_i32().max(1) as usize;
495 updates.extend(self.allocate_arrays());
496 } else if reason == self.p.ts_acquire_mode {
497 self.acquire_mode = if params.value.as_i32() == 0 {
499 AcquireMode::Fixed
500 } else {
501 AcquireMode::Circular
502 };
503 updates.extend(self.acquire_reset());
504 updates.extend(self.create_axis_array());
505 } else if reason == self.p.ts_acquire {
506 if params.value.as_i32() != 0 {
508 self.acquiring = true;
509 updates.extend(self.acquire_reset());
510 } else {
511 self.acquiring = false;
512 updates.extend(self.do_time_series_callbacks());
513 }
514 } else if reason == self.p.ts_read {
515 updates.extend(self.do_time_series_callbacks());
517 } else if reason == self.p.ts_time_per_point {
518 self.time_per_point = params.value.as_f64();
520 updates.extend(self.compute_num_average());
521 } else if reason == self.p.ts_averaging_time {
522 self.averaging_time_requested = params.value.as_f64();
524 updates.extend(self.compute_num_average());
525 }
526 ParamChangeResult::updates(updates)
527 }
528}
529
530#[cfg(test)]
531mod tests {
532 use super::*;
533 use ad_core_rs::ndarray::NDDimension;
534 use asyn_rs::port::{PortDriverBase, PortFlags};
535
536 #[test]
546 fn test_averaged_value_uint8_divides_before_narrowing() {
547 assert_eq!(averaged_value(600.0, 3, NDDataType::UInt8), 200.0); }
549
550 #[test]
553 fn test_averaged_value_int8_negative_divides_before_narrowing() {
554 assert_eq!(averaged_value(-600.0, 3, NDDataType::Int8), -200.0); }
556
557 #[test]
560 fn test_averaged_value_uint16_does_not_wrap() {
561 assert_eq!(averaged_value(70000.0, 2, NDDataType::UInt16), 35000.0); }
563
564 #[test]
568 fn test_averaged_value_narrows_the_mean_toward_zero() {
569 assert_eq!(averaged_value(7.0, 2, NDDataType::Int8), 3.0);
571 assert_eq!(averaged_value(-7.0, 2, NDDataType::Int8), -3.0);
572 assert_eq!(averaged_value(7.0, 2, NDDataType::UInt16), 3.0);
573 }
574
575 #[test]
576 fn test_averaged_value_int32_in_range_no_wrap() {
577 assert_eq!(averaged_value(600.0, 3, NDDataType::Int32), 200.0);
578 }
579
580 #[test]
582 fn test_averaged_value_float_types_exact() {
583 assert_eq!(averaged_value(600.0, 3, NDDataType::Float64), 200.0);
584 assert_eq!(averaged_value(600.0, 3, NDDataType::Float32), 200.0);
585 assert_eq!(averaged_value(7.0, 2, NDDataType::Float64), 3.5);
587 }
588
589 #[test]
590 fn test_averaged_value_numaverage_one_is_passthrough() {
591 assert_eq!(averaged_value(200.0, 1, NDDataType::UInt8), 200.0);
592 assert_eq!(averaged_value(-50.0, 1, NDDataType::Int8), -50.0);
593 }
594
595 fn make_proc(max_signals: usize, port: &str) -> TimeSeriesProcessor {
598 let mut proc = TimeSeriesProcessor::new(max_signals);
599 let mut base = PortDriverBase::new(port, max_signals + 1, PortFlags::default());
600 proc.register_params(&mut base).unwrap();
601 proc
602 }
603
604 fn find_array(res: &ProcessResult, reason: usize, addr: i32) -> Option<Vec<f64>> {
605 res.param_updates.iter().find_map(|u| match u {
606 ParamUpdate::Float64Array {
607 reason: r,
608 addr: a,
609 value,
610 } if *r == reason && *a == addr => Some(value.clone()),
611 _ => None,
612 })
613 }
614
615 fn find_int(res: &ProcessResult, reason: usize) -> Option<i32> {
616 res.param_updates.iter().find_map(|u| match u {
617 ParamUpdate::Int32 {
618 reason: r, value, ..
619 } if *r == reason => Some(*value),
620 _ => None,
621 })
622 }
623
624 #[test]
629 fn test_process_array_uint8_average_per_signal() {
630 let mut proc = make_proc(2, "TST_TS_U8");
631 proc.time_per_point = 1.0;
633 proc.averaging_time_requested = 3.0;
634 let _ = proc.compute_num_average();
635 assert_eq!(proc.num_average, 3);
636 proc.acquiring = true;
637
638 let pool = NDArrayPool::new(1_000_000);
639 let arr = NDArray::with_data(
641 vec![NDDimension::new(2), NDDimension::new(3)],
642 NDDataBuffer::U8(vec![200; 6]),
643 );
644 let res = proc.process_array(&arr, &pool);
645
646 assert_eq!(proc.current_time_point, 1);
648 assert_eq!(proc.circular[0 * proc.num_time_points], 200.0);
649 assert_eq!(proc.circular[proc.num_time_points], 200.0); assert_eq!(find_int(&res, proc.p.ts_current_point), Some(1));
652 assert!(find_array(&res, proc.p.ts_time_series, 0).is_none());
653 }
654
655 #[test]
656 fn test_fixed_mode_fills_stops_and_emits_waveforms() {
657 let mut proc = make_proc(1, "TST_TS_FIX");
658 proc.num_time_points = 2; proc.acquiring = true; let pool = NDArrayPool::new(1_000_000);
662 let arr = NDArray::with_data(
664 vec![NDDimension::new(1), NDDimension::new(3)],
665 NDDataBuffer::F64(vec![10.0, 20.0, 30.0]),
666 );
667 let res = proc.process_array(&arr, &pool);
668
669 assert!(!proc.acquiring);
671 assert_eq!(proc.current_time_point, 2);
672 assert_eq!(find_int(&res, proc.p.ts_acquire), Some(0));
673 let wf = find_array(&res, proc.p.ts_time_series, 0).expect("waveform emitted");
674 assert_eq!(wf, vec![10.0, 20.0]);
675 }
676
677 #[test]
678 fn test_circular_mode_wraps_and_rotates_oldest_first() {
679 let mut proc = make_proc(1, "TST_TS_CIRC");
680 proc.num_time_points = 3;
681 proc.acquire_mode = AcquireMode::Circular;
682 proc.acquiring = true;
683
684 let pool = NDArrayPool::new(1_000_000);
685 let arr = NDArray::with_data(
687 vec![NDDimension::new(1), NDDimension::new(5)],
688 NDDataBuffer::F64(vec![1.0, 2.0, 3.0, 4.0, 5.0]),
689 );
690 proc.process_array(&arr, &pool);
691
692 assert!(proc.acquiring);
694 assert_eq!(proc.current_time_point, 2);
695 let updates = proc.do_time_series_callbacks();
697 let wf = updates
698 .iter()
699 .find_map(|u| match u {
700 ParamUpdate::Float64Array {
701 reason,
702 addr,
703 value,
704 } if *reason == proc.p.ts_time_series && *addr == 0 => Some(value.clone()),
705 _ => None,
706 })
707 .unwrap();
708 assert_eq!(wf, vec![3.0, 4.0, 5.0]);
709 }
710
711 #[test]
712 fn test_one_d_array_is_single_time_point_across_signals() {
713 let mut proc = make_proc(3, "TST_TS_1D");
714 proc.acquiring = true; let pool = NDArrayPool::new(1_000_000);
717 let arr = NDArray::with_data(
719 vec![NDDimension::new(3)],
720 NDDataBuffer::F64(vec![11.0, 22.0, 33.0]),
721 );
722 proc.process_array(&arr, &pool);
723
724 assert_eq!(proc.num_signals, 3);
725 assert_eq!(proc.current_time_point, 1);
726 let ntp = proc.num_time_points;
727 assert_eq!(proc.circular[0], 11.0);
728 assert_eq!(proc.circular[ntp], 22.0);
729 assert_eq!(proc.circular[2 * ntp], 33.0);
730 }
731
732 #[test]
733 fn test_num_signals_capped_at_max_signals() {
734 let mut proc = make_proc(2, "TST_TS_CAP");
735 proc.acquiring = true;
736
737 let pool = NDArrayPool::new(1_000_000);
738 let arr = NDArray::with_data(
740 vec![NDDimension::new(4), NDDimension::new(1)],
741 NDDataBuffer::F64(vec![1.0, 2.0, 3.0, 4.0]),
742 );
743 proc.process_array(&arr, &pool);
744
745 assert_eq!(proc.num_signals, 2);
746 assert_eq!(proc.circular[0], 1.0);
747 assert_eq!(proc.circular[proc.num_time_points], 2.0);
748 }
749
750 #[test]
751 fn test_invalid_ndims_is_ignored() {
752 let mut proc = make_proc(1, "TST_TS_BAD");
753 proc.acquiring = true;
754 let pool = NDArrayPool::new(1_000_000);
755 let arr = NDArray::with_data(
757 vec![
758 NDDimension::new(2),
759 NDDimension::new(2),
760 NDDimension::new(2),
761 ],
762 NDDataBuffer::F64(vec![0.0; 8]),
763 );
764 let res = proc.process_array(&arr, &pool);
765 assert!(res.param_updates.is_empty());
766 assert_eq!(proc.current_time_point, 0);
767 }
768
769 #[test]
770 fn test_acquire_mode_flips_time_axis() {
771 let mut proc = make_proc(1, "TST_TS_AXIS");
772 proc.num_time_points = 4;
773 let _ = proc.allocate_arrays();
775
776 let fixed = proc.create_axis_array();
778 let fixed_axis = match &fixed[0] {
779 ParamUpdate::Float64Array { value, .. } => value.clone(),
780 _ => panic!("expected axis"),
781 };
782 assert_eq!(fixed_axis, vec![0.0, 1.0, 2.0, 3.0]);
783
784 proc.acquire_mode = AcquireMode::Circular;
786 let circ = proc.create_axis_array();
787 let circ_axis = match &circ[0] {
788 ParamUpdate::Float64Array { value, .. } => value.clone(),
789 _ => panic!("expected axis"),
790 };
791 assert_eq!(circ_axis, vec![-3.0, -2.0, -1.0, 0.0]);
792 }
793
794 #[test]
795 fn test_compute_num_average_from_averaging_time() {
796 let mut proc = make_proc(1, "TST_TS_NAVG");
797 proc.time_per_point = 0.5;
798 proc.averaging_time_requested = 2.0;
799 proc.compute_num_average();
800 assert_eq!(proc.num_average, 4);
802 assert_eq!(proc.averaging_time_actual, 2.0);
803
804 proc.time_per_point = 0.0;
806 proc.averaging_time_requested = 7.0;
807 proc.compute_num_average();
808 assert_eq!(proc.num_average, 1);
809 assert_eq!(proc.averaging_time_actual, 7.0);
810 }
811}