1use std::sync::Arc;
2
3use ad_core_rs::ndarray::{NDArray, NDDataBuffer, NDDataType, NDDimension};
4use ad_core_rs::ndarray_pool::NDArrayPool;
5use ad_core_rs::plugin::runtime::{NDPluginProcess, ProcessResult};
6use parking_lot::Mutex;
7use rustfft::num_complex::Complex;
8use rustfft::{Fft, FftPlanner};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum FFTDirection {
13 Forward,
14 Inverse,
15}
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub struct FFTConfig {
25 pub direction: FFTDirection,
26 pub suppress_dc: bool,
28 pub num_average: usize,
30}
31
32impl Default for FFTConfig {
33 fn default() -> Self {
34 Self {
35 direction: FFTDirection::Forward,
36 suppress_dc: false,
37 num_average: 0,
38 }
39 }
40}
41
42pub fn next_pow2(n: usize) -> usize {
46 if n <= 1 {
47 return 1;
48 }
49 let mut p = 1usize;
50 while p < n {
51 p <<= 1;
52 }
53 p
54}
55
56pub fn fft_1d_rows(src: &NDArray, suppress_dc: bool) -> Option<NDArray> {
62 if src.dims.is_empty() {
63 return None;
64 }
65
66 let width = src.dims[0].size;
67 let height = if src.dims.len() >= 2 {
68 src.dims[1].size
69 } else {
70 1
71 };
72
73 if width == 0 {
74 return None;
75 }
76
77 let padded = next_pow2(width);
79
80 let mut planner = FftPlanner::<f64>::new();
81 let fft = planner.plan_fft_forward(padded);
82
83 let n_freq = padded / 2;
85 if n_freq == 0 {
86 return None;
87 }
88 let scale = 1.0 / padded as f64;
89
90 let mut magnitudes = vec![0.0f64; n_freq * height];
91 let mut row_buf = vec![Complex::new(0.0, 0.0); padded];
92
93 for row in 0..height {
94 for c in row_buf.iter_mut() {
96 *c = Complex::new(0.0, 0.0);
97 }
98 for i in 0..width {
99 row_buf[i] = Complex::new(src.data.get_as_f64(row * width + i).unwrap_or(0.0), 0.0);
100 }
101
102 fft.process(&mut row_buf);
103
104 for i in 0..n_freq {
106 magnitudes[row * n_freq + i] = row_buf[i].norm() * scale;
107 }
108
109 if suppress_dc {
110 magnitudes[row * n_freq] = 0.0;
111 }
112 }
113
114 let dims = if height > 1 {
115 vec![NDDimension::new(n_freq), NDDimension::new(height)]
116 } else {
117 vec![NDDimension::new(n_freq)]
118 };
119 let mut arr = NDArray::new(dims, NDDataType::Float64);
120 arr.data = NDDataBuffer::F64(magnitudes);
121 arr.unique_id = src.unique_id;
122 arr.timestamp = src.timestamp;
123 arr.attributes = src.attributes.clone();
124 Some(arr)
125}
126
127pub fn fft_2d(src: &NDArray, suppress_dc: bool) -> Option<NDArray> {
129 if src.dims.len() < 2 {
130 return None;
131 }
132
133 let src_w = src.dims[0].size;
134 let src_h = src.dims[1].size;
135
136 if src_w == 0 || src_h == 0 {
137 return None;
138 }
139
140 let w = next_pow2(src_w);
142 let h = next_pow2(src_h);
143
144 let mut planner = FftPlanner::<f64>::new();
145 let fft_row = planner.plan_fft_forward(w);
146 let fft_col = planner.plan_fft_forward(h);
147
148 let mut data = vec![Complex::new(0.0, 0.0); w * h];
150 let mut row_buf = vec![Complex::new(0.0, 0.0); w];
151
152 for row in 0..src_h {
153 for c in row_buf.iter_mut() {
154 *c = Complex::new(0.0, 0.0);
155 }
156 for i in 0..src_w {
157 row_buf[i] = Complex::new(src.data.get_as_f64(row * src_w + i).unwrap_or(0.0), 0.0);
158 }
159 fft_row.process(&mut row_buf);
160 data[row * w..(row * w + w)].copy_from_slice(&row_buf);
161 }
162
163 let mut col_buf = vec![Complex::new(0.0, 0.0); h];
165
166 for col in 0..w {
167 for row in 0..h {
169 col_buf[row] = data[row * w + col];
170 }
171 fft_col.process(&mut col_buf);
172 for row in 0..h {
174 data[row * w + col] = col_buf[row];
175 }
176 }
177
178 let n_freq_x = w / 2;
180 let n_freq_y = h / 2;
181 if n_freq_x == 0 || n_freq_y == 0 {
182 return None;
183 }
184 let scale = 1.0 / (w * h) as f64;
185
186 let mut magnitudes = vec![0.0f64; n_freq_x * n_freq_y];
187 for fy in 0..n_freq_y {
188 for fx in 0..n_freq_x {
189 magnitudes[fy * n_freq_x + fx] = data[fy * w + fx].norm() * scale;
190 }
191 }
192
193 if suppress_dc {
194 magnitudes[0] = 0.0;
195 }
196
197 let dims = vec![NDDimension::new(n_freq_x), NDDimension::new(n_freq_y)];
198 let mut arr = NDArray::new(dims, NDDataType::Float64);
199 arr.data = NDDataBuffer::F64(magnitudes);
200 arr.unique_id = src.unique_id;
201 arr.timestamp = src.timestamp;
202 arr.attributes = src.attributes.clone();
203 Some(arr)
204}
205
206#[derive(Default)]
208struct FFTParamIndices {
209 direction: Option<usize>,
210 suppress_dc: Option<usize>,
211 num_average: Option<usize>,
212 num_averaged: Option<usize>,
213 reset_average: Option<usize>,
214 time_per_point: Option<usize>,
215 time_series: Option<usize>,
217 real: Option<usize>,
219 imaginary: Option<usize>,
221 abs_value: Option<usize>,
223 time_axis: Option<usize>,
225 freq_axis: Option<usize>,
227}
228
229struct FFTState {
238 config: FFTConfig,
239 avg_buffer: Option<Vec<f64>>,
241 avg_count: usize,
243 cached_dims: Vec<usize>,
245 time_per_point: f64,
248}
249
250pub struct FFTProcessor {
251 state: Mutex<FFTState>,
252 planner: Mutex<FftPlanner<f64>>,
256 params: FFTParamIndices,
257}
258
259impl FFTProcessor {
260 pub fn new() -> Self {
261 Self::with_config(FFTConfig::default())
262 }
263
264 pub fn with_config(config: FFTConfig) -> Self {
265 Self {
266 state: Mutex::new(FFTState {
267 config,
268 avg_buffer: None,
269 avg_count: 0,
270 cached_dims: Vec::new(),
271 time_per_point: 1.0,
272 }),
273 planner: Mutex::new(FftPlanner::new()),
274 params: FFTParamIndices::default(),
275 }
276 }
277}
278
279impl FFTState {
280 fn check_dims_changed(&mut self, dims: &[NDDimension]) {
282 let current: Vec<usize> = dims.iter().map(|d| d.size).collect();
283 if current != self.cached_dims {
284 self.cached_dims = current;
285 self.avg_buffer = None;
286 self.avg_count = 0;
287 }
288 }
289
290 fn apply_averaging(&mut self, magnitudes: &[f64]) -> Vec<f64> {
295 let num_avg = self.config.num_average;
296 if num_avg <= 1 {
297 return magnitudes.to_vec();
298 }
299
300 let buf = self
301 .avg_buffer
302 .get_or_insert_with(|| vec![0.0; magnitudes.len()]);
303
304 if buf.len() != magnitudes.len() {
306 *buf = vec![0.0; magnitudes.len()];
307 self.avg_count = 0;
308 }
309
310 self.avg_count += 1;
311 let n = self.avg_count.min(num_avg) as f64;
313 let new_fraction = 1.0 / n;
314 let old_fraction = 1.0 - new_fraction;
315
316 for (b, &m) in buf.iter_mut().zip(magnitudes.iter()) {
318 *b = *b * old_fraction + m * new_fraction;
319 }
320
321 buf.clone()
322 }
323}
324
325struct FFTFrame<'a> {
335 config: FFTConfig,
336 time_per_point: f64,
337 planner: &'a Mutex<FftPlanner<f64>>,
338}
339
340impl FFTFrame<'_> {
341 fn plan_forward(&self, len: usize) -> Arc<dyn Fft<f64>> {
344 self.planner.lock().plan_fft_forward(len)
345 }
346
347 fn plan_inverse(&self, len: usize) -> Arc<dyn Fft<f64>> {
349 self.planner.lock().plan_fft_inverse(len)
350 }
351
352 fn compute_fft(&self, src: &NDArray) -> Option<NDArray> {
359 let suppress_dc = self.config.suppress_dc;
360
361 match (src.dims.len(), self.config.direction) {
362 (1, FFTDirection::Forward) => self.compute_fft_1d_rows_forward(src, suppress_dc),
363 (1, FFTDirection::Inverse) => self.compute_fft_1d_rows_inverse(src, suppress_dc),
364 (2, FFTDirection::Forward) => self.compute_fft_2d_forward(src, suppress_dc),
365 (2, FFTDirection::Inverse) => self.compute_fft_2d_inverse(src, suppress_dc),
366 _ => None,
367 }
368 }
369
370 fn compute_row_spectrum(
382 &self,
383 src: &NDArray,
384 suppress_dc: bool,
385 ) -> Option<(Vec<f64>, Vec<f64>, Vec<f64>)> {
386 if src.dims.is_empty() {
387 return None;
388 }
389 let width = src.dims[0].size;
390 if width == 0 {
391 return None;
392 }
393 let padded = next_pow2(width);
394 let n_freq = padded / 2;
395 if n_freq == 0 {
396 return None;
397 }
398 let fft = self.plan_forward(padded);
399
400 let mut time_series = vec![0.0f64; padded];
404 for (i, slot) in time_series.iter_mut().enumerate().take(width) {
405 *slot = src.data.get_as_f64(i).unwrap_or(0.0);
406 }
407
408 let mut row_buf = vec![Complex::new(0.0, 0.0); padded];
409 for (i, &v) in time_series.iter().enumerate() {
410 row_buf[i] = Complex::new(v, 0.0);
411 }
412 fft.process(&mut row_buf);
413
414 let mut real = vec![0.0f64; n_freq];
415 let mut imag = vec![0.0f64; n_freq];
416 for i in 0..n_freq {
417 real[i] = row_buf[i].re;
418 imag[i] = row_buf[i].im;
419 }
420 if suppress_dc {
421 real[0] = 0.0;
422 imag[0] = 0.0;
423 }
424 Some((time_series, real, imag))
425 }
426
427 fn freq_axis(&self, n_freq: usize) -> Vec<f64> {
430 if n_freq <= 1 {
431 return vec![0.0; n_freq];
432 }
433 let tpp = if self.time_per_point > 0.0 {
434 self.time_per_point
435 } else {
436 1.0
437 };
438 let step = 0.5 / tpp / (n_freq - 1) as f64;
439 (0..n_freq).map(|i| i as f64 * step).collect()
440 }
441
442 fn time_axis(&self, n_time: usize) -> Vec<f64> {
444 let tpp = if self.time_per_point > 0.0 {
445 self.time_per_point
446 } else {
447 1.0
448 };
449 (0..n_time).map(|i| i as f64 * tpp).collect()
450 }
451
452 fn compute_fft_1d_rows_forward(&self, src: &NDArray, suppress_dc: bool) -> Option<NDArray> {
453 if src.dims.is_empty() {
454 return None;
455 }
456
457 let width = src.dims[0].size;
458 let height = if src.dims.len() >= 2 {
459 src.dims[1].size
460 } else {
461 1
462 };
463
464 if width == 0 {
465 return None;
466 }
467
468 let padded = next_pow2(width);
470 let fft = self.plan_forward(padded);
471
472 let n_freq = padded / 2;
474 if n_freq == 0 {
475 return None;
476 }
477 let scale = 1.0 / padded as f64;
478
479 let mut magnitudes = vec![0.0f64; n_freq * height];
480 let mut row_buf = vec![Complex::new(0.0, 0.0); padded];
481
482 for row in 0..height {
483 for c in row_buf.iter_mut() {
484 *c = Complex::new(0.0, 0.0);
485 }
486 for i in 0..width {
487 row_buf[i] = Complex::new(src.data.get_as_f64(row * width + i).unwrap_or(0.0), 0.0);
488 }
489 fft.process(&mut row_buf);
490 for i in 0..n_freq {
491 magnitudes[row * n_freq + i] = row_buf[i].norm() * scale;
492 }
493 if suppress_dc {
494 magnitudes[row * n_freq] = 0.0;
495 }
496 }
497
498 let dims = if height > 1 {
499 vec![NDDimension::new(n_freq), NDDimension::new(height)]
500 } else {
501 vec![NDDimension::new(n_freq)]
502 };
503 let mut arr = NDArray::new(dims, NDDataType::Float64);
504 arr.data = NDDataBuffer::F64(magnitudes);
505 arr.unique_id = src.unique_id;
506 arr.timestamp = src.timestamp;
507 arr.attributes = src.attributes.clone();
508 Some(arr)
509 }
510
511 fn compute_fft_1d_rows_inverse(&self, src: &NDArray, suppress_dc: bool) -> Option<NDArray> {
512 if src.dims.is_empty() {
513 return None;
514 }
515
516 let width = src.dims[0].size;
517 let height = if src.dims.len() >= 2 {
518 src.dims[1].size
519 } else {
520 1
521 };
522
523 if width == 0 {
524 return None;
525 }
526
527 let fft = self.plan_inverse(width);
528 let scale = 1.0 / width as f64;
529
530 let mut samples = vec![0.0f64; width * height];
534 let mut row_buf = vec![Complex::new(0.0, 0.0); width];
535
536 for row in 0..height {
537 for i in 0..width {
538 row_buf[i] = Complex::new(src.data.get_as_f64(row * width + i).unwrap_or(0.0), 0.0);
539 }
540 if suppress_dc {
541 row_buf[0] = Complex::new(0.0, 0.0);
542 }
543 fft.process(&mut row_buf);
544 for (i, c) in row_buf.iter().enumerate() {
545 samples[row * width + i] = c.re * scale;
546 }
547 }
548
549 let dims = src.dims.clone();
550 let mut arr = NDArray::new(dims, NDDataType::Float64);
551 arr.data = NDDataBuffer::F64(samples);
552 arr.unique_id = src.unique_id;
553 arr.timestamp = src.timestamp;
554 arr.attributes = src.attributes.clone();
555 Some(arr)
556 }
557
558 fn compute_fft_2d_forward(&self, src: &NDArray, suppress_dc: bool) -> Option<NDArray> {
559 if src.dims.len() < 2 {
560 return None;
561 }
562
563 let src_w = src.dims[0].size;
564 let src_h = src.dims[1].size;
565
566 if src_w == 0 || src_h == 0 {
567 return None;
568 }
569
570 let w = next_pow2(src_w);
572 let h = next_pow2(src_h);
573
574 let fft_row = self.plan_forward(w);
575 let fft_col = self.plan_forward(h);
576
577 let mut data = vec![Complex::new(0.0, 0.0); w * h];
578 let mut row_buf = vec![Complex::new(0.0, 0.0); w];
579
580 for row in 0..src_h {
581 for c in row_buf.iter_mut() {
582 *c = Complex::new(0.0, 0.0);
583 }
584 for i in 0..src_w {
585 row_buf[i] = Complex::new(src.data.get_as_f64(row * src_w + i).unwrap_or(0.0), 0.0);
586 }
587 fft_row.process(&mut row_buf);
588 data[row * w..(row * w + w)].copy_from_slice(&row_buf);
589 }
590
591 let mut col_buf = vec![Complex::new(0.0, 0.0); h];
592 for col in 0..w {
593 for row in 0..h {
594 col_buf[row] = data[row * w + col];
595 }
596 fft_col.process(&mut col_buf);
597 for row in 0..h {
598 data[row * w + col] = col_buf[row];
599 }
600 }
601
602 let n_freq_x = w / 2;
604 let n_freq_y = h / 2;
605 if n_freq_x == 0 || n_freq_y == 0 {
606 return None;
607 }
608 let scale = 1.0 / (w * h) as f64;
609
610 let mut magnitudes = vec![0.0f64; n_freq_x * n_freq_y];
611 for fy in 0..n_freq_y {
612 for fx in 0..n_freq_x {
613 magnitudes[fy * n_freq_x + fx] = data[fy * w + fx].norm() * scale;
614 }
615 }
616
617 if suppress_dc {
618 magnitudes[0] = 0.0;
619 }
620
621 let dims = vec![NDDimension::new(n_freq_x), NDDimension::new(n_freq_y)];
622 let mut arr = NDArray::new(dims, NDDataType::Float64);
623 arr.data = NDDataBuffer::F64(magnitudes);
624 arr.unique_id = src.unique_id;
625 arr.timestamp = src.timestamp;
626 arr.attributes = src.attributes.clone();
627 Some(arr)
628 }
629
630 fn compute_fft_2d_inverse(&self, src: &NDArray, suppress_dc: bool) -> Option<NDArray> {
631 if src.dims.len() < 2 {
632 return None;
633 }
634
635 let w = src.dims[0].size;
636 let h = src.dims[1].size;
637
638 if w == 0 || h == 0 {
639 return None;
640 }
641
642 let fft_row = self.plan_inverse(w);
643 let fft_col = self.plan_inverse(h);
644 let scale = 1.0 / (w * h) as f64;
645
646 let mut data = vec![Complex::new(0.0, 0.0); w * h];
647 for i in 0..w * h {
648 data[i] = Complex::new(src.data.get_as_f64(i).unwrap_or(0.0), 0.0);
649 }
650
651 if suppress_dc {
652 data[0] = Complex::new(0.0, 0.0);
653 }
654
655 let mut col_buf = vec![Complex::new(0.0, 0.0); h];
656 for col in 0..w {
657 for row in 0..h {
658 col_buf[row] = data[row * w + col];
659 }
660 fft_col.process(&mut col_buf);
661 for row in 0..h {
662 data[row * w + col] = col_buf[row];
663 }
664 }
665
666 let mut row_buf = vec![Complex::new(0.0, 0.0); w];
667 for row in 0..h {
668 row_buf.copy_from_slice(&data[row * w..(row * w + w)]);
669 fft_row.process(&mut row_buf);
670 data[row * w..(row * w + w)].copy_from_slice(&row_buf);
671 }
672
673 let samples: Vec<f64> = data.iter().map(|c| c.re * scale).collect();
675
676 let dims = vec![NDDimension::new(w), NDDimension::new(h)];
677 let mut arr = NDArray::new(dims, NDDataType::Float64);
678 arr.data = NDDataBuffer::F64(samples);
679 arr.unique_id = src.unique_id;
680 arr.timestamp = src.timestamp;
681 arr.attributes = src.attributes.clone();
682 Some(arr)
683 }
684}
685
686impl Default for FFTProcessor {
687 fn default() -> Self {
688 Self::new()
689 }
690}
691
692impl NDPluginProcess for FFTProcessor {
693 fn process_array(&self, array: &NDArray, _pool: &NDArrayPool) -> ProcessResult {
694 use ad_core_rs::plugin::runtime::ParamUpdate;
695
696 let rank = array.dims.len();
701 if rank != 1 && rank != 2 {
702 return ProcessResult::sink(Vec::new());
703 }
704
705 let (frame, avg_count) = {
711 let mut state = self.state.lock();
712 state.check_dims_changed(&array.dims);
713 (
714 FFTFrame {
715 config: state.config,
716 time_per_point: state.time_per_point,
717 planner: &self.planner,
718 },
719 state.avg_count,
720 )
721 };
722
723 let result = frame.compute_fft(array);
724 let mut updates = Vec::new();
725 if let Some(idx) = self.params.num_averaged {
726 updates.push(ParamUpdate::int32(idx, avg_count as i32));
727 }
728
729 let mut averaged_mags: Option<Vec<f64>> = None;
738 if frame.config.direction == FFTDirection::Forward {
739 let suppress_dc = frame.config.suppress_dc;
740 if let Some((time_series, real, imag)) = frame.compute_row_spectrum(array, suppress_dc)
741 {
742 let n_time = time_series.len();
743 let n_freq = real.len();
744 if let Some(idx) = self.params.time_series {
745 updates.push(ParamUpdate::float64_array(idx, time_series));
746 }
747 if let Some(idx) = self.params.real {
748 updates.push(ParamUpdate::float64_array(idx, real));
749 }
750 if let Some(idx) = self.params.imaginary {
751 updates.push(ParamUpdate::float64_array(idx, imag));
752 }
753 if let Some(idx) = self.params.time_axis {
754 updates.push(ParamUpdate::float64_array(idx, frame.time_axis(n_time)));
755 }
756 if let Some(idx) = self.params.freq_axis {
757 updates.push(ParamUpdate::float64_array(idx, frame.freq_axis(n_freq)));
758 }
759 }
760 }
761
762 match result {
763 Some(mut out) => {
764 if let NDDataBuffer::F64(ref mags) = out.data {
771 let mut state = self.state.lock();
772 if state.config.num_average > 1 {
773 let averaged = state.apply_averaging(mags);
774 drop(state);
775 averaged_mags = Some(averaged.clone());
776 out.data = NDDataBuffer::F64(averaged);
777 }
778 }
779 if frame.config.direction == FFTDirection::Forward {
783 if let Some(idx) = self.params.abs_value {
784 let abs = match (&averaged_mags, &out.data) {
785 (Some(avg), _) => avg.clone(),
786 (None, NDDataBuffer::F64(mags)) => mags.clone(),
787 _ => Vec::new(),
788 };
789 if !abs.is_empty() {
790 updates.push(ParamUpdate::float64_array(idx, abs));
791 }
792 }
793 }
794 let mut r = ProcessResult::arrays(vec![Arc::new(out)]);
795 r.param_updates = updates;
796 r
797 }
798 None => ProcessResult::sink(updates),
799 }
800 }
801
802 fn plugin_type(&self) -> &str {
803 "NDPluginFFT"
804 }
805
806 fn register_params(
807 &mut self,
808 base: &mut asyn_rs::port::PortDriverBase,
809 ) -> asyn_rs::error::AsynResult<()> {
810 use asyn_rs::param::ParamType;
811 base.create_param("FFT_TIME_PER_POINT", ParamType::Float64)?;
812 base.create_param("FFT_TIME_AXIS", ParamType::Float64Array)?;
813 base.create_param("FFT_FREQ_AXIS", ParamType::Float64Array)?;
814 base.create_param("FFT_DIRECTION", ParamType::Int32)?;
815 base.create_param("FFT_SUPPRESS_DC", ParamType::Int32)?;
816 base.create_param("FFT_NUM_AVERAGE", ParamType::Int32)?;
817 base.create_param("FFT_NUM_AVERAGED", ParamType::Int32)?;
818 base.create_param("FFT_RESET_AVERAGE", ParamType::Int32)?;
819 base.create_param("FFT_TIME_SERIES", ParamType::Float64Array)?;
820 base.create_param("FFT_REAL", ParamType::Float64Array)?;
821 base.create_param("FFT_IMAGINARY", ParamType::Float64Array)?;
822 base.create_param("FFT_ABS_VALUE", ParamType::Float64Array)?;
823
824 self.params.direction = base.find_param("FFT_DIRECTION");
825 self.params.suppress_dc = base.find_param("FFT_SUPPRESS_DC");
826 self.params.num_average = base.find_param("FFT_NUM_AVERAGE");
827 self.params.num_averaged = base.find_param("FFT_NUM_AVERAGED");
828 self.params.reset_average = base.find_param("FFT_RESET_AVERAGE");
829 self.params.time_per_point = base.find_param("FFT_TIME_PER_POINT");
830 self.params.time_series = base.find_param("FFT_TIME_SERIES");
831 self.params.real = base.find_param("FFT_REAL");
832 self.params.imaginary = base.find_param("FFT_IMAGINARY");
833 self.params.abs_value = base.find_param("FFT_ABS_VALUE");
834 self.params.time_axis = base.find_param("FFT_TIME_AXIS");
835 self.params.freq_axis = base.find_param("FFT_FREQ_AXIS");
836 Ok(())
837 }
838
839 fn on_param_change(
840 &self,
841 reason: usize,
842 params: &ad_core_rs::plugin::runtime::PluginParamSnapshot,
843 ) -> ad_core_rs::plugin::runtime::ParamChangeResult {
844 let mut state = self.state.lock();
845 if Some(reason) == self.params.direction {
846 state.config.direction = if params.value.as_i32() == 0 {
847 FFTDirection::Forward
848 } else {
849 FFTDirection::Inverse
850 };
851 } else if Some(reason) == self.params.suppress_dc {
852 state.config.suppress_dc = params.value.as_i32() != 0;
853 } else if Some(reason) == self.params.num_average {
854 state.config.num_average = params.value.as_i32().max(0) as usize;
855 } else if Some(reason) == self.params.reset_average {
856 if params.value.as_i32() != 0 {
857 state.avg_buffer = None;
858 state.avg_count = 0;
859 }
860 } else if Some(reason) == self.params.time_per_point {
861 let v = params.value.as_f64();
863 if v > 0.0 {
864 state.time_per_point = v;
865 }
866 }
867 ad_core_rs::plugin::runtime::ParamChangeResult::updates(vec![])
868 }
869}
870
871#[cfg(test)]
872mod tests {
873 use super::*;
874
875 #[test]
876 fn test_fft_1d_dc() {
877 let mut arr = NDArray::new(vec![NDDimension::new(8)], NDDataType::Float64);
879 if let NDDataBuffer::F64(ref mut v) = arr.data {
880 for i in 0..8 {
881 v[i] = 1.0;
882 }
883 }
884
885 let result = fft_1d_rows(&arr, false).unwrap();
886 assert_eq!(result.dims[0].size, 4);
888 if let NDDataBuffer::F64(ref v) = result.data {
889 assert!((v[0] - 1.0).abs() < 1e-10);
891 assert!(v[1].abs() < 1e-10);
893 }
894 }
895
896 #[test]
897 fn test_fft_1d_sine() {
898 let n = 16;
900 let mut arr = NDArray::new(vec![NDDimension::new(n)], NDDataType::Float64);
901 if let NDDataBuffer::F64(ref mut v) = arr.data {
902 for i in 0..n {
903 v[i] = (2.0 * std::f64::consts::PI * i as f64 / n as f64).sin();
904 }
905 }
906
907 let result = fft_1d_rows(&arr, false).unwrap();
908 assert_eq!(result.dims[0].size, 8);
910 if let NDDataBuffer::F64(ref v) = result.data {
911 assert!(v[0].abs() < 1e-10);
913 assert!((v[1] - 0.5).abs() < 1e-10);
915 assert!(v[2].abs() < 1e-10);
917 }
918 }
919
920 #[test]
921 fn test_fft_2d_dimensions() {
922 let arr = NDArray::new(
923 vec![NDDimension::new(4), NDDimension::new(4)],
924 NDDataType::UInt8,
925 );
926 let result = fft_2d(&arr, false).unwrap();
927 assert_eq!(result.dims[0].size, 2);
929 assert_eq!(result.dims[1].size, 2);
930 assert_eq!(result.data.data_type(), NDDataType::Float64);
931 }
932
933 #[test]
934 fn test_fft_1d_suppress_dc() {
935 let mut arr = NDArray::new(vec![NDDimension::new(8)], NDDataType::Float64);
937 if let NDDataBuffer::F64(ref mut v) = arr.data {
938 for i in 0..8 {
939 v[i] = 1.0;
940 }
941 }
942
943 let result = fft_1d_rows(&arr, true).unwrap();
944 if let NDDataBuffer::F64(ref v) = result.data {
945 assert!((v[0]).abs() < 1e-15);
947 assert!(v[1].abs() < 1e-10);
949 } else {
950 panic!("expected F64 data");
951 }
952 }
953
954 #[test]
955 fn test_fft_2d_suppress_dc() {
956 let mut arr = NDArray::new(
958 vec![NDDimension::new(4), NDDimension::new(4)],
959 NDDataType::Float64,
960 );
961 if let NDDataBuffer::F64(ref mut v) = arr.data {
962 for val in v.iter_mut() {
963 *val = 3.0;
964 }
965 }
966
967 let result = fft_2d(&arr, true).unwrap();
968 if let NDDataBuffer::F64(ref v) = result.data {
969 assert!((v[0]).abs() < 1e-15);
971 } else {
972 panic!("expected F64 data");
973 }
974 }
975
976 #[test]
977 fn test_fft_2d_known_dc() {
978 let mut arr = NDArray::new(
980 vec![NDDimension::new(4), NDDimension::new(4)],
981 NDDataType::Float64,
982 );
983 if let NDDataBuffer::F64(ref mut v) = arr.data {
984 for val in v.iter_mut() {
985 *val = 2.0;
986 }
987 }
988
989 let result = fft_2d(&arr, false).unwrap();
990 assert_eq!(result.dims[0].size, 2);
992 assert_eq!(result.dims[1].size, 2);
993 if let NDDataBuffer::F64(ref v) = result.data {
994 assert!((v[0] - 2.0).abs() < 1e-10, "DC = {}, expected 2", v[0]);
996 for i in 1..v.len() {
998 assert!(v[i].abs() < 1e-10, "bin {} = {}, expected ~0", i, v[i]);
999 }
1000 } else {
1001 panic!("expected F64 data");
1002 }
1003 }
1004
1005 #[test]
1006 fn test_fft_1d_known_cosine_peaks() {
1007 let n = 16;
1009 let mut arr = NDArray::new(vec![NDDimension::new(n)], NDDataType::Float64);
1010 if let NDDataBuffer::F64(ref mut v) = arr.data {
1011 for i in 0..n {
1012 v[i] = (2.0 * std::f64::consts::PI * 3.0 * i as f64 / n as f64).cos();
1013 }
1014 }
1015
1016 let result = fft_1d_rows(&arr, false).unwrap();
1017 assert_eq!(result.dims[0].size, 8);
1019 if let NDDataBuffer::F64(ref v) = result.data {
1020 assert!(v[0].abs() < 1e-10);
1022 assert!(
1024 (v[3] - 0.5).abs() < 1e-10,
1025 "k=3 magnitude = {}, expected 0.5",
1026 v[3]
1027 );
1028 for k in [1, 2, 4, 5, 6, 7] {
1030 assert!(
1031 v[k].abs() < 1e-10,
1032 "k={} magnitude = {}, expected ~0",
1033 k,
1034 v[k]
1035 );
1036 }
1037 } else {
1038 panic!("expected F64 data");
1039 }
1040 }
1041
1042 #[test]
1043 fn test_processor_with_config() {
1044 let config = FFTConfig {
1045 direction: FFTDirection::Forward,
1046 suppress_dc: true,
1047 num_average: 0,
1048 };
1049 let proc = FFTProcessor::with_config(config);
1050 let pool = NDArrayPool::new(0);
1051
1052 let mut arr = NDArray::new(vec![NDDimension::new(8)], NDDataType::Float64);
1053 if let NDDataBuffer::F64(ref mut v) = arr.data {
1054 for i in 0..8 {
1055 v[i] = 5.0;
1056 }
1057 }
1058
1059 let result = proc.process_array(&arr, &pool);
1060 assert_eq!(result.output_arrays.len(), 1);
1061 if let NDDataBuffer::F64(ref v) = result.output_arrays[0].data {
1062 assert!(v[0].abs() < 1e-15);
1064 } else {
1065 panic!("expected F64 data");
1066 }
1067 }
1068
1069 #[test]
1070 fn test_processor_averaging() {
1071 let config = FFTConfig {
1072 direction: FFTDirection::Forward,
1073 suppress_dc: false,
1074 num_average: 2,
1075 };
1076 let proc = FFTProcessor::with_config(config);
1077 let pool = NDArrayPool::new(0);
1078
1079 let mut arr1 = NDArray::new(vec![NDDimension::new(8)], NDDataType::Float64);
1081 if let NDDataBuffer::F64(ref mut v) = arr1.data {
1082 for i in 0..8 {
1083 v[i] = 2.0;
1084 }
1085 }
1086
1087 let mut arr2 = NDArray::new(vec![NDDimension::new(8)], NDDataType::Float64);
1089 if let NDDataBuffer::F64(ref mut v) = arr2.data {
1090 for i in 0..8 {
1091 v[i] = 4.0;
1092 }
1093 }
1094
1095 let r1 = proc.process_array(&arr1, &pool);
1096 assert_eq!(r1.output_arrays.len(), 1);
1097 if let NDDataBuffer::F64(ref v) = r1.output_arrays[0].data {
1099 assert!((v[0] - 2.0).abs() < 1e-10, "partial avg DC = {}", v[0]);
1100 }
1101
1102 let r2 = proc.process_array(&arr2, &pool);
1103 assert_eq!(r2.output_arrays.len(), 1);
1104 if let NDDataBuffer::F64(ref v) = r2.output_arrays[0].data {
1106 assert!((v[0] - 3.0).abs() < 1e-10, "averaged DC = {}", v[0]);
1107 }
1108 }
1109
1110 #[test]
1111 fn test_processor_averaging_dimension_change_resets() {
1112 let config = FFTConfig {
1113 direction: FFTDirection::Forward,
1114 suppress_dc: false,
1115 num_average: 3,
1116 };
1117 let proc = FFTProcessor::with_config(config);
1118 let pool = NDArrayPool::new(0);
1119
1120 let mut arr1 = NDArray::new(vec![NDDimension::new(8)], NDDataType::Float64);
1122 if let NDDataBuffer::F64(ref mut v) = arr1.data {
1123 for i in 0..8 {
1124 v[i] = 1.0;
1125 }
1126 }
1127 let _ = proc.process_array(&arr1, &pool);
1128 assert_eq!(proc.state.lock().avg_count, 1);
1129
1130 let mut arr2 = NDArray::new(vec![NDDimension::new(4)], NDDataType::Float64);
1132 if let NDDataBuffer::F64(ref mut v) = arr2.data {
1133 for i in 0..4 {
1134 v[i] = 1.0;
1135 }
1136 }
1137 let _ = proc.process_array(&arr2, &pool);
1138 assert_eq!(proc.state.lock().avg_count, 1);
1140 }
1141
1142 #[test]
1143 fn test_fft_1d_multirow() {
1144 let w = 4;
1146 let h = 2;
1147 let mut arr = NDArray::new(
1148 vec![NDDimension::new(w), NDDimension::new(h)],
1149 NDDataType::Float64,
1150 );
1151 if let NDDataBuffer::F64(ref mut v) = arr.data {
1152 for i in 0..w {
1154 v[i] = 1.0;
1155 }
1156 for i in w..2 * w {
1158 v[i] = 3.0;
1159 }
1160 }
1161
1162 let result = fft_1d_rows(&arr, false).unwrap();
1163 let n_freq = w / 2; assert_eq!(result.dims[0].size, n_freq);
1165 if let NDDataBuffer::F64(ref v) = result.data {
1166 assert!((v[0] - 1.0).abs() < 1e-10);
1168 assert!((v[n_freq] - 3.0).abs() < 1e-10);
1170 } else {
1171 panic!("expected F64 data");
1172 }
1173 }
1174
1175 #[test]
1176 fn test_inverse_fft_1d() {
1177 let n = 8;
1181 let mut arr = NDArray::new(vec![NDDimension::new(n)], NDDataType::Float64);
1182 if let NDDataBuffer::F64(ref mut v) = arr.data {
1183 v[0] = 8.0; }
1186
1187 let config = FFTConfig {
1188 direction: FFTDirection::Inverse,
1189 suppress_dc: false,
1190 num_average: 0,
1191 };
1192 let proc = FFTProcessor::with_config(config);
1193 let pool = NDArrayPool::new(0);
1194
1195 let result = proc.process_array(&arr, &pool);
1196 assert_eq!(result.output_arrays.len(), 1);
1197 if let NDDataBuffer::F64(ref v) = result.output_arrays[0].data {
1198 for i in 0..n {
1200 assert!(
1201 (v[i] - 1.0).abs() < 1e-10,
1202 "sample {} = {}, expected 1.0",
1203 i,
1204 v[i]
1205 );
1206 }
1207 } else {
1208 panic!("expected F64 data");
1209 }
1210 }
1211
1212 #[test]
1213 fn test_fft_preserves_metadata() {
1214 let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::Float64);
1215 arr.unique_id = 42;
1216 if let NDDataBuffer::F64(ref mut v) = arr.data {
1217 v[0] = 1.0;
1218 }
1219
1220 let result = fft_1d_rows(&arr, false).unwrap();
1221 assert_eq!(result.unique_id, 42);
1222 assert_eq!(result.timestamp, arr.timestamp);
1223 }
1224
1225 #[test]
1226 fn test_next_pow2() {
1227 assert_eq!(next_pow2(0), 1);
1228 assert_eq!(next_pow2(1), 1);
1229 assert_eq!(next_pow2(2), 2);
1230 assert_eq!(next_pow2(3), 4);
1231 assert_eq!(next_pow2(5), 8);
1232 assert_eq!(next_pow2(8), 8);
1233 assert_eq!(next_pow2(100), 128);
1234 }
1235
1236 #[test]
1237 fn test_fft_1d_pads_to_power_of_two() {
1238 let n = 5; let mut arr = NDArray::new(vec![NDDimension::new(n)], NDDataType::Float64);
1242 if let NDDataBuffer::F64(ref mut v) = arr.data {
1243 for i in 0..n {
1244 v[i] = 1.0;
1245 }
1246 }
1247 let result = fft_1d_rows(&arr, false).unwrap();
1248 assert_eq!(result.dims[0].size, 4); }
1250
1251 #[test]
1252 fn test_fft_2d_pads_to_power_of_two() {
1253 let arr = NDArray::new(
1255 vec![NDDimension::new(6), NDDimension::new(3)],
1256 NDDataType::Float64,
1257 );
1258 let result = fft_2d(&arr, false).unwrap();
1259 assert_eq!(result.dims[0].size, 4); assert_eq!(result.dims[1].size, 2); }
1262
1263 #[test]
1264 fn test_adp9_processor_selects_2d_fft_from_input_rank() {
1265 let proc = FFTProcessor::new();
1269 let pool = NDArrayPool::new(0);
1270
1271 let mut arr = NDArray::new(
1272 vec![NDDimension::new(4), NDDimension::new(4)],
1273 NDDataType::Float64,
1274 );
1275 if let NDDataBuffer::F64(ref mut v) = arr.data {
1276 v.iter_mut().for_each(|x| *x = 2.0);
1277 }
1278 let result = proc.process_array(&arr, &pool);
1279 assert_eq!(result.output_arrays.len(), 1);
1280 let out = &result.output_arrays[0];
1281 assert_eq!(out.dims.len(), 2);
1282 assert_eq!(out.dims[0].size, 2); assert_eq!(out.dims[1].size, 2); if let NDDataBuffer::F64(ref v) = out.data {
1285 assert!((v[0] - 2.0).abs() < 1e-10, "DC = {}", v[0]);
1287 }
1288 }
1289
1290 #[test]
1291 fn test_adp9_processor_keeps_1d_input_1d() {
1292 let proc = FFTProcessor::new();
1294 let pool = NDArrayPool::new(0);
1295 let mut arr = NDArray::new(vec![NDDimension::new(8)], NDDataType::Float64);
1296 if let NDDataBuffer::F64(ref mut v) = arr.data {
1297 v.iter_mut().for_each(|x| *x = 1.0);
1298 }
1299 let result = proc.process_array(&arr, &pool);
1300 let out = &result.output_arrays[0];
1301 assert_eq!(out.dims.len(), 1);
1302 assert_eq!(out.dims[0].size, 4); }
1304
1305 #[test]
1306 fn test_adp9_processor_rejects_rank_above_2() {
1307 let proc = fft_proc_with_params(FFTConfig::default());
1310 let pool = NDArrayPool::new(0);
1311 let arr = NDArray::new(
1312 vec![
1313 NDDimension::new(3),
1314 NDDimension::new(4),
1315 NDDimension::new(4),
1316 ],
1317 NDDataType::Float64,
1318 );
1319 let result = proc.process_array(&arr, &pool);
1320 assert_eq!(result.output_arrays.len(), 0);
1321 assert!(
1322 result.param_updates.is_empty(),
1323 "rank>2 must emit no waveforms, got {} updates",
1324 result.param_updates.len()
1325 );
1326 }
1327
1328 use ad_core_rs::plugin::runtime::ParamUpdate;
1331
1332 fn fft_proc_with_params(config: FFTConfig) -> FFTProcessor {
1334 let mut proc = FFTProcessor::with_config(config);
1335 let mut base =
1336 asyn_rs::port::PortDriverBase::new("FFT_TEST", 1, asyn_rs::port::PortFlags::default());
1337 proc.register_params(&mut base).unwrap();
1338 proc
1339 }
1340
1341 fn find_array_update(updates: &[ParamUpdate], reason: usize) -> Option<&[f64]> {
1343 updates.iter().find_map(|u| match u {
1344 ParamUpdate::Float64Array {
1345 reason: r, value, ..
1346 } if *r == reason => Some(value.as_slice()),
1347 _ => None,
1348 })
1349 }
1350
1351 #[test]
1352 fn test_fft_emits_all_waveforms() {
1353 let proc = fft_proc_with_params(FFTConfig::default());
1356 let pool = NDArrayPool::new(0);
1357
1358 let n = 16;
1359 let mut arr = NDArray::new(vec![NDDimension::new(n)], NDDataType::Float64);
1360 if let NDDataBuffer::F64(ref mut v) = arr.data {
1361 for i in 0..n {
1362 v[i] = (2.0 * std::f64::consts::PI * 3.0 * i as f64 / n as f64).cos();
1363 }
1364 }
1365 let result = proc.process_array(&arr, &pool);
1366 let u = &result.param_updates;
1367
1368 for reason in [
1371 proc.params.time_series.unwrap(),
1372 proc.params.real.unwrap(),
1373 proc.params.imaginary.unwrap(),
1374 proc.params.abs_value.unwrap(),
1375 proc.params.time_axis.unwrap(),
1376 proc.params.freq_axis.unwrap(),
1377 ] {
1378 let wf = find_array_update(u, reason)
1379 .unwrap_or_else(|| panic!("missing waveform for reason {reason}"));
1380 assert!(!wf.is_empty(), "waveform {reason} is empty");
1381 }
1382 let array_updates = u
1383 .iter()
1384 .filter(|x| matches!(x, ParamUpdate::Float64Array { .. }))
1385 .count();
1386 assert_eq!(
1387 array_updates, 6,
1388 "expected 6 waveform updates, got {array_updates}"
1389 );
1390 }
1391
1392 #[test]
1393 fn test_fft_real_imaginary_match_spectrum() {
1394 let proc = fft_proc_with_params(FFTConfig::default());
1397 let real_reason = proc.params.real.unwrap();
1398 let imag_reason = proc.params.imaginary.unwrap();
1399 let abs_reason = proc.params.abs_value.unwrap();
1400 let ts_reason = proc.params.time_series.unwrap();
1401 let pool = NDArrayPool::new(0);
1402
1403 let n = 16;
1404 let mut arr = NDArray::new(vec![NDDimension::new(n)], NDDataType::Float64);
1405 if let NDDataBuffer::F64(ref mut v) = arr.data {
1406 for i in 0..n {
1407 v[i] = (2.0 * std::f64::consts::PI * 3.0 * i as f64 / n as f64).cos();
1408 }
1409 }
1410 let result = proc.process_array(&arr, &pool);
1411 let u = &result.param_updates;
1412
1413 let real = find_array_update(u, real_reason).unwrap();
1414 let imag = find_array_update(u, imag_reason).unwrap();
1415 let abs = find_array_update(u, abs_reason).unwrap();
1416 let ts = find_array_update(u, ts_reason).unwrap();
1417
1418 assert_eq!(real.len(), 8);
1420 assert_eq!(imag.len(), 8);
1421 assert!((real[3] - 8.0).abs() < 1e-9, "real[3] = {}", real[3]);
1423 for k in [0usize, 1, 2, 4, 5, 6, 7] {
1424 assert!(real[k].abs() < 1e-9, "real[{k}] = {}", real[k]);
1425 assert!(imag[k].abs() < 1e-9, "imag[{k}] = {}", imag[k]);
1426 }
1427 assert!(imag[3].abs() < 1e-9, "imag[3] = {}", imag[3]);
1429 assert!((abs[3] - 0.5).abs() < 1e-9, "abs[3] = {}", abs[3]);
1431 assert_eq!(ts.len(), n);
1433 assert!((ts[0] - 1.0).abs() < 1e-9);
1434 }
1435
1436 #[test]
1437 fn test_fft_axes_scale_with_time_per_point() {
1438 let proc = fft_proc_with_params(FFTConfig::default());
1440 let time_axis_reason = proc.params.time_axis.unwrap();
1441 let freq_axis_reason = proc.params.freq_axis.unwrap();
1442 let tpp_reason = proc.params.time_per_point.unwrap();
1443 let pool = NDArrayPool::new(0);
1444
1445 use ad_core_rs::plugin::runtime::{ParamChangeValue, PluginParamSnapshot};
1447 proc.on_param_change(
1448 tpp_reason,
1449 &PluginParamSnapshot {
1450 enable_callbacks: true,
1451 reason: tpp_reason,
1452 addr: 0,
1453 value: ParamChangeValue::Float64(0.5),
1454 },
1455 );
1456
1457 let n = 8;
1458 let mut arr = NDArray::new(vec![NDDimension::new(n)], NDDataType::Float64);
1459 if let NDDataBuffer::F64(ref mut v) = arr.data {
1460 v[0] = 1.0;
1461 }
1462 let result = proc.process_array(&arr, &pool);
1463 let u = &result.param_updates;
1464
1465 let time_axis = find_array_update(u, time_axis_reason).unwrap();
1466 let freq_axis = find_array_update(u, freq_axis_reason).unwrap();
1467
1468 assert_eq!(time_axis.len(), 8);
1470 assert!((time_axis[1] - 0.5).abs() < 1e-12);
1471 assert!((time_axis[7] - 3.5).abs() < 1e-12);
1472 assert_eq!(freq_axis.len(), 4);
1474 let step = 0.5 / 0.5 / 3.0;
1475 assert!((freq_axis[1] - step).abs() < 1e-12);
1476 assert!((freq_axis[3] - 3.0 * step).abs() < 1e-12);
1477 }
1478
1479 #[test]
1480 fn test_adp25_timeseries_and_timeaxis_use_padded_length() {
1481 let proc = fft_proc_with_params(FFTConfig::default());
1485 let ts_reason = proc.params.time_series.unwrap();
1486 let time_axis_reason = proc.params.time_axis.unwrap();
1487 let real_reason = proc.params.real.unwrap();
1488 let freq_axis_reason = proc.params.freq_axis.unwrap();
1489 let pool = NDArrayPool::new(0);
1490
1491 let n = 5;
1492 let mut arr = NDArray::new(vec![NDDimension::new(n)], NDDataType::Float64);
1493 if let NDDataBuffer::F64(ref mut v) = arr.data {
1494 for (i, x) in v.iter_mut().enumerate() {
1495 *x = (i + 1) as f64; }
1497 }
1498 let result = proc.process_array(&arr, &pool);
1499 let u = &result.param_updates;
1500
1501 let ts = find_array_update(u, ts_reason).unwrap();
1502 let time_axis = find_array_update(u, time_axis_reason).unwrap();
1503 let real = find_array_update(u, real_reason).unwrap();
1504 let freq_axis = find_array_update(u, freq_axis_reason).unwrap();
1505
1506 assert_eq!(ts.len(), 8);
1508 assert_eq!(&ts[..5], &[1.0, 2.0, 3.0, 4.0, 5.0]);
1509 assert_eq!(&ts[5..], &[0.0, 0.0, 0.0]);
1510 assert_eq!(time_axis.len(), 8);
1512 assert_eq!(real.len(), 4);
1514 assert_eq!(freq_axis.len(), 4);
1515 }
1516
1517 #[test]
1518 fn test_fft_inverse_emits_no_spectrum_waveforms() {
1519 let config = FFTConfig {
1521 direction: FFTDirection::Inverse,
1522 suppress_dc: false,
1523 num_average: 0,
1524 };
1525 let proc = fft_proc_with_params(config);
1526 let pool = NDArrayPool::new(0);
1527 let mut arr = NDArray::new(vec![NDDimension::new(8)], NDDataType::Float64);
1528 if let NDDataBuffer::F64(ref mut v) = arr.data {
1529 v[0] = 8.0;
1530 }
1531 let result = proc.process_array(&arr, &pool);
1532 let array_updates = result
1533 .param_updates
1534 .iter()
1535 .filter(|x| matches!(x, ParamUpdate::Float64Array { .. }))
1536 .count();
1537 assert_eq!(
1538 array_updates, 0,
1539 "inverse FFT must not emit spectrum waveforms"
1540 );
1541 }
1542
1543 #[test]
1544 fn test_inverse_fft_preserves_sign() {
1545 let n = 8;
1549 let mut arr = NDArray::new(vec![NDDimension::new(n)], NDDataType::Float64);
1550 if let NDDataBuffer::F64(ref mut v) = arr.data {
1551 v[1] = 4.0;
1554 v[n - 1] = 4.0;
1555 }
1556 let config = FFTConfig {
1557 direction: FFTDirection::Inverse,
1558 suppress_dc: false,
1559 num_average: 0,
1560 };
1561 let proc = FFTProcessor::with_config(config);
1562 let pool = NDArrayPool::new(0);
1563 let result = proc.process_array(&arr, &pool);
1564 if let NDDataBuffer::F64(ref v) = result.output_arrays[0].data {
1565 let has_negative = v.iter().any(|&x| x < -1e-6);
1566 assert!(
1567 has_negative,
1568 "inverse FFT must keep negative samples: {v:?}"
1569 );
1570 } else {
1571 panic!("expected F64 data");
1572 }
1573 }
1574}