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