1use std::sync::Arc;
2
3#[cfg(feature = "parallel")]
4use crate::par_util;
5#[cfg(feature = "parallel")]
6use rayon::prelude::*;
7
8use ad_core_rs::ndarray::{NDArray, NDDataBuffer, NDDataType};
9use ad_core_rs::ndarray_pool::NDArrayPool;
10use ad_core_rs::plugin::runtime::{NDPluginProcess, ProcessResult};
11
12#[derive(Debug, Clone)]
27pub struct FilterConfig {
28 pub num_filter: usize,
30 pub auto_reset: bool,
32 pub filter_callbacks: usize,
34 pub oc: [f64; 4],
36 pub fc: [f64; 4],
38 pub rc: [f64; 2],
40 pub r_offset: f64,
42 pub o_offset: f64,
44 pub o_scale: f64,
46 pub f_offset: f64,
48 pub f_scale: f64,
50}
51
52impl Default for FilterConfig {
53 fn default() -> Self {
54 Self {
55 num_filter: 1,
56 auto_reset: false,
57 filter_callbacks: 0,
58 oc: [1.0, 0.0, 0.0, 0.0], fc: [1.0, 0.0, 0.0, 0.0],
60 rc: [1.0, 0.0],
61 r_offset: 0.0,
62 o_offset: 0.0,
63 o_scale: 1.0,
64 f_offset: 0.0,
65 f_scale: 1.0,
66 }
67 }
68}
69
70#[derive(Debug, Clone)]
72pub struct ProcessConfig {
73 pub enable_background: bool,
74 pub enable_flat_field: bool,
75 pub enable_offset_scale: bool,
76 pub offset: f64,
77 pub scale: f64,
78 pub enable_low_clip: bool,
79 pub low_clip_thresh: f64,
80 pub low_clip_value: f64,
81 pub enable_high_clip: bool,
82 pub high_clip_thresh: f64,
83 pub high_clip_value: f64,
84 pub scale_flat_field: f64,
85 pub enable_filter: bool,
86 pub filter: FilterConfig,
87 pub output_type: Option<NDDataType>,
88 pub auto_offset_scale_pending: bool,
91 pub valid_background: bool,
93 pub valid_flat_field: bool,
95}
96
97impl Default for ProcessConfig {
98 fn default() -> Self {
99 Self {
100 enable_background: false,
101 enable_flat_field: false,
102 enable_offset_scale: false,
103 offset: 0.0,
104 scale: 1.0,
105 enable_low_clip: false,
106 low_clip_thresh: 0.0,
107 low_clip_value: 0.0,
108 enable_high_clip: false,
109 high_clip_thresh: 100.0,
110 high_clip_value: 100.0,
111 scale_flat_field: 255.0,
112 enable_filter: false,
113 filter: FilterConfig::default(),
114 output_type: None,
115 auto_offset_scale_pending: false,
116 valid_background: false,
117 valid_flat_field: false,
118 }
119 }
120}
121
122fn elements_as_f64(array: &NDArray) -> Vec<f64> {
125 (0..array.data.len())
126 .map(|i| array.data.get_as_f64(i).unwrap_or(0.0))
127 .collect()
128}
129
130pub struct ProcessState {
134 pub config: ProcessConfig,
135 pub background: Option<Vec<f64>>,
136 pub flat_field: Option<Vec<f64>>,
137 pub filter_state: Option<Vec<f64>>,
144 pub num_filtered: usize,
146 reset_filter_pending: bool,
150 last_output: Option<NDArray>,
163}
164
165#[inline]
180fn accumulate(acc: f64, coef: f64, term: f64) -> f64 {
181 if coef != 0.0 { acc + coef * term } else { acc }
182}
183
184impl ProcessState {
185 pub fn new(config: ProcessConfig) -> Self {
186 Self {
187 config,
188 background: None,
189 flat_field: None,
190 filter_state: None,
191 num_filtered: 0,
192 reset_filter_pending: false,
193 last_output: None,
194 }
195 }
196
197 pub fn last_output(&self) -> Option<&NDArray> {
200 self.last_output.as_ref()
201 }
202
203 pub fn save_background(&mut self) {
223 let saved = self.last_output.as_ref().map(elements_as_f64);
224 self.config.valid_background = saved.is_some();
225 self.background = saved;
226 }
227
228 pub fn save_flat_field(&mut self) {
232 let saved = self.last_output.as_ref().map(elements_as_f64);
233 self.config.valid_flat_field = saved.is_some();
234 self.flat_field = saved;
235 }
236
237 pub fn auto_offset_scale(&mut self, array: &NDArray) {
242 let n = array.data.len();
243 if n == 0 {
244 return;
245 }
246 let mut min_val = f64::MAX;
247 let mut max_val = f64::MIN;
248 for i in 0..n {
249 let v = array.data.get_as_f64(i).unwrap_or(0.0);
250 if v < min_val {
251 min_val = v;
252 }
253 if v > max_val {
254 max_val = v;
255 }
256 }
257 let range = max_val - min_val;
258 if range > 0.0 {
259 let bytes_per_elem = match self.config.output_type.unwrap_or(array.data.data_type()) {
261 NDDataType::Int8 | NDDataType::UInt8 => 1,
262 NDDataType::Int16 | NDDataType::UInt16 => 2,
263 NDDataType::Int32 | NDDataType::UInt32 => 4,
264 NDDataType::Int64 | NDDataType::UInt64 => 8,
265 NDDataType::Float32 => 4,
266 NDDataType::Float64 => 8,
267 };
268 let max_scale = 2.0f64.powi(bytes_per_elem * 8) - 1.0;
269 self.config.scale = max_scale / range;
271 self.config.offset = -min_val;
272 self.config.enable_offset_scale = true;
274 self.config.enable_low_clip = true;
275 self.config.low_clip_thresh = 0.0;
276 self.config.enable_high_clip = true;
277 self.config.high_clip_thresh = max_scale;
278 }
279 }
280
281 pub fn apply_filter_type(&mut self, filter_type: i32) {
289 let fc = &mut self.config.filter;
290 match filter_type {
291 0 => {
292 fc.fc = [1.0, -1.0, 0.0, 1.0];
306 fc.oc = [1.0, 0.0, 0.0, 0.0];
307 fc.rc = [0.0, 1.0]; fc.r_offset = 0.0;
309 fc.f_offset = 0.0;
310 fc.f_scale = 1.0;
311 fc.o_offset = 0.0;
312 fc.o_scale = 1.0;
313 }
314 1 => {
315 fc.fc = [1.0, 0.0, 1.0, 0.0];
321 fc.oc = [0.0, 1.0, 0.0, 0.0];
322 fc.rc = [0.0, 1.0]; fc.r_offset = 0.0;
324 fc.f_offset = 0.0;
325 fc.f_scale = 1.0;
326 fc.o_offset = 0.0;
327 fc.o_scale = 1.0;
328 }
329 2 => {
330 fc.fc = [1.0, 0.0, 1.0, 0.0];
332 fc.oc = [1.0, 0.0, 0.0, 0.0];
333 fc.rc = [0.0, 1.0];
334 fc.r_offset = 0.0;
335 fc.f_offset = 0.0;
336 fc.f_scale = 1.0;
337 fc.o_offset = 0.0;
338 fc.o_scale = 1.0;
339 }
340 3 => {
341 fc.fc = [0.0, 0.0, 1.0, 0.0];
345 fc.oc = [-1.0, 0.0, 1.0, 0.0];
346 fc.rc = [0.0, 1.0];
347 fc.r_offset = 0.0;
348 fc.f_offset = 0.0;
349 fc.f_scale = 1.0;
350 fc.o_offset = 0.0;
351 fc.o_scale = 1.0;
352 }
353 4 => {
354 fc.fc = [1.0, -1.0, 0.0, 1.0];
357 fc.oc = [-1.0, 0.0, 1.0, 0.0];
358 fc.rc = [0.0, 1.0];
359 fc.r_offset = 0.0;
360 fc.f_offset = 0.0;
361 fc.f_scale = 1.0;
362 fc.o_offset = 0.0;
363 fc.o_scale = 1.0;
364 }
365 5 => {
366 fc.fc = [0.0, 0.0, 1.0, 0.0];
368 fc.oc = [1.0, 0.0, 0.0, 0.0];
369 fc.rc = [0.0, 1.0];
370 fc.r_offset = 0.0;
371 fc.f_offset = 0.0;
372 fc.f_scale = 1.0;
373 fc.o_offset = 0.0;
374 fc.o_scale = 1.0;
375 }
376 _ => {} }
378 }
379
380 pub fn reset_filter(&mut self) {
390 self.reset_filter_pending = true;
391 }
392
393 pub fn process(&mut self, src: &NDArray) -> Option<NDArray> {
400 let n = src.data.len();
401 let mut values = vec![0.0f64; n];
402 for i in 0..n {
403 values[i] = src.data.get_as_f64(i).unwrap_or(0.0);
404 }
405
406 let reset_requested = self.reset_filter_pending;
411 self.reset_filter_pending = false;
412
413 let auto_offset_scale_now = self.config.auto_offset_scale_pending;
420 self.config.auto_offset_scale_pending = false;
421
422 self.config.valid_background = self.background.as_ref().is_some_and(|b| b.len() == n);
428 self.config.valid_flat_field = self.flat_field.as_ref().is_some_and(|f| f.len() == n);
429
430 let needs_element_ops = self.config.enable_background
433 || self.config.enable_flat_field
434 || self.config.enable_offset_scale
435 || self.config.enable_low_clip
436 || self.config.enable_high_clip;
437
438 if needs_element_ops {
439 let bg = if self.config.enable_background && self.config.valid_background {
442 self.background.as_ref()
443 } else {
444 None
445 };
446 let (ff, ff_scale) = if self.config.enable_flat_field && self.config.valid_flat_field {
447 if let Some(ref ff) = self.flat_field {
448 (Some(ff.as_slice()), self.config.scale_flat_field)
452 } else {
453 (None, 0.0)
454 }
455 } else {
456 (None, 0.0)
457 };
458 let do_offset_scale = self.config.enable_offset_scale;
459 let scale = self.config.scale;
460 let offset = self.config.offset;
461 let do_low_clip = self.config.enable_low_clip;
462 let low_clip_thresh = self.config.low_clip_thresh;
463 let low_clip_value = self.config.low_clip_value;
464 let do_high_clip = self.config.enable_high_clip;
465 let high_clip_thresh = self.config.high_clip_thresh;
466 let high_clip_value = self.config.high_clip_value;
467
468 let apply_stages = |i: usize, v: &mut f64| {
469 if let Some(bg) = bg {
473 *v -= bg[i];
474 }
475 if let Some(ff) = ff {
477 if ff[i] != 0.0 {
478 *v = *v * ff_scale / ff[i];
479 }
480 }
481 if do_offset_scale {
483 *v = (*v + offset) * scale;
484 }
485 if do_high_clip && *v > high_clip_thresh {
489 *v = high_clip_value;
490 }
491 if do_low_clip && *v < low_clip_thresh {
492 *v = low_clip_value;
493 }
494 };
495
496 #[cfg(feature = "parallel")]
497 let use_parallel = par_util::should_parallelize(n);
498 #[cfg(not(feature = "parallel"))]
499 let use_parallel = false;
500
501 if use_parallel {
502 #[cfg(feature = "parallel")]
503 par_util::thread_pool().install(|| {
504 values.par_iter_mut().enumerate().for_each(|(i, v)| {
505 apply_stages(i, v);
506 });
507 });
508 } else {
509 for (i, v) in values.iter_mut().enumerate() {
510 apply_stages(i, v);
511 }
512 }
513 }
514
515 if self.config.enable_filter {
517 let fc = &self.config.filter;
518
519 if let Some(ref f) = self.filter_state {
523 if f.len() != n {
524 self.filter_state = None;
525 }
526 }
527
528 let mut reset_filter = reset_requested;
529 if self.filter_state.is_none() {
530 self.filter_state = Some(values.clone());
532 reset_filter = true;
533 }
534 if self.num_filtered >= fc.num_filter && fc.auto_reset {
535 reset_filter = true;
536 }
537
538 let filter = self.filter_state.as_mut().unwrap();
539
540 if reset_filter {
541 let r_offset = fc.r_offset;
546 let rc1 = fc.rc[0];
547 let rc2 = fc.rc[1];
548 for i in 0..n {
549 let mut new_filter = accumulate(r_offset, rc1, filter[i]);
550 new_filter = accumulate(new_filter, rc2, values[i]);
551 filter[i] = new_filter;
552 }
553 self.num_filtered = 0;
554 }
555
556 if self.num_filtered < fc.num_filter {
558 self.num_filtered += 1;
559 }
560
561 let nf = self.num_filtered as f64;
563 let o1 = fc.o_scale * (fc.oc[0] + fc.oc[1] / nf);
564 let o2 = fc.o_scale * (fc.oc[2] + fc.oc[3] / nf);
565 let f1 = fc.f_scale * (fc.fc[0] + fc.fc[1] / nf);
566 let f2 = fc.f_scale * (fc.fc[2] + fc.fc[3] / nf);
567 let o_offset = fc.o_offset;
568 let f_offset = fc.f_offset;
569
570 for i in 0..n {
583 let mut new_data = accumulate(o_offset, o1, filter[i]);
584 new_data = accumulate(new_data, o2, values[i]);
585 let mut new_filter = accumulate(f_offset, f1, filter[i]);
586 new_filter = accumulate(new_filter, f2, values[i]);
587 values[i] = new_data;
588 filter[i] = new_filter;
589 }
590
591 if fc.filter_callbacks > 0 && self.num_filtered != fc.num_filter {
596 return None;
597 }
598 }
599
600 let out_type = self.config.output_type.unwrap_or(src.data.data_type());
602 let mut out_data = NDDataBuffer::zeros(out_type, n);
603 for i in 0..n {
604 out_data.set_from_f64(i, values[i]);
605 }
606
607 let mut arr = NDArray::new(src.dims.clone(), out_type);
608 arr.data = out_data;
609 arr.unique_id = src.unique_id;
610 arr.timestamp = src.timestamp;
611 arr.attributes = src.attributes.clone();
612
613 if auto_offset_scale_now {
618 self.auto_offset_scale(src);
619 }
620
621 self.last_output = Some(arr.clone());
626
627 Some(arr)
628 }
629}
630
631#[derive(Default)]
635struct ProcParamIndices {
636 data_type: Option<usize>,
637 save_background: Option<usize>,
638 enable_background: Option<usize>,
639 valid_background: Option<usize>,
640 save_flat_field: Option<usize>,
641 enable_flat_field: Option<usize>,
642 valid_flat_field: Option<usize>,
643 scale_flat_field: Option<usize>,
644 enable_offset_scale: Option<usize>,
645 auto_offset_scale: Option<usize>,
646 offset: Option<usize>,
647 scale: Option<usize>,
648 enable_low_clip: Option<usize>,
649 low_clip_thresh: Option<usize>,
650 low_clip_value: Option<usize>,
651 enable_high_clip: Option<usize>,
652 high_clip_thresh: Option<usize>,
653 high_clip_value: Option<usize>,
654 enable_filter: Option<usize>,
655 filter_type: Option<usize>,
656 reset_filter: Option<usize>,
657 auto_reset_filter: Option<usize>,
658 filter_callbacks: Option<usize>,
659 num_filter: Option<usize>,
660 num_filtered: Option<usize>,
661 o_offset: Option<usize>,
662 o_scale: Option<usize>,
663 oc: [Option<usize>; 4],
664 f_offset: Option<usize>,
665 f_scale: Option<usize>,
666 fc: [Option<usize>; 4],
667 r_offset: Option<usize>,
668 rc: [Option<usize>; 2],
669}
670
671pub struct ProcessProcessor {
673 state: ProcessState,
674 params: ProcParamIndices,
675}
676
677impl ProcessProcessor {
678 pub fn new(config: ProcessConfig) -> Self {
679 Self {
680 state: ProcessState::new(config),
681 params: ProcParamIndices::default(),
682 }
683 }
684
685 pub fn state(&self) -> &ProcessState {
686 &self.state
687 }
688
689 pub fn state_mut(&mut self) -> &mut ProcessState {
690 &mut self.state
691 }
692}
693
694impl NDPluginProcess for ProcessProcessor {
695 fn process_array(&mut self, array: &NDArray, _pool: &NDArrayPool) -> ProcessResult {
696 use ad_core_rs::plugin::runtime::ParamUpdate;
697
698 let out = self.state.process(array);
699 let mut result = match out {
702 Some(arr) => ProcessResult::arrays(vec![Arc::new(arr)]),
703 None => ProcessResult::sink(vec![]),
704 };
705
706 if let Some(idx) = self.params.valid_background {
708 result.param_updates.push(ParamUpdate::int32(
709 idx,
710 if self.state.config.valid_background {
711 1
712 } else {
713 0
714 },
715 ));
716 }
717 if let Some(idx) = self.params.valid_flat_field {
718 result.param_updates.push(ParamUpdate::int32(
719 idx,
720 if self.state.config.valid_flat_field {
721 1
722 } else {
723 0
724 },
725 ));
726 }
727 if let Some(idx) = self.params.num_filtered {
728 result
729 .param_updates
730 .push(ParamUpdate::int32(idx, self.state.num_filtered as i32));
731 }
732 if let Some(idx) = self.params.reset_filter {
739 result.param_updates.push(ParamUpdate::int32(idx, 0));
740 }
741
742 result
743 }
744
745 fn plugin_type(&self) -> &str {
746 "NDPluginProcess"
747 }
748
749 fn register_params(
750 &mut self,
751 base: &mut asyn_rs::port::PortDriverBase,
752 ) -> asyn_rs::error::AsynResult<()> {
753 use asyn_rs::param::ParamType;
754 base.create_param("PROCESS_DATA_TYPE", ParamType::Int32)?;
755 base.create_param("SAVE_BACKGROUND", ParamType::Int32)?;
756 base.create_param("ENABLE_BACKGROUND", ParamType::Int32)?;
757 base.create_param("VALID_BACKGROUND", ParamType::Int32)?;
758 base.create_param("SAVE_FLAT_FIELD", ParamType::Int32)?;
759 base.create_param("ENABLE_FLAT_FIELD", ParamType::Int32)?;
760 base.create_param("VALID_FLAT_FIELD", ParamType::Int32)?;
761 base.create_param("SCALE_FLAT_FIELD", ParamType::Float64)?;
762 base.create_param("ENABLE_OFFSET_SCALE", ParamType::Int32)?;
763 base.create_param("AUTO_OFFSET_SCALE", ParamType::Int32)?;
764 base.create_param("OFFSET", ParamType::Float64)?;
765 base.create_param("SCALE", ParamType::Float64)?;
766 base.create_param("ENABLE_LOW_CLIP", ParamType::Int32)?;
767 base.create_param("LOW_CLIP_THRESH", ParamType::Float64)?;
768 base.create_param("LOW_CLIP_VALUE", ParamType::Float64)?;
769 base.create_param("ENABLE_HIGH_CLIP", ParamType::Int32)?;
770 base.create_param("HIGH_CLIP_THRESH", ParamType::Float64)?;
771 base.create_param("HIGH_CLIP_VALUE", ParamType::Float64)?;
772 base.create_param("ENABLE_FILTER", ParamType::Int32)?;
773 base.create_param("FILTER_TYPE", ParamType::Int32)?;
774 base.create_param("RESET_FILTER", ParamType::Int32)?;
775 base.create_param("AUTO_RESET_FILTER", ParamType::Int32)?;
776 base.create_param("FILTER_CALLBACKS", ParamType::Int32)?;
777 base.create_param("NUM_FILTER", ParamType::Int32)?;
778 base.create_param("NUM_FILTERED", ParamType::Int32)?;
779 base.create_param("FILTER_OOFFSET", ParamType::Float64)?;
780 base.create_param("FILTER_OSCALE", ParamType::Float64)?;
781 base.create_param("FILTER_OC1", ParamType::Float64)?;
782 base.create_param("FILTER_OC2", ParamType::Float64)?;
783 base.create_param("FILTER_OC3", ParamType::Float64)?;
784 base.create_param("FILTER_OC4", ParamType::Float64)?;
785 base.create_param("FILTER_FOFFSET", ParamType::Float64)?;
786 base.create_param("FILTER_FSCALE", ParamType::Float64)?;
787 base.create_param("FILTER_FC1", ParamType::Float64)?;
788 base.create_param("FILTER_FC2", ParamType::Float64)?;
789 base.create_param("FILTER_FC3", ParamType::Float64)?;
790 base.create_param("FILTER_FC4", ParamType::Float64)?;
791 base.create_param("FILTER_ROFFSET", ParamType::Float64)?;
792 base.create_param("FILTER_RC1", ParamType::Float64)?;
793 base.create_param("FILTER_RC2", ParamType::Float64)?;
794
795 self.params.data_type = base.find_param("PROCESS_DATA_TYPE");
797 self.params.save_background = base.find_param("SAVE_BACKGROUND");
798 self.params.enable_background = base.find_param("ENABLE_BACKGROUND");
799 self.params.valid_background = base.find_param("VALID_BACKGROUND");
800 self.params.save_flat_field = base.find_param("SAVE_FLAT_FIELD");
801 self.params.enable_flat_field = base.find_param("ENABLE_FLAT_FIELD");
802 self.params.valid_flat_field = base.find_param("VALID_FLAT_FIELD");
803 self.params.scale_flat_field = base.find_param("SCALE_FLAT_FIELD");
804 self.params.enable_offset_scale = base.find_param("ENABLE_OFFSET_SCALE");
805 self.params.auto_offset_scale = base.find_param("AUTO_OFFSET_SCALE");
806 self.params.offset = base.find_param("OFFSET");
807 self.params.scale = base.find_param("SCALE");
808 self.params.enable_low_clip = base.find_param("ENABLE_LOW_CLIP");
809 self.params.low_clip_thresh = base.find_param("LOW_CLIP_THRESH");
810 self.params.low_clip_value = base.find_param("LOW_CLIP_VALUE");
811 self.params.enable_high_clip = base.find_param("ENABLE_HIGH_CLIP");
812 self.params.high_clip_thresh = base.find_param("HIGH_CLIP_THRESH");
813 self.params.high_clip_value = base.find_param("HIGH_CLIP_VALUE");
814 self.params.enable_filter = base.find_param("ENABLE_FILTER");
815 self.params.filter_type = base.find_param("FILTER_TYPE");
816 self.params.reset_filter = base.find_param("RESET_FILTER");
817 self.params.auto_reset_filter = base.find_param("AUTO_RESET_FILTER");
818 self.params.filter_callbacks = base.find_param("FILTER_CALLBACKS");
819 self.params.num_filter = base.find_param("NUM_FILTER");
820 self.params.num_filtered = base.find_param("NUM_FILTERED");
821 self.params.o_offset = base.find_param("FILTER_OOFFSET");
822 self.params.o_scale = base.find_param("FILTER_OSCALE");
823 self.params.oc[0] = base.find_param("FILTER_OC1");
824 self.params.oc[1] = base.find_param("FILTER_OC2");
825 self.params.oc[2] = base.find_param("FILTER_OC3");
826 self.params.oc[3] = base.find_param("FILTER_OC4");
827 self.params.f_offset = base.find_param("FILTER_FOFFSET");
828 self.params.f_scale = base.find_param("FILTER_FSCALE");
829 self.params.fc[0] = base.find_param("FILTER_FC1");
830 self.params.fc[1] = base.find_param("FILTER_FC2");
831 self.params.fc[2] = base.find_param("FILTER_FC3");
832 self.params.fc[3] = base.find_param("FILTER_FC4");
833 self.params.r_offset = base.find_param("FILTER_ROFFSET");
834 self.params.rc[0] = base.find_param("FILTER_RC1");
835 self.params.rc[1] = base.find_param("FILTER_RC2");
836 Ok(())
837 }
838
839 fn on_param_change(
840 &mut self,
841 reason: usize,
842 params: &ad_core_rs::plugin::runtime::PluginParamSnapshot,
843 ) -> ad_core_rs::plugin::runtime::ParamChangeResult {
844 use ad_core_rs::plugin::runtime::{ParamChangeResult, ParamUpdate};
845
846 let s = &mut self.state;
847 let p = &self.params;
848 let mut updates = Vec::new();
849
850 if Some(reason) == p.data_type {
851 let v = params.value.as_i32();
852 s.config.output_type = if v < 0 {
853 None } else {
855 NDDataType::from_ordinal(v as u8)
856 };
857 } else if Some(reason) == p.save_background {
858 s.save_background();
863 updates.push(ParamUpdate::int32(reason, 0));
864 if let Some(idx) = p.valid_background {
865 updates.push(ParamUpdate::int32(idx, s.config.valid_background as i32));
866 }
867 } else if Some(reason) == p.enable_background {
868 s.config.enable_background = params.value.as_i32() != 0;
869 } else if Some(reason) == p.save_flat_field {
870 s.save_flat_field();
872 updates.push(ParamUpdate::int32(reason, 0));
873 if let Some(idx) = p.valid_flat_field {
874 updates.push(ParamUpdate::int32(idx, s.config.valid_flat_field as i32));
875 }
876 } else if Some(reason) == p.enable_flat_field {
877 s.config.enable_flat_field = params.value.as_i32() != 0;
878 } else if Some(reason) == p.scale_flat_field {
879 s.config.scale_flat_field = params.value.as_f64();
880 } else if Some(reason) == p.enable_offset_scale {
881 s.config.enable_offset_scale = params.value.as_i32() != 0;
882 } else if Some(reason) == p.auto_offset_scale {
883 if params.value.as_i32() != 0 {
884 s.config.auto_offset_scale_pending = true;
889 if let Some(idx) = p.auto_offset_scale {
890 updates.push(ParamUpdate::int32(idx, 0));
891 }
892 }
893 } else if Some(reason) == p.offset {
894 s.config.offset = params.value.as_f64();
895 } else if Some(reason) == p.scale {
896 s.config.scale = params.value.as_f64();
897 } else if Some(reason) == p.enable_low_clip {
898 s.config.enable_low_clip = params.value.as_i32() != 0;
899 } else if Some(reason) == p.low_clip_thresh {
900 s.config.low_clip_thresh = params.value.as_f64();
901 } else if Some(reason) == p.low_clip_value {
902 s.config.low_clip_value = params.value.as_f64();
903 } else if Some(reason) == p.enable_high_clip {
904 s.config.enable_high_clip = params.value.as_i32() != 0;
905 } else if Some(reason) == p.high_clip_thresh {
906 s.config.high_clip_thresh = params.value.as_f64();
907 } else if Some(reason) == p.high_clip_value {
908 s.config.high_clip_value = params.value.as_f64();
909 } else if Some(reason) == p.enable_filter {
910 s.config.enable_filter = params.value.as_i32() != 0;
911 } else if Some(reason) == p.filter_type {
912 s.apply_filter_type(params.value.as_i32());
917 let fc = &s.config.filter;
919 for (i, idx) in p.fc.iter().enumerate() {
920 if let Some(idx) = *idx {
921 updates.push(ParamUpdate::float64(idx, fc.fc[i]));
922 }
923 }
924 for (i, idx) in p.oc.iter().enumerate() {
925 if let Some(idx) = *idx {
926 updates.push(ParamUpdate::float64(idx, fc.oc[i]));
927 }
928 }
929 for (i, idx) in p.rc.iter().enumerate() {
930 if let Some(idx) = *idx {
931 updates.push(ParamUpdate::float64(idx, fc.rc[i]));
932 }
933 }
934 if let Some(idx) = p.f_offset {
935 updates.push(ParamUpdate::float64(idx, fc.f_offset));
936 }
937 if let Some(idx) = p.f_scale {
938 updates.push(ParamUpdate::float64(idx, fc.f_scale));
939 }
940 if let Some(idx) = p.o_offset {
941 updates.push(ParamUpdate::float64(idx, fc.o_offset));
942 }
943 if let Some(idx) = p.o_scale {
944 updates.push(ParamUpdate::float64(idx, fc.o_scale));
945 }
946 } else if Some(reason) == p.reset_filter {
947 if params.value.as_i32() != 0 {
948 s.reset_filter();
952 }
953 } else if Some(reason) == p.auto_reset_filter {
954 s.config.filter.auto_reset = params.value.as_i32() != 0;
955 } else if Some(reason) == p.filter_callbacks {
956 s.config.filter.filter_callbacks = params.value.as_i32().max(0) as usize;
957 } else if Some(reason) == p.num_filter {
958 s.config.filter.num_filter = params.value.as_i32().max(1) as usize;
959 } else if Some(reason) == p.o_offset {
960 s.config.filter.o_offset = params.value.as_f64();
961 } else if Some(reason) == p.o_scale {
962 s.config.filter.o_scale = params.value.as_f64();
963 } else if Some(reason) == p.f_offset {
964 s.config.filter.f_offset = params.value.as_f64();
965 } else if Some(reason) == p.f_scale {
966 s.config.filter.f_scale = params.value.as_f64();
967 } else if Some(reason) == p.r_offset {
968 s.config.filter.r_offset = params.value.as_f64();
969 } else {
970 for i in 0..4 {
972 if Some(reason) == p.oc[i] {
973 s.config.filter.oc[i] = params.value.as_f64();
974 return ParamChangeResult::updates(vec![]);
975 }
976 if Some(reason) == p.fc[i] {
977 s.config.filter.fc[i] = params.value.as_f64();
978 return ParamChangeResult::updates(vec![]);
979 }
980 }
981 for i in 0..2 {
982 if Some(reason) == p.rc[i] {
983 s.config.filter.rc[i] = params.value.as_f64();
984 return ParamChangeResult::updates(vec![]);
985 }
986 }
987 }
988
989 ParamChangeResult::updates(updates)
990 }
991}
992
993#[cfg(test)]
994mod tests {
995 use super::*;
996 use ad_core_rs::ndarray::{NDDataBuffer, NDDimension};
997
998 fn make_array(vals: &[u8]) -> NDArray {
999 let mut arr = NDArray::new(vec![NDDimension::new(vals.len())], NDDataType::UInt8);
1000 if let NDDataBuffer::U8(ref mut v) = arr.data {
1001 v.copy_from_slice(vals);
1002 }
1003 arr
1004 }
1005
1006 fn seed_background(state: &mut ProcessState, arr: &NDArray) {
1009 state.last_output = Some(arr.clone());
1010 state.save_background();
1011 }
1012
1013 fn seed_flat_field(state: &mut ProcessState, arr: &NDArray) {
1015 state.last_output = Some(arr.clone());
1016 state.save_flat_field();
1017 }
1018
1019 fn make_f64_array(vals: &[f64]) -> NDArray {
1020 let mut arr = NDArray::new(vec![NDDimension::new(vals.len())], NDDataType::Float64);
1021 if let NDDataBuffer::F64(ref mut v) = arr.data {
1022 v.copy_from_slice(vals);
1023 }
1024 arr
1025 }
1026
1027 #[test]
1028 fn test_background_subtraction() {
1029 let bg_arr = make_array(&[10, 20, 30]);
1030 let input = make_array(&[15, 25, 35]);
1031
1032 let mut state = ProcessState::new(ProcessConfig {
1033 enable_background: true,
1034 ..Default::default()
1035 });
1036 seed_background(&mut state, &bg_arr);
1037
1038 let result = state.process(&input).unwrap();
1039 if let NDDataBuffer::U8(ref v) = result.data {
1040 assert_eq!(v[0], 5);
1041 assert_eq!(v[1], 5);
1042 assert_eq!(v[2], 5);
1043 }
1044 }
1045
1046 #[test]
1047 fn test_adp7_size_mismatched_background_invalidated_not_partial() {
1048 let bg_arr = make_array(&[10, 20]); let input = make_array(&[15, 25, 35]); let mut state = ProcessState::new(ProcessConfig {
1055 enable_background: true,
1056 ..Default::default()
1057 });
1058 seed_background(&mut state, &bg_arr);
1059 assert!(state.config.valid_background); let result = state.process(&input).unwrap();
1062 assert!(!state.config.valid_background);
1065 if let NDDataBuffer::U8(ref v) = result.data {
1066 assert_eq!(v, &[15, 25, 35]);
1067 } else {
1068 panic!("expected U8 output");
1069 }
1070 }
1071
1072 #[test]
1073 fn test_flat_field() {
1074 let ff_arr = make_array(&[100, 200, 50]);
1077 let input = make_array(&[100, 100, 100]);
1078
1079 let mut state = ProcessState::new(ProcessConfig {
1080 enable_flat_field: true,
1081 scale_flat_field: 100.0,
1082 ..Default::default()
1083 });
1084 seed_flat_field(&mut state, &ff_arr);
1085
1086 let result = state.process(&input).unwrap();
1087 if let NDDataBuffer::U8(ref v) = result.data {
1088 assert_eq!(v[0], 100); assert_eq!(v[1], 50); assert_eq!(v[2], 200); } else {
1092 panic!("expected U8 output");
1093 }
1094 }
1095
1096 #[test]
1097 fn test_adp24_scale_flat_field_zero_zeroes_output() {
1098 let ff_arr = make_array(&[100, 200, 50]);
1102 let input = make_array(&[100, 100, 100]);
1103 let mut state = ProcessState::new(ProcessConfig {
1104 enable_flat_field: true,
1105 scale_flat_field: 0.0,
1106 ..Default::default()
1107 });
1108 seed_flat_field(&mut state, &ff_arr);
1109 let result = state.process(&input).unwrap();
1110 if let NDDataBuffer::U8(ref v) = result.data {
1111 assert_eq!(v, &[0, 0, 0]);
1112 } else {
1113 panic!("expected U8 output");
1114 }
1115 }
1116
1117 #[test]
1118 fn test_offset_scale() {
1119 let input = make_array(&[10, 20, 30]);
1120 let mut state = ProcessState::new(ProcessConfig {
1121 enable_offset_scale: true,
1122 scale: 2.0,
1123 offset: 5.0,
1124 ..Default::default()
1125 });
1126
1127 let result = state.process(&input).unwrap();
1128 if let NDDataBuffer::U8(ref v) = result.data {
1129 assert_eq!(v[0], 30); assert_eq!(v[1], 50); assert_eq!(v[2], 70); }
1134 }
1135
1136 #[test]
1137 fn test_clipping() {
1138 let input = make_array(&[5, 50, 200]);
1139 let mut state = ProcessState::new(ProcessConfig {
1140 enable_low_clip: true,
1141 low_clip_thresh: 10.0,
1142 low_clip_value: 10.0,
1143 enable_high_clip: true,
1144 high_clip_thresh: 100.0,
1145 high_clip_value: 100.0,
1146 ..Default::default()
1147 });
1148
1149 let result = state.process(&input).unwrap();
1150 if let NDDataBuffer::U8(ref v) = result.data {
1151 assert_eq!(v[0], 10); assert_eq!(v[1], 50); assert_eq!(v[2], 100); }
1155 }
1156
1157 #[test]
1158 fn test_adp5_clip_order_high_before_low() {
1159 let input = make_f64_array(&[200.0]);
1164 let mut state = ProcessState::new(ProcessConfig {
1165 enable_high_clip: true,
1166 high_clip_thresh: 100.0,
1167 high_clip_value: 10.0,
1168 enable_low_clip: true,
1169 low_clip_thresh: 50.0,
1170 low_clip_value: 999.0,
1171 ..Default::default()
1172 });
1173 let result = state.process(&input).unwrap();
1174 if let NDDataBuffer::F64(ref v) = result.data {
1175 assert_eq!(v[0], 999.0);
1176 } else {
1177 panic!("expected F64 output");
1178 }
1179 }
1180
1181 #[test]
1182 fn test_recursive_filter() {
1183 let input1 = make_array(&[100, 100, 100]);
1189 let input2 = make_array(&[0, 0, 0]);
1190
1191 let mut state = ProcessState::new(ProcessConfig {
1192 enable_filter: true,
1193 filter: FilterConfig {
1194 num_filter: 10,
1195 fc: [0.5, 0.0, 0.5, 0.0], oc: [1.0, 0.0, 0.0, 0.0], rc: [0.0, 1.0], ..Default::default()
1199 },
1200 ..Default::default()
1201 });
1202
1203 let _ = state.process(&input1);
1214
1215 let result = state.process(&input2).unwrap();
1220 if let NDDataBuffer::U8(ref v) = result.data {
1221 assert_eq!(v[0], 100);
1223 assert_eq!(v[1], 100);
1224 }
1225 }
1226
1227 #[test]
1228 fn test_output_type_conversion() {
1229 let input = make_array(&[10, 20, 30]);
1230 let mut state = ProcessState::new(ProcessConfig {
1231 output_type: Some(NDDataType::Float64),
1232 ..Default::default()
1233 });
1234
1235 let result = state.process(&input).unwrap();
1236 assert_eq!(result.data.data_type(), NDDataType::Float64);
1237 }
1238
1239 #[test]
1242 fn test_process_processor() {
1243 let mut proc = ProcessProcessor::new(ProcessConfig {
1244 enable_offset_scale: true,
1245 scale: 2.0,
1246 offset: 1.0,
1247 ..Default::default()
1248 });
1249 let pool = NDArrayPool::new(1_000_000);
1250
1251 let input = make_array(&[10, 20, 30]);
1252 let result = proc.process_array(&input, &pool);
1253 assert_eq!(result.output_arrays.len(), 1);
1254 if let NDDataBuffer::U8(ref v) = result.output_arrays[0].data {
1255 assert_eq!(v[0], 22); }
1257 }
1258
1259 #[test]
1262 fn test_filter_sum_preset() {
1263 let mut state = ProcessState::new(ProcessConfig {
1266 enable_filter: true,
1267 filter: FilterConfig {
1268 num_filter: 10,
1269 fc: [1.0, 0.0, 1.0, 0.0],
1270 oc: [1.0, 0.0, 0.0, 0.0],
1271 rc: [0.0, 1.0],
1272 ..Default::default()
1273 },
1274 output_type: Some(NDDataType::Float64),
1275 ..Default::default()
1276 });
1277
1278 let r0 = state.process(&make_f64_array(&[100.0])).unwrap();
1290 let v0 = r0.data.get_as_f64(0).unwrap();
1291 assert!((v0 - 100.0).abs() < 1e-9, "frame 0: got {v0}");
1292
1293 let r1 = state.process(&make_f64_array(&[100.0])).unwrap();
1300 let v1 = r1.data.get_as_f64(0).unwrap();
1301 assert!((v1 - 200.0).abs() < 1e-9, "frame 1: got {v1}");
1302 }
1303
1304 #[test]
1305 fn test_filter_average_preset() {
1306 let mut state = ProcessState::new(ProcessConfig {
1309 enable_filter: true,
1310 filter: FilterConfig {
1311 num_filter: 10,
1312 fc: [1.0, 0.0, 1.0, 0.0],
1313 oc: [0.0, 1.0, 0.0, 0.0],
1314 rc: [0.0, 1.0],
1315 ..Default::default()
1316 },
1317 output_type: Some(NDDataType::Float64),
1318 ..Default::default()
1319 });
1320
1321 let r0 = state.process(&make_f64_array(&[100.0])).unwrap();
1330 let v0 = r0.data.get_as_f64(0).unwrap();
1331 assert!((v0 - 100.0).abs() < 1e-9, "frame 0: got {v0}");
1332
1333 let r1 = state.process(&make_f64_array(&[200.0])).unwrap();
1338 let v1 = r1.data.get_as_f64(0).unwrap();
1339 assert!((v1 - 100.0).abs() < 1e-9, "frame 1: got {v1}");
1340
1341 let r2 = state.process(&make_f64_array(&[300.0])).unwrap();
1346 let v2 = r2.data.get_as_f64(0).unwrap();
1347 let expected = 400.0 / 3.0;
1348 assert!((v2 - expected).abs() < 1e-9, "frame 2: got {v2}");
1349 }
1350
1351 #[test]
1352 fn test_filter_recursive_ave() {
1353 let mut state = ProcessState::new(ProcessConfig {
1358 enable_filter: true,
1359 filter: FilterConfig {
1360 num_filter: 10,
1361 fc: [1.0, -1.0, 0.0, 1.0],
1362 oc: [1.0, 0.0, 0.0, 0.0],
1363 rc: [0.0, 1.0],
1364 ..Default::default()
1365 },
1366 output_type: Some(NDDataType::Float64),
1367 ..Default::default()
1368 });
1369
1370 let r0 = state.process(&make_f64_array(&[100.0])).unwrap();
1382 let v0 = r0.data.get_as_f64(0).unwrap();
1383 assert!((v0 - 100.0).abs() < 1e-9, "frame 0: got {v0}");
1384
1385 let r1 = state.process(&make_f64_array(&[200.0])).unwrap();
1390 let v1 = r1.data.get_as_f64(0).unwrap();
1391 assert!((v1 - 100.0).abs() < 1e-9, "frame 1: got {v1}");
1392
1393 let r2 = state.process(&make_f64_array(&[300.0])).unwrap();
1398 let v2 = r2.data.get_as_f64(0).unwrap();
1399 assert!((v2 - 150.0).abs() < 1e-9, "frame 2: got {v2}");
1400 }
1401
1402 #[test]
1403 fn test_r9_68_save_background_copies_the_last_output_synchronously() {
1404 let mut state = ProcessState::new(ProcessConfig {
1413 enable_offset_scale: true,
1414 offset: 0.0,
1415 scale: 2.0,
1416 output_type: Some(NDDataType::Float64),
1417 ..Default::default()
1418 });
1419
1420 state.save_background();
1424 assert!(state.background.is_none());
1425 assert!(!state.config.valid_background);
1426
1427 let out = state.process(&make_array(&[10, 20, 30])).unwrap();
1429 assert_eq!(out.data.get_as_f64(0), Some(20.0));
1430
1431 state.save_background();
1434 assert!(
1435 state.config.valid_background,
1436 "ValidBackground latches at once"
1437 );
1438 let bg = state.background.as_ref().unwrap();
1439 assert_eq!(
1440 bg.as_slice(),
1441 &[20.0, 40.0, 60.0],
1442 "background is the OUTPUT array"
1443 );
1444
1445 let _ = state.process(&make_array(&[1, 2, 3]));
1447 assert_eq!(
1448 state.background.as_ref().unwrap().as_slice(),
1449 &[20.0, 40.0, 60.0]
1450 );
1451 }
1452
1453 #[test]
1454 fn test_r9_68_save_flat_field_copies_the_last_output_synchronously() {
1455 let mut state = ProcessState::new(ProcessConfig {
1457 enable_offset_scale: true,
1458 offset: 1.0,
1459 scale: 1.0,
1460 output_type: Some(NDDataType::Float64),
1461 ..Default::default()
1462 });
1463
1464 state.save_flat_field();
1465 assert!(state.flat_field.is_none());
1466 assert!(!state.config.valid_flat_field);
1467
1468 let _ = state.process(&make_array(&[50, 100, 150])).unwrap();
1470 state.save_flat_field();
1471
1472 assert!(state.config.valid_flat_field);
1473 assert_eq!(
1474 state.flat_field.as_ref().unwrap().as_slice(),
1475 &[51.0, 101.0, 151.0],
1476 "flat field is the OUTPUT array, not the input"
1477 );
1478
1479 let _ = state.process(&make_array(&[7, 7, 7]));
1480 assert_eq!(
1481 state.flat_field.as_ref().unwrap().as_slice(),
1482 &[51.0, 101.0, 151.0]
1483 );
1484 }
1485
1486 #[test]
1487 fn test_r9_68_save_background_write_of_zero_still_saves() {
1488 use ad_core_rs::plugin::runtime::{ParamChangeValue, ParamUpdate, PluginParamSnapshot};
1492 use asyn_rs::port::{PortDriverBase, PortFlags};
1493
1494 let mut proc = ProcessProcessor::new(ProcessConfig {
1495 output_type: Some(NDDataType::Float64),
1496 ..Default::default()
1497 });
1498
1499 let mut base = PortDriverBase::new("R9_68", 1, PortFlags::default());
1500 proc.register_params(&mut base).unwrap();
1501 let pool = NDArrayPool::new(1_000_000);
1502 let _ = proc.process_array(&make_array(&[4, 5, 6]), &pool);
1503
1504 let reason = proc.params.save_background.unwrap();
1505 let valid = proc.params.valid_background.unwrap();
1506 let snapshot = PluginParamSnapshot {
1507 enable_callbacks: true,
1508 reason,
1509 addr: 0,
1510 value: ParamChangeValue::Int32(0),
1511 };
1512 let result = proc.on_param_change(reason, &snapshot);
1513
1514 assert_eq!(
1515 proc.state.background.as_ref().unwrap().as_slice(),
1516 &[4.0, 5.0, 6.0],
1517 "a 0 write saves the background too"
1518 );
1519 let int_update = |r: usize| {
1521 result.param_updates.iter().find_map(|u| match u {
1522 ParamUpdate::Int32 {
1523 reason: ur, value, ..
1524 } if *ur == r => Some(*value),
1525 _ => None,
1526 })
1527 };
1528 assert_eq!(int_update(reason), Some(0), "SaveBackground echoes 0");
1529 assert_eq!(
1530 int_update(valid),
1531 Some(1),
1532 "ValidBackground latches on the write"
1533 );
1534 }
1535
1536 #[test]
1537 fn test_auto_reset_when_num_filter_reached() {
1538 let mut state = ProcessState::new(ProcessConfig {
1540 enable_filter: true,
1541 filter: FilterConfig {
1542 num_filter: 3,
1543 auto_reset: true,
1544 fc: [1.0, 0.0, 1.0, 0.0], oc: [1.0, 0.0, 0.0, 0.0],
1546 rc: [0.0, 1.0],
1547 ..Default::default()
1548 },
1549 output_type: Some(NDDataType::Float64),
1550 ..Default::default()
1551 });
1552
1553 let _ = state.process(&make_f64_array(&[100.0]));
1555 assert_eq!(state.num_filtered, 1);
1556
1557 let _ = state.process(&make_f64_array(&[100.0]));
1559 assert_eq!(state.num_filtered, 2);
1560
1561 let _ = state.process(&make_f64_array(&[100.0]));
1563 assert_eq!(state.num_filtered, 3);
1564
1565 let _ = state.process(&make_f64_array(&[200.0]));
1567 assert_eq!(state.num_filtered, 1, "fresh start after auto reset");
1569 }
1570
1571 #[test]
1572 fn test_filter_with_offset_scale() {
1573 let mut state = ProcessState::new(ProcessConfig {
1577 enable_filter: true,
1578 filter: FilterConfig {
1579 num_filter: 10,
1580 fc: [0.0, 0.0, 1.0, 0.0], oc: [1.0, 0.0, 0.0, 0.0], rc: [0.0, 1.0],
1583 f_offset: 10.0,
1584 f_scale: 2.0,
1585 o_offset: 5.0,
1586 o_scale: 3.0,
1587 ..Default::default()
1588 },
1589 output_type: Some(NDDataType::Float64),
1590 ..Default::default()
1591 });
1592
1593 let r0 = state.process(&make_f64_array(&[50.0])).unwrap();
1603 let v0 = r0.data.get_as_f64(0).unwrap();
1604 assert!((v0 - 155.0).abs() < 1e-9, "frame 0: got {v0}");
1605
1606 let r1 = state.process(&make_f64_array(&[20.0])).unwrap();
1611 let v1 = r1.data.get_as_f64(0).unwrap();
1612 assert!((v1 - 335.0).abs() < 1e-9, "frame 1: got {v1}");
1613 }
1614
1615 #[test]
1616 fn test_reset_filter_manual() {
1617 let mut state = ProcessState::new(ProcessConfig {
1618 enable_filter: true,
1619 filter: FilterConfig {
1620 num_filter: 10,
1621 fc: [1.0, 0.0, 1.0, 0.0],
1622 oc: [1.0, 0.0, 0.0, 0.0],
1623 rc: [0.0, 1.0],
1624 ..Default::default()
1625 },
1626 output_type: Some(NDDataType::Float64),
1627 ..Default::default()
1628 });
1629
1630 let _ = state.process(&make_f64_array(&[100.0]));
1632 let _ = state.process(&make_f64_array(&[100.0]));
1633 assert!(state.filter_state.is_some());
1634 assert_eq!(state.num_filtered, 2);
1635
1636 state.reset_filter();
1640 assert!(
1641 state.filter_state.is_some(),
1642 "buffer must survive the reset"
1643 );
1644 assert_eq!(state.num_filtered, 2);
1645
1646 let _ = state.process(&make_f64_array(&[200.0]));
1648 assert_eq!(state.num_filtered, 1);
1649 }
1650
1651 #[test]
1652 fn test_r6_69_manual_reset_keeps_previous_filter_contents() {
1653 let cfg = || ProcessConfig {
1663 enable_filter: true,
1664 filter: FilterConfig {
1665 num_filter: 10,
1666 fc: [0.0, 0.0, 1.0, 0.0],
1667 oc: [1.0, 0.0, 0.0, 0.0],
1668 rc: [0.5, 2.0], r_offset: 1.0,
1670 ..Default::default()
1671 },
1672 output_type: Some(NDDataType::Float64),
1673 ..Default::default()
1674 };
1675
1676 let mut state = ProcessState::new(cfg());
1677 let _ = state.process(&make_f64_array(&[100.0]));
1680 assert_eq!(state.filter_state.as_ref().unwrap()[0], 100.0);
1681
1682 state.reset_filter();
1684 let out = state.process(&make_f64_array(&[10.0])).unwrap();
1685
1686 assert_eq!(out.data.get_as_f64(0).unwrap(), 71.0);
1690 assert_eq!(state.num_filtered, 1);
1691 }
1694
1695 #[test]
1696 fn test_r6_69_element_count_mismatch_frees_the_buffer() {
1697 let mut state = ProcessState::new(ProcessConfig {
1700 enable_filter: true,
1701 filter: FilterConfig {
1702 num_filter: 10,
1703 fc: [0.0, 0.0, 1.0, 0.0],
1704 oc: [1.0, 0.0, 0.0, 0.0],
1705 rc: [0.5, 2.0],
1706 r_offset: 1.0,
1707 ..Default::default()
1708 },
1709 output_type: Some(NDDataType::Float64),
1710 ..Default::default()
1711 });
1712
1713 let _ = state.process(&make_f64_array(&[100.0]));
1714 assert_eq!(state.filter_state.as_ref().unwrap().len(), 1);
1715
1716 let out = state.process(&make_f64_array(&[10.0, 10.0])).unwrap();
1720 assert_eq!(state.filter_state.as_ref().unwrap().len(), 2);
1721 assert_eq!(out.data.get_as_f64(0).unwrap(), 26.0);
1722 assert_eq!(state.num_filtered, 1);
1723 }
1724
1725 #[test]
1726 fn test_adp6_auto_offset_scale_arms_next_frame_not_trigger() {
1727 let mut state = ProcessState::new(ProcessConfig {
1732 output_type: Some(NDDataType::UInt8),
1733 ..Default::default()
1734 });
1735 state.config.auto_offset_scale_pending = true;
1736
1737 let out1 = state.process(&make_f64_array(&[10.0, 20.0, 30.0])).unwrap();
1740 assert!(!state.config.auto_offset_scale_pending); if let NDDataBuffer::U8(v) = &out1.data {
1742 assert_eq!(v, &[10, 20, 30]); } else {
1744 panic!("expected u8 output");
1745 }
1746 assert!(state.config.enable_offset_scale);
1749 assert!((state.config.offset - (-10.0)).abs() < 1e-9);
1750 assert!((state.config.scale - 255.0 / 20.0).abs() < 1e-9);
1751
1752 let out2 = state.process(&make_f64_array(&[10.0, 20.0, 30.0])).unwrap();
1754 if let NDDataBuffer::U8(v) = &out2.data {
1755 assert_eq!(v[0], 0); assert_eq!(v[2], 255); } else {
1758 panic!("expected u8 output");
1759 }
1760 }
1761
1762 #[test]
1763 fn test_filter_callbacks_drops_suppressed_frame() {
1764 let mut state = ProcessState::new(ProcessConfig {
1768 enable_filter: true,
1769 filter: FilterConfig {
1770 num_filter: 3,
1771 filter_callbacks: 1,
1772 fc: [1.0, 0.0, 1.0, 0.0],
1773 oc: [0.0, 1.0, 0.0, 0.0],
1774 rc: [0.0, 1.0],
1775 ..Default::default()
1776 },
1777 output_type: Some(NDDataType::Float64),
1778 ..Default::default()
1779 });
1780
1781 assert!(state.process(&make_f64_array(&[100.0])).is_none());
1783 assert!(state.process(&make_f64_array(&[100.0])).is_none());
1784 assert!(state.process(&make_f64_array(&[100.0])).is_some());
1786 }
1787
1788 #[test]
1789 fn test_filter_recurrence_matches_cpp() {
1790 let mut state = ProcessState::new(ProcessConfig {
1818 enable_filter: true,
1819 filter: FilterConfig {
1820 num_filter: 100,
1821 fc: [1.0, 0.0, 1.0, 0.0],
1822 oc: [0.0, 1.0, 0.0, 0.0],
1823 rc: [0.0, 1.0],
1824 ..Default::default()
1825 },
1826 output_type: Some(NDDataType::Float64),
1827 ..Default::default()
1828 });
1829
1830 let inputs = [100.0, 200.0, 300.0, 400.0];
1831 let expected_data = [100.0, 100.0, 400.0 / 3.0, 175.0];
1832 let expected_filter = [200.0, 400.0, 700.0, 1100.0];
1833
1834 for k in 0..inputs.len() {
1835 let r = state.process(&make_f64_array(&[inputs[k]])).unwrap();
1836 let v = r.data.get_as_f64(0).unwrap();
1837 assert!(
1838 (v - expected_data[k]).abs() < 1e-9,
1839 "frame {k}: data got {v}, expected {}",
1840 expected_data[k]
1841 );
1842 let fs = state.filter_state.as_ref().unwrap()[0];
1843 assert!(
1844 (fs - expected_filter[k]).abs() < 1e-9,
1845 "frame {k}: filter got {fs}, expected {}",
1846 expected_filter[k]
1847 );
1848 }
1849 }
1850 #[test]
1862 fn r12_63_a_zero_coefficient_drops_its_term_instead_of_multiplying_it() {
1863 let input = make_f64_array(&[1.0, f64::NAN, 3.0]);
1864
1865 let mut state = ProcessState::new(ProcessConfig {
1866 enable_filter: true,
1867 filter: FilterConfig {
1868 num_filter: 2,
1869 rc: [0.0, 0.0],
1870 r_offset: 5.0,
1871 oc: [1.0, 0.0, 0.0, 0.0],
1872 fc: [1.0, 0.0, 0.0, 0.0],
1873 ..Default::default()
1874 },
1875 ..Default::default()
1876 });
1877
1878 let result = state.process(&input).unwrap();
1879 let NDDataBuffer::F64(ref v) = result.data else {
1880 panic!("expected an F64 output buffer, got {:?}", result.data);
1881 };
1882 assert_eq!(
1883 v.as_slice(),
1884 [5.0, 5.0, 5.0],
1885 "RC1=RC2=0 makes C's reset `filter[i] = rOffset`; O2=0 drops the NaN \
1886 data term. Every element is rOffset — 0.0 * NaN must not be summed in"
1887 );
1888
1889 let clean = make_f64_array(&[7.0, 8.0, 9.0]);
1892 let result = state.process(&clean).unwrap();
1893 let NDDataBuffer::F64(ref v) = result.data else {
1894 panic!("expected an F64 output buffer");
1895 };
1896 assert!(
1897 v.iter().all(|x| x.is_finite()),
1898 "the NaN must not survive in filter[] across frames: {v:?}"
1899 );
1900 }
1901}