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 {
126 let n = num_averaged.max(1);
127 match dt {
128 NDDataType::Int8 => {
129 let c = sum.trunc() as i64 as i8;
130 ((c as i32) / (n as i32)) as i8 as f64
131 }
132 NDDataType::UInt8 => {
133 let c = sum.trunc() as i64 as u8;
134 ((c as i32) / (n as i32)) as u8 as f64
135 }
136 NDDataType::Int16 => {
137 let c = sum.trunc() as i64 as i16;
138 ((c as i32) / (n as i32)) as i16 as f64
139 }
140 NDDataType::UInt16 => {
141 let c = sum.trunc() as i64 as u16;
142 ((c as i32) / (n as i32)) as u16 as f64
143 }
144 NDDataType::Int32 => {
145 let c = sum.trunc() as i64 as i32;
146 (c / (n as i32)) as f64
147 }
148 NDDataType::UInt32 => {
149 let c = sum.trunc() as i64 as u32;
150 (c / (n as u32)) as f64
151 }
152 NDDataType::Int64 => {
153 let c = sum.trunc() as i64;
154 (c / (n as i64)) as f64
155 }
156 NDDataType::UInt64 => {
157 let c = sum.trunc() as u64;
158 (c / (n as u64)) as f64
159 }
160 NDDataType::Float32 => ((sum as f32) / (n as f32)) as f64,
161 NDDataType::Float64 => sum / (n as f64),
162 }
163}
164
165fn coalesce_updates(updates: Vec<ParamUpdate>) -> Vec<ParamUpdate> {
175 fn key(u: &ParamUpdate) -> (usize, i32) {
176 match u {
177 ParamUpdate::Int32 { reason, addr, .. }
178 | ParamUpdate::Float64 { reason, addr, .. }
179 | ParamUpdate::Octet { reason, addr, .. }
180 | ParamUpdate::Float64Array { reason, addr, .. } => (*reason, *addr),
181 }
182 }
183 let mut seen = std::collections::HashSet::new();
184 let mut out = Vec::with_capacity(updates.len());
185 for u in updates.into_iter().rev() {
187 if seen.insert(key(&u)) {
188 out.push(u);
189 }
190 }
191 out.reverse();
192 out
193}
194
195pub struct TimeSeriesProcessor {
197 max_signals: usize,
199 num_signals_in: i64,
202 num_signals: usize,
204 data_type: NDDataType,
206 num_time_points: usize,
208 current_time_point: usize,
210 num_average: usize,
212 num_averaged: usize,
214 average_store: Vec<f64>,
216 time_per_point: f64,
218 averaging_time_requested: f64,
220 averaging_time_actual: f64,
222 acquire_mode: AcquireMode,
224 acquiring: bool,
226 circular: Vec<f64>,
229 time_stamp: Vec<f64>,
231 start_time: Instant,
233 p: Params,
234}
235
236impl TimeSeriesProcessor {
237 pub fn new(max_signals: usize) -> Self {
240 let max_signals = max_signals.max(1);
241 let num_time_points = DEFAULT_NUM_TSPOINTS;
242 Self {
243 max_signals,
244 num_signals_in: -1,
245 num_signals: max_signals,
246 data_type: NDDataType::Float64,
247 num_time_points,
248 current_time_point: 0,
249 num_average: 1,
250 num_averaged: 0,
251 average_store: vec![0.0; max_signals],
252 time_per_point: 0.0,
253 averaging_time_requested: 1.0,
254 averaging_time_actual: 1.0,
257 acquire_mode: AcquireMode::Fixed,
258 acquiring: false,
259 circular: vec![0.0; max_signals * num_time_points],
260 time_stamp: vec![0.0; num_time_points],
261 start_time: Instant::now(),
262 p: Params::sentinel(),
263 }
264 }
265
266 fn create_axis_array(&self) -> Vec<ParamUpdate> {
271 let axis: Vec<f64> = (0..self.num_time_points)
272 .map(|i| match self.acquire_mode {
273 AcquireMode::Fixed => i as f64 * self.averaging_time_actual,
274 AcquireMode::Circular => {
275 -(((self.num_time_points - 1) - i) as f64) * self.averaging_time_actual
276 }
277 })
278 .collect();
279 vec![ParamUpdate::float64_array(self.p.ts_time_axis, axis)]
280 }
281
282 fn acquire_reset(&mut self) -> Vec<ParamUpdate> {
286 self.circular.iter_mut().for_each(|v| *v = 0.0);
287 self.time_stamp.iter_mut().for_each(|v| *v = 0.0);
288 self.current_time_point = 0;
289 self.start_time = Instant::now();
290 vec![ParamUpdate::int32(self.p.ts_current_point, 0)]
291 }
292
293 fn allocate_arrays(&mut self) -> Vec<ParamUpdate> {
297 self.circular = vec![0.0; self.num_signals * self.num_time_points];
298 self.time_stamp = vec![0.0; self.num_time_points];
299 let mut updates = self.create_axis_array();
300 updates.extend(self.acquire_reset());
301 updates
302 }
303
304 fn compute_num_average(&mut self) -> Vec<ParamUpdate> {
308 if self.time_per_point == 0.0 {
309 self.num_average = 1;
310 self.averaging_time_actual = self.averaging_time_requested;
311 } else {
312 let n = (self.averaging_time_requested / self.time_per_point + 0.5) as i64;
314 self.num_average = if n < 1 { 1 } else { n as usize };
315 self.averaging_time_actual = self.time_per_point * self.num_average as f64;
316 }
317 self.num_averaged = 0;
318 let mut updates = vec![
319 ParamUpdate::float64(self.p.ts_averaging_time, self.averaging_time_actual),
320 ParamUpdate::int32(self.p.ts_num_average, self.num_average as i32),
321 ];
322 updates.extend(self.create_axis_array());
323 updates
324 }
325
326 fn do_time_series_callbacks(&self) -> Vec<ParamUpdate> {
331 let ntp = self.num_time_points;
332 let mut updates = Vec::with_capacity(self.num_signals);
333 match self.acquire_mode {
334 AcquireMode::Fixed => {
335 for signal in 0..self.num_signals {
336 let start = signal * ntp;
337 let series = self.circular[start..start + self.current_time_point].to_vec();
338 updates.push(ParamUpdate::float64_array_addr(
339 self.p.ts_time_series,
340 signal as i32,
341 series,
342 ));
343 }
344 }
345 AcquireMode::Circular => {
346 for signal in 0..self.num_signals {
347 let base = signal * ntp;
348 let mut series = Vec::with_capacity(ntp);
349 let mut time_in = self.current_time_point;
350 for _ in 0..ntp {
351 series.push(self.circular[base + time_in]);
352 time_in += 1;
353 if time_in >= ntp {
354 time_in = 0;
355 }
356 }
357 updates.push(ParamUpdate::float64_array_addr(
358 self.p.ts_time_series,
359 signal as i32,
360 series,
361 ));
362 }
363 }
364 }
365 updates
366 }
367
368 fn add_to_time_series(&mut self, array: &NDArray) -> Vec<ParamUpdate> {
374 let mut updates = Vec::new();
375 let num_signals_in = self.num_signals_in.max(0) as usize;
376 if num_signals_in == 0 {
377 return updates;
378 }
379 let data = &array.data;
380 let mut num_times = if array.dims.len() == 2 {
382 array.dims[1].size
383 } else {
384 1
385 };
386 let max_times = data.len() / num_signals_in;
389 if num_times > max_times {
390 num_times = max_times;
391 }
392
393 let ntp = self.num_time_points;
394 for i in 0..num_times {
395 let base = i * num_signals_in;
396 for s in 0..self.num_signals {
397 self.average_store[s] += sample_f64(data, base + s);
398 }
399 self.num_averaged += 1;
400 if self.num_averaged < self.num_average {
401 continue;
402 }
403 for s in 0..self.num_signals {
405 let avg = averaged_value(self.average_store[s], self.num_averaged, self.data_type);
406 self.circular[s * ntp + self.current_time_point] = avg;
407 self.average_store[s] = 0.0;
408 }
409 self.num_averaged = 0;
410 self.time_stamp[self.current_time_point] = array.time_stamp;
411 self.current_time_point += 1;
412 if self.current_time_point >= ntp {
413 match self.acquire_mode {
414 AcquireMode::Fixed => {
415 self.acquiring = false;
417 updates.push(ParamUpdate::int32(self.p.ts_acquire, 0));
418 updates.extend(self.do_time_series_callbacks());
419 break;
420 }
421 AcquireMode::Circular => {
422 self.current_time_point = 0;
423 }
424 }
425 }
426 }
427
428 updates.push(ParamUpdate::int32(
429 self.p.ts_current_point,
430 self.current_time_point as i32,
431 ));
432 let elapsed = self.start_time.elapsed().as_secs_f64();
433 updates.push(ParamUpdate::float64(self.p.ts_elapsed_time, elapsed));
434 updates
435 }
436}
437
438impl NDPluginProcess for TimeSeriesProcessor {
439 fn process_array(&mut self, array: &NDArray, _pool: &NDArrayPool) -> ProcessResult {
440 let ndims = array.dims.len();
442 if !(1..=2).contains(&ndims) {
443 return ProcessResult::empty();
444 }
445
446 let mut updates: Vec<ParamUpdate> = Vec::new();
447
448 let num_signals_in = array.dims[0].size;
450 let dtype = array.data.data_type();
451 if dtype != self.data_type || (num_signals_in as i64) != self.num_signals_in {
452 self.data_type = dtype;
453 self.num_signals_in = num_signals_in as i64;
454 self.num_signals = num_signals_in.min(self.max_signals);
455 updates.extend(self.allocate_arrays());
456 }
457
458 if self.acquiring {
460 updates.extend(self.add_to_time_series(array));
461 }
462
463 ProcessResult::sink(coalesce_updates(updates))
464 }
465
466 fn plugin_type(&self) -> &str {
467 "NDPluginTimeSeries"
468 }
469
470 fn register_params(&mut self, base: &mut PortDriverBase) -> asyn_rs::error::AsynResult<()> {
471 self.p.ts_acquire = base.create_param("TS_ACQUIRE", ParamType::Int32)?;
473 self.p.ts_read = base.create_param("TS_READ", ParamType::Int32)?;
474 self.p.ts_num_points = base.create_param("TS_NUM_POINTS", ParamType::Int32)?;
475 self.p.ts_current_point = base.create_param("TS_CURRENT_POINT", ParamType::Int32)?;
476 self.p.ts_time_per_point = base.create_param("TS_TIME_PER_POINT", ParamType::Float64)?;
477 self.p.ts_averaging_time = base.create_param("TS_AVERAGING_TIME", ParamType::Float64)?;
478 self.p.ts_num_average = base.create_param("TS_NUM_AVERAGE", ParamType::Int32)?;
479 self.p.ts_elapsed_time = base.create_param("TS_ELAPSED_TIME", ParamType::Float64)?;
480 self.p.ts_acquire_mode = base.create_param("TS_ACQUIRE_MODE", ParamType::Int32)?;
481 self.p.ts_time_axis = base.create_param("TS_TIME_AXIS", ParamType::Float64Array)?;
482 self.p.ts_timestamp = base.create_param("TS_TIMESTAMP", ParamType::Float64Array)?;
483 self.p.ts_time_series = base.create_param("TS_TIME_SERIES", ParamType::Float64Array)?;
485
486 base.set_int32_param(self.p.ts_num_points, 0, self.num_time_points as i32)?;
489 base.set_int32_param(self.p.ts_num_average, 0, self.num_average as i32)?;
490 base.set_int32_param(self.p.ts_acquire, 0, 0)?;
491 base.set_int32_param(self.p.ts_acquire_mode, 0, 0)?;
492 base.set_int32_param(self.p.ts_current_point, 0, 0)?;
493 base.set_float64_param(self.p.ts_averaging_time, 0, self.averaging_time_actual)?;
494 base.set_float64_param(self.p.ts_time_per_point, 0, self.time_per_point)?;
495
496 let axis: Vec<f64> = (0..self.num_time_points)
498 .map(|i| i as f64 * self.averaging_time_actual)
499 .collect();
500 base.params
501 .set_float64_array(self.p.ts_time_axis, 0, axis)?;
502 Ok(())
503 }
504
505 fn on_param_change(
506 &mut self,
507 reason: usize,
508 params: &PluginParamSnapshot,
509 ) -> ParamChangeResult {
510 let mut updates = Vec::new();
511 if reason == self.p.ts_num_points {
512 self.num_time_points = params.value.as_i32().max(1) as usize;
514 updates.extend(self.allocate_arrays());
515 } else if reason == self.p.ts_acquire_mode {
516 self.acquire_mode = if params.value.as_i32() == 0 {
518 AcquireMode::Fixed
519 } else {
520 AcquireMode::Circular
521 };
522 updates.extend(self.acquire_reset());
523 updates.extend(self.create_axis_array());
524 } else if reason == self.p.ts_acquire {
525 if params.value.as_i32() != 0 {
527 self.acquiring = true;
528 updates.extend(self.acquire_reset());
529 } else {
530 self.acquiring = false;
531 updates.extend(self.do_time_series_callbacks());
532 }
533 } else if reason == self.p.ts_read {
534 updates.extend(self.do_time_series_callbacks());
536 } else if reason == self.p.ts_time_per_point {
537 self.time_per_point = params.value.as_f64();
539 updates.extend(self.compute_num_average());
540 } else if reason == self.p.ts_averaging_time {
541 self.averaging_time_requested = params.value.as_f64();
543 updates.extend(self.compute_num_average());
544 }
545 ParamChangeResult::updates(updates)
546 }
547}
548
549#[cfg(test)]
550mod tests {
551 use super::*;
552 use ad_core_rs::ndarray::NDDimension;
553 use asyn_rs::port::{PortDriverBase, PortFlags};
554
555 #[test]
558 fn test_averaged_value_uint8_truncates_not_f64_divide() {
559 assert_eq!(averaged_value(600.0, 3, NDDataType::UInt8), 29.0);
561 assert_ne!(averaged_value(600.0, 3, NDDataType::UInt8), 200.0);
563 }
564
565 #[test]
566 fn test_averaged_value_int8_negative_truncates_toward_zero() {
567 assert_eq!(averaged_value(-600.0, 3, NDDataType::Int8), -29.0);
569 }
570
571 #[test]
572 fn test_averaged_value_uint16_wraps() {
573 assert_eq!(averaged_value(70000.0, 2, NDDataType::UInt16), 2232.0);
575 }
576
577 #[test]
578 fn test_averaged_value_int32_in_range_no_wrap() {
579 assert_eq!(averaged_value(600.0, 3, NDDataType::Int32), 200.0);
580 }
581
582 #[test]
583 fn test_averaged_value_float_types_exact() {
584 assert_eq!(averaged_value(600.0, 3, NDDataType::Float64), 200.0);
585 assert_eq!(averaged_value(600.0, 3, NDDataType::Float32), 200.0);
586 }
587
588 #[test]
589 fn test_averaged_value_numaverage_one_is_passthrough() {
590 assert_eq!(averaged_value(200.0, 1, NDDataType::UInt8), 200.0);
591 assert_eq!(averaged_value(-50.0, 1, NDDataType::Int8), -50.0);
592 }
593
594 fn make_proc(max_signals: usize, port: &str) -> TimeSeriesProcessor {
597 let mut proc = TimeSeriesProcessor::new(max_signals);
598 let mut base = PortDriverBase::new(port, max_signals + 1, PortFlags::default());
599 proc.register_params(&mut base).unwrap();
600 proc
601 }
602
603 fn find_array(res: &ProcessResult, reason: usize, addr: i32) -> Option<Vec<f64>> {
604 res.param_updates.iter().find_map(|u| match u {
605 ParamUpdate::Float64Array {
606 reason: r,
607 addr: a,
608 value,
609 } if *r == reason && *a == addr => Some(value.clone()),
610 _ => None,
611 })
612 }
613
614 fn find_int(res: &ProcessResult, reason: usize) -> Option<i32> {
615 res.param_updates.iter().find_map(|u| match u {
616 ParamUpdate::Int32 {
617 reason: r, value, ..
618 } if *r == reason => Some(*value),
619 _ => None,
620 })
621 }
622
623 #[test]
624 fn test_process_array_uint8_truncating_average_per_signal() {
625 let mut proc = make_proc(2, "TST_TS_U8");
626 proc.time_per_point = 1.0;
628 proc.averaging_time_requested = 3.0;
629 let _ = proc.compute_num_average();
630 assert_eq!(proc.num_average, 3);
631 proc.acquiring = true;
632
633 let pool = NDArrayPool::new(1_000_000);
634 let arr = NDArray::with_data(
636 vec![NDDimension::new(2), NDDimension::new(3)],
637 NDDataBuffer::U8(vec![200; 6]),
638 );
639 let res = proc.process_array(&arr, &pool);
640
641 assert_eq!(proc.current_time_point, 1);
643 assert_eq!(proc.circular[0 * proc.num_time_points], 29.0);
644 assert_eq!(proc.circular[proc.num_time_points], 29.0); assert_eq!(find_int(&res, proc.p.ts_current_point), Some(1));
647 assert!(find_array(&res, proc.p.ts_time_series, 0).is_none());
648 }
649
650 #[test]
651 fn test_fixed_mode_fills_stops_and_emits_waveforms() {
652 let mut proc = make_proc(1, "TST_TS_FIX");
653 proc.num_time_points = 2; proc.acquiring = true; let pool = NDArrayPool::new(1_000_000);
657 let arr = NDArray::with_data(
659 vec![NDDimension::new(1), NDDimension::new(3)],
660 NDDataBuffer::F64(vec![10.0, 20.0, 30.0]),
661 );
662 let res = proc.process_array(&arr, &pool);
663
664 assert!(!proc.acquiring);
666 assert_eq!(proc.current_time_point, 2);
667 assert_eq!(find_int(&res, proc.p.ts_acquire), Some(0));
668 let wf = find_array(&res, proc.p.ts_time_series, 0).expect("waveform emitted");
669 assert_eq!(wf, vec![10.0, 20.0]);
670 }
671
672 #[test]
673 fn test_circular_mode_wraps_and_rotates_oldest_first() {
674 let mut proc = make_proc(1, "TST_TS_CIRC");
675 proc.num_time_points = 3;
676 proc.acquire_mode = AcquireMode::Circular;
677 proc.acquiring = true;
678
679 let pool = NDArrayPool::new(1_000_000);
680 let arr = NDArray::with_data(
682 vec![NDDimension::new(1), NDDimension::new(5)],
683 NDDataBuffer::F64(vec![1.0, 2.0, 3.0, 4.0, 5.0]),
684 );
685 proc.process_array(&arr, &pool);
686
687 assert!(proc.acquiring);
689 assert_eq!(proc.current_time_point, 2);
690 let updates = proc.do_time_series_callbacks();
692 let wf = updates
693 .iter()
694 .find_map(|u| match u {
695 ParamUpdate::Float64Array {
696 reason,
697 addr,
698 value,
699 } if *reason == proc.p.ts_time_series && *addr == 0 => Some(value.clone()),
700 _ => None,
701 })
702 .unwrap();
703 assert_eq!(wf, vec![3.0, 4.0, 5.0]);
704 }
705
706 #[test]
707 fn test_one_d_array_is_single_time_point_across_signals() {
708 let mut proc = make_proc(3, "TST_TS_1D");
709 proc.acquiring = true; let pool = NDArrayPool::new(1_000_000);
712 let arr = NDArray::with_data(
714 vec![NDDimension::new(3)],
715 NDDataBuffer::F64(vec![11.0, 22.0, 33.0]),
716 );
717 proc.process_array(&arr, &pool);
718
719 assert_eq!(proc.num_signals, 3);
720 assert_eq!(proc.current_time_point, 1);
721 let ntp = proc.num_time_points;
722 assert_eq!(proc.circular[0], 11.0);
723 assert_eq!(proc.circular[ntp], 22.0);
724 assert_eq!(proc.circular[2 * ntp], 33.0);
725 }
726
727 #[test]
728 fn test_num_signals_capped_at_max_signals() {
729 let mut proc = make_proc(2, "TST_TS_CAP");
730 proc.acquiring = true;
731
732 let pool = NDArrayPool::new(1_000_000);
733 let arr = NDArray::with_data(
735 vec![NDDimension::new(4), NDDimension::new(1)],
736 NDDataBuffer::F64(vec![1.0, 2.0, 3.0, 4.0]),
737 );
738 proc.process_array(&arr, &pool);
739
740 assert_eq!(proc.num_signals, 2);
741 assert_eq!(proc.circular[0], 1.0);
742 assert_eq!(proc.circular[proc.num_time_points], 2.0);
743 }
744
745 #[test]
746 fn test_invalid_ndims_is_ignored() {
747 let mut proc = make_proc(1, "TST_TS_BAD");
748 proc.acquiring = true;
749 let pool = NDArrayPool::new(1_000_000);
750 let arr = NDArray::with_data(
752 vec![
753 NDDimension::new(2),
754 NDDimension::new(2),
755 NDDimension::new(2),
756 ],
757 NDDataBuffer::F64(vec![0.0; 8]),
758 );
759 let res = proc.process_array(&arr, &pool);
760 assert!(res.param_updates.is_empty());
761 assert_eq!(proc.current_time_point, 0);
762 }
763
764 #[test]
765 fn test_acquire_mode_flips_time_axis() {
766 let mut proc = make_proc(1, "TST_TS_AXIS");
767 proc.num_time_points = 4;
768 let _ = proc.allocate_arrays();
770
771 let fixed = proc.create_axis_array();
773 let fixed_axis = match &fixed[0] {
774 ParamUpdate::Float64Array { value, .. } => value.clone(),
775 _ => panic!("expected axis"),
776 };
777 assert_eq!(fixed_axis, vec![0.0, 1.0, 2.0, 3.0]);
778
779 proc.acquire_mode = AcquireMode::Circular;
781 let circ = proc.create_axis_array();
782 let circ_axis = match &circ[0] {
783 ParamUpdate::Float64Array { value, .. } => value.clone(),
784 _ => panic!("expected axis"),
785 };
786 assert_eq!(circ_axis, vec![-3.0, -2.0, -1.0, 0.0]);
787 }
788
789 #[test]
790 fn test_compute_num_average_from_averaging_time() {
791 let mut proc = make_proc(1, "TST_TS_NAVG");
792 proc.time_per_point = 0.5;
793 proc.averaging_time_requested = 2.0;
794 proc.compute_num_average();
795 assert_eq!(proc.num_average, 4);
797 assert_eq!(proc.averaging_time_actual, 2.0);
798
799 proc.time_per_point = 0.0;
801 proc.averaging_time_requested = 7.0;
802 proc.compute_num_average();
803 assert_eq!(proc.num_average, 1);
804 assert_eq!(proc.averaging_time_actual, 7.0);
805 }
806}