1use std::sync::Arc;
8
9use ad_core_rs::ndarray::{NDArray, NDDataBuffer};
10use ad_core_rs::ndarray_pool::NDArrayPool;
11use ad_core_rs::plugin::runtime::{
12 NDPluginProcess, ParamUpdate, PluginParamSnapshot, PluginRuntimeHandle, ProcessResult,
13};
14use ad_core_rs::plugin::wiring::WiringRegistry;
15use asyn_rs::param::ParamType;
16use asyn_rs::port::PortDriverBase;
17use parking_lot::{MappedMutexGuard, Mutex, MutexGuard};
18
19#[cfg(feature = "parallel")]
20use crate::par_util;
21use crate::time_series::{TimeSeriesData, TimeSeriesSender};
22#[cfg(feature = "parallel")]
23use rayon::prelude::*;
24
25#[derive(Debug, Clone)]
27pub struct ROIStatROI {
28 pub enabled: bool,
29 pub offset: [usize; 2],
31 pub size: [usize; 2],
33 pub bgd_width: usize,
35}
36
37impl Default for ROIStatROI {
38 fn default() -> Self {
39 Self {
40 enabled: true,
41 offset: [0, 0],
42 size: [0, 0],
43 bgd_width: 0,
44 }
45 }
46}
47
48#[derive(Debug, Clone, Default)]
50pub struct ROIStatResult {
51 pub min: f64,
52 pub max: f64,
53 pub mean: f64,
54 pub total: f64,
55 pub net: f64,
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub enum TSMode {
62 Idle,
63 Acquiring,
64}
65
66const NUM_STATS: usize = 5;
68
69const ROI_STAT_NAMES: [&str; NUM_STATS] = ["MinValue", "MaxValue", "MeanValue", "Total", "Net"];
71
72pub fn roi_stat_ts_channel_names(num_rois: usize) -> Vec<String> {
75 let mut names = Vec::with_capacity(num_rois * NUM_STATS);
76 for roi_idx in 0..num_rois {
77 for stat_name in &ROI_STAT_NAMES {
78 names.push(format!("TS{}:{}", roi_idx + 1, stat_name));
79 }
80 }
81 names
82}
83
84#[derive(Clone, Copy, Default)]
88pub struct ROIStatParams {
89 pub reset_all: usize,
91 pub ts_control: usize,
92 pub ts_num_points: usize,
93 pub ts_current_point: usize,
94 pub ts_acquiring: usize,
95 pub use_: usize,
97 pub name: usize,
98 pub reset: usize,
99 pub bgd_width: usize,
100 pub dim0_min: usize,
101 pub dim1_min: usize,
102 pub dim0_size: usize,
103 pub dim1_size: usize,
104 pub dim0_max_size: usize,
105 pub dim1_max_size: usize,
106 pub min_value: usize,
107 pub max_value: usize,
108 pub mean_value: usize,
109 pub total: usize,
110 pub net: usize,
111 pub ts_total: usize,
113 pub ts_net: usize,
114 pub ts_mean_value: usize,
115 pub ts_min_value: usize,
116 pub ts_max_value: usize,
117 pub ts_timestamp: usize,
118}
119
120struct ROIStatState {
125 rois: Vec<ROIStatROI>,
126 results: Vec<ROIStatResult>,
127 ts_mode: TSMode,
129 ts_buffers: Vec<Vec<Vec<f64>>>,
130 ts_num_points: usize,
131 ts_current: usize,
132}
133
134pub struct ROIStatProcessor {
135 state: Mutex<ROIStatState>,
136 ts_sender: Option<TimeSeriesSender>,
138 params: ROIStatParams,
140 params_out: Arc<Mutex<ROIStatParams>>,
142}
143
144impl ROIStatProcessor {
145 pub fn new(rois: Vec<ROIStatROI>, ts_num_points: usize) -> Self {
147 let n = rois.len();
148 let results = vec![ROIStatResult::default(); n];
149 let ts_buffers = vec![vec![Vec::new(); NUM_STATS]; n];
150 Self {
151 state: Mutex::new(ROIStatState {
152 rois,
153 results,
154 ts_mode: TSMode::Idle,
155 ts_buffers,
156 ts_num_points,
157 ts_current: 0,
158 }),
159 ts_sender: None,
160 params: ROIStatParams::default(),
161 params_out: Arc::new(Mutex::new(ROIStatParams::default())),
162 }
163 }
164
165 pub fn params_handle(&self) -> Arc<Mutex<ROIStatParams>> {
167 self.params_out.clone()
168 }
169
170 pub fn results(&self) -> Vec<ROIStatResult> {
172 self.state.lock().results.clone()
173 }
174
175 pub fn rois(&self) -> Vec<ROIStatROI> {
177 self.state.lock().rois.clone()
178 }
179
180 pub fn rois_mut(&self) -> MappedMutexGuard<'_, Vec<ROIStatROI>> {
182 MutexGuard::map(self.state.lock(), |s| &mut s.rois)
183 }
184
185 pub fn set_ts_mode(&self, mode: TSMode) {
187 let mut state = self.state.lock();
188 if mode == TSMode::Acquiring && state.ts_mode != TSMode::Acquiring {
189 for roi_bufs in &mut state.ts_buffers {
191 for stat_buf in roi_bufs.iter_mut() {
192 stat_buf.clear();
193 }
194 }
195 state.ts_current = 0;
196 }
197 state.ts_mode = mode;
198 }
199
200 pub fn ts_buffer(&self, roi_index: usize, stat_index: usize) -> Vec<f64> {
203 let state = self.state.lock();
204 if roi_index < state.ts_buffers.len() && stat_index < NUM_STATS {
205 state.ts_buffers[roi_index][stat_index].clone()
206 } else {
207 Vec::new()
208 }
209 }
210
211 pub fn set_ts_sender(&mut self, sender: TimeSeriesSender) {
213 self.ts_sender = Some(sender);
214 }
215
216 fn clamp_roi_geometry(
227 roi: &ROIStatROI,
228 ndims: usize,
229 dims: [usize; 2],
230 ) -> Option<([usize; 2], [usize; 2])> {
231 let mut offset = roi.offset;
232 let mut size = roi.size;
233 for d in 0..ndims.min(2) {
234 let dim = dims[d];
235 if dim == 0 {
236 return None;
237 }
238 offset[d] = offset[d].min(dim - 1);
239 size[d] = size[d].max(1).min(dim - offset[d]);
240 }
241 Some((offset, size))
242 }
243
244 pub fn compute_roi_stats(
252 data: &NDDataBuffer,
253 ndims: usize,
254 array_size_x: usize,
255 offset: [usize; 2],
256 size: [usize; 2],
257 bgd_width: usize,
258 ) -> ROIStatResult {
259 let offset_x = offset[0];
260 let size_x = size[0];
261
262 let mut min = f64::MAX;
263 let mut max = f64::MIN;
264 let mut total = 0.0f64;
265 let mut bgd = 0.0f64;
266 let mut n_bgd = 0usize;
267 let n_elements;
268
269 if ndims == 1 {
270 if size_x == 0 {
271 return ROIStatResult::default();
272 }
273 n_elements = size_x;
274 for x in offset_x..offset_x + size_x {
275 let v = data.get_as_f64(x).unwrap_or(0.0);
276 min = min.min(v);
277 max = max.max(v);
278 total += v;
279 }
280 if bgd_width > 0 {
281 let bw_x = bgd_width.min(size_x);
282 for x in offset_x..offset_x + bw_x {
283 n_bgd += 1;
284 bgd += data.get_as_f64(x).unwrap_or(0.0);
285 }
286 for x in (offset_x + size_x - bw_x)..(offset_x + size_x) {
287 n_bgd += 1;
288 bgd += data.get_as_f64(x).unwrap_or(0.0);
289 }
290 }
291 } else if ndims == 2 {
292 let offset_y = offset[1];
293 let size_y = size[1];
294 if size_x == 0 || size_y == 0 {
295 return ROIStatResult::default();
296 }
297 n_elements = size_x * size_y;
298 for y in offset_y..offset_y + size_y {
299 let row = y * array_size_x;
300 for x in offset_x..offset_x + size_x {
301 let v = data.get_as_f64(row + x).unwrap_or(0.0);
302 min = min.min(v);
303 max = max.max(v);
304 total += v;
305 }
306 }
307 if bgd_width > 0 {
308 let bw_x = bgd_width.min(size_x);
309 let bw_y = bgd_width.min(size_y);
310 for y in offset_y..offset_y + bw_y {
312 let row = y * array_size_x;
313 for x in offset_x..offset_x + size_x {
314 n_bgd += 1;
315 bgd += data.get_as_f64(row + x).unwrap_or(0.0);
316 }
317 }
318 for y in (offset_y + size_y - bw_y)..(offset_y + size_y) {
319 let row = y * array_size_x;
320 for x in offset_x..offset_x + size_x {
321 n_bgd += 1;
322 bgd += data.get_as_f64(row + x).unwrap_or(0.0);
323 }
324 }
325 for y in (offset_y + bw_y)..(offset_y + size_y - bw_y) {
327 let row = y * array_size_x;
328 for x in offset_x..offset_x + bw_x {
329 n_bgd += 1;
330 bgd += data.get_as_f64(row + x).unwrap_or(0.0);
331 }
332 for x in (offset_x + size_x - bw_x)..(offset_x + size_x) {
333 n_bgd += 1;
334 bgd += data.get_as_f64(row + x).unwrap_or(0.0);
335 }
336 }
337 }
338 } else {
339 return ROIStatResult::default();
340 }
341
342 if n_elements == 0 {
343 return ROIStatResult::default();
344 }
345
346 let bgd_scaled = if n_bgd > 0 {
351 bgd / n_bgd as f64 * n_elements as f64
352 } else {
353 0.0
354 };
355 ROIStatResult {
356 min,
357 max,
358 mean: total / n_elements as f64,
359 total,
360 net: total - bgd_scaled,
361 }
362 }
363}
364
365impl NDPluginProcess for ROIStatProcessor {
366 fn process_array(&self, array: &NDArray, _pool: &NDArrayPool) -> ProcessResult {
367 let ndims = array.dims.len();
372 let dims = [
373 array.dims.first().map(|d| d.size).unwrap_or(0),
374 array.dims.get(1).map(|d| d.size).unwrap_or(0),
375 ];
376 let array_size_x = dims[0];
377 let supported = ndims == 1 || ndims == 2;
378
379 let rois: Vec<ROIStatROI> = self.state.lock().rois.clone();
385
386 let clamped: Vec<Option<([usize; 2], [usize; 2])>> = rois
390 .iter()
391 .map(|roi| {
392 if roi.enabled && supported {
393 Self::clamp_roi_geometry(roi, ndims, dims)
394 } else {
395 None
396 }
397 })
398 .collect();
399
400 let serial_results = || -> Vec<ROIStatResult> {
401 rois.iter()
402 .zip(clamped.iter())
403 .map(|(roi, clamp)| match clamp {
404 Some((offset, size)) => Self::compute_roi_stats(
405 &array.data,
406 ndims,
407 array_size_x,
408 *offset,
409 *size,
410 roi.bgd_width,
411 ),
412 None => ROIStatResult::default(),
413 })
414 .collect()
415 };
416
417 #[cfg(feature = "parallel")]
418 let new_results: Vec<ROIStatResult> = {
419 let total_elements: usize = clamped
420 .iter()
421 .flatten()
422 .map(|(_, size)| {
423 if ndims == 1 {
424 size[0]
425 } else {
426 size[0] * size[1]
427 }
428 })
429 .sum();
430
431 if par_util::should_parallelize(total_elements) {
432 let data = &array.data;
433 par_util::thread_pool().install(|| {
434 rois.par_iter()
435 .zip(clamped.par_iter())
436 .map(|(roi, clamp)| match clamp {
437 Some((offset, size)) => Self::compute_roi_stats(
438 data,
439 ndims,
440 array_size_x,
441 *offset,
442 *size,
443 roi.bgd_width,
444 ),
445 None => ROIStatResult::default(),
446 })
447 .collect()
448 })
449 } else {
450 serial_results()
451 }
452 };
453
454 #[cfg(not(feature = "parallel"))]
455 let new_results: Vec<ROIStatResult> = serial_results();
456
457 let mut guard = self.state.lock();
458 let state = &mut *guard;
459 state.results = new_results;
460 state
464 .results
465 .resize(state.rois.len(), ROIStatResult::default());
466
467 if state.ts_mode == TSMode::Acquiring {
469 if state.ts_num_points > 0 && state.ts_current >= state.ts_num_points {
470 state.ts_mode = TSMode::Idle;
472 } else {
473 while state.ts_buffers.len() < state.rois.len() {
475 state.ts_buffers.push(vec![Vec::new(); NUM_STATS]);
476 }
477
478 for (i, result) in state.results.iter().enumerate() {
479 if i >= state.ts_buffers.len() {
480 break;
481 }
482 let stats = [
483 result.min,
484 result.max,
485 result.mean,
486 result.total,
487 result.net,
488 ];
489 for (s, &val) in stats.iter().enumerate() {
490 let buf = &mut state.ts_buffers[i][s];
491 buf.push(val);
492 }
493 }
494 state.ts_current += 1;
495 }
496 }
497
498 if let Some(ref sender) = self.ts_sender {
500 let mut values = Vec::with_capacity(state.results.len() * NUM_STATS);
501 for result in &state.results {
502 values.push(result.min);
503 values.push(result.max);
504 values.push(result.mean);
505 values.push(result.total);
506 values.push(result.net);
507 }
508 let _ = sender.try_send(TimeSeriesData { values });
509 }
510
511 let p = &self.params;
513 let mut updates = Vec::new();
514 for (i, roi) in rois.iter().enumerate() {
515 if !roi.enabled || i >= state.results.len() {
516 continue;
517 }
518 let result = &state.results[i];
519 let addr = i as i32;
520 updates.push(ParamUpdate::float64_addr(p.min_value, addr, result.min));
521 updates.push(ParamUpdate::float64_addr(p.max_value, addr, result.max));
522 updates.push(ParamUpdate::float64_addr(p.mean_value, addr, result.mean));
523 updates.push(ParamUpdate::float64_addr(p.total, addr, result.total));
524 updates.push(ParamUpdate::float64_addr(p.net, addr, result.net));
525
526 updates.push(ParamUpdate::int32_addr(
531 p.dim0_max_size,
532 addr,
533 if ndims >= 1 { dims[0] as i32 } else { 0 },
534 ));
535 updates.push(ParamUpdate::int32_addr(
536 p.dim1_max_size,
537 addr,
538 if ndims >= 2 { dims[1] as i32 } else { 0 },
539 ));
540 if let Some((offset, size)) = clamped[i] {
541 if ndims >= 1 {
542 updates.push(ParamUpdate::int32_addr(p.dim0_min, addr, offset[0] as i32));
543 updates.push(ParamUpdate::int32_addr(p.dim0_size, addr, size[0] as i32));
544 }
545 if ndims >= 2 {
546 updates.push(ParamUpdate::int32_addr(p.dim1_min, addr, offset[1] as i32));
547 updates.push(ParamUpdate::int32_addr(p.dim1_size, addr, size[1] as i32));
548 }
549 }
550 }
551 updates.push(ParamUpdate::int32(
552 p.ts_current_point,
553 state.ts_current as i32,
554 ));
555 updates.push(ParamUpdate::int32(
556 p.ts_acquiring,
557 if state.ts_mode == TSMode::Acquiring {
558 1
559 } else {
560 0
561 },
562 ));
563
564 for (i, roi) in rois.iter().enumerate() {
566 if !roi.enabled || i >= state.ts_buffers.len() {
567 continue;
568 }
569 let addr = i as i32;
570 let bufs = &state.ts_buffers[i];
571 if !bufs.is_empty() {
573 updates.push(ParamUpdate::float64_array_addr(
574 p.ts_min_value,
575 addr,
576 bufs[0].clone(),
577 ));
578 }
579 if bufs.len() > 1 {
580 updates.push(ParamUpdate::float64_array_addr(
581 p.ts_max_value,
582 addr,
583 bufs[1].clone(),
584 ));
585 }
586 if bufs.len() > 2 {
587 updates.push(ParamUpdate::float64_array_addr(
588 p.ts_mean_value,
589 addr,
590 bufs[2].clone(),
591 ));
592 }
593 if bufs.len() > 3 {
594 updates.push(ParamUpdate::float64_array_addr(
595 p.ts_total,
596 addr,
597 bufs[3].clone(),
598 ));
599 }
600 if bufs.len() > 4 {
601 updates.push(ParamUpdate::float64_array_addr(
602 p.ts_net,
603 addr,
604 bufs[4].clone(),
605 ));
606 }
607 }
608
609 ProcessResult::sink(updates)
610 }
611
612 fn plugin_type(&self) -> &str {
613 "NDPluginROIStat"
614 }
615
616 fn register_params(
617 &mut self,
618 base: &mut PortDriverBase,
619 ) -> Result<(), asyn_rs::error::AsynError> {
620 let state = self.state.get_mut();
622 self.params.reset_all = base.create_param("ROISTAT_RESETALL", ParamType::Int32)?;
623 self.params.ts_control = base.create_param("ROISTAT_TS_CONTROL", ParamType::Int32)?;
624 self.params.ts_num_points = base.create_param("ROISTAT_TS_NUM_POINTS", ParamType::Int32)?;
625 base.set_int32_param(self.params.ts_num_points, 0, state.ts_num_points as i32)?;
626 self.params.ts_current_point =
627 base.create_param("ROISTAT_TS_CURRENT_POINT", ParamType::Int32)?;
628 self.params.ts_acquiring = base.create_param("ROISTAT_TS_ACQUIRING", ParamType::Int32)?;
629
630 self.params.use_ = base.create_param("ROISTAT_USE", ParamType::Int32)?;
632 self.params.name = base.create_param("ROISTAT_NAME", ParamType::Octet)?;
633 self.params.reset = base.create_param("ROISTAT_RESET", ParamType::Int32)?;
634 self.params.bgd_width = base.create_param("ROISTAT_BGD_WIDTH", ParamType::Int32)?;
635 self.params.dim0_min = base.create_param("ROISTAT_DIM0_MIN", ParamType::Int32)?;
636 self.params.dim1_min = base.create_param("ROISTAT_DIM1_MIN", ParamType::Int32)?;
637 self.params.dim0_size = base.create_param("ROISTAT_DIM0_SIZE", ParamType::Int32)?;
638 self.params.dim1_size = base.create_param("ROISTAT_DIM1_SIZE", ParamType::Int32)?;
639 self.params.dim0_max_size = base.create_param("ROISTAT_DIM0_MAX_SIZE", ParamType::Int32)?;
640 self.params.dim1_max_size = base.create_param("ROISTAT_DIM1_MAX_SIZE", ParamType::Int32)?;
641 self.params.min_value = base.create_param("ROISTAT_MIN_VALUE", ParamType::Float64)?;
642 self.params.max_value = base.create_param("ROISTAT_MAX_VALUE", ParamType::Float64)?;
643 self.params.mean_value = base.create_param("ROISTAT_MEAN_VALUE", ParamType::Float64)?;
644 self.params.total = base.create_param("ROISTAT_TOTAL", ParamType::Float64)?;
645 self.params.net = base.create_param("ROISTAT_NET", ParamType::Float64)?;
646
647 self.params.ts_total = base.create_param("ROISTAT_TS_TOTAL", ParamType::Float64Array)?;
649 self.params.ts_net = base.create_param("ROISTAT_TS_NET", ParamType::Float64Array)?;
650 self.params.ts_mean_value =
651 base.create_param("ROISTAT_TS_MEAN_VALUE", ParamType::Float64Array)?;
652 self.params.ts_min_value =
653 base.create_param("ROISTAT_TS_MIN_VALUE", ParamType::Float64Array)?;
654 self.params.ts_max_value =
655 base.create_param("ROISTAT_TS_MAX_VALUE", ParamType::Float64Array)?;
656 self.params.ts_timestamp =
657 base.create_param("ROISTAT_TS_TIMESTAMP", ParamType::Float64Array)?;
658
659 for (i, roi) in state.rois.iter().enumerate() {
661 let addr = i as i32;
662 base.set_int32_param(self.params.use_, addr, roi.enabled as i32)?;
663 base.set_int32_param(self.params.bgd_width, addr, roi.bgd_width as i32)?;
664 base.set_int32_param(self.params.dim0_min, addr, roi.offset[0] as i32)?;
665 base.set_int32_param(self.params.dim1_min, addr, roi.offset[1] as i32)?;
666 base.set_int32_param(self.params.dim0_size, addr, roi.size[0] as i32)?;
667 base.set_int32_param(self.params.dim1_size, addr, roi.size[1] as i32)?;
668 }
669
670 *self.params_out.lock() = self.params;
672
673 Ok(())
674 }
675
676 fn on_param_change(
677 &self,
678 reason: usize,
679 snapshot: &PluginParamSnapshot,
680 ) -> ad_core_rs::plugin::runtime::ParamChangeResult {
681 let addr = snapshot.addr as usize;
682 let p = &self.params;
683 let mut guard = self.state.lock();
684 let state = &mut *guard;
685
686 if reason == p.use_ && addr < state.rois.len() {
687 state.rois[addr].enabled = snapshot.value.as_i32() != 0;
688 } else if reason == p.dim0_min && addr < state.rois.len() {
689 state.rois[addr].offset[0] = snapshot.value.as_i32().max(0) as usize;
690 } else if reason == p.dim1_min && addr < state.rois.len() {
691 state.rois[addr].offset[1] = snapshot.value.as_i32().max(0) as usize;
692 } else if reason == p.dim0_size && addr < state.rois.len() {
693 state.rois[addr].size[0] = snapshot.value.as_i32().max(0) as usize;
694 } else if reason == p.dim1_size && addr < state.rois.len() {
695 state.rois[addr].size[1] = snapshot.value.as_i32().max(0) as usize;
696 } else if reason == p.bgd_width && addr < state.rois.len() {
697 state.rois[addr].bgd_width = snapshot.value.as_i32().max(0) as usize;
698 } else if reason == p.reset && addr < state.rois.len() {
699 state.results[addr] = ROIStatResult::default();
700 } else if reason == p.reset_all {
701 for r in &mut state.results {
702 *r = ROIStatResult::default();
703 }
704 } else if reason == p.ts_control {
705 match snapshot.value.as_i32() {
707 0 => {
708 for roi_bufs in &mut state.ts_buffers {
710 for stat_buf in roi_bufs.iter_mut() {
711 stat_buf.clear();
712 }
713 }
714 state.ts_current = 0;
715 state.ts_mode = TSMode::Acquiring;
716 }
717 1 => {
718 state.ts_mode = TSMode::Acquiring;
720 }
721 2 => {
722 state.ts_mode = TSMode::Idle;
724 }
725 3 => {
726 }
728 4 => {
729 for roi_bufs in &mut state.ts_buffers {
731 for stat_buf in roi_bufs.iter_mut() {
732 stat_buf.clear();
733 }
734 }
735 state.ts_current = 0;
736 }
737 _ => {}
738 }
739 } else if reason == p.ts_num_points {
740 state.ts_num_points = snapshot.value.as_i32().max(0) as usize;
741 }
742 ad_core_rs::plugin::runtime::ParamChangeResult::empty()
743 }
744}
745
746pub fn create_roi_stat_runtime(
749 port_name: &str,
750 pool: Arc<NDArrayPool>,
751 queue_size: usize,
752 ndarray_port: &str,
753 wiring: Arc<WiringRegistry>,
754 num_rois: usize,
755 ts_registry: &crate::time_series::TsReceiverRegistry,
756) -> (
757 PluginRuntimeHandle,
758 ROIStatParams,
759 std::thread::JoinHandle<()>,
760) {
761 let (ts_tx, ts_rx) = tokio::sync::mpsc::channel(256);
762
763 let rois: Vec<ROIStatROI> = (0..num_rois).map(|_| ROIStatROI::default()).collect();
764 let mut processor = ROIStatProcessor::new(rois, 2048);
765 processor.set_ts_sender(ts_tx);
766 let params_handle = processor.params_handle();
767
768 let (handle, data_jh) = ad_core_rs::plugin::runtime::create_plugin_runtime_multi_addr(
769 port_name,
770 processor,
771 pool,
772 queue_size,
773 ndarray_port,
774 wiring,
775 num_rois,
776 );
777
778 let roi_stat_params = *params_handle.lock();
779
780 let channel_names = roi_stat_ts_channel_names(num_rois);
782 ts_registry.store(port_name, ts_rx, channel_names);
783
784 (handle, roi_stat_params, data_jh)
785}
786
787#[cfg(test)]
788mod tests {
789 use super::*;
790 use ad_core_rs::ndarray::{NDDataType, NDDimension};
791
792 fn make_2d_array(x: usize, y: usize, fill: impl Fn(usize, usize) -> f64) -> NDArray {
793 let mut arr = NDArray::new(
794 vec![NDDimension::new(x), NDDimension::new(y)],
795 NDDataType::Float64,
796 );
797 if let NDDataBuffer::F64(ref mut v) = arr.data {
798 for iy in 0..y {
799 for ix in 0..x {
800 v[iy * x + ix] = fill(ix, iy);
801 }
802 }
803 }
804 arr
805 }
806
807 #[test]
808 fn test_single_roi_full_image() {
809 let arr = make_2d_array(4, 4, |_x, _y| 10.0);
810 let rois = vec![ROIStatROI {
811 enabled: true,
812 offset: [0, 0],
813 size: [4, 4],
814 bgd_width: 0,
815 }];
816
817 let proc = ROIStatProcessor::new(rois, 0);
818 let pool = NDArrayPool::new(1_000_000);
819 proc.process_array(&arr, &pool);
820
821 let r = &proc.results()[0];
822 assert!((r.min - 10.0).abs() < 1e-10);
823 assert!((r.max - 10.0).abs() < 1e-10);
824 assert!((r.mean - 10.0).abs() < 1e-10);
825 assert!((r.total - 160.0).abs() < 1e-10);
826 }
827
828 #[test]
829 fn test_single_roi_subregion() {
830 let arr = make_2d_array(8, 8, |x, y| (x + y * 8) as f64);
832
833 let rois = vec![ROIStatROI {
834 enabled: true,
835 offset: [2, 2],
836 size: [3, 3],
837 bgd_width: 0,
838 }];
839
840 let proc = ROIStatProcessor::new(rois, 0);
841 let pool = NDArrayPool::new(1_000_000);
842 proc.process_array(&arr, &pool);
843
844 let r = &proc.results()[0];
845 assert!((r.min - 18.0).abs() < 1e-10);
847 assert!((r.max - 36.0).abs() < 1e-10);
848 let expected_total = 18.0 + 19.0 + 20.0 + 26.0 + 27.0 + 28.0 + 34.0 + 35.0 + 36.0;
849 assert!((r.total - expected_total).abs() < 1e-10);
850 assert!((r.mean - expected_total / 9.0).abs() < 1e-10);
851 }
852
853 #[test]
854 fn test_multiple_rois() {
855 let arr = make_2d_array(8, 8, |x, _y| x as f64);
856
857 let rois = vec![
858 ROIStatROI {
859 enabled: true,
860 offset: [0, 0],
861 size: [4, 4],
862 bgd_width: 0,
863 },
864 ROIStatROI {
865 enabled: true,
866 offset: [4, 0],
867 size: [4, 4],
868 bgd_width: 0,
869 },
870 ];
871
872 let proc = ROIStatProcessor::new(rois, 0);
873 let pool = NDArrayPool::new(1_000_000);
874 proc.process_array(&arr, &pool);
875
876 let r0 = &proc.results()[0];
877 assert!((r0.min - 0.0).abs() < 1e-10);
878 assert!((r0.max - 3.0).abs() < 1e-10);
879
880 let r1 = &proc.results()[1];
881 assert!((r1.min - 4.0).abs() < 1e-10);
882 assert!((r1.max - 7.0).abs() < 1e-10);
883 }
884
885 #[test]
886 fn test_bgd_width() {
887 let arr = make_2d_array(6, 6, |x, y| {
889 if x >= 2 && x < 4 && y >= 2 && y < 4 {
890 100.0
891 } else {
892 10.0
893 }
894 });
895
896 let rois = vec![ROIStatROI {
897 enabled: true,
898 offset: [1, 1],
899 size: [4, 4],
900 bgd_width: 1,
901 }];
902
903 let proc = ROIStatProcessor::new(rois, 0);
904 let pool = NDArrayPool::new(1_000_000);
905 proc.process_array(&arr, &pool);
906
907 let r = &proc.results()[0];
908 assert!(
913 r.net < r.total,
914 "net should be less than total with bgd subtraction"
915 );
916 }
917
918 #[test]
919 fn test_empty_roi() {
920 let arr = make_2d_array(4, 4, |_, _| 10.0);
921 let rois = vec![ROIStatROI {
922 enabled: true,
923 offset: [0, 0],
924 size: [0, 0],
925 bgd_width: 0,
926 }];
927
928 let proc = ROIStatProcessor::new(rois, 0);
929 let pool = NDArrayPool::new(1_000_000);
930 proc.process_array(&arr, &pool);
931
932 let r = &proc.results()[0];
935 assert!((r.total - 10.0).abs() < 1e-10);
936 assert!((r.mean - 10.0).abs() < 1e-10);
937 }
938
939 #[test]
940 fn test_disabled_roi() {
941 let arr = make_2d_array(4, 4, |_, _| 10.0);
942 let rois = vec![ROIStatROI {
943 enabled: false,
944 offset: [0, 0],
945 size: [4, 4],
946 bgd_width: 0,
947 }];
948
949 let proc = ROIStatProcessor::new(rois, 0);
950 let pool = NDArrayPool::new(1_000_000);
951 proc.process_array(&arr, &pool);
952
953 let r = &proc.results()[0];
954 assert!(
955 (r.total - 0.0).abs() < 1e-10,
956 "disabled ROI should have zero stats"
957 );
958 }
959
960 #[test]
961 fn test_roi_out_of_bounds() {
962 let arr = make_2d_array(4, 4, |_, _| 10.0);
963 let rois = vec![ROIStatROI {
964 enabled: true,
965 offset: [10, 10],
966 size: [4, 4],
967 bgd_width: 0,
968 }];
969
970 let proc = ROIStatProcessor::new(rois, 0);
971 let pool = NDArrayPool::new(1_000_000);
972 proc.process_array(&arr, &pool);
973
974 let r = &proc.results()[0];
977 assert!(
978 (r.total - 10.0).abs() < 1e-10,
979 "out-of-bounds ROI clamps to one edge pixel, not zero"
980 );
981 assert!((r.mean - 10.0).abs() < 1e-10);
982 }
983
984 #[test]
985 fn test_roi_partially_out_of_bounds() {
986 let arr = make_2d_array(4, 4, |_, _| 5.0);
987 let rois = vec![ROIStatROI {
988 enabled: true,
989 offset: [2, 2],
990 size: [10, 10], bgd_width: 0,
992 }];
993
994 let proc = ROIStatProcessor::new(rois, 0);
995 let pool = NDArrayPool::new(1_000_000);
996 proc.process_array(&arr, &pool);
997
998 let r = &proc.results()[0];
999 assert!((r.total - 20.0).abs() < 1e-10);
1001 assert!((r.mean - 5.0).abs() < 1e-10);
1002 }
1003
1004 #[test]
1005 fn test_time_series() {
1006 let rois = vec![ROIStatROI {
1007 enabled: true,
1008 offset: [0, 0],
1009 size: [4, 4],
1010 bgd_width: 0,
1011 }];
1012
1013 let proc = ROIStatProcessor::new(rois, 100);
1014 let pool = NDArrayPool::new(1_000_000);
1015 proc.set_ts_mode(TSMode::Acquiring);
1016
1017 for i in 0..5 {
1018 let arr = make_2d_array(4, 4, |_, _| (i + 1) as f64);
1019 proc.process_array(&arr, &pool);
1020 }
1021
1022 let ts = proc.ts_buffer(0, 2);
1024 assert_eq!(ts.len(), 5);
1025 assert!((ts[0] - 1.0).abs() < 1e-10);
1026 assert!((ts[4] - 5.0).abs() < 1e-10);
1027 }
1028
1029 #[test]
1030 fn test_u8_data() {
1031 let mut arr = NDArray::new(
1032 vec![NDDimension::new(4), NDDimension::new(4)],
1033 NDDataType::UInt8,
1034 );
1035 if let NDDataBuffer::U8(ref mut v) = arr.data {
1036 for (i, val) in v.iter_mut().enumerate() {
1037 *val = (i + 1) as u8;
1038 }
1039 }
1040
1041 let rois = vec![ROIStatROI {
1042 enabled: true,
1043 offset: [0, 0],
1044 size: [4, 4],
1045 bgd_width: 0,
1046 }];
1047
1048 let proc = ROIStatProcessor::new(rois, 0);
1049 let pool = NDArrayPool::new(1_000_000);
1050 proc.process_array(&arr, &pool);
1051
1052 let r = &proc.results()[0];
1053 assert!((r.min - 1.0).abs() < 1e-10);
1054 assert!((r.max - 16.0).abs() < 1e-10);
1055 }
1056
1057 #[test]
1058 fn test_ts_channel_names() {
1059 let names = roi_stat_ts_channel_names(2);
1060 assert_eq!(names.len(), 10); assert_eq!(names[0], "TS1:MinValue");
1062 assert_eq!(names[1], "TS1:MaxValue");
1063 assert_eq!(names[4], "TS1:Net");
1064 assert_eq!(names[5], "TS2:MinValue");
1065 assert_eq!(names[9], "TS2:Net");
1066 }
1067
1068 #[test]
1069 fn test_ts_sender_integration() {
1070 let (tx, mut rx) = tokio::sync::mpsc::channel::<TimeSeriesData>(16);
1071
1072 let rois = vec![
1073 ROIStatROI {
1074 enabled: true,
1075 offset: [0, 0],
1076 size: [4, 4],
1077 bgd_width: 0,
1078 },
1079 ROIStatROI {
1080 enabled: true,
1081 offset: [0, 0],
1082 size: [2, 2],
1083 bgd_width: 0,
1084 },
1085 ];
1086
1087 let mut proc = ROIStatProcessor::new(rois, 0);
1088 proc.set_ts_sender(tx);
1089
1090 let pool = NDArrayPool::new(1_000_000);
1091 let arr = make_2d_array(4, 4, |_, _| 7.0);
1092 proc.process_array(&arr, &pool);
1093
1094 let data = rx.try_recv().unwrap();
1095 assert_eq!(data.values.len(), 10);
1097 assert!((data.values[0] - 7.0).abs() < 1e-10); assert!((data.values[1] - 7.0).abs() < 1e-10); assert!((data.values[2] - 7.0).abs() < 1e-10); assert!((data.values[3] - 112.0).abs() < 1e-10); assert!((data.values[8] - 28.0).abs() < 1e-10); }
1105
1106 fn make_1d_array(n: usize, fill: impl Fn(usize) -> f64) -> NDArray {
1107 let mut arr = NDArray::new(vec![NDDimension::new(n)], NDDataType::Float64);
1108 if let NDDataBuffer::F64(ref mut v) = arr.data {
1109 for (i, slot) in v.iter_mut().enumerate() {
1110 *slot = fill(i);
1111 }
1112 }
1113 arr
1114 }
1115
1116 #[test]
1117 fn test_adp16_clamp_out_of_range_offset_to_one_pixel() {
1118 let roi = ROIStatROI {
1121 enabled: true,
1122 offset: [10, 1],
1123 size: [4, 9],
1124 bgd_width: 0,
1125 };
1126 let (offset, size) = ROIStatProcessor::clamp_roi_geometry(&roi, 2, [4, 4]).unwrap();
1127 assert_eq!(offset, [3, 1]); assert_eq!(size, [1, 3]); }
1130
1131 #[test]
1132 fn test_adp16_clamp_zero_size_to_one_pixel() {
1133 let roi = ROIStatROI {
1134 enabled: true,
1135 offset: [0, 0],
1136 size: [0, 0],
1137 bgd_width: 0,
1138 };
1139 let (offset, size) = ROIStatProcessor::clamp_roi_geometry(&roi, 2, [4, 4]).unwrap();
1140 assert_eq!(offset, [0, 0]);
1141 assert_eq!(size, [1, 1]); }
1143
1144 #[test]
1145 fn test_adp16_geometry_writeback_uses_clamped_values() {
1146 use asyn_rs::port::{PortDriverBase, PortFlags};
1147
1148 let arr = make_2d_array(4, 4, |_, _| 10.0);
1149 let rois = vec![ROIStatROI {
1150 enabled: true,
1151 offset: [10, 1],
1152 size: [4, 9],
1153 bgd_width: 0,
1154 }];
1155 let mut proc = ROIStatProcessor::new(rois, 0);
1156 let mut base = PortDriverBase::new("roistat_adp16", 1, PortFlags::default());
1157 proc.register_params(&mut base).unwrap();
1158 let p = *proc.params_handle().lock();
1159
1160 let pool = NDArrayPool::new(1_000_000);
1161 let res = proc.process_array(&arr, &pool);
1162
1163 let find = |reason: usize, addr: i32| {
1164 res.param_updates.iter().find_map(|u| match u {
1165 ParamUpdate::Int32 {
1166 reason: r,
1167 addr: a,
1168 value,
1169 } if *r == reason && *a == addr => Some(*value),
1170 _ => None,
1171 })
1172 };
1173 assert_eq!(find(p.dim0_min, 0), Some(3));
1175 assert_eq!(find(p.dim0_size, 0), Some(1));
1176 assert_eq!(find(p.dim1_min, 0), Some(1));
1177 assert_eq!(find(p.dim1_size, 0), Some(3));
1178 assert_eq!(find(p.dim0_max_size, 0), Some(4));
1179 assert_eq!(find(p.dim1_max_size, 0), Some(4));
1180 }
1181
1182 #[test]
1183 fn test_adp17_1d_background_uses_x_strips_only() {
1184 let arr = make_1d_array(6, |x| if x == 0 || x == 5 { 10.0 } else { 0.0 });
1190 let rois = vec![ROIStatROI {
1191 enabled: true,
1192 offset: [0, 0],
1193 size: [6, 0],
1194 bgd_width: 1,
1195 }];
1196 let proc = ROIStatProcessor::new(rois, 0);
1197 let pool = NDArrayPool::new(1_000_000);
1198 proc.process_array(&arr, &pool);
1199
1200 let r = &proc.results()[0];
1201 assert!((r.total - 20.0).abs() < 1e-10, "total={}", r.total);
1202 assert!((r.min - 0.0).abs() < 1e-10);
1203 assert!((r.max - 10.0).abs() < 1e-10);
1204 assert!((r.net + 40.0).abs() < 1e-10, "net={}", r.net);
1205 }
1206}