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)]
33pub struct FilterConfig {
34 pub num_filter: usize,
36 pub auto_reset: bool,
38 pub filter_callbacks: usize,
40 pub oc: [f64; 4],
42 pub fc: [f64; 4],
44 pub rc: [f64; 2],
46 pub r_offset: f64,
48 pub o_offset: f64,
50 pub o_scale: f64,
52 pub f_offset: f64,
54 pub f_scale: f64,
56}
57
58impl Default for FilterConfig {
59 fn default() -> Self {
60 Self {
61 num_filter: 1,
62 auto_reset: false,
63 filter_callbacks: 0,
64 oc: [1.0, 0.0, 0.0, 0.0], fc: [1.0, 0.0, 0.0, 0.0],
66 rc: [1.0, 0.0],
67 r_offset: 0.0,
68 o_offset: 0.0,
69 o_scale: 1.0,
70 f_offset: 0.0,
71 f_scale: 1.0,
72 }
73 }
74}
75
76#[derive(Debug, Clone)]
78pub struct ProcessConfig {
79 pub enable_background: bool,
80 pub enable_flat_field: bool,
81 pub enable_offset_scale: bool,
82 pub offset: f64,
83 pub scale: f64,
84 pub enable_low_clip: bool,
85 pub low_clip_thresh: f64,
86 pub low_clip_value: f64,
87 pub enable_high_clip: bool,
88 pub high_clip_thresh: f64,
89 pub high_clip_value: f64,
90 pub scale_flat_field: f64,
91 pub enable_filter: bool,
92 pub filter: FilterConfig,
93 pub output_type: Option<NDDataType>,
94 pub auto_offset_scale_pending: bool,
97 pub valid_background: bool,
99 pub valid_flat_field: bool,
101}
102
103impl Default for ProcessConfig {
104 fn default() -> Self {
105 Self {
106 enable_background: false,
107 enable_flat_field: false,
108 enable_offset_scale: false,
109 offset: 0.0,
110 scale: 1.0,
111 enable_low_clip: false,
112 low_clip_thresh: 0.0,
113 low_clip_value: 0.0,
114 enable_high_clip: false,
115 high_clip_thresh: 100.0,
116 high_clip_value: 100.0,
117 scale_flat_field: 255.0,
118 enable_filter: false,
119 filter: FilterConfig::default(),
120 output_type: None,
121 auto_offset_scale_pending: false,
122 valid_background: false,
123 valid_flat_field: false,
124 }
125 }
126}
127
128fn elements_as_f64(array: &NDArray) -> Vec<f64> {
131 (0..array.data.len())
132 .map(|i| array.data.get_as_f64(i).unwrap_or(0.0))
133 .collect()
134}
135
136pub struct ProcessState {
140 pub config: ProcessConfig,
141 pub background: Option<Vec<f64>>,
142 pub flat_field: Option<Vec<f64>>,
143 pub filter_state: Option<Vec<f64>>,
150 pub num_filtered: usize,
152 reset_filter_pending: bool,
156 last_output: Option<NDArray>,
169}
170
171#[inline]
186fn accumulate(acc: f64, coef: f64, term: f64) -> f64 {
187 if coef != 0.0 { acc + coef * term } else { acc }
188}
189
190impl ProcessState {
191 pub fn new(config: ProcessConfig) -> Self {
192 Self {
193 config,
194 background: None,
195 flat_field: None,
196 filter_state: None,
197 num_filtered: 0,
198 reset_filter_pending: false,
199 last_output: None,
200 }
201 }
202
203 pub fn last_output(&self) -> Option<&NDArray> {
206 self.last_output.as_ref()
207 }
208
209 pub fn save_background(&mut self) {
229 let saved = self.last_output.as_ref().map(elements_as_f64);
230 self.config.valid_background = saved.is_some();
231 self.background = saved;
232 }
233
234 pub fn save_flat_field(&mut self) {
238 let saved = self.last_output.as_ref().map(elements_as_f64);
239 self.config.valid_flat_field = saved.is_some();
240 self.flat_field = saved;
241 }
242
243 pub fn auto_offset_scale(&mut self, array: &NDArray) {
248 let n = array.data.len();
249 if n == 0 {
250 return;
251 }
252 let mut min_val = f64::MAX;
253 let mut max_val = f64::MIN;
254 for i in 0..n {
255 let v = array.data.get_as_f64(i).unwrap_or(0.0);
256 if v < min_val {
257 min_val = v;
258 }
259 if v > max_val {
260 max_val = v;
261 }
262 }
263 let range = max_val - min_val;
264 if range > 0.0 {
265 let bytes_per_elem = match self.config.output_type.unwrap_or(array.data.data_type()) {
267 NDDataType::Int8 | NDDataType::UInt8 => 1,
268 NDDataType::Int16 | NDDataType::UInt16 => 2,
269 NDDataType::Int32 | NDDataType::UInt32 => 4,
270 NDDataType::Int64 | NDDataType::UInt64 => 8,
271 NDDataType::Float32 => 4,
272 NDDataType::Float64 => 8,
273 };
274 let max_scale = 2.0f64.powi(bytes_per_elem * 8) - 1.0;
275 self.config.scale = max_scale / range;
277 self.config.offset = -min_val;
278 self.config.enable_offset_scale = true;
280 self.config.enable_low_clip = true;
281 self.config.low_clip_thresh = 0.0;
282 self.config.enable_high_clip = true;
283 self.config.high_clip_thresh = max_scale;
284 }
285 }
286
287 pub fn apply_filter_type(&mut self, filter_type: i32) {
298 let fc = &mut self.config.filter;
299 match filter_type {
300 0 => {
301 fc.fc = [1.0, -1.0, 0.0, 1.0];
315 fc.oc = [1.0, 0.0, 0.0, 0.0];
316 fc.rc = [0.0, 1.0]; fc.r_offset = 0.0;
318 fc.f_offset = 0.0;
319 fc.f_scale = 1.0;
320 fc.o_offset = 0.0;
321 fc.o_scale = 1.0;
322 }
323 1 => {
324 fc.fc = [1.0, 0.0, 1.0, 0.0];
330 fc.oc = [0.0, 1.0, 0.0, 0.0];
331 fc.rc = [0.0, 1.0]; fc.r_offset = 0.0;
333 fc.f_offset = 0.0;
334 fc.f_scale = 1.0;
335 fc.o_offset = 0.0;
336 fc.o_scale = 1.0;
337 }
338 2 => {
339 fc.fc = [1.0, 0.0, 1.0, 0.0];
341 fc.oc = [1.0, 0.0, 0.0, 0.0];
342 fc.rc = [0.0, 1.0];
343 fc.r_offset = 0.0;
344 fc.f_offset = 0.0;
345 fc.f_scale = 1.0;
346 fc.o_offset = 0.0;
347 fc.o_scale = 1.0;
348 }
349 3 => {
350 fc.fc = [0.0, 0.0, 1.0, 0.0];
354 fc.oc = [-1.0, 0.0, 1.0, 0.0];
355 fc.rc = [0.0, 1.0];
356 fc.r_offset = 0.0;
357 fc.f_offset = 0.0;
358 fc.f_scale = 1.0;
359 fc.o_offset = 0.0;
360 fc.o_scale = 1.0;
361 }
362 4 => {
363 fc.fc = [1.0, -1.0, 0.0, 1.0];
366 fc.oc = [-1.0, 0.0, 1.0, 0.0];
367 fc.rc = [0.0, 1.0];
368 fc.r_offset = 0.0;
369 fc.f_offset = 0.0;
370 fc.f_scale = 1.0;
371 fc.o_offset = 0.0;
372 fc.o_scale = 1.0;
373 }
374 5 => {
375 fc.fc = [0.0, 0.0, 1.0, 0.0];
377 fc.oc = [1.0, 0.0, 0.0, 0.0];
378 fc.rc = [0.0, 1.0];
379 fc.r_offset = 0.0;
380 fc.f_offset = 0.0;
381 fc.f_scale = 1.0;
382 fc.o_offset = 0.0;
383 fc.o_scale = 1.0;
384 }
385 _ => {} }
387 }
388
389 pub fn reset_filter(&mut self) {
399 self.reset_filter_pending = true;
400 }
401
402 pub fn process(&mut self, src: &NDArray) -> Option<NDArray> {
409 let n = src.data.len();
410 let mut values = vec![0.0f64; n];
411 for i in 0..n {
412 values[i] = src.data.get_as_f64(i).unwrap_or(0.0);
413 }
414
415 let reset_requested = self.reset_filter_pending;
420 self.reset_filter_pending = false;
421
422 let auto_offset_scale_now = self.config.auto_offset_scale_pending;
429 self.config.auto_offset_scale_pending = false;
430
431 self.config.valid_background = self.background.as_ref().is_some_and(|b| b.len() == n);
437 self.config.valid_flat_field = self.flat_field.as_ref().is_some_and(|f| f.len() == n);
438
439 let needs_element_ops = self.config.enable_background
442 || self.config.enable_flat_field
443 || self.config.enable_offset_scale
444 || self.config.enable_low_clip
445 || self.config.enable_high_clip;
446
447 if needs_element_ops {
448 let bg = if self.config.enable_background && self.config.valid_background {
451 self.background.as_ref()
452 } else {
453 None
454 };
455 let (ff, ff_scale) = if self.config.enable_flat_field && self.config.valid_flat_field {
456 if let Some(ref ff) = self.flat_field {
457 (Some(ff.as_slice()), self.config.scale_flat_field)
461 } else {
462 (None, 0.0)
463 }
464 } else {
465 (None, 0.0)
466 };
467 let do_offset_scale = self.config.enable_offset_scale;
468 let scale = self.config.scale;
469 let offset = self.config.offset;
470 let do_low_clip = self.config.enable_low_clip;
471 let low_clip_thresh = self.config.low_clip_thresh;
472 let low_clip_value = self.config.low_clip_value;
473 let do_high_clip = self.config.enable_high_clip;
474 let high_clip_thresh = self.config.high_clip_thresh;
475 let high_clip_value = self.config.high_clip_value;
476
477 let apply_stages = |i: usize, v: &mut f64| {
478 if let Some(bg) = bg {
482 *v -= bg[i];
483 }
484 if let Some(ff) = ff {
486 if ff[i] != 0.0 {
487 *v = *v * ff_scale / ff[i];
488 }
489 }
490 if do_offset_scale {
492 *v = (*v + offset) * scale;
493 }
494 if do_high_clip && *v > high_clip_thresh {
498 *v = high_clip_value;
499 }
500 if do_low_clip && *v < low_clip_thresh {
501 *v = low_clip_value;
502 }
503 };
504
505 #[cfg(feature = "parallel")]
506 let use_parallel = par_util::should_parallelize(n);
507 #[cfg(not(feature = "parallel"))]
508 let use_parallel = false;
509
510 if use_parallel {
511 #[cfg(feature = "parallel")]
512 par_util::thread_pool().install(|| {
513 values.par_iter_mut().enumerate().for_each(|(i, v)| {
514 apply_stages(i, v);
515 });
516 });
517 } else {
518 for (i, v) in values.iter_mut().enumerate() {
519 apply_stages(i, v);
520 }
521 }
522 }
523
524 if self.config.enable_filter {
526 let fc = &self.config.filter;
527
528 if let Some(ref f) = self.filter_state {
532 if f.len() != n {
533 self.filter_state = None;
534 }
535 }
536
537 let mut reset_filter = reset_requested;
538 if self.filter_state.is_none() {
539 self.filter_state = Some(values.clone());
541 reset_filter = true;
542 }
543 if self.num_filtered >= fc.num_filter && fc.auto_reset {
544 reset_filter = true;
545 }
546
547 let filter = self.filter_state.as_mut().unwrap();
548
549 if reset_filter {
550 let r_offset = fc.r_offset;
555 let rc1 = fc.rc[0];
556 let rc2 = fc.rc[1];
557 for i in 0..n {
558 let mut new_filter = accumulate(r_offset, rc1, filter[i]);
559 new_filter = accumulate(new_filter, rc2, values[i]);
560 filter[i] = new_filter;
561 }
562 self.num_filtered = 0;
563 }
564
565 if self.num_filtered < fc.num_filter {
567 self.num_filtered += 1;
568 }
569
570 let nf = self.num_filtered as f64;
572 let o1 = fc.o_scale * (fc.oc[0] + fc.oc[1] / nf);
573 let o2 = fc.o_scale * (fc.oc[2] + fc.oc[3] / nf);
574 let f1 = fc.f_scale * (fc.fc[0] + fc.fc[1] / nf);
575 let f2 = fc.f_scale * (fc.fc[2] + fc.fc[3] / nf);
576 let o_offset = fc.o_offset;
577 let f_offset = fc.f_offset;
578
579 for i in 0..n {
592 let mut new_data = accumulate(o_offset, o1, filter[i]);
593 new_data = accumulate(new_data, o2, values[i]);
594 let mut new_filter = accumulate(f_offset, f1, filter[i]);
595 new_filter = accumulate(new_filter, f2, values[i]);
596 values[i] = new_data;
597 filter[i] = new_filter;
598 }
599
600 if fc.filter_callbacks > 0 && self.num_filtered != fc.num_filter {
605 return None;
606 }
607 }
608
609 let out_type = self.config.output_type.unwrap_or(src.data.data_type());
611 let mut out_data = NDDataBuffer::zeros(out_type, n);
612 for i in 0..n {
613 out_data.set_from_f64(i, values[i]);
614 }
615
616 let mut arr = NDArray::new(src.dims.clone(), out_type);
617 arr.data = out_data;
618 arr.unique_id = src.unique_id;
619 arr.timestamp = src.timestamp;
620 arr.attributes = src.attributes.clone();
621
622 if auto_offset_scale_now {
627 self.auto_offset_scale(src);
628 }
629
630 self.last_output = Some(arr.clone());
635
636 Some(arr)
637 }
638}
639
640#[derive(Default)]
644struct ProcParamIndices {
645 data_type: Option<usize>,
646 save_background: Option<usize>,
647 enable_background: Option<usize>,
648 valid_background: Option<usize>,
649 save_flat_field: Option<usize>,
650 enable_flat_field: Option<usize>,
651 valid_flat_field: Option<usize>,
652 scale_flat_field: Option<usize>,
653 enable_offset_scale: Option<usize>,
654 auto_offset_scale: Option<usize>,
655 offset: Option<usize>,
656 scale: Option<usize>,
657 enable_low_clip: Option<usize>,
658 low_clip_thresh: Option<usize>,
659 low_clip_value: Option<usize>,
660 enable_high_clip: Option<usize>,
661 high_clip_thresh: Option<usize>,
662 high_clip_value: Option<usize>,
663 enable_filter: Option<usize>,
664 filter_type: Option<usize>,
665 reset_filter: Option<usize>,
666 auto_reset_filter: Option<usize>,
667 filter_callbacks: Option<usize>,
668 num_filter: Option<usize>,
669 num_filtered: Option<usize>,
670 o_offset: Option<usize>,
671 o_scale: Option<usize>,
672 oc: [Option<usize>; 4],
673 f_offset: Option<usize>,
674 f_scale: Option<usize>,
675 fc: [Option<usize>; 4],
676 r_offset: Option<usize>,
677 rc: [Option<usize>; 2],
678}
679
680pub struct ProcessProcessor {
682 state: ProcessState,
683 params: ProcParamIndices,
684}
685
686impl ProcessProcessor {
687 pub fn new(config: ProcessConfig) -> Self {
688 Self {
689 state: ProcessState::new(config),
690 params: ProcParamIndices::default(),
691 }
692 }
693
694 pub fn state(&self) -> &ProcessState {
695 &self.state
696 }
697
698 pub fn state_mut(&mut self) -> &mut ProcessState {
699 &mut self.state
700 }
701}
702
703impl NDPluginProcess for ProcessProcessor {
704 fn process_array(&mut self, array: &NDArray, _pool: &NDArrayPool) -> ProcessResult {
705 use ad_core_rs::plugin::runtime::ParamUpdate;
706
707 let out = self.state.process(array);
708 let mut result = match out {
711 Some(arr) => ProcessResult::arrays(vec![Arc::new(arr)]),
712 None => ProcessResult::sink(vec![]),
713 };
714
715 if let Some(idx) = self.params.valid_background {
717 result.param_updates.push(ParamUpdate::int32(
718 idx,
719 if self.state.config.valid_background {
720 1
721 } else {
722 0
723 },
724 ));
725 }
726 if let Some(idx) = self.params.valid_flat_field {
727 result.param_updates.push(ParamUpdate::int32(
728 idx,
729 if self.state.config.valid_flat_field {
730 1
731 } else {
732 0
733 },
734 ));
735 }
736 if let Some(idx) = self.params.num_filtered {
737 result
738 .param_updates
739 .push(ParamUpdate::int32(idx, self.state.num_filtered as i32));
740 }
741 if let Some(idx) = self.params.reset_filter {
748 result.param_updates.push(ParamUpdate::int32(idx, 0));
749 }
750
751 result
752 }
753
754 fn plugin_type(&self) -> &str {
755 "NDPluginProcess"
756 }
757
758 fn register_params(
759 &mut self,
760 base: &mut asyn_rs::port::PortDriverBase,
761 ) -> asyn_rs::error::AsynResult<()> {
762 use asyn_rs::param::ParamType;
763 base.create_param("PROCESS_DATA_TYPE", ParamType::Int32)?;
764 base.create_param("SAVE_BACKGROUND", ParamType::Int32)?;
765 base.create_param("ENABLE_BACKGROUND", ParamType::Int32)?;
766 base.create_param("VALID_BACKGROUND", ParamType::Int32)?;
767 base.create_param("SAVE_FLAT_FIELD", ParamType::Int32)?;
768 base.create_param("ENABLE_FLAT_FIELD", ParamType::Int32)?;
769 base.create_param("VALID_FLAT_FIELD", ParamType::Int32)?;
770 base.create_param("SCALE_FLAT_FIELD", ParamType::Float64)?;
771 base.create_param("ENABLE_OFFSET_SCALE", ParamType::Int32)?;
772 base.create_param("AUTO_OFFSET_SCALE", ParamType::Int32)?;
773 base.create_param("OFFSET", ParamType::Float64)?;
774 base.create_param("SCALE", ParamType::Float64)?;
775 base.create_param("ENABLE_LOW_CLIP", ParamType::Int32)?;
776 base.create_param("LOW_CLIP_THRESH", ParamType::Float64)?;
777 base.create_param("LOW_CLIP_VALUE", ParamType::Float64)?;
778 base.create_param("ENABLE_HIGH_CLIP", ParamType::Int32)?;
779 base.create_param("HIGH_CLIP_THRESH", ParamType::Float64)?;
780 base.create_param("HIGH_CLIP_VALUE", ParamType::Float64)?;
781 base.create_param("ENABLE_FILTER", ParamType::Int32)?;
782 base.create_param("FILTER_TYPE", ParamType::Int32)?;
783 base.create_param("RESET_FILTER", ParamType::Int32)?;
784 base.create_param("AUTO_RESET_FILTER", ParamType::Int32)?;
785 base.create_param("FILTER_CALLBACKS", ParamType::Int32)?;
786 base.create_param("NUM_FILTER", ParamType::Int32)?;
787 base.create_param("NUM_FILTERED", ParamType::Int32)?;
788 base.create_param("FILTER_OOFFSET", ParamType::Float64)?;
789 base.create_param("FILTER_OSCALE", ParamType::Float64)?;
790 base.create_param("FILTER_OC1", ParamType::Float64)?;
791 base.create_param("FILTER_OC2", ParamType::Float64)?;
792 base.create_param("FILTER_OC3", ParamType::Float64)?;
793 base.create_param("FILTER_OC4", ParamType::Float64)?;
794 base.create_param("FILTER_FOFFSET", ParamType::Float64)?;
795 base.create_param("FILTER_FSCALE", ParamType::Float64)?;
796 base.create_param("FILTER_FC1", ParamType::Float64)?;
797 base.create_param("FILTER_FC2", ParamType::Float64)?;
798 base.create_param("FILTER_FC3", ParamType::Float64)?;
799 base.create_param("FILTER_FC4", ParamType::Float64)?;
800 base.create_param("FILTER_ROFFSET", ParamType::Float64)?;
801 base.create_param("FILTER_RC1", ParamType::Float64)?;
802 base.create_param("FILTER_RC2", ParamType::Float64)?;
803
804 self.params.data_type = base.find_param("PROCESS_DATA_TYPE");
806 self.params.save_background = base.find_param("SAVE_BACKGROUND");
807 self.params.enable_background = base.find_param("ENABLE_BACKGROUND");
808 self.params.valid_background = base.find_param("VALID_BACKGROUND");
809 self.params.save_flat_field = base.find_param("SAVE_FLAT_FIELD");
810 self.params.enable_flat_field = base.find_param("ENABLE_FLAT_FIELD");
811 self.params.valid_flat_field = base.find_param("VALID_FLAT_FIELD");
812 self.params.scale_flat_field = base.find_param("SCALE_FLAT_FIELD");
813 self.params.enable_offset_scale = base.find_param("ENABLE_OFFSET_SCALE");
814 self.params.auto_offset_scale = base.find_param("AUTO_OFFSET_SCALE");
815 self.params.offset = base.find_param("OFFSET");
816 self.params.scale = base.find_param("SCALE");
817 self.params.enable_low_clip = base.find_param("ENABLE_LOW_CLIP");
818 self.params.low_clip_thresh = base.find_param("LOW_CLIP_THRESH");
819 self.params.low_clip_value = base.find_param("LOW_CLIP_VALUE");
820 self.params.enable_high_clip = base.find_param("ENABLE_HIGH_CLIP");
821 self.params.high_clip_thresh = base.find_param("HIGH_CLIP_THRESH");
822 self.params.high_clip_value = base.find_param("HIGH_CLIP_VALUE");
823 self.params.enable_filter = base.find_param("ENABLE_FILTER");
824 self.params.filter_type = base.find_param("FILTER_TYPE");
825 self.params.reset_filter = base.find_param("RESET_FILTER");
826 self.params.auto_reset_filter = base.find_param("AUTO_RESET_FILTER");
827 self.params.filter_callbacks = base.find_param("FILTER_CALLBACKS");
828 self.params.num_filter = base.find_param("NUM_FILTER");
829 self.params.num_filtered = base.find_param("NUM_FILTERED");
830 self.params.o_offset = base.find_param("FILTER_OOFFSET");
831 self.params.o_scale = base.find_param("FILTER_OSCALE");
832 self.params.oc[0] = base.find_param("FILTER_OC1");
833 self.params.oc[1] = base.find_param("FILTER_OC2");
834 self.params.oc[2] = base.find_param("FILTER_OC3");
835 self.params.oc[3] = base.find_param("FILTER_OC4");
836 self.params.f_offset = base.find_param("FILTER_FOFFSET");
837 self.params.f_scale = base.find_param("FILTER_FSCALE");
838 self.params.fc[0] = base.find_param("FILTER_FC1");
839 self.params.fc[1] = base.find_param("FILTER_FC2");
840 self.params.fc[2] = base.find_param("FILTER_FC3");
841 self.params.fc[3] = base.find_param("FILTER_FC4");
842 self.params.r_offset = base.find_param("FILTER_ROFFSET");
843 self.params.rc[0] = base.find_param("FILTER_RC1");
844 self.params.rc[1] = base.find_param("FILTER_RC2");
845 Ok(())
846 }
847
848 fn on_param_change(
849 &mut self,
850 reason: usize,
851 params: &ad_core_rs::plugin::runtime::PluginParamSnapshot,
852 ) -> ad_core_rs::plugin::runtime::ParamChangeResult {
853 use ad_core_rs::plugin::runtime::{ParamChangeResult, ParamUpdate};
854
855 let s = &mut self.state;
856 let p = &self.params;
857 let mut updates = Vec::new();
858
859 if Some(reason) == p.data_type {
860 let v = params.value.as_i32();
861 s.config.output_type = if v < 0 {
862 None } else {
864 NDDataType::from_ordinal(v as u8)
865 };
866 } else if Some(reason) == p.save_background {
867 s.save_background();
872 updates.push(ParamUpdate::int32(reason, 0));
873 if let Some(idx) = p.valid_background {
874 updates.push(ParamUpdate::int32(idx, s.config.valid_background as i32));
875 }
876 } else if Some(reason) == p.enable_background {
877 s.config.enable_background = params.value.as_i32() != 0;
878 } else if Some(reason) == p.save_flat_field {
879 s.save_flat_field();
881 updates.push(ParamUpdate::int32(reason, 0));
882 if let Some(idx) = p.valid_flat_field {
883 updates.push(ParamUpdate::int32(idx, s.config.valid_flat_field as i32));
884 }
885 } else if Some(reason) == p.enable_flat_field {
886 s.config.enable_flat_field = params.value.as_i32() != 0;
887 } else if Some(reason) == p.scale_flat_field {
888 s.config.scale_flat_field = params.value.as_f64();
889 } else if Some(reason) == p.enable_offset_scale {
890 s.config.enable_offset_scale = params.value.as_i32() != 0;
891 } else if Some(reason) == p.auto_offset_scale {
892 if params.value.as_i32() != 0 {
893 s.config.auto_offset_scale_pending = true;
898 if let Some(idx) = p.auto_offset_scale {
899 updates.push(ParamUpdate::int32(idx, 0));
900 }
901 }
902 } else if Some(reason) == p.offset {
903 s.config.offset = params.value.as_f64();
904 } else if Some(reason) == p.scale {
905 s.config.scale = params.value.as_f64();
906 } else if Some(reason) == p.enable_low_clip {
907 s.config.enable_low_clip = params.value.as_i32() != 0;
908 } else if Some(reason) == p.low_clip_thresh {
909 s.config.low_clip_thresh = params.value.as_f64();
910 } else if Some(reason) == p.low_clip_value {
911 s.config.low_clip_value = params.value.as_f64();
912 } else if Some(reason) == p.enable_high_clip {
913 s.config.enable_high_clip = params.value.as_i32() != 0;
914 } else if Some(reason) == p.high_clip_thresh {
915 s.config.high_clip_thresh = params.value.as_f64();
916 } else if Some(reason) == p.high_clip_value {
917 s.config.high_clip_value = params.value.as_f64();
918 } else if Some(reason) == p.enable_filter {
919 s.config.enable_filter = params.value.as_i32() != 0;
920 } else if Some(reason) == p.filter_type {
921 s.apply_filter_type(params.value.as_i32());
926 let fc = &s.config.filter;
928 for (i, idx) in p.fc.iter().enumerate() {
929 if let Some(idx) = *idx {
930 updates.push(ParamUpdate::float64(idx, fc.fc[i]));
931 }
932 }
933 for (i, idx) in p.oc.iter().enumerate() {
934 if let Some(idx) = *idx {
935 updates.push(ParamUpdate::float64(idx, fc.oc[i]));
936 }
937 }
938 for (i, idx) in p.rc.iter().enumerate() {
939 if let Some(idx) = *idx {
940 updates.push(ParamUpdate::float64(idx, fc.rc[i]));
941 }
942 }
943 if let Some(idx) = p.f_offset {
944 updates.push(ParamUpdate::float64(idx, fc.f_offset));
945 }
946 if let Some(idx) = p.f_scale {
947 updates.push(ParamUpdate::float64(idx, fc.f_scale));
948 }
949 if let Some(idx) = p.o_offset {
950 updates.push(ParamUpdate::float64(idx, fc.o_offset));
951 }
952 if let Some(idx) = p.o_scale {
953 updates.push(ParamUpdate::float64(idx, fc.o_scale));
954 }
955 } else if Some(reason) == p.reset_filter {
956 if params.value.as_i32() != 0 {
957 s.reset_filter();
961 }
962 } else if Some(reason) == p.auto_reset_filter {
963 s.config.filter.auto_reset = params.value.as_i32() != 0;
964 } else if Some(reason) == p.filter_callbacks {
965 s.config.filter.filter_callbacks = params.value.as_i32().max(0) as usize;
966 } else if Some(reason) == p.num_filter {
967 s.config.filter.num_filter = params.value.as_i32().max(1) as usize;
968 } else if Some(reason) == p.o_offset {
969 s.config.filter.o_offset = params.value.as_f64();
970 } else if Some(reason) == p.o_scale {
971 s.config.filter.o_scale = params.value.as_f64();
972 } else if Some(reason) == p.f_offset {
973 s.config.filter.f_offset = params.value.as_f64();
974 } else if Some(reason) == p.f_scale {
975 s.config.filter.f_scale = params.value.as_f64();
976 } else if Some(reason) == p.r_offset {
977 s.config.filter.r_offset = params.value.as_f64();
978 } else {
979 for i in 0..4 {
981 if Some(reason) == p.oc[i] {
982 s.config.filter.oc[i] = params.value.as_f64();
983 return ParamChangeResult::updates(vec![]);
984 }
985 if Some(reason) == p.fc[i] {
986 s.config.filter.fc[i] = params.value.as_f64();
987 return ParamChangeResult::updates(vec![]);
988 }
989 }
990 for i in 0..2 {
991 if Some(reason) == p.rc[i] {
992 s.config.filter.rc[i] = params.value.as_f64();
993 return ParamChangeResult::updates(vec![]);
994 }
995 }
996 }
997
998 ParamChangeResult::updates(updates)
999 }
1000}
1001
1002#[cfg(test)]
1003mod tests {
1004 use super::*;
1005 use ad_core_rs::ndarray::{NDDataBuffer, NDDimension};
1006
1007 fn make_array(vals: &[u8]) -> NDArray {
1008 let mut arr = NDArray::new(vec![NDDimension::new(vals.len())], NDDataType::UInt8);
1009 if let NDDataBuffer::U8(ref mut v) = arr.data {
1010 v.copy_from_slice(vals);
1011 }
1012 arr
1013 }
1014
1015 fn seed_background(state: &mut ProcessState, arr: &NDArray) {
1018 state.last_output = Some(arr.clone());
1019 state.save_background();
1020 }
1021
1022 fn seed_flat_field(state: &mut ProcessState, arr: &NDArray) {
1024 state.last_output = Some(arr.clone());
1025 state.save_flat_field();
1026 }
1027
1028 fn make_f64_array(vals: &[f64]) -> NDArray {
1029 let mut arr = NDArray::new(vec![NDDimension::new(vals.len())], NDDataType::Float64);
1030 if let NDDataBuffer::F64(ref mut v) = arr.data {
1031 v.copy_from_slice(vals);
1032 }
1033 arr
1034 }
1035
1036 #[test]
1037 fn test_background_subtraction() {
1038 let bg_arr = make_array(&[10, 20, 30]);
1039 let input = make_array(&[15, 25, 35]);
1040
1041 let mut state = ProcessState::new(ProcessConfig {
1042 enable_background: true,
1043 ..Default::default()
1044 });
1045 seed_background(&mut state, &bg_arr);
1046
1047 let result = state.process(&input).unwrap();
1048 if let NDDataBuffer::U8(ref v) = result.data {
1049 assert_eq!(v[0], 5);
1050 assert_eq!(v[1], 5);
1051 assert_eq!(v[2], 5);
1052 }
1053 }
1054
1055 #[test]
1056 fn test_adp7_size_mismatched_background_invalidated_not_partial() {
1057 let bg_arr = make_array(&[10, 20]); let input = make_array(&[15, 25, 35]); let mut state = ProcessState::new(ProcessConfig {
1064 enable_background: true,
1065 ..Default::default()
1066 });
1067 seed_background(&mut state, &bg_arr);
1068 assert!(state.config.valid_background); let result = state.process(&input).unwrap();
1071 assert!(!state.config.valid_background);
1074 if let NDDataBuffer::U8(ref v) = result.data {
1075 assert_eq!(v, &[15, 25, 35]);
1076 } else {
1077 panic!("expected U8 output");
1078 }
1079 }
1080
1081 #[test]
1082 fn test_flat_field() {
1083 let ff_arr = make_array(&[100, 200, 50]);
1086 let input = make_array(&[100, 100, 100]);
1087
1088 let mut state = ProcessState::new(ProcessConfig {
1089 enable_flat_field: true,
1090 scale_flat_field: 100.0,
1091 ..Default::default()
1092 });
1093 seed_flat_field(&mut state, &ff_arr);
1094
1095 let result = state.process(&input).unwrap();
1096 if let NDDataBuffer::U8(ref v) = result.data {
1097 assert_eq!(v[0], 100); assert_eq!(v[1], 50); assert_eq!(v[2], 200); } else {
1101 panic!("expected U8 output");
1102 }
1103 }
1104
1105 #[test]
1106 fn test_adp24_scale_flat_field_zero_zeroes_output() {
1107 let ff_arr = make_array(&[100, 200, 50]);
1111 let input = make_array(&[100, 100, 100]);
1112 let mut state = ProcessState::new(ProcessConfig {
1113 enable_flat_field: true,
1114 scale_flat_field: 0.0,
1115 ..Default::default()
1116 });
1117 seed_flat_field(&mut state, &ff_arr);
1118 let result = state.process(&input).unwrap();
1119 if let NDDataBuffer::U8(ref v) = result.data {
1120 assert_eq!(v, &[0, 0, 0]);
1121 } else {
1122 panic!("expected U8 output");
1123 }
1124 }
1125
1126 #[test]
1127 fn test_offset_scale() {
1128 let input = make_array(&[10, 20, 30]);
1129 let mut state = ProcessState::new(ProcessConfig {
1130 enable_offset_scale: true,
1131 scale: 2.0,
1132 offset: 5.0,
1133 ..Default::default()
1134 });
1135
1136 let result = state.process(&input).unwrap();
1137 if let NDDataBuffer::U8(ref v) = result.data {
1138 assert_eq!(v[0], 30); assert_eq!(v[1], 50); assert_eq!(v[2], 70); }
1143 }
1144
1145 #[test]
1146 fn test_clipping() {
1147 let input = make_array(&[5, 50, 200]);
1148 let mut state = ProcessState::new(ProcessConfig {
1149 enable_low_clip: true,
1150 low_clip_thresh: 10.0,
1151 low_clip_value: 10.0,
1152 enable_high_clip: true,
1153 high_clip_thresh: 100.0,
1154 high_clip_value: 100.0,
1155 ..Default::default()
1156 });
1157
1158 let result = state.process(&input).unwrap();
1159 if let NDDataBuffer::U8(ref v) = result.data {
1160 assert_eq!(v[0], 10); assert_eq!(v[1], 50); assert_eq!(v[2], 100); }
1164 }
1165
1166 #[test]
1167 fn test_adp5_clip_order_high_before_low() {
1168 let input = make_f64_array(&[200.0]);
1173 let mut state = ProcessState::new(ProcessConfig {
1174 enable_high_clip: true,
1175 high_clip_thresh: 100.0,
1176 high_clip_value: 10.0,
1177 enable_low_clip: true,
1178 low_clip_thresh: 50.0,
1179 low_clip_value: 999.0,
1180 ..Default::default()
1181 });
1182 let result = state.process(&input).unwrap();
1183 if let NDDataBuffer::F64(ref v) = result.data {
1184 assert_eq!(v[0], 999.0);
1185 } else {
1186 panic!("expected F64 output");
1187 }
1188 }
1189
1190 #[test]
1191 fn test_recursive_filter() {
1192 let input1 = make_array(&[100, 100, 100]);
1198 let input2 = make_array(&[0, 0, 0]);
1199
1200 let mut state = ProcessState::new(ProcessConfig {
1201 enable_filter: true,
1202 filter: FilterConfig {
1203 num_filter: 10,
1204 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()
1208 },
1209 ..Default::default()
1210 });
1211
1212 let _ = state.process(&input1);
1223
1224 let result = state.process(&input2).unwrap();
1229 if let NDDataBuffer::U8(ref v) = result.data {
1230 assert_eq!(v[0], 100);
1232 assert_eq!(v[1], 100);
1233 }
1234 }
1235
1236 #[test]
1237 fn test_output_type_conversion() {
1238 let input = make_array(&[10, 20, 30]);
1239 let mut state = ProcessState::new(ProcessConfig {
1240 output_type: Some(NDDataType::Float64),
1241 ..Default::default()
1242 });
1243
1244 let result = state.process(&input).unwrap();
1245 assert_eq!(result.data.data_type(), NDDataType::Float64);
1246 }
1247
1248 #[test]
1251 fn test_process_processor() {
1252 let mut proc = ProcessProcessor::new(ProcessConfig {
1253 enable_offset_scale: true,
1254 scale: 2.0,
1255 offset: 1.0,
1256 ..Default::default()
1257 });
1258 let pool = NDArrayPool::new(1_000_000);
1259
1260 let input = make_array(&[10, 20, 30]);
1261 let result = proc.process_array(&input, &pool);
1262 assert_eq!(result.output_arrays.len(), 1);
1263 if let NDDataBuffer::U8(ref v) = result.output_arrays[0].data {
1264 assert_eq!(v[0], 22); }
1266 }
1267
1268 #[test]
1271 fn test_filter_sum_preset() {
1272 let mut state = ProcessState::new(ProcessConfig {
1275 enable_filter: true,
1276 filter: FilterConfig {
1277 num_filter: 10,
1278 fc: [1.0, 0.0, 1.0, 0.0],
1279 oc: [1.0, 0.0, 0.0, 0.0],
1280 rc: [0.0, 1.0],
1281 ..Default::default()
1282 },
1283 output_type: Some(NDDataType::Float64),
1284 ..Default::default()
1285 });
1286
1287 let r0 = state.process(&make_f64_array(&[100.0])).unwrap();
1299 let v0 = r0.data.get_as_f64(0).unwrap();
1300 assert!((v0 - 100.0).abs() < 1e-9, "frame 0: got {v0}");
1301
1302 let r1 = state.process(&make_f64_array(&[100.0])).unwrap();
1309 let v1 = r1.data.get_as_f64(0).unwrap();
1310 assert!((v1 - 200.0).abs() < 1e-9, "frame 1: got {v1}");
1311 }
1312
1313 #[test]
1314 fn test_filter_average_preset() {
1315 let mut state = ProcessState::new(ProcessConfig {
1318 enable_filter: true,
1319 filter: FilterConfig {
1320 num_filter: 10,
1321 fc: [1.0, 0.0, 1.0, 0.0],
1322 oc: [0.0, 1.0, 0.0, 0.0],
1323 rc: [0.0, 1.0],
1324 ..Default::default()
1325 },
1326 output_type: Some(NDDataType::Float64),
1327 ..Default::default()
1328 });
1329
1330 let r0 = state.process(&make_f64_array(&[100.0])).unwrap();
1339 let v0 = r0.data.get_as_f64(0).unwrap();
1340 assert!((v0 - 100.0).abs() < 1e-9, "frame 0: got {v0}");
1341
1342 let r1 = state.process(&make_f64_array(&[200.0])).unwrap();
1347 let v1 = r1.data.get_as_f64(0).unwrap();
1348 assert!((v1 - 100.0).abs() < 1e-9, "frame 1: got {v1}");
1349
1350 let r2 = state.process(&make_f64_array(&[300.0])).unwrap();
1355 let v2 = r2.data.get_as_f64(0).unwrap();
1356 let expected = 400.0 / 3.0;
1357 assert!((v2 - expected).abs() < 1e-9, "frame 2: got {v2}");
1358 }
1359
1360 #[test]
1361 fn test_filter_recursive_ave() {
1362 let mut state = ProcessState::new(ProcessConfig {
1367 enable_filter: true,
1368 filter: FilterConfig {
1369 num_filter: 10,
1370 fc: [1.0, -1.0, 0.0, 1.0],
1371 oc: [1.0, 0.0, 0.0, 0.0],
1372 rc: [0.0, 1.0],
1373 ..Default::default()
1374 },
1375 output_type: Some(NDDataType::Float64),
1376 ..Default::default()
1377 });
1378
1379 let r0 = state.process(&make_f64_array(&[100.0])).unwrap();
1391 let v0 = r0.data.get_as_f64(0).unwrap();
1392 assert!((v0 - 100.0).abs() < 1e-9, "frame 0: got {v0}");
1393
1394 let r1 = state.process(&make_f64_array(&[200.0])).unwrap();
1399 let v1 = r1.data.get_as_f64(0).unwrap();
1400 assert!((v1 - 100.0).abs() < 1e-9, "frame 1: got {v1}");
1401
1402 let r2 = state.process(&make_f64_array(&[300.0])).unwrap();
1407 let v2 = r2.data.get_as_f64(0).unwrap();
1408 assert!((v2 - 150.0).abs() < 1e-9, "frame 2: got {v2}");
1409 }
1410
1411 #[test]
1412 fn test_r9_68_save_background_copies_the_last_output_synchronously() {
1413 let mut state = ProcessState::new(ProcessConfig {
1422 enable_offset_scale: true,
1423 offset: 0.0,
1424 scale: 2.0,
1425 output_type: Some(NDDataType::Float64),
1426 ..Default::default()
1427 });
1428
1429 state.save_background();
1433 assert!(state.background.is_none());
1434 assert!(!state.config.valid_background);
1435
1436 let out = state.process(&make_array(&[10, 20, 30])).unwrap();
1438 assert_eq!(out.data.get_as_f64(0), Some(20.0));
1439
1440 state.save_background();
1443 assert!(
1444 state.config.valid_background,
1445 "ValidBackground latches at once"
1446 );
1447 let bg = state.background.as_ref().unwrap();
1448 assert_eq!(
1449 bg.as_slice(),
1450 &[20.0, 40.0, 60.0],
1451 "background is the OUTPUT array"
1452 );
1453
1454 let _ = state.process(&make_array(&[1, 2, 3]));
1456 assert_eq!(
1457 state.background.as_ref().unwrap().as_slice(),
1458 &[20.0, 40.0, 60.0]
1459 );
1460 }
1461
1462 #[test]
1463 fn test_r9_68_save_flat_field_copies_the_last_output_synchronously() {
1464 let mut state = ProcessState::new(ProcessConfig {
1466 enable_offset_scale: true,
1467 offset: 1.0,
1468 scale: 1.0,
1469 output_type: Some(NDDataType::Float64),
1470 ..Default::default()
1471 });
1472
1473 state.save_flat_field();
1474 assert!(state.flat_field.is_none());
1475 assert!(!state.config.valid_flat_field);
1476
1477 let _ = state.process(&make_array(&[50, 100, 150])).unwrap();
1479 state.save_flat_field();
1480
1481 assert!(state.config.valid_flat_field);
1482 assert_eq!(
1483 state.flat_field.as_ref().unwrap().as_slice(),
1484 &[51.0, 101.0, 151.0],
1485 "flat field is the OUTPUT array, not the input"
1486 );
1487
1488 let _ = state.process(&make_array(&[7, 7, 7]));
1489 assert_eq!(
1490 state.flat_field.as_ref().unwrap().as_slice(),
1491 &[51.0, 101.0, 151.0]
1492 );
1493 }
1494
1495 #[test]
1496 fn test_r9_68_save_background_write_of_zero_still_saves() {
1497 use ad_core_rs::plugin::runtime::{ParamChangeValue, ParamUpdate, PluginParamSnapshot};
1501 use asyn_rs::port::{PortDriverBase, PortFlags};
1502
1503 let mut proc = ProcessProcessor::new(ProcessConfig {
1504 output_type: Some(NDDataType::Float64),
1505 ..Default::default()
1506 });
1507
1508 let mut base = PortDriverBase::new("R9_68", 1, PortFlags::default());
1509 proc.register_params(&mut base).unwrap();
1510 let pool = NDArrayPool::new(1_000_000);
1511 let _ = proc.process_array(&make_array(&[4, 5, 6]), &pool);
1512
1513 let reason = proc.params.save_background.unwrap();
1514 let valid = proc.params.valid_background.unwrap();
1515 let snapshot = PluginParamSnapshot {
1516 enable_callbacks: true,
1517 reason,
1518 addr: 0,
1519 value: ParamChangeValue::Int32(0),
1520 };
1521 let result = proc.on_param_change(reason, &snapshot);
1522
1523 assert_eq!(
1524 proc.state.background.as_ref().unwrap().as_slice(),
1525 &[4.0, 5.0, 6.0],
1526 "a 0 write saves the background too"
1527 );
1528 let int_update = |r: usize| {
1530 result.param_updates.iter().find_map(|u| match u {
1531 ParamUpdate::Int32 {
1532 reason: ur, value, ..
1533 } if *ur == r => Some(*value),
1534 _ => None,
1535 })
1536 };
1537 assert_eq!(int_update(reason), Some(0), "SaveBackground echoes 0");
1538 assert_eq!(
1539 int_update(valid),
1540 Some(1),
1541 "ValidBackground latches on the write"
1542 );
1543 }
1544
1545 #[test]
1546 fn test_auto_reset_when_num_filter_reached() {
1547 let mut state = ProcessState::new(ProcessConfig {
1549 enable_filter: true,
1550 filter: FilterConfig {
1551 num_filter: 3,
1552 auto_reset: true,
1553 fc: [1.0, 0.0, 1.0, 0.0], oc: [1.0, 0.0, 0.0, 0.0],
1555 rc: [0.0, 1.0],
1556 ..Default::default()
1557 },
1558 output_type: Some(NDDataType::Float64),
1559 ..Default::default()
1560 });
1561
1562 let _ = state.process(&make_f64_array(&[100.0]));
1564 assert_eq!(state.num_filtered, 1);
1565
1566 let _ = state.process(&make_f64_array(&[100.0]));
1568 assert_eq!(state.num_filtered, 2);
1569
1570 let _ = state.process(&make_f64_array(&[100.0]));
1572 assert_eq!(state.num_filtered, 3);
1573
1574 let _ = state.process(&make_f64_array(&[200.0]));
1576 assert_eq!(state.num_filtered, 1, "fresh start after auto reset");
1578 }
1579
1580 #[test]
1581 fn test_filter_with_offset_scale() {
1582 let mut state = ProcessState::new(ProcessConfig {
1586 enable_filter: true,
1587 filter: FilterConfig {
1588 num_filter: 10,
1589 fc: [0.0, 0.0, 1.0, 0.0], oc: [1.0, 0.0, 0.0, 0.0], rc: [0.0, 1.0],
1592 f_offset: 10.0,
1593 f_scale: 2.0,
1594 o_offset: 5.0,
1595 o_scale: 3.0,
1596 ..Default::default()
1597 },
1598 output_type: Some(NDDataType::Float64),
1599 ..Default::default()
1600 });
1601
1602 let r0 = state.process(&make_f64_array(&[50.0])).unwrap();
1612 let v0 = r0.data.get_as_f64(0).unwrap();
1613 assert!((v0 - 155.0).abs() < 1e-9, "frame 0: got {v0}");
1614
1615 let r1 = state.process(&make_f64_array(&[20.0])).unwrap();
1620 let v1 = r1.data.get_as_f64(0).unwrap();
1621 assert!((v1 - 335.0).abs() < 1e-9, "frame 1: got {v1}");
1622 }
1623
1624 #[test]
1625 fn test_reset_filter_manual() {
1626 let mut state = ProcessState::new(ProcessConfig {
1627 enable_filter: true,
1628 filter: FilterConfig {
1629 num_filter: 10,
1630 fc: [1.0, 0.0, 1.0, 0.0],
1631 oc: [1.0, 0.0, 0.0, 0.0],
1632 rc: [0.0, 1.0],
1633 ..Default::default()
1634 },
1635 output_type: Some(NDDataType::Float64),
1636 ..Default::default()
1637 });
1638
1639 let _ = state.process(&make_f64_array(&[100.0]));
1641 let _ = state.process(&make_f64_array(&[100.0]));
1642 assert!(state.filter_state.is_some());
1643 assert_eq!(state.num_filtered, 2);
1644
1645 state.reset_filter();
1649 assert!(
1650 state.filter_state.is_some(),
1651 "buffer must survive the reset"
1652 );
1653 assert_eq!(state.num_filtered, 2);
1654
1655 let _ = state.process(&make_f64_array(&[200.0]));
1657 assert_eq!(state.num_filtered, 1);
1658 }
1659
1660 #[test]
1661 fn test_r6_69_manual_reset_keeps_previous_filter_contents() {
1662 let cfg = || ProcessConfig {
1672 enable_filter: true,
1673 filter: FilterConfig {
1674 num_filter: 10,
1675 fc: [0.0, 0.0, 1.0, 0.0],
1676 oc: [1.0, 0.0, 0.0, 0.0],
1677 rc: [0.5, 2.0], r_offset: 1.0,
1679 ..Default::default()
1680 },
1681 output_type: Some(NDDataType::Float64),
1682 ..Default::default()
1683 };
1684
1685 let mut state = ProcessState::new(cfg());
1686 let _ = state.process(&make_f64_array(&[100.0]));
1689 assert_eq!(state.filter_state.as_ref().unwrap()[0], 100.0);
1690
1691 state.reset_filter();
1693 let out = state.process(&make_f64_array(&[10.0])).unwrap();
1694
1695 assert_eq!(out.data.get_as_f64(0).unwrap(), 71.0);
1699 assert_eq!(state.num_filtered, 1);
1700 }
1703
1704 #[test]
1705 fn test_r6_69_element_count_mismatch_frees_the_buffer() {
1706 let mut state = ProcessState::new(ProcessConfig {
1709 enable_filter: true,
1710 filter: FilterConfig {
1711 num_filter: 10,
1712 fc: [0.0, 0.0, 1.0, 0.0],
1713 oc: [1.0, 0.0, 0.0, 0.0],
1714 rc: [0.5, 2.0],
1715 r_offset: 1.0,
1716 ..Default::default()
1717 },
1718 output_type: Some(NDDataType::Float64),
1719 ..Default::default()
1720 });
1721
1722 let _ = state.process(&make_f64_array(&[100.0]));
1723 assert_eq!(state.filter_state.as_ref().unwrap().len(), 1);
1724
1725 let out = state.process(&make_f64_array(&[10.0, 10.0])).unwrap();
1729 assert_eq!(state.filter_state.as_ref().unwrap().len(), 2);
1730 assert_eq!(out.data.get_as_f64(0).unwrap(), 26.0);
1731 assert_eq!(state.num_filtered, 1);
1732 }
1733
1734 #[test]
1735 fn test_adp6_auto_offset_scale_arms_next_frame_not_trigger() {
1736 let mut state = ProcessState::new(ProcessConfig {
1741 output_type: Some(NDDataType::UInt8),
1742 ..Default::default()
1743 });
1744 state.config.auto_offset_scale_pending = true;
1745
1746 let out1 = state.process(&make_f64_array(&[10.0, 20.0, 30.0])).unwrap();
1749 assert!(!state.config.auto_offset_scale_pending); if let NDDataBuffer::U8(v) = &out1.data {
1751 assert_eq!(v, &[10, 20, 30]); } else {
1753 panic!("expected u8 output");
1754 }
1755 assert!(state.config.enable_offset_scale);
1758 assert!((state.config.offset - (-10.0)).abs() < 1e-9);
1759 assert!((state.config.scale - 255.0 / 20.0).abs() < 1e-9);
1760
1761 let out2 = state.process(&make_f64_array(&[10.0, 20.0, 30.0])).unwrap();
1763 if let NDDataBuffer::U8(v) = &out2.data {
1764 assert_eq!(v[0], 0); assert_eq!(v[2], 255); } else {
1767 panic!("expected u8 output");
1768 }
1769 }
1770
1771 #[test]
1772 fn test_filter_callbacks_drops_suppressed_frame() {
1773 let mut state = ProcessState::new(ProcessConfig {
1777 enable_filter: true,
1778 filter: FilterConfig {
1779 num_filter: 3,
1780 filter_callbacks: 1,
1781 fc: [1.0, 0.0, 1.0, 0.0],
1782 oc: [0.0, 1.0, 0.0, 0.0],
1783 rc: [0.0, 1.0],
1784 ..Default::default()
1785 },
1786 output_type: Some(NDDataType::Float64),
1787 ..Default::default()
1788 });
1789
1790 assert!(state.process(&make_f64_array(&[100.0])).is_none());
1792 assert!(state.process(&make_f64_array(&[100.0])).is_none());
1793 assert!(state.process(&make_f64_array(&[100.0])).is_some());
1795 }
1796
1797 #[test]
1798 fn test_filter_recurrence_matches_cpp() {
1799 let mut state = ProcessState::new(ProcessConfig {
1827 enable_filter: true,
1828 filter: FilterConfig {
1829 num_filter: 100,
1830 fc: [1.0, 0.0, 1.0, 0.0],
1831 oc: [0.0, 1.0, 0.0, 0.0],
1832 rc: [0.0, 1.0],
1833 ..Default::default()
1834 },
1835 output_type: Some(NDDataType::Float64),
1836 ..Default::default()
1837 });
1838
1839 let inputs = [100.0, 200.0, 300.0, 400.0];
1840 let expected_data = [100.0, 100.0, 400.0 / 3.0, 175.0];
1841 let expected_filter = [200.0, 400.0, 700.0, 1100.0];
1842
1843 for k in 0..inputs.len() {
1844 let r = state.process(&make_f64_array(&[inputs[k]])).unwrap();
1845 let v = r.data.get_as_f64(0).unwrap();
1846 assert!(
1847 (v - expected_data[k]).abs() < 1e-9,
1848 "frame {k}: data got {v}, expected {}",
1849 expected_data[k]
1850 );
1851 let fs = state.filter_state.as_ref().unwrap()[0];
1852 assert!(
1853 (fs - expected_filter[k]).abs() < 1e-9,
1854 "frame {k}: filter got {fs}, expected {}",
1855 expected_filter[k]
1856 );
1857 }
1858 }
1859 #[test]
1871 fn r12_63_a_zero_coefficient_drops_its_term_instead_of_multiplying_it() {
1872 let input = make_f64_array(&[1.0, f64::NAN, 3.0]);
1873
1874 let mut state = ProcessState::new(ProcessConfig {
1875 enable_filter: true,
1876 filter: FilterConfig {
1877 num_filter: 2,
1878 rc: [0.0, 0.0],
1879 r_offset: 5.0,
1880 oc: [1.0, 0.0, 0.0, 0.0],
1881 fc: [1.0, 0.0, 0.0, 0.0],
1882 ..Default::default()
1883 },
1884 ..Default::default()
1885 });
1886
1887 let result = state.process(&input).unwrap();
1888 let NDDataBuffer::F64(ref v) = result.data else {
1889 panic!("expected an F64 output buffer, got {:?}", result.data);
1890 };
1891 assert_eq!(
1892 v.as_slice(),
1893 [5.0, 5.0, 5.0],
1894 "RC1=RC2=0 makes C's reset `filter[i] = rOffset`; O2=0 drops the NaN \
1895 data term. Every element is rOffset — 0.0 * NaN must not be summed in"
1896 );
1897
1898 let clean = make_f64_array(&[7.0, 8.0, 9.0]);
1901 let result = state.process(&clean).unwrap();
1902 let NDDataBuffer::F64(ref v) = result.data else {
1903 panic!("expected an F64 output buffer");
1904 };
1905 assert!(
1906 v.iter().all(|x| x.is_finite()),
1907 "the NaN must not survive in filter[] across frames: {v:?}"
1908 );
1909 }
1910}