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}