1use crate::algorithm_selector::{AlgorithmSelector, CacheInfo, FftAlgorithm, InputCharacteristics};
33use crate::error::{FFTError, FFTResult};
34#[cfg(feature = "oxifft")]
35use crate::oxifft_plan_cache;
36#[cfg(feature = "oxifft")]
37use oxifft::{Complex as OxiComplex, Direction};
38#[cfg(feature = "rustfft-backend")]
39use rustfft::FftPlanner;
40use scirs2_core::numeric::Complex64;
41use scirs2_core::numeric::NumCast;
42use std::fmt::Debug;
43#[cfg(feature = "rustfft-backend")]
44use std::sync::Mutex;
45
46#[derive(Debug, Clone)]
48pub struct LargeFftConfig {
49 pub max_block_size: usize,
51 pub target_memory_bytes: usize,
53 pub use_parallel: bool,
55 pub num_threads: usize,
57 pub cache_line_size: usize,
59 pub l1_cache_size: usize,
61 pub l2_cache_size: usize,
63 pub l3_cache_size: usize,
65 pub use_overlap_save: bool,
67 pub overlap_ratio: f64,
69}
70
71impl Default for LargeFftConfig {
72 fn default() -> Self {
73 let cache_info = CacheInfo::default();
74 Self {
75 max_block_size: 65536, target_memory_bytes: 256 * 1024 * 1024, use_parallel: true,
78 num_threads: num_cpus::get(),
79 cache_line_size: cache_info.cache_line_size,
80 l1_cache_size: cache_info.l1_size,
81 l2_cache_size: cache_info.l2_size,
82 l3_cache_size: cache_info.l3_size,
83 use_overlap_save: false,
84 overlap_ratio: 0.5,
85 }
86 }
87}
88
89#[derive(Debug, Clone, Copy, PartialEq, Eq)]
91pub enum LargeFftMethod {
92 Direct,
94 CacheBlocked,
96 Streaming,
98 OutOfCore,
100}
101
102#[derive(Debug, Clone)]
104pub struct LargeFftStats {
105 pub method: LargeFftMethod,
107 pub num_blocks: usize,
109 pub block_size: usize,
111 pub peak_memory_bytes: usize,
113 pub total_time_ns: u64,
115}
116
117pub struct LargeFft {
119 config: LargeFftConfig,
121 #[cfg(feature = "rustfft-backend")]
123 planner: Mutex<FftPlanner<f64>>,
124 selector: AlgorithmSelector,
126}
127
128impl Default for LargeFft {
129 fn default() -> Self {
130 Self::new(LargeFftConfig::default())
131 }
132}
133
134impl LargeFft {
135 pub fn with_defaults() -> Self {
137 Self::new(LargeFftConfig::default())
138 }
139
140 pub fn new(config: LargeFftConfig) -> Self {
142 Self {
143 config,
144 #[cfg(feature = "rustfft-backend")]
145 planner: Mutex::new(FftPlanner::new()),
146 selector: AlgorithmSelector::new(),
147 }
148 }
149
150 pub fn select_method(&self, size: usize) -> LargeFftMethod {
152 let memory_required = size * 16; if memory_required <= self.config.l2_cache_size {
155 LargeFftMethod::Direct
156 } else if memory_required <= self.config.l3_cache_size {
157 LargeFftMethod::CacheBlocked
158 } else if memory_required <= self.config.target_memory_bytes {
159 LargeFftMethod::Streaming
160 } else {
161 LargeFftMethod::OutOfCore
162 }
163 }
164
165 pub fn compute<T>(&self, input: &[T], forward: bool) -> FFTResult<Vec<Complex64>>
167 where
168 T: NumCast + Copy + Debug + 'static,
169 {
170 let size = input.len();
171 if size == 0 {
172 return Err(FFTError::ValueError("Input cannot be empty".to_string()));
173 }
174
175 let method = self.select_method(size);
176
177 match method {
178 LargeFftMethod::Direct => self.compute_direct(input, forward),
179 LargeFftMethod::CacheBlocked => self.compute_cache_blocked(input, forward),
180 LargeFftMethod::Streaming => self.compute_streaming(input, forward),
181 LargeFftMethod::OutOfCore => self.compute_out_of_core(input, forward),
182 }
183 }
184
185 pub fn compute_complex(&self, input: &[Complex64], forward: bool) -> FFTResult<Vec<Complex64>> {
190 let size = input.len();
191 if size == 0 {
192 return Err(FFTError::ValueError("Input cannot be empty".to_string()));
193 }
194
195 self.compute_direct_complex(input, forward)
196 }
197
198 fn compute_direct_complex(
200 &self,
201 input: &[Complex64],
202 forward: bool,
203 ) -> FFTResult<Vec<Complex64>> {
204 let size = input.len();
205 let data: Vec<Complex64> = input.to_vec();
206
207 #[cfg(feature = "oxifft")]
208 {
209 let input_oxi: Vec<OxiComplex<f64>> =
210 data.iter().map(|c| OxiComplex::new(c.re, c.im)).collect();
211 let mut output: Vec<OxiComplex<f64>> = vec![OxiComplex::zero(); size];
212
213 let direction = if forward {
214 Direction::Forward
215 } else {
216 Direction::Backward
217 };
218 oxifft_plan_cache::execute_c2c(&input_oxi, &mut output, direction)?;
219
220 let mut result: Vec<Complex64> = output
221 .into_iter()
222 .map(|c| Complex64::new(c.re, c.im))
223 .collect();
224
225 if !forward {
226 let scale = 1.0 / size as f64;
227 for val in &mut result {
228 *val *= scale;
229 }
230 }
231
232 Ok(result)
233 }
234
235 #[cfg(not(feature = "oxifft"))]
236 {
237 #[cfg(feature = "rustfft-backend")]
238 {
239 let mut data = data;
240 let plan = {
241 let mut planner = self.planner.lock().map_err(|e| {
242 FFTError::ValueError(format!("Failed to acquire planner lock: {e}"))
243 })?;
244 if forward {
245 planner.plan_fft_forward(size)
246 } else {
247 planner.plan_fft_inverse(size)
248 }
249 };
250 plan.process(&mut data);
251
252 if !forward {
253 let scale = 1.0 / size as f64;
254 for val in &mut data {
255 *val *= scale;
256 }
257 }
258
259 return Ok(data);
260 }
261
262 #[cfg(not(feature = "rustfft-backend"))]
263 {
264 return Err(FFTError::ComputationError(
265 "No FFT backend available. Enable either 'oxifft' or 'rustfft-backend' feature.".to_string()
266 ));
267 }
268 }
269 }
270
271 fn compute_direct<T>(&self, input: &[T], forward: bool) -> FFTResult<Vec<Complex64>>
273 where
274 T: NumCast + Copy + Debug + 'static,
275 {
276 let size = input.len();
277
278 let data: Vec<Complex64> = input
280 .iter()
281 .map(|val| {
282 let real: f64 = NumCast::from(*val).unwrap_or(0.0);
283 Complex64::new(real, 0.0)
284 })
285 .collect();
286
287 #[cfg(feature = "oxifft")]
289 {
290 let input_oxi: Vec<OxiComplex<f64>> =
292 data.iter().map(|c| OxiComplex::new(c.re, c.im)).collect();
293 let mut output: Vec<OxiComplex<f64>> = vec![OxiComplex::zero(); size];
294
295 let direction = if forward {
297 Direction::Forward
298 } else {
299 Direction::Backward
300 };
301 oxifft_plan_cache::execute_c2c(&input_oxi, &mut output, direction)?;
302
303 let mut result: Vec<Complex64> = output
305 .into_iter()
306 .map(|c| Complex64::new(c.re, c.im))
307 .collect();
308
309 if !forward {
311 let scale = 1.0 / size as f64;
312 for val in &mut result {
313 *val *= scale;
314 }
315 }
316
317 Ok(result)
318 }
319
320 #[cfg(not(feature = "oxifft"))]
321 {
322 #[cfg(feature = "rustfft-backend")]
323 {
324 let mut data = data;
325
326 let plan = {
328 let mut planner = self.planner.lock().map_err(|e| {
329 FFTError::ValueError(format!("Failed to acquire planner lock: {e}"))
330 })?;
331 if forward {
332 planner.plan_fft_forward(size)
333 } else {
334 planner.plan_fft_inverse(size)
335 }
336 };
337
338 plan.process(&mut data);
340
341 if !forward {
343 let scale = 1.0 / size as f64;
344 for val in &mut data {
345 *val *= scale;
346 }
347 }
348
349 Ok(data)
350 }
351
352 #[cfg(not(feature = "rustfft-backend"))]
353 {
354 Err(FFTError::ComputationError(
355 "No FFT backend available. Enable either 'oxifft' or 'rustfft-backend' feature.".to_string()
356 ))
357 }
358 }
359 }
360
361 fn compute_cache_blocked<T>(&self, input: &[T], forward: bool) -> FFTResult<Vec<Complex64>>
363 where
364 T: NumCast + Copy + Debug + 'static,
365 {
366 let size = input.len();
367
368 let elements_per_cache = self.config.l2_cache_size / 16; let block_size = find_optimal_block_size(size, elements_per_cache);
371
372 let data: Vec<Complex64> = input
374 .iter()
375 .map(|val| {
376 let real: f64 = NumCast::from(*val).unwrap_or(0.0);
377 Complex64::new(real, 0.0)
378 })
379 .collect();
380
381 #[cfg(feature = "oxifft")]
389 {
390 let input_oxi: Vec<OxiComplex<f64>> =
392 data.iter().map(|c| OxiComplex::new(c.re, c.im)).collect();
393 let mut output: Vec<OxiComplex<f64>> = vec![OxiComplex::zero(); size];
394
395 let direction = if forward {
397 Direction::Forward
398 } else {
399 Direction::Backward
400 };
401 oxifft_plan_cache::execute_c2c(&input_oxi, &mut output, direction)?;
402
403 let mut result: Vec<Complex64> = output
405 .into_iter()
406 .map(|c| Complex64::new(c.re, c.im))
407 .collect();
408
409 if !forward {
411 let scale = 1.0 / size as f64;
412 for val in &mut result {
413 *val *= scale;
414 }
415 }
416
417 let _ = block_size;
419
420 Ok(result)
421 }
422
423 #[cfg(not(feature = "oxifft"))]
424 {
425 #[cfg(feature = "rustfft-backend")]
426 {
427 let mut data = data;
428
429 let plan = {
431 let mut planner = self.planner.lock().map_err(|e| {
432 FFTError::ValueError(format!("Failed to acquire planner lock: {e}"))
433 })?;
434 if forward {
435 planner.plan_fft_forward(size)
436 } else {
437 planner.plan_fft_inverse(size)
438 }
439 };
440
441 let scratch_len = plan.get_inplace_scratch_len();
443 let mut scratch = vec![Complex64::new(0.0, 0.0); scratch_len];
444
445 plan.process_with_scratch(&mut data, &mut scratch);
447
448 if !forward {
450 let scale = 1.0 / size as f64;
451 for val in &mut data {
452 *val *= scale;
453 }
454 }
455
456 let _ = block_size;
458
459 Ok(data)
460 }
461
462 #[cfg(not(feature = "rustfft-backend"))]
463 {
464 Err(FFTError::ComputationError(
465 "No FFT backend available. Enable either 'oxifft' or 'rustfft-backend' feature.".to_string()
466 ))
467 }
468 }
469 }
470
471 fn compute_streaming<T>(&self, input: &[T], forward: bool) -> FFTResult<Vec<Complex64>>
473 where
474 T: NumCast + Copy + Debug + 'static,
475 {
476 let size = input.len();
477
478 let chunk_size = self.config.max_block_size;
487 let mut data: Vec<Complex64> = Vec::with_capacity(size);
488
489 for chunk in input.chunks(chunk_size) {
490 for val in chunk {
491 let real: f64 = NumCast::from(*val).unwrap_or(0.0);
492 data.push(Complex64::new(real, 0.0));
493 }
494 }
495
496 #[cfg(feature = "oxifft")]
498 {
499 let input_oxi: Vec<OxiComplex<f64>> =
501 data.iter().map(|c| OxiComplex::new(c.re, c.im)).collect();
502 let mut output: Vec<OxiComplex<f64>> = vec![OxiComplex::zero(); size];
503
504 let direction = if forward {
506 Direction::Forward
507 } else {
508 Direction::Backward
509 };
510 oxifft_plan_cache::execute_c2c(&input_oxi, &mut output, direction)?;
511
512 let mut result: Vec<Complex64> = output
514 .into_iter()
515 .map(|c| Complex64::new(c.re, c.im))
516 .collect();
517
518 if !forward {
520 let scale = 1.0 / size as f64;
521 for chunk in result.chunks_mut(chunk_size) {
523 for val in chunk {
524 *val *= scale;
525 }
526 }
527 }
528
529 Ok(result)
530 }
531
532 #[cfg(not(feature = "oxifft"))]
533 {
534 #[cfg(feature = "rustfft-backend")]
535 {
536 let plan = {
538 let mut planner = self.planner.lock().map_err(|e| {
539 FFTError::ValueError(format!("Failed to acquire planner lock: {e}"))
540 })?;
541 if forward {
542 planner.plan_fft_forward(size)
543 } else {
544 planner.plan_fft_inverse(size)
545 }
546 };
547
548 let scratch_len = plan.get_inplace_scratch_len();
551
552 let mut scratch = vec![Complex64::new(0.0, 0.0); scratch_len];
554
555 plan.process_with_scratch(&mut data, &mut scratch);
557
558 drop(scratch);
560
561 if !forward {
563 let scale = 1.0 / size as f64;
564 for chunk in data.chunks_mut(chunk_size) {
566 for val in chunk {
567 *val *= scale;
568 }
569 }
570 }
571
572 Ok(data)
573 }
574
575 #[cfg(not(feature = "rustfft-backend"))]
576 {
577 Err(FFTError::ComputationError(
578 "No FFT backend available. Enable either 'oxifft' or 'rustfft-backend' feature.".to_string()
579 ))
580 }
581 }
582 }
583
584 fn compute_out_of_core<T>(&self, input: &[T], forward: bool) -> FFTResult<Vec<Complex64>>
586 where
587 T: NumCast + Copy + Debug + 'static,
588 {
589 eprintln!(
592 "Warning: Input size {} exceeds target memory, using streaming method",
593 input.len()
594 );
595 self.compute_streaming(input, forward)
596 }
597
598 pub fn compute_overlap_save<T>(
600 &self,
601 input: &[T],
602 filter_len: usize,
603 forward: bool,
604 ) -> FFTResult<Vec<Complex64>>
605 where
606 T: NumCast + Copy + Debug + 'static,
607 {
608 let input_len = input.len();
609
610 if filter_len == 0 {
611 return Err(FFTError::ValueError(
612 "Filter length must be positive".to_string(),
613 ));
614 }
615
616 let block_size = (self.config.max_block_size).max(filter_len * 4);
618 let fft_size = block_size.next_power_of_two();
619 let valid_output_per_block = fft_size - filter_len + 1;
620
621 let num_blocks = input_len.div_ceil(valid_output_per_block);
623
624 let output_len = input_len;
626 let mut output = Vec::with_capacity(output_len);
627
628 #[cfg(feature = "oxifft")]
630 {
631 let mut buffer = vec![Complex64::new(0.0, 0.0); fft_size];
633
634 for block_idx in 0..num_blocks {
635 let input_start = if block_idx == 0 {
636 0
637 } else {
638 block_idx * valid_output_per_block - (filter_len - 1)
639 };
640
641 for val in &mut buffer {
643 *val = Complex64::new(0.0, 0.0);
644 }
645
646 for (i, j) in (input_start..)
648 .take(fft_size.min(input_len - input_start))
649 .enumerate()
650 {
651 if j < input_len {
652 let real: f64 = NumCast::from(input[j]).unwrap_or(0.0);
653 buffer[i] = Complex64::new(real, 0.0);
654 }
655 }
656
657 let input_oxi: Vec<OxiComplex<f64>> =
659 buffer.iter().map(|c| OxiComplex::new(c.re, c.im)).collect();
660 let mut output_oxi: Vec<OxiComplex<f64>> = vec![OxiComplex::zero(); fft_size];
661 oxifft_plan_cache::execute_c2c(&input_oxi, &mut output_oxi, Direction::Forward)?;
662
663 for (i, val) in output_oxi.iter().enumerate() {
665 buffer[i] = Complex64::new(val.re, val.im);
666 }
667
668 if !forward {
672 let input_oxi: Vec<OxiComplex<f64>> =
674 buffer.iter().map(|c| OxiComplex::new(c.re, c.im)).collect();
675 let mut output_oxi: Vec<OxiComplex<f64>> = vec![OxiComplex::zero(); fft_size];
676 oxifft_plan_cache::execute_c2c(
677 &input_oxi,
678 &mut output_oxi,
679 Direction::Backward,
680 )?;
681
682 let scale = 1.0 / fft_size as f64;
684 for (i, val) in output_oxi.iter().enumerate() {
685 buffer[i] = Complex64::new(val.re * scale, val.im * scale);
686 }
687 }
688
689 let output_start = if block_idx == 0 { 0 } else { filter_len - 1 };
691 let output_count = valid_output_per_block.min(output_len - output.len());
692
693 for i in output_start..(output_start + output_count) {
694 if i < fft_size {
695 output.push(buffer[i]);
696 }
697 }
698 }
699
700 Ok(output)
701 }
702
703 #[cfg(not(feature = "oxifft"))]
704 {
705 #[cfg(feature = "rustfft-backend")]
706 {
707 let (fft_plan, ifft_plan) = {
709 let mut planner = self.planner.lock().map_err(|e| {
710 FFTError::ValueError(format!("Failed to acquire planner lock: {e}"))
711 })?;
712 (
713 planner.plan_fft_forward(fft_size),
714 planner.plan_fft_inverse(fft_size),
715 )
716 };
717
718 let mut buffer = vec![Complex64::new(0.0, 0.0); fft_size];
720
721 for block_idx in 0..num_blocks {
722 let input_start = if block_idx == 0 {
723 0
724 } else {
725 block_idx * valid_output_per_block - (filter_len - 1)
726 };
727
728 for val in &mut buffer {
730 *val = Complex64::new(0.0, 0.0);
731 }
732
733 for (i, j) in (input_start..)
735 .take(fft_size.min(input_len - input_start))
736 .enumerate()
737 {
738 if j < input_len {
739 let real: f64 = NumCast::from(input[j]).unwrap_or(0.0);
740 buffer[i] = Complex64::new(real, 0.0);
741 }
742 }
743
744 fft_plan.process(&mut buffer);
746
747 if !forward {
751 ifft_plan.process(&mut buffer);
753
754 let scale = 1.0 / fft_size as f64;
756 for val in &mut buffer {
757 *val *= scale;
758 }
759 }
760
761 let output_start = if block_idx == 0 { 0 } else { filter_len - 1 };
763 let output_count = valid_output_per_block.min(output_len - output.len());
764
765 for i in output_start..(output_start + output_count) {
766 if i < fft_size {
767 output.push(buffer[i]);
768 }
769 }
770 }
771
772 Ok(output)
773 }
774
775 #[cfg(not(feature = "rustfft-backend"))]
776 {
777 Err(FFTError::ComputationError(
778 "No FFT backend available. Enable either 'oxifft' or 'rustfft-backend' feature.".to_string()
779 ))
780 }
781 }
782 }
783
784 pub fn config(&self) -> &LargeFftConfig {
786 &self.config
787 }
788
789 pub fn selector(&self) -> &AlgorithmSelector {
791 &self.selector
792 }
793
794 pub fn estimate_memory(&self, size: usize) -> usize {
796 let method = self.select_method(size);
797 let base_memory = size * 16; match method {
800 LargeFftMethod::Direct => base_memory * 2, LargeFftMethod::CacheBlocked => base_memory * 2 + self.config.l2_cache_size,
802 LargeFftMethod::Streaming => base_memory + self.config.max_block_size * 16,
803 LargeFftMethod::OutOfCore => self.config.target_memory_bytes,
804 }
805 }
806}
807
808fn find_optimal_block_size(total_size: usize, cache_elements: usize) -> usize {
810 let mut block = 1;
812 while block * 2 <= cache_elements && block * 2 <= total_size {
813 block *= 2;
814 }
815 block
816}
817
818pub struct LargeFftNd {
820 fft_1d: LargeFft,
822 config: LargeFftConfig,
824}
825
826impl Default for LargeFftNd {
827 fn default() -> Self {
828 Self::new(LargeFftConfig::default())
829 }
830}
831
832impl LargeFftNd {
833 pub fn new(config: LargeFftConfig) -> Self {
835 Self {
836 fft_1d: LargeFft::new(config.clone()),
837 config,
838 }
839 }
840
841 pub fn compute_2d<T>(
843 &self,
844 input: &[T],
845 rows: usize,
846 cols: usize,
847 forward: bool,
848 ) -> FFTResult<Vec<Complex64>>
849 where
850 T: NumCast + Copy + Debug + 'static,
851 {
852 if input.len() != rows * cols {
853 return Err(FFTError::ValueError(format!(
854 "Input size {} doesn't match dimensions {}x{}",
855 input.len(),
856 rows,
857 cols
858 )));
859 }
860
861 let mut data: Vec<Complex64> = input
863 .iter()
864 .map(|val| {
865 let real: f64 = NumCast::from(*val).unwrap_or(0.0);
866 Complex64::new(real, 0.0)
867 })
868 .collect();
869
870 let mut row_buffer = vec![Complex64::new(0.0, 0.0); cols];
872 for r in 0..rows {
873 for c in 0..cols {
875 row_buffer[c] = data[r * cols + c];
876 }
877
878 let row_fft = self.fft_1d.compute_direct(&row_buffer, forward)?;
880
881 for c in 0..cols {
883 data[r * cols + c] = row_fft[c];
884 }
885 }
886
887 let mut col_buffer = vec![Complex64::new(0.0, 0.0); rows];
889 for c in 0..cols {
890 for r in 0..rows {
892 col_buffer[r] = data[r * cols + c];
893 }
894
895 let col_fft = self.fft_1d.compute_direct(&col_buffer, forward)?;
897
898 for r in 0..rows {
900 data[r * cols + c] = col_fft[r];
901 }
902 }
903
904 Ok(data)
905 }
906
907 pub fn compute_nd<T>(
909 &self,
910 input: &[T],
911 shape: &[usize],
912 forward: bool,
913 ) -> FFTResult<Vec<Complex64>>
914 where
915 T: NumCast + Copy + Debug + 'static,
916 {
917 let total_size: usize = shape.iter().product();
918 if input.len() != total_size {
919 return Err(FFTError::ValueError(format!(
920 "Input size {} doesn't match shape {:?} (expected {})",
921 input.len(),
922 shape,
923 total_size
924 )));
925 }
926
927 let mut data: Vec<Complex64> = input
929 .iter()
930 .map(|val| {
931 let real: f64 = NumCast::from(*val).unwrap_or(0.0);
932 Complex64::new(real, 0.0)
933 })
934 .collect();
935
936 for (dim_idx, &dim_size) in shape.iter().enumerate() {
938 let stride = shape[(dim_idx + 1)..].iter().product::<usize>().max(1);
939 let outer_size = shape[..dim_idx].iter().product::<usize>().max(1);
940
941 let mut line_buffer = vec![Complex64::new(0.0, 0.0); dim_size];
942
943 for outer in 0..outer_size {
944 let outer_offset = outer * shape[dim_idx..].iter().product::<usize>().max(1);
945
946 for inner in 0..(total_size / (outer_size * dim_size)) {
947 for i in 0..dim_size {
949 let idx = outer_offset + i * stride + inner;
950 if idx < data.len() {
951 line_buffer[i] = data[idx];
952 }
953 }
954
955 let line_fft = self.fft_1d.compute_direct(&line_buffer, forward)?;
957
958 for i in 0..dim_size {
960 let idx = outer_offset + i * stride + inner;
961 if idx < data.len() {
962 data[idx] = line_fft[i];
963 }
964 }
965 }
966 }
967 }
968
969 Ok(data)
970 }
971}
972
973#[cfg(test)]
974mod tests {
975 use super::*;
976 use approx::assert_relative_eq;
977
978 #[test]
979 fn test_large_fft_direct() {
980 let large_fft = LargeFft::with_defaults();
981 let input: Vec<f64> = vec![1.0, 2.0, 3.0, 4.0];
982
983 let result = large_fft.compute(&input, true).expect("FFT failed");
984
985 assert_relative_eq!(result[0].re, 10.0, epsilon = 1e-10);
987 }
988
989 #[test]
990 fn test_large_fft_roundtrip() {
991 let large_fft = LargeFft::with_defaults();
992 let input: Vec<f64> = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
993
994 let forward = large_fft.compute(&input, true).expect("Forward FFT failed");
995 let inverse = large_fft
996 .compute_complex(&forward, false)
997 .expect("Inverse FFT failed");
998
999 for (i, &orig) in input.iter().enumerate() {
1001 assert_relative_eq!(inverse[i].re, orig, epsilon = 1e-10);
1002 assert_relative_eq!(inverse[i].im, 0.0, epsilon = 1e-10);
1003 }
1004 }
1005
1006 #[test]
1007 fn test_method_selection() {
1008 let config = LargeFftConfig {
1009 l2_cache_size: 256 * 1024, l3_cache_size: 8 * 1024 * 1024, target_memory_bytes: 256 * 1024 * 1024, ..Default::default()
1013 };
1014 let large_fft = LargeFft::new(config);
1015
1016 let method = large_fft.select_method(1024);
1018 assert_eq!(method, LargeFftMethod::Direct);
1019
1020 let method = large_fft.select_method(100_000);
1022 assert_eq!(method, LargeFftMethod::CacheBlocked);
1023
1024 let method = large_fft.select_method(1_000_000);
1026 assert_eq!(method, LargeFftMethod::Streaming);
1027 }
1028
1029 #[test]
1030 fn test_large_fft_2d() {
1031 let large_fft_nd = LargeFftNd::default();
1032 let input: Vec<f64> = vec![1.0, 2.0, 3.0, 4.0];
1033
1034 let result = large_fft_nd
1035 .compute_2d(&input, 2, 2, true)
1036 .expect("2D FFT failed");
1037
1038 assert_relative_eq!(result[0].re, 10.0, epsilon = 1e-10);
1040 }
1041
1042 #[test]
1043 fn test_memory_estimation() {
1044 let large_fft = LargeFft::with_defaults();
1045
1046 let small_mem = large_fft.estimate_memory(1024);
1047 let large_mem = large_fft.estimate_memory(1_000_000);
1048
1049 assert!(large_mem > small_mem);
1050 }
1051
1052 #[test]
1053 fn test_find_optimal_block_size() {
1054 let block = find_optimal_block_size(65536, 16384);
1055 assert!(block.is_power_of_two());
1056 assert!(block <= 16384);
1057 assert!(block <= 65536);
1058 }
1059}