Skip to main content

scirs2_fft/
padding.rs

1//! Automatic padding strategies for optimal FFT performance
2//!
3//! This module provides functionality to automatically pad input data
4//! to optimal sizes for FFT computation, improving performance by
5//! ensuring the FFT size has small prime factors.
6
7use crate::{next_fast_len, FFTError, FFTResult};
8use scirs2_core::ndarray::{s, Array1, ArrayBase, ArrayD, Data, Dimension, IxDyn, Slice};
9use scirs2_core::numeric::Complex;
10use scirs2_core::numeric::Zero;
11
12/// Padding mode for FFT operations
13#[derive(Debug, Clone, Copy, PartialEq)]
14pub enum PaddingMode {
15    /// No padding
16    None,
17    /// Zero padding
18    Zero,
19    /// Constant value padding
20    Constant(f64),
21    /// Edge value replication
22    Edge,
23    /// Reflect padding (mirror at edge)
24    Reflect,
25    /// Symmetric padding (mirror with edge duplication)
26    Symmetric,
27    /// Wrap around (circular)
28    Wrap,
29    /// Linear ramp to zero
30    LinearRamp,
31}
32
33/// Auto-padding configuration
34#[derive(Debug, Clone)]
35pub struct AutoPadConfig {
36    /// Padding mode
37    pub mode: PaddingMode,
38    /// Minimum padding length (default: 0)
39    pub min_pad: usize,
40    /// Maximum padding length (default: input length)
41    pub max_pad: Option<usize>,
42    /// Whether to pad to power of 2
43    pub power_of_2: bool,
44    /// Whether to center the data in padded array
45    pub center: bool,
46}
47
48impl Default for AutoPadConfig {
49    fn default() -> Self {
50        Self {
51            mode: PaddingMode::Zero,
52            min_pad: 0,
53            max_pad: None,
54            power_of_2: false,
55            center: false,
56        }
57    }
58}
59
60impl AutoPadConfig {
61    /// Create a new auto-padding configuration
62    pub fn new(mode: PaddingMode) -> Self {
63        Self {
64            mode,
65            ..Default::default()
66        }
67    }
68
69    /// Set minimum padding
70    pub fn with_min_pad(mut self, minpad: usize) -> Self {
71        self.min_pad = minpad;
72        self
73    }
74
75    /// Set maximum padding
76    pub fn with_max_pad(mut self, maxpad: usize) -> Self {
77        self.max_pad = Some(maxpad);
78        self
79    }
80
81    /// Require power of 2 size
82    pub fn with_power_of_2(mut self) -> Self {
83        self.power_of_2 = true;
84        self
85    }
86
87    /// Center the data in padded array
88    pub fn with_center(mut self) -> Self {
89        self.center = true;
90        self
91    }
92}
93
94/// Automatically pad a 1D array for optimal FFT performance
95#[allow(dead_code)]
96pub fn auto_pad_1d<T>(x: &Array1<T>, config: &AutoPadConfig) -> FFTResult<Array1<T>>
97where
98    T: Clone + Zero,
99{
100    let n = x.len();
101
102    // Determine target size
103    let target_size = if config.power_of_2 {
104        // Next power of 2
105        let min_size = n + config.min_pad;
106        let mut size = 1;
107        while size < min_size {
108            size *= 2;
109        }
110        size
111    } else {
112        // Next fast length
113        next_fast_len(n + config.min_pad, false)
114    };
115
116    // Apply maximum padding constraint
117    let padded_size = if let Some(max_pad) = config.max_pad {
118        target_size.min(n + max_pad)
119    } else {
120        target_size
121    };
122
123    // No padding needed
124    if padded_size == n {
125        return Ok(x.clone());
126    }
127
128    // Create padded array
129    let mut padded = Array1::zeros(padded_size);
130
131    // Determine where to place the original data
132    let start_idx = if config.center {
133        (padded_size - n) / 2
134    } else {
135        0
136    };
137
138    // Copy original data
139    padded.slice_mut(s![start_idx..start_idx + n]).assign(x);
140
141    // Apply padding based on mode
142    match config.mode {
143        PaddingMode::None | PaddingMode::Zero => {
144            // Already zero-initialized
145        }
146        PaddingMode::Constant(_value) => {
147            let const_val = T::zero(); // Need to convert f64 to T properly
148            if start_idx > 0 {
149                padded.slice_mut(s![..start_idx]).fill(const_val.clone());
150            }
151            if start_idx + n < padded_size {
152                padded.slice_mut(s![start_idx + n..]).fill(const_val);
153            }
154        }
155        PaddingMode::Edge => {
156            // Replicate edge values
157            if start_idx > 0 {
158                let left_val = x[0].clone();
159                padded.slice_mut(s![..start_idx]).fill(left_val);
160            }
161            if start_idx + n < padded_size {
162                let right_val = x[n - 1].clone();
163                padded.slice_mut(s![start_idx + n..]).fill(right_val);
164            }
165        }
166        PaddingMode::Reflect => {
167            // Mirror at edges
168            for i in 0..start_idx {
169                let offset = start_idx - i - 1;
170                let cycle = 2 * (n - 1);
171                let src_idx = offset % cycle;
172                let src_idx = if src_idx >= n {
173                    cycle - src_idx
174                } else {
175                    src_idx
176                };
177                padded[i] = x[src_idx].clone();
178            }
179            for i in (start_idx + n)..padded_size {
180                let offset = i - (start_idx + n);
181                let cycle = 2 * (n - 1);
182                let src_idx = n - 1 - (offset % cycle);
183                padded[i] = x[src_idx].clone();
184            }
185        }
186        PaddingMode::Symmetric => {
187            // Mirror with edge duplication
188            for i in 0..start_idx {
189                let offset = start_idx - i;
190                let cycle = 2 * n;
191                let src_idx = (offset - 1) % cycle;
192                let src_idx = if src_idx >= n {
193                    cycle - 1 - src_idx
194                } else {
195                    src_idx
196                };
197                padded[i] = x[src_idx].clone();
198            }
199            for i in (start_idx + n)..padded_size {
200                let offset = i - (start_idx + n);
201                let cycle = 2 * n;
202                let src_idx = n - 1 - (offset % cycle);
203                padded[i] = x[src_idx].clone();
204            }
205        }
206        PaddingMode::Wrap => {
207            // Circular padding
208            for i in 0..start_idx {
209                let src_idx = (n - (start_idx - i) % n) % n;
210                padded[i] = x[src_idx].clone();
211            }
212            for i in (start_idx + n)..padded_size {
213                let src_idx = (i - start_idx) % n;
214                padded[i] = x[src_idx].clone();
215            }
216        }
217        PaddingMode::LinearRamp => {
218            // Linear fade to zero
219            if start_idx > 0 {
220                for i in 0..start_idx {
221                    // This would need proper numeric operations for type T
222                    padded[i] = T::zero();
223                }
224            }
225            if start_idx + n < padded_size {
226                for i in (start_idx + n)..padded_size {
227                    // This would need proper numeric operations for type T
228                    padded[i] = T::zero();
229                }
230            }
231        }
232    }
233
234    Ok(padded)
235}
236
237/// Automatically pad a complex array for optimal FFT performance
238#[allow(dead_code)]
239pub fn auto_pad_complex(
240    x: &Array1<Complex<f64>>,
241    config: &AutoPadConfig,
242) -> FFTResult<Array1<Complex<f64>>> {
243    let n = x.len();
244
245    // Determine target size
246    let target_size = if config.power_of_2 {
247        let min_size = n + config.min_pad;
248        let mut size = 1;
249        while size < min_size {
250            size *= 2;
251        }
252        size
253    } else {
254        next_fast_len(n + config.min_pad, false)
255    };
256
257    // Apply maximum padding constraint
258    let padded_size = if let Some(max_pad) = config.max_pad {
259        target_size.min(n + max_pad)
260    } else {
261        target_size
262    };
263
264    if padded_size == n {
265        return Ok(x.clone());
266    }
267
268    let mut padded = Array1::zeros(padded_size);
269    let start_idx = if config.center {
270        (padded_size - n) / 2
271    } else {
272        0
273    };
274
275    padded.slice_mut(s![start_idx..start_idx + n]).assign(x);
276
277    // Apply padding
278    match config.mode {
279        PaddingMode::None | PaddingMode::Zero => {}
280        PaddingMode::Constant(value) => {
281            let const_val = Complex::new(value, 0.0);
282            if start_idx > 0 {
283                padded.slice_mut(s![..start_idx]).fill(const_val);
284            }
285            if start_idx + n < padded_size {
286                padded.slice_mut(s![start_idx + n..]).fill(const_val);
287            }
288        }
289        PaddingMode::Edge => {
290            if start_idx > 0 {
291                let left_val = x[0];
292                padded.slice_mut(s![..start_idx]).fill(left_val);
293            }
294            if start_idx + n < padded_size {
295                let right_val = x[n - 1];
296                padded.slice_mut(s![start_idx + n..]).fill(right_val);
297            }
298        }
299        PaddingMode::LinearRamp => {
300            // Linear fade from edges to zero
301            if start_idx > 0 {
302                let edge_val = x[0];
303                for i in 0..start_idx {
304                    let t = i as f64 / start_idx as f64;
305                    padded[start_idx - 1 - i] = edge_val * t;
306                }
307            }
308            if start_idx + n < padded_size {
309                let edge_val = x[n - 1];
310                let pad_len = padded_size - (start_idx + n);
311                for i in 0..pad_len {
312                    let t = 1.0 - (i as f64 / pad_len as f64);
313                    padded[start_idx + n + i] = edge_val * t;
314                }
315            }
316        }
317        _ => {
318            // For other modes, use simpler implementations or delegate to auto_pad_1d
319            return auto_pad_1d(x, config);
320        }
321    }
322
323    Ok(padded)
324}
325
326/// Remove padding from a 1D array after FFT
327#[allow(dead_code)]
328pub fn remove_padding_1d<T>(
329    padded: &Array1<T>,
330    original_size: usize,
331    config: &AutoPadConfig,
332) -> Array1<T>
333where
334    T: Clone,
335{
336    let padded_size = padded.len();
337
338    if padded_size == original_size {
339        return padded.clone();
340    }
341
342    let start_idx = if config.center {
343        (padded_size - original_size) / 2
344    } else {
345        0
346    };
347
348    padded
349        .slice(s![start_idx..start_idx + original_size])
350        .to_owned()
351}
352
353/// Automatic padding for N-dimensional arrays of any dimensionality.
354///
355/// Every axis listed in `axes` (all axes, by default) is padded up to the
356/// size [`AutoPadConfig`] computes for it, and the newly-added border
357/// region is filled according to `config.mode`. All [`PaddingMode`]
358/// variants are supported, for arbitrary `ndim` (not just 1D/2D).
359///
360/// Border fills are applied one axis at a time, in ascending axis order,
361/// each pass building on the previous axis's already-filled data. This
362/// mirrors NumPy's own `numpy.pad`, which composes independent per-axis 1D
363/// fills the same way for `edge`/`reflect`/`symmetric`/`wrap` -- so corner
364/// values (where two or more padded axes meet) match NumPy for those modes
365/// too, not just the interior border.
366#[allow(dead_code)]
367pub fn auto_pad_nd<S, D>(
368    x: &ArrayBase<S, D>,
369    config: &AutoPadConfig,
370    axes: Option<&[usize]>,
371) -> FFTResult<ArrayD<Complex<f64>>>
372where
373    S: Data<Elem = Complex<f64>>,
374    D: Dimension,
375{
376    let shape = x.shape().to_vec();
377    let ndim = shape.len();
378    let default_axes: Vec<usize> = (0..ndim).collect();
379    let axes: Vec<usize> = axes.map(<[usize]>::to_vec).unwrap_or(default_axes);
380
381    for &axis in &axes {
382        if axis >= ndim {
383            return Err(FFTError::ValueError(format!(
384                "Axis {axis} is out of bounds for array of dimension {ndim}"
385            )));
386        }
387    }
388
389    let mut paddedshape = shape.clone();
390    // Offset of the original data along each axis within the padded array.
391    let mut start_idx = vec![0usize; ndim];
392
393    // Calculate padded sizes (and placement offsets) for the specified axes.
394    for &axis in &axes {
395        let n = shape[axis];
396        let target_size = if config.power_of_2 {
397            let min_size = n + config.min_pad;
398            let mut size = 1;
399            while size < min_size {
400                size *= 2;
401            }
402            size
403        } else {
404            next_fast_len(n + config.min_pad, false)
405        };
406
407        let padded_axis_len = if let Some(max_pad) = config.max_pad {
408            target_size.min(n + max_pad)
409        } else {
410            target_size
411        };
412        paddedshape[axis] = padded_axis_len;
413        start_idx[axis] = if config.center {
414            (padded_axis_len - n) / 2
415        } else {
416            0
417        };
418    }
419
420    // Nothing to do: every requested axis is already at its target size.
421    if paddedshape == shape {
422        return Ok(x.to_owned().into_dyn());
423    }
424
425    let mut padded = ArrayD::<Complex<f64>>::zeros(paddedshape.clone());
426
427    // Place the original data into its (possibly centered) sub-block. This
428    // works for any `ndim` via `slice_each_axis_mut`, unlike the previous
429    // hand-written 1D/2D-only implementation.
430    {
431        let mut core = padded.slice_each_axis_mut(|desc| {
432            let axis = desc.axis.index();
433            let s = start_idx[axis];
434            Slice::from(s..s + shape[axis])
435        });
436        core.assign(&x.view().into_dyn());
437    }
438
439    // Fill the newly-added border region for every padded axis, in
440    // ascending axis order (see doc comment above for why the order
441    // matters for modes that depend on already-padded neighboring data).
442    if !matches!(config.mode, PaddingMode::None | PaddingMode::Zero) {
443        let mut ordered_axes = axes.clone();
444        ordered_axes.sort_unstable();
445        ordered_axes.dedup();
446        for axis in ordered_axes {
447            let n = shape[axis];
448            let total = paddedshape[axis];
449            if n == 0 || total == n {
450                continue;
451            }
452            fill_axis_border(&mut padded, axis, start_idx[axis], n, total, config.mode);
453        }
454    }
455
456    Ok(padded)
457}
458
459/// Enumerate every fixed combination of indices for all axes other than
460/// `axis`, as full `shape.len()`-length index vectors (the slot at `axis`
461/// is left at `0`; callers overwrite it while walking the fiber along that
462/// axis). This is what lets [`fill_axis_border`] sweep *every* row/column/
463/// etc. along an axis instead of just the one at the origin.
464fn other_axis_index_combinations(shape: &[usize], axis: usize) -> Vec<Vec<usize>> {
465    let ndim = shape.len();
466    let mut combos: Vec<Vec<usize>> = vec![vec![0; ndim]];
467    for (dim, &size) in shape.iter().enumerate() {
468        if dim == axis {
469            continue;
470        }
471        let mut expanded = Vec::with_capacity(combos.len() * size.max(1));
472        for combo in &combos {
473            for v in 0..size {
474                let mut next = combo.clone();
475                next[dim] = v;
476                expanded.push(next);
477            }
478        }
479        combos = expanded;
480    }
481    combos
482}
483
484/// Source index within `[0, n)` for NumPy's `reflect` padding mode (mirrors
485/// without repeating the edge sample; period `2*(n-1)`), given a logical
486/// position `p` relative to the start of the original data (`p` in
487/// `[0, n)` is the data itself; `p < 0` or `p >= n` is padding).
488fn reflect_source_index(p: isize, n: usize) -> usize {
489    if n <= 1 {
490        return 0;
491    }
492    let period = 2 * (n as isize - 1);
493    let m = p.rem_euclid(period);
494    if m < n as isize {
495        m as usize
496    } else {
497        (period - m) as usize
498    }
499}
500
501/// Source index within `[0, n)` for NumPy's `symmetric` padding mode
502/// (mirrors *including* the edge sample; period `2*n`).
503fn symmetric_source_index(p: isize, n: usize) -> usize {
504    if n == 0 {
505        return 0;
506    }
507    let period = 2 * n as isize;
508    let m = p.rem_euclid(period);
509    if m < n as isize {
510        m as usize
511    } else {
512        (period - 1 - m) as usize
513    }
514}
515
516/// Source index within `[0, n)` for NumPy's `wrap` (circular) padding mode.
517fn wrap_source_index(p: isize, n: usize) -> usize {
518    if n == 0 {
519        return 0;
520    }
521    p.rem_euclid(n as isize) as usize
522}
523
524/// Fill the border region of `padded` along `axis` (positions before
525/// `start` and from `start + n` to `total`) from the `n` already-placed
526/// elements at `[start, start + n)`, per `mode`. Every fiber along `axis`
527/// (every combination of the *other* axes' indices) is swept
528/// independently, so this handles arrays of any dimensionality.
529fn fill_axis_border(
530    padded: &mut ArrayD<Complex<f64>>,
531    axis: usize,
532    start: usize,
533    n: usize,
534    total: usize,
535    mode: PaddingMode,
536) {
537    let shape = padded.shape().to_vec();
538    let end_value = Complex::new(0.0, 0.0);
539
540    for mut indices in other_axis_index_combinations(&shape, axis) {
541        // Snapshot the current core fiber. On the second (and later) axis
542        // processed by `auto_pad_nd`, this may itself already contain
543        // border values filled in by an earlier axis's pass, which is
544        // exactly what makes corner regions come out right.
545        let mut core = Vec::with_capacity(n);
546        for i in 0..n {
547            indices[axis] = start + i;
548            core.push(padded[IxDyn(&indices)]);
549        }
550
551        match mode {
552            PaddingMode::None | PaddingMode::Zero => {}
553            PaddingMode::Constant(value) => {
554                let fill = Complex::new(value, 0.0);
555                for i in 0..start {
556                    indices[axis] = i;
557                    padded[IxDyn(&indices)] = fill;
558                }
559                for i in (start + n)..total {
560                    indices[axis] = i;
561                    padded[IxDyn(&indices)] = fill;
562                }
563            }
564            PaddingMode::Edge => {
565                let left = core[0];
566                let right = core[n - 1];
567                for i in 0..start {
568                    indices[axis] = i;
569                    padded[IxDyn(&indices)] = left;
570                }
571                for i in (start + n)..total {
572                    indices[axis] = i;
573                    padded[IxDyn(&indices)] = right;
574                }
575            }
576            PaddingMode::Reflect => {
577                for i in 0..start {
578                    let src = reflect_source_index(i as isize - start as isize, n);
579                    indices[axis] = i;
580                    padded[IxDyn(&indices)] = core[src];
581                }
582                for i in (start + n)..total {
583                    let src = reflect_source_index(i as isize - start as isize, n);
584                    indices[axis] = i;
585                    padded[IxDyn(&indices)] = core[src];
586                }
587            }
588            PaddingMode::Symmetric => {
589                for i in 0..start {
590                    let src = symmetric_source_index(i as isize - start as isize, n);
591                    indices[axis] = i;
592                    padded[IxDyn(&indices)] = core[src];
593                }
594                for i in (start + n)..total {
595                    let src = symmetric_source_index(i as isize - start as isize, n);
596                    indices[axis] = i;
597                    padded[IxDyn(&indices)] = core[src];
598                }
599            }
600            PaddingMode::Wrap => {
601                for i in 0..start {
602                    let src = wrap_source_index(i as isize - start as isize, n);
603                    indices[axis] = i;
604                    padded[IxDyn(&indices)] = core[src];
605                }
606                for i in (start + n)..total {
607                    let src = wrap_source_index(i as isize - start as isize, n);
608                    indices[axis] = i;
609                    padded[IxDyn(&indices)] = core[src];
610                }
611            }
612            PaddingMode::LinearRamp => {
613                // Linearly ramp from the edge sample down to `end_value`
614                // (0) at the outermost padded position, independently on
615                // each side (matching NumPy's `linear_ramp` with its
616                // default `end_values=0`).
617                let left_edge = core[0];
618                if start > 0 {
619                    let w = start as f64;
620                    for i in 0..start {
621                        let k = (start - 1 - i) as f64;
622                        let val = left_edge - (left_edge - end_value) * ((k + 1.0) / w);
623                        indices[axis] = i;
624                        padded[IxDyn(&indices)] = val;
625                    }
626                }
627                let right_edge = core[n - 1];
628                let right_len = total - (start + n);
629                if right_len > 0 {
630                    let w = right_len as f64;
631                    for i in (start + n)..total {
632                        let k = (i - (start + n)) as f64;
633                        let val = right_edge - (right_edge - end_value) * ((k + 1.0) / w);
634                        indices[axis] = i;
635                        padded[IxDyn(&indices)] = val;
636                    }
637                }
638            }
639        }
640    }
641}
642
643#[cfg(test)]
644mod tests {
645    use super::*;
646    use approx::assert_abs_diff_eq;
647
648    #[test]
649    fn test_auto_pad_zero() {
650        let x = Array1::from_vec(vec![1.0, 2.0, 3.0, 4.0]);
651        let config = AutoPadConfig::new(PaddingMode::Zero);
652
653        let padded =
654            auto_pad_complex(&x.mapv(|v| Complex::new(v, 0.0)), &config).expect("Operation failed");
655
656        // Should pad to next fast length
657        assert!(padded.len() >= x.len());
658
659        // Original values should be preserved
660        for i in 0..x.len() {
661            assert_abs_diff_eq!(padded[i].re, x[i], epsilon = 1e-10);
662        }
663    }
664
665    #[test]
666    fn test_auto_pad_power_of_2() {
667        let x = Array1::from_vec(vec![1.0; 5]);
668        let config = AutoPadConfig::new(PaddingMode::Zero).with_power_of_2();
669
670        let padded =
671            auto_pad_complex(&x.mapv(|v| Complex::new(v, 0.0)), &config).expect("Operation failed");
672
673        // Should pad to 8 (next power of 2)
674        assert_eq!(padded.len(), 8);
675    }
676
677    #[test]
678    fn test_remove_padding() {
679        let padded = Array1::from_vec(vec![0.0, 1.0, 2.0, 3.0, 0.0, 0.0]);
680        let config = AutoPadConfig::new(PaddingMode::Zero);
681
682        let unpadded = remove_padding_1d(&padded, 4, &config);
683        assert_eq!(unpadded.len(), 4);
684        assert_eq!(
685            unpadded.as_slice().expect("Operation failed"),
686            &[0.0, 1.0, 2.0, 3.0]
687        );
688    }
689
690    #[test]
691    fn test_auto_pad_center() {
692        let x = Array1::from_vec(vec![1.0, 2.0, 3.0]);
693        let config = AutoPadConfig::new(PaddingMode::Zero)
694            .with_center()
695            .with_min_pad(3);
696
697        let padded =
698            auto_pad_complex(&x.mapv(|v| Complex::new(v, 0.0)), &config).expect("Operation failed");
699
700        // Should center the data
701        assert!(padded.len() >= 6);
702        let start = (padded.len() - 3) / 2;
703        assert_abs_diff_eq!(padded[start].re, 1.0, epsilon = 1e-10);
704        assert_abs_diff_eq!(padded[start + 1].re, 2.0, epsilon = 1e-10);
705        assert_abs_diff_eq!(padded[start + 2].re, 3.0, epsilon = 1e-10);
706    }
707
708    fn complex_vec(vals: &[f64]) -> Array1<Complex<f64>> {
709        Array1::from_vec(vals.iter().map(|&v| Complex::new(v, 0.0)).collect())
710    }
711
712    fn assert_real_close(actual: &ArrayD<Complex<f64>>, expected: &[f64], eps: f64) {
713        assert_eq!(actual.len(), expected.len());
714        for (a, e) in actual.iter().zip(expected.iter()) {
715            assert_abs_diff_eq!(a.re, e, epsilon = eps);
716            assert_abs_diff_eq!(a.im, 0.0, epsilon = eps);
717        }
718    }
719
720    // Reference values throughout this section were computed with
721    // `numpy.pad` for the exact input and pad widths described in each
722    // test (non-constant data throughout, so a fabricated/constant stub
723    // could not pass).
724
725    #[test]
726    fn test_auto_pad_nd_1d_noncentered_all_modes() {
727        // x has length 5; `power_of_2` rounds up to 8, so all 3 padding
728        // elements land on the right (non-centered). Reference: `np.pad(x,
729        // (0, 3), mode=...)`.
730        let x = complex_vec(&[1.0, 2.0, 3.0, 4.0, 5.0]);
731        let cases: [(PaddingMode, [f64; 8]); 6] = [
732            (
733                PaddingMode::Constant(9.0),
734                [1.0, 2.0, 3.0, 4.0, 5.0, 9.0, 9.0, 9.0],
735            ),
736            (PaddingMode::Edge, [1.0, 2.0, 3.0, 4.0, 5.0, 5.0, 5.0, 5.0]),
737            (
738                PaddingMode::Reflect,
739                [1.0, 2.0, 3.0, 4.0, 5.0, 4.0, 3.0, 2.0],
740            ),
741            (
742                PaddingMode::Symmetric,
743                [1.0, 2.0, 3.0, 4.0, 5.0, 5.0, 4.0, 3.0],
744            ),
745            (PaddingMode::Wrap, [1.0, 2.0, 3.0, 4.0, 5.0, 1.0, 2.0, 3.0]),
746            (
747                PaddingMode::LinearRamp,
748                [
749                    1.0,
750                    2.0,
751                    3.0,
752                    4.0,
753                    5.0,
754                    3.333_333_333_333_333_5,
755                    1.666_666_666_666_666_7,
756                    0.0,
757                ],
758            ),
759        ];
760        for (mode, expected) in cases {
761            let config = AutoPadConfig::new(mode).with_power_of_2();
762            let result = auto_pad_nd(&x, &config, None).expect("auto_pad_nd failed");
763            assert_eq!(result.shape(), &[8]);
764            assert_real_close(&result, &expected, 1e-9);
765        }
766    }
767
768    #[test]
769    fn test_auto_pad_nd_1d_centered_all_modes() {
770        // x has length 3; `power_of_2` with `min_pad(5)` forces a minimum
771        // size of 8, split as 2 left / 3 right when centered. Reference:
772        // `np.pad(x, (2, 3), mode=...)`.
773        let x = complex_vec(&[1.0, 2.0, 3.0]);
774        let cases: [(PaddingMode, [f64; 8]); 6] = [
775            (PaddingMode::Edge, [1.0, 1.0, 1.0, 2.0, 3.0, 3.0, 3.0, 3.0]),
776            (
777                PaddingMode::Reflect,
778                [3.0, 2.0, 1.0, 2.0, 3.0, 2.0, 1.0, 2.0],
779            ),
780            (
781                PaddingMode::Symmetric,
782                [2.0, 1.0, 1.0, 2.0, 3.0, 3.0, 2.0, 1.0],
783            ),
784            (PaddingMode::Wrap, [2.0, 3.0, 1.0, 2.0, 3.0, 1.0, 2.0, 3.0]),
785            (
786                PaddingMode::LinearRamp,
787                [0.0, 0.5, 1.0, 2.0, 3.0, 2.0, 1.0, 0.0],
788            ),
789            (
790                PaddingMode::Constant(-4.0),
791                [-4.0, -4.0, 1.0, 2.0, 3.0, -4.0, -4.0, -4.0],
792            ),
793        ];
794        for (mode, expected) in cases {
795            let config = AutoPadConfig::new(mode)
796                .with_power_of_2()
797                .with_min_pad(5)
798                .with_center();
799            let result = auto_pad_nd(&x, &config, None).expect("auto_pad_nd failed");
800            assert_eq!(result.shape(), &[8]);
801            assert_real_close(&result, &expected, 1e-9);
802        }
803    }
804
805    #[test]
806    fn test_auto_pad_nd_invalid_axis_is_an_error() {
807        let x = complex_vec(&[1.0, 2.0, 3.0]);
808        let config = AutoPadConfig::new(PaddingMode::Zero);
809        let err = auto_pad_nd(&x, &config, Some(&[5])).unwrap_err();
810        assert!(matches!(err, FFTError::ValueError(_)));
811    }
812
813    /// Build a real 3x5 `Complex<f64>` array from a row-major flat `Vec`.
814    fn complex_2d(
815        rows: usize,
816        cols: usize,
817        vals: &[f64],
818    ) -> scirs2_core::ndarray::Array2<Complex<f64>> {
819        scirs2_core::ndarray::Array2::from_shape_vec(
820            (rows, cols),
821            vals.iter().map(|&v| Complex::new(v, 0.0)).collect(),
822        )
823        .expect("valid shape")
824    }
825
826    #[test]
827    fn test_auto_pad_nd_2d_corners_match_numpy() {
828        // shape (3,5); `power_of_2` rounds axis0 3->4 (pad 1) and axis1
829        // 5->8 (pad 3), non-centered. This exercises the sequential
830        // per-axis composition (the corner region depends on axis-0's
831        // padding already being present when axis-1 is padded), which a
832        // naive "pad each axis independently from the original data only"
833        // implementation would get wrong. Reference: `np.pad(x, ((0,1),
834        // (0,3)), mode=...)`.
835        #[rustfmt::skip]
836        let input = [
837            1.0, 2.0, 3.0, 4.0, 5.0,
838            6.0, 7.0, 8.0, 9.0, 10.0,
839            11.0, 12.0, 13.0, 14.0, 15.0,
840        ];
841        let x = complex_2d(3, 5, &input);
842
843        let cases: [(PaddingMode, [f64; 32]); 6] = [
844            (
845                PaddingMode::Edge,
846                [
847                    1.0, 2.0, 3.0, 4.0, 5.0, 5.0, 5.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 10.0, 10.0,
848                    10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 15.0, 15.0, 15.0, 11.0, 12.0, 13.0, 14.0,
849                    15.0, 15.0, 15.0, 15.0,
850                ],
851            ),
852            (
853                PaddingMode::Reflect,
854                [
855                    1.0, 2.0, 3.0, 4.0, 5.0, 4.0, 3.0, 2.0, 6.0, 7.0, 8.0, 9.0, 10.0, 9.0, 8.0,
856                    7.0, 11.0, 12.0, 13.0, 14.0, 15.0, 14.0, 13.0, 12.0, 6.0, 7.0, 8.0, 9.0, 10.0,
857                    9.0, 8.0, 7.0,
858                ],
859            ),
860            (
861                PaddingMode::Symmetric,
862                [
863                    1.0, 2.0, 3.0, 4.0, 5.0, 5.0, 4.0, 3.0, 6.0, 7.0, 8.0, 9.0, 10.0, 10.0, 9.0,
864                    8.0, 11.0, 12.0, 13.0, 14.0, 15.0, 15.0, 14.0, 13.0, 11.0, 12.0, 13.0, 14.0,
865                    15.0, 15.0, 14.0, 13.0,
866                ],
867            ),
868            (
869                PaddingMode::Wrap,
870                [
871                    1.0, 2.0, 3.0, 4.0, 5.0, 1.0, 2.0, 3.0, 6.0, 7.0, 8.0, 9.0, 10.0, 6.0, 7.0,
872                    8.0, 11.0, 12.0, 13.0, 14.0, 15.0, 11.0, 12.0, 13.0, 1.0, 2.0, 3.0, 4.0, 5.0,
873                    1.0, 2.0, 3.0,
874                ],
875            ),
876            (
877                PaddingMode::Constant(7.0),
878                [
879                    1.0, 2.0, 3.0, 4.0, 5.0, 7.0, 7.0, 7.0, 6.0, 7.0, 8.0, 9.0, 10.0, 7.0, 7.0,
880                    7.0, 11.0, 12.0, 13.0, 14.0, 15.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0,
881                    7.0, 7.0,
882                ],
883            ),
884            (
885                PaddingMode::LinearRamp,
886                [
887                    1.0,
888                    2.0,
889                    3.0,
890                    4.0,
891                    5.0,
892                    3.333_333_333_333_333_5,
893                    1.666_666_666_666_666_7,
894                    0.0,
895                    6.0,
896                    7.0,
897                    8.0,
898                    9.0,
899                    10.0,
900                    6.666_666_666_666_667,
901                    3.333_333_333_333_333_5,
902                    0.0,
903                    11.0,
904                    12.0,
905                    13.0,
906                    14.0,
907                    15.0,
908                    10.0,
909                    5.0,
910                    0.0,
911                    0.0,
912                    0.0,
913                    0.0,
914                    0.0,
915                    0.0,
916                    0.0,
917                    0.0,
918                    0.0,
919                ],
920            ),
921        ];
922
923        for (mode, expected) in cases {
924            let config = AutoPadConfig::new(mode).with_power_of_2();
925            let result = auto_pad_nd(&x, &config, None).expect("auto_pad_nd failed");
926            assert_eq!(result.shape(), &[4, 8]);
927            assert_real_close(&result, &expected, 1e-9);
928        }
929    }
930
931    #[test]
932    fn test_auto_pad_nd_3d_reflect_matches_numpy() {
933        // shape (2,3,4); `power_of_2` + per-axis `min_pad(1)` (applied via
934        // 3 separate calls is not supported by `AutoPadConfig`, so instead
935        // this uses axis sizes chosen so the *same* config -- power_of_2
936        // with min_pad=1, centered -- produces pad (1,1) on axis 0, (0,1)
937        // on axis 1, and (2,2) on axis 2). Confirms `auto_pad_nd` no
938        // longer rejects 3D input, and that corner blending across all 3
939        // axes matches NumPy. Reference: `np.pad(x, ((1,1),(0,1),(2,2)),
940        // mode='reflect')`.
941        let input: Vec<f64> = (0..24).map(|v| v as f64).collect();
942        let x = ArrayD::from_shape_vec(vec![2, 3, 4], input)
943            .expect("valid shape")
944            .mapv(|v| Complex::new(v, 0.0));
945
946        let config = AutoPadConfig::new(PaddingMode::Reflect)
947            .with_power_of_2()
948            .with_min_pad(1)
949            .with_center();
950        let result = auto_pad_nd(&x, &config, None).expect("auto_pad_nd failed");
951        assert_eq!(result.shape(), &[4, 4, 8]);
952
953        #[rustfmt::skip]
954        let expected = [
955            14.0, 13.0, 12.0, 13.0, 14.0, 15.0, 14.0, 13.0, 18.0, 17.0, 16.0, 17.0, 18.0, 19.0, 18.0, 17.0,
956            22.0, 21.0, 20.0, 21.0, 22.0, 23.0, 22.0, 21.0, 18.0, 17.0, 16.0, 17.0, 18.0, 19.0, 18.0, 17.0, 2.0,
957            1.0, 0.0, 1.0, 2.0, 3.0, 2.0, 1.0, 6.0, 5.0, 4.0, 5.0, 6.0, 7.0, 6.0, 5.0, 10.0, 9.0, 8.0, 9.0,
958            10.0, 11.0, 10.0, 9.0, 6.0, 5.0, 4.0, 5.0, 6.0, 7.0, 6.0, 5.0, 14.0, 13.0, 12.0, 13.0, 14.0, 15.0,
959            14.0, 13.0, 18.0, 17.0, 16.0, 17.0, 18.0, 19.0, 18.0, 17.0, 22.0, 21.0, 20.0, 21.0, 22.0, 23.0,
960            22.0, 21.0, 18.0, 17.0, 16.0, 17.0, 18.0, 19.0, 18.0, 17.0, 2.0, 1.0, 0.0, 1.0, 2.0, 3.0, 2.0, 1.0,
961            6.0, 5.0, 4.0, 5.0, 6.0, 7.0, 6.0, 5.0, 10.0, 9.0, 8.0, 9.0, 10.0, 11.0, 10.0, 9.0, 6.0, 5.0, 4.0,
962            5.0, 6.0, 7.0, 6.0, 5.0,
963        ];
964        assert_real_close(&result, &expected, 1e-9);
965    }
966
967    #[test]
968    fn test_auto_pad_nd_no_padding_needed_returns_input() {
969        // Every axis is already at its `power_of_2` target size, so
970        // `auto_pad_nd` must return the data unchanged (not an all-zero
971        // array of the same shape, which a careless implementation might
972        // produce by always allocating a fresh `ArrayD::zeros` and
973        // forgetting the early-return).
974        let x = complex_vec(&[1.0, 2.0, 3.0, 4.0]);
975        let config = AutoPadConfig::new(PaddingMode::Reflect).with_power_of_2();
976        let result = auto_pad_nd(&x, &config, None).expect("auto_pad_nd failed");
977        assert_eq!(result.shape(), &[4]);
978        assert_real_close(&result, &[1.0, 2.0, 3.0, 4.0], 1e-9);
979    }
980}