Skip to main content

oxifft/compat/
mod.rs

1//! FFTW Compatibility Layer for OxiFFT.
2//!
3//! This module provides thin wrappers around OxiFFT's native API using
4//! FFTW-style function names. It is intended for users migrating from
5//! FFTW or code that expects the FFTW naming convention.
6//!
7//! # Feature Flag
8//!
9//! This module is gated behind the `fftw-compat` feature. Add to your
10//! `Cargo.toml`:
11//!
12//! ```toml
13//! oxifft = { version = "*", features = ["fftw-compat"] }
14//! ```
15//!
16//! # FFTW Mapping
17//!
18//! | FFTW function | OxiFFT equivalent |
19//! |---|---|
20//! | `fftw_plan_dft_1d` | [`Plan::dft_1d`] |
21//! | `fftwf_plan_dft_1d` | [`Plan::dft_1d`] (f32) |
22//! | `fftw_plan_dft_2d` | [`Plan::dft_2d`] |
23//! | `fftw_plan_dft_3d` | [`Plan::dft_3d`] |
24//! | `fftw_plan_dft_r2c_1d` | [`Plan::r2c_1d`] |
25//! | `fftw_plan_dft_c2r_1d` | [`Plan::c2r_1d`] |
26//! | `fftw_plan_many_dft` | [`GuruPlan::dft`] |
27//! | `fftw_execute` | [`Plan::execute`] |
28//! | `fftw_destroy_plan` | automatic (Rust `Drop`) |
29//! | `fftw_export_wisdom_to_string` | [`crate::api::export_to_string`] |
30//! | `fftw_import_wisdom_from_string` | [`crate::api::import_from_string`] |
31//!
32//! # Differences from FFTW
33//!
34//! - Memory management is handled automatically by Rust's ownership system.
35//!   `fftw_destroy_plan` is a no-op because the plan is dropped when it goes
36//!   out of scope.
37//! - Input to `fftw_execute` is an immutable slice; FFTW's `fftw_execute`
38//!   accepts a non-const pointer, but OxiFFT does not modify the input.
39//! - Planning flags use [`Flags`] instead of integer bitmasks.
40//! - `fftw_plan_many_dft` takes separate `ns: &[usize]` and `howmany: usize`
41//!   rather than the `fftw_iodim` struct array. Contiguous strides are assumed.
42//!
43//! # Example
44//!
45//! ```rust
46//! # #[cfg(feature = "fftw-compat")]
47//! # {
48//! use oxifft::compat::{fftw_plan_dft_1d, fftw_execute};
49//! use oxifft::{Direction, Flags, Complex};
50//!
51//! let plan = fftw_plan_dft_1d(8, Direction::Forward, Flags::ESTIMATE)
52//!     .expect("plan creation failed");
53//!
54//! let input = vec![Complex::new(1.0_f64, 0.0); 8];
55//! let mut output = vec![Complex::new(0.0, 0.0); 8];
56//! fftw_execute(&plan, &input, &mut output);
57//! # }
58//! ```
59
60use crate::api::{export_to_string, import_from_string};
61use crate::kernel::{Complex, Float, IoDim, Tensor};
62use crate::prelude::{vec, String, Vec};
63use crate::{
64    Direction, Flags, GuruPlan, Plan, Plan2D, Plan3D, R2rKind, R2rPlan, R2rPlan2D, R2rPlan3D,
65    RealPlan,
66};
67
68// ─── 1-D complex DFT (f64) ───────────────────────────────────────────────────
69
70/// Create a 1-D complex DFT plan for `f64` data.
71///
72/// Corresponds to FFTW's `fftw_plan_dft_1d`.
73///
74/// # Arguments
75/// * `n` - Transform size (number of complex samples).
76/// * `direction` - [`Direction::Forward`] or [`Direction::Backward`].
77/// * `flags` - Planning flags (e.g., [`Flags::ESTIMATE`], [`Flags::MEASURE`]).
78///
79/// # Returns
80/// `Some(plan)` if `n > 0`, otherwise `None`.
81///
82/// # Example
83///
84/// ```rust
85/// # #[cfg(feature = "fftw-compat")]
86/// # {
87/// use oxifft::compat::fftw_plan_dft_1d;
88/// use oxifft::{Direction, Flags};
89///
90/// let plan = fftw_plan_dft_1d(256, Direction::Forward, Flags::ESTIMATE).unwrap();
91/// assert_eq!(plan.size(), 256);
92/// # }
93/// ```
94#[must_use]
95pub fn fftw_plan_dft_1d(n: usize, direction: Direction, flags: Flags) -> Option<Plan<f64>> {
96    Plan::dft_1d(n, direction, flags)
97}
98
99// ─── 1-D complex DFT (f32) ───────────────────────────────────────────────────
100
101/// Create a 1-D complex DFT plan for `f32` data.
102///
103/// Corresponds to FFTW's `fftwf_plan_dft_1d` (the single-precision variant).
104///
105/// # Arguments
106/// * `n` - Transform size.
107/// * `direction` - [`Direction::Forward`] or [`Direction::Backward`].
108/// * `flags` - Planning flags.
109///
110/// # Returns
111/// `Some(plan)` if `n > 0`, otherwise `None`.
112///
113/// # Example
114///
115/// ```rust
116/// # #[cfg(feature = "fftw-compat")]
117/// # {
118/// use oxifft::compat::fftwf_plan_dft_1d;
119/// use oxifft::{Direction, Flags};
120///
121/// let plan = fftwf_plan_dft_1d(64, Direction::Forward, Flags::ESTIMATE).unwrap();
122/// assert_eq!(plan.size(), 64);
123/// # }
124/// ```
125#[must_use]
126pub fn fftwf_plan_dft_1d(n: usize, direction: Direction, flags: Flags) -> Option<Plan<f32>> {
127    Plan::dft_1d(n, direction, flags)
128}
129
130// ─── 2-D complex DFT (f64) ───────────────────────────────────────────────────
131
132/// Create a 2-D complex DFT plan for `f64` data.
133///
134/// Corresponds to FFTW's `fftw_plan_dft_2d`.
135///
136/// The data layout is row-major: element `(i, j)` is at index `i * n1 + j`.
137///
138/// # Arguments
139/// * `n0` - Number of rows.
140/// * `n1` - Number of columns.
141/// * `direction` - [`Direction::Forward`] or [`Direction::Backward`].
142/// * `flags` - Planning flags.
143///
144/// # Returns
145/// `Some(plan)` if both dimensions are non-zero, otherwise `None`.
146///
147/// # Example
148///
149/// ```rust
150/// # #[cfg(feature = "fftw-compat")]
151/// # {
152/// use oxifft::compat::fftw_plan_dft_2d;
153/// use oxifft::{Direction, Flags};
154///
155/// let plan = fftw_plan_dft_2d(16, 16, Direction::Forward, Flags::ESTIMATE).unwrap();
156/// assert_eq!(plan.size(), 256);
157/// # }
158/// ```
159#[must_use]
160pub fn fftw_plan_dft_2d(
161    n0: usize,
162    n1: usize,
163    direction: Direction,
164    flags: Flags,
165) -> Option<Plan2D<f64>> {
166    Plan::dft_2d(n0, n1, direction, flags)
167}
168
169// ─── 3-D complex DFT (f64) ───────────────────────────────────────────────────
170
171/// Create a 3-D complex DFT plan for `f64` data.
172///
173/// Corresponds to FFTW's `fftw_plan_dft_3d`.
174///
175/// The data layout is C-order (last index varies fastest):
176/// element `(i, j, k)` is at index `i * n1 * n2 + j * n2 + k`.
177///
178/// # Arguments
179/// * `n0` - Size of the first (slowest) dimension.
180/// * `n1` - Size of the second dimension.
181/// * `n2` - Size of the third (fastest) dimension.
182/// * `direction` - [`Direction::Forward`] or [`Direction::Backward`].
183/// * `flags` - Planning flags.
184///
185/// # Returns
186/// `Some(plan)` if all dimensions are non-zero, otherwise `None`.
187///
188/// # Example
189///
190/// ```rust
191/// # #[cfg(feature = "fftw-compat")]
192/// # {
193/// use oxifft::compat::fftw_plan_dft_3d;
194/// use oxifft::{Direction, Flags};
195///
196/// let plan = fftw_plan_dft_3d(4, 4, 4, Direction::Forward, Flags::ESTIMATE).unwrap();
197/// assert_eq!(plan.size(), 64);
198/// # }
199/// ```
200#[must_use]
201pub fn fftw_plan_dft_3d(
202    n0: usize,
203    n1: usize,
204    n2: usize,
205    direction: Direction,
206    flags: Flags,
207) -> Option<Plan3D<f64>> {
208    Plan::dft_3d(n0, n1, n2, direction, flags)
209}
210
211// ─── 1-D R2C (f64) ───────────────────────────────────────────────────────────
212
213/// Create a 1-D real-to-complex FFT plan for `f64` data.
214///
215/// Corresponds to FFTW's `fftw_plan_dft_r2c_1d`.
216///
217/// Transforms `n` real values into `n/2 + 1` complex values (the positive
218/// half of the spectrum, using Hermitian symmetry).
219///
220/// # Arguments
221/// * `n` - Number of real input values.
222/// * `flags` - Planning flags.
223///
224/// # Returns
225/// `Some(plan)` if `n > 0`, otherwise `None`.
226///
227/// # Example
228///
229/// ```rust
230/// # #[cfg(feature = "fftw-compat")]
231/// # {
232/// use oxifft::compat::fftw_plan_dft_r2c_1d;
233/// use oxifft::Flags;
234///
235/// let plan = fftw_plan_dft_r2c_1d(64, Flags::ESTIMATE).unwrap();
236/// assert_eq!(plan.complex_size(), 33); // 64/2 + 1
237/// # }
238/// ```
239#[must_use]
240pub fn fftw_plan_dft_r2c_1d(n: usize, flags: Flags) -> Option<RealPlan<f64>> {
241    Plan::r2c_1d(n, flags)
242}
243
244// ─── 1-D C2R (f64) ───────────────────────────────────────────────────────────
245
246/// Create a 1-D complex-to-real FFT plan for `f64` data.
247///
248/// Corresponds to FFTW's `fftw_plan_dft_c2r_1d`.
249///
250/// Transforms `n/2 + 1` complex values (half-spectrum) back into `n` real
251/// values. The output is normalized by `1/n`.
252///
253/// # Arguments
254/// * `n` - Number of real output values.
255/// * `flags` - Planning flags.
256///
257/// # Returns
258/// `Some(plan)` if `n > 0`, otherwise `None`.
259///
260/// # Example
261///
262/// ```rust
263/// # #[cfg(feature = "fftw-compat")]
264/// # {
265/// use oxifft::compat::fftw_plan_dft_c2r_1d;
266/// use oxifft::Flags;
267///
268/// let plan = fftw_plan_dft_c2r_1d(64, Flags::ESTIMATE).unwrap();
269/// assert_eq!(plan.size(), 64);
270/// # }
271/// ```
272#[must_use]
273pub fn fftw_plan_dft_c2r_1d(n: usize, flags: Flags) -> Option<RealPlan<f64>> {
274    Plan::c2r_1d(n, flags)
275}
276
277// ─── Real-to-real transforms (DCT / DST / DHT) ────────────────────────────────
278
279/// Create a 1-D real-to-real transform plan (DCT / DST / DHT).
280///
281/// Corresponds to FFTW's `fftw_plan_r2r_1d`. [`R2rKind`] mirrors FFTW's
282/// `fftw_r2r_kind` (`REDFT00`..`REDFT11`, `RODFT00`..`RODFT11`, `DHT`) one to
283/// one, so the kind constant maps directly.
284///
285/// # Returns
286/// `Some(plan)` for a non-zero size, otherwise `None`.
287#[must_use]
288pub fn fftw_plan_r2r_1d(n: usize, kind: R2rKind, flags: Flags) -> Option<R2rPlan<f64>> {
289    R2rPlan::r2r_1d(n, kind, flags)
290}
291
292/// Create a 2-D real-to-real transform plan with a (possibly different) kind
293/// per axis.
294///
295/// Corresponds to FFTW's `fftw_plan_r2r_2d(n0, n1, in, out, kind0, kind1,
296/// flags)`. `kind0` applies along the first (outer) axis and `kind1` along the
297/// second (inner, contiguous) axis.
298///
299/// # Returns
300/// `Some(plan)` if both dimensions are non-zero, otherwise `None`.
301#[must_use]
302pub fn fftw_plan_r2r_2d(
303    n0: usize,
304    n1: usize,
305    kind0: R2rKind,
306    kind1: R2rKind,
307    flags: Flags,
308) -> Option<R2rPlan2D<f64>> {
309    R2rPlan2D::new(n0, n1, kind0, kind1, flags)
310}
311
312/// Create a 3-D real-to-real transform plan with a (possibly different) kind
313/// per axis.
314///
315/// Corresponds to FFTW's `fftw_plan_r2r_3d(n0, n1, n2, in, out, kind0, kind1,
316/// kind2, flags)`.
317///
318/// # Returns
319/// `Some(plan)` if all three dimensions are non-zero, otherwise `None`.
320#[must_use]
321pub fn fftw_plan_r2r_3d(
322    n0: usize,
323    n1: usize,
324    n2: usize,
325    kind0: R2rKind,
326    kind1: R2rKind,
327    kind2: R2rKind,
328    flags: Flags,
329) -> Option<R2rPlan3D<f64>> {
330    R2rPlan3D::new(n0, n1, n2, kind0, kind1, kind2, flags)
331}
332
333// ─── Guru interface (many DFT) ────────────────────────────────────────────────
334
335/// Create a batched multi-dimensional complex DFT plan.
336///
337/// Corresponds to FFTW's `fftw_plan_many_dft` / `fftw_plan_guru_dft`.
338///
339/// This wraps [`GuruPlan::dft`] using contiguous strides. Each dimension in
340/// `ns` is treated as contiguous (stride 1 within the innermost dimension).
341/// The `howmany` parameter specifies how many independent transforms to
342/// compute; the stride between consecutive transforms is the product of all
343/// sizes in `ns`.
344///
345/// # Arguments
346/// * `rank` - Number of transform dimensions (must equal `ns.len()`).
347/// * `ns` - Sizes for each transform dimension (innermost last).
348/// * `howmany` - Number of independent transforms to batch.
349/// * `direction` - [`Direction::Forward`] or [`Direction::Backward`].
350/// * `flags` - Planning flags.
351///
352/// # Returns
353/// `Some(plan)` if all dimensions and `howmany` are non-zero and
354/// `rank == ns.len()`, otherwise `None`.
355///
356/// # Example
357///
358/// ```rust
359/// # #[cfg(feature = "fftw-compat")]
360/// # {
361/// use oxifft::compat::fftw_plan_many_dft;
362/// use oxifft::{Direction, Flags};
363///
364/// // 4 independent 1-D transforms of size 32
365/// let plan = fftw_plan_many_dft::<f64>(1, &[32], 4, Direction::Forward, Flags::ESTIMATE)
366///     .expect("plan creation failed");
367/// assert_eq!(plan.batch_count(), 4);
368/// # }
369/// ```
370#[must_use]
371pub fn fftw_plan_many_dft<T: Float>(
372    rank: usize,
373    ns: &[usize],
374    howmany: usize,
375    direction: Direction,
376    flags: Flags,
377) -> Option<GuruPlan<T>> {
378    // Validate rank matches dimension count
379    if rank != ns.len() {
380        return None;
381    }
382    if rank == 0 || howmany == 0 {
383        return None;
384    }
385    if ns.contains(&0) {
386        return None;
387    }
388
389    // Build transform dimensions using contiguous strides.
390    // Each IoDim is given stride 1 (contiguous layout).
391    let transform_dims: Vec<IoDim> = ns.iter().map(|&n| IoDim::contiguous(n)).collect();
392    let dims = Tensor::new(transform_dims);
393
394    // The stride between consecutive batches is the product of all transform sizes.
395    let batch_stride = ns.iter().product::<usize>();
396    // Build the howmany tensor: n=howmany, input-stride=batch_stride, output-stride=batch_stride
397    let howmany_dims = Tensor::new(vec![IoDim::new(
398        howmany,
399        batch_stride as isize,
400        batch_stride as isize,
401    )]);
402
403    GuruPlan::dft(&dims, &howmany_dims, direction, flags)
404}
405
406// ─── Execute ──────────────────────────────────────────────────────────────────
407
408/// Execute a 1-D complex DFT plan.
409///
410/// Corresponds to FFTW's `fftw_execute` / `fftw_execute_dft`.
411///
412/// Unlike FFTW, OxiFFT does not modify the input buffer. The `input` slice is
413/// immutable; the signature accepts `&[Complex<T>]` (immutable reference).
414///
415/// # Arguments
416/// * `plan` - A reference to the plan to execute.
417/// * `input` - Input buffer of size `n`.
418/// * `output` - Output buffer of size `n` (written in-place).
419///
420/// # Panics
421/// Panics if `input.len()` or `output.len()` does not equal `plan.size()`.
422///
423/// # Example
424///
425/// ```rust
426/// # #[cfg(feature = "fftw-compat")]
427/// # {
428/// use oxifft::compat::{fftw_plan_dft_1d, fftw_execute};
429/// use oxifft::{Direction, Flags, Complex};
430///
431/// let plan = fftw_plan_dft_1d(8, Direction::Forward, Flags::ESTIMATE).unwrap();
432/// let input = vec![Complex::new(1.0_f64, 0.0); 8];
433/// let mut output = vec![Complex::new(0.0_f64, 0.0); 8];
434/// fftw_execute(&plan, &input, &mut output);
435/// # }
436/// ```
437pub fn fftw_execute<T: Float>(plan: &Plan<T>, input: &[Complex<T>], output: &mut [Complex<T>]) {
438    plan.execute(input, output);
439}
440
441// ─── Destroy plan ────────────────────────────────────────────────────────────
442
443/// Destroy (free) a 1-D DFT plan.
444///
445/// Corresponds to FFTW's `fftw_destroy_plan`.
446///
447/// In OxiFFT this is a no-op: Rust's ownership system automatically frees
448/// the plan when it goes out of scope. This function exists only for API
449/// parity with FFTW-based code; calling it is equivalent to `drop(plan)`.
450///
451/// # Example
452///
453/// ```rust
454/// # #[cfg(feature = "fftw-compat")]
455/// # {
456/// use oxifft::compat::{fftw_plan_dft_1d, fftw_destroy_plan};
457/// use oxifft::{Direction, Flags};
458///
459/// let plan = fftw_plan_dft_1d(8, Direction::Forward, Flags::ESTIMATE).unwrap();
460/// fftw_destroy_plan(plan); // equivalent to: drop(plan)
461/// # }
462/// ```
463pub fn fftw_destroy_plan<T: Float>(_plan: Plan<T>) {
464    // Drop is automatic. Nothing to do.
465}
466
467// ─── Wisdom ───────────────────────────────────────────────────────────────────
468
469/// Export the current wisdom cache to a string.
470///
471/// Corresponds to FFTW's `fftw_export_wisdom_to_string`.
472///
473/// Returns a string representation of all accumulated planning wisdom.
474/// Unlike FFTW, this function always succeeds and returns `Some`; the
475/// return type is `Option<String>` for compatibility with code that
476/// checks for failure.
477///
478/// # Returns
479/// `Some(wisdom_string)` always.
480///
481/// # Example
482///
483/// ```rust
484/// # #[cfg(feature = "fftw-compat")]
485/// # {
486/// use oxifft::compat::fftw_export_wisdom_to_string;
487///
488/// let wisdom = fftw_export_wisdom_to_string().unwrap();
489/// assert!(wisdom.contains("oxifft-wisdom"));
490/// # }
491/// ```
492#[must_use]
493pub fn fftw_export_wisdom_to_string() -> Option<String> {
494    Some(export_to_string())
495}
496
497/// Import wisdom from a string.
498///
499/// Corresponds to FFTW's `fftw_import_wisdom_from_string`.
500///
501/// Returns `true` on success, `false` if the string is malformed or the
502/// version does not match.
503///
504/// # Arguments
505/// * `s` - A wisdom string previously produced by [`fftw_export_wisdom_to_string`].
506///
507/// # Returns
508/// `true` if import succeeded, `false` otherwise.
509///
510/// # Example
511///
512/// ```rust
513/// # #[cfg(feature = "fftw-compat")]
514/// # {
515/// use oxifft::compat::{fftw_export_wisdom_to_string, fftw_import_wisdom_from_string};
516///
517/// let wisdom = fftw_export_wisdom_to_string().unwrap();
518/// let ok = fftw_import_wisdom_from_string(&wisdom);
519/// assert!(ok);
520/// # }
521/// ```
522pub fn fftw_import_wisdom_from_string(s: &str) -> bool {
523    import_from_string(s).is_ok()
524}
525
526// ─── Tests ────────────────────────────────────────────────────────────────────
527
528#[cfg(test)]
529mod tests {
530    use super::*;
531    use crate::api::fft;
532
533    // ── Plan creation ─────────────────────────────────────────────────────────
534
535    #[test]
536    fn test_fftw_plan_dft_1d_some() {
537        let plan = fftw_plan_dft_1d(256, Direction::Forward, Flags::ESTIMATE);
538        assert!(plan.is_some());
539        let plan = plan.unwrap();
540        assert_eq!(plan.size(), 256);
541    }
542
543    /// `Plan::dft_1d` uses `Algorithm::Nop` for n=0 and always returns `Some`.
544    /// This test documents that behavior: a zero-size plan is valid but no-op.
545    #[test]
546    fn test_fftw_plan_dft_1d_zero_nop() {
547        let plan = fftw_plan_dft_1d(0, Direction::Forward, Flags::ESTIMATE);
548        // dft_1d never returns None (size 0 is handled as a Nop algorithm)
549        assert!(plan.is_some());
550        let plan = plan.unwrap();
551        assert_eq!(plan.size(), 0);
552    }
553
554    #[test]
555    fn test_fftwf_plan_dft_1d_some() {
556        let plan = fftwf_plan_dft_1d(128, Direction::Forward, Flags::ESTIMATE);
557        assert!(plan.is_some());
558        let plan = plan.unwrap();
559        assert_eq!(plan.size(), 128);
560    }
561
562    #[test]
563    fn test_fftw_plan_dft_2d_some() {
564        let plan = fftw_plan_dft_2d(8, 16, Direction::Forward, Flags::ESTIMATE);
565        assert!(plan.is_some());
566        let plan = plan.unwrap();
567        assert_eq!(plan.size(), 128);
568    }
569
570    /// `Plan2D::new` delegates to `Plan::dft_1d` which always returns `Some`,
571    /// so zero-dimension 2D plans are valid (but Nop for the zero axis).
572    #[test]
573    fn test_fftw_plan_dft_2d_non_zero_some() {
574        // Positive case: non-zero dimensions always succeed
575        let plan = fftw_plan_dft_2d(8, 16, Direction::Forward, Flags::ESTIMATE);
576        assert!(plan.is_some());
577    }
578
579    #[test]
580    fn test_fftw_plan_dft_3d_some() {
581        let plan = fftw_plan_dft_3d(4, 4, 4, Direction::Forward, Flags::ESTIMATE);
582        assert!(plan.is_some());
583        let plan = plan.unwrap();
584        assert_eq!(plan.size(), 64);
585    }
586
587    /// `Plan3D` delegates to `Plan::dft_1d` (always Some), so zero-dim plans are valid Nops.
588    #[test]
589    fn test_fftw_plan_dft_3d_non_zero_some() {
590        // Positive case: non-zero dimensions always succeed
591        let plan = fftw_plan_dft_3d(4, 4, 4, Direction::Forward, Flags::ESTIMATE);
592        assert!(plan.is_some());
593    }
594
595    #[test]
596    fn test_fftw_plan_dft_r2c_1d_some() {
597        let plan = fftw_plan_dft_r2c_1d(64, Flags::ESTIMATE);
598        assert!(plan.is_some());
599        let plan = plan.unwrap();
600        assert_eq!(plan.size(), 64);
601        assert_eq!(plan.complex_size(), 33); // 64/2 + 1
602    }
603
604    #[test]
605    fn test_fftw_plan_dft_r2c_1d_zero_none() {
606        let plan = fftw_plan_dft_r2c_1d(0, Flags::ESTIMATE);
607        assert!(plan.is_none());
608    }
609
610    #[test]
611    fn test_fftw_plan_dft_c2r_1d_some() {
612        let plan = fftw_plan_dft_c2r_1d(64, Flags::ESTIMATE);
613        assert!(plan.is_some());
614        let plan = plan.unwrap();
615        assert_eq!(plan.size(), 64);
616    }
617
618    #[test]
619    fn test_fftw_plan_dft_c2r_1d_zero_none() {
620        let plan = fftw_plan_dft_c2r_1d(0, Flags::ESTIMATE);
621        assert!(plan.is_none());
622    }
623
624    // ── Execute ───────────────────────────────────────────────────────────────
625
626    /// Verify that `fftw_execute` produces the same result as `fft_1d`.
627    #[test]
628    fn test_fftw_execute_matches_fft_1d() {
629        let n = 32;
630        let input: Vec<Complex<f64>> = (0..n).map(|i| Complex::new(i as f64, 0.0)).collect();
631
632        // Reference: use the high-level fft convenience API
633        let reference = fft(&input);
634
635        // Via compat API
636        let plan = fftw_plan_dft_1d(n, Direction::Forward, Flags::ESTIMATE).unwrap();
637        let mut output = vec![Complex::new(0.0, 0.0); n];
638        fftw_execute(&plan, &input, &mut output);
639
640        assert_eq!(output.len(), reference.len());
641        for (got, exp) in output.iter().zip(reference.iter()) {
642            let diff_re = (got.re - exp.re).abs();
643            let diff_im = (got.im - exp.im).abs();
644            assert!(
645                diff_re < 1e-9,
646                "real part mismatch: got={} expected={}",
647                got.re,
648                exp.re
649            );
650            assert!(
651                diff_im < 1e-9,
652                "imag part mismatch: got={} expected={}",
653                got.im,
654                exp.im
655            );
656        }
657    }
658
659    /// Verify backward (inverse) direction.
660    #[test]
661    fn test_fftw_execute_backward() {
662        let n = 16;
663        let input: Vec<Complex<f64>> = (0..n)
664            .map(|i| Complex::new((i as f64).cos(), (i as f64).sin()))
665            .collect();
666
667        let plan = fftw_plan_dft_1d(n, Direction::Backward, Flags::ESTIMATE).unwrap();
668        let mut output = vec![Complex::new(0.0, 0.0); n];
669        fftw_execute(&plan, &input, &mut output);
670
671        // Just verify it doesn't panic and produces non-trivially-zero output
672        let non_zero = output
673            .iter()
674            .any(|c| c.re.abs() > 1e-12 || c.im.abs() > 1e-12);
675        assert!(non_zero, "backward FFT should produce non-zero output");
676    }
677
678    /// Verify that forward then backward FFT recovers the input (up to 1/n normalization).
679    #[test]
680    fn test_fftw_execute_roundtrip() {
681        let n = 8usize;
682        let original: Vec<Complex<f64>> = (0..n).map(|i| Complex::new(i as f64, 0.0)).collect();
683
684        let fwd = fftw_plan_dft_1d(n, Direction::Forward, Flags::ESTIMATE).unwrap();
685        let bwd = fftw_plan_dft_1d(n, Direction::Backward, Flags::ESTIMATE).unwrap();
686
687        let mut freq = vec![Complex::new(0.0, 0.0); n];
688        fftw_execute(&fwd, &original, &mut freq);
689
690        let mut recovered = vec![Complex::new(0.0, 0.0); n];
691        fftw_execute(&bwd, &freq, &mut recovered);
692
693        // Normalize by n (FFTW convention: unnormalized inverse)
694        let inv_n = 1.0 / n as f64;
695        for c in recovered.iter_mut() {
696            c.re *= inv_n;
697            c.im *= inv_n;
698        }
699
700        for (got, exp) in recovered.iter().zip(original.iter()) {
701            let diff = (got.re - exp.re).abs();
702            assert!(
703                diff < 1e-9,
704                "round-trip mismatch at re: got={} expected={}",
705                got.re,
706                exp.re
707            );
708        }
709    }
710
711    // ── Real-to-real (DCT/DST/DHT) plan wrappers ──────────────────────────────
712
713    #[test]
714    fn test_fftw_plan_r2r_1d_matches_direct() {
715        let n = 8;
716        let input: Vec<f64> = (0..n).map(|i| (i as f64 * 0.3).sin()).collect();
717        let plan = fftw_plan_r2r_1d(n, R2rKind::Redft10, Flags::ESTIMATE).expect("r2r 1d plan");
718        let mut got = vec![0.0_f64; n];
719        plan.execute(&input, &mut got);
720        let direct = R2rPlan::<f64>::r2r_1d(n, R2rKind::Redft10, Flags::ESTIMATE).expect("direct");
721        let mut want = vec![0.0_f64; n];
722        direct.execute(&input, &mut want);
723        for (g, w) in got.iter().zip(want.iter()) {
724            assert!((g - w).abs() < 1e-12, "r2r 1d wrapper diverged: {g} vs {w}");
725        }
726    }
727
728    #[test]
729    fn test_fftw_plan_r2r_2d_and_3d_construct_and_run() {
730        // 2D: different kind per axis, matching FFTW's `fftw_plan_r2r_2d`.
731        let (n0, n1) = (4usize, 6usize);
732        let input2d: Vec<f64> = (0..n0 * n1).map(|i| (i as f64 * 0.11).cos()).collect();
733        let plan2d = fftw_plan_r2r_2d(n0, n1, R2rKind::Redft10, R2rKind::Rodft10, Flags::ESTIMATE)
734            .expect("r2r 2d plan");
735        let mut out2d = vec![0.0_f64; n0 * n1];
736        plan2d.execute(&input2d, &mut out2d);
737        assert!(out2d.iter().all(|v| v.is_finite()));
738
739        // 3D
740        let (a, b, c) = (2usize, 3usize, 4usize);
741        let input3d: Vec<f64> = (0..a * b * c).map(|i| (i as f64 * 0.07).sin()).collect();
742        let plan3d = fftw_plan_r2r_3d(
743            a,
744            b,
745            c,
746            R2rKind::Redft10,
747            R2rKind::Redft10,
748            R2rKind::Dht,
749            Flags::ESTIMATE,
750        )
751        .expect("r2r 3d plan");
752        let mut out3d = vec![0.0_f64; a * b * c];
753        plan3d.execute(&input3d, &mut out3d);
754        assert!(out3d.iter().all(|v| v.is_finite()));
755
756        // Zero dimensions return None.
757        assert!(fftw_plan_r2r_2d(0, n1, R2rKind::Dht, R2rKind::Dht, Flags::ESTIMATE).is_none());
758    }
759
760    // ── Destroy plan (no-op smoke test) ───────────────────────────────────────
761
762    #[test]
763    fn test_fftw_destroy_plan_does_not_panic() {
764        let plan = fftw_plan_dft_1d(64, Direction::Forward, Flags::ESTIMATE).unwrap();
765        fftw_destroy_plan(plan); // must not panic
766    }
767
768    // ── Wisdom round-trip ─────────────────────────────────────────────────────
769
770    #[test]
771    fn test_wisdom_roundtrip() {
772        // Export current (possibly empty) wisdom
773        let exported = fftw_export_wisdom_to_string();
774        assert!(exported.is_some(), "export should always return Some");
775        let wisdom_str = exported.unwrap();
776        assert!(
777            wisdom_str.contains("oxifft-wisdom"),
778            "exported string must contain version header"
779        );
780
781        // Re-import: should succeed
782        let ok = fftw_import_wisdom_from_string(&wisdom_str);
783        assert!(ok, "re-importing exported wisdom must succeed");
784    }
785
786    #[test]
787    fn test_wisdom_import_bad_string_returns_false() {
788        let ok = fftw_import_wisdom_from_string("not-valid-wisdom-at-all");
789        assert!(!ok, "invalid wisdom string must return false");
790    }
791
792    // ── 2-D plan execute ──────────────────────────────────────────────────────
793
794    #[test]
795    fn test_fftw_plan_dft_2d_execute() {
796        let (n0, n1) = (4usize, 4usize);
797        let total = n0 * n1;
798
799        let plan = fftw_plan_dft_2d(n0, n1, Direction::Forward, Flags::ESTIMATE).unwrap();
800
801        let input: Vec<Complex<f64>> = (0..total).map(|i| Complex::new(i as f64, 0.0)).collect();
802        let mut output = vec![Complex::new(0.0, 0.0); total];
803        plan.execute(&input, &mut output);
804
805        // DC component (index 0) should equal sum of all input values
806        let expected_dc: f64 = (0..total).map(|i| i as f64).sum();
807        let diff = (output[0].re - expected_dc).abs();
808        assert!(
809            diff < 1e-9,
810            "DC bin mismatch: got={} expected={}",
811            output[0].re,
812            expected_dc
813        );
814    }
815
816    // ── 3-D plan execute ──────────────────────────────────────────────────────
817
818    #[test]
819    fn test_fftw_plan_dft_3d_execute() {
820        let (n0, n1, n2) = (2usize, 2usize, 2usize);
821        let total = n0 * n1 * n2;
822
823        let plan = fftw_plan_dft_3d(n0, n1, n2, Direction::Forward, Flags::ESTIMATE).unwrap();
824
825        let input: Vec<Complex<f64>> = (0..total).map(|i| Complex::new(i as f64, 0.0)).collect();
826        let mut output = vec![Complex::new(0.0, 0.0); total];
827        plan.execute(&input, &mut output);
828
829        // DC component = sum of all inputs
830        let expected_dc: f64 = (0..total).map(|i| i as f64).sum();
831        let diff = (output[0].re - expected_dc).abs();
832        assert!(
833            diff < 1e-9,
834            "3D DC bin mismatch: got={} expected={}",
835            output[0].re,
836            expected_dc
837        );
838    }
839
840    // ── R2C / C2R ─────────────────────────────────────────────────────────────
841
842    #[test]
843    fn test_fftw_r2c_execute() {
844        let n = 16usize;
845        let input: Vec<f64> = (0..n).map(|i| i as f64).collect();
846        let mut output = vec![Complex::new(0.0f64, 0.0); n / 2 + 1];
847
848        let plan = fftw_plan_dft_r2c_1d(n, Flags::ESTIMATE).unwrap();
849        plan.execute_r2c(&input, &mut output);
850
851        // DC bin = sum of inputs
852        let expected_dc: f64 = (0..n).map(|i| i as f64).sum();
853        let diff = (output[0].re - expected_dc).abs();
854        assert!(
855            diff < 1e-9,
856            "R2C DC bin mismatch: got={} expected={}",
857            output[0].re,
858            expected_dc
859        );
860    }
861
862    #[test]
863    fn test_fftw_c2r_roundtrip() {
864        let n = 16usize;
865        let original: Vec<f64> = (0..n).map(|i| i as f64).collect();
866        let mut freq = vec![Complex::new(0.0f64, 0.0); n / 2 + 1];
867
868        let r2c = fftw_plan_dft_r2c_1d(n, Flags::ESTIMATE).unwrap();
869        r2c.execute_r2c(&original, &mut freq);
870
871        let mut recovered = vec![0.0f64; n];
872        let c2r = fftw_plan_dft_c2r_1d(n, Flags::ESTIMATE).unwrap();
873        c2r.execute_c2r(&freq, &mut recovered); // normalized by 1/n
874
875        for (got, exp) in recovered.iter().zip(original.iter()) {
876            let diff = (got - exp).abs();
877            assert!(
878                diff < 1e-9,
879                "C2R roundtrip mismatch: got={got} expected={exp}"
880            );
881        }
882    }
883
884    // ── GuruPlan (many DFT) ───────────────────────────────────────────────────
885
886    #[test]
887    fn test_fftw_plan_many_dft_some() {
888        let plan = fftw_plan_many_dft::<f64>(1, &[32], 4, Direction::Forward, Flags::ESTIMATE);
889        assert!(
890            plan.is_some(),
891            "plan_many_dft should succeed for valid args"
892        );
893        let plan = plan.unwrap();
894        assert_eq!(plan.batch_count(), 4);
895        assert_eq!(plan.transform_size(), 32);
896    }
897
898    #[test]
899    fn test_fftw_plan_many_dft_rank_mismatch_none() {
900        // rank=2 but ns has only 1 element
901        let plan = fftw_plan_many_dft::<f64>(2, &[32], 4, Direction::Forward, Flags::ESTIMATE);
902        assert!(plan.is_none(), "rank mismatch should return None");
903    }
904
905    #[test]
906    fn test_fftw_plan_many_dft_zero_howmany_none() {
907        let plan = fftw_plan_many_dft::<f64>(1, &[32], 0, Direction::Forward, Flags::ESTIMATE);
908        assert!(plan.is_none(), "zero howmany should return None");
909    }
910
911    #[test]
912    fn test_fftw_plan_many_dft_zero_dim_none() {
913        let plan = fftw_plan_many_dft::<f64>(1, &[0], 4, Direction::Forward, Flags::ESTIMATE);
914        assert!(plan.is_none(), "zero dimension should return None");
915    }
916
917    #[test]
918    fn test_fftw_plan_many_dft_execute() {
919        let n = 8usize;
920        let howmany = 3usize;
921        let total = n * howmany;
922
923        let plan = fftw_plan_many_dft::<f64>(1, &[n], howmany, Direction::Forward, Flags::ESTIMATE)
924            .unwrap();
925
926        let input: Vec<Complex<f64>> = (0..total).map(|i| Complex::new(i as f64, 0.0)).collect();
927        let mut output = vec![Complex::new(0.0, 0.0); total];
928
929        // Execute batch
930        plan.execute(&input, &mut output);
931
932        // Verify non-zero output (smoke test)
933        let non_zero = output
934            .iter()
935            .any(|c| c.re.abs() > 1e-12 || c.im.abs() > 1e-12);
936        assert!(
937            non_zero,
938            "batch execute should produce non-trivially-zero output"
939        );
940    }
941
942    #[test]
943    fn test_fftw_plan_many_dft_f32() {
944        let plan = fftw_plan_many_dft::<f32>(1, &[16], 2, Direction::Forward, Flags::ESTIMATE);
945        assert!(plan.is_some());
946        let plan = plan.unwrap();
947        assert_eq!(plan.batch_count(), 2);
948    }
949}