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;
44use parking_lot::Mutex;
45
46const DEFAULT_NUM_TSPOINTS: usize = 2048;
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51enum AcquireMode {
52 Fixed,
54 Circular,
56}
57
58struct Params {
60 ts_acquire: usize,
61 ts_read: usize,
62 ts_num_points: usize,
63 ts_current_point: usize,
64 ts_time_per_point: usize,
65 ts_averaging_time: usize,
66 ts_num_average: usize,
67 ts_elapsed_time: usize,
68 ts_acquire_mode: usize,
69 ts_time_axis: usize,
70 ts_timestamp: usize,
71 ts_time_series: usize,
72}
73
74impl Params {
75 const fn sentinel() -> Self {
78 Self {
79 ts_acquire: usize::MAX,
80 ts_read: usize::MAX,
81 ts_num_points: usize::MAX,
82 ts_current_point: usize::MAX,
83 ts_time_per_point: usize::MAX,
84 ts_averaging_time: usize::MAX,
85 ts_num_average: usize::MAX,
86 ts_elapsed_time: usize::MAX,
87 ts_acquire_mode: usize::MAX,
88 ts_time_axis: usize::MAX,
89 ts_timestamp: usize::MAX,
90 ts_time_series: usize::MAX,
91 }
92 }
93}
94
95#[inline]
98fn sample_f64(data: &NDDataBuffer, idx: usize) -> f64 {
99 match data {
100 NDDataBuffer::I8(v) => v[idx] as f64,
101 NDDataBuffer::U8(v) => v[idx] as f64,
102 NDDataBuffer::I16(v) => v[idx] as f64,
103 NDDataBuffer::U16(v) => v[idx] as f64,
104 NDDataBuffer::I32(v) => v[idx] as f64,
105 NDDataBuffer::U32(v) => v[idx] as f64,
106 NDDataBuffer::I64(v) => v[idx] as f64,
107 NDDataBuffer::U64(v) => v[idx] as f64,
108 NDDataBuffer::F32(v) => v[idx] as f64,
109 NDDataBuffer::F64(v) => v[idx],
110 }
111}
112
113fn averaged_value(sum: f64, num_averaged: usize, dt: NDDataType) -> f64 {
142 let mean = sum / num_averaged.max(1) as f64;
143 match dt {
144 NDDataType::Float32 => mean as f32 as f64,
145 NDDataType::Float64 => mean,
146 _ => mean.trunc(),
148 }
149}
150
151fn coalesce_updates(updates: Vec<ParamUpdate>) -> Vec<ParamUpdate> {
161 fn key(u: &ParamUpdate) -> (usize, i32) {
162 match u {
163 ParamUpdate::Int32 { reason, addr, .. }
164 | ParamUpdate::Float64 { reason, addr, .. }
165 | ParamUpdate::Octet { reason, addr, .. }
166 | ParamUpdate::Float64Array { reason, addr, .. } => (*reason, *addr),
167 }
168 }
169 let mut seen = std::collections::HashSet::new();
170 let mut out = Vec::with_capacity(updates.len());
171 for u in updates.into_iter().rev() {
173 if seen.insert(key(&u)) {
174 out.push(u);
175 }
176 }
177 out.reverse();
178 out
179}
180
181pub struct TimeSeriesProcessor {
183 state: Mutex<TimeSeriesState>,
184}
185
186struct TimeSeriesState {
190 max_signals: usize,
192 num_signals_in: i64,
195 num_signals: usize,
197 data_type: NDDataType,
199 num_time_points: usize,
201 current_time_point: usize,
203 num_average: usize,
205 num_averaged: usize,
207 average_store: Vec<f64>,
209 time_per_point: f64,
211 averaging_time_requested: f64,
213 averaging_time_actual: f64,
215 acquire_mode: AcquireMode,
217 acquiring: bool,
219 circular: Vec<f64>,
222 time_stamp: Vec<f64>,
224 start_time: Instant,
226 p: Params,
227}
228
229impl TimeSeriesProcessor {
230 pub fn new(max_signals: usize) -> Self {
233 Self {
234 state: Mutex::new(TimeSeriesState::new(max_signals)),
235 }
236 }
237}
238
239impl TimeSeriesState {
240 fn new(max_signals: usize) -> Self {
241 let max_signals = max_signals.max(1);
242 let num_time_points = DEFAULT_NUM_TSPOINTS;
243 Self {
244 max_signals,
245 num_signals_in: -1,
246 num_signals: max_signals,
247 data_type: NDDataType::Float64,
248 num_time_points,
249 current_time_point: 0,
250 num_average: 1,
251 num_averaged: 0,
252 average_store: vec![0.0; max_signals],
253 time_per_point: 0.0,
254 averaging_time_requested: 1.0,
255 averaging_time_actual: 1.0,
258 acquire_mode: AcquireMode::Fixed,
259 acquiring: false,
260 circular: vec![0.0; max_signals * num_time_points],
261 time_stamp: vec![0.0; num_time_points],
262 start_time: Instant::now(),
263 p: Params::sentinel(),
264 }
265 }
266
267 fn create_axis_array(&self) -> Vec<ParamUpdate> {
272 let axis: Vec<f64> = (0..self.num_time_points)
273 .map(|i| match self.acquire_mode {
274 AcquireMode::Fixed => i as f64 * self.averaging_time_actual,
275 AcquireMode::Circular => {
276 -(((self.num_time_points - 1) - i) as f64) * self.averaging_time_actual
277 }
278 })
279 .collect();
280 vec![ParamUpdate::float64_array(self.p.ts_time_axis, axis)]
281 }
282
283 fn acquire_reset(&mut self) -> Vec<ParamUpdate> {
287 self.circular.iter_mut().for_each(|v| *v = 0.0);
288 self.time_stamp.iter_mut().for_each(|v| *v = 0.0);
289 self.current_time_point = 0;
290 self.start_time = Instant::now();
291 vec![ParamUpdate::int32(self.p.ts_current_point, 0)]
292 }
293
294 fn allocate_arrays(&mut self) -> Vec<ParamUpdate> {
298 self.circular = vec![0.0; self.num_signals * self.num_time_points];
299 self.time_stamp = vec![0.0; self.num_time_points];
300 let mut updates = self.create_axis_array();
301 updates.extend(self.acquire_reset());
302 updates
303 }
304
305 fn compute_num_average(&mut self) -> Vec<ParamUpdate> {
309 if self.time_per_point == 0.0 {
310 self.num_average = 1;
311 self.averaging_time_actual = self.averaging_time_requested;
312 } else {
313 let n = (self.averaging_time_requested / self.time_per_point + 0.5) as i64;
315 self.num_average = if n < 1 { 1 } else { n as usize };
316 self.averaging_time_actual = self.time_per_point * self.num_average as f64;
317 }
318 self.num_averaged = 0;
319 let mut updates = vec![
320 ParamUpdate::float64(self.p.ts_averaging_time, self.averaging_time_actual),
321 ParamUpdate::int32(self.p.ts_num_average, self.num_average as i32),
322 ];
323 updates.extend(self.create_axis_array());
324 updates
325 }
326
327 fn do_time_series_callbacks(&self) -> Vec<ParamUpdate> {
332 let ntp = self.num_time_points;
333 let mut updates = Vec::with_capacity(self.num_signals);
334 match self.acquire_mode {
335 AcquireMode::Fixed => {
336 for signal in 0..self.num_signals {
337 let start = signal * ntp;
338 let series = self.circular[start..start + self.current_time_point].to_vec();
339 updates.push(ParamUpdate::float64_array_addr(
340 self.p.ts_time_series,
341 signal as i32,
342 series,
343 ));
344 }
345 }
346 AcquireMode::Circular => {
347 for signal in 0..self.num_signals {
348 let base = signal * ntp;
349 let mut series = Vec::with_capacity(ntp);
350 let mut time_in = self.current_time_point;
351 for _ in 0..ntp {
352 series.push(self.circular[base + time_in]);
353 time_in += 1;
354 if time_in >= ntp {
355 time_in = 0;
356 }
357 }
358 updates.push(ParamUpdate::float64_array_addr(
359 self.p.ts_time_series,
360 signal as i32,
361 series,
362 ));
363 }
364 }
365 }
366 updates
367 }
368
369 fn add_to_time_series(&mut self, array: &NDArray) -> Vec<ParamUpdate> {
375 let mut updates = Vec::new();
376 let num_signals_in = self.num_signals_in.max(0) as usize;
377 if num_signals_in == 0 {
378 return updates;
379 }
380 let data = &array.data;
381 let mut num_times = if array.dims.len() == 2 {
383 array.dims[1].size
384 } else {
385 1
386 };
387 let max_times = data.len() / num_signals_in;
390 if num_times > max_times {
391 num_times = max_times;
392 }
393
394 let ntp = self.num_time_points;
395 for i in 0..num_times {
396 let base = i * num_signals_in;
397 for s in 0..self.num_signals {
398 self.average_store[s] += sample_f64(data, base + s);
399 }
400 self.num_averaged += 1;
401 if self.num_averaged < self.num_average {
402 continue;
403 }
404 for s in 0..self.num_signals {
406 let avg = averaged_value(self.average_store[s], self.num_averaged, self.data_type);
407 self.circular[s * ntp + self.current_time_point] = avg;
408 self.average_store[s] = 0.0;
409 }
410 self.num_averaged = 0;
411 self.time_stamp[self.current_time_point] = array.time_stamp;
412 self.current_time_point += 1;
413 if self.current_time_point >= ntp {
414 match self.acquire_mode {
415 AcquireMode::Fixed => {
416 self.acquiring = false;
418 updates.push(ParamUpdate::int32(self.p.ts_acquire, 0));
419 updates.extend(self.do_time_series_callbacks());
420 break;
421 }
422 AcquireMode::Circular => {
423 self.current_time_point = 0;
424 }
425 }
426 }
427 }
428
429 updates.push(ParamUpdate::int32(
430 self.p.ts_current_point,
431 self.current_time_point as i32,
432 ));
433 let elapsed = self.start_time.elapsed().as_secs_f64();
434 updates.push(ParamUpdate::float64(self.p.ts_elapsed_time, elapsed));
435 updates
436 }
437}
438
439impl NDPluginProcess for TimeSeriesProcessor {
440 fn process_array(&self, array: &NDArray, _pool: &NDArrayPool) -> ProcessResult {
441 let ndims = array.dims.len();
443 if !(1..=2).contains(&ndims) {
444 return ProcessResult::empty();
445 }
446
447 let mut state = self.state.lock();
448
449 let mut updates: Vec<ParamUpdate> = Vec::new();
450
451 let num_signals_in = array.dims[0].size;
453 let dtype = array.data.data_type();
454 if dtype != state.data_type || (num_signals_in as i64) != state.num_signals_in {
455 state.data_type = dtype;
456 state.num_signals_in = num_signals_in as i64;
457 state.num_signals = num_signals_in.min(state.max_signals);
458 updates.extend(state.allocate_arrays());
459 }
460
461 if state.acquiring {
463 updates.extend(state.add_to_time_series(array));
464 }
465
466 ProcessResult::sink(coalesce_updates(updates))
467 }
468
469 fn plugin_type(&self) -> &str {
470 "NDPluginTimeSeries"
471 }
472
473 fn register_params(&mut self, base: &mut PortDriverBase) -> asyn_rs::error::AsynResult<()> {
474 let state = self.state.get_mut();
475 state.p.ts_acquire = base.create_param("TS_ACQUIRE", ParamType::Int32)?;
477 state.p.ts_read = base.create_param("TS_READ", ParamType::Int32)?;
478 state.p.ts_num_points = base.create_param("TS_NUM_POINTS", ParamType::Int32)?;
479 state.p.ts_current_point = base.create_param("TS_CURRENT_POINT", ParamType::Int32)?;
480 state.p.ts_time_per_point = base.create_param("TS_TIME_PER_POINT", ParamType::Float64)?;
481 state.p.ts_averaging_time = base.create_param("TS_AVERAGING_TIME", ParamType::Float64)?;
482 state.p.ts_num_average = base.create_param("TS_NUM_AVERAGE", ParamType::Int32)?;
483 state.p.ts_elapsed_time = base.create_param("TS_ELAPSED_TIME", ParamType::Float64)?;
484 state.p.ts_acquire_mode = base.create_param("TS_ACQUIRE_MODE", ParamType::Int32)?;
485 state.p.ts_time_axis = base.create_param("TS_TIME_AXIS", ParamType::Float64Array)?;
486 state.p.ts_timestamp = base.create_param("TS_TIMESTAMP", ParamType::Float64Array)?;
487 state.p.ts_time_series = base.create_param("TS_TIME_SERIES", ParamType::Float64Array)?;
489
490 base.set_int32_param(state.p.ts_num_points, 0, state.num_time_points as i32)?;
493 base.set_int32_param(state.p.ts_num_average, 0, state.num_average as i32)?;
494 base.set_int32_param(state.p.ts_acquire, 0, 0)?;
495 base.set_int32_param(state.p.ts_acquire_mode, 0, 0)?;
496 base.set_int32_param(state.p.ts_current_point, 0, 0)?;
497 base.set_float64_param(state.p.ts_averaging_time, 0, state.averaging_time_actual)?;
498 base.set_float64_param(state.p.ts_time_per_point, 0, state.time_per_point)?;
499
500 let axis: Vec<f64> = (0..state.num_time_points)
502 .map(|i| i as f64 * state.averaging_time_actual)
503 .collect();
504 base.params
505 .set_float64_array(state.p.ts_time_axis, 0, axis)?;
506 Ok(())
507 }
508
509 fn on_param_change(&self, reason: usize, params: &PluginParamSnapshot) -> ParamChangeResult {
510 let mut state = self.state.lock();
511 let mut updates = Vec::new();
512 if reason == state.p.ts_num_points {
513 state.num_time_points = params.value.as_i32().max(1) as usize;
515 updates.extend(state.allocate_arrays());
516 } else if reason == state.p.ts_acquire_mode {
517 state.acquire_mode = if params.value.as_i32() == 0 {
519 AcquireMode::Fixed
520 } else {
521 AcquireMode::Circular
522 };
523 updates.extend(state.acquire_reset());
524 updates.extend(state.create_axis_array());
525 } else if reason == state.p.ts_acquire {
526 if params.value.as_i32() != 0 {
528 state.acquiring = true;
529 updates.extend(state.acquire_reset());
530 } else {
531 state.acquiring = false;
532 updates.extend(state.do_time_series_callbacks());
533 }
534 } else if reason == state.p.ts_read {
535 updates.extend(state.do_time_series_callbacks());
537 } else if reason == state.p.ts_time_per_point {
538 state.time_per_point = params.value.as_f64();
540 updates.extend(state.compute_num_average());
541 } else if reason == state.p.ts_averaging_time {
542 state.averaging_time_requested = params.value.as_f64();
544 updates.extend(state.compute_num_average());
545 }
546 ParamChangeResult::updates(updates)
547 }
548}
549
550#[cfg(test)]
551mod tests {
552 use super::*;
553 use ad_core_rs::ndarray::NDDimension;
554 use asyn_rs::port::{PortDriverBase, PortFlags};
555
556 #[test]
566 fn test_averaged_value_uint8_divides_before_narrowing() {
567 assert_eq!(averaged_value(600.0, 3, NDDataType::UInt8), 200.0); }
569
570 #[test]
573 fn test_averaged_value_int8_negative_divides_before_narrowing() {
574 assert_eq!(averaged_value(-600.0, 3, NDDataType::Int8), -200.0); }
576
577 #[test]
580 fn test_averaged_value_uint16_does_not_wrap() {
581 assert_eq!(averaged_value(70000.0, 2, NDDataType::UInt16), 35000.0); }
583
584 #[test]
588 fn test_averaged_value_narrows_the_mean_toward_zero() {
589 assert_eq!(averaged_value(7.0, 2, NDDataType::Int8), 3.0);
591 assert_eq!(averaged_value(-7.0, 2, NDDataType::Int8), -3.0);
592 assert_eq!(averaged_value(7.0, 2, NDDataType::UInt16), 3.0);
593 }
594
595 #[test]
596 fn test_averaged_value_int32_in_range_no_wrap() {
597 assert_eq!(averaged_value(600.0, 3, NDDataType::Int32), 200.0);
598 }
599
600 #[test]
602 fn test_averaged_value_float_types_exact() {
603 assert_eq!(averaged_value(600.0, 3, NDDataType::Float64), 200.0);
604 assert_eq!(averaged_value(600.0, 3, NDDataType::Float32), 200.0);
605 assert_eq!(averaged_value(7.0, 2, NDDataType::Float64), 3.5);
607 }
608
609 #[test]
610 fn test_averaged_value_numaverage_one_is_passthrough() {
611 assert_eq!(averaged_value(200.0, 1, NDDataType::UInt8), 200.0);
612 assert_eq!(averaged_value(-50.0, 1, NDDataType::Int8), -50.0);
613 }
614
615 fn make_proc(max_signals: usize, port: &str) -> TimeSeriesProcessor {
618 let mut proc = TimeSeriesProcessor::new(max_signals);
619 let mut base = PortDriverBase::new(port, max_signals + 1, PortFlags::default());
620 proc.register_params(&mut base).unwrap();
621 proc
622 }
623
624 fn find_array(res: &ProcessResult, reason: usize, addr: i32) -> Option<Vec<f64>> {
625 res.param_updates.iter().find_map(|u| match u {
626 ParamUpdate::Float64Array {
627 reason: r,
628 addr: a,
629 value,
630 } if *r == reason && *a == addr => Some(value.clone()),
631 _ => None,
632 })
633 }
634
635 fn find_int(res: &ProcessResult, reason: usize) -> Option<i32> {
636 res.param_updates.iter().find_map(|u| match u {
637 ParamUpdate::Int32 {
638 reason: r, value, ..
639 } if *r == reason => Some(*value),
640 _ => None,
641 })
642 }
643
644 #[test]
649 fn test_process_array_uint8_average_per_signal() {
650 let proc = make_proc(2, "TST_TS_U8");
651 proc.state.lock().time_per_point = 1.0;
653 proc.state.lock().averaging_time_requested = 3.0;
654 let _ = proc.state.lock().compute_num_average();
655 assert_eq!(proc.state.lock().num_average, 3);
656 proc.state.lock().acquiring = true;
657
658 let pool = NDArrayPool::new(1_000_000);
659 let arr = NDArray::with_data(
661 vec![NDDimension::new(2), NDDimension::new(3)],
662 NDDataBuffer::U8(vec![200; 6]),
663 );
664 let res = proc.process_array(&arr, &pool);
665
666 assert_eq!(proc.state.lock().current_time_point, 1);
668 let ntp = proc.state.lock().num_time_points;
669 assert_eq!(proc.state.lock().circular[0], 200.0);
670 assert_eq!(proc.state.lock().circular[ntp], 200.0); assert_eq!(
673 find_int(&res, proc.state.lock().p.ts_current_point),
674 Some(1)
675 );
676 assert!(find_array(&res, proc.state.lock().p.ts_time_series, 0).is_none());
677 }
678
679 #[test]
680 fn test_fixed_mode_fills_stops_and_emits_waveforms() {
681 let proc = make_proc(1, "TST_TS_FIX");
682 proc.state.lock().num_time_points = 2; proc.state.lock().acquiring = true; let pool = NDArrayPool::new(1_000_000);
686 let arr = NDArray::with_data(
688 vec![NDDimension::new(1), NDDimension::new(3)],
689 NDDataBuffer::F64(vec![10.0, 20.0, 30.0]),
690 );
691 let res = proc.process_array(&arr, &pool);
692
693 assert!(!proc.state.lock().acquiring);
695 assert_eq!(proc.state.lock().current_time_point, 2);
696 assert_eq!(find_int(&res, proc.state.lock().p.ts_acquire), Some(0));
697 let wf = find_array(&res, proc.state.lock().p.ts_time_series, 0).expect("waveform emitted");
698 assert_eq!(wf, vec![10.0, 20.0]);
699 }
700
701 #[test]
702 fn test_circular_mode_wraps_and_rotates_oldest_first() {
703 let proc = make_proc(1, "TST_TS_CIRC");
704 proc.state.lock().num_time_points = 3;
705 proc.state.lock().acquire_mode = AcquireMode::Circular;
706 proc.state.lock().acquiring = true;
707
708 let pool = NDArrayPool::new(1_000_000);
709 let arr = NDArray::with_data(
711 vec![NDDimension::new(1), NDDimension::new(5)],
712 NDDataBuffer::F64(vec![1.0, 2.0, 3.0, 4.0, 5.0]),
713 );
714 proc.process_array(&arr, &pool);
715
716 assert!(proc.state.lock().acquiring);
718 assert_eq!(proc.state.lock().current_time_point, 2);
719 let updates = proc.state.lock().do_time_series_callbacks();
721 let wf = updates
722 .iter()
723 .find_map(|u| match u {
724 ParamUpdate::Float64Array {
725 reason,
726 addr,
727 value,
728 } if *reason == proc.state.lock().p.ts_time_series && *addr == 0 => {
729 Some(value.clone())
730 }
731 _ => None,
732 })
733 .unwrap();
734 assert_eq!(wf, vec![3.0, 4.0, 5.0]);
735 }
736
737 #[test]
738 fn test_one_d_array_is_single_time_point_across_signals() {
739 let proc = make_proc(3, "TST_TS_1D");
740 proc.state.lock().acquiring = true; let pool = NDArrayPool::new(1_000_000);
743 let arr = NDArray::with_data(
745 vec![NDDimension::new(3)],
746 NDDataBuffer::F64(vec![11.0, 22.0, 33.0]),
747 );
748 proc.process_array(&arr, &pool);
749
750 assert_eq!(proc.state.lock().num_signals, 3);
751 assert_eq!(proc.state.lock().current_time_point, 1);
752 let ntp = proc.state.lock().num_time_points;
753 assert_eq!(proc.state.lock().circular[0], 11.0);
754 assert_eq!(proc.state.lock().circular[ntp], 22.0);
755 assert_eq!(proc.state.lock().circular[2 * ntp], 33.0);
756 }
757
758 #[test]
759 fn test_num_signals_capped_at_max_signals() {
760 let proc = make_proc(2, "TST_TS_CAP");
761 proc.state.lock().acquiring = true;
762
763 let pool = NDArrayPool::new(1_000_000);
764 let arr = NDArray::with_data(
766 vec![NDDimension::new(4), NDDimension::new(1)],
767 NDDataBuffer::F64(vec![1.0, 2.0, 3.0, 4.0]),
768 );
769 proc.process_array(&arr, &pool);
770
771 assert_eq!(proc.state.lock().num_signals, 2);
772 assert_eq!(proc.state.lock().circular[0], 1.0);
773 let ntp = proc.state.lock().num_time_points;
774 assert_eq!(proc.state.lock().circular[ntp], 2.0);
775 }
776
777 #[test]
778 fn test_invalid_ndims_is_ignored() {
779 let proc = make_proc(1, "TST_TS_BAD");
780 proc.state.lock().acquiring = true;
781 let pool = NDArrayPool::new(1_000_000);
782 let arr = NDArray::with_data(
784 vec![
785 NDDimension::new(2),
786 NDDimension::new(2),
787 NDDimension::new(2),
788 ],
789 NDDataBuffer::F64(vec![0.0; 8]),
790 );
791 let res = proc.process_array(&arr, &pool);
792 assert!(res.param_updates.is_empty());
793 assert_eq!(proc.state.lock().current_time_point, 0);
794 }
795
796 #[test]
797 fn test_acquire_mode_flips_time_axis() {
798 let proc = make_proc(1, "TST_TS_AXIS");
799 proc.state.lock().num_time_points = 4;
800 let _ = proc.state.lock().allocate_arrays();
802
803 let fixed = proc.state.lock().create_axis_array();
805 let fixed_axis = match &fixed[0] {
806 ParamUpdate::Float64Array { value, .. } => value.clone(),
807 _ => panic!("expected axis"),
808 };
809 assert_eq!(fixed_axis, vec![0.0, 1.0, 2.0, 3.0]);
810
811 proc.state.lock().acquire_mode = AcquireMode::Circular;
813 let circ = proc.state.lock().create_axis_array();
814 let circ_axis = match &circ[0] {
815 ParamUpdate::Float64Array { value, .. } => value.clone(),
816 _ => panic!("expected axis"),
817 };
818 assert_eq!(circ_axis, vec![-3.0, -2.0, -1.0, 0.0]);
819 }
820
821 #[test]
822 fn test_compute_num_average_from_averaging_time() {
823 let proc = make_proc(1, "TST_TS_NAVG");
824 proc.state.lock().time_per_point = 0.5;
825 proc.state.lock().averaging_time_requested = 2.0;
826 proc.state.lock().compute_num_average();
827 assert_eq!(proc.state.lock().num_average, 4);
829 assert_eq!(proc.state.lock().averaging_time_actual, 2.0);
830
831 proc.state.lock().time_per_point = 0.0;
833 proc.state.lock().averaging_time_requested = 7.0;
834 proc.state.lock().compute_num_average();
835 assert_eq!(proc.state.lock().num_average, 1);
836 assert_eq!(proc.state.lock().averaging_time_actual, 7.0);
837 }
838}